In the last part we created the AnimNode_GradualScaleJoints anim node and initialized it in such a way that we could add it to an AnimBP graph. In this part I’ll go over the individual parts of the AnimNode class, order of operations, and how we want to go about the joint scaling.
General thoughts
As with anything involving runtime animation, remember to always be as lean as possible. Animation runs every single frame. You don’t want to be doing complex validations or transformations if you can help it. Unreal does help out with this by multithreading this stuff, but that isn’t an excuse to write bad code and abuse the system.
Design Pt. 2
So now that we have the node running in our AnimBP, let’s revisit what we want this node to do.
We want to drive the scale of bones based on the values along a user-generate curve. To do this, we need to know a few things:
The depth of any particular joint relative to the ChainStart joint. Is it a direct child? Is it a distant relative? We need to know this to grab the float value from the curve at the correct time along the curve.
Do we want to mask off certain axes of the joints that the scale won’t apply to? For me this was the X axis, down the bones. It looks really bad.
Remember, we are operating in ComponentSpace. Scaling a parent bone WILL NOT translate a child bone in world space. Sure, we could figure out all the math needed to do that, but it would probably be heavy and it’s outside the scope of this guide.
Do we want the scale to go from the ChainStart all the way to the very end of the chain? Or do we want it to have a set depth it will operate over?
If we do have a set depth we want to scale over, do we want the scale of the deepest bone we’re scaling to apply to all its children? So for instance, Hand_L is scaled to 1.5. Do we want all the fingers to be locked in at 1.5 as well?
In order to accomplish this all we’re going to add some stuff to the AnimNode_GradualScaleJoints.h file.
The FBoneReferenceDepth struct will be a lightway way for us to gather a BoneReference/BoneDepth pair and pass them around. We could possibly use a TMap<FBoneReference, int32> to store this stuff, but I would be hesitate to do so because of the threading involved in evaluation.
The FAxisMask struct is pretty self-explanatory. It’s a simple way for us to display each axis bool to the user. If this node were part of a larger ecosystem I would likely move this struct out into its own namespace for reuse.
Once these are made, add a few more params to the class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Mask off the scale from each axis individually */UPROPERTY(EditAnywhere,Category="Gradual Scale Joints")FAxisMaskAxisMasks;/* The maximum hierarchy depth to which we will do the scale. If set to -1, we will scale to the end of the chain */UPROPERTY(EditAnywhere,Category="Gradual Scale Joints")int16MaximumDepth;/* If true, we will set the scale to all joints DEEPER THAN MaximumDepth to the scale of the joint AT MaximumDepth */UPROPERTY(EditAnywhere,Category="Gradual Scale Joints")boolContinuousScale;TArray<FBoneReferenceDepth>BoneRefDepthBuckets;int16foundMaxDepth;int16effectiveMaxDepth;
foundMaxDepth is the greatest hierarchy depth from our ChainStart to the very end of the chain. We need this to scale our FloatCurve values later on
effectiveMaxDepth will be used to determine where we want the scale to end: the very end of the chain, or some arbitrary, user-defined depth?
This is it for the .h file. Now, I’ll jump into the order of operations AnimNodes go through to arrive at a final, evaluated pose.
Order of Operations
Initialize_AnyThread
This is used to set up any objects we need for the rest of the calculation. For instance, if you need to initialize some sockets, it will be done here. We don’t need to do that so we won’t be using this.
This will run when the graph is compiled, and when the SkeletalMeshComponent’s LOD changes.
CacheBones_AnyThread
This is used to cache incoming poses. In the case of the SkeletalControl anim node, that is the ComponentPose pin that plugs into the node. This will run ONCE when a mesh LOD is initialized.
We can use this, but we won’t, because…
InitializeBoneReferences
…it calls InitializeBoneReferences, so we will use this.
Just like the CacheBones_AnyThread method, InitializeBoneReferences only runs once when a mesh LOD is initialized. Therefore, we want to do as much caching and validation here as possible to make the per-frame operations later cheap and fast.
This is where we want to take our ChainStart and grab all of its child bones, then order those bones into buckets based on depth from ChainStart.
You must ALWAYS initialize BoneReferences with a BoneContainer. Once they are, we can ask IsValidToEvaluate and continue on with our initialize step.
FReferenceSkeleton has a ton of useful functionality. Here, we will be using GetDepthBetweenBones. It will return an int with the depth between two joints, or -1 if they aren’t in a parent/child relationship.
Once we gather all of our child joints into our depth buckets, we need to determine what our effectiveMaxDepth should be. Note: if you want this to change during runtime, move it to the EvaluateSkeletalControl_AnyThread method. I didn’t need that for my purposes so it’s here.
IsValidToEvaluate
This is a preliminary go/no-go for the evaluation step. If this returns true, we evaluate; false, we exit.
This runs every single frame, so don’t do any calculation or anything here. This should be a fairly simple check that everything on the node is correct, valid, and can be evaluated.
This is the final go/no-go. If a BoneReference has an INDEX_NONECompactPoseIndex, that means it is not in the current reference skeleton, ie. has been LOD’d out. We CANNOT operate on a bone that has been LOD’d. It’ll crash. This check ensures that everything we touch from here on out is valid to be worked on.
This is pretty self-explanatory, right? If the depth of the bone we’re working on is less than or equal to the depth we want to scale to, we find where that depth would line up on our float curve, and set the scale value to that value.
If it doesn’t, which means we are beyond the desired depth, our scale should just be 1.f…unless we have ContinuousScale set to true, in which case we want to set our scale to the final value on the float curve.
Finally, we’ll grab the ComponentSpaceTransform of the bone we’re working on, set it’s scale according to the masks we have set, and then add our transform to the outBoneTransforms array.
Now, one final note: you CANNOT evaluate bones out of order. You can’t do a child before a parent. Therefore, we sort the array after every addition using the FCompareBoneTransformIndex predicate. This ensures our array is always in parent->child order.
Final Steps
Now you just need to create a FloatCurve asset in the browser, set all your parameters on the node, and start messing around.
I hope this helps a bit. If you have any questions, comments, or suggestions, leave a comment or ping me on Twitter!
#pragma once
#include"AnimGraphRuntime/Public/BoneControllers/AnimNode_SkeletalControlBase.h"#include"BoneContainer.h"#include"BonePose.h"#include"CoreMinimal.h"#include"AnimNode_GradualScaleJoints.generated.h"USTRUCT()structFBoneReferenceDepth{GENERATED_BODY()FBoneReferenceBoneReference;int32BoneDepth;FBoneReferenceDepth(){BoneReference=FBoneReference();BoneDepth=-1;}FBoneReferenceDepth(constFBoneReference&boneReference,constint32boneDepth){BoneReference=boneReference;BoneDepth=boneDepth;}};USTRUCT()structFAxisMask{GENERATED_BODY()UPROPERTY(EditAnywhere)boolMaskX;UPROPERTY(EditAnywhere)boolMaskY;UPROPERTY(EditAnywhere)boolMaskZ;FAxisMask(){MaskX=false;MaskY=false;MaskZ=false;}};USTRUCT(BlueprintInternalUseOnly)structMYMODULE_APIFAnimNode_GradualScaleJoints:publicFAnimNode_SkeletalControlBase{GENERATED_BODY();public:FAnimNode_GradualScaleJoints();// FAnimNode_Base interface
virtualvoidGatherDebugData(FNodeDebugData&debugData)override;virtualboolNeedsOnInitializeAnimInstance()constoverride{returntrue;}// End of FAnimNode_Base interface
// FAnimNode_SkeletalControlBase interface
virtualvoidEvaluateSkeletalControl_AnyThread(FComponentSpacePoseContext&output,TArray<FBoneTransform>&outBoneTransforms)override;virtualboolIsValidToEvaluate(constUSkeleton*skeleton,constFBoneContainer&requiredBones)override;// End of FAnimNode_SkeletalControlBase interface
private:// FAnimNode_SkeletalControlBase interface
virtualvoidInitializeBoneReferences(constFBoneContainer&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")FBoneReferenceChainStart;/* The float curve that will drive the scale of the joints */UPROPERTY(EditAnywhere,Category="Gradual Scale Joints")UCurveFloat*ScaleCurve;/* Mask off the scale from each axis individually */UPROPERTY(EditAnywhere,Category="Gradual Scale Joints")FAxisMaskAxisMasks;/* The maximum hierarchy depth to which we will do the scale. If set to -1, we will scale to the end of the chain */UPROPERTY(EditAnywhere,Category="Gradual Scale Joints")int16MaximumDepth;/* If true, we will set the scale to all joints DEEPER THAN MaximumDepth to the scale of the joint AT MaximumDepth */UPROPERTY(EditAnywhere,Category="Gradual Scale Joints")boolContinuousScale;TArray<FBoneReferenceDepth>BoneRefDepthBuckets;int16foundMaxDepth;int16effectiveMaxDepth;};
#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)classUAnimGraphNode_GradualScaleJoints:publicUAnimGraphNode_SkeletalControlBase{GENERATED_BODY()public:// UEdGraphNode interface
virtualFTextGetNodeTitle(ENodeTitleType::TypetitleType)constoverride;virtualFTextGetTooltipText()constoverride;// End of UEdGraphNode interface
protected:// UAnimGraphNode_SkeletalControlBase interface
virtualFTextGetControllerDescription()constoverride;virtualconstFAnimNode_SkeletalControlBase*GetNode()constoverride{return&Node;}virtualvoidDraw(FPrimitiveDrawInterface*PDI,USkeletalMeshComponent*previewSkelMeshComp)constoverride;// End of UAnimGraphNode_SkeletalControlBase interface
private:UPROPERTY(EditAnywhere,Category=Settings)FAnimNode_GradualScaleJointsNode;};
#include"Animation/AnimGraphNode_GradualScaleJoints.h"#include"Components/SkeletalMeshComponent.h"/////////////////////////////////////////////////////
// UAnimGraphNode_GradualScaleJoints
#define LOCTEXT_NAMESPACE "A3Nodes"
FTextUAnimGraphNode_GradualScaleJoints::GetControllerDescription()const{returnLOCTEXT("GradualScaleJoints","Gradual Scale Joints");}FTextUAnimGraphNode_GradualScaleJoints::GetNodeTitle(ENodeTitleType::TypetitleType)const{returnGetControllerDescription();}FTextUAnimGraphNode_GradualScaleJoints::GetTooltipText()const{returnLOCTEXT("AnimGraphNode_GradualScaleJoints_Tooltip","Scales a chain of joints based on a curve and a delta values");}voidUAnimGraphNode_GradualScaleJoints::Draw(FPrimitiveDrawInterface*PDI,USkeletalMeshComponent*previewSkelMeshComp)const{}#undef LOCTEXT_NAMESPACE