《Real-Time Rendering》第七章 阴影

开篇

  阴影对于创建真实的图像和为用户提供关于物体摆放的视觉线索来说是非常重要的。这章将会专注于阴影计算的基本准则,并描述最重要且受欢迎的用于计算阴影的实时算法。我们也会简要地讨论那些不那么受欢迎的但能体现重要准则的方法。这一章不会花费时间涵盖所有的选项和方法,因为已经有两本全面的有深度的研究这个领域的书籍。我们会专注于调查报告文章和演讲报告,并偏向于那些经过检验的技术。
  这章中会使用的一些术语如下图所示

img

图中的遮挡物Occluder)是向受影物体Receiver)投射阴影的物体。点式光没有体积,它会照亮表面,那些被遮挡的表面上将会有完全被阴影笼罩的区域,这些区域有时被称为硬阴影Hard Shadow)。如果区域光源或体积光源被使用了,那么会有软阴影Soft Shadow)。在这种情况下,每个阴影会有一个完全被阴影笼罩的区域,这个区域被称为本影Umbra)。还有另外一个被阴影部分笼罩的区域,这个区域被称为半影Penumbra)。软阴影以模糊的阴影边缘为特点。有一点需要注意,通过使用低通滤波器来模糊硬阴影是无法得到正确的软阴影的。一个例子如下图所示

img

可以看到,正确的软阴影会在投射阴影的几何体靠近受影物体的地方变得锐利。软阴影的本影区域是不等价于点式光生成的硬阴影的。软阴影的本影区域会随着光源的变大而减少,甚至是消失也是有可能的,比如当光源足够大且受影物体离遮蔽物很远时。软阴影是通常被青睐的,因为半影边缘能让观察者知道“我确实看到了阴影”。硬边缘阴影通常看起来不会那么真实,而且有时候会被误解为真实的几何特性,就比如误解为表面上的折痕。然而,硬阴影相比于软阴影能被更快速地渲染。
  比有半影更重要的是要有阴影。如果没有阴影这种视觉线索,场景通常会显得不真而且更难进行感知。如Wanger所展示的那样,不准确的阴影比没有阴影要好些,因为眼睛不会特别在意阴影的形状。例如,一个被映射到地面上的模糊的黑色圆形会锚定地面上的一个角色。
  在接下来的部分,我们不止会了解一些简单建模的阴影,而且会展示一些能实时从场景中的遮挡物自动地计算阴影的方法。第一个部分介绍了向平面投射阴影的特殊情况,第二个部分会涵盖更加通用的阴影算法,比如向任意表面投射阴影。硬阴影和软阴影都会被涵盖到。一些能应用于不同的阴影算法的优化技术也会被介绍。

平面阴影(Planar Shadow)

  一种简单的阴影会出现在物体投射阴影到平面上时。用于平面阴影的一些算法会在这个部分被介绍,这些算法在软度和真实感上会有区别。

投影阴影(Projection Shadows)

  在这个方法中,三维物体会被额外渲染一次来制造阴影。一个矩阵可以被计算,通过这个矩阵,我们可以把物体的顶点投影到一个平面上。考虑下图这种情况

img

在上图中,光源处于\(\mathbf{l}\),被投影的顶点位于\(\mathbf{v}\),投影后的顶点位于\(\mathbf{p}\)。我们首先考虑投影阴影到\(y=0\)平面这个特殊情况,接着将其推广到任意平面。
  对于\(x\)坐标的投影来说。根据三角形的相似关系,我们可以得到

\[\frac{p_x-l_x}{v_x-l_x} = \frac{l_y}{l_y-v_y} \Longleftrightarrow p_x = \frac{l_yv_x-l_xv_y}{l_y-v_y} \]

\(z\)坐标会通过相同的方式计算出来,公式为\(p_z=(l_yv_z-l_zv_y)/(l_y-v_y)\)。我们接着可以把这两个公式转化到投影矩阵\(\mathbf{M}\)

\[\mathbf{M} = \begin{pmatrix} l_y & -l_x & 0 & 0\\ 0 & 0 & 0 & 0\\0 & -l_z & l_y & 0\\ 0 & -1 & 0 & l_y \end{pmatrix} \]

在一般情况下,投影到的平面不止有\(y = 0\),而是\(\pi:\mathbf{n} \cdot \mathbf{x} + d = 0\)。这对应着上图中的右半部分。光线从\(\mathbf{l}\)出发,经过\(\mathbf{v}\)接着和平面\(\pi\)相交,经过一系列推导可以得出

\[\mathbf{p} = \mathbf{l} - \frac{d+\mathbf{n} \cdot \mathbf{l}}{\mathbf{n} \cdot (\mathbf{v-l})} (\mathbf{v-l}) \]

这个式子其实很好理解,虽然\(\mathbf{p}\)是未知的,但是我们能利用相似关系先把\(\mathbf{v-l}\)缩放到\(\mathbf{l-p}\),接着让\(\mathbf{l}\)减去\(\mathbf{l-p}\)就能得到\(\mathbf{p}\)。它也可以被转化为如下的投影矩阵

\[\mathbf{M} = \begin{pmatrix} \mathbf{n} \cdot \mathbf{l} + d - l_xn_x & -l_xn_y & -l_xn_z & -l_xd\\-l_yn_x & \mathbf{n} \cdot \mathbf{l} + d - l_yn_y & -l_yn_z & -l_yd\\-l_zn_x & -l_zn_y & \mathbf{n} \cdot \mathbf{l} + d - l_zn_z & -l_zd\\-n_x & -n_y & -n_z & \mathbf{n} \cdot \mathbf{l} \end{pmatrix} \]

正如我们期望的那样,当\(\mathbf{n} = (0,1,0)\)时这个矩阵会变为用于\(y=0\)平面的投影矩阵。
  为了渲染阴影,你只需要为物体应用这个投影矩阵,将其投影到平面\(\pi\)上,在着色的时候赋予其无光照带来的暗色就行。在实践中,应该避免投影后的三角形在受影物体表面的下方被渲染。一个用来解决的方法是添加小偏移到要投影到的平面上,这样能让阴影三角形总是在受影物体的表面前。
  一个更加安全的方法是先绘制地平面,接着关闭z缓冲并绘制投影后的三角形,然后正常地渲染剩余的几何形状。这样做能让投影后的三角形永远在地平面上方,因为没有进行深度比较。
  如果地平面是有限的,比如是个矩形。那么投影后的阴影有可能在几何形状外,那么看起来会很不正常。为了解决这个问题,我们可以使用一个模板缓冲。首先,绘制受影物体到屏幕和模板缓冲上。接着关闭z缓冲,然后利用模板缓冲,只在受影物体被绘制的地方绘制投影后的三角形,接着以正常方式渲染场景的剩余部分。
  另外一个阴影算法会渲染三角形到纹理上,这个纹理会被映射到平面上。这个纹理是光照贴图Light Map)中的一种,它会调节在它之下的表面的强度。正如我们将会看到的那样,把阴影投影到纹理上的这一想法,可以在曲面上创造有着半影的阴影。这个技术的一个劣势是,当纹理被放大时,单独一个纹素会覆盖多个像素,这会让阴影显得不那么真。
  如果阴影的状况在帧与帧之间不改变,例如光源和遮挡物不相对移动时,那么这个纹理可以被复用。绝大多数阴影技术都可以受益于重复利用帧到帧之间不变的中间计算结果。
  所有的遮挡物必须在光源和地平面之间。如果光源在物体的最高点的下方,那么一个反向阴影Antishadow)会被生成,因为每个顶点会沿着过光源的线被投影到受影物体上。正确的阴影和反向阴影如下图所示。

img

此外,另一个问题会在物体位于受影物体背后时发生,因为理论上来说受影物体背后的物体是不能向受影物体投射阴影的。因此需要进行剔除并裁剪阴影三角形来避免这类伪影。接下来要介绍的一个简单方法会利用现存的GPU管线来执行有着裁剪的投影。

软阴影(Soft Shadows)

  投影阴影可以通过使用不同的技术被软化。在这里我们描述一个来自Heckbert和Herf的生成软阴影的算法。这个技术的目标是在地平面上生成一个纹理用来显示软阴影。我们后续会描述不那么精确但更快的方法。
  当光源有体积时软阴影会出现。一个用来近似区域光源的影响的方法是使用一些在区域光源表面上放置的点式光进行采样。对于每个点式光来说,一个图像会被渲染并被累积到一个缓冲中。这些图像的一张平均图像就会有着软阴影。从理论上来说,任意生成硬阴影的算法可以使用这种累计技术来生成半影。但是在实践中,这种方法一般都达不到交互性帧率,因为这个方法有很大时间开销。
  Heckbert和Herf使用了一个基于截头锥体的方法来生成阴影。它背后的想法在于把光源当作观察者,并且把地平面当作截头锥体的远裁剪平面。截头锥体会有足够的宽度来容纳遮挡物。
  通过生成一系列地平面纹理,可以得到一个软阴影纹理。区域光源会在它的表面上被采样多次,采样的位置每次会变动用来着色表示地平面的图像,遮挡物接着会被投影到表示地平面的图像上。这些图像会被加起来并被平均来生成一个有着阴影的地平面纹理。下图为一张渲染示例图。

img

  区域光源采样算法会有一个问题,生成的阴影虽然表现得软化了,但有时候看起来会像是由点式光生成的阴影重叠而成。此外,对于\(n\)个阴影通道来说,只有\(n+1\)个不同的着色结果会生成。大量的通道会有精确的结果,但是会有非常高的额外开销。这个方法可以被用来获得质量非常高得软阴影用于测试其它的阴影生成算法。
  一个更高效的方法是使用卷积,比如滤波。从一个点生成的硬阴影可以被模糊,这在某些情况下是足够的,模糊后生成的半透明纹理接着可以与世界中的内容进行合成。下图是个相关的示例图。

img

它的缺点在于对阴影的不同位置进行了相同程度的模糊,物体靠近地面处的阴影会看起来不那么真。
  也有一些其它的方法能更好地生成软阴影。比如,Haines从投射的硬阴影开始,使用从中心处的黑色到边缘处的白色的渐变色来渲染轮廓的边缘,从而生成了更好的半影。下方为一张相关的示例图。

img

要注意的是,这些半影并不是物理正确的,因为半影应该也存在于轮廓边缘内部。Iwanicki从球谐函数中汲取了想法,并使用椭球体来近似被遮挡的角色来获得软阴影。这些方法都有着不同的近似程度和缺点,但是比平均一系列投射阴影后的图像来说要高效得多。

曲面上的阴影(Shadows on Curved Surfaces)

  一个简单的扩展平面阴影到曲面上的方法是把生成的阴影图像作为投影纹理。以光源的视角来思考,光源看到的表面都会被照亮,而那些没看到的表面会处于阴影中。假如遮挡物在光源的视角下被观察且被渲染成了黑色,渲染结果被保存到了一张其它区域都是白色的纹理上。这个纹理接着可以被投影到受影的表面上。表面在被渲染时接着可以插值顶点存储的纹理坐标来访问纹理上的对应位置。这些顶点存储的纹理坐标可以显式地由应用程序计算。这和之前所述的地平面阴影纹理不同,在那个方法中物体会被投影到一个特定的平面。而在这里,图像是以光源的位置作为视点渲染得到的。
  在渲染时,投影后的阴影纹理会修改受影表面。它也能与其它的阴影算法结合,它有时主要被用来改善观察者对物体位置的感知。例如,在一个平台跳跃游戏中,主角的下方可能一直会有投影阴影,甚至是角色完全处于阴影中时。更加复杂的算法可以给予更好效果。例如,Eisemann和D´ecoret假设了一个头顶上的矩形光源,并创建物体水平切片的一叠阴影图像,这些阴影图像接着可以转变为mipmap或类似的东西。每个切片的对应位置的访问会通过访问切片的mipmap进行,且访问的mipmap和对应位置与受影物体之间的距离成比,这意味着更远的切片将投射更软的阴影。
  纹理投影方法有些严重的缺点。首先,应用程序必须识别哪些物体是遮挡物,哪些物体是受影物体。受影物体必须被程序维护,来让其相比于遮挡物更加远离光源,要不然阴影会“向后投射”。此外,遮挡物不能遮挡它们自己。接下来的两个部分会介绍一些能正确生成阴影,并且不需要上述这种措施的方法。
  要注意的是,多种光照模式可以通过预构建投影纹理做到。一个聚光灯仅是个方形的投影纹理,在纹理中有个定义光源的圆。百叶窗效果可以通过使用由水平线条构成的投影纹理获得。这种纹理类型被称为光照衰减遮罩Light Attenuation Mask)、Cookie纹理Cookie Texture)、Gobo贴图Gobo Map)。一个预构建的光照模式可以和在运行时被创建的投影纹理结合,比如简单地把两个纹理乘到一起。这种光源在Section 6.9中有更深入的讨论。

阴影体积(Shadow Volumes)

  Heidmann在1991年提出了一个基于Crow的阴影体积Shadow Volume)的方法,这个方法巧妙地利用模板缓冲来向任意物体投射阴影。它能被用于任意GPU上,因为唯一的要求就是模板缓冲。它不是基于图像的(不像接下来要讲述的阴影贴图算法)也因此避免了采样的问题,能在非常多的地方生成锐利的阴影。这在某些时候可能是不利的一点。比如,一个角色的衣服可能有褶皱,那么会带来严重走样的薄硬阴影。阴影体积在如今已经很少被使用了,主要由于这种方法的不可预测的开销。我们在这里简洁地介绍下这个算法,因为它能体现一些重要的准则,并且基于这些准则的研究还在继续。
  首先想象一个点和一个三角形。现在从点发出直线让其穿过三角形的顶点到无穷远处,这会带来一个无穷的有着三个侧面的金字塔。三角形之下的部分是个截断的无穷金字塔,它之上的部分仅是个金字塔。下图为一张示例图。

img

现在想象一下点实际上是个点光源。那么任何一个在截断的金字塔内部的物体都会处于阴影中。截断的金字塔这个体积因此被称为阴影体积
  假设我们在某个视点观察场景,跟随一条穿过眼睛和像素的光线直到光线击中要在屏幕上被显示的物体。在光线到达物体的途中,当光线每次从正面(面向观察者的那一面)穿过阴影体积的一个面时,我们增加计数器的数值。因此,计数器会在光线每次进入阴影时增加。以相同的方式,当每次光线穿过截断的金字塔的背面时,我们减少计数器的数值。在光线到达物体的途中我们一直这样做,直到光线击中要在那个像素被显示的物体。击中要被显示的物体后,我们检查计数器,如果计数器大于零,那么像素会在阴影中。否则像素不在阴影中。这个准则也用于不止一个三角形投射阴影的情况,下图为一个相关的例子。

img

  上述这种跟随光线的方法有较高的时间开销。实际上还有一个更巧妙的方法,比如借助模板缓冲来完成计数的工作。首先,模板缓冲的数值要被清除。然后,整个场景要被渲染到帧缓冲中,并只使用未点亮材质的颜色,来让这些着色分量存储于颜色缓冲并让深度信息存储于z缓冲中。接着,z缓冲保持更新,而颜色缓冲的写入被关闭,然后绘制阴影体积的正面三角形。在这个过程中,模板缓冲的操作是在三角形被绘制的地方增加数值。然后另一个通道绘制阴影体积的背面三角形,这会让模板缓冲上相应位置存储的数值减少。增加和减少只会在被渲染的阴影体积的面的像素可见时进行(比如那些在帧缓冲中的物体背后的物体是不可见的,因此不能让这些物体对应的阴影体积影响模板缓冲)。上述过程完成后,模板缓冲实际上就存储着每个像素的阴影状态。最终,整个场景又会被渲染一次,这次只使用被光源影响的材质分量,并只在模板缓冲中数值为\(0\)的地方显示。数值为\(0\)意味着色点没有被遮挡物影响,因此这个位置要被光源照亮。
  这个计数方法是阴影体积的基本思想。使用这个方法生成的阴影的一个例子如下图所示

img

有一些高效的方法可以在单次通道中实现。然而,计数方法会在物体穿透相机的近平面时出问题。这个问题的解决方法被称为Z-fail,它涉及到计数在可见表面背后的交叉。下面以下图为例讲解一下

img

这次我们从无穷远处出发向着色点靠近进行计数,对于上图的\(A\)点来说计数的结果为\(0\),而对于\(B\)点来说结果为\(+2\),对于C点来说结果为\(+1\)。因此可以看到和一般的计数方法会有相同的结果,但现在设想另一种特殊情况。假设有个点光源被放置于场景中,相机位于点光源前面并向正前方观察,同时还有一个很大的遮蔽点光源的三角形图元位于相机和点光源之间,如果相机前方有个物体要被渲染,那么使用一般的方法进行计数会得到\(0\),我们这时就发现出问题了,这其实是因为阴影体积的顶面对于相机来说是不可见的。而使用Z-fail,我们可以得到计数结果为\(1\),因为从无穷远处出发穿过了阴影体积的底面到达了着色点。
  为每个三角形创建四边形会导致大量的绘制。也就是说,每个三角形会创建三个必须被渲染的四边形。一个有着一千个三角形组成的球面会创建三千个四边形,这些四边形可能都在屏幕上可见。一个解决方案是只绘制沿着物体的轮廓边缘的四边形,之前所说的球体可能只有五十个轮廓边,因此只需要绘制五十个四边形。几何着色器可以被用来自动地生成这种轮廓边。剔除和钳制技术也可以被用来降低填充的开销。
  然而,阴影体积算法有着极端的变化性这一非常大的缺点。想象一个单独在视野中的小三角形。如果相机和光源在完全相同的位置,那么阴影体积的开销会最小。四边形不会覆盖屏幕上的像素。只有三角形(阴影体积的顶面)和阴影体积的底面会被绘制。假设观察者现在开始绕着三角形观察。当相机远离光源时,阴影体积的四边形会更加可见并覆盖屏幕上更多的像素,导致了更多的计算发生。 如果观察者碰巧走进了三角形的阴影中,那么阴影体积会完全填充屏幕,导致大量时间被花费在评估是否被遮蔽上。这种变化性让阴影体积无法被用于交互式引用程序中。
  正是因为这些缺点,阴影体积这个方法在很多情况下已经被弃用了。然而,随着GPU的不断演化,GPU的数据逐渐有更多新的不同的访问方式,研究者可以巧妙地利用这些功能,阴影体积可能有天会回到我们的视野中。例如,Sintorn等人对一些能改善效率的阴影体积算法进行了概述,并提出了他们自己的层次加速结构。
  下一个被解释的算法为阴影映射,它有着更加可预测的开销,而且很适用于GPU。它对于很多应用场景来说是阴影生成的基础。

阴影贴图(Shadow Maps)

  在1978年,Williams提出了一个基于z缓冲的渲染器,这个渲染器可以为任意物体快速地生成阴影。它的想法是先从光源的位置使用z缓冲渲染场景,这样能记录离光源最近的表面。当使用这个方法时,只有z缓冲是需要的。光照、纹理映射、写入值到颜色缓冲可以被关闭。
  记录完离光源最近的表面后,我们称z缓冲的内容为阴影贴图Shadow Map),它有时也被称作阴影深度贴图Shadow Depth Map)或阴影缓冲Shadow Buffer)。为了使用阴影贴图,场景会以相机的视角被第二次渲染。当图元被绘制时,它在每个像素处的位置会被变换到阴影贴图上,阴影贴图上的位置的z深度接着会与阴影贴图上对应位置存储的z深度比较。如果这个位置的z深度要大,那么这个位置就被判断在阴影中,否则则不在阴影中。这个技术是通过纹理映射实现的,一张示例图如下所示

img

注解:可以看到\(\mathbf{v}_b\)相对于光源的z深度比阴影贴图上对应位置存储的z深度要高,因此\(\mathbf{v}_b\)附近渲染出来会有阴影。

阴影映射这个算法受欢迎,因为它相对来说是可预测的。构建阴影贴图的开销和被渲染的图元数量大致线性相关,并且它的访问时间是常量。当光源和物体保持静止时,阴影贴图生成后可以在每帧中被重复利用。
  一个z缓冲的生成对应着光源“看向”某个方向。对于遥远的定向光来说,光源的视野需要包含所有会向视图体投射阴影的物体。这种光源一般会使用正交投影,它的视野范围需要足够高并且足够宽。局部光源也会需要相似的调整。如果局部光源距离投射阴影的物体很远,那么单独一个视锥体也许是足够用来容纳所有投射阴影的物体的。另外,如果局部光源是聚光灯,那么会有一个视锥体与它关联,所有在视锥体外的物体都会被认为在阴影中。
  如果局部光源在场景中并且被投射阴影的物体环绕,一个常见的解决方法是使用立方体贴图。这些贴图被称为全向阴影贴图Omnidirectional Shadow Map)。全向阴影贴图要避免在缝隙处的伪影。King和Newhall深入分析了这个问题并提供了解决方案,Gerasimov提供了一些实现的细节。Forsyth提出了一个通用的多锥体划分方法用于全向光源,它在需要的地方提供更多的阴影贴图的分辨率。Crytek基于每个面的视锥体被投影后的屏幕空间覆盖率设置每个面使用的阴影贴图的分辨率,并将每个面的贴图存储于纹理图集中。
  现在所有在场景中的物体需要被渲染到光源的视图体中。首先,只有会投射阴影的物体需要被渲染。比如,如果已知地面只能接受阴影,那么就没必要把它渲染到阴影贴图中。
  阴影投射物按定义来说是那些在光源的视锥体中的物体。这个锥体可以使用一些方式来扩展或收紧,这能让我们安全地忽略掉一些阴影投射物。考虑一些对于眼睛可见的受影物体。这些物体在光源的观察方向上与光源之间的距离会小于某个最大距离。任何在这个距离外的物体都不能向可见的受影物体投射阴影。另一个例子是如果光源在眼睛的视锥体内,那么在这个额外的锥体外部的物体都不能向受影物体投射阴影。只渲染相关的物体不只能节约渲染的时间,同样也能降低光源的锥体的尺寸,从而增加阴影贴图的有效分辨率,因而能改善质量。此外,如果光源的锥体的近平面离光源远而又离远平面近,那么这能增加z缓冲的有效精度。
  阴影映射的一个劣势是阴影的质量会取决于阴影贴图的分辨率和z缓冲的数值精度。因为阴影贴图会在深度比较时被采样,所以这个算法容易有走样问题,尤其是在物体相互之间比较接近的地方。一个常见的问题是自阴影走样Self-shadow Aliasing),它通常被称为“表面痤疮”或“阴影痤疮”,这种问题的出现是因为三角形被错误地判断在阴影中。这个问题有两个来源。第一个源自于处理器的数值精度极限。另一个来源和几何有关,因为我们一般使用点采样来表示一个区域的深度。当光源存储的深度和可见表面的深度比较时,存储的深度可能会略微比表面的低,这就会导致自遮挡。这个问题的一张示例图如下图所示。

img

  一个常见的用来避免(缓解)各种阴影贴图的伪影的方法是引入偏移因子。当进行深度比较时,受影物体的深度会减去一个小偏移。这个偏移可以是一个常量,但是当表面不那么面向光源时会出问题。一个更有效的方法是使用和受影物体的朝向与光照方向之间的角度成比的偏移。当表面的朝向越偏离光照方向时,偏移就会越大来避免使用常量偏移会遇到的问题。这种类型的偏移被称为斜率缩放偏移Slope Scale Bias)。这两种偏移都可以使用如OpenGL的glPolygonOffset这类指令来应用,让每个多边形远离光源。要注意当表面直面光源时,斜率缩放偏移就不会起效果。因此一个常量偏移可以与斜率缩放偏移一起使用来避免可能的精度错误。斜率缩放偏移通常会被钳制到某个最大值,因为当光源相对于表面近乎侧向时正切值会非常高。下图是一张使用常量偏移和斜率缩放偏移的一张示例图

img

注解:左边这种情况未使用缓解阴影痤疮的方法,因此蓝着色点和橙着色点会被错误地判断在阴影中。而中间这种情况使用了常量偏移,改善了一点,但是蓝着色点仍旧会被错误地判断在阴影中。右边这种情况使用了斜率缩放偏移,在生成阴影贴图的时候,表面根据相对于光源的斜率被偏移,因此获得了更好的着色结果。

  Holbert引入了法线偏移Normal Offset Bias),这个方法会让受影物体的世界空间的位置沿着表面的法线方向偏移,偏移的程度和光照方向与几何法线的夹角的正弦成比。在修改深度的同时改变了阴影贴图上的采样位置。光照方向越偏离表面的法线,偏移的程度会越大,以让偏移后的位置位于表面上方足够远的位置来避免自遮蔽。这个偏移是世界空间上的偏移距离,Pettineo因此建议使用阴影贴图的深度范围来缩放它。Pesce提出可以沿着相机的观察方向偏移,它也是通过调整阴影贴图的坐标做到的。其它的偏移方法会在后续讨论。
  过大的偏移会导致漏光Light Leak)或彼得潘效应Peter Panning),物体会表现得像漂浮在它下面的表面之上。这种伪影的发生是因为物体与它之下的物体的接触点的附近位置被推得太靠前以至于接受不到阴影。
  一个用来避免自遮蔽的问题的方法是只把背面渲染到阴影贴图中。这被称为第二深度阴影映射Second-depth Shadow Mapping),这个方法在很多情况下都工作良好,特别是对于那些无法使用手动设置的偏移的渲染系统来说。然而,问题会发生在物体是双面的或厚度薄又或是与其它物体接触时,自遮蔽在这些情况下会发生是因为背面和正面都处于相同的位置。相似的,如果没有偏移,那么问题会发生在薄物体的轮廓边缘,因为背面在这些区域会离正面很近。在这种情况下增加偏移可以帮助避免表面痤疮,但是会导致漏光,这是因为受影物体与遮挡物背面的接触点附近没有间隔。下图展示了不同的阴影贴图的生成方法,包括使用正面、背面、中点。

img

要使用哪种方案会取决于实际情况。例如,Sousa等人发现使用正面用于阳光阴影,并且使用背面用于内部的光源对于它们的应用来说最好。
  在这里要注意物体必须是“防水的”(流形并且封闭的),或者必须同时把正面和背面渲染到阴影贴图中,要不然物体可能不会完全投射阴影。Woo提出了一个通用的方法,在只使用正面或背面用于阴影中找了个折中方案。它的想法是渲染封闭实体到阴影贴图中,并持续追踪离光源最近的两个表面。这个过程可以通过深度剥离或其它的和透明度相关的技术进行。两个物体的平均深度会作为中间层被存储于阴影贴图中,这有时被称为双阴影贴图Dual Shadow Map)。如果物体足够厚,那么自遮蔽和漏光伪影会被最小化。Bavoil等人讨论了一些方法来解决潜在的伪影,并提供了一些实现的细节。这个方法的主要缺点在于使用两个阴影贴图带来的额外开销。Myers讨论了一个被美术师控制的在遮挡物和受影物体之间的深度层。
  当观察者移动时,光源的视图体的尺寸通常会随着投射阴影的物体变化而改变大小。这种尺寸上的变化会导致阴影在帧与帧之间会有略微的偏移。这会发生是因为光源的阴影贴图在采样不同的光线方向的集合,这些方向与之前的方向是不对齐的。对于定向光来说,解决方法是强制连续生成的阴影贴图保持在世界空间中相同的相对纹素光束位置。也就是说,你可以认为阴影贴图在整个世界上施加了二维的网格化的参考系,每个网络单元格表示在贴图上的一个像素样本。当你移动时,阴影贴图会为不同的网络单元格的集合生成。用另一句话来说就是,光源的视图投影会作用于这个网格来维持帧到帧的一致性。

分辨率增强(Resolution Enhancement)

  和纹理是如何被使用的相似,在理想的情况下我们想让阴影贴图上的一个纹素覆盖一个图像像素。如果我们让眼睛处于光源的位置,那么阴影贴图上的纹素与屏幕空间内的像素将会有完美的一对一关系。当光照的方向变化时,这个对应关系将会被打破,因此会导致伪影。下图为一个相关的例子

img

注解:右图使用了LiSPSM改良了光源的矩阵,来让光源的采样模式尽可能与相机的采样模式相似。

可以看到左图中的阴影呈块状质量非常低,这是因为在这种情况下,前景中的大量像素与阴影贴图中的同一个纹素关联。这种不匹配被称为透视走样Perspective Aliasing)。当光照方向相对于表面近乎侧向时,而表面又面向观察者时,单个阴影贴图上的纹素也会覆盖多个像素。这个问题被称为投影走样Projective Aliasing),下图是个相关的例子。块状感可以通过增加阴影贴图的分辨率来减少,但是这会有更多的内存开销和处理开销。

img

  还有另一种方法能让光源的采样模式尽可能与相机的采样模式相似。这是通过改变场景向光源投影的方式做到的。一般来说我们会认为视图是对称的,视图向量会处于锥体的中心。然而,视图方向仅仅定义了一个视图平面,而没有定义哪些像素被采样。定义锥体的窗口可以偏移、歪斜、在平面上旋转,这创造了一些给予从世界空间到视图空间不同映射的四边形。这个四边形仍旧会被规则地采样,这是因为被GPU使用的线性变换矩阵的本质。采样率可以通过改变光源的视线方向和视图窗口的范围来被修改。下方是一个相关的例子

img

  将光源的视图映射到眼睛的视图有22个自由度。在这个空间中的探索最终得到了一些不同的算法用来让光源的采样率更好地匹配眼睛的采样率。这些方法包括透视阴影贴图Perspective Shadow MapPSM)、梯形阴影贴图Trapezoidal Shadow MapTSM)、光空间透视阴影贴图Light Space Perspective Shadow MapLiSPSM)。这一类的技术被称为透视扭曲Perspective Warping)方法。
  这些矩阵扭曲算法的优势在于除了修改光源的矩阵外不需要额外的工作。每个方法都有着优势和劣势,能为某些几何体和光照情况帮助匹配采样率,而另外一些情况的采样率则会更差。Lloyd等人分析了PSM、TSM、LiSPSM方法之间的等价关系,并给出了这些方法涉及到的采样问题和走样问题的优秀概述。当光照方向垂直于观察者的视线方向时,这些方法会处于最佳状态,因为在这种情况下透视变换会被偏移来让更多的样本集中到近处。
  当光源在相机前并指向相机时,矩阵扭曲技术将会失败。这种情况被称为视锥体对抗Dueling Frusta)。在这种情况下更多的阴影贴图的样本需要被用于近处,而线性扭曲只会让情况变糟。这些以及其它的问题,例如由于相机的移动导致阴影质量的突变,导致这些方法不再被青睐。
  在观察者附近集中更多的样本是个好想法,于是有了基于给定的视图生成一些阴影贴图的算法。Carmack参加了Quakecon 2004,并在他的主题演讲中描述了这一想法并造成了不小的影响。Blow独自实现了一个这样的系统。它的理念很简单,即生成固定数量的阴影贴图(分辨率有可能不同),并让每个阴影贴图覆盖场景中的不同区域。在Blow的方法中,四个阴影贴图会分层嵌套围绕观察者。这么做后,一个高分辨率的贴图可以用于近处的物体,更低分辨率的贴图可以用于更远的物体。Forsyth提出了一个类似的想法,生成不同的阴影贴图用于不同的可见物体。当物体跨过两个阴影贴图之间的边界时,上述算法会出问题,因此问题在于要解决边界处的过渡。在Forsyth的方法中,它让一个物体只与唯一一个阴影贴图关联。Flagship Studio开发了一个融合这两个想法的系统。一个阴影贴图会用于近处的动态物体,另一个阴影贴图被用于近处的静态物体的网格分区,此外还有一个阴影贴图会被用于整个场景中的静态物体。第一个阴影贴图会以每帧一次的频率进行更新。另外两个阴影贴图可以只生成一次,因为光源和几何体都是静态的。尽管这些系统都非常老了,但是它们基于的使用多个贴图用于不同的物体和不同情况的想法,是后来许多的算法的核心。
  在2006年,Engel、Lloyd等人、Zhang等人独立研究了相同的基本想法。这个想法是沿着视图方向使用相互平行的平面分割视锥体到一些不同的部分。下图是个相关的例子

img

随着深度增加,每个三维体的深度范围会是之前一个三维体的两到三倍。对于每个视图体来说,光源可以创建一个紧紧地包围分割后的三维体的锥体,并生成一个阴影贴图。通过使用纹理图集或纹理数组,不同的阴影贴图可以被当作一个更大的纹理对象,从而降低缓存访问延迟。使用这个方法得到的改善如下所示

img

Engel称这个算法为级联阴影贴图Cascaded Shadow MapCSM),它相比于Zhang的术语平行分割阴影贴图Parallel-split Shadow Map)来说更常被使用,但是两者通常都会出现于文献中。
  这种算法实现起来很直接,而且能覆盖场景的一大片区域,同时还有着合理的结果,与此同时抗扰性比较好。视锥体对抗这一问题可以以更高的采样率在近处采样来解决,使用级联阴影贴图不会有严重的问题。由于有这些优势,它因此被用于了许多应用中。
  在级联阴影贴图中,标准方法是为每个级联(cascade)使用一个单独的阴影贴图,每个阴影贴图可以使用透视扭曲来集中样本到更近的区域。如下图所示,每个阴影贴图覆盖的范围可以是不同的。

img

将更近的阴影贴图用于更小的视图体可以在近处提供更多的细节。在贴图之间的z深度范围划分被称为深度划分z-partitioning),过程可以相当简单或复杂。一个方法是使用对数划分,在这个方法中要先计算分割平面之间的距离比率,计算公式如下

\[r=\sqrt[c]{\frac{f}{n}} \]

式中的\(n\)\(f\)是整个场景的近平面和远平面,\(c\)为贴图的数量。假设场景中最近的物体有\(1\)米远,最远的距离为\(1000\)米。如果要使用三个级联贴图,那么\(r=\sqrt[3]{1000/1} = 10\),那么分割范围分别为\(1\)\(10\)\(10\)\(100\)\(100\)\(1000\)。在这个划分方法中,开始的近平面深度会有很大的影响。如果近深度只有\(0.1\)米,那么\(r \approx 21.54\)。这意味着每个阴影贴图会覆盖更大的范围,因而导致精度降低。在实践中,这种划分方法可以给予近平面附近的区域可观的分辨率,但是这个区域如果没有物体那么会被浪费。一个用来避免这个不匹配的方法是让划分距离为对数划分和均匀划分的加权混合。
  近平面的设置是一个挑战。如果它离眼睛太远,那么物体有可能被这个平面裁剪,导致非常不好的伪影。对于过场动画来说,美术师可以提前精确地设置这个值,但是对于交互式环境来说是个更难的挑战。Lauritzen等人提出了采样分布阴影贴图Sample Distribution Shadow MapSDSM),这个方法会使用来自上一个帧的z深度来从两个方法中确定一个更好的划分方法。
  第一个方法是查看z深度,找到最小值和最大值并基于这些值设置近平面和远平面。这是通过GPU上的叫规约Reduce)的操作做到的,在这个操作中一系列逐渐变小的缓冲会在计算着色器或其它着色器上被分析,输出的缓冲会反馈回来作为输入,直到只有一个\(1 \times 1\)的缓冲剩下。一般来说,计算出的值会被调整来适应场景中移动的物体。
  第二个方法也会分析深度缓冲中的值,创建一个叫直方图Histogram)的记录z深度的分布的图表。并找到靠近的近平面和远平面,图表中可能存在没有物体的空隙。分割平面因此可以被调整到有物体存在的附近区域,这能让级联贴图有着更高的z深度精度。
  在实践中,第一个方法更通用,而且速度很快(每帧\(1\)毫秒范围左右),同时质量也不错,它因此被许多应用采纳。下方为一张示例图

img

注解:左侧没有使用特殊的处理来调整近平面和远平面。右侧使用了SDSM来找到更紧凑的包围范围。

  当使用单独一个阴影贴图时,闪烁伪影会因为光源采样点在帧与帧之间移动而出现,当物体在级联之间移动时会更糟。有一些方法可以被用来保持世界空间中的稳定采样点,每个方法都有各自的优势。当物体跨过了阴影贴图之间的边界时,阴影质量的突变会发生。一个解决方法是让视图体略微重叠。从两个相邻的阴影贴图重叠的区域获取样本并进行混合。或者,在这个区域通过抖动取一个样本。
  由于级联阴影贴图的受欢迎度,有不同的方法已经被开发出来用于改善效率和质量。如果在一个阴影贴图的锥体内没有改变,那么这个阴影贴图就不需要被重新计算。对于每个光源来说,投射阴影的物体可以被预计算。由于较难察觉阴影是否正确,一些技巧可以被采取用于级联或是其它的算法。一个技术是让具有低细节等级的模型作为代理几何体来投射阴影。另一个技术是移除那些过小的遮挡物。更遥远的阴影贴图可能不会在每帧都更新,因为理论上来说这种阴影是不那么重要的。但是场景里如果有大的移动物体,那么会有伪影的风险,因此需要谨慎使用。Day提出了一个想法,让遥远的贴图在帧与帧之间平移,他的想法是静态的阴影贴图在在帧与帧之间是可复用的,可能只有边缘区域会变化因此需要渲染。DOOM(2016)等游戏使用了一个阴影贴图的大图集,当物体移动时只更新对应的阴影贴图。更远的级联贴图可以完全忽略动态物体,因为这种阴影对场景的贡献可能很小。在某些环境下,一个高分辨率的静态阴影贴图可以被用于更远的级联,这能显著降低工作负载。一个稀疏纹理系统可以被用于当单独一个静态的阴影贴图对场景而言太大的时候。级联阴影映射可以与预烘培的光照贴图纹理或其它的适用于某些特定情况的阴影技术结合。Valient的报告是值得注意的,他描述了用于不同类型的视频游戏的阴影系统的定制化和技术。Section 11.5.1详细描述了预计算的光照和阴影算法。
  创建一些分开的阴影贴图意味着要遍历几何体。有一些在单次通道中渲染遮挡物到一系列阴影贴图的方法已经被开发用来改善效率。几何着色器可以被用来复制物体的数据,并发送数据到多个视图。实例化几何着色器允许一个物体被输出到至多\(32\)个深度纹理上。多视口扩展可以执行一些操作,比如渲染一个物体到特定的纹理数组切片上。Section 21.3.1在虚拟现实的上下文中进行了相关的详细描述。视口共享的一个可能的缺点是为所有阴影贴图生成的遮挡物必须要被送入管线,而不是只在遮挡物与阴影贴图相关时被渲染。
  你现在所处的地方正位于数以亿计的光源的阴影中。只有少量光线进入了你的眼睛。在实时渲染中,如果要为大场景中的许多光源同时计算阴影,那么会极其费力。如果空间中有个三维体在视锥体内但是又对于眼睛不可见,那么遮挡这个受影三维体内的物体不需要被评估。Bittner等人使用了遮挡剔除来找到所有对眼睛可见的受影物体,并从光源的视角将所有潜在的受影物体渲染到一个模板缓冲掩码中。这个掩码编码了那些从光源的视角下被观察的受影物体。为了生成阴影贴图,他们从光源的视角使用遮挡剔除渲染物体,并使用掩码剔除没有受影物体存在的地方的遮挡物。各种各样的剔除策略也可以用于光源。因为辐照度是随距离的平方的反比降低的,一个常用的技术是在某个阈值距离后剔除光源。例如,在Section 19.5的门户剔除技术会找到光源影响的空间单元。这是一个正在被研究的领域,因为可以从中获得可观的性能收益。

百分比更近滤波(Percentage-Closer Filtering)

  阴影贴图的一个简单扩展技术可以提供伪软阴影。这个方法也能帮助改善当光源采样单元格覆盖太多屏幕上的像素带来的块状阴影这种分辨率问题。解决方法和纹理放大相似。与其在阴影贴图上取单独一个样本,不如取四个邻近样本。这个技术不会插值深度,而会插值比较后的结果。也就是说,表面深度会与四个纹素深度分别比较,这些结果可以是\(0\)(在阴影中)或\(1\)(被光源点亮),它们会被双线性插值用来计算光源对表面位置的实际贡献有多少。这样滤波后会有一个人工的软阴影。这些半影和阴影贴图的分辨率、相机位置还有其它的因素有关。例如,更高分辨率的阴影贴图会让阴影边缘的软化区域变窄。当然了,有一点半影和平滑是比完全没有要好的。
  这个从阴影贴图中取回多个样本并混合比较结果的想法被称为百分比更近滤波Percentage-Closer FilteringPCF)。区域光源能生成软阴影。因为在表面位置的着色其实就是看区域光源有多少比例可以看到着色点。PCF则尝试反转这个过程,来近似软阴影用于点式光(或定向光)。与其找到光源相对于某个表面位置的可见范围有多少,不如找到表面位置周围的位置在点式光视角下的可见度。下图是个相关的例子

img

“百分比更近滤波”其实指代着终极目标,即找到样本中有多少百分比在光源的视角下可见。这个百分比会被用来决定光源对表面着色点有多少贡献。
  在PCF中,位置会生成于表面位置周围,它们的深度差不多,但是会处于阴影贴图上的不同纹素位置。每个位置的可见度都会被检查,这些结果都是布尔值,即点亮或未点亮,它们会被混合用来获得软阴影。要注意这个过程是非物理的,它没有直接采样光源而是依赖于在表面上采样这一想法。到遮挡物的距离不会影响结果,阴影因此会有着相似尺寸的半影。尽管如此,这个算法在很多情况下还是可以提供合理的近似的。
  一旦采样的范围确定了,那么在接下来的采样中要注意避免走样伪影。对于采样和滤波邻近的阴影贴图位置来说有许多的变体方法。这些方法区别于采样范围、采样的样本数量、采样模式、样本的权重的确定。在那些能力较弱的API中,采样过程可以通过一个与双线性插值相似的特殊纹理采样模式来加速,这个模式会访问四个相邻位置。四个样本会与一个给定的值比较,通过比较的次数所占的比率会被返回。然而,在规则的网格中进行最近邻采样会有显眼的伪影。使用一个模糊结果但又注意物体边缘的联合双边滤波器可以在改善质量的同时避免阴影泄漏到其它表面上。Section 12.1.1更加详细地描述了这个技术。
  DirectX 10引入了单指令双线性滤波用于PCF,带来了更加平滑的结果。相比于最近邻采样来说有着好得多的视觉提升,但是规则采样带来的问题还是存在。一个用来最小化规则采样伪影的方法是使用预计算的泊松分布模式采样一个区域,下图为一张示例图

img

注解:最左边使用了\(4 \times 4\)的最近邻采样,右边两张使用了在圆盘上的12采样点的泊松采样模式。可以看到中间这张图相比于左边有更好的结果,但是伪影依然可见。右边这张图的采样模式在像素间会有随机的旋转,结构化的阴影伪影变成了不那么令人反感的噪声。

这个分布会让采样点散开,来让它们在保持合理的距离同时又不处于规则的模式。然而正如我们所知的是,为每个像素使用相同的位置分布还是会导致重复模式伪影。这种伪影可以让采样点围绕中心随机旋转来避免,这操作会把走样转化为噪声。Casta˜no发现泊松采样生成的噪声因它们的光滑和风格化的外表而格外显眼。他提出了一个基于双线性插值的高效高斯加权采样方法。
  自遮蔽问题和漏光还有痤疮和彼得潘效应在PCF中会变得更糟糕。斜率缩放偏移会基于表面与光源的相对角度推远表面,这基于样本在阴影贴图上偏移不超过一个纹素的假设。在表面上的某个位置采样更大的一片范围时,有些测试样本可能被真实存在的表面挡住。
  有一些其它的偏移因子已经被开发出来了,它们也能被用来减少自遮蔽现象。Burley描述了偏移锥Bias Cone),每个样本会向光源移动,移动的距离和它到原始样本的距离成比。Burley建议使用\(2.0\)的斜率和一个小的常量偏移。下图为一个相关的例子

img

注解:这些是不同的阴影偏移方法。对于PCF来说,一些样本会在原始样本周围。这些样本应该都是被点亮的。在左边这张图中,一个偏移锥被使用,样本都移动到了偏移锥上。它的坡度可以增加来让更多的样本被点亮,但是有着漏光的代价。在中间这张图中,所有样本都被调整到了受影物体的平面上。这对于凸表面来说很有用,但是在凹陷处会适得其反。在右边这张图中,法线偏移被使用,让所有样本沿着法线方向移动,移动的距离与法线和光照之间的角度的正弦成比。对于中间的样本来说,可以认为是有个假想的表面移动到了原始表面的上方。这个偏移不止影响了深度,同时也改变了采样阴影贴图的纹理坐标。

  Sch¨uler、Isidoro、Tuft提出了一些技术,这些技术都基于受影物体表面的斜率应该被用来调整其余样本的深度这一想法。在这三个技术中,Tuft的最容易被应用于级联阴影贴图。Dou等人进一步改进并扩展了这一技术,把z深度是非线性变化的这一点纳入了考量。这些方法都假设邻近的样本位置都在同一个平面上。这涉及到受影平面深度偏移Receiver Plane Depth Bias)或其它相似的术语,在很多情况下这个技术是非常精确的,因为这个假想平面上的位置确实在表面上,又或者在前方,如果模型是凸起的。如上图所示那样,在凹陷处附近的一些采样会被判断在阴影中。常量偏移、斜率缩放偏移、受影物体平面偏移、视角偏移、法线偏移等技术会被结合使用来避免自遮蔽的问题。当然了,针对每个环境进行手动调整也是非常必要的。
  PCF还有一个问题,由于采样范围的大小保持不变,阴影会表现得均匀软化,阴影边缘会具有相同宽度的半影。这在一些情况下也许是可以接受的,但是当遮挡物与受影物体接触时会出问题,下图为一张示例图。

img

百分比更近软阴影(Percentage-Closer Soft Shadows)

  在2005年,Fernando提出了一个具有影响力的方法,这个方法被称为百分比更近软阴影Percentage-Closer Soft ShadowPCSS)。它专注于搜寻阴影贴图上的附近区域来找到所有可能的遮挡物。这些遮挡物到光源的距离的平均值会被用来决定采样区域的宽度,公式如下所示

\[w_\text{sample} = w_\text{light} \frac{d_r-d_o}{d_r} \]

\(d_r\)为受影物体到光源的距离,而\(d_o\)为平均遮挡物距离。当平均距离越高(遮挡物越靠近受影物体)时,表面采样区域的宽度会越低。下图为一张示例图

img

  如果没找到遮挡物,那么位置会被点亮从而不需要后续的处理。相似的,如果位置被完全遮挡那么也不需要处理。其它的情况则需要在区域内采样并计算光源的近似贡献。为了节省处理的开销,可以让样本的个数随采样区域的大小变化。其它的技术也可以被使用,比如使用更低的采样率用于遥远的贡献不是很大的软阴影。
  这个方法的一个缺点是需要采样阴影贴图上合理大小的区域来寻找遮挡物。使用旋转后的泊松圆盘模式可以帮助隐藏降采样伪影。Jimenez指出泊松在物体运动时会不稳定,并发现使用一个介于抖动和随机的函数获得的螺旋采样模式可以在帧与帧之间给予更好的结果。
  Sikachev等人深入讨论了使用SM 5.0的PCSS的一个执行速度更快的实现,这个实现通常被称为接触硬化阴影Contact Hardening ShadowCHS),它被AMD引入。这个新版本同时也致力于解决另一个基础的PCSS会遇到的问题,即半影的大小会受到阴影贴图分辨率的影响。这个问题可以通过为阴影贴图生成mipmap,接着选择和用户定义的世界空间的滤波核大小最接近的mipmap等级。一个\(8 \times 8\)的区域会被采样来找到平均遮挡物深度,这只需要16次GatherRed()纹理调用。一旦半影的估计范围被找到了,更高分辨率的mipmap级别会被用于阴影的锐利区域,更低分辨率的mipmap级别会被用于更软的区域。
  CHS已经被用于很多视频游戏中,相关的研究还在继续。例如,Buades等人提出了可分离软阴影映射Separable Soft Shadow Mapping),PCSS采样网格的过程被分到了一些单独的部分中,从像素到像素会尽可能重复利用元素。
  对于每像素需要多个样本的加速算法来说,层次的minmax的阴影贴图被证实很有用。阴影贴图的深度一般是不能被平均的,在每个mipmap存储上一级mipmap中每组纹素深度的最小值和最大值是很有用的。也就是说,可以有两个mipmap,一个用来存储最大的深度(有时被称为HiZ),另一个用来存储最小的深度。给定一个纹素位置、深度、被采样的范围,采样对应的mipmap可以快速地判断是否被光源点亮或是被完全遮蔽。例如,如果纹素的z深度比对应的mipmap存储的最大z深度还要大,那么这个纹素一定在阴影中。这种类型的阴影贴图让光源的可见度判断变得高效了许多。
  PCF等类似的方法会采样受影位置邻近处。PCSS则会计算周围遮挡物的平均深度。这些算法没有把光源的范围纳入考量,而是采样邻近表面,因此会受到阴影贴图分辨率的影响。PCSS背后的一个主要猜想是平均遮挡物距离是半影大小的合理估计。当两个遮挡物,比如街道上的灯和远处的高山部分遮蔽了在相同表面上的一个像素时,这个假设会不成立而且会导致伪影。理想地来说,半影的计算应该是要看光源有多少区域对于某个受影位置是可见的。一些研究者利用GPU探索了反投影Backprojection)。它背后的想法是每个受影位置会被当作视点,而区域光源会被当作视平面的一部分,遮挡物会被投影到这个平面上。Schwarz和Stamminger还有Guennebaud等人总结了前人的工作,并提供了他们的一些改善方法。Bavoil等人则采取了一个不同的方法,使用深度剥离来创建一个多层的阴影贴图。反投影可以给予非常好的结果,但是每像素的开销很高,因此在交互式应用中不怎么被使用。

滤波阴影贴图(Filtered Shadow Maps)

  一个算法允许生成的阴影贴图的滤波,这个算法名为方差阴影贴图Variance Shadow MapVSM)由Donnelly和Lauritzen提出。它在一个贴图中存储深度,同时在另一个贴图中存储深度的平方。MSAA或其它的抗走样方法能被使用来生成方差阴影贴图。这些贴图能被模糊、mipmapped、放到累积面积表或被应用其它的方法。把这些贴图看作可滤波纹理的能力是个非常大的优势,因为在这种情况下各种采样和滤波技术得以被使用。
  在这个部分我们会稍微深入的描述下VSM,解释它的工作过程。当然了,这一类算法都采用相同类型的可见性测试方法。对这一领域感兴趣的读者应该去查找一些相关的资料,我们在这里推荐Eisemann等人写的书,这本书更加有深度地进行了相关的讨论。
  首先,对于VSM来说深度贴图会在受影位置上被采样一次,来获得离光源最近的遮挡物的平均深度。我们记这个平均深度为\(M_1\),它被称为一阶矩First Moment),如果受影物体的深度比它低,那么受影物体会被判断处于光照中。否则就需要先使用如下的公式

\[p_\text{max}(t) = \frac{\sigma^2}{\sigma^2+(t-M_1)^2} \]

式中的\(p_\text{max}\)是样本在光照中的最大百分比,\(\sigma^2\)是方差,\(t\)为受影物体的深度,\(M_1\)为阴影贴图中的平均期望深度。我们记深度平方的阴影贴图中对应位置上的值为\(M_2\),它被称为二阶矩Second Moment),它会被用来计算方差

\[\sigma^2 = M_2 -M_1^2 \]

  \(p_\text{max}\)值是受影物体对于光源的可见百分比的上界。实际的点亮百分比\(p\)不能超过这个值。这个上界来自切比雪夫不等式的单侧形式。上方这两个公式在尝试使用概率论估计遮挡物有多少分布在相比于表面位置离光源更远的地方。Donnelly和Lauritzen展示了平面遮挡物和平面受影物体在固定深度的例子,在这个例子中\(p=p_\text{max}\),因此上述的公式可以较好地近似许多真实的阴影情况。
  Myers建立了一个直观的理解方法。区域上的方差会在阴影边缘处增加。深度差越大,方差就会越大。\((t-M_1)^2\)因此在可见百分比中是个重要的决定项。如果这个值略微大于\(0\),那么就意味着平均遮挡物深度相比于受影物体的深度来说略微低一些,\(p_\text{max}\)会接近于\(1\)。这种情况会发生在半影的完全照亮的边缘处。从边缘向内移动,平均遮挡物深度会更接近光源,\((t-M_1)^2\)因此变得更大,\(p_\text{max}\)于是降低。方差与此同时也会在半影内改变,从边缘接近\(0\),逐渐变化到有最大值的区域上,这些区域上有着均匀分布的不同深度的遮挡物。这些项最终协调到一起,给予了在半影上线性变化的阴影。下方为一张对比图

img

注解:左上为标准的阴影映射,右上为透视阴影映射,左下为百分比更近软阴影,右下为方差阴影映射。

  方差阴影映射的一个重要特性是能以优雅的方式处理由于几何体带来的表面偏移问题。Lauritzen推导了表面的斜率应该怎么样修改二阶矩的值。对于方差映射来说,偏移以及其它和数值稳定性有关的问题对于方差映射来说是个麻烦。例如\(\sigma^2 = M_2 -M_1^2\)这种让一个较大的值减去另一个较大的值这类运算容易放大底层的数值表示的精度缺陷。使用浮点纹理可以帮助避免这个问题。
  VSM总体来说可以给予可观的质量改善,此外在速度上也有着一定的优势,因为这个算法会利用GPU优化后的纹理相关的能力。而PCF则需要更多的样本,因此需要更多的时间,来避免在生成更软的阴影时出现噪声,VSM可以只凭借单独一个高质量的样本来计算整个区域的效果来生成平滑的半影。这意味着阴影在算法的限制内可以有任意的软化程度且无额外的开销。
  在PCF中,滤波核的宽度决定了半影的宽度。受影物体和最近的遮挡物之间的距离可以被用来改变滤波核的宽度,这能给予更令人信服的软阴影。Mipmap上的样本是半影覆盖率较差的估计量,使用它会有块状伪影。Lauritzen更细节地描述了如何使用累计面积表来生成更好的阴影,它的一个例子如下图所示。

img

  当有两个或更多的遮挡物在半影区域并且其中一个更靠近受影物体时方差阴影映射会出问题。在这种情况下概率论中的切比雪夫不等式将会生成一个最大光照值,我们的近似公式因此就失效了。这会导致漏光,那些被完全遮挡的区域仍然会受到光照,下方为一张示例图。

img

通过在更小的区域上取更多的样本,这个问题可以被解决,这其实把方差阴影映射变成了某种形式的PCF。PCF需要在速度和性能之间进行权衡,对于那些有着低深度复杂度的场景来说,方差映射效果不错。Lauritzen给予了一个由美术师控制的缓解这个问题的方法,在这个方法中低百分比会被认为完全在阴影中,剩余的百分比会被重映射到\(0\%\)\(100\%\)。这个方法可以缓解漏光,但是会以半影变窄为代价。尽管漏光是个严重的限制,但是VSM对于地面的阴影生成来说是个好方法,因为这种情况下的阴影很少会涉及多个遮挡物。
  由于能使用滤波技术来快速地生成平滑的阴影,滤波阴影贴图引起了很大的关注。最主要的挑战是要解决不同的漏光问题。Annen等人引入了卷积阴影贴图Convolution Shadow Map)。它拓展了Soler和Sillion的用于平面遮挡物的算法背后的想法,这一方法会编码阴影深度到傅里叶展开。和方差阴影映射一样,这种贴图也可以被滤波。这个方法可以收敛到正确的结果,漏光问题因此被减轻。
  卷积阴影映射的一个缺点是一些项需要被计算和访问,这导致了执行开销和存储开销的增加。Salvi和Annen等人同时独立地想到了使用单独一个基于指数函数的项。这个方法被称为指数阴影贴图Exponential Shadow MapESM)或指数方差阴影贴图Exponential Variance Shadow MapEVSM),它会存储深度的指数和它的二阶矩到两个缓冲中。指数函数可以更加接近地近似阴影贴图会执行的阶跃函数(比如在光照中或不在光照中),这个方法因此能显著降低漏光伪影。它也避免了卷积阴影映射会有的叫振铃Ringing)的问题,小的漏光现象会发生在深度略微超过原始遮挡物的深度时。
  存储指数值的一个限制是二阶矩值会变得非常大,以至于超过浮点数的范围。为了改善精度并让指数函数下降得更快,z深度可以是线性的。
  由于在质量上优于VSM,以及相比于卷积贴图有着更低的存储开销和更好的性能,指数阴影贴图在三个滤波方法中是最令人关注的。Pettineo指出了其它的改善方法,比如使用MSAA来改善结果并获得一些受限的透明度,他并且还描述了滤波性能如何通过计算着色器来改善。
  在更近的一段时间,矩阴影贴图Moment Shadow Mapping)由Klein等人引入。这个方法提供了更好的质量,但是会使用四个或更多的矩,因而增加了存储的开销。通过使用16比特位的整数来存储矩,开销可以降低。Pettineo实现了这个方法,并将其与ESM比较,而且还提供了相关的代码来探索更多的变体。
  级联阴影贴图可以用于滤波贴图来改善精度。级联ESM在标准的级联阴影贴图之上的优势就是所有的级联可以使用相同的一个偏移因子。Chen和Tatarchuk探索了级联ESM有的一些漏光问题和其它会遇到的伪影问题的一些细节,并提供了一些解决方法。
  滤波贴图可以被认为是PCF的一种不那么昂贵的需要更少样本的形式。和PCF相似,阴影会有着常宽度。这些滤波方法可以与PCSS一起使用来得到宽度变化的半影。有些矩阴影映射的扩展方法也包含了提供光散射和透明度效果的能力。

posted @ 2026-05-05 19:22  TiredInkRaven  阅读(0)  评论(0)    收藏  举报