Difficulty: Intermediate - Recommended after Two Bone IK
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.
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 are not available in runtime builds unless you purchase Animancer Pro.
Scripts
This example uses two scripts:
The job itself is a
struct
which implements the IAnimationJob interface. In this case, that's theDampingJob
script from the Animation Jobs Samples. The contents of this script will are mostly the same as the sample, except that theProcessRootMotion
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; } } }
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), }; // Initialize 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 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. |
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 NativeArray s 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 Count
s 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 |
---|---|
|
![]() |
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
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:
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 Initializer 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 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,
_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 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:
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.