UE5 渲染架构
UE5 Rendering Architecture
UE5 的渲染器是独立的 Renderer 模块,在渲染线程 (Rendering Thread) 上以「场景代理」镜像游戏线程的组件状态,每帧按固定顺序执行深度预通道、G-buffer 基础通道、光照、雾、半透明和后处理。现代渲染代码用渲染依赖图 (Render Dependency Graph, RDG) 声明通道与资源,由 RDG 自动排屏障、复用瞬态内存、并行录制;最终命令经 RHI 线程翻译成 D3D12/Vulkan 等图形 API 调用。默认走延迟着色 (Deferred Shading),也可切换前向着色 (Forward Shading)。
三条线程与"落后一帧"
UE 把一帧拆给三条主线程:游戏线程 (Game Thread) 跑玩法与 Tick;渲染线程 (Render Thread) 作为前端,把与平台无关的图形命令录进命令列表 (Command List);RHI 线程 (RHI Thread) 作为后端,把命令翻译到具体图形 API。"前端并行生成的内容,在后端也并行翻译"。(Parallel Rendering Overview)
默认是"落后一帧"的流水线:游戏线程处理第 N+1 帧时,渲染线程处理第 N 帧;有了 RHI 线程后,渲染线程还能在 RHI 线程翻译第 N 帧时先做第 N+1 帧的可见性计算。同步由 FFrameEndSync 和 RHIThreadFence 仲裁,并且无论怎么并行,提交给 GPU 的命令顺序与单线程渲染器一致。(Parallel Rendering Overview)
下图表示稳态下三条线程和 GPU 同时处理相邻的几帧(具体错位取决于各自耗时):
gantt
dateFormat X
axisFormat %s
section 游戏线程
帧 N+1 :0, 3
帧 N+2 :3, 6
section 渲染线程
帧 N :0, 3
帧 N+1 :3, 6
section RHI 线程
帧 N-1/N :0, 3
帧 N/N+1 :3, 6
section GPU
帧 N-1 :0, 3
帧 N :3, 6这正是 多线程渲染与任务系统 中"按阶段流水线化、用延迟换吞吐"的思路在 UE 中的落地;代价是输入到显示的延迟增加,且跨线程访问必须严格遵守所有权。
游戏线程对象与渲染线程镜像
引擎把横跨两条线程的类按所有权一分为二:(Graphics Programming Overview)
| 游戏线程(Engine 模块) | 渲染线程 |
|---|---|
UWorld | FScene |
UPrimitiveComponent | FPrimitiveSceneProxy / FPrimitiveSceneInfo |
ULightComponent | FLightSceneProxy / FLightSceneInfo |
ULocalPlayer | FSceneViewState(跨帧的视图私有数据) |
| — | FSceneView / FViewInfo(每帧新建) |
组件注册 (Register) 后才对渲染器可见;游戏线程改了组件属性必须调用 MarkRenderStateDirty() 才会同步到渲染线程,渲染线程不得直接读 UPrimitiveComponent。图元组件 (Primitive Component) 是可见性与相关性判断的基本单位,视锥剔除、遮挡剔除都按图元进行,其包围盒还决定阴影投射和光源影响范围——所以组件拆多大是一个设计决策。(Graphics Programming Overview) 场景内用八叉树 (Octree) 加速空间查询。
实际项目中最常见的坑:在 Tick(见 委托与 Tick 机制)里每帧改组件渲染属性会反复触发渲染状态重建;在渲染线程读 UObject 会产生数据竞争。
一帧的渲染顺序(延迟路径)
入口是 FDeferredShadingSceneRenderer::Render。官方给出的高层顺序如下(文档自称是简化视图):(Graphics Programming Overview)
flowchart LR
A["InitViews<br/>可见性剔除/阴影准备"] --> B["PrePass<br/>仅深度, 建 HiZ"]
B --> C["Base Pass<br/>写 G-buffer"]
C --> D["遮挡查询<br/>供下一帧用"]
D --> E["Lighting<br/>阴影图+延迟/分块延迟光照"]
E --> F[Fog]
F --> G[Translucency]
G --> H[Post Processing]与通用理论的对应:
| UE 步骤 | 对应理论页 | 说明 |
|---|---|---|
| PrePass 填充 Hierarchical Z | 硬件深度剔除与纹理单元 | 先写深度,让后续昂贵的基础通道像素着色被 early-z/HiZ 剔掉 |
| Base Pass 写 G-buffer | 延迟着色 | 几何与光照解耦的经典延迟着色 |
| 光照"标准延迟 + 分块延迟" | 分块着色 | 多光源时按屏幕分块裁剪光源 |
| 遮挡查询结果用于下一帧 InitViews | 遮挡剔除 | 用一帧延迟换取不阻塞 GPU 的硬件遮挡查询 |
| 每图元剔除 | 剔除技术 | 图元粒度决定剔除效率 |
注意:这份 Graphics Programming Overview 虽挂在 5.8 文档下,但其中关于绘制策略 (Drawing Policy)、静态绘制列表的文字明显沿袭 UE4,可能与当前代码(Mesh Draw Command、Nanite 独立通道、Lumen)不完全一致,阅读源码时以 FDeferredShadingSceneRenderer::Render 为准。(Graphics Programming Overview)
静态与动态绘制路径
动态路径每帧遍历可见动态图元、调用代理回调生成网格元素:插入成本低、遍历成本高、无状态排序。静态路径在组件挂接时就把绘制状态缓存好并排序,官方称静态网格在渲染线程上约快 3 倍,但只能缓存与视图无关的状态。(Graphics Programming Overview) 这就是 绘制调用与状态切换优化 中"按状态排序、缓存绘制命令"的思想。RDG 文档也提到 Mesh Draw Command 会被缓存并在多个通道和视图间共享,因此通道/视图级参数改用静态统一缓冲 (Static Uniform Buffer) 按通道频率绑定,而不是按 PSO 频率重绑。(Render Dependency Graph)
渲染依赖图 (RDG)
RDG 是"立即模式 API,把渲染命令记录进图数据结构再编译执行"。官方要求"所有高层渲染代码都应使用 RDG"。(Render Dependency Graph)
它把渲染管线拆成两条时间线:设置 (Setup) 时创建资源描述、添加通道、做分支;执行 (Execute) 时 RDG 编译图后在多个线程调用各通道的 lambda 录制 RHI 命令。(Render Dependency Graph)
flowchart TB
S1["AllocParameters 声明读写资源"] --> S2["AddPass 名称+参数+标志+lambda"]
S2 --> S3[GraphBuilder.Execute]
S3 --> C1["编译: 剔除无用通道/资源"]
C1 --> C2["计算资源生命周期 瞬态内存别名"]
C2 --> C3["插入分离屏障 split barrier 与异步计算栅栏"]
C3 --> X["并行执行 lambda 录制命令列表"]RDG 自动完成的事情:异步计算 (Async Compute) 栅栏、瞬态资源的最优生命周期与内存别名 (Aliasing)、子资源状态转换、并行命令录制、剔除未使用的通道和资源、API 用法校验,以及在 RDG Insights 中可视化。(Render Dependency Graph) 关键规则与坑:
CreateTexture/CreateBuffer只分配描述符,真正的 RHI 资源在执行期才分配;GetRHI()只能在声明了该资源的通道 lambda 里调用,否则校验层断言。(Render Dependency Graph)- lambda 应无副作用,且不要按引用捕获栈上对象——执行时它们已经不存在;用
AllocObject/AllocPOD走 RDG 的线性分配器。(Render Dependency Graph) - 瞬态分配器让生命周期不相交的资源共用内存,显著降低显存峰值,但资源的"旧内容"是垃圾,不能依赖;排查时可关
r.RDG.TransientAllocator。(Render Dependency Graph) - 使用
FRHICommandListImmediate会让通道失去并行资格。(Render Dependency Graph) - 调试:
r.RDG.ImmediateMode/-rdgimmediate让通道在 AddPass 时立刻执行(关掉所有图优化),配合r.RHICmdBypass得到单一调用栈。(Render Dependency Graph)
RDG 本质是业界"帧图 (Frame Graph)"思路:先声明后执行,让引擎掌握整帧资源依赖。它把 计算着色器 的异步计算、多线程渲染与任务系统 的并行命令录制都变成自动化的。异步计算的调度是:RDG 找到异步通道在图形队列上的最后一个生产者并插入栅栏,到第一个图形队列消费者处再汇合。(Render Dependency Graph)
延迟 vs 前向
| 维度 | 延迟渲染器(默认) | 前向渲染器 |
|---|---|---|
| 定位 | 功能最全、最通用 | 更快的基线,适合 VR |
| 抗锯齿 | TSR/TAA/FXAA | 额外支持 MSAA |
| 光源处理 | G-buffer + 延迟/分块延迟光照 | 光源和反射捕获按视锥空间网格剔除,每像素遍历影响它的光源 |
| 屏幕空间效果 | 支持 SSR、SSAO、接触阴影 | 不支持 |
| 材质 | 可采样 G-buffer 场景纹理 | 不能访问 G-buffer;全部特性打包进一个 shader,采样器更少 |
| UE5 新特性 | Lumen、Nanite 可用 | Lumen 不兼容前向;Nanite 不支持前向 |
来源:(Forward Shading Renderer)、Lumen Technical Details、Nanite Virtualized Geometry Overview。前向渲染器"按视锥空间网格剔除光源"正对应 分簇着色。官方案例 Robo Recall 中前向比延迟快约 22%(GTX 970),主要来自按材质关闭的特性;而 MSAA 替代 TAA 约增加 25% GPU 帧时间。(Forward Shading Renderer)
选型:PC/主机高画质项目用默认延迟 + Nanite/Lumen/VSM/TSR 一套;VR 或需要 MSAA 的项目考虑前向,但要接受失去 Lumen、Nanite 和屏幕空间效果。
RHI
RHI (Render Hardware Interface) 是平台图形 API 之上的薄层,抽象层级"尽可能低",让大多数功能写一次即可在支持对应特性级别的平台运行。(Graphics Programming Overview) 渲染器作为独立模块(见 模块、插件与构建系统),引擎通过 IRendererModule/FSceneInterface 接口调用它。(Graphics Programming Overview)
取舍小结
- 多线程流水线提高吞吐但增加延迟与同步复杂度;
- RDG 带来自动屏障/别名/并行,代价是两条时间线让调试变难(崩溃栈离设置代码"隔了两层");(Render Dependency Graph)
- 延迟渲染功能全,前向渲染基线快、MSAA 友好,但与 UE5 主力特性不兼容。
出处
- Graphics Programming Overview UE 5.8 文档
- Render Dependency Graph UE 5.8 文档
- Parallel Rendering Overview UE 5.8 文档
- Forward Shading Renderer UE 5.8 文档