はじめに
今回は、メビウスの帯(∞っぽい形)を自前で生成し、その上を MMD キャラクターに歩かせる実装をやってみたので、その備忘録メモです。
- メビウス帯の自前メッシュ生成
- 帯の中心線に沿った姿勢制御(進行方向・左右・上方向)
- babylon-mmd による MMD モデル再生
- MMDアニメーションが途中で止まる問題の回避
- パラメータをその場で触れる Inspector UI の追加
前回(#07)は MMD の編成・追従系の話でしたが、今回は続編というより 別ラインの実験回 です。 MMD 側の知見(特にループ停止対策)はそのまま流用しています。
[Babylon.js #07] 地形上を複数MMDキャラが隊列移動する仕組みを作る
Babylon.js と babylon-mmd を使い、MMDキャラクターの複数体運用を一歩進めて、地形上での隊列移動(RPG風の縦列歩行)を実装します。TransformNode による制御、Ray による地面吸着、Yaw 補間、履歴追 …
https://humanxai.info/posts/babylonjs-07-mmd-party-formation-follow/動画(YouTube):
【Babylon.js × MMD #03】メビウスの帯の上をMMDモデルが歩く(Inspector調整付き)
Babylon.js と babylon-mmd を使って、メビウスの帯の上を MMDモデルが歩くシーンを作成。 -メビウス帯の自前メッシュ生成 - 中心線に沿った姿勢制御(進行方向・左右・上方向) - MMDモデルの接続(TransformNode / Anchor) - MMDアニメーションが途中で止まる...
https://www.youtube.com/shorts/m13EcwTmIbcRedditにも投稿してみました:
MMD character walking on a Möbius strip with Babylon.js (WIP)
A small WIP experiment in Babylon.js. I’m testing an MMD character walking on a custom Möbius strip mesh. Current focus is orientation control along the center path (forward/up/right) and keeping the motion stable. Built with Babylon.js + babylon-mmd. I may not be able to reply quickly, but thanks for watching.
https://www.reddit.com/r/babylonjs/comments/1rbjny6/mmd_character_walking_on_a_m%C3%B6bius_strip_with/動画(PC):
今回の完成イメージ
やりたかったのは、こういう感じです。
- 赤いメビウス帯がゆっくり回転する
- キャラが帯の表面に沿って歩く
- 帯の幅や形状を UI で調整できる
見た目の調整パラメータを UI で触れるようにしたことで、コードを書き換えずに試行錯誤できるのがかなり良かったです。
この記事でやること
- メビウス帯の形状パラメータを定義する
- 中心線(トラック)とフレームを作る
- そこから帯メッシュを生成する
- トラック上の姿勢をサンプリングする
- MMD モデルを読み込み、歩行体として接続する
- MMDアニメーション停止対策を入れる
- 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.トラックからメビウス帯メッシュを生成する
トラックができたら、各サンプル点で
NとBを使って幅方向を作る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,scaleZbaseHalfWidthyLiftMaxpinchsmoothPowmeshScale
Motion
rotSpeedX/Y/Zspeed(歩行速度)laneOffsetfootLift
MMD Anchor
mmdOffsetYmmdScalemmdYaw
実装の考え方
UI変更時に大きく2種類あります。
-
即時反映できるもの
mmdAnchor.position.ymmdAnchor.scalingmmdAnchor.rotation.y
-
メッシュを作り直す必要があるもの
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 で詰めた値を元に、見た目をもう少し仕上げていきます。
💬 コメント