图形学渲染基础(6)实时阴影(Real-Time Shadows)

2,321 阅读9分钟

shadowMapping

实时阴影的生成是实时渲染中的一个重要的组成部分。所以阴影的生成使得渲染结果更加真实。那么要怎么生成阴影?一个基础的方法就是Shadow Mapping 1409576-20210312081535978-551873964.png Shadow Mapping 基本原理:

  1. 阴影生成 Pass:
  • 额外设置一个摄像机在光源位置(Light Camera,光源摄像机),并且朝光照方向看去。
  • 用一张 Texture(称为 阴影贴图 Shadow Map)来记录 Light Camera 所看到的像素深度(每个像素位置只记录所见最近深度,而不用做别的 shading 计算)来作为遮挡深度。 image.png 2.渲染Pass:
  • 主摄像机需要渲染屏幕每个像素时,该像素对应的世界坐标进行 Light Camera 的MVP变换后能得到在 Light Camera 屏幕空间中的对应位置 shadowCoord=(x′,y′,z′)
  • Shadow Map 里用(x′,y′)(采样得到的遮挡深度 depthdepth 与深度值 z′z′ 做比较: 若 depth<z′(意味着该像素的光被遮挡),这时就可以对该像素降低可见度(Visibility)。 image.png Shadow Mapping是一种非常好用的实时生成阴影的方法,他不需要已知场景中的任何几何信息,仅需要在正式渲染之前多进行一次渲染,生成一张Shadow Map,即可得到不错的阴影生成效果。通常适用于场景中的平行光(注意,如果光照没有方向性,可能需要在不同方向生成多张Shadow Map。比如,如果光源是点光源,可能需要沿坐标轴的六个方向分别以90度视角1:1宽高比渲染到一个cubemap的六个面上)

Shadow Bias

shadow map与真实的数字图像相同,是由一个个像素组成,只不过像素值不是RGB,而是深度,记录的深度值是离散的不连续的,而且shadow map的分辨率会直接影响到数字之间的离散程度。

image.png Shadow Bias是针对Shadow Acne现象引入的概念,由于Shadow Bias本身的使用也会有一些问题,因此后续会提出一种自适应Bias的方法。

  • Shadow Bias过小会产生Shadow Acne的现象
  • Shadow Bias过大会产生Peter Panning的现象

Shadow Acne

Shadow acne 产生的原因根本原因就是 shadow map的分辨率不够,因此多个 pixel 会对应 map 上的同一个点。 image.png 图中黄色箭头是照射的光线,黑色长方形是实际物体表面,黄色的波浪线是 shadow map中的对应值的情况。

   可以看到,由于map是对场景的离散取样,所以黄色的线段呈阶梯状的波浪变化,相对于实际场景中的情况,就有一部分比实际场景中的深度要大(对应着黑色线段部分),着部分不会产生阴影(注意图画反了);一部分比实际场景中的深度要小(对应着黄色线段部分),这部分会产生阴影,所以就出现了条纹状的阴影。

   由于这种情况,是物体的实际深度,与自己的采样深度,相比较不相等(实际深度大于采样深度)导致的,所以可谓是自己(采样的副本)遮挡了自己(实际的物体),所以被称为 self shadowing。

解决方法:

image.png

  • 直接给采样阴影深度加一个 偏移量 Bias(相当于把阴影深度往远处加,从而更不容易产生遮挡)。

Peter Panning

image.png 这个现象只有在加入了 shadow Bias的时候才会出现,如上图往往在物体缝隙间发生漏光现象。由于Shadow Bias过大,阴影的计算位置和物体之间不太贴合,产生中间的缝隙,就像影子脱离了物体(像飞起来的小飞侠一样)被称为 peter-panning

解决方法:

  • 不使用过大的Bias
  • 避免使用单薄的几何体(例如薄墙、薄地面);只要几何体厚度大于Bias,影子边界便会产生在几何体内部,从而不易看见影子与几何体的分离现象。
  • 不采用Bias,第一个Pass生成Shadow map时设置成仅渲染背面(正面剔除),相当于给出高门限

Slope Scale Based Depth Bias

通过上面知道,Bias 过小时可能不能解决 Shadow Acne 现象,Bias 过大时又可能导致严重的 Peter Panning问题。 image.png Slope Scale Based Depth Bias :为了尽可能减少由于 Bias 过大过小引起的问题,采取了根据平面倾角的一种自适应 Bias(例如:当光线与平面垂直时,Bias应该为0;当光线与平面的夹角越小,则Bias应越大)。

抗锯齿

Shadow Mapping 还存在 阴影锯齿(Shadow Aliasing)  问题 image.png Percentage Closer Filtering(PCF) 正是解决阴影锯齿的方案,它的核心想法是计算阴影时不是考虑单个采样点,而是在一定范围内进行多重采样,这样可以让阴影的边缘不那么锯齿,因为 Visibility 不再是非0即1,而是带有渐变的取值,而PCSS和VSSM都都是软阴影的额优化手段。

PCF

PCF采样过程中会遇到主要的两个问题:

  • 采样点的选择
  • 采样范围的确定

首先,我们将在这里解决第一个问题,采样点的选择。在对周围一定范围内若干个坐标进行采样的时候,可以通过分布采样函数来确定 NUM_SAMPLES 个采样位置,为了让阴影边缘更加柔和,我们可以用一些较好的分布采样函数。

  • 均匀圆盘分布采样(Uniform-Disk Sample) :圆范围内随机取一系列坐标作为采样点;看上去比较杂乱无章,采样效果的 noise 比较严重。

  • 泊松圆盘分布采样(Poisson-Disk Sample) :圆范围内随机取一系列坐标作为采样点,但是这些坐标还需要满足一定约束,即坐标与坐标之间至少有一定距离间隔。

image.png PCF具体的算法过程如下:

  1. 计算 Visibility 时,原本对 Shadow Map 的一次坐标采样换成对周围一定范围内若干个坐标进行采样。
  2. 各个采样结果同样用来与 z′z′ 做比较,最后取比较结果的平均作为 Visibility。
float visibility_PCF(sampler2D shadowMap, vec4 coords) {
const float bias = 0.005; 
float sum = 0.0;
// 初始化泊松分布 
poissonDiskSamples(coords.xy); 
// 采样 
for(int i = 0;i<NUM_SAMPLES;++i){ 
float depthInShadowmap=unpack(texture2D(shadowMap,coords.xy+disk[i]*0.001).rgba); 
sum += ((depthInShadowmap + bias)< coords.z?0.0:1.0);
} 
// 返还平均采样结果
return sum/float(NUM_SAMPLES);
}

从下图可知,使用了PCF后阴影的锯齿现象缓解了很多。 image.png

PCSS

PCF的第二个问题就是 采样范围的选择,即所谓的滤波核尺寸大小

  • 滤波核越小-----模糊程度越低-----阴影越硬(尖锐)
  • 滤波核越大-----模糊程度越高-----阴影越软 PCSS(Percentage Closer Soft Shadows) 的核心思想就是控制PCF算法中滤波核的大小来生成软阴影

总的来说,PCSS算法的实现流程如下:

第一步:Blocker search,即获取某个区域的平均遮挡物深度dBlocker

第二步:Penumbra estimation,使用dBlocker计算滤波核尺寸Wpenumbra

第三步:Percentage Closer Filtering,对应该滤波核尺寸应用PCF算法

Blocker Search

Blocker Search是指获取某个区域平均遮挡物深度,为此,我们可以对 ShadowMap 的一定范围内进行多重采样,每次采样得到的深度若小于 dReceiver 则认为遇到遮挡物并算入平均遮挡深度的贡献,这样多重采样之后得到的平均遮挡深度就作为 dBlockerdBlocker。那么这个范围怎么确定呢? image.png 如图所示,根据Light的范围和投影点与光源的距离确定了查询范围,具体推导过程省略 image.png

Penumbra estimation

那么接下来需要解决的问题就是如何确定滤波核的大小,一个直观的观察结果是距离遮挡物越远的区域,阴影越软,需要的滤波核尺寸越大。具体的滤波核尺寸与遮挡物深度的关系如下图所示 image.png image.png 其中,wLight 是光源面积尺寸,dBlocker是遮挡物的深度,dReceiver是被投影物(实际上就是shading point)的深度。

Percentage Closer Filtering

第三步就是根据滤波核生成软化阴影,代码思路如下:

float visibility_PCSS(sampler2D shadowMap, vec4 coords){
poissonDiskSamples(coords.xy); 
// STEP 1: avgblocker depth 
float dBlocker = findBlocker(shadowMap,coords.xy,coords.z); 
// STEP 2: penumbra size 
const float wLight = 0.006;
float penumbra = (coords.z-dBlocker)/dBlocker * wLight; 
// STEP 3: filtering 
const float bias = 0.005;
float sum = 0.0; 
for(int i = 0;i<PCF_NUM_SAMPLES;++i){ 
float depthInShadowmap = unpack(texture2D(shadowMap,coords.xy+disk[i]*penumbra).rgba); 
sum += ((depthInShadowmap + bias)< coords.z?0.0:1.0); } 
return sum/float(PCF_NUM_SAMPLES);
}

VSSM

Variance Soft Shadow Mapping(VSSM)其实是对PCSS中的Blocker search和PCF进行优化的过程,因为这两部都需要多重采样,严重影响了算法的时间效率,如何优化这一过程就是我们接下来要考虑的事情。

为了避免多重采样的计算,VSSM假定一定范围内的深度的分布符合 正态分布(Normal Distribution)  ,那么只要知道该段范围的均值和方差,就能先得到该范围的正态分布模型(即知道对应的 概率密度函数 PDF) image.png 当我们知道该正态分布的PDF,即可以得到CDF(累计分布函数),通过CDF就能快速推算出该范围内有多少比例的 x 大于(或小于)给定的某个值。那么,接下来的问题就是如何获取一定范围内深度的平均值,即数学期望E(X)和方差Var(X)。

一些优化手段

  • E(x)的计算可以通过mipmap和SAT(前缀和数组)方式
  • Var(x)的计算可以公式Var(x)=E(X²)−E²(X),E(X²)对应的是深度²的shadow Map
  • 通过切克比夫不等式优化CDF的计算
  • 通过上述手段可以加速Blocker Search算法和PCF算法

参考