Post

My Understanding of the Unreal Animation Framework in 5.6

My Understanding of the Unreal Animation Framework in 5.6

About two months ago, the technical demonstration of The Witcher 4 at Unreal Fest showcased the next-generation animation system of the Unreal Engine (Unreal Animation Framework, hereafter referred to as UAF, while the previous animation blueprint system will be referred to as ABP). This sparked my strong curiosity, and I felt it was time to dive into understanding this system.

This article will analyze the system from a architecture perspective, primarily introducing its components, the meanings of various types, and their logical relationships. I hope this will help everyone grasp and get started with this new animation system, but it will not cover details such as animation blending calculations or animation retargeting.

Simple demo

Let’s start with a simple demo:

UAF vs ABP

The video shows a simple layered blending effect, with the upper body coming from the static frame of the bow drawing animation and the lower body using a looping sprint animation.

BlendMask utilizes a HierarchyTable:

HierarchyTable

It is a general-purpose hierarchical data container, used here as BlendProfile

From the left view, it’s clear that the two models completely overlap, and the animation effects are identical:

Left View

This is because both are running the same animation graph of UAF:

UAF Graph LayerBlend

The upper left is the lower body animation, the lower left is the upper body animation, and the right is the layered blending

The difference is that the animation graph on the left is updated using the UAF framework:

UAF Module PrePhysics

These nodes here may not be the optimal implementation, but they are sufficient for simple demonstration

while on the right side, the animation graph is updated using ABP :

ABP Graph

animation graph of UAF can be integrated into animation blueprints through this special animation node

Unified Workspace Interface

UAF has integrated the Workspace Editor, providing a unified view of multiple assets.
The workspace itself also has a corresponding asset, classified as UAF Workspace, which should be used to store metadata related to the workspace.

The Workspace Editor module comes from the new experimental plugin Workspace, which allows multiple assets to be edited in a unified interface.

UAF has integrated this feature, which specifies that UAF-related asset types can be edited in the same workspace.

See:UAnimNextWorkspaceSchema, IWorkspaceEditorModule::RegisterObjectDocumentType

The workspace tab in the upper left corner lists the assets opened in the current workspace:

UAF Module PrePhysics

Namely, AG_SequencePlayer, UAFM_Module, and the workspace’s own asset: UAFW_Workspace:

Workspace

System Composition

These raw cpp types bellow are basically located in the UE::AnimNext namespace

The logical carriers of UAF currently consist of two main components: Module and AnimationGraph, both running within RigVM, supporting multithreaded execution.

AnimNextDataInterface

AnimNextRigVMAsset

Data exchange between threads is accomplished through UAnimNextComponent::PublicVariablesProxy.

The comment in FAnimNextPublicVariablesProxy mentions that currently, it copies dirty-marked data every frame, with plans to change it to a double-buffered array in the future (refer to USkinnedMeshComponent::ComponentSpaceTransformsArray).

See:

FAnimNextModuleInstance::CopyProxyVariables

IAnimNextVariableProxyHost::FlipPublicVariablesProxy

UAnimNextComponent::SetVariable

UAnimNextComponentWorldSubsystem::Register

Module

The module here is where various functions are used to write logical business, similar to the blueprint section in ABP / UAnimInstance::NativeUpdateAnimation, UAnimInstance::NativeThreadSafeUpdateAnimation, but more powerful and flexible.

FRigUnit_AnimNextModuleEventBase

AnimNextModuleEventBase

Through the interface provided by the base class, each module can choose whether it needs an independent TickFunction, which Tick Group to run in, whether to operate on the game thread, and other functionalities.

The UAF compiler will also automatically generate some modules, such as variable binding-related FRigUnit_AnimNextExecuteBindings_GT and FRigUnit_AnimNextExecuteBindings_WT.

AnimationGraph

The animation graph is a collection of animation logic and its data, similar to the animation tree in ABP.

The difference is that in UAF, there are no longer various animation nodes; instead, there is a TraitStack node combined with various Trait combinations.

The animation graph itself acts as a UObject, also holding references to UObject references that referenced by shared data within the graph, preventing them from being garbage collected.

See:

UAnimNextAnimationGraph::GraphReferencedObjects

UAnimNextAnimationGraph::GraphReferencedSoftObjects

Additionally, animated charts can have multiple entries, not just Root

TraitStack and TraitStack Node

TraitStack: As the name suggests, this is a stack structure composed of Traits, which includes 1 base trait and several additive traits.

The corresponding node is simply a standard RigUnit node (struct):

Trait Stack Node

A TraitStack node can contain one or more TraitStacks.

In the editor, it appears as shown in the layered blending animation graph above.

The node form is just for the convenience of visualization in the editor. After compiling, the corresponding TraitStack will be serialized into the animation graph. This RigUnit node will not be executed

Trait

“trait” or feature, it refers to reusable functionalities in animation logic, similar to animation nodes in ABP, but again, more powerful and flexible.

FTrait

FTrait is the base class for all Traits, defining the necessary basic interface, such as obtaining its unique ID.

FTrait

Derived traits are composed of FBaseTrait or FAdditiveTrait along with the derived interface class of ITraitInterface.

Base and Additive Trait

TraitHierarchy

ITraitInterface is the base class for all trait interfaces.

ITraitInterface

It contains only one method for getting UID, meaning each trait interface also has a unique ID.

Currently, these two unique IDs are derived by applying the FNV1a hash algorithm to the class name. This algorithm is characterized by the fact that for the same character combination, whether the characters are normal characters or wide characters, it does not affect the hash result, producing the same hash value, and is simple and efficient.

See:

FTraitUID::MakeUID

FTraitInterfaceUID::MakeUID

Trait objects themselves cannot have internal state, meaning they are stateless, as their logic runs in worker threads (for example, multiple objects reusing the same animation graph within the same frame execute in different threads).

Their state data should be declared using the type aliases FSharedData and FInstanceData, which the UAF system will allocate externally for the Trait objects.

SharedData and InstanceData

FSharedData is read-only data that can be shared among multiple instances of the same animation graph; it is a USTRUCT that will serialize and save to a file, typically consisting of some hardcoded configurations.

FInstanceData contains the dynamic/instanced data required by the nodes in each animation graph instance and is a raw CPP structure.

FSharedData is similar to FoldProperty in ABP,

while the mechanism of InstanceData is almost the same as FInstanceDataType/UInstanceDataType in StateTree

Code Generation

UAF uses several macros to quickly and easily generate the code required for the framework, reducing repetitive work.

Macros for Trait Interface

The trait interface part is relatively simple, with only two macros.

DECLARE_ANIM_TRAIT_INTERFACE declares and implements GetInterfaceUID, returning a compile-time constant:

DECLARE_ANIM_TRAIT_INTERFACE

Trait Interface: IEvaluate

AUTO_REGISTER_ANIM_TRAIT_INTERFACE statically registers the shared pointer of the trait interface class to the global trait interface registry:

AUTO_REGISTER_ANIM_TRAIT_INTERFACE

Macros for Trait

The trait section is considerably more complex:

First, you need to use the DECLARE_ANIM_TRAIT macro within the trait class to declare some virtual function overrides:

DECLARE_ANIM_TRAIT

This includes several nested macros:

ANIM_NEXT_IMPL_DECLARE_ANIM_TRAIT_BASIC declares and implements GetTraitUID, returning a compile-time constant; GetTraitName returns the trait name; declares an alias for TraitSuper.

ANIM_NEXT_IMPL_DECLARE_ANIM_TRAIT_INSTANCING_SUPPORT add declarations related to trait data.

ANIM_NEXT_IMPL_DECLARE_ANIM_TRAIT_INTERFACE_SUPPORT add declarations for accessing the trait interface.

ANIM_NEXT_IMPL_DECLARE_ANIM_TRAIT_EVENT_SUPPORT add declarations related to trait events.

ANIM_NEXT_IMPL_DECLARE_ANIM_TRAIT_LATENT_PROPERTY_SUPPORT add declarations related to Latent Property (see below for the meaning of Latent Property).

Then, use the GENERATE_ANIM_TRAIT_IMPLEMENTATION macro to define the above interfaces.

GENERATE_ANIM_TRAIT_IMPLEMENTATION

Notably, the parameters InterfaceEnumeratorMacro, RequiredInterfaceEnumeratorMacro, EventEnumeratorMacro are all EnumeratorMacros, which are macros used for enumeration, with their prefixes indicating what they enumerate: trait interface, required trait interface, and trait events.

The enumeration macro has one parameter, which is also a macro that takes the enumerated item as an argument and performs the corresponding operations.

Taking FBlendTwoWayTrait as an example:

FBlendTwoWayTrait

Macro definition for BlendTwoWayTrait

The locally defined TRAIT_INTERFACE_ENUMERATOR macro enumerates all the trait interfaces implemented by FBlendTwoWayTrait and passes these interfaces to the GeneratorMacro parameter.

Combining with the nested macros in GENERATE_ANIM_TRAIT_IMPLEMENTATION:

ANIM_NEXT_IMPL_DEFINE_ANIM_TRAIT defines the memory size and alignment for shared and instance data, as well as the constructor and destructor.

ANIM_NEXT_IMPL_DEFINE_ANIM_TRAIT_GET_LATENT_PROPERTY_MEMORY_LAYOUT defines the function to retrieve the memory layout information for Latent Property.

ANIM_NEXT_IMPL_DEFINE_ANIM_TRAIT_IS_PROPERTY_LATENT defines the function to determine whether the property with the corresponding name is a Latent Property.

ANIM_NEXT_IMPL_DEFINE_ANIM_TRAIT_GET_INTERFACE defines the function to retrieve a pointer to the specified trait interface.

ANIM_NEXT_IMPL_DEFINE_ANIM_TRAIT_GET_INTERFACES defines the function to retrieve the IDs of all implemented trait interfaces.

ANIM_NEXT_IMPL_DEFINE_ANIM_TRAIT_GET_REQUIRED_INTERFACES defines the function to retrieve the IDs of all required trait interfaces.

ANIM_NEXT_IMPL_DEFINE_ANIM_TRAIT_ON_TRAIT_EVENT defines the function to respond to the required trait event callbacks.

ANIM_NEXT_IMPL_DEFINE_ANIM_TRAIT_GET_TRAIT_EVENTS defines the function to retrieve the IDs of all responsive trait events.

At this point, the necessary trait definitions for the framework has been automatically generated.

AUTO_REGISTER_ANIM_TRAIT, similar to AUTO_REGISTER_ANIM_TRAIT_INTERFACE, registers the trait this time.

Trait registration does not use shared pointers, but is constructed from the callback of UE::AnimNext::TraitConstructorFunc with the DestPtr address passed in.

Latent Property

Latent Property is the part of shared data that needs to be instantiated, placed after the FInstanceData section.

For shared data that inherits from FAnimNextTraitSharedData, you need to use the GENERATE_TRAIT_LATENT_PROPERTIES macro to manuallyselectively register the properties that should be marked as Latent Property:

Latent Property

This macro also uses an enumeration macro as a parameter, within which it nests the macro:

ANIM_NEXT_IMPL_DEFINE_LATENT_CONSTRUCTOR uses placement new to individually construct Latent Property (the memory address is discussed in the following section on FNodeInstance).

See: FExecutionContext::AllocateNodeInstance for how it allocates memory, constructs instance data, and Latent Property

GENERATE_ANIM_TRAIT_IMPLEMENTATION-ANIM_NEXT_IMPL_DEFINE_ANIM_TRAIT-ConstructTraitInstance

GENERATE_TRAIT_LATENT_PROPERTIES-ANIM_NEXT_IMPL_DEFINE_LATENT_CONSTRUCTOR-ConstructLatentProperties

ANIM_NEXT_IMPL_DEFINE_LATENT_DESTRUCTOR Destruct Latent Property one by one

ANIM_NEXT_IMPL_DEFINE_GET_LATENT_PROPERTY_INDEX Query the index of the Latent Property for the corresponding offset

FAnimNextTraitSharedData::GetLatentPropertyIndex comments mentioned: If the Latent Property corresponding to the offset can be found, it returns the index starting from 1; if not found, it returns the number of Latent Property, which is less than or equal to 0 (needs to be negative)

ANIM_NEXT_IMPL_DEFINE_LATENT_GETTER generates a getter function for retrieving property values from FTraitBinding for each Latent Property

Magic of Macro ! It uses the constexpr function GetLatentPropertyIndex to get the LatentPropertyIndex, and then get the Latent Property reference from the Binding.

TraitEvent Trait Events

FAnimNextTraitEvent is the base class for trait events.

FAnimNextTraitEvent

DECLARE_ANIM_TRAIT_EVENT, similar to traits and trait interfaces, declares and defines EventUID, and additionally supports the IsA functionality, serving as a simple alternative mechanism for RTTI.

Since FAnimNextTraitEvent is USTRUCT, the IsA should not be necessary. This may be for performance or other considerations.

Trait events are similar to UI click events and can be marked as Handled, with the option to set a valid duration or an infinite duration, among other settings.

Global Registry

FTraitRegistry

The global registry for trait objects.

When registering traits using macros, FTraitRegistry prioritizes the default allocated 8KB size StaticTraitBuffer to store traits. If this limit is exceeded, it uses DynamicTraits for storing new trait, which is an optimization for memory locality.

There is an interesting little detail here. The DynamicTraits array stores uintptr_t instead of void* or FTrait*, which means using integers to store pointers.

Because integers are used, index of array can be stored at the same time to implement the subsequent FreeList mechanism:

When DynamicTraitFreeIndexHead is valid, DynamicTraits[DynamicTraitFreeIndexHead] stores the next reusable array element

Additionally, several Maps are stored to speed up queries.

FTraitRegistry::Register is used to register traits to DynamicTraits.

FTraitInterfaceRegistry

The global registry for trait interface objects.

In comparison, FTraitInterfaceRegistry is quite straightforward, simply a map from interface IDs to smart pointers.

Node

rough memory layout,drawing with drawio

This section introduces some key types related to nodes.

FNodeTemplate

A FNodeTemplate is a combination of a set of traits, which can include multiple sets of base + additive traits. All animation graphs can share the same template object.

FNodeTemplate

The traits that make up the node template are stored as FTraitTemplate objects at the end of the contiguous memory of the FNodeTemplate object.

FNodeTemplate::GetTraits

FNodeTemplateBuilder::BuildNodeTemplate constructs an FNodeTemplate object and the FTraitTemplate contained in it in a contiguous buffer of TArray<uint8>

From it, you can obtain the UID, base address of the trait array, the number of traits, and other information.

  • FNodeTemplate::NodeSharedDataSize is the size of the shared data for all traits (including the FLatentPropertiesHeader of the base trait) after alignment.
  • FNodeTemplate::NodeInstanceDataSize is the size of the instance data for all traits after alignment.

rough memory layout of FNodeTemplate

FTraitTemplate

FTraitTemplate is the trait within the node template. In addition to providing basic information about the trait such as UID, trait type, shared/instance data, number of subtraits, and number of Latent Properties, it also allows you to obtain the offset of the shared data, the offset of the pointer to the shared latent property array, and the offset of the instance data.

FTraitTemplate

See:FNodeTemplate::Finalize

I think the naming of the two member functions FTraitTemplate::GetTraitDescription is a bit confusing. The more understandable name should be GetTraitSharedData, which is used to get the shared data pointer of the trait. It may be a typo or a name change.

FNodeDescription

The only read-only data within an animation graph. Although the object itself is 8 bytes in size, when allocating memory, it includes the size of the shared data from the traits on the node, making it an object whose size varies in usage.

rough memory layout of FNodeDescription

See:

FTraitReader::ReadGraphSharedData

and some other details: FNodeDescription::Serialize FTrait::SerializeTraitSharedData FTraitWriter::WriteNode FTrait::SaveTraitSharedData

After reading, it is stored in UAnimNextAnimationGraph::SharedDataBuffer. For usage, refer to FExecutionContext::GetNodeDescription

  • FNodeDescription::TemplateHandle is used to obtain an instance of the FNodeTemplate object from FNodeTemplateRegistry.
  • FNodeDescription::NodeInstanceDataSize contains the size of the instantiation data, plus the total size of all Latent Properties.

Note the difference between FNodeDescription::NodeInstanceDataSize and FNodeTemplate::NodeInstanceDataSize

In addition, FNodeDescription and FNodeTemplate are many-to-one, which can be understood from their usage.

FNodeInstance

The instantiated data of the node, dynamically created at runtime, has a size of 16 bytes itself. When allocating memory, it includes the size of the instantiated data of the traits and their Latent Property size, which is also an object that varies in size depending on usage; built-in reference counting.

rough memory layout of FNodeInstance

For usage, refer to :FAnimationAnimNextRuntimeTest_TraitSerialization::RunTestFExecutionContext::AllocateNodeInstance

FNodeTemplateRegistry

The global registry of the FNodeTemplate object, ensuring that all FNodeTemplate instances are contiguous in memory.

FTraitStackBinding

Describes the data required for a set of traits (one base trait and its children/additive traits) used to query traits or the trait interface.

See:FTraitStackBinding::FTraitStackBinding,especially the last few lines

For example, FTraitStackBinding::GetInterfaceImpl: attempts to find the trait that implements the specified InterfaceUID from the trait stack and returns the binding of that trait.

FTraitBinding

Describes the data of a specific trait within a set of traits, allowing you to query whether the current trait implements the specified trait interface.

TTraitBinding

Strongly typed/type-safe FTraitBinding.

FExecutionContext

Since traits are stateless, the dynamic data/instance data during execution requires an object to hold it, which is the FExecutionContext, the execution context object.

It is used to bind to a graph instance and provides a unified trait query interface for nodes.

The Update and Evaluate processes of the graph are encapsulated in the execution function of a RigVM node: FRigUnit_AnimNextRunAnimationGraph_v2_Execute().

FUpdateTraversalContext

The derived context object used when updating the graph.

Internally, it uses a stack (LIFO) allocated on the MemStack to implement a depth-first traversal of the trait tree, rather than the recursive method used in ABP.

See: UE::AnimNext::UpdateGraph. Each trait is executed twice in the while loop, corresponding to IUpdate::PreUpdate and IUpdate::PostUpdate.

IUpdate::OnBecomeRelevant is also called here.

FEvaluateTraversalContext

The derived context object used when evaluating the graph.

The internal FEvaluationProgram is used to store the FAnimNextEvaluationTask that each node needs to execute while traversing the graph.
It then calls FEvaluationProgram::Execute to perform each evaluation task on the FEvaluationVMStack.

See: UE::AnimNext::EvaluateGraph, the execution process is similar to UpdateGraph

FAnimNextEvaluationTask

FAnimNextEvaluationTask is a logical object that can be reused between traits, representing micro-instructions running on the evaluation virtual machine, which can handle input and output at the same time through the internal state of the virtual machine (also a stack).

UAF Modules

UAF consists of multiple modules:

UAF On 5.6

On the ue5-main branch, the prefix of the series of module names has changed from AnimNext to UAF.

UAF On ue5-main

Among them: UAF/AnimNext: Provides core animation utility functions, defines base class interfaces, etc. For example: UE::AnimNext::FDecompressionTools::GetAnimationPose

UAFAnimGraph/AnimNextAnimGraph: Implements RigVM-based functionality related to animation graphs.

These two modules are the main focus of this article.

Other modules introduce functionalities from other modules/systems to UAF, such as incorporating StateTree, PoseSearch, etc.

SoA (struct of array)

Each utility function in TransformArrayOperations.h has both AoS and SoA versions, with the code prioritizing the use of AoS. This indicates that in terms of data structure design, UAF is more data-oriented.

Conclusion

UAF is a redesigned, data-oriented, composition-oriented, high-performance, flexible, concise, and easily extensible animation framework.

It completely abandons the ABP framework and embraces RigVM.

Due to its feature set still being in development, it is currently in an experimental phase.

Miscellaneous

Writing Animation Blueprints in Code

Refer to code of test cases from UAFTestSuite/AnimNextTestSuite and UAFAnimGraphTestSuite/AnimNextAnimGraphTestSuite modules.

Playing “Montages” in UAF

Due to the length of this article, I decide to not to elaborate further.

The relevant nodes are UInjectionCallbackProxy, UPlayAnimCallbackProxy.

The related code can be found in UE::AnimNext::FInjectionUtils::Inject.

Unreal Animation Framework (UAF, AnimNext) FAQ

This post is licensed under CC BY 4.0 by the author.