【ゲーム制作】モーションブレンドとは?|アニメの切り替わりの「がくっ」をイージングで滑らかにする方法

モーションブレンドのサムネイル ゲーム制作

今回は、3Dゲームを作っていると必ずぶつかる「モーションの切り替わりでキャラが『がくっ』となる問題」と、その解決策であるモーションブレンドについて解説していきます。

「歩きから走りに切り替えた瞬間、キャラがカクッと不自然に飛ぶ」
「攻撃モーションの出だしがワープしたみたいに見える」
「モーションブレンドって聞くけど、結局どう実装するの?」

こんな悩みはありませんか?

あの「がくっ」の正体は、前のモーションの終わりのポーズと、次のモーションの始まりのポーズが一致していないのに、1フレームでいきなり切り替わっているのが原因です。姿勢が瞬間移動するので、目には「カクッ」と映ります。

この記事では、なぜ「がくっ」となるのか・クロスフェードで混ぜる仕組み・イージングで自然にするコツ・実装コード・ハマりどころまで、C++の擬似コードつきで解説します。読み終えると、モーションの繋ぎ目をプロっぽく滑らかにできるようになりますので、ぜひ最後まで読んでください。

なぜ「がくっ」となるのか|ポーズの不連続

アニメーションは、各ボーンの「位置」と「回転」を毎フレーム更新して姿勢を作っています。モーションを切り替えるということは、参照するアニメクリップを別のものに差し替えるということです。

ここで問題になるのが、クリップAの最後のポーズクリップBの最初のポーズがふつう一致していないこと。たとえば歩きの「左足が前」の瞬間から、走りの「両足が揃った」瞬間へいきなり飛ぶと、ボーンが一瞬でワープします。これが「がくっ」の正体です。

解決策はシンプルで、切り替えを一瞬でやらず、短い時間をかけて2つのポーズを混ぜることです。これをクロスフェード(ブレンド)と呼びます。

クロスフェードの仕組み|2つのポーズを重みで混ぜる

ブレンドの中身は「重み付き平均」です。t という重みを 0 → 1 へ少しずつ動かし、各ボーンごとにクリップAとクリップBのポーズを混ぜます。t=0ならA100%、t=1ならB100%、その間はA:Bを比率で混ぜる、というイメージです。

  • 位置(移動量)Lerp(線形補間)で混ぜる
  • 回転必ず Slerp(球面線形補間)で混ぜる
// ボーン1本ぶんのブレンド(t: 0→1)
BonePose Blend(const BonePose& a, const BonePose& b, float t)
{
    BonePose out;
    out.position = XMVectorLerp(a.position, b.position, t);   // 位置はLerp
    out.rotation = XMQuaternionSlerp(a.rotation, b.rotation, t); // 回転はSlerp必須
    out.scale    = XMVectorLerp(a.scale, b.scale, t);
    return out;
}

回転で Slerp を使うのは重要ポイントです。回転はクォータニオン(4つの数)で表され、これを普通の Lerp で混ぜると回転速度がムラになったり、途中でスケールが狂ったりします。角度に沿ってなめらかに回すのが Slerp の役割です。値がずれてきたときに備えて正規化しておくとより安全です。

ブレンドの進め方|deltaTimeで時間管理する

重み t は「ブレンドにかける時間」で割って進めます。0.2秒でブレンドしたいなら、毎フレーム deltaTime / 0.2 ずつ t を増やす、という具合です。ここでフレームレートに依存しないよう、必ずdeltaTimeで進めるのがコツです。

// ブレンド中の毎フレーム更新
blendTime += deltaTime;
float t = blendTime / blendDuration;   // blendDuration = 0.2f など
if (t >= 1.0f) { t = 1.0f; blending = false; } // 完了

// t をイージングで加工(次の章で解説)
float e = EaseInOut(t);

// 全ボーンをブレンド
for (int i = 0; i < boneCount; ++i)
    finalPose[i] = Blend(poseA[i], poseB[i], e);

フレームの刻みを安定させたい場合は、FPS固定・固定タイムステップと組み合わせるとブレンドの見え方も安定します。

イージングで自然にする|直線的な混ぜ方を卒業する

t をそのまま(線形=Linear)で使ってもブレンドはできますが、始まりと終わりが機械的に見えます。人の動きは「すっと始まってふわっと終わる」ので、tイージング関数で加工して緩急をつけると、一気に自然になります。

一番手軽で万能なのが smoothstep(イーズイン・アウト)です。開始と終了がなめらかに減速します。三角関数の sin・cos を使った書き方もよく使われます。

// 0→1 の t を、緩急のついた t' に変換する

// ① smoothstep:最も手軽で万能。始点・終点がなめらか
float EaseInOut(float t) {
    return t * t * (3.0f - 2.0f * t);
}

// ② cosを使ったイーズイン・アウト(同じく滑らか)
float EaseInOutCos(float t) {
    return 0.5f - 0.5f * cosf(t * 3.14159265f);
}

// ③ イーズアウト:出だしは速く、終わりでゆっくり止まる
float EaseOut(float t) {
    return 1.0f - (1.0f - t) * (1.0f - t);
}

「歩き↔走り」のような移動系は smoothstep、「待機→攻撃」のように出だしをキビキビさせたいものは イーズアウト、と使い分けると気持ちよくなります。ブレンド時間は0.1〜0.3秒が目安です。

【重要】私がモーションの繋ぎでハマった体験談

自主制作の3Dアクションで、走りから攻撃に移る瞬間だけキャラの上半身がビクッと一瞬あらぬ方向を向くバグに悩まされました。ブレンドは入れているのに直りません。

原因は回転をSlerpではなくLerpで混ぜていたことでした。クォータニオンをそのままLerpすると、回転の取り方によっては遠回りの経路で補間され、一瞬あらぬ向きを通ってしまうんです。XMQuaternionSlerpに変え、さらに内積が負なら片方の符号を反転して「近い方向に回す」処理を足したら、ピタッと直りました。

「回転のブレンドはLerpではなくSlerp、しかも最短経路で」と体に叩き込まれた出来事でした。

モーションブレンドのよくある失敗例と対処法

①回転が一瞬あらぬ方向へ回る

体験談のとおり、回転をLerpで混ぜているか、最短経路の処理が抜けているのが原因です。Slerpを使い、2つのクォータニオンの内積が負なら片方を反転してから混ぜましょう。

②ブレンドがフレームレートで速さが変わる

t を毎フレーム固定値(例:t += 0.05f)で足していると、60FPSと30FPSでブレンド速度が変わります。必ずdeltaTimeで時間管理し、t = blendTime / blendDurationで進めましょう。

③ブレンド中にもう一度切り替えると破綻する

ブレンドが終わる前に次のモーションへ切り替えると、tがリセットされてまた「がくっ」となります。今表示している合成後のポーズを新しいAとして扱う(スナップショットを取る)と、途中からでも滑らかに繋がります。

注意点

  • 位置はLerp、回転はSlerp(最短経路で)
  • tdeltaTimeで進める(フレームレート非依存)
  • イージングは0.1〜0.3秒で、動きの性質に合わせて選ぶ

まとめ

  • 「がくっ」の正体はポーズの不連続(瞬間移動)
  • 短い時間をかけて2つのポーズをクロスフェードで混ぜると解決
  • 回転はSlerp、時間はdeltaTimeで管理
  • イージングで緩急をつけると一気に自然になる

モーションの繋ぎ目が滑らかになると、それだけでゲームの見栄えがぐっとプロっぽくなります。まずはブレンド時間0.2秒+smoothstepから試して、動きに合わせて微調整してみてください。

関連記事