GeoAR.jsでAR地図アプリを作ってみた

**GeoAR.jsを用いたロケーションベースのwebARアプリ**を作ってみました．

<div align="center">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263889/efe804c2-3e38-babe-09f8-5ccffbf7e956.png" width="300">
</div>

この記事では主に以下の事項について紹介します．

- webARを開発するために必要な知識やライブラリ

- それらを用いたロケーションベースのwebARアプリの実装

- 今後のAR/MRをめぐる議論

# 1. はじめに

## 1-1. AR.jsとは
[AR.js](https://github.com/jeromeetienne/AR.js)はWebブラウザでARを実現するためのJSのライブラリです．オープンソースとして公開されていて，無料で使用することができます．AR.jsでARコンテンツを作る際には，主に2つの方法があります．**マーカーベース**と**ロケーションベース**です．

- マーカーベース
あらかじめ設定されたマーカーと呼ばれる2次元画像にカメラをかざすと，その位置を基準としてオブジェクトを描画する形式です．この場合，オブジェクトを表示したい場所に事前にマーカーを設置しておく必要があります．

- ロケーションベース
端末のGPS機能から取得した位置情報を基準にしています．したがって，マーカーを準備する必要がなく，地球上のある地点にピンポイントで絶対位置を指定することができます．

## 1-2. GeoAR.jsとは

ロケーションベースのwebARを開発するにあたり，今回は先ほどのAR.jsに含まれている**[GeoAR.js](https://github.com/nicolocarpignoli/GeoAR.js)**を使っていきます．GPSデータの取得から傾きの推定，それらをもとにしたオブジェクトとの距離導出までを一気に担ってくれます．これを使うと，以下のようなことが実現できます．

- カメラの位置情報と角度をもとにした現実世界におけるARコンテンツ表示

- ARコンテンツとのインタラクション

# 2. 作るもの

今回は位置情報に基づいたARということで，**自分が好きな場所にピンをぶっ刺してカスタマイズできるARマップ**を作りました．ボタンを押すとその場所にARでピンが建ちます．お気に入りのご飯屋さんを登録して，AR食べログみたいな使い方もできるかも．

下のURLにアクセスすると完成形が見られます．
https://te260ku.github.io/geoar/

<div align="center">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263889/5f4c8f97-5a11-428f-9ebe-6ddd57cc7c68.png" width="300">
</div>

入力エリアにスポットの名前を入力して＋ボタンを押すことでその地点を登録[^1]，-ボタンを押して全てのデータをリセットします．画面上部にデータの全数が表示されています．ピンがピンをタップするとそのスポットの名前を見られます．

## 2-1. 開発環境
- iOS13.2 (iPhone XR)
- Safari

## 2-2. 実装

ソースコード全文は以下で公開しています．
https://github.com/te260ku/geoar

HTMLとJSをさっと書くだけで作ることができます．GeoAR.jsの内部では[aframe](https://aframe.io/)を利用しているので，HTMLには以下のように特殊なタグが入ります．お店の位置を示すピンは[こちら](https://sketchfab.com/3d-models/lowpoly-pin-652c421271bf4a05a97ec80063f04919)のgltfモデルを使用しています．

- GeoAR.jsはaframeのカスタムコンポーネントとしてgps-cameraとgps-entity-placeをもっています．


- gps-cameraコンポーネントを追加するだけで，GPSデータの取得からオブジェクトとの距離導出までを自動で行ってくれます．楽ですね．

- gps-entity-placeはARオブジェクト用です．aframe-extraを読み込むとアニメーションの再生もできますが，今回はやりません．

```index.html
<body style='margin: 0; overflow: hidden;'>

    <div class="centered instructions">Click Pins</div>
    <div class="centered instructions mt-5" id="count-area">0</div>

    <a-scene cursor='rayOrigin: mouse; fuse: true; fuseTimeout: 0;' raycaster="objects: [gps-entity-place];"
        vr-mode-ui='enabled: false' embedded
        arjs='sourceType: webcam; sourceWidth:1280; sourceHeight:960; displayWidth: 1280; displayHeight: 960; debugUIEnabled: false;'>
        <!-- minDistance指定した方がいいかも -->
        <a-camera gps-camera rotation-reader></a-camera>
    </a-scene>

    <div class="centered">
        <input type="text" placeholder="Name of Spot" class="form-control" id="title">
        <div class="container">
            <div class="row">
                <div class="col-3"></div>
                <div class="col-3">
                    <button data-action="change" id="add-button"></button>
                </div>
                <div class="col-3">
                    <button data-action="change" id="reset-button"></button>
                </div>
                <div class="col-3"></div>
            </div>
        </div>
    </div>
    <script src="./script.js"></script>
</body>
</html>
```


- 位置情報の取得にはGeoLocation APIを使っています

- ピンを立てる度にその地点の緯度と経度を手打ちするのはあまりにもアホらしいので，ここはマップアプリらしくスポットの保存もできるようにしました．スポットの名称と緯度，経度を格納します．ただしデータの保存，取得にはLocalStorage APIを使っているのでブラウザ依存です．データベース何もわからん．

```script.js
var add = function(){
        var title = document.getElementById("title").value;
        var label = document.querySelector('.instructions');

        if( navigator.geolocation )
        {
            navigator.geolocation.getCurrentPosition(

                function( position )
                {
                    var location = position.coords ;
                    lat = location.latitude ;
                    lng = location.longitude ;
                    
                    saveData(title,lat,lng);
                    addData(title,lat,lng);
                },
                function( error )
                {
                    var errorInfo = [
                        "unexpected error" ,
                        "prihibited" ,
                        "signal blocked" ,
                        "timeout"
                    ] ;
                    var errorNo = error.code ;
                    var errorMessage = "[error : " + errorNo + "]\n" + errorInfo[ errorNo ];
                    
                    label.innerText = errorMessage;
                } ,
                {
                    // ここtrueにすると精度が上がる
                    "enableHighAccuracy": false,
                    "timeout": 8000,
                    "maximumAge": 30000,
                }
            ) ;
        }
        else
        {
            var errorMessage = "Don't Compatible with GeoLocation API" ;
            label.innerText = errorMessage;
        }
	}; 
```

- renderPlaces関数でモデルの描画を行なっています．最初に空のa-entityタグを生成して，それにsetModel関数内で要素を加えるようになっています．

- [公式では推奨されていませんが](https://medium.com/swlh/how-to-handle-click-events-on-ar-js-f397ea5994d)，ARオブジェクトに対して直接クリックイベントを指定します．こうすることで，画面に見えているピンをクリックすると，その地点の名称が表示されるようにしています．連打するか近づくかしないと反応してくれません．

> Tip to remember: You can also interact directly with 3D models or AR content in general. I don’t advise it because of bad reliability when click is not on the centre of the screen.


```script.js
function renderPlaces(places) {
    let scene = document.querySelector('a-scene');

    places.forEach((place) => {

        let latitude = place.lat;
        let longitude = place.lng;
        let title = place.title;
        // モデル用の空entityタグを生成
        let model = document.createElement('a-entity');
        // タグに緯度と経度を追加
        model.setAttribute('gps-entity-place', `latitude: ${latitude}; longitude: ${longitude};`);
        model.setAttribute('name', `${title}`);

        
        setModel(models[modelIndex], model);

        // オブジェクトに対するクリックイベント
        const clickListener = function (ev) {
            ev.stopPropagation();
            ev.preventDefault();

            let name = ev.target.getAttribute('name');

            const el = ev.detail.intersection && ev.detail.intersection.object.el;

            if (el && el === ev.target) {
                const label = document.querySelector('.instructions');
                label.innerText = name;
            }
        };
        model.addEventListener('click', clickListener);
        scene.appendChild(model);
    });
}
```

## 2-3. デプロイ
最後に，github pagesとかnetlifyとか何でもいいのでデプロイします．

試してみるとわかりますが，GeoAR.jsに組み込まれているユーザーとオブジェクトの距離を算出するアルゴリズムによって，目的地から遠すぎると自動的にオブジェクトが見えなくなり，近くにつれて大きく見えるようになっています．

# 3. ARクラウドで作られる理想郷
ARクラウドとは，**地形や建物など，現実世界に紐付くデータをもとに構成したクラウド上の3次元マップ情報**のことです[^2]．ARクラウドが満たす要件として以下のような項目があげられることが多いです．

- ポイントクラウドの保有
現実世界の3次元空間情報をスケーラブルな形で保有すること．これがあると，オクルージョン機能，例えば木の背後に隠れたポケモンは見えなくなる，などを実装できます．

- 複数デバイスによる点群の絶対位置把握
カメラの位置や向いてる方向を推定して，空間座標に存在するコンテンツをリアルタイムで表示する機能．

- リアルタイム共有とインタラクション
複数プレイヤーで同じものを見たり，AR世界のモデルとインタラクションする機能[^3].


今回GeoAR.jsで作ったwebARは，ユーザーの位置情報のみをもとにARオブジェクトを表示しています．また，スポットの保存は各ユーザーのブラウザごとに保存されるため，例えば自分が作ったマップを誰かと共有したりすることはできません．かろうじてライブラリに組み込まれている有難いアルゴリズムによる角度推定と，ARオブジェクトとの間接的（＝画面越し）なインタラクションができる程度であり，**ARクラウドが目指す理想とは離れている**と思っています．

<div align="center">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263889/1ea9932f-9bec-0044-2733-afcec90ae12d.png" width="700">
</div>

ARクラウドは現実世界に情報を加えたり，削除したりするためのデジタル空間が物理世界とは異なるレイヤーとして存在する考え方と言えます．それぞれのワールドはシームレスに接続，ときに融合しており，インタラクティブであることが想定されます．


# 4. おわりに

今回はAR.jsに含まれるGeoAR.jsをも用いたロケーションベースのwevARアプリを実装しました．ブラウザ上で動作するARアプリには欠点も多く，実用に足るクオリティに仕上がることは稀だと思います．しかし，ARクラウドをはじめとする次世代のAR/MR技術についての理解を深めるきっかけになるかと思いました．

[^1]: 位置情報の精度がガバガバなので思ったように見えないこともしばしば，バッグたらとりあえずリロード．

[^2]: ARクラウドの解説をしてくれている記事．https://blog.graffity.jp/n/nbad5b986005d

[^3]: ARクラウドのイメージ動画．電脳コイルの世界線に近いですね．https://www.youtube.com/watch?time_continue=33&v=_34TWnOuCxY&feature=emb_logo