HoloLens (第 1 代) 共用 240:多個 HoloLens 裝置

重要

Mixed Reality Academy 教學課程是使用 HoloLens (第 1 代) 、Unity 2017 和 Mixed Reality 沈浸式頭戴式裝置所設計。 因此,對於仍在尋找這些裝置開發指引的開發人員而言,我們覺得這些教學課程很重要。 這些教學課程不會使用用於 HoloLens 2 的最新工具組或互動進行更新,而且可能與較新版本的 Unity 不相容。 系統會保留這些資訊,以繼續在支援的裝置上運作。 已針對 HoloLens 2 公佈一系列新的教學課程

在空間中移動時,全像投影會保留於世界。 HoloLens 會使用各種 座標系統 來保留全像投影,以追蹤物件的位置和方向。 當我們在裝置之間共用這些座標系統時,我們可以建立共用體驗,讓我們參與共用全像攝影世界。

在此教學課程中,我們將:

  • 設定網路以取得共享體驗。
  • 跨 HoloLens 裝置共用全像投影。
  • 探索我們共用全像攝影世界中的其他人。
  • 建立共用的互動式體驗,您可以在其中以其他玩家為目標 -- 並在其中啟動投影項!

裝置支援

課程 HoloLens 沉浸式頭戴裝置
MR Sharing 240:多個 HoloLens 裝置 ✔️

在您開始使用 Intune 之前

必要條件

專案檔

  • 下載專案所需的 檔案 。 需要 Unity 2017.2 或更新版本。
    • 如果您仍然需要 Unity 5.6 支援,請使用 此版本
    • 如果您仍然需要 Unity 5.5 支援,請使用 此版本
    • 如果您仍然需要 Unity 5.4 支援,請使用 此版本
  • 將檔案解除封存到桌面或其他容易觸達的位置。 將資料夾名稱保留為 SharedHolograms

注意

如果您想要在下載之前查看原始程式碼,可在 GitHub 上取得

第 1 章 - Holo World

在本章中,我們將設定第一個 Unity 專案,並逐步完成建置和部署程式。

目標

  • 設定 Unity 以開發全像攝影應用程式。
  • 查看全像投影!

指示

  • 啟動 Unity。
  • 選取 [開啟]。
  • 輸入位置作為您先前未架構的 SharedHolograms 資料夾。
  • 選取 [項目名稱 ],然後按下 [ 選取資料夾]。
  • [階層] 中,以滑鼠右鍵按兩下 主要相機 ,然後選取 [ 刪除]。
  • HoloToolkit-Sharing-240/Prefabs/Camera 資料夾中,尋找 主要相機 預製專案。
  • 主要相機 拖放到 階層中。
  • [階層] 中,按兩下 [ 建立建立空白]。
  • 以滑鼠右鍵按下新的 GameObject ,然後選取 [重新命名]。
  • 將 GameObject 重新命名為 HologramCollection
  • 選取Hierarchy中的HologramCollection物件。
  • [偵測器 ] 中,將 轉換位置 設定為: X: 0、Y: -0.25、Z: 2
  • [專案] 面板中[全像投影] 資料夾中,尋找 EnergyHub 資產。
  • EnergyHub 物件從 [專案] 面板 拖放到 [階層 ] 作為 HologramCollection 的子系
  • 選取 [檔案 > ] [儲存場景...
  • 將場景命名為 SharedHolograms, 然後按兩下 [ 儲存]。
  • 按 Unity 中的 [ 播放] 按鈕以預覽全像投影。
  • 按第二次 播放 以停止預覽模式。

將專案從 Unity 導出至 Visual Studio

  • 在 Unity 中,選取 [ 檔案 > 建置設定]。
  • 按兩下 [新增開啟場景 ] 以新增場景。
  • 在 [平臺] 列表中選取 [通用 Windows 平台],然後按兩下 [切換平臺]。
  • SDK 設定為 通用 10
  • 目標裝置 設定為 HoloLens並將 UWP 組建類型 設定為 D3D
  • 檢查 Unity C# 專案
  • 按一下 [建置]
  • 在出現的檔案總管視窗中,建立名為 「App」 的新資料夾
  • 按兩下 [ 應用程式 ] 資料夾。
  • [選取資料夾]。
  • 當 Unity 完成時,會出現 檔案總管 視窗。
  • 開啟 [應用程式 ] 資料夾。
  • 開啟 SharedHolograms.sln 以啟動 Visual Studio。
  • 在 Visual Studio 中使用頂端工具列,將目標從 [偵錯] 變更為 [發行 ],並將 [ARM] 變更為 [X86]。
  • 按兩下 [本機計算機] 旁的下拉式箭號,然後選取 [ 遠端裝置]。
    • [位址 ] 設定為 HoloLens 的名稱或 IP 位址。 如果您不知道您的裝置 IP 位址,請查看 > [設定網络] & [因特網>進階選項],或詢問 Cortana「Hey Cortana,我的 IP 地址為何?」
    • [驗證模式 ] 保留為 [通用]。
    • 按一下 [選取]
  • 按兩下 [ 偵錯 > 開始但不 偵錯] 或按 Ctrl + F5。 如果這是第一次部署到您的裝置,您必須 將它與 Visual Studio 配對
  • 放置 HoloLens 並尋找 EnergyHub 全像投影。

第 2 章 - 互動

在本章中,我們將與全像投影互動。 首先,我們會新增游標以可視化 方式呈現我們的注視。 然後,我們將新增 手勢 並使用手部將全像投影放在空間中。

目標

  • 使用註視輸入來控制游標。
  • 使用手勢輸入來與全像投影互動。

指示

目光

  • 在 [ 階層] 面板中 ,選取 HologramCollection 物件。
  • [偵測器] 面板中 ,按兩下 [ 新增元件 ] 按鈕。
  • 在功能表中,輸入搜尋方塊 注視管理員。 選取搜尋結果。
  • HoloToolkit-Sharing-240\Prefabs\Input 資料夾中,尋找 Cursor 資產。
  • 游標 資產拖放至 階層

手勢

  • 在 [ 階層] 面板中 ,選取 HologramCollection 物件。
  • 按兩下 [新增元件 ],然後在搜尋欄位中輸入 [筆勢管理員 ]。 選取搜尋結果。
  • 在 [ 階層] 面板中,展開 [HologramCollection]。
  • 選取子 EnergyHub 物件。
  • [偵測器] 面板中 ,按兩下 [ 新增元件] 按鈕。
  • 在功能表中,輸入搜尋方塊 全像投影放置。 選取搜尋結果。
  • 選取 [ 檔案 > 儲存場景] 以儲存場景。

部署和享受

  • 使用上一章中的指示,建置並部署至 HoloLens。
  • 在 HoloLens 上啟動應用程式之後,請四處行動您的頭部,並注意 EnergyHub 如何遵循您的注視。
  • 請注意當您注視全像投影時光標的顯示方式,以及當沒有在全像投影上擷取時變更為點光線的方式。
  • 執行空中點選以放置全像投影。 目前在我們的專案中,您只能將全像投影放在 (重新部署一次,再試一次) 。

第 3 章 - 共用座標

查看全像投影並與全像投影互動很有趣,但讓我們進一步瞭解。 我們將設定我們的第一個共享體驗 - 全像投影每個人都可以一起看到。

目標

  • 為共用體驗設定網路。
  • 建立通用參考點。
  • 跨裝置共用座標系統。
  • 每個人都會看到相同的全像投影!

注意

必須宣告 InternetClientServerPrivateNetworkClientServer 功能,應用程式才能連線到共享伺服器。 這已為您在全像投影 240 中完成,但請留意您自己的專案。

  1. 在 Unity 編輯器中,流覽至 [編輯>專案設定播放機] 以移至播放機設定>
  2. 按兩下 [Windows 市集] 索引標籤
  3. 在 [發佈設定 > 功能] 區段中,檢查 InternetClientServer 功能和 PrivateNetworkClientServer 功能

指示

  • [專案] 面板中 ,流覽至 HoloToolkit-Sharing-240\Prefabs\Sharing 資料夾。
  • [共用 預製專案] 拖放到 [ 階層] 面板中

接下來,我們需要啟動共享服務。 共享體驗中只有 一部計算機 需要執行此步驟。

  • 在 Unity 的頂端功能表中,選取 HoloToolkit-Sharing-240 功能表
  • 選取下拉式清單中的 [啟動共用服務 ] 專案。
  • 核取 [專用網] 選項,然後在防火牆提示出現時按兩下 [ 允許存取 ]。
  • 記下 [共享服務控制台] 視窗中顯示的 IPv4 位址。 這是與服務執行所在的電腦相同的IP。

請遵循將加入共享體驗 之所有計算機的 其餘指示。

  • 在 [ 階層] 中,選取 [共用 ] 物件。
  • [偵測器] 的 [共享階段 ] 元件上,將 [伺服器位址 ] 從 'localhost' 變更為執行 SharingService.exe 之計算機的 IPv4 位址。
  • [階層] 中,選取 HologramCollection 物件。
  • [偵測器 ] 中,按兩下 [ 新增元件] 按鈕。
  • 在搜尋方塊中,輸入 [匯入導出錨點管理員]。 選取搜尋結果。
  • [專案] 面板中 ,流覽至 [ 腳稿 ] 資料夾。
  • 按兩下 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]。
  • [偵測器] 面板中 ,按兩下 [ 新增元件] 按鈕。
  • 在功能表中,輸入搜尋方塊 [應用程式狀態管理員]。 選取搜尋結果。

部署和享受

  • 建置 HoloLens 裝置的專案。
  • 指定要先部署的一個 HoloLens。 您必須等候錨點上傳至服務,才能放置 EnergyHub (這可能需要大約 30-60 秒) 。 在上傳完成之前,將會忽略您的點選手勢。
  • 放置 EnergyHub 之後,其位置會上傳至服務,然後您就可以部署到所有其他 HoloLens 裝置。
  • 當新的 HoloLens 第一次加入工作階段時,該裝置上的 EnergyHub 位置可能不正確。 不過,只要從服務下載錨點和 EnergyHub 位置,EnergyHub 就應該跳到新的共用位置。 如果這不會在 ~30-60 秒內發生,請逐步前往將錨點設定為收集更多環境線索時的原始 HoloLens 所在位置。 如果位置仍然未鎖定,請重新部署至裝置。
  • 當裝置準備就緒並執行應用程式時,請尋找 EnergyHub。 您是否全都同意全像投影的位置,以及文字面向的方向?

第 4 章 - 探索

每個人都可以看到相同的全像投影! 現在讓我們看看其他人已連線到我們共用全像攝影世界。 在本章中,我們將擷取相同共用會話中所有其他 HoloLens 裝置的頭部位置和旋轉。

目標

  • 在我們的共享體驗中探索彼此。
  • 選擇並共用玩家虛擬人偶。
  • 將玩家虛擬人偶附加至每個人的頭部旁邊。

指示

  • [專案] 面板中 ,流覽至 [全像投影] 資料夾。
  • PlayerAvatarStore 拖放到 階層中
  • [專案] 面板中 ,流覽至 [ 腳稿 ] 資料夾。
  • 按兩下 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。 選取搜尋結果。
  • 在 Visual Studio 中開啟 HologramPlacement 腳本。
  • 使用以下列程式碼取代內容。
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 腳本。
  • 使用以下列程式碼取代內容。
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 裝置。
  • 當您聽到 ping 音效時,請尋找虛擬人偶選取功能表,然後選取具有空中點選手勢的虛擬人偶。
  • 如果您未查看任何全像投影,當您的 HoloLens 與服務通訊時,游標周圍的點光線會變成不同的色彩:初始化 (深紫色) 、下載錨點 (綠色) 、匯入/導出位置數據 (黃色) 、上傳錨點 (藍色) 。 如果您的游標周圍的點光線是預設色彩 (淺紫色) ,則您已準備好與會話中的其他玩家互動!
  • 查看其他人已連線到您的空間 - 會有一個全像攝影機器人浮動在他們的頭部上方,並模擬其頭部動作!

第 5 章 - 放置

在本章中,我們將讓錨點能夠放置在真實世界表面。 我們將使用共用座標,將該錨點放在連線到共用體驗的每個人之間的中間點。

目標

  • 根據玩家的頭部位置,將全像投影放在空間對應網格上。

指示

  • [專案] 面板中 ,流覽至 全像投影 資料夾。
  • CustomSpatialMapping 預製專案拖放至 階層
  • [專案] 面板中 ,流覽至 [ 腳稿 ] 資料夾。
  • 按兩下 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;
        }
    }
}
  • [專案] 面板中 ,流覽至 [ 腳稿 ] 資料夾。
  • 按兩下 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。
  • 嘗試使用語音命令 「重設目標」來挑選 EnergyHub 備份,並一起合作以群組方式將全像投影移至新位置。

第 6 章 - Real-World 物理

在本章中,我們將新增全像投影,以從真實世界表面彈跳。 觀看您的空間填滿您和朋友所啟動的專案!

目標

  • 啟動跳離真實世界表面的投影。
  • 共用投影區,讓其他玩家可以看到這些專案。

指示

  • [階層 ] 中,選取 HologramCollection 物件。
  • [偵測器 ] 中,按兩下 [新增元件]。
  • 在搜尋方塊中,輸入 Projectile Launcher。 選取搜尋結果。

部署並享受

  • 建置並部署至您的 HoloLens 裝置。
  • 當應用程式在所有裝置上執行時,請執行空中點選以在真實世界表面啟動投影。
  • 查看當投影位與另一位玩家的虛擬人偶衝突時會發生什麼事!

第 7 章 - Grand Finale

在本章中,我們將發現只能透過共同作業探索的入口網站。

目標

  • 一起在錨點啟動足夠的投影,以發現秘密入口網站!

指示

  • [專案] 面板中 ,流覽至 全像投影 資料夾。
  • Underworld 資產拖放為 全像投影Collection 的子系
  • 選取全像投影Collection 后,按兩下Inspector中的[新增元件] 按鈕。
  • 在功能表中,於搜尋方塊中輸入 「分解目標」。 選取搜尋結果。
  • 選取全像投影Collection 之後,從 [階層] 將 EnergyHub 物件拖曳至 Inspector 中的 [目標] 字段。
  • 選取全像投影Collection 后,從 [階層] 將 Underworld 物件拖曳到 Inspector 中的 [下層] 欄位。

部署並享受

  • 建置並部署至您的 HoloLens 裝置。
  • 當應用程式啟動時,請共同作業,以在 EnergyHub 上啟動投影。
  • 當下層出現時,在下層機器人 (三次啟動投影機,以取得額外的) 。