Metal
10.1 An Abstract Class for Materials
现在我们想要实现镜面反射的材质,但我们要如何处理不同物体所使用的不同材质呢?我们可以创建一个材质的抽象类,并且在我们的渲染器中,material
类还需要负责两个功能:
- 创建散布光线,或者入射光线被材质吸收掉
- 如果存在光线散布,则需要给出光线的衰减程度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifndef MATERIAL_H
#define MATERIAL_H
#include "rayTracing.h"
class material
{
public:
virtual ~material() = default;
virtual bool scatter(const ray& rayIncoming, const hitInfo& info, color& attenuation, ray& rayScattered) const
{
return false;
}
};
#endif
10.2 A Data Structure to Describe Ray-Object Inttersections
之前,我们创建了hitInfo
类,用于记录相交点处的信息,现在,我们可以将相交点处的表面所使用的材质也添加到hitInfo
类中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class material;
class hit_record
{
public:
point3 p;
vec3 normal;
shared_ptr<material> mat;
double t;
bool front_face;
void set_face_normal(const ray& r, const vec3& outward_normal)
{
// sets the hit record normal vector
// NOTE: the parameter 'outward_normal' is assumed to have unit length
front_face = dot(r.direction(), outward_normal) < 0;
normal = front_face ? outward_normal : -outward_normal;
}
};
以球体为例,现在我们需要在sphere::hit()
函数中对hitInfo
类中的材质进行赋值:
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
class sphere : public hittable
{
public:
sphere(const point3& center, double radius) : center(center), radius(fmax(0, radius))
{
// TODO: initialize the material pointer `mat`
}
bool hit(const ray& r, interval ray_t, hit_record& rec) const override
{
...
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 = mat;
return true;
}
private:
point3 center;
double radius;
shared_ptr<material> mat;
};
10.3 Modeling Light Scatter and Reflectance
在图形学中,我们会使用反射率albedo这个术语来描述物体表面对光的反射能力,当albedo为0,表示表面完全吸收所有入射光,不反射任何光,如果albedo为1,则表示表面反射所有入射光,不吸收任何光。反射率会根据材质颜色和入射光角度的变化而变化。
在处理漫反射材质时,有两种可供选择的策略:
- 光线与表面交互时都会被散射,同时会根据材质的反射率reflectance R来衰减光线强度。这种方法确保了每次光线与几何体相交都会有反射光线,并且强度会根据反射率逐渐减弱。
- 在光线与表面相互作用时,有1 - R的概率光线会被散射但不会影响强度,且有R的概率光线会被吸收(即不再有反射光线)。这种方法导致有时会有交强的反射光线,而有时会完全没有反射光线
对于我们的渲染器来说,我们选择第一种策略,这样对应的兰伯特材质就会相对简单很多。漫反射材质需要继承自material
类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class material
{
...
};
class lambertian final : public material
{
public:
explicit lambertian(const color& albedo) : albedo(albedo) {}
bool scatter(const ray& rayIncoming, const hitInfo& info, color& attenuation, ray& rayScattered)
const override
{
vec3 direction =info.normal + randomVectorOnUnitSphere();
rayScattered = ray(info.position, direction);
attenuation = albedo;
return true;
}
private:
color albedo;
};
但是我们的代码还存在一点隐患。randomVectorOnUnitSphere()
所返回的向量,只满足长度是归一化的,向量的方向并没有被归一化。如果randomVectorOnUnitSphere()
返回的向量刚好与法线向量相反,则得到的direction
就变成了零向量,从而有可能导致无穷大和NaN的出现。
为此,我们将创建一个新的向量函数vec3::nearZero()
,如果该向量在三个分量上都非常接近0,则返回true
。这个函数需要用到C++的std::fabs
,所以首先在rayTracing.h
中包含它:
1
2
3
4
5
6
// C++ Std Usings
using std::fabs;
using std::make_shared;
using std::shared_ptr;
using std::sqrt;
接下来,我们在vec.h
中定义vec3::nearZero()
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class vec3
{
...
[[nodiscard]]
bool nearZero() const
{
// return true if the vector is close to zero in all dimensions
double 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
22
23
class lambertian final : public material
{
public:
explicit lambertian(const color& albedo) : albedo(albedo) {}
bool scatter(const ray& rayIncoming, const hitInfo& info, color& attenuation, ray& rayScattered)
const override
{
// we use lambertian distribution model to scatter rays
vec3 direction =info.normal + randomVectorOnUnitSphere();
// catch degenerate scatter direction
if (direction.nearZero())
{
direction = info.normal;
}
rayScattered = ray(info.position, direction);
attenuation = albedo;
return true;
}
private:
color albedo;
};
10.4 Mirrored Light Reflection
对于抛光的金属而言,光线并不会被随机地散射。那么对于镜面反射的材质来说,光线是如何被反射的呢?
在上图中,反射方向用红色剪头表示,我们可以轻易地得出结论,反射光线等于v+2b。在我们的程序中,向量n具有单位长度,但入射光线v的长度并不确定。所以,为了计算向量b,我们需要计算出v在n上的投影的长度,也就是两个向量之间的点乘,然后用这个长度对向量n进行缩放。最后,因为向量v指向表面,但我们希望向量b与法线同向,所以还需要反转计算结果。
我们将上述计算过程整理为代码,即:
1
2
3
4
5
6
7
8
9
10
11
...
inline vec3 randomVectorOnHemiSphere(const vec3& normal)
{
...
}
inline vec3 reflect(const vec3& v, const vec3& n)
{
return v - 2 * dot(v, n) * n;
}
现在,我们可以实现金属材质的类了,它与漫反射材质当前的区别只有光线散布方式上的不同:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class lambertian final : public material
{
...
}
class metal final : public material
{
public:
explicit metal(const color& albedo) : albedo(albedo) {}
bool scatter(const ray& rayIncoming, const hitInfo& info, color& attenuation, ray& rayScattered)
const override
{
vec3 direction = reflect(rayIncoming.direction(), info.normal);
rayScattered = ray(info.position, direction);
attenuation = albedo;
return true;
}
private:
color albedo;
};
当我们构建好两个材质的类时,我们就可以修改rayColor
函数了:
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
...
#include "rayTracing.h"
#include "hittable.h"
#include "material.h"
class camera
{
...
private:
...
[[nodiscard]]
static color rayColor(const ray& rayIncoming, int depth, const hittable& world)
{
// if we've exceeded the ray bounce limit, no more light is gathered
if (depth <= 0) { return {0.0, 0.0, 0.0};}
if (hitInfo info; world.hit(rayIncoming, interval(0.001, infinity), info))
{
ray rayScattered;
color attenuation;
if(info.material->scatter(rayIncoming, info, attenuation, rayScattered))
{
return attenuation * rayColor(rayScattered, depth - 1, world);
}
return {0, 0, 0};
}
// Background
...
}
}
此外,由于我们在sphere
类中添加了新的成员变量shared_ptr<material> mat
,我们还需要修改对应的构造函数:
1
2
3
4
5
6
7
8
class sphere final : public hittable
{
public:
sphere(const point3& center, double radius, shared_ptr<material> material):
center(center), radius(fmax(0, radius)), material(material) {}
...
};
10.5 A Scene with Metal Spheres
现在,让我们在测试场景中添加一些金属球体:
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
#include "rayTracing.h"
#include "camera.h"
#include "hittable.h"
#include "hittableList.h"
#include "material.h"
#include "sphere.h"
int main()
{
// World-----------------------------------------------------------
hittableList world;
auto groundMaterial = make_shared<lambertian>(color(0.8, 0.8, 0.0));
auto centerSphereMaterial = make_shared<lambertian>(color(0.1, 0.2, 0.5));
auto leftSphereMaterial = make_shared<metal>(color(0.8, 0.8, 0.8));
auto rightSphereMaterial = make_shared<metal>(color(0.8, 0.6, 0.2));
world.add(make_shared<sphere>(point3(0.0, -100.5, -1.0), 100.0, groundMaterial));
world.add(make_shared<sphere>(point3(0.0, 0.0, -1.2), 0.5, centerSphereMaterial));
world.add(make_shared<sphere>(point3(-1.0, 0.0, -1.0), 0.5, leftSphereMaterial));
world.add(make_shared<sphere>(point3(1.0, 0.0, -1.0), 0.5, rightSphereMaterial));
// Render----------------------------------------------------------
camera cam;
cam.imageWidth = 400;
cam.aspectRatio = 16.0 / 9.0;
cam.samplesPerPixel = 100;
cam.maxDepth = 50;
cam.render(world);
}
得到的渲染结果如下:
酷
10.6 Fuzzy Reflection
为了模拟更真实的反射,我们可以在反射光线的方向上引入受控的随机性来模糊反射。我们可以让反射方向有一点随机的偏移量,也就是在反射光线的终点上添加一个小球体,如下图所示:
fuzz球体越大,则反射效果看起来就会模糊,所以我们可以添加一个参数,作为fuzz球体的半径。但问题在于,对于过大的fuzz球体,或者掠过表面的入射光线,得到的散射可能会位于表面以下,对于这些光线,我们可以视作它们被表面吸收掉了。
此外,由于反射向量的长度是任意的,如果反射向量没有归一化,模糊会受到反射向量的长度的影响,从而导致不一致的模糊效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class metal final : public material
{
public:
explicit metal(const color& albedo, double fuzz) : albedo(albedo), fuzz(fuzz < 1 ? fuzz : 1) {}
bool scatter(const ray& rayIncoming, const hitInfo& info, color& attenuation, ray& rayScattered)
const override
{
vec3 direction = unitVectorLength(reflect(rayIncoming.direction(), info.normal));
direction += fuzz * randomVectorOnUnitSphere();
rayScattered = ray(info.position, direction);
attenuation = albedo;
return dot(direction, info.normal) > 0;
}
private:
color albedo;
double fuzz;
};
最后,给场景中的两个金属球体添加不同的fuzz值:
1
2
3
4
5
6
7
8
int main() {
...
auto groundMaterial = make_shared<lambertian>(color(0.8, 0.8, 0.0));
auto centerSphereMaterial = make_shared<lambertian>(color(0.1, 0.2, 0.5));
auto leftSphereMaterial = make_shared<metal>(color(0.8, 0.8, 0.8), 0.3);
auto rightSphereMaterial = make_shared<metal>(color(0.8, 0.6, 0.2), 1.0);
...
}
渲染中……