/** [EasyMotionRecorder] Copyright (c) 2018 Duo.inc This software is released under the MIT License. http://opensource.org/licenses/mit-license.php */ using UnityEngine; using System; using System.Text; using System.Collections.Generic; using System.Linq; using System.IO; #if UNITY_EDITOR using UnityEditor; using EasyMotionRecorder; #endif namespace Entum { [Serializable] public class MotionDataSettings { public enum Rootbonesystem { Hipbone, Objectroot } /// /// Humanoid用のMuscleマッピング /// public static Dictionary TraitPropMap = new Dictionary { {"Left Thumb 1 Stretched", "LeftHand.Thumb.1 Stretched"}, {"Left Thumb Spread", "LeftHand.Thumb.Spread"}, {"Left Thumb 2 Stretched", "LeftHand.Thumb.2 Stretched"}, {"Left Thumb 3 Stretched", "LeftHand.Thumb.3 Stretched"}, {"Left Index 1 Stretched", "LeftHand.Index.1 Stretched"}, {"Left Index Spread", "LeftHand.Index.Spread"}, {"Left Index 2 Stretched", "LeftHand.Index.2 Stretched"}, {"Left Index 3 Stretched", "LeftHand.Index.3 Stretched"}, {"Left Middle 1 Stretched", "LeftHand.Middle.1 Stretched"}, {"Left Middle Spread", "LeftHand.Middle.Spread"}, {"Left Middle 2 Stretched", "LeftHand.Middle.2 Stretched"}, {"Left Middle 3 Stretched", "LeftHand.Middle.3 Stretched"}, {"Left Ring 1 Stretched", "LeftHand.Ring.1 Stretched"}, {"Left Ring Spread", "LeftHand.Ring.Spread"}, {"Left Ring 2 Stretched", "LeftHand.Ring.2 Stretched"}, {"Left Ring 3 Stretched", "LeftHand.Ring.3 Stretched"}, {"Left Little 1 Stretched", "LeftHand.Little.1 Stretched"}, {"Left Little Spread", "LeftHand.Little.Spread"}, {"Left Little 2 Stretched", "LeftHand.Little.2 Stretched"}, {"Left Little 3 Stretched", "LeftHand.Little.3 Stretched"}, {"Right Thumb 1 Stretched", "RightHand.Thumb.1 Stretched"}, {"Right Thumb Spread", "RightHand.Thumb.Spread"}, {"Right Thumb 2 Stretched", "RightHand.Thumb.2 Stretched"}, {"Right Thumb 3 Stretched", "RightHand.Thumb.3 Stretched"}, {"Right Index 1 Stretched", "RightHand.Index.1 Stretched"}, {"Right Index Spread", "RightHand.Index.Spread"}, {"Right Index 2 Stretched", "RightHand.Index.2 Stretched"}, {"Right Index 3 Stretched", "RightHand.Index.3 Stretched"}, {"Right Middle 1 Stretched", "RightHand.Middle.1 Stretched"}, {"Right Middle Spread", "RightHand.Middle.Spread"}, {"Right Middle 2 Stretched", "RightHand.Middle.2 Stretched"}, {"Right Middle 3 Stretched", "RightHand.Middle.3 Stretched"}, {"Right Ring 1 Stretched", "RightHand.Ring.1 Stretched"}, {"Right Ring Spread", "RightHand.Ring.Spread"}, {"Right Ring 2 Stretched", "RightHand.Ring.2 Stretched"}, {"Right Ring 3 Stretched", "RightHand.Ring.3 Stretched"}, {"Right Little 1 Stretched", "RightHand.Little.1 Stretched"}, {"Right Little Spread", "RightHand.Little.Spread"}, {"Right Little 2 Stretched", "RightHand.Little.2 Stretched"}, {"Right Little 3 Stretched", "RightHand.Little.3 Stretched"}, }; } /// /// モーションデータの中身 /// [System.Serializable] public class HumanoidPoses : ScriptableObject { [SerializeField] public string AvatarName = ""; // 아바타 이름 저장 [SerializeField, Tooltip("T-포즈 데이터 (별도 저장)")] public SerializeHumanoidPose TPoseData = null; [SerializeField, Tooltip("T-포즈가 저장되었는지 여부")] public bool HasTPoseData = false; [SerializeField] public string SessionID = ""; // 세션 ID 저장 [SerializeField] public string InstanceID = ""; // 인스턴스 ID 저장 #if UNITY_EDITOR // 세션 ID를 가져오는 메서드 (다중 인스턴스 지원) private string GetSessionID() { // 1. 이미 저장된 세션 ID가 있으면 사용 if (!string.IsNullOrEmpty(SessionID)) { Debug.Log($"저장된 세션 ID 사용: {SessionID}"); return SessionID; } // 2. 스크립터블 오브젝트의 이름에서 세션 ID 추출 시도 if (!string.IsNullOrEmpty(this.name)) { // 파일명에서 세션 ID 패턴 찾기 (예: 250717_192404_Motion_abc12345) var nameParts = this.name.Split('_'); if (nameParts.Length >= 2) { // 첫 번째 두 부분이 날짜와 시간인지 확인 if (nameParts[0].Length == 6 && nameParts[1].Length == 6) // yyMMdd_HHmmss 형식 { string sessionID = $"{nameParts[0]}_{nameParts[1]}"; Debug.Log($"스크립터블 오브젝트 이름에서 세션 ID 추출: {sessionID}"); return sessionID; } } } // 3. 현재 에셋 파일 경로에서 세션 ID 추출 시도 string assetPath = AssetDatabase.GetAssetPath(this); if (!string.IsNullOrEmpty(assetPath)) { string fileName = Path.GetFileNameWithoutExtension(assetPath); var nameParts = fileName.Split('_'); if (nameParts.Length >= 2) { if (nameParts[0].Length == 6 && nameParts[1].Length == 6) // yyMMdd_HHmmss 형식 { string sessionID = $"{nameParts[0]}_{nameParts[1]}"; Debug.Log($"에셋 파일명에서 세션 ID 추출: {sessionID}"); return sessionID; } } } // 4. 마지막 수단으로 현재 시간 사용 (경고 메시지와 함께) string fallbackSessionID = DateTime.Now.ToString("yyMMdd_HHmmss"); Debug.LogWarning($"세션 ID를 찾을 수 없어 현재 시간 사용: {fallbackSessionID}"); return fallbackSessionID; } #endif #if UNITY_EDITOR //Genericなanimファイルとして出力する [ContextMenu("Export as Generic animation clips")] public void ExportGenericAnim() { var clip = new AnimationClip { frameRate = 30 }; AnimationUtility.SetAnimationClipSettings(clip, new AnimationClipSettings { loopTime = false }); // 본 데이터가 있는지 확인 if (Poses.Count == 0 || Poses[0].HumanoidBones.Count == 0) { Debug.LogError("ExportGenericAnim: 본 데이터가 없습니다. Poses.Count=" + Poses.Count + (Poses.Count > 0 ? ", HumanoidBones.Count=" + Poses[0].HumanoidBones.Count : "")); return; } Debug.Log($"ExportGenericAnim: 총 포즈 수 = {Poses.Count}"); Debug.Log($"첫 번째 포즈: 시간={Poses[0].Time}, 프레임={Poses[0].FrameCount}"); if (Poses.Count > 1) { Debug.Log($"마지막 포즈: 시간={Poses[Poses.Count-1].Time}, 프레임={Poses[Poses.Count-1].FrameCount}"); } // T-포즈 데이터 확인 if (HasTPoseData && TPoseData != null) { Debug.Log($"T-포즈 데이터 발견: 시간={TPoseData.Time}, 프레임={TPoseData.FrameCount}"); } var bones = Poses[0].HumanoidBones; for (int i = 0; i < bones.Count; i++) { var bone = bones[i]; // 경로가 비어있는지 확인 if (string.IsNullOrEmpty(bone.Name)) { Debug.LogError($"본 {i}: 이름이 비어있습니다!"); continue; } // 경로 정리: 끝의 슬래시만 제거 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(); int processedPoses = 0; // T-포즈가 있으면 0프레임에 먼저 추가 if (HasTPoseData && TPoseData != null && TPoseData.HumanoidBones.Count > i) { var tPoseBone = TPoseData.HumanoidBones[i]; positionCurveX.AddKey(TPoseData.Time, tPoseBone.LocalPosition.x); positionCurveY.AddKey(TPoseData.Time, tPoseBone.LocalPosition.y); positionCurveZ.AddKey(TPoseData.Time, tPoseBone.LocalPosition.z); rotationCurveX.AddKey(TPoseData.Time, tPoseBone.LocalRotation.x); rotationCurveY.AddKey(TPoseData.Time, tPoseBone.LocalRotation.y); rotationCurveZ.AddKey(TPoseData.Time, tPoseBone.LocalRotation.z); rotationCurveW.AddKey(TPoseData.Time, tPoseBone.LocalRotation.w); processedPoses++; Debug.Log($"T-포즈 키프레임 추가: {cleanPath} (시간: {TPoseData.Time})"); } // 실제 녹화된 포즈들 추가 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); processedPoses++; } } Debug.Log($"본 '{cleanPath}': {processedPoses}개 포즈 처리됨 (T-포즈 포함)"); //path는 계층 //http://mebiustos.hatenablog.com/entry/2015/09/16/230000 var binding = new EditorCurveBinding { path = cleanPath, type = typeof(Transform), propertyName = "m_LocalPosition.x" }; AnimationUtility.SetEditorCurve(clip, binding, 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(); // 세션 ID 사용 (MotionDataRecorder와 동일한 세션 ID 사용) string sessionID = GetSessionID(); // 아바타 이름이 있으면 포함, 없으면 기본값 사용 string avatarName = !string.IsNullOrEmpty(AvatarName) ? AvatarName : "Unknown"; // 인스턴스 ID 디버깅 로그 Debug.Log($"출력 시 인스턴스 ID 상태: '{InstanceID}' (비어있음: {string.IsNullOrEmpty(InstanceID)})"); // 인스턴스 ID가 비어있으면 현재 씬의 SavePathManager에서 가져오기 string currentInstanceID = InstanceID; if (string.IsNullOrEmpty(currentInstanceID)) { var savePathManager = FindObjectOfType(); if (savePathManager != null) { currentInstanceID = savePathManager.InstanceID; Debug.Log($"SavePathManager에서 인스턴스 ID 가져옴: '{currentInstanceID}'"); } else { Debug.LogWarning("SavePathManager를 찾을 수 없습니다. 인스턴스 ID 없이 파일 생성됩니다."); } } // 인스턴스 ID 포함 파일명 생성 string fileName = !string.IsNullOrEmpty(currentInstanceID) ? $"{sessionID}_{avatarName}_Generic_{currentInstanceID}.anim" : $"{sessionID}_{avatarName}_Generic.anim"; // 에셋 파일의 경로를 기반으로 저장 경로 결정 string savePath = "Assets/Resources"; // 기본값 // 현재 에셋 파일의 경로 가져오기 string 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); AssetDatabase.CreateAsset(clip, uniqueAssetPath); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log($"제네릭 애니메이션 파일이 저장되었습니다: {uniqueAssetPath}"); Debug.Log($"클립 길이: {clip.length}초, 총 키프레임 수: {clip.frameRate * clip.length}"); } //Humanoidなanimファイルとして出力する。 [ContextMenu("Export as Humanoid animation clips")] public void ExportHumanoidAnim() { // 데이터 검증 if (Poses == null || Poses.Count == 0) { Debug.LogError("ExportHumanoidAnim: Poses 데이터가 없습니다. Poses.Count=" + (Poses?.Count ?? 0)); return; } Debug.Log($"ExportHumanoidAnim: 총 포즈 수 = {Poses.Count}"); Debug.Log($"첫 번째 포즈: 시간={Poses[0].Time}, 프레임={Poses[0].FrameCount}"); if (Poses.Count > 1) { Debug.Log($"마지막 포즈: 시간={Poses[Poses.Count-1].Time}, 프레임={Poses[Poses.Count-1].FrameCount}"); } // T-포즈 데이터 확인 if (HasTPoseData && TPoseData != null) { Debug.Log($"T-포즈 데이터 발견: 시간={TPoseData.Time}, 프레임={TPoseData.FrameCount}"); } // 첫 번째 포즈 데이터 검증 var firstPose = Poses[0]; Debug.Log($"첫 번째 포즈: BodyPosition={firstPose.BodyPosition}, Muscles.Length={firstPose.Muscles?.Length ?? 0}"); if (firstPose.Muscles == null || firstPose.Muscles.Length == 0) { Debug.LogError("ExportHumanoidAnim: Muscles 데이터가 없습니다."); return; } var clip = new AnimationClip { frameRate = 30 }; // 시작할 때 설정을 적용 (커브 데이터 추가 전) var settings = new AnimationClipSettings { loopTime = false, // Loop Time: false cycleOffset = 0, // Cycle Offset: 0 loopBlend = false, // Loop Blend: false loopBlendOrientation = true, // Root Transform Rotation - Bake Into Pose: true loopBlendPositionY = true, // Root Transform Position (Y) - Bake Into Pose: true loopBlendPositionXZ = true, // Root Transform Position (XZ) - Bake Into Pose: true keepOriginalOrientation = true, // Root Transform Rotation - Based Upon: Original keepOriginalPositionY = true, // Root Transform Position (Y) - Based Upon: Original keepOriginalPositionXZ = true, // Root Transform Position (XZ) - Based Upon: Original heightFromFeet = false, // Height From Feet: false mirror = false // Mirror: false }; AnimationUtility.SetAnimationClipSettings(clip, settings); // body position { var curveX = new AnimationCurve(); var curveY = new AnimationCurve(); var curveZ = new AnimationCurve(); int processedPoses = 0; // T-포즈가 있으면 0프레임에 먼저 추가 if (HasTPoseData && TPoseData != null) { curveX.AddKey(TPoseData.Time, TPoseData.BodyPosition.x); curveY.AddKey(TPoseData.Time, TPoseData.BodyPosition.y); curveZ.AddKey(TPoseData.Time, TPoseData.BodyPosition.z); processedPoses++; Debug.Log($"T-포즈 Body Position 키프레임 추가 (시간: {TPoseData.Time})"); } // 실제 녹화된 포즈들 추가 foreach (var item in Poses) { curveX.AddKey(item.Time, item.BodyPosition.x); curveY.AddKey(item.Time, item.BodyPosition.y); curveZ.AddKey(item.Time, item.BodyPosition.z); processedPoses++; } Debug.Log($"Body Position 커브: 키 개수 X={curveX.length}, Y={curveY.length}, Z={curveZ.length} (처리된 포즈: {processedPoses}개)"); const string muscleX = "RootT.x"; clip.SetCurve("", typeof(Animator), muscleX, curveX); const string muscleY = "RootT.y"; clip.SetCurve("", typeof(Animator), muscleY, curveY); const string muscleZ = "RootT.z"; clip.SetCurve("", typeof(Animator), muscleZ, curveZ); } // Leftfoot position { var curveX = new AnimationCurve(); var curveY = new AnimationCurve(); var curveZ = new AnimationCurve(); int processedPoses = 0; // T-포즈가 있으면 0프레임에 먼저 추가 if (HasTPoseData && TPoseData != null) { curveX.AddKey(TPoseData.Time, TPoseData.LeftfootIK_Pos.x); curveY.AddKey(TPoseData.Time, TPoseData.LeftfootIK_Pos.y); curveZ.AddKey(TPoseData.Time, TPoseData.LeftfootIK_Pos.z); processedPoses++; Debug.Log($"T-포즈 Leftfoot Position 키프레임 추가 (시간: {TPoseData.Time})"); } // 실제 녹화된 포즈들 추가 foreach (var item in Poses) { curveX.AddKey(item.Time, item.LeftfootIK_Pos.x); curveY.AddKey(item.Time, item.LeftfootIK_Pos.y); curveZ.AddKey(item.Time, item.LeftfootIK_Pos.z); processedPoses++; } Debug.Log($"Leftfoot Position 커브: 키 개수 X={curveX.length}, Y={curveY.length}, Z={curveZ.length} (처리된 포즈: {processedPoses}개)"); const string muscleX = "LeftFootT.x"; clip.SetCurve("", typeof(Animator), muscleX, curveX); const string muscleY = "LeftFootT.y"; clip.SetCurve("", typeof(Animator), muscleY, curveY); const string muscleZ = "LeftFootT.z"; clip.SetCurve("", typeof(Animator), muscleZ, curveZ); } // Rightfoot position { var curveX = new AnimationCurve(); var curveY = new AnimationCurve(); var curveZ = new AnimationCurve(); int processedPoses = 0; // T-포즈가 있으면 0프레임에 먼저 추가 if (HasTPoseData && TPoseData != null) { curveX.AddKey(TPoseData.Time, TPoseData.RightfootIK_Pos.x); curveY.AddKey(TPoseData.Time, TPoseData.RightfootIK_Pos.y); curveZ.AddKey(TPoseData.Time, TPoseData.RightfootIK_Pos.z); processedPoses++; Debug.Log($"T-포즈 Rightfoot Position 키프레임 추가 (시간: {TPoseData.Time})"); } // 실제 녹화된 포즈들 추가 foreach (var item in Poses) { curveX.AddKey(item.Time, item.RightfootIK_Pos.x); curveY.AddKey(item.Time, item.RightfootIK_Pos.y); curveZ.AddKey(item.Time, item.RightfootIK_Pos.z); processedPoses++; } Debug.Log($"Rightfoot Position 커브: 키 개수 X={curveX.length}, Y={curveY.length}, Z={curveZ.length} (처리된 포즈: {processedPoses}개)"); const string muscleX = "RightFootT.x"; clip.SetCurve("", typeof(Animator), muscleX, curveX); const string muscleY = "RightFootT.y"; clip.SetCurve("", typeof(Animator), muscleY, curveY); const string muscleZ = "RightFootT.z"; clip.SetCurve("", typeof(Animator), muscleZ, curveZ); } // body rotation { var curveX = new AnimationCurve(); var curveY = new AnimationCurve(); var curveZ = new AnimationCurve(); var curveW = new AnimationCurve(); int processedPoses = 0; // T-포즈가 있으면 0프레임에 먼저 추가 if (HasTPoseData && TPoseData != null) { curveX.AddKey(TPoseData.Time, TPoseData.BodyRotation.x); curveY.AddKey(TPoseData.Time, TPoseData.BodyRotation.y); curveZ.AddKey(TPoseData.Time, TPoseData.BodyRotation.z); curveW.AddKey(TPoseData.Time, TPoseData.BodyRotation.w); processedPoses++; Debug.Log($"T-포즈 Body Rotation 키프레임 추가 (시간: {TPoseData.Time})"); } // 실제 녹화된 포즈들 추가 foreach (var item in Poses) { curveX.AddKey(item.Time, item.BodyRotation.x); curveY.AddKey(item.Time, item.BodyRotation.y); curveZ.AddKey(item.Time, item.BodyRotation.z); curveW.AddKey(item.Time, item.BodyRotation.w); processedPoses++; } Debug.Log($"Body Rotation 커브: 키 개수 X={curveX.length}, Y={curveY.length}, Z={curveZ.length}, W={curveW.length} (처리된 포즈: {processedPoses}개)"); const string muscleX = "RootQ.x"; clip.SetCurve("", typeof(Animator), muscleX, curveX); const string muscleY = "RootQ.y"; clip.SetCurve("", typeof(Animator), muscleY, curveY); const string muscleZ = "RootQ.z"; clip.SetCurve("", typeof(Animator), muscleZ, curveZ); const string muscleW = "RootQ.w"; clip.SetCurve("", typeof(Animator), muscleW, curveW); } // Leftfoot rotation { var curveX = new AnimationCurve(); var curveY = new AnimationCurve(); var curveZ = new AnimationCurve(); var curveW = new AnimationCurve(); int processedPoses = 0; // T-포즈가 있으면 0프레임에 먼저 추가 if (HasTPoseData && TPoseData != null) { curveX.AddKey(TPoseData.Time, TPoseData.LeftfootIK_Rot.x); curveY.AddKey(TPoseData.Time, TPoseData.LeftfootIK_Rot.y); curveZ.AddKey(TPoseData.Time, TPoseData.LeftfootIK_Rot.z); curveW.AddKey(TPoseData.Time, TPoseData.LeftfootIK_Rot.w); processedPoses++; Debug.Log($"T-포즈 Leftfoot Rotation 키프레임 추가 (시간: {TPoseData.Time})"); } // 실제 녹화된 포즈들 추가 foreach (var item in Poses) { curveX.AddKey(item.Time, item.LeftfootIK_Rot.x); curveY.AddKey(item.Time, item.LeftfootIK_Rot.y); curveZ.AddKey(item.Time, item.LeftfootIK_Rot.z); curveW.AddKey(item.Time, item.LeftfootIK_Rot.w); processedPoses++; } Debug.Log($"Leftfoot Rotation 커브: 키 개수 X={curveX.length}, Y={curveY.length}, Z={curveZ.length}, W={curveW.length} (처리된 포즈: {processedPoses}개)"); const string muscleX = "LeftFootQ.x"; clip.SetCurve("", typeof(Animator), muscleX, curveX); const string muscleY = "LeftFootQ.y"; clip.SetCurve("", typeof(Animator), muscleY, curveY); const string muscleZ = "LeftFootQ.z"; clip.SetCurve("", typeof(Animator), muscleZ, curveZ); const string muscleW = "LeftFootQ.w"; clip.SetCurve("", typeof(Animator), muscleW, curveW); } // Rightfoot rotation { var curveX = new AnimationCurve(); var curveY = new AnimationCurve(); var curveZ = new AnimationCurve(); var curveW = new AnimationCurve(); int processedPoses = 0; // T-포즈가 있으면 0프레임에 먼저 추가 if (HasTPoseData && TPoseData != null) { curveX.AddKey(TPoseData.Time, TPoseData.RightfootIK_Rot.x); curveY.AddKey(TPoseData.Time, TPoseData.RightfootIK_Rot.y); curveZ.AddKey(TPoseData.Time, TPoseData.RightfootIK_Rot.z); curveW.AddKey(TPoseData.Time, TPoseData.RightfootIK_Rot.w); processedPoses++; Debug.Log($"T-포즈 Rightfoot Rotation 키프레임 추가 (시간: {TPoseData.Time})"); } // 실제 녹화된 포즈들 추가 foreach (var item in Poses) { curveX.AddKey(item.Time, item.RightfootIK_Rot.x); curveY.AddKey(item.Time, item.RightfootIK_Rot.y); curveZ.AddKey(item.Time, item.RightfootIK_Rot.z); curveW.AddKey(item.Time, item.RightfootIK_Rot.w); processedPoses++; } Debug.Log($"Rightfoot Rotation 커브: 키 개수 X={curveX.length}, Y={curveY.length}, Z={curveZ.length}, W={curveW.length} (처리된 포즈: {processedPoses}개)"); const string muscleX = "RightFootQ.x"; clip.SetCurve("", typeof(Animator), muscleX, curveX); const string muscleY = "RightFootQ.y"; clip.SetCurve("", typeof(Animator), muscleY, curveY); const string muscleZ = "RightFootQ.z"; clip.SetCurve("", typeof(Animator), muscleZ, curveZ); const string muscleW = "RightFootQ.w"; clip.SetCurve("", typeof(Animator), muscleW, curveW); } // muscles for (int i = 0; i < HumanTrait.MuscleCount; i++) { var curve = new AnimationCurve(); int processedPoses = 0; // T-포즈가 있으면 0프레임에 먼저 추가 if (HasTPoseData && TPoseData != null && TPoseData.Muscles != null && TPoseData.Muscles.Length > i) { curve.AddKey(TPoseData.Time, TPoseData.Muscles[i]); processedPoses++; Debug.Log($"T-포즈 Muscle {i} 키프레임 추가 (시간: {TPoseData.Time})"); } // 실제 녹화된 포즈들 추가 foreach (var item in Poses) { curve.AddKey(item.Time, item.Muscles[i]); processedPoses++; } Debug.Log($"Muscle {i} 커브: 키 개수 {curve.length} (처리된 포즈: {processedPoses}개)"); var muscle = HumanTrait.MuscleName[i]; if (MotionDataSettings.TraitPropMap.ContainsKey(muscle)) { muscle = MotionDataSettings.TraitPropMap[muscle]; } clip.SetCurve("", typeof(Animator), muscle, curve); } clip.EnsureQuaternionContinuity(); // 세션 ID 사용 (MotionDataRecorder와 동일한 세션 ID 사용) string sessionID = GetSessionID(); // 아바타 이름이 있으면 포함, 없으면 기본값 사용 string avatarName = !string.IsNullOrEmpty(AvatarName) ? AvatarName : "Unknown"; // 인스턴스 ID 디버깅 로그 Debug.Log($"Humanoid 출력 시 인스턴스 ID 상태: '{InstanceID}' (비어있음: {string.IsNullOrEmpty(InstanceID)})"); // 인스턴스 ID가 비어있으면 현재 씬의 SavePathManager에서 가져오기 string currentInstanceID = InstanceID; if (string.IsNullOrEmpty(currentInstanceID)) { var savePathManager = FindObjectOfType(); if (savePathManager != null) { currentInstanceID = savePathManager.InstanceID; Debug.Log($"SavePathManager에서 인스턴스 ID 가져옴: '{currentInstanceID}'"); } else { Debug.LogWarning("SavePathManager를 찾을 수 없습니다. 인스턴스 ID 없이 파일 생성됩니다."); } } // 인스턴스 ID 포함 파일명 생성 string fileName = !string.IsNullOrEmpty(currentInstanceID) ? $"{sessionID}_{avatarName}_Humanoid_{currentInstanceID}.anim" : $"{sessionID}_{avatarName}_Humanoid.anim"; // 에셋 파일의 경로를 기반으로 저장 경로 결정 string savePath = "Assets/Resources"; // 기본값 // 현재 에셋 파일의 경로 가져오기 string 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); AssetDatabase.CreateAsset(clip, uniqueAssetPath); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log($"휴머노이드 애니메이션 파일이 저장되었습니다: {uniqueAssetPath}"); } // ASCII 형식으로 FBX 내보내기 [ContextMenu("Export as FBX animation (ASCII)")] public void ExportFBXAscii() { ExportFBXWithEncoding(true); } // 바이너리 형식으로 FBX 내보내기 [ContextMenu("Export as FBX animation (Binary)")] public void ExportFBXBinary() { ExportFBXWithEncoding(false); } // FBX 내보내기 (인코딩 설정 포함) private void ExportFBXWithEncoding(bool useAscii) { // 데이터 검증 if (Poses == null || Poses.Count == 0) { Debug.LogError("ExportFBX: Poses 데이터가 없습니다. Poses.Count=" + (Poses?.Count ?? 0)); return; } // 본 데이터가 있는지 확인 if (Poses.Count == 0 || Poses[0].HumanoidBones.Count == 0) { Debug.LogError("ExportFBX: 본 데이터가 없습니다."); return; } // 세션 ID 사용 string sessionID = GetSessionID(); string avatarName = !string.IsNullOrEmpty(AvatarName) ? AvatarName : "Unknown"; // 인스턴스 ID가 비어있으면 현재 씬의 SavePathManager에서 가져오기 string currentInstanceID = InstanceID; if (string.IsNullOrEmpty(currentInstanceID)) { var savePathManager = FindObjectOfType(); if (savePathManager != null) { currentInstanceID = savePathManager.InstanceID; Debug.Log($"FBX 출력 시 SavePathManager에서 인스턴스 ID 가져옴: '{currentInstanceID}'"); } else { Debug.LogWarning("SavePathManager를 찾을 수 없습니다. 인스턴스 ID 없이 FBX 파일 생성됩니다."); } } // 저장 경로 결정 string savePath = "Assets/Resources"; // 인스턴스 ID 포함 파일명 생성 string fileName = !string.IsNullOrEmpty(currentInstanceID) ? $"{sessionID}_{avatarName}_Motion_{(useAscii ? "ASCII" : "Binary")}_{currentInstanceID}.fbx" : $"{sessionID}_{avatarName}_Motion_{(useAscii ? "ASCII" : "Binary")}.fbx"; // 현재 에셋 파일의 경로 가져오기 string assetPath = AssetDatabase.GetAssetPath(this); 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); // 스켈레톤 생성 후 FBX 내보내기 (인코딩 설정 포함) ExportSkeletonWithAnimationToFBX(uniqueAssetPath, useAscii); Debug.Log($"FBX 애니메이션 파일이 저장되었습니다: {uniqueAssetPath}"); } // 제네릭 애니메이션 클립 생성 (내부 메서드) private AnimationClip CreateGenericAnimationClip() { var clip = new AnimationClip { frameRate = 30 }; // 클립 이름 설정 (중요!) string sessionID = GetSessionID(); clip.name = $"{sessionID}_Motion"; AnimationUtility.SetAnimationClipSettings(clip, new AnimationClipSettings { loopTime = false }); // 본 데이터가 있는지 확인 if (Poses.Count == 0 || Poses[0].HumanoidBones.Count == 0) { Debug.LogError("CreateGenericAnimationClip: 본 데이터가 없습니다. Poses.Count=" + Poses.Count + (Poses.Count > 0 ? ", HumanoidBones.Count=" + Poses[0].HumanoidBones.Count : "")); return null; } var bones = Poses[0].HumanoidBones; for (int i = 0; i < bones.Count; i++) { var bone = bones[i]; // 경로가 비어있는지 확인 if (string.IsNullOrEmpty(bone.Name)) { Debug.LogError($"본 {i}: 이름이 비어있습니다!"); 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('/'); 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); } } //path는 계층 //http://mebiustos.hatenablog.com/entry/2015/09/16/230000 var binding = new EditorCurveBinding { path = cleanPath, type = typeof(Transform), propertyName = "m_LocalPosition.x" }; AnimationUtility.SetEditorCurve(clip, binding, 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; } // 애니메이션 클립 생성 (내부 메서드) private AnimationClip CreateAnimationClip() { var clip = new AnimationClip { frameRate = 30 }; // 클립 이름 설정 (중요!) string sessionID = GetSessionID(); clip.name = $"{sessionID}_Motion"; // 애니메이션 설정 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("CreateAnimationClip: 본 데이터가 없습니다."); return null; } var bones = Poses[0].HumanoidBones; // 각 본에 대한 애니메이션 커브 생성 for (int i = 0; i < bones.Count; i++) { var bone = bones[i]; if (string.IsNullOrEmpty(bone.Name)) { Debug.LogError($"본 {i}: 이름이 비어있습니다!"); continue; } 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) { // EditorApplication.delayCall을 사용하여 다음 프레임에서 실행 EditorApplication.delayCall += () => ExportSkeletonWithAnimationToFBXStepByStep(fbxPath, useAscii); } private void ExportSkeletonWithAnimationToFBXStepByStep(string fbxPath, bool useAscii = true) { try { #if UNITY_EDITOR Debug.Log("FBX 내보내기 시작..."); // 1단계: 제네릭 애니메이션 클립 생성 (메모리에서만) Debug.Log("1단계: 제네릭 애니메이션 클립 생성 중..."); var genericClip = CreateGenericAnimationClip(); if (genericClip == null) { Debug.LogError("제네릭 애니메이션 클립 생성에 실패했습니다."); return; } Debug.Log($"제네릭 애니메이션 클립 생성 완료: {genericClip.name} (길이: {genericClip.length}초)"); // 2단계: 스켈레톤 생성 Debug.Log("2단계: 스켈레톤 생성 중..."); var skeletonRoot = CreateSkeletonFromBoneData(); if (skeletonRoot == null) { Debug.LogError("스켈레톤 생성에 실패했습니다."); return; } Debug.Log($"스켈레톤 생성 성공: {skeletonRoot.name} (자식 수: {skeletonRoot.transform.childCount})"); // 3단계: Animator 컴포넌트 추가 및 생성된 클립 연결 Debug.Log("3단계: Animator 컴포넌트 설정 중..."); var animatorComponent = AnimationHelper.SetupAnimatorComponent(skeletonRoot, genericClip); 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단계: FBX 내보내기 중..."); bool exportSuccess = ExportSkeletonWithAnimationUsingFBXExporter(skeletonRoot, fbxPath, useAscii); if (exportSuccess) { Debug.Log($"✅ FBX 내보내기 성공: {fbxPath}"); // 6단계: FBX 파일 설정 조정 Debug.Log("6단계: FBX 설정 조정 중..."); AnimationHelper.AdjustFBXImporterSettings(fbxPath); } else { Debug.LogError("❌ FBX 내보내기에 실패했습니다."); } // 7단계: 정리 (메모리에서 클립 언로드) Debug.Log("7단계: 정리 중..."); DestroyImmediate(skeletonRoot); // 메모리에서 애니메이션 클립 정리 if (genericClip != null) { Debug.Log($"메모리에서 애니메이션 클립 정리: {genericClip.name}"); // Unity가 자동으로 메모리에서 언로드하도록 함 } // 가비지 컬렉션 강제 실행 (선택사항) System.GC.Collect(); Debug.Log("✅ FBX 내보내기 완료!"); # endif } catch (System.Exception e) { Debug.LogError($"FBX 내보내기 실패: {e.Message}\n{e.StackTrace}"); } } // 애니메이션 관련 헬퍼 클래스 (이름 충돌 방지) private static class AnimationHelper { /// /// 스켈레톤에 Animator 컴포넌트를 설정합니다. (Generic 애니메이션) /// public static Animator SetupAnimatorComponent(GameObject skeletonRoot, AnimationClip clip) { try { // Animator 컴포넌트 추가 var animatorComponent = skeletonRoot.GetComponent(); if (animatorComponent == null) { animatorComponent = skeletonRoot.AddComponent(); Debug.Log("Animator 컴포넌트 추가됨"); } // Avatar 설정 (Generic - Avatar 없음) animatorComponent.avatar = null; Debug.Log("Generic 애니메이션 설정됨 (Avatar 없음)"); // RuntimeAnimatorController 생성 및 클립 추가 var controller = new UnityEditor.Animations.AnimatorController(); controller.name = $"{clip.name}_Controller"; // 기본 레이어 생성 var layer = new UnityEditor.Animations.AnimatorControllerLayer(); layer.name = "Base Layer"; layer.defaultWeight = 1.0f; // 상태 머신 생성 var stateMachine = new UnityEditor.Animations.AnimatorStateMachine(); layer.stateMachine = stateMachine; // 애니메이션 상태 생성 및 클립 할당 var animationState = stateMachine.AddState(clip.name); animationState.motion = clip; // 컨트롤러에 레이어 추가 controller.AddLayer(layer); // Animator에 컨트롤러 할당 animatorComponent.runtimeAnimatorController = controller; // Unity가 변경사항을 인식하도록 강제 업데이트 EditorUtility.SetDirty(animatorComponent); EditorUtility.SetDirty(controller); Debug.Log($"Animator 컴포넌트 설정 완료 (Generic): {clip.name}"); Debug.Log($"- 클립 이름: {clip.name}"); Debug.Log($"- 클립 길이: {clip.length}초"); Debug.Log($"- 클립 프레임레이트: {clip.frameRate}"); Debug.Log($"- 컨트롤러 이름: {controller.name}"); Debug.Log($"- Avatar: 없음 (Generic 애니메이션)"); return animatorComponent; } catch (System.Exception e) { Debug.LogError($"Animator 컴포넌트 설정 실패: {e.Message}"); return null; } } /// /// 스켈레톤에 애니메이션 컴포넌트를 설정합니다. /// public static UnityEngine.Animation SetupAnimationComponent(GameObject skeletonRoot, AnimationClip clip) { try { // Animation 컴포넌트 추가 var animationComponent = skeletonRoot.GetComponent(); if (animationComponent == null) { animationComponent = skeletonRoot.AddComponent(); Debug.Log("Animation 컴포넌트 추가됨"); } // 클립을 Animations 배열에 추가 (FBX 내보내기용) animationComponent.AddClip(clip, clip.name); // 메인 클립으로도 설정 animationComponent.clip = clip; // Animations 배열에 직접 추가하는 방법도 시도 var animations = new AnimationClip[1]; animations[0] = clip; // SerializedObject를 사용하여 Animations 배열에 직접 설정 var serializedObject = new SerializedObject(animationComponent); var animationsProperty = serializedObject.FindProperty("m_Animations"); if (animationsProperty != null) { animationsProperty.ClearArray(); animationsProperty.arraySize = 1; var element = animationsProperty.GetArrayElementAtIndex(0); element.objectReferenceValue = clip; serializedObject.ApplyModifiedProperties(); Debug.Log("SerializedObject를 통해 Animations 배열에 클립 추가됨"); } // Unity가 변경사항을 인식하도록 강제 업데이트 EditorUtility.SetDirty(animationComponent); Debug.Log($"애니메이션 컴포넌트 설정 완료: {clip.name}"); Debug.Log($"- 클립 이름: {clip.name}"); Debug.Log($"- 클립 길이: {clip.length}초"); Debug.Log($"- 클립 프레임레이트: {clip.frameRate}"); Debug.Log($"- Animations 배열 크기: {animationComponent.GetClipCount()}"); return animationComponent; } catch (System.Exception e) { Debug.LogError($"애니메이션 컴포넌트 설정 실패: {e.Message}"); return null; } } /// /// FBX 내보내기를 위한 설정을 조정합니다. /// public static void AdjustFBXImporterSettings(string fbxPath) { try { var importer = AssetImporter.GetAtPath(fbxPath) as ModelImporter; if (importer != null) { Debug.Log("FBX 파일 설정 조정 중..."); // 애니메이션 설정 importer.importAnimation = true; 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) { string clipName = Path.GetFileNameWithoutExtension(fbxPath); clipSettings[0].name = clipName; clipSettings[0].loopTime = false; clipSettings[0].lockRootRotation = false; clipSettings[0].lockRootHeightY = false; clipSettings[0].lockRootPositionXZ = false; clipSettings[0].keepOriginalOrientation = true; clipSettings[0].keepOriginalPositionY = true; clipSettings[0].keepOriginalPositionXZ = true; clipSettings[0].heightFromFeet = false; clipSettings[0].mirror = false; importer.clipAnimations = clipSettings; } // 변경사항 저장 importer.SaveAndReimport(); Debug.Log("FBX 파일 설정 조정 완료"); } else { Debug.LogWarning("FBX 파일의 ModelImporter를 찾을 수 없습니다."); } } catch (System.Exception e) { Debug.LogError($"FBX 설정 조정 중 오류: {e.Message}"); } } /// /// Animator 컴포넌트의 상태를 상세히 출력합니다. /// public static void DebugAnimatorComponent(Animator animatorComponent) { if (animatorComponent == null) { Debug.LogError("Animator 컴포넌트가 null입니다."); return; } Debug.Log("=== Animator 컴포넌트 디버그 정보 ==="); Debug.Log($"- 컴포넌트 활성화: {animatorComponent.enabled}"); Debug.Log($"- Avatar: {animatorComponent.avatar?.name ?? "없음"}"); Debug.Log($"- RuntimeAnimatorController: {animatorComponent.runtimeAnimatorController?.name ?? "없음"}"); Debug.Log($"- Culling Mode: {animatorComponent.cullingMode}"); Debug.Log($"- Update Mode: {animatorComponent.updateMode}"); // 컨트롤러 정보 출력 if (animatorComponent.runtimeAnimatorController != null) { var controller = animatorComponent.runtimeAnimatorController; Debug.Log($" 컨트롤러: {controller.name}"); Debug.Log($" 컨트롤러 타입: {controller.GetType().Name}"); } // 현재 상태 정보 출력 Debug.Log($"- 현재 상태: {animatorComponent.GetCurrentAnimatorStateInfo(0).IsName("")}"); Debug.Log($"- 애니메이션 길이: {animatorComponent.GetCurrentAnimatorStateInfo(0).length}초"); Debug.Log("=== 디버그 정보 끝 ==="); } /// /// 애니메이션 컴포넌트의 상태를 상세히 출력합니다. /// public static void DebugAnimationComponent(UnityEngine.Animation animationComponent) { if (animationComponent == null) { Debug.LogError("애니메이션 컴포넌트가 null입니다."); return; } Debug.Log("=== 애니메이션 컴포넌트 디버그 정보 ==="); Debug.Log($"- 컴포넌트 활성화: {animationComponent.enabled}"); Debug.Log($"- 메인 클립: {animationComponent.clip?.name ?? "없음"}"); Debug.Log($"- 자동 재생: {animationComponent.playAutomatically}"); Debug.Log($"- 래핑 모드: {animationComponent.wrapMode}"); // 메인 클립 정보 출력 if (animationComponent.clip != null) { var clip = animationComponent.clip; Debug.Log($" 메인 클립: {clip.name} (길이: {clip.length}초, 프레임레이트: {clip.frameRate})"); // 클립의 커브 정보 출력 var bindings = AnimationUtility.GetCurveBindings(clip); Debug.Log($" 커브 바인딩 수: {bindings.Length}"); for (int j = 0; j < Math.Min(bindings.Length, 5); j++) // 처음 5개만 출력 { var binding = bindings[j]; Debug.Log($" 바인딩 {j}: {binding.path}.{binding.propertyName}"); } } Debug.Log("=== 디버그 정보 끝 ==="); } } // Unity FBX Exporter 패키지를 사용한 내보내기 (스켈레톤만) private bool ExportSkeletonWithAnimationUsingFBXExporter(GameObject skeletonRoot, string fbxPath, bool useAscii) { try { // FBX Exporter 패키지 확인 var modelExporterType = System.Type.GetType("UnityEditor.Formats.Fbx.Exporter.ModelExporter, Unity.Formats.Fbx.Editor"); if (modelExporterType == null) { Debug.LogError("Unity FBX Exporter 패키지가 설치되지 않았습니다."); Debug.LogError("Package Manager에서 'FBX Exporter' 패키지를 설치해주세요."); return false; } Debug.Log("Unity FBX Exporter 패키지 발견"); // ExportModelOptions 타입 가져오기 var exportModelOptionsType = System.Type.GetType("UnityEditor.Formats.Fbx.Exporter.ExportModelOptions, Unity.Formats.Fbx.Editor"); if (exportModelOptionsType == null) { Debug.LogError("ExportModelOptions 타입을 찾을 수 없습니다."); return false; } // ExportObjects 메서드 찾기 var exportObjectsMethod = modelExporterType.GetMethod("ExportObjects", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static, null, new System.Type[] { typeof(string), typeof(UnityEngine.Object[]), exportModelOptionsType }, null); if (exportObjectsMethod == null) { Debug.LogError("ModelExporter.ExportObjects 메서드를 찾을 수 없습니다."); return false; } // ExportModelOptions 생성 및 설정 var exportOptions = System.Activator.CreateInstance(exportModelOptionsType); Debug.Log("ExportModelOptions 인스턴스 생성됨"); // ASCII/Binary 설정 - ExportFormat 속성 사용 var exportFormatProperty = exportModelOptionsType.GetProperty("ExportFormat"); if (exportFormatProperty != null) { try { // ExportFormat enum 값을 가져와서 설정 var exportFormatType = exportFormatProperty.PropertyType; Debug.Log($"ExportFormat 타입: {exportFormatType.Name}"); // enum 값들을 확인 var enumValues = System.Enum.GetValues(exportFormatType); Debug.Log("사용 가능한 ExportFormat 값들:"); foreach (var enumValue in enumValues) { Debug.Log($" - {enumValue}: {(int)enumValue}"); } // Binary와 ASCII enum 값 찾기 object targetFormat = null; foreach (var enumValue in enumValues) { string enumName = enumValue.ToString().ToLower(); if (useAscii && (enumName.Contains("ascii") || enumName.Contains("text"))) { targetFormat = enumValue; break; } else if (!useAscii && (enumName.Contains("binary") || enumName.Contains("bin"))) { targetFormat = enumValue; break; } } if (targetFormat != null) { exportFormatProperty.SetValue(exportOptions, targetFormat); Debug.Log($"FBX 형식 설정 (ExportFormat): {targetFormat} ({(useAscii ? "ASCII" : "Binary")})"); } else { Debug.LogWarning($"적절한 ExportFormat 값을 찾을 수 없습니다. useAscii={useAscii}"); } } catch (System.Exception e) { Debug.LogError($"ExportFormat 설정 실패: {e.Message}"); } } else { Debug.LogWarning("ExportFormat 속성을 찾을 수 없습니다."); } // ExportModelOptions의 모든 속성 확인 var properties = exportModelOptionsType.GetProperties(); Debug.Log("ExportModelOptions의 모든 속성:"); foreach (var property in properties) { 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("UseMayaCompatibleNames 속성을 찾을 수 없습니다."); } // 스켈레톤만 내보내기 (Animation 컴포넌트가 포함됨) var objectsToExport = new UnityEngine.Object[] { skeletonRoot }; Debug.Log($"내보낼 오브젝트: {objectsToExport.Length}개"); Debug.Log($"1. 스켈레톤 (Animation 컴포넌트 포함): {skeletonRoot.name}"); // 스켈레톤의 컴포넌트 확인 var components = skeletonRoot.GetComponents(); Debug.Log($"스켈레톤의 컴포넌트 수: {components.Length}"); foreach (var component in components) { Debug.Log($" 컴포넌트: {component.GetType().Name}"); } // FBX 내보내기 실행 try { Debug.Log("FBX 내보내기 메서드 호출 시작..."); Debug.Log($"- 경로: {fbxPath}"); Debug.Log($"- 오브젝트 수: {objectsToExport.Length}"); Debug.Log($"- ASCII 모드: {useAscii}"); exportObjectsMethod.Invoke(null, new object[] { fbxPath, objectsToExport, exportOptions }); Debug.Log("FBX 내보내기 메서드 호출 완료"); } catch (System.Exception e) { Debug.LogError($"FBX 내보내기 중 상세 오류: {e.Message}"); Debug.LogError($"내부 오류: {e.InnerException?.Message ?? "없음"}"); Debug.LogError($"스택 트레이스: {e.StackTrace}"); throw; // 오류를 다시 던져서 상위에서 처리 } // 파일 생성 확인 if (System.IO.File.Exists(fbxPath)) { Debug.Log($"FBX 파일 생성 확인: {fbxPath}"); AssetDatabase.Refresh(); return true; } else { Debug.LogError($"FBX 파일이 생성되지 않았습니다: {fbxPath}"); return false; } } catch (System.Exception e) { Debug.LogError($"FBX Exporter 사용 중 오류: {e.Message}"); return false; } } // 본 데이터로부터 스켈레톤 생성 private GameObject CreateSkeletonFromBoneData() { if (Poses.Count == 0 || Poses[0].HumanoidBones.Count == 0) { Debug.LogError("본 데이터가 없습니다."); return null; } var firstPose = Poses[0]; var bones = firstPose.HumanoidBones; Debug.Log($"스켈레톤 생성 시작: {bones.Count}개의 본 데이터"); // 본 데이터 구조 상세 분석 Debug.Log("=== 본 데이터 구조 분석 ==="); for (int i = 0; i < Math.Min(bones.Count, 15); i++) // 처음 15개 출력 { 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("=== 분석 완료 ==="); // 본 데이터에서 첫 번째 경로 부분을 찾기 (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} (첫 번째 본: {firstBoneName})"); // 본 계층 구조 생성 var boneGameObjects = new Dictionary(); int createdBones = 0; // 루트가 본 데이터에 포함되어 있는지 확인 (더 정확한 분석) bool hasRootInData = false; string rootBoneName = ""; var rootBones = new List(); 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; rootBones.Add(bone.Name); Debug.Log($"루트 본 발견: '{rootBoneName}'"); } } if (hasRootInData) { Debug.Log($"본 데이터에 {rootBones.Count}개의 루트 본이 포함됨: {string.Join(", ", rootBones)}"); } else { Debug.Log("본 데이터에 루트 본이 포함되지 않음"); } // 본 데이터를 경로별로 정리하여 중복 제거 var uniqueBonePaths = new Dictionary(); foreach (var bone in bones) { if (string.IsNullOrEmpty(bone.Name)) continue; string cleanPath = bone.Name.TrimEnd('/'); if (!uniqueBonePaths.ContainsKey(cleanPath)) { uniqueBonePaths[cleanPath] = bone; } } Debug.Log($"중복 제거 후 고유 본 경로 수: {uniqueBonePaths.Count}"); foreach (var kvp in uniqueBonePaths) { var bonePath = kvp.Key; var bone = kvp.Value; Debug.Log($"본 처리 중: {bonePath}"); // 본 경로를 '/'로 분할하여 계층 구조 생성 var pathParts = bonePath.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)) { // 첫 번째 경로 부분이 첫 번째 본인 경우 (Bip001 등) if (i == 0 && part == firstBoneName) { // 첫 번째 본을 루트 하위에 생성 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}"); boneGameObjects[currentPath] = firstBoneGO; parent = firstBoneGO; continue; } var boneGO = new GameObject(part); boneGO.transform.SetParent(parent.transform); createdBones++; // 첫 번째 포즈의 위치와 회전 설정 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}"); } boneGameObjects[currentPath] = boneGO; parent = boneGO; } else { parent = boneGameObjects[currentPath]; } } } Debug.Log($"스켈레톤 생성 완료: {createdBones}개의 GameObject 생성됨"); // 스켈레톤 구조 출력 PrintSkeletonHierarchy(root); // Bip001 Pelvis 강제 수정 FixBip001PelvisTransform(root); // 스켈레톤이 실제로 씬에 있는지 확인 if (root != null && root.transform.childCount > 0) { Debug.Log($"✅ 스켈레톤 생성 성공: 루트={root.name}, 자식 수={root.transform.childCount}"); return root; } else { Debug.LogError("❌ 스켈레톤 생성 실패: 자식이 없음"); return null; } } // 스켈레톤 계층 구조 출력 (디버깅용) private void PrintSkeletonHierarchy(GameObject root, string indent = "") { Debug.Log($"{indent}{root.name} ({root.transform.childCount} children)"); for (int i = 0; i < root.transform.childCount; i++) { var child = root.transform.GetChild(i); PrintSkeletonHierarchy(child.gameObject, indent + " "); } } // 상대 경로 가져오기 private string GetRelativePath(Transform root, Transform target) { var pathList = new List(); var current = target; while (current != null && current != root) { pathList.Add(current.name); current = current.parent; } pathList.Reverse(); return string.Join("/", pathList); } // Bip001 Pelvis 강제 수정 메서드 private void FixBip001PelvisTransform(GameObject root) { 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] public class SerializeHumanoidPose { public Vector3 BodyRootPosition; public Quaternion BodyRootRotation; public Vector3 BodyPosition; public Quaternion BodyRotation; public Vector3 LeftfootIK_Pos; public Quaternion LeftfootIK_Rot; public Vector3 RightfootIK_Pos; public Quaternion RightfootIK_Rot; public float[] Muscles; //フレーム数 public int FrameCount; //記録開始後の経過時間。処理落ち対策 public float Time; [Serializable] public class HumanoidBone { public string Name; public Vector3 LocalPosition; public Quaternion LocalRotation; private static Dictionary _pathCache = new Dictionary(); private static string BuildRelativePath(Transform root, Transform target) { var path = ""; _pathCache.TryGetValue(target, out path); if(path != null) return path; var current = target; var pathList = new List(); // 타겟이 루트와 같은 경우 빈 문자열 반환 if (current == root) { path = ""; _pathCache.Add(target, path); return path; } // 루트까지 올라가면서 경로 구성 while (current != null && current != root) { pathList.Add(current.name); current = current.parent; } if (current == null) { Debug.LogError($"{target.name}는 {root.name}의 자식이 아닙니다."); throw new Exception(target.name + "는" + root.name + "의 자식이 아닙니다"); } // 경로를 역순으로 조합 (Unity 애니메이션 경로 형식) pathList.Reverse(); path = string.Join("/", pathList); // 경로 끝의 슬래시 제거 path = path.TrimEnd('/'); _pathCache.Add(target, path); return path; } public void Set(Transform root, Transform t) { Name = BuildRelativePath(root, t); // 루트 본인지 확인 (이름이 비어있거나 루트와 같은 경우) bool isRoot = string.IsNullOrEmpty(Name) || t == root; if (isRoot) { // 루트 본: 월드 좌표계 사용 LocalPosition = t.position; // 월드 위치 LocalRotation = t.rotation; // 월드 회전 } else { // 자식 본: 로컬 좌표계 사용 LocalPosition = t.localPosition; LocalRotation = t.localRotation; } } } [SerializeField, HideInInspector] public List HumanoidBones = new List(); //CSVシリアライズ public string SerializeCSV() { StringBuilder sb = new StringBuilder(); SerializeVector3(sb, BodyRootPosition); SerializeQuaternion(sb, BodyRootRotation); SerializeVector3(sb, BodyPosition); SerializeQuaternion(sb, BodyRotation); foreach (var muscle in Muscles) { sb.Append(muscle); sb.Append(","); } sb.Append(FrameCount); sb.Append(","); sb.Append(Time); sb.Append(","); foreach (var humanoidBone in HumanoidBones) { sb.Append(humanoidBone.Name); sb.Append(","); SerializeVector3(sb, humanoidBone.LocalPosition); SerializeQuaternion(sb, humanoidBone.LocalRotation); } sb.Length = sb.Length - 1; //最後のカンマ削除 return sb.ToString(); } private static void SerializeVector3(StringBuilder sb, Vector3 vec) { sb.Append(vec.x); sb.Append(","); sb.Append(vec.y); sb.Append(","); sb.Append(vec.z); sb.Append(","); } private static void SerializeQuaternion(StringBuilder sb, Quaternion q) { sb.Append(q.x); sb.Append(","); sb.Append(q.y); sb.Append(","); sb.Append(q.z); sb.Append(","); sb.Append(q.w); sb.Append(","); } //CSVデシリアライズ public void DeserializeCSV(string str) { string[] dataString = str.Split(','); BodyRootPosition = DeserializeVector3(dataString, 0); BodyRootRotation = DeserializeQuaternion(dataString, 3); BodyPosition = DeserializeVector3(dataString, 7); BodyRotation = DeserializeQuaternion(dataString, 10); Muscles = new float[HumanTrait.MuscleCount]; for (int i = 0; i < HumanTrait.MuscleCount; i++) { Muscles[i] = float.Parse(dataString[i + 14]); } FrameCount = int.Parse(dataString[14 + HumanTrait.MuscleCount]); Time = float.Parse(dataString[15 + HumanTrait.MuscleCount]); var boneValues = Enum.GetValues(typeof(HumanBodyBones)) as HumanBodyBones[]; for (int i = 0; i < boneValues.Length; i++) { int startIndex = 16 + HumanTrait.MuscleCount + (i * 8); if (dataString.Length <= startIndex) { break; } HumanoidBone bone = new HumanoidBone(); bone.Name = dataString[startIndex]; bone.LocalPosition = DeserializeVector3(dataString, startIndex + 1); bone.LocalRotation = DeserializeQuaternion(dataString, startIndex + 4); } } private static Vector3 DeserializeVector3(IList str, int startIndex) { return new Vector3(float.Parse(str[startIndex]), float.Parse(str[startIndex + 1]), float.Parse(str[startIndex + 2])); } private static Quaternion DeserializeQuaternion(IList str, int startIndex) { return new Quaternion(float.Parse(str[startIndex]), float.Parse(str[startIndex + 1]), float.Parse(str[startIndex + 2]), float.Parse(str[startIndex + 3])); } } [SerializeField, HideInInspector] public List Poses = new List(); // 인스펙터 최적화를 위한 요약 정보 [System.Serializable] public class SummaryInfo { public int TotalPoses; public float TotalTime; public int TotalBones; public int TotalMuscles; public float AverageFPS; } [SerializeField] public SummaryInfo Summary = new SummaryInfo(); } }