GJK算法:两个凸集的碰撞测试
GJK算法用于判断两个凸集是否相交,其中GJK是三个提出者的姓名首字母。为了便于理解(偷懒),下面的内容都只在二维平面内讨论。
1. 回顾凸集
可能有很多小伙伴忘了什么是凸集。凸集有很多等价的定义,最常用的一种是在集合中任取两点,连接这两点的线段一定在此集合内。常见的形状,例如三角形、矩形、圆、椭圆,都是凸集。
对于两个凸集A, B, 从集合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. A-B也是个凸集,因为集合-B是B中所有点的坐标取反,相当于相对于原点做了个对称变换,自身的形态并没有改变,只是在空间所处的位置发生了变化,从而-B也是个凸集,因此A-B = A + (-B), 两个凸集的和当然是凸集。
好了,到这里可以简单介绍下GJK算法的思想了:对于两个凸集A, B,想一想,如果A, B相交,那么必然有某个点同时包含在A与B中,从而凸集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算法
假设输入的两个凸集为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 为这条边指向原点的方向,然后继续循环下去。(用上面的图不太好画这种情况的例子,我就不画了,改用口头叙述)
4. 小结
可以看到,GJK算法所做的,就是用一个三角形来近似两个凸集A,B的差A-B, 然后更新这个三角形使之不断接近原点。 GJK算法的泛化性也很强,如果对于不同类型的物体,每两个类型都要写一个碰撞检测算法,比如说圆与矩形、矩形与正六边形,那得写多少?!反而,GJK算法只要求物体是凸的,而且只要提供support方法就好。而且GJK算法的收敛速度看起来也很快,不过现在我还没有想清楚为什么收敛这么快,知道原因的朋友可以在评论区交流下。
碎碎念:算法看起来不难,用Python实现了下,我的感受是一步一个坑。很多细节都需要考虑,例如如何判断原点是否在三角形内部、如何找到三角形距离原点最近的边等等。事实上,最后出现bug的地方在二维向量类的实现,有个重载运算符的地方写错了调试了好久,下次这种就用现成的类吧。
GJK算法:两个凸集的碰撞测试的更多相关文章
- Flyod 算法(两两之间的最短路径)
Flyod 算法(两两之间的最短路径)动态规划方法,通过相邻矩阵, 然后把最后的结果存在这么一个矩阵里面,(i,j), #include <iostream> #include <v ...
- JAVA算法两道
算法(JAVA)----两道小小课后题 LZ最近翻了翻JAVA版的数据结构与算法,无聊之下将书中的课后题一一给做了一遍,在此给出书中课后题的答案(非标准答案,是LZ的答案,猿友们可以贡献出自己更快 ...
- Fisher算法+两类问题
文章目录 一.Fisher算法 二.蠓的分类问题: 三.代码实现: 一.Fisher算法 二.蠓的分类问题: 两种蠓Af和Apf已由生物学家根据它们的触角和翼长加以区分(Af是能传播花粉的益虫,Apf ...
- [PHP] 算法-两个n位的二进制整数相加问题PHP实现
两个n位二进制数分别存储在两个n元数组A和B中,这两个整数的和存在一个n+1元的数组C中答:此问题主要是考察相加进位的问题,元素1+1 =0 并且往前进一位ADD-BINARY(A,B) C=new ...
- 编程算法 - 两个链表的第一个公共结点 代码(C)
两个链表的第一个公共结点 代码(C) 本文地址: http://blog.csdn.net/caroline_wendy 题目: 输入两个链表, 找出它们的第一个公共结点. 计算链表的长度, 然后移动 ...
- hihocoder#1050 : 树中的最长路(树中最长路算法 两次BFS找根节点求最长+BFS标记路径长度+bfs不容易超时,用dfs做TLE了)
#1050 : 树中的最长路 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 上回说到,小Ho得到了一棵二叉树玩具,这个玩具是由小球和木棍连接起来的,而在拆拼它的过程中, ...
- 编程算法 - 两个升序列的同样元素 代码(C)
两个升序列的同样元素 代码(C) 本文地址: http://blog.csdn.net/caroline_wendy 两个升序列的同样元素, 须要使用两个指针, 依次遍历, 假设相等输出, 假设小于或 ...
- Map-produce算法两个开源实现
https://github.com/michaelfairley/mincemeatpy https://github.com/denghongcai/mincemeat-node
- python实现高速排序算法(两种不同实现方式)
# -*- coding: utf-8 -*- """ Created on Fri May 16 17:24:05 2014 @author: lifeix " ...
- 算法两数之和 python版
方法一.暴力解法 -- 5s 复杂度分析:时间复杂度:O(n^2)空间复杂度:O(1) length = len(nums)for i in range(length): for j in ra ...
随机推荐
- 使用 Preload&Prefetch 优化前端页面的资源加载
对于前端页面来说,静态资源的加载对页面性能起着至关重要的作用.本文将介绍浏览器提供的两个资源指令-preload/prefetch,它们能够辅助浏览器优化资源加载的顺序和时机,提升页面性能. 一.从一 ...
- 浅谈 Java 中的 AutoCloseable 接口
本文对 try-with-resources 语法进行了较为深入的剖析,验证了其为一种语法糖,同时给出了其实际的实现方式的反编译结果,相信你在看完本文后,关于 AutoCloseable 的使用你会有 ...
- PVE API创建虚拟机
度娘,谷歌都搜了一圈没有找到通过PVE API创建虚拟机的方式, 于是查官网自己试了试,部分代码抄的Sam Liu大佬的作业,感谢大佬. python代码如下: import requests # s ...
- 5、SpringBoot连接数据库引入mybatis
系列导航 springBoot项目打jar包 1.springboot工程新建(单模块) 2.springboot创建多模块工程 3.springboot连接数据库 4.SpringBoot连接数据库 ...
- 使用WTM框架创建博客系统后台并在云服务器发布
阅读导航 关于lqclass.com 博客后台前后端部署 2.1 已部署访问链接 2.2 nginx 部署 2.2.1 后台后端发布 2.2.2 后台前端发布 2.2.3 云服务器部署 下次分享 1. ...
- 使用React简短代码动态生成栅格布局
使用React简短代码动态生成栅格布局 作为 TerminalMACS 的一个子进程模块 - React Web管理端,使用Ant Design Pro作为框架. 本文应用到的知识 1.样式文件les ...
- 基于AHB_BUS Clac slave详解
基于AHB-APB BUS slave详解 1.目录 高内聚:让模块的功能更集中,更单一. AMBA总线例子,需要有一个模块和AMBA进行交互,就可以单独将与AHB总线进行交互的部分作为一个模块.经常 ...
- Vue - 父子级的相互调用
父级调用子级 父级: <script> this.$refs.child.load(); 或 this.$refs.one.load(); </script> 子级: < ...
- web - 解决 formdata 打印空对象
获取单个值可以使用formData对象.get();而直接打印是看不到的.因为外界访问不到,你使用append方法后,对应的键值对就已经添加到表单里面了,你在控制台看到的是FormData原型,存储的 ...
- MongoDB 部署分片集群
部署配置服务器:configsvr 先生成.conf文件 mkdir -p /data/mongodb/configsvr vim /data/mongodb/configsvr/configsvr. ...