2015.10.12

创建自定义动画节点

作者 Lina Halper

动画蓝图提供了运行特定操作的节点,比如基于alpha值混合多个节点或者播放一个动画。 这里,您可以找到关于基本动画蓝图节点的更多信息。

这些节点提供了您将需要的标准功能,但是通常您应用动画时,一般需要创建自定义节点。这非常简单,但是却需要您了解基础系统的设计原理。该链接提供了关于该系统的深入知识,但是这里我想着重强调一下基本系统的内容,因为我发现有些人经常会遇到问题。

正如上面链接中所述的,系统需要两个类:一个是您在编辑器中看到的图表节点,一个是真正在运行时工作的行为节点。我们出于优化目的将其分离开来。节点构建的性能消耗非常大,使用400个节点每30秒让20个角色死亡或生成将会给内存及CPU造成巨大负担。如果您使用动画蓝图生成了一个角色,那么该角色将不会具有任何图表节点,仅具有行为节点。

让我们比较一下动画图表节点和动画行为节点的代码:

动画图表节点

class UAnimGraphNode_SequencePlayer : public UAnimGraphNode_Base

动画行为节点

struct ENGINE_API FAnimNode_SequencePlayer : public FAnimNode_Base

您将注意到这两个节点的基类是不同的:一个基类是UObject,另一个的基类是UStruct。在该博客中,我们将前一个作为动画图表节点,后一个作为动画行为节点。所有的图表节点包含了类似这样的对应行为节点:

class UAnimGraphNode_SequencePlayer : public UAnimGraphNode_Base
{
    GENERATED_UCLASS_BODY()

    UPROPERTY(EditAnywhere, Category=Settings)
    FAnimNode_SequencePlayer Node;

}

该动画图表节点知道另一个节点的存在,但反之则不能,这一个非常重要的区别。所有动画图表节点都是出于这个原因存在于编辑器中的,因为它不会随同游戏加载,仅存在于编辑器中。另一方面,行为节点存在于运行时代码中,而这正是真正发生混合的地方。请确保您的类指向正确的模块。

这对骨架控制节点来说也是一样的。

动画图表节点

class UAnimGraphNode_ModifyBone : public UAnimGraphNode_SkeletalControlBase

动画行为节点

struct ENGINE_API FAnimNode_ModifyBone : public FAnimNode_SkeletalControlBase

您将注意到骨架控制节点也具有不同的基类。

该系统如此设计,动画图表节点负责任何编辑器工作,比如显示节点名称、显示工具提示信息或创建自定义引脚。动画行为节点负责实际工作,比如混合、计算目标位置,及输出正确姿势。所以动画图表节点在编辑器中是重要的,而动画行为节点则在运行时是重要的。

再次说明,请参照该链接来查看变量的元数据是如何变为节点的输入或输出的。

比如,FPoseLink是传入骨骼变换数组的姿势连接,如果您像下面这样声明它,那么它将会像图片中那样显示出来。

UPROPERTY(Category=Links)
FPoseLink BasePose;

知道FPoseLink如何工作非常重要,因为任何时候当您调用任何动画函数时,您也必须调用该Pose函数。比如在您的Update函数中您应该调用BasePose->Update。同样,如果您有BasePose作为成员变量,您也应该在CacheBones函数中调用BasePose->CacheBones。

现在,我想谈下对于每种节点类型您应该关注的函数。我将不会集中介绍动画蓝图图表节点,因为它同任何其他蓝图节点的工作方式非常类似,但是我想集中介绍下这个真正工作的节点。

让我们看下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){}

};

它不是这么简单,但我正在进行简化以仅集中介绍您应该关心的主要事情。

有三个决定了您的节点如何表现的主要函数。它们是Initialize、Update和Evaluate,这里是对它们应用的简单描述:

  • Initialize - 任何时候当您需要进行初始化或重新初始化时调用该函数(当修改实例的网格物体时)。
  • Update - 调用该函数来更新当前状态(比如更新播放时间或混合权重)。该函数取入一个FAnimationUpdateContext,它知道更新的DeltaTime和当前的节点混合权重。
  • Evaluate - 调用该函数来生成一个‘姿势’(一系列的骨骼变换)。
    • 示例:
      • 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 判断是否设置了序列及它是否同当前骨架兼容。如果是,那么它将该骨骼变换填充到Output.Pose中。如果不是,则将Output设置为参考姿势。

 

在这些基本函数的基础上,您需要提供两个函数的实现,以确保您的节点可以正常同图表的其他部分协同工作:

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

CacheBones用于刷新该节点所引用的骨骼索引,GatherDebugData用于使用"ShowDebug Animation"数据进行调试。为了保持到子项的连接,使用这些是很重要的。正如我之前所提到的,FPoseLink 应该调用它下面的所有节点,以确保您的节点连接的任何姿势连接都会被调用。

请参照该示例:

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

您需要实现它,以便这些事件不会被您的节点阻止。

同时注意我们有FAnimationRuntime,当进行动画时它提供了大量功能。

这里是一个关于骨架控制节点的示例:

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
}

这同动画节点类似但又有所不同,因为骨架控制节点在组件空间上工作。再次说明,查看FAnimationRuntime将向您展示一种在本地空间和组件空间之间转换的好方法。

您一般都使用EvaluateComponentSpace。这里是一个涉及到CopyBone节点的简单应用示例:

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));
}

其目的是在OutBoneTransforms中返回您想要的数据。您可以返回您需要的任何数量的骨骼变换,但请注意层次结构。保持父项到子项的顺序将能确保总是安全的。

我希望这向您进行了进一步介绍,以使得您开始创建自己的自定义节点变得更加容易。

最近文章

超越想象:视觉设计的驾乘体验

什么是体验设计?了解各大企业如何使用强大的虚幻引擎和实时技术以全新方式实现作品的驾乘体验。

虚幻引擎设计制造实时可视化网络研讨会

准备好迎接更快速更高效的实时产品设计流程了吗?本次网络研讨会绝对不容错过!

2018:游戏行业迎来虚幻大年

随着各大游戏媒体纷纷选定自己最期待的2018年游戏,我们发现这些榜单上随处可见备受期待的虚幻引擎作品!