天天看點

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

目錄

  • 一、光線追蹤概述
    • 1.1 光線追蹤是什麼
    • 1.2 光線追蹤的特點
    • 1.3 光線追蹤的曆史
    • 1.4 光線追蹤的應用
  • 二、光線追蹤的原理
    • 2.1 光線追蹤的實體原理
    • 2.2 光線追蹤算法
    • 2.3 RTX和DXR
      • **2.3.1 RTX(NV) **
      • 2.3.2 DXR(Microsoft)
  • 三、UE4的光線追蹤
    • 3.1 UE4光線追蹤的開啟
    • 3.2 UE4光線追蹤的特性
      • 3.2.1 光線追蹤的陰影
      • 3.2.2 光線追蹤的反射
      • 3.2.3 光線追蹤的透明
      • 3.2.4 光線追蹤的環境光遮蔽
      • 3.2.5 光線追蹤的全局光照
      • 3.2.6 光線追蹤的其它特性
    • 3.3 UE4光線追蹤的調試
    • 3.4 UE4光線追蹤的不足
  • 四、UE的底層實作
  • 五、總結
  • 特别說明
  • 參考文獻

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

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

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

與傳統方法的掃描線技術相比,這種方法有更好的光學效果,例如對于反射與折射有更準确的模拟效果,并且效率非常高,是以當追求高品質的效果時經常使用這種方法。

在實體學中,光線追迹可以用來計算光束在媒體中傳播的情況。在媒體中傳播時,光束可能會被媒體吸收,改變傳播方向或者射出媒體表面等。我們通過計算理想化的窄光束(光線)通過媒體中的情形來解決這種複雜的情況。

在實際應用中,可以将各種電磁波或者微小粒子看成理想化的窄波束(即光線),基于這種假設,人們利用光線追迹來計算光線在媒體中傳播的情況。光線追迹方法首先計算一條光線在被媒體吸收,或者改變方向前,光線在媒體中傳播的距離,方向以及到達的新位置,然後從這個新的位置産生出一條新的光線,使用同樣的處理方法,最終計算出一個完整的光線在媒體中傳播的路徑。

光線追蹤 VS 光栅化

光栅化渲染管線(Raster pipeline)是傳統的渲染管線流程,是以一個三角形為單元,将三角形變成像素的過程,在目前圖像API和顯示卡硬體有着廣泛的支援和應用。

光線追蹤渲染管線(Ray tracing pipeline)則是以一根光線為單元,描述光線與物體的求交和求交後計算的過程。和光栅化線性管線不同的是,光線追蹤的管線是可以通過遞歸調用來衍生出另一根光線,并且執行另一個管線執行個體。

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

  • 更精确的反射、折射和透射。
  • 更準确的陰影。包括自陰影、軟陰影、區域陰影、多光源陰影等。
  • 更精準的全局光照。
  • 更真實的環境光遮蔽(AO)。
探究光線追蹤技術及UE4的實作

光線追蹤技術可以精确地反映複雜的反射、折射、透射、陰影、全局光等實體特性。

當然,光線追蹤也不是萬全的渲染技術,它有苛刻的硬體要求、有限度的渲染特性支援以及噪點幹擾等負面特點。後面章節會更多談及。

光線追蹤渲染技術從自然界中的光線簡化、光線投射算法、光線追蹤算法一步步演變而來。

  • 光線投射算法(1968年)

    由Arthur Appel提出用于渲染的光線投射算法。光線投射的基礎就是從眼睛投射光線到物體上的每個點,查找阻擋光線的最近物體,也就是将圖像當作一個屏風,每個點就是屏風上的一個正方形。

    根據材料的特性以及場景中的光線效果,這個算法可以确定物體的濃淡效果。其中一個簡單假設就是如果表面面向光線,那麼這個表面就會被照亮而不會處于陰影中。

    光線投射超出掃描線渲染的一個重要優點是它能夠很容易地處理非平面的表面以及實體,如圓錐和球體等。如果一個數學表面與光線相交,那麼就可以用光線投射進行渲染。複雜的物體可以用實體造型技術建構,并且可以很容易地進行渲染。

  • 光線追蹤算法(1979年)

    最先由Turner Whitted于 1979 年做出的突破性嘗試。以前的算法從眼睛到場景投射光線,但是并不跟蹤這些光線。而光線追蹤算法則追蹤這些光線,并且每次與物體表面相交時,計算一次所有光影的貢獻量。

  • 光線追蹤API及硬體內建(2018年)

    在早些年,NV就聯合Microsoft共同打造基于硬體的新一代光線追蹤渲染API及硬體。在2018年,他們共同釋出了RTX(Ray tracing X)标準。Direct X 12支援了RTX,而NV的RTX系列顯示卡支援了RTX技術,進而宣告光線追蹤實時化的到來。

    探究光線追蹤技術及UE4的實作
    NV RTX示範視訊截圖。
  • UE內建光線追蹤(2019年)

    UE于2019年4月釋出了4.22版本,該版本最耀眼的新特性無疑是支援了光線追蹤技術。這将助力廣大啟用UE的個人或團隊更加有效地渲染出照片級的畫面。

    探究光線追蹤技術及UE4的實作
    利用UE的光線追蹤技術渲染出的逼真畫面。

早在上世紀60年代,美國科學家已經嘗試将光線投射應用于軍事領域的計算機圖形生成。随着技術的成熟,很快應用于好萊塢電影及動漫制作。目前,絕大多數需要後期特效的好萊塢電影,除了風格化的類型之外,基本都使用了光線追蹤技術。

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

《獅子王》利用光線追蹤技術渲染的畫面。

近幾年,雖則RTX标準的釋出及顯示卡的支援,光線追蹤技術進入了實時渲染領域,近期釋出的很多3A遊戲大作已經支援了光線追蹤渲染。

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

單機遊戲《光明記憶》開啟和關閉RTX的對比圖。

除了電影、動漫、遊戲領域,光線追蹤技術還可以應用教學、設計、醫學、科學、AR等等領域,以在虛拟的世界渲染出逼真的畫面。

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

利用光線追蹤渲染的室内設計圖。

在幾何光學中,可以忽略光線的波動性而直接簡化成直線,進而研究光線的實體特性。同樣地,在計算機圖形學,也可以利用這一特點,以簡化光照着色過程。

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

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

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

光線追蹤技術就是利用以上的實體原理衍生出來。将眼睛抽象成錄影機,視網膜抽象成顯示螢幕,5億個像素簡化成螢幕像素,從錄影機位置與螢幕的每個像素連成一條射線,去追蹤這些射線與場景物體交點的光照資訊。

當然,實際的光線追蹤算法會更加複雜,下一小節會較長的描述。

與傳統的光栅化渲染技術相比,光線追蹤的算法過程還是比較明晰的。

以視點為起點,向場景發射N條光線,然後根據碰撞點的材質進行BXDF、BRDF的運算,然後再進行漫反射、鏡面反射或者折射,如此遞歸循環直到光線逃離場景或者到達最大反射次數,最後對N條光線進行蒙特卡洛積分即可獲得結果。

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

結合上圖,可以将光線追蹤的算法過程抽象成以下僞代碼:

周遊螢幕的每個像素 {
  建立從視點通過該像素的光線
  初始化 最近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\),通過縮小采樣結果,防止光源的貢獻量太大。

當然,引入這個方法,如果采樣數量不夠多,會造成光照貢獻量與實際值偏差依然會很大,形成噪點。随着采樣數量的增加,局部估算越來越接近實際光照積分,噪點逐漸消失(下圖)。

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

從左到右分别對應的每個象素采樣為1、16、256、4096、65536。

結合了蒙特卡羅積分和重要性采樣的光線追蹤技術,也被稱為路徑追蹤(Path tracing)。

NV作為世界級的圖形學界的探索先鋒隊,在光線追蹤方面有着深入的研發,最終抽象成技術标準RTX平台。

随着DirectX 12的DXR和Vulkan的支援,使得支援硬體級的光線追蹤技術漸漸普及。NV最先在Turing架構的GPU支援了RTX技術:

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

由上圖可見,最上層是使用者層(MDL和USD),包含了深度學習和普通應用開發;中間層是圖形API層,支援RTX的有OptiX、DXR、Vulkan,OpenGL并不支援RTX;最底層就是RTX平台,它又包含了4個部分:傳統的光栅化器、光線追蹤(RT Core)、CUDA電腦、AI核心。

當然,除了Turing架構的GPU,還有PASCAL、VOLTA、TURING RTX等架構的衆多款GPU支援RTX技術。(下圖)

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

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

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

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

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

上圖涉及的BVH(Bounding volume hierarchy)是層次包圍盒,是一種加速場景物體查找的算法和結構體。

對于開發者,需要根據品質等級,做好各類名額預選項,以便程式能夠良好地運作在各個畫質級别的裝置中。

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

在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()

接口建構。

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

如上圖,對于場景中的每個幾何體,在GPU内部都存在兩個級别的加速結構。底層加速結構(Bottom-Level AS)從輸入的圖元資訊建構而成,如三角形、四邊形。頂層加速結構(Top-Level AS)從底層加速結建構立而來,相當于是底層加速結構的執行個體,儲存了底層結構的變換矩陣和shader偏移。

Shader映射表(Shader Table)描述了shader與場景的哪個物體關聯,也包含了shader中涉及的所有資源(紋理、buffer、常量等)。

在GPU底層,Shader映射表是一個等尺寸的記錄體(record),每個記錄體關聯着帶着一組資源的shader(或相交組(Hit group))。通常每個幾何體存在一個記錄體。

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

由上圖可見,每個記錄體由shader編号起始,随後存着CBV、UAV、常量、描述表等shader資源。

這種雙層架構的好處是将資源和執行個體化分離,加速執行個體建立和初始化,降低帶寬和顯存占用。

PIX作為Microsoft的老牌且強大的圖形調試軟體,在DXR釋出之初就支援了對它的調試。利用PIX可友善調試各類調用棧、渲染狀态及資源等資訊。

探究光線追蹤技術及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。

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

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

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

勾選光線追蹤之後,編輯器會提示是否重新開機,點選是即可。

如果熟悉引擎的配置檔案及指令行啟動,可以直接修改

ConsoleVariables.ini

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

然後在啟動UE工程時附加

-d3d12

标記,即可直接啟用DX12模式渲染。

4、添加後處理卷積(Post Process Volume)。

重新開機完編輯器,等待Shader全部編譯完成,便可以往關卡添加後處理體積,以便啟用光線追蹤的相關特性,調節各類參數。

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

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

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

UE4目前版本可支援的光線追蹤有以下特性:

可模拟多光源的過渡性軟陰影、區域陰影、模型的自陰影,以及其它各種複雜的遮擋陰影,能夠與場景物體緊密結合,無明顯瑕疵。

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

上:光栅化陰影;下:光線追蹤陰影。

光線追蹤的反射可實時動态反射場景的任意物體,完全不受之前SSR、平面反射、立方體圖等的限制,所渲染的結畫面更加真實,融入場景内。

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

上:SSR效果;下:光線追蹤反射。

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

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

上:光栅化的反射;下:光線追蹤的反射。

光線追蹤的透明可以精确地模拟玻璃、流體等材質的實體正确的反射、吸收、折射等表面特性。

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

上:光栅化的透明;下:光線追蹤的透明。

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

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

上:SSAO;下:光線追蹤的AO。

光線追蹤模式的全局光照增加了光線在場景中的若幹次彈跳,并權重它們的權重,使得物體與物體、物體與光源之間的關系更實體正确,渲染效果更真實。

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

上:隻有天空光;下:光線追蹤的全局光。

以上皆是靜态地對比傳統渲染技術和光線追蹤的效果,下面的連結提供了視訊動态地對比它們之間的差别,能更直覺體會到光線追蹤的特性:

  • NVIDIA RTX and GameWorks Ray Tracing Technology Demonstration

UE還提供了路徑追蹤的渲染模式,在場景編輯視窗,将視圖模式(View Mode)選為路徑追蹤(Path Tracing)即可開啟:

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

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

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

上:光線追蹤;下:路徑追蹤。

以上特性除了可以在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。

由于UE4的光線追蹤采用的是DXR,是以可以使用微軟的PIX調試UE4光線追蹤的應用程式。

此外,UE4本身也提供了一些指令和GUI調試光線追蹤的資訊和性能。

  • Stat GPU

    :可跟蹤GPU的光線追蹤的各個特性的消耗。
    探究光線追蹤技術及UE4的實作
  • Stat D3D12RayTracing

    :可檢測光線追蹤使用的資源。
    探究光線追蹤技術及UE4的實作
  • 視圖模式的調試視窗:可實時檢視光照各個部分的GBuffer資料等。
    探究光線追蹤技術及UE4的實作

由于RTX、DRX等技術标準尚處于初始階段,平台和技術标準的存在着不少缺陷,這也同樣存在于UE4的光線追蹤當中。

  • 對軟體、硬體要求苛刻。

    UE4的光線追蹤開啟的先決條件足以印證這一點。筆者的RTX 2060在開啟光線追蹤之後,無降噪算法的情況下渲染相同的場景,幀率大概不到光栅化渲染的一半。

  • 不支援部分傳統渲染特性。

    更具體地,不支援或不完全支援光照透射(Light Transmission)、體積霧(Volumetric Fog)、光照函數(Light Functions)、世界坐标偏移(World Position Offset)、植被(Foliage)等等。

    探究光線追蹤技術及UE4的實作
    探究光線追蹤技術及UE4的實作
    更多請參看官方說明文檔:Ray Tracing Supported Features。
  • 畫面噪點。

    由于實時光線追蹤不可能對表面的每次BxDF執行半球積分,隻能利用重要性采樣估算光照積分。由于通常采樣次數不足,隻能用很低的采樣次數(如1次),光照積分與實際值偏差較大,是以會形成很嚴重的噪點,特别是在陰影處。(下圖)

    探究光線追蹤技術及UE4的實作
    讓人欣慰的是,目前存在很多降低噪點的方法,比如NV的AI降噪,可利用1采樣高噪點圖,通過降噪算法,獲得很好的降噪結果。
    探究光線追蹤技術及UE4的實作
    探究光線追蹤技術及UE4的實作

    上:1次采樣的原始噪點圖;下:開啟了降噪處理的畫面。

    降噪算法更多資訊可參見:

    • Ray Tracing in Games with NVIDIA RTX (Presented by NVIDIA)
    • Real-Time_Rendering_4th-Real-Time_Ray_Tracing

由于UE的源碼很多邏輯對是否開啟光線追蹤進行了判斷,影響面非常廣,C++和Shader檔案涉及數量成百上千。Shader代碼主要集中在:

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

    此目錄基本囊括了光線追蹤所有特性的shader實作代碼:

    探究光線追蹤技術及UE4的實作
  • Engine\Shaders\Private\PathTracing\目錄。

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

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

由于精力有限,無法對所有涉及光線追蹤的邏輯進行分析,下面隻對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在處理光線追蹤的流程和技術,進而更加具體地了解光線追蹤的實作和應用。

本文開頭光線追蹤的概念、特點、曆史、應用,随着介紹了其原理和常見的僞代碼實作形式,然後介紹了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
  • 【遊戲開發】淺談光線追蹤
  • 為什麼光線追蹤會出現噪點,為什麼需要蒙特卡洛?
  • 光線追蹤與實時渲染的未來
  • Introduction to NVIDIA RTX and DirectX Ray Tracing
  • Bounding volume hierarchy

繼續閱讀