您的位置:首页 > 其它

判断两个形状是否相交(二)-GJK

2017-01-29 14:10 162 查看
判断两个形状是否相交二-GJK
原文地址

一 介绍

二凸多边形性质

三Minkowski和

四单纯形

五支持函数

六创建单纯形

七相交检测

八迭代

然后我们开始循环迭代

第二次迭代

第三次迭代

九检测单纯形

结束语

判断两个形状是否相交(二)-GJK

原文地址

一、 介绍:

GJK和SAT一样,只适用于凸多边形。GJK更为强大的地方在于,它可以通过“支持函数”(稍后讨论)支持所有的形状。因此,和SAT不同,你不用对曲边型使用特殊的代码或者方法进行特殊处理,就可以使用GJK。

GJK是一个迭代式的方法但是收敛的非常快,如果使用最新的渗透(分离)向量,它几乎是在一个很短的时间内得出结果。它是SAT在3D环境下的一个很好的替代,就因为SAT要测试的轴de数量非常多。

GJK的最初始的目的是为了计算两个凸边形的距离.GJK同样可以被用作在渗透很短的情况下的碰撞信息,还可以在其他的计算很大的渗透的方法中使用.

二、凸多边形性质:

就像之前说过的一样,GJK是一个只能被用在凸多边形上的方法。我看上一个关于SAT的博文了解更多关于凸多边形的信息。

三、Minkowski和:

GJK算法依靠一个很重要的理论叫做,闵科夫斯基和。闵科夫斯基和在原理上很容易理解:如果你有俩个形状,闵科夫斯基和就是形状a的所有的点加上形状b的所有的点,

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

如果两个形状都是凸多边形那么结果也是凸多边形。你可能会想“这又有什么意义呢?”。其实真正的意义所在是不再于“和”而在于“差”,(译者注,这里有些绕,姑且关注“闵科夫斯基差”即可)。

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

(译者注,表示形状A中的所有点和形状B中的所有点相减,如果A中有n个点,B中有n个点,那么最终将会产生n2个点。)

在我们继续之前申明一下,尽管我们使用的是一个“差”操作,这同样是闵科夫斯基和的一部分,在以后的部分里我把它叫做“闵科夫斯基差”只是为了方便,它的名字同样还是“闵科夫斯基和”。

如果两个形状重叠或者相交那么这两个形状的闵科夫斯基差产生的点的外围形状会包含原点

如果有下图这样两个相交的图形



做闵科夫斯基差,可以得到12个点。这12个点的包含多边形会包含原点。如下图示:



如上面讨论过一样,做这样一个运算将要进行n2次减法。如果两个多边形的顶点数目比较多这样会使巨大的运算量。不过幸运的是在使用GJK的时候可以不对所有的点都进行闵科夫斯基差操作,就可以得出想要的结果。

四、单纯形:

我们并不想真的计算出来闵科夫斯基差。取而代之的是我们只需要知道闵科夫斯基差是不是包含原点。如果它包含远点那么我们就可以得出结论,这两个多边形相交,反之亦然。

我们可以做的就是迭代地在闵科夫斯基差里面计算出来一个多边形去试图包含原点。如果我们建造的多边形(当然要在闵科夫斯基差内部)包含原点那么我们就可以说闵科夫斯基差包含原点,我们给我们要创建的这个多边形起名叫做“单纯形”(英文原文Simplex)。

单纯性就是一个三角形,只不过它的订单都是通过闵科夫斯基差计算出来的。

五、支持函数:

那么下一个问题就是我们如何创建这样一个单纯形。单纯形使用一个叫做支持函数的方法去创建。支持函数在给定两个形状之后应该返回一个在闵科夫斯基差内部的点。我们已经知道了我们可以从形状a中得到一个点然后从形状b中得到一个点,将这两个点相减就可以得到一个在闵科夫斯基差边缘上的点。但是我们不想总是得到同一个点。

如果我们让这个支持函数是以一个向量为基础的那么我们就可以保证在使用不同的向量是就可以得到不同的点。换句话说,如果我们让支持函数返回的是在一个方向上的最近的点,我们就可以选择不同的方向得到不同的点。(译者注,这儿看不懂没关系其实很简单,一会看到后面有很详细的例子看看就会明白了,这里只需明白,这个支持函数是输入一个向量返回一个点就行了)。

选择一个方向上的最近的两个点有着非常重要的意义,因为它能够创建一个包含最大面积的单纯形,这样就能够快速地增加让这个函数退出得到结果的机会了。而且,我们可以还可以利这种方式返回的点都在闵科夫斯基边缘的这样一个事实,因此如果我们不能够添加一个沿着一个特定向量的朝向原点方向的一个点,我们就可以得出结论,这个闵科夫斯基差不包含原点。这增加了算法快速得出结论的机会。下面会介绍更多信息。

支持函数如下

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);


我们可以得到这样一个单纯形。

这三次运行的结果如下图



(译者注,一定要认真的看这个例子对照着图示2这样才能够看懂之前说的那些东西)

七、相交检测:

我们之前说过,如果两个多边形相交那么他们的闵科夫斯基差就会包含原点。在图示3中单纯形并没有包含原点,但是两个多边形明明相交。这里的问题在于,我们第一次选择的方向d并没有成功的的生产出一个包含原点的单纯形。

如果我们使用d=(0,-1)作为第三次的d,那么

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


这样就能够产生一个下图所示的包含远点的单纯型,这样我们就可以判断出来两个多边形相交。



所以正如我们所见,d的选择能够影响结果。我们不能保证我们选择的三个点就能够包含原点,我们同样不能够保证闵科夫斯基差包含原点。我们用选择指向原点的点的方式替换原来的选择点的方式。如果我们改变我们选择第三个闵科夫斯基点的方式产生的简单型就能够包含原点了。

d = ...//这里表示还是和上面的前两步一样
a = support(..., d)
d = ...
b = support(..., d)
AB = b - a
AO = ORIGIN - a
d = (AB x AO) x AB//向量的叉乘,这样计算的点
//在直线AB和原点同向的一侧
c = support(..., d)


因为d是依靠与ab形成的线,我们可以选择相反的方向作为第二次的d,这样就能够保证b和a的距离尽可能的远。

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


那么现在我们需要做的就是为第一次闵科夫斯基差选择一个d。这里有很多的选择,一个随机的方向,这两个形状的中心连线创建的方向,等等。任何一种都可以,但是有一些选择更优。

八、迭代:

尽管我们改变了之前的做法来判断是否碰撞,但是我们依然没有通过那三个步骤得到一个包含原点的单纯形。我们必须创建单纯形,这样就能够保证我们得到的单型和原点的距离越来越近。迭代过程中有两点需要注意:

是否已经得到了一个包含原点的单纯形。

是否有能够得到一个包含原点的单纯型。

下面是算法示意代码:

(译者注:这个伪代码和下面的例子好好看)

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);
}
}
}


下面就让我们用这个方法进行测试:

初始化

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.dot(A)) – A(C.dot(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)


下面这张图展示了第一次迭代的结果



第二次迭代

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,9)因为无论何时我们只需要三个点即可,而且在每次迭代开始的时候我们将插入一个新的点。

第二次循环产生的简单型如下图示



第二次迭代产生的方向如下图示



第三次迭代

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)!(下图)


第三次迭代将产生期望的简单型,如下图



九、检测单纯形:

我们忽略了这个算法的两个操作。一个是,我们如何我们如何知道当前的单纯形包含原点,另一个是,我们如何选择下一个方向。在伪代码中,我为了描述方便将这些操作单列出来,但是在实际操作中,他们应该在一块进行,因为他们需要很多相同的信息。

我们可以通过对单纯形进行一系列的简单的点乘操作来判断原点和单纯形的位置关系。第一个要解决的问题是线段问题。让我们看一下上面的第一次循环。在第九行添加了第二个点之后,现在单纯形是一个线段。我们可以检测泰森多边形区域,来判断单纯形是否有可能包含原点。

泰森多边形示例如下图



线段被定义为从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。

现在让我们检测第二次循环,第二次循环将我们的单纯形转化为了三角形,下图所示。



白的区域不必测试因为他们通过了第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

现在最终的把所有的情况都考虑在内的算法为:

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;
}


结束语

到这里 GJK 判断两个凸多边形是否相交就已经翻译完了,原文的下方有很多非常有意义的问答,都是一些这方面的爱好者对作者的提问,作者的耐心回答,很有意义的。如果有时间可以自己看,翻译那个东西太费事了。。。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息