feat: 기본기3 브릿지 클립 및 애니메이션 편집 유틸 추가
- 기본기3 1타 브릿지 클립과 패턴/스킬 자산을 정리해 1타-2타 연결 흐름을 보강 - 애니메이션 브릿지 클립 생성 에디터 창을 추가해 앞뒤 프레임을 잘라 새 클립을 만들 수 있게 지원 - 선택한 애니메이션 클립의 마지막 프레임에 누락 키를 일괄 추가하는 에디터 메뉴를 추가
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cde2d1a448bf3be59b2c9bbebe2ad7e3
|
||||
guid: ead3ca0dff8c0a3468e3234269c4a62e
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
|
||||
16180
Assets/_Game/Animations/Anim_Drog_콤보-기본기3_1_1.anim
Normal file
16180
Assets/_Game/Animations/Anim_Drog_콤보-기본기3_1_1.anim
Normal file
File diff suppressed because it is too large
Load Diff
8
Assets/_Game/Animations/Anim_Drog_콤보-기본기3_1_1.anim.meta
Normal file
8
Assets/_Game/Animations/Anim_Drog_콤보-기본기3_1_1.anim.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d13751966b5403386ab7a397454197e2
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -32,13 +32,6 @@ MonoBehaviour:
|
||||
requiredDamageRatio: 0.1
|
||||
telegraphAbnormality: {fileID: 0}
|
||||
staggerDuration: 2
|
||||
- Type: 0
|
||||
Skill: {fileID: 0}
|
||||
Duration: 0
|
||||
ChargeData:
|
||||
requiredDamageRatio: 0.1
|
||||
telegraphAbnormality: {fileID: 0}
|
||||
staggerDuration: 2
|
||||
cooldown: 3.25
|
||||
minPhase: 1
|
||||
skipJumpStepOnNoTarget: 0
|
||||
|
||||
@@ -19,7 +19,8 @@ MonoBehaviour:
|
||||
activationType: 1
|
||||
baseTypes: 1
|
||||
animationClips:
|
||||
- {fileID: 7400000, guid: cde2d1a448bf3be59b2c9bbebe2ad7e3, type: 2}
|
||||
- {fileID: 7400000, guid: ead3ca0dff8c0a3468e3234269c4a62e, type: 2}
|
||||
- {fileID: 7400000, guid: d13751966b5403386ab7a397454197e2, type: 2}
|
||||
animationSpeed: 1
|
||||
useRootMotion: 1
|
||||
ignoreRootMotionY: 1
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
using UnityEditor;
|
||||
|
||||
/// <summary>
|
||||
/// 두 AnimationClip의 경계 포즈를 샘플링해 브릿지용 AnimationClip을 생성합니다.
|
||||
/// 독립 .anim 클립 기준으로 사용하며, 생성 후 수동 미세 조정을 권장합니다.
|
||||
/// </summary>
|
||||
public class AnimationBridgeClipGeneratorWindow : EditorWindow
|
||||
{
|
||||
private const string MenuPath = "Tools/Animation/Generate Bridge Clip";
|
||||
|
||||
private AnimationClip sourceClip;
|
||||
private AnimationClip targetClip;
|
||||
private int sourceEndTrimFrames;
|
||||
private int targetStartTrimFrames;
|
||||
private int bridgeDurationFrames = 4;
|
||||
private string outputName = string.Empty;
|
||||
private string outputDirectory = "Assets/_Game/Animations";
|
||||
|
||||
[MenuItem(MenuPath, false, 22)]
|
||||
public static void ShowWindow()
|
||||
{
|
||||
AnimationClip[] selectedClips = Selection.GetFiltered<AnimationClip>(SelectionMode.Assets);
|
||||
|
||||
AnimationBridgeClipGeneratorWindow window = GetWindow<AnimationBridgeClipGeneratorWindow>("Bridge Clip");
|
||||
window.minSize = new Vector2(380f, 280f);
|
||||
window.InitializeFromSelection(selectedClips);
|
||||
window.Show();
|
||||
}
|
||||
|
||||
private void InitializeFromSelection(AnimationClip[] selectedClips)
|
||||
{
|
||||
if (selectedClips == null || selectedClips.Length < 2)
|
||||
return;
|
||||
|
||||
sourceClip = selectedClips[0];
|
||||
targetClip = selectedClips[1];
|
||||
|
||||
string sourcePath = AssetDatabase.GetAssetPath(sourceClip);
|
||||
if (!string.IsNullOrEmpty(sourcePath))
|
||||
outputDirectory = Path.GetDirectoryName(sourcePath)?.Replace("\\", "/") ?? outputDirectory;
|
||||
|
||||
RefreshOutputName();
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
EditorGUILayout.Space(8f);
|
||||
EditorGUILayout.LabelField("브릿지 클립 생성", EditorStyles.boldLabel);
|
||||
EditorGUILayout.HelpBox(
|
||||
"앞 클립의 끝 포즈와 뒤 클립의 시작 포즈를 샘플링해 짧은 브릿지 클립을 생성합니다.\n" +
|
||||
"Humanoid 클립은 생성 후 수동 미세 조정을 권장합니다.",
|
||||
MessageType.Info);
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
sourceClip = (AnimationClip)EditorGUILayout.ObjectField("앞 클립", sourceClip, typeof(AnimationClip), false);
|
||||
targetClip = (AnimationClip)EditorGUILayout.ObjectField("뒤 클립", targetClip, typeof(AnimationClip), false);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
RefreshOutputName();
|
||||
|
||||
using (new EditorGUI.DisabledScope(sourceClip == null))
|
||||
{
|
||||
sourceEndTrimFrames = Mathf.Max(0, EditorGUILayout.IntField("앞 클립 끝에서 제외 프레임", sourceEndTrimFrames));
|
||||
}
|
||||
|
||||
using (new EditorGUI.DisabledScope(targetClip == null))
|
||||
{
|
||||
targetStartTrimFrames = Mathf.Max(0, EditorGUILayout.IntField("뒤 클립 시작에서 제외 프레임", targetStartTrimFrames));
|
||||
}
|
||||
|
||||
bridgeDurationFrames = Mathf.Max(1, EditorGUILayout.IntField("브릿지 길이(프레임)", bridgeDurationFrames));
|
||||
outputName = EditorGUILayout.TextField("출력 이름", outputName);
|
||||
outputDirectory = EditorGUILayout.TextField("출력 폴더", outputDirectory);
|
||||
|
||||
EditorGUILayout.Space(8f);
|
||||
|
||||
if (sourceClip != null)
|
||||
DrawClipSummary("앞 클립 샘플", sourceClip, sourceEndTrimFrames, useEndSample: true);
|
||||
|
||||
if (targetClip != null)
|
||||
DrawClipSummary("뒤 클립 샘플", targetClip, targetStartTrimFrames, useEndSample: false);
|
||||
|
||||
EditorGUILayout.Space(12f);
|
||||
|
||||
using (new EditorGUI.DisabledScope(!CanGenerate()))
|
||||
{
|
||||
if (GUILayout.Button("브릿지 클립 생성", GUILayout.Height(32f)))
|
||||
GenerateBridgeClip();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawClipSummary(string label, AnimationClip clip, int trimFrames, bool useEndSample)
|
||||
{
|
||||
float sampleTime = GetSampleTime(clip, trimFrames, useEndSample);
|
||||
int totalFrames = Mathf.RoundToInt(clip.length * clip.frameRate);
|
||||
int sampleFrame = Mathf.RoundToInt(sampleTime * clip.frameRate);
|
||||
|
||||
EditorGUILayout.LabelField(label, EditorStyles.boldLabel);
|
||||
EditorGUILayout.LabelField(
|
||||
$" {clip.name} / {clip.frameRate:F0} FPS / {clip.length:F3}초 / {totalFrames}프레임");
|
||||
EditorGUILayout.LabelField($" 샘플 프레임: {sampleFrame} / 샘플 시간: {sampleTime:F3}초");
|
||||
}
|
||||
|
||||
private bool CanGenerate()
|
||||
{
|
||||
if (sourceClip == null || targetClip == null)
|
||||
return false;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(outputName) || string.IsNullOrWhiteSpace(outputDirectory))
|
||||
return false;
|
||||
|
||||
return AssetDatabase.IsValidFolder(outputDirectory);
|
||||
}
|
||||
|
||||
private void RefreshOutputName()
|
||||
{
|
||||
if (sourceClip == null || targetClip == null)
|
||||
return;
|
||||
|
||||
outputName = $"{sourceClip.name}_to_{targetClip.name}_Bridge";
|
||||
}
|
||||
|
||||
private void GenerateBridgeClip()
|
||||
{
|
||||
float frameRate = sourceClip != null ? sourceClip.frameRate : 30f;
|
||||
float duration = bridgeDurationFrames / frameRate;
|
||||
float sourceSampleTime = GetSampleTime(sourceClip, sourceEndTrimFrames, useEndSample: true);
|
||||
float targetSampleTime = GetSampleTime(targetClip, targetStartTrimFrames, useEndSample: false);
|
||||
|
||||
AnimationClip bridgeClip = new AnimationClip
|
||||
{
|
||||
name = outputName,
|
||||
frameRate = frameRate,
|
||||
legacy = sourceClip.legacy,
|
||||
wrapMode = WrapMode.Once,
|
||||
localBounds = sourceClip.localBounds
|
||||
};
|
||||
|
||||
HashSet<EditorCurveBinding> floatBindings = new HashSet<EditorCurveBinding>(new EditorCurveBindingComparer());
|
||||
AddCurveBindings(floatBindings, AnimationUtility.GetCurveBindings(sourceClip));
|
||||
AddCurveBindings(floatBindings, AnimationUtility.GetCurveBindings(targetClip));
|
||||
|
||||
foreach (EditorCurveBinding binding in floatBindings)
|
||||
{
|
||||
AnimationCurve sourceCurve = AnimationUtility.GetEditorCurve(sourceClip, binding);
|
||||
AnimationCurve targetCurve = AnimationUtility.GetEditorCurve(targetClip, binding);
|
||||
if (sourceCurve == null && targetCurve == null)
|
||||
continue;
|
||||
|
||||
float startValue = EvaluateCurveOrDefault(sourceCurve, sourceSampleTime);
|
||||
float endValue = EvaluateCurveOrDefault(targetCurve, targetSampleTime);
|
||||
AnimationCurve bridgeCurve = CreateBridgeCurve(startValue, endValue, duration);
|
||||
AnimationUtility.SetEditorCurve(bridgeClip, binding, bridgeCurve);
|
||||
}
|
||||
|
||||
HashSet<EditorCurveBinding> objectBindings = new HashSet<EditorCurveBinding>(new EditorCurveBindingComparer());
|
||||
AddCurveBindings(objectBindings, AnimationUtility.GetObjectReferenceCurveBindings(sourceClip));
|
||||
AddCurveBindings(objectBindings, AnimationUtility.GetObjectReferenceCurveBindings(targetClip));
|
||||
|
||||
foreach (EditorCurveBinding binding in objectBindings)
|
||||
{
|
||||
ObjectReferenceKeyframe[] sourceKeys = AnimationUtility.GetObjectReferenceCurve(sourceClip, binding);
|
||||
ObjectReferenceKeyframe[] targetKeys = AnimationUtility.GetObjectReferenceCurve(targetClip, binding);
|
||||
Object startValue = SampleObjectReferenceValue(sourceKeys, sourceSampleTime);
|
||||
Object endValue = SampleObjectReferenceValue(targetKeys, targetSampleTime);
|
||||
|
||||
if (startValue == null && endValue == null)
|
||||
continue;
|
||||
|
||||
ObjectReferenceKeyframe[] bridgeKeys = CreateBridgeObjectCurve(startValue, endValue, duration);
|
||||
AnimationUtility.SetObjectReferenceCurve(bridgeClip, binding, bridgeKeys);
|
||||
}
|
||||
|
||||
string savePath = AssetDatabase.GenerateUniqueAssetPath(
|
||||
Path.Combine(outputDirectory, outputName + ".anim").Replace("\\", "/"));
|
||||
|
||||
AssetDatabase.CreateAsset(bridgeClip, savePath);
|
||||
CopyClipSettings(sourceClip, bridgeClip, duration);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
EditorUtility.FocusProjectWindow();
|
||||
Selection.activeObject = bridgeClip;
|
||||
|
||||
Debug.Log(
|
||||
$"[AnimationBridgeClipGenerator] 브릿지 생성 완료: {sourceClip.name} -> {targetClip.name} " +
|
||||
$"(앞-{sourceEndTrimFrames}f / 뒤+{targetStartTrimFrames}f / 길이 {bridgeDurationFrames}f) => {savePath}");
|
||||
}
|
||||
|
||||
private static void AddCurveBindings(HashSet<EditorCurveBinding> destination, EditorCurveBinding[] bindings)
|
||||
{
|
||||
if (bindings == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < bindings.Length; i++)
|
||||
destination.Add(bindings[i]);
|
||||
}
|
||||
|
||||
private static float GetSampleTime(AnimationClip clip, int trimFrames, bool useEndSample)
|
||||
{
|
||||
if (clip == null)
|
||||
return 0f;
|
||||
|
||||
float frameRate = Mathf.Max(1f, clip.frameRate);
|
||||
int totalFrames = Mathf.Max(0, Mathf.RoundToInt(clip.length * frameRate));
|
||||
int clampedTrim = Mathf.Clamp(trimFrames, 0, totalFrames);
|
||||
int sampleFrame = useEndSample
|
||||
? Mathf.Max(0, totalFrames - clampedTrim)
|
||||
: clampedTrim;
|
||||
|
||||
float sampleTime = sampleFrame / frameRate;
|
||||
return Mathf.Clamp(sampleTime, 0f, clip.length);
|
||||
}
|
||||
|
||||
private static float EvaluateCurveOrDefault(AnimationCurve curve, float time)
|
||||
{
|
||||
return curve != null ? curve.Evaluate(time) : 0f;
|
||||
}
|
||||
|
||||
private static AnimationCurve CreateBridgeCurve(float startValue, float endValue, float duration)
|
||||
{
|
||||
AnimationCurve curve = new AnimationCurve(
|
||||
new Keyframe(0f, startValue),
|
||||
new Keyframe(duration, endValue));
|
||||
|
||||
for (int i = 0; i < curve.keys.Length; i++)
|
||||
{
|
||||
AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.ClampedAuto);
|
||||
AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.ClampedAuto);
|
||||
}
|
||||
|
||||
return curve;
|
||||
}
|
||||
|
||||
private static Object SampleObjectReferenceValue(ObjectReferenceKeyframe[] keys, float time)
|
||||
{
|
||||
if (keys == null || keys.Length == 0)
|
||||
return null;
|
||||
|
||||
Object value = keys[0].value;
|
||||
for (int i = 0; i < keys.Length; i++)
|
||||
{
|
||||
if (keys[i].time > time)
|
||||
break;
|
||||
|
||||
value = keys[i].value;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static ObjectReferenceKeyframe[] CreateBridgeObjectCurve(Object startValue, Object endValue, float duration)
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new ObjectReferenceKeyframe { time = 0f, value = startValue },
|
||||
new ObjectReferenceKeyframe { time = duration, value = endValue }
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 원본 클립의 루프/미러/루트 모션 설정을 유지합니다.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class EditorCurveBindingComparer : IEqualityComparer<EditorCurveBinding>
|
||||
{
|
||||
public bool Equals(EditorCurveBinding x, EditorCurveBinding y)
|
||||
{
|
||||
return x.path == y.path
|
||||
&& x.propertyName == y.propertyName
|
||||
&& x.type == y.type
|
||||
&& x.isPPtrCurve == y.isPPtrCurve;
|
||||
}
|
||||
|
||||
public int GetHashCode(EditorCurveBinding obj)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int hash = 17;
|
||||
hash = (hash * 31) + (obj.path != null ? obj.path.GetHashCode() : 0);
|
||||
hash = (hash * 31) + (obj.propertyName != null ? obj.propertyName.GetHashCode() : 0);
|
||||
hash = (hash * 31) + (obj.type != null ? obj.type.GetHashCode() : 0);
|
||||
hash = (hash * 31) + obj.isPPtrCurve.GetHashCode();
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f0f0b49f4c980df0fb386a85a3d7a869
|
||||
165
Assets/_Game/Scripts/Editor/AnimationClipEndKeyBaker.cs
Normal file
165
Assets/_Game/Scripts/Editor/AnimationClipEndKeyBaker.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
using UnityEditor;
|
||||
|
||||
/// <summary>
|
||||
/// 선택한 .anim 클립의 마지막 프레임에 모든 커브 키를 보강합니다.
|
||||
/// 애니메이션을 잘라서 연결할 때 마지막 포즈가 정확히 유지되도록 돕는 도구입니다.
|
||||
/// </summary>
|
||||
public static class AnimationClipEndKeyBaker
|
||||
{
|
||||
private const string MenuPath = "Tools/Animation/Bake End Frame Keys";
|
||||
|
||||
[MenuItem(MenuPath, false, 23)]
|
||||
public static void BakeSelectedClipEndKeys()
|
||||
{
|
||||
AnimationClip[] clips = Selection.GetFiltered<AnimationClip>(SelectionMode.Assets);
|
||||
if (clips == null || clips.Length == 0)
|
||||
{
|
||||
EditorUtility.DisplayDialog(
|
||||
"Bake End Frame Keys",
|
||||
"마지막 프레임 키를 보강할 AnimationClip을 1개 이상 선택해 주세요.",
|
||||
"확인");
|
||||
return;
|
||||
}
|
||||
|
||||
int processedClipCount = 0;
|
||||
int addedFloatKeyCount = 0;
|
||||
int addedObjectKeyCount = 0;
|
||||
List<string> skippedClips = new List<string>();
|
||||
|
||||
for (int i = 0; i < clips.Length; i++)
|
||||
{
|
||||
AnimationClip clip = clips[i];
|
||||
string clipPath = AssetDatabase.GetAssetPath(clip);
|
||||
if (clip == null || string.IsNullOrEmpty(clipPath) || !clipPath.EndsWith(".anim"))
|
||||
{
|
||||
skippedClips.Add(clip != null ? clip.name : "(null)");
|
||||
continue;
|
||||
}
|
||||
|
||||
processedClipCount++;
|
||||
float lastFrameTime = clip.length;
|
||||
|
||||
EditorCurveBinding[] floatBindings = AnimationUtility.GetCurveBindings(clip);
|
||||
for (int bindingIndex = 0; bindingIndex < floatBindings.Length; bindingIndex++)
|
||||
{
|
||||
EditorCurveBinding binding = floatBindings[bindingIndex];
|
||||
AnimationCurve curve = AnimationUtility.GetEditorCurve(clip, binding);
|
||||
if (curve == null)
|
||||
continue;
|
||||
|
||||
if (HasFloatKeyAtTime(curve, lastFrameTime))
|
||||
continue;
|
||||
|
||||
float value = curve.Evaluate(lastFrameTime);
|
||||
int keyIndex = curve.AddKey(new Keyframe(lastFrameTime, value));
|
||||
if (keyIndex >= 0)
|
||||
{
|
||||
AnimationUtility.SetKeyLeftTangentMode(curve, keyIndex, AnimationUtility.TangentMode.ClampedAuto);
|
||||
AnimationUtility.SetKeyRightTangentMode(curve, keyIndex, AnimationUtility.TangentMode.ClampedAuto);
|
||||
}
|
||||
|
||||
AnimationUtility.SetEditorCurve(clip, binding, curve);
|
||||
addedFloatKeyCount++;
|
||||
}
|
||||
|
||||
EditorCurveBinding[] objectBindings = AnimationUtility.GetObjectReferenceCurveBindings(clip);
|
||||
for (int bindingIndex = 0; bindingIndex < objectBindings.Length; bindingIndex++)
|
||||
{
|
||||
EditorCurveBinding binding = objectBindings[bindingIndex];
|
||||
ObjectReferenceKeyframe[] keys = AnimationUtility.GetObjectReferenceCurve(clip, binding);
|
||||
if (keys == null || keys.Length == 0)
|
||||
continue;
|
||||
|
||||
if (HasObjectKeyAtTime(keys, lastFrameTime))
|
||||
continue;
|
||||
|
||||
Object sampledValue = SampleObjectReferenceValue(keys, lastFrameTime);
|
||||
List<ObjectReferenceKeyframe> bakedKeys = new List<ObjectReferenceKeyframe>(keys)
|
||||
{
|
||||
new ObjectReferenceKeyframe
|
||||
{
|
||||
time = lastFrameTime,
|
||||
value = sampledValue
|
||||
}
|
||||
};
|
||||
|
||||
bakedKeys.Sort((a, b) => a.time.CompareTo(b.time));
|
||||
AnimationUtility.SetObjectReferenceCurve(clip, binding, bakedKeys.ToArray());
|
||||
addedObjectKeyCount++;
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(clip);
|
||||
AssetDatabase.ImportAsset(clipPath, ImportAssetOptions.ForceUpdate);
|
||||
}
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
string message =
|
||||
$"처리한 클립: {processedClipCount}개\n" +
|
||||
$"추가한 Float 키: {addedFloatKeyCount}개\n" +
|
||||
$"추가한 Object 키: {addedObjectKeyCount}개";
|
||||
|
||||
if (skippedClips.Count > 0)
|
||||
message += $"\n\n건너뛴 클립: {string.Join(", ", skippedClips)}";
|
||||
|
||||
Debug.Log($"[AnimationClipEndKeyBaker] {message.Replace('\n', ' ')}");
|
||||
EditorUtility.DisplayDialog("Bake End Frame Keys", message, "확인");
|
||||
}
|
||||
|
||||
[MenuItem(MenuPath, true)]
|
||||
public static bool ValidateBakeSelectedClipEndKeys()
|
||||
{
|
||||
return Selection.GetFiltered<AnimationClip>(SelectionMode.Assets).Length > 0;
|
||||
}
|
||||
|
||||
private static bool HasFloatKeyAtTime(AnimationCurve curve, float time)
|
||||
{
|
||||
if (curve == null)
|
||||
return false;
|
||||
|
||||
Keyframe[] keys = curve.keys;
|
||||
for (int i = 0; i < keys.Length; i++)
|
||||
{
|
||||
if (Mathf.Abs(keys[i].time - time) <= 0.0001f)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool HasObjectKeyAtTime(ObjectReferenceKeyframe[] keys, float time)
|
||||
{
|
||||
if (keys == null)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < keys.Length; i++)
|
||||
{
|
||||
if (Mathf.Abs(keys[i].time - time) <= 0.0001f)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Object SampleObjectReferenceValue(ObjectReferenceKeyframe[] keys, float time)
|
||||
{
|
||||
if (keys == null || keys.Length == 0)
|
||||
return null;
|
||||
|
||||
Object value = keys[0].value;
|
||||
for (int i = 0; i < keys.Length; i++)
|
||||
{
|
||||
if (keys[i].time > time)
|
||||
break;
|
||||
|
||||
value = keys[i].value;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2b4cdd66881753313932344805cb72c3
|
||||
Reference in New Issue
Block a user