10-01 Two Bone IK

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

Recommended After: Puppet

Learning Outcomes: in this example you will learn:

How to apply Inverse Kinematics to a Generic Rig.

How to use the TwoBoneIKJob from the Animation Jobs Samples.

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.

Pro-Only Features are used in this example: Animation Jobs. Animancer Lite allows you to try out these features in the Unity Editor, but they're not available in runtime builds unless you purchase Animancer Pro.

Summary

  1. Define a job by making a struct that implements IAnimationJob.
  2. On startup, make an instance of your job and configure its fields as necessary: var job = new MyJobStruct();.
  3. Add the job to Animancer using animancerComponent.Playable.InsertOutputJob(job);.
  • The job in this example could be used in a real project, however the Animation Rigging package contains a much more powerful system which also uses Animation Jobs and can be used alongside Animancer without any modification.

Overview

This example uses two new scripts:

  • The TwoBoneIK component applies IK to one limb, so the SpiderBot has 4 of them.
  • Each TwoBoneIK adds a TwoBoneIKJob to Animancer's PlayableGraph, which is where the actual IK algorithm is implemented.

It also reuses the MouseDrag and TransformResetter scripts from the Puppet example.

Job Script

An Animation Job 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 notable modifications. The contents of the 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);
    }
}

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. In a real project, you would generally have other scripts controlling the animations.
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.
Creates a sphere in the script for the IK target. Uses a serialized field to reference an existing target object since there's no point in hard-coding it in the script.
Provides no way to move the IK target, so you need to use the Scene view gizmos or the Inspector. Uses the MouseDrag script from the Puppet example to allow the IK target to be dragged around in the Game view.
Creates a new 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 it becomes 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;

Initialization

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. Note that the system does have some limitations which are explained on that page.