Shadow Map
一个基础的双 Pass 计算阴影的算法,这篇先说最基础的 Shadow Map,至于优化方法后面再说.
其核心思维是先通过一个 Pass 从光源出发,知道光源可以“到达”的点,然后在第二个 Pass 做 Shading 的时候再借助第一个 Pass 生成的 Shadow Map 来进行可见性分析
渲染方程
先从渲染方程的角度来看看我们在计算什么 ——
老演员了,不过我们在实时渲染中通常会先计算光源在无遮挡情况下能够提供的辐射亮度,再显式引入可见性函数
那么加入可见性函数之后,渲染方程即变为
Shadow Map 就是为了实现 V 函数的估计这样一件事情
第一个 Pass
我们假设光源是一台相机,先跑一遍渲染,不过不需要输出什么颜色信息,只需要把深度信息生成即可
此时光源类型会导致我们使用不同的“相机” ——
- Directional Light: 使用正交投影
- Spot Light: 普通的透视相机,使用透视投影
- Point Light: 可以使用 Cube Shadow Map 等,因为它的光向所有方向都会射出,所以一张普通的 2D Texture 无法保留所有信息
那么跑这遍渲染,遍历所有物体的时候,会记录这个方向距离光源最近的点的深度信息 —— 因为同一个方向上,如果有东西距离光更近,说明你们之间有遮挡,所以其实光应该是看不到你的
第二个 Pass
现在假设我们在对 P 点进行 Shading,那么找到其世界坐标之后,变换到光源的观察空间,然后借助透视除法我们就可以得知 P 点在光源观察下的深度信息
如果
问题
那么问题非常显然 —— 在理想情况下,一个直接被光源看到的表面点应满足
- 空间离散误差
- 数值精度
于是会出现「自遮蔽」(Shadow Acne) 问题
先说“离散空间”的问题 —— (实际上这个问题非常的常见啊,而且重要啊)
空间离散误差
首先现实中的场景表面是连续的,所以比如说中光源角度来观察一个平面,其深度可以写成一个函数
其中
表示 Shadow Map 上的连续位置 表示沿着这个位置看到的最近的表面深度
在理想情况下,这应该是一个“无限分辨率”的图像 —— 因为要求连续,对应所有的
所以在理想模型中,实际上不会出现自遮蔽的问题
但很坏,在真实的生成过程,我们的 Texture 是有分辨率上限的,比如
所以可能出现一个点本来深度够小,没有被遮挡,但是由于它投影之后在 Shadow Map 中所属的 texel 记录的深度比它更小(记录了近邻的信息),导致其最后
显然可得,表面在光源投影空间中的深度变化越快,一个 texel 内可能产生的深度差越大,因此越容易发生错误自遮蔽。对于平面来说,可以简单看作是平面相对于光源方向的倾斜程度
数值精度
除空间离散外,Shadow Map 中保存的深度以及第二遍重新计算的深度都具有有限数值精度。即使理论上二者相等,在深度量化、浮点舍入和两次计算路径差异的影响下,也可能得到错误的结果
Trick
加上一个 Bias 来规避掉自遮蔽的问题,也就是判断的时候不是单纯
那或许可以规避掉自遮蔽问题,但是这个 Bias 一方面是以来经验来设计大小,需要通过试验来确定(当然,也可以根据深度斜率、表面法线与光线夹角或 Shadow Texel 覆盖范围等进行变化—— Slope-Scaled Bias 这块);另一方面,如果太大的话可能会导致原先被遮蔽的物体被光源点亮了,在图像上看类似说 —— 站在地上的人,影子和脚的位置断开了,类似这样(俗称 Peter Panning)