using UnityEngine; namespace KindRetargeting { /// /// 커스텀 Two-Bone IK 솔버. /// 소스 무릎 위치를 비율 스케일하여 타겟 무릎을 직접 배치합니다. /// cosine law 없이 동작하므로 180° 특이점이 없고 역관절도 자연스럽게 지원합니다. /// [System.Serializable] public class TwoBoneIKSolver { [System.Serializable] public class LimbIK { public bool enabled = true; public Transform target; public Transform bendGoal; [Range(0f, 1f)] public float positionWeight = 0f; [Range(0f, 1f)] public float rotationWeight = 0f; [Range(0f, 1f)] public float bendGoalWeight = 0f; [HideInInspector] public Transform upper; [HideInInspector] public Transform lower; [HideInInspector] public Transform end; [HideInInspector] public float upperLength; [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; [Header("팔")] public LimbIK leftArm = new LimbIK(); public LimbIK rightArm = new LimbIK(); [Header("다리")] public LimbIK leftLeg = new LimbIK(); public LimbIK rightLeg = new LimbIK(); private bool isInitialized; public void Initialize(Animator targetAnimator) { animator = targetAnimator; if (animator == null || !animator.isHuman) return; CacheLimb(leftArm, HumanBodyBones.LeftUpperArm, HumanBodyBones.LeftLowerArm, HumanBodyBones.LeftHand); CacheLimb(rightArm, HumanBodyBones.RightUpperArm, HumanBodyBones.RightLowerArm, HumanBodyBones.RightHand); CacheLimb(leftLeg, HumanBodyBones.LeftUpperLeg, HumanBodyBones.LeftLowerLeg, HumanBodyBones.LeftFoot); CacheLimb(rightLeg, HumanBodyBones.RightUpperLeg, HumanBodyBones.RightLowerLeg, HumanBodyBones.RightFoot); isInitialized = true; } private void CacheLimb(LimbIK limb, HumanBodyBones upperBone, HumanBodyBones lowerBone, HumanBodyBones endBone) { limb.upper = animator.GetBoneTransform(upperBone); limb.lower = animator.GetBoneTransform(lowerBone); limb.end = animator.GetBoneTransform(endBone); if (limb.upper == null || limb.lower == null || limb.end == null) return; limb.upperLength = Vector3.Distance(limb.upper.position, limb.lower.position); limb.lowerLength = Vector3.Distance(limb.lower.position, limb.end.position); Vector3 ab = limb.lower.position - limb.upper.position; Vector3 bc = limb.end.position - limb.lower.position; Vector3 bendNormal = Vector3.Cross(ab, bc); if (bendNormal.sqrMagnitude < 0.0001f) { bendNormal = Vector3.Cross(ab.normalized, Vector3.up); if (bendNormal.sqrMagnitude < 0.0001f) bendNormal = Vector3.Cross(ab.normalized, Vector3.forward); } bendNormal.Normalize(); limb.localBendNormal = Quaternion.Inverse(limb.upper.rotation) * bendNormal; } public void OnUpdate() { if (!isInitialized) return; SolveLimb(leftArm); SolveLimb(rightArm); SolveLimb(leftLeg); SolveLimb(rightLeg); } private void SolveLimb(LimbIK limb) { if (!limb.enabled || limb.target == null || limb.positionWeight < 0.001f) return; if (limb.upper == null || limb.lower == null || limb.end == null) return; float upperLen = Vector3.Distance(limb.upper.position, limb.lower.position); float lowerLen = Vector3.Distance(limb.lower.position, limb.end.position); Vector3 targetPos = Vector3.Lerp(limb.end.position, limb.target.position, limb.positionWeight); // --- 무릎 위치 결정 --- Vector3 kneePos; if (limb.sourceUpper != null && limb.sourceLower != null && limb.sourceEnd != null) { // 소스 무릎 위치 기반: 소스의 무릎 위치를 타겟 비율로 스케일 kneePos = ComputeKneePosFromSource(limb, upperLen, lowerLen, targetPos); } else { // 소스 참조 없음: 기존 bendGoal 기반 fallback (팔 등) kneePos = ComputeKneePosFromBendGoal(limb, upperLen, lowerLen, targetPos); } // --- 본 회전 적용 --- 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) { limb.end.rotation = Quaternion.Slerp( limb.end.rotation, limb.target.rotation, limb.rotationWeight ); } } /// /// 소스 무릎 위치를 hip→foot 직선 기준으로 분해(투영+수직)한 뒤 /// 타겟 비율로 스케일하여 타겟 무릎 위치를 결정합니다. /// 소스가 역관절이면 수직 성분이 반대쪽 → 타겟도 자연스럽게 역관절. /// private Vector3 ComputeKneePosFromSource(LimbIK limb, float upperLen, float lowerLen, Vector3 targetPos) { // 소스 체인 길이 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) { if (animator == null || leftLeg.upper == null || leftLeg.lower == null || leftLeg.end == null) return 0f; float upperLen = Vector3.Distance(leftLeg.upper.position, leftLeg.lower.position); float lowerLen = Vector3.Distance(leftLeg.lower.position, leftLeg.end.position); float totalLegLength = upperLen + lowerLen; float comfortHeight = totalLegLength * comfortRatio; Transform hips = animator.GetBoneTransform(HumanBodyBones.Hips); Transform foot = leftLeg.end; if (hips == null || foot == null) return 0f; float currentHipToFoot = hips.position.y - foot.position.y; float heightDiff = currentHipToFoot - comfortHeight; return -heightDiff; } } }