はじめに
今回は、VPSとしてImmersalを使っていた表参道未来都市をGeospatial APIへ移植してみました。
そして、Immersal版とGeosptial版での開発時の手間の違いや実機の動作の比較検証を行いました。
こちらがGeospatial APIで動作している動画です。
目次
開発環境
- Unity2021.3.3f1
- XR Plugin Management 4.2.1
- ARCore Extensions 1.32.0
- AR Foundation 4.2.3
- ARCore XR Plugin 4.2.3
- ARKit XR Plugin 4.2.3
- UniTask 2.3.1
- Android 12
ImmersalからGeospatialへ
Immersal版の確認
こちらがImmersal版のシーンです。
Immersalマップの点群やテクスチャ付きメッシュ、オクルージョン用のメッシュ(PLATEAUのfbxモデルなど)、AR空間に表示するコンテンツなどがあります。

Immersal版ではARコンテンツ周りの構成は以下のようになっていました。
ARSpace
├─ AR Map : VPS用の点群データ
├─ __Occlusion__ : オクルージョン用のメッシュの親
├─ __Dummy__ : LiDARスキャンしたメッシュやImmersalのテクスチャ付きメッシュの親
└─ Contents : AR表示するオブジェクトの親
AR Map
はImmersalで使われるVPS用の点群データです。

__Occlusion__
下にはAR画面でのオクルージョン用のメッシュオブジェクトが置かれています。
主にPLATEAUの建物モデルや、PLATEAUモデルで足りない範囲を補うメッシュオブジェクトなどがあります。

__Dummy__
下にはエディタ上でのコンテンツ配置の目印にするためのテクスチャ付きメッシュオブジェクトが置かれています。
主にiOSのLiDARでスキャン・生成した3DモデルやImmersalで点群と合わせて生成されるglbモデルなどがあります。

Contents
下にはAR表示したいオブジェクトが置かれています。

Immersalでのコンテンツ開発時には、AR Map
用の点群を現地で撮影した上でこれらの相互的な位置や回転や大きさをズレなく配置する必要があり、面倒で時間のかかる作業でした。
Geospatial用シーンの作成
次のような構成のシーンを作成します。GeospatialManager
とAnchorAttacher
コンポーネントは後述のスクリプトを追加して使用して下さい。




GeospatialManager.cs
- Geospatial APIの呼び出しの大部分を行っており、これによってトラッキングの状態や精度を管理しています。
- Geospatialの自己位置推定の精度がインスペクターの各
Accuracy
の値を満たしたときにイベントが呼ばれます。 - こちらのクラスをTerrainAnchorへの対応などのため、一部改変したものです。
変更箇所は次の通りです。
private void Update()
{
...
/// Check earth state
EarthState earthState = EarthManager.EarthState;
+ if (earthState == EarthState.ErrorEarthNotReady)
+ {
+ return;
+ }
+
if (earthState != EarthState.Enabled)
{
SetErrorState(ErrorState.Message, "Error: Unable to start Geospatial AR");
enabled = false;
return;
}
...
}
...
+ /// <summary>
+ /// Creates and returns a new Geospatial Anchor at the specified Geospatial Pose
+ /// </summary>
+ /// <param name="pose"></param>
+ /// <param name="useTerrain"></param>
+ /// <returns></returns>
+ public ARGeospatialAnchor RequestGeospatialAnchor(GeospatialPose pose, bool useTerrain = false)
+ {
+ var quaternion = Quaternion.AngleAxis(180f - (float)pose.Heading, Vector3.up);
+ return useTerrain
+ ? AnchorManager.ResolveAnchorOnTerrain(pose.Latitude, pose.Longitude, pose.Altitude, quaternion)
+ : AnchorManager.AddAnchor(pose.Latitude, pose.Longitude, pose.Altitude, quaternion);
+ }
コンテンツの移植
Geospatialで任意の位置にオブジェクトを配置するには、対象の地点の地理座標を指定する必要があります。
そのため、Unity上で狙った場所に表示できるよう位置調整するというのが難しいのですが、以前の記事でとりあげた PLATEAUのCityGML建物モデルを配置する方法 をとることで建物モデルを基準にコンテンツの位置調整を行うことができます。 今回も同手法をベースに、CityGMLモデルのセットアップや配置を行います。
それでは、以下の手順でGeospatialへ移植してみます。
- 対象エリアのCityGMLデータをダウンロード
- 構築範囲図を参考に対象エリアのgmlファイルを見つける
- CityGMLToObjでgmlファイルからobjへ変換
- Unity上で調整
1. 対象エリアのCityGMLデータをダウンロード
3D都市モデル(Project PLATEAU)ポータルサイトから東京都23区のCityGMLデータをダウンロードします。
ダウンロードしたデータのうち、gmlファイルはudx/bldg
内にあります。
2. 対象エリアのgmlファイルを見つける
今回対象となるエリアは表参道駅の周辺です。

PLATEAUの23区構築範囲図を見ると53393596,53393597のちょうど境界の位置となっていたため、この2つのエリア分のgmlファイルを使用します。

gmlをobjへ変換
CityGMLToObj では、指定した地理座標位置を基準点(モデルの原点)として指定することができます。
基準点を指定しない場合はモデル全体の緯度、経度、標高の最小値が原点として設定されるため、コンテンツに使用したい建物がある場所と原点位置が遠く離れてしまう場合があります。(下記画像ではギズモ位置が原点ですが、モデルのメッシュは遠くにある状態です。)

今回は基準点の位置を、AR体験時に想定している立ち位置に指定しました。
緯度と経度はGoogleMapやPLATEAU Viewで対象の地点を選択すると確認できます。


標高は国土地理院の経緯度から標高を求めるプログラムから取得しました。

それでは、gmlファイルを変換します。
CityGMLToObjの第1引数にgmlファイルのパス、第2~4引数に基準点の緯度、経度、標高を指定するとその位置を原点としたobjファイルが出力されます。
$ CityGMLToObj.exe path/to/bldg/53393596_bldg_6697_2_op.gml 35.66515 139.71218 33.8
$ CityGMLToObj.exe path/to/bldg/53393597_bldg_6697_2_op.gml 35.66515 139.71218 33.8
4. Unity上で調整
生成されたobjファイルを含むフォルダごとUnityへインポートしてシーン上へ並べてみます。

基準として指定した場所がUnityでの原点にありますね。
余分な建物モデルが多く含まれているので、必要なさそうな建物を削除していきます。

とりあえずこれぐらい残して1つのPrefabにまとめました。

次に、Immersal版のシーン上のARSpace
以下のオブジェクトのうち、AR Map
以外のオブジェクトをGeospatialのシーンへ配置します。
Immersal版のシーンにも元がfbxのPLATEAUモデルはあったので、その位置がgmlのPLATEAUモデルとなるべく重なるように調整します。このとき、gmlから作成した建物モデル は動かさずにおいておきます。
(白い建物がgmlから作成したメッシュ、黄緑がfbxからのメッシュ、水色はPLATEAUがカバーしていない範囲を補うメッシュ)

位置調整が済んだらfbxからのメッシュは不要なので削除して、次のような構造でPrefab化します。
BuildingsAnchor
├─ bldgs
├─ __Occlusion__
└─ Contents

このPrefabに次の BuildingsAnchor
をアタッチします。
インスペクターの Origin Geo Pose
にはgmlモデル変換の際に使用した座標を入力し、 ContentsRoot
には子の Contents
を指定してください。BuildingsAnchor
は初期化時に自身の親に TerrainAnchor
を生成するため、 BuildingsAnchor
オブジェクト配下の建物モデルやコンテンツオブジェクトなどが、AR空間でもエディタで指定した位置に表示されるようになります。
using System;
using System.Collections.Generic;
using Google.XR.ARCoreExtensions;
using UnityEngine;
namespace Sandbox.Geospatial.Gaprot
{
[Serializable]
public struct GeoPose
{
/// <summary>
/// 緯度
/// </summary>
public double Latitude;
/// <summary>
/// 経度
/// </summary>
public double Longitude;
}
public class BuildingsAnchor : MonoBehaviour
{
[SerializeField] private GeoPose _originGeoPose;
[SerializeField] private GameObject _contentsRoot;
public void ActivateContents()
{
_contentsRoot.SetActive(true);
}
public void DeactivateContents()
{
_contentsRoot.SetActive(false);
}
public void Initialize()
{
DeactivateContents();
InitializeAnchor();
}
private void InitializeAnchor()
{
var pose = new GeospatialPose
{
Latitude = _originGeoPose.Latitude,
Longitude = _originGeoPose.Longitude,
Altitude = 0, // TerrainAnchorで地表の高さに置きたいので0m
};
var anchor = GeospatialManager.Instance.RequestGeospatialAnchor(pose, useTerrain: true);
if (anchor == null)
{
throw new NullReferenceException("Geospatial Anchorの生成に失敗しました。");
}
// アンカーは基準点の位置に配置されるので、その子となる自身は親と相対的に同じ姿勢になるようにする
var self = transform;
self.position = Vector3.zero;
self.rotation = Quaternion.identity;
self.SetParent(anchor.transform);
}
}
}
動作確認
次のスクリプトをシーン上の適当なオブジェクトに追加して動作確認したのが冒頭の動画です。
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace Sandbox.Geospatial.Gaprot
{
public class Demo : MonoBehaviour
{
[SerializeField] private BuildingsAnchor _bldgsAnchor;
private void Init()
{
var anchor = Instantiate(_bldgsAnchor);
anchor.Initialize();
anchor.ActivateContents();
}
private void Start()
{
GeospatialManager.Instance
.TargetAccuracyReached
.GetAsyncEventHandler(this.GetCancellationTokenOnDestroy())
.OnInvokeAsync()
.ContinueWith(Init);
}
}
}
比較
Immersal版とGeospatial版とを比較検証してみます。
検証に先立って
本検証では認識成功とみなす条件を以下のようにします。
Immersal | Geospatial | |
---|---|---|
認識成功条件 | マップの姿勢検出されたとき (SDKで提供されているイベント) | 精度の値が急に大きく向上したとき |
認識成功時 | 画面上でワープエフェクトが開始される | 建物のワイヤーフレームが表示される |
ImmersalではSDK内でマップ検出のタイミングを判断しているのでそのときに認識成功とします。
Immersal版アプリでは認識成功後にワープエフェクト演出が始まります。 その後はコンテンツが表示されます。
Geospatial APIでは常にGPSとVPSを併用しながら自身の地理的な姿勢(緯度、経度、高度、方位)とその精度を推定しています。
VPSでマップ検出されるとこの精度が急に大きく向上するため、この急変動を「認識成功」と定義しました。
Geospatial版アプリでは認識成功後に (都合上ワープエフェクトでなく) ワイヤーフレームとコンテンツを同時に表示するようにしました。
また、検証動画の中でデバッグ機能用のUIが一部違うものも混ざっていますが本検証内容には影響しないため、そのまま使用しています。
認識速度と精度
カメラを地面に向けた状態から、マップとなっている建物へ向けて認識するまでの時間を比較しました。
認識速度はどちらも数秒で認識が完了しています。
精度は各VPSともに期待した場所にコンテンツが表示されているようです。
精度や速度に関しては大きな差はなさそうですが、Immersal はマッパーアプリで作成するマップ(点群データ)の出来に依存してしまうという難しさがあります。
Immersal

Geospatial

最高水平精度 | 最高垂直精度 | 最高方位精度 |
---|---|---|
0.212313(m) | 0.14(m) | 1.1(°) |
認識範囲(距離)
各VPSがマップを認識できる範囲や距離を比較しました。
Immersal
こちらがマップとなる点群の範囲です。

今回のケースでは65メートル程度まで近づくと認識しています。

Geospatial
Geospatialではマップの範囲がストリートビュー対応範囲全体となっておりマップから距離をとることが難しいため、VPSで認識される境界を探しました。
特徴が少なそうな路地裏でもすぐに認識したほか、2つ目の動画のように建物の端の部分をちらっと映すだけでVPSの認識されていることが確認できました。
ドリフトからの復帰
デバイスのカメラを手で隠した状態でデバイスを振り回し、トラッキングを意図的にドリフトさせます。
その後、再びカメラに周囲の建物を映したときにドリフト状態から復帰する挙動を見てみます。
結果は、ImmersalとGeospatialともに数秒で復帰できていました。
Immersal
Geospatial
おわりに
今回はImmersalで作成されたプロジェクトのImmersalへの移植と、移植前後での比較をしました。Immersalでの開発の際はマップ用点群の撮影や、Unity上で点群とオクルージョン用のメッシュの位置を合わせるのに手間がかかっていましたが、Geospatialではその必要がなくなりました。
移植作業自体も非常にスムーズに進められたので、Immersalでなにか制作していた方はぜひGeospatial版も作ってみてはいかがでしょうか。