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倍。
總結
以上是生活随笔為你收集整理的BVH树的构建与遍历的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 图像放大 问题 即 二维数组放大
- 下一篇: 通过修改manifest文件来解决Vis