引子
之前在做项目时,经常会遇到:需要模型保持动画中某一帧的状态的需求;或者直接将一系列模型的状态数据做成动画,给到我们。这时我总是会先想着怎么样能从动画中将这些数据(关键帧)提取出来呢?但之前因为思路的方向错误,导致一直都没有找到合适的解决方案,最终这些需求大部分都是,将动画手动分割成对应的小段循环播放,或手动抄写数据。
这几天终于无意间在网上找到了,简单方便的方案来解决这个问题。
解决方案
之前我的错误思路总是在找Animator
、Animation
、AnimationClip
、RuntimeAnimatorController
等类中的相应变量、属性或方法,来获取动画中的关键帧数据。这是错的,至少当前的这些类中没有这样的接口。
根据我查到的文章,并查询了官方文档发现通过一个编辑器类可以获取到帧信息AnimationUtility。
也就是说它只能在编辑器模式下使用,是无法在程序打包之后使用获取的,也因此我上面提到的思路是错误的。
由此便想到了在编辑器模式下把帧信息序列化(可以保存成数据文件,也可以保存在场景的某物体脚本组件上),然后在运行时直接读取便能解决问题。
核心接口使用:
foreach (var binding in AnimationUtility.GetCurveBindings(clip))
{
AnimationCurve curve = AnimationUtility.GetEditorCurve(clip, binding);
EditorGUILayout.LabelField(binding.path + "/" + binding.propertyName + ", Keys: " + curve.keys.Length);
for (int i = 0; i < curve.length; i++)
{
EditorGUILayout.LabelField("Keys" + i + ":" + curve[i].value);
}
}
代码中clip
为传入的AnimationClip
对象,通过AnimationUtility.GetCurveBindings
接口获取与动画剪辑相关的所有动画事件,然后再通过AnimationUtility.GetEditorCurve
接口得到绑定所指向的float曲线,之后便可以得到我们想要得到的数据了,这里列举几个主要的:binding.path
是模型中所操作的对象节点路径;binding.propertyName
是所操作的对象节点属性(例如Position、Rotation、Scale、BlendShape等);curve
为关键帧曲线;curve.length
和curve.keys.Length
都是关键帧个数;最后循环中的curve[i].value
是关键帧中数值,curve[i].time
是关键帧中时间。
示例
以下是我项目中的示例,由于只能是编辑器模式下使用,所以是一个Basic
脚本的Inspector
窗口的Editor
编辑脚本。
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(Basic), true)]
public class BasicEditor : Editor
{
protected Basic m_script;
protected SerializedProperty scriptProp;
protected float width_view;
protected GUILayoutOption width_whole;
protected GUILayoutOption width_half;
private void OnEnable()
{
m_script = (Basic)target;
scriptProp = serializedObject.FindProperty("m_Script");
}
public override void OnInspectorGUI()
{
width_whole = GUILayout.Width(width_view);
width_half = GUILayout.Width(width_view / 2);
//base.OnInspectorGUI();
serializedObject.Update();
GUI.enabled = false;
EditorGUILayout.PropertyField(scriptProp);
GUI.enabled = true;
EditorGUILayout.Space();
m_script.m_clip = (AnimationClip)EditorGUILayout.ObjectField("基础动画", m_script.m_clip, typeof(AnimationClip), true);
if (m_script.m_clip != null)
{
BasicExpressionsInfo info = m_script.m_info;
if (GUILayout.Button("读取动画数据"))
{
foreach (var binding in AnimationUtility.GetCurveBindings(m_script.m_clip))
{
//Debug.Log(binding.path);
AnimationCurve curve = AnimationUtility.GetEditorCurve(m_script.m_clip, binding);
string[] temp = binding.propertyName.Split('.');
string name = temp[temp.Length - 1];
name = name.Split('_')[0];
if (!m_script.m_baseExpressionName.Contains(name))
{
m_script.m_baseExpressionName.Add(name);
m_script.m_baseExpressionValue.Add(0);
FloatArr floatArr = new FloatArr()
{
m_value = new float[curve.keys.Length]
};
info.m_baseExpressionFloat.Add(floatArr);
StringArr stringArr = new StringArr
{
m_value = new string[curve.keys.Length]
};
info.m_baseExpressionString.Add(stringArr);
info.m_baseExpressionIndex.Add(0);
}
int index = m_script.m_baseExpressionName.IndexOf(name);
for (int i = 0; i < curve.length; i++)
{
info.m_baseExpressionFloat[index].m_value[i] = curve[i].value;
info.m_baseExpressionString[index].m_value[i] = curve[i].time.ToString() + " --- " + curve[i].value.ToString();
}
}
}
bool isWidth = false;
info.m_foldoutAnimation = EditorGUILayout.BeginFoldoutHeaderGroup(info.m_foldoutAnimation, "BlendShape(动画:帧数 --- 数值)");
if (!isWidth && info.m_foldoutAnimation)
{
isWidth = true;
}
if (info.m_foldoutAnimation)
{
string[] names = m_script.m_baseExpressionName.ToArray();
for (int i = 0; i < names.Length; i++)
{
info.m_baseExpressionIndex[i] = EditorGUILayout.Popup(names[i], info.m_baseExpressionIndex[i], info.m_baseExpressionString[i].m_value);
}
}
EditorGUILayout.EndFoldoutHeaderGroup();
EditorGUILayout.Space();
if (GUILayout.Button("重置所有数据为动画数据"))
{
string[] names = m_script.m_baseExpressionName.ToArray();
for (int i = 0; i < names.Length; i++)
{
m_script.m_baseExpressionValue[i] = info.m_baseExpressionFloat[i].m_value[info.m_baseExpressionIndex[i]];
}
}
info.m_foldoutPredefine = EditorGUILayout.BeginFoldoutHeaderGroup(info.m_foldoutPredefine, "BlendShape(预设值)");
if (!isWidth && info.m_foldoutPredefine)
{
isWidth = true;
}
if (info.m_foldoutPredefine)
{
string[] names = m_script.m_baseExpressionName.ToArray();
for (int i = 0; i < names.Length; i++)
{
GUILayout.BeginHorizontal();
EditorGUILayout.LabelField(names[i], width_half);
if (GUILayout.Button("重置为动画数据", width_half))
{
m_script.m_baseExpressionValue[i] = info.m_baseExpressionFloat[i].m_value[info.m_baseExpressionIndex[i]];
}
GUILayout.EndHorizontal();
m_script.m_baseExpressionValue[i] = EditorGUILayout.Slider(m_script.m_baseExpressionValue[i], 0, 100);
}
}
if (isWidth)
{
width_view = EditorGUIUtility.currentViewWidth - 39;
}
else
{
width_view = EditorGUIUtility.currentViewWidth - 30;
}
}
serializedObject.ApplyModifiedProperties();
if (GUI.changed)
{//当Inspector 面板发生变化时保存数据
EditorUtility.SetDirty(target);
}
}
private void OnDestroy()
{
}
}
在将Basic
脚本放到场景中,拖入动画,点击读取动画数据按钮,便会将动画中的BlendShape数据读取并保存到了脚本中,再点击重置所有数据为动画数据按钮,即,将这些脚本动画数据,覆盖真正程序运行的所要使用的变量属性,保存场景之后这些数据也就会保存在场景中,以便程序运行时使用。
下面是示例脚本的其他相关代码及Inspector
窗口效果:
using System;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 基础表情
/// </summary>
public class Basic : MonoBehaviour
{
#if UNITY_EDITOR
public BasicExpressionsInfo m_info = new BasicExpressionsInfo();
#endif
public AnimationClip m_clip;
public List<string> m_baseExpressionName = new List<string>();
public List<float> m_baseExpressionValue = new List<float>();
}
#if UNITY_EDITOR
/// <summary>
/// 编辑器信息
/// </summary>
[Serializable]//便于编辑器储存,可视化操作
public class BasicExpressionsInfo
{
public bool m_foldoutAnimation;
public bool m_foldoutPredefine;
public List<FloatArr> m_baseExpressionFloat = new List<FloatArr>();
public List<StringArr> m_baseExpressionString = new List<StringArr>();
public List<int> m_baseExpressionIndex = new List<int>();
}
[Serializable]//便于编辑器储存,可视化操作
public class FloatArr { public float[] m_value; }
[Serializable]//便于编辑器储存,可视化操作
public class StringArr { public string[] m_value; }
#endif
参考链接
地址:https://blog.csdn.net/qq_36292069/article/details/88601302