【後編】Unityを使ったARコンテンツの開発

# 目次
[概要](#概要)
[開発環境](#開発環境)
[位置情報の求め方](#位置情報の求め方)
[制作](#製作)
[実行・テスト](#実行テスト)
[まとめ](#まとめ)

# 製作
AR技術を使ったサービスの企画提案と必要最小限の機能を持ったプロダクト(Minimum Viable Product)という事で私は１つのARマーカーに対して１つの目的地を設定して案内するアプリを作ろうとしました。

今回屋内での位置合わせをするために３つの手法を考えました。

#### まず最初に
`Scene` の中のHierarchyにある `Main Camera` を `Delete` します。
`Hierarchy` の上で右クリックし `XR` > `ARSession` と `XR` > `XROrigin` を置きます。

:::note info
`Main Camera` を消す理由
ARではスマホなどのカメラで撮ったものを使うため `XROrigin` に入っている `Main Camera` を利用する。そのため元々あった `Main Camera` は消さなけらばならない。
:::
新しい `Scene` を作るたびにこの処理は行ってください。

### 1つ目の考え方
　ARマーカーの場所をAnchorとして設定することでAnchorから（ x, y, z ）離れた位置にオブジェクトを生成することで案内できると考えました。
 
 シーンの中に `ARSession` と `XROrigine` があるのを確認しプロジェクトに必要なコンポーネントやスクリプト、`Assets` の設定をしていきます。
 
１．**`Reference Image Library`** をAssetsに置く
画像トラッキングサブシステムを開始する際は、まず検索対象を認識させるため、参照画像ライブラリを提供する必要があります。

 入れ方はProjectのAssetsの上で右クリック
 `Create` > `XR` > `Reference Image Library`
 を押しAssetsの中に `Reference Image Library` があれば大丈夫です。
 ![スクリーンショット 2025-02-05 154915.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001685/59f04713-0d5b-22c5-beef-13006dcf04b6.png)

 次に `Assets` に認識させたい画像をプロジェクト内に置き `Reference Image Library` の `Add Image` をクリックし`None`となっているところにドラッグ＆ドロップで入れます。
 `Specify Size` にチェックを入れ `Physical Size` に現実の画像のサイズを入力します（単位はメートルなので注意）。

![スクリーンショット 2025-02-05 155338.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001685/8e9835b5-615e-7593-31a3-5ab8d98cc752.png)

 
２．**`AR Tracked Image Manager`** を `XROrigine` に入れる

https://docs.unity3d.com/ja/Packages/com.unity.xr.arfoundation@5.1/manual/features/image-tracking.html

 入れ方はXROriginを選択した後Inspectorの下にある `Add Component` をクリックします。検索欄で検索すればすぐに見つけることができます。そして`ARTrackedImageManager` を選択すれば `XROrigine` にコンポーネントが入ったと思います。
 次にARTrackedImageManagerの中にある `Max Number Of Moving Images` の数値を１に変更し、`Serialized Library` へ先ほど作った `ReferenceImageLibrary` をドラッグ＆ドロップします。
 ![スクリーンショット 2025-02-05 155504.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001685/5978633e-3b8a-cef7-6534-79cdafa0f21f.png)
こうなっていればOKです。
 
 ３．スクリプトの生成
・**画像の認識**
・**オブジェクトのスポーン**
・**オブジェクトのスポーン位置を（ x, y, z ）にする**
上の三つの機能を持ったスクリプトを書いていきます。
 スクリプトを書き終わったらスクリプトをドラッグ＆ドロップで `XROrigin` にいれて配置するオブジェクトを `objectToPlace` の枠に入れます。
移動させたいだけ`offset x, y, z` の数値を変えればマーカーの上に設置したアンカーから（ x, y, z ）の位置に移動したオブジェクトがスポーンします。
 
 その時のスクリプトがこちらです。
 
```ruby:ImageToAnchor.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;

public class ImageToAnchor : MonoBehaviour
{
    public GameObject objectToSpawn; // スポーンするオブジェクト
    public ARTrackedImageManager trackedImageManager; // ARTrackedImageManagerの参照

    // オフセット値
    [Header("OFSET")]
    [SerializeField] private Vector3 position = new Vector3(0, 0, 0); // 位置オフセット
    [SerializeField] private Vector3 rotation = new Vector3(0, 0, 0); // 回転オフセット

    [Header("Position Correction")]
    [SerializeField] private float positionSmoothSpeed = 5f; // 位置のスムージング速度
    [SerializeField] private float rotationSmoothSpeed = 5f; // 回転のスムージング速度

    // 生成されたオブジェクトを格納する辞書
    private Dictionary<string, GameObject> spawnedObjects = new Dictionary<string, GameObject>();

    void OnEnable()
    {
        // トラッキング画像の変化を監視
        trackedImageManager.trackedImagesChanged += OnTrackedImagesChanged;
    }

    void OnDisable()
    {
        // トラッキング画像の監視を停止
        trackedImageManager.trackedImagesChanged -= OnTrackedImagesChanged;
    }

    // トラッキング画像の変化があったときの処理
    private void OnTrackedImagesChanged(ARTrackedImagesChangedEventArgs eventArgs)
    {
        // 新しく追加された画像に対して処理
        foreach (var addedImage in eventArgs.added)
        {
            // 画像がトラッキングされている場合にオブジェクトをスポーン
            if (addedImage.trackingState == TrackingState.Tracking)
            {
                SpawnObject(addedImage);
                AnchorContent(addedImage, objectToSpawn);
            }
        }

        // 更新された画像に対して処理
        foreach (var updatedImage in eventArgs.updated)
        {
            // トラッキング状態がTrackingの場合オブジェクトの位置を更新またはスポーン
            if (updatedImage.trackingState == TrackingState.Tracking)
            {
                if (!spawnedObjects.ContainsKey(updatedImage.referenceImage.name))
                {
                    // オブジェクトがまだスポーンされていなければスポーン
                    SpawnObject(updatedImage);
                }
                else
                {
                    // 既存のオブジェクトの位置を更新
                    UpdateObjectPosition(updatedImage);
                }
            }
        }
    }

    // 新しいオブジェクトをスポーンする処理
    private void SpawnObject(ARTrackedImage trackedImage)
    {
        // すでにオブジェクトがスポーンされていない場合
        if (!spawnedObjects.ContainsKey(trackedImage.referenceImage.name))
        {
            if (trackedImage.trackingState == TrackingState.Tracking)
            {
                Vector3 spawnPosition = trackedImage.transform.position + position;
                Quaternion spawnRotation = trackedImage.transform.rotation * Quaternion.Euler(rotation);
                GameObject newObject = Instantiate(objectToSpawn, spawnPosition, spawnRotation);

                // オブジェクトを辞書に追加
                spawnedObjects.Add(trackedImage.referenceImage.name, newObject);
            }
        }
    }

    // 既存オブジェクトの位置を更新する処理
    private void UpdateObjectPosition(ARTrackedImage trackedImage)
    {
        // すでに生成されているオブジェクトがあれば位置を更新
        if (spawnedObjects.ContainsKey(trackedImage.referenceImage.name))
        {
            if (trackedImage.trackingState == TrackingState.Tracking)
            {
                Vector3 newPosition = trackedImage.transform.position + position;
                Quaternion newRotation = trackedImage.transform.rotation * Quaternion.Euler(rotation);

                // 位置の補正(スムージング)
                spawnedObjects[trackedImage.referenceImage.name].transform.position =
                    Vector3.Lerp(spawnedObjects[trackedImage.referenceImage.name].transform.position, newPosition, positionSmoothSpeed * Time.deltaTime);

                // 回転の補正(スムージング)
                spawnedObjects[trackedImage.referenceImage.name].transform.rotation =
                    Quaternion.Lerp(spawnedObjects[trackedImage.referenceImage.name].transform.rotation, newRotation, rotationSmoothSpeed * Time.deltaTime);

                // 微小な移動を防ぐための閾値設定
                if (Vector3.Distance(spawnedObjects[trackedImage.referenceImage.name].transform.position, newPosition) < 0.01f)
                {
                    spawnedObjects[trackedImage.referenceImage.name].transform.position = newPosition;
                }

                if (Quaternion.Angle(spawnedObjects[trackedImage.referenceImage.name].transform.rotation, newRotation) < 1f)
                {
                    spawnedObjects[trackedImage.referenceImage.name].transform.rotation = newRotation;
                }
            }
        }
    }

    // 画像の位置にアンカーを作成しオブジェクトを配置する処理
    void AnchorContent(ARTrackedImage trackedImage, GameObject prefab)
    {
        // 画像のトラッキング位置を基準にしてオブジェクトを配置
        Vector3 spawnPosition = trackedImage.transform.position + position;
        Quaternion spawnRotation = trackedImage.transform.rotation * Quaternion.Euler(rotation);

        // 画像の位置にアンカーを作成
        var instance = Instantiate(prefab, spawnPosition, spawnRotation);

        // ARAnchor コンポーネントを追加(存在しない場合のみ)
        ARAnchor anchor = instance.GetComponent<ARAnchor>();
        if (anchor == null)
        {
            anchor = instance.AddComponent<ARAnchor>();
        }

        // アンカーの位置を画像の位置に設定
        anchor.transform.position = trackedImage.transform.position;

        // オブジェクトの位置をアンカーの位置からオフセット
        instance.transform.position = anchor.transform.position + position;

        // アンカーが正しくオブジェクトに追従するようになる
        spawnedObjects[trackedImage.referenceImage.name] = instance;
    }
}
```
![スクリーンショット 2025-02-05 160521.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001685/1c27bc59-3ee3-61b8-7e8c-0c7d570c45f5.png)
XROrigineがこうなっていればOKです。


### 2つ目の考え方
 ARマーカーを設定しそこを原点として案内情報も含めた3Dマップを展開すれば動くのではないかと考えました。
こちらはARマーカーを読み取った時に中心を（ x, y, z ）ずらした3Dマップ（実際の空間と同じサイズで作ったオブジェクト）を配置するというものです。

１．新しいSceneを作って先ほどの**ARTrackedImageManager**を設定します。

２．３Dマップの制作
私はこちらを**Blender**を使って制作しました。
Blenderのインストールや操作はこちらを参考にしてください。

https://www.pc-koubou.jp/magazine/68319?srsltid=AfmBOoppZQZuyoTljI2AAZB4velODnnmfV6nrisWjxBkibQUThFrAGJp#section04

実際の寸法に沿ってオブジェクトを作るだけなのでそう難しくはありません。
作り終わったら左上の `ファイル` > `エクスポート` > `FBX(.fbx)` を選択し出力したモデルをUnityのAssetsに置きます。

３．スクリプトの生成
・**画像の認識**
・**オブジェクトのスポーン**
・**オブジェクトのスポーン位置を（ x, y, z ）にする**
上の三つの機能を持ったスクリプトを書いていきます。

スクリプトを書き終わったらスクリプトをドラッグ＆ドロップで `XROrigin` にいれて配置するオブジェクト（Blenderで作ったモデル）を `objectToPlace` の枠に入れます。
移動させたいだけoffset x, y, z の数値を変えればマーカーの位置から
（ x, y, z ）の位置に移動しつつオブジェクトがスポーンします。

```ruby:ImageObject.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.ARFoundation;

public class MarkerBasePlacement : MonoBehaviour
{
    public GameObject objectToPlace;  // 配置するオブジェクト
    [SerializeField] public float offsetX = 0.0f;     // x方向のオフセット
    [SerializeField] public float offsetY = 0.0f;     // y方向のオフセット
    [SerializeField] public float offsetZ = 0.0f;     // z方向のオフセット

    public ARTrackedImageManager _arTrackedImageManager;

    void Start()
    {
        // AR Tracked Image Managerを取得
        _arTrackedImageManager = GetComponent<ARTrackedImageManager>();
    }

    // マーカーが認識されたときに呼び出されるイベント
    void OnEnable()
    {
        _arTrackedImageManager.trackedImagesChanged += OnTrackedImagesChanged;
    }

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

    // マーカーの状態が変化したときに呼び出される
    private void OnTrackedImagesChanged(ARTrackedImagesChangedEventArgs eventArgs)
    {
        foreach (var addedImage in eventArgs.added)
        {
            // 新しいマーカーが認識された場合
            PlaceObject(addedImage);
        }
    }

    // マーカーの位置にオフセットを加えてオブジェクトを配置
    private void PlaceObject(ARTrackedImage trackedImage)
    {
        // マーカーの位置を取得
        Vector3 markerPosition = trackedImage.transform.position;

        // オフセットを適用してオブジェクトの位置を決定
        Vector3 targetPosition = markerPosition + new Vector3(offsetX, offsetY, offsetZ);

        // オブジェクトを配置
        if (objectToPlace != null)
        {
            objectToPlace.transform.position = targetPosition;
            objectToPlace.SetActive(true);  // オブジェクトを表示
        }
    }
}
```
![スクリーンショット 2025-02-05 161617.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001685/18db3b40-8378-2977-ea89-a6bff1aa94cc.png)

offsetの位置を調整して現実と同じように映れば成功です。



### 3つ目の考え方
 平面認識した平面上にタッチすることでアンカーを設置しそこから（ x, y, z ）離れた位置にオブジェクトを生成することで案内できるようなアプリを作れるのではと考えました。アンカーの位置がバラバラになる問題は、床（平面）にアンカーを置く際、アンカーの場所を特定の位置にすることで解決し、同じ位置に見えるようにしました。

１．新しい `Scene` を作って複数のコンポーネントを `XROrigin` に入れる。
**ARPlaneManager**

https://docs.unity3d.com/ja/Packages/com.unity.xr.arfoundation@5.1/manual/features/plane-detection.html

**ARAnchorManager**

https://docs.unity3d.com/ja/Packages/com.unity.xr.arfoundation@5.1/manual/features/anchors.html

**ARRaycastManager**

https://docs.unity3d.com/ja/Packages/com.unity.xr.arfoundation@5.1/manual/features/raycasts.html

 入れ方は最初の `ARTrackedImageManager` と同じです。

２．スクリプトの制作
・**平面を検知**
・**レイキャストでタッチした位置にアンカーを設置**
・**アンカーの管理**
・**オブジェクトをアンカーの子として設定**
・**アンカーからの（ x, y, z ）の位置になるようにスポーン**
これらを満たすようにスクリプトを書いていきます。

スクリプトを書き終わったらスクリプトをドラッグ＆ドロップで `XROrigin` にいれて配置するオブジェクト（アンカー）を `anchorObject` に、オブジェクト（ずれた位置に置く）を `objectToPlace` の枠に入れます。
次に`ARPlaneManager`、`ARAnchorManager`、`ARRaycastManager`をそれぞれ正しい位置にドラッグ＆ドロップで入力する。
移動させたいだけoffset x, y, z の数値を変えればアンカーの位置から
（ x, y, z ）の位置に移動したオブジェクトがスポーンします。
 
```ruby:RayCastToAnchor.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;

public class RaycastToAnchor : MonoBehaviour
{
    public GameObject anchorObject;  // アンカーとして使うオブジェクト
    public GameObject objectToPlace; // 配置するオブジェクト
    public ARRaycastManager raycastManager;
    public ARAnchorManager anchorManager;
    
    private ARAnchor currentAnchor = null;  // 現在配置されているアンカー
    private GameObject currentAnchorObject = null;  // 現在配置されているアンカーオブジェクト
    private GameObject placedObject = null;  // 配置したオブジェクト

    // オフセット(X, Y, Z)
    public Vector3 offset = new Vector3(0.0f, 0.0f, 1.0f); // Z軸方向に1mのオフセット
    // 回転
    public Vector3 rotation = new Vector3(0.0f, 0.0f, 0.0f);

    void Update()
    {
        // レイキャストで平面を検出
        List<ARRaycastHit> hits = new List<ARRaycastHit>();
        if (raycastManager.Raycast(new Vector2(Screen.width / 2, Screen.height / 2), hits, TrackableType.PlaneWithinPolygon))
        {
            // 最初にヒットした平面の位置を取得
            ARRaycastHit hit = hits[0];
            Vector3 hitPosition = hit.pose.position;
            Quaternion hitRotation = hit.pose.rotation;

            // **既存のアンカーとほぼ同じ位置なら更新しない**
            if (currentAnchor != null && Vector3.Distance(currentAnchor.transform.position, hitPosition) < 0.05f)
            {
                return; // ほぼ同じ位置なら処理しない
            }

            // **新しいアンカーを作成する前に古いアンカーを削除**
            if (currentAnchorObject != null)
            {
                Destroy(currentAnchorObject); // 既存のアンカーオブジェクトを削除
                currentAnchor = null;
            }

            // **新しいアンカーを作成**
            currentAnchorObject = Instantiate(anchorObject, hitPosition, hitRotation);
            currentAnchor = currentAnchorObject.AddComponent<ARAnchor>();

            // **オブジェクトの生成は最初の1回だけ行う**
            if (placedObject == null)
            {
                placedObject = Instantiate(objectToPlace);
            }

            // **オブジェクトの位置と回転を更新**
            placedObject.transform.position = currentAnchor.transform.position + offset;
            placedObject.transform.rotation = Quaternion.Euler(rotation);
            placedObject.transform.SetParent(currentAnchorObject.transform); // アンカーに固定
        }
    }
}

```
![スクリーンショット 2025-02-05 162143.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001685/3f53e20d-9d10-e912-25e2-c5228942e8e7.png)
こうなっていればOKです。

 
# 実行・テスト
考え方１の結果
アンカーコンポーネントをつけたオブジェクトの生成はできたもののARマーカーをカメラがトラッキングできなくなったときからアンカーで置いておいたオブジェクトがどんどんずれていってしまって失敗に終わりました。


考え方２の結果
ARシステムは一般的に上方向に対する安定した認識を持ちやすいため、下方向での位置合わせはシステムにとって難易度が高く、うまく表示されませんでした。具体的にはoffsetでずらした位置にオブジェクト（に見立てた３Dマップ）が表示されなかったり、オブジェクトが斜めに配置されてしまったりしました。ARマーカーを下にし、その上にオブジェクトを置きましたがそちらも回転してしまいうまくいくことはありませんでした。

考え方３の結果
平面検出を利用したアンカーの設置に変更したところ画像ではなく平面を認識しているためある程度うまくいきました。しかしアンカーから遠くに移動するとアンカーが移動してしまったり、アンカーがうまく固定されていてもオブジェクトの表示位置がずれるなどの問題点も出てきました。

# まとめ
今回「屋内で行きたい場所にARを使って案内するアプリ」を作っていきましたが `Anchor` の設置とそこから (x, y, z ) 離れた位置にオブジェクトを生成するという事だけで手いっぱいになってしまいました。自分で書いたスクリプトをChatGPTに添削してもらったりしてスクリプトの制作では余裕があったと思うのですが思うようにいかず悔しいです。ARコンテンツは今回初の制作だったので何か改善案やアドバイスがあれば教えてください。コメントお待ちしています。

# 参考
Unity を使ったAR開発に欠かせないARFoundationの解説

https://rightcode.co.jp/blogs/26261

ARFoundation Sample 使うことのできる機能が詰まっています

https://github.com/Unity-Technologies/arfoundation-samples