系列文章前言
1.1 系列定位
本系列逐层拆解实时渲染管线(Real-Time Rendering Pipeline)的关键环节。上一篇讲延迟渲染为什么快、代价在哪,收在"三座大山"上;这一篇接着讲阴影——它踩的是同一个三角:质量、性能、调参成本,三者总得让出一个。
1.2 本篇要讲清的一件事
阴影难,不难在"怎么算出一个影子",难在代价从哪来、又被谁消掉。
两趟渲染(Two-Pass Rendering)本身是廉价的,麻烦出在深度精度上:为了压制自遮蔽(Shadow Acne),必须给一个深度偏移(Depth Bias),而这个偏移一旦调大到压住所有角度,阴影就会从物体上脱开(Peter-Panning)。这个两难是整条演进脉络的起点——后面每一种方法,本质上都在回答"能不能不靠手工调这个偏移"。
1.3 全系列内容总览
- 【实时渲染管线探析】(一) 延迟渲染:从原理到实现的性能权衡
- 【实时渲染管线探析】(二) 阴影贴图:从 PCF 到 VSM 的演进 ← 本篇
- 【实时渲染管线探析】(三) 后处理链:Bloom、色调映射与抗锯齿
- 【实时渲染管线探析】(四) PBR 与 IBL:从渲染方程到工业实践
- 【实时渲染管线探析】(五) 可见性与剔除:从视锥到 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.1 为什么阴影是最贵的"免费效果"
阴影在观感上几乎是免费的——它不增加几何,不改变材质,只是让该暗的地方暗下去。但从管线的角度看,它是唯一需要为每个光源额外渲染一整张深度图的效果。
一张方向光的阴影贴图(Shadow Map)通常要覆盖整个可见视锥,分辨率 2048² 起步;多光源场景下这是成倍的开销。更麻烦的是,这张图的质量与主画面的质量不是同一套标准:主画面可以按屏幕分辨率逐像素着色,而阴影贴图只有固定分辨率,一个纹素(Texel)往往要覆盖屏幕上好几个像素,于是所有精度问题都被放大到肉眼可见。
这也解释了一个常见困惑:为什么把阴影分辨率从 2048 提到 4096,帧率掉得比预期多。提升的不只是一张纹理的内存,而是整个深度渲染通道的填充率——它随面积增长,不是线性。可以对照 Unity 手册对实时阴影开销构成的说明 —— https://docs.unity3d.com/Manual/shadow-realtime.html
2.2 本文的范围边界
本文只讨论基于阴影贴图的实时阴影,具体覆盖 PCF、PCSS、VSM、ESM 与 CSM 这条主线。以下内容不在本文范围内:
- 不需要阴影贴图的方案(光线追踪阴影、屏幕空间接触阴影 SSCR)——前者属光追管线,后者精度与场景相关,是另一个话题
- 点光源立方体阴影贴图(Cube Shadow Map)的实现细节——原理与 2D 版本一致,只在采样方向上有别
- 半透明物体的阴影(透射率估计)——只作为"未解决的难题"提一句,不展开
性能数据只引用论文与官方技术章的实测,不采信社区横评——原因见第七章。
2.3 术语对齐
后文统一使用简称,避免中英混用造成歧义:
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
同样的自遮蔽伪影,在实时引擎里的表现是这样的:
有阴影与没阴影的差别,一眼可见:
第二趟要先把当前像素变换回光源空间、拿到它在深度图上的位置,才能做那次比较——这一步的坐标变换是这样的:
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 从光源视图到最终阴影:一组对照
抽象讨论之外,先用一组示意图把流程走一遍。下面四张示意图依次说明光源视角、深度图内容、遮挡判定与最终结果(为便于看清原理,此处用示意图表达):
对照这四张可以看出:阴影贴图里没有任何颜色信息,"阴影"完全是第二趟比较出来的副产品。这一点是后面所有伪影的根源——因为深度图上相邻纹素的关系,并不能完全代表它们在真实表面上的关系。
上面四张为本系列自绘的原理示意。作为对照,下面给出同一流程的真实实现结果——作者 Praetor alpha 的阴影贴图演示,从左到右依次是:第一趟的光源视角、阴影贴图(光源视角的深度)、第二趟把阴影图投影回场景做遮挡判定、以及阴影填充完成后的最终画面:
阴影贴图的内容形态,还可以用一条单行剖切来表达:
两趟渲染的几何关系,先看这张原理图:
第二趟要把像素变换到光源空间,坐标关系是这样的:
阴影贴图本身(光源视角的深度)在这类可视化里长这样:
最终叠加上阴影后的画面:
3.5 为什么 acne 会出现:剖面视角
把视线切到光线的侧面,成因就一目了然:
自遮蔽的成因,用剖面图解看最清楚:
图中灰色是真实表面,斜线是光线方向。一个纹素在表面上覆盖的区间,在光线方向上被拉伸成一段深度范围;而阴影贴图只记录其中一个采样点的深度。当采样点选在区间靠前的位置时,区间后段的表面就会把自己判定为"被前方遮挡"。
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 必然顾此失彼"的算术依据。
再算一步:0.29 m 的偏移意味着,任何厚度小于 0.29 m 的物体(薄墙、栏杆、叶片)都会丢失自己投下的阴影,因为它的深度差被偏移吃掉了。这就是 Peter-Panning 在薄几何上最先暴露的原因。
3.8 深度精度:另一个放大因素
除了几何夹角,深度缓冲本身的精度分布也在推波助澜。透视投影下,深度值的分布是非线性的:
近处一小段世界距离占用很大一部分深度值范围,远处则相反。这意味着同一张阴影贴图上,远处的 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(彼得潘现象)。
真正麻烦的地方在于:这个两难没有"正确"取值,只有针对特定场景的折中。Slope-Scale Depth Bias(斜率相关深度偏移)把 $\cos\theta$ 引入偏移计算,缓解了大部分情况,但掠射角仍然是盲区。
偏置的取值区间,可以看这张示意:
应用偏置之后,伪影消失:
斜率相关偏移的工程落地方式是取夹角的正切而非余弦——因为 $\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 又不产生明显脱开——代价是薄壁几何(薄墙、叶片)会失效。
这条绕开思路落地后的效果:
//-----------------------------------【反转剔除 + 负偏移】-------------------------------------
// 描述:用背面深度规避掠射角自遮蔽,薄壁几何不适用
//--------------------------------------------------------------------------------------------
//【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 才有了它"代价随核面积增长"的批评对象。
偏置调过头的代价,在引擎里一眼可见:
二、PCF:过滤比较结果
4.1 从「比一个纹素」到「比一片」
PCF(Percentage-Closer Filtering)的想法很直白——不要只比一个纹素,比周围一片,取比例。它确实让阴影边缘变软了,但代价是采样次数随核面积平方增长,而且无法用 mipmap 之类的预过滤来加速。
4.2 原理:为什么不能过滤深度
一个自然的想法是:把阴影贴图当普通纹理,做一次模糊,再比较。这是错的。
原因是深度值与可见性之间是非线性关系:两个点深度取平均后,其遮挡状态不等于两点遮挡状态的平均。一个深度 10 和一个深度 20 的纹素平均得到 15,而 15 这个深度可能被任何介于两者之间的物体遮挡——平均之后的深度是没有物理意义的。
所以 PCF 只能反过来做:先逐个比较,得到 0/1 结果,再对结果求平均。
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 行的顺序:比较在前、平均在后。一旦写反,得到的就是上一节说的"没有物理意义的平均深度"。
最终效果与未处理时对比,差异相当直观:
同一场景在 LearnOpenGL 示例里的真实对比,直观到不用解释:
同样的滤波开关,在引擎的阴影滤波档位里也能直接看到:
上一节说过"比较本身没错,错在精度"——上面硬阴影那张把这句话坐实了:整个阴影区域布满了锯齿状的条带,这正是深度量化与缺偏置共同作用的自遮蔽伪影;而下面那张只做了 3×3 卷积,边缘立刻柔化,伪影消失。注意软阴影接地处的梯度过渡——它是刀刃穿透、滤波粒径与偏置的共同产物,也是第三章 PCSS 要做"可变量"滤波的直接理由。
而更高保真的软阴影(下图由光线追踪离线渲染)展示的是同一个问题的"标准答案"——逐像素按面积光源采样,天然得到正确的半影:
把这张图与实时方案的结果对照着看,可以直观理解第三章 PCSS 想逼近的目标是什么。
4.4 参数说明
radius是采样半径(以纹素为单位)。取值越大边缘越软,但采样数按 $O(n^2)$ 增长,且所需 bias 也随之增大——因为偏移要覆盖整个滤波区域。- 硬件 PCF 通过
sampler2DShadow与硬件比较采样器实现,一次采样完成 2×2 双线性比较,是 4 次采样换来的软边。
4.5 PCF 的三个固有代价
| 代价 | 原因 |
|---|---|
| 采样成本随核面积平方增长 | 每次采样都是一次纹理读取 + 比较 |
| 无法预过滤加速 | 深度不能做线性滤波(见上文) |
| bias 必须随核面积增大 | 否则滤波区域内出现 acne,反过来加剧 Peter-Panning |
第二条是 PCF 最硬的限制,也是 VSM 的动机所在——第三章讲 PCSS 如何在 PCF 上叠一层自适应,第四章讲矩方法如何绕开"逐次比较"这件事。
4.6 采样图案:从网格到泊松盘
朴素的 PCF 用矩形网格采样,这会带来一个副作用:规则的采样图案会在阴影边缘留下规则的条带痕迹。当核半径较大时,这种痕迹会明显到被肉眼识别为"方块状渐变"。
实践中常见的替代图案有三类:
| 图案 | 特点 | 代价 |
|---|---|---|
| 矩形网格 | 实现最简单,可分离(先横后竖两趟) | 核较大时出现条带 |
| 泊松盘(Poisson Disk) | 采样点分布自然,无明显周期结构 | 需要预生成的采样表 |
| 螺旋 / 黄金角 | 无需查找表,用固定公式生成 | 分布略有偏心 |
泊松盘采样的样本分布有现成的真实图案可供对照:
可分离采样(Separable Filtering)值得一提:矩形网格的 PCF 可以拆成横向与纵向两趟,把 $n^2$ 次采样降为 $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 在纹理单元里内置了比较逻辑:
4.8 边缘软硬与核半径的关系
PCF 的核半径直接决定阴影边缘的软化程度,但它们不是线性关系:
| 核半径 | 采样数(3×3 基数) | 边缘表现 |
|---|---|---|
| 1 纹素 | 9 | 轻微软化,几乎仍像硬阴影 |
| 2 纹素 | 25 | 明显软边 |
| 4 纹素 | 81 | 很软,但条带痕迹开始显现 |
| 8 纹素 | 289 | 不适合实时;此时应改用 VSM |
这张表也解释了为什么 PCF 通常只做"轻微软化":它擅长的是消除锯齿感,而不是产生物理正确的软阴影。真正要让半影宽度有物理依据,得靠第三章的 PCSS。
滤波质量档位之间的真实差异:
把阴影边缘放大,滤波前后的差别才看得清:
三、PCSS:让核面积随遮挡距离自适应
5.1 固定采样半径的局限
PCF 的采样半径是个常数,所以它给出的软硬程度与真实光照不符——真实世界里,遮挡体离接收面越远,半影越宽。PCSS(Percentage-Closer Soft Shadows)的做法是让核面积随遮挡距离自适应:先估计遮挡体的平均距离,据此推出半影宽度,再按这个宽度做 PCF。
固定半径做不到的事,离线全局光照给出了参照——真实场景里的半影随遮挡距离连续变化:
点光源投出的影子是锐的——它没有面积,半影宽度为零:
5.2 原理:三步流程
PCSS 不是对 PCF 的替代,而是在它前面加了两步:
① Blocker Search 在阴影贴图中搜索遮挡体,求其平均深度
② 估计半影宽度 由遮挡体距离与接收面距离算出滤波半径
③ PCF 用上一步的半径做变半径 PCF
PCSS 在引擎里也不是纸面概念:
注意它把 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}}$(遮挡体紧贴接收面)时,半影宽度趋于零——得到硬阴影,正如现实中贴合的接触阴影一样锐利。
一个能感知到的参照是面光源与点光源的差异。面光源(Area Light)在物理上必然产生半影,而点光源不会:
换成球体这个更简洁的几何体,软阴影在各处的宽度差异会更清楚:
PCSS 想复现的正是这个现象——用一张深度图,估计出本该由面光源采样得到的半影宽度。
真实世界里,半影随遮挡距离增大是常态:
半影不是渲染器的发明,而是几何光学的结果——地球本影投在月球上的边缘正是半影:
渲染器要逼近的正是这种效果——面光源之所以给出软阴影,是因为它本身有面积:
换成有面积的光源,影子立刻出现软边:
真实世界里同一遮挡物的半影是有层次的:
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 论文自述的卖点里,有一条对工程判断最有用:它独立于几何复杂度——因为全部计算都在图像空间。这一点在第七章与其他方法横向对比时还会用到。原文与 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 直链,以保证长期可访问。
延伸:点光源的立方体阴影贴图——同一套深度比较,只是把一张图换成六个方向(超出本文正文范围,仅作对照):
它存下来的深度立方体贴图长这样:
5.7 Blocker Search 的采样数怎么定
Blocker Search 是 PCSS 里唯一"新增"的开销,它的采样数 $m$ 决定了两件事:能否找到足够的遮挡体、以及总成本。
| Blocker 采样数 | 表现 | 适用 |
|---|---|---|
| 4–8 | 遮挡体估计噪声大,半影宽度抖动 | 低成本档 |
| 16 | 多数场景稳定 | 常规推荐 |
| 32+ | 稳定但成本接近一次完整 PCF | 影视级 |
关键限制在于:Blocker Search 的搜索范围是固定的,不随滤波半径变化。这意味着当遮挡体在阴影贴图上非常稀疏(例如细长物体的投影)时,固定范围内的采样可能一个都没打中遮挡体,此时估计退化为"无遮挡"——表现为阴影局部消失。
工程上的兜底是给一个最小半影宽度,让"完全无遮挡"与"局部无遮挡"之间不至于跳变。
5.8 用矩方法加速 Blocker Search
Blocker Search 需要多次采样才能估计遮挡体平均深度。但如果阴影贴图存的是矩(第四章的 VSM),那么遮挡体的平均深度可以直接由矩反推,无需逐次采样:
这就是 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 提供廉价的遮挡估计。
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$ 都是线性可滤波的量,把阴影贴图做一次模糊,等于对整片邻域求矩——滤波半径不再需要逐次采样。
作为对照,下面这张是传统阴影贴图在离线渲染中的表现,可以看到软阴影与自遮挡处理得当时的参考质量:
6.3 代码:VSM 的渲染与采样
//-----------------------------------【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 相对早期矩方法最实用的改进。
6.4 该方法的失效边界
两阶矩只能描述单峰分布。当同一片邻域内出现两个分离的深度层(例如前景物体与背景墙共处一个滤波核),矩方法无法表达这种双峰,上界会失真:
- 漏光(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 的相对性能,并强调三者都工作在图像空间、基本与几何复杂度无关。
6.6 双峰问题:一个具体例子
抽象描述不好理解,看一个具体场景。假设滤波核(例如 5×5 邻域)内包含两块表面:
| 纹素 | 深度值 | 含义 |
|---|---|---|
| 60% 的纹素 | 0.30 | 前景物体表面 |
| 40% 的纹素 | 0.70 | 背景墙面 |
矩方法据此算出 $\mu = 0.46$、$\sigma^2 \approx 0.038$。现在屏幕上某点在深度 0.68(位于背景墙面上,理应只受前景物体遮挡):
代入切比雪夫上界,得到的遮挡概率是一个明显小于 1 的值——背景墙上本应完全处于阴影的区域,被算成了半亮。这就是矩方法无法表达双峰分布的直观后果。
反过来,如果屏幕点在深度 0.28(前景物体朝光的一面,理应完全受光),矩估计也可能给出偏暗的结果,造成前景物体自身出现虚假阴影。
这两种错误的方向相反,且无法靠调整任何单一参数同时消除——缩核能同时缓解两者,但缓解的是程度,不是结构。
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/
代价是存储与带宽成比例上升:$n$ 阶矩需要 $n+1$ 个通道。在延迟渲染本就吃带宽的前提下(见本系列第一篇对 G-Buffer 带宽的分析),这笔账要仔细算——矩方法的所有优势,都建立在"额外的通道换免调参"这个交易上。
6.8 一张对照图
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 一样做大幅模糊。
一个高质量的离屏渲染能直观展示"正确的软阴影与间接光"应该长什么样,作为实时方案的目标参照:
同一类“离线标准答案”的另一张对照:
7.3 代价:有条件的近似
ESM 的近似只在接收面深度不小于参考深度时成立。一旦越界(例如物体紧贴或穿插),近似值会急剧偏离,出现整片错误的暗区。
这意味着 ESM 必须配备回退路径:越界时退回 PCF。工程上这不算严重问题——多一次分支判断即可——但它说明了一件事:凡是"用统计量近似可见性"的方法,都必须清楚自己的有效区间。
7.4 三种方法的取舍
| 方法 | 存储 | 可线性过滤 | 手工调参 | 主要失效 |
|---|---|---|---|---|
| PCF | 1 张深度 | ✗ | bias 必须手调 | 掠射角 acne、边缘软硬不均 |
| VSM | 2 张(depth + depth²) | ✓ | 基本免调 | 双峰分布导致漏光 |
| ESM | 1~2 张 | ✓ | 基本免调 | 深度越界时近似失效 |
| PCSS | 1 张深度 | ✗ | bias 仍需手调 | Blocker Search 在稀疏遮挡下失准 |
参照 Soft Shadow Mip-maps 论文的结论:ESM 以更少存储获得更好质量与性能,但需要在越界时回退。这也解释了为什么工程实践中常见的是"PCF 打底 + 关键场景上 VSM/ESM"的混合方案,而不是全盘替换。
7.5 一个容易忽略的细节:指数常数与场景尺度耦合
$c$ 的取值与场景的深度范围强相关。若把同一套参数从室内场景搬到室外大场景,$c$ 的相对尺度会失效——指数近似对"深度单位"敏感,而 PCF/VSM 对深度单位不敏感(VSM 的切比雪夫上界只用到深度的相对差值)。
这带来一个跨项目复用上的实际差别:VSM 的参数换个场景基本能沿用,ESM 的 $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)的解法是把视锥切成几段,每段配一张独立的正交投影阴影贴图,按距离分配分辨率。
方向光的阴影模式在引擎里是显式可选的:
阴影贴图覆盖范围之外的区域,需要单独处理:
覆盖范围定得再大也是有限的,范围之外没有深度可比:
把边界处的采样夹到极值是一种修法,代价是交界处出现过采样:
8.2 原理:把一维的距离问题切成段
近处的物体在屏幕上占的像素多,远处的占得少。所以分辨率也该按这个比例分配:
每一级联(Cascade)是一张独立的阴影贴图,各自用一套正交投影矩阵,只覆盖自己负责的那段视锥。渲染时按像素到相机的距离选择对应的级联,再在级联交界处做混合以避免接缝。
来看一张真实 CSM 调试视图,直观感受"切段"发生了什么:
级联落进真实场景后是这样——下面是同一套 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$ 是论文给出的默认经验值。
这个混合的意义在于:纯对数切分能让近处分辨率极高,但最远一段会覆盖过大的距离;纯均匀切分则浪费了近处的分辨率。加权后两者兼顾。
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 级联数量该取几
级联数量是成本与质量的直接杠杆。每多一级联就多一张阴影贴图、多一趟渲染,但分辨率分配更精细:
| 级联数 | 近处精度 | 渲染开销 | 适用 |
|---|---|---|---|
| 1 | 低(等同单张贴图) | 1× | 小场景、远景为主 |
| 2 | 中 | 2× | 移动端常见取舍 |
| 4 | 高 | 4× | PC 平台常规默认 |
| 8 | 很高 | 8× | 大世界、影视级 |
数量增多还会带来一个副作用:切换点的接缝数量随之增加。4 级联有 3 个切换边界,8 级联有 7 个——每一个都可能是可察觉的接缝。所以"更多级联"不是单调更优,工程上通常在 4 附近收敛。
8.6 投影抖动:一个被低估的问题
当相机移动时,每级联的正交投影范围会随视锥变化而滑动,导致阴影贴图的纹素网格相对世界空间持续移动。结果是阴影边缘出现"爬行"(Shadow Crawling)——看起来像阴影在轻微抖动。
标准对策是纹素捕捉(Texel Snapping):把每级联的正交投影中心吸附到纹素网格的整数倍上,使纹素相对世界空间稳定。代价是投影范围会有微小偏移,但换来了稳定的边缘。
这个问题的存在说明:CSM 的复杂度不只来自"切几段",还来自"段怎么跟着相机走"。
级联之间的过渡同样需要处理——直接切换会在边界留下可见的接缝:
级联之间的接缝靠混合区抹平,引擎里的划分是这样的:
8.7 投影类方法的谱系
CSM 并非唯一解。围绕"如何让方向光的阴影贴图更贴合视锥",还出现过一串投影变形方案:
| 方法 | 核心变形 | 主要问题 |
|---|---|---|
| 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 管每张级联贴图内部的滤波质量。
七、工程实践:引擎怎么落地
9.1 公式到了引擎里变成几个参数
论文里的公式到了引擎里会变成几个参数。理解这些参数在引擎中的映射,比记住公式更有用——尤其是 bias 那一组,它们是第一章那个两难的工程化表述。
公式到了引擎里,落成的就是这样一个面板:
9.2 参数映射:三个引擎的对照
Unity URP 的阴影配置是最直白的样本:
方向光的阴影部分再展开一层是这样,与上表逐项对得上:
| 引擎 | 阴影贴图组织 | 偏移参数 | 官方提示 |
|---|---|---|---|
| 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 的性能数据在社区里流传很广,但多数横评不交代测试环境。本文只引用三处可追溯的实测:
| 出处 | 对比内容 | 可引用结论 |
|---|---|---|
| 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 帧率横评"多数不交代显卡型号、分辨率与场景,本文不采信。这个取舍标准本身也可以画成一条判断线:
9.5 图集:为什么要把多张阴影贴图拼在一起
Unity URP 的做法点出了一个工程细节:点光 6 张、聚光 1 张、方向光每级联 1 张,但 punctual 光与方向光各自只用一个图集。
这么做的理由是减少纹理绑定与状态切换:一个场景若有 8 盏聚光灯,逐光绑定 8 张纹理意味着一帧内 8 次状态变更;拼成一张图集后只需绑定一次,采样时用偏移区分。代价是图集内各块的边距需要额外处理,且动态光源数量变化时图集要重排。
这是本文里少见的"纯工程、无算法"的优化点——但它在实际帧率上的影响往往比换个滤波算法更大。
对照一张低质量光栅化渲染,可以看出分辨率与采样策略的取舍对最终观感的影响,往往大于算法本身的选择:
多张阴影贴图拼成图集时,空间是这样分配的:
9.6 值得警惕的一个组合:VSM 与 MSAA
VSM 需要对阴影贴图做预过滤,而多重采样抗锯齿(MSAA)作用于主画面——两者并不直接冲突,但 VSM 的双矩通道在 MSAA 下需要逐样本写矩,否则过滤出来的方差是错的。这类"两个正确的东西凑在一起出错"的问题,是矩方法落地时的主要摩擦。
9.7 参数是公式的接口
工程实践这一章说明了一件事:参数是公式的接口,而 clamp 的存在本身就承认公式有边界。这也解释了为什么"读论文"与"调引擎"之间总有一道沟——论文给的是理想条件下的推导,引擎给的是带兜底的近似。
八、选型建议与未来展望
10.1 没有一种方法能通吃
没有一种阴影方法在所有场景下最优。选型的依据是三件事:光源类型、场景规模、以及这个项目更怕 acne 还是更怕漏光。
10.2 横向对比总表
| 维度 | PCF | PCSS | VSM | ESM | CSM |
|---|---|---|---|---|---|
| 阴影类型 | 硬/可调软 | 自适应软 | 软 | 软 | 硬(分辨率分配) |
| 采样成本 | $n^2$ | $m + n^2$ | $O(1)$(查表) | $O(1)$ | 每级联各一套 |
| 可预过滤 | ✗ | ✗ | ✓ | ✓ | ✗ |
| 手工调参 | bias 必调 | bias 必调 | 免调 | 免调 | 精度分配 |
| 主要失效 | 掠射角 acne | Blocker 稀疏 | 双峰漏光 | 深度越界 | 级联接缝 |
| 适用场景 | 通用打底 | 影视级软阴影 | 中距离软阴影 | 大范围软阴影 | 方向光必配 |
10.3 推荐的组合方式
从实践看,稳定的组合是分层的:
方向光 → CSM 管覆盖范围(4 级联起步,远端可降到 2 级联)
每张级联 → PCF 打底(3×3 或 5×5,硬件比较采样器)
关键镜头 → 在 CSM 之上叠 VSM/ESM 做软阴影
阴影偏移 → Slope-Scale 为主 + 小常数 Normal Bias,clamp 兜底
这个组合的逻辑是:用成熟手段覆盖 90% 的情况,把近似方法的失效风险限制在可以人工干预的关键镜头上。
落到具体的调参顺序,经验上按这个次序走最省时间:
10.4 演进时间线
| 年份 | 节点 | 解决的问题 |
|---|---|---|
| 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 未来方向:从"优化一张图"到"不生成那张图"
前面八章讨论的所有方法,都建立在同一个前提上:先渲染出一张完整的阴影贴图,再在采样时想办法提高它的质量。而这个前提本身正在被挑战。
| 方向 | 代表 | 思路 |
|---|---|---|
| 优化采样 | 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 用级联换掉了超高分辨率贴图,代价是接缝与爬行
面对一个新方法时,先别问它有多快,先问它的失效区间在哪、失效时会怎样表现——失效的方式决定了它能否被工程接受。
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.