以下为个人学习笔记整理。参考 PhysX SDK 3.4.0 文档,部分代码可能来源于更高版本。
# 「Scene Queries」AABBTree—— 下
下篇主要介绍 AABBTree 的增量版本 ——IncrementalAABBTree。阅读本文之前建议请先了解 「Scene Queries」AABBTree—— 上,不然很可能看的迷迷糊糊 (~ ̄▽ ̄)~
IncrementalAABBTree 相比于 AABBTree 最大的区别在于支持了插入删除时能够动态更新树结构(旋转操作,并且由于动态特性,一定程度上简化了数据结构,没那么扣扣嗖嗖了~
# IncrementalAABBTree 数据组成
比较简单,只有四个结构:
Ps::Pool<AABBTreeIndices> mIndicesPool;  | |
Ps::Pool<IncrementalAABBTreeNodePair> mNodesPool;  | |
IncrementalAABBTreeNode* mRoot;  | |
NodeAllocator                               mNodeAllocator; | 
- mIndicesPool:AABBTree.mIndices 的池化版本,支持动态增删,至于 AABBTreeIndices,稍后再做介绍。
 - mNodesPool:和 AABBTree.mRuntimePool 比较相似,都是存储最终构建的结果,不过为了支持动态增删做了一定程度的妥协,所以内存布局没那么紧凑,IncrementalAABBTreeNodePair 就是两个 IncrementalAABBTreeNode 的结构体。
 - mRoot:根节点。
 - mNodeAllocator:参考 AABBTree,功能用法完全一致。
 
# AABBTreeIndices
struct AABBTreeIndices  | |
{ | |
    PxU32               nbIndices; | |
PoolIndex indices[NB_OBJECTS_PER_NODE];  | |
};  | 
- nbIndices:记录节点内包围盒数量,有点类似 AABBTreeBuildNode.mNbPrimitives。
 - indices:记录每个包围盒对应于 AABBTreeBuildParams.mAABBArray 中下标。
 
# IncrementalAABBTreeNode
功能上和 AABBTreeRuntimeNode 近似,表示 IncrementalAABBTree 最小单位。不过节点大小是 AABBTreeRuntimeNode 的两倍(28 vs 56)。

mBVMax && mBVMin:节点包围盒数据。
mParent:父节点指针。
mChilds[2] / mIndices:union 类型。如果是非叶节点情况下,通过 mChilds[2] 指向左右子节点;如果是叶节点通过 mIndices 标识包围盒所在位置。辨别是否为叶节点可以通过 mChilds[1] 是否为 0 判定,因为两者都是指针类型,mIndices 只用到了第一个指针因此第二个是空缺的。
# IncrementalAABBTree 构建流程
IncrementalAABBTree 构建流程和 AABBTree 几乎一样,都是通过 AABBTreeBuildNode 的 _buildHierarchy 函数实现。具体细节可以参考 AABBTree—— 上,这里不再赘述。构建完成后有一个 clone 操作。
# clone
这一步主要是把 AABBTreeBuildNode 树形结构转为 IncrementalAABBTreeNode 树形结构,有点和 flatten 相同的意味。顺带还会生成一个 map 结构,用来映射 mAABBArray 下标和 IncrementalAABBTreeNode 对应叶节点指针,和 AABBTreeUpdateMap 比较相近。
# IncrementalAABBTree 结构图
最终构建完成后,mNodesPool 存储所有节点信息,叶节点还会关联到 mIndicesPool 中的 AABBTreeIndices,由于每个叶节点最多存储 4 个包围盒信息,因此 AABBTreeIndices 中又记录了每个包围盒的 bounds 下标。

# IncrementalAABBTree 增删改
IncrementalAABBTree 的增删改操作本身并不复杂,但是为了提高二叉树的查找效率,在变更二叉树节点的同时还需要考虑树的平衡性。
因此在介绍增删改之前,先来看看 IncrementalAABBTree 如何平衡化的:
# IncrementalAABBTree 平衡性
如何衡量一棵树是否平衡?常规的二叉树通过树的左右子树层级作为指标,如果两者层级相差超过一层则进行平衡(rotate)。
不过 AABBTree 的划分标准有所不同:由于 AABBTree 的构建是基于包围盒包含关系来的,因此平衡性的标准也需要有所调整。为了尽可能保证查找时所有叶节点命中的概率尽可能相似,使得 AABBTree 在抽象层面更加扁平。而查找的命中和 AABB 节点包围盒的「体积」成正相关,因此体积大小就成了衡量 AABBTree 平衡的标准。
# 相关概念
在介绍平衡性和旋转之前,先了解一些名字术语:
- largerNode && smallerNode:通常情况下,两个兄弟节点里其中一个的体积如果超过另一个的 3 倍,那么就把体积大的称为 largerNode,体积小的称为 smallerNode。
 - closestNode && remainingNode:如果已知一个 largerNode 和 smallerNode,那么 largerNode 中最靠近 smallerNode 的叶节点被称为 closestNode ,而 closestNode 的兄弟节点则叫做 remainingNode。
 - spotNode:表示 smallerNode 中最靠近 closestNode 的叶节点。
 

# traversalDirection && rotateTree
traversalDirection 操作用于检测两个兄弟节点是否平衡,并且计算出兄弟节点中距离测试点最近的和体积较大的。
PX_FORCE_INLINE static PxU32 traversalDirection(const IncrementalAABBTreeNode& child0, const IncrementalAABBTreeNode& child1, const Vec4V& testCenterV,  | |
bool testRotation, bool& rotateNode, PxU32& largesRotateNode)  | |
{ | |
	// traverse in the direction of a node which is closer | |
	// we compare the node and object centers | |
const Vec4V centerCh0V = V4Add(child0.mBVMax, child0.mBVMin);  | |
const Vec4V centerCh1V = V4Add(child1.mBVMax, child1.mBVMin);  | |
const Vec4V ch0D = V4Sub(testCenterV, centerCh0V);  | |
const Vec4V ch1D = V4Sub(testCenterV, centerCh1V);  | |
if(testRotation)  | |
	{ | |
		// if some volume is 3x larger than we do a rotation | |
const float volumeCompare = 3.0f;  | |
PX_ALIGN(16, PxVec4) sizeCh0;  | |
PX_ALIGN(16, PxVec4) sizeCh1;  | |
const Vec4V sizeCh0V = V4Sub(child0.mBVMax, child0.mBVMin);  | |
const Vec4V sizeCh1V = V4Sub(child1.mBVMax, child1.mBVMin);  | |
V4StoreA(sizeCh0V, &sizeCh0.x);  | |
V4StoreA(sizeCh1V, &sizeCh1.x);  | |
const float volumeCh0 = sizeCh0.x*sizeCh0.y*sizeCh0.z;  | |
const float volumeCh1 = sizeCh1.x*sizeCh1.y*sizeCh1.z;  | |
if((volumeCh0*volumeCompare < volumeCh1) || (volumeCh1*volumeCompare < volumeCh0))  | |
		{ | |
largesRotateNode = (volumeCh0 > volumeCh1) ? 0u : 1u;  | |
rotateNode = true;  | |
		} | |
	} | |
const BoolV con = FIsGrtr(V4Dot3(ch0D, ch0D), V4Dot3(ch1D, ch1D));  | |
return (BAllEqTTTT(con) == 1) ? PxU32(1) : PxU32(0);  | |
} | 
rotateTree 用来对树进行旋转。旋转的大体流程可以分为以下几个步骤:
- 【step.1】找出 largerNode 中最靠近 smallerNode 的叶节点 closestNode。
 - 【step.2】找出 smallerNode 中最靠近 closestNode 的叶节点 spotNode。
 - 【step.3】把 closestNode 合并到 spotNode,并调整 remainingNode 和 largerNode 的父子关系。到此第一步旋转就算完成了~
 - 【step.4】由于 spotNode 合并了 closestNode 后体积发生了较大变化,因此需要对 smallerNode 整体做个扫描,判定需不需要再旋转一次。如果需要的话,重复【step.1~3】。
 
调整 remainingNode 和 largerNode 的父子关系可能出现两种情况:
- remainingNode 是叶节点。
 - remainingNode 是非叶节点。
 

把 closestNode 合并到 spotNode 同样也存在两种情况:
- spotNode 合并了 closestNode 之后容量没超过上限。
 - spotNode 合并了 closestNode 之后容量超过上限。
 

讨论完 rotateTree,整个增删改操作就变得较为简单:
- 增:查询距离插入包围盒最近叶节点进行插入,有点类似节点合并。插入完成后如果发现节点需要平衡化,还会进行一次 rotateTree。
 - 删:删除其实更为简单,前面提到构建时有一个 map 映射,可以直接定位到对应叶节点进行删除,如果叶节点删除后为空,则再执行一次树结构调整,把兄弟节点替换掉父节点即可。
 - 改:该操作有两种模式 
update&&updateFast。更新操作本质上是「删」、「增」操作的组合。updateFast则是先对变更后的包围盒和原本的叶节点包围盒做一次相交性检查,如果相交的情况,则仅更新包围盒大小,不调整节点排布,应该是为了应对包围盒在小范围内移动的情况下,导致 IncrementalAABBTree 频繁增删而做的优化。 
# 总结
到此,IncrementalAABBTree 也介绍完毕了。AABBTree 追求极致的内存占用,在很多方面都精打细算,像一个精致的艺术品;而 IncrementalAABBTree 为了实现动态变化的灵活,频繁的增删改。牺牲了部分性能、提高了内存占用,但也在一定程度上简化了编码,像一个乐高玩具。两者可谓是各有优劣。