天天看點

引擎開發_ 碰撞檢測_GJK 算法詳細介紹

概述

和SAT(分離軸定理)算法一樣,GJK算法也隻對凸體有效。 GJK算法的優勢是:通過support函數(後面會詳細講述),進而支援任何凸體形狀之間的碰撞檢測;相比SAT算法,你不需要一些額外的操作,比如增加特殊的代碼和算法處理曲面形狀。

   GJK是一個疊代算法,但是如果事先給出穿透/分離向量,則它的收斂會很快,可以在常量時間内完成。在3D環境中,GJK可以取代SAT算法。GJK算法的最初目的是計算兩個凸體之間的距離,在兩個物體穿透深度比較小的情況下,可用它判定物體之間的碰撞。它也可以和别的算法相結合,用來檢測兩個物體之間深度穿透時候的碰撞情況。

凸體(凸多面體或凸多邊形)

面說過,GJK算法隻适用于凸體形狀。凸體(其實就是一條直線穿越凸體,和該凸體殼的交點不能超過2個)的定義在介紹SAT算法時講過,可參照那篇文章了解相關資訊。

明可夫斯基和(Minkowski Sum)

 GJK算法中使用了明可夫斯基和的概念。明可夫斯基和很好了解,假設有兩個物體,它們的明可夫斯基和就是物體1上的所有點和物體2上的所有點的和集。用公式表示就是:

A + B = {a + b|a∈A, b∈B}

如果兩個物體都是凸體,它們的明可夫斯基和也是凸體。

對于減法,明可夫斯基和的概念也成立,這時也可稱作明可夫斯基差。

A – B = A + (-B) = {a + (– b)|a∈A, b∈B} = {a – b)|a∈A, b∈B}

接着往下講,在兩個物體之間執行明可夫斯基差操作的解釋如下:

如果兩個物體重疊或者相交,它們的明可夫斯基差肯定包括原點。

引擎開發_ 碰撞檢測_GJK 算法詳細介紹

我們看一個例子,圖1中兩個物體進行明可夫斯基差操作,将得到圖2的形狀。可以看到,該形狀包含 原點 ,這是因為這兩個物體是相交的。

引擎開發_ 碰撞檢測_GJK 算法詳細介紹

執行這些操作需要物體1的頂點數*物體2的頂點數*2(原作者用的二維向量,如果在三維空間,當然就是*3了,如果是向量減法數量就什麼都不用乘了) 個減法操作。物體包含無窮多個點,但由于是凸體,我們可以隻對它們的頂點執行明可夫斯基差操作。在執行GJK算法過程中,實際上我們并不需要顯式計算物體之間明可夫斯基差,這也是GJK算法的優勢所在。

單純形(Simplex)

 我們不需要顯式計算物體之間的明可夫斯基差,隻要知道它們的明可夫斯基差是否包含原點就ok了。如果包含原點,物體之間就相交,否則,則不相交。

   我們可以在明可夫斯基差形成的物體内疊代的形成一個多面體(或多變形),并使這個多面體盡量包圍原點。如果這個多面體包含原點,顯然明可夫斯基差形成的物體必然包括原點。這個多面體就稱作單純形。

Support函數

下面我們講述如何建立一個單純形?首先看什麼是support函數,給定兩個凸體,該函數傳回這兩個凸體明可夫斯基差形狀中的一個點。我們知道,物體1上的一個點,它的位置減去物體2上的一個點的位置,可以得到它們明可夫斯基差形狀上的一個點,但我們不希望每次都得到相同的點。如何保證做到這一點呢?我們可以給support函數傳遞一個參數,該參數表示一個方向(direction),方向不同,得到的點也不同。

    在某個方向上選擇最遠的點有重要作用,因為這樣産生的單純形包含最大的空間區域,增加了算法快速傳回的可能。另外,通過這種方式傳回的點都在明可夫斯基差形狀的邊上。如果我們不能通過一個過原點的方向在單純形上增加一個點,則明可夫斯基差不過原點,這樣在物體不想交的情況下,算法會很快退出。

public Point support(Shape shape1, Shape shape2, Vector d) {
 // d is a vector direction (doesn't have to be normalized)
 // get points on the edge of the shapes in opposite directions
 Point p1 = shape1.getFarthestPointInDirection(d);
 Point p2 = shape2.getFarthestPointInDirection(d.negative());
 // perform the Minkowski Difference
 Point p3 = p1.subtract(p2);
 // p3 is now a point in Minkowski space on the edge of the Minkowski Difference
 return p3;
 }
           

下面這個例子使用圖2的物體形狀,執行函數三次

開始操作,使用d = (1, 0)

p1 = (9, 9);
p2 = (5, 7);
p3 = p1 - p2 = (4, 2);
           

第二步,使用d = (-1, 0)

p1 = (4, 5);
p2 = (12, 7);
p3 = p1 - p2 = (-8, -2);
           

注意:p1可能是 (4, 5) 或者 (4, 11)。這兩個都将在明可夫斯差形狀的邊上産生一個點。

下一步 假定 d = (0, 1)

p1 = (4, 11);
p2 = (10, 2);
p3 = p1 - p2 = (-6, 9);
           

這樣,我們得到圖3所示的單純形。

引擎開發_ 碰撞檢測_GJK 算法詳細介紹

判定碰撞

 前面我們說過,兩個物體的明可夫斯基差中的單純形包含原點時候,這個兩個物體相交。在圖3中,單純形沒有包含原點,但實際上,這兩個物體是相交的。問題在于我們選擇的方向,在第三步中,如果我們選擇d = (0, -1) 作為方向,那麼

p1 = (4, 5);
p2 = (5, 7);
p3 = p1 - p2 = (-1, -2);
           

這樣産生的單純形如圖4所示,顯然它包含了原點,我們由此能夠判定這兩個物體之間有碰撞。

引擎開發_ 碰撞檢測_GJK 算法詳細介紹

可見,方向的選擇影響輸出結果。如果我們得到的單純形不包含原點的話,我們能夠用另一個點代替,産生新的單純形來判斷是否碰撞。這也是這個算法需要疊代的原因。我們不能保證我們最初選擇的三個點包含原點,也不能保證明可夫斯基差形狀包含原點。

   如果我們改變第三個明可夫斯基差圖形中選擇點的方式,我們就能夠包圍原點,改變的方式如下所示:

d = ...
a = support(..., d)
d = ...
b = support(..., d)
AB = a - b
AO = a - ORIGIN
d = AB x AO x AB
c = support(..., d)
           

c 中使用的方向 d 依賴于 a 和 b 形成的直線,通過用相反的方向,我們可以選擇離 a 最遠的 b 點。

d = ...
a = support(..., d)
b = support(..., -d)
AB = a - b
AO = a - ORIGIN
d = AB x AO x AB
c = support(..., d)
           

  現在,我們僅需要為第一次的明可夫斯基差圖形求邊界交點選擇方向。當然我們也可以選擇任意的方向,比如兩個物體形狀中心的差作為方向等等。怎樣選擇有很多的優化工作去做。

疊代

即使我們使用上面的方法,也仍有可能在三步内不能得到包含原點的單純形,是以我們必須用疊代的方法建立單純形,每次生成的單純形比上次更接近包含原點。我們也需要檢查兩個條件:1)現在的單純形包含原點嗎? 2)我們能夠包含原點嗎?

下面我們看看疊代算法主要代碼:

Vector d = // choose a search direction
 
 // get the first Minkowski Difference point
 Simplex.add(support(A, B, d));
 
//下面開始循環: 第一次疊代
// negate d for the next point
 d.negate();
 // start looping
 while (true) {
  // add a new point to the simplex because we haven't terminated yet
  Simplex.add(support(A, B, d));
  // make sure that the last point we added actually passed the origin
  if (Simplex.getLast().dot(d) <= 0) {
  // if the point added last was not past the origin in the direction of d
  // then the Minkowski Sum cannot possibly contain the origin since
  // the last point added is on the edge of the Minkowski Difference
  return false;
  } else {
 // otherwise we need to determine if the origin is in
  // the current simplex
  if (Simplex.contains(ORIGIN)) {
    // if it does then we know there is a collision
   return true;
    } else {
    // otherwise we cannot be certain so find the edge who is
   // closest to the origin and use its normal (in the direction
  // of the origin) as the new d and continue the loop
   d = getDirection(Simplex);
 }
 }
}
           

下面我們示範一下這個算法架構在圖1的例子中如何工作。我們假設最初的方向是2個物體中心的連線的方向。

d = c2 - c1 = (9, 5) - (7.5, 6) = (1.5, -1);
p1 = support(A, B, d) = (9, 9) - (5, 7) = (4, 2);
Simplex.add(p1);
d.negate() = (-1.5, 1);
           

下面開始循環:

第一次疊代

last = support(A, B, d) = (4, 11) - (10, 2) = (-6, 9);
//we past the origin so check if we contain the origin
// we dont because we are line // get the new direction by (AB x AO) x AB = B(A.dot(C)) - A(B.dot(C))
AB = (-6, 9) - (4, 2)  = (-10, 7);
AO = (0, 0) - (-6, 9) = (6, -9);
ABxAOxAB = AO(149) - AB(-123)
              = (894, -1341) - (1230, -861)
              = (-336, -480)
              = (-0.573, -0.819)
           
引擎開發_ 碰撞檢測_GJK 算法詳細介紹

第一次疊代的結果,這時,我們在明可夫斯基差中有一個線段的單純形(棕色),以及下一次使用的方向(藍色),這個方向過垂直于上次增加的兩個頂點形成的線段(藍色垂直于棕色)。注意,這個方向不需要歸一化,這兒歸一化主要是驗證給定方向的縮放是否有效。

第二次疊代

last = support(A, B, d) = (4, 5) - (12, 7) = (-8, -2)
proj = (-8, -2).dot(-336, -480) = 2688 + 960 = 3648
//we past the origin so check if we contain the origin
//we dont (see Figure 6a)
// the new direction will be the perp of (4, 2) and (-8, -2)
// and the point (-6, 9) can be removed[把離原點較遠的點移去]
AB = (-8, -2) - (4, 2)  = (-12, -4);
AO = (0, 0) - (-8, -2) = (8, 2);
ABxAOxAB = AO(160) - AB(-104)
              = (1280, 320) - (1248, 416)
              = (32, -96)
              = (0.316, -0.948)
           
引擎開發_ 碰撞檢測_GJK 算法詳細介紹

第二次疊代後,單純形仍沒有包含原點,是以我們不能推斷出兩個物體是否相交。在第二次疊代中,我們移去了   (-6, 9) 點,因為我們任何時刻隻需要 3 個點,我們在疊代開始後,會增加一個新的點。

第三次疊代

last = support(A, B, d) = (4, 5) - (5, 7) = (-1, -2)
proj = (-1, -2).dot(32, -96) = -32 + 192 = 160
// we past the origin so check if we contain the origin
// we do (Figure 7)!
           

檢測單純形

在上面的算法中,我們通過圖和僞代碼的形式進行了兩個操作:一個是怎麼知道現在的單純形是否包含原點;另一個是我們怎麼選擇下一次疊代的方向。在前面的僞代碼中,為了便于了解,我把這兩個步驟分開,但實際上他們應該放在一起,應為它們有很多共用的東西。

   通過一系列基于點積操作的面測試(在二維是線測試),我們能夠判定原點位于單純形的什麼位置。首先我們必須處理線段測試,看前面第一次疊代的例子,增加第二個點後(代碼第9行),單純形現在是一條線段(AB)。我們能夠通過Voronoi 區域判定單純形是否包含原點(看圖8).

引擎開發_ 碰撞檢測_GJK 算法詳細介紹

線段的兩個端點是A和B,A是增加到單純形的最後一個頂點。我們知道A和B在明可夫斯基差的邊上,是以原點不能位于R1和R4區域,這是因為11行的代碼沒有傳回false,即AB和AO的點積大于0,是以原點位于R2或者R3區域。當單純形(這兒是線段)沒有包括原點的時候,我們就選擇一個新的方向,準備下一次疊代。這可以通過下面的代碼完成:

// the perp of AB in the direction of O can be found by
AB = B - A;
AO = O - A;
perp = AB x AO x AB;
           

當原點位于線段上的時候,我們将得到零向量,在11行的代碼将會傳回false,如果我們要考慮接觸碰撞(兩個物體正好接觸上),這時就要做一些特殊處理,可以考慮用AB的左手或右手法向作為新的方向。【如果為0,而且原點在AB之間,就可傳回true,直接判定為接觸碰撞】

第二次疊代中個,我們得到一個三角形單純形(ABC)(圖9)

引擎開發_ 碰撞檢測_GJK 算法詳細介紹

圖中白色的區域不會被測試,因為通過了11行代碼的測試[否則會傳回false

]

,顯然原點不會位于該區域。R2區域也不可能包含原點,因為上一個方向是在相反的方向,是以我們需要測試的是R3,R4,R5區域,我們能夠執行AC x AB x AB 得到一個垂直于AB的向量,接着執行 ABPerp.dot(AO) 去判定是否原點在R4區域(小于0的話不在R4)。

AB = (-6, 9) - (-8, -2) = (2, 11)
AC = (4, 2) - (-8, -2) = (12, 4)
// AC x AB x AB = AB(AC.dot(AB)) - AC(AB.dot(AB))
ABPerp = AB(68) - AC(125)
          = (136, 748) - (1500, 500)
          = (-1364, 248)
          = (-11, 2)
// compute AO
AO = (0, 0) - (-8, -2) = (8, 2)
ABPerp.dot(AO) = -11 * 8 + 2 * 2 = -84
// its negative so the origin does not lie in R4
           

通過更多的測試,我們能夠判定原點的位置:

AB = (-6, 9) - (-8, -2) = (2, 11)
AC = (4, 2) - (-8, -2) = (12, 4)
// AB x AC x AC = AC(AB.dot(AC)) - AB(AC.dot(AC))
ACPerp = AC(68) - AB(160)
           = (816, 272) - (320, 1760)
           = (496, -1488)
           = (1, -3)
// compute AO
AO = (0, 0) - (-8, -2) = (8, 2)
ACPerp.dot(AO) = 1 * 8 + -3 * 2 = 2
// its positive so that means the origin lies in R3正值表示在R3,負的表示在R5
           

是以我們能夠判定原點在R3區域。最後,我們還要選擇一個方向,以便得到在此方向上的下一個明可夫斯基點。由于已經知道AC的Voronoi區域包含原點,是以這是很容易實作的:

AC x AO x AC

這時,已經不需要點B,是以我們去掉它。最終代碼如下所示:

Vector d = // choose a search direction
 
	// get the first Minkowski Difference point
	Simplex.add(support(A, B, d));
	// negate d for the next point
	d.negate();
	// start looping
	while (true) {
	  // add a new point to the simplex because we haven't terminated yet
	  Simplex.add(support(A, B, d));
	  // make sure that the last point we added actually passed the origin
	  if (Simplex.getLast().dot(d) <= 0) {
	    // if the point added last was not past the origin in the direction of d
	    // then the Minkowski Sum cannot possibly contain the origin since
	    // the last point added is on the edge of the Minkowski Difference
	    return false;
	  } else {
	    // otherwise we need to determine if the origin is in
	    // the current simplex
	    if (containsOrigin(Simplex, d) {
	      // if it does then we know there is a collision
	      return true;
	    }
	  }
	}
	  
	public boolean containsOrigin(Simplex s, Vector d) {
	  // get the last point added to the simplex
	  a = Simplex.getLast();
	  // compute AO (same thing as -A)
	  ao = a.negate();
	  if (Simplex.points.size() == 3) {
	    // then its the triangle case
	    // get b and c
	    b = Simplex.getB();
	    c = Simplex.getC();
	    // compute the edges
	    ab = b - a;
	    ac = c - a;
	    // compute the normals
	    abPerp = tripleProduct(ac, ab, ab);
	    acPerp = tripleProduct(ab, ac, ac);
	    // is the origin in R4
	    if (abPerp.dot(ao) > 0) {
	      // remove point c
	      Simplex.remove(c);
	      // set the new direction to abPerp
	      d.set(abPerp);
	    } else {
	      // is the origin in R3
	      if (acPerp.dot(ao) > 0) {
	        // remove point b
	        Simplex.remove(b);
	        // set the new direction to acPerp
	        d.set(acPerp);
	      } else{
	        // otherwise we know its in R5 so we can return true
	        return true;
	      }
	    }
	  } else {
	    // then its the line segment case
	    b = Simplex.getB();
	    // compute AB
	    ab = b - a;
	    // get the perp to AB in the direction of the origin
	    abPerp = tripleProduct(ab, ao, ab);
	    // set the direction to abPerp
	    d.set(abPerp);
	  }
	  return false;
	}
           

上面是二維凸多邊形碰撞檢測的代碼。在判斷原點是否包含在多面體中(兩個物體的明可夫斯基差)時,我們使用了在基于三角形的單純形測試法。這是根據Caratheodory定理:一個凸多面體的中任意一個點,能夠被表示為其n+1點的組合。凸多面體是2維的,是以測試時用了三角形(3個點),在3D的情況下,我們則需要測試四面體就ok了(4個點)。

    現在已經完成了GJK碰撞檢測算法教程。最初的GJK算法是計算兩個凸體之間的距離。另外,如果你需要碰撞資訊,比如法向和深度,你應該自己修改GJK算法或者把它和别的算法結合起來。EPA就是一個這樣的算法。

繼續閱讀