01 Two Bone IK

Difficulty: Intermediate

Location: Assets/Plugins/Animancer/Examples/10 Animation Jobs/01 Two Bone IK

Namespace: Animancer.Examples.Jobs

This example demonstrates how the TwoBoneIKJob from the Animation Jobs Samples can be used with Animancer. Unlike Unity's inbuilt Inverse Kinematics system which only works for Humanoid Rigs, this allows you to apply IK to a Generic Rig as well. This IK system could be used in a real project, however the Animation Rigging package contains a much better system which also uses Animation Jobs and can be used alongside Animancer without any modification.

Scripts

This example uses two scripts:

  1. The job itself is a struct which implements the IAnimationJob interface. In this case, that's the TwoBoneIKJob script which was taken directly from the Animation Jobs Samples without any real modifications. The contents of this script will not be explained here since this example is about how to use jobs, not how to implement IK.

    using UnityEngine;
    
    #if UNITY_2019_3_OR_NEWER
    using UnityEngine.Animations;
    #else
    using UnityEngine.Experimental.Animations;
    #endif
    
    public struct TwoBoneIKJob : IAnimationJob
    {
        public TransformSceneHandle effector;
    
        public TransformStreamHandle top;
        public TransformStreamHandle mid;
        public TransformStreamHandle low;
    
        public void Setup(Animator animator, Transform topX, Transform midX, Transform lowX, Transform effectorX)
        {
            top = animator.BindStreamTransform(topX);
            mid = animator.BindStreamTransform(midX);
            low = animator.BindStreamTransform(lowX);
    
            effector = animator.BindSceneTransform(effectorX);
        }
    
        public void ProcessRootMotion(AnimationStream stream)
        {
        }
    
        public void ProcessAnimation(AnimationStream stream)
        {
            Solve(stream, top, mid, low, effector);
        }
    
        /// <summary>
        /// Returns the angle needed between v1 and v2 so that their extremities are
        /// spaced with a specific length.
        /// </summary>
        /// <returns>The angle between v1 and v2.</returns>
        /// <param name="aLen">The desired length between the extremities of v1 and v2.</param>
        /// <param name="v1">First triangle edge.</param>
        /// <param name="v2">Second triangle edge.</param>
        private static float TriangleAngle(float aLen, Vector3 v1, Vector3 v2)
        {
            float aLen1 = v1.magnitude;
            float aLen2 = v2.magnitude;
            float c = Mathf.Clamp((aLen1 * aLen1 + aLen2 * aLen2 - aLen * aLen) / (aLen1 * aLen2) / 2.0f, -1.0f, 1.0f);
            return Mathf.Acos(c);
        }
    
        private static void Solve(AnimationStream stream, TransformStreamHandle topHandle, TransformStreamHandle midHandle,TransformStreamHandle lowHandle, TransformSceneHandle effectorHandle)
        {
            Quaternion aRotation = topHandle.GetRotation(stream);
            Quaternion bRotation = midHandle.GetRotation(stream);
            Quaternion eRotation = effectorHandle.GetRotation(stream);
    
            Vector3 aPosition = topHandle.GetPosition(stream);
            Vector3 bPosition = midHandle.GetPosition(stream);
            Vector3 cPosition = lowHandle.GetPosition(stream);
            Vector3 ePosition = effectorHandle.GetPosition(stream);
    
            Vector3 ab = bPosition - aPosition;
            Vector3 bc = cPosition - bPosition;
            Vector3 ac = cPosition - aPosition;
            Vector3 ae = ePosition - aPosition;
    
            float abcAngle = TriangleAngle(ac.magnitude, ab, bc);
            float abeAngle = TriangleAngle(ae.magnitude, ab, bc);
            float angle = (abcAngle - abeAngle) * Mathf.Rad2Deg;
            Vector3 axis = Vector3.Cross(ab, bc).normalized;
    
            Quaternion fromToRotation = Quaternion.AngleAxis(angle, axis);
    
            Quaternion worldQ = fromToRotation * bRotation;
            midHandle.SetRotation(stream, worldQ);
    
            cPosition = lowHandle.GetPosition(stream);
            ac = cPosition - aPosition;
            Quaternion fromTo = Quaternion.FromToRotation(ac, ae);
            topHandle.SetRotation(stream, fromTo * aRotation);
    
            lowHandle.SetRotation(stream, eRotation);
        }
    }
    

  2. Another script which creates the job and manages access to it. In this case, that's the TwoBoneIK script which is a modified version of the sample TwoBoneIK script which has been adapted for use with Animancer:

    using UnityEngine;
    
    public class TwoBoneIK : MonoBehaviour
    {
        [SerializeField] private AnimancerComponent _Animancer;
        [SerializeField] private Transform _EndBone;
        [SerializeField] private Transform _Target;
    
        private void Awake()
        {
            var midBone = _EndBone.parent;
            var topBone = midBone.parent;
    
            var twoBoneIKJob = new TwoBoneIKJob();
            twoBoneIKJob.Setup(_Animancer.Animator, topBone, midBone, _EndBone, _Target);
    
            _Animancer.Playable.InsertOutputJob(twoBoneIKJob);
        }
    }
    

Responsibilities

Since the original TwoBoneIK script was just a sample, it contained a variety of responsibilities which would not need to be squeezed into a single script in a real system like Animancer. The following table compares the differences between the original and the TwoBoneIK script used in this example:

Original Animancer
Loads an AnimationClip from a hard coded resource path. Uses a NamedAnimancerComponent so the animation can be assigned in the Inspector.
Returns early if any of the references are null to avoid causing exceptions. The script serves no purpose if the references are null so it's better to cause exceptions instead of having it silently fail to work.
Creates a sphere in the script which can be moved around using the scene gizmos to determine the IK target. Uses a serialized field to reference a target object since there is no point in hard-coding it in the script and uses the MouseDrag script from the Puppet example to allow the target to be dragged around in the Game view.
Creates a PlayableGraph to play the animation and manually destroys it in OnDisable. The job is inserted into Animancer's existing graph.
Creates a AnimationScriptPlayable to run the TwoBoneIKJob and connects it to the AnimationClipPlayable of the animation and the graph output. Uses AnimancerPlayable.InsertOutputJob to have animancer do all that internally.

Fields

Code Inspector

While the reduced complexity from the original script is nice, the most important improvement is actually the fact that the job gets inserted into the PlayableGraph managed by Animancer. The original had the script create its own graph to play its own animation which meant that only one instance could actually be used per character to apply IK to one limb, but by inserting the job into an existing graph we have made it possible to use multiple instances of the script to apply IK to multiple limbs.

[SerializeField]
private AnimancerComponent _Animancer;

[SerializeField]
private Transform _EndBone;

[SerializeField]
private Transform _Target;

Initialisation

The first thing we need to do is get the bones we want to apply the IK to, simply using the parent of the _EndBone we assigned in the Inspector (midBone) and the parent of that bone (topBone):

private void Awake()
{
    var midBone = _EndBone.parent;
    var topBone = midBone.parent;

Then we create the TwoBoneIKJob and call its Setup method to specify the Animator, bones, and target object:

    var twoBoneIKJob = new TwoBoneIKJob();
    twoBoneIKJob.Setup(_Animancer.Animator, topBone, midBone, _EndBone, _Target);

And finally, we call AnimancerPlayable.InsertOutputJob to insert it in Animancer's PlayableGraph between the finaly output and whatever playable was previously connected to the output:

    _Animancer.Playable.InsertOutputJob(twoBoneIKJob);
}

After that, Animancer will still be able to play animations and do everything else normally, except that every frame before the animations are applied to the model the TwoBoneIKJob.ProcessAnimation will be executed (once for each of the 4 instances we set up to control the 4 legs of the SpiderBot). That method does some math in order to determine how it needs to rotate the midBone and topBone in order to bring the position of the _EndBone as close to the position of the _Target as possible.

Conclusion

As noted earlier, this IK system could be used in a real project, however the Animation Rigging package contains a much better system which also uses Animation Jobs and can be used alongside Animancer without any modification.