じゅころぐAR

ARのブログ

ARCore Cloud Anchor APIをマスターする(後編)

前回の記事に前編と付けることで後編を書くためのネタを作るという『はてブ駆動開発』をやってみました。

後編では、Cloud Anchorを利用してプレイヤー間で複数のオブジェクトを共有するARアプリの具体的な実装を考えてみます。

Cloud Anchor APIの制限事項

どう使うかを考える前に、利用上の制限を調べておきましょう。

帯域制限

Cloud Anchor APIには、1分間あたりのリクエスト回数に上限が設けられています。

項目 制限値 適用対象
Anchorの数 無制限 Project
Hostリクエスト 1分間あたり30回 Project、IPアドレス
Resolveリクエスト 1分間あたり300回 Project、IPアドレス

https://developers.google.com/ar/develop/unity/cloud-anchors/cloud-anchors-developer-guide-unity#api_quotas

適用対象の"Project"がCloud Anchor APIのAPIキーを取得するために作ったGCPのProjectだとすると、同じAPIキーを持つアプリすべてのリクエストを合計した値が上記の制限になるはずです。そのため、1ユーザが複数回のHostを行ってしまうと、同時にプレイできるユーザの母数が減ってしまいます。

また、複数のアプリでCloud Anchorを使いたい場合、同じAPIキーは使い回さず、新しいProjectでAPIキーを作成した方がよさそうです。

リクエスト時の遅延

実際に試すとわかりますが、Host、Resolveのリクエストを送ってから応答が返ってくるまでに数秒のラグが起きることがあります。
GCPのCloud Anchor APIへAnchorの情報を送受信するためにインターネットに接続しているためです。

周囲の空間検知をしっかり行うほど、Resolveに成功しやすくなりますが、送信するデータ量が増えるのか、その分時間がかかりやすくなる印象です。

Cloud Anchorのベストプラクティス

上記の制限とExampleの実装から、最適な使い方を考えてみます。

共有するオブジェクト単位でCloud Anchorは使わない

これが『大原則』だと思います。

Anchorをオブジェクト全部に付けておけば実装も楽で安心と考えるかもしれませんが、前述の通り、GCPを経由する分のラグが発生しやすいですし、APIの帯域制限でプレイできないユーザが増える要因になります。

RoomにCloud Anchorは1つが基本

ExampleがRoomIdとCloud Anchorを紐付けていたことからも、Roomの基準点を示すCloud Anchorを1つだけHostし、RoomへのJoin時に各ユーザが1回だけResolveする形が正しいと思います。

この使い方であれば、アプリをストアで公開した際、全世界でRoomを1分間に30個作ることができ、各Roomに平均10ユーザの300ユーザが同時にプレイできるようになります。
加えて、Anchorの数には制限がないため、1分経てば前のユーザのプレイが終わっていなくても、新たなRoomの作成(Host、Resolve)はできるようになります。

プレイエリアが広ければ複数も?

Resolveの仕様として、HostされたAnchorの周囲をデバイスで検知する必要があります。

スタート地点が決まっていたり、プレイヤー同士の物理的な距離が近ければ問題ないですが、プレイエリアを広く使いたいような場合には、複数のCloud Anchorを配置して、近くにあるAnchorをResolveという使い方もありかもしれません。

Cloud Anchorを利用したオブジェクト共有の実装

RoomにCloud Anchorが1つあることを前提として、マルチプレイを実装していきます。

ARの座標系

まず、ARの座標系について知っておかないと実装で混乱します。ARの開発をしたことがあれば、知っていて当然の知識かもしれませんが、一応。

ARKit、ARCore共通で、ARを開始した時点のデバイスの姿勢が基準になります。デバイスの初期位置が(0, 0, 0)の左手座標系になり、実空間で1メートル動くと、Unity上の距離で1増減します。

初期位置から見て、

  • 右に移動:xが増える
  • 左に移動:xが減る
  • 上に移動:yが増える
  • 下に移動:yが減る
  • 前に移動:zが増える
  • 後ろに移動:zが減る

あくまで初期位置から見た向きが基準になるので、デバイスの向きが変われば前後左右の関係性は変わります。
例えば、初期位置から右を向いた状態で前に移動すると、移動方向は前ですが、xが増えていきます。(初期位置から見ると右に移動しているので)

確認のため、画面左下にデバイス(MainCamera)の現在位置を表示してみました。

Vector3は、ToStringするだけで(x,y,z)が出力されるため、開発中に表示しておくと何かと便利です。

[SerializeField]
private Text info;

void Update () {
  info.text = Camera.main.transform.position.ToString();
}

ARのマルチプレイを考えたとき、複数のデバイスは同じ空間にいますが、起動時の姿勢により異なる座標系を持っています。そのため、空間上は同じ位置でもデバイスごとに異なる座標となります。

f:id:jyuko49:20180617084314p:plain

デバイスAが空間上の位置を自分から見た座標系で教えても、デバイスBの座標系では異なる位置を示すため、単純にtransformを送り合うだけでは空間上の位置を伝えることはできません。

仮にデバイスをぴったり同じ位置と向きで起動すれば同じ座標系になりますが、誤差なく同じ位置で起動するのは難しいので、現実的な解決策ではありません。

Cloud Anchorのtransform

前述の問題を解決するために、Cloud Anchorの登場です。

Cloud AnchorはAPIによって空間上の同じ位置に配置されています。 一方で、デバイスごとの座標系の差異については例外ではなく、Cloud Anchorのtransformが保持している座標はデバイスごとに異なります。

上記を実機で確かめてみます。

まず、Hostする側のデバイスでCloud Anchorとして登録したAnchorの位置_lastPlacedAnchor.transform.position.ToString()を表示させます。

f:id:jyuko49:20180617083146j:plain

この例では、デバイスAから見たCloud Anchorの座標は(0.2, -1.0, 0.1)になっています。

次に、デバイスB(初期位置がデバイスAと異なる)で上記のCloud AnchorをResolveして、取得したAnchorの位置result.Anchor.transform.position.ToString()を表示させます。

Cloud Anchor(キャラクター)は空間上の同じ位置、向きにいますが、デバイスBから見た座標は(-2.0, -0.7, -0.9)になっています。

Cloud Anchor APIによるResolveは、デバイスの座標系そのものをHostに合わせているのではないという点は理解が必要です。
そのため、Cloud Anchorを使わずに他のオブジェクトを共有すると位置がズレてしまいます。一方で、先に述べた通り、闇雲にCloud Anchorを使えばいいという実装は得策ではありません。

ではどうするか。

デバイスの座標系が違っていてもCloud Anchorの空間上の位置、向きが同じことは保証されているので、Cloud Anchorの座標系を基準に位置を伝えて、デバイスごとの座標に変換するようにすれば、空間上で同じ位置を指すはずです。

Cloud Anchorとデバイスの座標系変換

※注:ここから先の説明は、Unityのマニュアルが非常にわかりにくく、理解が追いついていない部分もあります。実装上は正しく動作していますが、もし間違いがあれば指摘ください。

他でデバイスにCloud Anchorを基準にした位置を伝えるには、Cloud Anchorの座標系に変換の逆変換を行えばよいはずです。

※なんでそうなるのかは別の記事に書きました!
どうして空間共有で先に座標系の逆変換(inverse)を行うのか? - じゅころぐAR

座標変換には行列を用いるのが簡単なため、Matrix4x4を使います。

Vector3 position = _lastPlacedAnchor.transform.position;
Quaternion rotation = _lastPlacedAnchor.transform.rotation;
Vector3 scale = Vector3.one;

Matrix4x4 cloudAnchorMatrix = Matrix4x4.TRS(position, rotation, scale);
Matrix4x4 inverseCloudAnchorMatrix = cloudAnchorMatrix.inverse;

Matrix4x4.TRSを使うとCloud AnchorのtransformからMatrix4x4を作れます。また、作成したMatrix4x4の.inverseで逆変換のための行列を取得できます。

他のデバイスに位置を送る際はinverseの方を使い、受け取った座標を自分のデバイスの座標系に戻す際は元のMatrix4x4を使います。

// Cloud Anchor座標系の位置、向きを求める(位置を送る側)
Vector3 Position = inverseCloudAnchorMatrix.MultiplyPoint3x4(position),

// Cloud Anchor座標系の位置を復元する(位置を受け取る側)
Vector3 position = cloudAnchorMatrix.MultiplyPoint3x4(Position)

この行列さえあれば、1つのCloud Anchorでオブジェクトの空間共有ができます。

オブジェクトの共有

Exampleに倣い、NetworkServerとNetworkClientを使ってデータを送受信します。

Networkingの前準備

NetworkClientからNetworkClientにデータを送信するための処理を追加していきます。

まず、オブジェクトの位置を受信するためのMsgTypeを定義します。 ExampleのRoomSharingMsgType.csを使うのであれば、1行追加するだけです。

using UnityEngine.Networking;

public struct RoomSharingMsgType
{
    public const short MakeItem = MsgType.Highest + 3; // 追加
}

次に、送受信するMassageを定義します。 今回は共有するオブジェクトのPositionとどちらをLookAtで回転をさせるためのForwardをVector3で送信します。

using UnityEngine.Networking;

public class MakeItemMessage : MessageBase {
    public Vector3 Position;
    public Vector3 Forward;
}

最後にMessageを受信した際の処理をNetworkClient側で登録します。

_roomSharingClient.RegisterHandler(RoomSharingMsgType.MakeItem, _OnMakeItem);

これで、NetworkServerからのMessageを受信する準備ができました。

NetworkClientの送信処理

Cloud AnchorのHostおよびCloud Anchor座標系に変換するMatrix4x4を作成してある前提です。

NetworkServerのデバイス座標系でtransformを持つitemを共有します。

// Messageの作成(itemのtransformをMatrix4x4で変換)
MakeItemMessage makeItemMessage = new MakeItemMessage {
    Position = inverseCloudAnchorMatrix.MultiplyPoint3x4(item.transform.position),
    Forward = inverseCloudAnchorMatrix.MultiplyPoint3x4(item.transform.position + item.transform.forward)
};

// すべてのClientにMessageを送信
NetworkServer.SendToAll (RoomSharingMsgType.MakeItem, makeItemMessage);

先の通り、送信側はCloudAnchorMatrixのinverseで変換をかけてから位置を送信しています。

【追記】当初は、Forwardを`.MultiplyPoint3x4(transform.forward)で計算し、Direction として渡す実装を想定していましたが、垂直面にCloud Anchorが置かれたときの実装が複雑になるため、position+forwardをLookAtのPointとして渡す形に変えました。

NetworkServerの受信処理

同様に、Cloud AnchorのResolveおよびCloud Anchor座標系に変換するMatrix4x4を作成してある前提です。

private void _OnMakeItem(NetworkMessage networkMessage)
{
    var response = networkMessage.ReadMessage<MakeItemMessage>();

    GameObject item = (GameObject)Instantiate (itemPrefab);
    item.transform.position = cloudAnchorMatrix.MultiplyPoint3x4(response.Position);
    item.transform.LookAt(cloudAnchorMatrix.MultiplyPoint3x4(response.Forward));
}

Messageで受信したVector3はCloud Anchor基準の座標になっているので、Cloud Anchorから作成したMatrix4x4で座標変換をかけてから使います。

【追記】送信側でForwardをLookAtのPointに変更したため、受信側はLootAtに復元したPointをセットするだけになります。

動作デモ

AndroidとiPhoneでCloud Anchorを共有した後、Matrix4x4の座標変換とNetworkServer、NetworkClientを使って、一方のデバイスで配置したオブジェクトをもう一方のデバイスにも表示させます。

平面検知の結果に差異があるのと、移動するキャラクターの同期を取っていないのでわかりにくいですが、空間上に置いたオブジェクトが共有されており、位置関係の整合が取れているのがわかるでしょうか。

オブジェクトの配置はローカルネットワーク内の通信のみとなるため、ほぼリアルタイムでClientに反映され、Cloud Anchorを共有する処理のようなラグは感じません。

まとめ

大事なことなのでもう一度、Cloud Anchor APIは回数制限があり、共有に時間もかかるので、Roomに1つで大丈夫です。
他のオブジェクトはCloud Anchorを基準に座標変換してあげれば同じ位置に置けます。

座標変換は理解が難しく、初めて実装する際は混乱しやすいです。いざ実装してみるとイメージと挙動が異なることが何度かあり、かなり試行錯誤しましたが、変換できてしまえばこっちのものなので、そういうものだと割り切ってしまうのも手だと思います。

デモでもあったように、課題としては平面検知の結果がデバイスによって異なる点です。平面の有無で見え方や動き方が変わってしまうのですが、平面をすべて共有するのはどうかと思うので悩みどころです。
もし、オクルージョンやコリジョンを必要としないなら、いっそ平面は使わないようにして、HitTestでオブジェクトを置くだけにした方がキレイに見えると思います。

あとがき

座標系変換にも苦戦しましたが、『AndroidとiOSのプラットフォームを切り替えて交互にビルド』『1人でデバイス2台を持っての実機テスト(シングルマルチプレイ)』が割とつらいです。

前者については、どちらか一方のプラットフォームでデバイスが2台あれば、ネットワーク周りを作り切ってしまってから水平展開が良いと思います。
後者は、個人で開発しているので仕方ないんですけど・・・テスト手伝ってくれる人がいない哀しみ。

最後に、作成したデモアプリではクエリちゃんアセットを使用しています。
ライセンスの詳細はこちらです。
DOWNLOAD | クエリちゃん公式サイト

f:id:jyuko49:20180617214324p:plain