新年新气象,《C++光追渲染器》迎来新的篇章。中篇中,我们将继续给光追器添加新功能,挑战更多的可能。

新篇导览

初篇中,我们已经拥有了一个功能完善的渲染器,它围绕着一个主题——“蓝天”。在第四章,“光线”这个概念诞生以来,到第十五章绘制出最终场景。我们的相机绘制着蓝天之下静止的万物。
蓝天一直陪着我们,就好像是本光追渲染器的一个符号、一个标志一样。
新篇章的主题是“时空”,我们的渲染器会“摆脱束缚,飞到浩瀚的宇宙中去”,将会有:

  1. 移动的球。物体不再被禁锢于时间里,而是按照某种规律动起来,感受飞速移动的物体在镜头上留下的残影吧!
  2. 包围盒子。一种应对场景中物体过多而卡顿的经典加速手段,如果没有它,插上翅膀飞向宇宙的渲染器将会变得缓慢无比。
  3. (固态)纹理。世间万物皆有纹理,按理来说,不存在只用材质可以描述的物体。给我们的金属球加点铜锈?或者给玻璃球加一个逼真的裂痕?随你的便!当然,我们的重点研究对象是怎样用噪声模拟大理石表面奇异的纹路。
  4. 矩面。一个单纯的面或者六个面组成的盒子!想要渲染出空间感十足的作品,盒子是最好的道具。
  5. 。在第十章中的最后一道习题中,或许你已经尝试过光源带给整个世界的变化。但这一次,我们会系统的认识和安排发光物体——这是历史性的突破,能量将不再仅仅来自于蓝天,而是某颗恒星,或者某个异世界传送门。
  6. 。挑战一下这种奇妙的材质,它对场景空间感的提升的帮助会改变你的认知。记得夜晚汽车远光灯照射出去,透过灯光看到的纷飞的灰尘么?我们要模拟的就是类似的效果。

在完成这一切之后,渲染器将有能力渲染出下面的图片:
image.png
看一下中篇对应的原版书的名字:《Ray Tracing: The Next Week》,你就知道接下来的内容比起初篇会有很大的难度提升,初篇只需要一个weekend就可以完成,而中篇则需要整整一个week!
本篇难度主要体现在于本篇内容是基于初篇的全部代码的,逻辑不断累加,相互调用,会让人难以理清。而且,本篇中的包围盒BVH以及固态纹理Solid Textures的理论和技术都有各自的难处,属于本篇的两大难点。本文尽量将难点内容切开并扩充详细,多分几个小节慢慢阐述,以方便读者理解。
注意,本篇中提到的算法和上一篇中一样,也是从简处理,如果想了解最准确最完善的各个算法,请浏览相关论文,和上一篇不同的是,本篇中的很多算法在业界都有某一篇或几篇公认的模范论文,我会贴出,如果有需求像深入了解此算法,可以查阅引用的论文。

康奈尔盒子

图形学领域有很多经典的场景和模型,就好像我们刚学习语言的时候都在学习怎么输出HelloWorld一样。图形学领域中,检验渲染器是否强大的标准就是康奈尔盒子(Cornell Box)。
它其实是计算机图形学专业最牛x的高校之一——康奈尔大学里的一间真实存在的房间,如下图是其某一视角真实照片的缩略图:
image.png
1984年它被提出以来,就被当作对世界上所有的图形工作人员的一个必经考验——你的渲染器渲染出来的场景和它越像,说明算法越准确,渲染效果越逼真。
在本篇的中后段,我们的渲染器要正式开始挑战它了!这可不再是打打闹闹了,说明本项目也开始变得学术起来,变得严谨起来了!

曝光时间

虽然说中篇的难度会高于初篇,但是我还是决定从最简单的内容开始。
初篇的最后我们一直在和相机较劲,一个拥有景深(散焦模糊)的相机也并不是我们相机进化的终点,在本篇的开始,我们将让相机进化为完全形态。这将是本系列最后一次改动相机类,在本篇之后,相机类将不会再有任何改动——当然,你自己可以对其进行一些个性化改动或者添加你认为需要的功能,但是对于我们介绍的所有功能中,相机将进化为完全体。
真实的相机,有一个概念叫做曝光时间:它表示镜头打开的时间,在这段时间内射到相机的光线都会被相机捕捉。
但是我们当前的相机,曝光时间是趋于无穷小的,也就意味着,任何高速移动的物体,在相机镜头打开的这一瞬间里,移动的距离都为0。我们的相机牛逼到有一个超人从我们的镜头前路过,我们都能精准的捕捉到它穿在秋裤外面的裤头上炸开的线头!
依照我们在散焦模糊那一章的处理思路,我们得自断一臂,通过某些方式让我们的相机产生缺陷去迎合真实的相机。
要怎么模拟曝光时间这一概念呢?按照逆光路模型,我们得让光线在一段时间内先后从相机射出,而不是一瞬间。按照曝光时间的长短,让某一个像素中射出的若干采样光线在此曝光时间内的随机时刻射出,并带回信息即可。
要想实现这一点,需要先改动光线类,让光线和时间挂钩。
请不要把这个时间和我们光线公式中 P( t ) = A + tb 的 t 弄混,这个 t 表示这一根光线在发射之后过了多少时间,并不涉及到光源发射的时间。
看代码:

  1. class ray {
  2. public:
  3. ray() {}
  4. // 更改构造函数,tm的缺省默认值为0。
  5. ray(const point3& origin, const vec3& direction, double time = 0.0)
  6. : orig(origin), dir(direction), tm(time)
  7. {}
  8. point3 origin() const { return orig; }
  9. vec3 direction() const { return dir; }
  10. //tm的getter函数。
  11. double time() const { return tm; }
  12. point3 at(double t) const {
  13. return orig + t*dir;
  14. }
  15. public:
  16. point3 orig;
  17. vec3 dir;
  18. // 光线发射时刻。
  19. double tm;
  20. };

光线现在有发射时间的概念了,接下来修改发射光线的相机类,让相机能发射有不同tm值的光线。

  1. class camera {
  2. public:
  3. camera(
  4. point3 lookfrom,
  5. point3 lookat,
  6. vec3 vup,
  7. double vfov,
  8. double aspect_ratio,
  9. double aperture,
  10. double focus_dist,
  11. //相机快门打开时间。
  12. double _time0 = 0,
  13. //相机快门关闭时间,开关差值既是曝光时间。
  14. double _time1 = 0
  15. ) {
  16. auto theta = degrees_to_radians(vfov);
  17. auto h = tan(theta/2);
  18. auto viewport_height = 2.0 * h;
  19. auto viewport_width = aspect_ratio * viewport_height;
  20. w = unit_vector(lookfrom - lookat);
  21. u = unit_vector(cross(vup, w));
  22. v = cross(w, u);
  23. origin = lookfrom;
  24. horizontal = focus_dist * viewport_width * u;
  25. vertical = focus_dist * viewport_height * v;
  26. lower_left_corner = origin - horizontal/2 - vertical/2 - focus_dist*w;
  27. lens_radius = aperture / 2;
  28. //开关时间初始化
  29. time0 = _time0;
  30. time1 = _time1;
  31. }
  32. ray get_ray(double s, double t) const {
  33. vec3 rd = lens_radius * random_in_unit_disk();
  34. vec3 offset = u * rd.x() + v * rd.y();
  35. return ray(
  36. origin + offset,
  37. lower_left_corner + s*horizontal + t*vertical - origin - offset,
  38. //光线的tm值给一个time0和time1中的随机值。
  39. random_double(time0, time1)
  40. );
  41. }
  42. private:
  43. point3 origin;
  44. point3 lower_left_corner;
  45. vec3 horizontal;
  46. vec3 vertical;
  47. vec3 u, v, w;
  48. double lens_radius;
  49. //快门开关时间。
  50. double time0, time1;
  51. };

好了!你现在拥有了一个能在曝光时间内随机发射光线的相机!你可能会觉得这根本不算什么,就是给光线类多加了一个成员变量罢了,但这对我们的渲染器来说是跨时代的一步。我们的光追器不再仅仅只是描绘光线在空间上的碰撞,而且开始描绘时间上的交错变化。这叫做时空光线追踪(SpaceTime Ray Tracing)。
我们正式引入的时间和运动这些动态的概念,会和后续介绍的功能相辅相成,绘制出更奇妙的世界。
但是现在还没有其用武之地,因为我们场景中所有的物体暂时还都是静止不动的。
下一章我们会创建跃动的小球,准备迎接动起来的世界吧!

参考文献

https://raytracing.github.io/books/RayTracingTheNextWeek.html
参考自《Ray Tracing: The Next Week》第1节和第2.1节。