From 3f160527a9cda9061d1c8a6acf418d4135ceb31b Mon Sep 17 00:00:00 2001 From: "qsxft258@gmail.com" Date: Thu, 13 Nov 2025 18:53:36 +0900 Subject: [PATCH] =?UTF-8?q?Fix=20:=20=EB=A6=AC=ED=83=80=EA=B2=9F=ED=8C=85?= =?UTF-8?q?=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20=ED=97=88=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=A9=ED=96=A5=20=EB=B3=B4=EC=A0=95=20=EB=A1=9C=EC=BB=AC=20?= =?UTF-8?q?=EB=B0=A9=ED=96=A5=EC=9C=BC=EB=A1=9C=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomRetargetingScript.cs | 120 +++++++++++++++--- .../Editor/CustomRetargetingScriptEditor.cs | 48 ++++++- .../Editor/RetargetingControlWindow.cs | 32 ++++- 3 files changed, 177 insertions(+), 23 deletions(-) diff --git a/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs b/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs index c8df0e0b..1993683b 100644 --- a/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs +++ b/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs @@ -28,12 +28,23 @@ namespace KindRetargeting // IK 컴포넌트 참조 변경 private FullBodyInverseKinematics_RND ikComponent; - [Header("힙 위치 보정")] + [Header("힙 위치 보정 (로컬 좌표계 기반)")] [SerializeField, Range(-1, 1)] - private float hipsWeight = 0f; // 힙의 위치를 위아래로 보정하는 가중치 + private float hipsOffsetX = 0f; // 캐릭터 기준 좌우 (항상 Right 방향) + [SerializeField, Range(-1, 1)] + private float hipsOffsetY = 0f; // 캐릭터 기준 상하 (항상 Up 방향) + [SerializeField, Range(-1, 1)] + private float hipsOffsetZ = 0f; // 캐릭터 기준 앞뒤 (항상 Forward 방향) [HideInInspector] public float HipsWeightOffset = 1f; + // 로컬-월드 축 방향 정규화 계수 + private Vector3 axisNormalizer = Vector3.one; + + [Header("축 정규화 정보 (읽기 전용)")] + [SerializeField, HideInInspector] + private Vector3 debugAxisNormalizer = Vector3.one; + // HumanPoseHandler를 이용하여 원본 및 대상 아바타의 포즈를 관리 private HumanPoseHandler sourcePoseHandler; private HumanPoseHandler targetPoseHandler; @@ -119,7 +130,9 @@ namespace KindRetargeting [System.Serializable] private class RetargetingSettings { - public float hipsWeight; + public float hipsOffsetX; // 변경: hipsWeight → hipsOffsetX/Y/Z + public float hipsOffsetY; + public float hipsOffsetZ; public float kneeInOutWeight; public float kneeFrontBackWeight; public float floorHeight; @@ -167,6 +180,9 @@ namespace KindRetargeting // IK 타겟 생성 (무릎 시각화 오브젝트 포함) CreateIKTargets(); + // T-포즈 전에 축 정규화 계수 계산 + CalculateAxisNormalizer(); + // 원본 및 대상 아바타를 T-포즈로 복원 SetTPose(sourceAnimator); SetTPose(targetAnimator); @@ -203,6 +219,59 @@ namespace KindRetargeting ApplyScale(); } + /// + /// 타겟 아바타의 로컬 축과 월드 축의 관계를 분석하여 정규화 계수를 계산합니다. + /// 이를 통해 사용자는 항상 직관적인 방향(앞/뒤/위/아래)으로 제어할 수 있습니다. + /// + private void CalculateAxisNormalizer() + { + if (targetAnimator == null) return; + + Transform hips = targetAnimator.GetBoneTransform(HumanBodyBones.Hips); + if (hips == null) return; + + // 힙의 초기 로컬 축 방향을 월드 공간으로 변환 + Vector3 localRight = hips.TransformDirection(Vector3.right); + Vector3 localUp = hips.TransformDirection(Vector3.up); + Vector3 localForward = hips.TransformDirection(Vector3.forward); + + // 각 로컬 축이 월드의 어느 축과 가장 가까운지 판단 + axisNormalizer = new Vector3( + GetAxisSign(localRight, Vector3.right), // X축 정규화 + GetAxisSign(localUp, Vector3.up), // Y축 정규화 + GetAxisSign(localForward, Vector3.forward) // Z축 정규화 + ); + + debugAxisNormalizer = axisNormalizer; // Inspector 표시용 + + Debug.Log($"[{gameObject.name}] 축 정규화 계수:\n" + + $" X(좌우): {axisNormalizer.x} (로컬 Right {(axisNormalizer.x > 0 ? "→" : "←")} 월드 Right)\n" + + $" Y(상하): {axisNormalizer.y} (로컬 Up {(axisNormalizer.y > 0 ? "↑" : "↓")} 월드 Up)\n" + + $" Z(앞뒤): {axisNormalizer.z} (로컬 Forward {(axisNormalizer.z > 0 ? "→" : "←")} 월드 Forward)"); + } + + /// + /// 로컬 축이 월드 축과 같은 방향인지 반대 방향인지 판단합니다. + /// + /// 로컬 축 방향 (월드 공간) + /// 비교할 월드 축 + /// 같은 방향이면 1, 반대면 -1 + private float GetAxisSign(Vector3 localAxis, Vector3 worldAxis) + { + // 내적으로 방향성 판단 + float dot = Vector3.Dot(localAxis.normalized, worldAxis); + + // 가장 지배적인 축 성분의 부호를 반환 + if (Mathf.Abs(dot) > 0.5f) // 45도 이내면 해당 축으로 간주 + { + return Mathf.Sign(dot); + } + + // 축이 너무 비스듬하면 경고 (정상적인 휴머노이드 리그라면 발생 안함) + Debug.LogWarning($"[AxisNormalizer] 축이 비정상적으로 기울어져 있습니다: {localAxis} vs {worldAxis}"); + return 1f; + } + /// /// HumanPoseHandler를 초기화합니다. /// @@ -341,7 +410,9 @@ namespace KindRetargeting var settings = new RetargetingSettings { - hipsWeight = hipsWeight, + hipsOffsetX = hipsOffsetX, + hipsOffsetY = hipsOffsetY, + hipsOffsetZ = hipsOffsetZ, kneeInOutWeight = kneeInOutWeight, kneeFrontBackWeight = kneeFrontBackWeight, floorHeight = floorHeight, @@ -397,7 +468,9 @@ namespace KindRetargeting var settings = JsonUtility.FromJson(json); // 설정 적용 - hipsWeight = settings.hipsWeight; + hipsOffsetX = settings.hipsOffsetX; + hipsOffsetY = settings.hipsOffsetY; + hipsOffsetZ = settings.hipsOffsetZ; kneeInOutWeight = settings.kneeInOutWeight; kneeFrontBackWeight = settings.kneeFrontBackWeight; floorHeight = settings.floorHeight; @@ -707,20 +780,35 @@ namespace KindRetargeting if (sourceHips != null && targetHips != null) { - // 힙 위치 동기화 + 힙 위치 보정 적용 + 바닥 높이 적용용 - Vector3 adjustedPosition = sourceHips.position; - adjustedPosition.y += hipsWeight * HipsWeightOffset; // 기존 힙 높이 조정 - adjustedPosition.y += floorHeight; // 바닥 높이 조정 추가 - targetHips.position = adjustedPosition; - - // 힙 회전 동기화 (회전 오프셋 적용) + // 1. 힙 회전 먼저 동기화 (회전 오프셋 적용) + Quaternion finalHipsRotation = sourceHips.rotation; if (rotationOffsets.TryGetValue(HumanBodyBones.Hips, out Quaternion hipsOffset)) { - targetHips.rotation = sourceHips.rotation * hipsOffset; - - ikComponent.solver.spine.pelvisTarget.position = targetHips.position; - ikComponent.solver.spine.pelvisTarget.rotation = targetHips.rotation; + finalHipsRotation = sourceHips.rotation * hipsOffset; + targetHips.rotation = finalHipsRotation; } + + // 2. 로컬 오프셋 벡터 생성 (정규화 계수 적용) + Vector3 normalizedOffset = new Vector3( + hipsOffsetX * axisNormalizer.x * HipsWeightOffset, // 좌우 + hipsOffsetY * axisNormalizer.y * HipsWeightOffset, // 상하 + hipsOffsetZ * axisNormalizer.z * HipsWeightOffset // 앞뒤 + ); + + // 3. 로컬 좌표계를 월드 좌표계로 변환 + Vector3 worldOffset = targetHips.rotation * normalizedOffset; + + // 4. 힙 위치 동기화 + 로컬 오프셋 적용 + Vector3 adjustedPosition = sourceHips.position + worldOffset; + + // 5. 바닥 높이는 월드 Y축에 직접 적용 (중력 방향이므로 월드 기준) + adjustedPosition.y += floorHeight; + + targetHips.position = adjustedPosition; + + // 6. IK 타겟에도 동기화 + ikComponent.solver.spine.pelvisTarget.position = targetHips.position; + ikComponent.solver.spine.pelvisTarget.rotation = targetHips.rotation; } // 힙을 제외한 본들의 회전 동기화 diff --git a/Assets/Scripts/KindRetargeting/Editor/CustomRetargetingScriptEditor.cs b/Assets/Scripts/KindRetargeting/Editor/CustomRetargetingScriptEditor.cs index 1b611ecb..38d2cb92 100644 --- a/Assets/Scripts/KindRetargeting/Editor/CustomRetargetingScriptEditor.cs +++ b/Assets/Scripts/KindRetargeting/Editor/CustomRetargetingScriptEditor.cs @@ -22,7 +22,10 @@ namespace KindRetargeting // SerializedProperty 변수들 private SerializedProperty sourceAnimatorProp; private SerializedProperty targetAnimatorProp; - private SerializedProperty hipsWeightProp; + private SerializedProperty hipsOffsetXProp; + private SerializedProperty hipsOffsetYProp; + private SerializedProperty hipsOffsetZProp; + private SerializedProperty debugAxisNormalizerProp; private SerializedProperty fingerCopyModeProp; private SerializedProperty useMotionFilterProp; private SerializedProperty filterBufferSizeProp; @@ -41,7 +44,10 @@ namespace KindRetargeting // SerializedProperty 초기화 sourceAnimatorProp = serializedObject.FindProperty("sourceAnimator"); targetAnimatorProp = serializedObject.FindProperty("targetAnimator"); - hipsWeightProp = serializedObject.FindProperty("hipsWeight"); + 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"); @@ -86,11 +92,45 @@ namespace KindRetargeting GUILayout.Space(5); // 힙 위치 보정 Foldout - showHipsSettings = EditorGUILayout.Foldout(showHipsSettings, "힙 위치 보정"); + showHipsSettings = EditorGUILayout.Foldout(showHipsSettings, "힙 위치 보정 (로컬 좌표계)"); if (showHipsSettings) { EditorGUI.indentLevel++; - EditorGUILayout.PropertyField(hipsWeightProp, new GUIContent("힙 가중치")); + + // 축 정규화 정보 표시 + if (debugAxisNormalizerProp != null) + { + Vector3 normalizer = debugAxisNormalizerProp.vector3Value; + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.LabelField("축 정규화 정보", EditorStyles.boldLabel); + + EditorGUI.BeginDisabledGroup(true); + EditorGUILayout.Vector3Field("정규화 계수", normalizer); + EditorGUI.EndDisabledGroup(); + + EditorGUILayout.HelpBox( + $"X축: {(normalizer.x > 0 ? "정방향 ↔" : "역방향 ↔")}\n" + + $"Y축: {(normalizer.y > 0 ? "정방향 ↕" : "역방향 ↕")}\n" + + $"Z축: {(normalizer.z > 0 ? "정방향 ⇄" : "역방향 ⇄")}\n\n" + + "이 계수는 Start()에서 자동 계산되며, 항상 직관적인 방향으로 제어할 수 있게 합니다.", + 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--; } diff --git a/Assets/Scripts/KindRetargeting/Editor/RetargetingControlWindow.cs b/Assets/Scripts/KindRetargeting/Editor/RetargetingControlWindow.cs index 60f223ec..1bfbb98d 100644 --- a/Assets/Scripts/KindRetargeting/Editor/RetargetingControlWindow.cs +++ b/Assets/Scripts/KindRetargeting/Editor/RetargetingControlWindow.cs @@ -255,12 +255,38 @@ public class RetargetingControlWindow : EditorWindow } // 힙 설정 - hipsSettingsFoldouts[instanceID] = EditorGUILayout.Foldout(hipsSettingsFoldouts[instanceID], "힙 위치 보정", true); + hipsSettingsFoldouts[instanceID] = EditorGUILayout.Foldout(hipsSettingsFoldouts[instanceID], "힙 위치 보정 (로컬)", true); if (hipsSettingsFoldouts[instanceID]) { EditorGUI.indentLevel++; - var hipsWeightProp = serializedObject.FindProperty("hipsWeight"); - EditorGUILayout.PropertyField(hipsWeightProp, new GUIContent("힙 가중치")); + + var hipsOffsetXProp = serializedObject.FindProperty("hipsOffsetX"); + var hipsOffsetYProp = serializedObject.FindProperty("hipsOffsetY"); + var hipsOffsetZProp = serializedObject.FindProperty("hipsOffsetZ"); + + // 정규화 계수 표시 + var normalizerProp = serializedObject.FindProperty("debugAxisNormalizer"); + if (normalizerProp != null) + { + Vector3 norm = normalizerProp.vector3Value; + if (norm != Vector3.zero) // 초기화되었을 때만 표시 + { + string xIcon = norm.x > 0 ? "↔" : "↔(반전)"; + string yIcon = norm.y > 0 ? "↕" : "↕(반전)"; + string zIcon = norm.z > 0 ? "⇄" : "⇄(반전)"; + + EditorGUILayout.LabelField($"축 상태: X{xIcon} Y{yIcon} Z{zIcon}", + EditorStyles.miniLabel); + } + } + + EditorGUILayout.Slider(hipsOffsetXProp, -1f, 1f, + new GUIContent("← 좌우 →", "캐릭터 기준 왼쪽(-) / 오른쪽(+)")); + EditorGUILayout.Slider(hipsOffsetYProp, -1f, 1f, + new GUIContent("↓ 상하 ↑", "캐릭터 기준 아래(-) / 위(+)")); + EditorGUILayout.Slider(hipsOffsetZProp, -1f, 1f, + new GUIContent("← 앞뒤 →", "캐릭터 기준 뒤(-) / 앞(+)")); + EditorGUI.indentLevel--; }