HPP_Graphics_4.3 实时阴影介绍
图形 4.3 实时阴影介绍
更深的可看笔记Games202 Real-time Shadow
a). 基于图片实时阴影技术
a.1). Shadow Map
关键思想:一个点在相机视角中可见,但在光源视角中不可见,那该点就处于阴影中。
做法: 在光源视角下渲染深度图。对于着色点$A$,将其转换到光源视角(其他空间也行,只要两者在同一坐标空间)后,与Shadow Map中对应点进行深度比较,判断一点是否在阴影中;
A 2-Pass Algorithm
- Light pass: Generate the SM(Shadow Map)
- Camera pass: uses the SM
Pass 1: Render from Light
- 输出一张光源视角的深度图(Depth Buffer)
Pass 2: Render from Eye(Camera)
将光源视角对应的深度转换到View Space, 与Camera视角的深度进行深度比较;
- 如$Depth_{cam} > Depth_{light}$ ,那说明该点在阴影中(相机可见,光源不可见)
- 如$Depth_{cam} < Depth_{light}$ ,那说明该点在不在阴影中(相机可见,光源可见)
a.2). Unity中的屏幕空间阴影映射
Unity的阴影映射是屏幕空间阴影映射,即逐像素生成屏幕空间的阴影贴图后,再在Shading中采样,进而着色。
Step1: 渲染屏幕空间的深度贴图;
Step2: 调用每个物体的Shadow Cast Pass,从光源方向生成Shadow Map;
Step3: 屏幕空间进行阴影收集(Shadow Collector),即进行深度比较后,得到屏幕空间的阴影贴图;
- Unity采用了屏幕空间的阴影映射,在进行阴影收集时,是逐Pixel进行的。
Step4: Shading时,用屏幕空间的uv,采样Step3中得到的屏幕空间阴影贴图;
b). 阴影映射优化
b.1). Self occlusion(自遮挡)
- 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;
- 认为对于$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)
- 深度偏移:简单添加Bias,使得该像素深度(Shading Point的深度,而不是SM中的深度)朝光源靠近;
- 法线偏移:沿表面法线法线向外偏移;
- 偏移单位是阴影纹理映射的纹素大小;
b.1.2). Unity中的偏移优化
b.2). 走样
以信号重建的过程来审视阴影映射:
- 初始采样:渲染Shadow Map;
- 重采样:从摄影机视角对Shadow Map重采样;
b.2.1). 初始采样 - 透视走样
- 原因: 由于Shadow Map初始采样是在摄影机进行透视投影之前(左图),使得经过透视投影后,近景大量pixel与Shadow Map中的同一个texel对应造成走样。
- 解决方法:
- 在光源位置采样获得Shadow Map之前,我们先做一次透视投影。即采样经过透视投影后的场景;
- 级联阴影映射(Cascaded Shadow Map),减小近景和远景对应到屏幕空间中像素大小的差异;
b.3). 级联阴影映射(Cascaded Shadow Map)
实现:通过平行于视图方向的切片将视锥体截成多个子视锥体,每一个子视锥体都对应一张Shadow Map,每张Shadow Map独立计算但分辨率相同。
- 如果在View Space也就是视锥体划分级联,一旦镜头转动会产生很严重的闪烁,所以一般划分级联是在光源空间中划分。(https://zhuanlan.zhihu.com/p/92017307)
c). PCSS(Percentage-Closer Soft Shadow)
c.1). PCF(Percentage Closer Filtering)
- PCF用于抗锯齿,而不用于软阴影(用于软阴影的叫PCSS,两者实质是一个东西,但应用不同叫法不同)
- 在生成Shadow Map后,阴影比较时(即对阴影比较的结果),进行Filtering
- 面光源生成Shadow Map:以面光源的中心点(放置相机)生成shadow map
做法: 不止对着色点与其在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
- 观察可得:
- 钢笔(Blocker)与接收平面(Receiver)的距离越小(笔尖),阴影越硬
- 钢笔(Blocker)与接收平面(Receiver)的距离越大(笔尖),阴影越软
- 即阴影的软硬程度,一部分取决于Blocker和Receiver的距离
阴影的软硬取决于
- $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). 做法
首先将shading point点$x$投应到shadow map上,找到其对应的像素点$P$。PCSS算法的实现流程如下:
第一步:Blocker search,即获取某个区域的平均遮挡物深度(在点p附近取一个范围(这个范围是自己定义或动态计算的),将范围内各像素的最小深度与x的实际深度比较,从而判断哪些像素是遮挡物,把所有遮挡物的深度记下来取个平均值作为blocker distance。)
第二步:Penumbra estimation,使用平均遮挡物深度计算滤波核尺寸(用取得的遮挡物深度距离来算在PCF中filtering的范围。)
第三步:Percentage Closer Filtering,对应该滤波核尺寸应用PCF算法。
- 如何动态计算Blocker search的“某个范围”
- 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
- 其中$\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软阴影
本次实现了屏幕空间的软阴影。其主要步骤如下:
渲染屏幕空间的深度贴图;
从光源方向生成Shadow Map;(下图深度经过EncodeFloatRGBA(),提高精度)
屏幕空间进行阴影收集(Shadow Collector),即进行深度比较后,得到屏幕空间的阴影贴图;
Shading时,用屏幕空间的uv,采样Step3中得到的屏幕空间阴影贴图;
至于为什么要使用屏幕空间的软阴影,而不是直接在Shader中实现阴影主要有以下考虑:
- 屏幕空间的阴影可在一个Pass内完成,管理和调整方便,更贴近实际的应用;
- 屏幕空间的阴影性能开销小;
- 方便阴影的后处理;
- 此方案主要考虑场景阴影,而不考虑角色阴影。
此方案主要有2个C#脚本,3个Shader实现。
- C#:
- DepthTextureCamera.cs: 实现从光源方向生成Shadow Map;
- ShadowCollector.cs: 屏幕空间进行阴影收集(Shadow Collector)
- Shader:
- CustomCaster.shader: 用于渲染深度图;
- ShadowCollector.shader: 用于逐像素比较深度,并实现具体的PCSS算法;
- CustromReciver.shader: 用于应用ShadowCollector得到的阴影图片,并通过屏幕空间的坐标进行采样,得到Visibility项;
以下,给出PCSS的主要代码:
1 | float findBlocker(float2 UVfromLight, float ZReciver) { // 返回Blocker的平均深度 |