10-03 Lean

Location: Assets/Plugins/Animancer/Examples/10 Animation Jobs/03 Lean

Recommended After: Two Bone IK and Damping

Learning Outcomes: in this example you will learn:

How to dynamically lean a character by rotating their spine.

How to write a custom Animation Job.

This example demonstrates how you can use an Animation Job to dynamically rotate a character's spine to the side with a slider. The effect can be modified by changing the axis around which the rotation occurs:

  • (0, 0, 0) leans over to the left/right
  • (90, 0, 0) turns to the left/right
  • (0, 90, 0) bends forward/back

This example is based on an implementation by ted-hou on GitHub.

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

  • Jobs can't use reference types, but since NativeArrays behave like them, you can allocate a NativeArray with a single element then use it to easily and efficiently communicate with a job.
  • AnimancerUtilities.CreateNativeReference allocates a single element array.
  • Don't forget that the array needs to be Disposed when you're done with it (or added to animancerComponent.Playable.Disposables so Animancer will do it for you).

Overview

This example uses two scripts like the previous Animation Jobs examples, but this time we're writing the entire job from scratch so we can organise things to be a bit more reusable. Instead of using one script for a job struct and one for a component that initializes the job, this time we will be using three Types:

  • The SimpleLean class inherits from AnimancerJob as a wrapper around the job to provide a clean API for other scripts to access the job.
  • The Job struct is nested inside the SimpleLean class and contains the actual IAnimationJob implementation.
  • The SimpleLeanComponent class is a regular MonoBehaviour component which initializes a SimpleLean for the purpose of this example.

This setup allows SimpleLean and its Job to be fairly reusable without tieing them to specific implementation details that only exist for the purpose of this demonstration. For example, SimpleLeanComponent has a reference to the Transform of the Axis object and sends its forward vector to the job every Update in case you rotate it, but that's not likely to be necessary in a real project since you would probably just use a fixed axis for a specific purpose.

Simple Lean Component

As mentioned above, the SimpleLeanComponent is a very simple script designed for this specific example scene to allow the real implementation of SimpleLean to be potentially reused in other circumstances.

It has an array of Transforms so you can use the Inspector to pick which bones you want it to control.

public sealed class SimpleLeanComponent : MonoBehaviour
{
    [SerializeField] private AnimancerComponent _Animancer;
    [SerializeField] private Transform[] _Bones;

For this example, it's set to control the Chest and Spine.

On startup, it asserts that at least one bone has been assigned, converts the Transform[] into a NativeArray<TransformStreamHandle> for the job to use, and creates a SimpleLean with it.

    private SimpleLean _Lean;

    private void Awake()
    {
        Debug.Assert(_Bones.Length > 0, "No bones are assigned.", this);
        var boneHandles = AnimancerUtilities.ConvertToTransformStreamHandles(_Bones, _Animancer.Animator);
        _Lean = new SimpleLean(_Animancer.Playable, _Axis.forward, boneHandles);
    }

It has a public Angle property for a UI Slider to control the SimpleLean.Angle.

    public float Angle
    {
        get => _Lean.Angle;
        set => _Lean.Angle = value;
    }

And a reference to the Transform of an object to define the SimpleLean.Axis around which the rotation will be performed.

    [SerializeField]
    private Transform _Axis;

    private void Update()
    {
        _Lean.Axis = _Axis.forward;
    }
}

Initialization

SimpleLean inherits from AnimancerJob which is a simple base class that makes it a bit easier to implement wrappers for Animation Jobs in Animancer. The benefits of this inheritance will be noted when they are relevant.

public sealed class SimpleLean : AnimancerJob<SimpleLean.Job>, IDisposable
{

The constructor initializes the Job and passes it the required details as we have seen in the previous Animation Jobs examples.

    public SimpleLean(AnimancerPlayable animancer, Vector3 axis, NativeArray<TransformStreamHandle> leanBones)
    {
        var animator = animancer.Component.Animator;

        _Job = new Job
        {
            root = animator.BindStreamTransform(animator.transform),
            bones = leanBones,
            axis = axis,
            angle = AnimancerUtilities.CreateNativeReference<float>(),// See the Angle section.
        };

The CreatePlayable method is inherited from AnimancerJob. It calls AnimancerPlayable.InsertOutputJob like in the earlier examples, but it also stores the returned AnimationScriptPlayable for the Cleanup process which also requires this object to be registered in the AnimancerPlayable.Disposables list.

        CreatePlayable(animancer);

        animancer.Disposables.Add(this);
    }

Axis

Since the _Job field (inherited from AnimancerJob) stores the job created in the constructor, we can just get the current axis directly from it.

    public Vector3 Axis
    {
        get => _Job.axis;

But since the job is a Value Type, simply modifying the values stored in the _Job field will not actually modify the values of the job inside the AnimationScriptPlayable in the PlayableGraph so after we make any modifications we need to use AnimationScriptPlayable.SetJobData to give it the new data. That method is not very efficient because it needs to copy the value of every field in the Job struct even though we are only actually changing one of them. In a real project, this sort of effect is not likely to need to change the axis which the bones are rotated around so that overhead is not a big deal for this.

        set
        {
            if (_Job.axis == value)
                return;

            _Job.axis = value;
            _Playable.SetJobData(_Job);
        }
    }

Angle

Unlike the Axis, the whole point of this system is to be able to dynamically change the Angle of the lean so we want this property to be as efficient as possible. Even though jobs are not allowed to use reference types, we can exploit the fact that NativeArrays actually behave like reference types. By allocating a NativeArray with a size of 1 we will be able to efficiently access its value both inside and outside the job.

Inside the SimpleLean.Job struct, we declare the angle field as a NativeArray.

    public struct Job : IAnimationJob
    {
        ...
        public NativeArray<float> angle;
        ...
    }

Then when we initialize the Job in the SimpleLean constructor, we use AnimancerUtilities.CreateNativeReference to allocate a single element array for it.

    public SimpleLean(AnimancerPlayable animancer, Vector3 axis, NativeArray<TransformStreamHandle> leanBones)
    {
        ...
        _Job = new Job
        {
            ...
            angle = AnimancerUtilities.CreateNativeReference<float>(),
        };
        ...
    }

And that allows the Angle property to set the value without needing to use the AnimationScriptPlayable.SetJobData method to copy the entire Job.

    public float Angle
    {
        get => _Job.angle[0];
        set => _Job.angle[0] = value;
    }

Cleanup

As explained in the Damping example, when you're done with a NativeArray you must call Dispose on it to avoid leaking its memory. We could add the arrays directly to the AnimancerPlayable.Disposables list, but it might be useful to be able to remove the SimpleLean system separately from the destruction of the rest of the character and we don't want to let it cause an error if it tries to Dispose an array we already disposed.

We start by implementing the IDisposable interface with our own Dispose method.

public sealed class SimpleLean : AnimancerJob<SimpleLean.Job>, IDisposable
{
    public SimpleLean(...)
    {
        ...
        animancer.Disposables.Add(this);
    }

    public void Dispose()
    {
        if (_Job.angle.IsCreated)
            _Job.angle.Dispose();

        if (_Job.bones.IsCreated)
            _Job.bones.Dispose();
    }

That would take care of the automatic disposal, but if another script calls that Dispose method the Job would stop working without actually being removed from the PlayableGraph so it would still be executing and causing errors. So instead we can instead use an Explicit Interface Implementation and make the Dispose method private.

    void IDisposable.Dispose() => Dispose();

    private void Dispose()
    {
        if (_Job.angle.IsCreated)
            _Job.angle.Dispose();

        if (_Job.bones.IsCreated)
            _Job.bones.Dispose();
    }

Then we override the AnimancerJob.Destroy method to call the Dispose method as well.

    public override void Destroy()
    {
        Dispose();
        base.Destroy();
    }

So now the AnimancerPlayable will call our Dispose method when the whole graph is being destroyed and other scripts can call that Destroy method which will both Dispose the job and remove it from the graph.

Job

The actual Job implementation is fairly simple. We already initialized the fields in the SimpleLean constructor.

    public struct Job : IAnimationJob
    {
        public TransformStreamHandle root;
        public NativeArray<TransformStreamHandle> bones;
        public Vector3 axis;
        public NativeArray<float> angle;

The IAnimationJob interface contains two methods which are both called every time the PlayableGraph is updated (generally once per frame). We don't care about ProcessRootMotion here and want to do all our math in ProcessAnimation.

        public void ProcessRootMotion(AnimationStream stream) { }

        public void ProcessAnimation(AnimationStream stream)
        {

We start by calculating the angle that each bone will be rotated by dividing the specified total Angle by the number of bones. For this example we are controlling the character's Chest and Spine bones, so each will receive half of the total angle.

            var angle = this.angle[0] / bones.Length;

Since the Axis is easier for other scripts to specify in local space, so we want to rotate that axis according to the character's root.

            var worldAxis = root.GetRotation(stream) * axis;

Then we can use that angle and worldAxis to calculate the rotation Quaternion we want to add to each bone.

            var offset = Quaternion.AngleAxis(angle, worldAxis);

And finally, we just add that offset to each of the bones.

            for (int i = bones.Length - 1; i >= 0; i--)
            {
                var bone = bones[i];
                bone.SetRotation(stream, offset * bone.GetRotation(stream));
            }
        }
    }
}

Note that "adding" rotations actually uses the multiply operator (*) due to the way Quaternion math works.

Conclusion

This technique could be used to make a character lean to the side while running around a corner, or turn and bend according to the direction they want to look.

It could also be adapted to widen the character's legs and arms to accomodate skinny or bulky characters without needing separate animations for each body type.