Fix : 스크립트 디테일 제어

This commit is contained in:
user 2026-03-22 00:37:57 +09:00
parent b14c3f6f40
commit 783ab14f71
10 changed files with 11 additions and 373 deletions

Binary file not shown.

View File

@ -92,9 +92,6 @@ namespace KindRetargeting
[Header("바닥 높이 조정")]
[SerializeField, Range(-1f, 1f)] public float floorHeight = 0f; // 바닥 높이 조정값
[Header("발목 높이 설정")]
[SerializeField] public float minimumAnkleHeight = 0.2f; // 수동 설정용 최소 발목 높이
[Header("머리 회전 오프셋")]
[SerializeField, Range(-180f, 180f)] public float headRotationOffsetX = 0f; // 머리 좌우 기울기 (Roll)
[SerializeField, Range(-180f, 180f)] public float headRotationOffsetY = 0f; // 머리 좌우 회전 (Yaw)
@ -112,9 +109,6 @@ namespace KindRetargeting
[Header("어깨 보정")]
[SerializeField] public ShoulderCorrectionFunction shoulderCorrection = new ShoulderCorrectionFunction();
[Header("발 접지")]
[SerializeField] public FootGroundingController footGrounding = new FootGroundingController();
[Header("프랍 부착")]
[SerializeField] public PropLocationController propLocation = new PropLocationController();
@ -273,9 +267,6 @@ namespace KindRetargeting
if (targetAnimator != null)
shoulderCorrection.Initialize(targetAnimator);
// 발 접지 모듈 초기화
footGrounding.Initialize(ikSolver, targetAnimator);
// 프랍 부착 모듈 초기화
if (targetAnimator != null)
propLocation.Initialize(targetAnimator);
@ -749,7 +740,6 @@ namespace KindRetargeting
fingerShaped.OnUpdate();
shoulderCorrection.OnUpdate();
limbWeight.OnUpdate();
footGrounding.OnUpdate();
ikSolver.OnUpdate();
if (!Mathf.Approximately(previousScale, avatarScale))
@ -764,7 +754,6 @@ namespace KindRetargeting
/// </summary>
void LateUpdate()
{
footGrounding.OnLateUpdate();
ApplyHeadRotationOffset();
ApplyHeadScale();
}
@ -1186,7 +1175,7 @@ namespace KindRetargeting
GameObject rightArmGoal = CreateIKTargetObject("RightArmGoal", targetAnimator.GetBoneTransform(HumanBodyBones.RightLowerArm), ikTargetsParent);
ikSolver.rightArm.bendGoal = rightArmGoal.transform;
// 다리 타겟 (Foot + BendGoal) — 2-bone IK는 Foot까지만 풂, Toes는 FootGrounding에서 처리
// 다리 타겟 (Foot + BendGoal)
GameObject leftFoot = CreateIKTargetObject("LeftFoot", targetAnimator.GetBoneTransform(HumanBodyBones.LeftFoot), ikTargetsParent);
ikSolver.leftLeg.target = leftFoot.transform;
GameObject rightFoot = CreateIKTargetObject("RightFoot", targetAnimator.GetBoneTransform(HumanBodyBones.RightFoot), ikTargetsParent);
@ -1234,7 +1223,7 @@ namespace KindRetargeting
{
if (targetAnimator == null) return;
// 손과 발 타겟 업데이트 (다리는 Foot 타겟, FootGrounding이 Toes 처리)
// 손과 발 타겟 업데이트
UpdateEndTarget(ikSolver.leftArm.target, HumanBodyBones.LeftHand);
UpdateEndTarget(ikSolver.rightArm.target, HumanBodyBones.RightHand);
UpdateEndTarget(ikSolver.leftLeg.target, HumanBodyBones.LeftFoot);

View File

@ -57,7 +57,6 @@ namespace KindRetargeting
// ── 바닥 높이 ──
var floorFoldout = new Foldout { text = "바닥 높이 조정", value = false };
floorFoldout.Add(new PropertyField(serializedObject.FindProperty("floorHeight"), "바닥 높이 (-1 ~ 1)"));
floorFoldout.Add(new PropertyField(serializedObject.FindProperty("minimumAnkleHeight"), "최소 발목 높이"));
root.Add(floorFoldout);
// ── 머리 회전 오프셋 ──
@ -69,14 +68,6 @@ namespace KindRetargeting
// ── 사지 가중치 (LimbWeight) ──
root.Add(BuildLimbWeightSection());
// ── 접지 설정 (FootGrounding) ──
root.Add(BuildGroundingSection());
// ── 손가락 복제 설정 ──
var fingerCopyFoldout = new Foldout { text = "손가락 복제 설정", value = false };
fingerCopyFoldout.Add(new PropertyField(serializedObject.FindProperty("fingerCopyMode"), "복제 방식"));
root.Add(fingerCopyFoldout);
// ── 손가락 셰이핑 (FingerShaped) ──
root.Add(BuildFingerShapedSection());
@ -258,34 +249,6 @@ namespace KindRetargeting
return foldout;
}
// ========== 접지 설정 ==========
private VisualElement BuildGroundingSection()
{
var foldout = new Foldout { text = "접지 설정 (FootGrounding)", value = false };
foldout.Add(new HelpBox(
"HIK 스타일 2-Pass 접지 시스템:\n" +
"• Pre-IK: 발이 바닥을 뚫지 않도록 IK 타겟 보정\n" +
"• Post-IK: Foot 회전으로 Toes 접지 미세 보정",
HelpBoxMessageType.Info));
foldout.Add(new PropertyField(serializedObject.FindProperty("footGrounding.groundHeight"), "바닥 높이"));
var weightSlider = new Slider("접지 강도", 0f, 1f) { showInputField = true };
weightSlider.BindProperty(serializedObject.FindProperty("footGrounding.groundingWeight"));
foldout.Add(weightSlider);
foldout.Add(new PropertyField(serializedObject.FindProperty("footGrounding.activationHeight"), "활성화 높이") { tooltip = "발목이 이 높이 이상이면 보정 비활성화" });
foldout.Add(new PropertyField(serializedObject.FindProperty("footGrounding.plantThreshold"), "접지 판정 범위"));
var smoothField = new Slider("스무딩 속도", 1f, 30f) { showInputField = true };
smoothField.BindProperty(serializedObject.FindProperty("footGrounding.smoothSpeed"));
foldout.Add(smoothField);
return foldout;
}
// ========== 손가락 셰이핑 ==========
private VisualElement BuildFingerShapedSection()

View File

@ -228,19 +228,14 @@ public class RetargetingControlWindow : EditorWindow
panel.Add(BuildShoulderSection(script, so));
// 접지 설정
panel.Add(BuildGroundingSection(script, so));
// 손가락 제어 설정
panel.Add(BuildFingerControlSection(script, so));
// 손가락 복제 설정
panel.Add(BuildFingerCopySection(script, so));
// 바닥 높이 설정
var floorFoldout = new Foldout { text = "바닥 높이 설정", value = false };
var floorContainer = new VisualElement();
floorContainer.Add(new PropertyField(so.FindProperty("floorHeight"), "바닥 높이 (-1 ~ 1)"));
floorContainer.Add(new PropertyField(so.FindProperty("minimumAnkleHeight"), "최소 발목 높이"));
floorContainer.Bind(so);
floorFoldout.Add(floorContainer);
panel.Add(floorFoldout);
@ -416,6 +411,7 @@ public class RetargetingControlWindow : EditorWindow
// 프리셋
container.Add(BuildFingerPresets(script, script.fingerShaped));
container.Bind(so);
foldout.Add(container);
return foldout;
}
@ -512,21 +508,6 @@ public class RetargetingControlWindow : EditorWindow
return container;
}
// ========== Finger Copy ==========
private VisualElement BuildFingerCopySection(CustomRetargetingScript script, SerializedObject so)
{
var foldout = new Foldout { text = "손가락 복제 설정", value = false };
var container = new VisualElement();
var copyModeProp = so.FindProperty("fingerCopyMode");
container.Add(new PropertyField(copyModeProp, "복제 방식"));
container.Bind(so);
foldout.Add(container);
return foldout;
}
// ========== Head Rotation ==========
private VisualElement BuildHeadRotationSection(CustomRetargetingScript script, SerializedObject so)
@ -778,30 +759,6 @@ public class RetargetingControlWindow : EditorWindow
return foldout;
}
// ========== Foot Grounding ==========
private VisualElement BuildGroundingSection(CustomRetargetingScript script, SerializedObject so)
{
var foldout = new Foldout { text = "접지 설정", value = false };
var container = new VisualElement();
container.Add(new PropertyField(so.FindProperty("footGrounding.groundHeight"), "바닥 높이"));
var weightSlider = new Slider("접지 강도", 0f, 1f) { showInputField = true };
weightSlider.BindProperty(so.FindProperty("footGrounding.groundingWeight"));
container.Add(weightSlider);
container.Add(new PropertyField(so.FindProperty("footGrounding.activationHeight"), "활성화 높이") { tooltip = "발목이 이 높이 이상이면 보정 비활성화" });
container.Add(new PropertyField(so.FindProperty("footGrounding.plantThreshold"), "접지 판정 범위"));
var smoothField = new Slider("스무딩 속도", 1f, 30f) { showInputField = true };
smoothField.BindProperty(so.FindProperty("footGrounding.smoothSpeed"));
container.Add(smoothField);
container.Bind(so);
foldout.Add(container);
return foldout;
}
// ========== Helpers ==========

View File

@ -1,238 +0,0 @@
using UnityEngine;
namespace KindRetargeting
{
/// <summary>
/// HIK 스타일 2-Pass 접지 시스템.
///
/// Pass 1 (OnUpdate, Order 5 → IK 전):
/// IK 타겟 위치를 수정하여 Toes가 바닥을 뚫지 않도록 보정.
///
/// Pass 2 (OnLateUpdate → IK 후):
/// IK 결과의 잔차를 Foot 회전으로 미세 보정.
/// </summary>
[System.Serializable]
public class FootGroundingController
{
[Header("접지 설정")]
[Tooltip("바닥 Y 좌표 (월드 공간)")]
public float groundHeight = 0f;
[Tooltip("접지 보정 강도")]
[Range(0f, 1f)]
public float groundingWeight = 1f;
[Tooltip("이 높이 이상이면 AIRBORNE (보정 안 함)")]
public float activationHeight = 0.5f;
[Tooltip("Toes가 이 범위 안이면 접지 중으로 판정")]
public float plantThreshold = 0.02f;
[Header("스무딩")]
[Tooltip("보정량 변화 속도 (높을수록 빠른 반응)")]
[Range(1f, 30f)]
public float smoothSpeed = 10f;
private TwoBoneIKSolver ikSolver;
private Animator animator;
private Transform leftFoot;
private Transform rightFoot;
private Transform leftToes;
private Transform rightToes;
private Vector3 leftLocalToesOffset;
private Vector3 rightLocalToesOffset;
private float leftFootHeight;
private float rightFootHeight;
private bool leftHasToes;
private bool rightHasToes;
private float leftPrevAdj;
private float rightPrevAdj;
private bool isInitialized;
public void Initialize(TwoBoneIKSolver ikSolver, Animator animator)
{
this.ikSolver = ikSolver;
this.animator = animator;
if (animator == null || !animator.isHuman || ikSolver == null) return;
leftFoot = animator.GetBoneTransform(HumanBodyBones.LeftFoot);
rightFoot = animator.GetBoneTransform(HumanBodyBones.RightFoot);
leftToes = animator.GetBoneTransform(HumanBodyBones.LeftToes);
rightToes = animator.GetBoneTransform(HumanBodyBones.RightToes);
if (leftFoot == null || rightFoot == null) return;
leftHasToes = leftToes != null;
rightHasToes = rightToes != null;
if (leftHasToes)
{
leftLocalToesOffset = leftFoot.InverseTransformPoint(leftToes.position);
leftFootHeight = Mathf.Abs(leftFoot.position.y - leftToes.position.y);
}
else
{
leftFootHeight = 0.05f;
}
if (rightHasToes)
{
rightLocalToesOffset = rightFoot.InverseTransformPoint(rightToes.position);
rightFootHeight = Mathf.Abs(rightFoot.position.y - rightToes.position.y);
}
else
{
rightFootHeight = 0.05f;
}
isInitialized = true;
}
/// <summary>
/// Pass 1: Pre-IK — IK 타겟 위치를 수정합니다.
/// </summary>
public void OnUpdate()
{
if (!isInitialized || groundingWeight < 0.001f) return;
float leftAdj = AdjustFootTarget(
ikSolver.leftLeg, leftLocalToesOffset, leftFootHeight,
leftHasToes, ikSolver.leftLeg.positionWeight);
float rightAdj = AdjustFootTarget(
ikSolver.rightLeg, rightLocalToesOffset, rightFootHeight,
rightHasToes, ikSolver.rightLeg.positionWeight);
float dt = Time.deltaTime * smoothSpeed;
leftPrevAdj = Mathf.Lerp(leftPrevAdj, leftAdj, Mathf.Clamp01(dt));
rightPrevAdj = Mathf.Lerp(rightPrevAdj, rightAdj, Mathf.Clamp01(dt));
}
private float AdjustFootTarget(TwoBoneIKSolver.LimbIK limb, Vector3 localToesOffset,
float footHeight, bool hasToes, float ikWeight)
{
if (limb.target == null || ikWeight < 0.01f) return 0f;
Transform ankleTarget = limb.target;
Vector3 anklePos = ankleTarget.position;
float ankleY = anklePos.y;
if (ankleY - groundHeight > activationHeight) return 0f;
float weight = groundingWeight * ikWeight;
if (!hasToes)
{
float minAnkleY = groundHeight + footHeight;
if (ankleY < minAnkleY)
{
float adj = (minAnkleY - ankleY) * weight;
anklePos.y += adj;
ankleTarget.position = anklePos;
return adj;
}
return 0f;
}
Vector3 predictedToesWorld = anklePos + ankleTarget.rotation * localToesOffset;
float predictedToesY = predictedToesWorld.y;
float adjustment = 0f;
if (predictedToesY < groundHeight + plantThreshold)
{
float toesError = groundHeight - predictedToesY;
if (ankleY < groundHeight + footHeight + plantThreshold)
{
float minAnkleY = groundHeight + footHeight;
if (ankleY < minAnkleY)
{
adjustment = (minAnkleY - ankleY) * weight;
anklePos.y += adjustment;
}
if (toesError > 0f)
{
float extra = toesError * weight;
anklePos.y += extra;
adjustment += extra;
}
}
else
{
if (toesError > 0f)
{
adjustment = toesError * weight;
anklePos.y += adjustment;
}
}
ankleTarget.position = anklePos;
}
else
{
float minAnkleY = groundHeight + footHeight;
if (ankleY < minAnkleY)
{
adjustment = (minAnkleY - ankleY) * weight;
anklePos.y += adjustment;
ankleTarget.position = anklePos;
}
}
return adjustment;
}
/// <summary>
/// Pass 2: Post-IK — 잔차를 Foot 회전으로 미세 보정합니다.
/// </summary>
public void OnLateUpdate()
{
if (!isInitialized || groundingWeight < 0.001f) return;
if (leftHasToes)
AlignFootToGround(leftFoot, leftToes, ikSolver.leftLeg.positionWeight);
if (rightHasToes)
AlignFootToGround(rightFoot, rightToes, ikSolver.rightLeg.positionWeight);
}
private void AlignFootToGround(Transform foot, Transform toes, float ikWeight)
{
if (foot == null || toes == null) return;
if (ikWeight < 0.01f) return;
if (foot.position.y - groundHeight > activationHeight) return;
float weight = groundingWeight * ikWeight;
float actualToesY = toes.position.y;
float error = actualToesY - groundHeight;
if (Mathf.Abs(error) < 0.001f) return;
if (error > plantThreshold) return;
Vector3 footToToes = toes.position - foot.position;
float horizontalDist = new Vector2(footToToes.x, footToToes.z).magnitude;
if (horizontalDist < 0.001f) return;
float pitchAngle = Mathf.Atan2(error, horizontalDist) * Mathf.Rad2Deg;
Vector3 footForwardFlat = new Vector3(footToToes.x, 0f, footToToes.z).normalized;
Vector3 pitchAxis = Vector3.Cross(Vector3.up, footForwardFlat);
if (pitchAxis.sqrMagnitude < 0.001f) return;
pitchAxis.Normalize();
Quaternion correction = Quaternion.AngleAxis(-pitchAngle, pitchAxis);
foot.rotation = Quaternion.Slerp(foot.rotation, correction * foot.rotation, weight);
}
}
}

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 1752f1fe3cee0b24e95ae0d5a0468865

BIN
Assets/Scripts/KindRetargeting/README.md (Stored with Git LFS)

Binary file not shown.

View File

@ -288,13 +288,6 @@ namespace KindRetargeting.Remote
{ "shoulderReverseLeft", script.shoulderCorrection.reverseLeftRotation },
{ "shoulderReverseRight", script.shoulderCorrection.reverseRightRotation },
// FootGrounding 데이터
{ "groundingWeight", script.footGrounding.groundingWeight },
{ "groundingGroundHeight", script.footGrounding.groundHeight },
{ "groundingActivationHeight", script.footGrounding.activationHeight },
{ "groundingPlantThreshold", script.footGrounding.plantThreshold },
{ "groundingSmoothSpeed", script.footGrounding.smoothSpeed },
// FingerShapedController 데이터
{ "handPoseEnabled", script.fingerShaped.enabled },
{ "leftHandEnabled", script.fingerShaped.leftHandEnabled },
@ -312,8 +305,6 @@ namespace KindRetargeting.Remote
{ "rightPinkyCurl", script.fingerShaped.rightPinkyCurl },
{ "rightSpreadFingers", script.fingerShaped.rightSpreadFingers },
// 최소 발목 높이
{ "minimumAnkleHeight", GetPrivateField<float>(script, "minimumAnkleHeight") },
};
var response = new
@ -449,23 +440,6 @@ namespace KindRetargeting.Remote
script.shoulderCorrection.reverseRightRotation = value > 0.5f;
break;
// FootGrounding 속성
case "groundingWeight":
script.footGrounding.groundingWeight = value;
break;
case "groundingGroundHeight":
script.footGrounding.groundHeight = value;
break;
case "groundingActivationHeight":
script.footGrounding.activationHeight = value;
break;
case "groundingPlantThreshold":
script.footGrounding.plantThreshold = value;
break;
case "groundingSmoothSpeed":
script.footGrounding.smoothSpeed = value;
break;
// FingerShapedController 속성
case "handPoseEnabled":
script.fingerShaped.enabled = value > 0.5f;
@ -519,11 +493,6 @@ namespace KindRetargeting.Remote
script.fingerShaped.rightSpreadFingers = value;
break;
// 최소 발목 높이
case "minimumAnkleHeight":
SetPrivateField(script, "minimumAnkleHeight", value);
break;
default:
Debug.LogWarning($"[RetargetingRemote] 알 수 없는 속성: {property}");
break;