NOTE研究状态:进行中。当前以 UE 5.8、桌面端 Deferred Renderer 为基线。引擎小版本会改变具体 Pass、控制台变量和源码位置,因此本文把稳定的架构关系与易变的实现细节分开记录。
先给结论
UE5 的“渲染管线”不能只理解成一条从 Vertex Shader 到 Pixel Shader 的 GPU 流水线。站在引擎源码层面,它至少包含四层:
- 场景同步:Game Thread 上的
UPrimitiveComponent等对象,把渲染所需数据同步为 Render Thread 拥有的FPrimitiveSceneProxy/FScene数据。 - 可见性与绘制准备:为每个 View 做剔除、LOD、遮挡判断,把
FMeshBatch转成适合提交和排序的FMeshDrawCommand。 - 帧图调度:Renderer 用 RDG 声明 Pass、资源与依赖,RDG 管理资源生命周期、屏障、Pass 裁剪和部分异步计算调度。
- GPU 执行:RHI 把引擎命令映射到 D3D12、Vulkan、Metal 等后端,GPU 最终执行光栅、计算、光追与拷贝工作。
Nanite、Lumen、Virtual Shadow Maps(VSM)和 Temporal Super Resolution(TSR)都嵌在这套框架中,但职责不同:
- Nanite 主要解决几何可见性、LOD/Cluster 选择和高密度几何光栅化,不等于完整渲染器。
- Lumen 主要提供动态全局光照与反射,不接管基础几何、材质和全部直接光照。
- VSM 生成和缓存阴影可见性,仍要被光照阶段消费。
- TSR 用低于输出分辨率的当前帧,加上运动矢量和历史数据重建高分辨率图像。
- RDG 是帧内工作与资源依赖的组织层,不是新的底层图形 API。
一张总图
这张图表达的是所有权与依赖,不是说各线程必须串行等待。正常情况下,Game Thread、Render Thread、RHI 提交和 GPU 会跨帧重叠;是否存在独立 RHI Thread、允许多少帧排队,也取决于平台和配置。
研究边界:先盯住哪条渲染路径
UE5 同时维护多条渲染路径,不能把其中一条的结论无条件套到全部平台。
| 路径 | 典型场景 | 与本研究的关系 |
|---|---|---|
| Desktop Deferred | PC / 主机,高质量实时渲染 | 主线;GBuffer、Lumen、Nanite、VSM 的常见组合 |
| Desktop Forward | VR、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。更可靠的记法是理解它们生产和消费什么资源:
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 Buffer | RenderDoc / 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 Color | ProfileGPU、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、包围盒、材质相关性等渲染信息;
- 让场景更新以命令形式跨线程传播。
需要跟读的核心类型:
| 类型 | 研究问题 |
|---|---|
UPrimitiveComponent | Gameplay 对象何时创建、销毁或标记渲染状态脏? |
FPrimitiveSceneProxy | 哪些数据只供 Renderer 使用?动态元素从哪里产生? |
FScene | Primitive、Light、Reflection Capture 等怎样注册到渲染场景? |
FSceneViewFamily / FSceneView | 同一场景的主视图、分屏、立体视图和 Scene Capture 怎样表达? |
FViewInfo | Renderer 在基础 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 工作;
- 生成事件、统计信息和图调试数据。
两个容易踩的坑:
FRDGTextureRef/FRDGBufferRef是图内句柄。若资源要跨出当前 Graph,必须按 RDG 规则注册 External Resource 或 Queue Extraction,不能保存句柄下帧直接用。- 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::Draw | Runtime/Engine/Private/GameViewportClient.cpp | View Family 是怎样建立的? |
FRendererModule::BeginRenderingView... | Runtime/Renderer/Private/RendererModule.cpp | 工作怎样从 Game Thread 进入 Render Thread? |
FSceneRenderer / FViewInfo | Runtime/Renderer/Private/SceneRendering.* | 每个 View 的内部状态怎样组织? |
FDeferredShadingSceneRenderer::Render | Runtime/Renderer/Private/DeferredShadingRenderer.cpp | 主帧图在哪里组装? |
Visibility / InitViews | Runtime/Renderer/Private/SceneVisibility.cpp 等 | CPU 可见性与遮挡流程是什么? |
FMeshPassProcessor | Runtime/Renderer/Private/MeshPassProcessor.* | Mesh Batch 怎样变成 Draw Command? |
| Base / Depth Pass | BasePassRendering.*、DepthRendering.* | GBuffer 和 Early Z 怎样生成? |
FRDGBuilder | Runtime/RenderCore/Public/RenderGraphBuilder.h | Pass、资源和 Execute 的生命周期是什么? |
| Nanite | Runtime/Renderer/Private/Nanite/ | Cluster Cull、Raster、Material 怎样衔接? |
| Lumen | Runtime/Renderer/Private/Lumen/ | 各追踪和缓存阶段怎样调度? |
| VSM | Runtime/Renderer/Private/VirtualShadowMaps/ | Page 分配、缓存和失效怎样实现? |
| Post Process | Runtime/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 unit、ProfileGPU、RenderDoc / PIX Capture |
| B:Nanite | 同一高模的 Nanite 开 / 关对照 | Draw Call、Triangles、Nanite Cluster、Depth / Base Pass 时间 |
| C:Lumen | Lumen 与非 Lumen GI / Reflection 对照 | Lumen Scene、Screen Probe、Reflection、总 Lighting 时间 |
| D:VSM | 固定灯光后分别移动相机、灯和 WPO 物体 | Cached / Invalidated Page、Shadow Depth 时间 |
| E:TSR | 固定输出分辨率,改变内部 Screen Percentage | TSR 时间、Velocity、闪烁和拖影区域 |
每次采集至少保存:
- CPU:Game、Render、RHI Thread 时间;
- GPU:总帧时间和最重的 5 个 Pass;
- 场景:分辨率、Screen Percentage、视角、灯光数、Nanite 三角形 / Instance 规模;
- 配置:RHI、是否硬件光追、关键 Project Settings;
- 证据:GPU Capture、Unreal Insights Trace、对应源码提交号。
性能排查顺序
- 先用
stat unit判断瓶颈主要在 Game、Draw/Render 还是 GPU。 - CPU 瓶颈进入 Unreal Insights,看线程时间线、TaskGraph 和等待关系。
- GPU 瓶颈先用
ProfileGPU定位大类,再用 RenderDoc 或 PIX 看资源、事件和具体 Draw / Dispatch。 - 用 View Mode / Visualization 验证“为什么贵”,例如 Overdraw、Shader Complexity、Nanite Cluster、Lumen Scene、VSM Page,而不是只看 Pass 名称猜。
- 改一个变量后重新捕获同一场景;不要同时改分辨率、光照、几何和 AA,再比较两个不可归因的数字。
后续专题路线图
- 00:总览——统一线程、帧图和四项核心技术的关系
- 01:Game Thread → Render Thread——Scene Proxy 与一帧延迟
- 02:InitViews、Instance Culling 与 Mesh Drawing Pipeline
- 03:RDG 实战——资源生命周期、Pass Culling、Barrier 与 Async Compute
- 04:Deferred Renderer 逐 Pass 捕获——从 Depth 到 Tonemap
- 05:Nanite——Cluster、Visibility Buffer、Raster 与 Streaming
- 06:Lumen——Scene 表示、Screen Probe、Surface Cache 与 Reflections
- 07:VSM——Page、Clipmap、缓存失效与 Nanite 阴影
- 08:TSR——Velocity、History、Disocclusion 与 Screen Percentage
- 09:性能实验室——Unreal Insights + ProfileGPU + RenderDoc / PIX 对齐
当前待验证问题
- UE 5.8 默认 Desktop Deferred 中,Nanite 材质阶段在不同平台/RHI 下的具体 Pass 拆分有什么差异?
- 开启 Hardware Ray Tracing 后,Lumen 的哪些阶段被替换,哪些缓存和 Screen Trace 仍然保留?
- VSM 缓存失效在 WPO、骨骼动画和移动局部光下的实际成本曲线如何?
- TSR 在 50%、67%、77% 内部分辨率下,对细线条、粒子和高速遮挡显露的失败模式分别是什么?
- RDG Async Compute 在目标平台上是否真正形成 GPU 重叠,还是被依赖、Barrier 或硬件队列限制抵消?
官方资料
以下链接于 2026-07-16 核对可访问;阅读时选择与项目一致的 UE 版本:
- Rendering Overview
- Graphics Programming
- Mesh Drawing Pipeline
- Render Dependency Graph
- Nanite Virtualized Geometry
- Lumen Global Illumination and Reflections
- Virtual Shadow Maps
- Temporal Super Resolution
- UE 5.8 Release Notes
- EpicGames/UnrealEngine 源码仓库(需要 Epic 与 GitHub 账号授权)
这项研究的判定标准不是“记住所有 Pass”,而是看到一个 GPU 事件时,能回答四个问题:谁在 CPU 端创建它、它读写哪些 RDG 资源、它依赖哪项场景表示、它的结果被谁消费。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时






