简介
如下图所示,Left、Right FootIK Weight两条动画曲线用于设置人物角色足部IK时获取权重值,手动编辑且动画片段较多时比较费事,可以考虑程序化烘焙这两条足部IK权重曲线。
实现思路
- 创建新的编辑器窗口,在OnGUI中获取当前所选中的游戏物体,并在该物体上获取Animator动画组件。
- 获取动画组件中的动画片段数组,遍历该数组通过滚动视图列举动画片段,添加Bake按钮用于烘培权重曲线。
private Vector2 scroll;
private void OnGUI()
{GameObject selectedGameObject = Selection.activeGameObject;if (selectedGameObject == null){EditorGUILayout.HelpBox("未选中任何游戏物体", MessageType.Warning);return;}Animator animator = selectedGameObject.GetComponent<Animator>();if (animator == null){EditorGUILayout.HelpBox("所选游戏物体不包含Animator组件", MessageType.Warning);return;}AnimationClip[] clips = animator.runtimeAnimatorController.animationClips;if (clips.Length == 0){EditorGUILayout.HelpBox("Animator组件中的动画片段数量为零", MessageType.Warning);return;}scroll = GUILayout.BeginScrollView(scroll);for (int i = 0; i < clips.Length; i++){AnimationClip clip = clips[i];GUILayout.BeginHorizontal();GUILayout.Label(clip.name);GUILayout.FlexibleSpace();if (GUILayout.Button("Bake", GUILayout.Width(50f)))EditorCoroutineUtility.StartCoroutine(BakeCoroutine(selectedGameObject, animator, clip), this);GUILayout.EndHorizontal();}GUILayout.EndScrollView();
}
- 根据动画片段获取其资产路径、资产导入器以及对应的ModelImporterClipAnimation,如果已经有目标曲线则进行过滤,以便重新生成。
//获取资产路径
string assetPath = AssetDatabase.GetAssetPath(clip);
//获取资产导入器
ModelImporter importer = AssetImporter.GetAtPath(assetPath) as ModelImporter;
ModelImporterClipAnimation[] clipAnimations = importer.clipAnimations;
ModelImporterClipAnimation target = clipAnimations.FirstOrDefault(m => m.name == clip.name);
//过滤(如果已经有对应的两条曲线)
target.curves = target.curves.Where(m=> m.name != "Left FootIK Weight"&& m.name != "Right FootIK Weight").ToArray();
//保存、重新导入
importer.SaveAndReimport();
- 通过AnimationClip.SampleAnimation函数进行采样,帧数 = 动画时长 * 采样率。采样时根据脚是否处于地面记录当前帧的值,在地面时权重为1,不在地面时权重为0。注意采样前记录初始姿态,采样后进行恢复。
//采样率30
int samplingRate = 30;
int frames = Mathf.CeilToInt(clip.length * samplingRate);
Keyframe[] leftFootKeyFrames = new Keyframe[frames];
Keyframe[] rightFootKeyFrames = new Keyframe[frames];
//采样前记录初始姿态
Dictionary<Transform, (Vector3, Quaternion)> pose = new Dictionary<Transform, (Vector3, Quaternion)>();
Transform[] children = animator.GetComponentsInChildren<Transform>();
for (int i = 0; i < children.Length; i++)
{Transform child = children[i];pose.Add(child, (child.position, child.rotation));
}
//采样
for (int i = 0; i < frames; i++)
{clip.SampleAnimation(go, (float)i / samplingRate);bool isFootGroundedLeft = IsFootGrounded(animator, HumanBodyBones.LeftFoot);bool isFootGroundedRight = IsFootGrounded(animator, HumanBodyBones.RightFoot);//在地面时权重为1 不在地面时权重为0leftFootKeyFrames[i] = new Keyframe((float)i / frames, isFootGroundedLeft ? 1f : 0f);rightFootKeyFrames[i] = new Keyframe((float)i / frames, isFootGroundedRight ? 1f : 0f);yield return null;
}
//采样后恢复初始姿态
foreach (var kv in pose)
{kv.Key.position = kv.Value.Item1;kv.Key.rotation = kv.Value.Item2;
}
- 进行过滤,当帧值与前帧、后帧值都一样的帧是不需要的。
//过滤(当帧值与前帧、后帧值都一样)
leftFootKeyFrames = Enumerable.Range(0, frames).Where(i =>{bool sameWithPrev = (i - 1) >= 0 && leftFootKeyFrames[i - 1].value == leftFootKeyFrames[i].value;bool sameWithLast = (i + 1) < frames && leftFootKeyFrames[i + 1].value == leftFootKeyFrames[i].value;return !sameWithPrev || !sameWithLast;}).Select(i => leftFootKeyFrames[i]).ToArray();
rightFootKeyFrames = Enumerable.Range(0, frames).Where(i =>{bool sameWithPrev = (i - 1) >= 0 && rightFootKeyFrames[i - 1].value == rightFootKeyFrames[i].value;bool sameWithLast = (i + 1) < frames && rightFootKeyFrames[i + 1].value == rightFootKeyFrames[i].value;return !sameWithPrev || !sameWithLast;}).Select(i => rightFootKeyFrames[i]).ToArray();
- 添加新生成的两条动画曲线。
ClipAnimationInfoCurve leftAnimInfoCurve = new ClipAnimationInfoCurve()
{name = "Left FootIK Weight",curve = new AnimationCurve(leftFootKeyFrames)
};
ClipAnimationInfoCurve rightAnimInfoCurve = new ClipAnimationInfoCurve()
{name = "Right FootIK Weight",curve = new AnimationCurve(rightFootKeyFrames)
};
//添加生成的两条曲线
target.curves = target.curves.Concat(new ClipAnimationInfoCurve[2] { leftAnimInfoCurve, rightAnimInfoCurve }).ToArray();
importer.clipAnimations = clipAnimations;
importer.SaveAndReimport();
相关工具
生成过程使用了协程,在Runtime运行时可以通过MonoBehaviour中的StartCoroutine开启协程,而工具工作在编辑器环境。可以使用Package Manager中的协程工具Editor Coroutines,如下图所示。