📄️ 1. 概览
我已经教授好几年图形学了。我通常通过实现一个光线追踪器来进行课程,因为这迫使学生自己写下所有代码。就算不调用 API,你仍可做出非常酷的图片。我决定将课程写成一个实战教程,带你尽快做出一个很酷的程序。虽然它不会是一个全功能光线追踪器,但其仍实现了间接光照 (间接光照使得光追成为电影中的常用技术)。跟随步骤,你会做出一个可拓展性极高的光线追踪器框架。若你对实现一个更进阶的光线追踪器感到兴趣,它应该非常容易改动。
📄️ 2. 输出图像
当你刚开始编写渲染器时,你应该想一个方法来显示图像。最直接的办法是将图像写入文件,但是图像文件格式实在是太多了。许多格式都非常复杂,所以我通常使用纯文本格式的 ppm 格式。这是 ppm 格式的 Wikipedia 介绍:
📄️ 3. vec3 類
几乎所有图形程序都有一些储存几何向量与颜色的类。在很多系统中,这些向量是四维的(几何中的三维位置加上一个齐次座标,或颜色中的 RGB 加上一个 Alpha 透明度分量)。为了我们的目的,三维座标便足够。我们将使用一样的 vec3 类来表示颜色、位置、方向、偏移量等等。有些人不喜欢这样,因为这无法预防你做出一些傻事,比如用一个颜色向量减去一个位置向量。他们的论点很对,但当没有明显错误时,我们将永远选择"代码最少"的路径。即便如此,我们的确为 vec3 声明了两个别名: point3 和 color 。由于这两种类型只是 vec3 的别名,如果你传递一个 color 给一个需要 point3 的函数,你不会得到任何警告,并且没有任何事会阻止你将 point3 和 color 相加,但这让代码更容易被阅读和理解一些。
📄️ 4. 光线、简单相机与背景
所有光线追踪器都具备的一个共同点,就是拥有一个 ray(射线)类,以及一个计算沿着射线能看到什么颜色的算法。我们可以把射线看作一个函数:$P(t)=a+tb$ 。在这里,$P$ 是三维空间中沿着一条直线的三维位置(坐标)。$A$ 是射线起点(ray origin),$B$ 是射线方向(ray direction)。射线参数 t 是一个实数(在代码中为 double 类型)。代入不同的 t 值,P(t) 就会沿着射线移动该点。如果加上负的 t 值,你可以到达这条三维直线上的任意位置。而对于正的 t 值,你只能得到 $A$ 前方的部分,这也就是人们常说的射线(half-line,或直接叫 ray)。
📄️ 5. 加入一个球体
让我们加入一个物件到光线追踪器。人们很常在光线追踪器中使用球体,因为计算一道光线是否击中一个球体相对简单。
📄️ 6. 表面法向量与多物体
首先,让我们获取一个表面法向量以进行着色。法向量是垂直于相交点表面的向量。
📄️ 7. 将相机代码移至它自己的类
在继续之前,现在是将我们的相机和场景渲染代码整合到一个单一新类的好时机:即 camera 类。camera 类将负责两项重要工作:
📄️ 8. 抗锯齿 (Antialiasing)
若你放大目前渲染出来的图像,可能会注意到边缘呈现出很明显的 “阶梯状” 粗糙感。这种阶梯状通常称为 “走样”(aliasing),或 “锯齿”(jaggies)。真实相机拍照时,边缘通常不会有锯齿,因为边缘处的像素往往是前景与背景的混合。注意到,与我们渲染出来的图像不同,真实世界中的图像是连续的。即,世界本身(以及它的任何真实图像)实际上都有无限分辨率。我们可以藉由对每个像素取一组样本并求平均,来取得类似的效果。