Files
CartoonRPG/Assets/KINEMATION/MotionWarping/Runtime/Core/TwoBoneIk.cs
Caleb Sandford deQuincey 02c4b5ab66 Adding in new packages
2025-06-05 22:59:18 +01:00

142 lines
5.1 KiB
C#

// 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;
}
}
}