基本概念
在進入本文前有一些基本的名詞需要熟悉:
- 加速結構(Acceleration Structure):用來加速計算射線與場景物件相交的資料結構,在本文的程式碼中以AS表示。
- -射線查詢(Ray Query):建立在傳統的圖像管線(Graphic Pipeline)的一個光追擴建,通常運用在計算著色器(Compute Shader)或像素著色器(Pixel Shader/Fragment Shader)中,在本文的程式碼中以rayQuery表示。
- 射線(Ray):在射線查詢中的射線是以射線起點(Ray Origin)、射線方向(Ray Direction)、射線標誌(Ray Flag)來描述一個射線的特性
射線起點(Ray Origin):射線的起點,在本文的程式碼中以rayOri表示。
射線方向(Ray Direction):射線的方向,在本文的程式碼中以rayDir表示。
射線標誌(Ray Flag):用來描述射線特性的標誌,在本文的程式碼中以rayFlags表示。
問題描述
使用射線查詢實現陰影的方法會從著色點為射線的起點向光源方向打出射線來判斷遮蔽,這種打向光源的射線稱為陰影射線(Shadow Ray),在Khorno部落格的射線查詢段落有簡單地介紹了實作方法:
|
這樣的實作方法是射線查詢的標準實作方法,在邏輯上它很完整地判斷了與射線相交的物件是否為不透明物,並根據其判斷來更新射線查詢的相交保證(committed intersection),然而在實際應用上這種完整的邏輯卻是會拖累GPU在執行光追計算的性能。由於陰影射線的一些特性,在射線查詢的實作上是可以被優化的,文章的下一段會仔細說明這優化方法以及其優化的預期幅度。
陰影射線的射線查詢優化
使用射線查詢實作陰影的邏輯是建立在兩個概念上:
- 陰影的產生是由非透明物遮蔽光源所造成的
- 場景中的透明物不會造成遮蔽
基於這兩點,我們可以透過設置射線標誌來改變光追計算的行為,讓使用射線查詢時我們只要針對非透明物與射線的相交判斷就行了,藉以優化陰影射線。在射線標誌的設置上除了gl_RayFlagsTerminateOnFirstHitEXT,我們還要再加上gl_RayFlagsCullNoOpaqueEXT:
uint rayFlags = gl_RayFlagsTerminateOnFirstHitEXT | gl_RayFlagsCullNoOpaqueEXT;
射線標誌gl_RayFlagsCullNoOpaqueEXT代表硬體在執行光追計算時會忽略透明的物件,僅針對非透明物件去做計算,如此便能減少光追計算的數量。
另一個可以新增的射線標誌是gl_RayFlagsSkipAABBEXT,由於陰影的形成在邏輯上是由非透明物件的遮蔽所產生,這些物件都是由三角片所形成,所以在相交的判斷上我們可以忽略掉AABB(Axis Align Bounding Box),只去判斷射線與三角片的相交就行了:
uint rayFlags = gl_RayFlagsTerminateOnFirstHitEXT | gl_RayFlagsCullNoOpaqueEXT | gl_RayFlagsSkipAABBEXT;
下一個可以優化的地方就是rayQueryProceedEXT(...)的while迴圈部分,前面我們已經對射線標誌設置了gl_RayFlagsCullNoOpaqueEXT和gl_RayFlagsSkipAABBEXT,這時射線查詢所找到的相交都會是非透明三角片,因此不用再去判斷相交的物件是否透明或是否為三角片,這代表while迴圈裡面的判斷式是可以捨棄的:
while(rayQueryProceedEXT(rayQuery)) {}
另外由於陰影的形成是只要有非透明三角片的遮蔽就會產生陰影,不需要去考量是否為最近的非透明三角片,會在因此rayQueryProceedEXT(...)只需要呼叫一次,之後再直接檢查是否有相交就行了:
rayQueryProceedEXT(rayQuery);
在經過前面的優化後,最後的陰影射線實作會變成:
rayQueryEXT rayQuery;
uint rayFlags = gl_RayFlagsTerminateOnFirstHitEXT | gl_RayFlagsCullNoOpaqueEXT | gl_RayFlagsSkipAABBEXT;
rayQueryInitializeEXT(rayQuery, AS, rayFlags, cullMask, rayOri, tMin, rayDir, tMax);
rayQueryProceedEXT(rayQuery);
if (rayQueryGetIntersectionTypeEXT(rayQuery, true) == gl_RayQueryCommittedIntersectionNoneEXT)
{
// Not shadow!
}
else
{
// Shadow!
}
優化成果
我們用優化前與優化後的方法分別實作了陰影射線的範例,如圖1.所示:
圖1. 陰影射線實作範例
優化前與優化後的差異只有在著色器程式碼的部分有所不同,優化前在陰影射線的實作上會在rayQueryProceedEXT(...)的while迴圈中讀取相交物件的材質紋理判斷與射線相交的這個物件是否為非透明的,優化後的陰影射線實作就是前一段最後面的程式碼。我們測量了兩者間的FPS,範例的FPS對時間的曲線圖如下:
圖2. 陰影射線FPS對時間曲線圖
從圖表中的"Bad Practice"代表的是優化前的陰影射線實作,"Best Practice"則是優化後的,我們可以觀察到優化前的陰影射線平均FPS大約落在0.85左右,而優化後的平均FPS可以達到85左右,可以說優化後的整體性能提升了大約100倍,在性能上的優化是十分顯著的。
結論
使用射線查詢實作陰影射線時可以運用以下的手段優化性能提升FPS:
- rayFlags使用gl_RayFlagsTerminateOnFirstHitEXT、gl_RayFlagsCullNoOpaqueEXT,以及gl_RayFlagsSkipAABBEXT
- 在rayQueryInitializeEXT(...)後面只需要一次的rayQueryProceedEXT(...)後可以直接判斷有無相交來決定遮蔽
光追透明物體陰影優化
概述
在渲染半透明、帶有孔洞的物體如樹葉、草叢等,為了減少模型的複雜度(三角形數量),經常使用 alpha map 將物體的透明度儲存於紋理中。
本章將講解光追陰影常見的做法,解析渲染處理帶有 alpha map 物體陰影的做法,以及可以提升3倍效能(FPS 26 -> 97)的的優化方法。
Before |
Optimized |
After |
|
陰影渲染
RayQuery 計算陰影經常使用以下代碼:
來源: khronos - VK_KHR_ray_query
|
由於計算陰影時,我們只需要關注物體表面與光源之間,是否存在遮擋,並不在意遮擋物體的遠近。
因此我們可以在 rayQueryInitializeEXT 的標誌參數加上 gl_RayFlagsTerminateOnFirstHitEXT,使 rayQuery 在與物體相交後就結束,而不是直到找到最近相交為止,減少檢查相交的三角形數量,提升光追效能。
但射線與我們的物體相交並不一定代表該點為陰影,需要額外採樣 alpha 值才能決定是否為陰影。
Figure 1. Khronos的範例代碼缺少參考 alpha 值資訊,而渲染出不正確的陰影。 |
修正錯誤陰影
- alpha 採樣:
由於 3D 模型實際上並沒有孔洞,在射線相交後,我們必須另外採樣紋理中的 alpha 值判斷該點是否為透明。- alpha == 0 : 表示此射線交點為透明,我們將此射線視為沒有交點。
- alpha != 0 : 表示此射線交點為非透明,表示與光源間有被物件遮擋。我們可以根據需求計算 alpha 持續追蹤射線,或是判斷為陰影並停止繼續追蹤射線。
- 持續光追:
為了讓我們的光追持續追蹤直到 alpha 值 > 1.0,我們將 rayQueryInitializeEXT 的標誌加上 gl_RayFlagsNoOpaqueEXT,強制將加速結構中的物體都視為透明物體,以避免 rayQueryProceedEXT 呼叫一次就結束。
修正後的代碼:
|
Figure 2. 修正後的透明陰影 |
效能優化
- 在 rayQueryInitializeEXT 盡可能地使用不透明標誌:
光追在尋訪加速結構時,若相交物體為不透明,GPU 會自動更新 hit T 值,並找到最近的設為 comitted 結束光追的計算;若相交物體為透明,GPU 將需要中斷運算,回傳 candidate,等待後續使用者判斷該 candidate 是否為 comitted,才能進行下一步的運算,使得不透明光追有較差的效能表現。
我們將 rayQueryInitializeEXT 的標誌由 gl_RayFlagsNoOpaqueEXT 改為 gl_RayFlagsOpaqueEXT,強制將加速結構中的物體都視為不透明物體,以獲得更佳的效能。 - 再次初始化 rayquery:
由於我們使用了 gl_RayFlagsOpaqueEXT 標誌,rayQueryProceedEXT 在執行一次後必定會回傳 false 結束。若我們要繼續光追則需要給訂新的起點,再次呼叫 rayQueryInitializeEXT 重新初始化 rayquery。
另外,由於 rayQueryProceedEXT 一次就會結束,若加上了 gl_RayFlagsTerminateOnFirstHitEXT ,無法保證得到的焦點是最近焦點,我們將無法確定下一次光追的射線方向,因此這裡我們必須捨棄 gl_RayFlagsTerminateOnFirstHitEXT 標誌以確保得到最近的交點。 - 在 rayQueryInitializeEXT 加入 gl_RayFlagsSkipAABBEXT 標誌:
若我們的加速結構中沒有加入 AABB 物件或是我們不需要判斷與 AABB 相交,我們可以加上 gl_RayFlagsSkipAABBEXT 做更進一步的優化,讓相交檢測直接跳過 AABB 物件。 - 在 rayQueryProceedEXT 過程中使用帶 Lod 參數的貼圖採樣函數:
由於 rayquery 相交的表面不一定等於當前 fragment 的表面,且鄰近像素光追焦點不一定是同一表面,因此光柵化自動帶入的 texture lod 資訊( ddx/ddy )是無效的,我們可以根據需求,自訂 lod 運算或是給一定值。 - 限制最大光追次數:
雖然會些許的降低陰影的質量,但為了避免 rayQueryProceedEXT 執行的次數過多,我們可以設定光追的最大次數 max_shadow_ray_depth = 3,當光追次數超過設定的最大次數就跳出迴圈停止光追。
光追的最大次數越多陰影的質量越高,反之則效能越好,我們可以根據應用場景調整此數值。
其中 for 迴圈最大次數若能在 compile time 確定,compiler 將有機會做進一步的 unroll, vectorize 等優化,提升效能。
優化後如以下代碼所示:
|
Figure 3. 效能優化,不限制最大光追次數 |
Figure 4. 效能優化,限制最大光追次數為2次 |
Figure 5. 效能優化,限制最大光追次數為3次 |
總結
- 避免光追不透明物體,將標誌設為 opaque :
其中 opaque 可以在以下三處進行設定,優先度由上而下,上層設定將會覆蓋下層設定:- Shader 中初始化 rayquery 時的 rayQueryInitializeEXT
- 建立 TLAS 時的 VkAccelerationStructureInstanceKHR::flags
- 建立 BLAS 時的 VkAccelerationStructureGeometryKHR::flags
- 成對呼叫 rayQueryInitializeEXT 與 rayQueryProceedEXT,避免一次 rayQueryInitializeEXT 多次 rayQueryProceedEXT
- rayQueryInitializeEXT 不需要判斷 AABB 相交時在 rayFlag 加上 gl_RayFlagsSkipAABBEXT
- rayQueryInitializeEXT 不需要最近交點時在 rayFlag加上 gl_RayFlagsTerminateOnFirstHitEXT
數據展示
Figure 6. Khronos範例代碼 |
Figure 7. 修正透明陰影 |
Figure 8. 效能優化,不限制最大光追次數 |
Figure 9. 效能優化,max_shadow_ray_depth = 2 |
Figure 10. 效能優化,max_shadow_ray_depth = 3 |
版本 |
FPS |
註記 |
Khronos範例代碼 |
120+ (limited by flash rate) |
雖然有最好的效能,但是渲染錯誤的陰影。 |
修正透明陰影 |
26 |
正確的陰影,但效能不佳。 |
效能優化 |
95 |
正確的陰影,比修正後有3倍以上的效能優化。 |
效能優化 |
106 |
限制最大光追次數2次,降低陰影的質量以提升效能。 由左圖可見最大次數2次的陰影品質不佳。 |
效能優化 |
97 |
限制最大光追次數3次,降低陰影的質量以提升效能。 最大次數3次的陰影品質尚可。 |
光追環境遮蔽 (RTAO)
簡介
起因於光追間接光照仍為一項計算複雜度高且耗時的效果,而光追環境遮蔽為一種間接光照的近似簡化,目標在模擬物體表面被附近幾何遮蔽的效果,背後原因為較少光線可以反射進入眼睛,故該區域看起來會較暗,常見於物件與物件間的邊角 (見Figure 1-1.)。
Figure 1‑1. 左圖為光追環境遮蔽結果;右圖為疊合結果 (包含光追軟陰影)。圖片來源: 暗區突圍。
實作上,以可見的表面頂點 (origin) 為中心,和該點的法向量 (normal) 為軸心,向其上半球採樣一個方向 (見Figure 1-2. 的虛線半圓),然後平均測試半徑 (aoMaxRadius) 內沿該方向有被幾何遮蔽的次數,最後其值為光追環境遮蔽,並可由一個參數與該值相乘以調整效果的明顯程度。
Figure 1‑2. 實作光追環境遮蔽示意圖。圖片修改自: LearnOpenGL。
對應範例虛擬碼可參考 Section 1.2.1。
優化建議
設定推薦rayQueryInitializeEXT相交測試與半徑以節省時間
在shader實作中 (如Table 1-1.),設定rayQueryInitializeEXT的tMax為最大有效環境遮蔽半徑,即可直接以 rayQueryGetIntersectionTypeEXT判斷回傳型別,決定是否在指定範圍內有被遮蔽,以省去事後由 rayQueryGetIntersectionTEXT取得距離再判斷有無效,並加入gl_RayFlagsTerminateOnFirstHitEXT至rayFlags,可進一步簡化判斷只要在指定範圍內有任意相交測試成立即可,且不須找到最近的相交結果。
Table 1‑1. 光追環境遮蔽範例虛擬碼,以下左右格必較兩種寫法。
float ao; rayQueryEXT rq; rayQueryInitializeEXT(rq, tlas, gl_RayFlagsOpaqueEXT, cullMask, origin, tMin, direction, tMax); rayQueryProceedEXT(rq); uint res = rayQueryGetIntersectionTypeEXT(rq, true); float t = rayQueryGetIntersectionTEXT(rq, true); if (res == gl_RayQueryCommittedIntersectionTriangleEXT) { // Hit something within the interval ao = float(t > aoMaxRadius); } else { // Nothing ao = 1.0; } |
float ao; rayQueryEXT rq; rayQueryInitializeEXT(rq, tlas, rayFlags | gl_RayFlagsTerminateOnFirstHitEXT, cullMask, origin, tMin, direction, aoMaxRadius); rayQueryProceedEXT(rq); uint res = rayQueryGetIntersectionTypeEXT(rq, true); if (res == gl_RayQueryCommittedIntersectionTriangleEXT) { // Hit something within the interval ao = 0.0; } else { // Nothing ao = 1.0; } |
---|
另外,在一固定測試場景下,比較兩種寫法的counter (如Figure 1-3.),可以觀察到仍是使用相同的採樣數 (如Figure 1-3. 中Rays started counter),但右邊的寫法所需要的相交測試次數減少約50%,且每幀時間也下降約50%。
Figure 1‑3. 對應Table 1-1. 左右格寫法的counter,亦是對應左右圖。
避免實作透明效果以節省計算負擔
對透明物,如植被等,需做透明採樣並逐一測試一條光線所有可能的相交測試,如Figure 1-4.,會使得效能有兩倍以上的負擔,因此強烈建議在shader使用rayQueryInitializeEXT的ray flag皆為gl_RayFlagsOpaqueEXT,直接將場景所有幾何接當作非透明物體,以省去上述的透明值檢查,但會犧牲原本透明物所產生的效果,如樹葉的光追環境遮蔽不符合葉片形狀,可參考針對透明物混合使用SSAO等方式以減輕其視覺影響,如AMD GPUOpen FidelityFX CACAO。
Figure 1‑4. 當實作透明效果時,需對一條光線上多個相交的三角片,採樣其透明值以判斷有無有效之相交結果。圖片修改自: NVIDIA ORCA: SpeedTree。
rayQueryInitializeEXT之方向單位長影響rayQueryGetIntersectionTEXT之回傳距離大小
注意採樣的方向 (舉例Table 1-1. 中rayQueryInitializeEXT的direction) 是否有正規化,否則其會影響由rayQueryGetIntersectionTEXT回傳距離 (t) 的單位,在Figure 1-5. 比較有無正規化從相機到場景表面頂點的方向,可以看到Figure 1-5. 左圖的遠近關係符合,遠的場景顏色接近白色 (值為 1.0),而近的場景顏色接近黑色 (值為 0.0),但在Figure 1-5. 右圖則不能完全符合。
Figure 1‑5. 左圖有正規化方向後對距離除以25.0;右圖無正規化方向直接視覺化距離。
設定gl_RayFlagsCullBackFacingTrianglesEXT對所需相交測試數量無顯著增加
如同傳統光柵化渲染可以設定去除反面的三角形,在rayQueryInitializeEXT中亦可加入gl_RayFlagsCullBackFacingTrianglesEXT至rayFlags中,不將反面的三角形當作有效的碰撞,主要針對效果畫面而使用,對效能則較無影響,可比較沿用Figure 1-5. 視覺化距離的範例,從Table 1-2. 觀察到Ray misses與Opaque triangle hits的消長量相似,代表仍需要做相似的相交測試次數,但可以在效果上達到去除反面,消長量仍會受場景幾何所影響。
Table 1‑2. 有無使用gl_RayFlagsCullBackFacingTrianglesEXT的差異。
rayFlags |
FPS / frame time (ms) |
BW/f (R / W MB) |
Rays started/f |
Box nodes tested/f |
Tri batches tested/f |
Opaque triangle hits/f |
Ray misses/f |
w/o |
101.76 / 9.83 |
6.87 / 1.87 |
227,821 |
521,273 |
139,498 |
1,024,056 |
3,024,020 |
w |
103.16 / 9.69 |
7.01 / 2.10 |
227,903 |
521,515 |
139,562 |
972,139 |
3,077,956 |
Difference |
+1.4 / -0.14 |
+0.14 / +0.23 |
+82 |
+242 |
+64 |
-51,917 |
+53,936 |
對於光追環境遮蔽和軟陰影節省每幀所需之採樣數量
直接降低光追環境遮蔽的解析度,使得減少的計算量與降低的解析度平方成反比,但可能會造成還原到原解析度時有模糊的瑕疵,對於較低頻資訊影響較小,如光追環境遮蔽和軟陰影 (如Figure 1-6.),或進一步使用 checkerboard sampling (請參考Microsoft D3D12 範例) 或adaptive sampling (請參考RT Gems Ch 13.) 在有限採樣數量下得到較少噪點和更好的去噪的效果。
Figure 1‑6. 左圖為原解析度下的光追環境遮蔽 (紅色) 和軟陰影 (綠色);右圖為降低1.5倍後還原的結果。圖片來源: 暗區突圍。
結合光追軟陰影實作案例對時間節省之影響
接下來的實驗討論對於同一shader實作光追環境遮蔽和軟陰影,以及分開對效能之影響。在固定GPU頻率下,合併光追環境遮蔽和軟陰影會使得在Figure 1-7. 的測試場景比分開兩者實作取得約6ms的縮短,因兩種效果皆是存取相同的AS以及相同的光線原點計算方式,所以可能因此共用部分初始化的處理,然而渲染目標 (render target) 的用量從兩張合為一張,且仍須根據個別案例實際測試驗證。
Figure 1‑7. 結合光追軟陰影實作的測試場景。圖片來源: 暗區突圍。
在Figure 1-8. 中可觀察到總共的Rays started是相似的,因為都是要用一樣的算法產生光追環境遮蔽和軟陰影,所以仍然需要一樣造訪AS來作相交測試的數量,但是Fragment warps和Instructions皆有下降。
Figure 1‑8. 比較有無合併效果下的counter,左圖為無合併光追環境遮蔽和軟陰影;右圖為合併兩者。
避免對skydome等無需效果之幾何計算
若場景中包含具有幾何的skydome時,須注意不用對其計算光追環境遮蔽校果,因離其它場景物件距離遠卻涵蓋整個場景,且不易發生表面被附近幾何遮蔽的效果,實際畫面可見 Figure 1-9.。
Figure 1‑9. 於室外場景面向skydome的視角。圖片來源: 暗區突圍。
而在上述情況下,對應Streamline中Rays started counter有約兩倍的減少,如Figure 1-10.,可用來驗證算法是否有確實減少所需的光線採樣。
Figure 1‑10. 採用此項在室內 (左圖) 和室外 (右圖) 視角所需的rays started變化。
另外,經平均後的Table 1-3. 中可觀察到與室內視角 (如Figure 1-1.) 相比下,FPS會有所上升且Rays started/frame下降,然而此項針對玩家在室外場景的視角包含天空等遠景時才對效能有提升。
Table 1‑3. 只考慮光追環境遮蔽下,比較過濾skydome後室內與室外視角所需的平均採樣數量。
FPS |
Rays started/f |
Box nodes tested/f |
Tri batches tested/f |
|
Indoor |
48.31 |
104,311 |
273,835 |
33,482 |
Outdoor |
59.48 |
52,369 |
75,321 |
8,721 |
Difference |
+11.17 |
-51,942 |
-198,514 |
-24,761 |
避免於每幀BLAS重建指令間設置不必要之同步
若每個BLAS有自己獨立的scratch buffer (如Figure 1-11. 流程中綠色方框),無須於每兩個BLAS重建指令間設置barrier同步 (如Figure 1-11. 流程中紅色方框的barrier),除非該scratch buffer會被前後指令重複使用。
Figure 1‑11. 範例流程展示於每兩個BLAS重建指令間不必要之barrier。
在Figure 1-12. 中比較每幀重建32個BLAS和其總三角形量為31,796的應用,由於此時的Compute active區間代表重建AS,可觀察到移除每兩個BLAS重建指令間不必要的barrier可減少約4 ms。
Figure 1‑12. 比較左圖加入不必要的barrier與右圖移除後的差異。
另外,於Arm AS best practice中提及合併多個單獨的BLAS重建指令有利於平行以提升效能。
減少BLAS重建指令對每幀時間所佔比例
對於每幀重建32個BLAS和其總三角形量為31,796的應用,比較BLAS使用不同flag對於一幀所需時間的影響 (請見Table 1-4.), FAST_BUILD在BLAS縮短的幅度較TLAS大,且每幀重建BLAS所佔比例也較大,因此,需要注意遊戲中每幀TLAS和BLAS分別的使用量以分配適當有限的時間處理,而相關的管理機制亦可參考此篇 Khronos 文章。
Table 1‑4. 針對BLAS在不同flag設定以及不重建下對一幀所需時間的影響。
BLAS |
Frame Time (ms) |
Difference (ms) |
FAST_TRACE |
95 |
+47 |
FAST_BUILD |
47 |
0 (pivot) |
X |
32 |
-15 |
另外,可參考DirectX-Specs中對於具有哪些特性的物件,建議其BLAS搭配使用哪種mode和flag組合