Loading... # Lec 3~4:实时阴影 Real-time Shadow ## 1. Shadow Mapping Shodow Mapping 是一个 **2-pass 算法**: - 先一遍光源pass,生成Shadow Map,即从光源的位置渲染一遍场景,将得到的深度信息写入到贴图中 - 再一遍相机pass,根据Shadow Map,比较其到光源的深度,判断其是否能被光源照到 它是一个图像空间的算法 - **优点**:是无需知道场景的几何信息 - **缺点**:是会导致**自我遮挡**现象以及**走样**的问题 ### 1.1 Shadow Mapping的问题 ① **自我遮挡**  上图左图中地面上有很多异常的阴影纹路。这是由于精度导致的**自我遮挡**现象(或又称**阴影抖动**)。 在上图右图中,从光源处开始对地板进行采样。但是由于**采样并非连续**的问题,某**一个像素内的整块区域的深度对于光源来说都是一个常值**。这意味着,地板上的每一个像素内的区域对于光源来说都不是平整的,而是一小块一小块像素倾斜着的(如图中红线一般) 或者下图可以看的更清楚一点,四道虚线中所夹成的三块区域是三个相邻的像素,每一个像素在下面的平面上对应的区域的深度是不连续的,而是会**取到其中一点的距离作为整块区域的深度**。红线表示一块区域实际上对于光源来说的深度。  接着,从相机处沿着蓝线看向平面一点,这一点显然是可以被光看见的。但是在之前生成的Shadow Map上,这一点对于光源来说的深度,是小于其实际到光源的距离的(橙色连线被红线挡住)。所以会判定这一点看不到光源,即为阴影处。这种被自己挡住的现象,叫做自我遮挡。  一个显而易见的解决方法是:添加偏移容忍。两个深度差距在一定范围内都会判定为能看到光照。不过这样会导致其他新的问题: 比如下图,在添加偏差容忍后,地面的阴影纹路消失了,但是发现人影的脚部阴影消失了。这就是因为添加的偏差容忍过大,导致将阴影误判为照亮的地方了。  另一个解决方法:双深度Shadow Mapping. Shadow Map除了记录离光源最近的距离,还要额外记录第二近的距离,取两个距离的中间值作为真正的深度,做后续的阴影比较。  但是这方法几乎不会用,缺点是:要求物体是密封的,这样才能找到第二深度。同样,这样的开销是很大的(即使复杂度没有变,只是多了几个if语句,但是还是会带来不小的开销,**实时渲染不相信复杂度**) ② **走样** 因为Shadow Map是不连续的,所以很容易想到会有走样的问题。  ③ **只适用于点光源,无法生成“软阴影”** (GAMES101已提及,下文将会讲解) ### 1.2 Shadow Map背后的数学  如图的约等式,将会贯穿整个实时渲染。 这个约等式在这种情况下会比较准确:积分域够小、$g(x)$ 足够光滑 回忆渲染方程:我们添加了一个 $V(p, \omega_i)$ 项表示**对光源的可见性** *( 实时渲染的Shadow Map不考虑间接光照 )*  将其约等于上图公式二的形式。坐标红框的部分其实就是Shadow Map(对于普通的Shadow Mapping,0为不可见,1为可见;但是如果考虑部分遮挡导致的**软阴影**,那么就会有[0,1]范围内的数值,表示“可视度”,后续会有提及)。而右边的部分其实就是原本的渲染方程的着色计算。 所以如果光源是点光源 或者 方向光源(积分域小)、漫反射的BSDF 或者 常数辐照度的面积光(光滑积分)的情况下这个约等式会比较准确 ## 2. PCF & PCSS 如果光源是一个面光源,就会存在“**软阴影**”的现象。普通的Shadow Mapping是无法实现的。  ### 2.1 百分比渐进滤波 Percentage Closer Filtering (PCF) 这个方法起初只是为了实现阴影边缘的反走样,并非为了做出软阴影。但是后来发现可以用作生成软阴影(PCSS,下文会讲)。 **PCF并不是在已经有锯齿的阴影图结果上进行滤波,也不是对Shadow Map中的深度进行滤波**。它会对“对光源的可视性” 这个结果(0表示不能,1表示能)进行滤波。 举一个例子,在Shadow Mapping后第二次pass时,对着色点在Shadow Map中对应的纹素(texel) 周围的3*3的模板内的纹素的深度进行比较(不一定是这么小,也不一定是全部遍历),如果深度比它大,那么就是可见1,否则即为不可见0。 接着对得到的0/1结果进行滤波,得到(加权)平均值。得到的结果设为当前着色点的 **可视度**,也即该点的明亮度。这样便可以得到比较柔和的阴影,减缓阴影锯齿。   PCF过程可以这么理解: 下图左图为软阴影的形成原理,对于面光源存在部分遮挡。而右图即为PCF反转处理,通过获取着色点周围的一系列点的**由点光源造成的深度值**,与P点深度比较再计算一个平均值(**不是平均深度,也不是平均结果**),近似得到着色点软阴影程度。 *注:面光源是不能生成 Shadow Map 的,所以一般都是将面光源视作一个点*  PCF并非是基于物理的,即并非对于光源进行采样,结果依赖于接收表面,且与遮挡物距离也不会影响最终结果,所以只是一个对于半影的近似,但也能在许多情况下提供一个可信结果。 如果模板过大,而且要模板内每一个像素都遍历一遍,那时间开销会非常的大。所以也有在模板内**随机稀疏采样**的方法,只访问其中随机的若干个像素。只不过这样的做法会导致结果存在噪声。 而且如何选择模板也是有所影响:下图p1是一个统一的3*3的模板,可以看出有很明显的 “图块”。p2则是采用了p4的泊松随机采样模板。p3是在p2的基础上加了一点随机旋转模板,不过可以看出明显的噪声。  ### 2.2 百分比渐进软阴影 Percentage Closer Soft Shadows(PCSS) 生活中一种很常见的现象:物体与表面相接触,光源斜着照过来,呈现的阴影在接触点是最清晰明显的(硬阴影);而越向外延伸,阴影就越模糊(软阴影)。  这其实是由于遮挡物与阴影的距离关系导致的。之所以有软阴影,是因为某些区域处于光源的**半影区**(Penumbra),而这个半影区会随着距离发生改变(下文详细解释)。如果PCF采用的是统一大小的模板,是不可能呈现出这种动态的软阴影大小的。 所以PCSS算法会**设法估算出当前位置的半影区大小**,这个大小决定了PCF算法的模板大小,最终呈现出一个视觉上的软阴影效果。**通过控制PCF的模板大小,就可以改变阴影的模糊半径,进而模拟出软阴影的效果**,这就是PCSS算法的思路。 PCSS使用 **相似三角形** 来近似计算出半影区大小。   其中$w_{Penumbra}$是半影区的大小,一定程度上反映了阴影软硬的程度,我们将根据其来决定PCF的模板大小。 PCSS的完整过程即为: 1. 计算光源与平均遮挡物的距离 $d_{Blocker}$ 2. 利用平均遮挡物距离 $d_{Blocker}$ 计算PCF用到的模板大小、采样范围 $w_{Penumbra}$ 3. 使用上一步得到的采样范围计算PCF 还有一个问题,如何计算出第一步的平均遮挡物的距离呢?这可能同样需要一个“模板”来在Shadow Map上取平均,通常是取5*5. 当然也同样可以使用一个从着色点出发向面光源的视锥,这个视锥会在该光源生成的shadow map(通常位于光源的近平面上)中圈出一片范围,如下图中红色部分,则这部分范围内的深度值将会用来采样并计算平均遮挡物距离。 只有当采样点深度小于着色点(产生遮挡)时,才计入遮挡距离,对于不存在遮挡的采样点,则计入遮挡距离的值就是0,最后将总的遮挡距离除以采样点个数,得到结果。  下图P1是小模板的PCF,P2是大模板的PCF,P3是PCSS  下图是PCSS在游戏《消逝的光芒》中的应用:  ## 3. Variance Soft Shadow Maps (VSSM) 正如前文所说,PCSS中在第一步、第三步有多次对区域内进行比较并计算平均(滤波/卷积)的步骤,若不采样则会导致巨大的计算量,如果采样,则必然会有误差或噪声,此时需要执行图像空间降噪来处理。 为了解决PCSS的这个问题,提出了VSSM(VSM)方法,针对性解决PCSS第一步、第三步的速度慢的问题。 ① **第三步优化** 在第三步的PCF中,我们需要将着色点的深度和周边的深度进行**比较**,计算平均;但本质上是要**找深度比它小**的;也就相当于在Shadow Map范围内**所有深度进行排序**,找到当前着色点的深度的排名。可以使用**正态分布来近似整个的深度的分布**,从而直接获得着色点深度的大概位置,就能知道被遮挡的比例。  正态分布两大属性:**平均值、方差**。我们只要知道这两个值,就可以快速描述出一个正态分布。 - 平均值: - 硬件上的MipMap,不过并不是准确的 - Summed Area Tables (SAT),也就是 $ \rm{sum}(x1:x2,y1:y2) = f(x_2, y_2) - f(x_2, y_1) - f(x_1,y_2) + f(x_1,y_1)$ 这个公式,需要额外空间维护一个前缀和数组 $\rm f$  - 方差: - 根据公式:$\rm{Var}(X)= E(X^2) - E^2(X) $ - 我们只需要求出深度的平方的均值,而这在生成深度的Shadow Map时【顺手】生成一个 深度平方值的Map 即可 确定了正态分布后,我们还需要得知着色点的排名。这相当于在正态分布上求积分。  而我们没必要求出那么精确的值,可以利用 **切比雪夫不等式** 求出近似:  于是就能知道深度为 $t$ 的着色点的近似排名: $$ 1-P(x\gt t) = \frac{(t-\mu)^2}{\sigma^2 + (t-\mu)^2} $$ 注意这个不等式要求 $t$ 值大于 平均值 $\mu$ ,所以如果所求的 $t$ 值低于平均值的话,我们可以利用对称性求 $P(x > (2\mu-t))$ 即可 ② **第一步优化** 第一步需要计算**遮挡物的平均深度**,本质上是计算Shadow Map上**范围内深度小于着色点深度的纹素的平均值**。 我们定义 $\rm z_{occ}、z_{unocc}、z_{Avg}$ 分别为所有遮挡深度的平均值、所有非遮挡深度的平均值、所有深度平均值。那么会有这么一个关系:  - $\rm z_{occ}$ 为我们所求值,而 $\rm z_{Avg}$ 可以用之前所提范围求和方法求出 - N1 / N、N2 / N都是各自所占百分比。所以很容易想到之前所提切比雪夫不等式: - $N_1 / N = P(x>t)$ - $N_2/N = 1- P(x>t)$ - $\rm z_{unocc}$ 是未知的,但是我们可以使用一个大胆的假设:$\rm z_{unocc}=t$,因为阴影的receiver在采样范围内一般可以视作是一个平面 这样就可以近似求出所求值了  VSM在为PCSS算法提高效率的过程中使用了**很多假设的分布条件**,而当这些假设分布条件与真实情况误差较大的时候(比如深度并非按照正态分布、阴影receiver并非平面),就可能会造成**Light Leak 漏光**等问题。  比如下图中,左图是场景中的一个茶壶,光源在上方。这时在茶壶很高的上方添加一个三角形面(不在图里显示),挡住一部分光源。可以看见右图中有明显的漏光现象。这是因为,在Shadow Map中的三角形面的边缘处,只有一部分“遮挡”住了光源:一边是很小的三角形面的深度,另一边就是很大的茶壶的深度。这样的话,在PCF的采样模板内的深度分布显然是“两极分化”的,并不符合正态分布的状态,会**让切比雪夫不等式失效**,所以VSM算法会将 应该被完全阴影的地方 当做 被照亮的地方,导致漏光现象。  下面要讲的MSM是对VSM的一个改进,主要提高了范围内数据分布的精确性。 ## 4. Moment Shadow Mapping(MSM) 为了让VSM中**对分布的描述更加精确**,提出了使用**高阶矩**$m$来描述分布的方法。如下图中,蓝色线段是PCF的模板范围内的深度概率分布CDF,VSM只使用了两阶的矩,则只能表现出一个台阶($m/2$),而**MSM会使用四阶矩(两个台阶)来近似这个CDF**,显然会更加准确。类似将CDF多项式展开,并保留前m项。存储前四阶矩只需要四通道贴图(分别存4个阶)即可,但是用四阶矩来恢复这个CDF涉及到很复杂的数学推导问题。  优点是效果比较不错 缺点是开销巨大,已经几乎不怎么用  ## 5. 级联阴影贴图 Cascade Shadow Mapping (CSM) 基础的Shadow Mapping方法对于**大型场景**渲染显得力不从心,很容易出现**阴影抖动(自我遮挡)**和**边缘走样**现象。**Cascaded Shadow Maps(CSM)**方法根据物体到观察者的距离提供**不同分辨率的深度纹理**来解决上述问题。它将**相机的视锥体分割成若干部分**,然后为分割的每一部分生成**独立的Shadow Map**。 CSM通常用来在**大型场景**模拟太阳投射的**阴影**,在一张Shadow Map中**包含所有物体要求Shadow Map具有非常高的分辨率**。而使用多张**Shadow Map**就可以解决这个问题,对于**近处**的场景使用**较高分辨率的Shadow Map**,对于**远处**的场景使用**粗糙的Shadow Map**,在两张Shadow Map过渡的地方选择其中一张使用。 因为**远处的物体只占画面的很少一部分像素,而近处的物体占据了画面的很大一部分**,进行这样的处理显然非常合理。 CSM方法的基本思路如下: - 使用每个光源的**光椎体**渲染场景的**深度值**。 - 从相机位置渲染场景。根据像素对于相机的**深度值**,选择**合适**的Shadow Map查询该像素的深度。 - 将其和像素在 **对应Shadow Map中的深度值** 进行比较,根据比较结果决定像素的最终颜色。 **具体内容见**:[知乎:Cascaded Shadow Maps(CSM)实时阴影的原理与实现](https://zhuanlan.zhihu.com/p/53689987) 比如下图,黑色的是相机的视椎体,被划成了两部分。其面前有3棵树,最近的1棵被划分在近的部分,而远处的2棵被划分在远的部分。所以在渲染近树的时候,用的是近处的Shadow Map(粉色);而远处的2棵树用远处的Shadow Map(蓝色)  下图体现了场景中对应的不同层级ShadowMap的区域  ## 6. 距离场软阴影 Distance Field Soft Shadow 距离场和SDF在 [我的GAMES101笔记4的1.3节](https://www.irimsky.top/archives/260/) 中提及过,不重复解释 距离场的作用: ① **光线追踪求交** 距离场的定义是:离这里**最近的**一个表面的距离。在进行光线追踪,光线与SDF(Signed Distance Field,带符号距离场)求交时,我们可以利用其来不断“推进”光线。只需将其沿着光线方向移动“当前点在距离场中的值”的距离,因为这段范围内不会与任何物体相交,可以说是一个“安全距离”。  ② **实现软阴影** 从某处看向光源,在过程中可以根据SDF找到一个“安全角度”,意义为:在这里角度范围内不会被任何物体遮挡。在点到光源上推进(每次移动“安全距离”)的过程中取最小值。这可以作为一个估算阴影亮度的方法。  而计算这个我们不用三角函数去计算安全角度,而是使用下面这个简化的公式直接进行估算出安全角度所占的范围:  $p-o$ 的意义是p点到原点的距离,$SDF(p)$是p点在距离场的值 而 $k$ 的意义在于 “限制半影区大小”,k值越大,求得的比例越大,“全照区”越大,半影区也就越小,阴影也就越硬。  **优点**:相对较快,效果不错 **缺点**:需要预先计算距离场,存储开销大;此外还有距离场本身的缺点 最后修改:2021 年 11 月 30 日 05 : 32 PM © 允许规范转载