はじめに

ついに、米Niantic社から提供されているLightship ARDKにVPS機能が追加されました。
今回は、Immersalとの比較、Lightship VPSのセットアップ、Immersalからの移植について書いていきます。

※本稿はVPSやImmersalの基礎知識がある方向けに書かれています。

動作環境

  • AndroidAPIレベル24以上
  • 機能制限で iOS 11、フル機能でiPhone 12 Pro以上で iOS 14以上
    • 後述するスキャンアプリ Niantic Wayfarer は LiDAR搭載のiOS15以上

開発環境

  • Unity 2020.3.30f1
  • ARDK 2.0.0
  • Niantic Wayfarer 1.0.0
  • iPhone 13 Pro Max iOS 15.0.2
  • Xcode 13.2.1

Immersalとの比較

以下は、ImmersalとLightship VPSの比較です。

ImmersalLightship VPS
通信環境不要必要
位置情報(GPS)不要必要
位置合わせにかかる時間20~30秒5秒~1分
位置のずれ10cm~20cm撮影した対象によっては、向きが大きくずれる
遠景のマップ化可能不可能
屋内での使用
取得できる姿勢情報マップ自身マップに置かれたアンカー

通信環境、位置情報(GPS)

  • Immersal – マップデータがローカルに存在しています。
  • Lightship VPS – マップデータがサーバ上に存在しています。

上記のような違いがあり、Lightship VPSは、位置情報(GPS)をもとに周囲のマップをサーバから取得するようになっているので、通信環境と位置情報(GPS)が必要です。

位置合わせにかかる時間、位置のずれ

  • ある程度物体の周囲を撮影できるようなもの(冷蔵庫など)の場合 – 位置合わせにかかる時間は5秒~10秒程度と非常に早く、位置のずれも10cm程度でした。
  • 物体の1面しか撮影できないもの(建物の壁面など)の場合 – 位置合わせにかかる時間は30秒~1分程度、位置のずれは、向きが20~30°程度ずれました。

上記のように、位置合わせにかかる時間、位置のずれともに、マップ作成の際に撮影した対象の影響が、Immersalよりも大きいようです。
基本的には、ある程度物体の周囲を撮影できるものでマップを作成するのが良さそうです。

遠景のマップ化

  • Immersal – 遠くにあるビルなども位置合わせの特徴点として使えます。
  • Lightship VPS – LiDARでスキャンできる範囲(5m以内)が特徴点として使えます。

Lightship VPSの強みは、LiDARでスキャンできる範囲であれば、カメラ画像のみのImmersalよりも高精度なマップを作成できるところだと思います。

屋内での使用

  • Immersal – 特に問題はありません。
  • Lightship VPS – GPSなどの位置情報が必要なので、GPSがずれるような建物の中だと位置合わせが正常に行えない場合があります。

現状では、GPSがずれてしまう場所では、Immersalの方が適していますが、GPSがずれないのであれば、LiDARによるスキャンができる分、Lightship VPSの方が使いやすそうです。

取得できる姿勢情報

  • Immersal – 認識できたマップの姿勢情報を取得します。
  • Lightship VPS – 認識できたマップに置いた、もしくは過去に置かれたアンカーの姿勢情報を取得します。

Lightship VPS はマップの姿勢情報を直接取得できません。(2022/6 現在)
ただし、マップ上の⾃由な場所にアンカーを複数置けますし、時間をおいて復元もできます。
⼯夫次第で⾯⽩いことが出来そうです。

マップの作成とアクティベート

ここからは、実際にLightship VPSを使っていきます。

Lightship VPSで位置合わせをするためには、Niantic Wayfarer という専用のアプリを使ってマップのスキャンとアクティベートをする必要があります。

Niantic Wayfarer のインストール

Niantic WayfarerはTestFlightで公開されているアプリです。

iOS端末で、以下の⼿順を⾏うことでNiantic Wayfarerをインストールできます。(以下は、TestFlightをイ
ンストールしたiPhone上での操作です。)

Lightship開発者ページにログインして、右上のハンバーガーメニューを開きます。

My Meshes を開きます。

Upload more meshed in the Niantic Wayfarer app と書かれたボタンをタップします。
Test Flightがインストールされていれば、そのままインストールの画面に進みます。

周囲のスキャン

インストールしたアプリを起動して Lightship の開発者アカウントでログインします。

SCANボタンを押下して、マップを作成したい空間をスキャンします。

マップのアクティベート

次に、今スキャンしたデータをサーバにアップロードします。
アップロード後しばらく(4時間以内)待つとマップのアクティベート処理が完了します。
今回は、20分程度で完了しました。

Lightship VPS のセットアップ手順

ARDKの導入

ARDKをUnityプロジェクトに導入するには、LightshipのホームページからARDK v2.0.0のUnityパッケージをダウンロードし、Unityプロジェクトにインポートする必要があります。

LightshipのホームページでDownloads→Download ARDK(v 2.0.0)と選択することでUnityパッケージをダウンロードできます。

ダウンロードしたUnityパッケージを、任意のプロジェクトにインポートしてください。これで
Lightship VPS の開発環境は整います。AR Foundation の導⼊は必要ありません。

APIキーの設定

まず、Lightshipのプロジェクトを作成します。

Projectsタブを開いて、 New Project ボタンを押下します。

プロジェクトの設定画面が表示されるので、わかりやすいようにプロジェクト名を設定します。
今回は、 com.Upft.Sample としました。

Create New Key を押下すると新しいAPIキーが生成されます。

この後Unity側で使用するので、生成されたAPIキーをコピーしておきます。

Unityエディタで任意のフォルダ内に Resources/ARDK とフォルダを作成します。
作成したフォルダで右クリックをし、 Create→ARDK→ArdkAuthConfig を選択して、 ArdkAuthConfig を生成します。

生成したArdkAuthConfigの API Key に先程コピーしたAPIキーを設定します。

シーンのコンポーネント

まず Assets/ARDK/Extensions/Prefabs 配下の ARSceneManager をシーン上に配置します。
このプレハブの子に ARSceneCamera が含まれています。
少々乱暴な表現になりますが、ARFounationでいうところの AR Session OriginAR Camera に近いイメージです。

次に、カメラと位置情報の権限を要求するために、空オブジェクト AndroidPermissionRequester を作成し、 Android Permission Requester をアタッチします。
そして、 PermissionsCameraFine Location を設定します。

位置合わせ処理の実装

位置合わせをする処理を実装していきます。

using Niantic.ARDK.AR;
using Niantic.ARDK.AR.ARSessionEventArgs;
using Niantic.ARDK.AR.WayspotAnchors;
using Niantic.ARDK.Extensions;
using Niantic.ARDK.LocationService;
using UniRx;
using UniRx.Triggers;
using UnityEngine;

namespace Upft.Behaviour
{
    /// <summary>
    /// コンテンツ管理クラス
    /// </summary>
    public class UpftContentManager : MonoBehaviour
    {
        /// <summary>
        /// 精度(m)
        /// 推奨値 : 1
        /// </summary>
        [SerializeField] private float _desiredAccuracyInMeters = 1.0f;

        /// <summary>
        /// 更新距離(m)
        /// 推奨値 : 0.001
        /// </summary>
        [SerializeField] private float _updateDistanceInMeters = 0.001f;
        
        /// <summary>
        /// コンテンツ
        /// </summary>
        [SerializeField] private GameObject _content;

        /// <summary>
        /// ARセッション管理
        /// </summary>
        [SerializeField] private ARSessionManager _arSessionManager;

        /// <summary>
        /// WayspotAnchorService
        /// </summary>
        private WayspotAnchorService _wayspotAnchorService = null;

        /// <summary>
        /// ARセッション
        /// </summary>
        private IARSession _arSession = null;

        /// <summary>
        /// アンカー
        /// </summary>
        private IWayspotAnchor _anchor = null;
        
        /// <summary>
        /// Start
        /// </summary>
        private void Start()
        {
            _content.SetActive(false);
            startArSession();
        }

        /// <summary>
        /// OnDestroy
        /// </summary>
        private void OnDestroy()
        {
            if (_anchor != null)
            {
                _anchor.TrackingStateUpdated -= onWayspotAnchorTrackingStateUpdated;  // WayspotAnchor更新時の処理を削除
            }
        }

        /// <summary>
        /// ARセッションを開始する
        /// </summary>
        private void startArSession()
        {
            ARSessionFactory.SessionInitialized += onArSessionInitialized;  // ARセッション初期化完了時の処理を登録
            _arSessionManager.EnableFeatures();  // ARセッションの初期化と有効化
        }

        /// <summary>
        /// ARセッション初期化完了時処理
        /// </summary>
        /// <param name="args"></param>
        private void onArSessionInitialized(AnyARSessionInitializedArgs args)
        {
            ARSessionFactory.SessionInitialized -= onArSessionInitialized;  // ARセッション初期化完了時の処理を解除
            _arSession = args.Session;
            _arSession.Ran += onSessionRan;  // ARセッション開始時の処理を登録
        }

        /// <summary>
        /// ARセッション開始時処理
        /// </summary>
        /// <param name="args"></param>
        private void onSessionRan(ARSessionRanArgs args)
        {
            _arSession.Ran -= onSessionRan;  // ARセッション開始時の処理を解除

            startLocalize();
        }
        
        /// <summary>
        /// 位置合わせを開始する
        /// </summary>
        /// <returns></returns>
        private void startLocalize()
        {
            var wayspotAnchorsConfig = WayspotAnchorsConfigurationFactory.Create();
            wayspotAnchorsConfig.ContinuousLocalizationEnabled = true;  // 継続的な位置合わせを有効化
            var locationService = LocationServiceFactory.Create(_arSession.RuntimeEnvironment);
            locationService.Start(_desiredAccuracyInMeters, _updateDistanceInMeters);

            _wayspotAnchorService = new WayspotAnchorService(_arSession, locationService, wayspotAnchorsConfig);  // WayspotAnchorServiceのインスタンスを生成すると位置合わせが開始される
            
            this.UpdateAsObservable()
                .Where(_ => _wayspotAnchorService.LocalizationState == LocalizationState.Localized)
                .First()
                .Subscribe(_ => onLocalized())
                .AddTo(this);
        }

        /// <summary>
        /// 位置合わせ完了時
        /// </summary>
        private void onLocalized()
        {
            createOriginAnchor();
        }

        /// <summary>
        /// 原点アンカーを作成する
        /// </summary>
        private void createOriginAnchor()
        {
            Matrix4x4 matrix;
            if (Application.isEditor)
            {
                // エディタ実行時には、ARコンテンツと同じ位置にアンカーを置く
                var pos = _content.transform.position;
                var rot = _content.transform.rotation;
                var scale = _content.transform.localScale;
                matrix = Matrix4x4.TRS(pos, rot, scale);
            }
            else
            {
                // 実機実行時には、認識したマップの (0,0,0) にアンカーを置く
                matrix = Matrix4x4.zero;
            }
            
            _wayspotAnchorService.CreateWayspotAnchors(onWayspotAnchorsCreated, matrix);  // AR空間の原点のWayspotAnchorを作成する
        }

        /// <summary>
        /// WayspotAnchors作成時
        /// </summary>
        /// <param name="anchors"></param>
        private void onWayspotAnchorsCreated(IWayspotAnchor[] anchors)
        {
            var wayspotAnchor = anchors[0];
            _anchor = wayspotAnchor;
            _anchor.TrackingStateUpdated += onWayspotAnchorTrackingStateUpdated;  // WayspotAnchor更新時の処理を登録
            _content.SetActive(true);
        }

        /// <summary>
        /// WayspotAnchor更新時
        /// </summary>
        /// <param name="args"></param>
        private void onWayspotAnchorTrackingStateUpdated(WayspotAnchorResolvedArgs args)
        {
            if(args.ID != _anchor.ID) return;
            
            var pos = args.Position;
            var rot = args.Rotation;
            _content.transform.position = pos;  // 現実空間の位置に合わせる
            _content.transform.rotation = rot;  // 現実空間の向きに合わせる
            _content.transform.localScale *= _arSession.WorldScale;  // 現実空間の大きさに合わせる
        }
    }
}

今回は認識したマップの (0,0,0) にWayspotAnchorを作成して、その上にコンテンツの⼟台(次項で説明)
を配置することにしました。そのためアンカーの座標と⽅位をコンテンツに反映していますが、スケールは
ARSession.WorldScale に合わせています。

var pos = args.Position;
var rot = args.Rotation;
_content.transform.position = pos;  // 現実空間の位置に合わせる
_content.transform.rotation = rot;  // 現実空間の向きに合わせる
_content.transform.localScale *= _arSession.WorldScale;  // 現実空間の大きさに合わせる

空オブジェクト UpftContentManager を作成し、上記のコンポーネントをアタッチします。
Ar Session ManagerARSceneManager にアタッチされた Ar Session Manager コンポーネントを設定します。

土台の配置

最後にコンテンツの土台となるモデルを配置していきます。
この土台は、AR空間上にARオブジェクトを配置するための目印となります。

まず、コンテンツのルートオブジェクトとなる空オブジェクト ARContent を作成します。

ARContent オブジェクトを、先程のコンポーネント UpftContentManager のパラメータ Content に設
定することで、 WayspotAnchor 更新時に⾃動でコンテンツの位置合わせが⾏われるようになります。

次に、ARContent オブジェクト配下に最初に Niantic Wayfarer で作成したモデルを配置します。

モデルデータはLightshipのダッシュボードからダウンロードできます。(drc形式なので、obj形式などに変換する必要があります。)

実機での実行時に、AR空間の原点に置いたアンカーで位置合わせを行うので、Positionを(0, 0, 0)に配置しておきます。
また、モデルの向きが上下逆さまになっているので、Rotationを(0, 0, 180)にしておきます。

AR Foundation × Immersal で作成したコンテンツの移植

ここまでで、位置合わせの基盤の実装ができました。
ここからは以前、 Immersalを使って作成したAR未来都市(以後 Immersalプロジェクト)を移植していきます。

以下の画像がImmersalプロジェクトの構成です。⼟台となるPLATEAUのモデルとAR空間に表⽰するコンテン
ツのプレハブをUnitypackageにしてエクスポートします。

このUnitypackageをLightship VPSのプロジェクトにインポートします。

以下の画像のように土台とコンテンツの中身を配置します。

※本記事で詳細な⽅法の説明は省きますが、⼟台のオブジェクト群はビルド時に削除されるようにして
います。

現実空間の配置に⼀致するように、 Niantic Wayfarer のモデルは動かさず、にPLATEAUのモデルとARコ
ンテンツを移動して調整します。

これで完成です。

動作確認

Unityエディタ上で確認

実機で確認する前にエディタ上で動作確認してみます。

エディタ上での操作

エディタ上では以下の操作が可能です。

  • WASD & QE – 前後左右と上下のカメラ移動
  • 右クリック+マウス – カメラの回転

いまのオブジェクト配置状態で実行すると、開始位置がPLATEAUのモデルに埋まってしまいました。
このままでも、WASDで移動すれば動作確認はできますが、動作確認のたびに移動するのは面倒なので、初期配置を変更します。

しかし、カメラ位置を変更しても、実行時に原点に戻されてしまいます。
なので、カメラを移動させるのではなく、ARコンテンツの方を移動させます。

実機実⾏時は IWayspotAnchor.TrackingStateUpdated のタイミングで更新されたアンカーの座標にコンテンツを移動させています。(後述のドリフト問題の対策にもなっています。)
そこでエディタ実⾏時はその逆に、コンテンツの中⼼にアンカーが置かれるように意図的に仕向けました。

/// <summary>
/// 原点アンカーを作成する
/// </summary>
private void createOriginAnchor()
{
    Matrix4x4 matrix;
    if (Application.isEditor)
    {   
        // エディタ実行時には、ARコンテンツと同じ位置にアンカーを置く
        var pos = _content.transform.position;
        var rot = _content.transform.rotation;
        var scale = _content.transform.localScale;
        matrix = Matrix4x4.TRS(pos, rot, scale);
    }
    else
    {
        // 実機実行時には、AR空間の原点にアンカーを置く
        matrix = Matrix4x4.zero;
    }
     
    _wayspotAnchorService.CreateWayspotAnchors(onWayspotAnchorsCreated, matrix);  // AR空間の原点のWayspotAnchorを作成する
}

これでエディタで実⾏時も快適に動作確認ができるようになりました。

実機確認

ビルドして実機で動作確認してみます。
前述のとおり、実機では更新され続ける WayspotAnchor の姿勢情報でコンテンツの位置を更新し続け
ているため、この動画のように現実空間とピッタリと重なるように⾒えます。

実機での位置合わせでは、GPSで付近のマップを取得するので、位置合わせに成功しない場合は、端末のGPSが正常に動作しているか確認してみてください。
(調査の段階では、端末の充電が少ないと位置合わせに失敗することがありました。)

ドリフト問題への対処

⼤きな距離を移動したり、端末を振ったりすると搭載されたジャイロが誤作動する、ドリフトが発⽣し
ます。
これの対処として WayspotAnchorsConfig.ContinuousLocalizationEnabled の値を True にして継続的にアンカーの姿勢情報を取得してコンテンツを再配置するようにしています。

おわりに

Lightship VPSを使ってみて、1番良いと思った点はマップの作成が簡単ということです。
Immersalでは、1つのマップを作成するのに、何回か撮り直して数十分かかることもあったのですが、Lightship VPSではNiantic Wayfarerでほとんどの確率で一回のみで成功するので、とてもスムーズにマップを作成することができました。
手軽にマップの作成ができるので、ぜひ皆さんも使ってみてください。



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる