Help us understand the problem. What is going on with this article?

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>

まとめ

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした