12 octobre 2015

Creating Custom Animation Nodes

Par Lina Halper

Anim Blueprints provide animation nodes that run specific actions such as blending multiple nodes based on alpha value or playing an animation. Here, you can find additional information on basic Anim Blueprint nodes.

These nodes provide standard features you will need, but often when you work on an animation you will want to create custom nodes. This is fairly easy to do, but it requires an understanding of how the base system is designed. This link provides in-depth knowledge of the system, but I’d like to emphasize a bit more on the basic system here since I see people often struggle with it.

As described in the link above, the system requires two classes; one graph node you see in editor and one behavior node that does actual run-time work. We've made this separation for optimization purposes. The node construction tends to be very expensive, and having 20 characters dying/spawning every 30 seconds with more than 400 nodes can be taxing on memory and the CPU. If you spawn a character using Anim Blueprints, the character will not have any graph nodes, but only behavior nodes. 

Let's compare the Anim Graph Node and the Anim Behavior Node code:

Anim Graph Node

class UAnimGraphNode_SequencePlayer : public UAnimGraphNode_Base

Anim Behavior Node 

struct ENGINE_API FAnimNode_SequencePlayer : public FAnimNode_Base

You will notice both are from different base class; one is UObject, and the other is UStruct. In this blog, we'll refer to the first as Anim Graph Node and the latter as Anim Behavior Node. All graph nodes contain the corresponding behavior node like this: 

class UAnimGraphNode_SequencePlayer : public UAnimGraphNode_Base
{
    GENERATED_UCLASS_BODY()

    UPROPERTY(EditAnywhere, Category=Settings)
    FAnimNode_SequencePlayer Node;

}

The Anim Graph Node knows about the other node, but not the other way around, a very important distinction. All Anim Graph Nodes exist in the editor module for that reason, as it does not load with game but only exists in the editor. On the other hand, the behavior node exists in run-time code where the real blending happens. Make sure your class points to the right module. 

The same is true for the Skeletal Control node.

Anim Graph Node

class UAnimGraphNode_ModifyBone : public UAnimGraphNode_SkeletalControlBase

Anim Behavior Node

struct ENGINE_API FAnimNode_ModifyBone : public FAnimNode_SkeletalControlBase

You'll notice that the Skeletal Control Node also has a different base class. 

The system is designed so the Anim Graph Node is responsible for any editor work such as showing the node name, displaying tooltips or creating custom pins. The Anim Behavior Node is responsible for the actual work such as blending, calculating target position, and outputing the correct pose. So the Anim Graph Node is important in the editor, while the Anim Behavior Node will be the one that matters at runtime.

Again, reference this link to see how the meta data of the variable can change to the input or output of the node.

For example, FPoseLink is the pose link that passes array of bone transform, and if you can declare it like below, it will show up as in the picture.

UPROPERTY(Category=Links)
FPoseLink BasePose;

Knowing how FPoseLink works is important because whenever you're calling any animation function, you'll also have to call the Pose functions. For example, you should call BasePose->Update in your Update function. Likewise, you also should call BasePose->CacheBones in your CacheBones function if you have BasePose as a member variable.

Now, I'd like to talk about the functions that you should focus on for each node type. I am not going to focus on the Anim Graph Node because its work is pretty similar with any other blueprint node, but I’d like to focus on the actual working node. 

Let’s take a look at FAnimNode_Base:

struct ENGINE_API FAnimNode_Base
{
    // Interface to implement
    virtual void Initialize(const FAnimationInitializeContext& Context) {}
    virtual void Update(const FAnimationUpdateContext& Context) {}
    virtual void Evaluate(FPoseContext& Output) { check(false); }
    virtual void CacheBones(const FAnimationCacheBonesContext& Context) {}
    virtual void GatherDebugData(FNodeDebugData& DebugData){}

};

It's not this simple, but I'm summarizing to focus only on the major things you should care about. 

There are 3 main functions that determine how your node will behave. These are Initialize, Update and Evaluate, and here's a brief description of their use:

  • Initialize - Called whenever we need to Initialize/Reinitialize (when changing mesh for instance).
  • Update - Called to update current state (such as advancing play time or updating blend weights). This function takes an FAnimationUpdateContext that knows the DeltaTime for the update and the current nodes blend weight.
  • Evaluate - Called to generate a ‘pose’ (list of bone transforms).
    • Example:
      • void FAnimNode_SequenceEvaluator::Evaluate(FPoseContext& Output)
        {
            if ((Sequence != NULL) && (Output.AnimInstance->CurrentSkeleton->IsCompatible(Sequence->GetSkeleton())))
            {
                Output.AnimInstance->SequenceEvaluatePose(Sequence, Output.Pose, FAnimExtractContext(ExplicitTime));
            }
            else
            {
                Output.ResetToRefPose();
            }
        }

    • Evaluate checks if Sequence is set and if it’s compatible with the current Skeleton. If so, it fills up the bone transform to Output.Pose. If not, Output is set to the reference pose. ​

 

On top of these basic functions, you will need to provide implementations of two functions to ensure that your node works well with the rest of the graph: 

virtual void CacheBones(const FAnimationCacheBonesContext& Context) {}
virtual void GatherDebugData(FNodeDebugData& DebugData){}

CacheBones is used for refreshing bone indices that are referenced by the node, and GatherDebugData is used for debugging using "ShowDebug Animation" data. These are important to use in order to preserve the connection to children. As I mentioned earlier, FPoseLink should call whatever is below to make sure any pose link that your node is connected to also get called. 

See this example:

void FAnimNode_BlendListBase::CacheBones(const FAnimationCacheBonesContext& Context)
{
    for(int32 ChildIndex=0; ChildIndex<BlendPose.Num(); ChildIndex++)
    {
        BlendPose[ChildIndex].CacheBones(Context);
    }
}

You want to implement it so those events won’t get stopped by your node.

Also note that we have FAnimationRuntime which provides lots of functionalities when it comes to animation. 

Here's an example for Skeletal Control nodes: 

struct ENGINE_API FAnimNode_SkeletalControlBase : public FAnimNode_Base
{
    // FAnimNode_Base interface
    virtual void Initialize(const FAnimationInitializeContext& Context) override;
    virtual void CacheBones(const FAnimationCacheBonesContext& Context)  override;
    virtual void Update(const FAnimationUpdateContext& Context) override;
    virtual void EvaluateComponentSpace(FComponentSpacePoseContext& Output) override;
    // End of FAnimNode_Base interface
}

This is simliar to Anim Node but different since the Skeletal Control Node works on Component space. Again, checking out FAnimationRuntime will show you a good way to convert between Local and Component space. 

You will commonly use EvaluateComponentSpace. Here's a simple example use case that involves the CopyBone node:

void FAnimNode_CopyBone::EvaluateBoneTransforms(USkeletalMeshComponent* SkelComp, FCSPose<FCompactPose>& MeshBases, TArray<FBoneTransform>& OutBoneTransforms)
{
    check(OutBoneTransforms.Num() == 0);
    // Pass through if we're not doing anything.
    if( !bCopyTranslation && !bCopyRotation && !bCopyScale )
    {
        return;
    }
    // Get component space transform for source and current bone.
    const FBoneContainer& BoneContainer = MeshBases.GetPose().GetBoneContainer();
    FCompactPoseBoneIndex TargetBoneIndex = TargetBone.GetCompactPoseIndex(BoneContainer);
    const FTransform& SourceBoneTM = MeshBases.GetComponentSpaceTransform(SourceBone.GetCompactPoseIndex(BoneContainer));
    FTransform CurrentBoneTM = MeshBases.GetComponentSpaceTransform(TargetBoneIndex);
    // Copy individual components
    if (bCopyTranslation)
    {
        CurrentBoneTM.SetTranslation( SourceBoneTM.GetTranslation() );
    }
    if (bCopyRotation)
    {
        CurrentBoneTM.SetRotation( SourceBoneTM.GetRotation() );
    }
    if (bCopyScale)
    {
        CurrentBoneTM.SetScale3D( SourceBoneTM.GetScale3D() );
    }
    // Output new transform for current bone.
    OutBoneTransforms.Add(FBoneTransform(TargetBoneIndex, CurrentBoneTM));
}

The goal is to return OutBoneTransforms with the data you want. You can return as many bone transforms as you want, but be mindful of the hierarchy. It is always safe to preserve the order from parent to children. 

I hope this explains a bit to make it easier to start creating your own custom nodes.