本来计划是最近一年专心写书,不要花心思和精力写博客的,因为写一篇优质的博客文章其实也要花费不少的时间构思的:单篇博客虽然文字少但是你可能需要花费更多的精力在有限的篇幅内包括更多上下文信息,以及更精简地组织内容,在我看来它的创作付出不亚于图书内容写作(当然如果作者对自己要求没那么严谨的话可能也没那么严重)。
《游戏引擎全局光照技术》采取了一种新的出版形式,它从写作第一章开始,就积极和社区互动并开始宣传,其方式和游戏发行的思路一致:即在开发阶段不断推出测试版积极和玩家互动,并收集反馈信息进行持续改进。这样做的好处是:读者较早获得试读版的信息,了解和监督了书的写作质量,从而可以做出有效判断是否值得购买,糟糕的图书质量可能在这一阶段就直接被淘汰,甚至失去了出版意义,从而保障读者利益;同时对于作者,我能够持续吸收社区反馈意见以改进内容质量,使图书的质量可以不断地形成增益,好的内容能够被社区传播扩散;这对于读者和作者都是一件共赢的事情。
为此,我已经提供了该书1,2,3三章共计157页正文的试读内容供所有对该书感兴趣的朋友免费下载,如果您还不知道可以从这里下载。然而尽管如此,从目前社区讨论的信息来看,大部分读者还是不太明白这本书跟其它同类书籍会有什么不同,尽管我已经使用一个问答的形式来简要概述这本书的特点,然而我们都知道,这些文字跟你在技术大会上那些厂商递给你的一个介绍他们公司产品的小册子上的文字一样:在你使用它们的产品之前,那些文字通常跟屁没有什么差别。
所以我需要写一点更通俗篇幅相对长一点的文章来解释这本书的内容和特点,通俗使得您可以像读其它社交信息一样很轻松地进行阅读,不需要太多思考和理解,而篇幅需要长到足以介绍这本书覆盖的内容和特点。正巧在百度贴吧的“孤岛危机吧”看到一篇想要了解各种全局光照技术的特点及联系的帖子,他所提出的问题正是本书试图去讨论的内容,所以我就想回答了他的问题其实基本上能够说明这本书的内容和特点。
学习全局光照技术(Global Illumination,以下简称GI)比较头疼的一个因素是它的方法派别太多了,每个算法可能涉及到完全不同的数学方法,而如果我们没有对方法本身的思路具有一定的理解,在运用方面也不会很顺手,尤其涉及到要进行优化或修改以满足特定需求时,你必须要比较全面的理解该方法:它的起源,历史,数学模型,新方法的优缺点,它在计算性能和图像质量方面有着怎样的折中等等;并且,通常每种方法并不像一个软件模块那样比较独立很好理解,每种GI算法往往涉及CPU/GPU的数据结构表述,内存布局以及访问,和渲染管线其它阶段(如Deferred Shading,AA等)的协作,图形接口的运用,算法处理器级别的优化等,这些因素使得GI的学习并不那么轻松。
所以我在2015年初就萌生了写作一本围绕各种GI技术来进行内容组织的书籍,它不像《Real-time Rendering》这类书籍一样基本上以各个理论知识点为中心,它以方法为中心(尽管如此,本书还是包括了将近300页的基本理论知识的介绍),着重讨论各种GI方法背后的思路以及方法之间的联系,因此它较偏理论性的书籍拥有更强的实践性。这种写作思路其实类似于《Advanced Global Illumination》(简称AGI),但是AGI基本上主要围绕路径追踪和辐射度理论两种方法来讲述,其它一些如光子映射等有一定的介绍,但是篇幅极短;本书不但会介绍路径追踪和辐射度理论等这些离线全局光照技术,还会介绍时下比较流行的距离场,体素等实时的全局光照技术,并且本书结合Unreal Engine等游戏引擎来讲述,读者更够更好地理解这些引擎的功能特性。
这篇文章,我们就来看看全局光照技术的进化史。遵循通俗的原则,本文不会包含数学公式,全部内容是以文字和配图描述,当然这样的方式肯定不可能包含很多细节,它更注重的是思路描述,更具体的信息还请您参考《游戏引擎全局光照技术》图书内容。
我们从光线追踪算法开始。光线追踪算法的起源可以早至1968年,Arthur Appel在一篇名为《Ray-tracing and other Rendering Approaches》的论文中提出的Ray Casting的概念,我们称为光线投射,如下图所示,光线投射其实只是一根单一的从一个点向一个方向发射出光线,它与场景中的物体相交时停止,Appel的算法中使用了View Ray和Shadow Ray两条光线,这计算出来的其实就是光照方程中的直接光照部分。
1979年,Turner Whitted在光线投射的基础上,加入光与物体表面的交互,是光线在物体表面沿着反射,折射以及散射方式上继续传播,直到与光源相交。如下图所示,这种算法形成了一个递归的光线穿梭,因此此时不再是一根单一的光线,而是形成了一个光传输的路径,此时的算法称为递归光线追踪(Recursive Ray Tracing,或者Whitted-style ray tracing);显然,光线在多个表面上反射或折射,间接光被考虑进来,光在经过每一个表面的时候,通过该表面的反射/折射率的“过滤”,它将物体的颜色渗透到邻近物体的表面上,这就是我们所说的Bleeding。
然而Whitted的模型是基于纯高光反射的,它假设物体表面绝对光滑,这显然和大自然中大部分物质的表面属性不相符。在计算机图形学中,一个像素的尺寸远远大于光的波长,在这个微观尺寸(Microfacet)下,物体表面是不光滑的,也就是说进入一个像素的多个光线可能分别被反射到不同的方向上,根据表面粗糙度的不同,这些散射的方向呈现不同的分布,非常粗糙的表面可能比较均匀地周围反射,而比较光滑的表面反射光则集中在光滑表面的反射方向附近。在现代渲染技术中,这些反射特性通常被使用Microfacet BRDF公式表述出来,它基本上使用一个简单的粗糙度方向就可以模拟出比较真实的光反射分布。结合金属性等一些参数,这就是目前流行的基于物理的渲染模型。
通常这些不同的分布被使用漫反射和高光反射两种分布来表述,如上图所示。1984年,Cook提出了分配光线追踪(Distribution ray tracing),他使得原来一束单一的反射光变为围绕一个空间中漫反射或高光反射范围内的积分计算,如下图所示。为了计算积分方程,蒙特卡洛方法被引入,所以Cook的方法又称为随机光线追踪(stochastic ray tracing)。
- Stochastic ray tracing
然而,Cook的模型计算代价非常高,每一条从摄像机发出的光线在表面点是被反射至多个不同的方向,分散成多束光线,以此递归,每条光线最终形成一个光线树(a tree of rays),尤其对于间接漫反射光,它几乎要反射至整个可见空间。
Cook的模型是由光照公式递归的特性决定的,光照公式中每个入射光的值来源于其它许多表面点反射的计算结果。1986年,Kajiya统一了光照公式,并推导出了光照公式的路径表述形式,使得光照公式由一个递归的结构,变成一个路径函数的积分,因此蒙特卡洛的每个随机数只要产生一条路径即可,这些路径不需要是递归的,因此每条路径可以随机生成,然后每个路径的值作为一个随机数用于计算最终的光照结果。这种新的形式称为路径追踪(Path tracing),如下图所示。在路径追踪算法中,首先在场景中物体的表面随机产生一些点,然后将这些点和光源以及摄像机链接起来形成一条路径,每个路径就是一个路径函数的随机值。这样的路径,根据场景的复杂度,每帧可能包括上亿条光线,因此传统的路径追踪算法很难运用到实时渲染领域。
- path tracing
上述这样的产生随机路径的方式有一个问题,有相当部分的路径组合由于表面间可能被遮挡而形成无效的路径,对最终光照没有任何贡献,因此大部分实现都是以增量的形式,在每个有效的反射或折射方向上进行随机采样,已形成更多有效的路径;
另一个问题是由于光源面积相对于整个场景很小,由摄像机出发的路径最终落在光源面积内的几率很小,因此双向路径追踪(Bidirectional path tracing)被提出,它分别从光源和摄像机两个方向出发,分别经过一定的路径之后,将该两条路径的终点链接起来形成一条完整的路径,这样大大增加了光源的有效贡献,可以从下图看出两者之间的区别。
当前大部分高质量的离线渲染器基本上都是基于路径追踪算法实现的,然而路径追踪的计算成本仍然非常高昂。现代路径追踪算法的发展主要有两大方向:其一是围绕提升上亿光线之间的连贯性(coherence)来提升处理器的利用率从而大幅提升计算效率,其二是微软Metropolis算法来使采样的随机路径更接近最终图像的真实颜色分布,后者称为MLT(Metropolis Light transfer)。
光线/路径追踪算法高昂的计算成本不仅来源于蒙特卡洛积分使用的大数定律要求的巨大的光线数量,另一方面重要的因素还在于路径追踪算法及其使用的数据结构不能适应现代CPU/GPU使用的执行模型。首先是内存执行模型,由于处理器计算单元的速度和内存数据存取的速度存在巨大差异,计算单元从内存中获取数据存在巨大的延迟(Latency),现代处理器非常严重地依赖于缓存技术,即将较大一块的内存数据缓存在具有更高读取速度的缓存中,如下图所示,缓存系统通常设计为多层机构,每一层比下一层具有更高的访问速度,但是更高速度的缓存硅片的成本更高,所以更高速度的缓存往往存储的数据量更小,这样顶层的一级缓存的速度跟寄存器的访问速度比较接近,通过缓存系统,计算单元到内存的数据访问延迟就被掩藏了。
缓存系统是根据传统应用程序的特点设计的,通常,相邻指令使用的数据在内存区域也是相邻的,所以相对寄存器更大一块的数据能够被多条指令使用。当指令从上一级缓存获取不到需要的数据时,就会从下一级缓存一次性获取另一块数据,并替换原来的数据,这种情况称为缓存失效。因此应用程序必须在数据上保持一定的连贯性,才能充分利用缓存特性,以提高计算性能。而路径追踪算法显然不符合这样的条件,每一条光线可能随机穿向任意一个方向,从而与环境中任意表面进行相交,所以相邻的指令使用的数据往往分散在内存中的各个区域,大大减少缓存命中的几率。
处理器架构的另一个特点是称为单指令多数据(SIMD)的计算模型,在SIMD中,寄存器一次性读取多个数据变量,这些数据被同一条指令执行,例如传统的CPU环境中SIMD寄存器可以读取128位数据,分别可以表示4个32位的数据,如下图所示,而在GPU环境下,每个GPU线程束可以一次性计算32个线程,当这32个线程所需要的数据在内存结构上相邻时,它们可以被一次性存储,大大减少每个线程获取线程带来的事务开销。
所以,基于连贯性(coherence)的路径追踪技术将这些数据分组成一些小的数据包,称为光线包(ray packet),这些数据包包含多个内存相邻的数据,并能能够被同一个指令执行。传统的基于光线包的技术主要是针对主要光线(Primary rays),即摄像机向场景发射出的光线,之后的光线可能向场景随处发射,并且对性能影响更大。
2013年,迪斯尼的Christian Eisenacher等在一篇名为《sorted deferred shading for production path tracing》的论文中提出了一种改进方法,这种方法的核心思想是在实际计算之前对光线进行排序,如下图所示。
在实际处理中,这主要分为三步(如上图左边的流程):首先,在每次增量计算光线与场景相交时,首先对光线进行排序,并将这些经过排序的光线按照方向以及数量大小分成多个包,然后以这些包为单位进行计算;其次,对场景建立一个BVH的加速结构,经过打包后的光线是连贯的,所以其相交计算涉及的场景表面在空间上也是连续的,因此能够比较友好地使用缓存和SIMD处理器特性;最后,由于来自各个方向光线可能与同一表面进行相交,所以在相交计算时直接计算光照反射并不是很高效,所以迪斯尼将着色分离出来,在相交计算的时候,所有相交点与纹理信息进行关联,纹理被划分成一些区域,然后着色计算以纹理区域为单位进行计算。通过以上这些优化,如下图所示,Disney新的路径追踪渲染器渲染性能得到极大提高,这些技术连同Disney的BSDF等技术一同被首次运用在《超能陆战队》以及后续的电影中。
基于连贯性的路径追踪算法的思想能够很好地利用现代处理器的架构特征,不管什么样的路径追踪算法基本上都可以在这方面进行改善,这也是传统路径追踪技术走向实时的方向。
基于Metropolis算法的MLT方法则着重于更准确地对路径进行采样,以计算更高品质的图像,毕竟传统的路径追踪算法很多路径采样的贡献率可能很低。Metropolis算法的核心思想是使用马尔可夫链(Markov Chain),它对当前随机数进行一个适当尺度的扰动(perturbation)以产生一个新的随机数,然后使用逼近真实分布函数的概率来对新的随机点进行取舍,这样使得新的采样点满足实际分布函数的分布。
由于路径追踪算法每个随机数产生的是一个路径计算的光照结果,因此MLT中的每一个随机数是一个路径,这个新的路径根据一定的策略对前一路径的某些部分进行扰动,以产生一条新的路径,然后计算出的该路径的光照结果就是每个随机数计算出的光照数值。
由于MLT中的每个随机数是一个路径的结果,整个路径的分布就是整个图像的颜色分布,因此MLT计算出的是整个图像的结果,然后每个像素点需要使用过滤器对每个像素周围的颜色值按照一定的权重比例进行加权平均。
原始的MLT算法直接在路径空间(Path space)对路径中的顶点进行扰动,这样新产生的路径可能存在被遮挡的可能,因此产生非常多的无效的被舍弃的随机数,Csaba Kelemen等于2012年的论文《A Simple and Robust Mutation Strategy for the Metropolis Light Transport Algorithm》中提出在一个超立方体(hypercube)作用域内,即所谓的Primary space对路径进行扰动,超立方体内的每个随机数是[0,1]范围内的均匀分布产生的随机数,这个随机数使用BRDF等分布的逆向变换算法求出实际的方向或者点灯随机数,这些随机数用于产生新的路径,这样的基于Primary space产生的随机路径具有较高的接受率。
更新的基于MLT的算法基于路径求导来产生更好的符合表面光照分布的扰动,例如对于高光,它能够在高光方向上选择更多的方向,以使MLT计算的结果更接近于真实图像的分布,如上图所示。关于路径求导,以及其它如基于梯度的MLT算法等这种使马尔可夫链产生的随机数分布更加接近图像真实分布的思路,是路径追踪技术方向目前比较热门的主题。