🐈

【three.js】8thwall Lightship VPSをthree.jsで試してみる

2022/10/29に公開

はじめに

VPSとは「Visual Positioning System」の略であり、簡単に言うと現実のオブジェクトを利用したリッチなAR演出ができる技術です。
今年になってさまざまな会社が立て続けにVPSをリリースしていますが、2022年10月現在でブラウザで利用できるVPSは8thwallが提供するLightship VPSのみになります。

VPSについて、また8thwallが提供するVPSについての詳細は下記記事がわかりやすかったのでぜひご確認を!

https://www.moguravr.com/niantic-lightship-vps-3/
https://www.moguravr.com/ar-as-a-metaverse/

また、8thwall Lightshipの一連の実装手順については、丁寧にまとめてくださっている記事が下記にあります。
https://zenn.dev/iwaken71/articles/8thwall-lightship-vps-intro

ここまでほぼ他力本願ですが、次から本記事の本題です。

three.jsのサンプルが無い!

8thwallさんは結構親切で、コードサンプルも結構揃っているのですが、今日現在でA-FRAMEを利用したものしかなく、three.jsを利用するにはdocumentationを紐解かなければなりません。
そんな訳で、私がthree.jsで試してみましたのでコードを共有します。
ぜひ参考にしてください!

ソースコード

前提

  • LiDARでスキャンされたモデルを、現実のオブジェクトに重ねる単純なVPSです。
  • self hosting(8thwallのサーバーではなくローカル環境や独自サーバーで動かしたい時)です
  • three.js v142

code

index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>8thwall VPS three.js example</title>
    <script src="//unpkg.com/three@0.142.0/build/three.min.js"></script>
    <script src="//unpkg.com/three@0.142.0/examples/js/loaders/GLTFLoader.js"></script>
    <script src="//unpkg.com/three@0.142.0/examples/js/loaders/DRACOLoader.js"></script>
    <script src="//cdn.8thwall.com/web/xrextras/xrextras.js"></script>
    <script src="//cdn.8thwall.com/web/coaching-overlay/coaching-overlay.js"></script>
    <script src="//apps.8thwall.com/xrweb?appKey=xxxxxxxxx"></script>
</head>
<body>
    <canvas id="canvas"></canvas>
    <script>
        let scanModel = null;
        const rootGroup = new THREE.Group();
        rootGroup.visible = false;


        function modelLoad() {
            //scanしたモデルをロードする
            const dracoLoader = new THREE.DRACOLoader();
            dracoLoader.setDecoderPath('//unpkg.com/browse/three@0.142.0/examples/js/libs/draco/');
            dracoLoader.setDecoderConfig({ type: 'js' });
            const gltfLoader = new THREE.GLTFLoader();
            gltfLoader.setDRACOLoader(dracoLoader);

            gltfLoader.load("./model/scan.glb", (model) => {
                scanModel = model;
                rootGroup.add(scanModel.scene);
            });
        }
        modelLoad();

        window.XR8 ? onLoad() : window.addEventListener('xrloaded', onLoad);

        function onLoad() {
            XRExtras.Loading.showLoading();//8thwallが用意しているローディング画面を表示d
            XRLoaded();//と同時に8thwall起動の用意
        }

        function XRLoaded() {
            const vpsPipeline = VPSPipelineModule();

            XR8.XrController.configure({
                enableVps: true//VPSを有効にする
            });
            XR8.addCameraPipelineModules([
                // Add camera pipeline modules.

                XR8.GlTextureRenderer.pipelineModule(),      // Draws the camera feed.
                XR8.Threejs.pipelineModule(),                // Creates a ThreeJS AR Scene.
                XR8.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.

                VpsCoachingOverlay.pipelineModule(),// 体験の最初のチュートリアル的な表示
                vpsPipeline,// Custom pipeline modules.
            ]);

            XR8.run({canvas: document.getElementById("canvas")});
        }

        function VPSPipelineModule() {
            return {
                // Camera pipeline modules need a name. It can be whatever you want but must be
                // unique within your app.
                name: 'vps-example',

                // onStart is called once when the camera feed begins. In this case, we need to wait for the
                // XR8.Threejs scene to be ready before we can access it to add content. It was created in
                // XR8.Threejs.pipelineModule()'s onStart method.
                onStart: onXRCameraStart,

                // onUpdate is called once per camera loop prior to render. Any 3js geometry scene would
                // typically happen here.
                onUpdate: onXRCameraRender,

                // Listeners are called right after the processing stage that fired them. This guarantees that
                // updates can be applied at an appropriate synchronized point in the rendering cycle.
                listeners: [
                    {event: 'reality.projectwayspotfound', process: spotFound},//VPSスポットが検出された時
                    {event: 'reality.projectwayspotupdated', process: spotUpdate},//VPSスポットが検出中
                    {event: 'reality.projectwayspotlost', process: spotLost},//VPSスポットが検出できなくなった時
                ],
            }
        }

        function onXRCameraStart({canvas}) {
            //ARカメラスタート
            const {scene, camera} = XR8.Threejs.xrScene();// Get the 3js scene from XR
            initXrScene({scene, camera});//sceneとcamera渡して、three.js関連の記述をこの関数に

            // prevent scroll/pinch gestures on canvas
            canvas.addEventListener('touchmove', (event) => {
                event.preventDefault()
            })
            // Sync the xr controller's 6DoF position and camera paremeters with our scene.
            XR8.XrController.updateCameraProjectionMatrix(
                {origin: camera.position, facing: camera.quaternion}
            )
            // Recenter content when the canvas is tapped.
            canvas.addEventListener(
                'touchstart', (e) => {
                e.touches.length === 1 && XR8.XrController.recenter()
                }, true
            )
        }

        function onXRCameraRender() {
            //ARカメラ動作中の処理
            //今回のサンプルでは特に何もしない
            const {scene, camera, renderer, cameraTexture} = XR8.Threejs.xrScene();
        }

        function initXrScene({scene, camera}) {
            scene.add(rootGroup);

            camera.position.set(0, 3, 0);
        }

        function spotFound({detail}) {
            rootGroup.visible = true;
            rootGroup.position.copy(detail.position);
            rootGroup.quaternion.copy(detail.rotation);
        }

        function spotUpdate({detail}) {
            rootGroup.position.copy(detail.position);
            rootGroup.quaternion.copy(detail.rotation);
        }

        function spotLost() {
            rootGroup.visible = false;
        }
    </script>
</body>
</html>

実装したものがこちら。
https://twitter.com/tanabee_8/status/1586053053823668224

self hostingの場合は、localで開発するときもhttps環境が必要です。
また、8thwallの管理画面でドメイン登録を忘れないように!

(おまけ)試してみてわかった8thwall Lightship VPSの長所・短所

VPSの大きな特徴として、「位置情報+カメラからの画像情報」で対象のオブジェクトを検知しているところがあります。
これを理解することによって、8thwall Lightship VPSの長所と短所が見えてきます。

まず、8thwall Lightship VPSはスポットをLiDARでスキャンします。
例えばハチ公はこんな感じでスキャンされてますね。

逆に言うと、LiDARでスキャンできる範囲でしか登録できないということになります。
例えば、大きなビルをスポットにしたい、という演出には不向きでしょう。
公式も10m以上のカメラに収まらない範囲は避けた方がよいと言っています。

逆に、スケールの小さいスポットは8thwallに向いています。
しかも屋内であっても問題ありません。階数も問いません。
「とあるマンションのどっかの部屋の3Fにある置物」でも8thwallは威力を発揮すると思います。

役に、8thwallの短所がそのまま長所になっているのがGoogleのGeospatial APIです。
Googleの方はストリートビューの画像をリソースとしているので、大きい建物に強いです。
以下のように、渋谷の街並みをVPSしたい、という需要に向いています。

https://prtimes.jp/main/html/rd/p/000000178.000023281.html

ただし、Geospatial APIはブラウザでは使えません。。

まとめると以下になります。できること、できないことを頭に入れつつ、いいコンテンツを作れるようになりたいですね!

  • 8thwall Lightship VPSの長所
    • 屋内でも使える
    • 階数も問わない
    • 小規模のオブジェクトに対して精度が高い
  • 8thwall Lightship VPSの短所
    • LiDARでスキャンできないくらいの大きい建物、街並みには向いてない

Discussion