Azure Spatial Anchors V2とHoloLens ２のチュートリアルの解説

# Azure Spatial Anhors V2以降の便利な機能
Azure Spatial Anchorsは現実空間にデジタルの物体を固定するために利用できる「アンカー」の制御をクロスプラットフォームで利用できる技術です。

今回は、Azure Spatial Anchors V2系の機能についての解説とサンプルの仕組みから
Azure Spatial Anchorsの基本的な使い方を紹介したいと思います。
V2の新機能を使ったサンプルについてはボリュームが多くなるので別の記事で記載します。

1. [Azure Spatial Anchorsの機能](#1Azure-Spatial-Anchors%E3%81%AE%E6%A9%9F%E8%83%BD) 
    1. [背景](#11%E8%83%8C%E6%99%AF)
    1. [概要](#12%E6%A6%82%E8%A6%81)
    1. [基本的な機能](#13%E5%9F%BA%E6%9C%AC%E7%9A%84%E3%81%AA%E6%A9%9F%E8%83%BD)
    1. [Coarse Relocalarization](#14Coarse-Relocalization)

2. [実装サンプル](#2%E5%AE%9F%E8%A3%85%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB)
    1. [提供されているサンプルの紹介](#21%E6%8F%90%E4%BE%9B%E3%81%95%E3%82%8C%E3%81%A6%E3%81%84%E3%82%8B%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E3%81%AE%E7%B4%B9%E4%BB%8B)
    2. [HoloLens 2チュートリアルによる実装例](#22HoloLens-2%E3%83%81%E3%83%A5%E3%83%BC%E3%83%88%E3%83%AA%E3%82%A2%E3%83%AB%E3%81%AB%E3%82%88%E3%82%8B%E5%AE%9F%E8%A3%85%E4%BE%8B)

## 1.Azure Spatial Anchorsの機能

Azure Spatial Anchorsについては少し登壇する機会があったのでその時の情報をベースに少し整理してみます。

* [BRK30154 - Azure Spatial Anchorsを活用したMR/ARアプリケーションのクロスプラットフォーム開発](https://osaka.myignitetour.techcommunity.microsoft.com/sessions/91082?source=sessions)
* [その時の資料はここに上がるらしい](https://techcommunity.microsoft.com/t5/microsoft-ignite-the-tour-2019/cross-platform-development-of-mr-ar-applications-using-azure/m-p/1175062#)

### 1.1.背景

Azure Spatial Anchorsが出てきた背景だと思ってることについて。
近年はXR界隈は以前に比べるとかなり注目されてきています。少なくても自分がHoloLensで遊び始めたころに比べるとかなりXR関連の言葉等はニュースでもよく上がってきている印象があります。
そういう状況下で空間情報を活用したコンテンツ開発や技術というのは今後活用が進むと考えられます。
ただXRとひとくくりにせずVR/AR/MRという各技術要素で考えるとVRとは異なりAR/MRについてはその活用にあたっていくつかの課題があります。
どのあたりかとわかりやすくするために違いを簡単に整理すると次の通りになります。

|Virtual Reality<br/> = 疑似的な体験|Augment Reality = 拡張現実<br/>Mixed Reality = 複合現実|
|:--|:--|
|<ul><li>現実世界の情報の一部をデジタルで置換</li><li>現実さながらの疑似的体験を可能にする技術</li></ul>|<ul><li>現実世界の情報とデジタル情報の相互作用</li><li>VRよりも現実世界との相互作用を強く感じる</li><li>ARとMRは境界がなくなりつつある</li></ul>|

それぞれの技術は要素多少はあれど現実世界の情報とデジタル情報を相互に作用させるところが概念の中心です。
つまり、コンテンツにおけるデジタル情報の量（現実世界を置き換える情報量）が違うというのがあります。大雑把にはVRはほぼデジタル情報の中に没入するイメージになると思いますが、AR/MRはそうでもないという形です。
この際に課題になりうる問題の１つが「マルチユーザコンテンツ」になります。
マルチユーザコンテンツは以下の要素を満たしている必要があると考えられます。

* 参加者が同じデジタル情報を"共通の座標系で"共有できる。

VRの場合比較的容易に実現できます。VRの場合は参加するデジタル空間は共有の座標系を持っているためこの問題を意識する必要がありません。
一方AR/MRについてはコンテンツの開始位置が原点(0,0,0)になるためマルチユーザコンテンツを作る場合は、「現実空間の座標系にデジタルの座標系」を合わせていく必要があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/1ef12b22-2364-031e-d1cb-afa256373682.png)

このための仕組みが「アンカー」と呼ばれるもので、アンカーにより現実空間とデジタル空間の位置を調整する仕組みが必要となります。
空間のどこか1点以上にデジタルの座標系の１つをアンカーとして割当てることができれば、そこからの相対座標系でデジタルコンテンツを作り込めば、いつでも同じ場所にデジタルコンテンツを表示することができます。そしてこのアンカーを他ユーザと共有仕組みを用意することができれば、マルチユーザコンテンツが開発可能になります。
アンカーを共有する方法としてはいくつかテクニックがあります。

1. 目印を用意して同じ位置、同じ向きでコンテンツを開始する
2. ARマーカを利用し、デジタルコンテンツを同じ場所に出力させ、それをアンカー位置とする。
3. Azure Spatial Anchors

1つ目は非常に単純ですが、簡単なコンテンツならこれでもいいかもしれません。アプリ同時の位置が原点になることを逆利用して、参加者全員を同じ位置と向きでコンテンツ起動させることで位置情報をそろえるというものです。
2つ目はマーカ＝現実空間の位置、マーカー認識で表示するコンテンツ＝アンカーとすることで位置をそろえるというものです。コンテンツの開始時にマーカを読ませて調整します。この方法は比較的安定します。アンカーを利用した位置合わせは過去試したことがあるので以下の記事も参考にしてもらえるといいかもしれません。

[Mixed Reality Toolkit のUNETサンプルをベースにマルチプレーヤーな撃ちゲーを作ってみる ～ vuforia で位置合わせ編 ～](https://qiita.com/miyaura/items/11f94c73a870b1913cfa)

そして、最後にAzure Spatial Anchors。今回はこのサービスについて色々を解説したいと思います。

### 1.2.概要

Azure Spatial AnchorsはAzureのクラウドサービスの１つとして提供されています（2020/02/024時点ではPreview)

* [Spatial Anchors](https://azure.microsoft.com/ja-jp/services/spatial-anchors/?WT.mc_id=MR-MVP-5003104)

Azure Spatial Anchorsは現実空間の特徴とデジタル情報をセットで管理し、現実空間とデジタル情報のリンクを実現する技術です。


#### 現実空間の特徴とは

Azure Spatial Anchorsといっても実はARマーカの仕組みとさほど違いがありません。ARマーカの場合、QRコードやマーカーとなる物体を用意します。それを認識した際にデジタルのコンテンツを出力することが可能です。Azure Spatial AnchorsはこのQRコードやマーカーに相当するものを現実空間の特徴点を使って行っています。現実空間の特徴は例えば以下のような空間にある物体や模様などから特徴を見出します。この特徴をマーカとしてアンカーとセットでクラウドで管理します。アンカーを復元する場合は、セットの空間特徴にある場所を探して一致する部分があればアンカーを設置する仕組みになっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/efb64ac2-66c1-e0ca-3cf8-1ea2216c0145.png)

実はこの仕様からわかるかもしれませんが、「現実空間が様子が変わる部分にアンカーは設置できない」という制約があります。現実には配置することは可能ですが、特徴点が一致しなければアンカーが設置されることがありません。例えば上記の図でドーナツ状の物体が空間上から消えると位置合わせができなくなります。また、空間がシンプルなほど特徴点が少なくなる傾向があります。このため、Azure Spatial Anchorsなどの空間の特徴を扱ったAR/MRコンテンツでは利用する空間の状態をうまく考えておく必要があります。自宅での動作確認ではうまくいったものがイベント会場ではうまくいかない場合などは空間の特徴を疑うと解決する可能性があります。

### 1.3.基本的な機能

Azure Spatial Anchorsは以大きくは以下の機能を持っています。これらの機能はクロスプラットフォーム(iOS/Android/HoloLens)で利用が可能です。

#### セッション管理  
アンカー操作のためのセッション管理です。Azure Spatial Anchorsは単発のWebAPIではなくサービスに対する操作はセッション単位で実施されます。
このため、アンカーの制御は最初のセッションの開始を行います。その後アンカー操作を行い最後に接続を閉じる形で実装します。
このセッションでの単位が１単位となっており、セッション中のアンカー制御によりアンカー同士のリンクされる仕組みです。
例えば、以下のようなオペレーションです。

* 同一セッション内で複数のアンカーを登録した。
* 登録済のアンカーを取得し、追加でアンカーの登録を行った。

前者は登録したアンカー同士がリンクされ、後者は既存のアンカーに登録したアンカーがリンクされます。

#### アンカーの登録  
アンカーの登録処理です。Azure Spatial Anchorsではメイン機能の１つになると思います。アンカーの登録時には以下の情報の設定が必要です。

|情報|必須/任意|説明|
|:--|:-:|:--|
|アンカー周辺の空間情報|必須<br/>（SDKで自動設定）|アンカーの登録には空間の特徴点が必要で、ある程度情報が集まっていないと登録時にエラーになります。このため、空間情報が十分取得できているかチェックし、十分な情報が確保できるまで待ちの処理を行う必要があります。3デバイス(iOS/Android/HoloLens)の中ではHoloLensがダントツで空間情報の精度/収集が早いです。コンテンツ開発時は登録をHoloLensにするほうがより精度が高くなると思います。|
|アンカーの座標|必須<br/>（SDKで自動設定）|アンカーの座標はそれ自体あまり気にする必要はありません。コンテンツ開発者は設置されたアンカーからの相対座標/向きでデジタルコンテンツを作成します。具体的にはアンカー位置に親のオブジェクトを設置しデジタルコンテンツをその子要素として開発します。|
|アンカーに埋め込む情報（`Dictionary<string,String>`）|任意|アンカー情報に文字列型の辞書データを格納することができます。これを利用すると、別情報を取得するためのIDをアンカーに登録し、アンカー設置時に情報を出力するなども可能です。|
|アンカーの寿命（無制限も可）|設定した方がいい|アンカーにはあらかじめ寿命を設定することができます。指定は日単位です。登録から指定時間が過ぎると自動的に廃棄されます。必要に応じて無制限のアンカーも作成可能ですが、テスト時には有限にしておくように注意書きがあります。現状、Preview版のため無償利用できますが、有償化された場合、アンカーの登録数で課金される可能性があるため永続化していると唐突に課金される可能性があります（笑）|
|センサー情報(※V2で追加) |任意|これは後述の[Coarse Relocalarization](#14Coarse-Relocalization)で紹介します。V2からは各種センサー情報も登録することで、センサー周辺のアンカーを取得可能になります。|

#### アンカーの取得

登録したアンカーの取得を行います。取得の方法はいくつかの手段がありますが、注意が必要なものが多いです。また、アンカーの取得～設置は時間がかかります。現実空間の特徴と一致する部分を探すための検索にコストがかかるためです。このため必要に応じてアンカーに埋め込んだ情報のみ取得する方法も用意されています。

|取得方法|説明|
|:--|:--|
|AnchorID指定|登録済みのアンカーIDを利用してクラウドから情報を取得します。アンカーIDは登録時に生成されるためこの方法で検索するためにはIDの永続化が必要です。|
|登録済みアンカーの周辺|1つでもアンカーを取得すれば、そのアンカーを中心に他のアンカーを取得することが可能になります。ただし、この方法は癖があり新規に登録したアンカーIDで周辺の検索を行ってもアンカーは取得できません。これはセッション管理のところでも少し触れたアンカーのリンクの問題です。実は「新規追加したアンカーは登録済みのアンカーとはリンクしていない」ため、周辺のアンカーを問い合わせても結果が返ってきません。この方法を使う場合も基本的にはAnchorIDの永続化が必要になります。|
|センサー周辺|V2から追加されました。この仕組みが一番便利でエキサイティングな機能です。V2からはアンカー登録時にセンサーの情報を含めて登録することが可能です。当然その情報を利用してアンカーを取得することが可能になりました。このおかげで例えばビーコンの周辺にあるアンカーを取得するなどの機能もできるようになっています。|

#### アンカーの削除  
  不要なアンカーを削除することが可能です。削除にはAhcnorIDが必要になります。

### 1.4.Coarse Relocalization

V2系から追加された新機能です。上記のAzure Spatial Anchorsの機能のうち取得の際にV1では「登録済みのAnchorId」が必要でした。実はこの仕様が非常に使い勝手が悪かったです。というのも、AnchorIdはAzure Spatial Anchorsに登録時に自動生成し通知されます。つまりこちら側から指定するすべがないため、受け取ったIdを別の方法で永続化しなければ、次回以降アンカーを取得することができませんでした。
このため、Azure Spatial Anchorsのサンプルではローカルファイルにコピーしたり、Webサービスを経由してIdを管理する手法が用いられました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/c16b5f09-46c2-dbcd-a39d-0e7c0ae39248.png)

V2ではこの問題を解決するために各種センサー情報を利用してセンサーを中心にアンカーを取得する仕組みが追加されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/dd3268f1-3c18-2147-55e8-0c85f739ce77.png)


センサーは利用する空間環境に応じて使い分けを行う想定となっています。使えるセンサーの種類から想像できるかと思います。
GPSが屋外、ビーコンが屋内向きとされています。Wifiはその中間に位置する形です。GPSを除けば屋外で使えないわけではないですが、空間のスケールを考慮して分類されている形です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/fa761a58-966d-efb0-86bd-9fef63ea3d00.png)




センサー情報として利用できるものは現時点では以下の3つでデバイス毎に使える機能に制約があります。主にハード仕様からくる制限です。

|センサー種別|HoloLens|Android|iOS|
|:-:|:-:|:-:|:-:|
|GPS|×<br/>機能なし|〇<br/>APIサポート|〇<br/>APIサポート|
|Wifi|〇<br/>3 秒ごとに約 1 回のスキャン|〇<br/>API レベル 28 以降スキャンは 2 分毎に 4 回の呼出に調整。 Android 10 からは調整可能|×<br/>機能なし|
|BLE Beacon|〇<br/>Eddystone</br>iBeacon|〇<br/>Eddystone</br>iBeacon|〇<br/>Eddystone</br>iBeacon|
参照：[Coarse Relocalization - プラットフォームごとのサポート](https://docs.microsoft.com/ja-jp/azure/spatial-anchors/concepts/coarse-reloc?WT.mc_id=MR-MVP-5003104&tabs=csharp#per-platform-support)

#### 各種センサーの仕様

センサーの仕様は以下の通りです。詳しくはサイトの情報も併せて確認してください。
いずれの場合もセンサ情報を取得するためには各センサーの信号が安定している状態が望ましいとされています。このためAzure Spatial Anchorsのセッションを開始してからしばらくの間、センサー情報を取得し安定するのを待ってから処理を行う方が精度が高いそうです。

##### GPS
[Coarse Relocalization - GPSを有効化にする](https://docs.microsoft.com/ja-jp/azure/spatial-anchors/concepts/coarse-reloc?WT.mc_id=MR-MVP-5003104&tabs=csharp#enabling-gps)

デバイスのGPS情報を基準にして周辺のアンカーを検索します。センサーの特性については以下の通り記載があります。

* 非同期および低周波数（1 Hz未満）
* 信頼性が低い/ノイズが多い（平均で7 mの標準偏差）
* 検索空間の半径（概算） 20~30m

##### Wifi

[Coarse Relocalization - Wifiを有効化にする](https://docs.microsoft.com/ja-jp/azure/spatial-anchors/concepts/coarse-reloc?WT.mc_id=MR-MVP-5003104&tabs=csharp#enabling-wifi)

デバイスのWifi情報を基準にして周辺のアンカーを検索します。センサーの特性については以下の通り記載があります。

* 非同期および低周波数（0.1 Hz未満）
* 信頼性の低い/ノイズの多い（平均3 dBmの標準偏差）
* OSレベルで潜在的に調整
* 検索空間の半径（概算） 50~100m


##### Bluetooth(ビーコン）

[Coarse Relocalization - Bluetoothビーコンを有効化にする](https://docs.microsoft.com/ja-jp/azure/spatial-anchors/concepts/coarse-reloc?WT.mc_id=MR-MVP-5003104&tabs=csharp#enabling-bluetooth-beacons)


デバイスのBluetoothによるビーコン情報を基準にして周辺のアンカーを検索します。センサーの特性については以下の通り記載があります。

* ビーコンを活用（iBeacon,Eddystone)
* ビーコンにはUUIDを指定
* アプリケーションにハードコーディングでUUIDを実装
* 検索空間の半径（概算）70m

Bluetoothのみなのですが、注意事項が記載されています。ビーコンを使用する際にはビーコンで記録されているUUIDでアンカーを検索する仕様です。また、誤動作を防ぐことも想定しUUIDはホワイトリスト方式でAzure Spatial Anchorsに登録します。また、このUUIDがAzure Spatial Anchorsで利用されていることがわかると、ビーコンの複製や、別コンテンツを作成して不正にアンカーを制御することが可能なため扱いに注意が必要とのことです。

以上がAzure Spatial Anchorsの仕様に関することになります。次以降は実際の実装例をもとに仕組みを解説します。

## 2.実装サンプル

実際にAzure Spatial Anchorsでのサンプル/チュートリアルコードを抜粋して実際の仕様面の紹介をしたいと思います。

### 2.1.提供されているサンプルの紹介

Azure Spatial Anchors関連のサンプルはいくつかあります。公式には以下の2つあります。
クロスプラットフォームで利用できるサンプルとしてはAzure Spatial Anchorsのドキュメントで公開されているものになります。こちらは各デバイスのネイティブ実装及び、Unityでの各デバイスでの開発のサンプルとなっています。

Mixed Reality の開発チュートリアルはHoloLens 2用のAzure Spatial Anchorsのサンプルです。
どちらのサンプルを利用しても仕組みの理解はできるのですが、両方を試した結果個人的にはHoloLens 2の仕組みの方がわかりやすいのでそちらをベースに説明したいと思います。

* [Azure Spatial Anchors のドキュメント](https://docs.microsoft.com/ja-jp/azure/spatial-anchors/?WT.mc_id=MR-MVP-5003104)
    * [azure-spatial-anchors-samples(Gitサンプル)](https://github.com/Azure/azure-spatial-anchors-samples)
* Mixed Reality の開発チュートリアル  
[HoloLens 2 tutorials / Azure Spatial Anchor tutorials ](https://docs.microsoft.com/en-US/windows/mixed-reality/mrlearning-asa-ch1?WT.mc_id=MR-MVP-5003104)

Mixed Reality の開発チュートリアルを実施すると以下のようなデモアプリが利用できるようになります。
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">I&#39;m trying HoloLens 2 tutorial using Azure Spatial Anchors...<a href="https://twitter.com/cre8ivepark?ref_src=twsrc%5Etfw">@cre8ivepark</a> <a href="https://twitter.com/jbmcculloch?ref_src=twsrc%5Etfw">@jbmcculloch</a> <a href="https://t.co/vVeDQPCFU1">https://t.co/vVeDQPCFU1</a><br><br>And I could run the tutorial on Android device :)<a href="https://twitter.com/hashtag/Azure?src=hash&amp;ref_src=twsrc%5Etfw">#Azure</a> <a href="https://twitter.com/hashtag/MixedReality?src=hash&amp;ref_src=twsrc%5Etfw">#MixedReality</a> <a href="https://twitter.com/hashtag/HoloLens?src=hash&amp;ref_src=twsrc%5Etfw">#HoloLens</a> <a href="https://twitter.com/hashtag/HoloLens2?src=hash&amp;ref_src=twsrc%5Etfw">#HoloLens2</a> <a href="https://twitter.com/hashtag/MixedRealityToolkit?src=hash&amp;ref_src=twsrc%5Etfw">#MixedRealityToolkit</a> <a href="https://t.co/2tpozozG1R">pic.twitter.com/2tpozozG1R</a></p>&mdash; takabrz1@MRTKV2とAzure Spatial Anchorsと格闘中 (@takabrz1) <a href="https://twitter.com/takabrz1/status/1231431463905591296?ref_src=twsrc%5Etfw">February 23, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

### 2.2.HoloLens 2チュートリアルによる実装例

実装方法の手順などは上記の開発チュートリアルを一通り実施してください。これを書いている時点(2020/02/25)では英語サイトの手順がいいと思います。日本語のものは1世代前になっています。大きな差はないのですが、英語版だとMixed Reality Toolkit V2.3.0とAzure Spatial Anchors V2.1.1で構築できます。

#### HoloLens 1で動かしたい場合の設定変更

HoloLens 2用ではあるのですが、HoloLens 1でも普通に動きます。HoloLens 1用でデプロイしたい場合は、チュートリアルの作業を実施後に以下の部分を修正してください。

* Mixed Reality ToolkitのProfileを「DefalutHoloLens1ConfigurationProfile」に変更
* [Add the Rocket Launcher experience](https://docs.microsoft.com/en-US/windows/mixed-reality/mrlearning-asa-ch1#1-add-the-rocket-launcher-experience&WT.mc_id=MR-MVP-5003104)で追加するPrefabの各部品（）の[Inspector]内の[ManinpulationHandler]コンポーネントのプロパティ[AllowFarManipulation]にチェックを入れる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/b5d80dc8-e2e6-8a9f-5f08-4890145286b8.png)

AllowFarManipulationの設定はなくてもAzure Spatial Anchorsとしては関係ないのですが、操作できないと面白くないのでせっかくなのでチェックを入れておいてください。

#### セッションの開始と終了

チュートリアルでは[Assets\MRTK.Tutorials.AzureSpatialAnchors\scripts\AnchorModuleScript.cs]がAzure Spatial Anchorsに必要な処理がすべて記載されています。セッションの開始と終了はそれぞれStartAzureSessionメソッド、StopAzureSessionメソッドで実装されています。
Azure Spatial Anchors SDKにはSpatialAnchorManagerというコンポーネントがありこのコンポーネントを介してサービスを利用します。


セッションの開始については、SpatialAnchorManagerの以下のメソッドを呼出します。

* CreateSessionAsync
* StartSessionAsync

いずれも非同期メソッドのため注意してください。CreateSessionAsyncはSpatialAnchorManagerでセッションが準備されていない場合のみ実施します。

```csharp:Assets\MRTK.Tutorials.AzureSpatialAnchors\scripts\AnchorModuleScript.cs
//セッション開始用のイベント
public async void StartAzureSession() {
    Debug.Log("\nAnchorModuleScript.StartAzureSession()");

    // Notify AnchorFeedbackScript
    OnStartASASession?.Invoke();

    Debug.Log("Starting Azure session... please wait...");

    if (cloudManager.Session == null) {
        // Creates a new session if one does not exist
        await cloudManager.CreateSessionAsync();
    }

    // Starts the session if not already started
    await cloudManager.StartSessionAsync();

    Debug.Log("Azure session started successfully");
}
```

セッションの終了は以下のメソッドを呼出します。

* StopSession

また、セッションの情報をリセットするためにはResetSessionAsyncを呼出します。ResetSessionAsyncについてはセッションに設定している各種の情報取得の設定などをリセットすることができます。。

```csharp:Assets\MRTK.Tutorials.AzureSpatialAnchors\scripts\AnchorModuleScript.cs
//セッション終了用のイベント
public async void StopAzureSession() {
    Debug.Log("\nAnchorModuleScript.StopAzureSession()");

    // Notify AnchorFeedbackScript
    OnEndASASession?.Invoke();

    Debug.Log("Stopping Azure session... please wait...");

    // Stops any existing session
    cloudManager.StopSession();

    // Resets the current session if there is one, and waits for any active queries to be stopped
    await cloudManager.ResetSessionAsync();

    Debug.Log("Azure session stopped successfully");
}
```

#### アンカー設置のためのイベント追加

Azure Spatial Anchorsではクラウド上に格納された情報をもとに現実空間との位置合わせを行います。この処理については時間がかかるため、Azure Spatial Anchorsでは以下の流れで処理を行います。

1. アンカー情報の取得を依頼しAzure Spatial Anchorsサービスから情報を取得する
2. 取得したアンカー情報をもとに現実空間の特徴と一致する領域を検索する
3. 特徴が一致した場合、その情報をAnchorLocatedイベントで通知する
4. AnchorLocated イベント内でアンカーに対する処理（位置の調整や可視化など） 

つまりリクエストを投げてもすぐに情報を取得できるわけではなく、内部処理を通して配置できたアンカーから順にイベントが通知される仕組みです。
この実装を行っているところが以下の個所になります。
StartメソッドではSpatialAnchorManagerでアンカー設置が完了した際の通知するイベントAnchorLocatedにCloudManager_AnchorLocatedメソッドを割り当てています。

```csharp:Assets\MRTK.Tutorials.AzureSpatialAnchors\scripts\AnchorModuleScript.cs
void Start()
{
    // Get a reference to the SpatialAnchorManager component (must be on the same gameobject)
    cloudManager = GetComponent<SpatialAnchorManager>();

    // Register for Azure Spatial Anchor events
    cloudManager.AnchorLocated += CloudManager_AnchorLocated;

    anchorLocateCriteria = new AnchorLocateCriteria();
}
```

CloudManager_AnchorLocatedメソッドではイベント引数の情報を利用して処理を行います。AnchorLocatedEventArgs にはアンカー設置についての情報が格納されています。以下のメソッドの中では以下の条件でオブジェクトの座標と向きを設定する処理が入っています。

```csharp:Assets\MRTK.Tutorials.AzureSpatialAnchors\scripts\AnchorModuleScript.cs
#region Event Handlers
private void CloudManager_AnchorLocated(object sender, AnchorLocatedEventArgs args)
{
    QueueOnUpdate(new Action(() => Debug.Log($"Anchor recognized as a possible Azure anchor")));

    if (args.Status == LocateAnchorStatus.Located || args.Status == LocateAnchorStatus.AlreadyTracked)
    {
        currentCloudAnchor = args.Anchor;

        QueueOnUpdate(() =>
        {
            Debug.Log($"Azure anchor located successfully");

            // Notify AnchorFeedbackScript
            OnASAAnchorLocated?.Invoke();

#if UNITY_ANDROID || UNITY_IOS
            Pose anchorPose = Pose.identity;
            anchorPose = currentCloudAnchor.GetPose();
#endif

#if WINDOWS_UWP || UNITY_WSA
            // HoloLens: The position will be set based on the unityARUserAnchor that was located.

            // Create a local anchor at the location of the object in question
            gameObject.CreateNativeAnchor();

            // Notify AnchorFeedbackScript
            OnCreateLocalAnchor?.Invoke();

            // On HoloLens, if we do not have a cloudAnchor already, we will have already positioned the
            // object based on the passed in worldPos/worldRot and attached a new world anchor,
            // so we are ready to commit the anchor to the cloud if requested.
            // If we do have a cloudAnchor, we will use it's pointer to setup the world anchor,
            // which will position the object automatically.
            if (currentCloudAnchor != null)
            {
                Debug.Log("Local anchor position successfully set to Azure anchor position");

                gameObject.GetComponent<UnityEngine.XR.WSA.WorldAnchor>().SetNativeSpatialAnchorPtr(currentCloudAnchor.LocalAnchor);
            }
#else
            Debug.Log($"Setting object to anchor pose with position '{anchorPose.position}' and rotation '{anchorPose.rotation}'");
            transform.position = anchorPose.position;
            transform.rotation = anchorPose.rotation;

            // Create a native anchor at the location of the object in question
            gameObject.CreateNativeAnchor();

            // Notify AnchorFeedbackScript
            OnCreateLocalAnchor?.Invoke();

#endif
        });
    }
    else
    {
        QueueOnUpdate(new Action(() => Debug.Log($"Attempt to locate Anchor with ID '{args.Identifier}' failed, locate anchor status was not 'Located' but '{args.Status}'")));
    }
}
```

AnchorLocatedEventArgsはステータスを持っており、アンカーの設置の状態を判断することができます。今回のメソッドでは配置ができた場合、もしくは配置済みの場合にアンカーに合わせてコンテンツの位置を修正するコードが実装されています。

* 配置できた場合（LocateAnchorStatus.Located）
* すでに配置済みの場合(LocateAnchorStatus.AlreadyTracked)

LocateAnchorStatusの他のステータスは以下になります。

* アンカーの設置ができなかった場合。特徴が一致しない場合など（LocateAnchorStatus.NotLocated)
* アンカーの情報が見つからずにアンカーの設置ができなった場合(LocateAnchorStatus.NotLocatedAnchorDoesNotExist)

#### アンカーの登録

アンカーの登録にはいくつか手順があります。以下はその部分の抜粋です。登録処理を実行するには以下のメソッドを呼出します。
引数には登録するアンカー情報を設定します。

* CreateAnchorAsync

```csharp:Assets\MRTK.Tutorials.AzureSpatialAnchors\scripts\AnchorModuleScript.cs
public async void CreateAzureAnchor(GameObject theObject)
{
    Debug.Log("\nAnchorModuleScript.CreateAzureAnchor()");

    // Notify AnchorFeedbackScript
    OnCreateAnchorStarted?.Invoke();

    // First we create a native XR anchor at the location of the object in question
    theObject.CreateNativeAnchor();

    // Notify AnchorFeedbackScript
    OnCreateLocalAnchor?.Invoke();

    // Then we create a new local cloud anchor
    CloudSpatialAnchor localCloudAnchor = new CloudSpatialAnchor();

    // Now we set the local cloud anchor's position to the native XR anchor's position
    localCloudAnchor.LocalAnchor = theObject.FindNativeAnchor().GetPointer();

    // Check to see if we got the local XR anchor pointer
    if (localCloudAnchor.LocalAnchor == IntPtr.Zero)
    {
        Debug.Log("Didn't get the local anchor...");
        return;
    }
    else
    {
        Debug.Log("Local anchor created");
    }

    // In this sample app we delete the cloud anchor explicitly, but here we show how to set an anchor to expire automatically
    localCloudAnchor.Expiration = DateTimeOffset.Now.AddDays(7);

    // Save anchor to cloud
    while (!cloudManager.IsReadyForCreate)
    {
        await Task.Delay(330);
        float createProgress = cloudManager.SessionStatus.RecommendedForCreateProgress;
        QueueOnUpdate(new Action(() => Debug.Log($"Move your device to capture more environment data: {createProgress:0%}")));
    }

    bool success;

    try
    {
        Debug.Log("Creating Azure anchor... please wait...");

        // Actually save
        await cloudManager.CreateAnchorAsync(localCloudAnchor);

        // Store
        currentCloudAnchor = localCloudAnchor;
        localCloudAnchor = null;

        // Success?
        success = currentCloudAnchor != null;

        if (success)
        {
            Debug.Log($"Azure anchor with ID '{currentCloudAnchor.Identifier}' created successfully");

            // Notify AnchorFeedbackScript
            OnCreateAnchorSucceeded?.Invoke();

            // Update the current Azure anchor ID
            Debug.Log($"Current Azure anchor ID updated to '{currentCloudAnchor.Identifier}'");
            currentAzureAnchorID = currentCloudAnchor.Identifier;
        }
        else
        {
            Debug.Log($"Failed to save cloud anchor with ID '{currentAzureAnchorID}' to Azure");

            // Notify AnchorFeedbackScript
            OnCreateAnchorFailed?.Invoke();
        }
    }
    catch (Exception ex)
    {
        Debug.Log(ex.ToString());
    }
}

static public NativeAnchor CreateNativeAnchor(this GameObject gameObject)
{
    // Validate
    if (gameObject == null)
    {
        throw new ArgumentNullException(nameof(gameObject));
    }

    // Remove any existing native anchor
    DeleteNativeAnchor(gameObject);

    // Add the platform-specific anchor
    return gameObject.AddComponent<NativeAnchor>();
}
```

この処理で重要なことは ***「現実空間の情報を十分に取得してから実行する」*** です。アンカーの登録時には現実空間の特徴も併せて送るためこの情報が不十分だと登録に失敗します。現実空間の特徴点の収集が十分であるかは以下のプロパティで判断が可能です。

* SpatialAnchorManager.IsReadyForCreate

このフラグが立っている場合は十分に情報がある状態になっています。どの程度情報が充足されているかは以下のプロパティをチェックします。

* SpatialAnchorManager.SessionStatus.RecommendedForCreateProgress

このプロパティはCloudSpatialAnchorSessionクラスの情報で現実空間の情報が必要量に達した場合は1になります。
よってAzure Spatial Anchorsのサンプルでは以下のようにループで特徴点の情報収集を行わせる実装が入ります。

```csharp
// Save anchor to cloud
while (!cloudManager.IsReadyForCreate)
{
    await Task.Delay(330);
    float createProgress = cloudManager.SessionStatus.RecommendedForCreateProgress;
    QueueOnUpdate(new Action(() => Debug.Log($"Move your device to capture more environment data: {createProgress:0%}")));
}
```

Azure Spatial AnchorsではCloudSpatialAnchorでアンカー情報を管理します。
サンプルでは以下のパラメータへの設定を行っています。

|パラメータ|説明|
|:-:|:-:|
|CloudSpatialAnchor.LocalAnchor|設置したアンカー情報。情報はNativeAnchorコンポーネントを経由して取得する|
|CloudSpatialAnchor.Expiration |有効期限。単位は二日で設定した日数の間クラウド上で保管されます。無制限も可能です。現在、Previewのため無課金で利用できますが、GAとなると課金されると思うので検証レベルでは有限がおすすめです。|

このほかにも以下のようなものがあります。

|パラメータ|説明|
|:-:|:-:|
|CloudSpatialAnchor.AppProperties|アンカーに含めるデータです。String型のKey-Value1形式で格納できます。|
|CloudSpatialAnchor.VersionTag|アンカーのバージョンタグです。例えばアプリケーションのバージョンアップで以前のバージョンのアンカーを無視するなど識別を行いたい場合に利用します。|
|CloudSpatialAnchor.Identifier|アンカーの識別子です。|

実際の設置したアンカー情報はNativeAnchorを経由して変換する仕組みになっています。NativeAnchorはアンカー情報のラッパーとして用意されています。Azure Spatial AnchorsはUnity以外もネイティブで開発できるようにするため、アンカーの実装がUnity等特定のプラットフォームに依存しないように作る必要がありこういう仕組みがあります。

#### アンカーの取得

アンカーの取得方法にはいくつか種類があるのですが、今回はチュートリアルでも利用されているIdを利用した取得方法を解説します。IDを取得するためには検索対象となるAnchorIdのリストをAnchorLocateCriteria.Identifiersプロパティに設定し取得を開始します。取得処理については以下の処理を呼出し引数にIdを設定したAnchorLocateCriteriaを渡します。

* SpatialAnchorManager.Session.CreateWatcher

このメソッドを実行するとAzure Spatial Anchorsに問い合わせを行いアンカー情報を取得します。取得した情報をもとに現実空間への設置を試みたのち処理が完了したアンカーから順次[アンカー設置のためのイベント追加](#%E3%82%A2%E3%83%B3%E3%82%AB%E3%83%BC%E8%A8%AD%E7%BD%AE%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AE%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E8%BF%BD%E5%8A%A0)で説明したイベント通知が行われます。

```csharp:Assets\MRTK.Tutorials.AzureSpatialAnchors\scripts\AnchorModuleScript.cs
public void FindAzureAnchor()
{
    Debug.Log("\nAnchorModuleScript.FindAzureAnchor()");

    // Notify AnchorFeedbackScript
    OnFindASAAnchor?.Invoke();

    // Set up list of anchor IDs to locate
    List<string> anchorsToFind = new List<string>();

    if (currentAzureAnchorID != "")
    {
        anchorsToFind.Add(currentAzureAnchorID);
    }
    else
    {
        Debug.Log("Current Azure anchor ID is empty");
        return;
    }

    anchorLocateCriteria.Identifiers = anchorsToFind.ToArray();
    Debug.Log($"Anchor locate criteria configured to look for Azure anchor with ID '{currentAzureAnchorID}'");

    // Start watching for Anchors
    if ((cloudManager != null) && (cloudManager.Session != null))
    {
        currentWatcher = cloudManager.Session.CreateWatcher(anchorLocateCriteria);
        Debug.Log("Watcher created");
        Debug.Log("Looking for Azure anchor... please wait...");
    }
    else
    {
        Debug.Log("Attempt to create watcher failed, no session exists");
        currentWatcher = null;
    }
}
```

## まとめ

Azure Spatial Anchorsの仕組みや概念の紹介と、その基本的な実装サンプルである[HoloLens 2 tutorials / Azure Spatial Anchor tutorials ](https://docs.microsoft.com/en-US/windows/mixed-reality/mrlearning-asa-ch1?WT.mc_id=MR-MVP-5003104)で実装情報を紹介しました。基本的な概念はこの辺りの情報を参照することで理解を深めることができると思います。
次回は、このサンプルではアンカーIDをローカルファイルもしくはネットワーク上で共有している部分を、Coarse Relocalization対応するための実装例を紹介したいと思います。

--- 
【参考サイト】
* [Azure Spatial Anchors のドキュメント](https://docs.microsoft.com/ja-jp/azure/spatial-anchors/?WT.mc_id=MR-MVP-5003104)
* [1. Getting started with Azure Spatial Anchors](https://docs.microsoft.com/en-US/windows/mixed-reality/mrlearning-asa-ch1?WT.mc_id=MR-MVP-5003104)
* [Git:azure-spatial-anchors-samples](https://github.com/Azure/azure-spatial-anchors-samples)
* [Git:Mixed Reality Toolkit](https://github.com/Microsoft/MixedRealityToolkit-Unity)