继续练习一个模拟冰块的效果,模拟的是一个不透明内部有杂质的冰块,内部杂质用视差映射来实现,表面就是简单的法线贴图+Cubemap反射采样,也可以直接只计算高光不反射图案。文章会把视差映射讲一下,算是对学习的记录和总结。
Parallax Mapping视差映射
好多人的文章里都写到:
视差映射是法线映射的增强版,不止改变了光照作用,还在平坦的多边形上创建了3D细节的假象。
其实我们看原理和代码能看出来,视差映射只是增加了高低错落的感觉,并没有扰动表面法线改变光照结果,所以在需要的时候我们还是要通过法线贴图+视差映射来渲染出想要的视觉效果。
视差映射只需要用到一个浮点值,也就是只需要贴图中的一个通道就可以表达,所以经常保存在法线贴图的A通道或者其他贴图的其中一个通道中。这个通道中的值保存的是对应的每个点要沉入物体表面多少深度而不是高出表面的高度,所以也可以叫做深度图。
视差映射只是对视向量进行了偏移,对主贴图进行偏移采样来达到让平面看起来是立体的效果。物体表面本身还是原来的样子,顶点并没有发生偏移,高低落差只是一个假象。
当前片元是图中的T0,视向量为V,纹理左边对深度图采样得到当前的深度是0.55,所以V碰到的并不是T0,而是继续向前延伸碰到的高度为0.15的点,对应的纹理坐标是T1,所以应该使用T1的纹理坐标进行主纹理和法线的采样。
简单视差映射 Parallax Mapping
带偏移上限的视差映射 Parallax Mapping With Offse Limiting
只取一步近似计算得到新纹理坐标是最简单的视差映射,被直接成为视差映射。
视差映射只有在高度相对平滑,并且不存在复杂细节时,才能得到相对可以接受的结果。如果视向量和表面法线夹角过大的话就会出现严重错误的结果。
偏移纹理坐标的方法:
切线空间是沿着物体表面建立的,法线垂直于表面,而TB分量和纹理坐标的xy分量重合,所以视向量V的z分量为法线分量,xy分量和纹理的xy分量重合,所以视向量的xy分量可以不加换算的直接用作纹理坐标来计算偏移。用xy除以z,就是视差映射技术中对纹理坐标偏移的原始计算。
如果不除以z,得到的就是带偏移上限的视差映射,带偏移上线的视差映射可以避免在向量V和法向量N夹角太大时的一些错误的结果。
然后把V的xy分量加到T0的纹理坐标上,并且和T0纹理的深度值H(T0)相乘,就得到了沿着V方向的新的纹理坐标。
然后用一个scale系数来控制视差映射效果的幅度。把scale乘给V的xy分量。最有意义的值在0-0.5之间。更高的值会得到错误的映射计算。也可以把scale设为负数,这样的话法向量的z分量需要反转过来进行计算。
从上图中可以看出,视向量正确的交点和偏移后的采样点Tp差距还是很大的,所以视差映射和带偏移上限的视差映射在视向量与法线的夹角越大的情况下误差就会越大。
陡峭视差映射 Steep Parallax Mapping
陡峭视差映射,不是简单的视差近似,不只是简单粗暴的对纹理坐标进行偏移而不进行合理性检查,会检查结果是否接近于正确值。陡峭视差映射是将深度分割为等距的若干层,然后从0层开始采样高度图,每一次会沿着V的方向偏移纹理坐标,如果采样的深度已经大于当前层的深度,停止检查并使用最后一次采样的纹理坐标作为结果。
以上图为例,深度被分割为8层,每层高度为0.125,每层的纹理坐标偏移是V.xy/V.z*sclae/numLayer,从黄色方块开始检查:
- 0层开始,层深为0,采样深度图得到值为0.75,采样的结果大于层的深度,开始下一次迭代
- 沿着V方向偏移纹理坐标,1层开始,层深为0.125,采样深度图的值为0.625,采样结果大于层的深度,开始下一次迭代
- 沿着V方向偏移纹理坐标,2层开始,层深为0.25,采样高度图的值为0.4,采样结果大于层的深度,开始下一次迭代
- 沿着V方向偏移纹理坐标,3层开始,层深为0.375,采样高度图的值为0.2,采样结果小于层的深度,所以得到了实际交点的近似点,采样本次使用的纹理坐标。
可以看出T3的坐标离实际交点还是有距离的,但是这种方式可以检查偏移采样的正确性,如果想得到更精确的结果,可以增加层的数量,提高采样的精确度。层数的提高会降低性能,降低层数会有明显的锯齿现象,可以根据视向量V和表面法向量N的夹角来动态决定层的数量。
vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir)
{
//高度层数
float numLayers = 5;
//每层高度
float layerHeight = 1.0 / numLayers;
// 当前层级高度
float currentLayerHeight = 0.0;
//视点方向偏移总量
vec2 P = viewDir.xy / viewDir.z * heightScale;
//每层高度偏移量
vec2 deltaTexCoords = P / numLayers;
//当前 UV
vec2 currentTexCoords = texCoords;
float currentHeightMapValue = texture(heightMap, currentTexCoords).r;
while(currentLayerHeight < currentHeightMapValue)
{
// 按高度层级进行 UV 偏移
currentTexCoords += deltaTexCoords;
// 从高度贴图采样获取的高度
currentDepthMapValue = texture(depthMap, currentTexCoords).r;
// 采样点高度
currentLayerHeight += layerHeight;
}
return finalTexCoords;
}
浮雕视差映射 Relief Parallax Mapping
浮雕视差映射是在陡峭视差映射的基础上做出的升级优化,先进行陡峭视差映射,可以得到准确交点的前后两个层,和对应的深度值,然后在两层之间使用二分法进行迭代查找。
执行步骤:
- ST、SH除以2,把纹理坐标T3沿着反方向偏移ST,深度沿反方向偏移SH,得到此次迭代的纹理坐标T4和深度H(T4)
- * 采样高度图,ST、SH除以2
- 如果采样高度图得到的深度值大于当前层的深度H(T4),将当前迭代层的深度增加SH,纹理坐标沿着V的方向偏移ST
- 如果采样高度图得到的深度值小于当前层的深度H(T4),将当前迭代层的深度减去SH,纹理坐标沿着V的反向偏移ST
- 从*处循环,知道达到规定的次数,或者两个深度偏差达到一个阈值
- 得到的纹理坐标就是浮雕视差映射的结果
视差遮蔽映射 Parallax Occlusion Mapping
视差遮蔽映射是陡峭视差映射的另一个优化版本,浮雕映射使用二分法来提升精度,但是会降低性能。视差遮蔽性能比陡峭映射好,但是效果比浮雕映射要差。
视差遮蔽映射是使用陡峭映射得到的最后一次采样的H(T3)、UV和前一次采样的到的层深H(T2)、UV,进行一次插值的到的结果。
float2 ParallaxMapping(float2 texCoords, float3 viewDir)
{
//高度层数
float numLayers = 50;
//每层高度
float layerHeight = 1.0 / numLayers;
// 当前层级高度
float currentLayerHeight = 0.0;
//视点方向偏移总量
float2 P = viewDir.xy / viewDir.z * _ParallaxStrength;
//每层高度偏移量
float2 deltaTexCoords = P / numLayers;
//当前 UV
float2 currentTexCoords = texCoords;
float currentHeightMapValue = tex2D(_ParallaxMap, currentTexCoords).r;
while(currentLayerHeight < currentHeightMapValue)
{
// 按高度层级进行 UV 偏移
currentTexCoords += deltaTexCoords;
// 从高度贴图采样获取的高度
currentHeightMapValue = tex2Dlod(_ParallaxMap, float4(currentTexCoords,0,0)).r;
// 采样点高度
currentLayerHeight += layerHeight;
}
//前一个采样的点
float2 prevTexCoords = currentTexCoords - deltaTexCoords;
//线性插值
float afterHeight = currentHeightMapValue - currentLayerHeight;
float beforeHeight = tex2D(_ParallaxMap, currentTexCoords).r - (currentLayerHeight - layerHeight);
float weight = afterHeight / (afterHeight - beforeHeight);
float2 finalTexCoords = prevTexCoords * weight + currentTexCoords * (1.0 - weight);
return finalTexCoords;
}
完成后的冰块效果:
知乎视频www.zhihu.com
虽然完成了效果,但是对视差映射一直还是有一些没有搞懂的地方,为什么简单视差和陡峭视差在计算的时候偏移使用的是V.xy/V.z,而在带偏移上限的计算中偏移使用的是V.xy。在看过的很多文章中都是对所有视差映射的原理方法和代码进行了描述,并没有对这部分的讲解。
后来 @梦旅人 给我发了一篇文章:
https://catlikecoding.com/unity/tutorials/rendering/part-20/catlikecoding.com
在这篇文章中详细的讲解了视差映射的相关内容,在这篇文章中找到了关于我的疑问的讲解,看过以后发现大家的文章中没有对于这部分的讲解估计是因为确实太基础了.....
从上图中可以看出,深度采样H的值肯定是在0-1之间的:
- V.xy*H就可以得到一个一定范围内的偏移量,所以带偏移上限的视差映射使用的V.xy来计算偏移量的
- 那为什么correct offset的偏移距离,也就是陡峭中的总偏移距离为什么是V.xy/V.z呢,将我们的数学功力发挥到初中水平就可以得到结果,∵ ∠a=∠b,∴ cot∠a=cot∠b,∴ xy/z = correct offset / 最大深度1,∴ correct offset = xy/z,这样xy分量除以z分量得到的就是总的UV坐标偏移量了........
结合陡峭的算法也就能想明白为什么要取UV的总偏移量.....
文章中部分内容、插图和代码参考和转自:
[译] GLSL 中的视差遮蔽映射(Parallax Occlusion Mapping in GLSL)segmentfault.com 吴洲:视差贴图(Parallax Mapping)学习笔记zhuanlan.zhihu.com
梦旅人:Unity Shader基于视差映射的云海效果zhuanlan.zhihu.com
GEngine:视差映射(Parallax Mapping)zhuanlan.zhihu.com