本文目录
光线追踪的整体框架更多地介绍在上篇文章,本次作业重点在于实现BVH
算法,该算法在整个光线追踪框架中的作用如上篇文章中3.1小节中描述。对于包含多物体的复杂场景,快速找到光线与物体的交点是十分重要的。
闫老师的课上花了大半节课介绍了一些历史中存在的算法,什么KD-Tree
等等,但是这些都用的很少了,主要是开阔眼界,引导思考吧,所以最后直接关注课上重点讲的BVH
就好了。
1 BVH算法思路简述
1.1 AABB包围盒
对于本次作业的答案,明显大部分的光线都不会与兔子有交点,对于不会相交的光线,如果也要与兔子上所有的三角面进行求交判断,运算量=像素量*三角面数量。但是如果先把兔子框起来,判断是否与框有交点,在进而判断是否有三角面有交点,则能明显把运算量降下来。
我们把这种三个面分别平行于xy面、xz面、yz面的包围盒称为Axis-Aligned Bounding Box (AABB)轴对⻬包围盒
。
1.2 与包围盒相交的判定条件
首先计算光线和包围盒的公式非常简单,如上面PPT所示。分别求出光线在x、y、z
三个方向的进出入时间,最后求得进出入包围盒的时间,
则光线和包围盒有交点的判据为:
①t_enter <= t_exit
②t_exit >=0
!!注意
其中第一个判据,一定要是<=
,因为如果物体恰好是一个平面形状,平行于某个面,则这个物体的包围盒也会是一个平面,光线出入的时间都会是0
1.3 算法思路简述
总结如下:
- 寻找物体的包围盒
- 判断光线是否与包围盒相交
- 如果相交,则对最先相交的物体进一步划分,继续判断与划分的两个包围盒哪个相交
- 重复上一过程,直到包围盒只有一个三角面,是二叉树上的叶子节点
- 计算光线与这个叶子节点里三角面的交点,并返回
2 BVH实现过程
场景包含多个物体,可以以物体为最小单元进行一次BVH
划分和光线碰撞检测;
物体包含多个三角面,可以以三角面为最小单元进行一次BVH
划分和光线碰撞检测。
如果有更多的层级,可以每一个层级都应用一次。
所以对BVH进行抽象,单独做成一个类,哪个层级需要调用,把
BVH
类当成这个层级类的一个属性即可。
2.1 BVH类 — 划分过程
这是一个二叉树的划分过程,首先定义树节点的结构体。
struct BVHBuildNode
{
包围盒 bound;
左子节点 left;
右子节点 right;
包含的objects;
}
然后就是树的构建过程,直接伪代码表示如下:
BVHBuildNode* BVHAccel::recursiveBuild(std::vector<Object*> objects)
{
BVHBuildNode* node = new BVHBuildNode();//用来返回的root节点
获取objects的bound;
if(objects.size == 1)
{//初始化这个根节点
node.objects = objects;
node.left = node.right = bullptr;
node.bound = bound;
return node;
}
else if (objects.size == 2){
node.left = recursiveBuild(objects[0]);
node.right = recursiveBuild(objects[1]);
.....//初始化这个节点
return node;
}
else{
获取bound上最长的维度dim;
把objects里面的元素按照dim从小到大进行排序;
把objects按照排序分成两部分objects_left , objects_right;
node.left = recursiveBuild(objects_left);
node.right = recursiveBuild(objects_right);
.....//初始化这个节点
return node;
}
这里面划分的关键在于怎么对objects进行划分,作业框架里提供的思路是从维度最长的那个尺度进行一分为二,每次从一个维度上切一刀。
2.2 BVH类 — 碰撞检测
BVH
算法除了建立一个二叉树,应用的时候其实是一个查找的过程。所以还需要给这个类添加一个检查光线与二叉树的碰撞函数。因为BVH
的划分是一个递归过程,其实在查找中也同样是一个递归过程。
伪代码如下:
Intersection BVHAccel::getIntersection(BVHBuildNode* node, const Ray& ray) const
{
//如果光线与node的bound不相交,则返回
if(node->bound->Intersection(ray相信输入) == false)
return ;
//如果是子节点,直接计算子节点里面的碰撞检测
if(node.left == nullptr && node.right == nullptr)
return node_>object->getIntersection(ray);
//如果是父节点,就递归检查
Intersection hit1 = getIntersection(node.left, ray);
Intersection hit2 = getIntersection(node.right,ray);
return (hit1.distance < hit2.distance)?hit1:hit2;
}
3 BVH的具体代码应用
光线查找二叉树的过程其实就是先判断光线与bound
的碰撞检测,如果碰撞,在继续检测子节点的bound
的相交,持续过程……一直到跟三角面的相交。所以这个过程其实是分别调用bound
类的碰撞检测函数和objects
的光线相交函数。
scene
这个层级会建立一个它bvh,然后查找过程也会在他这个层级进行查到,如果到了子节点,进行下一个层级,重复该过程,直到找到三角面。
所以这个光线求交函数,在triangle类
和其他类中是不一样的。
4 作业代码及结果
4.1 代码
Bounds3.hpp:
inline bool Bounds3::IntersectP(const Ray& ray, const Vector3f& invDir,
const std::array<int, 3>& dirIsNeg) const
{
// invDir: ray direction(x,y,z), invDir=(1.0/x,1.0/y,1.0/z), use this because Multiply is faster that Division
// dirIsNeg: ray direction(x,y,z), dirIsNeg=[int(x>0),int(y>0),int(z>0)], use this to simplify your logic
// TODO test if ray bound intersects
float tmin_x = (pMin.x - ray.origin.x) * invDir.x;
float tmin_y = (pMin.y - ray.origin.y) * invDir.y;
float tmin_z = (pMin.z - ray.origin.z) * invDir.z;
float tmax_x = (pMax.x - ray.origin.x) * invDir.x;
float tmax_y = (pMax.y - ray.origin.y) * invDir.y;
float tmax_z = (pMax.z - ray.origin.z) * invDir.z;
if (dirIsNeg[0] <= 0) std::swap(tmin_x, tmax_x);
if (dirIsNeg[1] <= 0) std::swap(tmin_y, tmax_y);
if (dirIsNeg[2] <= 0) std::swap(tmin_z, tmax_z);
float tmin = std::max(tmin_x, std::max(tmin_y, tmin_z));
float tmax = std::min(tmax_x, std::min(tmax_y, tmax_z));
//这里是个大坑!必须改为<=,因为有很多bound可能是z维度是0,此时tmin=tmax
if (tmin <= tmax && tmax>0)
return true;
else
return false;
}
注意上面的判断条件一定要是
<=
!!!
BVH.cpp:
Intersection BVHAccel::getIntersection(BVHBuildNode* node, const Ray& ray) const
{
// TODO Traverse the BVH to find intersection
Intersection inter;
std::array<int, 3> dirIsNeg = { int(ray.direction.x > 0),int(ray.direction.y > 0), int(ray.direction.z > 0) };
//如果光线跟bound不相交,终止
if (node->bounds.IntersectP(ray, ray.direction_inv, dirIsNeg) == false) {
return inter;
}
//如果相交,并且是叶子节点,则调用triangle的相交函数
if (node->left == nullptr && node->right == nullptr) {
return node->object->getIntersection(ray);
}
//如果是父节点,则递归比较
Intersection hit1;
Intersection hit2;
hit1 = getIntersection(node->left, ray);
hit2 = getIntersection(node->right, ray);
return (hit1.distance < hit2.distance) ? hit1 : hit2;
}
不同类及其函数的相互调用关系整理如上一小节图所示。没有那张图总感觉理不清这些类之间的关系。
4.2 作业结果
作业结果如图所示:
作业06中其实只有一个物体,BVH的完整应用过程在作业07中更明显。