0%

【RayTracer】(二十二)最后一步

到此为止关于漫反射材质的全部内容就都完成了,现在只剩最后一步——用我们新的实现方式修改金属和电介质材质,使得新的光线追踪器支持镜面反射和折射。

1 统一管理散射光线

对于镜面反射和折射,如果用新的渲染方程会出现 pdf 值为 0 的情况,因此我们使用之前隐式的渲染方程,也就是采样 pdf 和光线散射 pdf 一致。为此我们首先需要新增一个结构体来统一管理散射光线,然后根据散射光线的类型选择对应的渲染方程:

1
2
3
4
5
6
7
// 统一管理散射光线
struct scatter_record {
ray specular_ray; // 散射光线
bool is_specular; // 是否是镜面反射,金属或者电介质为true
color attenuation; // 反射率
shared_ptr<pdf> pdf_ptr; // 散射光线pdf,如果是金属或者电介质就是空指针
};

然后修改材质抽象类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class material {
public:
// 散射函数
virtual bool scatter(
ray& r_in, const hit_record& rec, scatter_record& srec
) const {
return false;
};
// 计算材质散射光线的pdf
virtual double scattering_pdf(
const ray& r_in, const hit_record& rec, const ray& scattered
) const {
return 0;
}
// 自发光,可选
virtual color emitted(
const ray& r_in, const hit_record& rec, double u, double v, const point3& p) const {
return color(0, 0, 0);
}
};

接下来用新的结构体和方法改写之前实现的 Lambertian 材质:

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
// lambertian材质类,在单位球面上采样得到散射方向
class lambertian : public material {
public:
lambertian(const color& a) : albedo(make_shared<solid_color>(a)) {}
lambertian(shared_ptr<texture> a) : albedo(a) {}

// 采样散射光线
virtual bool scatter(
ray& r_in, const hit_record& rec, scatter_record& srec
) const override {
srec.is_specular = false;
srec.attenuation = albedo->value(rec.u, rec.v, rec.p);
// 采样光线的概率密度
srec.pdf_ptr = make_shared<cosine_pdf>(rec.normal);
return true;
}

// 材质本身散射光线的概率密度
double scattering_pdf(
const ray& r_in, const hit_record& rec, const ray& scattered
) const {
auto cosine = dot(rec.normal, normalize(scattered.direction()));
return cosine < 0 ? 0 : cosine / pi;
}

public:
shared_ptr<texture> albedo; //反射率
};

接下来修改主函数:

1
2
3
4
5
6
7
8
/*******创建场景*******/
hittable_list world;
auto lights = make_shared<hittable_list>();
lights->add(make_shared<xz_rect>(213, 343, 227, 332, 554, shared_ptr<material>()));
// 对透光的玻璃也进行额外采样
lights->add(make_shared<sphere>(
point3(190, 90, 190), point3(190, 90, 190), 0, 1, 90, shared_ptr<material>()));
...

2 金属和电介质

有了统一管理散射光线的方法,我们可以修改之前的金属材质:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 金属材质类
class metal : public material {
public:
metal(const color& a, const double f) : albedo(a), fuzz(f < 1 ? f : 1) {}

virtual bool scatter(
ray& r_in, const hit_record& rec, scatter_record& srec
) const override {
vec3 reflected = reflect(normalize(r_in.direction()), rec.normal);
srec.specular_ray = ray(rec.p, reflected + fuzz * random_in_unit_sphere());
srec.attenuation = albedo;
srec.is_specular = true;
srec.pdf_ptr = nullptr;
return true;
}

public:
color albedo; //反射率
double fuzz; //Glossy反射扰动系数
};

同时修改电介质材质类:

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
// 电介质材质类
class dielectric : public material {
public:
dielectric(double index_of_refraction) : ir(index_of_refraction) {}

virtual bool scatter(
ray& r_in, const hit_record& rec, scatter_record& srec
) const override {
srec.is_specular = true;
srec.pdf_ptr = nullptr;
// 电介质不吸收任何光,全部被折射或者反射,所以衰减系数恒为1
srec.attenuation = color(1.0, 1.0, 1.0);
// 如果是正面,则是从空气进入介质,反之从介质折射出去
double refraction_ratio = rec.front_face ? (1.0 / ir) : ir;
// 入射光线记得单位化,折射函数传入的参数都是单位向量
vec3 unit_direction = normalize(r_in.direction());
// 计算是否发生折射
double cos_theta = fmin(dot(-unit_direction, rec.normal), 1.0);
double sin_theta = sqrt(1.0 - cos_theta * cos_theta);

bool cannot_refract = refraction_ratio * sin_theta > 1.0;
vec3 direction;

if (cannot_refract || reflectance(cos_theta, refraction_ratio) > random_double())
direction = reflect(unit_direction, rec.normal);
else
direction = refract(unit_direction, rec.normal, refraction_ratio);

srec.specular_ray = ray(rec.p, direction, r_in.time());
return true;
}

public:
double ir; //介质折射率

private:
static double reflectance(double cosine, double ref_idx) {
// 使用Schlick's近似计算菲涅尔项
auto r0 = (1 - ref_idx) / (1 + ref_idx);
r0 = r0 * r0;
return r0 + (1 - r0) * pow((1 - cosine), 5);
}
};

然后修改 ray_color 函数:

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
49
50
51
52
53
#pragma once

#include "hittable.h"
#include "pdf.h"

// 得到光线颜色
color ray_color(
ray& r,
const color& background,
const hittable& world,
shared_ptr<hittable>& lights,
int depth,
double RR)
{
hit_record rec;

// 光线弹射指定次数后开始用RR算法终止递归
if (depth < 0 && random_double() >= RR) return color(0, 0, 0);

// 如果光线没有打到任何物体,返回背景颜色
// 这里的t的下界设为0.001是为了防止一些光线弹射到物体上得到的t非常接近0,比如可能出现0.000001这样的值
if (!world.hit(r, 0.001, infinity, rec))
return background;

// 根据物体材质得到光线传播方向、反射率及自发光颜色
scatter_record srec;
color emitted = rec.mat_ptr->emitted(r, rec, rec.u, rec.v, rec.p);

// 对于光源,不会发生散射,返回光源颜色
if (!rec.mat_ptr->scatter(r, rec, srec))
return emitted;

// 如果是高光反射或折射,采用之前的渲染方程,隐式的使采样pdf和散射pdf保持一致
if (srec.is_specular) {
return srec.attenuation
* ray_color(srec.specular_ray, background, world, lights, depth - 1, RR) / RR;
}

// 对光源采样的pdf
auto light_ptr = make_shared<hittable_pdf>(lights, rec.p);
// 混合pdf
mixture_pdf p(light_ptr, srec.pdf_ptr);

// 采样光线
ray scattered = ray(rec.p, p.generate(), r.time());
// 采样光线的pdf值
auto pdf_val = p.value(scattered.direction());

// 渲染方程
return emitted
+ srec.attenuation * rec.mat_ptr->scattering_pdf(r, rec, scattered)
* ray_color(scattered, background, world, lights, depth - 1, RR) / pdf_val / RR;
}

然后修改场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Cornell Box场景
hittable_list cornell_box() {
hittable_list objects;

...

shared_ptr<material> aluminum = make_shared<metal>(color(0.8, 0.85, 0.88), 0.0);
shared_ptr<hittable> box1 = make_shared<box>(point3(0, 0, 0), point3(165, 330, 165), aluminum);
box1 = make_shared<rotate_y>(box1, 15);
box1 = make_shared<translate>(box1, vec3(265, 0, 295));
objects.add(box1);

auto glass = make_shared<dielectric>(1.5);
objects.add(make_shared<sphere>(
point3(190, 90, 190), point3(190, 90, 190), 0, 1, 90, glass));

return objects;
}

3 对球体和物体列表采样

上面的主函数中我们对透明玻璃球也进行了额外采样,因此类似于之前光源所在的 zx 平面,现在我们需要实现球体和物体列表的 pdf_value 函数和 random 函数,对球体采样的具体推导过程可以查看《RayTracingTheRestOfYourLife》第 12.3 节,这里直接给出代码,首先修改球体类:

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:
...

double pdf_value(const point3& o, const vec3& v) const {
hit_record rec;
if (!this->hit(ray(o, v), 0.001, infinity, rec))
return 0;

auto cos_theta_max = sqrt(1 - radius * radius / (center0 - o).length_squared());
auto solid_angle = 2 * pi * (1 - cos_theta_max);

return 1 / solid_angle;
}

vec3 random(const point3& o) const {
vec3 direction = center0 - o;
auto distance_squared = direction.length_squared();
onb uvw;
uvw.build_from_w(direction);
return uvw.local(random_to_sphere(radius, distance_squared));
}

...
};

工具函数新增:

1
2
3
4
5
6
7
8
9
10
11
12
// 在球体外对球体随机采样
inline vec3 random_to_sphere(double radius, double distance_squared) {
auto r1 = random_double();
auto r2 = random_double();
auto z = 1 + r2 * (sqrt(1 - radius * radius / distance_squared) - 1);

auto phi = 2 * pi * r1;
auto x = cos(phi) * sqrt(1 - z * z);
auto y = sin(phi) * sqrt(1 - z * z);

return vec3(x, y, z);
}

然后是物体列表 hittable_list

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class hittable_list : public hittable {
public:
...

double hittable_list::pdf_value(const point3& o, const vec3& v) const {
auto weight = 1.0 / objects.size();
auto sum = 0.0;

for (const auto& object : objects)
sum += weight * object->pdf_value(o, v);

return sum;
}

vec3 hittable_list::random(const vec3& o) const {
auto int_size = static_cast<int>(objects.size());
return objects[random_int(0, int_size - 1)]->random(o);
}
...
};

4 最后一步

最后一步我们来处理掉之前图片中有时会出现的黑点或者异常像素,这是因为一些不好的采样计算出了很大的或者 NaN 的颜色,使得整个像素受损,因此我们可以在写颜色的时候处理这种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 向数组中写入一个颜色,用到了指针的引用传递
// 输入的color是[0,1]范围的
inline void write_color(unsigned char*& p, color pixel_color, int samples_per_pixel)
{
auto r = pixel_color.x();
auto g = pixel_color.y();
auto b = pixel_color.z();

// 处理异常像素值
if (r != r) r = 0.0;
if (g != g) g = 0.0;
if (b != b) b = 0.0;

auto scale = 1.0 / samples_per_pixel;

// 伽马校正,假设显示gamma=2.0
r = sqrt(scale * r);
g = sqrt(scale * g);
b = sqrt(scale * b);

*p++ = (unsigned char)(256 * clamp(r, 0.0, 0.999));
*p++ = (unsigned char)(256 * clamp(g, 0.0, 0.999));
*p++ = (unsigned char)(256 * clamp(b, 0.0, 0.999));
}

最后看一下效果:

CornellBoxConv

完结撒花!

---- 本文结束 知识又增加了亿点点!----

文章版权声明 1、博客名称:LycTechStack
2、博客网址:https://lz328.github.io/LycTechStack.github.io/
3、本博客的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系博主进行删除处理。
4、本博客所有文章版权归博主所有,如需转载请标明出处。