BVH树的构建与遍历
在計(jì)算機(jī)圖形學(xué)中,BVH樹(shù)是一種空間劃分的數(shù)據(jù)結(jié)構(gòu),廣泛運(yùn)用于光線追蹤。今天來(lái)講述一下它的建立和遍歷方法。
BVH樹(shù)的建立
BVH樹(shù)的建立分為以下幾步:
1.遍歷當(dāng)前場(chǎng)景中的所有物體,存儲(chǔ)下它們的每一個(gè)圖元(primitive,例如三角形、圓形等);對(duì)每一個(gè)圖元,計(jì)算它們的包圍盒。
2.遞歸構(gòu)建BVH樹(shù)。
BVH樹(shù)是一種二叉樹(shù),每一個(gè)節(jié)點(diǎn)記錄了它自己的包圍盒。對(duì)于葉子節(jié)點(diǎn),它存儲(chǔ)了它所包含的所有圖元;對(duì)于非葉子節(jié)點(diǎn),記錄了它所包含的孩子節(jié)點(diǎn)。節(jié)點(diǎn)的定義如下:
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);
};
接下來(lái)展示遞歸建立BVH樹(shù)的代碼:
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;
}
這里最重要的步驟就是給定一個(gè)節(jié)點(diǎn)及其包圍盒,如何對(duì)它進(jìn)行空間劃分。在這里我們采用SAH(Surface Area Heuristic)算法。該算法首先尋找boundingbox中跨度最長(zhǎng)的一個(gè)軸作為用來(lái)分割的軸,然后沿著該軸N等分為一個(gè)個(gè)塊,最后根據(jù)代價(jià)公式遍歷每一個(gè)塊,如圖所示:
我們要做的是尋找出從哪一個(gè)塊開(kāi)始分割,使得代價(jià)最小。代價(jià)公式如下所示:
A和B是由當(dāng)前的包圍盒分割出的兩個(gè)子模塊,t(trav)和t(isect)我們可以當(dāng)做是常數(shù),pA和pB代表光線打到兩個(gè)子塊的概率,我們用兩個(gè)子塊相對(duì)于父親的面積來(lái)計(jì)算。
這樣一來(lái),就可以寫(xiě)出計(jì)算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;
}
}
經(jīng)過(guò)上面的步驟后,就可以對(duì)空間進(jìn)行劃分,建立出SAH樹(shù)。
建立完BVH樹(shù)后,為了節(jié)省空間和提高遍歷的性能,我們需要將這個(gè)二叉樹(shù)的結(jié)構(gòu)壓縮到一個(gè)線性數(shù)組中。做到:
1.初始節(jié)點(diǎn)是數(shù)組中第一個(gè)元素
2.對(duì)于非葉子節(jié)點(diǎn),它的第一個(gè)孩子就是數(shù)組中的下一個(gè)元素,同時(shí)它會(huì)存儲(chǔ)第二個(gè)孩子的索引
3.對(duì)于葉子節(jié)點(diǎn),它會(huì)記錄自己包含的圖元
下圖是線性化二叉樹(shù)的示意圖:
具體的線性化二叉樹(shù)節(jié)點(diǎn)定義及建立過(guò)程如下:
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;
}
經(jīng)過(guò)上面的一系列步驟,我們就將BVH樹(shù)建立了起來(lái),可以用于實(shí)戰(zhàn)了。
BVH樹(shù)的遍歷
在進(jìn)行投射光線,尋找場(chǎng)景中的交點(diǎn)時(shí),就可以遍歷BVH樹(shù)來(lái)加速。BVH樹(shù)的遍歷和線性化二叉樹(shù)的遍歷基本一致,代碼如下:
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();
}
}
注意代碼中有一處判斷光線的方向是否為負(fù)的地方,它是為了讓當(dāng)前最接近光線方向的孩子包圍盒首先被搜尋,這樣在搜尋第二個(gè)孩子的時(shí)候,如果進(jìn)行的包圍盒相交判斷得到t值比之前存儲(chǔ)的最小t值大的話,就無(wú)需再進(jìn)一步深入該子節(jié)點(diǎn)進(jìn)行相交檢測(cè),可以節(jié)省一定的計(jì)算量。
如圖就是我根據(jù)一個(gè)Mesh建立出的BVH樹(shù)的包圍盒:
經(jīng)我測(cè)試,加入BVH樹(shù)后,對(duì)于上圖的Mesh,進(jìn)行光線相交檢測(cè)的速度提高了近25倍。
總結(jié)
以上是生活随笔為你收集整理的BVH树的构建与遍历的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 图像放大 问题 即 二维数组放大
- 下一篇: 通过修改manifest文件来解决Vis