Fix : 리타겟팅 스크립트 허리 방향 보정 로컬 방향으로 업데이트

This commit is contained in:
qsxft258@gmail.com 2025-11-13 18:53:36 +09:00
parent 637859bada
commit 3f160527a9
3 changed files with 177 additions and 23 deletions

View File

@ -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();
}
/// <summary>
/// 타겟 아바타의 로컬 축과 월드 축의 관계를 분석하여 정규화 계수를 계산합니다.
/// 이를 통해 사용자는 항상 직관적인 방향(앞/뒤/위/아래)으로 제어할 수 있습니다.
/// </summary>
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)");
}
/// <summary>
/// 로컬 축이 월드 축과 같은 방향인지 반대 방향인지 판단합니다.
/// </summary>
/// <param name="localAxis">로컬 축 방향 (월드 공간)</param>
/// <param name="worldAxis">비교할 월드 축</param>
/// <returns>같은 방향이면 1, 반대면 -1</returns>
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;
}
/// <summary>
/// HumanPoseHandler를 초기화합니다.
/// </summary>
@ -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<RetargetingSettings>(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;
}
// 힙을 제외한 본들의 회전 동기화

View File

@ -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--;
}

View File

@ -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--;
}