Learn OpenGL 笔记6.6 Parallax Mapping(视差贴图)

Parallax Mapping视差贴图是一种类似于法线贴图的技术,但基于不同的原理。

Parallax Mapping视差贴图displacement mapping位移贴图(看样子是得位移顶点了)技术系列密切相关,这些技术基于存储在纹理中的几何信息来置换或偏移顶点。一种方法是取一个大约有 1000 个顶点的平面,并根据纹理中的一个值来置换每个顶点,该值告诉我们平面在该特定区域的高度。这种包含每个纹素高度值的纹理称为高度图。从简单砖表面的几何属性派生的示例高度图看起来有点像这样:

当跨越平面时,每个顶点都根据高度图中的采样高度值进行位移,根据材料的几何属性将平面转换为粗糙的凹凸不平的表面。 例如,使用上面的高度图置换一个平面会产生下图:

我们可以在不需要额外顶点的情况下以某种方式实现类似的真实感。事实上,如果我告诉你之前显示的置换表面实际上只用 2 个三角形渲染会怎样。显示的这个砖表面是用视差贴图渲染的,这是一种不需要额外顶点数据来传达深度的置换贴图技术,但(类似于法线贴图)使用了一种巧妙的技术来欺骗用户。

视差映射背后的想法是以这样一种方式改变纹理坐标,即片段的表面看起来比实际高或低,所有这些都基于视图方向和高度图。要了解它是如何工作的,请查看下面的砖块表面图像:

 

 诀窍是弄清楚如何从 A 点获取 B 点的纹理坐标(本来我看到的是A,但是高度形成之后,我看到的就变成B了,所以A应该变成B)Parallax mapping视差映射试图通过将片段到视图方向向量 V缩放到片段 A 处的高度来解决这个问题。

意思就是:把向量V缩成向量P,让向量P的长度=H(A)的高度(粗糙的等于)

 这个小技巧在大多数情况下都能给出很好的结果,但它仍然是一个非常粗略的近似值 B 点。当高度在表面上快速变化时(陡峭时候就不准确了),结果往往看起来不切实际,因为向量 P¯ 最终不会接近 B 如下所示:

此时P向量长度与H(A)向量高度相等,但实际上,高度图的高度应该是蓝点,而不是H(P)点

视差映射的另一个问题是,当表面以某种方式任意旋转时,P的角度无法确定,所以我们得在切线空间中进行视差映射。

1.Parallax mapping 视差贴图具体做法

对于视差映射,我们将使用一个简单的 2D 平面,在将其发送到 GPU 之前,我们计算了它的tangent切线和bitangent副切线向量;类似于我们在法线贴图章节中所做的。在平面上,我们将附加一个漫反射纹理、一个法线贴图和一个置换贴图。在这个例子中,我们将结合使用Parallax mapping视差贴图normal mapping法线贴图。因为视差贴图会产生置换表面的错觉,所以当光照不匹配时,错觉就会破裂。由于法线贴图通常是从高度图生成的,因此将法线贴图与高度图一起使用可确保光照与位移一致。

您可能已经注意到,上面链接的置换贴图与本章开头显示的高度图相反。对于视差贴图,使用高度图的倒数更有意义,因为在平面上伪造深度比伪造高度更容易。这稍微改变了我们对视差映射的看法,如下所示:

我们再次有一个点 A 和 B,但是这次我们依据点 A 的纹理坐标H(A)的长度来缩减延申向量 V的长度得到向量 P(A出发,到P的尽头)。我们可以通过从着色器中的 1.0 减去采样的高度图值来获得深度值而不是高度值 ,或者通过简单地在图像编辑软件中反转其纹理值

V:可以根据viewPos和FragPos计算得出

H(A):可以根据parallax图得出

P:从A出发,经过B,到P的尽头。由H(A)计算得出

H(P):由A点和P向量,以及parallax图计算得出

parallax mapping视差贴图是在片段着色器中实现的,因为三角形表面的移位效果是不同的。 在片段着色器中,我们将需要计算片段到视图的方向向量 V,因此我们需要视图位置切线空间中的片段位置。 在法线贴图章节中,我们已经有一个顶点着色器,它在切线空间中发送这些向量,因此我们可以获取该章节顶点着色器的copy:

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
layout (location = 3) in vec3 aTangent;
layout (location = 4) in vec3 aBitangent;

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(aPos, 1.0);
    vs_out.FragPos   = vec3(model * vec4(aPos, 1.0));   
    vs_out.TexCoords = aTexCoords;    
    
    vec3 T   = normalize(mat3(model) * aTangent);
    vec3 B   = normalize(mat3(model) * aBitangent);
    vec3 N   = normalize(mat3(model) * aNormal);
    mat3 TBN = transpose(mat3(T, B, N));

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

然后在片段着色器中实现视差映射逻辑。 片段着色器看起来有点像这样:

#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;
  
uniform float height_scale;
  
vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir);
  
void main()
{           
    // offset texture coordinates with Parallax Mapping
    vec3 viewDir   = normalize(fs_in.TangentViewPos - fs_in.TangentFragPos);
    // 本来看到A点,增加高度之后,实际看到的是B点,ParallaxMapping()由A点转换成B点
    vec2 texCoords = ParallaxMapping(fs_in.TexCoords,  viewDir);

    // then sample textures with new texture coords
    vec3 diffuse = texture(diffuseMap, texCoords);
    vec3 normal  = texture(normalMap, texCoords);
    normal = normalize(normal * 2.0 - 1.0);
    // proceed with lighting code
    [...]    
}

我们定义了一个名为 ParallaxMapping 的函数,它将片段的纹理坐标切线空间中的片段到视图方向 V作为输入。 该函数返回位移后的纹理坐标。 然后我们使用这些置换的纹理坐标作为纹理坐标来采样漫反射和法线贴图。 因此,片段的漫反射和法线向量正确对应于表面的位移后的几何体

本来看到A点,增加高度之后,实际看到的是B点,ParallaxMapping()由A点转换成B点

 

让我们来看看ParallaxMapping函数内部:

vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir)
{ 
    //获取高度
    float height =  texture(depthMap, texCoords).r;    
    // viewDir.xy相当于一个v2,观察者向量的z分量,相当于俯视的角度,z越小,俯视角度越小,除以z分量,则越俯视越深
    vec2 p = viewDir.xy / viewDir.z * (height * height_scale);
    //转换为下陷
    return texCoords - p;    
} 

这里需要注意的是 viewDir.xy 与 viewDir.z 的划分。随着 viewDir 向量被normalize标准化,viewDir.z 将在 0.0 和 1.0 之间的某个范围内。当 viewDir 很大程度上平行于表面时,其 z 分量接近 0.0,与 viewDir 大部分垂直于表面时相比,除法返回一个更大的向量 P。我们正在调整 P的大小,以便与从顶部看表面相比,从某个角度看表面时,它以更大的比例偏移纹理坐标;这在角度上提供了更逼真的结果。
有些人更喜欢将 viewDir.z 的除法排除在等式之外,因为默认的视差贴图可能会在角度产生不良结果;该技术被称为Parallax Mapping with Offset Limiting具有偏移限制的视差映射。选择哪种技术通常取决于个人喜好。

然后使用生成的纹理坐标对其他纹理(漫反射和法线)进行采样,这提供了非常整洁的置换效果,如下所示,height_scale 大约为 0.1:

 

在这里你可以看到法线贴图和视差贴图结合法线贴图的区别。 因为视差贴图试图模拟深度,所以实际上可以根据您查看它们的方向让砖与其他砖重叠。

您仍然可以在视差映射平面的边缘看到一些奇怪的边界伪影。 发生这种情况是因为在平面的边缘,位移后的纹理坐标可能会在 [0, 1] 范围之外过采样。 这会根据纹理的环绕模式给出不切实际的结果。 解决这个问题的一个很酷的技巧是在它在默认纹理坐标范围之外采样时丢弃片段:

texCoords = ParallaxMapping(fs_in.TexCoords,  viewDir);
if(texCoords.x > 1.0 || texCoords.y > 1.0 || texCoords.x < 0.0 || texCoords.y < 0.0)
    discard;

所有(位移)纹理坐标超出默认范围的片段都将被丢弃,然后视差贴图会在表面边缘周围给出正确的结果。 请注意,此技巧不适用于所有类型的表面,但是当应用于平面时,它会产生很好的效果:

 它看起来很棒而且速度非常快,而且我们只需要一个额外的纹理样本即可让视差贴图工作。 它确实存在一些问题,因为它在从某个角度(类似于法线贴图)查看时会出现故障,并且在高度变化陡峭时会给出不正确的结果,如下所示:

它有时不能正常工作的原因是它只是置换贴图的粗略近似。 然而,有一些额外的技巧仍然可以让我们在陡峭的高度变化下获得几乎完美的结果,即使是从一个角度看也是如此。 例如,如果我们不是一个样本,而是取多个样本来找到最接近 B 的点会怎样? 

2.Steep Parallax Mapping(陡峭的视差贴图)

陡峭视差映射是视差映射之上的扩展,因为它使用相同的原理,但它需要多个样本而不是 1 个样本来更好地确定向量 P 到 B。即使高度变化陡峭,这也能提供更好的结果,如 该技术的准确性因样本数量而提高。

Steep Parallax Mapping 的总体思路是将总深度范围划分为多个相同高度/深度的层。 对于这些层中的每一个,我们对深度图进行采样,沿着 P 的方向移动纹理坐标,直到我们找到一个小于当前层深度值的采样深度值。 看看下面的图片:

 沿着紫色线,从T0出发,一路来到T1与紫色线交叉地点,发现比高度图的高,继续往前,直到来到了T3与紫色交汇点,此时第一次比高度图低,就选定了这个点。

然后我们从向量 P3获取纹理坐标偏移 T3 来替换片段的纹理坐标。您可以看到精度如何随着深度层的增加而增加。

要实现这项技术,我们只需要更改 ParallaxMapping 函数,因为我们已经拥有了我们需要的所有变量:

vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir)
{ 
    // number of depth layers
    const float numLayers = 10;
    // calculate the size of each layer
    float layerDepth = 1.0 / numLayers;
    // depth of current layer
    float currentLayerDepth = 0.0;
    // the amount to shift the texture coordinates per layer (from vector P)
    vec2 P = viewDir.xy * height_scale; 
    vec2 deltaTexCoords = P / numLayers;
  
    [...]     
}   

我们指定层数计算每层的深度偏移,最后计算我们必须沿着每层P方向移动的纹理坐标偏移。

然后我们遍历所有层,从顶部开始,直到我们找到一个小于层的深度值的深度图值:

// get initial values
vec2  currentTexCoords     = texCoords;
float currentDepthMapValue = texture(depthMap, currentTexCoords).r;

//沿着V的紫色部分延申,直到与某个T相交,比parallax高度小
while(currentLayerDepth < currentDepthMapValue)
{
    // shift texture coordinates along direction of P
    currentTexCoords -= deltaTexCoords;
    // get depthmap value at current texture coordinates
    currentDepthMapValue = texture(depthMap, currentTexCoords).r;  
    // get depth of next layer
    currentLayerDepth += layerDepth;  
}

return currentTexCoords;

在这里,我们循环遍历每个深度层并停止,直到我们找到沿向量 P的纹理坐标偏移,该偏移首先返回低于(位移)表面的深度。 从片段的纹理坐标中减去产生的偏移量以获得最终置换的纹理坐标向量,与传统的视差映射相比,这一次具有更高的精度。

大约有 10 个样本,即使从某个角度看,砖表面看起来也更加可行,但是当具有高度变化陡峭的复杂表面时,陡峭的视差贴图确实很出色; 就像之前展示的木制玩具表面:

 我们可以通过利用视差映射的属性之一来稍微改进算法。 当直视表面时,不会发生太多纹理位移,而从某个角度观察表面时会发生大量位移(在两种情况下都可视化视图方向)。 通过在直视表面时采集较少的样本而在观察某个角度时采集更多的样本,我们只采样了必要的数量:

const float minLayers = 8.0;
const float maxLayers = 32.0;
//根据viewDir来插值取8.0和32.0中间的值
float numLayers = mix(maxLayers, minLayers, max(dot(vec3(0.0, 0.0, 1.0), viewDir), 0.0)); 

尽管如此,陡峭的视差贴图也有它的问题。 因为该技术基于有限数量的样本,所以我们得到了混叠效果,并且很容易发现层之间的明显区别:

我们可以通过采集大量样本来减少这个问题,但这很快就会对性能造成太大的负担。 有几种方法旨在通过不采用(位移)表面下方的第一个位置来解决此问题,而是通过在该位置的两个最近的深度层之间进行插值来找到与 B 更接近的匹配

其中两种比较流行的方法称为Relief Parallax Mapping浮雕视差贴图Parallax Occlusion Mapping视差遮挡贴图,其中Relief Parallax Mapping浮雕视差贴图提供最准确的结果,但与Parallax Occlusion Mapping视差遮挡贴图相比,其性能消耗也更高。 因为Parallax Occlusion Mapping视差遮挡贴图给出的结果与浮雕视差贴图几乎相同,而且效率更高,所以它通常是首选方法。

3.Parallax Occlusion Mapping(视差遮挡贴图

视差遮挡贴图基于与陡峭视差贴图相同的原理,但不是在碰撞后取第一个深度层的纹理坐标,而是在碰撞前后深度层之间进行线性插值。 我们将线性插值的权重基于表面高度与两个层的深度层值之间的距离。 看看下面的图片来了解它是如何工作的:

 总结:就是Steep Parallax Mapping陡峭视差贴图,最后的步骤,最后两个值采用插值而不是直接选最后一个值

如您所见,它在很大程度上类似于陡峭视差映射,但作为一个额外的步骤,两个深度层的纹理坐标之间的线性插值围绕相交点。 这又是一个近似值,但比陡峭视差贴图准确得多。

Parallax Occlusion Mapping 的代码是在 Steep Parallax Mapping 之上的扩展:

[...] // steep parallax mapping code here
  
// get texture coordinates before collision (reverse operations)
vec2 prevTexCoords = currentTexCoords + deltaTexCoords;

// get depth after and before collision for linear interpolation
// 获取紫色线条最后的两个交点的depth,高度值
float afterDepth  = currentDepthMapValue - currentLayerDepth;
float beforeDepth = texture(depthMap, prevTexCoords).r - currentLayerDepth + layerDepth;
 
// interpolation of texture coordinates
// 配置权重,数学技巧,导致prevTexCoords和currentTexCoords的比例,等同于beforeDepth与afterDepth
float weight = afterDepth / (afterDepth - beforeDepth);
vec2 finalTexCoords = prevTexCoords * weight + currentTexCoords * (1.0 - weight);

return finalTexCoords;  

 视差贴图是一种增强场景细节的好技术,但它确实带来了一些您在使用时必须考虑的伪影。 大多数情况下,视差贴图用于地板或类似墙壁的表面,在这些表面上确定表面的轮廓并不那么容易,并且视角通常大致垂直于表面。 这样,视差贴图的伪像就不会那么明显,并使其成为增强对象细节的一种非常有趣的技术。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值