Azure Spatial Anchorsの調査③

本記事はAzure Spatial Anchorsの調査②の続きであり、最後の記事となります。
Azure Spatial Anchorsを使用して、複数のアンカーを配置したり、アンカーにリレーションをつけたり、アンカーの再現をしたりする検証アプリを作成しました。これらの実装を行う上でのポイントについて紹介していきます。

検証アプリの動画

実装のポイント

①セッションの開始

Azure Spatial Anchorsを使用するためには、セッションを生成しておく必要があります。

private async Task StartSessionAsync()
{
    // セッションの作成
    if (m_SpatialAnchorManager.Session == null)
    {
        await m_SpatialAnchorManager.CreateSessionAsync();
    }
    // セッションの開始
    if (!m_SpatialAnchorManager.IsSessionStarted)
    {
        await m_SpatialAnchorManager.StartSessionAsync();
    }
}

②アンカー情報の保存

アンカー情報を保存する上で、下記の2点がポイントとなります。
クラウドにアップロードしたCloudSpatialAnchor(アンカー情報)を再現するためには、CoundSpatialAnchor.Identifier(アンカー識別子)が必要となるため、サーバーorローカル問わずどこかに永続化しておく必要があります。

  • CloudSpatialAnchorをクラウドにアップロード
  • CoundSpatialAnchor.Identifierを永続化(今回はローカルに保存)
private async Task SaveAnchorAsync(GameObject anchorObject)
{
    // CloudNativeAnchorコンポーネントを取得
    CloudNativeAnchor cna = anchorObject.GetComponent<CloudNativeAnchor>();

    // CloudAnchorのCloud Positionが未生成の場合、生成する
    if (cna.CloudAnchor == null) await cna.NativeToCloud();
    CloudSpatialAnchor cloudAnchor = cna.CloudAnchor;

    // アンカーの有効期限を設定
    cloudAnchor.Expiration = DateTimeOffset.Now.AddDays(7);

    // 現実空間の特徴点の収集が十分であるかの判定
    while (!m_SpatialAnchorManager.IsReadyForCreate)
    {
        await Task.Delay(330);
        float createProgress = m_SpatialAnchorManager.SessionStatus.RecommendedForCreateProgress;
        Debug.Log($"Move your device to capture more environment data: {createProgress:0%}");
    }

    try
    {
        // クラウドにアンカーを保存
        await m_SpatialAnchorManager.CreateAnchorAsync(cloudAnchor);

        // ローカルにアンカーIdentifierを保存
        SaveAnchorIdentifier(cloudAnchor.Identifier);

        Debug.Log($"Saved anchor. Idendifier is {cloudAnchor.Identifier}");

    }
    catch (Exception exception)
    {
        Debug.LogException(exception);
        Debug.Log("Failed to save anchor " + exception.ToString());
    }
}

private void SaveAnchorIdentifier(string identifier)
{
    File.AppendAllText("任意のファイルパス", identifier + Environment.NewLine);
}

③保存したアンカーの再現

保存したアンカーを再現するためには下記のような手順が必要となります。

1. ウォッチャーを生成

AnchorLocateCriteria.Identifiersに永続化しておいたCoundSpatialAnchor.Identifierを設定し、ウォッチャーを生成します。

private CloudSpatialAnchorWatcher CreateWatcher()
{
    if ((m_SpatialAnchorManager != null) && (m_SpatialAnchorManager.Session != null))
    {
        AnchorLocateCriteria anchorLocateCriteria = new AnchorLocateCriteria();
        // ローカルに永続化したアンカーIdentiferを取得
        string[] identifiers = FileUtility.ReadFile();
        // AnchorLocateCriteriaにアンカーIdentifierを設定
        anchorLocateCriteria.Identifiers = identifiers;
        // ウォッチャーを生成
        return m_SpatialAnchorManager.Session.CreateWatcher(anchorLocateCriteria);
    }
    else
    {
        Debug.Log("Wacher cannot be created.");
        return null;
    }
}

private string[] ReadFile()
{
    if (!File.Exists("任意のファイルパス")) return null;

    string readText = File.ReadAllText("任意のファイルパス");
    readText = readText.Replace(Environment.NewLine, "\r");
    readText = readText.Trim('\r');
    var result = readText.Split('\r');
    if (result.Length == 1 && result[0].Equals(string.Empty)) result = null;

    return result;
}

2. SpatialAnchorManager.AnchorLocatedイベントでアンカーを再現

SpatialAnchorManager.AnchorLocatedイベントは、ウォッチャーに登録したアンカーが探知されたときor探知できなかったときに呼び出されます。 AnchorLocatedEventArgs.Statusを判定し、アンカーが探知できた(=LocateAnchorStatus.Located)ときにアンカーを再現します。このとき、Unityの機能を使用する際は、UnityDispatcher.InvokeOnAppThread()を使用し、Unityのメインスレッドで処理を行う必要があります。

private void Start()
{
    // 初期化の際にイベントを登録
    m_SpatialAnchorManager.AnchorLocated += SpatialAnchorManager_AnchorLocated;
}

private void SpatialAnchorManager_AnchorLocated(object sender, AnchorLocatedEventArgs args)
{
    Debug.LogFormat("Anchor recognized as a possible anchor {0} {1}", args.Identifier, args.Status);
    if (args.Status == LocateAnchorStatus.Located)
    {
        // 引数からCloudSpatialAnchorを取得
        var cloudAnchor = args.Anchor;

        // Unityのメインスレッドで処理
        UnityDispatcher.InvokeOnAppThread(() =>
        {
            Pose anchorPose = Pose.identity;
            // アンカーの位置を取得
            anchorPose = cloudAnchor.GetPose();
            // アンカーを生成
            var anchorObject = CreateAnchorObject(anchorPose.position, anchorPose.rotation);

            Debug.Log($"Reproduce anchor. Idendifier is {cloudAnchor.Identifier}");
        });
    }
}

private GameObject CreateAnchorObject(Vector3 worldPos, Quaternion worldRot)
{
    // アンカー用のGameObjectを生成
    GameObject anchorObject = Instantiate(m_AnchorPrefab.gameObject, worldPos, worldRot);

    // CloudNativeAnchorコンポーネントをアタッチ
    anchorObject.AddComponent<CloudNativeAnchor>();

    // 色を設定
    anchorObject.GetComponent<MeshRenderer>().material.color = Color.yellow;

    // コライダーを非アクティブ化
    anchorObject.GetComponent<BoxCollider>().enabled = false;

    return anchorObject;
}

3. SpatialAnchorManager.LocateAnchorsCompletedイベントで完了通知

SpatialAnchorManager.LocateAnchorsCompletedイベントはウォッチャーに登録したすべてのアンカーが処理された(探知されたかどうかを問わず)後に呼び出されます。 このとき、Unityの機能を使用する際は、UnityDispatcher.InvokeOnAppThread()を使用し、Unityのメインスレッドで処理を行う必要があります。

private void Start()
{
    // 初期化の際にイベントを登録
    m_SpatialAnchorManager.LocateAnchorsCompleted += SpatialAnchorManager_LocateAnchorsCompleted;
}

private void SpatialAnchorManager_LocateAnchorsCompleted(object sender, LocateAnchorsCompletedEventArgs args)
{
    Debug.Log($"アンカーの検索が完了し、{_existingCloudAnchors.Count}個のアンカーが見つかりました。");
    UnityDispatcher.InvokeOnAppThread(() =>
    {
        // do something...
    });
}

まとめ

HoloLens 2をマーカーレスでの使用を目指して、Azure Spatial Anchorsの調査を3本の記事に渡り行ってきました。
今まではQRコードを使った位置合わせが主流であったと思いますが、Azure Spatial Anchorsを使うことで現実空間の特徴点をマーカー代わりに使用することが可能となります。
Azure Spatial Anchorsをはじめとした、色々な技術やサービスを組み合わせることで、ユーザーが位置合わせをあまり意識しなくてもMR体験ができる未来もそう遠くないでしょう。