[Babylon.js #07] 地形上を複数MMDキャラが隊列移動する仕組みを作る

はじめに

昨日、地形を作り、MMDを複数表示させてランダム移動するまで実装しましたが、今日は、RPGのように隊列を組んで移動する処理を実装してみたのでそのメモです。

前回の記事:


動画(Youtube):

動画(PC):

動画(VR):
※人数を増やすと処理落ちするするので、3人に減らしています。

1. 今回やること

前回は、MMDキャラクターを地形上に配置して、ランダム移動+地面吸着(Ray)で歩かせるところまで作りました。

今回はそこから一歩進めて、**複数キャラクターをRPGっぽく「列で歩かせる」**実装を追加します。

やりたいことはシンプルで、

  • 先頭キャラが移動する
  • 後続キャラがその軌跡を追う
  • 地形の高低差にも追従する

という流れです。

見た目の変化は地味ですが、中身の実装はそれなりに増えます。 そのぶん、ここで一度まとめておくと後で「WASD操作」「カメラ追従」に進みやすくなります。

2. 隊列移動の考え方(先頭と後続で役割を分ける)

最初に結論を書くと、今回の隊列移動は 履歴追従 で作っています。

役割分担

  • 先頭(leader)

    • これまで通り、ターゲットへ向かって移動
  • 後続(followers)

    • 先頭の「過去の位置」を一定距離ぶん遅れて追う

これをやると、曲がり角で後続も同じ軌跡を通るので、見た目が自然になります。

なぜ「先頭の横や後ろの固定位置」を追わせないのか?

たとえば「先頭の後ろに常に -Z オフセット」みたいな実装だと、直進中はよくても、旋回で不自然になりやすいです。

  • 曲がると後続がショートカットする
  • 隊列の曲がり方が機械的になる
  • 地形上でズレが目立つ

その点、履歴追従 は「先頭の通った道をなぞる」ので、RPG風の列移動に相性が良いです。


3. 複数MMDキャラをロードする構成にする

前回の1体構成から、今回は複数体を前提にしてロード部分を整理しました。

キャラ定義 CharacterSpec[]

まず、キャラクターごとの設定を配列で持つようにします。

  • モデルの場所
  • モデルファイル名
  • 開始位置
  • 歩行速度などの調整値
type CharacterSpec = {
  name: string;
  modelRootUrl: string;
  modelFileName: string;
  motionFilePath: string;
  startPos: Vector3;
  walkSpeed?: number;
  halfRange?: number;
  footOffsetY?: number;
  modelForwardOffset?: number;
};

これを characters: CharacterSpec[] として並べるだけで、人数を増やせます。

ロード結果は WalkerHandle で管理

MMDメッシュそのものに加えて、移動制御用の rootTransformNode)も必要になるので、返り値をまとめた型を用意しておくと扱いやすいです。

type WalkerHandle = {
  root: TransformNode;
  mesh: Mesh;
};

4. MMD本体はそのまま、移動制御は root(TransformNode)に分離する

今回の実装で大事なのがここです。

メッシュを直接動かさない

MMDモデルのメッシュは、モーション再生・スケルトン・姿勢更新など、内部的にいろいろ動きます。 そこへ移動・回転・地面吸着まで混ぜると、責務がごちゃつきます。

そこで、キャラ制御用の TransformNode を1つ作って、MMDメッシュをその子にする 形にします。

function createWalkerFromMesh(scene: Scene, ground: Mesh, mmdMesh: Mesh, options: WalkerOptions): WalkerHandle {
  const root = new TransformNode(`${options.name}_root`, scene);
  root.position.copyFrom(options.startPos);

  mmdMesh.parent = root;
  mmdMesh.position.set(0, 0, 0);
  mmdMesh.rotation.set(0, 0, 0);

  return { root, mesh: mmdMesh };
}

これで、以下をすべて root 側に集約できます。

  • 平面上の移動
  • 地形への吸着
  • 進行方向への回転

MMD本体は「アニメーションを再生するだけ」に近い役割になります。


5. 個別制御をやめて、隊列制御を1か所に集約する

前回まではキャラごとに onBeforeRenderObservable を持っても問題なかったのですが、隊列にする場合は 全員をまとめて制御した方が分かりやすく、競合もしにくいです。

やめたこと

  • createWalkerFromMesh() の中で個別 setupWalkerBehavior() を呼ぶ

追加したこと

  • setupPartyWalkerBehavior(scene, ground, roots, options) を1つ作る
  • 先頭・後続を同じ onBeforeRender 内で更新する

この構造にしたことで、後続が「先頭の履歴」を使う処理を自然に書けるようになります。


6. 先頭キャラの移動履歴を記録する

履歴追従のコアになるのが、先頭キャラの位置履歴です。

履歴データの型

位置だけでも動きますが、向きも一緒に持っておくと後で使いやすいので、今回は yaw も入れています。

type PathSample = {
  pos: Vector3;
  yaw: number;
};

履歴配列

const history: PathSample[] = [
  {
    pos: leader.position.clone(),
    yaw: leader.rotation.y,
  },
];

毎フレーム入れるのではなく、距離ベースで記録する

毎フレーム履歴を積むと、FPSで密度が変わるうえ、配列が増えすぎます。 そこで、先頭が一定距離以上動いたら1点追加するようにしています。

const HISTORY_STEP_DIST = options.historyStepDist ?? 1.0;

これを pushLeaderHistory() 内で使って、

  • 最後の履歴点からの距離を測る
  • HISTORY_STEP_DIST を超えたら push

という形にしています。

この値が小さいほど軌跡は滑らかになりますが、履歴点は増えます。


7. 後続キャラは「何メートル後ろか」で履歴を引く

後続は、単純に「最新の履歴点」を目指すのではなく、一定距離ぶん過去の位置を使います。

隊列間隔 followSpacing

const FOLLOW_SPACING = options.followSpacing ?? 18;

たとえば、

  • 2人目 → FOLLOW_SPACING * 1
  • 3人目 → FOLLOW_SPACING * 2
  • 4人目 → FOLLOW_SPACING * 3

という感じで、先頭からの遅れ距離を増やします。

履歴から「指定距離だけ戻った位置」を取る

履歴の末尾(最新)から逆方向にたどっていき、指定距離ぶん戻った地点を返す関数を作ります。

この処理が getHistorySampleByDistance() です。

中でやっていることは、

  1. 履歴末尾から1区間ずつ見る
  2. 区間の長さを引いていく
  3. 目的距離の区間に入ったら線形補間で位置を出す

という内容です。

この方式にすると、後続キャラが「先頭の通った道」をそのままなぞるように動きます。


8. 地形への吸着(Ray)は全員に共通で適用する

前回の実装でも使った、Rayによる地形吸着もそのまま使います。

流れ

  • root.position.x/z から上方向の位置を作る
  • 下向き Ray を飛ばす
  • ground に当たったら pickedPoint.y を使う
  • footOffsetY で足元の補正を入れる
  • yLerp で滑らかに追従させる
const rayOrigin = new Vector3(root.position.x, RAY_FROM_Y, root.position.z);
const ray = new Ray(rayOrigin, Vector3.Down(), RAY_FROM_Y + 50);
const hit = scene.pickWithRay(ray, (m) => m === ground);

yLerp を入れている理由

地形の凹凸があると、Y座標を毎フレーム直接代入すると上下がカクついて見えます。 yLerp で補間しておくと、吸着しつつ自然に見えます。


9. 進行方向へ回転する(Yaw補間)

移動だけだとキャラの向きが固定になってしまうので、移動ベクトルから向きを作って root.rotation.y を補間します。

向きの計算

const targetYaw = Math.atan2(move.x, move.z) + MODEL_FORWARD_OFFSET;

MODEL_FORWARD_OFFSET は、モデルの正面方向の差(前向きの軸が違う問題)を吸収するための値です。 MMDモデルでは Math.PI が必要になることが多いです。

角度補間で気をつける点

0 の境界をまたぐと、普通に引き算しただけでは大回りになります。 そこで shortestAngleDelta() を使って、最短方向に補間するようにしています。

この関数は地味ですが、見た目の安定感にかなり効きます。


10. 調整値を PARTY_TUNE にまとめる

実装が増えてくると、速度や間隔の調整値が関数内に散らばりがちです。 今回はここを一度まとめて、PARTY_TUNE として外出ししました。

const PARTY_TUNE = {
  walkSpeed: 36,
  followSpacing: 20,
  historyStepDist: 1.0,
  yLerp: 0.25,
  yawLerpSpeed: 6.0,
  footOffsetY: -0.2,
  halfRange: 180,
  modelForwardOffset: Math.PI,
} satisfies PartyWalkerOptions;

この形にしてよかった点

  • 調整対象が一か所に集まる
  • 実装コードとチューニングコードを分けられる
  • 次回のWASD操作対応でも流用しやすい

11. setupMmd() の流れ(今回の全体像)

今回の setupMmd() は、ざっくり以下の順序になっています。

  1. MmdRuntime を作成
  2. 共通モーション(BVMD)を1回ロード
  3. キャラ定義 characters を用意
  4. 各キャラをロードして walkers に格納
  5. MMDモデルを作って共通モーションを適用
  6. mmdRuntime.playAnimation()
  7. setupPartyWalkerBehavior(..., PARTY_TUNE) で隊列移動を開始
  8. ループ監視 + bloom 演出

今回は「共通モーションを1回ロードして使い回す」構成なので、人数を増やした時の見通しも良いです。


12. 人数を増やしてみた結果(2体→5体)

まず2体で動作確認して、その後5体まで増やしてみましたが、履歴追従の構成のまま問題なく動かせました。

この時点で確認できたのは、少なくとも次の点です。

  • 隊列の基本挙動は成立している
  • 地形の高低差にも追従できる
  • 複数MMDの同時再生と干渉していない
  • followSpacing 調整で見た目の印象を変えられる

まだ残っている調整ポイント

実装は成立しましたが、見た目の仕上げとしてはまだ触れる余地があります。

  • ランダムターゲット更新時の急旋回
  • 隊列間隔の微調整
  • キャラごとの差(モデルサイズ・向きオフセット)

このへんは「バグ」というより、仕上げのチューニングです。


13. まとめ

今回は、Babylon.js + babylon-mmd で複数MMDキャラクターを地形上に配置し、履歴追従による隊列移動を実装しました。

やったことをまとめると、以下です。

  • 複数キャラのロード構成を作る
  • TransformNode(root)でキャラ制御を分離
  • 先頭の移動履歴を記録
  • 後続が履歴を距離ベースで追従
  • Rayで地形吸着
  • Yaw補間で進行方向へ回転
  • PARTY_TUNE で調整値を分離

「列で歩く」だけですが、中身としてはかなりゲーム寄りの構成になってきました。