[译]A Verlet based approach for 2D game physics-Part One

译者:i_dovelemon

日期:2016 / 03 / 06

来源:GameDev,CSDN

主题:Physics Engine, Verlet integration, Collision Detection, Collision Response



介绍



        本篇文章将要讲述如何在2D场景中模拟游戏物理。我们将会使用一个名为Verlet积分器进行设计,并且会讲述点运动,形状建立,碰撞检测和碰撞反应。读者应该对基本的向量加减运算有一定的了解。如果对2D欧几里得几何学有很好的掌握就再好不过了。


Verlet积分



        首先,什么是Verlet积分?Verlet积分器指的是一种对运动方程进行数值积分的方法。对于本篇文章来说,你不需要知道数值积分实际的含义是什么;一般来说,Verlet积分描述了一个点在一段时间里的运动情况。有很多的方法能够实现这样的功能--我想,你们大多数时候都是使用Euler积分来进行,如下:



        如果将上面的两个公式合并成一个公式,也就是将上面的Velocity[New]带入到下面的公式中去就得到如下的公式:



        Verlet积分器有很多种不同的类型-- Position Verlet, Velocity Verlet, Leapfog。在这篇文章里面,我们将主要使用Position Verlet积分器,它有很多的优点,我们将在本篇文章中使用到。

关于Position Verlet积分器的公式,和上面的Euler积分器的公式相差不多,如下所示:



        从上面,我们可以看到,在Position Verlet中,我们使用(Position[Current] - Position[Old])来代替在Euler积分器中的Velocity[New] * Timestep。也就是说,在Position Verlet积分器中,我们不需要考虑Velocity这个参数。实际上这种方法是不精确的,我们仅仅使用了Position[Current] - Position[Old]来近似的模拟物体运动中的速度。但是,使用这种方法却是非常的快速和稳定的,所以对于游戏来说,这种方法非常的高效。同样,使用这种方法接下来的碰撞反应也能够非常容易处理。我们来考虑下面的情况:



        灰色的表示的是Position[Old],黑色的表示的是Position[Current]。过了一段时间后,这个点就会碰撞到前面的方块。当检测到了碰撞检测之后,我们只要将点移出到方块的外面,碰撞反应就成功的处理了。由于在Position Verlet积分器中,我们使用的是(Position[Current] - Position[Old])来作为点运动的速度,那么当我们改变了Position[Current]或者Position[Old]的时候,点的运动速度也就随之而改变,而这正是我们要在碰撞反应里面要做的内容。如下图所示,这个点会自动的进行减速,并且最终停止运动:



        我们将上面讲述的理论编码成如下的程序:

struct Point {
  Vec2 Position;
  Vec2 OldPosition;
  Vec2 Acceleration;
};

class Physics {
  int PointCount;
  Point* Points[ MAX_VERTICES ];

  float Timestep;

public:
  void  UpdateVerlet();

  //Constructors, getters/setters etc. omitted
};

void Physics::UpdateVerlet() {
  for( int I = 0; I < PointCount; I++ ) {
    Point& P = *Points[ I ];

    Vec2 Temp = P.Position;
    P.Position += P.Position - P.OldPosition + P.Acceleration*Timestep*Timestep;
    P.OldPosition = Temp;
  }
}

        上面的代码,能够对任意数量的点进行处理。但是单独的点并没有什么实际的意义,除非你在编写的是一个粒子系统。而在游戏中,我们常常需要处理的是刚体模拟,所以我们需要将上面的代码扩展称为支持刚体的代码。在现实世界中,我们知道一个刚体实际上是有很多的点(原子)组成的,他们之间通过各种力的作用相互融合在一起。

        当然,我们能够通过模拟大量的粒子,让他们以某种方式粘合在一起,以此来近似的模拟刚体的运动。但是,对于这样的系统,我们要进行非常庞大的计算才能够实现,对于一个游戏来说,往往需要大量的刚体,使用这种方式进行模拟完全就不能够接受。幸运的是,我们只要模拟刚体的几个顶点就能够实现类似的效果。比如说,如果我们要模拟一个盒子,那么我们只需要4个顶点,并且以某种方式粘合他们,然后就成功的模拟出来了。

        那么,现在的问题就是这种将顶点粘合在一起的东西是什么了?还是考虑前面的盒子,我们已经有了四个顶点,那么很显然,这四个顶点之间的距离如果不发生改变,那么这个刚体就形成了。如果任意两个顶点之间的距离发生了改变,我们就认为这个刚体发生了形变,而对于刚体来说这是不可接受的。所以,我们需要找到一个方案,它能够使各个顶点之间距离保持不变。

        在现实世界中,这样的问题很容易处理,我们只要在这四个顶点之间放置一些杆子,就能够保证他们之间的距离不发生改变。所以,在我们的程序中,我们也需要创建一些想象中的杆子,来将这四个顶点相互粘合。我们需要保持每两个顶点之间的距离不发生任何的变化。在每一次的游戏循环中,我们都需要在进行Position Verlet积分之前,更新这些杆子,从而保证每一个顶点之间的距离没有发生变化。实际上,这种更新算法非常的简单。首先,我们要记录下这些顶点在创建的时候与其他顶点之间的距离。在每一帧的运算里面,我们计算当前帧里面,这些顶点之间的距离。然后使用以前记录的距离和当前的距离进行比较,强制的将当前的距离变换称为当初创建时的距离即可。

struct Edge {
  Vertex* V1;
  Vertex* V2;

  float OriginalLength; //The length of the edge when it was created

  //Constructors etc. omitted
};

void Physics::UpdateEdges() {
  for( int I = 0; I < EdgeCount; I++ ) {
    Edge& E = *Edges[ I ];

    //Calculate the vector mentioned above
    Vec2 V1V2 = E.V2->Position - E.V1->Position; 

    //Calculate the current distance
    float V1V2Length = V1V2.Length(); 
    
    //Calculate the difference from the original length
    float Diff       = V1V2Length - E.OriginalLength; 
    
    V1V2.Normalize();

    //Push both vertices apart by half of the difference respectively 
    //so the distance between them equals the original length
    E.V1->Position += V1V2*Diff*0.5f; 
    E.V2->Position -= V1V2*Diff*0.5f;
  }
}

        当我们使用上面代码中创建的Edge结构来链接任意两个顶点,我们就能够很好的模拟刚体的行为,并且能够在碰撞到地面的时候有很好的旋转效果出现。那么,为什么这样就能够实现效果了?我们没有做什么特别的事情,只是添加了任意两个顶点之间的约束关系,然后我们就成功的模拟出刚体的效果来了。而造成这个结果的原因就是我们前面所讲述的Position Verlet积分器的功能。还记得我们在前面讨论中说到,Position Verlet积分器不直接和Velocity打交道,如果我们改变了顶点的位置,相应的就会改变顶点的速度。而这样的情况,刚好符合我们对刚体操作的理解,虽然和真实的情况还有点差距,但是对游戏来说,已经足够使用了。

      老实说,我前面和大家撒了个谎。如果你只是当当执行上面的代码的话,那么实际得到的结果并不是你所期待的那样。实际上,当一个刚体碰撞到其他的物体的时候,使用上面的方案来进行的模拟,我们的刚体或多或少都会发生点形变,形变的程度依赖于碰撞之前的速度。为什么会这样了?UpdateEdges算法是正确的,但是,调用一次之后顶点之间的距离还是会发生改变。下图清晰的表明了这种情况:如果一个顶点被超过一个以上的Edge连接的时候,对一条边的长度约束会导致其他的边长度发生改变,而这就是导致物体变形的原因所在:



        想要避免这个情况的发生,我们只能够通过多次的迭代调用前面提到的UpdateEdges函数来尽量减少这种情况的发生。UpdateEdges函数调用的次数越多,我们就越能够近似的处理这个任务。而这个就给了我们程序员很大的方便,我们可以根据游戏计算量的分配,来合理的安排到底需要调用几次这个函数。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章