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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值