zoukankan      html  css  js  c++  java
  • BVH树的构建与遍历

    在计算机图形学中,BVH树是一种空间划分的数据结构,广泛运用于光线追踪。今天来讲述一下它的建立和遍历方法。

    BVH树的建立

    BVH树的建立分为以下几步:
    1.遍历当前场景中的所有物体,存储下它们的每一个图元(primitive,例如三角形、圆形等);对每一个图元,计算它们的包围盒。
    2.递归构建BVH树。
    BVH树是一种二叉树,每一个节点记录了它自己的包围盒。对于叶子节点,它存储了它所包含的所有图元;对于非叶子节点,记录了它所包含的孩子节点。节点的定义如下:

    struct BVHBuildNode {
        BVHBuildNode* children[2];
        BoundingBox boundingbox;
        int splitAxis, firstPrimeOffset, nPrimitives;
        void initLeaf(int first, int n, const BoundingBox&b);
        void initInterior(int axis, BVHBuildNode*c0, BVHBuildNode*c1);
    };
    

    接下来展示递归建立BVH树的代码:

    BVHBuildNode* BVHManager::recursiveBuild(int start, int end, int* totalnodes, std::vector<Primitive*>& ordered_prims)
    {
    	BVHBuildNode* node = nullptr;
    	(*totalnodes)++;
    	int nPrimitives = end - start;
    	BoundingBox bounds;
    	for (int i = start; i < end; i++)
    		bounds = BoundingBox::Union(bounds, primitives[i]->getBoundingBox());
    	if (nPrimitives == 1)
    		node = createLeafNode(start, end, totalnodes, ordered_prims, bounds);
    	else if(nPrimitives > 1)
    	{
    		int dim = bounds.maximumExtent();
    		if(bounds.getTopFromDim(dim)==bounds.getBottomFromDim(dim))
    			node = createLeafNode(start, end, totalnodes, ordered_prims, bounds);
    		else
    		{
    			int mid = partitionPrimitivesWithSAH(start, end, dim, bounds);
    			if(mid < 0)
    				node = createLeafNode(start, end, totalnodes, ordered_prims, bounds);
    			else {
    				node = new BVHBuildNode;
    				node->initInterior(dim,
    					recursiveBuild(start, mid, totalnodes, ordered_prims),
    					recursiveBuild(mid, end, totalnodes, ordered_prims));
    			}
    		}
    	}
    	return node;
    }
    

    这里最重要的步骤就是给定一个节点及其包围盒,如何对它进行空间划分。在这里我们采用SAH(Surface Area Heuristic)算法。该算法首先寻找boundingbox中跨度最长的一个轴作为用来分割的轴,然后沿着该轴N等分为一个个块,最后根据代价公式遍历每一个块,如图所示:

    我们要做的是寻找出从哪一个块开始分割,使得代价最小。代价公式如下所示:

    A和B是由当前的包围盒分割出的两个子模块,t(trav)和t(isect)我们可以当做是常数,pA和pB代表光线打到两个子块的概率,我们用两个子块相对于父亲的面积来计算。
    这样一来,就可以写出计算SAH的代码:

    int BVHManager::partitionPrimitivesWithSAH(int start, int end, int dim, BoundingBox& bounds)
    {
    	int nPrimitives = end - start;
    	int nBuckets = BVHManager::nBuckets;
    	if (nPrimitives <= 4)
    		return partitionPrimitivesWithEquallySizedSubsets(start, end, dim);
    	else
    	{
    		for (int i = start; i < end; i++)
    		{
    			BoundingBox prim_bounds = primitives[i]->getBoundingBox();
    			int b = nBuckets * 
    				(prim_bounds.getCenterValFromDim(dim) - bounds.getBottomFromDim(dim)) /
    				(bounds.getTopFromDim(dim) - bounds.getBottomFromDim(dim));
    			if (b == nBuckets)
    				b--;
    			buckets[b].count++;
    			buckets[b].bounds = BoundingBox::Union(buckets[b].bounds, prim_bounds);
    		}
    		float cost[BVHManager::nBuckets - 1];
    		for (int i = 0; i < nBuckets - 1; i++)
    		{
    			BoundingBox b0, b1;
    			int count0 = 0, count1 = 0;
    			for (int j = 0; j <= i; j++)
    			{
    				b0 = BoundingBox::Union(b0, buckets[j].bounds);
    				count0 += buckets[j].count;
    			}
    
    			for (int j = i+1; j < BVHManager::nBuckets; j++)
    			{
    				b1 = BoundingBox::Union(b1, buckets[j].bounds);
    				count1 += buckets[j].count;
    			}
    			float val0 = count0 ? count0 * b0.surfaceArea() : 0.0f;
    			float val1 = count1 ? count1 * b1.surfaceArea() : 0.0f;
    
    			cost[i] = 0.125f + (val0 + val1) / bounds.surfaceArea();
    		}
    
    		float min_cost = cost[0];
    		int min_ind = 0;
    		for (int i = 0; i < BVHManager::nBuckets -1; i++)
    		{
    			if (cost[i] < min_cost)
    			{
    				min_cost = cost[i];
    				min_ind = i;
    			}
    		}
    		if (nPrimitives > maxPrimsInNode || min_cost < nPrimitives)
    		{
    			Primitive** p = std::partition(&primitives[start], &primitives[end - 1] + 1,
    				[=](const Primitive* pi) {
    				int b = nBuckets *
    					(pi->getBoundingBox().getCenterValFromDim(dim) - bounds.getBottomFromDim(dim)) /
    					(bounds.getTopFromDim(dim) - bounds.getBottomFromDim(dim));
    				if (b == nBuckets)
    					b--;
    				return b <= min_ind;
    			});
    			return p - &primitives[0];
    		}
    		else
    			return -1;
    	}
    }
    

    经过上面的步骤后,就可以对空间进行划分,建立出SAH树。
    建立完BVH树后,为了节省空间和提高遍历的性能,我们需要将这个二叉树的结构压缩到一个线性数组中。做到:
    1.初始节点是数组中第一个元素
    2.对于非叶子节点,它的第一个孩子就是数组中的下一个元素,同时它会存储第二个孩子的索引
    3.对于叶子节点,它会记录自己包含的图元
    下图是线性化二叉树的示意图:

    具体的线性化二叉树节点定义及建立过程如下:

    struct LinearBVHNode {
    	BoundingBox boundingbox;
    	union 
    	{
    		int primitivesOffset;  // leaf
    		int secondChildOfset;  // interior
    	};
    	int nPrimitives;
    	int axis;
    };
    
    int BVHManager::flattenBVHTree(BVHBuildNode* node, int* offset)
    {
    	LinearBVHNode& lnode = linear_nodes[*offset];
    	lnode.boundingbox = node->boundingbox;
    	int myOffset = (*offset)++;
    
    	if (node->nPrimitives > 0)
    	{
    		lnode.primitivesOffset = node->firstPrimeOffset;
    		lnode.nPrimitives = node->nPrimitives;
    	}
    	else
    	{
    		lnode.axis = node->splitAxis;
    		lnode.nPrimitives = 0;
    		flattenBVHTree(node->children[0], offset);
    		lnode.secondChildOfset = flattenBVHTree(node->children[1], offset);
    	}
    	return myOffset;
    }
    

    经过上面的一系列步骤,我们就将BVH树建立了起来,可以用于实战了。

    BVH树的遍历

    在进行投射光线,寻找场景中的交点时,就可以遍历BVH树来加速。BVH树的遍历和线性化二叉树的遍历基本一致,代码如下:

    float min_t = -1.0f;
    auto bvh_nodes = objectManager.getBVHManager()->getBvhNodes();
    std::stack<int> nodesToVisit;
    int cur_node_index = 0;
    while (true)
    {
    	LinearBVHNode node = bvh_nodes[cur_node_index];
    	if (node.boundingbox.isIntersect(ray, XMMatrixIdentity()))
    	{
    		if (node.nPrimitives > 0)
    		{
    			for (int i = 0; i < node.nPrimitives; i++)
    			{
    				float t=-1.0f;
    				Primitive* p = objectManager.getBVHManager()->getPrimitive(node.primitivesOffset + i);
    				IntersectInfo it;
    				if (p->is_intersect(ray, t, it) && (min_t < 0.0f || t < min_t))
    				{
    					min_t = t;
    					info = it;
    				}
    			}
    			if (nodesToVisit.empty())
    				break;
    			cur_node_index = nodesToVisit.top();
    			nodesToVisit.pop();
    		}
    		else
    		{
    		if(ray.dirIsNeg(node.axis))
    		{
    			nodesToVisit.push(cur_node_index + 1);
    			cur_node_index = node.secondChildOfset;
    
    		}
    		else
    		{
    			nodesToVisit.push(node.secondChildOfset);
    			cur_node_index++;
    		}
    		}
    	    }
    	else
    	{
    		if (nodesToVisit.empty())
    			break;
    		cur_node_index = nodesToVisit.top();
    		nodesToVisit.pop();
    	}
    }
    

    注意代码中有一处判断光线的方向是否为负的地方,它是为了让当前最接近光线方向的孩子包围盒首先被搜寻,这样在搜寻第二个孩子的时候,如果进行的包围盒相交判断得到t值比之前存储的最小t值大的话,就无需再进一步深入该子节点进行相交检测,可以节省一定的计算量。
    如图就是我根据一个Mesh建立出的BVH树的包围盒:

    经我测试,加入BVH树后,对于上图的Mesh,进行光线相交检测的速度提高了近25倍。

  • 相关阅读:
    20165329 《Java程序设计》第九周学习总结
    20165329 四则运算2
    20165329 第八周补做
    20165329 《Java程序设计》第八周学习总结
    2017-2018-2 学号20165329 实验二《Java面向对象程序设计》实验报告
    20165329 结对编程项目-四则运算
    20165329 第七周学习总结
    20165329 第六周学习总结
    20165329 实验一 java环境的熟悉
    20165329 第五周学习总结
  • 原文地址:https://www.cnblogs.com/wickedpriest/p/12269564.html
Copyright © 2011-2022 走看看