需要预先学习:


1. ShadowMapping(阴影贴图)

着色与渲染管线 里提过,着色是局部的:每个点只根据光源与自身材质计算颜色,完全不知道其他物体的存在。所以 Blinn-Phong 管线本身画不出阴影——被遮挡的点和直接被照亮的点会得到相同的着色结果。ShadowMapping(阴影贴图)为光栅化管线补上这块能力,出发点是一个关键观察:

一个点不在阴影中,当且仅当光源能直接看到这个点。

"从某个视点看,谁能被看见"正是 深度缓冲 解决的问题。ShadowMapping 的思路,就是把深度测试搬到光源的位置再做一遍。


1.1 算法流程

ShadowMapping 分两遍(two-pass)渲染。

Pass 1:从光源渲染场景,只记深度。把光源当作一台摄像机——平行光配正交投影,点光源配透视投影(见 视图(摄像机)与投影)——渲染整个场景,但不存颜色,只把深度缓冲存下来。这张深度图就是阴影贴图(shadow map):它记录了从光源看过去,每个方向上最近的表面有多远。

**Pass 2:从摄像机正常渲染场景。**着色的同时,对每个片元做一次阴影判断:

  1. 把片元的位置变换到光源的观察空间,投影到光源的"屏幕"上,得到它对应的阴影贴图采样位置和深度;
  2. 采样阴影贴图,取出该方向上最近的表面深度;
  3. 比较二者:片元比记录中的最近表面更远,说明从光源看,这个片元前面还有别的表面挡着光——它在阴影中。
// 伪代码:Pass 2 中的阴影判断
float d_frag = to_light_view(fragment.pos).z;   // 片元在光源视角下的深度
float d_map  = shadow_map.sample(uv);           // 贴图记录的最近深度
bool in_shadow = d_frag > d_map;                // 更远 -> 被遮挡

判定为阴影的片元,着色时砍掉漫反射与高光,只保留环境光项——阴影处不是黑色,而是失去直接光照后变暗的固有色。


1.2 数值问题

ShadowMapping 的两个主要毛病都出在"逐像素比较两个深度"上。

自遮挡(self-shadowing):Pass 1 存下的深度是离散采样的结果,一块斜着的平面在阴影贴图里被量化成台阶状的深度。Pass 2 拿片元的精确深度去比较,一部分像素"刚好更远一点点",通过与否在像素间抖动,平面上出现黑白相间的条纹,俗称 shadow acne(阴影粉刺)。缓解办法是在比较时加一个偏移(bias),给量化误差留出容差。

bias 不是免费的:给多了,阴影整体向后退,物体与地面接触处的阴影断开,物体看起来像悬浮在阴影上方,称为 peter panning。bias 的取值需要在 acne 与漏光之间权衡。

走样:阴影贴图分辨率有限,当一张低分辨率贴图覆盖大范围场景时,一个贴图纹素对应屏幕上的一串像素,阴影边缘出现块状锯齿——本质又是采样不足(见 光栅化 的走样)。常见对策是提高贴图分辨率,或让近处场景使用单独的高分辨率贴图(级联阴影贴图,cascaded shadow map)。

注:以上问题各有后继的改进方案(如 variance shadow map、moment shadow map),但"光源视角记深度 + 摄像机视角比深度"的两遍框架始终没变。


2. 软阴影与硬阴影

ShadowMapping 的判断结果只有"是 / 否"两种,画出的阴影边界锐利、非暗即亮。这一节看看真实阴影的形态,以及它为什么不是这样的。


2.1 硬阴影

边界一刀切的阴影称为硬阴影(hard shadow)。它隐含了一个假设:光源是一个点,或一个无穷远的方向——只有点光源的遮挡才会产生绝对分明的边界。

现实里不存在无限小的光源。


2.2 本影与半影

光源有面积时,遮挡是一个程度问题。从光源两端点经过遮挡物边缘作连线,投影面上会出现两类区域:

  ████████   面光源
   \      /
    \    /     <- 光源两端发出的边界光线
    ▄▄▄▄
    ████       遮挡物
    ▀▀▀▀
   /    \
  /      \
━━░░░▓▓▓▓░░░━━  投影面:▓ 本影,░ 半影
  • 本影(umbra):从这一点完全看不到光源,阴影最暗;
  • 半影(penumbra):从这一点能看到光源的一部分,明暗介于本影与无遮挡之间。

2.3 软阴影

软阴影(soft shadow)就是半影在视觉上的表现:阴影边缘有一段从全暗到全亮的渐变,而不是一刀切的边界。它传递了两个物理事实:

  • 光源有面积;
  • 半影的宽度与遮挡物到接收面的距离相关——遮挡物贴着接收面时阴影边缘锐利,悬得越高边缘越柔和。反过来说,从一张照片里阴影的软硬程度,能推断出光源的大小。

光栅化下实现软阴影的基本思路,是把"在不在阴影中"的二值判断改写成"光源被遮挡的比例":

  • PCF(percentage closer filtering):比较深度时不止查一次,而是在阴影贴图上取一个邻域多次采样,把多个"是 / 否"的结果平均成 0 到 1 之间的系数,阴影边缘得到过渡;
  • PCSS(percentage closer soft shadows):在 PCF 基础上,让滤波范围随遮挡物与接收面的距离动态变化——近处滤波窄(阴影硬),远处滤波宽(阴影软),与半影的几何规律一致。

注:PCF是用于消除硬阴影锯齿的方法,PCSS 才是真正模拟软阴影的算法。PCF 只是参与了 PCSS 的滤波阶段。