Published on

自定义动画节点AnimGraphNode

前言

录制_2021_08_03_16_45_57_443

首先要准备两个类, 一个继承自UAnimGraphNode_Base为蓝图节点类, 还有一个是数据类FAnimNode_Base

UAnimGraphNode_Base

这个类定义了节点的显示方式,分类以及引脚连接方式等操作, 然后最主要的是要在头文件申明一个数据类, 如下

UCLASS()
class UTILITY_API UMyAnimGraphNode_T01 : public UAnimGraphNode_Base
{
	GENERATED_BODY()
	
	UPROPERTY(EditAnywhere, Category = Settings)
	FAnimNode_Test01 Node;
    //..................
};

不需要对这个数据类做任何操作

FAnimNode_Base

我们在动画蓝图里点击节点以后显示的所有变量都是申明在这个类里面

比如我们申明这么一个数据类, 打算做一个简单的Blend操作

USTRUCT(BlueprintInternalUseOnly)
struct  UTILITY_API FAnimNode_Test01 : public FAnimNode_Base
{
	GENERATED_BODY()
public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Links)
		FPoseLink Pose;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Links)
		FPoseLink OtherPose;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Alpha, meta = (PinShownByDefault))
		float Alpha = 1.0;
};

然后就显示如下

image-20210803160905680

我们重写下面几个虚函数

// FAnimNode_Base interface
virtual void Initialize_AnyThread(const FAnimationInitializeContext& Context) override;
virtual void CacheBones_AnyThread(const FAnimationCacheBonesContext& Context) override;
virtual void Update_AnyThread(const FAnimationUpdateContext& Context) override;
virtual void Evaluate_AnyThread(FPoseContext& Output) override;
// End of FAnimNode_Base interface

其中Initialize_AnyThreadCacheBones_AnyThread会在初始化(编译)的时候先后调用, 每次调用2次

这俩函数就模仿其他基础的节点做一个基础的操作即可

CacheBones_AnyThread用于刷新该节点所引用的骨骼索引

void FAnimNode_Test01::Initialize_AnyThread(const FAnimationInitializeContext& Context)
{
	DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(Initialize_AnyThread)
	FAnimNode_Base::Initialize_AnyThread(Context);

	Pose.Initialize(Context);
	OtherPose.Initialize(Context);
}

void FAnimNode_Test01::CacheBones_AnyThread(const FAnimationCacheBonesContext& Context)
{
	DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(CacheBones_AnyThread)

	Pose.CacheBones(Context);
	OtherPose.CacheBones(Context);

}

然后就是两个Tick函数, 也是按照先后顺序调用, 看下图

image-20210803145311983

起点分别是上图中红色框部分, TickPose最后执行的是Update_AnyThread

调用该函数来更新当前状态(比如更新播放时间或混合权重)。该函数取入一个FAnimationUpdateContext,它知道更新的DeltaTime和当前的节点混合权重。

image-20210803163309807

比如我们的节点有Alpha的存在, 就可以在Update中通过这个Alpha做一些预处理

void FAnimNode_Test01::Update_AnyThread(const FAnimationUpdateContext& Context)
{
	DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(Update_AnyThread);
	GetEvaluateGraphExposedInputs().Execute(Context);

	float InternalBlendAlpha = FMath::`Clamp<float>`(Alpha, 0.f, 1.f);

	if (FAnimWeight::IsRelevant(InternalBlendAlpha))
	{
		Pose.Update(Context.FractionalWeight(1.0f - InternalBlendAlpha));
		OtherPose.Update(Context.FractionalWeight(InternalBlendAlpha));
	}
	else
	{
		Pose.Update(Context);
	}

}

然后再执行Evaluate_AnyThread

也就是在执行Evaluate_AnyThread之前, 用到的Pose的数据已经经过了权重计算了

调用该函数来生成一个‘姿势’(一系列的骨骼变换)。当动画图表节点的输出是FPoseLink时,执行的是该函数, 如果是FComponetSpacePoseLink,执行的应该是EvaluateComponentSpace_AnyThread

void FAnimNode_Test01::Evaluate_AnyThread(FPoseContext& Output)
{
	DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(Evaluate_AnyThread)

	float InternalBlendAlpha = FMath::`Clamp<float>`(Alpha, 0.f, 1.f);

	if (FAnimWeight::IsRelevant(InternalBlendAlpha))
	{
		FPoseContext Pose1(Output);//创建上下文
		FPoseContext Pose2(Output);

		Pose.Evaluate(Pose1);//将当前的Pose数据写入到上下文对象中
		OtherPose.Evaluate(Pose2);

		FAnimationPoseData BlendedAnimationPoseData(Output);
		const FAnimationPoseData AnimationPoseOneData(Pose1);
		const FAnimationPoseData AnimationPoseTwoData(Pose2);
		FAnimationRuntime::BlendTwoPosesTogether(AnimationPoseOneData, AnimationPoseTwoData, (1.0f - InternalBlendAlpha), BlendedAnimationPoseData);

	}
	else
	{
		Pose.Evaluate(Output);
	}
}

这里需要特别说明一下, FAnimationRuntime类里面基本包含动画蓝图里面绝大多数的动画计算函数库,比如Blend, Additive等, 如下图

image-20210803163704137

所以我们就直接调用里面的方法FAnimationRuntime::BlendTwoPosesTogether即可


录制_2021_08_03_16_41_48_971

再来一个

再扩展一个稍微复杂一点的, 一个基础Pose, 然后再提供2个Pose, 我们把后两个Pose的插值叠加到基础Pose上.

主要代码

void FAnimNode_Test02::Update_AnyThread(const FAnimationUpdateContext& Context)
{
	DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(Update_AnyThread);
	GetEvaluateGraphExposedInputs().Execute(Context);


	float InternalBlendAlpha = FMath::`Clamp<float>`(AdditiveAlpha, 0.f, 1.f);
	float SourceAlpha = FMath::`Clamp<float>`(AlphaSource, 0.f, 1.f);
	float TargetAlpha = FMath::`Clamp<float>`(AlphaTarget, 0.f, 1.f);

	if (FAnimWeight::IsRelevant(InternalBlendAlpha) && 
		FAnimWeight::IsRelevant(SourceAlpha) &&
		FAnimWeight::IsRelevant(TargetAlpha))
	{
		Pose.Update(Context.FractionalWeight(1.0f - InternalBlendAlpha));
		SubSource.Update(Context.FractionalWeight(SourceAlpha));
		SubTarget.Update(Context.FractionalWeight(TargetAlpha));
	}
	else
	{
		Pose.Update(Context);
	}
}

void FAnimNode_Test02::Evaluate_AnyThread(FPoseContext& Output)
{
	DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(Evaluate_AnyThread)

	float InternalBlendAlpha = FMath::`Clamp<float>`(AdditiveAlpha, 0.f, 1.f);
	float SourceAlpha = FMath::`Clamp<float>`(AlphaSource, 0.f, 1.f);
	float TargetAlpha = FMath::`Clamp<float>`(AlphaTarget, 0.f, 1.f);

	if (FAnimWeight::IsRelevant(InternalBlendAlpha) &&
		FAnimWeight::IsRelevant(SourceAlpha) &&
		FAnimWeight::IsRelevant(TargetAlpha))
	{
		FPoseContext PoseS(Output);
		FPoseContext PoseT(Output);


		Pose.Evaluate(Output);
		SubSource.Evaluate(PoseS);
		SubTarget.Evaluate(PoseT);

		FAnimationRuntime::ConvertPoseToAdditive(PoseT.Pose, PoseS.Pose);
		PoseT.Curve.ConvertToAdditive(PoseS.Curve);
		FCustomAttributesRuntime::SubtractAttributes(PoseS.CustomAttributes, PoseT.CustomAttributes);


		FAnimationPoseData OutAnimationPoseData(Output);
		const FAnimationPoseData AdditiveAnimationPoseData(PoseT);

		FAnimationRuntime::AccumulateAdditivePose(OutAnimationPoseData, AdditiveAnimationPoseData, AdditiveAlpha, AAT_LocalSpaceBase);
		Output.Pose.NormalizeRotations();
	}
	else
	{
		Pose.Evaluate(Output);
	}
}

先看效果

录制_2021_08_03_16_45_57_443

这个节点实现了, 先计算 Walk 减去 Idle, 然后把这个插值叠加到 JumpLoop动画上

ps: 实际上上面的内容就是把2个节点合并成了一个节点, 即ApplyAdditive + MakeDynamicAdditive