In Unity 2018.2, the Animation C# Jobs feature extends the animation Playables with the C# Job System released with 2018.1. It gives you the freedom to create original solutions when implementing your animation system, and improve performance with safe multithreaded code at the same time. Animation C# Jobs is a low-level API that requires a solid understanding of the Playable API. It’s therefore aimed at developers who are interested in extending the Unity animation system beyond its out-of-the-box capabilities. If that sounds like you, read on to find out when is it a good idea to use it and how to get the most out of it!

With Animation C# Jobs, you can write C# code that will be invoked at user-defined places in the PlayableGraph, and thanks to the C# Job System, the users can harness the power of modern multicore hardware. For projects which see a significant cost in C# scripts on the main thread, some of the animation tasks can be parallelized. This unlocks valuable performance gains. The user made C# scripts can modify the animation stream that flows through the PlayableGraph.

Features

New Playable node: AnimationScriptPlayable

Control the animation data stream in the PlayableGraph

Multithreaded C# code

Disclaimer

The Animation C# Jobs is still an experimental feature (living in UnityEngine.Experimental.Animations). The API might change a bit over time, depending on your feedback. Please join the discussion on our Animation Forum!

Use cases

So, say, you want to have a foot-locking feature for your brand new dragon character. You could code that with a regular MonoBehaviour, but all the code would be run in the main thread, and not until the animation pass is over. With the Animation C# Jobs, you can write your algorithm and use it directly in a custom Playable node in your PlayableGraph, and the code will run during PlayableGraph processing, in a separate thread.

Or, if you didn’t want to animate the tail of your dragon, the Animation C# Jobs would be the perfect tool for setting up the ability to procedurally compute this movement.

Animation C# Jobs also gives you the ability to write a super-specific LookAt algorithm that would allow you to target the 10 bones in your dragon’s neck, for example.

Another great example would be making your own animation mixer. Let’s say you have something very specific that you need – a node that takes positions from one input, rotations from another, scales from a third node, and mixes them all together into a single animation stream – Animation C# Jobs gives you the ability to get creative and build for your specific needs.

Examples

Before getting into the meaty details of how to use the Animation C# Jobs API, let’s take a look at some examples that showcase what is possible to do with this feature.

All the examples are available on our Animation Jobs Samples GitHub page. To install it you can either git clone it or download the latest release. Once installed, the examples have their own scenes which are all located in the “Scenes” directory:

LookAt

The LookAt is a very simple example that orients a bone (also called a joint) toward an effector. In the example below, you can see how it works on a quadruped from our 3D Game Kit package.

TwoBoneIK

The TwoBoneIK implements a simple two-bone IK algorithm that can be applied to three consecutive joints (e.g. a human arm or leg). The character in this demo is made with a generic humanoid avatar.

FullbodyIK

The FullbodyIK example shows how to modify values in a humanoid avatar (e.g. goals, hints, look-at, body rotation, etc.). This example, in particular, uses the human implementation of the animation stream.

Damping

The Damping example implements a damping algorithm that can be applied to an animal tail or a human ponytail. It illustrates how to generate a procedural animation.

﻿

SimpleMixer

The SimpleMixer is a sort of “Hello, world!” of animation mixers. It takes two input streams (e.g. animation clips) and mixes them together based on a blending value, exactly like an AnimationMixerPlayable would do.

WeightedMaskMixer

The WeigthedMaskMixer example is a bit more advanced animation mixer. It takes two input streams and mixes them together based on a weight mask that defines how to blend each and every joint. For example, you can play a classic idle animation and take just the animation of the arms from another animation clip. Or you can smooth the blend of an upper-body animation by applying successively higher weights on the spine bones.

API

The Animation C# Jobs feature is powered by the Playable API. It comes with three new structs: AnimationScriptPlayable, IAnimationJob, and AnimationStream.

AnimationScriptPlayable and the IAnimationJob

The AnimationScriptPlayable is a new animation Playable which, like any other Playable, can be added anywhere in a PlayableGraph. The interesting thing about it is that it contains an animation job and acts as a proxy between the PlayableGraph and the job. The job is a user-defined struct that implements IAnimationJob.

A regular job processes the Playable inputs streams and mixes the result in its stream. The animation process is separated in two passes and each pass has its own callback in IPlayableJob:

ProcessRootMotion handles the root transform motion, it is always called before ProcessAnimation and it determines if ProcessAnimation should be called (it depends on the Animator culling mode); ProcessAnimation is for everything else that is not the root motion.

The example below is like the “Hello, world!” of Animation C# Jobs. It does nothing at all, but it allows us to see how to create an AnimationScriptPlayable with an animation job:

using UnityEngine; using UnityEngine.Playables; using UnityEngine.Animations; using UnityEngine.Experimental.Animations; public struct AnimationJob : IAnimationJob { public void ProcessRootMotion(AnimationStream stream) { } public void ProcessAnimation(AnimationStream stream) { } } [RequireComponent(typeof(Animator))] public class AnimationScriptExample : MonoBehaviour { PlayableGraph m_Graph; AnimationScriptPlayable m_ScriptPlayable; void OnEnable() { // Create the graph. m_Graph = PlayableGraph.Create("AnimationScriptExample"); // Create the animation job and its playable. var animationJob = new AnimationJob(); m_ScriptPlayable = AnimationScriptPlayable.Create(m_Graph, animationJob); // Create the output and link it to the playable. var output = AnimationPlayableOutput.Create(m_Graph, "Output", GetComponent<Animator>()); output.SetSourcePlayable(m_ScriptPlayable); } void OnDisable() { m_Graph.Destroy(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 using UnityEngine ; using UnityEngine . Playables ; using UnityEngine . Animations ; using UnityEngine . Experimental . Animations ; public struct AnimationJob : IAnimationJob { public void ProcessRootMotion ( AnimationStream stream ) { } public void ProcessAnimation ( AnimationStream stream ) { } } [ RequireComponent ( typeof ( Animator ) ) ] public class AnimationScriptExample : MonoBehaviour { PlayableGraph m_Graph ; AnimationScriptPlayable m_ScriptPlayable ; void OnEnable ( ) { // Create the graph. m_Graph = PlayableGraph . Create ( "AnimationScriptExample" ) ; // Create the animation job and its playable. var animationJob = new AnimationJob ( ) ; m_ScriptPlayable = AnimationScriptPlayable . Create ( m_Graph , animationJob ) ; // Create the output and link it to the playable. var output = AnimationPlayableOutput . Create ( m_Graph , "Output" , GetComponent < Animator > ( ) ) ; output . SetSourcePlayable ( m_ScriptPlayable ) ; } void OnDisable ( ) { m_Graph . Destroy ( ) ; } }

The stream passed as a parameter of the IAnimationJob methods is the one you will be working on during each processing pass.

By default, all the AnimationScriptPlayable inputs are processed. In the case of only one input (a.k.a. a post-process job), this stream will contain the result of the processed input. In the case of multiple inputs (a.k.a. a mix job), it’s preferable to process the inputs manually. To do so, the method AnimationScriptPlayable.SetProcessInputs(bool) will enable or disable the processing passes on the inputs. To trigger the processing of an input and acquire the resulting stream in manual mode, call AnimationStream.GetInputStream().

AnimationStream and the handles

The AnimationStream gives you access to the data that flows through the graph from one playable to another. It gives access to all the values animated by the Animator component

public struct AnimationStream { public bool isValid { get; } public float deltaTime { get; } public Vector3 velocity { get; set; } public Vector3 angularVelocity { get; set; } public Vector3 rootMotionPosition { get; } public Quaternion rootMotionRotation { get; } public bool isHumanStream { get; } public AnimationHumanStream AsHuman(); public int inputStreamCount { get; } public AnimationStream GetInputStream(int index); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public struct AnimationStream { public bool isValid { get ; } public float deltaTime { get ; } public Vector3 velocity { get ; set ; } public Vector3 angularVelocity { get ; set ; } public Vector3 rootMotionPosition { get ; } public Quaternion rootMotionRotation { get ; } public bool isHumanStream { get ; } public AnimationHumanStream AsHuman ( ) ; public int inputStreamCount { get ; } public AnimationStream GetInputStream ( int index ) ; }

It isn’t possible to have a direct access to the stream data since the same data can be at a different offset in the stream from one frame to the other (for example, by adding or removing an AnimationClip in the graph). The data may have moved, or may not exist anymore in the stream. To ensure the safety and validity of those accesses, we’re introducing two sets of handles: the stream and the scene handles, which each have a transform and a component property handle.

The stream handles

The stream handles manage, in a safe way, all the accesses to the AnimationStream data. If an error occurs the system throws a C# exception. There are two types of stream handles: TransformStreamHandle and PropertyStreamHandle.

The TransformStreamHandle manages Transform and takes care of the transform hierarchy. That means you can change the local or global transform position in the stream, and future position requests will give predictable results.

The PropertyStreamHandle manages all other properties that the system can animate and find on the other components. For instance, it can be used to read, or write, the value of the Light.m_Intensity property.

public struct TransformStreamHandle { public bool IsValid(AnimationStream stream); public bool IsResolved(AnimationStream stream); public void Resolve(AnimationStream stream); public void SetLocalPosition(AnimationStream stream, Vector3 position); public Vector3 GetLocalPosition(AnimationStream stream); public void SetLocalRotation(AnimationStream stream, Quaternion rotation); public Quaternion GetLocalRotation(AnimationStream stream); public void SetLocalScale(AnimationStream stream, Vector3 scale); public Vector3 GetLocalScale(AnimationStream stream); public void SetPosition(AnimationStream stream, Vector3 position); public Vector3 GetPosition(AnimationStream stream); public void SetRotation(AnimationStream stream, Quaternion rotation); public Quaternion GetRotation(AnimationStream stream); } public struct PropertyStreamHandle { public bool IsValid(AnimationStream stream); public bool IsResolved(AnimationStream stream); public void Resolve(AnimationStream stream); public void SetFloat(AnimationStream stream, float value); public float GetFloat(AnimationStream stream); public void SetInt(AnimationStream stream, int value); public int GetInt(AnimationStream stream); public void SetBool(AnimationStream stream, bool value); public bool GetBool(AnimationStream stream); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public struct TransformStreamHandle { public bool IsValid ( AnimationStream stream ) ; public bool IsResolved ( AnimationStream stream ) ; public void Resolve ( AnimationStream stream ) ; public void SetLocalPosition ( AnimationStream stream , Vector3 position ) ; public Vector3 GetLocalPosition ( AnimationStream stream ) ; public void SetLocalRotation ( AnimationStream stream , Quaternion rotation ) ; public Quaternion GetLocalRotation ( AnimationStream stream ) ; public void SetLocalScale ( AnimationStream stream , Vector3 scale ) ; public Vector3 GetLocalScale ( AnimationStream stream ) ; public void SetPosition ( AnimationStream stream , Vector3 position ) ; public Vector3 GetPosition ( AnimationStream stream ) ; public void SetRotation ( AnimationStream stream , Quaternion rotation ) ; public Quaternion GetRotation ( AnimationStream stream ) ; } public struct PropertyStreamHandle { public bool IsValid ( AnimationStream stream ) ; public bool IsResolved ( AnimationStream stream ) ; public void Resolve ( AnimationStream stream ) ; public void SetFloat ( AnimationStream stream , float value ) ; public float GetFloat ( AnimationStream stream ) ; public void SetInt ( AnimationStream stream , int value ) ; public int GetInt ( AnimationStream stream ) ; public void SetBool ( AnimationStream stream , bool value ) ; public bool GetBool ( AnimationStream stream ) ; }

The scene handles

The scene handles are another form of safe access to any values, but from the scene rather than from the AnimationStream. As for the stream handles, there are two types of scene handles: TransformSceneHandle and PropertySceneHandle.

A concrete usage of a scene handle is to implement an effector for a foot IK. The IK effector is usually a GameObject not animated by an Animator, and therefore external to the transforms modified by the animation clips in the PlayableGraph. The job needs to know the global position of the IK effector in order to calculate the desired position of the foot. Thus the IK effector is accessed through a scene handle, while stream handles are used for the leg bones.

public struct TransformSceneHandle { public bool IsValid(AnimationStream stream); public void SetLocalPosition(AnimationStream stream, Vector3 position); public Vector3 GetLocalPosition(AnimationStream stream); public void SetLocalRotation(AnimationStream stream, Quaternion rotation); public Quaternion GetLocalRotation(AnimationStream stream); public void SetLocalScale(AnimationStream stream, Vector3 scale); public Vector3 GetLocalScale(AnimationStream stream); public void SetPosition(AnimationStream stream, Vector3 position); public Vector3 GetPosition(AnimationStream stream); public void SetRotation(AnimationStream stream, Quaternion rotation); public Quaternion GetRotation(AnimationStream stream); } public struct PropertySceneHandle { public bool IsValid(AnimationStream stream); public bool IsResolved(AnimationStream stream); public void Resolve(AnimationStream stream); public void SetFloat(AnimationStream stream, float value); public float GetFloat(AnimationStream stream); public void SetInt(AnimationStream stream, int value); public int GetInt(AnimationStream stream); public void SetBool(AnimationStream stream, bool value); public bool GetBool(AnimationStream stream); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public struct TransformSceneHandle { public bool IsValid ( AnimationStream stream ) ; public void SetLocalPosition ( AnimationStream stream , Vector3 position ) ; public Vector3 GetLocalPosition ( AnimationStream stream ) ; public void SetLocalRotation ( AnimationStream stream , Quaternion rotation ) ; public Quaternion GetLocalRotation ( AnimationStream stream ) ; public void SetLocalScale ( AnimationStream stream , Vector3 scale ) ; public Vector3 GetLocalScale ( AnimationStream stream ) ; public void SetPosition ( AnimationStream stream , Vector3 position ) ; public Vector3 GetPosition ( AnimationStream stream ) ; public void SetRotation ( AnimationStream stream , Quaternion rotation ) ; public Quaternion GetRotation ( AnimationStream stream ) ; } public struct PropertySceneHandle { public bool IsValid ( AnimationStream stream ) ; public bool IsResolved ( AnimationStream stream ) ; public void Resolve ( AnimationStream stream ) ; public void SetFloat ( AnimationStream stream , float value ) ; public float GetFloat ( AnimationStream stream ) ; public void SetInt ( AnimationStream stream , int value ) ; public int GetInt ( AnimationStream stream ) ; public void SetBool ( AnimationStream stream , bool value ) ; public bool GetBool ( AnimationStream stream ) ; }

AnimationJobExtensions

The last piece is the AnimationJobExtension class. It’s the glue that makes it all work. It extends the Animator to create the four handles seen above, thanks to these four methods: BindStreamTransform, BindStreamProperty, BindSceneTransform, and BindSceneProperty.

public static class AnimatorJobExtensions { public static TransformStreamHandle BindStreamTransform(this Animator animator, Transform transform); public static PropertyStreamHandle BindStreamProperty(this Animator animator, Transform transform, Type type, string property); public static TransformSceneHandle BindSceneTransform(this Animator animator, Transform transform); public static PropertySceneHandle BindSceneProperty(this Animator animator, Transform transform, Type type, string property); } 1 2 3 4 5 6 7 8 public static class AnimatorJobExtensions { public static TransformStreamHandle BindStreamTransform ( this Animator animator , Transform transform ) ; public static PropertyStreamHandle BindStreamProperty ( this Animator animator , Transform transform , Type type , string property ) ; public static TransformSceneHandle BindSceneTransform ( this Animator animator , Transform transform ) ; public static PropertySceneHandle BindSceneProperty ( this Animator animator , Transform transform , Type type , string property ) ; }

The “BindStream” methods can be used to create handles on already animated properties or for newly animated properties in the stream.

See also

API documentation:

If you encounter a bug, please file it using the Bug Reporter built in Unity.

For any feedback on this experimental feature, please go this forum thread: Animation C# jobs in 2018.2a5