games101 作业6 详解SAH

games101 作业6 详解SAH

可参考资料:https://www.pbr-book.org/3ed-2018/Primitives_and_Intersection_Acceleration/Bounding_Volume_Hierarchies

代码分析

作业6 的代码框架在作业五的基础上进行了进一步地丰富与改进
一开始还是创建场景 添加物体以及灯光 这次的物体只有一个用objloader读取的兔子的3d模型 但是在将该模型转换为meshtriangle数据结构的时候,也为该模型中的各个三角形片元创建BVH(Bounding volume hierarchies)的加速结构 这里虽然也为场景中的各个物体也构建了BVH 但是目前场景中只有一个物体

MeshTriangle bunny(Utils::PathFromAsset("model/bunnyAssignment6/bunny.obj"));

scene.Add(&bunny);
scene.Add(std::make_unique<Light>(Vector3f(-20, 70, 20), 1));
scene.Add(std::make_unique<Light>(Vector3f(20, 70, 20), 1));
scene.buildBVH();

MeshTriangle(const std::string& filename){
.......
std::vector<Object*> ptrs;
for (auto& tri : triangles)
    ptrs.push_back(&tri);

bvh = new BVHAccel(ptrs);
}

BVHAccel::BVHAccel(std::vector<Object*> p, int maxPrimsInNode,
                   SplitMethod splitMethod)
    : maxPrimsInNode(std::min(255, maxPrimsInNode)), splitMethod(splitMethod),
      primitives(std::move(p))
{
    time_t start, stop;
    time(&start);
    if (primitives.empty())
        return;

    root = recursiveBuild(primitives);

    time(&stop);
    double diff = difftime(stop, start);
    int hrs = (int)diff / 3600;
    int mins = ((int)diff / 60) - (hrs * 60);
    int secs = (int)diff - (hrs * 3600) - (mins * 60);

    printf(
        "\rBVH Generation complete: \nTime Taken: %i hrs, %i mins, %i secs\n\n",
        hrs, mins, secs);
}

而BVH的构建过程就是一颗树的递归构建的过程 划分BVH节点的方法 就是在最长的维度上 对所有的三角形进行排序 找出位于中间的这个三角形的位置 左边的就是左节点 右边就是右节点,划分结束的终止条件就是只有一个物体
对应课件的ppt:


pbrt图示:

BVHBuildNode* BVHAccel::recursiveBuild(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 = recursiveBuild(std::vector{objects[0]});
        node->right = recursiveBuild(std::vector{objects[1]});

        node->bounds = Union(node->left->bounds, node->right->bounds);
        return node;
    }
    else {
        //用每个三角形对应的box的中点 拼成一个box 然后找出这个box最长的维度 在这个维度上进行划分
        Bounds3 centroidBounds;
        for (int i = 0; i < objects.size(); ++i)
            centroidBounds =
                Union(centroidBounds, objects[i]->getBounds().Centroid());
        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);
    }

    return node;
}

之后就是光线追踪castRay 求交 然后进行着色计算 和作业5类似
但是这次求交要利用我们构建的BVH加速结构进行加速 这也是我们要完成的部分

分析与解决

与BVH求交

这里主要是对构建的BVH加速结构进行一个遍历 和节点的boundingbox求交 如果没有交点就返回 如果有 则查看该节点是不是叶子节点 不是叶子节点 则继续遍历该节点的子节点 如果是叶子节点 则对其中的物体进行相交测试 找出最近的交点返回:


Intersection BVHAccel::getIntersection(BVHBuildNode* node, const Ray& ray) const
{
    // TODO Traverse the BVH to find intersection
    Intersection hit1;
    Intersection hit2;
    std::array<int, 3> dirIsNeg{ 0,0,0 };
    for (int i = 0; i < 3; i++)
    {
        if (ray.direction[i] < 0) {
            dirIsNeg[i] = 1;
        }
    }
    if (!node->bounds.IntersectP(ray, ray.direction_inv, std::move(dirIsNeg)))
    {
        return Intersection{};
    }
    if (node->left == nullptr && node->right == nullptr) {
        return node->object->getIntersection(ray);
    }
    hit1 = getIntersection(node->left, ray);
    hit2 = getIntersection(node->right, ray);
    
    return hit1.distance > hit2.distance ? hit2 : hit1;
}

boundingbox求交

boundingbox是个长方体 并且与各个面与xyz轴平行 我们只需要求出和每组平面 的tenter 与 texit 并从enter中找出最大值 exit中找出最小值 这样可以防止错误的将交点找到长方体的延展面上 同时也要考虑dir光线为负的情况 这样的话会和坐标值较小的平面先相交:

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 txmin = (pMin - ray.origin).x * invDir.x;
    float txmax = (pMax - ray.origin).x * invDir.x;
    float tymin = (pMin - ray.origin).y * invDir.y;
    float tymax = (pMax - ray.origin).y * invDir.y;
    float tzmin = (pMin - ray.origin).z * invDir.z;
    float tzmax = (pMax - ray.origin).z * invDir.z;
    //如果方向某个维度是负的  会和pMax对应维度上的面上先相交
    if (dirIsNeg[0]) {
        std::swap(txmin, txmax);
    }
    if (dirIsNeg[1]) {
        std::swap(tymin, tymax);
    }
    if (dirIsNeg[2]) {
        std::swap(tzmin, tzmax);
    }
    float tenter = std::max(std::max(txmin, tymin), tzmin);
    float texit = std::min(std::min(txmax, tymax), tzmax);
    return tenter < texit && texit >= 0;
}

三角形求交

和作业5中的实现类似
这里需要返回交点的属性 比如材质 法线等等:

inline Intersection Triangle::getIntersection(Ray ray)
{
    Intersection inter;
    //如果法线与光线异侧 说明光线是从背侧射入
    if (dotProduct(ray.direction, normal) > 0)
        return inter;
    double u, v, t_tmp = 0;
    // D E2 = S1
    Vector3f pvec = crossProduct(ray.direction, e2);
    // S1 . E1
    double det = dotProduct(e1, pvec);
    if (fabs(det) < EPSILON)
        return inter;

    double det_inv = 1. / det;
    //S 
    Vector3f tvec = ray.origin - v0;
    u = dotProduct(tvec, pvec) * det_inv;
    if (u < 0 || u > 1)
        return inter;
    //S2
    Vector3f qvec = crossProduct(tvec, e1);
    v = dotProduct(ray.direction, qvec) * det_inv;
    if (v < 0 || u + v > 1)
        return inter;
    t_tmp = dotProduct(e2, qvec) * det_inv;

    // TODO find ray triangle intersection
    if (t_tmp < 0) {
        return inter;
    }
    inter.happened = true;
    inter.coords = ray(t_tmp);
    inter.normal = normal;
    inter.distance = t_tmp;
    inter.obj = this;
    inter.m = this->m;


    return inter;
}

SAH

SAH 全称是Surface Area Heuristic 是在构建BVH过程中 进行划分的一种方法 之前我们也提到了一种简单的划分方法 之前的方法在下图所示的情况下:


会选择图中蓝线这样的划分 最终boundingbox会有部分重叠 从而降低光线求交的效率
SAH提出了一个函数来量化地估计划分区域的代价 以将一个boundingbox划分为AB两个为例:

其中t-trav是遍历树节点的代价 这里固定为一个常数0.125 pbrt的解释是我们将光线求交的代价设置为1 遍历效率大概是求交的1/8 重要的是它们之间的相对关系 而不是数值
t-isect代表着求交的代价 具体设置为每个区域内图元的数目
pa pb代表着光线穿过子节点的概率 使用A的boundingbox面积 与 总boundingbox面积的比值来确定

具体流程概括如下:
1.首先我们和之前的划分算法一样 找出最长的维度
2.这里我们定义一定数目的bucket 在整体的boundingbox上进行划分 计算出每个物体在哪个bucket的索引
3.每个bucket都代表着一种划分方法 我们遍历计算各方法的代价 然后找出最小代价对应的bucket索引:


4.借助最小索引将物体分为两堆

代码:参考https://github.com/mmp/pbrt-v3/blob/master/src/accelerators/bvh.cpp

BVHBuildNode* BVHAccel::SAHrecursiveBuild(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() <= 2) {
        node = recursiveBuild(objects);
        return node;
    }
    else {
        Bounds3 centroidBounds;
        for (int i = 0; i < objects.size(); ++i)
            centroidBounds =
            Union(centroidBounds, objects[i]->getBounds().Centroid());
        int dim = centroidBounds.maxExtent();
        //计算索引
        constexpr int nBuckets = 8;
        BucketInfo buckets[nBuckets];
        for (int i = 0; i < objects.size(); ++i) {
            int b = nBuckets *
                centroidBounds.Offset(
                    objects[i]->getBounds().Centroid())[dim];
            if (b == nBuckets) b = nBuckets - 1;
            buckets[b].count++;
            buckets[b].bounds =
                Union(buckets[b].bounds, objects[i]->getBounds());
        }
        //计算每个划分的代价
        float cost[nBuckets - 1];
        for (int i = 0; i < nBuckets - 1; ++i) {
            Bounds3 b0, b1;
            int count0 = 0, count1 = 0;
            for (int j = 0; j <= i; ++j) {
                b0 = Union(b0, buckets[j].bounds);
                count0 += buckets[j].count;
            }
            for (int j = i + 1; j < nBuckets; ++j) {
                b1 = Union(b1, buckets[j].bounds);
                count1 += buckets[j].count;
            }
            cost[i] = 0.125f +
                (count0 * b0.SurfaceArea() +
                    count1 * b1.SurfaceArea()) /
                bounds.SurfaceArea();
        }
        //找出最小代价
        float minCost = cost[0];
        int minCostSplitBucket = 0;
        for (int i = 1; i < nBuckets - 1; ++i) {
            if (cost[i] < minCost) {
                minCost = cost[i];
                minCostSplitBucket = i;
            }
        }
        float leafCost = objects.size();
        int mid = 0;
        //如果包含的物体过多 或者缩小划分代价 则进行划分
        if (objects.size() > maxPrimsInNode || minCost < leafCost) {
            for (int i = 0; i < objects.size(); ++i) {
                int b = nBuckets *
                    centroidBounds.Offset(
                        objects[i]->getBounds().Centroid())[dim];
                if (b == nBuckets) b = nBuckets - 1;

                // 满足条件的元素放到数组前半部分
                if (b <= minCostSplitBucket) {
                    std::swap(objects[i], objects[mid]);
                    mid++;
                }
            }
        }
       
        auto beginning = objects.begin();
        auto middling = objects.begin() + mid;
        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 = SAHrecursiveBuild(leftshapes);
        node->right = SAHrecursiveBuild(rightshapes);

        node->bounds = Union(node->left->bounds, node->right->bounds);
    }

    return node;
}

结果展示

BVH:


BVH-SAH:

热门相关:我真不是开玩笑   来自地狱的男人   超级吸收   呆萌配腹黑:倒追男神1000次   呆萌配腹黑:欢喜小冤家