feat: 애니메이션 클립 병합/잘라내기 에디터 툴 추가 및 메뉴 통합
- AnimationClipMerger: 여러 클립을 순서 조정 후 이어 붙이는 에디터 윈도우 추가 - AnimationClipTrimmerWindow: 시작/종료 프레임 지정으로 클립 잘라내기 에디터 윈도우 추가 - 기존 애니메이션 에디터 툴 메뉴를 Tools/Animation/ 서브메뉴로 통합 - Merge Clips, Trim Clip, Reverse Clip, Section Speed Editor
This commit is contained in:
280
Assets/_Game/Scripts/Editor/AnimationClipMerger.cs
Normal file
280
Assets/_Game/Scripts/Editor/AnimationClipMerger.cs
Normal file
@@ -0,0 +1,280 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
using UnityEditor;
|
||||
|
||||
/// <summary>
|
||||
/// 여러 AnimationClip을 시간 순으로 이어 붙여 새로운 클립을 생성하는 에디터 윈도우입니다.
|
||||
/// 클립 순서를 자유롭게 조정할 수 있습니다.
|
||||
/// FBX 임포트 클립에는 적용 불가 — 우선 Extract 후 사용하세요.
|
||||
/// </summary>
|
||||
public class AnimationClipMergerWindow : EditorWindow
|
||||
{
|
||||
private List<AnimationClip> clips = new List<AnimationClip>();
|
||||
private Vector2 scrollPosition;
|
||||
|
||||
private const string MenuPath = "Tools/Animation/Merge Clips";
|
||||
|
||||
[MenuItem(MenuPath, false, 20)]
|
||||
public static void ShowWindow()
|
||||
{
|
||||
var selectedClips = Selection.GetFiltered<AnimationClip>(SelectionMode.Assets);
|
||||
|
||||
if (selectedClips.Length < 2)
|
||||
{
|
||||
EditorUtility.DisplayDialog(
|
||||
"Merge Animation Clips",
|
||||
"병합할 AnimationClip을 2개 이상 선택해 주세요.",
|
||||
"확인");
|
||||
return;
|
||||
}
|
||||
|
||||
var window = GetWindow<AnimationClipMergerWindow>("Clip Merger");
|
||||
// 선택 순서 유지 (Selection.GetFiltered는 역순일 수 있으므로 뒤집음)
|
||||
window.clips = selectedClips.Reverse().ToList();
|
||||
window.minSize = new Vector2(300, 250);
|
||||
window.Show();
|
||||
}
|
||||
|
||||
[MenuItem(MenuPath, true)]
|
||||
public static bool ValidateShowWindow()
|
||||
{
|
||||
return Selection.GetFiltered<AnimationClip>(SelectionMode.Assets).Length >= 2;
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
EditorGUILayout.Space(8f);
|
||||
EditorGUILayout.LabelField($"클립 순서 ({clips.Count}개)", EditorStyles.boldLabel);
|
||||
|
||||
EditorGUILayout.Space(4f);
|
||||
|
||||
if (clips.Count == 0)
|
||||
{
|
||||
EditorGUILayout.HelpBox("클립이 없습니다.", MessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
|
||||
|
||||
for (int i = 0; i < clips.Count; i++)
|
||||
{
|
||||
AnimationClip clip = clips[i];
|
||||
if (clip == null)
|
||||
{
|
||||
EditorGUILayout.LabelField($"[{i + 1}] (삭제됨)", EditorStyles.boldLabel);
|
||||
continue;
|
||||
}
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
|
||||
// 인덱스
|
||||
EditorGUILayout.LabelField($"[{i + 1}]", GUILayout.Width(30));
|
||||
|
||||
// 클립 정보
|
||||
EditorGUI.BeginDisabledGroup(true);
|
||||
EditorGUILayout.ObjectField(clip, typeof(AnimationClip), false);
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
// 길이 표시
|
||||
EditorGUILayout.LabelField($"{clip.length:F2}s", GUILayout.Width(50));
|
||||
|
||||
// 위로 / 아래로 버튼
|
||||
EditorGUI.BeginDisabledGroup(i == 0);
|
||||
if (GUILayout.Button("▲", GUILayout.Width(28)))
|
||||
{
|
||||
clips.Swap(i, i - 1);
|
||||
break;
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
EditorGUI.BeginDisabledGroup(i == clips.Count - 1);
|
||||
if (GUILayout.Button("▼", GUILayout.Width(28)))
|
||||
{
|
||||
clips.Swap(i, i + 1);
|
||||
break;
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
// 제거 버튼
|
||||
if (GUILayout.Button("✕", GUILayout.Width(28)))
|
||||
{
|
||||
clips.RemoveAt(i);
|
||||
break;
|
||||
}
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
|
||||
EditorGUILayout.Space(8f);
|
||||
|
||||
// 총 길이
|
||||
float totalLength = clips.Sum(c => c != null ? c.length : 0f);
|
||||
EditorGUILayout.LabelField($"총 길이: {totalLength:F3}초", EditorStyles.miniBoldLabel);
|
||||
|
||||
EditorGUILayout.Space(8f);
|
||||
|
||||
// 병합 버튼
|
||||
EditorGUI.BeginDisabledGroup(clips.Count < 2);
|
||||
if (GUILayout.Button("병합", GUILayout.Height(32f)))
|
||||
{
|
||||
PerformMerge();
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 순서대로 클립들을 이어 붙여 새 AnimationClip을 생성합니다.
|
||||
/// </summary>
|
||||
private void PerformMerge()
|
||||
{
|
||||
// null 클립 제거
|
||||
clips.RemoveAll(c => c == null);
|
||||
|
||||
if (clips.Count < 2)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Merge Animation Clips", "유효한 클립이 2개 이상 필요합니다.", "확인");
|
||||
return;
|
||||
}
|
||||
|
||||
// 첫 번째 클립이 위치한 폴더에 결과 저장
|
||||
string firstClipPath = AssetDatabase.GetAssetPath(clips[0]);
|
||||
string directory = System.IO.Path.GetDirectoryName(firstClipPath);
|
||||
string mergedName = "Merged_" + string.Join("_", clips.Select(c => c.name));
|
||||
string savePath = System.IO.Path.Combine(directory, mergedName + ".anim")
|
||||
.Replace("\\", "/");
|
||||
savePath = AssetDatabase.GenerateUniqueAssetPath(savePath);
|
||||
|
||||
AnimationClip mergedClip = new AnimationClip
|
||||
{
|
||||
name = mergedName,
|
||||
frameRate = clips[0].frameRate,
|
||||
legacy = clips[0].legacy,
|
||||
wrapMode = WrapMode.Once
|
||||
};
|
||||
|
||||
float timeOffset = 0f;
|
||||
|
||||
foreach (AnimationClip sourceClip in clips)
|
||||
{
|
||||
CopyCurvesToClip(sourceClip, mergedClip, timeOffset);
|
||||
CopyEventsToClip(sourceClip, mergedClip, timeOffset);
|
||||
timeOffset += sourceClip.length;
|
||||
}
|
||||
|
||||
AssetDatabase.CreateAsset(mergedClip, savePath);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
EditorUtility.FocusProjectWindow();
|
||||
Selection.activeObject = mergedClip;
|
||||
|
||||
Debug.Log($"[AnimationClipMerger] {clips.Count}개 클립 병합 완료: {savePath} (총 {timeOffset:F3}초)");
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 소스 클립의 모든 커브를 타겟 클립에 오프셋 적용하여 복사합니다.
|
||||
/// 동일 바인딩이 이미 존재하면 키 프레임을 병합합니다.
|
||||
/// </summary>
|
||||
private static void CopyCurvesToClip(AnimationClip source, AnimationClip target, float timeOffset)
|
||||
{
|
||||
EditorCurveBinding[] bindings = AnimationUtility.GetCurveBindings(source);
|
||||
|
||||
foreach (EditorCurveBinding binding in bindings)
|
||||
{
|
||||
AnimationCurve sourceCurve = AnimationUtility.GetEditorCurve(source, binding);
|
||||
if (sourceCurve == null || sourceCurve.keys.Length == 0)
|
||||
continue;
|
||||
|
||||
AnimationCurve existingCurve = AnimationUtility.GetEditorCurve(target, binding);
|
||||
AnimationCurve newCurve = existingCurve != null
|
||||
? AppendKeys(existingCurve, sourceCurve, timeOffset)
|
||||
: OffsetCurve(sourceCurve, timeOffset);
|
||||
|
||||
target.SetCurve(binding.path, binding.type, binding.propertyName, newCurve);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 커브의 모든 키 프레임 시간을 오프셋만큼 이동시킨 새 커브를 반환합니다.
|
||||
/// </summary>
|
||||
private static AnimationCurve OffsetCurve(AnimationCurve curve, float offset)
|
||||
{
|
||||
Keyframe[] keys = curve.keys;
|
||||
for (int i = 0; i < keys.Length; i++)
|
||||
{
|
||||
keys[i].time += offset;
|
||||
}
|
||||
|
||||
return new AnimationCurve(keys)
|
||||
{
|
||||
preWrapMode = curve.preWrapMode,
|
||||
postWrapMode = curve.postWrapMode
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기존 커브에 새 키 프레임들을 오프셋 적용하여 추가합니다.
|
||||
/// </summary>
|
||||
private static AnimationCurve AppendKeys(AnimationCurve existing, AnimationCurve toAppend, float offset)
|
||||
{
|
||||
List<Keyframe> allKeys = new List<Keyframe>(existing.keys);
|
||||
|
||||
foreach (Keyframe key in toAppend.keys)
|
||||
{
|
||||
allKeys.Add(new Keyframe(key.time + offset, key.value, key.inTangent, key.outTangent));
|
||||
}
|
||||
|
||||
return new AnimationCurve(allKeys.ToArray())
|
||||
{
|
||||
preWrapMode = existing.preWrapMode,
|
||||
postWrapMode = existing.postWrapMode
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 소스 클립의 AnimationEvent를 오프셋 적용하여 타겟 클립에 추가합니다.
|
||||
/// </summary>
|
||||
private static void CopyEventsToClip(AnimationClip source, AnimationClip target, float timeOffset)
|
||||
{
|
||||
AnimationEvent[] events = AnimationUtility.GetAnimationEvents(source);
|
||||
if (events.Length == 0)
|
||||
return;
|
||||
|
||||
AnimationEvent[] existingEvents = AnimationUtility.GetAnimationEvents(target);
|
||||
List<AnimationEvent> allEvents = new List<AnimationEvent>(existingEvents);
|
||||
|
||||
foreach (AnimationEvent evt in events)
|
||||
{
|
||||
allEvents.Add(new AnimationEvent
|
||||
{
|
||||
time = evt.time + timeOffset,
|
||||
functionName = evt.functionName,
|
||||
floatParameter = evt.floatParameter,
|
||||
intParameter = evt.intParameter,
|
||||
stringParameter = evt.stringParameter,
|
||||
objectReferenceParameter = evt.objectReferenceParameter,
|
||||
messageOptions = evt.messageOptions
|
||||
});
|
||||
}
|
||||
|
||||
AnimationUtility.SetAnimationEvents(target, allEvents.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// List 확장 메서드: 두 요소의 위치를 교체합니다.
|
||||
/// </summary>
|
||||
internal static class ListExtensions
|
||||
{
|
||||
public static void Swap<T>(this IList<T> list, int a, int b)
|
||||
{
|
||||
(list[a], list[b]) = (list[b], list[a]);
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/Editor/AnimationClipMerger.cs.meta
Normal file
2
Assets/_Game/Scripts/Editor/AnimationClipMerger.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: df6d4bb7132b1f4408c8ce2fae50bd11
|
||||
238
Assets/_Game/Scripts/Editor/AnimationClipTrimmerWindow.cs
Normal file
238
Assets/_Game/Scripts/Editor/AnimationClipTrimmerWindow.cs
Normal file
@@ -0,0 +1,238 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 603e437915f49744fb7e18298349dd5e
|
||||
@@ -18,7 +18,7 @@ using UnityEditor;
|
||||
/// </summary>
|
||||
public class AnimationSectionSpeedEditor : EditorWindow
|
||||
{
|
||||
[MenuItem("Tools/Animation Section Speed Editor")]
|
||||
[MenuItem("Tools/Animation/Section Speed Editor")]
|
||||
public static void ShowWindow()
|
||||
{
|
||||
GetWindow<AnimationSectionSpeedEditor>("Section Speed Editor");
|
||||
|
||||
@@ -19,7 +19,7 @@ public static class ReverseAnimation
|
||||
return null;
|
||||
}
|
||||
|
||||
[MenuItem("Tools/ReverseAnimation")]
|
||||
[MenuItem("Tools/Animation/Reverse Clip")]
|
||||
public static void Reverse()
|
||||
{
|
||||
var clip = GetSelectedClip();
|
||||
|
||||
Reference in New Issue
Block a user