お手軽にSharingの接続成功率と精度をあげる

投稿者: | 2017-04-09

【注意】ワークアラウンド的な対応で、根本解決策ではありません!

HoloLensの目玉機能とも言える、MR表現された空間をHoloLens間で共有できるSharing。しかしその道は茨の道といわれ、日本だけでなく世界のユーザも頭を悩ましているようです。現在のSharingのハマりポイントについては、HoloLens MeetUP vol.2でじんぐるさん(@xin9le)が詳しく発表していただいておりその資料も公開されていますので、Sharingをやってみたい、という方は是非一読することをお勧めします。(今回の記事は下記の資料を読んでることを前提に書いてます)

そうはいえども、Sharing機能が難しいからと言って使わないのは非常にもったいないので、今回はタイトルにある通りお手軽にSharingを実現する方法をまとめたいと思います。

結論から先に

Sharingを実現する上で最も重要なことは、じんぐるさんの資料にもある通り、デバイス間でいかに座標系を共有するか?というところです。

Academy 240やHoloToolkitのサンプルでは、一番最初にアプリを起動したユーザが配置したアンカーについてのデータを他のユーザに送信し、そのアンカーが設置された地点を基準となる座標系の原点にする、というやり方です。しかしこの方法によるSharingは、成功率が著しく低いです(考察は後述)。

そこで何とか他にアンカーを共有する方法がないかと思っていろいろ考えてみたところ、HoloLensアプリのある特徴に着目しました。それはアプリ起動時のHoloLensの位置がワールド座標の原点であり、HoloLensの前方(カメラの向き)がZ軸の+方向になる、という点です。

ということは、アプリ起動時にHoloLensの前方(例えばワールド座標(0,0,2)とか)にアンカーを設置するようスクリプトを書いておきに、Sharingしたいユーザが物理的に同じ位置/向きでアプリを起動すれば、通信しなくても物理空間の同じ位置/向きにアンカーを配置したといえるのではないでしょうか。


(ひでー絵だな…)

実装例は以下の通りです。

1. WorldAnchorManagerを配置する。

2. (0,0,2)にGameObjectを配置する(emptyでいい)。

3. 配置したGameObjectに以下のスクリプトをアタッチする。

using HoloToolkit.Unity;
using System;
using UnityEngine;

public class RefCoordinatesAnchor : MonoBehaviour {
    private Guid anchorID;
	void Start () {
        // 配置したGameObjectにアンカーを取り付けてるだけ
        anchorID = Guid.NewGuid();
        WorldAnchorManager.Instance.AttachAnchor(gameObject, anchorID.ToString());
	}

    private void OnDestroy() {
        WorldAnchorManager.Instance.RemoveAnchor(gameObject);
    }
}

4. SharingPrefabをシーンに配置し、IPアドレスをSharingService.exeを動かすマシンのものに設定する

以上です。あとはHoloToolkitのサンプルSharingTestシーンを参考に、サーバとの通信の送受信を担当するCustomMessage.csと、自分のHoloLens(頭)の位置の送信とリモートユーザのHoloLensの位置と受信を行うRemoteHeadManager.csのようなアプリケーションロジック部分を実装すればSharingアプリケーションの完成です。(SharingTestのアプリケーションロジック部分は次回の記事で説明します)

GitHubにサンプルをアップしましたので、よろしければご利用、ご参考ください。
https://github.com/dykarohora/EasySharing

私が思う、Sharingが失敗する最も厄介な要因(妄想成分多め)

Sharingが失敗するポイントとしてアンカーの送受信と前項で書きましたが、さらにいうとアンカーをデシリアライズするところが一番エラーが発生する箇所だと思っています。SharingTestシーンで言うところのImportExportAnchorManager.csの以下の部分です。(ここも次回の記事でもう少し詳しく説明します)

private void Update()
{
    // アンカーの処理ステータスによって分岐する
    switch (currentState)
    {
        // If the local anchor store is initialized.
        case ImportExportState.Ready:
            if (sharingServiceReady)
            {
                StartCoroutine(InitRoomApi());
            }
            break;
        case ImportExportState.RoomApiInitialized:
            StartAnchorProcess();
            break;

        // アンカーのダウンロードが完了するとここに来る
        // ここがヤヴァイ!!!!!!!!!!
        case ImportExportState.DataReady:
            currentState = ImportExportState.Importing;
            // アンカーのインポート(たぶんデシリアライズ)
            WorldAnchorTransferBatch.ImportAsync(rawAnchorData, ImportComplete);
            break;

        case ImportExportState.InitialAnchorRequired:
            currentState = ImportExportState.CreatingInitialAnchor;
            CreateAnchorLocally();
            break;
        case ImportExportState.ReadyToExportInitialAnchor:
            // We've created an anchor locally and it is ready to export.
            currentState = ImportExportState.UploadingInitialAnchor;
            Export();
            break;
    }
}
        

// アンカーのデシリアライズが完了したときのコールバック
private void ImportComplete(SerializationCompletionReason status, WorldAnchorTransferBatch anchorBatch)
{
    if (status == SerializationCompletionReason.Succeeded)
    {
        if (anchorBatch.GetAllIds().Length > 0)
        {
            string first = anchorBatch.GetAllIds()[0];
            if (SharingStage.Instance.ShowDetailedLogs)
            {
                Debug.Log("Anchor Manager: Sucessfully imported anchor " + first);
            }
            if (AnchorDebugText != null)
            {
                AnchorDebugText.text += string.Format("\nSucessfully imported anchor " + first);
            }
            // デシリアライズしたアンカーをgameObject(ここではHologramCollection)に取り付ける
            // ここもかなり怪しい
            WorldAnchor anchor = anchorBatch.LockObject(first, gameObject);
            anchorStore.Save(first, anchor);
        }
        AnchorLoadComplete();
    }
    else
    {
        Debug.LogError("Anchor Manager: Import failed");
        if (AnchorDebugText != null)
        {
            AnchorDebugText.text += string.Format("\nImport failed");
        }
        currentState = ImportExportState.DataReady;
    }
}

どうしてアンカーのデシリアライズで失敗してしまうのか調べてみたところ、MSのソフトウェアエンジニアのNeeraj Wadhwaさんの以下のやり取りを見つけました。

https://forums.hololens.com/discussion/1171/worldanchortransferbatch-failures

要は、「WorldHAnchorTransferBatchを使ったアンカーのデシリアライズが全然うまくいかないZE!どうにかしてくれYO!」という書き込みなのですが、そのやり取りの中でNeeraj Wadhwaさんが以下のようにおっしゃっています。

Anchors work based on tracking and not spatial mapping really.

どうやらアンカーデータは空間情報(デプスデータ)ではなく、HoloLensのトラッキングで行われているとのことです。HoloLensには4台の環境認識カメラが搭載されており、このカメラ群で撮影した画像をもとに特徴点抽出をおこないセルフトラッキングを行っているのだと思います(自信はない)。したがってアンカーデータにはこの特徴点情報が主に含まれているのだと思います。(この辺も参考になるかもGoogle Tango AreaLearning)

なのでアンカーをデシリアライズして送信元のHoloLensが持っている特徴点データと、受信者のHoloLensが持っている特徴点データを何らかの方法で評価して受信者のアンカーを配置しているのだと思います。

したがってアンカーを共有する際には空間マップではなく、アンカー送信元と同じような特徴点データを受信側もHoloLensに持っていないといけないのだと思います。特徴点データはおそらく環境認識カメラで取得しているため、例えば部屋の対角線上にいるユーザ間ではカメラが撮影している映像が全く異なるためSharingに失敗する可能性が上がると思います。もしかするとアンカー送信元と同じ位置、同じ動きをすると成功率は上がるのかもしれません(誰か試してみてください)。でもこれって結構めんどくさそうですよね?厄介だ。

まとめ

今回はSharingを簡単に実現する方法についてまとめました。少しでも皆さんが良いSharing体験ができれば幸いです。
ただ、今回紹介した方法は冒頭で但し書きの通り、万能的な解決方法ではありません。少し思いつくだけでも以下のような課題があると思っています。

アプリ起動中は基準点が補正されない
こちらの方法はアプリ起動時に基準となるアンカーを設置し、以後はいっさいアンカーに関するものは操作しないようにしているため、アンカーがドリフトしてしまっても補正されることはありません。多少のズレでしたらSharingの性質上ユーザ間の共有体験に支障はありませんが、長時間の利用でズレが大きくなってしまった場合は体験に違和感が生じると思います。デモの実施くらいでしたら支障はないと思いますが、どこまで耐えられるかはわかりません。
透過的にSharingを体験させることができない
今回はすべてのユーザが物理的に同じ場所、同じ向きでアプリを起動するようにしなくてはいけない、という制約があります。現時点ではその制約も許容できると思いますが、理想的にはユーザがアプリを起動するだけで起動位置に関係なくSharingできるというのが理想だと思います。

まだまだ改善の余地があるSharingですが、まずはその醍醐味を体験するのが一番かと思いますので、皆さん積極的に実装していきましょう。
(個人的な思いとして、じんぐるさんとMIROさん(@MobileHackerz)にSharingの神髄をご教授いただきたいです)

なお、HoloToolkitサンプルのSharingTestのコードはとても参考になったので、次回の記事でまとめたいと思います。