3
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?

【第2回】【Unity】【初心者向け】 AR Foundationを利用してARマーカーを認識しマーカーを基準にオブジェクトを生成する&UIの実装

Last updated at Posted at 2025-02-08

はじめに

この度インターンシップにて、株式会社GENEROSITY様でARコンテンツの開発を行いました。そこでの実習内容を第1回から第4回の全4部に渡って記事にさせていただきます。

第2回では2週間行ったAR開発の内容のモデル生成方法についてをまとめています。ARの技術として今回は、ARマーカーを認識しその上にオブジェクトを出現させるという事を行いました。前回の第1回の内容を踏まえて進んでいきますので第1回をご覧になっていない方はぜひ以下からご覧になってください。

blenderでのモデル作成からプログラムを書いて実装するまですべて1人で進めましたのでぜひ第2回もご覧いただけると嬉しいです!

目次

対象者
企画概要
使用ツール
開発環境
 OS
 Unity
 テストデバイス
制作
 Illustrator
 Blender
 Unity
  モデルのインポート
  ARマーカーの設定
  フラスコの生成スクリプト
   TrackedImageRotationFixer.cs
  UIの実装
   ARマーカー読み取りスキャン枠
   枠の非表示設定
     ScanUIManager.cs
   アイコンのUI
   UIの配置
   アイコンの出現タイミング
     IconDisplayManager
まとめ

対象者

・プログラミング初心者の方
・UnityでのAR開発に興味がある方

企画概要

企画名はAR授業資料 。例えば地理の学習でARを用いて仮想の地球儀を出現させ国の位置を目で見て学んだり、化学では物質の分子構造を視覚化した疑似的なモデルをARを用いて出現させることでより理解度が深まる工夫をした、頭だけでなく視覚的情報を与えることで子どものさらなる学力を向上を目指したコンテンツです。

今回実習では価値の検証に必要な最低限の機能を備えたプロダクト、MVP(Minimum Viable Product)として、水素と酸素の混合気体の爆発実験のシミュレーション映像を制作することにしました。

以下は私の手書きではありますが今回作るコンテンツの一連の流れになります。

1.まずARマーカーを検出する画面が開きます。

1.png

2.ARマーカーを認識するとフラスコの3Dオブジェクトがマーカーの上に出現します。
("けむり"とありますが、MVPを作る上で、出現時の演出である煙は最低限必要な機能ではないため実装していません。)

2.png

3.フラスコの出現後、右側にアイコンのパネルが出てきます。

3.png

4.上から酸素、水素、マッチ棒のアイコンです。元素のアイコンを押すとその3Dオブジェクトが出現し、フラスコの中に入るアニメーションが再生されます。

4.png
5.png

5.マッチ棒のアイコンを押してマッチ棒を出現させます。火のついたマッチ棒がフラスコに近づきます。

6.png

6.水素が火に反応して酸素の働きも加わることから爆発が起きます。フラスコは粉々に粉砕されます。

7.png

7.最後になぜその爆発が起こったのか解説が表示されます。

8.png

これが私の今回の企画の一連の流れです。この流れを頭に入れて制作を進めます。

使用ツール

Unity
Visual Studio 2022
blender4.3
Adobe Illustrator 2025
ChatGPT

開発環境

OS

Windows 11 Home

Unity

Unity 2022.3.56f1
開発開始時にUnity6がリリースされていましたが、Unity6でのAR開発について情報が少なかったため、過去バージョンで制作しました。

AR Foundation ver. 5.1.5
AR機能を提供するフレームワークです。

テストデバイス

Google pixel 6 (Android 15)
実習にてお借りした端末です。Android端末で設定から開発者向けオプションを開き、USBデバッグをオンにしましょう。

制作

それではいよいよ制作の中身について入っていきます。Unityを開く前にまずは必要な素材を作ります。

Illustrator

Adobe IllustratorでARマーカーのデザインとUIアイコンの作成を行いました。
今回はこのマーカーをAndroid端末のカメラで映し、この上にフラスコを生成させます。

フラスコの絵はIllustratorのベクター生成AIを駆使して短時間でマーカーを作成しました。

マーカーは印刷しても良いですし、pngjpegで保存してスマホなどのデバイスでマーカーの画像を表示させておくのでも良いです。

ARicon.png

Blender

出現させる3DモデルはすべてBlenderで自作しました。

スクリーンショット 2025-02-04 160739.png

スクリーンショット 2025-02-04 160749.png

スクリーンショット 2025-02-04 160725.png

スクリーンショット 2025-02-04 162909.png

マテリアルはUnity内で設定するのでBlenderではモデル作成だけでOKです。

スクリーンショット 2025-02-03 112720.png

後ほどプログラムで物理演算をこのフラスコにつけて粉砕を実装するのですが、その粉砕する欠片をすべてUnityに持っていく必要があるためこのように作成しています。

フラスコの破片はBlenderのCell Fractureという無料のアドオンでランダムに分割します。
(ここでは詳しい説明は省かせていただきます。)

スクリーンショット 2025-02-03 113004.png

分割をしすぎてポリゴン数が多くなるとビルドした際に重くなってしまうので分割しすぎないでください。

割れる前のフラスコも忘れずに用意しましょう。

スクリーンショット 2025-02-04 161639.png

フラスコのモデリングはこちらを参考にしました。

作成したモデルはBlenderFileExportFBXからfbx形式で書き出しをしてください。

書き出す際にBlenderを開いたとき、シーンにデフォルトで存在するライトとカメラは削除してシーンにモデルのみがある状態で書き出しをしてください。fbxで書き出す際にライトとカメラも一緒に書き出されてしまいます。

Unity

モデルのインポート

必要な素材を作り終えたのでまずはUnityに3Dモデルをインポートしましょう。
制作中様々なアセットをUnityProject内に入れていくのでフォルダを作って整理してください。

Unity画面のAssetsのパネルの上で右クリックをし、CreateFolderを選択します。

スクリーンショット 2025-02-04 164039.png

するとフォルダ名を変更できるのでフォルダ名をPrefabsと名付けます。

スクリーンショット 2025-02-04 164755.png

Prefabsという名前のフォルダが作れていれば問題ありません。

次に、PCのエクスプローラーから先ほど書き出したモデルのfbxファイルをドラッグ&ドロップで今作ったPrefabsフォルダに格納します。

スクリーンショット 2025-02-04 165423.png

インポートしてプレハブ化されたモデルはPositionをすべて0にしておきましょう。

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

マテリアルはお好みで設定してください。今回はMVP開発のため単色の簡単なマテリアル設定のみ行いました。

ARマーカーの設定

前編をご覧になってそのプロジェクトのまま進めている方は、HierarchyにはDirectional LightAR SessionXR Originのみがある状態にしてください。オブジェクトを消去する際は消去するオブジェクトを選択してDeleteキーを押すことでシーンから削除できます。

前編をご覧になっていない方は以下を参照してAR開発の準備をしてください。

マーカーの設定は以下の記事を参考にしましたのでその記事の中の画像認識準備の見出しの内容までご覧いただき設定を行ってください。

フラスコの生成スクリプト

フラスコの生成を私はスクリプトで行いました。開発中、マーカーとフラスコの位置がずれてしまうのが気になり、何とかできないかと思いスクリプトでフラスコがマーカーに追従するように工夫しました。

Prefabフォルダを作成したときと同様、Assetsの上で右クリックをし、CreateFolderを選択します。

フォルダ名をScriptsとします。

スクリーンショット 2025-02-04 164755_s.png

ではそのScriptsフォルダの中にフラスコの生成を行うスクリプトを作りましょう。Scriptsフォルダを選択した状態で右クリックをし、CriateC# Scriptを選択します。

スクリーンショット 2025-02-04 175018.png

スクリプト作成直後、スクリプト名が選択された状態なのでその状態のままスクリプト名を変更します。私はTrackedImageRotationFixerとしました。

このように作成直後の名前に青い帯がある状態で名前を変更してください。
スクリーンショット 2025-02-04 175915.png

スクリプト名は自分が分かりやすい名前にしましょう。

TrackedImageRotationFixer.cs

ProjectAssetsでスクリプトをダブルクリックするとエディタが立ち上がります。私の場合はVisual Studio 2022が立ち上がります。

コードはこちらです。

TrackedImageRotationFixer.cs

using System.Collections;
using System.Collections.Generic;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;


public class TrackedImageRotationFixer : MonoBehaviour
{
    [SerializeField] private GameObject normalFlaskPrefab; // 通常のフラスコ
    private ARTrackedImageManager trackedImageManager;

    // 生成されたフラスコを保持するリストまたは変数
    private GameObject spawnedFlask;

    private void Awake()
    {
        trackedImageManager = FindObjectOfType<ARTrackedImageManager>(); // シーン内のARTrackedImageManagerを取得
    }

    // オブジェクトが有効になる際 = ARトラッキング画像が検出される際に呼ばれる
    private void OnEnable()
    {
        if (trackedImageManager != null)
        {
            trackedImageManager.trackedImagesChanged += OnTrackedImagesChanged;
        }
    }

    // オブジェクトが無効になる際に呼ばれる
    private void OnDisable()
    {
        if (trackedImageManager != null)
        {
            trackedImageManager.trackedImagesChanged -= OnTrackedImagesChanged;
        }
    }

    // マーカーが追加されたり更新されたときに実行される
    private void OnTrackedImagesChanged(ARTrackedImagesChangedEventArgs eventArgs)
    {
        // foreachは配列やListの要素を順番に読み取り要素の数だけ処理を繰り返す
        // 新しく検出されたマーカーのリスト
        foreach (var trackedimage in eventArgs.added)
        {
            // FixRotationAndSpawn関数の呼び出し
            FixRotationAndSpawn(trackedimage);
        }

        // すでに検出されているマーカーが更新された場合のリスト
        foreach (var trackedImage in eventArgs.updated)
        {
            FixRotationAndSpawn(trackedImage);
        }
    }

    private void FixRotationAndSpawn(ARTrackedImage trackedImage)
    {

        // すでにフラスコが生成されている場合は何もしない
        if (spawnedFlask != null) return;

        //trackedImage.trackingStateがTrackingState.Tracking の場合にのみ処理を行う
        if (trackedImage.trackingState == TrackingState.Tracking)
        {
            // フラスコを生成
            spawnedFlask = Instantiate(normalFlaskPrefab, trackedImage.transform.position, Quaternion.identity);
            spawnedFlask.name = normalFlaskPrefab.name;

            // 回転補正
            Quaternion targetRotation = trackedImage.transform.rotation;
            Quaternion rotationOffset = Quaternion.Euler(-90f, 0f, 0f); // 必要に応じて調整 //モデルのローカル軸を調整
            spawnedFlask.transform.rotation = targetRotation * rotationOffset;

            // 画像の子として設定
            // マーカーに追従するようにしている
            spawnedFlask.transform.SetParent(trackedImage.transform, true);
            }
        }
    }

    // 生成されたフラスコを返すメソッド
    public GameObject GetSpawnedFlask()
    {
        return spawnedFlask;
    }
}

コードが書けましたらCtrl+Sでセーブし、Unityに戻ってください。

今書いたスクリプトを選択するとInspectorNormal Flask Prefabという項目があると思います。その右側にNoneと書いてある箇所があります。
スクリーンショット 2025-02-04 181606.png

ここには最初に出現させるフラスコを入れてあげる必要があります。ここがNoneのままだと、ARマーカーを読み取っても何も出現しません。

スクリプトのInspectorを開いた状態で、Prefabフォルダの中のヒビが入っていないフラスコをNoneの場所までドラッグします。

スクリーンショット 2025-02-04 181936.png

↑このように青くなったらマウスのボタンを離してドロップさせます。

スクリーンショット 2025-02-04 181949.png

このスクリプトを実行させるにはシーンにあるGame Objectへアタッチする必要があります。
Hierarchyで右クリック→Create Emptyを選択して空のオブジェクトを用意します。

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

オブジェクトの名前はRotationFixerとします。

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

このRotationFixerTrackedImageRotationFixer スクリプトをドラッグ&ドロップをしてアタッチをします。RotationFixerInspectorにドラッグ&ドロップでも大丈夫です。

オブジェクトのInspectorがこのようになっていればOKです!Normal Flask Prefabにも、ProjectAssetからPrefabsフォルダを開いて、最初に出現させるフラスコをドラッグ&ドロップをしてアタッチしてください。

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

ここで一旦FileBuild SettingsBuild And Runでマーカーの上にモデルが表示するか確認してみてください。

UIの実装

制作では先に分子やマッチ棒のモデルを出現させてからUIの操作による動作を実装しましたが、記事での説明上、先にUIの実装から説明します。

ARマーカー読み取りスキャン枠

ARマーカーを探している時に出すフレームを作ります。フレームはIllustratorで画像書き出しまで行って用意しました。

Unityで画像をシーンに配置する方法を説明します。Hirarchyで右クリック→UIImageを選択します。するとCanvasの子にImageが入った状態でHierarchyに追加されます。Imageの名前をScanFrameとします。

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

InspectorSource Imageにフレームの画像をドラッグ&ドロップで埋め込みます。その前にProjectAssetsで右クリック→CreateFolderを選択して、画像を入れておくためのフォルダを用意しましょう。名前はImagesとします。

ARマーカーの画像素材がどこのフォルダにも入っていない状態でAssetsに存在している場合はこの段階で今作ったImageフォルダにスキャン時のフレーム画像素材とともに入れておきましょう。

Source Imageの部分にフレームの画像をアタッチするのを忘れないでください。

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

Positionは画面の中央に来るようにし、Scaleは起動したときに適切な大きさになるように何度かBuild And Runをして調整してください。

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

シーンはこのようになっていればOKです!

枠の非表示設定

ARマーカーを認識してフラスコが出現したらスキャン時のフレームは必要がないので、マーカーが検出された際にこのフレームを消す処理をスクリプトで作成します。

ProjectAssetsに先ほど作成したScriptsフォルダを選択してCreateC# Scriptから新しいスクリプトを用意してください。名前はScanUIManagerとしました。

ScanUIManager.cs

作成したスクリプトをダブルクリックしてコードを書き入れましょう。

コードはこちらです。

ScanUIManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.XR.ARFoundation;

public class ScanUIManager : MonoBehaviour
{
    [SerializeField] private Image scanUI; // スキャン枠のUIオブジェクトを設定
    private ARTrackedImageManager trackedImageManager;

    private void Awake()
    {
        // ARFoundation  ARTrackedImageManager を取得しマーカーの状態を監視する
        trackedImageManager = FindObjectOfType<ARTrackedImageManager>();
    }

    private void OnEnable()
    {
        if (trackedImageManager != null)
        {
            trackedImageManager.trackedImagesChanged += OnTrackedImagesChanged;
        }
    }

    private void OnDisable()
    {
        if (trackedImageManager != null)
        {
            trackedImageManager.trackedImagesChanged -= OnTrackedImagesChanged;
        }
    }

    private void OnTrackedImagesChanged(ARTrackedImagesChangedEventArgs eventArgs)
    {
        // マーカーが検出された場合スキャン枠を非表示にする
        if (eventArgs.added.Count > 0 || eventArgs.updated.Count > 0)
        {
            HideScanUI();
        }
        else if (eventArgs.removed.Count > 0)
        {
            ShowScanUI();
        }
    }

    public void ShowScanUI()
    {
        if (scanUI != null)
        {
            scanUI.enabled = (true); // Imageを表示
        }
    }

    public void HideScanUI()
    {
        if (scanUI != null)
        {
            scanUI.enabled = (false); // Imageを非表示
        }
    }
}

コードがが書けたらUnityに戻り、このスクリプトをGame Objectにアタッチします。
Hierarchyで右クリック→Create Emptyを選択してシーンに空のゲームオブジェクトを用意します。名前をScanUIManagerObjectとします。

ScanUIManagerObjectScanUIManagerスクリプトをドラッグ&ドロップをしてアタッチしましょう。

以下のようになっていればOKです!

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

Scan UIと書いてあるところには作ったスキャン時に表示されるフレームの画像を入れてください。

スキャン前

Screenshot_2025-02-05-10-22-45-42_60acaf36eb21ddc1bae5c6992ed50ac6.jpg

スキャン後

Screenshot_2025-02-05-10-23-01-40_60acaf36eb21ddc1bae5c6992ed50ac6.jpg

ここまで確認できればOKです!

アイコンのUI

ここからはマーカーを読み取ってから少しだけ間をあけてアイコンを表示させる機能を実装します。このアイコンをタップするとそれぞれの3Dモデルが出現して次に進むというものです。

デモ画像

Screenshot_2025-02-05-10-28-51-09_60acaf36eb21ddc1bae5c6992ed50ac6.jpg

UIの配置

先ほどマーカー読み込み前に表示するフレームを作成したときにCanvasというものが作られていたと思います。このCanvasの子にアイコンを入れてあげます。
以下をもう一度ご覧いただき、酸素・水素・マッチ棒・ツールボックスのUIを今回実装するので4つImageを用意してそれぞれ名前分かる形でを変更してください。
ARマーカー読み取りスキャン枠

Canvasがこのようになっていれば大丈夫です。

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

それぞれのImageを選択し、Inspectorに表示されているSource Imageに画像を入れましょう。Unityではほとんどの一般的な画像ファイルに対応しています。詳しくは以下のリンクからご覧ください。

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

toolboxという名前のUI(私ので言うと緑のパネル)はHierarchyではCanvasの子の中で一番上に来るように並べてください。UnityではHierarchyの中で上にあるものが画面上で言うと奥のレイヤーになります。

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

仮に、toolboxを下にしてみると、このようにHierarchyの上側にある他のアイコンが隠れてしまいました。

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

位置調整

このUIたちは画面が回転しても右端に表示させたいので右端に固定されるように設定します。HierarchyでアイコンのImageを選択し、Inspectorに四角いマスのようなものがあると思います。

画像のように選択をすると右端に寄るのでBuild And Runをして確認してみてください。(できていなかったらすみません。もしその場合はPosXを変更したりしてみてください)

Scaleははみ出ないようにそして見にくくない程度でいい感じに調整してください。

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

アイコンの出現タイミング

ではスクリプトで、マーカーを認識した2秒後にアイコンのUIを表示させるようにします。

スクリプトを新規作成して名前をIconDisplayManagerとします。(スクリプトの新規作成方法を忘れてしまった方は記事を遡ってみてください。)

IconDisplayManager.cs

以下コードです。

IconDisplayManager

using System.Collections;
using System.Collections.Generic;
using System.Data;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.UI;
using UnityEngine.XR.ARFoundation;


public class IconDisplayManager : MonoBehaviour
{
    [SerializeField] private GameObject[] icons; // 表示するUIアイコン(複数対応)
    private ARTrackedImageManager trackedImageManager;
    private bool[] isIconHidden; // アイコンが非表示になっているかを記録
    
    private void Awake()
    {
        trackedImageManager = FindObjectOfType<ARTrackedImageManager>();

        // 非表示状態の配列を初期化
        isIconHidden = new bool[icons.Length];

        // シーン開始時に全アイコンを非表示にする
        foreach (GameObject icon in icons)
        {
            if (icon != null)
            {
                icon.SetActive(false); // 非表示に設定
            }
        }
    }

    private void OnEnable()
    {
        if (trackedImageManager != null)
        {
            trackedImageManager.trackedImagesChanged += OnTrackedImagesChanged;
        }
    }

    private void OnDisable()
    {
        if (trackedImageManager != null)
        {
            trackedImageManager.trackedImagesChanged -= OnTrackedImagesChanged;
        }
    }

    private void OnTrackedImagesChanged(ARTrackedImagesChangedEventArgs eventArgs)
    {
        // ARマーカーが検出された場合
        if (eventArgs.added.Count > 0 || eventArgs.updated.Count > 0)
        {
            StartCoroutine(DisplayIconsWithDelay());
            SetupIconButtons();
        }
    }

    private IEnumerator DisplayIconsWithDelay()
    {
        yield return new WaitForSeconds(2f); // 2秒待機

        Debug.Log($"Icons array length: {icons.Length}"); // 配列の長さを表示

        // すべてのアイコンを表示 (ただし、非表示にされたものは除外)
        for (int i = 0; i < icons.Length; i++)
        {
            if (icons[i] != null && !isIconHidden[i]) // 非表示にされていないアイコンだけ表示
            {
                icons[i].SetActive(true);
                Debug.Log($"Icon {i} is now active: {icons[i].name}");
            }
            else
            {
                Debug.LogWarning($"Icon {i} is null or hidden.");
            }
        }
    }
}

これまでと同様にコードを書いたらUnityに戻って空のゲームオブジェクトにスクリプトをアタッチします。名前をIconDisplayManagerObjectとします。

スクリプトがアタッチされたIconDisplayManagerObjectInspectorを見るとIconsという項目があるので、+を押して箱を増やし、それぞれのモデルのプレハブをドラッグ&ドロップしてください。

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

私は水素→酸素→マッチ棒の順番にタップすることを想定して作っているのでIconsにもこの順番に入れています。順番を誤ると押したUIと出てくるモデルが違ってしまうので注意してください。

デモ画像
ここまで出来たらBuild And Runで確認してみてください。UIが表示されればOKです!

Screenshot_2025-02-05-10-28-51-09_60acaf36eb21ddc1bae5c6992ed50ac6.jpg

まとめ

 ここまでお読みいただきありがとうございました。モデルの生成はうまく出来たでしょうか。私と同じ初心者の方にも分かるような説明を心掛けましたのでお役に立てていたら嬉しいです!

次回の第3回では生成したモデルにアニメーションをつけていきますので次回もよろしくお願いします。

おすすめ

第1回はこちら

第3回はこちら

第4回はこちら

3
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
3
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?