GJK算法:两个凸集的碰撞测试
GJK算法用于判断两个凸集是否相交,以下在二维平面内讨论此算法。
1. 回顾凸集
凸集有很多定义,最常见的是:在集合中任取两点,连接这两点的线段在集合内,常见的三角形、椭圆等都是凸集。
对于凸集A, B, 从两集合中各取一点a, b, 所有a+b构成的集合称为A与B的闵可夫斯基和,记作A+B. 显然,A+B也是凸集。
A+B的大致模样
假设在原点有一支铅笔,用一根铁棒将铅笔与集合B连接,使得它们的相对位置不发生改变。之后用铅笔将集合A涂一遍,在此过程中B扫过的区域就是集合A+B.
还可以定义A与B的闵可夫斯基差A-B: 从两集合中各取一点a, b, 所有的a-b构成A-B. A-B也是个凸集,因为集合-B是B中所有点的坐标取反,相当于作关于原点的对称变换,自身形态并没有改变,因此-B也是凸集,A-B = A + (-B)亦是凸集。
至此,可以简述GJK算法的思想:对于凸集A, B,如果A, B相交,凸集A-B必然包含原点,GJK算法就是通过判断原点是否在A-B中。当然,我们不会直接计算出A-B, 因为基本上无法操作。对于二维平面,GJK算法会在A-B内找一个三角形,在迭代过程中不断更新三角形,使得三角形不断靠近原点。为了高效实现此目的,需要为凸集定义support
方法。
此处有一个动画演示。
2. 凸集的support方法
support
方法很简单:它接受一个二维向量direction
作为输入,返回凸集在这个方向上最远的点:
class ConvexSet:
def support(direction: Vector2d) -> Vector2d:
...
例如,在下图中,圆在方向d1上最远的点是图中标红的点,矩形在方向d2上最远的点是图中标绿的点。如何找一个凸集沿着方向d 最远的点呢?作一条与d 垂直的直线l,然后沿着d 移动,直到一个极限的状态:如果沿着d 再移动一点点,凸集将会与直线l 没有任何交点;此时凸集与直线l 的交点就是沿着方向d 最远的点。
有的凸集沿某方向的最远点可能不止一个,例如以下矩形,整条蓝色边上的点都是沿方向d 最远的点,这时任取一个即可:
对于凸集边界上的任意一点s,存在过s 的直线(支撑超平面),使得凸集在直线的同一侧。给定方向d, support
方法找到凸集沿此方向最远的点s(不难看出,s 在凸集的边界上),此时过点s 且与方向d 垂直的直线可以将整个凸集划分到直线的一侧(如上图所示)。此时,如果原点在直线的另一侧,则说明原点不在凸集中。
3. GJK算法
假设两个凸集为A和B, GJK算法的步骤如下:
-
- 随机选择一个初始方向d.
- 计算A沿着方向d 最远的点pa 以及B沿着方向-d (注意,这里是-d )最远的点pb.令p = pa - pb .
- 令S={ p }, 更新方向d =-p.
- 计算A沿着方向d 最远的点pa 以及B沿着方向-d 最远的点pb ,令p = pa - pb.
- 如果p 与d 的点乘小于0, 则A, B不相交, 返回false.
- 否则, 将p 添加到集合S中.
- 如果S包含原点, 则A, B相交, 返回true.否则,从S中选择距离原点最近的一条边, 更新方向d =这条边指向原点的方向,并跳转到第4步.
二维平面的一个点既可以看作是一个点,又可以看作是原点指向这一点的向量。因此,上述中出现的 -p 实际上指的是从点 p 指向原点的向量。
4. GJK算法的解读
前面说过,给定凸集A, B, GJK算法就是判断凸集A-B是否包含原点,只是它没有显式计算出A-B. 下面就来说明GJK算法的每一步都做了些什么。
我们没有必要知道A-B长什么样子,只要知道它是个凸集就够了。为了说明,可以想象下A-B的样子,比如说,A-B可能长成下面这样:
注意到GJK算法的一个关键操作:对于方向d, 计算p = A.support(d) - B.supoort(-d)
. 此时p 必然是A-B沿着方向d 最远的一个点。为什么?想一想,A.support(d)
是A沿着方向d 最远的一个点,而B.support(-d)
是B沿着方向-d 最远的一个点,考虑集合A-B的定义,还能在方向d 上找到一个更远的点吗?
例1:A与B不相交
GJK算法的第一步,随机选择一个方向,例如(1, 0), 然后计算出A.support((1, 0)) - B.supoort((-1, 0))
(即凸集A-B中沿着(1, 0)最远的一个点,下图中标红的点,记作P1)。下一步,将标红的点加入点集S, 并更新方向d 为标红的点指向原点(图中标黑的点)。
下一步,计算A.support(d) - B.supoort(-d)
, 这个点位于下图中标绿的点,我们将这个点记作P2. 可以看到,此时向量OP2 与d 的夹角大于90°,因此它们的点乘小于0. 从而说明原点O不可能在A-B中,从而可以判断A与B不相交。
例2:A与B相交
这种情况下,原点O在A-B的内部,前面的步骤同上。不过,此时向量OP2 与d 的点乘大于0, 如下图所示。
下一步,将P2 加入点集S. 此时S中只有两个点,直线P1P2 显然不包含原点O. 下一步更新d为P1P2 指向O的方向,如下图所示:
更新d 后,就开始了下一次的循环,计算A.support(d) - B.supoort(-d)
, 这个点位于下图中标蓝的点,记作P3. 如下图所示:
此时向量OP3 与d 的点乘仍然大于0. 下一步,P1P2P3 构成的三角形已经包含原点O. 从而说明A-B包含原点O, 即A, B相交。
例3:A与B相交
如果在例2中,P1P2P3 构成的三角形并不包含原点O. 这时就需要找到此三角形中与原点O 最近的那条边,更新点集S为这条边的两个顶点,接下来的步骤和例2中的相同:更新方向d 为这条边指向原点的方向,然后继续循环下去。
5. 小结
可以看到,GJK算法所做的,就是用一个三角形来近似两个凸集A,B的差A-B, 然后更新这个三角形使之不断接近原点。 GJK算法的泛化性也很强,如果对于不同类型的物体,每两个类型都要写一个碰撞检测算法,比如说圆与矩形、矩形与正六边形,那得写多少?!反而,GJK算法只要求物体是凸的,并提供support
方法即可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步