HoloLens (第 1 世代) 共有 240 - 複数の HoloLens デバイス

重要

Mixed Reality Academy のチュートリアルは、HoloLens (第 1 世代)、Unity 2017、Mixed Reality イマーシブ ヘッドセットを考慮して設計されています。 そのため、それらのデバイスの開発に関するガイダンスを引き続き探している開発者のために、これらのチュートリアルをそのまま残しておくことが重要だと考えています。 これらのチュートリアルは、HoloLens 2 に使用される最新のツールセットや対話式操作を反映して更新されることは "ありません"。また、最新バージョンの Unity には対応していない可能性があります。 これらは、サポートされているデバイス上で継続して動作するように、保守されます。 HoloLens 2 向けには、新しいチュートリアル シリーズが投稿されています。

ホログラムは、私たちが空間内をあちこち移動する動きに従って適切な位置を保つようにすることで、仮想世界の中で存在感が与えられます。 HoloLens はホログラムを適切な位置に保つために、さまざまな座標系を使用してオブジェクトの位置と向きを追跡します。 このような座標系をデバイス間で共有すると、共有のエクスペリエンスを生み出すことができ、共有されたホログラフィックの世界に参加できるようになります。

このチュートリアルでは、次のことを行います。

  • 共有エクスペリエンス用にネットワークをセットアップする。
  • HoloLens デバイス間でホログラムを共有する。
  • 共有されたホログラフィックの世界で他のユーザーを検出する。
  • 他のプレイヤーをターゲットにして弾を発射するようなインタラクティブな共有エクスペリエンスを作成する。

デバイス サポート

コース HoloLens イマーシブ ヘッドセット
MR 共有 240:複数の HoloLens デバイス ✔️

開始する前に

前提条件

プロジェクト ファイル

  • プロジェクトに必要なファイルをダウンロードします。 Unity 2017.2 以降が必要です。
  • デスクトップなどのアクセスしやすい場所にファイルをアーカイブ解除します。 フォルダー名は SharedHolograms のままにします。

Note

ダウンロードする前にソース コードを確認する場合は、GitHub で参照できます。

第 1 章 - ホログラムの世界

この章では、最初の Unity プロジェクトをセットアップし、ビルドと配置のプロセスについて順を追って説明します。

目標

  • ホログラフィック アプリを開発するために Unity をセットアップする。
  • ホログラムを見る。

Instructions

  • Unity を起動します。
  • [Open] を選択します。
  • 前もってアーカイブ解除した「SharedHolograms」フォルダーの場所を入力します。
  • [プロジェクト名] を選び、[フォルダーの選択] をクリックします。
  • [階層] で、[メイン カメラ] を右クリックし、[削除] を選びます。
  • HoloToolkit-Sharing-240/Prefabs/Camera フォルダーで、Main Camera プレハブを探します。
  • [メイン カメラ][階層] にドラッグ アンド ドロップします。
  • [階層] で、[作成][空アイテムの作成] の順にクリックします。
  • 新しい GameObject を右クリックし、[名前の変更] を選択します。
  • GameObject の名前を HologramCollection に変更します。
  • [階層][HologramCollection] オブジェクトを選びます。
  • [インスペクター]変換位置X: 0, Y: -0.25, Z: 2 に設定します。
  • プロジェクト パネルHolograms フォルダーで、EnergyHub アセットを見つけます。
  • EnergyHub オブジェクトを プロジェクト パネルから 階層へ、HologramCollection の子になるようにドラッグ アンド ドロップします。
  • [ファイル] > [名前を付けてシーンを保存] の順に選びます。
  • シーンに SharedHolograms という名前を付け、[保存] をクリックします。
  • Unity の [再生] ボタンをクリックしてホログラムをプレビューします。
  • もう一度 [再生] を押すと、プレビュー モードが停止します。

Unity から Visual Studio にプロジェクトをエクスポートする

  • Unity で [ファイル] > [ビルド設定] の順に選択します。
  • [開いているシーンを追加] をクリックしてシーンを追加します。
  • [プラットフォーム] リストで [ユニバーサル Windows プラットフォーム] を選択し、[プラットフォームの切り替え] をクリックします。
  • [SDK][Universal 10] に設定します。
  • [ターゲット デバイス][HoloLens] に、[UWP ビルドの種類][D3D] に設定します。
  • [Unity C# プロジェクト] をオンにします。
  • [ビルド] をクリックします。
  • 表示されたエクスプローラー ウィンドウで、"App" という名前の新しいフォルダーを作成します。
  • [App] フォルダーを 1 回クリックします。
  • [フォルダーの選択] をクリックします。
  • Unity での作業が終了すると、エクスプローラー ウィンドウが表示されます。
  • [App] フォルダーを開きます。
  • SharedHolograms.sln を開いて、Visual Studio を起動します。
  • Visual Studio の上部のツールバーを使用して、ターゲットを [デバッグ] から [リリース] に、[ARM] から [X86] に変更します。
  • [ローカル コンピューター] の横にあるドロップダウン矢印をクリックし、[リモート デバイス] を選びます。
    • [アドレス] を、お使いの HoloLens の名前または IP アドレスに設定します。 デバイスの IP アドレスがわからない場合は、[ 設定] > [ネットワーク] & [インターネット > の詳細設定] を 確認するか、Cortana に 「Cortana さん、IP アドレスは何ですか?」
    • [認証モード] の設定は [ユニバーサル] のままにします。
    • [選択] をクリックします。
  • [デバッグ] > [デバッグなしで開始] の順にクリックするか、Ctrl + F5 キーを押します。 初めてデバイスにデプロイしている場合は、Visual Studio とのペアリングが必要です。
  • HoloLens を装着して、EnergyHub ホログラムを探します。

第 2 章 - 操作

この章では、ホログラムを操作します。 まず、視線を視覚化するためにカーソルを追加します。 次に、ジェスチャを追加し、手を使ってホログラムを空間に置きます。

目標

  • 視線入力を使ってカーソルを制御する。
  • ジェスチャ入力を使ってホログラムを操作する。

Instructions

視線入力

  • [階層] パネルで、[HologramCollection] オブジェクトを選びます。
  • [インスペクター] パネルで、[コンポーネントの追加] ボタンをクリックします。
  • メニューの検索ボックスに「Gaze Manager」と入力します。 検索結果を選びます。
  • HoloToolkit-Sharing-240\Prefabs\Input フォルダーで、Cursor アセットを探します。
  • [Cursor] アセットを [階層] にドラッグ アンド ドロップします。

ジェスチャ

  • [階層] パネルで、[HologramCollection] オブジェクトを選びます。
  • [コンポーネントの追加] をクリックし、検索フィールドに「Gesture Manager」と入力します。 検索結果を選びます。
  • [階層] パネルで、[HologramCollection] を展開します。
  • 子の [EnergyHub] オブジェクトを選びます。
  • [インスペクター] パネルで、[コンポーネントの追加] ボタンをクリックします。
  • メニューの検索ボックスに「Hologram Placement」と入力します。 検索結果を選びます。
  • [ファイル] > [シーンを保存] の順に選んでシーンを保存します。

配置と体験

  • 前の章の手順に従ってビルドし、HoloLens に配置します。
  • HoloLens でアプリを起動後、頭をぐるりと動かすと EnergyHub が視線に従う様子がわかります。
  • ホログラムに視線を合わせるとカーソルが現れ、視線の先にホログラムがないときはポイント ライトに変わる様子がわかります。
  • エアタップを行って、ホログラムを置きます。 この時点のプロジェクトでは、ホログラムを 1 回しか置くことができません (もう一度試すには再配置します)。

第 3 章 - 共有座標

ホログラムを見て操作するのは楽しい作業ですが、次に進みましょう。 最初の共有エクスペリエンスを設定します。つまり、全員が一緒に見ることができるホログラムです。

目標

  • 共有エクスペリエンス用にネットワークをセットアップする。
  • 共通参照ポイントを設定する。
  • デバイス間で座標系を共有する。
  • 全員で同じホログラムを見る。

Note

アプリから共有サーバーに接続するには、InternetClientServerPrivateNetworkClientServer の機能を宣言する必要があります。 これは、Holograms 240 では自動的に行われますが、独自に作成するプロジェクトではこのことを考慮してください。

  1. Unity Editor で、[編集] > [プロジェクトの設定] > [プレーヤー] の順に移動して、プレイヤー設定に移動します
  2. [Windows Store] タブをクリックします
  3. [公開の設定] > [機能] セクションで、InternetClientServer 機能と PrivateNetworkClientServer 機能をオンにします

Instructions

  • プロジェクト パネルで、HoloToolkit-Sharing-240\Prefabs\Sharing フォルダーに移動します。
  • [Sharing] プレハブを [階層] パネルにドラッグ アンド ドロップします。

次に、共有サービスを起動する必要があります。 こちらの手順は、共有エクスペリエンスに参加する 1 台の PC のみで行う必要があります。

  • Unity 上部のメニューで [HoloToolkit-Sharing-240] メニューを選びます。
  • ドロップダウン リストで [共有サービスの起動] 項目を選びます。
  • [プライベート ネットワーク] オプションをオンにし、ファイアウォールのプロンプトが表示されたら [アクセスを許可する] をクリックします。
  • [共有サービス] コンソール ウィンドウに表示されている IPv4 アドレスをメモしておきます。 これは、サービスが実行されているコンピューターと同じ IP アドレスです。

これ以降の手順は、共有エクスペリエンスに参加するすべての PC で行います。

  • [階層] で、[Sharing] オブジェクトを選びます。
  • インスペクターSharing Stage コンポーネントで、[サーバー アドレス] を "localhost" から、SharingService.exe を実行しているコンピューターの IPv4 アドレスに変更します。
  • [階層][HologramCollection] オブジェクトを選びます。
  • [インスペクター][コンポーネントの追加] ボタンをクリックします。
  • 検索ボックスに「Import Export Anchor Manager」と入力します。 検索結果を選びます。
  • プロジェクト パネルScripts フォルダーに移動します。
  • HologramPlacement スクリプトをダブルクリックして Visual Studio で開きます。
  • 内容を次のコードに置き換えます。
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.Windows.Speech;
using Academy.HoloToolkit.Unity;
using Academy.HoloToolkit.Sharing;

public class HologramPlacement : Singleton<HologramPlacement>
{
    /// <summary>
    /// Tracks if we have been sent a transform for the anchor model.
    /// The anchor model is rendered relative to the actual anchor.
    /// </summary>
    public bool GotTransform { get; private set; }

    private bool animationPlayed = false;

    void Start()
    {
        // We care about getting updates for the anchor transform.
        CustomMessages.Instance.MessageHandlers[CustomMessages.TestMessageID.StageTransform] = this.OnStageTransform;

        // And when a new user join we will send the anchor transform we have.
        SharingSessionTracker.Instance.SessionJoined += Instance_SessionJoined;
    }

    /// <summary>
    /// When a new user joins we want to send them the relative transform for the anchor if we have it.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void Instance_SessionJoined(object sender, SharingSessionTracker.SessionJoinedEventArgs e)
    {
        if (GotTransform)
        {
            CustomMessages.Instance.SendStageTransform(transform.localPosition, transform.localRotation);
        }
    }

    void Update()
    {
        if (GotTransform)
        {
            if (ImportExportAnchorManager.Instance.AnchorEstablished &&
                animationPlayed == false)
            {
                // This triggers the animation sequence for the anchor model and 
                // puts the cool materials on the model.
                GetComponent<EnergyHubBase>().SendMessage("OnSelect");
                animationPlayed = true;
            }
        }
        else
        {
            transform.position = Vector3.Lerp(transform.position, ProposeTransformPosition(), 0.2f);
        }
    }

    Vector3 ProposeTransformPosition()
    {
        // Put the anchor 2m in front of the user.
        Vector3 retval = Camera.main.transform.position + Camera.main.transform.forward * 2;

        return retval;
    }

    public void OnSelect()
    {
        // Note that we have a transform.
        GotTransform = true;

        // And send it to our friends.
        CustomMessages.Instance.SendStageTransform(transform.localPosition, transform.localRotation);
    }

    /// <summary>
    /// When a remote system has a transform for us, we'll get it here.
    /// </summary>
    /// <param name="msg"></param>
    void OnStageTransform(NetworkInMessage msg)
    {
        // We read the user ID but we don't use it here.
        msg.ReadInt64();

        transform.localPosition = CustomMessages.Instance.ReadVector3(msg);
        transform.localRotation = CustomMessages.Instance.ReadQuaternion(msg);

        // The first time, we'll want to send the message to the anchor to do its animation and
        // swap its materials.
        if (GotTransform == false)
        {
            GetComponent<EnergyHubBase>().SendMessage("OnSelect");
        }

        GotTransform = true;
    }

    public void ResetStage()
    {
        // We'll use this later.
    }
}
  • Unity に戻り、[階層] パネルで [HologramCollection] を選びます。
  • [インスペクター] パネルで、[コンポーネントの追加] ボタンをクリックします。
  • メニューの検索ボックスに「App State Manager」と入力します。 検索結果を選びます。

配置と体験

  • HoloLens デバイス用にプロジェクトをビルドします。
  • 最初に配置する HoloLens を 1 つ指定します。 EnergyHub を置く前に、アンカーがサービスにアップロードされるのを待つ必要があります (これには 30 から 60 秒ほどかかります)。 アップロードが完了するまで、タップ ジェスチャは無視されます。
  • EnergyHub を置くと、その場所がサービスにアップロードされ、他のすべての HoloLens デバイスに配置できるようになります。
  • 新しい HoloLens がセッションに初めて参加したときは、そのデバイスでは EnergyHub の場所が正しくない場合があります。 ただし、アンカーと EnergyHub の場所がサービスからダウンロードされるとすぐに、EnergyHub は新しい共有の場所に移動します。 30 から 60 秒ほど経過してもこれが行われない場合は、アンカーの設定時に元の HoloLens があった場所まで歩いて行って、周囲の状況についての手掛かりをさらに集めます。 その場所がまだロックオンされていない場合は、デバイスに再配置します。
  • デバイスの準備が完了し、アプリを実行したら、EnergyHub を探します。 ホログラムの場所とテキストが表示されている方向に問題がないことを確認します。

第 4 章 - 検出

全員が同じホログラムを見ることができるようになりました。 それでは、共有されたホログラフィックの世界に接続している他の人を見てみましょう。 この章では、同じ共有セッション内の他のすべての HoloLens デバイスの頭の場所と回転を把握します。

目標

  • 共有エクスペリエンスに参加している人を互いに見つける。
  • プレイヤーのアバターを選んで共有する。
  • すべてのユーザーの頭部の横にプレイヤーのアバターを付ける。

Instructions

  • プロジェクト パネルHolograms フォルダーに移動します。
  • [PlayerAvatarStore][階層] にドラッグ アンド ドロップします。
  • プロジェクト パネルScripts フォルダーに移動します。
  • AvatarSelector スクリプトをダブルクリックして Visual Studio で開きます。
  • 内容を次のコードに置き換えます。
using UnityEngine;
using Academy.HoloToolkit.Unity;

/// <summary>
/// Script to handle the user selecting the avatar.
/// </summary>
public class AvatarSelector : MonoBehaviour
{
    /// <summary>
    /// This is the index set by the PlayerAvatarStore for the avatar.
    /// </summary>
    public int AvatarIndex { get; set; }

    /// <summary>
    /// Called when the user is gazing at this avatar and air-taps it.
    /// This sends the user's selection to the rest of the devices in the experience.
    /// </summary>
    void OnSelect()
    {
        PlayerAvatarStore.Instance.DismissAvatarPicker();

        LocalPlayerManager.Instance.SetUserAvatar(AvatarIndex);
    }

    void Start()
    {
        // Add Billboard component so the avatar always faces the user.
        Billboard billboard = gameObject.GetComponent<Billboard>();
        if (billboard == null)
        {
            billboard = gameObject.AddComponent<Billboard>();
        }

        // Lock rotation along the Y axis.
        billboard.PivotAxis = PivotAxis.Y;
    }
}
  • [階層][HologramCollection] オブジェクトを選びます。
  • [インスペクター] で、[コンポーネントの追加] をクリックします。
  • 検索ボックスに「Local Player Manager」と入力します。 検索結果を選びます。
  • [階層][HologramCollection] オブジェクトを選びます。
  • [インスペクター] で、[コンポーネントの追加] をクリックします。
  • 検索ボックスに「Remote Player Manager」と入力します。 検索結果を選びます。
  • HologramPlacement スクリプトを Visual Studio で開きます。
  • 内容を次のコードに置き換えます。
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.Windows.Speech;
using Academy.HoloToolkit.Unity;
using Academy.HoloToolkit.Sharing;

public class HologramPlacement : Singleton<HologramPlacement>
{
    /// <summary>
    /// Tracks if we have been sent a transform for the model.
    /// The model is rendered relative to the actual anchor.
    /// </summary>
    public bool GotTransform { get; private set; }

    /// <summary>
    /// When the experience starts, we disable all of the rendering of the model.
    /// </summary>
    List<MeshRenderer> disabledRenderers = new List<MeshRenderer>();

    void Start()
    {
        // When we first start, we need to disable the model to avoid it obstructing the user picking a hat.
        DisableModel();

        // We care about getting updates for the model transform.
        CustomMessages.Instance.MessageHandlers[CustomMessages.TestMessageID.StageTransform] = this.OnStageTransform;

        // And when a new user join we will send the model transform we have.
        SharingSessionTracker.Instance.SessionJoined += Instance_SessionJoined;
    }

    /// <summary>
    /// When a new user joins we want to send them the relative transform for the model if we have it.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void Instance_SessionJoined(object sender, SharingSessionTracker.SessionJoinedEventArgs e)
    {
        if (GotTransform)
        {
            CustomMessages.Instance.SendStageTransform(transform.localPosition, transform.localRotation);
        }
    }

    /// <summary>
    /// Turns off all renderers for the model.
    /// </summary>
    void DisableModel()
    {
        foreach (MeshRenderer renderer in gameObject.GetComponentsInChildren<MeshRenderer>())
        {
            if (renderer.enabled)
            {
                renderer.enabled = false;
                disabledRenderers.Add(renderer);
            }
        }

        foreach (MeshCollider collider in gameObject.GetComponentsInChildren<MeshCollider>())
        {
            collider.enabled = false;
        }
    }

    /// <summary>
    /// Turns on all renderers that were disabled.
    /// </summary>
    void EnableModel()
    {
        foreach (MeshRenderer renderer in disabledRenderers)
        {
            renderer.enabled = true;
        }

        foreach (MeshCollider collider in gameObject.GetComponentsInChildren<MeshCollider>())
        {
            collider.enabled = true;
        }

        disabledRenderers.Clear();
    }


    void Update()
    {
        // Wait till users pick an avatar to enable renderers.
        if (disabledRenderers.Count > 0)
        {
            if (!PlayerAvatarStore.Instance.PickerActive &&
            ImportExportAnchorManager.Instance.AnchorEstablished)
            {
                // After which we want to start rendering.
                EnableModel();

                // And if we've already been sent the relative transform, we will use it.
                if (GotTransform)
                {
                    // This triggers the animation sequence for the model and
                    // puts the cool materials on the model.
                    GetComponent<EnergyHubBase>().SendMessage("OnSelect");
                }
            }
        }
        else if (GotTransform == false)
        {
            transform.position = Vector3.Lerp(transform.position, ProposeTransformPosition(), 0.2f);
        }
    }

    Vector3 ProposeTransformPosition()
    {
        // Put the model 2m in front of the user.
        Vector3 retval = Camera.main.transform.position + Camera.main.transform.forward * 2;

        return retval;
    }

    public void OnSelect()
    {
        // Note that we have a transform.
        GotTransform = true;

        // And send it to our friends.
        CustomMessages.Instance.SendStageTransform(transform.localPosition, transform.localRotation);
    }

    /// <summary>
    /// When a remote system has a transform for us, we'll get it here.
    /// </summary>
    /// <param name="msg"></param>
    void OnStageTransform(NetworkInMessage msg)
    {
        // We read the user ID but we don't use it here.
        msg.ReadInt64();

        transform.localPosition = CustomMessages.Instance.ReadVector3(msg);
        transform.localRotation = CustomMessages.Instance.ReadQuaternion(msg);

        // The first time, we'll want to send the message to the model to do its animation and
        // swap its materials.
        if (disabledRenderers.Count == 0 && GotTransform == false)
        {
            GetComponent<EnergyHubBase>().SendMessage("OnSelect");
        }

        GotTransform = true;
    }

    public void ResetStage()
    {
        // We'll use this later.
    }
}
  • AppStateManager スクリプトを Visual Studio で開きます。
  • 内容を次のコードに置き換えます。
using UnityEngine;
using Academy.HoloToolkit.Unity;

/// <summary>
/// Keeps track of the current state of the experience.
/// </summary>
public class AppStateManager : Singleton<AppStateManager>
{
    /// <summary>
    /// Enum to track progress through the experience.
    /// </summary>
    public enum AppState
    {
        Starting = 0,
        WaitingForAnchor,
        WaitingForStageTransform,
        PickingAvatar,
        Ready
    }

    /// <summary>
    /// Tracks the current state in the experience.
    /// </summary>
    public AppState CurrentAppState { get; set; }

    void Start()
    {
        // We start in the 'picking avatar' mode.
        CurrentAppState = AppState.PickingAvatar;

        // We start by showing the avatar picker.
        PlayerAvatarStore.Instance.SpawnAvatarPicker();
    }

    void Update()
    {
        switch (CurrentAppState)
        {
            case AppState.PickingAvatar:
                // Avatar picking is done when the avatar picker has been dismissed.
                if (PlayerAvatarStore.Instance.PickerActive == false)
                {
                    CurrentAppState = AppState.WaitingForAnchor;
                }
                break;
            case AppState.WaitingForAnchor:
                if (ImportExportAnchorManager.Instance.AnchorEstablished)
                {
                    CurrentAppState = AppState.WaitingForStageTransform;
                    GestureManager.Instance.OverrideFocusedObject = HologramPlacement.Instance.gameObject;
                }
                break;
            case AppState.WaitingForStageTransform:
                // Now if we have the stage transform we are ready to go.
                if (HologramPlacement.Instance.GotTransform)
                {
                    CurrentAppState = AppState.Ready;
                    GestureManager.Instance.OverrideFocusedObject = null;
                }
                break;
        }
    }
}

配置と体験

  • プロジェクトをビルドし、HoloLens デバイスに配置します。
  • ピーンという音が聞こえたら、アバターを選ぶメニューを見つけて、エアタップ ジェスチャでアバターを選びます。
  • ホログラムを見ていないと、HoloLens がサービスと通信しているときはカーソルの周りのポイント ライトの色が変わります。初期化中は濃い紫、アンカーのダウンロード中は緑、位置データのインポートまたはエクスポート中は黄色、アンカーのアップロード中は青に変わります。 カーソルの周りのポイント ライトが既定の色 (淡い紫) の場合は、セッション内の他のプレイヤーと対話する準備ができています。
  • 同じ空間に接続している他のユーザーを見ると、肩の上にホログラフィック ロボットが浮遊していて、頭の動きを模倣しています。

第 5 章 - 配置

この章では、アンカーを現実世界の面に配置できるようにします。 共有座標を使用して、共有エクスペリエンスに接続している全員の中間点にアンカーを置きます。

目標

  • プレイヤーの頭の位置に基づいて、空間マッピング メッシュにホログラムを置く。

Instructions

  • プロジェクト パネルHolograms フォルダーに移動します。
  • [CustomSpatialMapping] プレハブを [階層] にドラッグ アンド ドロップします。
  • プロジェクト パネルScripts フォルダーに移動します。
  • AppStateManager スクリプトをダブルクリックして Visual Studio で開きます。
  • 内容を次のコードに置き換えます。
using UnityEngine;
using Academy.HoloToolkit.Unity;

/// <summary>
/// Keeps track of the current state of the experience.
/// </summary>
public class AppStateManager : Singleton<AppStateManager>
{
    /// <summary>
    /// Enum to track progress through the experience.
    /// </summary>
    public enum AppState
    {
        Starting = 0,
        PickingAvatar,
        WaitingForAnchor,
        WaitingForStageTransform,
        Ready
    }

    // The object to call to make a projectile.
    GameObject shootHandler = null;

    /// <summary>
    /// Tracks the current state in the experience.
    /// </summary>
    public AppState CurrentAppState { get; set; }

    void Start()
    {
        // The shootHandler shoots projectiles.
        if (GetComponent<ProjectileLauncher>() != null)
        {
            shootHandler = GetComponent<ProjectileLauncher>().gameObject;
        }

        // We start in the 'picking avatar' mode.
        CurrentAppState = AppState.PickingAvatar;

        // Spatial mapping should be disabled when we start up so as not
        // to distract from the avatar picking.
        SpatialMappingManager.Instance.StopObserver();
        SpatialMappingManager.Instance.gameObject.SetActive(false);

        // On device we start by showing the avatar picker.
        PlayerAvatarStore.Instance.SpawnAvatarPicker();
    }

    public void ResetStage()
    {
        // If we fall back to waiting for anchor, everything needed to
        // get us into setting the target transform state will be setup.
        if (CurrentAppState != AppState.PickingAvatar)
        {
            CurrentAppState = AppState.WaitingForAnchor;
        }

        // Reset the underworld.
        if (UnderworldBase.Instance)
        {
            UnderworldBase.Instance.ResetUnderworld();
        }
    }

    void Update()
    {
        switch (CurrentAppState)
        {
            case AppState.PickingAvatar:
                // Avatar picking is done when the avatar picker has been dismissed.
                if (PlayerAvatarStore.Instance.PickerActive == false)
                {
                    CurrentAppState = AppState.WaitingForAnchor;
                }
                break;
            case AppState.WaitingForAnchor:
                // Once the anchor is established we need to run spatial mapping for a
                // little while to build up some meshes.
                if (ImportExportAnchorManager.Instance.AnchorEstablished)
                {
                    CurrentAppState = AppState.WaitingForStageTransform;
                    GestureManager.Instance.OverrideFocusedObject = HologramPlacement.Instance.gameObject;

                    SpatialMappingManager.Instance.gameObject.SetActive(true);
                    SpatialMappingManager.Instance.DrawVisualMeshes = true;
                    SpatialMappingDeformation.Instance.ResetGlobalRendering();
                    SpatialMappingManager.Instance.StartObserver();
                }
                break;
            case AppState.WaitingForStageTransform:
                // Now if we have the stage transform we are ready to go.
                if (HologramPlacement.Instance.GotTransform)
                {
                    CurrentAppState = AppState.Ready;
                    GestureManager.Instance.OverrideFocusedObject = shootHandler;
                }
                break;
        }
    }
}
  • プロジェクト パネルScripts フォルダーに移動します。
  • HologramPlacement スクリプトをダブルクリックして Visual Studio で開きます。
  • 内容を次のコードに置き換えます。
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.Windows.Speech;
using Academy.HoloToolkit.Unity;
using Academy.HoloToolkit.Sharing;

public class HologramPlacement : Singleton<HologramPlacement>
{
    /// <summary>
    /// Tracks if we have been sent a transform for the model.
    /// The model is rendered relative to the actual anchor.
    /// </summary>
    public bool GotTransform { get; private set; }

    /// <summary>
    /// When the experience starts, we disable all of the rendering of the model.
    /// </summary>
    List<MeshRenderer> disabledRenderers = new List<MeshRenderer>();

    /// <summary>
    /// We use a voice command to enable moving the target.
    /// </summary>
    KeywordRecognizer keywordRecognizer;

    void Start()
    {
        // When we first start, we need to disable the model to avoid it obstructing the user picking a hat.
        DisableModel();

        // We care about getting updates for the model transform.
        CustomMessages.Instance.MessageHandlers[CustomMessages.TestMessageID.StageTransform] = this.OnStageTransform;

        // And when a new user join we will send the model transform we have.
        SharingSessionTracker.Instance.SessionJoined += Instance_SessionJoined;

        // And if the users want to reset the stage transform.
        CustomMessages.Instance.MessageHandlers[CustomMessages.TestMessageID.ResetStage] = this.OnResetStage;

        // Setup a keyword recognizer to enable resetting the target location.
        List<string> keywords = new List<string>();
        keywords.Add("Reset Target");
        keywordRecognizer = new KeywordRecognizer(keywords.ToArray());
        keywordRecognizer.OnPhraseRecognized += KeywordRecognizer_OnPhraseRecognized;
        keywordRecognizer.Start();
    }

    /// <summary>
    /// When the keyword recognizer hears a command this will be called.  
    /// In this case we only have one keyword, which will re-enable moving the
    /// target.
    /// </summary>
    /// <param name="args">information to help route the voice command.</param>
    private void KeywordRecognizer_OnPhraseRecognized(PhraseRecognizedEventArgs args)
    {
        ResetStage();
    }

    /// <summary>
    /// Resets the stage transform, so users can place the target again.
    /// </summary>
    public void ResetStage()
    {
        GotTransform = false;

        // AppStateManager needs to know about this so that
        // the right objects get input routed to them.
        AppStateManager.Instance.ResetStage();

        // Other devices in the experience need to know about this as well.
        CustomMessages.Instance.SendResetStage();

        // And we need to reset the object to its start animation state.
        GetComponent<EnergyHubBase>().ResetAnimation();
    }

    /// <summary>
    /// When a new user joins we want to send them the relative transform for the model if we have it.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void Instance_SessionJoined(object sender, SharingSessionTracker.SessionJoinedEventArgs e)
    {
        if (GotTransform)
        {
            CustomMessages.Instance.SendStageTransform(transform.localPosition, transform.localRotation);
        }
    }

    /// <summary>
    /// Turns off all renderers for the model.
    /// </summary>
    void DisableModel()
    {
        foreach (MeshRenderer renderer in gameObject.GetComponentsInChildren<MeshRenderer>())
        {
            if (renderer.enabled)
            {
                renderer.enabled = false;
                disabledRenderers.Add(renderer);
            }
        }

        foreach (MeshCollider collider in gameObject.GetComponentsInChildren<MeshCollider>())
        {
            collider.enabled = false;
        }
    }

    /// <summary>
    /// Turns on all renderers that were disabled.
    /// </summary>
    void EnableModel()
    {
        foreach (MeshRenderer renderer in disabledRenderers)
        {
            renderer.enabled = true;
        }

        foreach (MeshCollider collider in gameObject.GetComponentsInChildren<MeshCollider>())
        {
            collider.enabled = true;
        }

        disabledRenderers.Clear();
    }


    void Update()
    {
        // Wait till users pick an avatar to enable renderers.
        if (disabledRenderers.Count > 0)
        {
            if (!PlayerAvatarStore.Instance.PickerActive &&
            ImportExportAnchorManager.Instance.AnchorEstablished)
            {
                // After which we want to start rendering.
                EnableModel();

                // And if we've already been sent the relative transform, we will use it.
                if (GotTransform)
                {
                    // This triggers the animation sequence for the model and
                    // puts the cool materials on the model.
                    GetComponent<EnergyHubBase>().SendMessage("OnSelect");
                }
            }
        }
        else if (GotTransform == false)
        {
            transform.position = Vector3.Lerp(transform.position, ProposeTransformPosition(), 0.2f);
        }
    }

    Vector3 ProposeTransformPosition()
    {
        Vector3 retval;
        // We need to know how many users are in the experience with good transforms.
        Vector3 cumulatedPosition = Camera.main.transform.position;
        int playerCount = 1;
        foreach (RemotePlayerManager.RemoteHeadInfo remoteHead in RemotePlayerManager.Instance.remoteHeadInfos)
        {
            if (remoteHead.Anchored && remoteHead.Active)
            {
                playerCount++;
                cumulatedPosition += remoteHead.HeadObject.transform.position;
            }
        }

        // If we have more than one player ...
        if (playerCount > 1)
        {
            // Put the transform in between the players.
            retval = cumulatedPosition / playerCount;
            RaycastHit hitInfo;

            // And try to put the transform on a surface below the midpoint of the players.
            if (Physics.Raycast(retval, Vector3.down, out hitInfo, 5, SpatialMappingManager.Instance.LayerMask))
            {
                retval = hitInfo.point;
            }
        }
        // If we are the only player, have the model act as the 'cursor' ...
        else
        {
            // We prefer to put the model on a real world surface.
            RaycastHit hitInfo;

            if (Physics.Raycast(Camera.main.transform.position, Camera.main.transform.forward, out hitInfo, 30, SpatialMappingManager.Instance.LayerMask))
            {
                retval = hitInfo.point;
            }
            else
            {
                // But if we don't have a ray that intersects the real world, just put the model 2m in
                // front of the user.
                retval = Camera.main.transform.position + Camera.main.transform.forward * 2;
            }
        }
        return retval;
    }

    public void OnSelect()
    {
        // Note that we have a transform.
        GotTransform = true;

        // And send it to our friends.
        CustomMessages.Instance.SendStageTransform(transform.localPosition, transform.localRotation);
    }

    /// <summary>
    /// When a remote system has a transform for us, we'll get it here.
    /// </summary>
    /// <param name="msg"></param>
    void OnStageTransform(NetworkInMessage msg)
    {
        // We read the user ID but we don't use it here.
        msg.ReadInt64();

        transform.localPosition = CustomMessages.Instance.ReadVector3(msg);
        transform.localRotation = CustomMessages.Instance.ReadQuaternion(msg);

        // The first time, we'll want to send the message to the model to do its animation and
        // swap its materials.
        if (disabledRenderers.Count == 0 && GotTransform == false)
        {
            GetComponent<EnergyHubBase>().SendMessage("OnSelect");
        }

        GotTransform = true;
    }

    /// <summary>
    /// When a remote system has a transform for us, we'll get it here.
    /// </summary>
    void OnResetStage(NetworkInMessage msg)
    {
        GotTransform = false;

        GetComponent<EnergyHubBase>().ResetAnimation();
        AppStateManager.Instance.ResetStage();
    }
}

配置と体験

  • プロジェクトをビルドし、HoloLens デバイスに配置します。
  • アプリの準備が整ったら、立って円陣を作ります。その中心に EnergyHub が現れるのがわかります。
  • タップして EnergyHub を置きます。
  • 音声コマンド "Reset Target" を使用して EnergyHub バックアップを選んでみます。全員がグループとして連携して、ホログラムを新しい場所に動かします。

第 6 章 - 現実世界の物理特性

この章では、現実世界の面から飛び出すホログラムを追加します。 自分と仲間が起動した各プロジェクトで、空間が覆われるのを確認します。

目標

  • 現実世界の面から飛び出す弾を打ち出す。
  • その弾を他のプレイヤーに見えるように共有する。

Instructions

  • [階層][HologramCollection] オブジェクトを選びます。
  • [インスペクター] で、[コンポーネントの追加] をクリックします。
  • 検索ボックスに「Projectile Launcher」と入力します。 検索結果を選びます。

配置と体験

  • ビルドして HoloLens デバイスに配置します。
  • アプリが全員のデバイスで実行されている状態で、エアタップを行うと、現実世界の面から弾が打ち出されます。
  • 弾が他のプレイヤーのアバターに当るとどうなるかを確認します。

第 7 章 - グランド フィナーレ

この章では、協力しないと見つからない入り口を見つけます。

目標

  • 秘密の入り口を見つけるために、全員で協力して弾を打ち出す。

Instructions

  • プロジェクト パネルHolograms フォルダーに移動します。
  • Underworld アセットを、HologramCollection の子 になるようにドラッグ アンド ドロップします。
  • [HologramCollection] を選んだ状態で、[インスペクター][コンポーネントの追加] ボタンをクリックします。
  • メニューの検索ボックスに「ExplodeTarget」と入力します。 検索結果を選びます。
  • [HologramCollection] を選んだ状態で、EnergyHub オブジェクトを、[階層] から [インスペクター][ターゲット] フィールドにドラッグします。
  • [HologramCollection] を選んだ状態で、Underworld オブジェクトを、[階層] から [インスペクター][Underworld] フィールドにドラッグします。

配置と体験

  • ビルドして HoloLens デバイスに配置します。
  • アプリが起動したら、協力して EnergyHub に弾を打ち出します。
  • 闇の世界が現れたら、闇の世界のロボットに弾を打ち出します (ロボットに 3 回当ると楽しさが倍増します)。