DBVT 在bullet 引擎中是很基础且重要的一个数据结构,本质上是一个可以动态更新的AABB树。在bullet的远距阶段是很高效的碰撞检测数据结构(比较OOB,K- DOP)。是组成dbvtbroadphase的重要成员。
首先看看树中节点的定义
- struct btDbvtNode
- {
- btDbvtVolume volume; // point to the bounding volume
- btDbvtNode* parent; // point to parent node
- DBVT_INLINE bool isleaf() const { return(childs[1]==0); }
- DBVT_INLINE bool isinternal() const { return(!isleaf()); }
- union
- {
- btDbvtNode* childs[2]; // point to child nodes if node is internal type
- void* data; // point to Dbvtproxy object if node is leaf node
- int dataAsInt; // padding ?
- };
- };
很明显这是一个典型的二叉树节点,同时当node是内部节点时将指向2个子结点,node是叶子节点时,将指向用户定义数据(具体见后续分析)
接下来是DBVT 的部分基础定义
- struct btDbvt
- {
- // Fields
- btDbvtNode* m_root; // root node of the tree
- btDbvtNode* m_free; // node buffer last one deleted
- int m_lkhd; // number of look ahead
- int m_leaves; // number of nodes
- unsigned m_opath; // bitmap, mean the path to the node
- }
这里的look ahead 基本没有在代码中用到。 m_opath将在优化中使用,主要用于纪录通往特定节点的路径。
创建节点比较直接,唯一值得注意的就是利用到了m_free 这个最后被从树中删除的节点(节点分配内存未释放)
如果m_free仍未被释放就重复利用,节省一次malloc调用。
- static DBVT_INLINE btDbvtNode* createnode( btDbvt* pdbvt,
- btDbvtNode* parent,
- void* data)
- {
- btDbvtNode* node;
- //if the node pool is avaliable
- if(pdbvt->m_free)
- { node=pdbvt->m_free;pdbvt->m_free=0; } // if yes, use it and reset the pointer
- else
- { node=new(btAlignedAlloc(sizeof(btDbvtNode),16)) btDbvtNode(); } // otherwise alloc memory to node
- node->parent = parent; // set the parent
- node->data = data; // set the data
- node->childs[1] = 0; // set the right child pointer as null
- return(node);
- }
插入节点到树中较为复杂,主要算法是插入到树中距离被插入节点距离(曼哈顿距离)最近的节点,并且合成新的父节点,并且向上传导包围体的变化(复习一下AABB)。
删除节点和插入节点比较类似,主要算法是用兄弟节点替换父节点,同时向上传导产生的包围体变化。
节点排序,检查父节点和字节点对象的地址,如果父节点地址高于子节点,则交换父子节点,
- //make sure the parent's address is smaller than child node
- static DBVT_INLINE btDbvtNode* sort(btDbvtNode* n,btDbvtNode*& r) // r is reference
- {
- btDbvtNode* p=n->parent;
- btAssert(n->isinternal());
- if(p>n) //all idea is swap the postion betwwen p and n . if the n address is smaller than p address.
- {
- const int i=indexof(n);
- const int j=1-i;
- btDbvtNode* s=p->childs[j]; // get the sibling node
- btDbvtNode* q=p->parent; // get the grand father node
- btAssert(n==p->childs[i]); // confirm again!
- if(q) q->childs[indexof(p)]=n; else r=n;
- s->parent=n; // reset the sibling node's parent to node
- p->parent=n; // reset the parent's parent to node
- n->parent=q; // reset the node's parent to grand father
- p->childs[0]=n->childs[0]; //reset parent node' child node to node's child
- p->childs[1]=n->childs[1]; //reset parent node' child node to node's child
- n->childs[0]->parent=p; //reset node's child node's parent node to parent
- n->childs[1]->parent=p; //reset node's child node's parent node to parent
- n->childs[i]=p; //reset node's child to parent node
- n->childs[j]=s; //reset node's child to parent node
- btSwap(p->volume,n->volume); //swap the volume
- return(p); //make sure return the greater one
- }
- return(n); //make sure return the greater one
- }
开始阅读前可以先浏览一下之前框架分析
在物理模拟的场景中, 一个节点首先应该具备2种属性
- 物理属性 包含 质量,速度,惯性,加速度,角速度,约束,摩擦系数 等
- 几何属性 形状, 包围体层次,碰撞检测类型掩码。world 变换。
物理属性大部分都在collosionObject 和rigidObjec 这样的类之中。
几何属性则又一次被细分为包围体层次和形状,碰撞检测类型掩码。
为了进一步介绍Dbvt的核心函数 btDbvt::collideTV
必须首先引入btDbvtProxy。btDbvtProxy 继承自 btBroadphaseProxy。
- ATTRIBUTE_ALIGNED16(struct) btBroadphaseProxy
- {
- ///optional filtering to cull potential collisions
- enum CollisionFilterGroups
- {
- DefaultFilter = 1,
- StaticFilter = 2,
- KinematicFilter = 4,
- DebrisFilter = 8,
- SensorTrigger = 16,
- CharacterFilter = 32,
- AllFilter = -1 //all bits sets: DefaultFilter | StaticFilter | KinematicFilter | DebrisFilter | SensorTrigger
- };
- void* m_clientObject;//Usually the client btCollisionObject or Rigidbody class
- short int m_collisionFilterGroup; //碰撞检测类型
- short int m_collisionFilterMask; //碰撞检测类型掩码
- void * m_multiSapParentProxy; //sweep and prune broadphase 测试研究用
- int m_uniqueId;//m_uniqueId 在重叠节点管理结构中要用到,非必须
- btVector3 m_aabbMin; //store the volume
- btVector3 m_aabbMax; //store the volume
- }
btBroadphaseProxy 基类包含了最基本的碰撞检测类型以及对象本身支持的碰撞检测类型,关于碰撞检测类型的详细描述可以参见bullet手册的第5章节有详细描述。对象可以根据支持的碰撞检测类型选择碰撞的对象。还包含了指向物理属性对象的指针。
再看btDbvtProxy
- struct btDbvtProxy : btBroadphaseProxy
- {
- /* Fields */
- btDbvtNode* leaf; //指向Dbvt 中对应的节点
- btDbvtProxy* links[2]; //指向双向列表的前后节点 见btDbvtBroadphase分析
- int stage; //所处的队列 见btDbvtBroadphase分析
- }
btDbvtProxy 是一个节点,利用了代理的设计模式,连接物理属性节点与几何节点。
本身还是重合节点管理的重要组成部分。然后再看看btDbvtTreeCollider
- /* Tree collider */
- struct btDbvtTreeCollider : btDbvt::ICollide
- {
- btDbvtBroadphase* pbp;
- btDbvtProxy* proxy;
- void Process(const btDbvtNode* n)
- {
- Process(n,proxy->leaf);
- }
- }
btDbvtTreeCollider 定义了当两个几何节点在远距状态下碰撞后的处理方法,btDbvt::ICollide 定义的是接口标准btDbvtTreeCollider 是在dbvt下的一个具体实现,开发者也可以根据自己的需要嵌入不同的处理逻辑。proxy代表的是主节点,n代表的是被比较节点。
现在进入主题 btDbvt::collideTV。这个函数的主要算法是遍历所有和目标节点重合的叶子节点,然后使用已有的处理逻辑模块 btDbvtTreeCollider 来处理这两个节点。会引入新的数据结构btHashedOverlappingPairCache,可以参见后续的btDbvtBroadphase分析
- btDbvt::collideTV(const btDbvtNode* root,
- const btDbvtVolume& vol,
- DBVT_IPOLICY)
- {
- ATTRIBUTE_ALIGNED16(btDbvtVolume) volume(vol);
- btAlignedObjectArray<const btDbvtNode*> stack;
- stack.resize(0);
- stack.reserve(SIMPLE_STACKSIZE); //生成一个栈用来遍历Dbvt
- stack.push_back(root); //根节点入栈
- do {
- const btDbvtNode* n=stack[stack.size()-1]; // 栈顶元素出栈
- stack.pop_back(); // 栈顶元素出栈
- if(Intersect(n->volume,volume))// 如果这个节点包含了目标节点
- {
- if(n->isinternal())// 同时是内部节点,则遍历其子树
- {
- stack.push_back(n->childs[0]); //左孩子入栈
- stack.push_back(n->childs[1]); //右孩子入栈
- }
- else //如果已经是叶子节点
- {
- policy.Process(n); // 利用既有的处理策略来处理这一对碰撞的节点
- }
- }
- } while(stack.size()>0); // run until all intersect leaf node is visited .
- }
接着上次关于Dbvt得分析,不过漏掉了一个比较重要的函数
function btDbvt:: collideTTpersistentStack, btDbvt::collideTT
* btDbvt:: collideTTpersistentStack. 负责进行两个dbvt子树的比较,找出两个子树中重叠的节点对,基于一个全局栈(一个成员变量实例)
* btDbvt::collideTT. 负责进行两个dbvt子树的比较,找出两个子树中重叠的节点对,但是基于的是一个局部栈(函数调用结束则释放)。
* btDbvt::collideTV. 负责在一个树中搜索和对应包围体重叠的节点。
btDbvt:: collideTTpersistentStack 的算法主要是遍历两个目标子树所有可能重合的节点对(基于栈的遍历)只要两个节点中有一个不是叶子节点则压入栈中直到两个节点都为叶子节点且相互重叠,都调用预先制定好的碰撞逻辑来处理。
- /* Stack element Node pair*/
- struct sStkNN
- {
- const btDbvtNode* a;
- const btDbvtNode* b;
- }
- m_stkStack.resize(DOUBLE_STACKSIZE);
- m_stkStack[0]=sStkNN(root0,root1); // push both sub tree root nodes into stack
- do {
- sStkNN p=m_stkStack[--depth]; //pop out the top of stack
- if(depth>treshold)//dynamic expand stack size
- {
- m_stkStack.resize(m_stkStack.size()*2);
- treshold=m_stkStack.size()-4;
- }
- if(p.a==p.b) // If compare the same sub-tree
- {
- if(p.a->isinternal())//and if node is internal node
- {
- //push 3 possible branch into stack
- //1 compare the same left sub tree
- m_stkStack[depth++]=sStkNN(p.a->childs[0],p.a->childs[0]);
- //2 compare the same right sub tree
- m_stkStack[depth++]=sStkNN(p.a->childs[1],p.a->childs[1]);
- //3 compare the left subtree and right subtree
- m_stkStack[depth++]=sStkNN(p.a->childs[0],p.a->childs[1]);
- }
- }
- //if compare different sub-tree then test if the
- // the volume is intersect
- else if(Intersect(p.a->volume,p.b->volume))
- {
- if(p.a->isinternal()) // if a is internal node
- {
- if(p.b->isinternal())
- {
- //if both node are internal node
- //push 4 possible branch into stack
- //1 compare the a left and b left subtree
- m_stkStack[depth++]=sStkNN(p.a->childs[0],p.b->childs[0]);
- //2 compare the a right and b left subtree
- m_stkStack[depth++]=sStkNN(p.a->childs[1],p.b->childs[0]);
- //3 compare the a left and b right subtree
- m_stkStack[depth++]=sStkNN(p.a->childs[0],p.b->childs[1]);
- //4 compare the a right and b right subtree
- m_stkStack[depth++]=sStkNN(p.a->childs[1],p.b->childs[1]);
- }
- else //if b is leaf node
- {
- //1 compare the a left and b node
- m_stkStack[depth++]=sStkNN(p.a->childs[0],p.b);
- //1 compare the a right and b node
- m_stkStack[depth++]=sStkNN(p.a->childs[1],p.b);
- }
- }
- else // if a is leaf node
- {
- if(p.b->isinternal()) // if b is internal node
- {
- //1 compare a and b left sub tree
- m_stkStack[depth++]=sStkNN(p.a,p.b->childs[0]);
- //2 compare a and b right sub tree
- m_stkStack[depth++]=sStkNN(p.a,p.b->childs[1]);
- }
- else
- { //if both a and b is leaf node
- policy.Process(p.a,p.b);
- }
- }
- }
- } while(depth);
光线与AABB 相交检测:
这是一个非常经典的问题, <<real time collision detection>> 5.33章节有非常详尽的讨论。
下面是光线的方程
t是可变参数, P是光线的起始点 RayFrom, d是光线的方向向量
下面这个是平面的方程
向量n是平面的法向量,所以如果光线与任何一个平面相交,应该有如下等式
对于AABB来说是六个平面, 每个面的法向量为(0,0,1)(0,1,0) ..... 总之三个坐标有两个是0,另外的一个是1或者-1
AABB同时是3个平面槽的交集, 光线与AABB最多有2个相交点,一个是前景点,一个后景点,分别与两个平面相交。
特例是完全与一个平面重合。可以被认为是和相邻的两个平面相交。
btRayAabb2.是bullet中用于检测的相关函数
- SIMD_FORCE_INLINE bool btRayAabb2(const btVector3& rayFrom, // point P
- const btVector3& rayInvDirection, // Direction Vector inverse
- const unsigned int raySign[3],
- const btVector3 bounds[2], // min x, max x AABB volume
- btScalar& tmin,
- btScalar lambda_min,
- btScalar lambda_max)
- {
- btScalar tmax, tymin, tymax, tzmin, tzmax;
- // get the far plane intersect param t along x axis
- tmax = (bounds[1-raySign[0]].getX() - rayFrom.getX()) * rayInvDirection.getX();
- // get the near plane intersect param t along y axis
- tymin = (bounds[raySign[1]].getY() - rayFrom.getY()) * rayInvDirection.getY();
- // get the far plane intersect param t along y axis
- tymax = (bounds[1-raySign[1]].getY() - rayFrom.getY()) * rayInvDirection.getY();
- // if it is not intersect with any plane then exit
- if ( (tmin > tymax) || (tymin > tmax) )
- return false;
- if (tymin > tmin)
- tmin = tymin; // update the tmin
- if (tymax < tmax)
- tmax = tymax; // update tmax
- // get the near plane intersect param t along Z axis
- tzmin = (bounds[raySign[2]].getZ() - rayFrom.getZ()) * rayInvDirection.getZ();
- // get the far plane intersect param t along Z axis
- tzmax = (bounds[1-raySign[2]].getZ() - rayFrom.getZ()) * rayInvDirection.getZ();
- // if it is not intersect with any plane then exit
- if ( (tmin > tzmax) || (tzmin > tmax) )
- return false;
- // caculate the interval
- if (tzmin > tmin)
- tmin = tzmin; // if find nearer point update tmin
- if (tzmax < tmax)
- tmax = tzmax; // if found the farer point update tmax
- return ( (tmin < lambda_max) && (tmax > lambda_min) );
- }
现在可以讨论ayTestInternal 函数, 这个函数基本的算法就是遍历所有的节点(基于栈)
光线讲和树中的每个节点做相交测试,如果相交就继续处理对应的子结点,否则就跳过。
对应所有最终的相交叶子节点,调用相交处理逻辑(回调函数)来处理。
- do
- {
- //pop out the top of stack
- const btDbvtNode* node=stack[--depth];
- //set up the AABB BOX
- bounds[0] = node->volume.Mins()-aabbMax;
- bounds[1] = node->volume.Maxs()-aabbMin;
- btScalar tmin=1.f,lambda_min=0.f;
- unsigned int result1=false;
- // Do the intersect tes t!!
- result1 = btRayAabb2(rayFrom,rayDirectionInverse,
- signs,bounds,tmin,lambda_min,
- lambda_max);
- if(result1)
- {
- //if test pass
- if(node->isinternal())
- { //if node is intertal
- if(depth>treshold) //dynamic expand stack
- {
- stack.resize(stack.size()*2);
- treshold=stack.size()-2;
- }
- //push the left child into stack
- stack[depth++]=node->childs[0];
- //push the right child into stack
- stack[depth++]=node->childs[1];
- }
- else
- {
- //if node is leaf node,then process it by callback
- policy.Process(node);
- }
- }
- } while(depth);