using UnityEngine; using UnityEditor; using System.Collections.Generic; using KindRetargeting; public class RetargetingControlWindow : EditorWindow { private Vector2 scrollPosition; private bool showGlobalSettings = true; private CustomRetargetingScript[] retargetingScripts; private Dictionary presets = new Dictionary(); private string currentPresetName = ""; // 각 객체별 foldout 상태를 저장할 Dictionary들 private Dictionary weightSettingsFoldouts = new Dictionary(); private Dictionary hipsSettingsFoldouts = new Dictionary(); private Dictionary kneeSettingsFoldouts = new Dictionary(); private Dictionary fingerControlFoldouts = new Dictionary(); private Dictionary fingerCopyFoldouts = new Dictionary(); private Dictionary motionSettingsFoldouts = new Dictionary(); private Dictionary floorSettingsFoldouts = new Dictionary(); private Dictionary scaleSettingsFoldouts = new Dictionary(); private Dictionary leftHandFoldouts = new Dictionary(); private Dictionary rightHandFoldouts = new Dictionary(); private Dictionary headFoldouts = new Dictionary(); private Dictionary propControlFoldouts = new Dictionary(); private bool isDirty = false; private double lastUpdateTime; private const double UPDATE_INTERVAL = 0.25; // 250ms private const float SLIDER_HEIGHT = 100f; private const float SLIDER_WIDTH = 25f; private const float SPACING = 5f; [MenuItem("Tools/리타게팅 컨트롤 패널")] public static void ShowWindow() { GetWindow("리타게팅 컨트롤"); } private void OnEnable() { // 씬에서 모든 리타게팅 스크립트 찾기 retargetingScripts = FindObjectsByType(FindObjectsSortMode.None); EditorApplication.update += OnEditorUpdate; Undo.undoRedoPerformed += OnUndoRedo; lastUpdateTime = EditorApplication.timeSinceStartup; } private void OnDisable() { EditorApplication.update -= OnEditorUpdate; Undo.undoRedoPerformed -= OnUndoRedo; } private void OnEditorUpdate() { // 플레이 모드일 때는 업데이트 처리를 하지 않음 if (Application.isPlaying) return; // 업데이트 간격을 더 길게 조정 (100ms -> 250ms) if (EditorApplication.timeSinceStartup - lastUpdateTime < UPDATE_INTERVAL) return; // 실제로 변경된 경우에만 리프레시 수행 if (isDirty) { RefreshScripts(); isDirty = false; lastUpdateTime = EditorApplication.timeSinceStartup; } } private void OnUndoRedo() { isDirty = true; Repaint(); } private void RefreshScripts() { // 플레이 모드일 때는 씬 수정을 하지 않음 if (Application.isPlaying) return; // 캐싱된 스크립트 사용 if (retargetingScripts == null || retargetingScripts.Length == 0) retargetingScripts = FindObjectsByType(FindObjectsSortMode.None); bool anyDirty = false; foreach (var script in retargetingScripts) { if (script != null && script.isActiveAndEnabled) // 활성화된 컴포넌트만 처리 { EditorUtility.SetDirty(script); anyDirty = true; } } if (anyDirty && !Application.isPlaying) // 플레이 모드가 아닐 때만 씬을 수정 { UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty( retargetingScripts[0].gameObject.scene); } // 불필요한 리페인트 제거 if (focusedWindow == this) { Repaint(); } } private void OnGUI() { if (retargetingScripts == null || retargetingScripts.Length == 0) { EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.HelpBox("씬에서 리타게팅 스크립트를 찾을 수 없습니다.", MessageType.Warning); if (GUILayout.Button("리타게팅 스크립트 리프레시", GUILayout.Height(30))) { retargetingScripts = FindObjectsByType(FindObjectsSortMode.None); // 각 리타게팅 스크립트에 대해 필요한 컴포넌트들 체크 foreach (var script in retargetingScripts) { if (script != null) { // LimbWeightController가 없다면 추가 if (script.GetComponent() == null) { script.gameObject.AddComponent(); } // FingerShapedController가 없다면 추가 if (script.GetComponent() == null) { script.gameObject.AddComponent(); } // PropLocationController가 없다면 추가 if (script.GetComponent() == null) { script.gameObject.AddComponent(); } EditorUtility.SetDirty(script.gameObject); } } isDirty = true; Repaint(); } EditorGUILayout.Space(5); EditorGUILayout.LabelField("마지막 리프레시: " + System.DateTime.Now.ToString("HH:mm:ss"), EditorStyles.miniLabel); EditorGUILayout.EndVertical(); return; } // 전역 설정 옵션 DrawGlobalSettings(); EditorGUILayout.Space(); // 개별 캐릭터 설정 scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); var scripts = FindObjectsByType(FindObjectsSortMode.None); foreach (var script in scripts) { DrawRetargetingControls(script); } EditorGUILayout.EndScrollView(); } private void DrawGlobalSettings() { showGlobalSettings = EditorGUILayout.Foldout(showGlobalSettings, "전역 설정", true); if (showGlobalSettings) { EditorGUILayout.BeginVertical(EditorStyles.helpBox); // 전역 캘리브레이션 버튼 EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("모든 캐릭터 I-포즈 캘리브레이션")) { var scripts = FindObjectsByType(FindObjectsSortMode.None); foreach (var script in scripts) { // I-포즈 캘리브레이션 로직 } AssetDatabase.SaveAssets(); } if (GUILayout.Button("모든 캐릭터 캘리브레이션 초기화")) { var scripts = FindObjectsByType(FindObjectsSortMode.None); foreach (var script in scripts) { script.ResetPoseAndCache(); EditorUtility.SetDirty(script); } AssetDatabase.SaveAssets(); } EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); } } private void DrawRetargetingControls(CustomRetargetingScript script) { if (script == null) return; int instanceID = script.GetInstanceID(); // Dictionary 초기화 if (!weightSettingsFoldouts.ContainsKey(instanceID)) weightSettingsFoldouts[instanceID] = false; if (!hipsSettingsFoldouts.ContainsKey(instanceID)) hipsSettingsFoldouts[instanceID] = false; if (!kneeSettingsFoldouts.ContainsKey(instanceID)) kneeSettingsFoldouts[instanceID] = false; if (!fingerControlFoldouts.ContainsKey(instanceID)) fingerControlFoldouts[instanceID] = false; if (!fingerCopyFoldouts.ContainsKey(instanceID)) fingerCopyFoldouts[instanceID] = false; if (!motionSettingsFoldouts.ContainsKey(instanceID)) motionSettingsFoldouts[instanceID] = false; if (!floorSettingsFoldouts.ContainsKey(instanceID)) floorSettingsFoldouts[instanceID] = false; if (!scaleSettingsFoldouts.ContainsKey(instanceID)) scaleSettingsFoldouts[instanceID] = false; if (!leftHandFoldouts.ContainsKey(instanceID)) leftHandFoldouts[instanceID] = false; if (!rightHandFoldouts.ContainsKey(instanceID)) rightHandFoldouts[instanceID] = false; if (!headFoldouts.ContainsKey(instanceID)) headFoldouts[instanceID] = false; if (!propControlFoldouts.ContainsKey(instanceID)) propControlFoldouts[instanceID] = false; SerializedObject serializedObject = new SerializedObject(script); // 시작 시 Update 호출 serializedObject.Update(); EditorGUI.BeginChangeCheck(); EditorGUILayout.BeginVertical(EditorStyles.helpBox); // 헤더 부분 DrawHeader(script); GUI.backgroundColor = new Color(0.8f, 0.9f, 1f); // 가중치 설정 weightSettingsFoldouts[instanceID] = EditorGUILayout.Foldout(weightSettingsFoldouts[instanceID], "가중치 설정", true); if (weightSettingsFoldouts[instanceID]) { EditorGUI.indentLevel++; DrawWeightSettings(script); EditorGUI.indentLevel--; } // 힙 설정 hipsSettingsFoldouts[instanceID] = EditorGUILayout.Foldout(hipsSettingsFoldouts[instanceID], "힙 위치 보정", true); if (hipsSettingsFoldouts[instanceID]) { EditorGUI.indentLevel++; var hipsWeightProp = serializedObject.FindProperty("hipsWeight"); EditorGUILayout.PropertyField(hipsWeightProp, new GUIContent("힙 가중치")); EditorGUI.indentLevel--; } // 무릎 설정 kneeSettingsFoldouts[instanceID] = EditorGUILayout.Foldout(kneeSettingsFoldouts[instanceID], "무릎 위치 조정", true); if (kneeSettingsFoldouts[instanceID]) { EditorGUI.indentLevel++; var kneeFrontBackProp = serializedObject.FindProperty("kneeFrontBackWeight"); var kneeInOutProp = serializedObject.FindProperty("kneeInOutWeight"); EditorGUILayout.Slider(kneeFrontBackProp, -1f, 1f, new GUIContent("무릎 앞/뒤 가중치")); EditorGUILayout.Slider(kneeInOutProp, -1f, 1f, new GUIContent("무릎 안/밖 가중치")); EditorGUI.indentLevel--; } // 손가락 제어 설정 fingerControlFoldouts[instanceID] = EditorGUILayout.Foldout(fingerControlFoldouts[instanceID], "손가락 제어 설정", true); if (fingerControlFoldouts[instanceID]) { EditorGUI.indentLevel++; DrawFingerControls(script); EditorGUI.indentLevel--; } // 손가락 복제 설정 fingerCopyFoldouts[instanceID] = EditorGUILayout.Foldout(fingerCopyFoldouts[instanceID], "손가락 복제 설정", true); if (fingerCopyFoldouts[instanceID]) { EditorGUI.indentLevel++; DrawFingerCopySettings(script); EditorGUI.indentLevel--; } // 모션 설정 motionSettingsFoldouts[instanceID] = EditorGUILayout.Foldout(motionSettingsFoldouts[instanceID], "모션 설정", true); if (motionSettingsFoldouts[instanceID]) { EditorGUI.indentLevel++; DrawMotionSettings(serializedObject); EditorGUI.indentLevel--; } // 바닥 높이 설정 섹션 floorSettingsFoldouts[instanceID] = EditorGUILayout.Foldout(floorSettingsFoldouts[instanceID], "바닥 높이 설정", true); if (floorSettingsFoldouts[instanceID]) { EditorGUI.indentLevel++; DrawFloorSettings(serializedObject); EditorGUI.indentLevel--; } // 아바타 크기 설정 섹션 scaleSettingsFoldouts[instanceID] = EditorGUILayout.Foldout(scaleSettingsFoldouts[instanceID], "아바타 크기 설정", true); if (scaleSettingsFoldouts[instanceID]) { EditorGUI.indentLevel++; DrawScaleSettings(serializedObject); EditorGUI.indentLevel--; } // 프랍 컨트롤 설정 propControlFoldouts[instanceID] = EditorGUILayout.Foldout(propControlFoldouts[instanceID], "프랍 설정", true); if (propControlFoldouts[instanceID]) { EditorGUI.indentLevel++; DrawPropControls(script); EditorGUI.indentLevel--; } EditorGUILayout.EndVertical(); // 캘리브레이션 설정 EditorGUILayout.Space(10); EditorGUILayout.BeginVertical(EditorStyles.helpBox); DrawCalibrationControls(script); EditorGUILayout.EndVertical(); // 변경사항이 있을 경우 적용 if (EditorGUI.EndChangeCheck()) { // 수정된 속성 적용 serializedObject.ApplyModifiedProperties(); // 스크립트를 더티로 표시 EditorUtility.SetDirty(script); // isDirty 플래그 설정 isDirty = true; // 씬 뷰 갱신 SceneView.RepaintAll(); } } // 헤더 그리기 함수 (기존의 버튼 코드) private void DrawHeader(CustomRetargetingScript script) { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField($"캐릭터: {script.gameObject.name}", EditorStyles.boldLabel); GUILayout.FlexibleSpace(); var animator = script.GetComponent(); if (animator != null) { if (GUILayout.Button("전체", GUILayout.Width(50))) { Selection.activeGameObject = script.gameObject; if (SceneView.lastActiveSceneView != null) { SceneView.lastActiveSceneView.FrameSelected(); } } if (GUILayout.Button("머리", GUILayout.Width(50))) { var head = animator.GetBoneTransform(HumanBodyBones.Head); if (head != null) { Selection.activeGameObject = head.gameObject; if (SceneView.lastActiveSceneView != null) { SceneView.lastActiveSceneView.FrameSelected(); } } } if (GUILayout.Button("왼손", GUILayout.Width(50))) { var leftHand = animator.GetBoneTransform(HumanBodyBones.LeftHand); if (leftHand != null) { Selection.activeGameObject = leftHand.gameObject; if (SceneView.lastActiveSceneView != null) { SceneView.lastActiveSceneView.FrameSelected(); } } } if (GUILayout.Button("오른손", GUILayout.Width(50))) { var rightHand = animator.GetBoneTransform(HumanBodyBones.RightHand); if (rightHand != null) { Selection.activeGameObject = rightHand.gameObject; if (SceneView.lastActiveSceneView != null) { SceneView.lastActiveSceneView.FrameSelected(); } } } } EditorGUILayout.EndHorizontal(); } private void DrawCalibrationControls(CustomRetargetingScript script) { // 캘리브레이션 상태 표시 EditorGUILayout.LabelField(script.HasCachedSettings() ? "캘리브레이션 데이터가 저장되어 있습니다." : "저장된 캘리브레이션 데이터가 없습니다.", EditorStyles.boldLabel); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("I-포즈 캘리브레이션")) { script.I_PoseCalibration(); } if (GUILayout.Button("캘리브레이션 초기화")) { script.ResetPoseAndCache(); } EditorGUILayout.EndHorizontal(); } private void ResetWeights(CustomRetargetingScript script) { var limb = script.GetComponent(); if (limb != null) { // 기본 설정값으로 초기화 limb.maxDistance = 0.3f; limb.minDistance = 0.1f; limb.weightSmoothSpeed = 10f; limb.groundHipsMinHeight = 0.3f; limb.groundHipsMaxHeight = 0.7f; limb.chairHipsFixedGap = 0.1f; EditorUtility.SetDirty(limb); } } private void ResetAllSettings(CustomRetargetingScript script) { // 모든 컴포넌트의 설정을 기본값으로 초기화 ResetWeights(script); script.ResetPoseAndCache(); var shoulder = script.GetComponent(); if (shoulder != null) { shoulder.blendStrength = 2f; shoulder.maxHeightDifference = 0.3f; EditorUtility.SetDirty(shoulder); } } private void SavePreset() { if (string.IsNullOrEmpty(currentPresetName)) return; var preset = new RetargetingPreset(); if (retargetingScripts.Length > 0) { var script = retargetingScripts[0]; var shoulder = script.GetComponent(); var limb = script.GetComponent(); if (shoulder != null) { preset.shoulderBlendStrength = shoulder.blendStrength; preset.shoulderMaxHeightDiff = shoulder.maxHeightDifference; } if (limb != null) { preset.limbMaxDistance = limb.maxDistance; preset.limbMinDistance = limb.minDistance; preset.footHeightMinThreshold = limb.footHeightMinThreshold; preset.footHeightMaxThreshold = limb.footHeightMaxThreshold; preset.useFixedChairGap = limb.useFixedChairGap; preset.chairHipsFixedGap = limb.chairHipsFixedGap; } presets[currentPresetName] = preset; EditorUtility.SetDirty(this); } } private void LoadPreset() { if (!presets.TryGetValue(currentPresetName, out RetargetingPreset preset)) return; foreach (var script in retargetingScripts) { var shoulder = script.GetComponent(); var limb = script.GetComponent(); if (shoulder != null) { shoulder.blendStrength = preset.shoulderBlendStrength; shoulder.maxHeightDifference = preset.shoulderMaxHeightDiff; } if (limb != null) { limb.maxDistance = preset.limbMaxDistance; limb.minDistance = preset.limbMinDistance; limb.footHeightMinThreshold = preset.footHeightMinThreshold; limb.footHeightMaxThreshold = preset.footHeightMaxThreshold; limb.useFixedChairGap = preset.useFixedChairGap; limb.chairHipsFixedGap = preset.chairHipsFixedGap; } EditorUtility.SetDirty(script); } } private bool DrawHandControls(string label, string prefix, bool foldout, SerializedObject serializedController) { EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.BeginHorizontal(); EditorGUILayout.BeginHorizontal(GUILayout.Width(200)); bool newFoldout = EditorGUILayout.Foldout(foldout, label, true); SerializedProperty handEnabledProp = serializedController.FindProperty($"{prefix}HandEnabled"); bool handEnabled = EditorGUILayout.Toggle("제어 활성화", handEnabledProp.boolValue); if (handEnabled != handEnabledProp.boolValue) { handEnabledProp.boolValue = handEnabled; if (handEnabled && !((FingerShapedController)serializedController.targetObject).enabled) { ((FingerShapedController)serializedController.targetObject).enabled = true; } } EditorGUILayout.EndHorizontal(); if (GUILayout.Button("초기화", GUILayout.Width(60))) { ResetHandValues(prefix, serializedController); } EditorGUILayout.EndHorizontal(); if (newFoldout && handEnabledProp.boolValue) { EditorGUILayout.Space(SPACING); DrawFingerSliders(prefix, serializedController); EditorGUILayout.Space(SPACING); DrawSpreadSlider(prefix, serializedController); } EditorGUILayout.EndVertical(); return newFoldout; } private void DrawFingerControls(CustomRetargetingScript script) { var fingerController = script.GetComponent(); if (fingerController == null) { EditorGUILayout.HelpBox("FingerShapedController가 없습니다.", MessageType.Warning); if (GUILayout.Button("FingerShapedController 추가")) { fingerController = script.gameObject.AddComponent(); EditorUtility.SetDirty(script.gameObject); } return; } SerializedObject serializedController = new SerializedObject(fingerController); serializedController.Update(); // 활성화/비활성화 토글 EditorGUI.BeginChangeCheck(); bool isEnabled = EditorGUILayout.Toggle("손가락 제어 활성화", fingerController.enabled); if (EditorGUI.EndChangeCheck()) { fingerController.enabled = isEnabled; EditorUtility.SetDirty(fingerController); } if (!fingerController.enabled) return; int instanceID = fingerController.GetInstanceID(); // Dictionary 초기화 확인 if (!leftHandFoldouts.ContainsKey(instanceID)) leftHandFoldouts[instanceID] = true; if (!rightHandFoldouts.ContainsKey(instanceID)) rightHandFoldouts[instanceID] = true; EditorGUILayout.BeginVertical(EditorStyles.helpBox); // 왼손 컨트롤 leftHandFoldouts[instanceID] = DrawHandControls("왼손", "left", leftHandFoldouts[instanceID], serializedController); EditorGUILayout.Space(SPACING); // 오른손 컨트롤 rightHandFoldouts[instanceID] = DrawHandControls("오른손", "right", rightHandFoldouts[instanceID], serializedController); EditorGUILayout.Space(SPACING); // 프리셋 버튼들 DrawFingerPresetButtons(fingerController); EditorGUILayout.EndVertical(); serializedController.ApplyModifiedProperties(); } private void DrawFingerCopySettings(CustomRetargetingScript script) { if (script == null) return; SerializedObject serializedObject = new SerializedObject(script); serializedObject.Update(); EditorGUILayout.BeginVertical(EditorStyles.helpBox); // CustomRetargetingScript의 fingerCopyMode 설정을 표시 var fingerCopyModeProp = serializedObject.FindProperty("fingerCopyMode"); if (fingerCopyModeProp != null) { EditorGUILayout.PropertyField(fingerCopyModeProp, new GUIContent("복제 방식", "손가락 포즈를 복제하는 방식을 선택합니다.")); if (GUI.changed) { serializedObject.ApplyModifiedProperties(); EditorUtility.SetDirty(script); } } else { EditorGUILayout.HelpBox("fingerCopyMode 프로퍼티를 찾을 수 없습니다.", MessageType.Warning); } EditorGUILayout.EndVertical(); } private void DrawFingerSliders(string prefix, SerializedObject serializedController) { string[] fingerNames = { "Thumb", "Index", "Middle", "Ring", "Pinky" }; float totalWidth = (SLIDER_WIDTH + SPACING) * fingerNames.Length; EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); EditorGUILayout.BeginHorizontal(GUILayout.Width(totalWidth)); for (int i = 0; i < fingerNames.Length; i++) { EditorGUILayout.BeginVertical(GUILayout.Width(SLIDER_WIDTH)); // 손가락 이름 GUILayout.Label(GetKoreanFingerName(fingerNames[i]), EditorStyles.centeredGreyMiniLabel, GUILayout.Width(SLIDER_WIDTH)); // 값 표시 SerializedProperty prop = serializedController.FindProperty($"{prefix}{fingerNames[i]}Curl"); GUILayout.Label(prop.floatValue.ToString("F1"), EditorStyles.centeredGreyMiniLabel, GUILayout.Width(SLIDER_WIDTH)); // 세로 슬라이더를 중앙에 배치 EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); prop.floatValue = GUILayout.VerticalSlider( prop.floatValue, 1f, -1f, GUILayout.Width(SLIDER_WIDTH), GUILayout.Height(SLIDER_HEIGHT) ); GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); if (i < fingerNames.Length - 1) GUILayout.Space(SPACING); } EditorGUILayout.EndHorizontal(); GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); } private string GetKoreanFingerName(string englishName) { switch (englishName) { case "Thumb": return "엄지"; case "Index": return "검지"; case "Middle": return "중지"; case "Ring": return "약지"; case "Pinky": return "새끼"; default: return englishName; } } private void DrawSpreadSlider(string prefix, SerializedObject serializedController) { SerializedProperty spreadProp = serializedController.FindProperty($"{prefix}SpreadFingers"); EditorGUILayout.BeginHorizontal(); GUILayout.Space(15); EditorGUILayout.LabelField("벌리기", GUILayout.Width(50)); spreadProp.floatValue = EditorGUILayout.Slider(spreadProp.floatValue, -1f, 1f); GUILayout.Space(15); EditorGUILayout.EndHorizontal(); } private void DrawFingerPresetButtons(FingerShapedController controller) { EditorGUILayout.LabelField("손 모양 프리셋", EditorStyles.boldLabel); string[,] presets = { {"가위", "바위", "보"}, {"브이", "검지", "초기화"} }; for (int row = 0; row < 2; row++) { EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); for (int col = 0; col < 3; col++) { if (GUILayout.Button(presets[row, col], GUILayout.Height(30), GUILayout.Width(100))) { ApplyPreset(controller, presets[row, col]); } if (col < 2) { GUILayout.Space(10); } } GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); if (row < 1) { GUILayout.Space(5); } } } private void ResetHandValues(string prefix, SerializedObject serializedController) { string[] props = { "ThumbCurl", "IndexCurl", "MiddleCurl", "RingCurl", "PinkyCurl", "SpreadFingers" }; foreach (var prop in props) { serializedController.FindProperty($"{prefix}{prop}").floatValue = 0f; } serializedController.ApplyModifiedProperties(); } private void ApplyPreset(FingerShapedController controller, string presetName) { // 프리셋 적용 시 양손 모두 활성화 if (!controller.enabled) { controller.enabled = true; } switch (presetName) { case "가위": SetPreset(controller, 1f, 1f, -1f, -1f, -1f, 0.3f); break; case "바위": SetPreset(controller, -1f, -1f, -1f, -1f, -1f, 0f); break; case "보": SetPreset(controller, 1f, 1f, 1f, 1f, 1f, 1f); break; case "브이": SetPreset(controller, -1f, 1f, 1f, -1f, -1f, 1f); break; case "검지": SetPreset(controller, -1f, 1f, -1f, -1f, -1f, 0f); break; case "초기화": SetPreset(controller, 0.8f, 0.8f, 0.8f, 0.8f, 0.8f, 0.8f); break; } } private void SetPreset(FingerShapedController controller, float thumb, float index, float middle, float ring, float pinky, float spread) { if (controller.leftHandEnabled) { controller.leftThumbCurl = thumb; controller.leftIndexCurl = index; controller.leftMiddleCurl = middle; controller.leftRingCurl = ring; controller.leftPinkyCurl = pinky; controller.leftSpreadFingers = spread; } if (controller.rightHandEnabled) { controller.rightThumbCurl = thumb; controller.rightIndexCurl = index; controller.rightMiddleCurl = middle; controller.rightRingCurl = ring; controller.rightPinkyCurl = pinky; controller.rightSpreadFingers = spread; } EditorUtility.SetDirty(controller); } private void DrawFingerSettings(SerializedObject serializedObject) { var fingerCopyModeProp = serializedObject.FindProperty("fingerCopyMode"); var useFingerRoughProp = serializedObject.FindProperty("useFingerRoughMotion"); var fingerRoughnessProp = serializedObject.FindProperty("fingerRoughness"); EditorGUILayout.PropertyField(fingerCopyModeProp, new GUIContent("복제 방식")); EditorGUILayout.PropertyField(useFingerRoughProp, new GUIContent("러프 모션 사용")); if (useFingerRoughProp.boolValue) { EditorGUILayout.PropertyField(fingerRoughnessProp, new GUIContent("러프니스")); } } private void DrawMotionSettings(SerializedObject serializedObject) { serializedObject.Update(); var useMotionFilterProp = serializedObject.FindProperty("useMotionFilter"); var filterBufferSizeProp = serializedObject.FindProperty("filterBufferSize"); var useBodyRoughProp = serializedObject.FindProperty("useBodyRoughMotion"); var bodyRoughnessProp = serializedObject.FindProperty("bodyRoughness"); EditorGUI.BeginChangeCheck(); EditorGUILayout.PropertyField(useMotionFilterProp, new GUIContent("모션 필터 사용")); if (useMotionFilterProp.boolValue) { EditorGUILayout.PropertyField(filterBufferSizeProp, new GUIContent("필터 버퍼 크기")); } EditorGUILayout.PropertyField(useBodyRoughProp, new GUIContent("몸 러프 모션 사용")); if (useBodyRoughProp.boolValue) { EditorGUILayout.PropertyField(bodyRoughnessProp, new GUIContent("몸 러프니스")); } if (EditorGUI.EndChangeCheck()) { serializedObject.ApplyModifiedProperties(); EditorUtility.SetDirty(serializedObject.targetObject); SceneView.RepaintAll(); } } private void DrawWeightSettings(CustomRetargetingScript script) { var limb = script.GetComponent(); if (limb != null) { var serializedLimb = new SerializedObject(limb); serializedLimb.Update(); EditorGUI.BeginChangeCheck(); // 거리 기반 가중치 설정 EditorGUILayout.LabelField("손과 프랍과의 범위 (가중치 1 -> 0)"); EditorGUILayout.BeginHorizontal(); var minDistance = serializedLimb.FindProperty("minDistance"); var maxDistance = serializedLimb.FindProperty("maxDistance"); float minVal = minDistance.floatValue; float maxVal = maxDistance.floatValue; minVal = EditorGUILayout.FloatField(minVal, GUILayout.Width(50)); EditorGUILayout.MinMaxSlider(ref minVal, ref maxVal, 0f, 1f); maxVal = EditorGUILayout.FloatField(maxVal, GUILayout.Width(50)); minDistance.floatValue = minVal; maxDistance.floatValue = maxVal; EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(5); // 허리 거리 범위 설정 EditorGUILayout.LabelField("의자와 허리 거리 범위(가중치 1 -> 0)"); EditorGUILayout.BeginHorizontal(); var hipsMinDistance = serializedLimb.FindProperty("hipsMinDistance"); var hipsMaxDistance = serializedLimb.FindProperty("hipsMaxDistance"); float hipsMin = hipsMinDistance.floatValue; float hipsMax = hipsMaxDistance.floatValue; hipsMin = EditorGUILayout.FloatField(hipsMin, GUILayout.Width(50)); EditorGUILayout.MinMaxSlider(ref hipsMin, ref hipsMax, 0f, 1f); hipsMax = EditorGUILayout.FloatField(hipsMax, GUILayout.Width(50)); hipsMinDistance.floatValue = hipsMin; hipsMaxDistance.floatValue = hipsMax; EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(5); // 의자 허리 고정 간격 설정 추가 var useFixedChairGap = serializedLimb.FindProperty("useFixedChairGap"); var chairHipsFixedGap = serializedLimb.FindProperty("chairHipsFixedGap"); EditorGUILayout.PropertyField(useFixedChairGap, new GUIContent("의자 고정 간격 사용")); if (useFixedChairGap.boolValue) { EditorGUILayout.Slider(chairHipsFixedGap, 0f, 0.3f, new GUIContent("의자-허리 고정 간격")); } EditorGUILayout.Space(5); // 바닥 기준 히프 높이 범위 EditorGUILayout.LabelField("바닥과 허리 높이에 의한 블렌딩 (가중치 0 -> 1)"); EditorGUILayout.BeginHorizontal(); var groundHipsMin = serializedLimb.FindProperty("groundHipsMinHeight"); var groundHipsMax = serializedLimb.FindProperty("groundHipsMaxHeight"); float groundMin = groundHipsMin.floatValue; float groundMax = groundHipsMax.floatValue; groundMin = EditorGUILayout.FloatField(groundMin, GUILayout.Width(50)); EditorGUILayout.MinMaxSlider(ref groundMin, ref groundMax, 0f, 2f); groundMax = EditorGUILayout.FloatField(groundMax, GUILayout.Width(50)); groundHipsMin.floatValue = groundMin; groundHipsMax.floatValue = groundMax; EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(5); // 발 높이 기반 가중치 설정 추가 EditorGUILayout.LabelField("지면으로부터 발의 범위에 의한 IK 블렌딩 (가중치 1 -> 0)"); EditorGUILayout.BeginHorizontal(); var footHeightMin = serializedLimb.FindProperty("footHeightMinThreshold"); var footHeightMax = serializedLimb.FindProperty("footHeightMaxThreshold"); float footMin = footHeightMin.floatValue; float footMax = footHeightMax.floatValue; footMin = EditorGUILayout.FloatField(footMin, GUILayout.Width(50)); EditorGUILayout.MinMaxSlider(ref footMin, ref footMax, 0.1f, 1f); footMax = EditorGUILayout.FloatField(footMax, GUILayout.Width(50)); footHeightMin.floatValue = footMin; footHeightMax.floatValue = footMax; EditorGUILayout.EndHorizontal(); // 가중치 보간 설정 var weightSmoothSpeed = serializedLimb.FindProperty("weightSmoothSpeed"); EditorGUILayout.PropertyField(weightSmoothSpeed, new GUIContent("가중치 변화 속도")); if (EditorGUI.EndChangeCheck()) { serializedLimb.ApplyModifiedProperties(); EditorUtility.SetDirty(limb); SceneView.RepaintAll(); } } } // 바닥 높이 설정 그리기 함수 private void DrawFloorSettings(SerializedObject serializedObject) { EditorGUI.BeginChangeCheck(); var floorHeightProp = serializedObject.FindProperty("floorHeight"); EditorGUILayout.PropertyField(floorHeightProp, new GUIContent("바닥 높이 (-1 ~ 1)")); if (EditorGUI.EndChangeCheck()) { serializedObject.ApplyModifiedProperties(); } } // 크기 설정 UI 그리기 함수 private void DrawScaleSettings(SerializedObject serializedObject) { EditorGUI.BeginChangeCheck(); var avatarScaleProp = serializedObject.FindProperty("avatarScale"); EditorGUILayout.PropertyField(avatarScaleProp, new GUIContent("아바타 크기")); if (EditorGUI.EndChangeCheck()) { serializedObject.ApplyModifiedProperties(); } } // 창이 포커스를 얻거나 잃을 때 리페인트 private void OnFocus() { Repaint(); } private void OnLostFocus() { Repaint(); } // 에디터 업데이트 시 리페인트 private void OnInspectorUpdate() { Repaint(); } private void DrawPropControls(CustomRetargetingScript script) { var propController = script.GetComponent(); if (propController == null) { EditorGUILayout.HelpBox("PropLocationController가 없습니다.", MessageType.Warning); if (GUILayout.Button("PropLocationController 추가")) { propController = script.gameObject.AddComponent(); EditorUtility.SetDirty(script.gameObject); } return; } EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.LabelField("오프셋 조정", EditorStyles.boldLabel); // 왼손 오프셋 Transform leftOffset = propController.GetLeftHandOffset(); if (leftOffset != null) { bool showLeftHand = EditorGUILayout.Foldout(leftHandFoldouts.ContainsKey(script.GetInstanceID()) ? leftHandFoldouts[script.GetInstanceID()] : true, "왼손 오프셋"); leftHandFoldouts[script.GetInstanceID()] = showLeftHand; if (showLeftHand) { EditorGUI.indentLevel++; EditorGUI.BeginChangeCheck(); Vector3 leftPos = EditorGUILayout.Vector3Field("위치", leftOffset.localPosition); Vector3 leftRot = EditorGUILayout.Vector3Field("회전", leftOffset.localRotation.eulerAngles); if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(leftOffset, "Update Left Hand Offset"); leftOffset.localPosition = leftPos; leftOffset.localRotation = Quaternion.Euler(leftRot); EditorUtility.SetDirty(leftOffset); } EditorGUI.indentLevel--; } } // 오른손 오프셋 Transform rightOffset = propController.GetRightHandOffset(); if (rightOffset != null) { bool showRightHand = EditorGUILayout.Foldout(rightHandFoldouts.ContainsKey(script.GetInstanceID()) ? rightHandFoldouts[script.GetInstanceID()] : true, "오른손 오프셋"); rightHandFoldouts[script.GetInstanceID()] = showRightHand; if (showRightHand) { EditorGUI.indentLevel++; EditorGUI.BeginChangeCheck(); Vector3 rightPos = EditorGUILayout.Vector3Field("위치", rightOffset.localPosition); Vector3 rightRot = EditorGUILayout.Vector3Field("회전", rightOffset.localRotation.eulerAngles); if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(rightOffset, "Update Right Hand Offset"); rightOffset.localPosition = rightPos; rightOffset.localRotation = Quaternion.Euler(rightRot); EditorUtility.SetDirty(rightOffset); } EditorGUI.indentLevel--; } } // 머리 오프셋 Transform headOffset = propController.GetHeadOffset(); if (headOffset != null) { bool showHead = EditorGUILayout.Foldout(headFoldouts.ContainsKey(script.GetInstanceID()) ? headFoldouts[script.GetInstanceID()] : true, "머리 오프셋"); headFoldouts[script.GetInstanceID()] = showHead; if (showHead) { EditorGUI.indentLevel++; EditorGUI.BeginChangeCheck(); Vector3 headPos = EditorGUILayout.Vector3Field("위치", headOffset.localPosition); Vector3 headRot = EditorGUILayout.Vector3Field("회전", headOffset.localRotation.eulerAngles); if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(headOffset, "Update Head Offset"); headOffset.localPosition = headPos; headOffset.localRotation = Quaternion.Euler(headRot); EditorUtility.SetDirty(headOffset); } EditorGUI.indentLevel--; } } // 프랍 목록 EditorGUILayout.Space(10); EditorGUILayout.LabelField("부착된 프랍 목록", EditorStyles.boldLabel); // 머리 프랍 EditorGUILayout.BeginVertical("box"); EditorGUILayout.LabelField("머리 프랍", EditorStyles.boldLabel); GameObject[] headProps = propController.GetHeadProps(); DrawPropList(headProps); EditorGUILayout.EndVertical(); // 왼손 프랍 EditorGUILayout.BeginVertical("box"); EditorGUILayout.LabelField("왼손 프랍", EditorStyles.boldLabel); GameObject[] leftHandProps = propController.GetLeftHandProps(); DrawPropList(leftHandProps); EditorGUILayout.EndVertical(); // 오른손 프랍 EditorGUILayout.BeginVertical("box"); EditorGUILayout.LabelField("오른손 프랍", EditorStyles.boldLabel); GameObject[] rightHandProps = propController.GetRightHandProps(); DrawPropList(rightHandProps); EditorGUILayout.EndVertical(); // 프랍 이동 버튼 EditorGUILayout.Space(); EditorGUILayout.LabelField("프랍 위치 이동", EditorStyles.boldLabel); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("머리로 이동", GUILayout.Height(30))) { propController.MoveToHead(); } if (GUILayout.Button("왼손으로 이동", GUILayout.Height(30))) { propController.MoveToLeftHand(); } if (GUILayout.Button("오른손으로 이동", GUILayout.Height(30))) { propController.MoveToRightHand(); } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); if (GUILayout.Button("프랍 해제", GUILayout.Height(30))) { Undo.RecordObject(Selection.activeGameObject?.transform, "Detach Prop"); propController.DetachProp(); } EditorGUILayout.EndVertical(); } private void DrawPropList(GameObject[] props) { if (props.Length > 0) { foreach (GameObject prop in props) { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField(prop.name); if (GUILayout.Button("선택", GUILayout.Width(60))) { Selection.activeGameObject = prop; } EditorGUILayout.EndHorizontal(); } } else { EditorGUILayout.LabelField("부착된 프랍 없음"); } } } [System.Serializable] public class RetargetingPreset { public int ikIterations; public float ikDeltaThreshold; public float shoulderBlendStrength; public float shoulderMaxHeightDiff; public float limbMaxDistance; public float limbMinDistance; public float footHeightMinThreshold; public float footHeightMaxThreshold; public bool useFixedChairGap; public float chairHipsFixedGap; // ... 기타 필요한 설정들 }