Table of Contents
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
で大丈夫です。
※ インストール完了すると、Warning に書いてあったように Unity が再起動します。
続けて同様の手順で Google ARCore XR Plugin
をインストールします。
さらに ARCore Extensions
をインストールしたいのですが、ARCore Extensions
は Unity Registry
ではなく GitHub からとってくる必要があるので、右上の +
から Add Package from git URL...
をクリックし、
https://github.com/google-ar/arcore-unity-extensions.git
を入力して Add
でインストールします。
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 Origin
と ARCore Extensions
を追加します。
4.2. ARCoreExtensions で Geospatial を有効化
Project
> Assets
ペインを右クリックして Create
> XR
> ARCore Extensions Config
をクリックして作成します。
作られた ARCore Extensions Config
を選択し Inspector
ペインで Geospatial
を Enabled
にします。
ついでに、後で使う StreetscapeGeometry
も Enabled
にしておきます。
4.3. ARCore Extensions にオブジェクトを接続
設置したゲームオブジェクト ARCore Extensions
で、Inspector
ペインを見ると、Session
, Origin
, Camera Manager
, AR Core Extensions Config
が選択されていないので、それぞれ、対応するものを選択します。
※ ここで Origin
に XR 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 Intensity
と Ambient Color
を設定しています。
詳しくは ↓ に書かれています。
5.3. AR Occlusion Manager の設定
デバイスから取得できる深度情報を用いて、Occlusion する設定です。
これによって、デバイスによりますが、建物を検知したり、手を検知したりして、その後ろにあるように見せることができます。
Fastest
、Medium
、Best
が選べますが、今回は Fastest
にしています。
詳しくは ↓ に書かれています。
ここまでで、基本的な設定は終わりです。
次からはスクリプトを書いて AR を実装していきます。
6. VPS の起動
VPS を起動するた流れは以下です。
- ロケーションサービススタート
Input.location.Start
- 起動状態
LocationServiceStatus.Running
待ち合わせ - VPS が利用可能かチェック
AREarthManager.CheckVpsAvailabilityAsync
- 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 を貼る
ARStreetscapeGeometryManager
の StreetscapeGeometriesChanged
アクションで、追加・削除・更新された 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 が建物の前に現れました!
左上にデバック情報が見えています。
建物に被っている部分はちゃんと Occlusion されているのも分かると思います。
StreetscapeGeometry が細かいところまで再現されていないので、一部 Cube で隠れてしまっていて、そこを Occlusion Manager が建物だと検知して Occlusion しようとした結果、Cube と建物の間がモヤモヤした感じになっているようです。
Occlusion 周りはもっと改善したいですね。
ともあれ、AR っぽいものが出来たので、今後、AR での観光案内とか、 AR で宝探しゲームとか、色々作ってみたいですね。