基於頂點的霧效,由於計算的霧色最終是疊加到模型上渲染出來,無法表達出體積感。所以需要一種新的可以表達體積的霧效,我們把它稱之為體積霧。這篇文章主要是介紹下體積霧的一種實現原理,其中的方法和思路都是來自網上各種資料,結合我自己的理解整理而來。先看一下效果圖。
從圖上可以看出,由於兩座山體之間沒有任何物體,普通的頂點霧只能附着於模型上,也就是說只能在山體上產生霧效,無法產生上圖中的效果。對於普通的渲染來說,既然要在原本沒有物體的兩座山峰之間渲染出東西(霧效),那在這些空白的區域中一定是有物體存在的,只是不是把這些物體當做普通的模型來渲染,而是以某種疊加的方式進行處理,根據霧的濃度來疊加到山體上。下面就來說說體積霧的實現原理。
這是一張示意圖,用來把我們要處理的問題進行簡化,把復雜的模型用線條來表示,把三維空間用平面切面代替。藍色的斜線表示在體積霧中的一個物體(效果圖中的山峰)。點e表示眼睛所在的位置,也就是主攝像機的位置。外層的矩形表示體積霧的范圍,既然的體積霧那就必須是有體積的,所以在這個例子中,體積是一個矩形(三維空間中的立方體的一個切面)。從點e會發出無數條射線,穿過體積霧,這里我只畫出了其中一條。點c是射線和物體的交點。點a和點b表示射線和體積霧的兩個交點。t表示線段ac的長度,d表示線段bc的長度,f表示線段ae的長度。至此我們已經把模型簡化成了上圖的樣子,下面就是在此基礎上進行分析。需要申明的是,為了不把問題復雜化,我們先只考慮最簡單的情況,也就是上圖所示的物體完全在體積霧中。等把這種情況理解后再去思考其他情況就會容易許多。
當你看霧中的某個物體,這個物體的清晰度取決於這個物體離你有多遠。離得越遠看得越模糊,越近就越清晰。根據這個思路,在上面的示意圖中也可以找到這么一個對應的參數。這個參數就是t。當t為0的時候,點a和點c是重疊的,這是點c看得最清楚的時候。當t和d相等的時候,這時點c在矩形的中間,看點c就不那么清楚了。當d為0的時候,點c和點b是重疊的,這時應該完全看不到點c了(完全被霧擋住)。總結為一個公式。
k表示霧的濃度。其實這就是體積霧的全部思路的,非常簡單,下面就是要找到能夠表達出這整個過程的數學方法和程序方法。體積霧是要在 Shader 中實現,並且是要在 fragment 中實現。
點e,相機的位置,這個好辦,Unity 已經為我們定義好了這個值 _WorldSpaceCameraPos
,也可以自己處理把相機的世界空間坐標通過 uniform 值傳遞給 shader。
點a和點b,其實是在求射線和矩形的交點,這里介紹一種簡單快速的可以在gpu中實現的方法,來自網上的資料。具體詳見raytracing、gpurt和我以前的一篇學習筆記RaySlabIntersection。至此,我們就能求得f和s。
最后只要再求得點c,就能得到t和d的長度了。這里要用到的是 Depth Buffer。Unity 內置了獲取 Depth Buffer 的方法。
// 在 CSharp 腳本中開啟
Camera.main.depthTextureMode |= DepthTextureMode.Depth;
// 在 Shader 中獲取
sampler2D _CameraDepthTexture;
depth = tex2Dproj(_CameraDepthTexture, uv);
這里的uv坐標獲取有一點技巧,需要將頂點坐標映射到屏幕坐標,細節可以看下ComputeScreenPos和ProjectiveTexturing。獲取的depth還不能直接拿來使用,需要考慮到是否將深度值編碼到了rgb中,如果是那需要解碼,如果沒有,就直接從單通道中獲取並且將非線性的01空間映射到線性的視空間。當然 Unity 為我們封裝了這些工具函數,你不想關心這些細節也沒關系,直接調用即可。如果計算正確的話就可以得到線段l的長度。
至此,示意圖中的所有參數都已經齊全了,就是說上面的示意圖所示的體積霧已經可以實現了。然后就要處理其他的情況,比如說物體不再體積霧中的情況。如果上面所有的步驟都能很好理解的話,這應該不是難事。通過觀察我們完全可以找出任何情況下,f、t、d這三個參數之間的一些關系,先在 shader 中用 if 條件判斷實現功能,然后再考去掉條件判斷即可,這里就不展開細節了。
根據上文中的那個公式算出來的體積霧,是一個線性衰減的過程,如果需要非線性的衰減,可以通過一些數學技巧來達到目的。
最后說一下 Depth Buffer 的事情。上文中說到的是我們使用 Unity 提供的接口來獲取 Depth Buffer,其實完全可以自己來渲染 Depth Buffer,詳見CustomDepthTexture。這樣做是有一定的好處的,比如說有很多小物件我們並不需要渲染到 Depth Buffer 中,因為這種細小的深度值變化根本不會影響到最終的效果,這樣在渲染深度紋理時就能節省下一部分資源。