ほげたつブログ

プログラムとアニメーションをかじって生きてる

慣性ベースドなアニメーションブレンド(実装編)

慣性ベースドなアニメーションブレンドの実装編です。前回の解説編は以下のリンクから。一番下にGitHubへのリンクもあります。


hogetatu.hatenablog.com


アニメーションノードの作成

今回は AnimGraph で使えるノードとして実装します。AnimNode_InertialBlend は2つのポーズを入力し、フラグによって切り替える仕様です。

f:id:hogetatu:20180610190725p:plain


アニメーションノード自作の具体的なやり方は、以下のページが参考になります。

qiita.com


各ボーンの Transform

アニメーションノードがポーズを評価する時は Evaluate_AnyThread が呼ばれます。こちらの関数は FPoseContext が引数となっており、ここから現在ポーズにおける各ボーンの Transform 情報が取得できます。

void FAnimNode_InertialBlend::Evaluate_AnyThread(FPoseContext& Output)
{
    if (bAIsRelevant)
    {
        // Aポーズを評価
        A.Evaluate(Output);
    }
    else
    {
        // Bポーズを評価
        B.Evaluate(Output);
    }

    // 評価したポーズの各ボーンのTransformリスト
    const auto& CurrentTransforms = Output.Pose.GetBones();

    ~
}


また、各ボーンの Transform は前回、前々回フレームのものをキャッシュとして残しておきます。

struct FPoseCache
{
    bool bEnabled;
    TArray<FTransform> Transforms;
    float DeltaSeconds;
};

FPoseCache Caches[2];
FPoseCache* PrimaryCache;
FPoseCache* SecondaryCache;

~

/**
 * ポーズキャッシュ更新
 * @param PoseContext 現在ポーズ情報
 * @param DeltaSeconds 経過時間
 */
void FAnimNode_InertialBlend::UpdatePoseCache(FPoseContext& PoseContext, float DeltaSeconds)
{
    FPoseCache* Temp = PrimaryCache;
    PrimaryCache = SecondaryCache;
    SecondaryCache = Temp;

    PrimaryCache->bEnabled = true;
    PoseContext.Pose.CopyBonesTo(PrimaryCache->Transforms);
    PrimaryCache->DeltaSeconds = DeltaSeconds;
}

 

遷移情報を初期化

アニメーション遷移を始める時に遷移情報の初期化を行います。解説編で導いた式を元に関数化します。


 a_0 = \frac{-8v_0t_1 - 20x_0}{t_1^2}

 t_1 = \min(t_1, - \frac{5x_0}{v_0})

 x(t) = At^5 + Bt^4 + Ct^3 + \frac{a_0}{2}t^2 + v_0t + x_0

 A = -\frac{a_0t_1^2 + 6v_0t_1 + 12x_0}{2t_1^5}

 B = \frac{3a_0t_1^2 + 16v_0t_1 + 30x_0}{2t_1^4}

 C = -\frac{3a_0t_1^2 + 12v_0t_1 + 20x_0}{2t_1^3}


struct FTransitionBase
{
    float X0;
    float V0;
    float T1;
    float A0;
    float A;
    float B;
    float C;
};

~

/**
 * 遷移情報を初期化
 * @param Transition 遷移情報
 */
void FAnimNode_InertialBlend::InitTransition(FTransitionBase& Transition)
{
    if (FMath::IsNearlyZero(Transition.X0))
    {
        Transition.T1 = 0.f;
        Transition.A0 = 0.f;
        Transition.A = 0.f;
        Transition.B = 0.f;
        Transition.C = 0.f;
    }
    else
    {
        if (Transition.V0 != 0.f && (Transition.X0 / Transition.V0) < 0.f)
        {
            Transition.T1 = FMath::Min(BlendTime, -5.f * Transition.X0 / Transition.V0);
        }
        else
        {
            Transition.T1 = BlendTime;
        }

        Transition.A0 = ((-8.f * Transition.V0 * Transition.T1) + (-20.f * Transition.X0)) / FMath::Pow(Transition.T1, 2.f);

        Transition.A  = -((1.f * Transition.A0 * Transition.T1 * Transition.T1) + ( 6.f * Transition.V0 * Transition.T1) + (12.f * Transition.X0)) / (2.f * FMath::Pow(Transition.T1, 5.f));
        Transition.B  =  ((3.f * Transition.A0 * Transition.T1 * Transition.T1) + (16.f * Transition.V0 * Transition.T1) + (30.f * Transition.X0)) / (2.f * FMath::Pow(Transition.T1, 4.f));
        Transition.C  = -((3.f * Transition.A0 * Transition.T1 * Transition.T1) + (12.f * Transition.V0 * Transition.T1) + (20.f * Transition.X0)) / (2.f * FMath::Pow(Transition.T1, 3.f));
    }
}

 

ベクトルとクォータニオン

上記初期化はスカラー値に対して行っていますが、Transform を補間する場合はベクトルとクォータニオンに対して行う必要があります。

ベクトル

ベクトル補間は各アトリビュートに対してそれぞれ行うと良い結果が得られません。そのため、ベクトルを方向と長さに分けて考えた上で、[現在フレーム -1 ポーズ] の方向を基準とした長さについて補間を行います。

struct FVectorTransition : public FTransitionBase
{
    FVector NormalizedVecX0;
};

~

/**
 * 遷移情報を初期化(ベクトル)
 * @param Transition 遷移情報
 * @param RequestedValue 遷移先ポーズ値
 * @param PrimaryCacheValue 現在フレーム -1 ポーズ値
 * @param SecondaryCacheValue 現在フレーム -2 ポーズ値
 * @param PrimaryToSecondaryDeltaSeconds 現在フレーム -2 から 現在フレーム -1 の差分時間
 */
void FAnimNode_InertialBlend::InitTransition(
    FVectorTransition& Transition,
    const FVector& RequestedValue,
    const FVector& PrimaryCacheValue,
    const FVector& SecondaryCacheValue,
    float PrimaryToSecondaryDeltaSeconds)
{
    FVector VecX0 = PrimaryCacheValue - RequestedValue;
    Transition.X0 = VecX0.Size();

    if (Transition.X0 >= SMALL_NUMBER)
    {
        Transition.NormalizedVecX0 = VecX0 / Transition.X0;

        FVector VecXn1 = SecondaryCacheValue - RequestedValue;
        float Xn1 = FVector::DotProduct(VecXn1, Transition.NormalizedVecX0);

        Transition.V0 = (Transition.X0 - Xn1) / PrimaryToSecondaryDeltaSeconds;
    }
    else
    {
        Transition.NormalizedVecX0 = FVector::ZeroVector;
        Transition.V0 = 0.f;
    }

    InitTransition(Transition);
}

 

クォータニオン

クォータニオンもベクトルと同様に、軸と回転角に分けて考えた上で、[現在フレーム -1 ポーズ] の軸を基準とした回転について補間を行います。

struct FQuatTransition : public FTransitionBase
{
    FVector AxisX0;
};

~

/**
 * 遷移情報を初期化(クォータニオン)
 * @param Transition 遷移情報
 * @param RequestedValue 遷移先ポーズ値
 * @param PrimaryCacheValue 現在フレーム -1 ポーズ値
 * @param SecondaryCacheValue 現在フレーム -2 ポーズ値
 * @param PrimaryToSecondaryDeltaSeconds 現在フレーム -2 から 現在フレーム -1 の差分時間
 */
void FAnimNode_InertialBlend::InitTransition(
    FQuatTransition& Transition,
    const FQuat& RequestedValue,
    const FQuat& PrimaryCacheValue,
    const FQuat& SecondaryCacheValue,
    float PrimaryToSecondaryDeltaSeconds)
{
    FQuat InvRequestedValue = RequestedValue.Inverse();

    FQuat Q0 = PrimaryCacheValue * InvRequestedValue;
    Q0.ToAxisAndAngle(Transition.AxisX0, Transition.X0);
    NormalizeAngle(Transition.X0);

    FQuat Qn1 = SecondaryCacheValue * InvRequestedValue;
    float Xn1 = PI;

    if (!FMath::IsNearlyZero(Qn1.W))
    {
        FVector Qxyz = FVector(Qn1.X, Qn1.Y, Qn1.Z);
        Xn1 = 2.f * FMath::Atan(FVector::DotProduct(Qxyz, Transition.AxisX0) / Qn1.W);
        NormalizeAngle(Xn1);
    }

    float DeltaAngle = Transition.X0 - Xn1;
    NormalizeAngle(DeltaAngle);

    Transition.V0 = DeltaAngle / PrimaryToSecondaryDeltaSeconds;

    InitTransition(Transition);
}

 

差分の更新

初期化が完了したら、あとは時間 t を更新してその時の差分 Xt を現在ポーズに加算するだけです。Xt の式を元に関数化します。


 x(t) = At^5 + Bt^4 + Ct^3 + \frac{a_0}{2}t^2 + v_0t + x_0


/**
 * ブレンド値を取得(スカラー)
 * @param Transition 遷移情報
 * @param T 現在時間
 */
float FAnimNode_InertialBlend::CalcBlendValue(const FTransitionBase& Transition, float T) const
{
    float T_1 = FMath::Min(T, Transition.T1);
    float T_2 = T_1 * T_1;
    float T_3 = T_1 * T_2;
    float T_4 = T_1 * T_3;
    float T_5 = T_1 * T_4;

    return
        (Transition.A * T_5) +
        (Transition.B * T_4) +
        (Transition.C * T_3) +
        (0.5f * Transition.A0 * T_2) +
        (Transition.V0 * T_1) +
        (Transition.X0);
}

 

ベクトルのブレンドされた値

/**
 * ブレンドされた値を取得(ベクトル)
 * @param CurrentValue 現在値
 * @param Transition 遷移情報
 * @param T 現在時間
 */
FVector FAnimNode_InertialBlend::CalcBlendedValue(const FVector& CurrentValue, const FVectorTransition& Transition, float T) const
{
    float Xt = CalcBlendValue(Transition, T);
    return Xt * Transition.NormalizedVecX0 + CurrentValue;
}

 

クォータニオンブレンドされた値

/**
 * ブレンドされた値を取得(クォータニオン)
 * @param CurrentValue 現在値
 * @param Transition 遷移情報
 * @param T 現在時間
 */
FQuat FAnimNode_InertialBlend::CalcBlendedValue(const FQuat& CurrentValue, const FQuatTransition& Transition, float T) const
{
    float Xt = CalcBlendValue(Transition, T);
    return FQuat(Transition.AxisX0, Xt) * CurrentValue;
}

 

Evaluate 関数(評価関数)

上記の関数を用いることで、慣性ベースドなアニメーションブレンドが実現できます。実際の評価関数は以下の様な実装になります。

struct FTransformTransition
{
    FVectorTransition Translation;
    FQuatTransition Rotation;
    FVectorTransition Scale;
};

struct FPoseTransition
{
    TArray<FTransformTransition> Transforms;
    float T;
};

TSharedPtr<FPoseTransition> PoseTransition;

~

void FAnimNode_InertialBlend::Evaluate_AnyThread(FPoseContext& Output)
{
    if (bAIsRelevant)
    {
        A.Evaluate(Output);
    }
    else
    {
        B.Evaluate(Output);
    }

    const auto& CurrentTransforms = Output.Pose.GetBones();
    float DeltaSeconds = Output.AnimInstanceProxy->GetDeltaSeconds();

    if (bRequestedTransition && PrimaryCache->bEnabled && SecondaryCache->bEnabled)
    {
        bRequestedTransition = false;

        PoseTransition = MakeShareable(new FPoseTransition);
        PoseTransition->Transforms.SetNumUninitialized(CurrentTransforms.Num());
        PoseTransition->T = 0.f;

        int32 BoneIndex = 0;
        for (FTransformTransition& TransformTransision : PoseTransition->Transforms)
        {
            const FTransform& CurrentTransform = CurrentTransforms[BoneIndex];
            const FTransform& PrimaryCacheTransform = PrimaryCache->Transforms[BoneIndex];
            const FTransform& SecondaryCacheTransform = SecondaryCache->Transforms[BoneIndex];

            InitTransition(
                TransformTransision.Translation,
                CurrentTransform.GetTranslation(),
                PrimaryCacheTransform.GetTranslation(),
                SecondaryCacheTransform.GetTranslation(),
                PrimaryCache->DeltaSeconds
            );

            InitTransition(
                TransformTransision.Rotation,
                CurrentTransform.GetRotation(),
                PrimaryCacheTransform.GetRotation(),
                SecondaryCacheTransform.GetRotation(),
                PrimaryCache->DeltaSeconds
            );

            InitTransition(
                TransformTransision.Scale,
                CurrentTransform.GetScale3D(),
                PrimaryCacheTransform.GetScale3D(),
                SecondaryCacheTransform.GetScale3D(),
                PrimaryCache->DeltaSeconds
            );

            ++BoneIndex;
        }
    }

    if (PoseTransition.IsValid())
    {
        PoseTransition->T += DeltaSeconds;

        if (PoseTransition->T < BlendTime)
        {
            int32 BoneIndex = 0;
            for (FTransformTransition& TransformTransition : PoseTransition->Transforms)
            {
                const FTransform& CurrentTransform = CurrentTransforms[BoneIndex];

                FVector Translation = CalcBlendedValue(CurrentTransform.GetTranslation(), TransformTransition.Translation, PoseTransition->T);
                FQuat Rotation = CalcBlendedValue(CurrentTransform.GetRotation(), TransformTransition.Rotation, PoseTransition->T);
                FVector Scale = CalcBlendedValue(CurrentTransform.GetScale3D(), TransformTransition.Scale, PoseTransition->T);

                FCompactPoseBoneIndex CPBoneIndex(BoneIndex);
                Output.Pose[CPBoneIndex] = FTransform(Rotation, Translation, Scale);

                ++BoneIndex;
            }

            Output.Pose.NormalizeRotations();
        }
        else
        {
            PoseTransition.Reset();
        }
    }

    UpdatePoseCache(Output, DeltaSeconds);
}

 

サンプルプロジェクト

GitHubに上げてます。

github.com

※ Readmeにも書かれていますが、サンプルにはマーケットプレイスで無料配布されている「Animation Starter Pack」を利用しています。こちらは配布物には含めていないので、プロジェクトを起動する前にマーケットプレイスからダウンロードしてプロジェクトに含めて下さい。



長かった・・・!