在上一章由于急于实现功能而污染了表层文件,我们需要将它抽象出来,才能继续制作其他材质。
抽象材质类
材质类应该干什么?应该能产生反射光线,并且记录诸如光线衰减信息(上一章中的对半衰减)等,总结一句话,它得通过入射光线,得知反射光线的方向和能量大小。
创建material.h文件,写入”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #ifndef MATERIAL_H #define MATERIAL_H
#include "rtweekend.h" #include "hittable.h"
class material { public: virtual bool scatter( const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const = 0; }; #endif
|
物体颜色信息指的是物体对于各个红绿蓝三种光线的吸收率,在上一章中,一直使用的是各种光都对半吸收的参数0.5,这一章中,我们要把各个颜色分量给区分开,以创造出更绚烂的色彩变化。
函数的返回值是bool,留下一个标记,为了在函数出问题的时候让我们有能力进行追踪。
在hit_record里加入材质信息:
1 2 3 4 5 6 7 8 9 10 11 12 13
| #include "rtweekend.h" #include "material.h"
struct hit_record { ... shared_ptr<material> mat_ptr;
inline void set_face_normal(const ray& r, const vec3& outward_normal) { ... } };
|
这时候我们会发现,程序开始报一些莫名其妙的错误了。
这是因为material.h和hittable.h文件互相包含了。main函数开始链接各个文件的时候,采取的策略是见一个就包含一个,并且包含所包含文件的头文件。互相包含问题会导致我们的main函数怎么努力包含都无法包含完这些文件,所以直接摆烂了。
遇到这种情况怎么办呢?我们观察到,hittable.h中虽然定义了一个material的智能指针,但是,它从来没有试图访问过这个指针所指向的对象。所以完全不需要让它包含material.h文件,只需要在使用它之前声明即可:
1 2 3
| class material; struct hit_record { ... }
|
sphere.h文件也产生了一些变化,具体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class sphere : public hittable { public: sphere() {} sphere(point3 cen, double r, shared_ptr<material> m) : center(cen), radius(r), mat_ptr(m) {};
virtual bool hit( const ray& r, double t_min, double t_max, hit_record& rec) const override;
public: point3 center; double radius; shared_ptr<material> mat_ptr; };
bool sphere::hit(const ray& r, double t_min, double t_max, hit_record& rec) const { ... rec.mat_ptr = mat_ptr; return true; }
|
磨砂材质
我们只需要模仿main函数中已经写过一遍的代码就可以得到代码。可以把这个类直接写在material.h文件中,这种设计的方式是“一档多类”。它对于多个短小的类来说很有用,它可以防止文件数量过多带来的不便。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class lambertian : public material { public: lambertian(const color& a) : albedo(a) {}
virtual bool scatter( const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered ) const override { auto scatter_direction = rec.normal + random_unit_vector(); scattered = ray(rec.p, scatter_direction); attenuation = albedo; return true; }
public: color albedo; };
|
和main函数中唯一的不同就是我们可以指定材质的颜色了。
但作为底层代码,我们应该想到更多,比如在随机选点的时候,我们有可能会选到离碰撞点很近的点,然后进而导致生成的反射方向极度接近于0向量。
从代码鲁棒性的角度来看,这会导致我们在后续的某些计算中——比如球类中光线和球求焦点的代码中,除以一个和0很接近的数,这可能会导致我们最终的值变成infinities或者NaNs等等奇怪的结果(即便现在没有不代表未来不会有)。
所以我们要剔除零反射向量。先在vec3.h中写一个判断一个向量是否是0向量的函数:
1 2 3 4 5 6 7 8 9 10
| class vec3 { ... bool near_zero() const { const auto s = 1e-8; return (fabs(e[0]) < s) && (fabs(e[1]) < s) && (fabs(e[2]) < s); } ... };
|
修改磨砂材质类,消灭零反射向量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class lambertian : public material { public: lambertian(const color& a) : albedo(a) {}
virtual bool scatter( const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered ) const override { auto scatter_direction = rec.normal + random_unit_vector();
if (scatter_direction.near_zero()) scatter_direction = rec.normal;
scattered = ray(rec.p, scatter_direction); attenuation = albedo; return true; }
public: color albedo; };
|
成果
在main函数所在文件中使用磨砂材质类,除了要包含material头文件之外,还需要在main中定义数个智能指针指向新创建的材质对象,再把原本两个参数的球构造函数改成三个参数的。当然最重要的部分还是我们的光线取色函数中和材质类的交互,.cpp文件中代码修改为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| ... #include "material.h"
color ray_color(const ray& r, const hittable& world, int depth) { hit_record rec; if (depth <= 0) return color(0,0,0);
if (world.hit(r, 0.001, infinity, rec)) { ray scattered; color attenuation; if (rec.mat_ptr->scatter(r, rec, attenuation, scattered)) return attenuation * ray_color(scattered, world, depth-1); return color(0,0,0); }
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(){ ... hittable_list world; auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0)); auto material_center = make_shared<lambertian>(color(0.7, 0.3, 0.3)); world.add(make_shared<sphere>(point3(0, 0, -1), 0.5, material_center)); world.add(make_shared<sphere>(point3(0, -100.5, -1), 100, material_ground))
... }
|
就可以得到有颜色的磨砂材质了:

拓展
材质属性可以放在hittable中吗?
我认为是可以的,但是这样做目前看来并没有什么好处。在没有出现其他形状的物体之前,把它放在球类中,我们可以清晰的看到该类的所有成员,是目前的最优解。
材质类与hitable可解耦吗?
我认为是不可以的,材质类本就不是一个很独立的模块,碰撞点信息里包含物体材质信息,而材质类依托碰撞点信息(如法线)才能计算反射方向。我们从学习代码最初就被叮嘱要进行代码解耦,但是解耦并不代表着类与类之间一定不能有半点交际。材质和物体本就是关系极为紧密的两个事物。只有有了物体,我们才会讨论物体的材质,而且没了材质,我们也就无法知晓物体的外观。把它们分成两个类,是为了能更好的实现代码复用,是为了以后有了多种物体和多种材质之后,能由我们自己随心所欲的进行“连连看”配对。
参考文献
https://raytracing.github.io/books/RayTracingInOneWeekend.html
参考自《RayTracingInOneWeekend》第9.1节到9.3节。