久しぶり3Dモデルをブラウザ上で動かしたいと思い、調査し直してみました
Qiitaや個人ブログなど様々なサイトを参考にしたのですが現バージョンの方法が解説されていません
そのため、2019/07/03におけるモダンなブラウザ上で3Dモデルを動かす方法を記載しておきます
Three.jsとは
Web上でMMDを動かすにためにThree.jsを使用します
Three.jsとは、ウェブブラウザ上で簡単にコンピューターグラフィックス(CG)ができるJavaScirptライブラリです
簡単にコンピュータグラフィックスができるというところがポイントで、WebGLというWebに標準搭載されている技術があるのですが、これで作るのは大変むずかしいため、Three.jsで簡単にCGができるようになっています
公式サイト:
概要(wiki):

また、MMDを使用するにはメインライブラリであるThree.jsの他に、モジュール的なものであるmmdparser.min.jsやMMDLoader.js等が必要です
さらに、Three.jsは最近?Documentが更新されて大変見やすくなりました
以前はソースコードがドーンとあるだけで「見ればわかるやろ」状態で大変わかりづらかった記憶があります
MMDを動かすのに参考になる箇所をピックアップしておきます
- MMDLoaderthree.js docs
- MMDAnimationHelperthree.js docs
- AudioListenerthree.js docs
- AnimationActionthree.js docs
Three.jsは更新スピードがはやい
今日(2019/07/03現在)、Three.jsのバージョンはr106となっています
r105からr106への更新間隔めっちゃ早かったです
更新内容は以下のサイトで確認することができます
MMDを使用する上で必要なライブラリもちょいちょい更新がかかっているので作る上では見たほうが良いと思います
ブラウザ上でMMDを動かしてみる
「Three.js MMD」で調べると、実際にMMDで動かすプログラムはでてきますが、前のバージョンだったり、モデルやVMDのロード方法がコールバック地獄で書かれていたりして大変見づらいです
そのため、現在(2019/07/03現在)の最新バージョンであるr106でMMDを動かしてみつつ、async awaitを使うことでプログラムを見やすくしてみました
はじめに動いているところ
プログラムを掲載する前に期待通りに動いている動画を貼り付けておきます
プロ生ちゃんを動かしています
Qiitaに上がってるWeb上でMMD動かす記事は古いThree.js使って実現してて、現バージョンと全く違うから調査してなんとか動くようになった
kawaii pic.twitter.com/1HelNKNOaf
— みやかわ (@momijinn_aka) July 2, 2019
プログラム
重要な箇所であるmain.jsを掲載しておきます
すべてを見たい人はgithubをプログラムを上げていますのでご確認ください
プログラム解説
動かす方法はGithubにあげているので割愛するとして、ここではプログラムの解説をします
全体的な流れ
プログラムの全体的な流れは、
- カメラ(視点)や光の定義
- PMXファイルを読み込む
- 複数のVMDファイルを読み込む
- 複数のAudioファイルを読み込む
- 再生
となっています
動かしたいMMDモデルとモーション
はじめにグローバル変数で読み込みたいPMXとMotionObjectsを定義してあげます
MotionObjectsという連想配列の中に読み込むVMDやAudioをID管理で格納しています
読み込んだVMDやAudioはこの配列に格納されます
MotionObjectsで少しトリッキーに作っているのがAudioClipです
idがloopだけfalseになっていますが、これには理由があります
vmdによっては音を出すもの(しゃべるや歌う等)の他にも、発話せずに動いているだけということをしたいときがあると思います
後々にAuidoを読み込むための箇所(new THREE.AudioLoader().load)がありますが、このAudioを読み込むかどうかのFlagをAudioClipをここに入れています
trueであれば、audioに格納されているmp3を読み込み、読み込まれたAudioのオブジェクトはAudioClipに格納されます
falseの場合はfalseのままです
const Pmx = "./pmx/pronama/プロ生ちゃん.pmx"; const MotionObjects = [ { id: "loop", VmdClip: null, AudioClip: false }, { id: "kei_voice_009_1", VmdClip: null, AudioClip: true }, { id: "kei_voice_010_2", VmdClip: null, AudioClip: true }, ];
window.onload
HTMLが読み込みが完了後(window.onload)、Init()とLoadModeler()とRender()が呼ばれます
Init()
Init()は、カメラや証明、描画する範囲の指定をしています
LoadModeler()
LoadModeler()は、PMXとVMDとAudioを読み込んでいます
この関数がこのプログラムで重要な箇所になります
ほとんどのサイトではここをコールバッグ地獄で記述しています
また、Three.jsのVer.r93以前を使っているケースがほとんどです
r93以前は、THREE.MMDLoaderでPMXとVMDを読み込みができましたが、r93以上はTHREE.MMDAnimationHelperというものが加わり、少しVMDの読み込み方が変わりました
これらを踏まえて、モダンな書き方はどうするのかと調査ししつつ今どきは(たぶん)こんな書くのかということで、コールバッグ地獄にならないようにasync awaitで書くとことでわかりやすしました
また、VMDの読み込み方もthree.jsの現バージョン(2019/07/03現在)らしく書いてみました
LoadModeler = async () => { const loader = new THREE.MMDLoader(); //Loading PMX LoadPMX = () => { return new Promise(resolve => { loader.load(Pmx, (object) => { mesh = object; scene.add(mesh); resolve(true); }, onProgress, onError); }); } //Loading VMD LoadVMD = (id) => { return new Promise(resolve => { const path = "./vmd/" + id + ".vmd"; const val = MotionObjects.findIndex(MotionObject => MotionObject.id == id); loader.loadAnimation(path, mesh, (vmd) => { vmd.name = id; MotionObjects[val].VmdClip = vmd; resolve(true); }, onProgress, onError); }); } //Load Audio LoadAudio = (id) => { return new Promise(resolve => { const path = "./audio/" + id + ".mp3"; const val = MotionObjects.findIndex(MotionObject => MotionObject.id == id); if (MotionObjects[val].AudioClip) { new THREE.AudioLoader().load(path, (buffer) => { const listener = new THREE.AudioListener(); const audio = new THREE.Audio(listener).setBuffer(buffer); MotionObjects[val].AudioClip = audio; resolve(true); }, onProgress, onError); } else { resolve(false); } }); } // Loading PMX... await LoadPMX(); // Loading VMD... await Promise.all(MotionObjects.map(async (MotionObject) => { return await LoadVMD(MotionObject.id); })); // Loading Audio... await Promise.all(MotionObjects.map(async (MotionObject) => { return await LoadAudio(MotionObject.id); })); //Set VMD on Mesh VmdControl("loop", true); }
読み込んだあとはVmdControl()というVMDの切り替えを行う関数へ渡しています
VmdControl(id, flag)では、再生してほしいIDとループするかしないかのFlagを投げてあげます
IDはMotionObjects内に定義しているものを渡して上げてください
MotionObjects内に存在しないIDが渡されると動きません(returnで返されます)
ループフラッグは、trueにするとループし、falseにすると一度だけ再生されます
この関数で重要なのは、mixerという変数です
const mixer = helper.objects.get(mesh).mixer;
r93以上からTHREE.MMDAnimationHelperにmesh(PMXを読み込んだオブジェクトファイル)とVMDを読み込んだオブジェクトファイルを合体させて再生をします
helper = new THREE.MMDAnimationHelper({ afterglow: 2.0, resetPhysicsOnLoop: true }); helper.add(mesh, { animation: MotionObjects[index].VmdClip, physics: false });
そして、デフォルトでは永久ループする設定になっているため、ループさせないアニメーションの場合は別途一度だけの再生をするように設定しないといけません
mixer.existingAction(MotionObjects[index].VmdClip).setLoop(THREE.LoopOnce);
このループをさせるかさせないかは、どうあがいてもわからなかったのでTwitterで開発者本人聞いて解決をしました
takahiro(John Smith)@superhoge 様、本当にありがとうございます
質問内容(一部抜粋)
helper.objects.get(mesh).mixerでAnimationMixerを参照できるので、addEventListenerを設定してください。https://t.co/I3puQ2v2kP
— takahiro(John Smith) (@superhoge) June 12, 2019
loader.loadWithAnimation(modelFile, vmdFiles, function(mmd) {
mesh = mmd.mesh;
helper.add(mesh, {
animation: mmd.animation,
physics: true
});
helper.objects.get(mesh).mixer.existingAction(mmd.animation).setLoop(THREE.LoopOnce);
…— takahiro(John Smith) (@superhoge) June 13, 2019
ループ終了イベントと一度だけのモーション再生の終了イベントはmixerのaddEventListenerにて”loop”と”finished”で受け取れます
// VMD Loop Event mixer.addEventListener("loop", (event) => { console.log("loop"); }); // VMD Loop Once Event mixer.addEventListener("finished", (event) => { console.log("finished"); });
Render()
Render()ではアニメーションの描画をしています
注意点
Web上でMMDを動かす方法は一通り説明しましたが、今回作成したプログラムは物理演算を付与していません
物理演算を付与できることはできるのですが、モーションを変え続けると以下のエラーがでます
Cannot enlarge memory arrays. Either (1) compile with -s TOTAL_MEMORY=X with X higher than the current value 67108864, (2) compile with -s ALLOW_MEMORY_GROWTH=1 which adjusts the size at runtime but prevents some optimizations, (3) set Module.TOTAL_MEMORY to a higher value before the program runs, or if you want malloc to return NULL (0) instead of this abort, compile with -s ABORTING_MALLOC=0
物理演算を計算しているammo.jsにてメモリがたりねーわと言われているような気がします
物理エンジンを切れば、私が試したところでは上記のエラーは発生しません
解決策をお待ちしています
ちなみに物理エンジンはhelper.addのところで付与することができます
helper.add(mesh, { animation: MotionObjects[index].VmdClip, physics: false //ここをtrueにする });
その他
プログラムについていろいろ説明してきましたが、やはりMMDをいじるときはMMDの動画を見ながらかつ、聞きながらやる必要があると思います
Youutbeやニコニコ動画でMMDの動画をみながらやると捗ります
ぜひやってみてください
おすすめ
大変かわいい
参考
- Three.js wikipedia, https://ja.wikipedia.org/wiki/Three.js
- Three.js, https://threejs.org/
- Three.js docs [MMDAnimationHelper], https://threejs.org/docs/#examples/en/animations/MMDAnimationHelper
- Three.js docs [Audio Listener], https://threejs.org/docs/#api/en/audio/AudioListener
- Qiita three.jsでMMDを使う, https://qiita.com/shoichi1023/items/6cbaefe078c33f600bfe
ライセンス関係
このプログラムはプロ生ちゃん(暮井 慧)を利用して作成しました

コメント
こんにちは。
この記事の、Github上にあるソースコードを使わせていただいたのですが、エラーが出てしまいます。
エラーは、chromeのコンソールにて、
Uncaught TypeError: MotionObjects[index].AudioClip.play is not a function
at VmdControl (main.js:149)
at PoseClickEvent (main.js:214)
at HTMLInputElement.onclick ((index):20)
と出てきます。
AudioClip: trueにした場合のみ出てきます。
※audioフォルダにidの名前のmp3ファイルはしっかりと入れています。
エラーメッセージを見ますと正しくモデルデータが読み込まれていないと推測します。
そのため、正しくモデルデータが入っているか確認させてください。
モデルのデータは、 “/SampleWebMMD/pmx/pronama” に格納します。
pronama ディレクトリに、プロ生ちゃんの モデルデータやテクスチャデータを入れていますでしょうか。
eg) プロ生ちゃん.pmx, hadashi.png, pronama_skirt.png
今一度確認お願いします。