AnimationCurve关键帧数值修改小工具
效果图
用途:界面动效已经由动效同事做完(假设k了100帧),然后UI同事又把一些节点的位置做了10px的调整。此时一帧一帧去手动改,费事费力还可能出错。
这个工具的用途就是:对相关节点的所有关键帧批量做偏移。
public class AnimClipEditWnd : EditorWindow { [MenuItem("MyTools/AnimClipEdit", false)] static void ShowWindow() { var win = GetWindow(typeof(AnimClipEditWnd), false, "AnimClipEdit"); win.minSize = new Vector2(500, 300); } private Vector2 m_ScrollPos; private AnimationClip m_Clip; private EditorCurveBinding[] m_CurveBindings; private List<string> m_PathList = new List<string>(); private Dictionary<string, List<string>> m_PathPropNamesDict = new Dictionary<string, List<string>>(); private int m_PathOptionIndex; private GUIContent[] m_PathOptions; private GUIContent m_LabelPath = new GUIContent("path"); private int m_PropNameOptionIndex; private string[] m_PropNameOptions; private AnimationCurve m_Curve; private int m_CurveBindingIndex; private float m_Delta; 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 != clip) { if (GUILayout.Button($"Refresh")) isClipChange = true; } bool isPathOptionChange = false; if (isClipChange) { isPathOptionChange = true; m_CurveBindings = null; m_PathList.Clear(); m_PathPropNamesDict.Clear(); m_PathOptionIndex = 0; m_PathOptions = null; m_PropNameOptionIndex = 0; m_PropNameOptions = null; if (null != clip) { m_CurveBindings = AnimationUtility.GetCurveBindings(clip); foreach (var b in m_CurveBindings) { if (!m_PathPropNamesDict.TryGetValue(b.path, out var list)) { list = new List<string>(); m_PathPropNamesDict.Add(b.path, list); m_PathList.Add(b.path); } if (!IsIgnoredPropty(b.propertyName)) { list.Add(b.propertyName); } } m_PathOptions = new GUIContent[m_PathList.Count]; for (int i = 0; i < m_PathList.Count; ++i) m_PathOptions[i] = new GUIContent(m_PathList[i].Replace("/", "\\")); } } if (null == m_Clip) return; bool isPropNameOptionChange = false; int pathOptionIndex = EditorGUILayout.Popup(m_LabelPath, 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_PathPropNamesDict[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]; string propName = m_PropNameOptions[m_PropNameOptionIndex]; UpdateCurveAndBinding(m_Clip, path, propName); } 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}"); } EditorGUILayout.EndVertical(); } GUILayout.Space(10); m_Delta = EditorGUILayout.FloatField("Delta", m_Delta); if (GUILayout.Button("所有关键帧的值+Delta")) { if (null != m_Curve) { Keyframe[] keyFrames = m_Curve.keys; for (int i = 0; i < keyFrames.Length; ++i) { Keyframe kf = keyFrames[i]; float old = kf.value; kf.value = TrimFloat(old + m_Delta); Debug.Log($"idx:{i}, t:{TrimFloat(kf.time)}, old:{old} -> {kf.value}"); m_Curve.MoveKey(i, kf); } AnimationUtility.SetEditorCurve(m_Clip, m_CurveBindings[m_CurveBindingIndex], m_Curve); EditorUtility.SetDirty(m_Clip); AssetDatabase.SaveAssets(); //AssetDatabase.Refresh(); Debug.Log($"finish"); } } } public static float TrimFloat(float f) { int i = (int)(f * 1000); float result = i / 1000.0f; return result; } private void UpdateCurveAndBinding(AnimationClip clip, string path, string propName) { m_Curve = null; int index = 0; foreach (var b in m_CurveBindings) { if (b.path == path) { //Debug.Log($"path:{b.path}, propName:{b.propertyName}, type:{b.type.Name}, discrete:{b.isDiscreteCurve}, pptr:{b.isPPtrCurve}"); if (b.propertyName == propName) { m_Curve = AnimationUtility.GetEditorCurve(clip, b); m_CurveBindingIndex = index; break; } } index++; } } private bool IsIgnoredPropty(string propertyName) { switch (propertyName) { case "m_Maskable": case "m_IsActive": case "m_RaycastTarget": case "m_FillCenter": case "m_FillMethod": case "m_PixelsPerUnitMultiplier": case "m_PreserveAspect": //bool类型忽略 return true; } return false; } }
分类:
Unity / anim
, Unity
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端