0%

SlowPT 总结

image

这个系列总算是做完了。

这篇会以 top-down 的逻辑解释整个项目做的事情。主要讲原理、关键概念和实现的时候遇到的坑,因此篇幅会比上一个要短……吧。

写的逻辑可能不太通顺,更像是旁注类型的笔记。覆盖的内容是 Ray Tracing: The Next Week 以及 Ray Tracing: The Rest of Your Life

以及 自己实现的代码

1 光线追踪概念

这部分解释一下项目大概是个啥。首先说计算机图形学是什么。

简单来说,计算机图形学的研究对象是所有人眼可以看到的事物,但与 CV 不同的是,图形学研究如何利用计算机将这些对象建模并以图像等方式表示出来,而不是从表示好的数据中恢复原始的物体信息。图形学在建模、渲染、动画和人机交互等方向的研究,都是为了这一个目标而努力。

1.1 光线追踪做什么

渲染基本上是图形处理的最后一环,负责把已经处理好的物体和光源转换成一张或一系列图片。主流的渲染方式分两种,光栅化 rasterization 和光线追踪 ray tracing。光栅化是非常经典的方法,它通过一系列坐标变换和裁剪,将使用顶点表示的物体映射到屏幕空间,再根据顶点信息生成像素颜色。光栅化方法对每个物体(或者说面片)的处理策略都是一样的,因此非常适合计算机处理,而缺点则是法向量等对光照非常关键的信息在齐次变换下并不总是一致,导致比较难模拟光线的行为,在基于物理的渲染上比较乏力。

光线追踪则是从屏幕空间去反向追踪光线在场景中的行为,获得屏幕上像素的颜色,进而生成整张图片。这是一个更符合物理和直觉的方法,在处理透明物体、粗糙物体、阴影等方面都优于光栅化。其缺点,或者说所有模拟方法的通病,就是巨大且难以加速的计算量。

1.2 ray tracing,ray casting,ray marching 和 path tracing

一点点消歧。

广义上的 ray tracing 就是指通过模拟光线行为实现渲染的方法,如果要对应一个具体方法,就是从屏幕(或者说相机)投出一条光线,递归追踪它与场景中物体的相交情况,或者干脆在第一次相交就确定光线颜色。

path tracing 在追踪光线时使用了蒙特卡洛积分,积分对象是渲染方程,或者说 BRDF 等材质函数。由于引入了随机采样,采样策略就是一个需要注意的地方,无偏的采样策略结果更正确,而有偏的策略可能有收敛快、方差小等优点。

ray casting 是体渲染的一种实现,从相机向纹理射出一条光线,光线经过的纹理形成光线的颜色。感觉也可以叫 line shooting。

ray marching 则是一种“过程性”的模拟方式,它将光线每次步进固定或可变长度,处理期间经过的的数据。

1.3 路径追踪的大致流程

可以从生成一个像素的过程看路径追踪的流程。

首先明确几个主体:光源、物体、相机。其他对象在下一小节会提到。

相机维护一张结果图片,即一个像素阵列,并负责向场景透射光线。通过每个像素投出的光线不止一条,而且是在像素覆盖的方向内随机取得。

光线与场景的交互是一个递归过程。根据光线与场景内物体和光源的相交情况,求得最初从相机投出的光线颜色,累加到这个像素的采样结果上,再求平均,经 gamma 校正后就是要写入像素的颜色。

2 代码解释

接下来讲一下设计上的几个对象。差不多是解释一下每个文件干的事情,以及实现时的一些要点。

2.1 向量

向量形式的数据贯穿整个渲染流程。除了需要支持常见的向量运算,还需要实现三维向量的一些随机采样,比如在单位球内、单位球面或者半球面上的均匀分布,以及在球面外一点对球面均匀采样。

此外,点和 RGB 颜色使用的也是向量,直接别名就好。

2.2 相机

相机类算是比较独立的一个类了,设定好朝向之后只管生成光线。

初始化一个相机需要设置的参数有:

  • 相机位置,光线的出发点。
  • 视点,相机凝视的位置。
  • 上方向,指示相机“上方”的方向向量。以上三个参数的设计参考了 OpenGL。根据这三个参数可以构造出以相机为原点的一组正交基。
  • 水平视角,相机视场的水平张角。
  • 图像宽高比,顾名思义。
  • 光圈直径,用于实现焦散。
  • 焦距,这里是成像平面到相机位置的距离。
  • 曝光开始和结束时间,即光圈打开和关闭的时间,主要用于实现动态模糊。

根据传入的参数,相机会维护一个抽象的、与像素无关的成像平面,可以根据传入的(归一化之后的)相对位置,生成射向对应位置的光线,而光线记录的时间在曝光时间区间内随机取得。

2.3 正交基

正交基主要用来减少生成与法向相关的随机向量的代码量。比如指定某个半球面的均匀分布,或者指定某个法向的朗博特散射,可以转化为生成固定朝向 z 轴的随机向量,再数乘一个以法向为 z 轴的正交基。这样比考虑如何旋转向量更简洁。

正交基的构造需要两个不平行的向量,我们已经有了法向量作为之一,而另外一个向量需要尽可能不平行,这里采用的方法是当法向量在世界坐标系下的某个分量接近 1 时,取其他分量的单位向量,否则取这个分量的单位向量。

2.4 光线

光线需要支持求交,所以我们使用参数方程实现。一个源点加一个方向,再来一个时间参数就可以了。

2.5 物体

广义上的物体就是可以和光线交互的所有类,派生出球体、矩形、立方体、均匀参与性介质,以及场景的若干结构。

总体而言,物体需要支持求交、求包围盒、求材质坐标等功能,如果要实现对物体采样,还需要支持求随机样本和概率密度。

  • 求交需要测试一条光线在给定时间区间内是否与物体相交,返回相交信息,包括交点、相交时间、交点材质、材质坐标、法向量和光线方向(区分内外侧)。
  • 求包围盒则是返回物体的包围盒即可。
  • 材质坐标需要根据物体的几何形状将表面的三维点映射为一个二维点。
  • 随机取样和概率密度马上就讲。

2.5.0 采样和概率密度值

为了实现对物体的采样,我们需要根据给定的源点生成物体表面的一个随机位置。同时,也需要知道得到这样一个采样的概率,如此才能适合蒙特卡洛积分。

采样一般使用单位球面坐标,则只需要处理 $\theta$ 和 $\varphi$ 两个值。在各向同的采样下,$\varphi$ 可以视作在 $[-\pi, \pi]$ 上均匀分布,而偏离法向的角度 $\theta$ 的概率累计函数符合:

其中 $2\pi \sin(t)$ 为偏离法向量 $t$ 角度的圆周长,$f(t)$ 为取一个偏离法向量 $t$ 角度的采样方向的概率。

2.5.1 球体

球体的实现比较简单。代码里实现的是匀速移动的球体,只要确定时间点就可以当作静止球体来处理。求材质坐标需要一点球面坐标知识。

(从球面外一点)对球体均匀采样,就是对视锥能够看到的球面均匀采样。根据距离和半径我们可以算出视锥的张角,然后生成采样,套入法向量的正交基即可。

2.5.3 矩形

为了方便实现,我只写了三种轴平行的(或者说垂直于某个轴的)矩形,然后通过“扭转光线”的方式实现旋转后的物体。

因此,只需要 5 个参数就可以描述一个矩形:在垂直轴上的坐标,以及在另外两根轴上的区间。如果要实现单向的一些特性,还可以指定表面法向。

求交时利用光线起点和矩形在垂直轴上的距离算出到达时间,再根据与矩形平面的交点计算是否在矩形内即可。

矩形的采样很简单,面上随机一个点,减去源点就能得到从源点向矩形采样的一个方向。

2.5.4 立方体

立方体,把六个矩形拼起来就行。

立方体仍然是轴平行的,根据两个对角的点来确定六个面的大小和位置。注意指定好法向。

至于六个面的存储,可以用数组,也可以用物体列表。用物体列表的抽象程度更好。

2.5.5 均匀参与介质

这个东西有点麻烦。我们想要的是均匀的雾状效果,即光线在经过物体时不断散射。

一般来说,光线在介质中行进的距离与被散射的概率成正比,比例系数与介质的光学密度相关。

要模拟这样的效果,可以在光线与介质相交(穿过,或从内部穿出)时算出经过的总距离,再随机一个散射前经过的距离,如果从光线起点经过这个距离后超出了总距离,则认为不散射,否则产生一个相交记录。对于散射前距离的随机,书上这里是直接给了一个公式,即光学密度的负倒数乘上 $[0, 1)$ 之间的均匀分布。

2.5.6 “物体”

光不过来,我就过去。

这里讲一些继承自物体类但是只负责改变原本物体的类。它们可以通过抽象嵌套修改一个实际物体。

2.5.6.1 平移

将一个物体向某处平移,相当与将那处的光线平移过来与物体求交。逻辑就是这么简单。记得求交之后把相交记录的交点平移回去。

2.5.6.2 旋转

只实现了绕 y 轴旋转。与平移的逻辑相同,不过是手动实现了旋转矩阵。另外一定记得恢复相交记录的法向量,如果没有恢复法向量,渲染 Cornell Box 时会发现长方体的正面上部偏黑且不均匀。当时查了好久。

2.5.7 场景和加速结构

场景记录了所有物体,其最抽象的行为就是根据光线去查询相情况。

2.5.7.1 基础场景列表

基础场景是用顺序表实现的,所有物体放在一个 vector 里面,查询时挨个遍历。比较慢。

2.5.7.2 轴向包围盒 AABB

一个显然的加速方法是,用结构比物体更简单的包围盒包住物体,如果光线与包围盒不相交,则一定不与其中的物体相交,否则再仔细去和其中的物体求交。

包围盒这里选用的是 Axis-Aligned Bounding Box。求交策略比较巧妙:求出光线与三对轴向面的相交时间区间,如果这三个区间并集为空则不相交,否则相交。

每种物体的包围盒构造需要各自实现。旋转和平移当然也需要构造新的包围盒。注意两点:无限平面(代码没有实现)等物体是没有包围盒的,而矩形面这种太薄的物体,应设定一个最小厚度。

AABB 类没有继承物体基类,因为不需要返回求交信息。如果想让 AABB 可视化,也可以继承。

2.5.7.3 层次包围体 BVH

AABB 的思想如果再抽象一下,很容易想到把若干个物体合并成一个包围盒,如果相交,再去和更低一级的几个包围盒求交,而这些小一点的包围盒也是包含了若干物体的。这是一个包围盒的树形结构,即 Bounding Volume Hierarchy。

每个包围盒的细分,是两个更小的子包围盒。显然兄弟包围盒并非不相交的,我们需要让交集尽可能小,避免重复的查询。但是如果严格遵守这个性质,会影响构造 BVH 的效率。因此我们的策略是:在世界坐标系中随机一个比较轴,将父包围盒内的物体按照在各自包围盒比较轴上的最小坐标排升序,然后从中间切分,递归构造下一层。

由于 BVH 节点继承自物体基类,光线求交可以通过重写的求交函数实现。如果传入的光线不和节点本身的包围盒相交,则返回不相交,否则对两个子包围盒求交。注意这里查询求交的时间区间。我们需要的是第一次相交,即最小的时间。因此,如果先求交的子包围盒确认了相交,再查询另一个子包围盒时,时间区间的最大值应该是上一次相交的时间。这是一个设计上的选择,固定时间区间然后比较相交时间也可行,但是开销会变大。

2.6 材质

物体只描述了形状,而视觉效果与物体分离,通过材质实现。每个可以被“看见”的物体都拥有一个材质,在求交时将材质记录下来,这样不必依赖物体本身就可以获取光线的行为。材质负责将入射光线散射出去,并告知积分器取到这个散射方向的概率。我们现在处理的光线和颜色是独立的,可以用一个结构体记录光线、颜色等信息。

在控制光线行为方面,材质实现了散射和发光。散射负责生成新的光线(后面改成了传出颜色、光线分布函数等信息),而发光是针对光源实现的,在积分器里直接叠加到总体颜色上。

2.7 纹理

材质对光线附加的颜色,是纹理来负责的。从纹理获取颜色一般需要提供 UV 坐标(二维函数),如果是空间函数还需要提供相交点。

这部分的 Berlin 噪声比较有意思,有点像伪随机序列。

2.8 概率分布函数

由于路径追踪涉及到蒙特卡洛积分,概率相关的处理就非常重要。

用蒙特卡洛积分计算像素颜色有一个常见的问题,即采样数不够的情况下,容易出现黑色的像素点。这是因为多数光线被散射到了场景之外,或需要多次弹射才能击中光源的方向。

这里需要区分两个分布:物体材质决定的散射分布和积分器选择的采样分布。前者是相对固定的,属于积分方程的一部分,后者才是需要调整来减少噪点的。特殊地,镜面反射和高光等光路是确定的,不需要前者。

将随机采样独立成一个概率分布函数,就可以嵌套混合的采样方式(重要性采样),比如向镜面和光源投射更多的采样。这需要概率分布函数提供采样和此采样的概率。前文提到的物体类中的随机采样,就是为针对物体的概率分布准备的。

2.9 积分器

代码里的积分器,其实就是 ray_color 这个函数。为了能让人快速看懂整个渲染过程,现在做一个详细的描述。

积分器接收一条光线、一个背景色、一个场景、一个采样物体列表(重要性采样用),以及一个递归深度。

首先是一些终止递归的检查,比如超过深度、无相交之类。超过深度返回黑色,无相交则返回背景色。背景色可以改成一个函数,然后实现天空盒等效果。

接着是有相交的情况,此时已经有了相交点的位置、法向、相交时间、材质、UV 坐标等信息。先获得材质的发光颜色,然后测试是否发生散射。如果不散射,则返回发光颜色,否则根据获取的散射信息进一步处理。

如果散射是来自镜面、高光等确定光路,则不需要随机采样,直接用散射的光线和颜色进行递归。否则,根据采样物体列表构造一个物体采样函数,与散射记录的采样函数线性混合,据此生成一个新的光线并获得其采样概率,再进行递归,返回的结果就是 $材质发光 + \frac{散射颜色\times 散射光线后续的递归颜色 \times 材质散射出此光线的概率}{散射采样概率}$。

2.10 图像库

使用了两个图像库,都是来自 Sean T. Barrett 的 stb 库,分别是读取和写入。读取在图像材质里用到了,而写入是用来把渲染结果转成 jpg 格式,字符型 ppm 文件打开速度太慢了。

3 其他

浮点问题要仔细考虑,这个算是一直萦绕的幽灵了。

0 到 255 的颜色是单位化成 0 到 1 的,但是光源的颜色可以超过 1,即亮度很高。事实上如果光源颜色还是 0 到 1 的话场景会非常暗。

至此,这个小路径追踪器算是写完了。下面摘自书中结语:

蒙特卡洛方法还包括了双向和基于路径空间的方法,这些方法的概率空间是整个光线路径。

电影渲染器方向,相关的工作室发表了很多研究成果。

高性能光追当属 NVIDIA 和 Intel 的研究。

如果要做事实上的“PBR”,首先要做的是把颜色描述从 RGB 空间转换到光谱空间。

以及,谨以此文祝贺 MoonRay 即将开源真不是摸了太久才写完的

4 参考

什么是计算机图形学?

Ray Tracing: The Next Week

Ray Tracing: The Rest of Your Life