PBRT阅读:第十八章 总结和结论

http://www.opengpu.org/forum.php?mod=viewthread&tid=7369&fromuid=10107

 

18 总结和结论


pbrt只代表了渲染系统设计这一广阔空间中的沧海一粟。 我们所做的基本决定---即光线追踪是几何可见性算法,蒙特卡罗算法是用于数值积分中主要方法---所有这些对系统的设计有广泛而深刻的影响。如果pbrt的设计着眼于实时性能,或者为艺术表达的最大灵活性而设计,那么就会使用全然不同的折衷技术。本章将回顾整个系统的某些细节,讨论一些可选的技术方案,并讨论一些主要的系统扩展。


18.1 设计回顾


pbrt设计中有一个基本假定,即我们要渲染的图像具有复杂的几何形体和照明。我们也假定为了生成这些高质量的图像(利用良好的采样模式,光线微分技术,反走样纹理等等),所耗费的计算资源是值得的。这些假定的后果之一就是pbrt在生成简单图像时的效率相对不高。


例如,在一台拥有3.02GHZPentium 4处理器的电脑上,pbrt用了2秒钟生成一个512x 512的没有任何几何体和光源的图像(这仅仅是一个全黑的图像)!只有数十个几何体素和几个光源的场景并不会比这个黑图像耗费更长的时间,这表面每个光线所花费的固定的时间严重影响了简单场景中求交和着色运算的时间耗费。很显然,所有计算采样、计算相机光线微分、以及累加光线的光线值的这些开销构成了渲染简单场景开销的重要部分。例如,在使用缺省参数设定的情况下,每个图像采样大概对16个像素有贡献值。如果计算光线辐射亮度的耗时极少,那么用一个并不简单的重构滤波器来计算图像值的耗费要比用“一个像素宽”的方盒滤波器进行图像重构的耗费要高得多。


当然,对于更复杂的场景而言,耗在光线求交、纹理求值、使用蒙特卡罗积分算法上的运行时间占了主要部分,而花在采样生成和图像采样滤波上的时间就无足轻重了。因为我们认为这些(复杂的)场景是更重要的渲染对象,所以某些系统部分对于简单场景的性能不佳的情形,就不再被视为急迫的问题了。


在基本设计中的另一个性能问题是,在光线交点出求BSDF的计算耗费比那些没有注重滤波纹理和光线微分计算的渲染器还要高。我们相信,通过追踪更多的相机光线来解决纹理走样问题,这种求BSDF的努力就会在总体上得以补偿。当然,对于简单的场景,纹理走样并不成问题。所以,当使用更多的采样来计算交点处的出离辐射亮度时,pbrt通常会表现得更好一些。例如,增加到面光源的阴影光线数目就分摊了计算BSDF的纹理所做的反走样工作,而通过增加相机光线来减少阴影中的噪声就不会那么有效率,因为需要在每个交点处计算BSDF


系统中某些接口过于简单导致了不必要的工作。例如,Sampler类总是计算镜头和时间采样,即使它们并不会被Camera用到,而Camera没有办法通知它的采样需要。类似地,如果一个Integrator没有用到所有GetSamples()的所有采样,那么Sampler生成这些采样的工作就白做了。例如,当光线跟几何体没有任何交点时,就会出现这种情况。


另一个可能会浪费掉的计算是,Shape总是计算它们法向量的偏导数,即使它们并不一定用得上。它们现在只是在BSDF有镜面反射成分并且为反射、折射光线计算光线微分时才被用到。当前确实没有办法知道在做光线/形体求交时BSDF是否有镜面分量。为此,就需要从材质计算BSDF,而材质需要得知交点处的微分几何信息才可以计算BSDF


解决这个缺陷的方法可以是允许材质描述它所需要的DifferentialGeometry中的成员(比如用设置标志的方式)。这些标志被传入到Shape的求交函数中,这样就可以避免设置不需要的成员变量。该方法来可以允许shape跳过(u,v)参数坐标的计算(如果不需要这些坐标的话),从而进一步地节省运行时间。


18.1.1 抽象与效率


在设计软件系统的接口时,一个令人头痛的事情就是在抽象和效率之间寻找合理的平衡。例如,许多程序员虔诚地将所有类中的数据申明为privatepublic函数来读取或修改数据项,对于小的类(如Vector)而言,我们认为这种方法不必要地隐藏了实现细节---这个类只有三个浮点数坐标,而这些细节是不会改变的。当然,如果不用信息隐藏技术,把所有类内部的细节暴露出来,也会导致维护上的噩梦。然而,我们认为在整个系统中将基本的设计决定比较明智地暴露出来并没有什么错误。例如,光线可以用一个点、向量和用来确定范围的两个浮点数表示,我们无需将它们隐藏在抽象层的后面。当诸如此类的信息被暴露之后,代码就会更简短,并容易理解。


在写一个软件系统并做这类的平衡时,另一个需要注意的重要事情是我们所期望的最后系统的大小。pbrt的核心代码(除去插件模块)包括所有的基本接口、抽象类、策略决定在内,不多于9000行代码。加入额外的功能通常只需增加插件中的代码。pbrt绝不会膨胀到百万级的代码量;这个事实可以也应该反映在系统的信息隐藏的程度之中。否则的话,为这种复杂的系统设计接口确实时间了。


 

18.1.2设计选项:只用三角形


虽然光线追踪算法可以处理很多类型的形体,但在实际应用中这个特性并非想象那样有用。大多数现实世界中的场景都是直接用多边形或平滑曲面(样条曲面和细分曲面,其求交算法要么难以实现要么很低效)所搭建的模型。所以,在实际应用中,为了做光线-形体求交测试,就必须将它们分解成三角形。


如果将光线追踪器的设计建立在底层形体表示如三角形上,并在管线中只对这种表示操作,那么就得到一些优点。这样的渲染器仍可以在场景描述中支持很多类型的体素,但最终会在做求交测试之前被三角形化。这个设计的优点包括:


1. 三角形顶点可以事先被变换到世界空间中,所以根本没有必要将光线变换到物体空间。

2. 可以对加速结构做些特殊处理,使它们的结点直接存放可以覆盖它们的三角形。这可以改进几何信息在内存中的局部性,并且使光线-体素求交测试在遍历例程中直接进行,而无需象pbrt那样通过两层的虚拟函数调用。

3. 如果所有体素可以分解成三角形,那么位移映射(几何体被细分成三角形,然后将它们的顶点用过程或纹理映射的方式扰动)可以更容易实现。

这些优点是很实质性的,因为既可以提高性能,有去除了系统许多地方的复杂度。对于一个商业级渲染器(而不是以教学为目的的pbrt)而言,这个选项是非常值得的。


18.1.3 设计选项: 流计算


当前计算机体系结构的最为令人激动的进展之一是流模型的处理器设计。流体系结构跟传统CPU不同之处在于它是高度并行化的,在单一的芯片上集成了多个计算单元,并有更好的内存局部性支持,在内存不同的层面上有高效的带宽。如果算法的设计基于使用流数据的计算核心模块,就可以在这类体系结构上得到实质性的性能提升。相反地,通用CPU只注重于很少几个计算单元的快速运行,而不是探索多个单元的并行性。


当代的图像加速器是流模型的一种实现。这使得大众化图形硬件的性能提升跟CPU相比要快得多--6个月性能就加倍(Hanrahan2002)。几乎所有当代计算机都有专业化的流处理器。


在解决如何使用单一芯片上的巨大数目的晶体管这个问题上人们对流体系结构寄予厚望。流处理器可以为额外的计算元素分配额外的晶体管,而传统的CPU体系结构无法支持这种使更多的计算单元保持忙碌的编程模型。这意味这CPU倾向于将这些晶体管用于硬件控制和片上内存缓存,以保证单一的管线可以全速运行。


有许多图形和媒体相关的应用程序可以很好地适应流编程模型。特别地,Purcell(2002,2003)Carr,Hall Hart (2002)演示了一个通用光线追踪器在流图形硬件上的实现。虽然这些系统并没有提高pbrt那么多的功能,但确实可以证明使用GPU做快速光线追踪的可行性。


为了写出运行在流体系结构上的高效代码,常常需要(跟CPU上的高效算法相比)不同的算法。例如,在流体系结构上向内存写数据要比在CPU上受更多的限制。然而,为流体系结构写软件的性能回报也是很大的,并且回报会越来越大。实际上,由于流体系结构性能上的巨大潜能,我们相信在本书的下一版中,整个系统将更注重流体系结构,而不是通用CPU


 

18.2主要项目


每一章后面的大部分练习是自成一体的,只包括对插件模块的修改和扩充,并没有对系统的总体结构做实质性的改动。本节将大致描述一些几个系统上的颇具野心的改动,其中包括对主要抽象层和接口的广泛改动。


18.2.1 并行渲染


由于光线追踪的巨大的计算量,当光线追踪算法刚刚提出之后,就引起人们对光线追踪的并行算法的兴趣。在当代CPU并行能力已经具备的情况下,研究人员开始使用几十个CPU做交互式的光线追踪了。例如,Parkter(1999)开发了运行在有64个处理器的共享内存式计算机上的交换式光线追踪器,Wald及其合作者在一个PC机群上可以以交互式的速率对复杂场景做光线追踪(Wald,2001,2002)Chalmers,DavidReinhard(2002)对平行光线追踪的最新动态做了很好的概述。


根据所采用的方法和所要使用的硬件,pbrt的并行化实现需要对系统的许多部分进行修改。一个选项是将系统定位在一个网络互联机群,每个进程运行在自己的内存空间中,并通过消息传递跟其它进程通讯。另一个可能性是,在一个内存共享环境中,有一定数目的线程共享同一个内存空间。本节将注重第二个方法中各种问题的讨论;对于第一个方法,可参见Wald的工作和Chalmer的书。


当多个独立的线程对同一内存区域进行存取时,要解决的核心问题是同步问题。我们要确保当一个线程不可以修改另一个线程正在读取的数据结构,否则就会出现不一致性的问题或无效的结果。解决这个问题的机制是互斥机制:这些线程必须相互协调,确保这种情况不可以发生。有时这个问题可以通过约定来解决:只有一个线程可以读或写某个数据结构,而其它线程无法对之存取,这个问题就不会出现了。


我们可以使用“加锁”这样更一般的机制,即线程可以请求一个锁对象的拥有权,然后在放弃,操作系统保证在任何时间不多于一个的线程持有这个锁。如果共享数据结构被锁保护住,所有的线程必须依照这样的约定:在读取或修改被保护的数据结构之前,线程必须取得相应的锁,这样程序就可以正确地运行了。这个思想很直接了当,却很难在一个复杂系统中得以一致性的、正确的实现。互斥相关的臭虫是很难重现的,因为有时运行没有任何错误,有时却由于造作系统的线程调度致使系统崩溃。


如果我们为共享内存架构对pbrt并行化,我们可以用一个线程来分析场景描述文件并建立场景表示,包括加速结构等等。这个阶段的运行是很难并行化的,因为所有的工作是创建数据结构,并且这一步通常不会是图像合成中的主要瓶颈。然后,我们创建多个线程来共同处理渲染构造,然后再用一个线程输出最后的图像并做清理工作。


一旦创建好Scene对象,并且所有渲染线程准备就绪,我们仍需一种机制来确定每个线程要做的工作。一个有效而直接的方式是将图像平面分区成不同的区域,并令不同的线程运行于不同的区域上,并负责所有相关的计算图像采样点贡献值的工作。

对图像分区既可以用静态的方式也可以用动态的方式。静态分区将每个区域赋给一个线程(注意区域数并不一定等于线程数)。然后每个线程可以各自工作于自己的区域,无需跟其它的线程通讯。因为有些图像的部分要比其它的部分需要更多的计算时间,更好的方式是令线程共享一个数据结构,用它来确定线程下一个要工作的区域。我们需要仔细地选择图像分解的粒度:如果分得太细,线程就会很快地完成了它们的区域上的工作,并将很多时间耗费在共享工作队列的等待上。另一方面,如果区域分得太大,那么一个运行在一个复杂区域的线程将耽误了整个渲染的完成,而其它线程不得不处于空闲状态。不论那种情况,我们都要修改Sampler,使之支持相应的分区方法。


在简单的光线追踪系统中,主渲染阶段是很容易被并行化的,因为除了输出图像之外,其它所有的数据结构都是只读的。因为这个原因,光线追踪常被称为“使人尴尬的并行”(embarrassinglyparallel)。在复杂的光线追踪系统中,要处理许多问题。幸运的是,pbrt中的许多类是线程安全的。例如,Shapes,Cameras, Filters, VolumeRegionsMaterial都只对传入的数据操作,它们被创建以后,并不会改变它们的成员数据。这样一来,当多个线程同时调用同一个函数时,就没有可能产生麻烦。


系统其它的部分,如Film类,就需要加锁。在pbrt中,多个线程为了每个图像采样加锁/解锁会导致性能低下,因为化在加锁/解锁的时间使得修改图像表示所需的时间变长。一个最容易的方法是,各个线程将采样贡献值积累到一块局部内存中,并定期地批量地传送给Film。另一个方法是,每个线程有一个单独的Film对象,无需加锁就可以修改它,当然所耗的内存要高一些。当完成渲染时,再将各个Film合并成单一的Film对象,然后做最后的处理并输出。


加速器也有相关的一些问题。如果加速器在创建阶段对所有的体素进行加细,那么加速结构就是只读的,就没有必要加锁。如果加速器在求交阶段才去加细体素,那么线程需要用一个“读写”锁来保护加速器。一个“读写”锁允许多个线程持有读锁来读取数据结构,但仅允许一个线程持有写锁,并且当没有线程持有读锁时才可以得到写锁。体素的加细和对加速结构的修改需要一个持有写锁的线程来完成。


多线程的信箱技术(mailboxing)会给加速器增加一些复杂性。用于给光线赋值ID的变量是潜在的竞争源,如果多个线程其它修改共享的信箱,就有可能引起麻烦。最后的解决方案是每个线程在给光线做信箱ID赋值时要独立于其它线程,每一个线程用局部的私有的内存变量来存放最后一条测试过的光线的ID


pbrt剩下的其余部分大多是线程安全的,当然如果对系统做并行化,还应该对每个模块的并行性问题做认真审核,以确保找到所有需要加锁的地方。例如,需要重写统计系统,使得在多线程的情况下得以正确的收集统计信息,PhotonlntegratorIrradianceCache的数据结构都要额外加锁。



 

18.2.2 处理场景复杂度增大的情况


如果有了良好的加速结构,光线追踪的一个优势是,当场景复杂度增加时,花在光线-体素求交的时间只是缓慢地增长。所以,光线追踪程序所能够处理的最大复杂度更多地受限于内存而不是计算时间。因为光线可以在相当短的时间内穿过场景中的许多不同的区域,所以,由于受制于内存存取模式的不连贯性,虚拟内存的性能就会很差。


为了增加渲染器能够处理的场景复杂度,一个方法是减少场景所需的内存。例如,现在pbrt用了300M内存来表示图4.1的生态系统的1百万个三角形。如果考虑处理几何复杂性的所有内存,那么每个三角形需要300个字节。以前我们写过每个三角形只占用大约40字节的光线追踪程序,其中包括了所有内存开销。很明显,内存耗费可以有很大的削减。


为了成功地做到这一点,我们需要仔细地着眼于整个系统对内存的使用情况。例如,在另一个系统中,我们使用了三种Triangle的实现,其中用一个8u_char来存放顶点指针,一个用16位,一个用32位。适用于网格的最小索引大小是在运行时间中决定的。


更复杂的方法是几何缓存(PharrHanrahan1996,渲染器在内存中只存放固定数目的几何体,最近没有被使用过的几何体会被丢弃。这个方法对于那些有大量的网格化几何体是很有用的,其中象细分表面这样的高层形体表示可以爆出一大堆三角形。当可用内存很低时,需要舍弃其中一部分几何体,并在需要的时候再重新生成它们。


对于这种缓存,我们可以对被追踪光线重新排序来改进它们的空间(进而内存)的连贯性,从而改进缓存的性能(Pharr,1997)Christensen(2003)提出了一个更容易实现的且更有效率的改进缓存性能的方法,他的光线追踪程序在几何缓存中用了场景几何体的简化表示。还有,Rushmeier,

PattersonVeerasamy(1993) 给出了一个用简化的场景表示来计算间接照明的例子。


18.2.3 子表面散射


BSDF和散射方程中有一个重要的假定:在点p上产生出离辐射亮度的唯一入射光源在该点也是入射(incident)的,即到达其它点p’的光不应该影响到在p的出离辐射亮度。


对于许多类型的表面,如人的皮肤,大理石,等等,这个假设并不正确,其中有相当大的“子表面光传输”的成分。图1.11模拟了雪地上的子表面散射,它给出了真实雪地的软漫反射的观感。进入这种表面的光在可以在表面下面旅行一段距离才从另外一个点上离开。

双向散射表面反射分布函数(BSSRDF)是描述这种散射过程的正规化描述。它是一个分布函数S(p’,ωi,p,ωo),它描述了“在点p上的沿方向ωo的出离辐射亮度微分”跟“在点p’上的来自方向ωi的辐射照度微分”之比。将散射方程一般化为BSSRDF需要对面积和入射方向进行积分。由于有两个维需要积分,就比散射方程5.6要复杂得多:

幸运的是,点p’距离点p有相当远的距离,对Lo(p, ωo)没有什么贡献。这个事实对我们实现子表面散射算法有很大的帮助。

在表面下的光传输可以用参与介质中的体光传输原理来描述,即用传输方程来表达。子表面散射跟光在云彩中的散射效果一样,只不过在一个很小的范围之内。对于子表面散射,我们可以对一般性的体光传输方法做几个有用的简化,因为最终我们感兴趣的只是离开表面的光的分布,而不是在参与介质内的光的实际分布。


pbrt现在严重依赖这样一个假定,即BSDF是表面反射模型的抽象层。为了支持子表面散射,pbrt需要加以扩充来支持描述透光材料的体散射性质的函数。更进一步地,积分器需要支持子表面光传输方法来计算反射。因为某些算法需要关于局部表面的(DifferentialGeometry无法提供的)更多的信息,包括在交点附近的表面内做点的移动的能力,所以Shape接口需要扩充来实现这些算法。


18.2.4 交互式渲染的预计算


蒙特卡罗光线追踪算法除了做用于显示的图像合成之外,还有其它应用。近来人们对几何模型的预计算算法感兴趣,这种预计算对模型如何响应任意(而不是特定的)照明分布的描述加以编码。这种信息可以用于扫描线渲染或交互z缓存渲染来计算基于任意照明条件的真实性着色。例如,预计算的辐射亮度传输(PRT)算法考虑到了几何模型中的光的相互反射情况,将其表达为更容易在交互式程序中求值的形式。


因为pbrt的总体设计着眼于最终的图像合成而不是这类的预计算,为给定模型做这种预计算所要求的系统扩充就不仅仅是写个新的SurfaceIntegrator类的问题了。基本的Integrator::Li()接口不够灵活,无法满足这些算法的需要。例如,PRT算法需要计算一个三角形网格顶点上对入射照明的反应编码所需的系数,积分器的任务就不是计算一组独立光线的辐射亮度值了。还有,Sample的结构不足以对要采样的地方进行自然的编码。


pbrt接口的另一个缺点是没有提供除了包围盒和光线-物体求交查询之外的场景几何存取方法;例如,我们无法对一个三角形网格的所有顶点进行迭代。这是一个有其意图的设计决定,因为将Shape所提供的函数的数目和种类达到最小,就很容易加入新的、非常规的形体。对于大多数的形体类型(如球),对其顶点做迭代操作没有什么意义。


一个解决所有这些问题的方法是写一个积分器重载函数Integrator::Preprocess(),并在这个函数中做预计算。场景描述文件可以设置为渲染一个1 x 1 大小的图像。在这种情况下,这个积分器负责确定模型上有那些点上的值需要计算,在Preprocess()中做好恰当的计算,在将结果写盘。


另外,我们可以给定一个适当的重要性函数(importancefunction并将放射辐射亮度函数作为一个任意函数(而不是场景中光源所定义的特定函数),那么我们就可以根据一系列的测度(利用测度方程来计算)来表示一些预计算辐射亮度传输算法。一个选项就是从一个特定的PRT方法推导出一个恰当的重要性函数,用测度方程得到相应的值,设计Integrator::Li()的一个替代接口,它用一个重要性函数的表示为参数,计算给定重要性函数的测度方程的估算值。


18.3 结论


在这5年里,pbrt从一个只支持斯坦福大学CS348b课程的学习系统演化成一个强壮的、功能丰富的、基于物理的和可扩充的渲染软件包。自从我们开展这个项目以来,我们学到了如何创建一个不仅渲染出漂亮图像的渲染系统,而且还可以让人们喜欢使用及修改的系统。最重要的是,我们设计了一个让其它人喜欢“读”的大型软件。这要比仅仅实现渲染算法本身更具挑战性(也更有成就感)。我们感谢你们阅读此书,希望你们喜欢读这本书,就像我们喜欢写这本书那样。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值