碰撞检测技术简介

无论是 PC 游戏,还是移动应用,碰撞检测始终是程序开发的难点,甚至可以用碰撞检测作为衡量游戏引擎是否完善的标准。

好的碰撞检测要求人物在场景中可以平滑移动,遇到一定高度的台阶可以自动上去,而过高的台阶则把人物挡住,遇到斜率较小的斜坡可以上去,斜率过大则会把人物挡住,在各种前进方向被挡住的情况下都要尽可能地让人物沿合理的方向滑动而不是被迫停下。

在满足这些要求的同时还要做到足够精确和稳定,防止人物在特殊情况下穿墙而掉出场景。

做碰撞检测时,该技术的重要性容易被人忽视,因为这符合日常生活中的常识。如果出现 Bug ,很容易被人发现,例如人物无缘无故被卡住不能前进或者人物穿越了障碍。所以,碰撞检测是让很多程序员头疼的算法,算法复杂,容易出错。

对于移动终端有限的运算能力,几乎不可能检测每个物体的多边形和顶点的穿透,那样的运算量对手机等设备来讲是不可完成的,所以移动游戏上使用的碰撞检测不可能使用太精确的检测,而且对于 3D 碰撞检测问题,还没有几乎完美的解决方案。目前只能根据需要来取舍运算速度和精确性。

目前成功商业 3D 游戏普遍采用的碰撞检测是 BSP 树及 AABB ( axially aligned bounding box )包装盒(球)方式。简单地讲, AABB 检测法就是采用一个描述用的立方体或者球形体包裹住 3D 物体对象的整体(或者是主要部分),之后根据包装盒的距离、位置等信息来计算是否发生碰撞,如图 10-24 所示。

除了球体和正方体以外,其他形状也可以作包装盒,但是相比计算量和方便性来讲还是立方体和球体更方便些,所以其他形状的包装只用在一些特殊场合使用。 BSP 树是用来控制检测顺序和方向的数据描述。

在一个游戏场景中可能存在很多物体,它们之间大多属于较远位置或者相对无关的状态,一个物体的碰撞运算没必要遍历这些物体,同时还可以节省重要的时间。

如果使用单步碰撞检测,需要注意当时间步长较大时会发生两个物体完全穿透而算法却未检测出来的问题,如图 10-25 所示。其解决方案是产生一个 4D 空间,在物体运动的开始和结束时间之间产生一个 4D 超多面体,用于穿透测试。

      

           图10-24  AABB 包装盒                   图10-25  碰撞检测的单步失控和4D 测试

读者在程序开发初期有必要对碰撞检测有一个初步的估计,以免最后把大量精力消耗在碰撞检测问题上,从而降低了在基础的图形编程之上的注意力。

10.3.2   球体碰撞检测

真实的物理模拟系统需要非常精确的碰撞检测算法,但是游戏中常常只需要较为简单的碰撞检测,因为只需要知道物体什么时候发生碰撞,而不用知道模型的哪个多边形发生了碰撞,因此可以将不规则的物体投影成较规则的物体进行碰撞检测。

球体只有一个自由度,其碰撞检测是最简单的数学模型,我们只需要知道两个球体的球心和半径就能进行检测。

那么球体碰撞是如何工作的?主要过程如下。

n     计算两个物体中心之间的距离,并且将其与两个球体的半径和进行比较。

n     如果距离大于半径和,则没有发生碰撞。

n     否则,如果距离小于半径和,则发生了物体碰撞。

考虑由球心 、 和半径 、 定义的两个球,如图 10-26 所示。设 为球心间的距离。很明显,当< +r 2 时相交,在实践中通过比较 < (r 1 +r 2 )2 ,可以避免包括计算 在内的平方根运算。

对两个运动的球进行碰撞检测要麻烦一些,假设两个球的运动向量为和 ,球与位移向量是一一对应的,它们描述了所讨论时间段中的运动方式。

事实上,物体的运动是相对的,例如两列在两条平行轨道上相向行驶的火车,在其中一列中观察,对方的速度是两车速度之和。同样,也可以从第一个球的角度来简化问题,假设第一个球是 “ 静止 ” 的,另一个是 “ 运动 ” 的,那么该运动向量等于原向量 和 之差,如图 10-27 所示。

图10-27  动态球的检测过程

球体碰撞的优点是非常适用于需要快速检测的游戏,因为它不需要精确的碰撞检测算法。执行速度相对较快,不会给 CPU 带来过大的计算负担。

球体碰撞的另一个劣势是只适用于近似球形物体,如果物体非常窄或者非常宽,该碰撞检测算法将会失效,因为会在物体实际发生碰撞之前,碰撞检测系统就发出碰撞信号,如图 10-28 所示是球体碰撞检测中可能出现的坏情况,其解决方法是缩小检测半径,或者使用其他检测模型,如图 10-29 所示。

            

                图10-28  球体碰撞的坏情况              图10-29  缩小检测半径

为了解决包容球精确度不高的问题,人们又提出了球体树的方法。

球体树实际上是一种表达 3D 物体的层次结构。对一个形状复杂的 3D 物体,先用一个大球体包容整个物体,然后对物体的各个主要部分用小一点的球体来表示,然后对更小的细节用更小的包容球体,这些球体和它们之间的层次关系就形成了一个球体树。

举例来说,对一个游戏中的人物角色,可以用一个大球来表示整个人,然后用中等大小的球体来表示四肢和躯干,然后用更小的球体来表示手脚等。这样在对两个物体进行碰撞检测时,先比较两个最大的球体。

如果有重叠,则沿树结构向下遍历,对小一点的球体进行比较,直到没有任何球体重叠,或者到了最小的球体,这个最小的球体所包含的部分就是碰撞的部分,如图 10-30 所示。

10.3.3   AABB 立方体边界框检测

用球体去近似地代表物体运算量很小,但在游戏中的大多数物体是方的或者长条形的,应该用方盒来代表物体。另一种常见的检测模型是立方体边界框,如图 10-31 展示了一个 AABB 检测盒和它里面的物体。

座标轴平行( Axially-aligned )不仅指盒体与世界座标轴平行,同时也指盒体的每个面都和一条座标轴垂直,这样一个基本信息就能减少转换盒体时操作的次数。 AABB 技术在当今的许多游戏中都得到了应用,开发者经常用它们作为模型的检测模型,再次指出,提高精度的同时也会降低速度。

因为 AABB 总是与座标轴平行,不能在旋转物体时简单地旋转 AABB ,而是应该在每一帧都重新计算。如果知道每个对象的内容,这个计算就不算困难,也不会降低游戏的速度。然而,还面临着精度的问题。

假如有一个 3D 的细长刚性直棒,并且要在每一帧动画中都重建它的 AABB 。可以看到每一帧中的包装盒都不一样而且精度也会随之改变,如图 10-32 所示。

       

    图10-31  3D 模型与AABB 检测盒                     图10-32  不同方向的AABB

可以注意到 AABB 对物体的方向很敏感,同一物体的不同方向, AABB 也可能不同(由于球体只有一个自由度,所以检测球对物体方向不敏感)。

当物体在场景中移动时,它的 AABB 也需要随之移动,当物体发生旋转时,有两种选择:用变换后的物体来重新计算 AABB ,或者对 AABB 做和物体同样的变换。

如果物体没有发生扭曲,可以通过 “ 变换后的 AABB” 重新计算,因为该方法要比通过 “ 变换后的物体 ” 计算快得多,因为 AABB 只有 8 个顶点。变换 AABB 得出新的 AABB 要比变换物体的运算量小,但是也会带来一定的误差,如图 10-33 所示。

比较图中原 AABB (灰色部分)和新 AABB (右边比较大的方框),它是通过旋转后的 AABB 计算得到的,新 AABB 几乎是原来AABB 的两倍,注意,如果从旋转后的物体而不是旋转后的 AABB来计算新 AABB ,它的大小将和原来的 AABB 相同。

先介绍 AABB 的表达方法, AABB 内的点满足以下条件:

min x ≤x max

min y ≤y max

min z ≤z max

因此只需要知道两个特别重要的顶点( min , min , min )、( max , max , max ),记作:

float[] min = new float []{0.0f,0.0f,0.0f};

float[] max = new float []{0.0f,0.0f,0.0f};

中心点是两个顶点的中点,代表了包装盒的质点。

float[] center = new float []{0.0f,0.0f,0.0f};

中心点的计算方法如下:

float [] center(){ 

  center[0] = (min[0] + max[0])*0.5f;

  center[1] = (min[1] + max[1])*0.5f;

  center[2] = (min[2] + max[2])*0.5f;

  return center;

}

通过这两个顶点可以知道以下属性。

float xSize() { return (max[0]-min[0]); }

float ySize() { return (max[1]-min[1]); }

float zSize() { return (max[2]-min[2]); }

float size(){ return (max[0]-min[0])*(max[1]-min[1])*(max[2]-min[2]);}

当添加一个顶点到包装盒时,需要先与这两个顶点进行比较。

void add(float []p) { 

   if (p[0] < min[0]) min[0] = p[0];

   if (p[0] > max[0]) max[0] = p[0];

   if (p[1] < min[1]) min[1] = p[1];

   if (p[1] > max[1]) max[1] = p[1];

   if (p[2] < min[2]) min[2] = p[2];

   if (p[2] > max[2]) max[2] = p[2];

}

检测包装盒是否为空,可以将这两个顶点进行比较。

boolean isEmpty() {

   return (min[0] > max[0]) || (min[1] > max[1]) || (min[2] > max[2]);

}

检测某个点是否属于 AABB 范围之内的代码如下:

boolean contains(float []p){ 

   return

    (p[0] >= min[0]) && (p[0] <= max[0]) &&

    (p[1] >= min[1]) && (p[1] <= max[1]) &&

    (p[2] >= min[2]) && (p[2] <= max[2]);

}

AABB 的静态检测比较简单,检测两个静止包装盒是否相交,它是一种布尔测试,测试结果只有相交或者不相交。这里我们还提供了获取相交范围信息的方法,一般来说,这种测试的目的是为了返回一个布尔值。碰撞的示意如图 10-34 所示。

图10-34  包装盒的碰撞

检测静态 AABB 碰撞的方法如下:

boolean intersectAABBs(AABB box2,AABB boxIntersect)

{

   float []box2_min = box2.getMin();

   float []box2_max = box2.getMax();

   if (min[0] > box2_max[0]) return false;

   if (max[0] < box2_min[0]) return false;

   if (min[1] > box2_max[1]) return false;

   if (max[1] < box2_min[1]) return false;

   if (min[2] > box2_max[2]) return false;

   if (max[2] < box2_min[2]) return false;

   if (boxIntersect != null) {

       float []box_intersect_min = new float[3];

       float []box_intersect_max = new float[3];

       box_intersect_min[0] = Math.max(min[0], box2_min[0]);

       box_intersect_max[0] = Math.min(max[0], box2_max[0]);

       box_intersect_min[1] = Math.max(min[1], box2_min[1]);

       box_intersect_max[1] = Math.min(max[1], box2_max[1]);

       box_intersect_min[2] = Math.max(min[2], box2_min[2]);

       box_intersect_max[2] = Math.min(max[2], box2_max[2]);

   }

   return true;

}

可以利用 AABB 的结构来加快新的 AABB 的计算速度,而不用变换 8 个顶点,再从这 8 个顶点中计算新AABB 。下面简单地回顾 4×4 矩阵变换一个 3D 点的过程。

通过原边界框( min , min , min , max , max , max )计算新边界框(  ,  ,  ,  ,  ,  ),现在的任务是计算  的速度。换句话说,希望找到 11 +m 12 y +m 13 z +m14 的最小值。其中 [x , , ] 是原 8 个顶点中的任意一个。

变换的目的是找出这些点经过变换后哪一个的 座标最小。看第一个乘积 11 ,为了最小化乘积,必须决定是用 min 还是 max 来替换其中的 。显然,如果 11 > 0 ,用 min 能得到最小化的乘积;如果 11< 0 ,则用 max 能得到最小化乘积。

比较方便的是,不管 min 还是 max 中哪一个被用来计算  ,都可以用另外一个来计算  。可以对矩阵中的 9 个元素中的每一个都应用这个计算过程(其他元素不影响大小)。

根据变换矩阵和原有的 AABB 包装盒计算新的 AABB 包装盒的代码如下:

void setToTransformedBox(Transform t)

{

   if (isEmpty()) {                              // 判断包装盒是否为空

      return;

   }

   float[] m = new float [16];

   t.get(m);                                     // 将变换矩阵存入数组

   float minx=0,miny=0,minz=0;

   float maxx=0,maxy=0,maxz=0;

   minx += m[3];                                 //x 方向上平移

   maxx += m[3];                                 //x 方向上平移

   miny += m[7];                                 //y 方向上平移

   maxy += m[7];                                 //y 方向上平移

   minz += m[11];                                //z 方向上平移

   maxz += m[11];                                //z 方向上平移

   if (m[0] > 0.0f) {

      minx += m[0] * min[0]; maxx += m[0] * max[0];

   } else {

      minx += m[0] * max[0]; maxx += m[0] * min[0];

   }

   if (m[1] > 0.0f) {

      minx += m[1] * min[1]; maxx += m[1] * max[1];

   } else {

      minx += m[1] * max[1]; maxx += m[1] * min[1];

   }

   if (m[2] > 0.0f) {

      minx += m[2] * min[2]; maxx += m[2] * max[2];

   } else {

      minx += m[2] * max[2]; maxx += m[2] * min[2];

   }

   if (m[4] > 0.0f) {

      miny += m[4] * min[0]; maxy += m[4] * max[0];

  } else {

      miny += m[4] * max[0]; maxy += m[4] * min[0];

   }

   if (m[5] > 0.0f) {

      miny += m[5] * min[1]; maxy += m[5] * max[1];

   } else {

      miny += m[5] * max[1]; maxy += m[5] * min[1];

   }

   if (m[6] > 0.0f) {

      miny += m[6] * min[2]; maxy += m[6] * max[2];

   } else {

      miny += m[6] * max[2]; maxy += m[6] * min[2];

   }

   if (m[8] > 0.0f) {

      minz += m[8] * min[0]; maxz += m[8] * max[0];

   } else {

      minz += m[8] * max[0]; maxz += m[8] * min[0];

   }

   if (m[9] > 0.0f) {

      minz += m[9] * min[1]; maxz += m[9] * max[1];

   } else {

      minz += m[9] * max[1]; maxz += m[9] * min[1];

   }

   if (m[10] > 0.0f) {

      minz += m[10] * min[2]; maxz += m[10] * max[2];

   } else {

      minz += m[10] * max[2]; maxz += m[10] * min[2];

   }

   min[0] = minx; min[1] = miny; min[2] = minz;    // 用新的AABB 座标替换原有座标

   max[0] = maxx; max[1] = maxy; max[2] = maxz;    // 用新的AABB 座标替换原有座标

}

为了使用 AABB 包装盒进行碰撞检测,将这些方法和属性封装为 AABB 类,代码如下:

import java.lang.Math;

import javax.microedition.m3g.Transform;

class AABB{ 

   public AABB(){}

   float [] getMin(){return min;}

   float [] getMax(){return max;}

   void setMin(float x,float y,float z){min[0]=x;min[1]=y;min[2]=z;}

   void setMax(float x,float y,float z){max[0]=x;max[1]=y;max[2]=z;}

   void reset(){

      for(int i =0;i<3;i++)

      {

         min[i]=0;

         max[i]=0;

      }

   }

   // 其他方法同上

}

为了检验碰撞检测的使用构造了两个立方体,并各自绑定了一个包装盒。

/************** 立方体1***************/

mesh1 = createCube();                            // 创建立方体1

mesh1.setTranslation(1.0f, 0.0f,0.0f) ;          // 平移

mesh1.setOrientation(90,0.0f,1.0f,0.0f);         // 旋转

mesh1.setScale(0.5f,0.5f,0.5f);                  // 缩放

box1 = new AABB();                               // 包装盒

box1.setMin(-1.0f,-1.0f,-1.0f);                  // 设置包装盒1 的最小顶点

box1.setMax(1.0f,1.0f,1.0f);                     // 设置包装盒1 的最大顶点

mesh1.getCompositeTransform(cubeTransform);      // 获取立方体1 的混合矩阵

box1.setToTransformedBox(cubeTransform);         // 将变换矩阵应用到包装盒中

world.addChild(mesh1);                           // 将立方体1 添加到场景中

/************** 立方体2***************/

mesh2 = createCube();                            // 创建立方体2

mesh2.setTranslation(-0.5f, 0.0f,0.0f) ;         // 平移

mesh2.setScale(0.5f,0.5f,0.5f);                  // 缩放

box2 = new AABB();                               // 包装盒

box2.setMin(-1.0f,-1.0f,-1.0f);                  // 设置包装盒2 的最小顶点

box2.setMax(1.0f,1.0f,1.0f);                     // 设置包装盒2 的最大顶点

mesh2.getCompositeTransform(cubeTransform);      // 获取立方体2 的混合矩阵

box2.setToTransformedBox(cubeTransform);         // 将变换矩阵应用到包装盒2 中

world.addChild(mesh2);                           // 将立方体2 添加到场景中

检测包装盒 1 和包装盒 2 是否碰撞的代码如下:

isCollided = box1.intersectAABBs(box2,null);     // 检测两个AABB 包装盒是否碰撞

编译运行程序,设置两个立方体不同的位置和角度,可以比较精确地检测出它们的碰撞情况,如图 10-35 所示。

检测两个静止 AABB 的碰撞情况比较简单,只需要在每一维上单独检查它们的重合程度即可。如果在所有维上都没有重合,那么这两个 AABB 就不会相交。

AABB 间的动态检测稍微复杂一些,考虑一个由顶点 min 和 max 指定的静态包装盒和一个由顶点 min max 指定的动态包装盒(如果两个都是动态的,可以根据相对运动视作如此)。运动的速度由向量 给出,运动时间 假定为 0 ~ 1 。

图10-35  静态物体碰撞检测示意

移动检测的目标是计算运动 AABB 碰撞到静态 AABB 的时刻,因此需要计算出两个 AABB 在所有维上的第一个点。为了简化起见,可以把上述问题先归结到某一维,然后再将三维结合到一起。假设把问题投影到 x轴,如图 10-36 所示。

图10-36  AABB 的动态检测

黑色矩形代表沿座标轴滑动的 AABB , =0 时,运动 AABB 完全位于静止 AABB 的左边。当 =1 时,运动AABB 完全位于静止 AABB 的右边。当 =tenter 时,两个 AABB 刚刚相交,当 =tleave 时,两个 AABB 脱离碰撞。

对照上图,可以推导出两个 AABB 接触和离开的时间:

 , 

AABB 的动态检测有 3 个要点。

n     如果速度为0 ,两个包装盒要么一直相交,要么一直分离。

n     不管物体从哪个方向运动,碰撞过程中,肯定是先入后出,所以有tenter tleave 

n     如果tenter tleave 超出运动时间范围,那么在此范围内它们是不相交的。

检测出某一维的碰撞还不够,还需要进行其他两维的检测,然后取结果的交集。如果交集为空,那么两 AABB包装盒没有相交,如果区间范围在时间段 [0 , 1] 之外,那么在此区间也不相交。对 AABB 进行动态检测的方法定义如下:

float intersectMovingAABB(AABB stationaryBox,AABB movingBox,float []s)

    float NoIntersection = 1e30f;                      // 没有碰撞则返回大数

    float tEnter = 0.0f;                               // 初始化碰撞时间

    float tLeave = 1.0f;                               // 初始化离开时间

    float Swap = 0.0f;                                 // 交换操作中间变量

    float [] sBoxmin= stationaryBox.getMin();          // 静止包装盒的最小值顶点

    float [] sBoxmax= stationaryBox.getMax();          // 静止包装盒的最大值顶点

    float [] mBoxmin= movingBox.getMin();              // 运动包装盒的最小值顶点

    float [] mBoxmax= movingBox.getMax();              // 运动包装盒的最大值顶点

    if (s[0] == 0.0f) {                                // 如果方向速度为0

       if ((sBoxmin[0] >= mBoxmax[0]) ||(sBoxmax[0] <= mBoxmin[0])) {

           return NoIntersection;                       // 进行静态检测

       }

    } else {

       float xEnter = (sBoxmin[0]-mBoxmax[0])/s[0];    // 计算碰撞时间

       float xLeave = (sBoxmax[0]-mBoxmin[0])/ s[0];   // 计算离开时间

       if (xEnter > xLeave) {                          // 检查顺序

           Swap = xEnter;

           xEnter = xLeave;

           xLeave = Swap;

       }

       if (xEnter > tEnter) tEnter = xEnter;           // 更新区间

       if (xLeave < tLeave) tLeave = xLeave;

       if (tEnter > tLeave) {                          // 是否导致空重叠区

           return NoIntersection;                       // 没有碰撞

       }

    }

    if (s[1] == 0.0f) {                                //y 轴速度为0

       if ( (sBoxmin[1] >= mBoxmax[1]) || (sBoxmax[1] <= mBoxmin[1])) {

           return NoIntersection;                       // 没有相交

       }

    } else {

       float yEnter = (sBoxmin[1]-mBoxmax[1]) / s[1];

       float yLeave = (sBoxmax[1]-mBoxmin[1]) / s[1];

       if (yEnter > yLeave) {

           Swap = yEnter;

           yEnter = yLeave;

           yLeave = Swap;

       }

       if (yEnter > tEnter) tEnter = yEnter;           // 更新区间

       if (yLeave < tLeave) tLeave = yLeave;

       if (tEnter > tLeave) {

           return NoIntersection;

       }

    }

    if (s[2] == 0.0f) {                                //z 方向速度为0

       if ((sBoxmin[2] >= mBoxmax[2]) ||(sBoxmax[2] <= mBoxmin[2])) {

           return NoIntersection;

       }

    } else {

       float oneOverD = 1.0f / s[2];

       float zEnter = (sBoxmin[2]-mBoxmax[2]) / s[2];

       float zLeave = (sBoxmax[2]- mBoxmin[2]) / s[2];

       if (zEnter > zLeave) {

           Swap = zEnter;

           zEnter = zLeave;

           zLeave = Swap;

       }

       if (zEnter > tEnter) tEnter = zEnter;           // 更新区间

       if (zLeave < tLeave) tLeave = zLeave;

       if (tEnter > tLeave) {

           return NoIntersection;

       }

    }

    return tEnter;                                     // 返回碰撞时间

}

为了对移动 AABB 进行检测,创建两个 AABB 如图 10-37 所示。两个包装盒距离 0.5 ,速度为 3 。

图10-37  移动AABB 检测

检测代码如下:

float[] speed = new float []{3.0f,0.0f,0.0f};

float tEnter = intersectMovingAABB(box1,box2,speed);

输出结果为 0.16667 ,完全符合预期的猜测。

10.3.4   OBB 树碰撞检测

前面提到了长条物体在旋转时 AABB 盒的变化,那么是否有能够在任意方向都更为精确的检测方式,答案是肯定的,这是一种基于 OBB 即定向包容盒子( Oriented Bounding Box,OBB )的技术,它已经广泛用于光线追踪和碰撞检测中。

OBB 这种方法是根据物体本身的几何形状来决定盒子的大小和方向,盒子无须和座标轴垂直。这样就可以选择最合适的最紧凑的包容盒子。OBB 盒子的生成比较复杂。一般是考虑物体所有的顶点在空间的分布,通过一定的算法找到最好的方向 (OBB 盒子的几个轴 ) 。

一个 2D 示意图如图 10-38 所示。

这种技术比 AABB 技术更精确而且更健壮,但 OBB 实现起来比较困难,执行速度慢,并且不太适合动态的或柔性的物体。特别注意的是,当把一个物体分得越来越小的时候,事实上是在创建一棵有层次的树,如图 10-39 所示。

图10-39  OBB 树的生成(曲折线为物体)

为任意的网格模型创建 OBB 树可能是算法里最难的一个部分,而且它还要调整以适合特定的引擎或游戏类型。从图中可以看出,不得不找出包围给定模型的最近似的包装盒(或者其他 3D 体)。

现在得到了所有的包装盒,下一步将构造一棵树。

从最初的 AABB 包装盒开始从上至下地反复分割它。另外,还可以用从下至上的方式,逐步地合并小包装盒从而得到最大的包装盒。把大的包装盒分割成小的包装盒,应该遵守以下几条原则。

( 1 )用一个面(这个面垂直于包装盒中的一条座标轴)来分割包装盒上最长的轴,然后根据多边形处在分割轴的哪一边把多边形分离开来(如图 10-38 所示)。

( 2 )如果不能沿着最长的轴进行分割,那就沿第二长的边分割。持续地分割直到包装盒不能再分割为止。

( 3 )依据需要的精度(比如,是否真的要判断单个三角形的碰撞),可以按选择的方式(是按树的深度或是按包装盒中多边形的数目)以任意的条件停止分割。

正如读者所看到的,创建阶段相当复杂,其中包括了大量的运算,很明显不能实时地创建树,只能是事先创建。事先创建可以免去实时改变多边形的可能。另一个缺点是 OBB 要求进行大量的矩阵运算,不得不把它们定位在适当的地方,并且每棵子树必须与矩阵相乘。

现在假设已经有了 OBB 或者 AABB 树。那么该怎么进行碰撞检测呢?首先检测最大的包装盒是否相交(AABB 级别),如果相交了,它们可能(注意,只是可能)发生了碰撞,接下来将进一步地递归处理它们(OBB 级别,不断地递归用下一级进行处理)。

如果沿着下一级,发现子树并没有发生相交,这时就可以停止,并得出结论没有发生碰撞。如果发现子树相交,那么要进一步处理它的子树直到到达叶子节点,并最终得出结论。

碰撞检测最直观的想法是把一个 OBB 盒子的每个边都和另一个盒子的所有面来比较,如果这个边穿过了另一个 OBB 盒子的一个面,则两个 OBB 盒子发生了碰撞。显然这种方法的计算量是比较大的,因为要进行12×6×2=144 次边和面的比较。

但是,在考察两个没有碰撞的 OBB 盒子时,人们发现一些规律来简化比较。

( 1 )如果两个 OBB 盒子不互相接触,则应该可以找到一个盒子上的一个面,这个面所在的平面可以把 3D 空间分为两部分,两个 OBB盒子各在两边。

( 2 )如果没有这样的表面存在,则一定可以在两个 OBB 盒子上各找出一条边,这两条边所在的平面可以把两个 OBB 盒子分在两边。有了这个平面,就可以找到垂直于它的分割轴( separating axis),如图 10-40 所示。

( 3 )进行相交测试时,可以把包装盒投影到分割轴上,并检查它们是否线性相交。两个 OBB 盒子在这个分割轴上的投影将是分离的。

如上所述,要判断两个 OBB 盒子是否碰撞,只需要看两个 OBB 盒子之间是否有这样的平面和分割轴存在。如果存在,则没有碰撞。如果不存在,则碰撞。对第一种情况,每个盒子有 6 个表面 ( 其中每两个平行 ) ,可以决定 3 个分割轴。两个 OBB 盒子一共有 6 个可能的分割轴需要考虑。对第二种情况,两个 OBB 盒子之间的边的组合可以有 3×3=9 种情况,也就是有 9 个可能的分割轴。这样对任意两个 OBB 盒子,只需要考察15 个分割轴就可以了。如果在任一分割轴上的阴影不重合,则 OBB 盒子之间没有碰撞。

选择 AABB 还是选择 OBB 应该根据所需的精确程度而定。对一个需要快速反应的 3D 射击游戏来说,可能用AABB 来进行碰撞检测更好些 —— 可以牺牲一些精度来换取速度和实现的简单化,因此总能在游戏中看到一些小疏漏。当然随着硬件能力的提高, OBB 处理会逐渐被重视起来。

在做碰撞检测时应当遵循以下的优化理论,这样可以改善检测速度。

n     分两步检验,距离远时看作质点,距离近时采用包装盒。

n     距离很远的物体不会在短时间内相撞(可以采用BSP 树分割空间)。

n     一个物体不能隔着第二个物体和第三个物体相撞。

n     一旦一个物体检测到和另一物体碰撞,另一物体对这个物体不再检测。

n     静止的物体不主动与其他物体碰撞。

10.3.5   碰撞反应

碰撞以后需要做一些反应,比如说产生反冲力反弹出去,或者停下来,或者让阻挡物体飞出去,或者穿墙,而碰撞最讨厌的就是穿越,因为这通常不合逻辑。

首先看看弹性碰撞。弹性碰撞就是初中物理中说的动量守恒。物体在碰撞前后的动量守恒,没有任何能量损失。这样的碰撞用于打砖块的游戏中。引入质量的概念,有的物体会具有一定的质量,这些物体通常来说需要在碰撞以后进行另外一个方向的运动,另外一些物体设定为质量无限大,这些物体通常是碰撞墙壁。

当物体碰到质量非常大的物体,默认为碰到了一个弹性物体,其速度会改变,但是能量不会受到损失。一般在代码上的做法就是在速度向量上加上一个负号。

绝对的弹性碰撞是很少的,大多数情况下运用的还是非弹性碰撞。非弹性碰撞就是有能量损失的碰撞,其中完全非弹性碰撞是能量损失最多的碰撞。在非弹性碰撞过程中,有一部分机械能转变为其他形式的能量,如发热则是属于非弹性碰撞。

有一种很特殊的非弹性碰撞,比如碰后两小球结合在一起,不再分开;子弹射入到木箱中等,这些称为完全非弹性碰撞,这种碰撞的动能损失最大。

现在玩的大多数游戏都用的是很接近现实的非弹性碰撞。如果需要非弹性碰撞,需要介入摩擦力这个因素,因此也无法简单使用动量守恒这个公式。

为了简化运算,可以采取比较简单的方法,假设摩擦系数 μ 非常大,那么只要物体接触,并且拥有一个加速度,就可以产生一个无穷大的摩擦力,造成物体停止的状态。

基于别人的引擎写出一个让自己满意的碰撞是不容易的,那么如果读者需要自己建立一个碰撞系统,以下内容是应该考虑到的:静态和动态的碰撞检测系统,从概念上可以接受的物理系统,物体的质量、速度、摩擦系数等属性,地心引力。

点赞