MR 共有 240:複数の HoloLens デバイスMR Sharing 240: Multiple HoloLens devices

注意

Mixed Reality Academy のチュートリアルは、HoloLens (第 1 世代) と Mixed Reality イマーシブ ヘッドセットを念頭に置いて編成されています。The Mixed Reality Academy tutorials were designed with HoloLens (1st gen) and Mixed Reality Immersive Headsets in mind. そのため、それらのデバイスの開発に関するガイダンスを引き続き探している開発者のために、これらのチュートリアルをそのまま残しておくことが重要だと考えています。As such, we feel it is important to leave these tutorials in place for developers who are still looking for guidance in developing for those devices. これらのチュートリアルが、HoloLens 2 に使用されている最新のツールセットや操作に更新されることは "ありません"。These tutorials will not be updated with the latest toolsets or interactions being used for HoloLens 2. これらは、サポートされているデバイス上で継続して動作するように、保守されます。They will be maintained to continue working on the supported devices. HoloLens 2 向けには、新しいチュートリアル シリーズが投稿されています。A new series of tutorials has been posted for HoloLens 2.

ホログラムは、領域内での移動によって、世界中に残されています。Holograms are given presence in our world by remaining in place as we move about in space. HoloLens は、さまざまな 座標 系を使用して、オブジェクトの位置と向きを追跡することで、ホログラムを保持します。HoloLens keeps holograms in place by using various coordinate systems to keep track of the location and orientation of objects. これらの座標系をデバイス間で共有すると、共有された holographic 世界に参加するための共有エクスペリエンスを作成できます。When we share these coordinate systems between devices, we can create a shared experience that allows us to take part in a shared holographic world.

このチュートリアルでは、次のことを行います。In this tutorial, we will:

  • 共有エクスペリエンスのためにネットワークをセットアップします。Setup a network for a shared experience.
  • HoloLens デバイス間でホログラムを共有します。Share holograms across HoloLens devices.
  • 共有 holographic 世界の他のメンバーを発見します。Discover other people in our shared holographic world.
  • 他のプレーヤーをターゲットにして projectiles を起動できる、共有のインタラクティブなエクスペリエンスを作成しましょう。Create a shared interactive experience where you can target other players - and launch projectiles at them!

デバイス サポートDevice support

コースCourse HoloLensHoloLens イマーシブ ヘッドセットImmersive headsets
MR 共有 240:複数の HoloLens デバイスMR Sharing 240: Multiple HoloLens devices ✔️✔️

開始する前にBefore you start

必須コンポーネントPrerequisites

プロジェクト ファイルProject files

  • プロジェクトに必要な ファイル をダウンロードします。Download the files required by the project. Unity 2017.2 以降が必要です。Requires Unity 2017.2 or later.
    • 引き続き Unity 5.6 のサポートが必要な場合は、 このリリースをご利用ください。If you still need Unity 5.6 support, please use this release.
    • 引き続き Unity 5.5 のサポートが必要な場合は、 このリリースをご利用ください。If you still need Unity 5.5 support, please use this release.
    • 引き続き Unity 5.4 のサポートが必要な場合は、 このリリースをご利用ください。If you still need Unity 5.4 support, please use this release.
  • ファイルをデスクトップまたはその他の簡単な場所に保管します。Un-archive the files to your desktop or other easy to reach location. フォルダー名を Sharedholograms として保持します。Keep the folder name as SharedHolograms.

注意

ダウンロードする前にソースコードを確認する場合は、GitHub から 入手できます。If you want to look through the source code before downloading, it's available on GitHub.

Chapter 1-Holo WorldChapter 1 - Holo World

この章では、最初の Unity プロジェクトをセットアップし、ビルドとデプロイのプロセスをステップ実行します。In this chapter, we'll setup our first Unity project and step through the build and deploy process.

目標Objectives

  • Unity をセットアップして、holographic アプリを開発します。Setup Unity to develop holographic apps.
  • ホログラムをご覧ください。See your hologram!

手順Instructions

  • Unity を起動します。Start Unity.
  • [Open (開く)] を選択します。Select Open.
  • 以前に unarchived した Sharedholograms フォルダーとして場所を入力します。Enter location as the SharedHolograms folder you previously unarchived.
  • [ プロジェクト名 ] を選択し、[ フォルダーの選択] をクリックします。Select Project Name and click Select Folder.
  • 階層 で、メインカメラ を右クリックし、[削除] を選択します。In the Hierarchy, right-click the Main Camera and select Delete.
  • HoloToolkit-240/Prefabs/カメラ フォルダーで、メインカメラ の事前 fab を見つけます。In the HoloToolkit-Sharing-240/Prefabs/Camera folder, find the Main Camera prefab.
  • メインカメラ階層 にドラッグアンドドロップします。Drag and drop the Main Camera into the Hierarchy.
  • 階層 で、[作成] をクリックし、[空の作成] をクリックします。In the Hierarchy, click on Create and Create Empty.
  • 新しい [作成] オブジェクト を右クリックし、[ 名前の変更] を選択します。Right-click the new GameObject and select Rename.
  • HologramCollection オブジェクトの名前を「」に変更します。Rename the GameObject to HologramCollection.
  • 階層 内の HologramCollection オブジェクトを選択します。Select the HologramCollection object in the Hierarchy.
  • インスペクター で、変換位置X: 0、Y:-0.25、Z: 2 のように設定します。In the Inspector set the transform position to: X: 0, Y: -0.25, Z: 2.
  • [プロジェクト] パネル の [ホログラム] フォルダーで、 EnergyHub 資産を見つけます。In the Holograms folder in the Project panel, find the EnergyHub asset.
  • [プロジェクト] パネル から、 EnergyHub オブジェクトを HologramCollection の子 として 階層 にドラッグアンドドロップします。Drag and drop the EnergyHub object from the Project panel to the Hierarchy as a child of HologramCollection.
  • [ ファイル] > 選択してシーンを保存...Select File > Save Scene As...
  • シーンに Sharedholograms という名前を付け、[ 保存] をクリックします。Name the scene SharedHolograms and click Save.
  • Unity の [ 再生 ] ボタンをクリックして、ホログラムをプレビューします。Press the Play button in Unity to preview your holograms.
  • プレビューモードを停止するには、もう一度 Play を押します。Press Play a second time to stop preview mode.

Unity から Visual Studio にプロジェクトをエクスポートするExport the project from Unity to Visual Studio

  • Unity で、[ ファイル > ビルド設定] を選択します。In Unity select File > Build Settings.
  • シーンを追加するには、[開いている シーンの追加 ] をクリックします。Click Add Open Scenes to add the scene.
  • [プラットフォーム] ボックスの一覧の [ユニバーサル Windows プラットフォーム] を選択し、[プラットフォームの切り替え] をクリックします。Select Universal Windows Platform in the Platform list and click Switch Platform.
  • SDKUniversal 10 に設定します。Set SDK to Universal 10.
  • ターゲットデバイスHoloLens に設定し、 UWP ビルドの種類D3D に設定します。Set Target device to HoloLens and UWP Build Type to D3D.
  • Unity C# プロジェクト を確認します。Check Unity C# Projects.
  • [ビルド] をクリックします。Click Build.
  • 表示された [エクスプローラー] ウィンドウで、"App" という名前の 新しいフォルダー を作成します。In the file explorer window that appears, create a New Folder named "App".
  • アプリ フォルダーをシングルクリックします。Single click the App folder.
  • [フォルダーの選択] をクリックします。Press Select Folder.
  • Unity が完了すると、エクスプローラーウィンドウが表示されます。When Unity is done, a File Explorer window will appear.
  • アプリ フォルダーを開きます。Open the App folder.
  • Sharedholograms を開いて、Visual Studio を起動します。Open SharedHolograms.sln to launch Visual Studio.
  • Visual Studio の上部のツールバーを使用して、ターゲットをデバッグから リリース に変更し、ARM から X86 に変更します。Using the top toolbar in Visual Studio, change the target from Debug to Release and from ARM to X86.
  • [ローカルコンピューター] の横にあるドロップダウン矢印をクリックし、[ リモートデバイス] を選択します。Click on the drop-down arrow next to Local Machine, and select Remote Device.
    • アドレス を HoloLens の名前または IP アドレスに設定します。Set the Address to the name or IP address of your HoloLens. デバイスの IP アドレスがわからない場合は、[設定] の [ネットワーク & Internet > 詳細オプション > 確認するか、cortana に "Cortana さん、どのような IP アドレスがあるか" を 確認してください。If you do not know your device IP address, look in Settings > Network & Internet > Advanced Options or ask Cortana "Hey Cortana, What's my IP address?"
    • [ 認証モード ( ユニバーサル) に設定したままにします。Leave the Authentication Mode set to Universal.
    • [選択] を クリックClick Select
  • [デバッグ] をクリックして [ デバッグなしで開始 ] を >、Ctrl キーを押し ながら F5 キーを押します。Click Debug > Start Without debugging or press Ctrl + F5. 初めてデバイスをデプロイする場合は、 Visual Studio とペアリングする必要があります。If this is the first time deploying to your device, you will need to pair it with Visual Studio.
  • HoloLens に配置し、EnergyHub ホログラムを見つけます。Put on your HoloLens and find the EnergyHub hologram.

Chapter 2-相互作用Chapter 2 - Interaction

この章では、ホログラムを操作します。In this chapter, we'll interact with our holograms. 最初に、 見つめを視覚化するカーソルを追加します。First, we'll add a cursor to visualize our Gaze. 次に、 ジェスチャ を追加し、手を使用してホログラムをスペースに配置します。Then, we'll add Gestures and use our hand to place our holograms in space.

目標Objectives

  • 行方向の入力を使用してカーソルを制御します。Use gaze input to control a cursor.
  • ジェスチャ入力を使用して、ホログラムを操作します。Use gesture input to interact with holograms.

手順Instructions

視線入力Gaze

  • [ 階層] パネル で、 HologramCollection オブジェクトを選択します。In the Hierarchy panel select the HologramCollection object.
  • [ インスペクター] パネル で、[ コンポーネントの追加 ] ボタンをクリックします。In the Inspector panel click the Add Component button.
  • メニューの [検索] ボックスに「と 入力してください」 と入力します。In the menu, type in the search box Gaze Manager. 検索結果を選択します。Select the search result.
  • HoloToolkit-Sharing-240\Prefabs\Input フォルダーで、カーソル アセットを見つけます。In the HoloToolkit-Sharing-240\Prefabs\Input folder, find the Cursor asset.
  • カーソル アセットを 階層 にドラッグアンドドロップします。Drag and drop the Cursor asset onto the Hierarchy.

ジェスチャGesture

  • [ 階層] パネル で、 HologramCollection オブジェクトを選択します。In the Hierarchy panel select the HologramCollection object.
  • [ コンポーネントの追加 ] をクリックし、検索フィールドに「 ジェスチャマネージャー 」と入力します。Click Add Component and type Gesture Manager in the search field. 検索結果を選択します。Select the search result.
  • [ 階層] パネル で、[ HologramCollection] を展開します。In the Hierarchy panel, expand HologramCollection.
  • EnergyHub オブジェクトを選択します。Select the child EnergyHub object.
  • [ インスペクター] パネル で、[ コンポーネントの追加 ] ボタンをクリックします。In the Inspector panel click the Add Component button.
  • メニューで、検索ボックスの ホログラムの配置 を入力します。In the menu, type in the search box Hologram Placement. 検索結果を選択します。Select the search result.
  • [ ファイル > [シーンの保存] を選択してシーンを保存します。Save the scene by selecting File > Save Scene.

デプロイと活用Deploy and enjoy

  • 前の章の指示に従って、HoloLens にビルドしてデプロイします。Build and deploy to your HoloLens, using the instructions from the previous chapter.
  • HoloLens でアプリが起動したら、頭を動かして、EnergyHub がどのようになっているかをご確認ください。Once the app launches on your HoloLens, move your head around and notice how the EnergyHub follows your gaze.
  • ホログラムを見つめたときにカーソルがどのように表示されるかに注目してください。また、ホログラムで見られない場合はポイントライトに変わります。Notice how the cursor appears when you gaze upon the hologram, and changes to a point light when not gazing at a hologram.
  • エアタップを実行して、ホログラムを配置します。Perform an air-tap to place the hologram. この時点で、このプロジェクトでは、ホログラムを1回だけ配置できます (再デプロイしてもう一度試すことができます)。At this time in our project, you can only place the hologram once (redeploy to try again).

第3章-共有座標Chapter 3 - Shared Coordinates

ホログラムを見て操作するのは楽しい作業ですが、さらに詳しく見ていきましょう。It's fun to see and interact with holograms, but let's go further. 最初の共有エクスペリエンスを設定します。すべてのユーザーが一緒に見ることができます。We'll set up our first shared experience - a hologram everyone can see together.

目標Objectives

  • 共有エクスペリエンスのためにネットワークをセットアップします。Setup a network for a shared experience.
  • 共通の参照ポイントを確立します。Establish a common reference point.
  • デバイス間で座標系を共有します。Share coordinate systems across devices.
  • 全員が同じホログラムを見ることがあります。Everyone sees the same hologram!

注意

アプリが共有サーバーに接続するには、 InternetclientserverPrivateNetworkClientServer の機能を宣言する必要があります。The InternetClientServer and PrivateNetworkClientServer capabilities must be declared for an app to connect to the sharing server. これは、既にホログラム240に含まれていますが、独自のプロジェクトでは考慮しておいてください。This is done for you already in Holograms 240, but keep this in mind for your own projects.

  1. Unity エディターで、[Edit > Project Settings > Player] の順に移動して、windows media player の設定に移動します。In the Unity Editor, go to the player settings by navigating to "Edit > Project Settings > Player"
  2. [Windows ストア] タブをクリックします。Click on the "Windows Store" tab
  3. [発行の設定 > 機能] セクションで、 Internetclientserver の機能と PrivateNetworkClientServer 機能を確認します。In the "Publishing Settings > Capabilities" section, check the InternetClientServer capability and the PrivateNetworkClientServer capability

手順Instructions

  • [ プロジェクト] パネル で、 HoloToolkit-Sharing-240\Prefabs\Sharing フォルダーに移動します。In the Project panel navigate to the HoloToolkit-Sharing-240\Prefabs\Sharing folder.
  • 共有 prefab を [階層] パネル にドラッグアンドドロップします。Drag and drop the Sharing prefab into the Hierarchy panel.

次に、共有サービスを起動する必要があります。Next we need to launch the sharing service. この手順を実行する必要があるのは、共有エクスペリエンス内の 1 台の PC だけです。Only one PC in the shared experience needs to do this step.

  • Unity で、上部のメニューの [ HoloToolkit-240] メニュー を選択します。In Unity - in the top-hand menu - select the HoloToolkit-Sharing-240 menu.
  • ドロップダウンリストで [ 共有サービスの起動 ] 項目を選択します。Select the Launch Sharing Service item in the drop-down.
  • [ プライベートネットワーク ] オプションをオンにし、[ファイアウォールプロンプトが表示されたら アクセスを許可 する] をクリックします。Check the Private Network option and click Allow Access when the firewall prompt appears.
  • 共有サービスコンソールウィンドウに表示されている IPv4 アドレスをメモしておきます。Note down the IPv4 address displayed in the Sharing Service console window. これは、サービスが実行されているコンピューターと同じ IP です。This is the same IP as the machine the service is being run on.

共有エクスペリエンスに参加するすべての pc で、残りの手順に従います。Follow the rest of the instructions on all PCs that will join the shared experience.

  • 階層 で、共有 オブジェクトを選択します。In the Hierarchy, select the Sharing object.
  • インスペクター の [共有ステージ] コンポーネントで、サーバーアドレス を ' localhost ' から SharingService.exe を実行しているコンピューターの IPv4 アドレスに変更します。In the Inspector, on the Sharing Stage component, change the Server Address from 'localhost' to the IPv4 address of the machine running SharingService.exe.
  • 階層 で、 HologramCollection オブジェクトを選択します。In the Hierarchy select the HologramCollection object.
  • インスペクター で [コンポーネントの 追加] ボタンをクリックします。In the Inspector click the Add Component button.
  • 検索ボックスに、「 Import Export Anchor Manager」と入力します。In the search box, type Import Export Anchor Manager. 検索結果を選択します。Select the search result.
  • [ プロジェクト] パネル で、 Scripts フォルダーに移動します。In the Project panel navigate to the Scripts folder.
  • HologramPlacement スクリプトをダブルクリックして、Visual Studio で開きます。Double-click the HologramPlacement script to open it in Visual Studio.
  • 内容を次のコードに置き換えます。Replace the contents with the code below.
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 ] を選択します。Back in Unity, select the HologramCollection in the Hierarchy panel.
  • [ インスペクター] パネル で、[ コンポーネントの追加 ] ボタンをクリックします。In the Inspector panel click the Add Component button.
  • メニューで、検索ボックスに「 App State Manager」と入力します。In the menu, type in the search box App State Manager. 検索結果を選択します。Select the search result.

デプロイと活用Deploy and enjoy

  • HoloLens デバイス用のプロジェクトをビルドします。Build the project for your HoloLens devices.
  • 1つの HoloLens を最初に展開するように指定します。Designate one HoloLens to deploy to first. EnergyHub を配置する前に、アンカーがサービスにアップロードされるまで待機する必要があります (これには約30-60 秒かかることがあります)。You will need to wait for the Anchor to be uploaded to the service before you can place the EnergyHub (this can take ~30-60 seconds). アップロードが完了するまで、タップジェスチャは無視されます。Until the upload is done, your tap gestures will be ignored.
  • EnergyHub が配置されると、その場所がサービスにアップロードされ、他のすべての HoloLens デバイスに展開できるようになります。After the EnergyHub has been placed, its location will be uploaded to the service and you can then deploy to all other HoloLens devices.
  • 新しい HoloLens が最初にセッションに参加したときに、そのデバイスで EnergyHub の場所が正しくない可能性があります。When a new HoloLens first joins the session, the location of the EnergyHub may not be correct on that device. ただし、アンカーと EnergyHub の場所がサービスからダウンロードされるとすぐに、EnergyHub は新しい共有の場所に移動する必要があります。However, as soon as the anchor and EnergyHub locations have been downloaded from the service, the EnergyHub should jump to the new, shared location. これが約30-60 秒以内に行われない場合は、アンカーを設定して、より多くの環境の手掛かりを収集するときに、元の HoloLens がどこにあったかを説明します。If this does not happen within ~30-60 seconds, walk to where the original HoloLens was when setting the anchor to gather more environment clues. その場所がまだロックされていない場合は、デバイスに再デプロイします。If the location still does not lock on, redeploy to the device.
  • デバイスの準備が完了し、アプリを実行している場合は、EnergyHub を探します。When the devices are all ready and running the app, look for the EnergyHub. ホログラムの場所とテキストがどの方向に接しているかについては、すべて同意できますか。Can you all agree on the hologram's location and which direction the text is facing?

Chapter 4-検出Chapter 4 - Discovery

全員が同じホログラムを見ることができるようになりました。Everyone can now see the same hologram! それでは、共有 holographic 世界に接続されているすべてのユーザーを見てみましょう。Now let's see everyone else connected to our shared holographic world. この章では、同じ共有セッション内の他のすべての HoloLens デバイスの場所とローテーションについて説明します。In this chapter, we'll grab the head location and rotation of all other HoloLens devices in the same sharing session.

目標Objectives

  • 共有エクスペリエンスで互いを検出します。Discover each other in our shared experience.
  • プレーヤーアバターを選択して共有します。Choose and share a player avatar.
  • すべてのユーザーのヘッドの横に、プレーヤーアバターを添付します。Attach the player avatar next to everyone's heads.

手順Instructions

  • [ プロジェクト] パネル で、[ ホログラム ] フォルダーに移動します。In the Project panel navigate to the Holograms folder.
  • PlayerAvatarStore階層 にドラッグアンドドロップします。Drag and drop the PlayerAvatarStore into the Hierarchy.
  • [ プロジェクト] パネル で、 Scripts フォルダーに移動します。In the Project panel navigate to the Scripts folder.
  • AvatarSelector スクリプトをダブルクリックして、Visual Studio で開きます。Double-click the AvatarSelector script to open it in Visual Studio.
  • 内容を次のコードに置き換えます。Replace the contents with the code below.
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 オブジェクトを選択します。In the Hierarchy select the HologramCollection object.
  • インスペクター で [コンポーネントの追加] をクリックします。In the Inspector click Add Component.
  • 検索ボックスに、「 Local Player Manager」と入力します。In the search box, type Local Player Manager. 検索結果を選択します。Select the search result.
  • 階層 で、 HologramCollection オブジェクトを選択します。In the Hierarchy select the HologramCollection object.
  • インスペクター で [コンポーネントの追加] をクリックします。In the Inspector click Add Component.
  • 検索ボックスに、「 リモートプレーヤーマネージャー」と入力します。In the search box, type Remote Player Manager. 検索結果を選択します。Select the search result.
  • Visual Studio で HologramPlacement スクリプトを開きます。Open the HologramPlacement script in Visual Studio.
  • 内容を次のコードに置き換えます。Replace the contents with the code below.
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.
    }
}
  • Visual Studio で AppStateManager スクリプトを開きます。Open the AppStateManager script in Visual Studio.
  • 内容を次のコードに置き換えます。Replace the contents with the code below.
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;
        }
    }
}

デプロイと活用Deploy and Enjoy

  • プロジェクトをビルドし、HoloLens デバイスにデプロイします。Build and deploy the project to your HoloLens devices.
  • Ping 音が聞こえたら、アバター選択メニューを見つけて、エアタップジェスチャでアバターを選択します。When you hear a pinging sound, find the avatar selection menu and select an avatar with the air-tap gesture.
  • ホログラムを見ていない場合、カーソルの周囲のポイントライトは、HoloLens がサービスと通信しているときに、(濃い紫の) 初期化、アンカーのダウンロード (緑)、位置データのインポート/エクスポート (黄色)、アンカーのアップロード (青) で異なる色になります。If you're not looking at any holograms, the point light around your cursor will turn a different color when your HoloLens is communicating with the service: initializing (dark purple), downloading the anchor (green), importing/exporting location data (yellow), uploading the anchor (blue). カーソルの周囲のポイントライトが既定の色 (淡い紫) の場合は、セッション内の他のプレーヤーと対話する準備ができています。If your point light around your cursor is the default color (light purple), then you are ready to interact with other players in your session!
  • スペースに接続されている他のユーザーを確認します。 holographic ロボットはショルダーの上にフローティングし、頭の動きを模倣しています。Look at other people connected to your space - there will be a holographic robot floating above their shoulder and mimicking their head motions!

章 5: 配置Chapter 5 - Placement

この章では、アンカーを実際のサーフェイスに配置できるようにします。In this chapter, we'll make the anchor able to be placed on real-world surfaces. 共有座標を使用して、共有エクスペリエンスに接続されているすべてのユーザー間の中間点にアンカーを配置します。We'll use shared coordinates to place that anchor in the middle point between everyone connected to the shared experience.

目標Objectives

  • プレーヤーのヘッド位置に基づいて、空間マッピングメッシュにホログラムを配置します。Place holograms on the spatial mapping mesh based on players’ head position.

手順Instructions

  • [ プロジェクト] パネル で、[ ホログラム ] フォルダーに移動します。In the Project panel navigate to the Holograms folder.
  • CustomSpatialMapping Prefab を 階層 にドラッグアンドドロップします。Drag and drop the CustomSpatialMapping prefab onto the Hierarchy.
  • [ プロジェクト] パネル で、 Scripts フォルダーに移動します。In the Project panel navigate to the Scripts folder.
  • AppStateManager スクリプトをダブルクリックして、Visual Studio で開きます。Double-click the AppStateManager script to open it in Visual Studio.
  • 内容を次のコードに置き換えます。Replace the contents with the code below.
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 フォルダーに移動します。In the Project panel navigate to the Scripts folder.
  • HologramPlacement スクリプトをダブルクリックして、Visual Studio で開きます。Double-click the HologramPlacement script to open it in Visual Studio.
  • 内容を次のコードに置き換えます。Replace the contents with the code below.
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();
    }
}

デプロイと活用Deploy and enjoy

  • プロジェクトをビルドし、HoloLens デバイスにデプロイします。Build and deploy the project to your HoloLens devices.
  • アプリの準備が整ったら、サークルに EnergyHub て、すべてのユーザーの中央に表示されることを確認します。When the app is ready, stand in a circle and notice how the EnergyHub appears in the center of everyone.
  • タップして、EnergyHub を配置します。Tap to place the EnergyHub.
  • 音声コマンド ' Reset Target ' を使用して EnergyHub バックアップを選択し、グループとして連携して、ホログラムを新しい場所に移動してみてください。Try the voice command 'Reset Target' to pick the EnergyHub back up and work together as a group to move the hologram to a new location.

第6章-Real-World の物理Chapter 6 - Real-World Physics

この章では、現実世界の表面にバウンスするホログラムを追加します。In this chapter we'll add holograms that bounce off real-world surfaces. 自分と友人の両方によって起動されたプロジェクトで、スペースを埋めることができます。Watch your space fill up with projects launched by both you and your friends!

目標Objectives

  • 現実世界の表面にバウンドする projectiles を起動します。Launch projectiles that bounce off real-world surfaces.
  • 他のプレーヤーが見ることができるように、projectiles を共有します。Share the projectiles so other players can see them.

手順Instructions

  • 階層 で、 HologramCollection オブジェクトを選択します。In the Hierarchy select the HologramCollection object.
  • インスペクター で [コンポーネントの追加] をクリックします。In the Inspector click Add Component.
  • [検索] ボックスに、「"" の種類の表示 ツール」と入力します。In the search box, type Projectile Launcher. 検索結果を選択します。Select the search result.

デプロイと活用Deploy and enjoy

  • HoloLens デバイスにビルドしてデプロイします。Build and deploy to your HoloLens devices.
  • アプリがすべてのデバイスで実行されている場合は、エアタップを実行して、実世界のサーフェイスで航空タイルを起動します。When the app is running on all devices, perform an air-tap to launch projectile at real world surfaces.
  • 他のプレーヤーのアバターと競合している場合はどうなるかを確認してください。See what happens when your projectile collides with another player's avatar!

第7章-グランドくくりChapter 7 - Grand Finale

この章では、コラボレーションによってのみ検出できるポータルについて説明します。In this chapter, we'll uncover a portal that can only be discovered with collaboration.

目標Objectives

  • 連携して、秘密ポータルを見つけるために十分な projectiles をアンカーで立ち上げましょう。Work together to launch enough projectiles at the anchor to uncover a secret portal!

手順Instructions

  • [ プロジェクト] パネル で、[ ホログラム ] フォルダーに移動します。In the Project panel navigate to the Holograms folder.
  • HologramCollection の子 として、黄泉 の資産をドラッグアンドドロップします。Drag and drop the Underworld asset as a child of HologramCollection.
  • HologramCollection を選択した状態で、インスペクター の [コンポーネントの追加] ボタンをクリックします。With HologramCollection selected, click the Add Component button in the Inspector.
  • メニューで、検索ボックスに「 ExplodeTarget」と入力します。In the menu, type in the search box ExplodeTarget. 検索結果を選択します。Select the search result.
  • HologramCollection を選択した状態で、階層 から、 EnergyHub オブジェクトを インスペクター の [ターゲット] フィールドにドラッグします。With HologramCollection selected, from the Hierarchy drag the EnergyHub object to the Target field in the Inspector.
  • HologramCollection を選択した状態で、階層 から、黄泉 のオブジェクトを インスペクター の [黄泉] フィールドにドラッグします。With HologramCollection selected, from the Hierarchy drag the Underworld object to the Underworld field in the Inspector.

デプロイと活用Deploy and enjoy

  • HoloLens デバイスにビルドしてデプロイします。Build and deploy to your HoloLens devices.
  • アプリが起動したら、共同作業を行って、EnergyHub で projectiles を起動します。When the app has launched, collaborate together to launch projectiles at the EnergyHub.
  • 黄泉の場所にいる場合は、黄泉のロボットロボットで projectiles を起動します (特別に楽しいようにロボットを3回押します)。When the underworld appears, launch projectiles at underworld robots (hit a robot three times for extra fun).