AnimationClip优化工具 - 删除连续相同的帧
下图中Rotation.z的前4个关键帧[0, 3](即15帧, 30帧, 45帧, 60帧),值都没变;
(3, 4)Rotation.z变为60(即61帧到90帧);
后3个关键帧[5, 7]一直维持在60没变。
可以分析下:前4个关键帧,[1, 2]删除对动画没影响,后3个关键帧[5, 7]删除对动画也没影响。
public class AnimClipCurveOptWnd : EditorWindow { [MenuItem("MyTools/Anim/AnimClipCurveOptWnd", false)] static void ShowWindow() { var win = GetWindow(typeof(AnimClipCurveOptWnd), false, "AnimClipCurveOptWnd"); } private Vector2 m_ScrollPos; private AnimationClip m_Clip; ///对应AnimationWnd的一行轨道 private EditorCurveBinding[] m_CurveBindings; ///有动画的节点的路径 private List<string> m_PathList = new List<string>(); private Dictionary<string, List<string>> m_PathToPropNamesDict = new Dictionary<string, List<string>>(); private int m_PathOptionIndex; private string[] m_PathOptions; private int m_PropNameOptionIndex; private string[] m_PropNameOptions; ///关键帧数据都保存在Curve.keys上 private AnimationCurve m_Curve; private int m_CurveBindingIndex = -1; private void OnEnable() { if (null != m_Clip && null == m_CurveBindings) { m_PathList.Clear(); m_PathToPropNamesDict.Clear(); m_CurveBindings = AnimationUtility.GetCurveBindings(m_Clip); foreach (var b in m_CurveBindings) { if (!m_PathToPropNamesDict.TryGetValue(b.path, out var propNames)) { propNames = new List<string>(); m_PathToPropNamesDict.Add(b.path, propNames); m_PathList.Add(b.path); } } } } private void OnGUI() { m_ScrollPos = EditorGUILayout.BeginScrollView(m_ScrollPos); { OnGUI_ScrollView(); } EditorGUILayout.EndScrollView(); } private void OnGUI_ScrollView() { var clip = (AnimationClip)EditorGUILayout.ObjectField("Clip", m_Clip, typeof(AnimationClip), false); bool isClipChange = m_Clip != clip; m_Clip = clip; if (null != m_Clip) { if (GUILayout.Button($"Refresh")) isClipChange = true; if (GUILayout.Button("检测连续相同的帧")) { CheckAllNoChange(); } if (m_NoChangeInfos.Count > 0) { if (GUILayout.Button("清除多余的连续相同帧数据")) { ClearAllNoChange(); } } } bool isPathOptionChange = false; if (isClipChange) { Debug.Log($"clip change"); isPathOptionChange = true; m_CurveBindings = null; m_NoChangeInfos.Clear(); m_NoChangeInfosPage = 0; m_PathList.Clear(); m_PathToPropNamesDict.Clear(); m_PathOptionIndex = 0; m_PathOptions = null; m_PropNameOptionIndex = 0; m_PropNameOptions = null; if (null != m_Clip) { m_CurveBindings = AnimationUtility.GetCurveBindings(m_Clip); foreach (var b in m_CurveBindings) { if (!m_PathToPropNamesDict.TryGetValue(b.path, out var propNames)) { propNames = new List<string>(); m_PathToPropNamesDict.Add(b.path, propNames); m_PathList.Add(b.path); } propNames.Add(b.propertyName); } m_PathOptions = m_PathList.ToArray(); for (int i = 0; i < m_PathList.Count; ++i) { var path = m_PathList[i]; if ("" == path) m_PathOptions[i] = "Root"; else m_PathOptions[i] = path.Replace("/", "\\"); } } } if (null == m_Clip) return; bool isPropNameOptionChange = false; GUILayout.Space(10); int pathOptionIndex = EditorGUILayout.Popup("path", m_PathOptionIndex, m_PathOptions); if (isPathOptionChange || m_PathOptionIndex != pathOptionIndex) { Debug.Log($"PathOptionChange: {m_PathOptionIndex} -> {pathOptionIndex}"); m_PathOptionIndex = pathOptionIndex; string path = m_PathList[pathOptionIndex]; var propNameList = m_PathToPropNamesDict[path]; m_PropNameOptions = propNameList.ToArray(); m_PropNameOptionIndex = 0; isPropNameOptionChange = true; } int propNameOptionIndex = EditorGUILayout.Popup("propName", m_PropNameOptionIndex, m_PropNameOptions); if (isPropNameOptionChange || m_PropNameOptionIndex != propNameOptionIndex) { Debug.Log($"PropNameOptionChange: {m_PropNameOptionIndex} -> {propNameOptionIndex}"); m_PropNameOptionIndex = propNameOptionIndex; string path = m_PathList[pathOptionIndex]; UpdateCurBindingIndexAndCurve(m_Clip, path, m_PropNameOptionIndex); } if (null != m_Curve) { GUILayout.Space(10); EditorGUILayout.CurveField(m_Curve, GUILayout.Height(100)); Keyframe[] keyFrames = m_Curve.keys; EditorGUILayout.LabelField("所有关键帧:"); EditorGUILayout.BeginVertical("box"); for (int i = 0; i < keyFrames.Length; ++i) { var kf = keyFrames[i]; EditorGUILayout.LabelField($"{i} -> t: {kf.time}, v: {kf.value}, 帧: {Utils.TrimFloat2(m_Clip.frameRate * kf.time)}"); } EditorGUILayout.EndVertical(); } if (m_NoChangeInfos.Count > 0) { GUILayout.Space(10); EditorGUILayout.LabelField($"连续相同的帧信息:"); EditorGUILayout.BeginVertical("box"); for (int i = 0; i < 10; ++i) { int pageIndex = m_NoChangeInfosPage * 10 + i; if (pageIndex >= m_NoChangeInfos.Count) break; var info = m_NoChangeInfos[pageIndex]; int lastFrameIndex = info.totalFrames - 1; if ("" == info.binding.path) EditorGUILayout.LabelField($"Root -> {info.binding.propertyName}, last:{lastFrameIndex}"); else EditorGUILayout.LabelField($"{info.binding.path} -> {info.binding.propertyName}, last:{lastFrameIndex}"); for (int i2 = 0; i2 < info.frames.Count; i2 += 2) { int index1 = info.frames[i2]; int index2 = info.frames[i2+1]; if (index2 == lastFrameIndex) EditorGUILayout.LabelField($"相同: [{index1}, {index2}], 可删除: [{index1+1}, {index2}]"); else EditorGUILayout.LabelField($"相同: [{index1}, {index2}], 可删除: [{index1+1}, {index2-1}]"); } } int totalPage = Mathf.CeilToInt(m_NoChangeInfos.Count / 10.0f); if (totalPage > 1) { EditorGUILayout.BeginHorizontal(); if (GUILayout.Button($"上一页")) { if (m_NoChangeInfosPage > 0) m_NoChangeInfosPage--; else Debug.Log("first page"); } if (GUILayout.Button($"下一页")) { if (m_NoChangeInfosPage < totalPage - 1) m_NoChangeInfosPage++; else Debug.Log("last page"); } GUILayout.Label($"({m_NoChangeInfosPage+1}/{totalPage})"); EditorGUILayout.EndHorizontal(); } EditorGUILayout.EndVertical(); } } struct NoChangeInfo { public EditorCurveBinding binding; public List<int> frames; // i, i+1的方式存放连续相同帧区间 public int totalFrames; //总帧数 } private List<NoChangeInfo> m_NoChangeInfos = new List<NoChangeInfo>(); private int m_NoChangeInfosPage = 0; private void ClearAllNoChange() { Undo.RecordObject(m_Clip, "opti"); foreach (var info in m_NoChangeInfos) { var curve = AnimationUtility.GetEditorCurve(m_Clip, info.binding); int lastFrameIndex = curve.length - 1; Debug.Log($"----- removeSameFrame: {info.binding.path} -> {info.binding.propertyName}"); for (int i = info.frames.Count - 2; i >= 0; i -= 2) { int index1 = info.frames[i]; int index2 = info.frames[i + 1]; if (index2 == lastFrameIndex) { Debug.Log($"[{index1 + 1}, {index2}]"); for (int j = index2; j > index1; --j) curve.RemoveKey(j); } else { Debug.Log($"[{index1 + 1}, {index2 - 1}]"); for (int j = index2 - 1; j > index1; --j) //中间的帧全部删除 { curve.RemoveKey(j); } } } Debug.Log($"-----"); AnimationUtility.SetEditorCurve(m_Clip, info.binding, curve); } var binding = m_CurveBindings[m_CurveBindingIndex]; m_Curve = AnimationUtility.GetEditorCurve(m_Clip, binding); m_NoChangeInfos.Clear(); m_NoChangeInfosPage = 0; AssetDatabase.SaveAssets(); //内存写到磁盘 //Repaint(); } private void CheckAllNoChange() { m_NoChangeInfos.Clear(); m_NoChangeInfosPage = 0; var list = new List<int>(); foreach (var b in m_CurveBindings) { var curve = AnimationUtility.GetEditorCurve(m_Clip, b); Utils.CheckNoChangeFrames(curve, b, list); if (list.Count > 0) { var info = new NoChangeInfo(); info.binding = b; info.frames = list; info.totalFrames = curve.length; m_NoChangeInfos.Add(info); list = new List<int>(); } } } private void UpdateCurBindingIndexAndCurve(AnimationClip clip, string path, int propNameOptIndex) { m_CurveBindingIndex = -1; m_Curve = null; if (m_PropNameOptions.Length <= 0) { Debug.Log($"PropNameOptions len == 0"); return; } string propName = m_PropNameOptions[propNameOptIndex]; int index = 0; foreach (var b in m_CurveBindings) { if (b.path == path) { if (b.propertyName == propName) { m_CurveBindingIndex = index; m_Curve = AnimationUtility.GetEditorCurve(clip, b); break; } } index++; } } }
public static class Utils { //保留2位小数 public static float TrimFloat2(float f1) { int i = (int)(f1 * 100); float result = i / 100.0f; return result; } //保留4位小数 public static float TrimFloat4(float f1) { int i = (int)(f1 * 10000); float result = i / 10000.0f; return result; } public static bool IsFloatEquals(float f1, float f2) { float delta = f1 - f2; if (Mathf.Abs(delta) <= 0.0001f) return true; return false; } ///两帧没有变化 public static bool IsFrameNoChange(Keyframe kf1, Keyframe kf2) { if (Utils.IsFloatEquals(kf1.value, kf2.value)) { if (kf1.outTangent == 0 && kf2.inTangent == 0) //flat, 水平 return true; if (float.IsInfinity(kf1.outTangent) || float.IsInfinity(kf2.inTangent)) return true; } return false; } public static void CheckNoChangeFrames(AnimationCurve curve, EditorCurveBinding binding, List<int> outList) { int index1 = -1; //获取前面不变的帧下标和时间 var keys = curve.keys; for (int i = 0; i < keys.Length - 1; ++i) { var kf1 = keys[i]; var kf2 = keys[i + 1]; if (-1 == index1) //找开始 { if (IsFrameNoChange(kf1, kf2)) { index1 = i; } } else { if (!IsFrameNoChange(kf1, kf2)) { int deltaFrame = i - index1; if (deltaFrame <= 1) { //相邻帧相互的忽略 } else { outList.Add(index1); outList.Add(i); } index1 = -1; } } } if (-1 != index1) { outList.Add(index1); outList.Add(keys.Length - 1); } } }