Shadow - 阴影简述
ShadowVolume
- ShadowVolume是从光源沿着模型边缘拉伸至无限远处,与前盖(投射阴影的对象)和后盖(接收阴影的对象)组合成的网格。
- 只有在ShadowVolume内部的物体,才具有阴影。
- z-pass算法
- pass1:打开深度测试,渲染整个场景,得到深度图。
- pass2:打开模板测试,并设置为Always Pass,关掉深度和颜色写入,渲染ShadowVolume网格。
- 深度测试通过时:若是front faces,则stencil value +1;若是back faces,则stencil value -1。
- pass3:pass2中stencil value不为0的像素即为阴影区域,据此绘制阴影。
- 缺陷:视点位于阴影锥内时会得到错误的结果。
- z-fail算法
- pass1和pass3与z-pass算法保持一致。
- pass2改为:
- 深度测试失败时:若是front faces,则 stencil value -1;若是back faces,则stencil value +1。
- 修复了z-pass算法的缺陷,但ShadowVolume必须是闭合的。
- ShadowVolume阴影依赖Stencil Buffer实现,没有锯齿,但计算ShadowVolume会占用较多的CPU耗时,几何体也必须是闭合的,有一定限制。
ShadowMap
ShadowMap的实现
- 主要分为两个步骤:
- 步骤1:在光照空间中渲染投影对象,写入深度,得到ShadowMap。
- 步骤2:在相机空间正常渲染对象,将渲染对象的坐标转换到光照空间中,将其相对光源的距离与其在Shadow Map中的深度值进行比较,若其距离更大,则在阴影中。
- 光照空间视锥的计算:
- 将世界空间视锥的8个顶点,变换到光照空间,计算出根据最远和最近的z值,再计算出AABB的边界。
- 还会根据整个场景的AABB空间,对得到的光照空间视锥进行扩展,使其能覆盖到可能产生阴影的物体。
- 补充:
- 对于平行光源,需要用正交投影来计算深度。
- 对于锥形光源,需要沿其光照方向生成ShadowMap。
- 对于点光源,为了能保存各个方向的深度值,需要使用cube map。
ShadowMap的锯齿问题
- ShadowMap是在光照空间中计算的,在相机空间近处观察时,或在斜面上渲染阴影时,可能会因为两个空间中的像素比例存在较大差距而产生锯齿。
- 解决方案:
- Perspective Warping(透视扭曲),通过修改光照空间的投影矩阵,来为视锥近处的对象阴影提供更高的精度。但其在相机移动时非常不稳定,目前已经被彻底淘汰。
- Cascaded Shadow Maps(CSM,级联阴影),将视锥分割成数个部分,每个部分单独生成一张ShadowMap,最后组合成一张Atlas。
- Perspective Warping(透视扭曲),通过修改光照空间的投影矩阵,来为视锥近处的对象阴影提供更高的精度。但其在相机移动时非常不稳定,目前已经被彻底淘汰。
ShadowMap的自阴影走样问题
- ShadowMap中存储的深度值和阴影接收对象的深度值可能是相同的,但由于ShadowMap的精度限制,会在比较深度值时产生误差,出现摩尔纹的问题。
- 解决方式:
- 使用ShadowBias,对采样时的深度值,沿着光照的方向进行偏移(一般是在vertex shader中添加bias来偏移计算出的ShadowMap深度值)。
- 有三种偏移方式:常量偏移、斜率偏移、法线偏移。(Unreal中是常量+斜率,Unity中是常量+法线)。
- 将物体背面的深度写入ShadowMap,也可以避免深度冲突的问题,但要求使用的模型必须是正确封闭的而且也不能太薄,有着很大的限制,不太通用。
- 使用ShadowBias,对采样时的深度值,沿着光照的方向进行偏移(一般是在vertex shader中添加bias来偏移计算出的ShadowMap深度值)。
ShadowMap的软阴影
PCF
- Percentage-Closer Filtering
- 硬件PCF:在目标采样点周围,进行四次采样,并根据其深度测试的结果(0或1),进行加权平均。
- 优化:使用预先算好的Possion(泊松分布)的采样点进行采样,并使用逐像素的噪声值对采样点进行旋转,让相邻像素的采样模式都不同,使半影区域更加平滑。
- 缺陷:半影的宽度非常固定。
PCSS
- Percentage-Closer Soft Shadows
- PCSS会通过判断半影到遮挡物和半影到光源的距离,来动态确定半影的宽度。
- 半影的宽度越大,采样阴影的kernel分布也越大,得到的阴影也越柔和。
- 但半影宽度非常大时,需要的采样点也非常多,采样ShadowMap的开销也会变大,是一种不太稳定的软阴影方案。
左:硬阴影,中:PCF,右:PCSS
Filterable ShadowMap
- Filterable ShadowMap也是一类软阴影的实现方案。
- 在采样非常软的阴影时,相对PCF,有性能优势,但在硬阴影下,性能反而会下降。
- 而且还需要考虑硬件兼容,以及漏光等边界条件,一般只在静态烘焙阴影中使用。
VSM
- Variance Shadow Map,方差阴影图
- 使用两张ShadowMap,分布存储深度值和深度值的平方。
- 将两张ShadowMap进行mipmap处理(或者使用双pass高斯模糊),再使用切比雪夫不等式计算阴影系数。
- 在复杂光照环境下,可能会出现漏光的问题。
ESM
- Exponential Shadow Map,指数阴影图
- 将到光源的距离d,与当前方向上最近的遮挡物的距离z,进行相减,再乘上系数c并作为阴影系数的指数。
- 将其结果替代原本的遮挡结果(0或1),再进行PCF的计算。
EVSM
- Exponential Variance Shadow Map,指数方差阴影图
- 在使用VSM的前,将深度使用一个指数函数进行处理,可以得到一个更好的阴影效果。
- 是一种对VSM的改进,但需要用到4张ShadowMap。
ShadowMap相关的技术
Stabilize CSM
- 移动相机时,相机的视锥发生改变,光照空间的视锥也会发生改变,需要重新计算,容易造成两帧的阴影位置不一致,产生阴影的闪烁问题。
- Stabilize CSM将相机的移动分为两个部分来处理:
- 旋转:根据相机空间的视锥计算出球形的bound volume,用其算出光照空间的视锥,这样,当我们沿着球体中心旋转时,光照空间的视锥是不变的。
- 平移:通常会取世界空间的零点作为参考点,变换到ShadowMap的坐标中,后续通过偏移投影矩阵,确保前后两帧得到的ShadowMap坐标对齐于同一像素。
CSM Cache
- 降低远处CSM的更新频率。
- 将CSM中算出的阴影进行动态缓存,上一帧静态物体的ShadowMap经过一些处理,在当前帧仍然可用,只需绘制未覆盖区域的阴影。
Virtual Shadow Map
- TODO
ShadowMap和ShadowMask
- 都是阴影贴图。
- ShadowMask在编辑器下预计算(烘培)生成,专门用于静态物体。
- ShadowMap在运行时计算生成,可以用于所有物体。
其它的阴影方案
贴图阴影
- 直接将阴影做成一张贴图,贴在角色的脚下。
平面投射阴影
- 将需要投射阴影的对象再渲染一次,根据平面的位置,计算出投射的矩阵,将物体的坐标变换到平面上。
- 通常有两种渲染顺序:
- 先渲染阴影,再渲染地面和投影对象,需要设置depthBias,否则地面和阴影之间可能会产生深度冲突。
- 先渲染地面,再渲染阴影(关闭深度测试),最后再渲染投影对象。
- 平面投射阴影是将阴影的颜色与阴影接收对象的颜色进行混合,并不是真实的阴影。
Modelated Shadow
- 调制阴影
- 将投影对象的AABB转换到光照空间,再将其深度渲染到一张ShadowMap中。
- 将光照空间的AABB沿着光照方向进行延长,得到一个ShadowVolume。
- 将ShadowVolume作为几何体进行渲染,用_CameraDepthTexture获取当前位置的深度值,反算出世界坐标。
- 再根据世界坐标算出光照空间下的坐标,在ShadowMap中进行采样,获取阴影。
- Modelated Shadow是Per-Object方式的动态阴影,可以提升角色的阴影精度。
Contact Shadow
- 接触阴影
- 利用深度构建三维空间,从当前绘制像素往Light方向做RayMarching。
- 判断每个step的深度是否有邻近深度,若有,则表明被这个深度值所对应的几何遮挡了,从而判定在阴影内。
基于SDF的阴影
- Signed Distance Field,有向距离场
- 每个像素记录自己与距离自己最近物体之间的距离。
- 如果在物体内,则距离为负;在物体边界,则为0;在物体外,则距离为正。
- 需要一组中间过程图,并对每张图生成对应的SDF图,然后利用SDF进行插值,得到最后的阴影结果。
Capture Shadow
- 又称Capture AO
- 用胶囊体对动态物体进行模拟,以实现非直接光照环境下的动态软影模拟。
- 需要用球谐函数SH计算环境光和方向光两个分量。
半透物体的阴影
- 主要是利用抖动实现半透明的效果。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 鹏の箱庭!