文章

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,我们需要计算出vn上的投影的长度,也就是两个向量之间的点乘,然后用这个长度对向量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); 
    ...
}

渲染中……

本文由作者按照 CC BY 4.0 进行授权