2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-02-09

目次

概要
開発環境
位置情報の求め方
制作
実行・テスト
まとめ

製作

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

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

まず最初に

Scene の中のHierarchyにある Main CameraDelete します。
Hierarchy の上で右クリックし XR > ARSessionXR > XROrigin を置きます。

Main Camera を消す理由
ARではスマホなどのカメラで撮ったものを使うため XROrigin に入っている Main Camera を利用する。そのため元々あった Main Camera は消さなけらばならない。

新しい Scene を作るたびにこの処理は行ってください。

1つ目の考え方

 ARマーカーの場所をAnchorとして設定することでAnchorから( x, y, z )離れた位置にオブジェクトを生成することで案内できると考えました。

シーンの中に ARSessionXROrigine があるのを確認しプロジェクトに必要なコンポーネントやスクリプト、Assets の設定をしていきます。

1.Reference Image Library をAssetsに置く
画像トラッキングサブシステムを開始する際は、まず検索対象を認識させるため、参照画像ライブラリを提供する必要があります。

入れ方はProjectのAssetsの上で右クリック
Create > XR > Reference Image Library
を押しAssetsの中に Reference Image Library があれば大丈夫です。
スクリーンショット 2025-02-05 154915.png

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

スクリーンショット 2025-02-05 155338.png

2.AR Tracked Image ManagerXROrigine に入れる

入れ方はXROriginを選択した後Inspectorの下にある Add Component をクリックします。検索欄で検索すればすぐに見つけることができます。そしてARTrackedImageManager を選択すれば XROrigine にコンポーネントが入ったと思います。
次にARTrackedImageManagerの中にある Max Number Of Moving Images の数値を1に変更し、Serialized Library へ先ほど作った ReferenceImageLibrary をドラッグ&ドロップします。
スクリーンショット 2025-02-05 155504.png
こうなっていればOKです。

3.スクリプトの生成
画像の認識
オブジェクトのスポーン
オブジェクトのスポーン位置を( x, y, z )にする
上の三つの機能を持ったスクリプトを書いていきます。
スクリプトを書き終わったらスクリプトをドラッグ&ドロップで XROrigin にいれて配置するオブジェクトを objectToPlace の枠に入れます。
移動させたいだけoffset x, y, z の数値を変えればマーカーの上に設置したアンカーから( x, y, z )の位置に移動したオブジェクトがスポーンします。

その時のスクリプトがこちらです。

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
XROrigineがこうなっていればOKです。

2つ目の考え方

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

1.新しいSceneを作って先ほどのARTrackedImageManagerを設定します。

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

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

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

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

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

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

3つ目の考え方

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

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

ARAnchorManager

ARRaycastManager

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

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

スクリプトを書き終わったらスクリプトをドラッグ&ドロップで XROrigin にいれて配置するオブジェクト(アンカー)を anchorObject に、オブジェクト(ずれた位置に置く)を objectToPlace の枠に入れます。
次にARPlaneManagerARAnchorManagerARRaycastManagerをそれぞれ正しい位置にドラッグ&ドロップで入力する。
移動させたいだけoffset x, y, z の数値を変えればアンカーの位置から
( x, y, z )の位置に移動したオブジェクトがスポーンします。

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
こうなっていればOKです。

実行・テスト

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

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

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

まとめ

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

参考

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

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

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?