02 Damping

Difficulty: Intermediate

Location: Assets/Plugins/Animancer/Examples/10 Animation Jobs/02 Damping

Namespace: Animancer.Examples.Jobs

This example demonstrates how the DampingJob from the Animation Jobs Samples can be used with Animancer to apply physics based drag to some of a character's bones. That would normally be useful for things like hair or a tail, but since we don't have a character with those this example will just apply the effect to an arm of the DefaultHumanoid and the legs of the SpiderBot. This system could be used in a real project, however the Animation Rigging package contains a much better Damped Transform 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 DampingJob script from the Animation Jobs Samples. The contents of this script will are mostly the same as the sample, except that the ProcessRootMotion method has been cleared because its original code was actually unnecessary and did not work as intended. Its functionality will not be explained here (beyond its original comments) since this example is about how to use jobs, not how to implement the damping itself.

    using Unity.Collections;
    using UnityEngine;
    #if UNITY_2019_3_OR_NEWER
    using UnityEngine.Animations;
    #else
    using UnityEngine.Experimental.Animations;
    #endif
    
    public struct DampingJob : IAnimationJob
    {
        public TransformStreamHandle rootHandle;
        public NativeArray<TransformStreamHandle> jointHandles;
        public NativeArray<Vector3> localPositions;
        public NativeArray<Quaternion> localRotations;
        public NativeArray<Vector3> positions;
        public NativeArray<Vector3> velocities;
    
        /// <summary>
        /// Transfer the root position and rotation through the graph.
        /// </summary>
        /// <param name="stream">The animation stream</param>
        public void ProcessRootMotion(AnimationStream stream)
        {
            // This was in the original sample, but it causes problems if the character is a child of a moving object.
            // There is no need for this method to do anything in order to support root motion.
    
            //// Get root position and rotation.
            //var rootPosition = rootHandle.GetPosition(stream);
            //var rootRotation = rootHandle.GetRotation(stream);
    
            //// The root always follow the given position and rotation.
            //rootHandle.SetPosition(stream, rootPosition);
            //rootHandle.SetRotation(stream, rootRotation);
        }
    
        /// <summary>
        /// Procedurally generate the joints rotation.
        /// </summary>
        /// <param name="stream">The animation stream</param>
        public void ProcessAnimation(AnimationStream stream)
        {
            if (jointHandles.Length < 2)
                return;
    
            ComputeDampedPositions(stream);
            ComputeJointLocalRotations(stream);
        }
    
        /// <summary>
        /// Compute the new global positions of the joints.
        ///
        /// The position of the first joint is driven by the root's position, and
        /// then the other joints positions are recomputed in order to follow their
        /// initial local positions, smoothly.
        ///
        /// Algorithm breakdown:
        ///     1. Compute the target position;
        ///     2. Damp this target position based on the current position;
        ///     3. Constrain the damped position to the joint initial length;
        ///     4. Iterate on the next joint.
        /// </summary>
        /// <param name="stream">The animation stream</param>
        private void ComputeDampedPositions(AnimationStream stream)
        {
            // Get root position and rotation.
            var rootPosition = rootHandle.GetPosition(stream);
            var rootRotation = rootHandle.GetRotation(stream);
    
            // The first non-root joint follows the root position,
            // but its rotation is damped (see ComputeJointLocalRotations).
            var parentPosition = rootPosition + rootRotation * localPositions[0];
            var parentRotation = rootRotation * localRotations[0];
            positions[0] = parentPosition;
            for (var i = 1; i < jointHandles.Length; ++i)
            {
                // The target position is the global position, without damping.
                var newPosition = parentPosition + (parentRotation * localPositions[i]);
    
                // Apply damping on this target.
                var velocity = velocities[i];
                newPosition = Vector3.SmoothDamp(positions[i], newPosition, ref velocity, 0.15f, Mathf.Infinity,stream.deltaTime);
    
                // Apply constraint: keep original length between joints.
                newPosition = parentPosition + (newPosition - parentPosition).normalized * localPositions[i].magnitude;
    
                // Save new velocity and position for next frame.
                velocities[i] = velocity;
                positions[i] = newPosition;
    
                // Current joint is now the parent of the next joint.
                parentPosition = newPosition;
                parentRotation = parentRotation * localRotations[i];
            }
        }
    
        /// <summary>
        /// Compute the new local rotations of the joints.
        ///
        /// Based on the global positions computed in ComputeDampedPositions,
        /// recompute the local rotation of each joint.
        ///
        /// Algorithm breakdown:
        ///     1. Compute the rotation between the current and new directions of the joint;
        ///     2. Apply this rotation on the current joint rotation;
        ///     3. Compute the local rotation and set it in the stream;
        ///     4. Iterate on the next joint.
        /// </summary>
        /// <param name="stream">The animation stream</param>
        private void ComputeJointLocalRotations(AnimationStream stream)
        {
            var parentRotation = rootHandle.GetRotation(stream);
            for (var i = 0; i < jointHandles.Length - 1; ++i)
            {
                // Get the current joint rotation.
                var rotation = parentRotation * localRotations[i];
    
                // Get the current joint direction.
                var direction = (rotation * localPositions[i + 1]).normalized;
    
                // Get the wanted joint direction.
                var newDirection = (positions[i + 1] - positions[i]).normalized;
    
                // Compute the rotation from the current direction to the new direction.
                var currentToNewRotation = Quaternion.FromToRotation(direction, newDirection);
    
                // Pre-rotate the current rotation, to get the new global rotation.
                rotation = currentToNewRotation * rotation;
    
                // Set the new local rotation.
                var newLocalRotation = Quaternion.Inverse(parentRotation) * rotation;
                jointHandles[i].SetLocalRotation(stream, newLocalRotation);
    
                // Set the new parent for the next joint.
                parentRotation = rotation;
            }
        }
    }
    

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

    using Unity.Collections;
    using UnityEngine;
    using UnityEngine.Animations;
    using UnityEngine.Experimental.Animations;
    
    public class Damping : MonoBehaviour
    {
        [SerializeField] private AnimancerComponent _Animancer;
        [SerializeField] private Transform _EndBone;
        [SerializeField] private int _BoneCount = 1;
    
        private void Awake()
        {
            const Allocator Persistent = Allocator.Persistent;
            const NativeArrayOptions UninitializedMemory = NativeArrayOptions.UninitializedMemory;
    
            var job = new DampingJob()
            {
                jointHandles = new NativeArray<TransformStreamHandle>(_BoneCount, Persistent, UninitializedMemory),
                localPositions = new NativeArray<Vector3>(_BoneCount, Persistent, UninitializedMemory),
                localRotations = new NativeArray<Quaternion>(_BoneCount, Persistent, UninitializedMemory),
                positions = new NativeArray<Vector3>(_BoneCount, Persistent, UninitializedMemory),
                velocities = new NativeArray<Vector3>(_BoneCount, Persistent),
            };
    
            // Initialise the contents of the arrays for each bone.
            var animator = _Animancer.Animator;
            var bone = _EndBone;
            for (int i = _BoneCount - 1; i >= 0; i--)
            {
                job.jointHandles[i] = animator.BindStreamTransform(bone);
                job.localPositions[i] = bone.localPosition;
                job.localRotations[i] = bone.localRotation;
                job.positions[i] = bone.position;
    
                bone = bone.parent;
            }
    
            job.rootHandle = animator.BindStreamTransform(bone);
    
            // Add the job to Animancer's output.
            _Animancer.Playable.InsertOutputJob(job);
    
            // Make sure Animancer disposes the Native Arrays when it is destroyed so we don't leak memory.
            // If we were writing our own job rather than just using the sample, we could have it implement the
            // IDisposable interface to dispose its arrays so that we would only have to call ...Add(_Job); here.
            _Animancer.Playable.Disposables.Add(job.jointHandles);
            _Animancer.Playable.Disposables.Add(job.localPositions);
            _Animancer.Playable.Disposables.Add(job.localRotations);
            _Animancer.Playable.Disposables.Add(job.positions);
            _Animancer.Playable.Disposables.Add(job.velocities);
        }
    
        private void OnValidate()
        {
            if (_BoneCount < 1)
            {
                _BoneCount = 1;
            }
            else if (_EndBone != null && _Animancer != null && _Animancer.Animator != null)
            {
                var root = _Animancer.Animator.transform;
    
                var bone = _EndBone;
                for (int i = 0; i < _BoneCount; i++)
                {
                    bone = bone.parent;
                    if (bone == root)
                    {
                        _BoneCount = i + 1;
                        break;
                    }
                    else if (bone == null)
                    {
                        _EndBone = null;
                        Debug.LogWarning("The End Bone must be a child of the Animator.");
                        break;
                    }
                }
            }
        }
    
    #if !UNITY_2019_1_OR_NEWER
    
        private static bool _HasLoggedUnityVersionWarning;
    
        private void Start()
        {
            if (!_HasLoggedUnityVersionWarning && !_Animancer.Animator.isHuman)
            {
                _HasLoggedUnityVersionWarning = true;
                Debug.LogWarning("A bug in Unity versions older than 2019.1 prevents the Damping system from working on Generi Rigs." +
                    " The DampingJob relies on world positions but TransformStreamHandle.GetPosition returns local positions." this);
            }
        }
    
    #endif
    }
    

Unity Version

Unfortunately, A bug in Unity versions older than 2019.1 prevents the Damping system from working on Generic Rigs. The DampingJob relies on world positions but TransformStreamHandle.GetPosition returns local positions. So we simply use #if !UNITY_2019_1_OR_NEWER (note the !) to write some warning code that will automatically be compiled out of newer versions (see Platform Dependent Compilation for more details):

#if !UNITY_2019_1_OR_NEWER

private static bool _HasLoggedUnityVersionWarning;

private void Start()
{
    if (!_HasLoggedUnityVersionWarning && !_Animancer.Animator.isHuman)
    {
        _HasLoggedUnityVersionWarning = true;
        Debug.LogWarning("A bug in Unity versions older than 2019.1 prevents the Damping system from working on GeneriRigs." +
            " The DampingJob relies on world positions but TransformStreamHandle.GetPosition returns local positions."this);
    }
}

#endif

We could have just put that code in the Awake method, but using Start allows us to keep it entirely separate from the rest of the script and avoid needing a separate #if around both the field and the contents of the method.

Responsibilities

Since the original Damping script was just a sample, it had most of the same problems with Responsibilities as the Two Bone IK example which are again fixed in our example implementation. The following table compares the differences between the original and the Damping script used in this example, excluding any that were already covered by the Two Bone IK example:

Original Animancer
Has an array of bone Transforms which can be assigned in the Inspector and a comment explaining that "The joints must have a simple hierarchy (i.e. joint N is parent of joint N+1)". Only has a single _EndBone reference as well as a _BoneCount to specify the number of bones. This ensures that you can't accidentally assign bones in an incorrect order or hierarchy.
Created a sphere for each joint to visualise its damped position and updated them every frame. Removed this functionality for the sake of simplicity since it didn't even seem to show the spheres at the correct positions.
In addition to destroying the PlayableGraph in OnDisable, it also had to Dispose all the NativeArrays used by the job. Adds the arrays to the AnimancerPlayable.Disposables list for Animancer to Dispose them so we don't need an OnDisable method at all.
Had a separate field to store each of those NativeArrays for disposal. Doesn't need any of those fields because it gives the arrays to Animancer in the same method where they are allocated.

Fields

As with the Two Bone IK example, inserting the job into Animancer's PlayableGraph allows multiple instances to be applied to the same character so we are applying one to the DefaultHumanoid's LeftHand and one to each of the SpiderBot's 4 legs. Their Bone Counts are all set to 3 since we want to affect the Hand, Elbow, and Shoulder of the DefaultHumanoid and the corresponding leg bones on the SpiderBot.

Code Inspector
[SerializeField]
private AnimancerComponent _Animancer;

[SerializeField]
private Transform _EndBone;

[SerializeField]
private int _BoneCount = 1;

Initialisation

Unity's Animation Job system allows you to write high performance multithreaded code to access low-level data in the animation stream, but to do so it has to enforce several restrictions on the type of code you can use. One of the restrictions is that you can't use Reference Types in them. That means two things for this example:

  • Instead of using regular arrays (Vector3[]) we need to use Unity's native arrays (NativeArray<Vector3>).
  • Instead of being able to directly reference the bone Transforms we want to apply the damping effect to, we need to use TransformStreamHandle.

A NativeArray needs to be allocated with a specific length (_BoneCount) just like a regular array, but also has two additional parameters:

  1. The Allocator parameter can be used to allocate a temporary array which Unity will automatically clean up and reuse after a few frames. But in this case, we want Persistent arrays that will last for the entire lifetime of the job. This also means we will need to Dispose the arrays to avoid leaking memory.
  2. The NativeArrayOptions parameter determines if the allocated memory needs to be cleared or not. Most of them can use UninitializedMemory which is faster because we will be immediately filling them, but the velocities need to use the default ClearMemory to make sure all the values start at zero.

Since we are about to use those values several times, we can shorten the following lines a bit by using constants for them:

private void Awake()
{
    const Allocator Persistent = Allocator.Persistent;
    const NativeArrayOptions UninitializedMemory = NativeArrayOptions.UninitializedMemory;

    var job = new DampingJob()
    {
        jointHandles = new NativeArray<TransformStreamHandle>(_BoneCount, Persistent, UninitializedMemory),
        localPositions = new NativeArray<Vector3>(_BoneCount, Persistent, UninitializedMemory),
        localRotations = new NativeArray<Quaternion>(_BoneCount, Persistent, UninitializedMemory),
        positions = new NativeArray<Vector3>(_BoneCount, Persistent, UninitializedMemory),
        velocities = new NativeArray<Vector3>(_BoneCount, Persistent),
    };

The above syntax is called an Object Initialiser which is simply a neater way of initialising an object. We could have done the exact same thing using:

var job = new DampingJob();
job.jointHandles = ...;
job.localPositions = ...;
// Etc.

Once the arrays are allocated, we can fill all their values for each bone starting at the _EndBone and going up each of their parents in the Hierarchy up to the specified _BoneCount. As mentioned earlier, we can't use direct Transform references so we use the AnimatorJobExtensions.BindStreamTransform extension method to get a TransformStreamHandle to allow the job to access them.

    var animator = _Animancer.Animator;
    var bone = _EndBone;
    for (int i = _BoneCount - 1; i >= 0; i--)
    {
        job.jointHandles[i] = animator.BindStreamTransform(bone);
        job.localPositions[i] = bone.localPosition;
        job.localRotations[i] = bone.localRotation;
        job.positions[i] = bone.position;

        bone = bone.parent;
    }

After the last bone, the job also needs a handle for its parent to use as the root:

    job.rootHandle = animator.BindStreamTransform(bone);

And now that we have given the job everything it needs, we can give it to Animancer:

    _Animancer.Playable.InsertOutputJob(job);

Disposal

As mentioned earlier, using Allocator.Persistent for the NativeArrays means we need to Dispose them when we are done with them to free up the memory that was allocated for them so we don't leak memory (and Unity will give you a warning if you fail to do so). The original sample used an OnDisable method to Dispose its arrays,

    _Animancer.Playable.Disposables.Add(job.jointHandles);
    _Animancer.Playable.Disposables.Add(job.localPositions);
    _Animancer.Playable.Disposables.Add(job.localRotations);
    _Animancer.Playable.Disposables.Add(job.positions);
    _Animancer.Playable.Disposables.Add(job.velocities);
}

Note that NativeArrays will cause an error if you Dispose them multiple times, so if we were writing our own job (rather than just using the sample) and we wanted to be able to disable it separately from the destruction of Animancer's PlayableGraph, we could have the job implement the IDisposable interface to dispose its arrays so that we would only have to call ...Add(_Job); here. Then inside the job's Dispose method, we could check the IsCreated property of each array before destroying it to avoid that error.

Validation

As noted in the Responsibilities table, replacing the original sample's array of Transforms with a single _EndBone makes the script easier to use and ensures that you can't assign an invalid set of objects. We can make the script even safer by adding an OnValidate method which the Unity Editor will call whenever any of its fields are modified in the Inspector:

private void OnValidate()
{

The first thing we want to do is ensure that the _BoneCount is at least 1 (the _EndBone itself):

    if (_BoneCount < 1)
    {
        _BoneCount = 1;
    }

Then if we have all the references properly configured we can iterate up the hierarchy from the _EndBone to ensure that the _BoneCount is smaller than the number of bones between there and the root Animator:

    else if (_EndBone != null && _Animancer != null && _Animancer.Animator != null)
    {
        var root = _Animancer.Animator.transform;

        var bone = _EndBone;
        for (int i = 0; i < _BoneCount; i++)
        {
            bone = bone.parent;
            if (bone == root)
            {
                _BoneCount = i + 1;
                break;
            }

If we reach the top of the Hierarchy without finding the root Animator, then the _EndBone is not a child of it and won't work with the DampingJob so we just clear it and log a warning:

            else if (bone == null)
            {
                _EndBone = null;
                Debug.LogWarning("The End Bone must be a child of the Animator.");
                break;
            }
        }
    }
}

Conclusion

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