【Unity】ARKit 2 で、複数人同時 AR をやる

こんにちは!面白法人カヤックのごんです。
ARkit 2 の記事を書きます!

この記事はカヤックUnityアドベントカレンダー2018の2日目の記事です。

f:id:umai_bow:20181130120327g:plain

前段

ARKit 2 では、ARKit 1 と異なり、複数人でのARの共有体験ができます。
ネットワークライブラリでもないのに、なぜ複数人の体験?ということが気になる方もいるかと思うのですが、
これは具体的には、AR空間のワールドマップのシリアライズデシリアライズ、そしてリローカリゼーションをサポートするというものです。
シリアライズされたデータをネットワークで共有することで、複数人でARの共有体験をすることができます。
ARKitは、このネットワークで共有する部分は関与しません。
今回は、Photon Unity Networking (PUN) を使って、ARKitで複数人でARを体験できるアプリケーションを作成します。

ARKit 2 を使う前の準備

ARKit 2 を使う前の準備です。

1. Xcode 10 をインストールする

iOS 12 向けにアプリをビルドする必要があるので、Xcode 10 以上のバージョンが必要です。
App Store からダウンロードしてインストールします。

2. iOS を 12 にアップデートする

iOS を 12 にアップデートしないとアプリが動かないので、iOS を 12 にアップデートしましょう。
なお、ARKit 2 が使えるのは、iPhone 6s 以降、すべての iPad Pro モデル、iPad(第5世代)、およびiPad(第6世代)とされているので、注意しましょう。

3. Unity-ARKit-Plugin をダウンロードする

Unity で ARKit を扱うためのパッケージ Unity-ARKit-Plugin をダウンロードします。
Mercurial で clone するか、サイトからダウンロードしましょう。

4. Unity プロジェクトを作り、Player Settings を変更する

ARKit を使うにあたり、以下の設定が必要になります。

  • Target minimum iOS Version を 12.0 にする
  • Required ARKit support にチェックを入れる

なお、Unity のバージョンはどこから対応しているのかわからないのですが、この記事では、2018.2.6f1 で進めていきます。

1人で遊べるシーンをつくる

ARKit が動くミニマムのシーンを作ります!
新規シーンを作ったら、カメラに対して、以下のようにコンポーネントをつけます。

これで ARKit が動くようになります!手軽!

とはいえ、これだけでは何も起こらないので、画面をタップしたら Prefab を生成するようにしておきます。

Bubble.cs

using UnityEngine;

public class Bubble : MonoBehaviour {
  void Start () {
    MaterialPropertyBlock mpb = new MaterialPropertyBlock();
    mpb.SetColor("_Color", Color.HSVToRGB(Random.value, 1f, 1f));
    GetComponent<MeshRenderer>().SetPropertyBlock(mpb);
  }
}

BubbleGenerator.cs

using UnityEngine;

public class BubbleGenerator : MonoBehaviour {
  [SerializeField] Bubble bubblePrefab;

  void Update () {
    if (Input.GetButtonDown("Fire1"))
    {
      Instantiate(bubblePrefab, transform.position, Quaternion.identity);
    }
  }
}

若干雑な説明ですが、いい感じに想像で補ってください
ARKit には、他にも床を検出したり環境マップを生成したり、その他もろもろの素敵な機能があるのですが、今回は関係ないので使いません。

f:id:umai_bow:20181130120311g:plain

Photon Unity Networking を入れる

今回は、Unity 内で生成した Prefab などを、複数人で共有するために Photon Unity Networking (PUN) を使います。
PUN の細かいセットアップ手順などは省きますが、こんなものは動けばよいので適当に書きましょう

using UnityEngine;
using Photon.Pun;
using Photon.Realtime;

public class Game : MonoBehaviourPunCallbacks {
  const string GameVersion = "1";
  const string RoomName = "room";

  [SerializeField] BubbleGenerator bubbleGenerator;

  void Start()
  {
    PhotonNetwork.AutomaticallySyncScene = true;
    PhotonNetwork.GameVersion = GameVersion;
    PhotonNetwork.ConnectUsingSettings();
  }

  public override void OnConnectedToMaster()
  {
    PhotonNetwork.JoinLobby();
  }

  public override void OnJoinedLobby()
  {
    PhotonNetwork.JoinOrCreateRoom(RoomName, new RoomOptions(), new TypedLobby());
  }

  public override void OnJoinedRoom()
  {
    Debug.Log("I'm in the room.");
    bubbleGenerator.gameObject.SetActive(true);
  }
}

Photon の接続部分が書けたら、Prefab の Instantiate を PhotonNetwork のものに差し替えます。

PhotonNetwork.Instantiate(bubblePrefabName, transform.position, Quaternion.identity);

Prefab にも、同期用の PhotonView をつけておきます。

ワールドマップの共有

さて、ここからが本題です。
PUN を使うことで、オブジェクトの位置の同期はできましたが、AR空間の座標系が異なるため、このままでは2つの異なる端末で同期したときに、全然関係のない位置にオブジェクトが出てしまいます。
そこで、ARKit 2 のワールドマップ共有の機能の出番です。

ワールドマップのシリアライズ

ワールドマップの取得とシリアライズは、以下のようにできます。
Unity-ARKit-Plugin の WorldMapManager.cs の中身が参考になります。

var session = UnityARSessionNativeInterface.GetARSessionNativeInterface();
session.GetCurrentWorldMapAsync(worldMap => {
    var worldMapInBytes = worldMap.SerializeToByteArray(); // ← これがシリアライズされたワールドマップのデータ
});

また、ロードは次のように書けます(サンプルコードのほぼパクリ)

ARWorldMap worldMap = ARWorldMap.SerializeFromByteArray(worldMapInBytes);

Debug.LogFormat("Map loaded. Center: {0} Extent: {1}", worldMap.center, worldMap.extent);

UnityARSessionNativeInterface.ARSessionShouldAttemptRelocalization = true;

var config = m_ARCameraManager.sessionConfiguration;
config.worldMap = worldMap;
UnityARSessionRunOption runOption = UnityARSessionRunOption.ARSessionRunOptionRemoveExistingAnchors | UnityARSessionRunOption.ARSessionRunOptionResetTracking;

Debug.Log("Restarting session with worldMap");
session.RunWithConfigAndOptions(config, runOption);

ワールドマップのアップロード

シリアライズされたワールドマップのデータは容量が数百[kb]あり、PUN経由で共有するには、少しサイズが大きいため一旦サーバにアップロードして共有することにしました。
サーバアプリは node で書きました。本題とそれほど関係ないので、Github のリンクだけ貼ります。

サーバアプリのコード

Unity 側はこのサーバに POST でシリアライズされたワールドマップのデータを送りつけるだけです。

ワールドマップの共有

ワールドマップの共有は

  • ワールドマップをシリアライズする
  • シリアライズされたワールドマップをアップロード
  • アップロードした URL を PUN の RPC で共有
  • RPC を受信した側は、URL からシリアライズされたワールドマップをダウンロードし、適用

という手順で行います。
全部コードを書くと長くなってしまうので、なんとなく↓のコードで雰囲気を掴んでください!

public void UploadCurrentWorldMap()
{
  session.GetCurrentWorldMapAsync(worldMap => {
    var worldMapInBytes = worldMap.SerializeToByteArray();
    StartCoroutine(worldMapUploader.Upload(worldMapInBytes, path => {
      photonView.RPC("DownloadWorldMap", RpcTarget.AllBuffered, new object[] { path });
    }));
  });
}

[PunRPC]
public void DownloadWorldMap(string path)
{
  Debug.Log("Download World map");
  StartCoroutine(worldMapUploader.Download(path, LoadSerializedWorldMap));
}

完成!

ちゃんと他のデバイスと共有した空間でARができています。

f:id:umai_bow:20181130120327g:plain

おわりに

今回はちゃんと ARKit 2 を使って、複数人でARを共有できるやつをやりました。
もっと賢いやり方があるのかもしれないので、知っている方がいたら教えてください!

明日の記事はこみやによるGitHubのプルリクエストからリリースノートをさっさと作る話です。