This page looks best with JavaScript enabled

Creating New Anim Nodes Pt. 1

 ·  ☕ 7 min read  ·  ✍️ Taylor

Intro

The next few articles on this blog are going to go over creating an AnimNode that can be used in an AnimBP in UE4/UE5. I’ll try to keep it light and gloss over some of the more obvious parts of this system while diving into how the nodes actually work and do their thing.

There’s a few things you really need to know about and semi-understand before you jump into this though:

Bone Indexes

Read this link on how Unreal handles bone indexes. (Author’s twitter)

So for our purposes, the most important bone index will be FCompactPoseBoneIndex. Basically, if a bone has a valid compact index, that means it is currently being used for pose evaluation. If the Index turns out to be INDEX_NONE, that means the bone has been LODed out or something and we don’t want to operate on it (re: can’t operate on it. It will crash the engine. Wop wop.).

Spaces

We are going to operate completely in ComponentSpace, NOT LocalSpace. Think of ComponentSpace as WorldSpace if the origin was at the origin of the SkeletalMeshComponent.

What does this mean for us in practice? We cannot rely on local space child-parent relationships to help with our transformations. We need to mess with every bone as if its in world space, ‘cause it is.

FBoneReference

Read FBoneReference in BoneContainer.h. BoneRefs will be your best friend. They contain the name of the bone, its absolute (skeleton) index, its CompactPoseIndex, as well as the ability to evaluate if the bone is able to be used, and initialize it if not. They’re an easy way to transport all the data we will be using, and they’re very light-weight structs.

Class Overview

AnimNodes consist of two mandatory parts:

  • FAnimNode class
    • This is the runtime element to the node. This is where all the pose evaluation actually happens.
    • Will be derived from FAnimNode_Base or FAnimNode_SkeletalControlBase. This article will assume the latter, as we will be making a node that operates in ComponentSpace, not LocalSpace.
    • MUST be placed into a Runtime module AND be exported (for instance, with a MYMODULE_API specifier). The graph node will need to access this stuff.
    • To your Runtime module, you must include the AnimGraphRuntime module as a public dependency.
  • FAnimGraphNode class
    • This is the editor node that you place into the AnimBP graph. This simply displays the info of the FAnimNode, and well as some debug capabilities.
    • Will derive from FAnimGraphNode_Base or FAnimGraphNode_SkeletalControlBase. Again, we will assume the latter.
    • MUST be placed into an UncookedOnly or Developer module. I have mine in an UncookedOnly K2Node-specific module.
      • The docs say this class can be in an Editor module, and that would make sense, but when I did that I got a warning (not an error, but still). It might still work in an Editor module but I’m not sure.
    • To your UncookedOnly/Developer module, you must include the AnimGraph module as a public dependency.

Where to start

Open up AnimNode_TwoBoneIK.h/.cpp and AnimGraphNode_TwoBoneIK.h/.cpp These are great classes to look at to get a feel for how these two parts interconnect, and how the runtime class will operate. Also, read FAnimNode_Base. Read it. Seriously, read the whole thing. The comments on all the *_AnyThread methods are incredibly helpful.

Design

So, what do we want this node to actually do?

It will take in the mesh’s pose in component space, and based on a curve and some other parameters, will scale the joints of a chain to the desired amount.

I can see a few applications for something like this, but mostly I’m doing it because it shows most aspects of what an anim node can do.

Runtime Class

So let’s start with our Runtime class first. Create two new files in your runtime module called AnimNode_GradualScaleJoints.h/.cpp.

 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
#pragma once

#include "AnimGraphRuntime/Public/BoneControllers/AnimNode_SkeletalControlBase.h"
#include "BoneContainer.h"
#include "BonePose.h"
#include "CoreMinimal.h"
#include "AnimNode_GradualScaleJoints.generated.h"

USTRUCT(BlueprintInternalUseOnly)
struct MYMODULE_API FAnimNode_GradualScaleJoints : public FAnimNode_SkeletalControlBase
{
   GENERATED_BODY();
public:

   FAnimNode_GradualScaleJoints();

   // FAnimNode_Base interface
   virtual void GatherDebugData(FNodeDebugData& debugData) override;
   virtual bool NeedsOnInitializeAnimInstance() const override { return true; }
   // End of FAnimNode_Base interface

   // FAnimNode_SkeletalControlBase interface
   virtual void EvaluateSkeletalControl_AnyThread(FComponentSpacePoseContext& output, TArray<FBoneTransform>& outBoneTransforms) override;
   virtual bool IsValidToEvaluate(const USkeleton* skeleton, const FBoneContainer& requiredBones) override;
   // End of FAnimNode_SkeletalControlBase interface

private:
   // FAnimNode_SkeletalControlBase interface
   virtual void InitializeBoneReferences(const FBoneContainer& requiredBones) override;
   // End of FAnimNode_SkeletalControlBase interface

   /* The bone to start the scale at. Everything below this will be scaled by depth. */
   UPROPERTY(EditAnywhere, Category = "Gradual Scale Joints")
   FBoneReference ChainStart;
   
   /* The float curve that will drive the scale of the joints */
   UPROPERTY(EditAnywhere, Category = "Gradual Scale Joints")
   UCurveFloat* ScaleCurve;
};
 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
43
44
45
46
#include "Animation/AnimNode_GradualScaleJoints.h"
#include "Animation/AnimInstanceProxy.h"
#include "AnimationCoreLibrary.h"
#include "AnimationRuntime.h"

DECLARE_CYCLE_STAT(TEXT("Gradual Scale Joints Eval"), STAT_GradualScaleJoints_Eval, STATGROUP_Anim);

/////////////////////////////////////////////////////
// FAnimNode_GradualScaleJoint

FAnimNode_GradualScaleJoints::FAnimNode_GradualScaleJoints()
   : ScaleCurve(nullptr)
{
}

void FAnimNode_GradualScaleJoints::GatherDebugData(FNodeDebugData& debugData)
{
   DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(GatherDebugData)

   FString DebugLine = debugData.GetNodeName(this);

   DebugLine += "(";
   AddDebugNodeData(DebugLine);
   DebugLine += FString::Printf(TEXT(")"));
   debugData.AddDebugItem(DebugLine);

   ComponentPose.GatherDebugData(debugData);

}

void FAnimNode_GradualScaleJoints::EvaluateSkeletalControl_AnyThread(FComponentSpacePoseContext& output,
   TArray<FBoneTransform>& outBoneTransforms)
{
   DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(EvaluateSkeletalControl_AnyThread)
   SCOPE_CYCLE_COUNTER(STAT_GradualScaleJoints_Eval);
}

bool FAnimNode_GradualScaleJoints::IsValidToEvaluate(const USkeleton* skeleton, const FBoneContainer& requiredBones)
{
   return true;
}

void FAnimNode_GradualScaleJoints::InitializeBoneReferences(const FBoneContainer& requiredBones)
{
   DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(InitializeBoneReferences)
}

You’ll see a ton of boilerplate here. A few of these are pure virtual and need to be implemented, a few don’t. Fill out these methods in the .cpp so it will all compile, but leave it all blank for now. I will go over what all of these do in the next part when we get to order-of-operations talk.

We also went to set up the parameters we will need: the bone we want to start the scale chain at, and the curve that will drive the scale. We will add a few more parameters to this in the next part, but for now this will suffice.


This is all we need to create and view the...

Editor Class

Create two new files in your Uncooked/Developer module called AnimGraphNode_GradualScaleJoints.h/.cpp. These are going to be super simple. The underlying native code does all the heavy lifting for us.

 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
#pragma once

#include "AnimGraph/Classes/AnimGraphNode_SkeletalControlBase.h"
#include "Animation/AnimNode_GradualScaleJoints.h"
#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "AnimGraphNode_GradualScaleJoints.generated.h"

UCLASS(MinimalAPI)
class UAnimGraphNode_GradualScaleJoints : public UAnimGraphNode_SkeletalControlBase
{
   GENERATED_BODY()

public:

   // UEdGraphNode interface
   virtual FText GetNodeTitle(ENodeTitleType::Type titleType) const override;
   virtual FText GetTooltipText() const override;
   // End of UEdGraphNode interface

protected:
   
   // UAnimGraphNode_SkeletalControlBase interface
   virtual FText GetControllerDescription() const override;
   virtual const FAnimNode_SkeletalControlBase* GetNode() const override { return &Node; }
   virtual void Draw(FPrimitiveDrawInterface* PDI, USkeletalMeshComponent * previewSkelMeshComp) const override;
   // End of UAnimGraphNode_SkeletalControlBase interface

private:
   
   UPROPERTY(EditAnywhere, Category = Settings)
   FAnimNode_GradualScaleJoints Node;
   
};
 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
#include "Animation/AnimGraphNode_GradualScaleJoints.h"
#include "Components/SkeletalMeshComponent.h"

/////////////////////////////////////////////////////
// UAnimGraphNode_GradualScaleJoints

#define LOCTEXT_NAMESPACE "A3Nodes"

FText UAnimGraphNode_GradualScaleJoints::GetControllerDescription() const
{
   return LOCTEXT("GradualScaleJoints", "Gradual Scale Joints");
}

FText UAnimGraphNode_GradualScaleJoints::GetNodeTitle(ENodeTitleType::Type titleType) const
{
   return GetControllerDescription();
}

FText UAnimGraphNode_GradualScaleJoints::GetTooltipText() const
{
   return LOCTEXT("AnimGraphNode_GradualScaleJoints_Tooltip", "Scales a chain of joints based on a curve and a delta values");
}

void UAnimGraphNode_GradualScaleJoints::Draw(FPrimitiveDrawInterface* PDI, USkeletalMeshComponent* previewSkelMeshComp) const
{

}

#undef LOCTEXT_NAMESPACE

Notice the UPROPERTY near the top: FAnimNode_GradualScaleJoints Node. This is the runtime class. You don’t need to worry about exposing all of the class’s parameters to the graph node; it’s automagical.
The Draw method is NOT needed. Used for debug, whenever you click on the node in the anim graph, the Draw method will be called.

As long as you have everything set up correctly, you should be able to open an animBP and add the node to it:

In the next part, we’ll go over adding some needed utility functionality, the order of operations, and doing the actual transformations.

Share on