5-视差贴图(Parallax mapping)
视差贴图(Parallax Mapping)和法线贴图差不多,但实现方法不一样。视差贴图也利用了视错觉,且对深度有更好的表达,与法线贴图一起用能够产生更好的效果。
视差贴图
概念
视差贴图属于位移贴图(Displacement Mapping)技术的一种,它根据存储在纹理中的几何信息,对顶点进行位移/偏移。一种实现方法是准备一个1000的平面,根据高度贴图的信息对顶点进行位移。
一个简单的砖块表面的高度贴图如下:
利用该高度贴图,对平面上顶点进行位移后的结果如下:
思想
有个问题是必须使用很多顶点的平面才能获得具有真实感的效果,否则效果不好。但使用很多的顶点意味着有一堆三角形要计算,太麻烦了。可以不用这么多顶点来近似达到同样效果吗?答案是可以的,实际上,上面的表面就是用两个三角形渲染出来的。上面的表面使用视差贴图技术渲染,这种位移贴图技术不需要额外的顶点信息表达深度,类似于法线贴图,它也用一种聪明的技巧去欺骗用户。
视差贴图背后的思想就是:修改纹理坐标,让片段表面看起来变高/低了,基于视角方向和一个高度贴图。看看砖墙表面的剖面图:
其中,粗糙的红线是高度贴图中的几何信息表述;\(\bar{V}\)是着色点到摄像机的方向(viewDir
)。如果平面确实位移了,观察者应该在\(B\)看到着色点。然而我们的平面没有实际上的位移,着色点实际上在\(A\)。视差贴图偏移了着色点\(A\)的纹理坐标到\(B\),然后我们用\(B\)的纹理坐标采样,观察者就像看到点\(B\)一样。
接下来看看如何在\(A\)处获取到\(B\)的纹理坐标,视差贴图通过以点\(A\)的高度缩放向量\(\bar{V}\)来解决,最终得到缩放好的向量\(\bar{P}\):
然后就能得到纹理坐标的偏移量了,用H(P)
近似模拟\(B\)。但由于\(B\)是粗略算出来的,当表面高度变化得很快时,得到的结果就不会真实了:
此外还有一个问题,就是当表面旋转后就很难计算\(\bar{P}\)了,因此还需要在切线空间中计算视差贴图。将\(\bar{V}\)从世界空间转换到切线空间,然后通过求下\(\bar{P}\)的(x, y)
作为纹理坐标的偏移量。
shader实现
为了简易实现视差贴图,这里使用简易2D平面,并且手动计算了tangent
和bitangent
值。在这个平面上,将附加漫反射贴图,法线贴图和位移贴图。同时使用法线贴图和位移贴图可以保证光照和顶点“偏移”相匹配。
发现给出的位移贴图颜色和上面的高度贴图是反的,这是因为使用了反色高度贴图,模拟深度比模拟高度更方便:
这里通过对\(\bar{V}\)减去\(A\)的纹理坐标得到\(\bar{P}\),其中H(A)
可由1.0-采样深度值得到。在片段着色器中,根据着色点的纹理坐标和观察方向计算偏移纹理坐标的函数如下:
uniform float heightScale;
vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir)
{
float height = texture(depthMap, texCoords).r;
vec2 p = viewDir.xy / viewDir.z * (height * heightScale);
return texCoords - p;
}
其中,heightScale
是引入的额外控制量,因为视差效果没有一个缩放参数会过于强烈。还要注意的是/ viewDir.z
,由于viewDir
是标准化向量,viewDir.z
属于[0, 1]
,当观察方向大致平行于表面时,viewDir.z
接近于0,导致偏移量增加,让结果更真实。
此外,还需要注意偏移后的纹理坐标可能会超出[0, 1]
,需要丢弃掉不符合条件的纹理坐标:
vec2 texCoords = useParallaxMap ? ParallaxMapping(fs_in.TexCoords, viewDir) : fs_in.TexCoords;
if(texCoords.x > 1.0 || texCoords.y > 1.0 || texCoords.x < 0.0 || texCoords.y < 0.0)
discard;
最终结果如下:
陡峭视差映射
上面的结果可以发现,在陡峭的地方产生了“扭曲”效果。这是因为样本太少了,应该使用多重采样去找到\(B\)点。
概念
这里介绍陡峭视差映射(Steep Parallax Mapping),它是视差映射的一种拓展,并且使用多重采样去找到\(B\)点,即使在陡峭的地方结果也很好。
思想
陡峭视差映射的基本思想是 将总体深度范围划分为相同高度/深度的几层。采用“步进”的方式,沿着\(\bar{P}\)方向一层一层走,直到找到一个采样点\(B\),它的深度值低于当前层的深度值。
在这张图中,\(\bar{P}\)向前步进,步进到深度为0.2时,采样点为\(T_1\),发现深度\(H(T_1)>0.2\),还不行。直到深度为0.6时,采样点为\(T_3\),发现深度\(H(T_3)<0.6\),那\(T_3\)就可以是目标\(B\)点。
shader实现
接下来我们修改ParallaxMapping()
即可,首先给深度分个层:
vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir)
{
// 给深度分层
const float numLayers = 10;
float layerDepth = 1.0 / numLayers;
// 记录当前层的深度值
float currentLayerDepth = 0.0;
// 每层的偏移量
vec2 P = viewDir.xy * heightScale;
vec2 deltaTexCoords = P / numLayers;
...
}
然后进行步进,直到找到符合条件的采样点:
vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir)
{
...
// 步进找到合适的采样点
vec2 currentTexCoords = texCoords;
float currentDepthMapValue = texture(depthMap, currentTexCoords).r;
while (currentLayerDepth < currentDepthMapValue)
{
currentTexCoords -= deltaTexCoords;
currentDepthMapValue = texture(depthMap, currentTexCoords).r;
currentLayerDepth += layerDepth;
}
return currentTexCoords;
}
结果如下,减缓了陡峭点的“扭曲”效果:
此外还能进行优化,当视线和表面垂直时减少采样点;当视线和表面角度逐渐减少时增加采样点:
// 给深度分层
const float minLayers = 8;
const float maxLayers = 32;
float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0.0, 0.0, 1.0), viewDir)));
float layerDepth = 1.0 / numLayers;
视差遮蔽映射
现在陡峭点的“扭曲”效果减缓了,但又出现了“断层”效果,这是因为采样点间的变化太突兀,需要通过插值解决。这里通过 视差遮蔽映射 这一方法解决。
概念
视差遮蔽映射(Parallax Occlusion Mapping)在陡峭视差映射的基础上添加了插值概念:
shader实现
在陡峭视差映射的基础上,增加插值代码即可:
// 获取上一个采样点
vec2 prevTexCoords = currentTexCoords + deltaTexCoords;
// 获取两采样点的深度值
float afterDepth = currentDepthMapValue - currentLayerDepth;
float beforeDepth = texture(depthMap, prevTexCoords).r - currentLayerDepth + layerDepth;
// 线性插值
float weight = afterDepth / (afterDepth - beforeDepth);
vec2 finalTexCoores = prevTexCoords * weight + currentTexCoords * (1.0 - weight);
return finalTexCoores;
结果如下:
参考资料
- LearnOpenGL - Parallax Mapping
- 视差贴图 - LearnOpenGL CN (learnopengl-cn.github.io)