스크립트 업데이트

This commit is contained in:
Yamo4490 2025-08-01 07:49:42 +09:00
parent 7c1372088e
commit a93dcf7ec8
25 changed files with 3253 additions and 2756 deletions

View File

@ -3,52 +3,37 @@ using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace Entum
namespace EasyMotionRecorder
{
public class CharacterFacialData : ScriptableObject
{
[System.SerializableAttribute]
public class SerializeHumanoidFace
{
public class MeshAndBlendshape
{
public string path;
public float[] blendShapes;
}
public int BlendShapeNum()
{
return Smeshes.Count == 0 ? 0 : Smeshes.Sum(t => t.blendShapes.Length);
}
//フレーム数
public int FrameCount;
//記録開始後の経過時間。処理落ち対策
public float Time;
public SerializeHumanoidFace(SerializeHumanoidFace serializeHumanoidFace)
{
for (int i = 0; i < serializeHumanoidFace.Smeshes.Count; i++)
{
Smeshes.Add(serializeHumanoidFace.Smeshes[i]);
Array.Copy(serializeHumanoidFace.Smeshes[i].blendShapes,Smeshes[i].blendShapes,
serializeHumanoidFace.Smeshes[i].blendShapes.Length);
}
FrameCount = serializeHumanoidFace.FrameCount;
Time = serializeHumanoidFace.Time;
}
//単一フレームの中でも、口のメッシュや目のメッシュなどが個別にここに入る
public List<MeshAndBlendshape> Smeshes= new List<MeshAndBlendshape>();
public SerializeHumanoidFace()
{
}
}
[Serializable]
public class CharacterFacialData : ScriptableObject
{
[SerializeField]
public string SessionID = "";
public List<SerializeHumanoidFace> Facials = new List<SerializeHumanoidFace>();
}
[SerializeField]
public string InstanceID = "";
[SerializeField]
public List<SerializeHumanoidFace> Faces = new List<SerializeHumanoidFace>();
[Serializable]
public class SerializeHumanoidFace
{
[SerializeField]
public List<string> BlendShapeNames = new List<string>();
[SerializeField]
public List<float> BlendShapeValues = new List<float>();
[SerializeField]
public List<string> SkinnedMeshRendererNames = new List<string>();
[SerializeField]
public int FrameCount;
[SerializeField]
public float Time;
}
}
}

View File

@ -436,27 +436,7 @@ namespace Entum
EditorGUILayout.Space(8);
// 네 번째 행 - Biped FBX 내보내기
EditorGUILayout.BeginHorizontal();
GUI.backgroundColor = new Color(0.4f, 0.8f, 0.2f); // 연한 초록색
if (GUILayout.Button("🤖 Biped (Binary)", GUILayout.Height(30), GUILayout.ExpandWidth(true)))
{
ExportBipedFBXBinary(humanoidPoses);
}
GUILayout.Space(8);
GUI.backgroundColor = new Color(0.8f, 0.6f, 0.2f); // 연한 주황색
if (GUILayout.Button("🤖 Biped (ASCII)", GUILayout.Height(30), GUILayout.ExpandWidth(true)))
{
ExportBipedFBXAscii(humanoidPoses);
}
GUI.backgroundColor = oldColor;
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(8);
// 다섯 번째 행 - 유틸리티
EditorGUILayout.BeginHorizontal();
@ -582,43 +562,7 @@ namespace Entum
}
}
private void ExportBipedFBXBinary(HumanoidPoses humanoidPoses)
{
if (humanoidPoses.Poses == null || humanoidPoses.Poses.Count == 0)
{
EditorUtility.DisplayDialog("Biped FBX 내보내기", "내보낼 데이터가 없습니다.", "확인");
return;
}
try
{
humanoidPoses.ExportBipedFBXBinary();
EditorUtility.DisplayDialog("Biped FBX 내보내기 완료", "Binary 형식의 Biped FBX 파일이 내보내졌습니다.", "확인");
}
catch (System.Exception e)
{
EditorUtility.DisplayDialog("Biped FBX 내보내기 오류", $"Biped FBX 내보내기 중 오류가 발생했습니다:\n{e.Message}", "확인");
}
}
private void ExportBipedFBXAscii(HumanoidPoses humanoidPoses)
{
if (humanoidPoses.Poses == null || humanoidPoses.Poses.Count == 0)
{
EditorUtility.DisplayDialog("Biped FBX 내보내기", "내보낼 데이터가 없습니다.", "확인");
return;
}
try
{
humanoidPoses.ExportBipedFBXAscii();
EditorUtility.DisplayDialog("Biped FBX 내보내기 완료", "ASCII 형식의 Biped FBX 파일이 내보내졌습니다.", "확인");
}
catch (System.Exception e)
{
EditorUtility.DisplayDialog("Biped FBX 내보내기 오류", $"Biped FBX 내보내기 중 오류가 발생했습니다:\n{e.Message}", "확인");
}
}
private float EstimateFileSize(HumanoidPoses humanoidPoses)
{

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,8 +37,10 @@ namespace Entum {
[Tooltip("記録するFPS。0で制限しない。UpdateのFPSは超えられません。")]
public float TargetFPS = 60.0f;
private MotionDataRecorder _animRecorder;
[HideInInspector, SerializeField] private string instanceID = "";
[HideInInspector, SerializeField] private SavePathManager _savePathManager;
private MotionDataRecorder _animRecorder;
private SkinnedMeshRenderer[] _smeshs;
@ -48,8 +50,11 @@ namespace Entum {
private int _frameCount = 0;
CharacterFacialData.SerializeHumanoidFace _past = new CharacterFacialData.SerializeHumanoidFace();
CharacterFacialData.SerializeHumanoidFace _past = new CharacterFacialData.SerializeHumanoidFace() {
BlendShapeNames = new List<string>(),
BlendShapeValues = new List<float>(),
SkinnedMeshRendererNames = new List<string>()
};
private float _recordedTime = 0f;
private float _startTime;
@ -59,11 +64,30 @@ namespace Entum {
_animRecorder = GetComponent<MotionDataRecorder>();
_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);
}
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>();
@ -98,258 +122,334 @@ namespace Entum {
return;
}
if(_recording) {
return;
}
_facialData = ScriptableObject.CreateInstance<CharacterFacialData>();
_facialData.Faces = new List<CharacterFacialData.SerializeHumanoidFace>();
_facialData.SessionID = _animRecorder.SessionID;
_facialData.InstanceID = instanceID;
if(_smeshs.Length == 0) {
Debug.LogError("顔のメッシュ指定がされていないので顔のアニメーションは記録しません");
return;
}
Debug.Log("FaceAnimationRecorder record start");
_recording = true;
_frameCount = 0;
_recordedTime = 0f;
_startTime = Time.time;
_frameCount = 0;
_facialData = ScriptableObject.CreateInstance<CharacterFacialData>();
Debug.Log($"표정 애니메이션 녹화 시작 - 인스턴스: {instanceID}");
}
private void RecordEnd() {
if(_recording == false) {
return;
}
_recording = false;
Debug.Log($"표정 애니메이션 녹화 종료 - 인스턴스: {instanceID}, 총 프레임: {_frameCount}");
// 바로 애니메이션 클립으로 출력
ExportFacialAnimationClip(_animRecorder.CharacterAnimator, _facialData);
}
private void ExportFacialAnimationClip(Animator root, CharacterFacialData facial) {
if(facial == null || facial.Faces.Count == 0) {
Debug.LogError("저장할 표정 데이터가 없습니다.");
return;
}
var clip = new AnimationClip();
clip.frameRate = 30;
// 블렌드쉐이프와 렌더러별로 애니메이션 커브를 그룹화
var rendererCurves = new Dictionary<string, Dictionary<string, AnimationCurve>>();
// 첫 번째 프레임에서 모든 블렌드쉐이프와 렌더러 정보 수집
if (facial.Faces.Count > 0)
{
var firstFace = facial.Faces[0];
// 기존 데이터 호환성 체크 (SkinnedMeshRendererNames가 없는 경우)
bool hasRendererNames = firstFace.SkinnedMeshRendererNames != null &&
firstFace.SkinnedMeshRendererNames.Count > 0;
for(int i = 0; i < firstFace.BlendShapeNames.Count; i++) {
var blendShapeName = firstFace.BlendShapeNames[i];
var rendererName = hasRendererNames && i < firstFace.SkinnedMeshRendererNames.Count
? firstFace.SkinnedMeshRendererNames[i]
: "DefaultRenderer"; // 기존 데이터 호환성을 위한 기본값
if (!rendererCurves.ContainsKey(rendererName)) {
rendererCurves[rendererName] = new Dictionary<string, AnimationCurve>();
}
if (!rendererCurves[rendererName].ContainsKey(blendShapeName)) {
rendererCurves[rendererName][blendShapeName] = new AnimationCurve();
}
}
if (!hasRendererNames) {
Debug.LogWarning("기존 데이터 형식 감지: SkinnedMeshRenderer 정보가 없습니다. 기본 경로를 사용합니다.");
}
}
// 모든 프레임의 데이터를 커브에 추가
for(int frameIdx = 0; frameIdx < facial.Faces.Count; frameIdx++) {
var face = facial.Faces[frameIdx];
var time = face.Time;
// 기존 데이터 호환성 체크
bool hasRendererNames = face.SkinnedMeshRendererNames != null &&
face.SkinnedMeshRendererNames.Count > 0;
for(int i = 0; i < face.BlendShapeNames.Count; i++) {
var blendShapeName = face.BlendShapeNames[i];
var value = face.BlendShapeValues[i];
var rendererName = hasRendererNames && i < face.SkinnedMeshRendererNames.Count
? face.SkinnedMeshRendererNames[i]
: "DefaultRenderer"; // 기존 데이터 호환성을 위한 기본값
if (rendererCurves.ContainsKey(rendererName) &&
rendererCurves[rendererName].ContainsKey(blendShapeName)) {
rendererCurves[rendererName][blendShapeName].AddKey(time, value);
}
}
}
// 렌더러별로 애니메이션 커브를 클립에 추가
foreach(var rendererPair in rendererCurves) {
var rendererName = rendererPair.Key;
var curves = rendererPair.Value;
string rendererPath;
// 기존 데이터 호환성: DefaultRenderer인 경우 빈 경로 사용
if (rendererName == "DefaultRenderer") {
rendererPath = "";
Debug.Log($"기존 데이터 호환성: 빈 경로로 {curves.Count}개 블렌드쉐이프 커브 추가");
} else {
// 해당 렌더러의 Transform 경로 찾기
rendererPath = FindRendererPath(root.transform, rendererName);
if (string.IsNullOrEmpty(rendererPath)) {
Debug.LogWarning($"렌더러 '{rendererName}'의 경로를 찾을 수 없습니다. 루트 경로를 사용합니다.");
rendererPath = rendererName;
}
Debug.Log($"렌더러 '{rendererName}' ({rendererPath})에 {curves.Count}개 블렌드쉐이프 커브 추가");
}
foreach(var curvePair in curves) {
var blendShapeName = curvePair.Key;
var curve = curvePair.Value;
clip.SetCurve(rendererPath, typeof(SkinnedMeshRenderer), "blendShape." + blendShapeName, curve);
}
}
// 캐릭터 이름 가져오기
string characterName = GetCharacterName();
string fileName = string.IsNullOrEmpty(characterName)
? $"{facial.SessionID}_Facial"
: $"{facial.SessionID}_{characterName}_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
}
/// <summary>
/// 記録終了
/// 지정된 렌더러 이름의 Transform 경로를 찾습니다.
/// </summary>
private void RecordEnd() {
if(_recordFaceBlendshapes == false) {
return;
private string FindRendererPath(Transform root, string rendererName) {
if (root.name == rendererName) {
return "";
}
if(_smeshs.Length == 0) {
Debug.LogError("顔のメッシュ指定がされていないので顔のアニメーションは記録しませんでした");
if(_recording == true) {
Debug.LogAssertion("Unexpected execution!!!!");
for (int i = 0; i < root.childCount; i++) {
var child = root.GetChild(i);
if (child.name == rendererName) {
return child.name;
}
string childPath = FindRendererPath(child, rendererName);
if (!string.IsNullOrEmpty(childPath)) {
return child.name + "/" + childPath;
}
}
else {
//WriteAnimationFileToScriptableObject();
ExportFacialAnimationClip(_animRecorder.CharacterAnimator, _facialData);
}
Debug.Log("FaceAnimationRecorder record end");
_recording = false;
return null;
}
private void WriteAnimationFileToScriptableObject() {
MotionDataRecorder.SafeCreateDirectory("Assets/Resources");
string path = AssetDatabase.GenerateUniqueAssetPath(
"Assets/Resources/RecordMotion_ face" + _animRecorder.CharacterAnimator.name +
DateTime.Now.ToString("yyyy_MM_dd_HH_mm_ss") +
".asset");
if(_facialData == null) {
Debug.LogError("記録されたFaceデータがnull");
private string GetCharacterName()
{
if (_animRecorder?.CharacterAnimator == null) return "";
var animator = _animRecorder.CharacterAnimator;
// 1. GameObject 이름 사용
string objectName = animator.gameObject.name;
// 2. Avatar 이름이 있으면 우선 사용
if (animator.avatar != null && !string.IsNullOrEmpty(animator.avatar.name))
{
string avatarName = animator.avatar.name;
// "Avatar" 접미사 제거
if (avatarName.EndsWith("Avatar"))
{
avatarName = avatarName.Substring(0, avatarName.Length - 6);
}
if (!string.IsNullOrEmpty(avatarName))
{
return SanitizeFileName(avatarName);
}
}
else {
AssetDatabase.CreateAsset(_facialData, path);
AssetDatabase.Refresh();
// 3. 부모 오브젝트에서 캐릭터 루트 찾기
Transform current = animator.transform.parent;
while (current != null)
{
// VRM, humanoid, character 등의 키워드가 있는 경우
string parentName = current.name.ToLower();
if (parentName.Contains("character") || parentName.Contains("humanoid") ||
parentName.Contains("avatar") || parentName.Contains("vrm"))
{
return SanitizeFileName(current.name);
}
current = current.parent;
}
_startTime = Time.time;
_recordedTime = 0f;
_frameCount = 0;
// 4. GameObject 이름에서 불필요한 부분 제거
objectName = objectName.Replace("(Clone)", "").Trim();
return SanitizeFileName(objectName);
}
private string SanitizeFileName(string fileName)
{
if (string.IsNullOrEmpty(fileName)) return "";
// 파일명에 사용할 수 없는 문자 제거
char[] invalidChars = Path.GetInvalidFileNameChars();
foreach (char c in invalidChars)
{
fileName = fileName.Replace(c, '_');
}
// 공백을 언더스코어로 변경
fileName = fileName.Replace(' ', '_');
// 연속된 언더스코어 제거
while (fileName.Contains("__"))
{
fileName = fileName.Replace("__", "_");
}
// 앞뒤 언더스코어 제거
fileName = fileName.Trim('_');
return fileName;
}
//フレーム内の差分が無いかをチェックするやつ。
private bool IsSame(CharacterFacialData.SerializeHumanoidFace a, CharacterFacialData.SerializeHumanoidFace b) {
if(a == null || b == null || a.Smeshes.Count == 0 || b.Smeshes.Count == 0) {
if(a.BlendShapeNames.Count != b.BlendShapeNames.Count) {
return false;
}
if(a.BlendShapeNum() != b.BlendShapeNum()) {
if(a.SkinnedMeshRendererNames.Count != b.SkinnedMeshRendererNames.Count) {
return false;
}
return !a.Smeshes.Where((t1, i) =>
t1.blendShapes.Where((t, j) => Mathf.Abs(t - b.Smeshes[i].blendShapes[j]) > 1).Any()).Any();
for(int i = 0; i < a.BlendShapeNames.Count; i++) {
if(a.BlendShapeValues[i] != b.BlendShapeValues[i]) {
return false;
}
if(a.BlendShapeNames[i] != b.BlendShapeNames[i]) {
return false;
}
// SkinnedMeshRenderer 이름도 비교
if(i < a.SkinnedMeshRendererNames.Count && i < b.SkinnedMeshRendererNames.Count) {
if(a.SkinnedMeshRendererNames[i] != b.SkinnedMeshRendererNames[i]) {
return false;
}
}
}
return true;
}
private void LateUpdate() {
if(Input.GetKeyDown(KeyCode.Y)) {
ExportFacialAnimationClipTest();
}
if(!_recording) {
if(_recording == false) {
return;
}
_recordedTime = Time.time - _startTime;
if(TargetFPS != 0.0f) {
var nextTime = (1.0f * (_frameCount + 1)) / TargetFPS;
if(nextTime > _recordedTime) {
if (TargetFPS != 0.0f)
{
var nextTime = (1.0f * _frameCount) / TargetFPS;
if (nextTime > _recordedTime)
{
return;
}
if(_frameCount % TargetFPS == 0) {
print("Face_FPS=" + 1 / (_recordedTime / _frameCount));
}
}
else {
if(Time.frameCount % Application.targetFrameRate == 0) {
print("Face_FPS=" + 1 / Time.deltaTime);
}
}
var current = new CharacterFacialData.SerializeHumanoidFace();
current.BlendShapeNames = new List<string>();
current.BlendShapeValues = new List<float>();
current.SkinnedMeshRendererNames = new List<string>();
var p = new CharacterFacialData.SerializeHumanoidFace();
for(int i = 0; i < _smeshs.Length; i++) {
var mesh = new CharacterFacialData.SerializeHumanoidFace.MeshAndBlendshape();
mesh.path = _smeshs[i].name;
mesh.blendShapes = new float[_smeshs[i].sharedMesh.blendShapeCount];
var mesh = _smeshs[i];
var blendShapeCount = mesh.sharedMesh.blendShapeCount;
for(int j = 0; j < _smeshs[i].sharedMesh.blendShapeCount; j++) {
var tname = _smeshs[i].sharedMesh.GetBlendShapeName(j);
var useThis = true;
foreach(var item in _exclusiveBlendshapeNames) {
if(item.IndexOf(tname, StringComparison.Ordinal) >= 0) {
useThis = false;
for(int j = 0; j < blendShapeCount; j++) {
var blendShapeName = mesh.sharedMesh.GetBlendShapeName(j);
// 제외할 블렌드셰이프인지 확인
bool isExcluded = false;
for(int k = 0; k < _exclusiveBlendshapeNames.Count; k++) {
if(blendShapeName.Contains(_exclusiveBlendshapeNames[k])) {
isExcluded = true;
break;
}
}
if(useThis) {
mesh.blendShapes[j] = _smeshs[i].GetBlendShapeWeight(j);
if(isExcluded) {
continue;
}
}
p.Smeshes.Add(mesh);
var weight = mesh.GetBlendShapeWeight(j);
current.BlendShapeNames.Add(blendShapeName);
current.BlendShapeValues.Add(weight);
current.SkinnedMeshRendererNames.Add(mesh.name);
}
}
if(!IsSame(p, _past)) {
p.FrameCount = _frameCount;
p.Time = _recordedTime;
_facialData.Facials.Add(p);
_past = new CharacterFacialData.SerializeHumanoidFace(p);
if(IsSame(current, _past) == false) {
current.FrameCount = _frameCount;
current.Time = _recordedTime;
_facialData.Faces.Add(current);
_past = current;
}
_frameCount++;
}
/// <summary>
/// Animatorと記録したデータで書き込む
/// </summary>
/// <param name="root"></param>
/// <param name="facial"></param>
void ExportFacialAnimationClip(Animator root, CharacterFacialData facial) {
var animclip = new AnimationClip();
var mesh = _smeshs;
for(int faceTargetMeshIndex = 0; faceTargetMeshIndex < mesh.Length; faceTargetMeshIndex++) {
var pathsb = new StringBuilder().Append(mesh[faceTargetMeshIndex].transform.name);
var trans = mesh[faceTargetMeshIndex].transform;
while(trans.parent != null && trans.parent != root.transform) {
trans = trans.parent;
pathsb.Insert(0, "/").Insert(0, trans.name);
}
//pathにはBlendshapeのベース名が入る
//U_CHAR_1:SkinnedMeshRendererみたいなもの
var path = pathsb.ToString();
//個別メッシュの個別Blendshapeごとに、AnimationCurveを生成している
for(var blendShapeIndex = 0;
blendShapeIndex < mesh[faceTargetMeshIndex].sharedMesh.blendShapeCount;
blendShapeIndex++) {
var curveBinding = new EditorCurveBinding();
curveBinding.type = typeof(SkinnedMeshRenderer);
curveBinding.path = path;
curveBinding.propertyName = "blendShape." +
mesh[faceTargetMeshIndex].sharedMesh.GetBlendShapeName(blendShapeIndex);
AnimationCurve curve = new AnimationCurve();
float pastBlendshapeWeight = -1;
for(int k = 0; k < _facialData.Facials.Count; k++) {
if(!(Mathf.Abs(pastBlendshapeWeight - _facialData.Facials[k].Smeshes[faceTargetMeshIndex].blendShapes[blendShapeIndex]) >
0.1f)) continue;
curve.AddKey(new Keyframe(facial.Facials[k].Time, _facialData.Facials[k].Smeshes[faceTargetMeshIndex].blendShapes[blendShapeIndex], float.PositiveInfinity, 0f));
pastBlendshapeWeight = _facialData.Facials[k].Smeshes[faceTargetMeshIndex].blendShapes[blendShapeIndex];
}
AnimationUtility.SetEditorCurve(animclip, curveBinding, curve);
}
}
// SavePathManager 사용
string savePath = "Assets/Resources"; // 기본값
string fileName = $"{_animRecorder.SessionID}_{_animRecorder.CharacterAnimator.name}_Facial.anim";
// SavePathManager가 있으면 사용
if(SavePathManager.Instance != null) {
savePath = SavePathManager.Instance.GetFacialSavePath();
fileName = $"{_animRecorder.SessionID}_{_animRecorder.CharacterAnimator.name}_Facial.anim";
}
MotionDataRecorder.SafeCreateDirectory(savePath);
var outputPath = Path.Combine(savePath, fileName);
Debug.Log($"페이스 애니메이션 파일 저장 경로: {outputPath}");
AssetDatabase.CreateAsset(animclip,
AssetDatabase.GenerateUniqueAssetPath(outputPath));
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
public void SetInstanceID(string id)
{
instanceID = id;
}
/// <summary>
/// Animatorと記録したデータで書き込むテスト
/// </summary>
/// <param name="root"></param>
/// <param name="facial"></param>
void ExportFacialAnimationClipTest() {
var animclip = new AnimationClip();
var mesh = _smeshs;
for(int i = 0; i < mesh.Length; i++) {
var pathsb = new StringBuilder().Append(mesh[i].transform.name);
var trans = mesh[i].transform;
while(trans.parent != null && trans.parent != _animRecorder.CharacterAnimator.transform) {
trans = trans.parent;
pathsb.Insert(0, "/").Insert(0, trans.name);
}
var path = pathsb.ToString();
for(var j = 0; j < mesh[i].sharedMesh.blendShapeCount; j++) {
var curveBinding = new EditorCurveBinding();
curveBinding.type = typeof(SkinnedMeshRenderer);
curveBinding.path = path;
curveBinding.propertyName = "blendShape." + mesh[i].sharedMesh.GetBlendShapeName(j);
AnimationCurve curve = new AnimationCurve();
//全てのBlendshapeに対して0→100→0の遷移でキーを打つ
curve.AddKey(0, 0);
curve.AddKey(1, 100);
curve.AddKey(2, 0);
Debug.Log("path: " + curveBinding.path + "\r\nname: " + curveBinding.propertyName + " val:");
AnimationUtility.SetEditorCurve(animclip, curveBinding, curve);
}
}
AssetDatabase.CreateAsset(animclip,
AssetDatabase.GenerateUniqueAssetPath("Assets/" + _animRecorder.CharacterAnimator.name +
"_facial_ClipTest.anim"));
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
public string GetInstanceID()
{
return instanceID;
}
}
}

View File

@ -91,22 +91,28 @@ namespace Entum
[SerializeField, Tooltip("T-포즈가 저장되었는지 여부")]
public bool HasTPoseData = false;
[SerializeField]
public string SessionID = ""; // 세션 ID 저장
[SerializeField]
public string InstanceID = ""; // 인스턴스 ID 저장
// 세션 ID를 가져오는 메서드 (MotionDataRecorder와 동일한 세션 ID 사용)
#if UNITY_EDITOR
// 세션 ID를 가져오는 메서드 (다중 인스턴스 지원)
private string GetSessionID()
{
// 1. MotionDataRecorder에서 세션 ID를 가져오려고 시도
var motionRecorder = FindObjectOfType<MotionDataRecorder>();
if (motionRecorder != null && !string.IsNullOrEmpty(motionRecorder.SessionID))
// 1. 이미 저장된 세션 ID가 있으면 사용
if (!string.IsNullOrEmpty(SessionID))
{
Debug.Log($"MotionDataRecorder에서 세션 ID 가져옴: {motionRecorder.SessionID}");
return motionRecorder.SessionID;
Debug.Log($"저장된 세션 ID 사용: {SessionID}");
return SessionID;
}
// 2. 스크립터블 오브젝트의 이름에서 세션 ID 추출 시도
if (!string.IsNullOrEmpty(this.name))
{
// 파일명에서 세션 ID 패턴 찾기 (예: 250717_192404_SeNo_Motion)
// 파일명에서 세션 ID 패턴 찾기 (예: 250717_192404_Motion_abc12345)
var nameParts = this.name.Split('_');
if (nameParts.Length >= 2)
{
@ -142,6 +148,7 @@ namespace Entum
Debug.LogWarning($"세션 ID를 찾을 수 없어 현재 시간 사용: {fallbackSessionID}");
return fallbackSessionID;
}
#endif
#if UNITY_EDITOR
//Genericなanimファイルとして出力する
[ContextMenu("Export as Generic animation clips")]
@ -294,9 +301,32 @@ namespace Entum
// 아바타 이름이 있으면 포함, 없으면 기본값 사용
string avatarName = !string.IsNullOrEmpty(AvatarName) ? AvatarName : "Unknown";
// 인스턴스 ID 디버깅 로그
Debug.Log($"출력 시 인스턴스 ID 상태: '{InstanceID}' (비어있음: {string.IsNullOrEmpty(InstanceID)})");
// 인스턴스 ID가 비어있으면 현재 씬의 SavePathManager에서 가져오기
string currentInstanceID = InstanceID;
if (string.IsNullOrEmpty(currentInstanceID))
{
var savePathManager = FindObjectOfType<EasyMotionRecorder.SavePathManager>();
if (savePathManager != null)
{
currentInstanceID = savePathManager.InstanceID;
Debug.Log($"SavePathManager에서 인스턴스 ID 가져옴: '{currentInstanceID}'");
}
else
{
Debug.LogWarning("SavePathManager를 찾을 수 없습니다. 인스턴스 ID 없이 파일 생성됩니다.");
}
}
// 인스턴스 ID 포함 파일명 생성
string fileName = !string.IsNullOrEmpty(currentInstanceID)
? $"{sessionID}_{avatarName}_Generic_{currentInstanceID}.anim"
: $"{sessionID}_{avatarName}_Generic.anim";
// 에셋 파일의 경로를 기반으로 저장 경로 결정
string savePath = "Assets/Resources"; // 기본값
string fileName = $"{sessionID}_{avatarName}_Generic.anim";
// 현재 에셋 파일의 경로 가져오기
string assetPath = AssetDatabase.GetAssetPath(this);
@ -642,9 +672,32 @@ namespace Entum
// 아바타 이름이 있으면 포함, 없으면 기본값 사용
string avatarName = !string.IsNullOrEmpty(AvatarName) ? AvatarName : "Unknown";
// 인스턴스 ID 디버깅 로그
Debug.Log($"Humanoid 출력 시 인스턴스 ID 상태: '{InstanceID}' (비어있음: {string.IsNullOrEmpty(InstanceID)})");
// 인스턴스 ID가 비어있으면 현재 씬의 SavePathManager에서 가져오기
string currentInstanceID = InstanceID;
if (string.IsNullOrEmpty(currentInstanceID))
{
var savePathManager = FindObjectOfType<EasyMotionRecorder.SavePathManager>();
if (savePathManager != null)
{
currentInstanceID = savePathManager.InstanceID;
Debug.Log($"SavePathManager에서 인스턴스 ID 가져옴: '{currentInstanceID}'");
}
else
{
Debug.LogWarning("SavePathManager를 찾을 수 없습니다. 인스턴스 ID 없이 파일 생성됩니다.");
}
}
// 인스턴스 ID 포함 파일명 생성
string fileName = !string.IsNullOrEmpty(currentInstanceID)
? $"{sessionID}_{avatarName}_Humanoid_{currentInstanceID}.anim"
: $"{sessionID}_{avatarName}_Humanoid.anim";
// 에셋 파일의 경로를 기반으로 저장 경로 결정
string savePath = "Assets/Resources"; // 기본값
string fileName = $"{sessionID}_{avatarName}_Humanoid.anim";
// 현재 에셋 파일의 경로 가져오기
string assetPath = AssetDatabase.GetAssetPath(this);
@ -683,19 +736,7 @@ namespace Entum
ExportFBXWithEncoding(false);
}
// Biped 형식으로 FBX 내보내기 (ASCII)
[ContextMenu("Export as Biped FBX (ASCII)")]
public void ExportBipedFBXAscii()
{
ExportBipedFBXWithEncoding(true);
}
// Biped 형식으로 FBX 내보내기 (Binary)
[ContextMenu("Export as Biped FBX (Binary)")]
public void ExportBipedFBXBinary()
{
ExportBipedFBXWithEncoding(false);
}
// FBX 내보내기 (인코딩 설정 포함)
private void ExportFBXWithEncoding(bool useAscii)
@ -718,9 +759,29 @@ namespace Entum
string sessionID = GetSessionID();
string avatarName = !string.IsNullOrEmpty(AvatarName) ? AvatarName : "Unknown";
// 인스턴스 ID가 비어있으면 현재 씬의 SavePathManager에서 가져오기
string currentInstanceID = InstanceID;
if (string.IsNullOrEmpty(currentInstanceID))
{
var savePathManager = FindObjectOfType<EasyMotionRecorder.SavePathManager>();
if (savePathManager != null)
{
currentInstanceID = savePathManager.InstanceID;
Debug.Log($"FBX 출력 시 SavePathManager에서 인스턴스 ID 가져옴: '{currentInstanceID}'");
}
else
{
Debug.LogWarning("SavePathManager를 찾을 수 없습니다. 인스턴스 ID 없이 FBX 파일 생성됩니다.");
}
}
// 저장 경로 결정
string savePath = "Assets/Resources";
string fileName = $"{sessionID}_{avatarName}_Motion.fbx";
// 인스턴스 ID 포함 파일명 생성
string fileName = !string.IsNullOrEmpty(currentInstanceID)
? $"{sessionID}_{avatarName}_Motion_{(useAscii ? "ASCII" : "Binary")}_{currentInstanceID}.fbx"
: $"{sessionID}_{avatarName}_Motion_{(useAscii ? "ASCII" : "Binary")}.fbx";
// 현재 에셋 파일의 경로 가져오기
string assetPath = AssetDatabase.GetAssetPath(this);
@ -744,52 +805,7 @@ namespace Entum
Debug.Log($"FBX 애니메이션 파일이 저장되었습니다: {uniqueAssetPath}");
}
// Biped FBX 내보내기 (인코딩 설정 포함)
private void ExportBipedFBXWithEncoding(bool useAscii)
{
// 데이터 검증
if (Poses == null || Poses.Count == 0)
{
Debug.LogError("ExportBipedFBX: Poses 데이터가 없습니다. Poses.Count=" + (Poses?.Count ?? 0));
return;
}
// 본 데이터가 있는지 확인
if (Poses.Count == 0 || Poses[0].HumanoidBones.Count == 0)
{
Debug.LogError("ExportBipedFBX: 본 데이터가 없습니다.");
return;
}
// 세션 ID 사용
string sessionID = GetSessionID();
string avatarName = !string.IsNullOrEmpty(AvatarName) ? AvatarName : "Unknown";
// 저장 경로 결정
string savePath = "Assets/Resources";
string fileName = $"{sessionID}_{avatarName}_Biped.fbx";
// 현재 에셋 파일의 경로 가져오기
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);
// Biped 스켈레톤 생성 후 FBX 내보내기 (인코딩 설정 포함)
ExportBipedSkeletonWithAnimationToFBX(uniqueAssetPath, useAscii);
Debug.Log($"Biped FBX 애니메이션 파일이 저장되었습니다: {uniqueAssetPath}");
}
// 제네릭 애니메이션 클립 생성 (내부 메서드)
private AnimationClip CreateGenericAnimationClip()
@ -823,6 +839,18 @@ namespace Entum
continue;
}
// Bip001 Pelvis 특별 디버깅 (애니메이션 클립 생성)
if (bone.Name.Contains("Bip001 Pelvis"))
{
Debug.LogWarning($"=== 애니메이션 클립 생성 중 Bip001 Pelvis 발견! ===");
Debug.LogWarning($"본 인덱스: {i}");
Debug.LogWarning($"본 이름: '{bone.Name}'");
Debug.LogWarning($"LocalPosition: {bone.LocalPosition}");
Debug.LogWarning($"LocalRotation: {bone.LocalRotation}");
Debug.LogWarning($"LocalRotation (Euler): {bone.LocalRotation.eulerAngles}");
Debug.LogWarning($"================================");
}
// 경로 정리: 끝의 슬래시만 제거
string cleanPath = bone.Name.TrimEnd('/');
@ -1065,206 +1093,7 @@ namespace Entum
return clip;
}
// Biped 애니메이션 클립 생성 (내부 메서드)
private AnimationClip CreateBipedAnimationClip()
{
var clip = new AnimationClip { frameRate = 30 };
// 클립 이름 설정 (중요!)
string sessionID = GetSessionID();
clip.name = $"{sessionID}_Biped";
// 애니메이션 설정
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
};
AnimationUtility.SetAnimationClipSettings(clip, settings);
// 본 데이터가 있는지 확인
if (Poses.Count == 0 || Poses[0].HumanoidBones.Count == 0)
{
Debug.LogError("CreateBipedAnimationClip: 본 데이터가 없습니다.");
return null;
}
var bones = Poses[0].HumanoidBones;
// Bip001 Pelvis의 기본 회전 오프셋 (0, -90, -90)
Quaternion pelvisOffset = Quaternion.Euler(-90, 90, 0);
// Bip001(루트)용 커브
var bipRootPosX = new AnimationCurve();
var bipRootPosY = new AnimationCurve();
var bipRootPosZ = new AnimationCurve();
var bipRootRotX = new AnimationCurve();
var bipRootRotY = new AnimationCurve();
var bipRootRotZ = new AnimationCurve();
var bipRootRotW = new AnimationCurve();
for (int i = 0; i < bones.Count; i++)
{
var bone = bones[i];
if (string.IsNullOrEmpty(bone.Name))
{
Debug.LogError($"본 {i}: 이름이 비어있습니다!");
continue;
}
// Bip001 Pelvis의 데이터를 Bip001 경로에 키로 추가
if (i == 0 && bone.Name.Contains("Pelvis"))
{
// T-포즈를 0프레임에 추가
if (HasTPoseData && TPoseData != null && TPoseData.HumanoidBones.Count > i)
{
var tPoseBone = TPoseData.HumanoidBones[i];
bipRootPosX.AddKey(0f, tPoseBone.LocalPosition.x);
bipRootPosY.AddKey(0f, tPoseBone.LocalPosition.y);
bipRootPosZ.AddKey(0f, tPoseBone.LocalPosition.z);
// T-포즈의 회전 값에 pelvisOffset을 곱해서 적용
var tPoseRotation = tPoseBone.LocalRotation * pelvisOffset;
bipRootRotX.AddKey(0f, tPoseRotation.x);
bipRootRotY.AddKey(0f, tPoseRotation.y);
bipRootRotZ.AddKey(0f, tPoseRotation.z);
bipRootRotW.AddKey(0f, tPoseRotation.w);
}
foreach (var p in Poses)
{
if (p.HumanoidBones.Count > i)
{
var poseBone = p.HumanoidBones[i];
bipRootPosX.AddKey(p.Time, poseBone.LocalPosition.x);
bipRootPosY.AddKey(p.Time, poseBone.LocalPosition.y);
bipRootPosZ.AddKey(p.Time, poseBone.LocalPosition.z);
// 실제 애니메이션 데이터의 회전 값을 사용하되, pelvisOffset을 곱해서 적용
var actualRotation = poseBone.LocalRotation * pelvisOffset;
bipRootRotX.AddKey(p.Time, actualRotation.x);
bipRootRotY.AddKey(p.Time, actualRotation.y);
bipRootRotZ.AddKey(p.Time, actualRotation.z);
bipRootRotW.AddKey(p.Time, actualRotation.w);
}
}
// Bip001 경로에 커브 추가
AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve("Bip001", typeof(Transform), "m_LocalPosition.x"), bipRootPosX);
AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve("Bip001", typeof(Transform), "m_LocalPosition.y"), bipRootPosY);
AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve("Bip001", typeof(Transform), "m_LocalPosition.z"), bipRootPosZ);
AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve("Bip001", typeof(Transform), "m_LocalRotation.x"), bipRootRotX);
AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve("Bip001", typeof(Transform), "m_LocalRotation.y"), bipRootRotY);
AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve("Bip001", typeof(Transform), "m_LocalRotation.z"), bipRootRotZ);
AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve("Bip001", typeof(Transform), "m_LocalRotation.w"), bipRootRotW);
continue; // Pelvis 본에는 키를 넣지 않음
}
// 나머지 본은 기존 방식대로
string cleanPath = bone.Name.TrimEnd('/');
var positionCurveX = new AnimationCurve();
var positionCurveY = new AnimationCurve();
var positionCurveZ = new AnimationCurve();
var rotationCurveX = new AnimationCurve();
var rotationCurveY = new AnimationCurve();
var rotationCurveZ = new AnimationCurve();
var rotationCurveW = new AnimationCurve();
// T-포즈를 0프레임에 추가
if (HasTPoseData && TPoseData != null && TPoseData.HumanoidBones.Count > i)
{
var tPoseBone = TPoseData.HumanoidBones[i];
positionCurveX.AddKey(0f, tPoseBone.LocalPosition.x);
positionCurveY.AddKey(0f, tPoseBone.LocalPosition.y);
positionCurveZ.AddKey(0f, tPoseBone.LocalPosition.z);
rotationCurveX.AddKey(0f, tPoseBone.LocalRotation.x);
rotationCurveY.AddKey(0f, tPoseBone.LocalRotation.y);
rotationCurveZ.AddKey(0f, tPoseBone.LocalRotation.z);
rotationCurveW.AddKey(0f, tPoseBone.LocalRotation.w);
}
foreach (var p in Poses)
{
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);
}
}
// 위치 커브 설정
var positionBinding = new EditorCurveBinding
{
path = cleanPath,
type = typeof(Transform),
propertyName = "m_LocalPosition.x"
};
AnimationUtility.SetEditorCurve(clip, positionBinding, positionCurveX);
AnimationUtility.SetEditorCurve(clip,
new EditorCurveBinding
{
path = cleanPath,
type = typeof(Transform),
propertyName = "m_LocalPosition.y"
}, positionCurveY);
AnimationUtility.SetEditorCurve(clip,
new EditorCurveBinding
{
path = cleanPath,
type = typeof(Transform),
propertyName = "m_LocalPosition.z"
}, positionCurveZ);
// 회전 커브 설정
AnimationUtility.SetEditorCurve(clip,
new EditorCurveBinding
{
path = cleanPath,
type = typeof(Transform),
propertyName = "m_LocalRotation.x"
}, rotationCurveX);
AnimationUtility.SetEditorCurve(clip,
new EditorCurveBinding
{
path = cleanPath,
type = typeof(Transform),
propertyName = "m_LocalRotation.y"
}, rotationCurveY);
AnimationUtility.SetEditorCurve(clip,
new EditorCurveBinding
{
path = cleanPath,
type = typeof(Transform),
propertyName = "m_LocalRotation.z"
}, rotationCurveZ);
AnimationUtility.SetEditorCurve(clip,
new EditorCurveBinding
{
path = cleanPath,
type = typeof(Transform),
propertyName = "m_LocalRotation.w"
}, rotationCurveW);
}
clip.EnsureQuaternionContinuity();
return clip;
}
// 스켈레톤 생성 후 FBX 내보내기 메서드
private void ExportSkeletonWithAnimationToFBX(string fbxPath, bool useAscii = true)
@ -1273,12 +1102,7 @@ namespace Entum
EditorApplication.delayCall += () => ExportSkeletonWithAnimationToFBXStepByStep(fbxPath, useAscii);
}
// Biped 스켈레톤 생성 후 FBX 내보내기 메서드
private void ExportBipedSkeletonWithAnimationToFBX(string fbxPath, bool useAscii = true)
{
// EditorApplication.delayCall을 사용하여 다음 프레임에서 실행
EditorApplication.delayCall += () => ExportBipedSkeletonWithAnimationToFBXStepByStep(fbxPath, useAscii);
}
private void ExportSkeletonWithAnimationToFBXStepByStep(string fbxPath, bool useAscii = true)
{
@ -1368,93 +1192,7 @@ namespace Entum
}
}
private void ExportBipedSkeletonWithAnimationToFBXStepByStep(string fbxPath, bool useAscii = true)
{
try
{
#if UNITY_EDITOR
Debug.Log("Biped FBX 내보내기 시작...");
// 1단계: Biped 애니메이션 클립 생성 (메모리에서만)
Debug.Log("1단계: Biped 애니메이션 클립 생성 중...");
var bipedClip = CreateBipedAnimationClip();
if (bipedClip == null)
{
Debug.LogError("Biped 애니메이션 클립 생성에 실패했습니다.");
return;
}
Debug.Log($"Biped 애니메이션 클립 생성 완료: {bipedClip.name} (길이: {bipedClip.length}초)");
// 2단계: Biped 스켈레톤 생성
Debug.Log("2단계: Biped 스켈레톤 생성 중...");
var skeletonRoot = CreateBipedSkeletonFromBoneData();
if (skeletonRoot == null)
{
Debug.LogError("Biped 스켈레톤 생성에 실패했습니다.");
return;
}
Debug.Log($"Biped 스켈레톤 생성 성공: {skeletonRoot.name} (자식 수: {skeletonRoot.transform.childCount})");
// 3단계: Animator 컴포넌트 추가 및 생성된 클립 연결
Debug.Log("3단계: Animator 컴포넌트 설정 중...");
var animatorComponent = AnimationHelper.SetupAnimatorComponent(skeletonRoot, bipedClip);
if (animatorComponent == null)
{
Debug.LogError("Animator 컴포넌트 설정에 실패했습니다.");
DestroyImmediate(skeletonRoot);
return;
}
Debug.Log($"Animator 컴포넌트 설정 성공");
// 4단계: Animator 컴포넌트 상태 상세 확인
Debug.Log("4단계: Animator 컴포넌트 디버그 정보 출력 중...");
AnimationHelper.DebugAnimatorComponent(animatorComponent);
// 5단계: FBX Exporter 패키지 사용하여 내보내기
Debug.Log("5단계: Biped FBX 내보내기 중...");
bool exportSuccess = ExportSkeletonWithAnimationUsingFBXExporter(skeletonRoot, fbxPath, useAscii);
if (exportSuccess)
{
Debug.Log($"✅ Biped FBX 내보내기 성공: {fbxPath}");
// 6단계: FBX 파일 설정 조정
Debug.Log("6단계: FBX 설정 조정 중...");
AnimationHelper.AdjustFBXImporterSettings(fbxPath);
}
else
{
Debug.LogError("❌ Biped FBX 내보내기에 실패했습니다.");
}
// 7단계: 정리 (메모리에서 클립 언로드)
Debug.Log("7단계: 정리 중...");
DestroyImmediate(skeletonRoot);
// 메모리에서 애니메이션 클립 정리
if (bipedClip != null)
{
Debug.Log($"메모리에서 애니메이션 클립 정리: {bipedClip.name}");
// Unity가 자동으로 메모리에서 언로드하도록 함
}
// 가비지 컬렉션 강제 실행 (선택사항)
System.GC.Collect();
Debug.Log("✅ Biped FBX 내보내기 완료!");
# endif
}
catch (System.Exception e)
{
Debug.LogError($"Biped FBX 내보내기 실패: {e.Message}\n{e.StackTrace}");
}
}
@ -1596,6 +1334,13 @@ namespace Entum
importer.animationType = ModelImporterAnimationType.Generic;
importer.animationCompression = ModelImporterAnimationCompression.Off;
// 본 이름 설정 - 띄어쓰기 유지
importer.importBlendShapes = false;
importer.importVisibility = false;
importer.importCameras = false;
importer.importLights = false;
// 애니메이션 클립 설정
var clipSettings = importer.defaultClipAnimations;
if (clipSettings.Length > 0)
@ -1794,21 +1539,28 @@ namespace Entum
Debug.LogWarning("ExportFormat 속성을 찾을 수 없습니다.");
}
// 애니메이션 포함 설정
var includeAnimationProperty = exportModelOptionsType.GetProperty("IncludeAnimation");
if (includeAnimationProperty != null)
// ExportModelOptions의 모든 속성 확인
var properties = exportModelOptionsType.GetProperties();
Debug.Log("ExportModelOptions의 모든 속성:");
foreach (var property in properties)
{
includeAnimationProperty.SetValue(exportOptions, true);
Debug.Log("애니메이션 포함 설정: true");
Debug.Log($" - {property.Name}: {property.PropertyType.Name}");
}
// UseMayaCompatibleNames 속성이 존재하는지 확인 후 설정
var useMayaCompatibleNamesProperty = exportModelOptionsType.GetProperty("UseMayaCompatibleNames");
if (useMayaCompatibleNamesProperty != null)
{
useMayaCompatibleNamesProperty.SetValue(exportOptions, false);
Debug.Log("UseMayaCompatibleNames 속성을 false로 설정했습니다.");
}
else
{
Debug.LogWarning("IncludeAnimation 속성을 찾을 수 없습니다.");
Debug.LogWarning("UseMayaCompatibleNames 속성을 찾을 수 없습니다.");
}
// 스켈레톤만 내보내기 (Animation 컴포넌트가 포함됨)
var objectsToExport = new UnityEngine.Object[] { skeletonRoot };
Debug.Log($"내보낼 오브젝트: {objectsToExport.Length}개");
Debug.Log($"1. 스켈레톤 (Animation 컴포넌트 포함): {skeletonRoot.name}");
@ -1882,17 +1634,43 @@ namespace Entum
{
var bone = bones[i];
Debug.Log($"본 {i}: '{bone.Name}' - 위치: {bone.LocalPosition}, 회전: {bone.LocalRotation}");
// Bip001 Pelvis가 포함된 본 특별 표시
if (bone.Name.Contains("Bip001 Pelvis"))
{
Debug.LogWarning($"*** Bip001 Pelvis 발견! 인덱스: {i} ***");
Debug.LogWarning($" 전체 경로: '{bone.Name}'");
Debug.LogWarning($" LocalPosition: {bone.LocalPosition}");
Debug.LogWarning($" LocalRotation: {bone.LocalRotation}");
Debug.LogWarning($" LocalRotation (Euler): {bone.LocalRotation.eulerAngles}");
}
}
Debug.Log("=== 분석 완료 ===");
// 루트 GameObject 생성 (스크립터블 에셋 이름 사용)
// 본 데이터에서 첫 번째 경로 부분을 찾기 (Bip001 등)
string firstBoneName = "";
foreach (var bone in bones)
{
if (string.IsNullOrEmpty(bone.Name))
continue;
// 첫 번째 경로 부분을 찾기
var pathParts = bone.Name.Split('/');
if (pathParts.Length > 0)
{
firstBoneName = pathParts[0];
break;
}
}
// 루트 GameObject 생성 (스켈레톤 이름 사용, Bip001 위에 루트 생성)
string rootName = this.name;
if (string.IsNullOrEmpty(rootName))
{
rootName = "Skeleton";
}
var root = new GameObject(rootName);
Debug.Log($"루트 GameObject 생성됨: {root.name}");
Debug.Log($"루트 GameObject 생성됨: {root.name} (첫 번째 본: {firstBoneName})");
// 본 계층 구조 생성
var boneGameObjects = new Dictionary<string, GameObject>();
@ -1946,6 +1724,8 @@ namespace Entum
Debug.Log($"중복 제거 후 고유 본 경로 수: {uniqueBonePaths.Count}");
foreach (var kvp in uniqueBonePaths)
{
var bonePath = kvp.Key;
@ -1968,17 +1748,19 @@ namespace Entum
if (!boneGameObjects.ContainsKey(currentPath))
{
// 루트 본이 본 데이터에 포함되어 있고, 현재 처리 중인 본이 루트인 경우
if (hasRootInData && i == 0 && pathParts.Length == 1 && rootBones.Contains(part))
// 첫 번째 경로 부분이 첫 번째 본인 경우 (Bip001 등)
if (i == 0 && part == firstBoneName)
{
// 루트 본은 이미 생성된 루트 GameObject를 사용
boneGameObjects[currentPath] = root;
Debug.Log($"루트 본 '{part}'을 기존 루트 GameObject에 연결 (중복 방지)");
// 첫 번째 본을 루트 하위에 생성
var firstBoneGO = new GameObject(part);
firstBoneGO.transform.SetParent(root.transform);
firstBoneGO.transform.localPosition = bone.LocalPosition;
firstBoneGO.transform.localRotation = bone.LocalRotation;
createdBones++;
Debug.Log($"첫 번째 본 '{part}'을 루트 하위에 생성: 위치={bone.LocalPosition}, 회전={bone.LocalRotation}");
// 루트의 위치와 회전을 설정
root.transform.localPosition = bone.LocalPosition;
root.transform.localRotation = bone.LocalRotation;
Debug.Log($"루트 설정: 위치={bone.LocalPosition}, 회전={bone.LocalRotation}");
boneGameObjects[currentPath] = firstBoneGO;
parent = firstBoneGO;
continue;
}
@ -1989,6 +1771,18 @@ namespace Entum
// 첫 번째 포즈의 위치와 회전 설정
if (i == pathParts.Length - 1) // 마지막 부분 (실제 본)
{
// Bip001 Pelvis 특별 디버깅
if (part == "Bip001 Pelvis")
{
Debug.LogWarning($"=== Bip001 Pelvis 특별 디버깅 ===");
Debug.LogWarning($"본 데이터에서 가져온 값:");
Debug.LogWarning($" LocalPosition: {bone.LocalPosition}");
Debug.LogWarning($" LocalRotation: {bone.LocalRotation}");
Debug.LogWarning($" LocalRotation (Euler): {bone.LocalRotation.eulerAngles}");
Debug.LogWarning($"본 경로: {bonePath}");
Debug.LogWarning($"================================");
}
boneGO.transform.localPosition = bone.LocalPosition;
boneGO.transform.localRotation = bone.LocalRotation;
Debug.Log($"본 설정: {part} - 위치: {bone.LocalPosition}, 회전: {bone.LocalRotation}");
@ -2009,6 +1803,9 @@ namespace Entum
// 스켈레톤 구조 출력
PrintSkeletonHierarchy(root);
// Bip001 Pelvis 강제 수정
FixBip001PelvisTransform(root);
// 스켈레톤이 실제로 씬에 있는지 확인
if (root != null && root.transform.childCount > 0)
{
@ -2022,156 +1819,6 @@ namespace Entum
}
}
// 본 데이터로부터 Biped 스켈레톤 생성
private GameObject CreateBipedSkeletonFromBoneData()
{
if (Poses.Count == 0 || Poses[0].HumanoidBones.Count == 0)
{
Debug.LogError("본 데이터가 없습니다.");
return null;
}
var firstPose = Poses[0];
var bones = firstPose.HumanoidBones;
Debug.Log($"Biped 스켈레톤 생성 시작: {bones.Count}개의 본 데이터");
// 본 데이터 구조 확인
for (int i = 0; i < Math.Min(bones.Count, 10); i++) // 처음 10개만 출력
{
var bone = bones[i];
Debug.Log($"본 {i}: '{bone.Name}' - 위치: {bone.LocalPosition}, 회전: {bone.LocalRotation}");
}
// Bip001 루트 GameObject 생성 (스크립터블 에셋 이름 사용)
string rootName = this.name;
if (string.IsNullOrEmpty(rootName))
{
rootName = "Bip001";
}
var root = new GameObject(rootName);
Debug.Log($"Bip001 루트 GameObject 생성됨: {root.name}");
// 본 계층 구조 생성
var boneGameObjects = new Dictionary<string, GameObject>();
int createdBones = 0;
// 루트가 본 데이터에 포함되어 있는지 확인
bool hasRootInData = false;
string rootBoneName = "";
foreach (var bone in bones)
{
if (string.IsNullOrEmpty(bone.Name))
{
Debug.LogWarning("빈 본 이름 발견, 건너뜀");
continue;
}
// 루트 본인지 확인 (경로가 비어있거나 단일 이름인 경우)
if (string.IsNullOrEmpty(bone.Name) || !bone.Name.Contains("/"))
{
hasRootInData = true;
rootBoneName = bone.Name;
Debug.Log($"본 데이터에 루트가 포함됨: '{rootBoneName}'");
break;
}
}
foreach (var bone in bones)
{
if (string.IsNullOrEmpty(bone.Name))
{
Debug.LogWarning("빈 본 이름 발견, 건너뜀");
continue;
}
Debug.Log($"본 처리 중: {bone.Name}");
// 본 경로를 '/'로 분할하여 계층 구조 생성
var pathParts = bone.Name.Split('/');
var currentPath = "";
GameObject parent = root;
for (int i = 0; i < pathParts.Length; i++)
{
var part = pathParts[i];
if (string.IsNullOrEmpty(part))
continue;
currentPath = string.IsNullOrEmpty(currentPath) ? part : currentPath + "/" + part;
if (!boneGameObjects.ContainsKey(currentPath))
{
// 루트 본이 본 데이터에 포함되어 있고, 현재 처리 중인 본이 루트인 경우
if (hasRootInData && i == 0 && pathParts.Length == 1 && part == rootBoneName)
{
// 루트 본은 이미 생성된 루트 GameObject를 사용
boneGameObjects[currentPath] = root;
Debug.Log($"루트 본 '{part}'을 기존 루트 GameObject에 연결");
continue;
}
var boneGO = new GameObject(part);
boneGO.transform.SetParent(parent.transform);
createdBones++;
// 첫 번째 포즈의 위치와 회전 설정
if (i == pathParts.Length - 1) // 마지막 부분 (실제 본)
{
// Bip001 Pelvis인 경우 특별 처리
if (bone.Name.Contains("Pelvis") && bone.Name.Contains("Bip001"))
{
// Bip001 Pelvis는 로컬 위치를 0,0,0으로 고정
boneGO.transform.localPosition = Vector3.zero;
// Bip001 Pelvis는 기본 회전 오프셋 (0, -90, -90) 유지
boneGO.transform.localRotation = Quaternion.Euler(0, -90, -90);
Debug.Log($"Bip001 Pelvis 설정: {part} - 위치: {Vector3.zero}, 회전: {Quaternion.Euler(0, -90, -90)}");
}
else if (pathParts.Length == 1 && bone.Name.Contains("Pelvis"))
{
// Bip001 루트에 Pelvis 위치 데이터 설정
boneGO.transform.localPosition = bone.LocalPosition;
boneGO.transform.localRotation = Quaternion.Euler(0, -90, -90);
Debug.Log($"Bip001 루트 설정: {part} - 위치: {bone.LocalPosition}, 회전: {Quaternion.Euler(0, -90, -90)}");
}
else
{
// 일반 본은 기존 방식대로 처리
boneGO.transform.localPosition = bone.LocalPosition;
boneGO.transform.localRotation = bone.LocalRotation;
Debug.Log($"본 설정: {part} - 위치: {bone.LocalPosition}, 회전: {bone.LocalRotation}");
}
}
boneGameObjects[currentPath] = boneGO;
parent = boneGO;
}
else
{
parent = boneGameObjects[currentPath];
}
}
}
Debug.Log($"Biped 스켈레톤 생성 완료: {createdBones}개의 GameObject 생성됨");
// 스켈레톤 구조 출력
PrintSkeletonHierarchy(root);
// 스켈레톤이 실제로 씬에 있는지 확인
if (root != null && root.transform.childCount > 0)
{
Debug.Log($"✅ Biped 스켈레톤 생성 성공: 루트={root.name}, 자식 수={root.transform.childCount}");
return root;
}
else
{
Debug.LogError("❌ Biped 스켈레톤 생성 실패: 자식이 없음");
return null;
}
}
// 스켈레톤 계층 구조 출력 (디버깅용)
private void PrintSkeletonHierarchy(GameObject root, string indent = "")
{
@ -2183,20 +1830,6 @@ namespace Entum
}
}
// 스켈레톤에서 모든 본 GameObject 찾기
private void FindAllBoneGameObjects(GameObject root, Dictionary<string, GameObject> boneGameObjects)
{
var allChildren = root.GetComponentsInChildren<Transform>();
foreach (var child in allChildren)
{
if (child != root.transform)
{
string path = GetRelativePath(root.transform, child);
boneGameObjects[path] = child.gameObject;
}
}
}
// 상대 경로 가져오기
private string GetRelativePath(Transform root, Transform target)
{
@ -2213,11 +1846,59 @@ namespace Entum
return string.Join("/", pathList);
}
// 애니메이션 속성 경로 가져오기
private string GetPropertyPath(GameObject boneGO, GameObject root)
// Bip001 Pelvis 강제 수정 메서드
private void FixBip001PelvisTransform(GameObject root)
{
return GetRelativePath(root.transform, boneGO.transform);
Debug.LogWarning("=== Bip001 Pelvis 강제 수정 시작 ===");
// 스켈레톤에서 Bip001 Pelvis 찾기
Transform bip001Pelvis = FindBip001PelvisRecursive(root.transform);
if (bip001Pelvis != null)
{
Debug.LogWarning($"Bip001 Pelvis 발견: {bip001Pelvis.name}");
Debug.LogWarning($"수정 전 - 위치: {bip001Pelvis.localPosition}, 회전: {bip001Pelvis.localRotation.eulerAngles}");
// 강제로 올바른 값 적용
bip001Pelvis.localPosition = Vector3.zero;
bip001Pelvis.localRotation = Quaternion.Euler(-90, 0, 90);
Debug.LogWarning($"수정 후 - 위치: {bip001Pelvis.localPosition}, 회전: {bip001Pelvis.localRotation.eulerAngles}");
Debug.LogWarning("Bip001 Pelvis 강제 수정 완료!");
}
else
{
Debug.LogWarning("Bip001 Pelvis를 찾을 수 없습니다.");
}
Debug.LogWarning("=== Bip001 Pelvis 강제 수정 완료 ===");
}
// 재귀적으로 Bip001 Pelvis 찾기
private Transform FindBip001PelvisRecursive(Transform parent)
{
// 현재 Transform이 Bip001 Pelvis인지 확인
if (parent.name == "Bip001 Pelvis")
{
return parent;
}
// 자식들에서 재귀적으로 찾기
for (int i = 0; i < parent.childCount; i++)
{
var child = parent.GetChild(i);
var result = FindBip001PelvisRecursive(child);
if (result != null)
{
return result;
}
}
return null;
}
#endif
[Serializable]
@ -2287,12 +1968,7 @@ namespace Entum
// 경로 끝의 슬래시 제거
path = path.TrimEnd('/');
// Unity 애니메이션 시스템에서 루트 오브젝트 이름을 제거하는 경우를 대비
// 루트가 "Bip001"인 경우, 경로에서 "Bip001/" 부분을 제거
if (root.name == "Bip001" && path.StartsWith("Bip001/"))
{
path = path.Substring("Bip001/".Length);
}
_pathCache.Add(target, path);
return path;

View File

@ -43,6 +43,9 @@ namespace Entum
[SerializeField, Tooltip("rootBoneSystemがOBJECTROOTの時は使われないパラメータです。")]
private HumanBodyBones _targetRootBone = HumanBodyBones.Hips;
[HideInInspector, SerializeField] private string instanceID = "";
[HideInInspector, SerializeField] private bool useDontDestroyOnLoad = false;
private HumanPoseHandler _poseHandler;
private Action _onPlayFinish;
private float _playingTime;
@ -56,6 +59,17 @@ namespace Entum
return;
}
// 인스턴스 ID가 비어있으면 자동 생성
if (string.IsNullOrEmpty(instanceID))
{
instanceID = System.Guid.NewGuid().ToString().Substring(0, 8);
}
// DontDestroyOnLoad 설정 (선택적)
if (useDontDestroyOnLoad)
{
DontDestroyOnLoad(gameObject);
}
_poseHandler = new HumanPoseHandler(_animator.avatar, _animator.transform);
_onPlayFinish += StopMotion;
@ -82,7 +96,6 @@ namespace Entum
return;
}
_playingTime += Time.deltaTime;
SetHumanPose();
}
@ -99,19 +112,17 @@ namespace Entum
if (RecordedMotionData == null)
{
Debug.LogError("録画済みモーションデータが指定されていません。再生を行いません。");
Debug.LogError("재생할 모션 데이터가 없습니다.");
return;
}
_playingTime = _startFrame * (Time.deltaTime / 1f);
_frameIndex = _startFrame;
_playing = true;
_frameIndex = _startFrame;
_playingTime = 0f;
Debug.Log($"모션 재생 시작 - 인스턴스: {instanceID}, 시작 프레임: {_startFrame}");
}
/// <summary>
/// モーションデータ再生終了。フレーム数が最後になっても自動で呼ばれる
/// </summary>
private void StopMotion()
{
if (!_playing)
@ -119,52 +130,90 @@ namespace Entum
return;
}
_playingTime = 0f;
_frameIndex = _startFrame;
_playing = false;
Debug.Log($"모션 재생 중지 - 인스턴스: {instanceID}, 재생된 프레임: {_frameIndex}");
}
private void SetHumanPose()
{
var pose = new HumanPose();
pose.muscles = RecordedMotionData.Poses[_frameIndex].Muscles;
_poseHandler.SetHumanPose(ref pose);
pose.bodyPosition = RecordedMotionData.Poses[_frameIndex].BodyPosition;
pose.bodyRotation = RecordedMotionData.Poses[_frameIndex].BodyRotation;
if (RecordedMotionData == null || RecordedMotionData.Poses == null || RecordedMotionData.Poses.Count == 0)
{
StopMotion();
return;
}
if (_frameIndex >= RecordedMotionData.Poses.Count)
{
StopMotion();
_onPlayFinish?.Invoke();
return;
}
var pose = RecordedMotionData.Poses[_frameIndex];
var humanPose = new HumanPose();
// 본 데이터 설정
if (pose.HumanoidBones != null)
{
foreach (var bone in pose.HumanoidBones)
{
// HumanBodyBones enum으로 변환
HumanBodyBones bodyBone;
if (System.Enum.TryParse(bone.Name, out bodyBone))
{
var boneTransform = _animator.GetBoneTransform(bodyBone);
if (boneTransform != null)
{
boneTransform.localPosition = bone.LocalPosition;
boneTransform.localRotation = bone.LocalRotation;
}
}
}
}
// 근육 데이터 설정
if (pose.Muscles != null && pose.Muscles.Length > 0)
{
humanPose.muscles = pose.Muscles;
}
// 루트 본 설정
switch (_rootBoneSystem)
{
case MotionDataSettings.Rootbonesystem.Objectroot:
//_animator.transform.localPosition = RecordedMotionData.Poses[_frameIndex].BodyRootPosition;
//_animator.transform.localRotation = RecordedMotionData.Poses[_frameIndex].BodyRootRotation;
_animator.transform.localPosition = pose.BodyRootPosition;
_animator.transform.localRotation = pose.BodyRootRotation;
break;
case MotionDataSettings.Rootbonesystem.Hipbone:
pose.bodyPosition = RecordedMotionData.Poses[_frameIndex].BodyPosition;
pose.bodyRotation = RecordedMotionData.Poses[_frameIndex].BodyRotation;
_animator.GetBoneTransform(_targetRootBone).position = RecordedMotionData.Poses[_frameIndex].BodyRootPosition;
_animator.GetBoneTransform(_targetRootBone).rotation = RecordedMotionData.Poses[_frameIndex].BodyRootRotation;
var hipBone = _animator.GetBoneTransform(_targetRootBone);
if (hipBone != null)
{
hipBone.position = pose.BodyRootPosition;
hipBone.rotation = pose.BodyRootRotation;
}
break;
default:
throw new ArgumentOutOfRangeException();
}
//処理落ちしたモーションデータの再生速度調整
if (_playingTime > RecordedMotionData.Poses[_frameIndex].Time)
{
_frameIndex++;
}
// HumanPose 적용
_poseHandler.SetHumanPose(ref humanPose);
if (_frameIndex == RecordedMotionData.Poses.Count - 1)
{
if (_onPlayFinish != null)
{
_onPlayFinish();
}
}
_frameIndex++;
}
public void SetInstanceID(string id)
{
instanceID = id;
}
public void SetUseDontDestroyOnLoad(bool use)
{
useDontDestroyOnLoad = use;
}
public string GetInstanceID()
{
return instanceID;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -29,7 +29,15 @@ namespace Entum
[Header("파일명 설정")]
[SerializeField] private string objectNamePrefix = "Object";
[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;
@ -45,6 +53,24 @@ namespace Entum
public Action OnRecordStart;
public Action OnRecordEnd;
private void Awake()
{
// SavePathManager 자동 찾기
if (_savePathManager == null)
{
_savePathManager = GetComponent<SavePathManager>();
}
// 인스턴스 ID가 비어있으면 자동 생성
if (string.IsNullOrEmpty(instanceID))
{
instanceID = System.Guid.NewGuid().ToString().Substring(0, 8);
}
// SessionID 생성 (인스턴스 ID 제외)
SessionID = DateTime.Now.ToString("yyMMdd_HHmmss");
}
private void Update()
{
@ -111,53 +137,51 @@ namespace Entum
{
if (isRecording)
return;
if (targetObjects == null || targetObjects.Length == 0)
{
// 타겟 오브젝트가 없으면 조용히 무시
return;
}
// 세션 ID 생성 (MotionDataRecorder와 동일한 형식)
// 세션 ID 생성 (인스턴스 ID 제외)
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)
{
foreach (var target in targetObjects)
if (target == null) continue;
var clip = new AnimationClip();
clip.frameRate = targetFPS > 0 ? targetFPS : 60f;
objectClips[target] = clip;
// 포지션 커브 초기화 (X, Y, Z)
positionCurves[target] = new AnimationCurve[3];
for (int i = 0; i < 3; i++)
{
if (target == null) continue;
var clip = new AnimationClip();
clip.frameRate = targetFPS > 0 ? targetFPS : 60f;
// 포지션 커브 초기화
var posCurves = new AnimationCurve[3];
for (int i = 0; i < 3; i++)
{
posCurves[i] = new AnimationCurve();
}
// 로테이션 커브 초기화
var rotCurves = new AnimationCurve[4];
for (int i = 0; i < 4; i++)
{
rotCurves[i] = new AnimationCurve();
}
objectClips[target] = clip;
positionCurves[target] = posCurves;
rotationCurves[target] = rotCurves;
positionCurves[target][i] = new AnimationCurve();
}
// 로테이션 커브 초기화 (X, Y, Z, W)
rotationCurves[target] = new AnimationCurve[4];
for (int i = 0; i < 4; i++)
{
rotationCurves[target][i] = new AnimationCurve();
}
}
startTime = Time.time;
recordedTime = 0f;
frameIndex = 0;
isRecording = true;
startTime = Time.time;
frameIndex = 0;
Debug.Log($"오브젝트 모션 레코딩 시작 - 인스턴스: {instanceID}, 세션: {SessionID}");
OnRecordStart?.Invoke();
Debug.Log($"오브젝트 모션 레코딩 시작: {(targetObjects != null ? targetObjects.Length : 0)}개 오브젝트");
}
public void StopRecording()
@ -178,9 +202,8 @@ namespace Entum
}
}
Debug.Log($"오브젝트 모션 레코딩 종료 - 인스턴스: {instanceID}, 총 프레임: {frameIndex}");
OnRecordEnd?.Invoke();
Debug.Log("오브젝트 모션 레코딩 종료");
}
private void CreateAndSaveAnimationClip(Transform target)
@ -204,29 +227,98 @@ namespace Entum
// Quaternion 연속성 보장
clip.EnsureQuaternionContinuity();
// 파일명 생성
string objectName = target.name;
// 파일명 생성 (오브젝트 이름 정리)
string objectName = SanitizeFileName(target.name);
string fileName = $"{SessionID}_{objectName}_Object.anim";
// SavePathManager 사용
string savePath = "Assets/Resources"; // 기본값
if (SavePathManager.Instance != null)
{
savePath = SavePathManager.Instance.GetObjectSavePath();
}
string savePath = _savePathManager.GetObjectSavePath();
MotionDataRecorder.SafeCreateDirectory(savePath);
// 인스턴스별 고유 경로 생성
string filePath = Path.Combine(savePath, fileName);
filePath = _savePathManager.GetInstanceSpecificPath(filePath);
var path = Path.Combine(savePath, fileName);
var uniqueAssetPath = AssetDatabase.GenerateUniqueAssetPath(path);
SavePathManager.SafeCreateDirectory(Path.GetDirectoryName(filePath));
AssetDatabase.CreateAsset(clip, uniqueAssetPath);
AssetDatabase.CreateAsset(clip, filePath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log($"오브젝트 애니메이션 파일 저장: {uniqueAssetPath}");
Debug.Log($"오브젝트 애니메이션 파일 저장: {filePath}");
// 자동 출력 옵션 처리
if (_savePathManager != null)
{
if (_savePathManager.ExportHumanoidOnSave)
{
ExportObjectAnimationAsHumanoid(target, fileName);
}
if (_savePathManager.ExportGenericOnSave)
{
ExportObjectAnimationAsGeneric(target, fileName);
}
}
#endif
}
private string SanitizeFileName(string fileName)
{
if (string.IsNullOrEmpty(fileName)) return "";
// 불필요한 부분 제거
fileName = fileName.Replace("(Clone)", "").Trim();
// 파일명에 사용할 수 없는 문자 제거
char[] invalidChars = Path.GetInvalidFileNameChars();
foreach (char c in invalidChars)
{
fileName = fileName.Replace(c, '_');
}
// 공백을 언더스코어로 변경
fileName = fileName.Replace(' ', '_');
// 연속된 언더스코어 제거
while (fileName.Contains("__"))
{
fileName = fileName.Replace("__", "_");
}
// 앞뒤 언더스코어 제거
fileName = fileName.Trim('_');
return fileName;
}
#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")]
@ -251,10 +343,18 @@ namespace Entum
targetObjects = new Transform[0];
Debug.Log("모든 타겟 오브젝트 제거");
}
// 타겟 오브젝트 배열 접근자
public Transform[] TargetObjects => targetObjects;
public bool IsRecording => isRecording;
public float RecordedTime => recordedTime;
// 내부 메서드들 (SavePathManager에서 호출)
internal void SetInstanceID(string id)
{
instanceID = id;
// SessionID는 인스턴스 ID 없이 유지
SessionID = DateTime.Now.ToString("yyMMdd_HHmmss");
}
internal void SetSavePathManager(SavePathManager manager)
{
_savePathManager = manager;
}
}
}

Binary file not shown.

View File

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

View File

@ -1,5 +1,6 @@
using UnityEngine;
using System.IO;
using Entum;
#if UNITY_EDITOR
using UnityEditor;
#endif
@ -8,29 +9,8 @@ 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;
@ -40,27 +20,66 @@ namespace EasyMotionRecorder
[SerializeField] private bool exportGenericOnSave = false;
[SerializeField] private bool exportFBXAsciiOnSave = false;
[SerializeField] private bool exportFBXBinaryOnSave = false;
[SerializeField] private bool exportBipedFBXAsciiOnSave = false;
[SerializeField] private bool exportBipedFBXBinaryOnSave = false;
[Header("인스턴스 설정")]
[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;
public bool ExportFBXBinaryOnSave => exportFBXBinaryOnSave;
public bool ExportBipedFBXAsciiOnSave => exportBipedFBXAsciiOnSave;
public bool ExportBipedFBXBinaryOnSave => exportBipedFBXBinaryOnSave;
public string InstanceID => instanceID;
private void Awake()
{
if (_instance == null)
// 인스턴스 ID가 비어있으면 자동 생성
if (string.IsNullOrEmpty(instanceID))
{
_instance = this;
DontDestroyOnLoad(gameObject);
InitializePaths();
instanceID = System.Guid.NewGuid().ToString().Substring(0, 8);
}
else if (_instance != this)
// DontDestroyOnLoad 설정 (선택적)
if (useDontDestroyOnLoad)
{
Destroy(gameObject);
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);
}
}
@ -69,7 +88,6 @@ namespace EasyMotionRecorder
if (createSubdirectories)
{
CreateDirectoryIfNotExists(motionSavePath);
CreateDirectoryIfNotExists(facialSavePath);
}
}
@ -84,6 +102,15 @@ namespace EasyMotionRecorder
}
}
public static DirectoryInfo SafeCreateDirectory(string path)
{
if (!Directory.Exists(path))
{
return Directory.CreateDirectory(path);
}
return new DirectoryInfo(path);
}
public string GetMotionSavePath()
{
return motionSavePath;
@ -91,12 +118,12 @@ namespace EasyMotionRecorder
public string GetFacialSavePath()
{
return motionSavePath; // 모션 경로와 동일하게 설정
return motionSavePath; // 통합된 경로 사용
}
public string GetObjectSavePath()
{
return motionSavePath; // 모션 경로와 동일하게 설정
return motionSavePath; // 통합된 경로 사용
}
public void SetMotionSavePath(string path)
@ -104,36 +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
}
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
}
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;
// 자동 출력 옵션 초기화
@ -141,21 +192,57 @@ namespace EasyMotionRecorder
exportGenericOnSave = false;
exportFBXAsciiOnSave = false;
exportFBXBinaryOnSave = false;
exportBipedFBXAsciiOnSave = false;
exportBipedFBXBinaryOnSave = false;
InitializePaths();
#if UNITY_EDITOR
UnityEditor.EditorUtility.SetDirty(this);
#endif
}
public void SynchronizePaths()
// 인스턴스별 고유 경로 생성
public string GetInstanceSpecificPath(string basePath)
{
// 모든 경로를 모션 경로와 동일하게 설정
facialSavePath = motionSavePath;
objectSavePath = motionSavePath;
if (createSubdirectories)
{
InitializePaths();
}
if (string.IsNullOrEmpty(instanceID))
return basePath;
string directory = Path.GetDirectoryName(basePath);
string fileName = Path.GetFileNameWithoutExtension(basePath);
string extension = Path.GetExtension(basePath);
return Path.Combine(directory, $"{fileName}_{instanceID}{extension}");
}
// 자동 출력 옵션 설정 (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
}
}
}

View File

@ -1,89 +0,0 @@
using UnityEngine;
using UnityEditor;
using System.IO;
using System.Collections.Generic;
public class AssetBatchRenamer : EditorWindow
{
int removeFrontCount = 0;
int removeBackCount = 0;
string prefixToAdd = "";
string suffixToAdd = "";
[MenuItem("Tools/Asset Batch Renamer")]
public static void ShowWindow()
{
GetWindow<AssetBatchRenamer>("Asset Batch Renamer");
}
void OnGUI()
{
GUILayout.Label("선택한 에셋 이름 일괄 수정", EditorStyles.boldLabel);
removeFrontCount = EditorGUILayout.IntField("앞에서 제거할 문자 수", removeFrontCount);
removeBackCount = EditorGUILayout.IntField("뒤에서 제거할 문자 수", removeBackCount);
prefixToAdd = EditorGUILayout.TextField("앞에 추가할 문자열", prefixToAdd);
suffixToAdd = EditorGUILayout.TextField("뒤에 추가할 문자열", suffixToAdd);
if (GUILayout.Button("선택된 에셋 이름 변경"))
{
RenameSelectedAssets();
}
}
void RenameSelectedAssets()
{
Object[] selectedObjects = Selection.objects;
Dictionary<string, int> nameConflictMap = new Dictionary<string, int>();
Undo.RecordObjects(selectedObjects, "Batch Rename Assets");
foreach (Object obj in selectedObjects)
{
string assetPath = AssetDatabase.GetAssetPath(obj);
string assetName = Path.GetFileNameWithoutExtension(assetPath);
string assetExtension = Path.GetExtension(assetPath);
string assetDir = Path.GetDirectoryName(assetPath);
string newName = assetName;
// 앞 문자 제거
if (removeFrontCount > 0 && newName.Length > removeFrontCount)
newName = newName.Substring(removeFrontCount);
// 뒤 문자 제거
if (removeBackCount > 0 && newName.Length > removeBackCount)
newName = newName.Substring(0, newName.Length - removeBackCount);
// 앞뒤 추가
newName = prefixToAdd + newName + suffixToAdd;
// 이름 충돌 방지 처리
string finalName = newName;
int counter = 1;
while (AssetExists(assetDir, finalName, assetExtension) && finalName != assetName)
{
finalName = newName + "_" + counter;
counter++;
}
if (finalName != assetName)
{
string result = AssetDatabase.RenameAsset(assetPath, finalName);
if (!string.IsNullOrEmpty(result))
{
Debug.LogWarning($"[{obj.name}] 이름 변경 실패: {result}");
}
}
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
bool AssetExists(string directory, string name, string extension)
{
string fullPath = Path.Combine(directory, name + extension).Replace("\\", "/");
return AssetDatabase.LoadAssetAtPath<Object>(fullPath) != null;
}
}

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 3ab2d3837e4d1874a9bfe3986e13179e

View File

@ -1,755 +0,0 @@
using UnityEngine;
using UnityEditor;
using System.Linq;
using System.Collections.Generic;
using VRM;
using MagicaCloth2;
using MagicaClothType = MagicaCloth2.MagicaCloth;
using UnityEngine.Animations;
public class AvatarComponetCopier : EditorWindow
{
GameObject sourcePrefab;
GameObject destinationPrefab;
bool showHumanoidBoneInfo = false;
Dictionary<HumanBodyBones, Transform> sourceHumanoidBones = new Dictionary<HumanBodyBones, Transform>();
Dictionary<HumanBodyBones, Transform> targetHumanoidBones = new Dictionary<HumanBodyBones, Transform>();
Vector2 scrollPosition = Vector2.zero;
[MenuItem("Tools/Avatar Component Mover (SpringBone+MagicaCloth2)")]
static void ShowWindow()
{
GetWindow<AvatarComponetCopier>("Avatar Component Mover");
}
void OnGUI()
{
// 스크롤뷰 시작
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
GUILayout.Label("Move VRMSpringBone/MagicaCloth2 & Collider Components", EditorStyles.boldLabel);
sourcePrefab = (GameObject)EditorGUILayout.ObjectField("Source Object", sourcePrefab, typeof(GameObject), true);
destinationPrefab = (GameObject)EditorGUILayout.ObjectField("Target Object", destinationPrefab, typeof(GameObject), true);
if (GUILayout.Button("휴머노이드 본 구조 확인"))
{
if (sourcePrefab == null || destinationPrefab == null)
{
EditorUtility.DisplayDialog("Error", "Source and Target Objects must be set.", "OK");
return;
}
CheckHumanoidStructure();
}
if (showHumanoidBoneInfo)
{
EditorGUILayout.Space();
EditorGUILayout.LabelField("휴머노이드 본 정보", EditorStyles.boldLabel);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("소스 아바타 본 정보:");
foreach (var bone in sourceHumanoidBones)
{
if (bone.Value != null)
{
EditorGUILayout.LabelField($"{bone.Key}: {bone.Value.name}");
}
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space();
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("타겟 아바타 본 정보:");
foreach (var bone in targetHumanoidBones)
{
if (bone.Value != null)
{
EditorGUILayout.LabelField($"{bone.Key}: {bone.Value.name}");
}
}
EditorGUILayout.EndVertical();
}
if (GUILayout.Button("휴머노이드 본 하위 콜라이더 이동"))
{
if (sourcePrefab == null || destinationPrefab == null)
{
EditorUtility.DisplayDialog("Error", "Source and Target Objects must be set.", "OK");
return;
}
MoveCollidersUnderHumanoidBones();
}
if (GUILayout.Button("Spring본콜라이더 복사"))
{
if (sourcePrefab == null || destinationPrefab == null)
{
EditorUtility.DisplayDialog("Error", "Source and Target Objects must be set.", "OK");
return;
}
CopySpringBoneAndMagicaColliders();
}
if (GUILayout.Button("Spring본/Cloth 옮기기"))
{
if (sourcePrefab == null || destinationPrefab == null)
{
EditorUtility.DisplayDialog("Error", "Source and Target Objects must be set.", "OK");
return;
}
MoveSpringBonesAndMagicaCloth();
}
if (GUILayout.Button("Constraint 값 복사"))
{
if (sourcePrefab == null || destinationPrefab == null)
{
EditorUtility.DisplayDialog("Error", "Source and Target Objects must be set.", "OK");
return;
}
CopyConstraints();
}
if (GUILayout.Button("BlendShapeCopy"))
{
if (sourcePrefab == null || destinationPrefab == null)
{
EditorUtility.DisplayDialog("Error", "Source and Target Objects must be set.", "OK");
return;
}
CopyBlendShapes();
}
if (GUILayout.Button("ActiveStateCopy"))
{
if (sourcePrefab == null || destinationPrefab == null)
{
EditorUtility.DisplayDialog("Error", "Source and Target Objects must be set.", "OK");
return;
}
CopyActiveStates();
}
// 스크롤뷰 끝
EditorGUILayout.EndScrollView();
}
void CheckHumanoidStructure()
{
sourceHumanoidBones.Clear();
targetHumanoidBones.Clear();
var sourceAnimator = sourcePrefab.GetComponent<Animator>();
var targetAnimator = destinationPrefab.GetComponent<Animator>();
if (sourceAnimator == null || targetAnimator == null)
{
EditorUtility.DisplayDialog("Error", "소스와 타겟 오브젝트 모두 Animator 컴포넌트가 필요합니다.", "OK");
return;
}
if (!sourceAnimator.isHuman || !targetAnimator.isHuman)
{
EditorUtility.DisplayDialog("Error", "소스와 타겟 오브젝트 모두 Humanoid 타입의 아바타가 필요합니다.", "OK");
return;
}
// 모든 휴머노이드 본에 대해 매핑
foreach (HumanBodyBones bone in System.Enum.GetValues(typeof(HumanBodyBones)))
{
if (bone == HumanBodyBones.LastBone) continue;
var sourceBone = sourceAnimator.GetBoneTransform(bone);
var targetBone = targetAnimator.GetBoneTransform(bone);
if (sourceBone != null)
sourceHumanoidBones[bone] = sourceBone;
if (targetBone != null)
targetHumanoidBones[bone] = targetBone;
}
showHumanoidBoneInfo = true;
Debug.Log("휴머노이드 본 구조 확인이 완료되었습니다.");
}
void MoveCollidersUnderHumanoidBones()
{
if (sourceHumanoidBones.Count == 0 || targetHumanoidBones.Count == 0)
{
EditorUtility.DisplayDialog("Error", "먼저 휴머노이드 본 구조를 확인해주세요.", "OK");
return;
}
Debug.Log("콜라이더 이동 시작...");
// 소스 아바타의 각 휴머노이드 본에 대해
foreach (var sourceBone in sourceHumanoidBones)
{
if (sourceBone.Value == null) continue;
Debug.Log($"소스 본 검사 중: {sourceBone.Key} ({sourceBone.Value.name})");
// 타겟 아바타에서 동일한 HumanBodyBones를 가진 본 찾기
if (!targetHumanoidBones.TryGetValue(sourceBone.Key, out Transform targetBone))
{
Debug.LogWarning($"타겟 아바타에서 {sourceBone.Key}에 해당하는 본을 찾을 수 없습니다.");
continue;
}
Debug.Log($"대응되는 타겟 본 찾음: {targetBone.name}");
// 소스 본의 직접적인 자식 오브젝트만 검사
for (int i = 0; i < sourceBone.Value.childCount; i++)
{
var child = sourceBone.Value.GetChild(i);
Debug.Log($"자식 오브젝트 검사 중: {child.name}");
// VRMSpringBoneColliderGroup 검사
var springCollider = child.GetComponent<VRMSpringBoneColliderGroup>();
if (springCollider != null)
{
Debug.Log($"VRMSpringBoneColliderGroup 발견: {child.name}");
MoveColliderObject(child, sourceBone.Value, targetBone);
continue;
}
// MagicaCapsuleCollider 검사
var magicaCollider = child.GetComponent<MagicaCapsuleCollider>();
if (magicaCollider != null)
{
Debug.Log($"MagicaCapsuleCollider 발견: {child.name}");
MoveColliderObject(child, sourceBone.Value, targetBone);
continue;
}
}
}
Debug.Log("휴머노이드 본 하위 콜라이더 이동이 완료되었습니다.");
}
void MoveColliderObject(Transform colliderObject, Transform sourceBone, Transform targetBone)
{
// 타겟 본에서 동일한 이름의 자식이 있는지 확인
var existingCollider = targetBone.Find(colliderObject.name);
if (existingCollider != null)
{
Debug.LogWarning($"타겟 본 {targetBone.name}에 이미 {colliderObject.name}이(가) 존재합니다.");
return;
}
// 콜라이더 이동
var originalParent = colliderObject.parent;
var originalPosition = colliderObject.position;
var originalRotation = colliderObject.rotation;
var originalScale = colliderObject.localScale;
Debug.Log($"콜라이더 이동 시도: {colliderObject.name}");
Debug.Log($"- 원래 부모: {originalParent.name}");
Debug.Log($"- 원래 위치: {originalPosition}");
Debug.Log($"- 원래 회전: {originalRotation.eulerAngles}");
Debug.Log($"- 원래 크기: {originalScale}");
colliderObject.SetParent(targetBone, true);
colliderObject.position = originalPosition;
colliderObject.rotation = originalRotation;
colliderObject.localScale = originalScale;
Debug.Log($"콜라이더 {colliderObject.name}를 {sourceBone.name}에서 {targetBone.name}로 이동했습니다.");
Debug.Log($"- 새로운 부모: {colliderObject.parent.name}");
Debug.Log($"- 새로운 위치: {colliderObject.position}");
Debug.Log($"- 새로운 회전: {colliderObject.rotation.eulerAngles}");
Debug.Log($"- 새로운 크기: {colliderObject.localScale}");
}
string GetRelativePath(Transform target, Transform root)
{
if (target == root) return "";
var path = target.name;
while (target.parent != null && target.parent != root)
{
target = target.parent;
path = target.name + "/" + path;
}
return path;
}
void CopySpringBoneAndMagicaColliders()
{
// VRMSpringBoneColliderGroup
var srcColliders = sourcePrefab.GetComponentsInChildren<VRMSpringBoneColliderGroup>(true)
.OrderBy(c => GetTransformPath(c.transform, sourcePrefab.transform)).ToArray();
foreach (var srcCollider in srcColliders)
{
string path = GetTransformPath(srcCollider.transform, sourcePrefab.transform);
var tgtTransform = destinationPrefab.transform.Find(path);
if (tgtTransform == null) continue;
if (tgtTransform.GetComponent<VRMSpringBoneColliderGroup>() != null) continue;
var tgtCollider = tgtTransform.gameObject.AddComponent<VRMSpringBoneColliderGroup>();
CopyColliderGroupParameters(srcCollider, tgtCollider);
}
// MagicaCloth2 Colliders
CopyMagicaCollider<MagicaSphereCollider>();
CopyMagicaCollider<MagicaCapsuleCollider>();
CopyMagicaCollider<MagicaPlaneCollider>();
Debug.Log("VRMSpringBoneColliderGroup & MagicaCloth2 Collider 복사가 완료되었습니다.");
}
void CopyMagicaCollider<T>() where T : Component
{
var srcColliders = sourcePrefab.GetComponentsInChildren<T>(true)
.OrderBy(c => GetTransformPath(((Component)c).transform, sourcePrefab.transform)).ToArray();
foreach (var srcCollider in srcColliders)
{
var srcTr = ((Component)srcCollider).transform;
string path = GetTransformPath(srcTr, sourcePrefab.transform);
var tgtTransform = destinationPrefab.transform.Find(path);
if (tgtTransform == null) continue;
if (tgtTransform.GetComponent<T>() != null) continue;
var tgtCollider = tgtTransform.gameObject.AddComponent<T>();
CopyMagicaColliderComponents(srcCollider, tgtCollider);
}
}
void CopyMagicaColliderComponents(Component src, Component tgt)
{
var fields = src.GetType().GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
foreach (var field in fields)
{
var value = field.GetValue(src);
if (typeof(UnityEngine.Object).IsAssignableFrom(field.FieldType))
{
Object refObj = value as Object;
if (refObj == null) { field.SetValue(tgt, null); continue; }
if (refObj is Transform tr)
{
string refPath = GetTransformPath(tr, sourcePrefab.transform);
var tgtTr = destinationPrefab.transform.Find(refPath);
if (tgtTr != null)
field.SetValue(tgt, tgtTr);
}
else if (refObj is GameObject go)
{
string refPath = GetTransformPath(go.transform, sourcePrefab.transform);
var tgtGo = destinationPrefab.transform.Find(refPath);
if (tgtGo != null)
field.SetValue(tgt, tgtGo.gameObject);
}
else if (refObj is Component comp)
{
string refPath = GetTransformPath(comp.gameObject.transform, sourcePrefab.transform);
var tgtGo = destinationPrefab.transform.Find(refPath);
if (tgtGo != null)
{
var tgtComps = tgtGo.GetComponents(comp.GetType());
int compIdx = comp.gameObject.GetComponents(comp.GetType()).ToList().IndexOf(comp);
if (compIdx >= 0 && compIdx < tgtComps.Length)
field.SetValue(tgt, tgtComps[compIdx]);
}
}
else
{
// 기타 UnityEngine.Object 타입은 그대로 복사
field.SetValue(tgt, refObj);
}
}
else
{
// 값 타입, enum, string 등은 그대로 복사
field.SetValue(tgt, value);
}
}
}
void MoveSpringBonesAndMagicaCloth()
{
// VRMSpringBone
var srcSpringBones = sourcePrefab.GetComponentsInChildren<VRMSpringBone>(true)
.OrderBy(s => GetTransformPath(s.transform, sourcePrefab.transform)).ToArray();
foreach (var srcSpringBone in srcSpringBones)
{
string path = GetTransformPath(srcSpringBone.transform, sourcePrefab.transform);
var tgtTransform = destinationPrefab.transform.Find(path);
if (tgtTransform == null) continue;
var tgtSpringBone = tgtTransform.gameObject.AddComponent<VRMSpringBone>();
CopyVRMSpringBoneComponents(srcSpringBone, tgtSpringBone);
DestroyImmediate(srcSpringBone);
}
// MagicaCloth
var srcMagicaCloths = sourcePrefab.GetComponentsInChildren<MagicaClothType>(true)
.OrderBy(s => GetTransformPath(s.transform, sourcePrefab.transform)).ToArray();
foreach (var srcCloth in srcMagicaCloths)
{
string path = GetTransformPath(srcCloth.transform, sourcePrefab.transform);
var tgtTransform = destinationPrefab.transform.Find(path);
if (tgtTransform == null) continue;
var tgtCloth = tgtTransform.gameObject.AddComponent<MagicaClothType>();
CopyMagicaClothComponents(srcCloth, tgtCloth);
DestroyImmediate(srcCloth);
}
Debug.Log("VRMSpringBone & MagicaCloth 옮기기가 완료되었습니다.");
}
void CopyColliderGroupParameters(VRMSpringBoneColliderGroup source, VRMSpringBoneColliderGroup target)
{
target.Colliders = source.Colliders.Select(c => new VRMSpringBoneColliderGroup.SphereCollider
{
Offset = c.Offset,
Radius = c.Radius
}).ToArray();
}
void CopyVRMSpringBoneComponents(VRMSpringBone original, VRMSpringBone copy)
{
copy.m_comment = original.m_comment;
copy.m_stiffnessForce = original.m_stiffnessForce;
copy.m_gravityPower = original.m_gravityPower;
copy.m_gravityDir = original.m_gravityDir;
copy.m_dragForce = original.m_dragForce;
copy.m_center = FindCorrespondingTransform(original.m_center, destinationPrefab.transform);
copy.m_hitRadius = original.m_hitRadius;
copy.m_updateType = original.m_updateType;
copy.RootBones = original.RootBones
.Select(bone => FindCorrespondingTransform(bone, destinationPrefab.transform))
.Where(t => t != null)
.ToList();
copy.ColliderGroups = original.ColliderGroups
.Select(colliderGroup => FindCorrespondingColliderGroup(colliderGroup, destinationPrefab.transform))
.Where(cg => cg != null)
.ToArray();
}
Transform FindCorrespondingTransform(Transform original, Transform searchRoot)
{
if (original == null) return null;
var path = GetTransformPath(original, sourcePrefab.transform);
return searchRoot.Find(path);
}
string GetTransformPath(Transform current, Transform root)
{
if (current == root) return "";
var path = current.name;
while (current.parent != null && current.parent != root)
{
current = current.parent;
path = current.name + "/" + path;
}
return path;
}
VRMSpringBoneColliderGroup FindCorrespondingColliderGroup(VRMSpringBoneColliderGroup original, Transform searchRoot)
{
if (original == null) return null;
var path = GetTransformPath(original.transform, sourcePrefab.transform);
var correspondingTransform = searchRoot.Find(path);
return correspondingTransform != null ? correspondingTransform.GetComponent<VRMSpringBoneColliderGroup>() : null;
}
void CopyMagicaClothComponents(MagicaClothType src, MagicaClothType tgt)
{
var fields = typeof(MagicaClothType).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
foreach (var field in fields)
{
var value = field.GetValue(src);
// ClothType 우선 복사 및 분기 처리
if (field.Name == "serializeData" && value is MagicaCloth2.ClothSerializeData sdata)
{
var tdata = new MagicaCloth2.ClothSerializeData();
// ClothType을 먼저 복사
tdata.clothType = sdata.clothType;
// ClothType에 따라 분기
if (sdata.clothType == MagicaCloth2.ClothProcess.ClothType.MeshCloth)
{
// sourceRenderers 복사
tdata.sourceRenderers = sdata.sourceRenderers
.Select(r => {
if (r == null) return null;
string path = GetTransformPath(r.transform, sourcePrefab.transform);
var tgtTr = destinationPrefab.transform.Find(path);
return tgtTr ? tgtTr.GetComponent<Renderer>() : null;
})
.Where(r => r != null)
.ToList();
}
else if (sdata.clothType == MagicaCloth2.ClothProcess.ClothType.BoneCloth || sdata.clothType == MagicaCloth2.ClothProcess.ClothType.BoneSpring)
{
// rootBones 복사
tdata.rootBones = sdata.rootBones
.Select(bone => FindCorrespondingTransform(bone, destinationPrefab.transform))
.Where(t => t != null)
.ToList();
}
// colliderCollisionConstraint 복사
if (sdata.colliderCollisionConstraint != null)
{
var srcCol = sdata.colliderCollisionConstraint;
var tgtCol = new MagicaCloth2.ColliderCollisionConstraint.SerializeData();
tgtCol.mode = srcCol.mode;
tgtCol.friction = srcCol.friction;
tgtCol.limitDistance = srcCol.limitDistance;
// colliderList 변환
tgtCol.colliderList = srcCol.colliderList
.Select(c => {
if (c == null) return null;
string path = GetTransformPath(c.transform, sourcePrefab.transform);
var tgtTr = destinationPrefab.transform.Find(path);
return tgtTr ? tgtTr.GetComponent<MagicaCloth2.ColliderComponent>() : null;
})
.Where(c => c != null)
.ToList();
// collisionBones 변환
tgtCol.collisionBones = srcCol.collisionBones
.Select(bone => FindCorrespondingTransform(bone, destinationPrefab.transform))
.Where(t => t != null)
.ToList();
tdata.colliderCollisionConstraint = tgtCol;
}
// 공통 필드 복사
tdata.paintMaps = new List<Texture2D>(sdata.paintMaps);
tdata.paintMode = sdata.paintMode;
tdata.connectionMode = sdata.connectionMode;
tdata.rotationalInterpolation = sdata.rotationalInterpolation;
tdata.rootRotation = sdata.rootRotation;
tdata.updateMode = sdata.updateMode;
tdata.animationPoseRatio = sdata.animationPoseRatio;
tdata.reductionSetting = sdata.reductionSetting;
tdata.customSkinningSetting = sdata.customSkinningSetting;
tdata.normalAlignmentSetting = sdata.normalAlignmentSetting;
tdata.cullingSettings = sdata.cullingSettings;
tdata.normalAxis = sdata.normalAxis;
tdata.gravity = sdata.gravity;
tdata.gravityDirection = sdata.gravityDirection;
tdata.gravityFalloff = sdata.gravityFalloff;
tdata.stablizationTimeAfterReset = sdata.stablizationTimeAfterReset;
tdata.blendWeight = sdata.blendWeight;
tdata.damping = sdata.damping;
tdata.radius = sdata.radius;
// 기타 값들도 필요시 추가 복사
field.SetValue(tgt, tdata);
continue;
}
if (field.Name == "serializeData2" && value is MagicaCloth2.ClothSerializeData2 sdata2)
{
var tdata2 = new MagicaCloth2.ClothSerializeData2();
// boneAttributeDict 변환
tdata2.boneAttributeDict = sdata2.boneAttributeDict.ToDictionary(
kvp => FindCorrespondingTransform(kvp.Key, destinationPrefab.transform),
kvp => kvp.Value
);
// selectionData 등 나머지 값은 그대로 복사
tdata2.selectionData = sdata2.selectionData;
tdata2.preBuildData = sdata2.preBuildData;
field.SetValue(tgt, tdata2);
continue;
}
// customSkinningSetting.skinningBones 변환
if (field.Name == "customSkinningSetting" && value != null)
{
var srcSkin = value;
var skinField = value.GetType().GetField("skinningBones");
if (skinField != null)
{
var srcList = skinField.GetValue(srcSkin) as List<Transform>;
if (srcList != null)
{
var tgtList = srcList
.Select(bone => FindCorrespondingTransform(bone, destinationPrefab.transform))
.Where(t => t != null)
.ToList();
skinField.SetValue(srcSkin, tgtList);
}
}
field.SetValue(tgt, srcSkin);
continue;
}
// 이하 기존 로직
if (typeof(UnityEngine.Object).IsAssignableFrom(field.FieldType))
{
Object refObj = value as Object;
if (refObj == null) { field.SetValue(tgt, null); continue; }
if (refObj is Transform tr)
{
string refPath = GetTransformPath(tr, sourcePrefab.transform);
var tgtTr = destinationPrefab.transform.Find(refPath);
if (tgtTr != null)
field.SetValue(tgt, tgtTr);
}
else if (refObj is GameObject go)
{
string refPath = GetTransformPath(go.transform, sourcePrefab.transform);
var tgtGo = destinationPrefab.transform.Find(refPath);
if (tgtGo != null)
field.SetValue(tgt, tgtGo.gameObject);
}
else if (refObj is Component comp)
{
string refPath = GetTransformPath(comp.gameObject.transform, sourcePrefab.transform);
var tgtGo = destinationPrefab.transform.Find(refPath);
if (tgtGo != null)
{
var tgtComps = tgtGo.GetComponents(comp.GetType());
int compIdx = comp.gameObject.GetComponents(comp.GetType()).ToList().IndexOf(comp);
if (compIdx >= 0 && compIdx < tgtComps.Length)
field.SetValue(tgt, tgtComps[compIdx]);
}
}
else
{
// 기타 UnityEngine.Object 타입은 그대로 복사
field.SetValue(tgt, refObj);
}
}
else
{
// 값 타입, enum, string 등은 그대로 복사
field.SetValue(tgt, value);
}
}
}
void CopyConstraints()
{
var srcTransforms = sourcePrefab.GetComponentsInChildren<Transform>(true);
foreach (var srcTr in srcTransforms)
{
string path = GetTransformPath(srcTr, sourcePrefab.transform);
var tgtTr = destinationPrefab.transform.Find(path);
if (tgtTr == null) continue;
// PositionConstraint
var srcPos = srcTr.GetComponent<PositionConstraint>();
if (srcPos != null)
CopyConstraintComponent<PositionConstraint>(srcPos, tgtTr);
// RotationConstraint
var srcRot = srcTr.GetComponent<RotationConstraint>();
if (srcRot != null)
CopyConstraintComponent<RotationConstraint>(srcRot, tgtTr);
// ParentConstraint
var srcParent = srcTr.GetComponent<ParentConstraint>();
if (srcParent != null)
CopyConstraintComponent<ParentConstraint>(srcParent, tgtTr);
}
Debug.Log("Constraint 값 복사가 완료되었습니다.");
}
void CopyConstraintComponent<T>(T srcConstraint, Transform tgtTr) where T : Behaviour
{
// 이미 있으면 삭제 후 새로 추가
var tgtConstraint = tgtTr.GetComponent<T>();
if (tgtConstraint != null)
DestroyImmediate(tgtConstraint);
tgtConstraint = tgtTr.gameObject.AddComponent<T>();
if (srcConstraint is PositionConstraint srcPos && tgtConstraint is PositionConstraint tgtPos)
{
tgtPos.weight = srcPos.weight;
tgtPos.constraintActive = srcPos.constraintActive;
tgtPos.locked = srcPos.locked;
for (int i = 0; i < srcPos.sourceCount; i++)
{
var src = srcPos.GetSource(i);
var srcTr = src.sourceTransform;
if (srcTr == null) continue;
string srcPath = GetTransformPath(srcTr, sourcePrefab.transform);
var tgtSourceTr = destinationPrefab.transform.Find(srcPath);
if (tgtSourceTr == null) continue;
var newSource = src;
newSource.sourceTransform = tgtSourceTr;
tgtPos.AddSource(newSource);
}
tgtPos.translationAtRest = srcPos.translationAtRest;
tgtPos.translationOffset = srcPos.translationOffset;
tgtPos.translationAxis = srcPos.translationAxis;
}
else if (srcConstraint is RotationConstraint srcRot && tgtConstraint is RotationConstraint tgtRot)
{
tgtRot.weight = srcRot.weight;
tgtRot.constraintActive = srcRot.constraintActive;
tgtRot.locked = srcRot.locked;
for (int i = 0; i < srcRot.sourceCount; i++)
{
var src = srcRot.GetSource(i);
var srcTr = src.sourceTransform;
if (srcTr == null) continue;
string srcPath = GetTransformPath(srcTr, sourcePrefab.transform);
var tgtSourceTr = destinationPrefab.transform.Find(srcPath);
if (tgtSourceTr == null) continue;
var newSource = src;
newSource.sourceTransform = tgtSourceTr;
tgtRot.AddSource(newSource);
}
tgtRot.rotationAtRest = srcRot.rotationAtRest;
tgtRot.rotationOffset = srcRot.rotationOffset;
tgtRot.rotationAxis = srcRot.rotationAxis;
}
else if (srcConstraint is ParentConstraint srcParent && tgtConstraint is ParentConstraint tgtParent)
{
tgtParent.weight = srcParent.weight;
tgtParent.constraintActive = srcParent.constraintActive;
tgtParent.locked = srcParent.locked;
for (int i = 0; i < srcParent.sourceCount; i++)
{
var src = srcParent.GetSource(i);
var srcTr = src.sourceTransform;
if (srcTr == null) continue;
string srcPath = GetTransformPath(srcTr, sourcePrefab.transform);
var tgtSourceTr = destinationPrefab.transform.Find(srcPath);
if (tgtSourceTr == null) continue;
var newSource = src;
newSource.sourceTransform = tgtSourceTr;
tgtParent.AddSource(newSource);
// Offset도 복사
tgtParent.SetTranslationOffset(i, srcParent.GetTranslationOffset(i));
tgtParent.SetRotationOffset(i, srcParent.GetRotationOffset(i));
}
tgtParent.translationAtRest = srcParent.translationAtRest;
tgtParent.rotationAtRest = srcParent.rotationAtRest;
tgtParent.translationAxis = srcParent.translationAxis;
tgtParent.rotationAxis = srcParent.rotationAxis;
}
}
void CopyBlendShapes()
{
var srcRenderers = sourcePrefab.GetComponentsInChildren<SkinnedMeshRenderer>(true);
foreach (var srcRenderer in srcRenderers)
{
string path = GetTransformPath(srcRenderer.transform, sourcePrefab.transform);
var tgtTransform = destinationPrefab.transform.Find(path);
if (tgtTransform == null) continue;
var tgtRenderer = tgtTransform.GetComponent<SkinnedMeshRenderer>();
if (tgtRenderer == null) continue;
int blendShapeCount = srcRenderer.sharedMesh != null ? srcRenderer.sharedMesh.blendShapeCount : 0;
for (int i = 0; i < blendShapeCount; i++)
{
string shapeName = srcRenderer.sharedMesh.GetBlendShapeName(i);
int tgtIndex = tgtRenderer.sharedMesh != null ? tgtRenderer.sharedMesh.GetBlendShapeIndex(shapeName) : -1;
if (tgtIndex >= 0)
{
float value = srcRenderer.GetBlendShapeWeight(i);
tgtRenderer.SetBlendShapeWeight(tgtIndex, value);
}
}
}
Debug.Log("BlendShape 값 복사가 완료되었습니다.");
}
void CopyActiveStates()
{
var srcTransforms = sourcePrefab.GetComponentsInChildren<Transform>(true);
foreach (var srcTr in srcTransforms)
{
string path = GetTransformPath(srcTr, sourcePrefab.transform);
var tgtTr = destinationPrefab.transform.Find(path);
if (tgtTr == null) continue;
tgtTr.gameObject.SetActive(srcTr.gameObject.activeSelf);
}
Debug.Log("ActiveState 복사가 완료되었습니다.");
}
}

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 39f91032a3569014da62f31d9fe0cb8b

View File

@ -0,0 +1,315 @@
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
public class HumanBoneNameCopier : EditorWindow
{
private GameObject sourceAvatar;
private GameObject targetAvatar;
private Vector2 scrollPosition;
private bool showAdvancedOptions = false;
private bool copyAllBones = true;
private Dictionary<HumanBodyBones, bool> boneSelection = new Dictionary<HumanBodyBones, bool>();
[MenuItem("Tools/Human Bone Name Copier")]
public static void ShowWindow()
{
GetWindow<HumanBoneNameCopier>("Human Bone Name Copier");
}
private void OnEnable()
{
InitializeBoneSelection();
}
private void InitializeBoneSelection()
{
boneSelection.Clear();
foreach (HumanBodyBones bone in System.Enum.GetValues(typeof(HumanBodyBones)))
{
if (bone != HumanBodyBones.LastBone)
{
boneSelection[bone] = true;
}
}
}
private void OnGUI()
{
GUILayout.Label("Human Bone Name Copier", EditorStyles.boldLabel);
EditorGUILayout.Space();
// 소스 아바타 선택
EditorGUILayout.BeginHorizontal();
GUILayout.Label("Source Avatar:", GUILayout.Width(100));
sourceAvatar = (GameObject)EditorGUILayout.ObjectField(sourceAvatar, typeof(GameObject), true);
EditorGUILayout.EndHorizontal();
// 타겟 아바타 선택
EditorGUILayout.BeginHorizontal();
GUILayout.Label("Target Avatar:", GUILayout.Width(100));
targetAvatar = (GameObject)EditorGUILayout.ObjectField(targetAvatar, typeof(GameObject), true);
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
// 고급 옵션 토글
showAdvancedOptions = EditorGUILayout.Foldout(showAdvancedOptions, "Advanced Options");
if (showAdvancedOptions)
{
EditorGUI.indentLevel++;
copyAllBones = EditorGUILayout.Toggle("Copy All Bones", copyAllBones);
if (!copyAllBones)
{
EditorGUILayout.Space();
GUILayout.Label("Select Bones to Copy:", EditorStyles.boldLabel);
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, GUILayout.Height(200));
foreach (var bone in boneSelection)
{
boneSelection[bone.Key] = EditorGUILayout.Toggle(bone.Key.ToString(), bone.Value);
}
EditorGUILayout.EndScrollView();
}
EditorGUI.indentLevel--;
}
EditorGUILayout.Space();
// 정보 표시
if (sourceAvatar != null)
{
var sourceAnimator = sourceAvatar.GetComponent<Animator>();
if (sourceAnimator != null && sourceAnimator.avatar != null)
{
EditorGUILayout.HelpBox($"Source Avatar: {sourceAvatar.name}\n" +
$"Human Bone Count: {GetHumanBoneCount(sourceAnimator)}",
MessageType.Info);
}
}
if (targetAvatar != null)
{
var targetAnimator = targetAvatar.GetComponent<Animator>();
if (targetAnimator != null && targetAnimator.avatar != null)
{
EditorGUILayout.HelpBox($"Target Avatar: {targetAvatar.name}\n" +
$"Human Bone Count: {GetHumanBoneCount(targetAnimator)}",
MessageType.Info);
}
}
EditorGUILayout.Space();
// 버튼들
EditorGUILayout.BeginHorizontal();
GUI.enabled = sourceAvatar != null && targetAvatar != null;
if (GUILayout.Button("Copy Bone Names"))
{
CopyBoneNames();
}
if (GUILayout.Button("Preview Changes"))
{
PreviewChanges();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
// 추가 기능 버튼
EditorGUILayout.BeginHorizontal();
GUI.enabled = targetAvatar != null;
if (GUILayout.Button("Add 'zindnick : ' to Non-Human Bones"))
{
AddPrefixToNonHumanBones();
}
GUI.enabled = true;
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
if (GUILayout.Button("Clear Selection"))
{
sourceAvatar = null;
targetAvatar = null;
}
}
private int GetHumanBoneCount(Animator animator)
{
if (animator == null || animator.avatar == null) return 0;
int count = 0;
for (int i = 0; i < (int)HumanBodyBones.LastBone; i++)
{
if (animator.GetBoneTransform((HumanBodyBones)i) != null)
{
count++;
}
}
return count;
}
private void CopyBoneNames()
{
if (sourceAvatar == null || targetAvatar == null)
{
EditorUtility.DisplayDialog("Error", "Please select both source and target avatars.", "OK");
return;
}
var sourceAnimator = sourceAvatar.GetComponent<Animator>();
var targetAnimator = targetAvatar.GetComponent<Animator>();
if (sourceAnimator == null || sourceAnimator.avatar == null)
{
EditorUtility.DisplayDialog("Error", "Source avatar must have an Animator component with a valid Avatar.", "OK");
return;
}
if (targetAnimator == null || targetAnimator.avatar == null)
{
EditorUtility.DisplayDialog("Error", "Target avatar must have an Animator component with a valid Avatar.", "OK");
return;
}
Undo.RecordObject(targetAvatar, "Copy Human Bone Names");
int copiedCount = 0;
for (int i = 0; i < (int)HumanBodyBones.LastBone; i++)
{
HumanBodyBones bone = (HumanBodyBones)i;
if (!copyAllBones && boneSelection.ContainsKey(bone) && !boneSelection[bone])
{
continue;
}
Transform sourceBone = sourceAnimator.GetBoneTransform(bone);
Transform targetBone = targetAnimator.GetBoneTransform(bone);
if (sourceBone != null && targetBone != null)
{
targetBone.name = sourceBone.name;
copiedCount++;
}
}
EditorUtility.SetDirty(targetAvatar);
AssetDatabase.SaveAssets();
EditorUtility.DisplayDialog("Success",
$"Successfully copied {copiedCount} bone names from {this.sourceAvatar.name} to {this.targetAvatar.name}.",
"OK");
}
private void PreviewChanges()
{
if (sourceAvatar == null || targetAvatar == null)
{
EditorUtility.DisplayDialog("Error", "Please select both source and target avatars.", "OK");
return;
}
var sourceAnimator = sourceAvatar.GetComponent<Animator>();
var targetAnimator = targetAvatar.GetComponent<Animator>();
if (sourceAnimator == null || sourceAnimator.avatar == null)
{
EditorUtility.DisplayDialog("Error", "Source avatar must have an Animator component with a valid Avatar.", "OK");
return;
}
if (targetAnimator == null || targetAnimator.avatar == null)
{
EditorUtility.DisplayDialog("Error", "Target avatar must have an Animator component with a valid Avatar.", "OK");
return;
}
string previewText = "Preview of bone name changes:\n\n";
for (int i = 0; i < (int)HumanBodyBones.LastBone; i++)
{
HumanBodyBones bone = (HumanBodyBones)i;
if (!copyAllBones && boneSelection.ContainsKey(bone) && !boneSelection[bone])
{
continue;
}
Transform sourceBone = sourceAnimator.GetBoneTransform(bone);
Transform targetBone = targetAnimator.GetBoneTransform(bone);
if (sourceBone != null && targetBone != null)
{
previewText += $"{bone}: {targetBone.name} → {sourceBone.name}\n";
}
}
EditorUtility.DisplayDialog("Preview", previewText, "OK");
}
private void AddPrefixToNonHumanBones()
{
if (targetAvatar == null)
{
EditorUtility.DisplayDialog("Error", "Please select a target avatar.", "OK");
return;
}
var targetAnimator = targetAvatar.GetComponent<Animator>();
if (targetAnimator == null || targetAnimator.avatar == null)
{
EditorUtility.DisplayDialog("Error", "Target avatar must have an Animator component with a valid Avatar.", "OK");
return;
}
// 휴먼본 목록 수집
HashSet<Transform> humanBones = new HashSet<Transform>();
for (int i = 0; i < (int)HumanBodyBones.LastBone; i++)
{
Transform humanBone = targetAnimator.GetBoneTransform((HumanBodyBones)i);
if (humanBone != null)
{
humanBones.Add(humanBone);
}
}
Undo.RecordObject(targetAvatar, "Add Prefix to Non-Human Bones");
int modifiedCount = 0;
Transform[] allTransforms = targetAvatar.GetComponentsInChildren<Transform>();
foreach (Transform transform in allTransforms)
{
// 휴먼본이 아니고, 이미 접두사가 없는 경우에만 추가
if (!humanBones.Contains(transform) && !transform.name.StartsWith("zindnick : "))
{
transform.name = "zindnick : " + transform.name;
modifiedCount++;
}
}
EditorUtility.SetDirty(targetAvatar);
AssetDatabase.SaveAssets();
EditorUtility.DisplayDialog("Success",
$"Successfully added 'zindnick : ' prefix to {modifiedCount} non-human bones in {targetAvatar.name}.",
"OK");
}
}

View File

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

View File

@ -1,287 +0,0 @@
using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using System.IO;
public class MaterialAndTextureMover : EditorWindow
{
public List<GameObject> targetObjects = new List<GameObject>();
public string targetFolder = "Assets/";
private List<Material> foundMaterials = new List<Material>();
private List<bool> materialSelectionStatus = new List<bool>();
private List<Texture> foundTextures = new List<Texture>();
private List<bool> textureSelectionStatus = new List<bool>();
private bool materialsSearched = false;
private Vector2 scrollPos;
[MenuItem("Tools/Material and Texture Mover")]
public static void ShowWindow()
{
GetWindow<MaterialAndTextureMover>("Material & Texture Mover");
}
private void OnGUI()
{
GUILayout.Label("Step 1: Select Target Objects and Search Materials and Textures", EditorStyles.boldLabel);
int removeIndex = -1;
for (int i = 0; i < targetObjects.Count; i++)
{
GUILayout.BeginHorizontal();
targetObjects[i] = (GameObject)EditorGUILayout.ObjectField(targetObjects[i], typeof(GameObject), true);
if (GUILayout.Button("Remove", GUILayout.Width(60)))
{
removeIndex = i;
}
GUILayout.EndHorizontal();
}
if (removeIndex >= 0)
{
targetObjects.RemoveAt(removeIndex);
}
if (GUILayout.Button("Add Target Object"))
{
targetObjects.Add(null);
}
GUILayout.Space(10);
if (GUILayout.Button("Search Materials and Textures"))
{
SearchMaterialsAndTextures();
materialsSearched = true;
}
GUILayout.Space(10);
if (materialsSearched)
{
if (GUILayout.Button("Resolve Duplicated Names"))
{
ResolveDuplicatedNames();
}
GUILayout.Space(10);
}
scrollPos = EditorGUILayout.BeginScrollView(scrollPos, GUILayout.Height(300));
if (materialsSearched && foundMaterials.Count > 0)
{
GUILayout.Label("Remapped Materials", EditorStyles.boldLabel);
for (int i = 0; i < foundMaterials.Count; i++)
{
GUILayout.BeginHorizontal();
materialSelectionStatus[i] = EditorGUILayout.ToggleLeft(foundMaterials[i] ? foundMaterials[i].name : "None", materialSelectionStatus[i], GUILayout.Width(200));
if (foundMaterials[i] != null && GUILayout.Button("Ping", GUILayout.Width(50)))
{
EditorGUIUtility.PingObject(foundMaterials[i]);
}
GUILayout.EndHorizontal();
}
}
GUILayout.Space(10);
if (materialsSearched && foundTextures.Count > 0)
{
GUILayout.Label("Remapped Textures", EditorStyles.boldLabel);
for (int i = 0; i < foundTextures.Count; i++)
{
GUILayout.BeginHorizontal();
textureSelectionStatus[i] = EditorGUILayout.ToggleLeft(foundTextures[i] ? foundTextures[i].name : "None", textureSelectionStatus[i], GUILayout.Width(200));
if (foundTextures[i] != null && GUILayout.Button("Ping", GUILayout.Width(50)))
{
EditorGUIUtility.PingObject(foundTextures[i]);
}
GUILayout.EndHorizontal();
}
}
EditorGUILayout.EndScrollView();
GUILayout.Space(20);
GUILayout.Label("Step 2: Select Target Folder and Move Materials and Textures", EditorStyles.boldLabel);
GUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Folder Path:", targetFolder);
if (GUILayout.Button("Select Folder", GUILayout.Width(100)))
{
string selectedPath = EditorUtility.OpenFolderPanel("Select Target Folder", "Assets/", "");
if (!string.IsNullOrEmpty(selectedPath))
{
if (selectedPath.StartsWith(Application.dataPath))
{
targetFolder = "Assets" + selectedPath.Substring(Application.dataPath.Length);
}
else
{
Debug.LogError("Please select a folder within the Assets directory.");
}
}
}
GUILayout.EndHorizontal();
GUILayout.Space(10);
if (GUILayout.Button("Move Materials and Textures"))
{
MoveMaterialsAndTextures();
}
}
private void SearchMaterialsAndTextures()
{
foundMaterials.Clear();
materialSelectionStatus.Clear();
foundTextures.Clear();
textureSelectionStatus.Clear();
foreach (var obj in targetObjects)
{
if (obj == null) continue;
Renderer[] renderers = obj.GetComponentsInChildren<Renderer>();
foreach (var renderer in renderers)
{
foreach (var mat in renderer.sharedMaterials)
{
if (mat != null && !foundMaterials.Contains(mat))
{
foundMaterials.Add(mat);
materialSelectionStatus.Add(true);
// 모든 텍스처 검색
Shader shader = mat.shader;
for (int j = 0; j < ShaderUtil.GetPropertyCount(shader); j++)
{
if (ShaderUtil.GetPropertyType(shader, j) == ShaderUtil.ShaderPropertyType.TexEnv)
{
string propertyName = ShaderUtil.GetPropertyName(shader, j);
Texture texture = mat.GetTexture(propertyName);
if (texture != null && !foundTextures.Contains(texture))
{
foundTextures.Add(texture);
textureSelectionStatus.Add(true);
}
}
}
// 사용자 정의 속성 직접 검색
foreach (string propertyName in mat.GetTexturePropertyNames())
{
Texture texture = mat.GetTexture(propertyName);
if (texture != null && !foundTextures.Contains(texture))
{
foundTextures.Add(texture);
textureSelectionStatus.Add(true);
}
}
}
}
}
}
Debug.Log("Materials and Textures found and listed.");
}
private void ResolveDuplicatedNames()
{
HashSet<string> usedNames = new HashSet<string>();
for (int i = 0; i < foundMaterials.Count; i++)
{
Material mat = foundMaterials[i];
string originalName = mat.name;
int counter = 1;
while (usedNames.Contains(mat.name))
{
mat.name = $"{originalName}_{counter}";
counter++;
}
usedNames.Add(mat.name);
}
for (int i = 0; i < foundTextures.Count; i++)
{
Texture tex = foundTextures[i];
string originalName = tex.name;
int counter = 1;
while (usedNames.Contains(tex.name))
{
tex.name = $"{originalName}_{counter}";
counter++;
}
usedNames.Add(tex.name);
}
Debug.Log("Duplicated names resolved.");
}
private void MoveMaterialsAndTextures()
{
if (!AssetDatabase.IsValidFolder(targetFolder))
{
Debug.LogError("Target folder does not exist or is invalid.");
return;
}
for (int i = 0; i < foundMaterials.Count; i++)
{
if (!materialSelectionStatus[i]) continue;
Material mat = foundMaterials[i];
string path = AssetDatabase.GetAssetPath(mat);
string targetPath = Path.Combine(targetFolder, Path.GetFileName(path));
if (AssetDatabase.LoadAssetAtPath<Material>(targetPath) != null)
{
Debug.LogWarning($"Material '{mat.name}' already exists in the target folder. Skipping.");
continue;
}
if (!string.IsNullOrEmpty(path))
{
string uniquePath = AssetDatabase.GenerateUniqueAssetPath(targetPath);
AssetDatabase.MoveAsset(path, uniquePath);
}
}
for (int i = 0; i < foundTextures.Count; i++)
{
if (!textureSelectionStatus[i]) continue;
Texture tex = foundTextures[i];
string path = AssetDatabase.GetAssetPath(tex);
string targetPath = Path.Combine(targetFolder, Path.GetFileName(path));
if (AssetDatabase.LoadAssetAtPath<Texture>(targetPath) != null)
{
Debug.LogWarning($"Texture '{tex.name}' already exists in the target folder. Skipping.");
continue;
}
if (!string.IsNullOrEmpty(path))
{
string uniquePath = AssetDatabase.GenerateUniqueAssetPath(targetPath);
AssetDatabase.MoveAsset(path, uniquePath);
}
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log("Selected materials and textures moved successfully.");
}
}

View File

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

View File

@ -1,214 +0,0 @@
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
using System.IO;
public class NiloMaterialMatcapSetter : EditorWindow
{
private List<Material> materials = new List<Material>();
private Vector2 scrollPosObjects;
private bool enableBackup = true;
private string pEnableName_Front = "_BaseMapStackingLayer";
private string pEnableName_Back = "Enable";
private const string targetShaderName = "lilToon";
[MenuItem("Tools/Nilotoon Matcap Auto Mapper", false, 153)]
public static void ShowWindow()
{
GetWindow<NiloMaterialMatcapSetter>("닐로툰 매트캡 자동 인식기");
}
void OnGUI()
{
EditorGUILayout.Space();
EditorGUILayout.LabelField("닐로툰 매트캡 자동 인식기", EditorStyles.boldLabel);
EditorGUILayout.HelpBox("릴툰에서 변환된 닐로툰 머티리얼 중 누락된 Matcap 정보를 복원합니다.\n선택에 따라 백업본을 생성합니다.", MessageType.Info);
EditorGUILayout.Space();
enableBackup = EditorGUILayout.ToggleLeft("💾 머티리얼 백업 생성 (권장)", enableBackup);
EditorGUILayout.Space();
EditorGUILayout.LabelField("💠 머티리얼 드래그 앤 드롭", EditorStyles.boldLabel);
Rect dropArea = GUILayoutUtility.GetRect(0, 50, GUILayout.ExpandWidth(true));
GUI.Box(dropArea, "여기에 머티리얼을 드래그 하세요", EditorStyles.helpBox);
HandleDragAndDrop(dropArea);
EditorGUILayout.Space();
EditorGUILayout.LabelField("📝 추가된 머티리얼 목록", EditorStyles.boldLabel);
scrollPosObjects = EditorGUILayout.BeginScrollView(scrollPosObjects, GUILayout.Height(150));
foreach (var mat in materials)
{
EditorGUILayout.ObjectField(mat, typeof(Material), true);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.Space();
if (GUILayout.Button("자동 복사 실행", GUILayout.Height(30)))
{
ProcessMaterials();
materials.Clear();
}
}
void HandleDragAndDrop(Rect dropArea)
{
Event evt = Event.current;
switch (evt.type)
{
case EventType.DragUpdated:
case EventType.DragPerform:
if (!dropArea.Contains(evt.mousePosition)) return;
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
if (evt.type == EventType.DragPerform)
{
DragAndDrop.AcceptDrag();
foreach (Object draggedObject in DragAndDrop.objectReferences)
{
if (draggedObject is Material mat && !materials.Contains(mat))
{
materials.Add(mat);
}
}
evt.Use();
}
break;
}
}
void ProcessMaterials()
{
Shader lilToonShader = Shader.Find(targetShaderName);
if (lilToonShader == null)
{
Debug.LogError("lilToon Shader를 찾을 수 없습니다. 먼저 프로젝트에 추가해주세요.");
return;
}
foreach (Material originalMaterial in materials)
{
if (originalMaterial == null) continue;
if (enableBackup)
{
string originalPath = AssetDatabase.GetAssetPath(originalMaterial);
string originalDir = Path.GetDirectoryName(originalPath);
string backupDir = Path.Combine(originalDir, "M_Backup");
if (!Directory.Exists(backupDir))
{
Directory.CreateDirectory(backupDir);
AssetDatabase.Refresh();
}
string backupPath = Path.Combine(backupDir, originalMaterial.name + "_Backup.mat").Replace("\\", "/");
AssetDatabase.CopyAsset(originalPath, backupPath);
AssetDatabase.ImportAsset(backupPath);
Debug.Log($"백업 생성됨: {backupPath}");
}
Material clonedMaterial = Object.Instantiate(originalMaterial);
clonedMaterial.shader = lilToonShader;
List<stMatcapInfo> matcapInfoList = new List<stMatcapInfo>
{
GetMatcapInfoByName(clonedMaterial, "_UseMatCap", true),
GetMatcapInfoByName(clonedMaterial, "_UseMatCap2nd", false)
};
foreach (stMatcapInfo matcapInfo in matcapInfoList)
{
if (!matcapInfo._UseMatCap) continue;
int targetLayerNumber = 0;
for (int i = 1; i <= 10; i++)
{
if (originalMaterial.GetFloat(pEnableName_Front + i + pEnableName_Back) < 0.5f)
{
targetLayerNumber = i;
originalMaterial.SetFloat(pEnableName_Front + i + pEnableName_Back, 1f);
break;
}
}
if (targetLayerNumber == 0)
{
Debug.LogWarning($"{originalMaterial.name}: 사용할 수 있는 빈 레이어가 없습니다.");
continue;
}
string prefix = pEnableName_Front + targetLayerNumber;
originalMaterial.SetVector(prefix + "TexUVCenterPivotScalePos", new Vector4(1, 1, 0, 0));
originalMaterial.SetVector(prefix + "TexUVScaleOffset", new Vector4(1, 1, 0, 0));
originalMaterial.SetVector(prefix + "TexUVAnimSpeed", Vector4.zero);
originalMaterial.SetVector(prefix + "MaskTexChannel", new Vector4(0, 1, 0, 0));
originalMaterial.SetFloat(prefix + "TexUVRotatedAngle", 0);
originalMaterial.SetFloat(prefix + "TexUVRotateSpeed", 0);
originalMaterial.SetFloat(prefix + "MaskUVIndex", 0);
originalMaterial.SetFloat(prefix + "MaskInvertColor", 0);
originalMaterial.SetFloat(prefix + "TexIgnoreAlpha", 0);
originalMaterial.SetFloat(prefix + "ColorBlendMode",
matcapInfo._MatCapBlendMode == 0 ? 0 :
matcapInfo._MatCapBlendMode == 1 ? 2 :
matcapInfo._MatCapBlendMode == 2 ? 3 :
matcapInfo._MatCapBlendMode == 3 ? 4 : 5);
originalMaterial.SetTexture(prefix + "Tex", matcapInfo._MatCapTex);
originalMaterial.SetColor(prefix + "TintColor", new Color(matcapInfo._MatCapColor.r, matcapInfo._MatCapColor.g, matcapInfo._MatCapColor.b, 1f));
originalMaterial.SetFloat(prefix + "MasterStrength", matcapInfo._MatCapColor.a);
originalMaterial.SetFloat(prefix + "TexUVIndex", 4);
originalMaterial.SetTexture(prefix + "MaskTex", matcapInfo._MatCapBlendMask);
Debug.Log($"{originalMaterial.name}에 Matcap을 Layer {targetLayerNumber}에 적용함");
}
DestroyImmediate(clonedMaterial);
}
AssetDatabase.SaveAssets();
EditorUtility.DisplayDialog("Matcap 복사 완료", "모든 머티리얼에 Matcap 설정을 적용했습니다.", "확인");
}
private struct stMatcapInfo
{
public bool _UseMatCap;
public Texture _MatCapTex;
public Color _MatCapColor;
public Texture _MatCapBlendMask;
public int _MatCapBlendMode;
public stMatcapInfo(bool use)
{
_UseMatCap = use;
_MatCapTex = null;
_MatCapColor = Color.white;
_MatCapBlendMask = null;
_MatCapBlendMode = 0;
}
public stMatcapInfo(bool use, Texture tex, Color color, Texture mask, int mode)
{
_UseMatCap = use;
_MatCapTex = tex;
_MatCapColor = color;
_MatCapBlendMask = mask;
_MatCapBlendMode = mode;
}
}
private stMatcapInfo GetMatcapInfoByName(Material mat, string propertyName, bool isFirst)
{
string suffix = isFirst ? "" : "2nd";
if (mat.HasProperty(propertyName) && mat.GetInt(propertyName) == 1)
{
return new stMatcapInfo(true,
mat.GetTexture("_MatCap" + suffix + "Tex"),
mat.GetColor("_MatCap" + suffix + "Color"),
mat.GetTexture("_MatCap" + suffix + "BlendMask"),
mat.GetInt("_MatCap" + suffix + "BlendMode"));
}
return new stMatcapInfo(false);
}
}
#endif

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 7a6ca025d3c26104d888d49ec66215df

View File

@ -1,47 +0,0 @@
using UnityEngine;
using UnityEditor;
public class ParentChildSelectionTool : EditorWindow
{
[MenuItem("Tools/Select Parent %q")] // Q 키 (Ctrl/Command+Q)로 상위 오브젝트 선택
private static void SelectParent()
{
if (Selection.activeGameObject != null)
{
Transform selectedTransform = Selection.activeGameObject.transform;
if (selectedTransform.parent != null)
{
Selection.activeGameObject = selectedTransform.parent.gameObject;
}
else
{
Debug.Log("No parent object found.");
}
}
else
{
Debug.Log("No object selected.");
}
}
[MenuItem("Tools/Select Child %a")] // A 키 (Ctrl/Command+A)로 하위 오브젝트 선택
private static void SelectChild()
{
if (Selection.activeGameObject != null)
{
Transform selectedTransform = Selection.activeGameObject.transform;
if (selectedTransform.childCount > 0)
{
Selection.activeGameObject = selectedTransform.GetChild(0).gameObject;
}
else
{
Debug.Log("No child object found.");
}
}
else
{
Debug.Log("No object selected.");
}
}
}

View File

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

View File

@ -25,6 +25,12 @@ public class RenameHumanoidBones : EditorWindow
FindHumanoidBones();
}
// 믹사모 네이밍으로 일괄 변경 버튼 추가
if (GUILayout.Button("믹사모 네이밍으로 일괄 변경"))
{
RenameToMixamo();
}
if (selectedAnimator == null)
{
GUILayout.Label("No valid Humanoid Animator selected.", EditorStyles.helpBox);
@ -33,6 +39,27 @@ public class RenameHumanoidBones : EditorWindow
GUILayout.Label("Selected GameObject: " + selectedAnimator.gameObject.name, EditorStyles.boldLabel);
// T포즈 관련 버튼들 추가
EditorGUILayout.Space();
GUILayout.Label("T-Pose Functions", EditorStyles.boldLabel);
if (GUILayout.Button("Check T-Pose Conditions"))
{
CheckTPoseConditions();
}
if (GUILayout.Button("Apply Perfect T-Pose"))
{
ApplyPerfectTPose();
}
if (GUILayout.Button("Reset to Default Pose"))
{
ResetToDefaultPose();
}
EditorGUILayout.Space();
if (humanoidBones.Count > 0)
{
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, GUILayout.Height(400));
@ -124,4 +151,932 @@ public class RenameHumanoidBones : EditorWindow
Debug.Log("Bone renaming completed.");
}
// T포즈 조건 검증
private void CheckTPoseConditions()
{
if (selectedAnimator == null)
{
Debug.LogError("No valid Humanoid Animator selected.");
return;
}
Debug.Log("=== T-Pose Conditions Check ===");
// 1. 몸통 뼈대들의 World X축 좌표가 0인지 확인
CheckBodyAlignment();
// 2. 팔 뼈대들의 World Z축 좌표가 동일한지 확인
CheckArmAlignment();
// 3. 다리 뼈대들의 World X축 좌표가 동일한지 확인
CheckLegAlignment();
Debug.Log("=== T-Pose Conditions Check Complete ===");
}
private void CheckBodyAlignment()
{
Debug.Log("--- Body Alignment Check ---");
HumanBodyBones[] bodyBones = {
HumanBodyBones.Hips,
HumanBodyBones.Spine,
HumanBodyBones.Chest,
HumanBodyBones.UpperChest,
HumanBodyBones.Neck,
HumanBodyBones.Head
};
float targetX = 0f;
bool allAligned = true;
foreach (var boneType in bodyBones)
{
Transform bone = selectedAnimator.GetBoneTransform(boneType);
if (bone != null)
{
float worldX = bone.position.x;
float difference = Mathf.Abs(worldX - targetX);
Debug.Log($"{boneType}: World X = {worldX:F3} (Target: {targetX:F3}, Difference: {difference:F3})");
if (difference > 0.01f)
{
allAligned = false;
Debug.LogWarning($" -> {boneType} is not aligned! Difference: {difference:F3}");
}
}
}
if (allAligned)
{
Debug.Log("✓ Body bones are properly aligned on X-axis");
}
else
{
Debug.LogWarning("✗ Body bones are NOT properly aligned on X-axis");
}
}
private void CheckArmAlignment()
{
Debug.Log("--- Arm Alignment Check ---");
HumanBodyBones[] leftArmBones = {
HumanBodyBones.LeftShoulder,
HumanBodyBones.LeftUpperArm,
HumanBodyBones.LeftLowerArm,
HumanBodyBones.LeftHand
};
HumanBodyBones[] rightArmBones = {
HumanBodyBones.RightShoulder,
HumanBodyBones.RightUpperArm,
HumanBodyBones.RightLowerArm,
HumanBodyBones.RightHand
};
CheckSideArmAlignment("Left Arm", leftArmBones);
CheckSideArmAlignment("Right Arm", rightArmBones);
}
private void CheckSideArmAlignment(string sideName, HumanBodyBones[] armBones)
{
Debug.Log($"--- {sideName} Alignment Check ---");
float targetZ = 0f;
bool firstBone = true;
bool allAligned = true;
foreach (var boneType in armBones)
{
Transform bone = selectedAnimator.GetBoneTransform(boneType);
if (bone != null)
{
float worldZ = bone.position.z;
if (firstBone)
{
targetZ = worldZ;
firstBone = false;
}
float difference = Mathf.Abs(worldZ - targetZ);
Debug.Log($"{boneType}: World Z = {worldZ:F3} (Target: {targetZ:F3}, Difference: {difference:F3})");
if (difference > 0.01f)
{
allAligned = false;
Debug.LogWarning($" -> {boneType} is not aligned! Difference: {difference:F3}");
}
}
}
if (allAligned)
{
Debug.Log($"✓ {sideName} bones are properly aligned on Z-axis");
}
else
{
Debug.LogWarning($"✗ {sideName} bones are NOT properly aligned on Z-axis");
}
}
private void CheckLegAlignment()
{
Debug.Log("--- Leg Alignment Check ---");
HumanBodyBones[] leftLegBones = {
HumanBodyBones.LeftUpperLeg,
HumanBodyBones.LeftLowerLeg,
HumanBodyBones.LeftFoot,
HumanBodyBones.LeftToes
};
HumanBodyBones[] rightLegBones = {
HumanBodyBones.RightUpperLeg,
HumanBodyBones.RightLowerLeg,
HumanBodyBones.RightFoot,
HumanBodyBones.RightToes
};
CheckSideLegAlignment("Left Leg", leftLegBones);
CheckSideLegAlignment("Right Leg", rightLegBones);
}
private void CheckSideLegAlignment(string sideName, HumanBodyBones[] legBones)
{
Debug.Log($"--- {sideName} Alignment Check ---");
float targetX = 0f;
bool firstBone = true;
bool allAligned = true;
foreach (var boneType in legBones)
{
Transform bone = selectedAnimator.GetBoneTransform(boneType);
if (bone != null)
{
float worldX = bone.position.x;
if (firstBone)
{
targetX = worldX;
firstBone = false;
}
float difference = Mathf.Abs(worldX - targetX);
Debug.Log($"{boneType}: World X = {worldX:F3} (Target: {targetX:F3}, Difference: {difference:F3})");
if (difference > 0.01f)
{
allAligned = false;
Debug.LogWarning($" -> {boneType} is not aligned! Difference: {difference:F3}");
}
}
}
if (allAligned)
{
Debug.Log($"✓ {sideName} bones are properly aligned on X-axis");
}
else
{
Debug.LogWarning($"✗ {sideName} bones are NOT properly aligned on X-axis");
}
}
// 완벽한 T포즈 적용
private void ApplyPerfectTPose()
{
if (selectedAnimator == null)
{
Debug.LogError("No valid Humanoid Animator selected.");
return;
}
Debug.Log("=== Applying Perfect T-Pose ===");
// Undo 시스템에 등록
Undo.RecordObject(selectedAnimator.gameObject, "Apply T-Pose");
// 단계 1: 팔 부분 조정
AdjustArmBones();
// 단계 2: 다리 부분 조정
AdjustLegBones();
Debug.Log("Perfect T-Pose applied successfully!");
}
// 팔 뼈대 조정 (단계 1)
private void AdjustArmBones()
{
Debug.Log("Adjusting arm bones...");
// 왼쪽 팔
AdjustSingleArm(HumanBodyBones.LeftUpperArm, HumanBodyBones.LeftLowerArm, HumanBodyBones.LeftHand, "Left");
// 오른쪽 팔
AdjustSingleArm(HumanBodyBones.RightUpperArm, HumanBodyBones.RightLowerArm, HumanBodyBones.RightHand, "Right");
}
// 다리 뼈대 조정 (단계 2)
private void AdjustLegBones()
{
Debug.Log("Adjusting leg bones...");
// 왼쪽 다리
AdjustSingleLeg(HumanBodyBones.LeftLowerLeg, HumanBodyBones.LeftFoot, "Left");
// 오른쪽 다리
AdjustSingleLeg(HumanBodyBones.RightLowerLeg, HumanBodyBones.RightFoot, "Right");
}
private void AdjustSingleLeg(HumanBodyBones lowerLeg, HumanBodyBones foot, string side)
{
Debug.Log($"Adjusting {side} leg...");
// LowerLeg (무릎) 조정
Transform lowerLegBone = selectedAnimator.GetBoneTransform(lowerLeg);
if (lowerLegBone != null)
{
// 로테이션은 건드리지 않음 (기존 회전값 유지)
// 로컬 X, Z 포지션만 0으로 변경
Vector3 localPos = lowerLegBone.localPosition;
localPos.x = 0f;
localPos.z = 0f;
lowerLegBone.localPosition = localPos;
Debug.Log($"{side} LowerLeg - Local Rotation: {lowerLegBone.localRotation.eulerAngles}, Local Position: {lowerLegBone.localPosition}");
}
// Foot (발목) 조정
Transform footBone = selectedAnimator.GetBoneTransform(foot);
if (footBone != null)
{
// 로테이션은 건드리지 않음 (기존 회전값 유지)
// 로컬 X, Z 포지션만 0으로 변경
Vector3 localPos = footBone.localPosition;
localPos.x = 0f;
localPos.z = 0f;
footBone.localPosition = localPos;
Debug.Log($"{side} Foot - Local Rotation: {footBone.localRotation.eulerAngles}, Local Position: {footBone.localPosition}");
}
// 발가락 조정
AdjustToes(side);
}
private void AdjustToes(string side)
{
Debug.Log($"Adjusting {side} toes...");
HumanBodyBones toeBone;
if (side == "Left")
{
toeBone = HumanBodyBones.LeftToes;
}
else // Right
{
toeBone = HumanBodyBones.RightToes;
}
Transform bone = selectedAnimator.GetBoneTransform(toeBone);
if (bone != null)
{
// 로테이션은 건드리지 않음 (기존 회전값 유지)
// 로컬 X, Z 포지션만 0으로 변경
Vector3 localPos = bone.localPosition;
localPos.x = 0f;
localPos.z = 0f;
bone.localPosition = localPos;
Debug.Log($"{side} Toes - Local Rotation: {bone.localRotation.eulerAngles}, Local Position: {bone.localPosition}");
}
}
private void AdjustSingleArm(HumanBodyBones upperArm, HumanBodyBones lowerArm, HumanBodyBones hand, string side)
{
Debug.Log($"Adjusting {side} arm...");
// UpperArm 조정
Transform upperArmBone = selectedAnimator.GetBoneTransform(upperArm);
if (upperArmBone != null)
{
// 로컬 Rotation을 0,0,0으로 초기화
upperArmBone.localRotation = Quaternion.identity;
// 로컬 X, Z 포지션을 0으로 변경
Vector3 localPos = upperArmBone.localPosition;
localPos.x = 0f;
localPos.z = 0f;
upperArmBone.localPosition = localPos;
Debug.Log($"{side} UpperArm - Local Rotation: {upperArmBone.localRotation.eulerAngles}, Local Position: {upperArmBone.localPosition}");
}
// LowerArm 조정
Transform lowerArmBone = selectedAnimator.GetBoneTransform(lowerArm);
if (lowerArmBone != null)
{
// 로컬 Rotation을 0,0,0으로 초기화
lowerArmBone.localRotation = Quaternion.identity;
// 로컬 X, Z 포지션을 0으로 변경
Vector3 localPos = lowerArmBone.localPosition;
localPos.x = 0f;
localPos.z = 0f;
lowerArmBone.localPosition = localPos;
Debug.Log($"{side} LowerArm - Local Rotation: {lowerArmBone.localRotation.eulerAngles}, Local Position: {lowerArmBone.localPosition}");
}
// Hand 조정
Transform handBone = selectedAnimator.GetBoneTransform(hand);
if (handBone != null)
{
// 로컬 Rotation을 0,0,0으로 초기화
handBone.localRotation = Quaternion.identity;
// 로컬 X, Z 포지션을 0으로 변경
Vector3 localPos = handBone.localPosition;
localPos.x = 0f;
localPos.z = 0f;
handBone.localPosition = localPos;
Debug.Log($"{side} Hand - Local Rotation: {handBone.localRotation.eulerAngles}, Local Position: {handBone.localPosition}");
}
// 손가락 조정 (엄지손가락 제외)
AdjustFingers(side);
}
private void AdjustFingers(string side)
{
Debug.Log($"Adjusting {side} fingers (excluding thumb)...");
// 엄지손가락을 제외한 손가락들
HumanBodyBones[] fingerBones;
if (side == "Left")
{
fingerBones = new HumanBodyBones[]
{
HumanBodyBones.LeftIndexProximal,
HumanBodyBones.LeftIndexIntermediate,
HumanBodyBones.LeftIndexDistal,
HumanBodyBones.LeftMiddleProximal,
HumanBodyBones.LeftMiddleIntermediate,
HumanBodyBones.LeftMiddleDistal,
HumanBodyBones.LeftRingProximal,
HumanBodyBones.LeftRingIntermediate,
HumanBodyBones.LeftRingDistal,
HumanBodyBones.LeftLittleProximal,
HumanBodyBones.LeftLittleIntermediate,
HumanBodyBones.LeftLittleDistal
};
}
else // Right
{
fingerBones = new HumanBodyBones[]
{
HumanBodyBones.RightIndexProximal,
HumanBodyBones.RightIndexIntermediate,
HumanBodyBones.RightIndexDistal,
HumanBodyBones.RightMiddleProximal,
HumanBodyBones.RightMiddleIntermediate,
HumanBodyBones.RightMiddleDistal,
HumanBodyBones.RightRingProximal,
HumanBodyBones.RightRingIntermediate,
HumanBodyBones.RightRingDistal,
HumanBodyBones.RightLittleProximal,
HumanBodyBones.RightLittleIntermediate,
HumanBodyBones.RightLittleDistal
};
}
foreach (var fingerBone in fingerBones)
{
Transform bone = selectedAnimator.GetBoneTransform(fingerBone);
if (bone != null)
{
// 로컬 Rotation을 0,0,0으로 초기화
bone.localRotation = Quaternion.identity;
// 손가락 첫 마디(Proximal)는 X축을 유지, 나머지는 X, Z 포지션을 0으로 변경
Vector3 localPos = bone.localPosition;
if (fingerBone.ToString().Contains("Proximal"))
{
// 첫 마디는 X축 유지, Z축만 0으로
localPos.z = 0f;
}
else
{
// 두번째, 세번째 마디는 X, Z 모두 0으로
localPos.x = 0f;
localPos.z = 0f;
}
bone.localPosition = localPos;
Debug.Log($"{side} {fingerBone} - Local Rotation: {bone.localRotation.eulerAngles}, Local Position: {bone.localPosition}");
}
}
// 엄지손가락 두번째와 세번째 마디 조정
AdjustThumbFingers(side);
}
private void AdjustThumbFingers(string side)
{
Debug.Log($"Adjusting {side} thumb intermediate and distal...");
HumanBodyBones[] thumbBones;
if (side == "Left")
{
thumbBones = new HumanBodyBones[]
{
HumanBodyBones.LeftThumbIntermediate,
HumanBodyBones.LeftThumbDistal
};
}
else // Right
{
thumbBones = new HumanBodyBones[]
{
HumanBodyBones.RightThumbIntermediate,
HumanBodyBones.RightThumbDistal
};
}
foreach (var thumbBone in thumbBones)
{
Transform bone = selectedAnimator.GetBoneTransform(thumbBone);
if (bone != null)
{
// 로컬 Rotation을 0,0,0으로 초기화
bone.localRotation = Quaternion.identity;
// X, Z 포지션을 0으로 변경
Vector3 localPos = bone.localPosition;
localPos.x = 0f;
localPos.z = 0f;
bone.localPosition = localPos;
Debug.Log($"{side} {thumbBone} - Local Rotation: {bone.localRotation.eulerAngles}, Local Position: {bone.localPosition}");
}
}
}
// 뼈대들의 부모-자식 관계를 일시적으로 분리
private Dictionary<Transform, Transform> DetachBonesFromHierarchy()
{
Debug.Log("Detaching bones from hierarchy...");
Dictionary<Transform, Transform> originalParents = new Dictionary<Transform, Transform>();
// 모든 Humanoid 뼈대의 부모-자식 관계를 저장하고 분리
for (int i = 0; i < HumanTrait.BoneCount; i++)
{
HumanBodyBones bone = (HumanBodyBones)i;
Transform boneTransform = selectedAnimator.GetBoneTransform(bone);
if (boneTransform != null)
{
// 원래 부모를 저장
originalParents[boneTransform] = boneTransform.parent;
// 뼈대를 최상위 레벨로 이동 (계층구조에서 분리)
boneTransform.SetParent(null, true);
Debug.Log($"Detached {bone} from parent: {originalParents[boneTransform]?.name ?? "None"}");
}
}
Debug.Log($"Detached {originalParents.Count} bones from hierarchy");
return originalParents;
}
// 뼈대들을 원래 부모-자식 관계로 복원
private void RestoreBoneHierarchy(Dictionary<Transform, Transform> originalParents)
{
Debug.Log("Restoring bone hierarchy...");
foreach (var kvp in originalParents)
{
Transform bone = kvp.Key;
Transform originalParent = kvp.Value;
if (bone != null)
{
// 원래 부모로 복원
bone.SetParent(originalParent, true);
Debug.Log($"Restored {bone.name} to parent: {originalParent?.name ?? "None"}");
}
}
Debug.Log("Bone hierarchy restored");
}
private void AlignBodyBones()
{
Debug.Log("Aligning body bones...");
HumanBodyBones[] bodyBones = {
HumanBodyBones.Hips,
HumanBodyBones.Spine,
HumanBodyBones.Chest,
HumanBodyBones.UpperChest,
HumanBodyBones.Neck,
HumanBodyBones.Head
};
foreach (var boneType in bodyBones)
{
Transform bone = selectedAnimator.GetBoneTransform(boneType);
if (bone != null)
{
// 현재 위치에서 X축만 0으로 조정
Vector3 newPosition = bone.position;
newPosition.x = 0f;
bone.position = newPosition;
// 회전을 0으로 설정 (똑바로 서있음)
bone.rotation = Quaternion.identity;
Debug.Log($"Aligned {boneType} to X=0, rotation=0");
}
}
}
private void SetArmsToTPose()
{
Debug.Log("Setting arms to T-pose...");
// 왼쪽 팔
SetArmToTPose(HumanBodyBones.LeftShoulder, HumanBodyBones.LeftUpperArm,
HumanBodyBones.LeftLowerArm, HumanBodyBones.LeftHand, true);
// 오른쪽 팔
SetArmToTPose(HumanBodyBones.RightShoulder, HumanBodyBones.RightUpperArm,
HumanBodyBones.RightLowerArm, HumanBodyBones.RightHand, false);
}
private void SetArmToTPose(HumanBodyBones shoulder, HumanBodyBones upperArm,
HumanBodyBones lowerArm, HumanBodyBones hand, bool isLeft)
{
string side = isLeft ? "Left" : "Right";
// 어깨 위치 및 회전 조정
Transform shoulderBone = selectedAnimator.GetBoneTransform(shoulder);
if (shoulderBone != null)
{
// 어깨를 몸통 중심에서 적절한 거리로 이동
Vector3 shoulderPos = shoulderBone.position;
float shoulderOffset = isLeft ? -0.3f : 0.3f;
shoulderPos.x = shoulderOffset;
shoulderBone.position = shoulderPos;
// 어깨를 Z축 기준으로 회전하여 팔이 수평이 되도록 함
float shoulderZRotation = isLeft ? 90f : -90f;
shoulderBone.rotation = Quaternion.Euler(0, 0, shoulderZRotation);
Debug.Log($"Positioned {side} shoulder at X={shoulderOffset}, Z rotation={shoulderZRotation}°");
}
// 상완 위치 및 회전 조정
Transform upperArmBone = selectedAnimator.GetBoneTransform(upperArm);
if (upperArmBone != null)
{
// 상완을 어깨에서 수평으로 뻗도록 위치 조정
Vector3 upperArmPos = upperArmBone.position;
float armLength = Vector3.Distance(shoulderBone.position, upperArmBone.position);
upperArmPos.x = isLeft ? -armLength : armLength;
upperArmBone.position = upperArmPos;
// 상완을 완전히 펴기 위해 회전 조정
upperArmBone.rotation = Quaternion.identity;
Debug.Log($"Positioned {side} upper arm at X={upperArmPos.x}, rotation=0°");
}
// 하완 위치 및 회전 조정
Transform lowerArmBone = selectedAnimator.GetBoneTransform(lowerArm);
if (lowerArmBone != null)
{
// 하완을 상완에서 수평으로 뻗도록 위치 조정
Vector3 lowerArmPos = lowerArmBone.position;
float forearmLength = Vector3.Distance(upperArmBone.position, lowerArmBone.position);
lowerArmPos.x = isLeft ? -(upperArmBone.position.x + forearmLength) : (upperArmBone.position.x + forearmLength);
lowerArmBone.position = lowerArmPos;
// 하완을 완전히 펴기 위해 회전 조정
lowerArmBone.rotation = Quaternion.identity;
Debug.Log($"Positioned {side} lower arm at X={lowerArmPos.x}, rotation=0°");
}
// 손 위치 및 회전 조정
Transform handBone = selectedAnimator.GetBoneTransform(hand);
if (handBone != null)
{
// 손을 하완에서 수평으로 뻗도록 위치 조정
Vector3 handPos = handBone.position;
float handLength = Vector3.Distance(lowerArmBone.position, handBone.position);
handPos.x = isLeft ? -(lowerArmBone.position.x + handLength) : (lowerArmBone.position.x + handLength);
handBone.position = handPos;
// 손을 자연스럽게 펴기 위해 회전 조정
handBone.rotation = Quaternion.identity;
Debug.Log($"Positioned {side} hand at X={handPos.x}, rotation=0°");
}
}
private void AlignLegBones()
{
Debug.Log("Aligning leg bones...");
HumanBodyBones[] leftLegBones = {
HumanBodyBones.LeftUpperLeg,
HumanBodyBones.LeftLowerLeg,
HumanBodyBones.LeftFoot,
HumanBodyBones.LeftToes
};
HumanBodyBones[] rightLegBones = {
HumanBodyBones.RightUpperLeg,
HumanBodyBones.RightLowerLeg,
HumanBodyBones.RightFoot,
HumanBodyBones.RightToes
};
AlignSideLegBones("Left", leftLegBones, -0.2f);
AlignSideLegBones("Right", rightLegBones, 0.2f);
}
private void AlignSideLegBones(string side, HumanBodyBones[] legBones, float targetX)
{
foreach (var boneType in legBones)
{
Transform bone = selectedAnimator.GetBoneTransform(boneType);
if (bone != null)
{
// 위치를 목표 X축으로 조정
Vector3 newPosition = bone.position;
newPosition.x = targetX;
bone.position = newPosition;
// 다리 뼈대들을 똑바로 세우기 위해 회전 조정
bone.rotation = Quaternion.identity;
Debug.Log($"Aligned {boneType} to X={targetX:F3}, rotation=0°");
}
}
}
private void SetFingersAndToesToTPose()
{
Debug.Log("Setting fingers and toes to T-pose...");
// 손가락 설정 (완전히 펴짐)
SetFingersToTPose(true); // 왼쪽
SetFingersToTPose(false); // 오른쪽
// 발가락 설정 (자연스러운 상태)
SetToesToTPose(true); // 왼쪽
SetToesToTPose(false); // 오른쪽
}
private void SetFingersToTPose(bool isLeft)
{
string side = isLeft ? "Left" : "Right";
// 모든 손가락 관절을 0도로 설정
for (int finger = 0; finger < 5; finger++)
{
for (int joint = 0; joint < 3; joint++)
{
HumanBodyBones fingerBone = GetFingerBone(isLeft, finger, joint);
if (fingerBone != HumanBodyBones.LastBone)
{
Transform bone = selectedAnimator.GetBoneTransform(fingerBone);
if (bone != null)
{
bone.localRotation = Quaternion.identity;
}
}
}
}
Debug.Log($"Set {side} fingers to straight position");
}
private void SetToesToTPose(bool isLeft)
{
string side = isLeft ? "Left" : "Right";
// 발가락을 자연스러운 상태로 설정
HumanBodyBones toeBone = isLeft ? HumanBodyBones.LeftToes : HumanBodyBones.RightToes;
Transform bone = selectedAnimator.GetBoneTransform(toeBone);
if (bone != null)
{
bone.localRotation = Quaternion.identity;
Debug.Log($"Set {side} toes to natural position");
}
}
private HumanBodyBones GetFingerBone(bool isLeft, int finger, int joint)
{
if (finger == 0) // 엄지
{
if (isLeft)
{
switch (joint)
{
case 0: return HumanBodyBones.LeftThumbProximal;
case 1: return HumanBodyBones.LeftThumbIntermediate;
case 2: return HumanBodyBones.LeftThumbDistal;
}
}
else
{
switch (joint)
{
case 0: return HumanBodyBones.RightThumbProximal;
case 1: return HumanBodyBones.RightThumbIntermediate;
case 2: return HumanBodyBones.RightThumbDistal;
}
}
}
else // 나머지 손가락들
{
if (isLeft)
{
switch (finger)
{
case 1: return joint == 0 ? HumanBodyBones.LeftIndexProximal :
joint == 1 ? HumanBodyBones.LeftIndexIntermediate :
HumanBodyBones.LeftIndexDistal;
case 2: return joint == 0 ? HumanBodyBones.LeftMiddleProximal :
joint == 1 ? HumanBodyBones.LeftMiddleIntermediate :
HumanBodyBones.LeftMiddleDistal;
case 3: return joint == 0 ? HumanBodyBones.LeftRingProximal :
joint == 1 ? HumanBodyBones.LeftRingIntermediate :
HumanBodyBones.LeftRingDistal;
case 4: return joint == 0 ? HumanBodyBones.LeftLittleProximal :
joint == 1 ? HumanBodyBones.LeftLittleIntermediate :
HumanBodyBones.LeftLittleDistal;
}
}
else
{
switch (finger)
{
case 1: return joint == 0 ? HumanBodyBones.RightIndexProximal :
joint == 1 ? HumanBodyBones.RightIndexIntermediate :
HumanBodyBones.RightIndexDistal;
case 2: return joint == 0 ? HumanBodyBones.RightMiddleProximal :
joint == 1 ? HumanBodyBones.RightMiddleIntermediate :
HumanBodyBones.RightMiddleDistal;
case 3: return joint == 0 ? HumanBodyBones.RightRingProximal :
joint == 1 ? HumanBodyBones.RightRingIntermediate :
HumanBodyBones.RightRingDistal;
case 4: return joint == 0 ? HumanBodyBones.RightLittleProximal :
joint == 1 ? HumanBodyBones.RightLittleIntermediate :
HumanBodyBones.RightLittleDistal;
}
}
}
return HumanBodyBones.LastBone; // 유효하지 않은 경우
}
// 기본 포즈로 리셋
private void ResetToDefaultPose()
{
if (selectedAnimator == null)
{
Debug.LogError("No valid Humanoid Animator selected.");
return;
}
Debug.Log("=== Resetting to Default Pose ===");
// Undo 시스템에 등록
Undo.RecordObject(selectedAnimator.gameObject, "Reset to Default Pose");
// 모든 뼈대의 로컬 회전을 0으로 리셋
for (int i = 0; i < HumanTrait.BoneCount; i++)
{
HumanBodyBones bone = (HumanBodyBones)i;
Transform boneTransform = selectedAnimator.GetBoneTransform(bone);
if (boneTransform != null)
{
boneTransform.localRotation = Quaternion.identity;
}
}
Debug.Log("Reset to default pose completed!");
}
// 믹사모 네이밍 매핑 및 일괄 변경 함수 추가
private void RenameToMixamo()
{
if (selectedAnimator == null)
{
Debug.LogError("No valid Humanoid Animator selected.");
return;
}
// HumanBodyBones와 믹사모 네이밍 매핑
var mixamoMap = new Dictionary<HumanBodyBones, string>
{
{ HumanBodyBones.Hips, "mixamorig:Hips" },
{ HumanBodyBones.LeftUpperLeg, "mixamorig:LeftUpLeg" },
{ HumanBodyBones.LeftLowerLeg, "mixamorig:LeftLeg" },
{ HumanBodyBones.LeftFoot, "mixamorig:LeftFoot" },
{ HumanBodyBones.LeftToes, "mixamorig:LeftToeBase" },
{ HumanBodyBones.RightUpperLeg, "mixamorig:RightUpLeg" },
{ HumanBodyBones.RightLowerLeg, "mixamorig:RightLeg" },
{ HumanBodyBones.RightFoot, "mixamorig:RightFoot" },
{ HumanBodyBones.RightToes, "mixamorig:RightToeBase" },
{ HumanBodyBones.Spine, "mixamorig:Spine" },
{ HumanBodyBones.Chest, "mixamorig:Spine1" },
{ HumanBodyBones.UpperChest, "mixamorig:Spine2" }, // UpperChest는 있는 경우만
{ HumanBodyBones.Neck, "mixamorig:Neck" },
{ HumanBodyBones.Head, "mixamorig:Head" },
{ HumanBodyBones.LeftShoulder, "mixamorig:LeftShoulder" },
{ HumanBodyBones.LeftUpperArm, "mixamorig:LeftArm" },
{ HumanBodyBones.LeftLowerArm, "mixamorig:LeftForeArm" },
{ HumanBodyBones.LeftHand, "mixamorig:LeftHand" },
{ HumanBodyBones.RightShoulder, "mixamorig:RightShoulder" },
{ HumanBodyBones.RightUpperArm, "mixamorig:RightArm" },
{ HumanBodyBones.RightLowerArm, "mixamorig:RightForeArm" },
{ HumanBodyBones.RightHand, "mixamorig:RightHand" },
// 왼손 손가락
{ HumanBodyBones.LeftThumbProximal, "mixamorig:LeftHandThumb1" },
{ HumanBodyBones.LeftThumbIntermediate, "mixamorig:LeftHandThumb2" },
{ HumanBodyBones.LeftThumbDistal, "mixamorig:LeftHandThumb3" },
// mixamo에는 Thumb4가 있으나, Unity에는 없음
{ HumanBodyBones.LeftIndexProximal, "mixamorig:LeftHandIndex1" },
{ HumanBodyBones.LeftIndexIntermediate, "mixamorig:LeftHandIndex2" },
{ HumanBodyBones.LeftIndexDistal, "mixamorig:LeftHandIndex3" },
{ HumanBodyBones.LeftMiddleProximal, "mixamorig:LeftHandMiddle1" },
{ HumanBodyBones.LeftMiddleIntermediate, "mixamorig:LeftHandMiddle2" },
{ HumanBodyBones.LeftMiddleDistal, "mixamorig:LeftHandMiddle3" },
{ HumanBodyBones.LeftRingProximal, "mixamorig:LeftHandRing1" },
{ HumanBodyBones.LeftRingIntermediate, "mixamorig:LeftHandRing2" },
{ HumanBodyBones.LeftRingDistal, "mixamorig:LeftHandRing3" },
{ HumanBodyBones.LeftLittleProximal, "mixamorig:LeftHandPinky1" },
{ HumanBodyBones.LeftLittleIntermediate, "mixamorig:LeftHandPinky2" },
{ HumanBodyBones.LeftLittleDistal, "mixamorig:LeftHandPinky3" },
// 오른손 손가락
{ HumanBodyBones.RightThumbProximal, "mixamorig:RightHandThumb1" },
{ HumanBodyBones.RightThumbIntermediate, "mixamorig:RightHandThumb2" },
{ HumanBodyBones.RightThumbDistal, "mixamorig:RightHandThumb3" },
{ HumanBodyBones.RightIndexProximal, "mixamorig:RightHandIndex1" },
{ HumanBodyBones.RightIndexIntermediate, "mixamorig:RightHandIndex2" },
{ HumanBodyBones.RightIndexDistal, "mixamorig:RightHandIndex3" },
{ HumanBodyBones.RightMiddleProximal, "mixamorig:RightHandMiddle1" },
{ HumanBodyBones.RightMiddleIntermediate, "mixamorig:RightHandMiddle2" },
{ HumanBodyBones.RightMiddleDistal, "mixamorig:RightHandMiddle3" },
{ HumanBodyBones.RightRingProximal, "mixamorig:RightHandRing1" },
{ HumanBodyBones.RightRingIntermediate, "mixamorig:RightHandRing2" },
{ HumanBodyBones.RightRingDistal, "mixamorig:RightHandRing3" },
{ HumanBodyBones.RightLittleProximal, "mixamorig:RightHandPinky1" },
{ HumanBodyBones.RightLittleIntermediate, "mixamorig:RightHandPinky2" },
{ HumanBodyBones.RightLittleDistal, "mixamorig:RightHandPinky3" },
};
// UpperChest가 없는 경우 무시
foreach (var kvp in mixamoMap)
{
if (kvp.Key == HumanBodyBones.UpperChest)
{
var upperChest = selectedAnimator.GetBoneTransform(HumanBodyBones.UpperChest);
if (upperChest == null) continue;
upperChest.name = kvp.Value;
Debug.Log($"UpperChest 본이 있어 이름을 {kvp.Value}로 변경");
continue;
}
var bone = selectedAnimator.GetBoneTransform(kvp.Key);
if (bone != null)
{
bone.name = kvp.Value;
Debug.Log($"{kvp.Key} 이름을 {kvp.Value}로 변경");
}
}
Debug.Log("믹사모 네이밍으로 일괄 변경 완료!");
}
}