스크립트 업데이트
This commit is contained in:
parent
7c1372088e
commit
a93dcf7ec8
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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}개 오브젝트 추가됨");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
Assets/External/EasyMotionRecorder/Scripts/README_SavePathManager.md
(Stored with Git LFS)
vendored
BIN
Assets/External/EasyMotionRecorder/Scripts/README_SavePathManager.md
(Stored with Git LFS)
vendored
Binary file not shown.
@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d52d79965c0c87f4dbc7d4ea99597abe
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3ab2d3837e4d1874a9bfe3986e13179e
|
||||
@ -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 복사가 완료되었습니다.");
|
||||
}
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 39f91032a3569014da62f31d9fe0cb8b
|
||||
315
Assets/Scripts/HumanBoneNameCopier.cs
Normal file
315
Assets/Scripts/HumanBoneNameCopier.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/HumanBoneNameCopier.cs.meta
Normal file
2
Assets/Scripts/HumanBoneNameCopier.cs.meta
Normal file
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b3338de307f573e4988641354c1e2edd
|
||||
@ -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.");
|
||||
}
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4d444948c00f634bb8a74111035a4d6
|
||||
@ -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
|
||||
@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a6ca025d3c26104d888d49ec66215df
|
||||
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cf82a01a2d2362a459389171c4b3cb1d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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("믹사모 네이밍으로 일괄 변경 완료!");
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user