简述Shadow Mapping和Shadow Volume的新方法

本文探讨了Shadow Mapping和Shadow Volume在阴影渲染领域的最新进展,特别是高分辨率Shadow Mapping的压缩方法和基于聚类与度量树的实时阴影渲染。介绍了自顶向下和自底向上构建的压缩策略,以及四叉树压缩和树的遍历方法。此外,还阐述了基于球体和胶囊体角度距离的度量树构建,以及聚类、包围体构建和度量树遍历算法,为实时阴影渲染提供了高效解决方案。
摘要由CSDN通过智能技术生成

简介

阴影渲染在真实感图形渲染中非常重要,它作为一种视觉上的提示,将场景的空间结构和物体的相对关系反馈给用户。研究表明,阴影的有无对用户认知空间物体位置具有重要作用[1]。然而,实时、高清的阴影渲染始终是一个有挑战性的问题。早年的计算机图形学研究中,两种高效的阴影渲染技术被提了出来,分别是阴影体算法(Shadow Volume Algorithm)[2]和阴影深度映射(Shadow Depth Mapping)[3],后来许多阴影渲染技术都是在它们两者的基础上进行改进得到的。

Shadow Mapping的基本思想是,物体之所以会处在阴影当中,是由于在它和光源之间存在着遮蔽物。因此,首先我们以光源为视点,得到一个所有物体的深度图;然后将视点移回到摄像机,对每个像素点计算它和光源的距离,如果这一距离大于深度图中的深度,则说明该物体被遮挡了;否则,说明没有被遮挡。在光源不移动的情况下,无论摄像机位置如何移动,深度图都是不变的,因此将大大节省计算量。但它的缺陷是无法处理移动光源的情形。另一个缺陷在于,深度图是在像素上做的,因此天生具有锯齿。如果像素的分辨率不高,则锯齿现象会很明显;如果分辨率太高,则又很大地增加了计算量。

Shadow Volume的基本思想是,每个物体在光照下,投下了一个锥形的阴影体。如果我们观测的点在阴影体中,那么它就是阴影;否则它就不是阴影。具体实现上,可以分为Z-Pass[4]和Z-fail[5]两种方法。Z-Pass从摄像机位置出发,发射一条光线到目标点,当此光线从锥体的外部进入内部时,计数器加1;当光线从锥体内部出去时,计数器减1。显然,如果摄像机本身不在阴影中,那么如果最终计数器为正,则说明目标点在阴影中;如果计数器为0,则说明不在阴影中,如图1所示。

图1
图1. 光线从摄像机向目标点出发,每次从锥体外部进入内部则计数器加1,内部进入外部则计数器减1。由计数器为正或为零,可以判断目标点是否处于阴影中[6]。

Z-Pass不能适用于摄像机本身在阴影中的情况,因为这样计数器可能为负。为此,[5]提出了Z-Fail算法,它的光线从足够远的地方出发(来保证在非阴影区域),先经过目标点,再终止在视点。由于出发点一定在非阴影区域,因此这时计数器就不会出现负值,如图2所示。

图2
图2. 光线从远处向摄像机出发,每次从锥体外部进入内部则计数器加1,内部进入外部则计数器减1。由计数器为正或为零,可以判断目标点是否处于阴影中[6]。

本文探究Shadow Mapping和Shadow Volume在近年来的发展和进化,将以[7]和[8]为两种思路的代表方法,描述其核心思想和具体算法。

相关工作

不同的Shadow Mapping

在实际工程中,Shadow Mapping的一个主要问题在于自遮挡。由于浮点数和采样上的误差,如果我们以物体表面深度值作为Shadow Mapping中的基准,则由于小误差的缘故,一些深度略大于Shadow Mapping的表面点会被判定为在阴影中,从而形成自遮挡。[9]提出了一种中值Shadow Mapping(Midpoint Shadow Mapping)的方法,即不采用物体表面深度,而是物体和光源最近的两个面的平均,作为Shadow Mapping中的基准。这样,物体向光面和背光面的点计算时都有了一定的容错范围,解决了部分自遮挡的问题,如图3最左侧图所示。

图3
图3. 左图为中值Shadow Mapping,中图和右图为中值 Shadow Mapping可能出现的阴影点误判情况[10]

对中值Shadow Mapping方法的一个拓展是双Shadow Mapping(Dual Shadow Mapping)[10]。它是为了解决中值Shadow Mapping中仍可能出现的少数阴影点误判问题而诞生的。双Shadow Mapping除了保存了深度信息外,还维护了一个自适应的偏置量(Bias),从而在一些特殊点上也能得到正确的阴影。
在双Shadow Mapping的基础上,[11]提出了一种对Shadow Mapping进行压缩的方法CSM(Compressed Shadow Maps),[7]的方法可以视为是[11]在高维上的拓展。[12]也提出了一种通过对Shadow Mapping深度进行编码的压缩方式,但[11]和[12]都没有完全挖掘Shadow Mapping的压缩能力。

不同的Shadow Volume

Shadow Volume的第一个实现是Z-Pass[4],出于解决摄像机本身可能在阴影中的问题,[5]提出了Z-Fail算法。这两个算法往往使用OpenGL中的模板缓存(Stencil Buffer)来实现,但它们并没有解决Shadow Volume中最大的问题,即不必要地计算了许多阴影轮廓。一些方法尝试着减少部分阴影轮廓的计算,如[13]和[14],但它们仍没有完整地解决这一问题。

[15]提出了另一类基于几何体的阴影渲染方法。对于一个三角形在光照下形成的锥体,[15]使用三个面(分别对应三条边)和三角形本身,将空间划分为被该三角形遮挡的部分,和未被该三角形遮挡的部分。然后,[15]对空间建立空间二分树(Binary Space Partitioning,BSP),从而将空间划分为被遮挡区域和未被遮挡区域。同时,由于树对于检索的优良性质,我们可以高效地查找空间中一点是否属于阴影区域。然而,建立空间二分树需要对许多平面进行求交和裁剪,且需要大量的存储空间和计算代价,同时因为一些数值计算上的不稳定性,因此[15]的方法实用性不强。

[16]在[15]的基础上进行了改进。它们在BSP的基础上增加了一个表示空间相交的子树,从而构建了一棵物体三叉树(Ternary Object Partitioning,TOP)。这一改变避免了裁剪操作,因此大大提升了稳定性和效率。而[8]的方法可以视为是[16]的进一步拓展。

高分辨率Shadow Mapping压缩

高分辨率的Shadow Mapping之所以效率较低,是因为高分辨率下的深度纹理难以快速地检索和遍历。深度纹理实质上就是图片,对于图片,我们有一些多分辨率分解的方法(如小波分解或四叉树分解),它们可以将低频的信息存储在粗粒度的层级,将高频的细节存储在细粒度的层级。通常,在有损压缩下,在细粒度上系数较小的项就会被移除,只留下系数较大的项,从而提升效率,因此细粒度的层级往往是稀疏的。但是对于无损压缩,我们就不能忽略细粒度上的小系数,从而细粒度的层级就不稀疏了,消耗的代价也就更大。

[7]的主要贡献是,找到一种Shadow Map的替代品,这一替代品可以增加分解后的稀疏性。具体而言,[7]使用光线照到物体表面的入口,和穿透物体从另一侧穿出的出口之间的区间,来替代原本单一的表面。显然,这和双深度Shadow Map是一致的。对于只有单面的物体,或者不封闭的物体,我们需要一个额外的深度上限,从而在这些像素上也形成区间。完成这一替代后,我们的目标就转化为如何在一系列的区间中,嵌入连续的平面,从而增强分解后的稀疏性。[7]首先提出了两种贪心的构造方法,得到区间中的连续深度平面;然后使用四叉树对这些平面进行编码;最后提出如何在此数据结构上,进行遍历和查询,从而完成阴影的渲染。

构建

第一个需要解决的问题是,如何对每个像素计算深度区间。区间下界显然就是模型的表面(即常规Shadow Map的值),区间上界来自于模型的“第二层”,它往往采用了深度剥离(Depth Peeling)[17]的方法。考虑从光源发射向像素的光线,以及一个初始化为0的计数器。当光线和模型的外表面相交时,计数器加1;当光线和模型内表面相交时,计数器减1。当光线第一次相交到模型,这就是区间下界;当计数器的值回归到0时(这里仅考虑了封闭模型,但考虑了封闭模型相交的情况),此时的交点就是区间上界。通过Depth Peeling算法,我们可以同时在所有像素上计算深度区间,因此是非常高效的。
得到每个像素上的深度区间后,我们需要找到一个区间内的、简化的、分解后稀疏的平面。然而,在模型简化问题中,对给定包围壳中简化表面的寻找,是一个已被证明的NP难的问题[18]。因此,[6]提出了两个基于贪心算法的自顶而下和自底向上的算法,从而完成高效的平面寻找。

自顶而下的构建

自顶而下的构建从最粗的粒度,即整个Map开始。对于所有的区间,我们找到一个最合适的深度值,它落在尽可能多的区间内。对于包含这一深度值的区间,它们将被标记,并将以此深度作为自身的深度;对于不包含这一深度值的区间,它们将被放在粒度更细的下一层进行处理。对于下一层,我们采用四叉树的方法,将Map分成了四块,对每块重复上述的操作,直到到达最细粒度的一层(即每个像素)或一整块所有像素均已被标记。它的伪代码如下:

算法1
算法1. 自顶而下的构建

上述过程没有描述如何找到“最合适”的深度值。一种简单的思路是,我们将深度离散化,然后遍历每一个区间,构建直方图统计。显然,“最合适”的深度值就是直方图中最高的那一个。然而,均匀离散化代价太大,因此我们对每个区间的上界和下界进行排序,并统计直方图,可以更高效地得到“最合适”的区间。如图4所示。

图4
图4. 左图为各个深度区间,右图为深度区间的直方统计,从而找到“最合适”的深度

这一算法在最初需要 的复杂度用于排序,然后每一层需要遍历每个区间进行统计,复杂度为 ,而一共有 层,因此总复杂度仍为 。

自底向上的构建

自底向上的构建从像素开始。每次我们考察邻近的四个像素,类似地找到一个“最合适”的最大深度子区间,使它落在尽可能多的像素深度区间内。对于那些包含“最合适”区间的像素,我们就不再额外记录其区间值;对于不包含“最合适”区间的像素,我们将其保留。我们逐层向上,重复这样的构建,直到到达最粗粒度的层。它的过程如图5所示。

图5
图5. 从左至右表明了自底而上的构建方法

由于一次只需要在四个区间内选择“最合适”的子区间,我们就不需要进行耗时的排序操作了。同时,虽然它的复杂度和自顶而下的构建一样(都是 ),但由于它的方法更适用于并行的架构,因此可以使用GPU进行加速。

四叉树压缩

四叉树包含了三种节点:叶子节点、内部节点和空节点。叶子节点仅含有32位的深度值。内部节点不仅含有32位的深度值,还需要保存树的连接信息。空节点不含有深度值,但也需要保存树的连接信息。

对于内部节点和空节点,我们使用8位来存储其4个子节点的状态(每个子节点2位),状态共有4种,即子节点不存在、子节点为叶子节点、子节点为内部节点和子节点为空节点。另外,有16位用于存储第一个子节点的指针,由于子节点和它的兄弟节点连续存储,因此只需要存储第一个子节点就够了。考虑到4字节的对齐,我们还要给内部节点和空节点加上1字节的空间(或者也可以将16位指针扩展到24位,但[6]中实验证明区别不大)。总之,内部节点占用了8个字节,空节点和叶子节点分别占用了4个字节。

树的遍历

树的遍历和普通四叉树遍历没有什么不同。对每一个节点,我们可以通过8位的子节点状态,加上16位的首子节点的指针,计算出每个子节点的指针,然后继续迭代。

树的检索

高效的检索也是Shadow Mapping的一个重要组成部分。出于避免锯齿的考虑,通常我们会采用PCF(Percentage Closer Filtering)[19]的方法。PCF方法指判断阴影时,并不判断一个像素是否在阴影区域中,而是在一个以该像素为中心的固定窗口中,对邻近像素的Shadow Map深度值求平均,以此平均数来判断是否为阴影。显然,它可以一定程度上解决锯齿的问题。

Shadow Map堆

二维上的四叉树压缩方法,可以很容易地拓展到三维的八叉树方法。我们可以将一系列Shadow Map叠成一个堆,然后用八叉树对整体进行类似的压缩。这样的方法可以渲染出面光源软阴影的效果,也可以渲染移动光源的效果。对于面光源,我们在光源上进行采样,从而将其视为多个带权的点光源;对于移动光源,我们需要提前获取光源的运动轨迹,提前计算好每个轨迹下的Shadow Map。面光源的效果如图6左图所示,移动光源效果如图6右图所示。

图6
图6. 左图为面光源的效果,右图为移动光源的效果

基于聚类和度量树的实时阴影渲染方法

[8]的算法主要分成三步:聚类、度量树构建、度量树遍历。聚类是一个预处理的步骤,它将邻近的三角形面片聚类成一个簇,然后基于簇的包围体来计算阴影。聚类需要一定的权衡,即一方面聚类可以简化模型,聚类程度越高模型越简单;另一方面聚类后形成的包围体会给阴影带来误差,聚类程度越高也就意味着误差越大。[8]中选择了球状和胶囊状的包围体,也是出于平衡效率与效果的考虑得到的。

对于聚类好的簇,[8]定义了一个新的度量方式,并在新的度量方式下,对簇构建了树状结构。之所以需要新的度量方式,是因为在光源的视角下,球状和胶囊状的包围体将变成圆柱形和胶囊圆柱形,从而过去的度量方式不再适用了。[8]提出了新的基于角度的度量方式。

对于每个像素点,我们需要确定它们是否在阴影之中,因此,我们需要知道它们和簇之间的相对位置关系。由于有了度量树的数据结构,我们可以递归地完成这一遍历步骤。

聚类

聚类可以更加高效。对于 n n n个三角形面片,假设建立一棵树需要 O ( n log ⁡ n ) O\left( {n\log n} \right) O(nlogn)的复杂度,那么,当我们有一个聚类算法,每个簇中含有 p p p个三角形面片,再对簇进行建树,则复杂度将下降到 O ( n p log ⁡ n p ) O\left( { {n \over p}\log {n \over p}} \right) O(pnlogpn)。[8]的算法不依赖特定的聚类算法,但显然,不同的聚类算法将产生不同的结果。

[8]采用的聚类算法如下。首先通过k-mean++算法找到一组中心点,这些中心点并不是随机分布的,而是彼此间距尽可能大,从而保证中心点尽量均匀地分布。然后,每个三角形面片被分配给距离它最近的中心点,形成这一轮迭代的簇。再次,中心点被重新调整为实际簇中各个三角形的中心。然后我们重复上述的步骤,直到结果收敛或每个簇中的三角形面片数量小于指定的阈值。

包围体构建

我们需要对每个簇构建包围体。我们期待包围体尽可能紧密地包围着簇,又希望包围体的结构尽可能简单,从而降低存储和运算时间。常见的包围体有轴对齐包围盒(Axis Aligned Bounding Boxes,AABB)和方向包围盒(Oriented Bounding Boxes,OBB)。但是,考虑到我们更关心在光线透射下阴影的形状,AABB和OBB投影后都成为了平截头体,因此形状并不简单。作为替代,[8]采用了球状包围体,这样投影后的形状就是圆柱体。但是球形常常不能很好地紧密包围三角形面片,因此[8]还采用了胶囊状的包围体。

我们还需计算簇的均值与方差。假设簇中的第 i i i个三角形面片的三个点为 p i p^i pi q i q^i qi r i r^i ri,则它们的均值 μ \mu μ和方差 C C C的计算方式如下:

μ = 1 3 n ∑ i = 0 n ( p i + q i + r i ) \mu = {1 \over {3n}} \sum_{i = 0}^n \left( { {p^i} + {q^i} + {r^i}} \right) μ=3n1i=0n(pi+qi+ri)
C j k = 1 3 n ∑ i = 0 n ( p ′ j i p ′ k i + q ′ j i q ′ k i + r ′ j i r ′ k i ) {C_{jk}} = {1 \over {3n}} \sum_{i = 0}^n \left( {p'}_j^i {p'}_k^i + {q'}_j^i {q'}_k^i + {r'}_j^i {r'}_k^i \right) C

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值