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 aDampingJob
to Animancer'sPlayableGraph
, 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 Transform s 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 NativeArray s 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 |
---|---|
|
![]() |
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
Transform
s we want to apply the damping effect to, we need to useTransformStreamHandle
.
A NativeArray
needs to be allocated with a specific length (_BoneCount
) just like a regular array, but also has two additional parameters:
- 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 wantPersistent
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. - The
NativeArrayOptions
parameter determines if the allocated memory needs to be cleared or not. Most of them can useUninitializedMemory
which is faster because we will be immediately filling them, but thevelocities
need to use the defaultClearMemory
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 NativeArray
s 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 NativeArray
s 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 Transform
s 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.