写在前面的话:因为英语不好,所以看得慢,所以还不如索性按自己的理解简单粗糙翻译一遍,就当是自己的读书笔记了。不对之处甚多,以后理解深刻了,英语好了再回来修改。相信花在本书上的时间和精力是值得的。
阴影对于创建真实的图像很重要, 为用户提供了关于对象位置的视觉提示。 本章主要介绍阴影计算的基本原理,并描述了最重要和最流行的实时算法。 我们还简要讨论了不太流行但体现了重要原则的方法。
本章中使用的术语如图7.1所示,其中遮光板(occluder)是把阴影投射到接收器(receivers)上的物体。如果使用精准光源(punctual light source),即没有区域,会生成完全阴影区域,有时被称为硬阴影(hard shadows)。 如果使用面积或体积光源,则会产生软阴影(soft shadows)。 每个阴影可以有一个完全阴影的区域,称为本影(umbra)和一个部分阴影的区域,称为半影(penumbra)。软阴影可以通过其模糊的阴影边缘来识别。 然而,需要注意的是,如果只是利用一个低通滤波对硬阴影的边缘进行模糊,通常不能得到正确的渲染结果。如图7.2所示,一个正确的软阴影在接收器上的形状是近似于投射阴影的几何体。 软阴影的本影区域不等于由精准光源产生的硬阴影。 相反,软阴影的本影区域会随着光源变大而减小,如果光源足够大,且接收器距离遮光板足够远,它甚至可能消失。 软阴影效果更好,因为半阴影边缘让观众知道,这里确实是一个阴影。 硬边阴影通常看起来不那么真实,有时会被误解为当前的几何特征,比如表面的折痕。 然而,硬阴影比软阴影渲染速度快。
图 7.1 阴影的术语:光源(light source)、遮光板( occluder)、接收器(receiver)、阴影(shader)、本影(umbra)、半影(penumbra)
图 7.2 硬阴影和软阴影的混合。 板条箱的阴影很锐利, 因为遮挡者靠近接收器。人的影子在接触点是锐利的, 随着到遮光板的距离增加而软化。 远处的树枝投下柔和的阴影。
比拥有半影更重要的是要拥有阴影。 没有一些阴影作为视觉提示, 场景往往难以令人信服,也更难理解。 正如Wanger所表明的,有一个不准确的阴影通常比没有更好,因为眼睛对阴影的形状是相当宽容的。 例如,在地板上使用一个模糊的黑色圆圈作为纹理可以将一个角色锚定在地面上。
在接下来的部分中,我们将超出这些简单的建模阴影,并介绍从场景中的遮挡器实时自动计算阴影的方法。
一个阴影发生的最简单例子就是物体在一个平面上投射阴影。 本节介绍了几种用于平面阴影的算法,每种算法在阴影的柔度和真实感方面都有所不同。
在这个方案中 ,三维物体被渲染了两次来创建一个阴影。通过矩阵计算,把物体的顶点投射到平面上。考虑图7.3所示的情况,光源的位置为l,被投影的顶点是v,投影到平面的顶点是p。先推出投影矩阵的特殊情况下,即阴影平面是y = 0,然后这个结果将推广到任何平面。
图 7.3 左图:光源在I,投射一个阴影到平面y=0上。顶点v被投影到平面上,对应的投影点是p。两个相似三角形可以用来推导出投影矩阵。右图:在平面π:n·x + d = 0 上的投影。
我们先求出x坐标的投影,从图7.3左图的相似三角形,我们可以得到:
用同样的方式可以获得z坐标:,同事y坐标为0。然后把这些方程转换成投影矩阵M:
很容易验证Mv = p, 这意味着M确实是投影矩阵。
在通常情况下,阴影所在平面不是平面y=0,可以用π:n·x+d =0来表示,如图7.3右图所示。 们的目标是找到一个将v投影到p的矩阵。从l发出的射线,经过v,和π平面发生交叉,然后就得到了投影点p:
这个方程也可以转换成一个投影矩阵,如公式7.4所示,满足Mv=p:
如果平面为y=0,也就是有n=(0,1,0),d=0,公式7.4就变成了公式7.2。
为了渲染出阴影,简单把这个矩阵应用到物体上,在平面π上投射阴影,然后用黑色并且无光照来渲染这个投影在平面上的阴影。实际操作的时候,需要避免阴影三角形被渲染在接受表面的下方。一种方法是在投影表面上方加一些偏差,这样我们的阴影三角形就永远渲染在表面上方。
一种更安全的方法是先画出地平面,然后关闭z-buffer,画出阴影三角形,然后像往常一样渲染剩余的几何图形。这些阴影三角形就会永远绘制在地平面上方,因为没有深度比较。
如果地平面有限制,例如,它是一个矩形,投影阴影可能会落在它之外,打破了视觉错觉。为了解决这个问题,我们可以使用模板缓冲。首先,把接收器绘制到屏幕和模板缓冲中,然后关闭z-buffer,然后绘制只在接收器范围内的阴影三角形,最后正常渲染剩余的场景。
另外一种阴影算法是把阴影三角形渲染进一张纹理,这个纹理会被应用到地平面上。这个纹理是一种光照贴图(light map)。 正如我们所看到的,这种将阴影投射到纹理上的想法也允许在曲面上产生半阴影和阴影。这种技术的一个缺点是纹理可能被放大,一个纹素可能会覆盖多个像素,打破了视觉错觉。
如果阴影环境并不是每帧直接发生变化,例如,光源和阴影投射物彼此之间并无移动,这个纹理可以复用。 如果没有发生变化,大多数阴影技术都可以从从一帧到另一帧的中间计算结果的重用中获益。
所有的阴影发射者必须在光和地平面接收器之间, 如果光源处在物体的最高点下方,会有反阴影(antishadow)发生, 因为每个顶点都是通过光源的点来投影的。 正确的阴影和反阴影如图7.4所示。 如果我们投射一个在接收平面以下的物体,也会发生错误,因为它也不应该投射阴影。
图 7.4 左图展示的是正确的阴影,右图展示的是反阴影, 因为光源在物体的最上面的顶点之下。
当然可以显式地剔除和修剪阴影三角形,以避免这类瑕疵。 下面介绍一种更简单的方法,即使用现有的GPU管道来执行自带裁剪的投影。
使用一些技术也可以使投影阴影变成软阴影。在这描述一种Heckbert和Herf提出的算法,这个算法的目的是在地平面生成一张纹理来展示软阴影。
当光源有一定面积时,就会出现软阴影。 一种近似区域光效果的方法是通过在其表面放置几个精准光源来采样。 对于这些精准光源,都被渲染到一张纹理中并积累到一个缓冲区中。 这些纹理的平均值就是一个软阴影。注意,理论上,任何生成硬阴影的算法都用这类累加技术来生成半影。实际中,以交互的频率来做这类事情是站不住的,因为执行时间也需要考虑到。
Heckbert和Herf使用了一种基于平截头体的方法来生成阴影。思想就是把光源当作观察者,地平面当作平截头体的远裁剪平面。 截锥体做得足够宽,以包围遮挡板。
软阴影纹理是通过生成一系列的地平面纹理而形成的,以区域光源上不同的采样点为当前光源,对物体进行投影阴影到渲染到地平面纹理上。然后把这些纹理加起来进行求平均值,这个平均值就是我们要的阴影纹理,图7.5左图就是一个示例。
图 7.5 左图是采用的Heckbert和Herf的算法,使用了256个pass,右图采用的是Haines的算法,使用了一个pass。Haines的算法的本影太大了,尤其是在窗户和门处。
对区域光源进行采样的算法的一个问题就是它看起来像它本来的样子:几个重叠的影子来自精准光源。同时 ,对n个阴影pass,只生成了n+1个明显的阴影。 大量的传递可以得到准确的结果,但是付出的代价太高。
一个更高效的方法是使用卷积(convolution),即滤波。 在某些情况下,模糊单一点产生的硬阴影就足够了,并且可以产生半透明的纹理,可以与真实世界的内容混合。如图7.6所示。 然而,在物体与地面接触的地方,一个统一的模糊是无法令人信服的。
图 7.6 下落的阴影(Drop Shadow)。 阴影纹理是通过从上往下渲染出物体的阴影,然后模糊纹理,再渲染到地平面上。
还有一些其他的方法可以获得更好的近似效果,但是需要额外的成本。例如,Haines算法是先算出一个硬阴影,然后渲染轮廓的梯度边缘,从黑暗的中心到白色的边缘,创造似是而非的半阴影。如图7.5右图所示。然而,这些半阴影不是物理正确的, 因为它们也延伸到了轮廓边缘内的区域。 所有这些方法都有不同的近似值和缺点,但是比平均一组大的阴影纹理要有效得多。
将平面阴影扩展到曲面的一个简单方法是使用生成的阴影纹理作为投影纹理。从光源方向考虑阴影。 光能看见的,就能被照亮;它看不见的东西则在阴影里。 假设遮光板从光源方向上被渲染到另外一张白色纹理上。 这个纹理可以投射到接收阴影的表面上。在接收器上的每个顶点有一个(u,v)纹理坐标用于计算它,且纹理应用会用到它。 应用程序可以显式地计算这些纹理坐标。 这与前一节中的地面阴影纹理稍有不同,在前一节中,物体被投射到特定的物理平面上。 在这里,纹理是从光的角度拍摄的,就像投影机里的一帧胶片。
当渲染时,投影阴影纹理会修改接受器表面, 它也可以与其他阴影方法相结合,有时主要用于帮助感知到物体的位置。 例如,在一个平台跳跃的视频游戏中,主角可能总是被直接赋予一个Drop阴影,即使角色处于完全的阴影中。
纹理投影方法存在一些严重的缺陷。首先,应用程序必须识别哪些对象是遮光板,哪些对象是它们的接收者。 接收器必须由程序维护,使其比遮光板离光更远,否则阴影就会“向后投射(cast backward)”。另外,遮光板并不能给自己阴影。
注意,可以通过使用预构建投影纹理来获得各种光照模式。 聚光灯是一个简单的方形投射纹理,它的内部有一个圆圈来定义光线。
Heidmann在1991年提出了一种基于Crow 's shadow volume的方法,该方法巧妙地利用模板缓冲区将阴影投射到任意物体上。 它可以用于任何GPU,因为唯一的要求是模板缓存。 它不是基于图像的,从而避免了采样问题,从而可以产生正确的尖锐阴影。 这有时可能是一个不利因素。 例如,一个角色的衣服可能会有褶皱,产生稀薄、坚硬的阴影,会有严重的混叠。 由于其不可预测的成本,体积阴影现在很少被使用。我们在这里对算法进行了简要的描述,因为它说明了一些重要的原则和基于这些原则的研究。
首先,想象一个点和一个三角形。 把线穿过这个点以及该三角形的顶点,并延伸到无限远处,就得到了一个无限的三面金字塔。 三角形下面的部分,即不包括点的部分,是一个截断的无限金字塔,而上面部分只是一个金字塔,见图7.7。现在假设这个点是光源, 然后,在被截断的金字塔体(三角形下方)内的物体的任何部分都处于阴影中。这个体积被称为阴影体。
图 7.7 左图:点光源的光线通过三角形的顶点延伸,形成一个无限的金字塔。 右图:上半部分为金字塔,下半部分为无限截形金字塔,也称阴影体。所有在阴影体内的几何图形都在阴影中。
假设我们查看某个场景,并通过一个像素跟踪来自眼睛的光线,直到光线击中要在屏幕上显示的对象。 当光线到达这个物体时,当它穿过正对着的阴影体的一个面时(即,面向观众),我们给一个计数器加一。因此,每次射线进入阴影计数器是递增的。 以同样的方式,每次射线穿过截形金字塔的背面时,我们给计数器减一,光线会从阴影中消失。 我们持续对计数器进行递增和递减,直到射线击中的物体需要显示的那个像素。 如果计数器大于0,则该像素处于阴影中;否则就不是。 这个原则也适用于有多个三角形投射阴影的情况。参见图7.8。
图 7.8 使用两种不同的计数方法计算阴影-体积交叉点的二维侧视图。在z-pass体积计数中, 当光线通过阴影体的前面(frontfacing)三角时,计数递增;当光线通过后面(backfacing)三角时,计数递减。因此,在点A,射线进入了两个阴影体,计数为+2,然后离开了两个阴影体,计数清零,所以这个点在光中。在z-fail体计数中, 计数从表面开始(这些计数以斜体显示)。在B点处的射线,z-pass方法给出的计数为+2(穿过了两个前面三角形),z-fail给出了相同的计数(穿过了两个后面三角形)。点C展示了z-fail阴影体积必须要有上限。 射线从点C出发,首先击中前面三角形,给出了- 1。 然后,它退出两个阴影体积(通过它们的结束端,这是此方法正常工作所必需的),净计数为+1。计数不为0,所以这个点在阴影中。 这两种方法对所观察的表面上的所有点都给出相同的计数结果。
用射线做这件事很费时间。 但还有一个更聪明的解决方案: 模板缓冲可以帮我们计数。 首先,清除模板缓冲区, 其次,整个场景被绘制到帧缓冲中,只使用无光照材质的颜色,以获得颜色缓冲区中的这些着色组件和z-buffer中的深度信息。 第三,关闭z-buffer的更新和写入颜色缓冲区(尽管z缓冲测试仍在进行),然后绘制阴影体积的前面三角形。 在此过程中,增加在模板缓冲中需要绘制三角形的位置的值。 第四步,在另外一次Pass渲染过程中使用模板缓冲区再完成一次,这一次只绘制阴影体积的背面三角形。 在这次Pass中,模板缓冲区中绘制三角形的位置的值将递减。 当计数的加减都完成后,只有阴影体需要渲染的表面的像素是可见的(即不是被任何真实的几何图形所隐藏)。 在这一点上,模板缓冲区保持每个像素的阴影状态。 最后,再次渲染整个场景,这次只使用受光线影响的材质组件,并且只显示模板缓冲区中值为0的地方。 值为0表示射线从阴影中消失的次数与进入阴影体积的次数相同,这个位置被光照亮。
这种计数方法是阴影体背后的基本思想。 阴影体算法生成的阴影示例如图7.9所示。 有一些有效的方法可以只使用一次Pass就可以实现算法, 然而,当一个物体穿透相机的近平面时,计数问题就会出现。 一种解决方案称为z-fail,计算的是隐藏在可见表面的背面而不是前面。此方案的简要摘要如图7.8所示。
图 7.9 阴影体。左图,一个角色投射出阴影,右图,是阴影体积的三角形模型。
为每个三角形创建四边形会产生大量的overdraw。 也就是说,每个三角形将创建三个必须呈现的四边形。一千个三角形构成的球需要三千个四边形, 这些四边形中的每一个都可以跨屏幕。 一种解决方案是沿着物体的轮廓边缘只画那些四边形,例如,我们的球体可能只有50个轮廓边缘,那么我们只需要50个四边形。几何着色器可以用来自动生成这样的轮廓边缘。剔除和钳制技术也可用于降低充填成本。
然而,阴影体算法仍然有一个可怕的缺点:极端的可变性。 想象视觉中有一个小三角形, 如果相机和光源处于完全相同的位置,阴影体的成本是最小的。 形成的四边形不会覆盖任何像素,因为它们是视图的边缘。 假设观察者现在绕着三角形旋转,让它一直在视野中。 当相机从光源移开时,阴影体积的四边形将变得更加可见,并覆盖更多的屏幕,导致更多的计算发生。 如果观察者恰好移动到三角形的阴影中,阴影体积将完全填满屏幕,与我们最开始的视图相比,需要花费大量的时间。 这种可变性使得阴影体在交互应用程序中不可用,在交互应用程序中,一致的帧率是非常重要。
在1978年,Williams提出了一种通用的基于z缓冲的渲染器,可以用来快速生成任意物体上的阴影。 这个想法是渲染场景,使用z缓冲,并从光源的位置投射阴影。 凡是被光“看见”的都被照亮了,其余的都在阴影里。 当生成此纹理时,只需要了z缓冲。 可以关闭灯光、纹理并将值写入颜色缓冲区。 z缓冲区中的每个像素现在都包含最接近光源的对象的z深度。
我们称z缓冲区的全部内容为阴影贴图(shadow map),有时也称为阴影深度贴图(shadow depth map)或阴影缓冲区(shadow buffer)。 要使用阴影贴图,场景需要第二次渲染,但这次是相对于观察者而言的 。 在绘制每个图元时,将其在每个像素处的位置与阴影贴图进行比较。 如果一个渲染点离光源的距离比阴影贴图中相应的值更远,那么这个点就在阴影中,否则就不在阴影中。 这种技术是通过使用纹理映射来实现的。如图7.10所示,阴影贴图是一个流行的算法,因为它是相对可预测的。 构建阴影贴图的成本与绘制的图元数量大致成线性关系,并且访问时间是恒定的。 阴影贴图可以只生成一次,在光线和物体不动的场景中可以每帧都重用,如在计算机辅助设计的时候。
图 7.10 阴影贴图。 左上图,阴影贴图是通过将深度值存储到视图中的表面而形成的。右上图,眼睛观看两个位置。可以看到球体上的Va点,这个点也可以在阴影贴图上找到对应纹素a。 储存在那里的深度并不比Va点少,所以这个点被照亮了。 与纹素b中存储的深度相比,击中点Vb的矩形距离光线更远,因此处于阴影中。 左下图是以光的角度渲染一个场景,白色表示较远。右下图是用了这个阴影贴图渲染的场景。
当生成一个单独的z缓冲时,光只能“看到”一个特定的方向,就像照相机一样。 对于像太阳这样的远距离平行光,光的视野被设置成对眼睛看到的可视范围内的所有物体投射阴影。 光使用正交投影,它的视图需要在x和y中足够宽和足够高来查看这组对象。 局部光源需要类似的调整,尽可能。 如果局部光源离投射阴影的物体足够远,一个单一的视锥截体可能就足以包含所有这些物体。 如果局部光源是聚光灯,它有一个自然的截锥体与它相关联,在它的截锥体之外的一切都被认为是没有被照亮到的。
如果局部光源位于场景内部,并被阴影遮挡板包围,典型的解决方案是使用六视图立方体,类似于立方体环境映贴图。这些被称为全向性的阴影贴图(omnidirectional shadow maps)。 全向性的贴图的主要挑战是避免在两个独立贴图相交的接缝处出现瑕疵。 King和Newhall深入分析了问题并提供了解决方案, Gerasimov提供了一些实现细节。 Forsyth提出了一种适用于全向性的光源的通用多截锥体分割方案,该方案在需要的地方提供了更多的阴影贴图分辨率。 Crytek基于每个视图的投影截锥体的屏幕空间覆盖范围,为一个点光源设置六个视图的分辨率,并将所有的贴图存储在一个纹理图集中。
并不是场景中的所有对象都需要渲染到光的视体中。 首先,只有能投射阴影的对象需要被渲染, 例如,如果已知地面只能接收阴影而不能投射阴影,那么它就不必渲染到阴影贴图中。
根据定义,阴影的投射物是需在光的视截锥体内。 这个截锥体可以通过几种方式来扩大或收紧,这样我们就可以安全地忽略一些阴影投射物了。 想想那些肉眼可见的阴影接收器。 这组物体在光的视场方向上的最大距离内。 任何超过这个距离的东西都不能在可见的接收器上投下阴影。 同样地,可见接收器的集合可能会比光原来的x和y视图范围更小。如图7.11所示。 另一个例子是,如果光源在眼睛的视锥截体内部,那么这个视锥截体之外的任何物体都不能在接收器上投射阴影。 只渲染相关物体不仅可以节省渲染时间,还可以减小光的截锥体所需的尺寸,从而提高阴影贴图的有效分辨率,从而提高质量。 此外,如果光截锥体的近平面离光越远越好,远平面越近越好。 这样做可以提高z缓冲区的有效精度。
图 7.11 左图光的视图包含了眼睛的截锥体。中间图, 光的远平面剔除了蓝色三角形作为阴影投射者;近平面也做了类似的调整。 右图,光的截锥体的边缘面被用来约束可见的接收器,剔除了绿色的胶囊。
阴影贴图的一个缺点是阴影的质量取决于阴影贴图的分辨率(以像素为单位)和z-buffer的数值精度。 由于阴影贴图是在深度比较时采样的,因此该算法容易产生走样问题,特别是在物体之间的接触点附近。 一个常见的问题是自阴影走样(self-shadow aliasing),通常称为“surface acne”或“shadow acne”,这种情况下,本身的一个三角形被错误地认为是阴影。 这个问题有两个来源, 一个是处理器精度的数字限制,另一个来源是几何的,因为点样本的值被用来表示一个区域的深度。 也就是说,为光线生成的样本几乎从不与屏幕样本位于相同的位置(例如,像素通常在其中心采样)。 当光的存储深度值与被观察表面的深度相比较时,光的值可能略低于表面的深度值,从而导致自阴影。 这些错误的效果如图7.12所示。
图 7.12 阴影贴图的偏差瑕疵。左图,由于偏差太低,所以发生了自阴影。右图,高偏差导致了鞋子那块没有透射阴影。 阴影贴图的分辨率也太低,给了阴影一个块状的外观。
一个常见的帮助避免(但不总是消除)各种阴影贴图瑕疵的方法是引入一个偏差因子。 在检查阴影贴图中找到的距离和测试位置的距离时,从接收器的距离中减去一个小偏差。如图7.13所示。 这个偏差可以是一个常数值, 但是在接收器大部分不是面对光的时候,这样做可能会失败。 一个更有效的方法是使用一个偏差,这个偏差与接收器对光的角度成比例。 表面越偏离光源,偏差越大,从而避免了这个问题。 这种类型的偏差称为slop scale bias。注意,如果一个表面直接面对光源, 它完全不受slop scale bias的影响。 因此,为了避免可能出现的精度误差,在slop scale bias的基础上使用了常数偏差slop scale bias也经常钳制在一些最大值,因为从光的角度看,当表面接近侧立时,切线值可能会非常高。
图 7.13 阴影偏差。在头顶光源下,表面被渲染成一个阴影贴图, 用竖线表示阴影贴图的像素中心。 遮挡板的深度记录在×点, 我们想知道表面上的三个采样点是否有光照。 最接近的阴影贴图深度值用相同的颜色×表示。 左图中,如果没有添加任何偏差,蓝色和橙色的样本将被错误地判定为处于阴影中,因为它们离光的距离比对应的阴影贴图深度更远。 中间图,从每个样本中减去一个恒定的深度偏差,使每个样本更靠近光源。 蓝色样本仍然被认为是在阴影中。 右图,阴影贴图是通过将每个多边形沿着光的方向按其斜率比例移动而形成的。所有的样本深度现在都比阴影贴图深度更近了,所以都被点亮了。
Holbert提出了法向量偏移偏差(normal offset bias),它首先将接收器在世界空间的位置沿表面法向量偏移一点,与光的方向与几何法向量的夹角的正弦成正比。如图7.24所示。 这不仅改变了深度,还改变了在阴影贴图上测试样本的x坐标和y坐标。 当光和表面的角度变得更浅时,这个偏移量就会增加,采样点会变得离表面足够远以避免自阴影。 这种方法可以被想象成把采样点移动到接收器上方的“虚拟表面”。 这个偏移量(offset)是一个世界空间的距离,所以Pettineo建议按阴影贴图的深度范围缩放它。 Pesce提出了一种沿着摄像机视角方向的偏差(bias),这种偏差也可以通过调整阴影贴图的坐标来实现。 过多的偏差会导致光泄漏(light leaks)或彼得平移(Peter Panning), 在这种情况下,物体似乎漂浮在略高于下方表面的地方。 产生这种瑕疵的原因是因为物体接触点以下的区域,被向前推得太远,所以不会有阴影。
一种避免自阴影问题的方法是只渲染背面到阴影贴图,称为第二深度阴影贴图(second-depth shadow mapping), 这种方案在很多情况下都能很好地工作,特别是在不允许手工调整偏差的渲染系统中。 当物体是双面的、薄的或相互接触时,就会出现问题。 如果一个物体的网格模型的两边都是可见的,例如,一个棕榈叶或一张纸,自阴影可以发生,因为背面(backface)和正面(frontface)在同一个位置。 同样的,如果不执行偏差,问题可能发生在物体的轮廓边缘或薄物体附近,因为在这些区域,背向距离正面很近。 添加偏置可以帮助避免surface acne,但该方案更容易漏光,因为在接触点上接收器和遮挡板的背面没有分离。如图7.14所示。 选择哪种方案需要根据情况而定。
图 7.14 在头顶光源下的阴影贴图表面。左图中,表面正对着光源被发送给阴影贴图,用红色标记。 表面可能被错误地认为为阴影(“acne”),所以需要添加偏差远离光源。中间图,只有三角形的背面被渲染到阴影贴图中,偏差将这些遮光板向下推,使得光会泄漏到a点附近的地面上。 向前偏差会引起轮廓边界附近的有光照的点(标记为吧)会考虑进阴影中。右图中,在阴影贴图上每个位置的最近的正面和背面三角形之间的中点形成一个中间面,在点c附件可能会发生漏光( 第二深度阴影贴图也会发生), 因为最近的阴影贴图样本可能在这个位置左边的中间表面上,所以这个点会更靠近于光。
注意,对于阴影贴图,对象必须是“水密的”(流形和封闭,即固体),或必须同时正面和背面都被渲染到贴图中,否则该物体可能不会完全投射阴影。 Woo提出了一种通用的方法,字面上来说,它试图在仅仅使用正面和背面之间找到一个折衷的方法。 这个想法是把实体物体渲染到阴影贴图上,并跟踪离光最近的两个表面。 这个过程可以通过深度剥离(depth peeling)或其他透明相关技术来完成。 两个对象之间的平均深度形成一个中间层,其深度用作阴影贴图,有时称为对偶阴影贴图(dual shadow map)。 如果物体足够厚,自阴影和光泄漏的影响就会最小化。
当观察者移动时,光的视截锥体会随着阴影投射物改变而经常改变大小。 这些变化反过来又导致阴影在帧与帧之间轻微移动。 这是因为光的阴影贴图是从光的不同方向采样的,而这些方向与前一组方向不一致。 对于平行光,解决方案是强制每个后续生成的阴影贴图在世界空间中保持相同的相对纹素光束位置。 也就是说,可以将阴影贴图看作是在整个世界空间添加了一个二维网格框架,每个网格单元表示贴图上的一个像素样本。 移动时,网格单元的不同集合生成阴影贴图。 换句话说,光的视图投射被强制到这个网格上以保持帧与帧之间的一致性。
和使用纹理类型,理想情况下,我们想要一个阴影贴图纹素覆盖大约一个纹理像素。 如果我们有一个光源和眼睛在同一位置, 阴影贴图完美地与屏幕空间像素一一对应(并且没有可见的阴影,因为光线精确地照亮了眼睛看到的东西)。 一旦光的方向发生改变,每个像素的比例就会改变,就会发生错误。图7.15展示了一个例子。 由于前景中的大量像素与阴影贴图的每个纹素关联,因此阴影是块状的,质量很差。 这种不匹配称为透视走样(perspective aliasing)。如果一个表面和光几乎平行但是朝着观察者,单个阴影贴图纹素可以覆盖多个像素。这个问题称为投影走样(projective aliasing)。如图7.16。 可以通过增加阴影贴图的分辨率来降低块数量,但这是以增加内存和运算为代价的。
图 7.15 左图使用的是标准的阴影贴图,右图使用的是LiSPSM。 每个阴影贴图的纹素的投影都显示出来了。两种阴影贴图有相同的分辨率, 不同之处在于LiSPSM改变了光的矩阵,在靠近观察者的地方提供了更高的采样率。
图 7.16 左图的光源几乎就在头顶。 阴影的边缘有点粗糙,因为和观察视图相比它的分辨率有点低。 右图,光线方向接近地平线,所以每个阴影纹素覆盖了很多水平屏幕区域,因此产生了更多的锯齿状边缘。
还有另一种方法来创建光的采样模式,使其更接近相机的模式。 这是通过改变场景朝光投射的方式来实现的。 通常我们认为视图是对称的, 视向量在截锥体的中心。 然而,视图方向仅仅定义了一个视图平面, 但不包括采样的像素。 定义截锥体的窗口可以在这个平面上移动、倾斜或旋转, 创建一个四边形,给了视图空间一个不同的映射。 仍然按一定的间隔对四边形进行采样,因为这是线性变换矩阵的性质。 采样率可以通过改变光的视图方向和观察窗口的边界来改变。如图7.17。
图 7.17 给定一个头顶光源, 左图中的地板的采样率与眼睛的速率不匹配,右图中通过改变光的视图方向和投影窗口, 采样率会偏向于在眼睛附近产生更高密度的纹素。
将光的视图映射到眼睛有22个自由度。 探索这个解空间(solution space)导致了几种不同的算法来解决更好地匹配光线的采样率和眼睛的采样率问题,包括透视阴影贴图(perspective shadow map,PSM),梯形阴影贴图( trapezoidal shadow map, TSM),和光空间透视阴影贴图(light space perspective shadow map,LiSPSM)。如图7.15和7.26所示。
这些矩阵曲解算法的一个优点是,除了修改光的矩阵外,不需要额外的工作。 每种方法都有自己的优缺点, 因为每种方法都可以帮助匹配某些几何图形和照明情况下的采样率,而使其他情况下的采样率更糟。
一种光照情况,即当光在摄像机前并指向它时,矩阵曲解算法就会失效。 这种情况被称为dueling 截锥体,或者更通俗的说法是“聚光灯下的鹿,deer in the headlights”。 在眼睛附近需要更多的阴影贴图采样,但是线性曲解只会使情况变得更糟。 这个和其他的问题,如质量的突然变化和在摄像机移动过程中产生的阴影的不稳定的质量,都使得这些方法失宠。
Blow独立实现了这样一个系统:生成一组固定的阴影贴图(可能是不同分辨率的),覆盖场景的不同区域。 在Blow的方案中,四个阴影贴图被嵌套在观察者周围。 这种情况下,附近的物体就可以得到高分辨率的贴图,而远处物体的分辨率会下降。 Forsyth提出了一个相关的想法,为不同的可视对象集生成不同的阴影贴图。在他的设置中避免了如何处理物体跨越两个阴影贴图边界的转换问题,因为每个对象都有且只有一个与之关联的阴影贴图。Flagship Studios融合了这两种思想,开发出一套系统。 一个阴影贴图用于附近的动态对象,另一个映射用于附近静态对象的网格部分,第三个映射用于整个场景中的静态对象。第一个阴影贴图是每帧都生成,其他两个阴影贴图只需要生成一次,因为光源和几何物体都是静态的。
在2006年Engel, Lloyd等人,Zhang等人独立地研究了通过平行于视图方向的切片,将视图截体的体积分割成几个部分。如图7.18所示。 随着深度的增加,每个连续体积的深度值大约是先前体积的深度值的两到三倍。对每一个视体, 光源可以形成一个紧密包围它的截锥体,然后生成一个阴影贴图。 通过使用纹理图集或纹理数组,可以将这些不同的阴影贴图视为一个大型纹理对象,从而使缓存访问延迟最小化。图7.19展示了明显的质量提升的对比。 恩格尔将这种算法命名为级联阴影贴图(cascaded shadow maps,CSM),它比张的术语“平行分割阴影贴图(parallel-split shadow maps)”更常用,但两者在文献中都出现过,而且实际上是相同的。
图 7.18 左图,沿着眼睛观察方向的视图截锥体被分成四个部分。 右图,给每个体块创建的包围盒,包围盒决定了方向光下的每个体块渲染的阴影贴图。
图 7.19 左图,场景的可视范围太宽导致一个2048×2048分辨率的单个阴影贴图会显示出透视走样。右图,4个1024x1024的阴影贴图沿视图轴放置可以显著提高质量。
这种算法实现简单,能够覆盖较大的场景区域,且结果合理,鲁棒性强。 Dueling frusta问题可以通过在接近眼睛的地方采用更高的采样率来解决,并没有其他严重的问题。 由于这些优点,级联阴影贴图在许多应用中得到了应用。
虽然可以使用透视曲解(perspective warping)将更多的样本压缩到单个阴影贴图的细分区域, 规范是为每个级联使用单独的阴影贴图。 从图7.18和图7.20可以看出,每个贴图所覆盖的区域是不同的。 较小的视体为更近的阴影贴图在需要的地方提供了更多的采样。如何确定贴图分割开来的各个部分的z-buffer的范围—被称为z-partitioning。 一种方法是对数分割(logarithmic partitioning), 对于每个级联贴图,远平面到近平面的距离的比例是相同的。
其中,n和f是整个场景的近平面和远平面,c是贴图的数量,r是最终比例。 例如,如果场景中最近的物体是1米远,最大距离是1000米,我们有三个级联贴图,则
距离最近的视图的远近平面距离是1和10,下一个间隔是10到100来保持这个比例,最后一个间隔是100到1000米。 初始近平面深度对这种划分有很大的影响。 如果近平面的深度只有0.1米,则10000的立方根为21.54,这是一个相当高的比例,例如0.1比2.154比46.42比1000。 这意味着生成的每个阴影贴图必须覆盖更大的区域,从而降低其精度。 实际上,这样的划分为接近近平面的区域提供了相当高的分辨率,如果该区域内没有对象,则会浪费这种分辨率。 避免这种不匹配的一种方法是将分区距离设置为对数分布和等距分布的加权混合,但如果我们能够确定场景的紧密视图边界,那就更好了。
图 7.20 阴影级联可视化。 紫色、绿色、黄色和红色表示的是离我们最近的到最远的级联。
挑战在于如何设置近平面。 如果近平面设置得离眼睛太远,物体可能会被近平面截去。 Lauritzen等人提出了样本分布阴影贴图(sample distribution shadow maps, SDSM),它使用前一帧的z-depth值来决定使用两种方法中的一种来进行划分。
第一种方法是通过查找z-depth的最小值和最大值,并使用这些值设置远近平面。 这是通过GPU的reduce操作来完成的, 由计算机或其它着色器分析一系列很小的缓冲区,输出缓冲区作为输入反馈,直到剩下一个1×1的缓冲区。 通常情况下,这些值会根据场景中物体移动的速度进行调整。 除非采取纠正措施,否则从屏幕边缘进入的附近物体仍可能给画面带来问题,不过很快就会在下一帧中得到纠正。
第二种方法同样也是分析深度缓冲区的值,生成一个称为直方图(histogram),记录z-depth的分布范围。 除了找到紧凑的远近平面外,图中还可能有空隙(完全没有对象)。 任何划分的平面都会被添加到存在物体的地方,从而使级联贴图具有更高的z-depth精度。
在实际中,第一种方法更为普遍,比较快( 通常每帧在1毫秒范围内),效果也好,如图7.21所示。
图 7.21 深度界限(depth bounds)的效果。左图, 没有特殊的处理对近、远平面进行调整。 右图,采用了SDSM来寻找更紧密的界限。 注意每个图像左边缘附近的窗口框, 二楼花坛下面的地方, 还有一楼的窗户, 由于松散的视图边界而导致的欠采样。 虽然渲染这些特定的图像用的是指数阴影贴图(exponential shadow maps), 但是提高深度精度的想法对于所有的阴影贴图技术都是有用的。
就像使用单一阴影贴图一样, 由于光线采样在帧与帧之间的移动而产生的闪烁是一个问题,当物体在级联之间移动时可能会变得更糟。 在世界空间中保持稳定采样的方法有很多,每种都有自己的优势。 当一个物体跨越两个阴影贴图之间的边界时,阴影的质量会发生突变。 一种解决方案是让视体略微重叠。 对这些重叠区域,采集的样本会从相邻的阴影贴图中收集并进行混合。 另一种方法是,在这个区域使用抖动(dithering)进行单一采样。
在实时渲染中, 如果在带有多个灯光的大型场景中,所有的灯光在任何时候都是活跃的,则可能会被计算淹没。 如果在视锥截体内看不见的体积空间,则该空间内的遮挡物体不需要进行计算。 Bittner等人利用遮挡剔除(章节19.7)从眼睛找到所有可见的阴影接收器, 然后从光线的角度将所有潜在的阴影接收器渲染到模板缓冲遮罩中。 这个遮罩编码了从光中可以看到的可见阴影接收器。 为了生成阴影贴图,他们使用遮挡剔除来渲染来自光的物体,并使用遮罩来剔除没有接收器的物体。各种剔除策略同样也适用于光源。 因为辐照度随着距离的平方而衰减, 一种常用的技术是在一定的阈值距离后剔除光源。 例如,第19.5节中的门户剔除技术(portal culling technique)可以发现哪些光影响哪些单元。这是一个活跃的研究领域,因为性能收益是相当可观的。