// Designed by KINEMATION, 2024. using UnityEngine; namespace KINEMATION.MotionWarping.Runtime.Core { public class TwoBoneIk { private const float FloatMin = 1e-10f; private const float SqrEpsilon = 1e-8f; public static float TriangleAngle(float aLen, float aLen1, float aLen2) { float c = Mathf.Clamp((aLen1 * aLen1 + aLen2 * aLen2 - aLen * aLen) / (aLen1 * aLen2) / 2.0f, -1.0f, 1.0f); return Mathf.Acos(c); } public static Quaternion FromToRotation(Vector3 from, Vector3 to) { float theta = Vector3.Dot(from.normalized, to.normalized); if (theta >= 1f) return Quaternion.identity; if (theta <= -1f) { Vector3 axis = Vector3.Cross(from, Vector3.right); if (axis.sqrMagnitude == 0f) axis = Vector3.Cross(from, Vector3.up); return Quaternion.AngleAxis(180f, axis); } return Quaternion.AngleAxis(Mathf.Acos(theta) * Mathf.Rad2Deg, Vector3.Cross(from, to).normalized); } public static Quaternion NormalizeSafe(Quaternion q) { float dot = Quaternion.Dot(q, q); if (dot > FloatMin) { float rsqrt = 1.0f / Mathf.Sqrt(dot); return new Quaternion(q.x * rsqrt, q.y * rsqrt, q.z * rsqrt, q.w * rsqrt); } return Quaternion.identity; } public static void SolveTwoBoneIK( Transform root, Transform mid, Transform tip, (Vector3, Quaternion) target, Transform hint, float targetWeight, float hintWeight ) { if (Mathf.Approximately(targetWeight, 0f)) { return; } Vector3 aPosition = root.position; Vector3 bPosition = mid.position; Vector3 cPosition = tip.position; Vector3 tPosition = Vector3.Lerp(cPosition, target.Item1, targetWeight); Quaternion tRotation = Quaternion.Lerp(tip.rotation, target.Item2, targetWeight); bool hasHint = hint != null && hintWeight > 0f; Vector3 ab = bPosition - aPosition; Vector3 bc = cPosition - bPosition; Vector3 ac = cPosition - aPosition; Vector3 at = tPosition - aPosition; float abLen = ab.magnitude; float bcLen = bc.magnitude; float acLen = ac.magnitude; float atLen = at.magnitude; float oldAbcAngle = TriangleAngle(acLen, abLen, bcLen); float newAbcAngle = TriangleAngle(atLen, abLen, bcLen); // Bend normal strategy is to take whatever has been provided in the animation // stream to minimize configuration changes, however if this is collinear // try computing a bend normal given the desired target position. // If this also fails, try resolving axis using hint if provided. Vector3 axis = Vector3.Cross(ab, bc); if (axis.sqrMagnitude < SqrEpsilon) { axis = hasHint ? Vector3.Cross(hint.position - aPosition, bc) : Vector3.zero; if (axis.sqrMagnitude < SqrEpsilon) axis = Vector3.Cross(at, bc); if (axis.sqrMagnitude < SqrEpsilon) axis = Vector3.up; } axis = Vector3.Normalize(axis); float a = 0.5f * (oldAbcAngle - newAbcAngle); float sin = Mathf.Sin(a); float cos = Mathf.Cos(a); Quaternion deltaR = new Quaternion(axis.x * sin, axis.y * sin, axis.z * sin, cos); mid.rotation = deltaR * mid.rotation; cPosition = tip.position; ac = cPosition - aPosition; root.rotation = FromToRotation(ac, at) * root.rotation; if (hasHint) { float acSqrMag = ac.sqrMagnitude; if (acSqrMag > 0f) { bPosition = mid.position; cPosition = tip.position; ab = bPosition - aPosition; ac = cPosition - aPosition; Vector3 acNorm = ac / Mathf.Sqrt(acSqrMag); Vector3 ah = hint.position - aPosition; Vector3 abProj = ab - acNorm * Vector3.Dot(ab, acNorm); Vector3 ahProj = ah - acNorm * Vector3.Dot(ah, acNorm); float maxReach = abLen + bcLen; if (abProj.sqrMagnitude > (maxReach * maxReach * 0.001f) && ahProj.sqrMagnitude > 0f) { Quaternion hintR = FromToRotation(abProj, ahProj); hintR.x *= hintWeight; hintR.y *= hintWeight; hintR.z *= hintWeight; hintR = NormalizeSafe(hintR); root.rotation = hintR * root.rotation; } } } tip.rotation = tRotation; } } }