简化
首先可以简化一些不必要的常数项,聊胜于无的优化:
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(); 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 -1.0; } else { return (-half_b - sqrt(discriminant) ) / a; } }
|
抽象
现在有了球,或许需要一个球类去描述它,但是未来可能还会有更多的其他种类的物体,最容易想到的方法是首先编写一个物体基类。
这个抽象基类需要抽象出所有物体的共性:
一个物体一定可以被光线感知到,它可以被光线照到,并且光线可以通过这次碰撞获取一些关于物体的信息。即,它需要一个抽象的碰撞函数,就和我们之前在main所在文件中写的球简易碰撞函数那样。这个函数的返回值设计成bool,即返回是否碰撞到。至于其他的碰撞信息,我们可以通过一个结构体返回,其中包括:
- t值。除了判断t值可以确定是否碰撞到物体,t值在之后会有更大的作用,可以说,t值是光线和物体碰撞中最重要的信息之一,应当让外界知道这个值以方便其他的运算。
- 法线。前面的元气弹就是使用了法线实现的效果,但是这还不是全部,法线的作用还有很多,比如镜面反射,比如通过法线去计算反射方向。总之,我们需要返回法线信息。
- 碰撞点坐标。很多情况下都需要用到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) {};
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); 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); rec.normal = (rec.p - center) / radius;
return true; }
#endif
|
第40行开始的if语句非常的巧妙,因为只有在较小的根不满足
的时候,才会检查较大的根,换句话说,如果较小的根在我们的许可范围内,我们会直接采纳他。一般来说,较小的根是光线和物体的首次交汇点,所以优先返回较小根是非常合理的。
那么什么场景才能用到较大的根呢?非常明显,是相机在物体的里面的时候,但这种情况也会导致之前计算的法线方向相反,接下来就来解决它。
法线修正
通过什么方式判断法线是否反了呢?可以注意到的是,法线与入射光线的夹角一定小于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; rec.set_face_normal(r, outward_normal); return true; }
|
参考文献
https://raytracing.github.io/books/RayTracingInOneWeekend.html
参考自《RayTracingInOneWeekend》第6.2节到第6.4节。