Three.js x 8thwall でマーカーAR (Image Targets)

#概要
ブラウザ上でマーカーレスARができる[8th Wall](https://www.8thwall.com/)ですが、実際にイベントなんかで使うとなるとやっぱりマーカー、欲しくなりますよね。
そんなわたしたちに8th Wallは`Image Targets`という機能を提供してくれています！

### Image Targetsとは？
8th Wallでマーカーを検知＆トラッキングしてくれる機能です。
マーカーは開発者が独自に指定することができ、複数マーカー指定することもできます。

[公式サンプル](https://github.com/8thwall/web/tree/master/examples)の`Marker Based`あたりはImage Targets使っているようです。

# 準備
8th WallのAPIはサイトからキーなどを発行しないといけません。
ブラウザで[8thwall.com](https://www.8thwall.com/)を開き、webマークをクリック。
画面右上の`New Project`をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280075/4d5aaac4-aab6-d329-693d-e53b97fb39e9.png)

プロジェクト名を入力して`create`をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280075/962f510d-9a53-2928-003d-37e063b58136.png)

プロジェクトが作成されました。
左のサイドバーの歯車マークをクリック。
![img.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280075/3d8d25e1-4147-735e-8e72-9967b1c73b1a.png)

設定画面にAppKeyが表示されます。後で使います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280075/33114708-0171-15c5-2c61-67e6cac87592.png)

# いざ実装

## 画像の登録
先にマーカーを登録しておきましょう。
ダッシュボードの右側の`Manage Image Target`をクリック。
![img.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280075/d2269553-e75e-3297-c3f2-9e0d4e078f19.png)


`Upload Image`をクリックして任意の画像を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280075/7ba8bc0f-38c1-cb81-332e-2d64087f4f5d.png)

トリミングなどして`upload`をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280075/868716e9-48ab-efdb-e3d3-bbd1a790ea8c.png)

`Load automatically`にチェックを入れて`save`です。
metaデータも追加できるようですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280075/397b4ec4-3ace-0c90-04e5-f12e590b840e.png)

## 8th Wall APIの追加

続いてHTMLです。
scriptで以下の２つの追加が必要です。

appKeyには先程確認したAppKeyを貼り付けましょう。

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

全体です。

```html:index.html
<html lang="en">
<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">
	<link rel="stylesheet" href="./css/style.css">
	<title>title</title>
</head>
<body>
	<canvas id="canvas"></canvas>
	
	<!-- 8th Wall API -->
	<script defer src="https://apps.8thwall.com/xrweb?appKey=XXXXXXXXXX"></script>
	
	<!-- 8th Wall utilities -->
	<script src="https://cdn.8thwall.com/web/xrextras/xrextras.js"></script>

	<script src="./js/script.js"></script>
</body>
</html>
```


## PipeLineModuleの作成

8th Wallでは一つの処理を`PipeLineModule`というオブジェクト単位で行います。
`PipeLineModule`の最低限の形は以下のようになっています。

```typescript
PipeLineModule= {
	name: 'name', //PipeLineModuleの名前
	onStart: () => {}, //初期化時のコールバック
}
```

ImageTargetを使用する場合はこの**PipeLineModuleに画像を見つけたときのイベントリスナを登録しておきます！**

```typescript
PipeLineModule = {
	name: 'name',
	onStart: () => {},
	listeners: [
		{ event: 'reality.imagefound', process: () => {} }, //マーカーを見つけたとき
		{ event: 'reality.imageupdated', process: () => {} }, //マーカーが更新されたとき
		{ event: 'reality.imagelost', process: () => {} }, //マーカーを見失ったとき
	],
}
```

イベント発火時にリスナに渡される引数などは[公式ドキュメント](https://www.8thwall.com/docs/web/#aframe-events)を参照してみてください。(AFrameのやつだけど多分Three.jsでも同じ...?)

### まるごとソースコード

<details><summary>ソースコード</summary><div>

```typescript
import * as THREE from 'three';

declare var XR;
declare var XRExtras;

export class CustomPipelineModule{

	public xrModule: any;

	private scene: THREE.Scene;
	private camera: THREE.Camera;

	private obj: THREE.Object3D;

	constructor(){

		this.xrModule = {
			name: 'custom module',
			onStart: this.onStart.bind( this ),
			onUpdate: this.update.bind( this ),
			listeners: [
				{ event: 'reality.imagefound', process: this.showTarget.bind( this ) },
				{ event: 'reality.imageupdated', process: this.udpateTarget.bind( this ) },
				{ event: 'reality.imagelost', process: this.hideTarget.bind( this ) },
			],
		}

	}

	private onStart(){

		//XRからThree.jsのシーンやカメラを取得
		let {scene, camera} = XR.Threejs.xrScene();
				
		this.scene = scene;
		this.camera = camera;
		
		this.initScene();
		
		//カメラの位置と回転を8th wallに渡す
		XR.XrController.updateCameraProjectionMatrix({
			origin: this.camera.position,
			facing: this.camera.quaternion,
		})

	}

	private initScene(){

		//ライトの作成
		let light = new THREE.AmbientLight();
		this.scene.add( light );

		let dLight = new THREE.DirectionalLight();
		dLight.position.set( 5, 5, 5 );
		this.scene.add( dLight );

		//Boxの作成
		let geo = new THREE.BoxGeometry( 0.3, 0.3, 0.3 );
		let mat = new THREE.MeshStandardMaterial({ color: new THREE.Color( 0xdddddd )});
		let box = new THREE.Mesh( geo, mat );
		box.position.z = 0.15;

		this.obj = new THREE.Object3D();
		this.obj.add( box );
		this.obj.visible = false;

		this.scene.add( this.obj );

	}

	private update(){

	}

	private showTarget( args ){

		console.log( 'show' );
		
		//マーカーが表示されたらBoxも表示
		this.obj.visible = true;
		
		this.applyTransforms( args.detail );

	}

	private udpateTarget( args ){

		console.log( 'update' );

		this.applyTransforms( args.detail );

	}

	private hideTarget(){

		console.log( 'hide' );

		//マーカーを見失ったらBoxを非表示
		this.obj.visible = false;

	}

	//マーカーの位置と同期
	private applyTransforms( detail ){
		
		this.obj.position.copy( detail.position );
		this.obj.quaternion.copy( detail.rotation );
		this.obj.scale.set( detail.scale, detail.scale, detail.scale );

	}

}
```
</div></details>

## PipeLieModuleを動かす

作ったPipeLineModuleを`XR.addCameraPipelineModule`に登録します。
なんだかいっぱいpipeLineModuleが登録されていますが、とりあえずサンプルのコピペです＼(^o^)／

```typescript:main.ts
import { CustomPipelineModule } from './CustomPipelineModule';

XR.addCameraPipelineModules([
	XR.GlTextureRenderer.pipelineModule(),
	XR.Threejs.pipelineModule(),
	XR.XrController.pipelineModule(),
	XRExtras.AlmostThere.pipelineModule(),
	XRExtras.FullWindowCanvas.pipelineModule(),
	XRExtras.Loading.pipelineModule(),
	XRExtras.RuntimeError.pipelineModule(),
	this.customPipelineModule.xrModule, //ここだけ追加
])
```

最後に`XR.run`にcanvasを渡してRUNです！！

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

全体はこんなです

```typescript:main.ts
import * as THREE from 'three';
window.THREE = THREE;

import { CustomPipelineModule } from './CustomPipelineModule';

declare var XR;
declare var XRExtras;

class APP{

	private customPipelineModule: CustomPipelineModule;

	constructor() {

		this.customPipelineModule = new CustomPipelineModule();
		

        if( XR ){

			this.init();
			
        }else{

			window.addEventListener('xrloaded', this.init.bind(this));
			
        }
    }

    private 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.customPipelineModule.xrModule,
		])

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

}

window.addEventListener('load',()=>{

	let app = new APP();

});
```
ちなみに、**windowにTHREEを登録しておく必要がある**っぽいですね。

##完成
やった〜
<img width="375" alt="IMG_2455.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280075/9dc46389-ed55-7c8d-499a-68b53b1e8988.jpeg">