8.1 材料的抽象类
如果我们想要不同的物体有不同的材料,那么有两种做法,一种是硬编码,在通用材质函数中设置很多个参数,要呈现一种材质只需要将其他一些参数置零,或者将材料抽象成一个类。将材料抽象成一个material类会更加优雅。在这个项目中,材质需要做以下两件事:
- 生成散射光线(或者说吸收入射光线)
- 如果散射,说明射线应该衰减多少
我们的抽象类如下:
// 网页代码 material.h
#ifndef MATERIAL_H
#define MATERIAL_H
class material {
public:
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const = 0;
};
#endif
8.2 一种用于描述射线对象相交的数据结构
上面的scatter函数中的hit_record参数的使用是为了避免在函数参数列表中使用太多参数,将他们放到一个数据结构中。(当然也可以使用很多参数,只是个人喜好)Hittalbe和materials类需要互相知道对方,这将会导致循环引用。在C++中,你只需要提醒编译器指针指向一个类,下面的hittable类中的“calss material”就是这样做的:
// 网页上的代码 hittable.h
#ifndef HITTABLE_H
#define HITTABLE_H
#include "rtweekend.h"
#include "ray.h"
class material;
struct hit_record {
point3 p;
vec3 normal;
shared_ptr<material> mat_ptr;
double t;
bool front_face;
inline void set_face_normal(const ray& r, const vec3& outward_normal) {
front_face = dot(r.direction(), outward_normal) < 0;
normal = front_face ? outward_normal :-outward_normal;
}
};
class hittable {
public:
virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const = 0;
};
#endif
我们将在这里设置的是,这种材料将告诉我们光线如何与表面相互作用。当一条光线击中一个物体表面(假设是一个球体),当我们开始在main()中设置球体时,在hit_record中的材质的指针将被设置为指向材质的指针。(When a ray hits a surface (a particular sphere for example), the material pointer in the hit_record will be set to point at the material pointer the sphere was given when it was set up in main() when we start.) 当ray_color()例程获得hit_record时,它可以调用material pointer的成员函数找出散射的光线(如果存在的话)。
为了实现这个,我们需要在hit_record中返sphere calss的一个对material的引用。See the highlighted:
// 网页上的代码sphere.h
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 tmin, double tmax, hit_record& rec) const;
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 {
vec3 oc = r.origin() - center;
auto a = r.direction().length_squared();
auto half_b = dot(oc, r.direction());
auto c = oc.length_squared() - radius*radius;
auto discriminant = half_b*half_b - a*c;
if (discriminant > 0) {
auto root = sqrt(discriminant);
auto temp = (-half_b - root)/a;
if (temp < t_max && temp > t_min) {
rec.t = temp;
rec.p = r.at(rec.t);
vec3 outward_normal = (rec.p - center) / radius;
rec.set_face_normal(r, outward_normal);
rec.mat_ptr = mat_ptr;
return true;
}
temp = (-half_b + root) / a;
if (temp < t_max && temp > t_min) {
rec.t = temp;
rec.p = r.at(rec.t);
vec3 outward_normal = (rec.p - center) / radius;
rec.set_face_normal(r, outward_normal);
rec.mat_ptr = mat_ptr;
return true;
}
}
return false;
}
8.3光散射和反射率建模
对于Lambertian(diffuse)我们可以通过反射率R,或者(1 - R)的吸收率,或者同时使用这两个特征进行建模。通过对material类的p(ublic继承,我们可以写出一个简单的Lambertian类:
- 包含的成员变量:颜色color
- 拥有的方法:
- 带参初始化
- 判断是否散射(如果是,返回散射的光线和颜色)
- 计算散射方向:(最初的生成随机漫反射的方法,或自选)= 击中一侧的点上的表面法向量 + 随机生成单位球内的点
- 生成散射的光线并记录到引用参数中
- 记录颜色到引用参数中
代码如下:
// 网站上的代码 material.h
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 {
vec3 scatter_direction = rec.normal + random_unit_vector();
scattered = ray(rec.p, scatter_direction);
attenuation = albedo;
return true;
}
public:
color albedo;
};
注意,我们也可以只使用概率p进行散射,而衰减为albedo/p。你的选择。
8.4镜面光反射
光滑金属的材料不会随机反射,可以运用向量计算获得从金属镜中反射的光线:
在这里,我们的n是一个单位向量,而v不一定,由上面的图可知:反射光线:R = V + 2 * B,而B = -(v ⋅ n),我们将这些获得R的计算步骤写入函数reflect中,有:
// 网站上的代码 vec3.h
vec3 reflect(const vec3& v, const vec3& n) {
return v - 2*dot(v,n)*n;
}
这里我们使用上面的函数写一个metal类,还是和上面的Lambertian类的思路一样:
- 包含的成员变量:颜色color
- 拥有的方法:
- 带参初始化
- 判断是否散射(如果是,返回散射的光线和颜色)
- 计算散射方向:调用reflect函数
- 生成散射的光线并记录到引用参数中
- 记录颜色到引用参数中
- 判断散射的光线是否在反射面一侧,返回true or false
代码如下:
// 网页上的代码 material.h
class metal : public material {
public:
metal(const color& a) : albedo(a) {}
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const {
vec3 reflected = reflect(unit_vector(r_in.direction()), rec.normal);
scattered = ray(rec.p, reflected);
attenuation = albedo;
return (dot(scattered.direction(), rec.normal) > 0);
}
public:
color albedo;
};
我们需要修改ray_color()函数:
在判断射线是否击中物体并求交部分:
- 调用物体的material指针,调用求散射射线函数scatter, 如果散射的射线可见,则递归调用ray_color()并乘以光的衰减,否则该散射射线返回黑色。
// 网页上的代码 main.cc
color ray_color(const ray& r, const hittable& world, int depth) {
hit_record rec;
// If we've exceeded the ray bounce limit, no more light is gathered.
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);
}
8.5一个金属球的场景
现在在场景中添加一些金属球:
// 网页上的代码 main.cc
int main() {
const auto aspect_ratio = 16.0 / 9.0;
const int image_width = 384;
const int image_height = static_cast<int>(image_width / aspect_ratio);
const int samples_per_pixel = 100;
const int max_depth = 50;
std::cout << "P3\n" << image_width << " " << image_height << "\n255\n";
hittable_list world;
world.add(make_shared<sphere>(
point3(0,0,-1), 0.5, make_shared<lambertian>(color(0.7, 0.3, 0.3))));
world.add(make_shared<sphere>(
point3(0,-100.5,-1), 100, make_shared<lambertian>(color(0.8, 0.8, 0.0))));
world.add(make_shared<sphere>(point3(1,0,-1), 0.5, make_shared<metal>(color(.8,.6,.2))));
world.add(make_shared<sphere>(point3(-1,0,-1), 0.5, make_shared<metal>(color(.8,.8,.8))));
camera cam;
for (int j = image_height-1; j >= 0; --j) {
std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush;
for (int i = 0; i < image_width; ++i) {
color pixel_color(0, 0, 0);
for (int s = 0; s < samples_per_pixel; ++s) {
auto u = (i + random_double()) / (image_width-1);
auto v = (j + random_double()) / (image_height-1);
ray r = cam.get_ray(u, v);
pixel_color += ray_color(r, world, max_depth);
}
write_color(std::cout, pixel_color, samples_per_pixel);
}
}
std::cerr << "\nDone.\n";
}
效果如下:
8.6模糊反射
我们可以随机化光线反射的方向:在反射光线(下图红色的线,这里为单位长度)上的点做一个圆(下图黑色虚线的圆),在圆内随机选择一个点,让射线与反射面的交点到该店的方向作为反射光的方向(下图蓝色的线)。模糊程度随圆的增大而增大。
我们可以在metal类中添加一个fuzz变量作为圆的半径,表示模糊程度。问题是,对于大的球体或掠射光线,我们可能会分散到地表以下。我们可以让表面吸收它们。
更新后的代码如下:
// 网页上的代码 material.h
class metal : public material {
public:
metal(const color& a, double f) : albedo(a), fuzz(f < 1 ? f : 1) {}
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const {
vec3 reflected = reflect(unit_vector(r_in.direction()), rec.normal);
scattered = ray(rec.p, reflected + fuzz*random_in_unit_sphere());
attenuation = albedo;
return (dot(scattered.direction(), rec.normal) > 0);
}
public:
color albedo;
double fuzz;
};
我们可以在(main.cc中的)金属中加入0.3和1.0的模糊度来尝试一下:
使用第一种漫反射:
使用第二种漫反射:
使用第三种漫反射: