2055 lines
91 KiB
C#

/**
[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
}
/// <summary>
/// Humanoid用のMuscleマッピング
/// </summary>
public static Dictionary<string, string> TraitPropMap = new Dictionary<string, string>
{
{"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"},
};
}
/// <summary>
/// モーションデータの中身
/// </summary>
[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_abc12345_Motion)
var nameParts = this.name.Split('_');
if (nameParts.Length >= 3)
{
// 첫 번째 두 부분이 날짜와 시간인지 확인
if (nameParts[0].Length == 6 && nameParts[1].Length == 6) // yyMMdd_HHmmss 형식
{
string sessionID = $"{nameParts[0]}_{nameParts[1]}";
if (nameParts.Length > 2)
{
sessionID += "_" + nameParts[2]; // 인스턴스 ID 포함
}
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 >= 3)
{
if (nameParts[0].Length == 6 && nameParts[1].Length == 6) // yyMMdd_HHmmss 형식
{
string sessionID = $"{nameParts[0]}_{nameParts[1]}";
if (nameParts.Length > 2)
{
sessionID += "_" + nameParts[2]; // 인스턴스 ID 포함
}
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";
// 에셋 파일의 경로를 기반으로 저장 경로 결정
string savePath = "Assets/Resources"; // 기본값
string fileName = $"{sessionID}_{avatarName}_Generic.anim";
// 현재 에셋 파일의 경로 가져오기
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";
// 에셋 파일의 경로를 기반으로 저장 경로 결정
string savePath = "Assets/Resources"; // 기본값
string fileName = $"{sessionID}_{avatarName}_Humanoid.anim";
// 현재 에셋 파일의 경로 가져오기
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";
// 저장 경로 결정
string savePath = "Assets/Resources";
string fileName = $"{sessionID}_{avatarName}_Motion.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
{
/// <summary>
/// 스켈레톤에 Animator 컴포넌트를 설정합니다. (Generic 애니메이션)
/// </summary>
public static Animator SetupAnimatorComponent(GameObject skeletonRoot, AnimationClip clip)
{
try
{
// Animator 컴포넌트 추가
var animatorComponent = skeletonRoot.GetComponent<Animator>();
if (animatorComponent == null)
{
animatorComponent = skeletonRoot.AddComponent<Animator>();
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;
}
}
/// <summary>
/// 스켈레톤에 애니메이션 컴포넌트를 설정합니다.
/// </summary>
public static UnityEngine.Animation SetupAnimationComponent(GameObject skeletonRoot, AnimationClip clip)
{
try
{
// Animation 컴포넌트 추가
var animationComponent = skeletonRoot.GetComponent<UnityEngine.Animation>();
if (animationComponent == null)
{
animationComponent = skeletonRoot.AddComponent<UnityEngine.Animation>();
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;
}
}
/// <summary>
/// FBX 내보내기를 위한 설정을 조정합니다.
/// </summary>
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}");
}
}
/// <summary>
/// Animator 컴포넌트의 상태를 상세히 출력합니다.
/// </summary>
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("=== 디버그 정보 끝 ===");
}
/// <summary>
/// 애니메이션 컴포넌트의 상태를 상세히 출력합니다.
/// </summary>
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<Component>();
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<string, GameObject>();
int createdBones = 0;
// 루트가 본 데이터에 포함되어 있는지 확인 (더 정확한 분석)
bool hasRootInData = false;
string rootBoneName = "";
var rootBones = new List<string>();
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<string, SerializeHumanoidPose.HumanoidBone>();
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<string>();
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<Transform, string> _pathCache = new Dictionary<Transform, string>();
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<string>();
// 타겟이 루트와 같은 경우 빈 문자열 반환
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<HumanoidBone> HumanoidBones = new List<HumanoidBone>();
//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<string> 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<string> 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<SerializeHumanoidPose> Poses = new List<SerializeHumanoidPose>();
// 인스펙터 최적화를 위한 요약 정보
[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();
}
}