SAT(分离轴定理)
翻译自: SAT(Separating Axis Theorem)
2010年1月1日发表
这是我一直想写的一篇文章,但我一直没有抽出时间来做。首先我要说的是,网络上有大量关于这种特殊碰撞检测算法的资料。但是,在解释一些实现细节时,这些资源通常是模糊的(针对我们的目的)。
我计划解释算法,并填补我自己实现时的一些空白。
该教程具有交互式flash示例,可供参考。
引言
分离轴定理,简称SAT,是一种确定两个凸多边形是否相交的方法。该算法还可用于寻找最小穿透矢量,这对于物理仿真和许多其他应用是有用的。SAT是一种快速通用算法,无需对每一对形状类型进行碰撞检测,从而降低代码量和对代码的维护。
凸特性
如前所述,SAT是一种确定两个凸多边形是否相交的方法。如果某个形状与任何穿过该形状的直线只交叉两次,则该形状被称为凸多边形。如果某个形状与穿过该形状的直线交叉两次以上,则该形状为非凸(或凹)。更正式的定义参见维基的定义和数学世界的定义。让我们来看一些例子:
第一个形状被认为是凸的,因为不存在一条可以通过该形状绘制的线,该线将交叉两次以上。第二种形状是非凸的,因为存在一条相交两次以上的线。
SAT只能处理凸多边形,不过,非凸形状可以由凸形状的组合来表示(凸分解)。因此,如果我们需要采用图2中的非凸形状时,可以执行凸分解,这样便得到了两个凸形状。这样我们可以测试每个凸面形状,以确定整个形状的碰撞。
投影
SAT使用的下一个概念是投影。假设你有一个光源,它的光线都是平行的。如果您将灯光照射到对象上,它将在平面上形成阴影。该阴影是三维物体的二维投影。二维物体的投影是一个一维“阴影”。
算法
SAT指出:“如果两个凸面物体没有穿透,则存在一根轴,使得这两个物体在该轴上的投影不重叠。”
无交叉
首先,让我们讨论SAT如何确定两个形状不相交。图5中的两个形状不相交。可以通过在它们之间画一条线来说明。
做辅助线垂直于图5中分隔两个形状的线,并将形状投影到该辅助线上,则它们的投影不重叠。形状的投影(阴影)不重叠的辅助线称为分离轴。在图6中,深灰色线是分离轴,相应的彩色线是形状在分离轴上的投影,其中,投影不重叠,因此根据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;
}
}
相交
如果对于所有轴,形状的投影都重叠,则我们可以得出形状相交的结论。图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;
获取分离轴
实现该算法时的第一个问题是:如何知道要测试哪些轴?事实上,这非常简单:
只要测试每个多边形的边的法线即可
边的法线可以通过翻转坐标并取反来获得。例如:
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;
将多边形投影到轴
另一个问题是如何将形状投影到轴上。将多边形投影到轴上相对简单;在所有顶点上循环执行与轴的点积,并存储最小值和最大值即可。
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区域,该文中将不会讨论。
其他曲线形状将会是更大的问题,必须以自己的处理方式。例如,胶囊形状可以分解为一个矩形和两个圆形。
包含
许多开发人员选择忽略的问题之一是包含关系。当一个形状包含另一个形状时会发生什么?这个问题通常不会是一个大问题,因为大多数应用程序都不会出现这种情况。首先,让我解释一下这个问题以及如何处理它。然后我会解释为什么应该考虑。
如果一个形状包含在另一个形状中,则根据目前的伪代码,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最终可以测试很多轴。
-
我不是专家,请原谅我糟糕的图形。