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; }
核心问题分析
- 动画重置骨骼参数:动画Clip会直接覆盖骨骼的
localScale/localPosition值,直接修改Transform的方式无法抵抗动画的覆盖。 - 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
相关产品推荐
相关产品推荐

