[Babylon.js #08] メビウスの帯の上をMMDモデルが歩く

はじめに

今回は、メビウスの帯(∞っぽい形)を自前で生成し、その上を MMD キャラクターに歩かせる実装をやってみたので、その備忘録メモです。

  • メビウス帯の自前メッシュ生成
  • 帯の中心線に沿った姿勢制御(進行方向・左右・上方向)
  • babylon-mmd による MMD モデル再生
  • MMDアニメーションが途中で止まる問題の回避
  • パラメータをその場で触れる Inspector UI の追加

前回(#07)は MMD の編成・追従系の話でしたが、今回は続編というより 別ラインの実験回 です。 MMD 側の知見(特にループ停止対策)はそのまま流用しています。

動画(YouTube):

Redditにも投稿してみました:

動画(PC):

今回の完成イメージ

やりたかったのは、こういう感じです。

  • 赤いメビウス帯がゆっくり回転する
  • キャラが帯の表面に沿って歩く
  • 帯の幅や形状を UI で調整できる

見た目の調整パラメータを UI で触れるようにしたことで、コードを書き換えずに試行錯誤できるのがかなり良かったです。


この記事でやること

  1. メビウス帯の形状パラメータを定義する
  2. 中心線(トラック)とフレームを作る
  3. そこから帯メッシュを生成する
  4. トラック上の姿勢をサンプリングする
  5. MMD モデルを読み込み、歩行体として接続する
  6. MMDアニメーション停止対策を入れる
  7. Inspector UI でリアルタイム調整できるようにする

1.見た目調整パラメータを先にまとめる

今回は先にパラメータをまとめておくと、とても扱いやすくなりました。

type TuningParams = {
  scaleX: number;
  scaleZ: number;
  baseHalfWidth: number;
  yLiftMax: number;
  pinch: number;
  smoothPow: number;
  rotSpeedX: number;
  rotSpeedY: number;
  rotSpeedZ: number;
  speed: number;
  laneOffset: number;
  footLift: number;
  mmdOffsetY: number;
  mmdScale: number;
  mmdYaw: number;
  meshScale: number;
};

const params: TuningParams = {
  // Mobius shape
  scaleX: 4.4,
  scaleZ: 2.0,
  baseHalfWidth: 0.7,
  yLiftMax: 0.95,
  pinch: 0.45,
  smoothPow: 2.2,

  // Rotation speed
  rotSpeedX: 0.6,
  rotSpeedY: 0.0,
  rotSpeedZ: 0.0,

  // Walker
  speed: 0.06,
  laneOffset: 0.0,
  footLift: 1.0,

  // MMD anchor fine tune
  mmdOffsetY: -0.9,
  mmdScale: 0.08,
  mmdYaw: Math.PI,

  // Render
  meshScale: 2.5,
};

ポイントは以下です。

  • shape系(帯の形)
  • motion系(回転速度・歩行速度)
  • mmd系(モデルの位置・サイズ・向き補正)

を分けておくこと。

この構造にしておくと、あとで UI に乗せるのが簡単になります。


2.メビウス帯のトラックを作る

帯をいきなり作るのではなく、まずは 中心線(トラック) を作ります。 さらに、トラック上の各点で

  • 接線 T
  • 法線 N
  • 従法線 B

を持たせます(TNBフレーム)。

中心線の考え方

今回は「∞っぽいメビウス」にしたかったので、中心線はこんな形です。

  • x = scaleX * sin(t)
  • z = scaleZ * sin(t) * cos(t)
  • y は中央交差付近だけ持ち上げる

中央交差(t ≈ 0, π)で上下に逃がすために、nearCross を使って y を作っています。

フレームの構築

フレームは Frenet でもいいのですが、ねじれが暴れやすいので今回は 簡易 Parallel Transport を使っています。 最後に holonomy correction を入れて、継ぎ目のねじれも抑えています。

このあたりは「見た目の安定性」に直結するところで、今回の肝です。


3.トラックからメビウス帯メッシュを生成する

トラックができたら、各サンプル点で

  • NB を使って幅方向を作る
  • t * 0.5 だけ半ひねりを入れる

ことで、メビウス帯の表面を作れます。

const h = t * 0.5; // メビウス半ひねり
const ch = Math.cos(h);
const sh = Math.sin(h);

// 幅方向ベクトル(フレーム内で半ひねり)
const W = n.scale(ch).add(b.scale(sh)).normalize();
const pos = p.add(W.scale(v));

最後の継ぎ目は普通に貼ると破綻するので、幅方向のインデックスを反転してつないでいます。

ここがメビウスらしいポイントです。


4.トラック上の姿勢をサンプリングする

キャラを帯の上に乗せるには、位置だけでなく 向き が必要です。

そこで sampleTrackFrame(track, u) を作って、進行率 u (0..1) から以下を返すようにしました。

  • p: 位置
  • t: 進行方向(forward)
  • w: 帯の幅方向(right)
  • sn: 面法線(up)

これを使って、歩行体(walkerRoot)の姿勢を毎フレーム更新します。

const forward = f.t.normalize();
const right = f.w.normalize();
let up = Vector3.Cross(forward, right).normalize();

if (Vector3.Dot(up, f.sn) < 0) {
  up = up.scale(-1);
}

up が裏返るケースがあるので、Dot で向きをチェックして補正しているのが実用上のポイントです。


5.MMDモデルを歩行体に接続する

ここは前回までの MMD 実装を流用しました。 構成としてはこうです。

  • walkerRoot … 帯の上を動く親ノード(位置・姿勢担当)
  • mmdAnchor … MMDモデルの微調整ノード(オフセット・スケール・向き)
  • mmdMesh … 実際の MMD メッシュ
const walkerRoot = new TransformNode("walkerRoot", scene);
const mmdAnchor = new TransformNode("mmdAnchor", scene);
mmdAnchor.parent = walkerRoot;

MMD 読み込み後に mmdMesh.parent = mmdAnchor として接続します。


6. MMDアニメーションが途中で止まる問題の対策

ここ、今回も再発しました。 前に一度ハマっているので、今回は 前回の安定版ロジックをそのまま流用して正解でした。

原因っぽいところ

babylon-mmd の再生が環境差やモーションデータ差で、ループ境界付近で止まることがあります。 isLoop = true だけでは安定しないケースがある、という体感です。

実際の対策

  • boneTracks / morphTracks から 終了フレームをスキャン
  • frameCount / duration を補完
  • onBeforeRenderObservable で 境界直前に seek(0) + playAnimation()
const durationSeconds = maxFrame / 30; // MMDは通常30fps基準
scene.onBeforeRenderObservable.add(() => {
  if (mmdRuntime.currentTime >= durationSeconds - 0.016) {
    if (typeof (mmdRuntime as any).seekAnimation === "function") {
      (mmdRuntime as any).seekAnimation(0, false);
    } else {
      (mmdRuntime as any).currentTime = 0;
    }
    mmdRuntime.playAnimation();
  }
});

- 0.016(約1フレーム分)手前で巻き戻すのが、地味ですが効きます。 境界ぴったりで処理すると止まりやすい環境があるためです。


7. Inspector UI を追加する

今回かなり良かったのがこれです。 形状・速度・MMD補正をその場で触れるだけで、試行錯誤の速度が一気に上がりました。

UIで触れるもの

Mobius Shape

  • scaleX, scaleZ
  • baseHalfWidth
  • yLiftMax
  • pinch
  • smoothPow
  • meshScale

Motion

  • rotSpeedX/Y/Z
  • speed(歩行速度)
  • laneOffset
  • footLift

MMD Anchor

  • mmdOffsetY
  • mmdScale
  • mmdYaw

実装の考え方

UI変更時に大きく2種類あります。

  1. 即時反映できるもの

    • mmdAnchor.position.y
    • mmdAnchor.scaling
    • mmdAnchor.rotation.y
  2. メッシュを作り直す必要があるもの

    • scaleX, baseHalfWidth など形状に関わるもの

後者は rebuildMobius() を用意して、以下をやると管理しやすいです。

  • 古いメッシュを dispose
  • 新しい track を生成
  • 新しいメッシュを生成
  • walkerRoot.parent を新メッシュに付け直し

実装時にハマった点メモ

1. walkerRoot の position / rotation を毎フレーム更新する場所

親付け (walkerRoot.parent = mesh) は初回だけで OK ですが、 位置と回転のコピーは毎フレーム必要です。

ここを if (parent !== mesh) の中に入れると、初回しか更新されなくなります。


2. rotationX / rotationY / rotationZ の参照

定数を params に寄せた後、更新コードが古いままだと壊れます。

mesh.rotation.x += dt * params.rotSpeedX;
mesh.rotation.y += dt * params.rotSpeedY;
mesh.rotation.z += dt * params.rotSpeedZ;

このように、参照先を params に統一しておくのが安全です。


3. buildMobiusLemniscateTrack() の中でグローバル定数を参照しない

最初は scaleX などを直接参照していましたが、Inspector UI を入れるなら params を引数で渡す構成にした方が良いです。

function buildMobiusLemniscateTrack(params: TuningParams, segmentsU = 380): MobiusTrack

これだけで UI 連動がかなり綺麗になります。


完成コード(構成の要点)

ここでは長くなりすぎるので、要点だけ再掲します。

  • params に調整値を集約
  • buildMobiusLemniscateTrack(params, segmentsU)
  • createMobiusFromTrack(scene, track, segmentsV)
  • sampleTrackFrame(track, u)
  • setupMmdOnWalker(scene, mmdAnchor, shadowGenerator?)
  • setupInspectorUI(params, rebuildMobius, mmdAnchor)

今回のコードは「その場で実験しながら育てる」タイプだったので、最終的に Inspector UI まで入れたのはかなり正解でした。

次にやりたいこと

今回でベースはできたので、次はこのあたりをやりたいです。

  • 帯のマテリアル強化(グラデーション / 発光 / Fresnel)
  • MMD の歩行モーションに合わせて速度同期(足滑り軽減)
  • 複数キャラを同時に流す
  • カメラ演出(追従 / シネマ風)
  • WebXR での見え方確認

前回 #07 の路線(編成・追従)は別記事としてまた続けます。 今回は完全に「メビウス帯を歩かせる回」でした。

おわりに

一週間前はうまくいかなかったところでも、 前回の知見(特に MMD ループ停止対策)を再利用したら一気に進みました。

こういう「一度ハマった沼を、次に回避できる」のは実装の積み上げ感があって楽しいですね。

次は、Inspector UI で詰めた値を元に、見た目をもう少し仕上げていきます。