Back to tech

他人には教えたくないWebでMMD

4 min read
Table of Contents

久しぶり3Dモデルをブラウザ上で動かしたいと思い、調査し直してみました

Qiitaや個人ブログなど様々なサイトを参考にしたのですが現バージョンの方法が解説されていません

そのため、2019/07/03におけるモダンなブラウザ上で3Dモデルを動かす方法を記載しておきます

Three.jsとは

Web上でMMDを動かすにためにThree.jsを使用します

Three.jsとは、ウェブブラウザ上で簡単にコンピューターグラフィックス(CG)ができるJavaScirptライブラリです

簡単にコンピュータグラフィックスができるというところがポイントで、WebGLというWebに標準搭載されている技術があるのですが、これで作るのは大変むずかしいため、Three.jsで簡単にCGができるようになっています

公式サイト:

threejs.org
threejs.org

概要(wiki):

Three.js - Wikipedia
ja.wikipedia.org
image

また、MMDを使用するにはメインライブラリであるThree.jsの他に、モジュール的なものであるmmdparser.min.jsやMMDLoader.js等が必要です

さらに、Three.jsは最近?Documentが更新されて大変見やすくなりました

以前はソースコードがドーンとあるだけで「見ればわかるやろ」状態で大変わかりづらかった記憶があります

MMDを動かすのに参考になる箇所をピックアップしておきます

Three.jsは更新スピードがはやい

今日(2019/07/03現在)、Three.jsのバージョンはr106となっています

r105からr106への更新間隔めっちゃ早かったです

更新内容は以下のサイトで確認することができます

github.com
github.com

MMDを使用する上で必要なライブラリもちょいちょい更新がかかっているので作る上では見たほうが良いと思います

ブラウザ上でMMDを動かしてみる

「Three.js MMD」で調べると、実際にMMDで動かすプログラムはでてきますが、前のバージョンだったり、モデルやVMDのロード方法がコールバック地獄で書かれていたりして大変見づらいです

そのため、現在(2019/07/03現在)の最新バージョンであるr106でMMDを動かしてみつつ、async awaitを使うことでプログラムを見やすくしてみました

はじめに動いているところ

プログラムを掲載する前に期待通りに動いている動画を貼り付けておきます

プロ生ちゃんを動かしています

twitter.com
twitter.com

プログラム

重要な箇所であるmain.jsを掲載しておきます

すべてを見たい人はgithubをプログラムを上げていますのでご確認ください

GitHub - Momijinn/SampleWebMMD: Three.js ver_r106にてMMDを喋らせるサンプルプログラム
github.com
image

プログラム解説

動かす方法はGithubにあげているので割愛するとして、ここではプログラムの解説をします

全体的な流れ

プログラムの全体的な流れは、

  1. カメラ(視点)や光の定義
  2. PMXファイルを読み込む
  3. 複数のVMDファイルを読み込む
  4. 複数のAudioファイルを読み込む
  5. 再生

となっています

動かしたいMMDモデルとモーション

はじめにグローバル変数で読み込みたいPMXMotionObjectsを定義してあげます

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 様、本当にありがとうございます

質問内容(一部抜粋)

twitter.com
twitter.com
twitter.com
twitter.com

ループ終了イベントと一度だけのモーション再生の終了イベントは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()ではアニメーションの描画をしています

helper.update(clock.getDelta());で1コマずつ再生しています

注意点

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の動画をみながらやると捗ります

ぜひやってみてください

おすすめ

[MMD] ドーナツホール -DONUT HOLE- [Sour式初音ミク_Resonance]
www.youtube.com
image
[MMD] Booo! [Sour式鏡音リン]
www.youtube.com
image

大変かわいい

参考

ライセンス関係

このプログラムはプロ生ちゃん(暮井 慧)を利用して作成しました

プロ生ちゃん(暮井 慧)
kei.pronama.jp
image