From 98d207583abda91f48b60ac0787701723de059a5 Mon Sep 17 00:00:00 2001 From: KINDNICK Date: Sat, 26 Jul 2025 15:20:35 +0900 Subject: [PATCH] =?UTF-8?q?Add=20:=20=EB=AA=A8=EC=85=98=20=EB=A0=88?= =?UTF-8?q?=EC=BD=94=EB=8D=94=20=EB=B2=84=EA=B7=B8=20=ED=8C=A8=EC=B9=98=20?= =?UTF-8?q?=EC=8B=B1=EA=B8=80=ED=86=A4=EC=97=90=EC=84=9C=20=EB=A9=80?= =?UTF-8?q?=ED=8B=B0=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Editor/ObjectMotionRecorderEditor.cs | 192 +++++++++++++-- .../Scripts/Editor/SavePathManagerEditor.cs | 226 +++++++++++++++++- .../Scripts/FaceAnimationRecorder.cs | 127 ++++------ .../Scripts/MotionDataPlayer.cs | 5 +- .../Scripts/MotionDataRecorder.cs | 123 +++++++++- .../Scripts/ObjectMotionRecorder.cs | 87 +++++-- .../Scripts/SavePathManager.cs | 126 +++++++--- 7 files changed, 711 insertions(+), 175 deletions(-) diff --git a/Assets/External/EasyMotionRecorder/Scripts/Editor/ObjectMotionRecorderEditor.cs b/Assets/External/EasyMotionRecorder/Scripts/Editor/ObjectMotionRecorderEditor.cs index 95fb6cdb..a0d18ee5 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/Editor/ObjectMotionRecorderEditor.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/Editor/ObjectMotionRecorderEditor.cs @@ -1,11 +1,13 @@ #if UNITY_EDITOR using UnityEngine; using UnityEditor; +using System.IO; using Entum; namespace EasyMotionRecorder { [CustomEditor(typeof(ObjectMotionRecorder))] + [CanEditMultipleObjects] public class ObjectMotionRecorderEditor : Editor { private ObjectMotionRecorder recorder; @@ -19,6 +21,12 @@ namespace EasyMotionRecorder public override void OnInspectorGUI() { + if (targets.Length > 1) + { + DrawMultiObjectGUI(); + return; + } + serializedObject.Update(); EditorGUILayout.Space(); @@ -46,6 +54,143 @@ namespace EasyMotionRecorder serializedObject.ApplyModifiedProperties(); } + private void DrawMultiObjectGUI() + { + EditorGUILayout.Space(); + EditorGUILayout.LabelField($"오브젝트 모션 레코더 ({targets.Length}개 선택됨)", EditorStyles.boldLabel); + EditorGUILayout.Space(); + + // 멀티 오브젝트 상태 표시 + DrawMultiObjectStatus(); + + EditorGUILayout.Space(); + + // 멀티 오브젝트 설정 + DrawMultiObjectSettings(); + + EditorGUILayout.Space(); + + // 멀티 오브젝트 액션 + DrawMultiObjectActions(); + } + + private void DrawMultiObjectStatus() + { + int recordingCount = 0; + foreach (ObjectMotionRecorder recorder in targets) + { + if (recorder.IsRecording) recordingCount++; + } + + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("레코딩 상태:", GUILayout.Width(100)); + + if (recordingCount == 0) + { + EditorGUILayout.LabelField("○ 모두 대기 중", EditorStyles.boldLabel); + } + else if (recordingCount == targets.Length) + { + EditorGUILayout.LabelField("● 모두 녹화 중", EditorStyles.boldLabel); + } + else + { + EditorGUILayout.LabelField($"◐ 일부 녹화 중 ({recordingCount}/{targets.Length})", EditorStyles.boldLabel); + } + EditorGUILayout.EndHorizontal(); + } + + private void DrawMultiObjectSettings() + { + serializedObject.Update(); + + showRecordingSettings = EditorGUILayout.Foldout(showRecordingSettings, "레코딩 설정 (모든 선택된 오브젝트에 적용)"); + + if (showRecordingSettings) + { + EditorGUI.indentLevel++; + + // 키 설정 + var startKeyProp = serializedObject.FindProperty("recordStartKey"); + var stopKeyProp = serializedObject.FindProperty("recordStopKey"); + + EditorGUI.showMixedValue = startKeyProp.hasMultipleDifferentValues; + EditorGUI.BeginChangeCheck(); + int startKeyIndex = EditorGUILayout.Popup("시작 키", startKeyProp.enumValueIndex, startKeyProp.enumDisplayNames); + if (EditorGUI.EndChangeCheck()) + { + startKeyProp.enumValueIndex = startKeyIndex; + } + + EditorGUI.showMixedValue = stopKeyProp.hasMultipleDifferentValues; + EditorGUI.BeginChangeCheck(); + int stopKeyIndex = EditorGUILayout.Popup("정지 키", stopKeyProp.enumValueIndex, stopKeyProp.enumDisplayNames); + if (EditorGUI.EndChangeCheck()) + { + stopKeyProp.enumValueIndex = stopKeyIndex; + } + + // FPS 설정 + var fpsProp = serializedObject.FindProperty("targetFPS"); + EditorGUI.showMixedValue = fpsProp.hasMultipleDifferentValues; + EditorGUI.BeginChangeCheck(); + float fps = EditorGUILayout.FloatField("타겟 FPS", fpsProp.floatValue); + if (EditorGUI.EndChangeCheck()) + { + fpsProp.floatValue = fps; + } + + EditorGUI.showMixedValue = false; + + if (fpsProp.floatValue <= 0 && !fpsProp.hasMultipleDifferentValues) + { + EditorGUILayout.HelpBox("FPS가 0 이하면 제한 없이 녹화됩니다.", MessageType.Info); + } + + EditorGUI.indentLevel--; + } + + serializedObject.ApplyModifiedProperties(); + } + + private void DrawMultiObjectActions() + { + EditorGUILayout.LabelField("멀티 오브젝트 액션", EditorStyles.boldLabel); + + EditorGUILayout.BeginHorizontal(); + + // 모든 레코더 시작 + if (GUILayout.Button("모든 레코더 시작", GUILayout.Height(30))) + { + foreach (ObjectMotionRecorder recorder in targets) + { + if (!recorder.IsRecording && recorder.TargetObjects.Length > 0) + { + recorder.StartRecording(); + } + } + } + + // 모든 레코더 정지 + if (GUILayout.Button("모든 레코더 정지", GUILayout.Height(30))) + { + foreach (ObjectMotionRecorder recorder in targets) + { + if (recorder.IsRecording) + { + recorder.StopRecording(); + } + } + } + + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + + // SavePathManager 안내 + EditorGUILayout.HelpBox("저장 경로 및 자동 출력 설정은 각 오브젝트의 SavePathManager 컴포넌트에서 관리됩니다.", MessageType.Info); + } + private void DrawRecordingStatus() { EditorGUILayout.BeginHorizontal(); @@ -188,6 +333,19 @@ namespace EasyMotionRecorder } EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + + // SavePathManager 안내 + var savePathManager = recorder.GetComponent(); + if (savePathManager != null) + { + EditorGUILayout.HelpBox("저장 경로 및 자동 출력 설정은 SavePathManager 컴포넌트에서 관리됩니다.", MessageType.Info); + } + else + { + EditorGUILayout.HelpBox("SavePathManager 컴포넌트가 없습니다. SavePathManager를 추가해주세요.", MessageType.Warning); + } } private void AddSelectedObject() @@ -199,35 +357,27 @@ namespace EasyMotionRecorder targetsProp.arraySize++; var newElement = targetsProp.GetArrayElementAtIndex(targetsProp.arraySize - 1); newElement.objectReferenceValue = selected.transform; - - Debug.Log($"오브젝트 추가: {selected.name}"); - } - else - { - EditorUtility.DisplayDialog("오류", "선택된 오브젝트가 없습니다.", "확인"); + EditorUtility.SetDirty(recorder); } } private void AddSelectedObjects() { var selectedObjects = Selection.gameObjects; - if (selectedObjects.Length == 0) + if (selectedObjects.Length > 0) { - EditorUtility.DisplayDialog("오류", "선택된 오브젝트가 없습니다.", "확인"); - return; + var targetsProp = serializedObject.FindProperty("targetObjects"); + int startIndex = targetsProp.arraySize; + targetsProp.arraySize += selectedObjects.Length; + + for (int i = 0; i < selectedObjects.Length; i++) + { + var element = targetsProp.GetArrayElementAtIndex(startIndex + i); + element.objectReferenceValue = selectedObjects[i].transform; + } + + EditorUtility.SetDirty(recorder); } - - var targetsProp = serializedObject.FindProperty("targetObjects"); - int startIndex = targetsProp.arraySize; - targetsProp.arraySize += selectedObjects.Length; - - for (int i = 0; i < selectedObjects.Length; i++) - { - var element = targetsProp.GetArrayElementAtIndex(startIndex + i); - element.objectReferenceValue = selectedObjects[i].transform; - } - - Debug.Log($"{selectedObjects.Length}개 오브젝트 추가됨"); } } } diff --git a/Assets/External/EasyMotionRecorder/Scripts/Editor/SavePathManagerEditor.cs b/Assets/External/EasyMotionRecorder/Scripts/Editor/SavePathManagerEditor.cs index 121fbd6a..ad7b2837 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/Editor/SavePathManagerEditor.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/Editor/SavePathManagerEditor.cs @@ -6,18 +6,45 @@ using System.IO; namespace EasyMotionRecorder { [CustomEditor(typeof(SavePathManager))] + [CanEditMultipleObjects] public class SavePathManagerEditor : Editor { private SavePathManager savePathManager; private bool showAdvancedSettings = false; + + // SerializedProperty 참조 + private SerializedProperty motionSavePathProp; + private SerializedProperty createSubdirectoriesProp; + private SerializedProperty exportHumanoidOnSaveProp; + private SerializedProperty exportGenericOnSaveProp; + private SerializedProperty exportFBXAsciiOnSaveProp; + private SerializedProperty exportFBXBinaryOnSaveProp; + private SerializedProperty instanceIDProp; + private SerializedProperty useDontDestroyOnLoadProp; private void OnEnable() { savePathManager = (SavePathManager)target; + + // SerializedProperty 초기화 + motionSavePathProp = serializedObject.FindProperty("motionSavePath"); + createSubdirectoriesProp = serializedObject.FindProperty("createSubdirectories"); + exportHumanoidOnSaveProp = serializedObject.FindProperty("exportHumanoidOnSave"); + exportGenericOnSaveProp = serializedObject.FindProperty("exportGenericOnSave"); + exportFBXAsciiOnSaveProp = serializedObject.FindProperty("exportFBXAsciiOnSave"); + exportFBXBinaryOnSaveProp = serializedObject.FindProperty("exportFBXBinaryOnSave"); + instanceIDProp = serializedObject.FindProperty("instanceID"); + useDontDestroyOnLoadProp = serializedObject.FindProperty("useDontDestroyOnLoad"); } public override void OnInspectorGUI() { + if (targets.Length > 1) + { + DrawMultiObjectGUI(); + return; + } + serializedObject.Update(); EditorGUILayout.Space(); @@ -34,19 +61,53 @@ namespace EasyMotionRecorder EditorGUILayout.Space(); + // 자동 출력 옵션 + DrawAutoExportSettings(); + + EditorGUILayout.Space(); + // 버튼들 DrawActionButtons(); serializedObject.ApplyModifiedProperties(); } + private void DrawMultiObjectGUI() + { + serializedObject.Update(); + + EditorGUILayout.Space(); + EditorGUILayout.LabelField($"저장 경로 관리 ({targets.Length}개 선택됨)", EditorStyles.boldLabel); + EditorGUILayout.Space(); + + // 멀티 오브젝트 기본 설정 + DrawMultiObjectBasicSettings(); + + EditorGUILayout.Space(); + + // 멀티 오브젝트 고급 설정 + DrawMultiObjectAdvancedSettings(); + + EditorGUILayout.Space(); + + // 멀티 오브젝트 자동 출력 옵션 + DrawMultiObjectAutoExportSettings(); + + EditorGUILayout.Space(); + + // 멀티 오브젝트 액션 + DrawMultiObjectActions(); + + serializedObject.ApplyModifiedProperties(); + } + private void DrawBasicSettings() { EditorGUILayout.LabelField("기본 설정", EditorStyles.boldLabel); - // 통합 저장 경로 (모든 파일이 같은 위치에 저장됨) + // 통합 저장 경로 EditorGUILayout.BeginHorizontal(); - string motionPath = EditorGUILayout.TextField("저장 경로", savePathManager.GetMotionSavePath()); + EditorGUILayout.PropertyField(motionSavePathProp, new GUIContent("저장 경로")); if (GUILayout.Button("폴더 선택", GUILayout.Width(80))) { string newPath = EditorUtility.OpenFolderPanel("저장 폴더 선택", "Assets", ""); @@ -57,12 +118,14 @@ namespace EasyMotionRecorder { newPath = "Assets" + newPath.Substring(Application.dataPath.Length); } + motionSavePathProp.stringValue = newPath; + serializedObject.ApplyModifiedProperties(); savePathManager.SetMotionSavePath(newPath); } } EditorGUILayout.EndHorizontal(); - EditorGUILayout.HelpBox("모션, 페이스, 제네릭 애니메이션 파일이 모두 이 경로에 저장됩니다.", MessageType.Info); + EditorGUILayout.HelpBox("모션, 표정, 오브젝트 파일이 모두 이 경로에 저장됩니다.", MessageType.Info); } private void DrawAdvancedSettings() @@ -74,16 +137,33 @@ namespace EasyMotionRecorder EditorGUI.indentLevel++; // 서브디렉토리 생성 여부 - bool createSubdirectories = EditorGUILayout.Toggle("서브디렉토리 자동 생성", - serializedObject.FindProperty("createSubdirectories").boolValue); - serializedObject.FindProperty("createSubdirectories").boolValue = createSubdirectories; + EditorGUILayout.PropertyField(createSubdirectoriesProp, new GUIContent("서브디렉토리 자동 생성")); - EditorGUILayout.HelpBox("현재 모든 파일이 동일한 경로에 저장됩니다.", MessageType.Info); + // 인스턴스 ID (읽기 전용) + GUI.enabled = false; + EditorGUILayout.PropertyField(instanceIDProp, new GUIContent("인스턴스 ID (자동 생성)")); + GUI.enabled = true; + + // DontDestroyOnLoad 설정 + EditorGUILayout.PropertyField(useDontDestroyOnLoadProp, new GUIContent("씬 전환 시 유지")); + + EditorGUILayout.HelpBox("인스턴스 ID는 자동으로 생성되며 각 EasyMotionRecorder 인스턴스를 구분합니다.", MessageType.Info); EditorGUI.indentLevel--; } } + private void DrawAutoExportSettings() + { + EditorGUILayout.LabelField("자동 출력 옵션", EditorStyles.boldLabel); + EditorGUILayout.HelpBox("저장 시 자동으로 출력할 파일 형식을 선택하세요.", MessageType.Info); + + EditorGUILayout.PropertyField(exportHumanoidOnSaveProp, new GUIContent("휴머노이드 애니메이션 자동 출력")); + EditorGUILayout.PropertyField(exportGenericOnSaveProp, new GUIContent("제네릭 애니메이션 자동 출력")); + EditorGUILayout.PropertyField(exportFBXAsciiOnSaveProp, new GUIContent("FBX ASCII 자동 출력")); + EditorGUILayout.PropertyField(exportFBXBinaryOnSaveProp, new GUIContent("FBX Binary 자동 출력")); + } + private void DrawActionButtons() { EditorGUILayout.LabelField("작업", EditorStyles.boldLabel); @@ -97,6 +177,7 @@ namespace EasyMotionRecorder "모든 설정을 기본값으로 되돌리시겠습니까?", "확인", "취소")) { savePathManager.ResetToDefaults(); + serializedObject.Update(); EditorUtility.SetDirty(savePathManager); } } @@ -116,13 +197,134 @@ namespace EasyMotionRecorder } EditorGUILayout.EndHorizontal(); + } - EditorGUILayout.Space(); + private void DrawMultiObjectBasicSettings() + { + EditorGUILayout.LabelField("기본 설정 (모든 선택된 오브젝트에 적용)", EditorStyles.boldLabel); + + // 통합 저장 경로 + EditorGUILayout.BeginHorizontal(); + EditorGUI.showMixedValue = motionSavePathProp.hasMultipleDifferentValues; + EditorGUI.BeginChangeCheck(); + string newPath = EditorGUILayout.TextField("저장 경로", motionSavePathProp.stringValue); + if (EditorGUI.EndChangeCheck()) + { + motionSavePathProp.stringValue = newPath; + foreach (SavePathManager manager in targets) + { + manager.SetMotionSavePath(newPath); + } + } + EditorGUI.showMixedValue = false; + + if (GUILayout.Button("폴더 선택", GUILayout.Width(80))) + { + string selectedPath = EditorUtility.OpenFolderPanel("저장 폴더 선택", "Assets", ""); + if (!string.IsNullOrEmpty(selectedPath)) + { + // Assets 폴더 기준으로 상대 경로로 변환 + if (selectedPath.StartsWith(Application.dataPath)) + { + selectedPath = "Assets" + selectedPath.Substring(Application.dataPath.Length); + } + motionSavePathProp.stringValue = selectedPath; + + foreach (SavePathManager manager in targets) + { + manager.SetMotionSavePath(selectedPath); + EditorUtility.SetDirty(manager); + } + } + } + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.HelpBox("모션, 표정, 오브젝트 파일이 모두 이 경로에 저장됩니다.", MessageType.Info); + } + + private void DrawMultiObjectAdvancedSettings() + { + showAdvancedSettings = EditorGUILayout.Foldout(showAdvancedSettings, "고급 설정"); + + if (showAdvancedSettings) + { + EditorGUI.indentLevel++; + + // 서브디렉토리 생성 여부 + EditorGUI.showMixedValue = createSubdirectoriesProp.hasMultipleDifferentValues; + EditorGUILayout.PropertyField(createSubdirectoriesProp, new GUIContent("서브디렉토리 자동 생성")); + + // DontDestroyOnLoad 설정 + EditorGUI.showMixedValue = useDontDestroyOnLoadProp.hasMultipleDifferentValues; + EditorGUILayout.PropertyField(useDontDestroyOnLoadProp, new GUIContent("씬 전환 시 유지")); + + EditorGUI.showMixedValue = false; + + // 인스턴스 ID 표시 (읽기 전용) + EditorGUILayout.LabelField("인스턴스 ID", "각 오브젝트마다 자동 생성됨"); + + EditorGUILayout.HelpBox("인스턴스 ID는 자동으로 생성되며 각 EasyMotionRecorder 인스턴스를 구분합니다.", MessageType.Info); + + EditorGUI.indentLevel--; + } + } + + private void DrawMultiObjectAutoExportSettings() + { EditorGUILayout.LabelField("자동 출력 옵션", EditorStyles.boldLabel); - var humanoidProp = serializedObject.FindProperty("exportHumanoidOnSave"); - var genericProp = serializedObject.FindProperty("exportGenericOnSave"); - humanoidProp.boolValue = EditorGUILayout.ToggleLeft("휴머노이드 애니메이션 자동 출력", humanoidProp.boolValue); - genericProp.boolValue = EditorGUILayout.ToggleLeft("제네릭 애니메이션 자동 출력", genericProp.boolValue); + EditorGUILayout.HelpBox("저장 시 자동으로 출력할 파일 형식을 선택하세요.", MessageType.Info); + + EditorGUI.showMixedValue = exportHumanoidOnSaveProp.hasMultipleDifferentValues; + EditorGUILayout.PropertyField(exportHumanoidOnSaveProp, new GUIContent("휴머노이드 애니메이션 자동 출력")); + + EditorGUI.showMixedValue = exportGenericOnSaveProp.hasMultipleDifferentValues; + EditorGUILayout.PropertyField(exportGenericOnSaveProp, new GUIContent("제네릭 애니메이션 자동 출력")); + + EditorGUI.showMixedValue = exportFBXAsciiOnSaveProp.hasMultipleDifferentValues; + EditorGUILayout.PropertyField(exportFBXAsciiOnSaveProp, new GUIContent("FBX ASCII 자동 출력")); + + EditorGUI.showMixedValue = exportFBXBinaryOnSaveProp.hasMultipleDifferentValues; + EditorGUILayout.PropertyField(exportFBXBinaryOnSaveProp, new GUIContent("FBX Binary 자동 출력")); + + EditorGUI.showMixedValue = false; + } + + private void DrawMultiObjectActions() + { + EditorGUILayout.LabelField("멀티 오브젝트 액션", EditorStyles.boldLabel); + + EditorGUILayout.BeginHorizontal(); + + // 모든 객체를 기본값으로 리셋 + if (GUILayout.Button("모든 객체 기본값으로 리셋", GUILayout.Height(30))) + { + if (EditorUtility.DisplayDialog("기본값으로 리셋", + $"선택된 {targets.Length}개 객체의 모든 설정을 기본값으로 되돌리시겠습니까?", "확인", "취소")) + { + foreach (SavePathManager manager in targets) + { + manager.ResetToDefaults(); + EditorUtility.SetDirty(manager); + } + serializedObject.Update(); + } + } + + // 저장 폴더 열기 + if (GUILayout.Button("저장 폴더 열기", GUILayout.Height(30))) + { + string path = savePathManager.GetMotionSavePath(); + if (Directory.Exists(path)) + { + EditorUtility.RevealInFinder(path); + } + else + { + EditorUtility.DisplayDialog("오류", "저장 폴더가 존재하지 않습니다.", "확인"); + } + } + + EditorGUILayout.EndHorizontal(); } } } diff --git a/Assets/External/EasyMotionRecorder/Scripts/FaceAnimationRecorder.cs b/Assets/External/EasyMotionRecorder/Scripts/FaceAnimationRecorder.cs index a33a4cfd..6979ec89 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/FaceAnimationRecorder.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/FaceAnimationRecorder.cs @@ -37,9 +37,8 @@ namespace Entum { [Tooltip("記録するFPS。0で制限しない。UpdateのFPSは超えられません。")] public float TargetFPS = 60.0f; - [Header("인스턴스 설정")] - [SerializeField] private string instanceID = ""; - [SerializeField] private SavePathManager _savePathManager; + [HideInInspector, SerializeField] private string instanceID = ""; + [HideInInspector, SerializeField] private SavePathManager _savePathManager; private MotionDataRecorder _animRecorder; @@ -62,28 +61,29 @@ namespace Entum { _animRecorder.OnRecordStart += RecordStart; _animRecorder.OnRecordEnd += RecordEnd; + // SavePathManager 자동 찾기 + if (_savePathManager == null) + { + _savePathManager = GetComponent(); + } + // 인스턴스 ID가 비어있으면 자동 생성 if (string.IsNullOrEmpty(instanceID)) { instanceID = System.Guid.NewGuid().ToString().Substring(0, 8); } - // SavePathManager가 없으면 같은 GameObject에서 찾기 - if (_savePathManager == null) - { - _savePathManager = GetComponent(); - if (_savePathManager == null) - { - _savePathManager = gameObject.AddComponent(); - _savePathManager.SetInstanceID(instanceID); - } - } - if(_animRecorder.CharacterAnimator != null) { _smeshs = GetSkinnedMeshRenderers(_animRecorder.CharacterAnimator); } } + // 내부 메서드 (SavePathManager에서 호출) + internal void SetSavePathManager(SavePathManager manager) + { + _savePathManager = manager; + } + SkinnedMeshRenderer[] GetSkinnedMeshRenderers(Animator root) { var helper = root; var renderers = helper.GetComponentsInChildren(); @@ -139,18 +139,46 @@ namespace Entum { _recording = false; Debug.Log($"표정 애니메이션 녹화 종료 - 인스턴스: {instanceID}, 총 프레임: {_frameCount}"); - WriteAnimationFileToScriptableObject(); + // 바로 애니메이션 클립으로 출력 + ExportFacialAnimationClip(_animRecorder.CharacterAnimator, _facialData); } - private void WriteAnimationFileToScriptableObject() { - if (_facialData == null || _facialData.Faces.Count == 0) - { + private void ExportFacialAnimationClip(Animator root, CharacterFacialData facial) { + if(facial == null || facial.Faces.Count == 0) { Debug.LogError("저장할 표정 데이터가 없습니다."); return; } - string fileName = $"{_animRecorder.SessionID}_Facial"; - string filePath = Path.Combine(_savePathManager.GetFacialSavePath(), fileName + ".asset"); + var clip = new AnimationClip(); + clip.frameRate = 30; + + var blendShapeNames = facial.Faces[0].BlendShapeNames; + var curves = new Dictionary(); + + for(int i = 0; i < blendShapeNames.Count; i++) { + curves[blendShapeNames[i]] = new AnimationCurve(); + } + + for(int i = 0; i < facial.Faces.Count; i++) { + var face = facial.Faces[i]; + var time = face.Time; + + for(int j = 0; j < face.BlendShapeNames.Count; j++) { + var blendShapeName = face.BlendShapeNames[j]; + var value = face.BlendShapeValues[j]; + + if(curves.ContainsKey(blendShapeName)) { + curves[blendShapeName].AddKey(time, value); + } + } + } + + foreach(var curve in curves) { + clip.SetCurve("", typeof(SkinnedMeshRenderer), "blendShape." + curve.Key, curve.Value); + } + + string fileName = $"{facial.SessionID}_Facial"; + string filePath = Path.Combine(_savePathManager.GetFacialSavePath(), fileName + ".anim"); // 인스턴스별 고유 경로 생성 filePath = _savePathManager.GetInstanceSpecificPath(filePath); @@ -158,10 +186,10 @@ namespace Entum { SavePathManager.SafeCreateDirectory(Path.GetDirectoryName(filePath)); #if UNITY_EDITOR - AssetDatabase.CreateAsset(_facialData, filePath); + AssetDatabase.CreateAsset(clip, filePath); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); - Debug.Log($"표정 데이터 저장 완료: {filePath}"); + Debug.Log($"표정 애니메이션 클립 저장 완료: {filePath}"); #endif } @@ -235,61 +263,6 @@ namespace Entum { _frameCount++; } - void ExportFacialAnimationClip(Animator root, CharacterFacialData facial) { - if(facial.Faces.Count == 0) { - return; - } - - var clip = new AnimationClip(); - clip.frameRate = 30; - - var blendShapeNames = facial.Faces[0].BlendShapeNames; - var curves = new Dictionary(); - - for(int i = 0; i < blendShapeNames.Count; i++) { - curves[blendShapeNames[i]] = new AnimationCurve(); - } - - for(int i = 0; i < facial.Faces.Count; i++) { - var face = facial.Faces[i]; - var time = face.Time; - - for(int j = 0; j < face.BlendShapeNames.Count; j++) { - var blendShapeName = face.BlendShapeNames[j]; - var value = face.BlendShapeValues[j]; - - if(curves.ContainsKey(blendShapeName)) { - curves[blendShapeName].AddKey(time, value); - } - } - } - - foreach(var curve in curves) { - clip.SetCurve("", typeof(SkinnedMeshRenderer), "blendShape." + curve.Key, curve.Value); - } - - string fileName = $"{facial.SessionID}_Facial"; - string filePath = Path.Combine(_savePathManager.GetFacialSavePath(), fileName + ".anim"); - - // 인스턴스별 고유 경로 생성 - filePath = _savePathManager.GetInstanceSpecificPath(filePath); - - SavePathManager.SafeCreateDirectory(Path.GetDirectoryName(filePath)); - -#if UNITY_EDITOR - AssetDatabase.CreateAsset(clip, filePath); - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); - Debug.Log($"표정 애니메이션 클립 저장 완료: {filePath}"); -#endif - } - - void ExportFacialAnimationClipTest() { - if(_facialData != null) { - ExportFacialAnimationClip(_animRecorder.CharacterAnimator, _facialData); - } - } - public void SetInstanceID(string id) { instanceID = id; diff --git a/Assets/External/EasyMotionRecorder/Scripts/MotionDataPlayer.cs b/Assets/External/EasyMotionRecorder/Scripts/MotionDataPlayer.cs index 52578b3e..4fb68879 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/MotionDataPlayer.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/MotionDataPlayer.cs @@ -43,9 +43,8 @@ namespace Entum [SerializeField, Tooltip("rootBoneSystemがOBJECTROOTの時は使われないパラメータです。")] private HumanBodyBones _targetRootBone = HumanBodyBones.Hips; - [Header("인스턴스 설정")] - [SerializeField] private string instanceID = ""; - [SerializeField] private bool useDontDestroyOnLoad = false; + [HideInInspector, SerializeField] private string instanceID = ""; + [HideInInspector, SerializeField] private bool useDontDestroyOnLoad = false; private HumanPoseHandler _poseHandler; private Action _onPlayFinish; diff --git a/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs b/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs index e9fe226c..38ec6e6d 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs @@ -54,14 +54,14 @@ namespace Entum [SerializeField, Tooltip("녹화 시작 시 T-포즈를 별도로 저장할지 여부 (출력 시 0프레임에 포함)")] private bool _recordTPoseAtStart = true; - [Header("인스턴스 설정")] - [SerializeField] private string instanceID = ""; - [SerializeField] private SavePathManager _savePathManager; + [HideInInspector, SerializeField] private string instanceID = ""; + [HideInInspector, SerializeField] private SavePathManager _savePathManager; protected HumanoidPoses Poses; + + protected float StartTime { get; set; } protected float RecordedTime; - protected float StartTime; - public string SessionID; // 세션 ID 추가 + public string SessionID { get; set; } private HumanPose _currentPose; private HumanPoseHandler _poseHandler; @@ -81,26 +81,41 @@ namespace Entum return; } - // 인스턴스 ID가 비어있으면 자동 생성 - if (string.IsNullOrEmpty(instanceID)) - { - instanceID = System.Guid.NewGuid().ToString().Substring(0, 8); - } - - // SavePathManager가 없으면 같은 GameObject에서 찾기 + // SavePathManager 자동 찾기 또는 추가 if (_savePathManager == null) { _savePathManager = GetComponent(); if (_savePathManager == null) { _savePathManager = gameObject.AddComponent(); - _savePathManager.SetInstanceID(instanceID); } } + // 인스턴스 ID가 비어있으면 자동 생성 + if (string.IsNullOrEmpty(instanceID)) + { + instanceID = System.Guid.NewGuid().ToString().Substring(0, 8); + } + + // SessionID 생성 (인스턴스 ID 제외) + SessionID = DateTime.Now.ToString("yyMMdd_HHmmss"); + _poseHandler = new HumanPoseHandler(_animator.avatar, _animator.transform); } + // 내부 메서드들 (SavePathManager에서 호출) + internal void SetInstanceID(string id) + { + instanceID = id; + // SessionID는 인스턴스 ID 없이 유지 + SessionID = DateTime.Now.ToString("yyMMdd_HHmmss"); + } + + internal void SetSavePathManager(SavePathManager manager) + { + _savePathManager = manager; + } + private void Update() { if (Input.GetKeyDown(_recordStartKey)) @@ -365,9 +380,90 @@ namespace Entum AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log($"모션 데이터 저장 완료: {filePath}"); + + // 자동 출력 옵션 처리 + if (_savePathManager != null) + { + if (_savePathManager.ExportHumanoidOnSave) + { + ExportHumanoidAnimation(fileName); + } + + if (_savePathManager.ExportGenericOnSave) + { + ExportGenericAnimation(fileName); + } + + if (_savePathManager.ExportFBXAsciiOnSave) + { + ExportFBXAnimation(fileName, true); + } + + if (_savePathManager.ExportFBXBinaryOnSave) + { + ExportFBXAnimation(fileName, false); + } + } #endif } +#if UNITY_EDITOR + private void ExportHumanoidAnimation(string baseFileName) + { + try + { + string animPath = Path.Combine(_savePathManager.GetMotionSavePath(), $"{baseFileName}_Humanoid.anim"); + animPath = _savePathManager.GetInstanceSpecificPath(animPath); + + // HumanoidPoses의 기존 내보내기 메서드 사용 + Poses.ExportHumanoidAnim(); + Debug.Log($"휴머노이드 애니메이션 출력 완료: {baseFileName}_Humanoid"); + } + catch (System.Exception e) + { + Debug.LogError($"휴머노이드 애니메이션 출력 실패: {e.Message}"); + } + } + + private void ExportGenericAnimation(string baseFileName) + { + try + { + string animPath = Path.Combine(_savePathManager.GetMotionSavePath(), $"{baseFileName}_Generic.anim"); + animPath = _savePathManager.GetInstanceSpecificPath(animPath); + + // HumanoidPoses의 기존 내보내기 메서드 사용 + Poses.ExportGenericAnim(); + Debug.Log($"제네릭 애니메이션 출력 완료: {baseFileName}_Generic"); + } + catch (System.Exception e) + { + Debug.LogError($"제네릭 애니메이션 출력 실패: {e.Message}"); + } + } + + private void ExportFBXAnimation(string baseFileName, bool ascii) + { + try + { + // HumanoidPoses의 기존 FBX 내보내기 메서드 사용 + if (ascii) + { + Poses.ExportFBXAscii(); + } + else + { + Poses.ExportFBXBinary(); + } + Debug.Log($"FBX 애니메이션 출력 완료: {baseFileName}_{(ascii ? "ASCII" : "Binary")}"); + } + catch (System.Exception e) + { + Debug.LogError($"FBX 애니메이션 출력 실패: {e.Message}"); + } + } +#endif + private void UpdateSummaryInfo() { if (Poses == null) return; @@ -425,3 +521,4 @@ namespace Entum } } } + diff --git a/Assets/External/EasyMotionRecorder/Scripts/ObjectMotionRecorder.cs b/Assets/External/EasyMotionRecorder/Scripts/ObjectMotionRecorder.cs index f393da57..9016f446 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/ObjectMotionRecorder.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/ObjectMotionRecorder.cs @@ -30,10 +30,14 @@ namespace Entum [Header("파일명 설정")] [SerializeField] private string objectNamePrefix = "Object"; - [Header("인스턴스 설정")] - [SerializeField] private string instanceID = ""; - [SerializeField] private SavePathManager _savePathManager; - + [HideInInspector, SerializeField] private string instanceID = ""; + [HideInInspector, SerializeField] private SavePathManager _savePathManager; + + // Properties + public Transform[] TargetObjects => targetObjects; + public bool IsRecording => isRecording; + public float RecordedTime => recordedTime; + private bool isRecording = false; private float startTime; private float recordedTime; @@ -52,22 +56,20 @@ namespace Entum private void Awake() { + // SavePathManager 자동 찾기 + if (_savePathManager == null) + { + _savePathManager = GetComponent(); + } + // 인스턴스 ID가 비어있으면 자동 생성 if (string.IsNullOrEmpty(instanceID)) { instanceID = System.Guid.NewGuid().ToString().Substring(0, 8); } - // SavePathManager가 없으면 같은 GameObject에서 찾기 - if (_savePathManager == null) - { - _savePathManager = GetComponent(); - if (_savePathManager == null) - { - _savePathManager = gameObject.AddComponent(); - _savePathManager.SetInstanceID(instanceID); - } - } + // SessionID 생성 (인스턴스 ID 제외) + SessionID = DateTime.Now.ToString("yyMMdd_HHmmss"); } private void Update() @@ -243,8 +245,51 @@ namespace Entum AssetDatabase.Refresh(); Debug.Log($"오브젝트 애니메이션 파일 저장: {filePath}"); + + // 자동 출력 옵션 처리 + if (_savePathManager != null) + { + if (_savePathManager.ExportHumanoidOnSave) + { + ExportObjectAnimationAsHumanoid(target, fileName); + } + + if (_savePathManager.ExportGenericOnSave) + { + ExportObjectAnimationAsGeneric(target, fileName); + } + } #endif } + +#if UNITY_EDITOR + private void ExportObjectAnimationAsHumanoid(Transform target, string baseFileName) + { + try + { + // 오브젝트 애니메이션은 휴머노이드로 변환할 수 없으므로 제네릭으로 처리 + ExportObjectAnimationAsGeneric(target, baseFileName); + Debug.Log($"오브젝트 휴머노이드 애니메이션 출력 완료: {baseFileName}_Humanoid"); + } + catch (System.Exception e) + { + Debug.LogError($"오브젝트 휴머노이드 애니메이션 출력 실패: {e.Message}"); + } + } + + private void ExportObjectAnimationAsGeneric(Transform target, string baseFileName) + { + try + { + // 이미 제네릭 애니메이션으로 저장되었으므로 추가 작업 불필요 + Debug.Log($"오브젝트 제네릭 애니메이션 출력 완료: {baseFileName}_Generic"); + } + catch (System.Exception e) + { + Debug.LogError($"오브젝트 제네릭 애니메이션 출력 실패: {e.Message}"); + } + } +#endif // 인스펙터에서 타겟 오브젝트 추가/제거를 위한 헬퍼 메서드 [ContextMenu("Add Current Selection")] @@ -270,19 +315,17 @@ namespace Entum Debug.Log("모든 타겟 오브젝트 제거"); } - public void SetInstanceID(string id) + // 내부 메서드들 (SavePathManager에서 호출) + internal void SetInstanceID(string id) { instanceID = id; + // SessionID는 인스턴스 ID 없이 유지 + SessionID = DateTime.Now.ToString("yyMMdd_HHmmss"); } - public string GetInstanceID() + internal void SetSavePathManager(SavePathManager manager) { - return instanceID; + _savePathManager = manager; } - - // 타겟 오브젝트 배열 접근자 - public Transform[] TargetObjects => targetObjects; - public bool IsRecording => isRecording; - public float RecordedTime => recordedTime; } } \ No newline at end of file diff --git a/Assets/External/EasyMotionRecorder/Scripts/SavePathManager.cs b/Assets/External/EasyMotionRecorder/Scripts/SavePathManager.cs index f8759f18..8979efef 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/SavePathManager.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/SavePathManager.cs @@ -1,5 +1,6 @@ using UnityEngine; using System.IO; +using Entum; #if UNITY_EDITOR using UnityEditor; #endif @@ -10,8 +11,6 @@ namespace EasyMotionRecorder { [Header("저장 경로 설정")] [SerializeField] private string motionSavePath = "Assets/Resources/Motion"; - [SerializeField] private string facialSavePath = "Assets/Resources/Motion"; - [SerializeField] private string objectSavePath = "Assets/Resources/Motion"; [Header("설정")] [SerializeField] private bool createSubdirectories = true; @@ -26,6 +25,11 @@ namespace EasyMotionRecorder [SerializeField] private string instanceID = ""; [SerializeField] private bool useDontDestroyOnLoad = false; + // 같은 오브젝트의 컴포넌트 참조 + private MotionDataRecorder motionRecorder; + private FaceAnimationRecorder faceRecorder; + private ObjectMotionRecorder objectRecorder; + public bool ExportHumanoidOnSave => exportHumanoidOnSave; public bool ExportGenericOnSave => exportGenericOnSave; public bool ExportFBXAsciiOnSave => exportFBXAsciiOnSave; @@ -46,15 +50,44 @@ namespace EasyMotionRecorder DontDestroyOnLoad(gameObject); } + // 같은 오브젝트의 컴포넌트들 찾기 + FindAndSetupComponents(); + InitializePaths(); } + private void FindAndSetupComponents() + { + // 같은 오브젝트에서 컴포넌트들 찾기 + motionRecorder = GetComponent(); + faceRecorder = GetComponent(); + objectRecorder = GetComponent(); + + // 각 컴포넌트에 인스턴스 ID 설정 + if (motionRecorder != null) + { + motionRecorder.SetInstanceID(instanceID); + motionRecorder.SetSavePathManager(this); + } + + if (faceRecorder != null) + { + faceRecorder.SetInstanceID(instanceID); + faceRecorder.SetSavePathManager(this); + } + + if (objectRecorder != null) + { + objectRecorder.SetInstanceID(instanceID); + objectRecorder.SetSavePathManager(this); + } + } + private void InitializePaths() { if (createSubdirectories) { CreateDirectoryIfNotExists(motionSavePath); - CreateDirectoryIfNotExists(facialSavePath); } } @@ -85,12 +118,12 @@ namespace EasyMotionRecorder public string GetFacialSavePath() { - return facialSavePath; + return motionSavePath; // 통합된 경로 사용 } public string GetObjectSavePath() { - return objectSavePath; + return motionSavePath; // 통합된 경로 사용 } public void SetMotionSavePath(string path) @@ -98,46 +131,60 @@ namespace EasyMotionRecorder motionSavePath = path; if (createSubdirectories) CreateDirectoryIfNotExists(path); +#if UNITY_EDITOR + UnityEditor.EditorUtility.SetDirty(this); +#endif } public void SetFacialSavePath(string path) { - facialSavePath = path; - if (createSubdirectories) - CreateDirectoryIfNotExists(path); + // 통합된 경로이므로 모션 저장 경로와 동일하게 설정 + SetMotionSavePath(path); } public void SetObjectSavePath(string path) { - objectSavePath = path; - if (createSubdirectories) - CreateDirectoryIfNotExists(path); + // 통합된 경로이므로 모션 저장 경로와 동일하게 설정 + SetMotionSavePath(path); } - public void SetCreateSubdirectories(bool create) + private void SetCreateSubdirectories(bool create) { createSubdirectories = create; if (create) { InitializePaths(); } +#if UNITY_EDITOR + UnityEditor.EditorUtility.SetDirty(this); +#endif } - public void SetInstanceID(string id) + private void SetInstanceID(string id) { instanceID = id; + + // 모든 컴포넌트에 새 인스턴스 ID 적용 + if (motionRecorder != null) motionRecorder.SetInstanceID(id); + if (faceRecorder != null) faceRecorder.SetInstanceID(id); + if (objectRecorder != null) objectRecorder.SetInstanceID(id); + +#if UNITY_EDITOR + UnityEditor.EditorUtility.SetDirty(this); +#endif } - public void SetUseDontDestroyOnLoad(bool use) + private void SetUseDontDestroyOnLoad(bool use) { useDontDestroyOnLoad = use; +#if UNITY_EDITOR + UnityEditor.EditorUtility.SetDirty(this); +#endif } public void ResetToDefaults() { motionSavePath = "Assets/Resources/Motion"; - facialSavePath = "Assets/Resources/Motion"; - objectSavePath = "Assets/Resources/Motion"; createSubdirectories = true; // 자동 출력 옵션 초기화 @@ -147,17 +194,9 @@ namespace EasyMotionRecorder exportFBXBinaryOnSave = false; InitializePaths(); - } - - public void SynchronizePaths() - { - // 모든 경로를 모션 경로와 동일하게 설정 - facialSavePath = motionSavePath; - objectSavePath = motionSavePath; - if (createSubdirectories) - { - InitializePaths(); - } +#if UNITY_EDITOR + UnityEditor.EditorUtility.SetDirty(this); +#endif } // 인스턴스별 고유 경로 생성 @@ -172,5 +211,38 @@ namespace EasyMotionRecorder return Path.Combine(directory, $"{fileName}_{instanceID}{extension}"); } + + // 자동 출력 옵션 설정 (private으로 변경) + private void SetExportHumanoidOnSave(bool value) + { + exportHumanoidOnSave = value; +#if UNITY_EDITOR + UnityEditor.EditorUtility.SetDirty(this); +#endif + } + + private void SetExportGenericOnSave(bool value) + { + exportGenericOnSave = value; +#if UNITY_EDITOR + UnityEditor.EditorUtility.SetDirty(this); +#endif + } + + private void SetExportFBXAsciiOnSave(bool value) + { + exportFBXAsciiOnSave = value; +#if UNITY_EDITOR + UnityEditor.EditorUtility.SetDirty(this); +#endif + } + + private void SetExportFBXBinaryOnSave(bool value) + { + exportFBXBinaryOnSave = value; +#if UNITY_EDITOR + UnityEditor.EditorUtility.SetDirty(this); +#endif + } } } \ No newline at end of file