mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
4317 字
13 分钟
UE5 渲染管线 02:InitViews、可见性与 Mesh Drawing Pipeline
2026-07-16
NOTE

系列导航:00 总览01 线程与 Scene Proxy02 可见性与 Mesh Drawing03 RDG。版本基线为 UE 5.8 Desktop Deferred;移动端和 Nanite 的具体任务拆分以目标分支为准。

先给结论#

Renderer 收到 FScene 和一组 View 后,不能把所有 Primitive 都直接变成 Draw Call。UE5 会在不同粒度连续回答“这部分是否值得画”:

  1. Primitive 粒度:包围体是否在视锥内、距离是否合适、是否被隐藏、是否通过遮挡测试、是否与当前 View / Pass 相关。
  2. Instance 粒度:同一 Primitive 内的大量实例中,哪些实例对当前 View 与 Pass 可见。
  3. Cluster 粒度:对 Nanite 几何继续选择必要 Cluster,并在 GPU 上做更细的视锥、遮挡和尺度判断。
  4. Pixel / Sample 粒度:Early Z、Depth Test、Stencil 等继续拒绝不可见片元。

传统网格通过 FMeshBatch → FMeshPassProcessor → FMeshDrawCommand 进入各个 Mesh Pass;Nanite 则拥有 GPU 驱动的 Cluster 可见性和光栅路径,不会把每个 Cluster 都变成一条传统 Mesh Draw Command。

可见性是一组筛网#

flowchart TD A[FScene 中的 Primitive] B[Show Flags / Hidden / Layer / Main Pass] C[Distance / Min Draw Distance / Cull Distance] D[Frustum 与 Primitive Bounds] E[Precomputed Visibility 可选] F[Occlusion Query / HZB] G[FPrimitiveViewRelevance] H[每个 Mesh Pass 的可见命令] I[GPU Scene Instance Culling] J[Traditional Mesh Draw Commands] K[Nanite Cluster Culling] L[Depth Test / Early Z] A --> B --> C --> D --> E --> F --> G --> H H --> I --> J --> L H --> K --> L

这张图只表达过滤层次。真实实现会把工作任务化、批量化或移到 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 搜索 InitViewsComputeViewVisibilityGatherDynamicMeshElementsFPrimitiveViewRelevance,再从调用栈确认当前版本的真实边界。

第一层: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,用少量采样得到保守遮挡结论:

flowchart LR A[Scene Depth 1×] --> B[HZB Mip 1] B --> C[HZB Mip 2] C --> D[更粗 Mip] E[Projected Bounds] --> F[选择合适 Mip] D --> F F --> G{"Bounds 最近深度<br/>是否仍在遮挡面之后"} G -->|是| H[Occluded] G -->|否或不确定| I[Visible]

具体深度比较方向受 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 参数;
  • 让相同几何命令只消费真正可见的实例。

常见源码搜索点:

  • FGPUScene
  • FInstanceCullingManager
  • FInstanceCullingContext
  • FInstanceCullingDrawParams
  • InstanceCulling 目录或 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 Command
Nanite: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 指的是数据路径#

flowchart TD P[FPrimitiveSceneProxy] S[DrawStaticElements] D[GetDynamicMeshElements] SB[FStaticMeshBatch / 持久 Scene 数据] DB[每 View 收集 FMeshBatch] C[Cached Mesh Draw Commands] M[FMeshPassProcessor] V[Visible Mesh Draw Commands] P --> S --> SB --> C --> V P --> D --> DB --> M --> V
  • 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
→ FMeshDrawCommand

FMeshDrawCommand 是接近 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 并行完成:

  1. 过滤当前 Pass 的可见 Primitive / Mesh;
  2. 建立或复用 Draw Command;
  3. 生成 Sort Key 并排序;
  4. 执行 Dynamic Instancing 与 Instance Culling Setup;
  5. 生成 Visible Mesh Draw Command 列表;
  6. 向 RHI Command List 提交。

常见入口包括 FParallelMeshDrawCommandPassDispatchPassSetupBuildRenderingCommandsSubmitMeshDrawCommands 等。新版可能把 Setup 与 Instance Culling 进一步任务化;以 Insights 的 Task 依赖和本分支调用栈为准。

源码断点路线#

序号符号 / 目录要验证的问题
1FSceneRenderer::InitViews本分支把 InitViews 拆成了哪些阶段?
2ComputeViewVisibility...Primitive Visibility Map 在哪里生成?
3FPrimitiveSceneProxy::GetViewRelevance当前 Primitive 为什么进入或离开某个 Pass?
4具体 Proxy 的 DrawStaticElementsStatic Batch 何时建立,多久调用一次?
5具体 Proxy 的 GetDynamicMeshElements每个 View 每帧产生多少 Batch?
6Pass Processor 的 AddMeshBatchBatch 因什么条件被接受或拒绝?
7BuildMeshDrawCommandsShader、PSO、Bindings 怎样进入 Command?
8FParallelMeshDrawCommandPassSetup、Sort、Instance Culling 在哪些 Task 中发生?
9SubmitMeshDrawCommands最终多少可见命令被提交?
10FGPUScene / InstanceCullingInstance 数据怎样上传与压紧?
11NaniteCullRaster 等 Nanite 入口Cluster Cull 与 Raster 的输入输出是什么?

断点时同时记录 View、EMeshPass::Type、Primitive ID、Material、Vertex Factory 和 Draw Command 数。只记录函数命中,无法解释为什么 Draw Call 增减。

六个可证伪实验#

H1:错误 Bounds 是相机转动时消失的根因#

  1. 创建带 WPO 或自定义顶点位移的测试网格。
  2. 显示 Primitive Bounds,固定材质与 Occlusion 设置。
  3. 让顶点超出 Bounds,并从多个角度转动相机。
  4. 修正真实 Bounds 计算,再重复测试。

若只在超出 Bounds 时消失,且修正 Bounds 后恢复,可支持假设;若 Bounds 始终正确,则继续检查 Nanite 支持、材质 Pass 和遮挡历史。

H2:Static Mesh Element 不会每帧重新 Gather#

  1. 在一个 Proxy 的 DrawStaticElementsGetDynamicMeshElements 计数。
  2. 保持 Render State 不变运行 300 帧。
  3. 触发一次 MarkRenderStateDirty() 再计数。

预期:Static 收集与 Scene 注册 / 重建相关;Dynamic 收集随相关 View 重复。具体组件可能同时提供两类数据,应按 View Relevance 解读。

H3:相同 Mesh 与兼容材质可减少提交命令#

构建四组对象,每组保持总实例数一致:

Mesh材质 / 参数预期
A相同完全相同绑定最容易实例化合并
B相同不同 Material Instance可能因资源绑定拆分
C不同相同材质Vertex / Index 数据不同,通常拆分
DISM / HISM每实例数据由显式实例与 Instance Culling 路径处理

ProfileGPU、RHI 统计和当前版本的 Mesh Draw Command Dynamic Instancing 统计比较,不用 Actor 数直接推断 Draw Call。

H4:遮挡剔除结果具有时间性#

  1. 在大遮挡墙后放置大量对象。
  2. 固定相机观察稳定状态,再快速横移使对象显露。
  3. 捕获连续多帧的 Depth、HZB / Occlusion 事件和可见命令数。

预期:稳定遮挡时命令或实例工作下降;快速显露时系统保守恢复可见,可能出现短暂历史差异但不应持续漏画。

H5:Primitive 可见不代表全部 Instance 被提交#

  1. 用一个大范围 ISM / HISM 覆盖视锥内外。
  2. 固定 Primitive 总 Bounds,逐步改变可见 Instance 比例。
  3. 观察 Instance Culling 输入数、可见数和 Indirect Draw 参数。

若 Primitive 始终可见但 GPU 可见 Instance 数随视角改变,说明第二层剔除有效。

H6:Nanite 的几何成本不能用传统 Draw Call 单独解释#

  1. 准备视觉上相近的 Nanite 与传统 LOD 网格组。
  2. 固定材质、分辨率、View 与阴影设置。
  3. 分别记录 Mesh Draw Command、Nanite Instances / Clusters / Raster、Depth 与 Base Pass 时间。

预期:Nanite 传统 Draw Call 可能更少,但 Cluster、Raster、Streaming 与材质成本仍随可见复杂度变化。

性能观测表#

症状先看再验证
Render Thread InitViewsstat 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 PassBounds、实例层级、Vertex Factory 支持
遮挡后仍贵HZB / Occlusion 可视化、连续帧 CaptureBounds 太大、相机运动、Query 延迟、Pass 不参与遮挡
Nanite 几何贵Nanite Overview / Clusters / OverdrawCluster 数、Raster Bin、VSM、Streaming、材质
偶发 PSO 卡顿Insights、PSO 日志 / 平台工具PSO Precaching 覆盖,而非仅减少 Draw Call

可用于实验的命令和变量包括 FreezeRenderingr.VisualizeOccludedPrimitivesr.HZBOcclusionr.AllowOcclusionQueriesr.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 CallBatch 可拆分、多 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 又何时只是看起来异步。

官方资料#

分享

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

UE5 渲染管线 02:InitViews、可见性与 Mesh Drawing Pipeline
https://example.pages.dev/posts/ue5-rendering-pipeline/02-visibility-mesh-drawing/
作者
博主
发布于
2026-07-16
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录