🤍

【Unity2022】AR FoundationのImage Trackingでマーカー画像ごとに別のARオブジェクトを表示する方法

2023/07/15に公開

TL;DR

  • Unity AR FoundationのImage Trackingで複数のマーカー画像を使い、それぞれ別のARオブジェクトを表示するよ
  • XRReferenceImageLibrary(マーカー画像を登録するところ)の名前を特殊なもの(「0」「1」みたいな数字など)にしなくていいよ
  • マーカーごとのARオブジェクトを登録しやすくなるEditorクラスも作るよ

環境🌏

  • Unity 2022.3.4f1
  • AR Foundation 5.0.6

動作🔍

マーカー画像はAugmented Reality Marker Generatorというサイトで生成しました。
見た目が似ていて申し訳ない...🙇‍♂️

https://youtu.be/byZZHDe7wdY

ソースコード📄

最初に書いたコードを載せておきます。後程それぞれに関して説明をしていきます。

長いのでトグルにしておきます
ImageTrackingObjectManager.cs
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;
#if UNITY_EDITOR
using UnityEditor;
#endif

[Serializable]
public class NameToPrefab
{
    public string name;
    public GameObject prefab;
}

public class ImageTrackingObjectManager : MonoBehaviour
{
    [HideInInspector]
    public ARTrackedImageManager arTrackedImageManager;

    [HideInInspector, SerializeField]
    public List<NameToPrefab> markerNameToPrefab = new List<NameToPrefab>();

    void OnEnable()
    {
        arTrackedImageManager.trackedImagesChanged += OnTrackedImagesChanged;
    }

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

    void OnTrackedImagesChanged(ARTrackedImagesChangedEventArgs eventArgs)
    {
        foreach (var trackedImage in eventArgs.added)
        {
            var name = trackedImage.referenceImage.name;
            var prefab = markerNameToPrefab.Find(x => x.name == name)?.prefab;
            if (prefab != null)
            {
                var instance = Instantiate(
                    prefab,
                    trackedImage.transform.position,
                    trackedImage.transform.rotation,
                    trackedImage.transform
                );
            }
        }
        foreach (var trackedImage in eventArgs.updated)
        {
            if (trackedImage.trackingState == TrackingState.Tracking)
            {
                trackedImage.gameObject.SetActive(true);
            }
            else if (trackedImage.trackingState == TrackingState.Limited)
            {
                trackedImage.gameObject.SetActive(false);
            }
        }
        foreach (var trackedImage in eventArgs.removed)
        {
            trackedImage.gameObject.SetActive(false);
        }
    }

    bool HasNameInReferenceLibrary(IReferenceImageLibrary library, string name)
    {
        for (int i = 0; i < library.count; i++)
        {
            if (library[i].name == name)
            {
                return true;
            }
        }
        return false;
    }

    public void UpdateNameToPrefabMappings()
    {
        if (arTrackedImageManager == null || arTrackedImageManager.referenceLibrary == null)
        {
            return;
        }
        foreach (var pair in markerNameToPrefab)
        {
            if (!HasNameInReferenceLibrary(arTrackedImageManager.referenceLibrary, pair.name))
            {
                markerNameToPrefab.Remove(pair);
            }
        }
        for (int i = 0; i < arTrackedImageManager.referenceLibrary.count; i++)
        {
            var name = arTrackedImageManager.referenceLibrary[i].name;
            if (!markerNameToPrefab.Exists(x => x.name == name))
            {
                markerNameToPrefab.Add(new NameToPrefab { name = name });
            }
        }
    }
}

#if UNITY_EDITOR
[CustomEditor(typeof(ImageTrackingObjectManager))]
public class ImageTrackingObjectManagerEditor : Editor
{
    bool showNameToPrefabMappings = true;

    public override void OnInspectorGUI()
    {
        DrawDefaultInspector();

        var manager = (ImageTrackingObjectManager)target;
        ARTrackedImageManager newManager = (ARTrackedImageManager)
            EditorGUILayout.ObjectField(
                "AR Tracked Image Manager",
                manager.arTrackedImageManager,
                typeof(ARTrackedImageManager),
                true
            );
        if (newManager != manager.arTrackedImageManager)
        {
            manager.arTrackedImageManager = newManager;
        }
        if (manager.arTrackedImageManager == null)
        {
            EditorGUILayout.HelpBox("Tracked Image Manager is required.", MessageType.Error);
        }
        else
        {
            manager.UpdateNameToPrefabMappings();
            if (manager.markerNameToPrefab.Count == 0)
            {
                EditorGUILayout.HelpBox(
                    "There are no reference images in the Reference Image Library.",
                    MessageType.Warning
                );
            }
            else
            {
                showNameToPrefabMappings = EditorGUILayout.Foldout(
                    showNameToPrefabMappings,
                    new GUIContent("Marker To Prefab", "The mapping from marker name to prefab."),
                    true
                );
                if (showNameToPrefabMappings)
                {
                    foreach (var pair in manager.markerNameToPrefab)
                    {
                        EditorGUILayout.BeginHorizontal();
                        EditorGUILayout.Space();
                        EditorGUILayout.LabelField(pair.name);
                        var newPrefab = (GameObject)
                            EditorGUILayout.ObjectField(pair.prefab, typeof(GameObject), true);
                        if (newPrefab != pair.prefab)
                        {
                            pair.prefab = newPrefab;
                            EditorUtility.SetDirty(manager);
                        }
                        EditorGUILayout.EndHorizontal();
                    }
                }
            }
        }
    }
}
#endif

解説💡

基本設計

Unity AR Foundationではマーカー画像を検出した際に、ARTrackedImageManagerTrackedImagePrefabに登録したGameObjectを作成します。
ただしこれはそのマーカーに紐づいたコンテンツの為のものではありません。

ARTrackedImageManagerのInspector

そのため、マーカー検出時にそれぞれに紐づいたPrefabを追加する必要があります。
そこで検出されたマーカーの名前とPrefabを一対一でマッピングすることで、検出時に適切なPrefabを呼び出すことにします。

検出時の動き

以下はマーカー検出時にPrefabを追加する処理です。
マーカー更新時にTrackingState.Limitedであった場合と検出がなくなったときにPrefabを親ごと非表示にしていますが意図せず消えてしまうことが多ければ消しても大丈夫です。

ImageTrackingObjectManager.cs
void OnEnable()
{
    // イベントを登録
    arTrackedImageManager.trackedImagesChanged += OnTrackedImagesChanged;
}

void OnDisable()
{
    // イベントを解除
    arTrackedImageManager.trackedImagesChanged -= OnTrackedImagesChanged;
}

void OnTrackedImagesChanged(ARTrackedImagesChangedEventArgs eventArgs)
{
    // 新規に検出されたマーカー
    foreach (var trackedImage in eventArgs.added)
    {
        var name = trackedImage.referenceImage.name;
        var prefab = markerNameToPrefab.Find(x => x.name == name)?.prefab;
        if (prefab != null)
        {
            var instance = Instantiate(
                prefab,
                trackedImage.transform.position,
                trackedImage.transform.rotation,
                trackedImage.transform
            );
        }
    }
    // 既に検出されたマーカーで情報に更新があったもの
    foreach (var trackedImage in eventArgs.updated)
    {
        // 検出中のものを表示
        if (trackedImage.trackingState == TrackingState.Tracking)
        {
            trackedImage.gameObject.SetActive(true);
        }
	// 検出が途切れた(不安定)なものを非表示
        else if (trackedImage.trackingState == TrackingState.Limited)
        {
            trackedImage.gameObject.SetActive(false);
        }
    }
    // 検出されなくなったマーカー(動作不安定?)
    foreach (var trackedImage in eventArgs.removed)
    {
        trackedImage.gameObject.SetActive(false);
    }
}

マーカーとPrefabの紐づけ

Editor上でARTrackedImageManagerを設定した際に、自動で全マーカー分のPrefab設定欄が出てくるようにEditor拡張を制作しました。細かい説明はコードの該当部分にコメントしてありますので参照してください。

ImageTrackingObjectManager.cs
// Unityはシーンの保存・ロード時にデータをシリアル化するので
// マーカー名とPrefabの紐づけはシリアル化可能なデータ構造を使います
// ※Dictionaryを使っていないのはシリアル化できないから!
[Serializable]
public class NameToPrefab
{
    public string name;
    public GameObject prefab;
}

public class ImageTrackingObjectManager : MonoBehaviour
{
    [HideInInspector]
    public ARTrackedImageManager arTrackedImageManager;

    [HideInInspector]
    public List<NameToPrefab> markerNameToPrefab = new List<NameToPrefab>();

    /*
     ~~~~~ (中略) ~~~~~
     */
    
    // XRReferenceLibraryに該当の名前のマーカーがあるか判定します
    bool HasNameInReferenceLibrary(IReferenceImageLibrary library, string name)
    {
        for (int i = 0; i < library.count; i++)
        {
            if (library[i].name == name)
            {
                return true;
            }
        }
        return false;
    }

    // マーカーとPrefabの紐づけをしている markerNameToPrefab に
    // XRReferenceLibraryにあるマーカーがなければ追加
    // XRReferenceLibraryにないマーカーがあれば削除
    public void UpdateNameToPrefabMappings()
    {
        if (arTrackedImageManager == null || arTrackedImageManager.referenceLibrary == null)
        {
            return;
        }
        foreach (var pair in markerNameToPrefab)
        {
            if (!HasNameInReferenceLibrary(arTrackedImageManager.referenceLibrary, pair.name))
            {
                markerNameToPrefab.Remove(pair);
            }
        }
        for (int i = 0; i < arTrackedImageManager.referenceLibrary.count; i++)
        {
            var name = arTrackedImageManager.referenceLibrary[i].name;
            if (!markerNameToPrefab.Exists(x => x.name == name))
            {
                markerNameToPrefab.Add(new NameToPrefab { name = name });
            }
        }
    }
}

// Editor用なのでつけてます
#if UNITY_EDITOR
[CustomEditor(typeof(ImageTrackingObjectManager))]
public class ImageTrackingObjectManagerEditor : Editor
{
    bool showNameToPrefabMappings = true;
    
    // ImageTrackingObjectManagerコンポーネントをInspector上で描画する関数をoverrideします
    public override void OnInspectorGUI()
    {
        // 通常のUIを描画(Scriptの参照欄用)
        DrawDefaultInspector();

        var manager = (ImageTrackingObjectManager)target;
	// ARTrackedImageManagerコンポーネントの設定欄を作成
        ARTrackedImageManager newManager = (ARTrackedImageManager)
            EditorGUILayout.ObjectField(
                "AR Tracked Image Manager",
                manager.arTrackedImageManager,
                typeof(ARTrackedImageManager),
                true
            );
        if (newManager != manager.arTrackedImageManager)
        {
            manager.arTrackedImageManager = newManager;
        }
        if (manager.arTrackedImageManager == null)
        {
	    // ARTrackedImageManagerがなければ警告文を表示
            EditorGUILayout.HelpBox("Tracked Image Manager is required.", MessageType.Error);
        }
        else
        {
	    // markerNameToPrefabを更新
            manager.UpdateNameToPrefabMappings();
            if (manager.markerNameToPrefab.Count == 0)
            {
	        // マーカーが一つもなければ警告を表示
                EditorGUILayout.HelpBox(
                    "There are no reference images in the Reference Image Library.",
                    MessageType.Warning
                );
            }
            else
            {
	        // マーカー名とPrefabの紐づけ画面を作成(以下はトグル部分)
                showNameToPrefabMappings = EditorGUILayout.Foldout(
                    showNameToPrefabMappings,
                    new GUIContent("Marker To Prefab", "The mapping from marker name to prefab."),
                    true
                );
                if (showNameToPrefabMappings)
                {
		    // マーカー名をフィールド名にしてPrefabの設定欄を描画
                    foreach (var pair in manager.markerNameToPrefab)
                    {
                        EditorGUILayout.BeginHorizontal();
                        EditorGUILayout.Space();
                        EditorGUILayout.LabelField(pair.name);
                        var newPrefab = (GameObject)
                            EditorGUILayout.ObjectField(pair.prefab, typeof(GameObject), true);
                        if (newPrefab != pair.prefab)
                        {
                            pair.prefab = newPrefab;
			    EditorUtility.SetDirty(manager);
                        }
                        EditorGUILayout.EndHorizontal();
                    }
                }
            }
        }
    }
}
#endif

Inspectorでの実際の動きは以下のようになります。
https://youtu.be/-HxtYA_TS6w

参考📚

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

Discussion