欢迎光临散文网 会员登陆 & 注册

[光线追踪] 04 -- 程序结构

2022-05-27 19:49 作者:nyasyamorina  | 我要投稿

经过之前几篇专栏的讨论,  已经确定了光追渲染模型为 L_%7Bo%2C%5Cmathrm%7Bobj%7D%7D%3DK_s%5Ccirc%20L_e%2BK_h%5Ccirc%20L_%7Bo%2C%5Cmathrm%7Bobj%7D%7D,  那么下面就需要确定程序结构.  为了方便快捷,  这里的程序将会使用最新的 C++20,  尽管很多特性并不会用上.


颜色值

在之前的专栏里,  一直都是以辐射度表示光线的辐射强弱,  也就是单个浮点数数值.  为了产生彩色图像,  则需要对每种需要的色彩进行光追计算.  为了计算多种色彩,  需要对辐射度引入波长微元,  那么在波长 λ 处的辐射度变为  Ld%5Clambda.  人类可见光的波长范围大约在 380nm~780nm 内,  在这个范围内进行渲染即可得到准确的彩色图像.

实际上,  人类可以感知的色彩仅有 3 种: 红绿蓝,  那么整个可见光范围的图像可以通过加权累加得到在人眼里准确的红绿蓝图像 (请自行wiki: CIE-XYZ 色度空间).  既然最终图像仅有 3 种颜色,  那么可以假设仅对红绿蓝进行光追就可以得到足够接近的图像 (实际上某些条件下红绿蓝光追会产生极其错误的图像),  从而降低了需要计算的数值的数量.  另外假设所有改变光线的事件对所有颜色都是完全相等的 (这至少意味着折射是错误的, 即无法复现透镜色散之类的现象),  这样假设可以在单次光追里同时计算红绿蓝三种颜色的辐射度,  而不是对每种颜色都做一次光追.

不得不说的是,  如果改变光线的事件如果对每种颜色都有差异的话,  那么在可见光范围里对每个颜色进行单独光追是非常合理的,  但这种合理性在从可见光映射到红绿蓝时就已经消失了.  为了在红绿蓝光追里准确还原可见光光追,  性能消耗最多是可见光光追的三倍.

另外,  在实际程序里将会使用 RGB 值进行光追.  上面说到的红绿蓝实际上是指 XYZ 值而不是 RGB 值,  RGB 渲染比 XYZ 渲染产生的色彩更加局限 (wiki: CIE-XYZ色度空间)使用 RGB 的理由是:  RGB 可以直接由计算机显示,  而 XYZ 需要映射到 RGB 后才可以显示.  程序定义的 RGB 类几乎与 Vec3 类相等,  但浮点数精度是单精度,  这是因为颜色并不需要非常准确的数值来计算.  在常见的图像格式里,  RGB 分量为范围在 [0, 255] 的整数,  但在渲染缓冲区里,  RGB 的分量为范围在 [0, 1] 的浮点数,  使用浮点数是因为整数还没达到色彩计算的精度要求,  但使用 [0, 1] 内的数值是因为显卡计算的要求,  虽然这里设计的程序是使用 CPU运行的,  但出于习惯还是会使用 [0, 1] 的浮点数表示 RGB.


相机模板

既然光追是渲染程序,  那么肯定需要输出一个二维的 RGB 图像.  另外需要知道渲染图像上每个像素的光线,  当从像素处获得光线后,  就可以进入光追流程了.  那么相机模板为

其中虚函数 get_ray 由相机的子类来实现.


智能指针

为了不让内存管理成为一件痛苦的事,  并且考虑到类型之间的多态行为,  这里程序内将会使用大量的智能指针.  为了避免类型名过长,  将会使用类型名后接 -p 代表相应的智能指针类型,  相应的成员名将会以 -_p 结尾:

智能指针定义在 <memory> 里.  下面是一段智能指针的使用例子:

可以看到,  使用智能指针的 C++ 非常像自带垃圾回收的其他语言 (如 python, C# 等).  至于智能指针的回收垃圾性能问题就不是这里的讨论范围了,  因为这里程序创建和清除智能指针对象只会发生在 build_world 函数内,  而光追内部是不涉及创建/清除智能指针对象的.


材质,  物体和碰撞记录

渲染模型的专栏里介绍半球模型时,  提到了一个返回与光线最近碰撞点的的函数 h,  这个函数在 World 类里以方法 hit_object 实现.  尽管数学上是返回最近碰撞点,  但在实际实现里可以通过返回多个数值达到化简计算,  比如可以返回碰撞点处的法线供后续渲染.  所以这里提出 HitRecord 类来记录光线与物体碰撞时产生的数据,  下面是这个类的实现:

其中 World 引用和内置的 Ray 是为了减少后面方法的参数数量;  depth 表示当前光线的深度,  避免出现无限递归;  hit 表示光线是否有与物体碰撞;  object_p 表示与光线碰撞的物体指针,  因为光线可能不与物体碰撞,  所以使用 null 进行初始化;  t 为光线类里 at(t) 中的 t,  而初始化 HitRecord 时输入 t 是为了限制光线的最远距离;  point, normal, tex 则是碰撞点和碰撞点处的法线和纹理坐标.  那么 hit_object 可以用下面的方法实现:

另外,  在面积模型里判断两点是否有物体阻挡的函数,  可以这样实现 (absnorm 与之前的定义不一样了, 请与 github 上代码定义为准):

在代码里可以看到 Object 类定义了两个函数:  hit 和 hit_record,  hit 仅计算光线是否与物体碰撞,  而 hit_record 除了计算光线是否与物体碰撞,  还会计算碰撞时需要用到的数据.


当从 hit_object 里获得 HitRecord 后,  就可以进行光线的渲染,  渲染光线由材质 Material 类下的 render 方法进行.  因为 HitRecord 内包含渲染光线所需的所有数据,  所以 render 方法仅需输入 HitRecord 就可以进行渲染.  材质包括但不限于:  不透明材质,  透明材质,  发光材质.  在上一篇专栏里有说过,  为了避免重复计算光源,  光源的 Lₒ,obj 应该为 0,  所以为了保证接口的通用性,  Material 里应该有两个渲染方法:

自发光的材质类重载 render_emissive,  而其他材质重载 render.


因为材质和物体是绑定的,  所以 Object 类里还需要包含材质.  而对于自发光物体,  需要实现在物体表面上进行均匀采样,  这里使用一个函数 get_sample 返回物体表面上的采样和相应的法线,  那么最后 Object 定义为

这里直接偷懒把返回的采样点和法线打包成 Ray 返回了 (point 是采样点, direction 是法线).  其中 is_light 方法是判断材质是否为自发光材质.

另外,  为了实现在物体表面采样,  Object 的子类里必定含有 Sampler,  但一个 Sampler 是比较庞大的,  并且通常场景里发光物体是少数,  所以为了节省内存,  Object 子类里应该存放 Sampler 的指针,  而不是 Sampler 本身,  为此可以定义 Sampler 的智能指针类: 

光线追踪流程

从上面讨论的几个类中,  不难知道光追的大概流程为:  1) 使用 Camera::get_ray 得到初始光线 Ray;  2) 使用 World::hit_object 获得 HitRecord;  3) 使用 Material::render 渲染光线并返回 RGB 值.  不过从光追的数学模型知道,  Material::render 的具体实现会比较复杂,  很可能需要计算多条次级光线,  而计算光线的颜色又需要从上述第二步开始形成递归.

为了控制递归,  这里提出 RayTracer 类.  下面是 RayTracer 类的简单实现:

而 Material::render 内的次级光线可以这样渲染:

另外需要留意到,  RayTracer::trace 是调用 Material::render 方法,  这意味着从相机出发的光线如果与自发光物体碰撞将会返回黑色,  从而造成自发光物体不可见.  所以可以额外写一个方法区分自发光物体与普通物体的 trace 方法:

那么从相机直接出发的光线将调用 RayTracer::trace_from_camera 而不是 RayTracer::trace.


在这里介绍了光追的流程亦即实现了几个比较关键的模板类.  接下来的专栏将会逐个实现模板类的实类.  上面的代码可能存在性能或者逻辑问题,  所以绝对不是一成不变的,  代码的最终结果请以 gayhub 上的为准.

但就算是 gayhub 上的代码也只能确保编译通过,  随着代码的修改,  可能会使某些之前可以运行的特性在新更新的版本里失效 (一个人维护这么大的项目属于是有点累了).  如果有什么意见或者问题提出的话,  欢迎加q群讨论.


gayhub仓库:  https://github.com/nyasyamorina/nyasRT

扣扣群:  274767696

封面pid: 97290687

[光线追踪] 04 -- 程序结构的评论 (共 条)

分享到微博请遵守国家法律