2015年10月12日

カスタム アニメーション ノードの作成

作成 Lina Halper

Anim ブループリントには、アルファ値に基づいた複数ノードのブレンドやアニメーション再生などの特定のアクションを実行するためのアニメーション ノードがあります。ここをクリックすると、Anim ブループリントの基本について詳しい情報をご覧いただくことができます。

こうしたノードは必要な標準機能に対応していますが、アニメーションの作業をすると多くの場合、カスタム ノードを作成したくなるでしょう。カスタム ノードは非常に簡単に作成できますが、その基本システムがどのように設計されているかを理解する必要があります。このリンクをクリック すると、このシステムの詳細情報があります。しかし、ここでは基本システムに重点をおいて説明します。皆さんが、基本システムに関して悪戦苦闘しているのを知っているからです。

上記のリンクで説明しているように、このシステムでは 2 つのクラスを必要としています。ひとつはエディタにあるグラフ ノードです。もうひとつは実際のランタイムの動作を行う動作 (behavior) ノードです。最適化の目的上、このように分類しました。ノード構築は非常に負荷が高いものです。30 秒毎に 20 のキャラクターが消滅/スポーンする 400 ノード以上ある状態では、メモリや CPU に大きな負荷がかかります。Anim ブループリントを使用してキャラクターをスポーンすれば、キャラクターはグラフ ノードを持たず、動作ノードだけを持ちます。

Anim Graph ノードと Anim Behavior ノードのコードを比べてみましょう。

Anim Graph ノード

class UAnimGraphNode_SequencePlayer : public UAnimGraphNode_Base

Anim Behavior ノード 

struct ENGINE_API FAnimNode_SequencePlayer : public FAnimNode_Base

それぞれ異なる基底クラスからのものであることがわかります。ひとつは UObject で、もうひとつは UStruct です。このブログではひとつめを Anim Graph ノード、もうひとつを Anim Behavior ノードとします。すべてのグラフ ノードには、以下のように対応する動作ノードが含まれます。 

class UAnimGraphNode_SequencePlayer : public UAnimGraphNode_Base
{
    GENERATED_UCLASS_BODY()

    UPROPERTY(EditAnywhere, Category=Settings)
    FAnimNode_SequencePlayer Node;

}

Anim Graph ノードはもうひとつのノードを認識しますが、逆は該当しません。これは重要な違いです。すべての Anim Graph ノードは、こうした理由からエディタ モジュールに存在します。エディタに存在するだけでゲームとともに読み込まれないからです。一方、動作ノードは実際のブレンディングが起こるランタイム コードに存在します。クラスが適切なモジュールをポイントするようにしてください。 

Skeletal Control ノードでも同じことがあてはまります。

Anim Graph ノード

class UAnimGraphNode_ModifyBone : public UAnimGraphNode_SkeletalControlBase

Anim Behavior ノード

struct ENGINE_API FAnimNode_ModifyBone : public FAnimNode_SkeletalControlBase

Skeletal Control ノードにも異なる基底クラスがあることがわかります。 

このシステムは、Anim Graph ノードがノード名の表示、ツールチップの表示、カスタム ピンの作成などのエディタの作業を行うように設計されています。Anim Behavior ノードは、ブレンド、ターゲット位置の計算、正しいポーズの出力などの実際の動作を行います。そのため、Anim Graph ノードはエディタで非常に重要なものです。その一方で Anim Behavior ノードはランタイムに重要になります。

繰り返しになりますが、このリンク で、 変数のメタ データがどのようにノードの入力や出力に変わるかをご覧ください。

例えば、FPoseLink はボーン変形の配列を渡すポーズのリンクであり、以下のように宣言すると図のように表示されます。

UPROPERTY(Category=Links)
FPoseLink BasePose;

FPoseLink がどのように機能するかを理解することは重要です。アニメーション関数を呼び出す度に、Pose 関数も呼び出さなければならないからです。例えば、Update 関数で BasePose->Update を呼び出します。同様に、BasePose をメンバー変数として持っている場合は、CacheBones 関数でも BasePose->CacheBones を呼び出します。

ここで、各ノード タイプで重要となる関数について説明します。Anim Graph ノードには焦点をあてません。その動作は他のブループリントのノードと非常に良く似ているからです。ただし、実際に機能するノードについては重点をおいて説明します。 

まず、以下の FAnimNode_Base を見てみましょう。

struct ENGINE_API FAnimNode_Base
{
    // 実装するインターフェース
    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){}

};

こんなに単純ではありませんが、注意すべき主な点にのみ焦点をあてて説明します。

ノードがどのように動作するかを決める 3 つの主要な関数があります。すなわち、Initialize、Update、および Evaluate の 3 つです。以下にその用途を簡単に示します。

  • Initialize - 初期化/再初期化が必要な場合に常に呼び出されます (例、インスタンスのメッシュを変更する)。
  • Update - 現在の状態を更新するために呼び出されます (例、プレイ時間を進める、ブレンドのウェイトを更新する)。この関数は更新の DeltaTime と現在のノードのブレンド ウェイトを認識している FAnimationUpdateContext を取ります。
  • Evaluate - ‘pose’ (ボーン変形のリスト) を生成するために呼び出されます。
    • 以下はその例です。
      • 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 は、Sequence が設定され、現在のスケルトンと互換性があるかをチェックします。その場合、ボーン変形を Output.Pose に適用します。そうでなければ、Output は参照ポーズに設定されます。 ​

 

こうした基本的な関数に加えて、ノードがグラフの他の部分と確実にうまく機能するように、以下のように 2 つの関数を実装する必要があります。 

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 もあることを覚えておいてください。 

以下は Skeletal Control ノードの例です。

struct ENGINE_API FAnimNode_SkeletalControlBase : public FAnimNode_Base
{
    // FAnimNode_Base インターフェース
    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;
    // FAnimNode_Base インターフェースの終わり
}

これは Anim Node と類似していますが、Skeletal Control ノードがコンポーネント空間で動作するため異なります。FAnimationRuntime を調べると、ローカル空間とコンポーネント空間の間を変換する良い方法がわかります。 

一般的に EvaluateComponentSpace を使用します。以下は CopyBone ノードを伴うシンプルな使用例です。

void FAnimNode_CopyBone::EvaluateBoneTransforms(USkeletalMeshComponent* SkelComp, FCSPose<FCompactPose>& MeshBases, TArray<FBoneTransform>& OutBoneTransforms)
{
    check(OutBoneTransforms.Num() == 0);
    // 何もしなければ通過
    if( !bCopyTranslation && !bCopyRotation && !bCopyScale )
    {
        return;
    }
    // ソースと現在のボーンのコンポーネント空間の変形を取得
    const FBoneContainer& BoneContainer = MeshBases.GetPose().GetBoneContainer();
    FCompactPoseBoneIndex TargetBoneIndex = TargetBone.GetCompactPoseIndex(BoneContainer);
    const FTransform& SourceBoneTM = MeshBases.GetComponentSpaceTransform(SourceBone.GetCompactPoseIndex(BoneContainer));
    FTransform CurrentBoneTM = MeshBases.GetComponentSpaceTransform(TargetBoneIndex);
    // 個々のコンポーネントをコピー
    if (bCopyTranslation)
    {
        CurrentBoneTM.SetTranslation( SourceBoneTM.GetTranslation() );
    }
    if (bCopyRotation)
    {
        CurrentBoneTM.SetRotation( SourceBoneTM.GetRotation() );
    }
    if (bCopyScale)
    {
        CurrentBoneTM.SetScale3D( SourceBoneTM.GetScale3D() );
    }
    // 現在のボーンに対する新しい変形を出力
    OutBoneTransforms.Add(FBoneTransform(TargetBoneIndex, CurrentBoneTM));
}

ここでのゴールは、必要なデータと共に OutBoneTransforms を戻すことです。必要な数だけボーン変形を戻すことができますが、階層に配慮してください。常に、親から子への順序を守ると安全です。 

説明はここまでです。皆さんのカスタム ノード作成に少しでもお役に立てればと思います。