從零開始的簡單光線追蹤示例

遊資網發表於2019-10-31
從零開始的簡單光線追蹤示例

英偉達推出的NVIDIA RTX技術,讓實時光線追蹤(Real Time Ray Tracing)進入商業遊戲成為可能。光線追蹤相比傳統的光柵化演算法更符合物理學,能帶來更加真實的渲染效果,同時因為核心原理其實並不複雜,並能夠制定一個通用的美術流程,相信RTX技術會在未來取得很大的進展。

在這篇文章裡我想用Unity實現一個最簡單的基礎光線追蹤效果。作為對光線追蹤技術的一次實踐。

1.光線追蹤的原理

光線追蹤希望能模擬光線在真實環境下的折射,反射,漫反射,間接反射等物理現象。如果我們能夠模擬整個場景光線的傳播,就能得到非常真實的影象,但如果直接從光源開始發射的光線經過傳播後並不一定會進入攝像機,造成了大量計算浪費。使用逆向追蹤——即從攝像機開始發射光線進行追蹤,將有效減少無效計算,即使如此,計算量依然是巨大的,如果每一個畫素點發射一條光線進行追蹤,並且追蹤10次折射或反射的話.生成一張1080P的圖,相交的計算量將是1920*1080*10=20 736 000,約兩千萬次光線追蹤運算。

單獨看一個畫素上的追蹤過程的話:

(1)從視線方向發射一條射線。

(2)取得射線與場景最近的交點。

(3)取得交點的材質顏色。

(4)如果材質包含反射或折射,則改變光線方向。

(5)尋找下一個交點,並重復(2)。直到在場景內找不到交點,或達到最大追蹤次數。

以上幾個步驟中,(2)步驟因為需要遍歷整個場景,開銷隨場景複雜度上升而上升。對於一些簡單的示例場景中,可以簡化為射線與球,或者射線與平面的相交。而在有模型的場景中,可以看作射線與場景中所有三角形進行相交,求最近的交點。RTX技術內建了求交運算方法,將效率提高很多。

看RTX的示例可以看到,光線追蹤除了能模擬最基本的直接光,間接光,反射和折射,還能模擬陰影,環境光遮蔽(AO),粗糙表面的散射等。物體表面並非理想的平面,散射存在一定隨機性,就會會產生噪點(Noise),要去除噪點,要麼如離線渲染那樣進行大量取樣,疊加消除噪點,要麼採用RTX一樣,使用人工智慧進行降噪。這一塊比較複雜,在我的示例裡就不涉及散射,表面會是都是理想的平滑表面。

2.光線追蹤的實現

基本設想是,搭建一個光線追蹤的場景,包含多種材質,包括反射,折射,自發光等。做出更好的寶石材質,並且能夠看到反射中的折射/折射中的反射等複雜情況。

搭建個測試場景

從零開始的簡單光線追蹤示例
鑽石模型

從零開始的簡單光線追蹤示例
三角寶石模型

首先一個問題是,Unity內建渲染佇列,不同材質的物體將在不同的DrawCall裡渲染。在頂點/片段著色器之中無法拿到所有物體的三角形,也沒辦法表示多種材質。

我自定義了一個陣列來儲存整個場景的三角形和材質資訊。通過VectorArray傳入著色器。在後處理時進行光線追蹤。

在片段著色器中,每個畫素都會發射一條沿著視線方向的射線,和整個場景求交,也就是對整個三角形陣列求交。

要檢測射線與三角形的相交,就需要一個射線與三角形的求交演算法,這裡參考了CSDN的文章

射線與三角面-CSDN部落格

對場景進行碰撞檢測,如果碰撞到任何三角形返回白色,否則返回黑色,就可以看到場景的色塊。

  1. if (HitScene(rayTemp, intersection, matIndex, inGeometry))
  2.     return 0;
  3. else
  4.     return 1;
複製程式碼
從零開始的簡單光線追蹤示例
直接渲染場景輸出幾何體的法線

從零開始的簡單光線追蹤示例
後處理碰撞到三角形返回白色

直接輸出碰撞點的法線。

  1. if (HitScene(rayTemp, intersection, matIndex, inGeometry))
  2.     return float4(intersection.normal,0);
  3. else
  4.     return 1;
複製程式碼

從零開始的簡單光線追蹤示例

從零開始的簡單光線追蹤示例
處理碰撞到三角形返回碰撞點法線

對光線進行反射或折射

  1. if (mat.reflectiveness != 0)
  2. {
  3.         rayTemp.direction = reflect(rayTemp.direction, normal.xyz);
  4. }
  5. else
  6. {
  7.         float refractIndex = dot(mat.refractiveIndex, channel);

  8.         refractIndex = intersection.inside ? refractIndex : 1 / refractIndex;

  9.         float3 reflection = refract(rayTemp.direction, normal, refractIndex);

  10.         if (dot(reflection, reflection) < 0.001)
  11.         {
  12.                 rayTemp.direction = reflect(rayTemp.direction, normal.xyz);
  13.         }
  14.         else
  15.         {
  16.                 rayTemp.direction = reflection;
  17.                 inGeometry = !inGeometry;
  18.         }
複製程式碼


從零開始的簡單光線追蹤示例
光線反射或折射一次效果

從零開始的簡單光線追蹤示例
光線反射折射三次效果

最後給物體賦予不同材質,地面是棋盤格紋理加反射材質;三稜錐使用反射材質;鑽石採用帶有色散效果的折射;三角形寶石採用祖母綠色反射材質,小三稜錐採用HDR自發光材質。

從零開始的簡單光線追蹤示例
給物體賦予不同材質

鑽石的折射色散效果,其實是因為RGB每個通道的折射率有略微不同,對三個通道分別進行光線追蹤,就能用很初級暴力的方式得到色散效果。

從零開始的簡單光線追蹤示例
逐通道取樣實現光線色散

給折射上顏色比較麻煩,因為必須要得到最後的顏色才能進行上色,而要上色的畫素很可能經過了好幾次反射或折射。ps_4_0不支援遞迴程式。最終我使用了一個ColorMask來儲存顏色的遮罩。當整個上色結束後再用遮罩進行上色。

從零開始的簡單光線追蹤示例
祖母綠寶石帶顏色的折射

至此,光線追蹤基本完成。

3.專案的優化

直接採用光線追蹤,意味著每次追蹤都需要對整個場景內的三角形進行求交,當場景變得複雜時造成幀率下降。

我在專案裡採用包圍球的方法,可以讓射線先對包圍球求交,如果與包圍球相交,再判斷是否與三角形相交,可以避免每次相交都要與整個場景的三角形求交。

此外,我還加入提早結束迴圈的判斷,與場景無交點時,或是顏色的Alpha值接近1時,不再進行後續相交操作。

當然,目前專案的效能並不算多好,如果有什麼優化的建議歡迎交流討論。

專案還加入了方便展示的幾何體的旋轉動畫,當滑鼠點選幾何體時,幾何體旋轉進行展示。

https://www.zhihu.com/video/1028757005678256128?autoplay=false&useMSE=

擴充:

1.關於自定義資料結構

專案使用了一個float4的陣列_Vertices來儲存整個場景的資訊。最初是儲存了整個場景三角形的所有頂點,並在第一個頂點的w分量儲存了材質索引。這時候陣列裡沒有幾何體的概念,只是一系列不同材質的三角形。但這就意味著每一條光線與場景求交就需要遍歷整個場景的三角形。

在優化的時候又加上了每個幾何體的包圍球資料,以及每個幾何體三角形頂點的長度。這時候就可以先判斷光線是否交到這個幾何體的包圍球,如果交到了再逐三角形進行求交。

從零開始的簡單光線追蹤示例
自定義陣列資料

這裡儲存的材質是材質索引,真正的材質資料定義在著色器內,使用材質索引找到相應的材質。

此外,三角形頂點在輸入到Shader之前變換到了世界座標系,這樣就只用在CPU進行一次座標變換。

2.關於運算量

看到評論區有小夥伴對光線追蹤的運算量感興趣。

上文提到了如果每個畫素進行10次光線追蹤,生成一張1080p的圖片,運算量將是1920*1080*10=20 736 000次光線的追蹤運算。

而每一次光線追蹤運算,在未優化的情況下,需要用一條射線與整個場景的三角形進行相交,並且找到一個距離最近的交點(如果有)。追蹤10次,每個畫素著色器就需要遍歷10遍整個場景的三角形。

這個運算量是非常龐大的,並隨著光線追蹤次數,以及場景內的三角形數提升而提升。

用我的示例舉例,為了實現鑽石的色散,我對rgb三個通道分別取樣,每個通道做了4次光線追蹤,場景裡一共208個三角形。

生成一張1080p的圖片,用一條射線和一個三角形相交的運算,不優化的情況下要使用

1920*1080*3*4=24,883,200

約2500萬條光線,進行

1920*1080*3*4*208=5,175,705,600

約5.2億次射線和三角形的相交運算

注意,這僅僅是一幀,如果想要達到比較流暢的效果要30幀,意味著這一切需要發生在0.033秒內,這段時間還要減去遊戲邏輯和物理運算,以及GPU準備的時間。

此外,208個三角形真的可以算非常低的面數了,當前一個手機遊戲的人物模型都可以達到上萬面。隨著面數的上升計算量也會上升。

當前這個例子還僅僅包含直接反射或直接折射的計算,不包含陰影,環境光遮蔽,間接反射,漫反射和降噪。

這巨大的計算量導致實時光線追蹤遲遲無法應用到遊戲上,也導致現在的動畫離線渲染每渲染一幀都需要經過長時間的計算。

NVIDIA RTX的演示視訊讓人看到了實時光線計算實際應用的希望,相信在不久的將來遊戲畫質將會有極大的飛躍。

希望這篇文章能對大家有幫助,也希望實時光線追蹤技術早日普及。

最後附上專案原始碼

https://link.zhihu.com/?target=https%3A//github.com/IceDustEl/SimpleRayTracing

作者:IceDust
專欄地址:https://zhuanlan.zhihu.com/p/45335463

相關文章