前言
首先要准备两个类, 一个继承自UAnimGraphNode_Base
为蓝图节点类, 还有一个是数据类FAnimNode_Base
UAnimGraphNode_Base
这个类定义了节点的显示方式,分类以及引脚连接方式等操作, 然后最主要的是要在头文件申明一个数据类, 如下
1 2 3 4 5 6 7 8 9
| UCLASS() class UTILITY_API UMyAnimGraphNode_T01 : public UAnimGraphNode_Base { GENERATED_BODY() UPROPERTY(EditAnywhere, Category = Settings) FAnimNode_Test01 Node; };
|
不需要对这个数据类做任何操作
FAnimNode_Base
我们在动画蓝图里点击节点以后显示的所有变量都是申明在这个类里面
比如我们申明这么一个数据类, 打算做一个简单的Blend操作
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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; };
|
然后就显示如下
我们重写下面几个虚函数
1 2 3 4 5 6
| 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;
|
其中Initialize_AnyThread
和CacheBones_AnyThread
会在初始化(编译)的时候先后调用, 每次调用2次
这俩函数就模仿其他基础的节点做一个基础的操作即可
CacheBones_AnyThread用于刷新该节点所引用的骨骼索引
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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函数, 也是按照先后顺序调用, 看下图
起点分别是上图中红色框部分, TickPose
最后执行的是Update_AnyThread
调用该函数来更新当前状态(比如更新播放时间或混合权重)。该函数取入一个FAnimationUpdateContext,它知道更新的DeltaTime和当前的节点混合权重。
比如我们的节点有Alpha
的存在, 就可以在Update
中通过这个Alpha
做一些预处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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
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
| 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); 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等, 如下图
所以我们就直接调用里面的方法FAnimationRuntime::BlendTwoPosesTogether
即可
再来一个
再扩展一个稍微复杂一点的, 一个基础Pose, 然后再提供2个Pose, 我们把后两个Pose的插值叠加到基础Pose上.
主要代码
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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| 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); } }
|
先看效果
这个节点实现了, 先计算 Walk 减去 Idle, 然后把这个插值叠加到 JumpLoop动画上
ps: 实际上上面的内容就是把2个节点合并成了一个节点, 即ApplyAdditive + MakeDynamicAdditive