using UnityEditor; using UnityEngine; namespace KindRetargeting { /// /// CustomRetargetingScript의 인스펙터를 커스터마이징하여 프로퍼티들을 접었다 펼칠 수 있는 섹션으로 그룹화합니다. /// [CustomEditor(typeof(CustomRetargetingScript))] public class CustomRetargetingScriptEditor : BaseRetargetingEditor { // Foldout 상태를 저장할 변수들 private bool showHipsSettings = false; private bool showFingerCopySettings = false; private bool showMotionFilterSettings = false; private bool showRoughMotionSettings = false; private bool showKneeSettings = true; private bool showFootSettings = true; private bool hasCachedData = false; private bool showFloorSettings = false; private bool showScaleSettings = true; // SerializedProperty 변수들 private SerializedProperty sourceAnimatorProp; private SerializedProperty targetAnimatorProp; private SerializedProperty hipsOffsetXProp; private SerializedProperty hipsOffsetYProp; private SerializedProperty hipsOffsetZProp; private SerializedProperty debugAxisNormalizerProp; private SerializedProperty fingerCopyModeProp; private SerializedProperty useMotionFilterProp; private SerializedProperty filterBufferSizeProp; private SerializedProperty useBodyRoughMotionProp; private SerializedProperty useFingerRoughMotionProp; private SerializedProperty bodyRoughnessProp; private SerializedProperty fingerRoughnessProp; private SerializedProperty kneeInOutWeightProp; private SerializedProperty kneeFrontBackWeightProp; private SerializedProperty footFrontBackOffsetProp; private SerializedProperty footInOutOffsetProp; private SerializedProperty floorHeightProp; private SerializedProperty avatarScaleProp; protected override void OnEnable() { base.OnEnable(); // SerializedProperty 초기화 sourceAnimatorProp = serializedObject.FindProperty("sourceAnimator"); targetAnimatorProp = serializedObject.FindProperty("targetAnimator"); hipsOffsetXProp = serializedObject.FindProperty("hipsOffsetX"); hipsOffsetYProp = serializedObject.FindProperty("hipsOffsetY"); hipsOffsetZProp = serializedObject.FindProperty("hipsOffsetZ"); debugAxisNormalizerProp = serializedObject.FindProperty("debugAxisNormalizer"); fingerCopyModeProp = serializedObject.FindProperty("fingerCopyMode"); useMotionFilterProp = serializedObject.FindProperty("useMotionFilter"); filterBufferSizeProp = serializedObject.FindProperty("filterBufferSize"); useBodyRoughMotionProp = serializedObject.FindProperty("useBodyRoughMotion"); useFingerRoughMotionProp = serializedObject.FindProperty("useFingerRoughMotion"); bodyRoughnessProp = serializedObject.FindProperty("bodyRoughness"); fingerRoughnessProp = serializedObject.FindProperty("fingerRoughness"); kneeInOutWeightProp = serializedObject.FindProperty("kneeInOutWeight"); kneeFrontBackWeightProp = serializedObject.FindProperty("kneeFrontBackWeight"); footFrontBackOffsetProp = serializedObject.FindProperty("footFrontBackOffset"); footInOutOffsetProp = serializedObject.FindProperty("footInOutOffset"); floorHeightProp = serializedObject.FindProperty("floorHeight"); avatarScaleProp = serializedObject.FindProperty("avatarScale"); } private void CheckCacheStatus() { var script = (CustomRetargetingScript)target; hasCachedData = script.HasCachedSettings(); } public override void OnInspectorGUI() { serializedObject.Update(); EditorGUI.BeginChangeCheck(); GUILayout.Space(10); // sourceAnimator를 한 번만 표시 EditorGUILayout.PropertyField(sourceAnimatorProp, new GUIContent("원본 Animator")); GUILayout.Space(10); // 아바타 크기 조정 섹션 추가 showScaleSettings = EditorGUILayout.Foldout(showScaleSettings, "아바타 크기 설정", true); if (showScaleSettings) { EditorGUI.indentLevel++; EditorGUILayout.PropertyField(avatarScaleProp, new GUIContent("아바타 크기")); EditorGUI.indentLevel--; } GUILayout.Space(5); // 힙 위치 보정 Foldout showHipsSettings = EditorGUILayout.Foldout(showHipsSettings, "힙 위치 보정 (로컬 좌표계)"); if (showHipsSettings) { EditorGUI.indentLevel++; // 축 매핑 정보 표시 if (debugAxisNormalizerProp != null) { Vector3 axisMapping = debugAxisNormalizerProp.vector3Value; EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.LabelField("축 매핑 정보 (T-포즈 기준)", EditorStyles.boldLabel); // 플레이 모드에서만 정확한 정보 표시 if (Application.isPlaying && axisMapping != Vector3.one) { // axisMapping: 1=X, 2=Y, 3=Z, 부호는 방향 string GetAxisName(float value) { int axis = Mathf.RoundToInt(Mathf.Abs(value)); string sign = value > 0 ? "+" : "-"; return axis switch { 1 => $"{sign}X", 2 => $"{sign}Y", 3 => $"{sign}Z", _ => "?" }; } EditorGUILayout.HelpBox( "T-포즈에서 분석된 축 매핑:\n" + $" 좌우 오프셋 → 로컬 {GetAxisName(axisMapping.x)} 축\n" + $" 상하 오프셋 → 로컬 {GetAxisName(axisMapping.y)} 축\n" + $" 앞뒤 오프셋 → 로컬 {GetAxisName(axisMapping.z)} 축\n\n" + "이 매핑 덕분에 모든 아바타에서 동일하게 작동합니다.", MessageType.Info); } else { EditorGUILayout.HelpBox( "플레이 모드에서 T-포즈 분석 후 축 매핑 정보가 표시됩니다.\n" + "이 매핑은 각 아바타의 힙 로컬 축 방향에 맞춰 자동 계산됩니다.", MessageType.Info); } EditorGUILayout.EndVertical(); GUILayout.Space(5); } EditorGUILayout.PropertyField(hipsOffsetXProp, new GUIContent("좌우 오프셋 (←-/+→)", "캐릭터 기준 왼쪽(-) / 오른쪽(+)")); EditorGUILayout.PropertyField(hipsOffsetYProp, new GUIContent("상하 오프셋 (↓-/+↑)", "캐릭터 기준 아래(-) / 위(+)")); EditorGUILayout.PropertyField(hipsOffsetZProp, new GUIContent("앞뒤 오프셋 (←-/+→)", "캐릭터 기준 뒤(-) / 앞(+)")); EditorGUILayout.HelpBox( "로컬 좌표계 기반: 캐릭터의 회전 상태와 관계없이 항상 캐릭터 기준으로 이동합니다.", MessageType.Info); EditorGUI.indentLevel--; } GUILayout.Space(5); // 무릎 위치 조정 Foldout showKneeSettings = EditorGUILayout.Foldout(showKneeSettings, "무릎 위치 조정", true); if (showKneeSettings) { EditorGUI.indentLevel++; // 무릎 안/밖 조정 EditorGUILayout.Slider(kneeFrontBackWeightProp, -1f, 1f, new GUIContent("무릎 앞/뒤 가중치", "음수: 뒤로, 양수: 앞으로")); EditorGUILayout.Slider(kneeInOutWeightProp, -1f, 1f, new GUIContent("무릎 안/밖 가중치", "음수: 안쪽, 양수: 바깥쪽")); EditorGUI.indentLevel--; } GUILayout.Space(5); // 발 IK 위치 조정 Foldout showFootSettings = EditorGUILayout.Foldout(showFootSettings, "발 IK 위치 조정", true); if (showFootSettings) { EditorGUI.indentLevel++; EditorGUILayout.Slider(footFrontBackOffsetProp, -1f, 1f, new GUIContent("발 앞/뒤 오프셋", "+: 앞으로, -: 뒤로")); EditorGUILayout.Slider(footInOutOffsetProp, -1f, 1f, new GUIContent("발 벌리기/모으기", "+: 벌리기, -: 모으기")); EditorGUI.indentLevel--; } GUILayout.Space(5); // 손가락 복제 설정 Foldout showFingerCopySettings = EditorGUILayout.Foldout(showFingerCopySettings, "손가락 복제 설정"); if (showFingerCopySettings) { EditorGUI.indentLevel++; EditorGUILayout.PropertyField(fingerCopyModeProp, new GUIContent("복제 방식", "손가락 포즈를 복제하는 방식을 선택합니다.")); // Mingle 모드일 때 캘리브레이션 버튼 표시 if (fingerCopyModeProp.enumValueIndex == (int)EnumsList.FingerCopyMode.Mingle) { GUILayout.Space(5); EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.LabelField("Mingle 캘리브레이션", EditorStyles.boldLabel); EditorGUILayout.HelpBox( "Mingle 모드는 소스 아바타의 손가락 회전 범위를 캘리브레이션하여 타겟에 적용합니다.\n" + "1. 손가락을 완전히 펼친 상태에서 '펼침 기록' 클릭\n" + "2. 손가락을 완전히 모은(주먹) 상태에서 '모음 기록' 클릭", MessageType.Info); var script = (CustomRetargetingScript)target; // 자동 캘리브레이션 진행 중일 때 if (Application.isPlaying && script.IsAutoCalibrating) { EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.LabelField("자동 캘리브레이션 진행 중", EditorStyles.boldLabel); EditorGUILayout.LabelField($"상태: {script.AutoCalibrationStatus}"); EditorGUILayout.LabelField($"남은 시간: {script.AutoCalibrationTimeRemaining:F1}초"); if (GUILayout.Button("취소")) { script.StopAutoCalibration(); } EditorGUILayout.EndVertical(); // 인스펙터 갱신 Repaint(); } else { // 수동 캘리브레이션 버튼 EditorGUILayout.BeginHorizontal(); GUI.enabled = Application.isPlaying; if (GUILayout.Button("펼침 기록 (Open)")) { script.CalibrateMingleOpen(); } if (GUILayout.Button("모음 기록 (Close)")) { script.CalibrateMingleClose(); } GUI.enabled = true; EditorGUILayout.EndHorizontal(); // 자동 캘리브레이션 버튼 GUILayout.Space(5); GUI.enabled = Application.isPlaying; if (GUILayout.Button("자동 캘리브레이션 (3초 펼침 → 3초 모음)")) { script.StartAutoCalibration(); } GUI.enabled = true; } if (!Application.isPlaying) { EditorGUILayout.HelpBox("캘리브레이션은 플레이 모드에서만 가능합니다.", MessageType.Warning); } EditorGUILayout.EndVertical(); } EditorGUI.indentLevel--; } GUILayout.Space(5); // 모션 필터링 설정 Foldout showMotionFilterSettings = EditorGUILayout.Foldout(showMotionFilterSettings, "모션 필터링 설정"); if (showMotionFilterSettings) { EditorGUI.indentLevel++; EditorGUILayout.PropertyField(useMotionFilterProp, new GUIContent("모션 필터 사용", "모션 필터링을 적용할지 여부를 설정합니다.")); if (useMotionFilterProp.boolValue) { EditorGUILayout.PropertyField(filterBufferSizeProp, new GUIContent("필터 버퍼 크기", "모션 필터링에 사용할 버퍼의 크기를 설정합니다. (2-10)")); } EditorGUI.indentLevel--; } GUILayout.Space(5); // 러프 모션 설정 Foldout showRoughMotionSettings = EditorGUILayout.Foldout(showRoughMotionSettings, "러프 모션 설정"); if (showRoughMotionSettings) { EditorGUI.indentLevel++; // 몸 러프 모션 설정 EditorGUILayout.PropertyField(useBodyRoughMotionProp, new GUIContent("몸 러프 모션 사용", "몸의 러프한 움직임을 적용할지 여부를 설정합니다.")); if (useBodyRoughMotionProp.boolValue) { EditorGUILayout.PropertyField(bodyRoughnessProp, new GUIContent("몸 러프니스", "몸 전체의 러프한 정도를 설정합니다 (0: 없음, 1: 최대)")); } // 손가락 러프 모션 설정 EditorGUILayout.PropertyField(useFingerRoughMotionProp, new GUIContent("손가락 러프 모션 사용", "손가락의 러프한 움직임을 적용할지 여부를 설정합니다.")); if (useFingerRoughMotionProp.boolValue) { EditorGUILayout.PropertyField(fingerRoughnessProp, new GUIContent("손가락 러프니스", "손가락의 러프한 정도를 설정합니다 (0: 없음, 1: 최대)")); } EditorGUI.indentLevel--; } GUILayout.Space(5); // 바닥 높이 조정 Foldout showFloorSettings = EditorGUILayout.Foldout(showFloorSettings, "바닥 높이 조정", true); if (showFloorSettings) { EditorGUI.indentLevel++; EditorGUILayout.PropertyField(floorHeightProp, new GUIContent("바닥 높이 (-1 ~ 1)")); EditorGUI.indentLevel--; } // 캐시 상태를 텍스트로 표시 EditorGUILayout.LabelField(hasCachedData ? "캘리브레이션 데이터가 저장되어 있습니다." : "저장된 캘리브레이션 데이터가 없습니다.", EditorStyles.boldLabel); // 버튼들을 수평으로 배치 EditorGUILayout.BeginHorizontal(); // I-포즈 캘리브레이션 버튼 if (GUILayout.Button("I-포즈 캘리브레이션")) { var script = (CustomRetargetingScript)target; script.I_PoseCalibration(); CheckCacheStatus(); Repaint(); } // 캐시 삭제 버튼 (캐시가 있을 때만 표시) if (hasCachedData) { if (GUILayout.Button("캐시 데이터 삭제")) { var script = (CustomRetargetingScript)target; script.ResetPoseAndCache(); CheckCacheStatus(); Repaint(); } } EditorGUILayout.EndHorizontal(); GUILayout.Space(10); if (EditorGUI.EndChangeCheck()) { serializedObject.ApplyModifiedProperties(); var script = (CustomRetargetingScript)target; if (script.targetAnimator != null) // targetAnimator가 설정되어 있을 때만 저장 { script.SaveSettings(); } } else { serializedObject.ApplyModifiedProperties(); } } } }