feat: 애니메이션 구간별 재생 속도 조절 에디터 툴 추가
- AnimationSectionSpeedEditor: 에디터 윈도우로 .anim 클립의 특정 프레임 구간 키프레임 간격 조절 - In-Memory API 방식(AnimationUtility + SerializedObject)로 FBX 타임라인 확장 문제 회피 - ImportAsset 미사용으로 Model Importer 재처리 차단 - 본/IK/prop/eyeLight 모든 커브 동시 수정, Humanoid muscle curve만 제외 - AnimationEvent 시간도 구간에 맞게 리맵핑 - 접선 스케일링으로 속도 변경 시 곡선 형태 유지 - ReverseAnimation: 애니메이션 반전 에디터 스크립트 추가
This commit is contained in:
441
Assets/_Game/Scripts/Editor/AnimationSectionSpeedEditor.cs
Normal file
441
Assets/_Game/Scripts/Editor/AnimationSectionSpeedEditor.cs
Normal file
@@ -0,0 +1,441 @@
|
||||
using UnityEngine;
|
||||
|
||||
using UnityEditor;
|
||||
|
||||
/// <summary>
|
||||
/// 애니메이션 클립의 특정 프레임 구간 재생 속도를 조절하는 에디터 윈도우입니다.
|
||||
/// 지정한 프레임 범위의 키프레임 간격을 늘리거나 줄여 특정 동작을 강조할 수 있습니다.
|
||||
///
|
||||
/// 사용법:
|
||||
/// 1. Tools → Animation Section Speed Editor 열기
|
||||
/// 2. Project 창에서 .anim 파일 선택 (FBX 내부 클립은 Extract 후 사용)
|
||||
/// 3. 프레임 범위와 속도 배율 설정
|
||||
/// 4. Apply 클릭
|
||||
///
|
||||
/// ※ AssetDatabase.ImportAsset()을 호출하지 않고 AnimationUtility API로 메모리에서 수정 후
|
||||
/// SetDirty + SaveAssets로 저장합니다. Model Importer 파이프라인을 우회하여
|
||||
/// FBX 전체 타임라인이 끌려오는 문제를 방지합니다.
|
||||
/// </summary>
|
||||
public class AnimationSectionSpeedEditor : EditorWindow
|
||||
{
|
||||
[MenuItem("Tools/Animation Section Speed Editor")]
|
||||
public static void ShowWindow()
|
||||
{
|
||||
GetWindow<AnimationSectionSpeedEditor>("Section Speed Editor");
|
||||
}
|
||||
|
||||
private AnimationClip clip;
|
||||
private int fps = 30;
|
||||
private int startFrame;
|
||||
private int endFrame = 10;
|
||||
private float speedMultiplier = 1f;
|
||||
private Vector2 scrollPos;
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
|
||||
|
||||
// ── 클립 선택 ──
|
||||
EditorGUI.BeginChangeCheck();
|
||||
clip = (AnimationClip)EditorGUILayout.ObjectField("Animation Clip", clip, typeof(AnimationClip), false);
|
||||
if (EditorGUI.EndChangeCheck() && clip != null)
|
||||
{
|
||||
endFrame = Mathf.FloorToInt(clip.length * fps);
|
||||
}
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (clip == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox("Animation Clip을 선택하세요.\n(FBX 내부 클립은 미리 Extract 해야 합니다)", MessageType.Info);
|
||||
EditorGUILayout.EndScrollView();
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 클립 정보 (실제 키프레임 기반) ──
|
||||
float actualLength = GetActualLastKeyframeTime(clip);
|
||||
EditorGUILayout.LabelField("Clip Info", EditorStyles.boldLabel);
|
||||
EditorGUI.indentLevel++;
|
||||
EditorGUILayout.LabelField("Length (keyframes)", $"{actualLength:F3}s");
|
||||
EditorGUILayout.LabelField("m_StopTime", $"{clip.length:F3}s");
|
||||
|
||||
if (Mathf.Abs(clip.length - actualLength) > 0.01f)
|
||||
{
|
||||
EditorGUILayout.HelpBox($"m_StopTime({clip.length:F3}s)이 실제 키프레임 길이({actualLength:F3}s)와 다릅니다.\n" +
|
||||
"스피드 변경 시 실제 키프레임 길이를 기준으로 계산합니다.", MessageType.Warning);
|
||||
}
|
||||
|
||||
EditorGUILayout.LabelField("Total Frames ({fps}fps)", $"{Mathf.Max(1, Mathf.FloorToInt(actualLength * fps))}");
|
||||
EditorGUI.indentLevel--;
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
// ── FPS 설정 ──
|
||||
fps = EditorGUILayout.IntField("FPS", fps);
|
||||
fps = Mathf.Max(1, fps);
|
||||
|
||||
int totalFrames = Mathf.Max(1, Mathf.FloorToInt(actualLength * fps));
|
||||
|
||||
// ── 프레임 범위 ──
|
||||
EditorGUILayout.LabelField("Target Frame Range", EditorStyles.boldLabel);
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
int newStart = EditorGUILayout.IntField("Start Frame", startFrame);
|
||||
int newEnd = EditorGUILayout.IntField("End Frame", endFrame);
|
||||
|
||||
startFrame = Mathf.Clamp(newStart, 0, totalFrames - 1);
|
||||
endFrame = Mathf.Clamp(newEnd, 0, totalFrames);
|
||||
|
||||
// 슬라이더
|
||||
float startFloat = startFrame;
|
||||
float endFloat = endFrame;
|
||||
EditorGUILayout.MinMaxSlider(ref startFloat, ref endFloat, 0, totalFrames);
|
||||
startFrame = Mathf.RoundToInt(startFloat);
|
||||
endFrame = Mathf.RoundToInt(endFloat);
|
||||
EditorGUI.indentLevel--;
|
||||
|
||||
if (startFrame >= endFrame)
|
||||
{
|
||||
EditorGUILayout.HelpBox("시작 프레임이 끝 프레임보다 크거나 같습니다.", MessageType.Warning);
|
||||
EditorGUILayout.EndScrollView();
|
||||
return;
|
||||
}
|
||||
|
||||
float startTime = (float)startFrame / fps;
|
||||
float endTime = (float)endFrame / fps;
|
||||
float duration = endTime - startTime;
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
// ── 타임라인 시각화 ──
|
||||
DrawTimeline(startTime, endTime, actualLength);
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
// ── 속도 배율 ──
|
||||
EditorGUILayout.LabelField("Speed", EditorStyles.boldLabel);
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
speedMultiplier = EditorGUILayout.FloatField("Speed Multiplier", speedMultiplier);
|
||||
speedMultiplier = Mathf.Max(0.1f, speedMultiplier);
|
||||
|
||||
// 프리셋 버튼
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("0.25x")) speedMultiplier = 0.25f;
|
||||
if (GUILayout.Button("0.5x")) speedMultiplier = 0.5f;
|
||||
if (GUILayout.Button("1.0x")) speedMultiplier = 1f;
|
||||
if (GUILayout.Button("2.0x")) speedMultiplier = 2f;
|
||||
EditorGUILayout.EndHorizontal();
|
||||
EditorGUI.indentLevel--;
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
// ── 미리보기 ──
|
||||
float newDuration = duration / speedMultiplier;
|
||||
float timeDelta = newDuration - duration;
|
||||
float newLength = actualLength + timeDelta;
|
||||
|
||||
EditorGUILayout.LabelField("Preview", EditorStyles.boldLabel);
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
EditorGUIUtility.labelWidth = 160;
|
||||
EditorGUILayout.LabelField("Original Section", $"{duration:F3}s (frame {startFrame}~{endFrame})");
|
||||
EditorGUILayout.LabelField("New Section", $"{newDuration:F3}s");
|
||||
EditorGUILayout.LabelField("Time Delta", $"{(timeDelta >= 0 ? "+" : "")}{timeDelta:F3}s");
|
||||
EditorGUILayout.LabelField("New Clip Length", $"{newLength:F3}s");
|
||||
EditorGUIUtility.labelWidth = 0;
|
||||
EditorGUI.indentLevel--;
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
// ── 적용 버튼 ──
|
||||
if (speedMultiplier == 1f)
|
||||
{
|
||||
EditorGUILayout.HelpBox("속도 배율이 1.0이면 변경 사항이 없습니다.", MessageType.Info);
|
||||
}
|
||||
|
||||
EditorGUI.BeginDisabledGroup(speedMultiplier == 1f);
|
||||
if (GUILayout.Button("Apply", GUILayout.Height(40)))
|
||||
{
|
||||
ApplySpeedChange();
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 타임라인을 간단한 바 형태로 시각화합니다.
|
||||
/// </summary>
|
||||
private void DrawTimeline(float startTime, float endTime, float clipLength)
|
||||
{
|
||||
float barHeight = 24f;
|
||||
float padding = 4f;
|
||||
Rect barRect = GUILayoutUtility.GetRect(EditorGUIUtility.currentViewWidth - 20, barHeight * 2 + padding * 3);
|
||||
|
||||
// 전체 타임라인 배경
|
||||
EditorGUI.DrawRect(barRect, new Color(0.2f, 0.2f, 0.2f, 1f));
|
||||
|
||||
float startNorm = Mathf.Clamp01(startTime / clipLength);
|
||||
float endNorm = Mathf.Clamp01(endTime / clipLength);
|
||||
|
||||
// ── 원본 구간 (위쪽 바) ──
|
||||
float topY = barRect.y + padding;
|
||||
Rect origSectionRect = new Rect(
|
||||
barRect.x + barRect.width * startNorm,
|
||||
topY,
|
||||
barRect.width * (endNorm - startNorm),
|
||||
barHeight);
|
||||
EditorGUI.DrawRect(origSectionRect, new Color(1f, 0.5f, 0.2f, 0.8f));
|
||||
|
||||
// 원본 구간 레이블
|
||||
GUIStyle labelStyle = new GUIStyle(EditorStyles.miniLabel);
|
||||
labelStyle.alignment = TextAnchor.MiddleCenter;
|
||||
labelStyle.normal.textColor = Color.white;
|
||||
EditorGUI.LabelField(origSectionRect, $"Original ({startFrame}~{endFrame})", labelStyle);
|
||||
|
||||
// ── 변경 후 구간 (아래쪽 바) ──
|
||||
float newDuration = (endTime - startTime) / speedMultiplier;
|
||||
float newClipLength = clipLength + (newDuration - (endTime - startTime));
|
||||
float newEndNorm = Mathf.Clamp01((startTime + newDuration) / newClipLength);
|
||||
|
||||
float botY = topY + barHeight + padding;
|
||||
Rect newSectionRect = new Rect(
|
||||
barRect.x + barRect.width * startNorm,
|
||||
botY,
|
||||
barRect.width * (newEndNorm - startNorm),
|
||||
barHeight);
|
||||
EditorGUI.DrawRect(newSectionRect, new Color(0.2f, 0.8f, 0.4f, 0.8f));
|
||||
|
||||
EditorGUI.LabelField(newSectionRect, $"New ({speedMultiplier}x)", labelStyle);
|
||||
|
||||
// 바 테두리
|
||||
EditorGUI.DrawRect(barRect, new Color(0.4f, 0.4f, 0.4f, 1f));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 본 트랜스폼 커브에서 실제 마지막 키프레임 시간을 구합니다.
|
||||
/// clip.length는 m_StopTime 기반이라 FBX Extract 클립에서는 실제 데이터와 다를 수 있습니다.
|
||||
/// IK 커브나 모션 플로트 커브는 제외하고, 본의 m_LocalRotation/Position/Scale만 참조합니다.
|
||||
/// </summary>
|
||||
private static readonly string[] BoneTransformProperties =
|
||||
{
|
||||
"m_LocalRotation.x", "m_LocalRotation.y", "m_LocalRotation.z", "m_LocalRotation.w",
|
||||
"m_LocalPosition.x", "m_LocalPosition.y", "m_LocalPosition.z",
|
||||
"m_LocalScale.x", "m_LocalScale.y", "m_LocalScale.z",
|
||||
"localEulerAnglesRaw.x", "localEulerAnglesRaw.y", "localEulerAnglesRaw.z",
|
||||
};
|
||||
|
||||
private static float GetActualLastKeyframeTime(AnimationClip clip)
|
||||
{
|
||||
float maxTime = 0f;
|
||||
EditorCurveBinding[] bindings = AnimationUtility.GetCurveBindings(clip);
|
||||
foreach (EditorCurveBinding binding in bindings)
|
||||
{
|
||||
// 모션 플로트 커브: path가 "0"이면 Humanoid muscle curve → 건너뛰기
|
||||
if (binding.path == "0")
|
||||
continue;
|
||||
|
||||
// 본 트랜스폼 프로퍼티만 참조 (IK 커브 등 제외)
|
||||
bool isBoneTransform = false;
|
||||
for (int i = 0; i < BoneTransformProperties.Length; i++)
|
||||
{
|
||||
if (binding.propertyName == BoneTransformProperties[i])
|
||||
{
|
||||
isBoneTransform = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isBoneTransform)
|
||||
continue;
|
||||
|
||||
AnimationCurve curve = AnimationUtility.GetEditorCurve(clip, binding);
|
||||
if (curve == null || curve.length == 0) continue;
|
||||
|
||||
float lastTime = curve.keys[curve.length - 1].time;
|
||||
if (lastTime > maxTime)
|
||||
maxTime = lastTime;
|
||||
}
|
||||
|
||||
// 이벤트도 고려
|
||||
AnimationEvent[] events = AnimationUtility.GetAnimationEvents(clip);
|
||||
foreach (var evt in events)
|
||||
{
|
||||
if (evt.time > maxTime)
|
||||
maxTime = evt.time;
|
||||
}
|
||||
|
||||
return maxTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Humanoid muscle curve인지 확인합니다. path가 "0"이면 Humanoid muscle curve입니다.
|
||||
/// bone transform, IK, prop, eyeLight 모두 수정 대상에 포함합니다.
|
||||
/// </summary>
|
||||
private static bool IsHumanoidMuscleCurve(string path)
|
||||
{
|
||||
return path == "0";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AnimationUtility API를 사용하여 메모리에서 특정 구간의 재생 속도를 조절합니다.
|
||||
/// AssetDatabase.ImportAsset()을 호출하지 않으므로 Model Importer 파이프라인을 우회합니다.
|
||||
///
|
||||
/// 처리 순서:
|
||||
/// 1. AnimationUtility.GetAllCurves로 모든 커브를 가져옴
|
||||
/// 2. 본 경로(IsBonePath) 필터링
|
||||
/// 3. 대상 구간 내 키프레임 시간 재배열 + 접선 스케일링
|
||||
/// 4. 대상 구간 외 후속 키프레임 시간 이동
|
||||
/// 5. AnimationUtility.SetEditorCurve로 커브 덮어쓰기
|
||||
/// 6. SerializedObject로 m_StopTime 갱신
|
||||
/// 7. AnimationEvent 시간 이동
|
||||
/// 8. SetDirty + SaveAssets (ImportAsset 호출 금지)
|
||||
/// </summary>
|
||||
private void ApplySpeedChange()
|
||||
{
|
||||
if (clip == null) return;
|
||||
|
||||
float startTime = (float)startFrame / fps;
|
||||
float endTime = (float)endFrame / fps;
|
||||
float duration = endTime - startTime;
|
||||
float newDuration = duration / speedMultiplier;
|
||||
float timeDelta = newDuration - duration;
|
||||
float actualLength = GetActualLastKeyframeTime(clip);
|
||||
|
||||
Undo.RegisterCompleteObjectUndo(clip, $"Section Speed {speedMultiplier}x (frames {startFrame}~{endFrame})");
|
||||
|
||||
// ── 1. 모든 커브 가져오기 ──
|
||||
EditorCurveBinding[] bindings = AnimationUtility.GetCurveBindings(clip);
|
||||
AnimationCurve[] curves = new AnimationCurve[bindings.Length];
|
||||
for (int i = 0; i < bindings.Length; i++)
|
||||
{
|
||||
curves[i] = AnimationUtility.GetEditorCurve(clip, bindings[i]);
|
||||
}
|
||||
|
||||
int modifiedCurves = 0;
|
||||
int modifiedKeyframes = 0;
|
||||
int skippedCurves = 0;
|
||||
|
||||
// ── 2~4. 커브별 키프레임 시간 수정 ──
|
||||
for (int i = 0; i < bindings.Length; i++)
|
||||
{
|
||||
EditorCurveBinding binding = bindings[i];
|
||||
AnimationCurve curve = curves[i];
|
||||
|
||||
// Humanoid muscle curve만 건너뛰기 (IK, prop, eyeLight 모두 수정 대상)
|
||||
if (IsHumanoidMuscleCurve(binding.path))
|
||||
{
|
||||
skippedCurves++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (curve == null || curve.length == 0)
|
||||
continue;
|
||||
|
||||
// 새 키프레임 배열 생성
|
||||
Keyframe[] newKeys = new Keyframe[curve.length];
|
||||
bool anyModified = false;
|
||||
|
||||
for (int k = 0; k < curve.length; k++)
|
||||
{
|
||||
Keyframe oldKey = curve.keys[k];
|
||||
float t = oldKey.time;
|
||||
|
||||
if (t >= startTime - 0.0001f && t <= endTime + 0.0001f)
|
||||
{
|
||||
// 구간 내 키프레임: 시간 재배열 + 접선 스케일링
|
||||
float normalizedPos = (t - startTime) / duration;
|
||||
float newTime = startTime + normalizedPos * newDuration;
|
||||
|
||||
// 접선 스케일링: 시간축이 늘어나면 기울기는 줄어야 함
|
||||
// dy/dt' = dy/dt * speedMultiplier (t' = t/speedMultiplier 이므로)
|
||||
float tangentScale = speedMultiplier;
|
||||
|
||||
newKeys[k] = new Keyframe(
|
||||
newTime,
|
||||
oldKey.value,
|
||||
oldKey.inTangent * tangentScale,
|
||||
oldKey.outTangent * tangentScale,
|
||||
oldKey.inWeight,
|
||||
oldKey.outWeight);
|
||||
|
||||
anyModified = true;
|
||||
modifiedKeyframes++;
|
||||
}
|
||||
else if (t > endTime + 0.0001f)
|
||||
{
|
||||
// 구간 이후 키프레임: 시간만 이동 (접선은 그대로)
|
||||
newKeys[k] = new Keyframe(
|
||||
t + timeDelta,
|
||||
oldKey.value,
|
||||
oldKey.inTangent,
|
||||
oldKey.outTangent,
|
||||
oldKey.inWeight,
|
||||
oldKey.outWeight);
|
||||
|
||||
anyModified = true;
|
||||
modifiedKeyframes++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 구간 이전 키프레임: 그대로
|
||||
newKeys[k] = oldKey;
|
||||
}
|
||||
}
|
||||
|
||||
if (anyModified)
|
||||
{
|
||||
curves[i] = new AnimationCurve(newKeys);
|
||||
AnimationUtility.SetEditorCurve(clip, binding, curves[i]);
|
||||
modifiedCurves++;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 5. m_StopTime 갱신 (SerializedObject로) ──
|
||||
float newStopTime = actualLength + timeDelta;
|
||||
var serializedClip = new SerializedObject(clip);
|
||||
SerializedProperty stopTimeProp = serializedClip.FindProperty("m_StopTime");
|
||||
if (stopTimeProp != null)
|
||||
{
|
||||
stopTimeProp.floatValue = newStopTime;
|
||||
serializedClip.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
// ── 6. AnimationEvent 시간 이동 ──
|
||||
AnimationEvent[] events = AnimationUtility.GetAnimationEvents(clip);
|
||||
bool eventsModified = false;
|
||||
for (int i = 0; i < events.Length; i++)
|
||||
{
|
||||
float evtTime = events[i].time;
|
||||
|
||||
if (evtTime >= startTime - 0.0001f && evtTime <= endTime + 0.0001f)
|
||||
{
|
||||
float normalizedPos = (evtTime - startTime) / duration;
|
||||
events[i].time = startTime + normalizedPos * newDuration;
|
||||
eventsModified = true;
|
||||
}
|
||||
else if (evtTime > endTime + 0.0001f)
|
||||
{
|
||||
events[i].time = evtTime + timeDelta;
|
||||
eventsModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (eventsModified)
|
||||
{
|
||||
AnimationUtility.SetAnimationEvents(clip, events);
|
||||
}
|
||||
|
||||
// ── 7. 저장 (ImportAsset 호출 금지) ──
|
||||
EditorUtility.SetDirty(clip);
|
||||
AssetDatabase.SaveAssets();
|
||||
Repaint();
|
||||
|
||||
Debug.Log($"[SectionSpeedEditor] {clip.name}: frames {startFrame}~{endFrame} → {speedMultiplier}x " +
|
||||
$"({duration:F3}s → {newDuration:F3}s) " +
|
||||
$"| clip: {actualLength:F3}s → {newStopTime:F3}s " +
|
||||
$"| curves: {modifiedCurves}, keyframes: {modifiedKeyframes}, skipped: {skippedCurves}" +
|
||||
$" | events: {(eventsModified ? "modified" : "none")}");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 11af7b19aec6eff43b6fa837908b7fe6
|
||||
83
Assets/_Game/Scripts/Editor/ReverseAnimation.cs
Normal file
83
Assets/_Game/Scripts/Editor/ReverseAnimation.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using UnityEngine;
|
||||
|
||||
using UnityEditor;
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// 선택한 AnimationClip의 키프레임 순서를 반전합니다.
|
||||
/// FBX 임포트 클립에는 적용 불가 — 우선 Extract 후 사용하세요.
|
||||
/// </summary>
|
||||
public static class ReverseAnimation
|
||||
{
|
||||
public static AnimationClip GetSelectedClip()
|
||||
{
|
||||
var clips = Selection.GetFiltered(typeof(AnimationClip), SelectionMode.Assets);
|
||||
if (clips.Length > 0)
|
||||
return clips[0] as AnimationClip;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[MenuItem("Tools/ReverseAnimation")]
|
||||
public static void Reverse()
|
||||
{
|
||||
var clip = GetSelectedClip();
|
||||
if (clip == null)
|
||||
return;
|
||||
|
||||
float clipLength = clip.length;
|
||||
|
||||
List<AnimationCurve> curves = new List<AnimationCurve>();
|
||||
EditorCurveBinding[] editorCurveBindings = AnimationUtility.GetCurveBindings(clip);
|
||||
|
||||
foreach (EditorCurveBinding i in editorCurveBindings)
|
||||
{
|
||||
var curve = AnimationUtility.GetEditorCurve(clip, i);
|
||||
curves.Add(curve);
|
||||
}
|
||||
|
||||
clip.ClearCurves();
|
||||
|
||||
for (int i = 0; i < curves.Count; i++)
|
||||
{
|
||||
var curve = curves[i];
|
||||
var binding = editorCurveBindings[i];
|
||||
var keys = curve.keys;
|
||||
int keyCount = keys.Length;
|
||||
|
||||
var postWrapmode = curve.postWrapMode;
|
||||
curve.postWrapMode = curve.preWrapMode;
|
||||
curve.preWrapMode = postWrapmode;
|
||||
|
||||
for (int j = 0; j < keyCount; j++)
|
||||
{
|
||||
Keyframe K = keys[j];
|
||||
K.time = clipLength - K.time;
|
||||
|
||||
var tmp = -K.inTangent;
|
||||
K.inTangent = -K.outTangent;
|
||||
K.outTangent = tmp;
|
||||
|
||||
keys[j] = K;
|
||||
}
|
||||
|
||||
curve.keys = keys;
|
||||
clip.SetCurve(binding.path, binding.type, binding.propertyName, curve);
|
||||
}
|
||||
|
||||
// AnimationEvent 시간도 반전
|
||||
var events = AnimationUtility.GetAnimationEvents(clip);
|
||||
if (events.Length > 0)
|
||||
{
|
||||
for (int i = 0; i < events.Length; i++)
|
||||
{
|
||||
events[i].time = clipLength - events[i].time;
|
||||
}
|
||||
AnimationUtility.SetAnimationEvents(clip, events);
|
||||
}
|
||||
|
||||
Debug.Log("[ReverseAnimation] Animation reversed: " + clip.name);
|
||||
EditorUtility.SetDirty(clip);
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/Editor/ReverseAnimation.cs.meta
Normal file
2
Assets/_Game/Scripts/Editor/ReverseAnimation.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cf6abf78150fb14409bc53815b9f432f
|
||||
Reference in New Issue
Block a user