nreal lightでハンドトラッキングしつつQRコードから３Dモデルを現出させる

ARグラスをてにいれたけれど３Dモデルを簡単に見る手段がない！
そんな悩みを解決するため、3Dモデル（GLTFのみ対応）のURLをQRコードにしてARグラスから見られるようなプログラムを作ってみましょう。

![qr.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189626/cfd2a049-4909-ca51-3f95-ee2c680e215f.gif)

## できるようになること

この記事はnreal light向けに以下を実装をする方法を紹介しています。
(内容もりだくさんなので駆け足で進む記事になっています。)

1. nreal lightハンドトラッキングを使った操作
3. nreal lightの視界に写っているQRコードから文字列の認識
4. 視界に写ったGLBファイルへのダウンロードリンクQRコードから３Dモデル表示

* 使う時の構成イメージ
    ![スクリーンショット 2022-12-07 11.22.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189626/e5e36a54-4e7a-b240-50b0-33a46578378e.png)

## 前提

* 対象読者
    * Unityの基礎を知っており、Android向けアプリのビルド・実行・デバッグができる人
* 必要なもの
    * nreal light
    * nreal light対応スマートフォン
    * Unity 2021系 (私はエディターバージョン2021.3.13f1を使いました。2021LTSなら多分動くと思います）
    * デバッグにつかう3Dモデル(GLTF,GLB形式）をダウンロードできるURLをQRコードにしたもの

## この記事が対象としないこと

* やりたいことの明確さに主眼をおき、チューニング等を捨てています。
  * 具体的なところで以下を気にしていません。
    * GCが走るきっかけとなるアロケーション周り
    * Update()による毎フレーム処理負荷への軽減・対策

## 公式のHandTrackingデモを試す

まずはNRSDKのサンプルとして付属するHandTrackingデモシーンを動かしてみましょう。
HandTrackingでどんなことができるかを試すことができます。
（詳しい解説は公式リファレンスを参照）

### 手順

1. nrealのWebサイト (https://developer.nreal.ai/) からSDKをダウンロードします。
   1. ページ上部のナビゲーション ”Downdload” からダウンロードします。
   2. 今回は Unity SDK 1.9.5 を使います。 (記事書いた直後に新バージョンのSDKが出てしまいそちらは試せていないです..)
   3. Unityで3Dの空プロジェクトを新規作成、Editorを起動します。
   4. メニューからFile/Build Settingsを開いてAndroidを選択、Switch Platformをクリックで切り替えます。
   6. Unityが起動したら事前ダウンロードしておいたNRSDKをUnityのProjectウィンドウの”Assets"へドラッグ＆ドロップします。
   7. Importウィンドウが表示されるのでImportボタンでインポートします。
   9. メニューからNRSDK -> Project Tips in Unity menuを開きます。
   10. プロジェクト設定の変更が必要な箇所のリストが表示されるので、確認しつつAccept Allします。
   11. ProjectウィンドウからAssets/NRSDK/DemosのなかのHandTrackingシーンを開きます。
   12. File/Build And Runからビルドを選択します。
      1. AndroidデバイスをPCに接続してデバッグできるようにしておきます。
      1. ビルド・実行すると端末でアプリが起動しますが、nrealアプリの場合このままだと動作しません。
      1. 一度アプリを終了したのち、nebulaアプリ経由でアプリ起動しましょう。
1. これでHandTrackingのデモシーンが動く筈です。

![hand.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189626/57c87588-0ad3-e50c-ea22-f9414eb280fb.png)

## 3Dモデル読込の準備

### 手順

1. 必要なパッケージの導入
    1. 今回はUniVRMに内包されているUniGLTFを使用することにします。
    1. Window/Package ManagerのAdd package from URL..から以下Packageをインストールします。
       1. https://github.com/vrm-c/UniVRM.git?path=/Assets/VRMShaders#v0.107.0
       2. https://github.com/vrm-c/UniVRM.git?path=/Assets/UniGLTF#v0.107.0

## QRコード読取～３Dモデル読込・表示の実装

### 準備

* https://github.com/micjahn/ZXing.Net/releases から [ZXing.Net.0.16.8.0.zip](https://github.com/micjahn/ZXing.Net/releases/download/v0.16.8.0/ZXing.Net.0.16.8.0.zip) をダウンロード、zipファイルを展開してzxing.unity.dllを取り出しておきます。

### 手順

HandTrackingのデモシーンを改造して作ります。

1. HandTrackingシーンを複製して開きます。これを今回の開発用シーンとします。
1. AssetsにPluginsフォルダを作成します。
2. ダウンロードしておいたzxing.unity.dllをPluginsフォルダにD&Dで登録します。
4. 以下２つのスクリプトを作成します。
4. ```C#
        using System.Collections;
        using NRKernal;
        using UnityEngine;
        using UnityEngine.Networking;
        using UnityEngine.UI;
        using ZXing;
        
        public sealed class QRCodeScanner : MonoBehaviour
        {
            [SerializeField]
            private Text _text;
            
            private BarcodeReader _barcodeReader;
            private NRRGBCamTexture _cameraTexture;
            private GltfLoader _gltfLoader;
        
            private string _scannedText;
        
            private void Start()
            {
                _barcodeReader = new BarcodeReader { AutoRotate = false };
                _gltfLoader = new ();
            }
            
            private void OnDestroy()
            {
                if (_cameraTexture != null)
                {
                    _cameraTexture.Stop();
                    _cameraTexture = null;
                }
        
                if (_gltfLoader != null)
                {
                    _gltfLoader.Dispose();
                    _gltfLoader = null;
                }
        
                _barcodeReader = null;
            }
        
            private void Update()
            {
                // 数フレームおきのQRコード検出を行う
                if (Time.frameCount % 5 != 0)
                {
                    return;
                }
        
                var rawImage = _cameraTexture.GetTexture().GetPixels32();
        
                var result = _barcodeReader.Decode(rawImage, _cameraTexture.Width, _cameraTexture.Height);
                if (result != null)
                {
                    _scannedText = $"{result.Text}";
                }
                else
                {
                    if (_scannedText == null)
                    {
                        _scannedText = "scanning..";
                    }
                }
        
                _text.text = _scannedText;
            }
        
            private void OnEnable()
            {
                _scannedText = null;
        
                if (_cameraTexture == null)
                {
                    _cameraTexture = new NRRGBCamTexture();
                }
        
                _cameraTexture.Play();
            }
        
            private void OnDisable()
            {
                _cameraTexture.Pause();
            }
        
            public void Load()
            {
                StartCoroutine(LoadProc());
            }
        
            public IEnumerator LoadProc()
            {
                var request = UnityWebRequest.Get(_scannedText);
                yield return request.Send();
                if (request.isNetworkError)
                {
                    Debug.Log(request.error);
                }
                else
                {
                    if (request.responseCode == 200)
                    {
                        var results = request.downloadHandler.data;
                        _gltfLoader.Load(results);
                    }
                }
            }
        }
   ```

6. Create/ScriptでGltfLoader.csを作成して以下スクリプトを書きます。
   1. ```c#
        using System;
        using System.Linq;
        using System.Threading.Tasks;
        using NRKernal;
        using UniGLTF;
        using UnityEngine;
        
        public sealed class GltfLoader : IDisposable
        {
            private GameObject _holderObject;
            private RuntimeGltfInstance _gltfInstance;
        
            public async Task Load(byte[] data)
            {
                UnsetModel();
        
                // Load new instance
                try
                {
                    _gltfInstance = await GltfUtility.LoadBytesAsync(null, data);
                    if (_gltfInstance == null)
                    {
                        Debug.LogWarning("LoadAsync: null");
                        return;
                    }
        
                    SetModel();
                }
                catch (Exception ex)
                {
                    Debug.LogException(ex);
                }
            }
        
            private void SetModel()
            {
                _holderObject = new GameObject();
                // 視界正面に設置
                _holderObject.transform.position = Camera.main.transform.position + Camera.main.transform.forward;
        
                // グラブで移動できるようにする
                var collider = _holderObject.AddComponent<SphereCollider>();
                collider.radius = 0.25f;
                var rigidbody = _holderObject.AddComponent<Rigidbody>();
                rigidbody.useGravity = false;
                _holderObject.AddComponent<NRGrabbableObject>();
        
                // GLTFモデルの姿勢をよしなに調整
                _gltfInstance.ShowMeshes();
                _gltfInstance.transform.localPosition = new Vector3(0, 0, 0);
                _gltfInstance.transform.localRotation = Quaternion.identity;
                _gltfInstance.transform.localScale = Vector3.one;
        
                var meshes = _gltfInstance.VisibleRenderers;
                var min = meshes.Select(x => x.bounds.min).Aggregate((lhs, rhs) => Vector3.Min(lhs, rhs));
                var max = meshes.Select(x => x.bounds.max).Aggregate((lhs, rhs) => Vector3.Max(lhs, rhs));
                var size = max - min;
                var scale = 0.5f / new[] { size.x, size.y, size.z }.Max();
                _gltfInstance.transform.localScale = new Vector3(scale, scale, scale);
        
                _gltfInstance.transform.SetParent(_holderObject.transform, false);
            }
        
            private void UnsetModel()
            {
                if (_gltfInstance)
                {
                    _gltfInstance.Dispose();
                    _gltfInstance = null;
                }
        
                if (_holderObject != null)
                {
                    GameObject.Destroy(_holderObject);
                    _holderObject = null;
                }
            }
        
            public void Dispose()
            {
                UnsetModel();
            }
        }
      ```

3. HierarchyのHandTrackingExample/ControllerPanel/CanvasにUI/Legacy/Buttonを追加します。
    3. ハンドトラッキングからでもクリックしやすいように大きめにリサイズしておきます。
7. 作成したボタンにQRCodeScanner ComponentをAddComponentで追加します。
8. InspectorからQRCodeScanner ComponentのTextに、Button配下のTextを設定します。
8. ビルド・インストールします。

### 使ってみる

任意のGLTFファイルをWebURLから参照できるようにして、URLをQRコードにして表示しておきます。
nebulaからアプリを起動して、表示しておいたQRコードのほうを見るとUIのボタンタイトルにURLが入ってきます。
そこでボタンをピンチ・クリックすると対象となるGLTFモデルが読み込まれ視界の正面に表示される筈です！

## まとめ

いかがだったでしょうか。
これである程度気軽に、ARグラスから３Dモデルを見られるようになったと思います。
持っているけれど活用していなかった３Dモデルがあれば、この機会にAR閲覧して眺めてみるのはいかがでしょう？
例えば外出した先のお店に貼ってあったおみやげ３DモデルQRコードから気軽に３Dモデルを持ち帰れる・鑑賞できるようになったりすると、コレクションするのが気軽になって、より楽しみやすいくなっていくかもしれませんね！

## 謝辞

この記事内容の開発をするにあたり下記のOSSを使わせていただきました。
広く便利な実装を提供されているかたがたに感謝です。

* https://github.com/vrm-c/UniVRM.git?path=/Assets/VRMShaders#v0.107.0
* https://github.com/vrm-c/UniVRM.git?path=/Assets/UniGLTF#v0.107.0
* https://github.com/micjahn/ZXing.Net/