背景

你在一个教室中趴在桌子上睡觉,天色开始逐渐变红,教室中的窗帘被一阵风拍的开始乱飞,你被惊醒,发现同学都不见了,你跑出教室,下楼的时候你突然脚滑摔晕。

等你醒来的时候已经晚上,周围的环境让你感到陌生,“有人吗?你好?!”,并没有得到回复。

你看向唯一发光的物体,是一台笔记本,上面写着你必须实现一个路径追踪渲染器,否则你将一直被关在这个纯黑的密室中,没有时间限制,我们提供了必要的文件,最后需要渲染出一个标准的CornellBox场景,没有性能要求,并发送到邮箱cglab@cglab.top,你发现这台电脑并不能连接网络,“岂可休!”。

你身为一个计算机系的学生,你对计算机图形学一窍不通只知道计算机图形学是门研究渲染,动画,模拟,几何等等的一门学科,但是你学过C++,cmake,概率论基础,线性代数基础,微积分基础,并且你懂得如何构建让C++代码成功编译并运行。

你打开了神秘人提供的文件,开打了README.md:

README

你好啊。当你看到这个文件的时候,说明你已经被邀请加入CGLab了。

你可能会问:为什么是我?

这个我们也不知道,我们加入CGLab的原因我们自己也没有搞明白,我们至今只知道拥有图形学潜力的人会被邀请加入CGLab。

每个被 xxx 邀请加入CGLab的人都会和你一样突然进入这个神秘的空间,并且每个人进入这个空间都会被分配一个任务,每个人的任务都是不一样的这次你被分配的任务是实现一个路径追踪渲染器。

如果这个任务没有完成你会永远被关在这个密室中,自生自灭。
但如果你完成了任务你将会得到对应的 “加护”,你被分配的是渲染领域的任务,xxx 认为你在渲染领域非常有潜力。

不过不用担心,我们会在这个README中逐步教你实现一个Path Tracing Renderer。让我们开始吧!!

PPM

在计算机图形学渲染领域学习的时候我们通常需要渲染一张图片或者多张图片所组成一个连续的动画,在这里我们只需要渲染一张图片,我们都知道图片是由一系列像素组成的,我们可以吧像素理解为一个小方块,每个小方块都有自己的颜色属性,由许多不同颜色的像素就可以组成一张图片。

这就需要我们使用程序将数据写入一张图片中,但是通常我们使用的 png,jpg 等图片格式并不适合我们进行学习,所以我们这里使用一种格式为 ppm 的图片格式,这种图片格式是一种文本图片格式, 它是由一系列字符串表示的。

ppm 文件有两种数据格式,一种是 ASCII 格式,一种是 BINARY 格式。我们将使用第一种格式,这种格式下我们可以轻松的知道每个像素是什么颜色。

举个例子

假设我们要创建一个 $2 \times 2$ 像素的微型图片,包含红、绿、蓝和黄四种颜色。它的文件内容看起来是这样的:

1P3
22 2
3255
4255 0 0    
50 255 0
60 0 255
7255 255 0
  • P3: 告诉解析器这是ASCII格式PPM文件
  • 2 2: 宽 2 像素,高 2 像素,定义图片的大小
  • 255: 颜色范围从 0 到 255,图形学中我们通常使用一个三维向量表示颜色,这个向量中的三个分量分别表示红、绿、蓝的权重。如果如果学过美术的应该很容易懂就类似美术中的红黄蓝可以调配出大部分颜色
  • 后面的就是颜色值,通常我们每行一个颜色值

所以我们第一步就是写一个写PPM文件的程序,在图形学中底层中我们通常选择C++进行编写程序

 1// 身为被选中的你,应该不需要我门对这段简单的C++代码做出解释了
 2#include <filesystem>
 3#include <fstream>
 4#include <iostream>
 5#include <vector>
 6
 7struct Color {
 8    int r, g, b;
 9};
10
11void writePPM(const std::filesystem::path& path, int width, int height, const std::vector<Color>& data)
12{
13    std::ofstream ofs(path);
14
15    ofs << "P3\n" << width << " " << height << "\n255\n";
16
17    for (int i = 0; i < width * height; ++i) {
18        int r = data[i].r;
19        int g = data[i].g;
20        int b = data[i].b;
21
22        ofs << r << " " << g << " " << b << "\n";
23    }
24
25    ofs.close();
26}

我们可以测试 writePPM 渲染一个简单的图,我们让图片中的像素的2维坐标 $(u,v)$ 作为图片的颜色 $(u,v,0)$

 1int main()
 2{
 3    int width = 100;
 4    int height = 100;
 5    std::vector<Color> data(width * height);
 6
 7    for (int i = 0; i < height; ++i) {
 8        for (int j = 0; j < width; ++j) {
 9            data[i * width + j] = {i, j, 0};
10        }
11    }
12
13    writePPM("out.ppm", width, height, data);
14
15    return 0;
16}

渲染出来的结果是 out.ppm

Path Tracing

好了我们已经拥有渲染图片的能力了,让我们正式开始学习Path Tracing的概念,在Path Tracing中我们使用的是几何光学,我们假设光线从我们的眼睛(或者叫Camera)出发经过视口(我们最终渲染出来的图)后在场景中经过一系列弹射最后打到光源的这么一个过程。

我们可以这么理解,我们通过数学的方式定义了一个在抽象空间的场景,我们想通过我们的眼睛在电脑的显示器上看到这个场景,我们需要通过渲染器才能有办法看到这个场景,这个渲染器会模拟我们的行为,渲染器中会定义我们的眼睛(Camera),我们想以什么角度看这个场景,以及我们的眼睛(Camera)的参数也可以调整,然后定义我们的眼睛最终看到的图片的大小,也就是可视范围,然后模拟光线的弹射,根据光线的弹射将场景中的信息写入可视范围中最后保存为一张图片。

这其中有很多我们需要解决的问题

  • 为什么光线是从相机出发不是从光源出发
  • 光线是怎么从相机打出去的
  • 光线怎么知道与物体作用了
  • 光线与物体作用了之后怎么弹射
  • 光线怎么知道打到了光源
  • 怎么让渲染的结果与真实物理世界一致

我们先回答这些问题

1. 为什么光线是从相机出发而不是从光源出发

从光源出发向四面八方发射数百万条光线。其中大部分光线会撞到墙角、地板或天花板,经过多次反弹后能量耗尽,永远无法进入你的瞳孔。对于计算机来说,计算这些光线完全是浪费资源。

而从相机出发我们只需要处理那些“注定”会影响屏幕像素的光线。每一条从相机射出的射线,只要撞击到物体,就必然对应着屏幕上的一个点。

2. 光线是怎么从相机打出去的

我们先定义我们使用的坐标系,我们使用的坐标系是这样的:
我们以标准坐姿面向显示器正面,原点位于你显示器正中心,z轴垂直于显示器向外(也就是指向你),y轴与你显示器垂直方向平行向上,x轴与显示器水平方向平行向右。

我们可以在抽象空间中定义一个点表示我们的相机的位置,我们假设相机的向上方向永远是与y轴正方向一致,并且位于(1,0,0)看向原点,视口是我们最终渲染的图片,或者说是我们的屏幕。

我们还需要了解屏幕空间和像素空间

  • 屏幕空间是一个2维空间,定义原点在左上角,x轴向右,y轴向下。假设你的屏幕的宽度是1920,高度是1080,那么屏幕空间就是(0,0)到(1920,1080)。
  • 像素空间是离散的网格系统
    像素空间与屏幕空间的映射关系是:像素空间中的点$(x,y)$对应屏幕空间中的点$(x+0.5,y+0.5)$,屏幕空间中的点$(x,y)$对应像素空间中的点$(\left \lfloor x\right \rfloor ,\left \lfloor y\right \rfloor )$ 我们在抽象空间中从相机点出发与像素空间中的点进行连线,这就构成了一个射线或者称作向量,我们遍历像素空间中所有的点即可渲染出我们所需的可视图了

3. 光线怎么知道与物体作用了

我们需要定义光线的方程,然后通过解析的方式判断这个方程在某些几何体表面上是否有解,如果有解则说明相交了,回想一下我们怎么计算两个直线的交点你就明白了

4. 光线与物体作用了之后怎么弹射

我们需要定义物体的材质,材质决定了光线在这个物体作用后怎么弹射

5. 光线怎么知道打到了光源

我们让光线在场景中不断的弹射,再定义哪些物体是光源,每次光线与改物体相交之后询问一下你是光源吗

6. 怎么让渲染的结果与真实物理世界一致

我们需要使用物理的方式去渲染一张图,而不是随便定义

在开始实现其他的代码之前我们提供一个Vec3向量运算头文件给你

  1#pragma once
  2
  3#include <cmath>
  4#include <cstddef>
  5
  6#include <array>
  7#include <iostream>
  8
  9class Vec3 {
 10public:
 11    constexpr Vec3() noexcept = default;
 12
 13    constexpr Vec3(double x, double y, double z) noexcept
 14        : elements_{x, y, z}
 15    {}
 16
 17    [[nodiscard]] constexpr double x() const noexcept
 18    {
 19        return elements_[0];
 20    }
 21
 22    [[nodiscard]] constexpr double y() const noexcept
 23    {
 24        return elements_[1];
 25    }
 26
 27    [[nodiscard]] constexpr double z() const noexcept
 28    {
 29        return elements_[2];
 30    }
 31
 32    [[nodiscard]] constexpr Vec3 operator-() const noexcept
 33    {
 34        return Vec3{-elements_[0], -elements_[1], -elements_[2]};
 35    }
 36
 37    [[nodiscard]] constexpr double operator[](std::size_t index) const noexcept
 38    {
 39        return elements_[index];
 40    }
 41
 42    constexpr double& operator[](std::size_t index) noexcept
 43    {
 44        return elements_[index];
 45    }
 46
 47    constexpr Vec3& operator+=(const Vec3& other) noexcept
 48    {
 49        elements_[0] += other.elements_[0];
 50        elements_[1] += other.elements_[1];
 51        elements_[2] += other.elements_[2];
 52        return *this;
 53    }
 54
 55    constexpr Vec3& operator*=(double scalar) noexcept
 56    {
 57        elements_[0] *= scalar;
 58        elements_[1] *= scalar;
 59        elements_[2] *= scalar;
 60        return *this;
 61    }
 62
 63    constexpr Vec3& operator/=(double scalar) noexcept
 64    {
 65        return *this *= 1.0 / scalar;
 66    }
 67
 68    [[nodiscard]] double length() const noexcept
 69    {
 70        return std::sqrt(lengthSquared());
 71    }
 72
 73    [[nodiscard]] constexpr double lengthSquared() const noexcept
 74    {
 75        return elements_[0] * elements_[0] + elements_[1] * elements_[1] + elements_[2] * elements_[2];
 76    }
 77
 78    [[nodiscard]] bool nearZero() const noexcept
 79    {
 80        constexpr auto threshold = 1e-8;
 81        return std::abs(elements_[0]) < threshold && std::abs(elements_[1]) < threshold &&
 82               std::abs(elements_[2]) < threshold;
 83    }
 84
 85private:
 86    std::array<double, 3> elements_{};
 87};
 88
 89inline std::ostream& operator<<(std::ostream& out, const Vec3& v)
 90{
 91    return out << v.x() << ' ' << v.y() << ' ' << v.z();
 92}
 93
 94[[nodiscard]] constexpr Vec3 operator+(const Vec3& lhs, const Vec3& rhs) noexcept
 95{
 96    return Vec3{lhs.x() + rhs.x(), lhs.y() + rhs.y(), lhs.z() + rhs.z()};
 97}
 98
 99[[nodiscard]] constexpr Vec3 operator-(const Vec3& lhs, const Vec3& rhs) noexcept
100{
101    return Vec3{lhs.x() - rhs.x(), lhs.y() - rhs.y(), lhs.z() - rhs.z()};
102}
103
104[[nodiscard]] constexpr Vec3 operator*(const Vec3& lhs, const Vec3& rhs) noexcept
105{
106    return Vec3{lhs.x() * rhs.x(), lhs.y() * rhs.y(), lhs.z() * rhs.z()};
107}
108
109[[nodiscard]] constexpr Vec3 operator*(double scalar, const Vec3& v) noexcept
110{
111    return Vec3{scalar * v.x(), scalar * v.y(), scalar * v.z()};
112}
113
114[[nodiscard]] constexpr Vec3 operator*(const Vec3& v, double scalar) noexcept
115{
116    return scalar * v;
117}
118
119[[nodiscard]] constexpr Vec3 operator/(const Vec3& v, double scalar) noexcept
120{
121    return (1.0 / scalar) * v;
122}
123
124[[nodiscard]] constexpr double dot(const Vec3& lhs, const Vec3& rhs) noexcept
125{
126    return lhs.x() * rhs.x() + lhs.y() * rhs.y() + lhs.z() * rhs.z();
127}
128
129[[nodiscard]] constexpr Vec3 cross(const Vec3& lhs, const Vec3& rhs) noexcept
130{
131    return Vec3{
132        lhs.y() * rhs.z() - lhs.z() * rhs.y(),
133        lhs.z() * rhs.x() - lhs.x() * rhs.z(),
134        lhs.x() * rhs.y() - lhs.y() * rhs.x(),
135    };
136}
137
138[[nodiscard]] inline Vec3 unitVector(const Vec3& v)
139{
140    return v / v.length();
141}

光线

我们先定义一下光线,光线在图形学中我们定义为:

$$\mathbf{r}(t) = \mathbf{o} + t\mathbf{d} \quad 0\le t < \infty$$

其中

  • $\mathbf{o}$ 表示光线的起点
  • $\mathbf{d}$ 表示光线的方向是一个单位向量,后续只要提到方向都是单位向量
  • $t$ 表示光线的步进,可以这样想象,我们的光线从点 $\mathbf{o}$ 出发,沿着 $\mathbf{d}$ 方向走过时间 $t$ 到达的位置 所以我们可这写出这样的代码
 1#pragma once
 2#include "Vec3.h"
 3
 4class Ray {
 5public:
 6    Ray(const Vec3& origin, const Vec3& direction, double t = 0.0)
 7        : origin_(origin),
 8          direction_(direction),
 9          t_(t)
10    {}
11
12    Vec3 getOrigin() const
13    {
14        return origin_;
15    }
16
17    Vec3 getDirection() const
18    {
19        return direction_;
20    }
21
22    Vec3 operator()(double t) const
23    {
24        return origin_ + direction_ * t;
25    }
26
27private:
28    Vec3 origin_;
29    Vec3 direction_;
30    double t_;
31};

相机

我们再定义一下相机,相机定义了我们以什么角度什么位置看向场景,所以它应该有以下属性

  • Position 相机的位置
  • Traget 相机看向哪个位置
  • Up 相机的向上方向,就像我们让一个向量从我们的大脑中心指向我们头顶的最高点一样,这个方向决定了相机的是怎么以z为轴旋转的(翻滚角),就像你用手机拍照可以竖着拍,也可以横着拍
  • Vertical Fov 垂直可视范围,定义了我们在垂直方向上能看到多少内容,就像我们的眼睛一样,我们看到的东西的范围是有限的,我们没办法看到我们头顶上的东西

除此之外我们还需要存放图片信息

  • Image Width 像素空间的宽度
  • Image Height 像素空间的高度
  • Aspect Ratio 宽高比,保留计算结果,后续多次使用
  • Frame Buffer 像素空间,存放我们渲染的结果

除此之外我们还需要预计算图片上的像素是怎么偏移的,这个偏移值不一定是1,因为我们的fov会影响图片的偏移量,为什么会被多次使用?因为我们上面提到过,我们会遍历像素空间多次获取光线

  • Pixel Left Top 左上角第一个像素(0,0)的中心点空间坐标,保留计算结果,后续多次使用
  • Pixel DeltaU 水平方向移动一个像素的向量差,保留计算结果,后续多次使用
  • Pixel DeltaV 垂直方向移动一个像素的向量差,保留计算结果,后续多次使用

我们需要从相机中发射出一条光线所以它应该需要一个获得光线的方法
获取光线很简单,获取一个像素点的位置,用像素点的位置减去相机原点位置就得到了一根光线。
所以最重要的问题是怎么获取一个像素点的位置 我们需要通过相机的除了我们需要计算的属性计算出 Pixel Left Top 、Pixel DeltaU 、Pixel DeltaV

vertical_fov_ 的定义:一个平面与经过了相机点,并且垂直于视口,这个平面会与视口平面相交,交线与相机点会形成一个三角形,在这个三角形中相机点这个这个顶点的角度就是Vertical Fov
我们假设我们的相机的焦距(相机与视口之间的距离)始终是1。 下面的图片解释了如何计算

使用代码实现

 1void initCamera()
 2{
 3    Vec3 forward = unitVector(target_ - position_);
 4    Vec3 right = unitVector(cross(forward, up_));
 5    Vec3 camera_up = unitVector(cross(right, forward));
 6
 7    double theta = vertical_fov_ * 3.141592653589793 / 180.0;
 8    double viewport_height = 2.0 * std::tan(theta / 2.0);
 9    double viewport_width = aspect_ratio_ * viewport_height;
10
11    Vec3 viewport_u = right * viewport_width;
12    Vec3 viewport_v = camera_up * (-viewport_height);
13
14    pixel_delta_u_ = viewport_u / image_width_;
15    pixel_delta_v_ = viewport_v / image_height_;
16
17    Vec3 viewport_left_top_ = position_ + forward - (viewport_u / 2.0) - (viewport_v / 2.0);
18
19    pixel_left_top_ = viewport_left_top_ + 0.5 * (pixel_delta_u_ + pixel_delta_v_);
20}

现在我们能够获取像素空间中任意像素的位置了,通过pixel_left_top_,pixel_delta_u_和pixel_delta_v_

1Ray getRay(int i, int j) const
2{
3    Vec3 pixel_center = pixel_left_top_ + (i * pixel_delta_u_) + (j * pixel_delta_v_);
4    Vec3 ray_direction = unitVector(pixel_center - position_);
5    return Ray{position_, ray_direction};
6}

最终我们的相机长这样

 1#pragma once
 2#include "Ray.h"
 3#include "Vec3.h"
 4
 5#include <cmath>
 6
 7#include <vector>
 8
 9class Camera {
10public:
11    Camera(int image_width, int image_height)
12        : position_(Vec3(278, 273, -800)),
13          target_(Vec3(0, 0, 0)),
14          up_(Vec3(0, 1, 0)),
15          vertical_fov_(90),
16          image_width_(image_width),
17          image_height_(image_height),
18          aspect_ratio_(static_cast<double>(image_width) / image_height),
19          frame_buffer_(image_width * image_height)
20    {
21        initCamera();
22    }
23
24    int getImageWidth() const
25    {
26        return image_width_;
27    }
28
29    int getImageHeight() const
30    {
31        return image_height_;
32    }
33
34    std::vector<Vec3>& getFrameBuffer()
35    {
36        return frame_buffer_;
37    }
38
39    const std::vector<Vec3>& getFrameBuffer() const
40    {
41        return frame_buffer_;
42    }
43
44    Ray getRay(int i, int j) const
45    {
46        Vec3 pixel_center = pixel_left_top_ + (i * pixel_delta_u_) + (j * pixel_delta_v_);
47        Vec3 ray_direction = unitVector(pixel_center - position_);
48        return Ray{position_, ray_direction};
49    }
50
51private:
52    Vec3 position_;
53    Vec3 target_;
54    Vec3 up_;
55    double vertical_fov_;
56    int image_width_;
57    int image_height_;
58    double aspect_ratio_;
59    std::vector<Vec3> frame_buffer_;
60
61    Vec3 pixel_left_top_;
62    Vec3 pixel_delta_u_;
63    Vec3 pixel_delta_v_;
64
65    void initCamera()
66    {
67        Vec3 forward = unitVector(target_ - position_);
68        Vec3 right = unitVector(cross(forward, up_));
69        Vec3 camera_up = unitVector(cross(right, forward));
70
71        double theta = vertical_fov_ * 3.141592653589793 / 180.0;
72        double viewport_height = 2.0 * std::tan(theta / 2.0);
73        double viewport_width = aspect_ratio_ * viewport_height;
74
75        Vec3 viewport_u = right * viewport_width;
76        Vec3 viewport_v = camera_up * (-viewport_height);
77
78        pixel_delta_u_ = viewport_u / image_width_;
79        pixel_delta_v_ = viewport_v / image_height_;
80
81        Vec3 viewport_left_top_ = position_ + forward - (viewport_u / 2.0) - (viewport_v / 2.0);
82
83        pixel_left_top_ = viewport_left_top_ + 0.5 * (pixel_delta_u_ + pixel_delta_v_);
84    }
85};

光源与物体求交

相机和光线我们解决了,接下来我们解决光线如何与物体作用中的光线如何判断与物体相交 我们首先看看光线如何与球体求交 我们知道光线的方程是

$$\mathbf{r}(t) = \mathbf{o} + t\mathbf{d} \quad 0\le t < \infty$$

球体的方程是

$$\mathbf{p}:(\mathbf{p}-\mathbf{c})^2 -R^2 = 0$$

我们令

$$\mathbf{r}(t) =\mathbf{p}$$

$$(\mathbf{o} + t\mathbf{d}-\mathbf{c})^2 -R^2 = 0$$

方程中 $\mathbf{o}$ 、$\mathbf{d}$、$\mathbf{c}$都是向量和$R$是已知的,展开我们得到

$$\mathbf{d}\cdot\mathbf{d}t^2+2(\mathbf{o}-\mathbf{c})\cdot\mathbf{d}t+(\mathbf{o}-\mathbf{c})\cdot(\mathbf{o}-\mathbf{c})-R^2=0$$

可以看到向量之间都变为了点乘运算,最后算出来的是t是一个标量,这也符合这个方程,而且这是一个二次方程,所以我们可以直接使用求根公式 我们令

  • $a = \mathbf{d}\cdot\mathbf{d}$
  • $b = 2(\mathbf{o}-\mathbf{c})\cdot\mathbf{d}$
  • $c = (\mathbf{o}-\mathbf{c})\cdot(\mathbf{o}-\mathbf{c})-R^2$ 使用求根公式 $$t = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$$我们取 t>0 的就行 使用代码实现,我们首先需要实现一个 Object 基类表示我们所有可以被光线击中的物体,其次我们需要记录我们的交点的信息,比如交点的坐标,是否相交,交点的材质(后续添加)等等
 1#pragma once
 2#include "Ray.h"
 3#include "Vec3.h"
 4
 5#include <optional>
 6
 7struct InterInfo {
 8    bool is_Intersected = false;
 9    Vec3 position;
10};
11
12class Object {
13    Object() = default;
14    virtual ~Object() = default;
15    virtual bool intersect(const Ray& ray) const = 0;
16
17    virtual InterInfo getInterInfo(const Ray& ray) const = 0;
18
19protected:
20    InterInfo inter_info_;
21};

我们再实现一个Sphere子类,通过我们上面的公式可以实现

 1#include "Object.h"
 2
 3class Sphere final : public Object {
 4public:
 5    Sphere(const Vec3& center, double radius)
 6        : center_(center),
 7          radius_(radius)
 8    {}
 9
10    ~Sphere() noexcept override = default;
11
12    // 使用 b^2 - 4ac > 0 来判断是否有实根,如果有实根则说明与球相交
13    bool intersect(const Ray& ray) const override
14    {
15        Vec3 oc = ray.getOrigin() - center_;
16        double a = dot(ray.getDirection(), ray.getDirection());
17        double b = 2.0 * dot(oc, ray.getDirection());
18        double c = dot(oc, oc) - radius_ * radius_;
19        double discriminant = b * b - 4 * a * c;
20        return discriminant > 0;
21    }
22
23    InterInfo getInterInfo(const Ray& ray) const override
24    {
25        Vec3 oc = ray.getOrigin() - center_;
26        double a = dot(ray.getDirection(), ray.getDirection());
27        double b = 2.0 * dot(oc, ray.getDirection());
28        double c = dot(oc, oc) - radius_ * radius_;
29        double discriminant = b * b - 4 * a * c;
30
31        if (discriminant > 0) {
32            double sqrt_disc = std::sqrt(discriminant);
33            double t1 = (-b - sqrt_disc) / (2.0 * a);
34            double t2 = (-b + sqrt_disc) / (2.0 * a);
35
36            double t = (t1 < t2 && t1 > 1e-8) ? t1 : t2; // 选择较小的正根
37            if (t > 1e-8) {
38                Vec3 hit_position = ray(t);
39                return InterInfo{true, hit_position};
40            }
41        }
42
43        return InterInfo{false, Vec3()};
44    }
45
46private:
47    Vec3 center_;
48    double radius_;
49};

我们再定义一个场景类,用于存放我们场景中存在的东西

 1#pragma once
 2#include "Camera.h"
 3#include "Object.h"
 4#include "Sphere.h"
 5#include "Vec3.h"
 6
 7#include <memory>
 8#include <vector>
 9
10class Scene {
11
12public:
13    Scene(Camera camera)
14        : camera_(camera)
15    {}
16
17    ~Scene() = default;
18
19    void addObject(std::unique_ptr<Object> object)
20    {
21        objects_.push_back(std::move(object));
22    }
23
24    void addSphere(const Vec3& center, double radius)
25    {
26        addObject(std::make_unique<Sphere>(center, radius));
27    }
28
29    void clearObjects()
30    {
31        objects_.clear();
32    }
33
34    Camera& getCamera()
35    {
36        return camera_;
37    }
38
39    const Camera& getCamera() const
40    {
41        return camera_;
42    }
43
44    const std::vector<std::unique_ptr<Object>>& getObjects() const
45    {
46        return objects_;
47    }
48
49private:
50    Camera camera_;
51    std::vector<std::unique_ptr<Object>> objects_;
52};

我们再定义一个渲染器用于渲染我们最终的图

 1#include "Camera.h"
 2#include "Ray.h"
 3#include "Scene.h"
 4#pragma once
 5
 6class Renderer {
 7public:
 8    Renderer() = default;
 9    ~Renderer() = default;
10
11    void render(Camera& camera, const std::vector<std::unique_ptr<Object>>& objects)
12    {
13        for (int j = 0; j < camera.getImageHeight(); ++j) {
14            for (int i = 0; i < camera.getImageWidth(); ++i) {
15                Ray ray = camera.getRay(i, j);
16                Vec3 pixel_color = traceRay(ray, objects);
17                camera.getFrameBuffer()[j * camera.getImageWidth() + i] = pixel_color;
18            }
19        }
20    }
21
22private:
23    Vec3 traceRay(const Ray& ray, const std::vector<std::unique_ptr<Object>>& objects)
24    {
25        for (const auto& object : objects) {
26            if (object->intersect(ray)) {
27                // 如果击中物体则显示红色
28                return Vec3(255.0, 0.0, 0.0);
29            }
30        }
31        // 否则显示黑色
32        return Vec3(0.0, 0.0, 0.0);
33    }
34};

最后我们修改mian.cpp

 1#include "Camera.h"
 2#include "Object.h"
 3#include "Renderer.h"
 4#include "Scene.h"
 5#include "Vec3.h"
 6
 7#include <filesystem>
 8#include <fstream>
 9#include <iostream>
10#include <memory>
11#include <vector>
12
13void writePPM(const std::filesystem::path& path, int width, int height, const std::vector<Vec3>& data)
14{
15    std::ofstream ofs(path);
16
17    ofs << "P3\n" << width << " " << height << "\n255\n";
18
19    for (int i = 0; i < width * height; ++i) {
20        int r = data[i].x();
21        int g = data[i].y();
22        int b = data[i].z();
23
24        ofs << r << " " << g << " " << b << "\n";
25    }
26
27    ofs.close();
28}
29
30int main()
31{
32    int width = 600;
33    int height = 600;
34    Camera camera(width, height);
35
36    std::unique_ptr<Object> sphere1 = std::make_unique<Sphere>(Vec3(0, 0, 0), 600);
37    Scene scene(camera);
38    scene.addObject(std::move(sphere1));
39
40    Renderer renderer;
41    renderer.render(scene.getCamera(), scene.getObjects());
42    renderer.render(camera, scene.getObjects());
43
44    writePPM("out.ppm", width, height, camera.getFrameBuffer());
45
46    return 0;
47}

最后我们能渲染出来这个图,跟着代码的思路来有一些C++项目开发经验应该就能懂