在现代游戏引擎的渲染优化中,剔除技术的选择与组合直接决定了场景的极限承载能力。从最基础的视锥剔除到 GPU 驱动的 meshlet 级裁剪,每一层裁剪都在以不同的粒度和成本换取更低的渲染开销。本文将以遮挡剔除和 GPU 驱动渲染为核心,深入剖析 Hi-Z(层次 Z 缓冲)遮挡剔除的工作机制、两遍渲染策略的工程实现,以及现代引擎如何通过层次化裁剪体系在复杂场景下维持帧率稳定。
遮挡剔除的核心挑战
遮挡剔除解决的是 “什么是被其他物体挡住” 的问题。在密集的城市街景或室内场景中,摄像机前方的大量物体被前景建筑遮挡,如果不做处理而直接提交给 GPU,会造成无意义的顶点处理和片元着色器开销。然而遮挡剔除的难点在于其计算成本和延迟特性:传统的硬件遮挡查询需要在 GPU 完成渲染后才能得到结果,这意味着 CPU 读取到的永远是上一帧的遮挡状态,存在天然的一帧延迟。软件遮挡剔除虽然在 CPU 端可以做到零延迟,但需要额外的简化的遮挡体网格,且 CPU 的并行计算能力远不及 GPU。
Hi-Z(层次 Z 缓冲)的出现为这一困境提供了优雅的解决方案。Hi-Z 本质上是一个深度缓冲的 MIP 链,也被称为深度金字塔。每一层 MIP 都存储着对应屏幕区域内的保守深度值 —— 对于传统的 LESS 深度测试,通常存储区域内的最大深度;对于反转 Z(Reversed-Z),则存储最小深度。这种保守性至关重要:当 Hi-Z 测试返回 “被遮挡” 时,渲染器可以安全地跳过该物体;而当测试返回 “可能可见” 时,物体仍会被正常渲染。好的实现倾向于产生假阴性(将可见物体误判为不可见)而非假阳性(将不可见物体判为可见),因为前者只是多渲染了少量物体,后者则会导致明显的视觉错误。
两遍遮挡剔除的工程实现
现代 GPU 驱动渲染器中,两遍遮挡剔除(Two-Pass Occlusion Culling)是极为常见的架构模式。其核心思想是利用上一帧构建的 Hi-Z 来剔除本帧的物体,但在实现细节上需要处理物体刚刚从遮挡变为可见的情况。
第一遍渲染时,系统使用上一帧构建的 Hi-Z 对所有物体进行可见性测试。上一帧的 Hi-Z 是可靠的,因为它来自已经完成渲染的完整帧画面。通过测试的物体会被提交渲染,同时利用这些可见物体构建本帧全新的 Hi-Z。第二遍渲染时,第一遍中被判定为不可见的物体会被重新提取出来,用新构建的 Hi-Z 进行二次测试。这一遍使用的 Hi-Z 来自本帧已经渲染的可见物体,因此能够捕获那些 “刚刚变得可见” 的物体 —— 它们在第一遍中被错误剔除,但在第二遍中获得了正确的判断。
这种架构的 Residual 误差无法通过更多遍数完全消除,因为第一遍使用的 Hi-Z 始终落后一帧。在正常游戏过程中,这种一帧的延迟几乎不可感知。然而在硬切镜头(如突然旋转 90 度)场景下,第一遍的可见集合完全失效,新构建的 Hi-Z 也只包含极少量物体,导致第二遍的测试结果同样不可靠。主流引擎通常会检测这种情况并回退到全深度预传递(Full Depth Prepass)策略,用可靠的深度缓冲来驱动后续的遮挡剔除。
从性能角度看,两遍剔除的 GPU 开销远低于每帧执行完整的深度预传递。深度预传递需要渲染所有几何体一次以建立深度缓冲,而两遍剔除只在第一遍渲染可见物体的同时就完成了 Hi-Z 的构建,后续的精细剔除完全基于这个已有的深度金字塔进行,避开了重复的几何体遍历。
GPU 驱动渲染与间接绘制
GPU 驱动渲染代表了另一个维度的范式转移。传统渲染架构中,CPU 负责遍历场景、判定每个物体的可见性、组装绘制调用并提交给 GPU。这种架构在物体数量较少时工作良好,但当场景包含数十万甚至数百万个物体时,CPU 端的遍历和状态切换成为瓶颈。GPU 驱动渲染将剔除逻辑转移到 GPU 上,利用计算着色器并行处理大量物体的可见性判定,并通过间接绘制(Indirect Draw)让 GPU 自行消费剔除结果。
间接绘制的 API 在不同图形 API 中略有差异,但核心思路一致:绘制参数不是由 CPU 代码指定,而是从 GPU 缓冲区读取。计算着色器执行完剔除后,将存活物体的绘制参数写入一个紧凑的缓冲区,同时原子递增一个计数器。随后的 ExecuteIndirect 调用读取这个计数器作为实例数量,从而只处理真正需要渲染的物体。这种模式彻底消除了 CPU 遍历所有物体的必要性,也避免了每帧数十万次绘制调用的提交开销。
Meshlet 与簇级裁剪
GPU 驱动渲染的下一个层次是在单个网格内部进行更细粒度的裁剪。Meshlet(网格簇)将一个网格拆分为大量小型三角形簇,每个簇通常包含 64 到 128 个三角形。每个 meshlet 有自己的包围球和一个法线锥 —— 法线锥表示该簇所有三角形的法线方向范围。这个法线锥的妙处在于:如果观察方向落在法线锥之外,说明整个 meshlet 相对于摄像机都是背面的,可以直接跳过整个簇的渲染。这是传统背面剔除在簇级别的扩展,但计算成本极低。
结合 Hi-Z 和视锥剔除,meshlet 级别的裁剪可以在 GPU 上以极高的并行度运行。一个大型角色模型可能包含数百个 meshlet,从任意视角通常只有少部分可见。通过在放大(Amplification)着色器或计算着色器中对每个 meshlet 执行视锥测试和 Hi-Z 遮挡测试,只有真正可见的 meshlet 才会进入后续的顶点处理和光栅化阶段。这从根本上解决了对象级裁剪的固有问题 —— 大型物体即使只有一小部分可见,也必须提交全部顶点,而 meshlet 级别的裁剪可以精确到每个三角形簇。
Nanite 虚拟化几何体的启示
Unreal Engine 5 的 Nanite 系统代表了虚拟化几何体的最新成果。它将上述技术整合为一套统一系统:每个网格都存储为簇的层次结构,不同层级的簇代表不同精度的简化版本。运行时 GPU 遍历这个层次结构,根据屏幕空间误差动态选择渲染哪个精度的簇。如果某个簇在屏幕上的投影过小,系统自动降级到更简化的版本;如果投影过大,则升级到更精细的版本。这种思路彻底解放了美术的手动 LOD 制作工作,同时保证了每帧只渲染视觉上必要的三角形数量。
Nanite 的另一项关键技术是对极小三角形的软件光栅化处理。当三角形小到一定程度时,固定功能硬件光栅化的每三角形开销会超过其实际贡献的像素数量。Nanite 检测这种情况并切换到自定义的软件光栅化路径,在 CPU 或 GPU 计算着色器中以极低开销处理这些微型三角形。这种分级策略确保了十亿级三角形场景在现代硬件上的可玩性。
光照与阴影的剔除策略
几何体剔除之外,光照和阴影的剔除同样关键但往往被忽视。在密集场景中,大量光源的着色开销可能远超几何体本身。前向增强(Forward+)将屏幕划分为 2D 瓦片,每个瓦片记录与之相交的光源列表,着色时只需遍历本地光源列表而非全局光源集合。簇式光照剔除(Clustered Light Culling)进一步将 2D 瓦片扩展为 3D 簇,沿着深度轴对视锥进行切片,对远近场景差异明显的场景效果更佳。
对于阴影渲染,级联阴影贴图(CSM)本身已按深度范围划分,每级级联覆盖不同的距离区间。几何体只需参与其所在深度区间对应的级联渲染,而阴影投射体的剔除则基于每级级联独立的视锥和光照体积。这些技术叠加在一起,构成了现代渲染器的完整可见性优化体系。
实践建议
构建自己的渲染器时,建议从最基础的优化入手:首先确保每个物体有准确的包围体,这是所有后续剔除的共同基础;然后实现视锥剔除和合理的 LOD 切换策略;最后根据场景特性决定是否需要遮挡剔除。对于开放世界或物体稀疏的场景,遮挡剔除的收益有限;而对于室内或城市密集场景,Hi-Z 两遍剔除配合 GPU 驱动渲染能带来显著的帧率提升。当 CPU 提交成为瓶颈时,间接绘制和 meshlet 级裁剪是自然的演进方向。
好的剔除系统永远不会追求 100% 的准确性 —— 那是永远不可能达到的目标。工程实践中的原则是:对正确性保持保守,对浪费保持激进。多渲染几个可见物体远比物体在玩家眼前凭空消失要好接受得多。正是这种务实态度,使得现代游戏能够在保持视觉保真度的同时,维持稳定的帧率表现。
参考资料