Streamingle_URP/Assets/Scripts/KindRetargeting/Editor/RetargetingControlWindow.cs

1478 lines
57 KiB
C#

using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using KindRetargeting;
using KindRetargeting.EnumsList;
public class RetargetingControlWindow : EditorWindow
{
private Vector2 scrollPosition;
private bool showGlobalSettings = true;
private CustomRetargetingScript[] retargetingScripts;
private Dictionary<string, RetargetingPreset> presets = new Dictionary<string, RetargetingPreset>();
private string currentPresetName = "";
// 각 객체별 foldout 상태를 저장할 Dictionary들
private Dictionary<int, bool> weightSettingsFoldouts = new Dictionary<int, bool>();
private Dictionary<int, bool> hipsSettingsFoldouts = new Dictionary<int, bool>();
private Dictionary<int, bool> kneeSettingsFoldouts = new Dictionary<int, bool>();
private Dictionary<int, bool> fingerControlFoldouts = new Dictionary<int, bool>();
private Dictionary<int, bool> fingerCopyFoldouts = new Dictionary<int, bool>();
private Dictionary<int, bool> motionSettingsFoldouts = new Dictionary<int, bool>();
private Dictionary<int, bool> floorSettingsFoldouts = new Dictionary<int, bool>();
private Dictionary<int, bool> scaleSettingsFoldouts = new Dictionary<int, bool>();
private Dictionary<int, bool> footSettingsFoldouts = new Dictionary<int, bool>();
private Dictionary<int, bool> leftHandFoldouts = new Dictionary<int, bool>();
private Dictionary<int, bool> rightHandFoldouts = new Dictionary<int, bool>();
private Dictionary<int, bool> headFoldouts = new Dictionary<int, bool>();
private Dictionary<int, bool> propControlFoldouts = new Dictionary<int, bool>();
private Dictionary<int, bool> headRotationFoldouts = new Dictionary<int, bool>();
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<RetargetingControlWindow>("리타게팅 컨트롤");
}
private void OnEnable()
{
// 씬에서 모든 리타게팅 스크립트 찾기
retargetingScripts = FindObjectsByType<CustomRetargetingScript>(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<CustomRetargetingScript>(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<CustomRetargetingScript>(FindObjectsSortMode.None);
// 각 리타게팅 스크립트에 대해 필요한 컴포넌트들 체크
foreach (var script in retargetingScripts)
{
if (script != null)
{
// LimbWeightController가 없다면 추가
if (script.GetComponent<LimbWeightController>() == null)
{
script.gameObject.AddComponent<LimbWeightController>();
}
// FingerShapedController가 없다면 추가
if (script.GetComponent<FingerShapedController>() == null)
{
script.gameObject.AddComponent<FingerShapedController>();
}
// PropLocationController가 없다면 추가
if (script.GetComponent<PropLocationController>() == null)
{
script.gameObject.AddComponent<PropLocationController>();
}
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<CustomRetargetingScript>(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<CustomRetargetingScript>(FindObjectsSortMode.None);
foreach (var script in scripts)
{
// I-포즈 캘리브레이션 로직
}
AssetDatabase.SaveAssets();
}
if (GUILayout.Button("모든 캐릭터 캘리브레이션 초기화"))
{
var scripts = FindObjectsByType<CustomRetargetingScript>(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 (!footSettingsFoldouts.ContainsKey(instanceID)) footSettingsFoldouts[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;
if (!headRotationFoldouts.ContainsKey(instanceID)) headRotationFoldouts[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 hipsOffsetXProp = serializedObject.FindProperty("hipsOffsetX");
var hipsOffsetYProp = serializedObject.FindProperty("hipsOffsetY");
var hipsOffsetZProp = serializedObject.FindProperty("hipsOffsetZ");
// 축 매핑 정보 표시
var normalizerProp = serializedObject.FindProperty("debugAxisNormalizer");
if (normalizerProp != null && Application.isPlaying)
{
Vector3 mapping = normalizerProp.vector3Value;
if (mapping != Vector3.one) // 초기화되었을 때만 표시
{
// mapping: 1=X, 2=Y, 3=Z, 부호는 방향
string GetAxis(float v) => Mathf.RoundToInt(Mathf.Abs(v)) switch
{
1 => v > 0 ? "+X" : "-X",
2 => v > 0 ? "+Y" : "-Y",
3 => v > 0 ? "+Z" : "-Z",
_ => "?"
};
EditorGUILayout.LabelField($"축 매핑: 좌우→{GetAxis(mapping.x)} 상하→{GetAxis(mapping.y)} 앞뒤→{GetAxis(mapping.z)}",
EditorStyles.miniLabel);
}
}
EditorGUILayout.Slider(hipsOffsetXProp, -1f, 1f,
new GUIContent("← 좌우 →", "캐릭터 기준 왼쪽(-) / 오른쪽(+)"));
EditorGUILayout.Slider(hipsOffsetYProp, -1f, 1f,
new GUIContent("↓ 상하 ↑", "캐릭터 기준 아래(-) / 위(+)"));
EditorGUILayout.Slider(hipsOffsetZProp, -1f, 1f,
new GUIContent("← 앞뒤 →", "캐릭터 기준 뒤(-) / 앞(+)"));
// 의자 앉기 높이 설정 (월드 Y 기준)
EditorGUILayout.Space(5);
var limb = script.GetComponent<LimbWeightController>();
if (limb != null)
{
var serializedLimb = new SerializedObject(limb);
serializedLimb.Update();
var chairSeatHeightOffset = serializedLimb.FindProperty("chairSeatHeightOffset");
EditorGUI.BeginChangeCheck();
EditorGUILayout.Slider(chairSeatHeightOffset, -1f, 1f,
new GUIContent("의자 앉기 높이", "의자에 앉을 때 엉덩이 높이 조정 (월드 Y 기준)"));
if (EditorGUI.EndChangeCheck())
{
serializedLimb.ApplyModifiedProperties();
EditorUtility.SetDirty(limb);
}
}
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--;
}
// 발 IK 위치 조정
footSettingsFoldouts[instanceID] = EditorGUILayout.Foldout(footSettingsFoldouts[instanceID], "발 IK 위치 조정", true);
if (footSettingsFoldouts[instanceID])
{
EditorGUI.indentLevel++;
var footFrontBackProp = serializedObject.FindProperty("footFrontBackOffset");
var footInOutProp = serializedObject.FindProperty("footInOutOffset");
EditorGUILayout.Slider(footFrontBackProp, -1f, 1f, new GUIContent("발 앞/뒤 오프셋", "+: 앞으로, -: 뒤로"));
EditorGUILayout.Slider(footInOutProp, -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--;
}
// 머리 회전 오프셋 설정 섹션
headRotationFoldouts[instanceID] = EditorGUILayout.Foldout(headRotationFoldouts[instanceID], "머리 회전 오프셋", true);
if (headRotationFoldouts[instanceID])
{
EditorGUI.indentLevel++;
DrawHeadRotationSettings(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<Animator>();
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<LimbWeightController>();
if (limb != null)
{
// 기본 설정값으로 초기화
limb.maxDistance = 0.3f;
limb.minDistance = 0.1f;
limb.weightSmoothSpeed = 10f;
limb.groundHipsMinHeight = 0.3f;
limb.groundHipsMaxHeight = 0.7f;
EditorUtility.SetDirty(limb);
}
}
private void ResetAllSettings(CustomRetargetingScript script)
{
// 모든 컴포넌트의 설정을 기본값으로 초기화
ResetWeights(script);
script.ResetPoseAndCache();
var shoulder = script.GetComponent<ShoulderCorrectionFunction>();
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<ShoulderCorrectionFunction>();
var limb = script.GetComponent<LimbWeightController>();
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;
}
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<ShoulderCorrectionFunction>();
var limb = script.GetComponent<LimbWeightController>();
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;
}
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<FingerShapedController>();
if (fingerController == null)
{
EditorGUILayout.HelpBox("FingerShapedController가 없습니다.", MessageType.Warning);
if (GUILayout.Button("FingerShapedController 추가"))
{
fingerController = script.gameObject.AddComponent<FingerShapedController>();
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);
}
// Mingle 모드일 때 캘리브레이션 버튼 표시
if (fingerCopyModeProp.enumValueIndex == (int)FingerCopyMode.Mingle)
{
GUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("Mingle 캘리브레이션", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"Mingle 모드는 소스 아바타의 손가락 회전 범위를 캘리브레이션하여 타겟에 적용합니다.\n" +
"1. 손가락을 완전히 펼친 상태에서 '펼침 기록' 클릭\n" +
"2. 손가락을 완전히 모은(주먹) 상태에서 '모음 기록' 클릭",
MessageType.Info);
// 자동 캘리브레이션 진행 중일 때
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();
}
}
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<LimbWeightController>();
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);
// 바닥 기준 히프 높이 범위
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("아바타 크기"));
var headScaleProp = serializedObject.FindProperty("headScale");
EditorGUILayout.PropertyField(headScaleProp, new GUIContent("머리 크기"));
if (EditorGUI.EndChangeCheck())
{
serializedObject.ApplyModifiedProperties();
}
}
// 머리 회전 오프셋 UI 그리기 함수
private void DrawHeadRotationSettings(SerializedObject serializedObject)
{
EditorGUI.BeginChangeCheck();
var headRotationXProp = serializedObject.FindProperty("headRotationOffsetX");
var headRotationYProp = serializedObject.FindProperty("headRotationOffsetY");
var headRotationZProp = serializedObject.FindProperty("headRotationOffsetZ");
EditorGUILayout.Slider(headRotationXProp, -180f, 180f,
new GUIContent("X (Roll) - 좌우 기울기", "머리를 좌우로 기울입니다"));
EditorGUILayout.Slider(headRotationYProp, -180f, 180f,
new GUIContent("Y (Yaw) - 좌우 회전", "머리를 좌우로 회전합니다"));
EditorGUILayout.Slider(headRotationZProp, -180f, 180f,
new GUIContent("Z (Pitch) - 상하 회전", "머리를 상하로 회전합니다"));
// 초기화 버튼과 정면 캘리브레이션 버튼을 가로로 배치
EditorGUILayout.Space(5);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("회전 초기화", GUILayout.Height(25)))
{
headRotationXProp.floatValue = 0f;
headRotationYProp.floatValue = 0f;
headRotationZProp.floatValue = 0f;
serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(serializedObject.targetObject);
}
// 정면 캘리브레이션 버튼
GUI.enabled = Application.isPlaying;
if (GUILayout.Button("정면 캘리브레이션", GUILayout.Height(25)))
{
CalibrateHeadToForward(serializedObject, headRotationXProp, headRotationYProp, headRotationZProp);
}
GUI.enabled = true;
EditorGUILayout.EndHorizontal();
if (!Application.isPlaying)
{
EditorGUILayout.HelpBox("정면 캘리브레이션은 플레이 모드에서만 사용 가능합니다.", MessageType.Info);
}
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<PropLocationController>();
if (propController == null)
{
EditorGUILayout.HelpBox("PropLocationController가 없습니다.", MessageType.Warning);
if (GUILayout.Button("PropLocationController 추가"))
{
propController = script.gameObject.AddComponent<PropLocationController>();
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("부착된 프랍 없음");
}
}
/// <summary>
/// T-포즈에서 캐싱한 머리 정면 방향과 현재 머리 방향을 비교하여 오프셋을 계산합니다.
/// 사용법: 모캡 배우가 정면을 바라볼 때 버튼을 누르세요.
/// </summary>
private void CalibrateHeadToForward(SerializedObject serializedObject,
SerializedProperty headRotationXProp,
SerializedProperty headRotationYProp,
SerializedProperty headRotationZProp)
{
CustomRetargetingScript script = serializedObject.targetObject as CustomRetargetingScript;
if (script == null)
{
Debug.LogWarning("CustomRetargetingScript가 없습니다.");
return;
}
Animator targetAnimator = script.GetComponent<Animator>();
if (targetAnimator == null)
{
Debug.LogWarning("타겟 Animator가 없습니다.");
return;
}
Transform targetHead = targetAnimator.GetBoneTransform(HumanBodyBones.Head);
if (targetHead == null)
{
Debug.LogWarning("타겟 아바타의 Head 본을 찾을 수 없습니다.");
return;
}
// T-포즈 머리 방향이 캐싱되어 있는지 확인
Vector3 tPoseForward = script.tPoseHeadForward;
Vector3 tPoseUp = script.tPoseHeadUp;
if (tPoseForward.sqrMagnitude < 0.001f)
{
Debug.LogWarning("T-포즈 머리 방향이 캐싱되지 않았습니다. 게임을 재시작해주세요.");
return;
}
// 1. 기존 오프셋 제거하여 원본 로컬 회전 계산
float prevX = headRotationXProp.floatValue;
float prevY = headRotationYProp.floatValue;
float prevZ = headRotationZProp.floatValue;
Quaternion currentLocalRot = targetHead.localRotation;
Quaternion prevOffset = Quaternion.Euler(prevX, prevY, prevZ);
Quaternion baseLocalRot = currentLocalRot * Quaternion.Inverse(prevOffset);
// 2. 부모 월드 회전
Transform headParent = targetHead.parent;
Quaternion parentWorldRot = headParent != null ? headParent.rotation : Quaternion.identity;
// 3. 오프셋 제거된 원본 상태의 월드 회전
Quaternion baseWorldRot = parentWorldRot * baseLocalRot;
// 4. 오프셋 제거된 원본 상태에서의 머리 forward/up 방향 계산
Vector3 currentHeadForward = baseWorldRot * Vector3.forward;
Vector3 currentHeadUp = baseWorldRot * Vector3.up;
// 5. 현재 머리 forward → T-포즈 forward로의 회전 계산
Quaternion forwardCorrection = Quaternion.FromToRotation(currentHeadForward, tPoseForward);
// 6. forward 보정 후 up 방향도 맞춰야 함
Vector3 correctedUp = forwardCorrection * currentHeadUp;
// T-포즈 up과의 차이 (roll)
float rollAngle = Vector3.SignedAngle(correctedUp, tPoseUp, tPoseForward);
Quaternion rollCorrection = Quaternion.AngleAxis(rollAngle, tPoseForward);
// 7. 전체 월드 보정 회전
Quaternion worldCorrection = rollCorrection * forwardCorrection;
// 8. 보정된 월드 회전
Quaternion correctedWorldRot = worldCorrection * baseWorldRot;
// 9. 보정된 로컬 회전
Quaternion correctedLocalRot = Quaternion.Inverse(parentWorldRot) * correctedWorldRot;
// 10. 오프셋 계산: baseLocalRot * offset = correctedLocalRot
Quaternion offsetQuat = Quaternion.Inverse(baseLocalRot) * correctedLocalRot;
// 11. 오일러로 변환
Vector3 euler = offsetQuat.eulerAngles;
if (euler.x > 180f) euler.x -= 360f;
if (euler.y > 180f) euler.y -= 360f;
if (euler.z > 180f) euler.z -= 360f;
// 12. 적용
headRotationXProp.floatValue = Mathf.Clamp(euler.x, -180f, 180f);
headRotationYProp.floatValue = Mathf.Clamp(euler.y, -180f, 180f);
headRotationZProp.floatValue = Mathf.Clamp(euler.z, -180f, 180f);
serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(serializedObject.targetObject);
Debug.Log($"정면 캘리브레이션 완료 - T-Pose Forward: {tPoseForward}, Current Forward: {currentHeadForward}\n" +
$" → Offset X: {euler.x:F1}°, Y: {euler.y:F1}°, Z: {euler.z:F1}°");
}
}
[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;
// ... 기타 필요한 설정들
}