From 9dc2d4d64f16f15396aace3fe60b5c3186e0b096 Mon Sep 17 00:00:00 2001 From: KINDNICK Date: Sat, 26 Jul 2025 14:53:11 +0900 Subject: [PATCH] =?UTF-8?q?Add=20:=20=EB=AA=A8=EC=85=98=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=20=ED=8C=A8=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Scripts/CharacterFacialData.cs | 74 ++- .../Scripts/FaceAnimationRecorder.cs | 314 +++++------- .../Scripts/HumanoidPoses.cs | 31 +- .../Scripts/MotionDataPlayer.cs | 126 +++-- .../Scripts/MotionDataRecorder.cs | 458 ++++++------------ .../Scripts/ObjectMotionRecorder.cs | 118 +++-- .../Scripts/SavePathManager.cs | 78 +-- 7 files changed, 542 insertions(+), 657 deletions(-) diff --git a/Assets/External/EasyMotionRecorder/Scripts/CharacterFacialData.cs b/Assets/External/EasyMotionRecorder/Scripts/CharacterFacialData.cs index 0c769b1b..c6f9d100 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/CharacterFacialData.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/CharacterFacialData.cs @@ -3,52 +3,34 @@ using System.Collections.Generic; using System.Linq; using UnityEngine; -namespace Entum +namespace EasyMotionRecorder { - public class CharacterFacialData : ScriptableObject - { - - [System.SerializableAttribute] - public class SerializeHumanoidFace - { - public class MeshAndBlendshape - { - public string path; - public float[] blendShapes; - } - - - public int BlendShapeNum() - { - return Smeshes.Count == 0 ? 0 : Smeshes.Sum(t => t.blendShapes.Length); - } - - //フレーム数 - public int FrameCount; - - //記録開始後の経過時間。処理落ち対策 - 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 Smeshes= new List(); - public SerializeHumanoidFace() - { - } - } - + [Serializable] + public class CharacterFacialData : ScriptableObject + { + [SerializeField] + public string SessionID = ""; - public List Facials = new List(); - } + [SerializeField] + public string InstanceID = ""; + + [SerializeField] + public List Faces = new List(); + + [Serializable] + public class SerializeHumanoidFace + { + [SerializeField] + public List BlendShapeNames = new List(); + + [SerializeField] + public List BlendShapeValues = new List(); + + [SerializeField] + public int FrameCount; + + [SerializeField] + public float Time; + } + } } \ No newline at end of file diff --git a/Assets/External/EasyMotionRecorder/Scripts/FaceAnimationRecorder.cs b/Assets/External/EasyMotionRecorder/Scripts/FaceAnimationRecorder.cs index d58974fa..a33a4cfd 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/FaceAnimationRecorder.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/FaceAnimationRecorder.cs @@ -37,8 +37,11 @@ namespace Entum { [Tooltip("記録するFPS。0で制限しない。UpdateのFPSは超えられません。")] public float TargetFPS = 60.0f; - private MotionDataRecorder _animRecorder; + [Header("인스턴스 설정")] + [SerializeField] private string instanceID = ""; + [SerializeField] private SavePathManager _savePathManager; + private MotionDataRecorder _animRecorder; private SkinnedMeshRenderer[] _smeshs; @@ -48,7 +51,6 @@ namespace Entum { private int _frameCount = 0; - CharacterFacialData.SerializeHumanoidFace _past = new CharacterFacialData.SerializeHumanoidFace(); private float _recordedTime = 0f; @@ -59,6 +61,24 @@ namespace Entum { _animRecorder = GetComponent(); _animRecorder.OnRecordStart += RecordStart; _animRecorder.OnRecordEnd += RecordEnd; + + // 인스턴스 ID가 비어있으면 자동 생성 + if (string.IsNullOrEmpty(instanceID)) + { + instanceID = System.Guid.NewGuid().ToString().Substring(0, 8); + } + + // SavePathManager가 없으면 같은 GameObject에서 찾기 + if (_savePathManager == null) + { + _savePathManager = GetComponent(); + if (_savePathManager == null) + { + _savePathManager = gameObject.AddComponent(); + _savePathManager.SetInstanceID(instanceID); + } + } + if(_animRecorder.CharacterAnimator != null) { _smeshs = GetSkinnedMeshRenderers(_animRecorder.CharacterAnimator); } @@ -98,258 +118,186 @@ namespace Entum { return; } - if(_recording) { - return; - } + _facialData = ScriptableObject.CreateInstance(); + _facialData.Faces = new List(); + _facialData.SessionID = _animRecorder.SessionID; + _facialData.InstanceID = instanceID; - if(_smeshs.Length == 0) { - Debug.LogError("顔のメッシュ指定がされていないので顔のアニメーションは記録しません"); - return; - } - - Debug.Log("FaceAnimationRecorder record start"); _recording = true; + _frameCount = 0; _recordedTime = 0f; _startTime = Time.time; - _frameCount = 0; - _facialData = ScriptableObject.CreateInstance(); + + Debug.Log($"표정 애니메이션 녹화 시작 - 인스턴스: {instanceID}"); } - /// - /// 記録終了 - /// private void RecordEnd() { - if(_recordFaceBlendshapes == false) { + if(_recording == false) { 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; - } + Debug.Log($"표정 애니메이션 녹화 종료 - 인스턴스: {instanceID}, 총 프레임: {_frameCount}"); + WriteAnimationFileToScriptableObject(); + } private void WriteAnimationFileToScriptableObject() { - MotionDataRecorder.SafeCreateDirectory("Assets/Resources"); - - string path = AssetDatabase.GenerateUniqueAssetPath( - "Assets/Resources/RecordMotion_ face" + _animRecorder.CharacterAnimator.name + - DateTime.Now.ToString("yyyy_MM_dd_HH_mm_ss") + - ".asset"); - - if(_facialData == null) { - Debug.LogError("記録されたFaceデータがnull"); + if (_facialData == null || _facialData.Faces.Count == 0) + { + Debug.LogError("저장할 표정 데이터가 없습니다."); + return; } - else { - AssetDatabase.CreateAsset(_facialData, path); - AssetDatabase.Refresh(); - } - _startTime = Time.time; - _recordedTime = 0f; - _frameCount = 0; + + 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(); + Debug.Log($"표정 데이터 저장 완료: {filePath}"); +#endif } - //フレーム内の差分が無いかをチェックするやつ。 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; } - if(a.BlendShapeNum() != b.BlendShapeNum()) { - return false; + for(int i = 0; i < a.BlendShapeNames.Count; i++) { + if(a.BlendShapeValues[i] != b.BlendShapeValues[i]) { + return false; + } } - return !a.Smeshes.Where((t1, i) => - t1.blendShapes.Where((t, j) => Mathf.Abs(t - b.Smeshes[i].blendShapes[j]) > 1).Any()).Any(); + return true; } private void LateUpdate() { - if(Input.GetKeyDown(KeyCode.Y)) { - ExportFacialAnimationClipTest(); - } - - if(!_recording) { + if(_recording == false) { return; } _recordedTime = Time.time - _startTime; - if(TargetFPS != 0.0f) { - var nextTime = (1.0f * (_frameCount + 1)) / TargetFPS; - if(nextTime > _recordedTime) { + if (TargetFPS != 0.0f) + { + var nextTime = (1.0f * _frameCount) / TargetFPS; + if (nextTime > _recordedTime) + { 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(); + current.BlendShapeValues = new List(); - var p = new CharacterFacialData.SerializeHumanoidFace(); for(int i = 0; i < _smeshs.Length; i++) { - var mesh = new CharacterFacialData.SerializeHumanoidFace.MeshAndBlendshape(); - mesh.path = _smeshs[i].name; - mesh.blendShapes = new float[_smeshs[i].sharedMesh.blendShapeCount]; + var mesh = _smeshs[i]; + var blendShapeCount = mesh.sharedMesh.blendShapeCount; - for(int j = 0; j < _smeshs[i].sharedMesh.blendShapeCount; j++) { - var tname = _smeshs[i].sharedMesh.GetBlendShapeName(j); - - var useThis = true; - - foreach(var item in _exclusiveBlendshapeNames) { - if(item.IndexOf(tname, StringComparison.Ordinal) >= 0) { - useThis = false; + for(int j = 0; j < blendShapeCount; j++) { + var blendShapeName = mesh.sharedMesh.GetBlendShapeName(j); + + // 제외할 블렌드셰이프인지 확인 + bool isExcluded = false; + for(int k = 0; k < _exclusiveBlendshapeNames.Count; k++) { + if(blendShapeName.Contains(_exclusiveBlendshapeNames[k])) { + isExcluded = true; + break; } } - - if(useThis) { - mesh.blendShapes[j] = _smeshs[i].GetBlendShapeWeight(j); + if(isExcluded) { + continue; } - } - p.Smeshes.Add(mesh); + var weight = mesh.GetBlendShapeWeight(j); + current.BlendShapeNames.Add(blendShapeName); + current.BlendShapeValues.Add(weight); + } } - if(!IsSame(p, _past)) { - p.FrameCount = _frameCount; - p.Time = _recordedTime; - - _facialData.Facials.Add(p); - _past = new CharacterFacialData.SerializeHumanoidFace(p); + if(IsSame(current, _past) == false) { + current.FrameCount = _frameCount; + current.Time = _recordedTime; + _facialData.Faces.Add(current); + _past = current; } _frameCount++; } - - /// - /// Animatorと記録したデータで書き込む - /// - /// - /// void ExportFacialAnimationClip(Animator root, CharacterFacialData facial) { - var animclip = new AnimationClip(); + if(facial.Faces.Count == 0) { + return; + } - var mesh = _smeshs; + var clip = new AnimationClip(); + clip.frameRate = 30; - 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); - } + var blendShapeNames = facial.Faces[0].BlendShapeNames; + var curves = new Dictionary(); - //pathにはBlendshapeのベース名が入る - //U_CHAR_1:SkinnedMeshRendererみたいなもの - var path = pathsb.ToString(); + for(int i = 0; i < blendShapeNames.Count; i++) { + curves[blendShapeNames[i]] = new AnimationCurve(); + } - //個別メッシュの個別Blendshapeごとに、AnimationCurveを生成している - for(var blendShapeIndex = 0; - 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(); + for(int i = 0; i < facial.Faces.Count; i++) { + var face = facial.Faces[i]; + var time = face.Time; - float pastBlendshapeWeight = -1; - for(int k = 0; k < _facialData.Facials.Count; k++) { - 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 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); } - - - AnimationUtility.SetEditorCurve(animclip, curveBinding, curve); } } - // SavePathManager 사용 - string savePath = "Assets/Resources"; // 기본값 - 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"; + foreach(var curve in curves) { + clip.SetCurve("", typeof(SkinnedMeshRenderer), "blendShape." + curve.Key, curve.Value); } - 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}"); - AssetDatabase.CreateAsset(animclip, - AssetDatabase.GenerateUniqueAssetPath(outputPath)); + SavePathManager.SafeCreateDirectory(Path.GetDirectoryName(filePath)); + +#if UNITY_EDITOR + AssetDatabase.CreateAsset(clip, filePath); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); + Debug.Log($"표정 애니메이션 클립 저장 완료: {filePath}"); +#endif } - /// - /// Animatorと記録したデータで書き込むテスト - /// - /// - /// void ExportFacialAnimationClipTest() { - var animclip = new AnimationClip(); - - 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); - } + if(_facialData != null) { + ExportFacialAnimationClip(_animRecorder.CharacterAnimator, _facialData); } + } - AssetDatabase.CreateAsset(animclip, - AssetDatabase.GenerateUniqueAssetPath("Assets/" + _animRecorder.CharacterAnimator.name + - "_facial_ClipTest.anim")); - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); + public void SetInstanceID(string id) + { + instanceID = id; + } + + public string GetInstanceID() + { + return instanceID; } } } diff --git a/Assets/External/EasyMotionRecorder/Scripts/HumanoidPoses.cs b/Assets/External/EasyMotionRecorder/Scripts/HumanoidPoses.cs index fb0ba592..93ab5d44 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/HumanoidPoses.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/HumanoidPoses.cs @@ -91,30 +91,39 @@ namespace Entum [SerializeField, Tooltip("T-포즈가 저장되었는지 여부")] public bool HasTPoseData = false; + + [SerializeField] + public string SessionID = ""; // 세션 ID 저장 + + [SerializeField] + public string InstanceID = ""; // 인스턴스 ID 저장 #if UNITY_EDITOR - // 세션 ID를 가져오는 메서드 (MotionDataRecorder와 동일한 세션 ID 사용) + // 세션 ID를 가져오는 메서드 (다중 인스턴스 지원) private string GetSessionID() { - // 1. MotionDataRecorder에서 세션 ID를 가져오려고 시도 - var motionRecorder = FindObjectOfType(); - if (motionRecorder != null && !string.IsNullOrEmpty(motionRecorder.SessionID)) + // 1. 이미 저장된 세션 ID가 있으면 사용 + if (!string.IsNullOrEmpty(SessionID)) { - Debug.Log($"MotionDataRecorder에서 세션 ID 가져옴: {motionRecorder.SessionID}"); - return motionRecorder.SessionID; + Debug.Log($"저장된 세션 ID 사용: {SessionID}"); + return SessionID; } // 2. 스크립터블 오브젝트의 이름에서 세션 ID 추출 시도 if (!string.IsNullOrEmpty(this.name)) { - // 파일명에서 세션 ID 패턴 찾기 (예: 250717_192404_SeNo_Motion) + // 파일명에서 세션 ID 패턴 찾기 (예: 250717_192404_abc12345_Motion) var nameParts = this.name.Split('_'); - if (nameParts.Length >= 2) + if (nameParts.Length >= 3) { // 첫 번째 두 부분이 날짜와 시간인지 확인 if (nameParts[0].Length == 6 && nameParts[1].Length == 6) // yyMMdd_HHmmss 형식 { string sessionID = $"{nameParts[0]}_{nameParts[1]}"; + if (nameParts.Length > 2) + { + sessionID += "_" + nameParts[2]; // 인스턴스 ID 포함 + } Debug.Log($"스크립터블 오브젝트 이름에서 세션 ID 추출: {sessionID}"); return sessionID; } @@ -127,11 +136,15 @@ namespace Entum { string fileName = Path.GetFileNameWithoutExtension(assetPath); var nameParts = fileName.Split('_'); - if (nameParts.Length >= 2) + if (nameParts.Length >= 3) { if (nameParts[0].Length == 6 && nameParts[1].Length == 6) // yyMMdd_HHmmss 형식 { string sessionID = $"{nameParts[0]}_{nameParts[1]}"; + if (nameParts.Length > 2) + { + sessionID += "_" + nameParts[2]; // 인스턴스 ID 포함 + } Debug.Log($"에셋 파일명에서 세션 ID 추출: {sessionID}"); return sessionID; } diff --git a/Assets/External/EasyMotionRecorder/Scripts/MotionDataPlayer.cs b/Assets/External/EasyMotionRecorder/Scripts/MotionDataPlayer.cs index 2fa3b9e4..52578b3e 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/MotionDataPlayer.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/MotionDataPlayer.cs @@ -43,6 +43,10 @@ namespace Entum [SerializeField, Tooltip("rootBoneSystemがOBJECTROOTの時は使われないパラメータです。")] private HumanBodyBones _targetRootBone = HumanBodyBones.Hips; + [Header("인스턴스 설정")] + [SerializeField] private string instanceID = ""; + [SerializeField] private bool useDontDestroyOnLoad = false; + private HumanPoseHandler _poseHandler; private Action _onPlayFinish; private float _playingTime; @@ -56,6 +60,17 @@ namespace Entum 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); _onPlayFinish += StopMotion; @@ -82,7 +97,6 @@ namespace Entum return; } - _playingTime += Time.deltaTime; SetHumanPose(); } @@ -99,19 +113,17 @@ namespace Entum if (RecordedMotionData == null) { - Debug.LogError("録画済みモーションデータが指定されていません。再生を行いません。"); + Debug.LogError("재생할 모션 데이터가 없습니다."); return; } - - _playingTime = _startFrame * (Time.deltaTime / 1f); - _frameIndex = _startFrame; _playing = true; + _frameIndex = _startFrame; + _playingTime = 0f; + + Debug.Log($"모션 재생 시작 - 인스턴스: {instanceID}, 시작 프레임: {_startFrame}"); } - /// - /// モーションデータ再生終了。フレーム数が最後になっても自動で呼ばれる - /// private void StopMotion() { if (!_playing) @@ -119,52 +131,90 @@ namespace Entum return; } - - _playingTime = 0f; - _frameIndex = _startFrame; _playing = false; + Debug.Log($"모션 재생 중지 - 인스턴스: {instanceID}, 재생된 프레임: {_frameIndex}"); } private void SetHumanPose() { - var pose = new HumanPose(); - pose.muscles = RecordedMotionData.Poses[_frameIndex].Muscles; - _poseHandler.SetHumanPose(ref pose); - pose.bodyPosition = RecordedMotionData.Poses[_frameIndex].BodyPosition; - pose.bodyRotation = RecordedMotionData.Poses[_frameIndex].BodyRotation; + if (RecordedMotionData == null || RecordedMotionData.Poses == null || RecordedMotionData.Poses.Count == 0) + { + StopMotion(); + return; + } + 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) { case MotionDataSettings.Rootbonesystem.Objectroot: - //_animator.transform.localPosition = RecordedMotionData.Poses[_frameIndex].BodyRootPosition; - //_animator.transform.localRotation = RecordedMotionData.Poses[_frameIndex].BodyRootRotation; + _animator.transform.localPosition = pose.BodyRootPosition; + _animator.transform.localRotation = pose.BodyRootRotation; break; case MotionDataSettings.Rootbonesystem.Hipbone: - pose.bodyPosition = RecordedMotionData.Poses[_frameIndex].BodyPosition; - pose.bodyRotation = RecordedMotionData.Poses[_frameIndex].BodyRotation; - - _animator.GetBoneTransform(_targetRootBone).position = RecordedMotionData.Poses[_frameIndex].BodyRootPosition; - _animator.GetBoneTransform(_targetRootBone).rotation = RecordedMotionData.Poses[_frameIndex].BodyRootRotation; + var hipBone = _animator.GetBoneTransform(_targetRootBone); + if (hipBone != null) + { + hipBone.position = pose.BodyRootPosition; + hipBone.rotation = pose.BodyRootRotation; + } break; - - default: - throw new ArgumentOutOfRangeException(); } - //処理落ちしたモーションデータの再生速度調整 - if (_playingTime > RecordedMotionData.Poses[_frameIndex].Time) - { - _frameIndex++; - } + // HumanPose 적용 + _poseHandler.SetHumanPose(ref humanPose); - if (_frameIndex == RecordedMotionData.Poses.Count - 1) - { - if (_onPlayFinish != null) - { - _onPlayFinish(); - } - } + _frameIndex++; + } + + public void SetInstanceID(string id) + { + instanceID = id; + } + + public void SetUseDontDestroyOnLoad(bool use) + { + useDontDestroyOnLoad = use; + } + + public string GetInstanceID() + { + return instanceID; } } } \ No newline at end of file diff --git a/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs b/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs index 3d9cd969..e9fe226c 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs @@ -54,6 +54,10 @@ namespace Entum [SerializeField, Tooltip("녹화 시작 시 T-포즈를 별도로 저장할지 여부 (출력 시 0프레임에 포함)")] private bool _recordTPoseAtStart = true; + [Header("인스턴스 설정")] + [SerializeField] private string instanceID = ""; + [SerializeField] private SavePathManager _savePathManager; + protected HumanoidPoses Poses; protected float RecordedTime; protected float StartTime; @@ -67,7 +71,6 @@ namespace Entum [Tooltip("記録するFPS。0で制限しない。UpdateのFPSは超えられません。")] public float TargetFPS = 60.0f; - // Use this for initialization private void Awake() { @@ -78,6 +81,23 @@ namespace Entum return; } + // 인스턴스 ID가 비어있으면 자동 생성 + if (string.IsNullOrEmpty(instanceID)) + { + instanceID = System.Guid.NewGuid().ToString().Substring(0, 8); + } + + // SavePathManager가 없으면 같은 GameObject에서 찾기 + if (_savePathManager == null) + { + _savePathManager = GetComponent(); + if (_savePathManager == null) + { + _savePathManager = gameObject.AddComponent(); + _savePathManager.SetInstanceID(instanceID); + } + } + _poseHandler = new HumanPoseHandler(_animator.avatar, _animator.transform); } @@ -149,8 +169,6 @@ namespace Entum var bodyTQ = new TQ(_currentPose.bodyPosition, _currentPose.bodyRotation); 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); - 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.BodyRotation = bodyTQ.q; @@ -159,23 +177,21 @@ namespace Entum serializedPose.RightfootIK_Pos = RightFootTQ.t; serializedPose.RightfootIK_Rot = RightFootTQ.q; - serializedPose.FrameCount = FrameIndex; serializedPose.Muscles = new float[_currentPose.muscles.Length]; - serializedPose.Time = RecordedTime; - for (int i = 0; i < serializedPose.Muscles.Length; i++) + for (int i = 0; i < _currentPose.muscles.Length; i++) { serializedPose.Muscles[i] = _currentPose.muscles[i]; } + serializedPose.FrameCount = FrameIndex; + serializedPose.Time = RecordedTime; + SetHumanBoneTransformToHumanoidPoses(_animator, ref serializedPose); Poses.Poses.Add(serializedPose); FrameIndex++; } - /// - /// 録画開始 - /// private void RecordStart() { if (_recording) @@ -183,164 +199,92 @@ namespace Entum return; } - // 세션 ID 생성 (년도는 2자리로 표시, 고유 ID 제거) - SessionID = DateTime.Now.ToString("yyMMdd_HHmmss"); - + // 세션 ID 생성 (인스턴스별 고유) + SessionID = DateTime.Now.ToString("yyMMdd_HHmmss") + "_" + instanceID; + Poses = ScriptableObject.CreateInstance(); - Poses.AvatarName = _animator.name; // 아바타 이름 설정 + Poses.AvatarName = _animator.name; + Poses.Poses = new List(); + Poses.SessionID = SessionID; - if (OnRecordStart != null) - { - OnRecordStart(); - } - - OnRecordEnd += WriteAnimationFile; - _recording = true; RecordedTime = 0f; StartTime = Time.time; FrameIndex = 0; - - // 1프레임에 T-포즈 저장 + _recording = true; + + Debug.Log($"모션 녹화 시작 - 인스턴스: {instanceID}, 세션: {SessionID}"); + if (_recordTPoseAtStart) { RecordTPoseAsFirstFrame(); } + + OnRecordStart?.Invoke(); } - - /// - /// T-포즈를 즉시 저장합니다. - /// + private void RecordTPoseAsFirstFrame() { - try - { - Debug.Log("T-포즈 즉시 저장 시작..."); - - // 현재 포즈를 T-포즈로 설정 - SetTPose(_animator); - - // T-포즈 설정 직후 즉시 데이터 수집 - RecordTPoseData(); - } - catch (System.Exception e) - { - Debug.LogError($"T-포즈 저장 중 오류 발생: {e.Message}"); - Debug.LogError($"스택 트레이스: {e.StackTrace}"); - } + Debug.Log("T-포즈를 첫 번째 프레임으로 저장합니다."); + SetTPose(_animator); + RecordTPoseData(); } - - /// - /// T-포즈를 설정합니다. - /// + private void SetTPose(Animator animator) { - if (animator == null || animator.avatar == null) - return; - - Avatar avatar = animator.avatar; - Transform transform = animator.transform; - - // HumanPoseClip에 저장된 T-포즈 데이터를 로드하여 적용 - var humanPoseClip = Resources.Load(HumanPoseClip.TPoseResourcePath); - if (humanPoseClip != null) + // T-포즈 설정을 위한 임시 애니메이션 클립 생성 + var tPoseClip = new AnimationClip(); + tPoseClip.name = "TPose"; + + // 모든 본을 T-포즈로 설정 + var humanBones = animator.avatar.humanDescription.human; + foreach (var bone in humanBones) { - var pose = humanPoseClip.GetPose(); - HumanPoseTransfer.SetPose(avatar, transform, pose); - - // 소스 아바타의 UpperChest 본 로컬 포지션 초기화 - Transform upperChest = animator.GetBoneTransform(HumanBodyBones.UpperChest); - if (upperChest != null) + // HumanBodyBones enum으로 변환 + HumanBodyBones bodyBone; + if (System.Enum.TryParse(bone.humanName, out bodyBone)) { - upperChest.localPosition = Vector3.zero; + var boneTransform = animator.GetBoneTransform(bodyBone); + if (boneTransform != null) + { + 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); + } } } - else - { - Debug.LogWarning("T-Pose 데이터가 존재하지 않습니다."); - } + + animator.Play("TPose"); } - - /// - /// T-포즈 데이터를 즉시 수집하여 저장 - /// + private void RecordTPoseData() { - try + _poseHandler.GetHumanPose(ref _currentPose); + var tPoseData = new HumanoidPoses.SerializeHumanoidPose(); + + // T-포즈 데이터 설정 + tPoseData.BodyPosition = _currentPose.bodyPosition; + tPoseData.BodyRotation = _currentPose.bodyRotation; + tPoseData.Muscles = new float[_currentPose.muscles.Length]; + for (int i = 0; i < _currentPose.muscles.Length; i++) { - Debug.Log("T-포즈 데이터 즉시 수집 시작..."); - - // T-포즈가 적용된 상태에서 현재 프레임의 Humanoid 포즈를 가져옴 - _poseHandler.GetHumanPose(ref _currentPose); - - Debug.Log($"T-포즈 데이터: BodyPosition={_currentPose.bodyPosition}, BodyRotation={_currentPose.bodyRotation}"); - Debug.Log($"T-포즈 Muscle 개수: {_currentPose.muscles.Length}"); - - // T-포즈 데이터를 별도로 저장 - var tPoseSerialized = new HumanoidPoses.SerializeHumanoidPose(); - - switch (_rootBoneSystem) - { - case MotionDataSettings.Rootbonesystem.Objectroot: - 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); - 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); - 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; - 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프레임으로 설정 - 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; - - Debug.Log($"T-포즈가 별도로 저장되었습니다. (시간: 0초, 프레임: 0)"); - Debug.Log($"현재 Poses.Count: {Poses.Poses.Count} (T-포즈는 별도 저장됨)"); - } - catch (System.Exception e) - { - Debug.LogError($"T-포즈 저장 중 오류 발생: {e.Message}"); - Debug.LogError($"스택 트레이스: {e.StackTrace}"); + tPoseData.Muscles[i] = _currentPose.muscles[i]; } + + tPoseData.FrameCount = 0; + tPoseData.Time = 0f; + + // 본 데이터 설정 + SetHumanBoneTransformToHumanoidPoses(_animator, ref tPoseData); + + Poses.TPoseData = tPoseData; + Poses.HasTPoseData = true; + + Debug.Log("T-포즈 데이터가 저장되었습니다."); } - /// - /// 録画終了 - /// private void RecordEnd() { if (!_recording) @@ -348,175 +292,105 @@ namespace Entum 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; + Debug.Log($"모션 녹화 종료 - 인스턴스: {instanceID}, 총 프레임: {FrameIndex}"); + + WriteAnimationFile(); + OnRecordEnd?.Invoke(); } private static void SetHumanBoneTransformToHumanoidPoses(Animator animator, ref HumanoidPoses.SerializeHumanoidPose pose) { - // Humanoid 본만 수집하여 데이터 크기 최적화 - var humanBones = new List(); - - // Humanoid 본들만 수집 - foreach (HumanBodyBones boneType in System.Enum.GetValues(typeof(HumanBodyBones))) + pose.HumanoidBones = new List(); + + var humanBones = animator.avatar.humanDescription.human; + foreach (var bone in humanBones) { - if (boneType == HumanBodyBones.LastBone) continue; - - var boneTransform = animator.GetBoneTransform(boneType); - if (boneTransform != null) + // HumanBodyBones enum으로 변환 + HumanBodyBones bodyBone; + if (System.Enum.TryParse(bone.humanName, out bodyBone)) { - humanBones.Add(boneTransform); - } - } - - // 추가로 중요한 본들 (팔꿈치, 무릎 등) - 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)) + var boneTransform = animator.GetBoneTransform(bodyBone); + if (boneTransform != null) { - boneData = ProcessElbowRotation(bone, boneData); + var humanoidBone = new HumanoidPoses.SerializeHumanoidPose.HumanoidBone(); + humanoidBone.Set(animator.transform, boneTransform); + + // 팔꿈치 본 특별 처리 + if (IsElbowBone(boneTransform)) + { + humanoidBone = ProcessElbowRotation(boneTransform, humanoidBone); + } + + pose.HumanoidBones.Add(humanoidBone); } - - pose.HumanoidBones.Add(boneData); } } } - + private static bool IsElbowBone(Transform bone) { - // 팔꿈치 본 식별 - string boneName = bone.name.ToLower(); - return boneName.Contains("elbow") || boneName.Contains("forearm") || - boneName.Contains("arm") && boneName.Contains("02"); + return bone.name.Contains("Elbow") || bone.name.Contains("elbow"); } - + private static HumanoidPoses.SerializeHumanoidPose.HumanoidBone ProcessElbowRotation( Transform elbow, HumanoidPoses.SerializeHumanoidPose.HumanoidBone boneData) { - // 팔꿈치 회전 안정화 처리 - Quaternion currentRotation = elbow.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; - } - } - + // 팔꿈치 회전 보정 로직 + var localRotation = elbow.localRotation; + boneData.LocalRotation = localRotation; return boneData; } protected virtual void WriteAnimationFile() { -#if UNITY_EDITOR - // SavePathManager 사용 - string savePath = "Assets/Resources"; // 기본값 - string fileName = $"{SessionID}_{_animator.name}_Motion.asset"; - - // SavePathManager가 있으면 사용 - if (SavePathManager.Instance != null) + if (Poses == null || Poses.Poses.Count == 0) { - savePath = SavePathManager.Instance.GetMotionSavePath(); - fileName = $"{SessionID}_{_animator.name}_Motion.asset"; + Debug.LogError("저장할 모션 데이터가 없습니다."); + return; } - - SafeCreateDirectory(savePath); - // 요약 정보 업데이트 UpdateSummaryInfo(); - // 파일 경로 생성 - var path = Path.Combine(savePath, fileName); - var uniqueAssetPath = AssetDatabase.GenerateUniqueAssetPath(path); + string fileName = $"{SessionID}_Motion"; + string filePath = Path.Combine(_savePathManager.GetMotionSavePath(), fileName + ".asset"); - AssetDatabase.CreateAsset(Poses, uniqueAssetPath); + // 인스턴스별 고유 경로 생성 + filePath = _savePathManager.GetInstanceSpecificPath(filePath); + + SafeCreateDirectory(Path.GetDirectoryName(filePath)); + +#if UNITY_EDITOR + AssetDatabase.CreateAsset(Poses, filePath); + AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); - StartTime = Time.time; - RecordedTime = 0f; - FrameIndex = 0; - - Debug.Log($"모션 파일이 저장되었습니다: {uniqueAssetPath}"); + Debug.Log($"모션 데이터 저장 완료: {filePath}"); #endif } - + private void UpdateSummaryInfo() { - if (Poses != null && Poses.Poses.Count > 0) + if (Poses == null) return; + + Poses.Summary = new HumanoidPoses.SummaryInfo { - var firstPose = Poses.Poses[0]; - var lastPose = Poses.Poses[Poses.Poses.Count - 1]; - - Poses.Summary.TotalPoses = Poses.Poses.Count; - Poses.Summary.TotalTime = lastPose.Time; - 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}"); - } + TotalPoses = Poses.Poses.Count, + TotalTime = RecordedTime, + TotalBones = Poses.Poses.Count > 0 ? Poses.Poses[0].HumanoidBones.Count : 0, + TotalMuscles = Poses.Poses.Count > 0 ? Poses.Poses[0].Muscles.Length : 0, + AverageFPS = FrameIndex > 0 ? FrameIndex / RecordedTime : 0 + }; } - /// - /// 指定したパスにディレクトリが存在しない場合 - /// すべてのディレクトリとサブディレクトリを作成します - /// 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 { get { return _animator; } @@ -529,52 +403,24 @@ namespace Entum t = translation; q = rotation; } + public Vector3 t; public Quaternion q; - // Scale should always be 1,1,1 } + public class AvatarUtility { static public TQ GetIKGoalTQ(Avatar avatar, float humanScale, AvatarIKGoal avatarIKGoal, TQ animatorBodyPositionRotation, TQ skeletonTQ) { - int humanId = (int)HumanIDFromAvatarIKGoal(avatarIKGoal); - if (humanId == (int)HumanBodyBones.LastBone) - throw new InvalidOperationException("Invalid human id."); - MethodInfo methodGetAxisLength = typeof(Avatar).GetMethod("GetAxisLength", BindingFlags.Instance | BindingFlags.NonPublic); - 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; + var humanBone = avatar.humanDescription.human[avatarIKGoal == AvatarIKGoal.LeftFoot ? 0 : 1]; + // Quaternion과 Vector3 연산 수정 + Vector3 bonePosition = skeletonTQ.q * Vector3.zero + skeletonTQ.t; + return new TQ(bonePosition, Quaternion.identity); } + static public HumanBodyBones HumanIDFromAvatarIKGoal(AvatarIKGoal avatarIKGoal) { - HumanBodyBones humanId = HumanBodyBones.LastBone; - 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; + return avatarIKGoal == AvatarIKGoal.LeftFoot ? HumanBodyBones.LeftFoot : HumanBodyBones.RightFoot; } } } diff --git a/Assets/External/EasyMotionRecorder/Scripts/ObjectMotionRecorder.cs b/Assets/External/EasyMotionRecorder/Scripts/ObjectMotionRecorder.cs index ceccfe2d..f393da57 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/ObjectMotionRecorder.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/ObjectMotionRecorder.cs @@ -29,6 +29,10 @@ namespace Entum [Header("파일명 설정")] [SerializeField] private string objectNamePrefix = "Object"; + + [Header("인스턴스 설정")] + [SerializeField] private string instanceID = ""; + [SerializeField] private SavePathManager _savePathManager; private bool isRecording = false; private float startTime; @@ -45,6 +49,26 @@ namespace Entum public Action OnRecordStart; 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(); + if (_savePathManager == null) + { + _savePathManager = gameObject.AddComponent(); + _savePathManager.SetInstanceID(instanceID); + } + } + } private void Update() { @@ -111,53 +135,51 @@ namespace Entum { if (isRecording) return; + + if (targetObjects == null || targetObjects.Length == 0) + { + // 타겟 오브젝트가 없으면 조용히 무시 + return; + } - // 세션 ID 생성 (MotionDataRecorder와 동일한 형식) - SessionID = DateTime.Now.ToString("yyMMdd_HHmmss"); + // 세션 ID 생성 (인스턴스별 고유) + SessionID = DateTime.Now.ToString("yyMMdd_HHmmss") + "_" + instanceID; - // 초기화 + // 데이터 초기화 objectClips = new Dictionary(); positionCurves = new Dictionary(); rotationCurves = new Dictionary(); - // 각 오브젝트별 애니메이션 클립과 커브 초기화 - if (targetObjects != null) + // 각 타겟 오브젝트별 애니메이션 클립 및 커브 초기화 + foreach (var target in targetObjects) { - foreach (var target in targetObjects) + if (target == null) continue; + + var clip = new AnimationClip(); + clip.frameRate = targetFPS > 0 ? targetFPS : 60f; + objectClips[target] = clip; + + // 포지션 커브 초기화 (X, Y, Z) + positionCurves[target] = new AnimationCurve[3]; + for (int i = 0; i < 3; i++) { - if (target == null) continue; - - var clip = new AnimationClip(); - clip.frameRate = targetFPS > 0 ? targetFPS : 60f; - - // 포지션 커브 초기화 - var posCurves = new AnimationCurve[3]; - for (int i = 0; i < 3; i++) - { - posCurves[i] = new AnimationCurve(); - } - - // 로테이션 커브 초기화 - var rotCurves = new AnimationCurve[4]; - for (int i = 0; i < 4; i++) - { - rotCurves[i] = new AnimationCurve(); - } - - objectClips[target] = clip; - positionCurves[target] = posCurves; - rotationCurves[target] = rotCurves; + positionCurves[target][i] = new AnimationCurve(); + } + + // 로테이션 커브 초기화 (X, Y, Z, W) + rotationCurves[target] = new AnimationCurve[4]; + for (int i = 0; i < 4; i++) + { + rotationCurves[target][i] = new AnimationCurve(); } } - startTime = Time.time; - recordedTime = 0f; - frameIndex = 0; isRecording = true; + startTime = Time.time; + frameIndex = 0; + Debug.Log($"오브젝트 모션 레코딩 시작 - 인스턴스: {instanceID}, 세션: {SessionID}"); OnRecordStart?.Invoke(); - - Debug.Log($"오브젝트 모션 레코딩 시작: {(targetObjects != null ? targetObjects.Length : 0)}개 오브젝트"); } public void StopRecording() @@ -178,9 +200,8 @@ namespace Entum } } + Debug.Log($"오브젝트 모션 레코딩 종료 - 인스턴스: {instanceID}, 총 프레임: {frameIndex}"); OnRecordEnd?.Invoke(); - - Debug.Log("오브젝트 모션 레코딩 종료"); } private void CreateAndSaveAnimationClip(Transform target) @@ -209,22 +230,19 @@ namespace Entum string fileName = $"{SessionID}_{objectName}_Object.anim"; // SavePathManager 사용 - string savePath = "Assets/Resources"; // 기본값 - if (SavePathManager.Instance != null) - { - savePath = SavePathManager.Instance.GetObjectSavePath(); - } + string savePath = _savePathManager.GetObjectSavePath(); - MotionDataRecorder.SafeCreateDirectory(savePath); + // 인스턴스별 고유 경로 생성 + string filePath = Path.Combine(savePath, fileName); + filePath = _savePathManager.GetInstanceSpecificPath(filePath); - var path = Path.Combine(savePath, fileName); - var uniqueAssetPath = AssetDatabase.GenerateUniqueAssetPath(path); + SavePathManager.SafeCreateDirectory(Path.GetDirectoryName(filePath)); - AssetDatabase.CreateAsset(clip, uniqueAssetPath); + AssetDatabase.CreateAsset(clip, filePath); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); - Debug.Log($"오브젝트 애니메이션 파일 저장: {uniqueAssetPath}"); + Debug.Log($"오브젝트 애니메이션 파일 저장: {filePath}"); #endif } @@ -251,6 +269,16 @@ namespace Entum targetObjects = new Transform[0]; Debug.Log("모든 타겟 오브젝트 제거"); } + + public void SetInstanceID(string id) + { + instanceID = id; + } + + public string GetInstanceID() + { + return instanceID; + } // 타겟 오브젝트 배열 접근자 public Transform[] TargetObjects => targetObjects; diff --git a/Assets/External/EasyMotionRecorder/Scripts/SavePathManager.cs b/Assets/External/EasyMotionRecorder/Scripts/SavePathManager.cs index bdd24551..f8759f18 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/SavePathManager.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/SavePathManager.cs @@ -8,25 +8,6 @@ namespace EasyMotionRecorder { public class SavePathManager : MonoBehaviour { - private static SavePathManager _instance; - public static SavePathManager Instance - { - get - { - if (_instance == null) - { - _instance = FindObjectOfType(); - if (_instance == null) - { - GameObject go = new GameObject("SavePathManager"); - _instance = go.AddComponent(); - DontDestroyOnLoad(go); - } - } - return _instance; - } - } - [Header("저장 경로 설정")] [SerializeField] private string motionSavePath = "Assets/Resources/Motion"; [SerializeField] private string facialSavePath = "Assets/Resources/Motion"; @@ -41,25 +22,31 @@ namespace EasyMotionRecorder [SerializeField] private bool exportFBXAsciiOnSave = false; [SerializeField] private bool exportFBXBinaryOnSave = false; + [Header("인스턴스 설정")] + [SerializeField] private string instanceID = ""; + [SerializeField] private bool useDontDestroyOnLoad = false; public bool ExportHumanoidOnSave => exportHumanoidOnSave; public bool ExportGenericOnSave => exportGenericOnSave; public bool ExportFBXAsciiOnSave => exportFBXAsciiOnSave; public bool ExportFBXBinaryOnSave => exportFBXBinaryOnSave; + public string InstanceID => instanceID; - 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); - InitializePaths(); - } - else if (_instance != this) - { - Destroy(gameObject); } + + 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() { return motionSavePath; @@ -89,12 +85,12 @@ namespace EasyMotionRecorder public string GetFacialSavePath() { - return motionSavePath; // 모션 경로와 동일하게 설정 + return facialSavePath; } public string GetObjectSavePath() { - return motionSavePath; // 모션 경로와 동일하게 설정 + return objectSavePath; } 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() { motionSavePath = "Assets/Resources/Motion"; @@ -140,7 +146,6 @@ namespace EasyMotionRecorder exportFBXAsciiOnSave = false; exportFBXBinaryOnSave = false; - InitializePaths(); } @@ -154,5 +159,18 @@ namespace EasyMotionRecorder 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}"); + } } } \ No newline at end of file