[譯]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函數調用的次數越多,我們就越能夠近似的處理這個任務。而這個就給了我們程序員很大的方便,我們可以根據遊戲計算量的分配,來合理的安排到底需要調用幾次這個函數。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章