mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
3891 字
11 分钟
UE5 渲染管线研究:从一帧到 Nanite、Lumen 与 RDG
2026-07-16
NOTE

研究状态:进行中。当前以 UE 5.8、桌面端 Deferred Renderer 为基线。引擎小版本会改变具体 Pass、控制台变量和源码位置,因此本文把稳定的架构关系与易变的实现细节分开记录。

先给结论#

UE5 的“渲染管线”不能只理解成一条从 Vertex Shader 到 Pixel Shader 的 GPU 流水线。站在引擎源码层面,它至少包含四层:

  1. 场景同步:Game Thread 上的 UPrimitiveComponent 等对象,把渲染所需数据同步为 Render Thread 拥有的 FPrimitiveSceneProxy / FScene 数据。
  2. 可见性与绘制准备:为每个 View 做剔除、LOD、遮挡判断,把 FMeshBatch 转成适合提交和排序的 FMeshDrawCommand
  3. 帧图调度:Renderer 用 RDG 声明 Pass、资源与依赖,RDG 管理资源生命周期、屏障、Pass 裁剪和部分异步计算调度。
  4. GPU 执行:RHI 把引擎命令映射到 D3D12、Vulkan、Metal 等后端,GPU 最终执行光栅、计算、光追与拷贝工作。

Nanite、Lumen、Virtual Shadow Maps(VSM)和 Temporal Super Resolution(TSR)都嵌在这套框架中,但职责不同:

  • Nanite 主要解决几何可见性、LOD/Cluster 选择和高密度几何光栅化,不等于完整渲染器。
  • Lumen 主要提供动态全局光照与反射,不接管基础几何、材质和全部直接光照。
  • VSM 生成和缓存阴影可见性,仍要被光照阶段消费。
  • TSR 用低于输出分辨率的当前帧,加上运动矢量和历史数据重建高分辨率图像。
  • RDG 是帧内工作与资源依赖的组织层,不是新的底层图形 API。

一张总图#

flowchart LR subgraph GT[Game Thread] A[UWorld / Components] --> B[更新渲染状态] B --> C[创建或更新 Scene Proxy] V[FSceneViewFamily / View 参数] end subgraph RT[Render Thread] D[FScene / FPrimitiveSceneProxy] E[创建 SceneRenderer] F[InitViews 与可见性] G[FMeshBatch → FMeshDrawCommand] H[FRDGBuilder 构建帧图] I[RDG Execute] end subgraph RHIT[RHI Thread 或并行提交路径] J[RHI Command Lists] K[D3D12 / Vulkan / Metal] end subgraph GPU[GPU] L[Depth / Nanite] M[GBuffer / Materials] N[Shadows / Lumen / Lighting] O[Translucency / Post / TSR] P[Present] end C -->|跨线程命令| D V --> E D --> E --> F --> G --> H --> I I --> J --> K --> L --> M --> N --> O --> P

这张图表达的是所有权与依赖,不是说各线程必须串行等待。正常情况下,Game Thread、Render Thread、RHI 提交和 GPU 会跨帧重叠;是否存在独立 RHI Thread、允许多少帧排队,也取决于平台和配置。

研究边界:先盯住哪条渲染路径#

UE5 同时维护多条渲染路径,不能把其中一条的结论无条件套到全部平台。

路径典型场景与本研究的关系
Desktop DeferredPC / 主机,高质量实时渲染主线;GBuffer、Lumen、Nanite、VSM 的常见组合
Desktop ForwardVR、MSAA、部分特殊性能需求后续对照;材质与光照组织不同
Mobile Deferred / Forward移动 GPU 与不同带宽约束单独专题,不从桌面 Pass 顺序直接推断
Path Tracer离线质量参考、影视输出单独专题;不是实时 Deferred 管线的“最后一个 Pass”

一帧是怎样进入 Renderer 的#

下面是适合源码跟读的概念调用链。具体函数名和拆分会随 UE 分支变化,不应把它当成固定 ABI:

FEngineLoop::Tick
└─ Game / Viewport Tick 与 Draw
└─ UGameViewportClient::Draw
└─ 构造 FSceneViewFamily 与 FSceneView
└─ RendererModule.BeginRenderingViewFamily / ViewFamilies
└─ 向 Render Thread 排队
└─ FSceneRenderer::CreateSceneRenderer
└─ FDeferredShadingSceneRenderer::Render
├─ InitViews / Visibility
├─ 创建 FRDGBuilder
├─ 添加各类 Render Pass
└─ GraphBuilder.Execute

阅读这条链时要分清两类数据:

  • UObject / Component 属于 Gameplay 世界,通常由 Game Thread 管理。
  • FPrimitiveSceneProxy 和 Renderer 内的场景数据服务于渲染,通常由 Render Thread 拥有;跨线程更新必须通过明确的同步或排队机制,不能在两个线程间随意读写同一份可变数据。

一个常见误解是“Renderer 每帧直接遍历所有 Actor”。实际中,Renderer 面向的是已经注册到 FScene 的渲染代理和紧凑数据结构;这正是 Scene Proxy 层存在的原因。

默认延迟渲染帧:按依赖理解,不背固定顺序#

一帧在 GPU Capture 里看起来会有很多 Pass。更可靠的记法是理解它们生产和消费什么资源:

flowchart TD A[View 参数与场景数据] --> B[CPU / GPU 可见性与实例剔除] B --> C[Depth / PrePass / Nanite Raster] C --> D[当前帧 HZB] B --> E[Base Pass / Nanite Materials] C --> E E --> F[GBuffer: 深度 法线 材质属性 速度等] C --> G[阴影页请求与阴影深度] G --> H[VSM 阴影可见性] C --> I[Lumen Screen Traces / Scene Tracing] F --> I I --> J[Lumen GI 与 Reflections] F --> K[Deferred Direct Lighting] H --> K J --> L[合成间接光与反射] K --> L L --> M[雾 半透明 水等] M --> N[曝光 TSR 景深 Bloom Tonemap 等] N --> O[UI / Present]
IMPORTANT

这是一张依赖图,不是所有项目都严格执行的线性清单。Early Z 模式、是否启用 Nanite/Lumen/VSM、半透明位置、硬件光追、异步计算、Scene Capture、反射捕获和插件都会改变实际帧图。以 RenderDoc、PIX 或 ProfileGPU 捕获到的项目帧为准。

关键阶段观察表#

阶段核心问题常见产物首选观察方式
InitViews / Visibility哪些 Primitive、Instance、Cluster 对当前 View 可见?可见列表、LOD、遮挡结果、Draw Command 范围Unreal Insights、stat initviews、Nanite Visualization
Depth / HZB哪些像素最靠近相机,后续能否早剔除?Scene Depth、Hierarchical Z BufferRenderDoc / PIX、Buffer Visualization
Base Pass可见表面有哪些材质属性?GBuffer、Velocity、深度GBuffer Visualization、Shader Complexity
Shadows每盏光的可见性是什么?VSM 物理页、阴影 Mask / 数据VSM Visualization、GPU Capture
Lumen间接光和反射从哪里来?Surface Cache、Screen Probe、Radiance Cache 等Lumen Overview / Scene / Surface Cache
Deferred Lighting如何组合 GBuffer、灯光和阴影?HDR Scene ColorProfileGPU、Light Complexity
Post / TSR如何从 HDR 当前帧得到显示图像?历史缓冲、重建图像、LDR 输出TSR Visualization、Velocity、GPU Capture

Scene Proxy:Gameplay 世界与渲染世界的边界#

大多数可渲染 Component 会创建某种 FPrimitiveSceneProxy。Proxy 的价值不是简单复制 Component,而是:

  • 把 Renderer 真正需要的数据从庞大的 Gameplay 对象中剥离出来;
  • 明确 Game Thread 与 Render Thread 的所有权;
  • 提供静态或动态 Mesh Element、包围盒、材质相关性等渲染信息;
  • 让场景更新以命令形式跨线程传播。

需要跟读的核心类型:

类型研究问题
UPrimitiveComponentGameplay 对象何时创建、销毁或标记渲染状态脏?
FPrimitiveSceneProxy哪些数据只供 Renderer 使用?动态元素从哪里产生?
FScenePrimitive、Light、Reflection Capture 等怎样注册到渲染场景?
FSceneViewFamily / FSceneView同一场景的主视图、分屏、立体视图和 Scene Capture 怎样表达?
FViewInfoRenderer 在基础 View 上补充了哪些每帧内部状态?

Mesh Drawing Pipeline:从“一个网格”到 GPU 命令#

传统网格绘制路径中,FMeshBatch 更像“这批几何应该怎样画”的高层描述;它不会原样直接变成一次 API Draw Call。Mesh Pass Processor 会结合目标 Pass、材质 Shader、Vertex Factory、Render State 等信息,生成或选择 FMeshDrawCommand

Primitive Scene Proxy
└─ FMeshBatch / Static Mesh Batch
└─ FMeshPassProcessor
├─ 选择 Shader Permutation
├─ 组合 PSO / Render State
├─ 绑定资源与参数
└─ 生成 FMeshDrawCommand
└─ 排序、缓存、合并或并行提交

研究时重点区分:

  • Static / Cached 路径:场景状态稳定时尽量复用已经构建的绘制命令。
  • Dynamic 路径:每帧或每 View 重新收集和生成,灵活但 CPU 成本更高。
  • Pass 类型:Depth、Base Pass、Shadow Depth、Velocity 等会用不同的 Mesh Pass Processor。
  • Nanite 路径:Nanite Mesh 的 Cluster 可见性与光栅化绕开了大量传统逐 Mesh Draw Call 工作,但最终仍要向深度、材质属性和光照阶段提供数据。

RDG:为什么现代 UE 渲染代码都在 AddPass#

Render Dependency Graph 的核心不是“把代码写成 Lambda”,而是先声明一帧中:

  • Pass 读取和写入哪些 Texture / Buffer;
  • 哪些资源只在图内临时存在;
  • 哪些工作之间存在真实依赖;
  • 最终哪些结果会被外部使用或提取。

典型骨架:

FRDGTextureDesc Desc = FRDGTextureDesc::Create2D(
Extent,
PF_FloatRGBA,
FClearValueBinding::Black,
TexCreate_ShaderResource | TexCreate_UAV);
FRDGTextureRef Output = GraphBuilder.CreateTexture(Desc, TEXT("Research.Output"));
FMyPassParameters* Parameters = GraphBuilder.AllocParameters<FMyPassParameters>();
Parameters->Output = GraphBuilder.CreateUAV(Output);
FComputeShaderUtils::AddPass(
GraphBuilder,
RDG_EVENT_NAME("ResearchPass"),
ComputeShader,
Parameters,
GroupCount);

RDG 能据此做的事情包括:

  • 自动推导大部分资源状态转换与屏障;
  • 裁掉既无副作用、结果又没有被消费的 Pass;
  • 复用生命周期不重叠的瞬态资源内存;
  • 按依赖安排 Graphics / Async Compute 工作;
  • 生成事件、统计信息和图调试数据。

两个容易踩的坑:

  1. FRDGTextureRef / FRDGBufferRef 是图内句柄。若资源要跨出当前 Graph,必须按 RDG 规则注册 External Resource 或 Queue Extraction,不能保存句柄下帧直接用。
  2. Lambda 捕获的普通 CPU 数据必须在 Pass 真正执行时仍然有效。优先把 GPU 参数放进 RDG Pass Parameters,不要随意引用临时栈对象。

四项 UE5 核心技术怎样接进主线#

Nanite#

Nanite 把高密度网格拆成可流送、可分层选择的 Cluster。每帧依据 View、遮挡和屏幕误差选择必要 Cluster,再通过 Nanite 的硬件或软件光栅路径生成可见表面信息。它主要改变的是几何前端

  • 减少传统 CPU Draw Call 和人工 LOD 负担;
  • 用 Cluster 级裁剪与按需流送控制可见几何成本;
  • 与 Depth、HZB、材质求值、VSM 页渲染存在紧密交互;
  • 透明材质、某些变形方式和特定渲染功能仍可能走非 Nanite 路径,支持矩阵必须以当前版本文档为准。

Lumen#

Lumen 把多种追踪方式和缓存组合起来,求解动态漫反射间接光与反射。理解它时不要只问“开没开硬件光追”,而要拆成:

  • Screen Traces 能否在屏幕空间找到命中;
  • Software Ray Tracing 或 Hardware Ray Tracing 追踪的是哪种场景表示;
  • Surface Cache、Screen Probe、Radiance Cache 各缓存什么;
  • 最终结果以什么分辨率、频率和历史方式更新。

画面出现漏光、黑面或镜面错误时,先用 Lumen Visualization 判断是场景表示、卡片覆盖、追踪距离、屏幕空间缺失还是历史积累问题,不要直接堆采样数。

Virtual Shadow Maps#

VSM 把阴影深度图虚拟化为按需分配的 Page。方向光通常结合 Clipmap 覆盖大范围世界,局部光也使用虚拟页机制。其性能关键不只是“分辨率多高”,而是:

  • 本帧请求和渲染了多少新 Page;
  • 相机、光源、WPO 或 Primitive 变化使多少缓存失效;
  • Nanite 与非 Nanite 几何分别贡献了多少阴影工作;
  • 粗页、局部光数量与屏幕覆盖怎样放大成本。

TSR#

TSR 位于时域重建与上采样链路中。它依赖当前帧颜色、深度、Velocity、曝光和历史数据。拖影或闪烁不一定是 TSR 算法本身的问题,常见根因包括:

  • 物体、骨骼、WPO 或材质没有写出正确运动矢量;
  • 遮挡显露区域没有可靠历史;
  • 半透明、粒子和细线条缺少稳定的响应策略;
  • 内部分辨率过低,输入中已经没有足够信息;
  • 自动曝光变化或历史失效处理不正确。

源码阅读地图#

若本地有从 Epic GitHub 获取的 UE 5.8 源码,可从以下位置开始。文件名会随分支调整,优先搜索符号而不是死记路径。

入口常见位置要回答的问题
UGameViewportClient::DrawRuntime/Engine/Private/GameViewportClient.cppView Family 是怎样建立的?
FRendererModule::BeginRenderingView...Runtime/Renderer/Private/RendererModule.cpp工作怎样从 Game Thread 进入 Render Thread?
FSceneRenderer / FViewInfoRuntime/Renderer/Private/SceneRendering.*每个 View 的内部状态怎样组织?
FDeferredShadingSceneRenderer::RenderRuntime/Renderer/Private/DeferredShadingRenderer.cpp主帧图在哪里组装?
Visibility / InitViewsRuntime/Renderer/Private/SceneVisibility.cppCPU 可见性与遮挡流程是什么?
FMeshPassProcessorRuntime/Renderer/Private/MeshPassProcessor.*Mesh Batch 怎样变成 Draw Command?
Base / Depth PassBasePassRendering.*DepthRendering.*GBuffer 和 Early Z 怎样生成?
FRDGBuilderRuntime/RenderCore/Public/RenderGraphBuilder.hPass、资源和 Execute 的生命周期是什么?
NaniteRuntime/Renderer/Private/Nanite/Cluster Cull、Raster、Material 怎样衔接?
LumenRuntime/Renderer/Private/Lumen/各追踪和缓存阶段怎样调度?
VSMRuntime/Renderer/Private/VirtualShadowMaps/Page 分配、缓存和失效怎样实现?
Post ProcessRuntime/Renderer/Private/PostProcess/TSR、曝光、Tonemap 的依赖是什么?

推荐按这条顺序读:

调用入口
→ FSceneRenderer / FDeferredShadingSceneRenderer
→ 只选一个 Pass(例如 Base Pass)
→ 对应 MeshPassProcessor 或 Compute Shader
→ RDG 资源声明
→ Shader 参数结构
→ .usf / .ush Shader
→ GPU Capture 中的同名事件

不要第一次就从 DeferredShadingRenderer.cpp 顶部顺序读到底;把一个 CPU 事件、一个 RDG Pass 和一个 GPU 事件对齐,信息闭环更快。

实验计划:用捕获验证,而不是只读文档#

建立一个最小测试工程,固定相机和内容,使用 Development Editor 构建。每次只改变一个变量并保存同一机位的数据:

实验改动记录
A:基线默认 Deferred + TSR,简单静态场景stat unitProfileGPU、RenderDoc / PIX Capture
B:Nanite同一高模的 Nanite 开 / 关对照Draw Call、Triangles、Nanite Cluster、Depth / Base Pass 时间
C:LumenLumen 与非 Lumen GI / Reflection 对照Lumen Scene、Screen Probe、Reflection、总 Lighting 时间
D:VSM固定灯光后分别移动相机、灯和 WPO 物体Cached / Invalidated Page、Shadow Depth 时间
E:TSR固定输出分辨率,改变内部 Screen PercentageTSR 时间、Velocity、闪烁和拖影区域

每次采集至少保存:

  • CPU:Game、Render、RHI Thread 时间;
  • GPU:总帧时间和最重的 5 个 Pass;
  • 场景:分辨率、Screen Percentage、视角、灯光数、Nanite 三角形 / Instance 规模;
  • 配置:RHI、是否硬件光追、关键 Project Settings;
  • 证据:GPU Capture、Unreal Insights Trace、对应源码提交号。

性能排查顺序#

  1. 先用 stat unit 判断瓶颈主要在 Game、Draw/Render 还是 GPU。
  2. CPU 瓶颈进入 Unreal Insights,看线程时间线、TaskGraph 和等待关系。
  3. GPU 瓶颈先用 ProfileGPU 定位大类,再用 RenderDoc 或 PIX 看资源、事件和具体 Draw / Dispatch。
  4. 用 View Mode / Visualization 验证“为什么贵”,例如 Overdraw、Shader Complexity、Nanite Cluster、Lumen Scene、VSM Page,而不是只看 Pass 名称猜。
  5. 改一个变量后重新捕获同一场景;不要同时改分辨率、光照、几何和 AA,再比较两个不可归因的数字。

后续专题路线图#

当前待验证问题#

  1. UE 5.8 默认 Desktop Deferred 中,Nanite 材质阶段在不同平台/RHI 下的具体 Pass 拆分有什么差异?
  2. 开启 Hardware Ray Tracing 后,Lumen 的哪些阶段被替换,哪些缓存和 Screen Trace 仍然保留?
  3. VSM 缓存失效在 WPO、骨骼动画和移动局部光下的实际成本曲线如何?
  4. TSR 在 50%、67%、77% 内部分辨率下,对细线条、粒子和高速遮挡显露的失败模式分别是什么?
  5. RDG Async Compute 在目标平台上是否真正形成 GPU 重叠,还是被依赖、Barrier 或硬件队列限制抵消?

官方资料#

以下链接于 2026-07-16 核对可访问;阅读时选择与项目一致的 UE 版本:


这项研究的判定标准不是“记住所有 Pass”,而是看到一个 GPU 事件时,能回答四个问题:谁在 CPU 端创建它、它读写哪些 RDG 资源、它依赖哪项场景表示、它的结果被谁消费。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

UE5 渲染管线研究:从一帧到 Nanite、Lumen 与 RDG
https://example.pages.dev/posts/ue5-rendering-pipeline/
作者
博主
发布于
2026-07-16
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录