Ray Marching 101

題圖:John Seymour

Ray Marching其實可以理解為是一種Ray Tracing[1]的方法, 本篇要討論的是在GPU端實現. ShaderToy大量的Demo都是基於此項技術實現, 它的創始人I?igo Quilez[2]可以說是Ray Marching推廣的領軍人物. RayMarching的原理如下:

使用Paint 3D繪製

從觀察點發射一條射線, 順著這條射線每次延伸一段固定距離, 判斷是否碰撞到物體. 通常延伸的步數是有限的, 並且延伸的單步距離太大會導致穿透很薄的物體. 那麼有了射線, 我們如何去跟物體做碰撞檢測呢? 一般來說我們會使用SDF(Signed Distance Function)[3]. 下面我們來看一個例子:

UE4材質藍圖

圖中最右側Ray Marching是一個Custom節點,裡面的shader代碼如下:

float4 hitCol = float4(0,0,0,0);for(int currStep = 0; currStep < steps; currStep++){ if(distance(currPos, objCenter) < radius) { return float4(0,1,0,1); } else { currPos -= direction * stepSize; }}return hitCol;

然後把這個材質放到一個立方體上:

上面代碼中的:

distance(currPos, objCenter) < radius

其實就是一個Distance Function, 用來定義一個球體. 目前這個的渲染結果看起來平平無奇, 下面我們嘗試給它加下簡單的lambert光照.

Light = C_{L}	imes N cdot I_{L}

CL是顏色, N是法線IL是入射光方向. 從前面可以法線, 我們的Distance Function缺乏法線這種信息, Quilez[2]用的方法是使用梯度來近似估演算法線. 在估算的時候需要同一時刻獲得多個點的表面信息, 常規的shader直接寫個函數就好, 但是我們使用的是材質編輯器. 材質編輯器的問題在於他的custom節點命名是隨機的, 可能修改過或者添加一些節點後, custom節點的名稱發生變化, 所以你不能放進去多個custom按名稱去調用. 所以這裡我做了一點hack. 因為custom的生成只是簡單的文本展開,所以利用這個特性我可以寫一個dummy custom 節點, 在這個節點裡寫上所有我要用的函數, 然後把這個節點連接到我們要用的真正的Ray Marching 節點上.這就解決了函數會自動改變名稱的問題. hack節點的代碼如下:

return float4(0,0,0,0);}float map(float3 Pos, float3 ObjCenter){ return distance(Pos, ObjCenter);

這段代碼可以寫不止一個工具函數, 我只用了map(), 如果你需要的話可以寫更多, 記住最後一個函數最後的"}"要省略掉, 不然會報錯.另外關於光照的顏色和角度, 我們可以直接用內置的 AtmosphericLightColor / AtmosphericLightVector 節點, 同時我們可以用Opacity把沒用的像素"透掉". 下圖是調整後的材質藍圖:

RayMarching節點內的代碼目前是:

float4 hitCol = float4(0,0,0,0);for(int currStep = 0; currStep < steps; currStep++){ if(distance(currPos, objCenter) < radius) { const float est = 0.01; float3 n = normalize( float3(map(currPos + float3(est, 0, 0),objCenter) - map(currPos - float3(est, 0, 0),objCenter), map(currPos + float3(0, est, 0),objCenter) - map(currPos - float3(0, est, 0),objCenter), map(currPos + float3(0, 0, est),objCenter) - map(currPos - float3(0, 0, est),objCenter) ) ); float cof = max(dot(n, lightDir), 0); return float4(cof*lightCol,1); } else { currPos -= direction * stepSize; }}return hitCol;

放進引擎看效果:

簡單的Lambert光照

還可以擴展下Specular:

diffuse + Specular

------------------------ 根據建議,補完SDF部分-------------------------

目前為止, 我們只是渲染出一個簡單的球體. 隨著場景的複雜, 我們會遇到如下的場景:

來自GPU GEMS2, Chapter8

被檢測物體太薄, 導致穿透. 亦或是步進不夠, 又或者是步進太多, 浪費算力.

Sphere tracing[4]可以在每次步進時候, 基於多個SDF結果相交後給出一個"最優」的步進距離:

GPU GEMS2, Chapter8

SDF故名思意, 他的結果為正表示沒碰到目標, 為負則表示在目標內. 所以利用SDF做步進, 可以從效率上極大提高Ray Marching的效率, 之前的演算法, 固定step, 固定step size. 適用的情況下針對單個Box進行操作. 如果是更為常規的基於整個場景的Ray Marching, 則更適用下面的方法:

for(int currStep = 0; currStep < MAX_STEPS; currStep++){ float dist = SDFMap(); if(dist < Epsilon) { return TracedColor; } currPos += rayDir * dist;}

以上是利用SDF的偽碼, SDFMap是場景內SDF體與射線的碰撞檢測. 得到的結果類似與P1, P2這些點, 如果有碰撞就返回對應物體的著色信息, 沒有就繼續累加射線長度,直至碰到目標或者超出最大步數為止. 網上還有很多相關文章, 如果感興趣可以自行更加深入的探索. 本文的來源主要是當初在知乎想看看關於Ray Marching的信息結果搜不到一篇問答或者文章, 逐有意寫此文分享跟更多關注Ray Marching的朋友.


[1]Keven Suffern, Ray Tracing from the Ground Up, 2007.

[2]I?igo Quilez, NVSCENE 08, Rendering Worlds with Two Triangles with raytracing on the GPU in 4096 bytes, 2008.

[3]Malladi, R.; Sethian, J.A.; Vemuri, B.C. IEEE Transactions on Pattern Analysis and Machine Intelligence. 17 (2). Shape modeling with front propagation: a level set approach, 1995.

[4]Scratchapixel, Rendering Implicit Surfaces and Distance Fields: Sphere Tracing, 2016.


推薦閱讀:

[UE4]Indirect Lighting Cache(間接光照緩存)
steam發布篇---2
[UE4]資源非同步載入(Assets Asynchronous Loading)與內存釋放(Free Memory)
我們的UE4項目——PixArk上線了[廣告]
真良心大廠EPIC,頁游廣告又有新素材了!

TAG:計算機圖形學 | 虛幻引擎 | 光線跟蹤 |