diff --git a/Assets/_Game/Scripts/Editor/AnimationClipTrimmerWindow.cs b/Assets/_Game/Scripts/Editor/AnimationClipTrimmerWindow.cs index 7a660065..eef75ce0 100644 --- a/Assets/_Game/Scripts/Editor/AnimationClipTrimmerWindow.cs +++ b/Assets/_Game/Scripts/Editor/AnimationClipTrimmerWindow.cs @@ -158,7 +158,7 @@ public class AnimationClipTrimmerWindow : EditorWindow continue; AnimationCurve trimmedCurve = TrimCurve(sourceCurve, startTime, endTime); - if (trimmedCurve.keys.Length > 0) + if (trimmedCurve != null && trimmedCurve.keys.Length > 0) { trimmedClip.SetCurve(binding.path, binding.type, binding.propertyName, trimmedCurve); } @@ -191,8 +191,21 @@ public class AnimationClipTrimmerWindow : EditorWindow AnimationUtility.SetAnimationEvents(trimmedClip, trimmedEvents.ToArray()); } + // Object reference 커브도 함께 복사해 VFX/소품 전환이 유지되도록 합니다. + EditorCurveBinding[] objectBindings = AnimationUtility.GetObjectReferenceCurveBindings(sourceClip); + foreach (EditorCurveBinding binding in objectBindings) + { + ObjectReferenceKeyframe[] sourceKeys = AnimationUtility.GetObjectReferenceCurve(sourceClip, binding); + ObjectReferenceKeyframe[] trimmedKeys = TrimObjectReferenceCurve(sourceKeys, startTime, endTime); + if (trimmedKeys != null && trimmedKeys.Length > 0) + { + AnimationUtility.SetObjectReferenceCurve(trimmedClip, binding, trimmedKeys); + } + } + // 저장 AssetDatabase.CreateAsset(trimmedClip, savePath); + CopyClipSettings(sourceClip, trimmedClip, endTime - startTime); AssetDatabase.SaveAssets(); EditorUtility.FocusProjectWindow(); @@ -210,8 +223,12 @@ public class AnimationClipTrimmerWindow : EditorWindow /// private static AnimationCurve TrimCurve(AnimationCurve curve, float startTime, float endTime) { + if (curve == null || endTime < startTime) + return null; + Keyframe[] sourceKeys = curve.keys; List trimmedKeys = new List(); + float duration = endTime - startTime; foreach (Keyframe key in sourceKeys) { @@ -226,8 +243,50 @@ public class AnimationClipTrimmerWindow : EditorWindow } } + // 선택 구간에 기존 키가 없더라도 경계 값을 샘플링해 고정 구간을 유지합니다. if (trimmedKeys.Count == 0) - return null; + { + float startValue = curve.Evaluate(startTime); + float endValue = curve.Evaluate(endTime); + + trimmedKeys.Add(new Keyframe(0f, startValue)); + + if (duration > Mathf.Epsilon) + trimmedKeys.Add(new Keyframe(duration, endValue)); + } + else + { + float startValue = curve.Evaluate(startTime); + float endValue = curve.Evaluate(endTime); + + if (!Mathf.Approximately(trimmedKeys[0].time, 0f)) + { + trimmedKeys.Insert(0, new Keyframe(0f, startValue)); + } + else + { + Keyframe firstKey = trimmedKeys[0]; + firstKey.time = 0f; + firstKey.value = startValue; + trimmedKeys[0] = firstKey; + } + + if (duration > Mathf.Epsilon) + { + int lastIndex = trimmedKeys.Count - 1; + if (!Mathf.Approximately(trimmedKeys[lastIndex].time, duration)) + { + trimmedKeys.Add(new Keyframe(duration, endValue)); + } + else + { + Keyframe lastKey = trimmedKeys[lastIndex]; + lastKey.time = duration; + lastKey.value = endValue; + trimmedKeys[lastIndex] = lastKey; + } + } + } return new AnimationCurve(trimmedKeys.ToArray()) { @@ -235,4 +294,150 @@ public class AnimationClipTrimmerWindow : EditorWindow postWrapMode = curve.postWrapMode }; } + + /// + /// Object reference 커브에서 선택 구간만 잘라내고 시간을 0부터 다시 시작합니다. + /// + private static ObjectReferenceKeyframe[] TrimObjectReferenceCurve( + ObjectReferenceKeyframe[] sourceKeys, + float startTime, + float endTime) + { + if (sourceKeys == null || sourceKeys.Length == 0 || endTime < startTime) + return null; + + List trimmedKeys = new List(); + float duration = endTime - startTime; + + for (int i = 0; i < sourceKeys.Length; i++) + { + ObjectReferenceKeyframe key = sourceKeys[i]; + if (key.time >= startTime && key.time <= endTime) + { + trimmedKeys.Add(new ObjectReferenceKeyframe + { + time = key.time - startTime, + value = key.value + }); + } + } + + if (trimmedKeys.Count == 0) + { + ObjectReferenceKeyframe sampledKey = sourceKeys[0]; + for (int i = sourceKeys.Length - 1; i >= 0; i--) + { + if (sourceKeys[i].time <= startTime) + { + sampledKey = sourceKeys[i]; + break; + } + } + + trimmedKeys.Add(new ObjectReferenceKeyframe + { + time = 0f, + value = sampledKey.value + }); + } + + if (!Mathf.Approximately(trimmedKeys[0].time, 0f)) + { + trimmedKeys.Insert(0, new ObjectReferenceKeyframe + { + time = 0f, + value = trimmedKeys[0].value + }); + } + + int lastIndex = trimmedKeys.Count - 1; + if (duration > Mathf.Epsilon && !Mathf.Approximately(trimmedKeys[lastIndex].time, duration)) + { + trimmedKeys.Add(new ObjectReferenceKeyframe + { + time = duration, + value = trimmedKeys[lastIndex].value + }); + } + + return trimmedKeys.ToArray(); + } + + /// + /// 원본 클립의 루트 모션/루프/미러 설정을 새 클립에 유지합니다. + /// + private static void CopyClipSettings(AnimationClip source, AnimationClip target, float duration) + { + if (source == null || target == null) + return; + + SerializedObject sourceSerializedObject = new SerializedObject(source); + SerializedObject targetSerializedObject = new SerializedObject(target); + + SerializedProperty sourceSettings = sourceSerializedObject.FindProperty("m_AnimationClipSettings"); + SerializedProperty targetSettings = targetSerializedObject.FindProperty("m_AnimationClipSettings"); + if (sourceSettings == null || targetSettings == null) + return; + + string[] relativePropertyNames = + { + "m_AdditiveReferencePoseClip", + "m_AdditiveReferencePoseTime", + "m_OrientationOffsetY", + "m_Level", + "m_CycleOffset", + "m_HasAdditiveReferencePose", + "m_LoopTime", + "m_LoopBlend", + "m_LoopBlendOrientation", + "m_LoopBlendPositionY", + "m_LoopBlendPositionXZ", + "m_KeepOriginalOrientation", + "m_KeepOriginalPositionY", + "m_KeepOriginalPositionXZ", + "m_HeightFromFeet", + "m_Mirror" + }; + + for (int i = 0; i < relativePropertyNames.Length; i++) + { + SerializedProperty sourceProperty = sourceSettings.FindPropertyRelative(relativePropertyNames[i]); + SerializedProperty targetProperty = targetSettings.FindPropertyRelative(relativePropertyNames[i]); + if (sourceProperty == null || targetProperty == null) + continue; + + CopySerializedPropertyValue(sourceProperty, targetProperty); + } + + SerializedProperty startTimeProperty = targetSettings.FindPropertyRelative("m_StartTime"); + if (startTimeProperty != null) + startTimeProperty.floatValue = 0f; + + SerializedProperty stopTimeProperty = targetSettings.FindPropertyRelative("m_StopTime"); + if (stopTimeProperty != null) + stopTimeProperty.floatValue = Mathf.Max(0f, duration); + + targetSerializedObject.ApplyModifiedPropertiesWithoutUndo(); + } + + private static void CopySerializedPropertyValue(SerializedProperty source, SerializedProperty target) + { + switch (source.propertyType) + { + case SerializedPropertyType.Integer: + target.intValue = source.intValue; + break; + case SerializedPropertyType.Boolean: + target.boolValue = source.boolValue; + break; + case SerializedPropertyType.Float: + target.floatValue = source.floatValue; + break; + case SerializedPropertyType.ObjectReference: + target.objectReferenceValue = source.objectReferenceValue; + break; + default: + break; + } + } }