重要
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 或更新版本。
- 將檔案解除封存到桌面或其他容易觸達的位置。 將資料夾名稱保留為 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 章 - 共用座標
查看全像投影並與全像投影互動很有趣,但讓我們進一步瞭解。 我們將設定我們的第一個共享體驗 - 全像投影每個人都可以一起看到。
目標
- 為共用體驗設定網路。
- 建立通用參考點。
- 跨裝置共用座標系統。
- 每個人都會看到相同的全像投影!
注意
必須宣告 InternetClientServer 和 PrivateNetworkClientServer 功能,應用程式才能連線到共享伺服器。 這已為您在全像投影 240 中完成,但請留意您自己的專案。
- 在 Unity 編輯器中,流覽至 [編輯>專案設定播放機] 以移至播放機設定>
- 按兩下 [Windows 市集] 索引標籤
- 在 [發佈設定 > 功能] 區段中,檢查 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 上啟動投影。
- 當下層出現時,在下層機器人 (三次啟動投影機,以取得額外的) 。