2286 lines
101 KiB
C#
2286 lines
101 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_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<EasyMotionRecorder.SavePathManager>();
|
|
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<EasyMotionRecorder.SavePathManager>();
|
|
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<EasyMotionRecorder.SavePathManager>();
|
|
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
|
|
{
|
|
/// <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();
|
|
|
|
#if UNITY_EDITOR
|
|
/// <summary>
|
|
/// 선택된 여러 HumanoidPoses 에셋에서 휴머노이드 애니메이션 일괄 출력
|
|
/// </summary>
|
|
[MenuItem("Assets/Easy Motion Recorder/Batch Export Humanoid Animations", false, 1)]
|
|
public static void BatchExportHumanoidAnimations()
|
|
{
|
|
var selectedHumanoidPoses = GetSelectedHumanoidPoses();
|
|
if (selectedHumanoidPoses.Length == 0)
|
|
{
|
|
EditorUtility.DisplayDialog("오류", "HumanoidPoses 에셋을 하나 이상 선택해주세요.", "확인");
|
|
return;
|
|
}
|
|
|
|
BatchExportProcess(selectedHumanoidPoses, "휴머노이드 애니메이션", (pose) => pose.ExportHumanoidAnim());
|
|
}
|
|
|
|
/// <summary>
|
|
/// 선택된 여러 HumanoidPoses 에셋에서 제네릭 애니메이션 일괄 출력
|
|
/// </summary>
|
|
[MenuItem("Assets/Easy Motion Recorder/Batch Export Generic Animations", false, 2)]
|
|
public static void BatchExportGenericAnimations()
|
|
{
|
|
var selectedHumanoidPoses = GetSelectedHumanoidPoses();
|
|
if (selectedHumanoidPoses.Length == 0)
|
|
{
|
|
EditorUtility.DisplayDialog("오류", "HumanoidPoses 에셋을 하나 이상 선택해주세요.", "확인");
|
|
return;
|
|
}
|
|
|
|
BatchExportProcess(selectedHumanoidPoses, "제네릭 애니메이션", (pose) => pose.ExportGenericAnim());
|
|
}
|
|
|
|
/// <summary>
|
|
/// 선택된 여러 HumanoidPoses 에셋에서 FBX ASCII 일괄 출력
|
|
/// </summary>
|
|
[MenuItem("Assets/Easy Motion Recorder/Batch Export FBX ASCII", false, 3)]
|
|
public static void BatchExportFBXAscii()
|
|
{
|
|
var selectedHumanoidPoses = GetSelectedHumanoidPoses();
|
|
if (selectedHumanoidPoses.Length == 0)
|
|
{
|
|
EditorUtility.DisplayDialog("오류", "HumanoidPoses 에셋을 하나 이상 선택해주세요.", "확인");
|
|
return;
|
|
}
|
|
|
|
BatchExportProcess(selectedHumanoidPoses, "FBX ASCII", (pose) => pose.ExportFBXAscii());
|
|
}
|
|
|
|
/// <summary>
|
|
/// 선택된 여러 HumanoidPoses 에셋에서 FBX Binary 일괄 출력
|
|
/// </summary>
|
|
[MenuItem("Assets/Easy Motion Recorder/Batch Export FBX Binary", false, 4)]
|
|
public static void BatchExportFBXBinary()
|
|
{
|
|
var selectedHumanoidPoses = GetSelectedHumanoidPoses();
|
|
if (selectedHumanoidPoses.Length == 0)
|
|
{
|
|
EditorUtility.DisplayDialog("오류", "HumanoidPoses 에셋을 하나 이상 선택해주세요.", "확인");
|
|
return;
|
|
}
|
|
|
|
BatchExportProcess(selectedHumanoidPoses, "FBX Binary", (pose) => pose.ExportFBXBinary());
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 선택된 오브젝트에서 HumanoidPoses 에셋들을 찾아 반환
|
|
/// </summary>
|
|
private static HumanoidPoses[] GetSelectedHumanoidPoses()
|
|
{
|
|
var selectedObjects = Selection.objects;
|
|
var humanoidPosesList = new List<HumanoidPoses>();
|
|
|
|
foreach (var obj in selectedObjects)
|
|
{
|
|
if (obj is HumanoidPoses humanoidPoses)
|
|
{
|
|
humanoidPosesList.Add(humanoidPoses);
|
|
}
|
|
}
|
|
|
|
return humanoidPosesList.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 일괄 출력 프로세스 실행
|
|
/// </summary>
|
|
private static void BatchExportProcess(HumanoidPoses[] poses, string exportType, System.Action<HumanoidPoses> exportAction)
|
|
{
|
|
if (poses.Length == 0) return;
|
|
|
|
bool confirmed = EditorUtility.DisplayDialog(
|
|
"일괄 출력 확인",
|
|
$"{poses.Length}개의 HumanoidPoses 에셋에서 {exportType} 파일을 출력하시겠습니까?",
|
|
"출력 시작",
|
|
"취소");
|
|
|
|
if (!confirmed) return;
|
|
|
|
int successCount = 0;
|
|
int failCount = 0;
|
|
float progress = 0f;
|
|
|
|
try
|
|
{
|
|
for (int i = 0; i < poses.Length; i++)
|
|
{
|
|
var pose = poses[i];
|
|
progress = (float)i / poses.Length;
|
|
|
|
string assetName = pose.name;
|
|
bool cancelled = EditorUtility.DisplayCancelableProgressBar(
|
|
$"{exportType} 일괄 출력 중...",
|
|
$"처리 중: {assetName} ({i + 1}/{poses.Length})",
|
|
progress);
|
|
|
|
if (cancelled)
|
|
{
|
|
EditorUtility.ClearProgressBar();
|
|
EditorUtility.DisplayDialog("취소됨", "일괄 출력이 사용자에 의해 취소되었습니다.", "확인");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
Debug.Log($"[일괄 출력] {exportType} 처리 시작: {assetName}");
|
|
exportAction(pose);
|
|
successCount++;
|
|
Debug.Log($"[일괄 출력] {exportType} 처리 완료: {assetName}");
|
|
}
|
|
catch (System.Exception e)
|
|
{
|
|
failCount++;
|
|
Debug.LogError($"[일괄 출력] {exportType} 처리 실패: {assetName} - {e.Message}");
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
EditorUtility.ClearProgressBar();
|
|
|
|
// 결과 다이얼로그 표시
|
|
string message = $"{exportType} 일괄 출력이 완료되었습니다.\n\n" +
|
|
$"성공: {successCount}개\n" +
|
|
$"실패: {failCount}개\n" +
|
|
$"총 처리: {poses.Length}개";
|
|
|
|
if (failCount > 0)
|
|
{
|
|
EditorUtility.DisplayDialog("일괄 출력 완료 (일부 실패)", message, "확인");
|
|
}
|
|
else
|
|
{
|
|
EditorUtility.DisplayDialog("일괄 출력 완료", message, "확인");
|
|
}
|
|
|
|
Debug.Log($"[일괄 출력 완료] {exportType}: 성공 {successCount}개, 실패 {failCount}개");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 메뉴 아이템 검증: HumanoidPoses 에셋이 선택되었을 때만 메뉴 활성화
|
|
/// </summary>
|
|
[MenuItem("Assets/Easy Motion Recorder/Batch Export Humanoid Animations", true)]
|
|
[MenuItem("Assets/Easy Motion Recorder/Batch Export Generic Animations", true)]
|
|
[MenuItem("Assets/Easy Motion Recorder/Batch Export FBX ASCII", true)]
|
|
[MenuItem("Assets/Easy Motion Recorder/Batch Export FBX Binary", true)]
|
|
public static bool ValidateBatchExport()
|
|
{
|
|
return GetSelectedHumanoidPoses().Length > 0;
|
|
}
|
|
#endif
|
|
}
|
|
}
|