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. VIM 入门手册, (VS Code)

    要想在VScode里使用Vim需要先行按照插件 安装 vim 插件 VS Code 中输入快捷键 shift + ctrl + x, 或直接打开 扩展安装导航 搜索 vim, 选择 Vim , 点击 ...

  2. SpringBoot多模块项目搭建以及搭建基础模板

    多模块项目搭建 目录 多模块项目搭建 1.父项目pom文件编辑 2.创建子模块 1.父项目pom文件编辑 <!--1.父工程 添加pom格式--> <packaging>pom ...

  3. vue-cli 3.x 项目,如何增加对 jsx 的支持?vue-cli 3.x 项目,如何增加对 jsx 的支持?

    https://segmentfault.com/q/1010000019655500

  4. 接口自动化复习第四天利用正则和faker提取替换变量值

    在做接口自动化测试的时候,我们经常会遇到,有些字段利用随机生成数据就行了,不需要自己去构造测试数据.今天我就是要python中的第三方库faker来构造随机数,其次使用正则表达式来提取变量. 首先在接 ...

  5. 面向对象C++学习总结

    洛谷日记3 2023.5 面向对象C++ : 运算符重载 1.运算符重载 (1)n定义重载运算符和定义普通函数类似,只是该函数的名字是operator@,@表示要重载的运算符. MinInt oper ...

  6. Linux系统CPU异常占用(minerd 、tplink等挖矿进程)

    转载请注明出处: 云服务器ECS(Linux) CPU使用率超过70%,严重时可达到100%,服务器响应越来越慢.  服务器中存在恶意minerd.tplink进程 该进程是服务器被入侵后,被恶意安装 ...

  7. LeetCode-Go:一个使用 Go 语言题解 LeetCode 的开源项目

    在中国的 IT 环境里,大多数场景下,学习算法的目的在于通过笔试算法题. 但算法书林林总总,有时候乱花渐欲迷人眼. 杜甫有诗云:读书破万卷,下笔如有神.不管选择哪本书,只要深入学习,分层次,逐层进阶, ...

  8. AHB 局限性

    AHB's problem SoC bus 架构 AXI is used more and more 频率200M使用AHB,频率再升高就使用AXI AHB的问题 AHB协议本身限制要求较高,比如co ...

  9. 08-Shell计算命令

    1.expr命令 expr (evaluate expressions 的缩写),译为"表达式求值".Shell expr 是一个功能强大,并且比较复杂的命令,它除了可以实现整数计 ...

  10. 部署开源项目管理工具focalboard

    前言 focalboard是一款开源项目管理工具,类似Jira.Trello.官网地址 组件 版本 说明 Debian 12.1 操作系统 docker 20.10.7 容器运行时 docker-co ...