分离轴定理(SAT)实现三维物体碰撞检测

本文详细介绍了如何将二维的SAT分离轴理论扩展到三维空间,通过计算AABB包围盒在分离轴上的投影判断物体碰撞,包括处理基本图元如球、圆柱和长方体,并提到了后续可能用到的GJK算法优化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

       在做三维运动防碰撞项目中需要用到SAT算法,但是查阅资料发现很少有讲明白三维SAT应用的。于是打算自己记录一下实现过程。

一.  简介

        分离轴理论,简称SAT(SeparatingAxisTheorem),通过判断任意两个凸多边形在任意角度下的投影是否均存在重叠,来判断是否发生碰撞。即两个不相交的多边形一定能找到一条轴,它们在这条轴上的投影不相交,而如果找不到这样的一条轴,那说明这两个物体相交。这就是分离轴定理的核心思想。

        注意:分离轴定理只适合凸多边形(体),所以如果是凹多边形(体)的话需要转换成多个凸多边形(体)。可以参考:二维多边形分解为凸块的库

二. SAT在二维的应用

        二维的SAT应用比较具象,理解起来更为容易,适合新手学习,建议新学者可以先看这篇文章 常见的2D碰撞检测 中的分离轴定理部分。

三. SAT在三维的应用

        在理解了SAT在二维中的应用后,尝试将其扩展到三维空间中对物体进行碰撞检测。在这过程中也遇到了不少坑,分享一下思路与部分代码。

        首先介绍一下模型的基本信息:模型中包含多个组件,每个组件有一个相对于绝对坐标系下的位姿矩阵。组件中又包含多个图元,每个图元有一个相对于组件坐标系下的位姿矩阵。

        部分数据结构信息如下:

//位姿信息
typedef struct _Matrix4x3 {
    union {
        float poseMatrix[12];    //表示4*3的矩阵
        struct {
            Matrix3x3 m;         //表示poseMatrix中的前9位,是一个3*3的旋转矩阵
            Point center;        //表示poseMatrix中的后3位,是体心坐标
        };
    };
}Matrix4x3; 

//结构体:AABB包围盒
typedef struct _Aabb{ 
    Point aabbMin;    //Aabb包围盒的xyz方向上的最小值构成的坐标点 
    Point aabbMax;    //Aabb包围盒的xyz方向上的最大值构成的坐标点 
}Aabb;

//结构体:球
typedef struct _Sphere { 
    float radius;    //球的半径 
}Sphere;


//结构体:圆柱
typedef struct _Cylinder { 
    float radius;   //底面圆的半径 
    float h;        //圆柱的高
}Cylinder;

//结构体:长方体
typedef struct _Cube {
    float length;    //长
    float width;     //宽
    float height;    //高
}Cube;
 
1. 创建组件坐标系下的AABB包围盒

        已知图元位姿矩阵与基本参数后,对于基本图元(长方体、圆柱、球等)创建AABB包围盒,分三种情况处理: 

Bit32 CalculateAabb(Primitive* primitive, Aabb* aabb) {
    //根据图元类型进行分流
    if (primitive->type == SPHERE_TYPE){ //球
        return CalculSphereAabb(&primitive->sphere, primitive->poseMatrix, aabb); 
    }
    else if (primitive->type == CYLINDER_TYPE){ //圆柱
        return CalculCylinderAabb(&primitive->cylinder, primitive->poseMatrix, aabb); 
    }
    else if (primitive->type == CUBE_TYPE){//长方体
        return CalculCubeAabb(&primitive->cube, primitive->poseMatrix, aabb); 
    }
    return -1;
}


        计算长方体与球的AABB盒都十分简单,只有在计算圆柱时稍微复杂一点,需要在图元坐标系中计算出包围盒顶点坐标,再将8个顶点转换到组件坐标系下,找出最大最小值,更新包围盒顶点坐标。已知圆柱的位姿矩阵为:R =  \begin{bmatrix} m00,m01,m02,centerX \\ m10,m11,m12,centerY \\ m20,m21,m22,centerZ \end{bmatrix}

Bit32 CalculCylinderAabb(Cylinder* cylinder, float* poseMatrix,Aabb* aabb){ 
    Point box[8] = {0}, vertices[8] = {0};    //圆柱在图元坐标系下的AABB盒8个顶点   
    int i = 0, j = 0, k = 0;
    int index = 0;

    //计算8个顶点坐标
    for (i = 0; i < 2; i++) { 
        for (j = 0; j < 2; j++) { 
            for (k = 0; k < 2; k++) {
                box[index].x = centerX + sign(i) * cylinder->radius; 
                box[index].y = centerY + sign(j) * cylinder->radius; 
                box[index].z = centerZ + sign(k) * cylinder->h; 
                index++;
            }
        }
    }

//遍历box的8个顶点,对点坐标进行转换成组件坐标系下的坐标之后,找出其中所有转换点的最大、最小值
    for (i = 0; i < 8; i++) {
        vertices[i] = MultiMatrixVector(&poseMatrix, &box[i]); 
        vertices[i] = Add(&vertices[i], &poseMatrix);

        aabb->aabbMax.x = fmax(vertices[i].x, xmax); 
        aabb->aabbMax.y = fmax(vertices[i].y, ymax); 
        aabb->aabbMax.z = fmax(vertices[i].z, zmax); 
        aabb->aabbMin.x = fmin(vertices[i].x, xmin); 
        aabb->aabbMin.y = fmin(vertices[i].y, ymin); 
        aabb->aabbMin.z = fmin(vertices[i].z, zmin); 
    }
    return 0;
}

通过计算得到基本图元AABB盒信息,该步骤是为了在后续使用SAT检测时减少计算量。

2. 确定绝对坐标系下的分离轴

如果两个包围盒不相交,那么必定存在能将它们分离开来的平面,也即必定存在满足分离条件的分离轴。判断两个包围盒是否相交,就是看是否能找到一条分离轴,使两者在其上的投影不重叠。

而怎么去找分离轴很关键,与二维不同,找三维空间中的分离轴要复杂一些。对于两个相交的由多面体,它们之间的相交方式可以归结为以下几种:面-面、面-边、边-边相交。对于上述三种相交方式,我们就可以逐条去找所有可能的分离平面:

1. 包围盒A三个互不平行的平面;

2. 包围盒B三个互不平行的平面;

3. 包围盒A中的三条互不平行边和包围盒B中的三条互不平行边两两之间的公共垂面

而分离轴是垂直于分离平面的,这样我们就可以得到两个包围盒之间潜在的分离轴可能存在于以下位置:

首先是每个包围盒平面的法向量,共3条互不平行的法向量(对包围盒来说,法向量与边平行):

const Bit32 CUBE_POINT_NUM = 8;
const Bit32 CUBE_MAX_AXIS = 3;
Point Cube1Vertices[CUBE_POINT_NUM] = {0};    //储存包围盒的8个顶点
Point Cube2Vertices[CUBE_POINT_NUM] = {0};
Point Proj_Axis[15] = {0};                    //储存3x3+3+3共15条分离轴
Bit32 vertexNum[CUBE_MAX_AXIS] = {1, 2, 4};   //顶点编号,用于求边向量
Bit32 axisCnt = 0;

//找到AABB1的3个边向量,并单位化
for (i = 0; i < CUBE_MAX_AXIS; i++) {
    Proj_Axis[axisCnt++] = Subtract(&Cube1Vertices[0], &Cube1Vertices[vertexNum[i]]); 
}

//找到AABB2的3个边向量,并单位化
for (i = 0; i < CUBE_MAX_AXIS; i++) {
    Proj_Axis[axisCnt++] = Subtract(&Cube2Vertices[0], &Cube2Vertices[vertexNum[i]]); 
}

//传入两个点计算向量
Point Subtract(Point* p1, Point* p2) 
{
    Point res = {0, 0, 0};

    if (p1 == NULL || p2 == NULL) { 
            return res;
    }

    res.x = p1->x - p2->x;
    res.y = p1->y - p2->y;
    res.z = p1->z - p2->z;
    // 向量单位化
    NormalizePoint(&res); 

    return res;
}

然后更关键的是,两个包围盒任意两条边公共垂面的法向量(共3*3=9条):

//循环计算两包围盒每条边之间公垂面的法向量
for (i = 0; i < CUBE_MAX_AXIS; i++) {

    for (j = 0; j < CUBE_MAX_AXIS; j++) {

        Proj_Axis[axisCnt] = Cross(&Proj_Axis[i], &Proj_Axis[CUBE_MAX_AXIS + j]);
        NormalizePoint (&Proj_Axis[axisCnt]);

        axisCnt++;
    }
}

//叉乘计算两向量公垂面的法向量
Point Cross(Point* p1, Point* p2) {
    Point res = {0, 0, 0};

    if (p1 == NULL || p2 == NULL) 
    { 
        return res;
    }
    res.x = p1->y * p2->z - p1->z * p2->y;
    res.y = -(p1->x * p2->z - p1->z * p2->x);
    res.z = p1->x * p2->y - p1->y * p2->x;

    return res;
}

这里使用叉乘来计算两个向量公共垂面的法向量。

3. 计算包围盒在分离轴方向上的投影

上一步得到15条分离轴,然后再计算两个包围盒分别在每条分离轴上的投影最大最小值。

这里使用向量点乘来计算包围盒每个顶点在分离轴上的投影值:

//储存计算值
float min1 = 0.0, min2 = 0.0, max1 = 0.0, max2 = 0.0, dot1 = 0.0, dot2 = 0.0;

//在每个分离轴上进行投影计算
for (i = 0; i < CUBE_MAX_AXIS * CUBE_MAX_AXIS + CUBE_MAX_AXIS * 2; i++) { 
    min1 = Dot(&Cube1Vertices[0], &Proj_Axis[i]);
    max1 = min1;

    min2 = Dot (&Cube2Vertices[0], &Proj_Axis[i]); 
    max2 = min2;
    //对两个包围盒中每个顶点进行投影
    for (j = 1; j < CUBE_POINT_NUM; j++) {
        dot1 = Dot(&Cube1Vertices[j], &Proj_Axis[i]); 
        dot2 = Dot(&Cube2Vertices[j], &Proj_Axis[i]);

        if (dot1 < min1) {min1 = dot1;}
        if (dot1 > max1) {max1 = dot1;}
        if (dot2 < min2) {min2 = dot2;}
        if (dot2 > max2) {max2 = dot2;}
    }

    //判断是否两包围盒在分离轴上投影是否相交
    if (max1 < min2 || max2 < min1) {
        return 0;    //分离轴上投影不相交,未发生碰撞
    }
    
    return 1;    //遍历所有分离轴都相交,则返回发生碰撞
}

//点乘计算投影
float Dot(Point* p1, Point* p2) { 
    if (p1 == NULL || p2 == NULL) { 
        return -1;
    }

    return p1->x*p2->x + p1->y*p2->y + p1->z*p2->z; 
}

4. 判断两物体包围盒投影是否相交

最后一步就是判断两包围盒投影是否相交,根据投影最大最小值进行判断:

//判断是否两包围盒在分离轴上投影是否相交
    if (max1 < min2 || max2 < min1) {
        return 0;    //存在分离轴,未发生碰撞
    }

四. 总结

        这样就完成了在绝对坐标系下对包围盒的碰撞检测,但由于包围盒相对图元来说存在冗余空间,包围盒发生并不代表图元一定发生碰撞,后续对三种基本图元之间进行碰撞检测需要用到GJK算法,有时间再继续完善。

SAT算法(Separating Axis Theorem,分离轴定理)是一种常用于二维和三维碰撞检测算法。它的基本思路是,如果两个物体之间不存在分离轴(即二者的投影重叠),则它们一定发生了碰撞。下面我们以二维碰撞检测为例,给出一个基于SAT算法的示例。 假设我们有两个矩形,分别用四个顶点表示为: 矩形A:A1(x1, y1), A2(x2, y1), A3(x2, y2), A4(x1, y2) 矩形B:B1(x3, y3), B2(x4, y3), B3(x4, y4), B4(x3, y4) 首先,我们需要找到所有可能成为分离轴的轴线。对于一个矩形而言,其边界上的法向量即可作为该矩形的分离轴。因此,我们需要计算出矩形A和矩形B的所有边界法向量,共计8条。具体计算方法如下: 矩形A的边界法向量: - AB1: (y2 - y1, -(x2 - x1)) - A1A4: (-(y2 - y1), x2 - x1) - A4A3: (y2 - y1, -(x2 - x1)) - A3A2: (-(y2 - y1), x2 - x1) 矩形B的边界法向量: - B1B2: (y4 - y3, -(x4 - x3)) - B2B3: (-(y4 - y3), x4 - x3) - B3B4: (y4 - y3, -(x4 - x3)) - B4B1: (-(y4 - y3), x4 - x3) 然后,我们需要将矩形A和矩形B分别投影到每个分离轴上,并计算它们在该轴上的投影重叠程度。如果存在任意一条分离轴上二者的投影不重叠,则二者一定不相交。具体计算方法如下: 对于一条分离轴(axis_x, axis_y),矩形A在该轴上的投影长度为: projA = (axis_x * x1 + axis_y * y1, axis_x * x2 + axis_y * y2, axis_x * x3 + axis_y * y3, axis_x * x4 + axis_y * y4) 即将矩形A的四个顶点坐标(x, y)带入分离轴的方程(ax + by = c)中,得到该点在该轴上的投影长度。 同理,矩形B在该轴上的投影长度为: projB = (axis_x * x3 + axis_y * y3, axis_x * x4 + axis_y * y4, axis_x * x1 + axis_y * y1, axis_x * x2 + axis_y * y2) 接下来,我们需要判断投影重叠程度。如果二者在该轴上的投影重叠,则它们在该轴上没有分离。否则,它们在该轴上分离。判断投影重叠程度的方法如下: maxA = max(projA) minA = min(projA) maxB = max(projB) minB = min(projB) 如果maxA < minB 或者 maxB < minA,则二者在该轴上分离。 最后,我们需要判断所有可能的分离轴上二者是否都有重叠。如果所有分离轴上二者都有重叠,则它们相交;否则,它们不相交。具体实现如下: def check_collision(A, B): for axis_x, axis_y in A.edges + B.edges: projA = [axis_x * x + axis_y * y for x, y in A.vertices] projB = [axis_x * x + axis_y * y for x, y in B.vertices] maxA, minA = max(projA), min(projA) maxB, minB = max(projB), min(projB) if maxA < minB or maxB < minA: return False return True 其中,A.edges和B.edges分别为矩形A和矩形B的所有边界法向量,A.vertices和B.vertices分别为矩形A和矩形B的所有顶点坐标。 以上就是一个基于SAT算法碰撞检测示例。在实际开发中,我们可以将其应用于各种物体碰撞检测,从而实现更加生动、丰富的游戏体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值