【javascript】MMDモデルをブラウザで動かす – three.jsで3Dプログラミング入門

モデル製作者 : つかさ さん

はじめに

今回はjava scriptの3Dライブラリであるthree.jsの環境構築と、3Dモデルをアニメーションさせる方法を記載する。

three.jsとは

three.jsは、ブラウザ上で3Dを扱うWebGLという技術を扱いやすくしたjava scriptライブラリ。WebGLはMicrosoft EdgeやChrome等、ほぼすべてのブラウザで対応しており、ドライバなどのインストールも不要。GPUの機能も利用可能。

数々の3Dファイルの読み込みに対応している他、IK(Inverse Kinematics)や物理演算にも対応しており、初心者にも使いやすいライブラリに感じる。

環境構築

three.jsを利用するには、npmを利用する方法とcdnを利用する方法が一般的である。ただ、npmのインストール等は敷居が高く感じるため、今回はthree.jsライブラリを手動でダウンロードして利用する方法を紹介する。

エディタとして、以下をインストールする

  1. atom
    githubが公開しているテキストエディタ。軽くて多機能。

また、atomを開き以下のプラグインを入れる。プラグインはatomのメニューから「edit – preference – install」と開き、プラグイン名で検索してインストールする。

  1. japanese-menu
    atomの日本語化プラグイン
  2. atom-live-server
    atomで編集したjava scriptをサーバ環境で実行するためのプラグイン。java scriptをローカルで実行すると、ファイル読み込みできないため、3Dモデルのファイル読み込みに必要。

3Dの基本

3Dモデルを表示するために最低限必要なものは以下のとおり。

  1. scene
    1. 3Dオブジェクト
    2. 光源
  2. カメラ
  3. レンダリングエンジン

3Dの世界に該当するsceneというクラスに表示したいもの(オブジェクトや光源)を配置して、カメラで撮影する。その処理結果をレンダリングエンジンが、画面に表示する画像に変換しているというイメージ。

サンプル

three.jsで3Dモデルを表示するサンプルプログラムを以下に示す。

今回は、モデルデータやモーションデータが多く公開されているMMD(Miku Miku Dance)形式のファイルをブラウザで表示して、アニメーションさせる。

フォルダ構成

フォルダ構成は以下のとおり。jsファイルはリンク先(three.jsのCDN)からダウンロードしてindex.htmlと同階層に配置する。なお、3Dモデル(.pmx)とモーション(.vmd)は任意のものに変えてもOK。

mmd/
┣ index.html
┣ three.js
┣ MMDToonShader.js
┣ mmdparser.js
┣ TGALoader.js
┣ MMDLoader.js
┣ CCDIKSolver.js
┣ MMDPhysics.js
┣ ammo.wasm.js
┣ ammo.wasm.wasm
┣ MMDAnimationHelper.js
┣ つかさ式みくV2_Ver1.0a
┃ ┗ つかさ式みくV2_Ver1.0a.pmx
┃   モデル製作者 : つかさ さん
┗ Prhythmatic_motion_ver3.0
  ┗ Prhythmatic配布用3.0.vmd
    配布元:mobiusP さん、原曲 : 「Prhythmatic」Renoさん、
    振り付け:あすぱらさん、モーションアクター:市川さん

サンプルプログラム

index.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>

<body>
    <!-- キャッシュバスティングして、jsファイル読込 -->
    <script>
    document.write(
      '<script src="three.js?' + new Date().getTime() + '"><\/script>',
      '<script src="MMDToonShader.js?' + new Date().getTime() + '"><\/script>',
      '<script src="mmdparser.js?' + new Date().getTime() + '"><\/script>',
      '<script src="TGALoader.js?' + new Date().getTime() + '"><\/script>',
      '<script src="MMDLoader.js?' + new Date().getTime() + '"><\/script>',
      '<script src="CCDIKSolver.js?' + new Date().getTime() + '"><\/script>',
      '<script src="MMDPhysics.js?' + new Date().getTime() + '"><\/script>',
      '<script src="ammo.wasm.js?' + new Date().getTime() + '"><\/script>',
      '<script src="MMDAnimationHelper.js?' + new Date().getTime() + '"><\/script>'
    );
    </script>

    <!-- メイン処理 -->
    <script>
    // シーンの準備
    const scene = new THREE.Scene();

    // ライトの準備
    const directionalLight = new THREE.DirectionalLight('#ffffff', 1);
    directionalLight.position.set(1, 1, 1);
    scene.add(directionalLight);

    // カメラの準備
    const camera = new THREE.PerspectiveCamera(
                          45,window.innerWidth / window.innerHeight,0.1,1000 );
    camera.position.set(0, 25, 25);
    camera.lookAt(new THREE.Vector3(0, 10, 0));

    // レンダラーの準備
    const renderer = new THREE.WebGLRenderer();
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setClearColor(0x7fbfff, 1.0);
    document.body.appendChild(renderer.domElement);

    // 初期状態を表示
    renderer.render(scene, camera);

    // ページ読み込み時に、モデルとモーションデータを読み込んで表示する。
    var helper    = new THREE.MMDAnimationHelper();
    var clock     = new THREE.Clock();
    window.onload = async function(){
      // Ammo呼び出しのバグ対応
      Ammo = await Ammo();

      // 3Dモデル準備
      const modelFileURL  = "つかさ式みくV2_Ver1.0a/つかさ式みくV2_Ver1.0a.pmx";
      const motionFileURL = "Prhythmatic_motion_ver3.0/Prhythmatic配布用3.0.vmd";
      let mmd       = await loadMMD( modelFileURL , motionFileURL );
      let mesh      = mmd.mesh;
      let clip      = mmd.animation;

      // シーンに追加
      scene.add(mesh);

      // ヘルパーに登録
      helper.add( mesh, { animation: clip , physics: true } );

      // アニメーションループの開始
      function animate() {
        // 次のフレーム描画要求(60fps目標)
        requestAnimationFrame(animate);

        // 画面を更新
        helper.update( clock.getDelta() );
        renderer.render( scene, camera );
      }
      animate();
    }

    // MMD読み込み
    async function loadMMD( modelFileURL , motionFileURL ){
      return await new Promise((resolve, reject) => {
        new THREE.MMDLoader().loadWithAnimation( modelFileURL , motionFileURL , obj => resolve(obj) );
      });
    }

    </script>
</body>
</html>

プログラム説明

  • 【10〜20行目】java scriptライブラリ読込
    「? new Data()…」は、ブラウザによるファイルキャッシュを回避して、ライブラリファイルの入れ替え時に新しいファイルが常に読み込まれるようにしている。

  • 【26〜47行目】基本オブジェクトの配置
    WebGLRenderer.render関数に、シーンとカメラを渡すことで画面に描写することができる。このタイミングでは、シーンに光源しか追加していないので、WebGLRenderer.setClearColor関数で設定した背景色のみ表示される。

  • 【56〜67行目】3Dモデル読込
    loadMMD(82〜86行目)により、モデル(mesh)とモーション(clip)を読み込んでsceneに追加している。THREE.MMDLoader().loadWithAnimation関数でライブラリを用いて、MMDの各ファイルを読み込んでおり、第三引数にファイル読み込み完了時の関数(onLoad関数)を渡している。

    onLoad関数では、読み込んだオブジェクト(obj)をPromise.resolve関数に渡すことにより、非同期関数の戻り値に設定している。このためawaitで、非同期関数であるloadMMD関数の処理完了を待つようにしている。

  • 【70〜78行目】アニメーションループ
    requestAnimationFrame関数は最大60FPS(1秒間に60回)となるように、引数に与えられた関数を呼び出す。これにより、animate関数を再帰的に呼び出している。

    helper.update関数で3Dモデルの状態を更新してrenderer.render関数で画面に描画している。

実行結果

atomのメニューから「パッケージ – atom-live-server – Start server」を選択すると、自動でブラウザが開き、index.htmlを表示する。MDDのモーションに必要なボーンがモデルにある等、モデルとモーションの組み合わせが問題ない場合には、アニメーションが開始される。

実行結果

参考資料

  1. three.js – docs 「MMDLoader
タイトルとURLをコピーしました