实时渲染管线探析 · 第 二 篇

阴影贴图:从 PCF 到 VSM 的演进

2026-09-1014,354 字124 张配图本文由@溯光 出品,转载请注明出处
题图
题图光线追踪直接光照渲染:清晰的投射阴影正是阴影贴图要复现的目标(离线渲染,作为实时近似的质量参照)@ Wikimedia Commons · Barahag(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File:Direct_lighting.png

系列文章前言

1.1 系列定位

本系列逐层拆解实时渲染管线(Real-Time Rendering Pipeline)的关键环节。上一篇讲延迟渲染为什么快、代价在哪,收在"三座大山"上;这一篇接着讲阴影——它踩的是同一个三角:质量、性能、调参成本,三者总得让出一个。

系列五篇的在管线中的位置
图 2-1本系列五篇在渲染管线中的位置与本篇的坐标@ 本系列绘制

1.2 本篇要讲清的一件事

阴影难,不难在"怎么算出一个影子",难在代价从哪来、又被谁消掉。

两趟渲染(Two-Pass Rendering)本身是廉价的,麻烦出在深度精度上:为了压制自遮蔽(Shadow Acne),必须给一个深度偏移(Depth Bias),而这个偏移一旦调大到压住所有角度,阴影就会从物体上脱开(Peter-Panning)。这个两难是整条演进脉络的起点——后面每一种方法,本质上都在回答"能不能不靠手工调这个偏移"。

阴影贴图的两趟渲染流程
图 2-2阴影贴图的两趟渲染流程:先从光源视角写入深度,再从相机视角比较@ 本系列绘制

1.3 全系列内容总览

  1. 【实时渲染管线探析】(一) 延迟渲染:从原理到实现的性能权衡
  2. 【实时渲染管线探析】(二) 阴影贴图:从 PCF 到 VSM 的演进 ← 本篇
  3. 【实时渲染管线探析】(三) 后处理链:Bloom、色调映射与抗锯齿
  4. 【实时渲染管线探析】(四) PBR 与 IBL:从渲染方程到工业实践
  5. 【实时渲染管线探析】(五) 可见性与剔除:从视锥到 GPU-Driven

1.4 相关资源链接

  • 上一篇:延迟渲染:从原理到实现的性能权衡(见系列目录)
  • Real-Time Rendering 官方网站(本系列常查的工具书站点):https://www.realtimerendering.com/

1.5 与已有中文文章的差异

写作前检索过同类中文文章,它们普遍止步于「PCF + CSM」。把差异列出来,方便你判断这篇是否值得读:

已有文章 切入角度 未覆盖的部分
《揭秘阴影贴图:实时渲染的魔法核心》 两步流程 + 公式 + 伪代码 + 问题清单 无 PCSS/VSM/ESM 的数学与实现
《游戏阴影为何远处模糊?》 用大白话讲 CSM 级联分区 无演进脉络,无偏移与漏光的定量讨论
《GAMES202 高质量实时渲染》课程笔记 课堂推导:PCF、PCSS 三步、VSM 动机 缺工业实现细节(级联选择、图集、参数映射)
《Real-Time Rendering》第七章中文精读 以教材章节为骨架 工程落地与性能实测较少
opengl-tutorials 第十六课中文版 LearnOpenGL 路线实操 仅基础 PCF,无 PCSS/VSM/ESM/CSM

这几篇的原文地址如下,都值得一读:

  • https://blog.csdn.net/qq_33060405/article/details/148981453
  • https://blog.csdn.net/qq_33060405/article/details/156115916
  • https://blog.csdn.net/lzh804121985/article/details/147463739
  • https://www.cnblogs.com/TiredInkRaven/p/19722483
  • https://github.com/joinerGreat/opengl-tutorials/blob/master/tut16/zh.md

本文相对它们的差异集中在四处:PCSS 的 blocker search 与半影推导、VSM 的切比雪夫上界与最小方差钳制、ESM 的指数近似失效条件、以及四种方法在真实引擎中的参数映射对照。

本文与已有中文文章的内容覆盖对照
图 2-3本文与已有中文文章在内容覆盖上的差异@ 本系列绘制
环境光遮蔽在场景中的应用对照
图 2-4环境光遮蔽在场景中的应用:遮挡带来的间接光照衰减@ Wikimedia Commons · TheWusa(CC BY-SA 3.0)· https://commons.wikimedia.org/wiki/File:Ambient_occlusion.jpg
超采样的采样密度原理
图 2-5超采样原理:采样密度决定边缘质量,与阴影滤波是同一类问题@ Wikimedia Commons · Hrkalona(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File:Fz_supersampling.png

背景铺垫

2.1 为什么阴影是最贵的"免费效果"

阴影在观感上几乎是免费的——它不增加几何,不改变材质,只是让该暗的地方暗下去。但从管线的角度看,它是唯一需要为每个光源额外渲染一整张深度图的效果。

一张方向光的阴影贴图(Shadow Map)通常要覆盖整个可见视锥,分辨率 2048² 起步;多光源场景下这是成倍的开销。更麻烦的是,这张图的质量与主画面的质量不是同一套标准:主画面可以按屏幕分辨率逐像素着色,而阴影贴图只有固定分辨率,一个纹素(Texel)往往要覆盖屏幕上好几个像素,于是所有精度问题都被放大到肉眼可见。

阴影产生的额外渲染通道
图 2-6阴影是管线上唯一的"为每个光源额外渲染一整张深度图"的效果@ 本系列绘制

这也解释了一个常见困惑:为什么把阴影分辨率从 2048 提到 4096,帧率掉得比预期多。提升的不只是一张纹理的内存,而是整个深度渲染通道的填充率——它随面积增长,不是线性。可以对照 Unity 手册对实时阴影开销构成的说明 —— https://docs.unity3d.com/Manual/shadow-realtime.html

2.2 本文的范围边界

本文只讨论基于阴影贴图的实时阴影,具体覆盖 PCF、PCSS、VSM、ESM 与 CSM 这条主线。以下内容不在本文范围内:

  • 不需要阴影贴图的方案(光线追踪阴影、屏幕空间接触阴影 SSCR)——前者属光追管线,后者精度与场景相关,是另一个话题
  • 点光源立方体阴影贴图(Cube Shadow Map)的实现细节——原理与 2D 版本一致,只在采样方向上有别
  • 半透明物体的阴影(透射率估计)——只作为"未解决的难题"提一句,不展开

性能数据只引用论文与官方技术章的实测,不采信社区横评——原因见第七章。

本文覆盖与不覆盖的范围
图 2-7本文的讨论范围:阴影贴图主线,以及明确排除的三类方案@ 本系列绘制

2.3 术语对齐

后文统一使用简称,避免中英混用造成歧义:

关键术语中英对照
图 2-8关键术语的中英对照与本系列简称约定@ 本系列绘制

2.4 一个贯穿全篇的问题

带着这个问题读下去会更有收获:既然 VSM 用二阶矩就能免掉手工调 bias,为什么今天的主流引擎还在用 PCF 和 CSM,而不是全换成 VSM?

答案在第四、五、七章——VSM 用近似换掉了调参,而近似有它自己的失效边界。

一、两趟渲染与 acne:bias 的两难

3.1 精度问题的一个共同根源

阴影贴图的全部精度问题,都可以追溯到同一件事:深度图是有分辨率的,而深度比较是逐纹素的。一个纹素覆盖的表面在一定深度范围内起伏,只要这个范围超过深度精度,就会有一部分表面把自己判定成"被遮挡"。

3.2 原理:比较本身没错,错在精度

第一趟从光源位置渲染,只写深度,得到阴影贴图;第二趟从相机位置渲染,把每个像素变换到光源空间,取出对应纹素的深度与自己的深度比较:

depth_camera > depth_light  →  该点被遮挡,处于阴影中
depth_camera ≤ depth_light  →  该点受光

判据本身没有问题。问题在于 depth_light 取自一个纹素的中心,而它要代表整个纹素覆盖的那一小块表面。当表面与光线夹角很大时,一块纹素在深度方向上跨越的范围会被拉得很长,这时纹素中心的深度就可能小于该点实际深度——自遮蔽就出现了,也就是 Shadow Acne(阴影痤疮)。

这个基础框架的实现细节,LearnOpenGL 的阴影贴图章节与 OGLDEV 的两篇教程给出的最小可运行版本最便于对照参考 —— https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping | https://ogldev.org/www/tutorial23/tutorial23.html | https://ogldev.org/www/tutorial24/tutorial24.html

阴影贴图的失败案例:自遮蔽伪影
图 2-9自遮蔽伪影的示意:表面出现条带状误判为阴影的区域@ 本系列绘制
自遮蔽伪影的真实渲染结果
图 2-10自遮蔽的真实结果:未加偏移的阴影贴图实现中出现的条带状误判像素("failed pixels")@ Wikimedia Commons · Praetor alpha(CC BY-SA 3.0)· https://commons.wikimedia.org/wiki/File:5failed.png

同样的自遮蔽伪影,在实时引擎里的表现是这样的:

引擎中的 shadow acne 实证:偏置不足时,阴影接收面浮起条带状的自遮蔽伪影
图 2-11引擎中的 shadow acne 实证:偏置不足时,阴影接收面浮起条带状的自遮蔽伪影@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/lights_and_shadows.html

有阴影与没阴影的差别,一眼可见:

有与无阴影对照:同一场景在启用/关闭阴影下的观感差异
图 2-12有与无阴影对照:同一场景在启用/关闭阴影下的观感差异@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping

第二趟要先把当前像素变换回光源空间、拿到它在深度图上的位置,才能做那次比较——这一步的坐标变换是这样的:

光源空间的投影变换:像素先被变换到光源裁剪空间,再用其结果查询深度图
图 2-13光源空间的投影变换:像素先被变换到光源裁剪空间,再用其结果查询深度图@ LearnOpenGL(CC BY 4.0) · Joey de Vries(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping

3.3 代码:最朴素的实现

//-----------------------------------【阴影贴图基础实现】---------------------------------------
// 描述:两趟渲染的最小实现,用于说明 bias 为何必要
//--------------------------------------------------------------------------------------------
//【1】第一趟:从光源视角写深度
gl_Position = lightSpaceMatrix * modelMatrix * vec4(aPos, 1.0);

//【2】第二趟:把当前像素变换到光源空间
vec4 fragPosLightSpace = lightSpaceMatrix * modelMatrix * vec4(fragPos, 1.0);
vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w * 0.5 + 0.5;

//【3】取出阴影贴图中的深度并比较
float closestDepth = texture(shadowMap, projCoords.xy).r;
float currentDepth = projCoords.z;
float shadow = currentDepth > closestDepth ? 1.0 : 0.0;   // ← 这里必然出 acne

上面代码中,最后一行没有任何偏移处理,直接比较必然产生 acne。偏移(bias)不是可选项,而是这套机制能工作的前提。

3.4 从光源视图到最终阴影:一组对照

抽象讨论之外,先用一组示意图把流程走一遍。下面四张示意图依次说明光源视角、深度图内容、遮挡判定与最终结果(为便于看清原理,此处用示意图表达):

光源视角下的场景
图 2-14第一趟的视角:从光源位置渲染场景@ 本系列绘制
阴影贴图的深度内容
图 2-15阴影贴图的内容:只存深度、不含颜色@ 本系列绘制
遮挡判定示意
图 2-16第二趟的判定:把相机视角的像素变换到光源空间做深度比较@ 本系列绘制
无阴影对照
图 2-17对照:未启用阴影时的同一场景@ 本系列绘制

对照这四张可以看出:阴影贴图里没有任何颜色信息,"阴影"完全是第二趟比较出来的副产品。这一点是后面所有伪影的根源——因为深度图上相邻纹素的关系,并不能完全代表它们在真实表面上的关系。

上面四张为本系列自绘的原理示意。作为对照,下面给出同一流程的真实实现结果——作者 Praetor alpha 的阴影贴图演示,从左到右依次是:第一趟的光源视角、阴影贴图(光源视角的深度)、第二趟把阴影图投影回场景做遮挡判定、以及阴影填充完成后的最终画面:

阴影贴图两趟渲染的真实结果
图 2-18阴影贴图两趟渲染的真实结果(从左至右:光源视角 / 阴影贴图深度 / 遮挡判定投影 / 最终阴影)@ Wikimedia Commons · Praetor alpha(CC BY-SA 3.0)· https://commons.wikimedia.org/wiki/Category:Shadow_mapping

阴影贴图的内容形态,还可以用一条单行剖切来表达:

阴影贴图深度条带
图 2-19阴影贴图的一行剖切:条带明暗直接对应深度@ 本系列绘制

两趟渲染的几何关系,先看这张原理图:

阴影贴图原理:从光源视角渲染一遍场景,把深度记进贴图
图 2-20阴影贴图原理:从光源视角渲染一遍场景,把深度记进贴图@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping

第二趟要把像素变换到光源空间,坐标关系是这样的:

光源空间变换:把相机视角的像素投影到光源坐标系再比较深度
图 2-21光源空间变换:把相机视角的像素投影到光源坐标系再比较深度@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping

阴影贴图本身(光源视角的深度)在这类可视化里长这样:

阴影贴图内容:只记录光源视角下每个纹素的最浅深度
图 2-22阴影贴图内容:只记录光源视角下每个纹素的最浅深度@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping

最终叠加上阴影后的画面:

阴影应用结果:比较出的遮挡关系被填回画面
图 2-23阴影应用结果:比较出的遮挡关系被填回画面@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping

3.5 为什么 acne 会出现:剖面视角

把视线切到光线的侧面,成因就一目了然:

acne 的成因剖面
图 2-24自遮蔽的成因:纹素中心的深度与像素实际深度不一致@ 本系列绘制

自遮蔽的成因,用剖面图解看最清楚:

自遮蔽成因图解:纹素深度与实际表面深度的偏差导致误判
图 2-25自遮蔽成因图解:纹素深度与实际表面深度的偏差导致误判@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping

图中灰色是真实表面,斜线是光线方向。一个纹素在表面上覆盖的区间,在光线方向上被拉伸成一段深度范围;而阴影贴图只记录其中一个采样点的深度。当采样点选在区间靠前的位置时,区间后段的表面就会把自己判定为"被前方遮挡"。

3.6 关键公式:bias 需要多大

偏移量并不是一个常数,它正比于纹素在深度方向上覆盖的最大跨度:

$$ \text{bias} \ge \frac{\text{纹素世界尺寸}}{|\cos\theta| \cdot \text{深度精度}} $$

其中:

  • $\text{纹素世界尺寸}$ 是阴影贴图一个纹素在世界空间中的边长,与分辨率成反比。
  • $\theta$ 是表面法线与光线方向的夹角——这才是问题的核心:当 $\theta \to 90°$ 时,$\cos\theta \to 0$,所需 bias 趋于无穷。
  • $\text{深度精度}$ 是深度缓冲的有效精度位。

这条式子说明了 acne 为什么在掠射角(光线几乎平行于表面)处最严重,也解释了为什么"调一个固定 bias"永远不能通吃所有情况。

3.7 一个具体的数字例子

抽象公式不好感受,代入一组实际参数:

项 取值
阴影贴图分辨率 2048²
覆盖世界范围 100 m × 100 m
单纹素世界尺寸 100 / 2048 ≈ 0.049 m
表面法线与光线夹角 80°(掠射,$\cos 80° \approx 0.17$)
由此需要的 bias 0.049 / 0.17 ≈ 0.29 m

反过来在垂直入射($\theta = 0°$)处,所需 bias 只有 0.049 m。同一个场景里,不同表面需要的偏移相差近 6 倍——这就是"固定 bias 必然顾此失彼"的算术依据。

所需偏移随夹角的变化
图 2-26所需 bias 随入射夹角的变化:垂直入射平缓,掠射角急剧上升@ 本系列绘制

再算一步:0.29 m 的偏移意味着,任何厚度小于 0.29 m 的物体(薄墙、栏杆、叶片)都会丢失自己投下的阴影,因为它的深度差被偏移吃掉了。这就是 Peter-Panning 在薄几何上最先暴露的原因。

3.8 深度精度:另一个放大因素

除了几何夹角,深度缓冲本身的精度分布也在推波助澜。透视投影下,深度值的分布是非线性的:

透视投影下的深度精度分布
图 2-27透视投影下深度值的非线性分布:近处精度过剩,远处精度严重不足@ 本系列绘制

近处一小段世界距离占用很大一部分深度值范围,远处则相反。这意味着同一张阴影贴图上,远处的 acne 风险远高于近处。两个缓解手段因此成为标准做法:

  • 反转 Z(Reverse-Z):把深度范围映射反过来,配合浮点数的精度分布,让远端的有效精度显著提升。OpenGL 侧可用 glClipControl 与 glDepthRange 实现反向映射,相关说明见 https://docs.gl/gl4/glClipControl | https://docs.gl/gl4/glDepthRange
  • 光源近平面贴近:方向光的正交投影没有透视除法,但聚光/点光有;把光源近平面贴近遮挡体可减少无效深度范围

深度精度的分布规律,Nathan Reed 的 Depth Precision Visualized 与 Emil Persson 的 A couple of notes about Z 是流传最广的两篇说明,也是反向 Z 方案的常见出处 —— https://developer.nvidia.com/content/depth-precision-visualized | https://www.humus.name/index.php?ID=255 | http://outerra.blogspot.com/2012/11/maximizing-depth-buffer-range-and.html

3.9 bias 的两难

调小 → 掠射角处仍出 acne;调大 → 阴影与投射体分离,即 Peter-Panning(彼得潘现象)。

bias 两难:acne 与 peter-panning 的区间
图 2-28bias 的两难:偏移过小残留自遮蔽,过大导致阴影脱开投射体@ 本系列绘制

真正麻烦的地方在于:这个两难没有"正确"取值,只有针对特定场景的折中。Slope-Scale Depth Bias(斜率相关深度偏移)把 $\cos\theta$ 引入偏移计算,缓解了大部分情况,但掠射角仍然是盲区。

斜率相关偏移对两难的缓解
图 2-29Slope-Scale Bias 按夹角缩放偏移:缓解大部分角度,掠射角仍是盲区@ 本系列绘制

偏置的取值区间,可以看这张示意:

偏置(bias)示意:偏移量需覆盖纹素的深度跨度,又不能过大
图 2-30偏置(bias)示意:偏移量需覆盖纹素的深度跨度,又不能过大@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping

应用偏置之后,伪影消失:

应用 bias 后的结果:自遮蔽条纹被消除,阴影保持贴合
图 2-31应用 bias 后的结果:自遮蔽条纹被消除,阴影保持贴合@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping

斜率相关偏移的工程落地方式是取夹角的正切而非余弦——因为 $\tan\theta$ 在掠射角附近增长更快,更贴合所需的偏移增长趋势:

$$ \text{offset} = \text{factor} \cdot \tan\theta_{\text{slope}} + \text{units} \cdot r $$

其中:

  • $\text{factor}$ 是斜率相关系数,对应 Vulkan 的 depthBiasSlopeFactor。
  • $\text{units}$ 是最小解析度单位的常数项,对应 depthBiasConstantFactor。
  • $r$ 是一个纹素在深度方向上的最小可分辨量。

注意这个公式仍然是线性的,而第一章开头那条公式里的 $1/\cos\theta$ 是非线性的——用线性近似非线性,这正是它在掠射角处必然失效的数学原因。API 层为此提供了 depthBiasClamp 作为兜底(见第七章)。

3.10 一种绕开思路的技巧

反转正面剔除(Reverse Cull Face)配合负偏移,可以让背面承担深度写入,从而在多数模型上同时压住 acne 又不产生明显脱开——代价是薄壁几何(薄墙、叶片)会失效。

反转剔除与负偏移的配合
图 2-32反转正面剔除让背面写入深度,绕开掠射角自遮蔽(薄壁几何失效)@ 本系列绘制

这条绕开思路落地后的效果:

启用正面剔除后的阴影结果:同一场景下自遮蔽条纹消失
图 2-33启用正面剔除后的阴影结果:同一场景下自遮蔽条纹消失@ Joey de Vries(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping
//-----------------------------------【反转剔除 + 负偏移】-------------------------------------
// 描述:用背面深度规避掠射角自遮蔽,薄壁几何不适用
//--------------------------------------------------------------------------------------------
//【1】第一趟只渲染背面
glEnable(GL_CULL_FACE);
glCullFace(GL_FRONT);

//【2】配合负偏移
glEnable(GL_POLYGON_OFFSET_FILL);
glPolygonOffset(-1.0, -1.0);

3.11 bias 是个躲不掉的参数

走完这一章只剩一个结论:在 PCF 这条路上,bias 是个躲不掉的手工参数。这是后面所有方法要解决的问题——不是让阴影更好看,而是把这个参数消掉。加深这一层后,第二章的 PCF 才有了它"代价随核面积增长"的批评对象。

bias 问题的三种缓解手段对照
图 2-34bias 问题的三种缓解手段:固定偏移、斜率相关偏移、反转剔除@ 本系列绘制

偏置调过头的代价,在引擎里一眼可见:

bias 过大的代价——peter-panning:阴影与物体“脱开”,像悬浮起来一样
图 2-35bias 过大的代价——peter-panning:阴影与物体“脱开”,像悬浮起来一样@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/lights_and_shadows.html

二、PCF:过滤比较结果

4.1 从「比一个纹素」到「比一片」

PCF(Percentage-Closer Filtering)的想法很直白——不要只比一个纹素,比周围一片,取比例。它确实让阴影边缘变软了,但代价是采样次数随核面积平方增长,而且无法用 mipmap 之类的预过滤来加速。

4.2 原理:为什么不能过滤深度

一个自然的想法是:把阴影贴图当普通纹理,做一次模糊,再比较。这是错的。

原因是深度值与可见性之间是非线性关系:两个点深度取平均后,其遮挡状态不等于两点遮挡状态的平均。一个深度 10 和一个深度 20 的纹素平均得到 15,而 15 这个深度可能被任何介于两者之间的物体遮挡——平均之后的深度是没有物理意义的。

所以 PCF 只能反过来做:先逐个比较,得到 0/1 结果,再对结果求平均。

PCF 采样核对比
图 2-36PCF 采样核:3×3、5×5 与泊松盘采样对阴影边缘的软化效果@ 本系列绘制

4.3 代码:PCF 的实现

//-----------------------------------【PCF 9 抽样】--------------------------------------------
// 描述:对比较结果做平均,而非对深度做平均
//--------------------------------------------------------------------------------------------
//【1】预先计算采样偏移
const vec2 offsets[9] = vec2[9](
    vec2(-1,1), vec2(0,1), vec2(1,1),
    vec2(-1,0), vec2(0,0), vec2(1,0),
    vec2(-1,-1), vec2(0,-1), vec2(1,-1)
);

//【2】逐点比较后求平均
float pcfShadow(vec3 projCoords, float currentDepth, float bias) {
    float sum = 0.0;
    vec2 texelSize = 1.0 / textureSize(shadowMap, 0) * radius;
    for (int i = 0; i < 9; ++i) {
        float d = texture(shadowMap, projCoords.xy + offsets[i] * texelSize).r;
        sum += currentDepth - bias > d ? 1.0 : 0.0;   // ← 先比较
    }
    return sum / 9.0;                                  // ← 再平均
}

上面代码中,关键是第 9 行与第 11 行的顺序:比较在前、平均在后。一旦写反,得到的就是上一节说的"没有物理意义的平均深度"。

PCF 的核心:过滤比较结果而非深度
图 2-37PCF 的核心思想:先逐点比较得到 0/1,再对结果求平均@ 本系列绘制

最终效果与未处理时对比,差异相当直观:

启用 PCF 后的阴影结果
图 2-38启用偏移与滤波后的阴影结果示意@ 本系列绘制

同一场景在 LearnOpenGL 示例里的真实对比,直观到不用解释:

未正确滤波/偏置的硬阴影,阴影区域遍布 acne 条纹
图 2-39未正确设置偏置与滤波的硬阴影:整个阴影接收面布满锯齿状的自遮蔽条纹@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping
启用 PCF 后的软阴影,左全景、右阴影边缘放大
图 2-40应用 3×3 PCF 后的软阴影:左为完整场景、右为阴影边缘放大,边缘柔化、伪影消失@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping

同样的滤波开关,在引擎的阴影滤波档位里也能直接看到:

引擎中的阴影滤波(filter):开启后阴影边缘由硬转软
图 2-41引擎中的阴影滤波(filter):开启后阴影边缘由硬转软@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/lights_and_shadows.html

上一节说过"比较本身没错,错在精度"——上面硬阴影那张把这句话坐实了:整个阴影区域布满了锯齿状的条带,这正是深度量化与缺偏置共同作用的自遮蔽伪影;而下面那张只做了 3×3 卷积,边缘立刻柔化,伪影消失。注意软阴影接地处的梯度过渡——它是刀刃穿透、滤波粒径与偏置的共同产物,也是第三章 PCSS 要做"可变量"滤波的直接理由。

而更高保真的软阴影(下图由光线追踪离线渲染)展示的是同一个问题的"标准答案"——逐像素按面积光源采样,天然得到正确的半影:

光线追踪与辐射度渲染的质量参照
图 2-42光线追踪与辐射度离线渲染:实时软阴影想逼近的目标@ Wikimedia Commons · KaiaVintr(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File:Rendering_techniques_example,_ray_tracing,_radiosity,_photon_mapping,_POV-Ray.png

把这张图与实时方案的结果对照着看,可以直观理解第三章 PCSS 想逼近的目标是什么。

4.4 参数说明

  • radius 是采样半径(以纹素为单位)。取值越大边缘越软,但采样数按 $O(n^2)$ 增长,且所需 bias 也随之增大——因为偏移要覆盖整个滤波区域。
  • 硬件 PCF 通过 sampler2DShadow 与硬件比较采样器实现,一次采样完成 2×2 双线性比较,是 4 次采样换来的软边。

4.5 PCF 的三个固有代价

代价 原因
采样成本随核面积平方增长 每次采样都是一次纹理读取 + 比较
无法预过滤加速 深度不能做线性滤波(见上文)
bias 必须随核面积增大 否则滤波区域内出现 acne,反过来加剧 Peter-Panning

第二条是 PCF 最硬的限制,也是 VSM 的动机所在——第三章讲 PCSS 如何在 PCF 上叠一层自适应,第四章讲矩方法如何绕开"逐次比较"这件事。

PCF 在阴影边缘上的采样过程
图 2-43PCF 在阴影边缘处的采样过程:部分采样落在阴影内,得到中间灰度@ 本系列绘制

4.6 采样图案:从网格到泊松盘

朴素的 PCF 用矩形网格采样,这会带来一个副作用:规则的采样图案会在阴影边缘留下规则的条带痕迹。当核半径较大时,这种痕迹会明显到被肉眼识别为"方块状渐变"。

实践中常见的替代图案有三类:

图案 特点 代价
矩形网格 实现最简单,可分离(先横后竖两趟) 核较大时出现条带
泊松盘(Poisson Disk) 采样点分布自然,无明显周期结构 需要预生成的采样表
螺旋 / 黄金角 无需查找表,用固定公式生成 分布略有偏心
PCF 采样图案对照
图 2-44从矩形网格到泊松盘的采样图案:图案决定边缘是否留下周期痕迹@ 本系列绘制

泊松盘采样的样本分布有现成的真实图案可供对照:

泊松盘采样图案:样本点保持最小间距,分布比规则网格更均匀
图 2-45泊松盘采样图案:样本点保持最小间距,分布比规则网格更均匀@ Eshipley(CC BY-SA 3.0)· https://commons.wikimedia.org/wiki/File:7_Filters_sampling_poisson_disk_sampling.png

可分离采样(Separable Filtering)值得一提:矩形网格的 PCF 可以拆成横向与纵向两趟,把 $n^2$ 次采样降为 $2n$ 次。但那需要两趟渲染与中间缓冲,在延迟渲染下未必划算——这是一个典型的"算法上更优、工程上未必"的取舍。

可分离滤波的两趟分解
图 2-46可分离滤波:把二维核拆成横纵两趟,采样数从 n² 降到 2n@ 本系列绘制

4.7 硬件 PCF:2×2 比较采样

现代 GPU 提供了硬件比较采样器(sampler2DShadow),一次采样即可完成 2×2 邻域的双线性比较插值:

//-----------------------------------【硬件 PCF】---------------------------------------------
// 描述:sampler2DShadow 一次采样完成 2×2 双线性比较
//--------------------------------------------------------------------------------------------
//【1】用比较采样器,第三个分量是要比较的参考深度
uniform sampler2DShadow shadowMap;
float shadow = texture(shadowMap, vec3(projCoords.xy, currentDepth - bias));
// 返回的是 2×2 邻域比较结果的插值,等价于 4 次采样的软边

这行代码用一次采样换来 4 次采样的软边,是 PCF 在实时渲染中得以普及的关键——没有硬件比较采样器,PCF 的成本会高到无法作为默认方案。

PCF 的来龙去脉与多种采样变体,MJP 的 A Sampling of Shadow Techniques 梳理得最完整;GPU Gems 第 11 章则是 PCF 早期工程化的原始章节 —— https://therealmjp.github.io/posts/shadow-maps/ | https://developer.nvidia.com/gpugems/gpugems/part-ii-lighting-and-shadows/chapter-11-shadow-map-antialiasing

硬件比较采样之所以能"一次换四次",是因为 GPU 在纹理单元里内置了比较逻辑:

硬件比较采样的工作方式
图 2-47硬件比较采样:在纹理单元内完成 2×2 双线性比较插值@ 本系列绘制

4.8 边缘软硬与核半径的关系

PCF 的核半径直接决定阴影边缘的软化程度,但它们不是线性关系:

PCF 核半径与边缘软化程度
图 2-48PCF 核半径与边缘软化程度、成本的关系@ 本系列绘制
核半径 采样数(3×3 基数) 边缘表现
1 纹素 9 轻微软化,几乎仍像硬阴影
2 纹素 25 明显软边
4 纹素 81 很软,但条带痕迹开始显现
8 纹素 289 不适合实时;此时应改用 VSM

这张表也解释了为什么 PCF 通常只做"轻微软化":它擅长的是消除锯齿感,而不是产生物理正确的软阴影。真正要让半影宽度有物理依据,得靠第三章的 PCSS。

PCF 成本随核半径的增长
图 2-49PCF 成本随核半径呈平方增长,与 VSM 的恒定代价形成对照@ 本系列绘制
PCF 与 VSM 的取舍对照
图 2-50PCF 与 VSM 的取舍对照:逐次比较 vs 一次查表@ 本系列绘制

滤波质量档位之间的真实差异:

引擎里的 shadow filter 质量档位对比:核越大边缘越软、代价越高
图 2-51引擎里的 shadow filter 质量档位对比:核越大边缘越软、代价越高@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/lights_and_shadows.html

把阴影边缘放大,滤波前后的差别才看得清:

阴影边缘放大:滤波半径直接决定边缘的软硬程度
图 2-52阴影边缘放大:滤波半径直接决定边缘的软硬程度@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping

三、PCSS:让核面积随遮挡距离自适应

5.1 固定采样半径的局限

PCF 的采样半径是个常数,所以它给出的软硬程度与真实光照不符——真实世界里,遮挡体离接收面越远,半影越宽。PCSS(Percentage-Closer Soft Shadows)的做法是让核面积随遮挡距离自适应:先估计遮挡体的平均距离,据此推出半影宽度,再按这个宽度做 PCF。

固定半径做不到的事,离线全局光照给出了参照——真实场景里的半影随遮挡距离连续变化:

全局光照离线渲染:软阴影宽度随遮挡距离自然变化
图 2-53全局光照离线渲染:软阴影宽度随遮挡距离自然变化@ Ldo(CC0)· https://commons.wikimedia.org/wiki/File:CyclesGlobalIlluminationExample.png

点光源投出的影子是锐的——它没有面积,半影宽度为零:

点光源的锐影:光源无面积、半影宽度为零,固定半径的采样核只会把它抹糊
图 2-54点光源的锐影:光源无面积、半影宽度为零,固定半径的采样核只会把它抹糊@ Wikimedia Commons · Timrb(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File:Point_light_source_sharp_shadow.png

5.2 原理:三步流程

PCSS 不是对 PCF 的替代,而是在它前面加了两步:

① Blocker Search   在阴影贴图中搜索遮挡体,求其平均深度
② 估计半影宽度     由遮挡体距离与接收面距离算出滤波半径
③ PCF              用上一步的半径做变半径 PCF
PCSS 的三步流程
图 2-55PCSS 的三步流程:Blocker Search → 估计半影宽度 → 变半径 PCF@ 本系列绘制

PCSS 在引擎里也不是纸面概念:

引擎中的 PCSS 软阴影演示:半影宽度随遮挡距离变化
图 2-56引擎中的 PCSS 软阴影演示:半影宽度随遮挡距离变化@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/lights_and_shadows.html

注意它把 PCF 变成了"变半径"版本——半径不再是常数,而是每个像素现算出来的。这正是它比 PCF 贵的原因。

5.3 关键公式:半影宽度

半影宽度由相似三角形给出:

$$ w_{\text{penumbra}} = \frac{d_{\text{receiver}} - d_{\text{blocker}}}{d_{\text{blocker}}} \cdot w_{\text{light}} $$

其中:

  • $w_{\text{penumbra}}$ 是屏幕上该点的半影宽度,即 PCF 的滤波半径。
  • $d_{\text{receiver}}$ 是接收面(当前像素对应表面)在光源空间的深度。
  • $d_{\text{blocker}}$ 是遮挡体的平均深度,由第①步的 Blocker Search 求得。
  • $w_{\text{light}}$ 是光源的物理尺寸。

这个公式的物理含义很直白:遮挡体离接收面越远,半影越宽。当 $d_{\text{blocker}} \approx d_{\text{receiver}}$(遮挡体紧贴接收面)时,半影宽度趋于零——得到硬阴影,正如现实中贴合的接触阴影一样锐利。

半影宽度的几何推导
图 2-57半影宽度的相似三角形推导:遮挡体越远,半影越宽@ 本系列绘制

一个能感知到的参照是面光源与点光源的差异。面光源(Area Light)在物理上必然产生半影,而点光源不会:

面光源软阴影
图 2-58面光源产生的软阴影:半影随遮挡距离变化@ 本系列绘制
无面光源的对照
图 2-59对照:不使用面光源时的硬阴影@ 本系列绘制

换成球体这个更简洁的几何体,软阴影在各处的宽度差异会更清楚:

球体的软阴影
图 2-60球体的软阴影:接触处锐利、远离处渐宽@ 本系列绘制

PCSS 想复现的正是这个现象——用一张深度图,估计出本该由面光源采样得到的半影宽度。

变半径核与固定核的对比
图 2-61PCSS 的变半径核与 PCF 固定核:软硬程度随遮挡距离变化@ 本系列绘制

真实世界里,半影随遮挡距离增大是常态:

真实世界的软阴影:树荫下的半影边缘(自然界本身就是面积光源)
图 2-62真实世界的软阴影:树荫下的半影边缘(自然界本身就是面积光源)@ W.carter(CC0)· https://commons.wikimedia.org/wiki/File:Sheep_lounging_in_the_shade_of_a_tree_at_R%C3%B6e_Castle.jpg

半影不是渲染器的发明,而是几何光学的结果——地球本影投在月球上的边缘正是半影:

半影的物理实证:月食中地球本影外围的半影区
图 2-63半影的物理实证:月食中地球本影外围的半影区@ Maxwell Hamilton from Greater London, England United Kingdom(CC BY 2.0)· https://commons.wikimedia.org/wiki/File:Moon_Penumbra_Eclipse_in_London_25_April_2013_(8681049257).jpg

渲染器要逼近的正是这种效果——面光源之所以给出软阴影,是因为它本身有面积:

面光源(area light)的阴影:光源有面积,影子里自然长出半影
图 2-64面光源(area light)的阴影:光源有面积,影子里自然长出半影@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/lights_and_shadows.html

换成有面积的光源,影子立刻出现软边:

面光源的软影:光源有面积必然产生半影,宽度随光源尺寸增大
图 2-65面光源的软影:光源有面积必然产生半影,宽度随光源尺寸增大@ Wikimedia Commons · Timrb(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File:Area_light_source_soft_shadow.png

真实世界里同一遮挡物的半影是有层次的:

半影的层次(实拍):同一遮挡物在不同距离上的半影宽度不同,越远越软
图 2-66半影的层次(实拍):同一遮挡物在不同距离上的半影宽度不同,越远越软@ Wikimedia Commons · Barraki(CC BY-SA 3.0)· https://commons.wikimedia.org/wiki/File:Different_shades_of_penumbra.JPG

5.4 代码:Blocker Search 与半影估计

//-----------------------------------【PCSS 半影宽度估计】-------------------------------------
// 描述:固定大小搜索遮挡体,按平均遮挡距离推出滤波半径
//--------------------------------------------------------------------------------------------
//【1】Blocker Search:在固定范围内找出所有遮挡当前点的纹素,求平均深度
float findBlockerDepth(vec3 projCoords, float bias) {
    float blockerSum = 0.0, blockerCount = 0.0;
    for (int i = 0; i < BLOCKER_SAMPLES; ++i) {
        float d = texture(shadowMap, projCoords.xy + blockOffsets[i] * texelSize).r;
        if (d < projCoords.z - bias) { blockerSum += d; blockerCount += 1.0; }
    }
    return blockerCount > 0.0 ? blockerSum / blockerCount : -1.0;  // -1 = 无遮挡,直接受光
}

//【2】由遮挡距离推半影宽度,再以此半径做 PCF
float penumbra = (projCoords.z - blockerDepth) / blockerDepth * lightSize;

上面代码中,第①步的成本是关键:Blocker Search 的采样数与滤波半径无关(它在一个固定范围内搜索),这让 PCSS 的总成本不至于失控——但也意味着遮挡体分布稀疏时,它可能搜不到足够的遮挡体。

5.5 与 PCF 的成本对照

项 PCF PCSS
采样次数 $n^2$(核面积) $m$(Blocker)+ $n^2$(变半径核)
半径 常数 每像素现算
软硬变化 无 随遮挡距离变化
主要失效场景 掠射角 acne 遮挡体稀疏时 Blocker Search 失准
PCSS 的成本分解
图 2-67PCSS 的成本分解:Blocker Search 为固定开销,变半径 PCF 随半影宽度增长@ 本系列绘制

把遮挡距离与光源尺寸都算进去之后,半影会是连续变化的:

真实半影渲染:软硬随笔尖距离连续变化,这是固定半径的核做不到的
图 2-68真实半影渲染:软硬随笔尖距离连续变化,这是固定半径的核做不到的@ Wikimedia Commons · screenshot of middleware developed by Štěpán Hrbek, who is also the author of this image. I have his agreement to use this image in all Wikipedias(CC BY-SA 3.0)· https://commons.wikimedia.org/wiki/File:Lightsprint-Penumbra-shadows.jpg

PCSS 论文自述的卖点里,有一条对工程判断最有用:它独立于几何复杂度——因为全部计算都在图像空间。这一点在第七章与其他方法横向对比时还会用到。原文与 NVIDIA 的官方 PDF、以及 Bavoil 在 GDC 2008 的系统性讲义,是理解 PCSS 定位的三份主要材料 —— https://developer.download.nvidia.com/shaderlibrary/docs/shadow_PCSS.pdf | https://www.th-nuremberg.de/cogr/uploads/Computergrafik/Bavoil-SoftShadowMapping.pdf

5.6 延伸阅读与实践资源

按主题整理一份可直接点开的清单,便于对照动手:

基础实现

  • Shadow Mapping — LearnOpenGL:https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping
  • Point Shadows(全向阴影)— LearnOpenGL:https://learnopengl.com/Advanced-Lighting/Shadows/Point-Shadows
  • Shadow Mapping Part 1 — OGLDEV:https://ogldev.org/www/tutorial23/tutorial23.html
  • Shadow Mapping Part 2 — OGLDEV:https://ogldev.org/www/tutorial24/tutorial24.html
  • Tutorial 16: Shadow mapping — opengl-tutorial:https://www.opengl-tutorial.org/intermediate-tutorials/tutorial-16-shadow-mapping/
  • Omnidirectional Shadow Mapping — GPU Gems Ch.12:https://developer.nvidia.com/gpugems/gpugems/part-ii-lighting-and-shadows/chapter-12-omnidirectional-shadow-mapping

软阴影与滤波

  • Shadow Map Antialiasing(PCF)— GPU Gems Ch.11:https://developer.nvidia.com/gpugems/gpugems/part-ii-lighting-and-shadows/chapter-11-shadow-map-antialiasing
  • Generating Soft Shadows Using Occlusion Interval Maps — GPU Gems Ch.13:https://developer.nvidia.com/gpugems/gpugems/part-ii-lighting-and-shadows/chapter-13-generating-soft-shadows-using-occlusion
  • A Sampling of Shadow Techniques — MJP:https://therealmjp.github.io/posts/shadow-maps/

矩方法与 VSM

  • Variance Shadow Maps 原文 PDF(Donnelly & Lauritzen):http://igm.univ-mlv.fr/~biri/Enseignement/MII2/Donnees/variance_shadow_maps.pdf
  • Moment Shadow Mapping — Christoph Peters:http://momentsingraphics.de/I3D2015.html
  • 矩方法论文索引 — Christoph Peters:https://momentsingraphics.de/Publications.html
  • selfshadow 论文与课程索引 — Stephen Hill:https://blog.selfshadow.com/publications/
  • SIGGRAPH 2013 物理着色课程 — selfshadow:https://blog.selfshadow.com/publications/s2013-shading-course/
  • SIGGRAPH Advances 课程归档(历年实时渲染前沿课程):https://advances.realtimerendering.com/
  • selfshadow 博客主页(Lauritzen 的 VSM 系列讨论多发布于此):https://blog.selfshadow.com/

深度精度与深度偏移

  • Depth Precision Visualized — Nathan Reed, NVIDIA:https://developer.nvidia.com/content/depth-precision-visualized
  • Attack of the depth buffer — M. Pettineo:https://mynameismjp.wordpress.com/2010/03/22/attack-of-the-depth-buffer/
  • Maximizing Depth Buffer Range and Precision — Outerra:http://outerra.blogspot.com/2012/11/maximizing-depth-buffer-range-and.html
  • A couple of notes about Z — Emil Persson:https://www.humus.name/index.php?ID=255
  • Depth testing — LearnOpenGL:https://learnopengl.com/Advanced-OpenGL/Depth-testing
  • The Visibility Problem and the Depth Buffer — Scratchapixel:https://www.scratchapixel.com/lessons/3d-basic-rendering/rasterization-practical-implementation/visibility-problem-depth-buffer-depth-interpolation.html
  • glPolygonOffset — docs.gl:https://docs.gl/gl4/glPolygonOffset
  • glClipControl — docs.gl:https://docs.gl/gl4/glClipControl
  • glDepthRange — docs.gl:https://docs.gl/gl4/glDepthRange
  • Depth Bias — Microsoft D3D11:https://learn.microsoft.com/en-us/windows/win32/direct3d11/d3d10-graphics-programming-guide-output-merger-stage-depth-bias
  • Vulkan Spec — Depth Bias:https://docs.vulkan.org/spec/latest/chapters/primsrast.html#primsrast-depthbias
  • Vulkan Spec — Fragment Operations:https://docs.vulkan.org/spec/latest/chapters/fragops.html

级联与引擎文档

  • Cascaded Shadow Maps — Microsoft DirectX Tech Article:https://learn.microsoft.com/en-us/windows/win32/dxtecharts/cascaded-shadow-maps
  • Common Techniques to Improve Shadow Depth Maps — Microsoft:https://learn.microsoft.com/en-us/windows/win32/dxtecharts/common-techniques-to-improve-shadow-depth-maps
  • Cascaded Shadow Mapping — LearnOpenGL Guest Article:https://learnopengl.com/Guest-Articles/2021/CSM
  • Perspective Shadow Maps: Care and Feeding — GPU Gems Ch.14:https://developer.nvidia.com/gpugems/gpugems/part-ii-lighting-and-shadows/chapter-14-perspective-shadow-maps-care-and-feeding
  • Real-time shadows — Unity Manual:https://docs.unity3d.com/Manual/shadow-realtime.html
  • Implementation details of shadow cascades — Unity:https://docs.unity3d.com/Manual/shadow-cascades-implementation-details.html
  • URP asset reference(Shadows 段)— Unity:https://docs.unity3d.com/Manual/urp/universalrp-asset.html
  • Virtual Shadow Maps — Unreal Engine:https://docs.unrealengine.com/5.3/en-US/virtual-shadow-maps-in-unreal-engine/
  • Efficient Shadow Volume Rendering(对照技术)— GPU Gems Ch.9:https://developer.nvidia.com/gpugems/gpugems/part-ii-lighting-and-shadows/chapter-9-efficient-shadow-volume-rendering

以上链接均经过实际访问确认可打开。少数资料站(如各论文的 ACM DOI 页)存在机器人校验,正文引用时优先给出作者主页或厂商服务器的 PDF 直链,以保证长期可访问。

延伸:点光源的立方体阴影贴图——同一套深度比较,只是把一张图换成六个方向(超出本文正文范围,仅作对照):

点光源的立方体阴影贴图(延伸):六个方向各存一张深度图,采样时按方向选面
图 2-69点光源的立方体阴影贴图(延伸):六个方向各存一张深度图,采样时按方向选面@ LearnOpenGL(CC BY 4.0) · Joey de Vries(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/Shadows/Point-Shadows

它存下来的深度立方体贴图长这样:

深度立方体贴图(延伸):六个面的深度各占一格,比较逻辑与 2D 版本完全一致
图 2-70深度立方体贴图(延伸):六个面的深度各占一格,比较逻辑与 2D 版本完全一致@ LearnOpenGL(CC BY 4.0) · Joey de Vries(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/Shadows/Point-Shadows

5.7 Blocker Search 的采样数怎么定

Blocker Search 是 PCSS 里唯一"新增"的开销,它的采样数 $m$ 决定了两件事:能否找到足够的遮挡体、以及总成本。

Blocker Search 采样数与遮挡估计准确度
图 2-71Blocker Search 采样数与遮挡估计准确度的关系:采样不足时半影宽度抖动@ 本系列绘制
Blocker 采样数 表现 适用
4–8 遮挡体估计噪声大,半影宽度抖动 低成本档
16 多数场景稳定 常规推荐
32+ 稳定但成本接近一次完整 PCF 影视级

关键限制在于:Blocker Search 的搜索范围是固定的,不随滤波半径变化。这意味着当遮挡体在阴影贴图上非常稀疏(例如细长物体的投影)时,固定范围内的采样可能一个都没打中遮挡体,此时估计退化为"无遮挡"——表现为阴影局部消失。

工程上的兜底是给一个最小半影宽度,让"完全无遮挡"与"局部无遮挡"之间不至于跳变。

5.8 用矩方法加速 Blocker Search

Blocker Search 需要多次采样才能估计遮挡体平均深度。但如果阴影贴图存的是矩(第四章的 VSM),那么遮挡体的平均深度可以直接由矩反推,无需逐次采样:

VSSM 用矩加速 Blocker Search
图 2-72VSSM 用矩反推遮挡体平均深度,把 Blocker Search 从采样变成解析求值@ 本系列绘制

这就是 VSSM(Variance Soft Shadow Mapping)的核心:它把 PCSS 的 Blocker Search 换成了 VSM 的矩,从而在保持 PCSS 软阴影质量的同时大幅降低成本。论文给出的三场景总耗时是 Balls 7.7、Sponza 9.8、Soldier 32.8(含 GBuf / SM / HSM / SAT 分项),这些数字在第七章还会用到。

这条路线绕开了取舍:它把两者的优点串起来——PCSS 提供物理正确的半影,VSM 提供廉价的遮挡估计。

屏幕空间 PCSS 与光空间 PCSS 的差异
图 2-73屏幕空间 PCSS 与光空间 PCSS:计算发生在哪个空间,决定了能拿到什么信息@ 本系列绘制

INRIA 的屏幕空间 PCSS 工作把 PCSS 放进屏幕空间渲染循环,声称比传统 PCF 软阴影更快且视觉质量相近,是这条"换空间"思路的代表 —— https://hal.inria.fr/inria-00536256/file/paper.pdf

PCSS 的原始论文则同时有 ACM 版本与 NVIDIA 服务器的 PDF 直链,后者更便于长期访问 —— https://http.download.nvidia.com/developer/presentations/2005/SIGGRAPH/Percentage_Closer_Soft_Shadows.pdf

5.9 仍建在逐次比较之上

PCSS 解决了"软硬不均"的问题,但它仍然建立在逐次比较之上,因此继承了 PCF 的两个固有代价:无法预过滤、bias 仍需手工调。真正想把"逐次比较"这件事换掉,需要换一套表示——这就是下一章的矩方法。

四、矩方法:VSM 的切比雪夫上界

6.1 从「比较」到「估计概率」

VSM(Variance Shadow Maps)的思路是一次换表示:不再存"这里有一个深度",而是存 $\text{depth}$ 与 $\text{depth}^2$ 两个矩,用切比雪夫不等式(Chebyshev's Inequality)算出"被遮挡的概率上界"。由于存的是可线性滤波的统计量,整张图可以被模糊、可以走 mipmap、可以走各向异性过滤——PCF 做不到的事它都能做。

6.2 原理:从"比较"到"估计概率"

VSM 把可见性问题重述为概率问题:

$$ P(x \ge t) \le \frac{\sigma^2}{\sigma^2 + (t - \mu)^2} $$

其中:

  • $t$ 是当前像素的深度。
  • $\mu$ 是该点邻域内深度的一阶矩(均值)。
  • $\sigma^2$ 是该邻域内深度的二阶中心矩(方差),由 $\sigma^2 = M_2 - \mu^2$ 求得,$M_2$ 为 $\text{depth}^2$ 的均值。
  • 右侧即切比雪夫上界,直接当作该点的阴影强度使用。

由于 $\mu$ 与 $M_2$ 都是线性可滤波的量,把阴影贴图做一次模糊,等于对整片邻域求矩——滤波半径不再需要逐次采样。

VSM 的两趟渲染管线
图 2-74VSM 的两趟渲染:第一趟写双矩通道,第二趟查表求上界@ 本系列绘制

作为对照,下面这张是传统阴影贴图在离线渲染中的表现,可以看到软阴影与自遮挡处理得当时的参考质量:

环境光遮蔽中的遮挡关系
图 2-75环境光遮蔽:遮挡关系如何影响最终光照,与阴影贴图解决的是同一类可见性问题@ Wikimedia Commons · Philip Rideout(CC BY-SA 3.0)· https://commons.wikimedia.org/wiki/File:Ambient_Occlusion.PNG
切比雪夫上界与最小方差钳制
图 2-76切比雪夫上界曲线与最小方差钳制对漏光的抑制@ 本系列绘制

6.3 代码:VSM 的渲染与采样

双矩通道的写入与读取
图 2-77双矩通道:第一趟写 depth 与 depth²,第二趟读回求方差@ 本系列绘制
//-----------------------------------【VSM 双矩渲染】-----------------------------------------
// 描述:第一趟写 depth 与 depth^2 两个通道
//--------------------------------------------------------------------------------------------
//【1】写双矩
out vec2 moments = vec2(gl_FragCoord.z, gl_FragCoord.z * gl_FragCoord.z);

//【2】采样时用切比雪夫上界
float chebyshevUpperBound(vec2 moments, float t) {
    if (t <= moments.x) return 1.0;                       // 完全受光
    float variance = moments.y - moments.x * moments.x;    // σ² = M₂ - μ²
    variance = max(variance, minVariance);                 // ← 最小方差钳制
    float d = t - moments.x;
    return variance / (variance + d * d);
}

上面代码中,第 6 行是 VSM 能实用的关键:最小方差钳制。若不钳制,当邻域内深度完全一致时 $\sigma^2 = 0$,上界退化为 0/0——表现为亮处出现黑色漏光。钳制给方差设一个下限,优雅地解决了这个问题,这也是 VSM 相对早期矩方法最实用的改进。

最小方差钳制对漏光的抑制
图 2-78最小方差钳制:给方差设下限,避免 0/0 导致的黑色漏光@ 本系列绘制

6.4 该方法的失效边界

两阶矩只能描述单峰分布。当同一片邻域内出现两个分离的深度层(例如前景物体与背景墙共处一个滤波核),矩方法无法表达这种双峰,上界会失真:

深度分布与矩的表达能力
图 2-79深度分布与矩的表达能力:单峰可描述,双峰失真@ 本系列绘制
  • 漏光(Light Bleeding):前景的阴影"渗出"到背景上,出现与实际不符的暗区
  • 过度变亮:分离深度层被平均后,本应全暗的区域变得半亮

这是 VSM 的结构性失效,不是调参能修好的。缩紧滤波核能缓解,但也就失去了它"可大幅过滤"的优势。第五章的 ESM 与后续的矩方法改进,都在这个问题上做文章。

6.5 SAVSM:任意矩形滤波区的恒定代价

VSM 的双矩通道为"可大幅过滤"提供了前提,但用什么方式过滤仍影响成本。GPU Gems 3 第 8 章给出的 SAVSM(Summed-Area Variance Shadow Maps)用求和面积表(Summed-Area Table)实现任意矩形滤波区的恒定代价查询:

过滤方式 代价 特点
高斯模糊 与核半径线性相关 边缘柔和,需额外模糊趟
mipmap 采样 对数级 有块状伪影
求和面积表 O(1)(与核面积无关) 任意矩形核,代价恒定

恒定代价这一点很关键:它意味着"想软到什么程度"不再受成本约束——这与 PCF 形成鲜明对照,后者的核面积每翻一倍,采样数翻四倍。GPU Gems 3 第 8 章的 Figure 8-13 直接对比了硬件 PCF、blurred VSM 与 SAVSM 的相对性能,并强调三者都工作在图像空间、基本与几何复杂度无关。

求和面积表的 O(1) 查询
图 2-80求和面积表:任意矩形区域的求和只需四次查表,与面积无关@ 本系列绘制

6.6 双峰问题:一个具体例子

抽象描述不好理解,看一个具体场景。假设滤波核(例如 5×5 邻域)内包含两块表面:

纹素 深度值 含义
60% 的纹素 0.30 前景物体表面
40% 的纹素 0.70 背景墙面

矩方法据此算出 $\mu = 0.46$、$\sigma^2 \approx 0.038$。现在屏幕上某点在深度 0.68(位于背景墙面上,理应只受前景物体遮挡):

代入切比雪夫上界,得到的遮挡概率是一个明显小于 1 的值——背景墙上本应完全处于阴影的区域,被算成了半亮。这就是矩方法无法表达双峰分布的直观后果。

反过来,如果屏幕点在深度 0.28(前景物体朝光的一面,理应完全受光),矩估计也可能给出偏暗的结果,造成前景物体自身出现虚假阴影。

VSM 双峰分布导致的漏光示意
图 2-81VSM 在双峰深度分布下的漏光与虚假阴影@ 本系列绘制

这两种错误的方向相反,且无法靠调整任何单一参数同时消除——缩核能同时缓解两者,但缓解的是程度,不是结构。

矩方法的三类失效模式
图 2-82矩方法的三类失效模式:漏光、虚假自阴影与过度平滑@ 本系列绘制

6.7 后续工作:更高阶的矩

既然两阶矩不够,自然的想法是多加几阶。Improved Moment Shadow Maps(JCGT)指出 VSM 实际使用的基是 $(1, z, z^2)$,并给出用更高阶矩重建可见性分布的方法。Christoph Peters 的矩方法论文序列(含 I3D 2015 的 Moment Shadow Mapping)是这条线的权威出处 —— http://momentsingraphics.de/I3D2015.html | https://momentsingraphics.de/Publications.html | https://blog.selfshadow.com/publications/

矩方法的阶数与表达能力
图 2-83矩的阶数与可表达的深度分布复杂度@ 本系列绘制

代价是存储与带宽成比例上升:$n$ 阶矩需要 $n+1$ 个通道。在延迟渲染本就吃带宽的前提下(见本系列第一篇对 G-Buffer 带宽的分析),这笔账要仔细算——矩方法的所有优势,都建立在"额外的通道换免调参"这个交易上。

6.8 一张对照图

PCF 与 VSM 的边缘处理对照
图 2-84PCF 逐次比较与 VSM 一次查表在阴影边缘上的处理差异@ 本系列绘制

6.9 免调参的代价是近似

VSM 用近似换掉了调 bias:它不需要手工偏移,因为方差本身就描述了深度波动。这也是回答开篇那个问题的一半——为什么主流引擎不全换 VSM?因为近似有失效边界,而失效方式(漏光)在美术上比 acne 更难接受。

五、ESM 与矩方法的边界

7.1 换一个基函数

ESM(Exponential Shadow Maps)换了一个更好的基函数:用指数函数近似可见性,以更少的存储获得更好的质量与性能。但它的代价更"硬"——当接收面深度小于参考深度时,指数近似会直接失效,必须回退到 PCF 或另做处理。

7.2 原理:指数化的可见性

ESM 的核心是把遮挡函数写成指数形式,使得线性滤波后仍能恢复出可用的可见性估计。相比 VSM 的两阶矩,指数形式在多数区间上的近似误差更小,因此可以在更小的滤波核下得到更平滑的结果。

具体做法是:渲染时把深度写成 $e^{-c \cdot d}$($c$ 为可调常数),采样时用两个指数值相除得到可见性:

$$ V = \frac{e^{-c \cdot d_{\text{occluder}}}}{e^{-c \cdot d_{\text{receiver}}}} $$

其中:

  • $d_{\text{occluder}}$ 是该纹素存的遮挡体深度。
  • $d_{\text{receiver}}$ 是当前像素的深度。
  • $c$ 是指数常数,它直接决定近似的有效区间——$c$ 越大,近似越陡峭、越接近硬阴影;$c$ 越小,越平滑但越容易饱和。

由于 $e^{-c \cdot d}$ 是线性可滤波量(它的滤波结果对应几何平均而非算术平均,这正是近似误差的来源),整张图可以像 VSM 一样做大幅模糊。

VSM 与 ESM 的近似曲线对照
图 2-85VSM 的矩近似与 ESM 的指数近似对照:ESM 在更小核下误差更小@ 本系列绘制

一个高质量的离屏渲染能直观展示"正确的软阴影与间接光"应该长什么样,作为实时方案的目标参照:

离屏渲染的透明与间接光
图 2-86离屏渲染中的透明与折射:间接光与软阴影的质量参照@ Wikimedia Commons · Morn(CC0)· https://commons.wikimedia.org/wiki/File:DBW-Render_glass_scene,_recreated_in_POV-Ray.png

同一类“离线标准答案”的另一张对照:

路径追踪离线渲染:软阴影与间接光的正确解
图 2-87路径追踪离线渲染:软阴影与间接光的正确解@ KaiaVintr(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File:Rendering_techniques_example,_path_tracing,_high_quality,_Blender_Cycles.png

7.3 代价:有条件的近似

ESM 的近似只在接收面深度不小于参考深度时成立。一旦越界(例如物体紧贴或穿插),近似值会急剧偏离,出现整片错误的暗区。

ESM 指数近似的有效区间
图 2-88ESM 指数近似的有效区间与越界失效@ 本系列绘制

这意味着 ESM 必须配备回退路径:越界时退回 PCF。工程上这不算严重问题——多一次分支判断即可——但它说明了一件事:凡是"用统计量近似可见性"的方法,都必须清楚自己的有效区间。

7.4 三种方法的取舍

方法 存储 可线性过滤 手工调参 主要失效
PCF 1 张深度 ✗ bias 必须手调 掠射角 acne、边缘软硬不均
VSM 2 张(depth + depth²) ✓ 基本免调 双峰分布导致漏光
ESM 1~2 张 ✓ 基本免调 深度越界时近似失效
PCSS 1 张深度 ✗ bias 仍需手调 Blocker Search 在稀疏遮挡下失准
四种方法的存储与带宽对照
图 2-89四种方法的存储与带宽占用对照@ 本系列绘制

参照 Soft Shadow Mip-maps 论文的结论:ESM 以更少存储获得更好质量与性能,但需要在越界时回退。这也解释了为什么工程实践中常见的是"PCF 打底 + 关键场景上 VSM/ESM"的混合方案,而不是全盘替换。

7.5 一个容易忽略的细节:指数常数与场景尺度耦合

$c$ 的取值与场景的深度范围强相关。若把同一套参数从室内场景搬到室外大场景,$c$ 的相对尺度会失效——指数近似对"深度单位"敏感,而 PCF/VSM 对深度单位不敏感(VSM 的切比雪夫上界只用到深度的相对差值)。

这带来一个跨项目复用上的实际差别:VSM 的参数换个场景基本能沿用,ESM 的 $c$ 往往要重调。

指数常数 c 对近似区间的影响
图 2-90指数常数 c 决定近似的陡峭程度与有效区间,并与场景深度尺度耦合@ 本系列绘制

7.6 代码:越界回退

//-----------------------------------【ESM 越界回退】-----------------------------------------
// 描述:指数近似在接收面深度小于参考深度时失效,需回退
//--------------------------------------------------------------------------------------------
//【1】正常区间用 ESM
float esmShadow(float occluder, float receiver) {
    return clamp(exp(-ESM_C * occluder) / exp(-ESM_C * receiver), 0.0, 1.0);
}

//【2】越界时回退到 PCF
float shadow = (receiver < occluder) ? esmShadow(occluder, receiver)
                                     : pcfShadow(projCoords, receiver, bias);

7.7 主线到这里已经清楚

第三章到第五章走完,主线已经清楚:每一次"消掉手工调参"的尝试,都换来了一个需要判断有效区间的新近似。下一章换个维度——不再讨论"一个像素怎么算阴影",而是讨论"方向光的阴影贴图该覆盖多大范围"。

六、CSM:方向光的分辨率分配难题

8.1 当覆盖范围可以是无限大

方向光(Directional Light)没有位置,它的覆盖范围可以是无限大。如果只用一张阴影贴图覆盖整个可见场景,那么在 2048² 的预算下,一个纹素要覆盖很大一块世界空间——近处细节全糊。CSM(Cascaded Shadow Maps)的解法是把视锥切成几段,每段配一张独立的正交投影阴影贴图,按距离分配分辨率。

方向光的阴影模式在引擎里是显式可选的:

方向光的阴影模式:从单张贴图到 PSSM 分级,引擎把它做成了开关
图 2-91方向光的阴影模式:从单张贴图到 PSSM 分级,引擎把它做成了开关@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/lights_and_shadows.html

阴影贴图覆盖范围之外的区域,需要单独处理:

阴影贴图边界(clamp edge):超出覆盖范围的采样需夹到边缘,避免错误遮挡
图 2-92阴影贴图边界(clamp edge):超出覆盖范围的采样需夹到边缘,避免错误遮挡@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping

覆盖范围定得再大也是有限的,范围之外没有深度可比:

覆盖范围之外:光源视锥未覆盖的区域取不到深度样本,会出现错误遮挡
图 2-93覆盖范围之外:光源视锥未覆盖的区域取不到深度样本,会出现错误遮挡@ LearnOpenGL(CC BY 4.0) · Joey de Vries(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping

把边界处的采样夹到极值是一种修法,代价是交界处出现过采样:

边缘过采样修正:改用边缘纹理采样后,交界处的采样结果被拉回正常
图 2-94边缘过采样修正:改用边缘纹理采样后,交界处的采样结果被拉回正常@ LearnOpenGL(CC BY 4.0) · Joey de Vries(CC BY 4.0)· https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping

8.2 原理:把一维的距离问题切成段

近处的物体在屏幕上占的像素多,远处的占得少。所以分辨率也该按这个比例分配:

CSM 级联分区示意
图 2-95CSM 的级联分区:视锥按距离切段,近段分得更高分辨率@ 本系列绘制

每一级联(Cascade)是一张独立的阴影贴图,各自用一套正交投影矩阵,只覆盖自己负责的那段视锥。渲染时按像素到相机的距离选择对应的级联,再在级联交界处做混合以避免接缝。

来看一张真实 CSM 调试视图,直观感受"切段"发生了什么:

CSM 级联分区的真实调试可视化
图 2-96CSM 级联分区可视化:上为各级联在相机视锥内的分配,下为单一级联的阴影贴图纹理空间@ Joey de Vries · LearnOpenGL(CC BY 4.0)· https://learnopengl.com/Guest-Articles/2021/CSM/

级联落进真实场景后是这样——下面是同一套 CSM 的示例工程画面:

CSM 落进真实场景:示例工程的渲染画面
图 2-97CSM 落进真实场景:示例工程的渲染画面@ Joey de Vries(CC BY 4.0)· https://learnopengl.com/Guest-Articles/2021/CSM

注意三级视图的分工:顶部显示整块阴影贴图被切成了多段级联、各自覆盖视锥的一部分;中间那张是所有级联在相机视锥(俯视)里的叠加,可以看到近处的级联小而密、远处的级联大而稀——这正是"近处占像素多、远处占得少"的分配逻辑;底部是单一级联的纹理空间,剩余大量空白说明这段的分辨率有富余。

8.3 关键抉择:级联怎么切

切法有两种思路,各有代价:

切法 做法 代价
fit-to-scene 正交投影范围贴合整个场景包围盒 简单稳定,但阴影分辨率被场景规模稀释
fit-to-cascade 每级联各自贴合该段视锥的包围盒 分辨率利用率高,但相机转动时投影会抖

PSSM(Parallel-Split Shadow Maps)给出了第三种方案——practical split scheme:把对数切分与均匀切分加权混合,默认权重 $\lambda = 0.5$:

$$ d_i = \lambda \cdot d_{\text{near}} \left(\frac{d_{\text{far}}}{d_{\text{near}}}\right)^{i/N} + (1-\lambda) \cdot \left(d_{\text{near}} + \frac{i}{N}(d_{\text{far}} - d_{\text{near}})\right) $$

其中:

  • $d_i$ 是第 $i$ 段的远平面距离,$N$ 是级联数量。
  • 第一项是对数切分,让近处的段更密(符合屏幕像素密度随距离递减的规律)。
  • 第二项是均匀切分,避免远处段被拉得过长。
  • $\lambda = 0.5$ 是论文给出的默认经验值。

这个混合的意义在于:纯对数切分能让近处分辨率极高,但最远一段会覆盖过大的距离;纯均匀切分则浪费了近处的分辨率。加权后两者兼顾。

CSM 与 PSSM 切分方式对照
图 2-98均匀切分、对数切分与 PSSM 加权切分的对照@ 本系列绘制

8.4 级联选择的两种实现

//-----------------------------------【级联选择】---------------------------------------------
// 描述:interval-based 按视锥距离直接选段,实现简单、切换处需混合
//--------------------------------------------------------------------------------------------
//【1】按视锥距离确定级联索引
int cascadeIndex = 0;
for (int i = 0; i < CASCADE_COUNT - 1; ++i) {
    if (viewDepth > cascadeSplits[i]) cascadeIndex = i + 1;
}

//【2】在段边界附近做混合,消除接缝
float blendWidth = cascadeSplits[cascadeIndex] * 0.1;
float shadow = mix(shadowPrev, shadowCurr, blendFactor);

两种实现的分野在于"用深度选段"还是"用纹理坐标选段"(map-based):前者实现简单、成本低,但相机转动时可能选到不合适的段;后者对每段都算一次投影,更精确但更贵。

8.5 级联数量该取几

级联数量是成本与质量的直接杠杆。每多一级联就多一张阴影贴图、多一趟渲染,但分辨率分配更精细:

级联数量与分辨率分配的取舍
图 2-99级联数量对分辨率分配与渲染开销的影响@ 本系列绘制
级联数 近处精度 渲染开销 适用
1 低(等同单张贴图) 1× 小场景、远景为主
2 中 2× 移动端常见取舍
4 高 4× PC 平台常规默认
8 很高 8× 大世界、影视级

数量增多还会带来一个副作用:切换点的接缝数量随之增加。4 级联有 3 个切换边界,8 级联有 7 个——每一个都可能是可察觉的接缝。所以"更多级联"不是单调更优,工程上通常在 4 附近收敛。

8.6 投影抖动:一个被低估的问题

当相机移动时,每级联的正交投影范围会随视锥变化而滑动,导致阴影贴图的纹素网格相对世界空间持续移动。结果是阴影边缘出现"爬行"(Shadow Crawling)——看起来像阴影在轻微抖动。

CSM 投影抖动与纹素捕捉
图 2-100级联投影随相机移动导致纹素网格滑动,引发阴影爬行@ 本系列绘制

标准对策是纹素捕捉(Texel Snapping):把每级联的正交投影中心吸附到纹素网格的整数倍上,使纹素相对世界空间稳定。代价是投影范围会有微小偏移,但换来了稳定的边缘。

这个问题的存在说明:CSM 的复杂度不只来自"切几段",还来自"段怎么跟着相机走"。

级联覆盖范围的几何关系
图 2-101级联覆盖范围的几何关系:每段的近远平面与正交投影盒@ 本系列绘制

级联之间的过渡同样需要处理——直接切换会在边界留下可见的接缝:

级联边界的混合带
图 2-102级联边界处的混合带:在两段重叠区内按距离插值,消除接缝@ 本系列绘制

级联之间的接缝靠混合区抹平,引擎里的划分是这样的:

级联 split 的混合区:相邻级联在边界处重叠、渐变过渡
图 2-103级联 split 的混合区:相邻级联在边界处重叠、渐变过渡@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/lights_and_shadows.html

8.7 投影类方法的谱系

CSM 并非唯一解。围绕"如何让方向光的阴影贴图更贴合视锥",还出现过一串投影变形方案:

方向光阴影投影方法谱系
图 2-104方向光阴影投影方法的谱系:PSM / LiSPSM / TSM / PSSM@ 本系列绘制
方法 核心变形 主要问题
PSM 透视投影后置变换,让近处纹素更密 靠近相机处出现奇异性
LiSPSM 在光空间做透视变换 实现复杂,仍有伪影
TSM 用梯形近似视锥投影 特殊情况需专门处理
PSSM 直接切分视锥,各段独立正交投影 无奇异性,无需特殊处理

PSSM 之所以在工业界胜出,正是因为它在保持分辨率分配效果的同时,避开了所有变形方案共同的麻烦——不需要为奇异性写额外的处理分支。

关于这一族方法的取舍,GPU Gems 第 14 章专门讨论过 PSM 的使用注意事项,而微软的 CSM 技术文章与 LearnOpenGL 的 CSM 客座章节各自给出了可直接落地的实现路径 —— https://developer.nvidia.com/gpugems/gpugems/part-ii-lighting-and-shadows/chapter-14-perspective-shadow-maps-care-and-feeding | https://learnopengl.com/Guest-Articles/2021/CSM | https://developer.nvidia.com/gpugems/gpugems3/part-ii-light-and-shadows

8.8 预算分配与单像素算法正交

CSM 解决的是预算分配问题,与前三章的"单像素算法"正交。这也是为什么工程实践中两者常常叠加使用:用 CSM 管方向光的大范围覆盖,用 PCF 或 VSM 管每张级联贴图内部的滤波质量。

CSM 与单像素算法的正交关系
图 2-105CSM 与滤波算法的正交关系:前者管预算分配,后者管单像素质量@ 本系列绘制

七、工程实践:引擎怎么落地

9.1 公式到了引擎里变成几个参数

论文里的公式到了引擎里会变成几个参数。理解这些参数在引擎中的映射,比记住公式更有用——尤其是 bias 那一组,它们是第一章那个两难的工程化表述。

公式到了引擎里,落成的就是这样一个面板:

引擎的光源参数面板:公式里的每一项,在这里都变成了一个可调的数值
图 2-106引擎的光源参数面板:公式里的每一项,在这里都变成了一个可调的数值@ Godot 官方文档(CC BY 4.0) · Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/lights_and_shadows.html

9.2 参数映射:三个引擎的对照

Unity URP 的阴影配置是最直白的样本:

引擎参数映射对照
图 2-107三个引擎的阴影参数映射对照@ 本系列绘制

方向光的阴影部分再展开一层是这样,与上表逐项对得上:

方向光的阴影参数:偏移、边缘裁剪、级联切分在引擎里对应的开关与数值
图 2-108方向光的阴影参数:偏移、边缘裁剪、级联切分在引擎里对应的开关与数值@ Godot 官方文档(CC BY 4.0) · Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/lights_and_shadows.html
引擎 阴影贴图组织 偏移参数 官方提示
Unity URP 点光 6 张(cubemap 面)、聚光 1 张、方向光每级联 1 张;punctual 光与方向光各用一个图集 Depth Bias / Normal Bias / Near Plane 高 bias 会导致 light leaking,阴影与投射体间出现可见间隙
Godot 每光源一张(含立方体) Shadow Bias / Shadow Normal Bias 明确 bias 过低→acne、过高→peter-panning;建议优先增大 Normal Bias
Unreal 传统 CSM + per-object shadow + 预计算阴影;VSM 开启后替换上述多种方式 各方式各有偏移组 VSM 替代 CSM 是"替换关系",不是叠加

Godot 那条建议要单独抄下来:优先调 Normal Bias 而不是 Bias——它沿法线内缩采样点,不引入深度方向的整体偏移,压住 acne 的同时不会造成明显脱开。

三个引擎各自的正式文档入口如下,参数含义以官方说明为准:Unreal 的阴影配置说明 —— https://dev.epicgames.com/documentation/en-us/unreal-engine/shadowing-in-unreal-engine ;Unity 早期的阴影总览(其中对 acne 与 bias 的表述至今仍常被引用) —— https://docs.unity3d.com/ru/530/Manual/ShadowOverview.html ;Godot 文档的源文件(便于查看参数的完整说明与边注) —— https://github.com/godotengine/godot-docs/blob/master/tutorials/3d/lights_and_shadows.rst

9.3 API 层:深度偏移的三个因子

偏移在图形 API 层的落地是光栅化状态的一部分。以 Vulkan 为例,由 VkPipelineRasterizationStateCreateInfo 的三个字段控制:

字段 作用
depthBiasConstantFactor 常数偏移,对应固定 bias
depthBiasSlopeFactor 斜率相关偏移,对应 Slope-Scale Bias(正比于 $\tan\theta$)
depthBiasClamp 偏移上限,防止掠射角下偏移过大

更精细的控制由扩展 VK_EXT_depth_bias_control 提供(新增 vkCmdSetDepthBias2EXT),允许在命令缓冲中动态调整。

对照第一章的公式可以看出:depthBiasSlopeFactor 正是把 $1/|\cos\theta|$ 那一项工程化了,而 depthBiasClamp 则是对它失控的兜底——这个 clamp 的存在本身就承认了公式在掠射角处不可用。OpenGL 侧的对应机制是 glPolygonOffset,Direct3D 侧则是深度偏移状态 —— https://docs.gl/gl4/glPolygonOffset | https://learn.microsoft.com/en-us/windows/win32/direct3d11/d3d10-graphics-programming-guide-output-merger-stage-depth-bias | https://docs.vulkan.org/spec/latest/chapters/primsrast.html#primsrast-depthbias

9.4 性能数据:只引用可复现的出处

Shadow Map 的性能数据在社区里流传很广,但多数横评不交代测试环境。本文只引用三处可追溯的实测:

各方法相对性能对照
图 2-109各方法的相对性能对照,数据取自 GPU Gems 3 与 VSSM 论文@ 本系列绘制
出处 对比内容 可引用结论
GPU Gems 3 第 8 章 Figure 8-13 PCF(硬件加速)、blurred VSM(含/不含 shadow multisampling)、SAVSM 三者均在图像空间,基本与几何复杂度无关
VSSM 论文(Laurinzen & McCool, PG 2010) 三场景分项耗时(GBuf / SM / HSM / SAT) Balls total 7.7;Sponza total 9.8;Soldier total 32.8
Soft Shadow Mip-maps 论文 CSM / VSM / ESM 的存储与质量 ESM 以更少存储获得更好质量与性能,越界需回退 PCF

社区里那些"PCF vs VSM 帧率横评"多数不交代显卡型号、分辨率与场景,本文不采信。这个取舍标准本身也可以画成一条判断线:

引用性能数据的可信度判断
图 2-110性能数据的可信度判断:交代了测试环境与可复现条件的才纳入@ 本系列绘制

9.5 图集:为什么要把多张阴影贴图拼在一起

Unity URP 的做法点出了一个工程细节:点光 6 张、聚光 1 张、方向光每级联 1 张,但 punctual 光与方向光各自只用一个图集。

阴影贴图集的组织方式
图 2-111阴影贴图集:把多张阴影图拼进一张纹理以减少状态切换@ 本系列绘制

这么做的理由是减少纹理绑定与状态切换:一个场景若有 8 盏聚光灯,逐光绑定 8 张纹理意味着一帧内 8 次状态变更;拼成一张图集后只需绑定一次,采样时用偏移区分。代价是图集内各块的边距需要额外处理,且动态光源数量变化时图集要重排。

这是本文里少见的"纯工程、无算法"的优化点——但它在实际帧率上的影响往往比换个滤波算法更大。

阴影通道在一帧中的时序位置
图 2-112阴影渲染通道在一帧中的时序位置:与主渲染通道的关系@ 本系列绘制

对照一张低质量光栅化渲染,可以看出分辨率与采样策略的取舍对最终观感的影响,往往大于算法本身的选择:

低质量光栅化渲染对照
图 2-113低质量光栅化渲染:采样与分辨率不足带来的观感损失@ Wikimedia Commons · KaiaVintr(CC BY-SA 4.0)· https://commons.wikimedia.org/wiki/File:Rendering_techniques_example,_rasterization,_low_quality,_Blender_EEVEE.png
阴影带宽预算的构成
图 2-114阴影相关的带宽预算构成:写深度、读阴影图、多光源叠加@ 本系列绘制

多张阴影贴图拼成图集时,空间是这样分配的:

阴影图集(shadow atlas)的象限分配:每格可再细分给不同光源
图 2-115阴影图集(shadow atlas)的象限分配:每格可再细分给不同光源@ Godot Engine documentation(CC BY 4.0)· https://docs.godotengine.org/en/stable/tutorials/3d/lights_and_shadows.html

9.6 值得警惕的一个组合:VSM 与 MSAA

VSM 需要对阴影贴图做预过滤,而多重采样抗锯齿(MSAA)作用于主画面——两者并不直接冲突,但 VSM 的双矩通道在 MSAA 下需要逐样本写矩,否则过滤出来的方差是错的。这类"两个正确的东西凑在一起出错"的问题,是矩方法落地时的主要摩擦。

各方法选型决策树
图 2-116阴影方案选型决策树:按光源类型、场景规模与质量要求分流@ 本系列绘制

9.7 参数是公式的接口

工程实践这一章说明了一件事:参数是公式的接口,而 clamp 的存在本身就承认公式有边界。这也解释了为什么"读论文"与"调引擎"之间总有一道沟——论文给的是理想条件下的推导,引擎给的是带兜底的近似。

从论文公式到引擎参数的映射链
图 2-117从论文公式到引擎参数的映射链:每一步都引入一次兜底@ 本系列绘制

八、选型建议与未来展望

10.1 没有一种方法能通吃

没有一种阴影方法在所有场景下最优。选型的依据是三件事:光源类型、场景规模、以及这个项目更怕 acne 还是更怕漏光。

10.2 横向对比总表

阴影方法横向对比矩阵
图 2-118阴影方法横向对比矩阵:质量、成本、调参、失效模式@ 本系列绘制
维度 PCF PCSS VSM ESM CSM
阴影类型 硬/可调软 自适应软 软 软 硬(分辨率分配)
采样成本 $n^2$ $m + n^2$ $O(1)$(查表) $O(1)$ 每级联各一套
可预过滤 ✗ ✗ ✓ ✓ ✗
手工调参 bias 必调 bias 必调 免调 免调 精度分配
主要失效 掠射角 acne Blocker 稀疏 双峰漏光 深度越界 级联接缝
适用场景 通用打底 影视级软阴影 中距离软阴影 大范围软阴影 方向光必配

10.3 推荐的组合方式

从实践看,稳定的组合是分层的:

单张阴影贴图与 CSM 的覆盖对比
图 2-119单张阴影贴图与 CSM 的覆盖对比:分辨率被摊薄 vs 分段分配@ 本系列绘制
方向光  → CSM 管覆盖范围(4 级联起步,远端可降到 2 级联)
每张级联 → PCF 打底(3×3 或 5×5,硬件比较采样器)
关键镜头 → 在 CSM 之上叠 VSM/ESM 做软阴影
阴影偏移 → Slope-Scale 为主 + 小常数 Normal Bias,clamp 兜底

这个组合的逻辑是:用成熟手段覆盖 90% 的情况,把近似方法的失效风险限制在可以人工干预的关键镜头上。

落到具体的调参顺序,经验上按这个次序走最省时间:

阴影调参的推荐次序
图 2-120阴影调参的推荐次序:先定分辨率与级联,再定偏移,最后动滤波@ 本系列绘制

10.4 演进时间线

阴影技术演进时间线
图 2-121阴影贴图技术的演进时间线:从提出到矩方法与级联@ 本系列绘制
年份 节点 解决的问题
1978 阴影贴图(Williams) 建立两趟渲染框架
1987 PCF(Reeves et al.) 软化边缘
2001 多阴影贴图思想(Tadamura) 覆盖范围与分辨率分配
2002 PSM(Stamminger & Drettakis) 透视变形提升近处精度
2004 PSSM / LiSPSM / TSM 切分视锥,规避奇异性
2005 PCSS(Fernando) 半影随距离自适应
2006 VSM(Donnelly & Lauritzen) 矩方法,免手工调 bias
2008 ESM(Annen et al.) 更优基函数
2010 VSSM(Lauritzen & McCool) 用矩加速 Blocker Search
近年 Virtual Shadow Maps(Unreal) 按需分页的虚拟化阴影贴图

10.5 回到开篇那个问题

为什么主流引擎不全换 VSM? 现在可以回答了:

VSM 确实消掉了手工调 bias,但它用一个统计近似替换了逐次比较。这个近似在双峰深度分布下失真(漏光),而漏光在美术上比 acne 更难接受——acne 是"多出来的暗斑",可以靠调参压掉;漏光是"阴影出现在了不该在的地方",会让美术对光照的判断整体失准。

于是工程上的选择是:用 PCF 保证不会出现结构性错误,把 VSM/ESM 留给能人工确认的关键镜头。这不是保守,而是把"可预测"放在了"理论更优"之前。

10.6 未来方向:从"优化一张图"到"不生成那张图"

前面八章讨论的所有方法,都建立在同一个前提上:先渲染出一张完整的阴影贴图,再在采样时想办法提高它的质量。而这个前提本身正在被挑战。

阴影技术的两个演进方向
图 2-122阴影技术的两个演进方向:优化采样 vs 虚拟化存储@ 本系列绘制
方向 代表 思路
优化采样 PCSS / VSM / ESM 接受固定分辨率的贴图,改进滤波与近似
虚拟化存储 Virtual Shadow Maps(Unreal) 按需分页,只渲染真正被采样的纹素
距离场近似 Distance Field Soft Shadows(Unreal) 用有符号距离场表达遮挡,很少自相交

第二条路线最有意思:它不再问"怎么把有限的纹素用好",而是问"为什么每一帧都要渲染整张贴图"。Unreal 的 Virtual Shadow Maps 开启后会替换 CSM、per-object shadow 与预计算阴影等多种方式——这是一个信号:当硬件支持稀疏纹理与页表查询后,"优化一张固定分辨率的图"这个框架本身可能被绕开。

第三条路线(距离场)则从另一个角度切入:它用有符号距离场表达遮挡,官方文档强调的特点是很少自相交,从而绕开了远处传统阴影贴图的漏光与过度偏移问题。代价是距离场的构建与内存开销。

10.7 一条贯穿全篇的经验

回头看这九章,有一条经验反复出现:

每当一个方法用近似换掉了手工调参,它就获得了一个必须被理解的失效区间。

  • PCF 用固定核换掉了"每次比较"的复杂性,代价是软硬不均
  • PCSS 用半影估计换掉了核面积的手工选择,代价是 Blocker Search 在稀疏遮挡下失准
  • VSM 用两阶矩换掉了 bias 调参,代价是双峰分布下的漏光
  • ESM 用指数基换掉了更大的滤波核,代价是深度越界时失效
  • CSM 用级联换掉了超高分辨率贴图,代价是接缝与爬行

面对一个新方法时,先别问它有多快,先问它的失效区间在哪、失效时会怎样表现——失效的方式决定了它能否被工程接受。

五种方法的失效区间对照
图 2-123五种方法的失效区间对照:各自的"安全区"与越界表现@ 本系列绘制

10.8 下篇预告

阴影之后,管线的下一站是后处理链。第三篇会讲 Bloom、色调映射与抗锯齿——它们不像阴影那样有独立的几何阶段,而是全部作用在一张已经渲染完的画面上,因此代价模型与本文完全不同。这里有个巧合:本文讲过的 VSM 与下篇的 Bloom 押的是同一件事——模糊是廉价的。



本篇图片来源台账

本文使用的外部图像来自 A 级源——Wikimedia Commons、Godot 官方文档、LearnOpenGL,这些站点均以明确许可公开其图像;许可与作者如下,原始页面可直接点开核对:

文件 来源 作者 许可 原始页面
src_raytrace_high.png Wikimedia Commons KaiaVintr CC BY-SA 4.0 https://commons.wikimedia.org/wiki/File:Rendering_techniques_example,_ray_tracing,_radiosity,_photon_mapping,_POV-Ray.png
src_raster_low.png Wikimedia Commons KaiaVintr CC BY-SA 4.0 https://commons.wikimedia.org/wiki/File:Rendering_techniques_example,_rasterization,_low_quality,_Blender_EEVEE.png
src_direct_lighting.png Wikimedia Commons Barahag CC BY-SA 4.0 https://commons.wikimedia.org/wiki/File:Direct_lighting.png
src_shadowmap_pipeline.png Wikimedia Commons Praetor alpha CC BY-SA 3.0 https://commons.wikimedia.org/wiki/Category:Shadow_mapping
src_shadowmap_failed.png Wikimedia Commons Praetor alpha CC BY-SA 3.0 https://commons.wikimedia.org/wiki/File:5failed.png
src_offline_glass.png Wikimedia Commons Morn CC0 https://commons.wikimedia.org/wiki/File:DBW-Render_glass_scene,_recreated_in_POV-Ray.png
src_ao_one.png Wikimedia Commons Philip Rideout CC BY-SA 3.0 https://commons.wikimedia.org/wiki/File:Ambient_Occlusion.PNG
src_ao_two.jpg Wikimedia Commons The original uploader was TheWusa at German Wikipedia. CC BY-SA 3.0 https://commons.wikimedia.org/wiki/File:Ambient_occlusion.jpg
src_supersampling.png Wikimedia Commons Hrkalona CC BY-SA 4.0 https://commons.wikimedia.org/wiki/File:Fz_supersampling.png
src_lo_pcf_hard.png LearnOpenGL(教程站,CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping
src_lo_pcf_soft.png LearnOpenGL(教程站,CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping
src_lo_csm_cascades.png LearnOpenGL(教程站,CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Guest-Articles/2021/CSM/
src_godot_acne.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/lights_and_shadows.html
src_lo_culling.png LearnOpenGL(CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping
src_godot_peter_panning.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/lights_and_shadows.html
src_godot_blur.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/lights_and_shadows.html
src_poisson_disk.png Wikimedia Commons Eshipley CC BY-SA 3.0 https://commons.wikimedia.org/wiki/File:7_Filters_sampling_poisson_disk_sampling.png
src_godot_filter_quality.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/lights_and_shadows.html
src_cycles_gi.png Wikimedia Commons Ldo CC0 https://commons.wikimedia.org/wiki/File:CyclesGlobalIlluminationExample.png
src_godot_pcss.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/lights_and_shadows.html
src_shade_tree.jpg Wikimedia Commons W.carter CC0 https://commons.wikimedia.org/wiki/File:Sheep_lounging_in_the_shade_of_a_tree_at_R%C3%B6e_Castle.jpg
src_moon_penumbra.jpg Wikimedia Commons Maxwell Hamilton from Greater London, England United Kingdom CC BY 2.0 https://commons.wikimedia.org/wiki/File:Moon_Penumbra_Eclipse_in_London_25_April_2013_(8681049257).jpg
src_godot_area_light.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/lights_and_shadows.html
src_path_tracing_hi.png Wikimedia Commons KaiaVintr CC BY-SA 4.0 https://commons.wikimedia.org/wiki/File:Rendering_techniques_example,_path_tracing,_high_quality,_Blender_Cycles.png
src_godot_shadow_mode.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/lights_and_shadows.html
src_lo_csm_demoscene.png LearnOpenGL(CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Guest-Articles/2021/CSM
src_godot_blend_splits.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/lights_and_shadows.html
src_godot_quadrants.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/lights_and_shadows.html
src_lo_theory.png LearnOpenGL(教程站,CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping
src_lo_theory_spaces.png LearnOpenGL(教程站,CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping
src_lo_depth_map.png LearnOpenGL(教程站,CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping
src_lo_shadows.png LearnOpenGL(教程站,CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping
src_lo_with_without.png LearnOpenGL(教程站,CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping
src_lo_acne_diagram.png LearnOpenGL(教程站,CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping
src_lo_acne_bias.png LearnOpenGL(教程站,CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping
src_lo_with_bias.png LearnOpenGL(教程站,CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping
src_lo_zoom.png LearnOpenGL(教程站,CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping
src_lo_clamp_edge.png LearnOpenGL(教程站,CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping
src_lo_projection.png LearnOpenGL(CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping
src_point_sharp_shadow.png Wikimedia Commons Timrb CC BY-SA 4.0 https://commons.wikimedia.org/wiki/File:Point_light_source_sharp_shadow.png
src_area_soft_shadow.png Wikimedia Commons Timrb CC BY-SA 4.0 https://commons.wikimedia.org/wiki/File:Area_light_source_soft_shadow.png
src_penumbra_shades.jpg Wikimedia Commons Barraki CC BY-SA 3.0 https://commons.wikimedia.org/wiki/File:Different_shades_of_penumbra.JPG
src_penumbra_render.jpg Wikimedia Commons screenshot of middleware developed by Štěpán Hrbek, who is also the author of this image. I have his agreement to use this image in all Wikipedias CC BY-SA 3.0 https://commons.wikimedia.org/wiki/File:Lightsprint-Penumbra-shadows.jpg
src_lo_outside_frustum.png LearnOpenGL(CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping
src_lo_over_sampling.png LearnOpenGL(CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping
src_lo_point_diagram.png LearnOpenGL(CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Advanced-Lighting/Shadows/Point-Shadows
src_lo_point_cubemap.png LearnOpenGL(CC BY 4.0) Joey de Vries CC BY 4.0 https://learnopengl.com/Advanced-Lighting/Shadows/Point-Shadows
src_godot_light_params.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/lights_and_shadows.html
src_godot_light_directional.png Godot 官方文档(CC BY 4.0) Godot Engine documentation CC BY 4.0 https://docs.godotengine.org/en/stable/tutorials/3d/lights_and_shadows.html

其余示意图均为本系列绘制,版权归本系列所有。

相关链接

  • 上一篇:【实时渲染管线探析】(一) 延迟渲染:从原理到实现的性能权衡
  • 下一篇(预告):【实时渲染管线探析】(三) 后处理链:Bloom、色调映射与抗锯齿

本篇引用的主要资料

  • Randima Fernando, Percentage-Closer Soft Shadows, SIGGRAPH 2005, DOI 10.1145/1187112.1187153 — https://dl.acm.org/doi/pdf/10.1145/1187112.1187153
  • William Donnelly & Andrew Lauritzen, Variance Shadow Maps, I3D 2006, DOI 10.1145/1111411.1111440 — https://dl.acm.org/doi/abs/10.1145/1111411.1111440
  • Annen et al., Exponential Shadow Maps, Graphics Hardware 2008 — http://www.cs.ucl.ac.uk/staff/J.Kautz/publications/esm_gi08.pdf
  • Lauritzen & McCool, Variance Soft Shadow Mapping, PG 2010 — https://jankautz.com/publications/VSSM_PG2010.pdf
  • GPU Gems 3, Ch8 Summed-Area Variance Shadow Maps — https://developer.nvidia.com/gpugems/gpugems3/part-ii-light-and-shadows/chapter-8-summed-area-variance-shadow-maps
  • GPU Gems 3, Ch10 Parallel-Split Shadow Maps on Programmable GPUs — https://developer.nvidia.com/gpugems/gpugems3/part-ii-light-and-shadows/chapter-10-parallel-split-shadow-maps-programmable-gpus
  • Microsoft, Cascaded Shadow Maps — https://learn.microsoft.com/en-gb/windows/win32/dxtecharts/cascaded-shadow-maps
  • Microsoft, Common Techniques to Improve Shadow Depth Maps — https://learn.microsoft.com/en-us/windows/desktop/DxTechArts/common-techniques-to-improve-shadow-depth-maps
  • Unity URP, Shadows in URP — https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@14.0/manual/Shadows-in-URP.html
  • Unity Manual, Shadow Cascades — https://docs.unity3d.com/Manual/shadow-cascades.html
  • Godot, 3D Lights and Shadows — https://docs.godotengine.org/en/stable/tutorials/3d/lights_and_shadows.html
  • Unreal, Virtual Shadow Maps — https://dev.epicgames.com/documentation/en-us/unreal-engine/virtual-shadow-maps-in-unreal-engine
  • Unreal, Distance Field Soft Shadows — https://dev.epicgames.com/documentation/en-us/unreal-engine/distance-field-soft-shadows-in-unreal-engine
  • Vulkan Guide, Depth — https://docs.vulkan.org/guide/latest/depth.html
  • VK_EXT_depth_bias_control — https://docs.vulkan.org/features/latest/features/proposals/VK_EXT_depth_bias_control.html
  • Soft Shadow Mip-maps(CSM/VSM/ESM 对比) — https://collections.lib.utah.edu/dl_files/31/8d/318d6e6639f1eea9ae33c396a52af19007e45742.pdf
  • Improved Moment Shadow Maps (JCGT) — https://www.jcgt.org/published/0006/01/03/paper.pdf
  • Godot QA:acne 修复(Reverse Cull Face + 负偏移) — https://godotengine.org/qa/33702/lights-or-shadows-have-odd-lines-in-them

With Best Wishes.