人型动画足部IK权重曲线烘焙

发布时间:2023年12月25日

简介

如下图所示,Left、Right FootIK Weight两条动画曲线用于设置人物角色足部IK时获取权重值,手动编辑且动画片段较多时比较费事,可以考虑程序化烘焙这两条足部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();
}

Humanoid FootIK AnimCurve Baker

  • 根据动画片段获取其资产路径、资产导入器以及对应的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 不在地面时权重为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,如下图所示。

Editor Coroutines

文章来源:https://blog.csdn.net/qq_42139931/article/details/135198517
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。