NOTE系列导航:00 总览 → 01 线程与 Scene Proxy → 02 可见性与 Mesh Drawing → 03 RDG。版本基线为 UE 5.8 Desktop Deferred;移动端和 Nanite 的具体任务拆分以目标分支为准。
先给结论
Renderer 收到 FScene 和一组 View 后,不能把所有 Primitive 都直接变成 Draw Call。UE5 会在不同粒度连续回答“这部分是否值得画”:
- Primitive 粒度:包围体是否在视锥内、距离是否合适、是否被隐藏、是否通过遮挡测试、是否与当前 View / Pass 相关。
- Instance 粒度:同一 Primitive 内的大量实例中,哪些实例对当前 View 与 Pass 可见。
- Cluster 粒度:对 Nanite 几何继续选择必要 Cluster,并在 GPU 上做更细的视锥、遮挡和尺度判断。
- Pixel / Sample 粒度:Early Z、Depth Test、Stencil 等继续拒绝不可见片元。
传统网格通过 FMeshBatch → FMeshPassProcessor → FMeshDrawCommand 进入各个 Mesh Pass;Nanite 则拥有 GPU 驱动的 Cluster 可见性和光栅路径,不会把每个 Cluster 都变成一条传统 Mesh Draw Command。
可见性是一组筛网
这张图只表达过滤层次。真实实现会把工作任务化、批量化或移到 GPU,不同测试也可能交换或合并。优化时应问“对象在哪一级被淘汰”,而不是笼统地说“它被 Culling 了”。
InitViews 是阶段名,不只是一个循环
在 Desktop Renderer 中,InitViews 常被用作一组 View 初始化与可见性工作的总称。它可能涵盖或触发:
- View 参数、View State 与历史状态准备;
- Scene / GPU Scene 更新与上传;
- Primitive Frustum、Distance、Occlusion 等可见性计算;
FPrimitiveViewRelevance计算;- Static Mesh 可见命令筛选;
- Dynamic Mesh Element 收集;
- Instance Culling 与各 Mesh Pass 的 Setup 工作;
- 为后续 Base Pass、Depth、Shadow、Velocity 等建立可消费的数据。
UE5 的新版本持续把这部分拆成并行任务。因此在 Insights 中看到 InitViews 时间下降,不一定代表某项工作消失,也可能是它移动到异步 Task 或 GPU Pass。判断时同时检查:
- Render Thread 主事件;
- Worker Thread 上的 Visibility / Mesh Draw Command 任务;
- GPU 上的 Instance / Nanite Culling Dispatch;
- 主 Pass 是否因准备未完成而出现 Wait。
推荐源码入口
FSceneRenderer::InitViews → PreVisibilityFrameSetup... → ComputeViewVisibility... → Primitive Visibility / Relevance → Occlusion → Gather Dynamic Mesh Elements → PostVisibilityFrameSetup... → Mesh Pass Setup / Instance Culling函数拆分会随分支变化。优先在 Renderer/Private/SceneVisibility.cpp 搜索 InitViews、ComputeViewVisibility、GatherDynamicMeshElements 和 FPrimitiveViewRelevance,再从调用栈确认当前版本的真实边界。
第一层:Primitive Bounds 与 View Relevance
Bounds 是多数粗粒度判断的地基
Primitive 通常以 Box、Sphere 或组合 Bounds 参与视锥、距离、遮挡、阴影和空间结构查询。Bounds 必须保守地包住所有可能产生像素的几何:
- Bounds 太小:相机或遮挡关系变化时物体突然消失;
- Bounds 太大:更难被剔除,遮挡查询更保守,还可能放大阴影、Lumen 或其他场景表示的成本;
- World Position Offset、粒子、程序化顶点位移和自定义 Primitive 最容易产生错误 Bounds;
- 盲目提高 Bounds Scale 能缓解消失,但会用持续性能损失掩盖根因。
TIP排查“转一下相机物体就没了”,先显示 Bounds 并确认它覆盖动画后的全部顶点,再检查材质、Nanite 和 Occlusion。不要先关闭整个项目的遮挡剔除。
FPrimitiveViewRelevance 决定参与哪些路径
通过基础几何测试并不代表 Primitive 要参加所有 Pass。Proxy 为当前 View 返回的 Relevance 会表达类似信息:
- 是否应绘制;
- 使用 Static 还是 Dynamic Mesh Element 路径;
- 是否投射阴影;
- 是否进入 Main Pass、Custom Depth、Velocity、Translucency 等;
- 材质与渲染功能相关性。
字段名会随版本扩展,但稳定问题是:
这个 Primitive 对当前 View 可见吗?如果可见,它与哪些 Mesh Pass 相关?它的 Mesh Element 从 Static Cache 还是 Dynamic Gather 获得?WasRecentlyRendered() 之类 Gameplay 查询不能替代当前 View 的精确可见性结果:它往往是异步更新的历史提示,还可能受到 Shadow、Scene Capture 或其他 View 影响。
第二层:Occlusion 与 HZB
视锥内的对象仍可能被墙体挡住。UE 会按平台和路径使用 Hardware Occlusion Query、Hierarchical Z Buffer(HZB)或 GPU 驱动方案。
为什么遮挡结果常带历史性
如果要用当前帧完整深度判断某对象是否可见,必须先画遮挡物;但决定画哪些对象又发生在完整 Base Pass 之前。实时渲染通常通过以下方式打破环:
- 使用前一帧 HZB 或更早提交的 Query 结果;
- 对相机快速运动、刚出现对象和不确定结果采取保守可见策略;
- 当前帧 Depth 生成新 HZB,供后续 Pass 或未来帧使用;
- Nanite 使用自己的两阶段遮挡与 Cluster 选择策略。
因此遮挡剔除追求的是“不错误漏画,同时尽量少画”,不是理论上零延迟的完美可见集合。
HZB 是什么
HZB 把深度纹理逐级降采样为 Mip 金字塔,每级覆盖更大屏幕区域。测试一个 Bounds 时,可以选择与其屏幕尺寸匹配的 Mip,用少量采样得到保守遮挡结论:
具体深度比较方向受 Reversed-Z 等实现影响,源码中不要凭数值“越大越近”猜测;使用引擎提供的深度转换和比较语义。
第三层:GPU Scene 与 Instance Culling
一个 Primitive 可能代表成千上万个实例。如果只做 Primitive Bounds 测试,那么森林的一角可见时,整片森林都可能进入后续工作。
GPU Scene 把 Shader 需要的 Primitive / Instance 数据组织到 GPU 可访问的场景缓冲中。Instance Culling 可以针对当前 View 与 Pass:
- 读取 Instance Transform、Bounds 和 Flags;
- 做 Frustum、Distance、Occlusion 等测试;
- 压紧可见 Instance ID;
- 生成或修正 Indirect Draw 参数;
- 让相同几何命令只消费真正可见的实例。
常见源码搜索点:
FGPUSceneFInstanceCullingManagerFInstanceCullingContextFInstanceCullingDrawParamsInstanceCulling目录或 Shader
不同 Vertex Factory、Pass 与平台对 GPU Scene 的支持不完全相同,不能看到 ISM/HISM 就默认所有剔除都发生在 GPU。
三种“实例化”不要混淆
| 名称 | 解决的问题 |
|---|---|
| ISM / HISM 等组件 | Gameplay / Scene 表示层面显式声明大量相同 Mesh 实例 |
| Mesh Draw Command Dynamic Instancing | 把状态和 Shader Binding 兼容的多个绘制命令合并提交 |
| GPU Scene Instance Culling | 在 GPU 上筛选当前 View / Pass 真正可见的 Instance |
它们可以共同工作,但不是同一个功能开关。
第四层:Nanite Cluster Culling
Nanite 对受支持几何使用 Cluster 层级。粗粒度 Primitive / Instance 判断之后,Nanite 继续在 GPU 上选择满足当前 View 的 Cluster:
- View Frustum 与 Clip Plane;
- Bounds / Cone 等 Cluster 测试;
- 屏幕误差与细节层级选择;
- HZB Occlusion;
- Streaming 中哪些 Page 已驻留;
- Main View、Shadow View 与 VSM Page 对应的可见性需求。
关键区别:
传统路径:CPU / GPU 准备有限数量的 Mesh Draw CommandNanite:GPU 遍历层级并产生 Cluster 工作,再通过 Nanite Raster / Material 路径输出表面所以“Nanite 场景的 Draw Call 很少”不代表没有几何成本。需要同时看 Cluster 数、Instances、Raster Bin、Streaming、Overdraw 和材质阶段。
从 Proxy 到 FMeshBatch
传统网格把一次可绘制描述组织成 FMeshBatch。它通常包含:
- Vertex Factory:顶点数据如何提供给 Shader;
- Material Render Proxy / Material Resource:使用哪个材质与 Shader Permutation;
FMeshBatchElement:Index Buffer、First Index、Primitive 数、Instance 等元素数据;- Raster / Culling / Wireframe 等状态提示;
- 与当前 Pass 相关的标志。
FMeshBatch 不是 Draw Call 的同义词:
- 一个 Batch 可包含多个 Element;
- 同一个 Batch 可被多个 Mesh Pass 处理;
- 某些 Batch 会被拒绝或拆分;
- 多条兼容命令可能在提交时实例化合并;
- Nanite Cluster 不走逐 Cluster
FMeshBatch路径。
Static 与 Dynamic 指的是数据路径
DrawStaticElements通常在 Primitive 加入 Scene 或 Render State 重建时收集可持久保存的 Batch。GetDynamicMeshElements针对相关 View 动态收集,适合真正依赖每帧 / 每 View 状态的几何。- Static / Dynamic Mesh Element 不等同于 Actor 的 Static / Movable Mobility;判断依据是 Proxy 怎样提供绘制数据。
若自定义 Primitive 的拓扑和材质长期不变,却每帧通过 Dynamic 路径重新生成大量 Batch,会产生不必要的 Render Thread 成本。
Mesh Pass Processor:同一 Mesh 怎样进入不同 Pass
Depth、Base Pass、Shadow Depth、Velocity、Custom Depth 等需要不同 Shader 和状态。每种 Mesh Pass 通过对应 FMeshPassProcessor 处理 FMeshBatch:
FMeshBatch → Pass 的 FMeshPassProcessor::AddMeshBatch → 检查 Material / Vertex Factory / Pass 条件 → 选择 Shader Permutation → 组合 Pipeline State → 建立 Shader Bindings → BuildMeshDrawCommands → FMeshDrawCommandFMeshDrawCommand 是接近 RHI 提交的、可排序和缓存的绘制描述,通常记录:
- Shader 与 Pipeline State;
- Vertex Streams;
- Shader Resource Bindings;
- Index / Primitive / Instance 参数;
- Stencil 等 Pass 状态;
- 用于排序、合并和提交的信息。
它仍不是 D3D12 / Vulkan Command Buffer 本身;RHI 提交阶段才把它落实为平台调用。
Cached Command、Dynamic Command 与排序合并
Cached Mesh Draw Command
若 Primitive、Material、Vertex Factory 和 Pass 满足缓存条件,命令可在 Scene 注册或状态变化时建立并复用。收益是减少每帧 Shader 选择和命令构建,但代价是状态变化时必须正确失效。
缓存路径通常不适合强依赖每 View 临时状态的绑定。可变的 View 数据应通过 View Uniform Buffer、GPU Scene 等稳定绑定间接访问,而不是把每个 View 的值硬编码进缓存命令。
Dynamic Mesh Draw Command
Dynamic Batch 或无法缓存的 Pass 在每帧 Setup 阶段建立命令。它更灵活,但会增加:
- Material / Shader 选择;
- Command 构建;
- 排序;
- 内存分配与 TaskGraph 工作。
Dynamic Instancing 与状态排序
命令经过排序后,状态和 Shader Binding 兼容的 Draw 可能合并为实例化绘制。合并失败常见原因:
- 不同 Material Instance 资源绑定;
- 不同 Vertex / Index Buffer;
- 不同 Shader Permutation 或 PSO;
- 每个 Primitive 使用无法通过 GPU Scene 索引的独立绑定;
- Pass 或 Vertex Factory 不支持该路径。
材质看起来颜色相同,不代表绑定完全一致;Draw Call 数也不能脱离三角形、实例数量和 GPU 成本单独评价。
并行准备与提交
Mesh Draw Command 的优势之一,是把高层场景遍历与低层提交解耦,让多个 Task 并行完成:
- 过滤当前 Pass 的可见 Primitive / Mesh;
- 建立或复用 Draw Command;
- 生成 Sort Key 并排序;
- 执行 Dynamic Instancing 与 Instance Culling Setup;
- 生成 Visible Mesh Draw Command 列表;
- 向 RHI Command List 提交。
常见入口包括 FParallelMeshDrawCommandPass、DispatchPassSetup、BuildRenderingCommands、SubmitMeshDrawCommands 等。新版可能把 Setup 与 Instance Culling 进一步任务化;以 Insights 的 Task 依赖和本分支调用栈为准。
源码断点路线
| 序号 | 符号 / 目录 | 要验证的问题 |
|---|---|---|
| 1 | FSceneRenderer::InitViews | 本分支把 InitViews 拆成了哪些阶段? |
| 2 | ComputeViewVisibility... | Primitive Visibility Map 在哪里生成? |
| 3 | FPrimitiveSceneProxy::GetViewRelevance | 当前 Primitive 为什么进入或离开某个 Pass? |
| 4 | 具体 Proxy 的 DrawStaticElements | Static Batch 何时建立,多久调用一次? |
| 5 | 具体 Proxy 的 GetDynamicMeshElements | 每个 View 每帧产生多少 Batch? |
| 6 | Pass Processor 的 AddMeshBatch | Batch 因什么条件被接受或拒绝? |
| 7 | BuildMeshDrawCommands | Shader、PSO、Bindings 怎样进入 Command? |
| 8 | FParallelMeshDrawCommandPass | Setup、Sort、Instance Culling 在哪些 Task 中发生? |
| 9 | SubmitMeshDrawCommands | 最终多少可见命令被提交? |
| 10 | FGPUScene / InstanceCulling | Instance 数据怎样上传与压紧? |
| 11 | NaniteCullRaster 等 Nanite 入口 | Cluster Cull 与 Raster 的输入输出是什么? |
断点时同时记录 View、EMeshPass::Type、Primitive ID、Material、Vertex Factory 和 Draw Command 数。只记录函数命中,无法解释为什么 Draw Call 增减。
六个可证伪实验
H1:错误 Bounds 是相机转动时消失的根因
- 创建带 WPO 或自定义顶点位移的测试网格。
- 显示 Primitive Bounds,固定材质与 Occlusion 设置。
- 让顶点超出 Bounds,并从多个角度转动相机。
- 修正真实 Bounds 计算,再重复测试。
若只在超出 Bounds 时消失,且修正 Bounds 后恢复,可支持假设;若 Bounds 始终正确,则继续检查 Nanite 支持、材质 Pass 和遮挡历史。
H2:Static Mesh Element 不会每帧重新 Gather
- 在一个 Proxy 的
DrawStaticElements与GetDynamicMeshElements计数。 - 保持 Render State 不变运行 300 帧。
- 触发一次
MarkRenderStateDirty()再计数。
预期:Static 收集与 Scene 注册 / 重建相关;Dynamic 收集随相关 View 重复。具体组件可能同时提供两类数据,应按 View Relevance 解读。
H3:相同 Mesh 与兼容材质可减少提交命令
构建四组对象,每组保持总实例数一致:
| 组 | Mesh | 材质 / 参数 | 预期 |
|---|---|---|---|
| A | 相同 | 完全相同绑定 | 最容易实例化合并 |
| B | 相同 | 不同 Material Instance | 可能因资源绑定拆分 |
| C | 不同 | 相同材质 | Vertex / Index 数据不同,通常拆分 |
| D | ISM / HISM | 每实例数据 | 由显式实例与 Instance Culling 路径处理 |
用 ProfileGPU、RHI 统计和当前版本的 Mesh Draw Command Dynamic Instancing 统计比较,不用 Actor 数直接推断 Draw Call。
H4:遮挡剔除结果具有时间性
- 在大遮挡墙后放置大量对象。
- 固定相机观察稳定状态,再快速横移使对象显露。
- 捕获连续多帧的 Depth、HZB / Occlusion 事件和可见命令数。
预期:稳定遮挡时命令或实例工作下降;快速显露时系统保守恢复可见,可能出现短暂历史差异但不应持续漏画。
H5:Primitive 可见不代表全部 Instance 被提交
- 用一个大范围 ISM / HISM 覆盖视锥内外。
- 固定 Primitive 总 Bounds,逐步改变可见 Instance 比例。
- 观察 Instance Culling 输入数、可见数和 Indirect Draw 参数。
若 Primitive 始终可见但 GPU 可见 Instance 数随视角改变,说明第二层剔除有效。
H6:Nanite 的几何成本不能用传统 Draw Call 单独解释
- 准备视觉上相近的 Nanite 与传统 LOD 网格组。
- 固定材质、分辨率、View 与阴影设置。
- 分别记录 Mesh Draw Command、Nanite Instances / Clusters / Raster、Depth 与 Base Pass 时间。
预期:Nanite 传统 Draw Call 可能更少,但 Cluster、Raster、Streaming 与材质成本仍随可见复杂度变化。
性能观测表
| 症状 | 先看 | 再验证 |
|---|---|---|
Render Thread InitViews 高 | stat initviews、Insights Task 时间线 | Primitive 数、动态 Proxy、Relevance、Occlusion、Wait |
| Dynamic Mesh Elements 高 | GetDynamicMeshElements 调用 / Batch 数 | 能否改为 Static / Cached 数据路径 |
| Draw Call 高 | stat rhi、ProfileGPU、Mesh Draw Command 统计 | 材质绑定、PSO、Vertex Factory、Dynamic Instancing |
| 实例很多但屏外仍贵 | GPU Scene / Instance Culling Pass | Bounds、实例层级、Vertex Factory 支持 |
| 遮挡后仍贵 | HZB / Occlusion 可视化、连续帧 Capture | Bounds 太大、相机运动、Query 延迟、Pass 不参与遮挡 |
| Nanite 几何贵 | Nanite Overview / Clusters / Overdraw | Cluster 数、Raster Bin、VSM、Streaming、材质 |
| 偶发 PSO 卡顿 | Insights、PSO 日志 / 平台工具 | PSO Precaching 覆盖,而非仅减少 Draw Call |
可用于实验的命令和变量包括 FreezeRendering、r.VisualizeOccludedPrimitives、r.HZBOcclusion、r.AllowOcclusionQueries、r.MeshDrawCommands.DynamicInstancing 与 Dynamic Instancing 统计命令。它们可能只在 Editor / Development 生效或随版本改名,使用前必须在 UE 5.8 当前分支的 Console Variables Reference / DumpConsoleCommands 中确认。
常见误区
| 误区 | 更准确的说法 |
|---|---|
| InitViews 只做视锥剔除 | 它是 View 初始化、可见性、动态收集和 Pass Setup 的一组工作 |
| Primitive 通过视锥测试就会画出全部实例 | Instance Culling 还能继续筛选 Primitive 内实例 |
| HZB 使用当前完整帧,所以结果没有历史 | 完整深度与可见性存在先后依赖,系统会使用历史或保守策略 |
| Static Mesh Element 等于 Mobility Static | 它描述 Proxy 提供 Batch 的缓存路径,不等于 Actor Mobility 枚举 |
一个 FMeshBatch 就是一条 Draw Call | Batch 可拆分、多 Pass 处理、被拒绝或与其他命令合并 |
FMeshDrawCommand 就是 D3D12 Command List | 它是 RHI 提交前的引擎绘制描述 |
| Nanite 消灭了可见性成本 | 它把大量几何选择转为 GPU Cluster 层级工作 |
| Draw Call 越少 GPU 一定越快 | Shader、带宽、Overdraw、Cluster、三角形和同步都可能主导成本 |
本篇建立的心智模型
FScene + View → Primitive Visibility / Relevance → Static Mesh Batch Cache → Dynamic Mesh Element Gather → 每个 Mesh Pass 的 Processor → Cached / Dynamic FMeshDrawCommand → Sort / Dynamic Instancing → GPU Scene Instance Culling → RHI Submit
Nanite Primitive / Instance → Nanite GPU Cluster Culling → Nanite Raster / Material Path下一篇进入 RDG:上面准备好的 Draw / Dispatch 为什么以 AddPass 形式组织,资源依赖怎样产生 Barrier,Transient Resource 为什么能别名复用,Async Compute 又何时只是看起来异步。
官方资料
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时






