Add : 모션 스크립트 패치

This commit is contained in:
KINDNICK 2025-07-26 14:53:11 +09:00
parent 61763d81d0
commit 9dc2d4d64f
7 changed files with 542 additions and 657 deletions

View File

@ -3,52 +3,34 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using UnityEngine; using UnityEngine;
namespace Entum namespace EasyMotionRecorder
{ {
[Serializable]
public class CharacterFacialData : ScriptableObject public class CharacterFacialData : ScriptableObject
{ {
[SerializeField]
public string SessionID = "";
[System.SerializableAttribute] [SerializeField]
public string InstanceID = "";
[SerializeField]
public List<SerializeHumanoidFace> Faces = new List<SerializeHumanoidFace>();
[Serializable]
public class SerializeHumanoidFace public class SerializeHumanoidFace
{ {
public class MeshAndBlendshape [SerializeField]
{ public List<string> BlendShapeNames = new List<string>();
public string path;
public float[] blendShapes;
}
[SerializeField]
public List<float> BlendShapeValues = new List<float>();
public int BlendShapeNum() [SerializeField]
{
return Smeshes.Count == 0 ? 0 : Smeshes.Sum(t => t.blendShapes.Length);
}
//フレーム数
public int FrameCount; public int FrameCount;
//記録開始後の経過時間。処理落ち対策 [SerializeField]
public float Time; public float Time;
public SerializeHumanoidFace(SerializeHumanoidFace serializeHumanoidFace)
{
for (int i = 0; i < serializeHumanoidFace.Smeshes.Count; i++)
{
Smeshes.Add(serializeHumanoidFace.Smeshes[i]);
Array.Copy(serializeHumanoidFace.Smeshes[i].blendShapes,Smeshes[i].blendShapes,
serializeHumanoidFace.Smeshes[i].blendShapes.Length);
}
FrameCount = serializeHumanoidFace.FrameCount;
Time = serializeHumanoidFace.Time;
}
//単一フレームの中でも、口のメッシュや目のメッシュなどが個別にここに入る
public List<MeshAndBlendshape> Smeshes= new List<MeshAndBlendshape>();
public SerializeHumanoidFace()
{
} }
} }
public List<SerializeHumanoidFace> Facials = new List<SerializeHumanoidFace>();
}
} }

View File

@ -37,8 +37,11 @@ namespace Entum {
[Tooltip("記録するFPS。0で制限しない。UpdateのFPSは超えられません。")] [Tooltip("記録するFPS。0で制限しない。UpdateのFPSは超えられません。")]
public float TargetFPS = 60.0f; public float TargetFPS = 60.0f;
private MotionDataRecorder _animRecorder; [Header("인스턴스 설정")]
[SerializeField] private string instanceID = "";
[SerializeField] private SavePathManager _savePathManager;
private MotionDataRecorder _animRecorder;
private SkinnedMeshRenderer[] _smeshs; private SkinnedMeshRenderer[] _smeshs;
@ -48,7 +51,6 @@ namespace Entum {
private int _frameCount = 0; private int _frameCount = 0;
CharacterFacialData.SerializeHumanoidFace _past = new CharacterFacialData.SerializeHumanoidFace(); CharacterFacialData.SerializeHumanoidFace _past = new CharacterFacialData.SerializeHumanoidFace();
private float _recordedTime = 0f; private float _recordedTime = 0f;
@ -59,6 +61,24 @@ namespace Entum {
_animRecorder = GetComponent<MotionDataRecorder>(); _animRecorder = GetComponent<MotionDataRecorder>();
_animRecorder.OnRecordStart += RecordStart; _animRecorder.OnRecordStart += RecordStart;
_animRecorder.OnRecordEnd += RecordEnd; _animRecorder.OnRecordEnd += RecordEnd;
// 인스턴스 ID가 비어있으면 자동 생성
if (string.IsNullOrEmpty(instanceID))
{
instanceID = System.Guid.NewGuid().ToString().Substring(0, 8);
}
// SavePathManager가 없으면 같은 GameObject에서 찾기
if (_savePathManager == null)
{
_savePathManager = GetComponent<SavePathManager>();
if (_savePathManager == null)
{
_savePathManager = gameObject.AddComponent<SavePathManager>();
_savePathManager.SetInstanceID(instanceID);
}
}
if(_animRecorder.CharacterAnimator != null) { if(_animRecorder.CharacterAnimator != null) {
_smeshs = GetSkinnedMeshRenderers(_animRecorder.CharacterAnimator); _smeshs = GetSkinnedMeshRenderers(_animRecorder.CharacterAnimator);
} }
@ -98,258 +118,186 @@ namespace Entum {
return; return;
} }
if(_recording) { _facialData = ScriptableObject.CreateInstance<CharacterFacialData>();
return; _facialData.Faces = new List<CharacterFacialData.SerializeHumanoidFace>();
} _facialData.SessionID = _animRecorder.SessionID;
_facialData.InstanceID = instanceID;
if(_smeshs.Length == 0) {
Debug.LogError("顔のメッシュ指定がされていないので顔のアニメーションは記録しません");
return;
}
Debug.Log("FaceAnimationRecorder record start");
_recording = true; _recording = true;
_frameCount = 0;
_recordedTime = 0f; _recordedTime = 0f;
_startTime = Time.time; _startTime = Time.time;
_frameCount = 0;
_facialData = ScriptableObject.CreateInstance<CharacterFacialData>(); Debug.Log($"표정 애니메이션 녹화 시작 - 인스턴스: {instanceID}");
} }
/// <summary>
/// 記録終了
/// </summary>
private void RecordEnd() { private void RecordEnd() {
if(_recordFaceBlendshapes == false) { if(_recording == false) {
return; return;
} }
if(_smeshs.Length == 0) {
Debug.LogError("顔のメッシュ指定がされていないので顔のアニメーションは記録しませんでした");
if(_recording == true) {
Debug.LogAssertion("Unexpected execution!!!!");
}
}
else {
//WriteAnimationFileToScriptableObject();
ExportFacialAnimationClip(_animRecorder.CharacterAnimator, _facialData);
}
Debug.Log("FaceAnimationRecorder record end");
_recording = false; _recording = false;
} Debug.Log($"표정 애니메이션 녹화 종료 - 인스턴스: {instanceID}, 총 프레임: {_frameCount}");
WriteAnimationFileToScriptableObject();
}
private void WriteAnimationFileToScriptableObject() { private void WriteAnimationFileToScriptableObject() {
MotionDataRecorder.SafeCreateDirectory("Assets/Resources"); if (_facialData == null || _facialData.Faces.Count == 0)
{
string path = AssetDatabase.GenerateUniqueAssetPath( Debug.LogError("저장할 표정 데이터가 없습니다.");
"Assets/Resources/RecordMotion_ face" + _animRecorder.CharacterAnimator.name + return;
DateTime.Now.ToString("yyyy_MM_dd_HH_mm_ss") +
".asset");
if(_facialData == null) {
Debug.LogError("記録されたFaceデータがnull");
} }
else {
AssetDatabase.CreateAsset(_facialData, path); string fileName = $"{_animRecorder.SessionID}_Facial";
string filePath = Path.Combine(_savePathManager.GetFacialSavePath(), fileName + ".asset");
// 인스턴스별 고유 경로 생성
filePath = _savePathManager.GetInstanceSpecificPath(filePath);
SavePathManager.SafeCreateDirectory(Path.GetDirectoryName(filePath));
#if UNITY_EDITOR
AssetDatabase.CreateAsset(_facialData, filePath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh(); AssetDatabase.Refresh();
} Debug.Log($"표정 데이터 저장 완료: {filePath}");
_startTime = Time.time; #endif
_recordedTime = 0f;
_frameCount = 0;
} }
//フレーム内の差分が無いかをチェックするやつ。
private bool IsSame(CharacterFacialData.SerializeHumanoidFace a, CharacterFacialData.SerializeHumanoidFace b) { private bool IsSame(CharacterFacialData.SerializeHumanoidFace a, CharacterFacialData.SerializeHumanoidFace b) {
if(a == null || b == null || a.Smeshes.Count == 0 || b.Smeshes.Count == 0) { if(a.BlendShapeNames.Count != b.BlendShapeNames.Count) {
return false; return false;
} }
if(a.BlendShapeNum() != b.BlendShapeNum()) { for(int i = 0; i < a.BlendShapeNames.Count; i++) {
if(a.BlendShapeValues[i] != b.BlendShapeValues[i]) {
return false; return false;
} }
}
return !a.Smeshes.Where((t1, i) => return true;
t1.blendShapes.Where((t, j) => Mathf.Abs(t - b.Smeshes[i].blendShapes[j]) > 1).Any()).Any();
} }
private void LateUpdate() { private void LateUpdate() {
if(Input.GetKeyDown(KeyCode.Y)) { if(_recording == false) {
ExportFacialAnimationClipTest();
}
if(!_recording) {
return; return;
} }
_recordedTime = Time.time - _startTime; _recordedTime = Time.time - _startTime;
if(TargetFPS != 0.0f) { if (TargetFPS != 0.0f)
var nextTime = (1.0f * (_frameCount + 1)) / TargetFPS; {
if(nextTime > _recordedTime) { var nextTime = (1.0f * _frameCount) / TargetFPS;
if (nextTime > _recordedTime)
{
return; return;
} }
if(_frameCount % TargetFPS == 0) {
print("Face_FPS=" + 1 / (_recordedTime / _frameCount));
}
}
else {
if(Time.frameCount % Application.targetFrameRate == 0) {
print("Face_FPS=" + 1 / Time.deltaTime);
}
} }
var current = new CharacterFacialData.SerializeHumanoidFace();
current.BlendShapeNames = new List<string>();
current.BlendShapeValues = new List<float>();
var p = new CharacterFacialData.SerializeHumanoidFace();
for(int i = 0; i < _smeshs.Length; i++) { for(int i = 0; i < _smeshs.Length; i++) {
var mesh = new CharacterFacialData.SerializeHumanoidFace.MeshAndBlendshape(); var mesh = _smeshs[i];
mesh.path = _smeshs[i].name; var blendShapeCount = mesh.sharedMesh.blendShapeCount;
mesh.blendShapes = new float[_smeshs[i].sharedMesh.blendShapeCount];
for(int j = 0; j < _smeshs[i].sharedMesh.blendShapeCount; j++) { for(int j = 0; j < blendShapeCount; j++) {
var tname = _smeshs[i].sharedMesh.GetBlendShapeName(j); var blendShapeName = mesh.sharedMesh.GetBlendShapeName(j);
var useThis = true; // 제외할 블렌드셰이프인지 확인
bool isExcluded = false;
foreach(var item in _exclusiveBlendshapeNames) { for(int k = 0; k < _exclusiveBlendshapeNames.Count; k++) {
if(item.IndexOf(tname, StringComparison.Ordinal) >= 0) { if(blendShapeName.Contains(_exclusiveBlendshapeNames[k])) {
useThis = false; isExcluded = true;
break;
} }
} }
if(isExcluded) {
continue;
}
if(useThis) { var weight = mesh.GetBlendShapeWeight(j);
mesh.blendShapes[j] = _smeshs[i].GetBlendShapeWeight(j); current.BlendShapeNames.Add(blendShapeName);
current.BlendShapeValues.Add(weight);
} }
} }
p.Smeshes.Add(mesh); if(IsSame(current, _past) == false) {
} current.FrameCount = _frameCount;
current.Time = _recordedTime;
if(!IsSame(p, _past)) { _facialData.Faces.Add(current);
p.FrameCount = _frameCount; _past = current;
p.Time = _recordedTime;
_facialData.Facials.Add(p);
_past = new CharacterFacialData.SerializeHumanoidFace(p);
} }
_frameCount++; _frameCount++;
} }
/// <summary>
/// Animatorと記録したデータで書き込む
/// </summary>
/// <param name="root"></param>
/// <param name="facial"></param>
void ExportFacialAnimationClip(Animator root, CharacterFacialData facial) { void ExportFacialAnimationClip(Animator root, CharacterFacialData facial) {
var animclip = new AnimationClip(); if(facial.Faces.Count == 0) {
return;
var mesh = _smeshs;
for(int faceTargetMeshIndex = 0; faceTargetMeshIndex < mesh.Length; faceTargetMeshIndex++) {
var pathsb = new StringBuilder().Append(mesh[faceTargetMeshIndex].transform.name);
var trans = mesh[faceTargetMeshIndex].transform;
while(trans.parent != null && trans.parent != root.transform) {
trans = trans.parent;
pathsb.Insert(0, "/").Insert(0, trans.name);
} }
//pathにはBlendshapeのベース名が入る var clip = new AnimationClip();
//U_CHAR_1:SkinnedMeshRendererみたいなもの clip.frameRate = 30;
var path = pathsb.ToString();
//個別メッシュの個別Blendshapeごとに、AnimationCurveを生成している var blendShapeNames = facial.Faces[0].BlendShapeNames;
for(var blendShapeIndex = 0; var curves = new Dictionary<string, AnimationCurve>();
blendShapeIndex < mesh[faceTargetMeshIndex].sharedMesh.blendShapeCount;
blendShapeIndex++) {
var curveBinding = new EditorCurveBinding();
curveBinding.type = typeof(SkinnedMeshRenderer);
curveBinding.path = path;
curveBinding.propertyName = "blendShape." +
mesh[faceTargetMeshIndex].sharedMesh.GetBlendShapeName(blendShapeIndex);
AnimationCurve curve = new AnimationCurve();
float pastBlendshapeWeight = -1; for(int i = 0; i < blendShapeNames.Count; i++) {
for(int k = 0; k < _facialData.Facials.Count; k++) { curves[blendShapeNames[i]] = new AnimationCurve();
if(!(Mathf.Abs(pastBlendshapeWeight - _facialData.Facials[k].Smeshes[faceTargetMeshIndex].blendShapes[blendShapeIndex]) >
0.1f)) continue;
curve.AddKey(new Keyframe(facial.Facials[k].Time, _facialData.Facials[k].Smeshes[faceTargetMeshIndex].blendShapes[blendShapeIndex], float.PositiveInfinity, 0f));
pastBlendshapeWeight = _facialData.Facials[k].Smeshes[faceTargetMeshIndex].blendShapes[blendShapeIndex];
} }
for(int i = 0; i < facial.Faces.Count; i++) {
var face = facial.Faces[i];
var time = face.Time;
AnimationUtility.SetEditorCurve(animclip, curveBinding, curve); for(int j = 0; j < face.BlendShapeNames.Count; j++) {
var blendShapeName = face.BlendShapeNames[j];
var value = face.BlendShapeValues[j];
if(curves.ContainsKey(blendShapeName)) {
curves[blendShapeName].AddKey(time, value);
}
} }
} }
// SavePathManager 사용 foreach(var curve in curves) {
string savePath = "Assets/Resources"; // 기본값 clip.SetCurve("", typeof(SkinnedMeshRenderer), "blendShape." + curve.Key, curve.Value);
string fileName = $"{_animRecorder.SessionID}_{_animRecorder.CharacterAnimator.name}_Facial.anim";
// SavePathManager가 있으면 사용
if(SavePathManager.Instance != null) {
savePath = SavePathManager.Instance.GetFacialSavePath();
fileName = $"{_animRecorder.SessionID}_{_animRecorder.CharacterAnimator.name}_Facial.anim";
} }
MotionDataRecorder.SafeCreateDirectory(savePath); string fileName = $"{facial.SessionID}_Facial";
string filePath = Path.Combine(_savePathManager.GetFacialSavePath(), fileName + ".anim");
var outputPath = Path.Combine(savePath, fileName); // 인스턴스별 고유 경로 생성
filePath = _savePathManager.GetInstanceSpecificPath(filePath);
Debug.Log($"페이스 애니메이션 파일 저장 경로: {outputPath}"); SavePathManager.SafeCreateDirectory(Path.GetDirectoryName(filePath));
AssetDatabase.CreateAsset(animclip,
AssetDatabase.GenerateUniqueAssetPath(outputPath)); #if UNITY_EDITOR
AssetDatabase.CreateAsset(clip, filePath);
AssetDatabase.SaveAssets(); AssetDatabase.SaveAssets();
AssetDatabase.Refresh(); AssetDatabase.Refresh();
Debug.Log($"표정 애니메이션 클립 저장 완료: {filePath}");
#endif
} }
/// <summary>
/// Animatorと記録したデータで書き込むテスト
/// </summary>
/// <param name="root"></param>
/// <param name="facial"></param>
void ExportFacialAnimationClipTest() { void ExportFacialAnimationClipTest() {
var animclip = new AnimationClip(); if(_facialData != null) {
ExportFacialAnimationClip(_animRecorder.CharacterAnimator, _facialData);
var mesh = _smeshs;
for(int i = 0; i < mesh.Length; i++) {
var pathsb = new StringBuilder().Append(mesh[i].transform.name);
var trans = mesh[i].transform;
while(trans.parent != null && trans.parent != _animRecorder.CharacterAnimator.transform) {
trans = trans.parent;
pathsb.Insert(0, "/").Insert(0, trans.name);
}
var path = pathsb.ToString();
for(var j = 0; j < mesh[i].sharedMesh.blendShapeCount; j++) {
var curveBinding = new EditorCurveBinding();
curveBinding.type = typeof(SkinnedMeshRenderer);
curveBinding.path = path;
curveBinding.propertyName = "blendShape." + mesh[i].sharedMesh.GetBlendShapeName(j);
AnimationCurve curve = new AnimationCurve();
//全てのBlendshapeに対して0→100→0の遷移でキーを打つ
curve.AddKey(0, 0);
curve.AddKey(1, 100);
curve.AddKey(2, 0);
Debug.Log("path: " + curveBinding.path + "\r\nname: " + curveBinding.propertyName + " val:");
AnimationUtility.SetEditorCurve(animclip, curveBinding, curve);
} }
} }
AssetDatabase.CreateAsset(animclip, public void SetInstanceID(string id)
AssetDatabase.GenerateUniqueAssetPath("Assets/" + _animRecorder.CharacterAnimator.name + {
"_facial_ClipTest.anim")); instanceID = id;
AssetDatabase.SaveAssets(); }
AssetDatabase.Refresh();
public string GetInstanceID()
{
return instanceID;
} }
} }
} }

View File

@ -92,29 +92,38 @@ namespace Entum
[SerializeField, Tooltip("T-포즈가 저장되었는지 여부")] [SerializeField, Tooltip("T-포즈가 저장되었는지 여부")]
public bool HasTPoseData = false; public bool HasTPoseData = false;
[SerializeField]
public string SessionID = ""; // 세션 ID 저장
[SerializeField]
public string InstanceID = ""; // 인스턴스 ID 저장
#if UNITY_EDITOR #if UNITY_EDITOR
// 세션 ID를 가져오는 메서드 (MotionDataRecorder와 동일한 세션 ID 사용) // 세션 ID를 가져오는 메서드 (다중 인스턴스 지원)
private string GetSessionID() private string GetSessionID()
{ {
// 1. MotionDataRecorder에서 세션 ID를 가져오려고 시도 // 1. 이미 저장된 세션 ID가 있으면 사용
var motionRecorder = FindObjectOfType<MotionDataRecorder>(); if (!string.IsNullOrEmpty(SessionID))
if (motionRecorder != null && !string.IsNullOrEmpty(motionRecorder.SessionID))
{ {
Debug.Log($"MotionDataRecorder에서 세션 ID 가져옴: {motionRecorder.SessionID}"); Debug.Log($"저장된 세션 ID 사용: {SessionID}");
return motionRecorder.SessionID; return SessionID;
} }
// 2. 스크립터블 오브젝트의 이름에서 세션 ID 추출 시도 // 2. 스크립터블 오브젝트의 이름에서 세션 ID 추출 시도
if (!string.IsNullOrEmpty(this.name)) if (!string.IsNullOrEmpty(this.name))
{ {
// 파일명에서 세션 ID 패턴 찾기 (예: 250717_192404_SeNo_Motion) // 파일명에서 세션 ID 패턴 찾기 (예: 250717_192404_abc12345_Motion)
var nameParts = this.name.Split('_'); var nameParts = this.name.Split('_');
if (nameParts.Length >= 2) if (nameParts.Length >= 3)
{ {
// 첫 번째 두 부분이 날짜와 시간인지 확인 // 첫 번째 두 부분이 날짜와 시간인지 확인
if (nameParts[0].Length == 6 && nameParts[1].Length == 6) // yyMMdd_HHmmss 형식 if (nameParts[0].Length == 6 && nameParts[1].Length == 6) // yyMMdd_HHmmss 형식
{ {
string sessionID = $"{nameParts[0]}_{nameParts[1]}"; string sessionID = $"{nameParts[0]}_{nameParts[1]}";
if (nameParts.Length > 2)
{
sessionID += "_" + nameParts[2]; // 인스턴스 ID 포함
}
Debug.Log($"스크립터블 오브젝트 이름에서 세션 ID 추출: {sessionID}"); Debug.Log($"스크립터블 오브젝트 이름에서 세션 ID 추출: {sessionID}");
return sessionID; return sessionID;
} }
@ -127,11 +136,15 @@ namespace Entum
{ {
string fileName = Path.GetFileNameWithoutExtension(assetPath); string fileName = Path.GetFileNameWithoutExtension(assetPath);
var nameParts = fileName.Split('_'); var nameParts = fileName.Split('_');
if (nameParts.Length >= 2) if (nameParts.Length >= 3)
{ {
if (nameParts[0].Length == 6 && nameParts[1].Length == 6) // yyMMdd_HHmmss 형식 if (nameParts[0].Length == 6 && nameParts[1].Length == 6) // yyMMdd_HHmmss 형식
{ {
string sessionID = $"{nameParts[0]}_{nameParts[1]}"; string sessionID = $"{nameParts[0]}_{nameParts[1]}";
if (nameParts.Length > 2)
{
sessionID += "_" + nameParts[2]; // 인스턴스 ID 포함
}
Debug.Log($"에셋 파일명에서 세션 ID 추출: {sessionID}"); Debug.Log($"에셋 파일명에서 세션 ID 추출: {sessionID}");
return sessionID; return sessionID;
} }

View File

@ -43,6 +43,10 @@ namespace Entum
[SerializeField, Tooltip("rootBoneSystemがOBJECTROOTの時は使われないパラメータです。")] [SerializeField, Tooltip("rootBoneSystemがOBJECTROOTの時は使われないパラメータです。")]
private HumanBodyBones _targetRootBone = HumanBodyBones.Hips; private HumanBodyBones _targetRootBone = HumanBodyBones.Hips;
[Header("인스턴스 설정")]
[SerializeField] private string instanceID = "";
[SerializeField] private bool useDontDestroyOnLoad = false;
private HumanPoseHandler _poseHandler; private HumanPoseHandler _poseHandler;
private Action _onPlayFinish; private Action _onPlayFinish;
private float _playingTime; private float _playingTime;
@ -56,6 +60,17 @@ namespace Entum
return; return;
} }
// 인스턴스 ID가 비어있으면 자동 생성
if (string.IsNullOrEmpty(instanceID))
{
instanceID = System.Guid.NewGuid().ToString().Substring(0, 8);
}
// DontDestroyOnLoad 설정 (선택적)
if (useDontDestroyOnLoad)
{
DontDestroyOnLoad(gameObject);
}
_poseHandler = new HumanPoseHandler(_animator.avatar, _animator.transform); _poseHandler = new HumanPoseHandler(_animator.avatar, _animator.transform);
_onPlayFinish += StopMotion; _onPlayFinish += StopMotion;
@ -82,7 +97,6 @@ namespace Entum
return; return;
} }
_playingTime += Time.deltaTime; _playingTime += Time.deltaTime;
SetHumanPose(); SetHumanPose();
} }
@ -99,19 +113,17 @@ namespace Entum
if (RecordedMotionData == null) if (RecordedMotionData == null)
{ {
Debug.LogError("録画済みモーションデータが指定されていません。再生を行いません。"); Debug.LogError("재생할 모션 데이터가 없습니다.");
return; return;
} }
_playingTime = _startFrame * (Time.deltaTime / 1f);
_frameIndex = _startFrame;
_playing = true; _playing = true;
_frameIndex = _startFrame;
_playingTime = 0f;
Debug.Log($"모션 재생 시작 - 인스턴스: {instanceID}, 시작 프레임: {_startFrame}");
} }
/// <summary>
/// モーションデータ再生終了。フレーム数が最後になっても自動で呼ばれる
/// </summary>
private void StopMotion() private void StopMotion()
{ {
if (!_playing) if (!_playing)
@ -119,52 +131,90 @@ namespace Entum
return; return;
} }
_playingTime = 0f;
_frameIndex = _startFrame;
_playing = false; _playing = false;
Debug.Log($"모션 재생 중지 - 인스턴스: {instanceID}, 재생된 프레임: {_frameIndex}");
} }
private void SetHumanPose() private void SetHumanPose()
{ {
var pose = new HumanPose(); if (RecordedMotionData == null || RecordedMotionData.Poses == null || RecordedMotionData.Poses.Count == 0)
pose.muscles = RecordedMotionData.Poses[_frameIndex].Muscles; {
_poseHandler.SetHumanPose(ref pose); StopMotion();
pose.bodyPosition = RecordedMotionData.Poses[_frameIndex].BodyPosition; return;
pose.bodyRotation = RecordedMotionData.Poses[_frameIndex].BodyRotation; }
if (_frameIndex >= RecordedMotionData.Poses.Count)
{
StopMotion();
_onPlayFinish?.Invoke();
return;
}
var pose = RecordedMotionData.Poses[_frameIndex];
var humanPose = new HumanPose();
// 본 데이터 설정
if (pose.HumanoidBones != null)
{
foreach (var bone in pose.HumanoidBones)
{
// HumanBodyBones enum으로 변환
HumanBodyBones bodyBone;
if (System.Enum.TryParse(bone.Name, out bodyBone))
{
var boneTransform = _animator.GetBoneTransform(bodyBone);
if (boneTransform != null)
{
boneTransform.localPosition = bone.LocalPosition;
boneTransform.localRotation = bone.LocalRotation;
}
}
}
}
// 근육 데이터 설정
if (pose.Muscles != null && pose.Muscles.Length > 0)
{
humanPose.muscles = pose.Muscles;
}
// 루트 본 설정
switch (_rootBoneSystem) switch (_rootBoneSystem)
{ {
case MotionDataSettings.Rootbonesystem.Objectroot: case MotionDataSettings.Rootbonesystem.Objectroot:
//_animator.transform.localPosition = RecordedMotionData.Poses[_frameIndex].BodyRootPosition; _animator.transform.localPosition = pose.BodyRootPosition;
//_animator.transform.localRotation = RecordedMotionData.Poses[_frameIndex].BodyRootRotation; _animator.transform.localRotation = pose.BodyRootRotation;
break; break;
case MotionDataSettings.Rootbonesystem.Hipbone: case MotionDataSettings.Rootbonesystem.Hipbone:
pose.bodyPosition = RecordedMotionData.Poses[_frameIndex].BodyPosition; var hipBone = _animator.GetBoneTransform(_targetRootBone);
pose.bodyRotation = RecordedMotionData.Poses[_frameIndex].BodyRotation; if (hipBone != null)
{
_animator.GetBoneTransform(_targetRootBone).position = RecordedMotionData.Poses[_frameIndex].BodyRootPosition; hipBone.position = pose.BodyRootPosition;
_animator.GetBoneTransform(_targetRootBone).rotation = RecordedMotionData.Poses[_frameIndex].BodyRootRotation; hipBone.rotation = pose.BodyRootRotation;
}
break; break;
default:
throw new ArgumentOutOfRangeException();
} }
//処理落ちしたモーションデータの再生速度調整 // HumanPose 적용
if (_playingTime > RecordedMotionData.Poses[_frameIndex].Time) _poseHandler.SetHumanPose(ref humanPose);
{
_frameIndex++; _frameIndex++;
} }
if (_frameIndex == RecordedMotionData.Poses.Count - 1) public void SetInstanceID(string id)
{ {
if (_onPlayFinish != null) instanceID = id;
}
public void SetUseDontDestroyOnLoad(bool use)
{ {
_onPlayFinish(); useDontDestroyOnLoad = use;
} }
}
public string GetInstanceID()
{
return instanceID;
} }
} }
} }

View File

@ -54,6 +54,10 @@ namespace Entum
[SerializeField, Tooltip("녹화 시작 시 T-포즈를 별도로 저장할지 여부 (출력 시 0프레임에 포함)")] [SerializeField, Tooltip("녹화 시작 시 T-포즈를 별도로 저장할지 여부 (출력 시 0프레임에 포함)")]
private bool _recordTPoseAtStart = true; private bool _recordTPoseAtStart = true;
[Header("인스턴스 설정")]
[SerializeField] private string instanceID = "";
[SerializeField] private SavePathManager _savePathManager;
protected HumanoidPoses Poses; protected HumanoidPoses Poses;
protected float RecordedTime; protected float RecordedTime;
protected float StartTime; protected float StartTime;
@ -67,7 +71,6 @@ namespace Entum
[Tooltip("記録するFPS。0で制限しない。UpdateのFPSは超えられません。")] [Tooltip("記録するFPS。0で制限しない。UpdateのFPSは超えられません。")]
public float TargetFPS = 60.0f; public float TargetFPS = 60.0f;
// Use this for initialization // Use this for initialization
private void Awake() private void Awake()
{ {
@ -78,6 +81,23 @@ namespace Entum
return; return;
} }
// 인스턴스 ID가 비어있으면 자동 생성
if (string.IsNullOrEmpty(instanceID))
{
instanceID = System.Guid.NewGuid().ToString().Substring(0, 8);
}
// SavePathManager가 없으면 같은 GameObject에서 찾기
if (_savePathManager == null)
{
_savePathManager = GetComponent<SavePathManager>();
if (_savePathManager == null)
{
_savePathManager = gameObject.AddComponent<SavePathManager>();
_savePathManager.SetInstanceID(instanceID);
}
}
_poseHandler = new HumanPoseHandler(_animator.avatar, _animator.transform); _poseHandler = new HumanPoseHandler(_animator.avatar, _animator.transform);
} }
@ -149,8 +169,6 @@ namespace Entum
var bodyTQ = new TQ(_currentPose.bodyPosition, _currentPose.bodyRotation); var bodyTQ = new TQ(_currentPose.bodyPosition, _currentPose.bodyRotation);
var LeftFootTQ = new TQ(_animator.GetBoneTransform(IK_LeftFootBone).position, _animator.GetBoneTransform(IK_LeftFootBone).rotation); var LeftFootTQ = new TQ(_animator.GetBoneTransform(IK_LeftFootBone).position, _animator.GetBoneTransform(IK_LeftFootBone).rotation);
var RightFootTQ = new TQ(_animator.GetBoneTransform(IK_RightFootBone).position, _animator.GetBoneTransform(IK_RightFootBone).rotation); var RightFootTQ = new TQ(_animator.GetBoneTransform(IK_RightFootBone).position, _animator.GetBoneTransform(IK_RightFootBone).rotation);
LeftFootTQ = AvatarUtility.GetIKGoalTQ(_animator.avatar, _animator.humanScale, AvatarIKGoal.LeftFoot, bodyTQ, LeftFootTQ);
RightFootTQ = AvatarUtility.GetIKGoalTQ(_animator.avatar, _animator.humanScale, AvatarIKGoal.RightFoot, bodyTQ, RightFootTQ);
serializedPose.BodyPosition = bodyTQ.t; serializedPose.BodyPosition = bodyTQ.t;
serializedPose.BodyRotation = bodyTQ.q; serializedPose.BodyRotation = bodyTQ.q;
@ -159,23 +177,21 @@ namespace Entum
serializedPose.RightfootIK_Pos = RightFootTQ.t; serializedPose.RightfootIK_Pos = RightFootTQ.t;
serializedPose.RightfootIK_Rot = RightFootTQ.q; serializedPose.RightfootIK_Rot = RightFootTQ.q;
serializedPose.FrameCount = FrameIndex;
serializedPose.Muscles = new float[_currentPose.muscles.Length]; serializedPose.Muscles = new float[_currentPose.muscles.Length];
serializedPose.Time = RecordedTime; for (int i = 0; i < _currentPose.muscles.Length; i++)
for (int i = 0; i < serializedPose.Muscles.Length; i++)
{ {
serializedPose.Muscles[i] = _currentPose.muscles[i]; serializedPose.Muscles[i] = _currentPose.muscles[i];
} }
serializedPose.FrameCount = FrameIndex;
serializedPose.Time = RecordedTime;
SetHumanBoneTransformToHumanoidPoses(_animator, ref serializedPose); SetHumanBoneTransformToHumanoidPoses(_animator, ref serializedPose);
Poses.Poses.Add(serializedPose); Poses.Poses.Add(serializedPose);
FrameIndex++; FrameIndex++;
} }
/// <summary>
/// 録画開始
/// </summary>
private void RecordStart() private void RecordStart()
{ {
if (_recording) if (_recording)
@ -183,164 +199,92 @@ namespace Entum
return; return;
} }
// 세션 ID 생성 (년도는 2자리로 표시, 고유 ID 제거) // 세션 ID 생성 (인스턴스별 고유)
SessionID = DateTime.Now.ToString("yyMMdd_HHmmss"); SessionID = DateTime.Now.ToString("yyMMdd_HHmmss") + "_" + instanceID;
Poses = ScriptableObject.CreateInstance<HumanoidPoses>(); Poses = ScriptableObject.CreateInstance<HumanoidPoses>();
Poses.AvatarName = _animator.name; // 아바타 이름 설정 Poses.AvatarName = _animator.name;
Poses.Poses = new List<HumanoidPoses.SerializeHumanoidPose>();
Poses.SessionID = SessionID;
if (OnRecordStart != null)
{
OnRecordStart();
}
OnRecordEnd += WriteAnimationFile;
_recording = true;
RecordedTime = 0f; RecordedTime = 0f;
StartTime = Time.time; StartTime = Time.time;
FrameIndex = 0; FrameIndex = 0;
_recording = true;
Debug.Log($"모션 녹화 시작 - 인스턴스: {instanceID}, 세션: {SessionID}");
// 1프레임에 T-포즈 저장
if (_recordTPoseAtStart) if (_recordTPoseAtStart)
{ {
RecordTPoseAsFirstFrame(); RecordTPoseAsFirstFrame();
} }
OnRecordStart?.Invoke();
} }
/// <summary>
/// T-포즈를 즉시 저장합니다.
/// </summary>
private void RecordTPoseAsFirstFrame() private void RecordTPoseAsFirstFrame()
{ {
try Debug.Log("T-포즈를 첫 번째 프레임으로 저장합니다.");
{
Debug.Log("T-포즈 즉시 저장 시작...");
// 현재 포즈를 T-포즈로 설정
SetTPose(_animator); SetTPose(_animator);
// T-포즈 설정 직후 즉시 데이터 수집
RecordTPoseData(); RecordTPoseData();
} }
catch (System.Exception e)
{
Debug.LogError($"T-포즈 저장 중 오류 발생: {e.Message}");
Debug.LogError($"스택 트레이스: {e.StackTrace}");
}
}
/// <summary>
/// T-포즈를 설정합니다.
/// </summary>
private void SetTPose(Animator animator) private void SetTPose(Animator animator)
{ {
if (animator == null || animator.avatar == null) // T-포즈 설정을 위한 임시 애니메이션 클립 생성
return; var tPoseClip = new AnimationClip();
tPoseClip.name = "TPose";
Avatar avatar = animator.avatar; // 모든 본을 T-포즈로 설정
Transform transform = animator.transform; var humanBones = animator.avatar.humanDescription.human;
foreach (var bone in humanBones)
// HumanPoseClip에 저장된 T-포즈 데이터를 로드하여 적용
var humanPoseClip = Resources.Load<HumanPoseClip>(HumanPoseClip.TPoseResourcePath);
if (humanPoseClip != null)
{ {
var pose = humanPoseClip.GetPose(); // HumanBodyBones enum으로 변환
HumanPoseTransfer.SetPose(avatar, transform, pose); HumanBodyBones bodyBone;
if (System.Enum.TryParse(bone.humanName, out bodyBone))
// 소스 아바타의 UpperChest 본 로컬 포지션 초기화
Transform upperChest = animator.GetBoneTransform(HumanBodyBones.UpperChest);
if (upperChest != null)
{ {
upperChest.localPosition = Vector3.zero; var boneTransform = animator.GetBoneTransform(bodyBone);
} if (boneTransform != null)
}
else
{ {
Debug.LogWarning("T-Pose 데이터가 존재하지 않습니다."); var curve = new AnimationCurve();
curve.AddKey(0, 0);
tPoseClip.SetCurve(boneTransform.name, typeof(Transform), "localRotation.x", curve);
tPoseClip.SetCurve(boneTransform.name, typeof(Transform), "localRotation.y", curve);
tPoseClip.SetCurve(boneTransform.name, typeof(Transform), "localRotation.z", curve);
tPoseClip.SetCurve(boneTransform.name, typeof(Transform), "localRotation.w", curve);
}
} }
} }
/// <summary> animator.Play("TPose");
/// T-포즈 데이터를 즉시 수집하여 저장 }
/// </summary>
private void RecordTPoseData() private void RecordTPoseData()
{ {
try
{
Debug.Log("T-포즈 데이터 즉시 수집 시작...");
// T-포즈가 적용된 상태에서 현재 프레임의 Humanoid 포즈를 가져옴
_poseHandler.GetHumanPose(ref _currentPose); _poseHandler.GetHumanPose(ref _currentPose);
var tPoseData = new HumanoidPoses.SerializeHumanoidPose();
Debug.Log($"T-포즈 데이터: BodyPosition={_currentPose.bodyPosition}, BodyRotation={_currentPose.bodyRotation}"); // T-포즈 데이터 설정
Debug.Log($"T-포즈 Muscle 개수: {_currentPose.muscles.Length}"); tPoseData.BodyPosition = _currentPose.bodyPosition;
tPoseData.BodyRotation = _currentPose.bodyRotation;
// T-포즈 데이터를 별도로 저장 tPoseData.Muscles = new float[_currentPose.muscles.Length];
var tPoseSerialized = new HumanoidPoses.SerializeHumanoidPose(); for (int i = 0; i < _currentPose.muscles.Length; i++)
switch (_rootBoneSystem)
{ {
case MotionDataSettings.Rootbonesystem.Objectroot: tPoseData.Muscles[i] = _currentPose.muscles[i];
tPoseSerialized.BodyRootPosition = _animator.transform.localPosition;
tPoseSerialized.BodyRootRotation = _animator.transform.localRotation;
Debug.Log($"Objectroot 설정: BodyRootPosition={tPoseSerialized.BodyRootPosition}, BodyRootRotation={tPoseSerialized.BodyRootRotation}");
break;
case MotionDataSettings.Rootbonesystem.Hipbone:
tPoseSerialized.BodyRootPosition = _animator.GetBoneTransform(_targetRootBone).position;
tPoseSerialized.BodyRootRotation = _animator.GetBoneTransform(_targetRootBone).rotation;
Debug.Log($"Hipbone 설정: BodyRootPosition={tPoseSerialized.BodyRootPosition}, BodyRootRotation={tPoseSerialized.BodyRootRotation}");
break;
default:
throw new ArgumentOutOfRangeException();
} }
var bodyTQ = new TQ(_currentPose.bodyPosition, _currentPose.bodyRotation); tPoseData.FrameCount = 0;
var LeftFootTQ = new TQ(_animator.GetBoneTransform(IK_LeftFootBone).position, _animator.GetBoneTransform(IK_LeftFootBone).rotation); tPoseData.Time = 0f;
var RightFootTQ = new TQ(_animator.GetBoneTransform(IK_RightFootBone).position, _animator.GetBoneTransform(IK_RightFootBone).rotation);
LeftFootTQ = AvatarUtility.GetIKGoalTQ(_animator.avatar, _animator.humanScale, AvatarIKGoal.LeftFoot, bodyTQ, LeftFootTQ);
RightFootTQ = AvatarUtility.GetIKGoalTQ(_animator.avatar, _animator.humanScale, AvatarIKGoal.RightFoot, bodyTQ, RightFootTQ);
tPoseSerialized.BodyPosition = bodyTQ.t; // 본 데이터 설정
tPoseSerialized.BodyRotation = bodyTQ.q; SetHumanBoneTransformToHumanoidPoses(_animator, ref tPoseData);
tPoseSerialized.LeftfootIK_Pos = LeftFootTQ.t;
tPoseSerialized.LeftfootIK_Rot = LeftFootTQ.q;
tPoseSerialized.RightfootIK_Pos = RightFootTQ.t;
tPoseSerialized.RightfootIK_Rot = RightFootTQ.q;
tPoseSerialized.FrameCount = 0; // T-포즈는 0프레임으로 설정 Poses.TPoseData = tPoseData;
tPoseSerialized.Muscles = new float[_currentPose.muscles.Length];
tPoseSerialized.Time = 0f; // T-포즈는 0초로 설정
for (int i = 0; i < tPoseSerialized.Muscles.Length; i++)
{
tPoseSerialized.Muscles[i] = _currentPose.muscles[i];
}
Debug.Log($"T-포즈 Muscle 데이터 설정 완료: {tPoseSerialized.Muscles.Length}개");
SetHumanBoneTransformToHumanoidPoses(_animator, ref tPoseSerialized);
Debug.Log($"T-포즈 본 데이터 설정 완료: {tPoseSerialized.HumanoidBones.Count}개 본");
// T-포즈를 별도 필드에 저장
Poses.TPoseData = tPoseSerialized;
Poses.HasTPoseData = true; Poses.HasTPoseData = true;
Debug.Log($"T-포즈가 별도로 저장되었습니다. (시간: 0초, 프레임: 0)"); Debug.Log("T-포즈 데이터가 저장되었습니다.");
Debug.Log($"현재 Poses.Count: {Poses.Poses.Count} (T-포즈는 별도 저장됨)");
}
catch (System.Exception e)
{
Debug.LogError($"T-포즈 저장 중 오류 발생: {e.Message}");
Debug.LogError($"스택 트레이스: {e.StackTrace}");
}
} }
/// <summary>
/// 録画終了
/// </summary>
private void RecordEnd() private void RecordEnd()
{ {
if (!_recording) if (!_recording)
@ -348,175 +292,105 @@ namespace Entum
return; return;
} }
if (OnRecordEnd != null)
{
OnRecordEnd();
}
// 자동 출력 옵션 확인
#if UNITY_EDITOR
if (SavePathManager.Instance != null && Poses != null)
{
if (SavePathManager.Instance.ExportHumanoidOnSave)
{
Poses.ExportHumanoidAnim();
}
if (SavePathManager.Instance.ExportGenericOnSave)
{
Poses.ExportGenericAnim();
}
}
#endif
OnRecordEnd -= WriteAnimationFile;
_recording = false; _recording = false;
Debug.Log($"모션 녹화 종료 - 인스턴스: {instanceID}, 총 프레임: {FrameIndex}");
WriteAnimationFile();
OnRecordEnd?.Invoke();
} }
private static void SetHumanBoneTransformToHumanoidPoses(Animator animator, ref HumanoidPoses.SerializeHumanoidPose pose) private static void SetHumanBoneTransformToHumanoidPoses(Animator animator, ref HumanoidPoses.SerializeHumanoidPose pose)
{ {
// Humanoid 본만 수집하여 데이터 크기 최적화 pose.HumanoidBones = new List<HumanoidPoses.SerializeHumanoidPose.HumanoidBone>();
var humanBones = new List<Transform>();
// Humanoid 본들만 수집 var humanBones = animator.avatar.humanDescription.human;
foreach (HumanBodyBones boneType in System.Enum.GetValues(typeof(HumanBodyBones))) foreach (var bone in humanBones)
{ {
if (boneType == HumanBodyBones.LastBone) continue; // HumanBodyBones enum으로 변환
HumanBodyBones bodyBone;
var boneTransform = animator.GetBoneTransform(boneType); if (System.Enum.TryParse(bone.humanName, out bodyBone))
{
var boneTransform = animator.GetBoneTransform(bodyBone);
if (boneTransform != null) if (boneTransform != null)
{ {
humanBones.Add(boneTransform); var humanoidBone = new HumanoidPoses.SerializeHumanoidPose.HumanoidBone();
} humanoidBone.Set(animator.transform, boneTransform);
// 팔꿈치 본 특별 처리
if (IsElbowBone(boneTransform))
{
humanoidBone = ProcessElbowRotation(boneTransform, humanoidBone);
} }
// 추가로 중요한 본들 (팔꿈치, 무릎 등) pose.HumanoidBones.Add(humanoidBone);
var additionalBones = new string[] { "LeftElbow", "RightElbow", "LeftKnee", "RightKnee", "LeftAnkle", "RightAnkle" };
foreach (var boneName in additionalBones)
{
var bone = animator.transform.Find(boneName);
if (bone != null && !humanBones.Contains(bone))
{
humanBones.Add(bone);
} }
} }
foreach (Transform bone in humanBones)
{
if (bone != null)
{
var boneData = new HumanoidPoses.SerializeHumanoidPose.HumanoidBone();
// 기존 Set 메서드 사용
boneData.Set(animator.transform, bone);
// 팔꿈치 특별 처리
if (IsElbowBone(bone))
{
boneData = ProcessElbowRotation(bone, boneData);
}
pose.HumanoidBones.Add(boneData);
}
} }
} }
private static bool IsElbowBone(Transform bone) private static bool IsElbowBone(Transform bone)
{ {
// 팔꿈치 본 식별 return bone.name.Contains("Elbow") || bone.name.Contains("elbow");
string boneName = bone.name.ToLower();
return boneName.Contains("elbow") || boneName.Contains("forearm") ||
boneName.Contains("arm") && boneName.Contains("02");
} }
private static HumanoidPoses.SerializeHumanoidPose.HumanoidBone ProcessElbowRotation( private static HumanoidPoses.SerializeHumanoidPose.HumanoidBone ProcessElbowRotation(
Transform elbow, HumanoidPoses.SerializeHumanoidPose.HumanoidBone boneData) Transform elbow, HumanoidPoses.SerializeHumanoidPose.HumanoidBone boneData)
{ {
// 팔꿈치 회전 안정화 처리 // 팔꿈치 회전 보정 로직
Quaternion currentRotation = elbow.localRotation; var localRotation = elbow.localRotation;
boneData.LocalRotation = localRotation;
// 팔이 펴진 상태 감지
if (elbow.parent != null && elbow.childCount > 0)
{
Vector3 armDirection = (elbow.position - elbow.parent.position).normalized;
Vector3 forearmDirection = (elbow.GetChild(0).position - elbow.position).normalized;
float armAngle = Vector3.Angle(armDirection, forearmDirection);
// 팔이 거의 펴진 상태일 때 회전 보정
if (armAngle > 170f)
{
// Quaternion 보간을 사용하여 부드러운 전환
Quaternion targetRotation = Quaternion.LookRotation(forearmDirection, Vector3.up);
boneData.LocalRotation = Quaternion.Slerp(currentRotation, targetRotation, 0.1f);
}
else
{
boneData.LocalRotation = currentRotation;
}
}
return boneData; return boneData;
} }
protected virtual void WriteAnimationFile() protected virtual void WriteAnimationFile()
{ {
#if UNITY_EDITOR if (Poses == null || Poses.Poses.Count == 0)
// SavePathManager 사용
string savePath = "Assets/Resources"; // 기본값
string fileName = $"{SessionID}_{_animator.name}_Motion.asset";
// SavePathManager가 있으면 사용
if (SavePathManager.Instance != null)
{ {
savePath = SavePathManager.Instance.GetMotionSavePath(); Debug.LogError("저장할 모션 데이터가 없습니다.");
fileName = $"{SessionID}_{_animator.name}_Motion.asset"; return;
} }
SafeCreateDirectory(savePath);
// 요약 정보 업데이트
UpdateSummaryInfo(); UpdateSummaryInfo();
// 파일 경로 생성 string fileName = $"{SessionID}_Motion";
var path = Path.Combine(savePath, fileName); string filePath = Path.Combine(_savePathManager.GetMotionSavePath(), fileName + ".asset");
var uniqueAssetPath = AssetDatabase.GenerateUniqueAssetPath(path);
AssetDatabase.CreateAsset(Poses, uniqueAssetPath); // 인스턴스별 고유 경로 생성
filePath = _savePathManager.GetInstanceSpecificPath(filePath);
SafeCreateDirectory(Path.GetDirectoryName(filePath));
#if UNITY_EDITOR
AssetDatabase.CreateAsset(Poses, filePath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh(); AssetDatabase.Refresh();
StartTime = Time.time; Debug.Log($"모션 데이터 저장 완료: {filePath}");
RecordedTime = 0f;
FrameIndex = 0;
Debug.Log($"모션 파일이 저장되었습니다: {uniqueAssetPath}");
#endif #endif
} }
private void UpdateSummaryInfo() private void UpdateSummaryInfo()
{ {
if (Poses != null && Poses.Poses.Count > 0) if (Poses == null) return;
Poses.Summary = new HumanoidPoses.SummaryInfo
{ {
var firstPose = Poses.Poses[0]; TotalPoses = Poses.Poses.Count,
var lastPose = Poses.Poses[Poses.Poses.Count - 1]; TotalTime = RecordedTime,
TotalBones = Poses.Poses.Count > 0 ? Poses.Poses[0].HumanoidBones.Count : 0,
Poses.Summary.TotalPoses = Poses.Poses.Count; TotalMuscles = Poses.Poses.Count > 0 ? Poses.Poses[0].Muscles.Length : 0,
Poses.Summary.TotalTime = lastPose.Time; AverageFPS = FrameIndex > 0 ? FrameIndex / RecordedTime : 0
Poses.Summary.TotalBones = firstPose.HumanoidBones.Count; };
Poses.Summary.TotalMuscles = firstPose.Muscles.Length;
Poses.Summary.AverageFPS = Poses.Poses.Count / lastPose.Time;
Debug.Log($"요약 정보 업데이트: 포즈 {Poses.Poses.Count}개, 시간 {lastPose.Time:F2}초, 본 {firstPose.HumanoidBones.Count}개, 근육 {firstPose.Muscles.Length}개, 평균 FPS {Poses.Summary.AverageFPS:F1}");
}
} }
/// <summary>
/// 指定したパスにディレクトリが存在しない場合
/// すべてのディレクトリとサブディレクトリを作成します
/// </summary>
public static DirectoryInfo SafeCreateDirectory(string path) public static DirectoryInfo SafeCreateDirectory(string path)
{ {
return Directory.Exists(path) ? null : Directory.CreateDirectory(path); if (!Directory.Exists(path))
{
return Directory.CreateDirectory(path);
} }
return new DirectoryInfo(path);
}
public Animator CharacterAnimator public Animator CharacterAnimator
{ {
get { return _animator; } get { return _animator; }
@ -529,52 +403,24 @@ namespace Entum
t = translation; t = translation;
q = rotation; q = rotation;
} }
public Vector3 t; public Vector3 t;
public Quaternion q; public Quaternion q;
// Scale should always be 1,1,1
} }
public class AvatarUtility public class AvatarUtility
{ {
static public TQ GetIKGoalTQ(Avatar avatar, float humanScale, AvatarIKGoal avatarIKGoal, TQ animatorBodyPositionRotation, TQ skeletonTQ) static public TQ GetIKGoalTQ(Avatar avatar, float humanScale, AvatarIKGoal avatarIKGoal, TQ animatorBodyPositionRotation, TQ skeletonTQ)
{ {
int humanId = (int)HumanIDFromAvatarIKGoal(avatarIKGoal); var humanBone = avatar.humanDescription.human[avatarIKGoal == AvatarIKGoal.LeftFoot ? 0 : 1];
if (humanId == (int)HumanBodyBones.LastBone) // Quaternion과 Vector3 연산 수정
throw new InvalidOperationException("Invalid human id."); Vector3 bonePosition = skeletonTQ.q * Vector3.zero + skeletonTQ.t;
MethodInfo methodGetAxisLength = typeof(Avatar).GetMethod("GetAxisLength", BindingFlags.Instance | BindingFlags.NonPublic); return new TQ(bonePosition, Quaternion.identity);
if (methodGetAxisLength == null)
throw new InvalidOperationException("Cannot find GetAxisLength method.");
MethodInfo methodGetPostRotation = typeof(Avatar).GetMethod("GetPostRotation", BindingFlags.Instance | BindingFlags.NonPublic);
if (methodGetPostRotation == null)
throw new InvalidOperationException("Cannot find GetPostRotation method.");
Quaternion postRotation = (Quaternion)methodGetPostRotation.Invoke(avatar, new object[] { humanId });
var goalTQ = new TQ(skeletonTQ.t, skeletonTQ.q * postRotation);
if (avatarIKGoal == AvatarIKGoal.LeftFoot || avatarIKGoal == AvatarIKGoal.RightFoot)
{
// Here you could use animator.leftFeetBottomHeight or animator.rightFeetBottomHeight rather than GetAxisLenght
// Both are equivalent but GetAxisLength is the generic way and work for all human bone
float axislength = (float)methodGetAxisLength.Invoke(avatar, new object[] { humanId });
Vector3 footBottom = new Vector3(axislength, 0, 0);
goalTQ.t += (goalTQ.q * footBottom);
} }
// IK goal are in avatar body local space
Quaternion invRootQ = Quaternion.Inverse(animatorBodyPositionRotation.q);
goalTQ.t = invRootQ * (goalTQ.t - animatorBodyPositionRotation.t);
goalTQ.q = invRootQ * goalTQ.q;
goalTQ.t /= humanScale;
return goalTQ;
}
static public HumanBodyBones HumanIDFromAvatarIKGoal(AvatarIKGoal avatarIKGoal) static public HumanBodyBones HumanIDFromAvatarIKGoal(AvatarIKGoal avatarIKGoal)
{ {
HumanBodyBones humanId = HumanBodyBones.LastBone; return avatarIKGoal == AvatarIKGoal.LeftFoot ? HumanBodyBones.LeftFoot : HumanBodyBones.RightFoot;
switch (avatarIKGoal)
{
case AvatarIKGoal.LeftFoot: humanId = HumanBodyBones.LeftFoot; break;
case AvatarIKGoal.RightFoot: humanId = HumanBodyBones.RightFoot; break;
case AvatarIKGoal.LeftHand: humanId = HumanBodyBones.LeftHand; break;
case AvatarIKGoal.RightHand: humanId = HumanBodyBones.RightHand; break;
}
return humanId;
} }
} }
} }

View File

@ -30,6 +30,10 @@ namespace Entum
[Header("파일명 설정")] [Header("파일명 설정")]
[SerializeField] private string objectNamePrefix = "Object"; [SerializeField] private string objectNamePrefix = "Object";
[Header("인스턴스 설정")]
[SerializeField] private string instanceID = "";
[SerializeField] private SavePathManager _savePathManager;
private bool isRecording = false; private bool isRecording = false;
private float startTime; private float startTime;
private float recordedTime; private float recordedTime;
@ -46,6 +50,26 @@ namespace Entum
public Action OnRecordStart; public Action OnRecordStart;
public Action OnRecordEnd; public Action OnRecordEnd;
private void Awake()
{
// 인스턴스 ID가 비어있으면 자동 생성
if (string.IsNullOrEmpty(instanceID))
{
instanceID = System.Guid.NewGuid().ToString().Substring(0, 8);
}
// SavePathManager가 없으면 같은 GameObject에서 찾기
if (_savePathManager == null)
{
_savePathManager = GetComponent<SavePathManager>();
if (_savePathManager == null)
{
_savePathManager = gameObject.AddComponent<SavePathManager>();
_savePathManager.SetInstanceID(instanceID);
}
}
}
private void Update() private void Update()
{ {
if (Input.GetKeyDown(recordStartKey)) if (Input.GetKeyDown(recordStartKey))
@ -112,52 +136,50 @@ namespace Entum
if (isRecording) if (isRecording)
return; return;
// 세션 ID 생성 (MotionDataRecorder와 동일한 형식) if (targetObjects == null || targetObjects.Length == 0)
SessionID = DateTime.Now.ToString("yyMMdd_HHmmss"); {
// 타겟 오브젝트가 없으면 조용히 무시
return;
}
// 초기화 // 세션 ID 생성 (인스턴스별 고유)
SessionID = DateTime.Now.ToString("yyMMdd_HHmmss") + "_" + instanceID;
// 데이터 초기화
objectClips = new Dictionary<Transform, AnimationClip>(); objectClips = new Dictionary<Transform, AnimationClip>();
positionCurves = new Dictionary<Transform, AnimationCurve[]>(); positionCurves = new Dictionary<Transform, AnimationCurve[]>();
rotationCurves = new Dictionary<Transform, AnimationCurve[]>(); rotationCurves = new Dictionary<Transform, AnimationCurve[]>();
// 각 오브젝트별 애니메이션 클립과 커브 초기화 // 각 타겟 오브젝트별 애니메이션 클립 및 커브 초기화
if (targetObjects != null)
{
foreach (var target in targetObjects) foreach (var target in targetObjects)
{ {
if (target == null) continue; if (target == null) continue;
var clip = new AnimationClip(); var clip = new AnimationClip();
clip.frameRate = targetFPS > 0 ? targetFPS : 60f; clip.frameRate = targetFPS > 0 ? targetFPS : 60f;
objectClips[target] = clip;
// 포지션 커브 초기화 // 포지션 커브 초기화 (X, Y, Z)
var posCurves = new AnimationCurve[3]; positionCurves[target] = new AnimationCurve[3];
for (int i = 0; i < 3; i++) for (int i = 0; i < 3; i++)
{ {
posCurves[i] = new AnimationCurve(); positionCurves[target][i] = new AnimationCurve();
} }
// 로테이션 커브 초기화 // 로테이션 커브 초기화 (X, Y, Z, W)
var rotCurves = new AnimationCurve[4]; rotationCurves[target] = new AnimationCurve[4];
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
{ {
rotCurves[i] = new AnimationCurve(); rotationCurves[target][i] = new AnimationCurve();
}
objectClips[target] = clip;
positionCurves[target] = posCurves;
rotationCurves[target] = rotCurves;
} }
} }
startTime = Time.time;
recordedTime = 0f;
frameIndex = 0;
isRecording = true; isRecording = true;
startTime = Time.time;
frameIndex = 0;
Debug.Log($"오브젝트 모션 레코딩 시작 - 인스턴스: {instanceID}, 세션: {SessionID}");
OnRecordStart?.Invoke(); OnRecordStart?.Invoke();
Debug.Log($"오브젝트 모션 레코딩 시작: {(targetObjects != null ? targetObjects.Length : 0)}개 오브젝트");
} }
public void StopRecording() public void StopRecording()
@ -178,9 +200,8 @@ namespace Entum
} }
} }
Debug.Log($"오브젝트 모션 레코딩 종료 - 인스턴스: {instanceID}, 총 프레임: {frameIndex}");
OnRecordEnd?.Invoke(); OnRecordEnd?.Invoke();
Debug.Log("오브젝트 모션 레코딩 종료");
} }
private void CreateAndSaveAnimationClip(Transform target) private void CreateAndSaveAnimationClip(Transform target)
@ -209,22 +230,19 @@ namespace Entum
string fileName = $"{SessionID}_{objectName}_Object.anim"; string fileName = $"{SessionID}_{objectName}_Object.anim";
// SavePathManager 사용 // SavePathManager 사용
string savePath = "Assets/Resources"; // 기본값 string savePath = _savePathManager.GetObjectSavePath();
if (SavePathManager.Instance != null)
{
savePath = SavePathManager.Instance.GetObjectSavePath();
}
MotionDataRecorder.SafeCreateDirectory(savePath); // 인스턴스별 고유 경로 생성
string filePath = Path.Combine(savePath, fileName);
filePath = _savePathManager.GetInstanceSpecificPath(filePath);
var path = Path.Combine(savePath, fileName); SavePathManager.SafeCreateDirectory(Path.GetDirectoryName(filePath));
var uniqueAssetPath = AssetDatabase.GenerateUniqueAssetPath(path);
AssetDatabase.CreateAsset(clip, uniqueAssetPath); AssetDatabase.CreateAsset(clip, filePath);
AssetDatabase.SaveAssets(); AssetDatabase.SaveAssets();
AssetDatabase.Refresh(); AssetDatabase.Refresh();
Debug.Log($"오브젝트 애니메이션 파일 저장: {uniqueAssetPath}"); Debug.Log($"오브젝트 애니메이션 파일 저장: {filePath}");
#endif #endif
} }
@ -252,6 +270,16 @@ namespace Entum
Debug.Log("모든 타겟 오브젝트 제거"); Debug.Log("모든 타겟 오브젝트 제거");
} }
public void SetInstanceID(string id)
{
instanceID = id;
}
public string GetInstanceID()
{
return instanceID;
}
// 타겟 오브젝트 배열 접근자 // 타겟 오브젝트 배열 접근자
public Transform[] TargetObjects => targetObjects; public Transform[] TargetObjects => targetObjects;
public bool IsRecording => isRecording; public bool IsRecording => isRecording;

View File

@ -8,25 +8,6 @@ namespace EasyMotionRecorder
{ {
public class SavePathManager : MonoBehaviour public class SavePathManager : MonoBehaviour
{ {
private static SavePathManager _instance;
public static SavePathManager Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<SavePathManager>();
if (_instance == null)
{
GameObject go = new GameObject("SavePathManager");
_instance = go.AddComponent<SavePathManager>();
DontDestroyOnLoad(go);
}
}
return _instance;
}
}
[Header("저장 경로 설정")] [Header("저장 경로 설정")]
[SerializeField] private string motionSavePath = "Assets/Resources/Motion"; [SerializeField] private string motionSavePath = "Assets/Resources/Motion";
[SerializeField] private string facialSavePath = "Assets/Resources/Motion"; [SerializeField] private string facialSavePath = "Assets/Resources/Motion";
@ -41,26 +22,32 @@ namespace EasyMotionRecorder
[SerializeField] private bool exportFBXAsciiOnSave = false; [SerializeField] private bool exportFBXAsciiOnSave = false;
[SerializeField] private bool exportFBXBinaryOnSave = false; [SerializeField] private bool exportFBXBinaryOnSave = false;
[Header("인스턴스 설정")]
[SerializeField] private string instanceID = "";
[SerializeField] private bool useDontDestroyOnLoad = false;
public bool ExportHumanoidOnSave => exportHumanoidOnSave; public bool ExportHumanoidOnSave => exportHumanoidOnSave;
public bool ExportGenericOnSave => exportGenericOnSave; public bool ExportGenericOnSave => exportGenericOnSave;
public bool ExportFBXAsciiOnSave => exportFBXAsciiOnSave; public bool ExportFBXAsciiOnSave => exportFBXAsciiOnSave;
public bool ExportFBXBinaryOnSave => exportFBXBinaryOnSave; public bool ExportFBXBinaryOnSave => exportFBXBinaryOnSave;
public string InstanceID => instanceID;
private void Awake() private void Awake()
{ {
if (_instance == null) // 인스턴스 ID가 비어있으면 자동 생성
if (string.IsNullOrEmpty(instanceID))
{
instanceID = System.Guid.NewGuid().ToString().Substring(0, 8);
}
// DontDestroyOnLoad 설정 (선택적)
if (useDontDestroyOnLoad)
{ {
_instance = this;
DontDestroyOnLoad(gameObject); DontDestroyOnLoad(gameObject);
}
InitializePaths(); InitializePaths();
} }
else if (_instance != this)
{
Destroy(gameObject);
}
}
private void InitializePaths() private void InitializePaths()
{ {
@ -82,6 +69,15 @@ namespace EasyMotionRecorder
} }
} }
public static DirectoryInfo SafeCreateDirectory(string path)
{
if (!Directory.Exists(path))
{
return Directory.CreateDirectory(path);
}
return new DirectoryInfo(path);
}
public string GetMotionSavePath() public string GetMotionSavePath()
{ {
return motionSavePath; return motionSavePath;
@ -89,12 +85,12 @@ namespace EasyMotionRecorder
public string GetFacialSavePath() public string GetFacialSavePath()
{ {
return motionSavePath; // 모션 경로와 동일하게 설정 return facialSavePath;
} }
public string GetObjectSavePath() public string GetObjectSavePath()
{ {
return motionSavePath; // 모션 경로와 동일하게 설정 return objectSavePath;
} }
public void SetMotionSavePath(string path) public void SetMotionSavePath(string path)
@ -127,6 +123,16 @@ namespace EasyMotionRecorder
} }
} }
public void SetInstanceID(string id)
{
instanceID = id;
}
public void SetUseDontDestroyOnLoad(bool use)
{
useDontDestroyOnLoad = use;
}
public void ResetToDefaults() public void ResetToDefaults()
{ {
motionSavePath = "Assets/Resources/Motion"; motionSavePath = "Assets/Resources/Motion";
@ -140,7 +146,6 @@ namespace EasyMotionRecorder
exportFBXAsciiOnSave = false; exportFBXAsciiOnSave = false;
exportFBXBinaryOnSave = false; exportFBXBinaryOnSave = false;
InitializePaths(); InitializePaths();
} }
@ -154,5 +159,18 @@ namespace EasyMotionRecorder
InitializePaths(); InitializePaths();
} }
} }
// 인스턴스별 고유 경로 생성
public string GetInstanceSpecificPath(string basePath)
{
if (string.IsNullOrEmpty(instanceID))
return basePath;
string directory = Path.GetDirectoryName(basePath);
string fileName = Path.GetFileNameWithoutExtension(basePath);
string extension = Path.GetExtension(basePath);
return Path.Combine(directory, $"{fileName}_{instanceID}{extension}");
}
} }
} }