【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

相关推荐
试试勇气20 分钟前
Linux学习笔记(八)--环境变量与进程地址空间
linux·笔记·学习
蒙奇D索大22 分钟前
【数据结构】考研数据结构核心考点:平衡二叉树(AVL树)详解——平衡因子与4大旋转操作入门指南
数据结构·笔记·学习·考研·改行学it
andwhataboutit?1 小时前
Docker Compose学习
学习·docker·容器
郭庆汝1 小时前
自然语言处理笔记
笔记·自然语言处理·easyui
二进制怪兽1 小时前
[笔记] 驱动开发:Virtual-Display-Driver编译过程
笔记
ouliten1 小时前
cuda编程笔记(28)-- cudaMemcpyPeer 与 P2P 访问机制
笔记·cuda
im_AMBER2 小时前
数据结构 04 栈和队列
数据结构·笔记·学习
尘似鹤2 小时前
微信小程序学习(六)--多媒体操作
学习·微信小程序·小程序
要做朋鱼燕3 小时前
密码学安全:CIA三元组与三大核心技术
网络·笔记·密码学·嵌入式·加密·aes
UpYoung!3 小时前
无广技术贴!【PDF编辑器】Solid Converter PDF保姆级图文下载安装指南——实用推荐之PDF编辑软件
学习·数学建模·pdf·编辑器·运维开发·个人开发