深入解析级联阴影映射(Cascaded Shadow Map, CSM):透视锯齿原理、视锥分割推导与工程平滑优化

深入解析级联阴影映射(Cascaded Shadow Map, CSM)

在实时图形学与 3D 渲染管线中,阴影渲染不仅是提升场景空间感与真实感的核心技术,也是性能与画质消耗的主要痛点之一。级联阴影映射(Cascaded Shadow Map,简称 CSM) 是目前工业级渲染引擎(如 Unreal Engine、Unity 及主流 AAA 游戏引擎)解决大场景平行光(如太阳光)透视锯齿(Perspective Aliasing) 的标准方案。

本文将系统解构透视锯齿的数学本质、视锥体分割策略、光照空间包围盒(AABB)推导、Texel Snapping 消除闪烁级联平滑过渡 等工程优化手段。


一、 引言:阴影透视锯齿问题 (Perspective Aliasing)

在基于传统 Shadow Mapping 算法渲染户外大场景时,近处的阴影往往呈现出严重的锯齿状或块状边缘,这种现象被称为透视锯齿(Perspective Aliasing)

1
2
3
4
5
6
7
8
9
10
视角相机 (View Camera)
\ 像素密度极高 (High Pixel Density)
\ | | |
v v v v
+---------------+-----------------------+
| 近处视锥区域 | 远处视锥区域 |
+---------------+-----------------------+
^ ^
| |
阴影贴图采样 (Shadow Map Texels: 均等分布 / Uniform Density)

1. 成因分析

透视锯齿的本质是视图像素与阴影贴图纹素(Texel)的采样率不匹配: * 相机透视投影(Perspective Projection)使得近处视锥截面较小、远处截面巨大。在屏幕空间中,近处一个像素所占有的世界空间物理面积远小于远处像素。 * 然而,平行光(定向光)使用的是正交投影矩阵(Orthographic Projection),它将整张阴影贴图均等划分为固定的 Texel 物理尺寸。 * 这导致视图空间近处的多个屏幕像素映射到了阴影贴图中的同一个纹素上。缺少足够的分辨率来精准覆盖视图近端,最终表现为块状锯齿。

2. 为什么简单的提高分辨率不可行?

直观的解决思路是直接提升阴影贴图的分辨率(如从 \(1024 \times 1024\) 提升至 \(8192 \times 8192\))。但这会带来难以承受的开销: * 显存与带宽暴增:显存占用按分辨率二次方增加,导致 Depth Pass 的带宽与显存开销激增。 * 远处资源浪费:远处物体在屏幕上占据像素极少,极高分辨率的阴影贴图在远景区域造成了严重的采样浪费。


二、 CSM 核心思想与视锥体分割策略

1. 解耦思想

级联阴影映射(CSM)建立在一个符合人类视觉习惯的观察之上:靠近相机的阴影细节对视觉质量至关重要,而远处物体的阴影细节感知度较低

CSM 的核心逻辑在于: 1. 将摄像机的视锥体沿深度 Z 轴分割成 \(N\) 个连续的子视锥体(Cascades,级联)。 2. 为每个级联分别配置独立的正交投影矩阵和专属的阴影贴图(Shadow Map)。 3. 近处级联仅需覆盖极小的视锥体积,从而在相同分辨率下赋予其极高的 Texel 密度;远处级联覆盖巨大体积,虽然 Texel 密度较低,但正好符合远景的视觉精度需求。


2. 视锥体分割方案 (Frustum Split Schemes)

如何划分级联边界(\(Z_0, Z_1, \dots, Z_N\))直接决定了阴影质量的分布。常见的划分数学模型包括:

A. 线性分割 (Linear Split)

按物理距离均匀切割:

\[ Z_i^{\text{lin}} = Z_{\text{near}} + (Z_{\text{far}} - Z_{\text{near}}) \cdot \frac{i}{N} \]

  • 缺点:忽略了透视投影的非线性特性,近处级联依然分不到足够的采样精度。

B. 对数分割 (Logarithmic Split)

按透视深度的对数比例切割,使近处级联分布更加密集:

\[ Z_i^{\text{log}} = Z_{\text{near}} \cdot \left(\frac{Z_{\text{far}}}{Z_{\text{near}}}\right)^{\frac{i}{N}} \]

  • 缺点:在远景区域分割过于稀疏,导致中远景阴影品质骤降。

C. 实用混合分割 (Practical Split Scheme, PSSM)

工业级引擎通常采用线性与对数的加权混合方案(通过参数 \(\lambda \in [0, 1]\) 调节):

\[ Z_i = \lambda Z_i^{\text{log}} + (1 - \lambda) Z_i^{\text{lin}} \]

推荐设定 \(\lambda \approx 0.5 \sim 0.75\),能够在中近景阴影细节与远景平滑度之间取得最佳平衡。


三、 级联包围盒(AABB)的数学推导

构建每个级联的正交投影矩阵是 CSM 算法的核心与难点。我们需要在光照空间(Light Space)中为每个级联视锥体拟合一个最小的轴对齐包围盒(AABB)。

1
[视图空间级联角点] ──(Inverse View Matrix)──> [世界空间坐标] ──(Light View Matrix)──> [光照空间坐标] ──(Min/Max AABB)──> [正交投影矩阵]

1. 计算视图空间中的级联角点

对于某个级联,已知其近裁剪面 \(Z_n\) 与远裁剪面 \(Z_f\),以及相机的垂直视场角 \(\text{FOV}_y\) 与宽高比 \(\text{Aspect}\)

在视图空间(View Space,相机位于原点,看向 \(-Z\)\(+Z\) 轴)中,截锥体 8 个顶点的坐标可以通过三角函数计算得到:

\[ \begin{aligned} h_n &= \tan\left(\frac{\text{FOV}_y}{2}\right) \cdot Z_n, & w_n &= h_n \cdot \text{Aspect} \\ h_f &= \tan\left(\frac{\text{FOV}_y}{2}\right) \cdot Z_f, & w_f &= h_f \cdot \text{Aspect} \end{aligned} \]

8 个顶点在 View Space 中的坐标表示为 \((\pm w, \pm h, Z)\)

\[ \begin{aligned} P_{\text{near\_corners}} &= (\pm w_n, \pm h_n, Z_n) \\ P_{\text{far\_corners}} &= (\pm w_f, \pm h_f, Z_f) \end{aligned} \]


2. 变换至世界空间 (World Space)

将视图空间中的 8 个角点乘以相机视图矩阵的逆矩阵 \(M_{\text{View}}^{-1}\),即可得到级联视锥体在世界空间中的绝对坐标:

\[ P_{\text{world}} = M_{\text{View}}^{-1} \cdot P_{\text{view}} \]


3. 变换至光照空间 (Light Space) 并提取 AABB

定义平行光的光照视图矩阵 \(M_{\text{LightView}}\)(将场景旋转平移,使光线方向与光照空间 \(+Z\) 轴对齐)。将世界空间坐标变换至光照空间:

\[ P_{\text{light}} = M_{\text{LightView}} \cdot P_{\text{world}} \]

遍历这 8 个光照空间中的顶点,分别提取 \(X, Y, Z\) 三个轴上的最小值与最大值,得到最小轴对齐包围盒(AABB):

\[ \begin{aligned} X_{\min}, X_{\max} &= \min(P_{\text{light}}.x), \max(P_{\text{light}}.x) \\ Y_{\min}, Y_{\max} &= \min(P_{\text{light}}.y), \max(P_{\text{light}}.y) \\ Z_{\min}, Z_{\max} &= \min(P_{\text{light}}.z), \max(P_{\text{light}}.z) \end{aligned} \]

4. 构造级联正交投影矩阵

根据生成的 \(X, Y, Z\) 范围,构造正交投影矩阵 \(M_{\text{Ortho}}\)

\[ M_{\text{Ortho}} = \text{Orthographic}(X_{\min}, X_{\max}, Y_{\min}, Y_{\max}, Z_{\min}, Z_{\max}) \]

最终用于渲染该级联阴影贴图的 WVP 矩阵 为:

\[ M_{\text{ShadowWVP}} = M_{\text{Ortho}} \cdot M_{\text{LightView}} \cdot M_{\text{World}} \]

注意(Light Z-Bounds 延伸):为了防止在视锥体外部但处于光源与视锥体之间的物体(如高树或建筑物)遗漏阴影,计算 \(Z_{\min}\) 时通常需要沿着光照方向向后延伸(Extend \(Z_{\min}\)),以包含可能投射阴影入视锥内的遮挡体。


四、 工业级工程瑕疵处理与优化策略

在实际工程实现中,基础 CSM 会带来阴影边缘抖动、接缝明显、阴影失真等瑕疵,需要采用以下技术解决:

1. 消除阴影闪烁/边缘游泳效应 (Texel Snapping / Anti-Flickering)

当相机在场景中移动或旋转时,级联包围盒的物理尺寸和位置会发生微小的连续变化。这会导致阴影贴图中的 Texel 覆盖范围在世界空间中产生亚像素级漂移,表现为阴影边缘像流水一样不停闪烁(Shadow Swimming)

解决方案:包围球与 Texel 增量对齐 (Texel Snapping)

  1. 固定级联包围体为包围球(Bounding Sphere): 不直接使用 AABB 的外接矩形,而是先计算级联视锥体的包围球(中心点 \(C\),半径 \(R\))。因为球体在相机旋转时其半径和几何形状保持不变,能彻底消除旋转导致的闪烁。

  2. Texel 尺寸对齐 (Snapping): 确定单位 Texel 代表的世界物理尺寸:

    \[ \Delta_{\text{texel}} = \frac{2 \cdot R}{\text{ShadowMapResolution}} \]

    将光照空间下的包围盒原点坐标向下取整到 \(\Delta_{\text{texel}}\) 的整数倍:

    \[ P_{\text{snapped}}.x = \left\lfloor \frac{P_{\text{light}}.x}{\Delta_{\text{texel}}} \right\rfloor \cdot \Delta_{\text{texel}} \]

    \[ P_{\text{snapped}}.y = \left\lfloor \frac{P_{\text{light}}.y}{\Delta_{\text{texel}}} \right\rfloor \cdot \Delta_{\text{texel}} \]

应用对齐后,包围盒只会在跨越完整 Texel 距离时产生阶跃,彻底消除亚像素平移导致的边缘闪烁。


2. 级联接缝消除 (Cascade Blending)

在阴影渲染阶段,当片段坐标从级联 0 跨越到级联 1 时,由于采样分辨率突然改变,交界处会出现一条极其明显的硬接缝(Seam)。

解决方案:过渡带混合 (Blend Band)

定义级联边界处占比 5%~10% 的过渡区域。当片元落在混合带内时,着色器同时对当前级联下一级联的阴影贴图进行采样,并基于深度距离进行线性插值(lerp):

\[ \text{ShadowFactor} = (1 - \alpha) \cdot \text{Shadow}_i + \alpha \cdot \text{Shadow}_{i+1} \]


3. 自阴影失真与悬浮 (Shadow Acne & Peter Panning)

  • Shadow Acne(阴影粉刺/自阴影失真):受限于阴影贴图精度,受光面表面像素的深度误判为遮挡。
  • Peter Panning(彼得潘效应/阴影悬浮):偏置过大导致阴影与物体根部断开脱节。

解决方案:Normal Offset Bias

根据表面法线向量与光照方向的夹角,沿着表面法线将片元在世界空间中向外微拉,动态调整 Bias 开销:

\[ \text{Bias} = \max\left(\text{Bias}_{\text{max}} \cdot (1.0 - \mathbf{N} \cdot \mathbf{L}), \text{Bias}_{\text{min}}\right) \]


五、 渲染管线伪代码实现

1. CPU 端级联包围盒计算伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
struct Cascade {
float splitDepth;
glm::mat4 viewProjMatrix;
};

void UpdateCSM(Camera& camera, Light& light, std::vector<Cascade>& cascades) {
// 1. 计算分割深度列表 (PSSM)
CalculatePSSMSplits(cascades, camera.near, camera.far);

glm::mat4 invView = glm::inverse(camera.GetViewMatrix());

for (size_t i = 0; i < cascades.size(); ++i) {
float prevSplit = (i == 0) ? camera.near : cascades[i - 1].splitDepth;
float currSplit = cascades[i].splitDepth;

// 2. 获取当前级联在 View Space 中的 8 个角点
std::vector<glm::vec3> projCorners = GetFrustumCornersVS(camera, prevSplit, currSplit);

// 3. 转换到 World Space,并计算包围球 (Bounding Sphere)
glm::vec3 center(0.0f);
for (const auto& pt : projCorners) {
glm::vec4 worldPt = invView * glm::vec4(pt, 1.0f);
center += glm::vec3(worldPt);
}
center /= projCorners.size(); // 包围球中心

float radius = 0.0f;
for (const auto& pt : projCorners) {
glm::vec4 worldPt = invView * glm::vec4(pt, 1.0f);
radius = std::max(radius, glm::distance(glm::vec3(worldPt), center));
}

// 4. 构建 Light View 矩阵
glm::mat4 lightView = glm::lookAt(center - light.direction * radius, center, glm::vec3(0, 1, 0));

// 5. Texel Snapping (消除阴影闪烁)
float texelPerUnit = SHADOW_MAP_RESOLUTION / (radius * 2.0f);
glm::mat4 snapMatrix = glm::scale(glm::mat4(1.0f), glm::vec3(texelPerUnit));
glm::vec4 snappedCenter = snapMatrix * glm::vec4(center, 1.0f);
snappedCenter = glm::floor(snappedCenter);
snappedCenter = glm::inverse(snapMatrix) * snappedCenter;

// 6. 重新构造对齐后的 Light View & Ortho 矩阵
lightView = glm::lookAt(glm::vec3(snappedCenter) - light.direction * radius, glm::vec3(snappedCenter), glm::vec3(0, 1, 0));
glm::mat4 lightOrtho = glm::ortho(-radius, radius, -radius, radius, 0.0f, radius * 2.0f);

cascades[i].viewProjMatrix = lightOrtho * lightView;
}
}

2. GPU 端 GLSL 级联采样着色器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#version 450

layout(location = 0) in vec3 inWorldPos;
layout(location = 1) in vec3 inNormal;

layout(binding = 0) uniform sampler2DArray shadowMapArray; // 使用 Texture Array 存储多级联 Shadow Map

layout(std140, binding = 1) uniform CSMBuffer {
mat4 lightSpaceMatrices[4];
float cascadeSplits[4];
vec3 lightDir;
int cascadeCount;
} csm;

uniform vec3 viewPos;

float CalculateCSMShadow(vec3 worldPos, vec3 normal) {
// 1. 根据片元视图空间深度确定当前级联索引
float depthVS = abs((viewMatrix * vec4(worldPos, 1.0)).z);
int layer = -1;
for (int i = 0; i < csm.cascadeCount; ++i) {
if (depthVS < csm.cascadeSplits[i]) {
layer = i;
break;
}
}
if (layer == -1) layer = csm.cascadeCount - 1;

// 2. 将世界空间坐标转换至对应级联的光照 NDC 空间
vec4 lightSpacePos = csm.lightSpaceMatrices[layer] * vec4(worldPos, 1.0);
vec3 projCoords = lightSpacePos.xyz / lightSpacePos.w;
projCoords = projCoords * 0.5 + 0.5; // 转换至 [0, 1] 纹理坐标

if (projCoords.z > 1.0) return 0.0;

// 3. 计算 Normal Offset Bias 避免 Shadow Acne
float bias = max(0.005 * (1.0 - dot(normal, -csm.lightDir)), 0.0005);

// 4. 执行 PCF (Percentage-Closer Filtering) 软阴影采样
float shadow = 0.0;
vec2 texelSize = 1.0 / textureSize(shadowMapArray, 0).xy;
for (int x = -1; x <= 1; ++x) {
for (int y = -1; y <= 1; ++y) {
float pcfDepth = texture(shadowMapArray, vec3(projCoords.xy + vec2(x, y) * texelSize, layer)).r;
shadow += (projCoords.z - bias > pcfDepth) ? 1.0 : 0.0;
}
}
shadow /= 9.0;

return shadow;
}

六、 技术方案选型与现代演进

技术并非一成不变,大场景阴影技术也在持续演化:

评估维度 标准 Shadow Map CSM (级联阴影) VSM (虚化/虚拟阴影贴图) 光线追踪阴影 (RTS)
解决痛点 基础遮挡判断 消除透视锯齿 超高分辨率/超大场景遮挡 物理真实软阴影/无锯齿
内存/显存开销 极低 中等 (\(N\) 个 Shadow Map) 高 (按页 Tile 管理) 极低 (无 Texture) / 高 (BVH 显存)
Draw Call 开销 低 (1 次 Depth Pass) 较高 (\(N\) 次 Cascades Pass) 极低 (GPU Driven/只提交变更) 无 Depth Pass
硬件要求 任意 GPU 工业标准 GPU (支持 Texture Array) 支持 Sparse Texture/Bindless 支持 DXR/Hardware Ray Tracing

现代演进(如 Unreal Engine 5 Virtual Shadow Maps, VSM)

在 UE5 等次世代引擎中,传统的 CSM 正逐渐被 Virtual Shadow Maps (VSM) 所补充或替代。VSM 将一个极高分辨率(如 \(16384 \times 16384\))的虚拟阴影贴图划分为微小的 Pages(如 \(128 \times 128\)),配合 Nanite 进行 GPU Driven 的按需加载与缓存。即便如此,CSM 凭借其极高的稳定性、低硬件门槛以及成熟的工程链,依然是绝大多数中手游和 PC 游戏的首选阴影方案。


七、 总结

级联阴影映射(CSM)通过“空间分割”与“多粒度采样”的思想,以极高的性价比破解了实时渲染中的透视锯齿难题: * 核心在于透视匹配:近距离使用小包围盒高 Texel 密度,远距离使用大包围盒覆盖。 * 数学要义在于 AABB 求解:在 Light Space 极小化拟合视锥顶点,并用 PSSM 进行合理切割。 * 工程成败在于细节优化:Texel Snapping 解决闪烁、Cascade Blending 解决接缝、Normal Offset Bias 消除 Acne。

本文作者:Berg Zha

本文链接:http://junglemanpro.com/2026/06/06/cascaded_shadow_map/

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!

ESC 关闭 | 导航 | Enter 打开
输入关键词开始搜索