视差映射技术
在我的上一篇文章里,有提到过基本的高度图的使用。具体的原理这里也不多解释,具体可以看回 基于物理的渲染PBR(三):视差贴图。而上一篇文章里介绍的,只是最基本的视差映射,其实它得到的采样点并不是百分百正确的。虽然Unity自带的视差处理是基础的近似值,但是通过查阅资料,我们可以更加精确的得到采样点,下面一一介绍几种视差映射算法。
陡峭视差映射
该方法的核心是把表面深度切分成等距的若干层,然后通过以视角方向为参考,比较自己定义的表面深度和实际的高度图。当表面深度大于高度图时,证明还没到实际的视角点,继续叠加纹理坐标直到找到第一个小于高度图的点,那一个点便取做真正的纹理坐标,具体的可以参考下图:
从上图得知,T3就是我们要求的纹理坐标,但我们不难看出,它离实际的视角接触到的高度图坐标还是有不小差距。所以,如果我们想要得到更加精确的结果,我们需要把层数增加,叠加的数值尽量的小,这样可以得到更加精确的结果。但是相应的,会增加性能上的开销,毕竟每次都需要采样得到高度图的值。下面列举核心代码:
float2 SteepParallax(float2 uv,float3 viewparallax)
{
//把切线空间的视角方向归一
float3 v = normalize(viewparallax);
//该操作是为了防止当视角方向和法线夹角过大时的意外情况
v.z += 0.42;
Layernum = 10;
v.xy *= _ParallaxScale;
//每层深度的增量
float everyheight = 1.0 / Layernum;
//当前的深度
float currentheight = 0.0;
//当前的纹理坐标
float2 currenttex = uv;
//纹理坐标的增量
float2 dtex = v.xy / v.z / Layernum;
//需要比较的高度图的值
float currentheightmap = tex2Dlod(_ParallaxMap,float4(currenttex,0,0)).a;
while(currentheightmap > currentheight)
{
currentheight += everyheight;
currenttex += dtex;
currentheightmap = tex2Dlod(_ParallaxMap,float4(currenttex,0,0)).a;
}
return currenttex;
}
基本就是先定义每层的深度,在计算层数深度、纹理坐标等的增量,然后通过循环去计算,需要注意的是在CG语言里,我们不能在for循环或者while循环里用tex2D,可以使用tex2Dlod代替。
下面是用陡峭视差映射的效果图。
我们可以看到它的锯齿非常的严重,那是因为我们的层数只设置了较为低的层数,得到的结果很多都是重复或者不是特别精确的,所以下面引入一个更加精确的方法,浮雕视差映射。
浮雕视差映射
浮雕视差映射其实前半段方法跟陡峭视差映射相差不多,它的核心是在做完陡峭视差技术后,通过二分法进一步得到更精确的纹理坐标,每一次的搜索迭代都可以让精确度提升许多。我们先看下面的图。
通过上图,我们得到T3纹理坐标后,把一开始得到的高度增量和纹理坐标增量除以2,然后往反方向按陡峭视差计算下一个坐标点。同理,如果得到的结果深度是大于高度图的数值,那么继续按反方向计算,反之则往正方向,即图中的左边方向计算。每次计算时都把增量除以2,而计算的次数也是由我们决定,当然还是那句话,次数越多,纹理坐标越精确,但相应性能损耗也很高。下面是核心代码:
******//与陡峭视差映射一样,此处省略了
//二分次数
float numsearch = 5;
for(int i = 0; i < numsearch; i ++)
{
//把增量除以2
everyheight *= 0.5;
dtex *= 0.5;
currentheightmap = tex2Dlod(_ParallaxMap,float4(currenttex,0,0)).a;
//比较
if(currentheightmap < currentheight)
{
currenttex -= dtex;
currentheight -= everyheight;
}
else
{
currenttex += dtex;
currentheight += everyheight;
}
}
return currenttex;
下面是效果图:
可以很明显看到,锯齿少了非常多,但是该方法比陡峭映射更消耗性能,因此,我们又推出了另一种映射方法,视差遮蔽映射。
视差遮蔽映射
该方法属于陡峭视差映射的另一种改进方法,它不使用二分法找精确点,因为在渲染里,循环操作是非常耗性能的,该方法使用的是对所得的两个近似结果进行插值,虽然它效果不如浮雕视差映射,但是性能比它好一点。
从上图,我们在陡峭视差映射里得到最后的结果和上一个结果,即H(T3)和H(T2)进行插值,得到的结果Tp也可以近似的看作纹理坐标结果。下面列举核心代码:
******//与陡峭视差映射一样,此处省略了
//得到上一个计算出的纹理坐标
float2 prevtex = currenttex - dtex;
//线性插值
float afterHeight = currentheightmap - currentheight;
float beforeHeight = tex2D(_ParallaxMap,prevtex).a - currentheight + everyheight;
float weight = afterHeight / (afterHeight - beforeHeight);
float2 finaltex = lerp(currenttex,prevtex,weight);
return finaltex;
下面上结果图:
我们可以看到还是有轻微的锯齿的,但是比起陡峭会好很多,性能上也比浮雕好,如果想要得到更加精确的结果,只能在层数上进行增加。
最后在层数上的数据,在阅读其他文章中也有提到一个优化方法,就是设定一个最小层和最大层,而它的层数由视角和法线的角度作为一个基准,做两个数的一个线性插值,下面是具体代码:
float minLayernum = 5;
float maxlayernum = 15;
float Layernum = lerp(minLayernum,maxlayernum,abs(dot(float3(0,0,1),v)));
最后再对比上一章给的,用基础的视差映射近似法得到的结果图:
咦,怎么回事。。。怎么效果这么好,其实这里我是不太懂的,如果有懂的老哥麻烦指导一下,我自己是觉得这个近似法的效果是最好的。。。可能跟贴图和高度图的复杂度也有关系吧
参考:
(译) GLSL 中的视差遮蔽映射(Parallax Occlusion Mapping in GLSL
Parallax Mapping需要翻墙