Fix : 모션 레코더 추가 패치

This commit is contained in:
KINDNICK 2025-09-04 00:59:32 +09:00
parent 42358b15a6
commit 80fd70c464
9 changed files with 1270 additions and 160 deletions

View File

@ -0,0 +1,261 @@
#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;
using System.Linq;
namespace Entum
{
[CustomEditor(typeof(FaceAnimationRecorder))]
[CanEditMultipleObjects]
public class FaceAnimationRecorderEditor : Editor
{
private FaceAnimationRecorder faceRecorder;
private bool showAdvancedSettings = false;
private bool showExclusiveSettings = false;
// SerializedProperty 참조
private SerializedProperty recordFaceBlendshapesProp;
private SerializedProperty exclusiveBlendshapeNamesProp;
private SerializedProperty targetFPSProp;
private SerializedProperty instanceIDProp;
private void OnEnable()
{
faceRecorder = (FaceAnimationRecorder)target;
// SerializedProperty 초기화
recordFaceBlendshapesProp = serializedObject.FindProperty("_recordFaceBlendshapes");
exclusiveBlendshapeNamesProp = serializedObject.FindProperty("_exclusiveBlendshapeNames");
targetFPSProp = serializedObject.FindProperty("TargetFPS");
instanceIDProp = serializedObject.FindProperty("instanceID");
}
public override void OnInspectorGUI()
{
if (targets.Length > 1)
{
DrawMultiObjectGUI();
return;
}
serializedObject.Update();
EditorGUILayout.Space();
EditorGUILayout.LabelField("표정 애니메이션 레코더", EditorStyles.boldLabel);
EditorGUILayout.Space();
// 기본 설정
DrawBasicSettings();
EditorGUILayout.Space();
// 제외 설정
DrawExclusiveSettings();
EditorGUILayout.Space();
// 고급 설정
DrawAdvancedSettings();
EditorGUILayout.Space();
// 상태 정보
DrawStatusInfo();
serializedObject.ApplyModifiedProperties();
}
private void DrawMultiObjectGUI()
{
serializedObject.Update();
EditorGUILayout.Space();
EditorGUILayout.LabelField($"표정 애니메이션 레코더 ({targets.Length}개 선택됨)", EditorStyles.boldLabel);
EditorGUILayout.Space();
DrawMultiObjectBasicSettings();
EditorGUILayout.Space();
DrawMultiObjectExclusiveSettings();
EditorGUILayout.Space();
DrawMultiObjectAdvancedSettings();
serializedObject.ApplyModifiedProperties();
}
private void DrawBasicSettings()
{
EditorGUILayout.LabelField("기본 설정", EditorStyles.boldLabel);
// 표정 기록 활성화 체크박스
EditorGUILayout.PropertyField(recordFaceBlendshapesProp,
new GUIContent("표정 기록 활성화", "체크하면 모션 기록과 함께 표정 애니메이션도 기록됩니다."));
if (!recordFaceBlendshapesProp.boolValue)
{
EditorGUILayout.HelpBox("표정 기록이 비활성화되어 있습니다. 표정 애니메이션을 기록하려면 위 옵션을 체크하세요.", MessageType.Warning);
}
// FPS 설정
EditorGUILayout.PropertyField(targetFPSProp,
new GUIContent("기록 FPS", "표정 애니메이션을 기록할 FPS입니다. 0으로 설정하면 제한하지 않습니다."));
}
private void DrawExclusiveSettings()
{
showExclusiveSettings = EditorGUILayout.Foldout(showExclusiveSettings, "제외할 블렌드셰이프 설정");
if (showExclusiveSettings)
{
EditorGUI.indentLevel++;
EditorGUILayout.HelpBox("립싱크나 특정 블렌드셰이프를 기록에서 제외하고 싶을 때 사용합니다.", MessageType.Info);
EditorGUILayout.PropertyField(exclusiveBlendshapeNamesProp,
new GUIContent("제외할 블렌드셰이프 이름", "이 목록의 문자열을 포함하는 블렌드셰이프는 기록되지 않습니다."), true);
if (exclusiveBlendshapeNamesProp.arraySize > 0)
{
EditorGUILayout.Space(5);
EditorGUILayout.LabelField("현재 제외 목록:", EditorStyles.miniLabel);
for (int i = 0; i < exclusiveBlendshapeNamesProp.arraySize; i++)
{
var element = exclusiveBlendshapeNamesProp.GetArrayElementAtIndex(i);
if (!string.IsNullOrEmpty(element.stringValue))
{
EditorGUILayout.LabelField($"• {element.stringValue}", EditorStyles.miniLabel);
}
}
}
EditorGUI.indentLevel--;
}
}
private void DrawAdvancedSettings()
{
showAdvancedSettings = EditorGUILayout.Foldout(showAdvancedSettings, "고급 설정");
if (showAdvancedSettings)
{
EditorGUI.indentLevel++;
// 인스턴스 ID (읽기 전용)
GUI.enabled = false;
EditorGUILayout.PropertyField(instanceIDProp, new GUIContent("인스턴스 ID (자동 생성)"));
GUI.enabled = true;
EditorGUILayout.HelpBox("인스턴스 ID는 자동으로 생성되며 각 FaceAnimationRecorder 인스턴스를 구분합니다.", MessageType.Info);
EditorGUI.indentLevel--;
}
}
private void DrawStatusInfo()
{
EditorGUILayout.LabelField("상태 정보", EditorStyles.boldLabel);
// MotionDataRecorder 연결 상태 확인
var motionRecorder = faceRecorder.GetComponent<MotionDataRecorder>();
if (motionRecorder != null)
{
EditorGUILayout.HelpBox("✓ MotionDataRecorder가 연결되어 있습니다.", MessageType.Info);
}
else
{
EditorGUILayout.HelpBox("⚠ MotionDataRecorder가 필요합니다. 같은 GameObject에 추가해주세요.", MessageType.Warning);
}
// SavePathManager 연결 상태 확인
var savePathManager = faceRecorder.GetComponent<EasyMotionRecorder.SavePathManager>();
if (savePathManager != null)
{
EditorGUILayout.HelpBox("✓ SavePathManager가 연결되어 있습니다.", MessageType.Info);
}
else
{
EditorGUILayout.HelpBox(" SavePathManager가 없습니다. 자동으로 생성됩니다.", MessageType.Info);
}
// 블렌드셰이프 발견 상태
if (motionRecorder?.CharacterAnimator != null)
{
var skinnedMeshRenderers = motionRecorder.CharacterAnimator.GetComponentsInChildren<SkinnedMeshRenderer>();
int totalBlendShapes = 0;
foreach (var smr in skinnedMeshRenderers)
{
if (smr.sharedMesh != null)
{
totalBlendShapes += smr.sharedMesh.blendShapeCount;
}
}
if (totalBlendShapes > 0)
{
EditorGUILayout.HelpBox($"✓ {totalBlendShapes}개의 블렌드셰이프를 발견했습니다.", MessageType.Info);
}
else
{
EditorGUILayout.HelpBox("⚠ 블렌드셰이프를 찾을 수 없습니다. 캐릭터에 블렌드셰이프가 있는지 확인하세요.", MessageType.Warning);
}
}
}
private void DrawMultiObjectBasicSettings()
{
EditorGUILayout.LabelField("기본 설정 (모든 선택된 오브젝트에 적용)", EditorStyles.boldLabel);
// 표정 기록 활성화
EditorGUI.showMixedValue = recordFaceBlendshapesProp.hasMultipleDifferentValues;
EditorGUILayout.PropertyField(recordFaceBlendshapesProp,
new GUIContent("표정 기록 활성화"));
// FPS 설정
EditorGUI.showMixedValue = targetFPSProp.hasMultipleDifferentValues;
EditorGUILayout.PropertyField(targetFPSProp,
new GUIContent("기록 FPS"));
EditorGUI.showMixedValue = false;
}
private void DrawMultiObjectExclusiveSettings()
{
showExclusiveSettings = EditorGUILayout.Foldout(showExclusiveSettings, "제외할 블렌드셰이프 설정");
if (showExclusiveSettings)
{
EditorGUI.indentLevel++;
EditorGUILayout.HelpBox("립싱크나 특정 블렌드셰이프를 기록에서 제외하고 싶을 때 사용합니다.", MessageType.Info);
EditorGUI.showMixedValue = exclusiveBlendshapeNamesProp.hasMultipleDifferentValues;
EditorGUILayout.PropertyField(exclusiveBlendshapeNamesProp,
new GUIContent("제외할 블렌드셰이프 이름"), true);
EditorGUI.showMixedValue = false;
EditorGUI.indentLevel--;
}
}
private void DrawMultiObjectAdvancedSettings()
{
showAdvancedSettings = EditorGUILayout.Foldout(showAdvancedSettings, "고급 설정");
if (showAdvancedSettings)
{
EditorGUI.indentLevel++;
// 인스턴스 ID 표시 (읽기 전용)
EditorGUILayout.LabelField("인스턴스 ID", "각 오브젝트마다 자동 생성됨");
EditorGUILayout.HelpBox("인스턴스 ID는 자동으로 생성되며 각 FaceAnimationRecorder 인스턴스를 구분합니다.", MessageType.Info);
EditorGUI.indentLevel--;
}
}
}
}
#endif

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 793f8f25531a10a45bbaf0ec019c20a3

View File

@ -2,6 +2,7 @@
using UnityEngine;
using UnityEditor;
using System.IO;
using System.Linq;
namespace EasyMotionRecorder
{
@ -11,6 +12,14 @@ namespace EasyMotionRecorder
{
private SavePathManager savePathManager;
private bool showAdvancedSettings = false;
private bool showExportSettings = true;
private bool showComponentInfo = false;
// 스타일 상수
private GUIStyle headerStyle;
private GUIStyle subHeaderStyle;
private GUIStyle buttonStyle;
private GUIStyle iconButtonStyle;
// SerializedProperty 참조
private SerializedProperty motionSavePathProp;
@ -37,8 +46,40 @@ namespace EasyMotionRecorder
useDontDestroyOnLoadProp = serializedObject.FindProperty("useDontDestroyOnLoad");
}
private void InitializeStyles()
{
if (headerStyle == null)
{
headerStyle = new GUIStyle(EditorStyles.boldLabel)
{
fontSize = 14,
normal = { textColor = EditorGUIUtility.isProSkin ? Color.white : Color.black }
};
subHeaderStyle = new GUIStyle(EditorStyles.boldLabel)
{
fontSize = 12,
normal = { textColor = EditorGUIUtility.isProSkin ? new Color(0.7f, 0.7f, 0.7f) : new Color(0.3f, 0.3f, 0.3f) }
};
buttonStyle = new GUIStyle(GUI.skin.button)
{
fixedHeight = 28,
fontSize = 11
};
iconButtonStyle = new GUIStyle(GUI.skin.button)
{
fixedHeight = 24,
fixedWidth = 24
};
}
}
public override void OnInspectorGUI()
{
InitializeStyles();
if (targets.Length > 1)
{
DrawMultiObjectGUI();
@ -47,70 +88,57 @@ namespace EasyMotionRecorder
serializedObject.Update();
EditorGUILayout.Space();
EditorGUILayout.LabelField("저장 경로 관리", EditorStyles.boldLabel);
EditorGUILayout.Space();
// 기본 설정
DrawHeader();
DrawBasicSettings();
EditorGUILayout.Space();
// 고급 설정
DrawExportSettings();
DrawAdvancedSettings();
EditorGUILayout.Space();
// 자동 출력 옵션
DrawAutoExportSettings();
EditorGUILayout.Space();
// 버튼들
DrawComponentInfo();
DrawActionButtons();
serializedObject.ApplyModifiedProperties();
}
private void DrawMultiObjectGUI()
private void DrawHeader()
{
serializedObject.Update();
EditorGUILayout.Space();
EditorGUILayout.LabelField($"저장 경로 관리 ({targets.Length}개 선택됨)", EditorStyles.boldLabel);
EditorGUILayout.Space();
// 멀티 오브젝트 기본 설정
DrawMultiObjectBasicSettings();
EditorGUILayout.Space(10);
EditorGUILayout.Space();
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
EditorGUILayout.LabelField("📁 저장 경로 관리자", headerStyle);
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
// 멀티 오브젝트 고급 설정
DrawMultiObjectAdvancedSettings();
EditorGUILayout.Space();
// 멀티 오브젝트 자동 출력 옵션
DrawMultiObjectAutoExportSettings();
EditorGUILayout.Space();
// 멀티 오브젝트 액션
DrawMultiObjectActions();
serializedObject.ApplyModifiedProperties();
EditorGUILayout.Space(5);
DrawSeparator();
EditorGUILayout.Space(10);
}
private void DrawSeparator()
{
var rect = EditorGUILayout.GetControlRect(false, 1);
rect.height = 1;
EditorGUI.DrawRect(rect, EditorGUIUtility.isProSkin ? new Color(0.2f, 0.2f, 0.2f) : new Color(0.7f, 0.7f, 0.7f));
}
private void DrawBasicSettings()
{
EditorGUILayout.LabelField("기본 설정", EditorStyles.boldLabel);
DrawSectionHeader("💾 기본 설정", true);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.Space(5);
// 통합 저장 경로
EditorGUILayout.LabelField("저장 경로", subHeaderStyle);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PropertyField(motionSavePathProp, new GUIContent("저장 경로"));
if (GUILayout.Button("폴더 선택", GUILayout.Width(80)))
EditorGUILayout.PropertyField(motionSavePathProp, GUIContent.none);
// 📂 폴더 선택 버튼
if (GUILayout.Button(new GUIContent("📂", "폴더 선택 다이얼로그 열기"), iconButtonStyle))
{
string newPath = EditorUtility.OpenFolderPanel("저장 폴더 선택", "Assets", "");
string currentPath = motionSavePathProp.stringValue;
string startPath = !string.IsNullOrEmpty(currentPath) ? currentPath : "Assets";
string newPath = EditorUtility.OpenFolderPanel("저장 폴더 선택", startPath, "");
if (!string.IsNullOrEmpty(newPath))
{
// Assets 폴더 기준으로 상대 경로로 변환
@ -123,57 +151,212 @@ namespace EasyMotionRecorder
savePathManager.SetMotionSavePath(newPath);
}
}
// 🔍 폴더 열기 버튼
if (GUILayout.Button(new GUIContent("🔍", "파일 탐색기에서 폴더 열기"), iconButtonStyle))
{
string path = savePathManager.GetMotionSavePath();
if (Directory.Exists(path))
{
EditorUtility.RevealInFinder(path);
}
else
{
EditorUtility.DisplayDialog("알림", "폴더가 아직 생성되지 않았습니다.", "확인");
}
}
// 폴더 생성 버튼
if (GUILayout.Button(new GUIContent("", "폴더가 없으면 생성"), iconButtonStyle))
{
string path = savePathManager.GetMotionSavePath();
if (!Directory.Exists(path))
{
try
{
Directory.CreateDirectory(path);
AssetDatabase.Refresh();
EditorUtility.DisplayDialog("성공", $"폴더가 생성되었습니다:\n{path}", "확인");
}
catch (System.Exception e)
{
EditorUtility.DisplayDialog("오류", $"폴더 생성 실패:\n{e.Message}", "확인");
}
}
else
{
EditorUtility.DisplayDialog("알림", "폴더가 이미 존재합니다.", "확인");
}
}
// 🏠 기본 경로 버튼
if (GUILayout.Button(new GUIContent("🏠", "기본 경로(Assets/Motion)로 설정"), iconButtonStyle))
{
string defaultPath = "Assets/Motion";
motionSavePathProp.stringValue = defaultPath;
serializedObject.ApplyModifiedProperties();
savePathManager.SetMotionSavePath(defaultPath);
}
// 📋 경로 복사 버튼
if (GUILayout.Button(new GUIContent("📋", "경로를 클립보드에 복사"), iconButtonStyle))
{
string path = savePathManager.GetMotionSavePath();
EditorGUIUtility.systemCopyBuffer = path;
EditorUtility.DisplayDialog("복사 완료", $"경로가 클립보드에 복사되었습니다:\n{path}", "확인");
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.HelpBox("모션, 표정, 오브젝트 파일이 모두 이 경로에 저장됩니다.", MessageType.Info);
EditorGUILayout.Space(5);
EditorGUILayout.HelpBox("📝 모션, 표정, 오브젝트 파일이 모두 이 경로에 저장됩니다.", MessageType.Info);
EditorGUILayout.Space(5);
EditorGUILayout.EndVertical();
EditorGUILayout.Space(10);
}
private void DrawExportSettings()
{
showExportSettings = DrawSectionHeader("🚀 자동 출력 옵션", showExportSettings);
if (showExportSettings)
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.Space(5);
EditorGUILayout.HelpBox("💾 저장 시 자동으로 출력할 파일 형식을 선택하세요.", MessageType.Info);
EditorGUILayout.Space(3);
// 그리드 레이아웃으로 옵션 나열
EditorGUILayout.BeginHorizontal();
EditorGUILayout.BeginVertical();
DrawToggleOption("🤖 휴머노이드", exportHumanoidOnSaveProp);
DrawToggleOption("🔧 제네릭", exportGenericOnSaveProp);
EditorGUILayout.EndVertical();
EditorGUILayout.BeginVertical();
DrawToggleOption("📄 FBX ASCII", exportFBXAsciiOnSaveProp);
DrawToggleOption("📊 FBX Binary", exportFBXBinaryOnSaveProp);
EditorGUILayout.EndVertical();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
EditorGUILayout.EndVertical();
EditorGUILayout.Space(10);
}
}
private void DrawToggleOption(string label, SerializedProperty prop)
{
EditorGUILayout.BeginHorizontal();
prop.boolValue = EditorGUILayout.Toggle(prop.boolValue, GUILayout.Width(15));
EditorGUILayout.LabelField(label, GUILayout.ExpandWidth(true));
EditorGUILayout.EndHorizontal();
}
private void DrawAdvancedSettings()
{
showAdvancedSettings = EditorGUILayout.Foldout(showAdvancedSettings, "고급 설정");
showAdvancedSettings = DrawSectionHeader("⚙️ 고급 설정", showAdvancedSettings);
if (showAdvancedSettings)
{
EditorGUI.indentLevel++;
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.Space(5);
// 서브디렉토리 생성 여부
EditorGUILayout.PropertyField(createSubdirectoriesProp, new GUIContent("서브디렉토리 자동 생성"));
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("📁 서브디렉토리 자동 생성", GUILayout.Width(200));
createSubdirectoriesProp.boolValue = EditorGUILayout.Toggle(createSubdirectoriesProp.boolValue);
EditorGUILayout.EndHorizontal();
// 인스턴스 ID (읽기 전용)
GUI.enabled = false;
EditorGUILayout.PropertyField(instanceIDProp, new GUIContent("인스턴스 ID (자동 생성)"));
GUI.enabled = true;
EditorGUILayout.Space(3);
// DontDestroyOnLoad 설정
EditorGUILayout.PropertyField(useDontDestroyOnLoadProp, new GUIContent("씬 전환 시 유지"));
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("🔒 씬 전환 시 유지", GUILayout.Width(200));
useDontDestroyOnLoadProp.boolValue = EditorGUILayout.Toggle(useDontDestroyOnLoadProp.boolValue);
EditorGUILayout.EndHorizontal();
EditorGUILayout.HelpBox("인스턴스 ID는 자동으로 생성되며 각 EasyMotionRecorder 인스턴스를 구분합니다.", MessageType.Info);
EditorGUILayout.Space(5);
EditorGUI.indentLevel--;
// 인스턴스 ID (읽기 전용)
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("🆔 인스턴스 ID", GUILayout.Width(120));
GUI.enabled = false;
EditorGUILayout.TextField(instanceIDProp.stringValue);
GUI.enabled = true;
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(3);
EditorGUILayout.HelpBox("📝 인스턴스 ID는 자동으로 생성되며 각 EasyMotionRecorder 인스턴스를 구분합니다.", MessageType.Info);
EditorGUILayout.Space(5);
EditorGUILayout.EndVertical();
EditorGUILayout.Space(10);
}
}
private void DrawAutoExportSettings()
private void DrawComponentInfo()
{
EditorGUILayout.LabelField("자동 출력 옵션", EditorStyles.boldLabel);
EditorGUILayout.HelpBox("저장 시 자동으로 출력할 파일 형식을 선택하세요.", MessageType.Info);
showComponentInfo = DrawSectionHeader("🔍 컴포넌트 상태", showComponentInfo);
EditorGUILayout.PropertyField(exportHumanoidOnSaveProp, new GUIContent("휴머노이드 애니메이션 자동 출력"));
EditorGUILayout.PropertyField(exportGenericOnSaveProp, new GUIContent("제네릭 애니메이션 자동 출력"));
EditorGUILayout.PropertyField(exportFBXAsciiOnSaveProp, new GUIContent("FBX ASCII 자동 출력"));
EditorGUILayout.PropertyField(exportFBXBinaryOnSaveProp, new GUIContent("FBX Binary 자동 출력"));
if (showComponentInfo)
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.Space(5);
// 연결된 컴포넌트들 확인
var motionRecorder = savePathManager.GetComponent<Entum.MotionDataRecorder>();
var faceRecorder = savePathManager.GetComponent<Entum.FaceAnimationRecorder>();
var objectRecorder = savePathManager.GetComponent<Entum.ObjectMotionRecorder>();
EditorGUILayout.LabelField("연결된 컴포넌트:", subHeaderStyle);
EditorGUILayout.Space(3);
DrawComponentStatus("🎥 MotionDataRecorder", motionRecorder != null);
DrawComponentStatus("😀 FaceAnimationRecorder", faceRecorder != null);
DrawComponentStatus("📦 ObjectMotionRecorder", objectRecorder != null);
EditorGUILayout.Space(5);
EditorGUILayout.EndVertical();
EditorGUILayout.Space(10);
}
}
private void DrawComponentStatus(string componentName, bool isConnected)
{
EditorGUILayout.BeginHorizontal();
string status = isConnected ? "✅" : "❌";
string statusText = isConnected ? "연결됨" : "비연결";
EditorGUILayout.LabelField($"{status} {componentName}", GUILayout.Width(200));
EditorGUILayout.LabelField(statusText, isConnected ? EditorStyles.miniLabel : EditorStyles.centeredGreyMiniLabel);
EditorGUILayout.EndHorizontal();
}
private bool DrawSectionHeader(string title, bool expanded)
{
EditorGUILayout.BeginHorizontal();
expanded = EditorGUILayout.Foldout(expanded, title, true, subHeaderStyle);
EditorGUILayout.EndHorizontal();
return expanded;
}
private void DrawActionButtons()
{
EditorGUILayout.LabelField("작업", EditorStyles.boldLabel);
DrawSectionHeader("🚀 작업", true);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.Space(5);
EditorGUILayout.BeginHorizontal();
// 기본값으로 리셋 버튼
if (GUILayout.Button("기본값으로 리셋", GUILayout.Height(30)))
if (GUILayout.Button("🔄 기본값으로 리셋", buttonStyle))
{
if (EditorUtility.DisplayDialog("기본값으로 리셋",
if (EditorUtility.DisplayDialog("설정 리셋",
"모든 설정을 기본값으로 되돌리시겠습니까?", "확인", "취소"))
{
savePathManager.ResetToDefaults();
@ -183,7 +366,7 @@ namespace EasyMotionRecorder
}
// 폴더 열기 버튼
if (GUILayout.Button("저장 폴더 열기", GUILayout.Height(30)))
if (GUILayout.Button("📂 저장 폴더 열기", buttonStyle))
{
string path = savePathManager.GetMotionSavePath();
if (Directory.Exists(path))
@ -192,11 +375,92 @@ namespace EasyMotionRecorder
}
else
{
EditorUtility.DisplayDialog("오류", "저장 폴더가 존재하지 않습니다.", "확인");
if (EditorUtility.DisplayDialog("폴더 생성",
"저장 폴더가 아직 생성되지 않았습니다.\n지금 생성하시겠습니까?", "생성", "취소"))
{
Directory.CreateDirectory(path);
EditorUtility.RevealInFinder(path);
AssetDatabase.Refresh();
}
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(3);
// 추가 유용한 버튼들
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("📊 인스턴스 상태 확인", buttonStyle))
{
ShowInstanceInfo();
}
if (GUILayout.Button("🛠️ 설정 내보내기", buttonStyle))
{
ExportSettings();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
EditorGUILayout.EndVertical();
EditorGUILayout.Space(10);
}
private void ShowInstanceInfo()
{
var motionRecorder = savePathManager.GetComponent<Entum.MotionDataRecorder>();
var faceRecorder = savePathManager.GetComponent<Entum.FaceAnimationRecorder>();
var objectRecorder = savePathManager.GetComponent<Entum.ObjectMotionRecorder>();
string info = $"인스턴스 ID: {savePathManager.InstanceID}\n";
info += $"저장 경로: {savePathManager.GetMotionSavePath()}\n\n";
info += "연결된 컴포넌트:\n";
info += $"- MotionDataRecorder: {(motionRecorder != null ? "" : "")}\n";
info += $"- FaceAnimationRecorder: {(faceRecorder != null ? "" : "")}\n";
info += $"- ObjectMotionRecorder: {(objectRecorder != null ? "" : "")}";
EditorUtility.DisplayDialog("인스턴스 정보", info, "확인");
}
private void ExportSettings()
{
string settings = $"SavePathManager 설정:\n";
settings += $"저장 경로: {savePathManager.GetMotionSavePath()}\n";
settings += $"휴머노이드 자동 출력: {savePathManager.ExportHumanoidOnSave}\n";
settings += $"제네릭 자동 출력: {savePathManager.ExportGenericOnSave}\n";
settings += $"FBX ASCII 자동 출력: {savePathManager.ExportFBXAsciiOnSave}\n";
settings += $"FBX Binary 자동 출력: {savePathManager.ExportFBXBinaryOnSave}";
GUIUtility.systemCopyBuffer = settings;
EditorUtility.DisplayDialog("설정 내보내기", "설정이 클립보드에 복사되었습니다.", "확인");
}
private void DrawMultiObjectGUI()
{
serializedObject.Update();
EditorGUILayout.Space();
EditorGUILayout.LabelField($"📁 저장 경로 관리자 ({targets.Length}개 선택됨)", headerStyle);
EditorGUILayout.Space();
DrawMultiObjectBasicSettings();
EditorGUILayout.Space();
DrawMultiObjectAdvancedSettings();
EditorGUILayout.Space();
DrawMultiObjectAutoExportSettings();
EditorGUILayout.Space();
DrawMultiObjectActions();
serializedObject.ApplyModifiedProperties();
}
private void DrawMultiObjectBasicSettings()
@ -218,9 +482,12 @@ namespace EasyMotionRecorder
}
EditorGUI.showMixedValue = false;
if (GUILayout.Button("폴더 선택", GUILayout.Width(80)))
// 📂 폴더 선택 버튼
if (GUILayout.Button(new GUIContent("📂", "폴더 선택 다이얼로그 열기"), iconButtonStyle))
{
string selectedPath = EditorUtility.OpenFolderPanel("저장 폴더 선택", "Assets", "");
string currentPath = motionSavePathProp.stringValue;
string startPath = !string.IsNullOrEmpty(currentPath) ? currentPath : "Assets";
string selectedPath = EditorUtility.OpenFolderPanel("저장 폴더 선택", startPath, "");
if (!string.IsNullOrEmpty(selectedPath))
{
// Assets 폴더 기준으로 상대 경로로 변환
@ -237,9 +504,54 @@ namespace EasyMotionRecorder
}
}
}
// 🏠 기본 경로 버튼
if (GUILayout.Button(new GUIContent("🏠", "기본 경로(Assets/Motion)로 설정"), iconButtonStyle))
{
string defaultPath = "Assets/Motion";
motionSavePathProp.stringValue = defaultPath;
foreach (SavePathManager manager in targets)
{
manager.SetMotionSavePath(defaultPath);
EditorUtility.SetDirty(manager);
}
}
// 폴더 생성 버튼
if (GUILayout.Button(new GUIContent("", "선택된 모든 경로의 폴더 생성"), iconButtonStyle))
{
int createdCount = 0;
int existCount = 0;
foreach (SavePathManager manager in targets)
{
string path = manager.GetMotionSavePath();
if (!Directory.Exists(path))
{
try
{
Directory.CreateDirectory(path);
createdCount++;
}
catch (System.Exception e)
{
Debug.LogError($"폴더 생성 실패: {path} - {e.Message}");
}
}
else
{
existCount++;
}
}
AssetDatabase.Refresh();
EditorUtility.DisplayDialog("폴더 생성 완료",
$"생성됨: {createdCount}개\n이미 존재: {existCount}개\n총 선택: {targets.Length}개", "확인");
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.HelpBox("모션, 표정, 오브젝트 파일이 모두 이 경로에 저장됩니다.", MessageType.Info);
EditorGUILayout.HelpBox("📝 모션, 표정, 오브젝트 파일이 모두 이 경로에 저장됩니다.", MessageType.Info);
}
private void DrawMultiObjectAdvancedSettings()
@ -328,4 +640,4 @@ namespace EasyMotionRecorder
}
}
}
#endif
#endif

View File

@ -20,25 +20,26 @@ http://opensource.org/licenses/mit-license.php
#if UNITY_EDITOR
namespace Entum {
/// <summary>
/// Blendshapeの動きを記録するクラス
/// リップシンクは後入れでTimeline上にAudioClipをつけて、みたいな可能性が高いので
/// Blendshape의 동작을 기록하는 클래스
/// 립싱크는 나중에 Timeline에 AudioClip을 추가할 가능성이 높으므로
/// Exclusive(除外)するBlendshape名を登録できるようにしています。
/// </summary>
[RequireComponent(typeof(MotionDataRecorder))]
public class FaceAnimationRecorder:MonoBehaviour {
[Header("表情記録を同時に行う場合はtrueにします")]
[SerializeField]
[Header("표정 애니메이션 기록 설정")]
[SerializeField, Tooltip("모션 데이터와 함께 표정 애니메이션도 기록합니다")]
private bool _recordFaceBlendshapes = false;
[Header("リップシンクを記録したくない場合はここにモーフ名を入れていく 例:face_mouse_eなど")]
[SerializeField]
private List<string> _exclusiveBlendshapeNames;
[Header("제외할 블렌드셈이프 설정")]
[SerializeField, Tooltip("립싱크나 특정 블렌드셈이프를 기록에서 제외하고 싶은 경우 사용합니다. 예: face_mouse_e")]
private List<string> _exclusiveBlendshapeNames = new List<string>();
[Tooltip("記録するFPS。0で制限しない。UpdateのFPSは超えられません。")]
[Header("고급 옵션")]
[SerializeField, Tooltip("기록할 FPS. 0으로 설정하면 제한하지 않습니다.")]
public float TargetFPS = 60.0f;
[HideInInspector, SerializeField] private string instanceID = "";
[HideInInspector, SerializeField] private SavePathManager _savePathManager;
[SerializeField, HideInInspector] private string instanceID = "";
[SerializeField, HideInInspector] private SavePathManager _savePathManager;
private MotionDataRecorder _animRecorder;
@ -62,9 +63,17 @@ namespace Entum {
// Use this for initialization
private void OnEnable() {
_animRecorder = GetComponent<MotionDataRecorder>();
// 이벤트 중복 연결 방지
_animRecorder.OnRecordStart -= RecordStart;
_animRecorder.OnRecordEnd -= RecordEnd;
// 이벤트 연결
_animRecorder.OnRecordStart += RecordStart;
_animRecorder.OnRecordEnd += RecordEnd;
Debug.Log($"FaceAnimationRecorder 이벤트 연결됨 - 인스턴스: {instanceID}");
// SavePathManager 자동 찾기
if (_savePathManager == null)
{
@ -79,6 +88,22 @@ namespace Entum {
if(_animRecorder.CharacterAnimator != null) {
_smeshs = GetSkinnedMeshRenderers(_animRecorder.CharacterAnimator);
Debug.Log($"SkinnedMeshRenderer 초기화 - 인스턴스: {instanceID}, 발견된 개수: {_smeshs?.Length ?? 0}");
// 각 SkinnedMeshRenderer의 블렌드셰이프 개수 로그
if (_smeshs != null)
{
for (int i = 0; i < _smeshs.Length; i++)
{
var mesh = _smeshs[i];
var blendShapeCount = mesh.sharedMesh?.blendShapeCount ?? 0;
Debug.Log($" - {mesh.name}: {blendShapeCount}개 블렌드셰이프");
}
}
}
else
{
Debug.LogWarning($"CharacterAnimator가 null - 인스턴스: {instanceID}");
}
}
@ -104,21 +129,65 @@ namespace Entum {
}
private void OnDisable() {
Debug.Log($"FaceAnimationRecorder OnDisable 호출 - 인스턴스: {instanceID}, 녹화 중: {_recording}");
if(_recording) {
RecordEnd();
_recording = false;
}
if(_animRecorder == null) return;
if(_animRecorder == null)
{
Debug.LogWarning($"FaceAnimationRecorder OnDisable: _animRecorder가 null - 인스턴스: {instanceID}");
return;
}
_animRecorder.OnRecordStart -= RecordStart;
_animRecorder.OnRecordEnd -= RecordEnd;
Debug.Log($"FaceAnimationRecorder 이벤트 해제됨 - 인스턴스: {instanceID}");
}
/// <summary>
/// 記録開始
/// </summary>
private void RecordStart() {
Debug.Log($"FaceAnimationRecorder.RecordStart 호출됨 - 인스턴스: {instanceID}, 활성화: {_recordFaceBlendshapes}, 이미 녹화 중: {_recording}");
if(_recordFaceBlendshapes == false) {
Debug.Log($"표정 기록이 비활성화되어 있음 - 인스턴스: {instanceID}");
return;
}
if (_recording) {
Debug.Log($"이미 표정 녹화가 진행 중임 - 인스턴스: {instanceID}");
return;
}
// _animRecorder가 null이면 다시 찾기
if (_animRecorder == null) {
_animRecorder = GetComponent<MotionDataRecorder>();
if (_animRecorder != null) {
Debug.Log($"RecordStart에서 MotionDataRecorder 재연결 - 인스턴스: {instanceID}");
// 이벤트도 다시 연결
_animRecorder.OnRecordStart -= RecordStart;
_animRecorder.OnRecordEnd -= RecordEnd;
_animRecorder.OnRecordStart += RecordStart;
_animRecorder.OnRecordEnd += RecordEnd;
Debug.Log($"이벤트 재연결 완료 - 인스턴스: {instanceID}");
} else {
Debug.LogError($"MotionDataRecorder를 찾을 수 없음 - 인스턴스: {instanceID}");
return;
}
}
// SkinnedMeshRenderer 재초기화 (두 번째 녹화 시 문제 방지)
if(_animRecorder.CharacterAnimator != null) {
_smeshs = GetSkinnedMeshRenderers(_animRecorder.CharacterAnimator);
Debug.Log($"RecordStart에서 SkinnedMeshRenderer 재초기화 - 인스턴스: {instanceID}, 발견된 개수: {_smeshs?.Length ?? 0}");
}
else {
Debug.LogError($"CharacterAnimator가 null이어서 표정 녹화를 시작할 수 없음 - 인스턴스: {instanceID}");
return;
}
@ -132,11 +201,14 @@ namespace Entum {
_recordedTime = 0f;
_startTime = Time.time;
Debug.Log($"표정 애니메이션 녹화 시작 - 인스턴스: {instanceID}");
Debug.Log($"표정 애니메이션 녹화 시작 - 인스턴스: {instanceID}, SessionID: {_animRecorder.SessionID}");
}
private void RecordEnd() {
Debug.Log($"FaceAnimationRecorder.RecordEnd 호출됨 - 인스턴스: {instanceID}, 녹화 중: {_recording}");
if(_recording == false) {
Debug.Log($"이미 녹화가 중단된 상태 - 인스턴스: {instanceID}");
return;
}
@ -144,10 +216,32 @@ namespace Entum {
Debug.Log($"표정 애니메이션 녹화 종료 - 인스턴스: {instanceID}, 총 프레임: {_frameCount}");
// 바로 애니메이션 클립으로 출력
ExportFacialAnimationClip(_animRecorder.CharacterAnimator, _facialData);
if (_facialData != null && _facialData.Faces.Count > 0)
{
ExportFacialAnimationClip(_animRecorder.CharacterAnimator, _facialData);
}
else
{
Debug.LogWarning($"표정 데이터가 없어서 출력할 수 없음 - 인스턴스: {instanceID}");
}
// 상태 완전 리셋 (다음 녹화를 위해)
_frameCount = 0;
_recordedTime = 0f;
_facialData = null;
_past = new CharacterFacialData.SerializeHumanoidFace() {
BlendShapeNames = new List<string>(),
BlendShapeValues = new List<float>(),
SkinnedMeshRendererNames = new List<string>()
};
Debug.Log($"표정 녹화 상태 리셋 완료 - 인스턴스: {instanceID}");
}
private void ExportFacialAnimationClip(Animator root, CharacterFacialData facial) {
Debug.Log($"ExportFacialAnimationClip 시작 - 인스턴스: {instanceID}");
Debug.Log($"facial null 여부: {facial == null}, Faces 개수: {facial?.Faces?.Count ?? 0}");
if(facial == null || facial.Faces.Count == 0) {
Debug.LogError("저장할 표정 데이터가 없습니다.");
return;
@ -389,6 +483,20 @@ namespace Entum {
return;
}
// SkinnedMeshRenderer 배열이 null이거나 비어있으면 중단
if (_smeshs == null || _smeshs.Length == 0) {
if (_frameCount == 0) {
Debug.LogError($"LateUpdate: SkinnedMeshRenderer 배열이 null이거나 비어있음 - 인스턴스: {instanceID}");
}
return;
}
// 첫 번째 프레임에서만 로그 출력
if (_frameCount == 0)
{
Debug.Log($"LateUpdate 표정 데이터 수집 시작 - 인스턴스: {instanceID}, SkinnedMesh 개수: {_smeshs?.Length ?? 0}");
}
_recordedTime = Time.time - _startTime;
if (TargetFPS != 0.0f)
@ -437,6 +545,19 @@ namespace Entum {
current.Time = _recordedTime;
_facialData.Faces.Add(current);
_past = current;
// 10프레임마다 진행상황 로그 출력
if (_frameCount % 10 == 0)
{
Debug.Log($"표정 데이터 수집 중 - 프레임: {_frameCount}, 총 데이터: {_facialData.Faces.Count}, 블렌드셰이프: {current.BlendShapeNames.Count}개");
}
}
else {
// 변화 없어서 스킵된 경우 (100프레임마다 로그)
if (_frameCount % 100 == 0 && _frameCount > 0)
{
Debug.Log($"표정 변화 없음 - 프레임: {_frameCount}, 스킵됨");
}
}
_frameCount++;

View File

@ -13,17 +13,17 @@ using System.IO;
namespace Entum
{
/// <summary>
/// CSVに吐かれたモーションデータを再生する
/// CSV에 출력된 모션 데이터를 재생하는 클래스
/// </summary>
public class MotionDataPlayerCSV : MotionDataPlayer
{
[SerializeField, Tooltip("スラッシュで終わる形で")]
[SerializeField, Tooltip("슬래시로 끝나는 형태로")]
private string _recordedDirectory;
[SerializeField, Tooltip("拡張子も")]
[SerializeField, Tooltip("확장자도 포함하여")]
private string _recordedFileName;
// Use this for initialization
// 초기화용
private void Start()
{
if (string.IsNullOrEmpty(_recordedDirectory))
@ -35,10 +35,10 @@ namespace Entum
LoadCSVData(motionCSVPath);
}
//CSVから_recordedMotionDataを作る
// CSV에서 _recordedMotionData를 생성
private void LoadCSVData(string motionDataPath)
{
//ファイルが存在しなければ終了
// 파일이 존재하지 않으면 종료
if (!File.Exists(motionDataPath))
{
return;
@ -50,7 +50,7 @@ namespace Entum
FileStream fs = null;
StreamReader sr = null;
//ファイル読み込み
// 파일 읽기
try
{
fs = new FileStream(motionDataPath, FileMode.Open);
@ -73,7 +73,7 @@ namespace Entum
}
catch (System.Exception e)
{
Debug.LogError("ファイル読み込み失敗!" + e.Message + e.StackTrace);
Debug.LogError("파일 읽기 실패!" + e.Message + e.StackTrace);
}
if (sr != null)

View File

@ -14,25 +14,25 @@ using System.IO;
namespace Entum
{
/// <summary>
/// モーションデータをCSVに記録するクラス
/// ランタイムでも記録できる
/// 모션 데이터를 CSV에 기록하는 클래스
/// 런타임에도 기록 가능
/// </summary>
[DefaultExecutionOrder(31000)]
public class MotionDataRecorderCSV : MotionDataRecorder
{
[SerializeField, Tooltip("スラッシュで終わる形で")]
[SerializeField, Tooltip("슬래시로 끝나는 형태로")]
private string _outputDirectory;
[SerializeField, Tooltip("拡張子も")]
[SerializeField, Tooltip("확장자도 포함하여")]
private string _outputFileName;
protected override void WriteAnimationFile()
{
//ファイルオープン
// 파일 열기
string directoryStr = _outputDirectory;
if (directoryStr == "")
{
//自動設定ディレクトリ
// 자동 설정 디렉토리
directoryStr = Application.streamingAssetsPath + "/";
if (!Directory.Exists(directoryStr))
@ -44,7 +44,7 @@ namespace Entum
string fileNameStr = _outputFileName;
if (fileNameStr == "")
{
//自動設定ファイル名
// 자동 설정 파일명
fileNameStr = string.Format("motion_{0:yyyy_MM_dd_HH_mm_ss}.csv", DateTime.Now);
}
@ -57,7 +57,7 @@ namespace Entum
sw.WriteLine(seriStr);
}
//ファイルクローズ
// 파일 닫기
try
{
sw.Close();
@ -67,7 +67,7 @@ namespace Entum
}
catch (Exception e)
{
Debug.LogError("ファイル書き出し失敗!" + e.Message + e.StackTrace);
Debug.LogError("파일 쓰기 실패!" + e.Message + e.StackTrace);
}
if (sw != null)

View File

@ -2108,5 +2108,178 @@ namespace Entum
[SerializeField]
public SummaryInfo Summary = new SummaryInfo();
#if UNITY_EDITOR
/// <summary>
/// 선택된 여러 HumanoidPoses 에셋에서 휴머노이드 애니메이션 일괄 출력
/// </summary>
[MenuItem("Assets/Easy Motion Recorder/Batch Export Humanoid Animations", false, 1)]
public static void BatchExportHumanoidAnimations()
{
var selectedHumanoidPoses = GetSelectedHumanoidPoses();
if (selectedHumanoidPoses.Length == 0)
{
EditorUtility.DisplayDialog("오류", "HumanoidPoses 에셋을 하나 이상 선택해주세요.", "확인");
return;
}
BatchExportProcess(selectedHumanoidPoses, "휴머노이드 애니메이션", (pose) => pose.ExportHumanoidAnim());
}
/// <summary>
/// 선택된 여러 HumanoidPoses 에셋에서 제네릭 애니메이션 일괄 출력
/// </summary>
[MenuItem("Assets/Easy Motion Recorder/Batch Export Generic Animations", false, 2)]
public static void BatchExportGenericAnimations()
{
var selectedHumanoidPoses = GetSelectedHumanoidPoses();
if (selectedHumanoidPoses.Length == 0)
{
EditorUtility.DisplayDialog("오류", "HumanoidPoses 에셋을 하나 이상 선택해주세요.", "확인");
return;
}
BatchExportProcess(selectedHumanoidPoses, "제네릭 애니메이션", (pose) => pose.ExportGenericAnim());
}
/// <summary>
/// 선택된 여러 HumanoidPoses 에셋에서 FBX ASCII 일괄 출력
/// </summary>
[MenuItem("Assets/Easy Motion Recorder/Batch Export FBX ASCII", false, 3)]
public static void BatchExportFBXAscii()
{
var selectedHumanoidPoses = GetSelectedHumanoidPoses();
if (selectedHumanoidPoses.Length == 0)
{
EditorUtility.DisplayDialog("오류", "HumanoidPoses 에셋을 하나 이상 선택해주세요.", "확인");
return;
}
BatchExportProcess(selectedHumanoidPoses, "FBX ASCII", (pose) => pose.ExportFBXAscii());
}
/// <summary>
/// 선택된 여러 HumanoidPoses 에셋에서 FBX Binary 일괄 출력
/// </summary>
[MenuItem("Assets/Easy Motion Recorder/Batch Export FBX Binary", false, 4)]
public static void BatchExportFBXBinary()
{
var selectedHumanoidPoses = GetSelectedHumanoidPoses();
if (selectedHumanoidPoses.Length == 0)
{
EditorUtility.DisplayDialog("오류", "HumanoidPoses 에셋을 하나 이상 선택해주세요.", "확인");
return;
}
BatchExportProcess(selectedHumanoidPoses, "FBX Binary", (pose) => pose.ExportFBXBinary());
}
/// <summary>
/// 현재 선택된 오브젝트에서 HumanoidPoses 에셋들을 찾아 반환
/// </summary>
private static HumanoidPoses[] GetSelectedHumanoidPoses()
{
var selectedObjects = Selection.objects;
var humanoidPosesList = new List<HumanoidPoses>();
foreach (var obj in selectedObjects)
{
if (obj is HumanoidPoses humanoidPoses)
{
humanoidPosesList.Add(humanoidPoses);
}
}
return humanoidPosesList.ToArray();
}
/// <summary>
/// 일괄 출력 프로세스 실행
/// </summary>
private static void BatchExportProcess(HumanoidPoses[] poses, string exportType, System.Action<HumanoidPoses> exportAction)
{
if (poses.Length == 0) return;
bool confirmed = EditorUtility.DisplayDialog(
"일괄 출력 확인",
$"{poses.Length}개의 HumanoidPoses 에셋에서 {exportType} 파일을 출력하시겠습니까?",
"출력 시작",
"취소");
if (!confirmed) return;
int successCount = 0;
int failCount = 0;
float progress = 0f;
try
{
for (int i = 0; i < poses.Length; i++)
{
var pose = poses[i];
progress = (float)i / poses.Length;
string assetName = pose.name;
bool cancelled = EditorUtility.DisplayCancelableProgressBar(
$"{exportType} 일괄 출력 중...",
$"처리 중: {assetName} ({i + 1}/{poses.Length})",
progress);
if (cancelled)
{
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog("취소됨", "일괄 출력이 사용자에 의해 취소되었습니다.", "확인");
return;
}
try
{
Debug.Log($"[일괄 출력] {exportType} 처리 시작: {assetName}");
exportAction(pose);
successCount++;
Debug.Log($"[일괄 출력] {exportType} 처리 완료: {assetName}");
}
catch (System.Exception e)
{
failCount++;
Debug.LogError($"[일괄 출력] {exportType} 처리 실패: {assetName} - {e.Message}");
}
}
}
finally
{
EditorUtility.ClearProgressBar();
// 결과 다이얼로그 표시
string message = $"{exportType} 일괄 출력이 완료되었습니다.\n\n" +
$"성공: {successCount}개\n" +
$"실패: {failCount}개\n" +
$"총 처리: {poses.Length}개";
if (failCount > 0)
{
EditorUtility.DisplayDialog("일괄 출력 완료 (일부 실패)", message, "확인");
}
else
{
EditorUtility.DisplayDialog("일괄 출력 완료", message, "확인");
}
Debug.Log($"[일괄 출력 완료] {exportType}: 성공 {successCount}개, 실패 {failCount}개");
}
}
/// <summary>
/// 메뉴 아이템 검증: HumanoidPoses 에셋이 선택되었을 때만 메뉴 활성화
/// </summary>
[MenuItem("Assets/Easy Motion Recorder/Batch Export Humanoid Animations", true)]
[MenuItem("Assets/Easy Motion Recorder/Batch Export Generic Animations", true)]
[MenuItem("Assets/Easy Motion Recorder/Batch Export FBX ASCII", true)]
[MenuItem("Assets/Easy Motion Recorder/Batch Export FBX Binary", true)]
public static bool ValidateBatchExport()
{
return GetSelectedHumanoidPoses().Length > 0;
}
#endif
}
}

View File

@ -13,10 +13,10 @@ using System;
namespace Entum
{
/// <summary>
/// モーションデータ再生クラス
/// SpringBone, DynamicBone, BulletPhysicsImplなどの揺れ物アセットのScript Execution Orderを20000など
/// 大きな値にしてください。
/// DefaultExecutionOrder(11000) はVRIK系より処理順を遅くする、という意図です
/// 모션 데이터 재생 클래스
/// SpringBone, DynamicBone, BulletPhysicsImpl 등의 흔들리는 에셋의 Script Execution Order를 20000 등
/// 큰 값으로 설정해주세요.
/// DefaultExecutionOrder(11000)은 VRIK계보다 처리 순서를 느리게 한다는 의도입니다
/// </summary>
[DefaultExecutionOrder(11000)]
public class MotionDataPlayer : MonoBehaviour
@ -31,7 +31,7 @@ namespace Entum
[SerializeField]
private Animator _animator;
[SerializeField, Tooltip("再生開始フレームを指定します。0だとファイル先頭から開始です")]
[SerializeField, Tooltip("재생 시작 프레임을 지정합니다. 0이면 파일 시작부터 시작합니다")]
private int _startFrame;
[SerializeField]
private bool _playing;
@ -75,7 +75,7 @@ namespace Entum
_onPlayFinish += StopMotion;
}
// Update is called once per frame
// 매 프레임마다 호출
private void Update()
{
if (Input.GetKeyDown(_playStartKey))
@ -101,7 +101,7 @@ namespace Entum
}
/// <summary>
/// モーションデータ再生開始
/// 모션 데이터 재생 시작
/// </summary>
private void PlayMotion()
{

View File

@ -22,9 +22,9 @@ using UniHumanoid;
namespace Entum
{
/// <summary>
/// モーションデータ記録クラス
/// スクリプト実行順はVRIKの処理が終わった後の姿勢を取得したいので
/// 最大値=32000を指定しています
/// 모션 데이터 기록 클래스
/// 스크립트 실행 순서는 VRIK 처리가 끝난 후의 자세를 취득하기 위해
/// 최대값 32000을 지정합니다
/// </summary>
[DefaultExecutionOrder(32000)]
public class MotionDataRecorder : MonoBehaviour
@ -68,10 +68,10 @@ namespace Entum
public Action OnRecordStart;
public Action OnRecordEnd;
[Tooltip("記録するFPS。0で制限しない。UpdateのFPSは超えられません。")]
[Tooltip("기록할 FPS. 0으로 설정하면 제한하지 않습니다. Update FPS를 초과할 수 없습니다.")]
public float TargetFPS = 60.0f;
// Use this for initialization
// 초기화용
private void Awake()
{
if (_animator == null)
@ -148,7 +148,7 @@ namespace Entum
}
}
// Update is called once per frame
// 매 프레임마다 호출
private void LateUpdate()
{
if (!_recording)
@ -179,7 +179,7 @@ namespace Entum
}
}
//現在のフレームのHumanoidの姿勢を取得
// 현재 프레임의 Humanoid 자세를 취득
if (_poseHandler == null)
{
Debug.LogError("PoseHandler가 초기화되지 않았습니다. 녹화를 중단합니다.");
@ -188,7 +188,7 @@ namespace Entum
}
_poseHandler.GetHumanPose(ref _currentPose);
//posesに取得한姿勢를書き込む
// poses에 취득한 자세를 기록
var serializedPose = new HumanoidPoses.SerializeHumanoidPose();
switch (_rootBoneSystem)
@ -312,6 +312,10 @@ namespace Entum
Debug.Log($"모션 녹화 시작 - 인스턴스: {instanceID}, 세션: {SessionID}, T포즈 옵션: {_recordTPoseAtStart}");
// 이벤트 구독자 확인
int subscriberCount = OnRecordStart?.GetInvocationList()?.Length ?? 0;
Debug.Log($"OnRecordStart 이벤트 구독자 수: {subscriberCount}");
OnRecordStart?.Invoke();
}
@ -659,18 +663,15 @@ namespace Entum
{
try
{
string animPath = Path.Combine(_savePathManager.GetMotionSavePath(), $"{baseFileName}_Humanoid.anim");
animPath = _savePathManager.GetInstanceSpecificPath(animPath);
// 직접 휴머노이드 애니메이션 클립 생성
var clip = CreateHumanoidAnimationClip();
if (clip != null)
// Poses 에셋에서 직접 휴머노이드 애니메이션 출력
if (Poses != null)
{
SavePathManager.SafeCreateDirectory(Path.GetDirectoryName(animPath));
AssetDatabase.CreateAsset(clip, animPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log($"휴머노이드 애니메이션 출력 완료: {animPath}");
Poses.ExportHumanoidAnim();
Debug.Log($"휴머노이드 애니메이션 출력 완료 (HumanoidPoses.ExportHumanoidAnim 사용)");
}
else
{
Debug.LogWarning("Poses가 null이어서 휴머노이드 애니메이션을 출력할 수 없습니다.");
}
}
catch (System.Exception e)
@ -683,18 +684,15 @@ namespace Entum
{
try
{
string animPath = Path.Combine(_savePathManager.GetMotionSavePath(), $"{baseFileName}_Generic.anim");
animPath = _savePathManager.GetInstanceSpecificPath(animPath);
// 직접 제네릭 애니메이션 클립 생성
var clip = CreateGenericAnimationClip();
if (clip != null)
// Poses 에셋에서 직접 제네릭 애니메이션 출력
if (Poses != null)
{
SavePathManager.SafeCreateDirectory(Path.GetDirectoryName(animPath));
AssetDatabase.CreateAsset(clip, animPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log($"제네릭 애니메이션 출력 완료: {animPath}");
Poses.ExportGenericAnim();
Debug.Log($"제네릭 애니메이션 출력 완료 (HumanoidPoses.ExportGenericAnim 사용)");
}
else
{
Debug.LogWarning("Poses가 null이어서 제네릭 애니메이션을 출력할 수 없습니다.");
}
}
catch (System.Exception e)
@ -727,29 +725,29 @@ namespace Entum
}
}
private AnimationClip CreateHumanoidAnimationClip()
// 더 이상 사용하지 않음 - HumanoidPoses.ExportHumanoidAnim() 직접 사용
/*private AnimationClip CreateHumanoidAnimationClip()
{
if (Poses == null || Poses.Poses.Count == 0) return null;
var clip = new AnimationClip { frameRate = 30 };
var clip = new AnimationClip();
clip.frameRate = 30;
// Humanoid 애니메이션 설정
// 중요: 휴머노이드 애니메이션으로 명시적 설정
clip.legacy = false;
// HumanoidPoses와 동일한 애니메이션 클립 설정
var settings = new AnimationClipSettings
{
loopTime = false,
cycleOffset = 0,
loopBlend = false,
loopBlendOrientation = true,
loopBlendPositionY = true,
loopBlendPositionXZ = true,
keepOriginalOrientation = true,
keepOriginalPositionY = true,
keepOriginalPositionXZ = true,
heightFromFeet = false,
mirror = false,
hasAdditiveReferencePose = false,
additiveReferencePoseTime = 0
loopTime = false, // Loop Time: false
cycleOffset = 0, // Cycle Offset: 0
startTime = 0, // Start Time: 0
stopTime = Poses.Poses.Count > 0 ? Poses.Poses.Last().Time : 1f, // Stop Time
orientationOffsetY = 0, // Orientation Offset Y: 0
level = 0, // Level: 0
mirror = false // Mirror: false
};
AnimationUtility.SetAnimationClipSettings(clip, settings);
// Muscles 데이터를 커브로 변환
@ -776,15 +774,228 @@ namespace Entum
string muscleName = HumanTrait.MuscleName[i];
clip.SetCurve("", typeof(Animator), muscleName, muscleCurves[i]);
}
// HumanoidPoses와 동일한 방식으로 Root Motion 설정
// 1. Body Position (RootT.x/y/z)
{
var curveX = new AnimationCurve();
var curveY = new AnimationCurve();
var curveZ = new AnimationCurve();
// T-포즈가 있으면 0프레임에 먼저 추가
if (Poses.HasTPoseData && Poses.TPoseData != null)
{
curveX.AddKey(Poses.TPoseData.Time, Poses.TPoseData.BodyPosition.x);
curveY.AddKey(Poses.TPoseData.Time, Poses.TPoseData.BodyPosition.y);
curveZ.AddKey(Poses.TPoseData.Time, Poses.TPoseData.BodyPosition.z);
}
// 실제 녹화된 포즈들 추가
foreach (var pose in Poses.Poses)
{
curveX.AddKey(pose.Time, pose.BodyPosition.x);
curveY.AddKey(pose.Time, pose.BodyPosition.y);
curveZ.AddKey(pose.Time, pose.BodyPosition.z);
}
clip.SetCurve("", typeof(Animator), "RootT.x", curveX);
clip.SetCurve("", typeof(Animator), "RootT.y", curveY);
clip.SetCurve("", typeof(Animator), "RootT.z", curveZ);
}
// 2. Body Rotation (RootQ.x/y/z/w)
{
var curveX = new AnimationCurve();
var curveY = new AnimationCurve();
var curveZ = new AnimationCurve();
var curveW = new AnimationCurve();
// T-포즈가 있으면 0프레임에 먼저 추가
if (Poses.HasTPoseData && Poses.TPoseData != null)
{
curveX.AddKey(Poses.TPoseData.Time, Poses.TPoseData.BodyRotation.x);
curveY.AddKey(Poses.TPoseData.Time, Poses.TPoseData.BodyRotation.y);
curveZ.AddKey(Poses.TPoseData.Time, Poses.TPoseData.BodyRotation.z);
curveW.AddKey(Poses.TPoseData.Time, Poses.TPoseData.BodyRotation.w);
}
// 실제 녹화된 포즈들 추가
foreach (var pose in Poses.Poses)
{
curveX.AddKey(pose.Time, pose.BodyRotation.x);
curveY.AddKey(pose.Time, pose.BodyRotation.y);
curveZ.AddKey(pose.Time, pose.BodyRotation.z);
curveW.AddKey(pose.Time, pose.BodyRotation.w);
}
clip.SetCurve("", typeof(Animator), "RootQ.x", curveX);
clip.SetCurve("", typeof(Animator), "RootQ.y", curveY);
clip.SetCurve("", typeof(Animator), "RootQ.z", curveZ);
clip.SetCurve("", typeof(Animator), "RootQ.w", curveW);
}
// 3. Left Foot IK Position (LeftFootT.x/y/z)
{
var curveX = new AnimationCurve();
var curveY = new AnimationCurve();
var curveZ = new AnimationCurve();
// T-포즈가 있으면 0프레임에 먼저 추가
if (Poses.HasTPoseData && Poses.TPoseData != null)
{
curveX.AddKey(Poses.TPoseData.Time, Poses.TPoseData.LeftfootIK_Pos.x);
curveY.AddKey(Poses.TPoseData.Time, Poses.TPoseData.LeftfootIK_Pos.y);
curveZ.AddKey(Poses.TPoseData.Time, Poses.TPoseData.LeftfootIK_Pos.z);
}
foreach (var pose in Poses.Poses)
{
curveX.AddKey(pose.Time, pose.LeftfootIK_Pos.x);
curveY.AddKey(pose.Time, pose.LeftfootIK_Pos.y);
curveZ.AddKey(pose.Time, pose.LeftfootIK_Pos.z);
}
clip.SetCurve("", typeof(Animator), "LeftFootT.x", curveX);
clip.SetCurve("", typeof(Animator), "LeftFootT.y", curveY);
clip.SetCurve("", typeof(Animator), "LeftFootT.z", curveZ);
}
// 4. Right Foot IK Position (RightFootT.x/y/z)
{
var curveX = new AnimationCurve();
var curveY = new AnimationCurve();
var curveZ = new AnimationCurve();
// T-포즈가 있으면 0프레임에 먼저 추가
if (Poses.HasTPoseData && Poses.TPoseData != null)
{
curveX.AddKey(Poses.TPoseData.Time, Poses.TPoseData.RightfootIK_Pos.x);
curveY.AddKey(Poses.TPoseData.Time, Poses.TPoseData.RightfootIK_Pos.y);
curveZ.AddKey(Poses.TPoseData.Time, Poses.TPoseData.RightfootIK_Pos.z);
}
foreach (var pose in Poses.Poses)
{
curveX.AddKey(pose.Time, pose.RightfootIK_Pos.x);
curveY.AddKey(pose.Time, pose.RightfootIK_Pos.y);
curveZ.AddKey(pose.Time, pose.RightfootIK_Pos.z);
}
clip.SetCurve("", typeof(Animator), "RightFootT.x", curveX);
clip.SetCurve("", typeof(Animator), "RightFootT.y", curveY);
clip.SetCurve("", typeof(Animator), "RightFootT.z", curveZ);
}
// 5. Left Foot IK Rotation (LeftFootQ.x/y/z/w)
{
var curveX = new AnimationCurve();
var curveY = new AnimationCurve();
var curveZ = new AnimationCurve();
var curveW = new AnimationCurve();
// T-포즈가 있으면 0프레임에 먼저 추가
if (Poses.HasTPoseData && Poses.TPoseData != null)
{
curveX.AddKey(Poses.TPoseData.Time, Poses.TPoseData.LeftfootIK_Rot.x);
curveY.AddKey(Poses.TPoseData.Time, Poses.TPoseData.LeftfootIK_Rot.y);
curveZ.AddKey(Poses.TPoseData.Time, Poses.TPoseData.LeftfootIK_Rot.z);
curveW.AddKey(Poses.TPoseData.Time, Poses.TPoseData.LeftfootIK_Rot.w);
}
foreach (var pose in Poses.Poses)
{
curveX.AddKey(pose.Time, pose.LeftfootIK_Rot.x);
curveY.AddKey(pose.Time, pose.LeftfootIK_Rot.y);
curveZ.AddKey(pose.Time, pose.LeftfootIK_Rot.z);
curveW.AddKey(pose.Time, pose.LeftfootIK_Rot.w);
}
clip.SetCurve("", typeof(Animator), "LeftFootQ.x", curveX);
clip.SetCurve("", typeof(Animator), "LeftFootQ.y", curveY);
clip.SetCurve("", typeof(Animator), "LeftFootQ.z", curveZ);
clip.SetCurve("", typeof(Animator), "LeftFootQ.w", curveW);
}
// 6. Right Foot IK Rotation (RightFootQ.x/y/z/w)
{
var curveX = new AnimationCurve();
var curveY = new AnimationCurve();
var curveZ = new AnimationCurve();
var curveW = new AnimationCurve();
// T-포즈가 있으면 0프레임에 먼저 추가
if (Poses.HasTPoseData && Poses.TPoseData != null)
{
curveX.AddKey(Poses.TPoseData.Time, Poses.TPoseData.RightfootIK_Rot.x);
curveY.AddKey(Poses.TPoseData.Time, Poses.TPoseData.RightfootIK_Rot.y);
curveZ.AddKey(Poses.TPoseData.Time, Poses.TPoseData.RightfootIK_Rot.z);
curveW.AddKey(Poses.TPoseData.Time, Poses.TPoseData.RightfootIK_Rot.w);
}
foreach (var pose in Poses.Poses)
{
curveX.AddKey(pose.Time, pose.RightfootIK_Rot.x);
curveY.AddKey(pose.Time, pose.RightfootIK_Rot.y);
curveZ.AddKey(pose.Time, pose.RightfootIK_Rot.z);
curveW.AddKey(pose.Time, pose.RightfootIK_Rot.w);
}
clip.SetCurve("", typeof(Animator), "RightFootQ.x", curveX);
clip.SetCurve("", typeof(Animator), "RightFootQ.y", curveY);
clip.SetCurve("", typeof(Animator), "RightFootQ.z", curveZ);
clip.SetCurve("", typeof(Animator), "RightFootQ.w", curveW);
}
// HumanoidPoses와 동일하게 쿼터니언 연속성 보장
clip.EnsureQuaternionContinuity();
// 2. 애니메이션 클립 타입 명시적 설정 (여러번 시도)
try
{
AnimationUtility.SetAnimationType(clip, ModelImporterAnimationType.Human);
Debug.Log("휴머노이드 애니메이션 타입 설정 성공");
}
catch (System.Exception e)
{
Debug.LogWarning($"휴머노이드 애니메이션 타입 설정 실패: {e.Message}");
}
// 3. 애니메이션 커브 최적화
foreach (var binding in AnimationUtility.GetCurveBindings(clip))
{
var curve = AnimationUtility.GetEditorCurve(clip, binding);
if (curve != null && curve.keys.Length > 1)
{
// 키프레임 최적화
for (int i = 0; i < curve.keys.Length; i++)
{
AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.ClampedAuto);
AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.ClampedAuto);
}
AnimationUtility.SetEditorCurve(clip, binding, curve);
}
}
// 4. 휴머노이드 확인을 위한 이벤트 추가
AnimationEvent humanoidEvent = new AnimationEvent();
humanoidEvent.time = 0f;
humanoidEvent.functionName = "HumanoidMotionStart";
humanoidEvent.stringParameter = "Humanoid";
clip.events = new AnimationEvent[] { humanoidEvent };
return clip;
}
}*/
private AnimationClip CreateGenericAnimationClip()
// 더 이상 사용하지 않음 - HumanoidPoses.ExportGenericAnim() 직접 사용
/*private AnimationClip CreateGenericAnimationClip()
{
if (Poses == null || Poses.Poses.Count == 0) return null;
var clip = new AnimationClip { frameRate = 30 };
var clip = new AnimationClip();
clip.frameRate = 30;
// 중요: 제네릭 애니메이션으로 명시적 설정
clip.legacy = false;
// 본별 커브 생성
var boneCurves = new Dictionary<string, Dictionary<string, AnimationCurve>>();
@ -833,6 +1044,36 @@ namespace Entum
}
}
// 애니메이션 클립 타입 명시적 설정 (제네릭)
try
{
AnimationUtility.SetAnimationType(clip, ModelImporterAnimationType.Generic);
Debug.Log("제네릭 애니메이션 타입 설정 성공");
}
catch (System.Exception e)
{
Debug.LogWarning($"제네릭 애니메이션 타입 설정 실패: {e.Message}");
}
// 애니메이션 커브 최적화 (제네릭)
foreach (var bonePair in boneCurves)
{
var curves = bonePair.Value;
foreach (var curvePair in curves)
{
var curve = curvePair.Value;
if (curve != null && curve.keys.Length > 1)
{
// 키프레임 최적화
for (int i = 0; i < curve.keys.Length; i++)
{
AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.ClampedAuto);
AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.ClampedAuto);
}
}
}
}
// 손가락 본들이 포함되었는지 로깅
var fingerBoneCount = boneCurves.Keys.Count(name =>
name.Contains("Thumb") || name.Contains("Index") || name.Contains("Middle") ||
@ -840,7 +1081,7 @@ namespace Entum
Debug.Log($"제네릭 애니메이션 클립 생성 완료 - 총 {boneCurves.Count}개 본, 손가락 본 {fingerBoneCount}개 포함");
return clip;
}
}*/
#endif
private void UpdateSummaryInfo()