SAT分离轴碰撞原理分析

1,803 阅读5分钟

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

在游戏中,我们有一个非常常用的算法功能,就是碰撞检测,对于常见的游戏,比如打飞机,飞机与飞机碰撞,子弹与飞机碰撞,都要用的碰撞检测,对于日常的碰撞检测来说,矩形碰撞,圆形碰撞大家可能都已经熟能生巧,也是个基本功,但如果遇到了多边形碰撞呢,比如我们需要一个六边形与七边形碰撞,该怎么处理呢?结合下面这张动图,我们来进入SAT的世界。

SAT检测.gif

概念与概念分析

分离轴检测可以用一句简单易懂的话来概括:如果两个物体没有发生碰撞,那么就肯定会有一条线,能将两个物体分离开,我们称这条线为分离轴。如下图所示:

两矩形裸分离轴图片.jpg 从图中可以看出,左边的两个矩形没有发生碰撞,中间的分离轴将其分隔开来,而右边的两个矩形发生碰撞,找不到一条直线将其分割,那么对于这两种情况,我们应该怎么去判断呢?

投影标注.jpg 图中用不同颜色标注出了ABCD,EFGH几个坐标点,这几个坐标点是矩形的每一个角所在的坐标点在X轴上的投影,我们可以清晰的看出,左边的矩形所有投影可以分为投影蓝为[A,D],右边的矩形所有投影可以分为投影红为[E,H]。对于左边对分离轴分隔开的投影,投影蓝与投影红是没有交集的,也就是交集为空,没有相同的地方,但对于右边的两个矩形投影来说,[C,D]与[E,F]区间是有一定的交集,交集不为空集,则两个矩形相交。

SAT的核心

综上所述,其实对于SAT来说,核心是找到分离轴,也就是找到交集为空的投影,所以我们要做的就是找到一条将两个凸多边形一分为二的轴。

SAT核心的问题与提出

如果我们想要证明或者说是解决SAT的核心理念,我们这里就会出现两个问题

1.我们去哪找这轴?

2.我们怎么判断这条轴上的投影没有空集?

我们如何找到这个轴

首先是第一个问题,

我们想要找到真正的分离轴,我们就需要先找到所有的潜在轴,然后再在每条潜在轴上玩投影,检测空集。但是对于两个不规则的凸多边形来说,潜在轴太多了,我随便画条线,那都能叫潜在轴。所以我们这时候需要知道一点东西,那就是凸多边形的性质。

我们直接打开浏览器百度一下:凸多边形的性质。我们可以直接的看到一条凸多边形的性质为----》》

如果把一个凸多边形的所有边中,有一条边向两边无限延长成为一条直线时,其他各边(各角)都在此直线的同旁。

有了这个性质,我们可以得出来一个结论:我们所要检测的潜在轴是两个凸多边形的边

这个时候第一个问题就解决了,那么我们如何检测多边形在每条边上的投影呢?这时候就要引入一个概念为法向量

投影相交副本.jpg 图中用数字标明了8个线段,分别是1-5和6-8,其中右方的蓝色直线即为线段5的投影轴,也就是线段5的法向量,线段5即为潜在轴,蓝色直线即为线段5的投影轴,那么我们如何求得线段与投影轴呢?

图中可知线段的两个顶点,P1与P2,那么我们可以用向量来表示一个线段

对于线段P1P2就可以使用P1与P2做向量相减得到。代码为

/**相减 */ 
static sub(v1: Vector, v2: Vector) { 
    return new Vector(v1.x - v2.x, v1.y - v2.y); 
}; 
Vector.sub(P1,P2);

sub为一个向量相减的静态方法,将P1与P2传入即可获得线段P1P2.具体的vector类可以查看源码。

有了线段之后,我们的投影轴就更好求了,投影轴其实就是法向量,所以我们直接用相减得到的结果来求法向量

/**法向量 */ 
normL() { 
    return new Vector(this.y, -this.x); 
} 
Vector.sub(P1,P2).normL();

normL即为Vector类中求法向量的方法。

法向量相信很多人都清楚,就是垂直于原向量的玩意,具体的法向量的性质这里就不做过多的解释了。

那么我们也就可以获取一个多边形每一条边以及每一条边所对应的法向量了:

/**获得多边形的边,这里使用路径点的坐标来求有向线段的向量,终点-起点 */ 
getSides() { 
    let _path = this.path, 
    len = _path.length, 
    sides = [], 
    pre = _path[0] 
    if (len >= 3) { 
    for (let i = 1; i < len; i++) { 
        let cur = _path[i]; 
        sides.push(Vector.sub(cur, pre)); 
        pre = cur; 
    } 
    sides.push(Vector.sub(_path[0], _path[len - 1])); 
    } 
    return sides; 
} 
let sides = polygon1.getSides().concat(polygon2.getSides()); 
let axises = []; 
for (let j = 0, l = sides.length; j < l; j++) { 
    axises.push(sides[j].normL()); 
}

path为待检测多边形的路径集合,即每个顶点的坐标集合,如上图所示求的所有边与法向量。

那么现在来到第二个问题,

我们怎么判断这条轴上的投影没有空集

投影未相交.jpg

投影相交.jpg 其实很简单,我们可以看到两个图中,都有着Amin,Amax和Bmin,Bmax,意思就是我们只需要判断Amax到Amin加上Bmax到Bmin小于Bmax到Amin就可以判断到投影有交集,反之没有了。

那么如何来求投影呢?

求投影副本.jpg 基于上图我们可以从向量的性质入手,b向量在a向量方向上的投影为

投影概念.jpg 而且我们知道对于a=(x1,y1),b=(x2,y2)有a·b为x1x2+y1y2,那么我们就可以求出来投影啦!!!!!!

/**点积 */
static dot(v1: Vector, v2: Vector) {
     return v1.x * v2.x + v1.y * v2.y;
}

/**获取每一个路径的当前坐标 */
getRootCoord() {
   let _path = this.path, readlPath = [];
   for (let i = 0; i < _path.length; i++) {
       readlPath.push(new Vector(_path[i].x + this.x, _path[i].y + this.y));
   }
   return readlPath;
}

/**获取投影 */
getProjection(axis:Vector) {
    let path = this.getRootCoord(), min = null, max = null;
    for (let i = 0, l = path.length; i < l; i++) {
         let p = path[i];
         let pro = Vector.dot(p, axis) / axis.length();
         if (min === null || pro < min) {
             min = pro;
         }
         if (max === null || pro > max) {
             max = pro;
         }
     }
    return { min: min, max: max };
}

紧接着就简单了,我们已经获取了所有潜在轴,所有潜在轴的投影轴,所有投影,所有投影的最大与最小值,那么来一个简单的判断就完事

    /**
     * 这里就是判断碰撞了,投影的最大值与最小值两段相减再与总值相比,即可求得了!
     */
    isCollsion(proA, proB) {
        let min, max;
        if (proA.min < proB.min) min = proA.min;
        else min = proB.min;
        if (proA.max > proB.max) max = proA.max;
        else max = proB.max;
        return (proA.max - proA.min) + (proB.max - proB.min) < max - min;
    }

好了,分离轴碰撞检测的原理分析完毕,希望能够在现在或者未来的某个时刻帮助到大家,如果有什么建议或者疑问欢迎讨论~