using Unity.Collections; namespace UnityEngine.Animations.Rigging { /// /// Utility functions for runtime constraints. /// public static class AnimationRuntimeUtils { const float k_SqrEpsilon = 1e-8f; /// /// Evaluates the Two-Bone IK algorithm. /// /// The animation stream to work on. /// The transform handle for the root transform. /// The transform handle for the mid transform. /// The transform handle for the tip transform. /// The transform handle for the target transform. /// The transform handle for the hint transform. /// The weight for which target position has an effect on IK calculations. This is a value in between 0 and 1. /// The weight for which target rotation has an effect on IK calculations. This is a value in between 0 and 1. /// The weight for which hint transform has an effect on IK calculations. This is a value in between 0 and 1. /// The offset applied to the target transform. public static void SolveTwoBoneIK( AnimationStream stream, ReadWriteTransformHandle root, ReadWriteTransformHandle mid, ReadWriteTransformHandle tip, ReadOnlyTransformHandle target, ReadOnlyTransformHandle hint, float posWeight, float rotWeight, float hintWeight, AffineTransform targetOffset ) { Vector3 aPosition = root.GetPosition(stream); Vector3 bPosition = mid.GetPosition(stream); Vector3 cPosition = tip.GetPosition(stream); target.GetGlobalTR(stream, out Vector3 targetPos, out Quaternion targetRot); Vector3 tPosition = Vector3.Lerp(cPosition, targetPos + targetOffset.translation, posWeight); Quaternion tRotation = Quaternion.Lerp(tip.GetRotation(stream), targetRot * targetOffset.rotation, rotWeight); bool hasHint = hint.IsValid(stream) && 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 < k_SqrEpsilon) { axis = hasHint ? Vector3.Cross(hint.GetPosition(stream) - aPosition, bc) : Vector3.zero; if (axis.sqrMagnitude < k_SqrEpsilon) axis = Vector3.Cross(at, bc); if (axis.sqrMagnitude < k_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.SetRotation(stream, deltaR * mid.GetRotation(stream)); cPosition = tip.GetPosition(stream); ac = cPosition - aPosition; root.SetRotation(stream, QuaternionExt.FromToRotation(ac, at) * root.GetRotation(stream)); if (hasHint) { float acSqrMag = ac.sqrMagnitude; if (acSqrMag > 0f) { bPosition = mid.GetPosition(stream); cPosition = tip.GetPosition(stream); ab = bPosition - aPosition; ac = cPosition - aPosition; Vector3 acNorm = ac / Mathf.Sqrt(acSqrMag); Vector3 ah = hint.GetPosition(stream) - 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 = QuaternionExt.FromToRotation(abProj, ahProj); hintR.x *= hintWeight; hintR.y *= hintWeight; hintR.z *= hintWeight; hintR = QuaternionExt.NormalizeSafe(hintR); root.SetRotation(stream, hintR * root.GetRotation(stream)); } } } tip.SetRotation(stream, tRotation); } /// /// Sets the position for a hint and target given bone positions. /// /// The animation stream to work on. /// The transform handle for the root transform. /// The transform handle for the mid transform. /// The transform handle for the tip transform. /// The transform handle for the target transform. /// The transform handle for the hint transform. /// The weight for which target position has an effect on IK calculations. This is a value in between 0 and 1. /// The weight for which target rotation has an effect on IK calculations. This is a value in between 0 and 1. /// The weight for which hint transform has an effect on IK calculations. This is a value in between 0 and 1. /// The offset applied to the target transform. public static void InverseSolveTwoBoneIK( AnimationStream stream, ReadOnlyTransformHandle root, ReadOnlyTransformHandle mid, ReadOnlyTransformHandle tip, ReadWriteTransformHandle target, ReadWriteTransformHandle hint, float posWeight, float rotWeight, float hintWeight, AffineTransform targetOffset ) { Vector3 rootPosition = root.GetPosition(stream); Vector3 midPosition = mid.GetPosition(stream); tip.GetGlobalTR(stream, out var tipPosition, out var tipRotation); target.GetGlobalTR(stream, out var targetPosition, out var targetRotation); bool isHintValid = hint.IsValid(stream); Vector3 hintPosition = Vector3.zero; if(isHintValid) hintPosition = hint.GetPosition(stream); InverseSolveTwoBoneIK(rootPosition, midPosition, tipPosition, tipRotation, ref targetPosition, ref targetRotation, ref hintPosition, isHintValid, posWeight, rotWeight, hintWeight, targetOffset); target.SetPosition(stream, targetPosition); target.SetRotation(stream, targetRotation); hint.SetPosition(stream, hintPosition); } /// /// Sets the position for a hint and target for given bone positions. /// /// The position of the root bone. /// The position of the mid bone. /// The position of the tip bone. /// The rotation of the tip bone. /// The position of the target. /// The rotation of the target. /// The position of the hint. /// Whether the hint position should be set. /// The weight for which target position has an effect on IK calculations. This is a value in between 0 and 1. /// The weight for which target rotation has an effect on IK calculations. This is a value in between 0 and 1. /// The weight for which hint transform has an effect on IK calculations. This is a value in between 0 and 1. /// The offset applied to the target transform. public static void InverseSolveTwoBoneIK( Vector3 rootPosition, Vector3 midPosition, Vector3 tipPosition, Quaternion tipRotation, ref Vector3 targetPosition, ref Quaternion targetRotation, ref Vector3 hintPosition, bool isHintValid, float posWeight, float rotWeight, float hintWeight, AffineTransform targetOffset ) { targetPosition = (posWeight > 0f) ? tipPosition + targetOffset.translation : targetPosition; targetRotation = (rotWeight > 0f) ? tipRotation * targetOffset.rotation : targetRotation; if (isHintValid) { var ac = tipPosition - rootPosition; var ab = midPosition - rootPosition; var bc = tipPosition - midPosition; float abLen = ab.magnitude; float bcLen = bc.magnitude; var acSqrMag = Vector3.Dot(ac, ac); var projectionPoint = rootPosition; if (acSqrMag > k_SqrEpsilon) projectionPoint += Vector3.Dot(ab / acSqrMag, ac) * ac; var poleVectorDirection = midPosition - projectionPoint; var scale = abLen + bcLen; hintPosition = (hintWeight > 0f) ? projectionPoint + (poleVectorDirection.normalized * scale) : hintPosition; } } 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); } /// /// Evaluates the FABRIK ChainIK algorithm. /// /// Uninitialized buffer of positions. linkPositions and linkLengths must have the same size. /// Array of distances in between positions. linkPositions and linkLenghts must have the same size. /// Target position. /// The maximum distance the resulting position and initial target are allowed to have in between them. /// The maximum distance the Transform chain can reach. /// The maximum number of iterations allowed for the ChainIK algorithm to converge to a solution. /// Returns true if ChainIK calculations were successful. False otherwise. /// /// Implementation of unconstrained FABRIK solver : Forward and Backward Reaching Inverse Kinematic /// Aristidou A, Lasenby J. FABRIK: a fast, iterative solver for the inverse kinematics problem. Graphical Models 2011; 73(5): 243–260. /// public static bool SolveFABRIK( ref NativeArray linkPositions, ref NativeArray linkLengths, Vector3 target, float tolerance, float maxReach, int maxIterations ) { // If the target is unreachable var rootToTargetDir = target - linkPositions[0]; if (rootToTargetDir.sqrMagnitude > Square(maxReach)) { // Line up chain towards target var dir = rootToTargetDir.normalized; for (int i = 1; i < linkPositions.Length; ++i) linkPositions[i] = linkPositions[i - 1] + dir * linkLengths[i - 1]; return true; } else { int tipIndex = linkPositions.Length - 1; float sqrTolerance = Square(tolerance); if (SqrDistance(linkPositions[tipIndex], target) > sqrTolerance) { var rootPos = linkPositions[0]; int iteration = 0; do { // Forward reaching phase // Set tip to target and propagate displacement to rest of chain linkPositions[tipIndex] = target; for (int i = tipIndex - 1; i > -1; --i) linkPositions[i] = linkPositions[i + 1] + ((linkPositions[i] - linkPositions[i + 1]).normalized * linkLengths[i]); // Backward reaching phase // Set root back at it's original position and propagate displacement to rest of chain linkPositions[0] = rootPos; for (int i = 1; i < linkPositions.Length; ++i) linkPositions[i] = linkPositions[i - 1] + ((linkPositions[i] - linkPositions[i - 1]).normalized * linkLengths[i - 1]); } while ((SqrDistance(linkPositions[tipIndex], target) > sqrTolerance) && (++iteration < maxIterations)); return true; } } return false; } /// /// Returns the square length between two vectors. /// /// Vector3 value. /// Vector3 value. /// Square length between lhs and rhs. public static float SqrDistance(Vector3 lhs, Vector3 rhs) { return (rhs - lhs).sqrMagnitude; } /// /// Returns the square value of a float. /// /// Float value. /// Squared value. public static float Square(float value) { return value * value; } /// /// Linearly interpolates between two vectors using a vector interpolant. /// /// Start Vector3 value. /// End Vector3 value. /// Interpolant Vector3 value. /// Interpolated value. public static Vector3 Lerp(Vector3 a, Vector3 b, Vector3 t) { return Vector3.Scale(a, Vector3.one - t) + Vector3.Scale(b, t); } /// /// Returns b if c is greater than zero, a otherwise. /// /// First float value. /// Second float value. /// Comparator float value. /// Selected float value. public static float Select(float a, float b, float c) { return (c > 0f) ? b : a; } /// /// Returns a componentwise selection between two vectors a and b based on a vector selection mask c. /// Per component, the component from b is selected when c is greater than zero, otherwise the component from a is selected. /// /// First Vector3 value. /// Second Vector3 value. /// Comparator Vector3 value. /// Selected Vector3 value. public static Vector3 Select(Vector3 a, Vector3 b, Vector3 c) { return new Vector3(Select(a.x, b.x, c.x), Select(a.y, b.y, c.y), Select(a.z, b.z, c.z)); } /// /// Projects a vector onto a plane defined by a normal orthogonal to the plane. /// /// The location of the vector above the plane. /// The direction from the vector towards the plane. /// The location of the vector on the plane. public static Vector3 ProjectOnPlane(Vector3 vector, Vector3 planeNormal) { float sqrMag = Vector3.Dot(planeNormal, planeNormal); var dot = Vector3.Dot(vector, planeNormal); return new Vector3(vector.x - planeNormal.x * dot / sqrMag, vector.y - planeNormal.y * dot / sqrMag, vector.z - planeNormal.z * dot / sqrMag); } internal static float Sum(AnimationJobCache cache, CacheIndex index, int count) { if (count == 0) return 0f; float sum = 0f; for (int i = 0; i < count; ++i) sum += cache.GetRaw(index, i); return sum; } /// /// Calculates the sum of all float elements in the array. /// /// An array of float elements. /// Sum of all float elements. public static float Sum(NativeArray floatBuffer) { if (floatBuffer.Length == 0) return 0f; float sum = 0f; for (int i = 0; i< floatBuffer.Length; ++i) { sum += floatBuffer[i]; } return sum; } /// /// Copies translation, rotation and scale values from specified Transform handle to stream. /// /// The animation stream to work on. /// The transform handle to copy. public static void PassThrough(AnimationStream stream, ReadWriteTransformHandle handle) { handle.GetLocalTRS(stream, out Vector3 position, out Quaternion rotation, out Vector3 scale); handle.SetLocalTRS(stream, position, rotation, scale); } } }