探究光線追蹤技術及UE4的實現
- 2019 年 10 月 3 日
- 筆記
一、光線追蹤概述
1.1 光線追蹤是什麼
與傳統的掃描線或光柵化渲染方式不同,光線追蹤(Ray tracing)是三維電腦圖形學中的特殊渲染演算法,跟蹤從攝像機發出的光線而不是光源發出的光線,通過這樣一項技術生成編排好的場景的數學模型顯現出來。

利用光線追蹤技術渲染出的照片級畫面。
與傳統方法的掃描線技術相比,這種方法有更好的光學效果,例如對於反射與折射有更準確的模擬效果,並且效率非常高,所以當追求高品質的效果時經常使用這種方法。
在物理學中,光線追跡可以用來計算光束在介質中傳播的情況。在介質中傳播時,光束可能會被介質吸收,改變傳播方向或者射出介質表面等。我們通過計算理想化的窄光束(光線)通過介質中的情形來解決這種複雜的情況。
在實際應用中,可以將各種電磁波或者微小粒子看成理想化的窄波束(即光線),基於這種假設,人們利用光線追跡來計算光線在介質中傳播的情況。光線追跡方法首先計算一條光線在被介質吸收,或者改變方向前,光線在介質中傳播的距離,方向以及到達的新位置,然後從這個新的位置產生出一條新的光線,使用同樣的處理方法,最終計算出一個完整的光線在介質中傳播的路徑。
光線追蹤 VS 光柵化
光柵化渲染管線(Raster pipeline)是傳統的渲染管線流程,是以一個三角形為單元,將三角形變成像素的過程,在目前影像API和顯示卡硬體有著廣泛的支援和應用。
光線追蹤渲染管線(Ray tracing pipeline)則是以一根光線為單元,描述光線與物體的求交和求交後計算的過程。和光柵化線性管線不同的是,光線追蹤的管線是可以通過遞歸調用來衍生出另一根光線,並且執行另一個管線實例。
1.2 光線追蹤的特點
運用光線追蹤技術,有以下渲染特性:
- 更精確的反射、折射和透射。
- 更準確的陰影。包括自陰影、軟陰影、區域陰影、多光源陰影等。
- 更精準的全局光照。
- 更真實的環境光遮蔽(AO)。

光線追蹤技術可以精確地反映複雜的反射、折射、透射、陰影、全局光等物理特性。
當然,光線追蹤也不是萬全的渲染技術,它有苛刻的硬體要求、有限度的渲染特性支援以及噪點干擾等負面特點。後面章節會更多談及。
1.3 光線追蹤的歷史
光線追蹤渲染技術從自然界中的光線簡化、光線投射演算法、光線追蹤演算法一步步演變而來。
光線投射演算法(1968年)
由Arthur Appel提出用於渲染的光線投射演算法。光線投射的基礎就是從眼睛投射光線到物體上的每個點,查找阻擋光線的最近物體,也就是將影像當作一個屏風,每個點就是屏風上的一個正方形。
根據材料的特性以及場景中的光線效果,這個演算法可以確定物體的濃淡效果。其中一個簡單假設就是如果表面面向光線,那麼這個表面就會被照亮而不會處於陰影中。
光線投射超出掃描線渲染的一個重要優點是它能夠很容易地處理非平面的表面以及實體,如圓錐和球體等。如果一個數學表面與光線相交,那麼就可以用光線投射進行渲染。複雜的物體可以用實體造型技術構建,並且可以很容易地進行渲染。
光線追蹤演算法(1979年)
最先由Turner Whitted於 1979 年做出的突破性嘗試。以前的演算法從眼睛到場景投射光線,但是並不跟蹤這些光線。而光線追蹤演算法則追蹤這些光線,並且每次與物體表面相交時,計算一次所有光影的貢獻量。
光線追蹤API及硬體集成(2018年)
在早些年,NV就聯合Microsoft共同打造基於硬體的新一代光線追蹤渲染API及硬體。在2018年,他們共同發布了RTX(Ray tracing X)標準。Direct X 12支援了RTX,而NV的RTX系列顯示卡支援了RTX技術,從而宣告光線追蹤實時化的到來。

NV RTX演示影片截圖。
UE集成光線追蹤(2019年)
UE於2019年4月發布了4.22版本,該版本最耀眼的新特性無疑是支援了光線追蹤技術。這將助力廣大啟用UE的個人或團隊更加有效地渲染出照片級的畫面。

利用UE的光線追蹤技術渲染出的逼真畫面。
1.4 光線追蹤的應用
早在上世紀60年代,美國科學家已經嘗試將光線投射應用于軍事領域的電腦圖形生成。隨著技術的成熟,很快應用於好萊塢電影及動漫製作。目前,絕大多數需要後期特效的好萊塢電影,除了風格化的類型之外,基本都使用了光線追蹤技術。

《獅子王》利用光線追蹤技術渲染的畫面。
近幾年,雖則RTX標準的發布及顯示卡的支援,光線追蹤技術進入了實時渲染領域,近期發布的很多3A遊戲大作已經支援了光線追蹤渲染。

單機遊戲《光明記憶》開啟和關閉RTX的對比圖。
除了電影、動漫、遊戲領域,光線追蹤技術還可以應用教學、設計、醫學、科學、AR等等領域,以在虛擬的世界渲染出逼真的畫面。

利用光線追蹤渲染的室內設計圖。
二、光線追蹤的原理
2.1 光線追蹤的物理原理
在幾何光學中,可以忽略光線的波動性而直接簡化成直線,從而研究光線的物理特性。同樣地,在電腦圖形學,也可以利用這一特點,以簡化光照著色過程。

此外,人類的眼睛接收到的光照資訊是有限的像素,大多數人的眼睛在5億像素左右。人類接收到的影像資訊可以分拆成5億個像素,也就是說,可以分拆成5億條非常微小的光線,以相反的方式去逆向追蹤這些光線,就可以檢測出這些光線對應的場景物體的資訊(位置、朝向、表明材質、光照顏色和亮度等等)。

光線追蹤技術就是利用以上的物理原理衍生出來。將眼睛抽象成攝像機,視網膜抽象成顯示器幕,5億個像素簡化成螢幕像素,從攝像機位置與螢幕的每個像素連成一條射線,去追蹤這些射線與場景物體交點的光照資訊。
當然,實際的光線追蹤演算法會更加複雜,下一小節會詳細描述。
2.2 光線追蹤演算法
與傳統的光柵化渲染技術相比,光線追蹤的演算法過程還是比較明晰的。
以視點為起點,向場景發射N條光線,然後根據碰撞點的材質進行BXDF、BRDF的運算,然後再進行漫反射、鏡面反射或者折射,如此遞歸循環直到光線逃離場景或者到達最大反射次數,最後對N條光線進行蒙特卡洛積分即可獲得結果。

結合上圖,可以將光線追蹤的演算法過程抽象成以下偽程式碼:
遍歷螢幕的每個像素 { 創建從視點通過該像素的光線 初始化 最近T 為 無限大,最近物體 為 空值 遍歷場景中的每個物體 { 如果光線與物體相交 { 如果交點處的 t 比 最近T 小 { 設置 最近T 為交點的 t 值 設置 最近物體 為該物體 } } } 如果 最近物體 為 空值{ 用背景色填充該像素 } 否則 { 對每個光源射出一條光線來檢測是否處在陰影中 如果表面是反射面,生成反射光,並遞歸 如果表面透明,生成折射光,並遞歸 使用 最近物體 和 最近T 來計算著色函數 以著色函數的結果填充該像素 } } 上述偽程式碼中涉及的著色函數可採用任意光照模型,可以是Lambert、Phong、Blinn-Phong、BRDF、BTDF、BSDF、BSSRDF等等。
若是更近一步,用電腦語言形式的偽程式碼描述,則光線追蹤的計算過程如下:
-- 遍歷影像的所有像素 function traceImage (scene): for each pixel (i,j) in image S = PointInPixel P = CameraOrigin d = (S - P) / || S – P|| I(i,j) = traceRay(scene, P, d) end for end function -- 追蹤光線 function traceRay(scene, P, d): (t, N, mtrl) ← scene.intersect (P, d) Q ← ray (P, d) evaluated at t I = shade(mtrl, scene, Q, N, d) R = reflectDirection(N, -d) I ← I + mtrl.kr ∗ traceRay(scene, Q, R) -- 遞歸追蹤反射光線 -- 區別進入介質的光和從介質出來的光 if ray is entering object then n_i = index_of_air n_t = mtrl.index else n_i = mtrl.index n_i = mtrl.index n_t = index_of_air end if if (mtrl.k_t > 0 and notTIR (n_i, n_t, N, -d)) then T = refractDirection (n_i, n_t, N, -d) I ← I + mtrl.kt ∗ traceRay(scene, Q, T) -- 遞歸追蹤折射光線 end if return I end function -- 計算所有光源對像素的貢獻量(包含陰影) function shade(mtrl, scene, Q, N, d): I ← mtrl.ke + mtrl. ka * scene->Ia for each light source l do: atten = l -> distanceAttenuation( Q ) * l -> shadowAttenuation( scene, Q ) I ← I + atten*(diffuse term + spec term) end for return I end function -- 此處只計算點光源的陰影,不適用其它類型光源的陰影 function PointLight::shadowAttenuation(scene, P) d = (l.position - P).normalize() (t, N, mtrl) ← scene.intersect(P, d) Q ← ray(t) if Q is before the light source then: atten = 0 else atten = 1 end if return atten end function 上述distanceAttenuation的介面中,通常還涉及到BRDF的光照積分,但是在實時渲染領域,要對每個相交點做一次積分是幾乎不可能的。
於是可以引入蒙特卡洛積分和重要性取樣(可參看《由淺入深學習PBR的原理及實現》的章節5.4.2.1 蒙特卡洛(Monte Carlo)積分和重要性取樣(Importance sampling)),以局部取樣估算整體光照積分。
均勻取樣(Uniform Sampling)是不區分光源重要性的平均化取樣,生成的光線樣本在各個方向上概率都相同,並不會對燈光特殊對待,偏差與實際值通常會很大。
蒙特卡洛取樣(Monte Carlo Sampling)著重考慮了光源方向的取樣,能突出光源對像素的貢獻量,但會造成光源貢獻量過度。
重要性取樣(Importance Sampling)則加入概率密度函數(pdf),通過縮小取樣結果,防止光源的貢獻量太大。
當然,引入這個方法,如果取樣數量不夠多,會造成光照貢獻量與實際值偏差依然會很大,形成噪點。隨著取樣數量的增加,局部估算越來越接近實際光照積分,噪點逐漸消失(下圖)。

從左到右分別對應的每個象素取樣為1、16、256、4096、65536。
結合了蒙特卡羅積分和重要性取樣的光線追蹤技術,也被稱為路徑追蹤(Path tracing)。
2.3 RTX和DXR
2.3.1 RTX(NV)
NV作為世界級的圖形學界的探索先鋒隊,在光線追蹤方面有著深入的研發,最終抽象成技術標準RTX平台。
隨著DirectX 12的DXR和Vulkan的支援,使得支援硬體級的光線追蹤技術漸漸普及。NV最先在Turing架構的GPU支援了RTX技術:

由上圖可見,最上層是用戶層(MDL和USD),包含了深度學習和普通應用開發;中間層是圖形API層,支援RTX的有OptiX、DXR、Vulkan,OpenGL並不支援RTX;最底層就是RTX平台,它又包含了4個部分:傳統的光柵化器、光線追蹤(RT Core)、CUDA計算器、AI核心。
當然,除了Turing架構的GPU,還有PASCAL、VOLTA、TURING RTX等架構的眾多款GPU支援RTX技術。(下圖)

下圖是若干款支援RTX技術的GPU運行同一個Demo(Battlefield)的性能對比:

此外,對於光線追蹤,每種光線追蹤的特性都會有不同的負載:

上圖涉及的BVH(Bounding volume hierarchy)是層次包圍盒,是一種加速場景物體查找的演算法和結構體。
對於開發者,需要根據品質等級,做好各類指標預選項,以便程式能夠良好地運行在各個畫質級別的設備中。
2.3.2 DXR(Microsoft)

在DX12的全新圖形API中,加入了可編程的光線追蹤渲染管線(上圖),簡稱DXR。和傳統光柵化管線一樣,光線追蹤的管線有固定的邏輯,也有可編程的部分。新管線中新增了5種著色器(Shader),分別是:
- Ray Generation:用於生成射線。在此shader中可以調用TraceRay()遞歸追蹤光線。
- Intersection和Any Hit:當TraceRay()內檢測到光線與物體相交時,會調用此shader,以便使用者檢測此相交的物體是否特殊的圖元(球體、細分表面或其它圖元類型)。
- Closest Hit和Miss:當TraceRay()遍歷完整個場景後,會根據光線相交與否調用這兩個Shader。Cloesit Hit可以執行像素著色處理,如材質、紋理查找、光照計算等。Cloesit Hit和Miss都可以繼續遞歸調用TraceRay()。
下面是以上部分shader的應用示例,以便更好說明它們的用途:
// An example payload struct. We can define and use as many different ones as we like. struct Payload { float4 color; float hitDistance; }; // The acceleration structure we'll trace against. // This represents the geometry of our scene. RaytracingAccelerationStructure scene : register(t5); [shader("raygeneration")] void RayGenMain() { // Get the location within the dispatched 2D grid of work items // (often maps to pixels, so this could represent a pixel coordinate). uint2 launchIndex = DispatchRaysIndex(); // Define a ray, consisting of origin, direction, and the t-interval // we're interested in. RayDesc ray; ray.Origin = SceneConstants.cameraPosition. ray.Direction = computeRayDirection( launchIndex ); // assume this function exists ray.TMin = 0; ray.TMax = 100000; Payload payload; // Trace the ray using the payload type we've defined. // Shaders that are triggered by this must operate on the same payload type. TraceRay( scene, 0 /*flags*/, 0xFF /*mask*/, 0 /*hit group offset*/, 1 /*hit group index multiplier*/, 0 /*miss shader index*/, ray, payload ); outputTexture[launchIndex.xy] = payload.color; } // Attributes contain hit information and are filled in by the intersection shader. // For the built-in triangle intersection shader, the attributes always consist of // the barycentric coordinates of the hit point. struct Attributes { float2 barys; }; [shader("closesthit")] void ClosestHitMain( inout Payload payload, in Attributes attr ) { // Read the intersection attributes and write a result into the payload. payload.color = float4( attr.barys.x, attr.barys.y, 1 - attr.barys.x - attr.barys.y, 1 ); // Demonstrate one of the new HLSL intrinsics: query distance along current ray payload.hitDistance = RayTCurrent(); } 光線追蹤渲染管線中,還涉及到加速結構(Acceleration Structure)。它的作用是保存場景的所有幾何物體資訊,在GPU內提供物體遍歷、相交測試、光線構造等等的極限加速演算法,使得光線追蹤達到實時渲染級別。它可以在應用程式通過BuildRaytracingAccelerationStructure()介面構建。

如上圖,對於場景中的每個幾何體,在GPU內部都存在兩個級別的加速結構。底層加速結構(Bottom-Level AS)從輸入的圖元資訊構建而成,如三角形、四邊形。頂層加速結構(Top-Level AS)從底層加速結構創建而來,相當於是底層加速結構的實例,保存了底層結構的變換矩陣和shader偏移。
Shader映射表(Shader Table)描述了shader與場景的哪個物體關聯,也包含了shader中涉及的所有資源(紋理、buffer、常量等)。
在GPU底層,Shader映射表是一個等尺寸的記錄體(record),每個記錄體關聯著帶著一組資源的shader(或相交組(Hit group))。通常每個幾何體存在一個記錄體。

由上圖可見,每個記錄體由shader編號起始,隨後存著CBV、UAV、常量、描述表等shader資源。
這種雙層架構的好處是將資源和實例化分離,加速實例創建和初始化,降低頻寬和顯示記憶體佔用。
PIX作為Microsoft的老牌且強大的圖形調試軟體,在DXR發布之初就支援了對它的調試。利用PIX可方便調試各類調用棧、渲染狀態及資源等資訊。

三、UE4的光線追蹤
3.1 UE4光線追蹤的開啟
如果要開啟UE的光線追蹤,必須滿足以下幾個條件:
作業系統:Windows 10 RS5 (Build 1809) 及之後版本。至於如何升級Windows版本,可參看微軟官方文檔:Get the Windows 10 May 2019 Update。
- 顯示卡:NVIDIA RTX,以及支援DXR的GTX系列。
UE版本:4.22及之後版本。
滿足以上所有條件,才可以按照以下步驟開啟UE的光線追蹤:
1、打開項目設置(文件-項目設置)介面。
2、找到項目設置的平台-windows頁, Default RHI選成DirectX 12。

3、找到渲染頁,勾選光線追蹤(Ray tracing)。

勾選光線追蹤之後,編輯器會提示是否重啟,點擊是即可。
如果熟悉引擎的配置文件及命令行啟動,可以直接修改ConsoleVariables.ini:
r.RayTracing=1 r.SkinCache.CompileShaders=1 然後在啟動UE工程時附加-d3d12標記,即可直接啟用DX12模式渲染。
4、添加後處理卷積(Post Process Volume)。
重啟完編輯器,等待Shader全部編譯完成,便可以往關卡添加後處理體積,以便啟用光線追蹤的相關特性,調節各類參數。

選中後處理體積,在細節面板,可以調整它的影響範圍,單獨開啟和設置各種特性的參數:

3.2 UE4光線追蹤的特性
UE4目前版本可支援的光線追蹤有以下特性:
3.2.1 光線追蹤的陰影
可模擬多光源的過渡性軟陰影、區域陰影、模型的自陰影,以及其它各種複雜的遮擋陰影,能夠與場景物體緊密結合,無明顯瑕疵。


上:光柵化陰影;下:光線追蹤陰影。
3.2.2 光線追蹤的反射
光線追蹤的反射可實時動態反射場景的任意物體,完全不受之前SSR、平面反射、立方體圖等的限制,所渲染的結畫面更加真實,融入場景內。


上:SSR效果;下:光線追蹤反射。
此外,光線追蹤的反射可以精準地表現出掠射角處被反射物體的拉長效應:


上:光柵化的反射;下:光線追蹤的反射。
3.2.3 光線追蹤的透明
光線追蹤的透明可以精確地模擬玻璃、流體等材質的物理正確的反射、吸收、折射等表面特性。


上:光柵化的透明;下:光線追蹤的透明。
3.2.4 光線追蹤的環境光遮蔽
螢幕空間的環境光遮蔽(SSAO)是後處理階段執行的AO處理,更類似於邊緣檢測,存在漏光現象,真實度不高。而光線追蹤的環境光遮蔽則可以根據場景各個物體的遮擋關係精確地計算出每個像素的AO,能夠非常好地融入到環境中。


上:SSAO;下:光線追蹤的AO。
3.2.5 光線追蹤的全局光照
光線追蹤模式的全局光照增加了光線在場景中的若干次彈跳,並加權它們的權重,使得物體與物體、物體與光源之間的關係更物理正確,渲染效果更真實。


上:只有天空光;下:光線追蹤的全局光。
以上皆是靜態地對比傳統渲染技術和光線追蹤的效果,下面的鏈接提供了影片動態地對比它們之間的差別,能更直觀體會到光線追蹤的特性:
UE還提供了路徑追蹤的渲染模式,在場景編輯窗口,將視圖模式(View Mode)選為路徑追蹤(Path Tracing)即可開啟:

下面是光線追蹤和路徑追蹤的對比圖:


上:光線追蹤;下:路徑追蹤。
3.2.6 光線追蹤的其它特性
以上特性除了可以在UE編輯器中開啟,還可以通過控制台命令更加精細化地設置光線追蹤:
// General Settings r.RayTracing.Reflections [0|1] r.RayTracing.Shadows [0|1] r.RayTracing.AmbientOcclusion [0|1] // Material Sorting r.RayTacing.Reflections.SortMaterials [0|1] // Shadow Materials r.RayTracing.Shadows.EnableMaterials [0|1] // Reflection Screen Percentage r.RayTracing.Reflections.ScreenPercentage [50|100] // Maximum Roughness r.RayTracing.Reflections.MaxRoughness [-1.0 | 0.0-1.0] // Samples Per Pixel r.RayTacing.Reflections.SamplesPerPixel [0-N] r.RayTacing.AmbientOcclusion.SamplesPerPixel [0-N] r.RayTacing.Shadow.SamplesPerPixel [0-N] // Maximum Bounces r.RayTracing.Reflections.MaxBounces [0-N] // Minimum and Maximum Ray Distance r.RayTracing.Reflections.MinRayDistance [0-N] r.RayTracing.Reflections.MaxRayDistance [0-N] // Lighting in Reflections r.RayTracing.Reflections.Shadows [0|1] r.RayTracing.Reflections.DirectLighting [0|1] r.RayTracing.Reflections.EmissiveAndIndirectLighting [0|1] // Height Fogging r.RayTracing.Reflections.HeightFog [0|1] // Two Sided Geometry r.RayTracing.Shadows.EnableTwoSidedGeometry [0|1] r.RayTracing.AmbientOcclusion.EnableTwoSidedGeometry [0|1] // Materials r.RayTracing.EnableMaterials [0|1] // Force Opaque r.RayTracing.DebugForceOpaque [0|1] // Texture LOD r.RayTacing.UseTextureLOD [0|1] // Normal Offset Bias r.RayTacing.NormalBias <float, default 0.1> 更多請參見:Introduction to Ray Tracing in Unreal Engine 4.22。
3.3 UE4光線追蹤的調試
由於UE4的光線追蹤採用的是DXR,所以可以使用微軟的PIX調試UE4光線追蹤的應用程式。
此外,UE4本身也提供了一些命令和GUI調試光線追蹤的資訊和性能。
Stat GPU:可跟蹤GPU的光線追蹤的各個特性的消耗。
Stat D3D12RayTracing:可檢測光線追蹤使用的資源。
視圖模式的調試窗口:可實時查看光照各個部分的GBuffer數據等。

3.4 UE4光線追蹤的不足
由於RTX、DRX等技術標準尚處於初始階段,平台和技術標準的存在著不少缺陷,這也同樣存在於UE4的光線追蹤當中。
對軟體、硬體要求苛刻。
UE4的光線追蹤開啟的先決條件足以印證這一點。筆者的RTX 2060在開啟光線追蹤之後,無降噪演算法的情況下渲染相同的場景,幀率大概不到光柵化渲染的一半。
不支援部分傳統渲染特性。
更具體地,不支援或不完全支援光照透射(Light Transmission)、體積霧(Volumetric Fog)、光照函數(Light Functions)、世界坐標偏移(World Position Offset)、植被(Foliage)等等。


更多請參看官方說明文檔:Ray Tracing Supported Features。
畫面噪點。
由於實時光線追蹤不可能對錶面的每次BxDF執行半球積分,只能利用重要性取樣估算光照積分。由於通常取樣次數不足,只能用很低的取樣次數(如1次),光照積分與實際值偏差較大,所以會形成很嚴重的噪點,特別是在陰影處。(下圖)

讓人欣慰的是,目前存在很多降低噪點的方法,比如NV的AI降噪,可利用1取樣高噪點圖,通過降噪演算法,獲得很好的降噪結果。


上:1次取樣的原始噪點圖;下:開啟了降噪處理的畫面。
降噪演算法更多資訊可參見:
四、UE的底層實現
由於UE的源碼很多邏輯對是否開啟光線追蹤進行了判斷,影響面非常廣,C++和Shader文件涉及數量成百上千。Shader程式碼主要集中在:
EngineShadersPrivateRayTracing目錄。
此目錄基本囊括了光線追蹤所有特性的shader實現程式碼:

EngineShadersPrivatePathTracing目錄。
此目錄下是路徑追蹤版本的shader程式碼。

由於精力有限,無法對所有涉及光線追蹤的邏輯進行分析,下面只對Ray Tracing版本的全局光照shader做剖析,其它特性(反射、AO、透明、陰影等)的shader可自行看UE源碼。
光線追蹤版本的全局光照shader涉及的文件主要有:
- EngineShadersPrivateRayTracingRayTracingCommon.ush
- EngineShadersPrivateRayTracingRayTracingGlobalIlluminationRGS.usf
- EngineShadersPrivateRayTracingRayTracingGlobalIlluminationCompositePS.usf
下面是RayTracingGlobalIlluminationRGS.usf的程式碼:
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. #include "../Common.ush" #include "../RectLight.ush" //#include "../MonteCarlo.ush" #include "../DeferredShadingCommon.ush" #include "../ShadingModels.ush" #include "RayTracingCommon.ush" #include "RayTracingHitGroupCommon.ush" #include "../PathTracing/Utilities/PathTracingRandomSequence.ush" #include "../PathTracing/Light/PathTracingLightSampling.ush" #include "../PathTracing/Material/PathTracingMaterialSampling.ush" #define USE_PATHTRACING_MATERIALS 0 // 加速結構體 RaytracingAccelerationStructure TLAS; // RWTexture2D是可讀寫紋理,無序訪問視圖(unordered access view,UAV),更多介紹參見微軟官方文檔:https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/sm5-object-rwtexture2d RWTexture2D<float4> RWGlobalIlluminationUAV; RWTexture2D<float2> RWRayDistanceUAV; uint SamplesPerPixel; uint MaxBounces; uint UpscaleFactor; float MaxRayDistanceForGI; float MaxRayDistanceForAO; float NextEventEstimationSamples; float DiffuseThreshold; bool EvalSkyLight; bool UseRussianRoulette; float MaxNormalBias; // #dxr_todo: Unify with reflections and translucency in RayTracingCommon.ush uint2 GetPixelCoord(uint2 DispatchThreadId) { uint UpscaleFactorPow2 = UpscaleFactor * UpscaleFactor; // TODO: find a way to not interfer with TAA's jittering. uint SubPixelId = View.StateFrameIndex & (UpscaleFactorPow2 - 1); return DispatchThreadId * UpscaleFactor + uint2(SubPixelId & (UpscaleFactor - 1), SubPixelId / UpscaleFactor); } uint CalcLinearIndex(uint2 PixelCoord) { return PixelCoord.y * View.BufferSizeAndInvSize.x + PixelCoord.x; } // 利用CosineSampleHemisphere生成取樣光線,以便更實時精準地生成光線。 void GenerateCosineNormalRay( float3 WorldPosition, float3 WorldNormal, inout RandomSequence RandSequence, out float3 RayOrigin, out float3 RayDirection, out float RayTMin, out float RayTMax, out float RayPdf ) { // Draw random variable float2 BufferSize = View.BufferSizeAndInvSize.xy; uint DummyVariable; float2 RandSample = RandomSequence_GenerateSample2D(RandSequence, DummyVariable); // Perform cosine-hemispherical sampling and convert to world-space float4 Direction_Tangent = CosineSampleHemisphere(RandSample); float3 Direction_World = TangentToWorld(Direction_Tangent.xyz, WorldNormal); RayOrigin = WorldPosition; RayDirection = Direction_World; RayTMin = 0.01; RayTMax = max(MaxRayDistanceForGI, MaxRayDistanceForAO); RayPdf = Direction_Tangent.w; } float GetHitT(FMaterialClosestHitPayload HitInfo) { return HitInfo.HitT; } bool IsHit(RayDesc Ray, FMaterialClosestHitPayload HitInfo) { return HitInfo.HitT >= 0.0; } // 射線生成Shader,即2.3.2提及的Ray Generation。 [shader("raygeneration")] void GlobalIlluminationRGS() { // 初始化當前光線的無序讀寫紋理。 uint2 DispatchThreadId = DispatchRaysIndex().xy; RWGlobalIlluminationUAV[DispatchThreadId] = 0.0; RWRayDistanceUAV[DispatchThreadId] = float2(-1.0, 0.0); // 計算像素坐標 uint2 PixelCoord = GetPixelCoord(DispatchThreadId); RandomSequence RandSequence; uint LinearIndex = CalcLinearIndex(PixelCoord); RandomSequence_Initialize(RandSequence, LinearIndex, View.FrameNumber); bool IsUnidirectionalEnabled = false; // 獲取材質表面的G-Buffer數據。 float2 InvBufferSize = View.BufferSizeAndInvSize.zw; float2 UV = (float2(PixelCoord) + 0.5) * InvBufferSize; FScreenSpaceData ScreenSpaceData = GetScreenSpaceData(UV); // Remap DiffuseColor when using SubsurfaceProfile (GBuffer decoding replaces with 100% albedo) if (UseSubsurfaceProfile(ScreenSpaceData.GBuffer.ShadingModelID)) { ScreenSpaceData.GBuffer.DiffuseColor = ScreenSpaceData.GBuffer.StoredBaseColor; } float Depth = ScreenSpaceData.GBuffer.Depth; float3 WorldPosition = ReconstructWorldPositionFromDepth(UV, Depth); float3 CameraOrigin = ReconstructWorldPositionFromDepth(UV, 0.0); float3 CameraDirection = normalize(WorldPosition - CameraOrigin); float3 WorldNormal = ScreenSpaceData.GBuffer.WorldNormal; uint ShadingModelID = ScreenSpaceData.GBuffer.ShadingModelID; if (ShadingModelID == SHADINGMODELID_UNLIT || ShadingModelID == SHADINGMODELID_TWOSIDED_FOLIAGE ) { return; } // Diffuse color rejection threshold float3 DiffuseColor = ScreenSpaceData.GBuffer.DiffuseColor; if (Luminance(DiffuseColor) < DiffuseThreshold) { return; } float3 Irradiance = 0; float HitDistance = 0.0; float HitCount = 0.0; float AmbientOcclusion = 0.0; // 生成每像素取樣數量相同的光線。 for (uint SampleIndex = 0; SampleIndex < SamplesPerPixel; ++SampleIndex) { // 使用Scrambled Halton低差異序列 uint FrameIndex = View.FrameNumber % 1024; RandomSequence_Initialize(RandSequence, LinearIndex, FrameIndex * SamplesPerPixel + SampleIndex); RandSequence.Type = 2; float3 RayThroughput = 1.0; // Russian roulette based on DiffuseColor if (UseRussianRoulette) { uint DummyVariable; float RRSample = RandomSequence_GenerateSample1D(RandSequence, DummyVariable); float ProbabilityOfSuccess = Luminance(DiffuseColor); float ProbabilityOfTermination = 1.0 - ProbabilityOfSuccess; if (RRSample < ProbabilityOfTermination) continue; RayThroughput /= ProbabilityOfSuccess; } // Initialize ray RayDesc Ray; float RayPdf = 1.0; // 使用重要性取樣生成射線,且計算BxDF光照結果。 #if 1 GenerateCosineNormalRay(WorldPosition, WorldNormal, RandSequence, Ray.Origin, Ray.Direction, Ray.TMin, Ray.TMax, RayPdf); half3 N = WorldNormal; half3 V = -CameraDirection; half3 L = Ray.Direction; float NoL = saturate(dot(N, L)); FShadowTerms ShadowTerms = { 0.0, 0.0, 0.0 }; // 光線追蹤的BxDF與光柵化的一樣,都是調用EvaluateBxDF。 FDirectLighting LightingSample = EvaluateBxDF(ScreenSpaceData.GBuffer, N, V, L, NoL, ShadowTerms); // 計算顏色各通道反射係數。 RayThroughput *= LightingSample.Diffuse / DiffuseColor; #else uint DummyVariable; float2 RandSample = RandomSequence_GenerateSample2D(RandSequence, DummyVariable); float2 ViewportUV = (PixelCoord.xy + RandSample.xy) * View.BufferSizeAndInvSize.zw; Ray.Origin = ReconstructWorldPositionFromDepth(ViewportUV, 0.0f); Ray.Direction = normalize(ReconstructWorldPositionFromDepth(ViewportUV, 1.f) - Ray.Origin); Ray.TMin = 0.0; Ray.TMax = 1.0e12; float3 RayThroughput = 1.0; #endif Ray.TMax = max(MaxRayDistanceForGI, MaxRayDistanceForAO); ApplyPositionBias(Ray, WorldNormal, MaxNormalBias); float MaterialPdf = 0.0; uint Bounce = 0; // 根據最大反射次數,遞歸處理反射光線 while (Bounce < MaxBounces) { // 計算射線 uint RayFlags = 0; FRayCone RayCone = (FRayCone)0; // TraceRayInternal是UE自己封裝的介面,內部會調用TraceRay以及解包Payload數據。 FMaterialClosestHitPayload Payload = TraceRayInternal( TLAS, // AccelerationStructure RayFlags, RAY_TRACING_MASK_OPAQUE, RAY_TRACING_SHADER_SLOT_MATERIAL, // RayContributionToHitGroupIndex RAY_TRACING_NUM_SHADER_SLOTS, // MultiplierForGeometryContributionToShaderIndex 0, // MissShaderIndex Ray, // RayDesc RayCone ); // Environment hit // 如果射線不與場景物體碰撞,則接收環境光。 if (!IsHit(Ray, Payload)) { // Optional multi-bounce SkyLight contribution if (EvalSkyLight && Bounce > 0) { uint SkyLightId = 0; float3 EnvironmentRadiance = 0.0; SkyLight_EvalLight(SkyLightId, Ray.Direction, Ray, EnvironmentRadiance); Irradiance += EnvironmentRadiance * RayThroughput / RayPdf; } break; } // #dxr_todo: Allow for material emission? if (Bounce == 0) { HitDistance += Payload.HitT; HitCount += 1.0; if (Payload.HitT < MaxRayDistanceForAO) { AmbientOcclusion += 1.0; } } if (Payload.HitT > MaxRayDistanceForGI) break; // Update intersection Ray.Origin += Ray.Direction * Payload.HitT; // Create faux GBuffer to use with EvaluateBxDF FGBufferData GBufferData = (FGBufferData)0; GBufferData.Depth = 1.f; // Do not use depth GBufferData.WorldNormal = Payload.WorldNormal; GBufferData.BaseColor = Payload.BaseColor; GBufferData.CustomData = Payload.CustomData; GBufferData.GBufferAO = Payload.GBufferAO; GBufferData.IndirectIrradiance = (Payload.IndirectIrradiance.x + Payload.IndirectIrradiance.y + Payload.IndirectIrradiance.z) / 3.f; GBufferData.SpecularColor = Payload.SpecularColor; GBufferData.DiffuseColor = Payload.DiffuseColor; GBufferData.Metallic = Payload.Metallic; GBufferData.Specular = Payload.Specular; GBufferData.Roughness = Payload.Roughness; GBufferData.ShadingModelID = Payload.ShadingModelID; GBufferData.CustomData = Payload.CustomData; // 對後續光線的評估(Perform next-event estimation)。 // NextEventEstimationSamples可通過r.RayTracing.GlobalIllumination.NextEventEstimationSamples設置。 float SplitFactor = 1.0 / NextEventEstimationSamples; for (uint NeeTrial = 0; NeeTrial < NextEventEstimationSamples; ++NeeTrial) { // Light selection int LightId; float3 LightUV; float NeePdf = 0.0; uint DummyVariable; float4 RandSample4 = RandomSequence_GenerateSample4D(RandSequence, DummyVariable); SampleLight(Ray, Payload, RandSample4, LightId, LightUV, NeePdf); if (NeePdf > 0.0) { RayDesc LightRay; GenerateLightRay(Ray, LightId, LightUV, LightRay); ApplyPositionBias(LightRay, Payload.WorldNormal, MaxNormalBias); // Trace visibility ray uint RayFlags = RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH | RAY_FLAG_SKIP_CLOSEST_HIT_SHADER; FRayCone LightRayCone = (FRayCone)0; FMaterialClosestHitPayload NeePayload = TraceRayInternal( TLAS, // AccelerationStructure RayFlags, RAY_TRACING_MASK_OPAQUE, RAY_TRACING_SHADER_SLOT_MATERIAL, // RayContributionToHitGroupIndex RAY_TRACING_NUM_SHADER_SLOTS, // MultiplierForGeometryContributionToShaderIndex 0, // MissShaderIndex LightRay, // RayDesc LightRayCone ); // No hit indicates successful next-event connection if (!IsHit(LightRay, NeePayload)) { // Evaluate radiance float3 Radiance; EvalLight(LightId, LightUV, LightRay, Radiance); // Evaluate material float3 MaterialThroughput; float MaterialEvalPdf = 0.0; #if USE_PATHTRACING_MATERIALS EvalMaterial(Ray.Direction, LightRay.Direction, Payload, MaterialThroughput, MaterialEvalPdf); #else half3 N = Payload.WorldNormal; half3 V = -Ray.Direction; half3 L = LightRay.Direction; float NoL = saturate(dot(N, L)); FShadowTerms ShadowTerms = { 0.0, 0.0, 0.0 }; FDirectLighting LightingSample = EvaluateBxDF(GBufferData, N, V, L, NoL, ShadowTerms); MaterialThroughput = LightingSample.Diffuse; MaterialEvalPdf = 1.0; #endif // Apply material Pdf for correct MIS weight float MisWeight = 1.0; #if 0 if (IsUnidirectionalEnabled && IsPhysicalLight(LightId)) { MisWeight = NeePdf / (NeePdf + MaterialEvalPdf); } #endif // Record the contribution float3 ExitantRadianceSample = Radiance * MaterialThroughput * RayThroughput * SplitFactor * MisWeight / (NeePdf * RayPdf); Irradiance += isfinite(ExitantRadianceSample) ? ExitantRadianceSample : 0.0; } } } // 處理材質取樣。 // dxr_todo: only worth doing when Bounce + 1 < MaxBounces if (Bounce + 1 < MaxBounces) { float3 Direction; float3 Throughput = 1.0; #if USE_PATHTRACING_MATERIALS uint DummyVariable; float4 RandSample = RandomSequence_GenerateSample4D(RandSequence, DummyVariable); // 取樣材質,內部會根據純鏡面反射、鏡面反射透射、倫勃朗等光照類型區別取樣。 SampleMaterial(Ray.Direction, Payload, RandSample, Direction, Throughput, MaterialPdf); #else float3 RayOrigin = Ray.Origin; GenerateCosineNormalRay(RayOrigin, Payload.WorldNormal, RandSequence, Ray.Origin, Direction, Ray.TMin, Ray.TMax, MaterialPdf); half3 N = Payload.WorldNormal; half3 V = -Ray.Direction; half3 L = Direction; float NoL = saturate(dot(N, L)); FShadowTerms ShadowTerms = { 0.0, 0.0, 0.0 }; FDirectLighting LightingSample = EvaluateBxDF(GBufferData, N, V, L, NoL, ShadowTerms); Throughput = LightingSample.Diffuse; #endif // #dxr_todo: Degenerate guard? if (MaterialPdf <= 0.0) { break; } // Update ray Ray.Direction = Direction; RayThroughput *= Throughput; RayPdf *= MaterialPdf; // #dxr_todo: Russian roulette? // #dxr_todo: Firefly rejection? } Bounce++; } } // 輻照度和AO都必須歸一化,防止權重過大。 if (SamplesPerPixel > 0) { Irradiance /= SamplesPerPixel; AmbientOcclusion /= SamplesPerPixel; } if (HitCount > 0.0) { HitDistance /= HitCount; } else { HitDistance = -1.0; } AmbientOcclusion = saturate(AmbientOcclusion); #if USE_PREEXPOSURE Irradiance *= View.PreExposure; #endif Irradiance = ClampToHalfFloatRange(Irradiance); RWGlobalIlluminationUAV[DispatchThreadId] = float4(Irradiance, AmbientOcclusion); RWRayDistanceUAV[DispatchThreadId] = float2(HitDistance, SamplesPerPixel); // For AO denoiser.. //RWRayDistanceUAV[DispatchThreadId] = float2(Luminance(Irradiance), HitDistance); } // 2.3.2提及的Miss Shader。 [shader("miss")] void RayTracingGlobalIlluminationMS(inout FPackedMaterialClosestHitPayload PackedPayload) { PackedPayload.HitT = -1; } // 2.3.2提及的Closest Hit Shader。 [shader("closesthit")] void RayTracingGlobalIlluminationCHS(inout FPackedMaterialClosestHitPayload PackedPayload, in FDefaultAttributes Attributes) { // 在最近碰撞點處理Payload數據(HitT、法線等),以供其它shader使用。 FMaterialClosestHitPayload Payload = (FMaterialClosestHitPayload)0; Payload.HitT = RayTCurrent(); FTriangleBaseAttributes Triangle = LoadTriangleBaseAttributes(PrimitiveIndex()); float3 Edge0 = Triangle.LocalPositions[2] - Triangle.LocalPositions[0]; float3 Edge1 = Triangle.LocalPositions[1] - Triangle.LocalPositions[0]; float3x3 WorldToLocal = (float3x3)WorldToObject(); float3x3 LocalToWorldNormal = transpose(WorldToLocal); Payload.WorldNormal = normalize(mul(LocalToWorldNormal, cross(Edge0, Edge1))); PackedPayload = PackRayTracingPayload(Payload, PackedPayload.RayCone); } 從上面可以看到,UE在處理光線追蹤的全局光照時,結合每像素取樣數量SamplesPerPixel和最大反射次數MaxBounces,使用了多種取樣策略,且考慮了Next-Event評估、路徑追蹤等情況,所以整個流程會比較複雜。
雖然本節只對全局光照的shader進行了分析,但從中可以窺視UE在處理光線追蹤的流程和技術,從而更加具體地理解光線追蹤的實現和應用。
五、總結
本文開頭光線追蹤的概念、特點、歷史、應用,隨著介紹了其原理和常見的偽程式碼實現形式,然後介紹了RTX和DXR技術,最後剖析了UE的使用方式和內部實現。可算是一篇比較系統、全面的光線追蹤的技術文章。
當然,光線追蹤的全部及未來無法在本文體現,更多更新的光線追蹤技術隨著時間漸漸湧現,作為影像渲染從業者,永遠都要保持學習的動力和探索的腳步。
光線追蹤技術現在只是起點,從未有終點。
The future has just begun!
特別說明
- 感謝所有參考文獻的作者們!
- 原創文章,版權所有,禁止轉載!
參考文獻
- Unreal Engine Sources
- Real-Time Ray Tracing
- Ray Tracing Features Settings
- Path Tracer
- Get the Windows 10 May 2019 Update
- Ray tracing (graphics)
- Basics Ray Tracing History Ray Tracing History Outline
- 由淺入深學習PBR的原理和實現
- Announcing Microsoft DirectX Raytracing!
- NVIDIA RTX Ray Tracing
- NVIDIA RTX 技術:在遊戲中運用即時光線追蹤技術不再是夢想
- Introduction to Ray Tracing in Unreal Engine 4.22
- 光線追蹤
- What』s the Difference Between Ray Tracing and Rasterization?
- NVIDIA RTX™ platform
- 《光明記憶》(搶先體驗版)RTX光線追蹤版預告 畫質全面升級、近期上線
- NVIDIA Releases DirectX Raytracing Driver for GTX Cards; Posts Trio of DXR Demos
- Raytracing Pseudocode
- 【遊戲開發】淺談光線追蹤
- 為什麼光線追蹤會出現噪點,為什麼需要蒙特卡洛?
- 光線追蹤與實時渲染的未來
- Real-Time_Rendering_4th-Real-Time_Ray_Tracing
- Introduction to NVIDIA RTX and DirectX Ray Tracing
- Bounding volume hierarchy
- Ray Tracing in Games with NVIDIA RTX (Presented by NVIDIA)
- NVIDIA RTX and GameWorks Ray Tracing Technology Demonstration