じゅころぐAR

AR/VRメインのブログ。時々ドローン。

ARKitのARObjectAnchorをUnityで使ってみる

ARKit2の新機能として追加されたARObjectAnchorをUnityで試してみます。

blogs.unity3d.com

ARObjectAnchorとは

現実の物体(オブジェクト)を検知して、Anchorの作成・トラッキングを行う機能です。
予め作成しておいたARReferenceObjectを元に検知を行います。

f:id:jyuko49:20180805172842p:plain

開発環境

ARKit2で追加された機能のため、iOS12およびXcode10が必要になります。
本記事執筆時点ではβ版のため、Apple Developerからダウンロードしました。

Unity ARKit Pluginのインストール

Unity ARKit PluginはBitbucketのから"arkit2.0_beta"ブランチをダウンロードします。

Unity-Technologies / Unity-ARKit-Plugin / Downloads — Bitbucket

ダウンロード後、Assets以下のUnityARKitPluginをUnityプロジェクトのAssetsにフォルダごとコピーして使います。

f:id:jyuko49:20180805171735p:plain

ARKitを使うのに必要なUnityの設定については割愛します。"Camera usage description"を忘れなければ、なんとかなると思います。

ARReferenceObjectを作成する

検知したいオブジェクトをスキャンして、ARReferenceObjectを作成します。
ここが上手くできないと検知の精度が落ちるので、重要な作業です。

UnityARKitPluginにExamplesとして付属しているUnityObjectScannerをビルドすると、スキャン用のアプリが起動します。

UnityARKitPlugin > Examples > ARKit2.0 > UnityObjectScanner

BoundingBoxを対象オブジェクトに合わせる

アプリ起動後の手順として、まずはBoundingBoxでスキャン対象のオブジェクトを囲み、Axisをオブジェクトの中心に合わせます。

f:id:jyuko49:20180805222935p:plain

基本的な操作方法は、以下の通り。

BoundingBoxの移動:オブジェクト付近の平面をタップ
BoundingBoxの伸縮:BoundingBoxの面をタップしてドラッグ
BoundingBoxの平行移動:Axisをタップしてドラッグ
BoundingBoxの回転:画面を二本指タップした状態で回転

対象オブジェクトをスキャンする

BoundingBoxの位置を合わせたら、その状態で対象オブジェクトを複数の方向からスキャンします。"Create Objects"、"Detect Objects"のボタンを押す必要はありません。カメラをかざすことで、オブジェクト上のPointCloudが特徴点として検出されていきます。

ARReferenceObjectを保存する

スキャンが完了したら、アプリ上のUIでARReferenceObjectを作成・保存します。

"Create Objects"でBoundingBox内の特徴点からARReferenceObjectが作られ、リストに追加されます。 この処理は多少時間がかかることがあり、画面中央のウィンドウにファイル名のリスト(objScan_0,objScan_1,...)が表示されたら、作成完了した合図です。
最後に、"Save Objects"でARReferenceObjectをファイルとしてデバイスに保存します。これを忘れると、せっかくスキャンした結果を利用できなくなります。

f:id:jyuko49:20180805225206p:plain

他のボタンは以下の用途に使えます。スキャンだけを行う場合、必須ではないです。

"Detect Object"は検知モードのON/OFFです。スキャン結果を用いた検知のテストに使えます。
"Clear Objects"は"Create Objects"で作成したARReferenceObjectを全て削除します。

ARReferenceObjectをコピーする

iTunesのファイル共有からコピーするのが簡単です。

スキャンを行ったデバイスMacに接続して、iTunesでファイル共有メニューを開きます。

f:id:jyuko49:20180805231521p:plain

アプリが作成したファイルに"ARReferenceObjects"という名称のフォルダがあり、ARReferenceObjectが保存されています。
フォルダごとMacにコピーしてFinderで開くと、拡張子が.arobjectのファイルがあるはずです。
このファイルをUnityにコピーして使います。

f:id:jyuko49:20180805232614p:plain

ARKitScannerを使ったARReferenceObjectの作成

スキャンを行う方法としてはもう一つ、Apple公式のサンプルコードをXcodeでビルドしたARKit Scannerが使えます。

Scanning and Detecting 3D Objects | Apple Developer Documentation

スキャンの流れはUnityのサンプルと同じですが、BoundingBoxの作成、Scan、Test、ファイルの保存・共有とステップを踏んでいくUIとなっており、UnityObjectScannerよりもわかりやすいです。

f:id:jyuko49:20180805221927p:plain

スキャンの進捗に応じてパーセンテージが表示され、スキャンを行った面はBoundingBoxが着色される機能もあるため、全方向から満遍なくスキャンを行えます。
100%になっていなくても"Finish" > "Cancel"(Continueのキャンセル)でスキャンを終了できます。

f:id:jyuko49:20180805221955p:plain

最終的に作成されるのは.arobjectファイルなので、Unityでも使えます。
ファイルの転送にAirDropが使える点も便利です。

ARObjectAnchorを使う

作成したARReferenceObjectを使って、ARObjectAnchorの作成、コンテンツの重ね合わせを行います。

Unity ARKit PluginでARObjectAnchorを使うには、UnityARCameraManagerの"Detection Objects"ARReferenceObjectsSetAssetをセットします。

f:id:jyuko49:20180805235927p:plain

ARReferenceObjectsSetAssetは、プロジェクトウィンドウで右クリックから"Create" > "UnityARKitPlugin" > "ARReferenceObjectsSetAsset"で作成できます。

f:id:jyuko49:20180805165517p:plain

ARReferenceObjectsSetAssetには、検知対象のARReferenceObjectAssetを複数セットできます。

f:id:jyuko49:20180806001817p:plain

ARReferenceObjectAssetもプロジェクトウィンドウで右クリックから"Create" > "UnityARKitPlugin" > "ARReferenceObjectAsset"で作成できます。

f:id:jyuko49:20180806002206p:plain

ARReferenceObjectsAssetには、スキャンで作成した.arobjectファイルをARReferenceObjectとしてセットします。

ここまでで、ARSessionに検知対象のARReferenceObjectが設定され、ARObjectAnchorが作成されるようになります。

作成されたARObjectAnchorを使ってARコンテンツを重ねるにはスクリプトを使います。UnityARObjectAnchorで利用されているGenerateObjectAnchor.csが参考になります。

まず、Start()でARObjectAnchor作成時、更新時に実行するメソッドを定義します。

void Start () {
    UnityARSessionNativeInterface.ARObjectAnchorAddedEvent += AddObjectAnchor;
    UnityARSessionNativeInterface.ARObjectAnchorUpdatedEvent += UpdateObjectAnchor;
}

ARSessionでオブジェクトが検知されると、定義したメソッドが呼ばれ、ARObjectAnchorが渡されます。
作成時に呼ばれるメソッド(AddObjectAnchor())では、ARObjectAnchorからposition、rotationを取得して、GameObjectを生成(Instantiate)しています。

void AddObjectAnchor(ARObjectAnchor arObjectAnchor)
{
    Debug.Log ("object anchor added");
    if (arObjectAnchor.referenceObjectName == referenceObjectAsset.objectName) {
        Vector3 position = UnityARMatrixOps.GetPosition (arObjectAnchor.transform);
        Quaternion rotation = UnityARMatrixOps.GetRotation (arObjectAnchor.transform);

        objectAnchorGO = Instantiate<GameObject> (prefabToGenerate, position, rotation);
    }
}

検知したオブジェクトの位置が更新されると、更新されたARObjectAnchorが渡されます。
更新時のメソッド(UpdateObjectAnchor())では、生成したGameObjectの位置をARObjectAnchorの更新に合わせて上書きしています。

void UpdateObjectAnchor(ARObjectAnchor arObjectAnchor)
{
    Debug.Log ("object anchor added");
    if (arObjectAnchor.referenceObjectName == referenceObjectAsset.objectName) {
        objectAnchorGO.transform.position = UnityARMatrixOps.GetPosition (arObjectAnchor.transform);
        objectAnchorGO.transform.rotation = UnityARMatrixOps.GetRotation (arObjectAnchor.transform);
    }
}

基本的には、上記のサンプルコードと同様にGameObjectの生成、位置の更新を行えばARコンテンツが現実のオブジェクトに重ねて表示され、対象が動いても追従するようになります。
複数のオブジェクトを検出しつつ、異なるARコンテンツを表示したい場合は、ARReferenceObjectAssetに設定したObjectNameがARObjectAnchorにセットされるため、何を検知したかを名称によって制御できます。

アプリで動かしてみる

まず、UnityARObjectAnchorと同様にAxisを表示してみます。

スキャンしたARReferenceObjectの精度にもよりますが、検知に数秒かかるようなこともなく、オブジェクトにカメラをかざすとすぐに検知してくれます。

続いて、ARコンテンツを3D Textに変更して、追従のテストを行ってみました。

検知対象のオブジェクトが移動すると、テキストも移動していることがわかります。

まとめ

ARKit2で追加されたARObjectAnchorは、現実の物体をARで拡張するのに適した機能です。
ARReferenceObjectのスキャンがやや面倒ではありますが、アプリの操作に慣れれば数分で作成できます。一度作成してしまえば、どのアプリでも使えますし、ファイルを共有することで同じ物を持っている人も使えます。

用途としてイメージしやすいのは、今回試したようにフィギュアや模型にエフェクトやUIを重ねる使い方でしょうか。市販されている製品であれば、ARReferenceObjectをインターネットで配布・共有できる点も魅力です。
また、物体の移動を検知できるので、チェスのような物の配置によって局面が変わるようなゲームにも使えそうです。

立体的な特徴が少なく検知が難しい場合、平面画像を検知・追従するARImageAnchorが代用できます。

『Unreal Engine 4 ゲーム開発入門』はこれからUEを始める人にオススメしたい

前回に引き続きUEを勉強中ですが、購入した書籍がよかったのでレビュー記事を書きました。

"リズちゃんが表紙のUnreal Engine入門書"、略して"リズ本"!!
UEを始めるなら、これは買うしかないと思っていました。
※書籍版とKindle版があります

「リズちゃん? 誰?」という方はこちら
弊ブログでも度々登場しているクエリちゃんの妹です。

オススメする理由

クエリちゃん推しだからだけではないよ。本当だよ。

著者が専門学校の先生

著者の荒川さんは普段からゲーム開発を教えている方だそうで、説明が非常にわかりやすいです。

UEだけではなく、Unityの入門書も執筆されていますね。

www.amazon.co.jp

画面キャプチャが多い

フルカラーになっており、画面キャプチャによる解説が多いです。
紙面の半分が画面キャプチャと言っても過言ではないです。

UEがブループリントで開発できる点もキャプチャでの説明に合っていて、コード(スクリプト)例などは全然出てこないですし、迷ったらキャプチャの通りに操作すればいいです。

開発に必要な要素が詰まっている

UEの操作方法に始まり、ブループリント、アニメーション、コリジョン、ナビメッシュ、UI作成、パッケージ化まで、初めてアプリを作るのに最低限必要な要素をしっかり押さえている感じです。

Webサイトで情報が更新されている

バージョン対応、正誤表、追加情報などが著者の運営するサイトで公開されています。

uebeginner.jp

サンプルプロジェクトの完成品やモデルデータもここからダウンロードできます。
また、書籍には収録されていなかった音関連(BGM、ボイス、SE)の情報も公開されています。

最新のバージョンには追従されていないこともありますが、そこは自分で検索して調べれば十分対応できました。例えば、被破壊性メッシュの作成が見当たらないケースでは"被破壊性メッシュ ue4"で検索できます。

非破壊性メッシュの作成について - UE4 AnswerHub

リズちゃん!

クエリちゃんではなくリズちゃんを表紙に起用したあたりにセンスを感じます。
なお、書籍内で開発するサンプルアプリの主役もリズちゃんです。

実際、動いてるリズちゃんは、なかなか可愛い。

クエリちゃんモデルは、ちょっぴりセクシーな感じに仕上がっております。

というか、UnityではSDクエリちゃんを使っていたので、セクシーさの概念はなかった。

f:id:jyuko49:20180224222021j:plain

感想

Unreal Engineの第一印象として、Unityよりもビューが豊富で機能も多そうなので、慣れるまで時間がかかりそうかなと思っていました。
でも、この本を読みながらUEの機能を色々さわっていると、なんとなくできた気分になってきます。

これって実は大事なことで、最初の一歩で躓くと、そこでもう諦めてしまうケースって多いと思うんですね。
初心者は情報量が多いと逆に混乱しますし、苦手意識が払拭されるだけで入門書としての役割は果たしていると思うので、対象読者とコンセプトがはっきりした良書という感じがしました。

Unreal EngineでARをやってみる

ARの開発環境では、Unityがデファクトスタンダードだと思いますが、Unityに使いにくさを感じることもあり、Unreal Engineも試してみることにしました。

そもそもなんでUnityを使ってたんだっけ?

ARを始めた当初(といっても1年半くらい前ですが)、最新のARデバイスと言えばHoloLensとTangoで、ネイティブ環境を除くとどちらも開発環境がUnity一択でした。

元々はUnityを一度も触ったことがなくて、積極的に選択したという訳ではなかったと思います。

Unityのつらみ

あくまで経験1年ちょっとの素人目線ですが、公式のスクリプトリファレンスが超絶わかりにくいです。
メソッドやプロパティが何をするためのものなのか、説明が1,2行でざっくりとしか書かれていないので、名称と型から雰囲気で察しながら実装しています。
一体、みんなどうやって開発しているのか...

当然、意図通りに動かないことも多く、有益な情報がないかとググる訳ですが、出てくる情報が軒並み古かったりするんですよね。

今までは、TangoやARCoreのドキュメント、サンプルコードを読み漁って対応していましたが、それ以上のことをやろうとすると想像以上に苦戦します。

というわけで、UEも使えるようになりたい。

Unreal EngineのAR対応

UEでARを利用するためのプラグインは、UEにビルトインされているようです。
"設定" > "Plugins" > "Augmented Reality"プラグインが見つかるので、"Enabled"にチェックを入れれば有効になります。

f:id:jyuko49:20180707081534p:plain

ビルトインで楽ではあるんですけど、ARKit/ARCoreのバージョンが上がったら、UEのバージョンも上げないとってことですよね?

この記事を書いている時点で、UEの最新バージョンは4.19、プレビュー版の4.20が出ている状況です。
UEのAR対応についての記事を見ると、ARKit 1.5、ARCore 1.2に対応しているのは4.20以上になっているので、プレビュー版を使わないと最新の機能は使えないみたいです。

Augmented Reality Overview

バージョンの追加は、Epic Games Launcherから簡単にできます。1バージョンで15GBくらいあるけどね…

f:id:jyuko49:20180707082724p:plain

ARKit

UE 4.20であれば、ARKit 1.5だけでなくARKit 2.0にも対応しているようです。

ARKit Prerequisites

ARCore

UE 4.20がARCore 1.2に対応しています。

ARCore Prerequisites

ARCoreに関しては既にv1.3.0が出ているのですが、UEのサイトには記述が見当たりません。
ARCoreのサイトを見てみると、GoogleがforkしたバージョンのUEをGithubからインストールしろと書かれています。

Quickstart for Unreal  |  ARCore  |  Google Developers

f:id:jyuko49:20180707075756p:plain

仕方ないので、リンク先のGithubをクリックしてみると…

f:id:jyuko49:20180707080023p:plain

ちょっと意味がよくわからないですね。

UEでARプロジェクトを作る

プロジェクトテンプレートから作る

UEにはARプロジェクトのブループリントがあり、簡単に開発をスタートすることができます。

f:id:jyuko49:20180707091825p:plain

手順としては、公式のQuick Startをそのままやっていくだけです。

Augmented Reality Quick Start

基本のビューは構成されており、ARKit/ARCoreのプラグインもデフォルトで有効になっているので、プラットフォーム(iOS/Android)の設定をして、デバイスで起動するだけです。
Androidの場合、SDKのセットアップで少し迷いましたが、以下の記事に載っているCodeworksを使ったら問題なく動きました。

Installing CodeWorks for Android 1R6u1

せっかくなので、どんな感じで動いているかも見てみました。

"コンテンツ" > "HandheldARBP" > "Blueprints" > "GameFramework"にある"BP_ARPawn"をダブルクリックします。

f:id:jyuko49:20180707105658p:plain

ブループリントのイベントグラフが表示されて、処理のフローが見れます。

f:id:jyuko49:20180707104220p:plain

メインフローとして、TouchイベントからHitTestが実行され、最終的にHitResultを受け取ったBP_PlaceableがSpawn(生成)されていることがわかります。
まだ全然慣れてないですが、処理のフローが可視化されているのはよいですね。

ちなみに、プルンと出てくる処理の正体は、BP_PlaceableのIntro Animのタイムラインアニメーションでした。

"コンテンツ" > "HandheldARBP" > "Blueprints" > "Placeable" > "BP_Placeable"をダブルクリックでイベントグラフが表示され、さらにIntro Animのノードをダブルクリックでタイムラインエディタが開きます。

f:id:jyuko49:20180707155733p:plain

f:id:jyuko49:20180707161532p:plain

ARCoreのサンプルプロジェクトを使う

UEのテンプレートとは別に、GoogleGithubにARCore SDK for Unrealリポジトリがあります。

github.com

こちらをgit cloneするか、zipファイルをダウンロードして解凍すれば、ARCoreのサンプルプロジェクトをベースに開発ができます。

プロジェクトごとにフォルダが作られており、直下にある.uprojectのファイルを開きます。

f:id:jyuko49:20180707112055p:plain

開くとUEのバージョン選択が表示され、Unreal Editorが起動します。

f:id:jyuko49:20180707112827p:plain

初回起動時にPluginの追加を促すコーションが表示された場合は"Yes"で起動すればOKです。

f:id:jyuko49:20180707113218p:plain

起動した直後は、Androidプラットフォームが有効になっていないので、Quick Startと同様の手順で設定します。
Androidであれば、"設定" > "プロジェクト設定" > "Android"でプラットフォームを有効にします。

f:id:jyuko49:20180707163110p:plain

実機で起動してみると、Unity版と同じようなサンプルアプリが動きました。

これならUEでも開発できるかなと思ったのですが、Cloud Anchorのサンプルに相当する"CloudPin"だけは起動しようとするとエラーになります。

f:id:jyuko49:20180707172929p:plain

UEのバージョンを4.20にしてもダメ。Not FoundになっていたGithubからARCore 1.3に対応したUEをインストールすればできるのかもしれませんが、Not Foundですし。

そもそも、Cloud AnchorはARCore 1.2で動くはずなのですけど、どうにも上手くいきません(諦め)

その他UE関連

ヒストリアさんのブログが半端ない

UEについて色々調べていたら、ヒストリアさんのブログの情報量がすごかったです。
これ読めば、UEかなり詳しくなりそう。

historia.co.jp

ありがとうの気持ちを込めて、Airtoneを宣伝しておく。

www.youtube.com

Unity引っ越しガイド

Unityの用語との対応表がありました。Unityで行なっていた操作がUEでどうやるのかわからないときは、見比べながら進めていけばなんとかなりそうです。

api.unrealengine.com

まとめ

  • ARKit/ARCoreの最新バージョン対応がUnityに比べると遅い
  • ブループリントは慣れれば便利そう

プレビュー版じゃないと最新のAPIに対応していない上、ARCore 1.3とCloud Anchorに至っては、すんなり試せなかったのが致命的です。
ブループリントは使いこなせれば便利だと思いますけど、慣れるまでに時間もかかりそう。面倒な処理はスクリプトをゴリゴリ書いていく派なので、その点もちょっと合わないかもしれません。

地味なところだと、AndroidiOSのプラットフォーム切り替えが要らないので、クロスプラットフォーム開発が楽です。

結論

UEよさそうだけど、ARならまだUnityかな・・・。

実際に試してみて、AR(特にモバイルAR)に関しては、依然としてUnityがファーストチョイスかなぁと思いました。
もしVRをメインでやっていくなら、UEを勉強する気がします。

ARKit/ARCoreやサードパーティ製のプラグインを使ってスクリプトを書いていきたい場合は、Unityで良さそうです。Unity自体の機能は、理解することを諦めた。
逆にUE使う場合は、ARの機能やスクリプトに頼る部分はシンプルにして、アニメーションやエフェクトなどグラフィックにこだわるとか、そういった使い方になると思います。

今後はWebARの利用範囲が徐々に増えていくと思うので、いずれはUnity/UEじゃなくて、Webと同じプラットフォームで開発していくことになるかと。

jyuko49.hatenablog.com

ARCoreのビルドに失敗したらadbのserverプロセスをkillしてみるべし

ARCore v1.2.0を使っているプロジェクトにv1.3.0をインポートしたら、いきなりビルドエラーが出た。

f:id:jyuko49:20180622075937p:plain

うん、このエラーには見覚えがある。

対処法

adbのserverプロセスが悪さをしていることがあるので、ターミナルを起動してkillして差し上げる。

$ ps aux | grep adb
$ kill [プロセス番号]

adbコマンドが使えれば、以下コマンドでも同じはず。

$ adb kill-server

実際にやってみる

$ ps aux | grep adb
jyuko            10742   0.4  0.0  4339252   3820   ??  Ss    8:25AM   1:39.03 adb -L tcp:5037 fork-server server --reply-fd 4
jyuko            10559   0.0  0.0        0      0   ??  Z     8:25AM   0:00.00 (adb)
jyuko            14536   0.0  0.0  4267768    896 s000  S+    7:55AM   0:00.00 grep adb

$ kill 10742

ビルドできた!

どうして空間共有で先に座標系の逆変換(inverse)を行うのか?

先日、Cloud Anchorを使ったオブジェクト共有の記事を書きましたが、Matrix4x4を使った座標変換の説明で若干モヤっとしなかったですか?
Cloud Anchorを基点にした座標系で位置を伝えるのに、なんで送信側がCloud Anchor座標系への逆変換(inverse)を使うのかです。

正直、記事を書いている途中も半信半疑(でも実装上は正しい)という状態だったのですが、何か降りてきたようで、今ならわかりやすく説明できる気がする!

やりたいこと

Andy君がCloud Anchor、オレンジのピンが共有したい地点だとします。(説明を簡単にするため、とりあえず回転は考えない)

f:id:jyuko49:20180621125708p:plain

座標系が異なる他のデバイスに正しく位置を教えるには、Cloud Anchor(Andy君)から見てココ!というCloud Anchor座標系で位置を伝える必要があります。(下図の赤いベクトル)

f:id:jyuko49:20180621125811p:plain

座標変換には行列を使うのが簡単で、UnityではMatrix4x4を使えば良さそうです。

正変換→逆変換:NG

まず、間違えた実装の方から。

Matrix4x4にCloud Anchorのposition、rotationを与えると、デバイス座標系→Cloud Anchor座標系の変換を行う行列ができます。
Cloud Anchorの座標系に変換する行列なんだから、これで変換をかけた座標を渡せばいけるでしょ!
と、最初は思ってました。

実際に変換すると、デバイス→Cloud Anchorの座標変換と同じだけピンを移動(青い矢印)させた地点の座標が返ります。

f:id:jyuko49:20180621125610p:plain

結果としては上図の赤い矢印で、最初に求めたかったベクトルと違います。

この座標を別のデバイスに伝えて、逆変換をしてみましょう。
Cloud Anchorの座標系から見た座標が今求めたベクトルなので、Cloud Anchorを基準にデバイスの座標系に変換すれば、元に…

f:id:jyuko49:20180621125615p:plain

戻らないですね!(・∀・)

実機テストでこれだけ盛大に位置がズレたら、流石に計算が間違っていると気付きました。

逆変換→正変換:OK

それならばと送信側でMatrix4x4.inverseによる逆変換をかけてみました。押してダメなら引いてみろ理論です。

逆変換なのでオレンジのピンの座標に対して、Cloud Anchor→デバイスの変換(青い矢印)がかかります。

f:id:jyuko49:20180621125612p:plain

結果として得られる座標は、Cloud Anchorから見たピンの位置(求めたかったベクトル)をデバイス座標系で表したものです。

この座標を別のデバイスに伝えて、さらにCloud Anchor座標系に変換します。
今求めた座標をデバイス→Cloud Anchorで変換すると、

f:id:jyuko49:20180621125613p:plain

できてそう!!

まとめ

理論から入るのもいいですけど、とりあえず動く実装を見つけるのも大事ですね。
正しく動いていれば、理論は後から付いて来ます。

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アドレス

Cloud Anchors Developer Guide for Unity  |  ARCore  |  Google Developers

適用対象の"Project"がCloud Anchor APIAPIキーを取得するために作った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をセットするだけになります。

動作デモ

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

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

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

まとめ

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

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

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

あとがき

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

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

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

f:id:jyuko49:20180617214324p:plain

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

ARCore v1.2でCloud Anchor APIが追加されてから早1ヶ月が経ってしまいました。
色々試して理解も深まってきましたし、ARKit v2で同様の機能も実装されるので、仕組みや基本的な使い方を一度整理しておきます。

ARCore v1.2全体の概要については、前回の記事にまとめてありますので、そちらを参照ください。

Cloud Anchor APIの基本

概要

Cloud Anchorで言うところのCloudは、GCP(Google Cloud Platform)になります。アプリ上で作成したAnchor(アンカー)はGCPで提供されているAPIを介して共有されます。

Anchorを複数デバイスで共有するためのネットワーク部分は、UnityのNetworking API(通称UNET)が使われています。

用語の整理

  • Cloud Anchor API
    • Cloud Anchorを共有するためのAPI
  • Cloud Anchor
    • 上記APIを介してCloudに登録され共有できるAnchor
  • Hosting
    • Cloud Anchor APIに対して、Cloud Anchorを登録する処理
    • UNETのHostと紛らわしい
  • Resolving(Resolve)
    • Cloud Anchor APIからCloud Anchorを取得し、空間上の座標に合わせる処理

GCP(Google Cloud Platform)とは?

名前の通り、Googleが提供しているクラウド・プラットフォームです。

cloud.google.com

AWSGoogle版みたいなものですが、AWSはWebコンソールで簡単に操作できるマネージドサービスが充実しているのに対し、GCPはアプリケーションに組み込んで使うWeb API群が充実している印象です。

Cloud Anchor APIについては、Web APIを直接叩くわけではなく、ARCoreの各種SDKを介して利用する形になります。

Anchorとは?

元々の単語は船が流れないように固定する錨の意味で、一般的にはAR空間上で位置を固定するために使います。

ラッキングに利用されるジャイロセンサの特性として時間とともに誤差が蓄積していくため、画像検知などで誤差を補正する必要があります。その際に既に空間上にあるオブジェクトにも同様の補正をかけないと、当然ズレて見えます
Anchorが付与されているオブジェクトは空間上の位置が固定されたものとして扱われ、自動で補正が行われます

Anchorについては、以下のURLにリファレンスがあるので目を通しておくとよいと思います。

Working with Anchors  |  ARCore  |  Google Developers

Cloud Anchorは上記の用途にプラスして、同じ空間にいる複数のデバイスで同じ位置を示すために使われます。

UNETとは?

マルチプレイを実装するためのUnity標準のAPIです。サードパーティ製の類似製品としてはPhotonが有名です。

マルチプレイでは、複数のデバイスのうちの1台がHostとなり、Serverとしての処理を行うと同時にLocal Clientとしてプレイヤーとしての処理も行います。
他のデバイスはRemote Clientとして、Hostと通信を行うことでServerとやり取りを行うプレイヤーになります。

https://docs.unity3d.com/uploads/Main/NetworkHost.png

サーバ(Server)とクライアント(Client)のやり取りを実装するAPIは、機能ごとにレイヤ分けされたHLAPI(High Level APIとして提供されています。

https://docs.unity3d.com/ja/current/uploads/Main/NetworkLayers.png

公式のマニュアルやUNETを取り扱った記事の多くは、"Network Manager""Network Transform"、"Network Identity"といった比較的上位レイヤのAPIを使っていますが、Cloud AnchorのExampleでは、Connection Managementレイヤの"Network Server"、"Network Client"が使われています。

Cloud Anchor APIを使うための準備

GCPでCloud Anchor APIを有効にする

GCPをはじめて利用する場合には、GCPの利用登録が必要です。

cloud.google.com

GCPへの登録には、Googleアカウントを使います。 登録が済んだら、適当な名前でプロジェクトを作ってください。

GCPではプロジェクト毎に使用するAPIを有効にして使います。
APIを有効にするには、"APIとサービス"から"APIとサービスの有効化"を行います。

f:id:jyuko49:20180514215238p:plain

利用できるAPIはたくさんあるので、検索窓にAPI名を入力して対象を絞り込みます。"ARCore Cloud Anchor API"と入力すれば1件だけヒットするはずです。

f:id:jyuko49:20180514224541p:plain

APIの詳細画面で"有効"ボタンをクリックすれば、プロジェクト内でAPIが利用可能になります。

f:id:jyuko49:20180514230140p:plain

GCPAPIキーを取得する

"Cloud Anchor API"にリクエストを行うには、APIキーが必要になります。

APIを有効にしたプロジェクトの"APIとサービス" > "認証情報"で"認証情報を作成"をクリックし、APIキーを選択すると、すぐにAPIキーが発行されます。

f:id:jyuko49:20180514225733p:plain

f:id:jyuko49:20180514231808p:plain

APIキーの名称変更やキーの制限は編集画面で行えます。アプリケーションの制限とAPIの制限を設定しておけば、万が一APIキーが漏洩した際に不正利用されるリスクを減らせます。

f:id:jyuko49:20180514232835p:plain

Unityのセットアップを行う

まずは、ARCore SDK for Unityのインポートです。
Githubに各バージョンのunitypackageがあるので、v1.2をUnityのプロジェクトにインポートします。

Releases · google-ar/arcore-unity-sdk · GitHub

次にAPIキーをUnityエディタ上で設定します。
入力フィールドは、"Edit" > "Project Settings" > "ARCore"と選択していくと表示されます。

f:id:jyuko49:20180510005537p:plain

ARCoreの基本設定を行う

Cloud Anchorに限らず、ARCoreを利用したアプリをビルドするためにはUnityで設定すべき項目があります。
ARCoreのチュートリアルAndroidiOSそれぞれ書かれているので忘れずにチェックしておきましょう。

Quickstart for Android  |  ARCore  |  Google Developers
Quickstart for iOS  |  ARCore  |  Google Developers

シーンの基本設定は変わっていないので、過去記事を参考に構成していってください。

jyuko49.hatenablog.com

以前のバージョンから変わっているのは、ARCoreの"Session Config"です。

f:id:jyuko49:20180514230231p:plain

f:id:jyuko49:20180605223948p:plain

ARCore v1.2で追加された垂直面(Vertical Plane)の検知、Cloud Anchorの項目が増えているので、有効になっていないと機能が使えません。

ARKitのクロスプラットフォーム対応を行う

AndroidiOSの空間共有はCloud Anchorの目玉でもあるので、ARKit対応がどう実現されているかについても軽く触れておきます。

常套手段ではありますが、シーン上にARCore用とARKit用のGame Objectがそれぞれ用意されており、初期状態は非アクティブになっています。

f:id:jyuko49:20180605225132p:plain

ARKit用のGame Objectは、Unity ARKit Pluginでトラッキングや平面検知が行えるように構成され、さらにARCore Sessionが付与された形になっています。

f:id:jyuko49:20180605232749p:plain

2つのGame Objectは、実行時のプラットフォームに応じて必要な方だけがスクリプトでアクティブになります。

public void Start()
{
    if (Application.platform != RuntimePlatform.IPhonePlayer)
    {
        ARCoreRoot.SetActive(true);
        ARKitRoot.SetActive(false);
    }
    else
    {
        ARCoreRoot.SetActive(false);
        ARKitRoot.SetActive(true);
    }
}

この切り替えでハマりがちなのが、Cameraの"Main Camera"タグです。

f:id:jyuko49:20180605230309p:plain

"Main Camera"タグが付いたObjectがシーン上に2つあると、一方が非アクティブでも意図しない動きをすることがあったので、Unityエディタで"Switch Platform"を行う際に"Main Camera"タグも付け替えています。
あまりスマートな運用ではないので、良い方法を思いついたら変えるかもしれません。

上記の対応で、AndroidiOSのどちらでビルドしても基本的には動きます。
あとは、Anchorの作成や空間上へのHitTestなどをスクリプトで行う際に、処理を分岐させてあげればOKです。

Cloud Anchorの使い方

ARCore SDK for UnityのExamplesに沿って説明します。

処理の流れ

バイスAをホスト(サーバ)、デバイスAと同一ネットワーク上にあるデバイスBをクライアントとします。

Exampleを動作させるには、以下の手順が必要です。

  1. バイスA(ホスト)がRoomを作る
  2. バイスA(ホスト)で共有したい空間を検知する
  3. バイスA(ホスト)がCloud AnchorをHostingする
  4. バイスB(クライアント)で共有したい空間を検知する
  5. バイスB(クライアント)でデバイスA(ホスト)が作ったRoomに接続する
  6. ResolvingでCloud Anchorの位置を復元する

よくある失敗パターンとしては、

  • 2,4の平面検知が不十分でResolvingに失敗
  • 3,6でCloud Anchor API(インターネット)との接続に失敗
  • 5でデバイス同士のリモート接続に失敗

あたりかと思います。
スクリプトと見比べながら、メッセージ表示によって切り分けができます。

CloudAnchor Exampleのスクリプト構成

ExampleにおけるCloud Anchorの共有処理は、以下のスクリプト群で構成されています。

  • CloudAnchorController.cs
    • メイン処理
  • CloudAnchorUIController.cs
    • UI操作時の処理、メッセージ表示処理
  • RoomSharingServer.cs
    • NetworkServerが行う処理
  • RoomSharingClient.cs
    • NetworkClientが行う処理
  • RoomSharingMsgType.cs
    • Server,Client間で送受信するメッセージ番号の定義
  • AnchorIdFromRoomRequestMessage.cs
  • AnchorIdFromRoomResponseMessage.cs
    • Server,Client間で送受信するメッセージのデータ定義
  • ARKitHelper.cs
    • ARCoreとARKitで実装が異なる部分のヘルパー

上記スクリプトは、すべて同一のName Space(GoogleARCore.Examples.CloudAnchor)のclassとして作られています。

1. Roomの作成

メイン処理のCloudAnchorController.csRoomSharingServer.csの参照が与えられており、Start()NetworkServer.Listen()でServerを起動しています。
また、RegisterHandler()AnchorIdFromRoomRequestのメッセージをClientから受信したらOnGetAnchorIdFromRoomRequestが実行されるようにしています。

public void Start()
{
  NetworkServer.Listen(8888);
  NetworkServer.RegisterHandler(RoomSharingMsgType.AnchorIdFromRoomRequest, OnGetAnchorIdFromRoomRequest);
}

上記の処理は、Remote Clientになる側のデバイスでも実行されていると思われます。
ただし、Serverの起動を行っているだけで、実際にServerとしてデータの送受信処理が実行されるのはHostになったデバイスのみです。

"Host"ボタンをクリックすることで、初期値:0000以外のRoomId(1〜9999)が発行されます。

public void OnEnterHostingModeClick()
{
    if (m_CurrentMode == ApplicationMode.Hosting)
    {
        m_CurrentMode = ApplicationMode.Ready;
        _ResetStatus();
        return;
    }

    m_CurrentMode = ApplicationMode.Hosting;
    m_CurrentRoom = Random.Range(1, 9999);
    UIController.SetRoomTextValue(m_CurrentRoom);
    UIController.ShowHostingModeBegin();
}

2. 空間の検知(Host)

Hostになったデバイスにて、Cloud Anchorを登録する前段階として空間検知を行います。

Cloud Anchorの位置を他のデバイスと共有する仕組みについて、Tangoで実装されていたエリアラーニングを考えると、Cloud AnchorのPose(transform)と周囲の空間情報(Point Cloudなど)をセットで登録し、同じ空間を別のデバイスで検知することでCloud Anchorの位置を推定していると推察できます。

Game Objectを置ける平面があればいいという訳でもなく、周囲の空間を検知してから、Cloud AnchorのHostingを行った方がResolvingが成功しやすくなります。

このあたりの特性は、Developer Guideにベストプラクティスとして記述されていますので、一読しておくとよいです。

3. Cloud AnchorのHosting

ExampleではGame Object(Andy君)を平面上に置いたタイミングでHostingが実行されています。

CloudAnchorController.cs_HostLastPlacedAnchor()が該当の処理です。

private void _HostLastPlacedAnchor()
{
#if !UNITY_IOS
    var anchor = (Anchor)m_LastPlacedAnchor;
#else
    var anchor = (UnityEngine.XR.iOS.UnityARUserAnchorComponent)m_LastPlacedAnchor;
#endif
    UIController.ShowHostingModeAttemptingHost();
    XPSession.CreateCloudAnchor(anchor).ThenAction(result =>
    {
        if (result.Response != CloudServiceResponse.Success)
        {
            UIController.ShowHostingModeBegin(
            string.Format("Failed to host cloud anchor: {0}", result.Response));
            return;
        }
   
        RoomSharingServer.SaveCloudAnchorToRoom(m_CurrentRoom, result.Anchor);
        UIController.ShowHostingModeBegin("Cloud anchor was created and saved.");
    });
}

通常のAnchorを作って、XPSession.CreateCloudAnchor(anchor)でCloudに登録します。

XPSession.CreateCloudAnchorに成功すると、CloudAnchorResultXPAnchorが入っており、XPAnchorにはCloudIdが振られています。

これをどう処理しているかというと、RoomSharingServer.SaveCloudAnchorToRoom(m_CurrentRoom, result.Anchor)で、RoomIdと合わせてServer処理のクラスに渡しています。

渡されたRoomSharingServerは、RoomIdとCloud AnchorをDictionaryに登録して保持しています。

private Dictionary<int, XPAnchor> m_RoomAnchorsDict = new Dictionary<int, XPAnchor>();

public void SaveCloudAnchorToRoom(int room, XPAnchor anchor)
{
    m_RoomAnchorsDict.Add(room, anchor);
}

4. 空間の検知(Remote Client)

Hostでの手順と同じです。
Hostingで検知した空間と同じ場所を検知させてから以降の処理を行うことで、スムーズにResolvingができます。

5. Roomに接続

UIで入力されたRoomId、IPアドレスに接続を試みます。

public void OnResolveRoomClick()
{
    var roomToResolve = UIController.GetRoomInputValue();
    if (roomToResolve == 0)
    {
        UIController.ShowResolvingModeBegin("Invalid room code.");
        return;
    }

    string ipAddress =
        UIController.GetResolveOnDeviceValue() ? k_LoopbackIpAddress : UIController.GetIpAddressInputValue();
         
    UIController.ShowResolvingModeAttemptingResolve();
    RoomSharingClient roomSharingClient = new RoomSharingClient();
    roomSharingClient.GetAnchorIdFromRoom(roomToResolve, ipAddress, (bool found, string cloudAnchorId) =>
    {
        if (!found)
        {
            UIController.ShowResolvingModeBegin("Invalid room code.");
        }
        else
        {
            _ResolveAnchorFromId(cloudAnchorId);
        }
    });
}

まず、new RoomSharingClient()でNetworkClientを作成し、roomSharingClient.GetAnchorIdFromRoom()でRoomに接続します。処理が完了したらCallbackで戻る形になっています。
Callbackに返されるのはcloudAnchorIdとなっており、Roomの接続と同時にHost(Server)がDictionaryに保持しているCloud AnchorのCloud Idを取得しようとしています。

こちらがRoomSharingClient.csの処理です。

public void GetAnchorIdFromRoom(Int32 roomId, string ipAddress, GetAnchorIdFromRoomDelegate GetAnchorIdFromRoomCallback)
{
    m_GetAnchorIdFromRoomCallback = GetAnchorIdFromRoomCallback;
    m_RoomId = roomId;
    RegisterHandler(MsgType.Connect, OnConnected);
    RegisterHandler(RoomSharingMsgType.AnchorIdFromRoomResponse, OnGetAnchorIdFromRoomResponse);
    Connect(ipAddress, 8888);
}

まず、呼び出し元に戻れるようにCallback関数をセットしています。
また、RegisterHandler()で"Connectに成功したらOnConnected"、"AnchorIdFromRoomResponseのメッセージを受信したらOnGetAnchorIdFromRoomResponse"がそれぞれ実行されるようになっています。
最後のConnect()がServerとの接続開始です。

Serverから接続完了のメッセージが届いたら、OnConnectedが呼ばれます。

private void OnConnected(NetworkMessage networkMessage)
{
    AnchorIdFromRoomRequestMessage anchorIdRequestMessage = new AnchorIdFromRoomRequestMessage
    {
        RoomId = m_RoomId
    };

    Send(RoomSharingMsgType.AnchorIdFromRoomRequest, anchorIdRequestMessage);
}

ServerへのメッセージにRoomIdを渡して、RoomIdに紐づくCloud AnchorのIDを要求します。
最後のSend()でメッセージのTypeと実際のメッセージをServerに送っています。

Clientからメッセージを受け取ったServerはというと、一番最初にRegisterHandler()で登録していたOnGetAnchorIdFromRoomRequestが応答します。

private void OnGetAnchorIdFromRoomRequest(NetworkMessage netMsg)
        {
            var roomMessage = netMsg.ReadMessage<AnchorIdFromRoomRequestMessage>();
            XPAnchor anchor;
            bool found = m_RoomAnchorsDict.TryGetValue(roomMessage.RoomId, out anchor);
            AnchorIdFromRoomResponseMessage response = new AnchorIdFromRoomResponseMessage
            {
                Found = found,
            };

    if (found)
    {
        response.AnchorId = anchor.CloudId;
    }

    NetworkServer.SendToClient(netMsg.conn.connectionId, RoomSharingMsgType.AnchorIdFromRoomResponse, response);
}

Serverがやっていることは、

メッセージからRoomIdを取り出し、
RoomIdに紐付くCloud AnchorがDictionaryにあるかを探し、
見つかったらAnchorのCloudIdをAnchorIdFromRoomResponseメッセージに乗せて、
Requestを送ってきたClientだけに送り返す

です。

再びClientに戻りまして、AnchorIdFromRoomResponseのメッセージを受信したのでRegisterHandler()で登録されていたOnGetAnchorIdFromRoomResponseが実行されます。

private void OnGetAnchorIdFromRoomResponse(NetworkMessage networkMessage)
{
    var response = networkMessage.ReadMessage<AnchorIdFromRoomResponseMessage>();
    m_GetAnchorIdFromRoomCallback(response.Found, response.AnchorId);
 }

メッセージからAnchorIdを取り出して、Callback関数に返すことができました。

整理すると、

  • メイン処理
    RoomId + IPアドレス
  • GetAnchorIdFromRoom(Client)
    Connect(IPアドレス)
  • 接続処理(Server)
    <MsgType.Connect>
  • OnConnected(Client)… RoomのCloud Anchorを要求
    <AnchorIdFromRoomRequestMessage>
  • OnGetAnchorIdFromRoomRequest(Server)… AnchorのCloudIdを返却
    <AnchorIdFromRoomRequestMessage>
  • OnGetAnchorIdFromRoomResponse(Client)... AnchorIdをCallback
    AnchorId
  • メイン処理

となります。

6. Cloud AnchorのResolving

Roomに接続する際のCallbackを改めて見てみると、AnchorIdが取得できたら_ResolveAnchorFromIdを実行となっています。

    roomSharingClient.GetAnchorIdFromRoom(roomToResolve, ipAddress, (bool found, string cloudAnchorId) =>
    {
        if (!found)
        {
            UIController.ShowResolvingModeBegin("Invalid room code.");
        }
        else
        {
            _ResolveAnchorFromId(cloudAnchorId);
        }
    });

次で最後です。ようやくここまでたどり着きました。

XPSession.ResolveCloudAnchor(cloudAnchorId)でCloud上に登録された情報を取得します。

private void _ResolveAnchorFromId(string cloudAnchorId)
{
    XPSession.ResolveCloudAnchor(cloudAnchorId).ThenAction((System.Action<CloudAnchorResult>)(result =>
    {
        if (result.Response != CloudServiceResponse.Success)
        {
            UIController.ShowResolvingModeBegin(string.Format("Resolving Error: {0}.", result.Response));
            return;
        }

        m_LastResolvedAnchor = result.Anchor;
        Instantiate(_GetAndyPrefab(), result.Anchor.transform);
        UIController.ShowResolvingModeSuccess();
    }));
}

cloudAnchorIdはServerがHostingしたCloud AnchorのCloudIdなのでAPIに接続できさえすれば取得できるはずです。
ただ、先に述べた通り、空間検知の状況によってはCloud Anchorが取得できても位置を復元できない可能性があります。

無事に復元ができたら、空間上の同じ位置にObjectを置くことができます!

まとめ

Network Managerを使わないUNETのマルチプレイ実装に加え、Roomへの接続とCloud AnchorのResolveが一連のフローになっているため混乱しがちですが、この記事をまとめているうちに、Exampleの挙動は大体理解できたかなと思います。

では、ここまででCloud Anchor APIをマスターできたかと言われると・・・実はまだ課題があります。
Cloud Anchor APIには、1分あたり30回しかHostが行えない(Resolveは300回)という帯域制限があります。また、Cloud AnchorのHosting、Rerolvingには秒単位のラグが発生することがあります。

つまり、Cloud Anchorはオブジェクトに付与するのではなく、Roomの代表点として1つだけ作成し、以降のオブジェクト共有はCloud Anchorを基点に座標変換してあげる形になりそうです。

前編としたので、後編に続ける余力があれば、Cloud Anchorを作成した後の処理について書きたいと思います。
後編書きました!