Final IK详细实现三——增加身体稳定性

如果捕捉对象的动作导致身体核心大幅移动,而你希望显示的3D模型小幅移动或几乎不移动,可以通过以下方法实现:


1. 核心思路

  • 固定核心位置:将角色的根骨骼(通常是髋部)固定在某个位置,避免大幅移动。
  • 动作归一化:将捕捉到的动作数据归一化到局部坐标系中,使其相对于核心位置的变化较小。
  • 限制移动范围:通过数学方法限制角色核心的移动范围,使其仅在小范围内移动。

2. 实现方法

2.1 固定核心位置

在Unity中,将角色的根骨骼(如髋部)固定在一个位置,同时将捕捉到的动作数据转换为局部坐标系。

  1. 获取核心位置

    • 使用髋部中点(MediaPipe索引23和24的平均值)作为核心位置。
    • 将核心位置固定在某个点(如原点)。
  2. 转换到局部坐标系
    • 将所有关键点坐标减去核心位置,使其相对于核心位置的变化较小。

2.2 限制移动范围

通过数学方法(如插值或限制函数)限制核心位置的移动范围,使其仅在小范围内移动。


3. 代码实现

3.1 修改IKController.cs

IKController.cs中添加核心位置固定和动作归一化的逻辑:

using UnityEngine;
using RootMotion.FinalIK;

public class IKController : MonoBehaviour
{
    public UDPReceiver udpReceiver;

    // Final IK组件
    public LimbIK leftArmIK;
    public LimbIK rightArmIK;
    public LimbIK leftLegIK;
    public LimbIK rightLegIK;

    // IK目标物体
    public Transform leftHandTarget;
    public Transform rightHandTarget;
    public Transform leftFootTarget;
    public Transform rightFootTarget;

    // 躯干和头部骨骼
    public Transform spine; // 躯干骨骼
    public Transform head;  // 头部骨骼

    // 核心位置固定
    private Vector3 coreOffset; // 核心位置偏移
    private bool isCoreInitialized = false;

    void Update()
    {
        if (udpReceiver.receivedLandmarks == null) return;

        // 初始化核心位置
        if (!isCoreInitialized)
        {
            InitializeCorePosition();
            isCoreInitialized = true;
        }

        // 更新四肢IK目标
        UpdateLimbs();

        // 更新躯干和头部
        UpdateTorso();
        UpdateHead();
    }

    void InitializeCorePosition()
    {
        // 获取初始核心位置(髋部中点)
        Vector3 leftHip = udpReceiver.receivedLandmarks[23];
        Vector3 rightHip = udpReceiver.receivedLandmarks[24];
        coreOffset = (leftHip + rightHip) / 2;
    }

    void UpdateLimbs()
    {
        // 将关键点坐标转换为局部坐标系
        Vector3 leftHandLocal = udpReceiver.receivedLandmarks[15] - coreOffset;
        Vector3 rightHandLocal = udpReceiver.receivedLandmarks[16] - coreOffset;
        Vector3 leftFootLocal = udpReceiver.receivedLandmarks[27] - coreOffset;
        Vector3 rightFootLocal = udpReceiver.receivedLandmarks[28] - coreOffset;

        // 更新IK目标位置
        leftHandTarget.position = leftHandLocal;
        rightHandTarget.position = rightHandLocal;
        leftFootTarget.position = leftFootLocal;
        rightFootTarget.position = rightFootLocal;

        // 更新IK计算
        leftArmIK.solver.Update();
        rightArmIK.solver.Update();
        leftLegIK.solver.Update();
        rightLegIK.solver.Update();
    }

    void UpdateTorso()
    {
        // 获取髋部中点(索引23和24)
        Vector3 leftHip = udpReceiver.receivedLandmarks[23] - coreOffset;
        Vector3 rightHip = udpReceiver.receivedLandmarks[24] - coreOffset;
        Vector3 hipCenter = (leftHip + rightHip) / 2;

        // 获取肩膀中点(索引11和12)
        Vector3 leftShoulder = udpReceiver.receivedLandmarks[11] - coreOffset;
        Vector3 rightShoulder = udpReceiver.receivedLandmarks[12] - coreOffset;
        Vector3 shoulderCenter = (leftShoulder + rightShoulder) / 2;

        // 计算躯干方向
        Vector3 torsoDirection = (shoulderCenter - hipCenter).normalized;
        Quaternion targetRotation = Quaternion.LookRotation(torsoDirection, Vector3.up);

        // 应用旋转到躯干骨骼
        spine.rotation = Quaternion.Slerp(spine.rotation, targetRotation, Time.deltaTime * 10);
    }

    void UpdateHead()
    {
        // 获取鼻子(索引0)和眼睛中点(索引2和5)
        Vector3 nose = udpReceiver.receivedLandmarks[0] - coreOffset;
        Vector3 leftEye = udpReceiver.receivedLandmarks[2] - coreOffset;
        Vector3 rightEye = udpReceiver.receivedLandmarks[5] - coreOffset;
        Vector3 eyeCenter = (leftEye + rightEye) / 2;

        // 计算头部方向
        Vector3 headDirection = (nose - eyeCenter).normalized;
        Quaternion targetRotation = Quaternion.LookRotation(headDirection, Vector3.up);

        // 应用旋转到头部骨骼
        head.rotation = Quaternion.Slerp(head.rotation, targetRotation, Time.deltaTime * 10);
    }
}

3.2 限制核心移动范围

如果需要进一步限制核心位置的移动范围,可以在UpdateLimbs方法中添加限制逻辑:

void UpdateLimbs()
{
    // 将关键点坐标转换为局部坐标系
    Vector3 leftHandLocal = udpReceiver.receivedLandmarks[15] - coreOffset;
    Vector3 rightHandLocal = udpReceiver.receivedLandmarks[16] - coreOffset;
    Vector3 leftFootLocal = udpReceiver.receivedLandmarks[27] - coreOffset;
    Vector3 rightFootLocal = udpReceiver.receivedLandmarks[28] - coreOffset;

    // 限制移动范围(例如:限制在±0.5单位内)
    leftHandLocal = ClampPosition(leftHandLocal, 0.5f);
    rightHandLocal = ClampPosition(rightHandLocal, 0.5f);
    leftFootLocal = ClampPosition(leftFootLocal, 0.5f);
    rightFootLocal = ClampPosition(rightFootLocal, 0.5f);

    // 更新IK目标位置
    leftHandTarget.position = leftHandLocal;
    rightHandTarget.position = rightHandLocal;
    leftFootTarget.position = leftFootLocal;
    rightFootTarget.position = rightFootLocal;

    // 更新IK计算
    leftArmIK.solver.Update();
    rightArmIK.solver.Update();
    leftLegIK.solver.Update();
    rightLegIK.solver.Update();
}

Vector3 ClampPosition(Vector3 position, float maxDistance)
{
    // 限制位置在maxDistance范围内
    return Vector3.ClampMagnitude(position, maxDistance);
}

4. 场景配置

  1. 角色根骨骼

    • 确保角色的根骨骼(通常是髋部)固定在场景中的某个位置。
    • coreOffset初始化为捕捉对象的初始核心位置。
  2. Final IK配置
    • 确保四肢的LimbIK组件和对应的目标物体已正确设置。

5. 优化与注意事项

  • 平滑处理:添加滤波(如指数平滑)减少抖动:
    Vector3 smoothedPosition = Vector3.Lerp(currentPosition, targetPosition, smoothFactor);
  • 动态调整核心位置:如果希望核心位置可以缓慢移动,可以使用插值方法动态调整coreOffset
    coreOffset = Vector3.Lerp(coreOffset, newCoreOffset, Time.deltaTime * smoothFactor);
  • 性能优化:确保Python和Unity的帧率匹配,避免数据堆积。

通过以上方法,你可以实现3D模型的核心位置固定或小幅移动,从而满足你的需求。

此条目发表在Final IK应用分类目录。将固定链接加入收藏夹。