GPU Gems 1 · Chapter 29. Efficient Occlusion Culling

用 occlusion query 和 early-z 在正确粒度拒绝不可见几何,结合上一帧结果、前后排序和 bounding box 控制 GPU/CPU 成本。

学习目标

  • 能区分 frustum culling、occlusion query 和 early-z rejection 的粒度与代价
  • 能实现不修改 color/depth buffer 的 bounding-box query,并在 visible pixels 超过阈值时绘制完整对象
  • 能用多 outstanding queries 或上一帧结果避免同帧读取造成的 CPU/GPU pipeline flush
  • 能根据对象复杂度、遮挡率、排序、分辨率和 CPU overhead 判断 occlusion culling 是否值得

遮挡剔除的收益来自“不画不可见的东西”,但 query 本身也需要提交、栅格化和读取结果。GPU Gems 1 第 29 章因此把它当成一个时序和成本问题:在正确粒度测试可见性,用廉价代理替换复杂对象,再把结果读取延迟到 GPU 有时间完成之后。

要先回答“在哪一层拒绝”:frustum 是对象级 CPU 测试,query 是几何级 GPU 判断,early-z 是 fragment 级拒绝。不同层级不能互相替代。

visibility levels:越早拒绝,节省的工作越多frustum cullingCPU / object testout of view → skip drawocclusion queryGPU / object testhidden object → skip geometryearly-zrasterizerhidden fragment → skip shaderfront-to-back order helps query and early-z share depth information
三种判断处于不同粒度:不要用 occlusion query 代替便宜的 frustum test,也不要把 early-z 当成已经避免了对象提交。

1. 三种可见性判断的边界

occlusion query 让 GPU 返回对象最终能通过 frustum、scissor、alpha、stencil 和 depth 等测试的可见像素数;如果对象代表零像素或低于阈值,就可以跳过完整几何。early-z 则在 rasterizer 中比较 fragment 深度,失败时不再 fetch texture 或执行 fragment program,从 per-fragment 层节省带宽。

需要应用参与状态设置和结果决策; 多数情况下由 GPU 自动执行,但仍受前后排序、stencil 使用和渲染状态影响。两者都受益于 front-to-back order,但工作粒度和控制权不同。

2. 用 bounding box 做不写入缓冲的 query

把复杂模型替换成少量三角形。规范步骤是:创建 query,关闭 color mask,关闭 depth write,开始计数,渲染 bbox 做 depth test,结束 query,恢复渲染状态并读取 visible pixels;结果大于零或阈值时才提交完整对象。

occlusion query:用廉价 bbox 代理复杂对象createQquery handlemaskno color / depth writebegin0reset counterbboxdepth test onlyend128visible pixelsdrawif pixels > 0state contract:query 期间只测,不写屏幕、不写 depth阈值可大于 0:用可见像素数量决定是否值得绘制完整对象
bounding box 只是探针,测试不能改变 color 或 depth buffer;结果大于零或阈值时,才提交昂贵的完整对象。
beginQuery(query);
setColorMask(false, false, false, false);
setDepthWrite(false);
drawBoundingBox(object.bounds);
endQuery(query);
 
setColorMask(true, true, true, true);
setDepthWrite(true);
if (queryResult(query) > visiblePixelThreshold) {
  drawFullObject(object);
}

query 期间的状态恢复是资源契约的一部分。若测试 bbox 写了 depth,代理就会成为后续对象的遮挡者;若 color 没有关闭,测试还会污染最终画面。实现中还要保存原始 depth write、color mask、cull mode 和 stencil 状态,而不是假设所有调用者都使用默认值。

3. 处理 CPU/GPU 解耦:不要让结果读取变成栅栏

正常渲染中 CPU 把命令放入 GPU queue 后可以继续工作;读取 query result 则要求 GPU 已完成 query 之前的命令,天真实现会把 CPU 卡在等待点。即便大多数对象都被遮挡,若每个 query 都同步读取,节省的完整对象绘制也可能抵不过 pipeline flush。

async query:用时间换掉 pipeline flushnaive same-frame readCPUGPUget result → CPU waitsnext-frame readCPUGPUAI / physics fill the gapvisible last frame → draw object · invisible → test bbox
查询的收益来自跳过复杂对象,但同步读取结果会破坏 CPU/GPU 并行;把结果延迟到下一帧是关键工程折中。

更好的策略是创建多个 outstanding queries,先发出一批 bbox 测试,再稍后读取结果;更稳妥的游戏循环则在下一帧读取上一帧结果。上一帧可见对象本帧先按完整对象绘制并继续发 query;上一帧不可见对象本帧只发 bbox query。不可见对象需要每帧重新测试,避免它刚刚进入视野却仍被错误跳过;可见对象的可见性检查可以降低频率,但要接受一帧延迟和轻微 popping 风险。

for (Object& object : scene) {
  const bool wasVisible = object.previousQueryVisible;
  beginQuery(object.query);
  if (wasVisible) {
    drawFullObject(object);
  } else {
    disableColorAndDepthWrites();
    drawBoundingBox(object.bounds);
  }
  endQuery(object.query);
}
 
for (Object& object : scene) {
  object.previousQueryVisible = readResultFromPreviousFrame(object.query);
}

4. 让 opaque 成为 occluder,再处理 occludee

不是必须预先列出的特殊对象:在改进的排序策略中,任何已经渲染且可见的 opaque 对象都能成为后续对象的 occluder。 则在已有深度信息后进行测试。

sort and bound:让深度结果可复用opaque · front → backoccluderobj 2obj 3obj 4translucent · back → frontoccludee onlytight bounding boxless false visible · lower query costoversized boxbox visible, object hidden → popping
排序让已渲染的 opaque 几何成为后续对象的遮挡者;包围盒必须在测试成本、紧致度和动画更新成本间折中。

opaque objects 应按距离 front-to-back 排序,先写入 depth;translucent objects 不能写入可复用的深度,因此只能作为 occludee,并按 back-to-front 绘制。这样 query 得到的深度环境更有用,许多远处或小物体可以在完整 shader 之前被拒绝。

包围盒的松紧也会影响正确性:盒子太大时可能可见而对象本身隐藏,下一帧就会在“画对象/画 bbox”间来回切换;盒子太复杂时,测试成本又接近直接绘制。静态对象可用紧凑盒子或多盒子,动画对象可存每帧、每动画或动画 union 的 bounds,再用 CPU 更新成本和 query 节省做对照。

5. 识别 query 不值得的场景

query 不是对所有对象都划算。若对象只有少量三角形,CPU 准备 bbox、设置状态和发 query 可能比直接画完整对象更贵;如果对象 fragment shader 极其复杂且占据较多像素,bbox depth-only 测试即使有额外像素也可能很便宜,query 才更有价值。

高分辨率下,简单 shader 的对象更容易变成 fill-rate limited;这时整个 bbox 可能覆盖很多像素,测试比直接绘制对象更贵。观察者进入 bbox 内部时,还必须考虑 back faces,否则 back-face culling 可能让 bbox 没有像素通过并产生错误“不可见”。距离观察者太近的模型通常应跳过 query。

Efficient Occlusion Culling Lab

先预测:遮挡率和对象复杂度要多高,query 才值得?

可交互
query economics previewasync / next framecomplexity 0.72 · occluded 0.68 · visible pixels 0.32skipped geometry 0.45 · query cost 0.28net gain 0.17 · 边界场景:和直接绘制对测

预测:复杂度和遮挡比例都低时,query 可能比直接绘制更贵;同帧读取还会增加 CPU/GPU stall。

交互实验用对象复杂度、遮挡比例、查询时序和分辨率表达成本方向:复杂度和遮挡率都高,跳过的几何才可能覆盖 query 成本;同帧读取会额外增加 stall;高分辨率会抬高 bbox 的 per-pixel 成本。数值是可解释的示意,不替代目标 GPU 的 timestamp、query latency 和 frame-time 数据。

6. lens flare 是一个可验证的应用

lens flare 不是部分透明地显示的物体,而是相机光学效果,通常要在已渲染场景上整体叠加。旧做法读取 depth buffer 会强制 GPU 完成队列;query 方案可以在光源位置和深度处测试一个像素,下一帧读取结果判断 flare 是否可见。

如果只测试一个像素,效果是 yes/no,容易 popping。可以根据 flare 屏幕尺寸测试一个小 block,例如 16×16,再用通过的像素比例调制亮度,得到更平滑的 fade。这个应用再次说明:延迟一个 frame 通常比同步读回更值得,query 结果还可以同时表达可见程度而不触碰 depth buffer。

小结:用代理、排序和延迟换取可见性收益

  • occlusion culling 的层级要和成本匹配:先 frustum,再 query 或 early-z;不要用昂贵 query 做便宜 CPU 判断。
  • occlusion query 用不写 color/depth 的 bounding box 统计 visible pixels,超过阈值才绘制完整对象。
  • 多 outstanding queries 或上一帧读取避免 pipeline flush;不可见对象要及时复测,可见对象可以接受一帧延迟。
  • opaque front-to-back 几何能成为 occluder,translucent 几何主要是 occludee;bounds 太松会产生 popping,太复杂会吃掉收益。
  • 对象复杂度、遮挡率、CPU submit、分辨率、query latency 和画质都要进入实机回归;lens flare 是 query 的典型应用。

练习

问题 1|状态题 写出一次安全的 bounding-box occlusion query 必须关闭和恢复的状态,并说明为什么不能让 bbox 更新 depth。

问题 2|时序题 为什么“同帧发 query、同帧立即读 result”可能比直接绘制更慢?给出一种降低 stall 的改法。

问题 3|收益判断 一个只有 200 个三角形、fragment shader 很简单的对象和一个 2000 个三角形、fragment shader 很复杂的对象,哪个更适合 query?还要观察哪些变量?

名词解释

本章出现的专业名词,用大白话再讲一遍。

occlusion culling
通过不渲染视锥外或被更近几何遮挡的对象来减少渲染工作的策略。
occlusion query
统计几何代理通过可见性测试的像素数量、供应用决定是否绘制完整对象的查询机制。
early-z rejection
在光栅化阶段先做 depth test、失败 fragment 不再执行 shader 的硬件路径。
bounding box
近似复杂对象空间范围、用于 occlusion query 的简单几何代理。
occluder
已写入 depth buffer、可以遮挡后续对象的 opaque 几何。
occludee
可能被更近几何遮挡、适合接受 occlusion query 的对象。

资料与写作方式声明

本章以GPU Gems 系列权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…