目录
前言
什么是阴影
- 当片元到光源路径上出现不透明物体时,该片元处于阴影状态。
P1
平面投影阴影
根据光的方向,利用相似三角形,把物体的每个顶点投影到平面地面上。
缺点
- 只能投影到平面。
- 投影物体必须在光线和平面之间。
步骤:
- 为每个阴影投影体设置一个相机。
- 把阴影投影体渲染到阴影纹理中。
- 渲染阴影接收者,并且与阴影纹理进行混合。
阴影映射(Shadow Map)
在灯光位置放一个摄像机,生成这个摄像机的深度图(Shadow Map)。
在渲染物体上方的阴影时,需要让物体位置的着色点转到灯光的视角空间下,对应的深度值设为A,而灯光深度图(Shadow Map)在着色点对应的位置的采样结果设为B,让这两个进行比较,如果A>B,说明在灯光视角下,看不到着色点,这时将着色点设为阴影,反之就是没有阴影。
屏幕空间阴影映射
步骤:
- 渲染屏幕空间的深度贴图。
- 从光源方向渲染阴影映射。
- 在屏幕空间做一次阴影收集计算(Shadows Collector),这次计算会计算得到一张屏幕空间阴影纹理。
- 在绘制物体的时候,用物体的屏幕坐标UV,采样3中生成的屏幕空间阴影贴图。
在Frame Debug中的渲染流程:
阴影映射优化
阴影映射自阴影问题
Z-Fighting问题
如上图,由于阴影映射的分辨率有限,离散的采样点以及数值上的偏差可能造成不正确的自阴影,也被成为Z-Fight或者阴影粉刺(Surface Acne)。
当比较深度时,为了避免表面自阴影,需要设置容错阈值。
优化方案:
- 深度偏移(Depth Bias):设置差值的容错阈值来比较深度。
- 法线偏移(Normal Bias):上一条中的方向为视角方向,本方法在法线方向上偏移。
补充:偏移单位为纹素(1/分辨率),只在阴影深度测试时使用,不影响其他效果。
但当深度偏移设置过大时,会导致漏光现象。即阴影与投影者之间发生脱节,也叫做Peter Panning问题。所以对于偏移值应当合理设置。
注意点:
- 深度偏移:增加深度偏移会使该像素向光源靠近。
- 法线偏移:沿表面法线方向向外偏移。
- 偏移单位是阴影映射的纹素。比如说ShadowMap分辨率为512,那么对应的Bias为1/512。
- 在Shadow Receive计算阶段是逐像素进行的,只会在阴影深度测试使用,不会影响真实场景。
- bias是在阴影深度测试的时候使用,不会影响真实场景中物体的法线。
Unity中的偏移优化
在Unity中计算屏幕空间阴影映射时,它会有一个叫ShadowCaster的Pass用来计算物体的深度值,在这个Pass中,主要就是对顶点位置做深度偏移和法线偏移的,然后才记录其深度。
其中的Normal Bias的使用如下图:
由以上代码可以知道,对于Normal Bias的使用,实际上就是对深度测试中的顶点做法线方向的反向偏移运算。
法线方向的反向偏移值运算就是根据光源方向和法线方向的夹角大小(这里的夹角大小通过Sin值反映),夹角越大则偏移值越大,如果光源方向和法线方向重叠,那么就不会偏移。而对于unity_LightShadowBias.z 实际上就是Unity中阴影设置中的法线偏移值。
Unity中的阴影中法线偏移设置:
阴影映射走样问题
透视走样
原因解释
- 阴影映射在世界空间均匀分配。
- 视锥体物体经过透视投影后,近大远小,近平面和远平面的像素一样。
- 靠近观察者的元素所用到采样点明显变少。
- 这种误差被称为透视走样。
下图可以看到,左边部分是在世界空间下的视图,物体均匀分配,而在右边部分是经过透视投影计算了的,空间分配是近大远小的
下图可以看到,B处部分因为距离观察者近,它所占据的阴影部分其实不能用更多的重采样的深度测试来对比,所以产生了一些边缘锯齿,A处部分则相反,虽然重采样点相同,但是需要表达的阴影面积小,所以远处的锯齿感并不明显。
解决方案-级联阴影映射(Cascaded Shadow Map)
级联阴影映射是透视走样最有效的解决方案。它可以把视锥体分割为多个子视锥体,然后为每个子视锥体计算独立的相等大小的阴影映射。
下图是划分两层级联阴影的视锥体:
可以看到,在视锥体近处渲染一个ShadowMap,远处也渲染一个,这样就能在与相机不同的距离的位置采样不同的ShadowMap以解决之前透视走样的问题。
但是缺点也很明显,会占用更多的内存。
(跟mipmap一样的占用量)
Unity中级联阴影的实现
下图是Unity中阴影的一些设置,在Project Settings/Quality中可以打开:
Showmak Mode:包含Shadowmask和Distance Shadowmask的选择。
Shadows:包含硬阴影和软阴影的选择。
Shadow Resolution:Shadow Map的分辨率选项,低、中、高、非常高。
Shadow Projection:这个选项会影响级联带的形状。一般是默认“Stable Fit”,在这个模式下,根据到相机的距离选择频段。另一个选项是“Close Fit”,根据相机的深度选择频段,这样可以更有效地使用阴影纹理,但是这个选项会导致改变相机位置的同时会产生阴影锯齿游泳的情况。
Shadow Distance:阴影距离。即光源相机到物体的距离。
Shadow Near Plane Offset:阴影近平面的偏移。
Shadow Cascads:阴影的级联数选项。目前有1、2、4选项,1就是没有开启级联阴影。
Cascade splits:显示每个级联阴影的距离范围占比。
从帧调试器中的级联阴影截图:
可以看到,对于整个ShadowMap来说,它是划分成了四部分,每部分存储不同距离的级联阴影的ShadowMap。
而且Unity也会根据设置的阴影距离显示不同数量的级联阴影,如果把距离拉高的话,可能就会出现如下情况:
如上图,只渲染了三个级联阴影。
重采样误差
原因解释
阴影映射是一张动态生成的纹理;
滤波是纹理采样误差的解决方案。
以下是设置图片滤波方式的截图:
什么是滤波?
- 图像处理中,通过滤波强调一些特征或者去除图像中一些不需要的部分。
- 滤波是一个邻域操作算子,利用给定像素周围的像素的值决定此像素的最终的输出值。
什么是阴影的滤波?
- 使用一部分阴影映射采样点来计算某个指定View采样点的最终阴影结果的方法。
解决方案 PCF滤波(Percent-Closer Filter)
流程图:
- 从光方向生成阴影映射。
- 从View视角渲染场景,用阴影映射进行深度测试。
- 当该着色点通过深度测试时,取该像素周围指定大小(滤波核)的范围,然后取这个范围的平均值作为输出。最后它的输出值就是"可见采样点数/总采样点数",范围是[0,1],代表着该像素的可见性。
所以通过周围像素来过滤决定当前阴影的颜色,我们称之为PCF滤波。这种算法的具体实现不是唯一的,可以有很多种采样思想,不同的实现方式主要体现在以下两方面:
采样数K:
规则滤波,3*3或5*5
采用Poisson Disk的形式来分布一定数量的采样点。
滤波核函数:
高斯函数作为滤波函数,也可以用其它。
下图就是Unity中用3*3高斯核做的一个滤波采样:
PCF滤波的缺点:
因为滤波的卷积运算总会涉及到多次采样,所以非常影响性能
Unity阴影映射优化总结
- 使用深度偏移和法线偏移解决自阴影的问题。
- 使用级联阴影映射解决透视走样的问题。
- 使用PCF解决重采样的问题,并且产生软阴影的效果。
P2
优化总结
方法 | 内容 | 解决问题 |
深度偏移 | 设置差值的容错阈值来比较深度 | 自阴影 |
法线偏移 | 在法线方向上偏移,只在阴影深度测试时使用,不影响其他效果 | 自阴影 |
级联阴影映射 | 生成不同分辨率的shadow map,根据视角深度远近选择对应映射 | 透视走样 |
PCF滤波 | 深度对比是一个点与一个范围内点的对比,再对结果求平均当最终可见值 | 重采样,能实现软阴影 |
PCSS | 相当于在PCF前加一步对范围大小的选择,以此实现软硬阴影的结合 | 软硬结合更模拟现实 |
VSSM | 计算累计分布函数,加速Blocker Search算法,加速PCF算法 | 优化PCSS PCF重采样 |