遊戲裏實現碰撞檢測方法

幾乎所有的3D遊戲都離不開碰撞檢測——無論是各物體之間的碰撞檢測,還是物體與場景之間的碰撞檢測。在真實世界中,你是很自然地不能穿牆而過的,所以很多人在玩各種3D遊戲的時候自然而然的忽視了碰撞檢測這一過程的存在。然而,碰撞檢測的過程是重要的。如果沒有它,你在CS中就會毫無遮攔的飛來飛去——如果考慮重力的話,則會一直不停地往下掉,直到符點數溢出(或你受不了這一漫長的過程而離開遊戲)爲止。碰撞檢測是在編程時實現的。不要以爲碰撞檢測是在顯示3D圖像的同時由顯卡完成的——這是幼稚的看法——現在沒有什麼硬件能支持碰撞檢測。

  看了這些後你可能對碰撞檢測產生了興趣,或者你作爲一個3D編程愛好者正在尋找碰撞檢測的原理和方法,或者已經找了很長時間還沒找到(我是不是有點貧:P),那麼,就讓我們一起來看一種十分有效而又易於理解的算法吧。
  這個算法是針對物體與場景之間的碰撞檢測的。它要求你的場景是以很多的三角形組成的。目前幾乎所有的3D程序的場景或者物體都是由許多三角形組成的,所以這一點不成問題。另外,你需要記錄物體在上一幀的位置和當前幀的位置。這也很容易辦到。除了這些,就每什麼其他的要求了。那麼我們來看一下它的原理。

一、原理

  首先,根據物體的運動規律或用戶的輸入計算物體在即將渲染的一幀(當前幀)時的位置(這時還不用考慮碰撞檢測的問題)然後,循環遍歷場景中每一個三角形。在循環過程中,作如下操作:

1、找到當前三角形所在的平面,我們暫且稱之爲平面S。沿平面法線方向對它做一個平移d,表示物體和平面之間所能接近的距離。

2、判斷物體在上一幀和當前幀時的位置OldPosition、NewPosition與平面S的關係:如果上一幀在平面前,而當前幀在平面後,如下圖所示:
null
則進一步做3。
否則跳過3、4,做5。

3、因爲前後兩幀物體在平面的異側,說明物體穿過了平面S。但這時還不能說物體與三角形發生了碰撞,因爲平面是無邊界的,還需進一步判斷物體是否在三角形三條邊範圍之內穿過平面S。過三角形的三邊,做垂直於三角形的平面PS1,PS2,PS3。並且令它們的法線指向三角形的內部,如下圖所示:

爲了體現物體和三角形所能接近的最短距離,這三個平面也需要做一個平移L,只不過是沿着其法線負方向:

我們將依靠這三個平面來判斷物體是否是在三角形三邊所界定的範圍之內穿過平面S的。但是,如果三角形有很尖利的銳角,就會使其界定的區域過大,如figure4中所示。所以我們需要另外三個與它們平行的平面PS4,PS5,PS6 來削去產生的銳角(figure5)。產生這三個平面很容易,只需將PS1、PS2、PS3分別向它們的法線正方向平移到與之相對的頂點,再加上L。


現在,判斷物體位置是否在這六個平面之內,如果是,則做4,否則做5。

4、我們已經能夠確定物體與當前的三角形發生了碰撞,這時,修正當前幀的位置NewPosition,使物體的運動沿着與平面S平行的方向。

5、我們已經能夠確定物體與當前三角形沒有發生碰撞。使下一三角形成爲當前三角形,回到1。

當所有的三角形都遍歷了一次之後,物體的位置NewPosition就是經過碰撞檢測並修正了之後的。以該位置渲染物體和場景,並在下一幀之前,更新舊的位置爲當前幀的位置(OldPosition = NewPosition)。

總之,該算法的原理就是:給出一個三角形——判斷物體是否穿過了三角形所在的面——再判斷物體是否是在三角型內部穿過的。下面我們來看一看它的具體實現。

二、具體實現

在具體的實現中,我們可能會遇到下面這些問題。

首先,如何得到三角形所在的平面呢?一個平面可以由法向量和它與原點的距離來表示,假設我們有一個三角形ABC,其所在平面爲 S ,如下圖所示:


其頂點位置爲三個三維向量a,b,c。我們首先得到三角形的法向量 N:(下面的vector 表示三維向量,float表示浮點型標量)

vector v1 = b - a '由 a 指向 b 的邊向量
vector v2 = c - a '由 a 指向 c 的邊向量。

vector N = Normalize(v1 X v2) ' X代表叉乘,注意叉乘是有順序的,如果v1和v2互換,結果向量將會反向。Normalize表示將所得結果單位化。所得的N就是三角形的法向量

vector S.N = N '三角形的法向量也就是平面 S 的法向量

然後,我們計算該平面與原點的距離 S.D:

float S.D = N * a ' *代表點乘,計算結果爲三角形頂點a到法線S.N上的投影的長度,即距離S.D,爲一標量。

有了法向量和距離,就可以表示一個平面了。如果想要對平面進行平移,只需修改 S.D 的值。

那麼,怎樣判斷物體在平面的那一邊呢?只需要比較物體位置在平面法向量上的投影長度和平面的 D 值就行了。假定我們的物體位置用3維向量 p 表示

float dp = S.N * p '點乘,dp爲物體位置在平面 S 法線 N 上的投影長度,爲一標量。

if (dp - S.D)>0 ,在平面 S 正面。
if (dp - S.D)m_pMesh == NULL)
    return E_FAIL;
  if(lpPs == NULL)
    return E_FAIL;
  if(lpMap->m_dwNumIndices == 0)
    return E_FAIL;
  if(lpMap->m_dwNumVertices == 0)
    return E_FAIL;
 

在DirectX 8 Graphics中,場景的網格模型頂點緩衝和索引緩衝組成。頂點緩衝保存了所有的頂點的信息,而索引緩衝中的每三個元素代表一個三角形,這些元素的值是該三角形的三個頂點在頂點緩衝中的索引號。我們可以由此獲得三角形的三個頂點的位置。下面的代碼鎖定網格模型中的頂點和索引緩衝,並得到指向它們的指針。

  lpMap->m_pMesh->LockIndexBuffer(D3DLOCK_READONLY,(BYTE**)&pIndexData);
  lpMap->m_pMesh->LockVertexBuffer(D3DLOCK_READONLY,(BYTE**)&pVertexData);
 然後,遍歷網格的每一個三角形:

  for(WORD i = 0;i m_dwNumIndices; i+=3)
  {
    WORD a = pIndexData[i+0]; //a:三角形的第一個頂點的索引
    WORD b = pIndexData[i+1]; //b:三角形的第二個頂點的索引
    WORD c = pIndexData[i+2]; //c:三角形的第三個頂點的索引
    D3DXVECTOR3 v1 = pVertexData[b].p - pVertexData[a].p;
           //v1:邊ab的向量
    D3DXVECTOR3 v2 = pVertexData[c].p - pVertexData[a].p;
           //v2:邊ac的向量

    D3DXVec3Cross(&vNormal,&v1,&v2); //叉積得到三角形法向量。
    D3DXVec3Normalize(&vNormal,&vNormal);

    fdistance = D3DXVec3Dot(&vNormal,&pVertexData[a].p)+5.0f;
    //頂點a 在法向量上的投影長度,即平面與原點的距離。

    if(PreCheckCollision(lpPs,&vNormal,fdistance) == 1)
    {//如果玩家穿過了該三角形的平面

      if(CheckCollision(lpPs,a,b,c,&vNormal,pVertexData) == 1)
      {//如果是在三角形三邊範圍之內穿過的

        //修改玩家的位置,使之不能穿牆。
        lpPs->vnewPos += vNormal*(fdistance
        - D3DXVec3Dot(&vNormal,&lpPs->vnewPos)+0.2);

      }
    }
    //否則,繼續循環,判斷下一三角形。
  }
 //循環結束,在退出前必須解鎖頂點和索引緩衝。
  lpMap->m_pMesh->UnlockVertexBuffer();
  lpMap->m_pMesh->UnlockIndexBuffer();
  return S_OK;
}
下面是函數PreCheckCollision()和CheckCollision()的代碼。
//=================================================
// 函數名:PreCheckCollision()
// 功能: 判斷玩家是否穿過了當前平面。
// 參數說明:
// _Player_State* lpps: 玩家的狀態,包含位置信息。
//       vNromal: 當前平面的法向量(已單位化)
//      fdistance: 當前平面與原點的距離
// 返回值: 如果需要進一步檢測,返回1
//     否則返回0。
//-------------------------------------------------
INT PreCheckCollision(Player_State* lpps,D3DXVECTOR3* pvNormal,FLOAT fDist)
{
  float StartSide,EndSide;
  //計算玩家的舊位置與平面的距離
  StartSide = D3DXVec3Dot(pvNormal,&lpps->vOldPos) - fDist;
  //計算玩家的新位置與平面的距離
  EndSide = D3DXVec3Dot(pvNormal,&lpps->vnewPos) - fDist;

  //如果玩家的舊位置在平面之前而新位置在平面之後,
  //說明穿過了平面。
  if(StartSide>0 && EndSide <=0) return 1;
  else return 0;
}

//==============================================
// 函數名稱:CheckCollision
// 功能: 判斷物體是否在三角形內穿過。
// 參數說明:
// _Player_State* lpps :玩家的狀態,包含位置信息。
//        a,b,c :三角形的頂點在頂點
//            緩衝中的索引。
// D3DXVECTOR3* vNormal:三角形的法向量
// CustomVertex* pVertices:指向頂點緩衝的指針
// 返回值: 如果玩家在三角形內部穿過,返回1
//     否則返回0
//----------------------------------------------
INT CheckCollision(Player_State* lpps,WORD a,WORD b,WORD c,D3DXVECTOR3 * pvNormal,CustomVertex * pVertices)
{
  D3DXVECTOR3 PerPlaneNormal;
  //垂直平面(perpendicular plane)的法向量
  float fPerPlaneDist;
  //垂直平面與原點的距離。

  D3DXVECTOR3 v1;
  //用於記錄三角形當前邊的向量。

//============================================================
// 檢查物體是否在過三角形第一條邊AB的垂直平面
// 和與它相對的平面之內。

// 首先計算三角形的AB邊向量。
  v1 = pVertices[b].p-pVertices[a].p;
// 計算過該邊的垂直平面的法向量。
  D3DXVec3Cross(&PerPlaneNormal,pvNormal,&v1);
  D3DXVec3Normalize(&PerPlaneNormal,&PerPlaneNormal);
// 保證垂直邊的法向量指向三角形內部。
  if(D3DXVec3Dot(&(pVertices[c].p-pVertices[a].p),&PerPlaneNormal)vOldPos)-fPerPlaneDistvOldPos)-fPerPlaneDist>0)
    return 0;

//=============================================================
// 下面檢查物體是否在過邊BC的垂直平面以及它的相對平面之內。
// 步驟與上面基本相似。
  v1 = pVertices[c].p-pVertices[b].p;
  D3DXVec3Cross(&PerPlaneNormal,pvNormal,&v1);
  D3DXVec3Normalize(&PerPlaneNormal,&PerPlaneNormal);
  if(D3DXVec3Dot(&(pVertices[a].p-pVertices[b].p),&PerPlaneNormal)vOldPos)-fPerPlaneDistvOldPos)-fPerPlaneDist>0)
    return 0;

//=============================================================
// 然後是過邊CA的垂直平面和它的相對平面。

  v1 = pVertices[a].p-pVertices[c].p;
  D3DXVec3Cross(&PerPlaneNormal,pvNormal,&v1);
  D3DXVec3Normalize(&PerPlaneNormal,&PerPlaneNormal);
  if(D3DXVec3Dot(&(pVertices[b].p-pVertices[c].p),&PerPlaneNormal)vOldPos)-fPerPlaneDistvOldPos)-fPerPlaneDist>0)
    return 0;

//如果物體在上面任意六個平面之外,則函數早已返回0。
// 如果到這裏還沒有返回,說明物體在六面之內,
// 應返回1。
  return 1;
}
注:上面的三個函數是我按照該算法的原理自己寫的,儘管在我的程序中運行正常,但是由於水平有限,難免會有疏漏之處,而且由於是針對例程而寫,所以並沒有太好的通用性。但是在函數中這種碰撞檢測的算法還是比較忠實地體現出來了的。總之是爲了更進一步的說明該算法。

一些改進

  在上面,我們一直把物體和平面所能接近的最小距離用一個常量表示。但是,這樣做等於是把物體看成一個球體,不能體現出物體形狀上的一些特點。例如,人體的長和寬是大於高的,在碰撞檢測時,最好能夠看成是一個高的橢球體。解決這一問題的一個簡單的方法是根據所要判斷的三角形的法線來計算物體和平面的最小距離,當三角形越陡,就令該距離越小。具體的算法在參考文獻中會給出。

關於例程

  本文所用的例程及源代碼可以點擊這裏下載。由於時間和水平關係,該例程遠遠沒有完善,所以並不具有較好的通用性,而且可能包含一些錯誤。要運行該程序,需要DirectX 8或以上版本。要編譯源程序,需要DirectX 8或以上版本的SDK。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章