はじめに
過去記事でも何度か実装しているPMMの表示およびアニメーションを「Babylon.js」でやってみたので、その備忘録メモです。
表示自体はネット上のサンプル通りのコードでうまくいくのですが、vmdアニメーション処理が思うようにいかず、以下のgithubで公開されている「vmd converter」を利用させてもらってます。
・pmx converter
・vmd converter
GitHub - TkymHrt/test-babylon-mmd
Contribute to TkymHrt/test-babylon-mmd development by creating an account on GitHub.
https://github.com/TkymHrt/test-babylon-mmdただ、それでもアニメーションが停止するバグにかなり手間取ったので、その辺りのトラブルTipsが主な記事内容になります。
Babylon.js +WebGPUでMMD(PMX)を動かす。BPMX変換とBloom表現のデモ #babylonjs #webgpu #mmd #bpmx #TypeScript
自作モデルを最新のWebGPU環境(Babylon.js)でアニメ―ション。WebGPUはWebGLよりも判定が厳しく、PMXのシェーダーエラーや、アニメーションが15秒で止まる謎のバグに苦戦しましたが、BPMX変換とフレーム数スキャンロジックで解決しています。Bloom(発光)と透明影(Transparency...
https://www.youtube.com/shorts/L1rhQAN9cQg前回の記事:
[Babylon.js #01] 環境構築とWebGPUの夜明け(Hello World)
2026年版Babylon.js入門。Viteを使用した高速な開発環境の構築から、必要なパッケージのインストール、そしてWebGPUEngineを使用した「Hello World」までの手順をステップバイステップで解説します。
https://humanxai.info/posts/babylonjs-01-setup-helloworld/1. PMX読み込みの壁:WebGPUは甘くない
前回の記事で、環境構築及び、球体表示する「Hello World」実装まで終了。
次は意気揚々と自作のMMDモデル(.pmx)を読み込もうとしたのですが、ブラウザがクラッシュ。
WebGLでは多少のテクスチャ不備や仕様違反があっても「なんとなく」表示してくれることが多かったのですが、WebGPUは非常に厳格です。
- シェーダーエラー: テクスチャが不足しているマテリアルがあると、パイプラインの生成自体に失敗する。
- 非同期の罠: 大容量のPMX解析中に処理が詰まる。
解決策:最適化フォーマット .bpmx への転換
「生のPMX」をWebGPUで扱うのは茨の道だと悟り、Babylon.js用に最適化されたフォーマット .bpmx を採用することにしました。
これは babylon-mmd ライブラリがサポートしている形式で、ロード時間が爆速になり、WebGPU上での安定性も格段に向上します。
// main.ts でのインポート
import "babylon-mmd/esm/Loader/Optimized/bpmxLoader";
// 読み込み部分
const mmdImportResult = await SceneLoader.ImportMeshAsync(
"",
"./lain/",
"lain.bpmx",
scene
);
2. 「動かない」トラブルシューティング
モデルの読み込みには成功し、画面に lain が表示されました。しかし……動きません。
Tポーズ(初期姿勢)のまま、微動だにしないのです。アニメーションファイル(.bvmd)は読み込んでいるはずなのに、なぜ?
罠:MMDランタイムは「登録」しないと動かない
ここが Babylon.js 初心者(そして私)が必ずハマる落とし穴です。
MmdRuntime を作成しただけでは、Babylon.js の描画ループ(Render Loop)と連携してくれません。物理演算やボーンの更新を行うには、シーンに対して明示的に「登録」を行う必要がありました。
// ランタイムを作成
const mmdRuntime = new MmdRuntime(scene);
// 【最重要】これを忘れると動きません!
// シーンの描画ループにMMDの更新処理を割り込ませる
mmdRuntime.register(scene);
// その後にモデルを作成
const mmdModel = mmdRuntime.createMmdModel(mmdMesh);
この register(scene) というたった1行を追加した瞬間、lain が呼吸を始め、物理演算でスカートがふわりと揺れました。
「動かない時は register を疑え」。これが WebGPU MMD の鉄則です。
3. 15秒の壁(無限ループ問題)
Tポーズを脱出し、軽快に踊り始めた lain。しかし、その喜びも束の間でした。 ダンスが始まって約15秒後(正確にはモーションの尺が終わった瞬間)、唐突にアニメーションが停止してしまったのです。
カメラは動かせるので、フリーズしたわけではありません。まるで「踊り疲れた」かのように、最後のポーズで固まってしまいます。
原因:エンジンの「待ちぼうけ」
デバッグログを仕込んで調査したところ、原因はモーションデータ(.bvmd)の メタデータ不足 にありました。
Babylon.js のエンジン側が、「アニメーションの正しい終了時間」を把握できておらず、データが終わっているのに「まだ続きがあるはずだ」と虚無の時間を再生し続けていたのです。
解決策:全フレームをスキャンして「真の終わり」を教える
データに長さが書いていないなら、自分で数えればいい。 力技ですが、モーションデータを解析して「最後のキーフレーム」を探し出し、正しい時間を上書きするロジックを実装しました。
// BVMDローダーで読み込み
const bvmd = await bvmdLoader.loadAsync("motion", "dance.bvmd");
const anyBvmd = bvmd as any;
// --- フレーム数スキャン ---
let maxFrame = 0;
// ボーンアニメーションの最大フレームを探す
if (Array.isArray(anyBvmd.boneTracks)) {
for (const track of anyBvmd.boneTracks) {
if (track.frameNumbers.length > 0) {
const lastFrame = track.frameNumbers[track.frameNumbers.length - 1];
if (lastFrame > maxFrame) maxFrame = lastFrame;
}
}
}
// モーフ(表情)アニメーションも同様にチェック...
// (省略)
// --- 解決! ---
// スキャンした「本当の長さ」を強制的に書き込む
anyBvmd.frameCount = maxFrame;
anyBvmd.duration = maxFrame;
// これでエンジンが正しくループ位置を理解できる
modelAny.setAnimation("motion", true);
3. 15秒の壁(無限ループ問題)
「動いた!」と喜んだのも束の間。 ダンスが始まって約15秒後、唐突にlainが動きを止めてしまいました。 ブラウザがフリーズしたわけではなく、カメラは動くのに、キャラクターだけが静止するのです。
原因:Duration(長さ)の取得失敗と「待ちぼうけ」
デバッグログを見て判明したのは、モーションファイル(.bvmd)の長さ情報が正しく読み取れていないことでした。
エンジン側が「アニメーションの長さは不明(または0秒)」と誤認したり、逆に「もっと長い」と勘違いして、データが存在しない虚無の時間で「終了待ち」をしていたのです。
解決策:フレーム数をスキャンして補完する
データ任せにするのをやめ、コード側で「本当の終わり」を探しに行くロジックを実装しました。
// モーションデータをロード
const bvmd = await bvmdLoader.loadAsync("motion", "dance.bvmd");
const anyBvmd = bvmd as any;
// ボーンとモーフの全トラックを走査して、最大のフレーム番号を探す
let maxFrame = 0;
if (Array.isArray(anyBvmd.boneTracks)) {
for (const track of anyBvmd.boneTracks) {
const lastFrame = track.frameNumbers[track.frameNumbers.length - 1];
if (lastFrame > maxFrame) maxFrame = lastFrame;
}
}
// 正しい長さを強制的に書き込む
anyBvmd.frameCount = maxFrame;
anyBvmd.duration = maxFrame;
この「手動スキャン」により、エンジンが正確なループ地点を理解できるようになり、途切れることのない永遠のループが実現しました。
ありがとうございます。セクション3の「無限ループ」の解決、非常に分かりやすくまとまっていますね。コードの抜粋も完璧です。
それでは、最後の仕上げとなる セクション4(ビジュアル) と セクション5(まとめ) を作成しました。 これで記事の技術的な部分と、感動のフィナーレが繋がります。
4. ビジュアルの仕上げ:WebGPUの輝き
動きは完璧になりましたが、デフォルトの見た目ではまだ少し「平面的」です。
せっかく WebGPU を使っているのですから、リッチな表現を追加して lain を魅力的に引き立てましょう。
影(Shadow):透明度を味方につける
MMDモデル、特にアニメ調のキャラクターは、髪の毛や服の裾に「透過テクスチャ」を多用します。 通常の影設定だと、透明な部分も「板」として認識されてしまい、地面に四角い影が落ちてしまいます。これでは興醒めです。
そこで、Babylon.js の ShadowGenerator にある 透明度対応オプション を有効化します。
const shadowGenerator = new ShadowGenerator(1024, dirLight);
// WebGPUならこれを使わない手はない!
// 透明テクスチャを考慮した、柔らかい影を落とす
shadowGenerator.transparencyShadow = true;
shadowGenerator.enableSoftTransparentShadow = true;
// モデル側も影を受け取る設定に
mesh.receiveShadows = true;
shadowGenerator.addShadowCaster(mesh);
これで、髪の毛の細かい束も、スカートのレースも、地面に美しく投影されるようになりました。
発光(Bloom):空間を演出する
仕上げに、ポストプロセス(後処理)エフェクトの Bloom(発光) を追加します。 WebGPU のパワーを使えば、負荷を気にせずリッチな光の表現が可能です。
// レンダリングパイプラインを作成
const pipeline = new DefaultRenderingPipeline("default", true, scene, [camera]);
// Bloomを有効化
pipeline.bloomEnabled = true;
pipeline.bloomThreshold = 0.8; // 輝度0.8以上の明るい部分だけを光らせる
pipeline.bloomWeight = 0.3; // 光の強さ
pipeline.bloomKernel = 64; // 光の拡散範囲
これを適用すると、白いエプロンや肌のハイライトがほんのりと輝き、キャラクターの実在感が一気に増します。
5. 完成
数々のエラー、Tポーズでの硬直、そして15秒の壁……。 多くの障害を乗り越え、ついに WebGPU 上で自作モデル「lain」が踊り続けられるようになりました。
- BPMX: 最適化フォーマットで高速ロード
- Register: 更新ループへの同期
- Manual Scan: アニメーション全長の補完
これらが揃って初めて、2026年のブラウザ3D表現は完成します。 実際の動作の様子は、以下の動画でご覧ください。
Babylon.js +WebGPUでMMD(PMX)を動かす。BPMX変換とBloom表現のデモ #babylonjs #webgpu #mmd #bpmx #TypeScript
自作モデルを最新のWebGPU環境(Babylon.js)でアニメ―ション。WebGPUはWebGLよりも判定が厳しく、PMXのシェーダーエラーや、アニメーションが15秒で止まる謎のバグに苦戦しましたが、BPMX変換とフレーム数スキャンロジックで解決しています。Bloom(発光)と透明影(Transparency...
https://www.youtube.com/shorts/L1rhQAN9cQg
💬 コメント