【OpenGL】LearnOpenGL学习笔记26 - 视差贴图 Parallax Map

上接:https://blog.csdn.net/weixin_44506615/article/details/151898818?spm=1001.2014.3001.5501

完整代码:https://gitee.com/Duo1J/learn-open-gl | https://github.com/Duo1J/LearnOpenGL

视差贴图 (Parallax Map)

视差贴图类似法线贴图,它利用了视错觉,能够极大地提升表面细节,使之具有深度感

视差贴图背后的思想是通过修改 纹理坐标(UV) 来使一个片段的表面看起来比实际更高或是更低,如下图所示 (图片来自于LearnOpenGL)

如果不进行干涉,观察者的视线就会落到点A上,我们的目的就是让A上的片段不再使用点A的纹理坐标,而是使用点B的纹理坐标进行采样,这样观察者的视线就仿佛落在了点B

首先准备一下要用到的纹理图片,我们绘制两种平面来看效果,砖墙和玩具盒,相关资源可在顶部Git仓库中找到

首先绘制两个平面,并应用法线贴图
Main.cpp

cpp 复制代码
// ...
// 渲染视差Quad
void RenderParallax()
{
	static unsigned int parallaxVAO = 0;
	static unsigned int parallaxVBO;

	if (parallaxVAO == 0)
	{
		// positions
		glm::vec3 pos1(-1.0, 1.0, 0.0);
		glm::vec3 pos2(-1.0, -1.0, 0.0);
		glm::vec3 pos3(1.0, -1.0, 0.0);
		glm::vec3 pos4(1.0, 1.0, 0.0);
		// texture coordinates
		glm::vec2 uv1(0.0, 1.0);
		glm::vec2 uv2(0.0, 0.0);
		glm::vec2 uv3(1.0, 0.0);
		glm::vec2 uv4(1.0, 1.0);
		// normal
		glm::vec3 nm(0.0, 0.0, 1.0);

		glm::vec3 tangent1, bitangent1;
		glm::vec3 tangent2, bitangent2;

		// - triangle 1
		glm::vec3 edge1 = pos2 - pos1;
		glm::vec3 edge2 = pos3 - pos1;
		glm::vec2 deltaUV1 = uv2 - uv1;
		glm::vec2 deltaUV2 = uv3 - uv1;

		GLfloat f = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y);

		tangent1.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x);
		tangent1.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y);
		tangent1.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);
		tangent1 = glm::normalize(tangent1);

		bitangent1.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x);
		bitangent1.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y);
		bitangent1.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z);
		bitangent1 = glm::normalize(bitangent1);

		// - triangle 2
		edge1 = pos3 - pos1;
		edge2 = pos4 - pos1;
		deltaUV1 = uv3 - uv1;
		deltaUV2 = uv4 - uv1;

		f = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y);

		tangent2.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x);
		tangent2.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y);
		tangent2.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);
		tangent2 = glm::normalize(tangent2);


		bitangent2.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x);
		bitangent2.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y);
		bitangent2.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z);
		bitangent2 = glm::normalize(bitangent2);


		GLfloat quadVertices[] = {
			// Positions            // normal         // TexCoords  // Tangent                          // Bitangent
			pos1.x, pos1.y, pos1.z, nm.x, nm.y, nm.z, uv1.x, uv1.y, tangent1.x, tangent1.y, tangent1.z, bitangent1.x, bitangent1.y, bitangent1.z,
			pos2.x, pos2.y, pos2.z, nm.x, nm.y, nm.z, uv2.x, uv2.y, tangent1.x, tangent1.y, tangent1.z, bitangent1.x, bitangent1.y, bitangent1.z,
			pos3.x, pos3.y, pos3.z, nm.x, nm.y, nm.z, uv3.x, uv3.y, tangent1.x, tangent1.y, tangent1.z, bitangent1.x, bitangent1.y, bitangent1.z,

			pos1.x, pos1.y, pos1.z, nm.x, nm.y, nm.z, uv1.x, uv1.y, tangent2.x, tangent2.y, tangent2.z, bitangent2.x, bitangent2.y, bitangent2.z,
			pos3.x, pos3.y, pos3.z, nm.x, nm.y, nm.z, uv3.x, uv3.y, tangent2.x, tangent2.y, tangent2.z, bitangent2.x, bitangent2.y, bitangent2.z,
			pos4.x, pos4.y, pos4.z, nm.x, nm.y, nm.z, uv4.x, uv4.y, tangent2.x, tangent2.y, tangent2.z, bitangent2.x, bitangent2.y, bitangent2.z
		};

		glGenVertexArrays(1, &parallaxVAO);
		glGenBuffers(1, &parallaxVBO);
		glBindVertexArray(parallaxVAO);
		glBindBuffer(GL_ARRAY_BUFFER, parallaxVBO);
		glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), &quadVertices, GL_STATIC_DRAW);
		glEnableVertexAttribArray(0);
		glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 14 * sizeof(GLfloat), (GLvoid*)0);
		glEnableVertexAttribArray(1);
		glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 14 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
		glEnableVertexAttribArray(2);
		glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 14 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat)));
		glEnableVertexAttribArray(3);
		glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, 14 * sizeof(GLfloat), (GLvoid*)(8 * sizeof(GLfloat)));
		glEnableVertexAttribArray(4);
		glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, 14 * sizeof(GLfloat), (GLvoid*)(11 * sizeof(GLfloat)));
	}

	glBindVertexArray(parallaxVAO);
	glDrawArrays(GL_TRIANGLES, 0, 6);
	glBindVertexArray(0);
}

int main()
{
	// ...
	Shader parallarShader("Shader/ParallaxVertex.glsl", "Shader/ParallaxFragment.glsl");
	// ...
	Texture bricksDiffuseTex("F:/Scripts/Cpp/LearnOpenGL/learn-open-gl/Resource/parallax/T_Bricks_Diffuse.jpg", TextureType::DIFFUSE, GL_CLAMP_TO_EDGE);
	Texture bricksNormalTex("F:/Scripts/Cpp/LearnOpenGL/learn-open-gl/Resource/parallax/T_Bricks_Normal.jpg", TextureType::NORMAL, GL_CLAMP_TO_EDGE);
	Texture bricksParallaxTex("F:/Scripts/Cpp/LearnOpenGL/learn-open-gl/Resource/parallax/T_Bricks_Parallax.jpg", TextureType::DIFFUSE, GL_CLAMP_TO_EDGE);
	Texture toyboxDiffuseTex("F:/Scripts/Cpp/LearnOpenGL/learn-open-gl/Resource/parallax/T_Toybox_Diffuse.png", TextureType::DIFFUSE, GL_CLAMP_TO_EDGE);
	Texture toyboxNormalTex("F:/Scripts/Cpp/LearnOpenGL/learn-open-gl/Resource/parallax/T_Toybox_Normal.png", TextureType::NORMAL, GL_CLAMP_TO_EDGE);
	Texture toyboxParallaxTex("F:/Scripts/Cpp/LearnOpenGL/learn-open-gl/Resource/parallax/T_Toybox_Parallax.png", TextureType::DIFFUSE, GL_CLAMP_TO_EDGE);

	// [主循环]
	// 绘制视差Quad
	parallarShader.Use();
	parallarShader.SetMat4("view", view);
	parallarShader.SetMat4("projection", projection);
	glm::mat4 parallaxModel(1);
	parallaxModel = glm::translate(parallaxModel, glm::vec3(0, -3, 2));
	parallarShader.SetMat4("model", parallaxModel);
	parallarShader.SetVec3("lightPos", glm::vec3(1.7f, -2.8f, 5.0f));
	parallarShader.SetVec3("viewPos", camera.transform.position);
	parallarShader.SetInt("diffuseMap", 0);
	parallarShader.SetInt("normalMap", 1);
	parallarShader.SetInt("parallaxMap", 2);
	glDisable(GL_CULL_FACE);
	// 砖墙
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, bricksDiffuseTex.GetTextureID());
	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2D, bricksNormalTex.GetTextureID());
	glActiveTexture(GL_TEXTURE2);
	glBindTexture(GL_TEXTURE_2D, bricksParallaxTex.GetTextureID());
	RenderParallax();
	//玩具盒
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, toyboxDiffuseTex.GetTextureID());
	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2D, toyboxNormalTex.GetTextureID());
	glActiveTexture(GL_TEXTURE2);
	glBindTexture(GL_TEXTURE_2D, toyboxParallaxTex.GetTextureID());
	glDisable(GL_CULL_FACE);
	parallaxModel = glm::translate(parallaxModel, glm::vec3(-3, 0, 0));
	parallarShader.SetMat4("model", parallaxModel);
	RenderParallax();
	glEnable(GL_CULL_FACE);
}

ParallaxVertex.glsl 新建

cpp 复制代码
#version 330 core

layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec2 texCoords;
layout (location = 3) in vec3 tangent;
layout (location = 4) in vec3 bitangent;

out VS_OUT {
    vec3 FragPos;
    vec2 TexCoords;
    vec3 TangentLightPos;
    vec3 TangentViewPos;
    vec3 TangentFragPos;
} vs_out;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

uniform vec3 lightPos;
uniform vec3 viewPos;

void main()
{
    gl_Position = projection * view * model * vec4(position, 1.0f);
    vs_out.FragPos = vec3(model * vec4(position, 1.0));   
    vs_out.TexCoords = texCoords;
    
    vec3 T = normalize(mat3(model) * tangent);
    vec3 B = normalize(mat3(model) * bitangent);
    vec3 N = normalize(mat3(model) * normal);
    mat3 TBN = transpose(mat3(T, B, N));

    vs_out.TangentLightPos = TBN * lightPos;
    vs_out.TangentViewPos  = TBN * viewPos;
    vs_out.TangentFragPos  = TBN * vs_out.FragPos;
}

ParallaxFragment.glsl 新建

cpp 复制代码
#version 330 core

out vec4 FragColor;

in VS_OUT {
    vec3 FragPos;
    vec2 TexCoords;
    vec3 TangentLightPos;
    vec3 TangentViewPos;
    vec3 TangentFragPos;
} fs_in;

uniform sampler2D diffuseMap;
uniform sampler2D normalMap;
uniform sampler2D depthMap;

void main()
{           
    vec3 viewDir = normalize(fs_in.TangentViewPos - fs_in.TangentFragPos);
    vec2 texCoords = fs_in.TexCoords;
        
    vec3 normal = texture(normalMap, texCoords).rgb;
    normal = normalize(normal * 2.0 - 1.0);   
   
    vec3 color = texture(diffuseMap, texCoords).rgb;
    // Ambient
    vec3 ambient = 0.1 * color;
    // Diffuse
    vec3 lightDir = normalize(fs_in.TangentLightPos - fs_in.TangentFragPos);
    float diff = max(dot(lightDir, normal), 0.0);
    vec3 diffuse = diff * color;
    // Specular    
    vec3 reflectDir = reflect(-lightDir, normal);
    vec3 halfwayDir = normalize(lightDir + viewDir);  
    float spec = pow(max(dot(normal, halfwayDir), 0.0), 32.0);
    vec3 specular = vec3(0.2) * spec;

    FragColor = vec4(ambient + diffuse + specular, 1.0f);
}

编译运行

视差映射 (Parallax Mapping)

ParallaxFragment.glsl

cpp 复制代码
uniform float heightScale;

// 视差映射
vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir)
{
    float depth = texture(parallaxMap, texCoords).r;
    vec2 p = viewDir.xy / viewDir.z * (depth * depthScale);
    return texCoords - p;
}

void main()
{           
    vec3 viewDir = normalize(fs_in.TangentViewPos - fs_in.TangentFragPos);
    vec2 texCoords = ParallaxMapping(fs_in.TexCoords, viewDir);
    // ...
}

定义一个名为 ParallaxMapping 的函数,它接收UV坐标和视线方向

首先使用原始的UV坐标采样视差贴图获取深度值 depth

然后 viewDir.xy / viewDir.z 进行透视矫正

最后乘以采样来的深度值 depth 和外部传入的常量 depthScale 获取UV偏移量
Main.cpp

cpp 复制代码
parallarShader.SetFloat("depthScale", 0.1f);

编译运行,顺利的话可以看见以下图像

砖墙的边缘看起来还有点怪,这是因为纹理坐标经过偏移超出了 0, 1 的范围,我们需要进行剔除
ParallaxFragment.glsl

cpp 复制代码
if(texCoords.x > 1.0 || texCoords.y > 1.0 || texCoords.x < 0.0 || texCoords.y < 0.0)
    discard;

三、陡峭视差映射 (Steep Parallax Mapping)

如果以大角度的侧面进行观察会发现一些问题

这是因为我们目前只是进行了一个大致的视差映射,接下来使用陡峭视差映射 来获取更精确的结果

陡峭视差映射的基本思想是将总深度范围划分为同一个深度/高度的多个层,我们从上至下遍历深度层,如果该层的深度值高于了存储在深度贴图中的值则结束

cpp 复制代码
// 陡峭视差映射
vec2 SteepParallaxMapping(vec2 texCoords, vec3 viewDir)
{
    // 划分的层数
    const float numLayers = 10;
    // 每层大小
    float layerDepth = 1.0 / numLayers;
    // 当前层深度
    float currentLayerDepth = 0.0;

    // 纹理坐标变化量
    vec2 P = viewDir.xy * depthScale; 
    vec2 deltaTexCoords = P / numLayers;

    vec2  currentTexCoords     = texCoords;
    float currentDepthMapValue = texture(parallaxMap, currentTexCoords).r;

    while (currentLayerDepth < currentDepthMapValue)
    {
        currentTexCoords -= deltaTexCoords;
        currentDepthMapValue = texture(parallaxMap, currentTexCoords).r;  
        currentLayerDepth += layerDepth;  
    }

    return currentTexCoords;
}

编译运行,从侧面看效果好了很多

不过由于采样深度变化非连续,会出现断层问题

可以通过观察的角度来优化层数,倾斜角度越大层数越多,提供更好的性能的同时减轻断层的问题

cpp 复制代码
const float minLayers = 8;
const float maxLayers = 32;

float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0.0, 0.0, 1.0), viewDir)));

但断层的根本原因是因为采样深度变化非连续,接下来我们使用视差遮蔽映射来解决这个问题

四、视差遮蔽映射 (Parallax Occlusion Mapping)

视差遮蔽映射和陡峭视差映射类似,但不是直接使用目标层的纹理坐标,而是根据表面高度距离来在深度层之间进行线性插值

ParallaxFragment.glsl

cpp 复制代码
// 视差遮蔽映射
vec2 ParallaxOcclusionMapping(vec2 texCoords, vec3 viewDir)
{ 
    // 划分的层数
    const float minLayers = 10;
    const float maxLayers = 20;
    float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0.0, 0.0, 1.0), viewDir)));  

    // 每层大小
    float layerDepth = 1.0 / numLayers;
    // 当前层深度
    float currentLayerDepth = 0.0;

    // 纹理坐标变化量
    vec2 P = viewDir.xy / viewDir.z * depthScale; 
    vec2 deltaTexCoords = P / numLayers;
  
    vec2  currentTexCoords     = texCoords;
    float currentDepthMapValue = texture(parallaxMap, currentTexCoords).r;
      
    while(currentLayerDepth < currentDepthMapValue)
    {
        currentTexCoords -= deltaTexCoords;
        currentDepthMapValue = texture(parallaxMap, currentTexCoords).r;  
        currentLayerDepth += layerDepth;  
    }
    
    // 上一个深度的纹理坐标
    vec2 prevTexCoords = currentTexCoords + deltaTexCoords;

    // 获取两层的深度,用于后续插值
    float afterDepth  = currentDepthMapValue - currentLayerDepth;
    float beforeDepth = texture(parallaxMap, prevTexCoords).r - currentLayerDepth + layerDepth;
 
    // 插值权重
    float weight = afterDepth / (afterDepth - beforeDepth);
    vec2 finalTexCoords = prevTexCoords * weight + currentTexCoords * (1.0 - weight);

    return finalTexCoords;
}

编译运行,断层的问题好了很多

完整代码可在顶部Git仓库中找到

下接:https://blog.csdn.net/weixin_44506615/article/details/151986616?spm=1001.2014.3001.5502

相关推荐
hongyucai2 小时前
随意学习-模型工程:因子、隐空间、采样策略
学习
Amazing_Cacao6 小时前
CFCA精品可可产区风土体系(亚洲):彻底拒绝应试背诵,将感官复核作为检验真实物理差异的唯一铁律
学习
谙弆悕博士10 小时前
信息系统项目管理师教程(第4版)笔记——第 2 章 信息技术发展
笔记·职场和发展·软考高级·软考·高项·项目经理
wp123_111 小时前
【硬件选型笔记】TONEVEE MVL4020-1R2M 与Coilcraft XAL4020-122MEC 大电流功率电感参数深度解析
笔记
aaaameliaaa12 小时前
指针之总结
c语言·笔记·算法
caimouse12 小时前
protoc-gen-c 支持 proto3 `optional` 关键字修改记录
c语言·学习
撩得Android一次心动12 小时前
Linux编程笔记3【个人用】
linux·笔记
憧憬成为java架构高手的小白12 小时前
黑马八股--中间件学习之MQ(RabbitMQ)
学习·中间件
撩得Android一次心动14 小时前
Linux编程笔记4【个人用】
linux·笔记·学习
user-猴子14 小时前
让网页“活”过来 —— 用AI打造会自主学习的动态知识图谱
人工智能·学习·知识图谱