0%

五:物体类

简化

首先可以简化一些不必要的常数项,聊胜于无的优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
double hit_sphere(const point3& center, double radius, const ray& r) {
vec3 oc = r.origin() - center;
auto a = r.direction().length_squared();
//默认直接把2除掉
auto half_b = dot(oc, r.direction());
auto c = oc.length_squared() - radius*radius;
//这个discriminant是之前的四分之一。
auto discriminant = half_b*half_b - a*c;

if (discriminant < 0) {
return -1.0;
} else {
//分子分母都是简化前的一半。
return (-half_b - sqrt(discriminant) ) / a;
}
}

抽象

现在有了球,或许需要一个球类去描述它,但是未来可能还会有更多的其他种类的物体,最容易想到的方法是首先编写一个物体基类。

这个抽象基类需要抽象出所有物体的共性:

一个物体一定可以被光线感知到,它可以被光线照到,并且光线可以通过这次碰撞获取一些关于物体的信息。即,它需要一个抽象的碰撞函数,就和我们之前在main所在文件中写的球简易碰撞函数那样。这个函数的返回值设计成bool,即返回是否碰撞到。至于其他的碰撞信息,我们可以通过一个结构体返回,其中包括:

  1. t值。除了判断t值可以确定是否碰撞到物体,t值在之后会有更大的作用,可以说,t值是光线和物体碰撞中最重要的信息之一,应当让外界知道这个值以方便其他的运算。
  2. 法线。前面的元气弹就是使用了法线实现的效果,但是这还不是全部,法线的作用还有很多,比如镜面反射,比如通过法线去计算反射方向。总之,我们需要返回法线信息。
  3. 碰撞点坐标。很多情况下都需要用到p点坐标,我们不希望每次用到的时候都去计算一遍,而且我们理应把这些计算放在更底层的地方,而且我们也不应该把这种底层计算放在用户看得到的地方。

创建hittable.h文件,写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef HITTABLE_H
#define HITTABLE_H

#include "ray.h"

//我们把需要返回的数据封装成结构体,按照上面分析的,暂时我们需要这三个东西。
struct hit_record {
point3 p;
vec3 normal;
double t;
};

//可碰撞物体类。
class hittable {
public:
//需要这样一个纯虚函数,所有继承自这个类的子类(如球),都需要实现这个函数。
virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const = 0;
};

#endif

在类中多了两个参数,分别为允许t的最小、最大值,有了它我们就可以剔除t小于0的情况(即物体在相机后后面的情况),同时它还有更多的用处。

实现

基于这个物体类来实现球类的代码,创建sphere.h文件,写入:

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
54
55
56
57
#ifndef SPHERE_H
#define SPHERE_H

#include "hittable.h"
#include "vec3.h"

class sphere : public hittable {
public:

//构造
sphere() {}
sphere(point3 cen, double r) : center(cen), radius(r) {};

//声明要override纯虚函数
virtual bool hit(
const ray& r, double t_min, double t_max, hit_record& rec) const override;

public:
point3 center;
double radius;
};

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) return false;

auto sqrtd = sqrt(discriminant);

//因为我们引入了tmax和tmin,所以这里还需要格外的运算。

//检查较小的根
auto root = (-half_b - sqrtd) / a;
if (root < t_min || t_max < root) {
//检查较大的根
root = (-half_b + sqrtd) / a;
if (root < t_min || t_max < root)
return false;
}

//封入结构体
rec.t = root;
rec.p = r.at(rec.t);
//不用麻烦使用unit_vector函数,直接利用已经存好的半径进行单位化,实现加速。
rec.normal = (rec.p - center) / radius;

return true;
}

#endif

第40行开始的if语句非常的巧妙,因为只有在较小的根不满足img的时候,才会检查较大的根,换句话说,如果较小的根在我们的许可范围内,我们会直接采纳他。一般来说,较小的根是光线和物体的首次交汇点,所以优先返回较小根是非常合理的。

那么什么场景才能用到较大的根呢?非常明显,是相机在物体的里面的时候,但这种情况也会导致之前计算的法线方向相反,接下来就来解决它。

法线修正

通过什么方式判断法线是否反了呢?可以注意到的是,法线与入射光线的夹角一定小于90度的,所以可以使用向量的点乘来进行判断,并且这部分判断可以直接放在hit_record内,同时在碰撞函数中调用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct hit_record {
point3 p;
vec3 normal;
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;
}
};

sphere.h中修改hit函数:

1
2
3
4
5
6
7
8
9
10
11
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;

//用这一个函数设置hit_record中的法线和front_face
rec.set_face_normal(r, outward_normal);
return true;
}

参考文献

https://raytracing.github.io/books/RayTracingInOneWeekend.html

参考自《RayTracingInOneWeekend》第6.2节到第6.4节。