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类:用于存储两个碰撞器实例、碰撞点及法向量的类
当前方法的问题
- 检测逻辑不完整:仅检测线段到对方中心的最近点,无法覆盖所有OBB碰撞场景,比如边缘交叉但中心不在线段最近点范围内的情况会漏检。
- 法向量计算错误:通过面中心距离选最近面,再用两个顶点叉乘求法线,无法保证法线方向正确,且棱/顶点碰撞时法线完全不准确。
- 坐标变换顺序错误:代码中先平移再逆旋转的变换逻辑不符合局部坐标系转换规则,应该先逆旋转再逆平移。
标准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; }
使用说明
- 将现有碰撞器转换为
OBB结构体:Center设为盒子世界空间中心HalfSize设为原尺寸的一半(如原size.X需除以2)Rotation设为盒子的世界空间旋转矩阵
- 调用
OBBvsOBB方法,返回true时,collisionNormal为碰撞法线(指向被碰撞物体),penetrationDepth为穿透深度,可直接用于物理响应(如推离物体)。
内容的提问来源于stack exchange,提问作者Zac
相关产品推荐
相关产品推荐

