Games101 作业 6 解析(含提高部分)

1.作业要求

  1. IntersectP(const Ray& ray, const Vector3f& invDir, const std::array<int, 3>& dirIsNeg) in the Bounds3.hpp: 这个函数的 作用是判断包围盒 BoundingBox 与光线是否相交,你需要按照课程介绍的算法实现求交过程
  2. getIntersection(BVHBuildNode* node, const Ray ray)in BVH.cpp: 建 立 BVH 之后,我们可以用它加速求交过程。该过程递归进行,你将在其中调用你实现的 Bounds3::IntersectP

其实就是实现课程中包围盒相交的过程,以及实现 BVH算法 加速光线追踪

2.原理剖析

需要注意一个细节:左右子树节点都可能相交,因此我们需要对左右子树都遍历,然后找到其中的距离最近的节点作为相交点

2.1 BVH的不同方法

1. 不同方法

  1. 先递归地生成 BVH 树,这里叶节点的划分可以不同

    1. 我们可以首先建立一个”原则“(Principle):即决定在哪根轴(x,y,z)上进行划分。”原则“(Principle)与”策略“(Strategy)的不同之处在于,不管用何种”策略“,总是遵守同一种”原则“

      1. principle: 决定在哪根轴(x,y,z)上进行划分,取决于场景中的物体在各个轴上分布的“散度”。如果这些物体沿着某根轴分布得最为“松散”(即落在该轴上靠一侧最近的物体与另一侧最近的物体,二者距离为最大),那么就沿该轴进行划分【其实也就是按照轴的最大范围进行划分 ,这种方式从某种程度上可以减少重叠

    2. 这时有多种策略可以考虑:pbrt中应用到的有三种划分策略:取中点划分(SPLIT_MIDDLE),按等量划分(SPLIT_EQUAL_COUNT),以及用表面积启发式算法划分(SPLIT_SAH, SAH=Surface Area Heuristic)

      1. SPLIT_MIDDLE

        1. 找轴上最靠近两轴的物体,连接二者重心 得到一条线段,那么就用线段的中点作为划分点进行划分

          1. 这样划分简单,但是效果不好:

            1. 物体分布不均匀,可能堆在一侧

            2. 包围盒重叠概率大,这样对所有子树都要进行查找和判别

      2. SPLIT_EQUAL_COUNT

        1. 对于第一种方法,不取重心中点,而是根据物体数量进行划分;形似 AVL 树,查找效率高

          1. 但是仍然不能解决包围盒的重叠问题

      3. SPLIT_SAH

        SAH的做法并不能完全做到”不重叠“或者使划分后的分布就很“均匀”,但它每做一次划分,选取的都是当前情形下最优的方案。因此称它是一种“启发式”(Heuristic)算法

         SAH考虑到了图元在空间中的分布也考虑到了子节点包围体的重叠程度,在实际应用中拥有很好的效果

        1. 这种方法通过对求交代价和遍历代价进行评估,给出了每一种划分的代价(Cost),而我们的目的便是去寻找代价最小的划分

        2. 假设当前节点的包围体中存在 n个物体,设对每一个物体求交的代价为 t(i) ,如果不做划分依次对其求交则总的代价为:

        3. 假设将物体分成 A 和 B 两个包围盒部分,光线和 AB 都可能相交,假设射中概率为 p(A),p(B)


          1. 因此,代价可以写成

          2. 若 A 有图元 a 个,B 有图元 b 个,则代价为:

            c(A,B)=p(A)*a+p(B)*b+0.125
        4. 光线击中包围盒的概率可以用表面积估计,对于三维其实就是长方体的表面积,则代价可以写成:

2. SAH 原理

直接找最小代价求的还效率不高,因此我们采用另外一种方式

是将节点 C 所包围的空间沿着跨度最长的那个坐标轴的方向将空间均等的划分为若干个桶(Buckets),划分只会出现在桶与桶之间的位置上。如图所示,若桶的个数为 n 则只会有 n−1 种划分的可能

img

实现思路

SAH的核心思想是在构建空间分割结构时,优先选择那些将场景空间划分得更加均匀且表面积较小的划分方案,从而提高光线追踪的效率。

遍历应该不变,但是实现变化

  1. 准备工作:首先,需要收集场景中所有的基本图元(如三角形、球等),并计算它们的包围盒(AABB)。

  2. 计算表面积:对于每个图元的包围盒,计算其表面积。这可以通过计算每个包围盒的长、宽和高,然后计算6个面的面积之和来实现。

  3. 选择划分轴:对于每个包围盒,选择一个划分轴(x、y或z轴)。对于每个轴,将包围盒沿该轴划分为两个子空间,并计算两个子空间的表面积之和。选择使得表面积之和最小的划分轴。

  4. 选择划分位置:在选定的划分轴上,找到一个划分位置,使得两个子空间的表面积之和最小。这可以通过在划分轴上进行线性搜索来实现。需要注意的是,为了避免过度划分,可以设置一个阈值来限制划分的粒度。

  5. 递归构建子树:根据选定的划分轴和划分位置,将图元划分为两个子空间,并递归地对每个子空间重复上述步骤,直到满足终止条件(如子空间中的图元数量小于一个阈值)。

  6. 构建BVH或kd树:根据上述步骤得到的空间划分方案,构建对应的BVH或kd树结构。

3.实现

3.1 IntersectP 实现

inline bool Bounds3::IntersectP(const Ray &ray, const Vector3f &invDir,
                                const std::array<int, 3> &dirIsNeg) const {
    //对于 x-y平面上的
    //因为 x,y,z 有可能小于 0,导致符号不对,min和 max 需要翻转
    float temp = 0;
    float t_xmin = (pMin.x - ray.origin.x) * invDir.x;
    float t_xmax = (pMax.x - ray.origin.x) * invDir.x;
    //这里可以优化下
    if (!dirIsNeg[0]) {
        temp = t_xmin;
        t_xmin = t_xmax;
        t_xmax = temp;
    }
    float t_ymin = (pMin.y - ray.origin.y) * invDir.y;
    float t_ymax = (pMax.y - ray.origin.y) * invDir.y;
    if (!dirIsNeg[1]) {
        temp = t_ymin;
        t_ymin = t_ymax;
        t_ymax = temp;
    }
    float t_zmin = (pMin.z - ray.origin.z) * invDir.z;
    float t_zmax = (pMax.z - ray.origin.z) * invDir.z;
    if (!dirIsNeg[2]) {
        temp = t_zmin;
        t_zmin = t_zmax;
        t_zmax = temp;
    }
    float t_enter = std::fmax(t_zmin, std::fmax(t_xmin, t_ymin));
    float t_exit = std::fmin(std::fmin(t_xmax, t_ymax), t_zmax);

    if (t_exit > 0 && t_enter < t_exit) {
        return true;
    }

    return false;
}

3.2 getIntersection实现

Intersection BVHAccel::getIntersection(BVHBuildNode* node, const Ray& ray) const
{
    // TODO Traverse the BVH to find intersection
    // 递归地遍历树节点进行求交
    Intersection inserction;
    if(node == nullptr || !node->bounds.IntersectP(ray,ray.direction_inv,std::array<int,3>{int(ray.direction.x>0),int(ray.direction.y>0),int(ray.direction.z>0)})){
        return inserction;
    }
    if(node->left == nullptr && node->right == nullptr){
        return node->object->getIntersection(ray);
    }
    Intersection l = getIntersection(node->left,ray);
    Intersection r = getIntersection(node->right,ray);

    return l.distance<r.distance?l:r;

}

3.3 SAH 实现

//每个图元,三角形或者圆形已经自带了 bounce 了
//这里需要注意:中点的node的 objects 是为 null 的,这是框架的问题
BVHBuildNode* BVHAccel::svhBuild(std::vector<Object*>objects){
    BVHBuildNode* node = new BVHBuildNode();
    Bounds3 bounces;
    //将物体放置在同一个 Bounce下,然后再进行划分
    for(Object* obj : objects)
    {
        bounces = Union(bounces,obj->getBounds());
    }
    if(objects.size()==1){
        node->bounds = objects[0]->getBounds();
        node->right = nullptr;
        node->left = nullptr;
        node->object = objects[0];
    }else if(objects.size() == 2){
        node->left = svhBuild(std::vector<Object*>{objects[0]});
        node->right = svhBuild(std::vector<Object*>{objects[1]});
        node->bounds = Union(node->left->bounds,node->right->bounds);
    }else{
        //根据表面积求代价进行计算
        //这里构成了重心中点作为包围盒边界的包围盒,因为包围盒为长方体,因此直接求 1/2  即可
        Bounds3 centroidBounds;
        for (int i = 0; i < objects.size(); ++i)
            centroidBounds =
                    Union(centroidBounds, objects[i]->getBounds().Centroid());
        int dim = centroidBounds.maxExtent(); //取 x-y-z中最大的值作为轴标准 进行排序
        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;
        }
        //剩下的物体按照对应的轴标准 进行对半划分,保证搜索效率
        int n = 0;  //记录位置
        float minValue = std::numeric_limits<float>::max(); //记录最小值
        float traw = 0.125;
        float area = 0;
        const int NUM = objects.size();
        //寻找cost 最小值
        //思考了一下:用 Bucket 也是需要求每一个 Bucket 里面面元的面积的,总体来说和遍历区别不大。
        for(int i = 0 ; i<NUM ; ++i){
            area += objects[i]->getBounds().SurfaceArea();
            float cost = area/bounces.SurfaceArea()*(i+1) + (bounces.SurfaceArea()-area)/area * (NUM-i+1)+traw;
            if(cost<minValue){
                minValue = cost;
                n = i;
            }
        }
        auto beginning = objects.begin();
        auto middling = objects.begin() + (n+1);
        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 = svhBuild(leftshapes);
        node->right = svhBuild(rightshapes);
        node->bounds = Union(node->left->bounds, node->right->bounds);
    }
    return node;
}

3.4 框架修改

需要注意!Triangle 部分需要正确修改,否则没有图像

 ...
tmp = ...
if(t_tmp<0)
    {
        return inter;
    }
    // TODO find ray triangle intersection
    //这里需要对三角形进行着色

    inter.happened = true;
    inter.coords= ray.origin + t_tmp * ray.direction;
    inter.normal= normal;
    inter.distance= t_tmp;
    inter.obj = this;
    inter.m = m;

 

posted @ 2024-01-23 19:12  Kellen_Gram  阅读(420)  评论(0)    收藏  举报