视差贴图(Parallax Mapping)
使用頂點光照的模型,當模型的面數很少的時候,光照效果會顯得很奇怪,因為只有頂點上的光照是正確計算出來的,三角面上的光照都是通過硬件插值得到,所以難免會出現問題。基于像素的光照可以很好的改善這個問題。如果想要表現出模型表面凹凸不平,那就需要很高的面數制作出凹凸的模型。
然后就出現法線貼圖。法線貼圖可以在低面數的模型上表現出高面數模型的很多細節。法線貼圖并不是把模型的面數提高了,而是使用法線貼圖中的法線來計算光照,通過明暗效果作假,讓觀察者誤以為模型有凹凸。法線貼圖只能在明暗效果上作假(模擬凹凸),無法控制表面的凹凸程度。即使我們使用圖像軟件強制調出一個凹凸非常明顯的法線貼圖,通過仔細觀察,會法線效果也是有問題的。
上面的是一張用了法線貼圖的地面,在紅色圓圈的地方是有問題的。我們使用一張平面圖來分析下。
這是一張凸起磚塊的截面圖,綠色箭頭表示視線的方向,白色線條表示磚塊的橫截面。按照常識來看,我們能看到磚塊的最遠的一點是藍色點,因為藍色點后面(紅色線條部分)的磚塊由于高度較低,被前面擋住了。但是從上面那張使用了法線貼圖的地面效果圖上可以看到,藍色點后的磚塊并沒有被擋住,甚至能夠看到黃色點的位置,這種效果顯然是不正確的。而這是法線貼圖無法避免的問題,因為上文已經說過了,法線貼圖只能模擬明暗,也就是說最多只能將紅色線條部分變暗(以此來模擬背光)。這就是視差貼圖可以解決的一個問題,它可以讓背面被遮擋住的部分完全不顯示出來,除此之外還能在一定范圍內調整磚塊凹凸的程度。視差貼圖也只是模擬作假,并沒有真的改變模型表面,下面就開始分析視差貼圖吧。
如圖所示,視線 e 落點是點 a,但是因為模型并不是真的有凹凸,而是一個平面,所以真實的落點是在點 b。這樣就變成了如何將點 a 糾正到點 b 的問題了。讓我們再在參考圖上加上一些輔助參數。
需要說明的是我們在分析視差貼圖的時候使用的是切線空間,這和法線貼圖是一樣的,切線空間中的切線和副切線是與紋理坐標 uv 對齊的,上圖中只顯示了 u 方向上的情況,在 v 方向上是一樣的。當前實際的落點是點 b,u 坐標是 ub,而理想的落點是在點 a,u坐標是 ua。如果能有一個 delta 量,把 ub 加上 delta 等于 ua,似乎就可以了。但是還有個問題是,因為視線的方向是一直在變化的,這就導致了 delta 量不可能是一個固定的值。所以暫且沒有什么好的辦法求出 delta,那么就把問題想簡單點。這里不要求精確的 delta,只要近似的就可以。于是有了一張稱為高度圖的紋理,它存儲了點 b 在切線空間的真實凹凸表面的凹陷或凸起程度。黑色(0)表示不凸起,白色(1)表示完全凸起。我們可以試著使用這個值來最大可能的近似模擬出 delta 值。
// 計算 uv 的偏移 delta
inline float2 ParallaxUvDelta(v2f i)
{
// 高度圖中描述的高度數據
half h = tex2D(_ParallaxMap, i.uvMain).r;
// 切線空間中的視線方向
float3 viewDir = normalize(i.viewDir);
// 將三維的視線向量投影到二維的 uv 平面,乘以高度數據
// _ParallaxScale 是一個用戶可調節的值,根據效果需要進行調節,數值太大造成視覺上的嚴重錯誤
float2 delta = viewDir.xy / viewDir.z * h * _ParallaxScale;
return delta;
}
float2 uvDelta = ParallaxUvDelta(i);
i.uvMain += uvDelta;
i.uvBump += uvDelta;
以上就是如何利用高度圖計算 uv 的偏移量的代碼了。需要注意,因為這只是近似模擬,所以能否得到完美的效果完全取決于各個參數調整的是否合理。其實上面的代碼只是一個框架,在此基礎上可以試著對 h,viewDir.z 這些參數進行一定的偏移,或許能夠得到更好的效果。完成后的效果圖如下,可以看到磚塊就像真的凸起了一樣,上文法線貼圖紅圈指出的問題也沒有了。
不足之處是你無法讓磚塊無限制的凸起,當到了某個臨界值后,效果就完全穿幫了。
由于 uv 的 delta 偏移量是一個估計值,并不是精確值,所以才會出現這樣的情況。下面我們的目標就是讓 delta uv 盡可能的精確。上面的方法中,我們只對高度圖進行了一次采樣,很難一下就找準落點,顯然一次是不夠的,因此需要對其進行改進,我們將使用逐步逼近的方式,來獲得一個更精確的 delta uv。
從圖中可以看出:最上層的高度值為1,最下層的高度值為0,對中間值劃分為四等分(劃分得越細,最終計算出來的精度就越高,效果也就越好,當然計算量也越大),這些值和高度圖中的值是對應的。視線 e 會和等分線產生交點(紅點),直接使用交點的 uv 對高度圖進行采樣,會得到對應的幾個高度值(藍點)。最理想的情況下計算出來的結果是正好在黃點上,觀察下紅點和藍點,在黃點左邊的藍點高于紅點,在黃點右邊的紅點高于藍點,我們可以通過這個規律找到位于黃點兩邊最近的兩個紅點和藍點。這樣就可以確定黃點就在這兩個紅點的中間。最后,沿著 e 的方向,在這兩個紅點之間進行插值,即可獲得黃點的位置了,而插值需要用到 h1 和 h2 這兩個線段的長度(紅藍兩點的間距)。差值的精確度和一開始劃分的精細度有關。這就是原理描述了,下面解釋下代碼。
inline float2 ParallaxUvDelta(Input i)
{
float3 viewDir = normalize(i.viewDir);
// 細分的層數
const float numLayers = 20;
// 單層步進的高度
float layerHeight = 1.0 / numLayers;
// 最高的高度值
float currentLayerHeight = 1.0;
// delta 最大值
float2 P = viewDir.xy * _ParallaxScale;
// delta 單步逼近值
float2 deltaTexCoords = P / numLayers;
// 開始一步步逼近,直到找到合適的紅點
float2 currentTexCoords = i.uv_MainTex;
float currentDepthMapValue = tex2D(_ParallaxMap, currentTexCoords).r;
while(currentLayerHeight > currentDepthMapValue)
{
currentTexCoords -= deltaTexCoords;
currentDepthMapValue = tex2D(_ParallaxMap, currentTexCoords).r;
currentLayerHeight -= layerHeight;
}
// 計算 h1 和 h2
float2 prevTexCoords = currentTexCoords + deltaTexCoords;
float afterHeight = currentDepthMapValue - currentLayerHeight;
float beforeHeight = currentLayerHeight + layerHeight - tex2D(_ParallaxMap, prevTexCoords).r;
// 利用 h1 h2 得到權重,在兩個紅點間使用權重進行差值
float weight = afterHeight / (afterHeight + beforeHeight);
float2 finalTexCoords = prevTexCoords * weight + currentTexCoords * (1.0 - weight);
return finalTexCoords - i.uv_MainTex;
}
float2 uvDelta = ParallaxUvDelta(i);
i.uvMain += uvDelta;
i.uvBump += uvDelta;
效果圖。Parallax Mapping 從性能上來說消耗是很大的,所有的操作都是像素級別的,并且其中包含大量的紋理采樣,根據需要使用。
最后,關于 Shader 中編寫循環指令的代碼,并且循環指令無法在編譯期間展開,比如 while(condition) for(condition),使用 cg 語言編寫的話,是無法通過編譯的,因為被編譯出的中間 ARB VP/FP 不包含循環指令。所以需要添加上 ‘#pragma glsl’,讓編譯器直接編譯成 glsl。
goto blog
總結
以上是生活随笔為你收集整理的视差贴图(Parallax Mapping)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 对称密钥
- 下一篇: Vue 渐进式的理解