GJK算法用于判断两个凸集是否相交,其中GJK是三个提出者的姓名首字母。为了便于理解(偷懒),下面的内容都只在二维平面内讨论。

1. 回顾凸集

  可能有很多小伙伴忘了什么是凸集。凸集有很多等价的定义,最常用的一种是在集合中任取两点,连接这两点的线段一定在此集合内。常见的形状,例如三角形、矩形、圆、椭圆,都是凸集。

  对于两个凸集A, B, 从集合A, B中分别任取一点a, b, 所有可能的a+b构成的集合称为AB的闵可夫斯基和,记作A+B. A+B也是一个凸集,这一点用定义很容易证明。

A+B大概长什么样子呢?假设在原点有一支铅笔,现在用一根铁棒将铅笔与集合B连接,使得它们的相对位置不发生改变。之后用铅笔将集合A涂一遍,在此过程中B扫过的区域就是集合A+B了。

  类似于加法的定义,我们可以定义AB的闵可夫斯基差A-B: 从A, B中分别任取一点a, b, 所有可能的a-b构成的集合便是A-B. A-B也是个凸集,因为集合-BB中所有点的坐标取反,相当于相对于原点做了个对称变换,自身的形态并没有改变,只是在空间所处的位置发生了变化,从而-B也是个凸集,因此A-B = A + (-B), 两个凸集的和当然是凸集。

  好了,到这里可以简单介绍下GJK算法的思想了:对于两个凸集A, B,想一想,如果A, B相交,那么必然有某个点同时包含在AB中,从而凸集A-B必然包含原点;反之,如果A, B不相交,那么A-B必然不包含原点。因此,GJK算法的本质就是判断原点是否包含在A-B中。当然,GJK算法不会直接计算出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 最远的点,这时我们任取一个就好。

  既然说到这里,就顺便说说support操作是用来做什么的,这与凸集的另一个性质相关。对于这里讨论的二维平面来说,这个性质说的是,对于凸集边界上的任意一点s,都存在过s 的一条直线,使得整个凸集都在这条直线的同一侧。这个性质在这里就不证了,有兴趣的朋友可以搜索“支撑超平面”。

  给定方向d, support方法找到凸集沿着此方向最远的一个点s(不难看出,s 在凸集的边界上),此时过点s 且与方向d 垂直的直线就可以将整个凸集划分到直线的一侧(可以看看上面的示意图);如果此时原点在直线的另一侧,就说明原点必然不包含在凸集中。

3. GJK算法

  假设输入的两个凸集为AB, GJK算法的描述如下:

    1. 随机选择一个初始方向d.
    2. 计算A沿着方向d 最远的点pa 以及B沿着方向-d (注意,这里是-d )最远的点pb.p = pa - pb .
    3. S={ p }, 更新方向d =-p.
    4. 计算A沿着方向d 最远的点pa 以及B沿着方向-d 最远的点pb ,令p = pa - pb.
    5. 如果p d 的点乘小于0, 则A, B不相交, 返回false.
    6. 否则, 将p 添加到集合S中.
    7. 如果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中,从而可以判断AB不相交。

例2:A与B相交

  这种情况下,原点OA-B的内部,前面的步骤同上。不过,此时向量OP2 d 的点乘大于0, 如下图所示。

  下一步,将P2 加入点集S. 此时S中只有两个点,直线P1P2 显然不包含原点O. 下一步更新dP1P2 指向O的方向,如下图所示:

  更新d 之后,就开始了下一次的循环,计算A.support(d) - B.supoort(-d), 这个点位于下图中标蓝的点,记作P3. 如下图所示:

  此时向量OPd 的点乘仍然大于0. 下一步,P1P2P3 构成的三角形已经包含原点O. 从而说明A-B包含原点O, 即A, B相交。

例3:A与B相交

  如果在例2中,P1P2P3 构成的三角形并不包含原点O. 这时就需要找到此三角形中与原点O 最近的那条边,更新点集S为这条边的两个顶点,接下来的步骤和例2中的相同:更新方向d 为这条边指向原点的方向,然后继续循环下去。(用上面的图不太好画这种情况的例子,我就不画了,改用口头叙述)

4. 小结

  可以看到,GJK算法所做的,就是用一个三角形来近似两个凸集A,B的差A-B, 然后更新这个三角形使之不断接近原点。 GJK算法的泛化性也很强,如果对于不同类型的物体,每两个类型都要写一个碰撞检测算法,比如说圆与矩形、矩形与正六边形,那得写多少?!反而,GJK算法只要求物体是凸的,而且只要提供support方法就好。而且GJK算法的收敛速度看起来也很快,不过现在我还没有想清楚为什么收敛这么快,知道原因的朋友可以在评论区交流下。

碎碎念:算法看起来不难,用Python实现了下,我的感受是一步一个坑。很多细节都需要考虑,例如如何判断原点是否在三角形内部、如何找到三角形距离原点最近的边等等。事实上,最后出现bug的地方在二维向量类的实现,有个重载运算符的地方写错了调试了好久,下次这种就用现成的类吧。

GJK算法:两个凸集的碰撞测试的更多相关文章

  1. Flyod 算法(两两之间的最短路径)

    Flyod 算法(两两之间的最短路径)动态规划方法,通过相邻矩阵, 然后把最后的结果存在这么一个矩阵里面,(i,j), #include <iostream> #include <v ...

  2. JAVA算法两道

    算法(JAVA)----两道小小课后题   LZ最近翻了翻JAVA版的数据结构与算法,无聊之下将书中的课后题一一给做了一遍,在此给出书中课后题的答案(非标准答案,是LZ的答案,猿友们可以贡献出自己更快 ...

  3. Fisher算法+两类问题

    文章目录 一.Fisher算法 二.蠓的分类问题: 三.代码实现: 一.Fisher算法 二.蠓的分类问题: 两种蠓Af和Apf已由生物学家根据它们的触角和翼长加以区分(Af是能传播花粉的益虫,Apf ...

  4. [PHP] 算法-两个n位的二进制整数相加问题PHP实现

    两个n位二进制数分别存储在两个n元数组A和B中,这两个整数的和存在一个n+1元的数组C中答:此问题主要是考察相加进位的问题,元素1+1 =0 并且往前进一位ADD-BINARY(A,B) C=new ...

  5. 编程算法 - 两个链表的第一个公共结点 代码(C)

    两个链表的第一个公共结点 代码(C) 本文地址: http://blog.csdn.net/caroline_wendy 题目: 输入两个链表, 找出它们的第一个公共结点. 计算链表的长度, 然后移动 ...

  6. hihocoder#1050 : 树中的最长路(树中最长路算法 两次BFS找根节点求最长+BFS标记路径长度+bfs不容易超时,用dfs做TLE了)

    #1050 : 树中的最长路 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 上回说到,小Ho得到了一棵二叉树玩具,这个玩具是由小球和木棍连接起来的,而在拆拼它的过程中, ...

  7. 编程算法 - 两个升序列的同样元素 代码(C)

    两个升序列的同样元素 代码(C) 本文地址: http://blog.csdn.net/caroline_wendy 两个升序列的同样元素, 须要使用两个指针, 依次遍历, 假设相等输出, 假设小于或 ...

  8. Map-produce算法两个开源实现

    https://github.com/michaelfairley/mincemeatpy https://github.com/denghongcai/mincemeat-node

  9. python实现高速排序算法(两种不同实现方式)

    # -*- coding: utf-8 -*- """ Created on Fri May 16 17:24:05 2014 @author: lifeix " ...

  10. 算法两数之和 python版

    方法一.暴力解法 -- 5s 复杂度分析:时间复杂度:O(n^2)空间复杂度:O(1) length = len(nums)for i in range(length):    for j in ra ...

随机推荐

  1. 使用 Preload&Prefetch 优化前端页面的资源加载

    对于前端页面来说,静态资源的加载对页面性能起着至关重要的作用.本文将介绍浏览器提供的两个资源指令-preload/prefetch,它们能够辅助浏览器优化资源加载的顺序和时机,提升页面性能. 一.从一 ...

  2. 浅谈 Java 中的 AutoCloseable 接口

    本文对 try-with-resources 语法进行了较为深入的剖析,验证了其为一种语法糖,同时给出了其实际的实现方式的反编译结果,相信你在看完本文后,关于 AutoCloseable 的使用你会有 ...

  3. PVE API创建虚拟机

    度娘,谷歌都搜了一圈没有找到通过PVE API创建虚拟机的方式, 于是查官网自己试了试,部分代码抄的Sam Liu大佬的作业,感谢大佬. python代码如下: import requests # s ...

  4. 5、SpringBoot连接数据库引入mybatis

    系列导航 springBoot项目打jar包 1.springboot工程新建(单模块) 2.springboot创建多模块工程 3.springboot连接数据库 4.SpringBoot连接数据库 ...

  5. 使用WTM框架创建博客系统后台并在云服务器发布

    阅读导航 关于lqclass.com 博客后台前后端部署 2.1 已部署访问链接 2.2 nginx 部署 2.2.1 后台后端发布 2.2.2 后台前端发布 2.2.3 云服务器部署 下次分享 1. ...

  6. 使用React简短代码动态生成栅格布局

    使用React简短代码动态生成栅格布局 作为 TerminalMACS 的一个子进程模块 - React Web管理端,使用Ant Design Pro作为框架. 本文应用到的知识 1.样式文件les ...

  7. 基于AHB_BUS Clac slave详解

    基于AHB-APB BUS slave详解 1.目录 高内聚:让模块的功能更集中,更单一. AMBA总线例子,需要有一个模块和AMBA进行交互,就可以单独将与AHB总线进行交互的部分作为一个模块.经常 ...

  8. Vue - 父子级的相互调用

    父级调用子级 父级: <script> this.$refs.child.load(); 或 this.$refs.one.load(); </script> 子级: < ...

  9. web - 解决 formdata 打印空对象

    获取单个值可以使用formData对象.get();而直接打印是看不到的.因为外界访问不到,你使用append方法后,对应的键值对就已经添加到表单里面了,你在控制台看到的是FormData原型,存储的 ...

  10. MongoDB 部署分片集群

    部署配置服务器:configsvr 先生成.conf文件 mkdir -p /data/mongodb/configsvr vim /data/mongodb/configsvr/configsvr. ...