Fix : 모션 녹화 툴 업데이트

This commit is contained in:
KINDNICK 2025-07-24 01:09:34 +09:00
parent baed224468
commit 419c5919cd
4 changed files with 159 additions and 606 deletions

View File

@ -436,27 +436,7 @@ namespace Entum
EditorGUILayout.Space(8); 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(); 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) private float EstimateFileSize(HumanoidPoses humanoidPoses)
{ {

View File

@ -92,6 +92,7 @@ namespace Entum
[SerializeField, Tooltip("T-포즈가 저장되었는지 여부")] [SerializeField, Tooltip("T-포즈가 저장되었는지 여부")]
public bool HasTPoseData = false; public bool HasTPoseData = false;
#if UNITY_EDITOR
// 세션 ID를 가져오는 메서드 (MotionDataRecorder와 동일한 세션 ID 사용) // 세션 ID를 가져오는 메서드 (MotionDataRecorder와 동일한 세션 ID 사용)
private string GetSessionID() private string GetSessionID()
{ {
@ -142,6 +143,7 @@ namespace Entum
Debug.LogWarning($"세션 ID를 찾을 수 없어 현재 시간 사용: {fallbackSessionID}"); Debug.LogWarning($"세션 ID를 찾을 수 없어 현재 시간 사용: {fallbackSessionID}");
return fallbackSessionID; return fallbackSessionID;
} }
#endif
#if UNITY_EDITOR #if UNITY_EDITOR
//Genericなanimファイルとして出力する //Genericなanimファイルとして出力する
[ContextMenu("Export as Generic animation clips")] [ContextMenu("Export as Generic animation clips")]
@ -683,19 +685,7 @@ namespace Entum
ExportFBXWithEncoding(false); 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 내보내기 (인코딩 설정 포함) // FBX 내보내기 (인코딩 설정 포함)
private void ExportFBXWithEncoding(bool useAscii) private void ExportFBXWithEncoding(bool useAscii)
@ -744,52 +734,7 @@ namespace Entum
Debug.Log($"FBX 애니메이션 파일이 저장되었습니다: {uniqueAssetPath}"); 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() private AnimationClip CreateGenericAnimationClip()
@ -823,6 +768,18 @@ namespace Entum
continue; 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('/'); string cleanPath = bone.Name.TrimEnd('/');
@ -1065,206 +1022,7 @@ namespace Entum
return clip; 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 내보내기 메서드 // 스켈레톤 생성 후 FBX 내보내기 메서드
private void ExportSkeletonWithAnimationToFBX(string fbxPath, bool useAscii = true) private void ExportSkeletonWithAnimationToFBX(string fbxPath, bool useAscii = true)
@ -1273,12 +1031,7 @@ namespace Entum
EditorApplication.delayCall += () => ExportSkeletonWithAnimationToFBXStepByStep(fbxPath, useAscii); 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) 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.animationType = ModelImporterAnimationType.Generic;
importer.animationCompression = ModelImporterAnimationCompression.Off; importer.animationCompression = ModelImporterAnimationCompression.Off;
// 본 이름 설정 - 띄어쓰기 유지
importer.importBlendShapes = false;
importer.importVisibility = false;
importer.importCameras = false;
importer.importLights = false;
// 애니메이션 클립 설정 // 애니메이션 클립 설정
var clipSettings = importer.defaultClipAnimations; var clipSettings = importer.defaultClipAnimations;
if (clipSettings.Length > 0) if (clipSettings.Length > 0)
@ -1794,21 +1468,28 @@ namespace Entum
Debug.LogWarning("ExportFormat 속성을 찾을 수 없습니다."); Debug.LogWarning("ExportFormat 속성을 찾을 수 없습니다.");
} }
// 애니메이션 포함 설정 // ExportModelOptions의 모든 속성 확인
var includeAnimationProperty = exportModelOptionsType.GetProperty("IncludeAnimation"); var properties = exportModelOptionsType.GetProperties();
if (includeAnimationProperty != null) Debug.Log("ExportModelOptions의 모든 속성:");
foreach (var property in properties)
{ {
includeAnimationProperty.SetValue(exportOptions, true); Debug.Log($" - {property.Name}: {property.PropertyType.Name}");
Debug.Log("애니메이션 포함 설정: true"); }
// UseMayaCompatibleNames 속성이 존재하는지 확인 후 설정
var useMayaCompatibleNamesProperty = exportModelOptionsType.GetProperty("UseMayaCompatibleNames");
if (useMayaCompatibleNamesProperty != null)
{
useMayaCompatibleNamesProperty.SetValue(exportOptions, false);
Debug.Log("UseMayaCompatibleNames 속성을 false로 설정했습니다.");
} }
else else
{ {
Debug.LogWarning("IncludeAnimation 속성을 찾을 수 없습니다."); Debug.LogWarning("UseMayaCompatibleNames 속성을 찾을 수 없습니다.");
} }
// 스켈레톤만 내보내기 (Animation 컴포넌트가 포함됨) // 스켈레톤만 내보내기 (Animation 컴포넌트가 포함됨)
var objectsToExport = new UnityEngine.Object[] { skeletonRoot }; var objectsToExport = new UnityEngine.Object[] { skeletonRoot };
Debug.Log($"내보낼 오브젝트: {objectsToExport.Length}개"); Debug.Log($"내보낼 오브젝트: {objectsToExport.Length}개");
Debug.Log($"1. 스켈레톤 (Animation 컴포넌트 포함): {skeletonRoot.name}"); Debug.Log($"1. 스켈레톤 (Animation 컴포넌트 포함): {skeletonRoot.name}");
@ -1882,17 +1563,43 @@ namespace Entum
{ {
var bone = bones[i]; var bone = bones[i];
Debug.Log($"본 {i}: '{bone.Name}' - 위치: {bone.LocalPosition}, 회전: {bone.LocalRotation}"); 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("=== 분석 완료 ==="); 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; string rootName = this.name;
if (string.IsNullOrEmpty(rootName)) if (string.IsNullOrEmpty(rootName))
{ {
rootName = "Skeleton"; rootName = "Skeleton";
} }
var root = new GameObject(rootName); var root = new GameObject(rootName);
Debug.Log($"루트 GameObject 생성됨: {root.name}"); Debug.Log($"루트 GameObject 생성됨: {root.name} (첫 번째 본: {firstBoneName})");
// 본 계층 구조 생성 // 본 계층 구조 생성
var boneGameObjects = new Dictionary<string, GameObject>(); var boneGameObjects = new Dictionary<string, GameObject>();
@ -1946,6 +1653,8 @@ namespace Entum
Debug.Log($"중복 제거 후 고유 본 경로 수: {uniqueBonePaths.Count}"); Debug.Log($"중복 제거 후 고유 본 경로 수: {uniqueBonePaths.Count}");
foreach (var kvp in uniqueBonePaths) foreach (var kvp in uniqueBonePaths)
{ {
var bonePath = kvp.Key; var bonePath = kvp.Key;
@ -1968,17 +1677,19 @@ namespace Entum
if (!boneGameObjects.ContainsKey(currentPath)) if (!boneGameObjects.ContainsKey(currentPath))
{ {
// 루트 본이 본 데이터에 포함되어 있고, 현재 처리 중인 본이 루트인 경우 // 첫 번째 경로 부분이 첫 번째 본인 경우 (Bip001 등)
if (hasRootInData && i == 0 && pathParts.Length == 1 && rootBones.Contains(part)) if (i == 0 && part == firstBoneName)
{ {
// 루트 본은 이미 생성된 루트 GameObject를 사용 // 첫 번째 본을 루트 하위에 생성
boneGameObjects[currentPath] = root; var firstBoneGO = new GameObject(part);
Debug.Log($"루트 본 '{part}'을 기존 루트 GameObject에 연결 (중복 방지)"); firstBoneGO.transform.SetParent(root.transform);
firstBoneGO.transform.localPosition = bone.LocalPosition;
firstBoneGO.transform.localRotation = bone.LocalRotation;
createdBones++;
Debug.Log($"첫 번째 본 '{part}'을 루트 하위에 생성: 위치={bone.LocalPosition}, 회전={bone.LocalRotation}");
// 루트의 위치와 회전을 설정 boneGameObjects[currentPath] = firstBoneGO;
root.transform.localPosition = bone.LocalPosition; parent = firstBoneGO;
root.transform.localRotation = bone.LocalRotation;
Debug.Log($"루트 설정: 위치={bone.LocalPosition}, 회전={bone.LocalRotation}");
continue; continue;
} }
@ -1989,6 +1700,18 @@ namespace Entum
// 첫 번째 포즈의 위치와 회전 설정 // 첫 번째 포즈의 위치와 회전 설정
if (i == pathParts.Length - 1) // 마지막 부분 (실제 본) 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.localPosition = bone.LocalPosition;
boneGO.transform.localRotation = bone.LocalRotation; boneGO.transform.localRotation = bone.LocalRotation;
Debug.Log($"본 설정: {part} - 위치: {bone.LocalPosition}, 회전: {bone.LocalRotation}"); Debug.Log($"본 설정: {part} - 위치: {bone.LocalPosition}, 회전: {bone.LocalRotation}");
@ -2009,6 +1732,9 @@ namespace Entum
// 스켈레톤 구조 출력 // 스켈레톤 구조 출력
PrintSkeletonHierarchy(root); PrintSkeletonHierarchy(root);
// Bip001 Pelvis 강제 수정
FixBip001PelvisTransform(root);
// 스켈레톤이 실제로 씬에 있는지 확인 // 스켈레톤이 실제로 씬에 있는지 확인
if (root != null && root.transform.childCount > 0) 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<string, GameObject>();
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 = "") private void PrintSkeletonHierarchy(GameObject root, string indent = "")
{ {
@ -2183,20 +1759,6 @@ namespace Entum
} }
} }
// 스켈레톤에서 모든 본 GameObject 찾기
private void FindAllBoneGameObjects(GameObject root, Dictionary<string, GameObject> boneGameObjects)
{
var allChildren = root.GetComponentsInChildren<Transform>();
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) private string GetRelativePath(Transform root, Transform target)
{ {
@ -2213,11 +1775,59 @@ namespace Entum
return string.Join("/", pathList); return string.Join("/", pathList);
} }
// 애니메이션 속성 경로 가져오기 // Bip001 Pelvis 강제 수정 메서드
private string GetPropertyPath(GameObject boneGO, GameObject root) 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 #endif
[Serializable] [Serializable]
@ -2287,12 +1897,7 @@ namespace Entum
// 경로 끝의 슬래시 제거 // 경로 끝의 슬래시 제거
path = path.TrimEnd('/'); path = path.TrimEnd('/');
// Unity 애니메이션 시스템에서 루트 오브젝트 이름을 제거하는 경우를 대비
// 루트가 "Bip001"인 경우, 경로에서 "Bip001/" 부분을 제거
if (root.name == "Bip001" && path.StartsWith("Bip001/"))
{
path = path.Substring("Bip001/".Length);
}
_pathCache.Add(target, path); _pathCache.Add(target, path);
return path; return path;

View File

@ -16,6 +16,7 @@ using System.Collections.Generic;
using UnityEditor; using UnityEditor;
#endif #endif
using EasyMotionRecorder; using EasyMotionRecorder;
using KindRetargeting;
using UniHumanoid; using UniHumanoid;
namespace Entum namespace Entum
@ -229,9 +230,8 @@ namespace Entum
} }
/// <summary> /// <summary>
/// 지정된 Animator의 포즈를 T-포즈로 설정합니다. /// T-포즈를 설정합니다.
/// </summary> /// </summary>
/// <param name="animator">T-포즈를 설정할 Animator</param>
private void SetTPose(Animator animator) private void SetTPose(Animator animator)
{ {
if (animator == null || animator.avatar == null) if (animator == null || animator.avatar == null)
@ -246,6 +246,13 @@ namespace Entum
{ {
var pose = humanPoseClip.GetPose(); var pose = humanPoseClip.GetPose();
HumanPoseTransfer.SetPose(avatar, transform, pose); HumanPoseTransfer.SetPose(avatar, transform, pose);
// 소스 아바타의 UpperChest 본 로컬 포지션 초기화
Transform upperChest = animator.GetBoneTransform(HumanBodyBones.UpperChest);
if (upperChest != null)
{
upperChest.localPosition = Vector3.zero;
}
} }
else else
{ {

View File

@ -40,15 +40,13 @@ namespace EasyMotionRecorder
[SerializeField] private bool exportGenericOnSave = false; [SerializeField] private bool exportGenericOnSave = false;
[SerializeField] private bool exportFBXAsciiOnSave = false; [SerializeField] private bool exportFBXAsciiOnSave = false;
[SerializeField] private bool exportFBXBinaryOnSave = false; [SerializeField] private bool exportFBXBinaryOnSave = false;
[SerializeField] private bool exportBipedFBXAsciiOnSave = false;
[SerializeField] private bool exportBipedFBXBinaryOnSave = false;
public bool ExportHumanoidOnSave => exportHumanoidOnSave; public bool ExportHumanoidOnSave => exportHumanoidOnSave;
public bool ExportGenericOnSave => exportGenericOnSave; public bool ExportGenericOnSave => exportGenericOnSave;
public bool ExportFBXAsciiOnSave => exportFBXAsciiOnSave; public bool ExportFBXAsciiOnSave => exportFBXAsciiOnSave;
public bool ExportFBXBinaryOnSave => exportFBXBinaryOnSave; public bool ExportFBXBinaryOnSave => exportFBXBinaryOnSave;
public bool ExportBipedFBXAsciiOnSave => exportBipedFBXAsciiOnSave;
public bool ExportBipedFBXBinaryOnSave => exportBipedFBXBinaryOnSave;
private void Awake() private void Awake()
{ {
@ -141,8 +139,7 @@ namespace EasyMotionRecorder
exportGenericOnSave = false; exportGenericOnSave = false;
exportFBXAsciiOnSave = false; exportFBXAsciiOnSave = false;
exportFBXBinaryOnSave = false; exportFBXBinaryOnSave = false;
exportBipedFBXAsciiOnSave = false;
exportBipedFBXBinaryOnSave = false;
InitializePaths(); InitializePaths();
} }