制作光线

光线是什么?你或许觉得要某个数学概念来定义光线,实在是太过于不能理解:“我怎么在程序中创造光线,难不成我先抽象出一个太阳不成?”
换一种说法你就明白了——射线。这下子你的脑海中是不是就有一些数学概念了?没错,一个点和一个方向会组成一根射线,我们顺着这个思路往下想。我们需要一个原点A和一个方向向量b,接下来我可以给出一个公式来描述光线:
四:光线 - 图1
A表示光线的原点(光源位置),b为一个单位向量表示一个方向,t则表示单位时间,通过给t取不同的值,我们可以得到沿路上所有的点的三维坐标,当然,这个值一般来说不会为负数,因为射线只有一个方向,看下图:
image.png
懂得光线的基本概念之后,我们就开始用代码描述它了,下面创建一个ray.h文件:

  1. #ifndef RAY_H
  2. #define RAY_H
  3. #include "vec3.h"
  4. class ray {
  5. public:
  6. //空构造。
  7. ray() {}
  8. //带参构造,显然我们需要一个原点和一个方向。
  9. ray(const point3& origin, const vec3& direction)
  10. : orig(origin), dir(direction)
  11. {}
  12. //通过这个函数拿取原点值。
  13. point3 origin() const { return orig; }
  14. //通过这个函数拿取方向值。
  15. vec3 direction() const { return dir; }
  16. //这个函数就对应了上方数学公式中的P(t),通过传入一个时间t,能得到当前光线传播到的坐标位置。
  17. point3 at(double t) const {
  18. return orig + t*dir;
  19. }
  20. public:
  21. point3 orig;
  22. vec3 dir;
  23. };
  24. #endif
  1. 它多简单啊!甚至都没有什么可以说的。完完全全按照我们上面的数学公式,就可以敲完这个代码,多提一嘴,在有参构造中我们的参数是point3vec3,这里你就可以看到我们对vec3类取了很多别名的好处了,有别名之后,世界是多么清晰!<br />下面的部分是本系列的第一个**难点**,我们要怎么射出光线呢?

逐像素发射光线

如果你关注过游戏引擎或者任何3D建模软件,你都应该知道有一个摄像机的概念。我们拖动这个摄像机在世界空间中移动,就可以看到在这个位置下的相机“照片”。
在本项目中我们也需要类似的概念,但是先不要这么复杂,我们把“相机”固定在一个位置,并且固定一下它观察的方向。
现在轮到我们刚刚定义的“光线”出场了:

  1. 光线从哪里射出呢?相机位置。你可能会很奇怪,按照常理来说,太阳发出的光线从物体上弹射了多次,最终会被摄像机(或者人眼)捕捉。相机位置应该是光线的终点才对啊,怎么会是起点呢?原因是我们需要逆光路取色,这是路径追踪的经典光线模型,你只需要记住,我们的光线和现实中的光线是反过来的,等做完这个项目你就会明白,如果正向光路进行光线追踪会极其困难,几乎寸步难行。
  2. 光线朝哪个方向射出呢?这就要引出一个“虚拟视口”的概念。想象一下相机对着的那个方向,难道那个方向上的所有物件都能被你拍到吗?显然不是这样,如果是这样,你会得到一张无穷大的照片。我们假设相机前方摆了一个虚拟的框框,它是方形的,且正好和我们要生成的图片的长宽比相似,接下来,我们用密集的光线射满这个框框。正确的来说,我们是按照行优先的顺序,从左上角开始,一排一排的射出光线,射出光线的数目就是像素的数目,换句话说,我们对每一个像素都会射出一根光线

感觉很抽象?没事,看看下面这张图。我们假设相机放在(0,0,0)位置上且相机镜头中心对准Z轴负方向,然后这个虚拟视口的位置在距离相机1个单位长度的地方,且它的长宽是4和2。我们来看看在空间中发生了什么事情。
image.png
假设图中的虚拟视口上有800个像素,每个像素长宽都是0.1,你想最终得到一张4020的图片,你要怎么射出光线?假设我们要瞄准每一个像素的中心
按照行优先左上角开始,我们发射的第一根光线应该是从(0,0,0)射向(-2+0.05,1-0.05,-1)方向。我们不必要求这个方向向量是一个单位向量,保持方向向量是单位向量并不能给我们的项目提供更多便利。
那第二个呢?是(-2+0.15,1-0.05,-1)方向。
第四十个呢?是(-2+3.95,1-0.05,-1)方向。
第八百个呢?是(-2+3.95,1-1.95,-1)方向。
对,按照上面我们给出的虚拟视口长宽以及像素数量,我们可以射出这样的八百个光线,这些光线将分别为这八百个像素决定最终的颜色,形成一幅40
20的精致小巧的图片。
如果你理解了上述的模型,你理解下面这些代码就不难了,我们改造main函数,并且为其所在的文件添加一个全局函数,如下:

  1. #include "color.h"
  2. #include "ray.h"
  3. #include "vec3.h"
  4. #include <iostream>
  5. //这是一个简单的决定光线所带回颜色的函数。
  6. color ray_color(const ray& r) {
  7. //我们先把这个光线的方向向量单位化。
  8. vec3 unit_direction = unit_vector(r.direction());
  9. //再根据这个单位化向量的y分量给他设定颜色,注意我们得保证t在[0,1]之间。
  10. auto t = 0.5*(unit_direction.y() + 1.0);
  11. // 嘿,你认得它吗?一个插值函数,t靠近0它就越靠近白色,越靠近1它就越靠近一种蓝色。
  12. return (1.0-t)*color(1.0, 1.0, 1.0) + t*color(0.5, 0.7, 1.0);
  13. }
  14. int main() {
  15. //图片数据,这次我们换一种角度去定义图片的长宽,我们定义一个长宽比,再把它的宽度定义出来。
  16. //长度就可以通过简单的计算得到。
  17. const auto aspect_ratio = 16.0 / 9.0;
  18. const int image_width = 400;
  19. const int image_height = static_cast<int>(image_width / aspect_ratio);
  20. //虚拟视口数据,我们保持它的高度(宽度)为2,长度同样通过长宽比得到。
  21. //之前介绍过,我们要保持视口和实际图片的长宽比一致。
  22. auto viewport_height = 2.0;
  23. auto viewport_width = aspect_ratio * viewport_height;
  24. //这是视口离相机的距离,保持为1就好,我们暂时把它写死。
  25. auto focal_length = 1.0;
  26. //相机位置
  27. auto origin = point3(0, 0, 0);
  28. //相机水平方向,即X轴正方向。
  29. auto horizontal = vec3(viewport_width, 0, 0);
  30. //相机头顶方向,即Y轴正方向。
  31. auto vertical = vec3(0, viewport_height, 0);
  32. //这个是虚拟视口左下角所在位置的坐标,在上面那个图片例子里,它就是(-2,-1,-1)。
  33. //注意因为长宽比不是2/1而是16/9,所以本例子里这个值和图片中的值不同。
  34. auto lower_left_corner = origin - horizontal/2 - vertical/2 - vec3(0, 0, focal_length);
  35. // 渲染循环(render loop)
  36. std::cout << "P3\n" << image_width << " " << image_height << "\n255\n";
  37. for (int j = image_height-1; j >= 0; --j) {
  38. std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush;
  39. for (int i = 0; i < image_width; ++i) {
  40. //这个uv就是当前像素位置的横纵坐标偏移。
  41. auto u = double(i) / (image_width-1);
  42. auto v = double(j) / (image_height-1);
  43. //创造射线。
  44. ray r(origin, lower_left_corner + u*horizontal + v*vertical - origin);
  45. //通过全局函数取到本像素颜色。
  46. color pixel_color = ray_color(r);
  47. //写颜色到输出流。
  48. write_color(std::cout, pixel_color);
  49. }
  50. }
  51. std::cerr << "\nDone.\n";
  52. }

看,蓝天!
image.png
我留下几个问题来考验你是否真的理解上面的代码:
(1)lower_left_corner的值在上面的代码中应该是多少呢?
(2)渲染循环中射出的光线是瞄准哪里的呢?还是每个像素的中心吗?
(3)更改ray_color函数如下,你觉得会输出一张什么样的图片,再实际运行一下,看看是否和你想象的一样?

  1. color ray_color(const ray& r) {
  2. auto vec = r.direction() * 1 - r.origin();
  3. auto absVec = vec3(std::abs(vec.x()), std::abs(vec.y()), std::abs(vec.z()));
  4. return unit_vector(absVec);
  5. }

参考文献

https://raytracing.github.io/books/RayTracingInOneWeekend.html
参考自《RayTracingInOneWeekend》第4节