Add : 모션 레코더 버그 패치 싱글톤에서 멀티로 변경

This commit is contained in:
KINDNICK 2025-07-26 15:20:35 +09:00
parent 9dc2d4d64f
commit 98d207583a
7 changed files with 711 additions and 175 deletions

View File

@ -1,11 +1,13 @@
#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;
using System.IO;
using Entum;
namespace EasyMotionRecorder
{
[CustomEditor(typeof(ObjectMotionRecorder))]
[CanEditMultipleObjects]
public class ObjectMotionRecorderEditor : Editor
{
private ObjectMotionRecorder recorder;
@ -19,6 +21,12 @@ namespace EasyMotionRecorder
public override void OnInspectorGUI()
{
if (targets.Length > 1)
{
DrawMultiObjectGUI();
return;
}
serializedObject.Update();
EditorGUILayout.Space();
@ -46,6 +54,143 @@ namespace EasyMotionRecorder
serializedObject.ApplyModifiedProperties();
}
private void DrawMultiObjectGUI()
{
EditorGUILayout.Space();
EditorGUILayout.LabelField($"오브젝트 모션 레코더 ({targets.Length}개 선택됨)", EditorStyles.boldLabel);
EditorGUILayout.Space();
// 멀티 오브젝트 상태 표시
DrawMultiObjectStatus();
EditorGUILayout.Space();
// 멀티 오브젝트 설정
DrawMultiObjectSettings();
EditorGUILayout.Space();
// 멀티 오브젝트 액션
DrawMultiObjectActions();
}
private void DrawMultiObjectStatus()
{
int recordingCount = 0;
foreach (ObjectMotionRecorder recorder in targets)
{
if (recorder.IsRecording) recordingCount++;
}
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("레코딩 상태:", GUILayout.Width(100));
if (recordingCount == 0)
{
EditorGUILayout.LabelField("○ 모두 대기 중", EditorStyles.boldLabel);
}
else if (recordingCount == targets.Length)
{
EditorGUILayout.LabelField("● 모두 녹화 중", EditorStyles.boldLabel);
}
else
{
EditorGUILayout.LabelField($"◐ 일부 녹화 중 ({recordingCount}/{targets.Length})", EditorStyles.boldLabel);
}
EditorGUILayout.EndHorizontal();
}
private void DrawMultiObjectSettings()
{
serializedObject.Update();
showRecordingSettings = EditorGUILayout.Foldout(showRecordingSettings, "레코딩 설정 (모든 선택된 오브젝트에 적용)");
if (showRecordingSettings)
{
EditorGUI.indentLevel++;
// 키 설정
var startKeyProp = serializedObject.FindProperty("recordStartKey");
var stopKeyProp = serializedObject.FindProperty("recordStopKey");
EditorGUI.showMixedValue = startKeyProp.hasMultipleDifferentValues;
EditorGUI.BeginChangeCheck();
int startKeyIndex = EditorGUILayout.Popup("시작 키", startKeyProp.enumValueIndex, startKeyProp.enumDisplayNames);
if (EditorGUI.EndChangeCheck())
{
startKeyProp.enumValueIndex = startKeyIndex;
}
EditorGUI.showMixedValue = stopKeyProp.hasMultipleDifferentValues;
EditorGUI.BeginChangeCheck();
int stopKeyIndex = EditorGUILayout.Popup("정지 키", stopKeyProp.enumValueIndex, stopKeyProp.enumDisplayNames);
if (EditorGUI.EndChangeCheck())
{
stopKeyProp.enumValueIndex = stopKeyIndex;
}
// FPS 설정
var fpsProp = serializedObject.FindProperty("targetFPS");
EditorGUI.showMixedValue = fpsProp.hasMultipleDifferentValues;
EditorGUI.BeginChangeCheck();
float fps = EditorGUILayout.FloatField("타겟 FPS", fpsProp.floatValue);
if (EditorGUI.EndChangeCheck())
{
fpsProp.floatValue = fps;
}
EditorGUI.showMixedValue = false;
if (fpsProp.floatValue <= 0 && !fpsProp.hasMultipleDifferentValues)
{
EditorGUILayout.HelpBox("FPS가 0 이하면 제한 없이 녹화됩니다.", MessageType.Info);
}
EditorGUI.indentLevel--;
}
serializedObject.ApplyModifiedProperties();
}
private void DrawMultiObjectActions()
{
EditorGUILayout.LabelField("멀티 오브젝트 액션", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
// 모든 레코더 시작
if (GUILayout.Button("모든 레코더 시작", GUILayout.Height(30)))
{
foreach (ObjectMotionRecorder recorder in targets)
{
if (!recorder.IsRecording && recorder.TargetObjects.Length > 0)
{
recorder.StartRecording();
}
}
}
// 모든 레코더 정지
if (GUILayout.Button("모든 레코더 정지", GUILayout.Height(30)))
{
foreach (ObjectMotionRecorder recorder in targets)
{
if (recorder.IsRecording)
{
recorder.StopRecording();
}
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
// SavePathManager 안내
EditorGUILayout.HelpBox("저장 경로 및 자동 출력 설정은 각 오브젝트의 SavePathManager 컴포넌트에서 관리됩니다.", MessageType.Info);
}
private void DrawRecordingStatus()
{
EditorGUILayout.BeginHorizontal();
@ -188,6 +333,19 @@ namespace EasyMotionRecorder
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
// SavePathManager 안내
var savePathManager = recorder.GetComponent<SavePathManager>();
if (savePathManager != null)
{
EditorGUILayout.HelpBox("저장 경로 및 자동 출력 설정은 SavePathManager 컴포넌트에서 관리됩니다.", MessageType.Info);
}
else
{
EditorGUILayout.HelpBox("SavePathManager 컴포넌트가 없습니다. SavePathManager를 추가해주세요.", MessageType.Warning);
}
}
private void AddSelectedObject()
@ -199,35 +357,27 @@ namespace EasyMotionRecorder
targetsProp.arraySize++;
var newElement = targetsProp.GetArrayElementAtIndex(targetsProp.arraySize - 1);
newElement.objectReferenceValue = selected.transform;
Debug.Log($"오브젝트 추가: {selected.name}");
}
else
{
EditorUtility.DisplayDialog("오류", "선택된 오브젝트가 없습니다.", "확인");
EditorUtility.SetDirty(recorder);
}
}
private void AddSelectedObjects()
{
var selectedObjects = Selection.gameObjects;
if (selectedObjects.Length == 0)
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;
}
EditorUtility.SetDirty(recorder);
}
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}개 오브젝트 추가됨");
}
}
}

View File

@ -6,18 +6,45 @@ using System.IO;
namespace EasyMotionRecorder
{
[CustomEditor(typeof(SavePathManager))]
[CanEditMultipleObjects]
public class SavePathManagerEditor : Editor
{
private SavePathManager savePathManager;
private bool showAdvancedSettings = false;
// SerializedProperty 참조
private SerializedProperty motionSavePathProp;
private SerializedProperty createSubdirectoriesProp;
private SerializedProperty exportHumanoidOnSaveProp;
private SerializedProperty exportGenericOnSaveProp;
private SerializedProperty exportFBXAsciiOnSaveProp;
private SerializedProperty exportFBXBinaryOnSaveProp;
private SerializedProperty instanceIDProp;
private SerializedProperty useDontDestroyOnLoadProp;
private void OnEnable()
{
savePathManager = (SavePathManager)target;
// SerializedProperty 초기화
motionSavePathProp = serializedObject.FindProperty("motionSavePath");
createSubdirectoriesProp = serializedObject.FindProperty("createSubdirectories");
exportHumanoidOnSaveProp = serializedObject.FindProperty("exportHumanoidOnSave");
exportGenericOnSaveProp = serializedObject.FindProperty("exportGenericOnSave");
exportFBXAsciiOnSaveProp = serializedObject.FindProperty("exportFBXAsciiOnSave");
exportFBXBinaryOnSaveProp = serializedObject.FindProperty("exportFBXBinaryOnSave");
instanceIDProp = serializedObject.FindProperty("instanceID");
useDontDestroyOnLoadProp = serializedObject.FindProperty("useDontDestroyOnLoad");
}
public override void OnInspectorGUI()
{
if (targets.Length > 1)
{
DrawMultiObjectGUI();
return;
}
serializedObject.Update();
EditorGUILayout.Space();
@ -34,19 +61,53 @@ namespace EasyMotionRecorder
EditorGUILayout.Space();
// 자동 출력 옵션
DrawAutoExportSettings();
EditorGUILayout.Space();
// 버튼들
DrawActionButtons();
serializedObject.ApplyModifiedProperties();
}
private void DrawMultiObjectGUI()
{
serializedObject.Update();
EditorGUILayout.Space();
EditorGUILayout.LabelField($"저장 경로 관리 ({targets.Length}개 선택됨)", EditorStyles.boldLabel);
EditorGUILayout.Space();
// 멀티 오브젝트 기본 설정
DrawMultiObjectBasicSettings();
EditorGUILayout.Space();
// 멀티 오브젝트 고급 설정
DrawMultiObjectAdvancedSettings();
EditorGUILayout.Space();
// 멀티 오브젝트 자동 출력 옵션
DrawMultiObjectAutoExportSettings();
EditorGUILayout.Space();
// 멀티 오브젝트 액션
DrawMultiObjectActions();
serializedObject.ApplyModifiedProperties();
}
private void DrawBasicSettings()
{
EditorGUILayout.LabelField("기본 설정", EditorStyles.boldLabel);
// 통합 저장 경로 (모든 파일이 같은 위치에 저장됨)
// 통합 저장 경로
EditorGUILayout.BeginHorizontal();
string motionPath = EditorGUILayout.TextField("저장 경로", savePathManager.GetMotionSavePath());
EditorGUILayout.PropertyField(motionSavePathProp, new GUIContent("저장 경로"));
if (GUILayout.Button("폴더 선택", GUILayout.Width(80)))
{
string newPath = EditorUtility.OpenFolderPanel("저장 폴더 선택", "Assets", "");
@ -57,12 +118,14 @@ namespace EasyMotionRecorder
{
newPath = "Assets" + newPath.Substring(Application.dataPath.Length);
}
motionSavePathProp.stringValue = newPath;
serializedObject.ApplyModifiedProperties();
savePathManager.SetMotionSavePath(newPath);
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.HelpBox("모션, 페이스, 제네릭 애니메이션 파일이 모두 이 경로에 저장됩니다.", MessageType.Info);
EditorGUILayout.HelpBox("모션, 표정, 오브젝트 파일이 모두 이 경로에 저장됩니다.", MessageType.Info);
}
private void DrawAdvancedSettings()
@ -74,16 +137,33 @@ namespace EasyMotionRecorder
EditorGUI.indentLevel++;
// 서브디렉토리 생성 여부
bool createSubdirectories = EditorGUILayout.Toggle("서브디렉토리 자동 생성",
serializedObject.FindProperty("createSubdirectories").boolValue);
serializedObject.FindProperty("createSubdirectories").boolValue = createSubdirectories;
EditorGUILayout.PropertyField(createSubdirectoriesProp, new GUIContent("서브디렉토리 자동 생성"));
EditorGUILayout.HelpBox("현재 모든 파일이 동일한 경로에 저장됩니다.", MessageType.Info);
// 인스턴스 ID (읽기 전용)
GUI.enabled = false;
EditorGUILayout.PropertyField(instanceIDProp, new GUIContent("인스턴스 ID (자동 생성)"));
GUI.enabled = true;
// DontDestroyOnLoad 설정
EditorGUILayout.PropertyField(useDontDestroyOnLoadProp, new GUIContent("씬 전환 시 유지"));
EditorGUILayout.HelpBox("인스턴스 ID는 자동으로 생성되며 각 EasyMotionRecorder 인스턴스를 구분합니다.", MessageType.Info);
EditorGUI.indentLevel--;
}
}
private void DrawAutoExportSettings()
{
EditorGUILayout.LabelField("자동 출력 옵션", EditorStyles.boldLabel);
EditorGUILayout.HelpBox("저장 시 자동으로 출력할 파일 형식을 선택하세요.", MessageType.Info);
EditorGUILayout.PropertyField(exportHumanoidOnSaveProp, new GUIContent("휴머노이드 애니메이션 자동 출력"));
EditorGUILayout.PropertyField(exportGenericOnSaveProp, new GUIContent("제네릭 애니메이션 자동 출력"));
EditorGUILayout.PropertyField(exportFBXAsciiOnSaveProp, new GUIContent("FBX ASCII 자동 출력"));
EditorGUILayout.PropertyField(exportFBXBinaryOnSaveProp, new GUIContent("FBX Binary 자동 출력"));
}
private void DrawActionButtons()
{
EditorGUILayout.LabelField("작업", EditorStyles.boldLabel);
@ -97,6 +177,7 @@ namespace EasyMotionRecorder
"모든 설정을 기본값으로 되돌리시겠습니까?", "확인", "취소"))
{
savePathManager.ResetToDefaults();
serializedObject.Update();
EditorUtility.SetDirty(savePathManager);
}
}
@ -116,13 +197,134 @@ namespace EasyMotionRecorder
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.Space();
private void DrawMultiObjectBasicSettings()
{
EditorGUILayout.LabelField("기본 설정 (모든 선택된 오브젝트에 적용)", EditorStyles.boldLabel);
// 통합 저장 경로
EditorGUILayout.BeginHorizontal();
EditorGUI.showMixedValue = motionSavePathProp.hasMultipleDifferentValues;
EditorGUI.BeginChangeCheck();
string newPath = EditorGUILayout.TextField("저장 경로", motionSavePathProp.stringValue);
if (EditorGUI.EndChangeCheck())
{
motionSavePathProp.stringValue = newPath;
foreach (SavePathManager manager in targets)
{
manager.SetMotionSavePath(newPath);
}
}
EditorGUI.showMixedValue = false;
if (GUILayout.Button("폴더 선택", GUILayout.Width(80)))
{
string selectedPath = EditorUtility.OpenFolderPanel("저장 폴더 선택", "Assets", "");
if (!string.IsNullOrEmpty(selectedPath))
{
// Assets 폴더 기준으로 상대 경로로 변환
if (selectedPath.StartsWith(Application.dataPath))
{
selectedPath = "Assets" + selectedPath.Substring(Application.dataPath.Length);
}
motionSavePathProp.stringValue = selectedPath;
foreach (SavePathManager manager in targets)
{
manager.SetMotionSavePath(selectedPath);
EditorUtility.SetDirty(manager);
}
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.HelpBox("모션, 표정, 오브젝트 파일이 모두 이 경로에 저장됩니다.", MessageType.Info);
}
private void DrawMultiObjectAdvancedSettings()
{
showAdvancedSettings = EditorGUILayout.Foldout(showAdvancedSettings, "고급 설정");
if (showAdvancedSettings)
{
EditorGUI.indentLevel++;
// 서브디렉토리 생성 여부
EditorGUI.showMixedValue = createSubdirectoriesProp.hasMultipleDifferentValues;
EditorGUILayout.PropertyField(createSubdirectoriesProp, new GUIContent("서브디렉토리 자동 생성"));
// DontDestroyOnLoad 설정
EditorGUI.showMixedValue = useDontDestroyOnLoadProp.hasMultipleDifferentValues;
EditorGUILayout.PropertyField(useDontDestroyOnLoadProp, new GUIContent("씬 전환 시 유지"));
EditorGUI.showMixedValue = false;
// 인스턴스 ID 표시 (읽기 전용)
EditorGUILayout.LabelField("인스턴스 ID", "각 오브젝트마다 자동 생성됨");
EditorGUILayout.HelpBox("인스턴스 ID는 자동으로 생성되며 각 EasyMotionRecorder 인스턴스를 구분합니다.", MessageType.Info);
EditorGUI.indentLevel--;
}
}
private void DrawMultiObjectAutoExportSettings()
{
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);
EditorGUILayout.HelpBox("저장 시 자동으로 출력할 파일 형식을 선택하세요.", MessageType.Info);
EditorGUI.showMixedValue = exportHumanoidOnSaveProp.hasMultipleDifferentValues;
EditorGUILayout.PropertyField(exportHumanoidOnSaveProp, new GUIContent("휴머노이드 애니메이션 자동 출력"));
EditorGUI.showMixedValue = exportGenericOnSaveProp.hasMultipleDifferentValues;
EditorGUILayout.PropertyField(exportGenericOnSaveProp, new GUIContent("제네릭 애니메이션 자동 출력"));
EditorGUI.showMixedValue = exportFBXAsciiOnSaveProp.hasMultipleDifferentValues;
EditorGUILayout.PropertyField(exportFBXAsciiOnSaveProp, new GUIContent("FBX ASCII 자동 출력"));
EditorGUI.showMixedValue = exportFBXBinaryOnSaveProp.hasMultipleDifferentValues;
EditorGUILayout.PropertyField(exportFBXBinaryOnSaveProp, new GUIContent("FBX Binary 자동 출력"));
EditorGUI.showMixedValue = false;
}
private void DrawMultiObjectActions()
{
EditorGUILayout.LabelField("멀티 오브젝트 액션", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
// 모든 객체를 기본값으로 리셋
if (GUILayout.Button("모든 객체 기본값으로 리셋", GUILayout.Height(30)))
{
if (EditorUtility.DisplayDialog("기본값으로 리셋",
$"선택된 {targets.Length}개 객체의 모든 설정을 기본값으로 되돌리시겠습니까?", "확인", "취소"))
{
foreach (SavePathManager manager in targets)
{
manager.ResetToDefaults();
EditorUtility.SetDirty(manager);
}
serializedObject.Update();
}
}
// 저장 폴더 열기
if (GUILayout.Button("저장 폴더 열기", GUILayout.Height(30)))
{
string path = savePathManager.GetMotionSavePath();
if (Directory.Exists(path))
{
EditorUtility.RevealInFinder(path);
}
else
{
EditorUtility.DisplayDialog("오류", "저장 폴더가 존재하지 않습니다.", "확인");
}
}
EditorGUILayout.EndHorizontal();
}
}
}

View File

@ -37,9 +37,8 @@ namespace Entum {
[Tooltip("記録するFPS。0で制限しない。UpdateのFPSは超えられません。")]
public float TargetFPS = 60.0f;
[Header("인스턴스 설정")]
[SerializeField] private string instanceID = "";
[SerializeField] private SavePathManager _savePathManager;
[HideInInspector, SerializeField] private string instanceID = "";
[HideInInspector, SerializeField] private SavePathManager _savePathManager;
private MotionDataRecorder _animRecorder;
@ -62,28 +61,29 @@ namespace Entum {
_animRecorder.OnRecordStart += RecordStart;
_animRecorder.OnRecordEnd += RecordEnd;
// SavePathManager 자동 찾기
if (_savePathManager == null)
{
_savePathManager = GetComponent<SavePathManager>();
}
// 인스턴스 ID가 비어있으면 자동 생성
if (string.IsNullOrEmpty(instanceID))
{
instanceID = System.Guid.NewGuid().ToString().Substring(0, 8);
}
// SavePathManager가 없으면 같은 GameObject에서 찾기
if (_savePathManager == null)
{
_savePathManager = GetComponent<SavePathManager>();
if (_savePathManager == null)
{
_savePathManager = gameObject.AddComponent<SavePathManager>();
_savePathManager.SetInstanceID(instanceID);
}
}
if(_animRecorder.CharacterAnimator != null) {
_smeshs = GetSkinnedMeshRenderers(_animRecorder.CharacterAnimator);
}
}
// 내부 메서드 (SavePathManager에서 호출)
internal void SetSavePathManager(SavePathManager manager)
{
_savePathManager = manager;
}
SkinnedMeshRenderer[] GetSkinnedMeshRenderers(Animator root) {
var helper = root;
var renderers = helper.GetComponentsInChildren<SkinnedMeshRenderer>();
@ -139,18 +139,46 @@ namespace Entum {
_recording = false;
Debug.Log($"표정 애니메이션 녹화 종료 - 인스턴스: {instanceID}, 총 프레임: {_frameCount}");
WriteAnimationFileToScriptableObject();
// 바로 애니메이션 클립으로 출력
ExportFacialAnimationClip(_animRecorder.CharacterAnimator, _facialData);
}
private void WriteAnimationFileToScriptableObject() {
if (_facialData == null || _facialData.Faces.Count == 0)
{
private void ExportFacialAnimationClip(Animator root, CharacterFacialData facial) {
if(facial == null || facial.Faces.Count == 0) {
Debug.LogError("저장할 표정 데이터가 없습니다.");
return;
}
string fileName = $"{_animRecorder.SessionID}_Facial";
string filePath = Path.Combine(_savePathManager.GetFacialSavePath(), fileName + ".asset");
var clip = new AnimationClip();
clip.frameRate = 30;
var blendShapeNames = facial.Faces[0].BlendShapeNames;
var curves = new Dictionary<string, AnimationCurve>();
for(int i = 0; i < blendShapeNames.Count; i++) {
curves[blendShapeNames[i]] = new AnimationCurve();
}
for(int i = 0; i < facial.Faces.Count; i++) {
var face = facial.Faces[i];
var time = face.Time;
for(int j = 0; j < face.BlendShapeNames.Count; j++) {
var blendShapeName = face.BlendShapeNames[j];
var value = face.BlendShapeValues[j];
if(curves.ContainsKey(blendShapeName)) {
curves[blendShapeName].AddKey(time, value);
}
}
}
foreach(var curve in curves) {
clip.SetCurve("", typeof(SkinnedMeshRenderer), "blendShape." + curve.Key, curve.Value);
}
string fileName = $"{facial.SessionID}_Facial";
string filePath = Path.Combine(_savePathManager.GetFacialSavePath(), fileName + ".anim");
// 인스턴스별 고유 경로 생성
filePath = _savePathManager.GetInstanceSpecificPath(filePath);
@ -158,10 +186,10 @@ namespace Entum {
SavePathManager.SafeCreateDirectory(Path.GetDirectoryName(filePath));
#if UNITY_EDITOR
AssetDatabase.CreateAsset(_facialData, filePath);
AssetDatabase.CreateAsset(clip, filePath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log($"표정 데이터 저장 완료: {filePath}");
Debug.Log($"표정 애니메이션 클립 저장 완료: {filePath}");
#endif
}
@ -235,61 +263,6 @@ namespace Entum {
_frameCount++;
}
void ExportFacialAnimationClip(Animator root, CharacterFacialData facial) {
if(facial.Faces.Count == 0) {
return;
}
var clip = new AnimationClip();
clip.frameRate = 30;
var blendShapeNames = facial.Faces[0].BlendShapeNames;
var curves = new Dictionary<string, AnimationCurve>();
for(int i = 0; i < blendShapeNames.Count; i++) {
curves[blendShapeNames[i]] = new AnimationCurve();
}
for(int i = 0; i < facial.Faces.Count; i++) {
var face = facial.Faces[i];
var time = face.Time;
for(int j = 0; j < face.BlendShapeNames.Count; j++) {
var blendShapeName = face.BlendShapeNames[j];
var value = face.BlendShapeValues[j];
if(curves.ContainsKey(blendShapeName)) {
curves[blendShapeName].AddKey(time, value);
}
}
}
foreach(var curve in curves) {
clip.SetCurve("", typeof(SkinnedMeshRenderer), "blendShape." + curve.Key, curve.Value);
}
string fileName = $"{facial.SessionID}_Facial";
string filePath = Path.Combine(_savePathManager.GetFacialSavePath(), fileName + ".anim");
// 인스턴스별 고유 경로 생성
filePath = _savePathManager.GetInstanceSpecificPath(filePath);
SavePathManager.SafeCreateDirectory(Path.GetDirectoryName(filePath));
#if UNITY_EDITOR
AssetDatabase.CreateAsset(clip, filePath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log($"표정 애니메이션 클립 저장 완료: {filePath}");
#endif
}
void ExportFacialAnimationClipTest() {
if(_facialData != null) {
ExportFacialAnimationClip(_animRecorder.CharacterAnimator, _facialData);
}
}
public void SetInstanceID(string id)
{
instanceID = id;

View File

@ -43,9 +43,8 @@ namespace Entum
[SerializeField, Tooltip("rootBoneSystemがOBJECTROOTの時は使われないパラメータです。")]
private HumanBodyBones _targetRootBone = HumanBodyBones.Hips;
[Header("인스턴스 설정")]
[SerializeField] private string instanceID = "";
[SerializeField] private bool useDontDestroyOnLoad = false;
[HideInInspector, SerializeField] private string instanceID = "";
[HideInInspector, SerializeField] private bool useDontDestroyOnLoad = false;
private HumanPoseHandler _poseHandler;
private Action _onPlayFinish;

View File

@ -54,14 +54,14 @@ namespace Entum
[SerializeField, Tooltip("녹화 시작 시 T-포즈를 별도로 저장할지 여부 (출력 시 0프레임에 포함)")]
private bool _recordTPoseAtStart = true;
[Header("인스턴스 설정")]
[SerializeField] private string instanceID = "";
[SerializeField] private SavePathManager _savePathManager;
[HideInInspector, SerializeField] private string instanceID = "";
[HideInInspector, SerializeField] private SavePathManager _savePathManager;
protected HumanoidPoses Poses;
protected float StartTime { get; set; }
protected float RecordedTime;
protected float StartTime;
public string SessionID; // 세션 ID 추가
public string SessionID { get; set; }
private HumanPose _currentPose;
private HumanPoseHandler _poseHandler;
@ -81,26 +81,41 @@ namespace Entum
return;
}
// 인스턴스 ID가 비어있으면 자동 생성
if (string.IsNullOrEmpty(instanceID))
{
instanceID = System.Guid.NewGuid().ToString().Substring(0, 8);
}
// SavePathManager가 없으면 같은 GameObject에서 찾기
// SavePathManager 자동 찾기 또는 추가
if (_savePathManager == null)
{
_savePathManager = GetComponent<SavePathManager>();
if (_savePathManager == null)
{
_savePathManager = gameObject.AddComponent<SavePathManager>();
_savePathManager.SetInstanceID(instanceID);
}
}
// 인스턴스 ID가 비어있으면 자동 생성
if (string.IsNullOrEmpty(instanceID))
{
instanceID = System.Guid.NewGuid().ToString().Substring(0, 8);
}
// SessionID 생성 (인스턴스 ID 제외)
SessionID = DateTime.Now.ToString("yyMMdd_HHmmss");
_poseHandler = new HumanPoseHandler(_animator.avatar, _animator.transform);
}
// 내부 메서드들 (SavePathManager에서 호출)
internal void SetInstanceID(string id)
{
instanceID = id;
// SessionID는 인스턴스 ID 없이 유지
SessionID = DateTime.Now.ToString("yyMMdd_HHmmss");
}
internal void SetSavePathManager(SavePathManager manager)
{
_savePathManager = manager;
}
private void Update()
{
if (Input.GetKeyDown(_recordStartKey))
@ -365,9 +380,90 @@ namespace Entum
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log($"모션 데이터 저장 완료: {filePath}");
// 자동 출력 옵션 처리
if (_savePathManager != null)
{
if (_savePathManager.ExportHumanoidOnSave)
{
ExportHumanoidAnimation(fileName);
}
if (_savePathManager.ExportGenericOnSave)
{
ExportGenericAnimation(fileName);
}
if (_savePathManager.ExportFBXAsciiOnSave)
{
ExportFBXAnimation(fileName, true);
}
if (_savePathManager.ExportFBXBinaryOnSave)
{
ExportFBXAnimation(fileName, false);
}
}
#endif
}
#if UNITY_EDITOR
private void ExportHumanoidAnimation(string baseFileName)
{
try
{
string animPath = Path.Combine(_savePathManager.GetMotionSavePath(), $"{baseFileName}_Humanoid.anim");
animPath = _savePathManager.GetInstanceSpecificPath(animPath);
// HumanoidPoses의 기존 내보내기 메서드 사용
Poses.ExportHumanoidAnim();
Debug.Log($"휴머노이드 애니메이션 출력 완료: {baseFileName}_Humanoid");
}
catch (System.Exception e)
{
Debug.LogError($"휴머노이드 애니메이션 출력 실패: {e.Message}");
}
}
private void ExportGenericAnimation(string baseFileName)
{
try
{
string animPath = Path.Combine(_savePathManager.GetMotionSavePath(), $"{baseFileName}_Generic.anim");
animPath = _savePathManager.GetInstanceSpecificPath(animPath);
// HumanoidPoses의 기존 내보내기 메서드 사용
Poses.ExportGenericAnim();
Debug.Log($"제네릭 애니메이션 출력 완료: {baseFileName}_Generic");
}
catch (System.Exception e)
{
Debug.LogError($"제네릭 애니메이션 출력 실패: {e.Message}");
}
}
private void ExportFBXAnimation(string baseFileName, bool ascii)
{
try
{
// HumanoidPoses의 기존 FBX 내보내기 메서드 사용
if (ascii)
{
Poses.ExportFBXAscii();
}
else
{
Poses.ExportFBXBinary();
}
Debug.Log($"FBX 애니메이션 출력 완료: {baseFileName}_{(ascii ? "ASCII" : "Binary")}");
}
catch (System.Exception e)
{
Debug.LogError($"FBX 애니메이션 출력 실패: {e.Message}");
}
}
#endif
private void UpdateSummaryInfo()
{
if (Poses == null) return;
@ -425,3 +521,4 @@ namespace Entum
}
}
}

View File

@ -30,10 +30,14 @@ namespace Entum
[Header("파일명 설정")]
[SerializeField] private string objectNamePrefix = "Object";
[Header("인스턴스 설정")]
[SerializeField] private string instanceID = "";
[SerializeField] private SavePathManager _savePathManager;
[HideInInspector, SerializeField] private string instanceID = "";
[HideInInspector, SerializeField] private SavePathManager _savePathManager;
// Properties
public Transform[] TargetObjects => targetObjects;
public bool IsRecording => isRecording;
public float RecordedTime => recordedTime;
private bool isRecording = false;
private float startTime;
private float recordedTime;
@ -52,22 +56,20 @@ namespace Entum
private void Awake()
{
// SavePathManager 자동 찾기
if (_savePathManager == null)
{
_savePathManager = GetComponent<SavePathManager>();
}
// 인스턴스 ID가 비어있으면 자동 생성
if (string.IsNullOrEmpty(instanceID))
{
instanceID = System.Guid.NewGuid().ToString().Substring(0, 8);
}
// SavePathManager가 없으면 같은 GameObject에서 찾기
if (_savePathManager == null)
{
_savePathManager = GetComponent<SavePathManager>();
if (_savePathManager == null)
{
_savePathManager = gameObject.AddComponent<SavePathManager>();
_savePathManager.SetInstanceID(instanceID);
}
}
// SessionID 생성 (인스턴스 ID 제외)
SessionID = DateTime.Now.ToString("yyMMdd_HHmmss");
}
private void Update()
@ -243,8 +245,51 @@ namespace Entum
AssetDatabase.Refresh();
Debug.Log($"오브젝트 애니메이션 파일 저장: {filePath}");
// 자동 출력 옵션 처리
if (_savePathManager != null)
{
if (_savePathManager.ExportHumanoidOnSave)
{
ExportObjectAnimationAsHumanoid(target, fileName);
}
if (_savePathManager.ExportGenericOnSave)
{
ExportObjectAnimationAsGeneric(target, fileName);
}
}
#endif
}
#if UNITY_EDITOR
private void ExportObjectAnimationAsHumanoid(Transform target, string baseFileName)
{
try
{
// 오브젝트 애니메이션은 휴머노이드로 변환할 수 없으므로 제네릭으로 처리
ExportObjectAnimationAsGeneric(target, baseFileName);
Debug.Log($"오브젝트 휴머노이드 애니메이션 출력 완료: {baseFileName}_Humanoid");
}
catch (System.Exception e)
{
Debug.LogError($"오브젝트 휴머노이드 애니메이션 출력 실패: {e.Message}");
}
}
private void ExportObjectAnimationAsGeneric(Transform target, string baseFileName)
{
try
{
// 이미 제네릭 애니메이션으로 저장되었으므로 추가 작업 불필요
Debug.Log($"오브젝트 제네릭 애니메이션 출력 완료: {baseFileName}_Generic");
}
catch (System.Exception e)
{
Debug.LogError($"오브젝트 제네릭 애니메이션 출력 실패: {e.Message}");
}
}
#endif
// 인스펙터에서 타겟 오브젝트 추가/제거를 위한 헬퍼 메서드
[ContextMenu("Add Current Selection")]
@ -270,19 +315,17 @@ namespace Entum
Debug.Log("모든 타겟 오브젝트 제거");
}
public void SetInstanceID(string id)
// 내부 메서드들 (SavePathManager에서 호출)
internal void SetInstanceID(string id)
{
instanceID = id;
// SessionID는 인스턴스 ID 없이 유지
SessionID = DateTime.Now.ToString("yyMMdd_HHmmss");
}
public string GetInstanceID()
internal void SetSavePathManager(SavePathManager manager)
{
return instanceID;
_savePathManager = manager;
}
// 타겟 오브젝트 배열 접근자
public Transform[] TargetObjects => targetObjects;
public bool IsRecording => isRecording;
public float RecordedTime => recordedTime;
}
}

View File

@ -1,5 +1,6 @@
using UnityEngine;
using System.IO;
using Entum;
#if UNITY_EDITOR
using UnityEditor;
#endif
@ -10,8 +11,6 @@ namespace EasyMotionRecorder
{
[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;
@ -26,6 +25,11 @@ namespace EasyMotionRecorder
[SerializeField] private string instanceID = "";
[SerializeField] private bool useDontDestroyOnLoad = false;
// 같은 오브젝트의 컴포넌트 참조
private MotionDataRecorder motionRecorder;
private FaceAnimationRecorder faceRecorder;
private ObjectMotionRecorder objectRecorder;
public bool ExportHumanoidOnSave => exportHumanoidOnSave;
public bool ExportGenericOnSave => exportGenericOnSave;
public bool ExportFBXAsciiOnSave => exportFBXAsciiOnSave;
@ -46,15 +50,44 @@ namespace EasyMotionRecorder
DontDestroyOnLoad(gameObject);
}
// 같은 오브젝트의 컴포넌트들 찾기
FindAndSetupComponents();
InitializePaths();
}
private void FindAndSetupComponents()
{
// 같은 오브젝트에서 컴포넌트들 찾기
motionRecorder = GetComponent<MotionDataRecorder>();
faceRecorder = GetComponent<FaceAnimationRecorder>();
objectRecorder = GetComponent<ObjectMotionRecorder>();
// 각 컴포넌트에 인스턴스 ID 설정
if (motionRecorder != null)
{
motionRecorder.SetInstanceID(instanceID);
motionRecorder.SetSavePathManager(this);
}
if (faceRecorder != null)
{
faceRecorder.SetInstanceID(instanceID);
faceRecorder.SetSavePathManager(this);
}
if (objectRecorder != null)
{
objectRecorder.SetInstanceID(instanceID);
objectRecorder.SetSavePathManager(this);
}
}
private void InitializePaths()
{
if (createSubdirectories)
{
CreateDirectoryIfNotExists(motionSavePath);
CreateDirectoryIfNotExists(facialSavePath);
}
}
@ -85,12 +118,12 @@ namespace EasyMotionRecorder
public string GetFacialSavePath()
{
return facialSavePath;
return motionSavePath; // 통합된 경로 사용
}
public string GetObjectSavePath()
{
return objectSavePath;
return motionSavePath; // 통합된 경로 사용
}
public void SetMotionSavePath(string path)
@ -98,46 +131,60 @@ namespace EasyMotionRecorder
motionSavePath = path;
if (createSubdirectories)
CreateDirectoryIfNotExists(path);
#if UNITY_EDITOR
UnityEditor.EditorUtility.SetDirty(this);
#endif
}
public void SetFacialSavePath(string path)
{
facialSavePath = path;
if (createSubdirectories)
CreateDirectoryIfNotExists(path);
// 통합된 경로이므로 모션 저장 경로와 동일하게 설정
SetMotionSavePath(path);
}
public void SetObjectSavePath(string path)
{
objectSavePath = path;
if (createSubdirectories)
CreateDirectoryIfNotExists(path);
// 통합된 경로이므로 모션 저장 경로와 동일하게 설정
SetMotionSavePath(path);
}
public void SetCreateSubdirectories(bool create)
private void SetCreateSubdirectories(bool create)
{
createSubdirectories = create;
if (create)
{
InitializePaths();
}
#if UNITY_EDITOR
UnityEditor.EditorUtility.SetDirty(this);
#endif
}
public void SetInstanceID(string id)
private void SetInstanceID(string id)
{
instanceID = id;
// 모든 컴포넌트에 새 인스턴스 ID 적용
if (motionRecorder != null) motionRecorder.SetInstanceID(id);
if (faceRecorder != null) faceRecorder.SetInstanceID(id);
if (objectRecorder != null) objectRecorder.SetInstanceID(id);
#if UNITY_EDITOR
UnityEditor.EditorUtility.SetDirty(this);
#endif
}
public void SetUseDontDestroyOnLoad(bool use)
private void SetUseDontDestroyOnLoad(bool use)
{
useDontDestroyOnLoad = use;
#if UNITY_EDITOR
UnityEditor.EditorUtility.SetDirty(this);
#endif
}
public void ResetToDefaults()
{
motionSavePath = "Assets/Resources/Motion";
facialSavePath = "Assets/Resources/Motion";
objectSavePath = "Assets/Resources/Motion";
createSubdirectories = true;
// 자동 출력 옵션 초기화
@ -147,17 +194,9 @@ namespace EasyMotionRecorder
exportFBXBinaryOnSave = false;
InitializePaths();
}
public void SynchronizePaths()
{
// 모든 경로를 모션 경로와 동일하게 설정
facialSavePath = motionSavePath;
objectSavePath = motionSavePath;
if (createSubdirectories)
{
InitializePaths();
}
#if UNITY_EDITOR
UnityEditor.EditorUtility.SetDirty(this);
#endif
}
// 인스턴스별 고유 경로 생성
@ -172,5 +211,38 @@ namespace EasyMotionRecorder
return Path.Combine(directory, $"{fileName}_{instanceID}{extension}");
}
// 자동 출력 옵션 설정 (private으로 변경)
private void SetExportHumanoidOnSave(bool value)
{
exportHumanoidOnSave = value;
#if UNITY_EDITOR
UnityEditor.EditorUtility.SetDirty(this);
#endif
}
private void SetExportGenericOnSave(bool value)
{
exportGenericOnSave = value;
#if UNITY_EDITOR
UnityEditor.EditorUtility.SetDirty(this);
#endif
}
private void SetExportFBXAsciiOnSave(bool value)
{
exportFBXAsciiOnSave = value;
#if UNITY_EDITOR
UnityEditor.EditorUtility.SetDirty(this);
#endif
}
private void SetExportFBXBinaryOnSave(bool value)
{
exportFBXBinaryOnSave = value;
#if UNITY_EDITOR
UnityEditor.EditorUtility.SetDirty(this);
#endif
}
}
}