Fix : 모션 녹화 툴 업데이트
This commit is contained in:
parent
baed224468
commit
419c5919cd
@ -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)
|
||||
{
|
||||
|
||||
@ -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<string, GameObject>();
|
||||
@ -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<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 = "")
|
||||
{
|
||||
@ -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)
|
||||
{
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 Animator의 포즈를 T-포즈로 설정합니다.
|
||||
/// T-포즈를 설정합니다.
|
||||
/// </summary>
|
||||
/// <param name="animator">T-포즈를 설정할 Animator</param>
|
||||
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
|
||||
{
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user