如下图所示,Left、Right FootIK Weight两条动画曲线用于设置人物角色足部IK时获取权重值,手动编辑且动画片段较多时比较费事,可以考虑程序化烘焙这两条足部IK权重曲线。
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();
}
//获取资产路径
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();
//采样率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 不在地面时权重为0
leftFootKeyFrames[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,如下图所示。