【Unity(C#)】ARFoundationのImageTrackingで複数の画像マーカーそれぞれに対応したARオブジェクトを出現させる

## はじめに
ARFoundationのImageTrackingを使えば登録した画像マーカーを認識することにより、
任意のARオブジェクトを出現させることができます。

【参考リンク】：[ARFoundationを触ってみた](https://gaprot.jp/2019/10/08/arfoundation/)

実は画像マーカーを複数登録することも可能なようです。
今回はそれについて調べたのでメモします。

内容としては"認識した画像マーカーそれぞれの上にARオブジェクトを出す"だけに留めます。
下記のような難しい実装の手法をこの記事で書くわけでないということ予め明示しておきます。

×複数の画像マーカーの位置関係を固定した、広域対応のARの手法。
【参考リンク】：[【GyroEye Holo】マーカー位置](https://youtu.be/NSzdqJssF-g)

## デモ

画像マーカー二枚のそれぞれに対応したARオブジェクトを表示しています。
あまり用途は無いかもですが、ロスト時に非表示にする実装も試してみました。

![MultiMarker.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/bcfce756-737b-efa5-d2f9-0078930a2fe2.gif)

## バージョン情報

| 諸々名前 | バージョン |
|:-:|:-:|
|  Unity | 2020.3.4f1 |
|  ARFoundation |  4.0.12 |
|  ARCore XR Plugin | 4.0.12  |
| ARKit XR Plugin  | 4.0.12  |
|  XR Plugin Management | 4.0.1  |

※Andoroidでしか試していませんが、ARFoundationなのでiosでも動くはず、、、です。

##下準備

バージョン情報に従って一通りビルドできるまでの環境構築が終わったら、下記画像を参考に`ReferenceImageLibrary`を作成します。

![ARMultiRibrary.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/d7629e6e-c86d-1554-51e5-0939aa65e685.png)

次に`ReferenceImageLibrary`を設定します。

![ImageMarker設定.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/d6249d8d-8bd6-25f4-9f11-96a122bfb3e4.png)

あとは`AR Session Origin`に`AR Tracked Image Manager`を設定し、
`Serialized Library`に先ほど作成した`ReferenceImageLibrary`を設定すれば準備完了です。

![Session Origin.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/482262f5-f6cb-2aee-1449-ef2e2f78c874.png)



## コード

```C#:適当なオブジェクトにアタッチ
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;

/// <summary>
/// 画像マーカー複数対応のサンプル
/// </summary>
public class MultiMarker : MonoBehaviour
{
    /// <summary>
    /// マーカー用オブジェクトのプレハブ
    /// </summary>
    [SerializeField] private GameObject[] _arPrefabs;

    /// <summary>
    /// ARTrackedImageManager
    /// </summary>
    [SerializeField] private ARTrackedImageManager _imageManager;

    /// <summary>
    /// マーカー用オブジェクトのプレハブと文字列を紐づけた辞書
    /// </summary>
    private readonly Dictionary<string, GameObject> _markerNameAndPrefabDictionary = new Dictionary<string, GameObject>();

    private void Start()
    {
        _imageManager.trackedImagesChanged += OnTrackedImagesChanged;

        //辞書を作る 画像の名前とARオブジェクトのPrefabを紐づける
        for (var i = 0; i < _arPrefabs.Length; i++)
        {
            var arPrefab = Instantiate(_arPrefabs[i]);
            _markerNameAndPrefabDictionary.Add(_imageManager.referenceLibrary[i].name, arPrefab);
            arPrefab.SetActive(false);
        }
    }

    private void OnDisable()
    {
        _imageManager.trackedImagesChanged -= OnTrackedImagesChanged;
    }

    /// <summary>
    /// 認識した画像マーカーに応じて紐づいたARオブジェクトを表示
    /// </summary>
    /// <param name="trackedImage">認識した画像マーカー</param>
    private void ActivateARObject(ARTrackedImage trackedImage)
    {
        //認識した画像マーカーの名前を使って辞書から任意のオブジェクトを引っ張り出す
        var arObject = _markerNameAndPrefabDictionary[trackedImage.referenceImage.name];
        var imageMarkerTransform = trackedImage.transform;
        
        //位置合わせ
        var markerFrontRotation = imageMarkerTransform.rotation * Quaternion.Euler(90f, 0f, 0f);
        arObject.transform.SetPositionAndRotation(imageMarkerTransform.transform.position, markerFrontRotation);
        arObject.transform.SetParent(imageMarkerTransform);

        //トラッキングの状態に応じてARオブジェクトの表示を切り替え
        arObject.SetActive(trackedImage.trackingState == TrackingState.Tracking);
    }

    /// <summary>
    /// TrackedImagesChanged時の処理
    /// </summary>
    /// <param name="eventArgs">検出イベントに関する引数</param>
    private void OnTrackedImagesChanged(ARTrackedImagesChangedEventArgs eventArgs)
    {
        foreach (var trackedImage in eventArgs.added)
        {
            ActivateARObject(trackedImage);
        }

        foreach (var trackedImage in eventArgs.updated)
        {
            ActivateARObject(trackedImage);
        }
    }
}
```

やっていることは下記です。

①"`ReferenceImageLibrary`に登録した画像の名前"と"表示したいARオブジェクトのPrefab"を辞書で紐づける。
②画像マーカー認識時に画像の名前に紐づけられたARオブジェクトを出現させる。

---

###ARTrackedImageManager.trackedImagesChanged
`trackedImagesChanged`に画像マーカー認識時のイベントハンドラーを設定できます。

イベントハンドラーの引数である`ARTrackedImagesChangedEventArgs`からは
下記3種類の情報をそれぞれのタイミングで得ることができます。

| Type | Name |Description|
|:-:|:-:|:-:|
|  List<ARTrackedImage> | added |The list of ARTrackedImages added since the last event.|
|  List<ARTrackedImage> |  updated |The list of ARTrackedImages updated since the last event. |
|  List<ARTrackedImage> | removed  |The list of ARTrackedImages removed since the last event. |

【引用元】：[Struct ARTrackedImagesChangedEventArgs](https://docs.unity3d.com/Packages/com.unity.xr.arfoundation@3.0/api/UnityEngine.XR.ARFoundation.ARTrackedImagesChangedEventArgs.html)

`added`と`updated`の使い道は理解できたのですが、`removed`がいまいち使い道がわかりませんでした。
`The list of ARTrackedImages removed since the last event.`という説明から、画像をロストした時？かと思い、
その前提で実装を組んでみましたが、何をやっても呼ばれずです。

使い道等知ってる方いたら教えてください。

---

###TrackingState

先ほど`画像をロストした時`について実装を試みたと書きましたが、
それについては`TrackingState`が有効でした。

ARTrackedImage(認識した画像)から三種類取得できます。

| TrackingState | Description |
|:-:|:-:|
| Limited| Some tracking information is available, but it is limited or of poor quality. |
| None|  Not tracking.|
| Tracking | Tracking is working normally.|


【引用元】: [Enum TrackingState](https://docs.unity3d.com/Packages/com.unity.xr.arsubsystems@3.0/api/UnityEngine.XR.ARSubsystems.TrackingState.html)

簡単に言うと`Limited`は認識精度が低い時、`None`は非認識時、`Tracking`は認識した時　といった感じです。

##おわりに
画像マーカーを動かすと、再認識とロストを繰り返してARオブジェクトがカクカクし始めるので、
平面に固定することを前提として使った方がいいかも　と色々と実験しながら思いました。

##参考リンク
[AR Foundation Improved Image Tracking - Multiple Objects/Images - Unity Augmented Reality/AR](https://www.youtube.com/watch?v=I9j3MD7gS5Y)