Fix : 모션 레코더 추가 패치 및 리컴파일 스크립트 추가

This commit is contained in:
KINDNICK 2025-07-18 11:37:53 +09:00
parent 7168bfea58
commit 6c9b5ad631
22 changed files with 1805 additions and 57 deletions

Binary file not shown.

View File

@ -1,5 +1,6 @@
fileFormatVersion: 2
guid: ae0a68acee725e141b02318f249f7990
guid: 9198c85589520e7489efbcc2812979c1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:

View File

@ -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("에셋 새로고침", "에셋이 새로고침되었습니다.", "확인");
}
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0b82188ca059a2b4ab954e1715a6ae3f

View File

@ -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

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 668795823d8b9124fba6f34bd1e32f35

View File

@ -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

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3381156202fae3546b3cdc3f7cf501e6

View File

@ -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;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e23bee3e795ff6643829034f2005dd62
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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();

View File

@ -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
/// <summary>
/// モーションデータの中身
/// </summary>
[System.Serializable]
public class HumanoidPoses : ScriptableObject
{
[SerializeField]
public string AvatarName = ""; // 아바타 이름 저장
// 세션 ID를 가져오는 메서드 (MotionDataRecorder와 동일한 세션 ID 사용)
private string GetSessionID()
{
// MotionDataRecorder에서 세션 ID를 가져오려고 시도
var motionRecorder = FindObjectOfType<MotionDataRecorder>();
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<string>();
// 타겟이 루트와 같은 경우 빈 문자열 반환
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<HumanoidBone> HumanoidBones = new List<HumanoidBone>();
//CSVシリアライズ
@ -491,6 +672,21 @@ namespace Entum
}
[SerializeField, HideInInspector]
public List<SerializeHumanoidPose> Poses = new List<SerializeHumanoidPose>();
// 인스펙터 최적화를 위한 요약 정보
[System.Serializable]
public class SummaryInfo
{
public int TotalPoses;
public float TotalTime;
public int TotalBones;
public int TotalMuscles;
public float AverageFPS;
}
[SerializeField]
public SummaryInfo Summary = new SummaryInfo();
}
}

View File

@ -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<HumanoidPoses>();
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<Transform>();
// 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<Transform>().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}");
}
}
/// <summary>
/// 指定したパスにディレクトリが存在しない場合

View File

@ -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
{
/// <summary>
/// 오브젝트 모션 데이터 기록 클래스
/// 여러 오브젝트의 포지션과 로테이션을 동시에 기록
/// </summary>
[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<Transform, AnimationClip> objectClips;
private Dictionary<Transform, AnimationCurve[]> positionCurves;
private Dictionary<Transform, AnimationCurve[]> 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<Transform, AnimationClip>();
positionCurves = new Dictionary<Transform, AnimationCurve[]>();
rotationCurves = new Dictionary<Transform, AnimationCurve[]>();
// 각 오브젝트별 애니메이션 클립과 커브 초기화
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;
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 530f525e71d58a94d9aa9ad830075d54

BIN
Assets/External/EasyMotionRecorder/Scripts/README_SavePathManager.md (Stored with Git LFS) vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: d52d79965c0c87f4dbc7d4ea99597abe
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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<SavePathManager>();
if (_instance == null)
{
GameObject go = new GameObject("SavePathManager");
_instance = go.AddComponent<SavePathManager>();
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();
}
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 717b945a8f3f682439ad3d79310cc265

View File

@ -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
{

View File

@ -0,0 +1,12 @@
using UnityEditor;
public class RecompileScript
{
[MenuItem("Tools/Recompile")]
public static void Recompile()
{
// 현재의 에디터 애플리케이션의 상태를 강제로 리컴파일합니다.
AssetDatabase.Refresh();
EditorUtility.RequestScriptReload();
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d3bbf819b6db23d459478aeafe463763