SAT分离轴--判断两个形状是否相交给出MTV

简介:分离轴理论,简称SAT(Separating AxisTheorem),是一个判断两个凸多边形是否碰撞的理论。此理论可以用于找到最小的渗透向量(感觉应该是模最小的),此向量在物理模拟和其他很多应用中很有用。SAT是一种高效的算法,能够出去每种形状对(譬如 圆和圆 圆和多边形 多边形和线段)对碰撞检测代码的需求从而减少代码减轻维护压力。

凸多边形:

SAT 就像以前说过的一样,是一个检测两个凸多边形是否相交的方法。对于一个形状,如果对于所有的穿过这个形状的线,这条线只和这个形状至多相交两次。如果多余两次,这个形状就是一个非凸多边形或者说凹多边形。看 维基百科的定义 和数学世界的定义 来了解更多的数学和官方定义。让我们来看一些例子:
分离轴理论,简称SAT(Separating AxisTheorem)

图示1:一个凸多边形


图示2:一个凹多边形

第一个图形被看做凸多边形因为不存在一条穿过它而且和他相交两次的的只想。第二个存在,所以是凹多边形。


图示3:凹多边形的分解

SAT只能处理凸多边形,不多问题不大,因为非凸多边形可以被分解为凸多边形,如上图示。所以如果我们将图示2 中的凹多边形分解我们可以得到两个凸多边形。然后我们就可以使用SAT对整个形状进行检测了。


图示4:投影

投影:

SAT使用的下一个概念叫做投影。想象一下如果你有一个全是平行光的光源。如果你将这个光投向一个物体它就会在一个平面上产生一个阴影。一个阴影是一个三维(3d)物体的二维(2d)的投影。一个二维物体的投影是一个一维“阴影”。

算法描述:

SAT描述为:如果两个凸多边形没有相交,那么存在这两个物体在一个轴上的投影不重叠。

不相交:

首先让我们讨论一下SAT如何判断两个形状没有相交。在图示五中我们可以看出这两个形状没有相交。可以划出一条线来展示出来。


图示5:没有相交的两个凸多边形

 

如果我们选择一条和图示中分割线垂直的线,将这两个凸多边形相对于这条垂线投影就会看出,投影没有重叠。那么这条垂线就被称作一条分离轴。在图示6中深灰色的线既是一个分离轴,而且各自对应颜色的线就是这两个多边形想这条分离轴的投影。注意:在图示6中投影没有重叠,所以根据SAT我们可以得出结论这两个多边形没有相交。


图示6:两个没有相交的凸多边形和他们各自对应的投影

 

SAT可能会测试多个轴来判断是否重叠,如果找到一个轴两个多边形对应的投影没有重叠,那么这个算法立马可以得出结论这两个多边形没有相交。因为这个前提,SAT对于一些物体很多碰撞很少的应用(游戏,模拟,等等)都是非常理想的。

 

为了更好地展示这个算法,给出一些伪代码:

01
    

Axis[] axes = // get the axes to test;

02
    

// loop over the axes

03
    

for (int i = 0; i < axes.length; i++) {

04
    

  Axis axis = axes[i];

05
    

  // project both shapes onto the axis

06
    

  Projection p1 = shape1.project(axis);

07
    

  Projection p2 = shape2.project(axis);

08
    

  // do the projections overlap?

09
    

  if (!p1.overlap(p2)) {

10
    

    // then we can guarantee that the shapes do not overlap

11
    

    return false;

12
    

  }

13
    

}

 

 

图示7:两个相交的凸多边形

 

相交:

如果对于所有的轴,两个多边形的投影都相交,那么我们就可以得出结论这两个多边形相交。图示7展示出了这种情况。

所有的轴必须都在考虑范围之内,跟改后的伪代码为:

01
    

Axis[] axes = // get the axes to test;

02
    

// loop over the axes

03
    

for (int i = 0; i < axes.length; i++) {

04
    

  Axis axis = axes[i];

05
    

  // project both shapes onto the axis

06
    

  Projection p1 = shape1.project(axis);

07
    

  Projection p2 = shape2.project(axis);

08
    

  // do the projections overlap?

09
    

  if (!p1.overlap(p2)) {

10
    

    // then we can guarantee that the shapes do not overlap

11
    

    return false;

12
    

  }

13
    

}

14
    

// if we get here then we know that every axis had overlap on it

15
    

// so we can guarantee an intersection

16
    

return true;

 

得到分离轴:


图示8:边的法向量

当我使用这个算法的时候第一个问题就是,我怎么知道去测试哪些轴。其实这个东西很简单:

你要测试的轴就是每个边的法向量。

边的法向量可以通过对换x,y然后对其中的一个取负就行了。

举例说明:

01
    

Vector[] axes = new Vector[shape.vertices.length];

02
    

// loop over the vertices

03
    

for (int i = 0; i < shape.vertices.length; i++) {

04
    

  // get the current vertex

05
    

  Vector p1 = shape.vertices[i];

06
    

  // get the next vertex

07
    

  Vector p2 = shape.vertices[i + 1 == shape.vertices.length ? 0 : i + 1];

08
    

  // subtract the two to get the edge vector

09
    

  Vector edge = p1.subtract(p2);

10
    

  // get either perpendicular vector

11
    

  Vector normal = edge.perp();

12
    

  // the perp method is just (x, y) => (-y, x) or (y, -x)

13
    

  axes[i] = normal;

14
    

}

 

 

在上面的方法中,我们返回的是多边形的每个边的垂直向量。也就是法向量。这些向量并没有被单位化(将其模置为单位长度)。如果你只是想要得到一个布尔结果从SAT算法中这样会有小,但是如果你想要得出碰撞的具体信息(这个东西将会在MTV部分讨论),那么这些法向量需要被单位化。

 

对每个形状执行上面的操作能够得到两组轴。那么就会再次需要我们更该伪代码:

01
    

Axis[] axes1 = shape1.getAxes();

02
    

Axis[] axes2 = shape2.getAxes();

03
    

// loop over the axes1

04
    

for (int i = 0; i < axes1.length; i++) {

05
    

  Axis axis = axes1[i];

06
    

  // project both shapes onto the axis

07
    

  Projection p1 = shape1.project(axis);

08
    

  Projection p2 = shape2.project(axis);

09
    

  // do the projections overlap?

10
    

  if (!p1.overlap(p2)) {

11
    

    // then we can guarantee that the shapes do not overlap

12
    

    return false;

13
    

  }

14
    

}

15
    

// loop over the axes2

16
    

for (int i = 0; i < axes2.length; i++) {

17
    

  Axis axis = axes2[i];

18
    

  // project both shapes onto the axis

19
    

  Projection p1 = shape1.project(axis);

20
    

  Projection p2 = shape2.project(axis);

21
    

  // do the projections overlap?

22
    

  if (!p1.overlap(p2)) {

23
    

    // then we can guarantee that the shapes do not overlap

24
    

    return false;

25
    

  }

26
    

}

27
    

// if we get here then we know that every axis had overlap on it

28
    

// so we can guarantee an intersection

29
    

return true;

 

将形状A向一个轴投影:

另一个没有很清晰的事情就是如何将一个图形向一个轴投影。将一个图形向一个轴投影其实是一个很简单的事情。循环所有的顶点执行和待测试轴的点乘,存储下最小值和最大值。

01
    

double min = axis.dot(shape.vertices[0]);

02
    

double max = min;

03
    

for (int i = 1; i < shape.vertices.length; i++) {

04
    

  // NOTE: the axis must be normalized to get accurate projections

05
    

  double p = axis.dot(shape.vertices[i]);

06
    

  if (p < min) {

07
    

    min = p;

08
    

  } else if (p > max) {

09
    

    max = p;

10
    

  }

11
    

}

12
    

Projection proj = new Projection(min, max);

13
    

return proj;

 

找到MTV

到目前为止我们只能判断这两个形状是否相交。除此之外,SAT可以返回一个MTV(Minimum Translation Vector 最小分离向量)。MTV是一个最小量级的将两个相交的图形分离的向量。如果我们回顾一下图示7就会发现 轴C 有着最小的投影重叠。那个轴和那个重叠就是MTV,那个轴指示出MTV的方向,重叠就是这个系数(翻译者注:重叠指的是重叠的长度,是一个标量,轴指示方向,可以理解为一个单位矢量,这样就组成了向量的一种表示方式,单位向量和长度)。

为了确定两个形状是否相交,我们必须要测试完所有的轴,即两个形状的所有的边的所有的法向量,而且同时我们还能求出最小的重叠和轴。如果我们更改我们的伪代码将这我们所说的包含其中我们就可以返回MTV当这两个形状相交的时候:

01
    

double overlap = // really large value;

02
    

Axis smallest = null;

03
    

Axis[] axes1 = shape1.getAxes();

04
    

Axis[] axes2 = shape2.getAxes();

05
    

// loop over the axes1

06
    

for (int i = 0; i < axes1.length; i++) {

07
    

  Axis axis = axes1[i];

08
    

  // project both shapes onto the axis

09
    

  Projection p1 = shape1.project(axis);

10
    

  Projection p2 = shape2.project(axis);

11
    

  // do the projections overlap?

12
    

  if (!p1.overlap(p2)) {

13
    

    // then we can guarantee that the shapes do not overlap

14
    

    return false;

15
    

  } else {

16
    

    // get the overlap

17
    

    double o = p1.getOverlap(p2);

18
    

    // check for minimum

19
    

    if (o < overlap) {

20
    

      // then set this one as the smallest

21
    

      overlap = o;

22
    

      smallest = axis;

23
    

    }

24
    

  }

25
    

}

26
    

// loop over the axes2

27
    

for (int i = 0; i < axes2.length; i++) {

28
    

  Axis axis = axes2[i];

29
    

  // project both shapes onto the axis

30
    

  Projection p1 = shape1.project(axis);

31
    

  Projection p2 = shape2.project(axis);

32
    

  // do the projections overlap?

33
    

  if (!p1.overlap(p2)) {

34
    

    // then we can guarantee that the shapes do not overlap

35
    

    return false;

36
    

  } else {

37
    

    // get the overlap

38
    

    double o = p1.getOverlap(p2);

39
    

    // check for minimum

40
    

    if (o < overlap) {

41
    

      // then set this one as the smallest

42
    

      overlap = o;

43
    

      smallest = axis;

44
    

    }

45
    

  }

46
    

}

47
    

MTV mtv = new MTV(smallest, overlap);

48
    

// if we get here then we know that every axis had overlap on it

49
    

// so we can guarantee an intersection

50
    

return mtv;

 

曲边图形:

我们已经看到了多边形如何使用SAT进行测试,但是像圆这种曲边图形如何被测试呢?曲边图形向SAT提出了挑战,因为曲边图形有着无限多的待测试轴.解决这种问题的通常办法就是将圆形和圆形,圆形和多边形分解,通过做一些特殊的操作.另一个可选择的操作不在所有的情况下都是用曲边图形,而是用多定点的多边形取代她.第二中选择方案对我们之前写的伪代码不会造成影响,但是我在这里想介绍第一种方法.

让我们首先看一下圆形和圆形,通常情况下,你会做如下的事情:

Vector c1 = circle1.getCenter();

2
    

Vector c2 = circle2.getCenter();

3
    

Vector v = c1.subtract(c2);

4
    

if (v.getMagnitude() < circle1.getRadius() + circle2.getRadius()) {

5
    

  // then there is an intersection

6
    

}

7
    

// else there isnt

 

我们知道当两个圆形的圆心距小于各自的半径之和时两个圆形相交.这其实就是一个SAT测试.为了得到结果我们这样进行SAT测试:

1
    

Vector[] axes = new Vector[1];

2
    

if (shape1.isCircle() && shape2.isCircle()) {

3
    

  // for two circles there is only one axis test

4
    

  axes[0] = shape1.getCenter().subtract(shape2.getCenter);

5
    

}

6
    

// then all the SAT code from above

多边形和圆形会带来更多的问题, 图心和图心在多边形的待测试轴上的测试并不奏效,事实上会得到意想不到的错误结果.在这种情况下,你必须要包含另一个轴:那个从距离圆形最近的顶点到圆心的轴.多边形上最近的顶点的求法有很多种,理想的解决办法是Voronoi区域算法,但是不会在这篇文章中涉及.

其他的曲边图形会带来更多的问题,必须要使用各自特定的解决办法.例如一个胶囊形状可以被分解为一个矩形和两个半圆.

 

图示9:包含

包含:

一个很多开发者选择忽略的问题就是包含.一个图形包含另一个图形会发生什么么?这个问题并不会带了巨大的影响因为绝大多数的应用都不会让这种情况发生.首先让我介绍一下这个问题然后是如何解决它.然后我会介绍为什么我们要将他置于考虑范围之内.

如果一个图形被另一个图形包含,在我们现有的伪代码中,SAT会返回不对的MTV.方向和量级都有可能不对.图示9展示了在轴上的投影的重叠不能够将两个图形分离开来.所以我们需要做的是在重叠测试中检测有没有包含.在上面的SAT代码中加上if语句:

01
    

if (!p1.overlap(p2)) {

02
    

  // then we can guarantee that the shapes do not overlap

03
    

  return false;

04
    

} else {

05
    

  // get the overlap

06
    

  double o = p1.getOverlap(p2);

07
    

  // check for containment

08
    

  if (p1.contains(p2) || p2.contains(p1)) {

09
    

    // get the overlap plus the distance from the minimum end points

10
    

    double mins = abs(p1.min - p2.min);

11
    

    double maxs = abs(p1.max - p2.max);

12
    

    // NOTE: depending on which is smaller you may need to

13
    

    // negate the separating axis!!

14
    

    if (mins < maxs) {

15
    

      o += mins;

16
    

    } else {

17
    

      o += maxs;

18
    

    }

19
    

  }

20
    

  // check for minimum

21
    

  if (o < overlap) {

22
    

    // then set this one as the smallest

23
    

    overlap = o;

24
    

    smallest = axis;

25
    

  }

将包含加入其中的原因:

1.游戏中给两个如此定义的形状是有可能的.不解决这个问题会要求我们根据两个形状的大小进行两次或者更多的SAT循环来解决这种特殊的碰撞.

2.如果你准备支持其他形状的线段分割,你有必要这样做,因为在一些情况下投影的重叠值可能为零,这是因为一个线段的分割是一个极小的图形这个事实(译者注:这些东西后面的翻译中会有).

其他需要注意的事情:

1.被测试轴的数量可以通过不测试平行轴的方式减少.这就是为什么一个长方形可以只测试两个轴.

2.之前的分离轴可以作为下一次SAT循环的基础,这样这个算法在两个形状不想交的时候就会使O(1)的时间复杂度.

3.SAT在3D情况下就会有可能要测试要非常多的轴.

4.我不是一个专家,所以请原谅我的糟糕的几何.

 

 

到这里翻译完毕,(*^__^*)嘻嘻……累死我了,再次万分感谢Wlliam将他学习的知识无私的分享出来,致敬!

学习源码非一日之功,且学且珍惜.
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值