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

Unity中使用BindPose实现角色自定义的正确实现方法求助

Unity角色自定义:BindPose动态修改骨骼的正确实现

我正在开发Unity游戏的角色自定义功能,需要让玩家动态调整角色肩宽、身高等外观参数。了解到通过修改BindPose可以在不影响动画的前提下实现此类调整,但目前遇到两个问题:

  • 无动画状态下修改骨骼参数有效,但播放动画时,骨骼的缩放与位置会被Unity重置;
  • 尝试修改BindPose的代码导致角色模型严重扭曲(示意图显示角色躯干与肢体出现不规则拉伸错位)。

以下是我尝试的代码,其中错误的BindPose修改方案已被注释:

private void SetBone(SkinnedMeshRenderer skinnedMeshRenderer, BodyParameterType paramType, float value)
{
    if (!skinnedMeshRenderer)
    {
        return;
    }
    BoneChangeType boneChangeType = paramType.BoneChangeType;
    if (boneChangeType == BoneChangeType.None)
    {
        return;
    }
    Mesh mesh = skinnedMeshRenderer.sharedMesh;
    Matrix4x4[] bindPoses = mesh.bindposes;
    Transform[] bones = skinnedMeshRenderer.bones;

    BoneChangeDimension dimension = paramType.BoneChangeDimension;
    int boneChangeDirection = paramType.BoneChangeDirection;
    float actualMinValue = paramType.GetActualMinValue(gender);
    float currentValue = BodyParams.GetValue(paramType);
    List<string> leftRight = paramType.HasLeftRight ? new (){"l", "r"} : new () {""};
    foreach (var boneName in paramType.Bones)
    {
        foreach (var direction in leftRight)
        {
            string boneRealName = direction + boneName;
            Transform bone = transform.FindChildOfName(boneRealName);
            if (!bone)
            {
                Debug.Log("bone not found: " + boneRealName);
                continue;
            }
            int boneIndex = bones.FindIndex(bone);
            if (boneIndex == -1)
            {
                Debug.Log("bone index not found: " + boneRealName);
                continue;
            }
            Vector3 scale = bone.localScale;
            Vector3 position = bone.localPosition;
            switch (boneChangeType)
            {
                case BoneChangeType.Scale:
                    scale[(int)dimension] = value / actualMinValue;
                    bone.localScale = scale;
                    // bindPoses[boneIndex] = Matrix4x4.Scale(position) * bindPoses[boneIndex];
                    // Vector3 vertexPositionInBone_20 = (BN_20.transform.worldToLocalMatrix * mesh.transform.localToWorldMatrix).MultiplyPoint3x4(vertex.transform.localPosition);
                    // vertex.transform.position = bone.transform.localToWorldMatrix.MultiplyPoint3x4(vertexPositionInBone_20);
                    break;
                case BoneChangeType.Position:
                    int leftRightDirection = dimension == BoneChangeDimension.X && direction == "r" ? -1 : 1;
                    float diff = value - currentValue;
                    float newValue = leftRightDirection * boneChangeDirection * diff / GlobalData.CmPerDistance;
                    position[(int)dimension] += newValue;
                    Matrix4x4 oldLocation = bone.transform.worldToLocalMatrix;
                    bone.localPosition = position;
                    bindPoses[boneIndex] = oldLocation * bone.worldToLocalMatrix * oldLocation * transform.localToWorldMatrix;
                    // Matrix4x4 newBindPose = oldBone.transform.worldToLocalMatrix * newBone.transform.worldToLocalMatrix * oldBone.transform.worldToLocalMatrix * mesh.localToWorldMatrix;
                    break;
            }
            if (_bodyParameterHandlers.TryGetValue(paramType, out BodyParameterHandler handler))
            {
                handler.HandleAttachment(new (value, currentValue));
            }
        }
    }
    _bodyMesh.bindposes = bindPoses;
}

核心问题分析

  1. 动画重置骨骼参数:动画Clip会直接覆盖骨骼的localScale/localPosition值,直接修改Transform的方式无法抵抗动画的覆盖。
  2. BindPose计算错误:之前的BindPose修改逻辑完全不符合矩阵运算规则,错误的矩阵乘法顺序和变换空间导致模型扭曲。BindPose的正确公式为:bindPose[i] = 逆(骨骼在绑定姿态下的世界矩阵) * SkinnedMeshRenderer的本地到世界矩阵。

修正后的代码实现

private void SetBone(SkinnedMeshRenderer skinnedMeshRenderer, BodyParameterType paramType, float value)
{
    if (!skinnedMeshRenderer) return;
    if (paramType.BoneChangeType == BoneChangeType.None) return;

    // 克隆Mesh和BindPose,避免修改原始共享资源影响其他实例
    Mesh mesh = Instantiate(skinnedMeshRenderer.sharedMesh);
    Matrix4x4[] bindPoses = mesh.bindposes;
    Transform[] bones = skinnedMeshRenderer.bones;
    Transform rootBone = skinnedMeshRenderer.rootBone;

    float actualMinValue = paramType.GetActualMinValue(gender);
    float currentValue = BodyParams.GetValue(paramType);
    List<string> leftRight = paramType.HasLeftRight ? new() { "l", "r" } : new() { "" };

    foreach (var boneName in paramType.Bones)
    {
        foreach (var direction in leftRight)
        {
            string boneRealName = direction + boneName;
            Transform bone = transform.FindChildOfName(boneRealName);
            if (!bone)
            {
                Debug.Log($"骨骼未找到: {boneRealName}");
                continue;
            }

            int boneIndex = System.Array.IndexOf(bones, bone);
            if (boneIndex == -1)
            {
                Debug.Log($"骨骼索引未找到: {boneRealName}");
                continue;
            }

            // 获取骨骼原始绑定姿态的世界矩阵
            Matrix4x4 originalBoneWorldMatrix = bone.localToWorldMatrix;
            Matrix4x4 newBoneWorldMatrix = originalBoneWorldMatrix;

            switch (paramType.BoneChangeType)
            {
                case BoneChangeType.Scale:
                    // 计算目标缩放比例
                    float scaleFactor = value / actualMinValue;
                    Vector3 targetScale = bone.localScale;
                    targetScale[(int)paramType.BoneChangeDimension] = scaleFactor;

                    // 构建新的骨骼世界矩阵:考虑父层级变换
                    Matrix4x4 parentWorldMatrix = bone.parent != null ? bone.parent.localToWorldMatrix : Matrix4x4.identity;
                    Matrix4x4 localMatrix = Matrix4x4.TRS(bone.localPosition, bone.localRotation, targetScale);
                    newBoneWorldMatrix = parentWorldMatrix * localMatrix;
                    break;

                case BoneChangeType.Position:
                    int leftRightDir = paramType.BoneChangeDimension == BoneChangeDimension.X && direction == "r" ? -1 : 1;
                    float diff = value - currentValue;
                    float offset = leftRightDir * paramType.BoneChangeDirection * diff / GlobalData.CmPerDistance;

                    // 计算本地空间的位置偏移并转换到世界空间
                    Vector3 localOffset = Vector3.zero;
                    localOffset[(int)paramType.BoneChangeDimension] = offset;
                    Vector3 worldOffset = bone.parent != null ? bone.parent.TransformVector(localOffset) : localOffset;

                    // 构建新的骨骼世界矩阵:保留原始旋转和缩放,只修改位置
                    newBoneWorldMatrix = Matrix4x4.TRS(
                        originalBoneWorldMatrix.GetColumn(3) + worldOffset,
                        originalBoneWorldMatrix.rotation,
                        originalBoneWorldMatrix.lossyScale
                    );
                    break;
            }

            // 计算并更新BindPose
            Matrix4x4 skinnedMeshWorldMatrix = skinnedMeshRenderer.transform.localToWorldMatrix;
            bindPoses[boneIndex] = newBoneWorldMatrix.inverse * skinnedMeshWorldMatrix;

            // 处理附件更新
            if (_bodyParameterHandlers.TryGetValue(paramType, out BodyParameterHandler handler))
            {
                handler.HandleAttachment(new(value, currentValue));
            }
        }
    }

    // 应用修改后的BindPose并替换Mesh
    mesh.bindposes = bindPoses;
    skinnedMeshRenderer.sharedMesh = mesh;
}

关键修正说明

  • 克隆Mesh资源:避免修改原始共享Mesh,防止其他角色实例受到影响。
  • 正确计算BindPose:严格遵循BindPose的数学定义,确保顶点到骨骼空间的转换正确。
  • 不直接修改骨骼Transform:通过调整BindPose改变顶点的绑定关系,动画会基于修改后的绑定姿态运行,不会被覆盖。
  • 层级变换处理:考虑骨骼的父层级关系,确保缩放和位移计算在正确的空间中进行。

额外注意事项

  • 确保动画Clip是基于原始绑定姿态制作的,没有烘焙骨骼的初始变换。
  • 对于联动性强的参数(如身高),需要同时调整多个相关骨骼的BindPose,避免局部拉伸扭曲。
  • 可以缓存原始BindPose和骨骼初始变换,减少重复计算开销。

内容的提问来源于stack exchange,提问作者Seaky Lone

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 07:05:55