LoginSignup
7
6

More than 5 years have passed since last update.

IOSでthree.ar.jsを使ってMMDを表示する

Last updated at Posted at 2018-10-06

IOSでthree.ar.jsを使ってMMDを表示する

はじめに

IOSでthree.ar.jsを動かす際にXcodeが必要になります。
Mac環境がない方は、android端末でお試しください。
androidでのthree.ar.jsはこちらのアプリをDLします。
ARCore by Google

その後、こちらのリンクから端末にインストールしていただければ完了です。
WebARonARCore
また、一部端末には対応しておりませんので、ご容赦ください。
対応端末の一覧はこちらから確認できます。
Supported Devices

モデルはLat様のものを拝借しており、無改変で使用させて頂いております。

モデルの使用、再配布、モーションの利用、再配布についてもそれぞれファイル内に同包されているRedeMeに従ってください。

この記事で書くこと

  • three.ar.jsとAR.js
  • 前提知識
  • 動作環境の準備
  • HelloWorld
  • MMDを表示とアニメーション

こんな感じ

○ three.ar.jsとAR.js

three.ar.jsの説明をする前にまず、従来のWeb上でのARについてですがAR.jsというものを使用するのが一般的かと思います。

AR.jsは定められたマーカーの上にオブジェクトなどを描画していく、マーカー式のARで、イメージとしては、カードをカメラで写すとそのキャラクターがカードの上に現れると言ったものが近いです。

 それに対して three.ar.jsですが、こちらは、現実にある物体を判定し、その上にオブジェクトを描画する形式です。そのため、より現実世界にマッチした描画等が行えます。
 ただし、three.ar.jsは本筋からそれるので詳しくは説明しませんが (詳しく知りたい方はこちら:ARCore Overview) 、Googleの提供するARCore(昔はTangoという技術だった)といった技術にJavaScriptのAPIからアクセスして動かしています。ちなみにARCoreはARと言ってますが技術的にはMR(Mixed Reality)という括りになると思います。

 また、このARCoreはまだ開発途中で、chrome等には組み込まれていませんので、専用のブラウザ (chromiumの開発途中のもの)をスマートフォン、タブレット等にインストールして動かす形になります。
 先に書きました通り、AndroidではARCoreと言ったアプリがPrayストアにあるのでそちらをインストールしていただければ大丈夫です。

○ 前提知識

この記事を読むにあたって、three.jsのVRの知識が必要となります。わからない方は下記の記事にまとめてありますので、よろしければご覧ください。

○ 動作環境の準備

  1. Webサーバーを動かせるようにする
    これについてはXAMMPなりlive-serverなりtomcatなりを使ってください。おそらく一番手順が簡単かと思うのでlive-serverの記事を貼っておきます。
    [その他] ChromeにてAjaxでローカルファイルにアクセス

  2. 端末側で専用のブラウザをインストールする。
    まずgithubから専用のブラウザのプロジェクトをダウンロードします。
    GUI : WebARonARKit
    CUI : git clone https://github.com/google-ar/WebARonARKit.git

    WebARonARKitの中にあるWebARonARKit.xcodeprojを開きます。
    端末を繋げておきます。

    ①、②の手順で画面を切り替え、③の名前を適当に変え、④と⑤を自分の環境に合わせて変更します。
    ④についてはStatusにエラーが出ていても大丈夫です。
    また、⑤については、IOS11以降が対象となるので、それ以前のものにはインストールできません。
    その後⑥を自分の繋げた端末の名前に切り替え、⑦を押すと端末上でアプリが実行され、閉じるとホーム画面にアプリが増えていると思います。
    スクリーンショット 2018-10-06 10.35.48.jpg

○ HelloWorld

画面クリックでヒットした際にオブジェクトが現れます。
平たいところで試してください。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <title>three.ar.js - Load Model</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, user-scalable=no,
  minimum-scale=1.0, maximum-scale=1.0">
  <style>
    body {
      font-family: monospace;
      margin: 0;
      overflow: hidden;
      position: fixed;
      width: 100%;
      height: 100vh;
      -webkit-user-select: none;
      user-select: none;
    }
    canvas {
      position: absolute;
      top: 0;
      left: 0;
    }
  </style>
</head>
<body>
<script src="../third_party/three.js/three.js"></script>
<script src="../third_party/three.js/VRControls.js"></script>
<script src="../third_party/three.js/OBJLoader.js"></script>
<script src="../third_party/three.js/MTLLoader.js"></script>

<script src="../third_party/three.ar.js"></script>
<script src="./js/main.js"></script>
</body>
</html>
main.js

var vrDisplay;
var vrControls;
var arView;

var canvas;
var camera;
var scene;
var renderer;
var model;

var shadowMesh;
var planeGeometry;
var light;
var directionalLight;

var OBJ_PATH = './assets/ArcticFox_Posed.obj';
var MTL_PATH = './assets/ArcticFox_Posed.mtl';
var SCALE = 0.1;

//ARが使えるかどうかの判定
THREE.ARUtils.getARDisplay().then(function (display) {
  if (display) {
    vrDisplay = display;
    init();
  } else {
    THREE.ARUtils.displayUnsupportedMessage();
  }
});

function init() {
  //デバッグパネルを画面上に表示
  var arDebug = new THREE.ARDebug(vrDisplay);
  document.body.appendChild(arDebug.getElement());

  //背景を透明にする
  renderer = new THREE.WebGLRenderer({ alpha: true });
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.autoClear = false;
  canvas = renderer.domElement;
  document.body.appendChild(canvas);
  scene = new THREE.Scene();

  //カメラからの映像を背景(というよりは背景のもう一個奥のディスプレイのレイヤー)にセット
  arView = new THREE.ARView(vrDisplay, renderer);

  // 大体three.jsのカメラと一緒だが、現実のカメラとの奥行きの同期ができるようになるっぽい?
  camera = new THREE.ARPerspectiveCamera(
    vrDisplay,
    60,
    window.innerWidth / window.innerHeight,
    vrDisplay.depthNear,
    vrDisplay.depthFar
  );

  //現実のカメラの動きとVR(scene)のカメラを同期させる
  vrControls = new THREE.VRControls(camera);

  renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;

  directionalLight = new THREE.DirectionalLight();
  // 将来的にはAR専用のライトを使うようになるらしい
  directionalLight.intensity = 0.3;
  directionalLight.position.set(10, 15, 10);
  // ここの光で影を作ってる
  directionalLight.castShadow = true;
  light = new THREE.AmbientLight();
  scene.add(light);
  scene.add(directionalLight);

  // 影を映すためのオブジェクト
  planeGeometry = new THREE.PlaneGeometry(2000, 2000);
  // 現実の床に合わせて回転させてる
  planeGeometry.rotateX(-Math.PI / 2);

  // 影を含むメッシュを作成してる
  //メッシュじゃないほうがいい場合はreceiveShadowをfalseにすれば影をレンダリングするだけになる
  shadowMesh = new THREE.Mesh(planeGeometry, new THREE.ShadowMaterial({
    color: 0x111111,
    opacity: 0.15,
  }));
  shadowMesh.receiveShadow = true;
  scene.add(shadowMesh);

  THREE.ARUtils.loadModel({
    objPath: OBJ_PATH,
    mtlPath: MTL_PATH,
    OBJLoader: undefined, // uses window.THREE.OBJLoader by default
    MTLLoader: undefined, // uses window.THREE.MTLLoader by default
  }).then(function(group) {
    model = group;
    // objectの持ってる全てのメッシュに影を追加する
    model.children.forEach(function(mesh) { mesh.castShadow = true; });

    model.scale.set(SCALE, SCALE, SCALE);
    model.position.set(10000, 10000, 10000);
    scene.add(model);
  });

  window.addEventListener('resize', onWindowResize, false);
  canvas.addEventListener('click', spawn, false);

  update();
}

function update() {

  // いま表示されてるのものをクリア
  renderer.clearColor();

  //現実のカメラと同期してオブジェクトを正しい場所に相対的に移動
  arView.render();

  //床用のオブジェクトを更新(おそらくこれがしたいがためにARのカメラを使用している)
  camera.updateProjectionMatrix();

  // 現実のカメラに合わせてカメラの位置をアップデート
  vrControls.update();

  //VR空間を描画
  renderer.clearDepth();
  renderer.render(scene, camera);

  // アニメーションフレームはVR空間が描画されたあと
  vrDisplay.requestAnimationFrame(update);
}

function onWindowResize () {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}

//オブジェクトを描画するための関数
function spawn (e) {
  var x = e.clientX / window.innerWidth;
  var y = e.clientY / window.innerHeight;

  //擬似的な光線を発射し、(おそらく床用のオブジェクトとの)衝突判定を行っている
  var hits = vrDisplay.hitTest(x, y);

  if (!model) {
    console.warn('Model not yet loaded');
    return;
  }

  // 衝突したらオブジェクトを描画
  if (hits && hits.length) {
    var hit = hits[0];

    //これで現実世界の床の上になるような正しい位置(かつ影がうまいこと表示できる位置)を取得
    //どうやら影をうまいこと描画するプロセスが複雑らしいが詳しいことは割愛(わかる方いたら教えてください)
    var matrix = new THREE.Matrix4();
    var position = new THREE.Vector3();
    matrix.fromArray(hit.modelMatrix);
    position.setFromMatrixPosition(matrix);

    // 影をセット、今後その影も現実に合わせて回転するようになるっぽい?
    shadowMesh.position.y = position.y;

    // モデルをその場所にセット
    THREE.ARUtils.placeObjectAtHit(model,hit,1,true);

    // 現実のカメラに合わせてオブジェクトを回転
    var angle = Math.atan2(
      camera.position.x - model.position.x,
      camera.position.z - model.position.z
    );
    model.rotation.set(0, angle, 0);
  }
}

three.ar.js/examples/load-modelのコメントをわかりやすく編集しました。
基本的なところはthree.jsと変わらず、

  • 空間の背景にカメラからの映像を描画していること
  • クリック(タップ)された位置から光線を発射し、衝突した位置にオブジェクトを配置していること
  • カメラを動かすとそれに合わせてオブジェクトを相対的に動かしてそこにあるように見せていること

あたりが特徴的なところでしょうか?

○ MMDの表示とアニメーション

MMDのモデルやVMDファイルはこちらからどうぞ

shoichi1023/three_ar_sample

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <title>three.ar.js - Load Model</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, user-scalable=no,
  minimum-scale=1.0, maximum-scale=1.0">
  <style>
    body {
      font-family: monospace;
      margin: 0;
      overflow: hidden;
      position: fixed;
      width: 100%;
      height: 100vh;
      -webkit-user-select: none;
      user-select: none;
    }
    canvas {
      position: absolute;
      top: 0;
      left: 0;
    }
  </style>
</head>
<body>
<script src="../third_party/three.js/three.js"></script>
<script src="../third_party/three.js/VRControls.js"></script>
<script src="../third_party/three.js/mmdparser.min.js"></script>
<script src="../third_party/three.js/TGALoader.js"></script>
<script src="../third_party/three.js/ammo.js"></script>
<script src="../third_party/three.js/CCDIKSolver.js"></script>
<script src="../third_party/three.js/MMDPhysics.js"></script>
<script src="../third_party/three.js/MMDLoader.js"></script>
<script src="../third_party/three.ar.js"></script>
<script src="./js/main.js"></script>
</body>
</html>

main.js

var modelReady = false;

var vrDisplay;
var vrControls;
var arView;

var canvas;
var camera;
var scene;
var renderer;
var model;

var shadowMesh;
var planeGeometry;
var light;
var directionalLight;


var clock = new THREE.Clock();
var helper;
var vmdIndex = 0;
var MMD_PATH = '../mmd/miku/Lat式ミクVer2.31_Normal.pmd';
var vmdFiles = [
  {
    name: "repeat",
    file: "../mmd/vmd_repeat/repeat.vmd"
  }
];
var SCALE = 0.02;

//ARが使えるかどうかの判定
THREE.ARUtils.getARDisplay().then(function (display) {
  if (display) {
    vrDisplay = display;
    init();
  } else {
    THREE.ARUtils.displayUnsupportedMessage();
  }
});

function init() {
  //デバッグパネルを画面上に表示
  var arDebug = new THREE.ARDebug(vrDisplay);
  document.body.appendChild(arDebug.getElement());

  //背景を透明にする
  renderer = new THREE.WebGLRenderer({ alpha: true });
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.autoClear = false;
  canvas = renderer.domElement;
  document.body.appendChild(canvas);
  scene = new THREE.Scene();

  //カメラからの映像を背景(というよりは背景のもう一個奥のディスプレイのレイヤー)にセット
  arView = new THREE.ARView(vrDisplay, renderer);

  // 大体three.jsのカメラと一緒だが、現実のカメラとの奥行きの同期ができるようになるっぽい?
  camera = new THREE.ARPerspectiveCamera(
    vrDisplay,
    60,
    window.innerWidth / window.innerHeight,
    vrDisplay.depthNear,
    vrDisplay.depthFar
  );

  //現実のカメラの動きとVR(scene)のカメラを同期させる
  vrControls = new THREE.VRControls(camera);

  renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;

  directionalLight = new THREE.DirectionalLight();
  // 将来的にはAR専用のライトを使うようになるらしい
  directionalLight.intensity = 0.3;
  directionalLight.position.set(10, 15, 10);
  // ここの光で影を作ってる
  directionalLight.castShadow = true;
  light = new THREE.AmbientLight();
  scene.add(light);
  scene.add(directionalLight);

  // 影を映すためのオブジェクト
  planeGeometry = new THREE.PlaneGeometry(2000, 2000);
  // 現実の床に合わせて回転させてる
  planeGeometry.rotateX(-Math.PI / 2);

  // 影を含むメッシュを作成してる
  //メッシュじゃないほうがいい場合はreceiveShadowをfalseにすれば影をレンダリングするだけになる
  shadowMesh = new THREE.Mesh(planeGeometry, new THREE.ShadowMaterial({
    color: 0x111111,
    opacity: 0.15,
  }));
  shadowMesh.receiveShadow = true;
  scene.add(shadowMesh);

  var onProgress = function(xhr) {};
  var onError = function(xhr) {
    console.log("load mmd error");
  }; //アニメーションをつけるためのヘルパー

  helper = new THREE.MMDHelper(); //MMDLoaderをインスタンス化
  var loader = new THREE.MMDLoader();
  //loadModelメソッドにモデルのPATH
  //コールバックに画面に描画するための諸々のプログラムを書く
  loader.loadModel(
    MMD_PATH,
    function(object) {
      model = object
      model.children.forEach(function(mesh) { mesh.castShadow = true; });
      model.scale.set(SCALE, SCALE, SCALE);
      model.position.set(10000, 10000, 10000);
      scene.add(model);

      helper.add(model);
      //vmdFileがあれば対応付けする
      if (vmdFiles && vmdFiles.length !== 0) {
        function readAnime() {
          var vmdFile = vmdFiles[vmdIndex].file;
          //vmdのローダー
          loader.loadVmd(
            vmdFile,
            function(vmd) {
              loader.createAnimation(model, vmd, vmdFiles[vmdIndex].name);
              vmdIndex++;
              if (vmdIndex < vmdFiles.length) {
                //配列分読み込むまで再帰呼び出し
                readAnime();
              }else{
                //モデルに対してアニメーションをセット
                helper.setAnimation(model);
                helper.unifyAnimationDuration({ afterglow: 1.0 });
                model.mixer.stopAllAction();
                //実行
                selectAnimation(model, 0, true);
              }
            },
            onProgress,
            onError
          );
        }
        readAnime();
      }
      modelReady = true;
    },
    onProgress,
    onError
  );


  window.addEventListener('resize', onWindowResize, false);
  canvas.addEventListener('click', spawn, false);

  update();
}

function update() {

  // いま表示されてるのものをクリア
  renderer.clearColor();

  //現実のカメラと同期してオブジェクトを正しい場所に相対的に移動
  arView.render();

  //床用のオブジェクトを更新(おそらくこれがしたいがためにARのカメラを使用している)
  camera.updateProjectionMatrix();

  // 現実のカメラに合わせてカメラの位置をアップデート
  vrControls.update();
 
  //VR空間を描画
  renderer.clearDepth();
  renderer.render(scene, camera);

  // アニメーションフレームはVR空間が描画されたあと
  vrDisplay.requestAnimationFrame(update);

  if (modelReady) {
    helper.animate(clock.getDelta());
  }
}

function onWindowResize () {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}

//オブジェクトを描画するための関数
function spawn (e) {
  var x = e.clientX / window.innerWidth;
  var y = e.clientY / window.innerHeight;

  //擬似的な光線を発射し、(おそらく床用のオブジェクトとの)衝突判定を行っている
  var hits = vrDisplay.hitTest(x, y);

  if (!model) {
    console.warn('Model not yet loaded');
    return;
  }

  // 衝突したらオブジェクトを描画
  if (hits && hits.length) {
    var hit = hits[0];

    //これで現実世界の床の上になるような正しい位置(かつ影がうまいこと表示できる位置)を取得
    //どうやら影をうまいこと描画するプロセスが複雑らしいが詳しいことは割愛(わかる方いたら教えてください)
    var matrix = new THREE.Matrix4();
    var position = new THREE.Vector3();
    matrix.fromArray(hit.modelMatrix);
    position.setFromMatrixPosition(matrix);

    // 影をセット、今後その影も現実に合わせて回転するようになるっぽい?
    shadowMesh.position.y = position.y;

    // モデルをその場所にセット
    THREE.ARUtils.placeObjectAtHit(model,hit,1,true);

    // 現実のカメラに合わせてオブジェクトを回転
    var angle = Math.atan2(
      camera.position.x - model.position.x,
      camera.position.z - model.position.z
    );
    model.rotation.set(0, angle, 0);
  }
}


//通常の関連付けだとモーフファイル(表情のモーションファイル)と分離されてしまうのでくっつけて実行するためのヘルパー
function selectAnimation(mesh, index, loop) {
  var clip, mclip, action, morph, i;
  i = 2 * index;
  //一つのアニメーションを抜き出し(モーフじゃない方)
  clip = mesh.geometry.animations[i];
  //ミキサーにセット
  action = mesh.mixer.clipAction(clip);
  //対応するモーフを抜き出し
  mclip = mesh.geometry.animations[i + 1];
  //ミキサーにセット
  morph = mesh.mixer.clipAction(mclip);
  //ループの設定、
  if (loop) {
    action.repetitions = "Infinity";
    morph.repetitions = "Infinity";
  } else {
    action.repetitions = 0;
    morph.repetitions = 0;
  }
  //一旦全部止めて
  mesh.mixer.stopAllAction();
  //同時に動かす
  action.play();
  morph.play();
}

かわいいミクちゃんですね。
パンツ覗いたらだめですよ。^^

7
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6