前言

前面几篇关于光栅化的文章中介绍了如何计算物体表面的光照,但并未提及阴影的计算,阴影需要单独进行处理,目前最常用的阴影计算技术之一就是Shadow Mapping技术,也就是俗称的阴影映射技术,今天就仔细梳理一下具体的原理。

 

Shadow Mapping工作原理

先来思考一下,为什么会存在阴影?

因为光源照射不到,更具体点,摄像机能看到的地方,光源“看”不见。

而这正是启发Shadow Mapping这种做法的动机,接下来我们便来看看详细过程是怎么样的。

第一个阶段,把光源当做一个摄像机去渲染一遍场景,然后得到一张光源处可以看到的图,这张图不需要着色,只需要把深度记录下来即可,此时得到的这个深度图即为Shadow Map。

第二个阶段,从摄像机视角渲染场景,将所有摄像机视角可见的点,利用光源视角下的那一套投影矩阵的逆矩阵,将坐标从世界坐标转换到光源视角的空间坐标,得到光源视角下的深度值,并找到在深度图上对应的深度值。

如果该点投影回光源视角的实际深度值不大于在深度图上对应的深度值(注意这里的深度值是浮点数,因为精度问题难以比较两个值相等),则说明此点可被光源照射,因此不在阴影中,如上图黄色线的这种情况。

如果该点投影回光源视角的实际深度值大于在深度图上对应的深度值,则说明该点前方有物体遮挡,因此在阴影中,如上图红色线这种情况。

如此便能确定每个可见像素点是否在阴影之中了,效果如下图:

对应可视化的Shadow Map如下,距离光源越近代表深度越小,所以颜色越黑,反之亦然:

 

Shadow Map的精度问题

看起来Shadow Mapping很好地计算出了阴影,但它带来了其它问题,首先就是精度问题。

可以看到地板上出现了交替的黑白线,这种现象叫做阴影失真(Shadow Acne,Acne是粉刺的意思),下图解释了成因:

Shadow Map中每个像素记录了一个深度值,对应了平面上的一个区域,由于Shadow Map的分辨率肯定是有限的,其记录的深度值必然是离散化的,而平面本身是连续的,这就造成平面上一定大小的区域内的深度值有一定差别,但对应在Shadow map上是一个相同的深度值。

而在查询Shadow Mapping时不采用双线性插值,只寻找最近的点,因为倘若插值发生在物体边缘时,与相邻点的深度差距很大,会导致插值结果有很大的误差。

图中紫色的锯齿状折线,演示了shadow map上深度值的变化。而shadow map算法第2个pass中,计算点在光源视角的深度值时,计算得到的是一个浮点数的比较精确的深度值。

例如图中红色粗线标出的区域,这个区域中每个点到光源的深度值都是不同的,但是由于这个区域对应的Shadow Map上的像素记录的深度比这个区域计算出来的深度要小,所以这个区域的片段都被遮挡了,产生了阴影。

而同理图中红色粗线左侧的小块区域,对应的Shadow Map上的像素记录的深度比这个区域计算出来的深度要大,所以这个区域的片段没有被遮挡,不会产生阴影。

这样每个离散化深度值对应的区域,有一半有阴影,另一半没有阴影,因此最后结果就是相隔的条纹。

由于其实是被自己在Shadow Map中记录的深度值所遮挡,这种现象也被叫做自遮挡(self occlusion)。

即使多耗费性能提高深度图的分辨率,也只能一定程度优化并不能完全解决这种自遮挡问题,并且对于大规模的场景来说,阴影图的精度要求会更高,而引擎对于纹理图片一般是有最大分辨率限制的,这个时候就得用其他的解决方案了。

 

Shadow Bias(阴影偏移)

由自遮挡问题产生的原因就能看出,只需要将物体表面的深度稍微往光的方向偏移就能让自遮挡几乎不发生,这样片段就不会被错误地认为在表面之下了,这种技术就叫做Shadow Bias(阴影偏移)。

为了保证阴影偏移值对于近处和远处有相近的效果,阴影偏移的单位是深度图的像素,这样在改变深度图分辨率时,阴影偏移的值也会自动跟随缩放。

并且为了让场景中不同角度的平面的偏移效果一致,还可以使用平面法线与光源方向做点乘再对阴影偏移值进行缩放,这样像地板这样的表面几乎与光源垂直,得到的偏移就很小,而比如墙面这种表面得到的偏移就更大。

需要说明的是阴影偏移是一种改善方案,并没有让自遮挡问题本质上消失,只是避免这一现象表现出来。Shadow Bias也没有绝对有效的值,需要根据场景实际情况多次测试,选择合适的值。

过大的阴影偏移值反而会带来另一种问题,因为对物体的实际深度应用了平移,当偏移足够大时,肉眼可以看出阴影相对实际物体位置的偏移,这种现象被称为Peter Panning,因为物体看起来轻轻悬浮在表面之上(Peter Pan指的是童话故事里会飞的小男孩彼得潘,而panning有平移、悬浮之意)。

原因也很简单,阴影靠近物体的位置本来应该有阴影的地方因为深度偏移导致深度值变小低于深度图上记对应的深度值,从而被判定为没有遮挡,因此不再计算阴影。

 

Front Face Culling(正面剔除)

除了阴影偏移外,还可以采用正面剔除方式解决自遮挡问题,当渲染深度图时候剔除遮挡物体的正面,实际上是对深度图做了偏移,让自遮挡不再出现。

因为自遮挡只有在一个表面能同时被我们眼睛和光线看到的情况下才可能发生,因此直接将光线看到的表面改成不渲染的其他面,就可以从本质上预防自遮挡问题。

但剔除物体的正面需要保证物体有背面,如果遮挡物体只是一个单层平面,就不会有效了。并且贴近阴影的物体仍然会出现漏光等不正确的效果,因此正面剔除同样属于一种补救方案,使用时也要仔细斟酌。

 

Shadow Map的走样问题

透视投影导致的走样

在第一步生成光源视角的Shadow Map时,如果光源与摄像机的夹角比较大时,可能会产生透视走样,距离光源越远的阴影锯齿越明显。

原因和前文讲的footprint现象类似,对于光源这个伪摄像机来说,距离越远,屏幕像素的覆盖区域越大,反过来就是越远的区域像素精度越低。

要解决透视走样问题,最有效的解决方案是Cascaded Shadow Mapping(级联阴影映射),原理是把视锥体分割成多个子视锥体,为每个视锥体计算独立的相等大小的阴影映射。

例如上图中将视锥体分成两部分,两部分子视锥体的分辨率相同,但覆盖范围不一样,距离摄像机越近的子视锥体覆盖范围越小,像素精度越高。

 

采样Shadow Map导致的走样

而在第二步从摄像机视角对Shadow Map进行采样对比时,由于深度图是有分辨率的图片,采样分辨率不够时得到的阴影也会产生锯齿。

这是因为计算阴影时要么在阴影中,要么不在阴影中,只能出现非黑即白的效果,因此边缘必定出现锯齿,只是锯齿程度大小的区别。

为了消除这种走样问题,于是出现了PCF滤波(percentage closer filtering)技术,通过滤波器的方式,在第二步着色点通过深度测试时,取该像素周围指定范围内的区域进行采样(例如周围3*3或者7*7个采样点等),将得到的结果进行加权平均。

这样的话,最终得到的结果是一个[0, 1]范围的值,于是就形成了阴影边缘的半透明过渡,能够减少锯齿问题。

Shadow Mapping技术生成的阴影属于硬阴影,理论上只适用于没有体积的点光源。

软硬阴影区别示意如下,上方棱角分明为硬阴影,下方为软阴影:

产生这种问题的原因是因为光源具有体积,导致有的地方完全看不到光源(本影, Umbra), 有的地方能看到一部分光源(半影,Penumbra)。所以阴影的边缘会有过渡的情况,从而产生软阴影现象,就像上图中太阳与地球的角度导致的日食现象一样。

而在用PCF滤波处理阴影锯齿的时候,阴影模糊的效果和软阴影的效果很相似,因此可以通过用较大的滤波让阴影更加模糊,从而模拟有体积的光源产生的软阴影。

由于脱胎于PCF,这种技术就被称为PCSS(percentage closer soft shadows)。

 

本文参考自本文参考自闫令琪老师的《GAMES101-现代计算机图形学入门》和孙晓磊的计算机图形学系列笔记以及晓痕的《百人计划作业》等,感谢。


人是卑鄙的东西,
什么都会习惯的。

《罪与罚》
——陀思妥耶夫斯基