Top View


Author matsuyama

Unity と Geospatial API を使ってスマホで見れる AR コンテンツを作る

2024/03/01

AR 作ってみたい!

AR では現実にはないコンテンツ (画像や 3D モデルなど) をあたかも現実にあるかのように空間に固定して見せているのですが、そのためには、カメラがどの場所にあって、どの方向を向いているかを正確に知る必要があります。

この空間上の場所と向きを正確に知るシステムのことを VPS (Visual Positioning System) と言います。

今回は VPS に ARCore Geospatial API を使って Unity でスマホ (Android) 向けの AR アプリ開発に挑戦してみます。

ざっくり言うと、以下のようなことを実施します。

  • VPS を起動
  • 自身の緯度・経度・高度を取得
  • AR コンテンツを設置 (緯度・経度・高度を指定)
  • 建物のモデルを使って、建物の後ろに AR コンテンツが隠れるようにする

※ ARCore API を利用するため GCP (Google Cloud Platform) で API キーを発行する必要がありますが、その手順は記載していません。

ARCore Geospatial API とは

ARCore Geospatial API は Google が提供する VPS で、スマホの GPS 情報だけでなく Google ストリートビューのデータとスマホのカメラの画像を用いて、精密な位置合わせを実施しているそうです。

Geospatial API の利点としては、事前にスキャンが必要なく、どこでも使えることです。

他の VPS では、事前に専用アプリなどで AR を実施したい場所をスキャンして、スキャンしたデータ上に AR を作るのですが、スキャン不要で、どこに対しても AR を作れるのはとても手軽です!

ただ屋外限定で、屋内では使えません。

詳しくは ↓ に書かれています。

バージョン情報

  • Unity: 2022.3.20f1
  • AR Foundation: 5.1.2
  • Google ARCore XR Plugin: 5.1.2
  • ARCore Extensions: 1.41.0

1. プロジェクトの作成

Unity Hub で New project を押すと 3D テンプレートがあるので、これを使ってプロジェクトを作成します。

最近だと AR Mobile テンプレートがありますが、まだ試せていないです。。。

2. パッケージインストール

Window > Package Manager をきます。 まず Packages をクリックして Unity Registry を選択します。

まずは、AR Foundation をインストールします。 左上の検索ボックスで AR などを入力してパッケージを探し Install をクリックします。

インストール中に Warning が出ますが Yes で大丈夫です。

スクリーンショット1

※ インストール完了すると、Warning に書いてあったように Unity が再起動します。

続けて同様の手順で Google ARCore XR Plugin をインストールします。

さらに ARCore Extensions をインストールしたいのですが、ARCore ExtensionsUnity Registry ではなく GitHub からとってくる必要があるので、右上の + から Add Package from git URL... をクリックし、

https://github.com/google-ar/arcore-unity-extensions.git

を入力して Add でインストールします。

スクリーンショット2

3. AR 向けの設定にする

3.1. ARCore Extensions で Geospatial を有効化設定

Edit > Project Settings... を開きます。 XR Plug-in Management > ARCore Extensions で、

  • Android Authentication Strategy を API Key に設定
  • Android API Key に GCP から取得した API Key を記載
  • Geospatial にチェックをつける

を実施します。

スクリーンショット

3.2. Android を AR できるような設定

Edit > Project Settings... を開きます。 Player > Android > Other Settings の設定を色々変更していきます。

Rendering > Auto Graphics API のチェックを外して、Graphics APIs で AR 未対応の Vulkan を外し、 OpenGLES3 のみにします。

スクリーンショット

Identification > Minimum API Level を AR で必須の Android 7.0 'Nougat' (API Level 24) にします。

スクリーンショット

Configuration > Scripting Backend で ARM64 のビルドに必要な IL2CPP にします。

スクリーンショット

Configuration > Target Architectures で ARCore に必要な ARM64 を選択します。

スクリーンショット

Script Compilation > Scripting Define Symbols に AR Foundation 5 であることを示すため ARCORE_USE_ARF_5 を追加します。

スクリーンショット

3.3. Android で Build する設定

File > Build Settings... を開いて、 Android を選択して Switch Platform します。

4. シーンの実装

4.1. ゲームオブジェクトの追加/削除

次は Hierarchy ペインよりゲームオブジェクトの追加/削除を行います。

まず不要な Main Camera を右クリックから Delete をクリックし、削除します。

次に、右クリックから AR Session を追加します。

スクリーンショット

同様に XR OriginARCore Extensions を追加します。

4.2. ARCoreExtensions で Geospatial を有効化

Project > Assets ペインを右クリックして Create > XR > ARCore Extensions Config をクリックして作成します。

作られた ARCore Extensions Config を選択し Inspector ペインで GeospatialEnabled にします。 ついでに、後で使う StreetscapeGeometryEnabled にしておきます。

スクリーンショット

4.3. ARCore Extensions にオブジェクトを接続

設置したゲームオブジェクト ARCore Extensions で、Inspector ペインを見ると、Session, Origin, Camera Manager, AR Core Extensions Config が選択されていないので、それぞれ、対応するものを選択します。

スクリーンショット

※ ここで OriginXR Origin が指定できない場合は、3.2. の ARCORE_USE_ARF_5 設定が漏れている可能性があります。

5. スクリプトの追加・設定

5.1 AR 関連スクリプトを追加

XR Origin オブジェクトに対して、

  • ロケーションのための AR Earth Manager
  • AR コンテンツを置くための AR Anchor Manager
  • 建物・地形情報を取得するための AR Streetscape Geometry Manager

Add Component から追加します。

スクリーンショット

※ Anchor Prefab が None のままですが、今回は使わないので None で大丈夫です。

5.2. AR Camera Manager の設定

周囲の光を推定して、AR コンテンツにも反映する設定です。

スクリーンショット

いくつか選べますが、とりあえず、Ambient IntensityAmbient Color を設定しています。

詳しくは ↓ に書かれています。

5.3. AR Occlusion Manager の設定

デバイスから取得できる深度情報を用いて、Occlusion する設定です。

これによって、デバイスによりますが、建物を検知したり、手を検知したりして、その後ろにあるように見せることができます。

スクリーンショット

FastestMediumBest が選べますが、今回は Fastest にしています。

詳しくは ↓ に書かれています。

ここまでで、基本的な設定は終わりです。

次からはスクリプトを書いて AR を実装していきます。

6. VPS の起動

VPS を起動するた流れは以下です。

  1. ロケーションサービススタート Input.location.Start
  2. 起動状態 LocationServiceStatus.Running 待ち合わせ
  3. VPS が利用可能かチェック AREarthManager.CheckVpsAvailabilityAsync
  4. VPS 利用可能 VpsAvailability.Available 待ち合わせ
using System;
using System.Collections;
using Google.XR.ARCoreExtensions;
using UnityEngine;
using UnityEngine.XR.ARFoundation;

public class VpsStarter : MonoBehaviour
{
    [NonSerialized] public VpsAvailability availability = VpsAvailability.Unknown;

    bool waitingForLocationService = false;

    void Start()
    {
        StartCoroutine(StartLocationService);
        StartCoroutine(AvailabilityCheck);
    }

    IEnumerator StartLocationService()
    {
        waitingForLocationService = true;

        if (!Input.location.isEnabledByUser)
        {
            Debug.Log("Location service is disabled by the user.");
            waitingForLocationService = false;
            yield break;
        }

        Debug.Log("Starting location service.");
        Input.location.Start();

        while (Input.location.status == LocationServiceStatus.Initializing)
        {
            yield return null;
        }

        waitingForLocationService = false;
        if (Input.location.status != LocationServiceStatus.Running)
        {
            Debug.LogWarning($"Location service ended with {Input.location.status} status.");
            Input.location.Stop();
        }
    }

    IEnumerator AvailabilityCheck()
    {
        if (ARSession.state == ARSessionState.None)
        {
            yield return ARSession.CheckAvailability();
        }

        // Waiting for ARSessionState.CheckingAvailability.
        yield return null;

        if (ARSession.state == ARSessionState.NeedsInstall)
        {
            yield return ARSession.Install();
        }

        // Waiting for ARSessionState.Installing.
        yield return null;

        while (waitingForLocationService)
        {
            yield return null;
        }

        if (Input.location.status != LocationServiceStatus.Running)
        {
            Debug.LogWarning("Location services aren't running. VPS availability check is not available.");
            yield break;
        }

        var location = Input.location.lastData;
        var vpsAvailabilityPromise = AREarthManager.CheckVpsAvailabilityAsync(location.latitude, location.longitude);
        yield return vpsAvailabilityPromise;

        availability = vpsAvailabilityPromise.Result;
        Debug.Log($"VPS Availability at ({location.latitude}, {location.longitude}): {vpsAvailabilityPromise.Result}");
    }
}

※ サンプルの このへん を参考にしています。

VPS が起動したかどうかを public に設定した availability で確認できるようにしています。

7. 位置情報を取得

主にデバッグ用ですが、VPS の状態や位置情報 (緯度/経度/高度)、VPS 精度情報が見られるようにした方が色々と便利なので、実装してみます。

位置情報、精度情報は earthManager.CameraGeospatialPose から取れます。

今回は、取得した情報を TextMeshPro の text に入れているので、Canvas と TextMeshPro の GameObject を作成して、このスクリプトを追加すれば、画面に表示されます。

using Google.XR.ARCoreExtensions;
using TMPro;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;

public class DebugText : MonoBehaviour
{
    [SerializeField] AREarthManager earthManager;
    [SerializeField] VpsStarter vpsStarter;
    TextMeshProUGUI debugText;

    void Awake()
    {
        debugText = GetComponent<TextMeshProUGUI>();
    }

    void Update()
    {
        if (vpsStarter.availability != VpsAvailability.Available)
        {
            debugText.text = string.Empty;
            return;
        }

        UpdateDebugInfo(debugText);
    }

    private void UpdateDebugInfo(TextMeshProUGUI debugText)
    {
        var pose = earthManager.EarthState == EarthState.Enabled &&
            earthManager.EarthTrackingState == TrackingState.Tracking ?
            earthManager.CameraGeospatialPose : new GeospatialPose();

        debugText.text =
            $"SessionState: {ARSession.state}\n" +
            $"LocationServiceStatus: {Input.location.status}\n" +
            $"EarthState: {earthManager.EarthState}\n" +
            $"EarthTrackingState: {earthManager.EarthTrackingState}\n" +
            $"  LAT/LNG/ALT: {pose.Latitude:F6}, {pose.Longitude:F6}, {pose.Altitude:F2}\n" +
            $"  EunRotation: {pose.EunRotation:F2}\n" +
            $"  HorizontalAcc: {pose.HorizontalAccuracy:F6}\n" +
            $"  VerticalAcc: {pose.VerticalAccuracy:F2}\n" +
            $"  OrientationYawAcc: {pose.OrientationYawAccuracy:F2}";
    }
}

※ サンプルの このへん を参考にしています。

8. ローカライズチェック

ローカライズ完了という絶対的の定義はないので、自分で定義します。

今回は、サンプルを参考に horizontalAccuracy (水平方向の精度) と orientationYawAccuracy (ヨー角度の精度) に閾値を設定してそれを超えたらローカライズ完了と判断しています。

using Google.XR.ARCoreExtensions;
using System;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;

public class LocalizeChecker : MonoBehaviour
{
    public double orientationYawAccuracyThreshold = 25;
    public double horizontalAccuracyThreshold = 20;

    public Action OnFinishedLocalization { get; set; }
    public Action OnLostLocalization { get; set; }

    [SerializeField] AREarthManager earthManager;
    [SerializeField] VpsStarter vpsStarter;

    bool isLocalizing = true;

    void Update()
    {
        if (vpsStarter.availability != VpsAvailability.Available) return;

        if (IsLocalizing())
        {
            // Lost localization during the session.
            if (!isLocalizing)
            {
                OnLostLocalization?.Invoke();
                isLocalizing = true;
            }
        }
        else if (isLocalizing)
        {
            // Finished localization.
            OnFinishedLocalization?.Invoke();
            isLocalizing = false;
        }
    }

    bool IsLocalizing()
    {
        bool isSessionReady = ARSession.state == ARSessionState.SessionTracking &&
            Input.location.status == LocationServiceStatus.Running;
        var earthTrackingState = earthManager.EarthTrackingState;
        var pose = earthTrackingState == TrackingState.Tracking ?
            earthManager.CameraGeospatialPose : new GeospatialPose();

        if (!isSessionReady ||
            earthTrackingState != TrackingState.Tracking ||
            pose.OrientationYawAccuracy > orientationYawAccuracyThreshold ||
            pose.HorizontalAccuracy > horizontalAccuracyThreshold)
        {
            return true;
        }

        return false;
    }
}

※ サンプルの このへん を参考にしています。

ローカライズ完了時に OnFinishedLocalization ローカライズ失敗時に OnLostLocalization Action を呼ぶようにしていて、他のスクリプトからこれらの Action を設定することで、ローカライズ完了/失敗時に処理を実行するようにしています。

9. AR コンテンツの配置

緯度・経度・高度を指定して AR コンテンツを出現させたいと思います。

9.1. AR コンテンツの作成

Cube とか適当なゲームオブジェクトを作成して、Prefab にしてください。

9.2. ARGeospatialAnchor を作成して AR コンテンツをインスタンス化

anchorManager.AddAnchor に緯度・経度・高度を設定することで、その場所に Anchor を作成し、Anchor を親として Prefab をインスタンス化することで、緯度・経度・高度の場所にコンテンツを表示させます。

using Google.XR.ARCoreExtensions;
using UnityEngine;
using UnityEngine.XR.ARFoundation;

public class ObjectSpawner : MonoBehaviour
{
    public double latitude;
    public double longitude;
    public double altitude;
    public GameObject prefab;

    ARGeospatialAnchor anchor;
    GameObject instance;

    [SerializeField] LocalizeChecker localizeChecker;
    [SerializeField] ARAnchorManager anchorManager;

    void Start()
    {
        localizeChecker.OnFinishedLocalization += SpawnObject;
        localizeChecker.OnLostLocalization += DestroyObject;
    }

    void SpawnObject()
    {
        Quaternion rotation = Quaternion.Euler(0f, 0f, 0f);
        anchor = anchorManager.AddAnchor(latitude, longitude, altitude, rotation);
        instance = Instantiate(prefab, anchor.transform);
    }

    void DestroyObject()
    {
        if (instance != null)
        {
            Destroy(instance);
            instance = null;
        }
        if (anchor != null)
        {
            Destroy(anchor);
            anchor = null;
        }
    }
}

今回は WGS84 アンカー を使いましたが、他には

があります。

ここまでで、AR コンテンツの生成までできたのですが、もう少し見栄えを良くするために、頑張りたいと思います。

10. StreetscapeGeometry を使って Occlusion を設定する

StreetscapeGeometry はストリートビューのデータをもとに作られた建物と地形の 3D モデルです。

これに Occlusion マテリアル (後ろにあるものを隠すマテリアル) をつけることによって、AR コンテンツが建物に隠れるようになる、はず。

10.1. Occlusion Material の作成

Project > Asset ペインで、右クリックして Create > Material を選択して、 作成した Material の Inspector ペインで Shader に VR/SpatialMapping/Occlusion を指定します。

スクリーンショット

10.2. ARStreetscapeGeometry をインスタンス化して Occlusion を貼る

ARStreetscapeGeometryManagerStreetscapeGeometriesChanged アクションで、追加・削除・更新された ARStreetscapeGeometry を知ることができるので、その Geometry をインスタンス化して、上で作成した Occlusion マテリアルを貼ります。

using Google.XR.ARCoreExtensions;
using System;
using System.Collections.Generic;
using UnityEngine;

public class StreetscapeSpawner : MonoBehaviour
{
    [SerializeField] private ARStreetscapeGeometryManager aRStreetscapeGeometryManager;
    [SerializeField] private Material material;

    readonly Dictionary<string, GameObject> instances = new();

    void Start()
    {
        aRStreetscapeGeometryManager.StreetscapeGeometriesChanged += OnChangeStreetscapeGeometry;
    }

    void OnChangeStreetscapeGeometry(ARStreetscapeGeometriesChangedEventArgs eventArgs)
    {
        DestroyStreetscape(eventArgs.Removed);
        SpawnStreetscape(eventArgs.Added);
        UpdateStreetscape(eventArgs.Updated);
    }

    void SpawnStreetscape(List<ARStreetscapeGeometry> streetscapeGeometries)
    {
        foreach (ARStreetscapeGeometry streetscapeGeometry in streetscapeGeometries)
        {
            var trackableId = streetscapeGeometry.trackableId.ToString();
            var renderObject = InstantiateStreetscapeGeometry(trackableId, streetscapeGeometry);
            instances.Add(trackableId, renderObject);
        }
    }

    void DestroyStreetscape(List<ARStreetscapeGeometry> streetscapeGeometries)
    {
        foreach (ARStreetscapeGeometry streetscapeGeometry in streetscapeGeometries)
        {
            var trackableId = streetscapeGeometry.trackableId.ToString();
            if (!instances.ContainsKey(trackableId)) continue;

            var instance = instances[trackableId];
            DestroyInstance(instance);
            instances.Remove(trackableId);
        }
    }

    void UpdateStreetscape(List<ARStreetscapeGeometry> streetscapeGeometries)
    {
        foreach (ARStreetscapeGeometry streetscapeGeometry in streetscapeGeometries)
        {
            var trackableId = streetscapeGeometry.trackableId.ToString();
            if (instances.ContainsKey(trackableId))
            {
                var renderObject = instances[trackableId];
                renderObject.transform.SetPositionAndRotation(streetscapeGeometry.pose.position, streetscapeGeometry.pose.rotation);
            }
    }

    GameObject InstantiateStreetscapeGeometry(string name, ARStreetscapeGeometry geometry)
    {
        GameObject renderObject = new(name, typeof(MeshFilter), typeof(MeshRenderer));
        renderObject.transform.SetPositionAndRotation(geometry.pose.position, geometry.pose.rotation);
        renderObject.GetComponent<MeshFilter>().mesh = geometry.mesh;
        var meshRenderer = renderObject.GetComponent<MeshRenderer>();
        Material[] materials = new Material[meshRenderer.materials.Length];
        Array.Fill(materials, material);
        meshRenderer.materials = materials;

        return renderObject;
    }

    void DestroyInstance(GameObject instance)
    {
        if (instance.TryGetComponent<MeshFilter>(out var mesh))
        {
            Destroy(mesh);
        }
        if (instance.TryGetComponent<Renderer>(out var renderer))
        {
            foreach (var material in renderer.materials)
            {
                Destroy(material);
            }
        }
        Destroy(instance);
    }
}

これらのスクリプトを GameObject に追加、設定することで実装は完了です!

結果

作ってみたものを実際にスマホ (Pixel 6a) でみてみると、AR コンテンツである Cube が建物の前に現れました!

AR

左上にデバック情報が見えています。

建物に被っている部分はちゃんと Occlusion されているのも分かると思います。

StreetscapeGeometry が細かいところまで再現されていないので、一部 Cube で隠れてしまっていて、そこを Occlusion Manager が建物だと検知して Occlusion しようとした結果、Cube と建物の間がモヤモヤした感じになっているようです。

Occlusion 周りはもっと改善したいですね。

ともあれ、AR っぽいものが出来たので、今後、AR での観光案内とか、 AR で宝探しゲームとか、色々作ってみたいですね。

matsuyama

matsuyama

最近 Unity 触ってます