本项目上接《用两天学习光线追踪》,继续学习光线追踪。
项目链接:https://github.com/maijiaquan/ray-tracing-with-imgui
目录:
《用两天学习光线追踪》1.项目介绍和ppm图片输出
《用两天学习光线追踪》2.射线、简单相机和背景输出
《用两天学习光线追踪》3.球体和表面法向量
《用两天学习光线追踪》4.封装成类
《用两天学习光线追踪》5.抗锯齿
《用两天学习光线追踪》6.漫反射材质
《用两天学习光线追踪》7.反射向量和金属材质
《用两天学习光线追踪》8.折射向量和电介质
《用两天学习光线追踪》9.可放置相机
《用两天学习光线追踪》10.散焦模糊
《用一周学习光线追踪》1.动态模糊
《用一周学习光线追踪》2.BVH树、AABB相交检测
《用一周学习光线追踪》3.纯色纹理和棋盘纹理
《用一周学习光线追踪》4.柏林噪声
《用一周学习光线追踪》5.球面纹理贴图
《用一周学习光线追踪》6.光照和轴对齐矩形
《用一周学习光线追踪》7.长方体和平移旋转
本节内容
射线和物体的相交检测,是光线追踪器的主要时间瓶颈,目前时间复杂度是线性的。有两种方法可以将时间复杂度降低到对数级别:一是空间划分,二是物体划分。本节将用BVH实现物体划分,对当前的项目进行加速。
层次包围体 BVH
Bounding volume hierarchy (BVH) 层次包围体
摘自BVH维基百科:在BVH中,所有的几何物体都会被包在bounding volume的叶子节点里面,bounding volume外面继续包着一个更大的bounding volume,递归地包裹下去,最终形成的根节点会包裹着整个场景。常用于碰撞检测和光线追踪的加速,能够将时间复杂度降低到关于物体数量的对数级别。
举例:找个volume包围10个物体,如果射线没击中这个volume,那么射线肯定没击中这10个物体。
注意:把物体分成子集,可能会bounding volums重叠的情况。
例如用方框来包围物体,一堆物体被分成红色和蓝色两组,这两组物体又被更大的紫色方框包围。
伪代码如下:
if (hits purple)
hit0 = hits blue enclosed objects
hit1 = hits red enclosed objects
if (hit0 or hit1)
return true and info of closer hit
return false
最常见的bounding volume就是AABB。
轴对齐包围盒 AABB
轴对齐包围盒:axis-aligned bounding box, 缩写为 AABB
可用slab方法检测射线于AABB相交。
下图是一个2D的AABB:
下图是射线在x方向上的相交情况:
p ( t ) = A + t ⋅ B p(t) = A + t \cdot B p(t)=A+t⋅B
x 0 = A x + t 0 ⋅ B x x_0 = A_x + t_0 \cdot B_x x0=Ax+t0⋅Bx
则x轴方向上的t区间为:
t
0
=
x
0
−
A
x
B
x
t_0 = \frac{x_0 - A_x}{B_x}
t0=Bxx0−Ax
t 1 = x 1 − A x B x t_1 = \frac{x_1 - A_x}{B_x} t1=Bxx1−Ax
射线和AABB是否相交,可以转化为不同轴方向上的t区间是否重叠。
如下图绿色区间和蓝色区间。
2D情况写成伪代码:
compute (tx0, tx1)
compute (ty0, ty1)
return overlap?( (tx0, tx1), (ty0, ty1))
3D情况写成伪代码:
compute (tx0, tx1)
compute (ty0, ty1)
compute (tz0, tz1)
return overlap?( (tx0, tx1), (ty0, ty1), (tz0, tz1))
注意2个问题:
1.射线沿着x轴的反方向发射,区间会反过来,例如(7,3)
2.分母可能为0
t x 0 = x 0 − A x B x t_{x0} = \frac{x_0 - A_x}{B_x} tx0=Bxx0−Ax
t x 1 = x 1 − A x B x t_{x1} = \frac{x_1 - A_x}{B_x} tx1=Bxx1−Ax
B x B_x Bx为0的情况, t x 0 t_{x0} tx0 和 t x 1 t_{x1} tx1都会是正无穷或者负无穷, t x 0 t_{x0} tx0 和 t x 1 t_{x1} tx1可以分别取两者之间的较小者和较大者。
t x 0 = m i n ( x 0 − A x B x , x 1 − A x B x ) t_{x0} = min(\frac{x_0 - A_x}{B_x}, \frac{x_1 - A_x}{B_x}) tx0=min(Bxx0−Ax,Bxx1−Ax)
t x 1 = m a x ( x 0 − A x B x , x 1 − A x B x ) t_{x1} = max(\frac{x_0 - A_x}{B_x}, \frac{x_1 - A_x}{B_x}) tx1=max(Bxx0−Ax,Bxx1−Ax)
问题2: B x = 0 B_x = 0 Bx=0 的同时, x 0 − A x = 0 x_0 - A_x = 0 x0−Ax=0 或 x 1 − A x = 0 x_1-A_x= 0 x1−Ax=0,这个问题稍后再处理。
假设没有反转区间,只需处理两对t区间是否重叠,伪代码如下:
bool overlap(d, D, e, E)
f = max(d, e) //f为两者起点的最大值
F = min(D, E) //F为两者终点的最小值
return (f < F) //区间相交条件:f<F
f为两者起点的最大值,F为两者终点的最小值,区间相交条件:f<F。
可参考下图理解:
3D的情况,则需要多判断一次,写成代码如下:
inline float ffmin(float a, float b) { return a < b ? a : b; }
inline float ffmax(float a, float b) { return a > b ? a : b; }
class aabb {
public:
vec3 _min; //左下角顶点
vec3 _max; //右上角顶点
aabb() {}
aabb(const vec3& a, const vec3& b) { _min = a; _max = b;}
vec3 min() const {return _min; }
vec3 max() const {return _max; }
//判断射线是否在t区间
bool hit(const ray &r, float tmin, float tmax) const
{
for (int a = 0; a < 3; a++)
{
float invD = 1.0f / r.direction()[a];
float t0 = (min()[a] - r.origin()[a]) * invD;
float t1 = (max()[a] - r.origin()[a]) * invD;
if (invD < 0.0f)
std::swap(t0, t1);
tmax = t1 < tmax ? t1 : tmax; //F为两者终点的最小值
tmin = t0 > tmin ? t0 : tmin; //f为两者起点的最大值
if (tmax <= tmin) //F <= f
return false;
}
return true;
}
};
此外我们还需要计算2个box的包围盒:
//计算2个box的包围盒
aabb surrounding_box(aabb box0, aabb box1)
{
vec3 small(ffmin(box0.min().x(), box1.min().x()),
ffmin(box0.min().y(), box1.min().y()),
ffmin(box0.min().z(), box1.min().z()));
vec3 big(ffmax(box0.max().x(), box1.max().x()),
ffmax(box0.max().y(), box1.max().y()),
ffmax(box0.max().z(), box1.max().z()));
return aabb(small, big);
}
用bounding_box来设置物体的包围盒,目前场景中的物体只有sphere,但后续会新增其他物体(例如无边界的平面),这些物体不一定都会有包围盒,所以该函数返回bool值。对于动态模糊球体moving_sphere则需要时间间隔参数来模拟相机快门曝光,所以参数里面带上t0和t1。
在基类hittable中定义虚函数bounding_box(),并在子类中分别实现:
class hittable {
public:
...
virtual bool bounding_box(float t0, float t1, aabb& box) const = 0;
};
bool moving_sphere::bounding_box(float t0, float t1, aabb &box) const
{
aabb box0(center(t0) - vec3(radius, radius, radius), center(t0) + vec3(radius, radius, radius));
aabb box1(center(t1) - vec3(radius, radius, radius), center(t1) + vec3(radius, radius, radius));
box = surrounding_box(box0, box1);
return true;
}
bool sphere::bounding_box(float t0, float t1, aabb &box) const
{
box = aabb(center - vec3(radius, radius, radius), center + vec3(radius, radius, radius));
return true;
}
bool hittable_list::bounding_box(float t0, float t1, aabb &box) const
{
if (list_size < 1)
return false;
aabb temp_box;
bool first_true = list[0]->bounding_box(t0, t1, temp_box);
if (!first_true)
return false;
else
box = temp_box;
for (int i = 1; i < list_size; i++)
{
if (list[i]->bounding_box(t0, t1, temp_box))
{
box = surrounding_box(box, temp_box);
}
else
return false;
}
return true;
}
BVH树的实现
下面开始实现BVH树,定义BVH树的节点为bvh_node类,继承自hittable。目前hittable子类的继承关系如下:
每个bvh_node都有一个AABB和左右孩子指针,左右孩子同样指向hittable的指针,可能指向bvh_node,也可能指向sphere或其他hittable子类。
hit()函数非常简单粗暴:检查当前节点的box是否命中,若是,则检查该节点的左右孩子(可能是bvh_node或sphere)是否命中。
对于所有的加速结构(包括BVH),最复杂的部分就是创建。使用自顶向下创建BVH树的方式如下:
1.随机选择x,y,z轴中的一个
2.对该轴正方向的顺序对物体排序,并分成前后两半
3.前一半物体放左子树,后一半物体放右子树,继续对左右子树递归创建。如果左右子树的物体数量等于2,直接左右节点分别指向物体。如果等于1,则左右节点均指向该物体。最后将当前节点的aabb设置为包含左右子树的box。
class bvh_node : public hittable
{
public:
bvh_node() {}
hittable *left;
hittable *right;
aabb box;
bool bounding_box(float t0, float t1, aabb &b) const
{
b = box;
return true;
}
bool hit(const ray &r, float t_min, float t_max, hit_record &rec) const
{
if (box.hit(r, t_min, t_max))
{
hit_record left_rec, right_rec;
bool hit_left = left->hit(r, t_min, t_max, left_rec);
bool hit_right = right->hit(r, t_min, t_max, right_rec);
if (hit_left && hit_right)
{
if (left_rec.t < right_rec.t)
rec = left_rec;
else
rec = right_rec;
return true;
}
else if (hit_left)
{
rec = left_rec;
return true;
}
else if (hit_right)
{
rec = right_rec;
return true;
}
else
return false;
}
else
return false;
}
bvh_node(hittable **l, int n, float time0, float time1)
{
int axis = int(3 * random_double());
if (axis == 0)
qsort(l, n, sizeof(hittable *), box_x_compare);
else if (axis == 1)
qsort(l, n, sizeof(hittable *), box_y_compare);
else
qsort(l, n, sizeof(hittable *), box_z_compare);
if (n == 1)
{
left = right = l[0];
}
else if (n == 2)
{
left = l[0];
right = l[1];
}
else
{
left = new bvh_node(l, n / 2, time0, time1);
right = new bvh_node(l + n / 2, n - n / 2, time0, time1);
}
aabb box_left, box_right;
if (!left->bounding_box(time0, time1, box_left) ||
!right->bounding_box(time0, time1, box_right))
{
std::cerr << "no bounding box in bvh_node constructor\n";
}
box = surrounding_box(box_left, box_right);
}
};
x轴上的qsort()
比较函数如下:
int box_x_compare (const void * a, const void * b) {
aabb box_left, box_right;
hittable *ah = *(hittable**)a;
hittable *bh = *(hittable**)b;
if (!ah->bounding_box(0,0, box_left) || !bh->bounding_box(0,0, box_right))
std::cerr << "no bounding box in bvh_node constructor\n";
if (box_left.min().x() - box_right.min().x() < 0.0)
return -1;
else
return 1;
}
同理可实现y轴和z轴的比较函数。
修改random_scene的返回值:
hittable *random_scene() {
...
// return new hittable_list(list,i);
return new bvh_node(list, i, 0.0, 1.0);
}
测试结果
43s,比上一节的92s快了接近一倍。