1.0 introduction

正文

01 介绍

渲染是这样的一个过程:它将一个3D场景描述成一张图片。显然,这像是在泛泛而谈,因为我们有很多种方式来实现渲染过程,并且每种方式的侧重点都不同。对于基于物理的方式,我们力求去表达场景的真实性——利用物理学中光学的知识。听起来使用基于物理的方式来渲染是一个直观的方式,但是这种渲染方式也仅在近10年才被广泛使用。在本章节最后的1.7小节,我们将简明地聊一下基于物理渲染的历史,以及近年来离线渲染在电影,游戏行业的发展。

这本书的书名叫 pbrt——基于物理的渲染系统(physical based rendering system),而这个系统背后的渲染算法是一个名为光线追踪的算法。让我们纵观一下其他讲述计算机图形学的书籍,它们大多数在行文时更侧重于算法思想和图形学理论,有时在关键的地方加入一小段代码辅助理解。读者在面对这些书学习时,通常会感到不踏实,无从下手。为了打破这种现状,我们这本书除了涵盖理论层面的讲解,还随书附带了一个功能完整的渲染系统实现!这个系统的源码(以及范例场景,材质等),都可以在官方网站 pbrt.org 上找到。

1.1 面向思路的编程

当 Donald Knuth 在开发TEX 排版系统时,发明了一种新的编程方法论,这种方法论极其简单而具有革命性——它认为程序的表达应该更遵照人类阅读的思路去表达,而不仅仅遵照计算机所理解的方式。Donald Knuth 称这种方法论为面向思路的编程(literate programming)。其实这本书(包括读者正在阅读的当前章节)涵盖了一个冗长的面向思路的程序(literate program)。这意味着,读者在阅读这本书的过程中,实际上可以阅读到pbrt渲染系统的完整实现,而不仅仅是抽象的描述。

面向思路的编程同时使用了文本格式的语言(比方说 TEX 或者 HTML) 以及高级编程语言(比方说 C++)。而且这两种语言可以互相转换,当我们需要理解这段代码发生了什么时,可以将高级编程语言转成文本格式来方便理解(思路上的理解),而当我们需要执行这段代码看效果时,也能无缝地将文本语言转换为高级编程语言让计算机执行(代码实现上的理解)。

举个简单的例子,考虑这样的一个函数 InitGlobals() 它用来初始化全局变量

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

尽管看起来很整洁,但是在缺乏上下文的情况下,我们很难理解这段代码的含义。比方说,为什么变量 nMarbles(弹珠的数目)的值是一个浮点数?为了理解这个变量的含义,我们可能要去找遍整个程序出现这个变量的地方,进一步猜测它的作用。尽管这种表达方式对编译器来说是友好的,但是它却不利于阅读。
对于面向思路的编程,我们可以这么表达 InitGlobals():

1
2
3
4
<Function Definitions> = 
void InitGlobals() {
<Initialize Global Variables 2>
}

我们定义了一个文本段,称为 <Funcgion Definitions>,它包含了一个函数 InitGlobals()。而在 InitGlobals 的函数体内,定义了另外一个文本段 <Initialize Global Variables>。由于函数体内的文本段还未定义,所以我们还无法完全理解整段代码的含义。
但,这是介于文本语言和编程语言之间一个恰当的抽象。当我们需要定义shoeSize时,可以这么写

1
2
<Initialize Global Variables> = 
shoeSize = 13;

在实际编译代码的时候,我们只要将文本段一一替换成代码即可(类似于C++宏)。当我们需要定义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>
}

同样的,在编译的时候,这些文本段都会被宏展开。在表述思路的时候,我们可以按思路依次去表达每个文本段的含义以及代码。这种拆分方式可以让读者一眼看出代码的思路,不至于陷入细节。通常一个文本段不会超过10行。

1.1.1 索引和交叉引用

下面我们将介绍一个特性,让我们更方便地找到需要的文本段:在定义文本段时,我们在边缘写下了页码,表示在这些页数引用了当前文本段,在附录C,我们收集了所有文本段的定义所在的页码。让我们看个定义文本段例子:

1
2
<A fascinating fragment> = 184,690
mMarbles += .001;

这意味着在184页和690页使用了这个文本段。有一部分文本段已经在之前的页码出现过,或者过于重复,我们就没有把页码列出。让我们看一个引用文本段的例子:

1
2
3
4
<Do something interesting> += 500
InitializeSomethingInteresting();
<Do something else Interesting 486>
CleanUp();

这表明我们引用的文本段在486页定义。