11.5 漫反射全局光照
接下来几个小节将介绍的各种方法,它们不仅可以实时模拟遮挡效果,还可以模拟完整的光线弹射。它们可以大致分为两种算法,它们各自拥有不同的假设,即光线在到达眼睛之前,从一个漫反射表面反射回来,还是从一个镜面反射回来。相应的光线路径可以表示为或者,其中有许多方法都对早期的反弹类型进行了限制。第一组解决方案假设光线的入射方向在着色点上半球范围内平滑变化,或者直接忽略这种变化。第二组解决方案则假设光线的入射方向具有很高的变化率,这个假设依赖于这样的一个事实,即光线只会从一个相对较小的立体角中照射到着色点上。由于这两种约束条件的差别很大,因此将它们分开处理是有益的。我们在本小节中介绍漫反射全局照明的方法,在下一节中介绍镜面全局光照的方法,然后在最后一节中介绍未来很可能会应用的统一方法。
11.5.1 表面预照明(Surface Prelighting)
辐射度算法和路径追踪算法都是为离线使用而设计的。虽然已经有了一些在实时环境中使用它们的尝试,但是结果仍然太不成熟,无法用于实际的产品中。目前最为常见的做法是使用它们来预先计算与光照相关的信息。这个昂贵的离线过程是预计算的,计算出来的结果会被存储起来,然后在显示期间使用,从而提供高质量的光照效果。正如章节11.3.4中所述,以这种方式对静态场景进行预计算的过程被称为烘焙(baking)。
这种做法有一定的限制。如果我们提前进行光照计算,那么我们将无法在运行过程中更改场景的设置。场景中的所有几何体、灯光和材质都需要保持不变,我们无法改变一天中的时间,也不能在墙上炸一个洞。但是在许多情况下,这种限制是一种可以接受的权衡,例如:建筑可视化的相关应用可以假设用户只在虚拟环境中走动;游戏同样也会限制玩家的行动等。在这样的应用中,我们可以将几何物体分为静态物体(static)和动态物体(dynamic)。在预计算过程中会使用静态物体来计算光照,让它们与光源充分进行交互作用。比如静态的墙壁会投下阴影,静态的红地毯反射出红光。动态物体只会充当接收者,它们不会遮挡光线,也不会产生间接的光照效果。在这样的场景中,动态几何物体的尺寸通常会被限制得相对较小,这样可以忽略它们对光照的影响,或者是使用其他技术来进行建模,从而最小化光照质量的损失。例如:动态几何物体可以使用屏幕空间中的一些方法来生成遮挡效果。常见的动态物体包括角色、装饰性的几何体以及车辆等。
可以进行预计算的、最简单的照明信息就是irradiance。对于一个平坦的Lambertian表面,irradiance和表面颜色一起,完整描述了材质对于光照的反应。因为光源的照明效果是彼此独立的,所以动态光源可以被添加在预计算的irradiance之上,如图11.22所示。
1996年的《雷神之锤》和1997年的《雷神之锤2》是第一个使用预计算irradiance的商业交互式应用程序。Quake预先计算了静态光源的直接贡献,这主要是作为一种提高性能的方法。《雷神之锤2》还计算了一个间接分量,使其成为第一款使用全局光照算法来生成更加真实光照的游戏。它使用了一种基于辐射度的算法,因为这种技术非常适合用于计算Lambertian环境中的irradiance。此外,由于内存的时间限制,光照的分辨率相对较低,这与辐射度解决方案中典型的模糊、低频阴影匹配得很好。
预计算的irradiance通常会和漫反射颜色或者albedo贴图相乘,并单独存储在一个纹理集合中。虽然在理论上可以预先计算出辐射出度(exitance,等于irradiance乘以漫反射颜色),并将其存储在一组纹理中,但是在大多数实际情况下,许多应用都没有采用这个做法。因为颜色贴图的使用频率通常会很高,它们利用了各种类型的分块平铺,并且其中的部分区域经常会在模型之间进行重复使用,所有的这些操作都是为了保持合理的内存使用。而irradiance的使用频率则要低得多,重复使用的情况也很少。因此将光照信息和表面颜色分开存储,可以消耗更少的内存空间。
除了限制最为严格的硬件平台之外,如今已经很少使用预计算irradiance的方法了。因为根据定义,irradiance是针对给定的法线方向进行计算的,这意味着我们无法对物体的表面法线进行修改,我们无法使用法线映射来提供高频的表面细节。这也意味着只能对平面进行预计算irradiance。如果我们需要在动态几何物体上使用烘焙光照,我们就需要其他的方法来存储这些光照信息。这些限制条件促使人们寻找一种方法,来存储带有方向分量的预计算光照。
11.5.2 定向表面预照明
为了在Lambertian表面上使用预照明和法线映射,我们需要一种方法来表示irradiance随表面法线的变化。为了给动态几何物体提供间接光照,我们还需要在每个可能的表面方向上进行计算。幸运的是,我们已经有了各种工具可以用于表示这样函数。在章节10.3中,我们描述了根据法线方向确定光照的各种方法。这些方法中包括了针对半球函数域的专门解决方案,就像不透明表面的情况一样,球体下半部分的值是无关紧要的。
最常用的方法是存储完整的球面irradiance信息,例如使用球谐函数来进行存储。该方案首先是由Good和Taylor [564]在加速光子映射(photon mapping)的背景下提出的,并被Shopf等人[1637]在应用在了实时场景中。在这两种情况下,定向的irradiance都会存储在纹理中。如果采用9个球谐系数(即三阶SH),可以获得较好的质量,但是存储和带宽的成本较高。如果只使用四个系数(即二阶SH)的话,虽然成本较低,但是会丢失许多细节信息,光线的对比度较低,法线贴图也不太明显。
Chen [257]使用了《光环3》方法的一种变体,这种方法的目的是以较低的成本来实现三阶SH的质量。他从球面信号中提取出最主要的光照,并将其分离存储为一个颜色和一个方向。剩余的基底则使用二阶SH来进行编码,使用这种方法,可以将系数的数量从27个减少到18个,而且质量损失很小。Hu [Lightmap Compression in Halo 3", Yaohua Hu, GDC 2008]描述了如何对这些数据进行进一步地压缩。Chen和Tatarchuk [258]在生产环境中使用了基于GPU的烘焙管线,他们提供了进一步的信息。
Habel等人[627]所提出的H-basis是另一种可选的解决方案。由于它只对半球面上的信号进行编码,因此可以使用较少的系数提供与球谐函数相同的精度。仅仅使用六个系数就可以获得与三阶SH相当的质量。因为H-basis只会针对一个半球进行定义,所以我们需要表面上的一些局部坐标系来正确地确定它的朝向。通常,由参数化所产生的切线坐标系可以用于此目的。如果想要在纹理中存储H-basis的分量,那么纹理的分辨率应当足够高,从而适应底层切线空间的变化。如果不同切线空间中的多个三角形覆盖了同一个纹素,那么重建出的信号将会是不精确的。
球谐函数和H-basis的一个问题是,它们都会出现振铃现象(章节10.6.1)。虽然预过滤可以减轻这种现象,但它也会使光照变得更加平滑,这可能并不总是我们想要的。此外,即使是成本较低的变体方法,在存储和计算方面仍然具有相对较高的成本。在一些限制更加严格的情况下,例如在低端平台或者虚拟现实平台上,这种开销可能会令人望而却步。
成本是那些简单替代方案最重要的流行原因。《半条命2》使用了一个自定义的半球基底(章节10.3.3),每个样本存储了三个颜色值,总共有九个系数。尽管AHD(章节10.3.3)很简单,但它也是一个较为流行的选择,它被用在了许多游戏中,例如使命召唤系列[809, 998]和《最后生还者》[806],如图11.23所示。
Crytek在游戏《孤岛惊魂》[806]中使用了一个变体。这种Crytek表示方法包含了切线空间中的平均光线方向、平均光线颜色和一个标量的方向因子。其中最后一个值用于混合环境(ambient)项和定向(directional)项,它们都使用了相同的颜色。这样可以将每个样本的存储空间减少到6个系数:3个系数用于颜色,2个系数用于方向,1个用于方向因子。Unity引擎在它的其中一个模式中也使用了类似的方法[315]。
这种类型的表示方法是非线性的,这意味着,在技术上而言,对单个组件进行线性插值(无论是在纹素之间还是顶点之间)在数学上是不正确的。如果主要光源的方向变化很快,例如在阴影边界上变化很快,那么在阴影中很可能会出现视觉瑕疵。尽管有这些不准确的地方,但是最终的结果在视觉上还是令人满意的。由于环境光照和定向光照区域之间具有较高的对比度,法线贴图的效果会被增强,这通常是我们想要发生的。此外,定向光照的分量还可以用于计算BRDF的高光响应,这可以为低光泽材质的环境贴图提供一种低成本的替代方案。
在这类算法谱系的另一端,是为高质量视觉表现而设计的方法。Neubelt和Pettineo [1268]在游戏《教团:1886》中,使用纹理贴图来存储球面高斯函数的系数,如图11.24所示。他们存储的是入射radiance,而不是irradiance,radiance会被投影到一组高斯波瓣上(章节10.3.2),它被定义在一个切线坐标系中。根据具体场景中光照的复杂程度,他们会使用5到9个波瓣。为了产生漫反射响应效果,球面高斯函数会与沿表面法线方向的余弦波瓣进行卷积。通过将高斯函数与镜面BRDF波瓣进行卷积,这种表示方法也足够精确,可以提供低光泽的高光效果。Pettineo详细描述了整个系统[1408],他还提供了一个应用程序的源代码,这个应用程序能够烘焙和渲染不同的光照表示方法。
如果我们需要着色点任意方向上的光照信息,而不仅仅是在着色点是半球范围内的光照信息(例如:为动态几何物体提供间接照明),那么我们可以使用一些编码完整球面信号的方法。这里自然而然会提到球谐函数。当不太关心内存开销的时候,三阶SH(每个颜色通道有9个系数)是最流行的选择;否则也可以使用二阶SH(每个颜色通道有4个系数,这与RGBA纹理的通道数量相匹配,因此一个贴图可以存储一个颜色通道的SH系数)。球面高斯函数也适用于完整球体的情况,因为波瓣可以分布在整个球体上,或者也可以只分布在法线周围的半球上。然而,由于需要被波瓣覆盖的立体角是球面技术的两倍,因此可能需要使用更多的波瓣来保持相同的质量。
如果我们想避免处理振铃问题,同时又负担不起使用大量波瓣所带来的开销,那么环境立方体(章节10.3.1)是一个可行的选择[1193]。它由六个clamped 波瓣组成,它们都沿着主轴方向。每个余弦波瓣只覆盖一个半球,即它们具有局部性(local support),这意味着它们只在其球面域的一个子集上具有非零值。因此,在重建过程中只需要使用6个存储值中的3个可见波瓣即可,这限制了光照计算的带宽成本。其重建质量与二阶球谐函数相类似。
环境骰子[808](章节10.3.1)可以生产比环境立方体更高质量的结果。该方案采用了12个沿二十面体顶点方向的波瓣,这些波瓣是和波瓣的线性组合。在重建期间会使用12个存储值中的6个,其重建质量可以与三阶球谐函数相媲美。这些表示方法和其他的类似表示方法(例如:由三个波瓣和一个波瓣所组成的基底,它们被扭曲从而覆盖整个球面)已经在许多商业成功的游戏中进行了使用,例如《半条命2》[1193],使命召唤系列[766, 808],《孤岛惊魂3》[533],《全境封锁》[1694]和《刺客信条4:黑旗》[1911]等。
11.5.3 预计算传输
虽然上述的预计算光照看起来很惊艳,但是它本质上还是静态的。任何几何物体或者光照的改变都会使整个解决方案失效。就像在现实世界中一样,拉开窗帘(场景中几何物体的局部变化)可能会让整个房间充满光线(光照的全局变化)。人们花费了大量的研究工作来寻找能够允许某些类型变化的解决方案。
如果我们假设场景中的几何物体没有发生变化,只有光照发生了变化,那么我们可以对光线与模型的相互作用进行预计算。物体之间的影响(例如相互反射或者次表面散射),可以预先进行一定程度的分析,并将结果存储下来,而不需要对实际的radiance进行操作。接收入射光线,并将其转换为整个场景的radiance分布,这个函数被称为传输函数(transfer function)。这样的方法被称为预计算传输(precomputed transfer)或者预计算radiance传输(precomputed radiance transfer,PRT)。
与之前所介绍的完全离线的烘焙光照不同,这类技术确实具有明显的运行时间开销。当在屏幕上显示场景时,我们需要计算特定光照环境中的radiance。为了实现这一点,我们需要将一定数量的直接光源“注入”到系统中,然后应用传输函数来将其传播到整个场景中。有些方法会假设这种直接光照来自于环境贴图,还有一些其他的解决方案允许任意的光照设置,并且能够以灵活的方式来进行改变。
Sloan等人[1651]将预计算radiance传输的概念引入了图形学,他们使用球谐函数来描述它,但是这个方法其实不必使用球谐函数。该方法的基本思想很简单,如果我们使用一定数量(最好是数量较少)的“构件(building block)”光源来描述直接光照,那么我们就可以对场景如何被这些光源单独照亮进行预计算。想象一下,现在房间里有三台电脑显示器,每个显示器只能显示一种颜色,但是其亮度可以发生变化。我们将每个显示器的最大亮度设为1,即归一化的“单位”亮度。我们可以独立地预计算每个显示器对房间照明的影响,这个过程可以使用章节11.2中介绍的方法来完成。因为光线的传输是线性的,所以三个显示器同时照亮场景的结果,就相当于每个显示器直接或者间接发出的光线总和。并且显示器的光照彼此相互独立,互不影响,因此如果我们将其中一个显示器设置为最大亮度的一半,那么这样做只会改变这个显示器对总照明的贡献,并不会影响其他的显示器。
这样做允许我们快速计算整个房间内的全部反弹光线。我们将每个预计算的光源解决方案,乘上显示器的实际亮度,然后再对这些结果进行求和。我们可以打开或者关闭显示器,让它们变得更亮或者更暗,甚至是改变它们的颜色,想要获得最终的光照效果,我们只需要做这些乘法和加法即可,如图11.25所示。
上述过程可以写出如下数学形式:
其中是点的最终radiance;是来自显示器的预计算单位(归一化)贡献;是该显示器的当前亮度。这个方程在数学意义上定义了一个向量空间(vector space),是这个空间中的基向量。任何可能的光照效果,都可以通过这些光源贡献的线性组合来生成。
Sloan等人[1651]的原始PRT论文使用了与上文相同的推理过程,但是具体的背景有所不同,他们使用球谐函数来表示的无限远的环境光照。同时,他们没有存储场景对显示器的响应,而是存储场景对于周围光线的响应,并使用球谐函数来定义周围光线的分布。通过对一些SH波段进行这样的操作,他们可以渲染一个被任意光照环境照亮的场景。他们将这种光照投影到球谐函数上,将每个结果系数乘以其各自的归一化“单位”贡献,然后再将结果加在一起,就像是我们对显示器所做的那样。
请注意,用于将光线“注入”到场景中的基底表示,与用于表达最终光照的表示是独立的。例如:我们可以使用球谐函数来描述场景是如何被照亮的,但是选择另一种基底来存储到达任意给定点上的radiance。假设我们使用一个环境立方体来进行存储,我们会计算有多少radiance会从顶部到达着色点,有多少radiance会从两侧到达着色点。每个方向上的传输都会单独进行存储,而不是作为表示总传输的单个标量值。
Sloan等人[1651]的PRT论文分析了两个案例。第一种是当接收基底只是表面上的一个标量irradiance时,此时接收物需要是一个完全漫反射的表面,并且需要具有预先定义好的法线,这意味着它无法使用法线贴图来获取精细尺度的细节。传输函数的形式是:输入光照的SH投影与预计算传输向量之间的点积。其中后者可以在整个场景中进行空间变化。
如果我们需要渲染非Lambertian材质,或者要使用法线映射,那么我们可以使用第二种变体。在这种情况下,周围光照的SH投影会被转换为给定点入射radiance的SH投影。因为这个操作为我们提供了整个球面上(或者半球,如果我们处理的是静态不透明物体的话)的完整radiance分布,我们可以将其与任何BRDF进行正确地卷积。此时的传输函数会将SH向量映射到其他的SH向量上,它具有一个矩阵乘法的形式,但是这种乘法操作的成本是很高的,无论是计算量还是内存开销。如果我们对源基底和接收基底都使用三阶SH,那么我们需要为场景中的每个点都存储一个的矩阵,并且这些数据仅仅用于黑白(monochrome)传输。如果我们想要实现彩色效果,那么我们就需要3个这样的矩阵,这样每个点需要的内存量就十分惊人了。
一年以后,Sloan等人[1652]解决了这个问题。他们没有直接存储传输向量或者传输矩阵,而是使用主成分分析(principal component analysis,PCA)技术对整个集合进行了分析。这里的传输系数可以被认为是多维空间中的点(例如:矩阵意味着空间是81维),但是这些点在该空间中并不是均匀分布的。它们会形成维数较低的簇,这种聚类就像是沿着直线分布的三维点一样,实际上它们都位于三维空间的一维子空间中。PCA可以有效地检测出这种统计意义上的关系。一旦PCA发现了子空间,就可以使用更少的坐标来表示这些点,因为我们可以使用更少的维度来存储子空间中的位置。用刚才的直线例子来类比,我们不需要使用三个坐标来存储一个点的完整位置信息,我们只要存储该点沿直线的距离即可。Sloan等人使用这种方法,将传输矩阵的维度从625维(传输矩阵)降低到256维。虽然这个维度对于常见的实时应用程序而言还是太高了,但是它为后续方法拓展了思路,许多后来的光线传输算法均采用了PCA来作为数据压缩的一种方式。
这种降维存储本质上是有损压缩的。在极少数情况下,数据点会形成完美的子空间;但是大多数情况下它只能对原始数据进行近似,因此将原始数据投影到子空间中的数据上会导致一些退化。为了提高质量,Sloan等人将一组传输矩阵划分为若干个簇,分别对每个簇进行PCA操作。这个过程还包括了一个优化步骤,以确保聚类边界上不会出现不连续现象。他们还提出了一种允许物体发生有限形变的扩展变体,被称为局部可变形预计算radiance传输(local deformable precomputed radiance transfer,LDPRT)[1653]。
PRT已经在一些游戏中以各种形式进行了使用。PRT在玩法侧重于户外区域的游戏中尤其受欢迎,因为这些区域的时间和天气条件都是动态变化的。《孤岛惊魂3》和《孤岛惊魂4》都使用了PRT,其中源基底是二阶SH,接收基底是一个自定义的四方向基底[533, 1154]。《刺客信条4:黑旗》使用一个基底函数作为来源(太阳颜色),在一天中的不同时间中对传输进行了预计算。这种表示方式可以理解为在时间维度上来定义源基底函数,而不是在方向维度上。《刺客信条4:黑旗》中的接收基底与《孤岛惊魂》系列中所使用的相同。
SIGGRAPH 2005关于预计算radiance传输的课程[870],对这个领域的研究进行了很好地综述。Lehtinen [1019, 1020]给出了一个数学框架,这个框架可以用来分析各种算法之间的差异,并据此开发新的算法。
原始PRT方法假设周围的光照是无限远的。虽然这个模型可以很好的模拟室外场景的光照效果,但是它对室内环境的限制太大了。然而,正如我们之前所提到的,这里的核心概念是:光照的初始来源是完全不可知的。Kristensen等人[941]描述了一种方法,该方法对一组分散在整个场景中的光源进行了PRT计算。这对应了存在大量的“源”基底函数,然后这些光源会被组合成聚类,接收光照的几何物体也会被划分到若干个区域中,每个区域中的物体都会受到不同光源子集的影响。这个过程会显著压缩传输的数据。在运行过程中,会通过从预计算集合中对最近的光源进行插值,从而来近似计算放置在任意位置上的光源所产生的光照效果。Gilabert和Stefanov [533]在游戏《孤岛惊魂3》中使用了这种方法来生产间接光照效果。但是这种方法的基本形式只能处理点光源,无法处理其他类型的光源。虽然这类方法也可以进行扩展,从而支持其他类型的光源,但是其开销会随着每个光源的自由度成指数级增长。
到目前为止所讨论的PRT技术预计算了来自一些元素之间的传输函数(向量和矩阵),然后会将其用于模拟光源。另一类流行的方法是对表面之间的传输进行预计算,在这种类型的系统中,光照的实际来源变得无关紧要。可以使用任何类型、任意位置的光源,因为这类方法的输入是来自某些表面集合的出射radiance(或者其他相关的物理量,例如irradiance,如果方法假设只存在漫反射表面的话)。这些直接光照的计算,可以使用阴影(第7章)、irradiance环境贴图(章节10.6),或者本章前面所讨论的环境光遮蔽和定向遮蔽等方法。场景中的任何表面,可以通过设置其出射radiance,来将其转换为一个面光源。
根据这些原则设计实现的系统,其中最流行的就是由Geomerics开发的Enlighten,如图11.26所示。虽然该算法的确切细节从未完全公开过(不开源),但是许多演讲和演示都准确描述了该系统的原理[315, 1101, 1131, 1435]。这个系统应用于早期版本的Unity引擎中。
为了实现光线传输的目的,我们假设场景中的表面都是Lambertian的。使用Heckbert的符号表示法,这里我们处理的路径集合是,因为眼睛所看到的最后一个表面不需要是纯漫反射的,只是在计算光线传输的时候,光线会在场景中的漫反射表面上进行弹射预计算。系统定义了一组“源”元素和另一组“接收”元素。源元素存在于表面上,并共享表面上的一些属性,例如漫反射颜色和表面法线。预处理步骤会计算光线在源元素和接收元素之间的传输情况和传输信息。这种信息的确切形式取决于源元素具体是什么,以及用于在接收器上收集光照的基底是什么。在最简单的形式中,源元素可以是点,然后我们会在接收位置处生成irradiance;在这种情况下,传输系数就是源和接收物之间的相互可见性。在运行过程中,会将所有源元素的出射radiance提供给系统,根据这些信息,我们可以利用预计算的可见性,以及已知的源和接收物的位置、方向等信息,来对反射方程(方程11.1)进行数值积分。使用这种方法,就完成了光线的一次弹射,由于大多数间接光照效果都来源于第一次弹射,因此仅仅执行一次弹射就足以提供合理的光照效果了。然而,我们可以使用这个光线,再次运行传播步骤来生成第二次反弹的光线。这个过程通常是在几帧中完成的,其中上一帧的输出会作为下一帧的输入。
使用点作为源元素会产生大量的连接(connection)。为了提高性能表现,法线和颜色相似区域的聚类(簇)也可以用作源集合。在这种情况下,传输系数与辐射度算法中所看到的形状因子相同(章节11.2.1)。请注意,尽管二者有相似之处,但是该算法与经典的辐射度算法是不同的,因为它每次只会计算一次弹射的光线,并且不涉及求解线性方程组的问题。该算法借鉴了渐进辐射度(progressive radiosity)的思想[275, 1642]。在这个系统的一次迭代过程中,一个patch可以确定它能够从其他patch接收到多少能量。将radiance传输到接收位置的过程被称为聚集(gathering)。
接收元素处的radiance可以使用不同的形式进行聚集。向接收元素的传输过程,可以使用我们之前所描述过的任何定向基底。在这种情况下,原来的单个系数会变成一个向量,其维数等于接收基底中的函数数量。当使用定向表示方法来执行聚集操作的时候,生成的结果与章节11.5.2中所描述的离线解决方案相同,因此它可以与法线映射方法一起使用,也可以提供低光泽材质的高光响应。
在许多变体中都使用了这个思想。为了节省内存,Sugden和Iwanicki [1721]使用了SH传输系数,对它们进行了量化,并将它们间接地存储为调色板中某个记录(entry)的索引(index)。Jendersie等人[820]构建了一个包含源patch的层次结构,当子patch所覆盖的立体角太小时,会将高层元素的引用存储在这个树中。Stefanov [1694]引入了一个中间步骤,其中表面元素的radiance首先会传播到场景的体素化表示中,然后再作为传输的源。
(在某种意义上)将表面划分为源patch的理想分割方式,取决于接收物的位置。对于距离较远的元素而言,将它们作为独立的实体会产生不必要的存储成本,但是当近距离观察它们的时候,则应当将其单独对待。层次化的源patch在一定程度上缓解了这个问题,但是并不能完全解决它。能够为特定接收物进行组合的patch,它们可能要相距足够远才能防止这种合并。Silvennoinen和Lehtinen [1644]提出了一种解决该问题的新方法。该方法没有显式地创建源patch,而是为每个接收位置生成一组不同的patch。物体会被渲染到散布在场景周围的一组稀疏环境贴图中。每个贴图都会被投影到球谐函数上,而这个低频版本则会“虚拟(virtually)”投影回环境中。接收点会记录它们能够看到多大的投影,并且这个过程会针对每个发送者的SH基函数分别完成。这样做会根据环境探针(probe)和接收点的可见性信息,为每个接收物创建一组不同的源元素。
由于源基底是由投影到SH的一个环境贴图构成的,因此它很自然地结合了更远的表面。为了选择要进行使用的探针,接收物会使用一种倾向于附近的探针的启发式方法,这使得接收物可以以相似的尺度来“观察”环境。为了限制必须存储的数据量,会使用聚类PCA对传输信息进行压缩。
Lehtinen等人[1021]描述了另一种形式的预计算传输方法。在这种方法中,源元素和接收元素都不位于网格上,而是位于体积中,因此可以在三维空间中的任何位置上进行查询。这种形式可以很方便地在静态物体和动态物体之间提供一致的光照效果,但是其计算量相当大。
Loos等人[1073]预计算了具有不同侧壁(side wall)配置的、模块化的、单元格内的传输。然后将多个这样的单元格缝合和扭曲,从而对场景的几何形状进行近似。radiance首先会传播到单元格边界处,然后使用预计算模型来将其传播到邻近的单元格中。这种方法的计算速度很快,即使是在移动平台上也可以有效运行,但是其结果质量较低,可能无法满足要求更高的应用程序。
11.5.4 存储方法
无论我们是想使用完全预计算的光照,还是对传输信息进行预计算,从而允许一些光照的变化,其生成的结果数据都必须要以某种形式进行存储,同时这种形式必须是GPU友好的。
光照贴图(light map)是存储预计算光照最常用的方法之一,它们是存储了预计算信息的纹理。虽然有时像irradiance贴图这样的术语,会用来表示存储特定类型的数据,但是术语光照贴图可以对这些数据和纹理进行统称。在运行过程中,会使用GPU内置的纹理机制,获取到的值通常都是双线性过滤的,这在某些情况下可能并不是完全正确的。例如,当我们使用AHD表示方法时,经过滤波后的D(方向)分量在插值之后将不再是单位长度,因此需要重新对其进行归一化。使用插值也意味着A(环境)和H(高亮)与我们在直接在采样点计算它们所获得的结果相比,也并不是完全相同的。但是,即使表示方法是非线性的,但是结果通常看起来也还能接受。
在大多数情况下,光照贴图都不会使用mipmap,通常而言都是没有必要的,因为与常见的albedo贴图或者法线贴图相比,光照贴图的分辨率都很小。即使在一些高质量的应用程序中,光照贴图中单个纹素所覆盖的面积至少也有厘米,甚至是更多。对于这种尺寸的纹素而言,几乎不需要添加额外的mipmap层级。
为了在纹理中存储光照信息,常见中的模型物体需要提供一个唯一的参数化(unique parameterization)。在将一个漫反射颜色纹理映射到一个模型上的时候,对于网格的不同部分使用相同的纹理区域,这样做通常是比较好的,尤其是当模型被一个包含重复图案的贴图纹理化时。但是想要重复使用光线贴图是非常困难的,网格上每个点的光照情况都是唯一的,因此场景中的每个三角形,都需要在光照贴图上占据一块属于自己的唯一区域。创建一个参数化的过程,最开始是将网格分割为更小的块,这可以使用一些启发式方法来自动完成[1036],也可以在创作工具中手动完成。通常情况下,其他纹理映射中已经包含了一部分的分割信息,这部分信息也会被使用。接下来,每个块都会被独立地参数化,从而确保它在纹理空间中不会发生重叠[1057, 1617]。在纹理空间中产生的元素会称为图表(chart)或者壳(shell)。最后,所有生成的chart都会被打包到同一个纹理中,如图11.27所示。
必须要小心确保chart之间不会发生重叠,并且chart之间的过滤占用空间(filtering footprint)也必须保持相互独立。当渲染一个给定chart的时候(双线性过滤会访问四个相邻的纹素),其他所有可以被访问的纹素都应该被标记为已使用,这样就不会有其他chart与它们发生重叠。否则,chart之间可能会出现颜色溢出现象,即其中一个chart的光照可能会出现在另一个chart中。对于光照贴图系统来说,提供一个用户可以控制的“排水沟(gutter)”量,用于调整光线贴图chart之间的间距,虽然这种做法十分常见,但是这种chart的分离是没有必要的。一个chart正确的过滤占用空间,可以通过使用一套特殊的规则,在光照贴图空间中通过光栅化来自动确定,如图11.28所示。如果以这种方式光栅化的shell不会发生重叠,那么我们就可以保证不会发生颜色溢出现象。
避免颜色溢出是光照贴图很少使用mipmap的另一个原因。chart的过滤占用空间需要在所有mipmap层级上都保持独立,这将会导致shell之间的间距过大。
将chart打包到纹理中的最优方法是一个NP-完全问题,这意味着没有任何已知的算法能够产生一个具有多项式级别复杂度的解。在实时应用程序中,单个纹理可能就会包含数十万个chart,所有现实世界的解决方案,都使用了微调的启发式方法和精心优化的代码来快速进行打包[183, 233, 1036]。如果这些光照贴图稍后会进行分块压缩(章节6.2.6),那么为了提高压缩质量,还可以向打包器添加一些额外的约束,从而确保单个块中只包含类似的值。
光照贴图的一个常见问题是接缝(seam,如图11.29所示)。因为模型网格被分割成了不同的chart,并且每个chart都是独立进行参数化的,所以不可能确保沿分割边缘两侧的光照效果是完全相同的,这种情况会表现为视觉上的不连续性。如果模型网格是手动分割(参数化)的,可以通过将它们的接缝设置在不可见的区域,从而来避免这个问题。然而,这样做是一个费时费力的过程,并且无法应用在自动生成参数化表示的过程中。Iwanicki [806]对最终生成的光照贴图进行后处理,对沿着分割边缘的纹素进行修改,从而最小化两侧插值结果的差异。Liu和Ferguson等人[1058]通过等式约束(equality constraint)来让插值结果与边缘强制匹配,并求解出最能保持两侧平滑的纹素值。另一种方法则是在创建参数化和打包chart的时候考虑这个约束。Ray等人[1467]展示了如何使用保持网格的参数化(grid-preserving parameterization)来创建不受接缝瑕疵影响的光照贴图。
预计算的光照信息也可以存储在网格的顶点上。这样做的缺点是光照质量取决于网格细分的精细程度。因为这个决定通常是在模型创作的早期阶段就做出的,因此很难确保网格上有足够的顶点,使得在所有预期的光照情况下看起来都表现很好。此外,这个网格细分的操作成本可能是很高的。如果网格被细分得很精细,那么光照信号将会被过采样。如果使用定向的光照存储方法,则需要通过GPU在顶点之间对整个光照表示进行插值,并将其传递到像素着色器阶段,从而执行光照计算。在顶点和像素着色器之间传递这么多参数的情况是相当罕见的,并且会产生现代GPU未经优化的工作负载,这会导致效率和性能的低下。由于这些原因,因此很少会在顶点上存储预计算的光照信息。
虽然我们需要表面上的入射radiance信息(第14章会讨论的体渲染除外),但是我们可以通过体积的方式对其进行预计算和存储。这样做可以在空间中的任意位置上查询光照效果,并为预计算阶段不存在于场景中的物体提供照明。但是请注意,这些物体并不会正确地反射光线或者遮挡光线。
Greger等人[594]提出了irradiance体积的概念,它代表了对irradiance环境贴图进行稀疏空间采样的五维(三个空间和两个方向)irradiance函数。即空间中存在一个三维网格,每个网格点上都是一个irradiance环境贴图。动态物体会从最近的贴图中插值出irradiance值。Greger等人使用了一个两级的自适应网格来进行空间采样,但是也可以使用其他一些体积数据结构,例如八叉树[1304, 1305]等。
在原始irradiance体积中,Greger等人将每个样本点的irradiance存储在一个小纹理中,但这种表示方法无法在GPU上进行高效过滤。如今,体积光照数据通常会存储在三维纹理中,因此对体积进行采样也可以使用GPU的硬件加速过滤。样本点处的irradiance函数包含以下常见表示方法:
- 二阶和三阶的球谐函数(SH),其中二阶球谐函数要更为常见,因为其单个颜色通道需要使用四个系数,可以很方便地打包成常见纹理格式的四个通道(RGBA)。
- 球面高斯函数。
- 环境立方体或者环境骰子。
AHD编码方法,虽然它在技术上能够表示球面上的irradiance信息,但同时也会产生视觉可见的、分散观众注意力的瑕疵。如果使用SH的话 ,还可以使用球谐梯度(harmonic gradient)来进一步提高质量[54]。上述这些表示方法在许多游戏中都进行了成功的应用[766, 808, 1193, 1268, 1643]。
Evans [444]描述了一个应用在《小小大星球》中计算irradiance体积的技巧,它没有存储完整的irradiance贴图,而是在每个点上存储平均irradiance。根据irradiance场的梯度信息,即irradiance变化最迅速的方向,来计算近似的方向因子。这个梯度并不是显式计算出来的,而是通过在irradiance场中取两个样本,其中一个样本位于表面点处,另一个样本位于沿方向上稍微偏移的点上,并让一个样本减去另一个样本,从而计算梯度与表面法线之间的点积。这种近似表示方法的动机是,《小小大星球》中的irradiance体积是动态计算的。
irradiance体积也可用于为静态物体表面提供照明效果。这样做的好处是不需要为光照贴图提供单独的参数化,因此该技术也不会产生接缝瑕疵。静态物体和动态物体都可以使用相同的光照表示方法,这样两种不同类型的几何物体之间可以得到一致的光照效果。在延迟渲染(章节20.1)中使用这种体积表示方法是很方便的,因为所有光照计算都可以在一个pass中完成。这种方法的主要缺点是内存开销过大,光照贴图所使用的内存量与分辨率的平方成正比;而对于规则的体积结构而言,它所使用的内存量则与分辨率的立方成正比。由于这个原因,网格体积表示方法使用了相当低的分辨率。自适应的、分层的光照体积具有更好的特性,但是它们仍然要比光照贴图存储更多的数据。与规则间距的网格体积相比,它们的执行速度要更慢,因为额外的间接表示会在着色器代码中创建加载依赖,这可能会导致停滞阻塞以及执行速度的变慢。
在体积结构中存储表面照明有些棘手。因为具有截然不同光照特征的多个表面,有时可能会占据相同的体素,我们不确定应当存储哪些数据。当从这样的体素中进行采样时,所获得的光照效果通常是不正确的。这种情况经常会发生在明暗交界处,例如明亮室外和黑暗室内之间的墙壁附近,最终会导致室外出现的黑暗面片或者室内出现的明亮面片。解决方法也很直接,让体素的尺寸足够小即可,使得一个体素永远不会跨越这样的边界,但是这样做通常是不切实际的,因为需要的数据量实在是太大了。处理这个问题最常见的方法是:将采样位置沿着法线进行一些移动,或者是插值过程中调整所使用的三线性混合权重。这些做法通常也是不完美的,可能还需要对几何形状进行手动调整来掩盖问题。Hooker [766]在irradiance体积中添加了额外的裁剪平面,从而将它们的影响限制在凸多面体的内部。Kontkanen和Laine [766]讨论了减少颜色溢出的各种策略。
存储光照信息的体积结构不一定要是规则均匀的。一种流行的做法是将光照数据存储在不规则的点云中,然后再将这些点连接起来构成Delaunay四面体(如图11.30所示),Cupisz [316]将这种方法进行了推广。为了检索光照信息,我们首先需要找到采样位置所在的四面体,这是一个迭代过程,其开销可能会有点高。我们对网格进行遍历,并在相邻的单元之间进行移动。会使用查找点(采样位置)相对于当前四面体顶点的重心坐标(barycentric coordinate),来选择下一步要进行访问的邻居四面体(如图11.31所示)。在一个常见的场景中,可能会包含数千个存储光照信息的点云位置,因此这个检索过程可能会很耗时。为了对它进行加速,我们可以在前一帧中记录一个用于查找的四面体(如果可能的话),或者使用一个简单的体积数据结构,来为场景中的任意采样点提供一个良好的“起始检索四面体”。
一旦检索到了合适的四面体,就会使用重心坐标来对存储在四面体顶点的光照信息进行插值。这个插值操作并不会被GPU加速,它只需要4个值进行插值,而不是网格上三线性插值所需要的8个值。
这些预计算和存储光照信息的位置可以手动放置[134, 316],也可以自动放置[809, 1812]。它们通常被称为光照探针(lighting probe或者light probe),因为它们对光照信号进行了探测(采样)。这个术语需要和章节10.4.2中的“光照探针”区分开来,后者是记录在环境贴图中的远距离光照。
从四面体网格中采样获得的光照质量,高度依赖于网格的结构,而不仅仅是探针的总体密度。如果光照探针分布不均匀的话,那么生成的网格中可能会包含一些细长的四面体,从而产生视觉上的瑕疵。如果这些探针是手动放置的,那么这些问题可以很容易地进行纠正,但是这毕竟是一个手动过程,费时费力。这个四面体网格的结构与场景的几何结构无关,因此如果处理不当的话,光照效果在插值的时候会跨越墙壁的两侧,从而产生漏光瑕疵,就像是上文中的irradiance体积一样。在手动放置探针的情况下,开发者可以通过插入额外的探针来避免发生这种情况。在自动放置探针的情况下,可以向探针或者四面体中添加某种形式的可见性信息,从而将单个四面体的影响范围限制在相关区域内[809, 1184, 1812]。
对于静态和动态的几何物体,通常会使用不同的光照存储方法。例如:静态物体可以使用光照贴图,而动态物体则可以从体积结构中获得光照信息。虽然这样做很流行,但是这种方案可能会导致不同类型的几何物体之间产生不一致的外观表现。其中一些差异可以通过正则化(regularization)来消除,即在这些表示方法中对光照信息进行平均。
当烘焙光照信息的时候,需要注意的是,只需要在它们真正有效且合法的地方来计算光照信息即可。生成的网格通常是不完美的,一些顶点可能会被放置在几何体内部,或者网格的部分区域可能会产生自相交现象。如果我们在这些有缺陷的位置上计算入射radiance,那么结果将是不正确的。它们可能会导致我们不希望出现的暗化,或者是无阴影光照的颜色溢出等现象。Kontkanen和Laine[926],Iwanicki和Sloan [809]讨论了不同的启发式方法,这些方法可以用于丢弃无效样本。
环境光遮蔽和定向遮蔽信号与漫反射光照共享许多空间特性,如章节11.3.4所述,上述所有的方法都可用于存储它们。
11.5.5 动态漫反射全局光照
尽管预计算光照可以产生令人印象深刻的效果,但是它的主要优点同样也是它的主要缺点,即这种方法需要进行预计算。这个离线预计算的过程可能会很长,在常见的游戏关卡中,可能需要花费数个小时来进行光照烘焙,这种情况并不少见。由于光照计算需要花费很长时间,因此艺术家们被迫在多个层次上同时工作,从而避免在等待烘焙完成的时候无所事事。反过来,这通常又会导致渲染资源的过度负载,从而导致烘焙时间变得更长。这种烘焙-调整-再烘焙的循环,会严重影响工作效率并导致挫败感(frustration)。同时在某些情况下,可能无法使用预计算光照,因为场景中的几何物体在运行过程中会不断发生改变,或者在某种程度上,场景中的几何物体是由用户控制创建的。
为了模拟动态环境中的全局光照效果,已经有好几种方法被开发了出来。它们要么不需要任何预处理过程,要么算法的准备阶段足够得快,可以每帧执行。
在完全动态环境中模拟全局光照的最早方法之一是基于“即时辐射度(Instant Radiosity)” [879]。尽管这个方法名为辐射度算法,但是它与辐射度算法几乎没有共同之处。在这种方法中,会从光源向外投射光线,对于这些光线照射到的每个位置,都会放置一个新的光源,用于代表来自该表面元素的间接照明,这些光源被称为虚拟点光源(virtual point light,VPL)。基于这个思路,Tabellion和Lamorlette [1734]开发了一种在《怪物史莱克2》制作过程中所使用的方法,该方法会对场景表面执行一次直接光照pass,并将结果存储在纹理中。然后,在渲染过程中,该方法会对光线进行追踪,并使用缓存下来的光照数据来创建一次弹射的间接光照效果。Tabellion和Lamorlette的研究表明,在很多情况下,一次弹射就足以产生令人信服的结果。虽然这是一种离线方法,但是它启发了Dachsbacher和Stamminger [321],他们提出一种名为反射阴影贴图(reflective shadow maps,RSM)的方法。
与常规的阴影贴图(章节7.4)类似,反射阴影贴图是从光源的视角来进行渲染的。除了深度信息之外,它们还会存储有关可见表面的其他信息,例如反照率albedo、法线、直接光照(通量flux)。在进行最后着色的时候,RSM的纹素会被视为虚拟点光源,从而提供单次弹射的间接照明效果。由于一个典型的RSM中可能会包含数十万个像素,将这像素都作为点光源明显是不现实的,因此需要使用重要性驱动(importance-driven)的启发式方法,来选择其中的一个子集。Dachsbacher和Stamminger [322]后来展示了如何通过逆转这个过程来对该方法进行优化。该方法会基于整个RSM来创建一些虚拟光源,并将其放置(splatted)在屏幕空间中(章节13.9),而不是为每个着色点都从RSM中选择相关的纹素。
该方法的主要缺点是,它无法为间接光照提供遮挡效果。虽然这样做是一个很显著的近似,但是该方法生成的结果看起来还是合理的,并且对于许多应用程序而言也是可以接受的。
为了获得高质量的结果,并在光线运动过程中保持时域稳定性,因此需要创建大量的间接光源。如果创建的间接光源数量太少,当重新生成RSM的时候,它们的位置往往会迅速发生改变,从而导致闪烁瑕疵的出现。另一方面,从性能的角度来看,场景中存在太多的间接光源是十分具有挑战性的。Xu [1938]描述了游戏《神秘海域4》是如何应用这种方法的。为了保证性能要求,他在每个像素上只使用了少量的间接光源(16个),但是会在几帧之间循环使用不同的光源集合,并且会对结果进行时域过滤,如图11.32所示。
针对缺乏间接遮挡的问题,人们提出了不同的解决方法。Laine等人[962]使用了双抛物面阴影贴图来作为间接光源,但是会逐步将它们添加到场景中,因此在一帧中只有少量阴影贴图会被渲染。Ritschel等人[1498]使用简化的、基于点的场景表示,来绘制大量不完美的阴影贴图(imperfect shadow maps)。这样的贴图很小,并且在直接使用的时候还会包含许多缺陷,但是在经过简单的过滤之后,能够提供足够的保真度,从而为间接光照提供适当的遮挡效果。
有些游戏则使用了与这些解决方案相关的方法。其中《Dust 514》渲染了一个自上而下的世界视图,并且在需要的时候可以拥有多达4个独立的图层[1110]。这些生成的纹理会用于间接光照的收集,这很像Tabellion和Lamorlette的方法。在风筝demo中,虚幻引擎使用了类似的方法来提供地形的间接光照效果[60]。
11.5.6 光照传播体积
辐射传输理论(radiative transfer theory)是一种模拟电磁辐射如何在介质中传播的一般方法,它包括了散射(scattering)、发射(emission)和吸收(absorption)。尽管实时图形学力求显示所有的这些效果,但是除了最简单的情况之外,用于模拟这些效果的方法都具有很高的成本,无法直接应用于渲染中。然而,该领域中所使用的一些技术,在实时图形应用中被证明是很有用的。
由Kaplanyan [854]提出了光照传播体积(light propagation volumes, LPV),其灵感来源于辐射传输中的离散坐标法(discrete ordinate methods)。在他的方法中,场景被离散成一个规则的三维网格,每个单元格内都会维护一个穿过它的定向radiance分布,他使用二阶球谐函数来处理这些信息。在第一步中,光照会被注入到包含直接光照表面的单元格中。可以使用反射阴影贴图来找到这些单元格,也可以使用任何的其他方法。注入这些单元格的光照信息,是该表面反射出的radiance,它在表面法线附近构成了一个分布,指向远离表面的方向,并且会从材质的颜色中获得自身的颜色。接下来会对光照进行传播,每个单元格都会对其邻居单元格的radiance场进行分析,并据此修改自身的radiance分布,从而考虑来自各个方向的radiance。在一个步骤中,radiance只在一个单元格的距离上进行传播,因此为了让radiance进行充分传播,需要进行多次迭代,如图11.33所示。
这种方法的重要优点在于,它会为每个单元格生成完整的radiance场,这意味着我们可以使用任意的BRDF来进行着色,尽管在使用二阶球谐函数的时候,光泽BRDF的反射质量会很低。Kaplanyan展示了漫反射表面和镜面的例子。
为了允许光照在更大的距离上进行传播,增加体积所覆盖的区域面积,同时保持合理的内存开销,Kaplanyan和Dachsbacher [855]开发了该方法的一种级联变体。他们不再使用与单元格大小相同的体积,而是使用一组逐渐变大的单元格,这些单元格彼此之间能够嵌套。光照会被注入到所有的层级中并独立地进行传播。而在查找过程中,会为给定位置选择最详细且可用的层级单元格来计算光照。
在最初的实现中,他们没有考虑间接照明的任何遮挡。修改后的方法使用了来自反射阴影贴图的深度信息,以及来自相机位置的深度缓冲,从而向这些体积块中添加了有关光线遮挡物的信息。这些信息是不完整的,但是场景也可以在预处理期间进行体素化,从而使用更加精确的表示方法。
该方法与其他体积方法存在相同的问题,其中最大的问题是颜色溢出。不幸的是,在LPV方法中单纯地增加网格分辨率来解决这个问题,还会导致出现其他问题。当使用较小尺寸的单元格时,就需要更多的迭代步骤,来在相同的世界空间距离上进行光线传播,这会使得该方法的成本明显上升。在网格分辨率和性能之间找到一个平衡并非易事。同时该方法还存在锯齿问题,网格的有限分辨率,加上radiance的粗糙定向表示(二阶球谐函数),会导致光照信号在相邻单元格之间移动时发生退化。例如对角条纹等空间瑕疵,可能会在多次迭代后出现。其中一些问题可以通过在执行传播pass之后,再执行空域过滤来进行消除。
11.5.7 基于体素的方法
Crassin [304]提出了体素锥形追踪全局光照(voxel cone tracing global illumination,VXGI),它也是基于一种体素化的场景表示。几何物体本身使用稀疏体素八叉树(sparse voxel octree)的形式进行存储,我们将在章节13.10中进行介绍。这种结构提供了一种类似于mipmap的场景表示,因此可以对体积空间进行快速的遮挡测试等操作。每个体素块还包含了它们所代表的几何物体所反射出的光线量等信息,它以一种定向形式进行存储,因为radiance会在六个主要方向上发生反射。首先会使用反射阴影贴图,将直接照明注入到八叉树的最低层节点中,然后再根据层次结构向上进行传播。
这个八叉树结构用于估计入射radiance。在理想情况下,我们将会追踪一条射线,从而计算来自特定方向上的radiance估计。然而,这样做需要追踪许多射线才能获得理想结果,因此我们会将整个光束近似于一个圆锥,这个圆锥位于它们的平均方向上,我们对这个圆锥进行追踪,最后只会返回一个值。想要精确测试圆锥与八叉树的交点并不是一件容易的事情,因此这个操作会被进一步近似,我们会沿着圆锥的中心轴,对八叉树结构进行一系列的查找。每次查找都会对八叉树的某个层次进行读取,该层次上的节点大小,应当与给定点处的锥形截面相对应。查找提供了在圆锥原点方向上反射的滤波radiance,以及几何物体占据查找空间的百分比,这个百分比信息会用于减弱来自后续点的光照强度,这有点类似于alpha混合。整个锥体的遮挡信息也会被追踪,在每个步骤中,它会被减少到几何物体占当前样本的百分比。在累积radiance的时候,首先会将其乘以合并后的遮挡因子(如图11.34所示)。虽然这种策略无法检测到由多个部分遮挡组合而成的完整遮挡,但是其结果仍然是可信的。
为了计算漫反射光照,我们需要跟踪若干个圆锥,具体生成和追踪的圆锥数量,取决于性能和精度之间的权衡。追踪更多的圆锥可以提供更高质量的结果,其代价是需要花费更多的时间。我们假设余弦项在整个圆锥上都是恒定的,因此这一项可以从反射方程的积分中提取出来。这样做可以使得漫反射光照的计算变得很简单,只需要计算锥形追踪的返回值,并计算其加权和即可。
正如Mittring [1229]所描述的,这个方法的原型版本是在虚幻引擎中实现的。他给出了一些开发人员需要进行的优化,从而可以使其作为完整渲染管线的一部分进行使用。这些改进包括以较低的分辨率来执行追踪,并在空间散布圆锥。这样做的目的是为了让每个像素只跟踪一个圆锥。并在屏幕空间中对结果进行过滤,从而获得漫反射响应的完整radiance。
使用稀疏八叉树存储光照信息,一个主要问题就是查找成本较高。找到包含给定位置的叶子节点,需要进行一系列的内存查找,中间还穿插着一些简单的逻辑来确定要遍历哪一个子树。一次典型的内存读取可能需要耗费几百个时钟周期。GPU试图通过并行执行多组着色线程(warp或者wavefront)来隐藏这种延迟(第3章)。即在任何给定时间内,只有一组着色线程会执行ALU操作,当它需要等待内存读取的时候,另一组着色线程会取而代之。能够同时激活的warp数量由不同的因素所决定,但所有的这些因素,都与单个组所使用的资源数量有关(章节23.3)。在遍历分层数据结构的时候,大部分时间都花在内存读取上,会等待从内存中获取下一个节点的数据。然而,在等待期间执行的其他warp中,很可能也会进行内存读取。与内存访问的次数相比,ALU其实工作得很少,并且由于实际运行的warp总数是有限的,因此经常会出现所有分组都在等待内存返回数据,都没有实际工作执行的情况。
大量的warp停滞会导致性能表现不佳,人们已经开发了一些方法来缓解这些低效问题。McLaren [1190]使用一组级联的三维纹理来代替八叉树结构,这种方法很像级联的光照传播体积[855](章节11.5.6)。它们具有相同的尺寸,但是所覆盖的区域面积越来越大。通过这种方式,只需进行一次常规的纹理查找即可完成数据的读取,而不需要进行额外的依赖读取。存储在纹理中的数据与存储在稀疏体素八叉树中的数据相同,它们都包含六个方向上的反照率、占用率和反弹光照信息。因为级联的位置会随着相机的移动而发生变化,因此物体可能会不断地进出高分辨率区域。由于内存的限制,我们不可能在内存中一直维护这些体素化内容,因此它们会在需要的时候才进行体素化。McLaren还介绍了一些优化方法,使得这种技术能够用于30 FPS的游戏,例如《明日之子(The Tomorrow Children)》,如图11.25所示。
11.5.8 屏幕空间方法
与屏幕空间环境光遮蔽(章节11.3.6)一样,可以只使用存储在屏幕位置上的表面信息[1499],来模拟一些漫反射全局光照效果 。这些方法并不像SSAO那样流行,主要是因为屏幕空间中可用的数据量十分有限,因此会导致更加明显的瑕疵。诸如颜色溢出(color bleeding)这样的效果,通常是由于强烈的直射光线照亮具有相对恒定颜色的大面积区域而产生的。像这样的表面通常不可能完全适应视图,即可能只有部分会出现在画面中。这种情况使得反射光线的数量强烈依赖于当前帧,并且会随着相机的移动而发生波动。出于这个原因,屏幕空间中的方法仅适用于在精细尺度上对其他解决方案进行扩展补充,这种精细尺度超出了主要算法所能够达到的分辨率。这类系统在游戏《量子破碎》[1643]中进行了使用,在这个游戏中,使用了irradiance体积来模拟大规模全局光照的效果,使用屏幕空间中的解决方案来提供有限距离的弹射光线。
11.5.9 其他方法
Bunnell用于计算环境光遮蔽的方法 [210](章节11.3.5),也可以用于动态计算全局光照效果。通过存储每个圆盘的反射radiance信息,来对基于点的场景表示方法(章节11.3.5)进行增强。在收集步骤中,可以在每个收集位置上构建一个完整的入射radiance函数,而不仅仅是收集遮挡信息。就像环境光遮蔽一样,必须要执行一些后续步骤,来消除那些来自于被遮挡圆盘的光照。
11.6 镜面全局光照
上一小节中所介绍的方法,主要是为了模拟漫反射全局光照效果,而在本小节中,我们将会介绍各种用于渲染视图依赖(view-dependent)效果的方法。对于光泽材质而言,其镜面波瓣要比漫反射光照中所使用的余弦波瓣紧密得多,其扩散范围小得多。如果我们想要渲染一种极有光泽的材质,这种材质具有很薄的镜面波瓣,我们需要一种能够传递这种高频细节的radiance表示方法。反过来,这些条件也意味着,反射方程只需要计算从有限立体角入射的光线即可,而不是像Lambertian BRDF那样,需要反射来自整个半球的入射光线,这与漫反射材质的要求完全不同。这些特性解释了想要实时渲染这样的效果,为什么需要进行完全不同的权衡考虑的原因。
存储入射radiance的方法可以用于提供粗略的视图依赖效果。当使用AHD编码或者HL2基底时,我们是可以计算镜面反射的,就好像光照来自于编码方向(在使用HL2基底时,是三个方向)的方向光一样。这种方法的确可以通过间接照明提供一些高光效果,但是它们相当不准确。在使用AHD编码时,这种方法尤其成问题,因为方向分量可能会在很小的距离内发生剧烈变化,这种方差会导致高光以不自然的方式发生变形。可以通过在空间方向上进行滤波来减少这种瑕疵[806]。在使用HL2基底时,如果相邻三角形之间的切线空间变化很快,同样也会出现类似的问题。
可以通过使用更高的精度来表示入射光线,从而减少瑕疵的出现。Neubelt和Pettineo在游戏《教团:1886》[1268]中使用球形高斯波瓣来表示入射radiance。为了渲染高光效果,他们使用了Xu等人[1940]提出的一种方法,该方法包含了一种典型微表面BRDF高光响应(章节9.8)的有效近似。如果使用一组球面高斯函数表示光照,并且假设菲涅尔项和masking-shadowing函数在其范围内为常数,则反射方程可以被近似为:
其中为第个球面高斯所表示的入射radiance,是结合了菲涅尔项和masking-shadowing函数的组合因子,为NDF项。Xu等人引入了一种各向异性的球面高斯(anisotropic spherical Gaussian,ASG),他们使用ASG来对NDF进行建模。他们还为计算SG和ASG乘积的积分提供了一种有效的近似,如方程11.37所示。
Neubelt和Pettineo使用了9-12个高斯波瓣来表示光照,这使得他们只能模拟中等光泽的材质。他们能够使用这种方法来表现大部分的游戏光照效果,因为游戏《教团:1886》发生在19世纪的伦敦,而那时具有高度抛光的材质,玻璃和反射表面是十分罕见的。
11.6.1 局部环境贴图
到目前为止我们所讨论的方法,还不足以渲染令人信服的抛光材质。对于这些技术而言,它们所能描述的radiance场太过粗糙,无法精确编码入射radiance的细节,这使得反射看起来很暗淡。如果在同一材质上进行使用的话,所产生的结果也与分析光源的镜面高光不一致。一种解决方案是使用更多的球面高斯函数或者更高阶的SH来获得我们所需要的细节。这样做是可行的,但是我们很快就会面临一个性能问题:SH和SG都有全局支持(global support)特点。即每个基函数在整个球面上都是非零的,这意味着我们需要将所有的基函数都计算一遍,才能获得给定方向上的光照信息。这样做的计算成本会变得很高,因为想要渲染尖锐的反射效果,我们可能需要数千个基函数。而且也不可能在漫反射光照的分辨率下,存储那么多的数据。
在实时环境中为全局光照提供高光分量,其中最流行的解决方案是局部环境贴图(localized environment map),它可以解决我们之前遇到的两个问题。首先,入射radiance会被表示为一个环境贴图,因此只需要几个值就可以获得所需的radiance。其次,这些局部环境贴图稀疏地分布在整个场景中,因此如果我们想要增加入射radiance的空间精度,只要增加这些局部环境贴图的角分辨率(angular resolution)即可。这种在场景中特定点进行渲染的环境贴图,通常会被称为反射探针(reflection probe)。图11.36展示了这样一个例子。
环境贴图非常适合用于渲染完美的反射效果,即镜面的间接照明。已经有很多方法可以利用纹理来实现各种各样的高光效果了(章节10.5)。所有这些方法都可以与局部环境贴图一起使用,以渲染间接光照的镜面响应效果。
最早将环境贴图与空间中特定点相绑定的游戏之一是《半条命2》[1193, 1222],在他们的系统中,会由艺术家首先在整个场景中放置采样位置。在预处理阶段中,会在每个位置上渲染一个立方体贴图。在进行高光计算的时候,物体会使用最近位置上的结果来作为入射radiance的表示。相邻的物体可能会使用不同的环境贴图,这将会导致视觉效果的不匹配,但是艺术家可以手动调整立方体贴图所覆盖的范围。
如果一个物体很小,并且环境贴图就是从其中心进行渲染的(在隐藏该物体之后,它就不会出现在纹理中),那么所生成的结果是相当精确的。不幸的是,这种情况十分少见;在大多数情况下,一个反射探针会同时用于多个物体,有时候还会具有明显的空间范围。高光表面的位置距离环境贴图的中心越远,其结果与现实的差异就越大。
Brennan [194]和Bjorke [155]提出了一种解决这个问题的方法。他们并没有将入射光照看作是来自一个无限远的包围球体,而是假设这些入射光照来自一个有限大小的球体,该球体的半径是用户进行定义的。在检索入射radiance的时候,输入的方向不会直接用于索引环境贴图,而是将其视为来自评估表面发射出的射线,并与该球体相交。然后会计算一个新的方向,即从环境贴图中心位置指向球面交点位置的方向,这个方向向量会用作查找方向,如图11.37所示。这个过程具有在空间中“固定”环境贴图的效果,这个做法通常被称为视差校正(parallax correction)。同样的方法也可以用于其他的基本形状类型,例如box [958]。用于与光线相交的形状通常会被称为反射代理(reflection proxy)。所使用的代理物体应当能够表示渲染到环境贴图中的几何物体的一般形状和大小。虽然通常而言这是不可能的,但是如果反射代理能够与几何体完全匹配(例如用一个box代表一个矩形房间),那么这种方法可以提供完美的局部反射效果。
这种技术在游戏中非常流行,它易于实现,运行速度快,可以应用于前向渲染和延迟渲染中。美术人员还可以直接控制其外观与内存开销。如果某些区域需要更加精确的照明效果,他们可以放置更多的反射探针,同时让代理物体更好地适应场景几何形状。如果用于存储环境贴图的内存过多,那么从场景中删除一些探针也是很容易的。当使用光泽材质的时候,着色点与反射代理交点之间的距离,可以用来决定使用哪个级别的预过滤环境贴图,如图11.38所示。这样做可以模拟在我们远离着色点时,BRDF波瓣不断增长的覆盖区域。
当多个探针覆盖同一区域时,可以建立如何组合它们的直观规则。例如:探针可以有一个用户设置的优先级参数,具有较高优先级参数的探针,其优先级会高于其他优先级较低的探针,或者可以在它们之间进行平滑地插值融合。
不幸的是,由于这种方法过于简单,因此会导致各种各样的瑕疵。反射代理的几何形状很少能够与底层几何结构完全匹配。这会使得某些区域上的反射效果被不自然地拉伸,这个问题主要会发生在高度反射、抛光的材质上。此外,渲染到环境贴图中的反射物体会根据贴图的位置来计算它们的BRDF。访问环境贴图的表面位置,不会与这些物体具有完全相同的视图,因此纹理中存储的结果不是完全正确的。
反射代理也会导致漏光问题(有时会很严重)。通常而言,查找过程会从环境贴图的明亮区域返回结果值,因为这个简化的光线投射会错过应当引起遮挡的局部几何形状。这个问题有时候可以通过使用定向遮蔽方法(章节11.4)来缓解。另一个缓解这个问题的流行策略,是使用预计算漫反射光照,它通常会以更高的分辨率进行存储。环境贴图中的反射值首先会除以渲染位置上的平均漫反射光照。这样做可以有效地从环境贴图中去除平滑、扩散的贡献值,从而只留下较高频率的成分。在进行着色时,反射值再乘以着色位置上的漫反射光照。这样做可以部分缓解反射探针空间精度不足的问题[384, 999]。
有一些方法可以使用反射探针来捕获更加复杂的几何表示。Szirmay-Kalos等人[1730]为每个反射探针都存储了一个深度贴图,并在查找时对使用它执行一次光线追踪,这样可以产生更加准确的结果,但是需要花费一些额外的开销。McGuire等人[1184]提出了一种更加有效的方法,它根据探针的深度缓冲来追踪光线。他们的系统会存储多个探针,如果最初选择的探针没有包含足够的信息来可靠地确定命中位置,则会选择使用备用探针,并继续使用新的深度数据来进行光线追踪。
当使用光泽BRDF的时候,环境贴图通常是预过滤的,并且每个mipmap层级所存储的入射radiance都会与一个逐渐增大的滤波核进行卷积。预过滤步骤会假设这个滤波核是径向对称的(详见章节10.5)。然而,当使用视差校正的时候,BRDF波瓣在反射代理形状上所占据的空间,会根据着色点位置而发生变化,这样做会使得预过滤过程变得稍微不正确。Pesce和Iwanicki对这个问题的不同方面进行了分析,并讨论了潜在的解决方案[807, 1395]。
所使用的反射代理形状,也不必是封闭的、凸的。也可以使用简单的平面矩形,也可以使用包含高质量细节的box或者球形代理[1228, 1640]。
11.6.2 环境贴图的动态更新
使用局部反射探针需要对每个环境贴图进行渲染和过滤,这项工作通常是离线完成的,但是在某些情况下可能也需要在运行时完成。在开放世界游戏中,一天中的时间会不断发生变化,世界场景中几何物体是动态生成的,因此将这些贴图都进行离线处理会花费太长时间,影响开发效率。在某些极端情况下,如果需要许多变体环境贴图时,我们甚至无法将它们全部都存储在磁盘上。
实际上,有些游戏会在运行过程中实时渲染反射探针,这种类型的系统需要进行仔细调整,以免对性能产生重大影响。除了一些很简单的情况之外,我们不可能在每一帧中都重新渲染所有可见的探针,因为对于现代游戏而言,每帧通常会使用数十个甚至数百个探针。幸运的是,我们也不需要这样做的。我们很少会要求反射探针在任何时候都能够准确地描述它们周围的所有几何形状。大多数情况下,我们确实希望反射探针能够对一天中某个时刻的变化做出正确反应,但是我们可以通过其他的一些方法来对动态几何物体的反射进行近似,例如我们后面要介绍的屏幕空间方法(章节11.6.5)。这些假设允许我们在加载阶段提前渲染一些探针,而后续的探针则会在它们进入相机视野时逐个进行渲染。
即使我们希望在反射探针中渲染动态几何物体,我们也只能以一个较低的帧率来对其进行更新。我们可以定义渲染反射探针所需要的帧时间,并且在每帧中更新固定数量的反射探针。基于每个探针到相机的距离、距离上次更新的时间,以及其他类似因素的启发式方法可以帮助我们确定反射探针的更新顺序。在时间预算特别紧张的情况下,我们甚至可以将单个环境贴图的渲染拆分到多个帧中进行。例如:我们可以在一帧中只渲染立方体贴图的其中一个面,在6帧中渲染一个完整的立方体贴图。
在离线执行卷积操作的时候,通常会使用高质量的滤波,这种滤波涉及对输入纹理的多次采样,这在要求高帧率的游戏中是不可能实现的。Colbert和Krivanek [279]开发了一种方法,该方法使用重要性采样,能够以相对较低的样本数量(约为64)来实现质量相当的过滤。为了消除大部分噪声,他们从具有完整mipmap链的立方体贴图中进行采样,并使用启发式方法来确定每个样本应该读取哪个mipmap层级。他们的方法是一种对环境贴图进行快速、运行时预过滤的流行选择[960, 1154]。Manson和Sloan [1120]在基函数中构造了所需的滤波核,构造一个特定滤波核的精确系数必须要在优化过程中获得,但是对于一个给定的形状,这个过程只会发生一次。卷积分为两个阶段进行:首先,使用一个简单的滤波核来对环境贴图同时进行下采样和过滤。接下来,将得到的mipmap链中的样本组合起来,构建最终的环境贴图。
为了限制光照pass的带宽开销以及内存开销,可以对最终生成的纹理进行压缩。Narkowicz [1259]描述了一种将高动态范围反射探针压缩为BC6H格式(章节6.2.6)的有效方法,该格式能够存储半精度的浮点值。
想要渲染复杂的场景,即使一次只渲染立方体贴图一个面,这对于CPU而言开销仍然可能会过大。一种解决方案是在离线过程中为环境贴图生成G-buffer,在运行时只需要计算光照和卷积即可[384, 1154],这大大降低了CPU的负载。如果需要的话,我们甚至可以在预生成的G-buffer上渲染动态的几何物体。
11.6.3 基于体素的方法
在大多数性能受限的情况下,局部环境贴图是一个很好的解决方案,然而,其质量往往不能令人满意。在实践中,必须使用一些变通方法来掩盖由于探针空间密度不足,或者反射代理对实际几何形状过于粗糙的近似而导致的问题。如果每帧有更多的可用时间,则可以使用一些更加精细的方法。
体素锥形追踪,无论是使用稀疏八叉树进行存储[307],还是其级联版本(章节11.5.7)[1190],同样可以用于渲染高光效果。该方法会将场景表示存储在稀疏体素八叉树中,并在这个体素数据结构中进行锥形跟踪。一次锥形追踪只会返回一个值,这个值表示了来自该圆锥所对应立体角的平均radiance。对于漫反射光照而言,我们需要对多个方向上的圆锥进行追踪,因为只用一个圆锥是不准确的。
对于光泽材质而言,使用锥形追踪的效率要高得多。在镜面光照的情况下,BRDF的波瓣会很狭窄,只需要考虑一个来自较小立体角的radiance,因此我们不再需要同时追踪多个圆锥区域,在大多数情况下,一个着色点只需要追踪一个圆锥就足够了。只有较为粗糙材质上的高光效果,才可能需要追踪多个圆锥,但是又因为这样的反射效果十分模糊,在这种情况下,我们只需要使用局部反射探针即可,根本不需要执行锥形追踪。
与之相反的是高度抛光的材质,它们的反射效果几乎像镜子一样,这会使得所进行追踪的圆锥区域变的十分狭窄,就像一条射线一样。有了这样一个精确的追踪,底层场景表示的体素本质可能会在反射中被注意到,所产生的反射效果将会表现出体素化的立方体外观,而不是多边形几何。但是这个瑕疵在实践中很少会成为一个问题,因为反射效果很少会被人们直接观察到,这个反射效果会叠在某个纹理表面上,其最终的贡献值会被纹理进行修正,这个过程通常会掩盖任何缺陷和瑕疵。当需要完美的镜面反射效果时,还可以使用其他方法,从而以更低的成本来实现这个效果。
11.6.4 平面反射
另一种方法是重复使用常规的场景表示,对其进行重新渲染从而创建一个反射图像。如果反射表面的数量是有限的,并且它们都是平面的,我们就可以使用常规的GPU渲染管线,来创建从这些表面上所反射的场景图像。这些图像不仅可以提供精确的镜面反射效果,而且还可以通过对图像进行一些额外处理,从而渲染令人信服的光泽效果。
理想的反射面遵循反射定律(law of reflection),即入射角等于反射角。也就是说,入射光线与表面法线之间的夹角,等于反射光线与表面法线之间的夹角,如图11.39所示。图11.39还展示了一个反射物体的“图像”,根据反射定律,物体的反射图像实际上就是该物体经过平面的物理反射。也就是说,我们沿着入射光线继续前进(注意这里不是反射光线),穿过反射平面,最终可以到达反射物体上相同的位置。
这就引出了一个原理:可以通过创建物体的一个副本,将其转换到反射位置上,然后再从那里渲染这个副本物体,从而实现反射效果。为了实现正确的光照效果,光源也必须要在平面上进行反射,包括光源的位置和光照的方向[1314]。一种等效的方法是保持场景表示不变,通过镜子将观察者的位置和观察方向反射到反射平面的另一侧。这种反射操作可以通过对投影矩阵进行简单地修改来实现。
位于反射平面背面的物体不应该被反射。这个问题可以使用反射平面的平面方程来解决,最简单的方法就是在像素着色器中定义一个裁剪平面。让这个裁剪平面与反射平面相重合 [654],在渲染反射场景的时候,使用这个裁剪平面将与观察位置位于同一侧(即原本位于镜子背面的物体)的所有反射几何物体都裁剪掉。
11.6.5 屏幕空间方法
就像环境光遮蔽与漫反射全局光照一样,一些高光效果也可以在屏幕空间中进行计算。由于镜面波瓣比较尖锐,因此这样做要比漫反射情况稍微精确一些。我们只需要在绕反射观察向量的有限立体角范围内,即可获得有关radiance的信息,而不需要在整个半球范围内进行计算,因此屏幕空间中的数据会更有可能包含这个信息。这种类型的方法最早由Sousa等人[1678]提出,同时也被其他开发人员所发现,整个系列的方法被称为屏幕空间反射(screen-space reflections,SSR)。
给定着色点的位置,观察向量和法线,我们可以沿着法线反射的观察向量来追踪一根光线,并测试其与深度缓冲的交点。这个测试是通过沿着光线进行迭代移动,每次步进一定的距离,并将光线此时的位置投影到屏幕空间中,再从z-buffer深度中检索该位置的深度信息来完成的。如果此时光线上的点到相机的距离,要比深度缓冲中对应位置的几何物体的深度更远,这意味着光源位于几何物体的内部,此时我们就可以认为光线与场景相交。然后我们可以从颜色缓冲中的对应位置处,读取到相应的颜色值,从而获得追踪方向入射到着色点的radiance。这种方法假设光线照射到的表面是Lambertian表面,但是这个条件只是许多方法的一种近似,在实践中当然可以使用其他BRDF。光线可以在世界空间中以均匀的步长进行追踪,但是这样做所获得的交点信息十分粗糙,因此当检测到光线与场景相交时,可以执行一个细化检索的pass,在有限的距离内可以使用二分查找来精确定位交点的位置。
McGuire和Mara指出[1179],由于透视投影的原因,在世界空间中以均匀间隔进行步进,所产生的采样点分布在屏幕空间中是不均匀的。在靠近相机的光线部分会采样不足,因此可能会错过一些光线与场景相交的位置;而距离相机较远的光线部分则会被过采样,因此相同的深度缓存像素会被多次读取,从而产生不必要的内存流量和冗余计算。他们建议使用一种数值微分法(digital differential analyzer,DDA)来在屏幕空间中执行射线步进,DDA是一种可以用于光栅化线条的方法。
首先,将待追踪光线的起点和终点都投影到屏幕空间中,沿着这条线段依次检查每个像素,以保证均匀的采样精度。使用这种方法的一个结果是,在执行相交测试的时候,不需要对每个像素的观察空间(view-space)深度进行重建。观察空间中深度值的倒数,即在常规透视投影下存储在z-buffer中的值,这个值在屏幕空间中呈线性变化。这意味着我们可以在实际追踪之前,计算该像素对屏幕空间坐标和坐标的导数(斜率),然后再使用简单的线性插值来获得屏幕空间线段上任何位置的值。使用这种方法计算出来的值,可以直接与深度缓冲中的数据进行比较。
基本形式的屏幕空间反射只对一条光线进行追踪,因此只能提供镜面反射效果。然而,完美的镜面是相当罕见的,在现代基于物理的渲染管线中,光泽反射是更加常见也是更加需要的,SSR同样也可以用于渲染这些效果。
在简单的临时方法中[1589, 1812],反射仍然沿着反射方向使用单一的光线追踪,并将结果存储在离屏缓冲区中,在后续步骤中进行处理。通过使用一系列的滤波核,通常还会与缓冲区的下采样操作相结合,从而创建一组具有不同模糊程度的反射缓冲区。在计算光照的时候,BRDF波瓣的宽度决定了哪个反射缓冲区会被采样。虽然通常会选择与BRDF波瓣形状相匹配的滤波核,但是这样做(模糊)只是一个粗略的近似,因为在进行屏幕空间过滤时,并不会考虑不连续性、表面朝向以及其他对结果精度至关重要的因素。最后会添加自定义的启发式方法,使得屏幕空间中的光泽反射,在视觉上与其他来源的镜面反射相匹配。尽管这只是一个近似值,但是最终生成的结果还是令人信服的。
Stachowiak [1684]以一种更有原则的方式来处理这个问题。计算屏幕空间反射是光线追踪的一种形式,就像常规的光线追踪一样,它可以用于执行适当的蒙特卡洛积分。他不仅使用了反射观察方向,还使用了对BRDF的重要性采样以及光线的随机发射。由于性能的限制,光线追踪是在屏幕半分辨率下完成的,每个像素上只会追踪少量光线(1到4根)。由于所使用的光线太少,会产生有噪声的图像,因此相交结果会在相邻像素之间进行共享。对于一定范围内的像素,假设它们的局部可见性是相同的。如果从点向方向发出的光线与场景在点处相交,那么我们可以假设,如果从点向方向发出一条光线,它也会和场景相交于点,并且在点之前不会与其他任何表面相交。这样我们可以直接重复使用光线数据,不需要真的对其进行追踪,只需要适当地修改这次追踪对邻域积分的贡献值即可。从形式上讲,在计算当前像素BRDF的概率分布函数(pdf)时,从相邻像素发出光线的方向将具有不同的概率分布。
为了进一步增加光线的有效数量,还需要对结果进行时域过滤。通过离线计算与场景无关的部分积分项,并将其存储在由BRDF参数索引的查找表中,还可以进一步降低最终积分结果的方差。如果反射光线的所有信息都可以在屏幕空间中找到,那么以上的这些策略可以让我们获得精确的、无噪声的结果,这个结果接近于离线路径追踪所获得的ground-truth图像,如图11.40所示。
在屏幕空间中进行光线追踪操作的成本通常是很高的。因为它包含了对深度缓冲的重复采样(可能会有多次),并且还会对查找结果执行某些额外的操作。由于这个读取过程是相当不连贯的,因此缓存的利用率可能会很差,从而导致着色器在执行期间为了等待内存数据的返回,而发生长时间的停滞。因此在具体的实现过程中需要格外注意,尽可能得优化执行效率。屏幕空间反射通常会在一个降低的分辨率下进行计算[1684, 1812],并使用时域过滤来弥补因为追踪分辨率下降而带来的质量下降。
Uludag [1798]描述了一种使用分层深度缓冲(Hi-Z,章节19.7.2)来加速光线追踪的优化方法。首先需要创建一个层次结构,深度缓冲会逐步进行下采样操作,每一步的下采样系数在每个方向上均为2。较高层级上的像素会存储较低层级上的四个对应像素中的最小深度值。接下来会使用这个层次结构来执行光线追踪。如果在给定的步骤中,光线穿过了单元格,但是没有击中单元格中存储的几何图形,那么则将光线推进到单元格的边界,并在下一次步进中使用更低分辨率的缓冲,更低分辨率的缓冲区意味着更大的步长。如果光线在当前单元格中发生了相交,则将光线推进到相交位置,并在下一次步进中使用更高分辨率的缓冲,更高分辨率的缓冲区意味着更小的步长。在命中最高分辨率的缓冲区时,追踪过程会被终止,此时认为光线与场景相交。这个动态步进过程如图11.41所示。
这个方案特别适用于较长距离的追踪,因为首先它确保了不会遗漏交点,同时还允许光线以较大的步长进行步进。它还可以很好地访问缓存,因为深度缓冲不是在随机的、遥远的位置上进行读取的,而是在本地邻近区域上读取的,这样大大提高了缓存效率。Grenier [599]还给出了实现这个方法的许多实用技巧。
其他人则完全避免使用光线追踪。Drobot [384]通过反射代理重用交点的位置,并从那里查找屏幕空间中的radiance。Cichocki [266]假设了平面反射器,他没有使用光线追踪,相反他执行了一个全屏pass,其中每个像素会将自身的像素值写入对应的反射位置中。
与其他屏幕空间的方法一样,由于屏幕空间中的信息是有限的,据此生成的反射效果也会受到有限数据所造成的瑕疵影响。对于反射光线而言,在没有与场景几何相交的情况下就离开屏幕区域,或者是击中场景几何的背面,这些情况是很常见的,在这些情况下,我们无法获得可用的光照信息。这种情况需要进行优雅(gracefully)地处理,因为即使是相邻像素,光线追踪的有效性也会经常不同。可用使用一些空间滤波器来部分填充追踪缓冲区中的空白区域[1812, 1913]。
SSR的另一个问题是缺乏关于深度缓冲中物体厚度的信息。因为深度缓冲中只存储了一个深度值,所以当光线到达由深度信息所描述的表面背后时,我们无法判断光线是否击中了场景中的其他物体。Cupisz [315]讨论了各种低成本的方法,来减轻由于不知道深度缓冲中物体的厚度而产生的瑕疵。Mara等人[1123]描述了深度G-buffer,它存储了多层数据,因此包含了更多有关表面和环境的信息。
屏幕空间反射是一个很好的工具,它可以提供一组特定的效果,例如在近乎平坦的平面上,渲染邻近物体的局部反射效果。它能够大大提高实时高光照明的质量,但是它们并没有提供一个完整的解决方案。本章节中所介绍的各种方法通常会叠加在一起使用,从而构建一个完整而健壮的系统。通常会将屏幕空间反射作为第一层方案,如果它无法提供准确的结果,则使用局部反射探针作为备用。如果给定区域中没有探针,则使用全局的默认探针[1812]。这种类型的设置思路,可以提供一种一致且健壮的方式,来获得令人信服的间接镜面反射效果,这对于生成可信外观而言尤其重要。
11.7 统一方法
到目前为止我们所介绍的方法,它们可以组合成一个能够渲染漂亮图像的完整系统。然而,这些系统交错在一起,缺乏路径追踪的优雅性和概念简洁性。渲染方程的不同方面都会以不同的方式进行处理,在每个方面都做出了不同程度的妥协。尽管最终的生成图像看起来很逼真,但是在很多情况下,这些方法依然会失败,导致视错觉的中断。由于上述的这些原因,实时路径追踪一直是研究工作的重点。
通过路径追踪来渲染可接受质量的图像,其所需的计算量远远超过了CPU的能力,即使是最快的CPU也不行,因此通常会使用GPU来进行计算。GPU极快的计算速度和计算单元的灵活性,使得它们成为这项任务的良好候选者。实时路径追踪的应用包括建筑可视化以及电影渲染预览等。对于这些情况而言,较低且可变的帧率是可以接受的。可以使用渐进收敛(progressive refinement)(章节13.2)等技术来提高相机静止时的图像质量。高端系统则可以同时使用多个GPU。
相比之下,游戏需要以最终的质量要求来渲染每一帧,并且需要能够在预算时间内稳定运行。GPU可能还需要执行一些其他任务,而不仅仅是渲染。例如:诸如粒子模拟之类的系统,通常会放到GPU上进行执行,从而释放一些CPU的处理能力。所有这些因素结合在一起,使得路径追踪在如今的游戏渲染中变得不切实际。
在图形学界有一种说法:“光线追踪是未来的技术,并且将永远是!”这句讽刺暗示了这个问题的复杂程度,即使硬件速度和算法都有了巨大的进步,也总会有更加高效的方法来处理渲染管线中的特定部分。使用额外的开销,并且只使用光线投射和主要的可见性(深度缓冲),可能很难证明是合理的,目前有相当多的事实可以佐证它,因为GPU从未被设计用于执行高效的光线追踪,它们的主要目标一直是光栅化三角形,并且GPU已经在这项任务上已经变得非常擅长了。虽然光线跟踪的过程可以被映射到GPU中进行,但是目前的解决方案还没有任何固定功能的硬件对其进行直接支持。想要使用运行在GPU计算单元上的软件解决方案,来击败硬件光栅化是很困难的。
译者注:硬件方面出现了RT Core,专门用于构建加速结构和光线求交;软件方面出现了UE5的Lumen。
更加合理、但不那么纯粹的方法是,使用路径跟踪方法来处理光栅化渲染框架内难以实现的效果。我们对相机可见的三角形进行光栅化,但是在计算反射效果的时候,我们不再依赖近似的反射代理或者不完整的屏幕空间信息,而是通过路径追踪来计算。我们不再尝试模拟具有模糊效果的面光源阴影,而是直接通过向光源追踪光线并计算正确的遮挡信息。我们要发挥GPU的优势,对于硬件无法有效处理的元素,会使用更加通用的解决方案来进行处理。虽然这样的系统仍然是一个拼凑起来的系统,并且还是缺乏路径追踪的简洁性,但是实时渲染总是包含了各种妥协。如果要为了节省几毫秒而不得不放弃一些优雅简洁性,那它就是正确的选择,因为帧率是没有商量余地的。
虽然我们可能永远无法声称实时渲染是一个“已解决的问题”,但是更多地使用路径追踪将有助于将理论和实践更紧密地结合在一起。随着GPU的计算速度越来越快,在不久的将来,这种混合式解决方案应该可以适用于大多数应用程序,甚至可能会适用于最苛刻的应用程序。已经出现了一些基于这些原则构建的初始系统 [1548]。
译者注:上述思路即混合渲染管线(Hybrid Rendering Pipeline)。
光线追踪系统依赖于加速方案的使用,例如层次包围结构(bounding volume hierarchy,BVH),这个加速结构用于对可见性测试进行加速。有关这个话题的更多信息,详见章节19.1.1。一个原生的、简单的BVH实现其实并不能很好地映射到GPU上。如第3章所述,GPU会原生执行若干个线程组,这些线程组称为warp或者wavefront。一个warp是通过锁步(lock-step)进行处理的,即一个warp中的每个线程都会执行相同的操作。如果warp中的某些线程不执行代码的特定部分(例如分支),那么它们会被暂时禁用。出于这个原因,GPU中的代码应该以一种特殊方式进行编写,使得一个wavefront中各个线程之间控制流的发散最小化。假设每个线程只处理一根光线,那么这种方案通常会导致线程之间产生较大的分歧和发散。不同的光线将执行遍历代码的不同发散分支,并在这个过程中与不同的边界体积相交,其中的有些光线会比其他光线更早完成树结构的遍历。这种行为偏离了GPU的理想使用状态,即所有线程都在使用GPU的计算能力。为了消除这些低效问题,人们开发了一些遍历方法,来最小化线程分歧并重新使用提前结束的线程[15, 16, 1947]。
为了生成高质量的图像,可能需要对每个像素追踪成百上千条光线。即使是使用最优的BVH、最高效的树遍历算法和最快速的GPU,目前也只能在最简单的场景中实时做到这一点,而在稍微复杂一点的场景中则根本无法实现。在可用的性能限制下,我们所生成的图像会具有非常多的噪点,根本无法用于显示。然而幸运的是,这些充满噪声的图像可以使用降噪算法来进行处理,从而产生基本无噪声的图像,如图11.42和图24.2所示。最近在实时光追降噪领域取得了令人印象深刻的进展,并且开发出了一些算法,可以在每像素仅追踪一根光线的情况下(1spp),创建视觉上接近高质量的、路径追踪生成的图像 [95, 200, 247, 1124, 1563]。
2014年,PowerVR发布了他们的Wizard GPU [1158]。除了常规的功能之外,其硬件中还包含了构建和遍历加速结构的特殊单元(详见章节23.11)。该系统证明了使用固定功能的硬件单元来加速光线投射的能力和吸引力。见证未来可能会发生什么将是十分令人兴奋的!
补充阅读和资源
Pharr等人的《Physically Based Rendering》[1413]一书,是非交互式全局光照算法的优秀指南,他们的工作特别具有价值,这在于他们深入地描述了他们所发现的有用方法。Glassner的书《Principles of Digital Image Synthesis》[543, 544](现在是免费的),在物理方面讨论了光线与物质的相互作用。Dutre等人[400]的《Advanced Global Illumination》为辐射度量学和求解Kajiya渲染方程(主要是离线求解)提供了基础。McGuire [1188]的《Graphics Codex》是一本电子参考书,其中包含了大量与计算机图形学相关的方程和算法。Dutre撰写的《Global Illumination Compendium》[399]所参考的工作是相当古老的,但是它是免费的。Shirley的一系列短书《Ray Tracing in One Weekend》[1628]是一个廉价且快速学习光线追踪的方法。