【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

相关推荐
张人玉14 分钟前
图像处理函数与形态学操作笔记(含 Halcon 示例)
图像处理·人工智能·笔记·halcon
星依网络28 分钟前
使用LabelImg工具标注数据(游戏辅助脚本开发)
python·游戏引擎·图形渲染·骨骼绑定
崇山峻岭之间29 分钟前
C++ Prime Plus 学习笔记041
c++·笔记·学习
万岳科技系统开发42 分钟前
私域直播小程序源码的整体架构设计与实现思路
学习·小程序
richxu202510011 小时前
嵌入式学习之路>单片机核心原理篇>(11) 存储器(Flash & SRam)
单片机·嵌入式硬件·学习
sszdlbw1 小时前
后端springboot框架入门学习--第二篇
java·spring boot·学习
9527(●—●)1 小时前
windows系统python开发pip命令使用(菜鸟学习)
开发语言·windows·python·学习·pip
Lv11770082 小时前
Visual Studio中的字典
ide·笔记·c#·visual studio
LXS_3572 小时前
Day 16 C++提高之模板
开发语言·c++·笔记·学习方法
好奇龙猫2 小时前
日语学习-日语知识点小记-构建基础-JLPT-N3阶段-二阶段(30):第8科
学习