实时阴影渲染技术深度对比:从 Shadow Map 到 Ray Tracing
阴影是真实感的灵魂。本文系统对比 Shadow Map、PCF、VSM、CSM、SDSM、Ray Traced Shadows 的原理、性能与适用场景,给出选型决策框架。
阴影是 3D 场景中“真实性”的最大杠杆——一个无阴影的角色像漂浮在场景里,加上阴影立刻“落地”。但实时阴影算法的选型一直是图形程序的痛点:从最基础的 Shadow Map 到基于硬件 RT 的阴影,每个方案都有明确的代价与适用边界。本文把主流方案系统对比,帮你为自己的项目选对方案。
一、Shadow Map:所有方案的起点
Shadow Map 是实时阴影的事实标准,几乎所有变体都基于它。原理简单:从光源视角渲染一张深度图,然后从相机视角检查每个像素的深度是否大于光源深度——是则在阴影中。
// 顶点着色器:把世界坐标转到光源裁剪空间
struct VSOut {
float4 pos : SV_POSITION;
float4 lightSpacePos : TEXCOORD0;
};
VSOut VS(float3 worldPos : POSITION) {
VSOut o;
o.pos = mul(float4(worldPos, 1), viewProj);
o.lightSpacePos = mul(float4(worldPos, 1), lightViewProj);
return o;
}
// 像素着色器:比较深度
float4 PS(VSOut i) : SV_Target {
float3 projCoords = i.lightSpacePos.xyz / i.lightSpacePos.w;
projCoords = projCoords * 0.5 + 0.5; // [-1,1] → [0,1]
float closestDepth = shadowMap.Sample(sampler, projCoords.xy).r;
float currentDepth = projCoords.z;
float shadow = currentDepth > closestDepth + 0.005 ? 0.3 : 1.0;
return float4(shadow, shadow, shadow, 1);
}
优点:实现简单,GPU 友好,所有平台都支持。 缺点:
- 硬边:阴影边缘是锯齿状的
- Acne:精度问题导致自阴影伪影
- Peter panning:bias 过大让阴影脱离物体
- 分辨率瓶颈:远离光源的区域阴影质量差
所有后续方案都是在解决这些问题。
二、PCF:最简单的软阴影
Percentage-Closer Filtering 不是新算法,而是对 Shadow Map 采样的多次比较 + 平均:
float PCF(float2 uv, float compareDepth) {
float shadow = 0;
float2 texelSize = 1.0 / shadowMapSize;
[unroll]
for (int x = -1; x <= 1; x++) {
[unroll]
for (int y = -1; y <= 1; y++) {
float pcfDepth = shadowMap.Sample(sampler, uv + float2(x, y) * texelSize).r;
shadow += compareDepth > pcfDepth + 0.005 ? 0.3 : 1.0;
}
}
return shadow / 9.0;
}
3x3 PCF 让边缘从锯齿变成“颗粒状”,5x5 更柔但更慢。PCF 是最常用的软阴影方案,性能/质量平衡好。
PCSS:物理正确的软阴影
PCSS(Percentage-Closer Soft Shadows)在 PCF 基础上动态调整采样范围:
// 根据"遮挡物到光源距离"调整 penumbra 大小
float penumbra = (receiverDepth - blockerDepth) * lightSize / blockerDepth;
float filterRadius = penumbra * shadowMapSize / receiverDepth;
// 用 filterRadius 做 PCF
近处阴影锐、远处阴影柔,更接近物理真实。但代价是两次纹理采样(blocker search + PCF),开销是普通 PCF 的 2-3 倍。
三、VSM:可过滤的阴影图
Variance Shadow Map 的核心思路:存储 depth 和 depth²,用切比雪夫不等式估算遮挡比例。
// 渲染时: RG 通道存 depth 和 depth²
float2 moments = float2(depth, depth * depth);
// 用硬件 bilinear filter 采样 moments
// 查询时
float2 m = shadowMap.Sample(sampler, uv).rg;
float p = (currentDepth <= m.x) ? 1.0 : 0.0;
float variance = m.y - m.x * m.x;
variance = max(variance, 0.00001);
float d = currentDepth - m.x;
float pMax = variance / (variance + d * d);
float shadow = max(p, pMax);
VSM 的最大优势:可以用硬件 bilinear / mipmap 过滤,远比 PCF 的多次采样便宜。但精度问题让 VSM 在“半遮挡”场景出现 light leaking(不该亮的地方亮了)。
四、CSM:远距离阴影的工程答案
Cascade Shadow Maps 把相机的视锥切成多个 cascade,每个 cascade 用独立的 shadow map:
// 通常 3-4 个 cascade
struct CascadeInfo {
Matrix viewProj;
float resolution; // 比如 2048
float farPlane; // 比如 10, 30, 100, 500
};
// 像素着色器:根据 view space Z 选 cascade
float3 viewPos = mul(worldPos, view);
float depth = -viewPos.z;
int cascadeIdx = 0;
[unroll]
for (int i = 0; i < CASCADE_COUNT; i++) {
if (depth > cascades[i].farPlane) cascadeIdx = i + 1;
}
// 用对应 cascade 的 shadow map
float shadow = SampleShadow(cascades[cascadeIdx], worldPos);
近处用高分辨率 shadow map(锐利),远处用低分辨率(覆盖范围大)。这是几乎所有 3A 游戏的标配。
SDSM:自动算 cascade 范围
Sample Distribution Shadow Maps 用上一帧的深度图反推“光源视锥应该包多大”,避免手动调参:
// GPU reduce pass:找出 view space 最小/最大 Z
// CPU readback:拿到这两个值
// 计算紧凑的光源视锥,重渲染 shadow map
效果是“无论镜头怎么动,shadow map 分辨率都用得最满”,比固定 cascade 划分质量高 30%+。
五、Ray Traced Shadows:终极方案
实时光线追踪在 2018 年 RTX 发布后逐步普及,2026 年的中高端 PC 几乎都支持。
// DXR / Vulkan RT
[shader("raygeneration")]
void RayGen() {
uint2 pixel = DispatchRaysIndex().xy;
float3 origin = cameraPos;
float3 dir = computeRayDir(pixel);
RayDesc ray;
ray.Origin = origin;
ray.Direction = dir;
ray.TMin = 0;
ray.TMax = 1000;
ShadowPayload payload;
payload.inShadow = false;
TraceRay(
topLevelAS,
RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH,
0xFF, 0, 0, 0, ray, payload
);
gOutput[pixel] = payload.inShadow ? 0.3 : 1.0;
}
优点:
- 完美的硬阴影(点光源)
- 完美的软阴影(面光源,多 ray 平均)
- 没有 shadow map 的 acne / peter panning / resolution 问题
缺点:
- 性能开销大(每像素至少一次 RT traversal)
- 需要 BVH 加速结构
- 移动端几乎不支持
六、选型决策框架
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 移动端 / Web | 1 cascade + PCF | 性能预算紧张 |
| 中端 PC 游戏 | CSM (3-4) + PCF | 平衡好 |
| 高端 PC 游戏 | CSM + PCSS 或 RT shadows | 软阴影更真实 |
| 室内场景 | 单张 shadow map + VSM | 紧凑范围用 VSM 不漏光 |
| 大型开放世界 | CSM + SDSM + 远距离 RT | 远景用 RT 替代极低分辨率 CSM |
| 体素/体积阴影 | Ray marched shadows | Shadow Map 不适合 |
七、性能预算参考
以 1080p / 60fps 为基准(总预算 16ms):
| 方案 | 阴影耗时 | 占总帧 |
|---|---|---|
| 单 cascade + PCF 3x3 | 0.3ms | 2% |
| 4 cascade + PCF 5x5 | 1.8ms | 11% |
| 4 cascade + PCSS | 4.2ms | 26% |
| RT shadows (1 ray/px) | 2.5ms | 16% |
| RT soft shadows (8 ray/px) | 8.0ms | 50% |
经验法则:阴影预算不要超过总帧的 20%,否则其他系统会爆。
八、常见伪影与对策
Shadow Acne(条纹状自阴影)
- 原因:深度精度不够
- 对策:增加 bias;用 front-face culling 渲染 shadow map
Peter Panning(阴影脱离物体)
- 原因:bias 过大
- 对策:减小 bias,或用 slope-scaled bias
Light Leaking(不该亮的亮了)
- 原因:VSM/ESM 高频信号丢失
- 对策:用 EVSM 4 moments,或回退 PCF
Perspective Aliasing(近处锯齿)
- 原因:单 cascade 分辨率不够
- 对策:CSM,近处用更高分辨率
结语
实时阴影没有“银弹”,每个方案都是在“质量 vs 性能 vs 复杂度”三角中做取舍。对一个新项目,建议从 CSM + PCF 起步,这是最稳妥的工程基线;有 RT 硬件再加 RT shadows 作为高端选项;移动端/Web 老老实实单 cascade + PCF。
阴影算法的演进史,本质上是 GPU 能力增长的镜像——从早期的 stencil shadow volume,到 shadow map 的各种变体,再到 RT shadows。每一次跳跃都让“真实”更近一步。但记住:用户感知的是“有没有阴影”远大于“阴影质量多高”,所以先把基础做好,再追求极致。