Usually, in Unreal Engine, we do not implement the animation logic by C++, instead, we use the Animation Blueprint. So here comes a problem, if the build-in nodes do not match our requirement, how can we extend the aniamtion blueprint nodes?
Generally speaking, to extend an animation blueprint node, we only need to implement a class inherits from the FAnimNode_Base
. The decleration of the latter one is not complex. However, a new beginner would be confuse about when will the methods being called. So in this post, we focus on not only how to implement a custom FAnimNode_Base
class, but also how the animation blueprint nodes work.
1. Dive into the animation blueprint
If you have read my posts about the editor of Unreal Engine, I guess you can figure out the essence of the animation blueprint nodes. It is a UEdGraphNode
on a UEdGraph
. What is actually executing is the instance editted by the UEdGraphNode
, it is called the FAnimNode_Base
. The UEdGraphNode
only exists in editor time.
What really executing in gameplay runtime is the FAnimNode_Base, we inherit from the FAnimNode_Base
for runtime and implement a class inherits from a UAnimGraphNode_Base
. We start with the FAnimNode_Base. Most of the time we only overwrite the following methods:
Initialize_AnyThread
CacheBones_AnyThread
Update_AnyThread
Evaluate_AnyThread
EvaluateComponentSpace_AnyThread
GatherDebugData
We must emphasize that you should either Evaluate or EvaluateComponentSpace, but not both of these.
USTRUCT()
struct FAnimNode_Base
{
GENERATED_BODY()
/**
* Called when the node first runs. If the node is inside a state machine or cached pose branch then this can be called multiple times.
* This can be called on any thread.
* @param Context Context structure providing access to relevant data
*/
ENGINE_API virtual void Initialize_AnyThread(const FAnimationInitializeContext& Context);
/**
* Called to cache any bones that this node needs to track (e.g. in a FBoneReference).
* This is usually called at startup when LOD switches occur.
* This can be called on any thread.
* @param Context Context structure providing access to relevant data
*/
ENGINE_API virtual void CacheBones_AnyThread(const FAnimationCacheBonesContext& Context);
/**
* Called to update the state of the graph relative to this node.
* Generally this should configure any weights (etc.) that could affect the poses that
* will need to be evaluated. This function is what usually executes EvaluateGraphExposedInputs.
* This can be called on any thread.
* @param Context Context structure providing access to relevant data
*/
ENGINE_API virtual void Update_AnyThread(const FAnimationUpdateContext& Context);
/**
* Called to evaluate local-space bones transforms according to the weights set up in Update().
* You should implement either Evaluate or EvaluateComponentSpace, but not both of these.
* This can be called on any thread.
* @param Output Output structure to write pose or curve data to. Also provides access to relevant data as a context.
*/
ENGINE_API virtual void Evaluate_AnyThread(FPoseContext& Output);
/**
* Called to evaluate component-space bone transforms according to the weights set up in Update().
* You should implement either Evaluate or EvaluateComponentSpace, but not both of these.
* This can be called on any thread.
* @param Output Output structure to write pose or curve data to. Also provides access to relevant data as a context.
*/
ENGINE_API virtual void EvaluateComponentSpace_AnyThread(FComponentSpacePoseContext& Output);
/**
* Called to gather on-screen debug data.
* This is called on the game thread.
* @param DebugData Debug data structure used to output any relevant data
*/
virtual void GatherDebugData(FNodeDebugData& DebugData)
{
DebugData.AddDebugItem(FString::Printf(TEXT("Non Overriden GatherDebugData! (%s)"), *DebugData.GetNodeName(this)));
}
/**
* Whether this node can run its Update() call on a worker thread.
* This is called on the game thread.
* If any node in a graph returns false from this function, then ALL nodes will update on the game thread.
*/
virtual bool CanUpdateInWorkerThread() const { return true; }
/**
* Override this to indicate that PreUpdate() should be called on the game thread (usually to
* gather non-thread safe data) before Update() is called.
* Note that this is called at load on the UAnimInstance CDO to avoid needing to call this at runtime.
* This is called on the game thread.
*/
virtual bool HasPreUpdate() const { return false; }
/** Override this to perform game-thread work prior to non-game thread Update() being called */
virtual void PreUpdate(const UAnimInstance* InAnimInstance) {}
/**
* For nodes that implement some kind of simulation, return true here so ResetDynamics() gets called
* when things like teleports, time skips etc. occur that might require special handling.
* Note that this is called at load on the UAnimInstance CDO to avoid needing to call this at runtime.
* This is called on the game thread.
*/
virtual bool NeedsDynamicReset() const { return false; }
C++Here comes a problem, how do these methods work? What should I write in each methods? To figure out this problem, we need to dive into source code of the UAnimInstance
and FAnimInstanceProxy
.
1.1 UAnimInstance and FAnimInstanceProxy
For most users of the Unreal Engine, they get familiar with the UAnimInstance
first, when we create a Animation Blueprint in content browser, we actually created a subclass of the UAnimInstance
. However, with the development over time, the animation system becomes so complex that it takes too much time on calculating the final pose on CPU, developers of Unreal Engine have to use multi-threading for animation system. As a result, we have the FAnimInstanceProxy
.
In short, the UAnimInstance
should be used for updating the data, which could only be done by main thread, and the animation blueprint nodes in the animation blueprint will be handled actually by FAnimInstanceProxy
.
A UAnimInstance
intance reference is held by the USkeletalMeshComponent, and the former holds a reference to FAnimInstanceProxy
instance.
1.2 Time sequence of the Anim Nodes
The following shows the animation blueprint in the 3rd template projection of UE5:
The root node is the output node instead of the Locomotion state machine node. when calculating the output pose, we start from the root node, calculating the pose recursively, so that we don’t waste time on nodes that are not combined into the root nodes.
As a result, the root node becomes the simplest node:
// AnimNode_Root.h
USTRUCT(BlueprintInternalUseOnly)
struct FAnimNode_Root : public FAnimNode_Base
{
GENERATED_USTRUCT_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Links)
FPoseLink Result;
#if WITH_EDITORONLY_DATA
protected:
/** The name of this root node, used to identify the output of this graph. Filled in by the compiler, propagated from the parent graph. */
UPROPERTY(meta=(FoldProperty, BlueprintCompilerGeneratedDefaults))
FName Name;
/** The group of this root node, used to group this output with others when used in a layer. */
UPROPERTY(meta=(FoldProperty))
FName Group;
#endif
public:
ENGINE_API FAnimNode_Root();
// FAnimNode_Base interface
ENGINE_API virtual void Initialize_AnyThread(const FAnimationInitializeContext& Context) override;
ENGINE_API virtual void CacheBones_AnyThread(const FAnimationCacheBonesContext& Context) override;
ENGINE_API virtual void Update_AnyThread(const FAnimationUpdateContext& Context) override;
ENGINE_API virtual void Evaluate_AnyThread(FPoseContext& Output) override;
ENGINE_API virtual void GatherDebugData(FNodeDebugData& DebugData) override;
// End of FAnimNode_Base interface
#if WITH_EDITORONLY_DATA
/** Set the name of this root node, used to identify the output of this graph */
void SetName(FName InName) { Name = InName; }
/** Set the group of this root node, used to group this output with others when used in a layer. */
void SetGroup(FName InGroup) { Group = InGroup; }
#endif
/** Get the name of this root node, used to identify the output of this graph */
ENGINE_API FName GetName() const;
/** Get the group of this root node, used to group this output with others when used in a layer. */
ENGINE_API FName GetGroup() const;
};
// AnimNode_Root.cpp
FAnimNode_Root::FAnimNode_Root()
{
}
void FAnimNode_Root::Initialize_AnyThread(const FAnimationInitializeContext& Context)
{
Result.Initialize(Context);
}
void FAnimNode_Root::CacheBones_AnyThread(const FAnimationCacheBonesContext& Context)
{
Result.CacheBones(Context);
}
void FAnimNode_Root::Update_AnyThread(const FAnimationUpdateContext& Context)
{
TRACE_ANIM_NODE_VALUE(Context, TEXT("Name"), GetName());
Result.Update(Context);
}
void FAnimNode_Root::Evaluate_AnyThread(FPoseContext& Output)
{
Result.Evaluate(Output);
}
void FAnimNode_Root::GatherDebugData(FNodeDebugData& DebugData)
{
FString DebugLine = DebugData.GetNodeName(this);
DebugData.AddDebugItem(DebugLine);
Result.GatherDebugData(DebugData);
}
FName FAnimNode_Root::GetName() const
{
return GET_ANIM_NODE_DATA(FName, Name);
}
FName FAnimNode_Root::GetGroup() const
{
return GET_ANIM_NODE_DATA(FName, Group);
}
C++It has a member of the type FLinkPose
result, when init, cache bones, evaluating and updating, it calls the correspounding methods of the FLinkPose
, the latter one simple calls the correspounding of the previous node, by this way, when calling the methods of the root, we call methods chain for the root to the first node.
void FPoseLinkBase::Initialize(const FAnimationInitializeContext& InContext)
{
#if DO_CHECK
checkf(!bProcessed, TEXT("Initialize already in progress, circular link for AnimInstance [%s] Blueprint [%s]"), \
*InContext.AnimInstanceProxy->GetAnimInstanceName(), *GetFullNameSafe(IAnimClassInterface::GetActualAnimClass(InContext.AnimInstanceProxy->GetAnimClassInterface())));
TGuardValue<bool> CircularGuard(bProcessed, true);
#endif
AttemptRelink(InContext);
#if ENABLE_ANIMGRAPH_TRAVERSAL_DEBUG
InitializationCounter.SynchronizeWith(InContext.AnimInstanceProxy->GetInitializationCounter());
// Initialization will require update to be called before an evaluate.
UpdateCounter.Reset();
#endif
// Do standard initialization
if (LinkedNode != nullptr)
{
FAnimationInitializeContext LinkContext(InContext);
LinkContext.SetNodeId(LinkID);
TRACE_SCOPED_ANIM_NODE(LinkContext);
LinkedNode->Initialize_AnyThread(LinkContext);
}
}
C++1.3 Initialize Method
We start with the Initialize method. Generally it will be called only for once, but it can be called for multiple times if it is in a state machine or cached pose.
Generally speaking, the initialize method will be called first, but it maybe surprise you that the initialize method could also be called delay to the update method. The following shows the initialize method of the UAnimInstance
:
void UAnimInstance::InitializeAnimation(bool bInDeferRootNodeInitialization)
{
FScopeCycleCounterUObject ContextScope(this);
SCOPE_CYCLE_COUNTER(STAT_AnimInitTime);
LLM_SCOPE(ELLMTag::Animation);
UninitializeAnimation();
TRACE_OBJECT_LIFETIME_BEGIN(this);
// make sure your skeleton is initialized
// you can overwrite different skeleton
USkeletalMeshComponent* OwnerComponent = GetSkelMeshComponent();
if (OwnerComponent->GetSkeletalMeshAsset() != NULL)
{
CurrentSkeleton = OwnerComponent->GetSkeletalMeshAsset()->GetSkeleton();
}
else
{
CurrentSkeleton = NULL;
}
if (IAnimClassInterface* AnimBlueprintClass = IAnimClassInterface::GetFromClass(GetClass()))
{
#if WITH_EDITOR
LifeTimer = 0.0;
CurrentLifeTimerScrubPosition = 0.0;
if (UAnimBlueprint* Blueprint = Cast<UAnimBlueprint>(Cast<UAnimBlueprintGeneratedClass>(AnimBlueprintClass)->ClassGeneratedBy))
{
if (Blueprint->GetObjectBeingDebugged() == this)
{
// Reset the snapshot buffer
Cast<UAnimBlueprintGeneratedClass>(AnimBlueprintClass)->GetAnimBlueprintDebugData().ResetSnapshotBuffer();
}
}
#endif
}
// before initialize, need to recalculate required bone list
RecalcRequiredBones();
GetProxyOnGameThread<FAnimInstanceProxy>().Initialize(this);
{
#if DO_CHECK
// Allow us to validate callbacks within user code
FGuardValue_Bitfield(bInitializing, true);
#endif
NativeInitializeAnimation();
BlueprintInitializeAnimation();
}
GetProxyOnGameThread<FAnimInstanceProxy>().InitializeRootNode(bInDeferRootNodeInitialization);
// we can bind rules & events now the graph has been initialized
GetProxyOnGameThread<FAnimInstanceProxy>().BindNativeDelegates();
InitializeGroupedLayers(bInDeferRootNodeInitialization);
BlueprintLinkedAnimationLayersInitialized();
}
C++There are several important features worth discussing here:
- It will first calls the
NativeInitializeAnimation
and thenBlueprintInitalizeAnimation
. It means that the methods of blueprint and C++ are individural. Overwrting the initialize method in blueprint has no influence on the C++ method. Both of the methods are empty by default. This situation could be found all along our journey of the animation system. - It will call the initialize root node method of the animation proxy, the latter actually initialize the animation nodes in the blueprint graph.
void FAnimInstanceProxy::InitializeRootNode(bool bInDeferRootNodeInitialization)
{
DECLARE_SCOPE_HIERARCHICAL_COUNTER_FUNC()
InitializeCachedClassData();
if(AnimClassInterface)
{
// Init any nodes that need non-relevancy based initialization
UAnimInstance* AnimInstance = CastChecked<UAnimInstance>(GetAnimInstanceObject());
for (const FStructProperty* Property : AnimClassInterface->GetInitializationNodeProperties())
{
FAnimNode_Base* AnimNode = Property->ContainerPtrToValuePtr<FAnimNode_Base>(AnimInstanceObject);
AnimNode->OnInitializeAnimInstance(this, AnimInstance);
}
}
else
{
auto InitializeNode = [this](FAnimNode_Base* AnimNode)
{
if(AnimNode->NeedsOnInitializeAnimInstance())
{
AnimNode->OnInitializeAnimInstance(this, CastChecked<UAnimInstance>(GetAnimInstanceObject()));
}
if (AnimNode->HasPreUpdate())
{
GameThreadPreUpdateNodes.Add(AnimNode);
}
if (AnimNode->NeedsDynamicReset())
{
DynamicResetNodes.Add(AnimNode);
}
};
//We have a custom root node, so get the associated nodes and initialize them
TArray<FAnimNode_Base*> CustomNodes;
GetCustomNodes(CustomNodes);
for(FAnimNode_Base* Node : CustomNodes)
{
if(Node)
{
InitializeNode(Node);
}
}
}
if(!bInDeferRootNodeInitialization)
{
InitializeRootNode_WithRoot(RootNode);
}
else
{
bDeferRootNodeInitialization = true;
}
bInitializeSubsystems = true;
}
void FAnimInstanceProxy::InitializeRootNode_WithRoot(FAnimNode_Base* InRootNode)
{
DECLARE_SCOPE_HIERARCHICAL_COUNTER_FUNC()
if (InRootNode != nullptr)
{
FAnimationUpdateSharedContext SharedContext;
FAnimationInitializeContext InitContext(this, &SharedContext);
if(InRootNode == RootNode)
{
InitializationCounter.Increment();
TRACE_SCOPED_ANIM_GRAPH(InitContext);
InRootNode->Initialize_AnyThread(InitContext);
}
else
{
InRootNode->Initialize_AnyThread(InitContext);
}
}
}
C++Node that if the bInDeferRootNodeInitialization
is true, it will not initialize the nodes, it just set the bDeferRootNodeInitialization
as true, and init the nodes after the first update:
void FAnimInstanceProxy::UpdateAnimation_WithRoot(const FAnimationUpdateContext& InContext, FAnimNode_Base* InRootNode, FName InLayerName)
{
DECLARE_SCOPE_HIERARCHICAL_COUNTER_FUNC()
ANIM_MT_SCOPE_CYCLE_COUNTER(ProxyUpdateAnimation, !IsInGameThread());
FScopeCycleCounterUObject AnimScope(bUpdatingRoot ? nullptr : GetAnimInstanceObject());
if(InRootNode == RootNode)
{
#if WITH_EDITORONLY_DATA
UpdatedNodesThisFrame.Reset();
NodeInputAttributesThisFrame.Reset();
NodeOutputAttributesThisFrame.Reset();
NodeSyncsThisFrame.Reset();
#endif
if(bInitializeSubsystems && AnimClassInterface)
{
AnimClassInterface->ForEachSubsystem(GetAnimInstanceObject(), [this](const FAnimSubsystemInstanceContext& InContext)
{
InContext.SubsystemInstance.Initialize_WorkerThread();
return EAnimSubsystemEnumeration::Continue;
});
bInitializeSubsystems = false;
}
if(bDeferRootNodeInitialization)
{
InitializeRootNode_WithRoot(RootNode);
if(AnimClassInterface)
{
// Initialize linked sub graphs
for(const FStructProperty* LayerNodeProperty : AnimClassInterface->GetLinkedAnimLayerNodeProperties())
{
if(FAnimNode_LinkedAnimLayer* LayerNode = LayerNodeProperty->ContainerPtrToValuePtr<FAnimNode_LinkedAnimLayer>(AnimInstanceObject))
{
if(UAnimInstance* LinkedInstance = LayerNode->GetTargetInstance<UAnimInstance>())
{
FAnimationInitializeContext InitContext(this);
LayerNode->InitializeSubGraph_AnyThread(InitContext);
FAnimationCacheBonesContext CacheBonesContext(this);
LayerNode->CacheBonesSubGraph_AnyThread(CacheBonesContext);
}
}
}
}
bDeferRootNodeInitialization = false;
}
...
C++As a conclusion, we usually just call the initialize method of the FLinkPose
in the Initialize method of the animation node class. And if necessary, we also init the custom member, set the member to the default value in the method. We take a build-in node in Unreal Engine as an example:
void FAnimNode_ApplyAdditive::Initialize_AnyThread(const FAnimationInitializeContext& Context)
{
DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(Initialize_AnyThread)
FAnimNode_Base::Initialize_AnyThread(Context);
Base.Initialize(Context);
Additive.Initialize(Context);
AlphaBoolBlend.Reinitialize();
AlphaScaleBiasClamp.Reinitialize();
}
C++1.4 Cached Method
And then is the cache method, the cached method will be called every time before the interval update method and the evaluate method.
void FAnimInstanceProxy::UpdateAnimation_WithRoot(const FAnimationUpdateContext& InContext, FAnimNode_Base* InRootNode, FName InLayerName)
{
DECLARE_SCOPE_HIERARCHICAL_COUNTER_FUNC()
ANIM_MT_SCOPE_CYCLE_COUNTER(ProxyUpdateAnimation, !IsInGameThread());
FScopeCycleCounterUObject AnimScope(bUpdatingRoot ? nullptr : GetAnimInstanceObject());
if(InRootNode == RootNode)
{
#if WITH_EDITORONLY_DATA
UpdatedNodesThisFrame.Reset();
NodeInputAttributesThisFrame.Reset();
NodeOutputAttributesThisFrame.Reset();
NodeSyncsThisFrame.Reset();
#endif
if(bInitializeSubsystems && AnimClassInterface)
{
AnimClassInterface->ForEachSubsystem(GetAnimInstanceObject(), [this](const FAnimSubsystemInstanceContext& InContext)
{
InContext.SubsystemInstance.Initialize_WorkerThread();
return EAnimSubsystemEnumeration::Continue;
});
bInitializeSubsystems = false;
}
if(bDeferRootNodeInitialization)
{
InitializeRootNode_WithRoot(RootNode);
if(AnimClassInterface)
{
// Initialize linked sub graphs
for(const FStructProperty* LayerNodeProperty : AnimClassInterface->GetLinkedAnimLayerNodeProperties())
{
if(FAnimNode_LinkedAnimLayer* LayerNode = LayerNodeProperty->ContainerPtrToValuePtr<FAnimNode_LinkedAnimLayer>(AnimInstanceObject))
{
if(UAnimInstance* LinkedInstance = LayerNode->GetTargetInstance<UAnimInstance>())
{
FAnimationInitializeContext InitContext(this);
LayerNode->InitializeSubGraph_AnyThread(InitContext);
FAnimationCacheBonesContext CacheBonesContext(this);
LayerNode->CacheBonesSubGraph_AnyThread(CacheBonesContext);
}
}
}
}
bDeferRootNodeInitialization = false;
}
// Call the correct override point if this is the root node
CacheBones();
}
else
{
CacheBones_WithRoot(InRootNode);
}
// update native update
if(!bUpdatingRoot)
{
// Make sure we only update this once the first time we update, as we can re-call this function
// from other linked instances with grouped layers
if(FrameCounterForUpdate != GFrameCounter)
{
if(AnimClassInterface)
{
C++void FAnimInstanceProxy::EvaluateAnimation_WithRoot(FPoseContext& Output, FAnimNode_Base* InRootNode)
{
TRACE_SCOPED_ANIM_GRAPH(Output);
DECLARE_SCOPE_HIERARCHICAL_COUNTER_FUNC()
ANIM_MT_SCOPE_CYCLE_COUNTER(EvaluateAnimInstance, !IsInGameThread());
if(InRootNode == RootNode)
{
// Call the correct override point if this is the root node
CacheBones();
}
else
{
CacheBones_WithRoot(InRootNode);
}
// Evaluate native code if implemented, otherwise evaluate the node graph
if (!Evaluate_WithRoot(Output, InRootNode))
{
EvaluateAnimationNode_WithRoot(Output, InRootNode);
}
}
C++Generally we don’t need to implement anything in the cached method except simply call the cached method of the FLinkedPose
. However, if we are using FBoneReference
, it is a good place to initialize them.
In fact, in Unreal Engine, we usually use FAnimNode_Base
to control the animation in local space, and use FAnimNode_SkeletalControlBase
in component space. We will introduce those spaces soon. For the latter one, it has a special method called InitializeBoneReferences
. It is used for initializing the FBoneReference
members, they will be called in the cached method.
void FAnimNode_SkeletalControlBase::CacheBones_AnyThread(const FAnimationCacheBonesContext& Context)
{
#if WITH_EDITOR
ClearValidationVisualWarnings();
#endif
DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(CacheBones_AnyThread)
FAnimNode_Base::CacheBones_AnyThread(Context);
InitializeBoneReferences(Context.AnimInstanceProxy->GetRequiredBones());
ComponentPose.CacheBones(Context);
}
// Example : AnimNode_CCDIK
// UPROPERTY(EditAnywhere, Category = Effector)
// FBoneSocketTarget EffectorTarget;
/** Name of tip bone */
// UPROPERTY(EditAnywhere, Category = Solver)
// FBoneReference TipBone;
/** Name of the root bone*/
// UPROPERTY(EditAnywhere, Category = Solver)
// FBoneReference RootBone;
void FAnimNode_CCDIK::InitializeBoneReferences(const FBoneContainer& RequiredBones)
{
DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(InitializeBoneReferences)
TipBone.Initialize(RequiredBones);
RootBone.Initialize(RequiredBones);
EffectorTarget.InitializeBoneReferences(RequiredBones);
}
C++That explains why you can choose a bone from the skeletal tree in the editor of the animation blueprint panel. The cached method also called in editor time. So the FBoneReference
are initialized.
1.5 Update Method
Then comes the update method. The update method is just like the tick method we used in an actor. It is called in the ParallelUpdate
method in FAnimInstanceProxy
. If the animation cannot be updated in multi thread, the method will be finally called by the anim instance:
void UAnimInstance::UpdateAnimation(float DeltaSeconds, bool bNeedsValidRootMotion, EUpdateAnimationFlag UpdateFlag)
{
LLM_SCOPE(ELLMTag::Animation);
#if WITH_EDITOR
if(GIsReinstancing)
{
return;
}
#endif
#if DO_CHECK
checkf(!bUpdatingAnimation, TEXT("UpdateAnimation already in progress, circular detected for SkeletalMeshComponent [%s], AnimInstance [%s]"), *GetNameSafe(GetOwningComponent()), *GetName());
TGuardValue<bool> CircularGuard(bUpdatingAnimation, true);
#endif
SCOPE_CYCLE_COUNTER(STAT_UpdateAnimation);
FScopeCycleCounterUObject AnimScope(this);
// acquire the proxy as we need to update
FAnimInstanceProxy& Proxy = GetProxyOnGameThread<FAnimInstanceProxy>();
// Apply any pending dynamics reset
if(PendingDynamicResetTeleportType != ETeleportType::None)
{
Proxy.ResetDynamics(PendingDynamicResetTeleportType);
PendingDynamicResetTeleportType = ETeleportType::None;
}
if (const USkeletalMeshComponent* SkelMeshComp = GetSkelMeshComponent())
{
/**
If we're set to OnlyTickMontagesWhenNotRendered and we haven't been recently rendered,
then only update montages and skip everything else.
*/
if (SkelMeshComp->ShouldOnlyTickMontages(DeltaSeconds))
{
/**
Clear NotifyQueue prior to ticking montages.
This is typically done in 'PreUpdate', but we're skipping this here since we're not updating the graph.
A side effect of this, is that we're stopping all state notifies in the graph, until ticking resumes.
This should be fine. But if it is ever a problem, we should keep two versions of them. One for montages and one for the graph.
*/
NotifyQueue.Reset(GetSkelMeshComponent());
/**
Reset UpdateCounter(), this will force Update to occur if Eval is triggered without an Update.
This is to ensure that SlotNode EvaluationData is resynced to evaluate properly.
*/
Proxy.ResetUpdateCounter();
UpdateMontage(DeltaSeconds);
/**
We intentionally skip UpdateMontageSyncGroup(), since SyncGroup update is skipped along with AnimGraph update.
We do need to reset tick records since the montage will appear to have "jumped" if normal ticking resumes.
*/
for (FAnimMontageInstance* MontageInstance : MontageInstances)
{
MontageInstance->bDidUseMarkerSyncThisTick = false;
MontageInstance->MarkerTickRecord.Reset();
MontageInstance->MarkersPassedThisTick.Reset();
};
/**
We also intentionally do not call UpdateMontageEvaluationData after the call to UpdateMontage.
As we would have to call 'UpdateAnimation' on the graph as well, so weights could be in sync with this new data.
The problem lies in the fact that 'Evaluation' can be called without a call to 'Update' prior.
This means our data would be out of sync. So we only call UpdateMontageEvaluationData below
when we also update the AnimGraph as well.
This means that calls to 'Evaluation' without a call to 'Update' prior will render stale data, but that's to be expected.
*/
return;
}
}
#if WITH_EDITOR
if (GIsEditor)
{
// Update the lifetimer and see if we should use the snapshot instead
CurrentLifeTimerScrubPosition += DeltaSeconds;
LifeTimer = FMath::Max<double>(CurrentLifeTimerScrubPosition, LifeTimer);
if (UpdateSnapshotAndSkipRemainingUpdate())
{
return;
}
}
#endif
PreUpdateAnimation(DeltaSeconds);
// need to update montage BEFORE node update or Native Update.
// so that node knows where montage is
{
UpdateMontage(DeltaSeconds);
// now we know all montage has advanced
// time to test sync groups
UpdateMontageSyncGroup();
// Update montage eval data, to be used by AnimGraph Update and Evaluate phases.
UpdateMontageEvaluationData();
}
if (IAnimClassInterface* AnimBlueprintClass = IAnimClassInterface::GetFromClass(GetClass()))
{
AnimBlueprintClass->ForEachSubsystem(this, [this, DeltaSeconds](const FAnimSubsystemInstanceContext& InContext)
{
FAnimSubsystemUpdateContext Context(InContext, this, DeltaSeconds);
InContext.Subsystem.OnPreUpdate_GameThread(Context);
return EAnimSubsystemEnumeration::Continue;
});
}
{
SCOPE_CYCLE_COUNTER(STAT_NativeUpdateAnimation);
CSV_SCOPED_TIMING_STAT(Animation, NativeUpdate);
NativeUpdateAnimation(DeltaSeconds);
}
{
SCOPE_CYCLE_COUNTER(STAT_BlueprintUpdateAnimation);
CSV_SCOPED_TIMING_STAT(Animation, BlueprintUpdate);
BlueprintUpdateAnimation(DeltaSeconds);
}
if (IAnimClassInterface* AnimBlueprintClass = IAnimClassInterface::GetFromClass(GetClass()))
{
AnimBlueprintClass->ForEachSubsystem(this, [this, DeltaSeconds](const FAnimSubsystemInstanceContext& InContext)
{
FAnimSubsystemUpdateContext Context(InContext, this, DeltaSeconds);
InContext.Subsystem.OnPostUpdate_GameThread(Context);
return EAnimSubsystemEnumeration::Continue;
});
}
// Determine whether or not the animation should be immediately updated according to current state
const bool bWantsImmediateUpdate = NeedsImmediateUpdate(DeltaSeconds, bNeedsValidRootMotion);
// Determine whether or not we can or should actually immediately update the animation state
bool bShouldImmediateUpdate = bWantsImmediateUpdate;
switch (UpdateFlag)
{
case EUpdateAnimationFlag::ForceParallelUpdate:
{
bShouldImmediateUpdate = false;
break;
}
}
if(bShouldImmediateUpdate)
{
// cant use parallel update, so just do the work here (we call this function here to do the work on the game thread)
ParallelUpdateAnimation();
PostUpdateAnimation();
}
}
C++From here we can also figure out the calling sequence of the important animation methods:
If allows multi-threading, the parallel update method will be called by the USkeletalMeshComponent
directly.
The update method is not like the tick method for it has not a parameter called DeltaTime. But you can get it from the FAnimationUpdateContext by calling FAnimationUpdateContext::GetDeltaTime()
. The update method usually used to update the data, the latter will be used in evaluate method to generate the final pose. There are several assitant classes and structs like FInputScaleBiasClamp
. They are very simple struct. The following shows the update method of the FAnimNode_RotateRootBone
, it shows a very simple usage of the update method:
void FAnimNode_RotateRootBone::Update_AnyThread(const FAnimationUpdateContext& Context)
{
DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(Update_AnyThread)
GetEvaluateGraphExposedInputs().Execute(Context);
BasePose.Update(Context);
ActualPitch = PitchScaleBiasClamp.ApplyTo(Pitch, Context.GetDeltaTime());
ActualYaw = YawScaleBiasClamp.ApplyTo(Yaw, Context.GetDeltaTime());
TRACE_ANIM_NODE_VALUE(Context, TEXT("Pitch"), ActualPitch);
TRACE_ANIM_NODE_VALUE(Context, TEXT("Yaw"), ActualYaw);
}
float FInputScaleBiasClamp::ApplyTo(float Value, float InDeltaTime) const
{
float Result = Value;
if (bMapRange)
{
Result = FMath::GetMappedRangeValueUnclamped(InRange.ToVector2f(), OutRange.ToVector2f(), Result);
}
Result = Result * Scale + Bias;
if (bClampResult)
{
Result = FMath::Clamp<float>(Result, ClampMin, ClampMax);
}
if (bInterpResult)
{
if (bInitialized)
{
const float InterpSpeed = (Result >= InterpolatedResult) ? InterpSpeedIncreasing : InterpSpeedDecreasing;
Result = FMath::FInterpTo(InterpolatedResult, Result, InDeltaTime, InterpSpeed);
}
InterpolatedResult = Result;
}
bInitialized = true;
return Result;
}
C++1.6 Evaluate Method
There are two evaluate methods: Evaluate_AnyThread
and EvaluateComponentSpace_AnyThread
. We use the former for local space calculating and the latter for component space calculating. And we have emphasized that we use FAnimNode_Base
for local space and FAnimNode_SkeletalControlBase
for component space. So a simple conclusion is : use Evaluate_AnyThread
for common subclasses of FAnimNode_Base
and the other one for FAnimNode_SkeletalControlBase
.
The evaluate method is not that hard to analysis, we just use the data that has already been prepared, and calculate the final pose:
void FAnimNode_ApplyAdditive::Evaluate_AnyThread(FPoseContext& Output)
{
DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(Evaluate_AnyThread)
ANIM_MT_SCOPE_CYCLE_COUNTER_VERBOSE(ApplyAdditive, !IsInGameThread());
//@TODO: Could evaluate Base into Output and save a copy
if (FAnimWeight::IsRelevant(ActualAlpha))
{
const bool bExpectsAdditivePose = true;
FPoseContext AdditiveEvalContext(Output, bExpectsAdditivePose);
Base.Evaluate(Output);
Additive.Evaluate(AdditiveEvalContext);
FAnimationPoseData OutAnimationPoseData(Output);
const FAnimationPoseData AdditiveAnimationPoseData(AdditiveEvalContext);
FAnimationRuntime::AccumulateAdditivePose(OutAnimationPoseData, AdditiveAnimationPoseData, ActualAlpha, AAT_LocalSpaceBase);
Output.Pose.NormalizeRotations();
}
else
{
Base.Evaluate(Output);
}
}
C++2. Primilinary Knowledge
But after leaning the calling sequence of the FAnimNode_Base
, we still cannot start the programming. What is the difference among Component Space, Local Space, World Space and Bone Space? What is a compact pose? Now, we try to figure them out.
2.1 Four difference spaces
We start with the for spaces of the bone.
Space | Usage |
---|---|
BCS_WorldSpace | The world space, use the {0, 0, 0} in world space to generate the coordinate. |
BCS_ComponentSpace | Based on the skeletal mesh component. Generally speaking, it is the space relative to the USkeletalComponent, use the transform of the USkeletalComponent to generate the coordinate. |
BCS_ParentBoneSpace | Use the transform of the parent bone as the zero of the coordinate. |
BCS_BoneSpace | The original space of the bone. |
These spaces could convert to each other but linear algebra method, for those who have never learnt linear algebra(To be honest, buy a book now), you must know that the transform matrix is a 4×4 matrix, but in unreal engine it is stores as the compose of the translate matrix, scale matrix and the rotation matrix, instead of a composite matrix. And if you transform a object and then transform it again, in mathmetically, you just multiple the two tranform matirx to get a composite tranform matrix.
M_{transform} = M_{translate} * M_{scale} * M_{rotation} \\ M_{transform-composite} = M_{transform1} * M_{transform2}
The following method shows how to convert one space to another space:
void FAnimationRuntime::ConvertBoneSpaceTransformToCS(const FTransform& ComponentTransform, FCSPose<FCompactPose>& MeshBases, FTransform& InOutBoneSpaceTM, FCompactPoseBoneIndex BoneIndex, EBoneControlSpace Space)
{
switch( Space )
{
case BCS_WorldSpace :
InOutBoneSpaceTM.SetToRelativeTransform(ComponentTransform);
break;
case BCS_ComponentSpace :
// Component Space, no change.
break;
case BCS_ParentBoneSpace :
if( BoneIndex != INDEX_NONE )
{
const FCompactPoseBoneIndex ParentIndex = MeshBases.GetPose().GetParentBoneIndex(BoneIndex);
if( ParentIndex != INDEX_NONE )
{
const FTransform& ParentTM = MeshBases.GetComponentSpaceTransform(ParentIndex);
InOutBoneSpaceTM *= ParentTM;
}
}
break;
case BCS_BoneSpace :
if( BoneIndex != INDEX_NONE )
{
const FTransform& BoneTM = MeshBases.GetComponentSpaceTransform(BoneIndex);
InOutBoneSpaceTM *= BoneTM;
}
break;
default:
UE_LOG(LogAnimation, Warning, TEXT("ConvertBoneSpaceTransformToCS: Unknown BoneSpace %d"), (int32)Space);
break;
}
}
C++2.2 Bone index
A classic method to index the bones is using an array, use each bone for the index of the array, and the content of the array is its parent bone.
But you may find that there are 3 different bone index in Unreal Engine: Mesh Bone Index, Skeletal Bone Index and the Compact Bone Index.
The bone index we discussed below correspounds the Mesh Bone Index. And the skeletal bone index is virtural, it doesn’t actually create bone index, it just reference to the mesh bone index. the skeletal bone index is used to you are importing a mesh, choose a skeleton but there is already a skeleton in the mesh. The Unreal Engine will calculate the union and generate a skeletal bone.
When programming, what we really care about is the Compace Bone Index. That is because the animatio has LOD. When programming, we doesn’t care about the bones that has already been ignored for optimization.
3. Programming
Finnaly, programming time. Let’s create a simplest node, set the root bone to a given world location. We create a plugin called AnimationNodeExtension, which contains two module: AnimationNodeExtension for runtime and AnimationNodeExtensionEditor for editor.
Note that the editor module shoule be UncookedOnly instead of Editor.
We control the skeleton transform directly, so we start with the FAnimNode_SkeletalControlBase, this requires for two modules: AnimGraphRuntime and BlueprintGraph:
PrivateDependencyModuleNames.AddRange(
new string[]
{
"CoreUObject",
"Engine",
"Slate",
"SlateCore",
"AnimGraphRuntime",
"BlueprintGraph"
// ... add private dependencies that you statically link with here ...
}
);
C++Implement the runtime class, note that the FAnimGraphNode_SkeletalControlBase
has 1 default input and 1 default output, and initialized, cached, updated and evaluated in base class, so the subclass does not need to override it:
// AnimNode_SetRootBoneToWorldZero.h
USTRUCT(BlueprintInternalUseOnly)
struct ANIMATIONNODEEXTENSION_API FAnimNode_SetRootBoneToWorldZero final : public FAnimNode_SkeletalControlBase
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, Category="Node")
FBoneReference RootBone;
virtual void InitializeBoneReferences(const FBoneContainer& RequiredBones) override;
// We did not introduce this node, it is called in EvaluateComponentSpace_AnyThread, the parameter OutBoneTransforms
// is used to control the transform of each bone.
virtual void EvaluateSkeletalControl_AnyThread(FComponentSpacePoseContext& Output, TArray<FBoneTransform>& OutBoneTransforms) override;
// used to show debug content, press "~" and type in "showdebug animation" to display the information here
virtual void GatherDebugData(FNodeDebugData& DebugData) override;
// This is default to false so I spend a lot of time wasting my time.
FORCEINLINE virtual bool IsValidToEvaluate(const USkeleton* Skeleton, const FBoneContainer& RequiredBones) override {return true;}
};
// // AnimNode_SetRootBoneToWorldZero.cpp
void FAnimNode_SetRootBoneToWorldZero::InitializeBoneReferences(const FBoneContainer& RequiredBones)
{
FAnimNode_SkeletalControlBase::InitializeBoneReferences(RequiredBones);
RootBone.Initialize(RequiredBones);
}
void FAnimNode_SetRootBoneToWorldZero::EvaluateSkeletalControl_AnyThread(FComponentSpacePoseContext& Output,
TArray<FBoneTransform>& OutBoneTransforms)
{
const FCompactPoseBoneIndex RootBoneIndex = RootBone.GetCompactPoseIndex(Output.Pose.GetPose().GetBoneContainer());
const FTransform OriginalRootBoneTransform = Output.Pose.GetPose().GetBoneContainer().GetRefPoseTransform(RootBoneIndex);
const FAnimInstanceProxy* AnimInstanceProxy = Output.AnimInstanceProxy;
// The location of WorldZeroTransform is in world space, but the data in out bone transforms should be
// component-space, so we convert it to the component space here.
FTransform WorldZeroTransform;
WorldZeroTransform.SetLocation({0, 0, 0});
FAnimationRuntime::ConvertBoneSpaceTransformToCS(AnimInstanceProxy->GetComponentTransform(), Output.Pose, WorldZeroTransform, RootBoneIndex, BCS_WorldSpace);
OutBoneTransforms.Add({RootBoneIndex, WorldZeroTransform});
// There are used to debug, draw the location we need so that we can check the result.
DrawDebugSphere(AnimInstanceProxy->GetAnimInstanceObject()->GetWorld(), OriginalRootBoneTransform.GetLocation(), 50.0f, 16, FColor::Red);
DrawDebugSphere(AnimInstanceProxy->GetAnimInstanceObject()->GetWorld(), {0, 0, 0}, 50.0f, 16, FColor::Green);
}
void FAnimNode_SetRootBoneToWorldZero::GatherDebugData(FNodeDebugData& DebugData)
{
FAnimNode_SkeletalControlBase::GatherDebugData(DebugData);
const FString NodeName = DebugData.GetNodeName(this);
const FString DebugContent{TEXTVIEW("Set root bone to world location {0, 0, 0}")};
DebugData.AddDebugItem(NodeName + DebugContent);
}
C++The editor time module requires for more: UnrealEd(Most of the editor class need it), AnimGraph, BlueprintGraph, Kismet, KismetCompiler, and of course our AnimationNodeExtension.
PrivateDependencyModuleNames.AddRange(
new string[]
{
"CoreUObject",
"Engine",
"Slate",
"SlateCore",
"UnrealEd",
"BlueprintGraph",
"AnimGraph",
"Kismet",
"KismetCompiler",
"AnimationNodeExtension"
}
);
C++Then implement the editor class, as we have discussed, we inherits from UAnimGraphNode_SkeletalControlBase
instead of UAnimGraphNode_Base
:
UCLASS()
class ANIMATIONNODEEXTENSIONEDITOR_API UAnimGraphNode_SetRootBoneToWorldZero final : public UAnimGraphNode_SkeletalControlBase
{
GENERATED_BODY()
public:
// The editor node must contains a runtime node.
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FAnimNode_SetRootBoneToWorldZero Node;
// The following methods are familiar if you have read my posts about the custom graph editor.
virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override;
virtual FLinearColor GetNodeTitleColor() const override;
virtual FText GetTooltipText() const override;
virtual FString GetNodeCategory() const override;
// This is method belongs to the UAnimGraphNode_SkeletalControlBase
FORCEINLINE virtual const FAnimNode_SkeletalControlBase* GetNode() const override {return &Node;}
};
C++The class is implemented as:
FText UAnimGraphNode_SetRootBoneToWorldZero::GetNodeTitle(ENodeTitleType::Type TitleType) const
{
return NSLOCTEXT("Animation Graph Node Extension", "Set Root Bone To World Zero Node Title", "Set Root Bont To World Zero");
}
FLinearColor UAnimGraphNode_SetRootBoneToWorldZero::GetNodeTitleColor() const
{
return FLinearColor::Blue;
}
FText UAnimGraphNode_SetRootBoneToWorldZero::GetTooltipText() const
{
return NSLOCTEXT("Animation Graph Node Extension", "Set Root Bone To World Zero Node Tooltip", "Set root bont to world zero, a custom node");
}
FString UAnimGraphNode_SetRootBoneToWorldZero::GetNodeCategory() const
{
return {"Animation|Custom"};
}
C++Use the 3rd template as an example, note that the animation blueprint for female is a sub class of the blueprint for male, so we only modify the parent animation blueprint graph:
Now run the game and press “F8” to check your character, you can find that it works: