はじめに

前回 Meta XR SDK を使用して共有空間アンカーを実装する方法について書きました。
今回は AR Foundation + OpenXR: Meta を使用して実装する方法について書いていきます。

開発環境

  • Unity 6000.1.8f1
  • OpenXR Plugin 1.14.3
  • AR Foundation 6.2.0
  • Meta OpenXR Feature 2.2.0

空間アンカーを作成する

1. 空間アンカーを有効化する

  • OpenXR Feature Group の Meta Quest: Anchors を有効にします。

2. シーンに AR Anchor Manager を追加する

  • XR Origin オブジェクトに AR Anchor Manager コンポーネントを追加します。
  • Anchor Prefab も合わせて設定しておきます。

3. 指定した Pose の空間アンカーを作成する

  • 指定した Pose の位置と向きで空間アンカーを作ることができます。
  • この時に作られるアンカーオブジェクトは AR Anchor Manager コンポーネントの Anchor Prefab に設定したプレハブを Instantiate したものです。
public async UniTask<ARAnchor> CreateAnchorAsync(Pose pose)
{
    var result = await _arAnchorManager.TryAddAnchorAsync(pose);
    if(!result.status.IsSuccess()) throw new Exception("Failed to create anchor.");
    var anchor = result.value;
    await UniTask.WaitWhile(() => anchor.pending);
    return anchor;
}

4. 空間アンカーを永続化する

  • 作成した空間アンカーを Meta Quest のローカルに保存します。
  • 空間アンカーの保存が完了すると、そのアンカーに紐付いた SerializableGuid が取得できるので、PlayerPrefs などで別途保存しておきます。
public async UniTask SaveAnchorAsync(ARAnchor anchor)
{
    var result = await _arAnchorManager.TrySaveAnchorAsync(anchor);
    if(!result.status.IsSuccess()) throw new Exception("Failed to save anchor.");
    // result.value(SerializableGuid) を PlayerPrefs などに保存する。
}

5. 永続化した空間アンカーで位置合わせをする

  • PlayerPrefs などに保存してある SerializableGuid で永続化した空間アンカーを読み込み位置合わせをします。
  • 前回の Meta XR SDK との違いとして、こちらの場合は ARAnchorManager.TryLoadAnchorAsync を呼ぶと空間アンカーの読み込み、位置合わせ、 Game Object の生成までが一括で行われます。
public async UniTask<ARAnchor> LocalizeAnchorsAsync(SerializableGuid uuid)
{
    var result = await _arAnchorManager.TryLoadAnchorAsync(uuid);
    if(!result.status.IsSuccess()) throw new Exception($"Failed to load anchor with UUID {uuid}. Error: {result.status.statusCode}");
    await UniTask.WaitWhile(() => result.value.pending);
    return result.value;
}

アンカー共有用GUIDを共有する

  • ホストとなるユーザーが GUID を作成し、他ユーザーに共有します。
  • 前回と同様 Colocation Discovery 機能を使用します。

1. Colocation Discovery を有効化

2. ColocationDiscoveryFeature を取得する

  • OpenXRSettings から ColocationDiscoveryFeature を取得します。
private ColocationDiscoveryFeature getFeature()
{
    var settings = OpenXRSettings.Instance;
    var feature = settings.GetFeature<ColocationDiscoveryFeature>();
    return feature;
}

3. 【ホスト】コロケーションの宣伝を開始する

  • ホストが GUID を Span<byte> としてコロケーションで宣伝します。
public async UniTask<bool> StartAdvertiseAsync(ColocationSessionData data)
{
    var json = JsonUtility.ToJson(data);
    var bytes = Encoding.UTF8.GetBytes(json);
    var result = await _feature.TryStartAdvertisementAsync(bytes.AsSpan());
    return result.status.IsSuccess();
}

4. 【クライアント】コロケーションの発見を開始する

  • クライアントはコロケーションの発見を開始し、ホストから宣伝されている GUID を受け取ります。
public async UniTask StartDiscover(Action<ColocationSessionData> onDiscovered)
{
    _feature.messageDiscovered += (_, message) =>
    {
        var json = Encoding.UTF8.GetString(message.data.ToArray());
        var data = JsonUtility.FromJson<ColocationSessionData>(json);
        onDiscovered?.Invoke(data);
    };
    var result = await _feature.TryStartDiscoveryAsync();
    if(!result.IsSuccess()) throw new Exception($"Failed to start discovery: {result.statusCode}");
}

空間アンカーを共有する

1. MetaOpenXRAnchorSubsystem を取得する

  • ARAnchorManager から MetaOpenXRAnchorSubsystem を取得します。
private MetaOpenXRAnchorSubsystem getMetaOpenXRAnchorSubsystem()
{
    if (!_arAnchorManager) return null;
    return _arAnchorManager.subsystem as MetaOpenXRAnchorSubsystem;
}

2. MetaOpenXRAnchorSubsystem にアンカー共有用GUIDを設定する

  • MetaOpenXRAnchorSubsystem.sharedAnchorsGroupId にアンカー共有用のGUIDを設定します。
  • この設定は空間アンカーの共有と共有された空間アンカーを取得する際の両方で参照されます。
public void SetGroupId(Guid groupId)
{
    var subsystem = getMetaOpenXRAnchorSubsystem();
    if(subsystem == null) throw new Exception("MetaOpenXRAnchorSubsystem is not available.");
    subsystem.sharedAnchorsGroupId = new SerializableGuid(groupId);
}

3. 【ホスト】空間アンカーを共有する

  • 空間アンカーを GUID を指定して共有します。
  • 複数の空間アンカーを一括で共有する ARAnchorManager.TryShareAnchorsAsync という関数も用意されていますが、こちらを呼び出すとアプリがクラッシュしてしまいました。
public async UniTask ShareAnchorAsync(ARAnchor anchor)
{
    var result = await _arAnchorManager.TryShareAnchorAsync(anchor).AsUniTask();
    if(!result.IsSuccess()) throw new Exception($"Failed to share anchor: {result.statusCode}");
}

4. 【クライアント】共有された空間アンカーで位置合わせをする

  • 2で設定した GUID に紐付いて共有された空間アンカーをクラウドから取得し、位置合わせを行います。
  • 位置合わせが完了すると、空間アンカーの位置や向きなどの情報を持った XRAnchor 構造体が取得できます。
public async UniTask LocalizeSharedAnchors(Action<ReadOnlyListSpan<XRAnchor>> onLocalized)
{
    var anchors = new List<XRAnchor>();
    var result = await _arAnchorManager.TryLoadAllSharedAnchorsAsync(anchors, onLocalized);
    if(!result.IsSuccess()) throw new Exception($"Load failed with error {result.statusCode}.");
}

5. 【クライアント】位置合わせ後のデータを元に空間アンカーを作成する

  • XRAnchor 構造体から Pose を取得できるので、空間アンカーを作成するの3. 指定した Pose の空間アンカーを作成すると同様にして空間アンカーを作成します。
public async UniTask<ARAnchor> CreateAnchorAsync(XRAnchor anchorData)
{
    var result = await _arAnchorManager.TryAddAnchorAsync(anchorData.pose);
    if(!result.status.IsSuccess()) throw new Exception("Failed to create anchor.");
    var anchor = result.value;
    await UniTask.WaitWhile(() => anchor.pending);
    return anchor;
}

共有空間アンカーの有効期限

クラウドに共有された空間アンカーには有効期限があります。
有効期限があることは公式ドキュメントでも言及されているので間違いないのですが、「この有効期限が何日なのか?」が記載されておらず、「残り期限を取得する」APIも用意されていないようです。

2年前の段階で↓のように有効期限は24時間という発言をしている人が何人か見つかりました。

また、Unity OpenXR Meta 2.2.0 のドキュメントでは有効期限は30日間と記載されています。

このように情報が錯綜している状況なので、本格的なアプリで使用するのはまだ早いかもしれません。

参考サイト

共有空間アンカーのトラブルシューティングガイド

マップ/アンカーが期限切れになっている。クラウドにアップロードされたどのアンカーについても、有効期間(TTL)が存在します。TTLの期限を過ぎると、アンカーはクラウドから消去されます。

マッチングについて

本記事で紹介した実装方法では、前述の有効期限以外にも問題点があります。
本記事では空間アンカー共有用GUIDの共有に Colocation Discovery 機能を使用しましたが、この機能はシンプルで使いやすい分、「現在クライアントが何人いるか?」、「GUIDが届いたか?」などのマッチング周りの機能が全くありません。

Colocation Discovery 機能を使用しない場合の選択肢として以下が考えられます。(共有空間アンカーの公式サンプルでは GUID の共有に Photon が使用されています。)

  1. Photon
  2. Epic Online Services
  3. Unity Gaming Services

1と2はゲーム以外のアプリでの使用が禁止されていたり、コストが高く設定されています。

そこで今回は 3. Unity Gaming Services について少し調べてみました。

Lobby

GUID を共有するだけであれば Unity Gaming Service (以下 UGS)の Lobby 機能を使用するのが良さそうです。
Lobby はホストがロビーを作成し、参加するクライアントを募ることができる機能です。
さらに、 Lobby が持っているロビーデータという参加者全員が参照できる箱があり、そこに共有用GUIDを格納すれば簡単にGUIDの共有ができます。

Relay

Lobby のデータ共有には数秒のラグがあり、リアルタイム性を重視する空間共有アプリの場合は不向きです。
その問題を解決するために UGS には Relay という機能が用意されています。
この Relay サーバーはホストとクライアントをP2P通信で繋ぐ中継地点の役割をします。
Relay サーバーから発行されたジョインコードをホストがロビーデータに格納することでロビー内の他のクライアントも Relay サーバーに接続できるようになります。

無料枠

UGS の各機能には無料枠が設けられています。
小規模のアプリであれば無料枠で収まりそうです。

Lobby

  • 50 CCU(同時接続ユーザー数)

Relay

  • CCU: 最初の50件 月平均CCU(約216万接続分)
  • データ転送量: CCUあたり3GB、月最大150GB

※ 月平均CCU

1ヶ月の全ユーザー:のべ接続時間(分) / 1ヶ月の合計分数(60分 ✖️ 24時間 ✖️ 30日 = 43200分)

おわりに

いかがでしたでしょうか?
有効期限に関するAPIがないなど未整備な部分もありますが、逆にここさえ解決されれば様々な用途で使っていけると思います。
今後に期待して、更新があったらまた記事にしたいと思います。



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