Unity:製作可相交的視差遮蔽貼圖 Shader 續 (Parallax Occlusion Mapping Shader with Pixel Depth Offset)

前言

幾年前的文章曾有提到嘗試製作可相交的視差遮蔽貼圖(以下都簡稱 POM) Shader,然而當時一直留下一個疑問,就是計算深度偏移的量一直不是很正確。

本文新的實現方法,與幾年前的看起來相似,但是有穩定的深度位移

多年後終於有能力重新整理過一次,看了 Unity 的原始碼才發現當年已經離問題的所在很接近了,在後來版本的 Unity 裡面可以看到解決方法,在 Shader Graph Parallax Occlusion Mapping 這個 Node 的參數可以看到問題的所在。

POM 控制垂直偏移高度的參數(Unity 中為 Amplitude)在本文章使用的是物件空間座標,如果偏移高度參數是 0.6,代表著表面到最底部距離在物件空間的座標下相差 0.6 公尺。

 

原始碼

有關原始碼,請詳見 Github 專案。 

 

問題根源-如何計算逐像素的世界空間座標偏移量

這個問題關鍵就在於 UV 空間座標的 U軸 和 V 軸這兩個基向量在物件空間的長度。當要計算各個像素位移後的深度時必須知道各個像素在偏移後的世界座標是多少,然後再計算出深度。

提醒一點:U 軸 和 V 軸基向量在某些條件對應到切線與副切線,但不是絕對,因為 U軸 和 V 軸可以沒有正交,切線與副切線卻必須正交,這個限制了使用 POM 的模型不能有太多 UV 扭曲和法線調整

從 POM 的計算裡會得到射線(viewDir)與位移後表面交點的高度(Tp 點的高度),可以藉此高度值計算 T0 到 Tp 的位移向量 T0Tp,如果這個位移向量的長度正好就是世界空間的長度,那麼只需要將是世界空間的射線向量乘以這個長度,然而在將射線的向量轉換的過程中會令這個長度可能沒有符合世界空間的長度,所以要在轉換過程中確保轉換後的長度一致。

Unity 版本的視差遮蔽,由物件表面為 1 起始
Created by modifying "LearnOpenGL" (©  (Licensed under CC BY 4.0))
 

用來計算 POM 的射線一開始為世界空間的單位向量(viewDirWS),如果使用此向量,得到的偏移高度會是世界空間,無法跟隨物體縮放,於是一開始先除以物件的縮放,令偏移的高度會隨著物件縮放而變化。再來為了處理物件表面的朝向,將該向量轉換成物件表面的相對坐標系,也就是切線空間,此時向量(viewDirTS)長度並無變化,即使是轉換到切線空間其長度依然等同套用物件縮放後射線向量在世界空間的長度。

  1. float3 inverseObjectScale = GetInverseObjectScale();
  2. float3 viewDirTS = WorldToTangentParallaxVectorWithObjectScaling(worldToTangent,i.viewDirWS,inverseObjectScale); 

不過設想一個狀況,假設在下圖的方塊,假設 POM 中有一個射線從原點開始,最後在 (1, 0, 0) 與位移的表面相交,UV 空間偏移的向量長度是 1。在下圖的案例中,該立方體的尺寸是邊長 2 公尺。因此世界空間中這個偏移的向量長度卻是 2。

 

左側是立方體 UV 拆分的樣貌,每面都剛好佔滿 [0, 1],右邊則是該立方體在場景中的樣貌

從以上的例子會發現直接使用切線空間的射線(viewDirTS),會導致偏移的向量也就是 T0Tp 的長度隨著UV 空間的基向量 U 和 V 在世界空間的長度而拉伸。因此必須知道 U 和 V 軸基向量的長度才能糾正長度變化的狀況。此外還需解決 UV Tiling 的拉伸的問題,如此得到新的射線(viewDirUVSpace)

  1. float2 uvScale = _OffsetDistance * _AlbedoMap_ST.xy / _UVObjectSpaceLength;
  2. float3 viewDirUVSpace = TangentToUVSpaceParallaxVector(viewDirTS,uvScale);
剩下的就很簡單了,首先是用比值的方式求出 T0Tp 的長度(rayTravelDistance),然後再乘以世界空間的射線單位向量即可計算出世界空間座標的位移量。
  1. // distance from surface to intersection
  2. float rayTravelDistance = (1 - rayHeight) * _OffsetDistance / max(viewDirTS.z,0.000001f); // prevent division by zero
  3. float3 posOffsetWS = -i.viewDirWS * rayTravelDistance;

待研究

至此,基本上深度位移的計算就解決了,但是會發現一種狀況,UV 空間的基向量 U 和 V 的長度是一個很關鍵的參數,如何自動化去計算這就是個問題,還需要更進一步研究。

 

留言

這個網誌中的熱門文章

Unity:嘗試製作可相交的視差遮蔽貼圖 Shader (Trying to Create an Parallax Occlusion Mapping Shader with Pixel Depth Offset)

Blender:Geometry Node 應用於營造法式生成大木作構件初探-梭柱篇