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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.01 01:30:57