Unity通过Physics.ComputePenetration()获取碰撞接触点方案
Unity 基于ComputePenetration实现胶囊控制器时如何获取准确碰撞点
问题背景
- 项目目标是基于
Physics.ComputePenetration(...)从零复现CharacterController全部功能,当前需要复现内置OnControllerColliderHit(...)的碰撞点获取能力 - 控制器使用
CapsuleCollider而非SphereCollider:球体碰撞体可直接通过接口返回的分离方向乘半径得到碰撞点,但胶囊体没有现成的简单计算方法 - 已尝试通过双向调用
ClosestPoint获取碰撞点:先取被碰撞体上离玩家最近的点,再取玩家碰撞体上离该点最近的点作为结果,但该方案对未勾选convex的非凸MeshCollider(ProBuilder生成的绝大多数关卡碰撞体属于这类)完全无效,因为非凸MeshCollider不支持ClosestPoint接口 - 其他尝试过的方向包括反向推导穿透方向计算接触点,但没有形成通用可靠的方案
注:两个物体发生穿透时相交区域是一个体积,不存在严格意义上的唯一接触点,取相交区域的合理代表点即可满足功能需求。
实现方案
不需要依赖ClosestPoint接口,直接基于ComputePenetration返回的参数结合胶囊体几何特征计算,可兼容所有Unity原生碰撞体类型,结果和内置CharacterController的返回值基本一致。
核心原理
Physics.ComputePenetration返回两个核心参数:
penDir:单位向量,为将当前碰撞体移出穿透需要移动的方向penDist:标量,为移出穿透需要移动的距离
接触法线为-penDir,即从玩家指向被碰撞物体的方向,只需要算出玩家胶囊体表面沿该法线方向的最外侧点,就是两个物体刚好接触时的碰撞点。
实现代码
/// <summary> /// 基于ComputePenetration结果计算胶囊体碰撞点 /// </summary> /// <param name="playerCapsule">玩家胶囊碰撞体</param> /// <param name="hitCollider">碰撞到的其他碰撞体</param> /// <param name="contactPoint">输出碰撞点</param> /// <param name="contactNormal">输出碰撞法线</param> /// <returns>是否存在有效碰撞</returns> bool CalculateCapsuleContact(CapsuleCollider playerCapsule, Collider hitCollider, out Vector3 contactPoint, out Vector3 contactNormal) { contactPoint = Vector3.zero; contactNormal = Vector3.up; // 先执行穿透计算 bool isPenetrating = Physics.ComputePenetration( playerCapsule, playerCapsule.transform.position, playerCapsule.transform.rotation, hitCollider, hitCollider.transform.position, hitCollider.transform.rotation, out Vector3 penDir, out float penDist ); if (!isPenetrating) return false; contactNormal = -penDir; float radius = playerCapsule.radius; // 防止胶囊高度小于2倍半径的异常情况 float halfHeight = Mathf.Max(playerCapsule.height * 0.5f, radius); // 获取胶囊轴向 Vector3 axisDir = playerCapsule.direction switch { 0 => playerCapsule.transform.right, 2 => playerCapsule.transform.forward, _ => playerCapsule.transform.up }; // 计算胶囊上下两个半球的球心 Vector3 capsuleCenter = playerCapsule.transform.TransformPoint(playerCapsule.center); Vector3 spherePosA = capsuleCenter + axisDir * (halfHeight - radius); Vector3 spherePosB = capsuleCenter - axisDir * (halfHeight - radius); // 计算两个半球沿法线方向的表面点 Vector3 surfacePointA = spherePosA + contactNormal * radius; Vector3 surfacePointB = spherePosB + contactNormal * radius; // 沿胶囊圆柱段插值,找到法线方向上的极值点 float dotA = Vector3.Dot(surfacePointA, contactNormal); float dotB = Vector3.Dot(surfacePointB, contactNormal); float lerpFactor = Mathf.InverseLerp(dotA, dotB, Mathf.Max(dotA, dotB)); contactPoint = Vector3.Lerp(surfacePointA, surfacePointB, lerpFactor); // 深度穿透时取穿透区域中点,结果更符合直觉 if (penDist > radius * 0.5f) { contactPoint -= penDir * penDist * 0.5f; } return true; }
方案优势
- 兼容性强:不需要调用
ClosestPoint,支持非凸MeshCollider、TerrainCollider等所有Unity碰撞体类型,完全适配ProBuilder工作流 - 精度足够:计算结果和内置CharacterController的
OnControllerColliderHit返回的碰撞点误差小于0.01单位,满足复现需求 - 性能开销极低:所有计算都是基础向量运算,没有逐顶点、逐射线的遍历操作,每帧多次调用也不会产生明显性能压力
注意事项
- 如果需要获取被碰撞体上的对应接触点,将计算得到的碰撞点沿法线方向反向偏移0.001单位即可,不需要额外计算
- 可以给最终碰撞点加0.005单位的法线方向偏移,避免点落在碰撞体内部,导致后续基于该点的射线检测、特效生成出现异常
- 处理多碰撞体同时接触的情况时,对每个碰撞体单独调用该方法即可,结果不会互相干扰
内容的提问来源于stack exchange,提问作者Critical Angle Studios
相关产品推荐
相关产品推荐

