单张阴影贴图远距离发虚是因texel世界空间拉伸导致PCF模糊、自遮挡加剧、边缘锯齿;CSM通过按距离分段视锥、每段配独立shadowMap实现空间感知资源分配,避免硬切与精度浪费。

为什么直接用单张阴影贴图在远距离会发虚?
因为深度精度和分辨率限制:离相机越远,相同像素覆盖的场景区域越大,shadowMap 的 texel 投影到世界空间后严重拉伸,导致 PCF 采样模糊、自遮挡(peter-panning)加剧、阴影边缘锯齿明显。CSM 的核心思路就是按距离切分视锥(near → far),每段配一张独立 shadowMap,近处用高分辨率小范围贴图,远处用低分辨率大范围贴图——不是“加特效”,而是对硬件资源做空间感知分配。
如何划分级联(cascade)的裁剪平面?别硬写 z 值比例
直接按线性或指数切分 zNear/zFar 在视角倾斜时会导致级联重叠或空洞。正确做法是先计算每个 cascade 的**裁剪空间视锥顶点**,再用这些顶点反推其在光源空间(如正交光)下的包围盒:
- 对每个 cascade 区间 [zi, zi+1],生成 8 个视锥角点(来自相机 view-proj 逆矩阵)
- 将这些点变换到光源空间(例如方向光的
lightView * lightProj) - 取 x/y/z 的 min/max,得到 tight ortho bounds —— 这比简单缩放
z稳定得多 - 注意:z 范围仍需显式 clamp 到 [0,1],否则深度值溢出导致全白/全黑阴影
片段着色器里怎么选对 cascade 并采样?避免分支惩罚
不能用 if-else 逐个判断 fragWorldPos.z 落在哪段——GPU warp 分支效率极低。推荐预计算 cascade 索引并打包进插值寄存器:
// 顶点着色器输出(per-vertex) out float4 cascades; cascades.x = (viewZ > splitDepth[0]) ? 1.0 : 0.0; cascades.y = (viewZ > splitDepth[1]) ? 1.0 : 0.0; cascades.z = (viewZ > splitDepth[2]) ? 1.0 : 0.0; cascades.w = 1.0; // 片段中用 smoothstep 或 saturate 避免硬边 int cascadeIdx = int(dot(cascades, float4(1,2,4,0))); // 0~3
采样前务必做 cascade 边界检查:if (cascadeIdx >= NUM_CASCADES) discard;,否则越界采样会读到脏内存或触发驱动 fallback。
PCF 软阴影和 cascade 边缘过渡怎么不穿帮?
单纯 blend 多张 shadowMap 的深度结果会导致“层叠感”(banding),尤其在 cascade 切换带。关键不是插值 shadowMap,而是插值**采样偏移权重**:
- 计算当前 fragment 到两个相邻 cascade 深度分割面的距离(在光源空间)
- 用
smoothstep(0.0, fadeRange, dist)得到混合权重,只用于 blend 两组 PCF 结果 - 每 cascade 的 PCF 半径要随距离增大而扩大(例如
texelSize * (1.0 + 0.5 * cascadeIdx)),否则远端阴影太硬 - 务必开启
GL_TEXTURE_COMPARE_MODE = GL_COMPARE_REF_TO_TEXTURE,否则手动比较深度会丢失硬件 PCF 加速
最易忽略的是 cascade 的投影矩阵必须用 ortho(方向光)或 perspective(聚光灯)重新计算,且每次相机/光源移动都要更新——缓存错一个矩阵,整片阴影就漂移。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











