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);