【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

相关推荐
Duo1J2 小时前
【OpenGL】LearnOpenGL学习笔记25 - 法线贴图 NormalMap
笔记·学习·图形渲染·贴图·着色器
da_vinci_x2 小时前
游戏UI告别“贴图”时代:用Adobe XD构建“活”的设计系统
游戏·ui·材质·贴图·游戏策划·游戏美术·pbr
风已经起了2 小时前
FPGA学习笔记——图像处理之亮度调节(乘法型)
图像处理·笔记·学习·fpga开发·fpga
能不能别报错2 小时前
K8s学习笔记(五) Velero结合minnio业务数据备份与恢复
笔记·学习·kubernetes
能不能别报错3 小时前
K8s学习笔记(六) K8s升级与节点管理
笔记·学习·kubernetes
今天也好累3 小时前
贪心算法之分数背包问题
c++·笔记·学习·算法·贪心算法
誰能久伴不乏4 小时前
Linux Shell 脚本:从零到进阶的实战笔记
linux·chrome·笔记
九年义务漏网鲨鱼4 小时前
等效学习率翻倍?梯度累积三连坑:未除以 accum_steps、调度器步进错位、梯度裁剪/正则标度错误(含可复现实验与修复模板)
python·深度学习·学习
ooolmf4 小时前
3.1.1话题,发布小说笔记20250923
笔记