From 419c5919cdd3b7172fca118abd6822c618c24a26 Mon Sep 17 00:00:00 2001 From: KINDNICK <68893236+KINDNICK@users.noreply.github.com> Date: Thu, 24 Jul 2025 01:09:34 +0900 Subject: [PATCH] =?UTF-8?q?Fix=20:=20=EB=AA=A8=EC=85=98=20=EB=85=B9?= =?UTF-8?q?=ED=99=94=20=ED=88=B4=20=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/Editor/HumanoidPosesEditor.cs | 58 +- .../Scripts/HumanoidPoses.cs | 687 ++++-------------- .../Scripts/MotionDataRecorder.cs | 11 +- .../Scripts/SavePathManager.cs | 9 +- 4 files changed, 159 insertions(+), 606 deletions(-) diff --git a/Assets/External/EasyMotionRecorder/Scripts/Editor/HumanoidPosesEditor.cs b/Assets/External/EasyMotionRecorder/Scripts/Editor/HumanoidPosesEditor.cs index 52971a51..3f780e8e 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/HumanoidPoses.cs b/Assets/External/EasyMotionRecorder/Scripts/HumanoidPoses.cs index 8cf5c6f2..fb0ba592 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/HumanoidPoses.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/HumanoidPoses.cs @@ -92,6 +92,7 @@ namespace Entum [SerializeField, Tooltip("T-포즈가 저장되었는지 여부")] public bool HasTPoseData = false; +#if UNITY_EDITOR // 세션 ID를 가져오는 메서드 (MotionDataRecorder와 동일한 세션 ID 사용) private string GetSessionID() { @@ -142,6 +143,7 @@ namespace Entum Debug.LogWarning($"세션 ID를 찾을 수 없어 현재 시간 사용: {fallbackSessionID}"); return fallbackSessionID; } +#endif #if UNITY_EDITOR //Genericなanimファイルとして出力する [ContextMenu("Export as Generic animation clips")] @@ -683,19 +685,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) @@ -744,52 +734,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 +768,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 +1022,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 +1031,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 +1121,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 +1263,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 +1468,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 +1563,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 +1653,8 @@ namespace Entum Debug.Log($"중복 제거 후 고유 본 경로 수: {uniqueBonePaths.Count}"); + + foreach (var kvp in uniqueBonePaths) { var bonePath = kvp.Key; @@ -1968,17 +1677,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 +1700,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 +1732,9 @@ namespace Entum // 스켈레톤 구조 출력 PrintSkeletonHierarchy(root); + // Bip001 Pelvis 강제 수정 + FixBip001PelvisTransform(root); + // 스켈레톤이 실제로 씬에 있는지 확인 if (root != null && root.transform.childCount > 0) { @@ -2022,156 +1748,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 +1759,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 +1775,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 +1897,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/MotionDataRecorder.cs b/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs index 252b27fc..3d9cd969 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs @@ -16,6 +16,7 @@ using System.Collections.Generic; using UnityEditor; #endif using EasyMotionRecorder; +using KindRetargeting; using UniHumanoid; namespace Entum @@ -229,9 +230,8 @@ namespace Entum } /// - /// 지정된 Animator의 포즈를 T-포즈로 설정합니다. + /// T-포즈를 설정합니다. /// - /// T-포즈를 설정할 Animator private void SetTPose(Animator animator) { if (animator == null || animator.avatar == null) @@ -246,6 +246,13 @@ namespace Entum { var pose = humanPoseClip.GetPose(); HumanPoseTransfer.SetPose(avatar, transform, pose); + + // 소스 아바타의 UpperChest 본 로컬 포지션 초기화 + Transform upperChest = animator.GetBoneTransform(HumanBodyBones.UpperChest); + if (upperChest != null) + { + upperChest.localPosition = Vector3.zero; + } } else { diff --git a/Assets/External/EasyMotionRecorder/Scripts/SavePathManager.cs b/Assets/External/EasyMotionRecorder/Scripts/SavePathManager.cs index 5e3d5139..bdd24551 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/SavePathManager.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/SavePathManager.cs @@ -40,15 +40,13 @@ 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; + public bool ExportHumanoidOnSave => exportHumanoidOnSave; public bool ExportGenericOnSave => exportGenericOnSave; public bool ExportFBXAsciiOnSave => exportFBXAsciiOnSave; public bool ExportFBXBinaryOnSave => exportFBXBinaryOnSave; - public bool ExportBipedFBXAsciiOnSave => exportBipedFBXAsciiOnSave; - public bool ExportBipedFBXBinaryOnSave => exportBipedFBXBinaryOnSave; + private void Awake() { @@ -141,8 +139,7 @@ namespace EasyMotionRecorder exportGenericOnSave = false; exportFBXAsciiOnSave = false; exportFBXBinaryOnSave = false; - exportBipedFBXAsciiOnSave = false; - exportBipedFBXBinaryOnSave = false; + InitializePaths(); }