Games101:作业6
说明
本次作业主要实现对上一次作业代码的重构以及使用BVH加速求交的交点判断和递归调用
代码框架的修改
有影响的改动就是框架中定义了两个结构体一个是光线ray,一个是交点Intersection
交点结构体
主要为相交交点定义了六个变量,
struct Intersection
{
Intersection(){
happened=false;
coords=Vector3f();
normal=Vector3f();
//the distance between ori and intersection
distance= std::numeric_limits<double>::max();
obj =nullptr;
m=nullptr;
}
//是否相交
bool happened;
//交点坐标
Vector3f coords;
//交点法线
Vector3f normal;
//交点距离摄像机的距离
double distance;
//交点的物体类型
Object* obj;
//交点的材质
Material* m;
};
光线结构体
主要为发射光线定义了3个变量
//发射起点--摄像机
Vector3f origin;
//发射方向,和发射方向各分量的倒数
Vector3f direction, direction_inv;
//传播时间 res = o+td
double t;//transportation time,
double t_min, t_max;
重构求发射光线的代码
同样是利用castRay函数计算framebuff,而这次代码中
Vector3f Scene::castRay(const Ray &ray, int depth) const
需要传入ray和depth,深度可以默认初始化为0。ray则通过x,y来求解,通过传入发射起点和发射方向即可
Vector3f dir = Vector3f(x,y,-1);
dir = normalize(dir);
Ray r(eye_pos,dir);
framebuffer[m++] = scene.castRay(r,0);
重构求三角形与发射光线交点的代码
框架中已经给出了t_tmp,u,v并且已经判断了不存在的情况,需要做的工作就是利用参数为Intersection变量赋值
//if inter true
// TODO find ray triangle intersection
inter.happened = true;
//o+td -> o : ||td||
double distance = dotProduct(t_tmp*ray.direction,t_tmp*ray.direction);
inter.distance = distance;
Vector3f coords = (1-u-v)*v0 + u*v1 + v*v2; // or ray.origin+t_tmp*ray.direction;
inter.coords = coords;
inter.normal = this->normal;
inter.m = this->m;
inter.obj = this;
求解发射光线是否与AABB有交点
课上有说如果发射光线与包围盒有交点,那么说明tmin<tmax && tmax >= 0,所以需要通过发射光线与平面的交点来判断
对于平行于坐标轴的包围平面
而我们的pMin和pMax表示三个坐标方向的最大最小值。
Vector3f tmin_v = (pMin-ray.origin)*invDir;
Vector3f tmax_v = (pMax-ray.origin)*invDir;
还需要注意的是,如果发射方向在x,y,z某个值为负,说明这个方向的分量是随着时间减小的,那么就需要调换min 和 max在这个方向上的分量值
//if x,y,z someone < 0 means the ray dir is neg in this axis, and max min need swap
if(dirIsNeg[0] == 0)
{
//swap x
std::swap(tmin_v.x,tmax_v.x);
}
if(dirIsNeg[1] == 0)
{
//swap y
std::swap(tmin_v.y,tmax_v.y);
}
if(dirIsNeg[2] == 0)
{
//swap z
std::swap(tmin_v.z,tmax_v.z);
}
最后就是比较这三个对应的分量,求tmin的三者最大值,tmax的三者最小值。
//tmin is max (minx,miny,minz)
double tmin = std::max(tmin_v.x,std::max(tmin_v.y,tmin_v.z));
//tmax is min (maxx,maxy,maxz)
double tmax = std::min(tmax_v.x,std::min(tmax_v.y,tmax_v.z));
if(tmin < tmax && tmax >= 0) return true;
else return false;
BVH树结构加速求交
BVH是一个二叉树结构,而只在其叶子节点中存储了可能有交点的物体信息,非叶子节点存储的都是下一个左右子节点。
框架代码中传入了BVH的头节点,和发射光线,需要返回一个交点
Intersection BVHAccel::getIntersection(BVHBuildNode* node, const Ray& ray) const
BVHBuildNode节点的结构为
struct BVHBuildNode {
//AABB
Bounds3 bounds;
//划分的左节点
BVHBuildNode *left;
//划分的右节点
BVHBuildNode *right;
//包围盒中的物体
Object* object;
public:
int splitAxis=0, firstPrimOffset=0, nPrimitives=0;
// BVHBuildNode Public Methods
BVHBuildNode(){
bounds = Bounds3();
left = nullptr;right = nullptr;
object = nullptr;
}
};
这个是一个递归函数
- 考虑终止条件
当发射光线与AABB没有交点是即tmin tmax不满足时,直接返回intersection的默认值
当该包围盒中有物体时,表明该节点是叶子节点,返回光线与该物体的交点 - 递归
对左节点右节点递归 - 单层逻辑
对于求得的左右节点的两个交点,应当返回的是两个中距离观测点更近的一点(z-buffer)
Intersection inters;
std::array<int, 3> dirIsNeg = {int(1.0/ray.direction.x>0),int(1.0/ray.direction.y>0),int(1.0/ray.direction.z>0)};
//no intersection with AABB
if(!node->bounds.IntersectP(ray,ray.direction_inv,dirIsNeg))
{
return inters;
}
//calculate intersection with triangle
if(node->object)
{
return node->object->getIntersection(ray);
}
Intersection left_inter = getIntersection(node->left,ray);
Intersection right_inter = getIntersection(node->right,ray);
//the lower distance return
return left_inter.distance < right_inter.distance ? left_inter : right_inter;
SAH
框架中的BVH的构建方式
Bounds3 centroidBounds;
for (int i = 0; i < objects.size(); ++i)
//Centroid() 获取包围盒的中间位置
centroidBounds =
Union(centroidBounds, objects[i]->getBounds().Centroid());
//maxExtent() x,y,z哪个方向比较长,选择该方向进行分割
int dim = centroidBounds.maxExtent();
switch (dim) {
case 0:
std::sort(objects.begin(), objects.end(), [](auto f1, auto f2) {
return f1->getBounds().Centroid().x <
f2->getBounds().Centroid().x;
});
break;
case 1:
std::sort(objects.begin(), objects.end(), [](auto f1, auto f2) {
return f1->getBounds().Centroid().y <
f2->getBounds().Centroid().y;
});
break;
case 2:
std::sort(objects.begin(), objects.end(), [](auto f1, auto f2) {
return f1->getBounds().Centroid().z <
f2->getBounds().Centroid().z;
});
break;
}
//为左右节点赋值,选择物体的一半进行等量划分
auto beginning = objects.begin();
auto middling = objects.begin() + (objects.size() / 2);
auto ending = objects.end();
auto leftshapes = std::vector<Object*>(beginning, middling);
auto rightshapes = std::vector<Object*>(middling, ending);
assert(objects.size() == (leftshapes.size() + rightshapes.size()));
node->left = recursiveBuild(leftshapes);
node->right = recursiveBuild(rightshapes);
node->bounds = Union(node->left->bounds, node->right->bounds);
SAH划分评估方式
目的是利用SAH对时间复杂度的优化,考虑不进行划分的情况,那么需要对每个物体进行求交
而SAH利用概率的方法加速了这一求解过程,一个父节点被分成了左右两个子节点,而这两个子节点表示的包围盒有一定的面积,如果发射光线与父节点的包围盒有交点,那么发射光线能够与这两个子节点产生交点的概率就是面积之比,那么对于当前的时间复杂度就为一个期望值
SAH评估办法将使得可以选择一个较优划分方式来实现时间复杂度的最低。
初步做法就是,对每种划分方式利用SAH计算时间消耗,然后选择时间消耗最少的那种方式进行划分
还有一种优化方式就是对于当前的“最长轴”,均分成B份,然后可以选择不同的分割线,在这个轴上表现为划分为2份,计算这种选取方式的时间消耗,最后选择消耗最小的一种划分方式
每次选择一条虚线,计算以该条虚线分割的时间消耗,最后选择消耗最小的虚线构建左右子节点。
假设第5条虚线的分割导致时间消耗最小,那么b0,b1,b2,b3中的物体可以构成求解左节点的数组,剩余区域中的物体构成求解右节点的数组
BVHBuildNode* BVHAccel::recursiveSAHBuild(std::vector<Object*> objects)
{
BVHBuildNode* node = new BVHBuildNode();
// Compute bounds of all primitives in BVH node
Bounds3 bounds;
for (int i = 0; i < objects.size(); ++i)
bounds = Union(bounds, objects[i]->getBounds());
if (objects.size() == 1) {
// Create leaf _BVHBuildNode_
node->bounds = objects[0]->getBounds();
node->object = objects[0];
node->left = nullptr;
node->right = nullptr;
return node;
}
else if (objects.size() == 2) {
node->left = recursiveSAHBuild(std::vector{objects[0]});
node->right = recursiveSAHBuild(std::vector{objects[1]});
node->bounds = Union(node->left->bounds, node->right->bounds);
return node;
}
//前面的1个节点和2个节点的逻辑适合BVH一样的
//假定划分了12个桶,然后ctrav = 0.125f,cisect = 1.0f;
else {
//if(objects.size() < 12) return recursiveBuild(objects);
Bounds3 centroidBounds;
//claim vector : cost and bounds
//save the cost choose different way
std::vector<double> cost(11);
//save the buckets we make Buckets是一个结构体 包含了 该划分区域的物体个数,该区域的边界以及物体
std::vector<Buckets> buckets(12);
for (int i = 0; i < objects.size(); ++i)
centroidBounds =
Union(centroidBounds, objects[i]->getBounds().Centroid());
int dim = centroidBounds.maxExtent();
//check the axis which use
//and we need calculate the obj belong which bucket
//we division pMin->pMax to 20 buckets, so we can calculate the obj's p where use
//p/(pMax - pMin) the percent in 20 which bucket = 20 * p/(pMax-pMin)
int b;
for(int i=0;i<objects.size();i++)
{
//which bucket
//choose which axis
switch(dim)
{
//x
case 0: {
b = 12*centroidBounds.Offset(objects[i]->getBounds().Centroid()).x;
break;
}
//y
case 1: {
b = 12*centroidBounds.Offset(objects[i]->getBounds().Centroid()).y;
break;
}
//z
case 2: {
b = 12*centroidBounds.Offset(objects[i]->getBounds().Centroid()).z;
break;
}
}
//if b >= 12 b = 11
if(b == 12) b = 11;
buckets[b].count++;
buckets[b].bounds = Union(buckets[b].bounds,objects[i]->getBounds());
buckets[b].objects.push_back(objects[i]);
}
//we need calculate the time cost
// the constant cost
float ctrav = 0.125f;
float cisect = 1.0f;
double SC = centroidBounds.SurfaceArea();
//the expression is c = ctrave + SA/SC*NUMA*cisect + SB/SC*NUMB*cisect
int countA = 0;
Bounds3 boundA;
for(int i = 0;i<11;i++)
{
int countB = 0;
Bounds3 boundB;
countA += buckets[i].count;
boundA = Union(boundA,buckets[i].bounds);
for(int j = i+1;j<12;j++)
{
countB += buckets[j].count;
boundB = Union(boundB,buckets[j].bounds);
}
//double SC = centroidBounds.SurfaceArea();
double timeA = (boundA.SurfaceArea()/SC) * countA * cisect;
double timeB = (boundB.SurfaceArea()/SC) * countB * cisect;
cost[i] = ctrav + timeA + timeB;
//std::cout << cost[i] << std::endl;
}
//get the min cost and the division way(i+1)
double mincost = cost[0];
int divisionWay = 0;
for(int i = 1;i<11;i++)
{
if(cost[i] < mincost)
{
mincost = cost[i];
divisionWay = i;
}
}
//create node;
int leftnum = 0;
std::vector<Object*> leftnode;
std::vector<Object*> rightnode;
//构建左右节点
for(int i = 0;i<=divisionWay;i++)
{
leftnode.insert(leftnode.end(),buckets[i].objects.begin(),buckets[i].objects.end());
}
for(int i = divisionWay+1;i<12;i++)
{
rightnode.insert(rightnode.end(),buckets[i].objects.begin(),buckets[i].objects.end());
}
// auto leftnode = std::vector<Object*>(objects.begin(),objects.begin()+leftnum-buckets[divisionWay].count+1);
// auto rightnode = std::vector<Object*>(objects.begin()+leftnum-buckets[divisionWay].count+1,objects.end());
assert(objects.size() == (leftnode.size() + rightnode.size()));
node->left = recursiveSAHBuild(leftnode);
node->right = recursiveSAHBuild(rightnode);
node->bounds = Union(node->left->bounds, node->right->bounds);
}
return node;
}
SAH中构建左右节点#
主要是最开始我做错了(即上面最后的两行注释),错误其实很明显。本来左右节点的构建也就是递归需要传入的物体数组应该是由buckets划分好了的,而我最开始直接使用Object然后利用分割点对应的物体下标来划分,导致了错误。
所以我在Buckets结构体中加入了一个记录当前木桶包含的物体数组,最后对于左右节点数组的更新直接使用insert将前i个buckets中的物体数组接到左节点需要的数组后面就可以了。
下面是Buckets结构体
struct Buckets
{
//the bounds belong this bucket
Bounds3 bounds;
//the number of bounds which this bucket have
int count;
std::vector<Object*> objects;
};
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~