using System.Collections.Generic; using UnityEngine; using UnityEditor; /// /// 선택한 .anim 클립의 마지막 프레임에 모든 커브 키를 보강합니다. /// 애니메이션을 잘라서 연결할 때 마지막 포즈가 정확히 유지되도록 돕는 도구입니다. /// public static class AnimationClipEndKeyBaker { private const string MenuPath = "Tools/Animation/Bake End Frame Keys"; [MenuItem(MenuPath, false, 23)] public static void BakeSelectedClipEndKeys() { AnimationClip[] clips = Selection.GetFiltered(SelectionMode.Assets); if (clips == null || clips.Length == 0) { EditorUtility.DisplayDialog( "Bake End Frame Keys", "마지막 프레임 키를 보강할 AnimationClip을 1개 이상 선택해 주세요.", "확인"); return; } int processedClipCount = 0; int addedFloatKeyCount = 0; int addedObjectKeyCount = 0; List skippedClips = new List(); for (int i = 0; i < clips.Length; i++) { AnimationClip clip = clips[i]; string clipPath = AssetDatabase.GetAssetPath(clip); if (clip == null || string.IsNullOrEmpty(clipPath) || !clipPath.EndsWith(".anim")) { skippedClips.Add(clip != null ? clip.name : "(null)"); continue; } processedClipCount++; float lastFrameTime = clip.length; EditorCurveBinding[] floatBindings = AnimationUtility.GetCurveBindings(clip); for (int bindingIndex = 0; bindingIndex < floatBindings.Length; bindingIndex++) { EditorCurveBinding binding = floatBindings[bindingIndex]; AnimationCurve curve = AnimationUtility.GetEditorCurve(clip, binding); if (curve == null) continue; if (HasFloatKeyAtTime(curve, lastFrameTime)) continue; float value = curve.Evaluate(lastFrameTime); int keyIndex = curve.AddKey(new Keyframe(lastFrameTime, value)); if (keyIndex >= 0) { AnimationUtility.SetKeyLeftTangentMode(curve, keyIndex, AnimationUtility.TangentMode.ClampedAuto); AnimationUtility.SetKeyRightTangentMode(curve, keyIndex, AnimationUtility.TangentMode.ClampedAuto); } AnimationUtility.SetEditorCurve(clip, binding, curve); addedFloatKeyCount++; } EditorCurveBinding[] objectBindings = AnimationUtility.GetObjectReferenceCurveBindings(clip); for (int bindingIndex = 0; bindingIndex < objectBindings.Length; bindingIndex++) { EditorCurveBinding binding = objectBindings[bindingIndex]; ObjectReferenceKeyframe[] keys = AnimationUtility.GetObjectReferenceCurve(clip, binding); if (keys == null || keys.Length == 0) continue; if (HasObjectKeyAtTime(keys, lastFrameTime)) continue; Object sampledValue = SampleObjectReferenceValue(keys, lastFrameTime); List bakedKeys = new List(keys) { new ObjectReferenceKeyframe { time = lastFrameTime, value = sampledValue } }; bakedKeys.Sort((a, b) => a.time.CompareTo(b.time)); AnimationUtility.SetObjectReferenceCurve(clip, binding, bakedKeys.ToArray()); addedObjectKeyCount++; } EditorUtility.SetDirty(clip); AssetDatabase.ImportAsset(clipPath, ImportAssetOptions.ForceUpdate); } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); string message = $"처리한 클립: {processedClipCount}개\n" + $"추가한 Float 키: {addedFloatKeyCount}개\n" + $"추가한 Object 키: {addedObjectKeyCount}개"; if (skippedClips.Count > 0) message += $"\n\n건너뛴 클립: {string.Join(", ", skippedClips)}"; Debug.Log($"[AnimationClipEndKeyBaker] {message.Replace('\n', ' ')}"); EditorUtility.DisplayDialog("Bake End Frame Keys", message, "확인"); } [MenuItem(MenuPath, true)] public static bool ValidateBakeSelectedClipEndKeys() { return Selection.GetFiltered(SelectionMode.Assets).Length > 0; } private static bool HasFloatKeyAtTime(AnimationCurve curve, float time) { if (curve == null) return false; Keyframe[] keys = curve.keys; for (int i = 0; i < keys.Length; i++) { if (Mathf.Abs(keys[i].time - time) <= 0.0001f) return true; } return false; } private static bool HasObjectKeyAtTime(ObjectReferenceKeyframe[] keys, float time) { if (keys == null) return false; for (int i = 0; i < keys.Length; i++) { if (Mathf.Abs(keys[i].time - time) <= 0.0001f) return true; } return false; } private static Object SampleObjectReferenceValue(ObjectReferenceKeyframe[] keys, float time) { if (keys == null || keys.Length == 0) return null; Object value = keys[0].value; for (int i = 0; i < keys.Length; i++) { if (keys[i].time > time) break; value = keys[i].value; } return value; } }