0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

モーションを 4 方向ブレンドする

0
Posted at

ozz-animation で被弾モーションを 4 方向ブレンドする

はじめに

スケルタルアニメーションのランタイムに ozz-animation を使っています。

今回のテーマは 方向つきモーションのブレンドです。敵が攻撃を受けたとき、攻撃が来た方向に応じて「前によろける/後ろにのけぞる/左右に振られる」を切り替えたい。ただし前後左右の4本だけを離散的に切り替えると、斜めから殴られたときに不自然になります。

そこで、前後左右4本の被弾モーションを1つの方向ベクトルから求めた重みで合成し、斜め方向を隣接2方向の中間として表現します。Unity の 2D ブレンドスペース(Simple Directional)の4点版のようなものです。ozz 側は ozz::animation::BlendingJob がそのまま使えるので、実装は思ったより小さく収まりました。

方向ブレンド.gif
[動画] モーションブレンドを2D入力で制御している様子

全体の流れ

ozz のポーズ計算は「サンプル → ブレンド → モデル空間化」のジョブを毎フレーム流すだけです。方向ブレンドを入れても、この流れにレイヤーが増えるだけで構造は変わりません。

  1. 4 本のアニメを 同じ正規化時刻(ratio)でそれぞれ SamplingJob でサンプル
  2. 方向ベクトルから求めた重みを付けて BlendingJob で 1 つのローカルポーズに合成
  3. LocalToModelJob でモデル空間の行列列へ変換

方向 → 重みの計算

キャラのローカル空間(x=右+、z=前+)の方向ベクトルを、front / back / left / right の 4 重みに変換します。やることは正規化した成分をそのまま各軸の重みにするだけです。

// 方向 → 重み (front back left right の順)
static void WeightsFor(const Vector2& localDir, float outW[4]) {
    Vector2 d = localDir;
    const float len = d.Length();
    if (len < 1e-4f) {
        outW[0] = 1.0f;                  // ゼロベクトルは前扱い
        outW[1] = outW[2] = outW[3] = 0.0f;
        return;
    }
    d /= len;
    outW[0] = std::max(0.0f,  d.y);   // front
    outW[1] = std::max(0.0f, -d.y);   // back
    outW[2] = std::max(0.0f, -d.x);   // left
    outW[3] = std::max(0.0f,  d.x);   // right
}

たとえば右斜め前(d = (0.707, 0.707))なら front = 0.707、right = 0.707、残りは 0。相反する方向(front と back)が同時に正になることはないので、常に「隣接 2 方向の合成」になります。重みの合計は 1 になりませんが、正規化はブレンド側でまとめて行うのでここでは気にしません。

BlendingJob で合成する

アニメーションコントローラ側には「複数アニメを重み付きで同時再生するブレンドセット」を追加しました。毎フレームの更新はこうなります。

// 全レイヤーを同じ正規化時刻でサンプルし重み合成して current ポーズにする
float weightSum = 0.0f;
for (const auto& layer : m_blendSet) weightSum += layer->weight;

std::vector<ozz::animation::BlendingJob::Layer> layers;
for (auto& layer : m_blendSet) {
    SampleAnim(layer->anim, currentRatio, layer->locals, layer->context);

    ozz::animation::BlendingJob::Layer l;
    // 合計 0 なら等分にフォールバック (rest pose 落ちを防ぐ)
    l.weight = (weightSum > 0.0f)
        ? layer->weight / weightSum
        : 1.0f / static_cast<float>(m_blendSet.size());
    l.transform = ozz::make_span(layer->locals);
    layers.push_back(l);
}

ozz::animation::BlendingJob blend;
blend.layers    = ozz::make_span(layers);
blend.rest_pose = m_skeleton->joint_rest_poses();
blend.output    = ozz::make_span(m_localTransforms);
blend.threshold = 0.1f;
blend.Run();

ポイントは 3 つです。

  • 各レイヤーに専用の SamplingJob::Context とローカルポーズバッファを持たせる。Context はアニメごとのキャッシュなので使い回せません
  • 全レイヤーを同じ ratio でサンプルする。4 本の被弾モーションは同じ尺で作ってあり、時間管理は先頭レイヤーを代表にしています。尺がバラバラだと足の接地などが合わなくなるので、素材側で揃えるのが楽です
  • 重みの合計が 0 のときのフォールバックを入れる。BlendingJob は合計重みが threshold を下回ると rest pose(バインドポーズ)へ落ちる仕様なので、一瞬 T ポーズが挟まる事故を防ぎます

使う側

呼び出し側は薄いヘルパにまとめました。攻撃が来たワールド方向を敵のローカル空間へ落として渡すだけです。

// 初期化 (4 方向ぶんのアニメを登録)
m_hitBlend.Init(&m_animController,
                { "HitBack_F", "HitBack_B", "HitBack_L", "HitBack_R" });

// 被弾時: 攻撃が来た方向を敵ローカル空間 (x=右 z=前) へ変換して再生
const Vector3 hitFrom = -knockbackDir;   // 敵から見た攻撃者側の方向
const Vector2 localDir(hitFrom.Dot(right), hitFrom.Dot(fwd));
m_hitBlend.Play(localDir, 0.05f, /*isLoop=*/false);

再生開始後も SetDirection() で重みだけ差し替えられるので、移動モーション(Walk の 8 方向ストレイフなど)にもそのまま流用できます。今回は被弾リアクション用に単発再生で使っています。

おわりに

  • 方向ブレンド自体は「正規化した方向成分 → レイヤー重み」の数行で実装可能
  • 4 点で足りなければ重み関数を差し替えるだけで 8 方向にも拡張できる

環境: C++20 / DirectX 11 / ozz-animation

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?