Recast Demo中BVH树的构建
数据结构介绍
首先介绍BVH树的数据结构:
//BVH(Bounding Volumn Hierachy,Chunk means cube)
struct rcChunkyTriMesh
{
inline rcChunkyTriMesh() : nodes(0), nnodes(0), tris(0), ntris(0), maxTrisPerChunk(0) {};
inline ~rcChunkyTriMesh() { delete [] nodes; delete [] tris; }
rcChunkyTriMeshNode* nodes;//这里用一个数组表示树的节点,nodes代表根节点
int nnodes;//节点总数
int* tris;//一个数组,记录了BVH树里的三角形索引值(具体的顶点数据仍然存放在load模型时存储的数据中)
int ntris;//三角形的总数
int maxTrisPerChunk;//一个叶节点,也就是一个Cube里面最多存储的三角形的数量
private:
// Explicitly disabled copy constructor and copy assignment operator.
rcChunkyTriMesh(const rcChunkyTriMesh&);
rcChunkyTriMesh& operator=(const rcChunkyTriMesh&);
};
这里的Chunk就是Cube的意思,代表3D空间的一个Cube,不过这里的Cube是长方体,不是正方体
具体节点Node的数据结构如下:
- bmin和bmax记录了这个节点对应的2D的Cube的大小
- i,若i>=0,则节点是叶子节点,i的值为这一部分三角形在BVH的tris数组中的位置索引,若 i<0,则节点不是叶子节点,此时i的大小代表包括自身在内的节点数量,比如一个BVH树一共有15个节点,则根节点的i值为-15
- n,如果是叶节点,则n代表叶节点所含的三角形个数,如果不是叶节点,则n这个值没有意义
struct rcChunkyTriMeshNode
{
float bmin[2];//size of the box of the node
float bmax[2];
int i;//i>0,means leaf node ,i<0 means non-leaf node
int n;//the number of triangles the node contains
};
创建BVH
在创建BVH树之前,需要拿到场景的三角形数据float *verts
和三角面数据int* tris
,具体步骤如下:
1. 创建BVH树
创建BVH,同时分配内存,BVH的数据信息为Nodes节点的信息和存储的三角形索引数组信息,代码如下所示:
// 三角形数除以每个chunk的三角形数,得到chunk数
int nchunks = (ntris + trisPerChunk-1) / trisPerChunk;
// 由于每个节点存储的三角形数在[trisPerChunk/2, trisPerChunk]间
// trisPerChunk+1个三角形就能占用两个叶子Node和两个父节点的内存
// 所以每个chunk需要分配四个Node节点,保证bvh有足够的内存
cm->nodes = new rcChunkyTriMeshNode[nchunks*4];
if (!cm->nodes)
return false;
// 为VBH树分配了tris的内存
cm->tris = new int[ntris*3];
if (!cm->tris)
return false;
cm->ntris = ntris;
2. 获取所有三角形的AABB包围盒
new一个数组,数组大小为场景里三角形的个数,这个数组用来记录所有三角形的2D的AABB,代码如下所示:
// 代表2D的AABB
struct BoundsItem
{
float bmin[2];
float bmax[2];
int i;
};
// 为每一个模型里的三角形,创建一个2D的AABB
// Build tree
BoundsItem* items = new BoundsItem[ntris];
if (!items)
return false;
// 遍历每一个三角形,计算其AABB,并记录三角形在原tris里的索引
for (int i = 0; i < ntris; i++)
{
const int* t = &tris[i*3];
BoundsItem& it = items[i];
it.i = i;// Bounds记录了该Bounds对应的三角形在源数据中的索引
// Calc triangle XZ bounds.
it.bmin[0] = it.bmax[0] = verts[t[0]*3+0];//x方向
it.bmin[1] = it.bmax[1] = verts[t[0]*3+2];//z方向
for (int j = 1; j < 3; ++j)
{
const float* v = &verts[t[j]*3];
if (v[0] < it.bmin[0]) it.bmin[0] = v[0];
if (v[2] < it.bmin[1]) it.bmin[1] = v[2];
if (v[0] > it.bmax[0]) it.bmax[0] = v[0];
if (v[2] > it.bmax[1]) it.bmax[1] = v[2];
}
}
3. 进行树的划分
这里划分的目标就是三角形,而且这些三角形都在树的tris数组里,具体的划分方法也很简单,当前节点从树的根节点开始:
- 对于一个三角形集合,如果这个集合的三角形个数小于一个节点能存放的最大三角形个数,则当前节点为叶节点,节点存储的三角形集合在bvh的tris数组的[node.i, node.i + node.n)范围内,同时节点需要记录存储这些三角形的最小AABB。
- 对于一个三角形集合,如果这个集合的三角形个数大于一个节点能存放的最大三角形个数,则当前节点为非叶节点,需要把这些三角形进行分类,这里根据三角形对应AABB的bmin的某一值进行排序后,再把原来的三角形集合一分为二,再次进行上一步的处理
所以这是一个深度递归的过程,建立BVH树的代码如下:
int curTri = 0;
int curNode = 0;
// 划分树
subdivide(items, ntris, 0, ntris, trisPerChunk, curNode, cm->nodes, nchunks*4, curTri, cm->tris, tris);
subdivide函数如下:
// 这是一个递归函数,用来创建BVH树,输入的items代表三角形对应的bounds数组
static void subdivide(BoundsItem* items, int nitems, int imin, int imax, int trisPerChunk,
int& curNode, rcChunkyTriMeshNode* nodes, const int maxNodes,
int& curTri, int* outTris, const int* inTris)
{
// imin和imax代表在tris里的范围,也就是处理tris[imin]到tris[imax]这一范围内的三角形
int inum = imax - imin;
int icur = curNode;
if (curNode > maxNodes)
return;
// 获取树里面的curNode索引对应的Node节点
rcChunkyTriMeshNode& node = nodes[curNode++];
// 如果所给的范围内的三角形数小于一个Cube能存储的最大的三角形数,那就全放到这个节点下
if (inum <= trisPerChunk)
{
// 计算这一块区间的AABB对应的总AABB
// Leaf
calcExtends(items, nitems, imin, imax, node.bmin, node.bmax);
// Copy triangles.
node.i = curTri;// 记录下这一Cube范围内对应的Node所含的三角形索引在源数据中的位置
node.n = inum;// 记录下这一节点内三角形的个数
// 存储三角形的索引数据,放到bvh树里
for (int i = imin; i < imax; ++i)
{
const int* src = &inTris[items[i].i*3];
int* dst = &outTris[curTri*3];
curTri++;
dst[0] = src[0];
dst[1] = src[1];
dst[2] = src[2];
}
}
else
{
// 如果不可以放在同一个cube里,那么需要进行二分
// Split
calcExtends(items, nitems, imin, imax, node.bmin, node.bmax);
int axis = longestAxis(node.bmax[0] - node.bmin[0],
node.bmax[1] - node.bmin[1]);
// 沿着长边进行排序
if (axis == 0)
{
// Sort along x-axis,根据BoundsItem的bmin的x从小到大,把Items进行排序
qsort(items+imin, static_cast<size_t>(inum), sizeof(BoundsItem), compareItemX);
}
else if (axis == 1)
{
// Sort along y-axis
qsort(items+imin, static_cast<size_t>(inum), sizeof(BoundsItem), compareItemY);
}
// 取中点,对两边各自建立新的树
int isplit = imin+inum/2;
// Left
subdivide(items, nitems, imin, isplit, trisPerChunk, curNode, nodes, maxNodes, curTri, outTris, inTris);
// Right
subdivide(items, nitems, isplit, imax, trisPerChunk, curNode, nodes, maxNodes, curTri, outTris, inTris);
// 当走到这里的时候,划分子树,说明此节点不为叶节点,所以其i值是负的
int iescape = curNode - icur;
// Negative index means escape.实际上就是该节点下的所有子节点数(包括该节点自身)
node.i = -iescape;
}
}
BVH树的应用
至此,一个BVH树就创建完了,代码也不复杂,下面举一个具体应用的代码,给点一个2D的长方形,找到与其相交的叶子节点(如果再想找与其相交的三角形,再去遍历其内部的三角形就可以了)
int rcGetChunksOverlappingSegment(const rcChunkyTriMesh* cm, float p[2], float q[2], int* ids, const int maxIds)
{
// Traverse tree
int i = 0;
int n = 0;
while (i < cm->nnodes)
{
const rcChunkyTriMeshNode* node = &cm->nodes[i];
const bool overlap = checkOverlapSegment(p, q, node->bmin, node->bmax);
const bool isLeafNode = node->i >= 0;
if (isLeafNode && overlap)
{
if (n < maxIds)
{
ids[n] = i;
n++;
}
}
if (overlap || isLeafNode)
i++;
else
{
// 如果父节点的AABB与Rect不相交,说明所有的子节点也不相交,可以直接跳过子树部分
const int escapeIndex = -node->i;
i += escapeIndex;
}
}
return n;
}
一点疑问
BVH的子树的AABB是否相交
我认为这里的AABB应该是有可能相交的,因为排序三角形的时候是按照其AABB的bmin的x或z值排序的,三角形如果很大,可能会导致一个非叶节点下的俩叶子节点的AABB相交,如下代码所示:
static int compareItemX(const void* va, const void* vb)
{
const BoundsItem* a = (const BoundsItem*)va;
const BoundsItem* b = (const BoundsItem*)vb;
if (a->bmin[0] < b->bmin[0])//排序只看bmin的x值
return -1;
if (a->bmin[0] > b->bmin[0])
return 1;
return 0;
}
static int compareItemY(const void* va, const void* vb)
{
const BoundsItem* a = (const BoundsItem*)va;
const BoundsItem* b = (const BoundsItem*)vb;
if (a->bmin[1] < b->bmin[1])
return -1;
if (a->bmin[1] > b->bmin[1])
return 1;
return 0;
}
// 如果不可以放在同一个cube里,那么需要进行切分
// Split
calcExtends(items, nitems, imin, imax, node.bmin, node.bmax);
int axis = longestAxis(node.bmax[0] - node.bmin[0], node.bmax[1] - node.bmin[1]);
// 沿着长边进行排序
if (axis == 0)
{
// Sort along x-axis,根据BoundsItem的bmin的x从小到大,把Items进行排序
qsort(items+imin, static_cast<size_t>(inum), sizeof(BoundsItem), compareItemX);
}
else if (axis == 1)
{
// Sort along y-axis
qsort(items+imin, static_cast<size_t>(inum), sizeof(BoundsItem), compareItemY);
}
可参考的链接
https://www.jianshu.com/p/074a4f7aca59
https://blog.csdn.net/u012138730/article/details/79928505
https://www.cnblogs.com/lookof/p/3546320.html
https://www.cnblogs.com/wickedpriest/p/12269564.html
维基百科Bounding volume hierarchy
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本