From 097678e5725bce357726aa8ad091c0e841d91aeb Mon Sep 17 00:00:00 2001 From: KINDNICK <68893236+KINDNICK@users.noreply.github.com> Date: Mon, 20 Oct 2025 23:21:37 +0900 Subject: [PATCH] =?UTF-8?q?ADD=20:=201=EC=B0=A8=20=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A0=88=EC=B9=98=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FinalIK/IK Solvers/IKSolverVRArm.cs | 24 +++++-- .../FinalIK/IK Solvers/IKSolverVRLeg.cs | 24 +++++-- .../FinalIK/IK Solvers/IKSolverVRUtilities.cs | 72 +++++++++++++++++-- 3 files changed, 106 insertions(+), 14 deletions(-) diff --git a/Assets/External/RootMotion/FinalIK/IK Solvers/IKSolverVRArm.cs b/Assets/External/RootMotion/FinalIK/IK Solvers/IKSolverVRArm.cs index 69a53215..f5d0b8b3 100644 --- a/Assets/External/RootMotion/FinalIK/IK Solvers/IKSolverVRArm.cs +++ b/Assets/External/RootMotion/FinalIK/IK Solvers/IKSolverVRArm.cs @@ -129,6 +129,22 @@ namespace RootMotion.FinalIK /// public AnimationCurve stretchCurve = new AnimationCurve(); + [LargeHeader("Struggle (Anti-Popping)")] + + [Tooltip("When the arm reaches near maximum extension, this feature smoothly transitions to prevent sudden 'popping' or 'snapping'. Start value represents the percentage of arm length (0.99 = 99%) where the smoothing begins. Inspired by hvr-ik's struggle mechanism.")] + /// + /// When the arm reaches near maximum extension, this feature smoothly transitions to prevent sudden 'popping' or 'snapping'. Start value represents the percentage of arm length where the smoothing begins. + /// + [Range(0.9f, 1.0f)] + public float struggleStart = 0.99f; + + [Tooltip("The end threshold for the struggle zone. Represents the percentage of arm length (1.04 = 104%) where the smoothing ends. The arm will smoothly approach full extension within the struggle zone (struggleStart to struggleEnd).")] + /// + /// The end threshold for the struggle zone. The arm will smoothly approach full extension within this range. + /// + [Range(1.0f, 1.2f)] + public float struggleEnd = 1.04f; + /// /// Target position of the hand. Will be overwritten if target is assigned. /// @@ -305,15 +321,15 @@ namespace RootMotion.FinalIK if (LOD < 1) Stretching(); Vector3 bendNormal = GetBendNormal(position - upperArm.solverPosition); - - // 팔 부분만 IK 적용 + + // 팔 부분만 IK 적용 (Struggle 메커니즘 포함) if (hasShoulder) { - VirtualBone.SolveTrigonometric(bones, 1, 2, 3, position, bendNormal, positionWeight); + VirtualBone.SolveTrigonometric(bones, 1, 2, 3, position, bendNormal, positionWeight, struggleStart, struggleEnd); } else { - VirtualBone.SolveTrigonometric(bones, 0, 1, 2, position, bendNormal, positionWeight); + VirtualBone.SolveTrigonometric(bones, 0, 1, 2, position, bendNormal, positionWeight, struggleStart, struggleEnd); } // 손 회전 적용 diff --git a/Assets/External/RootMotion/FinalIK/IK Solvers/IKSolverVRLeg.cs b/Assets/External/RootMotion/FinalIK/IK Solvers/IKSolverVRLeg.cs index 587acaee..021a7681 100644 --- a/Assets/External/RootMotion/FinalIK/IK Solvers/IKSolverVRLeg.cs +++ b/Assets/External/RootMotion/FinalIK/IK Solvers/IKSolverVRLeg.cs @@ -76,6 +76,22 @@ namespace RootMotion.FinalIK { /// public AnimationCurve stretchCurve = new AnimationCurve(); + [LargeHeader("Struggle (Anti-Popping)")] + + [Tooltip("When the leg reaches near maximum extension, this feature smoothly transitions to prevent sudden 'popping' or 'snapping'. Start value represents the percentage of leg length (0.99 = 99%) where the smoothing begins. Inspired by hvr-ik's struggle mechanism.")] + /// + /// When the leg reaches near maximum extension, this feature smoothly transitions to prevent sudden 'popping' or 'snapping'. Start value represents the percentage of leg length where the smoothing begins. + /// + [Range(0.9f, 1.0f)] + public float struggleStart = 0.99f; + + [Tooltip("The end threshold for the struggle zone. Represents the percentage of leg length (1.04 = 104%) where the smoothing ends. The leg will smoothly approach full extension within the struggle zone (struggleStart to struggleEnd).")] + /// + /// The end threshold for the struggle zone. The leg will smoothly approach full extension within this range. + /// + [Range(1.0f, 1.2f)] + public float struggleEnd = 1.04f; + /// /// Target position of the toe/foot. Will be overwritten if target is assigned. /// @@ -283,8 +299,8 @@ namespace RootMotion.FinalIK { public void Solve(bool stretch) { if (stretch && LOD < 1) Stretching(); - // Foot pass - VirtualBone.SolveTrigonometric(bones, 0, 1, 2, footPosition, bendNormal, 1f); + // Foot pass (with Struggle mechanism) + VirtualBone.SolveTrigonometric(bones, 0, 1, 2, footPosition, bendNormal, 1f, struggleStart, struggleEnd); // Rotate foot back to where it was before the last solving RotateTo(foot, footRotation); @@ -295,10 +311,10 @@ namespace RootMotion.FinalIK { FixTwistRotations(); return; } - + Vector3 b = Vector3.Cross(foot.solverPosition - thigh.solverPosition, toes.solverPosition - foot.solverPosition).normalized; - VirtualBone.SolveTrigonometric(bones, 0, 2, 3, position, b, 1f); + VirtualBone.SolveTrigonometric(bones, 0, 2, 3, position, b, 1f, struggleStart, struggleEnd); // Fix thigh twist relative to target rotation FixTwistRotations(); diff --git a/Assets/External/RootMotion/FinalIK/IK Solvers/IKSolverVRUtilities.cs b/Assets/External/RootMotion/FinalIK/IK Solvers/IKSolverVRUtilities.cs index 1fc58371..db7dd896 100644 --- a/Assets/External/RootMotion/FinalIK/IK Solvers/IKSolverVRUtilities.cs +++ b/Assets/External/RootMotion/FinalIK/IK Solvers/IKSolverVRUtilities.cs @@ -133,23 +133,83 @@ namespace RootMotion.FinalIK { // Direction of the limb in solver targetPosition = Vector3.Lerp(bones[third].solverPosition, targetPosition, weight); - + Vector3 dir = targetPosition - bones[first].solverPosition; - + // Distance between the first and the last transform solver positions float sqrMag = dir.sqrMagnitude; if (sqrMag == 0f) return; float length = Mathf.Sqrt(sqrMag); - + float sqrMag1 = (bones[second].solverPosition - bones[first].solverPosition).sqrMagnitude; float sqrMag2 = (bones[third].solverPosition - bones[second].solverPosition).sqrMagnitude; - + // Get the general world space bending direction Vector3 bendDir = Vector3.Cross(dir, bendNormal); - + // Get the direction to the trigonometrically solved position of the second transform Vector3 toBendPoint = GetDirectionToBendPoint(dir, length, bendDir, sqrMag1, sqrMag2); - + + // Position the second transform + Quaternion q1 = Quaternion.FromToRotation(bones[second].solverPosition - bones[first].solverPosition, toBendPoint); + if (weight < 1f) q1 = Quaternion.Lerp(Quaternion.identity, q1, weight); + + RotateAroundPoint(bones, first, bones[first].solverPosition, q1); + + Quaternion q2 = Quaternion.FromToRotation(bones[third].solverPosition - bones[second].solverPosition, targetPosition - bones[second].solverPosition); + if (weight < 1f) q2 = Quaternion.Lerp(Quaternion.identity, q2, weight); + + RotateAroundPoint(bones, second, bones[second].solverPosition, q2); + } + + // Overload with struggle parameters (inspired by hvr-ik) + /// + /// Solve the bone chain virtually with struggle mechanism to prevent popping when near maximum extension. + /// + public static void SolveTrigonometric(VirtualBone[] bones, int first, int second, int third, Vector3 targetPosition, Vector3 bendNormal, float weight, float struggleStart, float struggleEnd) { + if (weight <= 0f) return; + + // Direction of the limb in solver + targetPosition = Vector3.Lerp(bones[third].solverPosition, targetPosition, weight); + + Vector3 rootPos = bones[first].solverPosition; + float limbLength1 = Mathf.Sqrt((bones[second].solverPosition - bones[first].solverPosition).sqrMagnitude); + float limbLength2 = Mathf.Sqrt((bones[third].solverPosition - bones[second].solverPosition).sqrMagnitude); + float totalLength = limbLength1 + limbLength2; + + // Apply Struggle Correction (hvr-ik style) + float distance = Vector3.Distance(rootPos, targetPosition); + if (distance >= totalLength * struggleStart) { + float finalLength; + if (struggleStart != struggleEnd) { + // Map distance to 0-1 in the struggle zone + float lerpAmount = Mathf.Clamp01((distance - totalLength * struggleStart) / (totalLength * struggleEnd - totalLength * struggleStart)); + // Use pow(1-x, 4) curve for smooth transition (capacitor charge curve) + float easedAmount = 1f - Mathf.Pow(1f - lerpAmount, 4f); + finalLength = Mathf.Lerp(totalLength * struggleStart, totalLength, easedAmount); + } else { + finalLength = totalLength; + } + + // Pull target position inward to the corrected distance + targetPosition = rootPos + (targetPosition - rootPos).normalized * finalLength; + } + + // Continue with original trigonometric solving + Vector3 dir = targetPosition - rootPos; + float sqrMag = dir.sqrMagnitude; + if (sqrMag == 0f) return; + float length = Mathf.Sqrt(sqrMag); + + float sqrMag1 = limbLength1 * limbLength1; + float sqrMag2 = limbLength2 * limbLength2; + + // Get the general world space bending direction + Vector3 bendDir = Vector3.Cross(dir, bendNormal); + + // Get the direction to the trigonometrically solved position of the second transform + Vector3 toBendPoint = GetDirectionToBendPoint(dir, length, bendDir, sqrMag1, sqrMag2); + // Position the second transform Quaternion q1 = Quaternion.FromToRotation(bones[second].solverPosition - bones[first].solverPosition, toBendPoint); if (weight < 1f) q1 = Quaternion.Lerp(Quaternion.identity, q1, weight);