03 Lean

Difficulty: Intermediate

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

Namespace: Animancer.Examples.Jobs

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.

Architecture

This example uses two scripts like the previous Animation Jobs examples, but this time we are 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 initialises 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 initialises 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 is not likely to be necessary in a real project since you would probably just use a fixed axis for a specific purpose.

Scripts

The main SimpleLean script contains the job and a clean wrapper which we will go through in detail later on:

using System;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Experimental.Animations;

public sealed class SimpleLean : AnimancerJob<SimpleLean.Job>, IDisposable
{
    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>(),
        };

        CreatePlayable(animancer);

        animancer.Disposables.Add(this);
    }

    public SimpleLean(AnimancerPlayable animancer)
        : this(animancer, Vector3.forward, GetDefaultHumanoidLeanBones(animancer.Component.Animator))
    { }

    public static NativeArray<TransformStreamHandle> GetDefaultHumanoidLeanBones(Animator animator)
    {
        var leanBones = new NativeArray<TransformStreamHandle>(2, Allocator.Persistent,NativeArrayOptions.UninitializedMemory);
        leanBones[0] = animator.BindStreamTransform(animator.GetBoneTransform(HumanBodyBones.Spine));
        leanBones[1] = animator.BindStreamTransform(animator.GetBoneTransform(HumanBodyBones.Chest));
        return leanBones;
    }

    public Vector3 Axis
    {
        get => _Job.axis;
        set
        {
            if (_Job.axis == value)
                return;

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

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

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

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

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

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

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

        public void ProcessRootMotion(AnimationStream stream) { }

        public void ProcessAnimation(AnimationStream stream)
        {
            var angle = this.angle[0] / bones.Length;
            var worldAxis = root.GetRotation(stream) * axis;
            var offset = Quaternion.AngleAxis(angle, worldAxis);

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

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.

On startup, it creates a SimpleLean:

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

    private SimpleLean _Lean;

    private void Awake()
    {
        _Lean = new SimpleLean(_Animancer.Playable);
    }

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

Initialisation

SimpleLean inherits from AnimancerJob which is a simple base class that makes it a bit easier to implement wrappers for Animation Jobs in Animancer (its effects will be noted when they are relevant):

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

The main constructor initialises 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);
    }

While the main constructor allows the caller to specify their own array of leanBones which could include any hierarchy, it could be useful for Humanoid Rigs to be able to just use the standard bones this effect is likely to be used on (the Spine and Chest). So we have another constructor with a method to get those bones with the Animator.GetBoneTransform method:

    public SimpleLean(AnimancerPlayable animancer)
        : this(animancer, Vector3.forward, GetDefaultHumanoidLeanBones(animancer.Component.Animator))
    { }

    public static NativeArray<TransformStreamHandle> GetDefaultHumanoidLeanBones(Animator animator)
    {
        var leanBones = new NativeArray<TransformStreamHandle>(2, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
        leanBones[0] = animator.BindStreamTransform(animator.GetBoneTransform(HumanBodyBones.Spine));
        leanBones[1] = animator.BindStreamTransform(animator.GetBoneTransform(HumanBodyBones.Chest));
        return leanBones;
    }

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 can use it as a value that we can efficiently access 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 initialise the Job in the SimpleLean constructor, we use the AnimancerUtilities.CreateNativeReference method 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 are done with a NativeArray you must call Dispose on it to avoid causing a memory leak. 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 can 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 initialised 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 a multiply operation (*) due to the way Quaternion math works.