Shadows

"《Real-Time Rendering 4th》笔记——第七章 阴影"

Posted by Ciel on December 19, 2019

Shadows

本章主要介绍计算阴影的基本原理,并介绍最重要和最流行的实时算法,以及不太流行但是体现了重要原则的方法。

本章使用的术语如图7.1所示,其中occluders将阴影投射到receivers上。精确光源,即那些非区域光,只产生完全阴影区域,有时被称为硬阴影(hard shadows)。如果使用面积或体积光源,则产生柔和的阴影。每个阴影可以有一个完全阴影区域,称为本影,和一个部分阴影区域,称为半影。阴影边缘模糊的视为软阴影(soft shadows)。然而,需要注意,只是用低通滤波器模糊硬阴影不能正确渲染软阴影。

7.1

原书图7.1,阴影术语:light source 光源、occluder 遮光器、receiver 接收器、shadow 阴影、umbra 本影、penumbra 半影。

从图7.2中可以看出,投射阴影的几何图形离接收器越近,软阴影就越清晰。软阴影的本影区域不等于由精确光源产生的硬阴影。相反,软阴影的本影区域会随着光源变大而减小,如果光源足够大,接收器距离遮挡器足够远,它甚至可能消失。软阴影通常是可取的,因为半阴影边缘让观众知道,阴影确实是一个阴影。硬边阴影通常看起来不那么真实,有时会被误解为实际的几何特征,比如表面的折痕。然而,硬阴影比软阴影渲染速度快。

7.2

原书图7.2,硬阴影和软阴影的混合。板条箱的阴影很锋利,因为遮挡者靠近接收器。人的影子在接触点是尖锐的,随着与遮挡者距离的增加而变软。远处的树枝投下柔和的阴影。

有一个不准确的阴影通常比没有要好。没有一些阴影作为视觉线索,场景往往难以令人信服,更难以理解。眼睛对阴影的形状是相当宽容的。例如,在地板上使用一个模糊的黑色圆圈作为纹理,可以将角色锚定在地面上。

在接下来的部分中,将介绍从场景中的遮挡器实时自动计算阴影的方法。第一部分处理投射在平面表面上的阴影的特殊情况,第二部分介绍更一般的阴影算法,即将阴影投射到任意表面。硬阴影和软阴影都将被覆盖。最后,提出了适用于各种阴影算法的优化技术。

1平面阴影

当物体将阴影投射到平面上时,就会出现简单的阴影。本节介绍了几种用于平面阴影(Planar Shadows)的算法,每种算法在阴影的柔度和真实感方面都有所不同。

1.1投影阴影

在这个方案中,二次渲染三维对象以创建阴影。可以导出一个矩阵,将一个对象的顶点投射到平面上。考虑图7.3中的情况,光源位于l处,需要投影的顶点在v处,投影后顶点在p处。下面将计算投影矩阵特殊情况,即阴影平面y = 0时的计算,然后推广到任何平面。

7.3

原书图7.3,左图:一个光源在l位置,投射阴影到平面y=0处。顶点v被投影到平面上,投射点为p。用相似三角形推导投影矩阵。右图:影子投射到平面上,$\pi :n \cdot x+d$。

我们先求出x坐标的投影。由图7.3左半部分的相似三角形可得

e7.1

z坐标也是用同样的方法得到的:$p_z=(l_yv_x-l_xv_y)/(l_y-v_y)$,而y坐标是0。现在这些方程可以转换成投影矩阵M:

e7.2

很容易证明Mv = p,这意味着M确实是投影矩阵。

在一般情况下,阴影投射的平面不是y=0,而是$\pi :n \cdot x+d$,如图7.3。我们的目的是找到v投影到p的矩阵。l处的射线,经过v,与平面相交。这就得到了投影点p:

e7.3

这个等式转化成公式7.4中的投影矩阵,满足Mv=p:

e7.4

如果平面为y = 0,这个矩阵变成了7.2式中的矩阵,即n = (0,1);和d = 0。

要渲染阴影,只需将此矩阵应用于应该在平面π上投射阴影的对象,然后使用深色和无照明效果渲染此投影对象。在实践中,必须采取措施,以避免让投影在接收它们的表面下渲染。一种方法是在投影平面上添加一些偏差,这样阴影三角形总是渲染在平面前面。

一种更安全的方法是首先绘制地平面,然后在关闭z缓冲区的情况下绘制投影的三角形,然后照常渲染其余的几何图形。这样阴影总是在地平面的顶部绘制,因为不进行深度比较。

如果地平面有一个极限,例如,它是一个矩形,投影的阴影可能会落在它的外面,打破了错觉。为了解决这个问题,我们可以使用模板缓冲区。首先,将接收器绘制到屏幕和模板缓冲区。

另一种阴影算法是将阴影渲染成纹理,然后应用到地平面。这种纹理是一种光照贴图,一种调节底层表面强度的纹理(第11.5.1节)。正如我们所看到的,这种将阴影投射到纹理上的想法也允许在曲面上产生半阴影和阴影。此技术的一个缺点是纹理会被放大,单个纹理像素覆盖多个像素,从而打破了错觉。

如果阴影的情况在帧与帧之间没有变化,即光和阴影不相对移动,这种纹理可以重复使用。如果没有发生变化,大多数阴影技术都可以从重复使用帧与帧之间的中间计算结果中获益。

阴影投射器必须在光源和平面接收器之间。如果光源低于物体最高点,就会产生一个反阴影(antishadow),因为每个顶点都是通过光源投射的。正确的阴影和反阴影如图7.4所示。如果我们投射一个在接收平面以下的物体,也会发生错误,因为它也不应该投射阴影。

7.4

原书图7.4,左图是正确的阴影,右图是反阴影,因为光源在物体顶点之下。

当然可以显式地剔除和修剪阴影三角形以避免此类错误。下面介绍一种更简单的方法,使用现有的GPU管道来执行带有裁剪的投影。

1.2柔和阴影

投射的阴影可以用各种技术变得柔和。Heckbert和Herf提出了一种产生软阴影的算法。该算法的目标是在显示软阴影的地面上生成纹理。还有一种不那么精确但速度更快的方法。

当光源有一定面积时,就会出现柔和的阴影。一种近似区域光效果的方法是通过在其表面放置几个点状光来采样。对于每一个这样的点状光源,图像都被渲染并缓存到一个缓冲区中。这些图像的平均值就是一个带有柔和阴影的图像。请注意,在理论上,任何生成硬阴影的算法都可以与这种累积技术一起用于生成半阴影。在实践中,以交互速率这样做通常是站不住脚的,因为这会涉及到执行时间。

Heckbert和Herf使用基于视锥的方法生成阴影。想法是将光视为观察者,并且地平面形成了视锥的远裁剪平面。截锥体的宽度足以容纳遮光物体。

一个柔和的阴影纹理是通过生成一系列地平面纹理而形成的。区域光源在其表面上采样,每个位置用于对代表地面的图像进行阴影处理,然后将阴影投射到该图像上。对所有这些图像进行求和平均,生成一个地平面阴影纹理。有关示例参见图7.5的左侧。

7.5

原书图7.5,左边是使用Heckbert和Herf方法的渲染,使用了256个passes。右边是Haines的方法,使用一个pass。Haines的方法导致本影太大,这在门口和窗户周围尤为明显。

采样区域光方法的一个问题是它看起来像是:点光源发出的多个重叠阴影。同样,对于n次阴影遍历,只能生成n + 1个不同的阴影。大量遍历可以得到准确的结果,但是要付出高昂的代价。 该方法可用于获取(字面意义上的)“真实地面”图像,以测试其他更快算法的质量。一种更有效的方法是使用卷积,即滤波。在某些情况下,模糊单一点产生的硬阴影就足够了,并且可以产生半透明的纹理,可以与真实世界的内容融合,如图7.6。然而,在物体与地面接触的地方,一个均匀的模糊是无法令人信服的。

7.6

原书图7.6,从上方渲染阴影,然后使图像模糊并在地面上渲染生成阴影纹理。

还有许多其他方法可以提供更好的近似值,但需要付出额外的代价。例如,Haines从投影的硬阴影开始,然后使用从中心的黑暗到边缘的白色渐变来渲染剪影边缘,创造出类似半阴影。参见图7.5的右侧。然而,这些半阴影在物理上是不正确的,因为它们也应该延伸到轮廓边缘内的区域。Iwanicki借鉴了球谐函数的思想,用椭球体近似遮挡以产生柔和的阴影。所有这些方法都有不同的近似值和缺点,但是比平均计算一组大的阴影图像要有效得多。

2曲面上的阴影

将平面阴影扩展到曲面的一个简单方法是使用生成的阴影图像作为投影纹理。从光的角度考虑阴影。光能看见的就被照亮;它看不见的东西在阴影里。假设从光源的角度来看,遮挡物被渲染为黑色,否则呈现白色纹理。这个纹理可以投射到接收阴影的表面上。接收器上的每个顶点都有一个(u,v)计算它的纹理坐标,并将纹理应用于它。应用程序可以显式地计算这些纹理坐标。这与前一节中的地面阴影纹理稍有不同,在前一节中,对象被投射到特定的物理平面上。在这里,图像是从光的角度拍摄的,就像投影机里的一帧胶片。

渲染时,投影的阴影纹理修改接收器的表面。它也可以与其他阴影方法相结合,有时主要用于帮助感知物体的位置。例如,在一个平台跳跃的游戏中,主角可能总是被直接赋予一个阴影,即使角色处于完全的阴影中。更精细的算法可以得到更好的结果。例如,Eisemann和Decoret假设有一个矩形顶灯,并创建一个对象水平切片的阴影图像堆栈,然后将其转换为mipmaps或类似的东西。通过使用它的mipmap,每个切片的相应区域与它到接收器的距离成比例,这意味着更远处的切片会投射出更柔和的阴影。

纹理投影方法存在一些严重的缺陷。首先,应用程序必须识别哪些对象是遮挡器,哪些对象是它们的接收者。接收器必须由程序维护,使其比遮光器离光更远,否则阴影就会向后投射。此外,遮挡对象也无法遮挡自身。接下来的两部分将介绍生成正确阴影的算法,而不需要这种干预或限制。

注意,可以通过使用预构建的投射纹理获得各种照明模式。聚光灯只是一个正方形投影的纹理,在其中的一个圆圈定义了灯光。百叶帘效果可以通过由水平线组成的投射纹理来创建。这种类型的纹理被称为light attenuation mask、cookie texture或gobo map。通过简单地将两个纹理相乘,可以将预构建的模式与在动态创建的投影纹理相结合。这些灯在6.9节中进一步讨论了。

3阴影体

Heidmann在1991年提出了一种基于Crow的阴影体方法,该方法巧妙地利用模板缓冲将阴影投射到任意物体上。它可以用于任何GPU,唯一的要求是模板缓冲。它不是基于图像的(不像下面介绍的阴影映射算法),因此避免了采样问题,从而产生正确的锐利阴影。这有时是一个缺点。例如,角色的衣服可能会折叠起来,形成薄而硬的阴影,从而严重锯齿。由于其不可预测的成本,体积阴影现在很少被使用。我们在这里对算法进行了简要的描述,因为它说明了一些重要的原则和基于这些原则的研究。

首先,想象一个点和一个三角形。将这些线从一个点通过三角形的顶点延伸到无限远处,就得到了一个无限的三面金字塔。三角形下的部分,即不包括点的部分,是一个截断的无限金字塔,而上面部分只是一个金字塔。图7.7对此进行了说明。现在假设这个点是一个点光源。然后,在被截断的金字塔体(三角形下方)内的物体的任何部分都处于阴影中。这个体积称为阴影体shadow volume。

7.7

原书图7.7,左:点光源的光线通过三角形的顶点延伸,形成一个无限的金字塔。右:上半部分为金字塔,下半部分为无限截椎金字塔,也称阴影体。所有的几何图形都在阴影中。

假设我们查看某个场景,并通过一个像素跟踪来自眼睛的光线,直到光线击中要在屏幕上显示的对象。光线在到达该对象的途中,每当光线穿过阴影体的正面(即面向观察者)的一个面时,我们都会增加一个计数器。因此,每次光线进入阴影时,计数器都会增加。 以相同的方式,每当光线穿过截顶的金字塔的背面时,我们便递减相同的计数器。光线就会从阴影中消失。我们继续进行操作,递增和递减计数器,直到射线照射到要显示在该像素上的对象为止。如果计数器大于0,则该像素处于阴影中;否则就不是。这个原则也适用于有多个三角形投射阴影的情况,如图7.8。

7.8

原书图7.8,用两种不同的计数方法计算阴影-体积交叉点的二维侧面图。在z-pass体计数中,当射线通过阴影体的正面三角形时,计数递增;当射线通过背面三角形时,计数递减。对于点A,光线进入两个阴影区,加2,然后离开两个阴影区,净计数为0,所以点在光中。在z-fail体计数中,计数从表面以外开始(这些计数以斜体显示)。对于点B处的射线,z-pass方法通过两个正面三角形进行+2计数,而z-fail通过两个背面三角形进行相同计数。点C显示了必须为z-fail阴影体设置上限。射线从点C开始,首先击中一个正面三角形,得到-1。然后,它退出两个阴影体(通过它们的端部,这是此方法正常工作所必需的),净计数为+1。计数不是零,所以点在阴影中。这两种方法对所观察的表面上的所有点都给出相同的计数结果。

用射线做这件事很费时间。但是有一个更聪明的解决方案:模板缓冲区可以为我们计数。首先,清除模板缓冲区。其次,整个场景被绘制到framebuffer中,只使用不受光材质颜色,以获得颜色缓冲区中的这些阴影组件和z-buffer中的深度信息。第三,关闭z-buffer的更新和对颜色缓冲区的写入(尽管z-buffer 测试仍在进行),然后绘制阴影体的正面三角形。在此过程中,模板操作被设置为在绘制三角形的任何位置递增模板缓冲区中的值。第四步,使用模板缓冲区完成另一遍,这一次只绘制阴影体的背面三角形。对于此传递,当绘制三角形时,模板中的值递减。只有当渲染的阴影体表面的像素是可见的(即而不是被任何真实的几何图形所隐藏)。在这一点上,模板缓冲区保持每个像素的阴影状态。最后,再次渲染整个场景,这次只使用受光线影响的活性材料组件,并且只显示模板缓冲区中的值为0的地方。值为0表示射线从阴影中消失的次数与进入阴影体的次数相同,此位置被光线照亮。

这种计数方法是阴影体背后的基本思想。阴影体积算法生成的阴影示例如图7.9所示。有方法可以单遍实现该算法。但是,当物体穿透相机的近平面时,会发生计数问题。解决方案称为z-fail,涉及计算隐藏在可见表面后面而不是前面的交叉点。图7.8给出了这种选择的简要概述。

7.9

原书图7.9,阴影体,左边是角色投射的影子。右边是模型的拉伸三角形。

为每个三角形创建四边形会产生大量的透支。也就是说,每个三角形将创建三个必须渲染的四边形。一个球由一千个三角形组成的四边形有三千个,每一个四边形都可以跨越屏幕。一种解决方案是沿着物体的轮廓边缘只画那些四边形,例如,我们的球体可能只有50个轮廓边缘,所以只需要50个四边形。几何着色器可以用来自动生成这样的轮廓边缘。culling和clamping技术也可用于降低充填成本。

然而,阴影体算法仍然有一个可怕的缺点:极端的可变性。想象一个单独的小三角形。如果相机和光处于完全相同的位置,阴影体成本是最小的。形成的四边形不会覆盖任何像素,因为它们是视图的边缘。只有三角本身关系。假设观察者现在绕着三角形旋转,让它一直在视野中。当摄像机从光源移开时,阴影体的四边形将变得更加可见,并覆盖更多的屏幕,导致更多的计算发生。如果观看者恰好移动到三角形的阴影中,阴影体积将完全覆盖屏幕,与我们最初的视图相比,将花费相当多的时间进行评估。这种可变性使得阴影体在交互应用程序中不可用,而在交互应用程序中,一致的帧速率非常重要。与其他场景一样,朝光方向观看会导致算法成本出现巨大的、不可预测的跳跃。

由于这些原因,阴影体在很大程度上已被应用程序所放弃。然而,考虑到在GPU上访问数据的新方法和不同方法的不断发展,以及研究人员对这种功能的巧妙利用,阴影体可能有一天会重新被广泛使用。 例如,Sintorn等。 概述了阴影体算法,这些算法可提高效率并提出自己的分层加速结构。

下一个算法,阴影映射shadow mapping具有可预测的成本,非常适合GPU,因此构成了许多应用程序中阴影生成的基础。

4阴影映射

1978年,Williams提出了一个通用的基于z缓冲的渲染器可以用来快速生成任意物体上的阴影。这个想法是使用z缓冲渲染场景,从光源的位置投射阴影。凡是被光“看见”的都被照亮了,其余的都在阴影里。生成此图像只需要z缓冲,可以关闭灯光、纹理和将值写入颜色缓冲区。

z缓冲区中的每个像素现在都包含最接近光源的对象的z深度。我们称z缓冲区的全部内容为阴影映射shadow map,有时也称为阴影深度映射shadow depth map或阴影缓冲区shadow buffer。要使用shadow map,场景需要第二次渲染,但这次是相对于观察者而言的。在绘制每个图元时,将其在每个像素处的位置与shadow map进行比较。如果一个渲染点离光源的距离比shadow map中相应的值更远,那么这个点就是在阴影中,否则就不是。这种技术是通过使用纹理映射来实现的。见图7.10。shadow map是一个流行的算法,因为它是相对可预测的。构建阴影映射的成本与渲染的图元数量大致成线性关系,并且访问时间是恒定的。shadow map可以生成一次,并在光线和物体不动的场景中重用每一帧,例如用于计算机辅助设计。

7.10

原书图7.10,Shadow mapping。在左上角,阴影贴图是通过将深度存储到视图中的表面而形成的。右上角,从视线看两个点。在va点可以看到球体,这个点位于shadow map上的texel a。储存在那里的深度并不比va点到光的距离少很多,因此该点被照亮了。在vb点上与光的距离比在texel b上存储的深度要远得多,因此处于阴影中。在左下角是一个场景从光的角度看,白色是更远的。右下角是用这个阴影地图渲染的场景。

当产生一个单独的z缓冲时,光只能“看”一个特定的方向,就像照相机一样。对于像太阳这样的远距离定向光,光的视野被设置成包括所有物体投射的阴影到眼睛看到的可视范围内。光使用正交投影,它的视图需要在x和y中足够宽和足够高来查看这组对象。如果局部光线离投射阴影的物体足够远,一个单一的视锥截体可能就足以包含所有这些。如果光是聚光灯,它有一个自然的截锥体与它相关联,与它的截锥体之外的一切都被认为是没有照明的。

如果局部光源位于场景内部,并被阴影投射器包围,典型的解决方案是使用六视图立方体,类似于立方体环境映射。这些被称为全向阴影图omnidirectional shadow maps。全向映射的主要挑战是避免在两个独立映射相交的接缝处出现工件。King和Newhall深入分析问题并提供解决方案,Gerasimov提供了一些实现细节。Forsyth提出了一个通用的多截锥体分区方案,用于全向灯光,也提供了更多的阴影图分辨率需要的地方。Crytek根据每个视图的投影截头锥体的屏幕空间覆盖设置六个视图的分辨率,所有的图都存储在一个纹理图集中。

并不是场景中的所有对象都需要渲染到光的视图体中。首先,只需要渲染能够投射阴影的对象。例如,如果已知地面只能接收阴影而不能投射阴影,那么它就不必渲染到shadow map中。

根据定义,阴影投射器是在光的视锥中。可以通过多种方式增加或收紧此视锥,使我们可以安全地忽略某些阴影投射器。可见的阴影接收器在光线方向上的最大距离内。任何超过这个距离的东西都不能在可见的接收器上投下阴影。类似地,可见接收器的集合可能会比光原来的x和y视图范围更小。参见图7.11。另一个示例是,如果光源在眼睛的视锥内部,则在此视锥之外的任何物体都不能在接收器上投射阴影。只渲染相关的物体不仅可以节省渲染时间,还可以减小光的截锥体所需的尺寸,从而提高阴影贴图的有效分辨率,从而提高质量。另外,如果光截锥体的近平面离光越远越好,远平面离光越近越好。这样做可以提高z缓冲区的有效精度。

7.11

原书图7.11,左图光照视野包围视椎。在中间,光的远平面被拉进来,只包括可见的接收器,因此剔除三角形作为投射器;近平面也作了调整。右图,光的截锥体侧面用来约束可见的接收器,剔除了绿色的胶囊。

阴影映射的一个缺点是阴影的质量取决于阴影映射的分辨率(以像素为单位)和z-buffer的数值精度。由于阴影映射是在深度比较时采样的,因此该算法容易产生走样问题,特别是在物体之间的接触点附近。一个常见的问题是自阴影走样,通常被称为“surface acne”或“shadow acne”,其中一个三角形被错误地认为是阴影本身。这个问题有两个原因。一个是处理器精度限制。另一个原因是几何的,因为点样本的值被用来表示一个区域的深度。也就是说,为光线生成的样本几乎从不与屏幕样本位于相同的位置(例如,像素通常在其中心采样)。当光的存储深度值与被观察表面的深度相比较时,光的值可能略低于表面的深度值,从而导致自阴影。这些错误的影响如图7.12所示。

7.12

原书图7.12,shadow mapping的偏差走样(bias artifacts)。左图,偏差太低,所以会出现自阴影。右图,高偏差导致鞋子不产生接触阴影。阴影贴图的分辨率也很低,导致块状阴影。

帮助避免(不总是消除)各种阴影映射走样的一个常见方法是引入一个偏差因子(bias factor)。在检查阴影图中找到的距离与被测位置的距离时,从接收器的距离中减去一个小偏差。如图7.13。这种偏差可以是一个恒定的值,但是当接收器不是主要面对光线时,这样做可能会失败。一个更有效的方法是使用一个偏置,这个偏置与接收器对光的角度成比例。表面越偏离光线,偏差越大,从而避免这个问题。这种类型的偏差称为坡度比例偏差(slope scale bias)。这两种偏差都可以通过使用诸如OpenGL的glPolygonOffset之类的命令来将每个多边形移离光源。请注意,如果表面直接面对光,则坡度比例偏差根本不会将其向后偏移。因此,为了避免可能出现的精度误差,在坡度比例偏差的基础上使用了常数偏差。坡度比例尺偏差也经常在某一最大值处被限制,因为从光的角度看,当表面接近边缘时,切线值可能非常高。

7.13

原书图7.13,阴影偏移。这些表面被渲染为高架灯的阴影图,垂直线代表阴影图像素中心,在这些位置记录遮挡物深度。我们想知道表面是否在三个点上发光。最接近的阴影映射深度值显示为×相同的颜色。在左边,如果没有添加任何偏差,蓝色和橙色的样本将被错误地判定为处于阴影中,因为它们离光的距离比对应的阴影图深度更远。在中间,从每个样本中减去恒定的深度偏差,使每个样本都更靠近光线。蓝色样本仍被认为是在阴影中,因为它不比被测试的阴影贴图深度更接近光线。在右边,阴影映射是通过将每个多边形从光的方向按其斜率比例移动而形成的。所有的样本深度现在都比阴影贴图深度更近,所以都被点亮了。

Holbert引入了法线偏移偏置,它首先使接收器的世界空间位置沿表面法线方向稍微偏移一点,与光方向和几何法线之间的角度的正弦成比例。参见图7.24。这不仅改变了深度,还改变了在阴影图上测试样本的x和y坐标。当光的角度变得比表面更浅时,这个偏移量就会增加,采样点离表面足够远以避免自阴影。这种方法可以被想象成把采样点移到接收器上方的一个虚拟表面上。这个偏移量是一个世界空间距离,所以Pettineo建议按阴影图的深度范围缩放它。Pesce提出了一种沿着摄像机视角方向的偏置,这种偏置也可以通过调整阴影图的坐标来实现。其他的偏置方法在7.5节中讨论,因为阴影法也需要测试几个相邻的样本。

过多的偏置会导致光泄露light leaks或 Peter Panning,即物体看起来浮在下表面上。这种假象的产生是因为物体接触点以下的区域,例如,一英尺下的地面,被向前推得太远,所以不会受到阴影。

避免自遮蔽问题的一种方法是仅将背面渲染为阴影贴图。这种被称为第二深度阴影映射(second-depth shadow mapping)的方案在很多情况下都能很好地工作,特别是在不允许手工调整偏移的渲染系统中。当对象是双面的、薄的或相互接触时,就会出现问题。如果一个物体是一个模型,网格的两边都是可见的,例如,一个棕榈叶或一张纸,自阴影可以发生,因为背面和正面在同一个位置。同样,如果不执行任何偏置操作,则轮廓边缘或薄物体附近可能会出现问题,因为在这些区域中,背面接近正面。增加一个偏置可以帮助避免表面问题,但是该方案更容易light leaking,因为接收器和投影器背面在接触点之间没有分隔。参见图7.14。选择哪种方案取决于具体情况。例如,Sousa等人发现使用正面作为太阳阴影,使用背面作为室内灯光最适合他们的应用。

7.14

原书图7.14,高处光源的阴影贴图表面。在左边,面向光的表面,用红色标记,被发送到阴影图。表面可能被错误地确定为阴影,所以需要远离光线。在中间,仅将背面三角形渲染到阴影贴图中。向下推动这些遮光器的偏置可能会使光泄漏到位置a附近的地面上;向前偏斜会导致阴影标记为b的轮廓边界附近的照明位置被视为阴影。在右边,在阴影图上每个位置的最近的正面和背面三角形之间的中点形成一个中间面。光泄漏可能发生在点c附近(这也可能发生在第二深度阴影映射中),因为最近的阴影映射样本可能位于该位置左侧的中间表面,因此该点将更接近于光。

注意,对于阴影映射,对象必须是“watertight”(流形和封闭,即固体;第16.3.3节),或者必须同时渲染地图的正面和背面,否则该物体不能完全投射阴影。Woo提出了一种通用的方法,字面上来说,它试图在仅仅使用正面和背面之间找到一个折中的方法。这个想法是把实体物体渲染到阴影地图上,并踪离光最近的两个表面。这个过程可以通过深度剔除或其他透明相关技术来完成。两个对象之间的平均深度形成一个中间层,其深度用作阴影映射,有时称为对偶阴影映射dual shadow map。如果物体足够厚,自阴影和光泄漏的影响就会最小化。Bavoil等人讨论了处理潜在工件的方法,以及其他实现细节。主要的缺点是使用两个阴影映射相关联的额外成本。迈尔斯讨论了一个艺术家控制的深度层之间的遮光和接收器。

随着观察者的移动,光的可视体积通常会随着阴影投射器的设置而改变大小。这些变化反过来又导致阴影在帧与帧之间轻微移动。这是因为光的阴影图是从光的不同方向采样的,而这些方向与前一组方向不一致。对于定向光,解决方案是强制每个后续生成的阴影地图在世界空间中保持相同的相对texel光束位置。也就是说,您可以将阴影映射看作是在整个世界上施加一个二维网格参考框架,每个网格单元表示地图上的一个像素样本。移动时,将为这些相同网格单元的不同集合生成阴影映射。换句话说,光的视图投射被强制到这个网格上以保持帧与帧之间的一致性。

4.1分辨率增强

类似于纹理的使用,理想情况下,我们希望一个阴影贴图纹理覆盖一个图像像素。如果我们有一个光源位于与眼睛相同的位置,那么阴影映射就可以完美地与屏幕空间像素一一对应(并且没有可见的阴影,因为光线可以精确地照亮眼睛看到的东西)。当光的方向改变时,每像素的比例就会改变,这会造成工件。图7.15显示了一个示例,这种不匹配称为透视混叠。如果表面接近光线,但面向观察者,则单个阴影贴图纹理像素也可以覆盖许多像素。这个问题被称为投影混叠。参见图7.16。块度可以通过增加阴影贴图的分辨率来降低,但这是以增加内存和处理为代价的。

7.15

原书图7.15,左边的图像是使用标准阴影映射创建的;右边的图像使用LiSPSM。每个阴影贴图的纹理投影被显示出来。这两张阴影图有相同的分辨率,不同之处在于LiSPSM改变了光的矩阵,在靠近观察者的地方提供了更高的采样率。

7.16

原书图7.16,左边的灯几乎在头顶上。 由于与眼睛的视图相比分辨率较低,因此阴影的边缘有点参差不齐。 在右侧,光线接近地平线,因此每个阴影纹素水平覆盖了更大的屏幕区域,因此产生了更多的锯齿状边缘。

还有另一种方法来创建光的采样模式,使其更接近相机的模式。这是通过改变场景向光投射的方式来实现的。通常我们认为视图是对称的,视向量在截锥体的中心。但是,视图方向仅仅定义了一个视图平面,而没有定义采样的像素。定义截体的窗口可以在这个平面上移动、倾斜或旋转,创建一个四边形,以提供不同的世界映射来查看空间。因为这是线性变换矩阵的本质和GPU的使用,所以四边形仍然是按一定的间隔采样的。采样率可以通过改变光的观察方向和观察窗口的边界来改变。参见图7.17。

7.17

原书图7.17,对于顶灯,左边地板上的采样与眼睛的频率不匹配。通过改变光线的观察方向和右侧的投影窗口,采样率会偏向于在眼睛附近产生更高密度的纹理。

将光线的视线映射到眼睛的视线有22个自由度。对这个解决方案空间的探索导致了几种不同的算法,它们试图更好地将光的采样率与眼睛的匹配起来。方法包括透视阴影映射(perspective shadow maps PSM)、梯形阴影映射(trapezoidal shadow maps TSM)和光空间透视阴影映射(light space perspective shadow maps LiSPSM)。有关示例,请参见图7.15和图7.26。此类中的技术称为透视变形perspective warping方法。

这些矩阵扭曲算法的一个优点是,除了修改光的矩阵外,不需要额外的工作。每种方法都有自己的优缺点,因为每种方法都可以帮助匹配某些几何图形和照明情况下的采样率,但使其他情况下的采样率更糟。Lloyd等人分析了PSM、TSM和LiSPSM之间的等价关系,对这些方法的采样和混叠问题进行了很好的概述。当光的方向垂直于视图的方向(例如,在头顶上)时,这些方案的效果最好,因为随后可以改变视角变换,使更多的样本更靠近眼睛。

矩阵变形技术无法提供帮助的一种照明情况是,当光线在相机前面并指向相机时。 这种情况被称为dueling frusta,或更通俗地说是“deer in the headlights”。靠近眼睛需要更多阴影贴图样本,但是线性扭曲只会使情况变得更糟。 诸如此类的问题和其他问题,例如质量的突然变化以及相机移动过程中产生的阴影不稳定的质量,使这些方法失去作用。

在查看器所在位置添加更多样本的想法是一个很好的想法,从而产生了为给定视图生成多个阴影贴图的算法。当卡马克在2004年Quakecon的主题演讲中描述这个想法时,这个想法第一次产生了明显的影响。Blow独立实现了这样一个系统。这个想法很简单:生成一组固定的阴影图(可能是不同分辨率的),覆盖场景的不同区域。在Blow的方案中,四个阴影图嵌套在查看器周围。这样,附近的物体就可以得到高分辨率的图,而远处物体的分辨率就会下降。Forsyth提出了一个相关的想法,为不同的可视对象集生成不同的阴影贴图。在他的设置中避免了如何处理跨越两个阴影贴图边界的对象的转换问题,因为每个对象都有且只有一个与之关联的阴影贴图。Flagship Studios开发了一个融合了这两种理念的系统。一个阴影贴图用于附近的动态对象,另一个用于查看器附近静态对象的网格部分,第三个用于整个场景中的静态对象。第一个阴影贴图每一帧都生成。另外两个只能生成一次,因为光源和几何图形是静态的。尽管所有这些特定的系统现在都相当古老,但不同对象和情况的多映射思想(有些是预先计算的,有些是动态的)是此后发展的算法中的一个共同主题。

2006年Engel、Lloyd等、Zhang等分别独立研究了相同的基本思想。这个想法是通过平行于视图方向的切片将视图截体的体积分割成几个部分。见图7.18。随着深度的增加,每一体积的深度大约是前一体积深度的两到三倍。对于每个视图体,光源可以形成一个紧密的截锥体,然后生成一个阴影贴图。通过使用纹理地图集或阵列,不同的阴影贴图可以被视为一个大的纹理对象,从而最小化缓存访问延迟。所获得的质量改进的比较如图7.19所示。Engel将这种算法命名为级联阴影映射(cascaded shadow maps,CSM),它比平行分割阴影映射(parallel-split shadow maps)更常用,但两者都出现在文献中,而且实际上是相同的。

7.18

原书图7.18,左图,从眼睛看到的视台被分成四个部分。右图,为这些体积创建了边界框,该边界框确定了定向光的四个阴影贴图中的每一个渲染的体积。

7.19

原书图7.19,左图,场景的可视范围广,导致单个2048x2048分辨率的阴影贴图具有透视混叠。右图,沿视图轴放置的四个1024x1024阴影贴图可显着提高质量。插入的红色框中显示了围栏前角的缩放。

该算法实现简单,能够覆盖较大的场景区域,结果合理,鲁棒性强。对dueling frusta问题可以通过在更靠近眼睛的地方以更高的速率采样来解决,并且不存在严重的最坏情况问题。由于这些优点,许多应用使用了级联阴影映射。

虽然可以使用透视变形将更多的样本打包到单个阴影图的细分区域,但通常的做法是为每个级联使用单独的阴影图 。 如图7.18、图7.20所示,从查看者的角度来看,每个地图所覆盖的区域可能不同。靠近阴影贴图的视图体积较小,可以在需要的地方提供更多样本。 确定z-depth的范围如何在映射之间划分(称为z-partitioning),可能非常简单或复杂 。一种方法是对数分割,即对每个级联图使远平面距离与近平面距离之比相同 :

e7.5

其中f和n是整个场景的远近平面,c是图的数量,r是最终的比例 。例如,如果场景中最近的物体是1米远,最大距离是1000米,我们有三个级联地图,那么$r=\sqrt[3]{1000/1}=10$。距离最近的视图的远近平面距离是1和10,下一个间隔是10到100来保持这个比例,最后一个间隔是100到1000米。初始深度对这种划分有很大影响。如果近深度只有0.1米,那么10000的立方根是21.54,这是一个相当高的比例,例如:0.1到2.154到46.42到1000。这意味着生成的每个阴影映射必须覆盖更大的区域,从而降低其精度。在实践中,这样的划分为接近平面的区域提供了相当高的分辨率,如果该区域内没有对象,则会浪费这种分辨率。避免这种不匹配的一种方法是将分区距离设置为对数分布和等距分布的加权混合,但是如果我们能够确定场景的近距离视图边界,那就更好了 。

7.20

原书图7.20,阴影级联可视化。紫色、绿色、黄色和红色是代表从最近到最远的级联。

挑战在于如何设置近平面。如果设置得离眼睛太远,物体可能会被这个平面裁剪,这是一个非常糟糕的工件。对于过场动画,艺术家可以事先精确地设置这个值,但是对于交互式环境,这个问题更具挑战性。Lauritzen等人提出了样本分布阴影映射(sample distribution shadow maps SDSM),它使用前一帧的z深度值通过两种方法之一确定更好的分区。

第一种方法是通过z-depth查找最小值和最大值并使用这些值设置远近平面。这是通过在GPU上执行所谓的reduce操作来执行的,其中由GPU计算或其他着色器分析一系列越来越小的缓冲区,并将输出缓冲区作为输入反馈,直到剩下1×1个缓冲区为止。通常情况下,这些值会根据场景中物体移动的速度进行调整。除非采取纠正措施,否则从屏幕边缘进入的附近物体仍可能给画面带来问题,不过很快就会在下一帧中得到纠正。

第二种方法还分析深度缓冲区的值,制作一个称为直方图的图表,该图表记录z深度沿范围的分布。除了查找紧密的近平面和远平面之外,在根本没有对象的位置,图形中可能会有空隙。通常添加到该区域的任何分区平面都可以捕捉到实际存在对象的位置,从而为级联贴图集提供更高的z深度精度。

在实践中,第一种方法具有通用性,速度快(通常在每帧1 ms的范围内),并且给出了良好的结果,因此它已经被多个应用程序。参见图7.21。

7.21

原书图7.21,深度界限的效果。左图,没有使用特殊处理来调整远近平面。右图,使用SDSM寻找更紧密的界限。注意每个图像左边缘附近的窗户框,二楼花下面的区域,以及一楼的窗户,由于视图边界不严格而导致的欠采样会导致工件。指数阴影映射用于渲染这些特定的图像,但是提高深度精度的思想对于所有阴影映射技术都是有用的。

与单个阴影贴图一样,由于光样本逐帧移动而造成的闪烁工件是一个问题,并且随着对象在级联之间移动而变得更糟。可以使用多种方法来维护世界空间中的稳定采样点,每种方法都有其自身的优势。当对象跨越两个阴影贴图之间的边界时,阴影质量可能会突然发生变化。一种解决方案是使视图体积稍微重叠。在这些重叠区域中获取的样本从两个相邻的阴影贴图收集结果并进行混合。或者,可以通过使用抖动在该区域中采集单个样本。

由于这种方法的普及,已经投入了大量的努力来提高它的效率和质量。如果阴影图的截锥体内没有任何变化,则不需要重新计算阴影图。对于每一盏灯,阴影投射器的列表可以通过确定哪些物体对光线可见,哪些物体会对接收器产生阴影来预先计算。由于判断阴影是否正确相当困难,可以采取一些适用于级联和其他算法的捷径。一种技术是使用低细节模型作为代理来实际投射阴影。另一种方法是从考虑物体中去除微小的遮挡物。理论上,距离较远的阴影贴图的更新频率可能比一帧更新一次的频率低,因为这样的阴影不太重要。这种想法存在由大型移动对象引起的工件风险,因此需要谨慎使用。Day提出了从一帧到另一帧的“滚动”远程图的想法,这个想法是每个静态阴影图的大部分是可重复使用的,只有边缘可能会改变,因此需要渲染。DOOM(2016)等游戏维护着大量的阴影图集,仅重新生成物体移动的阴影图集。更远的级联映射可以被设置为完全忽略动态对象,因为这样的阴影可能对场景贡献很少。在某些环境中,可以使用高分辨率的静态阴影映射来代替这些较远的级联,这可以显著地减少工作负载。一个稀疏的纹理系统(章节19.10.1)可以用于一个巨大世界的单一静态阴影图。级联阴影映射可以与烘焙光映射纹理或其他更适合特定情况的阴影技术相结合。Valient的报告值得注意,因为它描述了适用于各种视频游戏的不同阴影系统定制和技术。第11.5.1节详细讨论了预先计算的光影算法。

创建几个单独的阴影贴图意味着要遍历每个几何图形。在通过一次pass将遮挡物渲染到一组阴影贴图的想法基础上,已经建立了许多提高效率的方法。几何着色器可用于复制对象数据并将其发送到多个视图。实例化的几何着色器允许将对象输出到多达32个深度纹理。多视口扩展可以执行诸如将对象渲染到特定纹理阵列切片的操作。第21.3.1节在它们用于虚拟现实的背景下更详细地讨论了这些。视口共享技术的可能缺点是,必须将生成的所有阴影贴图的遮挡物沿管道发送,而不是与每个阴影贴图相关的集合。

在实时渲染中,如果所有光源始终处于活动状态,则具有多个光源的大型场景可能会被计算淹没。如果一个空间的体积在视锥截体内,但肉眼不可见,则不需要评估遮挡该接收器的对象。Bittner等人使用遮挡剔除(章节19.7)从眼睛到所有可见的阴影接收器,然后从光的角度把所有潜在的阴影接收器渲染到一个模板缓冲遮罩。这个遮罩编码了从光中可以看到的可见阴影接收器。为了生成阴影图,他们使用遮挡剔除来渲染来自光的物体,并使用遮罩来剔除没有接收器的物体。各种淘汰策略也适用于灯光。由于辐照度与距离的平方成正比,一种常用的技术是在一定的阈值距离后剔除光源。例如,第19.5节中的门户剔除技术可以发现哪些光影响哪些单元。这是一个活跃的研究领域,因为性能收益是相当可观的。

5百分比近似滤波

一个简单的扩展阴影映射技术可以提供伪软阴影。这种方法还可以帮助改善当单个光样本单元覆盖多个屏幕像素时,会导致阴影看起来不均匀的分辨率问题。解决方案类似于纹理放大(第6.2.1节)。而不是从阴影地图中提取单个样本,而是检索最近的四个样本。这项技术并没有在深度之间进行插值,而是通过与表面深度的比较得出结果。也就是说,表面的深度分别与四个texel深度进行比较,然后确定每个阴影图样本的点处于光或阴影中。然后,对这些结果(即阴影的0和光的1)进行双线性插值,以计算实际上光对表面位置有多少贡献。这种过滤的结果是一个人为的软阴影。这些半边影会根据阴影地图的分辨率、摄像机的位置和其他因素而变化。例如,分辨率越高,软化边缘的范围就越窄。尽管如此,一点点的半影和平滑总比没有好。

这种从阴影图中检索多个样本并混合结果的思想称为百分比近似滤波(percentage-closer filtering PCF)。区域灯光产生柔和的阴影。到达表面某一位置的光量是该位置可见光面积比例的函数。PCF试图通过反转这一过程来近似点状(或定向)光的柔和阴影。它不是从一个表面位置找到光的可见区域,而是从一组接近原始位置的表面位置找到点状光的可见性。参见图7.22。“百分比近似滤波”指的是最终的目标,即找出在光照下可见的样本的百分比。这个百分比是用来给表面遮光的。

7.22

原书图7.22,在左边,来自区域光源的棕色线显示了半影的形成。对于接收器上的一个点p,通过测试区域光表面上的一组点,并找出没有被任何遮挡器遮挡的点,就可以计算出接收到的照明量。在右边,点光源不会投射半影。PCF通过反转过程来近似一个区域光的效果:在一个给定的位置,它在阴影图上一个可比较的区域上采样,以获得被照样本的百分比。红色椭圆表示阴影图上采样的区域。理想情况下,这个圆盘的宽度与接收器和遮光器之间的距离成正比。

在PCF中,位置是在一个表面位置附近生成的,深度大致相同,但在阴影图上的不同texel位置。每个位置的可见性被检查,这些结果的布尔值,点亮或未点亮,然后混合得到一个柔和的阴影。注意这个过程是非物理的:这个过程不是直接对光源进行采样,而是依赖于对表面本身进行采样。到遮挡者的距离不影响结果,所以阴影有相似大小的半影。尽管如此,这种方法在许多情况下提供了一个合理的近似值。

一旦被采样区域的宽度被确定,重要的是采样的方式要避免混叠工件。如何采样和过滤附近的阴影图位置有很多变化。变量包括采样区域的宽度、使用多少个样本、采样模式以及如何对结果进行加权。在api能力较差的情况下,可以通过一种特殊的纹理采样模式来加速采样过程,这种模式类似于双线性插值,它访问四个相邻的位置。不是混合结果,而是将四个样本中的每个与给定的值进行比较,然后返回通过测试的比率。然而,在规则的网格模式中执行最近邻居抽样可能会产生明显的工件。使用联合的双边过滤器,模糊的结果,但尊重对象的边缘可以提高质量,同时避免阴影泄漏到其他表面。有关此过滤技术的更多信息,请参见第12.1.1节

DirectX 10为PCF引入了单指令双线性化支持,使结果更加平滑。与最近邻采样相比,这种方法在视觉上有了很大的改进,但是常规采样中的工件仍然是一个问题。最小化网格模式的一个解决方案是使用预先计算的泊松分布模式对一个区域进行采样,如图7.23所示。这种分布将样本分散开来,使得它们既不在彼此附近,也没有固定的模式。众所周知,对于每个像素使用相同的采样位置,不管其分布如何,都可能产生模式图案。这种工件可以通过绕其中心随机旋转样本分布来避免,从而将混叠转化为噪声。Castano发现泊松采样产生的噪音在平滑、程式化的内容中特别明显。提出了一种基于双线性抽样的高效高斯加权抽样方案。

7.23

原书图7.23,最左边显示了4×4网格模式的PCF抽样,使用的是最近邻抽样。最右边显示了一个磁盘上的12次采样的泊松采样模式。使用此模式对阴影图进行采样,虽然仍然可以看到工件,但在第二张图得到了改进的结果。第三张图,采样图案围绕其中心随机地从一个像素旋转到另一个像素。结构化的阴影工件变成(不那么讨厌的)噪声。

自阴影问题和漏光(例如瑕疵和Peter Panning)在PCF中可能变得更糟。坡度比例尺偏差仅仅根据它与光线的角度将表面推离光线,假设一个样本在阴影地图上的距离不超过一个texel。通过在更大范围内从一个表面的单一位置采样,一些测试样本可能会被真实的表面所阻挡。

一些不同的附加偏置因素已经被发明出来,并在一定程度上成功地用于减少自阴影的风险。Burley描述了偏锥bias cone,其中每个样本朝向光的移动都与其与原始样本的距离成正比。Burley建议斜率为2.0,并保持一个小的恒定偏差。参见图7.24。

7.24

原书图7.24,附加阴影偏置法。对于PCF,在原始样本位置(五个点的中心)周围取几个样本。所有这些样品都应该被点亮。在左图中,一个偏置圆锥形成,样品被移到它上面。圆锥体的陡度可以增加,把右边的样品拉到足够近的地方,让它们被照亮,这样做的风险是,其他地方的样品(没有显示出来)的光泄漏会增加,而这些样品确实被遮挡了。在中间的图中,所有的样本都调整到位于接收平面上。这对凸面效果很好,但在凹处可能适得其反,如左侧所示。在右图中,法向偏移偏置使样品沿着表面的法向移动,与法向和光之间夹角的正弦成比例。对于中心样本,这可以被认为是移动到原始表面之上的一个假想表面。这种偏差不仅影响深度,还会改变用于测试阴影地图的纹理坐标。

schuler、Isidoro和Tuft提出了基于观察的技术,即接收器本身的斜率应该用来调整其余样本的深度。其中Tuft的公式最容易应用于级联阴影图。Dou等人进一步细化和扩展了这个概念,考虑了z-depth如何以非线性方式变化。这些方法假设附近的样本位置在三角形构成的同一平面上。这种技术被称为接收平面深度偏差(receiver plane depth bias)或其他类似术语,在许多情况下可以非常精确,因为这个假想平面上的位置确实位于表面上,或者如果模型是凸的,则位于它的前面。如图7.24所示,孔洞附近的样本可以被隐藏。常数、坡度比例尺、接收面、视场偏差和正常偏移的组合已经被用来解决自阴影的问题,尽管手动调整每个环境仍然是必要的。

PCF的一个问题是,由于采样区域的宽度保持不变,阴影会呈现出均匀的柔和,所有的阴影都具有相同的半阴影宽度。在某些情况下,这是可以接受的,但在遮挡器和接收器之间有地面接触的地方,这就不正确了。见图7.25。

7.25

原书图7.25,百分比近似滤波和百分比近似软阴影。左图,硬阴影有一点PCF过滤。中间,等宽柔影。右图,物体与地面接触的地方,具有适当硬度的变宽软阴影。

6百分比近似软阴影

2005年,Fernando发表了一种颇具影响力的方法,叫做百分比近似软阴影percentage-closer soft shadows (PCSS)。它试图通过在阴影图上搜索附近的区域来找到所有可能的遮挡物。这些遮挡体到位置的平均距离用来确定样本区域宽度:

e7.6

其中$d_r$为接收器与光的距离,$d_o$为平均遮挡距离。换句话说,当平均遮挡者离接收器越远,离光越近时,样品的表面积宽度就越大。图7.2、7.25和7.26给出了示例。

如果没有发现遮挡物,则位置是完全亮的,不需要进一步处理。类似地,如果位置完全闭塞,处理可以结束。否则,将对感兴趣的区域进行采样,并计算光的近似贡献。为了节省加工成本,样品区的宽度可以用来改变取样的数量。其他的技术也可以实现,例如,使用较低的采样率来处理不太重要的远距离软阴影。

这种方法的一个缺点是,它需要采样阴影图的一个公平大小的区域来找到遮挡者。使用旋转的泊松模式可以帮助隐藏采样不足的工件。Jimenez指出,泊松采样在运动状态下可能是不稳定的,而使用介于抖动和随机之间的函数所形成的螺旋模式在帧与帧之间可以得到更好的结果。

Sikachev等人详细讨论了使用SM 5.0中的特性更快地实现PCSS,该特性由AMD引入,通常称为接触硬化阴影(contact hardening shadows CHS)。这个新版本还解决了基本PCSS的另一个问题:半影的大小是由阴影图的分辨率决定的。见图7.25。通过首先生成影子映射的mipmaps,然后选择最接近用户定义的世界空间内核大小的mip级别,可以最小化这个问题。采样8×8个区域以找到平均块深度,只需要16 GatherRed()纹理调用。一旦找到半影估计,高分辨率的mip水平用于阴影的锐区,而低分辨率的mip水平用于较软的区域。

CHS已经在大量的视频游戏中被使用,研究还在继续。例如,Buades等人提出了可分离软阴影映射(separable soft shadow mapping SSSM),该方法将采样网格的PCSS过程分解为可分离的部分,并尽可能在像素之间重用元素。

对于每像素需要多个样本的加速算法,有一个概念被证明是有帮助的,那就是分层的最小/最大阴影图。虽然阴影图深度通常不能取平均值,但是每个mipmap级别的最小值和最大值是有用的。也就是说,可以形成两个mipmap,一个保存每个区域中最大的z- depth(有时称为HiZ),另一个保存最小的z- depth。给定texel的位置、深度和要采样的区域,可以使用mipmaps快速确定完全光照和完全阴影的条件。例如,如果texel的z-depth大于为mipmap相应区域存储的最大z-depth,那么texel必须位于阴影中,不需要进一步的样本。这种类型的阴影图使得确定光可见性的任务更加有效。

像PCF这样的方法是通过采样附近的接收器位置来工作的。PCSS的工作原理是找到附近投射器的平均深度。这些算法不直接考虑光源的面积,而是对附近的表面进行采样,并受阴影贴图分辨率的影响。PCSS背后的一个主要假设是,平均阻挡是对半影大小的合理估计。当两个遮挡器,比如一盏路灯和一座远山,在一个像素处遮挡了部分相同的表面时,这种假设就被打破了,并可能导致工件。理想情况下,我们要确定从单个接收器位置可见多少光源。一些研究人员已使用GPU探索了背投。这个想法是把每个接收器的位置当作一个视点,把区域光源当作视点平面的一部分,并把遮挡器投射到这个平面上。Schwarz和Stamminger以及Guennebaud等人总结了之前的工作并提出了自己的改进。Bavoil等人采取了不同的方法,使用深度剥离创建一个多层阴影地图。反投影算法可以得到很好的结果,但是每像素的高成本(到目前为止)导致它们还没有被应用到交互式应用中。

7过滤后的阴影图

Donnelly和Lauritzen的方差阴影图(variance shadow map VSM)算法允许对生成的阴影图进行过滤。该算法在一张图中存储深度,在另一张图中存储深度的平方。在生成映图时可以使用MSAA或其他抗锯齿方案。这些图可以被模糊,被细化mipmap,放在求和区域表,或任何其他方法。将这些图视为可过滤纹理的能力是一个巨大的优势,因为当从它们检索数据时,整个采样和过滤技术阵列都可以发挥作用。

首先,对于VSM,深度图在接收器的位置采样(仅一次),以返回最近的光遮挡器的平均深度。当这个平均深度M1,称为一阶矩,大于阴影接收器t上的深度时,接收器完全处于光中。当平均深度小于接收器深度时,采用下式:

e7.7

$p_{max}$是光照下样品的最大百分比,$\sigma^2$是方差,t是接收深度,M1是阴影图的平均期望深度。深度平方阴影图的样本M2,称为二阶矩,用于计算方差:$\sigma^2=M_2-M_1^2\tag{7.8}$

$p_{max}$值是接收器可见性百分比的上限。实际的光照百分比p不能大于这个值。这个上界来自于Chebyshev不等式的单边变量。该方程试图利用概率论来估计在地表位置上遮挡物的分布有多少超出了地表离光的距离。Donnelly和Lauritzen证明了对于固定深度的平面遮挡器和平面接收器$p=p_{max}$,因此,方程7.7可以作为许多真实阴影情况的一个很好的近似。

Myers建立了一个推断关于为什么这个方法有效。面积的方差在阴影边缘处增大。深度差越大,方差越大。$(t-M_1)^2$是能见度百分比的一个重要决定因素。如果该值略高于零,这意味着平均遮挡深度比接收器更接近于光,而pmax则接近于1(完全照明)。这将会发生在半暗区完全被照亮的边缘。进入半影,平均遮挡深度更接近光,所以这一项变大,pmax下降。与此同时,方差本身在半影内也在变化,从近于零的沿边缘变化到最大的方差,即遮挡者在较深的地方均匀地共享面积。这些项相互平衡,在半影上形成线性变化的阴影。与其他算法的比较见图7.26。

7.26

原书图7.26,左上是标准阴影映射。右上是透视阴影映射,增加阴影贴图的纹理密度。左下,百分比近似软阴影,随着遮挡器与接收器的距离增加,软化阴影。右下,方差阴影映射与一个恒定的软阴影宽度,每个像素用单个方差映射样本着色。

方差阴影映射的一个重要特点是,它可以优雅地处理由几何引起的表面偏差问题。Lauritzen给出了如何利用表面的斜率来修正第二弯矩值的推导。偏差和其他数值稳定性问题可以是一个方差映射问题。例如,方程7.8用另一个相似的值减去一个大的值。这种类型的计算往往放大了底层数字表示的准确性不足。使用浮点纹理有助于避免这个问题。

总体而言,由于GPU优化的纹理功能得到了有效利用,VSM在处理时间上有了显著的提高。PCF需要更多的样本,因此需要更多的时间,以避免产生更柔和的阴影时的噪音,VSM可以只使用一个高质量的样本来确定整个区域的效果,并产生一个平滑的半影。这种能力意味着阴影可以在算法的限制范围内,以不增加成本的方式任意柔化。

与PCF一样,滤波核的宽度决定了半影的宽度。通过找到接收器和最近的遮挡物之间的距离,可以改变内核宽度,从而产生令人信服的柔和阴影。 Mipmapped样本对于宽度缓慢增加的半影的覆盖率估计不佳,从而产生了盒状工件。Lauritzen详细介绍了如何使用和区表来提供更好的阴影效果。图7.27显示了一个示例。

7.27

原书图7.27,方差阴影映射,其中到光源的距离从左到右增加。

当两个或多个遮挡物覆盖接收器且一个遮挡物靠近接收器时,沿半影区域的一个位置方差阴影映射将被破坏。来自概率理论的Chebyshev不等式将产生与正确的光照百分比无关的最大光照值。最接近的遮挡者,通过只部分遮挡光线,得出方程的近似值。这导致光泄漏,完全封闭的区域仍能接收到光。参见图7.28。通过在更小的区域内取更多的样本,可以解决这个问题,将方差阴影映射转换为PCF的一种形式。与PCF一样,速度和性能都有所不同,但对于阴影深度较低的场景,方差映射可以很好地工作。Lauritzen提出了一种艺术家控制的方法来改善这个问题,即将低的百分比视为完全隐藏的,并将其余的百分比范围重新映射为0%到100%。这种方法使光泄露减弱,以整体缩小半暗带为代价。虽然光泄露是一个严重的限制,VSM依然是很好的产生阴影的算法,因为这种阴影很少涉及多个遮挡物。

7.28

原书图7.28,在左边,方差阴影图应用到一个茶壶。在右边,一个三角形(没有显示)在茶壶上投下阴影,在地面的阴影中造成了令人讨厌的工件。

能够使用滤波技术快速生成平滑阴影的希望在滤波阴影映射中产生了很大的兴趣;主要的挑战是解决各种光泄露问题。Annen等人介绍了卷积阴影映射convolution shadow map。扩展了Soler和Sillion针对平面接收器的算法的思想,该思想是在傅立叶展开中对阴影深度进行编码。与方差阴影映射一样,可以对此类映射进行过滤。该方法收敛于正确答案,从而减少了漏光问题。

卷积阴影映射的一个缺点是需要计算和访问几个项,这大大增加了执行和存储成本。Salvi和Annen等人同时独立地提出了使用一个基于指数函数的术语。这种方法称为指数阴影映射(exponential shadow map ESM)或指数方差阴影映射(exponential variance shadow map EVSM),它将深度的指数及其二阶矩保存到两个缓冲区中。指数函数更接近于阴影映射执行的阶跃函数(在光照下或不光照下),因此这可以显著减少光泄露工件。它避免了卷积阴影映射的另一个问题,称为振铃ringing,在特定深度会发生轻微的光泄漏,刚好超过原始遮挡器的深度。

存储指数值的一个限制是,二阶矩值可能变得非常大,因此使用浮点数会超出范围。为了提高精度,并允许指数函数下降得更陡峭,可以生成z-depth,使它们是线性的。

指数阴影映射法由于其相对于VSM的质量得到了改善,并且与卷积映射相比具有更低的存储空间和更好的性能,因此在三种测试方法中引起了极大的兴趣。Pettineo还提到了其他一些改进,比如使用MSAA改进结果和获得有限透明度的能力,并描述了如何使用计算着色器改进过滤性能。

最近,Peters和Klein提出了矩影映射moment shadow mapping。它提供了更好的质量,尽管以使用4个或更多的时间为代价,增加了存储成本。使用16位整数来存储磁矩可以降低这一成本。Pettineo实现了这种新方法,并将其与ESM进行了比较,提供了一个探索许多变体的代码基础。

级联阴影映射技术可以应用于过滤后的图,以提高精度。与标准级联映射相比,级联ESM的一个优点是可以为所有级联设置一个偏差因子。Chen和Tatarchuk详细讨论了级联ESM遇到的各种光泄漏问题和其他工件,并给出了一些解决方案。

可以将过滤后的图视为PCF的一种廉价形式,只需少量样本即可。像PCF一样,此类阴影具有恒定的宽度。这些过滤的方法都可以与PCSS结合使用,以提供可变宽度的半影。矩阴影贴图的扩展还包括提供光散射和透明效果的功能。

8体积阴影技术

透明物体会衰减并改变光的颜色。对于某些透明对象集,可以使用与第5.5节中讨论的类似的技术来模拟这种效果。例如,在某些情况下,可以生成第二种类型的阴影映射。将透明对象渲染给它,并存储最近的深度和颜色或alpha覆盖。如果接收器没有被不透明的阴影图所阻挡,那么透明深度图将被测试,如果被遮挡,则根据需要检索颜色或覆盖范围。这个想法让人联想到7.2节中的光影投射,存储的深度避免在透明物体和光之间投射到接收器上。这种技术不能应用于透明对象本身。

自阴影对于头发和云彩等物体的真实感渲染至关重要,因为这些物体要么很小,要么是半透明的。单深度阴影图在这种情况下是行不通的。Lokovic和Veach首先提出了深度阴影图的概念,在这个概念中,每个阴影图的texel都存储了光线随深度下降的函数。这个函数通常由一系列不同深度的样本来近似,每个样本都有一个不透明度值。在图的两个样本中,一个给定位置的深度被用来确定阴影的效果。GPU面临的挑战是如何高效地生成和评估这些函数。这些算法使用类似的方法,并且遇到一些与顺序无关的透明性算法(第5.5节)所遇到的类似挑战,例如紧凑存储忠实表示每个功能所需的数据。

Kim和Neumann率先提出了基于GPU的方法,他们将其称为不透明度阴影映射opacity shadow maps。仅存储不透明度是在一组固定的深度下生成的图。Nguyen和Donnelly给出了这种方法的更新版本,生成了如图17.2所示的图像。然而,由于深度切片都是平行且均匀的,因此需要大量的切片来隐藏由于线性插值造成的切片之间的不透明工件。Yuksel和Keyser通过创建更接近模型形状的不透明度图来提高效率和质量。这样做可以减少所需的层数,因为对每一层的评估比对最终的图像更重要。

为了避免依赖于固定的切片设置,提出了更多的自适应技术。Salvi等人引入了自适应体积阴影图,每个阴影图的texel都存储阴影和层深。像素着色操作用于在光栅化时对数据流(表面不透明度)进行有损压缩。这避免了需要无限量的内存来收集所有样本并在一个集合中处理它们。该技术类似于深阴影映射,但在像素着色器中动态完成压缩步骤。将函数表示形式限制在一个小的、固定数量的存储不透明度/深度上,可以使GPU上的压缩和检索更高效。这个代价比简单的混合要高,因为需要读取、更新和写回,并且它取决于用来表示曲线的点的数量。在这种情况下,该技术还需要支持UAV和ROV功能的最新硬件(第3.8节末)。参见图7.29中的示例。

7.29

原书图7.29,用自适应体积阴影图渲染头发和烟雾。

在游戏GRID2中,使用了自适应的体积阴影影映射方法来实现真实的烟雾渲染,平均成本低于2 ms/frame。Furst等人描述并提供了他们实现一个视频游戏的深阴影图的代码。他们使用链表来存储深度和阿尔法值,并使用指数阴影映射来提供光照和阴影区域之间的软过渡。

阴影算法的探索仍在继续,各种算法和技术的合成变得越来越普遍。例如,Selgrad等人研究了使用链表存储多个透明样本,并使用带有分散写操作的计算着色器来构建映射。他们的工作使用了深度阴影地图的概念,以及过滤地图和其他元素,为提供高质量的软阴影提供了一个更通用的解决方案

9不规则Z-Buffer阴影

各种各样的阴影图方法因为几个原因而流行。它们的成本是可预测的,并且能够很好地适应场景大小的增加,最坏的情况是与元素的数量成线性关系。他们很好地映射到GPU上,因为他们依赖光栅化来定期采样光的视线。然而,由于这种离散的采样,问题出现了,因为眼睛看到的位置与光看到的位置不是一一对应的。当光线对表面的采样频率低于人眼时,就会出现各种混叠问题。即使采样率是可比较的,也存在偏倚问题,因为表面采样的位置比肉眼看到的稍微不同。

阴影体积提供了一种精确的分析解决方案,因为光线与表面的相互作用会导致形成三角形集,从而确定任何给定位置是被照明还是处于阴影中。该算法在GPU上实现时不可预测的代价是一个严重的缺点。近年来所探索的改进是诱人的,但还没有“被用于商业应用的”存在证据。

从长远来看,另一种分析阴影测试方法可能具有潜力:光线跟踪。 在第11.2.2节中详细描述了基本思想,它非常简单,尤其是对于阴影而言。光线从接收器位置射向光源。如果发现任何物体阻挡了射线,接收器就处于阴影中。快速射线跟踪器的代码主要用于生成和使用分层数据结构,以最小化每条射线所需的对象测试数量。为动态场景的每一帧构建和更新这些结构是一个有几十年历史的主题,也是一个持续的研究领域。

另一种方法是使用GPU的光栅化硬件来查看场景,但不仅仅是z深度,还存储了有关每个光网格单元中遮挡物边缘的附加信息。例如,想象在每个阴影映射的texel中存储网格单元重叠的三角形列表。这样的列表可以通过保守的栅格化生成,在这种情况下,如果三角形的任何部分与像素重叠,而不仅仅是像素的中心,则三角形生成片段(章节23.1.2)。这种方案的一个问题是,每个texel的数据量通常需要限制,这可能导致在确定每个接收器位置的状态时出现错误。考虑到gpu的现代链表原则,每像素存储更多数据当然是可能的。但是,除了物理内存限制外,每个texel列表中存储数量可变的数据的问题还在于,GPU处理可能变得效率极低,因为单个wrap可能会有一些片段线程需要检索和处理许多项, 而其余的线程则处于空闲状态,无需进行任何工作。 构建着色器以避免动态“ if”语句和循环导致的线程发散对性能至关重要。

在阴影图中存储三角形或其他数据并测试接收方位置的另一种方法是翻转问题,存储接收方位置,然后测试每个接收方的三角形。Johnson等人、Aila和Laine首次提出的保存接收器位置的概念称为不规则z缓冲(irregular z-buffer,IZB)。这个名称有点误导人,因为缓冲区本身对于阴影图来说是一个正常的、规则的形状。相反,缓冲区的内容是不规则的,因为每个阴影映射texel将有一个或多个接收器位置存储在其中,或者可能根本没有。见图7.30。

7.30

原书图7.30,Irregular z-buffer。左上角,来自眼睛的视图在像素中心生成一组点。两个三角形形成一个立方体显示。右上角,这些点是从光的角度显示的。左下角,是一个阴影图网格。对于每个texel,将生成其网格单元内的所有点的列表。右下角,通过保守的栅格化对红色三角形进行阴影测试。在每个被触摸的texel上,以淡红色显示,它的列表中的所有点都在灯光下测试三角形的可见度。

利用Sintorn等人和Wyman等人提出的方法,一个多通道算法创建IZB,并测试其内容从光可见性。首先,场景是由眼睛渲染的,以确定从眼睛看到的表面的z深度。这些点被转换成光的场景视图,并从光的截锥体形成紧密的边界。然后这些点被放置在光的IZB中,每个点都被放置在相应的texel中。注意,有些列表可能是空的,即光可以看到但没有被眼睛看到的表面的空间。遮光器被保守地光栅化到光的IZB,以确定是否有任何点隐藏在阴影中。保守的栅格化确保了,即使一个三角形没有覆盖轻的texel的中心,它也会被测试在一些可能重叠的点上。

可见性测试发生在像素着色器中。测试本身可以被可视化为射线跟踪的一种形式。光线是从图像点的位置到光线的位置产生的。如果一个点在三角形内并且比三角形的平面更远,它就被隐藏了。一旦所有的遮挡器被栅格化,光可见的结果被用来遮蔽表面。这种测试也称为截锥体跟踪,因为三角形可以被认为定义了一个视图截锥体,该视图检查其体积中包含的点。

仔细的编码是使这种方法与GPU良好工作的关键。Wyman等人注意到他们的最终版本比最初的原型快了两个数量级。这种性能提升的部分原因是直接的算法改进,比如剔除表面法线朝向背光的图像点(因此总是没有光照),避免为空的texel生成碎片。其他的性能提升来自于改进GPU的数据结构,以及通过在每个texel中使用短而相似长度的点列表来最小化线程发散。图7.30显示了一幅低分辨率的阴影图,其中列出了很长的列表,以便于说明。理想情况是每个列表有一个图像点。较高的分辨率提供了更短的列表,但也增加了由遮挡器生成的用于评估的片段的数量。

从图7.30左下角的图像中可以看出,由于透视图的影响,地面上的可视点密度在左侧比右侧要高得多。使用级联阴影贴图可以通过将更多的光贴图分辨率聚焦在眼睛附近来帮助降低这些区域的列表大小。

这种方法避免了其他方法的采样和偏差问题,并提供了完美的锐利阴影。出于审美和感知的原因,柔和的阴影通常是需要的,但是附近的遮挡可能会有偏置问题,比如Peter Panning。Story和Wyman探索混合阴影技术。其核心思想是利用遮挡距离来混合IZB和PCSS阴影,利用遮挡距离较近时的硬阴影和较远时的软阴影。参见图7.31。阴影质量对于附近的对象来说通常是最重要的,所以IZB的成本可以通过在一个选定的子集上使用这种技术来降低。该解决方案已成功应用于视频游戏中。本章从这样一个图开始,如图7.2所示。

7.31

原书图7.31,在左边,PCF为所有物体提供了均匀软化的阴影。在中间,PCSS通过与遮挡器的距离柔化阴影,但是树枝的阴影覆盖了箱子的左下角,产生了工件。在右边,来自IZB的锐利阴影与来自PCSS的柔和阴影混合,得到了改进的结果。

10其他应用

将阴影图作为一个空间体来处理,将光与暗分开,也可以帮助确定物体的阴影部分。Gollent描述了CD Projekt的地形阴影系统是如何为每个区域计算出仍被遮挡的最大高度,然后不仅可以用来渲染地形阴影,还可以用来渲染树木和场景中的其他元素阴影。为了确定每个高度,可见区域的阴影图将为太阳渲染。然后检查每个地形高度场位置,以确定其在太阳下的可见度。换句话说,我们沿着一条垂直线前进,并迭代缩小它与阴影图的表面相交的位置,该表面将光与暗分开。相邻的高度被插值来查找任意位置的遮挡高度。这种技术用于地形高度场位置的软阴影的一个例子可以在图7.32中看到。在第14章中,我们将会看到更多的光线在光和黑中穿行。

7.32

原书图7.32,根据每个高度场的位置计算出太阳第一次出现的高度。注意沿着阴影边缘的树木是如何被适当地遮蔽的。

最后一个值得一提的方法是渲染屏幕空间阴影screen-space shadows。由于阴影图的分辨率有限,因此常常无法对小特征产生准确的遮挡。这在绘制人脸时尤其成问题,因为我们特别容易注意到人脸上的任何视觉假象。例如,渲染发光的鼻孔(不是故意)看起来很不和谐。虽然使用高分辨率的阴影图或单独的阴影图,只针对感兴趣的地区可以有所帮助,另一种可能性是利用现有的数据。在大多数现代渲染引擎中,来自早期prepass的摄像机视角的深度buffer在渲染过程中是可用的。存储在其中的数据可以作为一个高度场。通过对这个深度进行迭代采样,我们可以执行一个光线行进过程(第6.8.1节),并检查朝向光的方向是否畅通。尽管这样做很昂贵,因为它涉及到重复采样深度缓冲区,但这样做可以为过场动画的特写镜头提供高质量的结果,在过场动画中通常需要花费额外的毫秒是合理的。该方法由Sousa等人提出。并在当今许多游戏引擎中普遍使用。

总结整个章节,某种形式的阴影映射(shadow mapping)是目前为止最常用的投影到任意表面形状的算法。级联阴影贴图可以提高大面积阴影的采样质量,比如户外场景。通过SDSM为近平面寻找一个良好的最大距离可以进一步提高精度。百分比近似过滤(PCF)给阴影一些柔和,百分比近似柔化阴影(PCSS)及其变体给接触处硬化,而irregular z-buffer可以提供精确的硬阴影。滤波的阴影图提供了快速的软阴影计算,特别是当遮挡者远离接收器时,如地形。最后,屏幕空间技术可以用于额外的精度,尽管代价是显而易见的。

在本章中,我们重点讨论了当前应用程序中使用的关键概念和技术。每一种都有自己的优势,选择取决于世界的大小、组成(静态内容与动态内容)、材料类型(不透明、透明、头发或烟雾)以及灯光的数量和类型(静态或动态;本地或遥远;点光、聚光灯或区域光),以及诸如底层纹理隐藏工件的程度等因素。GPU的能力不断进化和改进,所以我们期望在未来几年能继续看到新的算法能够很好地映射到硬件上。例如,第19.10.1节中描述的稀疏纹理技术已经被应用到shadow map存储中,以提高分辨率。在创新方法中,Sintorn,Kampe,和其他人探讨的想法是将一个二维光阴影图转化为三维的体素(小盒子;见13.10节)。使用体素的一个优点是它可以被归类为亮的或阴影的,因此需要的存储空间很小。高度压缩的稀疏体素八叉树表示为大量的光和静态遮挡器的存储阴影。Scandolo等人将他们的压缩技术与使用双重阴影映射的基于间隔的方案相结合,从而提供更高的压缩率。Kasyan使用体素圆锥体跟踪(第13.10节)从区域灯光生成柔和的阴影。有关示例,请参见图7.33。更多的锥形跟踪阴影如图13.33所示。

7.33

原书图7.33,顶部是用基本的软阴影近似值生成的图像。在底部是基于体素的区域光阴影使用锥跟踪,体素化的场景。注意汽车上的漫反射阴影。照明也因一天中时间的变化而不同。

扩展阅读和参考资料

Eisemann等人的著作《Real-Time Shadows》

SIGGRAPH 2012课程

Woo 和 Poulin的《Shadow Algorithms Data Miner》

更多资料见原书