前言
SAT 是一种用于检测两个凸形状之间碰撞的方法,核心思想是:如果两个凸形状在任何可能的分离轴上投影不重叠,则这两个形状不会发生碰撞;反之,如果所有可能的分离轴上投影都重叠,则发生碰撞。
一、确定分离轴
对于矩形,分离轴可以是矩形的四条边的法线方向(即边与法线垂直的方向)。
对于圆形,可以选择从矩形的每个顶点到圆心的向量作为分离轴。
// 向量的法线(垂直方向)
Vector2 perpendicular(const Vector2& v) {
return {-v.y, v.x};
}
// 向量的单位化
Vector2 normalize(const Vector2& v) {
float length = std::sqrt(v.x * v.x + v.y * v.y);
return {v.x / length, v.y / length};
}
// 获取多边形的分离轴(法线方向)
std::vector<Vector2> getAxes(const Polygon& poly) {
std::vector<Vector2> axes;
size_t vertexCount = poly.vertices.size();
for (size_t i = 0; i < vertexCount; ++i) {
Vector2 edge = subtract(poly.vertices[(i + 1) % vertexCount], poly.vertices[i]);
axes.push_back(normalize(perpendicular(edge))); // 多边形边的法线
}
return axes;
}
二、投影到分离轴
矩形的投影: 将矩形的四个顶点沿分离轴投影,得到一个区间(即线段),这个区间表示矩形在该轴上的投影。
圆的投影: 圆的投影比较简单,它的投影区间就是圆心在分离轴上的投影点加减一个圆的半径。
// 在分离轴上对多边形进行投影
void projectPolygonOnAxis(const Polygon& poly, const Vector2& axis, float& min, float& max) {
min = max = dot(poly.vertices[0], axis);
for (const auto& vertex : poly.vertices) {
float projection = dot(vertex, axis);
if (projection < min) min = projection;
if (projection > max) max = projection;
}
}
// 在分离轴上对圆进行投影
void projectCircleOnAxis(const Circle& circle, const Vector2& axis, float& min, float& max) {
float centerProjection = dot(circle.center, axis);
min = centerProjection - circle.radius;
max = centerProjection + circle.radius;
}
三、检查投影重叠
检查矩形和圆在每个分离轴上的投影区间是否重叠。如果在某个轴上发现投影区间不重叠,则可以断定矩形和圆不发生碰撞;如果所有轴上的投影区间都重叠,则可以断定发生了碰撞。
// 检查两个投影区间是否重叠
bool checkOverlap(float minA, float maxA, float minB, float maxB) {
return !(maxA < minB || maxB < minA);
}
// 分离轴定理的核心函数,检测任意凸多边形和圆是否碰撞
bool satCirclePolygonCollision(const Circle& circle, const Polygon& poly) {
std::vector<Vector2> axes = getAxes(poly);
// 添加多边形顶点到圆心的向量作为额外的分离轴
for (const auto& vertex : poly.vertices) {
Vector2 axis = normalize(subtract(vertex, circle.center));
axes.push_back(axis);
}
// 检查每个轴上的投影
for (const auto& axis : axes) {
float minPoly, maxPoly, minCircle, maxCircle;
projectPolygonOnAxis(poly, axis, minPoly, maxPoly);
projectCircleOnAxis(circle, axis, minCircle, maxCircle);
if (!checkOverlap(minPoly, maxPoly, minCircle, maxCircle)) {
return false; // 找到一个轴没有重叠,说明没有碰撞
}
}
return true; // 所有轴都有重叠,说明发生碰撞
}
四、测试函数
#include <iostream>
#include <cmath>
#include <vector>
struct Vector2 {
float x, y;
};
struct Polygon {
std::vector<Vector2> vertices; // 多边形的顶点集合
};
struct Circle {
Vector2 center;
float radius;
};
// 向量加法
Vector2 add(const Vector2& a, const Vector2& b) {
return {a.x + b.x, a.y + b.y};
}
// 向量减法
Vector2 subtract(const Vector2& a, const Vector2& b) {
return {a.x - b.x, a.y - b.y};
}
// 向量点积
float dot(const Vector2& a, const Vector2& b) {
return a.x * b.x + a.y * b.y;
}
// 向量的法线(垂直方向)
Vector2 perpendicular(const Vector2& v) {
return {-v.y, v.x};
}
// 向量的单位化
Vector2 normalize(const Vector2& v) {
float length = std::sqrt(v.x * v.x + v.y * v.y);
return {v.x / length, v.y / length};
}
// 获取多边形的分离轴(法线方向)
std::vector<Vector2> getAxes(const Polygon& poly) {
std::vector<Vector2> axes;
size_t vertexCount = poly.vertices.size();
for (size_t i = 0; i < vertexCount; ++i) {
Vector2 edge = subtract(poly.vertices[(i + 1) % vertexCount], poly.vertices[i]);
axes.push_back(normalize(perpendicular(edge)));
}
return axes;
}
// 在分离轴上对多边形进行投影
void projectPolygonOnAxis(const Polygon& poly, const Vector2& axis, float& min, float& max) {
min = max = dot(poly.vertices[0], axis);
for (const auto& vertex : poly.vertices) {
float projection = dot(vertex, axis);
if (projection < min) min = projection;
if (projection > max) max = projection;
}
}
// 在分离轴上对圆进行投影
void projectCircleOnAxis(const Circle& circle, const Vector2& axis, float& min, float& max) {
float centerProjection = dot(circle.center, axis);
min = centerProjection - circle.radius;
max = centerProjection + circle.radius;
}
// 检查两个投影区间是否重叠
bool checkOverlap(float minA, float maxA, float minB, float maxB) {
return !(maxA < minB || maxB < minA);
}
// 分离轴定理的核心函数,检测任意凸多边形和圆是否碰撞
bool satCirclePolygonCollision(const Circle& circle, const Polygon& poly) {
std::vector<Vector2> axes = getAxes(poly);
// 添加多边形顶点到圆心的向量作为额外的分离轴
for (const auto& vertex : poly.vertices) {
Vector2 axis = normalize(subtract(vertex, circle.center));
axes.push_back(axis);
}
// 检查每个轴上的投影
for (const auto& axis : axes) {
float minPoly, maxPoly, minCircle, maxCircle;
projectPolygonOnAxis(poly, axis, minPoly, maxPoly);
projectCircleOnAxis(circle, axis, minCircle, maxCircle);
if (!checkOverlap(minPoly, maxPoly, minCircle, maxCircle)) {
return false; // 找到一个轴没有重叠,说明没有碰撞
}
}
return true; // 所有轴都有重叠,说明发生碰撞
}
int main() {
Polygon poly = {{{0, 0}, {4, 0}, {4, 3}, {0, 3}}}; // 一个四边形(矩形)
Circle circle = {{5, 1}, 1}; // 圆心在(5, 1),半径为1
if (satCirclePolygonCollision(circle, poly)) {
std::cout << "多边形和圆发生了碰撞" << std::endl;
} else {
std::cout << "多边形和圆没有发生碰撞" << std::endl;
}
return 0;
}