using System; using System.Collections.Generic; using UnityEngine; using UnityEditor; /// /// AnimationClip의 시작/종료 프레임을 설정하여 해당 구간만 잘라낸 새 클립을 생성합니다. /// FBX 임포트 클립에는 적용 불가 — 우선 Extract 후 사용하세요. /// public class AnimationClipTrimmerWindow : EditorWindow { private AnimationClip sourceClip; private int startFrame; private int endFrame; private float clipFrameRate; private float clipLength; private int totalFrames; private const string MenuPath = "Tools/Animation/Trim Clip"; [MenuItem(MenuPath, false, 21)] public static void ShowWindow() { var clips = Selection.GetFiltered(SelectionMode.Assets); if (clips.Length == 0) { EditorUtility.DisplayDialog( "Trim Animation Clip", "AnimationClip을 1개 선택해 주세요.", "확인"); return; } if (clips.Length > 1) { EditorUtility.DisplayDialog( "Trim Animation Clip", "1개의 AnimationClip만 선택해 주세요.", "확인"); return; } var window = GetWindow("Clip Trimmer"); window.sourceClip = clips[0]; window.clipFrameRate = clips[0].frameRate; window.clipLength = clips[0].length; window.totalFrames = Mathf.RoundToInt(clips[0].length * clips[0].frameRate); window.startFrame = 0; window.endFrame = window.totalFrames; window.minSize = new Vector2(320, 200); window.Show(); } [MenuItem(MenuPath, true)] public static bool ValidateShowWindow() { return Selection.GetFiltered(SelectionMode.Assets).Length == 1; } private void OnGUI() { if (sourceClip == null) { EditorGUILayout.HelpBox("AnimationClip을 찾을 수 없습니다.", MessageType.Error); return; } // 소스 클립 정보 EditorGUILayout.Space(8f); EditorGUILayout.LabelField("소스 클립", EditorStyles.boldLabel); EditorGUILayout.LabelField($" 이름: {sourceClip.name}"); EditorGUILayout.LabelField($" 프레임 레이트: {clipFrameRate} FPS"); EditorGUILayout.LabelField($" 전체 길이: {clipLength:F3}초 ({totalFrames}프레임)"); EditorGUILayout.Space(12f); // 프레임 범위 설정 EditorGUILayout.LabelField("잘라내기 범위", EditorStyles.boldLabel); EditorGUILayout.Space(4f); EditorGUI.BeginChangeCheck(); int newStart = EditorGUILayout.IntField("시작 프레임", startFrame); int newEnd = EditorGUILayout.IntField("종료 프레임", endFrame); if (EditorGUI.EndChangeCheck()) { startFrame = Mathf.Clamp(newStart, 0, totalFrames); endFrame = Mathf.Clamp(newEnd, 0, totalFrames); // 시작이 종료보다 크면 교체 if (startFrame > endFrame) (startFrame, endFrame) = (endFrame, startFrame); } // 시간 표시 float startTime = startFrame / clipFrameRate; float endTime = endFrame / clipFrameRate; float duration = endTime - startTime; EditorGUILayout.Space(4f); EditorGUILayout.LabelField($" 구간: {startTime:F3}초 ~ {endTime:F3}초"); EditorGUILayout.LabelField($" 길이: {duration:F3}초 ({endFrame - startFrame}프레임)"); // 유효성 경고 if (startFrame == 0 && endFrame == totalFrames) { EditorGUILayout.HelpBox("전체 구간이 선택되었습니다. 변경 후 잘라내기를 진행하세요.", MessageType.Info); } EditorGUILayout.Space(12f); // 잘라내기 버튼 EditorGUI.BeginDisabledGroup(startFrame == 0 && endFrame == totalFrames); if (GUILayout.Button("잘라내기 (새 클립 생성)", GUILayout.Height(32f))) { PerformTrim(); } EditorGUI.EndDisabledGroup(); } /// /// 지정된 프레임 구간만 포함하는 새 AnimationClip을 생성합니다. /// private void PerformTrim() { float startTime = startFrame / clipFrameRate; float endTime = endFrame / clipFrameRate; // 결과 클립 생성 string sourcePath = AssetDatabase.GetAssetPath(sourceClip); string directory = System.IO.Path.GetDirectoryName(sourcePath); string trimmedName = $"{sourceClip.name}_Trimmed_F{startFrame}_F{endFrame}"; string savePath = System.IO.Path.Combine(directory, trimmedName + ".anim") .Replace("\\", "/"); savePath = AssetDatabase.GenerateUniqueAssetPath(savePath); AnimationClip trimmedClip = new AnimationClip { name = trimmedName, frameRate = sourceClip.frameRate, legacy = sourceClip.legacy, wrapMode = sourceClip.wrapMode }; // 커브 복사 (시간 오프셋 반영) EditorCurveBinding[] bindings = AnimationUtility.GetCurveBindings(sourceClip); foreach (EditorCurveBinding binding in bindings) { AnimationCurve sourceCurve = AnimationUtility.GetEditorCurve(sourceClip, binding); if (sourceCurve == null || sourceCurve.keys.Length == 0) continue; AnimationCurve trimmedCurve = TrimCurve(sourceCurve, startTime, endTime); if (trimmedCurve.keys.Length > 0) { trimmedClip.SetCurve(binding.path, binding.type, binding.propertyName, trimmedCurve); } } // AnimationEvent 복사 (시간 오프셋 반영) AnimationEvent[] sourceEvents = AnimationUtility.GetAnimationEvents(sourceClip); if (sourceEvents.Length > 0) { List trimmedEvents = new List(); foreach (AnimationEvent evt in sourceEvents) { if (evt.time >= startTime && evt.time <= endTime) { trimmedEvents.Add(new AnimationEvent { time = evt.time - startTime, functionName = evt.functionName, floatParameter = evt.floatParameter, intParameter = evt.intParameter, stringParameter = evt.stringParameter, objectReferenceParameter = evt.objectReferenceParameter, messageOptions = evt.messageOptions }); } } if (trimmedEvents.Count > 0) AnimationUtility.SetAnimationEvents(trimmedClip, trimmedEvents.ToArray()); } // 저장 AssetDatabase.CreateAsset(trimmedClip, savePath); AssetDatabase.SaveAssets(); EditorUtility.FocusProjectWindow(); Selection.activeObject = trimmedClip; Debug.Log( $"[AnimationClipTrimmer] 잘라내기 완료: {sourceClip.name} " + $"F{startFrame}~F{endFrame} ({endTime - startTime:F3}초) → {savePath}"); Close(); } /// /// 커브에서 startTime ~ endTime 구간의 키 프레임만 추출하고 시간을 0부터 시작하도록 오프셋합니다. /// private static AnimationCurve TrimCurve(AnimationCurve curve, float startTime, float endTime) { Keyframe[] sourceKeys = curve.keys; List trimmedKeys = new List(); foreach (Keyframe key in sourceKeys) { if (key.time >= startTime && key.time <= endTime) { trimmedKeys.Add(new Keyframe( key.time - startTime, key.value, key.inTangent, key.outTangent )); } } if (trimmedKeys.Count == 0) return null; return new AnimationCurve(trimmedKeys.ToArray()) { preWrapMode = curve.preWrapMode, postWrapMode = curve.postWrapMode }; } }