1. 引言
在实时渲染中,遮挡剔除(Occlusion Culling)是提升性能的关键手段之一。它的核心思想很简单:不绘制那些被其他物体挡住、最终不会出现在屏幕上的物体。而 HZB(Hierarchical Z-Buffer,层次 Z 缓冲)正是实现高效遮挡剔除的一种经典且强大的数据结构。本文将从零开始,用通俗的语言带你理解 HZB 的原理、构建方式以及它在实际渲染管线中的应用。
2. 为什么需要遮挡剔除
想象一个大型游戏场景:你站在一栋大楼前,身后是整座城市。如果 GPU 老老实实地把城市里每一栋楼、每一条街道的三角形都画一遍,那么大量被大楼挡住的物体其实根本不会出现在屏幕上,白白浪费了宝贵的渲染时间。
遮挡剔除要解决的就是这个问题:在绘制之前,先判断某个物体是否可见,如果不可见就直接跳过。常见的剔除手段包括:
- 视锥剔除(Frustum Culling):剔除不在摄像机视野范围内的物体。
- 背面剔除(Backface Culling):剔除背对摄像机的三角形。
- 遮挡剔除(Occlusion Culling):剔除被其他物体完全遮挡的物体。
前两种相对简单,而遮挡剔除因为要判断“谁挡住了谁”,需要更巧妙的数据结构,HZB 就是其中的代表。
3. 从 Z-Buffer 到 HZB
要理解 HZB,先得回顾它的基础——Z-Buffer(深度缓冲)。
在渲染过程中,GPU 会为屏幕上的每个像素记录一个深度值,表示该像素距离摄像机有多远。当两个物体投射到同一个像素时,深度值小的(更靠近摄像机)会覆盖深度值大的。这个深度缓冲就是 Z-Buffer。
Z-Buffer 本身是逐像素的,分辨率很高。但如果我们想快速判断“一个很大的物体是否被遮挡”,逐像素检查显然太慢了。于是就有了 HZB 的思路:把 Z-Buffer 逐级缩小,每一级都记录上一级中更大(远)的深度值(即更远离摄像机的值),形成一棵“深度金字塔”。
这样,当我们想判断一个物体是否被遮挡时,不需要看每一个像素,只需要在合适的层级上做少量采样即可。
4. HZB 的构建过程
HZB 通常不是直接证明“物体可见”,而是尝试证明“物体肯定被完全挡住”。只要无法确定,就保守地让物体继续绘制。
HZB 的构建本质上是一个降采样(Downsampling)的过程。假设原始 Z-Buffer 的分辨率是 1024×1024,那么构建过程如下:
- 第一级(Level 0)就是原始的 Z-Buffer。
- 第二级(Level 1)分辨率降为 512×512,每个像素取 Level 0 中对应 2×2 区域里的最大(远)深度值。
- 第三级(Level 2)分辨率降为 256×256,每个像素取 Level 1 中对应 2×2 区域里的最大(远)深度值。
- 以此类推,直到分辨率降到 1×1 为止。
为什么要取最大值而不是最小值?因为最大值代表“这个区域里离摄像机最远的深度”。如果一个物体的深度比这个最远值还大,说明它完全在这个区域之后,必然被遮挡。
在实际工程中,在普通 Z 和 Reversed-Z 中,深度数值的远近方向相反,所以实现里到底取min还是max不能死记。原则始终是一样的:不能漏掉任何可能可见的部分,降采样时取最远的深度值。
5. 用 HZB 做遮挡剔除
有了 HZB 之后,遮挡剔除的流程大致如下:
- 把待检测物体的包围盒(Bounding Box)投影到屏幕上,得到一个屏幕空间矩形。
- 根据这个矩形的大小,在 HZB 中选择一个合适的层级。原则是:矩形越大,选择的层级越高(分辨率越低),这样只需要采样少数几个像素。
- 在选定的层级上,采样矩形覆盖范围内的深度值,取其中的最小(近)值。
- 把物体的实际深度(通常是包围盒离摄像机最近的那个点的深度)与采样到的深度值比较:如果物体的深度更大,说明它完全被遮挡,可以剔除;否则保留。
这里的关键是“选择合适的层级”。如果层级选得太高(分辨率太低),采样结果过于粗糙,可能会误判(把本来可见的物体剔除掉);如果层级选得太低(分辨率太高),采样次数太多,性能收益不明显。工程上通常根据投影矩形的面积来动态选择层级。
6. 一个简单的伪代码示例
下面用伪代码描述 HZB 遮挡剔除的核心逻辑,帮助你理解整体流程:
// 判断物体是否被遮挡 bool IsOccluded(HZB hzb, BoundingBox bounds, Camera camera) { // 1. 把包围盒投影到屏幕空间 ScreenRect rect = ProjectToScreen(bounds, camera); // 2. 根据投影面积选择合适的 HZB 层级 int level = SelectLevel(rect.Area()); // 3. 在选定层级上采样深度,取最大(远)值 float maxDepth = hzb.SampleMaxDepth(rect, level); // 4. 计算物体离摄像机最近的深度 float objectDepth = bounds.NearPlaneDepth(camera); // 5. 如果物体深度更大,说明被完全遮挡 return objectDepth > maxDepth ; }7. HZB 的优缺点
| 优点 | 缺点 |
|---|---|
| 剔除效率高,适合大量静态和动态物体 | 构建 HZB 本身需要额外的 GPU 开销 |
| 层级结构天然支持“由粗到细”的快速判断 | 对细长或形状不规则的物体,包围盒投影可能不够精确 |
| 实现相对成熟,主流引擎均有支持 | 需要维护深度缓冲,且更新时机需要精心设计 |
在实际项目中,HZB 通常不会单独使用,而是与视锥剔除、软件光栅化剔除等手段配合,形成一套完整的剔除方案。
8. 总结
HZB 是一种基于深度金字塔的遮挡剔除技术,核心思想是把逐像素的深度信息逐级降采样,从而用很小的代价快速判断物体是否被遮挡。它的构建过程简单、剔除效率高,是实时渲染中不可或缺的工具之一。
9.Tips
1、HZB 并不能完全判断所有遮挡情况,例如下图,下边一个简化的二维空间例子,A点比B点离摄像机更近,所以经过遮挡剔除算法并不能判断方块被遮挡,方块不会在遮挡剔除这个阶段被剔除。方块还是会被渲染,但是方块的像素颜色否显示在最终的画面上,还是看方块的每个像素的深度是否在相机的最近距离了