From 0a7624dab6b61953d864fdbb90eb47a484e81358 Mon Sep 17 00:00:00 2001 From: user Date: Thu, 26 Mar 2026 23:37:36 +0900 Subject: [PATCH] =?UTF-8?q?Fix=20:=20TwoBoneIKSolver=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20=EC=86=94=EB=B2=84=EB=A1=9C=20=EA=B5=90=EC=B2=B4=20?= =?UTF-8?q?=E2=80=94=20180=C2=B0=20=EB=AC=B4=EB=A6=8E=20=EB=8D=9C=EC=BB=A5?= =?UTF-8?q?=EA=B1=B0=EB=A6=BC=20=ED=95=B4=EA=B2=B0=20=EB=B0=8F=20=EC=97=AD?= =?UTF-8?q?=EA=B4=80=EC=A0=88=20=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FinalIK IKSolverTrigonometric 의존성 제거, 자체 솔버 구현 - cosine law 대신 소스 무릎 위치를 비율 스케일하여 타겟 무릎 직접 배치 - 180° 특이점 없이 정상↔역관절 자연스러운 전환 - FromToRotation 기반 본 회전으로 twist 보존 - 팔/다리 모두 소스 본 참조 설정, 소스 없으면 cosine law fallback Co-Authored-By: Claude Opus 4.6 (1M context) --- .../CustomRetargetingScript.cs | 21 +++ .../KindRetargeting/TwoBoneIKSolver.cs | 136 +++++++++++++++--- 2 files changed, 134 insertions(+), 23 deletions(-) diff --git a/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs b/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs index fc57731ef..d155469ca 100644 --- a/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs +++ b/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs @@ -1188,6 +1188,26 @@ namespace KindRetargeting // TwoBoneIKSolver 본 캐싱 초기화 ikSolver.Initialize(targetAnimator); + + // IK에 소스 본 참조 설정 (소스 관절 위치 기반 IK용) + if (optitrackSource != null) + { + ikSolver.leftLeg.sourceUpper = GetSourceBoneTransform(HumanBodyBones.LeftUpperLeg); + ikSolver.leftLeg.sourceLower = GetSourceBoneTransform(HumanBodyBones.LeftLowerLeg); + ikSolver.leftLeg.sourceEnd = GetSourceBoneTransform(HumanBodyBones.LeftFoot); + + ikSolver.rightLeg.sourceUpper = GetSourceBoneTransform(HumanBodyBones.RightUpperLeg); + ikSolver.rightLeg.sourceLower = GetSourceBoneTransform(HumanBodyBones.RightLowerLeg); + ikSolver.rightLeg.sourceEnd = GetSourceBoneTransform(HumanBodyBones.RightFoot); + + ikSolver.leftArm.sourceUpper = GetSourceBoneTransform(HumanBodyBones.LeftUpperArm); + ikSolver.leftArm.sourceLower = GetSourceBoneTransform(HumanBodyBones.LeftLowerArm); + ikSolver.leftArm.sourceEnd = GetSourceBoneTransform(HumanBodyBones.LeftHand); + + ikSolver.rightArm.sourceUpper = GetSourceBoneTransform(HumanBodyBones.RightUpperArm); + ikSolver.rightArm.sourceLower = GetSourceBoneTransform(HumanBodyBones.RightLowerArm); + ikSolver.rightArm.sourceEnd = GetSourceBoneTransform(HumanBodyBones.RightHand); + } } /// @@ -1233,6 +1253,7 @@ namespace KindRetargeting updatejointTarget(ikSolver.rightArm.bendGoal, HumanBodyBones.RightLowerArm); updatejointTarget(ikSolver.leftLeg.bendGoal, HumanBodyBones.LeftLowerLeg); updatejointTarget(ikSolver.rightLeg.bendGoal, HumanBodyBones.RightLowerLeg); + } /// diff --git a/Assets/Scripts/KindRetargeting/TwoBoneIKSolver.cs b/Assets/Scripts/KindRetargeting/TwoBoneIKSolver.cs index 13b5e9953..567bb079a 100644 --- a/Assets/Scripts/KindRetargeting/TwoBoneIKSolver.cs +++ b/Assets/Scripts/KindRetargeting/TwoBoneIKSolver.cs @@ -1,11 +1,11 @@ using UnityEngine; -using RootMotion.FinalIK; namespace KindRetargeting { /// - /// FinalIK IKSolverTrigonometric.Solve()를 사용하는 IK 래퍼. - /// 4개 사지(양팔, 양다리)에 대해 FinalIK의 검증된 코사인 법칙 솔버를 호출합니다. + /// 커스텀 Two-Bone IK 솔버. + /// 소스 무릎 위치를 비율 스케일하여 타겟 무릎을 직접 배치합니다. + /// cosine law 없이 동작하므로 180° 특이점이 없고 역관절도 자연스럽게 지원합니다. /// [System.Serializable] public class TwoBoneIKSolver @@ -28,6 +28,11 @@ namespace KindRetargeting [HideInInspector] public float lowerLength; [HideInInspector] public Vector3 localBendNormal; + + // 소스 본 참조 (소스 무릎 위치 기반 IK용) + [HideInInspector] public Transform sourceUpper; + [HideInInspector] public Transform sourceLower; + [HideInInspector] public Transform sourceEnd; } [HideInInspector] public Animator animator; @@ -96,28 +101,33 @@ namespace KindRetargeting if (!limb.enabled || limb.target == null || limb.positionWeight < 0.001f) return; if (limb.upper == null || limb.lower == null || limb.end == null) return; - Vector3 bendNormal = GetBendNormal(limb); + float upperLen = Vector3.Distance(limb.upper.position, limb.lower.position); + float lowerLen = Vector3.Distance(limb.lower.position, limb.end.position); - if (limb.bendGoal != null && limb.bendGoalWeight > 0.001f) + Vector3 targetPos = Vector3.Lerp(limb.end.position, limb.target.position, limb.positionWeight); + + // --- 무릎 위치 결정 --- + Vector3 kneePos; + + if (limb.sourceUpper != null && limb.sourceLower != null && limb.sourceEnd != null) { - Vector3 goalNormal = Vector3.Cross( - limb.bendGoal.position - limb.upper.position, - limb.target.position - limb.upper.position - ); - if (goalNormal.sqrMagnitude > 0.0001f) - { - bendNormal = Vector3.Lerp(bendNormal, goalNormal.normalized, limb.bendGoalWeight); - } + // 소스 무릎 위치 기반: 소스의 무릎 위치를 타겟 비율로 스케일 + kneePos = ComputeKneePosFromSource(limb, upperLen, lowerLen, targetPos); + } + else + { + // 소스 참조 없음: 기존 bendGoal 기반 fallback (팔 등) + kneePos = ComputeKneePosFromBendGoal(limb, upperLen, lowerLen, targetPos); } - IKSolverTrigonometric.Solve( - limb.upper, - limb.lower, - limb.end, - limb.target.position, - bendNormal, - limb.positionWeight - ); + // --- 본 회전 적용 --- + Vector3 currentUpperDir = (limb.lower.position - limb.upper.position).normalized; + Vector3 desiredUpperDir = (kneePos - limb.upper.position).normalized; + limb.upper.rotation = Quaternion.FromToRotation(currentUpperDir, desiredUpperDir) * limb.upper.rotation; + + Vector3 currentLowerDir = (limb.end.position - limb.lower.position).normalized; + Vector3 desiredLowerDir = (targetPos - limb.lower.position).normalized; + limb.lower.rotation = Quaternion.FromToRotation(currentLowerDir, desiredLowerDir) * limb.lower.rotation; if (limb.rotationWeight > 0.001f) { @@ -129,9 +139,89 @@ namespace KindRetargeting } } - private Vector3 GetBendNormal(LimbIK limb) + /// + /// 소스 무릎 위치를 hip→foot 직선 기준으로 분해(투영+수직)한 뒤 + /// 타겟 비율로 스케일하여 타겟 무릎 위치를 결정합니다. + /// 소스가 역관절이면 수직 성분이 반대쪽 → 타겟도 자연스럽게 역관절. + /// + private Vector3 ComputeKneePosFromSource(LimbIK limb, float upperLen, float lowerLen, Vector3 targetPos) { - return limb.upper.rotation * limb.localBendNormal; + // 소스 체인 길이 + float sourceUpperLen = Vector3.Distance(limb.sourceUpper.position, limb.sourceLower.position); + float sourceLowerLen = Vector3.Distance(limb.sourceLower.position, limb.sourceEnd.position); + float sourceChain = sourceUpperLen + sourceLowerLen; + float targetChain = upperLen + lowerLen; + + if (sourceChain < 0.001f || targetChain < 0.001f) + return limb.lower.position; + + // 소스 hip → foot 방향 + Vector3 sourceHipToFoot = limb.sourceEnd.position - limb.sourceUpper.position; + float sourceHipToFootMag = sourceHipToFoot.magnitude; + if (sourceHipToFootMag < 0.001f) + return limb.lower.position; + Vector3 sourceHipToFootDir = sourceHipToFoot / sourceHipToFootMag; + + // 소스 hip → knee 벡터를 투영(직선 성분)과 수직(오프셋 성분)으로 분해 + Vector3 sourceHipToKnee = limb.sourceLower.position - limb.sourceUpper.position; + float projection = Vector3.Dot(sourceHipToKnee, sourceHipToFootDir); + Vector3 rejection = sourceHipToKnee - projection * sourceHipToFootDir; + + // 소스 체인 길이 대비 비율로 정규화 → 타겟 체인 길이로 스케일 + float scale = targetChain / sourceChain; + float scaledProjection = projection * scale; + Vector3 scaledRejection = rejection * scale; + + // 타겟 hip → foot 방향 + Vector3 targetHipToFoot = targetPos - limb.upper.position; + float targetHipToFootMag = targetHipToFoot.magnitude; + if (targetHipToFootMag < 0.001f) + return limb.lower.position; + Vector3 targetHipToFootDir = targetHipToFoot / targetHipToFootMag; + + // 타겟 무릎 위치: 타겟 hip에서 투영 + 수직 성분 적용 + Vector3 kneePos = limb.upper.position + + targetHipToFootDir * scaledProjection + + scaledRejection; + + return kneePos; + } + + /// + /// bendGoal 기반 fallback (소스 참조가 없는 사지용). + /// 기존 cosine law + bendNormal 방식. + /// + private Vector3 ComputeKneePosFromBendGoal(LimbIK limb, float upperLen, float lowerLen, Vector3 targetPos) + { + float chainLength = upperLen + lowerLen; + Vector3 toTarget = targetPos - limb.upper.position; + float targetDist = toTarget.magnitude; + if (targetDist < 0.001f) return limb.lower.position; + Vector3 toTargetDir = toTarget / targetDist; + + Vector3 bendNormal = limb.upper.rotation * limb.localBendNormal; + + if (limb.bendGoal != null && limb.bendGoalWeight > 0.001f) + { + Vector3 goalNormal = Vector3.Cross( + limb.bendGoal.position - limb.upper.position, + targetPos - limb.upper.position + ); + if (goalNormal.sqrMagnitude > 0.01f) + { + bendNormal = Vector3.Lerp(bendNormal, goalNormal.normalized, limb.bendGoalWeight); + } + } + bendNormal.Normalize(); + + float clampedDist = Mathf.Clamp(targetDist, Mathf.Abs(upperLen - lowerLen) + 0.001f, chainLength - 0.001f); + float cosUpper = (clampedDist * clampedDist + upperLen * upperLen - lowerLen * lowerLen) + / (2f * clampedDist * upperLen); + cosUpper = Mathf.Clamp(cosUpper, -1f, 1f); + float upperAngleDeg = Mathf.Acos(cosUpper) * Mathf.Rad2Deg; + + Vector3 kneeDir = Quaternion.AngleAxis(-upperAngleDeg, bendNormal) * toTargetDir; + return limb.upper.position + kneeDir * upperLen; } public float CalculateAutoFloorHeight(float comfortRatio = 0.98f)