JavaScript
three.js
AR
AR.js
Tween.js

Three.js + AR.js + Tween.js で遊ぶ

More than 1 year has passed since last update.

はじめに

『A-FrameとAR.jsで超簡単AR』 でお手軽に AR で遊べましたが、少しものたりないと思いました。Three.jsAR.js でもう少し本格的なものを作って遊びました。
せっかくの機会ですので、AR.js のサンプルをベースに、json 形式の 3DCG モデルの表示、3D オブジェクトのピッキング、Tween.js を使って楽しいアニメーションを付けました。
詳しくはコード中のコメント文でどうぞ。

Screenshot_20170916-233216.png

動作サンプル

Windows の Chrome と Firefox、Android の Chrome で動作確認したサンプルをどうぞ(自分が飽きたら消します)。

  • 以下の二つのマーカを紙に印刷するか、PC 画面等に表示

hiro.jpg

kanji.jpg

  • こちらにアクセス(スマホの方は以下の QR)

QR_Code1505573323.png

  • マーカをカメラでうつし、表示された CG をクリック(タップ)

コード

index.html
<!DOCTYPE html>
<!-- Three/AR/Tween.js サンプル by mkoku 2017/09/16 -->
<!-- ベース... https://jeromeetienne.github.io/AR.js/three.js/examples/mobile-performance.html -->
<!-- カメラを使うため https 接続が必要(オレオレ証明書またはホスティング等で) -->
<!-- Windows の Chrome と Firefox、Android の Chrome で動作確認 -->
<html>
<head>
  <meta charset="utf8">
  <meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no,minimum-scale=1,maximum-scale=1">
  <style>
    body {
      margin: 0px;
      overflow: hidden;
    }  
  </style>
  <title>Three/AR/Tween.js サンプル by mkoku</title>
</head>

<body>

<!-- Three.js のインクルード -->
<!-- https://github.com/mrdoob/three.js/build/ より DL  -->
<script src='three.min.js'></script>

<!-- AR.js のインクルード -->
<!-- https://github.com/jeromeetienne/AR.js/tree/master/three.js/build/ より DL  -->
<script src="ar.min.js"></script>

<!-- アニメーションのために Tween.js をインクルード -->
<!-- https://github.com/tweenjs/tween.js/tree/master/src/ より DL -->
<script src="Tween.js"></script>

<script>
//===================================================================
// three.js の各種設定
//===================================================================
var scene = new THREE.Scene();                        // シーンの作成
var renderer = new THREE.WebGLRenderer({              // レンダラの作成
  antialias: true,                                    // アンチエイリアス有効
  alpha: true,                                        // canvasに透明度バッファを持たせる
});
renderer.setClearColor(new THREE.Color("black"), 0);  // レンダラの背景色
renderer.setSize(640, 480);                           // レンダラのサイズ
renderer.domElement.style.position = "absolute";      // レンダラの位置は絶対値
renderer.domElement.style.top = "0px";                // レンダラの上端
renderer.domElement.style.left = "0px";               // レンダラの左端
document.body.appendChild(renderer.domElement);       // レンダラの DOM を body に入れる
var camera = new THREE.Camera();                      // カメラの作成
scene.add(camera);                                    // カメラをシーンに追加
var light = new THREE.DirectionalLight(0xffffff);     // 平行光源(白)を作成
light.position.set(0, 0, 2);                          // カメラ方向から照らす
scene.add(light);                                     // シーンに光源を追加

//===================================================================
// arToolkitSource(マーカトラッキングするメディアソース)
//===================================================================
var source = new THREEx.ArToolkitSource({             // arToolkitSourceの作成
  sourceType: "webcam",                               // Webカメラを使う(スマホもこれでOK)
});
source.init(function onReady() {                      // ソースを初期化し、準備ができたら
  onResize();                                         // リサイズ処理
});

//===================================================================
// arToolkitContext(カメラパラメータ、マーカ検出設定)
//===================================================================
var context = new THREEx.ArToolkitContext({           // arToolkitContextの作成
  debug: false,                                       // デバッグ用キャンバス表示(デフォルトfalse)
  cameraParametersUrl: "camera_para.dat",             // カメラパラメータファイル
  detectionMode: "mono",                              // 検出モード(color/color_and_matrix/mono/mono_and_matrix)
  imageSmoothingEnabled: true,                        // 画像をスムージングするか(デフォルトfalse)
  maxDetectionRate: 60,                               // マーカの検出レート(デフォルト60)
  canvasWidth: source.parameters.sourceWidth,         // マーカ検出用画像の幅(デフォルト640)
  canvasHeight: source.parameters.sourceHeight,       // マーカ検出用画像の高さ(デフォルト480)
});
context.init(function onCompleted(){                  // コンテクスト初期化が完了したら
  camera.projectionMatrix.copy(context.getProjectionMatrix());   // 射影行列をコピー
});

//===================================================================
// リサイズ処理
//===================================================================
window.addEventListener("resize", function() {        // ウィンドウがリサイズされたら
  onResize();                                         // リサイズ処理
});
// リサイズ関数
function onResize(){
  source.onResizeElement();                           // トラッキングソースをリサイズ
  source.copyElementSizeTo(renderer.domElement);      // レンダラも同じサイズに
  if(context.arController !== null){                  // arControllerがnullでなければ
    source.copyElementSizeTo(context.arController.canvas);  // それも同じサイズに
  } 
}

//===================================================================
// ArMarkerControls(マーカと、マーカ検出時の表示オブジェクト)
//===================================================================
//-------------------------------
// その1(hiroマーカ+立方体)
//-------------------------------
// マーカ
// ネットでhiroマーカの画像を得て、以下の AR.js のマーカトレーニングサイトで patt を作成
// https://jeromeetienne.github.io/AR.js/three.js/examples/marker-training/examples/generator.html
var marker1 = new THREE.Group();                      // マーカをグループとして作成
var controls = new THREEx.ArMarkerControls(context, marker1, {    // マーカを登録
  type: "pattern",                                    // マーカのタイプ
  patternUrl: "hiro.patt",                            // マーカファイル
});
scene.add(marker1);                                   // マーカをシーンに追加
// モデル(メッシュ)
var geo = new THREE.CubeGeometry(1, 1, 1);            // cube ジオメトリ(サイズは 1x1x1)
var mat = new THREE.MeshNormalMaterial({              // マテリアルの作成
  transparent: true,                                  // 透過
  opacity: 0.5,                                       // 不透明度
  side: THREE.DoubleSide,                             // 内側も描く
});
var mesh1 = new THREE.Mesh(geo, mat);                 // メッシュを生成
mesh1.name = "cube";                                  // メッシュの名前(後でピッキングで使う)
mesh1.position.set(0, 0.5, 0);                        // 初期位置
marker1.add(mesh1);                                   // メッシュをマーカに追加
// マーカ隠蔽(cloaking)
var videoTex = new THREE.VideoTexture(source.domElement);  // 映像をテクスチャとして取得
videoTex.minFilter = THREE.NearestFilter;             // 映像テクスチャのフィルタ処理
var cloak = new THREEx.ArMarkerCloak(videoTex);       // マーカ隠蔽(cloak)オブジェクト
cloak.object3d.material.uniforms.opacity.value = 1.0; // cloakの不透明度
marker1.add(cloak.object3d);                          // cloakをマーカに追加
//-------------------------------
// その2(kanjiマーカ+.json)
//-------------------------------
// マーカ
// ネットでkanjiマーカの画像を得て、以下の AR.js のマーカトレーニングサイトで patt を作成
// https://jeromeetienne.github.io/AR.js/three.js/examples/marker-training/examples/generator.html
var marker2 = new THREE.Group();                      // マーカをグループとして作成
var controls = new THREEx.ArMarkerControls(context, marker2, {    // マーカを登録
  type: "pattern",                                    // マーカのタイプ
  patternUrl: "kanji.patt",                           // マーカファイル
});
scene.add(marker2);                                   // マーカをシーンに追加
// モデル(メッシュ)
var mesh2;                                            // モデルを入れる箱
var loader = new THREE.JSONLoader();                  // json形式のモデルを読み込むローダ
loader.load("rocket.json", function(geo, mat) {       // モデルを読み込む
  // Processing のサンプルに付属の rocket.obj を Blender で json形式にエクスポートして自作
  // rocket.obj, rocket.mtl, rocket.png を以下から DL
  // https://github.com/processing/processing-android/tree/master/examples/Basics/Shape/LoadDisplayOBJ/data
  // Blender 用のエクスポータは Three.js の utils/exporters/addons/io_three に有り
  mesh2 = new THREE.Mesh(geo, mat[0]);                // メッシュ化
  mesh2.name = "rocket";                              // メッシュの名前(後でピッキングで使う)
  mesh2.scale.set(0.5, 0.5, 0.5);                     // 初期サイズ(現物合わせ)
  mesh2.position.set(0, 0.5, 0);                      // 初期位置(現物合わせ)
  marker2.add(mesh2);                                 // メッシュをマーカに追加
});
// マーカ隠蔽(cloaking)
var videoTex = new THREE.VideoTexture(source.domElement);  // 映像をテクスチャとして取得
videoTex.minFilter = THREE.NearestFilter;             // 映像テクスチャのフィルタ処理?
var cloak = new THREEx.ArMarkerCloak(videoTex);       // マーカ隠蔽(cloak)オブジェクト
cloak.object3d.material.uniforms.opacity.value = 1.0; // cloakの不透明度
marker2.add(cloak.object3d);                          // cloakをマーカに追加

//===================================================================
// Tween アニメーション
//===================================================================
//-------------------------------
// mesh1 について(cubeが転がる)
//-------------------------------
var twIni1 = {posZ: 0, rotX: 0};                      // 初期パラメータ
var twVal1 = {posZ: 0, rotX: 0};                      // tweenによって更新されるパラメータ
var twFor1 = {posZ: -2, rotX: -Math.PI};              // ターゲットパラメータ
function tween1() {                                   // 「行き」のアニメーション
  var tween = new TWEEN.Tween(twVal1)                 // tweenオブジェクトを作成
  .to(twFor1, 2000)                                   // ターゲットと到達時間
  .easing(TWEEN.Easing.Back.Out)                      // イージング
  .onUpdate(function() {                              // フレーム更新時の処理
    mesh1.position.z = twVal1.posZ;                   // 位置を変更
    mesh1.rotation.x = twVal1.rotX;                   // 回転を変更
  })
  .onComplete(function() {                            // アニメーション完了時の処理
    tween1_back();                                    // 「帰り」のアニメーションを実行
  })
  .delay(0)                                           // 開始までの遅延時間
  .start();                                           // tweenアニメーション開始
}
function tween1_back() {                              // 「帰り」のアニメーション
  var tween = new TWEEN.Tween(twVal1)
  .to(twIni1, 2000)                                   // ターゲットを初期パラメータに設定
  .easing(TWEEN.Easing.Back.InOut)
  .onUpdate(function() {
    mesh1.position.z = twVal1.posZ;
    mesh1.rotation.x = twVal1.rotX;
  })
  .onComplete(function() {
    // なにもしない
  })
  .delay(100)
  .start();
}
//-------------------------------
// mesh2 について(rocketが飛ぶ)
//-------------------------------
var twIni2 = {posY: 0.5, rotY: 0};                    // 初期パラメータ
var twVal2 = {posY: 0.5, rotY: 0};                    // tweenによって更新されるパラメータ
var twFor2 = {posY: 5, rotY: 2*Math.PI};              // ターゲットパラメータ
function tween2() {                                   // 「行き」のアニメーション
  var tween = new TWEEN.Tween(twVal2)                 // tweenオブジェクトを作成
  .to(twFor2, 2000)                                   // ターゲットと到達時間
  .easing(TWEEN.Easing.Quadratic.InOut)               // イージング
  .onUpdate(function() {                              // フレーム更新時の処理
    mesh2.position.y = twVal2.posY;                   // 位置を変更
    mesh2.rotation.y = twVal2.rotY;                   // 回転を変更
  })
  .onComplete(function() {                            // アニメーション完了時の処理
    tween2_back();                                    // 「帰り」のアニメーションを実行
  })
  .delay(0)                                           // 開始までの遅延時間
  .start();                                           // tweenアニメーション開始
}
function tween2_back() {                              // 「帰り」のアニメーション
  var tween = new TWEEN.Tween(twVal2)
  .to(twIni2, 3000)                                   // ターゲットを初期パラメータに設定
  .easing(TWEEN.Easing.Quintic.InOut)
  .onUpdate(function() {
    mesh2.position.y = twVal2.posY;
    mesh2.rotation.y = twVal2.rotY;
  })
  .onComplete(function() {
    // なにもしない
  })
  .delay(0)
  .start();
}

//===================================================================
// マウスダウン(タップ)によるピッキング処理
//===================================================================
window.addEventListener("mousedown", function(ret) {
  var mouseX = ret.clientX;                           // マウスのx座標
  var mouseY = ret.clientY;                           // マウスのy座標
  mouseX =  (mouseX / window.innerWidth)  * 2 - 1;    // -1 ~ +1 に正規化されたx座標
  mouseY = -(mouseY / window.innerHeight) * 2 + 1;    // -1 ~ +1 に正規化されたy座標
  var pos = new THREE.Vector3(mouseX, mouseY, 1);     // マウスベクトル
  pos.unproject(camera);                              // スクリーン座標系をカメラ座標系に変換
  // レイキャスタを作成(始点, 向きのベクトル)
  var ray = new THREE.Raycaster(camera.position, pos.sub(camera.position).normalize());
  var obj = ray.intersectObjects(scene.children, true);   // レイと交差したオブジェクトの取得
  if(obj.length > 0) {                                // 交差したオブジェクトがあれば
    picked(obj[0].object.name);                       // ピックされた対象に応じた処理を実行
  }
});
// ピックされた対象に応じた処理
function picked(objName) {
  switch(objName) {
    case "cube":                                      // cubeなら
      tween1();                                       // cubeのアニメーションを実行
      break;
    case "rocket":                                    // rocketなら
      tween2();                                       // rocketのアニメーションを実行
      break;
    default:
      break;
  }
}

//===================================================================
// レンダリング・ループ
//===================================================================
function renderScene() {                              // レンダリング関数
  requestAnimationFrame(renderScene);                 // ループを要求
  if(source.ready === false)    { return; }             // メディアソースの準備ができていなければ抜ける
  context.update(source.domElement);                  // ARToolkitのコンテキストを更新
  TWEEN.update();                                     // Tweenアニメーションを更新
  renderer.render(scene, camera);                     // レンダリング実施
}
renderScene();                                        // 最初に1回だけレンダリングをトリガ

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

まとめ

かなり長いコードになってしまいましたが…
楽しかったです。