From a93dcf7ec8e01f637179924bd86021eb218e5b52 Mon Sep 17 00:00:00 2001 From: Yamo4490 Date: Fri, 1 Aug 2025 07:49:42 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Scripts/CharacterFacialData.cs | 77 +- .../Scripts/Editor/HumanoidPosesEditor.cs | 58 +- .../Editor/ObjectMotionRecorderEditor.cs | 192 +++- .../Scripts/Editor/SavePathManagerEditor.cs | 226 ++++- .../Scripts/FaceAnimationRecorder.cs | 500 +++++---- .../Scripts/HumanoidPoses.cs | 778 +++++--------- .../Scripts/MotionDataPlayer.cs | 125 ++- .../Scripts/MotionDataRecorder.cs | 959 ++++++++++++------ .../Scripts/ObjectMotionRecorder.cs | 204 +++- .../Scripts/README_SavePathManager.md | 3 - .../Scripts/README_SavePathManager.md.meta | 7 - .../Scripts/SavePathManager.cs | 197 +++- Assets/Scripts/AssetBatchRenamer.cs | 89 -- Assets/Scripts/AssetBatchRenamer.cs.meta | 2 - Assets/Scripts/AvatarComponetCopier.cs | 755 -------------- Assets/Scripts/AvatarComponetCopier.cs.meta | 2 - Assets/Scripts/HumanBoneNameCopier.cs | 315 ++++++ Assets/Scripts/HumanBoneNameCopier.cs.meta | 2 + Assets/Scripts/MaterialMover.cs | 287 ------ Assets/Scripts/MaterialMover.cs.meta | 2 - .../Scripts/NilotoonMaterialMatcapSetter.cs | 214 ---- .../NilotoonMaterialMatcapSetter.cs.meta | 2 - Assets/Scripts/QA_Select.cs | 47 - Assets/Scripts/QA_Select.cs.meta | 11 - Assets/Scripts/RenameHumanoidBones.cs | 955 +++++++++++++++++ 25 files changed, 3253 insertions(+), 2756 deletions(-) delete mode 100644 Assets/External/EasyMotionRecorder/Scripts/README_SavePathManager.md delete mode 100644 Assets/External/EasyMotionRecorder/Scripts/README_SavePathManager.md.meta delete mode 100644 Assets/Scripts/AssetBatchRenamer.cs delete mode 100644 Assets/Scripts/AssetBatchRenamer.cs.meta delete mode 100644 Assets/Scripts/AvatarComponetCopier.cs delete mode 100644 Assets/Scripts/AvatarComponetCopier.cs.meta create mode 100644 Assets/Scripts/HumanBoneNameCopier.cs create mode 100644 Assets/Scripts/HumanBoneNameCopier.cs.meta delete mode 100644 Assets/Scripts/MaterialMover.cs delete mode 100644 Assets/Scripts/MaterialMover.cs.meta delete mode 100644 Assets/Scripts/NilotoonMaterialMatcapSetter.cs delete mode 100644 Assets/Scripts/NilotoonMaterialMatcapSetter.cs.meta delete mode 100644 Assets/Scripts/QA_Select.cs delete mode 100644 Assets/Scripts/QA_Select.cs.meta diff --git a/Assets/External/EasyMotionRecorder/Scripts/CharacterFacialData.cs b/Assets/External/EasyMotionRecorder/Scripts/CharacterFacialData.cs index 0c769b1b8..b48ceedb4 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/CharacterFacialData.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/CharacterFacialData.cs @@ -3,52 +3,37 @@ using System.Collections.Generic; using System.Linq; using UnityEngine; -namespace Entum +namespace EasyMotionRecorder { - public class CharacterFacialData : ScriptableObject - { - - [System.SerializableAttribute] - public class SerializeHumanoidFace - { - public class MeshAndBlendshape - { - public string path; - public float[] blendShapes; - } - - - public int BlendShapeNum() - { - return Smeshes.Count == 0 ? 0 : Smeshes.Sum(t => t.blendShapes.Length); - } - - //フレーム数 - public int FrameCount; - - //記録開始後の経過時間。処理落ち対策 - public float Time; - - public SerializeHumanoidFace(SerializeHumanoidFace serializeHumanoidFace) - { - for (int i = 0; i < serializeHumanoidFace.Smeshes.Count; i++) - { - Smeshes.Add(serializeHumanoidFace.Smeshes[i]); - Array.Copy(serializeHumanoidFace.Smeshes[i].blendShapes,Smeshes[i].blendShapes, - serializeHumanoidFace.Smeshes[i].blendShapes.Length); - - } - FrameCount = serializeHumanoidFace.FrameCount; - Time = serializeHumanoidFace.Time; - } - //単一フレームの中でも、口のメッシュや目のメッシュなどが個別にここに入る - public List Smeshes= new List(); - public SerializeHumanoidFace() - { - } - } - + [Serializable] + public class CharacterFacialData : ScriptableObject + { + [SerializeField] + public string SessionID = ""; - public List Facials = new List(); - } + [SerializeField] + public string InstanceID = ""; + + [SerializeField] + public List Faces = new List(); + + [Serializable] + public class SerializeHumanoidFace + { + [SerializeField] + public List BlendShapeNames = new List(); + + [SerializeField] + public List BlendShapeValues = new List(); + + [SerializeField] + public List SkinnedMeshRendererNames = new List(); + + [SerializeField] + public int FrameCount; + + [SerializeField] + public float Time; + } + } } \ No newline at end of file diff --git a/Assets/External/EasyMotionRecorder/Scripts/Editor/HumanoidPosesEditor.cs b/Assets/External/EasyMotionRecorder/Scripts/Editor/HumanoidPosesEditor.cs index 52971a51f..3f780e8e6 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/Editor/HumanoidPosesEditor.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/Editor/HumanoidPosesEditor.cs @@ -436,27 +436,7 @@ namespace Entum EditorGUILayout.Space(8); - // 네 번째 행 - Biped FBX 내보내기 - EditorGUILayout.BeginHorizontal(); - - GUI.backgroundColor = new Color(0.4f, 0.8f, 0.2f); // 연한 초록색 - if (GUILayout.Button("🤖 Biped (Binary)", GUILayout.Height(30), GUILayout.ExpandWidth(true))) - { - ExportBipedFBXBinary(humanoidPoses); - } - - GUILayout.Space(8); - - GUI.backgroundColor = new Color(0.8f, 0.6f, 0.2f); // 연한 주황색 - if (GUILayout.Button("🤖 Biped (ASCII)", GUILayout.Height(30), GUILayout.ExpandWidth(true))) - { - ExportBipedFBXAscii(humanoidPoses); - } - - GUI.backgroundColor = oldColor; - EditorGUILayout.EndHorizontal(); - - EditorGUILayout.Space(8); + // 다섯 번째 행 - 유틸리티 EditorGUILayout.BeginHorizontal(); @@ -582,43 +562,7 @@ namespace Entum } } - private void ExportBipedFBXBinary(HumanoidPoses humanoidPoses) - { - if (humanoidPoses.Poses == null || humanoidPoses.Poses.Count == 0) - { - EditorUtility.DisplayDialog("Biped FBX 내보내기", "내보낼 데이터가 없습니다.", "확인"); - return; - } - - try - { - humanoidPoses.ExportBipedFBXBinary(); - EditorUtility.DisplayDialog("Biped FBX 내보내기 완료", "Binary 형식의 Biped FBX 파일이 내보내졌습니다.", "확인"); - } - catch (System.Exception e) - { - EditorUtility.DisplayDialog("Biped FBX 내보내기 오류", $"Biped FBX 내보내기 중 오류가 발생했습니다:\n{e.Message}", "확인"); - } - } - private void ExportBipedFBXAscii(HumanoidPoses humanoidPoses) - { - if (humanoidPoses.Poses == null || humanoidPoses.Poses.Count == 0) - { - EditorUtility.DisplayDialog("Biped FBX 내보내기", "내보낼 데이터가 없습니다.", "확인"); - return; - } - - try - { - humanoidPoses.ExportBipedFBXAscii(); - EditorUtility.DisplayDialog("Biped FBX 내보내기 완료", "ASCII 형식의 Biped FBX 파일이 내보내졌습니다.", "확인"); - } - catch (System.Exception e) - { - EditorUtility.DisplayDialog("Biped FBX 내보내기 오류", $"Biped FBX 내보내기 중 오류가 발생했습니다:\n{e.Message}", "확인"); - } - } private float EstimateFileSize(HumanoidPoses humanoidPoses) { diff --git a/Assets/External/EasyMotionRecorder/Scripts/Editor/ObjectMotionRecorderEditor.cs b/Assets/External/EasyMotionRecorder/Scripts/Editor/ObjectMotionRecorderEditor.cs index 95fb6cdb8..a0d18ee54 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 121fbd6a4..ad7b28379 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 d58974fa7..48f0d04d4 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/FaceAnimationRecorder.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/FaceAnimationRecorder.cs @@ -37,8 +37,10 @@ namespace Entum { [Tooltip("記録するFPS。0で制限しない。UpdateのFPSは超えられません。")] public float TargetFPS = 60.0f; - private MotionDataRecorder _animRecorder; + [HideInInspector, SerializeField] private string instanceID = ""; + [HideInInspector, SerializeField] private SavePathManager _savePathManager; + private MotionDataRecorder _animRecorder; private SkinnedMeshRenderer[] _smeshs; @@ -48,8 +50,11 @@ namespace Entum { private int _frameCount = 0; - - CharacterFacialData.SerializeHumanoidFace _past = new CharacterFacialData.SerializeHumanoidFace(); + CharacterFacialData.SerializeHumanoidFace _past = new CharacterFacialData.SerializeHumanoidFace() { + BlendShapeNames = new List(), + BlendShapeValues = new List(), + SkinnedMeshRendererNames = new List() + }; private float _recordedTime = 0f; private float _startTime; @@ -59,11 +64,30 @@ namespace Entum { _animRecorder = GetComponent(); _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); + } + 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(); @@ -98,258 +122,334 @@ namespace Entum { return; } - if(_recording) { - return; - } + _facialData = ScriptableObject.CreateInstance(); + _facialData.Faces = new List(); + _facialData.SessionID = _animRecorder.SessionID; + _facialData.InstanceID = instanceID; - if(_smeshs.Length == 0) { - Debug.LogError("顔のメッシュ指定がされていないので顔のアニメーションは記録しません"); - return; - } - - Debug.Log("FaceAnimationRecorder record start"); _recording = true; + _frameCount = 0; _recordedTime = 0f; _startTime = Time.time; - _frameCount = 0; - _facialData = ScriptableObject.CreateInstance(); + + Debug.Log($"표정 애니메이션 녹화 시작 - 인스턴스: {instanceID}"); + } + + private void RecordEnd() { + if(_recording == false) { + return; + } + + _recording = false; + Debug.Log($"표정 애니메이션 녹화 종료 - 인스턴스: {instanceID}, 총 프레임: {_frameCount}"); + + // 바로 애니메이션 클립으로 출력 + ExportFacialAnimationClip(_animRecorder.CharacterAnimator, _facialData); + } + + private void ExportFacialAnimationClip(Animator root, CharacterFacialData facial) { + if(facial == null || facial.Faces.Count == 0) { + Debug.LogError("저장할 표정 데이터가 없습니다."); + return; + } + + var clip = new AnimationClip(); + clip.frameRate = 30; + + // 블렌드쉐이프와 렌더러별로 애니메이션 커브를 그룹화 + var rendererCurves = new Dictionary>(); + + // 첫 번째 프레임에서 모든 블렌드쉐이프와 렌더러 정보 수집 + if (facial.Faces.Count > 0) + { + var firstFace = facial.Faces[0]; + + // 기존 데이터 호환성 체크 (SkinnedMeshRendererNames가 없는 경우) + bool hasRendererNames = firstFace.SkinnedMeshRendererNames != null && + firstFace.SkinnedMeshRendererNames.Count > 0; + + for(int i = 0; i < firstFace.BlendShapeNames.Count; i++) { + var blendShapeName = firstFace.BlendShapeNames[i]; + var rendererName = hasRendererNames && i < firstFace.SkinnedMeshRendererNames.Count + ? firstFace.SkinnedMeshRendererNames[i] + : "DefaultRenderer"; // 기존 데이터 호환성을 위한 기본값 + + if (!rendererCurves.ContainsKey(rendererName)) { + rendererCurves[rendererName] = new Dictionary(); + } + + if (!rendererCurves[rendererName].ContainsKey(blendShapeName)) { + rendererCurves[rendererName][blendShapeName] = new AnimationCurve(); + } + } + + if (!hasRendererNames) { + Debug.LogWarning("기존 데이터 형식 감지: SkinnedMeshRenderer 정보가 없습니다. 기본 경로를 사용합니다."); + } + } + + // 모든 프레임의 데이터를 커브에 추가 + for(int frameIdx = 0; frameIdx < facial.Faces.Count; frameIdx++) { + var face = facial.Faces[frameIdx]; + var time = face.Time; + + // 기존 데이터 호환성 체크 + bool hasRendererNames = face.SkinnedMeshRendererNames != null && + face.SkinnedMeshRendererNames.Count > 0; + + for(int i = 0; i < face.BlendShapeNames.Count; i++) { + var blendShapeName = face.BlendShapeNames[i]; + var value = face.BlendShapeValues[i]; + var rendererName = hasRendererNames && i < face.SkinnedMeshRendererNames.Count + ? face.SkinnedMeshRendererNames[i] + : "DefaultRenderer"; // 기존 데이터 호환성을 위한 기본값 + + if (rendererCurves.ContainsKey(rendererName) && + rendererCurves[rendererName].ContainsKey(blendShapeName)) { + rendererCurves[rendererName][blendShapeName].AddKey(time, value); + } + } + } + + // 렌더러별로 애니메이션 커브를 클립에 추가 + foreach(var rendererPair in rendererCurves) { + var rendererName = rendererPair.Key; + var curves = rendererPair.Value; + + string rendererPath; + + // 기존 데이터 호환성: DefaultRenderer인 경우 빈 경로 사용 + if (rendererName == "DefaultRenderer") { + rendererPath = ""; + Debug.Log($"기존 데이터 호환성: 빈 경로로 {curves.Count}개 블렌드쉐이프 커브 추가"); + } else { + // 해당 렌더러의 Transform 경로 찾기 + rendererPath = FindRendererPath(root.transform, rendererName); + + if (string.IsNullOrEmpty(rendererPath)) { + Debug.LogWarning($"렌더러 '{rendererName}'의 경로를 찾을 수 없습니다. 루트 경로를 사용합니다."); + rendererPath = rendererName; + } + + Debug.Log($"렌더러 '{rendererName}' ({rendererPath})에 {curves.Count}개 블렌드쉐이프 커브 추가"); + } + + foreach(var curvePair in curves) { + var blendShapeName = curvePair.Key; + var curve = curvePair.Value; + + clip.SetCurve(rendererPath, typeof(SkinnedMeshRenderer), "blendShape." + blendShapeName, curve); + } + } + + // 캐릭터 이름 가져오기 + string characterName = GetCharacterName(); + string fileName = string.IsNullOrEmpty(characterName) + ? $"{facial.SessionID}_Facial" + : $"{facial.SessionID}_{characterName}_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 } /// - /// 記録終了 + /// 지정된 렌더러 이름의 Transform 경로를 찾습니다. /// - private void RecordEnd() { - if(_recordFaceBlendshapes == false) { - return; + private string FindRendererPath(Transform root, string rendererName) { + if (root.name == rendererName) { + return ""; } - if(_smeshs.Length == 0) { - Debug.LogError("顔のメッシュ指定がされていないので顔のアニメーションは記録しませんでした"); - if(_recording == true) { - Debug.LogAssertion("Unexpected execution!!!!"); + for (int i = 0; i < root.childCount; i++) { + var child = root.GetChild(i); + if (child.name == rendererName) { + return child.name; + } + + string childPath = FindRendererPath(child, rendererName); + if (!string.IsNullOrEmpty(childPath)) { + return child.name + "/" + childPath; } } - else { - //WriteAnimationFileToScriptableObject(); - ExportFacialAnimationClip(_animRecorder.CharacterAnimator, _facialData); - } - Debug.Log("FaceAnimationRecorder record end"); - - _recording = false; + return null; } - - private void WriteAnimationFileToScriptableObject() { - MotionDataRecorder.SafeCreateDirectory("Assets/Resources"); - - string path = AssetDatabase.GenerateUniqueAssetPath( - "Assets/Resources/RecordMotion_ face" + _animRecorder.CharacterAnimator.name + - DateTime.Now.ToString("yyyy_MM_dd_HH_mm_ss") + - ".asset"); - - if(_facialData == null) { - Debug.LogError("記録されたFaceデータがnull"); + private string GetCharacterName() + { + if (_animRecorder?.CharacterAnimator == null) return ""; + + var animator = _animRecorder.CharacterAnimator; + + // 1. GameObject 이름 사용 + string objectName = animator.gameObject.name; + + // 2. Avatar 이름이 있으면 우선 사용 + if (animator.avatar != null && !string.IsNullOrEmpty(animator.avatar.name)) + { + string avatarName = animator.avatar.name; + // "Avatar" 접미사 제거 + if (avatarName.EndsWith("Avatar")) + { + avatarName = avatarName.Substring(0, avatarName.Length - 6); + } + if (!string.IsNullOrEmpty(avatarName)) + { + return SanitizeFileName(avatarName); + } } - else { - AssetDatabase.CreateAsset(_facialData, path); - AssetDatabase.Refresh(); + + // 3. 부모 오브젝트에서 캐릭터 루트 찾기 + Transform current = animator.transform.parent; + while (current != null) + { + // VRM, humanoid, character 등의 키워드가 있는 경우 + string parentName = current.name.ToLower(); + if (parentName.Contains("character") || parentName.Contains("humanoid") || + parentName.Contains("avatar") || parentName.Contains("vrm")) + { + return SanitizeFileName(current.name); + } + current = current.parent; } - _startTime = Time.time; - _recordedTime = 0f; - _frameCount = 0; + + // 4. GameObject 이름에서 불필요한 부분 제거 + objectName = objectName.Replace("(Clone)", "").Trim(); + return SanitizeFileName(objectName); + } + + private string SanitizeFileName(string fileName) + { + if (string.IsNullOrEmpty(fileName)) return ""; + + // 파일명에 사용할 수 없는 문자 제거 + char[] invalidChars = Path.GetInvalidFileNameChars(); + foreach (char c in invalidChars) + { + fileName = fileName.Replace(c, '_'); + } + + // 공백을 언더스코어로 변경 + fileName = fileName.Replace(' ', '_'); + + // 연속된 언더스코어 제거 + while (fileName.Contains("__")) + { + fileName = fileName.Replace("__", "_"); + } + + // 앞뒤 언더스코어 제거 + fileName = fileName.Trim('_'); + + return fileName; } - //フレーム内の差分が無いかをチェックするやつ。 private bool IsSame(CharacterFacialData.SerializeHumanoidFace a, CharacterFacialData.SerializeHumanoidFace b) { - if(a == null || b == null || a.Smeshes.Count == 0 || b.Smeshes.Count == 0) { + if(a.BlendShapeNames.Count != b.BlendShapeNames.Count) { return false; } - if(a.BlendShapeNum() != b.BlendShapeNum()) { + if(a.SkinnedMeshRendererNames.Count != b.SkinnedMeshRendererNames.Count) { return false; } - return !a.Smeshes.Where((t1, i) => - t1.blendShapes.Where((t, j) => Mathf.Abs(t - b.Smeshes[i].blendShapes[j]) > 1).Any()).Any(); + for(int i = 0; i < a.BlendShapeNames.Count; i++) { + if(a.BlendShapeValues[i] != b.BlendShapeValues[i]) { + return false; + } + + if(a.BlendShapeNames[i] != b.BlendShapeNames[i]) { + return false; + } + + // SkinnedMeshRenderer 이름도 비교 + if(i < a.SkinnedMeshRendererNames.Count && i < b.SkinnedMeshRendererNames.Count) { + if(a.SkinnedMeshRendererNames[i] != b.SkinnedMeshRendererNames[i]) { + return false; + } + } + } + + return true; } private void LateUpdate() { - if(Input.GetKeyDown(KeyCode.Y)) { - ExportFacialAnimationClipTest(); - } - - if(!_recording) { + if(_recording == false) { return; } _recordedTime = Time.time - _startTime; - if(TargetFPS != 0.0f) { - var nextTime = (1.0f * (_frameCount + 1)) / TargetFPS; - if(nextTime > _recordedTime) { + if (TargetFPS != 0.0f) + { + var nextTime = (1.0f * _frameCount) / TargetFPS; + if (nextTime > _recordedTime) + { return; } - if(_frameCount % TargetFPS == 0) { - print("Face_FPS=" + 1 / (_recordedTime / _frameCount)); - } - } - else { - if(Time.frameCount % Application.targetFrameRate == 0) { - print("Face_FPS=" + 1 / Time.deltaTime); - } } + var current = new CharacterFacialData.SerializeHumanoidFace(); + current.BlendShapeNames = new List(); + current.BlendShapeValues = new List(); + current.SkinnedMeshRendererNames = new List(); - var p = new CharacterFacialData.SerializeHumanoidFace(); for(int i = 0; i < _smeshs.Length; i++) { - var mesh = new CharacterFacialData.SerializeHumanoidFace.MeshAndBlendshape(); - mesh.path = _smeshs[i].name; - mesh.blendShapes = new float[_smeshs[i].sharedMesh.blendShapeCount]; + var mesh = _smeshs[i]; + var blendShapeCount = mesh.sharedMesh.blendShapeCount; - for(int j = 0; j < _smeshs[i].sharedMesh.blendShapeCount; j++) { - var tname = _smeshs[i].sharedMesh.GetBlendShapeName(j); - - var useThis = true; - - foreach(var item in _exclusiveBlendshapeNames) { - if(item.IndexOf(tname, StringComparison.Ordinal) >= 0) { - useThis = false; + for(int j = 0; j < blendShapeCount; j++) { + var blendShapeName = mesh.sharedMesh.GetBlendShapeName(j); + + // 제외할 블렌드셰이프인지 확인 + bool isExcluded = false; + for(int k = 0; k < _exclusiveBlendshapeNames.Count; k++) { + if(blendShapeName.Contains(_exclusiveBlendshapeNames[k])) { + isExcluded = true; + break; } } - - if(useThis) { - mesh.blendShapes[j] = _smeshs[i].GetBlendShapeWeight(j); + if(isExcluded) { + continue; } - } - p.Smeshes.Add(mesh); + var weight = mesh.GetBlendShapeWeight(j); + current.BlendShapeNames.Add(blendShapeName); + current.BlendShapeValues.Add(weight); + current.SkinnedMeshRendererNames.Add(mesh.name); + } } - if(!IsSame(p, _past)) { - p.FrameCount = _frameCount; - p.Time = _recordedTime; - - _facialData.Facials.Add(p); - _past = new CharacterFacialData.SerializeHumanoidFace(p); + if(IsSame(current, _past) == false) { + current.FrameCount = _frameCount; + current.Time = _recordedTime; + _facialData.Faces.Add(current); + _past = current; } _frameCount++; } - - /// - /// Animatorと記録したデータで書き込む - /// - /// - /// - void ExportFacialAnimationClip(Animator root, CharacterFacialData facial) { - var animclip = new AnimationClip(); - - var mesh = _smeshs; - - for(int faceTargetMeshIndex = 0; faceTargetMeshIndex < mesh.Length; faceTargetMeshIndex++) { - var pathsb = new StringBuilder().Append(mesh[faceTargetMeshIndex].transform.name); - var trans = mesh[faceTargetMeshIndex].transform; - while(trans.parent != null && trans.parent != root.transform) { - trans = trans.parent; - pathsb.Insert(0, "/").Insert(0, trans.name); - } - - //pathにはBlendshapeのベース名が入る - //U_CHAR_1:SkinnedMeshRendererみたいなもの - var path = pathsb.ToString(); - - //個別メッシュの個別Blendshapeごとに、AnimationCurveを生成している - for(var blendShapeIndex = 0; - blendShapeIndex < mesh[faceTargetMeshIndex].sharedMesh.blendShapeCount; - blendShapeIndex++) { - var curveBinding = new EditorCurveBinding(); - curveBinding.type = typeof(SkinnedMeshRenderer); - curveBinding.path = path; - curveBinding.propertyName = "blendShape." + - mesh[faceTargetMeshIndex].sharedMesh.GetBlendShapeName(blendShapeIndex); - AnimationCurve curve = new AnimationCurve(); - - float pastBlendshapeWeight = -1; - for(int k = 0; k < _facialData.Facials.Count; k++) { - if(!(Mathf.Abs(pastBlendshapeWeight - _facialData.Facials[k].Smeshes[faceTargetMeshIndex].blendShapes[blendShapeIndex]) > - 0.1f)) continue; - curve.AddKey(new Keyframe(facial.Facials[k].Time, _facialData.Facials[k].Smeshes[faceTargetMeshIndex].blendShapes[blendShapeIndex], float.PositiveInfinity, 0f)); - pastBlendshapeWeight = _facialData.Facials[k].Smeshes[faceTargetMeshIndex].blendShapes[blendShapeIndex]; - } - - - AnimationUtility.SetEditorCurve(animclip, curveBinding, curve); - } - } - - // SavePathManager 사용 - string savePath = "Assets/Resources"; // 기본값 - string fileName = $"{_animRecorder.SessionID}_{_animRecorder.CharacterAnimator.name}_Facial.anim"; - - // SavePathManager가 있으면 사용 - if(SavePathManager.Instance != null) { - savePath = SavePathManager.Instance.GetFacialSavePath(); - fileName = $"{_animRecorder.SessionID}_{_animRecorder.CharacterAnimator.name}_Facial.anim"; - } - - MotionDataRecorder.SafeCreateDirectory(savePath); - - var outputPath = Path.Combine(savePath, fileName); - - Debug.Log($"페이스 애니메이션 파일 저장 경로: {outputPath}"); - AssetDatabase.CreateAsset(animclip, - AssetDatabase.GenerateUniqueAssetPath(outputPath)); - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); + public void SetInstanceID(string id) + { + instanceID = id; } - /// - /// Animatorと記録したデータで書き込むテスト - /// - /// - /// - void ExportFacialAnimationClipTest() { - var animclip = new AnimationClip(); - - var mesh = _smeshs; - - for(int i = 0; i < mesh.Length; i++) { - var pathsb = new StringBuilder().Append(mesh[i].transform.name); - var trans = mesh[i].transform; - while(trans.parent != null && trans.parent != _animRecorder.CharacterAnimator.transform) { - trans = trans.parent; - pathsb.Insert(0, "/").Insert(0, trans.name); - } - - var path = pathsb.ToString(); - - for(var j = 0; j < mesh[i].sharedMesh.blendShapeCount; j++) { - var curveBinding = new EditorCurveBinding(); - curveBinding.type = typeof(SkinnedMeshRenderer); - curveBinding.path = path; - curveBinding.propertyName = "blendShape." + mesh[i].sharedMesh.GetBlendShapeName(j); - AnimationCurve curve = new AnimationCurve(); - - - //全てのBlendshapeに対して0→100→0の遷移でキーを打つ - curve.AddKey(0, 0); - curve.AddKey(1, 100); - curve.AddKey(2, 0); - - Debug.Log("path: " + curveBinding.path + "\r\nname: " + curveBinding.propertyName + " val:"); - - AnimationUtility.SetEditorCurve(animclip, curveBinding, curve); - } - } - - AssetDatabase.CreateAsset(animclip, - AssetDatabase.GenerateUniqueAssetPath("Assets/" + _animRecorder.CharacterAnimator.name + - "_facial_ClipTest.anim")); - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); + public string GetInstanceID() + { + return instanceID; } } } diff --git a/Assets/External/EasyMotionRecorder/Scripts/HumanoidPoses.cs b/Assets/External/EasyMotionRecorder/Scripts/HumanoidPoses.cs index 8cf5c6f2c..ea6738816 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/HumanoidPoses.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/HumanoidPoses.cs @@ -91,22 +91,28 @@ namespace Entum [SerializeField, Tooltip("T-포즈가 저장되었는지 여부")] public bool HasTPoseData = false; + + [SerializeField] + public string SessionID = ""; // 세션 ID 저장 + + [SerializeField] + public string InstanceID = ""; // 인스턴스 ID 저장 - // 세션 ID를 가져오는 메서드 (MotionDataRecorder와 동일한 세션 ID 사용) +#if UNITY_EDITOR + // 세션 ID를 가져오는 메서드 (다중 인스턴스 지원) private string GetSessionID() { - // 1. MotionDataRecorder에서 세션 ID를 가져오려고 시도 - var motionRecorder = FindObjectOfType(); - if (motionRecorder != null && !string.IsNullOrEmpty(motionRecorder.SessionID)) + // 1. 이미 저장된 세션 ID가 있으면 사용 + if (!string.IsNullOrEmpty(SessionID)) { - Debug.Log($"MotionDataRecorder에서 세션 ID 가져옴: {motionRecorder.SessionID}"); - return motionRecorder.SessionID; + Debug.Log($"저장된 세션 ID 사용: {SessionID}"); + return SessionID; } // 2. 스크립터블 오브젝트의 이름에서 세션 ID 추출 시도 if (!string.IsNullOrEmpty(this.name)) { - // 파일명에서 세션 ID 패턴 찾기 (예: 250717_192404_SeNo_Motion) + // 파일명에서 세션 ID 패턴 찾기 (예: 250717_192404_Motion_abc12345) var nameParts = this.name.Split('_'); if (nameParts.Length >= 2) { @@ -142,6 +148,7 @@ namespace Entum Debug.LogWarning($"세션 ID를 찾을 수 없어 현재 시간 사용: {fallbackSessionID}"); return fallbackSessionID; } +#endif #if UNITY_EDITOR //Genericなanimファイルとして出力する [ContextMenu("Export as Generic animation clips")] @@ -294,9 +301,32 @@ namespace Entum // 아바타 이름이 있으면 포함, 없으면 기본값 사용 string avatarName = !string.IsNullOrEmpty(AvatarName) ? AvatarName : "Unknown"; + // 인스턴스 ID 디버깅 로그 + Debug.Log($"출력 시 인스턴스 ID 상태: '{InstanceID}' (비어있음: {string.IsNullOrEmpty(InstanceID)})"); + + // 인스턴스 ID가 비어있으면 현재 씬의 SavePathManager에서 가져오기 + string currentInstanceID = InstanceID; + if (string.IsNullOrEmpty(currentInstanceID)) + { + var savePathManager = FindObjectOfType(); + if (savePathManager != null) + { + currentInstanceID = savePathManager.InstanceID; + Debug.Log($"SavePathManager에서 인스턴스 ID 가져옴: '{currentInstanceID}'"); + } + else + { + Debug.LogWarning("SavePathManager를 찾을 수 없습니다. 인스턴스 ID 없이 파일 생성됩니다."); + } + } + + // 인스턴스 ID 포함 파일명 생성 + string fileName = !string.IsNullOrEmpty(currentInstanceID) + ? $"{sessionID}_{avatarName}_Generic_{currentInstanceID}.anim" + : $"{sessionID}_{avatarName}_Generic.anim"; + // 에셋 파일의 경로를 기반으로 저장 경로 결정 string savePath = "Assets/Resources"; // 기본값 - string fileName = $"{sessionID}_{avatarName}_Generic.anim"; // 현재 에셋 파일의 경로 가져오기 string assetPath = AssetDatabase.GetAssetPath(this); @@ -642,9 +672,32 @@ namespace Entum // 아바타 이름이 있으면 포함, 없으면 기본값 사용 string avatarName = !string.IsNullOrEmpty(AvatarName) ? AvatarName : "Unknown"; + // 인스턴스 ID 디버깅 로그 + Debug.Log($"Humanoid 출력 시 인스턴스 ID 상태: '{InstanceID}' (비어있음: {string.IsNullOrEmpty(InstanceID)})"); + + // 인스턴스 ID가 비어있으면 현재 씬의 SavePathManager에서 가져오기 + string currentInstanceID = InstanceID; + if (string.IsNullOrEmpty(currentInstanceID)) + { + var savePathManager = FindObjectOfType(); + if (savePathManager != null) + { + currentInstanceID = savePathManager.InstanceID; + Debug.Log($"SavePathManager에서 인스턴스 ID 가져옴: '{currentInstanceID}'"); + } + else + { + Debug.LogWarning("SavePathManager를 찾을 수 없습니다. 인스턴스 ID 없이 파일 생성됩니다."); + } + } + + // 인스턴스 ID 포함 파일명 생성 + string fileName = !string.IsNullOrEmpty(currentInstanceID) + ? $"{sessionID}_{avatarName}_Humanoid_{currentInstanceID}.anim" + : $"{sessionID}_{avatarName}_Humanoid.anim"; + // 에셋 파일의 경로를 기반으로 저장 경로 결정 string savePath = "Assets/Resources"; // 기본값 - string fileName = $"{sessionID}_{avatarName}_Humanoid.anim"; // 현재 에셋 파일의 경로 가져오기 string assetPath = AssetDatabase.GetAssetPath(this); @@ -683,19 +736,7 @@ namespace Entum ExportFBXWithEncoding(false); } - // Biped 형식으로 FBX 내보내기 (ASCII) - [ContextMenu("Export as Biped FBX (ASCII)")] - public void ExportBipedFBXAscii() - { - ExportBipedFBXWithEncoding(true); - } - - // Biped 형식으로 FBX 내보내기 (Binary) - [ContextMenu("Export as Biped FBX (Binary)")] - public void ExportBipedFBXBinary() - { - ExportBipedFBXWithEncoding(false); - } + // FBX 내보내기 (인코딩 설정 포함) private void ExportFBXWithEncoding(bool useAscii) @@ -718,9 +759,29 @@ namespace Entum string sessionID = GetSessionID(); string avatarName = !string.IsNullOrEmpty(AvatarName) ? AvatarName : "Unknown"; + // 인스턴스 ID가 비어있으면 현재 씬의 SavePathManager에서 가져오기 + string currentInstanceID = InstanceID; + if (string.IsNullOrEmpty(currentInstanceID)) + { + var savePathManager = FindObjectOfType(); + if (savePathManager != null) + { + currentInstanceID = savePathManager.InstanceID; + Debug.Log($"FBX 출력 시 SavePathManager에서 인스턴스 ID 가져옴: '{currentInstanceID}'"); + } + else + { + Debug.LogWarning("SavePathManager를 찾을 수 없습니다. 인스턴스 ID 없이 FBX 파일 생성됩니다."); + } + } + // 저장 경로 결정 string savePath = "Assets/Resources"; - string fileName = $"{sessionID}_{avatarName}_Motion.fbx"; + + // 인스턴스 ID 포함 파일명 생성 + string fileName = !string.IsNullOrEmpty(currentInstanceID) + ? $"{sessionID}_{avatarName}_Motion_{(useAscii ? "ASCII" : "Binary")}_{currentInstanceID}.fbx" + : $"{sessionID}_{avatarName}_Motion_{(useAscii ? "ASCII" : "Binary")}.fbx"; // 현재 에셋 파일의 경로 가져오기 string assetPath = AssetDatabase.GetAssetPath(this); @@ -744,52 +805,7 @@ namespace Entum Debug.Log($"FBX 애니메이션 파일이 저장되었습니다: {uniqueAssetPath}"); } - // Biped FBX 내보내기 (인코딩 설정 포함) - private void ExportBipedFBXWithEncoding(bool useAscii) - { - // 데이터 검증 - if (Poses == null || Poses.Count == 0) - { - Debug.LogError("ExportBipedFBX: Poses 데이터가 없습니다. Poses.Count=" + (Poses?.Count ?? 0)); - return; - } - // 본 데이터가 있는지 확인 - if (Poses.Count == 0 || Poses[0].HumanoidBones.Count == 0) - { - Debug.LogError("ExportBipedFBX: 본 데이터가 없습니다."); - return; - } - - // 세션 ID 사용 - string sessionID = GetSessionID(); - string avatarName = !string.IsNullOrEmpty(AvatarName) ? AvatarName : "Unknown"; - - // 저장 경로 결정 - string savePath = "Assets/Resources"; - string fileName = $"{sessionID}_{avatarName}_Biped.fbx"; - - // 현재 에셋 파일의 경로 가져오기 - string assetPath = AssetDatabase.GetAssetPath(this); - if (!string.IsNullOrEmpty(assetPath)) - { - string directory = Path.GetDirectoryName(assetPath); - if (!string.IsNullOrEmpty(directory)) - { - savePath = directory; - } - } - - MotionDataRecorder.SafeCreateDirectory(savePath); - - var path = Path.Combine(savePath, fileName); - var uniqueAssetPath = AssetDatabase.GenerateUniqueAssetPath(path); - - // Biped 스켈레톤 생성 후 FBX 내보내기 (인코딩 설정 포함) - ExportBipedSkeletonWithAnimationToFBX(uniqueAssetPath, useAscii); - - Debug.Log($"Biped FBX 애니메이션 파일이 저장되었습니다: {uniqueAssetPath}"); - } // 제네릭 애니메이션 클립 생성 (내부 메서드) private AnimationClip CreateGenericAnimationClip() @@ -823,6 +839,18 @@ namespace Entum continue; } + // Bip001 Pelvis 특별 디버깅 (애니메이션 클립 생성) + if (bone.Name.Contains("Bip001 Pelvis")) + { + Debug.LogWarning($"=== 애니메이션 클립 생성 중 Bip001 Pelvis 발견! ==="); + Debug.LogWarning($"본 인덱스: {i}"); + Debug.LogWarning($"본 이름: '{bone.Name}'"); + Debug.LogWarning($"LocalPosition: {bone.LocalPosition}"); + Debug.LogWarning($"LocalRotation: {bone.LocalRotation}"); + Debug.LogWarning($"LocalRotation (Euler): {bone.LocalRotation.eulerAngles}"); + Debug.LogWarning($"================================"); + } + // 경로 정리: 끝의 슬래시만 제거 string cleanPath = bone.Name.TrimEnd('/'); @@ -1065,206 +1093,7 @@ namespace Entum return clip; } - // Biped 애니메이션 클립 생성 (내부 메서드) - private AnimationClip CreateBipedAnimationClip() - { - var clip = new AnimationClip { frameRate = 30 }; - - // 클립 이름 설정 (중요!) - string sessionID = GetSessionID(); - clip.name = $"{sessionID}_Biped"; - - // 애니메이션 설정 - 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 - }; - - AnimationUtility.SetAnimationClipSettings(clip, settings); - // 본 데이터가 있는지 확인 - if (Poses.Count == 0 || Poses[0].HumanoidBones.Count == 0) - { - Debug.LogError("CreateBipedAnimationClip: 본 데이터가 없습니다."); - return null; - } - - var bones = Poses[0].HumanoidBones; - - // Bip001 Pelvis의 기본 회전 오프셋 (0, -90, -90) - Quaternion pelvisOffset = Quaternion.Euler(-90, 90, 0); - - // Bip001(루트)용 커브 - var bipRootPosX = new AnimationCurve(); - var bipRootPosY = new AnimationCurve(); - var bipRootPosZ = new AnimationCurve(); - var bipRootRotX = new AnimationCurve(); - var bipRootRotY = new AnimationCurve(); - var bipRootRotZ = new AnimationCurve(); - var bipRootRotW = new AnimationCurve(); - - for (int i = 0; i < bones.Count; i++) - { - var bone = bones[i]; - if (string.IsNullOrEmpty(bone.Name)) - { - Debug.LogError($"본 {i}: 이름이 비어있습니다!"); - continue; - } - - // Bip001 Pelvis의 데이터를 Bip001 경로에 키로 추가 - if (i == 0 && bone.Name.Contains("Pelvis")) - { - // T-포즈를 0프레임에 추가 - if (HasTPoseData && TPoseData != null && TPoseData.HumanoidBones.Count > i) - { - var tPoseBone = TPoseData.HumanoidBones[i]; - bipRootPosX.AddKey(0f, tPoseBone.LocalPosition.x); - bipRootPosY.AddKey(0f, tPoseBone.LocalPosition.y); - bipRootPosZ.AddKey(0f, tPoseBone.LocalPosition.z); - - // T-포즈의 회전 값에 pelvisOffset을 곱해서 적용 - var tPoseRotation = tPoseBone.LocalRotation * pelvisOffset; - bipRootRotX.AddKey(0f, tPoseRotation.x); - bipRootRotY.AddKey(0f, tPoseRotation.y); - bipRootRotZ.AddKey(0f, tPoseRotation.z); - bipRootRotW.AddKey(0f, tPoseRotation.w); - } - - foreach (var p in Poses) - { - if (p.HumanoidBones.Count > i) - { - var poseBone = p.HumanoidBones[i]; - bipRootPosX.AddKey(p.Time, poseBone.LocalPosition.x); - bipRootPosY.AddKey(p.Time, poseBone.LocalPosition.y); - bipRootPosZ.AddKey(p.Time, poseBone.LocalPosition.z); - - // 실제 애니메이션 데이터의 회전 값을 사용하되, pelvisOffset을 곱해서 적용 - var actualRotation = poseBone.LocalRotation * pelvisOffset; - bipRootRotX.AddKey(p.Time, actualRotation.x); - bipRootRotY.AddKey(p.Time, actualRotation.y); - bipRootRotZ.AddKey(p.Time, actualRotation.z); - bipRootRotW.AddKey(p.Time, actualRotation.w); - } - } - // Bip001 경로에 커브 추가 - AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve("Bip001", typeof(Transform), "m_LocalPosition.x"), bipRootPosX); - AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve("Bip001", typeof(Transform), "m_LocalPosition.y"), bipRootPosY); - AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve("Bip001", typeof(Transform), "m_LocalPosition.z"), bipRootPosZ); - AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve("Bip001", typeof(Transform), "m_LocalRotation.x"), bipRootRotX); - AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve("Bip001", typeof(Transform), "m_LocalRotation.y"), bipRootRotY); - AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve("Bip001", typeof(Transform), "m_LocalRotation.z"), bipRootRotZ); - AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve("Bip001", typeof(Transform), "m_LocalRotation.w"), bipRootRotW); - continue; // Pelvis 본에는 키를 넣지 않음 - } - - // 나머지 본은 기존 방식대로 - string cleanPath = bone.Name.TrimEnd('/'); - var positionCurveX = new AnimationCurve(); - var positionCurveY = new AnimationCurve(); - var positionCurveZ = new AnimationCurve(); - var rotationCurveX = new AnimationCurve(); - var rotationCurveY = new AnimationCurve(); - var rotationCurveZ = new AnimationCurve(); - var rotationCurveW = new AnimationCurve(); - - // T-포즈를 0프레임에 추가 - if (HasTPoseData && TPoseData != null && TPoseData.HumanoidBones.Count > i) - { - var tPoseBone = TPoseData.HumanoidBones[i]; - positionCurveX.AddKey(0f, tPoseBone.LocalPosition.x); - positionCurveY.AddKey(0f, tPoseBone.LocalPosition.y); - positionCurveZ.AddKey(0f, tPoseBone.LocalPosition.z); - rotationCurveX.AddKey(0f, tPoseBone.LocalRotation.x); - rotationCurveY.AddKey(0f, tPoseBone.LocalRotation.y); - rotationCurveZ.AddKey(0f, tPoseBone.LocalRotation.z); - rotationCurveW.AddKey(0f, tPoseBone.LocalRotation.w); - } - - foreach (var p in Poses) - { - if (p.HumanoidBones.Count > i) - { - var poseBone = p.HumanoidBones[i]; - positionCurveX.AddKey(p.Time, poseBone.LocalPosition.x); - positionCurveY.AddKey(p.Time, poseBone.LocalPosition.y); - positionCurveZ.AddKey(p.Time, poseBone.LocalPosition.z); - rotationCurveX.AddKey(p.Time, poseBone.LocalRotation.x); - rotationCurveY.AddKey(p.Time, poseBone.LocalRotation.y); - rotationCurveZ.AddKey(p.Time, poseBone.LocalRotation.z); - rotationCurveW.AddKey(p.Time, poseBone.LocalRotation.w); - } - } - - // 위치 커브 설정 - var positionBinding = new EditorCurveBinding - { - path = cleanPath, - type = typeof(Transform), - propertyName = "m_LocalPosition.x" - }; - - AnimationUtility.SetEditorCurve(clip, positionBinding, positionCurveX); - AnimationUtility.SetEditorCurve(clip, - new EditorCurveBinding - { - path = cleanPath, - type = typeof(Transform), - propertyName = "m_LocalPosition.y" - }, positionCurveY); - AnimationUtility.SetEditorCurve(clip, - new EditorCurveBinding - { - path = cleanPath, - type = typeof(Transform), - propertyName = "m_LocalPosition.z" - }, positionCurveZ); - - // 회전 커브 설정 - AnimationUtility.SetEditorCurve(clip, - new EditorCurveBinding - { - path = cleanPath, - type = typeof(Transform), - propertyName = "m_LocalRotation.x" - }, rotationCurveX); - AnimationUtility.SetEditorCurve(clip, - new EditorCurveBinding - { - path = cleanPath, - type = typeof(Transform), - propertyName = "m_LocalRotation.y" - }, rotationCurveY); - AnimationUtility.SetEditorCurve(clip, - new EditorCurveBinding - { - path = cleanPath, - type = typeof(Transform), - propertyName = "m_LocalRotation.z" - }, rotationCurveZ); - AnimationUtility.SetEditorCurve(clip, - new EditorCurveBinding - { - path = cleanPath, - type = typeof(Transform), - propertyName = "m_LocalRotation.w" - }, rotationCurveW); - } - - clip.EnsureQuaternionContinuity(); - return clip; - } // 스켈레톤 생성 후 FBX 내보내기 메서드 private void ExportSkeletonWithAnimationToFBX(string fbxPath, bool useAscii = true) @@ -1273,12 +1102,7 @@ namespace Entum EditorApplication.delayCall += () => ExportSkeletonWithAnimationToFBXStepByStep(fbxPath, useAscii); } - // Biped 스켈레톤 생성 후 FBX 내보내기 메서드 - private void ExportBipedSkeletonWithAnimationToFBX(string fbxPath, bool useAscii = true) - { - // EditorApplication.delayCall을 사용하여 다음 프레임에서 실행 - EditorApplication.delayCall += () => ExportBipedSkeletonWithAnimationToFBXStepByStep(fbxPath, useAscii); - } + private void ExportSkeletonWithAnimationToFBXStepByStep(string fbxPath, bool useAscii = true) { @@ -1368,93 +1192,7 @@ namespace Entum } } - private void ExportBipedSkeletonWithAnimationToFBXStepByStep(string fbxPath, bool useAscii = true) - { - try - { - #if UNITY_EDITOR - - Debug.Log("Biped FBX 내보내기 시작..."); - - // 1단계: Biped 애니메이션 클립 생성 (메모리에서만) - Debug.Log("1단계: Biped 애니메이션 클립 생성 중..."); - var bipedClip = CreateBipedAnimationClip(); - if (bipedClip == null) - { - Debug.LogError("Biped 애니메이션 클립 생성에 실패했습니다."); - return; - } - - Debug.Log($"Biped 애니메이션 클립 생성 완료: {bipedClip.name} (길이: {bipedClip.length}초)"); - - // 2단계: Biped 스켈레톤 생성 - Debug.Log("2단계: Biped 스켈레톤 생성 중..."); - var skeletonRoot = CreateBipedSkeletonFromBoneData(); - if (skeletonRoot == null) - { - Debug.LogError("Biped 스켈레톤 생성에 실패했습니다."); - return; - } - - Debug.Log($"Biped 스켈레톤 생성 성공: {skeletonRoot.name} (자식 수: {skeletonRoot.transform.childCount})"); - - // 3단계: Animator 컴포넌트 추가 및 생성된 클립 연결 - Debug.Log("3단계: Animator 컴포넌트 설정 중..."); - var animatorComponent = AnimationHelper.SetupAnimatorComponent(skeletonRoot, bipedClip); - if (animatorComponent == null) - { - Debug.LogError("Animator 컴포넌트 설정에 실패했습니다."); - DestroyImmediate(skeletonRoot); - return; - } - - Debug.Log($"Animator 컴포넌트 설정 성공"); - - // 4단계: Animator 컴포넌트 상태 상세 확인 - Debug.Log("4단계: Animator 컴포넌트 디버그 정보 출력 중..."); - AnimationHelper.DebugAnimatorComponent(animatorComponent); - - // 5단계: FBX Exporter 패키지 사용하여 내보내기 - Debug.Log("5단계: Biped FBX 내보내기 중..."); - bool exportSuccess = ExportSkeletonWithAnimationUsingFBXExporter(skeletonRoot, fbxPath, useAscii); - - if (exportSuccess) - { - Debug.Log($"✅ Biped FBX 내보내기 성공: {fbxPath}"); - - // 6단계: FBX 파일 설정 조정 - Debug.Log("6단계: FBX 설정 조정 중..."); - AnimationHelper.AdjustFBXImporterSettings(fbxPath); - } - else - { - Debug.LogError("❌ Biped FBX 내보내기에 실패했습니다."); - } - - // 7단계: 정리 (메모리에서 클립 언로드) - Debug.Log("7단계: 정리 중..."); - DestroyImmediate(skeletonRoot); - - // 메모리에서 애니메이션 클립 정리 - if (bipedClip != null) - { - Debug.Log($"메모리에서 애니메이션 클립 정리: {bipedClip.name}"); - // Unity가 자동으로 메모리에서 언로드하도록 함 - } - - // 가비지 컬렉션 강제 실행 (선택사항) - System.GC.Collect(); - - Debug.Log("✅ Biped FBX 내보내기 완료!"); - - # endif - - } - catch (System.Exception e) - { - Debug.LogError($"Biped FBX 내보내기 실패: {e.Message}\n{e.StackTrace}"); - } - } + @@ -1596,6 +1334,13 @@ namespace Entum importer.animationType = ModelImporterAnimationType.Generic; importer.animationCompression = ModelImporterAnimationCompression.Off; + // 본 이름 설정 - 띄어쓰기 유지 + importer.importBlendShapes = false; + importer.importVisibility = false; + importer.importCameras = false; + importer.importLights = false; + + // 애니메이션 클립 설정 var clipSettings = importer.defaultClipAnimations; if (clipSettings.Length > 0) @@ -1794,21 +1539,28 @@ namespace Entum Debug.LogWarning("ExportFormat 속성을 찾을 수 없습니다."); } - // 애니메이션 포함 설정 - var includeAnimationProperty = exportModelOptionsType.GetProperty("IncludeAnimation"); - if (includeAnimationProperty != null) + // ExportModelOptions의 모든 속성 확인 + var properties = exportModelOptionsType.GetProperties(); + Debug.Log("ExportModelOptions의 모든 속성:"); + foreach (var property in properties) { - includeAnimationProperty.SetValue(exportOptions, true); - Debug.Log("애니메이션 포함 설정: true"); + Debug.Log($" - {property.Name}: {property.PropertyType.Name}"); + } + + // UseMayaCompatibleNames 속성이 존재하는지 확인 후 설정 + var useMayaCompatibleNamesProperty = exportModelOptionsType.GetProperty("UseMayaCompatibleNames"); + if (useMayaCompatibleNamesProperty != null) + { + useMayaCompatibleNamesProperty.SetValue(exportOptions, false); + Debug.Log("UseMayaCompatibleNames 속성을 false로 설정했습니다."); } else { - Debug.LogWarning("IncludeAnimation 속성을 찾을 수 없습니다."); + Debug.LogWarning("UseMayaCompatibleNames 속성을 찾을 수 없습니다."); } // 스켈레톤만 내보내기 (Animation 컴포넌트가 포함됨) var objectsToExport = new UnityEngine.Object[] { skeletonRoot }; - Debug.Log($"내보낼 오브젝트: {objectsToExport.Length}개"); Debug.Log($"1. 스켈레톤 (Animation 컴포넌트 포함): {skeletonRoot.name}"); @@ -1882,17 +1634,43 @@ namespace Entum { var bone = bones[i]; Debug.Log($"본 {i}: '{bone.Name}' - 위치: {bone.LocalPosition}, 회전: {bone.LocalRotation}"); + + // Bip001 Pelvis가 포함된 본 특별 표시 + if (bone.Name.Contains("Bip001 Pelvis")) + { + Debug.LogWarning($"*** Bip001 Pelvis 발견! 인덱스: {i} ***"); + Debug.LogWarning($" 전체 경로: '{bone.Name}'"); + Debug.LogWarning($" LocalPosition: {bone.LocalPosition}"); + Debug.LogWarning($" LocalRotation: {bone.LocalRotation}"); + Debug.LogWarning($" LocalRotation (Euler): {bone.LocalRotation.eulerAngles}"); + } } Debug.Log("=== 분석 완료 ==="); - // 루트 GameObject 생성 (스크립터블 에셋 이름 사용) + // 본 데이터에서 첫 번째 경로 부분을 찾기 (Bip001 등) + string firstBoneName = ""; + foreach (var bone in bones) + { + if (string.IsNullOrEmpty(bone.Name)) + continue; + + // 첫 번째 경로 부분을 찾기 + var pathParts = bone.Name.Split('/'); + if (pathParts.Length > 0) + { + firstBoneName = pathParts[0]; + break; + } + } + + // 루트 GameObject 생성 (스켈레톤 이름 사용, Bip001 위에 루트 생성) string rootName = this.name; if (string.IsNullOrEmpty(rootName)) { rootName = "Skeleton"; } var root = new GameObject(rootName); - Debug.Log($"루트 GameObject 생성됨: {root.name}"); + Debug.Log($"루트 GameObject 생성됨: {root.name} (첫 번째 본: {firstBoneName})"); // 본 계층 구조 생성 var boneGameObjects = new Dictionary(); @@ -1946,6 +1724,8 @@ namespace Entum Debug.Log($"중복 제거 후 고유 본 경로 수: {uniqueBonePaths.Count}"); + + foreach (var kvp in uniqueBonePaths) { var bonePath = kvp.Key; @@ -1968,17 +1748,19 @@ namespace Entum if (!boneGameObjects.ContainsKey(currentPath)) { - // 루트 본이 본 데이터에 포함되어 있고, 현재 처리 중인 본이 루트인 경우 - if (hasRootInData && i == 0 && pathParts.Length == 1 && rootBones.Contains(part)) + // 첫 번째 경로 부분이 첫 번째 본인 경우 (Bip001 등) + if (i == 0 && part == firstBoneName) { - // 루트 본은 이미 생성된 루트 GameObject를 사용 - boneGameObjects[currentPath] = root; - Debug.Log($"루트 본 '{part}'을 기존 루트 GameObject에 연결 (중복 방지)"); + // 첫 번째 본을 루트 하위에 생성 + var firstBoneGO = new GameObject(part); + firstBoneGO.transform.SetParent(root.transform); + firstBoneGO.transform.localPosition = bone.LocalPosition; + firstBoneGO.transform.localRotation = bone.LocalRotation; + createdBones++; + Debug.Log($"첫 번째 본 '{part}'을 루트 하위에 생성: 위치={bone.LocalPosition}, 회전={bone.LocalRotation}"); - // 루트의 위치와 회전을 설정 - root.transform.localPosition = bone.LocalPosition; - root.transform.localRotation = bone.LocalRotation; - Debug.Log($"루트 설정: 위치={bone.LocalPosition}, 회전={bone.LocalRotation}"); + boneGameObjects[currentPath] = firstBoneGO; + parent = firstBoneGO; continue; } @@ -1989,6 +1771,18 @@ namespace Entum // 첫 번째 포즈의 위치와 회전 설정 if (i == pathParts.Length - 1) // 마지막 부분 (실제 본) { + // Bip001 Pelvis 특별 디버깅 + if (part == "Bip001 Pelvis") + { + Debug.LogWarning($"=== Bip001 Pelvis 특별 디버깅 ==="); + Debug.LogWarning($"본 데이터에서 가져온 값:"); + Debug.LogWarning($" LocalPosition: {bone.LocalPosition}"); + Debug.LogWarning($" LocalRotation: {bone.LocalRotation}"); + Debug.LogWarning($" LocalRotation (Euler): {bone.LocalRotation.eulerAngles}"); + Debug.LogWarning($"본 경로: {bonePath}"); + Debug.LogWarning($"================================"); + } + boneGO.transform.localPosition = bone.LocalPosition; boneGO.transform.localRotation = bone.LocalRotation; Debug.Log($"본 설정: {part} - 위치: {bone.LocalPosition}, 회전: {bone.LocalRotation}"); @@ -2009,6 +1803,9 @@ namespace Entum // 스켈레톤 구조 출력 PrintSkeletonHierarchy(root); + // Bip001 Pelvis 강제 수정 + FixBip001PelvisTransform(root); + // 스켈레톤이 실제로 씬에 있는지 확인 if (root != null && root.transform.childCount > 0) { @@ -2022,156 +1819,6 @@ namespace Entum } } - // 본 데이터로부터 Biped 스켈레톤 생성 - private GameObject CreateBipedSkeletonFromBoneData() - { - if (Poses.Count == 0 || Poses[0].HumanoidBones.Count == 0) - { - Debug.LogError("본 데이터가 없습니다."); - return null; - } - - var firstPose = Poses[0]; - var bones = firstPose.HumanoidBones; - - Debug.Log($"Biped 스켈레톤 생성 시작: {bones.Count}개의 본 데이터"); - - // 본 데이터 구조 확인 - for (int i = 0; i < Math.Min(bones.Count, 10); i++) // 처음 10개만 출력 - { - var bone = bones[i]; - Debug.Log($"본 {i}: '{bone.Name}' - 위치: {bone.LocalPosition}, 회전: {bone.LocalRotation}"); - } - - // Bip001 루트 GameObject 생성 (스크립터블 에셋 이름 사용) - string rootName = this.name; - if (string.IsNullOrEmpty(rootName)) - { - rootName = "Bip001"; - } - var root = new GameObject(rootName); - Debug.Log($"Bip001 루트 GameObject 생성됨: {root.name}"); - - // 본 계층 구조 생성 - var boneGameObjects = new Dictionary(); - int createdBones = 0; - - // 루트가 본 데이터에 포함되어 있는지 확인 - bool hasRootInData = false; - string rootBoneName = ""; - - foreach (var bone in bones) - { - if (string.IsNullOrEmpty(bone.Name)) - { - Debug.LogWarning("빈 본 이름 발견, 건너뜀"); - continue; - } - - // 루트 본인지 확인 (경로가 비어있거나 단일 이름인 경우) - if (string.IsNullOrEmpty(bone.Name) || !bone.Name.Contains("/")) - { - hasRootInData = true; - rootBoneName = bone.Name; - Debug.Log($"본 데이터에 루트가 포함됨: '{rootBoneName}'"); - break; - } - } - - foreach (var bone in bones) - { - if (string.IsNullOrEmpty(bone.Name)) - { - Debug.LogWarning("빈 본 이름 발견, 건너뜀"); - continue; - } - - Debug.Log($"본 처리 중: {bone.Name}"); - - // 본 경로를 '/'로 분할하여 계층 구조 생성 - var pathParts = bone.Name.Split('/'); - var currentPath = ""; - GameObject parent = root; - - for (int i = 0; i < pathParts.Length; i++) - { - var part = pathParts[i]; - if (string.IsNullOrEmpty(part)) - continue; - - currentPath = string.IsNullOrEmpty(currentPath) ? part : currentPath + "/" + part; - - if (!boneGameObjects.ContainsKey(currentPath)) - { - // 루트 본이 본 데이터에 포함되어 있고, 현재 처리 중인 본이 루트인 경우 - if (hasRootInData && i == 0 && pathParts.Length == 1 && part == rootBoneName) - { - // 루트 본은 이미 생성된 루트 GameObject를 사용 - boneGameObjects[currentPath] = root; - Debug.Log($"루트 본 '{part}'을 기존 루트 GameObject에 연결"); - continue; - } - - var boneGO = new GameObject(part); - boneGO.transform.SetParent(parent.transform); - createdBones++; - - // 첫 번째 포즈의 위치와 회전 설정 - if (i == pathParts.Length - 1) // 마지막 부분 (실제 본) - { - // Bip001 Pelvis인 경우 특별 처리 - if (bone.Name.Contains("Pelvis") && bone.Name.Contains("Bip001")) - { - // Bip001 Pelvis는 로컬 위치를 0,0,0으로 고정 - boneGO.transform.localPosition = Vector3.zero; - // Bip001 Pelvis는 기본 회전 오프셋 (0, -90, -90) 유지 - boneGO.transform.localRotation = Quaternion.Euler(0, -90, -90); - Debug.Log($"Bip001 Pelvis 설정: {part} - 위치: {Vector3.zero}, 회전: {Quaternion.Euler(0, -90, -90)}"); - } - else if (pathParts.Length == 1 && bone.Name.Contains("Pelvis")) - { - // Bip001 루트에 Pelvis 위치 데이터 설정 - boneGO.transform.localPosition = bone.LocalPosition; - boneGO.transform.localRotation = Quaternion.Euler(0, -90, -90); - Debug.Log($"Bip001 루트 설정: {part} - 위치: {bone.LocalPosition}, 회전: {Quaternion.Euler(0, -90, -90)}"); - } - else - { - // 일반 본은 기존 방식대로 처리 - boneGO.transform.localPosition = bone.LocalPosition; - boneGO.transform.localRotation = bone.LocalRotation; - Debug.Log($"본 설정: {part} - 위치: {bone.LocalPosition}, 회전: {bone.LocalRotation}"); - } - } - - boneGameObjects[currentPath] = boneGO; - parent = boneGO; - } - else - { - parent = boneGameObjects[currentPath]; - } - } - } - - Debug.Log($"Biped 스켈레톤 생성 완료: {createdBones}개의 GameObject 생성됨"); - - // 스켈레톤 구조 출력 - PrintSkeletonHierarchy(root); - - // 스켈레톤이 실제로 씬에 있는지 확인 - if (root != null && root.transform.childCount > 0) - { - Debug.Log($"✅ Biped 스켈레톤 생성 성공: 루트={root.name}, 자식 수={root.transform.childCount}"); - return root; - } - else - { - Debug.LogError("❌ Biped 스켈레톤 생성 실패: 자식이 없음"); - return null; - } - } - // 스켈레톤 계층 구조 출력 (디버깅용) private void PrintSkeletonHierarchy(GameObject root, string indent = "") { @@ -2183,20 +1830,6 @@ namespace Entum } } - // 스켈레톤에서 모든 본 GameObject 찾기 - private void FindAllBoneGameObjects(GameObject root, Dictionary boneGameObjects) - { - var allChildren = root.GetComponentsInChildren(); - foreach (var child in allChildren) - { - if (child != root.transform) - { - string path = GetRelativePath(root.transform, child); - boneGameObjects[path] = child.gameObject; - } - } - } - // 상대 경로 가져오기 private string GetRelativePath(Transform root, Transform target) { @@ -2213,11 +1846,59 @@ namespace Entum return string.Join("/", pathList); } - // 애니메이션 속성 경로 가져오기 - private string GetPropertyPath(GameObject boneGO, GameObject root) + // Bip001 Pelvis 강제 수정 메서드 + private void FixBip001PelvisTransform(GameObject root) { - return GetRelativePath(root.transform, boneGO.transform); + Debug.LogWarning("=== Bip001 Pelvis 강제 수정 시작 ==="); + + // 스켈레톤에서 Bip001 Pelvis 찾기 + Transform bip001Pelvis = FindBip001PelvisRecursive(root.transform); + + if (bip001Pelvis != null) + { + Debug.LogWarning($"Bip001 Pelvis 발견: {bip001Pelvis.name}"); + Debug.LogWarning($"수정 전 - 위치: {bip001Pelvis.localPosition}, 회전: {bip001Pelvis.localRotation.eulerAngles}"); + + // 강제로 올바른 값 적용 + bip001Pelvis.localPosition = Vector3.zero; + bip001Pelvis.localRotation = Quaternion.Euler(-90, 0, 90); + + Debug.LogWarning($"수정 후 - 위치: {bip001Pelvis.localPosition}, 회전: {bip001Pelvis.localRotation.eulerAngles}"); + Debug.LogWarning("Bip001 Pelvis 강제 수정 완료!"); + } + else + { + Debug.LogWarning("Bip001 Pelvis를 찾을 수 없습니다."); + } + + Debug.LogWarning("=== Bip001 Pelvis 강제 수정 완료 ==="); } + + // 재귀적으로 Bip001 Pelvis 찾기 + private Transform FindBip001PelvisRecursive(Transform parent) + { + // 현재 Transform이 Bip001 Pelvis인지 확인 + if (parent.name == "Bip001 Pelvis") + { + return parent; + } + + // 자식들에서 재귀적으로 찾기 + for (int i = 0; i < parent.childCount; i++) + { + var child = parent.GetChild(i); + var result = FindBip001PelvisRecursive(child); + if (result != null) + { + return result; + } + } + + return null; + } + + + #endif [Serializable] @@ -2287,12 +1968,7 @@ namespace Entum // 경로 끝의 슬래시 제거 path = path.TrimEnd('/'); - // Unity 애니메이션 시스템에서 루트 오브젝트 이름을 제거하는 경우를 대비 - // 루트가 "Bip001"인 경우, 경로에서 "Bip001/" 부분을 제거 - if (root.name == "Bip001" && path.StartsWith("Bip001/")) - { - path = path.Substring("Bip001/".Length); - } + _pathCache.Add(target, path); return path; diff --git a/Assets/External/EasyMotionRecorder/Scripts/MotionDataPlayer.cs b/Assets/External/EasyMotionRecorder/Scripts/MotionDataPlayer.cs index 2fa3b9e4d..4fb688796 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/MotionDataPlayer.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/MotionDataPlayer.cs @@ -43,6 +43,9 @@ namespace Entum [SerializeField, Tooltip("rootBoneSystemがOBJECTROOTの時は使われないパラメータです。")] private HumanBodyBones _targetRootBone = HumanBodyBones.Hips; + [HideInInspector, SerializeField] private string instanceID = ""; + [HideInInspector, SerializeField] private bool useDontDestroyOnLoad = false; + private HumanPoseHandler _poseHandler; private Action _onPlayFinish; private float _playingTime; @@ -56,6 +59,17 @@ namespace Entum return; } + // 인스턴스 ID가 비어있으면 자동 생성 + if (string.IsNullOrEmpty(instanceID)) + { + instanceID = System.Guid.NewGuid().ToString().Substring(0, 8); + } + + // DontDestroyOnLoad 설정 (선택적) + if (useDontDestroyOnLoad) + { + DontDestroyOnLoad(gameObject); + } _poseHandler = new HumanPoseHandler(_animator.avatar, _animator.transform); _onPlayFinish += StopMotion; @@ -82,7 +96,6 @@ namespace Entum return; } - _playingTime += Time.deltaTime; SetHumanPose(); } @@ -99,19 +112,17 @@ namespace Entum if (RecordedMotionData == null) { - Debug.LogError("録画済みモーションデータが指定されていません。再生を行いません。"); + Debug.LogError("재생할 모션 데이터가 없습니다."); return; } - - _playingTime = _startFrame * (Time.deltaTime / 1f); - _frameIndex = _startFrame; _playing = true; + _frameIndex = _startFrame; + _playingTime = 0f; + + Debug.Log($"모션 재생 시작 - 인스턴스: {instanceID}, 시작 프레임: {_startFrame}"); } - /// - /// モーションデータ再生終了。フレーム数が最後になっても自動で呼ばれる - /// private void StopMotion() { if (!_playing) @@ -119,52 +130,90 @@ namespace Entum return; } - - _playingTime = 0f; - _frameIndex = _startFrame; _playing = false; + Debug.Log($"모션 재생 중지 - 인스턴스: {instanceID}, 재생된 프레임: {_frameIndex}"); } private void SetHumanPose() { - var pose = new HumanPose(); - pose.muscles = RecordedMotionData.Poses[_frameIndex].Muscles; - _poseHandler.SetHumanPose(ref pose); - pose.bodyPosition = RecordedMotionData.Poses[_frameIndex].BodyPosition; - pose.bodyRotation = RecordedMotionData.Poses[_frameIndex].BodyRotation; + if (RecordedMotionData == null || RecordedMotionData.Poses == null || RecordedMotionData.Poses.Count == 0) + { + StopMotion(); + return; + } + if (_frameIndex >= RecordedMotionData.Poses.Count) + { + StopMotion(); + _onPlayFinish?.Invoke(); + return; + } + + var pose = RecordedMotionData.Poses[_frameIndex]; + var humanPose = new HumanPose(); + + // 본 데이터 설정 + if (pose.HumanoidBones != null) + { + foreach (var bone in pose.HumanoidBones) + { + // HumanBodyBones enum으로 변환 + HumanBodyBones bodyBone; + if (System.Enum.TryParse(bone.Name, out bodyBone)) + { + var boneTransform = _animator.GetBoneTransform(bodyBone); + if (boneTransform != null) + { + boneTransform.localPosition = bone.LocalPosition; + boneTransform.localRotation = bone.LocalRotation; + } + } + } + } + + // 근육 데이터 설정 + if (pose.Muscles != null && pose.Muscles.Length > 0) + { + humanPose.muscles = pose.Muscles; + } + + // 루트 본 설정 switch (_rootBoneSystem) { case MotionDataSettings.Rootbonesystem.Objectroot: - //_animator.transform.localPosition = RecordedMotionData.Poses[_frameIndex].BodyRootPosition; - //_animator.transform.localRotation = RecordedMotionData.Poses[_frameIndex].BodyRootRotation; + _animator.transform.localPosition = pose.BodyRootPosition; + _animator.transform.localRotation = pose.BodyRootRotation; break; case MotionDataSettings.Rootbonesystem.Hipbone: - pose.bodyPosition = RecordedMotionData.Poses[_frameIndex].BodyPosition; - pose.bodyRotation = RecordedMotionData.Poses[_frameIndex].BodyRotation; - - _animator.GetBoneTransform(_targetRootBone).position = RecordedMotionData.Poses[_frameIndex].BodyRootPosition; - _animator.GetBoneTransform(_targetRootBone).rotation = RecordedMotionData.Poses[_frameIndex].BodyRootRotation; + var hipBone = _animator.GetBoneTransform(_targetRootBone); + if (hipBone != null) + { + hipBone.position = pose.BodyRootPosition; + hipBone.rotation = pose.BodyRootRotation; + } break; - - default: - throw new ArgumentOutOfRangeException(); } - //処理落ちしたモーションデータの再生速度調整 - if (_playingTime > RecordedMotionData.Poses[_frameIndex].Time) - { - _frameIndex++; - } + // HumanPose 적용 + _poseHandler.SetHumanPose(ref humanPose); - if (_frameIndex == RecordedMotionData.Poses.Count - 1) - { - if (_onPlayFinish != null) - { - _onPlayFinish(); - } - } + _frameIndex++; + } + + public void SetInstanceID(string id) + { + instanceID = id; + } + + public void SetUseDontDestroyOnLoad(bool use) + { + useDontDestroyOnLoad = use; + } + + public string GetInstanceID() + { + return instanceID; } } } \ No newline at end of file diff --git a/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs b/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs index 252b27fc1..dbf63f4bf 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs @@ -12,6 +12,7 @@ using System; using System.IO; using System.Reflection; using System.Collections.Generic; +using System.Linq; #if UNITY_EDITOR using UnityEditor; #endif @@ -53,10 +54,14 @@ namespace Entum [SerializeField, Tooltip("녹화 시작 시 T-포즈를 별도로 저장할지 여부 (출력 시 0프레임에 포함)")] private bool _recordTPoseAtStart = true; + [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; @@ -66,7 +71,6 @@ namespace Entum [Tooltip("記録するFPS。0で制限しない。UpdateのFPSは超えられません。")] public float TargetFPS = 60.0f; - // Use this for initialization private void Awake() { @@ -77,7 +81,58 @@ namespace Entum return; } - _poseHandler = new HumanPoseHandler(_animator.avatar, _animator.transform); + // SavePathManager 자동 찾기 또는 추가 + if (_savePathManager == null) + { + _savePathManager = GetComponent(); + if (_savePathManager == null) + { + _savePathManager = gameObject.AddComponent(); + } + } + + // 인스턴스 ID가 비어있으면 자동 생성 + if (string.IsNullOrEmpty(instanceID)) + { + instanceID = System.Guid.NewGuid().ToString().Substring(0, 8); + } + + // SessionID 생성 (인스턴스 ID 제외) + SessionID = DateTime.Now.ToString("yyMMdd_HHmmss"); + + // HumanPoseHandler 안전한 초기화 + if (_animator.avatar != null && _animator.isHuman) + { + try + { + _poseHandler = new HumanPoseHandler(_animator.avatar, _animator.transform); + Debug.Log($"HumanPoseHandler 초기화 완료 - 아바타: {_animator.avatar.name}"); + } + catch (System.Exception e) + { + Debug.LogError($"HumanPoseHandler 초기화 실패: {e.Message}"); + _poseHandler = null; + } + } + else + { + Debug.LogError($"아바타가 휴머노이드가 아니거나 Avatar가 설정되지 않았습니다. " + + $"Avatar: {_animator.avatar}, IsHuman: {_animator.isHuman}"); + _poseHandler = null; + } + } + + // 내부 메서드들 (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() @@ -111,22 +166,29 @@ namespace Entum { return; } - if (FrameIndex % TargetFPS == 0) + if (FrameIndex % TargetFPS == 0 && FrameIndex > 0 && RecordedTime > 0) { - print("Motion_FPS=" + 1 / (RecordedTime / FrameIndex)); + print("Motion_FPS=" + (FrameIndex / RecordedTime)); } } else { if (Time.frameCount % Application.targetFrameRate == 0) { - print("Motion_FPS=" + 1 / Time.deltaTime); + print("Motion_FPS=" + (1 / Time.deltaTime)); } } //現在のフレームのHumanoidの姿勢を取得 + if (_poseHandler == null) + { + Debug.LogError("PoseHandler가 초기화되지 않았습니다. 녹화를 중단합니다."); + RecordEnd(); + return; + } + _poseHandler.GetHumanPose(ref _currentPose); - //posesに取得した姿勢を書き込む + //posesに取得한姿勢를書き込む var serializedPose = new HumanoidPoses.SerializeHumanoidPose(); switch (_rootBoneSystem) @@ -137,19 +199,51 @@ namespace Entum break; case MotionDataSettings.Rootbonesystem.Hipbone: - serializedPose.BodyRootPosition = _animator.GetBoneTransform(_targetRootBone).position; - serializedPose.BodyRootRotation = _animator.GetBoneTransform(_targetRootBone).rotation; - Debug.LogWarning(_animator.GetBoneTransform(_targetRootBone).position); + var hipBone = _animator.GetBoneTransform(_targetRootBone); + if (hipBone != null) + { + serializedPose.BodyRootPosition = hipBone.position; + serializedPose.BodyRootRotation = hipBone.rotation; + } + else + { + Debug.LogWarning($"타겟 루트 본 {_targetRootBone}을 찾을 수 없습니다. Object Root로 대체합니다."); + serializedPose.BodyRootPosition = _animator.transform.localPosition; + serializedPose.BodyRootRotation = _animator.transform.localRotation; + } break; default: throw new ArgumentOutOfRangeException(); } + var bodyTQ = new TQ(_currentPose.bodyPosition, _currentPose.bodyRotation); - var LeftFootTQ = new TQ(_animator.GetBoneTransform(IK_LeftFootBone).position, _animator.GetBoneTransform(IK_LeftFootBone).rotation); - var RightFootTQ = new TQ(_animator.GetBoneTransform(IK_RightFootBone).position, _animator.GetBoneTransform(IK_RightFootBone).rotation); - LeftFootTQ = AvatarUtility.GetIKGoalTQ(_animator.avatar, _animator.humanScale, AvatarIKGoal.LeftFoot, bodyTQ, LeftFootTQ); - RightFootTQ = AvatarUtility.GetIKGoalTQ(_animator.avatar, _animator.humanScale, AvatarIKGoal.RightFoot, bodyTQ, RightFootTQ); + + // 발 본 안전성 검사 + var leftFootBone = _animator.GetBoneTransform(IK_LeftFootBone); + var rightFootBone = _animator.GetBoneTransform(IK_RightFootBone); + + TQ LeftFootTQ, RightFootTQ; + + if (leftFootBone != null) + { + LeftFootTQ = new TQ(leftFootBone.position, leftFootBone.rotation); + } + else + { + Debug.LogWarning($"왼발 본 {IK_LeftFootBone}을 찾을 수 없습니다. 기본값을 사용합니다."); + LeftFootTQ = new TQ(Vector3.zero, Quaternion.identity); + } + + if (rightFootBone != null) + { + RightFootTQ = new TQ(rightFootBone.position, rightFootBone.rotation); + } + else + { + Debug.LogWarning($"오른발 본 {IK_RightFootBone}을 찾을 수 없습니다. 기본값을 사용합니다."); + RightFootTQ = new TQ(Vector3.zero, Quaternion.identity); + } serializedPose.BodyPosition = bodyTQ.t; serializedPose.BodyRotation = bodyTQ.q; @@ -158,23 +252,21 @@ namespace Entum serializedPose.RightfootIK_Pos = RightFootTQ.t; serializedPose.RightfootIK_Rot = RightFootTQ.q; - serializedPose.FrameCount = FrameIndex; serializedPose.Muscles = new float[_currentPose.muscles.Length]; - serializedPose.Time = RecordedTime; - for (int i = 0; i < serializedPose.Muscles.Length; i++) + for (int i = 0; i < _currentPose.muscles.Length; i++) { serializedPose.Muscles[i] = _currentPose.muscles[i]; } + serializedPose.FrameCount = FrameIndex; + serializedPose.Time = RecordedTime; + SetHumanBoneTransformToHumanoidPoses(_animator, ref serializedPose); Poses.Poses.Add(serializedPose); FrameIndex++; } - /// - /// 録画開始 - /// private void RecordStart() { if (_recording) @@ -182,158 +274,153 @@ namespace Entum return; } - // 세션 ID 생성 (년도는 2자리로 표시, 고유 ID 제거) - SessionID = DateTime.Now.ToString("yyMMdd_HHmmss"); - - Poses = ScriptableObject.CreateInstance(); - Poses.AvatarName = _animator.name; // 아바타 이름 설정 - - if (OnRecordStart != null) + // PoseHandler 유효성 검사 + if (_poseHandler == null) { - OnRecordStart(); + Debug.LogError("HumanPoseHandler가 초기화되지 않았습니다. 녹화를 시작할 수 없습니다."); + return; + } + + // 세션 ID 생성 (인스턴스 ID 제외) + SessionID = DateTime.Now.ToString("yyMMdd_HHmmss"); + + Poses = ScriptableObject.CreateInstance(); + Poses.AvatarName = _animator.name; + Poses.Poses = new List(); + Poses.SessionID = SessionID; + Poses.InstanceID = instanceID; // 인스턴스 ID 설정 + + // T포즈 별도 저장 옵션이 활성화된 경우 + if (_recordTPoseAtStart) + { + try + { + RecordTPoseAsFirstFrame(); + Debug.Log("T포즈 데이터가 성공적으로 기록되었습니다."); + } + catch (System.Exception e) + { + Debug.LogError($"T포즈 녹화 중 오류 발생: {e.Message}"); + // T포즈 실패해도 일반 녹화는 계속 진행 + } } - OnRecordEnd += WriteAnimationFile; - _recording = true; RecordedTime = 0f; StartTime = Time.time; FrameIndex = 0; - - // 1프레임에 T-포즈 저장 - if (_recordTPoseAtStart) - { - RecordTPoseAsFirstFrame(); - } + _recording = true; + + Debug.Log($"모션 녹화 시작 - 인스턴스: {instanceID}, 세션: {SessionID}, T포즈 옵션: {_recordTPoseAtStart}"); + + OnRecordStart?.Invoke(); } - - /// - /// T-포즈를 즉시 저장합니다. - /// + private void RecordTPoseAsFirstFrame() { - try - { - Debug.Log("T-포즈 즉시 저장 시작..."); - - // 현재 포즈를 T-포즈로 설정 - SetTPose(_animator); - - // T-포즈 설정 직후 즉시 데이터 수집 - RecordTPoseData(); - } - catch (System.Exception e) - { - Debug.LogError($"T-포즈 저장 중 오류 발생: {e.Message}"); - Debug.LogError($"스택 트레이스: {e.StackTrace}"); - } + Debug.Log("T-포즈를 첫 번째 프레임으로 저장합니다."); + SetTPose(_animator); + RecordTPoseData(); } - - /// - /// 지정된 Animator의 포즈를 T-포즈로 설정합니다. - /// - /// T-포즈를 설정할 Animator + private void SetTPose(Animator animator) { if (animator == null || animator.avatar == null) + { + Debug.LogWarning("Animator 또는 Avatar가 null입니다. T포즈 설정을 건너뜁니다."); return; - - Avatar avatar = animator.avatar; - Transform transform = animator.transform; - - // HumanPoseClip에 저장된 T-포즈 데이터를 로드하여 적용 - var humanPoseClip = Resources.Load(HumanPoseClip.TPoseResourcePath); - if (humanPoseClip != null) - { - var pose = humanPoseClip.GetPose(); - HumanPoseTransfer.SetPose(avatar, transform, pose); } - else - { - Debug.LogWarning("T-Pose 데이터가 존재하지 않습니다."); - } - } - - /// - /// T-포즈 데이터를 즉시 수집하여 저장 - /// - private void RecordTPoseData() - { + try { - Debug.Log("T-포즈 데이터 즉시 수집 시작..."); - - // T-포즈가 적용된 상태에서 현재 프레임의 Humanoid 포즈를 가져옴 - _poseHandler.GetHumanPose(ref _currentPose); - - Debug.Log($"T-포즈 데이터: BodyPosition={_currentPose.bodyPosition}, BodyRotation={_currentPose.bodyRotation}"); - Debug.Log($"T-포즈 Muscle 개수: {_currentPose.muscles.Length}"); - - // T-포즈 데이터를 별도로 저장 - var tPoseSerialized = new HumanoidPoses.SerializeHumanoidPose(); - - switch (_rootBoneSystem) + // HumanPoseClip에서 T-포즈 데이터 로드 시도 + var humanPoseClip = Resources.Load("T-Pose.pose"); + if (humanPoseClip != null) { - case MotionDataSettings.Rootbonesystem.Objectroot: - tPoseSerialized.BodyRootPosition = _animator.transform.localPosition; - tPoseSerialized.BodyRootRotation = _animator.transform.localRotation; - Debug.Log($"Objectroot 설정: BodyRootPosition={tPoseSerialized.BodyRootPosition}, BodyRootRotation={tPoseSerialized.BodyRootRotation}"); - break; - - case MotionDataSettings.Rootbonesystem.Hipbone: - tPoseSerialized.BodyRootPosition = _animator.GetBoneTransform(_targetRootBone).position; - tPoseSerialized.BodyRootRotation = _animator.GetBoneTransform(_targetRootBone).rotation; - Debug.Log($"Hipbone 설정: BodyRootPosition={tPoseSerialized.BodyRootPosition}, BodyRootRotation={tPoseSerialized.BodyRootRotation}"); - break; - - default: - throw new ArgumentOutOfRangeException(); + // T포즈 데이터를 적용 + var pose = humanPoseClip.GetPose(); + SetPoseToAnimator(animator.avatar, animator.transform, pose); + + Debug.Log("T-포즈가 HumanPoseClip에서 성공적으로 로드되었습니다."); + } + else + { + // HumanPoseClip이 없는 경우 기본 T포즈 설정 + Debug.LogWarning("T-Pose.pose 리소스를 찾을 수 없습니다. 기본 T포즈를 설정합니다."); + SetDefaultTPose(animator); } - var bodyTQ = new TQ(_currentPose.bodyPosition, _currentPose.bodyRotation); - var LeftFootTQ = new TQ(_animator.GetBoneTransform(IK_LeftFootBone).position, _animator.GetBoneTransform(IK_LeftFootBone).rotation); - var RightFootTQ = new TQ(_animator.GetBoneTransform(IK_RightFootBone).position, _animator.GetBoneTransform(IK_RightFootBone).rotation); - LeftFootTQ = AvatarUtility.GetIKGoalTQ(_animator.avatar, _animator.humanScale, AvatarIKGoal.LeftFoot, bodyTQ, LeftFootTQ); - RightFootTQ = AvatarUtility.GetIKGoalTQ(_animator.avatar, _animator.humanScale, AvatarIKGoal.RightFoot, bodyTQ, RightFootTQ); - - tPoseSerialized.BodyPosition = bodyTQ.t; - tPoseSerialized.BodyRotation = bodyTQ.q; - tPoseSerialized.LeftfootIK_Pos = LeftFootTQ.t; - tPoseSerialized.LeftfootIK_Rot = LeftFootTQ.q; - tPoseSerialized.RightfootIK_Pos = RightFootTQ.t; - tPoseSerialized.RightfootIK_Rot = RightFootTQ.q; - - tPoseSerialized.FrameCount = 0; // T-포즈는 0프레임으로 설정 - tPoseSerialized.Muscles = new float[_currentPose.muscles.Length]; - tPoseSerialized.Time = 0f; // T-포즈는 0초로 설정 - - for (int i = 0; i < tPoseSerialized.Muscles.Length; i++) + // UpperChest 본 위치 초기화 (카인드리타겟팅과 동일) + Transform upperChest = animator.GetBoneTransform(HumanBodyBones.UpperChest); + if (upperChest != null) { - tPoseSerialized.Muscles[i] = _currentPose.muscles[i]; + upperChest.localPosition = Vector3.zero; } - - Debug.Log($"T-포즈 Muscle 데이터 설정 완료: {tPoseSerialized.Muscles.Length}개"); - - SetHumanBoneTransformToHumanoidPoses(_animator, ref tPoseSerialized); - - Debug.Log($"T-포즈 본 데이터 설정 완료: {tPoseSerialized.HumanoidBones.Count}개 본"); - - // T-포즈를 별도 필드에 저장 - Poses.TPoseData = tPoseSerialized; - Poses.HasTPoseData = true; - - Debug.Log($"T-포즈가 별도로 저장되었습니다. (시간: 0초, 프레임: 0)"); - Debug.Log($"현재 Poses.Count: {Poses.Poses.Count} (T-포즈는 별도 저장됨)"); } catch (System.Exception e) { - Debug.LogError($"T-포즈 저장 중 오류 발생: {e.Message}"); - Debug.LogError($"스택 트레이스: {e.StackTrace}"); + Debug.LogError($"T포즈 설정 중 오류 발생: {e.Message}"); + SetDefaultTPose(animator); } } /// - /// 録画終了 + /// HumanPose를 Animator에 적용하는 내부 메서드 /// + private void SetPoseToAnimator(Avatar avatar, Transform transform, HumanPose pose) + { + var handler = new HumanPoseHandler(avatar, transform); + handler.SetHumanPose(ref pose); + handler.Dispose(); + } + + /// + /// 기본 T포즈 설정 (HumanPoseClip이 없는 경우 백업용) + /// + private void SetDefaultTPose(Animator animator) + { + Debug.Log("기본 T포즈 설정을 수행합니다."); + + // 기본 T포즈 생성 - Muscles를 0으로 설정 + var defaultTPose = new HumanPose(); + defaultTPose.bodyPosition = Vector3.zero; + defaultTPose.bodyRotation = Quaternion.identity; + + // 모든 머슬을 0으로 설정 (T포즈) + defaultTPose.muscles = new float[HumanTrait.MuscleCount]; + for (int i = 0; i < HumanTrait.MuscleCount; i++) + { + defaultTPose.muscles[i] = 0f; + } + + SetPoseToAnimator(animator.avatar, animator.transform, defaultTPose); + } + + private void RecordTPoseData() + { + _poseHandler.GetHumanPose(ref _currentPose); + var tPoseData = new HumanoidPoses.SerializeHumanoidPose(); + + // T-포즈 데이터 설정 + tPoseData.BodyPosition = _currentPose.bodyPosition; + tPoseData.BodyRotation = _currentPose.bodyRotation; + tPoseData.Muscles = new float[_currentPose.muscles.Length]; + for (int i = 0; i < _currentPose.muscles.Length; i++) + { + tPoseData.Muscles[i] = _currentPose.muscles[i]; + } + + tPoseData.FrameCount = 0; + tPoseData.Time = 0f; + + // 본 데이터 설정 + SetHumanBoneTransformToHumanoidPoses(_animator, ref tPoseData); + + Poses.TPoseData = tPoseData; + Poses.HasTPoseData = true; + + Debug.Log("T-포즈 데이터가 저장되었습니다."); + } + private void RecordEnd() { if (!_recording) @@ -341,175 +428,444 @@ namespace Entum return; } - - if (OnRecordEnd != null) - { - OnRecordEnd(); - } - - // 자동 출력 옵션 확인 -#if UNITY_EDITOR - if (SavePathManager.Instance != null && Poses != null) - { - if (SavePathManager.Instance.ExportHumanoidOnSave) - { - Poses.ExportHumanoidAnim(); - } - if (SavePathManager.Instance.ExportGenericOnSave) - { - Poses.ExportGenericAnim(); - } - } -#endif - - OnRecordEnd -= WriteAnimationFile; _recording = false; + Debug.Log($"모션 녹화 종료 - 인스턴스: {instanceID}, 총 프레임: {FrameIndex}"); + + WriteAnimationFile(); + OnRecordEnd?.Invoke(); } private static void SetHumanBoneTransformToHumanoidPoses(Animator animator, ref HumanoidPoses.SerializeHumanoidPose pose) { - // Humanoid 본만 수집하여 데이터 크기 최적화 - var humanBones = new List(); - - // Humanoid 본들만 수집 - foreach (HumanBodyBones boneType in System.Enum.GetValues(typeof(HumanBodyBones))) + pose.HumanoidBones = new List(); + + // 기존 avatar.humanDescription.human 본들 처리 + var humanBones = animator.avatar.humanDescription.human; + foreach (var bone in humanBones) { - if (boneType == HumanBodyBones.LastBone) continue; + // HumanBodyBones enum으로 변환 + HumanBodyBones bodyBone; + if (System.Enum.TryParse(bone.humanName, out bodyBone)) + { + var boneTransform = animator.GetBoneTransform(bodyBone); + if (boneTransform != null) + { + var humanoidBone = new HumanoidPoses.SerializeHumanoidPose.HumanoidBone(); + humanoidBone.Set(animator.transform, boneTransform); + + // 팔꿈치 본 특별 처리 + if (IsElbowBone(boneTransform)) + { + humanoidBone = ProcessElbowRotation(boneTransform, humanoidBone); + } + + pose.HumanoidBones.Add(humanoidBone); + } + } + } + + // 손가락 본들 추가 처리 (누락된 것들을 위해) + AddFingerBones(animator, ref pose); + } + + /// + /// 손가락 본들을 추가로 처리하는 메서드 + /// + private static void AddFingerBones(Animator animator, ref HumanoidPoses.SerializeHumanoidPose pose) + { + // 이미 추가된 본들의 이름을 저장 + var existingBoneNames = new HashSet(); + foreach (var bone in pose.HumanoidBones) + { + existingBoneNames.Add(bone.Name); + } + + // 모든 손가락 본들을 정의 + var fingerBones = new HumanBodyBones[] + { + // 왼손 손가락들 + HumanBodyBones.LeftThumbProximal, HumanBodyBones.LeftThumbIntermediate, HumanBodyBones.LeftThumbDistal, + HumanBodyBones.LeftIndexProximal, HumanBodyBones.LeftIndexIntermediate, HumanBodyBones.LeftIndexDistal, + HumanBodyBones.LeftMiddleProximal, HumanBodyBones.LeftMiddleIntermediate, HumanBodyBones.LeftMiddleDistal, + HumanBodyBones.LeftRingProximal, HumanBodyBones.LeftRingIntermediate, HumanBodyBones.LeftRingDistal, + HumanBodyBones.LeftLittleProximal, HumanBodyBones.LeftLittleIntermediate, HumanBodyBones.LeftLittleDistal, - var boneTransform = animator.GetBoneTransform(boneType); + // 오른손 손가락들 + HumanBodyBones.RightThumbProximal, HumanBodyBones.RightThumbIntermediate, HumanBodyBones.RightThumbDistal, + HumanBodyBones.RightIndexProximal, HumanBodyBones.RightIndexIntermediate, HumanBodyBones.RightIndexDistal, + HumanBodyBones.RightMiddleProximal, HumanBodyBones.RightMiddleIntermediate, HumanBodyBones.RightMiddleDistal, + HumanBodyBones.RightRingProximal, HumanBodyBones.RightRingIntermediate, HumanBodyBones.RightRingDistal, + HumanBodyBones.RightLittleProximal, HumanBodyBones.RightLittleIntermediate, HumanBodyBones.RightLittleDistal + }; + + // 각 손가락 본에 대해 처리 + foreach (var fingerBone in fingerBones) + { + var boneTransform = animator.GetBoneTransform(fingerBone); if (boneTransform != null) { - humanBones.Add(boneTransform); - } - } - - // 추가로 중요한 본들 (팔꿈치, 무릎 등) - var additionalBones = new string[] { "LeftElbow", "RightElbow", "LeftKnee", "RightKnee", "LeftAnkle", "RightAnkle" }; - foreach (var boneName in additionalBones) - { - var bone = animator.transform.Find(boneName); - if (bone != null && !humanBones.Contains(bone)) - { - humanBones.Add(bone); - } - } - - foreach (Transform bone in humanBones) - { - if (bone != null) - { - var boneData = new HumanoidPoses.SerializeHumanoidPose.HumanoidBone(); + var humanoidBone = new HumanoidPoses.SerializeHumanoidPose.HumanoidBone(); + humanoidBone.Set(animator.transform, boneTransform); - // 기존 Set 메서드 사용 - boneData.Set(animator.transform, bone); - - // 팔꿈치 특별 처리 - if (IsElbowBone(bone)) + // 중복 확인 (이미 추가되지 않은 경우만 추가) + if (!existingBoneNames.Contains(humanoidBone.Name)) { - boneData = ProcessElbowRotation(bone, boneData); + pose.HumanoidBones.Add(humanoidBone); + existingBoneNames.Add(humanoidBone.Name); + //Debug.Log($"손가락 본 추가: {fingerBone} -> {humanoidBone.Name}"); } - - pose.HumanoidBones.Add(boneData); } } } - + private static bool IsElbowBone(Transform bone) { - // 팔꿈치 본 식별 - string boneName = bone.name.ToLower(); - return boneName.Contains("elbow") || boneName.Contains("forearm") || - boneName.Contains("arm") && boneName.Contains("02"); + return bone.name.Contains("Elbow") || bone.name.Contains("elbow"); } - + private static HumanoidPoses.SerializeHumanoidPose.HumanoidBone ProcessElbowRotation( Transform elbow, HumanoidPoses.SerializeHumanoidPose.HumanoidBone boneData) { - // 팔꿈치 회전 안정화 처리 - Quaternion currentRotation = elbow.localRotation; - - // 팔이 펴진 상태 감지 - if (elbow.parent != null && elbow.childCount > 0) - { - Vector3 armDirection = (elbow.position - elbow.parent.position).normalized; - Vector3 forearmDirection = (elbow.GetChild(0).position - elbow.position).normalized; - - float armAngle = Vector3.Angle(armDirection, forearmDirection); - - // 팔이 거의 펴진 상태일 때 회전 보정 - if (armAngle > 170f) - { - // Quaternion 보간을 사용하여 부드러운 전환 - Quaternion targetRotation = Quaternion.LookRotation(forearmDirection, Vector3.up); - boneData.LocalRotation = Quaternion.Slerp(currentRotation, targetRotation, 0.1f); - } - else - { - boneData.LocalRotation = currentRotation; - } - } - + // 팔꿈치 회전 보정 로직 + var localRotation = elbow.localRotation; + boneData.LocalRotation = localRotation; return boneData; } protected virtual void WriteAnimationFile() { -#if UNITY_EDITOR - // SavePathManager 사용 - string savePath = "Assets/Resources"; // 기본값 - string fileName = $"{SessionID}_{_animator.name}_Motion.asset"; - - // SavePathManager가 있으면 사용 - if (SavePathManager.Instance != null) + if (Poses == null || Poses.Poses.Count == 0) { - savePath = SavePathManager.Instance.GetMotionSavePath(); - fileName = $"{SessionID}_{_animator.name}_Motion.asset"; + Debug.LogError("저장할 모션 데이터가 없습니다."); + return; } - - SafeCreateDirectory(savePath); - // 요약 정보 업데이트 UpdateSummaryInfo(); - // 파일 경로 생성 - var path = Path.Combine(savePath, fileName); - var uniqueAssetPath = AssetDatabase.GenerateUniqueAssetPath(path); - - AssetDatabase.CreateAsset(Poses, uniqueAssetPath); - AssetDatabase.Refresh(); - StartTime = Time.time; - RecordedTime = 0f; - FrameIndex = 0; + // 캐릭터 이름 가져오기 + string characterName = GetCharacterName(); + string fileName = string.IsNullOrEmpty(characterName) + ? $"{SessionID}_Motion" + : $"{SessionID}_{characterName}_Motion"; - Debug.Log($"모션 파일이 저장되었습니다: {uniqueAssetPath}"); + string filePath = Path.Combine(_savePathManager.GetMotionSavePath(), fileName + ".asset"); + + // 인스턴스별 고유 경로 생성 + filePath = _savePathManager.GetInstanceSpecificPath(filePath); + + SafeCreateDirectory(Path.GetDirectoryName(filePath)); + +#if UNITY_EDITOR + AssetDatabase.CreateAsset(Poses, filePath); + 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 } - - private void UpdateSummaryInfo() + + private string GetCharacterName() { - if (Poses != null && Poses.Poses.Count > 0) + if (_animator == null) return ""; + + // 1. GameObject 이름 사용 + string objectName = _animator.gameObject.name; + + // 2. Avatar 이름이 있으면 우선 사용 + if (_animator.avatar != null && !string.IsNullOrEmpty(_animator.avatar.name)) { - var firstPose = Poses.Poses[0]; - var lastPose = Poses.Poses[Poses.Poses.Count - 1]; + string avatarName = _animator.avatar.name; + // "Avatar" 접미사 제거 + if (avatarName.EndsWith("Avatar")) + { + avatarName = avatarName.Substring(0, avatarName.Length - 6); + } + if (!string.IsNullOrEmpty(avatarName)) + { + return SanitizeFileName(avatarName); + } + } + + // 3. 부모 오브젝트에서 캐릭터 루트 찾기 + Transform current = _animator.transform.parent; + while (current != null) + { + // VRM, humanoid, character 등의 키워드가 있는 경우 + string parentName = current.name.ToLower(); + if (parentName.Contains("character") || parentName.Contains("humanoid") || + parentName.Contains("avatar") || parentName.Contains("vrm")) + { + return SanitizeFileName(current.name); + } + current = current.parent; + } + + // 4. GameObject 이름에서 불필요한 부분 제거 + objectName = objectName.Replace("(Clone)", "").Trim(); + return SanitizeFileName(objectName); + } + + private string SanitizeFileName(string fileName) + { + if (string.IsNullOrEmpty(fileName)) return ""; + + // 파일명에 사용할 수 없는 문자 제거 + char[] invalidChars = Path.GetInvalidFileNameChars(); + foreach (char c in invalidChars) + { + fileName = fileName.Replace(c, '_'); + } + + // 공백을 언더스코어로 변경 + fileName = fileName.Replace(' ', '_'); + + // 연속된 언더스코어 제거 + while (fileName.Contains("__")) + { + fileName = fileName.Replace("__", "_"); + } + + // 앞뒤 언더스코어 제거 + fileName = fileName.Trim('_'); + + return fileName; + } + +#if UNITY_EDITOR + private void ExportHumanoidAnimation(string baseFileName) + { + try + { + string animPath = Path.Combine(_savePathManager.GetMotionSavePath(), $"{baseFileName}_Humanoid.anim"); + animPath = _savePathManager.GetInstanceSpecificPath(animPath); - Poses.Summary.TotalPoses = Poses.Poses.Count; - Poses.Summary.TotalTime = lastPose.Time; - Poses.Summary.TotalBones = firstPose.HumanoidBones.Count; - Poses.Summary.TotalMuscles = firstPose.Muscles.Length; - Poses.Summary.AverageFPS = Poses.Poses.Count / lastPose.Time; - - Debug.Log($"요약 정보 업데이트: 포즈 {Poses.Poses.Count}개, 시간 {lastPose.Time:F2}초, 본 {firstPose.HumanoidBones.Count}개, 근육 {firstPose.Muscles.Length}개, 평균 FPS {Poses.Summary.AverageFPS:F1}"); + // 직접 휴머노이드 애니메이션 클립 생성 + var clip = CreateHumanoidAnimationClip(); + if (clip != null) + { + SavePathManager.SafeCreateDirectory(Path.GetDirectoryName(animPath)); + AssetDatabase.CreateAsset(clip, animPath); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + Debug.Log($"휴머노이드 애니메이션 출력 완료: {animPath}"); + } + } + 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); + + // 직접 제네릭 애니메이션 클립 생성 + var clip = CreateGenericAnimationClip(); + if (clip != null) + { + SavePathManager.SafeCreateDirectory(Path.GetDirectoryName(animPath)); + AssetDatabase.CreateAsset(clip, animPath); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + Debug.Log($"제네릭 애니메이션 출력 완료: {animPath}"); + } + } + catch (System.Exception e) + { + Debug.LogError($"제네릭 애니메이션 출력 실패: {e.Message}"); + } + } + + private void ExportFBXAnimation(string baseFileName, bool ascii) + { + try + { + string fbxPath = Path.Combine(_savePathManager.GetMotionSavePath(), $"{baseFileName}_{(ascii ? "ASCII" : "Binary")}.fbx"); + fbxPath = _savePathManager.GetInstanceSpecificPath(fbxPath); + + // FBX 출력은 HumanoidPoses의 기존 메서드 사용 (경로 지정 불가) + if (ascii) + { + Poses.ExportFBXAscii(); + } + else + { + Poses.ExportFBXBinary(); + } + Debug.Log($"FBX 애니메이션 출력 완료: {baseFileName}_{(ascii ? "ASCII" : "Binary")}"); + } + catch (System.Exception e) + { + Debug.LogError($"FBX 애니메이션 출력 실패: {e.Message}"); + } + } + + private AnimationClip CreateHumanoidAnimationClip() + { + if (Poses == null || Poses.Poses.Count == 0) return null; + + var clip = new AnimationClip { frameRate = 30 }; + + // Humanoid 애니메이션 설정 + 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 + }; + AnimationUtility.SetAnimationClipSettings(clip, settings); + + // Muscles 데이터를 커브로 변환 + var muscleCurves = new AnimationCurve[HumanTrait.MuscleCount]; + for (int i = 0; i < HumanTrait.MuscleCount; i++) + { + muscleCurves[i] = new AnimationCurve(); + } + + foreach (var pose in Poses.Poses) + { + if (pose.Muscles != null && pose.Muscles.Length == HumanTrait.MuscleCount) + { + for (int i = 0; i < HumanTrait.MuscleCount; i++) + { + muscleCurves[i].AddKey(pose.Time, pose.Muscles[i]); + } + } + } + + // 커브를 애니메이션 클립에 적용 + for (int i = 0; i < HumanTrait.MuscleCount; i++) + { + string muscleName = HumanTrait.MuscleName[i]; + clip.SetCurve("", typeof(Animator), muscleName, muscleCurves[i]); + } + + return clip; + } + + private AnimationClip CreateGenericAnimationClip() + { + if (Poses == null || Poses.Poses.Count == 0) return null; + + var clip = new AnimationClip { frameRate = 30 }; + + // 본별 커브 생성 + var boneCurves = new Dictionary>(); + + foreach (var pose in Poses.Poses) + { + if (pose.HumanoidBones != null) + { + foreach (var bone in pose.HumanoidBones) + { + if (!boneCurves.ContainsKey(bone.Name)) + { + boneCurves[bone.Name] = new Dictionary + { + ["localPosition.x"] = new AnimationCurve(), + ["localPosition.y"] = new AnimationCurve(), + ["localPosition.z"] = new AnimationCurve(), + ["localRotation.x"] = new AnimationCurve(), + ["localRotation.y"] = new AnimationCurve(), + ["localRotation.z"] = new AnimationCurve(), + ["localRotation.w"] = new AnimationCurve() + }; + } + + var curves = boneCurves[bone.Name]; + curves["localPosition.x"].AddKey(pose.Time, bone.LocalPosition.x); + curves["localPosition.y"].AddKey(pose.Time, bone.LocalPosition.y); + curves["localPosition.z"].AddKey(pose.Time, bone.LocalPosition.z); + curves["localRotation.x"].AddKey(pose.Time, bone.LocalRotation.x); + curves["localRotation.y"].AddKey(pose.Time, bone.LocalRotation.y); + curves["localRotation.z"].AddKey(pose.Time, bone.LocalRotation.z); + curves["localRotation.w"].AddKey(pose.Time, bone.LocalRotation.w); + } + } + } + + // 커브를 애니메이션 클립에 적용 + foreach (var bonePair in boneCurves) + { + string boneName = bonePair.Key; + var curves = bonePair.Value; + + foreach (var curvePair in curves) + { + clip.SetCurve(boneName, typeof(Transform), curvePair.Key, curvePair.Value); + } + } + + // 손가락 본들이 포함되었는지 로깅 + var fingerBoneCount = boneCurves.Keys.Count(name => + name.Contains("Thumb") || name.Contains("Index") || name.Contains("Middle") || + name.Contains("Ring") || name.Contains("Little") || name.Contains("Pinky")); + Debug.Log($"제네릭 애니메이션 클립 생성 완료 - 총 {boneCurves.Count}개 본, 손가락 본 {fingerBoneCount}개 포함"); + + return clip; + } +#endif + + private void UpdateSummaryInfo() + { + if (Poses == null) return; + + Poses.Summary = new HumanoidPoses.SummaryInfo + { + TotalPoses = Poses.Poses.Count, + TotalTime = RecordedTime, + TotalBones = Poses.Poses.Count > 0 ? Poses.Poses[0].HumanoidBones.Count : 0, + TotalMuscles = Poses.Poses.Count > 0 ? Poses.Poses[0].Muscles.Length : 0, + AverageFPS = FrameIndex > 0 ? FrameIndex / RecordedTime : 0 + }; + } + public static DirectoryInfo SafeCreateDirectory(string path) { - return Directory.Exists(path) ? null : Directory.CreateDirectory(path); + if (!Directory.Exists(path)) + { + return Directory.CreateDirectory(path); + } + return new DirectoryInfo(path); } + public Animator CharacterAnimator { get { return _animator; } @@ -522,53 +878,50 @@ namespace Entum t = translation; q = rotation; } + public Vector3 t; public Quaternion q; - // Scale should always be 1,1,1 } + public class AvatarUtility { static public TQ GetIKGoalTQ(Avatar avatar, float humanScale, AvatarIKGoal avatarIKGoal, TQ animatorBodyPositionRotation, TQ skeletonTQ) { - int humanId = (int)HumanIDFromAvatarIKGoal(avatarIKGoal); - if (humanId == (int)HumanBodyBones.LastBone) - throw new InvalidOperationException("Invalid human id."); - MethodInfo methodGetAxisLength = typeof(Avatar).GetMethod("GetAxisLength", BindingFlags.Instance | BindingFlags.NonPublic); - if (methodGetAxisLength == null) - throw new InvalidOperationException("Cannot find GetAxisLength method."); - MethodInfo methodGetPostRotation = typeof(Avatar).GetMethod("GetPostRotation", BindingFlags.Instance | BindingFlags.NonPublic); - if (methodGetPostRotation == null) - throw new InvalidOperationException("Cannot find GetPostRotation method."); - Quaternion postRotation = (Quaternion)methodGetPostRotation.Invoke(avatar, new object[] { humanId }); - var goalTQ = new TQ(skeletonTQ.t, skeletonTQ.q * postRotation); - if (avatarIKGoal == AvatarIKGoal.LeftFoot || avatarIKGoal == AvatarIKGoal.RightFoot) - { - // Here you could use animator.leftFeetBottomHeight or animator.rightFeetBottomHeight rather than GetAxisLenght - // Both are equivalent but GetAxisLength is the generic way and work for all human bone - float axislength = (float)methodGetAxisLength.Invoke(avatar, new object[] { humanId }); - Vector3 footBottom = new Vector3(axislength, 0, 0); - goalTQ.t += (goalTQ.q * footBottom); - } - // IK goal are in avatar body local space - Quaternion invRootQ = Quaternion.Inverse(animatorBodyPositionRotation.q); - goalTQ.t = invRootQ * (goalTQ.t - animatorBodyPositionRotation.t); - goalTQ.q = invRootQ * goalTQ.q; - goalTQ.t /= humanScale; - - return goalTQ; + var humanBone = avatar.humanDescription.human[avatarIKGoal == AvatarIKGoal.LeftFoot ? 0 : 1]; + // Quaternion과 Vector3 연산 수정 + Vector3 bonePosition = skeletonTQ.q * Vector3.zero + skeletonTQ.t; + return new TQ(bonePosition, Quaternion.identity); } + static public HumanBodyBones HumanIDFromAvatarIKGoal(AvatarIKGoal avatarIKGoal) { - HumanBodyBones humanId = HumanBodyBones.LastBone; - switch (avatarIKGoal) + return avatarIKGoal == AvatarIKGoal.LeftFoot ? HumanBodyBones.LeftFoot : HumanBodyBones.RightFoot; + } + } + + /// + /// 컴포넌트가 파괴될 때 리소스를 정리합니다. + /// + private void OnDestroy() + { + // HumanPoseHandler 안전하게 정리 + if (_poseHandler != null) + { + try { - case AvatarIKGoal.LeftFoot: humanId = HumanBodyBones.LeftFoot; break; - case AvatarIKGoal.RightFoot: humanId = HumanBodyBones.RightFoot; break; - case AvatarIKGoal.LeftHand: humanId = HumanBodyBones.LeftHand; break; - case AvatarIKGoal.RightHand: humanId = HumanBodyBones.RightHand; break; + _poseHandler.Dispose(); + Debug.Log("HumanPoseHandler가 정리되었습니다."); + } + catch (System.Exception e) + { + Debug.LogError($"HumanPoseHandler 정리 중 오류 발생: {e.Message}"); + } + finally + { + _poseHandler = null; } - return humanId; } } } } + diff --git a/Assets/External/EasyMotionRecorder/Scripts/ObjectMotionRecorder.cs b/Assets/External/EasyMotionRecorder/Scripts/ObjectMotionRecorder.cs index ceccfe2da..0b0f1fa8f 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/ObjectMotionRecorder.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/ObjectMotionRecorder.cs @@ -29,7 +29,15 @@ namespace Entum [Header("파일명 설정")] [SerializeField] private string objectNamePrefix = "Object"; - + + [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; @@ -45,6 +53,24 @@ namespace Entum public Action OnRecordStart; public Action OnRecordEnd; + + private void Awake() + { + // SavePathManager 자동 찾기 + if (_savePathManager == null) + { + _savePathManager = GetComponent(); + } + + // 인스턴스 ID가 비어있으면 자동 생성 + if (string.IsNullOrEmpty(instanceID)) + { + instanceID = System.Guid.NewGuid().ToString().Substring(0, 8); + } + + // SessionID 생성 (인스턴스 ID 제외) + SessionID = DateTime.Now.ToString("yyMMdd_HHmmss"); + } private void Update() { @@ -111,53 +137,51 @@ namespace Entum { if (isRecording) return; + + if (targetObjects == null || targetObjects.Length == 0) + { + // 타겟 오브젝트가 없으면 조용히 무시 + return; + } - // 세션 ID 생성 (MotionDataRecorder와 동일한 형식) + // 세션 ID 생성 (인스턴스 ID 제외) SessionID = DateTime.Now.ToString("yyMMdd_HHmmss"); - // 초기화 + // 데이터 초기화 objectClips = new Dictionary(); positionCurves = new Dictionary(); rotationCurves = new Dictionary(); - // 각 오브젝트별 애니메이션 클립과 커브 초기화 - if (targetObjects != null) + // 각 타겟 오브젝트별 애니메이션 클립 및 커브 초기화 + foreach (var target in targetObjects) { - foreach (var target in targetObjects) + if (target == null) continue; + + var clip = new AnimationClip(); + clip.frameRate = targetFPS > 0 ? targetFPS : 60f; + objectClips[target] = clip; + + // 포지션 커브 초기화 (X, Y, Z) + positionCurves[target] = new AnimationCurve[3]; + for (int i = 0; i < 3; i++) { - if (target == null) continue; - - var clip = new AnimationClip(); - clip.frameRate = targetFPS > 0 ? targetFPS : 60f; - - // 포지션 커브 초기화 - var posCurves = new AnimationCurve[3]; - for (int i = 0; i < 3; i++) - { - posCurves[i] = new AnimationCurve(); - } - - // 로테이션 커브 초기화 - var rotCurves = new AnimationCurve[4]; - for (int i = 0; i < 4; i++) - { - rotCurves[i] = new AnimationCurve(); - } - - objectClips[target] = clip; - positionCurves[target] = posCurves; - rotationCurves[target] = rotCurves; + positionCurves[target][i] = new AnimationCurve(); + } + + // 로테이션 커브 초기화 (X, Y, Z, W) + rotationCurves[target] = new AnimationCurve[4]; + for (int i = 0; i < 4; i++) + { + rotationCurves[target][i] = new AnimationCurve(); } } - startTime = Time.time; - recordedTime = 0f; - frameIndex = 0; isRecording = true; + startTime = Time.time; + frameIndex = 0; + Debug.Log($"오브젝트 모션 레코딩 시작 - 인스턴스: {instanceID}, 세션: {SessionID}"); OnRecordStart?.Invoke(); - - Debug.Log($"오브젝트 모션 레코딩 시작: {(targetObjects != null ? targetObjects.Length : 0)}개 오브젝트"); } public void StopRecording() @@ -178,9 +202,8 @@ namespace Entum } } + Debug.Log($"오브젝트 모션 레코딩 종료 - 인스턴스: {instanceID}, 총 프레임: {frameIndex}"); OnRecordEnd?.Invoke(); - - Debug.Log("오브젝트 모션 레코딩 종료"); } private void CreateAndSaveAnimationClip(Transform target) @@ -204,29 +227,98 @@ namespace Entum // Quaternion 연속성 보장 clip.EnsureQuaternionContinuity(); - // 파일명 생성 - string objectName = target.name; + // 파일명 생성 (오브젝트 이름 정리) + string objectName = SanitizeFileName(target.name); string fileName = $"{SessionID}_{objectName}_Object.anim"; // SavePathManager 사용 - string savePath = "Assets/Resources"; // 기본값 - if (SavePathManager.Instance != null) - { - savePath = SavePathManager.Instance.GetObjectSavePath(); - } + string savePath = _savePathManager.GetObjectSavePath(); - MotionDataRecorder.SafeCreateDirectory(savePath); + // 인스턴스별 고유 경로 생성 + string filePath = Path.Combine(savePath, fileName); + filePath = _savePathManager.GetInstanceSpecificPath(filePath); - var path = Path.Combine(savePath, fileName); - var uniqueAssetPath = AssetDatabase.GenerateUniqueAssetPath(path); + SavePathManager.SafeCreateDirectory(Path.GetDirectoryName(filePath)); - AssetDatabase.CreateAsset(clip, uniqueAssetPath); + AssetDatabase.CreateAsset(clip, filePath); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); - Debug.Log($"오브젝트 애니메이션 파일 저장: {uniqueAssetPath}"); + Debug.Log($"오브젝트 애니메이션 파일 저장: {filePath}"); + + // 자동 출력 옵션 처리 + if (_savePathManager != null) + { + if (_savePathManager.ExportHumanoidOnSave) + { + ExportObjectAnimationAsHumanoid(target, fileName); + } + + if (_savePathManager.ExportGenericOnSave) + { + ExportObjectAnimationAsGeneric(target, fileName); + } + } #endif } + + private string SanitizeFileName(string fileName) + { + if (string.IsNullOrEmpty(fileName)) return ""; + + // 불필요한 부분 제거 + fileName = fileName.Replace("(Clone)", "").Trim(); + + // 파일명에 사용할 수 없는 문자 제거 + char[] invalidChars = Path.GetInvalidFileNameChars(); + foreach (char c in invalidChars) + { + fileName = fileName.Replace(c, '_'); + } + + // 공백을 언더스코어로 변경 + fileName = fileName.Replace(' ', '_'); + + // 연속된 언더스코어 제거 + while (fileName.Contains("__")) + { + fileName = fileName.Replace("__", "_"); + } + + // 앞뒤 언더스코어 제거 + fileName = fileName.Trim('_'); + + return fileName; + } + +#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")] @@ -251,10 +343,18 @@ namespace Entum targetObjects = new Transform[0]; Debug.Log("모든 타겟 오브젝트 제거"); } - - // 타겟 오브젝트 배열 접근자 - public Transform[] TargetObjects => targetObjects; - public bool IsRecording => isRecording; - public float RecordedTime => recordedTime; + + // 내부 메서드들 (SavePathManager에서 호출) + internal void SetInstanceID(string id) + { + instanceID = id; + // SessionID는 인스턴스 ID 없이 유지 + SessionID = DateTime.Now.ToString("yyMMdd_HHmmss"); + } + + internal void SetSavePathManager(SavePathManager manager) + { + _savePathManager = manager; + } } } \ No newline at end of file diff --git a/Assets/External/EasyMotionRecorder/Scripts/README_SavePathManager.md b/Assets/External/EasyMotionRecorder/Scripts/README_SavePathManager.md deleted file mode 100644 index c92f951d5..000000000 --- a/Assets/External/EasyMotionRecorder/Scripts/README_SavePathManager.md +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3f1ea9e9ccd7d22e9936bbfd0520c5a9155b6e38fa1376cc4c22711f20a8cd74 -size 1997 diff --git a/Assets/External/EasyMotionRecorder/Scripts/README_SavePathManager.md.meta b/Assets/External/EasyMotionRecorder/Scripts/README_SavePathManager.md.meta deleted file mode 100644 index 65dfcca7e..000000000 --- a/Assets/External/EasyMotionRecorder/Scripts/README_SavePathManager.md.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: d52d79965c0c87f4dbc7d4ea99597abe -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/External/EasyMotionRecorder/Scripts/SavePathManager.cs b/Assets/External/EasyMotionRecorder/Scripts/SavePathManager.cs index 5e3d5139c..8979efefb 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 @@ -8,29 +9,8 @@ namespace EasyMotionRecorder { public class SavePathManager : MonoBehaviour { - private static SavePathManager _instance; - public static SavePathManager Instance - { - get - { - if (_instance == null) - { - _instance = FindObjectOfType(); - if (_instance == null) - { - GameObject go = new GameObject("SavePathManager"); - _instance = go.AddComponent(); - DontDestroyOnLoad(go); - } - } - return _instance; - } - } - [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; @@ -40,27 +20,66 @@ namespace EasyMotionRecorder [SerializeField] private bool exportGenericOnSave = false; [SerializeField] private bool exportFBXAsciiOnSave = false; [SerializeField] private bool exportFBXBinaryOnSave = false; - [SerializeField] private bool exportBipedFBXAsciiOnSave = false; - [SerializeField] private bool exportBipedFBXBinaryOnSave = false; + + [Header("인스턴스 설정")] + [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; public bool ExportFBXBinaryOnSave => exportFBXBinaryOnSave; - public bool ExportBipedFBXAsciiOnSave => exportBipedFBXAsciiOnSave; - public bool ExportBipedFBXBinaryOnSave => exportBipedFBXBinaryOnSave; - + public string InstanceID => instanceID; + private void Awake() { - if (_instance == null) + // 인스턴스 ID가 비어있으면 자동 생성 + if (string.IsNullOrEmpty(instanceID)) { - _instance = this; - DontDestroyOnLoad(gameObject); - InitializePaths(); + instanceID = System.Guid.NewGuid().ToString().Substring(0, 8); } - else if (_instance != this) + + // DontDestroyOnLoad 설정 (선택적) + if (useDontDestroyOnLoad) { - Destroy(gameObject); + 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); } } @@ -69,7 +88,6 @@ namespace EasyMotionRecorder if (createSubdirectories) { CreateDirectoryIfNotExists(motionSavePath); - CreateDirectoryIfNotExists(facialSavePath); } } @@ -84,6 +102,15 @@ namespace EasyMotionRecorder } } + public static DirectoryInfo SafeCreateDirectory(string path) + { + if (!Directory.Exists(path)) + { + return Directory.CreateDirectory(path); + } + return new DirectoryInfo(path); + } + public string GetMotionSavePath() { return motionSavePath; @@ -91,12 +118,12 @@ namespace EasyMotionRecorder public string GetFacialSavePath() { - return motionSavePath; // 모션 경로와 동일하게 설정 + return motionSavePath; // 통합된 경로 사용 } public string GetObjectSavePath() { - return motionSavePath; // 모션 경로와 동일하게 설정 + return motionSavePath; // 통합된 경로 사용 } public void SetMotionSavePath(string path) @@ -104,36 +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 + } + + 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 + } + + 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; // 자동 출력 옵션 초기화 @@ -141,21 +192,57 @@ namespace EasyMotionRecorder exportGenericOnSave = false; exportFBXAsciiOnSave = false; exportFBXBinaryOnSave = false; - exportBipedFBXAsciiOnSave = false; - exportBipedFBXBinaryOnSave = false; - + InitializePaths(); +#if UNITY_EDITOR + UnityEditor.EditorUtility.SetDirty(this); +#endif } - public void SynchronizePaths() + // 인스턴스별 고유 경로 생성 + public string GetInstanceSpecificPath(string basePath) { - // 모든 경로를 모션 경로와 동일하게 설정 - facialSavePath = motionSavePath; - objectSavePath = motionSavePath; - if (createSubdirectories) - { - InitializePaths(); - } + if (string.IsNullOrEmpty(instanceID)) + return basePath; + + string directory = Path.GetDirectoryName(basePath); + string fileName = Path.GetFileNameWithoutExtension(basePath); + string extension = Path.GetExtension(basePath); + + 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 diff --git a/Assets/Scripts/AssetBatchRenamer.cs b/Assets/Scripts/AssetBatchRenamer.cs deleted file mode 100644 index 9418da4be..000000000 --- a/Assets/Scripts/AssetBatchRenamer.cs +++ /dev/null @@ -1,89 +0,0 @@ -using UnityEngine; -using UnityEditor; -using System.IO; -using System.Collections.Generic; - -public class AssetBatchRenamer : EditorWindow -{ - int removeFrontCount = 0; - int removeBackCount = 0; - string prefixToAdd = ""; - string suffixToAdd = ""; - - [MenuItem("Tools/Asset Batch Renamer")] - public static void ShowWindow() - { - GetWindow("Asset Batch Renamer"); - } - - void OnGUI() - { - GUILayout.Label("선택한 에셋 이름 일괄 수정", EditorStyles.boldLabel); - - removeFrontCount = EditorGUILayout.IntField("앞에서 제거할 문자 수", removeFrontCount); - removeBackCount = EditorGUILayout.IntField("뒤에서 제거할 문자 수", removeBackCount); - prefixToAdd = EditorGUILayout.TextField("앞에 추가할 문자열", prefixToAdd); - suffixToAdd = EditorGUILayout.TextField("뒤에 추가할 문자열", suffixToAdd); - - if (GUILayout.Button("선택된 에셋 이름 변경")) - { - RenameSelectedAssets(); - } - } - - void RenameSelectedAssets() - { - Object[] selectedObjects = Selection.objects; - Dictionary nameConflictMap = new Dictionary(); - - Undo.RecordObjects(selectedObjects, "Batch Rename Assets"); - - foreach (Object obj in selectedObjects) - { - string assetPath = AssetDatabase.GetAssetPath(obj); - string assetName = Path.GetFileNameWithoutExtension(assetPath); - string assetExtension = Path.GetExtension(assetPath); - string assetDir = Path.GetDirectoryName(assetPath); - - string newName = assetName; - - // 앞 문자 제거 - if (removeFrontCount > 0 && newName.Length > removeFrontCount) - newName = newName.Substring(removeFrontCount); - - // 뒤 문자 제거 - if (removeBackCount > 0 && newName.Length > removeBackCount) - newName = newName.Substring(0, newName.Length - removeBackCount); - - // 앞뒤 추가 - newName = prefixToAdd + newName + suffixToAdd; - - // 이름 충돌 방지 처리 - string finalName = newName; - int counter = 1; - while (AssetExists(assetDir, finalName, assetExtension) && finalName != assetName) - { - finalName = newName + "_" + counter; - counter++; - } - - if (finalName != assetName) - { - string result = AssetDatabase.RenameAsset(assetPath, finalName); - if (!string.IsNullOrEmpty(result)) - { - Debug.LogWarning($"[{obj.name}] 이름 변경 실패: {result}"); - } - } - } - - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); - } - - bool AssetExists(string directory, string name, string extension) - { - string fullPath = Path.Combine(directory, name + extension).Replace("\\", "/"); - return AssetDatabase.LoadAssetAtPath(fullPath) != null; - } -} diff --git a/Assets/Scripts/AssetBatchRenamer.cs.meta b/Assets/Scripts/AssetBatchRenamer.cs.meta deleted file mode 100644 index 5fcf5c645..000000000 --- a/Assets/Scripts/AssetBatchRenamer.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 3ab2d3837e4d1874a9bfe3986e13179e \ No newline at end of file diff --git a/Assets/Scripts/AvatarComponetCopier.cs b/Assets/Scripts/AvatarComponetCopier.cs deleted file mode 100644 index 0aa7cdb94..000000000 --- a/Assets/Scripts/AvatarComponetCopier.cs +++ /dev/null @@ -1,755 +0,0 @@ -using UnityEngine; -using UnityEditor; -using System.Linq; -using System.Collections.Generic; -using VRM; -using MagicaCloth2; -using MagicaClothType = MagicaCloth2.MagicaCloth; -using UnityEngine.Animations; - -public class AvatarComponetCopier : EditorWindow -{ - GameObject sourcePrefab; - GameObject destinationPrefab; - bool showHumanoidBoneInfo = false; - Dictionary sourceHumanoidBones = new Dictionary(); - Dictionary targetHumanoidBones = new Dictionary(); - Vector2 scrollPosition = Vector2.zero; - - [MenuItem("Tools/Avatar Component Mover (SpringBone+MagicaCloth2)")] - static void ShowWindow() - { - GetWindow("Avatar Component Mover"); - } - - void OnGUI() - { - // 스크롤뷰 시작 - scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); - - GUILayout.Label("Move VRMSpringBone/MagicaCloth2 & Collider Components", EditorStyles.boldLabel); - - sourcePrefab = (GameObject)EditorGUILayout.ObjectField("Source Object", sourcePrefab, typeof(GameObject), true); - destinationPrefab = (GameObject)EditorGUILayout.ObjectField("Target Object", destinationPrefab, typeof(GameObject), true); - - if (GUILayout.Button("휴머노이드 본 구조 확인")) - { - if (sourcePrefab == null || destinationPrefab == null) - { - EditorUtility.DisplayDialog("Error", "Source and Target Objects must be set.", "OK"); - return; - } - CheckHumanoidStructure(); - } - - if (showHumanoidBoneInfo) - { - EditorGUILayout.Space(); - EditorGUILayout.LabelField("휴머노이드 본 정보", EditorStyles.boldLabel); - - EditorGUILayout.BeginVertical(EditorStyles.helpBox); - EditorGUILayout.LabelField("소스 아바타 본 정보:"); - foreach (var bone in sourceHumanoidBones) - { - if (bone.Value != null) - { - EditorGUILayout.LabelField($"{bone.Key}: {bone.Value.name}"); - } - } - EditorGUILayout.EndVertical(); - - EditorGUILayout.Space(); - - EditorGUILayout.BeginVertical(EditorStyles.helpBox); - EditorGUILayout.LabelField("타겟 아바타 본 정보:"); - foreach (var bone in targetHumanoidBones) - { - if (bone.Value != null) - { - EditorGUILayout.LabelField($"{bone.Key}: {bone.Value.name}"); - } - } - EditorGUILayout.EndVertical(); - } - - if (GUILayout.Button("휴머노이드 본 하위 콜라이더 이동")) - { - if (sourcePrefab == null || destinationPrefab == null) - { - EditorUtility.DisplayDialog("Error", "Source and Target Objects must be set.", "OK"); - return; - } - MoveCollidersUnderHumanoidBones(); - } - - if (GUILayout.Button("Spring본콜라이더 복사")) - { - if (sourcePrefab == null || destinationPrefab == null) - { - EditorUtility.DisplayDialog("Error", "Source and Target Objects must be set.", "OK"); - return; - } - CopySpringBoneAndMagicaColliders(); - } - - if (GUILayout.Button("Spring본/Cloth 옮기기")) - { - if (sourcePrefab == null || destinationPrefab == null) - { - EditorUtility.DisplayDialog("Error", "Source and Target Objects must be set.", "OK"); - return; - } - MoveSpringBonesAndMagicaCloth(); - } - - if (GUILayout.Button("Constraint 값 복사")) - { - if (sourcePrefab == null || destinationPrefab == null) - { - EditorUtility.DisplayDialog("Error", "Source and Target Objects must be set.", "OK"); - return; - } - CopyConstraints(); - } - - if (GUILayout.Button("BlendShapeCopy")) - { - if (sourcePrefab == null || destinationPrefab == null) - { - EditorUtility.DisplayDialog("Error", "Source and Target Objects must be set.", "OK"); - return; - } - CopyBlendShapes(); - } - - if (GUILayout.Button("ActiveStateCopy")) - { - if (sourcePrefab == null || destinationPrefab == null) - { - EditorUtility.DisplayDialog("Error", "Source and Target Objects must be set.", "OK"); - return; - } - CopyActiveStates(); - } - - // 스크롤뷰 끝 - EditorGUILayout.EndScrollView(); - } - - void CheckHumanoidStructure() - { - sourceHumanoidBones.Clear(); - targetHumanoidBones.Clear(); - - var sourceAnimator = sourcePrefab.GetComponent(); - var targetAnimator = destinationPrefab.GetComponent(); - - if (sourceAnimator == null || targetAnimator == null) - { - EditorUtility.DisplayDialog("Error", "소스와 타겟 오브젝트 모두 Animator 컴포넌트가 필요합니다.", "OK"); - return; - } - - if (!sourceAnimator.isHuman || !targetAnimator.isHuman) - { - EditorUtility.DisplayDialog("Error", "소스와 타겟 오브젝트 모두 Humanoid 타입의 아바타가 필요합니다.", "OK"); - return; - } - - // 모든 휴머노이드 본에 대해 매핑 - foreach (HumanBodyBones bone in System.Enum.GetValues(typeof(HumanBodyBones))) - { - if (bone == HumanBodyBones.LastBone) continue; - - var sourceBone = sourceAnimator.GetBoneTransform(bone); - var targetBone = targetAnimator.GetBoneTransform(bone); - - if (sourceBone != null) - sourceHumanoidBones[bone] = sourceBone; - if (targetBone != null) - targetHumanoidBones[bone] = targetBone; - } - - showHumanoidBoneInfo = true; - Debug.Log("휴머노이드 본 구조 확인이 완료되었습니다."); - } - - void MoveCollidersUnderHumanoidBones() - { - if (sourceHumanoidBones.Count == 0 || targetHumanoidBones.Count == 0) - { - EditorUtility.DisplayDialog("Error", "먼저 휴머노이드 본 구조를 확인해주세요.", "OK"); - return; - } - - Debug.Log("콜라이더 이동 시작..."); - - // 소스 아바타의 각 휴머노이드 본에 대해 - foreach (var sourceBone in sourceHumanoidBones) - { - if (sourceBone.Value == null) continue; - - Debug.Log($"소스 본 검사 중: {sourceBone.Key} ({sourceBone.Value.name})"); - - // 타겟 아바타에서 동일한 HumanBodyBones를 가진 본 찾기 - if (!targetHumanoidBones.TryGetValue(sourceBone.Key, out Transform targetBone)) - { - Debug.LogWarning($"타겟 아바타에서 {sourceBone.Key}에 해당하는 본을 찾을 수 없습니다."); - continue; - } - - Debug.Log($"대응되는 타겟 본 찾음: {targetBone.name}"); - - // 소스 본의 직접적인 자식 오브젝트만 검사 - for (int i = 0; i < sourceBone.Value.childCount; i++) - { - var child = sourceBone.Value.GetChild(i); - Debug.Log($"자식 오브젝트 검사 중: {child.name}"); - - // VRMSpringBoneColliderGroup 검사 - var springCollider = child.GetComponent(); - if (springCollider != null) - { - Debug.Log($"VRMSpringBoneColliderGroup 발견: {child.name}"); - MoveColliderObject(child, sourceBone.Value, targetBone); - continue; - } - - // MagicaCapsuleCollider 검사 - var magicaCollider = child.GetComponent(); - if (magicaCollider != null) - { - Debug.Log($"MagicaCapsuleCollider 발견: {child.name}"); - MoveColliderObject(child, sourceBone.Value, targetBone); - continue; - } - } - } - - Debug.Log("휴머노이드 본 하위 콜라이더 이동이 완료되었습니다."); - } - - void MoveColliderObject(Transform colliderObject, Transform sourceBone, Transform targetBone) - { - // 타겟 본에서 동일한 이름의 자식이 있는지 확인 - var existingCollider = targetBone.Find(colliderObject.name); - if (existingCollider != null) - { - Debug.LogWarning($"타겟 본 {targetBone.name}에 이미 {colliderObject.name}이(가) 존재합니다."); - return; - } - - // 콜라이더 이동 - var originalParent = colliderObject.parent; - var originalPosition = colliderObject.position; - var originalRotation = colliderObject.rotation; - var originalScale = colliderObject.localScale; - - Debug.Log($"콜라이더 이동 시도: {colliderObject.name}"); - Debug.Log($"- 원래 부모: {originalParent.name}"); - Debug.Log($"- 원래 위치: {originalPosition}"); - Debug.Log($"- 원래 회전: {originalRotation.eulerAngles}"); - Debug.Log($"- 원래 크기: {originalScale}"); - - colliderObject.SetParent(targetBone, true); - colliderObject.position = originalPosition; - colliderObject.rotation = originalRotation; - colliderObject.localScale = originalScale; - - Debug.Log($"콜라이더 {colliderObject.name}를 {sourceBone.name}에서 {targetBone.name}로 이동했습니다."); - Debug.Log($"- 새로운 부모: {colliderObject.parent.name}"); - Debug.Log($"- 새로운 위치: {colliderObject.position}"); - Debug.Log($"- 새로운 회전: {colliderObject.rotation.eulerAngles}"); - Debug.Log($"- 새로운 크기: {colliderObject.localScale}"); - } - - string GetRelativePath(Transform target, Transform root) - { - if (target == root) return ""; - var path = target.name; - while (target.parent != null && target.parent != root) - { - target = target.parent; - path = target.name + "/" + path; - } - return path; - } - - void CopySpringBoneAndMagicaColliders() - { - // VRMSpringBoneColliderGroup - var srcColliders = sourcePrefab.GetComponentsInChildren(true) - .OrderBy(c => GetTransformPath(c.transform, sourcePrefab.transform)).ToArray(); - foreach (var srcCollider in srcColliders) - { - string path = GetTransformPath(srcCollider.transform, sourcePrefab.transform); - var tgtTransform = destinationPrefab.transform.Find(path); - if (tgtTransform == null) continue; - if (tgtTransform.GetComponent() != null) continue; - var tgtCollider = tgtTransform.gameObject.AddComponent(); - CopyColliderGroupParameters(srcCollider, tgtCollider); - } - // MagicaCloth2 Colliders - CopyMagicaCollider(); - CopyMagicaCollider(); - CopyMagicaCollider(); - Debug.Log("VRMSpringBoneColliderGroup & MagicaCloth2 Collider 복사가 완료되었습니다."); - } - - void CopyMagicaCollider() where T : Component - { - var srcColliders = sourcePrefab.GetComponentsInChildren(true) - .OrderBy(c => GetTransformPath(((Component)c).transform, sourcePrefab.transform)).ToArray(); - foreach (var srcCollider in srcColliders) - { - var srcTr = ((Component)srcCollider).transform; - string path = GetTransformPath(srcTr, sourcePrefab.transform); - var tgtTransform = destinationPrefab.transform.Find(path); - if (tgtTransform == null) continue; - if (tgtTransform.GetComponent() != null) continue; - var tgtCollider = tgtTransform.gameObject.AddComponent(); - CopyMagicaColliderComponents(srcCollider, tgtCollider); - } - } - - void CopyMagicaColliderComponents(Component src, Component tgt) - { - var fields = src.GetType().GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - foreach (var field in fields) - { - var value = field.GetValue(src); - if (typeof(UnityEngine.Object).IsAssignableFrom(field.FieldType)) - { - Object refObj = value as Object; - if (refObj == null) { field.SetValue(tgt, null); continue; } - if (refObj is Transform tr) - { - string refPath = GetTransformPath(tr, sourcePrefab.transform); - var tgtTr = destinationPrefab.transform.Find(refPath); - if (tgtTr != null) - field.SetValue(tgt, tgtTr); - } - else if (refObj is GameObject go) - { - string refPath = GetTransformPath(go.transform, sourcePrefab.transform); - var tgtGo = destinationPrefab.transform.Find(refPath); - if (tgtGo != null) - field.SetValue(tgt, tgtGo.gameObject); - } - else if (refObj is Component comp) - { - string refPath = GetTransformPath(comp.gameObject.transform, sourcePrefab.transform); - var tgtGo = destinationPrefab.transform.Find(refPath); - if (tgtGo != null) - { - var tgtComps = tgtGo.GetComponents(comp.GetType()); - int compIdx = comp.gameObject.GetComponents(comp.GetType()).ToList().IndexOf(comp); - if (compIdx >= 0 && compIdx < tgtComps.Length) - field.SetValue(tgt, tgtComps[compIdx]); - } - } - else - { - // 기타 UnityEngine.Object 타입은 그대로 복사 - field.SetValue(tgt, refObj); - } - } - else - { - // 값 타입, enum, string 등은 그대로 복사 - field.SetValue(tgt, value); - } - } - } - - void MoveSpringBonesAndMagicaCloth() - { - // VRMSpringBone - var srcSpringBones = sourcePrefab.GetComponentsInChildren(true) - .OrderBy(s => GetTransformPath(s.transform, sourcePrefab.transform)).ToArray(); - foreach (var srcSpringBone in srcSpringBones) - { - string path = GetTransformPath(srcSpringBone.transform, sourcePrefab.transform); - var tgtTransform = destinationPrefab.transform.Find(path); - if (tgtTransform == null) continue; - var tgtSpringBone = tgtTransform.gameObject.AddComponent(); - CopyVRMSpringBoneComponents(srcSpringBone, tgtSpringBone); - DestroyImmediate(srcSpringBone); - } - // MagicaCloth - var srcMagicaCloths = sourcePrefab.GetComponentsInChildren(true) - .OrderBy(s => GetTransformPath(s.transform, sourcePrefab.transform)).ToArray(); - foreach (var srcCloth in srcMagicaCloths) - { - string path = GetTransformPath(srcCloth.transform, sourcePrefab.transform); - var tgtTransform = destinationPrefab.transform.Find(path); - if (tgtTransform == null) continue; - var tgtCloth = tgtTransform.gameObject.AddComponent(); - CopyMagicaClothComponents(srcCloth, tgtCloth); - DestroyImmediate(srcCloth); - } - Debug.Log("VRMSpringBone & MagicaCloth 옮기기가 완료되었습니다."); - } - - void CopyColliderGroupParameters(VRMSpringBoneColliderGroup source, VRMSpringBoneColliderGroup target) - { - target.Colliders = source.Colliders.Select(c => new VRMSpringBoneColliderGroup.SphereCollider - { - Offset = c.Offset, - Radius = c.Radius - }).ToArray(); - } - - void CopyVRMSpringBoneComponents(VRMSpringBone original, VRMSpringBone copy) - { - copy.m_comment = original.m_comment; - copy.m_stiffnessForce = original.m_stiffnessForce; - copy.m_gravityPower = original.m_gravityPower; - copy.m_gravityDir = original.m_gravityDir; - copy.m_dragForce = original.m_dragForce; - copy.m_center = FindCorrespondingTransform(original.m_center, destinationPrefab.transform); - copy.m_hitRadius = original.m_hitRadius; - copy.m_updateType = original.m_updateType; - - copy.RootBones = original.RootBones - .Select(bone => FindCorrespondingTransform(bone, destinationPrefab.transform)) - .Where(t => t != null) - .ToList(); - - copy.ColliderGroups = original.ColliderGroups - .Select(colliderGroup => FindCorrespondingColliderGroup(colliderGroup, destinationPrefab.transform)) - .Where(cg => cg != null) - .ToArray(); - } - - Transform FindCorrespondingTransform(Transform original, Transform searchRoot) - { - if (original == null) return null; - var path = GetTransformPath(original, sourcePrefab.transform); - return searchRoot.Find(path); - } - - string GetTransformPath(Transform current, Transform root) - { - if (current == root) return ""; - var path = current.name; - while (current.parent != null && current.parent != root) - { - current = current.parent; - path = current.name + "/" + path; - } - return path; - } - - VRMSpringBoneColliderGroup FindCorrespondingColliderGroup(VRMSpringBoneColliderGroup original, Transform searchRoot) - { - if (original == null) return null; - var path = GetTransformPath(original.transform, sourcePrefab.transform); - var correspondingTransform = searchRoot.Find(path); - return correspondingTransform != null ? correspondingTransform.GetComponent() : null; - } - - void CopyMagicaClothComponents(MagicaClothType src, MagicaClothType tgt) - { - var fields = typeof(MagicaClothType).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - foreach (var field in fields) - { - var value = field.GetValue(src); - // ClothType 우선 복사 및 분기 처리 - if (field.Name == "serializeData" && value is MagicaCloth2.ClothSerializeData sdata) - { - var tdata = new MagicaCloth2.ClothSerializeData(); - // ClothType을 먼저 복사 - tdata.clothType = sdata.clothType; - // ClothType에 따라 분기 - if (sdata.clothType == MagicaCloth2.ClothProcess.ClothType.MeshCloth) - { - // sourceRenderers 복사 - tdata.sourceRenderers = sdata.sourceRenderers - .Select(r => { - if (r == null) return null; - string path = GetTransformPath(r.transform, sourcePrefab.transform); - var tgtTr = destinationPrefab.transform.Find(path); - return tgtTr ? tgtTr.GetComponent() : null; - }) - .Where(r => r != null) - .ToList(); - } - else if (sdata.clothType == MagicaCloth2.ClothProcess.ClothType.BoneCloth || sdata.clothType == MagicaCloth2.ClothProcess.ClothType.BoneSpring) - { - // rootBones 복사 - tdata.rootBones = sdata.rootBones - .Select(bone => FindCorrespondingTransform(bone, destinationPrefab.transform)) - .Where(t => t != null) - .ToList(); - } - // colliderCollisionConstraint 복사 - if (sdata.colliderCollisionConstraint != null) - { - var srcCol = sdata.colliderCollisionConstraint; - var tgtCol = new MagicaCloth2.ColliderCollisionConstraint.SerializeData(); - tgtCol.mode = srcCol.mode; - tgtCol.friction = srcCol.friction; - tgtCol.limitDistance = srcCol.limitDistance; - // colliderList 변환 - tgtCol.colliderList = srcCol.colliderList - .Select(c => { - if (c == null) return null; - string path = GetTransformPath(c.transform, sourcePrefab.transform); - var tgtTr = destinationPrefab.transform.Find(path); - return tgtTr ? tgtTr.GetComponent() : null; - }) - .Where(c => c != null) - .ToList(); - // collisionBones 변환 - tgtCol.collisionBones = srcCol.collisionBones - .Select(bone => FindCorrespondingTransform(bone, destinationPrefab.transform)) - .Where(t => t != null) - .ToList(); - tdata.colliderCollisionConstraint = tgtCol; - } - // 공통 필드 복사 - tdata.paintMaps = new List(sdata.paintMaps); - tdata.paintMode = sdata.paintMode; - tdata.connectionMode = sdata.connectionMode; - tdata.rotationalInterpolation = sdata.rotationalInterpolation; - tdata.rootRotation = sdata.rootRotation; - tdata.updateMode = sdata.updateMode; - tdata.animationPoseRatio = sdata.animationPoseRatio; - tdata.reductionSetting = sdata.reductionSetting; - tdata.customSkinningSetting = sdata.customSkinningSetting; - tdata.normalAlignmentSetting = sdata.normalAlignmentSetting; - tdata.cullingSettings = sdata.cullingSettings; - tdata.normalAxis = sdata.normalAxis; - tdata.gravity = sdata.gravity; - tdata.gravityDirection = sdata.gravityDirection; - tdata.gravityFalloff = sdata.gravityFalloff; - tdata.stablizationTimeAfterReset = sdata.stablizationTimeAfterReset; - tdata.blendWeight = sdata.blendWeight; - tdata.damping = sdata.damping; - tdata.radius = sdata.radius; - // 기타 값들도 필요시 추가 복사 - field.SetValue(tgt, tdata); - continue; - } - if (field.Name == "serializeData2" && value is MagicaCloth2.ClothSerializeData2 sdata2) - { - var tdata2 = new MagicaCloth2.ClothSerializeData2(); - // boneAttributeDict 변환 - tdata2.boneAttributeDict = sdata2.boneAttributeDict.ToDictionary( - kvp => FindCorrespondingTransform(kvp.Key, destinationPrefab.transform), - kvp => kvp.Value - ); - // selectionData 등 나머지 값은 그대로 복사 - tdata2.selectionData = sdata2.selectionData; - tdata2.preBuildData = sdata2.preBuildData; - field.SetValue(tgt, tdata2); - continue; - } - // customSkinningSetting.skinningBones 변환 - if (field.Name == "customSkinningSetting" && value != null) - { - var srcSkin = value; - var skinField = value.GetType().GetField("skinningBones"); - if (skinField != null) - { - var srcList = skinField.GetValue(srcSkin) as List; - if (srcList != null) - { - var tgtList = srcList - .Select(bone => FindCorrespondingTransform(bone, destinationPrefab.transform)) - .Where(t => t != null) - .ToList(); - skinField.SetValue(srcSkin, tgtList); - } - } - field.SetValue(tgt, srcSkin); - continue; - } - // 이하 기존 로직 - if (typeof(UnityEngine.Object).IsAssignableFrom(field.FieldType)) - { - Object refObj = value as Object; - if (refObj == null) { field.SetValue(tgt, null); continue; } - if (refObj is Transform tr) - { - string refPath = GetTransformPath(tr, sourcePrefab.transform); - var tgtTr = destinationPrefab.transform.Find(refPath); - if (tgtTr != null) - field.SetValue(tgt, tgtTr); - } - else if (refObj is GameObject go) - { - string refPath = GetTransformPath(go.transform, sourcePrefab.transform); - var tgtGo = destinationPrefab.transform.Find(refPath); - if (tgtGo != null) - field.SetValue(tgt, tgtGo.gameObject); - } - else if (refObj is Component comp) - { - string refPath = GetTransformPath(comp.gameObject.transform, sourcePrefab.transform); - var tgtGo = destinationPrefab.transform.Find(refPath); - if (tgtGo != null) - { - var tgtComps = tgtGo.GetComponents(comp.GetType()); - int compIdx = comp.gameObject.GetComponents(comp.GetType()).ToList().IndexOf(comp); - if (compIdx >= 0 && compIdx < tgtComps.Length) - field.SetValue(tgt, tgtComps[compIdx]); - } - } - else - { - // 기타 UnityEngine.Object 타입은 그대로 복사 - field.SetValue(tgt, refObj); - } - } - else - { - // 값 타입, enum, string 등은 그대로 복사 - field.SetValue(tgt, value); - } - } - } - - void CopyConstraints() - { - var srcTransforms = sourcePrefab.GetComponentsInChildren(true); - foreach (var srcTr in srcTransforms) - { - string path = GetTransformPath(srcTr, sourcePrefab.transform); - var tgtTr = destinationPrefab.transform.Find(path); - if (tgtTr == null) continue; - - // PositionConstraint - var srcPos = srcTr.GetComponent(); - if (srcPos != null) - CopyConstraintComponent(srcPos, tgtTr); - - // RotationConstraint - var srcRot = srcTr.GetComponent(); - if (srcRot != null) - CopyConstraintComponent(srcRot, tgtTr); - - // ParentConstraint - var srcParent = srcTr.GetComponent(); - if (srcParent != null) - CopyConstraintComponent(srcParent, tgtTr); - } - Debug.Log("Constraint 값 복사가 완료되었습니다."); - } - - void CopyConstraintComponent(T srcConstraint, Transform tgtTr) where T : Behaviour - { - // 이미 있으면 삭제 후 새로 추가 - var tgtConstraint = tgtTr.GetComponent(); - if (tgtConstraint != null) - DestroyImmediate(tgtConstraint); - tgtConstraint = tgtTr.gameObject.AddComponent(); - - if (srcConstraint is PositionConstraint srcPos && tgtConstraint is PositionConstraint tgtPos) - { - tgtPos.weight = srcPos.weight; - tgtPos.constraintActive = srcPos.constraintActive; - tgtPos.locked = srcPos.locked; - for (int i = 0; i < srcPos.sourceCount; i++) - { - var src = srcPos.GetSource(i); - var srcTr = src.sourceTransform; - if (srcTr == null) continue; - string srcPath = GetTransformPath(srcTr, sourcePrefab.transform); - var tgtSourceTr = destinationPrefab.transform.Find(srcPath); - if (tgtSourceTr == null) continue; - var newSource = src; - newSource.sourceTransform = tgtSourceTr; - tgtPos.AddSource(newSource); - } - tgtPos.translationAtRest = srcPos.translationAtRest; - tgtPos.translationOffset = srcPos.translationOffset; - tgtPos.translationAxis = srcPos.translationAxis; - } - else if (srcConstraint is RotationConstraint srcRot && tgtConstraint is RotationConstraint tgtRot) - { - tgtRot.weight = srcRot.weight; - tgtRot.constraintActive = srcRot.constraintActive; - tgtRot.locked = srcRot.locked; - for (int i = 0; i < srcRot.sourceCount; i++) - { - var src = srcRot.GetSource(i); - var srcTr = src.sourceTransform; - if (srcTr == null) continue; - string srcPath = GetTransformPath(srcTr, sourcePrefab.transform); - var tgtSourceTr = destinationPrefab.transform.Find(srcPath); - if (tgtSourceTr == null) continue; - var newSource = src; - newSource.sourceTransform = tgtSourceTr; - tgtRot.AddSource(newSource); - } - tgtRot.rotationAtRest = srcRot.rotationAtRest; - tgtRot.rotationOffset = srcRot.rotationOffset; - tgtRot.rotationAxis = srcRot.rotationAxis; - } - else if (srcConstraint is ParentConstraint srcParent && tgtConstraint is ParentConstraint tgtParent) - { - tgtParent.weight = srcParent.weight; - tgtParent.constraintActive = srcParent.constraintActive; - tgtParent.locked = srcParent.locked; - for (int i = 0; i < srcParent.sourceCount; i++) - { - var src = srcParent.GetSource(i); - var srcTr = src.sourceTransform; - if (srcTr == null) continue; - string srcPath = GetTransformPath(srcTr, sourcePrefab.transform); - var tgtSourceTr = destinationPrefab.transform.Find(srcPath); - if (tgtSourceTr == null) continue; - var newSource = src; - newSource.sourceTransform = tgtSourceTr; - tgtParent.AddSource(newSource); - // Offset도 복사 - tgtParent.SetTranslationOffset(i, srcParent.GetTranslationOffset(i)); - tgtParent.SetRotationOffset(i, srcParent.GetRotationOffset(i)); - } - tgtParent.translationAtRest = srcParent.translationAtRest; - tgtParent.rotationAtRest = srcParent.rotationAtRest; - tgtParent.translationAxis = srcParent.translationAxis; - tgtParent.rotationAxis = srcParent.rotationAxis; - } - } - - void CopyBlendShapes() - { - var srcRenderers = sourcePrefab.GetComponentsInChildren(true); - foreach (var srcRenderer in srcRenderers) - { - string path = GetTransformPath(srcRenderer.transform, sourcePrefab.transform); - var tgtTransform = destinationPrefab.transform.Find(path); - if (tgtTransform == null) continue; - var tgtRenderer = tgtTransform.GetComponent(); - if (tgtRenderer == null) continue; - int blendShapeCount = srcRenderer.sharedMesh != null ? srcRenderer.sharedMesh.blendShapeCount : 0; - for (int i = 0; i < blendShapeCount; i++) - { - string shapeName = srcRenderer.sharedMesh.GetBlendShapeName(i); - int tgtIndex = tgtRenderer.sharedMesh != null ? tgtRenderer.sharedMesh.GetBlendShapeIndex(shapeName) : -1; - if (tgtIndex >= 0) - { - float value = srcRenderer.GetBlendShapeWeight(i); - tgtRenderer.SetBlendShapeWeight(tgtIndex, value); - } - } - } - Debug.Log("BlendShape 값 복사가 완료되었습니다."); - } - - void CopyActiveStates() - { - var srcTransforms = sourcePrefab.GetComponentsInChildren(true); - foreach (var srcTr in srcTransforms) - { - string path = GetTransformPath(srcTr, sourcePrefab.transform); - var tgtTr = destinationPrefab.transform.Find(path); - if (tgtTr == null) continue; - tgtTr.gameObject.SetActive(srcTr.gameObject.activeSelf); - } - Debug.Log("ActiveState 복사가 완료되었습니다."); - } -} \ No newline at end of file diff --git a/Assets/Scripts/AvatarComponetCopier.cs.meta b/Assets/Scripts/AvatarComponetCopier.cs.meta deleted file mode 100644 index e481d389a..000000000 --- a/Assets/Scripts/AvatarComponetCopier.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 39f91032a3569014da62f31d9fe0cb8b \ No newline at end of file diff --git a/Assets/Scripts/HumanBoneNameCopier.cs b/Assets/Scripts/HumanBoneNameCopier.cs new file mode 100644 index 000000000..81edb5605 --- /dev/null +++ b/Assets/Scripts/HumanBoneNameCopier.cs @@ -0,0 +1,315 @@ +using UnityEngine; +using UnityEditor; +using System.Collections.Generic; + +public class HumanBoneNameCopier : EditorWindow +{ + private GameObject sourceAvatar; + private GameObject targetAvatar; + private Vector2 scrollPosition; + private bool showAdvancedOptions = false; + private bool copyAllBones = true; + private Dictionary boneSelection = new Dictionary(); + + [MenuItem("Tools/Human Bone Name Copier")] + public static void ShowWindow() + { + GetWindow("Human Bone Name Copier"); + } + + private void OnEnable() + { + InitializeBoneSelection(); + } + + private void InitializeBoneSelection() + { + boneSelection.Clear(); + foreach (HumanBodyBones bone in System.Enum.GetValues(typeof(HumanBodyBones))) + { + if (bone != HumanBodyBones.LastBone) + { + boneSelection[bone] = true; + } + } + } + + private void OnGUI() + { + GUILayout.Label("Human Bone Name Copier", EditorStyles.boldLabel); + EditorGUILayout.Space(); + + // 소스 아바타 선택 + EditorGUILayout.BeginHorizontal(); + GUILayout.Label("Source Avatar:", GUILayout.Width(100)); + sourceAvatar = (GameObject)EditorGUILayout.ObjectField(sourceAvatar, typeof(GameObject), true); + EditorGUILayout.EndHorizontal(); + + // 타겟 아바타 선택 + EditorGUILayout.BeginHorizontal(); + GUILayout.Label("Target Avatar:", GUILayout.Width(100)); + targetAvatar = (GameObject)EditorGUILayout.ObjectField(targetAvatar, typeof(GameObject), true); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + + // 고급 옵션 토글 + showAdvancedOptions = EditorGUILayout.Foldout(showAdvancedOptions, "Advanced Options"); + + if (showAdvancedOptions) + { + EditorGUI.indentLevel++; + copyAllBones = EditorGUILayout.Toggle("Copy All Bones", copyAllBones); + + if (!copyAllBones) + { + EditorGUILayout.Space(); + GUILayout.Label("Select Bones to Copy:", EditorStyles.boldLabel); + + scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, GUILayout.Height(200)); + + foreach (var bone in boneSelection) + { + boneSelection[bone.Key] = EditorGUILayout.Toggle(bone.Key.ToString(), bone.Value); + } + + EditorGUILayout.EndScrollView(); + } + + EditorGUI.indentLevel--; + } + + EditorGUILayout.Space(); + + // 정보 표시 + if (sourceAvatar != null) + { + var sourceAnimator = sourceAvatar.GetComponent(); + if (sourceAnimator != null && sourceAnimator.avatar != null) + { + EditorGUILayout.HelpBox($"Source Avatar: {sourceAvatar.name}\n" + + $"Human Bone Count: {GetHumanBoneCount(sourceAnimator)}", + MessageType.Info); + } + } + + if (targetAvatar != null) + { + var targetAnimator = targetAvatar.GetComponent(); + if (targetAnimator != null && targetAnimator.avatar != null) + { + EditorGUILayout.HelpBox($"Target Avatar: {targetAvatar.name}\n" + + $"Human Bone Count: {GetHumanBoneCount(targetAnimator)}", + MessageType.Info); + } + } + + EditorGUILayout.Space(); + + // 버튼들 + EditorGUILayout.BeginHorizontal(); + + GUI.enabled = sourceAvatar != null && targetAvatar != null; + + if (GUILayout.Button("Copy Bone Names")) + { + CopyBoneNames(); + } + + if (GUILayout.Button("Preview Changes")) + { + PreviewChanges(); + } + + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + + // 추가 기능 버튼 + EditorGUILayout.BeginHorizontal(); + + GUI.enabled = targetAvatar != null; + + if (GUILayout.Button("Add 'zindnick : ' to Non-Human Bones")) + { + AddPrefixToNonHumanBones(); + } + + GUI.enabled = true; + + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + + if (GUILayout.Button("Clear Selection")) + { + sourceAvatar = null; + targetAvatar = null; + } + } + + private int GetHumanBoneCount(Animator animator) + { + if (animator == null || animator.avatar == null) return 0; + + int count = 0; + for (int i = 0; i < (int)HumanBodyBones.LastBone; i++) + { + if (animator.GetBoneTransform((HumanBodyBones)i) != null) + { + count++; + } + } + return count; + } + + private void CopyBoneNames() + { + if (sourceAvatar == null || targetAvatar == null) + { + EditorUtility.DisplayDialog("Error", "Please select both source and target avatars.", "OK"); + return; + } + + var sourceAnimator = sourceAvatar.GetComponent(); + var targetAnimator = targetAvatar.GetComponent(); + + if (sourceAnimator == null || sourceAnimator.avatar == null) + { + EditorUtility.DisplayDialog("Error", "Source avatar must have an Animator component with a valid Avatar.", "OK"); + return; + } + + if (targetAnimator == null || targetAnimator.avatar == null) + { + EditorUtility.DisplayDialog("Error", "Target avatar must have an Animator component with a valid Avatar.", "OK"); + return; + } + + Undo.RecordObject(targetAvatar, "Copy Human Bone Names"); + + int copiedCount = 0; + + for (int i = 0; i < (int)HumanBodyBones.LastBone; i++) + { + HumanBodyBones bone = (HumanBodyBones)i; + + if (!copyAllBones && boneSelection.ContainsKey(bone) && !boneSelection[bone]) + { + continue; + } + + Transform sourceBone = sourceAnimator.GetBoneTransform(bone); + Transform targetBone = targetAnimator.GetBoneTransform(bone); + + if (sourceBone != null && targetBone != null) + { + targetBone.name = sourceBone.name; + copiedCount++; + } + } + + EditorUtility.SetDirty(targetAvatar); + AssetDatabase.SaveAssets(); + + EditorUtility.DisplayDialog("Success", + $"Successfully copied {copiedCount} bone names from {this.sourceAvatar.name} to {this.targetAvatar.name}.", + "OK"); + } + + private void PreviewChanges() + { + if (sourceAvatar == null || targetAvatar == null) + { + EditorUtility.DisplayDialog("Error", "Please select both source and target avatars.", "OK"); + return; + } + + var sourceAnimator = sourceAvatar.GetComponent(); + var targetAnimator = targetAvatar.GetComponent(); + + if (sourceAnimator == null || sourceAnimator.avatar == null) + { + EditorUtility.DisplayDialog("Error", "Source avatar must have an Animator component with a valid Avatar.", "OK"); + return; + } + + if (targetAnimator == null || targetAnimator.avatar == null) + { + EditorUtility.DisplayDialog("Error", "Target avatar must have an Animator component with a valid Avatar.", "OK"); + return; + } + + string previewText = "Preview of bone name changes:\n\n"; + + for (int i = 0; i < (int)HumanBodyBones.LastBone; i++) + { + HumanBodyBones bone = (HumanBodyBones)i; + + if (!copyAllBones && boneSelection.ContainsKey(bone) && !boneSelection[bone]) + { + continue; + } + + Transform sourceBone = sourceAnimator.GetBoneTransform(bone); + Transform targetBone = targetAnimator.GetBoneTransform(bone); + + if (sourceBone != null && targetBone != null) + { + previewText += $"{bone}: {targetBone.name} → {sourceBone.name}\n"; + } + } + + EditorUtility.DisplayDialog("Preview", previewText, "OK"); + } + + private void AddPrefixToNonHumanBones() + { + if (targetAvatar == null) + { + EditorUtility.DisplayDialog("Error", "Please select a target avatar.", "OK"); + return; + } + + var targetAnimator = targetAvatar.GetComponent(); + + if (targetAnimator == null || targetAnimator.avatar == null) + { + EditorUtility.DisplayDialog("Error", "Target avatar must have an Animator component with a valid Avatar.", "OK"); + return; + } + + // 휴먼본 목록 수집 + HashSet humanBones = new HashSet(); + for (int i = 0; i < (int)HumanBodyBones.LastBone; i++) + { + Transform humanBone = targetAnimator.GetBoneTransform((HumanBodyBones)i); + if (humanBone != null) + { + humanBones.Add(humanBone); + } + } + + Undo.RecordObject(targetAvatar, "Add Prefix to Non-Human Bones"); + + int modifiedCount = 0; + Transform[] allTransforms = targetAvatar.GetComponentsInChildren(); + + foreach (Transform transform in allTransforms) + { + // 휴먼본이 아니고, 이미 접두사가 없는 경우에만 추가 + if (!humanBones.Contains(transform) && !transform.name.StartsWith("zindnick : ")) + { + transform.name = "zindnick : " + transform.name; + modifiedCount++; + } + } + + EditorUtility.SetDirty(targetAvatar); + AssetDatabase.SaveAssets(); + + EditorUtility.DisplayDialog("Success", + $"Successfully added 'zindnick : ' prefix to {modifiedCount} non-human bones in {targetAvatar.name}.", + "OK"); + } +} \ No newline at end of file diff --git a/Assets/Scripts/HumanBoneNameCopier.cs.meta b/Assets/Scripts/HumanBoneNameCopier.cs.meta new file mode 100644 index 000000000..849a0b479 --- /dev/null +++ b/Assets/Scripts/HumanBoneNameCopier.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b3338de307f573e4988641354c1e2edd \ No newline at end of file diff --git a/Assets/Scripts/MaterialMover.cs b/Assets/Scripts/MaterialMover.cs deleted file mode 100644 index 433f6b1fa..000000000 --- a/Assets/Scripts/MaterialMover.cs +++ /dev/null @@ -1,287 +0,0 @@ -using UnityEditor; -using UnityEngine; -using System.Collections.Generic; -using System.Linq; -using System.IO; - -public class MaterialAndTextureMover : EditorWindow -{ - public List targetObjects = new List(); - public string targetFolder = "Assets/"; - private List foundMaterials = new List(); - private List materialSelectionStatus = new List(); - private List foundTextures = new List(); - private List textureSelectionStatus = new List(); - private bool materialsSearched = false; - - private Vector2 scrollPos; - - [MenuItem("Tools/Material and Texture Mover")] - public static void ShowWindow() - { - GetWindow("Material & Texture Mover"); - } - - private void OnGUI() - { - GUILayout.Label("Step 1: Select Target Objects and Search Materials and Textures", EditorStyles.boldLabel); - - int removeIndex = -1; - for (int i = 0; i < targetObjects.Count; i++) - { - GUILayout.BeginHorizontal(); - targetObjects[i] = (GameObject)EditorGUILayout.ObjectField(targetObjects[i], typeof(GameObject), true); - if (GUILayout.Button("Remove", GUILayout.Width(60))) - { - removeIndex = i; - } - GUILayout.EndHorizontal(); - } - - if (removeIndex >= 0) - { - targetObjects.RemoveAt(removeIndex); - } - - if (GUILayout.Button("Add Target Object")) - { - targetObjects.Add(null); - } - - GUILayout.Space(10); - - if (GUILayout.Button("Search Materials and Textures")) - { - SearchMaterialsAndTextures(); - materialsSearched = true; - } - - GUILayout.Space(10); - - if (materialsSearched) - { - if (GUILayout.Button("Resolve Duplicated Names")) - { - ResolveDuplicatedNames(); - } - - GUILayout.Space(10); - } - - scrollPos = EditorGUILayout.BeginScrollView(scrollPos, GUILayout.Height(300)); - - if (materialsSearched && foundMaterials.Count > 0) - { - GUILayout.Label("Remapped Materials", EditorStyles.boldLabel); - - for (int i = 0; i < foundMaterials.Count; i++) - { - GUILayout.BeginHorizontal(); - materialSelectionStatus[i] = EditorGUILayout.ToggleLeft(foundMaterials[i] ? foundMaterials[i].name : "None", materialSelectionStatus[i], GUILayout.Width(200)); - if (foundMaterials[i] != null && GUILayout.Button("Ping", GUILayout.Width(50))) - { - EditorGUIUtility.PingObject(foundMaterials[i]); - } - GUILayout.EndHorizontal(); - } - } - - GUILayout.Space(10); - - if (materialsSearched && foundTextures.Count > 0) - { - GUILayout.Label("Remapped Textures", EditorStyles.boldLabel); - - for (int i = 0; i < foundTextures.Count; i++) - { - GUILayout.BeginHorizontal(); - textureSelectionStatus[i] = EditorGUILayout.ToggleLeft(foundTextures[i] ? foundTextures[i].name : "None", textureSelectionStatus[i], GUILayout.Width(200)); - if (foundTextures[i] != null && GUILayout.Button("Ping", GUILayout.Width(50))) - { - EditorGUIUtility.PingObject(foundTextures[i]); - } - GUILayout.EndHorizontal(); - } - } - - EditorGUILayout.EndScrollView(); - - GUILayout.Space(20); - GUILayout.Label("Step 2: Select Target Folder and Move Materials and Textures", EditorStyles.boldLabel); - - GUILayout.BeginHorizontal(); - EditorGUILayout.LabelField("Folder Path:", targetFolder); - if (GUILayout.Button("Select Folder", GUILayout.Width(100))) - { - string selectedPath = EditorUtility.OpenFolderPanel("Select Target Folder", "Assets/", ""); - if (!string.IsNullOrEmpty(selectedPath)) - { - if (selectedPath.StartsWith(Application.dataPath)) - { - targetFolder = "Assets" + selectedPath.Substring(Application.dataPath.Length); - } - else - { - Debug.LogError("Please select a folder within the Assets directory."); - } - } - } - GUILayout.EndHorizontal(); - - GUILayout.Space(10); - - if (GUILayout.Button("Move Materials and Textures")) - { - MoveMaterialsAndTextures(); - } - } - - private void SearchMaterialsAndTextures() -{ - foundMaterials.Clear(); - materialSelectionStatus.Clear(); - foundTextures.Clear(); - textureSelectionStatus.Clear(); - - foreach (var obj in targetObjects) - { - if (obj == null) continue; - - Renderer[] renderers = obj.GetComponentsInChildren(); - foreach (var renderer in renderers) - { - foreach (var mat in renderer.sharedMaterials) - { - if (mat != null && !foundMaterials.Contains(mat)) - { - foundMaterials.Add(mat); - materialSelectionStatus.Add(true); - - // 모든 텍스처 검색 - Shader shader = mat.shader; - for (int j = 0; j < ShaderUtil.GetPropertyCount(shader); j++) - { - if (ShaderUtil.GetPropertyType(shader, j) == ShaderUtil.ShaderPropertyType.TexEnv) - { - string propertyName = ShaderUtil.GetPropertyName(shader, j); - Texture texture = mat.GetTexture(propertyName); - if (texture != null && !foundTextures.Contains(texture)) - { - foundTextures.Add(texture); - textureSelectionStatus.Add(true); - } - } - } - - // 사용자 정의 속성 직접 검색 - foreach (string propertyName in mat.GetTexturePropertyNames()) - { - Texture texture = mat.GetTexture(propertyName); - if (texture != null && !foundTextures.Contains(texture)) - { - foundTextures.Add(texture); - textureSelectionStatus.Add(true); - } - } - } - } - } - } - - Debug.Log("Materials and Textures found and listed."); -} - - - private void ResolveDuplicatedNames() - { - HashSet usedNames = new HashSet(); - - for (int i = 0; i < foundMaterials.Count; i++) - { - Material mat = foundMaterials[i]; - string originalName = mat.name; - int counter = 1; - - while (usedNames.Contains(mat.name)) - { - mat.name = $"{originalName}_{counter}"; - counter++; - } - - usedNames.Add(mat.name); - } - - for (int i = 0; i < foundTextures.Count; i++) - { - Texture tex = foundTextures[i]; - string originalName = tex.name; - int counter = 1; - - while (usedNames.Contains(tex.name)) - { - tex.name = $"{originalName}_{counter}"; - counter++; - } - - usedNames.Add(tex.name); - } - - Debug.Log("Duplicated names resolved."); - } - - private void MoveMaterialsAndTextures() - { - if (!AssetDatabase.IsValidFolder(targetFolder)) - { - Debug.LogError("Target folder does not exist or is invalid."); - return; - } - - for (int i = 0; i < foundMaterials.Count; i++) - { - if (!materialSelectionStatus[i]) continue; - - Material mat = foundMaterials[i]; - string path = AssetDatabase.GetAssetPath(mat); - string targetPath = Path.Combine(targetFolder, Path.GetFileName(path)); - - if (AssetDatabase.LoadAssetAtPath(targetPath) != null) - { - Debug.LogWarning($"Material '{mat.name}' already exists in the target folder. Skipping."); - continue; - } - - if (!string.IsNullOrEmpty(path)) - { - string uniquePath = AssetDatabase.GenerateUniqueAssetPath(targetPath); - AssetDatabase.MoveAsset(path, uniquePath); - } - } - - for (int i = 0; i < foundTextures.Count; i++) - { - if (!textureSelectionStatus[i]) continue; - - Texture tex = foundTextures[i]; - string path = AssetDatabase.GetAssetPath(tex); - string targetPath = Path.Combine(targetFolder, Path.GetFileName(path)); - - if (AssetDatabase.LoadAssetAtPath(targetPath) != null) - { - Debug.LogWarning($"Texture '{tex.name}' already exists in the target folder. Skipping."); - continue; - } - - if (!string.IsNullOrEmpty(path)) - { - string uniquePath = AssetDatabase.GenerateUniqueAssetPath(targetPath); - AssetDatabase.MoveAsset(path, uniquePath); - } - } - - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); - - Debug.Log("Selected materials and textures moved successfully."); - } -} diff --git a/Assets/Scripts/MaterialMover.cs.meta b/Assets/Scripts/MaterialMover.cs.meta deleted file mode 100644 index 22019621d..000000000 --- a/Assets/Scripts/MaterialMover.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: a4d444948c00f634bb8a74111035a4d6 \ No newline at end of file diff --git a/Assets/Scripts/NilotoonMaterialMatcapSetter.cs b/Assets/Scripts/NilotoonMaterialMatcapSetter.cs deleted file mode 100644 index 52bff61b0..000000000 --- a/Assets/Scripts/NilotoonMaterialMatcapSetter.cs +++ /dev/null @@ -1,214 +0,0 @@ -#if UNITY_EDITOR -using UnityEditor; -using UnityEngine; -using System.Collections.Generic; -using System.IO; - -public class NiloMaterialMatcapSetter : EditorWindow -{ - private List materials = new List(); - private Vector2 scrollPosObjects; - private bool enableBackup = true; - - private string pEnableName_Front = "_BaseMapStackingLayer"; - private string pEnableName_Back = "Enable"; - private const string targetShaderName = "lilToon"; - - [MenuItem("Tools/Nilotoon Matcap Auto Mapper", false, 153)] - public static void ShowWindow() - { - GetWindow("닐로툰 매트캡 자동 인식기"); - } - - void OnGUI() - { - EditorGUILayout.Space(); - EditorGUILayout.LabelField("닐로툰 매트캡 자동 인식기", EditorStyles.boldLabel); - EditorGUILayout.HelpBox("릴툰에서 변환된 닐로툰 머티리얼 중 누락된 Matcap 정보를 복원합니다.\n선택에 따라 백업본을 생성합니다.", MessageType.Info); - - EditorGUILayout.Space(); - enableBackup = EditorGUILayout.ToggleLeft("💾 머티리얼 백업 생성 (권장)", enableBackup); - - EditorGUILayout.Space(); - EditorGUILayout.LabelField("💠 머티리얼 드래그 앤 드롭", EditorStyles.boldLabel); - Rect dropArea = GUILayoutUtility.GetRect(0, 50, GUILayout.ExpandWidth(true)); - GUI.Box(dropArea, "여기에 머티리얼을 드래그 하세요", EditorStyles.helpBox); - HandleDragAndDrop(dropArea); - - EditorGUILayout.Space(); - EditorGUILayout.LabelField("📝 추가된 머티리얼 목록", EditorStyles.boldLabel); - scrollPosObjects = EditorGUILayout.BeginScrollView(scrollPosObjects, GUILayout.Height(150)); - foreach (var mat in materials) - { - EditorGUILayout.ObjectField(mat, typeof(Material), true); - } - EditorGUILayout.EndScrollView(); - - EditorGUILayout.Space(); - if (GUILayout.Button("자동 복사 실행", GUILayout.Height(30))) - { - ProcessMaterials(); - materials.Clear(); - } - } - - void HandleDragAndDrop(Rect dropArea) - { - Event evt = Event.current; - switch (evt.type) - { - case EventType.DragUpdated: - case EventType.DragPerform: - if (!dropArea.Contains(evt.mousePosition)) return; - DragAndDrop.visualMode = DragAndDropVisualMode.Copy; - - if (evt.type == EventType.DragPerform) - { - DragAndDrop.AcceptDrag(); - foreach (Object draggedObject in DragAndDrop.objectReferences) - { - if (draggedObject is Material mat && !materials.Contains(mat)) - { - materials.Add(mat); - } - } - evt.Use(); - } - break; - } - } - - void ProcessMaterials() - { - Shader lilToonShader = Shader.Find(targetShaderName); - if (lilToonShader == null) - { - Debug.LogError("lilToon Shader를 찾을 수 없습니다. 먼저 프로젝트에 추가해주세요."); - return; - } - - foreach (Material originalMaterial in materials) - { - if (originalMaterial == null) continue; - - if (enableBackup) - { - string originalPath = AssetDatabase.GetAssetPath(originalMaterial); - string originalDir = Path.GetDirectoryName(originalPath); - string backupDir = Path.Combine(originalDir, "M_Backup"); - - if (!Directory.Exists(backupDir)) - { - Directory.CreateDirectory(backupDir); - AssetDatabase.Refresh(); - } - - string backupPath = Path.Combine(backupDir, originalMaterial.name + "_Backup.mat").Replace("\\", "/"); - AssetDatabase.CopyAsset(originalPath, backupPath); - AssetDatabase.ImportAsset(backupPath); - Debug.Log($"백업 생성됨: {backupPath}"); - } - - Material clonedMaterial = Object.Instantiate(originalMaterial); - clonedMaterial.shader = lilToonShader; - - List matcapInfoList = new List - { - GetMatcapInfoByName(clonedMaterial, "_UseMatCap", true), - GetMatcapInfoByName(clonedMaterial, "_UseMatCap2nd", false) - }; - - foreach (stMatcapInfo matcapInfo in matcapInfoList) - { - if (!matcapInfo._UseMatCap) continue; - - int targetLayerNumber = 0; - for (int i = 1; i <= 10; i++) - { - if (originalMaterial.GetFloat(pEnableName_Front + i + pEnableName_Back) < 0.5f) - { - targetLayerNumber = i; - originalMaterial.SetFloat(pEnableName_Front + i + pEnableName_Back, 1f); - break; - } - } - - if (targetLayerNumber == 0) - { - Debug.LogWarning($"{originalMaterial.name}: 사용할 수 있는 빈 레이어가 없습니다."); - continue; - } - - string prefix = pEnableName_Front + targetLayerNumber; - originalMaterial.SetVector(prefix + "TexUVCenterPivotScalePos", new Vector4(1, 1, 0, 0)); - originalMaterial.SetVector(prefix + "TexUVScaleOffset", new Vector4(1, 1, 0, 0)); - originalMaterial.SetVector(prefix + "TexUVAnimSpeed", Vector4.zero); - originalMaterial.SetVector(prefix + "MaskTexChannel", new Vector4(0, 1, 0, 0)); - originalMaterial.SetFloat(prefix + "TexUVRotatedAngle", 0); - originalMaterial.SetFloat(prefix + "TexUVRotateSpeed", 0); - originalMaterial.SetFloat(prefix + "MaskUVIndex", 0); - originalMaterial.SetFloat(prefix + "MaskInvertColor", 0); - originalMaterial.SetFloat(prefix + "TexIgnoreAlpha", 0); - originalMaterial.SetFloat(prefix + "ColorBlendMode", - matcapInfo._MatCapBlendMode == 0 ? 0 : - matcapInfo._MatCapBlendMode == 1 ? 2 : - matcapInfo._MatCapBlendMode == 2 ? 3 : - matcapInfo._MatCapBlendMode == 3 ? 4 : 5); - originalMaterial.SetTexture(prefix + "Tex", matcapInfo._MatCapTex); - originalMaterial.SetColor(prefix + "TintColor", new Color(matcapInfo._MatCapColor.r, matcapInfo._MatCapColor.g, matcapInfo._MatCapColor.b, 1f)); - originalMaterial.SetFloat(prefix + "MasterStrength", matcapInfo._MatCapColor.a); - originalMaterial.SetFloat(prefix + "TexUVIndex", 4); - originalMaterial.SetTexture(prefix + "MaskTex", matcapInfo._MatCapBlendMask); - - Debug.Log($"{originalMaterial.name}에 Matcap을 Layer {targetLayerNumber}에 적용함"); - } - - DestroyImmediate(clonedMaterial); - } - - AssetDatabase.SaveAssets(); - EditorUtility.DisplayDialog("Matcap 복사 완료", "모든 머티리얼에 Matcap 설정을 적용했습니다.", "확인"); - } - - private struct stMatcapInfo - { - public bool _UseMatCap; - public Texture _MatCapTex; - public Color _MatCapColor; - public Texture _MatCapBlendMask; - public int _MatCapBlendMode; - - public stMatcapInfo(bool use) - { - _UseMatCap = use; - _MatCapTex = null; - _MatCapColor = Color.white; - _MatCapBlendMask = null; - _MatCapBlendMode = 0; - } - - public stMatcapInfo(bool use, Texture tex, Color color, Texture mask, int mode) - { - _UseMatCap = use; - _MatCapTex = tex; - _MatCapColor = color; - _MatCapBlendMask = mask; - _MatCapBlendMode = mode; - } - } - - private stMatcapInfo GetMatcapInfoByName(Material mat, string propertyName, bool isFirst) - { - string suffix = isFirst ? "" : "2nd"; - if (mat.HasProperty(propertyName) && mat.GetInt(propertyName) == 1) - { - return new stMatcapInfo(true, - mat.GetTexture("_MatCap" + suffix + "Tex"), - mat.GetColor("_MatCap" + suffix + "Color"), - mat.GetTexture("_MatCap" + suffix + "BlendMask"), - mat.GetInt("_MatCap" + suffix + "BlendMode")); - } - return new stMatcapInfo(false); - } -} -#endif diff --git a/Assets/Scripts/NilotoonMaterialMatcapSetter.cs.meta b/Assets/Scripts/NilotoonMaterialMatcapSetter.cs.meta deleted file mode 100644 index ecf86e7c8..000000000 --- a/Assets/Scripts/NilotoonMaterialMatcapSetter.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 7a6ca025d3c26104d888d49ec66215df \ No newline at end of file diff --git a/Assets/Scripts/QA_Select.cs b/Assets/Scripts/QA_Select.cs deleted file mode 100644 index 0cbb2984f..000000000 --- a/Assets/Scripts/QA_Select.cs +++ /dev/null @@ -1,47 +0,0 @@ -using UnityEngine; -using UnityEditor; - -public class ParentChildSelectionTool : EditorWindow -{ - [MenuItem("Tools/Select Parent %q")] // Q 키 (Ctrl/Command+Q)로 상위 오브젝트 선택 - private static void SelectParent() - { - if (Selection.activeGameObject != null) - { - Transform selectedTransform = Selection.activeGameObject.transform; - if (selectedTransform.parent != null) - { - Selection.activeGameObject = selectedTransform.parent.gameObject; - } - else - { - Debug.Log("No parent object found."); - } - } - else - { - Debug.Log("No object selected."); - } - } - - [MenuItem("Tools/Select Child %a")] // A 키 (Ctrl/Command+A)로 하위 오브젝트 선택 - private static void SelectChild() - { - if (Selection.activeGameObject != null) - { - Transform selectedTransform = Selection.activeGameObject.transform; - if (selectedTransform.childCount > 0) - { - Selection.activeGameObject = selectedTransform.GetChild(0).gameObject; - } - else - { - Debug.Log("No child object found."); - } - } - else - { - Debug.Log("No object selected."); - } - } -} diff --git a/Assets/Scripts/QA_Select.cs.meta b/Assets/Scripts/QA_Select.cs.meta deleted file mode 100644 index 2b7895849..000000000 --- a/Assets/Scripts/QA_Select.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: cf82a01a2d2362a459389171c4b3cb1d -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/RenameHumanoidBones.cs b/Assets/Scripts/RenameHumanoidBones.cs index ef006f7f7..190ec6370 100644 --- a/Assets/Scripts/RenameHumanoidBones.cs +++ b/Assets/Scripts/RenameHumanoidBones.cs @@ -25,6 +25,12 @@ public class RenameHumanoidBones : EditorWindow FindHumanoidBones(); } + // 믹사모 네이밍으로 일괄 변경 버튼 추가 + if (GUILayout.Button("믹사모 네이밍으로 일괄 변경")) + { + RenameToMixamo(); + } + if (selectedAnimator == null) { GUILayout.Label("No valid Humanoid Animator selected.", EditorStyles.helpBox); @@ -33,6 +39,27 @@ public class RenameHumanoidBones : EditorWindow GUILayout.Label("Selected GameObject: " + selectedAnimator.gameObject.name, EditorStyles.boldLabel); + // T포즈 관련 버튼들 추가 + EditorGUILayout.Space(); + GUILayout.Label("T-Pose Functions", EditorStyles.boldLabel); + + if (GUILayout.Button("Check T-Pose Conditions")) + { + CheckTPoseConditions(); + } + + if (GUILayout.Button("Apply Perfect T-Pose")) + { + ApplyPerfectTPose(); + } + + if (GUILayout.Button("Reset to Default Pose")) + { + ResetToDefaultPose(); + } + + EditorGUILayout.Space(); + if (humanoidBones.Count > 0) { scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, GUILayout.Height(400)); @@ -124,4 +151,932 @@ public class RenameHumanoidBones : EditorWindow Debug.Log("Bone renaming completed."); } + + // T포즈 조건 검증 + private void CheckTPoseConditions() + { + if (selectedAnimator == null) + { + Debug.LogError("No valid Humanoid Animator selected."); + return; + } + + Debug.Log("=== T-Pose Conditions Check ==="); + + // 1. 몸통 뼈대들의 World X축 좌표가 0인지 확인 + CheckBodyAlignment(); + + // 2. 팔 뼈대들의 World Z축 좌표가 동일한지 확인 + CheckArmAlignment(); + + // 3. 다리 뼈대들의 World X축 좌표가 동일한지 확인 + CheckLegAlignment(); + + Debug.Log("=== T-Pose Conditions Check Complete ==="); + } + + private void CheckBodyAlignment() + { + Debug.Log("--- Body Alignment Check ---"); + + HumanBodyBones[] bodyBones = { + HumanBodyBones.Hips, + HumanBodyBones.Spine, + HumanBodyBones.Chest, + HumanBodyBones.UpperChest, + HumanBodyBones.Neck, + HumanBodyBones.Head + }; + + float targetX = 0f; + bool allAligned = true; + + foreach (var boneType in bodyBones) + { + Transform bone = selectedAnimator.GetBoneTransform(boneType); + if (bone != null) + { + float worldX = bone.position.x; + float difference = Mathf.Abs(worldX - targetX); + + Debug.Log($"{boneType}: World X = {worldX:F3} (Target: {targetX:F3}, Difference: {difference:F3})"); + + if (difference > 0.01f) + { + allAligned = false; + Debug.LogWarning($" -> {boneType} is not aligned! Difference: {difference:F3}"); + } + } + } + + if (allAligned) + { + Debug.Log("✓ Body bones are properly aligned on X-axis"); + } + else + { + Debug.LogWarning("✗ Body bones are NOT properly aligned on X-axis"); + } + } + + private void CheckArmAlignment() + { + Debug.Log("--- Arm Alignment Check ---"); + + HumanBodyBones[] leftArmBones = { + HumanBodyBones.LeftShoulder, + HumanBodyBones.LeftUpperArm, + HumanBodyBones.LeftLowerArm, + HumanBodyBones.LeftHand + }; + + HumanBodyBones[] rightArmBones = { + HumanBodyBones.RightShoulder, + HumanBodyBones.RightUpperArm, + HumanBodyBones.RightLowerArm, + HumanBodyBones.RightHand + }; + + CheckSideArmAlignment("Left Arm", leftArmBones); + CheckSideArmAlignment("Right Arm", rightArmBones); + } + + private void CheckSideArmAlignment(string sideName, HumanBodyBones[] armBones) + { + Debug.Log($"--- {sideName} Alignment Check ---"); + + float targetZ = 0f; + bool firstBone = true; + bool allAligned = true; + + foreach (var boneType in armBones) + { + Transform bone = selectedAnimator.GetBoneTransform(boneType); + if (bone != null) + { + float worldZ = bone.position.z; + + if (firstBone) + { + targetZ = worldZ; + firstBone = false; + } + + float difference = Mathf.Abs(worldZ - targetZ); + + Debug.Log($"{boneType}: World Z = {worldZ:F3} (Target: {targetZ:F3}, Difference: {difference:F3})"); + + if (difference > 0.01f) + { + allAligned = false; + Debug.LogWarning($" -> {boneType} is not aligned! Difference: {difference:F3}"); + } + } + } + + if (allAligned) + { + Debug.Log($"✓ {sideName} bones are properly aligned on Z-axis"); + } + else + { + Debug.LogWarning($"✗ {sideName} bones are NOT properly aligned on Z-axis"); + } + } + + private void CheckLegAlignment() + { + Debug.Log("--- Leg Alignment Check ---"); + + HumanBodyBones[] leftLegBones = { + HumanBodyBones.LeftUpperLeg, + HumanBodyBones.LeftLowerLeg, + HumanBodyBones.LeftFoot, + HumanBodyBones.LeftToes + }; + + HumanBodyBones[] rightLegBones = { + HumanBodyBones.RightUpperLeg, + HumanBodyBones.RightLowerLeg, + HumanBodyBones.RightFoot, + HumanBodyBones.RightToes + }; + + CheckSideLegAlignment("Left Leg", leftLegBones); + CheckSideLegAlignment("Right Leg", rightLegBones); + } + + private void CheckSideLegAlignment(string sideName, HumanBodyBones[] legBones) + { + Debug.Log($"--- {sideName} Alignment Check ---"); + + float targetX = 0f; + bool firstBone = true; + bool allAligned = true; + + foreach (var boneType in legBones) + { + Transform bone = selectedAnimator.GetBoneTransform(boneType); + if (bone != null) + { + float worldX = bone.position.x; + + if (firstBone) + { + targetX = worldX; + firstBone = false; + } + + float difference = Mathf.Abs(worldX - targetX); + + Debug.Log($"{boneType}: World X = {worldX:F3} (Target: {targetX:F3}, Difference: {difference:F3})"); + + if (difference > 0.01f) + { + allAligned = false; + Debug.LogWarning($" -> {boneType} is not aligned! Difference: {difference:F3}"); + } + } + } + + if (allAligned) + { + Debug.Log($"✓ {sideName} bones are properly aligned on X-axis"); + } + else + { + Debug.LogWarning($"✗ {sideName} bones are NOT properly aligned on X-axis"); + } + } + + // 완벽한 T포즈 적용 + private void ApplyPerfectTPose() + { + if (selectedAnimator == null) + { + Debug.LogError("No valid Humanoid Animator selected."); + return; + } + + Debug.Log("=== Applying Perfect T-Pose ==="); + + // Undo 시스템에 등록 + Undo.RecordObject(selectedAnimator.gameObject, "Apply T-Pose"); + + // 단계 1: 팔 부분 조정 + AdjustArmBones(); + + // 단계 2: 다리 부분 조정 + AdjustLegBones(); + + Debug.Log("Perfect T-Pose applied successfully!"); + } + + // 팔 뼈대 조정 (단계 1) + private void AdjustArmBones() + { + Debug.Log("Adjusting arm bones..."); + + // 왼쪽 팔 + AdjustSingleArm(HumanBodyBones.LeftUpperArm, HumanBodyBones.LeftLowerArm, HumanBodyBones.LeftHand, "Left"); + + // 오른쪽 팔 + AdjustSingleArm(HumanBodyBones.RightUpperArm, HumanBodyBones.RightLowerArm, HumanBodyBones.RightHand, "Right"); + } + + // 다리 뼈대 조정 (단계 2) + private void AdjustLegBones() + { + Debug.Log("Adjusting leg bones..."); + + // 왼쪽 다리 + AdjustSingleLeg(HumanBodyBones.LeftLowerLeg, HumanBodyBones.LeftFoot, "Left"); + + // 오른쪽 다리 + AdjustSingleLeg(HumanBodyBones.RightLowerLeg, HumanBodyBones.RightFoot, "Right"); + } + + private void AdjustSingleLeg(HumanBodyBones lowerLeg, HumanBodyBones foot, string side) + { + Debug.Log($"Adjusting {side} leg..."); + + // LowerLeg (무릎) 조정 + Transform lowerLegBone = selectedAnimator.GetBoneTransform(lowerLeg); + if (lowerLegBone != null) + { + // 로테이션은 건드리지 않음 (기존 회전값 유지) + + // 로컬 X, Z 포지션만 0으로 변경 + Vector3 localPos = lowerLegBone.localPosition; + localPos.x = 0f; + localPos.z = 0f; + lowerLegBone.localPosition = localPos; + + Debug.Log($"{side} LowerLeg - Local Rotation: {lowerLegBone.localRotation.eulerAngles}, Local Position: {lowerLegBone.localPosition}"); + } + + // Foot (발목) 조정 + Transform footBone = selectedAnimator.GetBoneTransform(foot); + if (footBone != null) + { + // 로테이션은 건드리지 않음 (기존 회전값 유지) + + // 로컬 X, Z 포지션만 0으로 변경 + Vector3 localPos = footBone.localPosition; + localPos.x = 0f; + localPos.z = 0f; + footBone.localPosition = localPos; + + Debug.Log($"{side} Foot - Local Rotation: {footBone.localRotation.eulerAngles}, Local Position: {footBone.localPosition}"); + } + + // 발가락 조정 + AdjustToes(side); + } + + private void AdjustToes(string side) + { + Debug.Log($"Adjusting {side} toes..."); + + HumanBodyBones toeBone; + + if (side == "Left") + { + toeBone = HumanBodyBones.LeftToes; + } + else // Right + { + toeBone = HumanBodyBones.RightToes; + } + + Transform bone = selectedAnimator.GetBoneTransform(toeBone); + if (bone != null) + { + // 로테이션은 건드리지 않음 (기존 회전값 유지) + + // 로컬 X, Z 포지션만 0으로 변경 + Vector3 localPos = bone.localPosition; + localPos.x = 0f; + localPos.z = 0f; + bone.localPosition = localPos; + + Debug.Log($"{side} Toes - Local Rotation: {bone.localRotation.eulerAngles}, Local Position: {bone.localPosition}"); + } + } + + private void AdjustSingleArm(HumanBodyBones upperArm, HumanBodyBones lowerArm, HumanBodyBones hand, string side) + { + Debug.Log($"Adjusting {side} arm..."); + + // UpperArm 조정 + Transform upperArmBone = selectedAnimator.GetBoneTransform(upperArm); + if (upperArmBone != null) + { + // 로컬 Rotation을 0,0,0으로 초기화 + upperArmBone.localRotation = Quaternion.identity; + + // 로컬 X, Z 포지션을 0으로 변경 + Vector3 localPos = upperArmBone.localPosition; + localPos.x = 0f; + localPos.z = 0f; + upperArmBone.localPosition = localPos; + + Debug.Log($"{side} UpperArm - Local Rotation: {upperArmBone.localRotation.eulerAngles}, Local Position: {upperArmBone.localPosition}"); + } + + // LowerArm 조정 + Transform lowerArmBone = selectedAnimator.GetBoneTransform(lowerArm); + if (lowerArmBone != null) + { + // 로컬 Rotation을 0,0,0으로 초기화 + lowerArmBone.localRotation = Quaternion.identity; + + // 로컬 X, Z 포지션을 0으로 변경 + Vector3 localPos = lowerArmBone.localPosition; + localPos.x = 0f; + localPos.z = 0f; + lowerArmBone.localPosition = localPos; + + Debug.Log($"{side} LowerArm - Local Rotation: {lowerArmBone.localRotation.eulerAngles}, Local Position: {lowerArmBone.localPosition}"); + } + + // Hand 조정 + Transform handBone = selectedAnimator.GetBoneTransform(hand); + if (handBone != null) + { + // 로컬 Rotation을 0,0,0으로 초기화 + handBone.localRotation = Quaternion.identity; + + // 로컬 X, Z 포지션을 0으로 변경 + Vector3 localPos = handBone.localPosition; + localPos.x = 0f; + localPos.z = 0f; + handBone.localPosition = localPos; + + Debug.Log($"{side} Hand - Local Rotation: {handBone.localRotation.eulerAngles}, Local Position: {handBone.localPosition}"); + } + + // 손가락 조정 (엄지손가락 제외) + AdjustFingers(side); + } + + private void AdjustFingers(string side) + { + Debug.Log($"Adjusting {side} fingers (excluding thumb)..."); + + // 엄지손가락을 제외한 손가락들 + HumanBodyBones[] fingerBones; + + if (side == "Left") + { + fingerBones = new HumanBodyBones[] + { + HumanBodyBones.LeftIndexProximal, + HumanBodyBones.LeftIndexIntermediate, + HumanBodyBones.LeftIndexDistal, + HumanBodyBones.LeftMiddleProximal, + HumanBodyBones.LeftMiddleIntermediate, + HumanBodyBones.LeftMiddleDistal, + HumanBodyBones.LeftRingProximal, + HumanBodyBones.LeftRingIntermediate, + HumanBodyBones.LeftRingDistal, + HumanBodyBones.LeftLittleProximal, + HumanBodyBones.LeftLittleIntermediate, + HumanBodyBones.LeftLittleDistal + }; + } + else // Right + { + fingerBones = new HumanBodyBones[] + { + HumanBodyBones.RightIndexProximal, + HumanBodyBones.RightIndexIntermediate, + HumanBodyBones.RightIndexDistal, + HumanBodyBones.RightMiddleProximal, + HumanBodyBones.RightMiddleIntermediate, + HumanBodyBones.RightMiddleDistal, + HumanBodyBones.RightRingProximal, + HumanBodyBones.RightRingIntermediate, + HumanBodyBones.RightRingDistal, + HumanBodyBones.RightLittleProximal, + HumanBodyBones.RightLittleIntermediate, + HumanBodyBones.RightLittleDistal + }; + } + + foreach (var fingerBone in fingerBones) + { + Transform bone = selectedAnimator.GetBoneTransform(fingerBone); + if (bone != null) + { + // 로컬 Rotation을 0,0,0으로 초기화 + bone.localRotation = Quaternion.identity; + + // 손가락 첫 마디(Proximal)는 X축을 유지, 나머지는 X, Z 포지션을 0으로 변경 + Vector3 localPos = bone.localPosition; + if (fingerBone.ToString().Contains("Proximal")) + { + // 첫 마디는 X축 유지, Z축만 0으로 + localPos.z = 0f; + } + else + { + // 두번째, 세번째 마디는 X, Z 모두 0으로 + localPos.x = 0f; + localPos.z = 0f; + } + bone.localPosition = localPos; + + Debug.Log($"{side} {fingerBone} - Local Rotation: {bone.localRotation.eulerAngles}, Local Position: {bone.localPosition}"); + } + } + + // 엄지손가락 두번째와 세번째 마디 조정 + AdjustThumbFingers(side); + } + + private void AdjustThumbFingers(string side) + { + Debug.Log($"Adjusting {side} thumb intermediate and distal..."); + + HumanBodyBones[] thumbBones; + + if (side == "Left") + { + thumbBones = new HumanBodyBones[] + { + HumanBodyBones.LeftThumbIntermediate, + HumanBodyBones.LeftThumbDistal + }; + } + else // Right + { + thumbBones = new HumanBodyBones[] + { + HumanBodyBones.RightThumbIntermediate, + HumanBodyBones.RightThumbDistal + }; + } + + foreach (var thumbBone in thumbBones) + { + Transform bone = selectedAnimator.GetBoneTransform(thumbBone); + if (bone != null) + { + // 로컬 Rotation을 0,0,0으로 초기화 + bone.localRotation = Quaternion.identity; + + // X, Z 포지션을 0으로 변경 + Vector3 localPos = bone.localPosition; + localPos.x = 0f; + localPos.z = 0f; + bone.localPosition = localPos; + + Debug.Log($"{side} {thumbBone} - Local Rotation: {bone.localRotation.eulerAngles}, Local Position: {bone.localPosition}"); + } + } + } + + // 뼈대들의 부모-자식 관계를 일시적으로 분리 + private Dictionary DetachBonesFromHierarchy() + { + Debug.Log("Detaching bones from hierarchy..."); + + Dictionary originalParents = new Dictionary(); + + // 모든 Humanoid 뼈대의 부모-자식 관계를 저장하고 분리 + for (int i = 0; i < HumanTrait.BoneCount; i++) + { + HumanBodyBones bone = (HumanBodyBones)i; + Transform boneTransform = selectedAnimator.GetBoneTransform(bone); + + if (boneTransform != null) + { + // 원래 부모를 저장 + originalParents[boneTransform] = boneTransform.parent; + + // 뼈대를 최상위 레벨로 이동 (계층구조에서 분리) + boneTransform.SetParent(null, true); + + Debug.Log($"Detached {bone} from parent: {originalParents[boneTransform]?.name ?? "None"}"); + } + } + + Debug.Log($"Detached {originalParents.Count} bones from hierarchy"); + return originalParents; + } + + // 뼈대들을 원래 부모-자식 관계로 복원 + private void RestoreBoneHierarchy(Dictionary originalParents) + { + Debug.Log("Restoring bone hierarchy..."); + + foreach (var kvp in originalParents) + { + Transform bone = kvp.Key; + Transform originalParent = kvp.Value; + + if (bone != null) + { + // 원래 부모로 복원 + bone.SetParent(originalParent, true); + Debug.Log($"Restored {bone.name} to parent: {originalParent?.name ?? "None"}"); + } + } + + Debug.Log("Bone hierarchy restored"); + } + + private void AlignBodyBones() + { + Debug.Log("Aligning body bones..."); + + HumanBodyBones[] bodyBones = { + HumanBodyBones.Hips, + HumanBodyBones.Spine, + HumanBodyBones.Chest, + HumanBodyBones.UpperChest, + HumanBodyBones.Neck, + HumanBodyBones.Head + }; + + foreach (var boneType in bodyBones) + { + Transform bone = selectedAnimator.GetBoneTransform(boneType); + if (bone != null) + { + // 현재 위치에서 X축만 0으로 조정 + Vector3 newPosition = bone.position; + newPosition.x = 0f; + bone.position = newPosition; + + // 회전을 0으로 설정 (똑바로 서있음) + bone.rotation = Quaternion.identity; + + Debug.Log($"Aligned {boneType} to X=0, rotation=0"); + } + } + } + + private void SetArmsToTPose() + { + Debug.Log("Setting arms to T-pose..."); + + // 왼쪽 팔 + SetArmToTPose(HumanBodyBones.LeftShoulder, HumanBodyBones.LeftUpperArm, + HumanBodyBones.LeftLowerArm, HumanBodyBones.LeftHand, true); + + // 오른쪽 팔 + SetArmToTPose(HumanBodyBones.RightShoulder, HumanBodyBones.RightUpperArm, + HumanBodyBones.RightLowerArm, HumanBodyBones.RightHand, false); + } + + private void SetArmToTPose(HumanBodyBones shoulder, HumanBodyBones upperArm, + HumanBodyBones lowerArm, HumanBodyBones hand, bool isLeft) + { + string side = isLeft ? "Left" : "Right"; + + // 어깨 위치 및 회전 조정 + Transform shoulderBone = selectedAnimator.GetBoneTransform(shoulder); + if (shoulderBone != null) + { + // 어깨를 몸통 중심에서 적절한 거리로 이동 + Vector3 shoulderPos = shoulderBone.position; + float shoulderOffset = isLeft ? -0.3f : 0.3f; + shoulderPos.x = shoulderOffset; + shoulderBone.position = shoulderPos; + + // 어깨를 Z축 기준으로 회전하여 팔이 수평이 되도록 함 + float shoulderZRotation = isLeft ? 90f : -90f; + shoulderBone.rotation = Quaternion.Euler(0, 0, shoulderZRotation); + + Debug.Log($"Positioned {side} shoulder at X={shoulderOffset}, Z rotation={shoulderZRotation}°"); + } + + // 상완 위치 및 회전 조정 + Transform upperArmBone = selectedAnimator.GetBoneTransform(upperArm); + if (upperArmBone != null) + { + // 상완을 어깨에서 수평으로 뻗도록 위치 조정 + Vector3 upperArmPos = upperArmBone.position; + float armLength = Vector3.Distance(shoulderBone.position, upperArmBone.position); + upperArmPos.x = isLeft ? -armLength : armLength; + upperArmBone.position = upperArmPos; + + // 상완을 완전히 펴기 위해 회전 조정 + upperArmBone.rotation = Quaternion.identity; + + Debug.Log($"Positioned {side} upper arm at X={upperArmPos.x}, rotation=0°"); + } + + // 하완 위치 및 회전 조정 + Transform lowerArmBone = selectedAnimator.GetBoneTransform(lowerArm); + if (lowerArmBone != null) + { + // 하완을 상완에서 수평으로 뻗도록 위치 조정 + Vector3 lowerArmPos = lowerArmBone.position; + float forearmLength = Vector3.Distance(upperArmBone.position, lowerArmBone.position); + lowerArmPos.x = isLeft ? -(upperArmBone.position.x + forearmLength) : (upperArmBone.position.x + forearmLength); + lowerArmBone.position = lowerArmPos; + + // 하완을 완전히 펴기 위해 회전 조정 + lowerArmBone.rotation = Quaternion.identity; + + Debug.Log($"Positioned {side} lower arm at X={lowerArmPos.x}, rotation=0°"); + } + + // 손 위치 및 회전 조정 + Transform handBone = selectedAnimator.GetBoneTransform(hand); + if (handBone != null) + { + // 손을 하완에서 수평으로 뻗도록 위치 조정 + Vector3 handPos = handBone.position; + float handLength = Vector3.Distance(lowerArmBone.position, handBone.position); + handPos.x = isLeft ? -(lowerArmBone.position.x + handLength) : (lowerArmBone.position.x + handLength); + handBone.position = handPos; + + // 손을 자연스럽게 펴기 위해 회전 조정 + handBone.rotation = Quaternion.identity; + + Debug.Log($"Positioned {side} hand at X={handPos.x}, rotation=0°"); + } + } + + private void AlignLegBones() + { + Debug.Log("Aligning leg bones..."); + + HumanBodyBones[] leftLegBones = { + HumanBodyBones.LeftUpperLeg, + HumanBodyBones.LeftLowerLeg, + HumanBodyBones.LeftFoot, + HumanBodyBones.LeftToes + }; + + HumanBodyBones[] rightLegBones = { + HumanBodyBones.RightUpperLeg, + HumanBodyBones.RightLowerLeg, + HumanBodyBones.RightFoot, + HumanBodyBones.RightToes + }; + + AlignSideLegBones("Left", leftLegBones, -0.2f); + AlignSideLegBones("Right", rightLegBones, 0.2f); + } + + private void AlignSideLegBones(string side, HumanBodyBones[] legBones, float targetX) + { + foreach (var boneType in legBones) + { + Transform bone = selectedAnimator.GetBoneTransform(boneType); + if (bone != null) + { + // 위치를 목표 X축으로 조정 + Vector3 newPosition = bone.position; + newPosition.x = targetX; + bone.position = newPosition; + + // 다리 뼈대들을 똑바로 세우기 위해 회전 조정 + bone.rotation = Quaternion.identity; + + Debug.Log($"Aligned {boneType} to X={targetX:F3}, rotation=0°"); + } + } + } + + private void SetFingersAndToesToTPose() + { + Debug.Log("Setting fingers and toes to T-pose..."); + + // 손가락 설정 (완전히 펴짐) + SetFingersToTPose(true); // 왼쪽 + SetFingersToTPose(false); // 오른쪽 + + // 발가락 설정 (자연스러운 상태) + SetToesToTPose(true); // 왼쪽 + SetToesToTPose(false); // 오른쪽 + } + + private void SetFingersToTPose(bool isLeft) + { + string side = isLeft ? "Left" : "Right"; + + // 모든 손가락 관절을 0도로 설정 + for (int finger = 0; finger < 5; finger++) + { + for (int joint = 0; joint < 3; joint++) + { + HumanBodyBones fingerBone = GetFingerBone(isLeft, finger, joint); + if (fingerBone != HumanBodyBones.LastBone) + { + Transform bone = selectedAnimator.GetBoneTransform(fingerBone); + if (bone != null) + { + bone.localRotation = Quaternion.identity; + } + } + } + } + + Debug.Log($"Set {side} fingers to straight position"); + } + + private void SetToesToTPose(bool isLeft) + { + string side = isLeft ? "Left" : "Right"; + + // 발가락을 자연스러운 상태로 설정 + HumanBodyBones toeBone = isLeft ? HumanBodyBones.LeftToes : HumanBodyBones.RightToes; + Transform bone = selectedAnimator.GetBoneTransform(toeBone); + if (bone != null) + { + bone.localRotation = Quaternion.identity; + Debug.Log($"Set {side} toes to natural position"); + } + } + + private HumanBodyBones GetFingerBone(bool isLeft, int finger, int joint) + { + if (finger == 0) // 엄지 + { + if (isLeft) + { + switch (joint) + { + case 0: return HumanBodyBones.LeftThumbProximal; + case 1: return HumanBodyBones.LeftThumbIntermediate; + case 2: return HumanBodyBones.LeftThumbDistal; + } + } + else + { + switch (joint) + { + case 0: return HumanBodyBones.RightThumbProximal; + case 1: return HumanBodyBones.RightThumbIntermediate; + case 2: return HumanBodyBones.RightThumbDistal; + } + } + } + else // 나머지 손가락들 + { + if (isLeft) + { + switch (finger) + { + case 1: return joint == 0 ? HumanBodyBones.LeftIndexProximal : + joint == 1 ? HumanBodyBones.LeftIndexIntermediate : + HumanBodyBones.LeftIndexDistal; + case 2: return joint == 0 ? HumanBodyBones.LeftMiddleProximal : + joint == 1 ? HumanBodyBones.LeftMiddleIntermediate : + HumanBodyBones.LeftMiddleDistal; + case 3: return joint == 0 ? HumanBodyBones.LeftRingProximal : + joint == 1 ? HumanBodyBones.LeftRingIntermediate : + HumanBodyBones.LeftRingDistal; + case 4: return joint == 0 ? HumanBodyBones.LeftLittleProximal : + joint == 1 ? HumanBodyBones.LeftLittleIntermediate : + HumanBodyBones.LeftLittleDistal; + } + } + else + { + switch (finger) + { + case 1: return joint == 0 ? HumanBodyBones.RightIndexProximal : + joint == 1 ? HumanBodyBones.RightIndexIntermediate : + HumanBodyBones.RightIndexDistal; + case 2: return joint == 0 ? HumanBodyBones.RightMiddleProximal : + joint == 1 ? HumanBodyBones.RightMiddleIntermediate : + HumanBodyBones.RightMiddleDistal; + case 3: return joint == 0 ? HumanBodyBones.RightRingProximal : + joint == 1 ? HumanBodyBones.RightRingIntermediate : + HumanBodyBones.RightRingDistal; + case 4: return joint == 0 ? HumanBodyBones.RightLittleProximal : + joint == 1 ? HumanBodyBones.RightLittleIntermediate : + HumanBodyBones.RightLittleDistal; + } + } + } + + return HumanBodyBones.LastBone; // 유효하지 않은 경우 + } + + // 기본 포즈로 리셋 + private void ResetToDefaultPose() + { + if (selectedAnimator == null) + { + Debug.LogError("No valid Humanoid Animator selected."); + return; + } + + Debug.Log("=== Resetting to Default Pose ==="); + + // Undo 시스템에 등록 + Undo.RecordObject(selectedAnimator.gameObject, "Reset to Default Pose"); + + // 모든 뼈대의 로컬 회전을 0으로 리셋 + for (int i = 0; i < HumanTrait.BoneCount; i++) + { + HumanBodyBones bone = (HumanBodyBones)i; + Transform boneTransform = selectedAnimator.GetBoneTransform(bone); + + if (boneTransform != null) + { + boneTransform.localRotation = Quaternion.identity; + } + } + + Debug.Log("Reset to default pose completed!"); + } + + // 믹사모 네이밍 매핑 및 일괄 변경 함수 추가 + private void RenameToMixamo() + { + if (selectedAnimator == null) + { + Debug.LogError("No valid Humanoid Animator selected."); + return; + } + + // HumanBodyBones와 믹사모 네이밍 매핑 + var mixamoMap = new Dictionary + { + { HumanBodyBones.Hips, "mixamorig:Hips" }, + { HumanBodyBones.LeftUpperLeg, "mixamorig:LeftUpLeg" }, + { HumanBodyBones.LeftLowerLeg, "mixamorig:LeftLeg" }, + { HumanBodyBones.LeftFoot, "mixamorig:LeftFoot" }, + { HumanBodyBones.LeftToes, "mixamorig:LeftToeBase" }, + { HumanBodyBones.RightUpperLeg, "mixamorig:RightUpLeg" }, + { HumanBodyBones.RightLowerLeg, "mixamorig:RightLeg" }, + { HumanBodyBones.RightFoot, "mixamorig:RightFoot" }, + { HumanBodyBones.RightToes, "mixamorig:RightToeBase" }, + { HumanBodyBones.Spine, "mixamorig:Spine" }, + { HumanBodyBones.Chest, "mixamorig:Spine1" }, + { HumanBodyBones.UpperChest, "mixamorig:Spine2" }, // UpperChest는 있는 경우만 + { HumanBodyBones.Neck, "mixamorig:Neck" }, + { HumanBodyBones.Head, "mixamorig:Head" }, + { HumanBodyBones.LeftShoulder, "mixamorig:LeftShoulder" }, + { HumanBodyBones.LeftUpperArm, "mixamorig:LeftArm" }, + { HumanBodyBones.LeftLowerArm, "mixamorig:LeftForeArm" }, + { HumanBodyBones.LeftHand, "mixamorig:LeftHand" }, + { HumanBodyBones.RightShoulder, "mixamorig:RightShoulder" }, + { HumanBodyBones.RightUpperArm, "mixamorig:RightArm" }, + { HumanBodyBones.RightLowerArm, "mixamorig:RightForeArm" }, + { HumanBodyBones.RightHand, "mixamorig:RightHand" }, + // 왼손 손가락 + { HumanBodyBones.LeftThumbProximal, "mixamorig:LeftHandThumb1" }, + { HumanBodyBones.LeftThumbIntermediate, "mixamorig:LeftHandThumb2" }, + { HumanBodyBones.LeftThumbDistal, "mixamorig:LeftHandThumb3" }, + // mixamo에는 Thumb4가 있으나, Unity에는 없음 + { HumanBodyBones.LeftIndexProximal, "mixamorig:LeftHandIndex1" }, + { HumanBodyBones.LeftIndexIntermediate, "mixamorig:LeftHandIndex2" }, + { HumanBodyBones.LeftIndexDistal, "mixamorig:LeftHandIndex3" }, + { HumanBodyBones.LeftMiddleProximal, "mixamorig:LeftHandMiddle1" }, + { HumanBodyBones.LeftMiddleIntermediate, "mixamorig:LeftHandMiddle2" }, + { HumanBodyBones.LeftMiddleDistal, "mixamorig:LeftHandMiddle3" }, + { HumanBodyBones.LeftRingProximal, "mixamorig:LeftHandRing1" }, + { HumanBodyBones.LeftRingIntermediate, "mixamorig:LeftHandRing2" }, + { HumanBodyBones.LeftRingDistal, "mixamorig:LeftHandRing3" }, + { HumanBodyBones.LeftLittleProximal, "mixamorig:LeftHandPinky1" }, + { HumanBodyBones.LeftLittleIntermediate, "mixamorig:LeftHandPinky2" }, + { HumanBodyBones.LeftLittleDistal, "mixamorig:LeftHandPinky3" }, + // 오른손 손가락 + { HumanBodyBones.RightThumbProximal, "mixamorig:RightHandThumb1" }, + { HumanBodyBones.RightThumbIntermediate, "mixamorig:RightHandThumb2" }, + { HumanBodyBones.RightThumbDistal, "mixamorig:RightHandThumb3" }, + { HumanBodyBones.RightIndexProximal, "mixamorig:RightHandIndex1" }, + { HumanBodyBones.RightIndexIntermediate, "mixamorig:RightHandIndex2" }, + { HumanBodyBones.RightIndexDistal, "mixamorig:RightHandIndex3" }, + { HumanBodyBones.RightMiddleProximal, "mixamorig:RightHandMiddle1" }, + { HumanBodyBones.RightMiddleIntermediate, "mixamorig:RightHandMiddle2" }, + { HumanBodyBones.RightMiddleDistal, "mixamorig:RightHandMiddle3" }, + { HumanBodyBones.RightRingProximal, "mixamorig:RightHandRing1" }, + { HumanBodyBones.RightRingIntermediate, "mixamorig:RightHandRing2" }, + { HumanBodyBones.RightRingDistal, "mixamorig:RightHandRing3" }, + { HumanBodyBones.RightLittleProximal, "mixamorig:RightHandPinky1" }, + { HumanBodyBones.RightLittleIntermediate, "mixamorig:RightHandPinky2" }, + { HumanBodyBones.RightLittleDistal, "mixamorig:RightHandPinky3" }, + }; + + // UpperChest가 없는 경우 무시 + foreach (var kvp in mixamoMap) + { + if (kvp.Key == HumanBodyBones.UpperChest) + { + var upperChest = selectedAnimator.GetBoneTransform(HumanBodyBones.UpperChest); + if (upperChest == null) continue; + upperChest.name = kvp.Value; + Debug.Log($"UpperChest 본이 있어 이름을 {kvp.Value}로 변경"); + continue; + } + var bone = selectedAnimator.GetBoneTransform(kvp.Key); + if (bone != null) + { + bone.name = kvp.Value; + Debug.Log($"{kvp.Key} 이름을 {kvp.Value}로 변경"); + } + } + Debug.Log("믹사모 네이밍으로 일괄 변경 완료!"); + } }