本文共 8723 字,大约阅读时间需要 29 分钟。
由于排版问题,新文章已经更新到
原文地址: http://www.codezealot.org/archives/88 一、 介绍: GJK和SAT一样,只适用于凸多边形。GJK更为强大的地方在于,它可以通过“支持函数”(稍后讨论)支持所有的形状。因此,和SAT不同,你不用对曲边型使用特殊的代码 或者方法进行特殊处理,就可以使用GJK。 GJK是一个迭代式的方法但是收敛的非常快,如果使用最新的渗透(分离)向量,它几乎是在一个很短的时间内得出结果。它是SAT在3D环境下的一个很好的替代,就因 为SAT要测试的轴de数量非常多。 GJK的最初始的目的是为了计算两个凸边形的距离.GJK同样可以被用作在渗透很短的情况下的碰撞信息,还可以在其他的计算很大的渗透的方法中使用. 二、凸多边形性质: 就像我之前说过的一样,GJK是一个只能被用在凸多边形上的方法。我看上一个关于SAT的博文了解更多关于凸多边形的信息。 三、闵科夫斯基 和(此处有个空格只是为了说明闵科夫斯基是个人名) GJK算法依靠一个很重要的理论叫做,闵科夫斯基和。闵科夫斯基和在原理上很容易理解:如果你有俩个形状,闵科夫斯基和就是形状a的所有的点加上形状b的所有的 点,A + B = {a + b|a∈A, b∈B} 。如果两个形状都是凸多边形那么结果也是凸多边形。你可能会想“这又有什么意义呢?”。其实真正的意义所在是不再于“和”而在于“差 ”,A – B ={a–b|a∈A,b∈B}。在我们继续之前申明一下,尽管我们使用的是一个“差”操作,这同样是闵科夫斯基和的一部分,在以后的部分里我把它叫做“闵科夫斯基差” 只是为了方便,它的名字同样还是“闵科夫斯基和”。 如果两个形状重叠或者相交那么这两个形状的闵科夫斯基差会包含原点。
图示1:两个相交的凸多边形。 让我们看一眼示例,将图示1中的两个相交的凸多边形做闵科夫斯基差,就可以得到图示2中的一个多边形,可以看到得到的多边形包含原点。
图示2:闵科夫斯基差 进行闵科夫斯基差的时候如果我们将所有的点都进行一边操作显然是不可能的,因为一个多边形含有的点是无数个的。既然所有的多边形都是凸多边形而且能够得到他 们的顶点,我们只需要对这些顶点进行闵科夫斯基差的操作。对于GJK来说,最有优势的事情就是你不必真的计算出闵科夫斯基差就能得到最终的你想要的结果。 四、单纯形 我们并不想真的计算出来闵科夫斯基差。取而代之的是我们只需要知道闵科夫斯基差是不是包含原点。如果它包含远点那么我们就可以得出结论,这两个多边形相交, 反之亦然。 我们可以做的就是迭代地在闵科夫斯基差里面计算出来一个多边形去试图包含原点。如果我们建造的多边形包含远点(当然要在闵科夫斯基差内部)那么我们就可以说 闵科夫斯基包含原点,我们给我们要创建的这个多边形起名叫做“单纯形”。 五、支持函数 那么下一个问题就是我们如何创建这样一个单纯形。单纯形使用一个叫做支持函数的方法去创建。支持函数在给定两个形状之后应该返回一个在闵科夫斯基差内部的点 。我们已经知道了我们可以从形状a中得到一个点然后从形状b中得到一个点,将这两个点相减就可以得到一个在闵科夫斯基差边缘上的点。但是我们不想总是得到同一个 点。 如果我们让这个支持函数是以一个向量为基础的那么我们就可以保证在使用不同的向量是就可以得到不同的点。换句话说,如果我们让支持函数返回的是在一个方向上 的最近的点,我们就可以选择不同的方向得到不同的点。(译者注,这儿看不懂没关系其实很简单,一会看到后面有很详细的例子看看就会明白了,这里只需明白,这个 支持函数是输入一个向量返回一个点就行了)。 选择一个方向上的最近的两个点有着非常重要的意义,因为它能够创建一个包含最大面积的单纯形,这样就能够快速地增加让这个函数退出得到结果的机会了。而且, 我们可以还可以利这种方式返回的点都在闵科夫斯基边缘的这样一个事实,因此如果我们不能够添加一个沿着一个特定向量的朝向原点方向的一个点,我们就可以得出结 论,这个闵科夫斯基差不包含原点。这增加了算法快速得出结论的机会。一会会介绍更多信息。、 支持函数如下: public Point support(Shape shape1, Shape shape2, Vector d) { // d是一个方向向量,可以不正规化(模化作1) // 通过不同的方向的到两个多边形边上的点 Point p1 = shape1.getFarthestPointInDirection(d); Point p2 = shape2.getFarthestPointInDirection(d.negative()); // perform the Minkowski Difference Point p3 = p1.subtract(p2); // p3就是闵科夫斯基上面的一个点 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)。这两个都会产生一个在闵科夫斯基边缘上的点。 最后一次 p1=(4,11); p2=(10,2); p3=p1-p2=(-6,9); 我们可以得到这样一个单纯形。 (译者注,一定要认真的看这个例子 对照着图示2这样才能够看懂之前说的那些东西)
图示3:单纯形示例 七、相交检测 我们之前说过,如果两个多边形相交那么他们的闵科夫斯基差就会包含原点。在图示3中单纯形并没有包含原点,但是两个多边形明明相交。这里的问题在于,我们第 一次选择的方向d并没有成功的的生产出一个包含原点的单纯形。 如果我们使用d=(0,-1)作为第三次的d,那么 p1=(4,5); p2=(5,7); p3=p1-p2=(-1,-2); 这样就能够产生一个如图4所示的包含远点的单纯型,这样我们就可以判断出来两个多边形相交。
图示4:包含原点的示例单纯形 所以正如我们所见,d的选择能够影响结果。我们不能保证我们选择的三个点就能够包含原点,我们同样不能够保证闵科夫斯基差包含原点。我们用选择指向原点的点 的方式 替换原来的选择点的方式。如果我们改变我们选择第三个闵科夫斯基点的方式我们就能够包含原点了。 d=。。。(译者注:还是之前上面的) a=support(d) d=*** b=support(d) AB=b-a; AO=ORIGIN(0,0)-a; direction(AB X AO) X AB(译者注:向量的叉乘,这样计算的点 在直线AB和原点同向的一侧) c=support(d); 因为d是依靠与ab形成的线,我们可以选择相反的方向作为第二次的d,这样就能够保证b和a的距离尽可能的远。 d=。。。 a=support(d) d=*** b=support(d) AB=b-a; AO=ORIGIN(0,0)-a; direction(AB X AO) X AB c=support(d); 那么现在我们需要做的就是 为第一次闵科夫斯基差选择一个d。这里有很多的选择,一个随机的方向,这两个形状的中心连线创建的方向,等等。任何一种都可以,但 是有一些选择更优。 八、迭代 尽管我们改变了之前的做法来判断是否碰撞,但是我们依然没有通过那三个步骤得到一个包含原点的单纯形。我们必须创建单纯形,这样就能够保证我们得到的单型和 原点的距离越来越近。迭代过程中有两点需要注意:1.我们是否已经得到了一个包含原点的单纯形。2,我们是否有能够得到一个包含原点的单纯型。 下面是算法示意代码: (译者注:这个伪代码和下面的例子好好看) Vector d=//选择一个初始方向 Simplex.add(support(A,B,d));//得到第一个点 simplex就是单纯形 d.negate();//将d取反向 while(true){ //向单纯形中添加一个点 simplex。add(support(A,B,d)); //确保新添加的这个点是在和原点同向的AB直线一侧 //如果if判断成立说明 我们不能找到一个simplex让其包含原点 //这个推出条件一定要好好理解,通过上面的几个图示 //如果最近一次添加进来的点和 (d.x,d.y)这个向量反向 //就说明不能找到simplex包含原点 if(simplex.getlast().dot(d)<=0) return false; }else{ //如果现在的单纯形包含原点 //就说明 闵科夫斯基差包含原点 就返回 if(simplex.contains(ORIGIN)){ return true; }else{ //计算新的d 这里的计算方法就是我们刚才讨论的 d=getDirection(simplex); } } 下面就让我们用这个方法进行测试: 初始化: d = c2 - c1 = (9, 5) - (5.5, 8.5) = (3.5, -3.5) = (1, -1); //这里的c 表示图形的图心 p1 = support(A, B, d) = (9, 9) - (5, 7) = (4, 2); Simplex.add(p1); d.negate() = (-1, 1); 然后我们开始循环: 需要注意的是:(A X B) X C=B(C.A)-A(C.B) last = support(A, B, d) = (4, 11) - (10, 2) = (-6, 9); proj = (-6, 9).dot(-1, 1) = 6 + 9 = 15 // 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 AB = (-6, 9) - (4, 2) = (-10, 7); AO = (0, 0) - (-6, 9) = (6, -9); (AB x AO) x AB = AO(149) - AB(-123) = (894, -1341) - (1230, -861) = (-336, -480) = (-0.573, -0.819)
图示5:第一次循环 第二次循环: 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); (AB x AO) x AB = AO(160) - AB(-104) = (1280, 320) - (1248, 416) = (32, -96) = (0.316, -0.948)
图示6:第二次循环
图示6B:第二次循环中的求d的方向 第二次循环之后我们仍然没有包含原点,但是并没有得出结论 我们不能够找到包含原点的simplex 第三次循环: teration 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)!
图示7:第三次循环 九、检测单纯形 我们忽略了这个这个算法的两个操作。一个是,我们如何我们如何知道当前的单纯形包含原点,另一个是,我们如何选择下一个方向。在伪代码中,我为了描述方便将 这些操作单列出来,但是在实际操作中,他们应该在一块进行,因为他们需要很多相同的信息。 我们可以通过对单纯形进行一系列的简单的点乘操作来判断原点和单纯形的位置关系。第一个要解决的问题是线段问题。让我们看一下上面的第一次循环。在第九行添 加了第二个点之后,现在单纯形是一个线段。我们可以检测泰森多边形区域,来判断单纯形是否有可能包含原点。
图示8:泰森多边形 线段被定义为从A到B,其中A是最后添加到单纯性中取得。我们知道了A和B都是在闵科夫斯基边缘上的,因此原点不能在R1(上图中的区域)或者R4.我们可以得出这样 的假设因为第11行的检测返回false指示出我们可以包含原点。原点只可能落在R2或者R3中,而且一个线段不可能包含原点,所以下一步需要做的就是计算出一个新的方 向。这可以按照先前的介绍的,通过AB的叉乘计算出朝向原点的向量。 // 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行的检测就会失败。这样会在如下两中情况下发生:1)原点 在闵科夫斯基内部(译者注,不懂什么意思 这个!) 2)在闵科夫斯基边缘。第二中情况表示接触但没有渗透(就是没有相交),所以具体如何处理这种情况需要你自己 决定。在其他情况下,你可以使用AB的左法线或者右法线作为新的d。 现在让我们检测第二次循环,第二次循环将我们的单纯形转化为了三角形,如图9所示。
图示9:泰森多边形区域 白的区域不必测试因为他们通过了第11行的测试。R2不可能包含原点,因为最后的我们选择的方向是相反的方向。所以需要测试的就是R3,R4和R5。我们可以通过(AC X AB) X AB 来计算出AB的法线。然后我们通过计算AB的法线点乘AO来判断原点是否在R4区域里。 AB = (-6, 9) - (-8, -2) = (2, 11) AC = (4, 2) - (-8, -2) = (12, 4) // (AC x AB) x AB = AB(AB.dot(AC)) - 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(AC.dot(AB)) - 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中,现在我们需要选择一个新的d作为我们得到下一个闵科夫斯基差的点的方向。这很简单,因为我们知道了AC的泰森多边形区域包含原点 (AC X AO) X AC 现在最终的算法为: 01 Vector d = // choose a search direction get the first Minkowski Difference point mplex.add(support(A, B, d)); negate d for the next point d.negate(); start looping ile (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; } 到这里 GJK 判断两个凸多边形是否相交就已经翻译完了,原文的下方有很多非常有意义的问答,都是一些这方面的爱好者对作者的提问,作者的耐心回答,很有意义的 是说,如果有时间可以自己看,翻译那个东西太费事了。。。