📸

8th Wall × Babylon.jsでイメージトラッキングする

2023/07/05に公開

この記事はBabylon.js ゆるほめLT会 vol.2で話す内容の一部です.
スライドもぜひ読んでください!
https://speakerdeck.com/sakutama_11/8th-wall-x-babylon-dot-jsdehavok-physics-babylon-dot-js-yuruhomelthui-vol-dot-2
https://babylonjs.connpass.com/event/285198/

【著者】 さくたまです. ARとドラムとNeRFが好きです.
https://twitter.com/sakutama_11

8th WallでBabylon.jsでもイメージトラッキングできます

8th WallはBabylon.jsへのサポートが薄く,イメージトラッキングのサンプルもBabylon.jsはないですが,しっかり動きます.
https://www.8thwall.com/8thwall/unitcube-babylonjs

方法

Babylon.jsテンプレートから8th Wallプロジェクトを作る

アカウントもろもろはあることを前提に書きます.
下記のBabylon.jsのサンプルプロジェクトにアクセスし,clone projectを押すだけで簡単に8th Wall上でBabylon.jsが動くプロジェクトが作れます.
https://www.8thwall.com/8thwall/unitcube-babylonjs

イメージターゲットを登録




左側のQRコードで検出されるかを確認できます.左上の名前を後で使うのでわかりやすくしておきます.

XR8の設定にイメージターゲットの使用を追加

先ほどの名前を入れます.

app.js
const onxrloaded = () => {
  XR8.XrController.configure({imageTargets: ['IwakenLab']})
  // Add camera pipeline modules.
  ...
}

scene.onXrImage〇〇Observableでイベントを検出

使えるObservableの種類は以下から確認できます!
https://www.8thwall.com/docs/ja/api/babylonjs/observables/

initXrSceneより後に以下のように書くことでイベントを検出できます

babylonjs-scene-init.js
scene.onXrImageFoundObservable.add((event) => {
    const { name, type, position, rotation, scale, scaledWidth, scaledHeight, height, radiusTop, radiusBottom, arcStartRadians, arcLengthRadians } = event
    // 処理
    console.log("found!")
})

オブジェクトに反映させてみる

eventからposition, rotation, scaleを取得し,そのままサンプルシーンのboxに適用してみます

babylonjs-scene-init.js
scene.onXrImageFoundObservable.add((event) => {
    const { name, type, position, rotation, scale, scaledWidth, scaledHeight, height, radiusTop, radiusBottom, arcStartRadians, arcLengthRadians } = event
    
    const mesh = scene.getMeshByName('box')
    mesh.position.set(position.x, position.y, position.z)
    mesh.rotationQuaternion = new BABYLON.Quaternion(rotation.x, rotation.y, rotation.z, rotation.w)
    mesh.scaling = new BABYLON.Vector3(scale * 0.3, scale * 0.3, scale * 0.3)
})

画像に追従しました! (影おかしいけど

終わりに

シーン全体の座標をイメージターゲットに合わせる方法が知りたい...!
全体をやっぱり動かしたいのですが,Babylon.jsのTransform周りが理解しきれておらず...

8th Wall Babylon.jsサンプルないなら作る!という気概でやっていきます〜

以下コード全文です

imageTargets:['IwakenLab'] の部分は登録したイメージターゲットの名前に変更してください

app.js
// Copyright (c) 2022 8th Wall, Inc.
//
// app.js is the main entry point for your 8th Wall app. Code here will execute after head.html
// is loaded, and before body.html is loaded.

import {startScene} from './babylonjs-scene-init'
import * as camerafeedHtml from './camerafeed.html'
import './index.css'

const onxrloaded = () => {
    XR8.XrController.configure({imageTargets: ['IwakenLab']})
  // Add camera pipeline modules.
  XR8.addCameraPipelineModules([
    window.LandingPage.pipelineModule(),     // Detects unsupported browsers and gives hints.
    XRExtras.Loading.pipelineModule(),       // Manages the loading screen on startup.
    XRExtras.RuntimeError.pipelineModule(),  // Shows an error image on runtime error.
  ])

  // Add a canvas to the document for our xr scene.
  document.body.insertAdjacentHTML('beforeend', camerafeedHtml)

  // Open the camera and start running the camera run loop.
  startScene(document.getElementById('camerafeed'))
}

window.XR8 ? onxrloaded() : window.addEventListener('xrloaded', onxrloaded)
babylonjs-scene-init.js
// Define an 8th Wall XR Camera Pipeline Module that adds a cube to a babylonjs scene on startup.

// Populates some object into an XR scene and sets the initial camera position.
const initXrScene = ({scene, camera}) => {
  // Light.
  const light = new BABYLON.DirectionalLight('light', new BABYLON.Vector3(-5, -10, 7), scene)
  light.intensity = 0.0

  // Cube.
  const box = BABYLON.MeshBuilder.CreateBox('box', {size: 1.0}, scene)
  box.material = new BABYLON.StandardMaterial('boxMaterial', scene)
  box.material.diffuseTexture = new BABYLON.Texture('https://cdn.8thwall.com/web/assets/cube-texture.png', scene)
  box.material.emissiveColor = new BABYLON.Color3.FromHexString('#AD50FF')
  box.position = new BABYLON.Vector3(0, 0.5, 2)

  // Shadow receiver.
  const ground = BABYLON.Mesh.CreatePlane('ground', 2000, scene)
  ground.rotation.x = Math.PI / 2
  ground.material = new BABYLON.ShadowOnlyMaterial('shadowOnly', scene)
  ground.receiveShadows = true
  ground.position.y = 0

  // Shadow generator.
  const shadowGenerator = new BABYLON.ShadowGenerator(512, light)
  shadowGenerator.addShadowCaster(box, true)
  shadowGenerator.useBlurExponentialShadowMap = true
  shadowGenerator.blurScale = 2
  shadowGenerator.setDarkness(0.33)

  // 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 = new BABYLON.Vector3(0, 2, -2)
}

const recenterTouchHandler = (e) => {
  // Call XrController.recenter() when the canvas is tapped with one finger.
  // This resets the AR camera to the position specified by
  // XrController.updateCameraProjectionMatrix() above.
  if (e.touches.length === 1) {
    XR8.XrController.recenter()
  }
}

export const startScene = (canvas) => {
  const engine = new BABYLON.Engine(canvas, true /* antialias */)
  const scene = new BABYLON.Scene(engine)
  const camera = new BABYLON.FreeCamera('camera', new BABYLON.Vector3(0, 0, 0), scene)

  initXrScene({scene, camera})

  // Connect camera to XR and show camera feed
  camera.addBehavior(XR8.Babylonjs.xrCameraBehavior(), true)

  scene.onXrImageFoundObservable.add((event) => {
    const {name, type, position, rotation, scale, scaledWidth, scaledHeight, height, radiusTop, radiusBottom, arcStartRadians, arcLengthRadians} = event

    const mesh = scene.getMeshByName('box')

    mesh.position.set(position.x, position.y, position.z)
    mesh.rotationQuaternion = new BABYLON.Quaternion(rotation.x, rotation.y, rotation.z, rotation.w)
    mesh.scaling = new BABYLON.Vector3(scale * 0.3, scale * 0.3, scale * 0.3)
  })

  canvas.addEventListener('touchstart', recenterTouchHandler, true)  // Add touch listener.
  engine.runRenderLoop(() => scene.render())

  window.addEventListener('resize', () => engine.resize())
}

Discussion