探究光线追踪技术及UE4的实现

爷,独闯天下 提交于 2019-11-27 13:46:26

一、光线追踪概述

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()递归追踪光线。
  • 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可方便调试各类调用栈、渲染状态及资源等信息。

三、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代码主要集中在:

  • 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在处理光线追踪的流程和技术,从而更加具体地理解光线追踪的实现和应用。

五、总结

本文开头光线追踪的概念、特点、历史、应用,随着介绍了其原理和常见的伪代码实现形式,然后介绍了RTX和DXR技术,最后剖析了UE的使用方式和内部实现。可算是一篇比较系统、全面的光线追踪的技术文章。

当然,光线追踪的全部及未来无法在本文体现,更多更新的光追技术随着时间渐渐涌现,作为图像渲染从业者,永远都要保持学习的动力和探索的脚步。

光线追踪技术现在只是起点,从未有终点。

The future has just begun!

特别说明

  • 感谢所有参考文献的作者们!
  • 原创文章,版权所有,禁止转载!

参考文献

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!