SAT原理的基本原理其实非常的简单与清晰,其核心就是说:如果有一条轴可以将两个凸多边形(多面体)分开,那么这两个物体就没有接触。多么清晰,具体证明这里由于并不是讲数学,就不说了,大家就默认有这样一条定理就好了。然而大家用眼睛看,就非常容易判断出来有一条轴能轻松的分开两个物体,可用计算机怎么算呢,这就是我们今天要说的。
上次讲到OBB及其计算,比如在2维空间下,我们找到了两个物体的OBB,我们想判断其是否接触,这个过程并不像AABB检测接触那样直截了当,我们看OBB都是凸的,那么我们就用SAT进行计算。
然而空间中,其实有无数条轴,我们在计算机角度怎么看出来有没有呢,毕竟计算机没有眼睛(其实我一直好奇我们的眼睛是咋能看出来的,内部逻辑是啥)。科学家们就得出了一个结论,其实我们并不需要检测所有的轴向,对于上述的两个4边形,其实我们只需要各自检测每条边的法向方向,总共也就是8个方向,检查所有节点在各个轴方向上的投影,检查两个物体的投影是否发生重叠,因为每个方向都需要检测每个点,所以复杂度大概就是 。然而这里其实有个简化因为对于二维的OBB,每个对边的法向方向都是在一条轴上的,所以其实只需要检查两个OBB的各自2个面的法向就OK了,所以共四条轴,每条轴检查每个顶点,所以就是做 次检查,下面就是这种情况的C++代码
#include <vector>
#include <cmath>
#include <algorithm>
struct Point2D {
double x, y;
Vector2 operator-(const Point2D& other) const {
return {x - other.x, y - other.y};
}
Vector2 operator+(const Point2D& other) const {
return {x + other.x, y + other.y};
}
Vector2 operator*(double scalar) const {
return {x * scalar, y * scalar};
}
double dot(const Point2D& other) const {
return x * other.x + y * other.y;
}
Point2D normalize() const {
double len = std::sqrt(x * x + y * y);
return {x / len, y / len};
}
};
// 计算边的法向量(分离轴)
Point2D getNormal(const Point2D& edge) {
return {-edge.y, edge.x}.normalize(); // 顺时针旋转90度得到法向量
}
// 计算物体在轴上的投影范围
std::pair<double, double> project(const std::vector<Point2D>& vertices, const Point2D& axis) {
double min = FLOAT_MAX;
double max = -FLOAT_MAX;
for (const auto& v : vertices) {
double p = v.dot(axis);
if (p < min) min = p;
if (p > max) max = p;
}
return {min, max};
}
// 检测投影是否重叠
bool overlap(const std::pair<double, double>& a, const std::pair<double, double>& b) {
return (a.second >= b.first && b.second >= a.first);
}
// 2D SAT碰撞检测
bool satCollision(const std::vector<Point2D>& polyA, const std::vector<Point2D>& polyB) {
// 获取所有边的法向量作为分离轴
std::vector<Point2D> axes;
const int size = 2;
for (size_t i = 0; i < size; ++i) {
Vector2 edge = polyA[i+1] - polyA[0];
axes.push_back(getNormal(edge));
}
for (size_t i = 0; i < size; ++i) {
Vector2 edge = polyB[(i+1)] - polyB[0];
axes.push_back(getNormal(edge));
}
// 检查所有分离轴
for (const auto& axis : axes) {
auto projA = project(polyA, axis);
auto projB = project(polyB, axis);
if (!overlap(projA, projB)) {
return false; // 存在分离轴,不相交
}
}
return true; // 所有轴投影重叠,相交
}
代码说明:
const int size = 2;
for (size_t i = 0; i < size; ++i) {
Vector2 edge = polyA[i+1] - polyA[0];
axes.push_back(getNormal(edge));
}
这里可以看到我们针对OBB的情况进行了特化,上边这段代码使得每个OBB只计算两条边的法向,而不是像通用SAT的情况下计算所有边的法向,如果想改为通用的方式也很简单,只需要改为如下的这段代码就可以了,这样对于一个任意凸四面体则就是计算了4条边的法向方向作为分离轴进行后续判断。
const int size = polyA.size();
for (size_t i = 0; i < size; ++i) {
Vector2 edge = polyA[(i+1)%size] - polyA[i];
axes.push_back(getNormal(edge));
}
总的来说代码逻辑也十分简单,就是先获得所有需要检测的分离轴,存在容器axes
里面,后面再对每个四边形的所有顶点都进行向这个轴投影,如果最大最小区域在某条轴没有overlap了,则说明分离轴存在,物体不相交,比如如下图所示的情况:
这样一条轴上,A、B两者的投影范围没有相交,其实就可以判定其物体并不发生碰撞了,也就不用继续后面的接触计算了。
好了到这里就说完了2D分离轴定理下OBB接触检测的基本算法流程,这里还有很多没有详细说明,比如为何检查这些法向方向就够了,大家可以参考计算图形学或者数学相关的书,这部分都会有严格的证明,本着大家能用为主,这里就不再详述。
对于3D情况会更加复杂一些,不过原理也是类似的,但现在需要检查的轴变多了一些:需检查两个OBB的面法线(各3个)及边之间的叉乘方向(共9个),总计15个轴,计算繁琐很多。
我们无论在接触搜索中采用OBB还是k-Dops亦或是convex hull其实都是凸的,其实对于凸的物体的多边形还有很多算法,比如大名鼎鼎的GJK算法,如果大家后续有兴趣也可以分享一下,非常的有趣。