探究光線追蹤技術及UE4的實現

1、光線追蹤概述

1.1 光線追蹤是什麼

與傳統的掃描線或光柵化渲染方式不一樣,光線追蹤(Ray tracing)是三維計算機圖形學中的特殊渲染算法,跟蹤從攝像機發出的光線而不是光源發出的光線,經過這樣一項技術生成編排好的場景的數學模型顯現出來。git

利用光線追蹤技術渲染出的照片級畫面。github

與傳統方法的掃描線技術相比,這種方法有更好的光學效果,例如對於反射與折射有更準確的模擬效果,而且效率很是高,因此當追求高質量的效果時常用這種方法。算法

在物理學中,光線追跡能夠用來計算光束在介質中傳播的狀況。在介質中傳播時,光束可能會被介質吸取,改變傳播方向或者射出介質表面等。咱們經過計算理想化的窄光束(光線)經過介質中的情形來解決這種複雜的狀況。編程

在實際應用中,能夠將各類電磁波或者微小粒子當作理想化的窄波束(即光線),基於這種假設,人們利用光線追跡來計算光線在介質中傳播的狀況。光線追跡方法首先計算一條光線在被介質吸取,或者改變方向前,光線在介質中傳播的距離,方向以及到達的新位置,而後從這個新的位置產生出一條新的光線,使用一樣的處理方法,最終計算出一個完整的光線在介質中傳播的路徑。windows

光線追蹤 VS 光柵化架構

光柵化渲染管線(Raster pipeline)是傳統的渲染管線流程,是以一個三角形爲單元,將三角形變成像素的過程,在目前圖像API和顯卡硬件有着普遍的支持和應用。dom

光線追蹤渲染管線(Ray tracing pipeline)則是以一根光線爲單元,描述光線與物體的求交和求交後計算的過程。和光柵化線性管線不一樣的是,光線追蹤的管線是能夠經過遞歸調用來衍生出另外一根光線,而且執行另外一個管線實例。編輯器

1.2 光線追蹤的特色

運用光線追蹤技術,有如下渲染特性:ide

  • 更精確的反射、折射和透射。
  • 更準確的陰影。包括自陰影、軟陰影、區域陰影、多光源陰影等。
  • 更精準的全局光照。
  • 更真實的環境光遮蔽(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、光線追蹤的原理

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六、25六、409六、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()遞歸追蹤光線。
  • IntersectionAny Hit:當TraceRay()內檢測到光線與物體相交時,會調用此shader,以便使用者檢測此相交的物體是否特殊的圖元(球體、細分表面或其它圖元類型)。
  • Closest HitMiss:當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可方便調試各種調用棧、渲染狀態及資源等信息。

3、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的光線追蹤:

一、打開項目設置(文件-項目設置)界面。

二、找到項目設置的平臺-windows頁, Default RHI選成DirectX 12。

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

勾選光線追蹤以後,編輯器會提示是否重啓,點擊是便可。

若是熟悉引擎的配置文件及命令行啓動,能夠直接修改ConsoleVariables.ini

r.RayTracing=1
r.SkinCache.CompileShaders=1

而後在啓動UE工程時附加-d3d12標記,便可直接啓用DX12模式渲染。

四、添加後處理卷積(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次採樣的原始噪點圖;下:開啓了降噪處理的畫面。

    降噪算法更多信息可參見:

4、UE的底層實現

因爲UE的源碼不少邏輯對是否開啓光線追蹤進行了判斷,影響面很是廣,C++和Shader文件涉及數量成百上千。Shader代碼主要集中在:

  • Engine\Shaders\Private\RayTracing\目錄。

    此目錄基本囊括了光線追蹤全部特性的shader實現代碼:

  • Engine\Shaders\Private\PathTracing\目錄。

    此目錄下是路徑追蹤版本的shader代碼。

因爲精力有限,沒法對全部涉及光線追蹤的邏輯進行分析,下面只對Ray Tracing版本的全局光照shader作剖析,其它特性(反射、AO、透明、陰影等)的shader可自行看UE源碼。

光線追蹤版本的全局光照shader涉及的文件主要有:

  • \Engine\Shaders\Private\RayTracing\RayTracingCommon.ush
  • \Engine\Shaders\Private\RayTracing\RayTracingGlobalIlluminationRGS.usf
  • \Engine\Shaders\Private\RayTracing\RayTracingGlobalIlluminationCompositePS.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在處理光線追蹤的流程和技術,從而更加具體地理解光線追蹤的實現和應用。

5、總結

本文開頭光線追蹤的概念、特色、歷史、應用,隨着介紹了其原理和常見的僞代碼實現形式,而後介紹了RTX和DXR技術,最後剖析了UE的使用方式和內部實現。可算是一篇比較系統、全面的光線追蹤的技術文章。

固然,光線追蹤的所有及將來沒法在本文體現,更多更新的光追技術隨着時間漸漸涌現,做爲圖像渲染從業者,永遠都要保持學習的動力和探索的腳步。

光線追蹤技術如今只是起點,從未有終點。

The future has just begun!

特別說明

  • 感謝全部參考文獻的做者們!
  • 原創文章,版權全部,禁止轉載!

參考文獻

相關文章
相關標籤/搜索