diff --git a/Assets/External/EasyMotionRecorder/Prefabs/EasyMotionRecorder.prefab b/Assets/External/EasyMotionRecorder/Prefabs/EasyMotionRecorder.prefab
index 063c5f25..14b5a65b 100644
--- a/Assets/External/EasyMotionRecorder/Prefabs/EasyMotionRecorder.prefab
+++ b/Assets/External/EasyMotionRecorder/Prefabs/EasyMotionRecorder.prefab
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6027a03b0246f8eb47eae980363fedfa067ceb1d729b5c7c641bddbf6715a68f
-size 2745
+oid sha256:53a5995d27dbde94885193ff1ddd2869a1bca24985e4d2387aa2a66bdda159a3
+size 3774
diff --git a/Assets/External/websocket-sharp/websocket-sharp.csproj.meta b/Assets/External/EasyMotionRecorder/Scripts/Editor.meta
similarity index 67%
rename from Assets/External/websocket-sharp/websocket-sharp.csproj.meta
rename to Assets/External/EasyMotionRecorder/Scripts/Editor.meta
index 66af5f92..4437bab2 100644
--- a/Assets/External/websocket-sharp/websocket-sharp.csproj.meta
+++ b/Assets/External/EasyMotionRecorder/Scripts/Editor.meta
@@ -1,5 +1,6 @@
fileFormatVersion: 2
-guid: ae0a68acee725e141b02318f249f7990
+guid: 9198c85589520e7489efbcc2812979c1
+folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
diff --git a/Assets/External/EasyMotionRecorder/Scripts/Editor/HumanoidPosesEditor.cs b/Assets/External/EasyMotionRecorder/Scripts/Editor/HumanoidPosesEditor.cs
new file mode 100644
index 00000000..83ed4932
--- /dev/null
+++ b/Assets/External/EasyMotionRecorder/Scripts/Editor/HumanoidPosesEditor.cs
@@ -0,0 +1,500 @@
+using UnityEngine;
+using UnityEditor;
+using System.Linq;
+using System.Collections.Generic;
+
+namespace Entum
+{
+ [CustomEditor(typeof(HumanoidPoses))]
+ public class HumanoidPosesEditor : Editor
+ {
+ // UI 상태 관리
+ private bool _showData = false;
+ private bool _showFrameData = false;
+ private bool _showBoneData = false;
+ private bool _showMuscleData = false;
+ private bool _showIKData = false;
+
+ // 프레임 탐색
+ private int _currentFrameIndex = 0;
+ private Vector2 _scrollPosition;
+
+ // UI 스타일
+ private GUIStyle _cardStyle;
+ private GUIStyle _headerStyle;
+ private GUIStyle _infoStyle;
+ private GUIStyle _buttonStyle;
+ private GUIStyle _sectionStyle;
+
+ // 색상 팔레트
+ private Color _primaryColor = new Color(0.2f, 0.6f, 0.8f);
+ private Color _secondaryColor = new Color(0.3f, 0.3f, 0.3f);
+ private Color _accentColor = new Color(0.8f, 0.4f, 0.2f);
+ private Color _successColor = new Color(0.2f, 0.7f, 0.4f);
+ private Color _warningColor = new Color(0.8f, 0.6f, 0.2f);
+
+ private void OnEnable()
+ {
+ Repaint();
+ }
+
+ private void InitializeStyles()
+ {
+ // 카드 스타일
+ _cardStyle = new GUIStyle();
+ _cardStyle.normal.background = CreateTexture(2, 2, new Color(0.15f, 0.15f, 0.15f, 1f));
+ _cardStyle.padding = new RectOffset(15, 15, 10, 10);
+ _cardStyle.margin = new RectOffset(0, 0, 5, 5);
+
+ // 헤더 스타일
+ _headerStyle = new GUIStyle(EditorStyles.boldLabel);
+ _headerStyle.fontSize = 14;
+ _headerStyle.normal.textColor = Color.white;
+ _headerStyle.margin = new RectOffset(0, 0, 5, 10);
+
+ // 정보 스타일
+ _infoStyle = new GUIStyle(EditorStyles.label);
+ _infoStyle.normal.textColor = new Color(0.8f, 0.8f, 0.8f);
+ _infoStyle.fontSize = 11;
+
+ // 버튼 스타일
+ _buttonStyle = new GUIStyle(EditorStyles.miniButton);
+ _buttonStyle.fontSize = 11;
+ _buttonStyle.padding = new RectOffset(8, 8, 4, 4);
+
+ // 섹션 스타일
+ _sectionStyle = new GUIStyle();
+ _sectionStyle.margin = new RectOffset(0, 0, 8, 8);
+ }
+
+ private Texture2D CreateTexture(int width, int height, Color color)
+ {
+ var texture = new Texture2D(width, height);
+ var pixels = new Color[width * height];
+ for (int i = 0; i < pixels.Length; i++)
+ pixels[i] = color;
+ texture.SetPixels(pixels);
+ texture.Apply();
+ return texture;
+ }
+
+ public override void OnInspectorGUI()
+ {
+ var humanoidPoses = (HumanoidPoses)target;
+
+ // 스타일 초기화 (OnGUI 내에서만 호출)
+ InitializeStyles();
+
+ EditorGUILayout.Space(5);
+
+ // 메인 헤더
+ DrawMainHeader();
+
+ // 데이터 상태 카드
+ DrawDataStatusCard(humanoidPoses);
+
+ // 기본 정보 카드
+ DrawBasicInfoCard(humanoidPoses);
+
+ // 데이터 탐색 섹션
+ if (_showData && humanoidPoses.Poses != null && humanoidPoses.Poses.Count > 0)
+ {
+ DrawFrameNavigationCard(humanoidPoses);
+ DrawDataExplorerCard(humanoidPoses);
+ }
+
+ // 액션 카드
+ DrawActionCard(humanoidPoses);
+
+ EditorGUILayout.Space(10);
+ }
+
+ private void DrawMainHeader()
+ {
+ EditorGUILayout.BeginVertical(_cardStyle);
+
+ EditorGUILayout.BeginHorizontal();
+ GUILayout.FlexibleSpace();
+ EditorGUILayout.LabelField("🎬 Humanoid Poses Viewer", _headerStyle);
+ GUILayout.FlexibleSpace();
+ EditorGUILayout.EndHorizontal();
+
+ EditorGUILayout.LabelField("휴머노이드 애니메이션 데이터 뷰어", _infoStyle, GUILayout.Height(16));
+
+ EditorGUILayout.EndVertical();
+ }
+
+ private void DrawDataStatusCard(HumanoidPoses humanoidPoses)
+ {
+ EditorGUILayout.BeginVertical(_cardStyle);
+
+ EditorGUILayout.LabelField("📊 데이터 상태", _headerStyle);
+
+ if (humanoidPoses.Poses != null && humanoidPoses.Poses.Count > 0)
+ {
+ EditorGUILayout.LabelField($"✅ {humanoidPoses.Poses.Count}개의 포즈 데이터 로드됨", _infoStyle);
+
+ EditorGUILayout.Space(5);
+
+ // 명확한 토글 버튼
+ EditorGUILayout.BeginHorizontal();
+ EditorGUILayout.LabelField("데이터 탐색:", _infoStyle, GUILayout.Width(80));
+
+ var oldColor = GUI.backgroundColor;
+ GUI.backgroundColor = _showData ? _successColor : _secondaryColor;
+
+ if (GUILayout.Button(_showData ? "🔽 숨기기" : "🔼 보기", GUILayout.Width(80)))
+ {
+ _showData = !_showData;
+ }
+
+ GUI.backgroundColor = oldColor;
+ EditorGUILayout.EndHorizontal();
+ }
+ else
+ {
+ EditorGUILayout.LabelField("❌ 데이터가 없습니다", _infoStyle);
+ }
+
+ EditorGUILayout.EndVertical();
+ }
+
+ private void DrawBasicInfoCard(HumanoidPoses humanoidPoses)
+ {
+ EditorGUILayout.BeginVertical(_cardStyle);
+
+ EditorGUILayout.LabelField("📈 기본 정보", _headerStyle);
+
+ if (humanoidPoses.Poses != null && humanoidPoses.Poses.Count > 0)
+ {
+ var firstPose = humanoidPoses.Poses[0];
+ var lastPose = humanoidPoses.Poses[humanoidPoses.Poses.Count - 1];
+
+ DrawInfoRow("🎭 총 포즈 수", humanoidPoses.Poses.Count.ToString());
+ DrawInfoRow("⏱️ 총 시간", $"{lastPose.Time:F2}초");
+ DrawInfoRow("🦴 본 수", firstPose.HumanoidBones.Count.ToString());
+ DrawInfoRow("💪 근육 수", firstPose.Muscles.Length.ToString());
+
+ float avgFPS = humanoidPoses.Poses.Count / lastPose.Time;
+ DrawInfoRow("🎬 평균 FPS", $"{avgFPS:F1}");
+
+ float fileSize = EstimateFileSize(humanoidPoses);
+ DrawInfoRow("💾 예상 크기", $"{fileSize:F1}KB");
+ }
+ else
+ {
+ EditorGUILayout.LabelField("데이터가 없습니다", _infoStyle);
+ }
+
+ EditorGUILayout.EndVertical();
+ }
+
+ private void DrawInfoRow(string label, string value)
+ {
+ EditorGUILayout.BeginHorizontal();
+ EditorGUILayout.LabelField(label, _infoStyle, GUILayout.Width(100));
+ EditorGUILayout.LabelField(value, EditorStyles.boldLabel);
+ EditorGUILayout.EndHorizontal();
+ }
+
+ private void DrawFrameNavigationCard(HumanoidPoses humanoidPoses)
+ {
+ EditorGUILayout.BeginVertical(_cardStyle);
+
+ EditorGUILayout.LabelField("🎯 프레임 탐색", _headerStyle);
+
+ var currentPose = humanoidPoses.Poses[_currentFrameIndex];
+
+ // 프레임 슬라이더
+ EditorGUILayout.BeginHorizontal();
+ EditorGUILayout.LabelField("현재 프레임", _infoStyle, GUILayout.Width(80));
+ _currentFrameIndex = EditorGUILayout.IntSlider(_currentFrameIndex, 0, humanoidPoses.Poses.Count - 1);
+ EditorGUILayout.LabelField($"{_currentFrameIndex + 1}/{humanoidPoses.Poses.Count}", _infoStyle, GUILayout.Width(50));
+ EditorGUILayout.EndHorizontal();
+
+ // 프레임 정보
+ EditorGUILayout.Space(5);
+ DrawInfoRow("⏱️ 시간", $"{currentPose.Time:F3}초");
+ DrawInfoRow("🎬 프레임", currentPose.FrameCount.ToString());
+
+ // 네비게이션 버튼들
+ EditorGUILayout.Space(8);
+ EditorGUILayout.BeginHorizontal();
+
+ var oldColor = GUI.backgroundColor;
+
+ GUI.backgroundColor = _primaryColor;
+ if (GUILayout.Button("⏮️ 첫"))
+ _currentFrameIndex = 0;
+
+ GUI.backgroundColor = _secondaryColor;
+ if (GUILayout.Button("◀ 이전"))
+ _currentFrameIndex = Mathf.Max(0, _currentFrameIndex - 1);
+
+ if (GUILayout.Button("다음 ▶"))
+ _currentFrameIndex = Mathf.Min(humanoidPoses.Poses.Count - 1, _currentFrameIndex + 1);
+
+ GUI.backgroundColor = _accentColor;
+ if (GUILayout.Button("마지막 ⏭️"))
+ _currentFrameIndex = humanoidPoses.Poses.Count - 1;
+
+ GUI.backgroundColor = oldColor;
+ EditorGUILayout.EndHorizontal();
+
+ EditorGUILayout.EndVertical();
+ }
+
+ private void DrawDataExplorerCard(HumanoidPoses humanoidPoses)
+ {
+ EditorGUILayout.BeginVertical(_cardStyle);
+
+ EditorGUILayout.LabelField("🔍 데이터 탐색", _headerStyle);
+
+ var currentPose = humanoidPoses.Poses[_currentFrameIndex];
+
+ // 프레임 데이터
+ _showFrameData = EditorGUILayout.Foldout(_showFrameData, "📐 프레임 데이터", true);
+ if (_showFrameData)
+ {
+ EditorGUI.indentLevel++;
+
+ EditorGUILayout.LabelField("바디 루트", EditorStyles.boldLabel);
+ EditorGUILayout.Vector3Field("위치", currentPose.BodyRootPosition);
+ EditorGUILayout.Vector4Field("회전", new Vector4(currentPose.BodyRootRotation.x, currentPose.BodyRootRotation.y, currentPose.BodyRootRotation.z, currentPose.BodyRootRotation.w));
+
+ EditorGUILayout.Space();
+
+ EditorGUILayout.LabelField("바디", EditorStyles.boldLabel);
+ EditorGUILayout.Vector3Field("위치", currentPose.BodyPosition);
+ EditorGUILayout.Vector4Field("회전", new Vector4(currentPose.BodyRotation.x, currentPose.BodyRotation.y, currentPose.BodyRotation.z, currentPose.BodyRotation.w));
+
+ EditorGUI.indentLevel--;
+ }
+
+ // 본 데이터
+ _showBoneData = EditorGUILayout.Foldout(_showBoneData, "🦴 본 데이터", true);
+ if (_showBoneData)
+ {
+ EditorGUI.indentLevel++;
+
+ _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition, GUILayout.Height(200));
+
+ for (int i = 0; i < currentPose.HumanoidBones.Count; i++)
+ {
+ var bone = currentPose.HumanoidBones[i];
+ EditorGUILayout.LabelField($"본 {i}: {bone.Name}", EditorStyles.boldLabel);
+ EditorGUILayout.Vector3Field(" 위치", bone.LocalPosition);
+ EditorGUILayout.Vector4Field(" 회전", new Vector4(bone.LocalRotation.x, bone.LocalRotation.y, bone.LocalRotation.z, bone.LocalRotation.w));
+ EditorGUILayout.Space();
+ }
+
+ EditorGUILayout.EndScrollView();
+
+ EditorGUI.indentLevel--;
+ }
+
+ // 근육 데이터
+ _showMuscleData = EditorGUILayout.Foldout(_showMuscleData, "💪 근육 데이터", true);
+ if (_showMuscleData)
+ {
+ EditorGUI.indentLevel++;
+
+ _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition, GUILayout.Height(200));
+
+ for (int i = 0; i < currentPose.Muscles.Length; i++)
+ {
+ EditorGUILayout.LabelField($"근육 {i}: {currentPose.Muscles[i]:F3}");
+ }
+
+ EditorGUILayout.EndScrollView();
+
+ EditorGUI.indentLevel--;
+ }
+
+ // IK 데이터
+ _showIKData = EditorGUILayout.Foldout(_showIKData, "🎯 IK 데이터", true);
+ if (_showIKData)
+ {
+ EditorGUI.indentLevel++;
+
+ EditorGUILayout.LabelField("왼발 IK", EditorStyles.boldLabel);
+ EditorGUILayout.Vector3Field("위치", currentPose.LeftfootIK_Pos);
+ EditorGUILayout.Vector4Field("회전", new Vector4(currentPose.LeftfootIK_Rot.x, currentPose.LeftfootIK_Rot.y, currentPose.LeftfootIK_Rot.z, currentPose.LeftfootIK_Rot.w));
+
+ EditorGUILayout.Space();
+
+ EditorGUILayout.LabelField("오른발 IK", EditorStyles.boldLabel);
+ EditorGUILayout.Vector3Field("위치", currentPose.RightfootIK_Pos);
+ EditorGUILayout.Vector4Field("회전", new Vector4(currentPose.RightfootIK_Rot.x, currentPose.RightfootIK_Rot.y, currentPose.RightfootIK_Rot.z, currentPose.RightfootIK_Rot.w));
+
+ EditorGUI.indentLevel--;
+ }
+
+ EditorGUILayout.EndVertical();
+ }
+
+ private void DrawActionCard(HumanoidPoses humanoidPoses)
+ {
+ EditorGUILayout.BeginVertical(_cardStyle);
+
+ EditorGUILayout.LabelField("⚡ 액션", _headerStyle);
+
+ // 첫 번째 행
+ EditorGUILayout.BeginHorizontal();
+
+ var oldColor = GUI.backgroundColor;
+
+ GUI.backgroundColor = _primaryColor;
+ if (GUILayout.Button("🔍 기본 인스펙터"))
+ {
+ EditorGUIUtility.ExitGUI();
+ return;
+ }
+
+ GUI.backgroundColor = _warningColor;
+ if (GUILayout.Button("📊 데이터 통계"))
+ {
+ ShowDataStatistics(humanoidPoses);
+ }
+
+ GUI.backgroundColor = oldColor;
+ EditorGUILayout.EndHorizontal();
+
+ // 두 번째 행
+ EditorGUILayout.BeginHorizontal();
+
+ GUI.backgroundColor = _successColor;
+ if (GUILayout.Button("🎬 Generic 출력"))
+ {
+ ExportGenericAnimation(humanoidPoses);
+ }
+
+ GUI.backgroundColor = new Color(0.7f, 0.3f, 0.8f);
+ if (GUILayout.Button("🎭 Humanoid 출력"))
+ {
+ ExportHumanoidAnimation(humanoidPoses);
+ }
+
+ GUI.backgroundColor = oldColor;
+ EditorGUILayout.EndHorizontal();
+
+ // 세 번째 행
+ EditorGUILayout.BeginHorizontal();
+
+ GUI.backgroundColor = _accentColor;
+ if (GUILayout.Button("💾 메모리 사용량"))
+ {
+ ShowMemoryUsage(humanoidPoses);
+ }
+
+ GUI.backgroundColor = _primaryColor;
+ if (GUILayout.Button("🔄 에셋 새로고침"))
+ {
+ RefreshAsset(humanoidPoses);
+ }
+
+ GUI.backgroundColor = oldColor;
+ EditorGUILayout.EndHorizontal();
+
+ EditorGUILayout.EndVertical();
+ }
+
+ private void ShowDataStatistics(HumanoidPoses humanoidPoses)
+ {
+ if (humanoidPoses.Poses == null || humanoidPoses.Poses.Count == 0)
+ {
+ EditorUtility.DisplayDialog("통계", "데이터가 없습니다.", "확인");
+ return;
+ }
+
+ var firstPose = humanoidPoses.Poses[0];
+ var lastPose = humanoidPoses.Poses[humanoidPoses.Poses.Count - 1];
+
+ string stats = $"총 포즈 수: {humanoidPoses.Poses.Count}\n" +
+ $"총 시간: {lastPose.Time:F2}초\n" +
+ $"본 수: {firstPose.HumanoidBones.Count}\n" +
+ $"근육 수: {firstPose.Muscles.Length}\n" +
+ $"평균 FPS: {humanoidPoses.Poses.Count / lastPose.Time:F1}\n" +
+ $"예상 파일 크기: {EstimateFileSize(humanoidPoses):F1}KB";
+
+ EditorUtility.DisplayDialog("데이터 통계", stats, "확인");
+ }
+
+ private void ShowMemoryUsage(HumanoidPoses humanoidPoses)
+ {
+ if (humanoidPoses.Poses == null || humanoidPoses.Poses.Count == 0)
+ {
+ EditorUtility.DisplayDialog("메모리 사용량", "데이터가 없습니다.", "확인");
+ return;
+ }
+
+ var firstPose = humanoidPoses.Poses[0];
+ long estimatedMemory = EstimateMemoryUsage(humanoidPoses);
+
+ string memoryInfo = $"예상 메모리 사용량: {estimatedMemory / 1024:F1}KB\n" +
+ $"포즈당 메모리: {estimatedMemory / humanoidPoses.Poses.Count / 1024:F1}KB\n" +
+ $"본당 메모리: {estimatedMemory / humanoidPoses.Poses.Count / firstPose.HumanoidBones.Count:F1}바이트";
+
+ EditorUtility.DisplayDialog("메모리 사용량", memoryInfo, "확인");
+ }
+
+ private void ExportGenericAnimation(HumanoidPoses humanoidPoses)
+ {
+ if (humanoidPoses.Poses == null || humanoidPoses.Poses.Count == 0)
+ {
+ EditorUtility.DisplayDialog("출력", "출력할 데이터가 없습니다.", "확인");
+ return;
+ }
+
+ humanoidPoses.ExportGenericAnim();
+ EditorUtility.DisplayDialog("출력 완료", "Generic 애니메이션이 출력되었습니다.", "확인");
+ }
+
+ private void ExportHumanoidAnimation(HumanoidPoses humanoidPoses)
+ {
+ if (humanoidPoses.Poses == null || humanoidPoses.Poses.Count == 0)
+ {
+ EditorUtility.DisplayDialog("출력", "출력할 데이터가 없습니다.", "확인");
+ return;
+ }
+
+ humanoidPoses.ExportHumanoidAnim();
+ EditorUtility.DisplayDialog("출력 완료", "Humanoid 애니메이션이 출력되었습니다.", "확인");
+ }
+
+ private float EstimateFileSize(HumanoidPoses humanoidPoses)
+ {
+ if (humanoidPoses.Poses == null || humanoidPoses.Poses.Count == 0) return 0;
+
+ var firstPose = humanoidPoses.Poses[0];
+ int poseSize = 4 * 3 + 4 * 4 + 4 * 3 + 4 * 4 + 4 * 3 + 4 * 4 + 4 + 4 + 4;
+ int boneSize = (4 * 3 + 4 * 4 + 50) * firstPose.HumanoidBones.Count;
+ int muscleSize = 4 * firstPose.Muscles.Length;
+
+ return (poseSize + boneSize + muscleSize) * humanoidPoses.Poses.Count / 1024f;
+ }
+
+ private long EstimateMemoryUsage(HumanoidPoses humanoidPoses)
+ {
+ if (humanoidPoses.Poses == null || humanoidPoses.Poses.Count == 0) return 0;
+
+ var firstPose = humanoidPoses.Poses[0];
+ long poseSize = 4 * 3 + 4 * 4 + 4 * 3 + 4 * 4 + 4 * 3 + 4 * 4 + 4 + 4 + 4;
+ long boneSize = (4 * 3 + 4 * 4 + 50) * firstPose.HumanoidBones.Count;
+ long muscleSize = 4 * firstPose.Muscles.Length;
+
+ return (poseSize + boneSize + muscleSize) * humanoidPoses.Poses.Count;
+ }
+
+ private void RefreshAsset(HumanoidPoses humanoidPoses)
+ {
+ var assetPath = AssetDatabase.GetAssetPath(humanoidPoses);
+ if (!string.IsNullOrEmpty(assetPath))
+ {
+ AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate);
+ Repaint();
+ EditorUtility.DisplayDialog("에셋 새로고침", "에셋이 새로고침되었습니다.", "확인");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/External/EasyMotionRecorder/Scripts/Editor/HumanoidPosesEditor.cs.meta b/Assets/External/EasyMotionRecorder/Scripts/Editor/HumanoidPosesEditor.cs.meta
new file mode 100644
index 00000000..f61e8879
--- /dev/null
+++ b/Assets/External/EasyMotionRecorder/Scripts/Editor/HumanoidPosesEditor.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 0b82188ca059a2b4ab954e1715a6ae3f
\ No newline at end of file
diff --git a/Assets/External/EasyMotionRecorder/Scripts/Editor/ObjectMotionRecorderEditor.cs b/Assets/External/EasyMotionRecorder/Scripts/Editor/ObjectMotionRecorderEditor.cs
new file mode 100644
index 00000000..95fb6cdb
--- /dev/null
+++ b/Assets/External/EasyMotionRecorder/Scripts/Editor/ObjectMotionRecorderEditor.cs
@@ -0,0 +1,234 @@
+#if UNITY_EDITOR
+using UnityEngine;
+using UnityEditor;
+using Entum;
+
+namespace EasyMotionRecorder
+{
+ [CustomEditor(typeof(ObjectMotionRecorder))]
+ public class ObjectMotionRecorderEditor : Editor
+ {
+ private ObjectMotionRecorder recorder;
+ private bool showTargetSettings = true;
+ private bool showRecordingSettings = true;
+
+ private void OnEnable()
+ {
+ recorder = (ObjectMotionRecorder)target;
+ }
+
+ public override void OnInspectorGUI()
+ {
+ serializedObject.Update();
+
+ EditorGUILayout.Space();
+ EditorGUILayout.LabelField("오브젝트 모션 레코더", EditorStyles.boldLabel);
+ EditorGUILayout.Space();
+
+ // 레코딩 상태 표시
+ DrawRecordingStatus();
+
+ EditorGUILayout.Space();
+
+ // 레코딩 설정
+ DrawRecordingSettings();
+
+ EditorGUILayout.Space();
+
+ // 타겟 오브젝트 관리
+ DrawTargetSettings();
+
+ EditorGUILayout.Space();
+
+ // 액션 버튼들
+ DrawActionButtons();
+
+ serializedObject.ApplyModifiedProperties();
+ }
+
+ private void DrawRecordingStatus()
+ {
+ EditorGUILayout.BeginHorizontal();
+ EditorGUILayout.LabelField("레코딩 상태:", GUILayout.Width(100));
+
+ if (recorder.IsRecording)
+ {
+ EditorGUILayout.LabelField("● 녹화 중", EditorStyles.boldLabel);
+ EditorGUILayout.LabelField($"시간: {recorder.RecordedTime:F2}초", GUILayout.Width(120));
+ }
+ else
+ {
+ EditorGUILayout.LabelField("○ 대기 중", EditorStyles.boldLabel);
+ }
+ EditorGUILayout.EndHorizontal();
+ }
+
+ private void DrawRecordingSettings()
+ {
+ showRecordingSettings = EditorGUILayout.Foldout(showRecordingSettings, "레코딩 설정");
+
+ if (showRecordingSettings)
+ {
+ EditorGUI.indentLevel++;
+
+ // 키 설정
+ var startKeyProp = serializedObject.FindProperty("recordStartKey");
+ var stopKeyProp = serializedObject.FindProperty("recordStopKey");
+
+ startKeyProp.enumValueIndex = EditorGUILayout.Popup("시작 키", startKeyProp.enumValueIndex, startKeyProp.enumDisplayNames);
+ stopKeyProp.enumValueIndex = EditorGUILayout.Popup("정지 키", stopKeyProp.enumValueIndex, stopKeyProp.enumDisplayNames);
+
+ // FPS 설정
+ var fpsProp = serializedObject.FindProperty("targetFPS");
+ fpsProp.floatValue = EditorGUILayout.FloatField("타겟 FPS", fpsProp.floatValue);
+
+ if (fpsProp.floatValue <= 0)
+ {
+ EditorGUILayout.HelpBox("FPS가 0 이하면 제한 없이 녹화됩니다.", MessageType.Info);
+ }
+
+ EditorGUI.indentLevel--;
+ }
+ }
+
+ private void DrawTargetSettings()
+ {
+ showTargetSettings = EditorGUILayout.Foldout(showTargetSettings, "타겟 오브젝트 관리");
+
+ if (showTargetSettings)
+ {
+ EditorGUI.indentLevel++;
+
+ var targetsProp = serializedObject.FindProperty("targetObjects");
+
+ EditorGUILayout.BeginHorizontal();
+ EditorGUILayout.LabelField($"타겟 오브젝트 ({targetsProp.arraySize}개)", EditorStyles.boldLabel);
+
+ if (GUILayout.Button("선택된 오브젝트 추가", GUILayout.Width(150)))
+ {
+ AddSelectedObject();
+ }
+ EditorGUILayout.EndHorizontal();
+
+ EditorGUILayout.Space();
+
+ // 타겟 오브젝트 리스트
+ for (int i = 0; i < targetsProp.arraySize; i++)
+ {
+ EditorGUILayout.BeginHorizontal();
+
+ var elementProp = targetsProp.GetArrayElementAtIndex(i);
+ EditorGUILayout.PropertyField(elementProp, GUIContent.none);
+
+ if (GUILayout.Button("제거", GUILayout.Width(60)))
+ {
+ targetsProp.DeleteArrayElementAtIndex(i);
+ break;
+ }
+
+ EditorGUILayout.EndHorizontal();
+ }
+
+ if (targetsProp.arraySize == 0)
+ {
+ EditorGUILayout.HelpBox("타겟 오브젝트가 없습니다. '선택된 오브젝트 추가' 버튼을 사용하거나 직접 추가해주세요.", MessageType.Warning);
+ }
+
+ EditorGUILayout.Space();
+
+ // 빠른 액션 버튼들
+ EditorGUILayout.BeginHorizontal();
+ if (GUILayout.Button("모든 타겟 제거"))
+ {
+ if (EditorUtility.DisplayDialog("타겟 제거", "모든 타겟 오브젝트를 제거하시겠습니까?", "확인", "취소"))
+ {
+ targetsProp.ClearArray();
+ }
+ }
+
+ if (GUILayout.Button("선택된 오브젝트들 추가"))
+ {
+ AddSelectedObjects();
+ }
+ EditorGUILayout.EndHorizontal();
+
+ EditorGUI.indentLevel--;
+ }
+ }
+
+ private void DrawActionButtons()
+ {
+ EditorGUILayout.LabelField("액션", EditorStyles.boldLabel);
+
+ EditorGUILayout.BeginHorizontal();
+
+ if (recorder.IsRecording)
+ {
+ GUI.enabled = false;
+ EditorGUILayout.HelpBox("녹화 중입니다. 정지 키를 눌러주세요.", MessageType.Info);
+ GUI.enabled = true;
+ }
+ else
+ {
+ if (GUILayout.Button("레코딩 시작", GUILayout.Height(30)))
+ {
+ if (recorder.TargetObjects.Length == 0)
+ {
+ EditorUtility.DisplayDialog("오류", "타겟 오브젝트가 설정되지 않았습니다.", "확인");
+ return;
+ }
+
+ recorder.StartRecording();
+ }
+ }
+
+ if (GUILayout.Button("설정 새로고침", GUILayout.Height(30)))
+ {
+ EditorUtility.SetDirty(recorder);
+ }
+
+ EditorGUILayout.EndHorizontal();
+ }
+
+ private void AddSelectedObject()
+ {
+ var selected = Selection.activeGameObject;
+ if (selected != null)
+ {
+ var targetsProp = serializedObject.FindProperty("targetObjects");
+ targetsProp.arraySize++;
+ var newElement = targetsProp.GetArrayElementAtIndex(targetsProp.arraySize - 1);
+ newElement.objectReferenceValue = selected.transform;
+
+ Debug.Log($"오브젝트 추가: {selected.name}");
+ }
+ else
+ {
+ EditorUtility.DisplayDialog("오류", "선택된 오브젝트가 없습니다.", "확인");
+ }
+ }
+
+ private void AddSelectedObjects()
+ {
+ var selectedObjects = Selection.gameObjects;
+ if (selectedObjects.Length == 0)
+ {
+ EditorUtility.DisplayDialog("오류", "선택된 오브젝트가 없습니다.", "확인");
+ return;
+ }
+
+ var targetsProp = serializedObject.FindProperty("targetObjects");
+ int startIndex = targetsProp.arraySize;
+ targetsProp.arraySize += selectedObjects.Length;
+
+ for (int i = 0; i < selectedObjects.Length; i++)
+ {
+ var element = targetsProp.GetArrayElementAtIndex(startIndex + i);
+ element.objectReferenceValue = selectedObjects[i].transform;
+ }
+
+ Debug.Log($"{selectedObjects.Length}개 오브젝트 추가됨");
+ }
+ }
+}
+#endif
\ No newline at end of file
diff --git a/Assets/External/EasyMotionRecorder/Scripts/Editor/ObjectMotionRecorderEditor.cs.meta b/Assets/External/EasyMotionRecorder/Scripts/Editor/ObjectMotionRecorderEditor.cs.meta
new file mode 100644
index 00000000..cf180b4f
--- /dev/null
+++ b/Assets/External/EasyMotionRecorder/Scripts/Editor/ObjectMotionRecorderEditor.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 668795823d8b9124fba6f34bd1e32f35
\ No newline at end of file
diff --git a/Assets/External/EasyMotionRecorder/Scripts/Editor/SavePathManagerEditor.cs b/Assets/External/EasyMotionRecorder/Scripts/Editor/SavePathManagerEditor.cs
new file mode 100644
index 00000000..121fbd6a
--- /dev/null
+++ b/Assets/External/EasyMotionRecorder/Scripts/Editor/SavePathManagerEditor.cs
@@ -0,0 +1,129 @@
+#if UNITY_EDITOR
+using UnityEngine;
+using UnityEditor;
+using System.IO;
+
+namespace EasyMotionRecorder
+{
+ [CustomEditor(typeof(SavePathManager))]
+ public class SavePathManagerEditor : Editor
+ {
+ private SavePathManager savePathManager;
+ private bool showAdvancedSettings = false;
+
+ private void OnEnable()
+ {
+ savePathManager = (SavePathManager)target;
+ }
+
+ public override void OnInspectorGUI()
+ {
+ serializedObject.Update();
+
+ EditorGUILayout.Space();
+ EditorGUILayout.LabelField("저장 경로 관리", EditorStyles.boldLabel);
+ EditorGUILayout.Space();
+
+ // 기본 설정
+ DrawBasicSettings();
+
+ EditorGUILayout.Space();
+
+ // 고급 설정
+ DrawAdvancedSettings();
+
+ EditorGUILayout.Space();
+
+ // 버튼들
+ DrawActionButtons();
+
+ serializedObject.ApplyModifiedProperties();
+ }
+
+ private void DrawBasicSettings()
+ {
+ EditorGUILayout.LabelField("기본 설정", EditorStyles.boldLabel);
+
+ // 통합 저장 경로 (모든 파일이 같은 위치에 저장됨)
+ EditorGUILayout.BeginHorizontal();
+ string motionPath = EditorGUILayout.TextField("저장 경로", savePathManager.GetMotionSavePath());
+ if (GUILayout.Button("폴더 선택", GUILayout.Width(80)))
+ {
+ string newPath = EditorUtility.OpenFolderPanel("저장 폴더 선택", "Assets", "");
+ if (!string.IsNullOrEmpty(newPath))
+ {
+ // Assets 폴더 기준으로 상대 경로로 변환
+ if (newPath.StartsWith(Application.dataPath))
+ {
+ newPath = "Assets" + newPath.Substring(Application.dataPath.Length);
+ }
+ savePathManager.SetMotionSavePath(newPath);
+ }
+ }
+ EditorGUILayout.EndHorizontal();
+
+ EditorGUILayout.HelpBox("모션, 페이스, 제네릭 애니메이션 파일이 모두 이 경로에 저장됩니다.", MessageType.Info);
+ }
+
+ private void DrawAdvancedSettings()
+ {
+ showAdvancedSettings = EditorGUILayout.Foldout(showAdvancedSettings, "고급 설정");
+
+ if (showAdvancedSettings)
+ {
+ EditorGUI.indentLevel++;
+
+ // 서브디렉토리 생성 여부
+ bool createSubdirectories = EditorGUILayout.Toggle("서브디렉토리 자동 생성",
+ serializedObject.FindProperty("createSubdirectories").boolValue);
+ serializedObject.FindProperty("createSubdirectories").boolValue = createSubdirectories;
+
+ EditorGUILayout.HelpBox("현재 모든 파일이 동일한 경로에 저장됩니다.", MessageType.Info);
+
+ EditorGUI.indentLevel--;
+ }
+ }
+
+ private void DrawActionButtons()
+ {
+ EditorGUILayout.LabelField("작업", EditorStyles.boldLabel);
+
+ EditorGUILayout.BeginHorizontal();
+
+ // 기본값으로 리셋 버튼
+ if (GUILayout.Button("기본값으로 리셋", GUILayout.Height(30)))
+ {
+ if (EditorUtility.DisplayDialog("기본값으로 리셋",
+ "모든 설정을 기본값으로 되돌리시겠습니까?", "확인", "취소"))
+ {
+ savePathManager.ResetToDefaults();
+ EditorUtility.SetDirty(savePathManager);
+ }
+ }
+
+ // 폴더 열기 버튼
+ if (GUILayout.Button("저장 폴더 열기", GUILayout.Height(30)))
+ {
+ string path = savePathManager.GetMotionSavePath();
+ if (Directory.Exists(path))
+ {
+ EditorUtility.RevealInFinder(path);
+ }
+ else
+ {
+ EditorUtility.DisplayDialog("오류", "저장 폴더가 존재하지 않습니다.", "확인");
+ }
+ }
+
+ EditorGUILayout.EndHorizontal();
+
+ EditorGUILayout.Space();
+ EditorGUILayout.LabelField("자동 출력 옵션", EditorStyles.boldLabel);
+ var humanoidProp = serializedObject.FindProperty("exportHumanoidOnSave");
+ var genericProp = serializedObject.FindProperty("exportGenericOnSave");
+ humanoidProp.boolValue = EditorGUILayout.ToggleLeft("휴머노이드 애니메이션 자동 출력", humanoidProp.boolValue);
+ genericProp.boolValue = EditorGUILayout.ToggleLeft("제네릭 애니메이션 자동 출력", genericProp.boolValue);
+ }
+ }
+}
+#endif
\ No newline at end of file
diff --git a/Assets/External/EasyMotionRecorder/Scripts/Editor/SavePathManagerEditor.cs.meta b/Assets/External/EasyMotionRecorder/Scripts/Editor/SavePathManagerEditor.cs.meta
new file mode 100644
index 00000000..1d7b6218
--- /dev/null
+++ b/Assets/External/EasyMotionRecorder/Scripts/Editor/SavePathManagerEditor.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 3381156202fae3546b3cdc3f7cf501e6
\ No newline at end of file
diff --git a/Assets/External/EasyMotionRecorder/Scripts/ExpressionRecorder.cs b/Assets/External/EasyMotionRecorder/Scripts/ExpressionRecorder.cs
new file mode 100644
index 00000000..26a47163
--- /dev/null
+++ b/Assets/External/EasyMotionRecorder/Scripts/ExpressionRecorder.cs
@@ -0,0 +1,105 @@
+using UnityEngine;
+
+public class ExpressionRecorder : MonoBehaviour
+{
+ public SkinnedMeshRenderer targetRenderer;
+ public float recordingInterval = 0.1f;
+ public AnimationClip animationClip;
+ public KeyCode startRecordingKey = KeyCode.R;
+ public KeyCode stopRecordingKey = KeyCode.X;
+
+ private float recordingTimer;
+ private AnimationCurve[] blendShapeCurves;
+ private bool isRecording;
+
+ private void Start()
+ {
+ if (targetRenderer == null || animationClip == null)
+ {
+ Debug.LogError("Required components/variables are not assigned.");
+ enabled = false;
+ return;
+ }
+
+ // Retrieve blend shape names from the target renderer
+ string[] blendShapeNames = targetRenderer.sharedMesh.blendShapeCount > 0 ? new string[targetRenderer.sharedMesh.blendShapeCount] : null;
+ if (blendShapeNames != null)
+ {
+ for (int i = 0; i < targetRenderer.sharedMesh.blendShapeCount; i++)
+ {
+ blendShapeNames[i] = targetRenderer.sharedMesh.GetBlendShapeName(i);
+ }
+ }
+ else
+ {
+ Debug.LogError("No blend shapes found in the target renderer.");
+ enabled = false;
+ return;
+ }
+
+ // Create blend shape curves
+ blendShapeCurves = new AnimationCurve[blendShapeNames.Length];
+ for (int i = 0; i < blendShapeNames.Length; i++)
+ {
+ blendShapeCurves[i] = new AnimationCurve();
+ }
+
+ // Set up animation clip
+ animationClip.ClearCurves();
+ for (int i = 0; i < blendShapeNames.Length; i++)
+ {
+ string curvePath = targetRenderer.gameObject.name + "." + blendShapeNames[i];
+ animationClip.SetCurve(curvePath, typeof(SkinnedMeshRenderer), "blendShape." + blendShapeNames[i], blendShapeCurves[i]);
+ }
+
+ // Initialize variables
+ recordingTimer = 0f;
+ isRecording = false;
+ }
+
+ private void Update()
+ {
+ if (Input.GetKeyDown(startRecordingKey))
+ {
+ StartRecording();
+ }
+
+ if (Input.GetKeyDown(stopRecordingKey))
+ {
+ StopRecording();
+ }
+
+ if (!isRecording)
+ {
+ return;
+ }
+
+ recordingTimer += Time.deltaTime;
+
+ if (recordingTimer >= recordingInterval)
+ {
+ RecordExpression();
+ recordingTimer = 0f;
+ }
+ }
+
+ private void RecordExpression()
+ {
+ for (int i = 0; i < blendShapeCurves.Length; i++)
+ {
+ float blendShapeValue = targetRenderer.GetBlendShapeWeight(i);
+ blendShapeCurves[i].AddKey(new Keyframe(recordingTimer, blendShapeValue));
+ }
+ }
+
+ public void StartRecording()
+ {
+ isRecording = true;
+ recordingTimer = 0f;
+ }
+
+ public void StopRecording()
+ {
+ isRecording = false;
+ }
+}
diff --git a/Assets/External/EasyMotionRecorder/Scripts/ExpressionRecorder.cs.meta b/Assets/External/EasyMotionRecorder/Scripts/ExpressionRecorder.cs.meta
new file mode 100644
index 00000000..8d2aa3fa
--- /dev/null
+++ b/Assets/External/EasyMotionRecorder/Scripts/ExpressionRecorder.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e23bee3e795ff6643829034f2005dd62
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/External/EasyMotionRecorder/Scripts/FaceAnimationRecorder.cs b/Assets/External/EasyMotionRecorder/Scripts/FaceAnimationRecorder.cs
index edc4655a..7c69ede1 100644
--- a/Assets/External/EasyMotionRecorder/Scripts/FaceAnimationRecorder.cs
+++ b/Assets/External/EasyMotionRecorder/Scripts/FaceAnimationRecorder.cs
@@ -3,8 +3,10 @@ using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
+using System.IO;
using UnityEditor;
using UnityEngine;
+using EasyMotionRecorder;
/**
[EasyMotionRecorder]
@@ -323,12 +325,22 @@ namespace Entum
}
}
- MotionDataRecorder.SafeCreateDirectory("Assets/Resources");
+ // 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";
+ }
+
+ MotionDataRecorder.SafeCreateDirectory(savePath);
- var outputPath = "Assets/Resources/FaceRecordMotion_" + _animRecorder.CharacterAnimator.name + "_" +
- DateTime.Now.ToString("yyyy_MM_dd_HH_mm_ss") + "_Clip.anim";
+ var outputPath = Path.Combine(savePath, fileName);
- Debug.Log("outputPath:" + outputPath);
+ Debug.Log($"페이스 애니메이션 파일 저장 경로: {outputPath}");
AssetDatabase.CreateAsset(animclip,
AssetDatabase.GenerateUniqueAssetPath(outputPath));
AssetDatabase.SaveAssets();
diff --git a/Assets/External/EasyMotionRecorder/Scripts/HumanoidPoses.cs b/Assets/External/EasyMotionRecorder/Scripts/HumanoidPoses.cs
index 5003bf44..c5f37ca2 100644
--- a/Assets/External/EasyMotionRecorder/Scripts/HumanoidPoses.cs
+++ b/Assets/External/EasyMotionRecorder/Scripts/HumanoidPoses.cs
@@ -11,9 +11,12 @@ using UnityEngine;
using System;
using System.Text;
using System.Collections.Generic;
+using System.Linq;
+using System.IO;
#if UNITY_EDITOR
using UnityEditor;
#endif
+using EasyMotionRecorder;
namespace Entum
{
@@ -77,8 +80,25 @@ namespace Entum
///
/// モーションデータの中身
///
+ [System.Serializable]
public class HumanoidPoses : ScriptableObject
{
+ [SerializeField]
+ public string AvatarName = ""; // 아바타 이름 저장
+
+ // 세션 ID를 가져오는 메서드 (MotionDataRecorder와 동일한 세션 ID 사용)
+ private string GetSessionID()
+ {
+ // MotionDataRecorder에서 세션 ID를 가져오려고 시도
+ var motionRecorder = FindObjectOfType();
+ if (motionRecorder != null && !string.IsNullOrEmpty(motionRecorder.SessionID))
+ {
+ return motionRecorder.SessionID;
+ }
+
+ // MotionDataRecorder가 없거나 세션 ID가 없으면 현재 시간으로 생성
+ return DateTime.Now.ToString("yyMMdd_HHmmss");
+ }
#if UNITY_EDITOR
//Genericなanimファイルとして出力する
[ContextMenu("Export as Generic animation clips")]
@@ -87,9 +107,30 @@ namespace Entum
var clip = new AnimationClip { frameRate = 30 };
AnimationUtility.SetAnimationClipSettings(clip, new AnimationClipSettings { loopTime = false });
+ // 본 데이터가 있는지 확인
+ if (Poses.Count == 0 || Poses[0].HumanoidBones.Count == 0)
+ {
+ Debug.LogError("ExportGenericAnim: 본 데이터가 없습니다. Poses.Count=" + Poses.Count +
+ (Poses.Count > 0 ? ", HumanoidBones.Count=" + Poses[0].HumanoidBones.Count : ""));
+ return;
+ }
+
var bones = Poses[0].HumanoidBones;
+
for (int i = 0; i < bones.Count; i++)
{
+ var bone = bones[i];
+
+ // 경로가 비어있는지 확인
+ if (string.IsNullOrEmpty(bone.Name))
+ {
+ Debug.LogError($"본 {i}: 이름이 비어있습니다!");
+ continue;
+ }
+
+ // 경로 정리: 끝의 슬래시만 제거
+ string cleanPath = bone.Name.TrimEnd('/');
+
var positionCurveX = new AnimationCurve();
var positionCurveY = new AnimationCurve();
var positionCurveZ = new AnimationCurve();
@@ -100,35 +141,40 @@ namespace Entum
foreach (var p in Poses)
{
- positionCurveX.AddKey(p.Time, p.HumanoidBones[i].LocalPosition.x);
- positionCurveY.AddKey(p.Time, p.HumanoidBones[i].LocalPosition.y);
- positionCurveZ.AddKey(p.Time, p.HumanoidBones[i].LocalPosition.z);
- rotationCurveX.AddKey(p.Time, p.HumanoidBones[i].LocalRotation.x);
- rotationCurveY.AddKey(p.Time, p.HumanoidBones[i].LocalRotation.y);
- rotationCurveZ.AddKey(p.Time, p.HumanoidBones[i].LocalRotation.z);
- rotationCurveW.AddKey(p.Time, p.HumanoidBones[i].LocalRotation.w);
+ if (p.HumanoidBones.Count > i)
+ {
+ var poseBone = p.HumanoidBones[i];
+ positionCurveX.AddKey(p.Time, poseBone.LocalPosition.x);
+ positionCurveY.AddKey(p.Time, poseBone.LocalPosition.y);
+ positionCurveZ.AddKey(p.Time, poseBone.LocalPosition.z);
+ rotationCurveX.AddKey(p.Time, poseBone.LocalRotation.x);
+ rotationCurveY.AddKey(p.Time, poseBone.LocalRotation.y);
+ rotationCurveZ.AddKey(p.Time, poseBone.LocalRotation.z);
+ rotationCurveW.AddKey(p.Time, poseBone.LocalRotation.w);
+ }
}
- //pathは階層
+ //path는 계층
//http://mebiustos.hatenablog.com/entry/2015/09/16/230000
+ var binding = new EditorCurveBinding
+ {
+ path = cleanPath,
+ type = typeof(Transform),
+ propertyName = "m_LocalPosition.x"
+ };
+
+ AnimationUtility.SetEditorCurve(clip, binding, positionCurveX);
AnimationUtility.SetEditorCurve(clip,
new EditorCurveBinding
{
- path = Poses[0].HumanoidBones[i].Name,
- type = typeof(Transform),
- propertyName = "m_LocalPosition.x"
- }, positionCurveX);
- AnimationUtility.SetEditorCurve(clip,
- new EditorCurveBinding
- {
- path = Poses[0].HumanoidBones[i].Name,
+ path = cleanPath,
type = typeof(Transform),
propertyName = "m_LocalPosition.y"
}, positionCurveY);
AnimationUtility.SetEditorCurve(clip,
new EditorCurveBinding
{
- path = Poses[0].HumanoidBones[i].Name,
+ path = cleanPath,
type = typeof(Transform),
propertyName = "m_LocalPosition.z"
}, positionCurveZ);
@@ -136,28 +182,28 @@ namespace Entum
AnimationUtility.SetEditorCurve(clip,
new EditorCurveBinding
{
- path = Poses[0].HumanoidBones[i].Name,
+ path = cleanPath,
type = typeof(Transform),
propertyName = "m_LocalRotation.x"
}, rotationCurveX);
AnimationUtility.SetEditorCurve(clip,
new EditorCurveBinding
{
- path = Poses[0].HumanoidBones[i].Name,
+ path = cleanPath,
type = typeof(Transform),
propertyName = "m_LocalRotation.y"
}, rotationCurveY);
AnimationUtility.SetEditorCurve(clip,
new EditorCurveBinding
{
- path = Poses[0].HumanoidBones[i].Name,
+ path = cleanPath,
type = typeof(Transform),
propertyName = "m_LocalRotation.z"
}, rotationCurveZ);
AnimationUtility.SetEditorCurve(clip,
new EditorCurveBinding
{
- path = Poses[0].HumanoidBones[i].Name,
+ path = cleanPath,
type = typeof(Transform),
propertyName = "m_LocalRotation.w"
}, rotationCurveW);
@@ -165,20 +211,81 @@ namespace Entum
clip.EnsureQuaternionContinuity();
- var path = string.Format("Assets/Resources/RecordMotion_{0:yyyy_MM_dd_HH_mm_ss}_Generic.anim", DateTime.Now);
+ // 세션 ID 사용 (MotionDataRecorder와 동일한 세션 ID 사용)
+ string sessionID = GetSessionID();
+
+ // 아바타 이름이 있으면 포함, 없으면 기본값 사용
+ string avatarName = !string.IsNullOrEmpty(AvatarName) ? AvatarName : "Unknown";
+
+ // 에셋 파일의 경로를 기반으로 저장 경로 결정
+ string savePath = "Assets/Resources"; // 기본값
+ string fileName = $"{sessionID}_{avatarName}_Generic.anim";
+
+ // 현재 에셋 파일의 경로 가져오기
+ string assetPath = AssetDatabase.GetAssetPath(this);
+ if (!string.IsNullOrEmpty(assetPath))
+ {
+ string directory = Path.GetDirectoryName(assetPath);
+ if (!string.IsNullOrEmpty(directory))
+ {
+ savePath = directory;
+ }
+ }
+
+ MotionDataRecorder.SafeCreateDirectory(savePath);
+
+ var path = Path.Combine(savePath, fileName);
var uniqueAssetPath = AssetDatabase.GenerateUniqueAssetPath(path);
AssetDatabase.CreateAsset(clip, uniqueAssetPath);
AssetDatabase.SaveAssets();
+ AssetDatabase.Refresh();
+
+ Debug.Log($"제네릭 애니메이션 파일이 저장되었습니다: {uniqueAssetPath}");
}
//Humanoidなanimファイルとして出力する。
[ContextMenu("Export as Humanoid animation clips")]
public void ExportHumanoidAnim()
{
- var clip = new AnimationClip { frameRate = 30 };
- AnimationUtility.SetAnimationClipSettings(clip, new AnimationClipSettings { loopTime = false });
+ // 데이터 검증
+ if (Poses == null || Poses.Count == 0)
+ {
+ Debug.LogError("ExportHumanoidAnim: Poses 데이터가 없습니다. Poses.Count=" + (Poses?.Count ?? 0));
+ return;
+ }
+ Debug.Log($"ExportHumanoidAnim: Poses.Count={Poses.Count}, 첫 번째 포즈 시간={Poses[0].Time}, 마지막 포즈 시간={Poses[Poses.Count-1].Time}");
+
+ // 첫 번째 포즈 데이터 검증
+ var firstPose = Poses[0];
+ Debug.Log($"첫 번째 포즈: BodyPosition={firstPose.BodyPosition}, Muscles.Length={firstPose.Muscles?.Length ?? 0}");
+
+ if (firstPose.Muscles == null || firstPose.Muscles.Length == 0)
+ {
+ Debug.LogError("ExportHumanoidAnim: Muscles 데이터가 없습니다.");
+ return;
+ }
+
+ var clip = new AnimationClip { frameRate = 30 };
+
+ // 시작할 때 설정을 적용 (커브 데이터 추가 전)
+ var settings = new AnimationClipSettings
+ {
+ loopTime = false, // Loop Time: false
+ cycleOffset = 0, // Cycle Offset: 0
+ loopBlend = false, // Loop Blend: false
+ loopBlendOrientation = true, // Root Transform Rotation - Bake Into Pose: true
+ loopBlendPositionY = true, // Root Transform Position (Y) - Bake Into Pose: true
+ loopBlendPositionXZ = true, // Root Transform Position (XZ) - Bake Into Pose: true
+ keepOriginalOrientation = true, // Root Transform Rotation - Based Upon: Original
+ keepOriginalPositionY = true, // Root Transform Position (Y) - Based Upon: Original
+ keepOriginalPositionXZ = true, // Root Transform Position (XZ) - Based Upon: Original
+ heightFromFeet = false, // Height From Feet: false
+ mirror = false // Mirror: false
+ };
+
+ AnimationUtility.SetAnimationClipSettings(clip, settings);
// body position
{
@@ -192,6 +299,8 @@ namespace Entum
curveZ.AddKey(item.Time, item.BodyPosition.z);
}
+ Debug.Log($"Body Position 커브: 키 개수 X={curveX.length}, Y={curveY.length}, Z={curveZ.length}");
+
const string muscleX = "RootT.x";
clip.SetCurve("", typeof(Animator), muscleX, curveX);
const string muscleY = "RootT.y";
@@ -327,12 +436,44 @@ namespace Entum
clip.EnsureQuaternionContinuity();
- var path = string.Format("Assets/Resources/RecordMotion_{0:yyyy_MM_dd_HH_mm_ss}_Humanoid.anim", DateTime.Now);
+ // 세션 ID 사용 (MotionDataRecorder와 동일한 세션 ID 사용)
+ string sessionID = GetSessionID();
+
+ // 아바타 이름이 있으면 포함, 없으면 기본값 사용
+ string avatarName = !string.IsNullOrEmpty(AvatarName) ? AvatarName : "Unknown";
+
+ // 에셋 파일의 경로를 기반으로 저장 경로 결정
+ string savePath = "Assets/Resources"; // 기본값
+ string fileName = $"{sessionID}_{avatarName}_Humanoid.anim";
+
+ // 현재 에셋 파일의 경로 가져오기
+ string assetPath = AssetDatabase.GetAssetPath(this);
+ if (!string.IsNullOrEmpty(assetPath))
+ {
+ string directory = Path.GetDirectoryName(assetPath);
+ if (!string.IsNullOrEmpty(directory))
+ {
+ savePath = directory;
+ }
+ }
+
+ MotionDataRecorder.SafeCreateDirectory(savePath);
+
+ var path = Path.Combine(savePath, fileName);
var uniqueAssetPath = AssetDatabase.GenerateUniqueAssetPath(path);
AssetDatabase.CreateAsset(clip, uniqueAssetPath);
AssetDatabase.SaveAssets();
+ AssetDatabase.Refresh();
+
+ Debug.Log($"휴머노이드 애니메이션 파일이 저장되었습니다: {uniqueAssetPath}");
}
+
+ // BVH export 기능 제거 - 새로운 시스템으로 대체 예정
+
+ // BVH 관련 메서드들 제거 - 새로운 시스템으로 대체 예정
+
+ // BVH 관련 헬퍼 메서드들 제거 - 새로운 시스템으로 대체 예정
#endif
[Serializable]
@@ -372,18 +513,44 @@ namespace Entum
if(path != null) return path;
var current = target;
- while (true)
+ var pathList = new List();
+
+ // 타겟이 루트와 같은 경우 빈 문자열 반환
+ if (current == root)
{
- if (current == null) throw new Exception(target.name + "は" + root.name + "の子ではありません");
- if (current == root) break;
-
- path = (path == "") ? current.name : current.name + "/" + path;
-
+ path = "";
+ _pathCache.Add(target, path);
+ return path;
+ }
+
+ // 루트까지 올라가면서 경로 구성
+ while (current != null && current != root)
+ {
+ pathList.Add(current.name);
current = current.parent;
}
+
+ if (current == null)
+ {
+ Debug.LogError($"{target.name}는 {root.name}의 자식이 아닙니다.");
+ throw new Exception(target.name + "는" + root.name + "의 자식이 아닙니다");
+ }
+
+ // 경로를 역순으로 조합 (Unity 애니메이션 경로 형식)
+ pathList.Reverse();
+ path = string.Join("/", pathList);
+
+ // 경로 끝의 슬래시 제거
+ path = path.TrimEnd('/');
+
+ // Unity 애니메이션 시스템에서 루트 오브젝트 이름을 제거하는 경우를 대비
+ // 루트가 "Bip001"인 경우, 경로에서 "Bip001/" 부분을 제거
+ if (root.name == "Bip001" && path.StartsWith("Bip001/"))
+ {
+ path = path.Substring("Bip001/".Length);
+ }
_pathCache.Add(target, path);
-
return path;
}
@@ -391,11 +558,25 @@ namespace Entum
{
Name = BuildRelativePath(root, t);
- LocalPosition = t.localPosition;
- LocalRotation = t.localRotation;
+ // 루트 본인지 확인 (이름이 비어있거나 루트와 같은 경우)
+ bool isRoot = string.IsNullOrEmpty(Name) || t == root;
+
+ if (isRoot)
+ {
+ // 루트 본: 월드 좌표계 사용
+ LocalPosition = t.position; // 월드 위치
+ LocalRotation = t.rotation; // 월드 회전
+ }
+ else
+ {
+ // 자식 본: 로컬 좌표계 사용
+ LocalPosition = t.localPosition;
+ LocalRotation = t.localRotation;
+ }
}
}
+ [SerializeField, HideInInspector]
public List HumanoidBones = new List();
//CSVシリアライズ
@@ -491,6 +672,21 @@ namespace Entum
}
+ [SerializeField, HideInInspector]
public List Poses = new List();
+
+ // 인스펙터 최적화를 위한 요약 정보
+ [System.Serializable]
+ public class SummaryInfo
+ {
+ public int TotalPoses;
+ public float TotalTime;
+ public int TotalBones;
+ public int TotalMuscles;
+ public float AverageFPS;
+ }
+
+ [SerializeField]
+ public SummaryInfo Summary = new SummaryInfo();
}
}
diff --git a/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs b/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs
index 5683405d..60aecdda 100644
--- a/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs
+++ b/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs
@@ -11,9 +11,11 @@ using UnityEngine;
using System;
using System.IO;
using System.Reflection;
+using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor;
#endif
+using EasyMotionRecorder;
namespace Entum
{
@@ -50,6 +52,7 @@ namespace Entum
protected HumanoidPoses Poses;
protected float RecordedTime;
protected float StartTime;
+ public string SessionID; // 세션 ID 추가
private HumanPose _currentPose;
private HumanPoseHandler _poseHandler;
@@ -178,8 +181,11 @@ namespace Entum
return;
}
+ // 세션 ID 생성 (년도는 2자리로 표시, 고유 ID 제거)
+ SessionID = DateTime.Now.ToString("yyMMdd_HHmmss");
Poses = ScriptableObject.CreateInstance();
+ Poses.AvatarName = _animator.name; // 아바타 이름 설정
if (OnRecordStart != null)
{
@@ -209,36 +215,134 @@ namespace Entum
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;
}
private static void SetHumanBoneTransformToHumanoidPoses(Animator animator, ref HumanoidPoses.SerializeHumanoidPose pose)
{
- HumanBodyBones[] values = Enum.GetValues(typeof(HumanBodyBones)) as HumanBodyBones[];
- foreach (HumanBodyBones b in values)
+ // Humanoid 본만 수집하여 데이터 크기 최적화
+ var humanBones = new List();
+
+ // Humanoid 본들만 수집
+ foreach (HumanBodyBones boneType in System.Enum.GetValues(typeof(HumanBodyBones)))
{
- if (b < 0 || b >= HumanBodyBones.LastBone)
+ if (boneType == HumanBodyBones.LastBone) continue;
+
+ var boneTransform = animator.GetBoneTransform(boneType);
+ if (boneTransform != null)
{
- continue;
- }
-
- Transform t = animator.GetBoneTransform(b);
- if (t != null)
- {
- var bone = new HumanoidPoses.SerializeHumanoidPose.HumanoidBone();
- bone.Set(animator.transform, t);
- pose.HumanoidBones.Add(bone);
+ 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))
+ {
+ boneData = ProcessElbowRotation(bone, boneData);
+ }
+
+ pose.HumanoidBones.Add(boneData);
+ }
+ }
+
+ Debug.Log($"Humanoid 본 수집 완료: {pose.HumanoidBones.Count}개 (이전: {animator.GetComponentsInChildren().Length}개)");
+ }
+
+ private static bool IsElbowBone(Transform bone)
+ {
+ // 팔꿈치 본 식별
+ string boneName = bone.name.ToLower();
+ return boneName.Contains("elbow") || boneName.Contains("forearm") ||
+ boneName.Contains("arm") && boneName.Contains("02");
+ }
+
+ 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;
+ }
+ }
+
+ return boneData;
}
protected virtual void WriteAnimationFile()
{
#if UNITY_EDITOR
- SafeCreateDirectory("Assets/Resources");
+ // SavePathManager 사용
+ string savePath = "Assets/Resources"; // 기본값
+ string fileName = $"{SessionID}_{_animator.name}_Motion.asset";
+
+ // SavePathManager가 있으면 사용
+ if (SavePathManager.Instance != null)
+ {
+ savePath = SavePathManager.Instance.GetMotionSavePath();
+ fileName = $"{SessionID}_{_animator.name}_Motion.asset";
+ }
+
+ SafeCreateDirectory(savePath);
- var path = string.Format("Assets/Resources/RecordMotion_{0}{1:yyyy_MM_dd_HH_mm_ss}.asset", _animator.name, DateTime.Now);
+ // 요약 정보 업데이트
+ UpdateSummaryInfo();
+
+ // 파일 경로 생성
+ var path = Path.Combine(savePath, fileName);
var uniqueAssetPath = AssetDatabase.GenerateUniqueAssetPath(path);
AssetDatabase.CreateAsset(Poses, uniqueAssetPath);
@@ -246,8 +350,27 @@ namespace Entum
StartTime = Time.time;
RecordedTime = 0f;
FrameIndex = 0;
+
+ Debug.Log($"모션 파일이 저장되었습니다: {uniqueAssetPath}");
#endif
}
+
+ private void UpdateSummaryInfo()
+ {
+ if (Poses != null && Poses.Poses.Count > 0)
+ {
+ 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}");
+ }
+ }
///
/// 指定したパスにディレクトリが存在しない場合
diff --git a/Assets/External/EasyMotionRecorder/Scripts/ObjectMotionRecorder.cs b/Assets/External/EasyMotionRecorder/Scripts/ObjectMotionRecorder.cs
new file mode 100644
index 00000000..ceccfe2d
--- /dev/null
+++ b/Assets/External/EasyMotionRecorder/Scripts/ObjectMotionRecorder.cs
@@ -0,0 +1,260 @@
+using UnityEngine;
+using System;
+using System.Collections.Generic;
+using System.IO;
+#if UNITY_EDITOR
+using UnityEditor;
+#endif
+using EasyMotionRecorder;
+
+namespace Entum
+{
+ ///
+ /// 오브젝트 모션 데이터 기록 클래스
+ /// 여러 오브젝트의 포지션과 로테이션을 동시에 기록
+ ///
+ [DefaultExecutionOrder(32001)] // MotionDataRecorder보다 나중에 실행
+ public class ObjectMotionRecorder : MonoBehaviour
+ {
+ [Header("레코딩 설정")]
+ [SerializeField] private KeyCode recordStartKey = KeyCode.R;
+ [SerializeField] private KeyCode recordStopKey = KeyCode.X;
+
+ [Header("타겟 오브젝트들")]
+ [SerializeField] private Transform[] targetObjects;
+
+ [Header("레코딩 설정")]
+ [Tooltip("기록할 FPS. 0으로 설정하면 제한 없음")]
+ [SerializeField] private float targetFPS = 60.0f;
+
+ [Header("파일명 설정")]
+ [SerializeField] private string objectNamePrefix = "Object";
+
+ private bool isRecording = false;
+ private float startTime;
+ private float recordedTime;
+ private int frameIndex;
+
+ // 각 오브젝트별 애니메이션 클립 데이터
+ private Dictionary objectClips;
+ private Dictionary positionCurves;
+ private Dictionary rotationCurves;
+
+ // 세션 ID (MotionDataRecorder와 동일한 형식)
+ public string SessionID { get; private set; }
+
+ public Action OnRecordStart;
+ public Action OnRecordEnd;
+
+ private void Update()
+ {
+ if (Input.GetKeyDown(recordStartKey))
+ {
+ StartRecording();
+ }
+
+ if (Input.GetKeyDown(recordStopKey))
+ {
+ StopRecording();
+ }
+ }
+
+ private void LateUpdate()
+ {
+ if (!isRecording)
+ return;
+
+ recordedTime = Time.time - startTime;
+
+ // FPS 제한 확인
+ if (targetFPS > 0.0f)
+ {
+ var nextTime = (1.0f * (frameIndex + 1)) / targetFPS;
+ if (nextTime > recordedTime)
+ {
+ return;
+ }
+ }
+
+ // 각 오브젝트의 포지션과 로테이션 기록
+ foreach (var target in targetObjects)
+ {
+ if (target == null) continue;
+
+ RecordObjectMotion(target, recordedTime);
+ }
+
+ frameIndex++;
+ }
+
+ private void RecordObjectMotion(Transform target, float time)
+ {
+ if (!positionCurves.ContainsKey(target) || !rotationCurves.ContainsKey(target))
+ return;
+
+ var posCurves = positionCurves[target];
+ var rotCurves = rotationCurves[target];
+
+ // 포지션 기록 (X, Y, Z)
+ posCurves[0].AddKey(time, target.position.x);
+ posCurves[1].AddKey(time, target.position.y);
+ posCurves[2].AddKey(time, target.position.z);
+
+ // 로테이션 기록 (X, Y, Z, W)
+ rotCurves[0].AddKey(time, target.rotation.x);
+ rotCurves[1].AddKey(time, target.rotation.y);
+ rotCurves[2].AddKey(time, target.rotation.z);
+ rotCurves[3].AddKey(time, target.rotation.w);
+ }
+
+ public void StartRecording()
+ {
+ if (isRecording)
+ return;
+
+ // 세션 ID 생성 (MotionDataRecorder와 동일한 형식)
+ SessionID = DateTime.Now.ToString("yyMMdd_HHmmss");
+
+ // 초기화
+ objectClips = new Dictionary();
+ positionCurves = new Dictionary();
+ rotationCurves = new Dictionary();
+
+ // 각 오브젝트별 애니메이션 클립과 커브 초기화
+ if (targetObjects != null)
+ {
+ foreach (var target in targetObjects)
+ {
+ 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;
+ }
+ }
+
+ startTime = Time.time;
+ recordedTime = 0f;
+ frameIndex = 0;
+ isRecording = true;
+
+ OnRecordStart?.Invoke();
+
+ Debug.Log($"오브젝트 모션 레코딩 시작: {(targetObjects != null ? targetObjects.Length : 0)}개 오브젝트");
+ }
+
+ public void StopRecording()
+ {
+ if (!isRecording)
+ return;
+
+ isRecording = false;
+
+ // 각 오브젝트별 애니메이션 클립 생성 및 저장
+ if (targetObjects != null)
+ {
+ foreach (var target in targetObjects)
+ {
+ if (target == null || !objectClips.ContainsKey(target)) continue;
+
+ CreateAndSaveAnimationClip(target);
+ }
+ }
+
+ OnRecordEnd?.Invoke();
+
+ Debug.Log("오브젝트 모션 레코딩 종료");
+ }
+
+ private void CreateAndSaveAnimationClip(Transform target)
+ {
+#if UNITY_EDITOR
+ var clip = objectClips[target];
+ var posCurves = positionCurves[target];
+ var rotCurves = rotationCurves[target];
+
+ // 포지션 커브 설정
+ clip.SetCurve("", typeof(Transform), "m_LocalPosition.x", posCurves[0]);
+ clip.SetCurve("", typeof(Transform), "m_LocalPosition.y", posCurves[1]);
+ clip.SetCurve("", typeof(Transform), "m_LocalPosition.z", posCurves[2]);
+
+ // 로테이션 커브 설정
+ clip.SetCurve("", typeof(Transform), "m_LocalRotation.x", rotCurves[0]);
+ clip.SetCurve("", typeof(Transform), "m_LocalRotation.y", rotCurves[1]);
+ clip.SetCurve("", typeof(Transform), "m_LocalRotation.z", rotCurves[2]);
+ clip.SetCurve("", typeof(Transform), "m_LocalRotation.w", rotCurves[3]);
+
+ // Quaternion 연속성 보장
+ clip.EnsureQuaternionContinuity();
+
+ // 파일명 생성
+ string objectName = target.name;
+ string fileName = $"{SessionID}_{objectName}_Object.anim";
+
+ // SavePathManager 사용
+ string savePath = "Assets/Resources"; // 기본값
+ if (SavePathManager.Instance != null)
+ {
+ savePath = SavePathManager.Instance.GetObjectSavePath();
+ }
+
+ MotionDataRecorder.SafeCreateDirectory(savePath);
+
+ var path = Path.Combine(savePath, fileName);
+ var uniqueAssetPath = AssetDatabase.GenerateUniqueAssetPath(path);
+
+ AssetDatabase.CreateAsset(clip, uniqueAssetPath);
+ AssetDatabase.SaveAssets();
+ AssetDatabase.Refresh();
+
+ Debug.Log($"오브젝트 애니메이션 파일 저장: {uniqueAssetPath}");
+#endif
+ }
+
+ // 인스펙터에서 타겟 오브젝트 추가/제거를 위한 헬퍼 메서드
+ [ContextMenu("Add Current Selection")]
+ public void AddCurrentSelection()
+ {
+#if UNITY_EDITOR
+ var selected = Selection.activeGameObject;
+ if (selected != null)
+ {
+ var newArray = new Transform[targetObjects.Length + 1];
+ Array.Copy(targetObjects, newArray, targetObjects.Length);
+ newArray[targetObjects.Length] = selected.transform;
+ targetObjects = newArray;
+ Debug.Log($"오브젝트 추가: {selected.name}");
+ }
+#endif
+ }
+
+ [ContextMenu("Clear All Targets")]
+ public void ClearAllTargets()
+ {
+ targetObjects = new Transform[0];
+ Debug.Log("모든 타겟 오브젝트 제거");
+ }
+
+ // 타겟 오브젝트 배열 접근자
+ public Transform[] TargetObjects => targetObjects;
+ public bool IsRecording => isRecording;
+ public float RecordedTime => recordedTime;
+ }
+}
\ No newline at end of file
diff --git a/Assets/External/EasyMotionRecorder/Scripts/ObjectMotionRecorder.cs.meta b/Assets/External/EasyMotionRecorder/Scripts/ObjectMotionRecorder.cs.meta
new file mode 100644
index 00000000..6ca63385
--- /dev/null
+++ b/Assets/External/EasyMotionRecorder/Scripts/ObjectMotionRecorder.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 530f525e71d58a94d9aa9ad830075d54
\ No newline at end of file
diff --git a/Assets/External/EasyMotionRecorder/Scripts/README_SavePathManager.md b/Assets/External/EasyMotionRecorder/Scripts/README_SavePathManager.md
new file mode 100644
index 00000000..c92f951d
--- /dev/null
+++ b/Assets/External/EasyMotionRecorder/Scripts/README_SavePathManager.md
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3f1ea9e9ccd7d22e9936bbfd0520c5a9155b6e38fa1376cc4c22711f20a8cd74
+size 1997
diff --git a/Assets/External/EasyMotionRecorder/Scripts/README_SavePathManager.md.meta b/Assets/External/EasyMotionRecorder/Scripts/README_SavePathManager.md.meta
new file mode 100644
index 00000000..65dfcca7
--- /dev/null
+++ b/Assets/External/EasyMotionRecorder/Scripts/README_SavePathManager.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: d52d79965c0c87f4dbc7d4ea99597abe
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/External/EasyMotionRecorder/Scripts/SavePathManager.cs b/Assets/External/EasyMotionRecorder/Scripts/SavePathManager.cs
new file mode 100644
index 00000000..cbaefd0d
--- /dev/null
+++ b/Assets/External/EasyMotionRecorder/Scripts/SavePathManager.cs
@@ -0,0 +1,143 @@
+using UnityEngine;
+using System.IO;
+#if UNITY_EDITOR
+using UnityEditor;
+#endif
+
+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";
+ [SerializeField] private string objectSavePath = "Assets/Resources/Motion";
+
+ [Header("설정")]
+ [SerializeField] private bool createSubdirectories = true;
+
+ [Header("자동 출력 옵션")]
+ [SerializeField] private bool exportHumanoidOnSave = false;
+ [SerializeField] private bool exportGenericOnSave = false;
+ public bool ExportHumanoidOnSave => exportHumanoidOnSave;
+ public bool ExportGenericOnSave => exportGenericOnSave;
+
+ private void Awake()
+ {
+ if (_instance == null)
+ {
+ _instance = this;
+ DontDestroyOnLoad(gameObject);
+ InitializePaths();
+ }
+ else if (_instance != this)
+ {
+ Destroy(gameObject);
+ }
+ }
+
+ private void InitializePaths()
+ {
+ if (createSubdirectories)
+ {
+ CreateDirectoryIfNotExists(motionSavePath);
+ CreateDirectoryIfNotExists(facialSavePath);
+ }
+ }
+
+ private void CreateDirectoryIfNotExists(string path)
+ {
+ if (!Directory.Exists(path))
+ {
+ Directory.CreateDirectory(path);
+#if UNITY_EDITOR
+ AssetDatabase.Refresh();
+#endif
+ }
+ }
+
+ public string GetMotionSavePath()
+ {
+ return motionSavePath;
+ }
+
+ public string GetFacialSavePath()
+ {
+ return motionSavePath; // 모션 경로와 동일하게 설정
+ }
+
+ public string GetObjectSavePath()
+ {
+ return motionSavePath; // 모션 경로와 동일하게 설정
+ }
+
+ public void SetMotionSavePath(string path)
+ {
+ motionSavePath = path;
+ if (createSubdirectories)
+ CreateDirectoryIfNotExists(path);
+ }
+
+ public void SetFacialSavePath(string path)
+ {
+ facialSavePath = path;
+ if (createSubdirectories)
+ CreateDirectoryIfNotExists(path);
+ }
+
+ public void SetObjectSavePath(string path)
+ {
+ objectSavePath = path;
+ if (createSubdirectories)
+ CreateDirectoryIfNotExists(path);
+ }
+
+ public void SetCreateSubdirectories(bool create)
+ {
+ createSubdirectories = create;
+ if (create)
+ {
+ InitializePaths();
+ }
+ }
+
+ public void ResetToDefaults()
+ {
+ motionSavePath = "Assets/Resources/Motion";
+ facialSavePath = "Assets/Resources/Motion";
+ objectSavePath = "Assets/Resources/Motion";
+ createSubdirectories = true;
+ InitializePaths();
+ }
+
+ public void SynchronizePaths()
+ {
+ // 모든 경로를 모션 경로와 동일하게 설정
+ facialSavePath = motionSavePath;
+ objectSavePath = motionSavePath;
+ if (createSubdirectories)
+ {
+ InitializePaths();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/External/EasyMotionRecorder/Scripts/SavePathManager.cs.meta b/Assets/External/EasyMotionRecorder/Scripts/SavePathManager.cs.meta
new file mode 100644
index 00000000..fe401b3a
--- /dev/null
+++ b/Assets/External/EasyMotionRecorder/Scripts/SavePathManager.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 717b945a8f3f682439ad3d79310cc265
\ No newline at end of file
diff --git a/Assets/External/lilToon/Editor/lilToonEditorUtils.cs b/Assets/External/lilToon/Editor/lilToonEditorUtils.cs
index 5ecd2bbd..2f849ab9 100644
--- a/Assets/External/lilToon/Editor/lilToonEditorUtils.cs
+++ b/Assets/External/lilToon/Editor/lilToonEditorUtils.cs
@@ -609,9 +609,9 @@ namespace lilToon
sb.AppendLine();
sb.AppendLine("# SRP Information");
- if(GraphicsSettings.renderPipelineAsset != null)
+ if(GraphicsSettings.defaultRenderPipeline != null)
{
- sb.AppendLine("Current RP: " + GraphicsSettings.renderPipelineAsset.ToString());
+ sb.AppendLine("Current RP: " + GraphicsSettings.defaultRenderPipeline.ToString());
}
else
{
diff --git a/Assets/Scripts/Editor/RecompileScript.cs b/Assets/Scripts/Editor/RecompileScript.cs
new file mode 100644
index 00000000..b4629467
--- /dev/null
+++ b/Assets/Scripts/Editor/RecompileScript.cs
@@ -0,0 +1,12 @@
+using UnityEditor;
+
+public class RecompileScript
+{
+ [MenuItem("Tools/Recompile")]
+ public static void Recompile()
+ {
+ // 현재의 에디터 애플리케이션의 상태를 강제로 리컴파일합니다.
+ AssetDatabase.Refresh();
+ EditorUtility.RequestScriptReload();
+ }
+}
diff --git a/Assets/Scripts/Editor/RecompileScript.cs.meta b/Assets/Scripts/Editor/RecompileScript.cs.meta
new file mode 100644
index 00000000..64498a65
--- /dev/null
+++ b/Assets/Scripts/Editor/RecompileScript.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: d3bbf819b6db23d459478aeafe463763
\ No newline at end of file