屏幕空间遮挡与可见性剔除工程 2026:HBAO/GTAO 与 GPU 驱动的统一取舍
把像素级遮挡信号与几何级可见性剔除放在一个统一的四元组框架下,2026 年的最优解是 GTAO + HiZ + GPU-Driven Meshlet Cull + Visibility Buffer 的组合,关键约束是跨硬件 tier 的时空稳定性预算。
约 60 分钟阅读17,741 字6 次阅读博主

把像素级遮挡信号与几何级可见性剔除放在一个统一的四元组框架下,2026 年的最优解是 GTAO + HiZ + GPU-Driven Meshlet Cull + Visibility Buffer 的组合,关键约束是跨硬件 tier 的时空稳定性预算。

本文面向游戏引擎、图形程序员、技术美术与渲染管线负责人。我们把"遮挡"拆成"像素级遮挡信号"(SSAO/HBAO/GTAO/SSDO 家族)与"几何级可见性剔除"(CPU 端 Frustum/Portal/Hierarchy + GPU 端 Indirect/Cull/Visibility Buffer)两条互锁的技术线,在 2026 年这个时点上给出一个跨硬件、跨引擎、跨平台的统一架构判断与可落地清单。涉及的硬件覆盖 NVIDIA Ada/Hopper、AMD RDNA 3/4、Apple M3/M4 的 GPU,以及 Mali G720、Immortalis-G720、Xclipse 等移动 GPU。
任何实时渲染管线,无论它声称自己是前向、延迟、clustered、还是 forward+,最终都要回答同一个问题:对于屏幕上的每一个像素 ,它在当前帧是否被场景几何遮挡?这个看似简单的二值问题,在工程上却分裂为两条互锁的子问题——"像素级遮挡信号"(ambient occlusion, contact shadow 等光照质量增强)与"几何级可见性剔除"(减少进入管线的几何量)。前者决定"画面看起来对不对",后者决定"管线能不能跑得动"。
把这两个问题分开看,2026 年的现状是:SSAO 家族(HBAO+、GTAO、SSDO、Horizon-Based、Ground Truth AO)已经收敛到 GTAO 主导;HBAO+ 在低端硬件和移动端仍有生存空间,因为它的指令数最少且对显存带宽友好。几何剔除这条线则从十年前的"Frustum Culling + Occluder Rasterization"演化到今天的"GPU-Driven Pipeline + Visibility Buffer + Meshlet + Indirect Draw",软件堆栈全面洗牌。但这两条线在工程上从来不是独立的——HBAO 的精度依赖于深度缓冲的稳定性,而深度缓冲的稳定性又依赖于 HiZ 剔除是否做好了;GTAO 的"地面真值"参考依赖于高质量的 software ROP 遮挡;Indirect Draw 的可行性又依赖于 CPU 的粗剔除提供了多小的 draw surface 范围。三者构成一个"软光栅 → 深度缓冲 → AO 信号 → draw call 数量"的反馈环。
这种耦合性导致一个常见现象:一个引擎在 PC 端看起来跑得很好的 GTAO,在移动端可能完全不可用,不是 GTAO 本身慢,而是它依赖的深度稳定性在移动 GPU 的 tile-based 渲染下被打破了。类似地,Nanite 风格的 GPU 驱动管线在 PC 端很自然,在移动端却需要重写,因为移动 GPU 的 tile memory 架构对 meshlet 的 local memory 占用极度敏感。这就是为什么本文必须把两条线绑在一起讲,而不是像大多数技术博客那样分别写一篇文章。
另一个常见陷阱是把"可见性"理解为"剔除的几何量"。其实像素级遮挡的最终目标不是剔除,而是给光照模型一个正确的遮挡信号——SSAO 算的是 ambient light 衰减,不是 visibility 二值掩码;GTAO 算的是 directional occlusion,理论上可以用于补 directional light 的 shadow leak。这两条信号在物理上并不等价,但在视觉上常常被混用,这是很多引擎"开了 SSAO 但漏缝"的根因。
因此本文的结构是:第二节先把"遮挡信号"与"可见性剔除"统一到一个四元组的数学框架里(visibility function + cost model + stability budget + hardware affinity),后面六节分别落在这个四元组的具体取值上,第九节给出一份给引擎团队的可落地清单。
我们用一个四元组 来描述一个完整的可见性子系统:
任意一个可见性方案都可以定位在这四个维度上。例如:HBAO+ 是 ,GTAO Multi-Bounce 是 ,Nanite-style software culling 是 。
稳定性预算 是被严重低估的维度。很多引擎在设计 SSAO 时只考虑 与 ,导致 AO 出现"呼吸"现象——TAA 上一帧消除了抖动,SSAO 下一帧又制造了抖动。GTAO 的成功很大程度上是因为它的水平角采样在 normal direction 上的稳定性比 HBAO+ 的随机方向旋转更好,使得 TAA 后处理能压住 AO 抖动。Nanite 的成功则是因为它在 mesh shader 里做的 occlusion query 是 conservative rasterization,保证 在相邻帧不会跳变,从而彻底避免了"剔除跳变 → 几何 pop"的经典问题。
维度上的硬件分裂是 2026 年引擎架构设计的最大变量。NVIDIA Ada/Hopper 的 hardware RT cores 让 RT-based visibility(visibility buffer、RTX AO)在 PC 端可行;AMD RDNA 3/4 同样支持,但 wave size 不同导致 warp occupancy 调优完全不同;Apple Silicon(M3/M4)的 tile memory + on-chip RT 让 NEON-friendly 的 software ROP 在 iOS/MacOS 上重新获得竞争力;ARM Mali G720 的 bifrost 架构对 mesh shader 支持不完整,所以 Mali 上的 visibility buffer 必须退回 compute shader 模拟。同一个"屏幕空间 AO"方案,在这四种硬件上的可用度差异巨大。
Hardware Affinity 的细粒度刻画:除了 GPU 型号,还有 6 个二级维度影响 :
这意味着引擎的可见性方案不能再是"选一个 SSAO 算法",而必须是"选一组 的配置,让它在不同硬件 tier 上都满足预算"。这就是后文 §3-§6 的核心。
四元组的具体取值表:基于 2024-2026 年的实测数据,主流方案的 大致定位如下:
| 方案 | (RDNA 4) | (Mali G720) | |||
|---|---|---|---|---|---|
| Frustum Culling (CPU SIMD) | binary | 0.3ms (10K obj) | 0.5ms | high | CPU-only |
| HBAO+ 4-sample | horizon | 0.4ms | 0.8ms | medium | tile-friendly |
| HBAO+ 2-sample (mobile) | horizon | 0.2ms | 0.3ms | low | tile-friendly |
| GTAO 8-sample | integrated | 0.7ms | N/A | high | tile-friendly |
| RT-AO 1-spp | visibility | 0.3ms | N/A | high | RT-required |
| Software ROP (HiZ) | binary | 0.4ms | 1.2ms | medium | mem-bound |
| GPU Meshlet Cull | binary | 0.2ms | 0.8ms | high | mesh-shader |
| Visibility Buffer (full) | ID | 1.2ms | N/A | high | bandwidth |
注: 列是 GPU 端的 milliseconds,Mali G720 不支持 GTAO 与 RT-AO 的 full feature。
四元组的工程含义:选 visibility 方案不是"选一个算法",而是"在 的多维空间里找一个 sweet spot"。同一个 AO 算法在 RTX 4090 与 Mali-G610 上需要完全不同的 budget 分配,这就是为什么移动引擎团队的 visibility system 设计与 PC 团队的 visibility system 设计在表面看完全不同。
HBAO+ (Horizon-Based Ambient Occlusion +) 来自 NVIDIA 2012 年发布的 SDK,核心思想是把每个像素的深度沿若干水平方向(horizon direction)做一次 horizon scanning,得到 4-8 个 horizon angle,再积分成 AO 值。它的优势是算法简单、指令数少、对 depth buffer 的格式不敏感(只读 R16_FLOAT 或 R32_FLOAT 即可),在 Mali G610 这种老一代移动 GPU 上仍是首选。劣势是水平角方向的 horizon scanning 会沿垂直方向产生**"haloing"伪影**(在高曲率边缘出现白色光环),且对法线方向的依赖是"投影"式的,不是真正的方向积分。
GTAO (Ground Truth Ambient Occlusion) 由 Activision 与 Unity 团队在 2018-2020 年间提出,核心是把 AO 视为"沿 viewing direction 的 spherical cap 积分",通过对 normal 周围的几个方向做 Monte Carlo 采样,直接逼近"ground truth"。GTAO 在 维度上更接近物理(因为它的积分上限取决于 normal 与 viewing direction 的几何关系),在 维度上更稳定(因为采样方向固定,TAA 的 jitter 模式可以预知)。
2026 年的工程取舍已经基本收敛:
关键 trade-off 是 sample 数与 stability:HBAO+ 4 sample 在 Mali 上是 0.3ms,GTAO 8 sample 在 RTX 3060 上是 0.6ms,RT-AO 1 spp 在 RTX 4090 上是 0.2ms——后两者看似都更好,但前者在 tile memory 复用上让整体 pipeline 节省 0.5ms 的 bandwidth,综合性价比反而最高。
HBAO+ 的工程实现细节:HBAO+ 的核心是 "horizon scanning"——在 4-8 个水平方向上沿角度扫描 depth buffer,找到 horizon angle(从当前像素看出去的"地平线"角度),然后用 horizon angle 计算 ambient occlusion 的衰减。关键数学是:
其中 是方向 上的 horizon angle, 是 viewing direction 与 normal 的夹角。
工程上 HBAO+ 的实现通常用两个 render target 输出:R 是 AO 强度(0-1),G 是 horizon angle(用于 bent normal approximation)。half-resolution 渲染后再 upscale 到 full-resolution 时必须用 bilateral upscale——深度差大的像素不能跨边缘混合,否则会出现 halos。
GTAO Multi-Bounce 的工程实现细节:GTAO 把 AO 视为 "ground truth AO"——理论上的正解,但用 Monte Carlo 采样近似。Multi-Bounce 版本在单次采样后做一次 AO 自递归,把单次 bounce 的 AO 衰减再次积分回去,模拟光线的多次反射。GTAO 的核心数学是:
其中 是方向 上的 visibility(0 或 1)。Multi-Bounce 用类似 Approximate Multi-Bounce (AMB) 的算法,1 次 Monte Carlo 后做 1 次 AO 自积分,开销增加约 30% 但视觉显著改善。
Mobile 上的 HBAO+ 进一步退化到 2-sample 模式:在 Mali-G610 上 4-sample 已经超过 1ms budget,只能砍到 2-sample。代价是 AO 强度估计的 variance 翻倍,需要 temporal reprojection 来弥补。这就是为什么移动端的 AO pipeline 几乎必须配 TSR/DLSS 类的 temporal upscale + temporal AO reuse——否则 2-sample HBAO+ 在视觉上不可用。工程上常见的做法是把 HBAO+ 的输出存到 R16G16_FLOAT(其中 R 是 AO,G 是 sample count,用于 temporal blending 的 confidence weight),下一帧用 motion vector 把 AO reproject 回来,做 4:1 的 temporal accumulation。这样虽然单帧只用 2 sample,但视觉上等价于 8 sample,且总成本仍然在 0.5ms 以内。
RT-based AO 的工程取舍:RT-AO 在 RTX 4090 上是 0.2ms,看似完美,但有三个隐藏成本:
所以 RT-AO 不是"取代 SSAO",而是"在已有 SSAO 上叠加一层 high-quality 修正"——例如 HBAO+ 做 base AO,RT-AO 做 dynamic objects 的精细 AO correction。这是 UE5 的 Lumen 与 Nanite AO 的混合模式。
SSDO (Screen Space Directional Occlusion) 的崛起:SSDO 是 2014 年提出的算法,但因为早期硬件性能不够被搁置,2026 年随着硬件性能提升重新回到舞台。SSDO 用 ray marching 在光照 scene 求"光源方向"上的可见性,直接输出 bent normal 与 directional occlusion。优点是直接补 directional light 的 shadow leak,缺点是 ray marching 的 cost 与 ray 长度强相关,需要 adaptive ray length。SSDO 与 GTAO 的 bent normal 模式功能重叠,但 SSDO 更直接(直接采样光源方向),GTAO 更间接(采样 normal 周围的方向)。
工程上常见的踩坑是:不要把 HBAO+ 与 GTAO 视为"可替换的算法",而是视为两种 budget 选择。HBAO+ 是"spending 0.3ms for medium stability",GTAO 是"spending 0.6ms for high stability"。如果你的项目已经有高质量 TAA + temporal upscaling,你大概率不需要 GTAO 的 stability 优势,选 HBAO+ 更划算。
代码上的一个关键差异:GTAO 需要 bent_normal 作为副产物(用于补 directional light 的 shadow leak),而 HBAO+ 不输出 bent normal。如果你的 lighting pipeline 依赖 bent normal 做 light leaking 修正,那 GTAO 是必选;否则 HBAO+ 是更安全的默认。
// GTAO Multi-Bounce 简化版(伪代码)
float GTAO_MB(float3 posCS, float3 normalWS, float2 uv, int sampleCount) {
float ao = 0.0;
float3 bent = float3(0, 0, 1);
[unroll(sampleCount)]
for (int i = 0; i < sampleCount; ++i) {
// 沿 normal 周围的固定方向采样
float3 dir = RotateTangent(normalWS, i);
float3 endPos = posCS + dir * radius;
float sampledDepth = SampleSceneDepth(endPos.xy / endPos.z);
float horizon = CompareDepth(sampledDepth, endPos.z);
ao += horizon * Falloff(endPos.z);
bent += dir * horizon;
}
ao /= sampleCount;
bent = normalize(bent);
return ao + BentNormalCorrection(ao, bent);
}
软件光栅化遮挡(software raster occlusion)是指在 CPU 端用一个简化的光栅化器把候选 occluder 的几何投影到屏幕,生成一张低分辨率的遮挡缓冲,再让被遮挡的物体做 frustum + occlusion query。这是 2005-2015 年间 Unreal、Unity、Crytek 都重度依赖的优化,至今仍然是大世界场景剔除的主力。
2026 年的 software raster occlusion 有三个关键演进:
Apple Silicon 上的 software ROP 有独特优势:Apple 的 GPU 是 tile-based deferred renderer (TBDR),它在 hardware 层就把 framebuffer 存在 on-chip tile memory(典型 32MB),这个 tile memory 天然适合做 software ROP——你可以在 render pass 开始前把 occluder 渲染到一张 4MB 的 tile-sized depth buffer,用 hardware tile compression 压缩后存回 system memory,然后用这张 HiZ 做 culling。整个过程带宽极低(4MB × 60fps = 240MB/s,远低于 GPU 主带宽 200GB/s)。
HiZ Pyramid 的工程实现细节:HiZ 是一个多层 mipmap,每层都是上一层的 2x2 min(或 max for occlusion culling)。典型 8 层 HiZ 在 1080p 渲染目标上是 1920×1080 + 960×540 + 480×270 + ... + 15×9 ≈ 2.5MB。GPU 通过 hardware 自动生成 HiZ(RDNA 4 的 8 mip、Ada 的 8 mip、Apple M4 的 6 mip),CPU 端只需 SampleLevel 查询即可。
关键工程问题:HiZ 在跨帧复用时的精度:HiZ 是基于上一帧 depth buffer 生成的,意味着如果相机快速移动或场景快速变化,HiZ 可能"漏掉"应该被检测为可见的物体。两种应对策略:
// Meshlet Software Culling(Pseudo C++)
struct MeshletCullJob {
Meshlet meshlets[1024];
vec4 cullingPlanes[6]; // frustum
mat4 viewProj;
};
void RunMeshletCull(MeshletCullJob* job, int numJobs) {
parallel_for(0, numJobs) {
for (m : job->meshlets) {
vec3 center = m.bounds.center;
float radius = m.bounds.radius;
// Frustum cull (sphere)
if (FrustumCull(job->cullingPlanes, center, radius)) continue;
// HiZ cull
vec2 screenPos = WorldToScreen(center);
float hiZDepth = SampleHiZ(screenPos, m.lod);
if (hiZDepth > center.z + radius) continue;
// 写入可见 meshlet 列表
AppendVisibleMeshlet(m);
}
}
}
踩坑清单:
CPU 端的可见性剔除在 2026 年仍是不可省略的——GPU 端剔除的 throughput 高,但latency 也高(compute pass + readback),所以 CPU 端粗剔除必须把"10000 个候选 occluder"先压到"500 个候选",再让 GPU 端做精细剔除。典型的 CPU 端剔除栈是:
关键工程问题:CPU 剔除与 GPU 剔除的边界在哪里? 经验法则是:
20000 → 必须 GPU-driven (Visibility Buffer / Meshlet Cull)。
RDNA 4 的 Wave Matrix 与 Ada 的 Cooperative Groups 让"CPU 端做 quad-tree,GPU 端做 meshlet cull"的混合模式变得可行——CPU 端选出 5000 个 candidate,GPU 端在 0.2ms 内并行剔除到 500 个 visible。
Portal Culling 的现代实现细节:传统的 portal graph 是预计算的静态结构,但 2026 年的引擎开始用 dynamic portal graph——基于场景图运行时分析生成 portal 集合,允许场景破坏(门被打开、墙被破坏)时 portal graph 仍然正确。Epic 在 UE5 的 MegaLights 演示里就展示了 dynamic portal graph 与 Lumen 的集成。关键技巧是用 GPU compute shader 并行化 portal traversal——一个 cell 内的所有 portal 由一个 wave 处理,跨 cell 的 portal chain 通过 LDS (Local Data Share, AMD 术语) 共享。
HiZ occlusion 的常见误区:很多团队把上一帧的 depth buffer 直接当 occlusion mask 用,但这忽略了**"被遮挡的物体在上一帧也是被遮挡的"这种退化情况——如果一帧没有遮挡信息,CPU 端 HiZ occlusion 就会把所有物体都判为"可能可见",造成 overdraw 飙升。正确的做法是双缓冲 HiZ**(上一帧 HiZ + 当前帧 low-res Z),用当前帧的低精度 Z 作为 fallback,上一帧的 HiZ 作为主要 reference。
GPU-driven rendering pipeline 是 2018-2026 年间最大的渲染架构革新。其核心思想是把 draw call 的发起也从 CPU 移到 GPU:
CPU: 准备 scene graph + meshlet descriptor buffer
GPU: (1) Compute shader: cull meshlets → visible meshlet buffer
(2) Compute shader: build draw argument buffer (Indirect Args)
(3) Render: Indirect Draw (meshlet count × instance count)
(4) Optional: Visibility Buffer pass → material ID + primitive ID
(5) Deferred shading / Forward shading
NVIDIA Mesh Shader(自 Turing 起)+ RDNA 3 Meshlet Pipeline + Apple M4 上的 Metal Mesh Shader + Mali G720(部分支持)让这条路径在 2026 年的可用硬件非常广。唯一仍然不能用的主流 GPU 是 Mali-G610 之前的 bifrost 架构——这些 GPU 上 mesh shader 不支持,必须退回 indirect draw + compute shader culling。
Visibility Buffer 是 GPU-driven 渲染的"终极形态":把所有几何体的 ID(primitive ID + triangle ID + material ID)直接渲染到一张 ID buffer,而不是渲染到 G-Buffer。后续的 shading pass 用 texture fetch + interpolation 计算 shading。优点是几何 pass 完全不依赖材质复杂度,缺点是 shading 时需要一次额外的"fetch material attributes"开销。Nanite 的 Lumen 集成、Frostbite 的"Strata + Visibility Buffer"、id Tech 的新一代 renderer 都是这条路径。
代码层面,GPU-driven pipeline 的核心是多 GPU pass 之间的同步:
// Compute Shader: Meshlet Cull (RDNA 3 / Ada)
// group size = 64 meshlets per dispatch
[numthreads(64, 1, 1)]
void CSCullMeshlets(uint3 dtid : SV_DispatchThreadID) {
uint meshletID = dtid.x;
if (meshletID >= numMeshlets) return;
Meshlet m = meshlets[meshletID];
AABB bounds = m.worldBounds;
// Frustum cull on GPU (faster than readback)
if (FrustumCull(frustumPlanes, bounds)) return;
// HiZ cull (sample hardware HiZ via UAV)
float hiZ = HiZTexture.SampleLevel(linearSampler, screenUV, m.lod);
if (hiZ > bounds.maxZ + radius) return;
// Append to visible buffer
uint visibleIndex = VisibleBuffer.IncrementCounter();
VisibleMeshlets[visibleIndex] = meshletID;
}
踩坑:
Meshlet Sizing 的工程方法论:Meshlet 大小是 visibility system 的核心 tunable 参数。Unity、Bevy、Nanite 都给出了经验值,但不能盲目套用——meshlet 大小必须根据场景特性调整:
Meshlet 生成工具链:开源的有 meshoptimizer、DirectXMesh,商业的有 Simplygon、InstaLOD。Meshlet 的生成必须在离线阶段完成(运行时生成太慢),且必须包含 topology 信息(adjacent meshlet)用于 cluster LOD 的平滑过渡。
Wavefront 优化:meshlet 在 mesh shader 内的处理顺序与 wave 划分强相关。Unity 与 Nanite 都做了 wavefront reordering——按 visible meshlet 在屏幕空间的相邻性重新排序,提升 mesh shader 输出到 rasterizer 的 spatial locality,减少 rasterizer 的工作集。实测这个 reordering 能带来 5-10% 的 overall frame 提升。
Indirect Draw 的 Multi-draw Pattern:当 visible meshlet 数 > 65K(DX12)或 > 4M(Vulkan with VK_KHR_multi_draw_indirect)时,必须 split draw call。一个常见模式是:
Mesh Shader vs Geometry Shader 的代际差异:Geometry Shader 是 DX10 时代的扩展,已经被 mesh shader 全面取代。Mesh shader 的优势:(a) 没有 1024 vertex 的硬上限;(b) 可以输出到 LDS / group shared memory;(c) 与 compute shader 共享 group memory API。劣势:(a) 仅 Turing+ 与 RDNA 3+ 支持;(b) driver maturity 不如 vertex/fragment shader。
GPU-Driven 的内存预算:GPU-driven 流水线需要额外的几个 buffer:
把前六节综合起来,2026 年的可见性系统应该被理解为**"一个时空一致的 occlusion field"**:
核心 insight:可见性的 维度(稳定性)是被低估的优化杠杆。一个 4ms 的 GTAO,如果配上 0.5ms 的 TAA 复用,可以在视觉上达到 8ms GTAO 的稳定性——这就是为什么 GTAO 的"贵"在大多数项目里是值得的。类似地,Nanite 的 software culling 之所以被接受,不是因为它"快",而是因为它提供的 per-frame visibility stability 让 pop-free LOD 切换成为可能。
统一的优化目标:
Meshlet-level culling + GTAO + TSR 是 2026 年"画质/性能平衡点"的最优解。Nanite-style 完整 visibility buffer pipeline 是"画质天花板",HBAO+ + frustum culling 是"性能地板",两者之间就是工程取舍的空间。
Temporal Occlusion Reuse 的工程细节:把 AO 信号视为一个可以在时间维度上累积的标量场,工程上有三种主流策略:
工程上最常见的是第一种 + 第三种的组合——motion vector 投影 + history clipping,两者的开销都在 0.1-0.3ms,且对大多数场景的 temporal stability 提升已经足够。
Spatial Occlusion Reuse 的工程细节:与 temporal reuse 对偶的还有 spatial reuse——同一帧内,相邻像素的 AO 通常相近,可以做 bilateral filter 进一步去噪。bilateral filter 的权重通常基于 depth + normal 的差,差越大权重越低,避免跨边缘的 AO bleed。Nanite 在 visibility buffer shading pass 里就用了 bilateral AO filter,显著降低了 GTAO 的 sample count 需求(从 8 sample 降到 4 sample,视觉等价)。
如果团队要从零搭建一个跨平台的可见性系统,以下是 2026 年的推荐 6 步流程:
最终目标是:让 visibility system 从"渲染管线的子模块"升级为"管线的骨架"。2026 年的实时渲染不再是"draw 一切,然后遮挡计算遮掉",而是"先精确知道哪些像素可见,再决定要不要算 shading"。这是 GDC talk 不会明说、引擎官方文档不会明写,但所有一线工作室都在做的工程转型。
一句话摘要:屏幕空间遮挡与可见性剔除正在从"渲染管线的子模块"演化到"管线的骨架",2026 年的最优解是"GTAO + HiZ + GPU-Driven Meshlet Cull + Visibility Buffer"的组合,关键约束是跨硬件 tier 的时空稳定性预算。
(no reference document available)
(no reference document available)
Conversation
0 条