分离轴理论,简称SAT(SeparatingAxisTheorem),是一个判断两个凸多边形是否碰撞的理论。此理论可以用于找到最小的渗透向量(感觉应该是模最小的),此向量在物理模拟和其他很多应用中很有用。SAT是一种高效的算法,能够出去每种形状对(譬如圆和圆,圆和多边形,多边形和线段)对碰撞检测代码的需求从而减少代码减轻维护压力。
SAT 就像以前说过的一样,是一个检测两个凸多边形是否相交的方法。对于一个形状,如果对于所有的穿过这个形状的线,这条线只和这个形状至多相交两次。如果多余两次,这个形状就是一个非凸多边形或者说凹多边形。看维基百科的定义 和数学世界的定义来了解更多的数学和官方定义。让我们来看一些例子
上图形被看做凸多边形因为不存在一条穿过它而且和他相交两次的直线。
这一个存在,所以其实一个凹多边形。
SAT只能处理凸多边形,不多问题不大,因为非凸多边形可以被分解为凸多边形,如上图示。所以如果我们将图示2中的凹多边形分解我们可以得到两个凸多边形。然后我们就可以使用SAT对整个形状进行检测了。
SAT使用的下一个概念叫做投影。想象一下如果你有一个全是平行光的光源。如果你将这个光投向一个物体它就会在一个平面上产生一个阴影。一个阴影是一个三维(3d)物体的二维(2d)的投影。一个二维物体的投影是一个一维“阴影”。具体如下图示
SAT描述为:如果两个凸多边形没有相交,那么存在这两个物体在一个轴上的投影不重叠。
首先让我们讨论一下SAT如何判断两个形状没有相交。在下图中我们可以看出这两个形状没有相交。可以划出一条线来展示出来。
如果我们选择一条和图示中分割线垂直的线,将这两个凸多边形相对于这条垂线投影就会看出,投影没有重叠。那么这条垂线就被称作一条分离轴。在下图中深灰色的线既是一个分离轴,而且各自对应颜色的线就是这两个多边形想这条分离轴的投影。注意:在下图中投影没有重叠,所以根据SAT我们可以得出结论这两个多边形没有相交。
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;
}
}
如果对于所有的轴,两个多边形的投影都相交,那么我们就可以得出结论这两个多边形相交。下图展示出了这种情况。
所有的轴必须都在考虑范围之内,跟改后的伪代码为:
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;
当我使用这个算法的时候第一个问题就是,我怎么知道去测试哪些轴。其实这个东西很简单:
你要测试的轴就是每个边的法向量。边的法向量可以通过对换x,y然后对其中的一个取负就行了。
举例说明
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) => (-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;
将形状A向一个轴投影:
另一个没有很清晰的事情就是如何将一个图形向一个轴投影。将一个图形向一个轴投影其实是一个很简单的事情。循环所有的顶点执行和待测试轴的点乘,存储下最小值和最大值。
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;
到目前为止我们只能判断这两个形状是否相交。除此之外,SAT可以返回一个MTV(Minimum Translation Vector 最小分离向量)。MTV是一个最小量级的将两个相交的图形分离的向量。如果我们回顾一下图示7就会发现 轴C 有着最小的投影重叠。那个轴和那个重叠就是MTV,那个轴指示出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提出了挑战,因为曲边图形有着无限多的待测试轴.解决这种问题的通常办法就是将圆形和圆形,圆形和多边形分解,通过做一些特殊的操作.另一个可选择的操作不在所有的情况下都是用曲边图形,而是用多定点的多边形取代她.第二中选择方案对我们之前写的伪代码不会造成影响,但是我在这里想介绍第一种方法.
让我们首先看一下圆形和圆形,通常情况下,你会做如下的事情:
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
多边形和圆形会带来更多的问题, 图心和图心在多边形的待测试轴上的测试并不奏效,事实上会得到意想不到的错误结果.在这种情况下,你必须要包含另一个轴:那个从距离圆形最近的顶点到圆心的轴.多边形上最近的顶点的求法有很多种,理想的解决办法是Voronoi区域算法,但是不会在这篇文章中涉及.
其他的曲边图形会带来更多的问题,必须要使用各自特定的解决办法.例如一个胶囊形状可以被分解为一个矩形和两个半圆.
一个很多开发者选择忽略的问题就是包含.一个图形包含另一个图形会发生什么么?这个问题并不会带了巨大的影响因为绝大多数的应用都不会让这种情况发生.首先让我介绍一下这个问题然后是如何解决它.然后我会介绍为什么我们要将他置于考虑范围之内.
如果一个图形被另一个图形包含,在我们现有的伪代码中,SAT会返回不对的MTV.方向和量级都有可能不对.下图展示了在轴上的投影的重叠不能够将两个图形分离开来.
所以我们需要做的是在重叠测试中检测有没有包含.在上面的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;
}
}
将包含加入其中的原因:
游戏中给两个如此定义的形状是有可能的.不解决这个问题会要求我们根据两个形状的大小进行两次或者更多的SAT循环来解决这种特殊的碰撞.
如果你准备支持其他形状的线段分割,你有必要这样做,因为在一些情况下投影的重叠值可能为零,这是因为一个线段的分割是一个极小的图形这个事实(译者注:这些东西后面的翻译中会有).