WebブラウザでマーカーレスAR (Three.js + 8th Wall)

#概要
WebGLでのARライブラリといえば[AR.js](https://github.com/jeromeetienne/AR.js/blob/master/README.md#browser-support)などがありますが、こちらは**マーカーが必要**で、あまり実用的ではありません。
そこでこちらの、[8th Wall](https://www.8thwall.com/)というサービスを使って、**マーカーレスARを**Three.jsで実装してみたいと思います。

こんなの作ります。
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">8thWallを使用した、Webブラウザで動くマーカーレスARです。<br><br>↓Qiitaにもまとめました。<a href="https://t.co/0JMgnpwYFg">https://t.co/0JMgnpwYFg</a><a href="https://twitter.com/hashtag/8thwall?src=hash&amp;ref_src=twsrc%5Etfw">#8thwall</a> <a href="https://twitter.com/hashtag/threejs?src=hash&amp;ref_src=twsrc%5Etfw">#threejs</a> <a href="https://twitter.com/hashtag/webgl?src=hash&amp;ref_src=twsrc%5Etfw">#webgl</a> <a href="https://t.co/zgHiGCbu3C">pic.twitter.com/zgHiGCbu3C</a></p>&mdash; 右近左近 (@UkskSan) <a href="https://twitter.com/UkskSan/status/1113823345655435265?ref_src=twsrc%5Etfw">2019年4月4日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


#8th Wallについて
8th Wallは**8th Wall SLAM(Simultaneous Localization and Mapping)**という独自の**6DoF** ARエンジンを使い、Webやアプリに**汎用的に使用できる**APIを提供しているようです。
[参考(公式ページ)](https://www.8thwall.com/faq.html)
また、8th Wallには以下の３つのモード(？)があるようです。

###8th Wall Web
ブラウザで動作するARのライブラリです。Three.js、A-Frame、AmazonSumerianに対応しているようです。
今回はこちらを使います。
###AR Camera
![image.png](https://qiita-image-store.s3.amazonaws.com/0/280075/95e5a281-da7b-0b56-e1fb-1dc13cfc3023.png)
**Webブラウザ上の**GUI上でぽちぽちやってARモデルを表示するページを作れるツールのようです。[ARkitのAR Quick Look](https://developer.apple.com/jp/arkit/gallery/)的な立ち位置でしょうか。
###8th Wall XR
**Unity用のライブラリ**らしいです。通常6DofのARは**ARkitかARCoreに対応しているデバイスのみ**ですが8th Wall XRを使うと、ARkitやARCoreに対応していないデバイスには**代わりにSLAMを使用**してカバーするようです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/280075/e39b5dc3-214f-4ef6-c1fd-8d7e480fd69d.png)
**強い**

##費用
個人向けには**1000view/月**以下の**ローカル開発**での使用までは**無料**のようです。
それ以上については公式ページを参照してください。

[8th Wall - Pricing](https://www.8thwall.com/pricing.html)
#作ってみよう
実装して行きます。
[公式サンプル](https://github.com/8thwall/web/blob/master/examples/threejs/placeground/index.js#L134)を参考にしました。

##実装の流れ

three.jsでの8th Wallは**Pipeline Module**というひとまとまりの処理を複数繋げてARを実装していくようです。Pipeline Moduleは**独自に定義する**こともでき、それを使って様々な処理を行わせるようです。

以下が実装の大まかな流れです。

1. APIキーの発行、8thWall APIを読み込ませる
2. タップした場所にモデルをロードするPipeline Moduleを定義
3. Pipeline Moduleをつなぎ、アプリを動作させる。

## 1.APIキーの発行・登録

アカウントを作成し、ログイン後、`Web Developer > Create a new Web App`を選択。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/280075/d6c0a3f1-efe7-34de-1414-f9c1db6d13de.png)

プロジェクト名を入力し、`Create`をクリック
![image.png](https://qiita-image-store.s3.amazonaws.com/0/280075/7ad59835-9125-c171-b579-ce08af999ee0.png)

APIキーが発行されます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/280075/a828d25c-8b6e-789a-ef7f-3bd0dfe59f9f.png)

Three.jsの場合、以下のXXXXを自分のAPIキーに書き換えます。

```html
<script defer src="https://apps.8thwall.com/xrweb?appKey=XXXXXXXXXX"></script>
```

ロード画面やエラー処理などのユーティリティ(?)を読み込ませます。

```html
<script src="//cdn.8thwall.com/web/xrextras/xrextras.js"></script>
```

HTML全体はこんな感じです。

```html
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>8thWall - Demo</title>
    <script src="./js/script.js"></script>
    <script src="//cdn.8thwall.com/web/xrextras/xrextras.js"></script>
    <script defer src="https://apps.8thwall.com/xrweb?appKey=XXXXXXXXXX"></script>
    <link rel="stylesheet" href="./css/style.css">
</head>
<body>
    <canvas id="canvas"></canvas>
</body>

</html>
```

##2.カスタムPipeLineModuleを作成
PipeLineModuleは**オブジェクト**で、`name`プロパティと実行開始時に呼ばれる`onStart`プロパティを最低限含んでいれば動くようです。

```javascript
let pipelineModule = {
    name: 'name',
    onStart: () => {
    }
}
```
onStartには**Canvasや回転の情報**などが送られてくるようです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/280075/a74c9cc0-e8f6-ebf0-360f-c98e14e0192e.png)
[引用(公式ドキュメント)](https://docs.8thwall.com/web/#onstart)

PipeLineMoudle全体は以下のようになりました。
サンプルでは関数の返り値をpipelineModuleとしていましたが、自分はクラスに持たせちゃいました。

```javascript
export default class putModelPipelineModule{
    constructor(){
        this.raycaster = new THREE.Raycaster();
        this.loader = new THREE.GLTFLoader();
        this.scene;
        this.camera;

        this.xrmodule = {
            name: 'putModel',
            onStart: ({canvas,canvasWidth,canvasHeight}) => {
                let {scene, camera} = XR.Threejs.xrScene();
                this.scene = scene;
                this.camera = camera;
                this.initScene();
                canvas.addEventListener('touchstart', this.onTouch.bind(this), true)
                XR.XrController.updateCameraProjectionMatrix({
                    origin: this.camera.position,
                    facing: this.camera.quaternion,
                })
            }
        }
    }

    initScene(){
        let groundGeo = new THREE.PlaneGeometry(100,100,10,10);
        let groundMat = new THREE.MeshBasicMaterial({
            transparent: true,
            opacity: 0.0,
            side: THREE.DoubleSide
        });
        this.ground = new THREE.Mesh(groundGeo,groundMat);
        this.ground.rotateX(-Math.PI / 2);
        this.ground.position.set(0,0,0);
        this.scene.add(this.ground);

        this.light = new THREE.DirectionalLight();
        this.light.position.set(0,10,0);
        this.scene.add(this.light);
        this.camera.position.set(0, 1, 0);

        this.alight = new THREE.AmbientLight();
        this.scene.add(this.alight);
    }

    onTouch(e){
        let tapPos = new THREE.Vector2((e.touches[0].clientX / window.innerWidth) * 2 - 1,- (e.touches[0].clientY / window.innerHeight) * 2 + 1);
        this.raycaster.setFromCamera(tapPos,this.camera);

        let intersects = this.raycaster.intersectObject(this.ground);
        if (intersects.length == 1) {
            let o = intersects[0];
            this.loader.load('assets/models/baku.glb',( gltf ) => {
                let model = gltf.scene;
                let baku = model.getObjectByName('baku_lowpoly');
                baku.position.set(o.point.x,o.point.y,o.point.z);
                this.scene.add(baku);
            });
        }

    }
}
```

基本的には通常のThree.jsと変わらずです。
Sceneは`XR.Threejs.xrScene()`から取得できるので、そこに好きなオブジェクトをおけば、AR表示してくれます。すごい。

##3.PipeLineModuleを組み合わせる

さっき作ったpipelineModuleを初期化します。

```javascript
this.pModelModule = new PutModelModule();
```
一覧のPipelineModuleを`XR.addCameraPipeLlineModules`を用いて登録します。
組み込みのPipelineModuleもいろいろあるようですが、よくわからないのでサンプル通りに設定しました。

```javascript
XR.addCameraPipelineModules([
    XR.GlTextureRenderer.pipelineModule(),
    XR.Threejs.pipelineModule(),
    XR.XrController.pipelineModule(),
    XRExtras.AlmostThere.pipelineModule(),
    XRExtras.FullWindowCanvas.pipelineModule(),
    XRExtras.Loading.pipelineModule(),
    XRExtras.RuntimeError.pipelineModule(),
    this.pModelModule.xrmodule, //カスタムpipelineModule
])
```

最後に、`XR.run`を実行します。描画用のcanvasを一緒に渡します。

```javascript
XR.run({
    canvas: document.getElementById('canvas')
})
```

全体はこんな感じです。

```javascript
import { BaseScene } from './utils/ore-three/';
import PutModelModule from './utils/putModelPipelineModule';

export default class MainScene extends BaseScene {
    constructor(renderer) {
        super(renderer);
        this.pModelModule = new PutModelModule();

        if(window.XR){
            this.init();
        }else{
            window.addEventListener('xrloaded', this.init.bind(this));
        }
    }

    init() {
        XR.addCameraPipelineModules([
            XR.GlTextureRenderer.pipelineModule(),
            XR.Threejs.pipelineModule(),
            XR.XrController.pipelineModule(),
            XRExtras.AlmostThere.pipelineModule(),
            XRExtras.FullWindowCanvas.pipelineModule(),
            XRExtras.Loading.pipelineModule(),
            XRExtras.RuntimeError.pipelineModule(),
            this.pModelModule.xrmodule,
        ])

        XR.run({
            canvas: document.getElementById('canvas')
        })
    }
}
```

#最後に
今回は8th Wallを使用して、マーカーレスARを実装してみました。
ARkitやARCoreを使わず、ブラウザでできてしまうのは未来を感じました...
ドキュメントを見る限りまだまだ機能がたくさんあるようなので、色々やってみたいですね！

[公式ドキュメント](https://docs.8thwall.com/web/)