GPU Gems 1 · Chapter 13. Generating Soft Shadows Using Occlusion Interval Maps
把静态场景沿固定光轨迹的离线可见性压缩成 rise/fall 区间,在运行时对线性光源窗口求平均,得到实时软阴影。
学习目标
- 能解释点光源硬阴影与有半影的软阴影之间的视觉差异,并说明为什么 Sun 可近似为沿轨迹移动的线性光源
- 能为静态场景构建每个接收点的 rise/fall 可见性区间,并理解它们如何被打包进纹理
- 能用区间交叠和窗口宽度推导运行时的软阴影可见比例,并读懂四路并行的 shader 伪代码
- 能识别该方法的适用边界:唯一 UV、固定光轨迹、离线预计算,以及禁用压缩和过滤的纹理约束
实时软阴影最难的部分不是把边缘“涂得模糊”,而是判断一个接收点在光源面积的多少位置上可见。GPU Gems 1 第 13 章的策略是收窄问题:场景不动,光源只沿一条预定轨迹移动;昂贵的可见性先离线算好,运行时只查表和做区间运算。
为什么要把可见性压缩成区间
↡具有从完全遮挡到完全受光的连续过渡、由面积光源部分可见性产生的阴影 的关键视觉信号是 penumbra:它让用户从阴影边界感知遮挡物与接收面的空间关系。点光源、shadow map 或 stencil volume 很容易给出硬边,但太阳这类非零面积光源需要计算一段光源位置的平均可见性。
把光源看成沿固定路径移动的点光源时,某个表面点 p 的可见性可以写成 V(p, t),其中 t 是轨迹参数。V 只有 0 和 1:0 表示从该位置看 p 被遮挡,1 表示 p 被照亮。若直接把 x、y、t 存成 3D 纹理,空间和时间都会造成昂贵的存储;更有效的表示是保存 0/1 信号的边界。
↡在纹理中保存光源轨迹上可见与遮挡起止边界的预计算可见性表示 用一组 rise 和 fall 表示可见性变化:进入遮挡区间的边是 rise,离开遮挡区间的边是 fall。对每个 UV 像素,若有两段遮挡,就保存两对起止参数,而不是保存整条时间采样。
↡用一条固定路径上的连续位置近似面积光源,并在窗口内平均可见性的光源模型 把真实的面积光源降成一维问题。这个近似在光源宽度适中时能产生很自然的软边,但沿轨迹方向和垂直方向的平滑程度可能不同。
1. 先锁定场景与光源的可变范围
↡在离线阶段收集可见性、运行时只查询预计算结果的渲染策略 适合静态户外环境:几何体不变,灯光可以在已知的日照路径上变化。它不适合任意动态对象,因为对象一动,已经写入纹理的遮挡区间就不再描述当前几何关系。
这也是本章最重要的工程判断:先问“哪些变量允许变化”,再决定是否值得预计算。章节中的 Last Chance Gas demo 用该方法表现从日出到日落的动态照明;它并没有声称对任意灯光和任意对象都成立。
实现前应建立三条不变量:
- 几何不变量:静态 caster 与 receiver 的位置保持不变;角色或可动门窗不能直接复用旧区间图。
- 轨迹不变量:光源只能出现在预定的单一路径上,t 的范围和方向必须与离线生成阶段一致。
- 参数化不变量:区间图像素依赖位置,所以对象的 UV 不能重叠;使用 tiled texture 的对象需要额外的一套唯一 UV。
2. 离线生成 rise/fall map
对区间图的每个像素,在光源轨迹上取均匀分布的位置,并从该像素向每个位置发射一条射线。沿 t 扫描遮挡状态时,0→1 与 1→0 的变化就是一组边界。章节中用 8 bit 存储边界,因此 256 条光线足以完整捕获量化后的区间;背向光源的像素可以跳过,因为它们本来就不会贡献受光。
一种可读的离线数据结构如下:
for each receiver texel p:
visibility = trace_visibility(p, lightTrajectory, 256 samples)
rises, falls = detect_edges(visibility)
write_four_channels(riseTexture, fallsTexture, rises, falls)实际追踪可以由可编程 ray tracer 或离线 shader 完成。成本可能是每个像素数百条射线和数小时的整场景计算;这正是它把运行时工作换成离线工作的地方。
两套四通道颜色纹理的含义是:riseTexture 的每个通道保存一个区间开始,匹配的 fallTexture 保存区间结束。若区间数超过四个,增加纹理组并在运行时累加结果。纹理分辨率可以约为颜色纹理的一半;软阴影的平滑效果会掩盖一部分低分辨率,但仍需用目标场景验证伪影。
3. 运行时用窗口平均得到软阴影
当前光源位置是 t,线性光源在轨迹上覆盖窗口 [t - dt/2, t + dt/2]。阴影不是查一个点,而是把窗口和遮挡区间做交叠,计算窗口内有多少长度保持可见。用 R_i 和 F_i 表示第 i 对 rise/fall,区间长度可以通过 min、max 和 subtraction 得到:
窗口越宽,等价的线性光源越长,半影通常越宽;窗口越窄,结果逐渐接近轨迹上某个点光源的硬阴影。对所有区间求和后要做 saturate,避免不相交区间贡献负值。
章节中的 shader 关键在于把四个区间塞进 half4,让一次向量运算并行完成四组 min/max:
half softShadow(sampler2D riseTex, sampler2D fallTex, float2 uv,
half intervalStart, half intervalEnd, half invWidth)
{
half4 rise = tex2D(riseTex, uv);
half4 fall = tex2D(fallTex, uv);
half4 overlap = saturate(min(fall, intervalEnd) - max(rise, intervalStart));
return dot(overlap, invWidth);
}这里的 dot 不是为了做几何方向点积,而是把四个并行区间的可见长度相加并除以窗口宽度。intervalStart = t - dt/2、intervalEnd = t + dt/2,invWidth = 1/dt;若有多组纹理,就逐组调用并累加。
4. 纹理格式与质量预算
区间图的边界是数据本身,不是可供平滑采样的颜色。压缩会损失每通道的 8 bit 精度,双线性过滤会在不连续的 rise/fall 之间制造假的中间边界,因此实现应关闭两者。这样做会牺牲一部分纹理便利性,但软阴影本身的平滑会缓解低分辨率带来的块状感。
另一个近似是用线性光源代替真正的面积光源。沿光轨迹方向的阴影会更平滑,垂直方向可能更硬;光源越大越容易看到这个各向异性。对章节目标中的 Sun 宽度,它通常是可接受的折中。
5. 动手实验:把一个接收点变成可解释的曲线
先预测:把 linear-light window dt 从窄拉宽,阴影的可见比例和半影宽度各会怎样变化?再把 stored interval pairs 从 2 降到 1,观察第二个遮挡时段如何消失。最后降低 map resolution,区分“区间数据少了”与“阴影窗口变宽了”这两种完全不同的原因。
Occlusion Interval Lab
先猜:把光源窗口拉宽,接收点的可见比例会怎样?
观察:窗口更宽时半影更宽;分辨率更低时区间图更省显存,但边界更依赖软化。可见比例是窗口内未被区间覆盖的部分。
实验中,橙色轨迹是固定的 light path,橙色窗口是当前线性光源覆盖的参数范围,紫红色短线只是接收面上的阴影宽度提示。visible 是窗口内未被区间覆盖的比例;它直接对应运行时 shader 最终要输出的 visibility coefficient。
小结:用受限的可见性换取实时软阴影
- 遮挡区间映射把静态场景中随 t 变化的 0/1 可见性压缩成 rise/fall 边界。
- 线性光源是固定轨迹上的一段窗口;软阴影可由窗口与可见区间的平均得到,而不是后处理硬边。
- 两套四通道纹理让四个区间并行参与 min/max/subtract/dot;更多区间需要额外纹理组。
- 256 条离线射线、半分辨率和禁用压缩/过滤分别对应精度、显存与采样规则的取舍。
- 适用边界必须写进系统设计:静态几何、单一固定光轨迹、唯一 UV,以及可接受的线性光源近似。
练习
问题 1|推导题 某接收点在 t=0.20–0.35 与 t=0.60–0.75 被遮挡。当前窗口是 [0.25, 0.65],窗口宽度 dt=0.40。按区间交叠计算 V_soft,并说明它对应的半影亮度趋势。
问题 2|改 Demo 代码 将离线采样从 256 个轨迹位置改成 128 个,并把区间图改为颜色纹理的 1/4 分辨率。你会检查哪些证据来判断结果仍可接受?
问题 3|独立实现题 为一张静态户外地图设计遮挡区间图的生产流水线。请列出一条会使预计算失效的运行时需求,并给出替代方案。
名词解释
本章出现的专业名词,用大白话再讲一遍。
- soft shadow
- 由非零面积光源产生、在完全受光与完全遮挡之间具有半影过渡的阴影。
- occlusion interval map
- 按接收点位置保存光源轨迹上可见性起止边界的预计算纹理。
- linear light
- 沿固定路径的一段线性光源,用窗口平均近似面积光源。
- precomputed visibility
- 把静态场景的可见性离线计算并在运行时查询的策略。
- rise/fall edge
- 可见性信号进入或离开遮挡状态时的边界参数。
- penumbra
- 面积光源只有部分可见时形成的半影区域。