SAT(Separating Axis Theorem)翻译

SAT(分离轴定理)

翻译自: SAT(Separating Axis Theorem)

2010年1月1日发表

  这是我一直想写的一篇文章,但我一直没有抽出时间来做。首先我要说的是,网络上有大量关于这种特殊碰撞检测算法的资料。但是,在解释一些实现细节时,这些资源通常是模糊的(针对我们的目的)。
  我计划解释算法,并填补我自己实现时的一些空白。
  该教程具有交互式flash示例,可供参考。

引言

  分离轴定理,简称SAT,是一种确定两个凸多边形是否相交的方法。该算法还可用于寻找最小穿透矢量,这对于物理仿真和许多其他应用是有用的。SAT是一种快速通用算法,无需对每一对形状类型进行碰撞检测,从而降低代码量和对代码的维护。

凸特性

  如前所述,SAT是一种确定两个凸多边形是否相交的方法。如果某个形状与任何穿过该形状的直线只交叉两次,则该形状被称为凸多边形。如果某个形状与穿过该形状的直线交叉两次以上,则该形状为非凸(或凹)。更正式的定义参见维基的定义数学世界的定义。让我们来看一些例子:在这里插入图片描述

图1: 凸多边形
在这里插入图片描述
图2: 非凸多边形

  第一个形状被认为是凸的,因为不存在一条可以通过该形状绘制的线,该线将交叉两次以上。第二种形状是非凸的,因为存在一条相交两次以上的线。
  SAT只能处理凸多边形,不过,非凸形状可以由凸形状的组合来表示(凸分解)。因此,如果我们需要采用图2中的非凸形状时,可以执行凸分解,这样便得到了两个凸形状。这样我们可以测试每个凸面形状,以确定整个形状的碰撞。
在这里插入图片描述

图3: 凸分解

投影

  SAT使用的下一个概念是投影。假设你有一个光源,它的光线都是平行的。如果您将灯光照射到对象上,它将在平面上形成阴影。该阴影是三维物体的二维投影。二维物体的投影是一个一维“阴影”。
在这里插入图片描述

图4: 投影(阴影)

算法

  SAT指出:“如果两个凸面物体没有穿透,则存在一根轴,使得这两个物体在该轴上的投影不重叠。

无交叉

  首先,让我们讨论SAT如何确定两个形状不相交。图5中的两个形状不相交。可以通过在它们之间画一条线来说明。
在这里插入图片描述

图5: 两个分开的凸多边形

  做辅助线垂直于图5中分隔两个形状的线,并将形状投影到该辅助线上,则它们的投影不重叠。形状的投影(阴影)不重叠的辅助线称为分离轴。在图6中,深灰色线是分离轴,相应的彩色线是形状在分离轴上的投影,其中,投影不重叠,因此根据SAT,形状不相交。
在这里插入图片描述

图6: 两个不相交的凸多边形以及相应的投影

  SAT可以测试多个轴判断是否重叠。只要投影不重叠,算法可以立即确定形状不相交,从而退出循环。基于这样的特性,SAT非常适合具有许多对象但很少碰撞的应用程序(如游戏、模拟等)。
  为了进一步解释,请检查以下伪代码。

Axis[] axes = // get the axes to test;
// loop over the axes
for (int i = 0; i < axes.length; i++) {
  Axis axis = axes[i];
  // project both shapes onto the axis
  Projection p1 = shape1.project(axis);
  Projection p2 = shape2.project(axis);
  // do the projections overlap?
  if (!p1.overlap(p2)) {
    // then we can guarantee that the shapes do not overlap
    return false;
  }
}

相交

  如果对于所有轴,形状的投影都重叠,则我们可以得出形状相交的结论。图7中,在多个轴上测试了两个凸多边形的投影。这些投影都重叠,因此我们可以断定这两个形状是相交的。
在这里插入图片描述

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

  必须测试所有轴是否重叠,以确定交叉点。上述代码修改为:

Axis[] axes = // get the axes to test;
// loop over the axes
for (int i = 0; i < axes.length; i++) {
  Axis axis = axes[i];
  // project both shapes onto the axis
  Projection p1 = shape1.project(axis);
  Projection p2 = shape2.project(axis);
  // do the projections overlap?
  if (!p1.overlap(p2)) {
    // then we can guarantee that the shapes do not overlap
    return false;
  }
}
// if we get here then we know that every axis had overlap on it
// so we can guarantee an intersection
return true;

获取分离轴

  实现该算法时的第一个问题是:如何知道要测试哪些轴?事实上,这非常简单:
  只要测试每个多边形的边的法线即可
在这里插入图片描述

图8: 边的法线

  边的法线可以通过翻转坐标并取反来获得。例如:

Vector[] axes = new Vector[shape.vertices.length];
// loop over the vertices
for (int i = 0; i < shape.vertices.length; i++) {
  // get the current vertex
  Vector p1 = shape.vertices[i];
  // get the next vertex
  Vector p2 = shape.vertices[i + 1 == shape.vertices.length ? 0 : i + 1];
  // subtract the two to get the edge vector
  Vector edge = p1.subtract(p2);
  // get either perpendicular vector
  Vector normal = edge.perp();
  // the perp method is just (x, y) =&gt; (-y, x) or (y, -x)
  axes[i] = normal;
}

在上面的代码中,我们返回形状每个边的垂直向量。这些向量称为“法向量”。然而,这些向量归一化(不是单位长度)。如果您只需要SAT算法的布尔结果,这就足够了,但如果您需要碰撞信息(稍后在MTV部分中讨论),则这些向量需要归一化(请参见将形状投影到轴的部分)

  对每个形状执行此操作,则可获得两个要测试的轴的列表。伪代码修改为:

Axis[] axes1 = shape1.getAxes();
Axis[] axes2 = shape2.getAxes();
// loop over the axes1
for (int i = 0; i < axes1.length; i++) {
  Axis axis = axes1[i];
  // project both shapes onto the axis
  Projection p1 = shape1.project(axis);
  Projection p2 = shape2.project(axis);
  // do the projections overlap?
  if (!p1.overlap(p2)) {
    // then we can guarantee that the shapes do not overlap
    return false;
  }
}
// loop over the axes2
for (int i = 0; i < axes2.length; i++) {
  Axis axis = axes2[i];
  // project both shapes onto the axis
  Projection p1 = shape1.project(axis);
  Projection p2 = shape2.project(axis);
  // do the projections overlap?
  if (!p1.overlap(p2)) {
    // then we can guarantee that the shapes do not overlap
    return false;
  }
}
// if we get here then we know that every axis had overlap on it
// so we can guarantee an intersection
return true;

将多边形投影到轴

  另一个问题是如何将形状投影到轴上。将多边形投影到轴上相对简单;在所有顶点上循环执行与轴的点积,并存储最小值和最大值即可。

double min = axis.dot(shape.vertices[0]);
double max = min;
for (int i = 1; i < shape.vertices.length; i++) {
  // NOTE: the axis must be normalized to get accurate projections
  double p = axis.dot(shape.vertices[i]);
  if (p < min) {
    min = p;
  } else if (p > max) {
    max = p;
  }
}
Projection proj = new Projection(min, max);
return proj;

寻找最小平移向量(MTV)

  到目前为止,我们只在两个形状是否相交时返回真或假。除此之外,SAT还可以返回最小平移向量(MTV)。MTV是可推导出形状发生碰撞的最小幅值向量。回到图7,我们可以看到轴C的重叠部分最小。该轴和重叠便是MTV,其中,轴是矢量部分,重叠是幅度部分。
  为了确定形状是否相交,我们必须在两个形状的所有轴上循环,以便同时跟踪最小重叠和轴。修改伪代码,在形状相交时返回一个MTV。

double overlap = // really large value;
Axis smallest = null;
Axis[] axes1 = shape1.getAxes();
Axis[] axes2 = shape2.getAxes();
// loop over the axes1
for (int i = 0; i < axes1.length; i++) {
  Axis axis = axes1[i];
  // project both shapes onto the axis
  Projection p1 = shape1.project(axis);
  Projection p2 = shape2.project(axis);
  // do the projections overlap?
  if (!p1.overlap(p2)) {
    // then we can guarantee that the shapes do not overlap
    return false;
  } else {
    // get the overlap
    double o = p1.getOverlap(p2);
    // check for minimum
    if (o < overlap) {
      // then set this one as the smallest
      overlap = o;
      smallest = axis;
    }
  }
}
// loop over the axes2
for (int i = 0; i < axes2.length; i++) {
  Axis axis = axes2[i];
  // project both shapes onto the axis
  Projection p1 = shape1.project(axis);
  Projection p2 = shape2.project(axis);
  // do the projections overlap?
  if (!p1.overlap(p2)) {
    // then we can guarantee that the shapes do not overlap
    return false;
  } else {
    // get the overlap
    double o = p1.getOverlap(p2);
    // check for minimum
    if (o < overlap) {
      // then set this one as the smallest
      overlap = o;
      smallest = axis;
    }
  }
}
MTV mtv = new MTV(smallest, overlap);
// if we get here then we know that every axis had overlap on it
// so we can guarantee an intersection
return mtv;

曲线形状

  我们已经看到了如何使用SAT测试多边形,但是像圆形这样的曲线形状呢?曲线形状会给SAT带来问题,因为曲线形状有无限多的分离轴要测试。解决这个问题的方法通常是分解为圆vs圆和圆vs多边形的测试,并做一些更具体的工作。另一种方法是完全不使用曲线形状,使用足够多的顶点数多边形来替换。第二种选择不需要对上面的伪代码进行更改,此处介绍第一种方法。
  让我们先看看圆vs圆。通常,首先会执行以下操作:

Vector c1 = circle1.getCenter();
Vector c2 = circle2.getCenter();
Vector v = c1.subtract(c2);
if (v.getMagnitude() < circle1.getRadius() + circle2.getRadius()) {
  // then there is an intersection
}
// else there isnt

  我们知道,如果圆心距比圆半径之和小,则两个圆就会发生碰撞。这个测试实际上是一个类似SAT的测试。要在SAT中实现这一点,我们可以执行以下操作:

Vector[] axes = new Vector[1];
if (shape1.isCircle() && shape2.isCircle()) {
  // for two circles there is only one axis test
  axes[0] = shape1.getCenter().subtract(shape2.getCenter);
}
// then all the SAT code from above</pre>

  圆形vs多边形带来了更多的问题。沿多边形轴的中心到中心测试是不够的(事实上,可以省略中心到中心的测试)。在这种情况下,必须包含另一个轴:从多边形上最近顶点到圆中心的轴。要找到多边形上最近的顶点有多种方法,如Voronoi区域,该文中将不会讨论。
  其他曲线形状将会是更大的问题,必须以自己的处理方式。例如,胶囊形状可以分解为一个矩形和两个圆形。

包含

  许多开发人员选择忽略的问题之一是包含关系。当一个形状包含另一个形状时会发生什么?这个问题通常不会是一个大问题,因为大多数应用程序都不会出现这种情况。首先,让我解释一下这个问题以及如何处理它。然后我会解释为什么应该考虑。
在这里插入图片描述

图9: 包含关系

  如果一个形状包含在另一个形状中,则根据目前的伪代码,SAT将返回不正确的MTV。矢量和幅度部分可能都不正确。图9显示返回的重叠不足以将形状移出交叉点。所以我们需要做的是检查重叠测试中的包容度。修改SAT代码中的if语句:

if (!p1.overlap(p2)) {
  // then we can guarantee that the shapes do not overlap
  return false;
} else {
  // get the overlap
  double o = p1.getOverlap(p2);
  // check for containment
  if (p1.contains(p2) || p2.contains(p1)) {
    // get the overlap plus the distance from the minimum end points
    double mins = abs(p1.min - p2.min);
    double maxs = abs(p1.max - p2.max);
    // NOTE: depending on which is smaller you may need to
    // negate the separating axis!!
    if (mins < maxs) {
      o += mins;
    } else {
      o += maxs;
    }
  }
  // check for minimum
  if (o < overlap) {
    // then set this one as the smallest
    overlap = o;
    smallest = axis;
  }
}

  原因1:形状可能会存在这样的类型。不处理将需要两次或多次SAT迭代来解决是否碰撞,这取决于形状的相对大小。
  原因2:如果计划使算法支持线段与其他形状之间的碰撞检测,则必须这样做,因为在某些情况下重叠可能为零(这是因为线段是无限薄的形状)。

其他注意事项

  一些其他注意事项:

  • 平行轴可以减少测试轴的数量。例如矩形只有两个轴要测试。

  • 一些形状,如矩形,如果有自己的投影和getAxes函数,可以执行得更快,因为矩形只需要测试2个轴。

  • 最后一个分离轴可用于触发SAT的下一次迭代,使得算法在非相交情况下可以是0(1)。

  • 3D中的SAT最终可以测试很多轴。

  • 我不是专家,请原谅我糟糕的图形。

  • 5
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值