如何实现适配旋转物体的碰撞检测?
旋转物体的同步碰撞盒实现方案
你当前用的THREE.Box3是轴对齐包围盒(AABB),它的特性就是始终和世界坐标系的X/Y/Z轴平行,无论物体怎么旋转,它都会重新计算顶点的轴对齐边界,所以无法跟随物体旋转。要实现随物体同步旋转的碰撞盒,必须使用定向包围盒(OBB),核心是用**分离轴定理(SAT)**做碰撞检测。
以下是无需额外库的实现步骤:
1. 实现OBB参数提取函数
先写一个函数,从物体中提取OBB的核心参数:中心位置、三个旋转后的轴向量、半尺寸(考虑缩放):
function getOBB(obj) { // 获取物体的世界变换矩阵 const worldMatrix = obj.matrixWorld.clone(); // 提取物体世界空间的中心位置 const center = new THREE.Vector3(); worldMatrix.decompose(center, new THREE.Quaternion(), new THREE.Vector3()); // 提取旋转后的三个轴向量(已归一化) const axes = [ new THREE.Vector3(worldMatrix.elements[0], worldMatrix.elements[1], worldMatrix.elements[2]).normalize(), new THREE.Vector3(worldMatrix.elements[4], worldMatrix.elements[5], worldMatrix.elements[6]).normalize(), new THREE.Vector3(worldMatrix.elements[8], worldMatrix.elements[9], worldMatrix.elements[10]).normalize() ]; // 计算物体局部空间的半尺寸,再叠加缩放 const localBox = new THREE.Box3().setFromObject(obj); const localHalfSize = localBox.getSize(new THREE.Vector3()).multiplyScalar(0.5); const halfExtents = new THREE.Vector3( localHalfSize.x * obj.scale.x, localHalfSize.y * obj.scale.y, localHalfSize.z * obj.scale.z ); return { center, axes, halfExtents }; }
2. 实现OBB碰撞检测(分离轴定理)
用SAT检测两个OBB是否碰撞,同时计算最小分离向量(用于碰撞响应):
function checkOBBCollision(obbA, obbB) { const axes = [ // OBB A的三个轴 obbA.axes[0], obbA.axes[1], obbA.axes[2], // OBB B的三个轴 obbB.axes[0], obbB.axes[1], obbB.axes[2], // 两两轴的叉乘 obbA.axes[0].clone().cross(obbB.axes[0]), obbA.axes[0].clone().cross(obbB.axes[1]), obbA.axes[0].clone().cross(obbB.axes[2]), obbA.axes[1].clone().cross(obbB.axes[0]), obbA.axes[1].clone().cross(obbB.axes[1]), obbA.axes[1].clone().cross(obbB.axes[2]), obbA.axes[2].clone().cross(obbB.axes[0]), obbA.axes[2].clone().cross(obbB.axes[1]), obbA.axes[2].clone().cross(obbB.axes[2]) ]; let minOverlap = Infinity; let collisionAxis = null; // 遍历所有分离轴 for (const axis of axes) { // 跳过零向量轴(叉乘可能出现) if (axis.lengthSq() < 0.0001) continue; axis.normalize(); // 计算两个OBB在当前轴上的投影区间 const projA = projectOBB(obbA, axis); const projB = projectOBB(obbB, axis); // 检查是否分离 const overlap = getIntervalOverlap(projA, projB); if (overlap <= 0) { // 存在分离轴,无碰撞 return { isColliding: false }; } // 记录最小的重叠量和对应轴 if (overlap < minOverlap) { minOverlap = overlap; collisionAxis = axis; } } // 修正轴方向(确保从B指向A,方便后续响应) const delta = obbA.center.clone().sub(obbB.center); if (delta.dot(collisionAxis) < 0) { collisionAxis.multiplyScalar(-1); } return { isColliding: true, overlap: minOverlap, axis: collisionAxis }; } // 辅助函数:计算OBB在指定轴上的投影区间 function projectOBB(obb, axis) { const extent = obb.halfExtents.x * Math.abs(obb.axes[0].dot(axis)) + obb.halfExtents.y * Math.abs(obb.axes[1].dot(axis)) + obb.halfExtents.z * Math.abs(obb.axes[2].dot(axis)); const centerProj = obb.center.dot(axis); return { min: centerProj - extent, max: centerProj + extent }; } // 辅助函数:计算两个投影区间的重叠量 function getIntervalOverlap(projA, projB) { const overlapStart = Math.max(projA.min, projB.min); const overlapEnd = Math.min(projA.max, projB.max); return overlapEnd - overlapStart; }
3. 修改碰撞检测主函数
替换原来的AABB逻辑,用OBB检测并处理碰撞响应:
function checkCollisions() { // 获取玩家的OBB参数 const playerOBB = getOBB(player); const nearbyObjects = scene.children.filter(obj => obj !== player && obj.isMesh && obj.name !== "deco"); nearbyObjects.forEach(obj => { const objOBB = getOBB(obj); const collisionResult = checkOBBCollision(playerOBB, objOBB); if (collisionResult.isColliding) { // 应用碰撞位移:将玩家沿碰撞轴方向推开重叠量的80%(和你原来的系数一致) player.position.add(collisionResult.axis.clone().multiplyScalar(collisionResult.overlap * 0.8)); // 处理速度衰减:将玩家速度沿碰撞轴的分量衰减 const velocityAlongAxis = playerVelocity.dot(collisionResult.axis); if (velocityAlongAxis < 0) { playerVelocity.sub(collisionResult.axis.clone().multiplyScalar(velocityAlongAxis * (1 - 0.2))); } // 特殊处理跳跃状态(如果碰撞轴是Y轴向上,判定为踩在物体上) if (Math.abs(collisionResult.axis.y - 1) < 0.1) { isJumping = false; playerVelocity.y = Math.max(playerVelocity.y, 0); } } }); }
关键说明
- OBB的碰撞响应不再依赖世界轴的X/Y/Z,而是基于碰撞发生的实际法线轴,所以旋转后的物体碰撞会更准确。
- 如果你玩家不需要旋转,可以把玩家的OBB换成AABB,只对障碍物用OBB,这样能稍微优化性能,但统一用OBB兼容性更好。
- 代码中保留了你原来的碰撞响应系数(0.8位移、0.2速度衰减),可以根据需求调整。
内容的提问来源于stack exchange,提问作者Kashu Kutosza
相关产品推荐
相关产品推荐

