のぐそんブログ

暗いおじさんがシコシコ書くブログです。

8th WallでマーカーレスARを試す

8th Wallとは

8th Wall社が提供するARプラットフォームです。
月1000viewまでは無料で利用できます。ただしローカル環境のみです。

使い方

アカウントを作成する

8th Wallの公式サイトでアカウント作成します。

利用するデバイスを認証する

「Device Authorization」ボタンを押します。

モバイルぼ場合は実機で、QRコードを読み込みます。
バイスが認証されます。

サンプルファイルを動かしてみる

サイドナビの「QUICK START」を押します。
QRコードが表示されるので、認証したデバイスQRコードを読み込みます。

サンプルファイルが表示されます。

他のサンプルはgithubからダウンロードすることができます。
ダウンロードしたファイルは以下です。
examplesの中にサンプルがあるので、こちらをひな形にプロジェクトを作成すると良さそうです。

.
├── README.md
├── examples
├── gettingstarted
├── images
├── index.html
├── serve
└── xrextras

プロジェクトを作成する

サイドナビの「DASHBOARD」から、「Create a new project」を押します。

プロジェクト名を入力してプロジェクトを作成すると、アプリケーションKEYが作成されます。

このKEYを<script async src="//apps.8thwall.com/xrweb?appKey=XXXXXXXX"></script>に設定します。
以下はサンプルファイルの/examples/threejs/placeground/index.html`になります。

<!doctype html>
<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>8th Wall Web: three.js</title>
    <link rel="stylesheet" type="text/css" href="index.css">

    <!-- THREE.js must be supplied -->
    <script src="//cdnjs.cloudflare.com/ajax/libs/three.js/93/three.min.js"></script>

    <!-- Required to load glTF (.gltf or .glb) models -->
    <script src="//cdn.rawgit.com/mrdoob/three.js/master/examples/js/loaders/GLTFLoader.js"></script>

    <!-- Javascript tweening engine -->
    <script src="//cdnjs.cloudflare.com/ajax/libs/tween.js/16.3.5/Tween.min.js"></script>

    <!-- XR Extras - provides utilities like load screen, almost there, and error handling.
         See github.com/8thwall/web/xrextras -->
    <script src="//cdn.8thwall.com/web/xrextras/xrextras.js"></script>

    <!-- 8thWall Web - Replace the app key here with your own app key -->
    <script async src="//apps.8thwall.com/xrweb?appKey=XXXXXXXX"></script>

    <!-- client code -->
    <script src="index.js"></script>
  </head>

  <body>
    <canvas id="camerafeed"></canvas>
  </body>
</html>

ローカルファイルを起動

サンプルファイルの/examples/threejs/placegroundをひな形に新しいプロジェクトを作成してみます。

まず上記のディレクトリをコピーして複製します。
名前はdinosaurとしました。

.
├── README.md
├── dinosaur
│   ├── index.html
│   ├── index.js
│   ├── model.gltf
│   ├── test.md
│   └── tree.glb
├── examples
├── gettingstarted
├── images
├── index.html
├── serve
└── xrextras

次にnpmモジュールをインストールします。

cd serve
npm i
cd ..

ローカルサーバーを起動します。

 ./serve/bin/serve -d dinosaur

サンプルファイルを少し変更してみる

サンプルファイルをひな形に、以下のようにしてみました。

// Copyright (c) 2018 8th Wall, Inc.

// Returns a pipeline module that initializes the threejs scene when the camera feed starts, and
// handles subsequent spawning of a glb model whenever the scene is tapped.
const placegroundScenePipelineModule = () => {
  const modelFile = ['models/d1.glb','models/d2.glb','models/d3.glb','models/d4.glb','models/d5.glb','models/tree.glb','models/tree2.glb']                                 // 3D model to spawn at tap
  const startScale = new THREE.Vector3(0.0001, 0.0001, 0.0001) // Initial scale value for our model
  const endScale = new THREE.Vector3(0.003, 0.003, 0.003)      // Ending scale value for our model
  const animationMillis = 750                                  // Animate over 0.75 seconds

  const raycaster = new THREE.Raycaster()
  const tapPosition = new THREE.Vector2()
  const loader = new THREE.GLTFLoader()  // This comes from GLTFLoader.js.

  let surface  // Transparent surface for raycasting for object placement.

  // Populates some object into an XR scene and sets the initial camera position. The scene and
  // camera come from xr3js, and are only available in the camera loop lifecycle onStart() or later.
  const initXrScene = ({ scene, camera }) => {
    console.log('initXrScene')
    surface = new THREE.Mesh(
      new THREE.PlaneGeometry( 100, 100, 1, 1 ),
      new THREE.MeshLambertMaterial({
        color: 0xffff00,
        transparent: true,
        opacity: 0.0,
        side: THREE.DoubleSide
      })
    )

    surface.rotateX(-Math.PI / 2)
    surface.position.set(0, 0, 0)
    scene.add(surface)

    const light = new THREE.AmbientLight( 0x404040, 5 )
    scene.add(light)  // Add soft white light to the scene.

    // 影を付けたいので平行光源をつけました
    const dLight = new THREE.DirectionalLight(0xffffff)
    scene.add(dLight)

    // Set the initial camera position relative to the scene we just laid out. This must be at a
    // height greater than y=0.
    camera.position.set(0, 3, 0)
  }

  const animateIn = (model, pointX, pointZ, yDegrees) => {
    console.log(`animateIn: ${pointX}, ${pointZ}, ${yDegrees}`)
    const scale = Object.assign({}, startScale)

    model.scene.rotation.set(0.0, yDegrees, 0.0)
    model.scene.position.set(pointX, 0.0, pointZ)
    model.scene.scale.set(scale.x, scale.y, scale.z)
    XR.Threejs.xrScene().scene.add(model.scene)

    new TWEEN.Tween(scale)
      .to(endScale, animationMillis)
      .easing(TWEEN.Easing.Elastic.Out) // Use an easing function to make the animation smooth.
      .onUpdate(() => { model.scene.scale.set(scale.x, scale.y, scale.z) })
      .start() // Start the tween immediately.
  }

  const getModelFile = () => {
    const modelFileLength = modelFile.length
    const n = Math.floor(Math.random() * modelFileLength)
    return modelFile[n]
  }

  // Load the glb model at the requested point on the surface.
  const placeObject = (pointX, pointZ) => {
    console.log(`placing at ${pointX}, ${pointZ}`)
    loader.load(
      getModelFile(),                                                              // resource URL.
      (gltf) => { animateIn(gltf, pointX, pointZ, Math.random() * 360) },     // loaded handler.
      (xhr) => {console.log(`${(xhr.loaded / xhr.total * 100 )}% loaded`)},   // progress handler.
      (error) => {console.log('An error happened')}                           // error handler.
    )
  }

  const placeObjectTouchHandler = (e) => {
    console.log('placeObjectTouchHandler')
    // Call XrController.recenter() when the canvas is tapped with two fingers. This resets the
    // AR camera to the position specified by XrController.updateCameraProjectionMatrix() above.
    if (e.touches.length == 2) {
      XR.XrController.recenter()
    }

    if (e.touches.length > 2) {
      return
    }

    // If the canvas is tapped with one finger and hits the "surface", spawn an object.
    const {scene, camera} = XR.Threejs.xrScene()

    // calculate tap position in normalized device coordinates (-1 to +1) for both components.
    tapPosition.x = (e.touches[0].clientX / window.innerWidth) * 2 - 1
    tapPosition.y = - (e.touches[0].clientY / window.innerHeight) * 2 + 1

    // Update the picking ray with the camera and tap position.
    raycaster.setFromCamera(tapPosition, camera)

    // Raycast against the "surface" object.
    const intersects = raycaster.intersectObject(surface)

    if (intersects.length == 1 && intersects[0].object == surface) {
      placeObject(intersects[0].point.x, intersects[0].point.z)
    }
  }

  return {
    // Pipeline modules need a name. It can be whatever you want but must be unique within your app.
    name: 'placeground',

    // onStart is called once when the camera feed begins. In this case, we need to wait for the
    // XR.Threejs scene to be ready before we can access it to add content. It was created in
    // XR.Threejs.pipelineModule()'s onStart method.
    onStart: ({canvas, canvasWidth, canvasHeight}) => {
      const {scene, camera} = XR.Threejs.xrScene()  // Get the 3js sceen from xr3js.

      initXrScene({ scene, camera }) // Add objects to the scene and set starting camera position.

      canvas.addEventListener('touchstart', placeObjectTouchHandler, true)  // Add touch listener.

      // Enable TWEEN animations.
      animate()
      function animate(time) {
        requestAnimationFrame(animate)
        TWEEN.update(time)
      }

      // Sync the xr controller's 6DoF position and camera paremeters with our scene.
      XR.XrController.updateCameraProjectionMatrix({
        origin: camera.position,
        facing: camera.quaternion,
      })
    },
  }
}

const onxrloaded = () => {
  XR.addCameraPipelineModules([  // Add camera pipeline modules.
    // Existing pipeline modules.
    XR.GlTextureRenderer.pipelineModule(),       // Draws the camera feed.
    XR.Threejs.pipelineModule(),                 // Creates a ThreeJS AR Scene.
    XR.XrController.pipelineModule(),            // Enables SLAM tracking.
    XRExtras.AlmostThere.pipelineModule(),       // Detects unsupported browsers and gives hints.
    XRExtras.FullWindowCanvas.pipelineModule(),  // Modifies the canvas to fill the window.
    XRExtras.Loading.pipelineModule(),           // Manages the loading screen on startup.
    XRExtras.RuntimeError.pipelineModule(),      // Shows an error image on runtime error.
    // Custom pipeline modules.
    placegroundScenePipelineModule(),
  ])

  // Open the camera and start running the camera run loop.
  XR.run({canvas: document.getElementById('camerafeed')})
}

// Show loading screen before the full XR library has been loaded.
const load = () => { XRExtras.Loading.showLoading({onxrloaded}) }
window.onload = () => { window.XRExtras ? load() : window.addEventListener('xrextrasloaded', load) }

変更したのは、並行光源を追加したのと、modelを複数ランダムに表示することくらいです。

出来上がりがこちらです。

無料枠だと、なかなか制限が多いので使いづらいですが、簡単にマーカーレスARを試すことができました。