GPU Gems 1 · Chapter 15. Managing Visibility for Per-Pixel Lighting
用 camera、light 与 shadow visibility sets 剪枝 per-pixel lighting 的 batch 和 fill-rate 成本,并用 scissor 限制真正受光的屏幕区域。
学习目标
- 能用 batch、pass、CPU 提交率解释 per-pixel lighting 为什么会把渲染成本乘上灯光数量
- 能构造 camera visible set、light set、illumination set 与 shadow set,并说明它们为什么不能互相替代
- 能为视锥内外的光源选择 F∩L 或 light-frustum convex hull 的 shadow candidates
- 能把可见对象投影到屏幕,生成 tight scissor rectangle,并通过集合与 fill-rate 指标验证剪枝收益
per-pixel lighting 让每个光源都能贡献更细腻的材质和阴影,但它也会把同一批几何重复送入硬件。GPU Gems 1 第 15 章的出发点很直接:GPU 内部处理一个 batch 越来越快,CPU 把 batch 提交给 GPU 的速率却没有同样增长;最快的 polygon,仍然是根本不提交的 polygon。
为什么 per-pixel lighting 需要 visibility
↡一组不被状态切换打断、一次送入图形硬件的多边形集合 是本章的计量单位;Direct3D 的一次 DrawPrimitive 可以看作一个 batch。场景的初始 ambient pass 要建立深度并处理全局 ambient/emissive,之后每盏灯通常还要先做 shadow pass,再做 lighting pass。
如果一个房间和三个模型被材质、骨骼拆成 14 个 batch,三个光源的朴素成本就是:
这不是 shader 指令数的问题,而是 CPU 提交与重复 pass 的乘法。即使一个 GPU 可以很快处理多边形,应用线程仍可能先被 batch submission 限制。
↡由相机视点能看见的对象构成的对象集合,记作 V 是所有裁剪和遮挡测试的起点。但 per-pixel lighting 还需要从每盏灯的视点构造另一套集合;如果只使用 V,无法知道灯是否能看见对象,也无法知道屏幕外 caster 是否能投影进画面。
1. 用集合逻辑拆开三种工作
↡从当前光源视点能看见、可能参与该光源渲染的对象集合,记作 L 对每盏灯单独计算。点光源是球形视野,不能简单投影到一个平面;可以把光源包在 cube 中,对六个 face 做 visibility query,但要去重同一对象被多个 face 看见的情况。
↡visible set 与 lights set 的交集,只有其中对象需要再次执行当前光源的 lighting pass,记作 I 满足 I = V ∩ L。它只保留同时出现在屏幕和光源视野中的对象;若 I 为空,可以直接跳过这盏灯。它特别适合剪掉位于视锥边缘、被遮挡或光源实际上碰不到的灯。
↡会把阴影投进可见区域的 caster 集合,必须包含必要的屏幕外对象,记作 S 与 I 不同:对象即使不在 V 中,也可能在 S 中。shadow pass 的判断是“从光源挤出的阴影体积是否与可见区域相交”,而不是“对象自身是否出现在镜头里”。
一个稳妥的 per-light 顺序是:先得到 V,再为当前灯得到 L;用 V∩L 生成 I;再根据光源位置和投影体积生成 S。最后以 S 生成 shadowing term,以 I 重新渲染 lighting contribution。
V = visible_from_camera()
L = visible_from_light(light)
I = intersection(V, L)
S = shadow_casters_into_view(light, V, L)
render_ambient(V)
render_shadow(S, light)
render_lighting(I, light)2. 生成 shadow set:光源在不在视锥内
当光源在 view frustum 内时,阴影总是从光源位置向外投射。此时,视锥内但被遮挡的对象也不能从 F 中删除;令 F 表示视锥内所有对象,shadow set 可以使用 S = F ∩ L。如果错误地使用 V,可能丢掉被另一个对象挡住、却仍然能把阴影投向可见表面的 caster。
当光源在视锥外,屏幕外的 caster 可能沿光线投影进视锥。此时构造由光源点和 view frustum 形成的 ↡包住光源与视锥投影区域、用于判断外部 caster 是否可能投影进画面的凸体积:保留光源位于 inside half-space 的原始平面,并为从光源看见的 silhouette edge 创建经过光源的平面。落入 hull 且属于 L 的对象才是 shadow candidates。
凸包算法可以用每个 frustum plane 对 light 的 inside/outside 判断开始:inside plane 直接加入结果;outside plane 则为它涉及的 edge 增加计数。某条 edge 的计数为 1 时,它是相对于 light 的 silhouette edge,需要用 edge 两端点和 light 生成一个新平面,并根据 winding 修正法向。
edgeCount = zeroes(frustum.edges)
for plane in frustum.planes:
if lightInside(plane): add(plane)
else: for edge in plane.edges: edgeCount[edge] += 1
for edge in frustum.edges:
if edgeCount[edge] == 1:
addPlane(through=edge.points + light, winding=edge.winding)这个计算不是替代所有 visibility 系统的通用实现,而是把已有 frustum、portal 或 occlusion 结果接到 per-pixel lighting 的集合语义上。集合生成的 CPU 成本必须和省下的 batch、shadow fill 一起评估。
3. 用集合缩小 fill rate
CPU batch 不是唯一瓶颈。stencil volume 或大型光照几何会让 GPU 重复填充屏幕;即使对象数下降,如果每盏灯仍覆盖整个 viewport,fill rate 也可能先饱和。
↡限制光源 pass 只能写入屏幕上受影响对象的紧包围矩形,也可扩展为深度范围的空间 scissor 应由实际受影响对象生成:把 I 中每个 primitive 的 bounding sphere 或 AABB 投影到屏幕,求总 bounding box,再与光源半径投影出的 box 相交。对象粗包围体通常足够,因为为了多挤出一点像素而计算精确几何,收益很小。
对 point light,sphere 视野可能需要六个方向的 visibility query;对大半径光源,仍应检查最终 I 的投影范围。若硬件支持 z-scissor,可以进一步限制深度范围,形成空间中的 scissor frustum。其目标都是让“不可能被这盏灯影响”的片元尽早被测试拒绝。
4. 集合数据结构与更新策略
集合可以有两种实现:集合持有对象引用,或对象持有自己属于哪些集合的 flags。前者需要排序列表后做线性 merge,适合本来就有排序集合的系统;后者可以先遍历对象设置 V/L 标志,再扫描轻量对象列表得到 I,代码更直接,但操作结束必须 reset flags。
对每盏灯不必同时把所有 L 保存在内存中:按灯依次计算并累加 lighting buffer 即可。对 portal visibility 之类已经同时服务相机与灯的系统,可以共享空间划分;对 point light 的球形视野,则需要额外的无平面投影方案或六面 cube 查询。
工程上建议把每帧的证据分成四组:V 的对象数与 occlusion 命中率、L 的对象数与查询成本、I/S 的 batch 数、scissor 前后的屏幕像素面积。这样才知道收益来自 CPU 剪枝、GPU fill-rate,还是只是改变了对象的提交顺序。
5. 动手实验:让 I 和 S 各自承担正确的工作
先预测:把 camera visible V 调小,I 会怎样变化?此时如果 light 在视锥外,S 是否一定同步变小?关闭 tight scissor 后,batch 数可能不变,但屏幕影响面积会发生什么?
Visibility Set Lab
先猜:为什么 I 小了,S 不能直接跟着变小?
观察:I 只决定 lighting pass;S 还要覆盖屏幕外 caster。减少 I 可降低 batch,scissor 则进一步降低 fill rate。
实验里的绿色对象属于 I,紫红色斜线对象属于 S;黄色表示光源可见但不一定在屏幕上。切换 light 是否在 frustum 内,会把 shadow-set 逻辑从 F ∩ L 切到 hull 近似;scissor 只改变屏幕影响面积,不改变 I/S 的集合计数。
小结:先求集合,再决定渲染
- per-pixel lighting 把 ambient、shadow 和 lighting pass 乘到每个光源上,batch submission 与 fill rate 都会成为瓶颈。
- V 表示相机可见对象,L 表示当前光源可见对象,I=V∩L 只服务 lighting pass。
- S 必须独立计算:光源在视锥内使用 F∩L,光源在视锥外需要 light 与 frustum 的 convex hull 来捕获屏幕外 caster。
- 对 I 中的对象做屏幕投影并求 tight scissor,可以减少真正被光源 pass 填充的区域;它不是用光源半径生成的粗 box。
- 集合生成成本、batch 数、scissor 面积和 fill-rate 需要一起测量,不能只看平均 FPS。
练习
问题 1|集合推导 当前相机可见集合 V 有 12 个对象,光源集合 L 有 9 个对象,其中交集 I 有 5 个。光源在视锥外,light-frustum hull 与 L 相交得到 7 个 shadow candidates。当前灯的 shadow 和 lighting 各执行一次,ambient 已完成 14 个 batch。额外需要多少 batch?
问题 2|改 Demo 代码 把实验中的 tight scissor 关闭,保持 V、L、I、S 不变。请分别记录 batch 数、屏幕影响面积和 GPU fill-rate,说明为什么前两者可能不变。
问题 3|独立实现题 一个点光源在视锥外,房间里有 portal visibility 系统但它只支持平面投影。你会如何构造 L 与 S,并如何避免同一对象因 cube 的多个 face 被重复加入?
名词解释
本章出现的专业名词,用大白话再讲一遍。
- batch
- 一次不被状态切换打断、送入图形硬件的多边形集合。
- visible set
- 从相机视点可见的对象集合 V。
- lights set
- 从当前光源视点可见、可能参与该灯渲染的对象集合 L。
- illumination set
- V 与 L 的交集 I,只用于当前光源的 lighting pass。
- shadow set
- 会把阴影投进可见区域的 caster 集合 S。
- convex hull
- 包住光源与视锥投影区域、用于筛选外部 shadow caster 的凸体积。
- scissor rectangle
- 限制光源 pass 屏幕写入区域的紧包围矩形。