前言
参考链接:
看似简单的复杂问题,奇怪而优雅的解决方式(GJK算法) | Reducible
GJK 算法
(ps:自用笔记)
先抛出我们的问题: 计算机如何判断两个多边形相交?
从形状开始
任何多边形都可以分成凸形(convex shape)和凹形(concave shape).
凸形的性质是,在形状上取任意两点,连线总是在凸形的形状里.而对于凹形,则总是存在反例(如下图).
处理凸形相交要比凹形简单(why?利用闵科夫斯特差的性质,见下文),因此一般会把凹形切割成若干凸形,这样判断两个多边形是否相交变成了凸形求交集问题。
闵科夫斯特差
把多边形看成这个形状内每一个点的集合,则如果有两个多边形相交,那我们可以找到两个点(向量),他们的差等于原点。
(ps: 如果两多边形相交,至少存在一个共同点,这个点在a集合可表示为(x1,y1),b集合可表示为(x2,y2))
闵科夫斯特差是两个多边形的任意两点相减,得出来的点的集合,,用公式表达: A + B = { a + b | a∈A ,b∈B}
。
(ps: 可以把每个点看成原点出发的向量,向量叠加)
闵科夫斯特差的两个性质:
- 取两个凸形的闵科夫斯特差,得到的形状也是凸形。
- 如果两个形状相交,则闵科夫斯特差必定包含原点(两个凸形必定包含一个共同点)。
利用上面的性质判断多边形相交。因此判断多边形是否相交最终变成了判断闵科夫斯特差是否存在原点。
单形
那如何检查原点在集合中呢(毕竟不可能检查集合中每个点)?
挑选闵科夫斯特差上的点,尝试建立一个包含原点的三角形。如果找不到这样的三角形,说明两多边形不相交。构建的三角形即为单型(simplex)。
单型简单说就是用最简单的形状包围某维的某点,对于二维,三角形是最符合这个标准的形状。
然而并非集合中任意点构成的单形都包含原点,这引申出下一个问题: 如何更快地找到一个包围原点的单形?
支撑函数
根据凸形性质,任意一点都存在一个方向使他是此方向上最远的点,遍历所有可能方向就可以得到凸形。对于凹形则总是存在反例。
凸形的支撑函数为将方向向量映射到多边形上最远的点的函数,而对应的点叫做支撑点。两个凸形的支撑函数相减,得到闵科夫斯特差的支撑函数。
计算支撑点:
给定一个方向向量,求该方向上的支撑点。以上图的六边形为例。首先将顶点转换成从原点出发的向量,然后求与给定方向向量的点积,点积最大的就是支撑点。
为何计算点积?点积公式为 a·b = |a||b|cosθ
同时考虑了与给定向量的夹角和向量大小。
不同的形状的支撑点函数不同(多态),例如对于圆,支撑点为给定向量归一化后乘以圆半径。
利用支撑函数获得闵科夫斯特差的支撑点以构建单形,判断原点是否在单形中,如不存在,迭代寻找其他的支撑点构成新单行。具体操作见下。
GJK算法实现
直接上伪代码:
public Point support(Shape shape1,Shape shape2,Vector d)
{
Point p1 = shape1.getFarthestPointInDirection(d);
Point p2 = shape2.getFarthestPointInDirection(d);
Point p3 = p1 - p2;
return p3;
}
//选择一个搜索方向获得单形第一个支撑点
Vector d = Simplex.add(support(A,B,d));
d = d.negate();
while(true)
{
//单形添加点
simplex.add(support(A,B,d));
//保证最后添加的点"经过"原点
//Question1:
if(simplex.getLast().dot(d) <= 0)
{
//支撑点在闵科夫斯特差的边缘,并且当前点不经过原点,说明不包含原点
return false;
}
else
{
//当前单形是否包含原点
if(ContainOrigin(simplex,d))
{
return true;
}
}
}
public boolean ContainOrigin(simplex s,Vector2 d)
{
//单形最后添加的点
a = simplex.GetLast();
//-A
ao = a.negate();
if(simplex.points.size == 3)
{
//2D平面,单形为三角形
b = simplex.getB();
c = simplex.getC();
//边
ab = b - a;
ac = c - a;
//法向量
//Question2:
abPerp = tripleProduct(ac,ab,ab); //矢量三重积
acPerp = tripleProduct(ab,ac,ac);
//Question3:
if(abPerp.dot(ao) > 0)
{
//移除c点
simplex.remove(c);
//搜索方向改为ab法向量
d.set(abPerp);
}
else if(acPerp.dot(ao) > 0)
{
//移除b点
simplex.remove(b);
d.set(acPerp);
}
//R5区域已经包含原点
else
{
return true;
}
}
else
{
//不构成单形
b = simplex.getB();
ab = b - a;
//法向量
abPerp = tripleProduct(ab,ao,ab);
d.Set(abPerp);
}
return false;
}
QA
Q1: 如何判断退出迭代?
如图,E为单形当前支撑点(simplex.last()),d为当前搜索方向。
d * EO < 0,表明当前的支撑点无法进一步向原点靠近,并且当前的单形也没有包含原点,说明不相交,提前退出迭代。
Q3: 如何选择迭代方向?
假设已经构建出一个单形,如下图ABC。其中A为当前迭代得到的最后一个支撑点。
图中L1、L2分别为AB、AC的法向量。
取法向量作为我们的搜索方向。此时我们迭代方向有两种可能: L1或者L2。如何选择呢?
点积 a * b = |a||b|cosx
,通过夹角余弦值可判断法向量是靠近原点还是远离原点。计算AO和法向量的点积。其中L2和AO的点积大于0,说明L2为接近原点的方向,选择L2作为方向向量,并移除B点(L1方向远离原点)。
当L1、L2方向的点积都为0,说明当前单形已经包含原点,即可证明相交。
(ps: 为什么不考虑BC法向量取负作为搜索方向?这个问题在看别的帖子的时候感觉回答的不是很清楚,因此我尝试自己解释一下。如上图中,取BC的法向量作为搜索向量d,获得支撑点A。此时如果取-d,则获取的新的支撑点远离原点,构成的单形必定不包含原点)
Q2: 解释矢量三重积:
利用矢量三重积计算法向量:
矢量三重积公式为(A x B)x C = B(C.dot(A))– A(C.dot(B))