下图中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";elsem_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}");elseEditorGUILayout.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}]");elseEditorGUILayout.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--;elseDebug.Log("first page");}if (GUILayout.Button($"下一页")){if (m_NoChangeInfosPage < totalPage - 1)m_NoChangeInfosPage++;elseDebug.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);}}}