diff --git a/Assets/External/EasyMotionRecorder/Scripts/Editor/FaceAnimationRecorderEditor.cs b/Assets/External/EasyMotionRecorder/Scripts/Editor/FaceAnimationRecorderEditor.cs new file mode 100644 index 00000000..e45aa9c2 --- /dev/null +++ b/Assets/External/EasyMotionRecorder/Scripts/Editor/FaceAnimationRecorderEditor.cs @@ -0,0 +1,261 @@ +#if UNITY_EDITOR +using UnityEngine; +using UnityEditor; +using System.Linq; + +namespace Entum +{ + [CustomEditor(typeof(FaceAnimationRecorder))] + [CanEditMultipleObjects] + public class FaceAnimationRecorderEditor : Editor + { + private FaceAnimationRecorder faceRecorder; + private bool showAdvancedSettings = false; + private bool showExclusiveSettings = false; + + // SerializedProperty 참조 + private SerializedProperty recordFaceBlendshapesProp; + private SerializedProperty exclusiveBlendshapeNamesProp; + private SerializedProperty targetFPSProp; + private SerializedProperty instanceIDProp; + + private void OnEnable() + { + faceRecorder = (FaceAnimationRecorder)target; + + // SerializedProperty 초기화 + recordFaceBlendshapesProp = serializedObject.FindProperty("_recordFaceBlendshapes"); + exclusiveBlendshapeNamesProp = serializedObject.FindProperty("_exclusiveBlendshapeNames"); + targetFPSProp = serializedObject.FindProperty("TargetFPS"); + instanceIDProp = serializedObject.FindProperty("instanceID"); + } + + public override void OnInspectorGUI() + { + if (targets.Length > 1) + { + DrawMultiObjectGUI(); + return; + } + + serializedObject.Update(); + + EditorGUILayout.Space(); + EditorGUILayout.LabelField("표정 애니메이션 레코더", EditorStyles.boldLabel); + EditorGUILayout.Space(); + + // 기본 설정 + DrawBasicSettings(); + + EditorGUILayout.Space(); + + // 제외 설정 + DrawExclusiveSettings(); + + EditorGUILayout.Space(); + + // 고급 설정 + DrawAdvancedSettings(); + + EditorGUILayout.Space(); + + // 상태 정보 + DrawStatusInfo(); + + serializedObject.ApplyModifiedProperties(); + } + + private void DrawMultiObjectGUI() + { + serializedObject.Update(); + + EditorGUILayout.Space(); + EditorGUILayout.LabelField($"표정 애니메이션 레코더 ({targets.Length}개 선택됨)", EditorStyles.boldLabel); + EditorGUILayout.Space(); + + DrawMultiObjectBasicSettings(); + + EditorGUILayout.Space(); + + DrawMultiObjectExclusiveSettings(); + + EditorGUILayout.Space(); + + DrawMultiObjectAdvancedSettings(); + + serializedObject.ApplyModifiedProperties(); + } + + private void DrawBasicSettings() + { + EditorGUILayout.LabelField("기본 설정", EditorStyles.boldLabel); + + // 표정 기록 활성화 체크박스 + EditorGUILayout.PropertyField(recordFaceBlendshapesProp, + new GUIContent("표정 기록 활성화", "체크하면 모션 기록과 함께 표정 애니메이션도 기록됩니다.")); + + if (!recordFaceBlendshapesProp.boolValue) + { + EditorGUILayout.HelpBox("표정 기록이 비활성화되어 있습니다. 표정 애니메이션을 기록하려면 위 옵션을 체크하세요.", MessageType.Warning); + } + + // FPS 설정 + EditorGUILayout.PropertyField(targetFPSProp, + new GUIContent("기록 FPS", "표정 애니메이션을 기록할 FPS입니다. 0으로 설정하면 제한하지 않습니다.")); + } + + private void DrawExclusiveSettings() + { + showExclusiveSettings = EditorGUILayout.Foldout(showExclusiveSettings, "제외할 블렌드셰이프 설정"); + + if (showExclusiveSettings) + { + EditorGUI.indentLevel++; + + EditorGUILayout.HelpBox("립싱크나 특정 블렌드셰이프를 기록에서 제외하고 싶을 때 사용합니다.", MessageType.Info); + + EditorGUILayout.PropertyField(exclusiveBlendshapeNamesProp, + new GUIContent("제외할 블렌드셰이프 이름", "이 목록의 문자열을 포함하는 블렌드셰이프는 기록되지 않습니다."), true); + + if (exclusiveBlendshapeNamesProp.arraySize > 0) + { + EditorGUILayout.Space(5); + EditorGUILayout.LabelField("현재 제외 목록:", EditorStyles.miniLabel); + for (int i = 0; i < exclusiveBlendshapeNamesProp.arraySize; i++) + { + var element = exclusiveBlendshapeNamesProp.GetArrayElementAtIndex(i); + if (!string.IsNullOrEmpty(element.stringValue)) + { + EditorGUILayout.LabelField($"• {element.stringValue}", EditorStyles.miniLabel); + } + } + } + + EditorGUI.indentLevel--; + } + } + + private void DrawAdvancedSettings() + { + showAdvancedSettings = EditorGUILayout.Foldout(showAdvancedSettings, "고급 설정"); + + if (showAdvancedSettings) + { + EditorGUI.indentLevel++; + + // 인스턴스 ID (읽기 전용) + GUI.enabled = false; + EditorGUILayout.PropertyField(instanceIDProp, new GUIContent("인스턴스 ID (자동 생성)")); + GUI.enabled = true; + + EditorGUILayout.HelpBox("인스턴스 ID는 자동으로 생성되며 각 FaceAnimationRecorder 인스턴스를 구분합니다.", MessageType.Info); + + EditorGUI.indentLevel--; + } + } + + private void DrawStatusInfo() + { + EditorGUILayout.LabelField("상태 정보", EditorStyles.boldLabel); + + // MotionDataRecorder 연결 상태 확인 + var motionRecorder = faceRecorder.GetComponent(); + if (motionRecorder != null) + { + EditorGUILayout.HelpBox("✓ MotionDataRecorder가 연결되어 있습니다.", MessageType.Info); + } + else + { + EditorGUILayout.HelpBox("⚠ MotionDataRecorder가 필요합니다. 같은 GameObject에 추가해주세요.", MessageType.Warning); + } + + // SavePathManager 연결 상태 확인 + var savePathManager = faceRecorder.GetComponent(); + if (savePathManager != null) + { + EditorGUILayout.HelpBox("✓ SavePathManager가 연결되어 있습니다.", MessageType.Info); + } + else + { + EditorGUILayout.HelpBox("ℹ SavePathManager가 없습니다. 자동으로 생성됩니다.", MessageType.Info); + } + + // 블렌드셰이프 발견 상태 + if (motionRecorder?.CharacterAnimator != null) + { + var skinnedMeshRenderers = motionRecorder.CharacterAnimator.GetComponentsInChildren(); + int totalBlendShapes = 0; + foreach (var smr in skinnedMeshRenderers) + { + if (smr.sharedMesh != null) + { + totalBlendShapes += smr.sharedMesh.blendShapeCount; + } + } + + if (totalBlendShapes > 0) + { + EditorGUILayout.HelpBox($"✓ {totalBlendShapes}개의 블렌드셰이프를 발견했습니다.", MessageType.Info); + } + else + { + EditorGUILayout.HelpBox("⚠ 블렌드셰이프를 찾을 수 없습니다. 캐릭터에 블렌드셰이프가 있는지 확인하세요.", MessageType.Warning); + } + } + } + + private void DrawMultiObjectBasicSettings() + { + EditorGUILayout.LabelField("기본 설정 (모든 선택된 오브젝트에 적용)", EditorStyles.boldLabel); + + // 표정 기록 활성화 + EditorGUI.showMixedValue = recordFaceBlendshapesProp.hasMultipleDifferentValues; + EditorGUILayout.PropertyField(recordFaceBlendshapesProp, + new GUIContent("표정 기록 활성화")); + + // FPS 설정 + EditorGUI.showMixedValue = targetFPSProp.hasMultipleDifferentValues; + EditorGUILayout.PropertyField(targetFPSProp, + new GUIContent("기록 FPS")); + + EditorGUI.showMixedValue = false; + } + + private void DrawMultiObjectExclusiveSettings() + { + showExclusiveSettings = EditorGUILayout.Foldout(showExclusiveSettings, "제외할 블렌드셰이프 설정"); + + if (showExclusiveSettings) + { + EditorGUI.indentLevel++; + + EditorGUILayout.HelpBox("립싱크나 특정 블렌드셰이프를 기록에서 제외하고 싶을 때 사용합니다.", MessageType.Info); + + EditorGUI.showMixedValue = exclusiveBlendshapeNamesProp.hasMultipleDifferentValues; + EditorGUILayout.PropertyField(exclusiveBlendshapeNamesProp, + new GUIContent("제외할 블렌드셰이프 이름"), true); + EditorGUI.showMixedValue = false; + + EditorGUI.indentLevel--; + } + } + + private void DrawMultiObjectAdvancedSettings() + { + showAdvancedSettings = EditorGUILayout.Foldout(showAdvancedSettings, "고급 설정"); + + if (showAdvancedSettings) + { + EditorGUI.indentLevel++; + + // 인스턴스 ID 표시 (읽기 전용) + EditorGUILayout.LabelField("인스턴스 ID", "각 오브젝트마다 자동 생성됨"); + + EditorGUILayout.HelpBox("인스턴스 ID는 자동으로 생성되며 각 FaceAnimationRecorder 인스턴스를 구분합니다.", MessageType.Info); + + EditorGUI.indentLevel--; + } + } + } +} +#endif \ No newline at end of file diff --git a/Assets/External/EasyMotionRecorder/Scripts/Editor/FaceAnimationRecorderEditor.cs.meta b/Assets/External/EasyMotionRecorder/Scripts/Editor/FaceAnimationRecorderEditor.cs.meta new file mode 100644 index 00000000..80826a68 --- /dev/null +++ b/Assets/External/EasyMotionRecorder/Scripts/Editor/FaceAnimationRecorderEditor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 793f8f25531a10a45bbaf0ec019c20a3 \ No newline at end of file diff --git a/Assets/External/EasyMotionRecorder/Scripts/Editor/SavePathManagerEditor.cs b/Assets/External/EasyMotionRecorder/Scripts/Editor/SavePathManagerEditor.cs index ad7b2837..a010888c 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/Editor/SavePathManagerEditor.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/Editor/SavePathManagerEditor.cs @@ -2,6 +2,7 @@ using UnityEngine; using UnityEditor; using System.IO; +using System.Linq; namespace EasyMotionRecorder { @@ -11,6 +12,14 @@ namespace EasyMotionRecorder { private SavePathManager savePathManager; private bool showAdvancedSettings = false; + private bool showExportSettings = true; + private bool showComponentInfo = false; + + // 스타일 상수 + private GUIStyle headerStyle; + private GUIStyle subHeaderStyle; + private GUIStyle buttonStyle; + private GUIStyle iconButtonStyle; // SerializedProperty 참조 private SerializedProperty motionSavePathProp; @@ -37,8 +46,40 @@ namespace EasyMotionRecorder useDontDestroyOnLoadProp = serializedObject.FindProperty("useDontDestroyOnLoad"); } + private void InitializeStyles() + { + if (headerStyle == null) + { + headerStyle = new GUIStyle(EditorStyles.boldLabel) + { + fontSize = 14, + normal = { textColor = EditorGUIUtility.isProSkin ? Color.white : Color.black } + }; + + subHeaderStyle = new GUIStyle(EditorStyles.boldLabel) + { + fontSize = 12, + normal = { textColor = EditorGUIUtility.isProSkin ? new Color(0.7f, 0.7f, 0.7f) : new Color(0.3f, 0.3f, 0.3f) } + }; + + buttonStyle = new GUIStyle(GUI.skin.button) + { + fixedHeight = 28, + fontSize = 11 + }; + + iconButtonStyle = new GUIStyle(GUI.skin.button) + { + fixedHeight = 24, + fixedWidth = 24 + }; + } + } + public override void OnInspectorGUI() { + InitializeStyles(); + if (targets.Length > 1) { DrawMultiObjectGUI(); @@ -47,70 +88,57 @@ namespace EasyMotionRecorder serializedObject.Update(); - EditorGUILayout.Space(); - EditorGUILayout.LabelField("저장 경로 관리", EditorStyles.boldLabel); - EditorGUILayout.Space(); - - // 기본 설정 + DrawHeader(); DrawBasicSettings(); - - EditorGUILayout.Space(); - - // 고급 설정 + DrawExportSettings(); DrawAdvancedSettings(); - - EditorGUILayout.Space(); - - // 자동 출력 옵션 - DrawAutoExportSettings(); - - EditorGUILayout.Space(); - - // 버튼들 + DrawComponentInfo(); DrawActionButtons(); serializedObject.ApplyModifiedProperties(); } - private void DrawMultiObjectGUI() + private void DrawHeader() { - serializedObject.Update(); - - EditorGUILayout.Space(); - EditorGUILayout.LabelField($"저장 경로 관리 ({targets.Length}개 선택됨)", EditorStyles.boldLabel); - EditorGUILayout.Space(); - - // 멀티 오브젝트 기본 설정 - DrawMultiObjectBasicSettings(); + EditorGUILayout.Space(10); - EditorGUILayout.Space(); + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + EditorGUILayout.LabelField("📁 저장 경로 관리자", headerStyle); + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); - // 멀티 오브젝트 고급 설정 - DrawMultiObjectAdvancedSettings(); - - EditorGUILayout.Space(); - - // 멀티 오브젝트 자동 출력 옵션 - DrawMultiObjectAutoExportSettings(); - - EditorGUILayout.Space(); - - // 멀티 오브젝트 액션 - DrawMultiObjectActions(); - - serializedObject.ApplyModifiedProperties(); + EditorGUILayout.Space(5); + DrawSeparator(); + EditorGUILayout.Space(10); + } + + private void DrawSeparator() + { + var rect = EditorGUILayout.GetControlRect(false, 1); + rect.height = 1; + EditorGUI.DrawRect(rect, EditorGUIUtility.isProSkin ? new Color(0.2f, 0.2f, 0.2f) : new Color(0.7f, 0.7f, 0.7f)); } private void DrawBasicSettings() { - EditorGUILayout.LabelField("기본 설정", EditorStyles.boldLabel); + DrawSectionHeader("💾 기본 설정", true); + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.Space(5); // 통합 저장 경로 + EditorGUILayout.LabelField("저장 경로", subHeaderStyle); EditorGUILayout.BeginHorizontal(); - EditorGUILayout.PropertyField(motionSavePathProp, new GUIContent("저장 경로")); - if (GUILayout.Button("폴더 선택", GUILayout.Width(80))) + + EditorGUILayout.PropertyField(motionSavePathProp, GUIContent.none); + + // 📂 폴더 선택 버튼 + if (GUILayout.Button(new GUIContent("📂", "폴더 선택 다이얼로그 열기"), iconButtonStyle)) { - string newPath = EditorUtility.OpenFolderPanel("저장 폴더 선택", "Assets", ""); + string currentPath = motionSavePathProp.stringValue; + string startPath = !string.IsNullOrEmpty(currentPath) ? currentPath : "Assets"; + string newPath = EditorUtility.OpenFolderPanel("저장 폴더 선택", startPath, ""); if (!string.IsNullOrEmpty(newPath)) { // Assets 폴더 기준으로 상대 경로로 변환 @@ -123,57 +151,212 @@ namespace EasyMotionRecorder savePathManager.SetMotionSavePath(newPath); } } + + // 🔍 폴더 열기 버튼 + if (GUILayout.Button(new GUIContent("🔍", "파일 탐색기에서 폴더 열기"), iconButtonStyle)) + { + string path = savePathManager.GetMotionSavePath(); + if (Directory.Exists(path)) + { + EditorUtility.RevealInFinder(path); + } + else + { + EditorUtility.DisplayDialog("알림", "폴더가 아직 생성되지 않았습니다.", "확인"); + } + } + + // ➕ 폴더 생성 버튼 + if (GUILayout.Button(new GUIContent("➕", "폴더가 없으면 생성"), iconButtonStyle)) + { + string path = savePathManager.GetMotionSavePath(); + if (!Directory.Exists(path)) + { + try + { + Directory.CreateDirectory(path); + AssetDatabase.Refresh(); + EditorUtility.DisplayDialog("성공", $"폴더가 생성되었습니다:\n{path}", "확인"); + } + catch (System.Exception e) + { + EditorUtility.DisplayDialog("오류", $"폴더 생성 실패:\n{e.Message}", "확인"); + } + } + else + { + EditorUtility.DisplayDialog("알림", "폴더가 이미 존재합니다.", "확인"); + } + } + + // 🏠 기본 경로 버튼 + if (GUILayout.Button(new GUIContent("🏠", "기본 경로(Assets/Motion)로 설정"), iconButtonStyle)) + { + string defaultPath = "Assets/Motion"; + motionSavePathProp.stringValue = defaultPath; + serializedObject.ApplyModifiedProperties(); + savePathManager.SetMotionSavePath(defaultPath); + } + + // 📋 경로 복사 버튼 + if (GUILayout.Button(new GUIContent("📋", "경로를 클립보드에 복사"), iconButtonStyle)) + { + string path = savePathManager.GetMotionSavePath(); + EditorGUIUtility.systemCopyBuffer = path; + EditorUtility.DisplayDialog("복사 완료", $"경로가 클립보드에 복사되었습니다:\n{path}", "확인"); + } + EditorGUILayout.EndHorizontal(); - EditorGUILayout.HelpBox("모션, 표정, 오브젝트 파일이 모두 이 경로에 저장됩니다.", MessageType.Info); + EditorGUILayout.Space(5); + EditorGUILayout.HelpBox("📝 모션, 표정, 오브젝트 파일이 모두 이 경로에 저장됩니다.", MessageType.Info); + + EditorGUILayout.Space(5); + EditorGUILayout.EndVertical(); + EditorGUILayout.Space(10); + } + + private void DrawExportSettings() + { + showExportSettings = DrawSectionHeader("🚀 자동 출력 옵션", showExportSettings); + + if (showExportSettings) + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.Space(5); + + EditorGUILayout.HelpBox("💾 저장 시 자동으로 출력할 파일 형식을 선택하세요.", MessageType.Info); + EditorGUILayout.Space(3); + + // 그리드 레이아웃으로 옵션 나열 + EditorGUILayout.BeginHorizontal(); + + EditorGUILayout.BeginVertical(); + DrawToggleOption("🤖 휴머노이드", exportHumanoidOnSaveProp); + DrawToggleOption("🔧 제네릭", exportGenericOnSaveProp); + EditorGUILayout.EndVertical(); + + EditorGUILayout.BeginVertical(); + DrawToggleOption("📄 FBX ASCII", exportFBXAsciiOnSaveProp); + DrawToggleOption("📊 FBX Binary", exportFBXBinaryOnSaveProp); + EditorGUILayout.EndVertical(); + + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(5); + EditorGUILayout.EndVertical(); + EditorGUILayout.Space(10); + } + } + + private void DrawToggleOption(string label, SerializedProperty prop) + { + EditorGUILayout.BeginHorizontal(); + prop.boolValue = EditorGUILayout.Toggle(prop.boolValue, GUILayout.Width(15)); + EditorGUILayout.LabelField(label, GUILayout.ExpandWidth(true)); + EditorGUILayout.EndHorizontal(); } private void DrawAdvancedSettings() { - showAdvancedSettings = EditorGUILayout.Foldout(showAdvancedSettings, "고급 설정"); + showAdvancedSettings = DrawSectionHeader("⚙️ 고급 설정", showAdvancedSettings); if (showAdvancedSettings) { - EditorGUI.indentLevel++; + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.Space(5); // 서브디렉토리 생성 여부 - EditorGUILayout.PropertyField(createSubdirectoriesProp, new GUIContent("서브디렉토리 자동 생성")); + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("📁 서브디렉토리 자동 생성", GUILayout.Width(200)); + createSubdirectoriesProp.boolValue = EditorGUILayout.Toggle(createSubdirectoriesProp.boolValue); + EditorGUILayout.EndHorizontal(); - // 인스턴스 ID (읽기 전용) - GUI.enabled = false; - EditorGUILayout.PropertyField(instanceIDProp, new GUIContent("인스턴스 ID (자동 생성)")); - GUI.enabled = true; + EditorGUILayout.Space(3); // DontDestroyOnLoad 설정 - EditorGUILayout.PropertyField(useDontDestroyOnLoadProp, new GUIContent("씬 전환 시 유지")); + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("🔒 씬 전환 시 유지", GUILayout.Width(200)); + useDontDestroyOnLoadProp.boolValue = EditorGUILayout.Toggle(useDontDestroyOnLoadProp.boolValue); + EditorGUILayout.EndHorizontal(); - EditorGUILayout.HelpBox("인스턴스 ID는 자동으로 생성되며 각 EasyMotionRecorder 인스턴스를 구분합니다.", MessageType.Info); + EditorGUILayout.Space(5); - EditorGUI.indentLevel--; + // 인스턴스 ID (읽기 전용) + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("🆔 인스턴스 ID", GUILayout.Width(120)); + GUI.enabled = false; + EditorGUILayout.TextField(instanceIDProp.stringValue); + GUI.enabled = true; + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(3); + EditorGUILayout.HelpBox("📝 인스턴스 ID는 자동으로 생성되며 각 EasyMotionRecorder 인스턴스를 구분합니다.", MessageType.Info); + + EditorGUILayout.Space(5); + EditorGUILayout.EndVertical(); + EditorGUILayout.Space(10); } } - private void DrawAutoExportSettings() + private void DrawComponentInfo() { - EditorGUILayout.LabelField("자동 출력 옵션", EditorStyles.boldLabel); - EditorGUILayout.HelpBox("저장 시 자동으로 출력할 파일 형식을 선택하세요.", MessageType.Info); + showComponentInfo = DrawSectionHeader("🔍 컴포넌트 상태", showComponentInfo); - EditorGUILayout.PropertyField(exportHumanoidOnSaveProp, new GUIContent("휴머노이드 애니메이션 자동 출력")); - EditorGUILayout.PropertyField(exportGenericOnSaveProp, new GUIContent("제네릭 애니메이션 자동 출력")); - EditorGUILayout.PropertyField(exportFBXAsciiOnSaveProp, new GUIContent("FBX ASCII 자동 출력")); - EditorGUILayout.PropertyField(exportFBXBinaryOnSaveProp, new GUIContent("FBX Binary 자동 출력")); + if (showComponentInfo) + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.Space(5); + + // 연결된 컴포넌트들 확인 + var motionRecorder = savePathManager.GetComponent(); + var faceRecorder = savePathManager.GetComponent(); + var objectRecorder = savePathManager.GetComponent(); + + EditorGUILayout.LabelField("연결된 컴포넌트:", subHeaderStyle); + EditorGUILayout.Space(3); + + DrawComponentStatus("🎥 MotionDataRecorder", motionRecorder != null); + DrawComponentStatus("😀 FaceAnimationRecorder", faceRecorder != null); + DrawComponentStatus("📦 ObjectMotionRecorder", objectRecorder != null); + + EditorGUILayout.Space(5); + EditorGUILayout.EndVertical(); + EditorGUILayout.Space(10); + } + } + + private void DrawComponentStatus(string componentName, bool isConnected) + { + EditorGUILayout.BeginHorizontal(); + string status = isConnected ? "✅" : "❌"; + string statusText = isConnected ? "연결됨" : "비연결"; + EditorGUILayout.LabelField($"{status} {componentName}", GUILayout.Width(200)); + EditorGUILayout.LabelField(statusText, isConnected ? EditorStyles.miniLabel : EditorStyles.centeredGreyMiniLabel); + EditorGUILayout.EndHorizontal(); + } + + private bool DrawSectionHeader(string title, bool expanded) + { + EditorGUILayout.BeginHorizontal(); + expanded = EditorGUILayout.Foldout(expanded, title, true, subHeaderStyle); + EditorGUILayout.EndHorizontal(); + return expanded; } private void DrawActionButtons() { - EditorGUILayout.LabelField("작업", EditorStyles.boldLabel); + DrawSectionHeader("🚀 작업", true); + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.Space(5); EditorGUILayout.BeginHorizontal(); // 기본값으로 리셋 버튼 - if (GUILayout.Button("기본값으로 리셋", GUILayout.Height(30))) + if (GUILayout.Button("🔄 기본값으로 리셋", buttonStyle)) { - if (EditorUtility.DisplayDialog("기본값으로 리셋", + if (EditorUtility.DisplayDialog("설정 리셋", "모든 설정을 기본값으로 되돌리시겠습니까?", "확인", "취소")) { savePathManager.ResetToDefaults(); @@ -183,7 +366,7 @@ namespace EasyMotionRecorder } // 폴더 열기 버튼 - if (GUILayout.Button("저장 폴더 열기", GUILayout.Height(30))) + if (GUILayout.Button("📂 저장 폴더 열기", buttonStyle)) { string path = savePathManager.GetMotionSavePath(); if (Directory.Exists(path)) @@ -192,11 +375,92 @@ namespace EasyMotionRecorder } else { - EditorUtility.DisplayDialog("오류", "저장 폴더가 존재하지 않습니다.", "확인"); + if (EditorUtility.DisplayDialog("폴더 생성", + "저장 폴더가 아직 생성되지 않았습니다.\n지금 생성하시겠습니까?", "생성", "취소")) + { + Directory.CreateDirectory(path); + EditorUtility.RevealInFinder(path); + AssetDatabase.Refresh(); + } } } EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(3); + + // 추가 유용한 버튼들 + EditorGUILayout.BeginHorizontal(); + + if (GUILayout.Button("📊 인스턴스 상태 확인", buttonStyle)) + { + ShowInstanceInfo(); + } + + if (GUILayout.Button("🛠️ 설정 내보내기", buttonStyle)) + { + ExportSettings(); + } + + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(5); + EditorGUILayout.EndVertical(); + EditorGUILayout.Space(10); + } + + private void ShowInstanceInfo() + { + var motionRecorder = savePathManager.GetComponent(); + var faceRecorder = savePathManager.GetComponent(); + var objectRecorder = savePathManager.GetComponent(); + + string info = $"인스턴스 ID: {savePathManager.InstanceID}\n"; + info += $"저장 경로: {savePathManager.GetMotionSavePath()}\n\n"; + info += "연결된 컴포넌트:\n"; + info += $"- MotionDataRecorder: {(motionRecorder != null ? "✅" : "❌")}\n"; + info += $"- FaceAnimationRecorder: {(faceRecorder != null ? "✅" : "❌")}\n"; + info += $"- ObjectMotionRecorder: {(objectRecorder != null ? "✅" : "❌")}"; + + EditorUtility.DisplayDialog("인스턴스 정보", info, "확인"); + } + + private void ExportSettings() + { + string settings = $"SavePathManager 설정:\n"; + settings += $"저장 경로: {savePathManager.GetMotionSavePath()}\n"; + settings += $"휴머노이드 자동 출력: {savePathManager.ExportHumanoidOnSave}\n"; + settings += $"제네릭 자동 출력: {savePathManager.ExportGenericOnSave}\n"; + settings += $"FBX ASCII 자동 출력: {savePathManager.ExportFBXAsciiOnSave}\n"; + settings += $"FBX Binary 자동 출력: {savePathManager.ExportFBXBinaryOnSave}"; + + GUIUtility.systemCopyBuffer = settings; + EditorUtility.DisplayDialog("설정 내보내기", "설정이 클립보드에 복사되었습니다.", "확인"); + } + + private void DrawMultiObjectGUI() + { + serializedObject.Update(); + + EditorGUILayout.Space(); + EditorGUILayout.LabelField($"📁 저장 경로 관리자 ({targets.Length}개 선택됨)", headerStyle); + EditorGUILayout.Space(); + + DrawMultiObjectBasicSettings(); + + EditorGUILayout.Space(); + + DrawMultiObjectAdvancedSettings(); + + EditorGUILayout.Space(); + + DrawMultiObjectAutoExportSettings(); + + EditorGUILayout.Space(); + + DrawMultiObjectActions(); + + serializedObject.ApplyModifiedProperties(); } private void DrawMultiObjectBasicSettings() @@ -218,9 +482,12 @@ namespace EasyMotionRecorder } EditorGUI.showMixedValue = false; - if (GUILayout.Button("폴더 선택", GUILayout.Width(80))) + // 📂 폴더 선택 버튼 + if (GUILayout.Button(new GUIContent("📂", "폴더 선택 다이얼로그 열기"), iconButtonStyle)) { - string selectedPath = EditorUtility.OpenFolderPanel("저장 폴더 선택", "Assets", ""); + string currentPath = motionSavePathProp.stringValue; + string startPath = !string.IsNullOrEmpty(currentPath) ? currentPath : "Assets"; + string selectedPath = EditorUtility.OpenFolderPanel("저장 폴더 선택", startPath, ""); if (!string.IsNullOrEmpty(selectedPath)) { // Assets 폴더 기준으로 상대 경로로 변환 @@ -237,9 +504,54 @@ namespace EasyMotionRecorder } } } + + // 🏠 기본 경로 버튼 + if (GUILayout.Button(new GUIContent("🏠", "기본 경로(Assets/Motion)로 설정"), iconButtonStyle)) + { + string defaultPath = "Assets/Motion"; + motionSavePathProp.stringValue = defaultPath; + foreach (SavePathManager manager in targets) + { + manager.SetMotionSavePath(defaultPath); + EditorUtility.SetDirty(manager); + } + } + + // ➕ 폴더 생성 버튼 + if (GUILayout.Button(new GUIContent("➕", "선택된 모든 경로의 폴더 생성"), iconButtonStyle)) + { + int createdCount = 0; + int existCount = 0; + + foreach (SavePathManager manager in targets) + { + string path = manager.GetMotionSavePath(); + if (!Directory.Exists(path)) + { + try + { + Directory.CreateDirectory(path); + createdCount++; + } + catch (System.Exception e) + { + Debug.LogError($"폴더 생성 실패: {path} - {e.Message}"); + } + } + else + { + existCount++; + } + } + + AssetDatabase.Refresh(); + EditorUtility.DisplayDialog("폴더 생성 완료", + $"생성됨: {createdCount}개\n이미 존재: {existCount}개\n총 선택: {targets.Length}개", "확인"); + } + EditorGUILayout.EndHorizontal(); - EditorGUILayout.HelpBox("모션, 표정, 오브젝트 파일이 모두 이 경로에 저장됩니다.", MessageType.Info); + EditorGUILayout.HelpBox("📝 모션, 표정, 오브젝트 파일이 모두 이 경로에 저장됩니다.", MessageType.Info); } private void DrawMultiObjectAdvancedSettings() @@ -328,4 +640,4 @@ namespace EasyMotionRecorder } } } -#endif \ No newline at end of file +#endif \ No newline at end of file diff --git a/Assets/External/EasyMotionRecorder/Scripts/FaceAnimationRecorder.cs b/Assets/External/EasyMotionRecorder/Scripts/FaceAnimationRecorder.cs index 48f0d04d..6c107405 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/FaceAnimationRecorder.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/FaceAnimationRecorder.cs @@ -20,25 +20,26 @@ http://opensource.org/licenses/mit-license.php #if UNITY_EDITOR namespace Entum { /// - /// Blendshapeの動きを記録するクラス - /// リップシンクは後入れでTimeline上にAudioClipをつけて、みたいな可能性が高いので + /// Blendshape의 동작을 기록하는 클래스 + /// 립싱크는 나중에 Timeline에 AudioClip을 추가할 가능성이 높으므로 /// Exclusive(除外)するBlendshape名を登録できるようにしています。 /// [RequireComponent(typeof(MotionDataRecorder))] public class FaceAnimationRecorder:MonoBehaviour { - [Header("表情記録を同時に行う場合はtrueにします")] - [SerializeField] + [Header("표정 애니메이션 기록 설정")] + [SerializeField, Tooltip("모션 데이터와 함께 표정 애니메이션도 기록합니다")] private bool _recordFaceBlendshapes = false; - [Header("リップシンクを記録したくない場合はここにモーフ名を入れていく 例:face_mouse_eなど")] - [SerializeField] - private List _exclusiveBlendshapeNames; + [Header("제외할 블렌드셈이프 설정")] + [SerializeField, Tooltip("립싱크나 특정 블렌드셈이프를 기록에서 제외하고 싶은 경우 사용합니다. 예: face_mouse_e")] + private List _exclusiveBlendshapeNames = new List(); - [Tooltip("記録するFPS。0で制限しない。UpdateのFPSは超えられません。")] + [Header("고급 옵션")] + [SerializeField, Tooltip("기록할 FPS. 0으로 설정하면 제한하지 않습니다.")] public float TargetFPS = 60.0f; - [HideInInspector, SerializeField] private string instanceID = ""; - [HideInInspector, SerializeField] private SavePathManager _savePathManager; + [SerializeField, HideInInspector] private string instanceID = ""; + [SerializeField, HideInInspector] private SavePathManager _savePathManager; private MotionDataRecorder _animRecorder; @@ -62,9 +63,17 @@ namespace Entum { // Use this for initialization private void OnEnable() { _animRecorder = GetComponent(); + + // 이벤트 중복 연결 방지 + _animRecorder.OnRecordStart -= RecordStart; + _animRecorder.OnRecordEnd -= RecordEnd; + + // 이벤트 연결 _animRecorder.OnRecordStart += RecordStart; _animRecorder.OnRecordEnd += RecordEnd; + Debug.Log($"FaceAnimationRecorder 이벤트 연결됨 - 인스턴스: {instanceID}"); + // SavePathManager 자동 찾기 if (_savePathManager == null) { @@ -79,6 +88,22 @@ namespace Entum { if(_animRecorder.CharacterAnimator != null) { _smeshs = GetSkinnedMeshRenderers(_animRecorder.CharacterAnimator); + Debug.Log($"SkinnedMeshRenderer 초기화 - 인스턴스: {instanceID}, 발견된 개수: {_smeshs?.Length ?? 0}"); + + // 각 SkinnedMeshRenderer의 블렌드셰이프 개수 로그 + if (_smeshs != null) + { + for (int i = 0; i < _smeshs.Length; i++) + { + var mesh = _smeshs[i]; + var blendShapeCount = mesh.sharedMesh?.blendShapeCount ?? 0; + Debug.Log($" - {mesh.name}: {blendShapeCount}개 블렌드셰이프"); + } + } + } + else + { + Debug.LogWarning($"CharacterAnimator가 null - 인스턴스: {instanceID}"); } } @@ -104,21 +129,65 @@ namespace Entum { } private void OnDisable() { + Debug.Log($"FaceAnimationRecorder OnDisable 호출 - 인스턴스: {instanceID}, 녹화 중: {_recording}"); + if(_recording) { RecordEnd(); _recording = false; } - if(_animRecorder == null) return; + if(_animRecorder == null) + { + Debug.LogWarning($"FaceAnimationRecorder OnDisable: _animRecorder가 null - 인스턴스: {instanceID}"); + return; + } + _animRecorder.OnRecordStart -= RecordStart; _animRecorder.OnRecordEnd -= RecordEnd; + Debug.Log($"FaceAnimationRecorder 이벤트 해제됨 - 인스턴스: {instanceID}"); } /// /// 記録開始 /// private void RecordStart() { + Debug.Log($"FaceAnimationRecorder.RecordStart 호출됨 - 인스턴스: {instanceID}, 활성화: {_recordFaceBlendshapes}, 이미 녹화 중: {_recording}"); + if(_recordFaceBlendshapes == false) { + Debug.Log($"표정 기록이 비활성화되어 있음 - 인스턴스: {instanceID}"); + return; + } + + if (_recording) { + Debug.Log($"이미 표정 녹화가 진행 중임 - 인스턴스: {instanceID}"); + return; + } + + // _animRecorder가 null이면 다시 찾기 + if (_animRecorder == null) { + _animRecorder = GetComponent(); + if (_animRecorder != null) { + Debug.Log($"RecordStart에서 MotionDataRecorder 재연결 - 인스턴스: {instanceID}"); + + // 이벤트도 다시 연결 + _animRecorder.OnRecordStart -= RecordStart; + _animRecorder.OnRecordEnd -= RecordEnd; + _animRecorder.OnRecordStart += RecordStart; + _animRecorder.OnRecordEnd += RecordEnd; + Debug.Log($"이벤트 재연결 완료 - 인스턴스: {instanceID}"); + } else { + Debug.LogError($"MotionDataRecorder를 찾을 수 없음 - 인스턴스: {instanceID}"); + return; + } + } + + // SkinnedMeshRenderer 재초기화 (두 번째 녹화 시 문제 방지) + if(_animRecorder.CharacterAnimator != null) { + _smeshs = GetSkinnedMeshRenderers(_animRecorder.CharacterAnimator); + Debug.Log($"RecordStart에서 SkinnedMeshRenderer 재초기화 - 인스턴스: {instanceID}, 발견된 개수: {_smeshs?.Length ?? 0}"); + } + else { + Debug.LogError($"CharacterAnimator가 null이어서 표정 녹화를 시작할 수 없음 - 인스턴스: {instanceID}"); return; } @@ -132,11 +201,14 @@ namespace Entum { _recordedTime = 0f; _startTime = Time.time; - Debug.Log($"표정 애니메이션 녹화 시작 - 인스턴스: {instanceID}"); + Debug.Log($"표정 애니메이션 녹화 시작 - 인스턴스: {instanceID}, SessionID: {_animRecorder.SessionID}"); } private void RecordEnd() { + Debug.Log($"FaceAnimationRecorder.RecordEnd 호출됨 - 인스턴스: {instanceID}, 녹화 중: {_recording}"); + if(_recording == false) { + Debug.Log($"이미 녹화가 중단된 상태 - 인스턴스: {instanceID}"); return; } @@ -144,10 +216,32 @@ namespace Entum { Debug.Log($"표정 애니메이션 녹화 종료 - 인스턴스: {instanceID}, 총 프레임: {_frameCount}"); // 바로 애니메이션 클립으로 출력 - ExportFacialAnimationClip(_animRecorder.CharacterAnimator, _facialData); + if (_facialData != null && _facialData.Faces.Count > 0) + { + ExportFacialAnimationClip(_animRecorder.CharacterAnimator, _facialData); + } + else + { + Debug.LogWarning($"표정 데이터가 없어서 출력할 수 없음 - 인스턴스: {instanceID}"); + } + + // 상태 완전 리셋 (다음 녹화를 위해) + _frameCount = 0; + _recordedTime = 0f; + _facialData = null; + _past = new CharacterFacialData.SerializeHumanoidFace() { + BlendShapeNames = new List(), + BlendShapeValues = new List(), + SkinnedMeshRendererNames = new List() + }; + + Debug.Log($"표정 녹화 상태 리셋 완료 - 인스턴스: {instanceID}"); } private void ExportFacialAnimationClip(Animator root, CharacterFacialData facial) { + Debug.Log($"ExportFacialAnimationClip 시작 - 인스턴스: {instanceID}"); + Debug.Log($"facial null 여부: {facial == null}, Faces 개수: {facial?.Faces?.Count ?? 0}"); + if(facial == null || facial.Faces.Count == 0) { Debug.LogError("저장할 표정 데이터가 없습니다."); return; @@ -389,6 +483,20 @@ namespace Entum { return; } + // SkinnedMeshRenderer 배열이 null이거나 비어있으면 중단 + if (_smeshs == null || _smeshs.Length == 0) { + if (_frameCount == 0) { + Debug.LogError($"LateUpdate: SkinnedMeshRenderer 배열이 null이거나 비어있음 - 인스턴스: {instanceID}"); + } + return; + } + + // 첫 번째 프레임에서만 로그 출력 + if (_frameCount == 0) + { + Debug.Log($"LateUpdate 표정 데이터 수집 시작 - 인스턴스: {instanceID}, SkinnedMesh 개수: {_smeshs?.Length ?? 0}"); + } + _recordedTime = Time.time - _startTime; if (TargetFPS != 0.0f) @@ -437,6 +545,19 @@ namespace Entum { current.Time = _recordedTime; _facialData.Faces.Add(current); _past = current; + + // 10프레임마다 진행상황 로그 출력 + if (_frameCount % 10 == 0) + { + Debug.Log($"표정 데이터 수집 중 - 프레임: {_frameCount}, 총 데이터: {_facialData.Faces.Count}, 블렌드셰이프: {current.BlendShapeNames.Count}개"); + } + } + else { + // 변화 없어서 스킵된 경우 (100프레임마다 로그) + if (_frameCount % 100 == 0 && _frameCount > 0) + { + Debug.Log($"표정 변화 없음 - 프레임: {_frameCount}, 스킵됨"); + } } _frameCount++; diff --git a/Assets/External/EasyMotionRecorder/Scripts/ForRuntime/MotionDataPlayerCSV.cs b/Assets/External/EasyMotionRecorder/Scripts/ForRuntime/MotionDataPlayerCSV.cs index 8bd1d714..dab13759 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/ForRuntime/MotionDataPlayerCSV.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/ForRuntime/MotionDataPlayerCSV.cs @@ -13,17 +13,17 @@ using System.IO; namespace Entum { /// - /// CSVに吐かれたモーションデータを再生する + /// CSV에 출력된 모션 데이터를 재생하는 클래스 /// public class MotionDataPlayerCSV : MotionDataPlayer { - [SerializeField, Tooltip("スラッシュで終わる形で")] + [SerializeField, Tooltip("슬래시로 끝나는 형태로")] private string _recordedDirectory; - [SerializeField, Tooltip("拡張子も")] + [SerializeField, Tooltip("확장자도 포함하여")] private string _recordedFileName; - // Use this for initialization + // 초기화용 private void Start() { if (string.IsNullOrEmpty(_recordedDirectory)) @@ -35,10 +35,10 @@ namespace Entum LoadCSVData(motionCSVPath); } - //CSVから_recordedMotionDataを作る + // CSV에서 _recordedMotionData를 생성 private void LoadCSVData(string motionDataPath) { - //ファイルが存在しなければ終了 + // 파일이 존재하지 않으면 종료 if (!File.Exists(motionDataPath)) { return; @@ -50,7 +50,7 @@ namespace Entum FileStream fs = null; StreamReader sr = null; - //ファイル読み込み + // 파일 읽기 try { fs = new FileStream(motionDataPath, FileMode.Open); @@ -73,7 +73,7 @@ namespace Entum } catch (System.Exception e) { - Debug.LogError("ファイル読み込み失敗!" + e.Message + e.StackTrace); + Debug.LogError("파일 읽기 실패!" + e.Message + e.StackTrace); } if (sr != null) diff --git a/Assets/External/EasyMotionRecorder/Scripts/ForRuntime/MotionDataRecorderCSV.cs b/Assets/External/EasyMotionRecorder/Scripts/ForRuntime/MotionDataRecorderCSV.cs index 2e389465..b14f85c0 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/ForRuntime/MotionDataRecorderCSV.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/ForRuntime/MotionDataRecorderCSV.cs @@ -14,25 +14,25 @@ using System.IO; namespace Entum { /// - /// モーションデータをCSVに記録するクラス - /// ランタイムでも記録できる + /// 모션 데이터를 CSV에 기록하는 클래스 + /// 런타임에도 기록 가능 /// [DefaultExecutionOrder(31000)] public class MotionDataRecorderCSV : MotionDataRecorder { - [SerializeField, Tooltip("スラッシュで終わる形で")] + [SerializeField, Tooltip("슬래시로 끝나는 형태로")] private string _outputDirectory; - [SerializeField, Tooltip("拡張子も")] + [SerializeField, Tooltip("확장자도 포함하여")] private string _outputFileName; protected override void WriteAnimationFile() { - //ファイルオープン + // 파일 열기 string directoryStr = _outputDirectory; if (directoryStr == "") { - //自動設定ディレクトリ + // 자동 설정 디렉토리 directoryStr = Application.streamingAssetsPath + "/"; if (!Directory.Exists(directoryStr)) @@ -44,7 +44,7 @@ namespace Entum string fileNameStr = _outputFileName; if (fileNameStr == "") { - //自動設定ファイル名 + // 자동 설정 파일명 fileNameStr = string.Format("motion_{0:yyyy_MM_dd_HH_mm_ss}.csv", DateTime.Now); } @@ -57,7 +57,7 @@ namespace Entum sw.WriteLine(seriStr); } - //ファイルクローズ + // 파일 닫기 try { sw.Close(); @@ -67,7 +67,7 @@ namespace Entum } catch (Exception e) { - Debug.LogError("ファイル書き出し失敗!" + e.Message + e.StackTrace); + Debug.LogError("파일 쓰기 실패!" + e.Message + e.StackTrace); } if (sw != null) diff --git a/Assets/External/EasyMotionRecorder/Scripts/HumanoidPoses.cs b/Assets/External/EasyMotionRecorder/Scripts/HumanoidPoses.cs index ea673881..ce2347ba 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/HumanoidPoses.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/HumanoidPoses.cs @@ -2108,5 +2108,178 @@ namespace Entum [SerializeField] public SummaryInfo Summary = new SummaryInfo(); + +#if UNITY_EDITOR + /// + /// 선택된 여러 HumanoidPoses 에셋에서 휴머노이드 애니메이션 일괄 출력 + /// + [MenuItem("Assets/Easy Motion Recorder/Batch Export Humanoid Animations", false, 1)] + public static void BatchExportHumanoidAnimations() + { + var selectedHumanoidPoses = GetSelectedHumanoidPoses(); + if (selectedHumanoidPoses.Length == 0) + { + EditorUtility.DisplayDialog("오류", "HumanoidPoses 에셋을 하나 이상 선택해주세요.", "확인"); + return; + } + + BatchExportProcess(selectedHumanoidPoses, "휴머노이드 애니메이션", (pose) => pose.ExportHumanoidAnim()); + } + + /// + /// 선택된 여러 HumanoidPoses 에셋에서 제네릭 애니메이션 일괄 출력 + /// + [MenuItem("Assets/Easy Motion Recorder/Batch Export Generic Animations", false, 2)] + public static void BatchExportGenericAnimations() + { + var selectedHumanoidPoses = GetSelectedHumanoidPoses(); + if (selectedHumanoidPoses.Length == 0) + { + EditorUtility.DisplayDialog("오류", "HumanoidPoses 에셋을 하나 이상 선택해주세요.", "확인"); + return; + } + + BatchExportProcess(selectedHumanoidPoses, "제네릭 애니메이션", (pose) => pose.ExportGenericAnim()); + } + + /// + /// 선택된 여러 HumanoidPoses 에셋에서 FBX ASCII 일괄 출력 + /// + [MenuItem("Assets/Easy Motion Recorder/Batch Export FBX ASCII", false, 3)] + public static void BatchExportFBXAscii() + { + var selectedHumanoidPoses = GetSelectedHumanoidPoses(); + if (selectedHumanoidPoses.Length == 0) + { + EditorUtility.DisplayDialog("오류", "HumanoidPoses 에셋을 하나 이상 선택해주세요.", "확인"); + return; + } + + BatchExportProcess(selectedHumanoidPoses, "FBX ASCII", (pose) => pose.ExportFBXAscii()); + } + + /// + /// 선택된 여러 HumanoidPoses 에셋에서 FBX Binary 일괄 출력 + /// + [MenuItem("Assets/Easy Motion Recorder/Batch Export FBX Binary", false, 4)] + public static void BatchExportFBXBinary() + { + var selectedHumanoidPoses = GetSelectedHumanoidPoses(); + if (selectedHumanoidPoses.Length == 0) + { + EditorUtility.DisplayDialog("오류", "HumanoidPoses 에셋을 하나 이상 선택해주세요.", "확인"); + return; + } + + BatchExportProcess(selectedHumanoidPoses, "FBX Binary", (pose) => pose.ExportFBXBinary()); + } + + /// + /// 현재 선택된 오브젝트에서 HumanoidPoses 에셋들을 찾아 반환 + /// + private static HumanoidPoses[] GetSelectedHumanoidPoses() + { + var selectedObjects = Selection.objects; + var humanoidPosesList = new List(); + + foreach (var obj in selectedObjects) + { + if (obj is HumanoidPoses humanoidPoses) + { + humanoidPosesList.Add(humanoidPoses); + } + } + + return humanoidPosesList.ToArray(); + } + + /// + /// 일괄 출력 프로세스 실행 + /// + private static void BatchExportProcess(HumanoidPoses[] poses, string exportType, System.Action exportAction) + { + if (poses.Length == 0) return; + + bool confirmed = EditorUtility.DisplayDialog( + "일괄 출력 확인", + $"{poses.Length}개의 HumanoidPoses 에셋에서 {exportType} 파일을 출력하시겠습니까?", + "출력 시작", + "취소"); + + if (!confirmed) return; + + int successCount = 0; + int failCount = 0; + float progress = 0f; + + try + { + for (int i = 0; i < poses.Length; i++) + { + var pose = poses[i]; + progress = (float)i / poses.Length; + + string assetName = pose.name; + bool cancelled = EditorUtility.DisplayCancelableProgressBar( + $"{exportType} 일괄 출력 중...", + $"처리 중: {assetName} ({i + 1}/{poses.Length})", + progress); + + if (cancelled) + { + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("취소됨", "일괄 출력이 사용자에 의해 취소되었습니다.", "확인"); + return; + } + + try + { + Debug.Log($"[일괄 출력] {exportType} 처리 시작: {assetName}"); + exportAction(pose); + successCount++; + Debug.Log($"[일괄 출력] {exportType} 처리 완료: {assetName}"); + } + catch (System.Exception e) + { + failCount++; + Debug.LogError($"[일괄 출력] {exportType} 처리 실패: {assetName} - {e.Message}"); + } + } + } + finally + { + EditorUtility.ClearProgressBar(); + + // 결과 다이얼로그 표시 + string message = $"{exportType} 일괄 출력이 완료되었습니다.\n\n" + + $"성공: {successCount}개\n" + + $"실패: {failCount}개\n" + + $"총 처리: {poses.Length}개"; + + if (failCount > 0) + { + EditorUtility.DisplayDialog("일괄 출력 완료 (일부 실패)", message, "확인"); + } + else + { + EditorUtility.DisplayDialog("일괄 출력 완료", message, "확인"); + } + + Debug.Log($"[일괄 출력 완료] {exportType}: 성공 {successCount}개, 실패 {failCount}개"); + } + } + + /// + /// 메뉴 아이템 검증: HumanoidPoses 에셋이 선택되었을 때만 메뉴 활성화 + /// + [MenuItem("Assets/Easy Motion Recorder/Batch Export Humanoid Animations", true)] + [MenuItem("Assets/Easy Motion Recorder/Batch Export Generic Animations", true)] + [MenuItem("Assets/Easy Motion Recorder/Batch Export FBX ASCII", true)] + [MenuItem("Assets/Easy Motion Recorder/Batch Export FBX Binary", true)] + public static bool ValidateBatchExport() + { + return GetSelectedHumanoidPoses().Length > 0; + } +#endif } } diff --git a/Assets/External/EasyMotionRecorder/Scripts/MotionDataPlayer.cs b/Assets/External/EasyMotionRecorder/Scripts/MotionDataPlayer.cs index 4fb68879..4a5b03ab 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/MotionDataPlayer.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/MotionDataPlayer.cs @@ -13,10 +13,10 @@ using System; namespace Entum { /// - /// モーションデータ再生クラス - /// SpringBone, DynamicBone, BulletPhysicsImplなどの揺れ物アセットのScript Execution Orderを20000など - /// 大きな値にしてください。 - /// DefaultExecutionOrder(11000) はVRIK系より処理順を遅くする、という意図です + /// 모션 데이터 재생 클래스 + /// SpringBone, DynamicBone, BulletPhysicsImpl 등의 흔들리는 에셋의 Script Execution Order를 20000 등 + /// 큰 값으로 설정해주세요. + /// DefaultExecutionOrder(11000)은 VRIK계보다 처리 순서를 느리게 한다는 의도입니다 /// [DefaultExecutionOrder(11000)] public class MotionDataPlayer : MonoBehaviour @@ -31,7 +31,7 @@ namespace Entum [SerializeField] private Animator _animator; - [SerializeField, Tooltip("再生開始フレームを指定します。0だとファイル先頭から開始です")] + [SerializeField, Tooltip("재생 시작 프레임을 지정합니다. 0이면 파일 시작부터 시작합니다")] private int _startFrame; [SerializeField] private bool _playing; @@ -75,7 +75,7 @@ namespace Entum _onPlayFinish += StopMotion; } - // Update is called once per frame + // 매 프레임마다 호출 private void Update() { if (Input.GetKeyDown(_playStartKey)) @@ -101,7 +101,7 @@ namespace Entum } /// - /// モーションデータ再生開始 + /// 모션 데이터 재생 시작 /// private void PlayMotion() { diff --git a/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs b/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs index d93d0efb..98a9cc53 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs @@ -22,9 +22,9 @@ using UniHumanoid; namespace Entum { /// - /// モーションデータ記録クラス - /// スクリプト実行順はVRIKの処理が終わった後の姿勢を取得したいので - /// 最大値=32000を指定しています + /// 모션 데이터 기록 클래스 + /// 스크립트 실행 순서는 VRIK 처리가 끝난 후의 자세를 취득하기 위해 + /// 최대값 32000을 지정합니다 /// [DefaultExecutionOrder(32000)] public class MotionDataRecorder : MonoBehaviour @@ -68,10 +68,10 @@ namespace Entum public Action OnRecordStart; public Action OnRecordEnd; - [Tooltip("記録するFPS。0で制限しない。UpdateのFPSは超えられません。")] + [Tooltip("기록할 FPS. 0으로 설정하면 제한하지 않습니다. Update FPS를 초과할 수 없습니다.")] public float TargetFPS = 60.0f; - // Use this for initialization + // 초기화용 private void Awake() { if (_animator == null) @@ -148,7 +148,7 @@ namespace Entum } } - // Update is called once per frame + // 매 프레임마다 호출 private void LateUpdate() { if (!_recording) @@ -179,7 +179,7 @@ namespace Entum } } - //現在のフレームのHumanoidの姿勢を取得 + // 현재 프레임의 Humanoid 자세를 취득 if (_poseHandler == null) { Debug.LogError("PoseHandler가 초기화되지 않았습니다. 녹화를 중단합니다."); @@ -188,7 +188,7 @@ namespace Entum } _poseHandler.GetHumanPose(ref _currentPose); - //posesに取得한姿勢를書き込む + // poses에 취득한 자세를 기록 var serializedPose = new HumanoidPoses.SerializeHumanoidPose(); switch (_rootBoneSystem) @@ -312,6 +312,10 @@ namespace Entum Debug.Log($"모션 녹화 시작 - 인스턴스: {instanceID}, 세션: {SessionID}, T포즈 옵션: {_recordTPoseAtStart}"); + // 이벤트 구독자 확인 + int subscriberCount = OnRecordStart?.GetInvocationList()?.Length ?? 0; + Debug.Log($"OnRecordStart 이벤트 구독자 수: {subscriberCount}"); + OnRecordStart?.Invoke(); } @@ -659,18 +663,15 @@ namespace Entum { try { - string animPath = Path.Combine(_savePathManager.GetMotionSavePath(), $"{baseFileName}_Humanoid.anim"); - animPath = _savePathManager.GetInstanceSpecificPath(animPath); - - // 직접 휴머노이드 애니메이션 클립 생성 - var clip = CreateHumanoidAnimationClip(); - if (clip != null) + // Poses 에셋에서 직접 휴머노이드 애니메이션 출력 + if (Poses != null) { - SavePathManager.SafeCreateDirectory(Path.GetDirectoryName(animPath)); - AssetDatabase.CreateAsset(clip, animPath); - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); - Debug.Log($"휴머노이드 애니메이션 출력 완료: {animPath}"); + Poses.ExportHumanoidAnim(); + Debug.Log($"휴머노이드 애니메이션 출력 완료 (HumanoidPoses.ExportHumanoidAnim 사용)"); + } + else + { + Debug.LogWarning("Poses가 null이어서 휴머노이드 애니메이션을 출력할 수 없습니다."); } } catch (System.Exception e) @@ -683,18 +684,15 @@ namespace Entum { try { - string animPath = Path.Combine(_savePathManager.GetMotionSavePath(), $"{baseFileName}_Generic.anim"); - animPath = _savePathManager.GetInstanceSpecificPath(animPath); - - // 직접 제네릭 애니메이션 클립 생성 - var clip = CreateGenericAnimationClip(); - if (clip != null) + // Poses 에셋에서 직접 제네릭 애니메이션 출력 + if (Poses != null) { - SavePathManager.SafeCreateDirectory(Path.GetDirectoryName(animPath)); - AssetDatabase.CreateAsset(clip, animPath); - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); - Debug.Log($"제네릭 애니메이션 출력 완료: {animPath}"); + Poses.ExportGenericAnim(); + Debug.Log($"제네릭 애니메이션 출력 완료 (HumanoidPoses.ExportGenericAnim 사용)"); + } + else + { + Debug.LogWarning("Poses가 null이어서 제네릭 애니메이션을 출력할 수 없습니다."); } } catch (System.Exception e) @@ -727,29 +725,29 @@ namespace Entum } } - private AnimationClip CreateHumanoidAnimationClip() + // 더 이상 사용하지 않음 - HumanoidPoses.ExportHumanoidAnim() 직접 사용 + /*private AnimationClip CreateHumanoidAnimationClip() { if (Poses == null || Poses.Poses.Count == 0) return null; - var clip = new AnimationClip { frameRate = 30 }; + var clip = new AnimationClip(); + clip.frameRate = 30; - // Humanoid 애니메이션 설정 + // 중요: 휴머노이드 애니메이션으로 명시적 설정 + clip.legacy = false; + + // HumanoidPoses와 동일한 애니메이션 클립 설정 var settings = new AnimationClipSettings { - loopTime = false, - cycleOffset = 0, - loopBlend = false, - loopBlendOrientation = true, - loopBlendPositionY = true, - loopBlendPositionXZ = true, - keepOriginalOrientation = true, - keepOriginalPositionY = true, - keepOriginalPositionXZ = true, - heightFromFeet = false, - mirror = false, - hasAdditiveReferencePose = false, - additiveReferencePoseTime = 0 + loopTime = false, // Loop Time: false + cycleOffset = 0, // Cycle Offset: 0 + startTime = 0, // Start Time: 0 + stopTime = Poses.Poses.Count > 0 ? Poses.Poses.Last().Time : 1f, // Stop Time + orientationOffsetY = 0, // Orientation Offset Y: 0 + level = 0, // Level: 0 + mirror = false // Mirror: false }; + AnimationUtility.SetAnimationClipSettings(clip, settings); // Muscles 데이터를 커브로 변환 @@ -776,15 +774,228 @@ namespace Entum string muscleName = HumanTrait.MuscleName[i]; clip.SetCurve("", typeof(Animator), muscleName, muscleCurves[i]); } + + // HumanoidPoses와 동일한 방식으로 Root Motion 설정 + // 1. Body Position (RootT.x/y/z) + { + var curveX = new AnimationCurve(); + var curveY = new AnimationCurve(); + var curveZ = new AnimationCurve(); + + // T-포즈가 있으면 0프레임에 먼저 추가 + if (Poses.HasTPoseData && Poses.TPoseData != null) + { + curveX.AddKey(Poses.TPoseData.Time, Poses.TPoseData.BodyPosition.x); + curveY.AddKey(Poses.TPoseData.Time, Poses.TPoseData.BodyPosition.y); + curveZ.AddKey(Poses.TPoseData.Time, Poses.TPoseData.BodyPosition.z); + } + + // 실제 녹화된 포즈들 추가 + foreach (var pose in Poses.Poses) + { + curveX.AddKey(pose.Time, pose.BodyPosition.x); + curveY.AddKey(pose.Time, pose.BodyPosition.y); + curveZ.AddKey(pose.Time, pose.BodyPosition.z); + } + + clip.SetCurve("", typeof(Animator), "RootT.x", curveX); + clip.SetCurve("", typeof(Animator), "RootT.y", curveY); + clip.SetCurve("", typeof(Animator), "RootT.z", curveZ); + } + + // 2. Body Rotation (RootQ.x/y/z/w) + { + var curveX = new AnimationCurve(); + var curveY = new AnimationCurve(); + var curveZ = new AnimationCurve(); + var curveW = new AnimationCurve(); + + // T-포즈가 있으면 0프레임에 먼저 추가 + if (Poses.HasTPoseData && Poses.TPoseData != null) + { + curveX.AddKey(Poses.TPoseData.Time, Poses.TPoseData.BodyRotation.x); + curveY.AddKey(Poses.TPoseData.Time, Poses.TPoseData.BodyRotation.y); + curveZ.AddKey(Poses.TPoseData.Time, Poses.TPoseData.BodyRotation.z); + curveW.AddKey(Poses.TPoseData.Time, Poses.TPoseData.BodyRotation.w); + } + + // 실제 녹화된 포즈들 추가 + foreach (var pose in Poses.Poses) + { + curveX.AddKey(pose.Time, pose.BodyRotation.x); + curveY.AddKey(pose.Time, pose.BodyRotation.y); + curveZ.AddKey(pose.Time, pose.BodyRotation.z); + curveW.AddKey(pose.Time, pose.BodyRotation.w); + } + + clip.SetCurve("", typeof(Animator), "RootQ.x", curveX); + clip.SetCurve("", typeof(Animator), "RootQ.y", curveY); + clip.SetCurve("", typeof(Animator), "RootQ.z", curveZ); + clip.SetCurve("", typeof(Animator), "RootQ.w", curveW); + } + + // 3. Left Foot IK Position (LeftFootT.x/y/z) + { + var curveX = new AnimationCurve(); + var curveY = new AnimationCurve(); + var curveZ = new AnimationCurve(); + + // T-포즈가 있으면 0프레임에 먼저 추가 + if (Poses.HasTPoseData && Poses.TPoseData != null) + { + curveX.AddKey(Poses.TPoseData.Time, Poses.TPoseData.LeftfootIK_Pos.x); + curveY.AddKey(Poses.TPoseData.Time, Poses.TPoseData.LeftfootIK_Pos.y); + curveZ.AddKey(Poses.TPoseData.Time, Poses.TPoseData.LeftfootIK_Pos.z); + } + + foreach (var pose in Poses.Poses) + { + curveX.AddKey(pose.Time, pose.LeftfootIK_Pos.x); + curveY.AddKey(pose.Time, pose.LeftfootIK_Pos.y); + curveZ.AddKey(pose.Time, pose.LeftfootIK_Pos.z); + } + + clip.SetCurve("", typeof(Animator), "LeftFootT.x", curveX); + clip.SetCurve("", typeof(Animator), "LeftFootT.y", curveY); + clip.SetCurve("", typeof(Animator), "LeftFootT.z", curveZ); + } + + // 4. Right Foot IK Position (RightFootT.x/y/z) + { + var curveX = new AnimationCurve(); + var curveY = new AnimationCurve(); + var curveZ = new AnimationCurve(); + + // T-포즈가 있으면 0프레임에 먼저 추가 + if (Poses.HasTPoseData && Poses.TPoseData != null) + { + curveX.AddKey(Poses.TPoseData.Time, Poses.TPoseData.RightfootIK_Pos.x); + curveY.AddKey(Poses.TPoseData.Time, Poses.TPoseData.RightfootIK_Pos.y); + curveZ.AddKey(Poses.TPoseData.Time, Poses.TPoseData.RightfootIK_Pos.z); + } + + foreach (var pose in Poses.Poses) + { + curveX.AddKey(pose.Time, pose.RightfootIK_Pos.x); + curveY.AddKey(pose.Time, pose.RightfootIK_Pos.y); + curveZ.AddKey(pose.Time, pose.RightfootIK_Pos.z); + } + + clip.SetCurve("", typeof(Animator), "RightFootT.x", curveX); + clip.SetCurve("", typeof(Animator), "RightFootT.y", curveY); + clip.SetCurve("", typeof(Animator), "RightFootT.z", curveZ); + } + + // 5. Left Foot IK Rotation (LeftFootQ.x/y/z/w) + { + var curveX = new AnimationCurve(); + var curveY = new AnimationCurve(); + var curveZ = new AnimationCurve(); + var curveW = new AnimationCurve(); + + // T-포즈가 있으면 0프레임에 먼저 추가 + if (Poses.HasTPoseData && Poses.TPoseData != null) + { + curveX.AddKey(Poses.TPoseData.Time, Poses.TPoseData.LeftfootIK_Rot.x); + curveY.AddKey(Poses.TPoseData.Time, Poses.TPoseData.LeftfootIK_Rot.y); + curveZ.AddKey(Poses.TPoseData.Time, Poses.TPoseData.LeftfootIK_Rot.z); + curveW.AddKey(Poses.TPoseData.Time, Poses.TPoseData.LeftfootIK_Rot.w); + } + + foreach (var pose in Poses.Poses) + { + curveX.AddKey(pose.Time, pose.LeftfootIK_Rot.x); + curveY.AddKey(pose.Time, pose.LeftfootIK_Rot.y); + curveZ.AddKey(pose.Time, pose.LeftfootIK_Rot.z); + curveW.AddKey(pose.Time, pose.LeftfootIK_Rot.w); + } + + clip.SetCurve("", typeof(Animator), "LeftFootQ.x", curveX); + clip.SetCurve("", typeof(Animator), "LeftFootQ.y", curveY); + clip.SetCurve("", typeof(Animator), "LeftFootQ.z", curveZ); + clip.SetCurve("", typeof(Animator), "LeftFootQ.w", curveW); + } + + // 6. Right Foot IK Rotation (RightFootQ.x/y/z/w) + { + var curveX = new AnimationCurve(); + var curveY = new AnimationCurve(); + var curveZ = new AnimationCurve(); + var curveW = new AnimationCurve(); + + // T-포즈가 있으면 0프레임에 먼저 추가 + if (Poses.HasTPoseData && Poses.TPoseData != null) + { + curveX.AddKey(Poses.TPoseData.Time, Poses.TPoseData.RightfootIK_Rot.x); + curveY.AddKey(Poses.TPoseData.Time, Poses.TPoseData.RightfootIK_Rot.y); + curveZ.AddKey(Poses.TPoseData.Time, Poses.TPoseData.RightfootIK_Rot.z); + curveW.AddKey(Poses.TPoseData.Time, Poses.TPoseData.RightfootIK_Rot.w); + } + + foreach (var pose in Poses.Poses) + { + curveX.AddKey(pose.Time, pose.RightfootIK_Rot.x); + curveY.AddKey(pose.Time, pose.RightfootIK_Rot.y); + curveZ.AddKey(pose.Time, pose.RightfootIK_Rot.z); + curveW.AddKey(pose.Time, pose.RightfootIK_Rot.w); + } + + clip.SetCurve("", typeof(Animator), "RightFootQ.x", curveX); + clip.SetCurve("", typeof(Animator), "RightFootQ.y", curveY); + clip.SetCurve("", typeof(Animator), "RightFootQ.z", curveZ); + clip.SetCurve("", typeof(Animator), "RightFootQ.w", curveW); + } + + // HumanoidPoses와 동일하게 쿼터니언 연속성 보장 + clip.EnsureQuaternionContinuity(); + + // 2. 애니메이션 클립 타입 명시적 설정 (여러번 시도) + try + { + AnimationUtility.SetAnimationType(clip, ModelImporterAnimationType.Human); + Debug.Log("휴머노이드 애니메이션 타입 설정 성공"); + } + catch (System.Exception e) + { + Debug.LogWarning($"휴머노이드 애니메이션 타입 설정 실패: {e.Message}"); + } + + // 3. 애니메이션 커브 최적화 + foreach (var binding in AnimationUtility.GetCurveBindings(clip)) + { + var curve = AnimationUtility.GetEditorCurve(clip, binding); + if (curve != null && curve.keys.Length > 1) + { + // 키프레임 최적화 + for (int i = 0; i < curve.keys.Length; i++) + { + AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.ClampedAuto); + AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.ClampedAuto); + } + AnimationUtility.SetEditorCurve(clip, binding, curve); + } + } + + // 4. 휴머노이드 확인을 위한 이벤트 추가 + AnimationEvent humanoidEvent = new AnimationEvent(); + humanoidEvent.time = 0f; + humanoidEvent.functionName = "HumanoidMotionStart"; + humanoidEvent.stringParameter = "Humanoid"; + clip.events = new AnimationEvent[] { humanoidEvent }; return clip; - } + }*/ - private AnimationClip CreateGenericAnimationClip() + // 더 이상 사용하지 않음 - HumanoidPoses.ExportGenericAnim() 직접 사용 + /*private AnimationClip CreateGenericAnimationClip() { if (Poses == null || Poses.Poses.Count == 0) return null; - var clip = new AnimationClip { frameRate = 30 }; + var clip = new AnimationClip(); + clip.frameRate = 30; + + // 중요: 제네릭 애니메이션으로 명시적 설정 + clip.legacy = false; // 본별 커브 생성 var boneCurves = new Dictionary>(); @@ -833,6 +1044,36 @@ namespace Entum } } + // 애니메이션 클립 타입 명시적 설정 (제네릭) + try + { + AnimationUtility.SetAnimationType(clip, ModelImporterAnimationType.Generic); + Debug.Log("제네릭 애니메이션 타입 설정 성공"); + } + catch (System.Exception e) + { + Debug.LogWarning($"제네릭 애니메이션 타입 설정 실패: {e.Message}"); + } + + // 애니메이션 커브 최적화 (제네릭) + foreach (var bonePair in boneCurves) + { + var curves = bonePair.Value; + foreach (var curvePair in curves) + { + var curve = curvePair.Value; + if (curve != null && curve.keys.Length > 1) + { + // 키프레임 최적화 + for (int i = 0; i < curve.keys.Length; i++) + { + AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.ClampedAuto); + AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.ClampedAuto); + } + } + } + } + // 손가락 본들이 포함되었는지 로깅 var fingerBoneCount = boneCurves.Keys.Count(name => name.Contains("Thumb") || name.Contains("Index") || name.Contains("Middle") || @@ -840,7 +1081,7 @@ namespace Entum Debug.Log($"제네릭 애니메이션 클립 생성 완료 - 총 {boneCurves.Count}개 본, 손가락 본 {fingerBoneCount}개 포함"); return clip; - } + }*/ #endif private void UpdateSummaryInfo()