2D多邊形碰撞檢測和反饋(轉)

2D多邊形碰撞檢測和反饋
介紹
這是一篇論證如何在2D動作遊戲中執行碰撞檢測的文章(Mario,宇宙入侵者等),爲了保證它的高效性和精確性,碰撞檢測是以多邊形爲基礎的,而不是以sprite爲基礎。這是兩種不同的設計途徑。
基於sprite的檢測執行的是檢測sprites的像素的交叉,以這種方式來檢測碰撞。多邊形是使用向量數學來精確的計算點,時間和碰撞的方向。當多邊形只是一種近似sprite自身的時候,它就超越了sprite系統。
表現出了更精確的物理現象,例如彈性,摩擦,在一個隨機的形狀中來處理斜坡。
碰撞檢測相對一個高速的sprite來講是更加的精確的。在一個基於sprite的系統中,對象有可以在一個強力的跳躍中因爲速度太快而相互穿越。
這是一個基於向量數學的系統,所以可以擴展到3D,但是一個sprite碰撞系統被完全的限制到了2D中。
特色:
因爲運算法則的限制,系統只能操作凸多邊形,例如三角形,矩形,五邊形,圓形。在一個非凸多邊形中,你可以將這些多邊形分成三角形來處理。
無論是快速或者慢速的多邊形,他的處理方式都是一樣的。不管對象的移動有多快,碰撞也不會丟失的。他也可以來處理交疊,分開已經交叉的對象。
裏面的範例也有線段的交叉。這可以用來模擬子彈。
它也提供了一些簡單的物理系統,模擬彈力,一些基本的摩擦力和靜摩擦力。在斜坡上的一些特性它也可以模擬。
這裏也有一個剛體系統的例子,引用的是Chrsi Hecker的物理文章。
侷限性:
碰撞的種類,我的意思是它不能順序的處理碰撞。在一個快速的對象中這可能會有問題。一旦一個碰撞被檢測了,它便會直線的離開。你可以檢測到第一個碰撞並處理它,然後再找到其他的碰撞。但是在一個2D的動作遊戲中,這是很具有殺傷力的。
必要條件:
一些普通的原料,一個編譯器,範例是基於GLUTGL開發包)的框架,所以需要下載一個小的GLUT SDK和一個兼容opengl的顯卡。
你還需要對向量數學有一個基本的瞭解。這裏的文章不準備對點乘進行講解,但是它可以幫助你對向量的操作有更好的瞭解。
對於剛體,你需要對Chris Hecker的文章仔細的閱讀一下。它需要你付出更多,但是它是值得的,並不是多麼的困難。
內容列表:
文章1:分離軸的方法
文章2:擴展應用於碰撞反饋的分離軸的方法。
文章3:對快速移動的對象來檢測他的碰撞。
文章4:基本的Arcade碰撞反饋
文章5:處理旋轉
文章6:計算碰撞點
文章7:剛體動力學。
分離軸的方法:
這是碰撞檢測的核心,原理是非常簡單的,也非常的容易實現。它也是快速和穩定的,因爲沒有除法在運算中被使用。我將會帶給大家一個簡單的兩個BOX碰撞測試的例子。


該運算法則試着來確定兩個對象中的一個合適的面。如果有這樣的一個面存在,如果有這樣的面存在,說明對象是分離的,沒有交叉。
爲了確定對象是否是分離的。只要將這個對象投影到這個平面的法線上,然後比較它們之間的間隔看他們是否交叉。
所以,很明顯有無數個邊能夠適合這兩個分離的對象,但是已經證明你只要測試少數的幾個面就可以了,對於上面圖形中的兩個BOX,你可以看到邊的法線是形狀B的邊。
從上面的這些形狀中可以發現,那些即將被測試的分離面是兩個BOX的邊的法線。對於這兩個BOX而言,你只需要測試4個分離的面。在這4個面中,一旦你發現一個分離面分隔了這兩個BOX。你就可以知道了這兩個BOX是分離的。然後返回一個沒有碰撞的標記。
如果這4個面沒有分隔這兩個BOX,那麼這兩個BOX肯定是交叉的,說明他們有一個碰撞。
擴展到一般的多邊形,該運算法則仍然是適用的。只不過是測試的邊變化了。那些分離的面的法線是和每個多邊形的邊的垂直方向是平行的。在下面的圖形中,你可以看到有兩個分離面被測試。在紅色的面被測試的時候,你可以看到那兩個間隔是交叉的,但是在藍色的被測試的時候,間隔並沒有交叉。所有藍色的面是分離面,對象因此是沒有交叉的。


現在,我們已經有一個檢測他們是否碰撞的法則了。這些代碼也能夠被擴展爲3D
所有的分隔軸都需要被測試
計算的是每一個多邊形所在的軸的間隔(分離面的法線)
測試這些間隔是否交叉。

bool Intersect(Polygon A, Polygon B)
{
     for(I = 0; I < A.num_edges; I ++)
     {
           Vector N = Vector(-A.EdgeDir[I].y, A.EdgeDir[I].x);
           if (AxisSeparatePolygons(N, A, B))
                 return false;
      }
      for(I = 0; I < B.num_edges; I ++)
      {
            Vector N = Vector(-B.EdgeDir[i].y, B.EdgeDir[I].x);
            if (AxisSeparatePolygons (N, A, B))
                  return false;
      }
      return true;
}

 

void CalculateInterval(Vector Axis, Polygon P, float& min, float& max)
{
      float d = Axis dot P.vertex[0];
      min = max = d;
      for(I = 0; I < P.num_vertices; I ++)
      {
            float d = P.vertex[I] dot Axis;
            if (d < min)
                  min = d;
            else
                  if(d > max)
                        max = d;
      }
}


到這裏,這個計算兩個2D多邊形碰撞的運算法則是非常快和穩定的。邊的方向不一定是單位化的,所以你可以避免將邊的方向都存儲起來,你可以直接根據多邊形的頂點來構建這些邊的方向。

for(J = A.num_vertices-1, I = 0; I < A.num_vertices; J = I, I ++)
{
      Vector E = A.vertex[I] – A.vertex[J];
      Vector N = Vector(-E.y, E.x);

      if (AxisSeparatePolygons(N, A, B))
            return false;
}

擴展用於碰撞反饋的分離軸的方法:
檢測多邊形是否碰撞是非常有用處的,但是我們可以做的更多。當多邊形交叉後,我希望能夠讓多邊形分離來制止他們交叉。
分隔軸的方法還是要被用到的,通過作一個細微的額外工作。他能夠返回他穿刺的深度和方向,並以此來分離它們。一個交叉的深度和方向的結合體也被稱爲MTD,或者是最小的轉換距離。這個最小的向量需要讓對象分離來制止他們交叉。
我們可以藉助於分離軸來計算MTD
當對象交叉的時候,我們知道了兩個對象的交叉的每個分離軸的間隔距離。沿着這個軸的兩個間隔的交叉的量提供了一個推進向量,你需要將其應用於這兩個對象來讓這些對象的投影來制止他們在這個軸上的交叉。


"
推力向量"是一個向量,它的作用是你需要應用到A中以便制止他同B交叉。
很明顯,你不能讓這個對象沿着一個隨機的軸來分離。待選的這些軸中應該選擇那個交叉的間隔最小的軸。這個推力向量提供了一個最小的轉換的距離。

bool Intersect(Polygon A, Polygon B, Vector& MTD)
{
       // potential separation axes. they get converted into push
       vectors Vector Axis[32];
       // max of 16 vertices per polygon
       int iNumAxis = 0;
       for(J = A.num_vertices–1, I = 0; I < A. num_vertices; J = I, I ++)
       {
              Vector E = A.vertex[I] – A.vertex[J];
              Axis[iNumAxis++] = Vector(-E.y, E.x);
       
       if (AxisSeparatePolygons(N, A, B))
                     return false;
       }
       for(J = B. num_vertices–1, I = 0; I < B.num_vertices; J = I, I ++)
       {
              Vector E = B.vertex[I] – B.vertex[J];
              Axis[iNumAxis++] = Vector(-E.y, E.x);
       
              if (AxisSeparatePolygons (N, A, B))
                     return false;
       }
       
       // find the MTD among all the separation vectors
       MTD = FindMTD(Axis, iNumAxis);

       // makes sure the push vector is pushing A away from B
       Vector D = A.Position – B.Position;
       if (D dot MTD < 0.0f)
              MTD = -MTD;

       return true;
}

 

bool AxisSeparatePolygons(Vector& Axis, Polygon A, Polygon B)
{
       float mina, maxa;
       float minb, maxb;

       CalculateInterval(Axis, A, mina, maxa);
       CalculateInterval(Axis, B, minb, maxb);

       if (mina > maxb || minb > maxa)
              return true;

       // find the interval overlap
       float d0 = maxa - minb;
       float d1 = maxb - mina;
       float depth = (d0 < d1)? d0 : d1;

       // convert the separation axis into a push vector (re-normalise
       // the axis and multiply by interval overlap)
       float axis_length_squared = Axis dot Axis;

       Axis *= depth / axis_length_squared;
       return false;
}

 

Vector FindMTD(Vector* PushVectors, int iNumVectors)
{
       Vector MTD = PushVector[0];
       float mind2 = PushVector[0] dot PushVector[0];
       for(int I = 1; I < iNumVectors; I ++)
       {
              float d2 = PushVector[I] * PushVector[I];
              if (d2 < mind2)
              {
                     mind2 = d2;
                     MTD = PushVector[I];
              }
       }
       return MTD;
}


當對象交叉的情況下,你知道了MTD向量,分離他們就簡單了。

A.Postion += MTD * 0.5f;
B.Position -= MTD * 0.5f;



這只不過是一個投影數學。如果間隔是分離的,我們就需要計算一下這兩個間隔接觸的時間。
比較一下那個"靜態"的分離軸的計算方法,有一個額外的軸我們需要測試,這很明顯是一個相對於位移向量(速度向量)的軸。
所以對於每個分離軸,有三種選擇。
間隔重疊
間隔分離,但是在未來的某個時間會重疊。
間隔分離,但是在未來的某個時間不會重疊或者是碰撞的很晚。
3個選項的意思是對象不會在這幀上碰撞,分離軸真正的分離裏這些對象。在這兩個對象之間在這一幀上沒有任何的碰撞。
AxisSeparatePolygon()
函數返回交疊的量或者碰撞的時間,爲了區分這兩種情況,當一個交疊被發現的時候,一個負數被返回,如果在將來一個碰撞被檢測到了,一個整數被返回的,函數類似下列的函數:

bool AxisSeparatePolygons(Vector Axis, Polygon A, Polygon B, Vector Offset, Vector Vel, float& t, float tmax);


Offset
AB的相對的位置,VelAB的相對的速度。
當找到碰撞面的時候,對於MTD是非常容易被找到的,但是對於在未來的某個時間的碰撞則優先於交叉的,如果碰撞在未來的某個時間被發現,最近一個會被選擇。
如果什麼都沒有發現,我只處理交叉,象以前那樣,最小的交疊會被使用。
碰撞計算的函數然後返回碰撞的法線,碰撞的深度(一個負數)或者碰撞的時間(一個正數)。
最終的僞代碼如下:

bool Collide(     const Vector* A, int Anum,
                            const Vector* B, int Bnum,
                            const Vector& xOffset, const Vector& xVel,
                            Vector& N, float& t)
{
       if (!A || !B) return false;
            
       // All the separation axes
       // note : a maximum of 32 vertices per poly is supported
       Vector xAxis[64];
       float taxis[64];
       int iNumAxes=0;

       xAxis[iNumAxes] = Vector(-xVel.y, xVel.x);
       float fVel2 = xVel * xVel;
       if (fVel2 > 0.00001f)
       {
              if (!IntervalIntersect( A, Anum, B, Bnum, xAxis[iNumAxes], xOffset, xVel, taxis[iNumAxes], t))
                      return false;
              iNumAxes++;
       }

       // test separation axes of A
       for(int j = Anum-1, i = 0; i < Anum; j = i, i ++)
       {
              Vector E0 = A[j];
              Vector E1 = A[i];
              Vector E = E1 - E0;
              xAxis[iNumAxes] = Vector(-E.y, E.x);
              
              if (!IntervalIntersect( A, Anum, B, Bnum, xAxis[iNumAxes], xOffset, xVel, taxis[iNumAxes], t))
                     return false;

              iNumAxes++;
       }

       // test separation axes of B
       for(int j = Bnum-1, i = 0; i < Bnum; j = i, i ++)
       {
              Vector E0 = B[j];
              Vector E1 = B[i];
              Vector E = E1 - E0;
              xAxis[iNumAxes] = Vector(-E.y, E.x);

              if (!IntervalIntersect( A, Anum, B, Bnum, xAxis[iNumAxes], xOffset, xVel, taxis[iNumAxes], t))
                     return false;
              iNumAxes++;
       }
       
       if (!FindMTD(xAxis, taxis, iNumAxes, N, t))
              return false;

       // make sure the polygons gets pushed away from each other.
       if (N * xOffset < 0.0f)
              N = -N;

       return true;
}

 

bool AxisSeparatePolygons ( Vector N, Polygon A, Polygon B, Vector Offset, Vector Vel, float &t, float tmax)
{
       float min0, max0;
       float min1, max1;

       CalculateInterval(N, A, min0, max0);
       CalculateInterval(N, B, min1, max1);
       
       float h = Offset dot N;
       min0 += h;
       max0 += h;

       float d0 = min0 - max1; // if overlapped, do < 0
       float d1 = min1 - max0; // if overlapped, d1 > 0

       // separated, test dynamic intervals
       if (d0 > 0.0f || d1 > 0.0f)
       {
              float v = Vel dot N;

              // small velocity, so only the overlap test will be relevant.
              if (fabs(v) < 0.0000001f)
                     return false;

              float t0 =-d0 / v; // time of impact to d0 reaches 0
              float t1 = d1 / v; // time of impact to d0 reaches 1
              // sort the times.
              if (t0 > t1)
              {
                     float temp = t0;
                     t0 = t1;
                     t1 = temp;
              }
              // take the minimum positive
              taxis = (t0 > 0.0f)? t0 : t1;

              // intersection time too late or back in time, no collision
              if (taxis < 0.0f || taxis > tmax)
                     return true;

              return false;
       }
       else
       {
              // overlap. get the interval, as a the smallest of |d0| and |d1|
              // return negative number to mark it as an overlap
              taxis = (d0 > d1)? d0 : d1;
              return false;
       }
}

 

bool FindCollisionPlane (Vector* Axis, float* taxis, int iNumAxes, Vector& Ncoll, float& tcoll)
{
       // find collision first
       int mini = -1;
       tcoll = 0.0f;
       for(int i = 0; i < iNumAxes; i ++)
       {
              if (taxis[i] > 0.0f)
              {
                     if (taxis[i] > tcoll)
                     {
                            mini = i;
                            tcoll = taxis[i];
                            Ncoll = Axis[i];
                            Ncoll.Normalise(); // normalise axis
                     }
              }
       }

       // found a collision
       if (mini != -1)
              return true;

       // nope, find overlaps
       mini = -1;
       for(int i = 0; i < iNumAxes; i ++)
       {
              float n = Axis[i].Normalise(); // axis length

              taxis[i] /= n; // normalise interval overlap too

              // remember, those numbers are negative, so take the closest to 0
              if (mini == -1 || taxis[i] > tcoll)
              {
                     mini = i;
                     tcoll = taxis[i];
                     Ncoll = Axis[i];
              }
       }
       
       return (mini != -1);
}


這就是全部了,一個檢測多邊形的碰撞的系統返回一個未來的時間或者是當交叉的時候返回那個碰撞的面。

 

 

很明顯了,如果對象A是靜態的,例如是環境的一部分,整個MTD都應用到了B(B.position-=MTD)
如何來處理快速移動的對象
以上的方法只能處理慢速的對象,但是當對象快速的移動的時候,碰撞會不準確,忽略掉一些碰撞,甚至是允許對象互相穿越,這是一種糟糕的情況。
我們可以再次的使用分離軸的方法,擴展到他的將來,使用這一規則來在未來的某個時間來計算碰撞,原理是一樣的,以下的這個圖片就是一個很好的解釋:
發佈了28 篇原創文章 · 獲贊 11 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章