[譯]A Verlet based approach for 2D game physics-Part Two

碰撞檢測



        現在,我們先前的文章中講述的方案已經能夠模擬大部分的剛體了,那麼接下來我解決另外一個問題--碰撞檢測。在本篇文章中,我將要使用一個名爲“分離軸理論”的算法來進行碰撞檢測。如果你已經知道了這個理論,那麼你就可以直接跳過這個部分,去看碰撞反應的內容。那麼,分離軸理論是怎麼樣工作的了?正如這個理論的名稱所講述的那樣,如果我們能夠在兩個剛體之間找到一條直線,這條直線不與這兩個剛體中的任何一個發生交叉,那麼我們就認爲這兩個物體是沒有碰撞的。下圖演示了這個理論:



        這個算法的唯一限制的地方就是,它只能夠作用與凸邊形。如果我們對兩個凹邊形進行這樣的測試,那麼這個理論就會失敗,也就是說,可能會錯誤的檢測出兩個形狀之間發生了碰撞,而實際上是沒有發生碰撞的。通過下圖,你就能夠很清晰的明白這個理論:



        我們能夠將上面的凹多邊形拆分稱爲多個凸多邊形組合而成,然後對這些形狀分別進行測試。本篇文章爲了簡便,將只對凸多邊形進行處理。如果你覺得有必要,那麼就增加對凹多邊形的處理。

        那麼,我們怎麼去發現我們是否能夠在兩個剛體之間插入一條直線了?我們當然能夠測試所有的直線,看看是否存在這樣的一條。但是這樣做是非常沒有效率的。爲了做到類似的效果,我們可以採用投影的方式進行。如果我們在圖1中的分離軸上,繪製一條垂直的線,那麼,我們就能夠發現這兩個剛體在這個垂直線上的投影實際上是沒有重合的部分。那麼,如果這兩個剛體在這條垂直線上的投影發生了交叉,那麼也就是說他們的投影有重疊的部分。



        無論我們將這條直線放在什麼地方,投影的結果都是一維空間裏面的,也就是說只有這條線的方向對我們有意義。所以,實際上我們並不是要找到那條確切的將兩個剛體分離開來的直線,而是要找到一個方向,在這個方向上,兩個剛體的投影不會發生交叉重疊。而找這樣的一個方向實際上是十分簡單的。我們來考慮下只有一條可能的線將兩個剛體分離的情況。



        很明顯,我們能夠看到這條線實際上是和下面的四邊形的邊界線是平行的。因此,我們只要遍歷這個四邊形的四條邊,來檢測兩個剛體的投影是否在這些邊上發生了重合。如果存在一條邊,沒有發生重合,那麼我們就能夠認爲這兩個剛體實際上是沒有發生碰撞的。如果沒有這樣的邊存在,那麼我們就認爲這兩個剛體發生了碰撞,接下來就需要對它們進行碰撞反應的處理。

        我們將這些內容編寫成代碼的形式。但是在實現我們的碰撞檢測之前,我們首先寫一個剛體的結構,它包含了它的頂點和邊界:

struct PhysicsBody {
  int VertexCount;
  int EdgeCount;

  Vertex* Vertices[ MAX_BODY_VERTICES ];
  Edge*   Edges   [ MAX_BODY_EDGES    ];

  void ProjectToAxis( Vec2& Axis, float& Min, float& Max );

  //Again, constructors etc. omitted
};

        這個結構體裏面的ProjectToAxis,將會將這個剛體投影到指定的軸線上去,並且通過Min和max來返回在這條軸線上的座標。由於投影是值將一個2D形狀變換爲1D的操作,所以這個投影的結果能夠通過兩個float型的數據來保存。投影的方法十分的簡單:

void PhysicsBody::ProjectToAxis( Vec2& Axis, float& Min, float& Max ) {
  float DotP = Axis*Vertices[ 0 ]->Position;

  //Set the minimum and maximum values to the projection of the first vertex
  Min = Max = DotP; 

  for( int I = 1; I < VertexCount; I++ ) {
    //Project the rest of the vertices onto the axis and extend 
    //the interval to the left/right if necessary
    DotP = Axis*Vertices[ I ]->Position; 

    Min = MIN( DotP, Min );
    Max = MAX( DotP, Max );
  }
}

        正如你所看到的,將2D投影到1D上的操作,僅僅是一個點積操作而已。那麼,碰撞檢測的代碼就像下面這樣:

bool Physics::DetectCollision( PhysicsBody* B1, PhysicsBody* B2 ) {
  //Just a fancy way of iterating through all of the edges of both bodies at once
  for( int I = 0; I < B1->EdgeCount + B2->EdgeCount; I++ ) { 
    Edge* E;

    if( I < B1->EdgeCount )
      E = B1->Edges[ I ];
    else
      E = B2->Edges[ I - B1->EdgeCount ];
    
    //Calculate the axis perpendicular to this edge and normalize it
    Vec2 Axis( E->V1->Position.Y - E->V2->Position.Y, E->V2->Position.X - E->V1->Position.X ); 
    Axis.Normalize();

    float MinA, MinB, MaxA, MaxB; //Project both bodies onto the perpendicular axis
    B1->ProjectToAxis( Axis, MinA, MaxA );
    B2->ProjectToAxis( Axis, MinB, MaxB );

    //Calculate the distance between the two intervals - see below
    float Distance = IntervalDistance( MinA, MaxA, MinB, MaxB ); 

    if( Distance > 0.0f ) //If the intervals don't overlap, return, since there is no collision
      return false;
  }

  return true; //There is no separating axis. Report a collision!
}

        上面的算法就是我們前面所描述的那樣。如果你對這段代碼存在疑惑,那麼我建議你一步一步的看下前面的解釋。IntervalDistance也是十分簡單的:

float Physics::IntervalDistance( float MinA, float MaxA, float MinB, float MaxB ) {
  if( MinA < MinB )
    return MinB - MaxA;
  else
    return MinA - MaxB;
}

        由於我們不知道剛體A是否落在剛體B的左邊或者右邊,所以我們先進行檢測,判斷下兩個端點的位置關係,從而判斷是否發生了重疊。

        上面的代碼就是碰撞檢測的所有內容,除了我們還需要在碰撞檢測系統中獲取一個碰撞向量,這個向量將要用來將兩個剛體分離,使他們不再發生碰撞而僅僅是相互接觸。實際上這樣的向量有很多,但是爲了讓我們的物理顯的更加真實,我們需要找到其中最小的一個向量。這個最小的向量有一個特殊的特性,那就是它總是垂直於我們將要投影到的那條直線上去,也就是說我們需要對每一條邊計算一次碰撞向量,從中找到最小的一個向量即可。這個碰撞向量的長度是容易計算出來的,大家只要看下下圖就能夠明白:



        上面的代碼我們將每一個剛體都投影到了一個歸一化的向量上去,然後調用IntervalDistance來檢測他們是否發生了重疊。而碰撞向量的長度剛好就等於這個哈蘇計算出來的兩個投影之間重疊的部分長度。爲了讓我們的碰撞檢測系統和接下來的碰撞反應系統能夠很好的進行交互,我們將這個碰撞向量的信息創建了一個單獨的結構體:

class Physics {
  struct {
    float Depth;
    Vec2  Normal;
  } CollisionInfo;

  //Everything else omitted
}

        Depth成員表示的就是碰撞向量的長度,Normal就是碰撞向量的方向。

        我們新的碰撞檢測函數將進行修改,如下所示:

bool Physics::DetectCollision( PhysicsBody* B1, PhysicsBody* B2 ) {
  float MinLength = 10000.0f; //Initialize the length of the collision vector to a relatively large value
  for( int I = 0; I < B1->EdgeCount + B2->EdgeCount; I++ ) {
    Edge* E;

    if( I < B1->EdgeCount )
      E = B1->Edges[ I ];
    else
      E = B2->Edges[ I - B1->EdgeCount ];

    Vec2 Axis( E->V1->Position.Y - E->V2->Position.Y, E->V2->Position.X - E->V1->Position.X );
    Axis.Normalize();

    float MinA, MinB, MaxA, MaxB;
    B1->ProjectToAxis( Axis, MinA, MaxA );
    B2->ProjectToAxis( Axis, MinB, MaxB );

    float Distance = IntervalDistance( MinA, MaxA, MinB, MaxB );

    if( Distance > 0.0f )
      return false;
      
    //If the intervals overlap, check, whether the vector length on this 
    //edge is smaller than the smallest length that has been reported so far
    else if( abs( Distance ) < MinDistance ) { 
      MinDistance = abs( Distance );

      CollisionInfo.Normal = Axis; //Save collision information for later
    }
  }

  CollisionInfo.Depth = MinDistance;

  return true; //There is no separating axis. Report a collision!
}

        一旦我們有了這個函數,我們就能夠實現一些非常簡單的碰撞反應 了。由於我們計算出來的碰撞向量,能夠將兩個剛體相互分離,並且不再發生碰撞,所以我們就可以簡單的使用這個碰撞向量對剛體的所有頂點進行移動,以此來進行碰撞反應的處理。這個能夠解決問題,但是結果看上去並不是非常的好。兩個剛體之間將會發生相對的滑動,他們的表現和現實世界的效果並不一致,不會有相對旋轉的效果出現。

        使用上面的方法,當剛體的頂點速度不相同的時候,剛體就會發生旋轉。同樣的,一個剛體只要當它的頂點具有不同的加速度的時候,這個剛體纔會存在旋轉速度。加速度就是速度的改變,而在Verlet積分器中速度的改變就是位置的改變。因此,如果我們將兩個剛體通過碰撞向量移動,那麼這兩個剛體中所有頂點的速度都會發生同樣的改變,也就是說,頂點速度一致,就不存在旋轉速度了。正是因爲這個原因,我們需要編寫出更好的碰撞反應出來。

        這裏就是爲什麼我們要使用Position Verlet的原因所在。在一個剛體系統中,我們需要通過非常複雜的公式去計算一個剛體的動量,然後在分別計算它的線性動量和角動量。在我們的系統中,整個事情變得十分簡單,我們只需要移動邊界和發生碰撞的一些參與點,直到兩個剛體不再發生碰撞而僅僅是相互接觸的狀態。由於邊界和頂點都和其他的剛體中的屬性相關聯,所以當我們改變了這些邊界和頂點的時候,由於邊界約束的存在,其他的邊界和頂點也會隨之發生改變,從而適應這個新的情況。最終就導致兩個相互碰撞的剛體會發生相互旋轉的效果。所以,整個碰撞反應的處理,就是將發生碰撞的一些邊界和頂點的位置進行改變,使他們相互分離,那麼剩下來的頂點就會在後面的約束中被自動的進行修正完善了。

        在一次碰撞中,標示那些參與的邊界和頂點並沒有什麼困難的。碰撞的頂點實際上就是距離另外一個剛體最近的頂點。因此,我們只要簡單的計算一個剛體中所有的頂點到碰撞向量的法向量那條直線的距離,得出最小距離的那個點即可,使用如下的幾何公式,就能夠得到我們想要的距離:



        上面公式中的N表示的是碰撞向量,R0是座標系的原點,R是剛體上的點,d就是R點到碰撞向量法向量那條直線的距離。一旦我們計算出了剛體中每一個頂點到這條直線的距離,我們選擇具有最小距離的那個頂點作爲碰撞頂點。注意,上面公式中的d可能爲負值。一條直線將一個空間分爲兩個不同的半空間,如果點R坐落在直線的法向向量所指向的空間中,那麼這個d就是正值,如果坐落在另外一個方向,那麼這個d就是負值。所以,碰撞向量所指向的方向十分重要。在我們的實現中,我們總是認爲碰撞向量的方向是包含碰撞頂點的那個剛體。

        碰撞邊界就更加容易尋找到。還記得我們前面通過將一個剛體投影到邊界的法線上去尋找最小的碰撞向量嗎?碰撞邊界實際上就是產生這個最小碰撞向量的邊界。

        是時候將這些內容編寫成代碼實現了。首先,我們需要擴展前面設計的類,以此來容納我們這裏需要的碰撞邊界和碰撞頂點的信息:

struct {
  float Depth;
  Vec2  Normal;

  Edge*   E;
  Vertex* V;
} CollisionInfo;

        然後,我們就能夠重新編寫我們的DetectCollision函數,以此來獲取額外的信息:

bool Physics::DetectCollision( PhysicsBody* B1, PhysicsBody* B2 ) {
  float MinDistance = 10000.0f;
  for( int I = 0; I < B1->EdgeCount + B2->EdgeCount; I++ ) { //Same old
    Edge* E;

    if( I < B1->EdgeCount )
      E = B1->Edges[ I ];
    else
      E = B2->Edges[ I - B1->EdgeCount ];

    Vec2 Axis( E->V1->Position.Y - E->V2->Position.Y, E->V2->Position.X - E->V1->Position.X );
    Axis.Normalize();

    float MinA, MinB, MaxA, MaxB;
    B1->ProjectToAxis( Axis, MinA, MaxA );
    B2->ProjectToAxis( Axis, MinB, MaxB );

    float Distance = IntervalDistance( MinA, MaxA, MinB, MaxB );

    if( Distance > 0.0f )
      return false;
    else if( abs( Distance ) < MinDistance ) {
      MinDistance = abs( Distance );

      CollisionInfo.Normal = Axis;
      CollisionInfo.E      = E; //Store the edge, as it is the collision edge
    }
  }

  CollisionInfo.Depth = MinDistance;

  //Ensure that the body containing the collision edge lies in 
  //B2 and the one containing the collision vertex in B1
  if( CollisionInfo.E->Parent != B2 ) { 
    PhysicsBody* Temp = B2;
    B2 = B1;
    B1 = Temp;
  }

  //This is needed to make sure that the collision normal is pointing at B1
  int Sign = SGN( CollisionInfo.Normal*( B1->Center - B2->Center ) ); 

  //Remember that the line equation is N*( R - R0 ). We choose B2->Center 
  //as R0; the normal N is given by the collision normal

  if( Sign != 1 )
    CollisionInfo.Normal = -CollisionInfo.Normal; //Revert the collision normal if it points away from B1


  float SmallestD = 10000.0f; //Initialize the smallest distance to a high value
  for( int I = 0; I < B1->VertexCount; I++ ) {
    //Measure the distance of the vertex from the line using the line equation
    float Distance = CollisionInfo.Normal*( B1->Vertices[ I ]->Position - B2->Center ); 

    //If the measured distance is smaller than the smallest distance reported 
    //so far, set the smallest distance and the collision vertex
    if( Distance < SmallestD ) { 
      SmallestD = Distance;
      CollisionInfo.V = B1->Vertices[ I ];
    }
  }

  return true;
}

        在上面的代碼中,我們爲PhysicsBody添加了另外一個新的成員屬性center。這個center會在碰撞步驟之前進行計算更新,我們只要對所有的頂點求平均值即可得到。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章