【Immersal × Nreal】200m規模のAR空間を実装した話

## STYLYアドベントカレンダー
この記事は『[STYLYアドベントカレンダー](https://qiita.com/advent-calendar/2021/styly)』5日目の記事です。

##　はじめに
副業で下記イベントにXRエンジニアとして参加させて頂きました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">先日ご紹介したソラ水族館<br>イベント情報を公開しました！<br><br>ARグラス(nreal light）を装着し<br>商店街のアーケードを丸ごとAR水族館にする企画です<br><br>予約受付開始です、奮ってご参加ください！<br><br>11/6-7 神戸@三宮センター街<a href="https://t.co/C9JfVvhnh8">https://t.co/C9JfVvhnh8</a><a href="https://twitter.com/hashtag/078kobe?src=hash&amp;ref_src=twsrc%5Etfw">#078kobe</a> <a href="https://twitter.com/hashtag/ar?src=hash&amp;ref_src=twsrc%5Etfw">#ar</a> <a href="https://t.co/ZfM6vJ5loL">pic.twitter.com/ZfM6vJ5loL</a></p>&mdash; daisuketaki@U. (@YUUU__jp) <a href="https://twitter.com/YUUU__jp/status/1447855281505927171?ref_src=twsrc%5Etfw">October 12, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

非常に多くの知見を得たので、メモしようと思います。
発起人兼エンジニアの[daisuketaki](https://twitter.com/YUUU__jp)さん、エンジニアの[Araki](https://twitter.com/alucky7th)さん、[ryuichi](https://twitter.com/arukana0813)さんにも記事執筆の許可を頂きました。

## イベント詳細

ソラ水族館は[Nreal](https://www.nreal.ai/)と[Immersal](https://qiita.com/OKsaiyowa/items/925df2d48f0bcfd2b6b3#immersal)を用いた[マーカーレスのビジョンベースAR](https://relipasoft.com/blog/ar-locationbased/#AR-6)です。

![Logo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/fc5512a1-40c8-76c0-1479-87f025e653c4.png)


この企画のおもしろいところは体験エリアが広いことです。
**Nrealをかけたまま約200ｍ、商店街を歩きながらのAR体験**です。
マーカーレスのビジョンベースARでこの規模は事例として少ないと思います。

初回のミーティングでこの仕様を聞いたときに、一か月という短い実装期間も相まって、
正直なところ**この企画大丈夫か？**と不安になりました。

## 開発の流れ

作成の流れとしては下記です。
**1. Matterportで商店街の実寸3Dモデルを作成
2. [Immersal](https://qiita.com/OKsaiyowa/items/925df2d48f0bcfd2b6b3#immersal)で特徴点データを作成。
3. Unityエディター上で1で作成した3Dデータを元に2で作成した特徴点データの配置を行う。
4. ARオブジェクトを1で作成した3Dデータを元に配置する。
5. アプリとしての体裁を整える。(起動後～体験までの一連の挙動、インタラクション等)
6. 現地にて実際の動作を確認する。**

今回、私含め他のエンジニアの皆さんもフルリモートでの開発で、担当したのは4、5の工程でした。

開発に含まれてない重要な準備として、下記が挙げられます。
**・イベントスタッフ、会場の準備、機材の準備
・イベントのSNSでの告知、告知用動画の準備
・イベント開催元との連携
・当日起こり得るトラブルのリスクマネジメント**

正直ここに関して私はノータッチでしたが動作確認も含めて最もヘビーな内容だと思います。
エンジニアとしては開発に集中できる環境を用意していただいて、感謝しかありません。

開発期間が1か月に満たない状況もあり、週1回以下の短いペースでミーティングを重ね、
**初期仕様にこだわらず臨機応変に"体験の質と実現可能性"を天秤にかけてベストを探りながら**
進めていただきました。


## 開発Tips
開発期間に得た技術的知見、ノウハウをメモします。

前提として、画像のように200mを4分割して50mごとにエリアを定義してAR空間を構築しました。
![スクリーンショット 2021-10-17 17.10.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/2ff9f152-9a47-aa4c-ea17-0b1bc2a99b2f.png)

---
### ホットスポット方式

今回、Immersal利用時にホットスポット方式で特徴点データを収集しました。
この手法は[公式ドキュメント](https://immersal.gitbook.io/sdk/tutorials/how-to-map#ar-hotspot)に記載がある利用方法とは少し異なりますが、
**広域での連続的な特徴点データの撮影が難しい場合に有効**です。

下記はドキュメントの画像を拝借したものですが、ホットスポットを撮影後、
若干離れた場所から再度マッピングすることで遠距離でのローカライズ精度が向上するようです。
![ar_hotspots_mapping2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/e6cd3ecf-e028-b9ca-5060-f934690fdd17.jpeg)

【引用元】：[ドキュメント](https://immersal.gitbook.io/sdk/tutorials/how-to-map#ar-hotspot)

Nrealの6DofとImmersalの位置補正でできるだけARコンテンツのずれが起きないように努めました。
商店街の**200ｍの道のりを12箇所**程度マップ化したそうです。

**1マップにつき平均して15枚**ほどの撮影枚数でした。
「15枚は少なくないかな？」と思いましたが、
看板などユニークで特徴点の多いエリアが多かったことも影響してか、
生成されたGLBデータは、はっきりときれいなものでした。

なお、**最終的に使用したマップは1エリアにつき1マップのみ**で
他は予備としてアプリに内包したそうです。
というのも、**Nrealの自己位置推定が強力**で、
各エリア初回の位置合わせ以外ではほとんど使わなかったそうです。

加えて、**大量のマップを1つの画角に配置すると、
複数のローカライズの処理が干渉してしまい、逆効果**となったそうです。
あくまで理論値ですが、**50m程度なら歩いても大きく位置を外すことはなく、
Nrealの自己位置推定だけで十分**ということになります。(求める精度によります)

途中で大きく自己位置をロストしたり、
アプリが途中で落ちたりした場合の**予備として数箇所予備のマップを仕込んだ**とのことです。

---
### NRLocalizerの罠
NrealSDKとImmersalの組み合わせは、
ARFoundationとImmersalの組み合わせの際とLocalize(位置合わせ)を担うコンポーネントが異なります。

ARFoundationとImmersalの場合は`ARLocalizer.cs`というコンポーネントを利用します。

一方で、NrealとImmersalの組み合わせの場合、
**NRLocalizer.cs**というコンポーネントがLocalize(位置合わせ)を担います。

`ImmersalSDK.cs`というコンポーネントに`OnPoseFound`というイベントが定義されており、
ローカライズ時に任意の処理を走らせることができます。
![ImmersalSDK.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/0f8a968e-d4b5-5c64-41ec-4dc339136f51.png)

`ARMap.cs`というコンポーネントにも`OnFirstLocalization`という
そのマップの初回ローカライズ時に呼ばれるイベントがありますが、
複数のマップをシーンに配置した際に、どのマップがローカライズされるかは制御できないので
どのマップがローカライズされても呼び出される`OnPoseFound`を使うことにしました。

`OnPoseFound`を利用して、
初回の位置合わせをARコンテンツ起動のトリガーにしようと試みましたが、
うまくいきませんでした。

何がうまくいかなかったのかというと**そもそも呼ばれていなかった**?です。

ARFoundationとImmersalの組み合わせの際には
確実に呼ばれていることを確認していたので少々混乱しました。

そこで`NRLocalizer.cs`の中を調査してみると
`ImmersalSDK.cs`と同様に`OnPoseFound`が存在しました。
まさかと思い適当な処理を登録してみると
`ImmersalSDK.cs`の`OnPoseFound`と同様の挙動になりました。

ちょっと理不尽すぎたので公式Discordにて要望としてリクエストしました。

しかし、「こっちでは動いてますよ」という返答を頂いたので
**バージョンによる違いがあるのかも**しれません。

↓今回の開発環境

| 名前 | バージョン |
|:-:|:-:|
| Unity  | 2020.3.18f  |
|  NRSDK | 1.7.0 or 1.6.0 |
| Immersal SDK  | 1.14.1  |

---
### ハンドトラッキング

下記のようなものを作成しました。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Nrealでハンドパーティクル😆<br><br>グラスから直接見たものはもっと綺麗なのですが、こればっかりはどうしようもないので録画機能のアップデートに期待です🙃<a href="https://twitter.com/hashtag/Nreal?src=hash&amp;ref_src=twsrc%5Etfw">#Nreal</a><a href="https://twitter.com/hashtag/AugmentedReality?src=hash&amp;ref_src=twsrc%5Etfw">#AugmentedReality</a> <a href="https://twitter.com/hashtag/AR?src=hash&amp;ref_src=twsrc%5Etfw">#AR</a> <a href="https://t.co/kuzTGr2ilh">pic.twitter.com/kuzTGr2ilh</a></p>&mdash; KENTO⚽️XRエンジニア😎Shader100記事マラソン挑戦中45/100 (@okprogramming) <a href="https://twitter.com/okprogramming/status/1457321977207681027?ref_src=twsrc%5Etfw">November 7, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

実装コードは下記です。
**単純にパーティクルをオンオフするだけだと機械的な挙動になる**ので、
"オフにする＝アルファ値を徐々に小さくする"という表現でやわらかさを残しました。

```C#:適当なオブジェクトにアタッチ
using NRKernal;
using UnityEngine;

/// <summary>
/// 手にパーティクルを追従させる
/// 単純なオンオフだと見栄えが悪いのでアルファの切り替えで実装
/// </summary>
public class HandParticle : MonoBehaviour
{
    /// <summary>
    /// パーティクルを出現させる関節
    /// </summary>
    [SerializeField] private HandJointID jointID = HandJointID.Palm;
    
    /// <summary>
    /// 手から出るパーティクルのオブジェクト
    /// </summary>
    [SerializeField] private ParticleSystem  handParticle;

    /// <summary>
    /// 透過にかける時間
    /// </summary>
    [SerializeField,Range(0.5f,5f)] private float alphaChangeTime = 1.0f;
    
    /// <summary>
    /// 手のステート
    /// </summary>
    private HandState handState;

    /// <summary>
    /// パーティクルの色取得に必要なモジュール
    /// </summary>
    private ParticleSystem.MainModule main;

    /// <summary>
    /// 経過時間
    /// </summary>
    private float elapsedTime;
    
    /// <summary>
    /// 補間の割合
    /// </summary>
    private float rate;

    /// <summary>
    /// 反応させたいポーズとそれ以外
    /// </summary>
    private enum CustomHandState
    {
        Open,
        Other
    }

    /// <summary>
    /// 現在の手のステート
    /// </summary>
    private CustomHandState currentHandState;
    
    /// <summary>
    /// 前フレームの手のステート
    /// </summary>
    private CustomHandState prevHandState = CustomHandState.Other;

    void Start()
    {
        handState = NRInput.Hands.GetHandState(HandEnum.RightHand);
        main = handParticle.main;
    }
    
    void Update()
    {
        //ジェスチャー取得
        currentHandState = handState.currentGesture == HandGesture.OpenHand ? CustomHandState.Open : CustomHandState.Other;
        //前回とジェスチャーが異なる場合、
        if (currentHandState != prevHandState) rate = 0;
        
        //アルファの透過割合計算
        elapsedTime += Time.deltaTime;
        rate = Mathf.Clamp01(elapsedTime / alphaChangeTime);

        //手のジェスチャーに応じてアルファ変更
        var particleColor = Color.white;
        particleColor.a = currentHandState == CustomHandState.Open ? Mathf.Lerp(0, 1, rate) : Mathf.Lerp(1, 0, rate);

        //前回のジェスチャーとして保持
        prevHandState = currentHandState;
        
        //アルファ、位置補正
        main.startColor = particleColor;
        handParticle.gameObject.transform.position = handState.GetJointPose(jointID).position;
    }
}

```

追従する関節はInspectorでEnumの値を変更することで自由に設定可能です。
下記が認識できる関節です。

![ht1-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/d0ba5ed3-be32-b01e-d20f-23669b0bfbc0.png)


【引用元】：[Joint Orientation](https://nrealsdkdoc.readthedocs.io/en/v1.6.0/Docs/Unity_EN/Develop/Hand%20Tracking.html#joint-orientation)

---

### 屋外での利用
光学式のデバイスは屋外での利用が正直厳しいです。
私もそこはもう諦めるしかないだろうと思ってましたが、
[daisuketaki](https://twitter.com/YUUU__jp)さんが**サングラス in ARグラス**という、
とてつもないアイデアで本番に向けてDIYしていました。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">AR Cityソラ水族館間もなく開始！ <a href="https://t.co/itaCAqYD8q">pic.twitter.com/itaCAqYD8q</a></p>&mdash; daisuketaki@U. (@YUUU__jp) <a href="https://twitter.com/YUUU__jp/status/1456785709579137025?ref_src=twsrc%5Etfw">November 6, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


「理屈はわかるけどそれ本当にやるのかよ」という感じで驚きましたが、
**体感的に3割増し**ではっきりとARオブジェクトを視認できるようになったそうです。

---

### バッテリー残量、熱との闘い
`Xperia5 Ⅱ`というそれなりにハイスペックなAndroid端末をNrealに接続して実験したそうですが、
おおげさではなく**1分に1％ペースで端末の充電が減少**したそうです。
バッテリーの事情と当日の体験スケジュールを考慮して、
急速充電器やモバイルバッテリーを完備して望んだとのことです。

また、端末の放熱が著しく、冷えピタで応急措置を取るなどして工夫されていました。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">熱暴走を防止するために、機材に冷えピタとか貼ってみるという、内部の暴走。<a href="https://twitter.com/hashtag/078kobe?src=hash&amp;ref_src=twsrc%5Etfw">#078kobe</a> <a href="https://t.co/qH6fdJg36Q">pic.twitter.com/qH6fdJg36Q</a></p>&mdash; SAIちゃん (@Saimmm) <a href="https://twitter.com/Saimmm/status/1456832798971469830?ref_src=twsrc%5Etfw">November 6, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

---

### 世界観の構築

商店街を水族館にするという内容のARですが、
ただ魚を出すだけではなく**世界観を構築したことがよりよい体験に繋がった**と振り返って感じています。

構想の段階でアイデアには大筋の世界観が付与されていました。
DEPTH1 → DEPTH2 → DEPTH3 → DEPTH4　とエリアを進むにつれて
体験エリアの水深が深くなっていくというものです。

最深部では全ての生き物が光そのものに変化し、ARならでは未知の世界を作り出します。
また、水深に応じて生き物や"現実をオーバーレイしたARの表現"が変化します。

エリアごとに冒頭でボイスガイダンスを流し、世界観の説明を行いました。
(神戸電子専門学校声優学科の方にボイスの依頼をしたそうです。ご協力ありがとうございました。)

例として、下記がDEPTH4のガイダンスの文言です。
先述の"ハンドパーティクル"が出るエリアです。

**「最深部に到着しました。ここではあらゆる生命が光そのものになります。
　手のひらを目の前に出してください。きっとあなたも、例外ではありません。」**

世界観を壊さず、体験者に正しく情報を付与する素晴らしい文言だと思いました。

## 後日談

イベントは全体的にかなり好評だったそうで、私もそう聞いて嬉しい気持ちになりました。
良いフィードバックも悪いフィードバックもそれぞれありましたが、
今回は心を鬼にして悪いフィードバックに焦点を当てて振り返ります。

### 視野が狭い
かなりの多い意見でした。 
デバイスのせいだ！で片づけることも可能ですが、
せっかくなので何が問題だったのか表現側で解消する方法がなかったか考えました。

まず、視野に対する言及が多いエリアが2箇所あったとのことなので、
そのエリアでの表現ついて深堀してみます。

---

#### 現実をオーバーレイするAR表現

視野に対する言及が多かったエリアの特徴の1つ目はコースティクスです。
![コースティクス.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/d93c5e5d-c272-2187-29ee-a7f697b5b826.png)

水上から差し込む光をイメージしたものです。
現実の壁面にオーバーレイする形で画面いっぱいに配置しています。

しかし、これが**視野角の制限がある状態では見え方として良くなかった**です。
下記はイメージですが、**視野の境界がはっきりしてしまい、視野の狭さが強調されている**ように見えます。
![視野.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/172a8433-a41a-7e31-ebea-0beaa7fe8eff.png)

現実にオーバーレイしてARのエフェクトをかける際には、
下記のようなに**視野角の切れ目をぼかす**などの工夫が必要でした。
![ぼかし.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/49c041e4-e320-919b-1eab-d5e7e70c148c.png)

**世界観でカバーする**ことも可能だったかなと思います。

例えば、
「"視野角を定義した枠を作り、その中にしかAR空間は広がっていない"　窓から覗くAR水族館」
という世界観を定義した作品であれば、視野角に対する言及は減ったかもしれません。

---
#### 巨大なオブジェクト

視野に対する言及が多かったエリアの特徴の2つ目は巨大なクジラです。

巨大なクジラが上空を漂うエリアがあるのですが、
クジラの**迫力を重視するあまり、視野角の境界を強調**させてしまう結果となりました。
![クジラ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/c393d810-a524-629c-c4d8-eb5a2bcd052e.png)

この辺りの調整は非常に難しいです。
手元のオブジェクトとの大きさの対比やオクルージョンによる奥行き感などで
ギリギリまでサイズを絞る工夫が必要だったかもしれません。

---
### 位置同期
作成したARコンテンツは位置同期の実装は行っておらず、
ソロプレイが前提でした。

一方で今回のイベントは、複数人で時間ごとに体験グループを作成し、
グループで同じコンテンツを同時に体験するという運用でした。

その際に、「あそこにカメがいる！」などと
プレイヤー間でARに関するコミュニケーションを取っても
**位置を同期していないがために話がかみ合わない**場面があったそうです。

魚などの動きのあるオブジェクトはランダムに遊泳するプログラムを組んでいました。
加えてコンテンツ開始のタイミングも、各々のグラスでのローカライズが完了次第でした。
複数人で同じ体験を構築する際には、ランダム性や開始タイミングが
このような形で落とし穴になるのかと勉強になりました。

今回の事象を逆手に取れば、**ビジョンベースのARである以上、
ランダム性を排除して開始タイミングさえ同期すれば、位置同期が不要**となります。

短期間での実装が求められる場面では有効な手法かもしれません。


## おわりに
開発後、メンバーで振り返りを行いましたが、
**「比較的余裕持って開発を終えることができた」**と口をそろえて仰っていました。
その理由を全員で考えた結果、**本当に必要な実装だけに仕様をそぎ落として開発できたこと**が
最も大きい要因ではないかという結論に至りました。

冒頭でも下記のようにプロジェクトは進行したと書きましたが、本当にこれに尽きると思います。
軸だけを残し、無駄をそぎ落として洗練されていくような良いプロジェクトの進め方でした。
>開発期間が1か月に満たない状況もあり、週1回以下の短いペースでミーティングを重ね、
**初期仕様にこだわらず臨機応変に"体験の質と実現可能性"を天秤にかけてベストを探りながら**
進めていただきました。



最後になりますが、このような機会を頂けたことに感謝します。
[株式会社U.](https://yuuu.jp/)さん、[daisuketaki](https://twitter.com/YUUU__jp)さん、[Araki](https://twitter.com/alucky7th)さん、[ryuichi](https://twitter.com/arukana0813)さん、ありがとうございます。

今できるXRの可能性を最大限引き出したイベントになったと思います。
今後もXRでおもしろいことをやりたいなと思いました。

## 参考リンク
[ARグラスで商店街を水族館に / Tips](https://note.com/miruplace/n/nb28ef77f92a3)