Unity:嘗試製作可相交的視差遮蔽貼圖 Shader (Trying to Create an Parallax Occlusion Mapping Shader with Pixel Depth Offset)
警告,本文只是分享摸索的過程,並不是完善的技術文。
此文在計算深度位移有錯誤,後續有所更新,詳見此文
一般的視差貼圖的做法是依照切線空間的視線相量位移貼圖採樣的 UV 座標而產生立體感,物件本身的幾何形狀並未更改,因此各個像素的深度也是原有形狀的位置,因此相交時無論畫面上的物體因為視差貼圖產生凹凸的樣貌為何,相交還是依照原有的幾何形狀,而接收物件陰影投射亦有相同狀況。(如下圖)
Unity 中的 HDRP 中內建的 Lit Shader 提供的像素深度位移(Pixel Depth Offset)的功能,可以讓各個像素的深度位置依照視差貼圖位移的量產生位移,進而讓視差貼圖凹凸的效果也能對應到物件的相交上。(如下圖)
需要提到的是 Unreal 引擎的 Shader 也有同個功能,但無論是 Unity 和 Unreal 引擎的做法我都無法取得夠詳細的資料,雖然 Unity 的 Lit Shader 可以找到原始碼,但是太複雜了,目前暫時還是沒有辦法理清做法。(゚∀。)
為了產生正確的物件相交與陰影投射勢必要計算出正確的深度,那麼怎麼計算正確的深度呢?我的做法就是直接將像素的世界空間座標依照凹凸的位移量計算出位移後的世界座標,然後再計算剪裁空間的座標,然後就得出深度了。
需要說的是我採用的視差貼圖技術是視差遮蔽貼圖(Parallax Occlusion Mapping,POM) ,原先是在 Shader Graph 中用 Custom Function 節點製作的,結果如下方的影片。
參考了 Unity 的做法,使用的是高度圖而非深度圖,因此高度(深度)最低為0,最高為1,需要特別注意。
取得視差位移後的深度首先第一步是求出視差位移後的高度(深度)。這個很簡單,回傳位移後的 UV 時一並回傳目前的高度(深度),如下圖可以看到高度最低至最高由 0 到 1 灰階漸層。
一旦取得高度後便可以取得世界空間的座標位移量,算法如下。
offsetDistance 在本文中指的都是最大的凹凸位移量。
- float3 worldPosOffset = viewDirWS * height * offsetDistance;
最後將世界空間的像素座標加上位移量後轉換到剪裁空間後得出像素在視差位移後的深度值,將深度值寫入(
後即可正常的產生相交的效果。
輸入至SV_Depth
)
視差位移後的深度值算法如下。
- positionWS += worldPosOffset;
- float4 positionCS = TransformWorldToHClip(positionWS);
- float depth= positionCS.z / positionCS.w;
到此應該就能夠產生正確的相交效果,然而某些狀況會發現問題,像是渲染出來的凹凸深度與相交的深度不吻合。如下圖的狀況,相交的深度位移量比實際渲染出來的小非常多。
這是因為UV被拉伸的關係,因為UV拉伸會影響視差貼圖UV的位移量,拉伸UV的比例越大在畫面上UV的位移量也會被拉伸更大,但是計算世界空間座標的位移量卻沒有跟著縮放的結果。Unity HDRP的Shader也會有相同狀況,如果不處理深度位移這種狀況不會造成困擾。
頂點UV座標的變化量必須等同世界空間座標的變化量,而且不會隨著物件縮放而改變這個比例,也就是1:1。要避免 UV 拉伸造成深度計算錯誤的方法最好是使用尺寸 1 * 1 單位的平面,而且各頂點的 UV 剛好範圍落在 [0,1] 的區間內, Unity 預設的 Cube 是最好的例子。
Plane則否,因為Plane的尺寸是10 * 10 單位,UV座標卻是落在[0,1]間,變化量只有10:1,這樣算出來的深度位移量會變成十分之一,這種狀況下必須手動將深度的位移量乘10才是正確的深度位移量。因此如果UV或物件有拉伸縮放導致UV變化量與世界空間的座標變化量不等於1:1時有兩種做法。
- 縮放 UV 的視差位移量:將 UV 的視差位移量鎖定成世界空間的長度,不隨 UV 拉伸與縮放改變。
- 縮放深度的位移量:UV 拉伸時跟著縮放深度的位移量, UV 的視差位移量隨 UV 拉伸與縮放改變。
- float CalculateUVOffsetDistance(float offsetDistance)
- {
- float3 objectScale = float3(length(float3(UNITY_MATRIX_M[0].x, UNITY_MATRIX_M[1].x, UNITY_MATRIX_M[2].x)),
- length(float3(UNITY_MATRIX_M[0].y, UNITY_MATRIX_M[1].y, UNITY_MATRIX_M[2].y)),
- length(float3(UNITY_MATRIX_M[0].z, UNITY_MATRIX_M[1].z, UNITY_MATRIX_M[2].z)));
- return offsetDistance / max(objectScale.x, objectScale.z);
- }
如何處理投射陰影的部分只需要額外寫一個渲染 ShadowMap 的 Pass 即可。採樣投射的陰影時則使用位移過後的世界空間座標即可。
因為視差位移貼圖本質上是下凹的凹凸貼圖(本文雖然使用高度圖也一樣),所以地平面會變成向法線相反方向偏移,為了處理物理碰撞會出現浮在空中的狀況,我將頂點向法線方向推出同樣距離以抵銷偏移。
到此,目前摸索可相交視差遮蔽貼圖 Shader 的過程告一段落。
留言
張貼留言
₍₍ς(OωO ς)⁾⁾