10-02 Damping

Location: Samples/10 Animation Jobs/02 Damping

Recommended After: Two Bone IK

Learning Outcomes: in this sample you will learn:

How to apply physics based drag to some of a character's bones.

How to use the DampingJob from the Animation Jobs Samples.

Summary

This sample 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 sample just uses the character's arms.

The job in this sample could be used in a real project, however FlexiMotion is a much more powerful system for achieving this sort of thing separately from Animancer.

Overview

This sample uses two new scripts:

  • The Damping component applies drag to one limb, so the character has one for each arm.
  • Each Damping adds a DampingJob to Animancer's PlayableGraph, which is where the actual drag algorithm is implemented.

It also reuses the MouseDrag script from the Puppet sample.

Job Script

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 sample is about how to use jobs, not how to implement the damping itself.

using Unity.Collections;
using UnityEngine;
using UnityEngine.Animations;

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

Responsibilities

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

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.
Creates a sphere for each joint to visualise its damped position and updates 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 AnimancerGraph.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 sample, inserting the job into Animancer's PlayableGraph allows multiple instances to be applied to the same character so we're applying one to each of the character's hands with their Bone Count set to 3 to make them affect affect the Hand, Elbow, and Shoulder.

Code Inspector
[SerializeField]
private AnimancerComponent _Animancer;

[SerializeField]
private Transform _EndBone;

[SerializeField]
private int _BoneCount = 3;

Initialization

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 sample:

  • 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:

protected virtual 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 Initializer which is simply a neater way of initializing 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 the job has 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, but with Animancer you can simply add them to the AnimancerGraph.Disposables list and it will Dispose them for you.

    _Animancer.Graph.Disposables.Add(job.jointHandles);
    _Animancer.Graph.Disposables.Add(job.localPositions);
    _Animancer.Graph.Disposables.Add(job.localRotations);
    _Animancer.Graph.Disposables.Add(job.positions);
    _Animancer.Graph.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:

protected virtual 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 FlexiMotion is a much more powerful system for achieving this sort of thing separately from Animancer.