You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

3D OBB碰撞检测实现求助:支持旋转并返回法向量

带旋转OBB碰撞检测的实现问题

我正在制作一款带碰撞检测功能的小型3D渲染程序,需要实现支持旋转的OBB(有向包围盒)碰撞检测,且要求检测结果返回可用于物理计算的法向量。

我试过两种方法:

  • 第一种:检测一个盒子的所有顶点是否在另一个盒子内部,但旋转场景下无法始终生效。
  • 第二种:提取立方体的12条线段,计算线段上到目标盒子中心的最近点,再判断该点是否处于盒子内部。理论可行,但测试结果不稳定,不确定是代码问题还是方法本身的问题。

当前碰撞检测代码

var ob1Rot = ob1.rotation;

Matrix4x4 m1rot = Matrix4x4.CreateFromYawPitchRoll(ob1Rot.Y, ob1Rot.X, ob1Rot.Z);
Matrix4x4.Invert(m1rot, out m1rot);
Matrix4x4 m1pos = Matrix4x4.CreateWorld(ob1.position, Vector3.UnitZ, Vector3.UnitY);
for (int i = 0; i < collidingLines.Length; i += 2)
{
    var center = mainCol.position;

    var point = GetClosestPointOnLineSegment(points[collidingLines[i]], points[collidingLines[i + 1]], center);

    var p = Vector3.Transform(Vector3.Transform(point, m1pos),m1rot);

    if (Math.Abs(p.X) < mainCol.size.X)
    {
        if (Math.Abs(p.Y) < mainCol.size.Y)
        {
            if (Math.Abs(p.Z) < mainCol.size.Z)
            {
                touched = true;

                var facePoints = new List<Vector3>();
                var normal = new Vector3();
                var closest = Vector3.Zero;
                for (int c = 0; c < defaultColliderFaces.Length; c+=4)
                {
                    var middle =(
                        
                        (collider.points[defaultColliderFaces[c]] +
                        collider.points[defaultColliderFaces[c+1]] +
                        collider.points[defaultColliderFaces[c+2]] +
                        collider.points[defaultColliderFaces[c+3]] ) / 4

                        );
                    if (Vector3.Distance(middle, point) < Vector3.Distance(closest, point))
                    {
                        closest = middle;
                        normal = Vector3.Normalize(Vector3.Cross(collider.points[defaultColliderFaces[c]], collider.points[defaultColliderFaces[c + 2]]));
                    }
                }

                Touch t;
                    t = new Touch(mainCol, collider, point, -normal);
                    if (currentTouch != -1)
                    mainCol.touchingColliders[currentTouch] = t;
                else
                    mainCol.touchingColliders.Add(t);
                //var b = new Touch(collider, mainCol, point2, normal2);
                    //b.normal =
                if (currentTouchB != -1)
                    collider.touchingColliders[currentTouchB] = t;
                else
                    collider.touchingColliders.Add(t);


                break;
            }
        }
    }
}

相关类与数组说明

  • defaultColliderFaces:整数数组,对应立方体顶点索引,用于构建6个面
int[] defaultColliderFaces =
{
    0,1,2,3,
    0,3,7,4,
    0,4,5,1,
    5,1,2,6,
    5,4,7,6,
    7,6,2,3
};
  • points:Vector3数组,存储立方体的8个顶点坐标
points = new Vector3[]
{
    new Vector3(size.X, size.Y, size.Z) ,
    new Vector3(-size.X, size.Y, size.Z) ,
    new Vector3(-size.X, size.Y, -size.Z),
    new Vector3(size.X, size.Y, -size.Z) ,
    new Vector3(size.X, -size.Y, size.Z) ,
    new Vector3(-size.X, -size.Y, size.Z),
    new Vector3(-size.X, -size.Y, -size.Z),
    new Vector3(size.X, -size.Y, -size.Z)
};
  • collidingLines:整数数组,对应立方体的12条线段顶点索引
int[] collidingLines =
{
    0,1,
    1,2,
    2,3,
    3,0,

    0,4,
    3,7,
    2,6,
    1,5,

    4,5,
    5,6,
    6,7,
    7,4
};
  • mainCol:对应ob1的碰撞器类实例
  • collider:对应ob2的碰撞器类实例
  • Touch类:用于存储两个碰撞器实例、碰撞点及法向量的类

当前方法的问题

  1. 检测逻辑不完整:仅检测线段到对方中心的最近点,无法覆盖所有OBB碰撞场景,比如边缘交叉但中心不在线段最近点范围内的情况会漏检。
  2. 法向量计算错误:通过面中心距离选最近面,再用两个顶点叉乘求法线,无法保证法线方向正确,且棱/顶点碰撞时法线完全不准确。
  3. 坐标变换顺序错误:代码中先平移再逆旋转的变换逻辑不符合局部坐标系转换规则,应该先逆旋转再逆平移。

标准OBB碰撞检测实现(分离轴定理)

OBB碰撞检测的标准方案是分离轴定理(SAT):若存在一个轴,两个OBB在该轴上的投影无重叠,则不碰撞;反之则碰撞。需要检测15条潜在分离轴:两个OBB各自的3个主轴,以及主轴两两叉乘得到的9个轴。以下是可返回法向量的实现:

public struct OBB
{
    public Vector3 Center;
    public Vector3 HalfSize; // 半尺寸:中心到面的距离
    public Matrix4x4 Rotation; // 旋转矩阵,列向量为OBB的三个主轴

    public Vector3 Right => Rotation.Right;
    public Vector3 Up => Rotation.Up;
    public Vector3 Forward => Rotation.Forward;
}

public static bool OBBvsOBB(OBB obb1, OBB obb2, out Vector3 collisionNormal, out float penetrationDepth)
{
    collisionNormal = Vector3.Zero;
    penetrationDepth = float.MaxValue;

    var axes = new List<Vector3>
    {
        // OBB1的主轴
        obb1.Right, obb1.Up, obb1.Forward,
        // OBB2的主轴
        obb2.Right, obb2.Up, obb2.Forward,
        // 主轴两两叉乘的轴
        Vector3.Cross(obb1.Right, obb2.Right),
        Vector3.Cross(obb1.Right, obb2.Up),
        Vector3.Cross(obb1.Right, obb2.Forward),
        Vector3.Cross(obb1.Up, obb2.Right),
        Vector3.Cross(obb1.Up, obb2.Up),
        Vector3.Cross(obb1.Up, obb2.Forward),
        Vector3.Cross(obb1.Forward, obb2.Right),
        Vector3.Cross(obb1.Forward, obb2.Up),
        Vector3.Cross(obb1.Forward, obb2.Forward)
    };

    foreach (var axis in axes)
    {
        if (axis.magnitude < 0.0001f) continue; // 跳过零向量轴
        
        var normalizedAxis = Vector3.Normalize(axis);
        
        // 计算投影区间
        ProjectOBB(obb1, normalizedAxis, out float min1, out float max1);
        ProjectOBB(obb2, normalizedAxis, out float min2, out float max2);

        // 检查分离
        if (max1 < min2 || max2 < min1) return false;

        // 记录最小重叠的轴(碰撞法线)
        float overlap = Math.Min(max1, max2) - Math.Max(min1, min2);
        if (overlap < penetrationDepth)
        {
            penetrationDepth = overlap;
            collisionNormal = normalizedAxis;
        }
    }

    // 调整法线方向,指向obb1到obb2的方向
    var delta = obb2.Center - obb1.Center;
    if (Vector3.Dot(delta, collisionNormal) < 0)
        collisionNormal = -collisionNormal;

    return true;
}

private static void ProjectOBB(OBB obb, Vector3 axis, out float min, out float max)
{
    float extent = Math.Abs(Vector3.Dot(obb.Right, axis) * obb.HalfSize.X) +
                   Math.Abs(Vector3.Dot(obb.Up, axis) * obb.HalfSize.Y) +
                   Math.Abs(Vector3.Dot(obb.Forward, axis) * obb.HalfSize.Z);
    float centerProj = Vector3.Dot(obb.Center, axis);
    min = centerProj - extent;
    max = centerProj + extent;
}

使用说明

  1. 将现有碰撞器转换为OBB结构体:
    • Center设为盒子世界空间中心
    • HalfSize设为原尺寸的一半(如原size.X需除以2)
    • Rotation设为盒子的世界空间旋转矩阵
  2. 调用OBBvsOBB方法,返回true时,collisionNormal为碰撞法线(指向被碰撞物体),penetrationDepth为穿透深度,可直接用于物理响应(如推离物体)。

内容的提问来源于stack exchange,提问作者Zac

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 14:49:51