美文网首页Unity进阶技术
视差贴图(Parallax Mapping)以及浮雕贴图(Rel

视差贴图(Parallax Mapping)以及浮雕贴图(Rel

作者: 上善若水_2019 | 来源:发表于2019-09-29 15:22 被阅读0次

    上次介绍了一下法线贴图,这次这次介绍一下进阶版--视差贴图(Parallax Mapping)以及视差贴图的极限优化版--浮雕贴图(Relief Mapping)

    首先在这里提一下历史。1978年的时候,大神James Blinn(就是那位优化了Phong光照模型,将其改为Blinn-Phong的牛人)在"Simulation of Wrinkled Surfaces"中提出了凹凸贴图(Bump mapping),而到了1996年,法线贴图(Normal Mapping)作为凹凸贴图的一种实现产生了,由Venkat Krishnamurthy and Marc LevoyFitting 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 SourceFrom 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层数,就是传入的float4w分量,所以可以。至于答主说的“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

    图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 2^8 = 256 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

    相关文章

      网友评论

        本文标题:视差贴图(Parallax Mapping)以及浮雕贴图(Rel

        本文链接:https://www.haomeiwen.com/subject/geskuctx.html