阴影技术(1)——朴素Shadowmap,PCF,PCSS

Shadow Mapping与常见问题

The Most Basic Method Of Shadow Mapping

​ 对于阴影生成, 首先需要额外设置一个摄像机在光源位置(Light Camera,光源摄像机),并且朝光照方向看去。用一张纹理贴图(也称阴影贴图,Shadow Map)来记录Light Camera所看到的像素深度。

​ 在主渲染流程时,对每一个着色点进行着色时,需要比较这一点在Light Camera视角下的深度是否大于Light Camera记录在纹理上的深度值。如果大于则说明这一点在光照时被其他更近于光源的物体遮挡住,我们需要这里对颜色进行衰减以形成阴影的效果。

Depth Buffer(In Light View)
image-20231129161027212
With Shadow Without Shadow
image-20231129173100355 image-20231128150403640

这两部分就是实时渲染中阴影技术的核心,但其在实际应用中有着诸多问题需要解决。

会出现的几个问题:

  • 阴影失真与自遮挡(Shadow Acne)
  • 阴影悬浮(Shadow Peter-panning)
  • 阴影锯齿(Shadow Aliasing)

  • 大场景中Shadow Map找不到对应的点(CSM)

Shadow Acne和自遮挡

image-20231224202229441

上图出现了条状的阴影:

image-20231224202314452

1
以方向光源为例,一般认为方向光是平行光,在光源处渲染时使用正交投影。因为Shadow Map的分辨率有限,Shadow Map上面的**一个片段**对应场景中的**一块区域**,又因为很多情况下光源与物体存在夹角,因此记录的深度通常与物体的实际深度存在偏差。

​ 上图中蓝色片段即为ShadowMap中记录的深度。在纹理中像素为最小单位,一个像素只能记录一个值,图中每个像素记录的是箭头处的深度。这就导致了明明本该整块被照亮的地板,会出现明暗相间的条纹:黑线处的地板由于在光源视角中深度小于记录的值,因此不在阴影中。红线处的地板深度大于记录的值,没有通过阴影测试。

​ 解决办法有些许trick,我们可以用一个叫做阴影偏移(shadow bias)的技巧来解决这个问题,我们简单的对表面的深度(或深度贴图)应用一个偏移量,这样片元就不会被错误地认为在表面之下了。

image-20231224202630631

我们这里添加一个bias, 阴影基本正常:

image-20231224202923283

因为阴影失真的问题是由于光线和物体表面的夹角导致的,光线越垂直于物体,失真的影响就越小,因此通常会将bias与光线和物体表面法线的夹角挂钩:

$\text { bias }=k \cdot(1.0-\operatorname{dot}(\text { normal, lightDir }))$

阴影悬浮(Shadow Peter-panning)和Shadow Bias

这其实是由于ShdowBias导致的:

image-20231224204404293

本来不能通过阴影测试,bias设置过大,导致本该被遮挡的物体,因为减去了bias导致深度值变小,通过了阴影测试。

解决方法:

1.其实bias设置合理一点即可,不要太大。

2.也可以当渲染深度贴图时候使用正面剔除(front face culling),我们只利用背面,这样阴影失真也解决了,不再需要bias的辅助。使用这个方法可以免去反复调整bias,并且也不会导致悬浮的问题。但使用该方法的前提是场景中的物体必须是一个体(有正面和反面)而非原先的一个面

阴影走样Shadow Aliasing

我们上述的阴影有的锯齿:

image-20231224211837915

  • 锯齿本质是采样频率不够,最直接的解决方法就是 提高采样频率,但也会带来时间/空间的开销
  • 使用PCF/PCSS等各种效果更好的阴影算法

PCF(Percentage Closer Filtering)

​ 注意这里不是对shadowMap做模糊,而是如果有一点是ShadowMap对其周围泊松分布采样算出其被遮挡的比例作为阴影系数。

image-20240311133328519

​ Shadow Map采样不足,也就是分辨率不够会导致阴影锯齿,一种名为(Percentage Closer Filtering)的算法,主要优化阴影测试时的采样方式,对一个Shadow Map上的点的周围采样,这相当于做了模糊滤波,以解决阴影锯齿的问题。

​ PCF中采样方式有很多,比较好的采样方式是通过泊松圆盘分布采样,下图左边是通过均匀圆盘分布采样,右边是泊松圆盘分布采样。

img

  • 均匀圆盘分布采样(Uniform-Disk Sample):圆范围内随机取一系列坐标作为采样点;看上去比较杂乱无章,采样效果的噪声比较严重。
  • 泊松圆盘分布采样(Poisson-Disk Sample):圆范围内随机取一系列坐标作为采样点,但是这些坐标还需要满足一定约束,即坐标与坐标之间至少有一定距离间隔。

image-20240717021707409

​ 上图中右图为应用PCF后的效果,可以看到锯齿明显减轻,PCF是软化阴影的主要方法。

​ 硬件支持上,现在的硬件都直接提供周围四点采样的加权PCF深度测试,比如OpenGL中的sampler2DShadow,DirectX中的SampleCmp。这种采样的加权方式类似于普通像素采样时的双线性采样,在目标位置附近2×2像素中,逐像素进行深度比较,得到结果值0或1,然后将结果按照相对周围像素位置进行加权平均。

 如果希望更大的采样可以使用多次进一步利用硬件 PCF 技术,我们进行更多次这样的硬件采样,就可以得到更多采样的结果。例如,进行 4 次硬件采样就可以获得 4×4 个采样的结果:

image-20240717022323387

PCSS(Percentage-Closer Soft Shadows)

PCF虽然“软化”了阴影,但是我们阴影往往是由硬阴影+软阴影组成,PCF这种全软化的方式算不上很好的效果。

现实中的阴影往往是如下图,沿着红线越来越“软”

image-20231228171039621

PCSS就是动态适应的选择PCF模糊的size:

image-20231228171344785

如上图,只需要光源到遮挡物体表面的平均距离$d{Blocker}$, 投射阴影处到光源处的竖直距离$d{Receiver}$,还需要一个光源大小$W_{Light}$(可以理解为调整软阴影边界宽度的系数)。

image-20240102214451107

​ 可以发现法线过度生硬,这是因为$d{Blocker}$出现了突变,即我们计算average $d{Blocker}$的时候选择的采样半径太小,导致变化不够平滑,我们调整计算平均$d{Blocker}$的采样大小为40,$W
{light}$大小为50
,此时过度平滑了很多

image-20240102215517557

优化1: Blocker Search与PCF采样开销

​ PCSS中计算最耗时的部分在于Blocker Search部分计算光源到遮挡物体表面的平均距离和PCF的采样。这些耗时的计算都是服务于计算软阴影,可以考虑只在软阴影附近才进行这两部耗时的计算。有几种优化手段:

  1. 进行Block Search操作后就会得知:若比较结果为部分样本被遮蔽而另一部分样本不被遮蔽,则说明这是半影区。若全被遮挡说明是全阴影区,对于全阴影区在后续PCF中无需采样。

  2. 先生成一张Shadow Map,然后对其进行边缘检测,对边缘附近的才进行Block Search和后续PCF采样。

  3. 边缘检测本身就是比较耗时的 Pass,所以可能还得想个性能更优的方式,一个基于Tile的边缘检测想法:先根据 Tile 内各个像素的 Shadow mask 情况来判断出当前 Tile 属于完全遮蔽区,完全不遮蔽区或半影区;接着每个 Tile 读取周围 8 个 Tiles 的 Mask,如果本 Tile 为 Shadowed 而存在相邻 Tile 为 Unshadowed(亦或相反的情况),则本 Tile 也应该为半影区。最后我们只对 Penumbra Tiles 里的像素进行昂贵的 PCF/PCSS 计算。

    image-20240717022126259

优化2:解决漏光

​ 我们使用上述的两种软阴影方法PCF/PCSS中,不难发现因为做高斯模糊导致在脚趾处有一定的漏光,这是因为对于脚趾边缘的阴影点在计算时高斯模糊导致了边缘很软,解决漏光的方法也很直接,使用带权的高斯模糊,而不是权重相同,权重可以以两个点之间的世界空间距离的倒数作为权重,即两个点差距很大(一点在地板的阴影上,另一个点在脚趾上),这个点的权重贡献会很小。

image-20240717022420415