物体列表的实现
如果你完成了上一章中的小问题,你应该知道我们的代码对单个物体支持良好。现在咱们把情况推广到更一般的情况——一个有多个物体的场景。<br />你看到本章的标题,你应该也想到我们应该怎么实现多个物体的场景了——一个列表,或者说一个数组。把物品都存进去,然后调用每个物体的hit函数。<br />心急的你是不是已经在main函数中创建物体数组了?你最好别这样做,我们已经强调了很多遍main函数并不是你的抽屉,什么东西都能塞到里面。<br />那我们能把物体列表放在什么地方呢?我们能放到hittable类里面吗?显然不行,因为hittable表示一个可以被光线碰撞的单个物体,把列表放在这里显然不合适。<br />或许我们应该把它放在某一个全局配置类里面,但实际上物体数组的维护远比你想象的要复杂,你有考虑过物体之间的相互遮挡吗?对于一个光线,我们需要调用数组中所有物体的hit函数,如果有多个物体返回true(即碰撞到),我们到底应该选择哪个?这些物体的生命周期又该如何管理,在各个函数的参数中传来传去会不会出现内存泄漏?<br />我们需要一个单独的类来管理物体列表,这个类把我们关于多个物体之间互相影响产生的麻烦事情都搞定。<br />我们在这里让这个**物品列表类继承自物品类**,你是不是会觉得这样设计非常的奇怪,就好像“一群动物”是“动物”的子类一样。但是,“一堆物品”是“物品”这个逻辑不奇怪,物品可以被光线hit,物品列表也可以被光线hit,我们可以在它的hit函数中调用父类“物品类”的hit函数处理我们刚刚提到的各个物体之间的相互遮挡问题。这样设计对我们编码的简易度来说有百利无一害。具体可以看下面的代码,创建hittable_list.h文件,敲入如下代码:
#ifndef HITTABLE_LIST_H
#define HITTABLE_LIST_H
#include "hittable.h"
//我们需要<memory>下的一些api。
#include <memory>
#include <vector>
//智能指针!它可是C++程序员的福音啊!我们用它来帮我们管理物体列表,就不用担心它们的内存泄漏问题了!
using std::shared_ptr;
using std::make_shared;
class hittable_list : public hittable {
public:
hittable_list() {}
//当创建物体列表的时候传入了某一个物体,我们直接调用add函数把这个物体加入列表。
hittable_list(shared_ptr<hittable> object) { add(object); }
//clear函数调用列表的clear函数清空列表。
void clear() { objects.clear(); }
//add函数调用push_back把新物体的智能指针加入列表
void add(shared_ptr<hittable> object) { objects.push_back(object); }
//声明我们需要override父类的hit函数。
virtual bool hit(
const ray& r, double t_min, double t_max, hit_record& rec) const override;
public:
//物体列表,我们使用vector去存,注意我们存的是每个物体的智能指针。
std::vector<shared_ptr<hittable>> objects;
};
//重写之后的hit函数。
bool hittable_list::hit(const ray& r, double t_min, double t_max, hit_record& rec) const {
hit_record temp_rec;
bool hit_anything = false;
//最开始,把能接受的最远位置设置成外部传进来的t_max。
//外面调用物体列表的hit函数的时候,会说“我给了你t的范围哈,如果发现碰撞点不在这个范围里,你就不要管”
auto closest_so_far = t_max;
//我们开始对每个物体做碰撞
for (const auto& object : objects) {
//进入这个if就表示,确实光线射到了这一堆物体里的某一个。
if (object->hit(r, t_min, closest_so_far, temp_rec)) {
//光线有射中东西,我们需要把最终函数的返回值设定为true。
hit_anything = true;
//注意,我现在已经射中了一个东西了,但我不确定我射中的这个物体是不是离相机最近的。
//即,它有没有被其他物体遮挡我们并不清楚,所以我得把范围缩小,然后继续遍历物体列表。
closest_so_far = temp_rec.t;
//每次有新的碰撞,就设定它的record为最终record。
//这样我们遍历下来,就可以找到最近的碰撞点并返回它的rec。
rec = temp_rec;
}
}
//返回这根光线是否有碰到物体列表中的任何物体。
return hit_anything;
}
#endif
代码分析
物品列表的代码有很多东西可以讲,我们先从基础的讲起,我得先确保你确实明白上述代码中的两个语法现象:智能指针和多态。
1)智能指针。
C++11给我们提供的瑰宝,它让我们C++程序员体验到了java中用完就丢,有保姆帮我们收拾的快感。它的本质是维护一个指针和一个计数,当创建智能指针并让他指向一个对象的时候,引用计数为1,之后每有一个新的指向它的指针被创建,引用计数加1,一个指针被销毁或者不再指向它,引用计数会下降。引用计数为0则释放对象所占内存。
既然智能指针这么好用为什么之前的类里我们没有搬出来用呢?因为之前的类我们一直都在“造轮子”,这个物品列表类的作用是管理我们之前做好的“轮子”。你要记住这句话:C++中用于管理一些对象的类,一般都是通过指针去管理。只要不是某些奇葩的设计模式或者某些对这方面不太在意的小型程序,基本上都不会通过引用或者直接通过直接拿取对象本身来管理,这样对象传来传去太蠢了。
况且通过指针管理对象还有另一层作用,那就是“多态”。
2)多态。
我不在这里详细描述多态的具体实现方式了,太冗长,它属于C++基础知识范畴,你应该掌握它。
继承结构内的多态的一大实现条件就是基类指针指向子类对象。这是一个极其经典的场景,我们通过虚函数的重写,再通过基类指针定义某一个接口,在实际程序运行过程中无论来的是哪个子类对象,我们的代码都可以精确的调到对应的虚函数。
看看我们的程序,我们的hit函数是如何完成多态的?我们的物品列表类只说明了这个列表里面的东西会是一个物体,即它存的是基类指针。物体列表的hit函数中分别调用了列表中每一个指针指向对象的hit函数。
我们并不知道这些物体到底是个球,或者是其他的什么东西,我们只管调用它,多态会帮我们找到具体到底是那个函数并且调用到它。
在本章的开头我就提过,我们把物品列表类设计成了物品类的子类。如果你还没有领会这样设计的好处,我来给你阐述一个只有这样设计才能达成的疯狂的玩法!多态是基类指针指向子类对象,别忘了物品列表也是物品类的子类,你知道这意味着什么吗?我们可以在这里进行无限层的嵌套!比如下面的某个物品列表:
-物品列表
-子物品列表1
-球1
-球2
-子物品列表2
-球3
-球4
嘿,我们现在给调用最外层的物品列表的hit函数,
我们问这个列表:“这根光线有没有和这一大堆东西碰撞呀,碰撞结果如何啊?”
最外层的列表说:“我给你看看把,我这里有俩玩意,但我不知道这俩玩意是啥,我帮你找他们问问(最外层列表只知道列表中有俩物体,它不知道这些物体到底是啥)”
紧接着程序开始按照名单给这俩小物品列表发消息:“嘿,你们是什么玩意啊?是球吗?我不管你们是不是球,上面发话了,要看看这根光线有没有和你们碰撞,碰撞情况如何啊?我把光线信息和上面要求的t的范围发给你了啊,再给你一个地址,你们俩把record填好啊!我不管了啊!”(像不像你的领导)
好了,俩小物品列表收到消息也急了,它赶忙找它们下面的东西:“嘿!听的到么!我不管你们是谁,老板要求你们把这根光线…….(省略)”(程序员就像这些球┭┮﹏┭┮)
最外层的物品列表只需要维护自己的closest_so_far,比较两个子物品列表给出的t的大小并选择最小的那个,而再下层的东西它完全不用去管。
这叫什么?递归!我们用“物品列表类是物品类的子类”这一设计,实现了一个精美的递归代码帮我们完成一个光线和一堆物品的碰撞。这也多亏了多态的支持。我无法想象如果我们没有多态,我们要实现一个嵌套列表到底要多出几百倍的代码。
我画了一张类图在这里,希望可以帮助你理解这部分代码:
图上可以看到,hittable_list虽然继承自hittable,但是它其中却有hittable类型的成员变量(物体指针vector),如果你能理解上面我对于多态的表示,相信你也很容易就能看懂这一张图。注意图上很多不重要的函数的参数我省略掉了。
数学类
我们现在还需要什么呢?我觉得现在最要紧的是一些常量数字,比如无穷大。不然我们要怎么传最初始的t_max值呢?我们不能在一开始就限定它为10000,1000之类的。显得很不专业。况且,一些常量和一些换算之后还会有更多作用。
我们先把我们能想到的写上,之后需要其他的再添加。
为了给《Ray Tracing in One Weekend》这本书给予最真诚的谢意,并遵循原作者意愿,我们把这个文件取名为rtweekend.h。我很推荐没有看过这个书的人去看一下这本英文原著,我觉得其中的有些用词是看翻译作品无法体会的。
敲入如下代码:
#ifndef RTWEEKEND_H
#define RTWEEKEND_H
#include <cmath>
#include <limits>
#include <memory>
using std::shared_ptr;
using std::make_shared;
using std::sqrt;
// Constants
// 直接使用C++帮我们定义好的double类型的无限值作为无穷大
const double infinity = std::numeric_limits<double>::infinity();
// Π。 我们先把这个东西放在这,虽然暂时还没有什么用。
const double pi = 3.1415926535897932385;
// 一个角度值转弧度制的函数,嗯。。或许暂时也没有什么用。
inline double degrees_to_radians(double degrees) {
return degrees * pi / 180.0;
}
//这俩头文件可是我们的常客,我们把它们包进来,之后用他们就直接包这个rtweekend文件就行了。
#include "ray.h"
#include "vec3.h"
#endif
“草原”
验收成果的时候到了,我们在main函数所在文件敲入如下代码:
#include "rtweekend.h"
#include "color.h"
#include "hittable_list.h"
#include "sphere.h"
#include <iostream>
//新的光线取色函数中,我们加入了一个名为世界的物体对象,别看它是一个物体基类类型,我们一般会传入一个物体列表。
color ray_color(const ray& r, const hittable& world) {
hit_record rec;
//我们使用到了我们定义的无限大,并且剔除了负t
if (world.hit(r, 0, infinity, rec)) {
//没错,依然是元气弹
return 0.5 * (rec.normal + color(1,1,1));
}
//呵呵...蓝天...
vec3 unit_direction = unit_vector(r.direction());
auto t = 0.5*(unit_direction.y() + 1.0);
return (1.0-t)*color(1.0, 1.0, 1.0) + t*color(0.5, 0.7, 1.0);
}
int main() {
const auto aspect_ratio = 16.0 / 9.0;
const int image_width = 400;
const int image_height = static_cast<int>(image_width / aspect_ratio);
// 创造世界
hittable_list world;
//插入俩个球
//这个球是老朋友。
world.add(make_shared<sphere>(point3(0,0,-1), 0.5));
//一个非常吓人的巨型球!
world.add(make_shared<sphere>(point3(0,-100.5,-1), 100));
//摄像机相关,是不是开始觉得这部分代码很烦人了?别急,下一章中我们就把它从这移走。
auto viewport_height = 2.0;
auto viewport_width = aspect_ratio * viewport_height;
auto focal_length = 1.0;
auto origin = point3(0, 0, 0);
auto horizontal = vec3(viewport_width, 0, 0);
auto vertical = vec3(0, viewport_height, 0);
auto lower_left_corner = origin - horizontal/2 - vertical/2 - vec3(0, 0, focal_length);
//渲染循环(render loop)
std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n";
for (int j = image_height-1; j >= 0; --j) {
std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush;
for (int i = 0; i < image_width; ++i) {
auto u = double(i) / (image_width-1);
auto v = double(j) / (image_height-1);
ray r(origin, lower_left_corner + u*horizontal + v*vertical);
//和原来没什么不同不是么?这不正说明我们的代码封装的非常完美,main函数几乎都不需要改动。
color pixel_color = ray_color(r, world);
write_color(std::cout, pixel_color);
}
}
std::cerr << "\nDone.\n";
}
看!草原!
我们把一颗巨大的球当成了地面,就好像我们的星球一样。
到此为止,我们实现了场景中多个物体的渲染,我们底层强大的物体列表让我们在main函数中不费吹灰之力就完成了这一切。
现在我们还缺少什么呢?你放大我们的图像看看,是不是觉得很粗糙,球体边缘的锯齿状让你感觉很不舒服吧?下一章中,我们将解决这个问题,并且你也觉得相机的代码放在main函数中很不合适对吧,我们也顺带把它解决了吧。
练习题
1)记得我们在解释物体列表中的多态的时候提到了一个物体列表:
-物品列表
-子物品列表1
-球1
-球2
-子物品列表2
-球3
-球4
请你改动main函数中的代码,制作一个这样的列表,并且能让四颗球可以在图片中显示出来(随便这些球摆成什么样子,尽可能好看哟~)
2) 你有办法绘制一张图片,图片中有数个纯色球,每个球的颜色都不相同吗?(可以尝试给球类和hit_record结构体添加一些成员变量来完成)
参考文献
https://raytracing.github.io/books/RayTracingInOneWeekend.html
参考自《RayTracingInOneWeekend》第6.5节到第6.7节。