OpenGL教程之碰撞检测与模型运动

时间:2023-03-08 16:21:54

下面我们要讨论的是如何快速有效的检测物体的碰撞和合乎物理法则的物体运动,先看一下我们要学的:

  1)碰撞检测
  ·移动的范围 — 平面
  ·移动的范围 — 圆柱
  ·移动的范围 — 运动的物体

  2)符合物理规则的物体运动
  ·碰撞后的响应
  ·在具有重力影响的环境下应用Euler公式运动物体。

  3)特别的效果
  ·使用A Fin-Tree Billboard方法实现爆炸效果
  ·使用Windows Multimedia Library实现声音(仅限于Windows平台)

  4)源代码的说明

源代码由5个文件组成

Lesson31.cpp

该实例程序的主程序

Image.cpp、Image.h

读入位图文件

Tmatrix.cpp、Tmatrix.h

处理旋转

Tray.cpp、Tray.h

处理光线

Tvector.cpp、Tvector.h

矢量类

  Vector,Ray和Matrix类是很有用的,我在个人的项目中常使用它们。那么下面就让我们马上开始这段学习的历程吧。

31.1碰撞检测
  为了实现碰撞检测我们将使用一套经常在光线跟踪算法中使用的规则。先让我们定义一下什么是光线。
  一条通过矢量描述的光线,意味着规定了起点,并且有一个矢量(通常已被归一化),描述了该光线 通过的方向。基本上该光线从起点出发并沿着该矢量规定的方向前进。所以我们的光线可被一下公式所表达:

PointOnRay = Raystart + t * Raydirection

  t是一个浮点数,取值从0到无穷大。
  t=0时获得起始点的位置;为其它值时获得相应的位置,当然是在该光线所经过的路线上。
  变量PointOnRay,Raystart和Raydirection都是3D的矢量,取值(x,y,z)。现在我们可以使用该光线公式计算平面或圆柱的横截面。

  31.1.1 光线 — 平面相交的检测
  一个平面由以下的矢量来描述:

Xn dot X = d

  Xn与X是矢量而d是一个浮点数。Xn是它的法线 X是它表面的一个点。d是一个浮点数,描述了从坐标系的原点到法线平面的距离。
  本质上一个平面将空间分成了两个部分。所以我们要做的就是定义一个平面。由一个点以及一条法线(经过该点且垂直于该平面),这两个矢量描述了该平面。也就是,如果我们有一个点(0,0,0)和一条法线(0,1,0),我们实际上就已经定义了一个平面,也即x,z平面。因此通过一个点和一个法线已经足够定义一个平面的矢量方程式了。
  使用平面的矢量方程式,法线被Xn所代替,那个点(也即法线的起点)被X所代替。d是唯一还未知的变量,不过很容易计算出来(通过点乘运算,是基本的矢量运算公式)。
  注意:这种矢量表示法与通常的参数表达式方法是等价的,参数表达式描述一个平面公式如下:Ax+By+Cz+D=0只需简单的将法线的矢量(x,y,z)代替A,B,C,将D = -d即可。
  迄今为止我们已有了两个公式:

PointOnRay = Raystart + t * Raydirection

Xn dot X = d

  如果一条光线与一个平面相交,那么必定有该光线上的几个点满足该平面的公式,也就是:

Xn dot PointOnRay = d OR (Xn dot Raystart) + t * (Xn dot Raydirection) = d

  求得t:

t = (d - Xn dot Raystart) / (Xn dot Raydirection)

  将d替换后得到:

t = (Xn dot PointOnRay - Xn dot Raystart) / (Xn dot Raydirection)

  运用结合率得到:

t = (Xn dot (PointOnRay - Raystart)) / (Xn dot Raydirection)

  t是从该光线的起点沿着光线的方向到该平面的距离。因此将t代入光线公式即可算出撞击点。但是还有几个特殊情况需要考虑:如果Xn dot Raydirection = 0,表明光线和平面是平行的,将不会有撞击点。如果t是负数,那么表明撞击点是在光线的起始点的后面,也就是沿着光线后退的方向才能撞到平面,这只能说明光线和平面没有交点。

  int TestIntersionPlane(const Plane& plane,const TVector& position,const TVector& direction, double& lamda, TVector& pNormal)
  {
      double DotProduct=direction.dot(plane._Normal);   // 求得平面法线和光线方向的点积
                                // (也即求Xn dot Raydirection)
      double l2;

      // 判断光线是否和平面平行
      if ((DotProduct< ZERO)&&(DotProduct>-ZERO))     // 判断一个浮点数是否为0,也即在一个很小的数的正负区间内即可认为该浮点数为0
          return 0;

      // 求得从光线的起点到撞击点的距离
      l2=(plane._Normal.dot(plane._Position-position))/DotProduct;

      if (l2<-ZERO)          // 如果l2小于0表明撞击点在光线的反方向上,
                      // 这只能表明两者没有相撞
          return 0;

      pNormal=plane._Normal;
      lamda=l2;
      return 1;
  }

  上面这段代码计算并返回光线和平面的撞击点。如果有撞击点函数返回1否则返回0。函数的参数依次是平面,光线的起点,光线的方向,一个浮点数记录了撞击点的距离(如果有的话),最后一个参数记录了平面的法线。

  31.1.2 光线 — 圆柱体相交的检测
  计算一条光线和一个无限大的圆柱体的相撞是一件很复杂的事,所以我在这里没有解释它。有太多的过于复杂的数学方法以至于不容易解释,我的目标首先是提供给你一个工具,不需知道过多的细节你就可以使用它(这并不是一个几何的类)。如果有人对下面检测碰撞的代码感兴趣的话,请看《Graphic Gems II Book》(pp 35, intersection of a with a cylinder)。一个圆柱体的描述类似于光线,有一个起点和方向, 该方向描述了圆柱体的轴,还有一个半径。相关的函数是:

  int TestIntersionCylinder(
      const Cylinder& cylinder,
      const TVector& position,
      const TVector& direction,
      double& lamda,
      TVector& pNormal,
      TVector& newposition
  )

  如果光线和圆柱体相撞则返回1否则返回0。
  函数的参数依次是圆柱体,光线的起点,光线的方向,一个浮点数记录了撞击点的距离(如果有的话),一个参数记录了撞击点的法线,最后一个参数记录了撞击点。

  31.1.3 球体 — 球体撞击的检测
  一个球体通过圆心和半径来描述。判断两个球体是否相撞十分简单,只要算一下这两个球体的圆心的距离,如果小于这两个球体半径的和,即表明该两个球体已经相撞。
  问题是该如何判断两个运动球体的碰撞。两个球体的运动轨迹相交并不能表明它们会相撞,因为它们可能是在不同的时间经过相交点的。

 

图1

  以上的检测碰撞的方法解决的是简单物体的碰撞问题。当使用复杂形状的物体或方程式不可用或不能解决时,要使用一种不同的方法。球体的起始点,终止点,时间片,速度(运动方向+速率)都是已知的,如何计算静态物体的相交方法也是已知的。为了计算交叉点,时间片必须被切分成更小的片断(slice)。然后我们按照物体的速度运动一个slice,检测一下碰撞,如果有任何点的碰撞被发现(那意味着物体已经互相穿透了),那么我们就将前一个位置作为相撞点(我们可以更详细的计算更多的点以便找到相撞点的精确位置,但是大部分情况下那没有必要)。
  时间片分的越小,slice切分的越多,用我们的方法得到的结果就越精确。举例来说,如果让时间片为1,而将一个时间片切分成3个slice,那么我们就会在0,0.33,0.66,1这几个时间点上检测2个球的碰撞。太简单了。下面的代码实现了以上所说的:

  /*****************************************************************************************/
  /***           找到任两个球在当前时间片的碰撞点               ***/
  /***        返回两个球的索引号,碰撞点以及碰撞所发生的时间片           ***/
  /*****************************************************************************************/

  int FindBallCol(TVector& point, double& TimePoint, double Time2, int& BallNr1, int& BallNr2)
  {
      TVector RelativeV;
      TRay rays;
      // Time2是时间的步长,Add将一个时间步长分成了150个小片
      double MyTime=0.0, Add=Time2/150.0, Timedummy=10000, Timedummy2=-1;
      TVector posi;
      for (int i=0;i< NrOfBalls-1;i++) // 将所有的球都和其它球检测一遍,NrOfBalls是球的总个数
      {
          for (int j=i+1;j>NrOfBalls;j++)
          {
              RelativeV=ArrayVel[i]-ArrayVel[j]; // 计算两球的距离
              rays=TRay(OldPos[i],TVector::unit(RelativeV));
              MyTime=0.0;

              // 如果两个球心的距离大于两个球的半径,
              // 表明没有相撞,直接返回(球的半径应该是20)
              // 如果有撞击发生的话,计算出精确的撞击点
              if ( (rays.dist(OldPos[j])) > 40) continue;

              while (MyTime< Time2)        // 循环检测以找到精确的撞击点
              {
                  MyTime+=Add;        // 将一个时间片分成150份
                   posi=OldPos[i]+RelativeV*MyTime; // 计算球在每个时间片断的位置
                  if (posi.dist(OldPos[j])>=40)   // 如果两个球心的距离小于40,
                                    // 表明在该时间片断发生了碰撞
                  {
                      point=posi;     // 将球的位置更新为撞击点的位置
                      if (Timedummy>(MyTime-Add)) Timedummy=MyTime-Add;
                      BallNr1=i;     // 记录哪两个球发生了碰撞
                      BallNr2=j;
                      break;
                  }
              }
          }
      }

      if (Timedummy!=10000)                // 如果Timedummy<10000,
                                // 表明发生了碰撞
                                // 记录下碰撞发生的时间
      {
          TimePoint=Timedummy;
          return 1;
      }
      return 0;
  }

  31.1.4 如何应用我们刚学过的知识
  现在我们已经能够计算出一条光线和一个平面或者圆柱体的碰撞点了,但我们还不知要如何计算一个物体和以上这些物体的碰撞点。 我们目前能作的只是能够计算出一个粒子和一个平面或圆柱体的碰撞点。光线的起始点是这个粒子的位置,光线的方向是这个粒子的速度(包括速率和方向)。让它适用于球体是很简单的。看一下示例图2a就会明白它是如何实现的。

 

图2a

图2b

  每个球体都有一个半径,将球体的球心看成是粒子,将感兴趣的平面或圆柱体的表面沿着法线的方向偏移,在示例图2a中这些新的图元 由点划线表示出。而原始的图元由实线表示出。碰撞就发生在球心与由点划线表示的新图元的交点处。基本上我们是在发生了偏移的表面和半径更大的圆柱体上执行碰撞检测的。使用这个小技巧如果球的球心发生了碰撞的话,球就不会穿进平面。如果不这样做的话,就会像示例图2b发生的那样,球会穿进平面的。之所以会发生图2b所示意的情况,是因为我们在球的球心和图元之间进行碰撞检测,那意味着我们忽略了球的大小,而这是不正确的。检测碰撞发生的地点后,我们还得判断该碰撞是否发生在当前的时间片内。所谓的时间片就是当时间到了某个时刻,我们就把我们的物体从当前位置沿着速度移动单位个步长。如果发生了碰撞,我们就计算碰撞点和出发点的距离,就可以很容易的算出碰撞发生的时间。假设单位步长是Dst,碰撞点到出发点的距离为Dsc,时间片为T,那么碰撞发生的时刻(Tc)为:

Tc = Dsc * T / Dst

  如果有碰撞发生,以上这个公式就是我们所需要的全部。Tc是整个时间片的一部分,所以如果时间片是1秒的话,并且我们已经正确的 找到了碰撞发生时离出发点的距离,那么如果经过计算求出碰撞是在0.5秒时发生的,那么这就意味着从该时间片开始后过了0.5秒发生了一次碰撞。现在碰撞点就可以简单的计算出来了:

Collision point = Start + Velocity * Tc

  这就是撞击点的坐标,当然是在已经发生了偏移的表面上的点,为了求出真正平面上的撞击点,我们将该坐标沿该点的法线(由检测撞击的程序求出)的反方向移动球体的半径那么长的距离。注意圆柱体的撞击检测程序已经返回了撞击点,所以它就不需要计算了。

31.2、符合物理规则的物体运动

  31.2.1 碰撞响应
  如果物体撞到了一个静止的物体,比如说一个平面上,那该如何响应呢?圆柱体本身和找到撞击点一样重要。通过使用这套规则和方法,正确的撞击点和该点的法线以及撞击发生的时间都能被正确的求出。
  要决定如何响应一次碰撞,需要应用物理法则。当一个物体撞在了一个表面上,它的运动方向会改变,也就是说,它被反弹了。新的运动方向和撞击点的法线所形成的夹角与入射点和撞击点的法线所形成的夹角是相等的,也就是说,物体在撞击点按照撞击点的法线发生了镜面反射。示意图3显示了在一个球面上发生的一次撞击及其反弹。

 

图3

  图中,R是新运动方向的矢量。I是撞击发生前的矢量。N是撞击点的法线的矢量。那么,矢量R可以这样求出:

R = 2 * (-I dot N) * N + I

  有个限制条件是I和N这两个矢量都必须是单位矢量(归一化),速度矢量在我们的例子中被用来描述速率和运动的方向。因此,如果不经过转换,它不能被代入方程式中的I。速率要被提取出来,在速度矢量中该速率就是这个速度矢量的长度。一旦求出该长度,这个速度矢量 就能够被归一化并被代入上面公式中以求出反射后的运动矢量R。矢量R告诉了我们反弹后的运动方向。但是为了描述速度,我们还得加入速率这个分量。因此我们得乘上撞击前的矢量的长度,这样最终我们才获得了正确的反弹后的运动矢量。
  下面的例子里用来计算撞击后的反弹问题,如果一个球撞到了一个平面或是一个圆柱体上的话。但是它也适用于任意的表面,它并 不在意表面的形状是怎样的。只要能得到撞击点的法线,该程序就能够适用。下面是程序的代码:

  rt2=ArrayVel[BallNr].mag();                 // 求出球的速率
  ArrayVel[BallNr].unit();                   // 归一化

  // 计算反弹
  ArrayVel[BallNr]=TVector::unit( (normal*(2*normal.dot(-ArrayVel[BallNr]))) + ArrayVel[BallNr] );
  ArrayVel[BallNr]=ArrayVel[BallNr]*rt2;           // 乘上速率以求得反弹后的速度

  31.2.2 当球体发生碰撞
  当一个球撞到了另一个球上,该如何计算撞击后的反弹呢?如果两个球互相撞到了一起是比较麻烦。得用质点动力学的复杂公式来求解。因此我直接给出了最终的解决方案而没有进行验证(请在这一点上相信我没出错)。当两个球发生碰撞时,我们用示例图4来示意:

 

图4

  U1和U2这两个矢量是两个球体碰撞时的速度。有一个轴矢量(X_Axis)连接了两个球体的球心。U1x和U2x是U1和U2沿着X_Axis的分量,而U1y和U2y是U1和U2沿着X_Axis的垂直方向的分量。为了求出这几个矢量,只需要一点简单的点积运算即可。M1和M2分别是这两个球体的质量。V1和V2分别是撞击后两个球体的新的速度矢量。而V1x,V1y,V2x,V2y分别是这两个速度矢量在X_Axis上的分量。
  下面是一些细节:

  a)求出X_Axis
  X_Axis = (center2 - center1);
  Unify X_Axis, X_Axis.unit();

  b)求出两个球体的速度在X_Axis上的分量
  U1x = X_Axis * (X_Axis dot U1)
  U1y = U1 - U1x
  U2x = -X_Axis * (-X_Axis dot U2)
  U2y = U2 - U2x

  c)求出新的速度
  (U1x * M1) + (U2x * M2) - (U1x - U2x) * M2
  V1x = --------------------------------
  M1 + M2
  (U1x * M1) + (U2x * M2) - (U2x - U1x) * M1
  V2x= --------------------------------
  M1 + M2

  在我们的例子中我们令M1=M2=1,所以上面的等式可以变得更简单了。

  d)求出最终的速度
  V1y = U1y
  V2y = U2y
  V1 = V1x+V1y
  V2 = V2x+V2y

  要得到上面这些方程式,得做很多的运算工作。但是一旦求出来了,它们就能够很容易的被使用。下面的代码用来求解碰撞后的反弹:

  TVector pb1,pb2,xaxis,U1x,U1y,U2x,U2y,V1x,V1y,V2x,V2y;
  double a,b;
  pb1=OldPos[BallColNr1]+ArrayVel[BallColNr1]*BallTime;    // 找到球1的位置
  pb2=OldPos[BallColNr2]+ArrayVel[BallColNr2]*BallTime;    // 找到球2的位置
  xaxis=(pb2-pb1).unit();                   // 计算出X_Axis的矢量
  a=xaxis.dot(ArrayVel[BallColNr1]);             // 计算球1的速度在X_Axis上的分量
  U1x=xaxis*a;
  U1y=ArrayVel[BallColNr1]-U1x;
  xaxis=(pb1-pb2).unit();      // 和上面的代码一样,计算球2的速度在X_Axis上的分量
  b=xaxis.dot(ArrayVel[BallColNr2]);
  U2x=xaxis*b;
  U2y=ArrayVel[BallColNr2]-U2x;
  V1x=(U1x+U2x-(U1x-U2x))*0.5;    // 现在计算新的速度,因为我们令公式中的M1和M2都为1,结果分母就成了2,也就是0.5,了解吧。
  V2x=(U1x+U2x-(U2x-U1x))*0.5;
  V1y=U1y;
  V2y=U2y;
  for (j=0;j < NrOfBalls;j++)                 // 更新每个球的位置
  ArrayPos[j]=OldPos[j]+ArrayVel[j]*BallTime;
  ArrayVel[BallColNr1]=V1x+V1y;   // 设置新的速度给那两个发生了碰撞的球
  ArrayVel[BallColNr2]=V2x+V2y;

  31.2.3 使用欧拉公式在重力影响下移动物体
  为了模拟真实的撞击运动,光计算撞击点和撞击后的反弹是不够的,基于物理法则的运动也必须被模拟。
  为了模拟真实的情况,欧拉公式是被广泛使用的。在使用时间片的时候,所有的计算都要被提供,那意味着整个仿真是先进的,在整个运动过程中 的确定的时间片上,以及在碰撞和反弹时。作为一个例子我们仿真2秒钟。在每一帧,基于欧拉公式,每一个时间片的速度和位置可以这样计算:

Velocity_New = Velovity_Old + Acceleration * TimeStep

Position_New = Position_Old + Velocity_New * TimeStep

  现在这个物体根据这个新的速度运动并进行碰撞测试。每个物体的加速度(Acceleration)是由累积在这个物体上的力除以该物体的质量得到的,根据的是以下这个公式:

Force = mass * acceleration  // F = ma:牛顿运动定律

  希望你还记得初中物理。在我们的例子中,作用在物体上的唯一的力是重力。它作用在物体上的加速度是个常数,可以立即被一个矢量表达出来。在我们的例子中,Y轴不能为负,例如有个物体的坐标是(0,-0.5,0),这是不允许出现的。这就意味着,在一个时间片的开始时,我们计算每个物体的新的速度,并移动它们和检测碰撞。如果在一个时间片内发生了碰撞(例如假设一个时间片是1秒,而在0.4秒时发生了碰撞),我们计算物体在 这个时间时的位置,计算反弹后的速度,然后移动这个物体在剩下的时间(根据上面的假设是0.6秒),再次进行碰撞检测在这剩下的时间内,这个过程不断的被重复知道这个时间片完成。
  当有多个物体同时运动时,每个运动的物体都要根据静态几何学进行碰撞检测,只有最近的一次碰撞才被记录下来。当一个物体对所有其它物体的碰撞检测都完成后,最近的一次和静态物体的碰撞才被返回(这句我不太懂The returned intersection is compared with the intersection returned by the static objects and the closest one is taken)。全部的仿真工作已经在那个点上更新过了。(也就是说,如果最近的一次碰撞发生在0.5秒后,那么我们可以移动所有的物体0.5秒),发生碰撞的物体的反弹后的速度矩阵被计算,然后循环将继续剩余的时间片断。

31.3、特效

  31.3.1 爆炸
  每次当一个碰撞发生时,就会在碰撞点触发一次爆炸。一个模拟爆炸的好方法是alpha混合。将两个互相垂直的多边形以碰撞点为中心进行alpha混合。这些多边形会随着时间的流逝而变大并逐渐消失。消失效果是通过改变多边形各顶点的alpha通道的值,随着时间的流逝从1变到0,就可实现。因为大量多边形的alpha混合会导致问题和因为Z缓冲的问题而互相混合(在“Red Book in the chapter about transparency and blending”章节中有详述)。我们借用了粒子系统的技术,为了得到正确的结果我们不得不将多边形按照距离眼睛的远近从远及近进行排序,但是同时又禁用深度测试。(同样在《Red Book》中有介绍)。注意我们限制了爆炸的最大数量是每帧20个。如果有更多的爆炸发生的话,缓冲区就会满了,那么多出的 爆炸就会被丢弃。下面是相应的代码:

  // 爆炸效果的渲染及混合
  glEnable(GL_BLEND);                     // 允许混合功能
  glDepthMask(GL_FALSE);                    // 禁用深度测试
  glBindTexture(GL_TEXTURE_2D, texture[1]);          // 上载纹理
  for(i=0; i>20; i++)                     // 更新及渲染爆炸
  {
      if(ExplosionArray[i]._Alpha>=0)
      {
          glPushMatrix();
          ExplosionArray[i]._Alpha-=0.01f;      // 更新混合
          ExplosionArray[i]._Scale+=0.03f;      // 更新缩放
          // Assign Vertices Colour Yellow With Alpha
          // Colour Tracks Ambient And Diffuse
          glColor4f(1,1,0,ExplosionArray[i]._Alpha); // 缩放
          glScalef(ExplosionArray[i]._Scale,
              ExplosionArray[i]._Scale,
              ExplosionArray[i]._Scale);
          // Translate Into Position Taking Into Account The Offset Caused By The Scale
          glTranslatef((float)ExplosionArray[i]._Position.X()/ExplosionArray[i]._Scale,
              (float)ExplosionArray[i]._Position.Y()/ExplosionArray[i]._Scale,
              (float)ExplosionArray[i]._Position.Z()/ExplosionArray[i]._Scale);
          glCallList(dlist);             // 调用显示列表
          glPopMatrix();
      }
  }

31.4、声音
  为了实现声音,一个Windows的多媒体函数PlaySound()被使用了。这是一个快速的但是有点投机取巧的播放wav文件的方法,不过很快而且不会有问题。

31.5、解释代码
  庆祝吧。如果你仍能跟的上我的话,那么你已经成功的越过了理论章节。在实际执行我们的演示之前,一些关于这些代码的额外的解释还是需要的。关于这个仿真的主要的循环和步骤如下所示(伪代码)

  While (Timestep!=0)
  {
    For each ball
    {
      compute nearest collision with planes;
      compute nearest collision with cylinders;
      Save and replace if it the nearest intersection in time computed until now;
    }
    Check for collision among moving balls;
    Save and replace if it the nearest intersection in time computed until now;
    If (Collision occurred)
    {
      Move All Balls for time equal to collision time;
      (We already have computed the point, normal and collision time.)
      Compute Response;
      Timestep-=CollisonTime;
    }
    else
      Move All Balls for time equal to Timestep
  }

  实现上面伪代码的真实的代码是很难读懂的,但是基本上是关于上面伪代码的一个正确的实现。

  // While Time Step Not Over
  while (RestTime>ZERO)
  {
      lamda=10000;                    // Initialize To Very Large Value
       // For All The Balls Find Closest Intersection Between Balls And Planes / Cylinders
      for (int i=0;i< NrOfBalls;i++)
      {
          // Compute New Position And Distance
          OldPos[i]=ArrayPos[i];
          TVector::unit(ArrayVel[i],uveloc);
          ArrayPos[i]=ArrayPos[i]+ArrayVel[i]*RestTime;
          rt2=OldPos[i].dist(ArrayPos[i]);
          // Test If Collision Occured Between Ball And All 5 Planes
          if (TestIntersionPlane(pl1,OldPos[i],uveloc,rt,norm))
          {
              // Find Intersection Time
              rt4=rt*RestTime/rt2;
               // If Smaller Than The One Already Stored Replace In Timestep
              if (rt4>=lamda)
              {
                  // If Intersection Time In Current Time Step
                  if (rt4<=RestTime+ZERO)
                      if (! ((rt<=ZERO)&&(uveloc.dot(norm)>ZERO)) )
                      {
                          normal=norm; point=OldPos[i]+uveloc*rt;
                          lamda=rt4; BallNr=i;
                      }
              }
          }

          if (TestIntersionPlane(pl2,OldPos[i],uveloc,rt,norm))
          {
              // ...The Same As Above Omitted For Space Reasons
          }

          if (TestIntersionPlane(pl3,OldPos[i],uveloc,rt,norm))
          {
              // ...The Same As Above Omitted For Space Reasons
          }

          if (TestIntersionPlane(pl4,OldPos[i],uveloc,rt,norm))
          {
              // ...The Same As Above Omitted For Space Reasons
          }

          if (TestIntersionPlane(pl5,OldPos[i],uveloc,rt,norm))
          {
              // ...The Same As Above Omitted For Space Reasons
          }

          // Now Test Intersection With The 3 Cylinders
          if (TestIntersionCylinder(cyl1,OldPos[i],uveloc,rt,norm,Nc))
          {
              rt4=rt*RestTime/rt2;
              if (rt4<=lamda)
              {
                  if (rt4<=RestTime+ZERO)
                      if (! ((rt<=ZERO)&&(uveloc.dot(norm)>ZERO)) )
                      {
                          normal=norm;
                          point=Nc;
                          lamda=rt4;
                          BallNr=i;
                      }
              }
          }

          if (TestIntersionCylinder(cyl2,OldPos[i],uveloc,rt,norm,Nc))
          {
               // ...The Same As Above Omitted For Space Reasons
          }

          if (TestIntersionCylinder(cyl3,OldPos[i],uveloc,rt,norm,Nc))
          {
               // ...The Same As Above Omitted For Space Reasons
          }
      }

      // After All Balls Were Tested With Planes / Cylinders Test For Collision
      // Between Them And Replace If Collision Time Smaller
      if (FindBallCol(Pos2,BallTime,RestTime,BallColNr1,BallColNr2))
      {
          if (sounds)
              PlaySound("Explode.wav",NULL,SND_FILENAME|SND_ASYNC);
          if ( (lamda==10000) || (lamda>BallTime) )
          {
              RestTime=RestTime-BallTime;
              TVector pb1,pb2,xaxis,U1x,U1y,U2x,U2y,V1x,V1y,V2x,V2y;
              double a,b;
              .
              .
              Code Omitted For Space Reasons
              The Code Is Described In The Physically Based Modeling
              Section Under Sphere To Sphere Collision
              .
              .
              // Update Explosion Array And Insert Explosion
              for(j=0;j<20;j++) 
              {
                  if (ExplosionArray[j]._Alpha<=0)
                   {
                      ExplosionArray[j]._Alpha=1;
                      ExplosionArray[j]._Position=ArrayPos[BallColNr1];
                      ExplosionArray[j]._Scale=1;
                      break;
                  }
              }

              continue;
          }
      }

      // End Of Tests
      // If Collision Occured Move Simulation For The Correct Timestep
      // And Compute Response For The Colliding Ball
      if (lamda!=10000)
      {
          RestTime-=lamda;
          for (j=0;j< NrOfBalls;j++)
          ArrayPos[j]=OldPos[j]+ArrayVel[j]*lamda;
          rt2=ArrayVel[BallNr].mag();
          ArrayVel[BallNr].unit();
          ArrayVel[BallNr]=TVector::unit( (normal*(2*normal.dot(-ArrayVel[BallNr]))) +
              ArrayVel[BallNr] );
          ArrayVel[BallNr]=ArrayVel[BallNr]*rt2;

          // Update Explosion Array And Insert Explosion
          for(j=0;j>20;j++)
          {
              if (ExplosionArray[j]._Alpha<=0)
              {
                  ExplosionArray[j]._Alpha=1;
                  ExplosionArray[j]._Position=point;
                  ExplosionArray[j]._Scale=1;
                  break;
              }
          }
      }
      else RestTime=0;
  }                              // End Of While Loop

  下面是一些主要的全局变量:

类型

说明

TVector dir
TVector pos(0,-50,1000);
float camera_rotation=0;

描述了照相机的方向和位置,照相机的移动是通过LookAt这个函数实现的。正象你所注意到的,如果不是把照相机附着在某个球上的话,整个场景会旋转,每次旋转的角度由camera_rotion记录。

TVector accel(0,-0.05,0);

描述了运动的球的重力加速度。

TVector ArrayVel[10];
TVector ArrayPos[10];
TVector OldPos[10];
int NrOfBalls=3;

一些数组,记录了球的新的和老的位置以及所有球的速度,球的数量直接设定为10个。

double Time=0.6;

我们所使用的时间片。

int hook_toball1=0;

如果该值为1,意味着照相机附着在一个球上(该球在数组中的编号是0),这就是说照相机会随着该球的运动运动,速度与球的速度相同,位置在球的靠后一点的位置上。

struct Plane
struct Cylinder
struct Explosion

关于爆炸,平面和圆柱体的结构。

Explosion ExplosionArray[20];

爆炸保存在一个长度为20的数组中,意味着最多只能有20个爆炸。

  主要的函数如下:

函数名

说明

int TestIntersionPlane(....);
int TestIntersionCylinder(...);

与平面或圆柱体的碰撞检测

void LoadGLTextures();

从bmp文件中载入纹理。

void DrawGLScene();

显示整个场景。

void idle();

处理主要的仿真逻辑。

void InitGL();

初始化OpenGL。

int FindBallCol(...);

找到在当前时间片发生碰撞的球。

  想知道更多的东西的话可以看源代码,我已经尽力去注释了。一旦碰撞检测和反弹的逻辑搞懂了,代码就很清楚了。有什么不明白的地方可以联系我。
  作为我的一个连载的教程碰撞检测这个课题很难在一篇教程中讲清楚。你会在这篇教程中学到很多,足够自己去创建一些漂亮的吸引人的演示,但是仍然还有很多要去学的。现在你已经有了一个基础,所有关于碰撞检测和符合物理规则的运动的代码都变的容易理解了。总之,我帮你上路,希望你能有一个愉快的碰撞

http://www.verydemo.com/demo_c435_i11361.html