チュートリアル:Azure Spatial Anchors を使用して新しい HoloLens Unity アプリを作成する詳細な手順

このチュートリアルでは、Azure Spatial Anchors を使用して新しい HoloLens Unity アプリを作成する方法について説明します。

前提条件

このチュートリアルを完了するには、以下のものが必要です。

  1. PC - Windows を実行している PC
  2. Visual Studio - ユニバーサル Windows プラットフォーム開発ワークロードがインストールされた Visual Studio 2019Windows 10 SDK (10.0.18362.0 以降) コンポーネント。 Visual Studio Marketplace から Visual Studio 用の C++/WinRT Visual Studio 拡張機能 (VSIX) をインストールする必要があります。
  3. HoloLens - 開発者モードが有効になっている HoloLens デバイス。 この記事では、Windows 10 May 2020 Update が適用された HoloLens デバイスが必要です。 HoloLens を最新のリリースに更新するには、[設定] アプリを開き、[更新とセキュリティ] を選択し、[更新プログラムの確認] ボタンをクリックします。
  4. Unity - Unity 2020.3.25 とモジュールユニバーサル Windows プラットフォーム ビルド サポートおよび Windows ビルド サポート (IL2CPP)

Unity プロジェクトの作成と設定

新しいプロジェクトの作成

  1. Unity Hub で、[New project](新しいプロジェクト) を選択します
  2. [3D] を選択します
  3. [Project name](プロジェクト名) を入力し、 [Location](保存先) を入力します
  4. [Create project](プロジェクトの作成) を選択し、Unity がプロジェクトを作成するのを待ちます

ビルド プラットフォームの変更

  1. Unity エディターで、[File]\(ファイル\)>[Build Settings]\(ビルド設定\) を選択します
  2. [Universal Windows Platform](ユニバーサル Windows プラットフォーム) を選択して、[Switch Platform](プラットフォームの切り替え) を選択します。 Unity がすべてのファイルの処理を完了するまで待ちます。

ASA と OpenXR のインポート

  1. [Mixed Reality Feature Tool] を起動します
  2. プロジェクト パスを選択します -AssetsPackagesProjectSettingsなどのフォルダーを含むフォルダーです。そして [Discover Features](機能の検出) を選択します
  3. [Azure Mixed Reality Services] で両方選択します
    1. [Azure Spatial Anchors SDK Core]
    2. [Azure Spatial Anchors SDK for Windows]
  4. [Platform Support]\(プラットフォーム サポート\) で次を選択します
    1. [Mixed Reality OpenXR Plugin](Mixed Reality OpenXR プラグイン)

注意

カタログが更新され、それぞれの最新バージョンが選択されていることを確認します

MRFT - Feature Selection

  1. [Get Features]\(機能の取得\) -->[Import]\(インポート\) -->[Approve]\(承認\) -->[Exit]\(終了する\) を押します
  2. Unity ウィンドウにフォーカスを再設定すると、Unity はモジュールのインポートを開始します
  3. 新しい入力システムの使用に関するメッセージを受け取ったら、[はい] を選択して Unity を再起動し、バックエンドを有効にします。

プロジェクト設定をセットアップする

次に、開発で Windows Holographic SDK をターゲットにするのに役立つ、Unity プロジェクトの設定をいくつか行います。

OpenXR の設定を変更する

  1. [File]\(ファイル\)>[Build Settings]\(ビルド設定\) を選択します (前の手順で引き続き開いている可能性があります)
  2. [Player Settings...](プレーヤーの設定) を選択します
  3. [XR Plug-in Management](XR プラグインの管理) を選択します
  4. [Universal Windows Platform Settings](ユニバーサル Windows プラットフォーム設定) タブが選択され、[OpenXR][Microsoft HoloLens feature group](Microsoft HoloLens 機能グループ) の隣のチェックボックスにチェックが入っていることを確認します
  5. [OpenXR] の横にある黄色の警告記号を選択して、OpenXR のすべての問題を表示します。
  6. [Fix all](すべて修正) を選択します
  7. "At least one interaction profile must be added (少なくとも 1 つの相互作用プロファイルを追加する必要がある)" という問題を修正するには、[Edit](編集) を選択して OpenXR のプロジェクトの設定を開きます。 [Interaction Profiles](相互作用プロファイル) の下の + 記号を選択し、[Microsoft Hand Interaction Profile](Microsoft ハンド インタラクション プロファイル) を選択しますUnity - OpenXR Setup

品質設定の変更

  1. [Edit]\(編集\)>[Project Settings]\(プロジェクト設定\)>[Quality]\(品質\) の順に選択します。
  2. ユニバーサル Windows プラットフォーム ロゴの下の列で、[Default](既定値) 行の矢印を選択し、[Very Low](非常に低い) を選択します。 [ユニバーサル Windows プラットフォーム] 列のボックスと [Very Low](非常に低い) 行が緑色の場合、設定が適切に適用されていることがわかります。

機能を設定する

  1. [Edit]\(編集\)>[Project Settings]\(プロジェクト設定\)>[Player]\(プレーヤー\) の順に移動します (前の手順から開いたままになっている場合があります)。
  2. [ユニバーサル Windows プラットフォーム 設定] タブが選択されていることを確認します
  3. [Publishing Settings]\(公開の設定\) セクションで、次のコマンドを有効にする
    1. InternetClient
    2. InternetClientServer
    3. PrivateNetworkClientServer
    4. SpatialPerception (既に有効になっている場合もあります)

メイン カメラを設定する

  1. [Hierarchy](階層) パネルで、 [Main Camera](メイン カメラ) を選択します。
  2. [Inspector](インスペクター) で、変換座標を 0、0、0 に設定します。
  3. [Clear Flags](フラグをクリア) プロパティを探して、ドロップダウンを [Skybox](スカイボックス) から [Solid Color](ソリッド カラー) に変更します。
  4. [Background](背景) フィールドを選択してカラー ピッカーを開きます。
  5. R、G、B、A0 に設定します。
  6. 下部の [Add Component](コンポーネントの追加) を選択し、[Tracked Pose Driver] コンポーネントをカメラへ追加しますUnity - Camera Setup

試してみましょう 1

これで、HoloLens デバイスにデプロイする準備が整った空のシーンができました。 問題ないことをテストするために、Unity でアプリをビルドし、Visual Studio でそれをデプロイします。 「Visual Studio を使用した配置とデバッグ」 に従って実行してください。 Unity のスタート画面、そしてクリアな表示が示されます。

Spatial Anchors リソースを作成する

Azure ポータルにアクセスします。

左側のウィンドウで、 [リソースの作成] を選択します。

検索ボックスを使用して、「Spatial Anchors」を検索します。

Screenshot showing the results of a search for Spatial Anchors.

[Spatial Anchors] を選択し、 [作成] を選択します。

[Spatial Anchors アカウント] ウィンドウで次を行います。

  • 通常の英数字を使用して一意のリソース名を入力します。

  • リソースをアタッチするサブスクリプションを選択します。

  • [新規作成] を選択して、リソース グループを作成します。 「myResourceGroup」と名前を付け、 [OK] を選択します。

    リソース グループとは、Web アプリ、データベース、ストレージ アカウントなどの Azure リソースのデプロイと管理に使用する論理コンテナーです。 たとえば、後から簡単な手順で一度にリソース グループ全体を削除することもできます。

  • リソースを配置する場所 (リージョン) を選択します。

  • [作成] を選択して、リソースの作成を開始します。

Screenshot of the Spatial Anchors pane for creating a resource.

リソースが作成されると、Azure portal に、デプロイが完了したことが表示されます。

Screenshot showing that the resource deployment is complete.

[リソースに移動] を選択します。 これでリソースのプロパティを表示できます。

リソースの [アカウント ID] 値は後で使用するためにテキスト エディターにコピーしておきます。

Screenshot of the resource properties pane.

また、リソースの [アカウント ドメイン] 値を後で使用するためにテキスト エディターにコピーします。

Screenshot showing the resource's account domain value.

[設定][アクセス キー] を選択します。 [主キー] 値の [アカウント キー] を後で使用するためにテキスト エディターにコピーします。

Screenshot of the Keys pane for the account.

スクリプトの作成と追加

  1. Unity の [Project](プロジェクト) ウィンドウで、 Assets フォルダに 「Scripts」 という新しいフォルダを作成します。
  2. フォルダを右クリックし、>> ->> の順に選択します。 「AzureSpatialAnchorsScript」とタイトルを付けます
  3. [GameObject]\(ゲームオブジェクト\) ->[Create Empty]\(空のオブジェクトの作成\) の順に移動します。
  4. それを選択し、[Inspector](インスペクター) でその名前を「GameObject」から「AzureSpatialAnchors」に変更します。
  5. 引き続き GameObject です
    1. 位置を 0,0,0 に設定する
    2. [Add Component](コンポーネントの追加) を選択し、[AzureSpatialAnchorsScript] を検索して追加します
    3. [Add Component](コンポーネントの追加) を再度選択し、[AR Anchor Manager] を検索して追加します。 これにより、[AR Session Origin] も自動的に追加されます。
    4. [Add Component](コンポーネントの追加) を再度選択し、SpatialAnchorManager スクリプトを検索して追加します
    5. 追加された [SpatialAnchorManager] コンポーネントで、前の手順で Azure portal の Spatial Anchors リソースからコピーしたアカウント IDアカウント キーおよびアカウント ドメインを入力します。

Unity - ASA GameObject

アプリの概要

アプリでは、次の操作がサポートされます。

ジェスチャ アクション
任意の場所をタップする セッションを開始\続行し、手の位置でアンカーを作成します
アンカーをタップする ASA クラウド サービスで GameObject を削除し、アンカーを削除します
タップし、2 秒間押し続ける (セッションが起動している) セッションを停止し、すべての GameObjects を削除します。 ASA クラウド サービスでアンカーを保持します
タップし、2秒間押し続ける (セッションが起動していない) セッションを開始し、すべてのアンカーを探します。

タップ認識を追加する

ユーザーのタップ ジェスチャを認識できるコードをスクリプトに追加しましょう。

  1. Unity AzureSpatialAnchorsScript.cs ウィンドウでスクリプトをダブルクリックして、Visual Studio でAzureSpatialAnchorsScript.csを開きます。
  2. 次の配列をクラスに追加します
public class AzureSpatialAnchorsScript : MonoBehaviour
{
    /// <summary>
    /// Used to distinguish short taps and long taps
    /// </summary>
    private float[] _tappingTimer = { 0, 0 };
  1. Update() メソッドの下に、次の 2 つのメソッドを追加します。 後のステージで実装を追加します
// Update is called once per frame
void Update()
{
}

/// <summary>
/// Called when a user is air tapping for a short time 
/// </summary>
/// <param name="handPosition">Location where tap was registered</param>
private async void ShortTap(Vector3 handPosition)
{
}

/// <summary>
/// Called when a user is air tapping for a long time (>=2 sec)
/// </summary>
private async void LongTap()
{
}
  1. 次のインポートを追加します
using UnityEngine.XR;
  1. Update() メソッドの先頭に次のコードを追加します。 これにより、アプリは短いハンド タップ ジェスチャと長い (2 秒) ハンド タップ ジェスチャを認識できます
// Update is called once per frame
void Update()
{

    //Check for any air taps from either hand
    for (int i = 0; i < 2; i++)
    {
        InputDevice device = InputDevices.GetDeviceAtXRNode((i == 0) ? XRNode.RightHand : XRNode.LeftHand);
        if (device.TryGetFeatureValue(CommonUsages.primaryButton, out bool isTapping))
        {
            if (!isTapping)
            {
                //Stopped Tapping or wasn't tapping
                if (0f < _tappingTimer[i] && _tappingTimer[i] < 1f)
                {
                    //User has been tapping for less than 1 sec. Get hand position and call ShortTap
                    if (device.TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 handPosition))
                    {
                        ShortTap(handPosition);
                    }
                }
                _tappingTimer[i] = 0;
            }
            else
            {
                _tappingTimer[i] += Time.deltaTime;
                if (_tappingTimer[i] >= 2f)
                {
                    //User has been air tapping for at least 2sec. Get hand position and call LongTap
                    if (device.TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 handPosition))
                    {
                        LongTap();
                    }
                    _tappingTimer[i] = -float.MaxValue; // reset the timer, to avoid retriggering if user is still holding tap
                }
            }
        }

    }
}

SpatialAnchorManager の追加と構成

ASA SDK は、ASA サービスの呼び出しを行うための SpatialAnchorManager というシンプルなインターフェイスを提供します。 変数として AzureSpatialAnchorsScript.cs に追加します

最初にインポートを追加します

using Microsoft.Azure.SpatialAnchors.Unity;

次に変数を宣言します

public class AzureSpatialAnchorsScript : MonoBehaviour
{
    /// <summary>
    /// Used to distinguish short taps and long taps
    /// </summary>
    private float[] _tappingTimer = { 0, 0 };

    /// <summary>
    /// Main interface to anything Spatial Anchors related
    /// </summary>
    private SpatialAnchorManager _spatialAnchorManager = null;

Start() メソッドで、前の手順で追加したコンポーネントに変数を割り当てます

// Start is called before the first frame update
void Start()
{
    _spatialAnchorManager = GetComponent<SpatialAnchorManager>();
}

デバッグ ログとエラー ログを受信するには、さまざまなコールバックをサブスクライブする必要があります

// Start is called before the first frame update
void Start()
{
    _spatialAnchorManager = GetComponent<SpatialAnchorManager>();
    _spatialAnchorManager.LogDebug += (sender, args) => Debug.Log($"ASA - Debug: {args.Message}");
    _spatialAnchorManager.Error += (sender, args) => Debug.LogError($"ASA - Error: {args.ErrorMessage}");
}

注意

ログを表示するには、Unity からプロジェクトをビルドし、Visual Studio ソリューション .sln を開いた後、[Debug](デバッグ) > [Run with Debugging](デバッグで実行) を選択し、アプリの実行中に HoloLens をコンピューターに接続したままにしてください。

セッションの開始

アンカーを作成して検索するには、まずセッションを開始する必要があります。 StartSessionAsync() を呼び出すと、SpatialAnchorManager は必要に応じてセッションを作成し、開始します。 これを ShortTap() メソッドに追加しましょう。

/// <summary>
/// Called when a user is air tapping for a short time 
/// </summary>
/// <param name="handPosition">Location where tap was registered</param>
private async void ShortTap(Vector3 handPosition)
{
    await _spatialAnchorManager.StartSessionAsync();
}

アンカーの作成

セッションが実行されたので、アンカーを作成できます。 このアプリケーションでは、作成されたアンカー GameObjects と作成されたアンカー識別子 (アンカー ID) を追跡します。 コードに 2 つのリストを追加しましょう。

using Microsoft.Azure.SpatialAnchors.Unity;
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR;
    /// <summary>
    /// Main interface to anything Spatial Anchors related
    /// </summary>
    private SpatialAnchorManager _spatialAnchorManager = null;

    /// <summary>
    /// Used to keep track of all GameObjects that represent a found or created anchor
    /// </summary>
    private List<GameObject> _foundOrCreatedAnchorGameObjects = new List<GameObject>();

    /// <summary>
    /// Used to keep track of all the created Anchor IDs
    /// </summary>
    private List<String> _createdAnchorIDs = new List<String>();

パラメーターで定義されたCreateAnchorにアンカーを作成するメソッド CreateAnchor を作成しましょう。

using System.Threading.Tasks;
/// <summary>
/// Creates an Azure Spatial Anchor at the given position rotated towards the user
/// </summary>
/// <param name="position">Position where Azure Spatial Anchor will be created</param>
/// <returns>Async Task</returns>
private async Task CreateAnchor(Vector3 position)
{
    //Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
}

Spatial Anchors は位置だけでなく回転も持つので、作成時に常に HoloLens の方向を向くように回転を設定してみましょう。

/// <summary>
/// Creates an Azure Spatial Anchor at the given position rotated towards the user
/// </summary>
/// <param name="position">Position where Azure Spatial Anchor will be created</param>
/// <returns>Async Task</returns>
private async Task CreateAnchor(Vector3 position)
{
    //Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
    if (!InputDevices.GetDeviceAtXRNode(XRNode.Head).TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition))
    {
        headPosition = Vector3.zero;
    }

    Quaternion orientationTowardsHead = Quaternion.LookRotation(position - headPosition, Vector3.up);

}

目的のアンカーの位置回転ができたので、表示されるGameObjectを作成しましょう。 Spatial Anchors の主な目的は、共通の永続的な参照フレームを提供することであるため、Spatial Anchors ではエンド ユーザーにアンカー GameObject を表示する必要がないことに注意してください。 このチュートリアルでは、アンカーをキューブとして視覚化します。 各アンカーは白いキューブとして初期化され、作成プロセスが成功すると緑色のキューブになります。

/// <summary>
/// Creates an Azure Spatial Anchor at the given position rotated towards the user
/// </summary>
/// <param name="position">Position where Azure Spatial Anchor will be created</param>
/// <returns>Async Task</returns>
private async Task CreateAnchor(Vector3 position)
{
    //Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
    if (!InputDevices.GetDeviceAtXRNode(XRNode.Head).TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition))
    {
        headPosition = Vector3.zero;
    }

    Quaternion orientationTowardsHead = Quaternion.LookRotation(position - headPosition, Vector3.up);

    GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
    anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
    anchorGameObject.transform.position = position;
    anchorGameObject.transform.rotation = orientationTowardsHead;
    anchorGameObject.transform.localScale = Vector3.one * 0.1f;

}

注意

既定の Unity ビルドに含まれているので、レガシ シェーダーを使用しています。 既定のシェーダーなどの他のシェーダーは、手動で指定した場合、またはシーンの直接一部である場合にのみ含まれます。 シェーダーが含まれていない場合、そしてアプリケーションがシェーダーをレンダリングしようとしている場合は、ピンク色の素材が生成されます。

次に、空間アンカー コンポーネントを追加して構成しましょう。 アンカーの有効期限をアンカーの作成から 3 日間に設定しています。 その後、クラウドから自動的に削除されます。 インポートを忘れずに追加してください

using Microsoft.Azure.SpatialAnchors;
/// <summary>
/// Creates an Azure Spatial Anchor at the given position rotated towards the user
/// </summary>
/// <param name="position">Position where Azure Spatial Anchor will be created</param>
/// <returns>Async Task</returns>
private async Task CreateAnchor(Vector3 position)
{
    //Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
    if (!InputDevices.GetDeviceAtXRNode(XRNode.Head).TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition))
    {
        headPosition = Vector3.zero;
    }

    Quaternion orientationTowardsHead = Quaternion.LookRotation(position - headPosition, Vector3.up);

    GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
    anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
    anchorGameObject.transform.position = position;
    anchorGameObject.transform.rotation = orientationTowardsHead;
    anchorGameObject.transform.localScale = Vector3.one * 0.1f;

    //Add and configure ASA components
    CloudNativeAnchor cloudNativeAnchor = anchorGameObject.AddComponent<CloudNativeAnchor>();
    await cloudNativeAnchor.NativeToCloud();
    CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;
    cloudSpatialAnchor.Expiration = DateTimeOffset.Now.AddDays(3);

}

アンカーを保存するには、ユーザーが環境データを収集する必要があります。

/// <summary>
/// Creates an Azure Spatial Anchor at the given position rotated towards the user
/// </summary>
/// <param name="position">Position where Azure Spatial Anchor will be created</param>
/// <returns>Async Task</returns>
private async Task CreateAnchor(Vector3 position)
{
    //Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
    if (!InputDevices.GetDeviceAtXRNode(XRNode.Head).TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition))
    {
        headPosition = Vector3.zero;
    }

    Quaternion orientationTowardsHead = Quaternion.LookRotation(position - headPosition, Vector3.up);

    GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
    anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
    anchorGameObject.transform.position = position;
    anchorGameObject.transform.rotation = orientationTowardsHead;
    anchorGameObject.transform.localScale = Vector3.one * 0.1f;

    //Add and configure ASA components
    CloudNativeAnchor cloudNativeAnchor = anchorGameObject.AddComponent<CloudNativeAnchor>();
    await cloudNativeAnchor.NativeToCloud();
    CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;
    cloudSpatialAnchor.Expiration = DateTimeOffset.Now.AddDays(3);

    //Collect Environment Data
    while (!_spatialAnchorManager.IsReadyForCreate)
    {
        float createProgress = _spatialAnchorManager.SessionStatus.RecommendedForCreateProgress;
        Debug.Log($"ASA - Move your device to capture more environment data: {createProgress:0%}");
    }

}

注意

HoloLens は、アンカーを囲んでいる既にキャプチャされた環境データを再利用できる可能性があります。その結果、初めて呼び出された時に IsReadyForCreate は既に true になります。

クラウド空間アンカーが準備されたので、ここで実際の保存を試しに行います。

/// <summary>
/// Creates an Azure Spatial Anchor at the given position rotated towards the user
/// </summary>
/// <param name="position">Position where Azure Spatial Anchor will be created</param>
/// <returns>Async Task</returns>
private async Task CreateAnchor(Vector3 position)
{
    //Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
    if (!InputDevices.GetDeviceAtXRNode(XRNode.Head).TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition))
    {
        headPosition = Vector3.zero;
    }

    Quaternion orientationTowardsHead = Quaternion.LookRotation(position - headPosition, Vector3.up);

    GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
    anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
    anchorGameObject.transform.position = position;
    anchorGameObject.transform.rotation = orientationTowardsHead;
    anchorGameObject.transform.localScale = Vector3.one * 0.1f;

    //Add and configure ASA components
    CloudNativeAnchor cloudNativeAnchor = anchorGameObject.AddComponent<CloudNativeAnchor>();
    await cloudNativeAnchor.NativeToCloud();
    CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;
    cloudSpatialAnchor.Expiration = DateTimeOffset.Now.AddDays(3);

    //Collect Environment Data
    while (!_spatialAnchorManager.IsReadyForCreate)
    {
        float createProgress = _spatialAnchorManager.SessionStatus.RecommendedForCreateProgress;
        Debug.Log($"ASA - Move your device to capture more environment data: {createProgress:0%}");
    }

    Debug.Log($"ASA - Saving cloud anchor... ");

    try
    {
        // Now that the cloud spatial anchor has been prepared, we can try the actual save here.
        await _spatialAnchorManager.CreateAnchorAsync(cloudSpatialAnchor);

        bool saveSucceeded = cloudSpatialAnchor != null;
        if (!saveSucceeded)
        {
            Debug.LogError("ASA - Failed to save, but no exception was thrown.");
            return;
        }

        Debug.Log($"ASA - Saved cloud anchor with ID: {cloudSpatialAnchor.Identifier}");
        _foundOrCreatedAnchorGameObjects.Add(anchorGameObject);
        _createdAnchorIDs.Add(cloudSpatialAnchor.Identifier);
        anchorGameObject.GetComponent<MeshRenderer>().material.color = Color.green;
    }
    catch (Exception exception)
    {
        Debug.Log("ASA - Failed to save anchor: " + exception.ToString());
        Debug.LogException(exception);
    }
}

最後に、 関数呼び出しを ShortTap メソッドに追加します

/// <summary>
/// Called when a user is air tapping for a short time 
/// </summary>
/// <param name="handPosition">Location where tap was registered</param>
private async void ShortTap(Vector3 handPosition)
{
    await _spatialAnchorManager.StartSessionAsync();
        await CreateAnchor(handPosition);
}

アプリで複数のアンカーを作成できます。 どのデバイスも、アンカーの ID がわかり、Azure 上の同じ Spatial Anchors リソースにアクセスできる限り、作成したアンカー (まだ期限切れではない場合) を見つけることができます。

セッションの停止とゲームオブジェクトの破棄

すべてのアンカーを見つける 2 つ目のデバイスをエミュレートするために、セッションを停止し、すべてのアンカー GameObjects を削除します (アンカー ID は保持します)。 その後、新しいセッションを開始し、保存されているアンカー ID を使用してアンカーにクエリを実行します。

SpatialAnchorManager は、DestroySession() メソッドを呼び出すだけでセッションの停止を処理できます。 これを LongTap() メソッドに追加しましょう

/// <summary>
/// Called when a user is air tapping for a long time (>=2 sec)
/// </summary>
private async void LongTap()
{
        _spatialAnchorManager.DestroySession();
}

すべてのアンカー GameObjects を削除するメソッドを作成しましょう

/// <summary>
/// Destroys all Anchor GameObjects
/// </summary>
private void RemoveAllAnchorGameObjects()
{
    foreach (var anchorGameObject in _foundOrCreatedAnchorGameObjects)
    {
        Destroy(anchorGameObject);
    }
    _foundOrCreatedAnchorGameObjects.Clear();
}

LongTap() でセッションを破棄した後、それを呼び出します

/// <summary>
/// Called when a user is air tapping for a long time (>=2 sec)
/// </summary>
private async void LongTap()
{
        // Stop Session and remove all GameObjects. This does not delete the Anchors in the cloud
        _spatialAnchorManager.DestroySession();
        RemoveAllAnchorGameObjects();
        Debug.Log("ASA - Stopped Session and removed all Anchor Objects");
}

アンカーを見つける

ここで、アンカーを作成した正しい位置と回転でアンカーを再び検索します。 これを行うには、セッションを開始し、特定の条件に適合するアンカーを探す Watcher を作成する必要があります。 条件として、先ほど作成したアンカーの ID をフィードします。 メソッド LocateAnchor() を作成し、SpatialAnchorManager を使用して Watcher を作成しましょう。 アンカーの ID を使用する以外の検索方法については、「アンカーの検索方法」を参照してください

/// <summary>
/// Looking for anchors with ID in _createdAnchorIDs
/// </summary>
private void LocateAnchor()
{
    if (_createdAnchorIDs.Count > 0)
    {
        //Create watcher to look for all stored anchor IDs
        Debug.Log($"ASA - Creating watcher to look for {_createdAnchorIDs.Count} spatial anchors");
        AnchorLocateCriteria anchorLocateCriteria = new AnchorLocateCriteria();
        anchorLocateCriteria.Identifiers = _createdAnchorIDs.ToArray();
        _spatialAnchorManager.Session.CreateWatcher(anchorLocateCriteria);
        Debug.Log($"ASA - Watcher created!");
    }
}

Watcher が開始すると、指定された条件に適合するアンカーが見つかったときにコールバックが発生します。 まず、Watcher がアンカーを探す時に呼び出されるように構成する SpatialAnchorManager_AnchorLocated() というアンカー配置メソッドを作成しましょう。 このメソッドは、ビジュアル GameObject を作成し、それにネイティブ アンカー コンポーネントをアタッチします。 ネイティブ アンカー コンポーネントは、GameObject の正しい位置と回転が設定されていることを確認します。

作成プロセスと同様に、アンカーは GameObject にアタッチされます。 Spatial Anchors が機能するために、この GameObject をシーンで表示する必要はありません。 このチュートリアルでは、各アンカーが配置された後、青色のキューブとして視覚化します。 アンカーを使用して共有座標系を確立するだけの場合は、作成された GameObject を視覚化する必要はありません。

/// <summary>
/// Callback when an anchor is located
/// </summary>
/// <param name="sender">Callback sender</param>
/// <param name="args">Callback AnchorLocatedEventArgs</param>
private void SpatialAnchorManager_AnchorLocated(object sender, AnchorLocatedEventArgs args)
{
    Debug.Log($"ASA - Anchor recognized as a possible anchor {args.Identifier} {args.Status}");

    if (args.Status == LocateAnchorStatus.Located)
    {
        //Creating and adjusting GameObjects have to run on the main thread. We are using the UnityDispatcher to make sure this happens.
        UnityDispatcher.InvokeOnAppThread(() =>
        {
            // Read out Cloud Anchor values
            CloudSpatialAnchor cloudSpatialAnchor = args.Anchor;

            //Create GameObject
            GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
            anchorGameObject.transform.localScale = Vector3.one * 0.1f;
            anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
            anchorGameObject.GetComponent<MeshRenderer>().material.color = Color.blue;

            // Link to Cloud Anchor
            anchorGameObject.AddComponent<CloudNativeAnchor>().CloudToNative(cloudSpatialAnchor);
            _foundOrCreatedAnchorGameObjects.Add(anchorGameObject);
        });
    }
}

次に、SpatialAnchorManager から AnchorLocated コールバックをサブスクライブして、Watcher がアンカーを見つけたら SpatialAnchorManager_AnchorLocated() メソッドが呼び出されるのを確認しましょう。

// Start is called before the first frame update
void Start()
{
    _spatialAnchorManager = GetComponent<SpatialAnchorManager>();
    _spatialAnchorManager.LogDebug += (sender, args) => Debug.Log($"ASA - Debug: {args.Message}");
    _spatialAnchorManager.Error += (sender, args) => Debug.LogError($"ASA - Error: {args.ErrorMessage}");
    _spatialAnchorManager.AnchorLocated += SpatialAnchorManager_AnchorLocated;
}

最後に、アンカーの検索を含める LongTap() メソッドを展開しましょう。 「IsSessionStarted」に関する説明に従い、IsSessionStarted boolean を使用して、すべてのアンカーを探すのか、すべてのアンカーを破棄するかを決めます

/// <summary>
/// Called when a user is air tapping for a long time (>=2 sec)
/// </summary>
private async void LongTap()
{
    if (_spatialAnchorManager.IsSessionStarted)
    {
        // Stop Session and remove all GameObjects. This does not delete the Anchors in the cloud
        _spatialAnchorManager.DestroySession();
        RemoveAllAnchorGameObjects();
        Debug.Log("ASA - Stopped Session and removed all Anchor Objects");
    }
    else
    {
        //Start session and search for all Anchors previously created
        await _spatialAnchorManager.StartSessionAsync();
        LocateAnchor();
    }
}

試してみましょう 2

これで、アプリでアンカーの作成と検索がサポートされます。 Unity でアプリをビルドし、「Visual Studio を使用した配置とデバッグ」に従って Visual Studio からデプロイします。

HoloLens がインターネットに接続されていることを確認します。 アプリが起動し Unity で作成されました というメッセージが消えたら、周囲を短くタップします。 作成するアンカーの位置と回転を示す白いキューブが表示されます。 アンカーの作成プロセスが自動的に呼び出されます。 周囲をゆっくりと見回す時は、環境データをキャプチャしています。 十分な環境データが収集された後、アプリは指定された場所にアンカーを作成します。 アンカーの作成プロセスが完了すると、キューブは緑色に変わります。 Visual Studio でデバッグ ログを確認し、すべてが意図した通り動作したのか確認します。

長押ししてシーンから GameObjects をすべて削除し、空間アンカー セッションを停止します。

シーンがクリアされたら、もう一度長押ししてセッションを開始し、以前に作成したアンカーを探します。 見つかったら、固定された位置と回転で青いキューブによって視覚化されます。 これらのアンカー (期限切れではない限り) は、適切なアンカー ID を持ち、空間アンカー リソースにアクセスできる限り、サポートされている任意のデバイスで見つかる可能性があります。

アンカーの削除

現在、アプリでアンカーを作成して検索できます。 GameObjectsを削除しても、クラウド内のアンカーは削除されません。 既存のアンカーをタップした時にクラウドでも削除する機能を追加しましょう。

GameObjectを受け取るメソッド DeleteAnchor を追加しましょう。 その後 SpatialAnchorManager をオブジェクトの CloudNativeAnchor コンポーネントと一緒に使用し、クラウド内のアンカーの削除を要求します。

/// <summary>
/// Deleting Cloud Anchor attached to the given GameObject and deleting the GameObject
/// </summary>
/// <param name="anchorGameObject">Anchor GameObject that is to be deleted</param>
private async void DeleteAnchor(GameObject anchorGameObject)
{
    CloudNativeAnchor cloudNativeAnchor = anchorGameObject.GetComponent<CloudNativeAnchor>();
    CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;

    Debug.Log($"ASA - Deleting cloud anchor: {cloudSpatialAnchor.Identifier}");

    //Request Deletion of Cloud Anchor
    await _spatialAnchorManager.DeleteAnchorAsync(cloudSpatialAnchor);

    //Remove local references
    _createdAnchorIDs.Remove(cloudSpatialAnchor.Identifier);
    _foundOrCreatedAnchorGameObjects.Remove(anchorGameObject);
    Destroy(anchorGameObject);

    Debug.Log($"ASA - Cloud anchor deleted!");
}

ShortTap からこのメソッドを呼び出すには、タップが既存の表示されているアンカーの近くにあるかどうかを判断できる必要があります。 その処理を行うヘルパー メソッドを作成しましょう

using System.Linq;
/// <summary>
/// Returns true if an Anchor GameObject is within 15cm of the received reference position
/// </summary>
/// <param name="position">Reference position</param>
/// <param name="anchorGameObject">Anchor GameObject within 15cm of received position. Not necessarily the nearest to this position. If no AnchorObject is within 15cm, this value will be null</param>
/// <returns>True if a Anchor GameObject is within 15cm</returns>
private bool IsAnchorNearby(Vector3 position, out GameObject anchorGameObject)
{
    anchorGameObject = null;

    if (_foundOrCreatedAnchorGameObjects.Count <= 0)
    {
        return false;
    }

    //Iterate over existing anchor gameobjects to find the nearest
    var (distance, closestObject) = _foundOrCreatedAnchorGameObjects.Aggregate(
        new Tuple<float, GameObject>(Mathf.Infinity, null),
        (minPair, gameobject) =>
        {
            Vector3 gameObjectPosition = gameobject.transform.position;
            float distance = (position - gameObjectPosition).magnitude;
            return distance < minPair.Item1 ? new Tuple<float, GameObject>(distance, gameobject) : minPair;
        });

    if (distance <= 0.15f)
    {
        //Found an anchor within 15cm
        anchorGameObject = closestObject;
        return true;
    }
    else
    {
        return false;
    }
}

DeleteAnchor 呼び出しを含むよう ShortTap メソッドを拡張できます

/// <summary>
/// Called when a user is air tapping for a short time 
/// </summary>
/// <param name="handPosition">Location where tap was registered</param>
private async void ShortTap(Vector3 handPosition)
{
    await _spatialAnchorManager.StartSessionAsync();
    if (!IsAnchorNearby(handPosition, out GameObject anchorGameObject))
    {
        //No Anchor Nearby, start session and create an anchor
        await CreateAnchor(handPosition);
    }
    else
    {
        //Delete nearby Anchor
        DeleteAnchor(anchorGameObject);
    }
}

試してみましょう 3

Unity でアプリをビルドし、「Visual Studio を使用した配置とデバッグ」に従って Visual Studio からデプロイします。

このアプリでは、ハンド タップ ジェスチャの位置は指先ではなく手の中心です。

作成された (緑) または配置された (青) いずれかのアンカーをタップすると、アカウントからこのアンカーを削除するための要求が空間アンカー サービスに送信されます。 セッションを停止し (長押し)、セッションを再度開始して (長押し)、すべてのアンカーを検索します。 削除されたアンカーは見つからなくなります。

すべてをまとめる

ここでは、さまざまな要素がすべてまとめられた後に完全な AzureSpatialAnchorsScript クラス ファイルがどのようになるかを示します。 これを参照用に使用してご自身のファイルと比較し、違いが残っているかどうかを特定できます。

Note

[RequireComponent(typeof(SpatialAnchorManager))] がスクリプトに含まれていることに注意してください。 これにより、Unity は、AzureSpatialAnchorsScript をアタッチする GameObject に SpatialAnchorManager もアタッチされていることを確認します。

using Microsoft.Azure.SpatialAnchors;
using Microsoft.Azure.SpatialAnchors.Unity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.XR;


[RequireComponent(typeof(SpatialAnchorManager))]
public class AzureSpatialAnchorsScript : MonoBehaviour
{
    /// <summary>
    /// Used to distinguish short taps and long taps
    /// </summary>
    private float[] _tappingTimer = { 0, 0 };

    /// <summary>
    /// Main interface to anything Spatial Anchors related
    /// </summary>
    private SpatialAnchorManager _spatialAnchorManager = null;

    /// <summary>
    /// Used to keep track of all GameObjects that represent a found or created anchor
    /// </summary>
    private List<GameObject> _foundOrCreatedAnchorGameObjects = new List<GameObject>();

    /// <summary>
    /// Used to keep track of all the created Anchor IDs
    /// </summary>
    private List<String> _createdAnchorIDs = new List<String>();

    // <Start>
    // Start is called before the first frame update
    void Start()
    {
        _spatialAnchorManager = GetComponent<SpatialAnchorManager>();
        _spatialAnchorManager.LogDebug += (sender, args) => Debug.Log($"ASA - Debug: {args.Message}");
        _spatialAnchorManager.Error += (sender, args) => Debug.LogError($"ASA - Error: {args.ErrorMessage}");
        _spatialAnchorManager.AnchorLocated += SpatialAnchorManager_AnchorLocated;
    }
    // </Start>

    // <Update>
    // Update is called once per frame
    void Update()
    {

        //Check for any air taps from either hand
        for (int i = 0; i < 2; i++)
        {
            InputDevice device = InputDevices.GetDeviceAtXRNode((i == 0) ? XRNode.RightHand : XRNode.LeftHand);
            if (device.TryGetFeatureValue(CommonUsages.primaryButton, out bool isTapping))
            {
                if (!isTapping)
                {
                    //Stopped Tapping or wasn't tapping
                    if (0f < _tappingTimer[i] && _tappingTimer[i] < 1f)
                    {
                        //User has been tapping for less than 1 sec. Get hand position and call ShortTap
                        if (device.TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 handPosition))
                        {
                            ShortTap(handPosition);
                        }
                    }
                    _tappingTimer[i] = 0;
                }
                else
                {
                    _tappingTimer[i] += Time.deltaTime;
                    if (_tappingTimer[i] >= 2f)
                    {
                        //User has been air tapping for at least 2sec. Get hand position and call LongTap
                        if (device.TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 handPosition))
                        {
                            LongTap();
                        }
                        _tappingTimer[i] = -float.MaxValue; // reset the timer, to avoid retriggering if user is still holding tap
                    }
                }
            }

        }
    }
    // </Update>


    // <ShortTap>
    /// <summary>
    /// Called when a user is air tapping for a short time 
    /// </summary>
    /// <param name="handPosition">Location where tap was registered</param>
    private async void ShortTap(Vector3 handPosition)
    {
        await _spatialAnchorManager.StartSessionAsync();
        if (!IsAnchorNearby(handPosition, out GameObject anchorGameObject))
        {
            //No Anchor Nearby, start session and create an anchor
            await CreateAnchor(handPosition);
        }
        else
        {
            //Delete nearby Anchor
            DeleteAnchor(anchorGameObject);
        }
    }
    // </ShortTap>

    // <LongTap>
    /// <summary>
    /// Called when a user is air tapping for a long time (>=2 sec)
    /// </summary>
    private async void LongTap()
    {
        if (_spatialAnchorManager.IsSessionStarted)
        {
            // Stop Session and remove all GameObjects. This does not delete the Anchors in the cloud
            _spatialAnchorManager.DestroySession();
            RemoveAllAnchorGameObjects();
            Debug.Log("ASA - Stopped Session and removed all Anchor Objects");
        }
        else
        {
            //Start session and search for all Anchors previously created
            await _spatialAnchorManager.StartSessionAsync();
            LocateAnchor();
        }
    }
    // </LongTap>

    // <RemoveAllAnchorGameObjects>
    /// <summary>
    /// Destroys all Anchor GameObjects
    /// </summary>
    private void RemoveAllAnchorGameObjects()
    {
        foreach (var anchorGameObject in _foundOrCreatedAnchorGameObjects)
        {
            Destroy(anchorGameObject);
        }
        _foundOrCreatedAnchorGameObjects.Clear();
    }
    // </RemoveAllAnchorGameObjects>

    // <IsAnchorNearby>
    /// <summary>
    /// Returns true if an Anchor GameObject is within 15cm of the received reference position
    /// </summary>
    /// <param name="position">Reference position</param>
    /// <param name="anchorGameObject">Anchor GameObject within 15cm of received position. Not necessarily the nearest to this position. If no AnchorObject is within 15cm, this value will be null</param>
    /// <returns>True if a Anchor GameObject is within 15cm</returns>
    private bool IsAnchorNearby(Vector3 position, out GameObject anchorGameObject)
    {
        anchorGameObject = null;

        if (_foundOrCreatedAnchorGameObjects.Count <= 0)
        {
            return false;
        }

        //Iterate over existing anchor gameobjects to find the nearest
        var (distance, closestObject) = _foundOrCreatedAnchorGameObjects.Aggregate(
            new Tuple<float, GameObject>(Mathf.Infinity, null),
            (minPair, gameobject) =>
            {
                Vector3 gameObjectPosition = gameobject.transform.position;
                float distance = (position - gameObjectPosition).magnitude;
                return distance < minPair.Item1 ? new Tuple<float, GameObject>(distance, gameobject) : minPair;
            });

        if (distance <= 0.15f)
        {
            //Found an anchor within 15cm
            anchorGameObject = closestObject;
            return true;
        }
        else
        {
            return false;
        }
    }
    // </IsAnchorNearby>
  
    // <CreateAnchor>
    /// <summary>
    /// Creates an Azure Spatial Anchor at the given position rotated towards the user
    /// </summary>
    /// <param name="position">Position where Azure Spatial Anchor will be created</param>
    /// <returns>Async Task</returns>
    private async Task CreateAnchor(Vector3 position)
    {
        //Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
        if (!InputDevices.GetDeviceAtXRNode(XRNode.Head).TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition))
        {
            headPosition = Vector3.zero;
        }

        Quaternion orientationTowardsHead = Quaternion.LookRotation(position - headPosition, Vector3.up);

        GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
        anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
        anchorGameObject.transform.position = position;
        anchorGameObject.transform.rotation = orientationTowardsHead;
        anchorGameObject.transform.localScale = Vector3.one * 0.1f;

        //Add and configure ASA components
        CloudNativeAnchor cloudNativeAnchor = anchorGameObject.AddComponent<CloudNativeAnchor>();
        await cloudNativeAnchor.NativeToCloud();
        CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;
        cloudSpatialAnchor.Expiration = DateTimeOffset.Now.AddDays(3);

        //Collect Environment Data
        while (!_spatialAnchorManager.IsReadyForCreate)
        {
            float createProgress = _spatialAnchorManager.SessionStatus.RecommendedForCreateProgress;
            Debug.Log($"ASA - Move your device to capture more environment data: {createProgress:0%}");
        }

        Debug.Log($"ASA - Saving cloud anchor... ");

        try
        {
            // Now that the cloud spatial anchor has been prepared, we can try the actual save here.
            await _spatialAnchorManager.CreateAnchorAsync(cloudSpatialAnchor);

            bool saveSucceeded = cloudSpatialAnchor != null;
            if (!saveSucceeded)
            {
                Debug.LogError("ASA - Failed to save, but no exception was thrown.");
                return;
            }

            Debug.Log($"ASA - Saved cloud anchor with ID: {cloudSpatialAnchor.Identifier}");
            _foundOrCreatedAnchorGameObjects.Add(anchorGameObject);
            _createdAnchorIDs.Add(cloudSpatialAnchor.Identifier);
            anchorGameObject.GetComponent<MeshRenderer>().material.color = Color.green;
        }
        catch (Exception exception)
        {
            Debug.Log("ASA - Failed to save anchor: " + exception.ToString());
            Debug.LogException(exception);
        }
    }
    // </CreateAnchor>

    // <LocateAnchor>
    /// <summary>
    /// Looking for anchors with ID in _createdAnchorIDs
    /// </summary>
    private void LocateAnchor()
    {
        if (_createdAnchorIDs.Count > 0)
        {
            //Create watcher to look for all stored anchor IDs
            Debug.Log($"ASA - Creating watcher to look for {_createdAnchorIDs.Count} spatial anchors");
            AnchorLocateCriteria anchorLocateCriteria = new AnchorLocateCriteria();
            anchorLocateCriteria.Identifiers = _createdAnchorIDs.ToArray();
            _spatialAnchorManager.Session.CreateWatcher(anchorLocateCriteria);
            Debug.Log($"ASA - Watcher created!");
        }
    }
    // </LocateAnchor>

    // <SpatialAnchorManagerAnchorLocated>
    /// <summary>
    /// Callback when an anchor is located
    /// </summary>
    /// <param name="sender">Callback sender</param>
    /// <param name="args">Callback AnchorLocatedEventArgs</param>
    private void SpatialAnchorManager_AnchorLocated(object sender, AnchorLocatedEventArgs args)
    {
        Debug.Log($"ASA - Anchor recognized as a possible anchor {args.Identifier} {args.Status}");

        if (args.Status == LocateAnchorStatus.Located)
        {
            //Creating and adjusting GameObjects have to run on the main thread. We are using the UnityDispatcher to make sure this happens.
            UnityDispatcher.InvokeOnAppThread(() =>
            {
                // Read out Cloud Anchor values
                CloudSpatialAnchor cloudSpatialAnchor = args.Anchor;

                //Create GameObject
                GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
                anchorGameObject.transform.localScale = Vector3.one * 0.1f;
                anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
                anchorGameObject.GetComponent<MeshRenderer>().material.color = Color.blue;

                // Link to Cloud Anchor
                anchorGameObject.AddComponent<CloudNativeAnchor>().CloudToNative(cloudSpatialAnchor);
                _foundOrCreatedAnchorGameObjects.Add(anchorGameObject);
            });
        }
    }
    // </SpatialAnchorManagerAnchorLocated>

    // <DeleteAnchor>
    /// <summary>
    /// Deleting Cloud Anchor attached to the given GameObject and deleting the GameObject
    /// </summary>
    /// <param name="anchorGameObject">Anchor GameObject that is to be deleted</param>
    private async void DeleteAnchor(GameObject anchorGameObject)
    {
        CloudNativeAnchor cloudNativeAnchor = anchorGameObject.GetComponent<CloudNativeAnchor>();
        CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;

        Debug.Log($"ASA - Deleting cloud anchor: {cloudSpatialAnchor.Identifier}");

        //Request Deletion of Cloud Anchor
        await _spatialAnchorManager.DeleteAnchorAsync(cloudSpatialAnchor);

        //Remove local references
        _createdAnchorIDs.Remove(cloudSpatialAnchor.Identifier);
        _foundOrCreatedAnchorGameObjects.Remove(anchorGameObject);
        Destroy(anchorGameObject);

        Debug.Log($"ASA - Cloud anchor deleted!");
    }
    // </DeleteAnchor>

}

次のステップ

このチュートリアルでは、Unity を使用して HoloLens 用の基本的なSpatial Anchors アプリケーションを実装する方法について学びました。 新しい Android アプリで Azure Spatial Anchors を使用する方法について詳しくは、次のチュートリアルに進んでください。