基于图片实时阴影技术
平面投影映射
平面投影阴影
根据光的方向,把物体的每个顶点投影到平面地面上
平面投影映射并不是一个基于图片的解决方案,归类这里是因为其思路值得借鉴,而且性能开销小
数学原理:
- 相似三角形
缺点:
- 只能投影到平面(阴影的接收物只能是平面)
- 投影物体必须在光线和平面之间
投影阴影
目的:
- 为了解决平面投影阴影不能投射非平面上
原理:
- 把光源当做一个相机/投影器
- 把阴影投影体渲染到阴影纹理中
- 渲染阴影接收者时,与得到的阴影纹理混合
投影阴影在Unity中的实现
参考教程:Blob Shadow Projector - Fake Shadow:https://www.youtube.com/watch?v=hQcZA3dYGxg
阴影映射(shadow mapping)
不同视角下的场景
相机视角:
光源视角(将光源当做相机渲染深度):
确定阴影:
- 相机视角能见着 + 光源视角能见着 = 光照亮区域,无阴影
- 相机视角能见着 + 光源视角不能见着 = 光照不到区域,有阴影
阴影映射流程
- 从光源的位置生成一张深度图(shadowmap),理论上一盏光源一张shadowmap
- 渲染片元时,将坐标转化为光源所处坐标系,与记录在shadowmap中的深度作比较
- 如果一个片元在阴影坐标的深度>它在shadowmap中的深度,那么就认为它在阴影中(说明光被他前面的物体挡住)
Unity中的屏幕空间阴影映射
流程
该方法原本是延迟渲染中产生的方法,传统的阴影映射流程如上一节所述,对比深度时需要将片元的坐标转换到光源所在的坐标系中,多了一次矩阵运算。而屏幕空间的阴影是直接将shadowmap记录在屏幕空间下,方便后续比较深度。
-
渲染摄像机的深度纹理
-
从光源方向渲染出shadowmap
-
在屏幕空间做一次阴影收集计算(Shadows Collector,将前两步生成的图做深度比较)
- 这次计算会得到一张屏幕空间的shadowmap(Collect Shadows)
- 实际上就是对前两步的深度图做一个比较,得到一张新的深度图
-
在绘制物体时,用物体的屏幕坐标uv采样第三步中生成的屏幕空间shadowmap,然后与摄像机的深度纹理作比较
需要补充的一点:
- 生成shadowmap我们只需要深度信息
- 所以如果走BassPass和AdditionalPass来正常渲染会浪费性能
- 所以Unity使用了一个ShadowCaster的pass来生成阴影投射纹理
- shader中自定义这个pass,没有的话会从fallback找(只有开启了阴影投射的物体才会执行这个过程)
从FrameDebugger查看过程
阴影映射的优化
自阴影/自遮挡
问题与原因
由于阴影映射纹理的分辨率有限,离散的采样点以及数值上的偏差可能造成不正确的自阴影,也被成为Z-Fight或者阴影粉刺(Surface Acne)。
产生的原因:
从上图可以看到,表面上相当一块区域的阴影的阴影计算中用了阴影映射的同一个的纹素,那么在这一大块区域中,对应的阴影映射却是取的同一个值,那么这种误差就会导致一个自阴影。
补充理解(GAMES101):
- 在光源眼中,shadowmap每个像素对应的深度是如红色斜线对应的深度(这片区域记录的都是同一个深度)
- 而在相机眼中(蓝色虚线)进行计算阴影时,比如说计算的点为两条蓝色虚线的交点,实际上这个交点在shadowmap中由橙色线那块像素负责记录,导致记录的深度偏小一些,实际上这块屏幕应该都是光照能照射到,但在深度比较时出错了,导致了自遮挡现象
- 其实还是分辨率有限,导致记录在shadowmap中的深度覆盖过大,特别还有光源角度的影响,如果光源方向几乎和相机方向垂直时,单像素记录的深度值会覆盖很大一片局域,导致误差最大
解决方案
计算时增加偏移量,偏移单位为shadowmap中纹素大小
- 深度偏移(Depth Bias),增加深度偏移会使该像素向光源靠近
- 法线偏移(Normal Bias),沿着表面法线方向向外偏移
深度偏移造成的问题
由于分辨率和光源角度问题,shadowmap中的一个纹素,可能对应屏幕上好几个像素
假设是1:4的关系,记录的深度值为4个像素中间的深度,一种理想情况下,深度比较的结果会导致2个有阴影,2个没阴影
但我们期待的假设是3个都无阴影,一个有阴影
使用bias增加偏移后可能导致4个都通过比较,都无阴影
在表现上就会出现阴影和人分开的问题:Peter panning
Peter panning,Peter是西方漫画中的人物,他和影子是可以分开的
补充理解
自适应Shadow Bias算法:https://zhuanlan.zhihu.com/p/370951892
Unity中实现自阴影的优化
- Shadow Caster阶段基于顶点的Normal Bias
- 在Shadow Caster阶段让遮蔽物进行反向偏移(先偏移再记录)
- 优点:性能高
1 | //对于Normal Bias的使用,实际上就是对深度测试中的顶点做法线方向的反向偏移运算。 |
走样/锯齿
以信号重建的过程来审视阴影映射
- 初始采样:渲染阴影映射
- 重采样:从摄像机视角对采样信号(阴影映射)重采样
- 重建:使用滤波函数(例如PCF)
初始采样阶段
透视采样问题
- shadowmap在世界空间均匀分配(左图的三段纹素对应的世界空间的一个像素大小是一样的)
- 经过透视投影后,根据近大远小的原理,原来大小不一样的近平面和远平面,在屏幕内占的像素便一样了。(右图)
- 这时远平面对应在shadowmap中的纹素就比近平面大了
- 这种离相机近的部分走样的情况一般称为透视误差
解决方案
- 在生成shadowmap时就进行一次透视投影
- 因为我们在使用shadowmap时,相机是经过透视投影的,但生成shadowmap时并没有经过透视投影
- 尽量减少近平面和远平面之间的像素差距
- Unity中的级联阴影映射(Cascaded Shadow Map)
级联阴影映射:
概述:
- 透视走样最有效的解决方案
- 把视锥体分割为多个子视锥体,然后为每个子视锥体计算独立的相等大小的阴影映射
- 能在与相机不同的距离的位置采样不同的ShadowMap来解决透视走样问题
- 缺点:会占用更多的内存
首先需要对视锥体划分成若干层(每一层在主摄像机观察空间下的 z值范围 分别从哪到哪),有如下方法:
- 通过自定义比例来分割
- 通过对数比例分割:这是较理想的比例公式,能够减少 Shadow Perspective Alias 问题的出现
分割好各层视锥体后,我们需要选择对应的 Shadow Map 来恰好包围住视锥体(即各层的 Light Camera 的光锥体刚好包围住视锥体)
有了若干层 Shadow Map 后,渲染某个shading poing时该如何判断点在哪个层级:
- 直接通过视锥分割的z值范围来判断所在是哪个层级
- 通过各层 Light Camera 的 View 变换和 Projection 变换,得到点在该层 Shadow Map 的 UV 坐标,当 UV 坐标在 [0,1] 范围内时则说明在该层级内
前面我们在视锥分割已经确定了z值划分范围,直接简单根据shading point的z值来判断层级不是更好吗?这是因为每一层的 Shadow Map 其实多多少少包含了更远一层的部分阴影信息,但是它的精准度明显要比更远层的 Shadow Map 要好,因此通过uv坐标判断点所属层,就可以尽量命中较近层的 Shadow Map。
Unity中级联阴影的实现:
位于Project Settings/Quality
名词 | 解释 |
---|---|
Shadowmask 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 | 显示每个级联阴影的距离范围占比 |
重采样阶段
阴影映射是一张动态生成的纹理, 滤波是纹理采样误差的解决方案
什么是滤波?
- 图像处理中,通过滤波强调一些特征或者去除图像中一些不需要的部分;
- 滤波是一个邻域操作算子,利用给定像素周围的像素的值决定此像素的最终的输出值。
什么是阴影的滤波?
- 使用一部分阴影映射采样点来计算某个指定View采样点的最终阴影结果的方法
PCF滤波(Percent-Closer Filter)
流程:
- 从光方向生成阴影映射
- 从View视角渲染场景,用阴影映射进行深度测试
- 当该着色点通过深度测试时,取该像素周围指定大小(滤波核)的范围,然后取这个范围的平均值作为输出。最后它的输出值就是"可见采样点数/总采样点数",范围是[0,1],代表着该像素的可见性
采样数K(Filter的大小)
- 使用规则滤波,3*3或者5*5等
- 也可以采用泊松滤波(Poisson DIsk)的形式来分布一定数量的采样点
Unity中用3*3高斯核做的一个滤波采样:
滤波的卷积运算总会涉及到多次采样,非常影响性能
软阴影方案
- PCSS(Percentage Closer Soft Shadows)
- VSSM(Variance Soft Shadow Mapping)
- Moment Shadow Mapping
- Distance Field Soft Shadows
相关内容推荐看GAMES202阴影部分,以及下面相关参考链接
参考资料
- 课程PPT(有部分课上没讲的内容):https://docs.qq.com/slide/DUUJ6ZUhrRmxpRlBF
- 实时阴影技术(Real-time Shadows):https://www.cnblogs.com/KillerAery/p/15201310.html
- 影子传说—三种Shadowmap改进算法的原理与在Unity中的实现:https://zhuanlan.zhihu.com/p/382202359
- 基础渲染系列(七)—阴影:https://zhuanlan.zhihu.com/p/144271158
- 图形学基础 - 阴影 - ShadowMap及其延伸:https://zhuanlan.zhihu.com/p/384446688
- Unity Shader - Custom DirectionalLight ShadowMap 自定义方向光的ShadowMap:https://blog.csdn.net/linjf520/article/details/105401157
- 米哈游技术总监:开局一个人,我把《原神》搬上了PS4:https://zhuanlan.zhihu.com/p/300248303
- 大佬笔记:
- https://note.youdao.com/ynoteshare/index.html?id=1862682cd7251376c171628c55aabee5&type=note&_time=1632905337733
- https://zhuanlan.zhihu.com/p/487457443
- https://www.yuque.com/hejincan/shader/yy3wgs
- https://www.yuque.com/6527chen/ldyt32/frhzdy#9d592adc
- https://blog.csdn.net/ddgf01/article/details/120443064
作业
总结实时阴影的优化方案
- 自阴影:使用深度偏移和法线偏移解决
- 透视投影:使用级联阴影映射解决
- 使用PCF解决重采样的问题,并产生软阴影的效果
尝试自己实现一套阴影方案
先放上GAMES202课上作业:
分别是基本shadowmap、PCF、PCSS
pcss部分噪点有点多,未做去噪处理
todo:在URP中实现