UE 光线追踪:概念、Render 模块与系统组成

从光栅与光追两条路线讲起,落到 UE 5.6 的 Render 模块、Lumen、通用光追系统与工程开关,附源码索引与控制台速查。

最近用 UE5.6 做项目,踩了一堆光追相关的坑——地编、美术、程序那边都遇到过不少两眼一抹黑的时刻。把一些概念性的东西和引擎模块设计整理一下,下次碰到光追相关的问题至少有个基础知识抓手。

光线追踪到底是什么、UE 的渲染模块如何容纳"光线"、以及 UE 的光追系统由哪些部分组成。概念口径参考《计算机图形学基础》(Fundamentals of Computer Graphics, 5th ed.),引擎实现参考 UE 5.6 本地源码。

0. 阅读说明

0.1 内容与基准

章节内容面向依据
第 1 章光线追踪是什么概念《计算机图形学基础》第 3-4 章
第 2 章UE Render 模块如何容纳光追工程UE 5.6 源码
第 3 章Lumen:UE 处理间接光的第一层架构工程UE 5.6 源码
第 4 章UE 通用光追系统设计工程UE 5.6 源码
第 5 章工程视图:要动的开关、常见翻车与排查入口美术 / TA / 程序UE 5.6 源码
第 6 章附录:源码索引 / 控制台速查 / 参考工具UE 5.6 源码

0.2 三条阅读路径

正文每节开头(以及若干关键段)有一行 > **一句话**:...,它是该节结论的白话版本。按下面三条路径读,可以只看自己需要的那部分:

角色建议路径读完能得到什么
美术 / 地编第 1 章 -> 2.0 节的总览图 -> 5.1 节(要动的开关)-> 5.2 节(常见翻车)知道光追在哪里介入画面、动哪个开关会影响什么、哪些设置容易被误解
TA / 技术美术上面 + 3.2 节(Lumen 组成)-> 3.4 节(每帧流程)-> 4.8 节(降噪)-> 5.3 节(排错谱系)能做质量与性能取舍,能定位常见异常
程序全篇;第 5 章可跳到 5.3 节拿到"概念 -> 代码实体"的映射、调用链入口与版本边界

0.3 术语最小集

首次出现的缩写先在这里给一句话口径,正文不再重复解释:

#术语一句话
1光栅化(Rasterization)把三角形投影到屏幕、按对象顺序逐像素写入;UE 的主流程
2G-Buffer延迟渲染的中间缓冲集合(法线、粗糙度、深度等),后续光照与降噪都从这里取信息
3Pass管线里的一段独立工作(一次绘制或一次 compute dispatch),有明确的输入输出资源
4RDG(Render Dependency Graph)UE 的 Pass 编排系统:Pass 只声明输入输出,排序与资源分配由它负责
5BVH(Bounding Volume Hierarchy)按包围盒分层组织的加速树,用于把"找最近交点"从线性扫描降到近似对数级
6BLAS(Bottom-Level Acceleration Structure)单个几何体内部的 BVH,只描述形状,不含世界位姿
7TLAS(Top-Level Acceleration Structure)场景实例的 BVH,每个实例引用一个 BLAS 并携带变换与掩码
8SBT(Shader Binding Table)把"几何 + 实例 + 材质"映射到具体命中着色器的索引表,DXR 规范要求
9Hit Shader光线命中后执行的着色程序:Closest Hit(取最近交点着色)、Any Hit(常用来做遮挡判定)
10permutation(变体)同一份 shader 源码按编译期开关(如是否启用 Nanite 光追)生成的多份实例
11全管线光追 / Inline 光追前者用独立的 RayGen 管线派发;后者在普通 compute shader 内嵌 TraceRayInline 调用
12SER(Shader Execution Reordering)硬件按材质/命中给光线重新排序、改善执行相干性的能力;硬件不支持时开关无效果
13时空降噪用当前帧邻域(空间)与历史帧(时间)把"每像素少量光线"的结果补成干净画面

0.4 约定

  • 引擎源码路径均相对于 UE 根目录(如 Engine/Source/Runtime/...)。
  • 概念口径统一参照《计算机图形学基础》(Fundamentals of Computer Graphics, 5th ed.)对应章节。
  • 文中 > **一句话**:... 是该节结论的白话版本,便于先建立直觉;正文其余部分保留术语精确,机制、公式、时序与约束按原样陈述。

1. 光线追踪是什么

1.1 渲染的两条路线:逐物体 vs 逐像素

渲染的目标是将"三维场景"转换为"二维像素图"。像素图的本质是什么?光栅图像是一个二维数组,每个元素(像素)存储图像在该位置附近颜色的局部平均值(点样本)。[1] 换言之,无论采用何种渲染方法,最终产物几乎总是一张光栅图像。

实现这一转换存在两种相反的组织方式:

对象顺序(Object-Order)            图像顺序(Image-Order)
for every object:                  for every pixel:
  计算它影响了哪些像素                计算它被哪个物体遮挡、
     -> 光栅化 / 扫描线                以及该物体的着色结果
                                        -> 光线追踪
  • 光栅化采用对象顺序:将三角形投影到屏幕空间,逐像素覆盖写入帧缓冲。其优势在于速度,但"某一像素究竟能看到什么"并非原生问题——可见性需要 Z 缓冲、阴影需要 Shadow Map 等额外机制来近似。
  • 光线追踪采用图像顺序:直接从"像素应看到什么"这一问题出发,天然能够精确求解可见性、阴影、反射、折射等效果。这些在对象顺序框架中需要大量近似手段的问题,在光线追踪中是算法流程的自然产物。[2]

需要澄清的常见误区:“光追输出光栅图像"并不矛盾。光追只是通过图像顺序的方式确定每个像素的颜色,输出仍然是光栅位图——图像在数学上可抽象为定义在矩形区域上的连续函数,光栅像素只是该函数的离散采样。[1]

1.2 基本做法:生成光线、求交、着色

基本光线追踪算法可归纳为三步[3]:

  1. 光线生成(Ray Generation):对每个像素,从视点 e 发射一条穿过该像素的视线(view ray)。视线的数学表达是三维参数直线:
p(t) = e + t * (s - e)

其中 e 为视点位置,s 为像素在图像平面上的采样点,参数 t 表示沿视线方向的距离。t < 0 对应相机后方的点,t = 0 即为视点本身。[4]

  1. 求交(Intersection):寻找视线与场景中最近的有效交点。这一步有两个关键语义:

    • “最近"的含义:离视点最近的物体会遮挡其后所有物体,因此只需保留最小的合法 t 值。
    • 与一组物体求交的朴素做法:把一组物体视为"一个复合表面”,逐个求交并持续收缩搜索区间 [t0, t1]——每找到一个更近的命中点,就把区间上界 t1 收紧到该点的 t,后续物体只在更窄的区间内参与竞争。[5]
// 一组物体求交:最近交点 + 区间收缩(示意代码)
HitRecord Hit(Group g, Ray r, float t0, float t1) {
    HitRecord closest = HitRecord();        // t = INFINITY,表示未命中
    for (auto surface : g.surfaces) {
        HitRecord rec = surface.Hit(r, t0, t1);
        if (rec.t < t1) {                   // 仅接受比当前更近的命中
            closest = rec;
            t1 = rec.t;                     // 收缩搜索区间
        }
    }
    return closest;
}

需要注意的是:逐物体线性扫描的时间复杂度为 O(N),在大规模场景中不可用。工程实践中会引入加速结构(如 BVH)将"查找最近物体"降为近 O(log N)——这正是第 4 章 UE 光追系统中 BLAS/TLAS 所要解决的问题。在概念上,加速结构同样被视为"一个可求交的对象”,与普通表面在组合层级上是平级的。[5]

  1. 着色(Shading):在获得交点位置、表面法线、材质属性与光源信息后,计算该像素的最终颜色。着色模型本身与光栅化完全通用——区别仅在于几何信息(交点、法线)与可见性信息是求交阶段直接给出的精确值。[6]

阴影是这套流程的自然延伸:判断点 x 是否被某光源照亮,只需从 x 出发向光源方向发射一条阴影光线(shadow ray);若中途被其他物体遮挡,则 x 处于该光源的阴影中。为避免 x 所在表面因浮点误差被自身误判为遮挡,阴影光线的起始参数需设置一个极小的正偏移 t = epsilon,而非从 t = 0 开始。

1.3 为什么是从摄像机"反向"发射光线

这是初学者最常遇到的困惑:现实中光从光源射向场景,为何算法要反过来从相机发射?

答案可以从物理与计算两个层面分别理解:

  1. 物理层面:光路具有可逆性。一条从光源出发、经多次弹射最终进入相机的光路,其反向路径(从相机到光源)在物理上是等价的。渲染所关心的只是"最终到达相机的光",无论正向还是反向计算,结果一致。

  2. 计算层面:正向追踪效率极低。若从光源向所有方向均匀发射"光子",能进入某一像素视锥的比例微乎其微——绝大多数计算资源都会浪费在永远不会被看见的路径上。反向追踪则不同:每条光线都指向一个确定的像素,发射的全部视线都对最终图像有贡献。[2]

因此"反向"并非对物理事实的违背,而是一种采样策略:图像顺序的光线追踪天然只采样"对最终图像有贡献的路径",将计算量集中在有效区域。

正向(物理直觉):  light -> 场景散射 .................... 绝大多数光子永远无法抵达相机
反向(算法实现):  camera -> 像素对应方向 -> 命中物体 -> (按需)继续发射阴影/反射光线
                    ^ 每条光线都对对应像素有确定性贡献

1.4 光线追踪与 Shader 的关系

一句话:光追只回答"光线碰到谁",碰到之后长什么样由着色程序决定——这条分界线在硬件光追里就是"几何层"与"Hit Shader"的分工。

“求交确定最近物体"只回答了"光线击中了谁”,还未回答"该物体呈现什么样"。答案取决于命中点执行哪段材质/光照程序。光线追踪与着色器(Shader)的关系可从以下角度理解:

  • 光线追踪框架先完成"几何判定",再向着色阶段提供标准输入参数:求交直接给出交点位置 p = e + t*d 与表面法线 n;视线方向 v 取入射光线方向的反向(即 v = -d_hat);此外还包括材质属性(漫反射/镜面系数、粗糙度等)与纹理坐标——以上构成着色所需的完整输入集。
  • 阴影 / 光源可见性属于附加输入:判断交点是否被某光源照亮,需沿光源方向 l 额外发射一条阴影光线,将遮挡结果(二元或软阴影系数)并入着色计算[7]、[8]。
  • 着色模型本身(漫反射 + BRDF、反射、折射)在光线追踪与光栅化之间完全通用——同一套着色公式可无缝用于两条渲染路线。[6]

进入 GPU 硬件光追(DXR / Vulkan RT)时代后,上述流程被拆分为固定管线遍历 + 多阶段可编程 Shader:遍历由硬件完成,可编程阶段负责生成光线、判定命中与计算颜色。这四个阶段(Ray Generation / Intersection 与 Any Hit / Closest Hit / Miss)的职责清单放在 4.3 节,与 UE 的 SBT 和材质 Hit Shader 一起讲——它们是同一件事的规范侧与实现侧。

核心结论可以用一句话概括:光追框架决定"光线去向、命中对象",Shader 决定"命中后计算的颜色值"。 在 UE 中,这两个角色被清晰地划分为两个系统:前者对应第 4 章的"光追场景 + 着色器绑定表",后者对应"材质 Hit Shader"(Closest Hit / Any Hit 与 UE 材质系统的绑定)。这条分界线是阅读后续 UE 实现代码的关键钥匙。

本章小结:光追以图像顺序从像素出发,经生成 -> 求交 -> 着色三步;反向发射是光路可逆下的高效采样策略;硬件光追拆为 RayGen / Intersection / ClosestHit / Miss 四阶段,光追框架与 Hit Shader 职责分离。

2. UE 渲染模块:光栅为主,光线为辅

2.0 一帧总览

一句话:UE 每帧先按光栅化画出画面,只有当某个效果"光栅化做不好"时,才临时把场景整理成光追可用的形式、额外发射一些光线去补,补完再降噪合成。

这段骨架的三层含义分别对应后文三章:整理场景与发射光线是第 4 章(通用光追系统);“间接光"这条路主要由第 3 章(Lumen)负责;“哪些效果需要额外光线、用什么开关控制"在第 5 章。

两个容易误解的点:一是光追不替代光栅化,它是插在主循环里的若干片段,关掉后画面仍然完整,只是少了那几类效果;二是**“有光追效果"不等于"每帧重建全部加速结构”**——几何的加速结构按静态 / Nanite / 动态三类分别维护,重建是增量的(4.2 节)。

第 1 章讲概念;本章进入 UE 工程实现。首先需要建立的整体认知是:UE 渲染器并未“以光追取代光栅化”,而是以光栅为主流程,将光追作为若干嵌入 Pass 执行。理解这一"嵌入"机制的三个关键维度是:代码组织位置(Renderer 模块)、Pass 编排方式(RDG)、以及光追 Pass 在主循环中的挂载点。

2.1 Renderer 模块与 RDG

UE 渲染器位于 Engine/Source/Runtime/Renderer/。核心实现代码集中在 Runtime/Renderer/Private/ 下,按功能拆分子目录。与光线追踪主题相关的目录与文件包括:

Engine/Source/Runtime/Renderer/Private/
  DeferredShadingRenderer.{h,cpp}   <- 延迟渲染主循环(所有光追 Pass 的最终编排入口)
  RayTracing/                       <- 通用硬件光追设施与独立 RT Pass
  Lumen/                            <- Lumen(全局光照 / 反射)实现
  Nanite/                           <- Nanite(含 Nanite 专用光追 BLAS 管理)
  PathTracing.{h,cpp}               <- 参考级 Path Tracing(收敛级完整光追)
  MegaLights/                       <- 5.6 新增灯光虚拟化系统(可选硬件光追)
  ScreenSpaceDenoise.{h,cpp}        <- 屏幕空间时空降噪器(光追效果降噪的统一入口)

自 UE 5.0 起,主渲染器废弃了"在普通函数内串行执行立即模式 RHI 调用"的旧模式,全面切换为RDG(Render Dependency Graph)声明式 Pass 编排:每个 Pass 在注册时显式声明输入与输出的 RDG 资源,RDG 内部构建依赖拓扑图,自动完成资源生命周期管理、依赖排序、资源屏障优化与 GPU Render-Pass 合并。RDG 的核心入口是 FRDGBuilder,头文件位于 Engine/Source/Runtime/RenderCore/Public/RenderGraphBuilder.h。渲染器的每一个步骤都注册为 RDG Pass,所有 RHI 调用被约束在 Pass 的执行回调内执行。光线追踪相关逻辑完全复用这套 RDG 体系,不存在独立的调度路径——唯一区别仅在于 Pass 内部执行的是光线追踪 GPU 阶段(RayGen / TraceRay / DispatchRays),而非常规的光栅化或计算着色器调度。

2.2 RHI 层的光追抽象:DXR 的封装

RHI(Render Hardware Interface)是 UE 对所有图形 API 的统一抽象层,位于 Engine/Source/Runtime/RHI/。硬件光追所需的资源对象——加速结构、光追管线状态、Shader 绑定表——在 RHI 层均有对应的抽象接口:FRHIRayTracingGeometry(BLAS)、FRHIRayTracingScene(TLAS)、FRHIRayTracingPipelineState 等,统一声明于 Engine/Source/Runtime/RHI/Public/RHIResources.h。

具体实现落在各平台的 RHI 后端。以桌面 D3D12 为例,Engine/Source/Runtime/D3D12RHI/Private/D3D12RayTracing.{h,cpp} 将 UE 的 RHI 抽象翻译为 DXR 原生调用,包括加速结构构建、DispatchRays 派发等。更高阶的执行优化(如 Shader Execution Reordering,SER)由渲染层通过控制台开关暴露(Lumen 侧对应 r.Lumen.HardwareRayTracing.ShaderExecutionReordering),实际生效与否取决于平台与驱动的能力支持,RHI 层在运行时以能力探测结果为最终依据。

UE 5.x 中硬件光追的主力后端为 D3D12 + DXR。光追功能的总开关是 r.RayTracing,它驱动渲染器内部 FFamilyPipelineState::bRayTracing 状态位;各类具体效果(阴影、AO、半透明等)在项目设置与 RaytracingOptions.h 中进一步细分独立开关。引擎代码中大量使用 IsRayTracingEnabled() / RHISupportsRayTracing*() 作为能力门禁,在光追不可用时零成本短路相关路径。

2.3 场景数据流与主循环挂载点

光线追踪需要遍历"完整场景”,因此对几何可见性的要求高于光栅化。UE 中场景侧的数据流向为:

FPrimitiveSceneInfo(CPU 端渲染代理)
      |
      v
FGPUScene(GPU 场景,存储实例数据 / 变换矩阵,挂载于 SceneRenderer::Scene->GPUScene)
      |
      v
FRayTracingScene(每帧重建的光追专用场景)

设计要点:UE 5 引入 GPUScene 后,光追场景的构建(第 4.1 节的 FRayTracingScene::Create)直接以 GPU 场景为输入,而非逐 Primitive 从 CPU 搬运数据。这一设计从根本上降低了场景构建的 CPU 开销——每帧只需将实例信息整理进加速结构,几何数据本身可跨帧缓存复用。

在延迟渲染器 FDeferredShadingSceneRenderer(DeferredShadingRenderer.cpp)的主循环中,有两个编排函数是光追链路的核心入口:

  • DispatchRayTracingWorldUpdates:收集本帧所有需要更新的光追几何与实例,提交 BLAS 构建与场景构建请求(Create + Build)。
  • SetupRayTracingRenderingData:为每个 View 组装 Lumen / Nanite / 光源网格等光追所需的统一缓冲,并判断本帧是否存在 Inline 光追 Pass。

实际的各光追 Pass 派发则嵌入在光栅化主循环的对应阶段内,例如天空光采样阶段、半透明渲染阶段。这些 Pass 的执行条件通过一组 ShouldRender* 判断汇总为"本帧是否需要构建光追场景"的综合标志,用于在不需要光追时提前短路整条场景构建链路。这正是"光追是嵌入光栅主循环的 image-order 计算片段"这一论断在代码层面的直接体现。

本章小结:UE 以光栅为主流程,光追作为嵌入 Pass 执行;Renderer 模块通过 RDG(FRDGBuilder)统一调度 Pass;RHI 层抽象 DXR 调用;场景数据沿 FPrimitiveSceneInfo -> FGPUScene -> FRayTracingScene 流转,主循环由 DispatchRayTracingWorldUpdates 和 SetupRayTracingRenderingData 两个编排函数驱动。

3. Lumen:UE 处理"间接光"的第一层架构

一句话:间接光与反射是最"吃光追能力"的两件事,UE 把它们交给 Lumen;Lumen 是第 4 章那套通用光追基础设施的最大使用者,而不是基础设施本身。

本章与第 4 章的读法(先给结论,避免读反顺序):第 4 章的通用光追系统是基础设施层,负责加速结构与命中分派,服务所有消费者;Lumen 是它的首要租户。Lumen 既可以完全不发射硬件光线(软件模式:屏幕空间追踪 + 距离场追踪),也可以借用基础设施发射硬件光线(硬件模式)。因此 3.2 与 3.4 节里的"光线来源"应理解为一个可替换的部件;若更想先看底座,可先读 4.1、4.2 节再回本章。

从用户视角提问"UE 5 如何处理光线”,第一个答案往往不是某个"光追开关",而是 Lumen——它是 UE 当前全局光照(GI)与反射的默认架构。在深入通用光追系统之前,先建立对 Lumen 的完整认知,原因有三:

  • Lumen 是 GI / 反射(多次弹射、间接光)这类"最依赖光追能力"问题的当前标准答案;
  • Lumen 可以选择性启用硬件光追作为部分光线的来源,是通用 RT 系统的首要租户;
  • 第 4 章的"通用光追系统"负责构建底层加速结构,供 Lumen(可选)与其他独立效果(阴影、AO、半透明等)共同复用。

3.1 为什么需要 Lumen

“间接光照”(Global Illumination, GI)是实时渲染中最具挑战的问题:一个像素的最终颜色不仅来自直接光照,还包含周围物体反弹而来的间接光,且反弹次数在理论上是无限的。在 Lumen 出现之前,实时引擎对这一问题的解决方案是一系列静态或近似手段(详见 3.3 节历史回顾):光照贴图烘焙(场景变更后需重新烘焙)、屏幕空间 GI(仅覆盖屏幕可见区域)、反射捕获体与 SSR(反射保真度有限)等。

Lumen 的定位是系统性补齐上述短板:提供全动态、可扩展的实时间接光照与反射方案——太阳方位可调、场景几何体可移动/销毁、物体位姿可动态变化,间接光照结果能够实时跟上,视觉目标逼近离线烘焙与离线光追的质量。Lumen 是 UE 5 的默认光照架构(延迟渲染器中 GI 与反射的"方法枚举"默认指向 Lumen,见 3.3 节)。

3.2 Lumen 的组成

Lumen 并非单一 Pass,而是一套场景表示 + 多级缓存 + 光线来源策略的组合体系。基于 5.6.1 源码(Engine/Source/Runtime/Renderer/Private/Lumen/),其核心可拆分为四个组件:

组成模块核心作用5.6.1 代表文件(短名)
场景表示将场景转换为"可供光线跟踪"的近似表示,避免对所有三角形做精确求交LumenScene.cpp、LumenMeshCards.cpp、LumenMeshSDFCulling.cpp
Screen Probes(屏幕探针)在屏幕空间稀疏布置探针,向四周发射光线,单帧内产出可用的间接光照结果LumenScreenProbeGather.cpp、LumenScreenProbeFiltering.cpp
Radiance Cache(辐照度缓存)将屏幕探针的结果缓存到世界空间并跨帧累积,使光照信息向屏幕不可见区域传播LumenRadianceCache.cpp
Surface Cache(表面缓存)在 Mesh 表面以卡片(Card)为单位存储辐照度与材质结果,供光线命中时直接查询LumenSurfaceCache.cpp、LumenSurfaceCacheFeedback.cpp

三个缓存组件的协作关系可以直观地理解为:

Screen Probes   -> 负责"当前可见像素"的快速出结果(屏幕空间,单帧内完成)
Radiance Cache -> 负责"不可见 / 远处 / 间接"信息的时空传播(世界空间,跨帧累积)
Surface Cache  -> 光线命中 Mesh 表面时,直接查询"该表面当时的颜色"而无需重新着色

在场景表示层面,Lumen 采用了两项极具 UE 特色的近似技巧:

  • 全局距离场(Global Distance Field / Mesh SDF):将场景网格近似为有符号距离场,光线通过"跟踪 SDF"能够以极低开销穿越大片空旷区域并快速逼近表面(LumenMeshSDFCulling.cpp、LumenMeshCards.cpp)。
  • Mesh Cards(网格卡片):将每个 Mesh 的"宏观光照状态"展开为若干张正交投影的卡片纹理,光线命中卡片即可采样——这正是 Surface Cache 的物理载体。

在光线来源策略上,Lumen 提供两种运行模式:

  1. 软件模式(默认):光线不经过硬件光追单元,而是采用屏幕空间追踪 + SDF 追踪的混合方案。Screen Probe 先对屏幕空间可见的几何发射光线(LumenScreenProbeTracing.cpp),不可见区域则通过 SDF 在世界空间追踪补足。
  2. 硬件光追模式(可选):当平台支持且项目设置开启时,同一套 Lumen 管线将"发射光线"替换为真正的硬件光追调用(硬件光追相关实现见 LumenScreenProbeHardwareRayTracing.cpp、LumenRadianceCacheHardwareRayTracing.cpp、LumenReflectionHardwareRayTracing.cpp 等)。硬件 RT 提供的并非"另一套 GI 求解器",而是更精确、更稳定的光线来源——这正是 UE 官方对"Lumen + RT"关系的定位:RT 是 Lumen 的可选加速器,而非独立的完整 GI 方案。

结构说明:软件模式与硬件模式的差异仅限于"光线来源层"——前者通过屏幕空间追踪 + SDF 追踪实现,后者通过 DXR 的 RayGen 实现(屏幕探针、辐照度缓存、反射三个采样环节各有独立的硬件 RayGen Shader);从"屏幕探针结果"往下的 Radiance Cache 与 Surface Cache 是两种模式共用的统一缓存结构,不随光线来源策略切换。

5.6.1 的硬件 RT 模式还有两项值得注意的工程细节:一是支持标准光追管线与 Inline 光追两种派发方式(控制台开关 r.Lumen.HardwareRayTracing.Inline);二是暴露了大量命中光照相关的细粒度开关(r.Lumen.HardwareRayTracing.HitLighting.* 族,包括命中处直接光、阴影模式、天空光、反射捕获参与度等),这些开关集中定义于 Lumen/LumenHardwareRayTracingCommon.cpp 顶部——若需要深入排查 Lumen 硬件 RT 的各类表现差异,该文件是首选的切入点。

3.3 Lumen 与"上代"渲染方案的差异

本节为架构演进回顾,聚焦历史方案与当前架构的对比,不涉及 5.6.1 代码的具体行为细节。

“上代"概念需要从两个维度分别考察:

(a)UE4.x / 早期 UE5 的实时间接光照方案谱系

历史方案核心思路主要限制
光照贴图烘焙(Lightmap)离线将间接光烘焙进 UV 贴图完全静态;光照/几何变化均需重新烘焙
反射捕获体(Reflection Capture)+ 探针将环境光烘焙为低分辨率 Cubemap仅能表现低频间接光,动态场景适应性差
屏幕空间 GI(SSGI)利用屏幕空间信息估算间接光仅"看见"屏幕内像素;遮挡与屏幕外区域失效
距离场软阴影 / AO基于 Distance Field 近似计算阴影与 AO仅为 AO/阴影级别近似,不涉及 GI 求解
独立硬件光追 GI/反射 Pass(约 5.0-5.1)直接用 RT 渲染间接光与反射蒙特卡洛噪声严重,每像素成本高,缺乏可扩展缓存机制

Lumen 相对于上述方案的根本突破在于多级缓存架构:它不在每个像素上执行完整的路径追踪,而是将"廉价的稀疏光线发射"与"跨帧、跨屏幕的缓存复用"相结合,使计算成本可控且场景保持完全动态。这正是 Lumen 能够将"接近离线质量的 GI"带入实时渲染的核心原因。

(b)独立 RT Pass 架构与 Lumen 的组织差异

早期 UE5 中,硬件光追的 GI 与反射以独立 Pass 的形式存在(如单独的 Ray Tracing GI、Ray Tracing Reflections 效果)。演进至 5.6.1,RayTracing/ 目录中已不存在独立的 GI / 反射 Pass 文件——两者在组织上完全并入 Lumen(反射对应 LumenReflections,GI 对应 Screen Probe / Radiance Cache 体系)。今日 RayTracing/ 目录保留的是"通用光追基础设施 + 非 GI 类效果”(阴影、AO、半透明、天空光等,详见第 4.4 节)。

渲染器内部的 GI / 反射 / AO “方法枚举"仍然保留了回退至上代方案的选项:

// DeferredShadingRenderer.h(5.6.1,节选)
enum class EDiffuseIndirectMethod { Disabled, SSGI, Lumen, Plugin };
enum class EAmbientOcclusionMethod { Disabled, SSAO, SSGI, RTAO };
enum class EReflectionsMethod    { Disabled, SSR, Lumen };

具体含义为:GI 可在 关闭 / SSGI(上代屏幕空间方案)/ Lumen / 自定义插件 之间选择;AO 可在 SSAO / SSGI 附带 AO / 硬件光追 AO(RTAO)之间选择;反射可在 SSR / Lumen 之间选择。默认组合为完整的 Lumen 体系(Lumen GI + Lumen Reflections),项目可根据目标平台按需回退。这张枚举表同时是理解"Lumen 与通用光追职责边界"的坐标系:RTAO 属于通用光追效果,而 GI 与反射默认由 Lumen 体系负责。

3.4 Lumen 每帧工作流概览

进入通用光追系统之前,先用极简流程将 Lumen 的时间线固定在整体框架中(各环节的具体细节在第 4 章相关章节展开):

Lumen 的核心哲学可以概括为:“每帧只发射少量光线,将信息存入缓存跨帧复用。” 这一设计理念是理解 Lumen 体系的核心抓手——无论是性能优化还是问题排查,都可以从"光线发射的稀疏性策略"与"缓存的复用效率"这两个维度系统切入。

本章小结:Lumen 是 UE 默认的 GI/反射架构,通过场景表示(SDF + Mesh Cards)、屏幕探针、辐照度缓存、表面缓存四块组合实现全动态间接光;采用 Screen Probes -> Radiance Cache -> Surface Cache 的多级缓存流水线;软件(Screen+SDF)与硬件(DXR RayGen)双模式仅差异在光线来源层,缓存结构共用。

4. UE 通用光追系统设计

一句话:这一章是"底座”——它只负责两件事:把场景整理成能高效求交的加速结构,以及把"命中后该跑哪个着色器"组织成一张索引表;具体是谁在用(Lumen、阴影、AO、半透明)属于上层。

本章进入 UE 光追系统的核心:通用硬件光追基础设施。它是"光线与谁相交"这一问题的工程实现者——向下对接 D3D12RHI / DXR,向上服务两类消费者:Lumen(作为可选的硬件光线来源)与独立 RT 效果(阴影、AO、天空光、半透明等)。代码主要分布于 RayTracing/ 目录与 DeferredShadingRenderer.cpp 的编排函数中。

4.1 光追场景:FRayTracingScene 的每帧生命周期

UE 中的光追场景是场景级对象(挂载于 FScene,即 Scene->RayTracingScene),而非 View 级对象。在展开生命周期之前,先明确 DXR 加速结构的两个核心概念 BLAS 与 TLAS:

  • BLAS(Bottom-Level,底层加速结构):将单个几何体的三角形组织为 BVH 树,仅描述"形状"(几何内部的层次关系),不包含世界位姿信息。
  • TLAS(Top-Level,顶层加速结构):将"场景中的实例"组织为 BVH 树,每个实例引用一个 BLAS 并携带变换矩阵、实例掩码、命中分组索引等元数据,描述"哪个物体以何种位姿出现在场景中"。

两者涉及两种关键顺序,需严格区分:构建顺序为先 BLAS 后 TLAS(TLAS 的实例必须引用已就绪的 BLAS 地址);光线遍历顺序则恰好相反,先 TLAS 后 BLAS(先确定命中哪个实例,再通过实例变换矩阵将光线变换至该 BLAS 的局部空间,最终查找具体三角形)。这正是下方 Build 方法注释中"BLAS 已就绪时,这里主要做 TLAS 构建"的语义来源。核心接口声明见 RayTracing/RayTracingScene.h:

class FRayTracingScene {
    // Create: 帧起始阶段,基于 FGPUScene 收集/计算需要入场景的实例与几何
    void Create(FRDGBuilder& GraphBuilder, const FViewInfo& View,
                const FGPUScene* GPUScene, ERDGPassFlags ComputePassFlags);

    // Build: 真正提交加速结构构建(BLAS 就绪后,此处以 TLAS 构建为主)
    void Build(FRDGBuilder& GraphBuilder, ERDGPassFlags ComputePassFlags,
               FRDGBufferRef DynamicGeometryScratchBuffer);

    // PostRender: 帧末收尾(统计复位 / 临时资源释放)
    void PostRender(FRDGBuilder& GraphBuilder, ERDGPassFlags ComputePassFlags);
};

每帧生命周期可归纳为"复位 -> 几何/实例更新 -> 场景构建 -> 数据就绪 -> 消费使用 -> 帧末收尾"五个阶段,各阶段对应函数如下(FRayTracingScene 方法见 RayTracingScene.h,编排函数见 DeferredShadingRenderer.cpp):

阶段对应函数核心职责
复位FRayTracingScene::Reset清空内部数组,为新一轮实例收集做准备
几何/实例更新DispatchRayTracingWorldUpdates等待场景初始化任务;提交"待构建 BLAS"列表;处理 Nanite 与动态几何的更新请求
场景构建FRayTracingScene::Create + FRayTracingScene::Build基于 Scene->GPUScene 重建 TLAS 顶层场景
数据就绪SetupRayTracingRenderingData为各 View 组装 Lumen / Nanite / 光源网格等统一缓冲
消费使用各光追 Pass(见 4.4 节)场景消费:阴影 / 天空光 / AO / 半透明 / Lumen 硬件光追
帧末收尾FRayTracingScene::PostRender释放临时资源、复位统计信息

整条链路的总开关由 FamilyPipelineState[&FFamilyPipelineState::bRayTracing](由 r.RayTracing 控制台变量驱动)控制。当整帧不存在任何需要执行的光追 Pass 时,整个场景构建链路会被完全短路——不会白白支付加速结构的构建与维护成本。

具体调用链(下表是 FDeferredShadingSceneRenderer::DispatchRayTracingWorldUpdates 内部的执行顺序,也是排查"加速结构没准备好"这类问题的第一现场):

顺序调用作用
1Nanite::GRayTracingManager.ProcessBuildRequests(GraphBuilder)处理 Nanite 侧已排队的 BLAS 构建请求
2GRayTracingGeometryManager->ForceBuildIfPending(GraphBuilder.RHICmdList, RayTracingScene.GeometriesToBuild)对本帧必须就绪的几何强制完成挂起的 BLAS 构建,输入来自 RequestBuildAccelerationStructure 排队的请求
3Nanite::GRayTracingManager.ProcessBuildRequests(GraphBuilder)(第二次)返回"本帧是否有 BLAS 被重建";为真时对相关视图调用 PathTracingInvalidate(),因为 Path Tracing 的跨帧样本累积要求几何在时间上一致(4.6 节)
4DynamicGeometryUpdateManager->AddDynamicGeometryUpdatePass(...)注册动态几何(蒙皮 / 布料 / 顶点动画)的顶点更新 Pass,并输出本帧的 scratch buffer,供随后的 FRayTracingScene::Build 使用
5((FRayTracingGeometryManager*)GRayTracingGeometryManager)->ResetVisibleGeometries()复位本帧的可见几何集合,供下一帧重新收集

除这条主链外还有两处调用点:一是 FDeferredShadingSceneRenderer::Render 里的 GRayTracingGeometryManager->PreRender() 与其后的 ProcessBuildRequests(GraphBuilder.RHICmdList)(源码此处留有 // TODO: should only process build requests once per frame,即当前实现每帧会处理两次请求);二是主循环中多次出现的 SetupRayTracingRenderingData(GraphBuilder),它是否需要执行由本函数的返回值 bNeedToSetupRayTracingRenderingData 与 Lumen 的两个谓词(Lumen::UseHardwareRayTracedSceneLighting / Lumen::UseAsyncCompute)共同决定,见 4.5 节。

4.2 几何与实例进入加速结构的机制

几何侧(BLAS) 按数据来源分为三类独立管理,实例侧(TLAS) 负责实例收集、剔除与掩码通道。UE 的具体实现如下:

几何侧(BLAS):三类来源的独立管理策略

几何来源核心管理机制
常规静态网格FRayTracingGeometryManager(声明于 Engine 模块的 Rendering/RayTracingGeometryManager.h):请求侧 RequestBuildAccelerationStructure(Geometry, Priority, BuildMode) 入队,BoostPriority 调整排队顺序,RemoveBuildRequest 撤销;提交侧 ProcessBuildRequests(CmdList, bBuildAll) 批量执行;ForceBuildIfPending 用于在本帧内强制完成指定几何的挂起构建
Nanite 网格Nanite::GRayTracingManager(Nanite/NaniteRayTracing.{h,cpp}):Add / Remove / AddVisiblePrimitive 维护参与光追的 Primitive,RequestUpdates 收集更新请求,Update + ProcessUpdateRequests 计算裁剪后可见的集群并生成 BLAS,ProcessBuildRequests 提交构建,EndFrame 收尾;ScheduledBuilds 是"本帧要构建的几何",PendingBuilds 的注释把未构建的几何归因于"每帧 BLAS 构建预算节流"(注释里用的是旧名 r.RayTracing.Nanite.MaxBlasBuildsPerFrame,实际 CVar 为 r.RayTracing.Nanite.MaxBuiltPrimitivesPerFrame)
动态/蒙皮几何FRayTracingDynamicGeometryUpdateManager(Renderer/Public/RayTracingDynamicGeometryUpdateManager.h):AddDynamicGeometryToUpdate 登记需要逐帧更新顶点的几何,AddDynamicGeometryUpdatePass 申请 scratch buffer 并注册更新 Pass;旧的 DispatchUpdates / EndUpdate / Update 已在 5.5、5.6 相继标记弃用,新代码只需调用 AddDynamicGeometryUpdatePass

Nanite 链路:UE 5.6 中 Nanite 网格参与硬件光追时,并不把原始高模塞进 BLAS,而是由 Nanite::GRayTracingManager 依据 Nanite 自身的裁剪结果,把"当前可见的集群集合"流式构建为 BLAS——光追场景下的几何裁剪收益因此与光栅化路径保持一致。

实例侧(TLAS):场景成员与可见性通道

TLAS 的输入是"实例列表",UE 通过一组专用工具对其进行管理:

Engine/Source/Runtime/Renderer/Private/RayTracing/
  RayTracingInstance.{h,cpp}        FRayTracingInstance:单个实例 = 几何 + 变换 + 掩码 + 标志位
  RayTracingInstanceMask.{h,cpp}    实例掩码:通道可见性控制(主光追 / 阴影 / 天空光分层)
  RayTracingInstanceCulling.{h,cpp} 实例剔除:每帧仅将"可能被需要的"实例放入 TLAS
  RayTracingInstanceBufferUtil.cpp  实例数据 -> GPU Buffer 的组装工具

“实例掩码"是"阴影光线仅判断遮挡、无需材质计算"这一原则的工程化落地:通过掩码通道,同一份场景能够分别服务于"材质命中求解”(Closest Hit)与"遮挡测试"(Any Hit / 阴影光线),避免为阴影路径支付昂贵的材质着色开销。[7]

4.3 命中与材质:从 HitRecord 到 Shader 绑定表

一句话:软件模型里的"命中记录 + 多态分派",在 UE 里被拆成三件具体东西:draw 命令集、Hit Shader,以及一张"命中索引 -> 该跑哪个 Shader"的绑定表。

硬件光追的四个可编程阶段(1.4 节提到的规范侧清单,此处给出职责划分):

可编程阶段在基本光追流程中的角色
Ray Generation(射线生成)第 1 步:生成视线 / 阴影 / 反射光线,对应 1.2 节的"光线生成"
Intersection / Any Hit求交期间的命中决策(加速结构遍历本身是固定管线),对应 1.2 节的"求交"
Closest Hit找到最近交点后执行,对应第 3 步"着色"的入口
Miss光线未击中任何物体时执行(返回背景色或采样天空光)

四个阶段里,只有 Closest Hit 与 Any Hit 会随"命中到什么材质"而不同——这正是 UE 需要材质 Hit Shader 与 SBT 的原因。

第 1.2 节的软件光追模型中,HitRecord(记录命中表面、参数 t、法线)与 Surface::hit 的多态分派是核心抽象[5]。UE 的工程化版本将这一概念拆分为三个独立组件:

  1. 将"表面集合"物化为 GPU 端 Mesh Draw 命令集:RayTracingMeshDrawCommands.cpp 将场景网格转换为光追可用的 draw 命令集合(对应软件模型中"将一组物体视为复合 Surface"的 Group 组织方式[5])。
  2. 将"材质着色程序"物化为 Hit Shader:RayTracingMaterialHitShaders.{h,cpp} 为每个 mesh draw 生成对应的 Closest Hit / Any Hit Shader——其内部通过 UE 统一材质系统完成"命中后颜色计算"。材质管线完全复用 FDeferredShadingSceneRenderer 统一管理的着色流程。[6]
  3. 将"命中后分派哪个 Shader"物化为 SBT(Shader Binding Table):RayTracingShaderBindingTable.{h,cpp} 将"几何 + 实例 + 材质"的笛卡尔积组装为 DXR 规范要求的着色器绑定表,使 GPU 命中时能够通过索引快速定位到正确的 Hit Shader。
软件光追基础模型UE 工程化实现
Scene 表面集合FGPUScene / FRayTracingScene(4.1、4.2 节)
Surface.hit(ray)BLAS 遍历(固定管线)+ 可编程命中阶段
HitRecord(s, t, n)命中数据 + RayTracingBarycentrics(重心坐标等)
材质(决定颜色)Closest Hit / Any Hit(RayTracingMaterialHitShaders)
光源 / 光照方向RayTracingLighting(光照采样与光源网格)
多态分派Shader Binding Table(命中索引 -> 正确 Shader)

这张表把第 1 章的概念模型与第 4 章的代码实体一一对应。1.4 节给出的职责分界线在这里可以直接落地:几何层(BLAS / TLAS / SBT 索引)决定"光线碰到谁",SBT 与 Hit Shader 决定"碰到后计算多少光照"。

4.4 应用 Pass 全景

一句话:这些效果没有一个是"整屏的光追开关",它们各自挂在主循环里最合适的阶段(光照循环、环境光、AO、半透明),关掉某一个只是少了那一类效果。

UE 中直接消费通用光追系统的效果以 RayTracing/ 目录下的实现文件为准。每个效果都有两道门:通用门(平台是否支持光追、是否允许该视图用光追)与效果门(ShouldRender* 系列函数,见 RaytracingOptions.h)。逐项梳理如下:

效果名称挂载点 / 调用链效果门与开关实现说明
Ray Traced Shadows(光追阴影)LightRendering.cpp 在光照循环内逐光源调用 RenderRayTracingShadows(RayTracingShadows.cpp)ShouldRenderRayTracingShadows / ShouldRenderRayTracingShadowsForLight;r.RayTracing.Shadows(默认 0,即仍用传统阴影贴图),另有 r.RayTracing.Shadows.Lights.* 按光源类型细分非独立全屏 Pass,而是作为阴影渲染的可选模式嵌入逐光源渲染流程;降噪也发生在光照循环内(DenoiseShadowVisibilityMasks)
Ray Traced Ambient OcclusionRayTracingAmbientOcclusion.cpp;渲染器侧通过 EAmbientOcclusionMethod::RTAO 枚举接入ShouldRenderRayTracingAmbientOcclusion;r.RayTracing.AmbientOcclusion(默认 -1,由后处理体积驱动;0 关 / 1 开)输出与 ScreenSpaceDenoise 结合执行时空降噪
Ray Traced Sky Light(天空光采样)渲染器在天空光采样阶段调用 RenderRayTracingSkyLight(RaytracingSkylight.cpp)ShouldRenderRayTracingSkyLight;r.RayTracing.SkyLight(默认 0)以 RT 精确求交取代"反射捕获体 + 低分辨率环境贴图"的近似,额外输出 SkyLightHitDistanceTexture 等缓冲;它不能按视图临时开关(原因见 4.9 节)
Ray Traced Translucency(半透明光追)渲染器在半透明阶段调用 RenderRayTracingTranslucency(RayTracingTranslucency.cpp)ShouldRenderRayTracingTranslucency;r.RayTracing.Translucency(默认 -1,由后处理体积驱动)由 RenderRayTracingPrimaryRaysView(RayTracingPrimaryRays.cpp)提供"主光线"求交与着色,处理半透明折射等光栅化难以精确表达的效果
配套实现(非独立效果)RayTracingDecals.{h,cpp}、RayTracingBarycentrics.cpp、RayTracingDebug.cpp、RayTracingTraversalStatistics.{h,cpp}无独立 ShouldRender* 门禁贴花在光追下的求交支持、重心坐标验证、可视化调试与遍历性能统计

效果门之上还有一层公共判定:ShouldRenderRayTracingEffect(bEffectEnabled, CompatibilityFlags, View) 会先检查 IsRayTracingEnabled(ShaderPlatform) 与 View.IsRayTracingAllowedForView(),再由 CompatibilityFlags 判断该效果能否在当前可用的派发方式上运行——效果必须声明支持"全管线"或"Inline"至少之一,否则直接不执行(详见 4.10 节)。

编号表示这些挂载点在一次渲染中的先后位置(同一阶段内部可能并行)。

4.5 光追系统与 Lumen 及其他系统的租户关系

一句话:底座只有一套,用它的有好几家;每家在"整理场景"这一步各自提需求,中间不需要谁认识谁。

本节是理解 UE 光追架构的核心:通用光追场景(第 4.1 节)是"基础设施层",Lumen 与各类独立效果仅是它的不同"租户"。

  • Lumen 硬件模式:当 r.Lumen.HardwareRayTracing 开启且平台支持时,Lumen 通过 Lumen::UseHardwareRayTracedSceneLighting(声明于 Lumen/Lumen.h,实现于 Lumen/LumenSceneLighting.cpp)参与调度编排:FDeferredShadingSceneRenderer::Render 中 SetupRayTracingRenderingData 的多处调用点都带 Lumen::UseHardwareRayTracedSceneLighting(ViewFamily) 或 Lumen::UseAsyncCompute(ViewFamily) 条件,即**“底座什么时候需要被组装好"这件事是由租户决定的**。Lumen 还向场景构建器传递定制化需求:场景选项的构造函数 FSceneOptions::FSceneOptions(...)(RayTracing/RayTracing.cpp)依次调用 LumenHardwareRayTracing::SetRayTracingSceneOptions(View, DiffuseIndirectMethod, ReflectionsMethod, SceneOptions)(Lumen/LumenHardwareRayTracingCommon.h)、RayTracingShadows::SetRayTracingSceneOptions(View.bHasRayTracingShadows, SceneOptions),再按半透明与 Path Tracing 的需求补齐——同一份场景选项由多个消费者分别写入,谁需要哪类几何就打开哪个开关,避免为单一用途构建整场景。
  • Lumen 硬件 Shader 的结构印证:FLumenHardwareRayTracingShaderBase(同文件中定义)是所有 Lumen 硬件 RayGen 的公共基类,其参数列表中同时出现 FNaniteRayTracingUniformParameters(Nanite 场景数据)与 FRayTracingLightGrid(通用光源网格)——这一设计再次印证"Lumen 硬件光追 = 通用光追场景 + 通用光源数据"的复用架构。
  • Inline 光追的调度:5.6 中的光追并不总是采用"全管线派发"模式。Lumen 可选 Inline 光追(在 Compute Shader 中内嵌 TraceRayInline 调用,开关 r.Lumen.HardwareRayTracing.Inline,默认 1),判定谓词是 Lumen::UseHardwareInlineRayTracing(ViewFamily);SetupRayTracingRenderingData 在编排时会检测本帧是否存在任何 Inline 光追 Pass。它的代价与收益见 4.10 节。
  • 5.6 新增的 RT 消费者:MegaLights(灯光虚拟化系统,MegaLights/MegaLightsRayTracing.cpp)可选择性使用 Inline 光追评估大规模光源的直接光照;HairStrands(毛发系统)与 HeterogeneousVolumes(异质体积渲染)也通过 IsRayTracingEnabled() 门禁接入通用光追体系(详见各系统实现中的入口判断)。

核心结论:Lumen 决定"需要多少间接光照”,通用光追系统决定"光线如何发射、命中如何计算";5.6 中越来越多的子系统(Lumen、MegaLights、毛发、体积)选择复用这套基础设施,而非各自独立造轮。

4.6 Path Tracing(参考级完整光追)

第 1.3 节提到,实时光追通过各类近似手段替代了"完整的多弹射积分"。UE 同时保留了"老老实实做积分"的完整参考实现——Path Tracing(Engine/Source/Runtime/Renderer/Private/PathTracing.cpp)。

  • 它作为可选渲染模式存在(非实时路径默认启用),逐像素逐样本执行完整的多弹射路径追踪直至收敛,输出近乎离线渲染质量的参考图像。
  • 它的核心价值之一是作为正确性验证的参照系:开发与调试光追 / Lumen 效果时,可对照 Path Tracing 的输出判断偏色、泄漏、噪声来源等问题。
  • 它内置独立的时空降噪器(PathTracingSpatialTemporalDenoising.cpp),通过跨帧样本累积压低蒙特卡洛噪声。
  • 它在架构上甚至绕开了实时那套"光栅为主"的主循环(代码注释明确指出 Path Tracing 使用自己的光源管理器,不需要常规的实时光源网格)。值得注意的联动点:当 Nanite BLAS 重建时会触发 ViewState->PathTracingInvalidate(),因为 Path Tracing 的逐帧样本累积要求几何数据在时间上保持一致。

4.7 每帧数据流时序

将第 4.1-4.5 节的各环节拼接为完整的时序图(以"本帧存在 RT / Lumen 硬件 Pass"为前置条件):

4.8 降噪体系

一句话:实时每像素只发很少的光线,噪声必然存在;UE 备了三套降噪通道,各自匹配不同形态的结果。

实时光追每像素通常仅发射 1 根(或极少量)光线,蒙特卡洛噪声是固有属性。UE 5.6.1 的降噪体系分为三层:

  1. 通用屏幕空间时空降噪器:ScreenSpaceDenoise.{h,cpp} 定义的 IScreenSpaceDenoiser 是 RT 阴影 / AO / 天空光 / 半透明的统一降噪通道。它按"逐像素信号 + 命中距离 / 掩码"的接口组织输入输出(FShadowVisibilityInputs、FAmbientOcclusionInputs、FReflectionsInputs、FDiffuseIndirectInputs 等),并提供三个运行模式(EMode):Disabled / DefaultDenoiser(用 IScreenSpaceDenoiser::GetDefaultDenoiser())/ ThirdPartyDenoiser(用全局 GScreenSpaceDenoiser,可被插件替换)。调用站在各效果自己那边:例如光追阴影的降噪发生在光照循环内(LightRendering.cpp 调用 DenoiseShadowVisibilityMasks),AO 与天空光分别在 RayTracingAmbientOcclusion.cpp、RaytracingSkylight.cpp 内调用对应入口;r.Shadow.Denoiser(默认 2)与 r.AmbientOcclusion.Denoiser(默认 2)这类开关选择的正是上面三种模式(注意后者的名字里没有 RayTracing 前缀)。
  2. Lumen 专用滤波管线:Lumen Screen Probe 的结果重建与缓存复用强耦合,因此不走上面的通用通道,而是自己的滤波管线(LumenScreenProbeFiltering.cpp 等);它只复用 ScreenSpaceDenoise.h 里的信号纹理结构(FSSDSignalTextures)作为数据容器。
  3. Path Tracing 的独立时空降噪:详见 4.6 节。

噪声问题从反面塑造了架构决策:独立 RT GI/反射(UE5 早期方案)最终被 Lumen 取代的核心原因之一,正是"每像素发射大量光线 + 强力降噪"的成本结构远不如"稀疏光线发射 + 缓存复用"的方案健康(呼应 3.3 节的架构演进分析)。

4.9 边界条件与工程限制

一句话:光追的可用性由平台能力决定,成本由"进场景的实例规模"决定,而它几乎从不全量开启。

  • 平台能力面:硬件光追需要 D3D12 + DXR 兼容的 GPU(RHI 层对应 D3D12RayTracing.{h,cpp} 的实现)。Inline 光追、Shader Execution Reordering(SER)等属于更细粒度的平台相关能力,引擎通过 RHISupportsRayTracing* 系列函数在运行时逐能力探测;渲染层的控制台开关(如 r.Lumen.HardwareRayTracing.Inline / ...ShaderExecutionReordering)仅在与硬件能力匹配时才真正生效。
  • 成本构成面:TLAS 每帧重建 + BLAS 增量更新的总代价与可见实例数强相关。RayTracingInstanceCulling(实例剔除)、掩码通道拆分、r.RayTracing.Culling(取值 0-3,分别对应不剔除 / 只剔除相机后方的远近与立体角组合策略)共同目标是把"进入光追场景"的实例规模压到最小。凡是本帧用不到的物体,都应通过剔除机制挡在光追场景之外。可参考的量级:r.Lumen.HardwareRayTracing 的帮助文本明确提示,场景实例超过 10 万时硬件光追的场景更新开销显著。
  • 与 Nanite 的联动面:5.6 中 Nanite 通过 Nanite::GRayTracingManager 参与硬件光追(BLAS 由可见集群流式构建,详见 4.2 节);Lumen 硬件 Shader 为此提供了 NANITE_RAY_TRACING 的专用 permutation。
  • 一个不能按视图开关的效果:光追天空光要求 FScene 的 base pass 着色器按"是否启用光追天空光"配置,而该配置变化会触发静态网格元素重建(bScenesPrimitivesNeedStaticMeshElementUpdate),无法逐帧或逐视图切换——因此它被单独实现为 ShouldRenderRayTracingSkyLightEffect(),属全局开关,常见于影视级捕获而非游戏运行时(DeferredShadingRenderer.cpp 中的注释说明了这一限制)。
  • 帮助文本与默认值可能不一致:r.RayTracing.Culling 的帮助文本写"0: Culling disabled (default)",而实际默认参数是 3;r.RayTracing.Shadows.SamplesPerPixel 的默认参数是 -1(表示交给默认值 1)。这类不一致在排查时容易误判,判断应以声明处的默认参数为准。

4.10 派发模式与执行相干性

一句话:同一套光线既可以用独立管线派发,也可以内嵌在普通 compute shader 里;选哪种取决于硬件能力与相干性优化。

全管线 vs Inline。效果本身要声明自己支持哪种派发方式:ERayTracingPipelineCompatibilityFlags(RaytracingOptions.h)只有两个取值 FullPipeline 与 Inline。门禁函数 ShouldRenderRayTracingEffectInternal(DeferredShadingRenderer.cpp)在此基础上再叠加平台与开关:

判定条件
允许全管线GRHISupportsRayTracingShaders 且 r.RayTracing.AllowPipeline 且效果声明了 FullPipeline
允许 InlineGRHISupportsInlineRayTracing 且 r.RayTracing.AllowInline 且效果声明了 Inline
两者都不允许该效果直接不执行

也就是说 Inline 光追的一个实际作用,是让效果在"支持 Inline 但不支持完整光追管线"(或反之)的设备上仍能存活。此外 r.RayTracing.ForceAllRayTracingEffects 可以强制开关全部效果,用于验证问题是否出在能力探测上。

Inline 模式有两条具体约束与收益(均来自源码注释):

  • 约束:当前的 Inline 实现要求线程组与 wave 一一对应(LumenHardwareRayTracingCommon.cpp 的 GetThreadGroupSizeInternal 注释),因此线程组尺寸不能自由选择。
  • 收益:Lumen 的异步计算路径"只能用于 Inline 光追,或 RHI 支持异步光追派发"(Lumen.cpp)——即 Inline 让硬件光追结果可以更自然地并入 Async Compute 队列。

SER(Shader Execution Reordering)。开关 r.Lumen.HardwareRayTracing.ShaderExecutionReordering 默认 true,帮助文本的口径是:用它改善材质求值的相干性,对使用大量材质的场景可能有性能收益,硬件不支持时该开关无效果——所以"开了没变化"通常不是 bug,而是能力探测未通过(可用 5.3 节的排查路径确认)。

本章小结:通用光追系统向下管理 FRayTracingScene 每帧生命周期(Reset -> Build -> Pass -> PostRender)、BLAS 三类管理(静态 / Nanite 流式 / 动态蒙皮)与 TLAS 剔除掩码,向上通过 SBT 命中分派服务 RT Pass 清单(阴影 / AO / 天空光 / 半透明等)与多租户(Lumen / MegaLights / 毛发 / 体积),配合三层降噪、两种派发模式与平台能力约束,构成完整的硬件光追基础设施。

5. 工程视图:要动的开关、常见翻车与排查入口

一句话:这一章不引入新机制,只把前面四章的结论按"谁在什么情况下会用到"重排成三类表——该动哪些开关、哪些现象最常见、出了问题先看哪里。

光追不是单个开关,而是链条上的若干道门;开关拨了没反应,通常是上游某道门没开。本章默认值均取自 5.6 源码的变量声明处;标 (只读) 的变量只能在启动时设置(ECVF_ReadOnly),运行时用控制台改无效。

5.1 要动的开关(按角色)

(a)美术 / 地编:影响画面表现

想改变什么关键项默认值说明
是否整体允许光追r.RayTracing0,(只读)总开关只决定"允许不允许",不代表每类效果都开;需在启动时设置
间接光 / 反射方案EDiffuseIndirectMethod / EReflectionsMethodLumen可回退 SSGI / SSR(3.3 节)
光追阴影r.RayTracing.Shadows0默认仍用阴影贴图;打开后阴影精确,但成本与噪声上升
光追天空光r.RayTracing.SkyLight0不能逐视图开关(4.9 节),面向影视级捕获
光追 AOr.RayTracing.AmbientOcclusion-1(后处理体积驱动)效果门还会要求 AO 强度大于 0
光追半透明r.RayTracing.Translucency-1(后处理体积驱动)默认走光栅化半透明

(b)TA / 技术美术:质量与性能取舍

想改变什么关键项默认值说明
Lumen 的光线来源r.Lumen.HardwareRayTracing1平台不支持时自动回退软件模式;实例超过 10 万的场景,场景更新开销显著
命中处是否实时着色r.Lumen.HardwareRayTracing.LightingMode00 走 Surface Cache;1 / 2 在命中点实时算光照,质量升、成本显著升
派发方式r.Lumen.HardwareRayTracing.Inline1Inline 更易并入 Async Compute(4.10 节)
执行相干性r.Lumen.HardwareRayTracing.ShaderExecutionReorderingtrue硬件不支持时该开关无效果
进入光追场景的实例规模r.RayTracing.Culling3(取值 0-3)帮助文本把 0 写成 default,与声明处不一致(4.9 节)
阴影 / AO 降噪算法r.Shadow.Denoiser、r.AmbientOcclusion.Denoiser均 2后者名字里没有 RayTracing 前缀,容易搜错(4.8 节)

(c)程序:能力门与调试入口

想做什么关键项 / 函数默认值说明
判断是不是能力探测挡掉了效果r.RayTracing.ForceAllRayTracingEffects-1设 0 / 1 强制关 / 开全部光追效果,绕过各效果门(4.10 节)
允许或禁止某种派发方式r.RayTracing.AllowPipeline、r.RayTracing.AllowInline均 1效果必须声明对应的 ERayTracingPipelineCompatibilityFlags 才会执行
观察遍历开销与命中分派RayTracingTraversalStatistics.{h,cpp}、RayTracingDebug.cpp无独立门遍历统计与可视化调试

5.2 常见翻车

现象最可能的成因先动哪里
打开光追后画面完全没变化总开关 r.RayTracing 默认 0 且只读,未在启动时设置;或平台不支持光追r.RayTracing(启动参数 / 项目设置)-> 4.10 节的能力门
某一类效果没出现该效果自己的门没开:阴影 / 天空光默认 0,AO 与半透明默认 -1 由后处理体积驱动5.1 节 (a) 逐项对照默认值
Lumen 看起来不受光追影响Lumen 默认走软件模式;硬件模式要求平台支持且 r.Lumen.HardwareRayTracing 开启4.5 节、5.1 节 (b)
开了 SER 性能没有变化硬件不支持 SER,开关不产生效果4.10 节
天空光无法按视图或按帧临时切换该效果是全局开关,切换会触发 base pass 重配置与静态网格元素重建4.9 节
实例规模变大后帧率骤降硬件光追的场景更新成本与实例数强相关,超过 10 万实例时显著4.9 节;检查剔除设置
Nanite 物体在光追里闪烁或缺失加速结构尚未就绪:Nanite BLAS 受帧预算节流,PendingBuilds 跨帧累积4.2 节、5.3 节
Path Tracing 画面跳变或重影BLAS 重建触发 PathTracingInvalidate(),跨帧样本累积被清空4.6 节
噪声残留降噪通道或模式选择不合适4.8 节

5.3 排错谱系

按"现象 -> 第一现场(开关 / 谓词)-> 第二现场(函数)-> 落地文件"的顺序定位:

现象第一现场第二现场落地文件
光追完全没生效r.RayTracing、IsRayTracingEnabled()、GRHISupportsRayTracingShadersShouldRenderRayTracingEffectInternalDeferredShadingRenderer.cpp
某个效果不执行该效果的 ShouldRender* 与其 CompatibilityFlagsShouldRenderRayTracingEffectRaytracingOptions.h
加速结构没准备好、几何缺失RayTracingScene.GeometriesToBuildDispatchRayTracingWorldUpdates(五步链,4.1 节)DeferredShadingRenderer.cpp、RayTracingScene.h
Nanite 光追几何跨帧闪烁ScheduledBuilds / PendingBuildsNanite::GRayTracingManager::ProcessBuildRequestsNaniteRayTracing.h
动态物体(蒙皮 / 布料)光追几何不更新AddDynamicGeometryToUpdateAddDynamicGeometryUpdatePassRayTracingDynamicGeometryUpdateManager.h
Lumen 硬件光追没生效r.Lumen.HardwareRayTracing、Lumen::UseHardwareInlineRayTracingSetupRayTracingRenderingData 的调用条件LumenSceneLighting.cpp、DeferredShadingRenderer.cpp
命中处光照表现异常r.Lumen.HardwareRayTracing.LightingMode、...HitLighting.* 族命中光照开关集中定义处LumenHardwareRayTracingCommon.cpp
光追天空光泄漏或关不掉ShouldRenderRayTracingSkyLightEffect()base pass 重配置 bScenesPrimitivesNeedStaticMeshElementUpdateDeferredShadingRenderer.cpp
实例过多导致卡顿r.RayTracing.CullingRayTracingInstanceCullingRayTracingInstanceCulling.cpp

本章小结:开关分三层(总门 / 效果门 / 能力门),且多处默认值是"关闭"或"交给后处理体积",因此"没效果"最常见的原因是门没开,而非功能异常;排错沿"开关 -> 谓词 -> 函数 -> 文件"逐级下沉,可覆盖绝大多数"光追不生效"类问题。

6. 附录

6.1 源码文件索引

正文采用"文件短名"引用,本节提供短名到引擎相对路径的完整映射。

短名相对引擎根路径
DeferredShadingRenderer.cppEngine/Source/Runtime/Renderer/Private/DeferredShadingRenderer.cpp
DeferredShadingRenderer.hEngine/Source/Runtime/Renderer/Private/DeferredShadingRenderer.h
RayTracingScene.{h,cpp}Engine/Source/Runtime/Renderer/Private/RayTracing/RayTracingScene.{h,cpp}
RayTracingInstance*.{h,cpp}Engine/Source/Runtime/Renderer/Private/RayTracing/RayTracingInstance*.{h,cpp}
RayTracingInstanceCulling.{h,cpp}Engine/Source/Runtime/Renderer/Private/RayTracing/RayTracingInstanceCulling.{h,cpp}
RayTracingInstanceMask.{h,cpp}Engine/Source/Runtime/Renderer/Private/RayTracing/RayTracingInstanceMask.{h,cpp}
RayTracingMaterialHitShaders.{h,cpp}Engine/Source/Runtime/Renderer/Private/RayTracing/RayTracingMaterialHitShaders.{h,cpp}
RayTracingShaderBindingTable.{h,cpp}Engine/Source/Runtime/Renderer/Private/RayTracing/RayTracingShaderBindingTable.{h,cpp}
RayTracingMeshDrawCommands.cppEngine/Source/Runtime/Renderer/Private/RayTracing/RayTracingMeshDrawCommands.cpp
RayTracingShadows.cppEngine/Source/Runtime/Renderer/Private/RayTracing/RayTracingShadows.cpp
RayTracingAmbientOcclusion.cppEngine/Source/Runtime/Renderer/Private/RayTracing/RayTracingAmbientOcclusion.cpp
RayTracingPrimaryRays.cppEngine/Source/Runtime/Renderer/Private/RayTracing/RayTracingPrimaryRays.cpp
RayTracingTranslucency.cppEngine/Source/Runtime/Renderer/Private/RayTracing/RayTracingTranslucency.cpp
RaytracingSkylight.cppEngine/Source/Runtime/Renderer/Private/RayTracing/RaytracingSkylight.cpp
RayTracingDecals.{h,cpp}Engine/Source/Runtime/Renderer/Private/RayTracing/RayTracingDecals.{h,cpp}
RayTracingBarycentrics.cppEngine/Source/Runtime/Renderer/Private/RayTracing/RayTracingBarycentrics.cpp
RayTracingDebug.cppEngine/Source/Runtime/Renderer/Private/RayTracing/RayTracingDebug.cpp
RaytracingOptions.hEngine/Source/Runtime/Renderer/Private/RayTracing/RaytracingOptions.h
RayTracingDynamicGeometryUpdateManager.cppEngine/Source/Runtime/Renderer/Private/RayTracing/RayTracingDynamicGeometryUpdateManager.cpp
NaniteRayTracing.{h,cpp}Engine/Source/Runtime/Renderer/Private/Nanite/NaniteRayTracing.{h,cpp}
LightRendering.cppEngine/Source/Runtime/Renderer/Private/LightRendering.cpp
ScreenSpaceDenoise.{h,cpp}Engine/Source/Runtime/Renderer/Private/ScreenSpaceDenoise.{h,cpp}
LumenScreenProbeHardwareRayTracing.cppEngine/Source/Runtime/Renderer/Private/Lumen/LumenScreenProbeHardwareRayTracing.cpp
LumenRadianceCacheHardwareRayTracing.cppEngine/Source/Runtime/Renderer/Private/Lumen/LumenRadianceCacheHardwareRayTracing.cpp
LumenReflectionHardwareRayTracing.cppEngine/Source/Runtime/Renderer/Private/Lumen/LumenReflectionHardwareRayTracing.cpp
LumenHardwareRayTracingCommon.{h,cpp}Engine/Source/Runtime/Renderer/Private/Lumen/LumenHardwareRayTracingCommon.{h,cpp}
LumenScreenProbeGather.cpp 等Engine/Source/Runtime/Renderer/Private/Lumen/(Screen Probe 系列文件)
LumenRadianceCache.cppEngine/Source/Runtime/Renderer/Private/Lumen/LumenRadianceCache.cpp
LumenSurfaceCache.cppEngine/Source/Runtime/Renderer/Private/Lumen/LumenSurfaceCache.cpp
LumenMeshCards.cppEngine/Source/Runtime/Renderer/Private/Lumen/LumenMeshCards.cpp
PathTracing.cppEngine/Source/Runtime/Renderer/Private/PathTracing.cpp
PathTracingSpatialTemporalDenoising.cppEngine/Source/Runtime/Renderer/Private/PathTracingSpatialTemporalDenoising.cpp
MegaLightsRayTracing.cppEngine/Source/Runtime/Renderer/Private/MegaLights/MegaLightsRayTracing.cpp
D3D12RayTracing.{h,cpp}Engine/Source/Runtime/D3D12RHI/Private/D3D12RayTracing.{h,cpp}
RHIResources.hEngine/Source/Runtime/RHI/Public/RHIResources.h
Lumen.h、LumenSceneLighting.cppEngine/Source/Runtime/Renderer/Private/Lumen/Lumen.h、Engine/Source/Runtime/Renderer/Private/Lumen/LumenSceneLighting.cpp
RayTracingGeometryManager.hEngine/Source/Runtime/Engine/Public/Rendering/RayTracingGeometryManager.h
RayTracingDynamicGeometryUpdateManager.hEngine/Source/Runtime/Renderer/Public/RayTracingDynamicGeometryUpdateManager.h
RayTracingTraversalStatistics.{h,cpp}Engine/Source/Runtime/Renderer/Private/RayTracing/RayTracingTraversalStatistics.{h,cpp}

反向索引(按问题找文件)

需求 / 问题首选文件
光追总开关与派发门禁DeferredShadingRenderer.cpp、RaytracingOptions.h
加速结构生命周期与实例剔除RayTracingScene.{h,cpp}、RayTracingInstanceCulling.{h,cpp}
材质命中与 SBT 组织RayTracingMaterialHitShaders.{h,cpp}、RayTracingShaderBindingTable.{h,cpp}
Lumen 光线来源与命中光照LumenHardwareRayTracingCommon.{h,cpp}
降噪统一入口ScreenSpaceDenoise.{h,cpp}
Nanite 光追 BLAS 管理NaniteRayTracing.{h,cpp}
动态几何顶点更新RayTracingDynamicGeometryUpdateManager.h
平台能力探测与 DXR 翻译D3D12RayTracing.{h,cpp}、RHIResources.h

6.2 控制台变量 / 设置速查

变量名默认值作用说明定义出处
r.RayTracing0(只读)光追总开关,驱动 bRayTracing 管线状态位DeferredShadingRenderer.cpp
r.RayTracing.AllowPipeline1允许全管线光追派发DeferredShadingRenderer.cpp
r.RayTracing.AllowInline1允许 Inline 光追派发DeferredShadingRenderer.cpp
r.RayTracing.ForceAllRayTracingEffects-1强制关 / 开全部光追效果(-1 表示不强制)DeferredShadingRenderer.cpp
r.RayTracing.Shadows0用光追阴影取代传统阴影贴图LightRendering.cpp
r.RayTracing.AmbientOcclusion-1光追 AO(-1 由后处理体积驱动)RayTracingAmbientOcclusion.cpp
r.RayTracing.AmbientOcclusion.SamplesPerPixel-1每像素采样数(-1 表示取默认值 1)RayTracingAmbientOcclusion.cpp
r.RayTracing.SkyLight0光追天空光(全局开关,不可逐视图)RaytracingSkylight.cpp
r.RayTracing.Translucency-1光追半透明(-1 由后处理体积驱动)RayTracingTranslucency.cpp
r.RayTracing.Culling3(取值 0-3)光追实例剔除策略RayTracingInstanceCulling.cpp
r.RayTracing.Nanite.MaxBuiltPrimitivesPerFrame8388608(约 8M 三角形)Nanite BLAS 每帧构建预算(节流来源);同族还有 MaxStagingBufferSizeMB(1024)、BLASScratchSizeMultipleMB(64)NaniteRayTracing.cpp
r.RayTracing.Nanite.MaxBlasBuildsPerFrame不存在旧名,仅残留于 NaniteRayTracing.h 的注释;实际控制项见上一行(待高版本源码复核)—
r.Lumen.HardwareRayTracing1Lumen 启用硬件光追(需平台能力支持)LumenHardwareRayTracingCommon.cpp
r.Lumen.HardwareRayTracing.Inline1Lumen 硬件光追切换为 Inline 派发模式同上
r.Lumen.HardwareRayTracing.LightingMode0命中处光照计算模式(0 走 Surface Cache)同上
r.Lumen.HardwareRayTracing.HitLighting.DirectLighting1命中处直接光是否实时计算同上
r.Lumen.HardwareRayTracing.HitLighting.ShadowMode2命中处阴影模式(0 关 / 1 硬 / 2 软)同上
r.Lumen.HardwareRayTracing.HitLighting.Skylight2命中处天空光(2 表示仅独立 Lumen 反射启用)同上
r.Lumen.HardwareRayTracing.ShaderExecutionReorderingtrue启用 SER(需硬件能力支持)同上
r.Shadow.Denoiser2阴影降噪算法模式(0 关 / 1 渲染器默认 / 2 可被插件替换)LightRendering.cpp
r.AmbientOcclusion.Denoiser2AO 降噪算法模式(名字中无 RayTracing 前缀)RayTracingAmbientOcclusion.cpp

使用提示(易误判项):一是 r.RayTracing.Culling 的帮助文本写 “0: Culling disabled (default)",而声明处的默认参数是 3;二是 r.RayTracing.AmbientOcclusion.SamplesPerPixel 的默认参数 -1 表示取默认值 1,而非"关闭”;三是 r.Shadow.Denoiser 与 r.AmbientOcclusion.Denoiser 的帮助文本都把第 0 项写成 “(default)",而两者的声明默认值都是 2;四是 r.RayTracing.Nanite.MaxBlasBuildsPerFrame 只在 NaniteRayTracing.h 的注释里出现,渲染器源码中没有该 CVar 声明——它已是旧名,实际控制项为 r.RayTracing.Nanite.MaxBuiltPrimitivesPerFrame(默认 8M 三角形),不要把旧名当作可调参数依赖(待高版本源码复核)。控制台变量的语义与默认值一律以声明处为准,帮助文本只作线索。

6.3 参考

正文中以 [1] 形式标注的引用编号,对应下列条目;全部内容出自《计算机图形学基础(第 5 版)》(Fundamentals of Computer Graphics, 5th ed.)。

[1]: 第 3 章"光栅图像” —— 像素定义、光栅概念与图像的函数抽象。 [2]: 第 4 章"光线追踪"开篇 —— 对象顺序与图像顺序两种渲染组织方式的对比。 [3]: 第 4.1 节"基本光追算法" —— 光线生成、求交、着色三步法的完整流程。 [4]: 第 4.3 节"计算视图光线" —— 视线参数方程 p(t) = e + t(s - e) 与参数 t 的几何语义。 [5]: 第 4.4 节"光线与物体的交点" —— 与一组物体求交的 Group 抽象、t 区间收缩算法与加速结构概念。 [6]: 第 4.5 节"着色 Shading" —— 着色输入参数的完整清单与着色模型在两条渲染路线间的通用性。 [7]: 第 4.5.3 节"阴影" —— 阴影光线的构造方法与 epsilon 偏移的数值处理。 [8]: 第 4.5.1 节"光源" —— 点光源 / 方向光源 / 环境光的数学表达与着色所需向量。

补充说明:以上 8 条引用均借用《计算机图形学基础》第 5 版的概念口径,作为本文术语定义的统一基准。本文范围之外的相邻主题(渲染方程与蒙特卡洛路径追踪的数学基础、DXR 加速结构构建的底层算法、降噪器的具体滤波核设计)不在本篇展开。


UE 5.6 的光追以图像顺序算法为基础,经 RDG 与 FGPUScene 接入渲染主循环,上层由 Lumen 负责 GI/反射的多级缓存软/硬件双模式求解,底层由通用光追系统管理 BLAS/TLAS/SBT 与多租户 RT Pass,硬件光线最终沿 TLAS -> BLAS -> SBT -> Hit Shader 完成闭环。