Physically Based Rendering翻译-Chapter1

是这里是对Matt Pharr、Wenzel Jakob以及Greg Humphreys三位大神所写图形学教材PBRT(Physically Based Rendering : From Theory To Implementation, Third Edition)的翻译,同时博主将书中代码依照C++11与C++14的新标准进行了改进。作者造诣不高因此可能出现错误,欢迎阅读到此文的读者在评论区提出。

需要注意的是,本文翻译是个人行为,未经官方认证,如有侵犯版权请立即告知。同时,翻译若与原文意思存在出入,以原文为准。译住将会以这样的格式出现。

这一篇是针对PBRT第一章:Introduction的翻译,希望对正在入门图形学的你有所帮助。

01 介绍

渲染是计算机将三维场景信息转换为图像的流程。显然,这是一个宏大的任务,而且这里也有许多不一样的技术来实现它。其中,基于物理渲染这一技术致力于营造具有真实感的渲染效果——在这种渲染技术中,通常使用真实的物理公式来模拟光线与物理的交互。当基于物理渲染的渲染方法被公认时,针对其的实践研究只进行了十余年。在本章末尾的1.7节简单介绍了基于物理渲染技术的历史,及其最近在影片制作中应用到的离线渲染方法和在游戏中应用到的交互渲染方法。

本书描述了一种被称为pbrt的基于物理渲染器,这一系统是基于光线追踪算法完成的。目前市面上的大部分计算机图形学书籍内容都专注于相关的算法与理论,有时会结合零星的代码碎片。与之相对的,本书在理论的基础上配套介绍了一个完整的可运行渲染器项目。这个项目的源代码(以及示例场景,还有一些渲染数据)都可以在pbrt的官方网站pbrt.org上找到。

1.1 文艺编程

原文在此使用的小标题为LITERATE PROGRAMMING。

Donald Knuth在使用TeX排版系统时提出了一种新的开发方法论,其基于一个简单但是革命性的想法,即程序文档的主要任务应当是向人解释它希望计算机做什么,而非指导计算机怎么做。他将这一方法论称之为文艺编程。本书(包含你现在正在阅读的这一章节)就是这样一个文艺编程的项目。这意味着在阅读本书的过程中,你将会看到pbrt渲染器的完整实现,而不仅仅是关于它的高层次描述。

斜体字部分在本书中的原文为:program should be written more for people’s comsumption than for computer’s comsumption.

这里采用的是Donald Knuth在其1983年发表的论文《LITERATE PROGRAMMING》中对文艺编程的阐述:Instead of imagining that our main task is to instruct a computer what to do, let us concentrate rather on explaining to human beings what we want a computer to do.

文艺编程是用一种混合了文档格式语言(像是HTML或TeX)和编程语言(像是C++)的元语言来书写的。这两个分离开的体系一起处理项目,就像是一个“纺织工人”正将项目转换成适合排版阅读的文档,而他的纺织工具则生产出可编译的源代码。我们的文艺编程系统是自研的,但它深受Norman Ramsey的noweb系统影响。

本书所使用的这一文艺编程方法的元语言具有两种重要的特性,其一是其具有将文本与代码结合的能力——这一特性使得项目的描述变得和真实的源代码同等重要,且其鼓励对项目的精心设计与归档。第二,这个语言提供了与编译器输入完全处于不同次序的向读者显示项目代码的工具。这意味着,我们可以以一个十分有条理的方式来描述项目。每一个被命名的代码块都被称作“片段”(fragment),而每一个片段都可以通过名字来引用其它代码块。

作为一个简单的例子,我们考虑一个名为InitGlobals()的函数,其用于初始化项目的所有全局变量:

1
2
3
4
5
void InitGLobals(){
nMarbies = 25.7;
shoeSize = 13;
dielectric = true;
}

尽管十分简洁,在没有上下文的情况下这个函数依然难以被理解。究其原因,举个例子,变量nMarbies可以使用浮点型数据赋值么?让我们来看看代码,查看全局变量的声明、使用方法来理解其作用以及数值的意义是必须首先完成的事情。虽然这个结构化的系统对编译器而言十分优雅,但人类读者不得不分开去逐个理解这些全局变量,阅读包含其声明与使用信息的代码。

而在文艺编程中,InitGlobals()会被这样表示:

1
2
3
4
(Function Definitions) ≡
void InitGlobals(){
(Initialize Global Variables)
}

这里定义了一个片段,它的名字是(Function Definitions),其包含了InitGlobals()这个函数的定义。而InitGlobals()函数本身又引用了另一个片段,(Initialize Global Variables)。因为初始化片段还没有被定义,我们只知道这个函数可能会对全局变量进行赋值,而对其他的事一无所知。这正是代码在现状下正确的抽象等级——因为现在还没有任何变量已经被声明了。当我们想要随后介绍全局变量shoeSize时,我们可以这样写:

1
2
(Initialize Global Variables) ≡
shoeSize = 13;

这里我们开始定义(Initialize Global Variables)的内容了。当文艺编程方法被混入源代码中一起编写时,文艺编程系统将会以代码“shoeSize = 13;”取代InitGlobals()中的(Initialize Global Variables)。在随后的文本中,我们也许会定义另一个全局变量dielectric,此时我们可以将其初始化代码附加至片段:

1
2
(Initialize Global Variables) +≡
dielectric = true;

片段命名之后的+≡符号表明我们想将当前代码片段加入到先前已经定义好的一个片段中。此时再进行合并这三个片段,将会得到这样的结果:

1
2
3
4
void InitGLobals(){
shoeSize = 13;
dielectric = true;
}

通过这种方式,我们可以将复杂的函数拆分成具有逻辑性的不同片段,让它们更容易理解。再举个例子,我们可以写下这样一个由一连串片段构成的函数:

1
2
3
4
5
6
7
8
9
(Function Definitions) +≡
void complexFunc(int x, int y, double *values){
(Check validity of arguments)
if(x<y){
(Swap parameter values)
}
(Do precomputation before loop)
(Loop through and update values array)
}

同样的,对于编译器而言,complexFunc()中的片段将会内联展开。在说明文档中,我们可以逐一介绍每个片段,并在其中穿插它们的实现。这种拆分使得我们得以一次仅展示几行代码,从而使其更容易理解。这种编码风格的另一大优点是,通过将代码拆分成具有逻辑性的片段,任意一个单一且已知的目的都可以独立地实现、验证以及阅读。通常来说,我们会保证每个片段的代码量少于十行。

在某种意义上,文艺编程系统仅仅是一个用于重新排列项目代码的宏替换包。这看起来似乎是一个琐碎的改进,但事实上文艺编程与其他结构化软件系统具有很大的不同。

1.1.1 索引与交叉引用

由于原书在此仅介绍了原文中各种序号及其代表含义,而在线博客无法真实有效地对其内容进行适当反应,因此该部分被略过,未做翻译。

1.2 真实感渲染与光线追踪算法

真实感渲染的目标是将三维场景渲染为与真实世界相同场景照片之间难以分辨的图像。在我们谈及渲染流程之前,理解“难以分辨”这一个词的含义是极其重要的,因为它需要人类评价,而不同人给出的评价是不一样的。虽说在这本书中我们会讲述到一点人类感知的相关内容,但精确描述给定的观察内容是一个非常困难且难以解决的问题。在大多数情况下,我们会对光照本身的物理模拟和其与物体之间的正确交互感到满意,并依赖我们对技术的理解来向观看者展示最佳的图像。

绝大多数真实感渲染器都依赖于光线追踪算法。光线追踪确实是一种十分简单的算法,它基于追踪光源发射出的射线穿过屏幕时与场景中物体交互、在场景中反射的路径来实现。虽然有许多种方法来实现路径追踪器,但所有这类系统都至少模拟了以下的物体或现象:

  • 相机(Camera):一个相机模型决定了场景从哪里开始能被观测、以及怎样被观测,同时也定义了如何将场景记录在探测器上。许多渲染器都从相机处向场景发射并跟踪观测射线。

  • 光线与物体的交互(Ray-object intersection):我们必须精确地知晓光线与物体之间交互的位置。此外,我们也需要确定在交互点进行交互的物体信息,例如其表面法线或是材质。大多数射线追踪器也针对光线和多个物体之间的交互测试做出了特殊设计,最经典的做法是返回穿过光线的最近物体。

  • 光源(Light sources):如果没有光,对场景的渲染就没有了意义。我们使用光线追踪器来模拟光在场景中的分布,其中包含对光源本身的定位,及其在空间中分配能量的方式。

  • 可见度(Visibility):为了判断是否应该让物体表面上的某一点p接收能量,我们必须知道在这个点是否有一条连续不断的通向光源的路径。幸运的是,这个问题在光线追踪中很容易解决——我们只用构造从物体表面到光源的射线,找到离该p点最近的射线与物体之间的交点,并且比较这条线段的长度和光源可到达的距离大小即可。

    这一段的后半部分不好理解,故贴上原文: Fortunately, this question is easy to answer in a ray tracer, since we can just construct the ray from the surface to the light, find the closest ray-object intersection, and compare the intersection distance to the light distance.

    p点为译者方便翻译所添加,原文无。译者所理解的原文在这里想表达的意思是:在光线追踪中很好判断物体上一个点是否可以被光源——由于光源本身被视为一个物体,我们只需要从待测的原物体上该点向光源发射一条射线,求出这条射线所碰撞到的第一个物体与原物体之间的距离,将其与原物体和光源之间的距离作比较即可。如果原物体与连接到的物体之间距离小于原物体与光源之间的距离,就说明二者中间有遮挡物,不能被照射到,否则就可以被照射到。

  • 表面散射(Surface scattering):每一个物体都应该以一种方式描述其表面信息,包括光线与其的交互方式,以及光场对其辐射的性质。表面模拟通常是参数化的,以方便模拟多种不同的表面。

  • 间接光传输(Indirect light transport):光线可以在经过多次反射或是穿过其他表面后再抵达一个表面,通常来说要追踪源自表面的其它射线才能完全捕捉到这种效果。

  • 光线传播(Ray propagation):我们需要知道光线在穿过一段空间时会发生什么。如果我们正在渲染真空中的场景,光线的能量将始终不衰减。虽然完全的真空在地球上难以见到的,但许多环境都有可能与之相似。更多常见的模拟都尝试过使光线穿过烟雾、地球大气等等。

在这一节中我们会简要介绍以上这些模拟中的要素。在下一节,我们将介绍pbrt的与底层模拟组件之间的高级接口,并通过主渲染循环来模拟单一光线的进程。我们也会使用Turner Whitted的original tracing算法来实现表面散射模拟。

1.2.1 相机

几乎所有人都用过相机且熟悉它的基本功能:你表现出你想要记录这个世界的欲望(通常是通过按按钮或是敲击屏幕),然后图像就被记录在了胶卷或是电子传感器上。有一种最简单的拍照设备,叫作针孔摄像机。针孔摄像机由一个不透光的盒子组成,这个盒子的一端有一个小孔。当这个小孔没有被遮挡时,光会进入小孔,并被盖在盒子另一端上的一张成像纸所接收到。尽管这个装置十分简单,这种相机在今天仍然在被使用,以创造一些艺术效果。对它来说,使胶卷长时间曝光以形成图像是十分必要的。

这里的针孔摄像机(Pinhole camera)指借助小孔成像原理记录图像的摄像机。

虽说绝大部分相机与之相比都更加复杂,但了解针孔摄像机是对学习渲染仿真来说很好的一个切入点。相机最重要的功能就是去定义场景的哪一部分会被记录在胶片上。在图1.1中,我们可以看见针孔摄像机是如何连接针孔(Pinhole)与胶卷边缘(Film)并向场景中创建双向视锥的。不在这个视锥中的物体就无法被捕获到屏幕上。由于现实中的相机成像往往会形成一个更为复杂的锥体,我们将可能被成像到胶片上的空间区域称之为视体(Viewing volume)。

Fig1.1

Fig1.2

另一种思考针孔摄像机的角度是,我们可以假设将针孔摄像机的胶卷平面放置到小孔的前方,但仍与小孔之间具有相同的距离(如图1.2)。同时,我们将小孔与胶片连接后定义了与之前相同的视体。显然,这不是现实中的制作相机的方法,但是就模拟而言是一个非常方便的抽象概念。当胶片(或者说成像区域)在针孔的前方时,小孔通常会被称为视点(eye)。

现在我们需要面对在渲染中一个十分关键的问题:对于图像上的每一点而言,摄像机应该记录哪种颜色?如果我们回到原始的针孔摄像机,我们应该意识到,只有针孔与胶片上某点之间单一的光线向量可以向这一点(的颜色)发出贡献。在我们将胶片放置在视点前方之后的假想相机中,我们更关注从图像上一点到视点之间的光量。

因此,相机模拟的一个重要任务就是从图像上取得一点,并生成多条对该点位置产生贡献的入射光线。由于一条光线仅由一个原点和一个方向向量组成,对于针孔相机模型来说这个任务就非常简单了。如图1.2所示:我们使用小孔来作为原点,再从小孔向近平面处的连线中取得方向向量即可。(6.4节描述了如何实现这样的一个模型。)

通过相机模型中封装好的方法来将图像的位置信息彻底转化成光线的流程,渲染器的余下部分将可以只与光线交互,同时我们也可以建立起一个可变的动态相机模型了。pbrt的相机模型抽象概念将在第六章详细地描述。

1.2.2 光线与物体的交互

每当相机生成了一条光线,渲染器针对它的第一个任务就是确定其碰撞的第一个物体以发生这一碰撞的位置。这个碰撞点一定是射线上的一个可见点,而我们想要光线在这个点产生的碰撞情况。为了找到这一碰撞信息,我们必须测试光线与场景中所有物体之间的碰撞关系,并选择其中第一个发生碰撞的。给定一条射线,我们首先以参数化的形式定义它:

代表光线的源点,代表光线的方向向量,而则规定了射线的范围,其取值为。我们可以通过控制参数的值并依照上述式子进行计算来取得射线上的任意一点。

不难发现,求得光线与隐式几何面的交点是十分容易的一件事。我们首先将光线的表达式代入隐式几何面函数中,然后创造一个以为未知数的方程。然后我们只需要解出这个关于t的方程的根,并将最小的根带入射线的表达式中即可得到交点。举个例子,对于一个半径为的隐式几何表达的球体而言,其表达式为:

​ 将射线的表达式代入其中,我们可以得到:

之外的所有值都是已知的,于是该方程就变成了一个容易求解的与t有关的二次方程。如果这个方程不存在实根,那么射线就没有和球体相交。如果存在任意实根,那么其中较小的那一个就是二者的交点。

单独只得到一个交点并不足以完成光线追踪;它还需要得到曲面的一些特征。首先,该点物体的材质必须被读取并传递到随后光线追踪的算法中。其次,当进行着色时需要交点的额外几何信息,比如说他的法线。尽管大量的光线在同一点上都会得到同样的的,像pbrt这样更复杂的渲染系统都需要更多的额外信息,比如点和表面法线关于局部参数的偏导数。

当然,大多数场景都会有不止一个物体。一个暴力的程序将会让光线一轮又一轮地对所有物体进行碰撞检测,并选择在所有检测中最小的实数值。这样的程序虽然是正确的,但是运行十分缓慢,以至于对于中等复杂度的程序来说也是如此。一个更好的程序将会包含一个加速结构,在光线追踪的环节以组的方式同时测试多个物体。这种剔除不相干几何物体的方法可以在的时间内快速运行,其中是图像上像素的数量,则是场景中物体的数量。(然而创建加速结构本身则需要至少的时间。)

pbrt各种形状的几何理论与实现在第三章会提及,加速结构的理论及其实现则在第四章详细阐述。

1.2.3 光照分布(Light Distribution)

光线与物体之间的交互向我们提供了一个待着色的点以及该点的一些本地几何信息。回到我们的最终目标上来——得出从相机方向上离开该点的所有光照,为了完成它,我们需要知道有多少的光线到达了这个点。这涉及到场景中的光(在该点)几何与辐照度上的分配。对于一个十分简单的光源(例如点光源)而言,只要有光的位置信息,求出其几何距离是十分容易的一件事。然而,点光源在真实世界中并不存在,而更加具有物理性质的光源往往是区域光源。这意味着光源是与从表面向外发光的几何物体相关联的。然而,我们在这一节将会使用点光源来计算光照的分配;对光照测量的更复杂的探讨是第五章和第十二章的主题。

我们常常会想要知道光在一个微分区域周围交点上的强度(如图1.3)。我们会假设点光源具有的能量为,且它同时向所有方向发射能量,由此可以推出,每个单位圆区域周围的能量为。(这些数据将在第5.4节做进一步解释。)

如果我们考虑两个球体(如图1.4),很明显大一点的球体上每个单位区域上的能量要少于小一点的球体。具体来讲,对于半径为的球体,其每单位所接收到的能量为总能量的。进一步思考,若有一个与微小区域,其与从物体表面点到光照点之间的向量的夹角为,则积累在该区域上的能量将占用总能量的比例为。把所有的内容合并在一起,则单位面积下的单位微分能量(differential irradiance)为:

differential irradiance 即 radiance。

对计算机图形学中的基础光照已经十分熟悉的读者会注意到这个式子中两条相似的编码规则:余弦值将随着倾斜角的上升而降低,同时随距离的升高球体上单位区域的面积也将降低。

1.3¥1.4.jpg

具有多个光源的场景也很容易推导,因为光照是线性的——任何一个光源的贡献都可以被单独计算,并最终加和到一起计算总贡献。

1.5.jpg

1.2.4 可见度

前一节里对光线分配的描述忽略了一个十分重要的内容:阴影。任何一个光源对单一点贡献的光照仅在从该点到光源位置没有障碍时会被计算。(如图1.5)

幸运的是,在光线追踪中很容易就能确定光源对于某一个点是否是可见的。我们通常会构造一条新的从物体表面上一点出发、向光源方向的射线,这种特殊的射线一般被称为阴影射线(shadow rays)。如果我们在环境中追踪这条射线,我们就能通过比较射线最先碰撞到物体的值是否与碰撞到光源时的值一致来确定是否有任何其它物体与这条射线相碰撞。如果没有任何的遮挡,才将这个光源的贡献计算进来。

1.2.5 表面散射

我们现在可以处理两部分对于在一个点上着色而言比较重要的信息了—:它的本地空间信息以及光照信息。现在我们需要确定光源是如何在表面上散射的。具体来说,我们现在要将注意力转移到新的部分上来——即对物体表面在接收到光照之后散射回的能量,因为这部分能量将射向相机。(如图1.6)

1.6.jpg

场景中的任何物体都具有一个材质,用于描述物体表面上任意一个点所应用的性质。这个描述由双向反射分布函数(bidirectional reflectance distribution function,BRDF)所赋予。这个函数可以告诉我们有多少能量将从入射方向向出射方向反射。我们将会用点与函数来表示BRDF。现在,计算光照度向相机散射的能量就十分直观了:

1
2
3
4
5
for each light:
if light is not blocked:
incident_light = light.L(point)
amount_reflected = surface.BRDF(hit_point, camera_vector, light_vector)
L += amount_reflected * incident_light

现在我们用符号来代表光照。这一替代符号和光照度的度量稍有不同,因为我们使用替代的是辐照度(radiance),一个表示单位面积里单位光照度的符号,我们随后会更多地看到它。

我们可以很轻松地将BRDF的概念套用到光线的传播上(这样我们就得到了BTDF),或是套用到常规的从表面任意一边进行的光线散射。一个描述更常规的散射的方法被称作双向散射分布函数(bidirectional scattering distribution function,BSDF)。pbrt就使用了BSDF模型,它们将在第八章被详细介绍。更复杂的还有双向次表面反射分布函数(bidirectional subsurface scattering reflectance distribution function,BSSRDF),其用于模拟光线从一个与进入位置不同的新位置退出的模型。BSSRDF将会在5.6.2节、11.4节和15.5节提到。

1.2.6 间接光传输

1.7.jpg

Turner Whitted在1980年发表的有关光线追踪的原论文中强调了它的递归性质,而那是使得间接光照在渲染中的镜面反射与传播成为可能的关键。举个例子,如果一根光线从相机撞击到发光点,比如说镜子,我们就可以依据该点的法线信息反射这根光线,并递归地调用光线追踪的方法来找到抵达镜子上这个点的光线,增加他们的贡献道原先相机的光线中。同样的技巧也可以被用在光线在透明物体之间的传播上。在很长时间,最早的光线追踪示例都选择展示镜子与玻璃球作为渲染结果,因为这类效果在其它渲染技术里是难以实现的。

通常来说,从物体上一个点到达相机的光照即为这个点所发出的光照(如果它自己是一个光源)以及它反射的光照之和。这个结果是由光线传播方程(也就是著名的渲染方程)推导而来的——从任意一点沿着方向发射出的辐照度为这一点在该方向上所发出的辐照度,加上其在以点为球心的球面空间上从所有方向接收到的辐照度大小依照BSDF和余弦值修正后的值。即:

我们随后会在5.6.1节与14.4节展示这一公式的更多派生类别。在最简单的场景中是无法得出对这一公式的完整分析的,所以我们必须创造一个对积分的假设,或是使用数值积分手段。

Whitted的算法通过忽略大多数方向的入射光,并将探讨局限在光源、全反射以及折射方向上的来简化了这个积分。在其它的相关工作里,策略变成了对一小部分方向上的光进行积分。

Whitted的方法可以可以被扩大到捕获更多效果而非仅仅是完美镜面或是玻璃上。举个例子,通过追踪镜面反射方向周围的大部分的递归光线,并将其贡献平均,我们就得到了一个光滑反射的近似值。事实上,当光线发生与物体的碰撞时,我们可以一直递归地求解它。举个例子,我们可以随机地选择一个反射方向,然后通过BRDF对一条新射线的贡献加权。这是一个用于得到十分真实的图像的简单但是有力的策略,因为它可以捕获所有光照在物体之间的相互反射。当然,我们需要知道什么时候应该终止递归,而完全随机地选择方向则可以使得渲染算法变得缓慢但得出负责任的结果。这些问题可以被解决,但它们是第十三章到十六章的主题。

当我们以这种方式递归地追踪光线时,我们实际上是在将一棵由光线构成的树与图像的各个位置相关联(如图1.8),而从摄像机发出的光线在这棵树的根部。这棵树中的每根光线都具有一个与之关联的权重,这允许我们对例如闪光表面这样并非百分之百反射入射光的表面进行模拟。

1.8.jpg

1.2.7 光线传播

这一探讨经常假定光线是在真空中传播的。举个例子,当对点光源的分配进行描述时,我们假设光照强度是平均分配在一个以光源点为球心的球形表面上的,且不存在衰减。像是烟雾或是泥浆等等的折射介质的存在可以推翻这一假设。这些效果对于模拟而言是重要的——即使我们并未对一个充满烟的房间进行模拟,大多数的户外场景都实质性地受到折射介质的影响。举个例子,地球的大气就会导致远处的物体看起来更模糊(如图1.9)。

1.9.jpg

1.10.jpg

有两种方法来表现光线在折射介质中的传播。首先,介质可以通过吸收或是将其散射至其他方向的方式来消除或是减弱光。我们通过计算光线原点到碰撞点之间的透射率(transmittance T)来取得这一效果。透射率可以告诉我们光在碰撞点处被散射了多少。其次,折射介质也可以只用于影响一条光线,者可以在折射介质本身发光(例如火焰)或是介质从另一个方向的散射回到了光线所在方位(如图1.10)。我们可以通过计算体积光传播方程来求得这个量,就像我们计算光线传播方程的表面反射光量一样。我们将在第十一章和第十五章再对折射介质和体积渲染进行探讨。现在,我们只用知道计算折射介质的效果并将其效果纳入光线计算之中是可行的即可。

1.3 pbrt:系统概述

pbrt是使用标准面向对象技术构造的:抽象基类被定义为重要的实体(比如,一个形状的抽象基类定义了所有几何形体必须继承的接口,光照的抽象基类也在光照上进行了同类别的定义。)。这个系统的大部分通过抽象基类所提供的基类来实现。举个例子,用于确认点与光源之间是否被物体遮挡的代码将会直接调用物体继承自Shape并自己实现的相交方法,而无需特地考虑场景中的某一物体。这种方法使得扩增系统变得更简单了,当我们想要添加新的形状时,只需要继承一个Shape类并实现它的接口就可以将其链接进系统中了。

table1.1.jpg

pbrt使用了总共十个抽象基类,如表格1.1所示。添加一个这些类型的新的实现是十分直观的。接口必须继承自适当的基类,并被链接入可执行文件之中,最后B中的对象创建例程必须被修改,以便于在解析场景描述文件时根据需要创建对象的实例。我们在B.4节讨论了对这一系统的更详细的额外扩充。

pbrt源代码被放置在pbrt.org。(一个大的包含多个样例场景的测试集被分离出来并可以单独下载。)所有的pbrt的核心代码都在src/core目录下,而main()方法则包含在src/main/pbrt/cpp之中。许多抽象基类的实现被分离出来,src/shapes目录下放置了继承自Shape抽象基类的实现,src/materials目录下则是实现了Material的子类,其余的四个也都如此。

在本节中还包含有许多使用pbrt渲染的图像。它们之中,图1.11和图1.14是值得注意的:不仅仅是因为它们在视觉上令人印象深刻,还因为它们都是由完成本课程学习后的学生所制作的大作业内容——使用新的功能来拓展pbrt以生成生动的的图像。这些图像是课程中最优秀的作品。图1.15和图1.16是由LuxRender渲染的。LuxRender是一个基于GPU的渲染系统,且由本书第一版中的pbrt源代码演变而来。(浏览www.luxrender.net以获取更多关于LuxRender的信息。)

1.11.jpg

1.31 执行阶段

pbrt可以从概念上将执行阶段分为两部分。第一,解析用户提供的场景描述文件。场景描述通常使用一个txt文件,其制定了场景中出现的几何形体、它们的材质属性、将之照明的光源、虚拟相机在场景中被摆放的位置,以及系统中个别算法所使用到的参数。输入文件中的任何信息都直接被映射到样例地图Appendix B中,这些样例程序包含描述场景的程序性接口。场景文件标准由文档所规定,详情查阅pbrt的网站,pbrt.org

在解析阶段的末尾是一个场景类的实例以及一个积分器的实例。这个示例场景包含了所有场景内容示例(几何物体,光照,等等),而积分器实现了一个渲染它的算法。它之所以被命名为积分器是因为其主要任务就是计算表达式中的积分。

1.12.jpg

当场景已经被确定后,第二个执行阶段就开始了,而且主渲染循环也会开始运行。这个阶段通常是pbrt主要的运行时间,这本书中的大多数内容都在描述这一阶段的代码。渲染循环通过调用Integrator中的Render()方法来执行,在1.3.4节我们会注重讲述它。

1.13.jpg

本章会描述一个积分器的子类,名为SamplerIntegrator,其Render()方法确定了模拟图像形成的过程中产生大量光线里能到达虚拟胶片位置的光线。在计算完所有的胶片采样贡献之后,最终图像被写入一个文件中。缓存中的屏幕描述数据被清除,系统随后重新从屏幕描述文件中开始渲染进程,直到没有更多剩余物。如果有需要,将会允许渲染另一个用户指定的场景。

1.3.2 场景表示

pbrt的main()方法可以在main/pbrt.cpp中找到。这个方法相当简单,它首先包含了读取命令行指令argv的一个循环,初始化Options结构体中的变量,并存储c参数中提供的场景文件名。运行带有指令–help的pbrt将会输出所有明确规定的指令。解释命令行参数的片段(Process command-line arguments)十分直截了当,但没有在本书中列出。

这一选项结构体之后会通过pbrtInit()函数进行全系统的初始化,随后main()函数将会解析给出的场景描述,创建Scene对象与Integrator对象。在所有渲染都的完成后,pbrtCleanup()将会进行系统退出前最后的清理工作。

1.14.jpg

这里省略了对页面角标介绍的翻译。

1.15.jpg

1
2
3
4
5
6
7
8
9
10
(Main program) ≡
int main(int argc, char *argv[]){
Options options;
std::vector<std::string> filenames;
(Process command-line arguments)
pbrtInit(options);
(Process scene description)
pbrtCleanup();
return 0;
}

如果pbrt在没有提供文件名的情况下运行,场景描述就会从标准输入中读取。否则,它会在每轮循环中通过提供的文件名依次处理每个文件。

1
2
3
4
5
6
7
(Process scene description) ≡
if(filenames.size() == 0){
(Parse scene from standard input)
}
else{
(Parse scene from input files)
}

ParseFile()函数将会从标准输入或是桌面上的一个文件输入中解析场景描述文件。如果无法打开文件,它将会返回false。解析场景文件的技术在本文中不会阐述。解析器的实现可以在lex、yacc文件以及core/pbrtlex.ll和core/pbrtparse.y中找到。读者如果想要理解这一解析子系统,但并不熟悉这些工具,可以询问Levine、Mason和Brown(1992)。

1.16.jpg

我们使用常见的UNIX习惯用语,将文件名以"-"来指代标准输入。

1
2
(Parse scene from standard input) ≡
ParseFile("-");

如果有一部分的输入文件无法被打开,Error()程序将会向用户报告这些信息。Error()使用同样的格式化字符串语句作为printf()。

1
2
3
4
(Parse scene from input files) ≡
for(const std::string &f : filenames)
if(!ParseFile(f))
Error("Couldn't open scene file \"%s\"", f.c_str());

场景被解析后,代表光照和原始几何信息的物品将被构建到场景中。它们都存储在Scene对象里,并通过在第B.3.7节里讲述的Appendix B文件中的RenderOptions::MakeScene()方法创建。这个Scene类在core/scene.h中被声明,在core/scene.cpp中被定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
(Scene Declarations) ≡
class Scene{
public:
(Scene Public Methods)
(Scene Public Data)
private:
(Scene Private Data)
};

(Scene Public Methods) ≡
Scene(std::shared_ptr<Primitive> aggregate),const std::vector<std::shared_prt<Light>> &lights &lights) : lights(lights), aggregate(aggregate){
(Scene Constructor Implementation)
}

场景中的每个光源都由一个光源物体代表,以指定其形状并描述其发出的能量。场景使用一个标准库中一个shared_ptr类型的vector来存储光源。pbrt使用shared pointers来追踪有多少对象被其它实例所引用。当最后一个持有引用(实际上就是Scene)的物体被销毁后,此时引用数会变为0,Light对象将被安全释放,而这一切都是自动的。

有一些渲染器支持依照几何物体来分离光照列表,并允许光源只照亮一部分物体,而这个特性在pbrt这样一个基于物理的渲染应用中是不被考虑的——pbbrt只有单独的全局光照列表。这一光照系统的许多部分基于光线来完成,因此Scene将它们都作为公有对象访问层级。

1
2
(Scene Public Data) ≡
std::vector<std::shared_ptr<Light>> lights;

每个场景中的几何物体都使用一个Primitive来指代,以组合两个对象:Shape用于说明其几何性质,Material则描述其外观(比如说物体的颜色,以及其是否粗糙等等)。所有的几何原始信息都从Scene的成员变量Scene::aggregate中的单个Primitive组合而来。这个aggregate是一种特殊的原始信息,其持有对其它原始信息的引用。由于它实现了Primitive的接口,因此它与系统中其他地方出现的单一原始信息没有什么不同。aggregate的实现以一个加速结构存储了所有场景中的原始信息,这个结构减少了与远离给定射线的物体之间的不必要交互次数。