EPA,是扩展多边形算法(Epanding Polytop Algorithm) ,用来计算两个多边形碰撞的穿透深度和方向,可用于将两个发生碰撞的多边形分离。本文的写作目的,主要是对GJK和EPA算法的理解和应用。对算法本身感兴趣的朋友,可以阅读源论文的文献。本系列GJK算法文章共三篇,本篇是第三篇,强烈建议从第一篇开始看:

  • GJK碰撞检测算法基础
  • GJK计算多边形之间的最近距离
  • GJK和EPA计算穿透向量

本文作者游蓝海。原创不易,未经许可,禁止任何形式的转载。

1. 基本原理

当碰撞发生时,原点到最近的闵可夫斯基差集多边形的边(下文称作“差集最近边”)的距离,就是穿透深度,原点到该边的垂直向量就是穿透向量的方向。因此,核心问题就转换成了,如何求得距离原点最近的差集边。

当碰撞检测完毕后,我们会得到一个单形体(Simplex),该单形体可能包含两个或三个support点,将这些support点首尾相连构成封闭的多边形。EPA算法每次迭代的时候,从这个多边形中选择一条最近的边,沿着该边的法线方向(原点到边的垂线方向),向外扩展。直到某条边无法向外扩展时,则该边就是差集最近边。

可以看到,EPA算法和GJK求最近距离算法很相似,都是在找一个差集最近边。不过EPA用于发生碰撞的情况下,GJK求最近距离用于没有发生碰撞的情况下。理论上EPA也可以用于求两个多边形的最近边,只不过EPA收敛的速度没有GJK算法快。

2. 算法解析

EPA算法的前提是,两个多边形发生了碰撞。因此,我们要先使用GJK算法检测出碰撞,然后将得到的单形体,作为EPA算法开始的条件。

2.1 算法伪代码

Vector2 EPA(Simplex simplex)
{// 构造一个首尾相连的多边形simplexEdge.initEdges(simplex);while(true){// 找到距离原点最近的边Edge e = simplexEdge.findClosestEdge();// 沿着边的法线方向,尝试找一个新的support点Vector2 point = support(e.normal);// 无法找到能够跨越该边的support点了。也就是说,该边就是差集最近边float distance = Vector2.Dot(point, e.normal);if (distance - e.distance <= 0){// 返回穿透向量return e.normal * distance;}// 将新的support点插入到多边形中。// 也就是将边e从support点位置分割成两条新的边,并替换到多边形集合中。simplexEdge.insertEdgePoint(e, point);}
}

算法步骤描述:

  1. 构造一个首尾相连的多边形,也就是得到边的集合;
  2. EPA迭代开始;
  3. 找到一个距离原点最近的边;
  4. 沿着该边的法线方向,尝试查找一个support点;
  5. 如果无法找到更远的support点,则说明该边就是差集最近边,算法结束;
  6. 将新的support点插入多边形中,相当于多边形向外扩展了;
  7. 跳转到步骤2。

计算步骤的分解动图:

2.2 构造边集

我们需要得到的是单形体多边形的边的集合,只用将support点首尾即可。为了方便后续计算,我们把每条边的法线和到原点的距离也计算出来。法线就是原点到边的垂线,取向外的方向。

有一个特殊的情况,如果GJK算法结束时,原点恰好位于单形体的某条边上。此时单形体中将会只有两个support点,且这两点的连线是经过原点的。这种情况下,无法用计算垂足的方法计算垂线,需要用到数学的方法:向量(x, y)的垂直向量为: (y, -x),或者(-y, x)。随便用哪一个都行,因为两个support点首尾相连,会构成两条方向相反的边,恰好用数学方法求得的垂直向量方向也自然是相反的,EPA算法将会沿着两个相反的方向向外扩展。

void initEdges(Simplex simplex)
{int n = simplex.count();for (int i = 0; i < n; ++i){int iNext = (i + 1) % n;Edge edge = createEdge(simplex.get(i), simplex.get(iNext));edge.index = i;edges.Add(edge);}
}Edge createEdge(Vector2 a, Vector2 b)
{Edge e = new Edge();e.a = a;e.b = b;// 计算垂足Q。则法线向量为 OQ = Q - (0, 0) = Qe.normal = GJKTool.getPerpendicularToOrigin(a, b);e.distance = e.normal.magnitude;// 单位化边if (e.distance > float.Epsilon){e.normal *= 1.0f / e.distance;}else{// 如果距离原点太近,用数学的方法来得到直线的垂线// 方向可以随便取,刚好另外一边是反着来的Vector2 v = a - b;v.Normalize();e.normal = new Vector2(-v.y, v.x);}return e;}

2.3 查找最近边

找到距离原点最近的边即可:

Edge findClosestEdge()
{float minDistance = float.MaxValue;Edge ret = null;foreach (var e in edges){if (e.distance < minDistance){ret = e;minDistance = e.distance;}}return ret;
}

2.4 插入新的support点

将边e断开,e原来的两个端点分别与support点进行连接,然后重新插入到边的集合中。

void insertEdgePoint(Edge e, Vector2 point)
{Edge e1 = createEdge(e.a, point);// 覆盖掉原来e的位置edges[e.index] = e1;Edge e2 = createEdge(point, e.b);// 插入新的边edges.Insert(e.index + 1, e2);// 重新分配边的索引updateEdgeIndex();
}

2.5 浮点数误差问题

(增加于2021/08/22) 当GJK算法结束后,如果某个单形体的边经过了原点或者离原点非常近,则这条边的法线方向将会无法正确的计算出来,会导致后续的EPA扩张方向出现混乱,无法正确的计算出穿透向量。因此,最稳妥的方法是,进入EPA算法的时候,仅保留单形体的一条边,也就是只留下2个顶点。则EPA初始的边集只有两个有向边 ab和ba,而且两者的法线方向刚好相反,后续会沿着相反的方向进行扩张,不会产生交叉混乱的情况。

// 仅保留距离原点最近的一条边,避免浮点数误差引起原点落在了边上,造成无法计算出该边的法线方向
if (simplex.count() > 2)
{findNextDirection(); // 会自动删除一个离远点最远的点
}
...
edges.Add(createInitEdge(simplex.get(0), simplex.get(1)));
edges.Add(createInitEdge(simplex.get(1), simplex.get(0)));

3. 小结

EPA算法计算穿透向量,本质上是求得差集最近边。EPA从GJK算法结束后开始,不断的扩展单形体,直到达到边界。

本章Demo使用Unity3D引擎开发,Demo工程已上传github: https://github.com/youlanhai/learn-physics/tree/master/Assets/05-gjk-epa

4. 参考

  • GJK算法论文: https://ieeexplore.ieee.org/document/2083?arnumber=2083
  • EPA (Expanding Polytope Algorithm): http://www.dyn4j.org/2010/05/epa-expanding-polytope-algorithm

本系列文章会和我的个人公众号同步更新,感兴趣的朋友可以关注下我的公众号:游戏引擎学习。扫下面的二维码加关注:

物理引擎学习05-GJK和EPA计算穿透向量相关推荐

  1. 物理引擎学习04-GJK计算多边形之间的最近距离

    计算多边形之间的最近距离,才是GJK算法原本的目的.只有两个多边形不相交,计算最近距离才有效.如果相交,则最近距离无效,但是可以使用EPA算法要计算碰撞深度.本文的写作目的,主要是对GJK算法的理解和 ...

  2. 物理引擎学习03-GJK碰撞检测算法基础

    GJK是由Gilbert,Johnson,Keerthi 三位前辈发明的,用来计算两个凸多面体之间的碰撞检测,以及最近距离.GJK算法可以在O(M+N)的时间复杂度内,检测出碰撞,算法在每次迭代的过程 ...

  3. 物理引擎学习06-碰撞反馈

    原本计划06章是一个碰撞检测的小demo,上手之后才发现,碰撞反馈也是一个非常复杂的话题,所以就单拎出来一章,详细说明.碰撞反馈是基于碰撞检测的结果,将发生接触的物体分离开,同时应用上物理效果,使碰撞 ...

  4. 物理引擎学习07-小游戏飞机大战

    到目前为止,碰撞检测的基本内容(狭义的碰撞检测)已经讲完了.广义的碰撞检测,我们到下一阶段再继续.本小节,在上节"碰撞反馈"的基础之上,扩展支持多物体间的碰撞检测.使用最简单的方法 ...

  5. 物理引擎学习08-AABB树

    AABB树是由AABB包围盒结点构成的二叉树,常用来加速场景中的射线检测和碰撞检测.树的每个结点都是一个包围盒,且结点的包围盒包裹了所有子结点的包围盒.本文深入的讲解了AABB树相关的算法,以及结合物 ...

  6. Chipmunk-js物理引擎学习笔记

    一.基本概念 空间:在Chipmunk中,空间是所有对象容器.因此,刚体.形状.链接节点等对象都需要添加到空间中.空间控制这些对象的相互作用. 刚体:物理上的刚体指的是在运动和受力作用后,形状和大小不 ...

  7. [Unity 3D] 物理引擎学习笔记(一)

    刚体: 同是物理引擎提供的功能,碰撞检测只需要有 Collider 便可以运作,但所有与作用力相关的属性和函数却都依赖 Rigidbody. 重力: 一旦使用了 Rigidbody 组件,这个 Gam ...

  8. bullet 物理引擎资料

    bullet 官方网站 https://pybullet.org gamekit-developers 开源的游戏引擎,由 bullet 创始人发起. https://github.com/gamek ...

  9. 物理引擎-弹性碰撞动量守恒

    效果演示 // 多物体物理引擎.cpp : 此文件包含 "main" 函数.程序执行将在此处开始并结束. // #include <stdio.h> #include ...

最新文章

  1. 金立M2017续航出色不仅是因为电池大,还有超强快充
  2. linux 子网和广播地址异常
  3. php百合网,来百合网直播,做最真实的自己
  4. Linux系统下Oracle11g r1的安装之四: 开始安装Oracle
  5. php attr,PHP DOMAttr isId()用法及代码示例
  6. DRF的序列化——Serializers 序列化组件
  7. matlab常用函数——数学函数
  8. 华为否认鸿蒙为噱头;谷歌公布 6 大 iOS 漏洞;GitLab 又发安全补丁 | 极客头条...
  9. windows 2003常用命令(推荐)
  10. 腾讯技术分享:Android版手机QQ的缓存监控与优化实践
  11. Spring Boot 2.0.3 集成 ActiveMQ 5.15.4 与 内嵌启动 ActiveMQ 服务
  12. SpringBoot入门篇-简介
  13. 机器视觉科学计算可视化_模因视觉:对模因进行分类的科学
  14. linux文件系统与sysfs,Linux设备模型与Sysfs文件系统.doc
  15. 能打开QQ,但打开不了网页-网络热门故障排查
  16. 机器视觉基础应用知识详解
  17. 计算机毕业设计-基于SSM的酒店客房管理系统-JavaWeb酒店客房管理系统
  18. C1——supermap获取对象的中心位置
  19. mysql 白天范围_急求mysql 统计一个时间范围内的白天与夜上的数据方法
  20. 什么是数学建模?如何在数学建模中拿奖?通过建模学到了啥?

热门文章

  1. 向前logistic回归与向后筛选出一样的变量_风控建模之特征筛选与建模(python)...
  2. 定时循环调用FastCopy自动备份脚本
  3. 苹果官网php,苹果官方购买产品服务器端验证代码PHP版
  4. java门禁系统_java实现门禁系统
  5. 芸赞通天下沈扬舍己为人亮风采
  6. SpringBoot2:基础篇(黑马程序员:P1~P53)
  7. 四元数非正式笔记梳理_Quaternion kinematics for the error-state Kalman filter
  8. STM32某些基础概念扫盲
  9. 虚拟机:无法连接虚拟设备 ide1:0,因为主机上没有响应的设备
  10. maltab绘制等量同/异号电荷电势图和电场线