目录
17 前言 17.1 和射线的碰撞检测 17.2 使用BSP树的动态碰撞检测 17.3 一般层次的碰撞检测 17.3.2 不同层之间的碰撞检测 17.3.3 代价函数 17.4 OBB树 17.5 多重物体碰撞检测系统 17.5.1 广阶段的碰撞检测 17.5.2 总结 17.6 更多样的话题 17.6.1 及时碰撞检测 17.6.2 距离查询 17.6.3 可变形的模型 17.6.4 碰撞响应 17.6.5 基于GPU的碰撞检测 |
一般层次的碰撞检测
这一部分将会展现检测两个给定模型的碰撞检测算法中一般的方法。这些算法有前言中介绍的4个特点。这些算法的共性是:
○ 为每个模型创建一个包围盒。
○ 碰撞查询的高层次的代码是类似的,不管使用了哪种包围盒。
○ 可以使用一个简单的代价函数来修剪、评估以及比较算法表现。
这些点在接下来的三个小部分中具体涉及。
分层(hierarchy)的构建


主要有三种的方式来创建一个分层:自底向上的方法,渐进树插(incremental tree-insertion),或者自顶向下方法。为了创建更有效,包得更紧的结构体,我们总是尽可能减少包围盒的区域以及体积。对于碰撞检测,缩小包围盒体积是有意义的,因为一个包围盒需要和另一个包围盒进行检测。其中第一种自底向上的方法,是从合并基元并为它们寻找包围盒开始的。这些基元应当靠得足够近,这可以由基元之间的距离来衡量。然后,新的包围盒要么是以同样的方式创建的,要么是使用已存在的包围盒和一个或多个其它包围盒用相似的方式构建在一起,因而产生了一个新的,更大的父包围盒。我们不断重复这一步,直到只存在一个包围盒,它就成为这个分层的根节点。在这种方式下,靠得近的基元总是与在包围盒里与彼此接近。Barequest et al. Present BOXTREE(???),一个可以执行光线追踪和碰撞检测数据结构,就是自底向上构建的。
渐进树插方法是从一棵空树开始的。然后剩下的基元以及它们的包围盒是一个个插入到树中的。下图中有了具体的阐释。为了创建高效的树,我们需要找到树上的一个插入点。我们应当选择使得整个树体积增加最慢的点。达到这一点的一个简单的方法是,我们下降到树中有更小增长的孩子。这一类的算法通常要花费O(nlogn)的时间。打乱基元插入的顺序可以使树的形成结构更好。对于更复杂的方法,可以看Omohundro的一些作品。它在碰撞检测这一内容里并不涉及渐进树插,但是渐进树插已经在光线追踪以及相交查询中有比较好的成果了,所以它对碰撞检测而言运行的效果也应当一样好。
左图中,显示了一个有三个结点的二叉树。我们假设每个叶结点只存储了一个基元。现在我们想要插入一个新的结点,叫做N。这个结点包含一个包围盒,包围盒中有一个基元。因此,这个结点会作为叶结点插入。在这个例子中,如果我们在C处插入N结点的话(而不是B),我们认为我们找到了更小的树体积,然后创建一个包含C和新节点N的新的父节点P。
用于大多数分层构建算法的自顶向下方法,是由找到模型的所有基元的包围盒开始的,它被当做树的根节点。然后再使用分治的方法,包围盒首先被分为k个或者更小的部分。对于每个部分而言,我们找到其包含的所有基元,然后再以和根同样的方式创建包围盒。i.e.分层是递归创建的。通常会去找基元被划分时应当沿着的坐标,然后再在这个坐标上找一个好的划分点。在第16章第四部分讨论了几何概型,这可以用于在坐标轴上搜索好的分割点,这样就能得到更好的包围盒。注意自顶向下方法的一个潜在优点是可以惰性创建一个层,i.e.根据需要的基础上。这意味着我们仅仅为真正需要的场景部分创建层。但是由于这一创建过程是在运行时完成的,当层次的一部分创建出来后,性能会大幅度下降。这对于要求实时性的游戏一样的应用是不能接受的,但是对于CAD应用或者像路线规划、动画以及其它的离线计算而言,则节省了大量的时间和内存。
碰撞检测算法的一个挑战是找到更紧的包围盒以及能创建出更平衡更高效的树的方法。注意在所有情况下,平衡树的平均性能是最好的,因为每个叶结点的深度都是一样的(或者几乎是一样的)。这意味着它在层向下遍历到叶的时候花费相等的时间,碰撞查询的时间不会随着叶结点的不同而变化。在这样的情况下,平衡树是最优的。但是,这并不意味着它对所有的输入都是最好的。例如,如果模型的某一部分很少或者从不涉及到碰撞查询,那么这一部分可以在不平衡树中处在比较深的位置,所以查询更频繁的部分可以更接近于根。这一OBB树过程的细节在第四部分有具体介绍。
同样注意,在第14章第一部分介绍了一些空间数据结构的加速算法,包围盒的创建则在16章第三部分介绍。
不同层之间的碰撞检测
一般而言,有两种我们想要在不同时间测试的不同情况。第一种,我们可能仅仅对两个模型是否碰撞了感兴趣,那么这一方法可在找到了一对三角形的重叠部分后终止。第二种,我们可能希望找出所有对重叠的三角形。第一个问题的解决叫做碰撞检测,第二个问题的解决叫做碰撞决定。在这里,给出了解决第一种方法的伪代码。第二种情况可以利用给出的代码做一些小改动得到,这在一会儿后会讨论到。
A和B是模型层次的两个节点,第一次调用的是模型的根节点。和
适用于访问合适结点的包围盒。回忆
是结点A的孩子结点的集合,而
则用于给定一个叶结点的三角形(在伪代码中,每个结点只对应一个三角形)。主要的想法是,当检测到重叠后,开启一个(更大的)包围盒并且递归的检测它的内容。
正如我们能在伪代码中看到的一样,代码中的一部分是能共用的,为了能够表达算法如何工作,它被表达成这样。有一些行需要特别需注意。1,2行考虑了两个结点都是叶结点的情况。3-10行则处理两个结点都是内部结点情况。Volume(A)>Volume(B)比较的结果是有着最大下降体积的结点。这样一个测试背后的想法是,在调用FindFirstHitCD(A,B)和FindFirstHitCD(B,A)后得到相同的树的遍历,所以遍历的结果是确定的。考虑到第一种碰撞检测有特殊的状态(提早结束,碰撞响应等),这一点是很重要的。也许更加重要的是这趋向于带来更好的性能,因为在每一步都先访问最大的盒子。另外一个想法是在下降的A和B间交替选择。这避免了体积计算,所以速度更快了。或者说,对于每个刚体而言,体积是可以提前计算的,但是这需要每个结点更多的内存。同样,注意对许多包围盒而言,实际的体积是不需要计算的,因为只需要计算“Volue order”就足够了。例如,对于球体而言,只要计算出半径就足够了。
为了找到碰撞的所有对三角形,伪代码可以这样修改。算法找到的三角形对存储在全局的链表L中,初始化为空。然后修改第2行:如果通过了测试,这个程序应当往L中加入一对三角形(而不是返回)。当递归结束后,所有的碰撞都能在L中找到。
代价函数
下面方程中的函数t,一开始被介绍为一个光线追踪加速算法中评价层次包围盒结构性能的框架。它也因此被用于评价碰撞检测算法性能,同时,它增加了最后一项,引入某些可能对性能有重大影响的新代价。这一估计的结果源于这样一个事实,如果模型正在进行刚体运动,那么它的包围盒以及它的部分或者全部层次都需要根据运动和包围盒的选择而重新计算。
这里,参数代表:
Nv : 包围盒重叠测试的个数
Cv:包围盒重叠测试的代价
Np:用于重叠测试的基元对对个数
Cp : 检测两个基元是否有重叠的代价
Nu : 根据模型运动更新的包围盒个数
Cu :更新一个包围盒的代价
创建一个更好的模型层分解可以导致更小的Nv,Np,Nu。创建决定两个包围盒或者三角形重叠是否有更小的Cv和Cp的方法。但是,这两个目标经常是矛盾的,因为改变包围盒类型以获得更快的重叠测试通常意味着我们得到了包的更松的包围盒。
过去用过的不同包围盒包括球体,轴向包围盒(AABB),方向包围盒(OBB),k-DOPs(离散方向多面体),扇形区,球壳(非常适合于Bezier面片),line swept spheres(LSSs),rectangle swept sphere(RSSs),以及QuOSPOs(它集合了OBBs和k-DOPs的优点)。球体是变换起来最快的,它的重叠测试同样也很快,但是它们的适应性很差。AABBs有更好的适应性以及更快的重叠测试,如果模型中有大量轴向分布的几何体的话,这会是一个很好的的选择(在大多数建筑物模型中)。OBBs有更好的适应性,但是它的重叠检测更慢。而k-DOPs的适应性则取决于参数k——k越高适应性越好,重叠检测和变换速度也就越慢。对于k-DOP树的大多数信息而言,读者们可以去看Klosowsk的文章。
显然还有更多参数可以微调,以得到好的性能,这就是OBB树章节重点要介绍的内容了。
OBB树
在这一部分,我们将会展现首次由Gottschalk提出的OBB树,这是一个给碰撞检测领域带来巨大影响的数据结构。在碰撞检测中,两个平面非常靠近并且几乎平行的情况下,OBBs树表现得非常好。这一类情况通常发生在公差分析(tolerance analysis)和虚拟原型(virtual prototyping)上。
包围盒的选择
正如OBB树算法的名字所示,它所使用的包围盒是方向包围盒,OBB。这一选择的一个原因是之前使用的AABB(轴向包围盒)和球体的适应性太差。也就是说,它们在包住几何体时会留下很多空隙。OBB则对其包围的模型几何体相比AABB或球体有更好的聚拢性。
另一个原因是Gottschalk发明了一种判断两个OBBs是否重叠的新方法。这一方法比以往的方法要快得多。这一测试的速度主要是因为OBB经过了变换,其中之一转换成了处在原点的AABB包围盒。实际上的变换要花费63次计算,OBB之间的测试可能会在15次轴向测试中的某一次提早终止。变换之后,第一次轴向测试的终止要花费17次计算,而最后一个轴则要花费180次计算。OBB/OBB的重叠测试在章节16.13.5中介绍。
根据17.3.3中的代价估计框架,之前的推断意味着OBBs的nv和np相比要更小。
Van den Bergen提议了一个加速两个OBBs重叠测试的简单技术:仅仅忽略最后的九个轴向测试,它们对应着与第一个OBB的某条边和第二个OBB的某条边垂直的方向。这一测试通常被称为SAT lite。几何上,这可以被看做是两个AABB/AABB的测试,其中第一个测试是在第一个OBB的坐标系统上进行的,而另一个是在另一个OBB的坐标系统上进行的。这在下图中有进一步阐释。简化的OBB/OBB测试(忽略了最后九个轴向测试)有时候将会认为两个不相交的OBBs是重叠的。在这个例子中,OBB树中的递归可能比需要的走的更深。之所以要使用这样扩展的递归,是因为我们发现最后九个轴向测试只找出了重叠的很小一部分。忽略这些测试最终的结果是平均性能得到了提升。Van den Berge的技术在一个叫做SOLID的碰撞检测包中得到了应用,它同样也能操纵可变形对象。
在左边是两个OBBs(A和B),我们对其进行OBB/OBB重叠测试,忽略最后九个轴的测试。如果从几何上解释,那么OBB/OBB重叠可以近似成两个AABB/AABB重叠测试。中间的图解释了B可以怎样由A坐标系统中的AABB包围盒围住。C和A重叠,所以这个测试继续执行到右图。在这里,A被B坐标系统中的AABB包围盒围住,B和D并不重叠。所以,测试终止。但是,在三维上,D和B也可能重叠,但是A和B不会因为剩余的轴向检测而重叠。在这样的例子中,A和B被错误的认为是重叠的。
分层构建
主要的数据结构是一个二分树(16.2给出了定义),它的每个内部结点都存着一个OBB,每个外部结点(叶结点)仅存储一个三角形。由Gottschalk发明的自顶向下构建分层的方法,是分开去寻找一个多边形集合的紧OBB包围盒,然后将其沿着OBB某个轴向分割,这也就把三角形分成了两组。对于每一组,计算出一个新的OBB。OBB的创建在16.3中讨论。
我们计算完一个三角形集合的OBB后,包围盒以及三角形应当被分裂然后形成两个新的OBBs。Gottschalk使用的策略是选择包围盒所占据的更长的轴向,并且把它分为两个长度相同的部分。下图给出了这一过程的解释。一个包含包围盒中心点并且法线为包围盒最长轴的平面,被用于把三角形分为两个子集。被这个平面分割成两部分的三角形属于包含其质心的那一组。如果最长轴没有办法被分割(在极少的情况下,所有三角形的质心都处在分割平面上,或者所有质心都处在分割平面的同一侧),将会降序去尝试另一个轴向。
这张图显示了我们如何将几何体集合的OBB沿着其最长的轴向分解,也就是虚线标注的点。然后这个几何体被分解成两个字部分,每一部分都找到了一个新的OBB。将递归调用这个过程来创建OBB树。
对于每个子集而言,在16.3中介绍的矩阵方法被用于计算(子)OBBs。如果OBB选择了在中点处分裂,那么就获得了平衡树。
因为计算凸包要花费O(nlogn)时间,而二叉树深度为O(logn),所以总的创建OBB树的时间复杂度为
操控刚体运动
在OBB树层次中,每个OBB,A,和一个刚体变换(一个旋转矩阵 R和一个平移向量t)矩阵Ma存储在一起。这个矩阵保存了OBB和其父母的相对朝向和位置。
现在,假设我们开始测试两个OBBs,A和B。A和B之间的重叠测试应当在其中一个OBBs的坐标系统完成。假定我们在A的坐标系统进行测试。在这种方式下,A是一个中心在原点的AABB包围盒(在它自己的坐标系统下)。接下来的想法是把B转换到A的坐标系统。这是利用下面给出的矩阵完成的,它首先将B转换到自己的位置和朝向(用矩阵Mb),然后再转换到A的坐标系统(使用A转换的逆)。回顾对于刚体变换而言,其倒置就是逆,所以不需要额外的计算:
OBB/OBB的重叠测试将一个含有3X3旋转矩阵R和一个平移向量t的矩阵作为输入,它保存了B相对于A的朝向和位置,所以可按如下分解:
现在,假设A和B有重叠,那么我们想要下降到A的孩子C。一个聪明的做法如下。我们选择在C的坐标系统进行测试。想法是把B转换到A的坐标系统(用 ),然后再转换到C的坐标空间(使用
)。这是使用以下矩阵完成的,它将作为OBB/OBB重叠检测的输入:
这一过程将递归地调用以测试所有的OBBs。
杂谈
检测两个层次树的FindFirstHitCD伪代码(第三部分),可以用于以上的算法。需要修改的地方是overlap()函数,它应当指向一个检测两个OBBs重叠的程序。
OBB树涉及到的所有算法可以使用一个免费软件包RAPID演示。