Deep Read

ShadowMap阴影

阴影的原理

Shadow Map 是一种基于图像的技术,核心思想是用两趟绘制来模拟阴影:

  1. Shadow Caster Pass(阴影投射通道):从光源视角绘制一遍场景,生成深度图。
  2. Shadow Receiver Pass(阴影接收通道):从摄像机视角正常绘制,采样深度图判断阴影。

第一趟:Shadow Caster Pass——生成阴影图

这一阶段的目标是生成一张或多张深度图(Depth Map),即 Shadow Map。

视锥体确定。 引擎会为每个需要投射阴影的光源构建一个特殊的视锥体,不同光源类型用不同的投影:

  • 方向光(Directional):用正交投影。视锥体是一个紧紧包住主摄像机视锥内所有潜在投影物体的包围盒;为处理动态物体和相机移动,这个包围盒会实时计算调整。大型场景通常用级联阴影贴图(Cascaded Shadow Maps, CSM)——把相机视锥沿深度方向切成多个子视锥(Split),每个子视锥生成一张独立、分辨率更高的 Shadow Map,以解决单张贴图精度不足的问题。
  • 聚光灯(Spot):用透视投影,视锥体就是灯光的锥形范围。
  • 点光源(Point):向 ±X、±Y、±Z 六个方向各做一次透视投影,生成 Cube Map 形式的 Shadow Map。

渲染目标切换。 GPU 的渲染目标从主摄像机的颜色/深度缓冲切换到一张临时的高精度深度缓冲(Render Texture),这就是 Shadow Map。它通常只含深度、不需要颜色缓冲。

绘制。 场景中所有 ShadowCastingMode 不为 Off 的 Renderer 都会被收集,用一个专门的 ShadowCaster Pass 绘制。这个 Pass 非常轻量:

  • 顶点着色器:核心任务是把顶点从模型空间变换到光源裁剪空间,变换矩阵是光源的视图-投影矩阵。
  • 片元着色器:极简,通常不做任何光照或颜色输出,唯一目的是把从光源视角看的片元深度写入 Shadow Map。

绘制完成后,这张 Shadow Map 就记录了从光源视角看过去、场景中离光源最近表面的深度。

第二趟:Shadow Receiver Pass——采样并应用阴影

这一阶段是正常的场景渲染(如 URP 的 UniversalForward Pass),只是在着色器里增加了采样和计算阴影的逻辑。物体用自身材质对应的 Shader(如 Lit)渲染,阴影判断发生在片元着色器中。

坐标变换。 取当前片元的世界坐标,变换到光源裁剪空间得到阴影坐标 shadowCoord——这个变换和第一趟顶点着色器里的完全一致,URP 中由 TransformWorldToShadowCoord 完成。shadowCoord 的 xy 经处理后作为采样 Shadow Map 的 UV,z 分量则是当前片元从光源视角看的深度 currentDepth

采样 Shadow Map。 用上一步的 UV 采样 Shadow Map,得到该位置离光源最近遮挡物表面的深度 shadowedDepth。对方向光的 CSM,还要先根据片元深度判断它属于哪个级联,再选对应的 Shadow Map 和变换矩阵。

深度比较。 比较 currentDepthshadowedDepth

  • currentDepth > shadowedDepth + bias:当前片元比最近遮挡物更远,处于阴影中。
  • currentDepth <= shadowedDepth + bias:当前片元就是最近表面,不在阴影中。

这里的 bias 是必需的:由于 Shadow Map 精度有限、变换过程有浮点误差,直接比较会让物体表面出现错误的条纹状自阴影(Shadow Acne),给 shadowedDepth 加一个小偏移可以缓解。

软阴影(PCF)。 为避免硬的锯齿边缘,URP 采用 Percentage-Closer FilteringPCF(Percentage-Closer Filtering)PCF(Percentage-Closer Filtering)用来解决标准阴影贴图(Shadow Mapping)的硬阴影边缘问题。它的核心思想不是判断一个片元"是否在阴影中",而是计算这个片元周围一小块区域"有多大比例在阴影中",用这个比例作为柔和过渡的可见度。 要理解 PCF,得先理解它改的是 Shadow Mapping 的哪一步。完整的两趟绘制流程见 [[ShadowMap阴影]],这里只回顾和 PCF 直接相关的部分。 Shadow Mapping 的硬阴影从哪来 Shadow Mapping 分两趟。第一趟从光源视角渲染场景,只写深度,得到 Shadow Map。 第二趟从主摄像机视角正常渲染,对每个片元判断它是否被挡住: 1. 把片元坐标从世界空间变换到光源空间; 2. 当前深度 $d$ 和 Shadow Map 采到的深度 $z$ 比较,$d > z$ 就在阴影里。 问题出在第 2 步:结果非 0 即 1。一个片元要么全黑要么全亮,边缘自然是锯齿状的硬边。这和图像走样是同一回事——单点采样表达不了"半覆盖"的中间状态(见 [[锯齿和抗锯齿]])。要软化边缘:不只采样一个点,而是在目标 UV 周围一小块区域内多次采样(如 5×5 核),对每次采样都做深度比较,最终阴影值是所有采样点中"不在阴影中"的比例。例如 25 个采样点有 10 个通过深度测试,阴影系数就是 10/25 = 0.4,从而在边缘形成平滑过渡。

应用阴影。 算出的阴影衰减系数(0 全阴影、1 完全照亮)与该光源的直接光照贡献(漫反射和高光)相乘,阴影区域的光照就被减弱或消除。

要点回顾

  1. 两趟过程:先从光源视角画深度,再从相机视角画场景并比较深度。
  2. 空间变换是桥梁:把片元从世界空间变换到光源裁剪空间,这一步同时提供了采样 UV 和用于比较的深度。
  3. 深度比较定阴影:本质是比较"当前点到光源的距离"和"遮挡物到光源的距离",当前点更远就在阴影里。
  4. PCF 实现软阴影:区域采样并统计通过深度测试的样本比例,替代 0/1 的硬阴影。
  5. CSM 解决方向光精度:为不同距离区域生成独立、更高精度的 Shadow Map,提升大型场景下方向光的阴影质量。