GPU Gems 3 · Chapter 10. Parallel-Split Shadow Maps on Programmable GPUs
从 shadow-map aliasing 出发,解释如何用平行深度分层、practical split、crop matrix 与可编程 GPU 路径,为大场景分配更合适的阴影采样密度。
学习目标
- 能解释为什么相机近处与远处需要不同的 shadow-map sampling density,以及平行深度分层如何减少 perspective aliasing
- 能修改 PSSM Design Lab 的 split 数量、practical split 权重、light projection 与硬件路径,观察画质和 rendering passes 的变化
- 能回答:什么时候选择 multipass、DX9 单 pass 合成或 DX10 texture array,并说明 split、crop matrix 与 caster 范围如何共同决定结果
先让影子纹理匹配观察距离
想象一张地图覆盖整片城市:如果地图的每一格在市中心代表一米,在郊外也仍代表一米,市中心的细节会被挤成模糊的色块,郊外却浪费很多空白。相机观察阴影时也有同样的问题:近处表面在屏幕上占很多像素,远处表面只占很少像素,但一张统一的深度纹理很难同时照顾两者。
本章解决的问题是:怎样把固定数量的 shadow-map texels 分给一条很长的视线范围,让近处的锯齿减少,又不让每个深度层的额外渲染成本失控。如果只把整条范围塞进一张图,阴影边缘会随着距离出现明显的采样失配;如果盲目复制很多张图,画质可能变好,帧时间却先坏掉。
1. 一张 shadow map 为什么会把分辨率用错
↡把相机视锥沿视空间深度切成多个层,并为每层生成独立 shadow map 的阴影技术;每层只负责较小的深度范围。标准 shadow mapping 的困难不是深度比较本身,而是屏幕像素与光空间 texel 的覆盖面积不匹配。近处的一个 texel 可能投到很长的屏幕表面,远处则可能有很多 texel 竞争很少的像素;这类 perspective aliasing 会让阴影边缘闪烁、锯齿或忽软忽硬。表面法线和光线方向还会带来 projection aliasing,但它需要场景几何信息,不能只靠分层消失。
parallel-split shadow map 用平行于 view plane 的 clip planes 把 view frustum 分成 V₀ ... Vₘ₋₁。每个 split 都计算自己的 light view-projection matrix,并以相同或可调的分辨率生成 Tᵢ。因为每张图只覆盖一段深度,view-space 的采样需求可以更接近 texture-space 的采样频率。
这里的关键不是“层数越多越好”。层数增加会减少每层覆盖范围,但也会增加 shadow-map 生成、绑定或 shader 选择的管理成本;PSSM 的设计目标是把误差和 pass 数放进同一个预算,而不是追求一个孤立的低 aliasing 指标。
2. practical split scheme:在近处与远处之间取平衡
↡把 logarithmic split 与 uniform split 的同一层位置按权重混合的分层策略,用较低的计算复杂度近似更均匀的 perspective-aliasing 分布。uniform split 沿相机的近远距离等分,简单但会继续让近处欠采样。logarithmic split 更接近让 dz / (z ds) 保持稳定的理论目标,却可能把最靠近相机的层压得过短,导致里面几乎没有物体、分辨率反而被浪费。practical split 用权重 λ 混合两组位置:Cᵢ = λCᵢˡᵒᵍ + (1−λ)Cᵢᵘⁿⁱ,让应用可以在两种偏差之间移动。
在切分前先收紧 camera near/far plane 也很重要。空的深度范围会让每个 split 都把 shadow-map texel 花在不可见空间;收紧后再调 λ,才是在有效可见物体上分配分辨率。λ 不是一个跨镜头永远正确的常数:远近物体分布、相机速度和阴影质量目标都会改变它的合适位置。
3. 每个 split 都需要自己的 light projection
↡把一个 split 的 light-space bounding values 映射到更紧的 off-center 投影范围的变换;它让有效 casters 和 receivers 占据更多 shadow-map texels。分好 view frustum 以后,还要为每个 Vᵢ 建立光源的子视锥 Wᵢ。最简单的 scene-independent 方法只在 light space 中包住 split frustum,逻辑稳定,却可能包含大量空白。scene-dependent 方法还找出可能投影到该 split 的 shadow casters 和 receivers,再把 light frustum 收紧。
常见的组合顺序可以写成 lightViewMatrix · lightProjMatrix · cropMatrixᵢ。crop matrix 是一个 off-center orthographic projection:它不改变光源的观察方向,只把当前 split 的有效边界缩放、平移到 shadow map 的可用区域。scene-dependent projection 提升了 texel 利用率,但也要求越界采样使用合适的 border color,并且不能把可能挡光的 caster 裁掉。
4. 生成与合成:fragment 先决定自己属于哪一层
PSSM 的四个主步骤可以保持不变:切分 view frustum;为每个 split 计算 light transform;生成各自的 shadow map;在场景中合成阴影。实现差异主要出现在第三和第四步:shadow map 是逐层重复生成,还是由 GPU 一次提交复制到多个目标;场景阴影是每层单独绘制,还是由 shader 按 fragment 的 view-space z 选择地图。
在 DX9-level 的单 pass 合成里,fragment 不应该用 light-space depth 来决定 split,因为 split planes 是在 view space 中定义的。顶点阶段传出 view-space z 和每张地图的 texture coordinate,pixel shader 用 z 与 split end 比较,找到唯一的 i 后只采样 Tᵢ。这让场景 shadow synthesis 从每层一次变成一张场景 pass,但同时要付出多组 sampler 与 texture coordinate 的资源限制。
float zView = fragment.viewSpaceZ;
int i = firstSplitContaining(zView, splitEnds);
float shadowDepth = sample(T[i], fragment.shadowUv[i]);
float lit = depthTest(fragment.lightDepth[i], shadowDepth);
return applyShadow(lit);这段伪代码只描述合成阶段的选择契约。multipass 仍然是最容易验证的基线;单 pass 路径减少了合成重复,却不能自动减少 shadow-map 生成的成本。
5. texture array 与可编程 GPU 的复制位置
↡把多个同尺寸的二维 shadow map 放进同一资源、用 layer index 选择的纹理组织方式;它让几何着色器或实例索引可以写入不同层。DX10 的 render-target array 配合 geometry shader 可以让一个 primitive 被送往不同的 layer;shadow maps 可以放在一个 texture array 中,生成阶段只提交一次几何。DX9 则通常把每张地图作为独立纹理同时绑定,由 pixel shader 根据 view-space z 选择 sampler。两者不是同一硬件能力的简单换名:资源绑定、sampler 数量和可写 render-target layer 的限制不同。
↡在 geometry shader 中根据 caster 覆盖的 split 范围复制三角形,并为每份三角形设置对应 render-target layer 的 DX10 路径。DX10 路径还可以用 instancing 让 instance index 表示 split index,把 split-specific crop matrix 放到 vertex shader,再让 geometry shader 主要设置 render-target index。为了避免把每个 caster 无条件复制到所有层,可以为它记录 firstSplit 与 lastSplit;连续包围体只会覆盖连续的 split 范围。
6. 把画质、pass 和资源限制放进同一张账
PSSM 的方案选择不是一个固定排行榜。multipass 的优点是路径直观、容易调试,代价是生成和合成都按 split 重复;DX9 single-pass shade 可以把合成 pass 合并,但 sampler 数量和更复杂的 shader 会限制层数;DX10 texture array 加 geometry shader cloning 或 instancing 可以减少地图生成的重复,却需要新的资源视图、layer 输出和硬件能力。
同一套 split 也不一定要同一张分辨率。近层往往更值得保留高密度 shadow map,远层可以降低分辨率;point light 可以沿同样的思路使用,只是 light projection 与覆盖面需要重新核对。最终应同时记录近远 aliasing、shadow-map 内存、caster 绘制次数、fragment 采样成本和设备功能,而不是只比较一张截图。
三步验收:从分层到可编程阴影
第一步:把 view frustum 切成可解释的深度层
先收紧 near/far,再用 practical split scheme 在 uniform 和 logarithmic 分布之间选位置。每个 split 都应有清晰的深度边界和自己的 shadow-map 预算。
PSSM Design Lab
把 split 分配、light frustum 对焦和硬件路径分开调整,观察近远采样密度与 rendering passes 的取舍。
shader 选择 split 后可减少合成 pass;继续检查 sampler 或 texture-array 资源限制。
本章小结
- PSSM 按 view-space depth 分层,让近远区域获得更匹配的 shadow-map sampling density。
- practical split scheme 混合 uniform 与 logarithmic,并应先收紧 near/far plane。
- 每个 split 都需要自己的 light transform;crop matrix 可提高有效 texel 利用率,但边界更敏感。
- multipass、DX9 single-pass shade 与 DX10 array + clone 把重复成本放在不同管线阶段。
- 评估方案时同时记录 aliasing、passes、sampler/array 资源、casters 与目标 GPU 能力。
练习
问题 1|分层直觉。 为什么 near/far 等距的 split 不一定是最好的?请说明 logarithmic split 的优势、实际缺点,以及 practical split 如何介于两者之间。
问题 2|修改 Demo 代码。 给 PSSM Design Lab 增加一个 splitEnds 预览:实现一个函数,输入 near、far、splitCount 和 λ,返回 uniform/logarithmic/practical 三组边界,并把 practical 边界画成竖线。你会怎样测试它没有把 near/far 空白也当成有效预算?
问题 3|方案选型。 一个旧 GPU 只能稳定执行多 pass,一个 DX9-level GPU 有足够 sampler 但没有 texture-array render target,一个 DX10 GPU 支持 geometry shader 和 render-target array。分别从 multipass、DX9 single-pass shade、DX10 array + clone 中选择,并指出每种路径仍需测什么。
名词解释
本章出现的专业名词,用大白话再讲一遍。
- parallel-split shadow map
- practical split scheme
- crop matrix
- texture array
- geometry shader cloning