# 阴影是怎么算出来的：光栅化为什么要靠一个滑杆硬撑，光追又凭什么自然给出软硬不同的半影

从阴影贴图的两难（acne 与 peter-panning）讲起，看清 PCF 为什么只是磨边而不是真软阴影，以及光追如何用阴影光线和光源采样让接触硬化自然出现

> 阴影 · 阴影贴图 · 软阴影 · 混合渲染 · 光栅化 · 约 7 分钟 · 10 月 17 日

## 本篇要点

1. 阴影要回答的只有一件事：表面点从灯的方向看是否被挡住。光栅化的做法是从灯的位置渲染一张深度图，再逐像素比较自己的深度，用光栅化本来就有的能力换来阴影。
2. 这张深度图必然对不准：一个 texel 只存一个深度，而它要回答的是连续表面上任意位置的深度。bias 太小会出现自遮挡条纹（acne），太大则影子与物体脱开（peter-panning），两者是同一个滑杆的两端，只能折中。
3. 掠射光和低分辨率都会压缩这条安全区间，所以 slope-scaled bias、normal offset、级联阴影贴图都是在给同一个前提打补丁。
4. 阴影贴图更根本的局限是它从单点出发，答案只有挡住或没挡住，所以只会产生硬边。PCF 把边缘平均成渐变，但柔化宽度由滤波核大小决定，与灯的大小和遮挡距离无关，因此给不出接触硬化。
5. 光栅化也能逼近接触硬化（PCSS），代价是遮挡物搜索加阴影滤波的多次采样，并假设场景里只有一层平行于光源的平板状遮挡物。
6. 光追直接朝灯发射光线测试可见性，不比对深度，因此没有分辨率错配、没有 bias 折中，也不会丢细物体的影子；只需把光线起点沿法线挪开一点，避免和出发面自相交。
7. 灯有面积时，在灯上取若干点分别射光线并统计通过比例，就得到软阴影。半影宽度由 $w_{\text{半影}} = (d_{\text{接收点}} - d_{\text{遮挡物}}) \cdot w_{\text{光源}} / d_{\text{遮挡物}}$ 决定，是几何的自然结果。
8. 代价转移到采样数：软阴影约为同场景硬阴影的 2 到 3 倍耗时，且随半影宽度增长；噪声集中在半影区，所以采样可以只在那里加密，剩下的缺口交给降噪。

---

在这个专题的第一篇里说过，光栅化的核心是“每个三角形各画各的”，所以阴影这种跨物体的关系它自己算不出来。但游戏里的影子是真的。这一篇看它怎么用一张深度图绕过去、留下了什么代价，以及为什么光追的影子能一半硬、一半软。

## 阴影只回答一件事：这个点看得见灯吗

一个表面点该亮还是该暗，只取决于从它出发朝灯的方向看，中间有没有东西挡着。挡着就是阴影。这是一道可见性问题，和表面自己的朝向、材质都无关。

难的地方在于：光栅化在画地面那个三角形的时候，手上只有地面自己的数据，它不知道远处还站着一个角色。

## 光栅化的绕路：从灯的位置拍一张深度图

阴影贴图（shadow map）的想法很聪明：光栅化擅长从一个视点渲染深度，那就把灯当成视点，把整个场景渲染一遍，只保留深度，得到一张“灯往每个方向看到的最前面那个表面有多远”的图。然后再按正常流程渲染画面，对每个像素多问一句：我在灯看来有多远？如果比那张图里记的更远，就说明有东西挡在前面，我在阴影里 [1][2]。

第二遍渲染没有引入新能力，复用的全是光栅化本来就会的东西，这正是它沿用至今的原因。

## 这张图天生对不准：acne 和 peter-panning

问题出在一对多上。那张图里的一个 texel 只存了一个深度，是灯看来这个 texel 中心方向上最前面那个点的深度；而第二遍渲染要提问的，是连续表面上任意位置的像素。屏幕上挨着的两个像素，在灯看来常常落在同一个 texel 里，各自的真实深度却并不一样。

于是斜对着光的表面上，比较结果变得随机：本该被照亮的一部分像素算出“比记录值更远”，自己给自己投出一片细密的黑色条纹和摩尔纹，这叫 shadow acne [3]。修法是比较时放宽一点，允许“近一点点也算没被挡”，这个宽容量叫 bias。bias 太小，acne 还在；bias 一大，问题从另一头冒出来——本该从物体脚底开始的影子往后退了一段，物体像浮在空中，这叫 peter-panning [1][3]。

这不是两个 bug，是同一个滑杆的两端。安全区间还会被两头挤压：灯越接近平着照，一个 texel 覆盖的表面在深度上跨度越大；贴图分辨率越低，texel 越大。两种情况都要求更大的 bias [2]。所以游戏画质选项里的 `Shadow Bias` 是替你选一个折中点，不是把误差消掉。常见的补救是让 bias 随表面相对光的斜率变大（slope-scaled bias），或把采样点沿法线挪出去一点（normal offset）[1][2]；分辨率不够，就把视野切成近中远几段各用一张贴图 [1]。这些都在补同一个前提，而不是绕开它。

## 它只认识一个点，所以只会画硬边

真正决定上限的是另一件事：整张图是从一个点拍出来的，每个 texel 的回答只有两种，挡住了或没挡住，所以影子边缘是一条硬线。

现实里几乎不存在这种影子，因为灯不是点：太阳是一块圆盘，灯泡是一小片玻璃。有面积的灯意味着，离遮挡物有一定距离的接收点会看到灯被挡住一部分、露出一部分，露出的那部分叫半影（penumbra），灯被完全挡住的一块叫本影（umbra）。

光栅化这里没有“挡住一半”这个状态，工业界的补法是 PCF：一个像素不比较一次，而是比较周围若干次，把结果平均成 0 到 1 之间的数，比如 0.4 [4]。

但要记牢：PCF 只是把边缘磨毛，得到的不是真正的软阴影。它的柔化宽度是你设的滤波核大小，是个固定值，与灯多大、遮挡物离接收面多远都无关。于是箱子放在地上和悬在半空，影子边缘糊的程度差不多。现实中这两者差别很大——遮挡物越贴近接收面，半影越窄，贴到一起时边缘完全锐利 [8]。这种随距离变锐的现象叫接触硬化（contact hardening），PCF 给不出来。

光栅化也能逼近它，做法叫 PCSS：先在一个核内采样，估计遮挡物离灯的平均距离，用这个距离推算出半影该多宽，再用对应宽度的核做 PCF [5]。代价落在两步采样上，遮挡物搜索和阴影滤波各要若干次采样，比普通 PCF 贵出数倍；而且这套推算假设场景里只有一层平板状遮挡物、并且它和接收面都平行于光源，遇到前后错落的遮挡物就会算错 [6]。

## 光追：不用比对深度，直接问“这条路通不通”

光追把整件事还原成原话：从这个表面点朝灯射一条光线，看它到达灯之前有没有撞上东西 [2]。没有深度图，就没有“一个 texel 对一个像素”的错配，acne 和 peter-panning 这整条链路也就不存在，不需要挑 bias，也不需要级联来分配分辨率；栏杆、头发、树叶也不会因为分辨率不够而丢影子。剩下的只有一个小问题：光线会立刻撞回它出发的那个面，所以起点要沿法线挪一丁点 [2]。这和 acne 同源，但量级小得多。

如果灯是一个点，这一条光线就给出精确答案，得到干净的硬阴影。这也是混合渲染里阴影最早被交给光追的原因之一：每个像素一条光线，相邻像素的光线都指向灯附近同一小片区域，走势一致，硬件求交和缓存都吃得消 [7]。

## 软阴影不是参数，是几何长出来的

灯有面积时，做法照样朴素：在表面点看得见的那片灯上取若干个点，各射一条光线，统计有多少条能通到灯。通一半，这个点就收到一半的光；一条都不通就是本影；全通就是全亮。

于是半影的宽度不是谁设的，而是几何自己算出来的。用相似三角形就能看出：设遮挡物到灯的距离为 $d_{\text{遮挡物}}$，接收点到灯的距离为 $d_{\text{接收点}}$，灯的宽度为 $w_{\text{光源}}$，接收面上半影的宽度约为

$$w_{\text{半影}} = \frac{(d_{\text{接收点}} - d_{\text{遮挡物}}) \cdot w_{\text{光源}}}{d_{\text{遮挡物}}}$$

接收点越靠近遮挡物，分子越小，半影越窄；贴合时半影缩到零，边缘锐利——接触硬化不是特效，就是这个比值的结果 [5][8]。灯越大、遮挡物离灯越近，同样的距离差给出的半影越宽。前后错落的遮挡物也各按自己的距离贡献一部分遮挡，不需要假设只有一层。

## 半影越宽，代价越高

代价转移到采样数上。只取一个点代表整片灯，答案是 0 或 1，逐帧还会跳——那是噪声，不是软阴影。要把“通了一半”估准，就得取足够多的点，随机采样的误差大约按 $1/\sqrt{N}$ 下降，正是上一篇讲噪点时的曲线。实测中，软阴影的计算时间大约是同场景硬阴影的 2 到 3 倍，而且高度取决于光源面积：半影越大，要的采样越多 [7]。

有个结构可以省力：完全照亮和完全遮挡的地方，可见性是确定的，方差为零，多采样纯属浪费，噪声几乎只集中在半影区。所以一个自然的做法是看每个像素的可见性随时间波动多大，只波动大的地方加光线，其他地方一条就够 [7]。这也解释了为什么实时软阴影离不开降噪——采样数被预算卡住，剩下的缺口只能靠时空滤波补。

## 从画面反推

- 影子边缘糊，但物体举高、贴地都不改变糊的程度：PCF 磨出来的假软边。
- 边缘有摩尔纹，或影子从脚底断开、物体像浮空：bias 没调好，后者是调过头了。
- 近处还行、远处块状并闪烁：分辨率的分配问题。
- 影子里有细颗粒、动起来像沸腾：软阴影采样不够，降噪压力大。
- 细窄物体（栏杆、树叶、电线）没有影子：阴影贴图的分辨率丢掉了它，光追下不该出现。

## 术语表

- 阴影贴图（shadow map）：从灯的位置渲染一张只含深度的图，记录灯沿每个方向看到的最前面表面有多远，据此判断可见性。
- 深度偏移（bias）：比较深度时给的一点宽容量。太小会出现自遮挡条纹，太大会让影子与物体脱开。
- Shadow acne：表面斜对光照时，被照亮处出现细密的自遮挡条纹和摩尔纹，根因是每个 texel 只存一个深度值。
- Peter-panning：bias 过大导致影子离开物体、物体像浮在空中。
- 半影与本影：灯被部分遮挡的区域叫半影，被完全遮挡的区域叫本影。灯有面积才有半影。
- PCF（percentage-closer filtering）：把一个像素的深度比较做多次并取比例，得出 0 到 1 之间的可见度，用来磨软边缘；它是抗锯齿，不是按几何算出的软阴影。
- 接触硬化（contact hardening）：遮挡物贴近接收面时半影变窄、贴合时影子边缘锐利，是软阴影里最容易看出真假的线索。
- PCSS：先估计平均遮挡距离、推出半影宽度、再按该宽度做 PCF 的近似软阴影方法，假设只有一层平行于光源的平板状遮挡物。
- 阴影光线（shadow ray）：从表面点朝灯发射、只用来测试中间是否被挡住的光线；多条分别射向灯上不同位置，通过的比例就是可见度。

## 来源

1. [Microsoft Learn: Common Techniques to Improve Shadow Depth Maps — shadow acne、peter-panning、slope-scale bias 与级联阴影贴图的官方说明](https://learn.microsoft.com/en-us/windows/win32/dxtecharts/common-techniques-to-improve-shadow-depth-maps)
2. [Shadow Mapping Explained: Acne, Peter-Panning, and PCF — 交互式对比 bias 两端，并说明光追绕开深度比较、只需挪开光线起点](https://deepraster.com/articles/shadow-mapping-explained/)
3. [OpenGL Tutorial 16: Shadow mapping — 从渲染结果出发解释 acne、bias 与 PCF 的实现](http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-16-shadow-mapping/)
4. [GPU Gems 11: Shadow Map Antialiasing — PCF 如何在硬件上把多次深度比较平均成可见度百分比](https://developer.nvidia.com/gpugems/gpugems/part-ii-lighting-and-shadows/chapter-11-shadow-map-antialiasing)
5. [Contact-hardening Soft Shadows Made Fast — PCSS 的遮挡物搜索与半影宽度估计，以及由此增加的采样开销](https://wojtsterna.com/wp-content/uploads/2023/02/contact_hardening_soft_shadows.pdf)
6. [Stochastic Soft Shadow Mapping — 指出 PCSS 假设单一平板状遮挡物且与光源平行，以及由此产生的误差](https://cg.ivd.kit.edu/publications/2015/sssm/StochasticSoftShadows.pdf)
7. [Ray Traced Shadows: Maintaining Real-Time Frame Rates — 光追阴影的采样预算、软硬阴影耗时对比、按半影区域自适应增加光线](https://boksajak.github.io/files/RTG1_RayTracedShadows.pdf)
8. [Penumbra Maps: Approximate Soft Shadows in Real-Time — 半影随遮挡物接近接收面而变窄、接触时完全锐利的表述](https://cwyman.org/papers/egsr03_PenumbraMaps.pdf)

---

原文：https://pangzhengboyin.com/articles/shadow-mapping-vs-ray-traced-soft-shadows-6196cb05

> **庞征博引** · 想学的，慢慢都会
>
> 庞征博引是把想学的东西写成连载的 AI 学习工具。说出想学什么，它会先了解你的基础，再把主题写成一篇篇 5–10 分钟能读完的文章；边读边问，接下来学什么跟着你走。这篇就是这样写出来的。
>
> 开始你自己的连载 → https://pangzhengboyin.com
