首页/文章/ 详情

有限元基础知识:接触检测中的分离轴定理SAT

10小时前浏览6

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<doubledoubleproject(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<doubledouble>& a, const std::pair<doubledouble>& 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// 所有轴投影重叠,相交
}

代码说明

  1. Point2D:定义二维向量运算(加减、点积、归一化), 包括定义2D点坐标。
  2. getNormal函数:通过边向量计算法向量(顺时针旋转90度),2D情况一条线的法向向量可以轻松得到,都不用算。
  3. project函数:计算多边形在轴上的投影范围。
  4. satCollision函数:遍历所有分离轴,判断是否存在不重叠的投影。
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算法,如果大家后续有兴趣也可以分享一下,非常的有趣。

来源:大狗子说数值模拟
碰撞通用
著作权归作者所有,欢迎分享,未经许可,不得转载
首次发布时间:2025-06-07
最近编辑:10小时前
大狗子说数值模拟
博士 传播国际一流的数值模拟算法
获赞 10粉丝 17文章 60课程 0
点赞
收藏
作者推荐

有限元基础知识:AABB包围盒的计算

上次说了接触计算中的几种不同的包围盒子,有限元基础知识:接触计算中的包围盒子下面开始分别讲一下各自的编码实现,先从最简单的AABB开始,AABB包围盒由于每个面(3D)都与坐标系平行,所以其实只需要找到,x,y,z 三个方向上所有节点的最大值和最小值就行了。下边以C++代码为例,给大家示范一下AABB的计算流程// 函数定义using point3D = std::array&lt;double, 3&gt;;struct AABB{ point3D min; point3D max; void init(); void addPoint(const point3D &amp; p)};void AABB::init(){ min[0] = min[1] = min[2] = FLT_MAX; max[0] = max[1] = max[2] = -FLT_MAX;};void addPoint(const point3D &amp; p){ if (p[0] &lt; min[0]) min[0] = p[0]; if (p[0] &gt; max[0]) max[0] = p[0]; if (p[1] &lt; min[1]) min[1] = p[1]; if (p[1] &gt; max[1]) max[1] = p[1]; if (p[2] &lt; min[2]) min[2] = p[2]; if (p[2] &gt; max[2]) max[2] = p[2];};// 真正的形成一个AABBAABB box;box.init();// 对于主面或者从面的节点循环// points is a vector of point3Dfor (auto p : points){ box.addPoint(p);} 那么这样一个主面或者从面的包围盒子就形成了,然而在真实的有限元计算中,由于我们需要定位潜在可能发生的接触,或者接触已经发生了,已经有了微小的穿透,以上还需要进行一点点补充,就是增加一些buffer,给现在的box扩大一圈,一般的做法如下:void AABB::addBuffer(double dis){ min[0] -= dis; min[1] -= dis; min[2] -= dis; max[0] += dis; max[1] += dis; max[2] += dis;};box.addBuffer(dis); 这样我们就完成了一个扩大化的bounding box, 如下图所示,蓝色的就是最开始计算的box,而红色虚线就是加上了buffer的box 那么然后就是如何检测两个bounding box 是否接触了,这个也非常简单,若所有轴均重叠则碰撞,否则就不bool AABBIntersect(const AABB &amp; box1, const AABB &amp; box2){ return (box1.min[0] &lt;= box2.max[0] &amp;&amp; box1.max[0] &gt;= box2.min[0]) &amp;&amp; (box1.min[1] &lt;= box2.max[1] &amp;&amp; box1.max[1] &gt;= box2.min[1]) &amp;&amp; (box1.min[2] &lt;= box2.max[2] &amp;&amp; box.max[2] &gt;= box2.min[2]);} 这样我们就能检测两个AABB的box是否接触,做一个粗放式的检查。好了今天就先到这,明天讲一下OBB与分离轴定理SAT的编码实现,敬请期待。来源:大狗子说数值模拟

未登录
还没有评论
课程
培训
服务
行家
VIP会员 学习计划 福利任务
下载APP
联系我们
帮助与反馈