视差贴图(Parallax Mapping)以及浮雕贴图(Relief Mapping)在Unity中的实现

上次介绍了一下法线贴图,这次这次介绍一下进阶版--视差贴图(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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容