如果我們希望不同的物體使用不同的材料,則需要進行設計決策。我們可以使用具有許多引數的通用材料,而將不同的材料型別僅將其中一些引數歸零。這不是一個壞方法。或者我們可以有一個抽象的材料類來封裝行為。我是後一種方法的粉絲。對於我們的程式,材料需要做兩件事:
- 產生散射射線(或說它吸收了入射射線)。
- 如果散射,說出應將射線衰減多少。
這建議了抽象類:
9.1. An Abstract Class for Materials( 材料抽象類)
#ifndef MATERIAL_H
#define MATERIAL_H
#include "rtweekend.h"
#include "hittable.h"
struct hit_record;
// abstract class
class material {
public:
virtual bool scatter(
const ray &r_in, const hit_record &rec, color &attenuation, ray &scattered
) const = 0;
};
#endif
9.2. A Data Structure to Describe Ray-Object Intersections(描述射線物件相交的資料結構)
hit_record
是為了避免使用一堆引數,因此我們可以在其中填充所需的任何資訊。 您可以改為使用引數。 這是一個品味問題。 hittables
和material
需要彼此瞭解,因此參考文獻有一定的迴圈性。 在C ++中,您只需要警告編譯器該指標是一個類,下面的hittable類中的“類材料”就是這樣做的:
/*該結構體記錄“撞點”處的資訊:離光線起點的距離t、撞點的座標向量p、撞點出的法向量normal.*/
struct hit_record {
point3 p;
vec3 normal;
// new variables
shared_ptr<material> mat_ptr;
double t;
bool front_face;
}
我們在這裡設定的是材料將告訴我們射線如何與表面相互作用。 hit_record只是將一堆引數填充到結構中的一種方法,因此我們可以將它們作為一組傳送。 當光線撞擊表面(例如特定的球體)時,hit_record中的材質指標將被設定為指向當我們開始時在main()中設定該球體時所給定的材質指標。 當ray_color()例程獲取hit_record時,它可以呼叫材質指標的成員函式來找出散射的光線(如果有)。
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.t = root;
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;
}
9.3. Modeling Light Scatter and Reflectance(模擬光散射和反射率)
如果您仔細閱讀上面的程式碼,您會發現一小撮惡作劇的機會。如果我們生成的隨機單位向量與法向向量完全相反,則兩者之和將為零,這將導致散射方向向量為零。這會在以後導致不良情況(無窮大和NaN),因此我們需要在傳遞條件之前先對其進行攔截。
為此,我們將建立一個新的vector方法—vec3::near_zero()
如果向量在所有維度上都非常接近零,則返回true。
class vec3 {
...
bool near_zero() const {
// Return true if the vector is close to zero in all dimensions.
const auto s = 1e-8;
return (fabs(e[0]) < s) && (fabs(e[1]) < s) && (fabs(e[2]) < s);
}
...
};
// 蘭伯特模型類
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();
// Catch degenerate scatter direction
// 此時隨機的向量近似為與normal法線向量相反方向的向量
if (scatter_direction.near_zero())
scatter_direction = rec.normal;
scattered = ray(rec.p, scatter_direction);
attenuation = albedo;
return true;
}
public:
color albedo;
};
9.4. Mirrored Light Reflection(鏡面光反射)
對於光滑的金屬,射線不會被隨機散射。關鍵數學是:射線如何從金屬鏡反射回來? 向量數學是我們的朋友在這裡:
Figure 11: Ray reflection
紅色的反射射線方向為 v+2b。在我們的設計中n是單位向量,但是 v未必。長度b 應該 v*n。因為v 點,我們將需要一個減號,產生:
// 反射
// 法線是單位向量 所以點積結果就是向量v在n上投影的長度
vec3 reflect(const vec3 &v, const vec3 &n) {
return v - 2 * dot(v, n) * n;
}
金屬材料只是使用該公式反射光線:
class metal : public material {
public:
metal(const color &a) : albedo(a) {}
bool scatter(const ray &r_in, const hit_record &rec, color &attenuation, ray &scattered) const override {
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()
函式以使用此函式:
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);
}
9.5. A Scene with Metal Spheres(金屬球的場景)
在場景中新增一些金屬球:
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));
auto material_left = make_shared<metal>(color(0.8, 0.8, 0.8));
auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2));
world.add(make_shared<sphere>(point3( 0.0, -100.5, -1.0), 100.0, material_ground));
world.add(make_shared<sphere>(point3( 0.0, 0.0, -1.0), 0.5, material_center));
world.add(make_shared<sphere>(point3(-1.0, 0.0, -1.0), 0.5, material_left));
world.add(make_shared<sphere>(point3( 1.0, 0.0, -1.0), 0.5, material_right));
大功告成
效果
9.6. Fuzzy Reflection(模糊反射)
我們還可以通過使用小球體併為射線選擇新的端點來隨機化反射方向:
球體越大,反射將變得越模糊。 這建議新增一個模糊度引數,該引數僅是球體的半徑(因此零是沒有擾動)。 要注意的是,對於大球體或掠食性射線,我們可能會散射到水面以下。 我們可以讓表面吸收那些
// 金屬類
class metal : public material {
public:
metal(const color &a, double f) : albedo(a), fuzz(f < 1 ? f : 1) {}
bool scatter(const ray &r_in, const hit_record &rec, color &attenuation, ray &scattered) const override {
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;
};
shared_ptr<metal> material_left = make_shared<metal>(color(0.8, 0.8, 0.8), 0.3);
shared_ptr<metal> material_right = make_shared<metal>(color(0.8, 0.6, 0.2), 1.0);
效果
這圖真的太漂亮了QAQ
修改下隨機函式,可以看不同的效果
// vec3 scatter_direction = rec.normal + random_unit_vector();
vec3 scatter_direction = rec.normal + random_in_unit_sphere();
再改隨機函式為半球散射,繼續跑,多看看效果
// vec3 scatter_direction = rec.normal + random_unit_vector();
// vec3 scatter_direction = rec.normal + random_in_unit_sphere();
vec3 scatter_direction = rec.normal + random_in_hemisphere(rec.normal);