Unity中分离轴定理(SAT)在世界轴附近碰撞检测失效问题排查
问题根源与修复方案
你的问题主要来自浮点数精度处理不当和冗余/误差轴的引入,以下是具体问题和修复步骤:
1. 浮点数直接比较是否为零
直接用axis == Vector3.zero或axis != Vector3.zero判断向量是否为零是浮点数编程的典型错误——由于精度限制,几乎不会有完全等于零的浮点向量,这会导致无效轴被加入测试,或者有效轴被错误跳过。
2. 微小叉积轴的归一化误差
当OBB轴与AABB轴接近平行时,叉积结果是模长极小的向量,直接归一化会引入巨大的数值误差,这类轴属于冗余轴(因为两个包围盒在平行轴上的投影已经被OBB/AABB自身的轴测试覆盖),应该直接跳过。
3. 区间重叠判断的精度容错
直接用严格小于判断区间是否分离,会因为浮点数计算的微小误差(尤其是顶点靠近世界原点时),导致本该重叠的区间被误判为分离。
4. 顶点投影的累积误差(可选优化)
遍历8个顶点计算投影极值会引入多次浮点运算的累积误差,改用中心+半长的方式计算投影区间,不仅效率更高,精度也更稳定。
修改后的完整代码
public bool IntersectAABB(AABBObstacle aabb) { // 替换为你的OBB世界中心、半长和旋转数据 Vector3 obbCenter = this.center; Vector3 obbExtents = this.extents; Quaternion obbRotation = this.rotation; // 替换为AABB的世界中心和半长 Vector3 aabbCenter = aabb.center; Vector3 aabbExtents = aabb.extents; // 获取包围盒的轴 Vector3[] aabbAxes = new Vector3[] { Vector3.right, Vector3.up, Vector3.forward }; Vector3[] obbAxes = new Vector3[] { obbRotation * Vector3.right, obbRotation * Vector3.up, obbRotation * Vector3.forward }; List<Vector3> axesToTest = new List<Vector3>(); axesToTest.AddRange(aabbAxes); axesToTest.AddRange(obbAxes); // 生成叉积轴,过滤微小向量避免误差 const float smallVectorThreshold = 1e-12f; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { Vector3 axis = Vector3.Cross(obbAxes[i], aabbAxes[j]); float sqrMag = axis.sqrMagnitude; if (sqrMag > smallVectorThreshold) { // 手动归一化,避免normalized方法的额外开销 axesToTest.Add(axis / Mathf.Sqrt(sqrMag)); } } } // 测试所有分离轴 foreach (Vector3 axis in axesToTest) { if (IsSeparatingAxis(axis, obbCenter, obbExtents, obbAxes, aabbCenter, aabbExtents)) { return false; } } return true; } private bool IsSeparatingAxis(Vector3 axis, Vector3 obbCenter, Vector3 obbExtents, Vector3[] obbAxes, Vector3 aabbCenter, Vector3 aabbExtents) { const float smallVectorThreshold = 1e-12f; if (axis.sqrMagnitude < smallVectorThreshold) return false; // 计算OBB在轴上的投影区间(中心+半长方式,无顶点遍历) float obbCenterProj = Vector3.Dot(obbCenter, axis); float obbHalfLength = Mathf.Abs(Vector3.Dot(obbAxes[0], axis)) * obbExtents.x + Mathf.Abs(Vector3.Dot(obbAxes[1], axis)) * obbExtents.y + Mathf.Abs(Vector3.Dot(obbAxes[2], axis)) * obbExtents.z; float obbMin = obbCenterProj - obbHalfLength; float obbMax = obbCenterProj + obbHalfLength; // 计算AABB在轴上的投影区间 float aabbCenterProj = Vector3.Dot(aabbCenter, axis); float aabbHalfLength = Mathf.Abs(Vector3.Dot(Vector3.right, axis)) * aabbExtents.x + Mathf.Abs(Vector3.Dot(Vector3.up, axis)) * aabbExtents.y + Mathf.Abs(Vector3.Dot(Vector3.forward, axis)) * aabbExtents.z; float aabbMin = aabbCenterProj - aabbHalfLength; float aabbMax = aabbCenterProj + aabbHalfLength; // 带容差的分离判断,处理浮点数精度误差 const float tolerance = 1e-6f; return obbMax + tolerance < aabbMin || aabbMax + tolerance < obbMin; }
关键修改说明
- 替换零向量判断:用
sqrMagnitude替代直接等于比较,阈值设为1e-12f(适配单精度浮点数的精度范围)。 - 过滤微小叉积轴:仅保留模长足够大的叉积轴,避免归一化引入的误差。
- 中心+半长计算投影:彻底避免顶点遍历的累积误差,尤其在顶点靠近世界原点时效果明显。
- 容差化区间判断:加入
1e-6f的容差,允许微小的计算误差,避免误判分离。
内容的提问来源于stack exchange,提问作者Thibaut Lagoda
相关产品推荐
相关产品推荐

