上次介绍了一下法线贴图,这次这次介绍一下进阶版--视差贴图(Parallax Mapping)以及视差贴图的极限优化版--浮雕贴图(Relief Mapping)
首先在这里提一下历史。1978年的时候,大神James Blinn(就是那位优化了Phong光照模型,将其改为Blinn-Phong的牛人)在"Simulation of Wrinkled Surfaces"中提出了凹凸贴图(Bump mapping),而到了1996年,法线贴图(Normal Mapping)作为凹凸贴图的一种实现产生了,由Venkat Krishnamurthy and Marc Levoy在Fitting Smooth Surfaces to Dense Polygon Meshes中提出。时间来到2001年,Tomomichi Kaneko et al(这个et al是拉丁文,代表其他人,也就是说这个技术不是他一个人搞出来的)在Detailed Shape Representation with Parallax Mapping提出了称得上是加强版的法线贴图技术,视差贴图。4年后,又有人在Real-time relief mapping on arbitrary polygonal surfaces一文中提出了终极加强版,浮雕贴图。
简单来说,他们都是一种模拟凹凸处光照效果的技术。但为什么说后面两种是加强版呢?因为后两种将视线方向考虑进去,造成更加真实的凹凸效果(如石墙的缝隙有更明显的深度)。当然,上文所说的所有的贴图技术都是通过扰动法线来欺骗我们的眼睛,让我们的眼睛以为渲染的这个东西是凹凸不平的,实际上渲染的那个模型的顶点没有任何改动。有种技术叫位移贴图(Displacement Mapping),图中的每个纹素(texel)储存了一个向量(模型有多少顶点则图中有多少纹素,纹素不与像素一一对应),代表对应顶点的位移,也即是对模型的顶点进行移动而造成凹凸感,不过这里就不展开了。
视差贴图(Parallax Mapping)
图1视差贴图的基本思想是我们利用一张高度图,求取视线与高度曲线相交点的坐标(u,v),即图中的(newU,newV)。这个怎么理解呢?
首先大家都知道z buffer吧?那是一张黑白照,是整个相机视野内所有物体空间位置关系的快照,通过z buffer我们就可以了解整个相机空间。而高度图也是一张黑白照,而且是值域在[0,1]之间的,我们可以把它理解为一个类似z buffer的空间。那么从侧面看这张黑白图的时候,图1的那条弯弯曲曲的蓝线就好理解了。
黑白图那么我们如何取求视线和高度图相交的位置呢?事实上没有任何方法可以准确求出。。。但是,我们可以通过一些方法近似求出。
图2图2就是我们常用的近似求法。我们看着某个顶点时,这个点的uv在高度图上的深度d可以知道,用这个d去乘以视线向量v,得出一个向量v',v'的x和y就是我们要的u,v值了,从图中可以看出黄线是我们真正要的,而在它右侧一点我们算出的(u1,v1)已经比较接近了。当然有人会说这种求法好扯淡,好不准。。。我第一次看到的时候也是这么觉得,的确在某些情况下这个方法完全不对,如图3
图3但高度图没有那么剧烈起伏的话,这么算还是可以接受的。下面是我实现的效果图
图4在Unity中我们可以很方便的实现视差贴图,Unity已经为我们准备好了ParallaxOffset
这个方法,我们可以直接调用。
在vertex shader中把视线向量转到tangent space中
TANGENT_SPACE_ROTATION;
o.viewDir = mul(rotation,vd);
这里要提一下为什么要转到tangent space中,我们把视线向量v转到tangent中那么前文算出来的v'的x,y分量会和表面的切线、副切线对齐,这样这个表面无论如何旋转都没问题了,否则一旦表面被任意旋转以后,很难指出v'的x,y到底在哪儿。
然后在fragment shader中执行
float h = tex2D(_HeightTex,i.uv).a;
float2 uvOffset = ParallaxOffset(h,_Parallax,i.viewDir);
高度图在Unity中需要设置Alpha Source为From Gray Scale,这样Unity会把高度信息存入a通道,然后我们取a通道的值就好了。
ParallaxOffset
需要的第一个高度信息有了,第二个是自己可以控制的值,用来调整从高度图中取出来的高度,第三个就是视线向量了。
得出一个偏移值后我们再加上原来的uv值,就可以去采样纹理了:
i.uv += uvOffset;
fixed4 col = tex2D(_MainTex, i.uv);
float3 normal = normalize(UnpackNormal(tex2D(_NormalTex,i.uv)));
最终出来图4的效果
在讲浮雕贴图之前,我们来看看前辈们对视差贴图做过哪些优化。由于那个v'的x,y并不那么准确,所以扩展出了陡峭视差贴图(Steep Parallax Mapping)和视差遮蔽贴图(Parallax Occlusion Mapping)来求取一个比较好的结果。
陡峭视差贴图的算法:
1.找到视线V与第0层的交点T0,层深度是0.0,对应高度场值为0.75,因为该点在高度场上,所以找下一个点。
2.找到视线V与第1层的交点T1,层深度是0.125,对应高度场值为0.625,因为该点在高度场上,所以找下一个点。
3.找到视线V与第2层的交点T2,层深度是0.25,对应高度场值为0.4,因为该点在高度场上,所以找下一个点。
4.找到视线V与第3层的交点T3,层深度是0.375,对应高度场值为0.2,因为该点在高度场下,所以这就是我们要找的点。
视差遮蔽贴图的算法则和陡峭视差贴图原则相同,但在它基础上进行插值。
利用陡峭视差贴图得到最靠近交点的T2和T3后,根据这两者的深度与对应层深度的差值作为比例进行插值。
代码如下:
float2 ParallaxMapping(float height,float3 viewDir,float2 uv)
{
float layerNum = 10.0;
float eachLayer = 0.1;
float currentLayerDepth = 0.0;
float currentDepth = tex2D(_HeightTex,uv).a;
float3 v = normalize(viewDir);
float step = v.xy / v.z / layerNum * height;
float2 currentUV = uv;
while (currentLayerDepth < currentDepth){
currentUV -= step;
currentDepth = tex2Dlod(_HeightTex,float4(currentUV,0,0)).a;
currentLayerDepth += eachLayer;
}
//到此为陡峭视差贴图,加上后面为视差遮蔽贴图
float2 preUV = currentUV + step;
float preLayerDepth = currentLayerDepth - eachLayer;
float afterDepth = currentDepth - currentLayerDepth;
float beforeDepth = tex2D(_HeightTex,preUV).a - preLayerDepth;
float weight = afterDepth / (afterDepth - beforeDepth);
float2 res = preUV * weight + currentUV * (1 - weight);
return res;
}
值得一提的是viewDir.xy / viewDir.z,为什么要除以z分量呢?因为视线向量大致平行于表面时为了获取较大的v'而做的,这时的z接近于0,所以v'的x,y分量都会比较大。但也有人不除以z分量,因为在某些角度看会不好看所以不除,不除的话这这种技术就叫有偏移量限制的视差贴图(Parallax Mapping with Offset Limiting)。这里除不除看个人喜好了。
另外在shader里如果用了循环则循环内不能用tex2D
方法,用了会有报错unable to unroll loop, loop does not appear to terminate in a timely manner (1024 iterations)
。如果非要用那么要指定unroll的次数,比如[unroll(100)],把它放在循环开头就行。至于为什么我在stackoverflow问了,这是地址。说是tex2D
这个方法需要求导来确定LOD层数才能取样,放到循环里不指定循环次数的话就不能确定求导次数,所以报错。而tex2Dlod
指定了LOD层数,就是传入的float4
的w
分量,所以可以。至于答主说的“uniform control flow”我就不明白了,有没有大神明白的说一下。
对于浮雕贴图,大神浅墨用一句话概括为:“在shader里做光线追踪”。它的基本思想为步进法(Ray Marching)。
图5从图5(这也是侧过来看的高度图)中我们先得到视线与表面的交点a,再一步步步进到b,然后取ab的中点1,用1替掉b,再取1和a之间的中点2,用2替掉a,再取1和2的中点3,即我们想要的视线和高度图的交点。
但这样在某些情况下获取的点会不对,例如下图
通过上面的方法我们会取得点3,而点3其实看不见,我们能看见的应该是上面那个蓝点。
所以我们稍微修改下前面的思想,看图6
我们先步进到高度图内侧的点3作为起点,然后选取点2作为终点(选择依据是高度图外侧所有点的最后一个点,也就是开始点为a,一步步进到3,外侧最后一点为2),做二分查找(就是上面的找中点步骤)。一般来说,做八次二分查找已经够了。
In practice, we have found that eight steps of binary subdivision is sufficient to produce very satisfactory results. This is equivalent to subdivide the depth range of the height field
in equally spaced intervals.
代码如下:
float2 ReliefMapping(float h,float height, float3 viewDir, float2 uv)
{
float3 v = normalize(viewDir);
v.z = abs(v.z);
float3 startPoint = float3(uv,0);
v.xy *= height;
int linearStep = 40;
int binarySearch = 8;
float3 offset = (v/v.z)/linearStep;
for (int index=0;index<linearStep;index++){
float depth = 1 - h;
if (startPoint.z < depth){
startPoint += offset;
}
}
float3 biOffset = offset;
for (int index=0;index<binarySearch;index++){
biOffset = biOffset / 2;
float depth = 1 - h;
if (startPoint.z < depth){
startPoint += biOffset;
}else{
startPoint -= biOffset;
}
}
float2 res = startPoint.xy;
return res;
}
参考
Unity Shader - 表面凹凸技术汇总
视差贴图
视差贴图的作用
【风宇冲】Shader:二十一视差贴图
Real-Time Relief Mapping on Arbitrary Polygonal Surfaces
Fabio Policarpo - Relief Mapping with Correct Silhouettes
《ShaderLab实战开发详解》 18.4 Relief Mapping
网友评论