From 62a5a9bbb50138b25fd33243301eb59be691cb04 Mon Sep 17 00:00:00 2001 From: user Date: Sat, 7 Mar 2026 23:06:57 +0900 Subject: [PATCH] =?UTF-8?q?Refactor=20:=20FootGroundingController=EB=A5=BC?= =?UTF-8?q?=20Serializable=20=EB=AA=A8=EB=93=88=EB=A1=9C=20=EC=A0=84?= =?UTF-8?q?=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FootGroundingController: MonoBehaviour → [Serializable] 클래스 - Start() → Initialize(TwoBoneIKSolver, Animator) - Update()/LateUpdate() → OnUpdate()/OnLateUpdate() - CRS에서 footGrounding 필드로 소유, Update/LateUpdate에서 호출 - CustomRetargetingScriptEditor: groundingSO 제거, serializedObject 경로로 접근 Co-Authored-By: Claude Opus 4.6 --- .../CustomRetargetingScript.cs | 12 +++++ .../Editor/CustomRetargetingScriptEditor.cs | 45 +++++++---------- .../FootGroundingController.cs | 49 ++++--------------- 3 files changed, 39 insertions(+), 67 deletions(-) diff --git a/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs b/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs index 9ec19621e..83bf24768 100644 --- a/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs +++ b/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs @@ -98,6 +98,9 @@ namespace KindRetargeting [Header("어깨 보정")] [SerializeField] public ShoulderCorrectionFunction shoulderCorrection = new ShoulderCorrectionFunction(); + [Header("발 접지")] + [SerializeField] public FootGroundingController footGrounding = new FootGroundingController(); + [Header("프랍 부착")] [SerializeField] public PropLocationController propLocation = new PropLocationController(); @@ -328,6 +331,9 @@ namespace KindRetargeting if (targetAnimator != null) shoulderCorrection.Initialize(targetAnimator); + // 발 접지 모듈 초기화 + footGrounding.Initialize(ikSolver, targetAnimator); + // 프랍 부착 모듈 초기화 if (targetAnimator != null) propLocation.Initialize(targetAnimator); @@ -831,6 +837,9 @@ namespace KindRetargeting // 어깨 보정 (기존 ExecutionOrder 3) shoulderCorrection.OnUpdate(); + // 발 접지 Pre-IK (기존 ExecutionOrder 5) + footGrounding.OnUpdate(); + // IK 솔버 (기존 ExecutionOrder 6) ikSolver.OnUpdate(); @@ -847,6 +856,9 @@ namespace KindRetargeting /// void LateUpdate() { + // 발 접지 Post-IK (기존 FootGroundingController LateUpdate) + footGrounding.OnLateUpdate(); + ApplyHeadRotationOffset(); ApplyHeadScale(); } diff --git a/Assets/Scripts/KindRetargeting/Editor/CustomRetargetingScriptEditor.cs b/Assets/Scripts/KindRetargeting/Editor/CustomRetargetingScriptEditor.cs index 9a3fc9256..25c49bc60 100644 --- a/Assets/Scripts/KindRetargeting/Editor/CustomRetargetingScriptEditor.cs +++ b/Assets/Scripts/KindRetargeting/Editor/CustomRetargetingScriptEditor.cs @@ -10,8 +10,6 @@ namespace KindRetargeting { private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss"; - private SerializedObject groundingSO; - // SerializedProperty private SerializedProperty sourceAnimatorProp; private SerializedProperty targetAnimatorProp; @@ -33,7 +31,7 @@ namespace KindRetargeting protected override void OnDisable() { base.OnDisable(); - groundingSO = null; + // groundingSO 삭제됨 — footGrounding은 CRS 내부 모듈 } protected override void OnEnable() @@ -197,36 +195,27 @@ namespace KindRetargeting "• Toe Pivot: 발끝 고정 + 발목 회전 자동 감지", HelpBoxMessageType.Info)); - // FootGroundingController의 SerializedObject를 직접 바인딩 - var script = (CustomRetargetingScript)target; - var grounding = script.GetComponent(); - if (grounding != null) - { - groundingSO = new SerializedObject(grounding); + // FootGroundingController는 CRS 내부 모듈 — serializedObject의 프로퍼티 경로로 접근 + var groundHeightField = new PropertyField(serializedObject.FindProperty("footGrounding.groundHeight"), "바닥 높이"); + foldout.Add(groundHeightField); - var groundHeightField = new PropertyField(groundingSO.FindProperty("groundHeight"), "바닥 높이"); - foldout.Add(groundHeightField); + var weightSlider = new Slider("접지 강도", 0f, 1f) { showInputField = true }; + weightSlider.BindProperty(serializedObject.FindProperty("footGrounding.groundingWeight")); + foldout.Add(weightSlider); - var weightSlider = new Slider("접지 강도", 0f, 1f) { showInputField = true }; - weightSlider.BindProperty(groundingSO.FindProperty("groundingWeight")); - foldout.Add(weightSlider); + var activationField = new PropertyField(serializedObject.FindProperty("footGrounding.activationHeight"), "활성화 높이"); + activationField.tooltip = "발목이 이 높이 이상이면 접지 보정 비활성화 (점프 등)"; + foldout.Add(activationField); - var activationField = new PropertyField(groundingSO.FindProperty("activationHeight"), "활성화 높이"); - activationField.tooltip = "발목이 이 높이 이상이면 접지 보정 비활성화 (점프 등)"; - foldout.Add(activationField); + var thresholdField = new PropertyField(serializedObject.FindProperty("footGrounding.plantThreshold"), "접지 판정 범위"); + thresholdField.tooltip = "Toes가 이 범위 안이면 접지 중으로 판정"; + foldout.Add(thresholdField); - var thresholdField = new PropertyField(groundingSO.FindProperty("plantThreshold"), "접지 판정 범위"); - thresholdField.tooltip = "Toes가 이 범위 안이면 접지 중으로 판정"; - foldout.Add(thresholdField); + var smoothField = new PropertyField(serializedObject.FindProperty("footGrounding.smoothSpeed"), "보정 스무딩 속도"); + smoothField.tooltip = "보정량 변화 속도 (높을수록 빠른 반응, 낮으면 부드러운 전환)"; + foldout.Add(smoothField); - var smoothField = new PropertyField(groundingSO.FindProperty("smoothSpeed"), "보정 스무딩 속도"); - smoothField.tooltip = "보정량 변화 속도 (높을수록 빠른 반응, 낮으면 부드러운 전환)"; - foldout.Add(smoothField); - - foldout.Add(new HelpBox("힙 높이 보정은 '바닥 높이 조정' 섹션의 floorHeight로 제어합니다.", HelpBoxMessageType.Info)); - - foldout.TrackSerializedObjectValue(groundingSO, so => so.ApplyModifiedProperties()); - } + foldout.Add(new HelpBox("힙 높이 보정은 '바닥 높이 조정' 섹션의 floorHeight로 제어합니다.", HelpBoxMessageType.Info)); else { foldout.Add(new HelpBox("FootGroundingController 컴포넌트를 찾을 수 없습니다.", HelpBoxMessageType.Warning)); diff --git a/Assets/Scripts/KindRetargeting/FootGroundingController.cs b/Assets/Scripts/KindRetargeting/FootGroundingController.cs index 8a5908da2..58010ea88 100644 --- a/Assets/Scripts/KindRetargeting/FootGroundingController.cs +++ b/Assets/Scripts/KindRetargeting/FootGroundingController.cs @@ -5,19 +5,14 @@ namespace KindRetargeting /// /// HIK 스타일 2-Pass 접지 시스템. /// - /// Pass 1 (Update, Order 5 → IK 전): + /// Pass 1 (OnUpdate, Order 5 → IK 전): /// IK 타겟 위치를 수정하여 Toes가 바닥을 뚫지 않도록 보정. - /// Toe Pivot 감지: 발끝이 바닥에 있고 발목이 올라가면 - /// 발목 타겟을 역산하여 Toes가 groundHeight에 고정. /// - /// Pass 2 (LateUpdate → IK 후): + /// Pass 2 (OnLateUpdate → IK 후): /// IK 결과의 잔차를 Foot 회전으로 미세 보정. - /// 위치 변경 없음 — 본 길이 보존. - /// - /// 힙 높이 보정은 CRS의 floorHeight가 담당합니다 (이중 보정 방지). /// - [DefaultExecutionOrder(5)] - public class FootGroundingController : MonoBehaviour + [System.Serializable] + public class FootGroundingController { [Header("접지 설정")] [Tooltip("바닥 Y 좌표 (월드 공간)")] @@ -41,38 +36,31 @@ namespace KindRetargeting private TwoBoneIKSolver ikSolver; private Animator animator; - // 타겟 아바타 캐싱 private Transform leftFoot; private Transform rightFoot; private Transform leftToes; private Transform rightToes; - // Toes의 Foot 로컬 오프셋 (T-pose에서 캐싱) private Vector3 leftLocalToesOffset; private Vector3 rightLocalToesOffset; - // flat 상태에서 발목 최소 높이 (Foot.y - Toes.y) private float leftFootHeight; private float rightFootHeight; - // Toes 본 존재 여부 private bool leftHasToes; private bool rightHasToes; - // 스무딩용: 이전 프레임 보정량 private float leftPrevAdj; private float rightPrevAdj; private bool isInitialized; - private void Start() + public void Initialize(TwoBoneIKSolver ikSolver, Animator animator) { - var crs = GetComponent(); - if (crs != null) ikSolver = crs.ikSolver; - animator = GetComponent(); + this.ikSolver = ikSolver; + this.animator = animator; if (animator == null || !animator.isHuman || ikSolver == null) return; - if (leftFoot == null && rightFoot == null) return; leftFoot = animator.GetBoneTransform(HumanBodyBones.LeftFoot); rightFoot = animator.GetBoneTransform(HumanBodyBones.RightFoot); @@ -81,7 +69,6 @@ namespace KindRetargeting if (leftFoot == null || rightFoot == null) return; - // Toes 존재 여부 + 캐싱 leftHasToes = leftToes != null; rightHasToes = rightToes != null; @@ -92,7 +79,7 @@ namespace KindRetargeting } else { - leftFootHeight = 0.05f; // Toes 없을 때 기본 발목 높이 + leftFootHeight = 0.05f; } if (rightHasToes) @@ -111,7 +98,7 @@ namespace KindRetargeting /// /// Pass 1: Pre-IK — IK 타겟 위치를 수정합니다. /// - private void Update() + public void OnUpdate() { if (!isInitialized || groundingWeight < 0.001f) return; @@ -122,16 +109,11 @@ namespace KindRetargeting 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)); } - /// - /// 발 IK 타겟을 접지 모드에 따라 보정합니다. - /// Toes가 없는 아바타는 발목 Y 클램프만 수행합니다. - /// private float AdjustFootTarget(TwoBoneIKSolver.LimbIK limb, Vector3 localToesOffset, float footHeight, bool hasToes, float ikWeight) { @@ -141,12 +123,10 @@ namespace KindRetargeting Vector3 anklePos = ankleTarget.position; float ankleY = anklePos.y; - // AIRBORNE 체크 if (ankleY - groundHeight > activationHeight) return 0f; float weight = groundingWeight * ikWeight; - // === Toes 없는 아바타: 단순 Y 클램프 === if (!hasToes) { float minAnkleY = groundHeight + footHeight; @@ -160,7 +140,6 @@ namespace KindRetargeting return 0f; } - // === Toes 있는 아바타: 예측 기반 보정 === Vector3 predictedToesWorld = anklePos + ankleTarget.rotation * localToesOffset; float predictedToesY = predictedToesWorld.y; @@ -172,7 +151,6 @@ namespace KindRetargeting if (ankleY < groundHeight + footHeight + plantThreshold) { - // PLANTED: 발 전체가 바닥 근처 float minAnkleY = groundHeight + footHeight; if (ankleY < minAnkleY) { @@ -189,7 +167,6 @@ namespace KindRetargeting } else { - // TOE_PIVOT: 발끝 고정, 발목 올라감 if (toesError > 0f) { adjustment = toesError * weight; @@ -201,7 +178,6 @@ namespace KindRetargeting } else { - // Toes 충분히 위 → 발목만 바닥 아래 방지 float minAnkleY = groundHeight + footHeight; if (ankleY < minAnkleY) { @@ -217,7 +193,7 @@ namespace KindRetargeting /// /// Pass 2: Post-IK — 잔차를 Foot 회전으로 미세 보정합니다. /// - private void LateUpdate() + public void OnLateUpdate() { if (!isInitialized || groundingWeight < 0.001f) return; @@ -227,10 +203,6 @@ namespace KindRetargeting AlignFootToGround(rightFoot, rightToes, ikSolver.rightLeg.positionWeight); } - /// - /// IK 후 실제 Toes 위치를 확인하고, Foot 본을 pitch 회전하여 잔차 보정. - /// 바닥 아래로 뚫린 경우만 보정합니다. - /// private void AlignFootToGround(Transform foot, Transform toes, float ikWeight) { if (foot == null || toes == null) return; @@ -245,7 +217,6 @@ namespace KindRetargeting if (Mathf.Abs(error) < 0.001f) return; - // 바닥 아래로 뚫린 경우만 보정 if (error > plantThreshold) return; Vector3 footToToes = toes.position - foot.position;