HPP_Graphics_4.3 实时阴影介绍

图形 4.3 实时阴影介绍

更深的可看笔记Games202 Real-time Shadow

a). 基于图片实时阴影技术

a.1). Shadow Map

  • 关键思想:一个点在相机视角中可见,但在光源视角中不可见,那该点就处于阴影中。

  • 做法: 在光源视角下渲染深度图。对于着色点$A$,将其转换到光源视角(其他空间也行,只要两者在同一坐标空间)后,与Shadow Map中对应点进行深度比较,判断一点是否在阴影中;

    深度比较

A 2-Pass Algorithm

  1. Light pass: Generate the SM(Shadow Map)
  2. Camera pass: uses the SM

Pass 1: Render from Light

  • 输出一张光源视角深度图(Depth Buffer)

SM_Pass01

Pass 2: Render from Eye(Camera)

SM_Pass02

  • 将光源视角对应的深度转换到View Space, 与Camera视角的深度进行深度比较;

    • 如$Depth_{cam} > Depth_{light}$ ,那说明该点在阴影中(相机可见,光源不可见)
    • 如$Depth_{cam} < Depth_{light}$ ,那说明该点在不在阴影中(相机可见,光源可见)

a.2). Unity中的屏幕空间阴影映射

SM00

Unity的阴影映射是屏幕空间阴影映射,即逐像素生成屏幕空间的阴影贴图后,再在Shading中采样,进而着色。

Step1: 渲染屏幕空间的深度贴图

Step2: 调用每个物体的Shadow Cast Pass,从光源方向生成Shadow Map

Step3: 屏幕空间进行阴影收集(Shadow Collector),即进行深度比较后,得到屏幕空间的阴影贴图;

  • Unity采用了屏幕空间的阴影映射,在进行阴影收集时,是逐Pixel进行的。

Step4: Shading时,用屏幕空间的uv,采样Step3中得到的屏幕空间阴影贴图;

SM01

b). 阴影映射优化

b.1). Self occlusion(自遮挡)

SelfOcclusion

  • Self occlusion: 也被称作Z-Fighting,阴影自遮挡,造成阴影毛刺的现象;
  • 原因: 如上图,
    • Shadow Map分辨率有限,一个像素内记录的深度值相同。如图中红色和橙色斜线表示Shadow Map中深度相同的位置($Depth_A = Depth_{A’}$);
    • 当计算平面中$B$点是否在阴影中时,$Depth_{light} = z1 = Depth_A$,而相机视角下的点$B$转换到光源视角下对应的深度为 $z2$ ,即$Depth_{cam} = z2 = Depth_B$
    • 因此,$Depth_{cam} > Depth_{light}$ ,说明该点在阴影中,因此造成Self occlusion
  • 解决方法: 引入Bias

    SelfOcclusion_Bias

    • 认为对于$B$点,如$Depth_{cam} > Depth_{light}$,但$Depth_{light}$ 处于橙色中,那该点仍然不在阴影中;
    • 即:
      • $Depth_{cam} > Depth_{light}+bias$,才使得该点在阴影中
      • $Depth_{cam} < Depth_{light}+bias$,该点不在阴影中
    • 易得,当光源方向垂直于平面时,所需的Bias最小,因此可引入光源与平面法线的夹角 $cos\alpha$ ,来调整Bias大小;
  • 引入bias会造成的问题:Detached shadow(不接触阴影,Peter Panning)

b.1.1). 偏移优化(Depth Bias)

DepthBias01

  • 深度偏移:简单添加Bias,使得该像素深度(Shading Point的深度,而不是SM中的深度)朝光源靠近;
  • 法线偏移:沿表面法线法线向外偏移;
  • 偏移单位是阴影纹理映射的纹素大小;

NormalBias01

b.1.2). Unity中的偏移优化

Unity_Bias


b.2). 走样

以信号重建的过程来审视阴影映射:

  • 初始采样:渲染Shadow Map;
  • 重采样:从摄影机视角对Shadow Map重采样;

ShadowMap_AA01

b.2.1). 初始采样 - 透视走样

透视走样

  • 原因: 由于Shadow Map初始采样是在摄影机进行透视投影之前(左图),使得经过透视投影后,近景大量pixel与Shadow Map中的同一个texel对应造成走样。
  • 解决方法:
    1. 在光源位置采样获得Shadow Map之前,我们先做一次透视投影。即采样经过透视投影后的场景;
    2. 级联阴影映射(Cascaded Shadow Map),减小近景和远景对应到屏幕空间中像素大小的差异;

b.3). 级联阴影映射(Cascaded Shadow Map)

CSM

实现:通过平行于视图方向的切片将视锥体截成多个子视锥体,每一个子视锥体都对应一张Shadow Map,每张Shadow Map独立计算但分辨率相同

  • 如果在View Space也就是视锥体划分级联,一旦镜头转动会产生很严重的闪烁,所以一般划分级联是在光源空间中划分。(https://zhuanlan.zhihu.com/p/92017307)

c). PCSS(Percentage-Closer Soft Shadow)

PCSS

c.1). PCF(Percentage Closer Filtering)

  • PCF用于抗锯齿,而不用于软阴影(用于软阴影的叫PCSS,两者实质是一个东西,但应用不同叫法不同)
  • 生成Shadow Map后,阴影比较时(即对阴影比较的结果),进行Filtering
    • 面光源生成Shadow Map:以面光源的中心点(放置相机)生成shadow map

PCF

  • 做法: 不止对着色点与其在Shadow Map中的对应点进行深度比较,而是着色点深度与其在Shadow Map中对应点及其周围点深度进行比较,最后对各个Visibility的结果取平均值(或加权平均)

    • eg1. $P$点在Cam视角下深度为$Depth_p$,转换到光源视角下深度为$Depth_{p’}$,$Depth_{p’}$ 与其在Shadow Map中对应点周围3x3(Filter size)像素进行比较,得到结果取平均得到Visibility为 0.667
  • Filter size
    • Small -> sharper
    • Large -> softer
    • 为选取合适的Filter size,产生了PCSS

c.2). PCSS(Percentage-Closer Soft Shadow)

c.2.1). 什么是PCSS?

  • 关键: 自适应Filter size

PCSS01

  • 观察可得:
    • 钢笔(Blocker)与接收平面(Receiver)的距离越小(笔尖),阴影越硬
    • 钢笔(Blocker)与接收平面(Receiver)的距离越大(笔尖),阴影越软
  • 即阴影的软硬程度,一部分取决于Blocker和Receiver的距离

PCSS_Key

  • 阴影的软硬取决于

    • $w_{Light}$ (光源的宽度)
    • $d_{Blocker}$ 与 $d_{BtoR}$ 的比值;
  • Blocker定义:

    Shading point变换到Light视角,对应深度为$Depth_{scene}$ 。查询区域内,深度值$z < Depth_{scene}$ 的texel即为Blocker;

  • $d_{Blocker}$ 为 Average blocker distance

    • Average blocker distance: Shadow Map一定范围内的Blocker的深度平均值

    • 类似eg1

      eg1. $P$点在Cam视角下深度为$Depth_p$,转换到光源视角下深度为$Depth_{p’}$,$Depth_{p’}$ 与其在Shadow Map中对应点周围3x3(Filter size)像素进行比较,得到结果

      取平均得到Visibility为 0.667

      其中,Visibility为0的点,即 处于阴影中,$Depth_{cam} > Depth_{light}+bias$ 的点即为Blocker,对Blocker在Shadow Map中的深度值取平均值,即得到Average blocker distance

c.2.2). 做法

PCSS_How

首先将shading point点$x$投应到shadow map上,找到其对应的像素点$P$。PCSS算法的实现流程如下:

第一步:Blocker search,即获取某个区域的平均遮挡物深度(在点p附近取一个范围(这个范围是自己定义或动态计算的),将范围内各像素的最小深度与x的实际深度比较,从而判断哪些像素是遮挡物,把所有遮挡物的深度记下来取个平均值作为blocker distance。)

第二步:Penumbra estimation,使用平均遮挡物深度计算滤波核尺寸(用取得的遮挡物深度距离来算在PCF中filtering的范围。)

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

  • 如何动态计算Blocker search的“某个范围”
    • PCSS_Region
    • Light越远,Region越小;Light越近,Region越大;(好像和图不太对应,如非要对应,就类似与Shadow Map位置不变,Light距离变大/小)

那么PCSS中那些步骤会导致速度变慢?

  • 第一步:Blocker search,需要多次采样查询深度信息并比较,计算Blocker的平均深度$d_{Blocker}$

  • 第三步:PCF,阴影越软→滤波核尺寸越大→采样查询次数变多→速度变慢

    • 由此可见,主要是多次采样并比较的方法使得速度变慢;
  • 加速方法:

    • 随机采样,后降噪;

    如果觉得区域过大不想对每一个texels都进行比较,就可以通过随机采样其中的texels,而不是全部采样,会得到一个近似的结果,近似的结果就可能会导致出现噪声。工业的处理的方式就是先稀疏采样得到一个有噪声的visibility的图,接着再在图像空间进行降噪。

    • Variance Soft Shadow Mapping(VSSM)

c.2.3). Math

PCF_02

  • 其中$\chi^{+}$ 类似于$step()$ 函数
    • $D_{\mathrm{SM}}(q)-D_{\text {scene }}(x) \geq 0$, 即$Depth_{ShadowMap} \geq Depth_{cam}$,$\chi^{+}\left[D_{\mathrm{SM}}(q)-D_{\text {scene }}(x)\right] = 1$
    • $D_{\mathrm{SM}}(q)-D_{\text {scene }}(x) < 0$, 即$Depth_{ShadowMap} < Depth_{cam}$,$\chi^{+}\left[D_{\mathrm{SM}}(q)-D_{\text {scene }}(x)\right] = 0$

Homework - 屏幕空间PCSS软阴影

HW_PCSS

本次实现了屏幕空间的软阴影。其主要步骤如下:

  1. 渲染屏幕空间的深度贴图

  2. 从光源方向生成Shadow Map;(下图深度经过EncodeFloatRGBA(),提高精度)

    HW_PCSS01

  3. 屏幕空间进行阴影收集(Shadow Collector),即进行深度比较后,得到屏幕空间的阴影贴图;

    HW_PCSS02

  4. Shading时,用屏幕空间的uv,采样Step3中得到的屏幕空间阴影贴图;

    HW_PCSS03

至于为什么要使用屏幕空间的软阴影,而不是直接在Shader中实现阴影主要有以下考虑:

  1. 屏幕空间的阴影可在一个Pass内完成,管理和调整方便,更贴近实际的应用;
  2. 屏幕空间的阴影性能开销小;
  3. 方便阴影的后处理;
  4. 此方案主要考虑场景阴影,而不考虑角色阴影。

此方案主要有2个C#脚本,3个Shader实现。

  • C#:
    1. DepthTextureCamera.cs: 实现从光源方向生成Shadow Map
    2. ShadowCollector.cs: 屏幕空间进行阴影收集(Shadow Collector)
  • Shader:
    1. CustomCaster.shader: 用于渲染深度图;
    2. ShadowCollector.shader: 用于逐像素比较深度,并实现具体的PCSS算法;
    3. CustromReciver.shader: 用于应用ShadowCollector得到的阴影图片,并通过屏幕空间的坐标进行采样,得到Visibility项;

以下,给出PCSS的主要代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
float findBlocker(float2 UVfromLight, float ZReciver) {	// 返回Blocker的平均深度
float num_occ = 0.0f;
float Z_avg = 0.0f;
float filterSize = 30.0f / _cusShadowMap_TexelSize.z;

for (int i = 0; i<BLOCKER_SEARCH_NUM_SAMPLES; i++) {
float Z_SM = DecodeFloatRGBA(tex2D(_cusShadowMap, poissonDisk[i]*filterSize + UVfromLight));
if (Z_SM < ZReciver) {
num_occ++;
Z_avg += Z_SM;
}
}
if (num_occ == 0) {
return 1.0;
}
return Z_avg / num_occ;
}

float PCSS(float4 coord) {
float zReceiver = coord.z / coord.w;
float2 uv = coord.xy / coord.w;
uv = uv * 0.5 + 0.5;
#if defined (SHADER_TARGET_GLSL) //(-1, 1) -> (0, 1)
zReceiver = zReceiver * 0.5 + 0.5;

#elif defined (UNITY_REVERSED_Z)
zReceiver = 1 - zReceiver;
#endif
poissonDiskSamples(uv); // 泊松圆盘采样
float visibility = 0.0f;

float zBlocker = findBlocker(uv, zReceiver);
// 计算半影宽度
float wPenumbra = (zReceiver - zBlocker) * LIGHT_WIDTH / zBlocker;

float scale = 1.0f;
float filterSize = scale * wPenumbra / _cusShadowMap_TexelSize.z + 1.0f / _cusShadowMap_TexelSize.z;;

// PCF
for (int i = 0; i < PCF_NUM_SAMPLES; i++) {
float zNear = DecodeFloatRGBA(tex2D(_cusShadowMap, poissonDisk[i]*filterSize + uv));
if (zNear > zReceiver) {
visibility += 1;
}
}

return visibility / PCF_NUM_SAMPLES;
}