- AnimationClipMerger: 여러 클립을 순서 조정 후 이어 붙이는 에디터 윈도우 추가 - AnimationClipTrimmerWindow: 시작/종료 프레임 지정으로 클립 잘라내기 에디터 윈도우 추가 - 기존 애니메이션 에디터 툴 메뉴를 Tools/Animation/ 서브메뉴로 통합 - Merge Clips, Trim Clip, Reverse Clip, Section Speed Editor
239 lines
8.2 KiB
C#
239 lines
8.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
|
|
using UnityEngine;
|
|
|
|
using UnityEditor;
|
|
|
|
/// <summary>
|
|
/// AnimationClip의 시작/종료 프레임을 설정하여 해당 구간만 잘라낸 새 클립을 생성합니다.
|
|
/// FBX 임포트 클립에는 적용 불가 — 우선 Extract 후 사용하세요.
|
|
/// </summary>
|
|
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<AnimationClip>(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<AnimationClipTrimmerWindow>("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<AnimationClip>(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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 프레임 구간만 포함하는 새 AnimationClip을 생성합니다.
|
|
/// </summary>
|
|
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<AnimationEvent> trimmedEvents = new List<AnimationEvent>();
|
|
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 커브에서 startTime ~ endTime 구간의 키 프레임만 추출하고 시간을 0부터 시작하도록 오프셋합니다.
|
|
/// </summary>
|
|
private static AnimationCurve TrimCurve(AnimationCurve curve, float startTime, float endTime)
|
|
{
|
|
Keyframe[] sourceKeys = curve.keys;
|
|
List<Keyframe> trimmedKeys = new List<Keyframe>();
|
|
|
|
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
|
|
};
|
|
}
|
|
}
|