现代渲染剔除技术
摘要
本文由 Saints Row: The Third Remastered 的一位开发者撰写,详细讲解了包含距离剔除、背面剔除和视锥体剔除在内的现代渲染剔除技术,并为致力于实时图形优化的游戏开发人员提供了宝贵的实践经验。
暂无内容
查看缓存全文
缓存时间: 2026/04/21 04:56
# 现代渲染剔除技术 | krupitskas 🌦️ 来源: https://krupitskas.com/posts/modern_culling_techniques/ ... https://krupitskas.com/modern_graphics_culling_techniques/srtt.png 《黑道圣徒:重启版》\- 我首款正式发售的作品。Steelport 是一座高密度的开放世界城市,但游戏也包含狭窄的室内走廊、喷气机、车辆和跳伞关卡。在如此复杂的环境中实现正确的剔除一直是个真正的挑战。向 Timur Gagiev 致敬\- 绝对是位大神!## 简介 在现代 AI 编程、“AI 生成游戏”、DLSS 5、Unreal Engine 5 以及惊艳的 Gaussian Splatting 演示层出不穷的时代,人们往往认为图形学和游戏开发已是“已解决的问题”。“只要拿上 AI,几天内就能开始做游戏。”他们说。显然这纯属胡扯。硬核的工程工作、专业知识、取舍权衡以及美术指导都不会消失。无论你是做 2D 还是 3D,写实还是卡通风格,设定在封闭的火星基地还是丧尸横行的开放世界纽约,你都需要对它进行优化。每个游戏都在使用、并将持续使用的最重要优化手段之一就是**剔除(Culling)**。好消息是:我职业生涯中见过的近 80% 的优化,归根结底就是“在不必要时别干蠢事”。坏消息是:你仍然需要实现剔除,同时平衡场景结构、游戏设计、美术方向、硬件限制和性能预算。因此,本文将梳理现代实时渲染器中使用的主要剔除技术。我会按类别分组,以便更直观地看出它们之间的联系。几乎每一项技术都值得单独写一篇文章,因为一如既往,**魔鬼都在细节里**。
---
## 1\. 基础技术:距离剔除、背面剔除与视锥体剔除 这些是最便宜且最广泛应用的技巧。它们在运行任何昂贵的逻辑之前先捕获明显的情况。
### 距离剔除(Distance Culling)
最简单的形式:如果对象离摄像机的距离超过某个最大阈值,就跳过它。就这么简单。这极快且对小道具非常有效,因为它们消失时的视觉影响微乎其微。大多数引擎允许你为每个网格或每种材质设置剔除距离。棘手的地方在于避免可见的突变(pop-in)。常见的缓解措施包括抖动淡出(dithered fade-out)、在剔除点之前激进的 LOD 过渡,或使用代理面(impostors,即用 billboard 替换远处真实网格的替身图)。这部分在下面“屏幕尺寸剔除”章节有更详细的说明,但值得在此指出:如果一个对象投影到屏幕上只有寥寥几个像素,那么绘制它的开销通常是不值得的。仅靠距离无法干净利落地捕捉这种情况\- 你还需要一个屏幕空间大小检查。
... https://krupitskas.com/modern_graphics_culling_techniques/popping-issue.gif 夸张示例:房屋和小石头。小石头在远处消失几乎不易察觉,但大物体忽隐忽现却很难不被注意到。
### 背面剔除(Backface Culling)
当你接触图形 API 时,通常会遇到的第一个剔除技术。因为它作为管线状态对象(PSO)配置的一部分提供,并且是启用后收益最容易实现的选项之一。每个三角形都有正面和背面。对于闭合网格,背面永远不可见,因为它们位于物体内部。GPU 可以基于环绕顺序(winding order)自动跳过它们,这通常能为典型几何体节省约一半的光栅化和片元工作。
... https://krupitskas.com/modern_graphics_culling_techniques/winding_order.png 三角形环绕顺序 ... https://krupitskas.com/modern_graphics_culling_techniques/backface_culling.gif 旋转二十面体显示面向观察者的三角形被光栅化,而其余三角形被跳过
值得注意的是:在传统顶点+片段管线中,背面剔除发生在*顶点着色器已经处理完顶点之后*。所以你并没有节省顶点工作量,只节省了光栅化和片元阶段的工作。在更依赖 GPU 驱动的管线中,你可以将这个决策提前,例如在 compute 或 task/amplification 阶段剔除 meshlets,使它们根本不会进入光栅化阶段。这几乎是免费的,但了解它很重要,因为它会与透明度、双面材质以及一些显式利用此特性的剔除算法产生交互。
### 视锥体剔除(Frustum Culling)
... https://krupitskas.com/modern_graphics_culling_techniques/frustum_culling.gif 俯视视角:视锥体外的对象会被剔除,永远不会提交给渲染管线
对于透视摄像机,视锥体是一个截头金字塔形状的空间,代表摄像机能看到的区域。超出这个范围的内容无需渲染。视锥体剔除通过将对象(通常通过包围体积如球体或 AABB)与视锥体的六个平面进行测试,来跳过任何不与之相交的对象。这几乎总是剔除流水线的第一步,或者是距离剔除之后的第二步。它快速、廉价,且能在一步中砍掉场景中巨大的部分,尤其是在开放世界中,地图的大部分可能位于摄像机后方或侧面。
... https://krupitskas.com/modern_graphics_culling_techniques/horizon-frustum.gif 这就是《地平线:零之曙光》的视锥体剔除工作原理
注意上面的动图中,像山这样的大型物体即使几乎完全移出视锥体依然会被渲染。这是对象级剔除的核心权衡:大量小型对象能让你进行细粒度的剔除操作,但每个对象都是一次绘制调用和一次 CPU 端的可见性测试。而少数大型对象在绘制调用上很节省,但当 90% 的三角形都位于屏幕外时,你还是得把整个模型渲染出来\- 而且你会为所有这些三角形支付**顶点着色器开销**,因为光栅器是在顶点着色之后才进行裁剪的。这种在屏幕外几何体上浪费的顶点工作,正是第 4 节中 meshlet 剔除所要解决的问题。
---
## 2\. 遮挡剔除(Occlusion Culling)
遮挡剔除告诉你哪些东西被其他东西挡住了。它更难实现,但在城市或室内等密集场景中通常能带来最大的性能提升。
... https://krupitskas.com/modern_graphics_culling_techniques/occlusion-culling.gif 这里仅展示了遮挡剔除效果。注意房子右侧后面的方块消失了;当我们从拐角窥探时,其中一些又出现了。
### 硬件遮挡查询(Hardware Occlusion Queries)
所有主流图形 API 都暴露了类似遮挡查询的功能。Direct3D 12 有查询堆(query heaps),Vulkan 有遮挡查询,Metal 有可见性结果缓冲区(visibility result buffers)。原理相同:渲染代理几何体(通常是对象的边界框),并统计有多少样本通过了深度测试。零可见样本意味着该代理完全被该视角遮挡,因此真实对象通常可以被跳过。在 DX12 中,你会使用 `D3D12_QUERY_TYPE_BINARY_OCCLUSION`,它只返回 0 或 1 而不是精确的样本计数\- 更便宜且足以用于剔除:
```cpp
// setup (once)
D3D12_QUERY_HEAP_DESC desc = { D3D12_QUERY_HEAP_TYPE_OCCLUSION, objectCount };
device->CreateQueryHeap(&desc, IID_PPV_ARGS(&queryHeap));
// per frame - render proxy, wrap with query
cmdList->BeginQuery(queryHeap, D3D12_QUERY_TYPE_BINARY_OCCLUSION, objectIndex);
cmdList->DrawIndexedInstanced(...); // draw bounding box
cmdList->EndQuery(queryHeap, D3D12_QUERY_TYPE_BINARY_OCCLUSION, objectIndex);
// resolve to a readback buffer (still on GPU timeline)
cmdList->ResolveQueryData(queryHeap, D3D12_QUERY_TYPE_BINARY_OCCLUSION, 0, objectCount, readbackBuffer, 0);
```
关键问题是**延迟和同步**。结果只有在 GPU 完成执行后才能对 CPU 可见,因此在实践中,你通常在渲染第 N+1 帧时读取第 N 帧的结果。这一帧的延迟通常可以接受,但它可能会短暂导致渲染刚刚变为遮挡的对象,或者跳过刚刚变为可见的对象。
### 软件遮挡剔除(CPU端)
不询问 GPU,而是在 CPU 上光栅化一个低分辨率的深度缓冲,并据此测试对象。Intel 的 Masked Software Occlusion Culling (MSOC) 可能是这里最著名的实现。它使用 SIMD 以 8x4 像素的瓦片为单位光栅化三角形,每秒可处理数百万个三角形。优点是完全没有读取延迟,因为它全部在 CPU 上进行,在你将任何东西提交给 GPU 之前就完成了。缺点是 CPU 开销大,且需要维护单独的简化遮挡网格,因为你负担不起光栅化完整场景几何体的开销。
... https://krupitskas.com/modern_graphics_culling_techniques/battlefield3-scene.png 《战地 3》\- 最终渲染场景 ... https://krupitskas.com/modern_graphics_culling_techniques/battlefield3-occ-scene.png 《战地 3》\- CPU 软件遮挡器光栅化的同一场景。结果是有意粗糙的\- 误剔除一个实际可见的对象比漏剔除一个应该跳过的对象更糟糕。
### Hi\-Z(层级 Z 缓冲 / Hierarchical Z\-Buffer)
Hi\-Z 是深度缓冲的 Mipmap 链,常被称为深度金字塔,其中每一层存储屏幕较大区域的保守深度值。
... https://krupitskas.com/modern_graphics_culling_techniques/hi_z_scene_mips.png 一个非常简单 3D 场景的可视化
要测试对象是否被遮挡,将其边界投影到屏幕空间,选择与其投影覆盖范围大致匹配的 Mip 级别,然后将对象的最近深度与金字塔进行比较。对于传统的 `LESS` 深度测试,该金字塔通常在每个区域存储**最大**深度;而在 reversed-Z 下,通常存储的是**最小**深度。重要的是,表示方式必须保持保守。如果测试结果是“被遮挡”,你可以安全地跳过该对象。如果不是,则保留。优秀的实现倾向于宁可假阴性(漏剔除)也不愿假阳性(误剔除)。
... https://krupitskas.com/modern_graphics_culling_techniques/hi_z_pyramid.png HI-Z 金字塔
这是目前大多数 GPU 驱动遮挡剔除的基础。构建和查询速度快,且完全存在于 GPU 上。
### 双通道遮挡剔除(Two-Pass Occlusion Culling)
GPU 驱动渲染器中的一种常见模式:使用上一帧的 Hi-Z 在当前帧渲染前先剔除对象。简单版本是一个通道:用上一帧的 Hi-Z 剔除一切,渲染幸存下来的对象。这很便宜,但刚刚变为可见的对象会被错误剔除并在一帧内保持不可见。双通道版本修复了这个问题。第一遍测试上一帧可见的对象,渲染幸存者,并从它们构建一个新的 Hi-Z。第二遍然后重新测试在第一遍中被剔除的所有对象,并使用新的 Hi-Z 进行检测。任何刚刚变为可见的对象都会获得第二次机会并在此帧渲染。由于第一遍使用的 Hi-Z 仍是一帧前的数据,因此会残留一点不准确性,额外的通道无法解决。在“正常游玩”中你几乎不会注意到。它在硬镜头切换(如突然旋转 90 度)的情况下会失效:第一遍的可见集合基本错误,重建的 Hi-Z 不可靠,导致出现一帧坏画面。引擎通常会检测这种情况并回退到该帧的全深度预通行。GPU 端的成本远低于始终执行全深度预通行,这也是为什么大多数现代游戏引擎采用这种方法的原因。
---
## 3\. 更多剔除技术!
### 屏幕尺寸剔除(Screen Size Culling)
不使用固定的世界空间距离,而是根据投影后的屏幕面积进行剔除。一个距离 10 米的对象可能值得渲染,但同一个对象在 2000 米外可能只投射成 3 个像素,不值得承担绘制调用的开销。屏幕尺寸剔除比纯粹的距离阈值更能优雅地处理这种情况。例如,Unreal 使用屏幕尺寸作为静态网格 LOD 过渡的主要指标,而最小/最大绘制距离则是独立的基于距离的控制。
### PVS(潜在可见集 / Potentially Visible Sets)
PVS 预先计算世界的每个区域中,从该区域出发理论上可以看到哪些其他区域。运行时只需查找当前区域的 PVS,并跳过不在其中的任何对象。这在运行时极快,但计算成本高,且对动态对象支持不佳。
... https://krupitskas.com/modern_graphics_culling_techniques/pvs_rooms.gif 虽然这是预计算且高效的,但对于程序化生成的游戏来说可能不切实际甚至不可能。
Quake 让 PVS 名声大噪。它在某些室内游戏中仍然有用,前提是场景几何体是静态的,且烘焙时间可以接受。
### 传送门剔除(Portal Culling)
对于具有明确定义的房间和门口的室内场景,传送门剔除**非常有效**。每个门口都是一个 portal。你沿着传送门追踪摄像机的视野,只渲染可通过可见传送门到达的房间。这能以极低的代价消除整整一整间房间的几何体。传送门剔除出现在许多设定在建筑内的第一人称游戏中。它与视锥体剔除很好地互补,因为当你穿过多个门口时,portal 自然会缩小有效的视野锥。
---
## 4\. GPU 驱动渲染与簇剔除(Cluster Culling)
有趣的部分来了!不再由 CPU 决定绘制什么并为每个对象发出一次绘制调用,你将剔除逻辑推送到 GPU,并使用间接绘制调用让 GPU 自行决定。
### 间接绘制(Indirect Drawing)
DirectX、Vulkan 和 Metal 都支持间接绘制,尽管具体的 API 有所不同。绘制参数(如索引数量和基础顶点索引)来自 GPU 缓冲区,而不是 CPU 代码。计算着色器运行剔除逻辑,并将仅幸存的对象写入该缓冲区。然后,一个或少量的间接绘制消耗这个紧凑列表,这样 CPU 就不再需要遍历每个可见对象。
```hlsl
// compute shader - one thread per object
[numthreads(64, 1, 1)]
void CullCS(uint id : SV_DispatchThreadID)
{
if (id >= g_objectCount) return;
ObjectData obj = g_objects[id];
if (!SphereInFrustum(obj.center, obj.radius)) return;
uint slot;
InterlockedAdd(g_drawCount[0], 1, slot); // 原子操作压缩槽位
g_drawArgs[slot] = MakeDrawArgs(obj); // 写入绘制参数
}
```
```cpp
// CPU side - dispatch cull, barrier, draw
cmdList->Dispatch((objectCount + 63) / 64, 1, 1);
cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::UAV(drawArgsBuffer));
cmdList->ExecuteIndirect(cmdSig, objectCount, drawArgsBuffer, 0, countBuffer, 0);
```
`InterlockedAdd` 将幸存的绘制打包成紧凑列表。`ExecuteIndirect` 接收 GPU 编写的计数,因此它只处理真正通过剔除阶段的对象。
### Meshlet / 簇剔除(Cluster Culling)
现代 GPU 驱动渲染器可以在子网格级别进行剔除,无论是在 compute 阶段还是在 mesh/task/amplification 着色器阶段。网格被拆分为 meshlets(小型三角形簇),通常每个包含 64\-128 个三角形。每个 meshlet 都有自己的包围球和一个代表其三角形法线范围的控制锥(cone)。
... https://krupitskas.com/modern_graphics_culling_techniques/meshlets.png 这里的每个彩色块都是独立的 meshlet
你可以在 GPU 上独立地对每个 meshlet 运行视锥体剔除、遮挡剔除和背面剔除,这比对象级剔除精确得多。一个大型角色网格可能有数百个 meshlet,而从任何给定角度看,只有一小部分 meshlet 是可见的。法线控制锥特别巧妙。如果 meshlet 中的所有法线大致指向同一方向,你可以通过一个非常廉价的锥体-vs-视图测试拒绝整个 meshlet。这本质上就是在簇级别的背面剔除。
---
## 5\. 虚拟化几何体:Nanite
Nanite(Unreal Engine 5)值得单独提及,因为它将上述几种思想结合到了一个统一的系统中。目标是通常你不需要手动制作 LOD 链。相反,每个网格都存储为一个簇层次结构,较粗的层级是通过简化更细的层级生成的。
... https://krupitskas.com/modern_graphics_culling_techniques/nanite.jpg Mooooore meshlets!!!
在运行时,该层次结构会在 GPU 上遍历。每个簇都会根据可见性和屏幕空间误差进行测试。如果一个簇
相似文章
像1993年那样制作图形
一位开发者详细介绍了如何构建《Catlantean 3D》——一款采用1993年时代图形技术(256色、320x240分辨率、手工制作资产、无人工智能)的第一人称射击游戏,计划在Steam上发布,重点讲解调色板渲染和资产创建。
天空、日落与行星的渲染
一篇技术博客文章,提供了使用着色器实现逼真天空、日落和行星渲染的逐步指南,重点介绍了瑞利散射和米氏散射等大气散射技术。
《细胞分裂》资深设计师称现代真实光照破坏了潜行游戏体验
资深《细胞分裂》设计师Clint Hocking指出,现代真实光照技术(如光线追踪)让潜行游戏更难被玩家解读,因为逼真的阴影和环境光遮蔽模糊了老游戏中清晰的视觉线索。他呼吁开发者重新思考如何使用高级光照来保持游戏可读性。
计算相机射线
一篇技术博客文章,推导如何从视图投影矩阵计算用于光线追踪的相机射线,包含着色器代码并处理数值稳定性问题。
成为图形程序员需要学习什么
一篇博客文章,概述了成为实时图形程序员所需的技能,涵盖现代渲染API、路径追踪、基于物理的渲染以及机器学习在游戏开发中的作用。