HoloLens(第一代)和 Azure 305:Functions 和存储
注意
混合现实学院教程在制作时考虑到了 HoloLens(第一代)和混合现实沉浸式头戴显示设备。 因此,对于仍在寻求这些设备的开发指导的开发人员而言,我们觉得很有必要保留这些教程。 我们不会在这些教程中更新 HoloLens 2 所用的最新工具集或集成相关的内容。 我们将维护这些教程,使之持续适用于支持的设备。 将来会发布一系列演示如何针对 HoloLens 2 进行开发的新教程。 此通知将在教程发布时通过指向这些教程的链接进行更新。
此课程介绍如何在混合现实应用中创建和使用 Azure Functions,以及在混合现实应用程序中的 Azure 存储资源内存储数据。
Azure Functions 是一项 Microsoft 服务,它允许开发人员在 Azure 中运行小段代码“函数”。 这提供了一种将工作委托给云(而不是本地应用程序)的方法,这种方法会有很多优势。 Azure Functions 支持多种开发语言,包括 C#、F#、Node.js、Java 和 PHP。 有关详细信息,请参阅 Azure Functions 一文。
Azure 存储是一种 Microsoft 云服务,它使开发人员能够存储数据,并确保其高度可用、安全、持久、可缩放和冗余。 这意味着,Microsoft 将为你处理所有维护和关键问题。 有关详细信息,请参阅 Azure 存储一文。
完成本课程后,你将拥有一个混合现实沉浸式头戴显示设备应用程序,该应用程序将能够执行以下操作:
- 让用户能够凝视周围场景。
- 当用户凝视 3D“按钮”时,会触发生成对象。
- 生成的对象将由 Azure Functions 选择。
- 生成每个对象时,应用程序会将对象类型存储在 Azure 存储下的 Azure 文件中。
- 第二次加载时,将检索 Azure 文件数据,并使用它来重播应用程序的上一个实例中的生成操作。
在应用程序中,由你决定结果与设计的集成方式。 本课程旨在教授如何将 Azure 服务与 Unity 项目集成。 你的任务是运用从本课程中学到的知识来增强混合现实应用程序。
设备支持
课程 | HoloLens | 沉浸式头戴显示设备 |
---|---|---|
MR 和 Azure 305:功能和存储 | ✔️ | ✔️ |
注意
尽管本课程主要重点介绍 Windows Mixed Reality 沉浸式 (VR) 头戴显示设备,但你也可以将本课程中学到的内容应用到 Microsoft HoloLens。 随着课程的进行,你将看到有关支持 HoloLens 可能需要进行的任何更改的说明。
先决条件
注意
本教程专为具有 Unity 和 C# 基本经验的开发人员设计。 另请注意,本文档中的先决条件和书面说明在编写时(2018 年 5 月)已经过测试和验证。 可随意使用最新软件(如安装工具一文所列的软件),但不应假设本课程中的信息会与你在比下列版本更高的软件中找到的内容完全一致。
建议在本课程中使用以下硬件和软件:
- 一台与 Windows Mixed Reality 兼容的开发电脑,用于沉浸式 (VR) 头戴显示设备方面的开发
- 已启用开发人员模式的 Windows 10 Fall Creators Update(或更高版本)
- 最新的 Windows 10 SDK
- Unity 2017.4
- Visual Studio 2017
- Windows Mixed Reality 沉浸式 (VR) 头戴显示设备或已启用开发人员模式的 Microsoft HoloLens
- Azure 帐户的订阅,用于创建 Azure 资源
- 可访问 Internet 以便进行 Azure 设置和数据检索
开始之前
为了避免在生成此项目时遇到问题,强烈建议在根文件夹或接近根的文件夹中创建本教程中提到的项目(长文件夹路径会在生成时导致问题)。
第 1 章 - Azure 门户
若要使用 Azure 存储服务,你将需要在 Azure 门户中创建和配置“存储帐户”。
登录到 Azure 门户。
注意
如果你没有 Azure 帐户,需要创建一个。 如果你在课堂或实验室场景中跟着本教程学习,请让讲师或监督人员帮助设置你的新帐户。
登录后,单击左上角的“新建”,搜索“存储帐户”,并单击“Enter”键。
注意
在更新的门户中,“新建”一词可能已替换为“创建资源”。
新页面将提供“Azure 存储帐户”服务的说明。 在该提示的左下角,选择“创建”按钮,以创建与此服务的关联。
单击“创建”后:
为帐户插入名称,请注意,此字段仅接受数字和小写字母。
对于“部署模型”,请选择“资源管理器”。
对于“帐户类型”,请选择“存储(常规用途 v1)”。
为资源组确定位置(如果正在创建新的资源组)。 理想情况下,此位置在运行应用程序的区域中。 某些 Azure 资产仅在特定区域可用。
对于“复制”,请选择“读取访问地域冗余存储(RA-GRS)”。
对于“性能”,请选择“标准”。
将“需要安全传输”保留为“已禁用”。
选择一个“订阅” 。
选择一个资源组或创建一个新资源组。 通过资源组,可监视和预配 Azure 资产集合、控制其访问权限并管理其计费。 建议保留与单个项目(例如通用资源组下的这些实验室)相关联的所有 Azure 服务。
若要详细了解 Azure 资源组,请访问资源组一文。
还需要确认了解应用于此服务的条款和条件。
选择创建。
单击“创建”后,必须等待服务创建完成,这可能需要一分钟时间。
创建服务实例后,门户中将显示一条通知。
单击通知以浏览新的服务实例。
单击通知中的“转到资源”按钮,浏览新的服务实例。 你将转到新的“存储帐户”服务实例。
单击“访问密钥”,显示此云服务的终结点。 使用记事本或类似的工具来复制其中一个密钥供稍后使用。 另外,请记下“连接字符串”值,因为在稍后创建“AzureServices”类时会用到它。
第 2 章 - 设置 Azure 函数
现在,将在 Azure 服务中编写 Azure 函数 。
你可以使用 Azure 函数执行使用经典函数在代码中执行的几乎任何操作,区别在于,具有访问 Azure 帐户的凭据的任何应用程序都可以访问此函数。
若要创建 Azure 函数,请执行以下操作:
在 Azure 门户中,单击左上角的“新建”,搜索“函数应用”,然后单击“Enter”键。
注意
在更新的门户中,“新建”一词可能已替换为“创建资源”。
新页面将提供“Azure 函数应用”服务的说明。 在该提示的左下角,选择“创建”按钮,以创建与此服务的关联。
单击“创建”后:
提供一个应用名称。 此处只能使用字母和数字(允许使用大写或小写)。
选择你的首选订阅。
选择一个资源组或创建一个新资源组。 通过资源组,可监视和预配 Azure 资产集合、控制其访问权限并管理其计费。 建议保留与单个项目(例如通用资源组下的这些实验室)相关联的所有 Azure 服务。
若要详细了解 Azure 资源组,请访问资源组一文。
对于本练习,请选择 Windows 作为所选 OS。
对于“托管计划”,请选择“消耗计划”。
为资源组确定位置(如果正在创建新的资源组)。 理想情况下,此位置在运行应用程序的区域中。 某些 Azure 资产仅在特定区域可用。 为了获得最佳性能,请选择与存储帐户相同的区域。
对于“存储”,选择“使用现有”,然后使用下拉菜单查找先前创建的存储。
对于本练习,请将“Application Insights”保留为“关闭”状态。
单击“创建” 按钮。
单击“创建”后,必须等待服务创建完成,这可能需要一分钟时间。
创建服务实例后,门户中将显示一条通知。
单击通知以浏览新的服务实例。
单击通知中的“转到资源”按钮,浏览新的服务实例。 你将转到新的函数应用服务实例。
在“函数应用”仪表板上,将鼠标悬停在左侧面板中的“函数”上方,并单击“+ (加号)”符号。
在下一页上,确保已选择“Webhook + API”,对于“选择语言”,请选择“CSharp”,因为本教程中将使用该语言。 最后,单击“创建此函数”按钮。
应转到代码页 (run.csx),如果没有,则在左侧面板的“函数”列表中,单击新创建的函数。
将以下代码复制到函数中。 调用时,此函数将只返回 0 到 2 之间的随机整数。 不要担心现有代码,可以随意粘贴到其顶部。
using System.Net; using System.Threading.Tasks; public static int Run(CustomObject req, TraceWriter log) { Random rnd = new Random(); int randomInt = rnd.Next(0, 3); return randomInt; } public class CustomObject { public String name {get; set;} }
选择“保存”。
结果应如下图所示:
单击“获取函数 URL”并记下显示的终结点。 你需要将其插入到你稍后将在本课程中创建的“AzureServices”类中。
第 3 章 - 设置 Unity 项目
下面是用于使用混合现实进行开发的典型设置,因此,这对其他项目来说是一个不错的模板。
设置并测试混合现实沉浸式头戴显示设备。
注意
本课程不需要运动控制器。 如果需要支持设置沉浸式头戴显示设备,请访问混合现实设置一文。
打开 Unity,并单击“新建”。
现在需要提供 Unity 项目名称。 插入 MR_Azure_Functions。 确保将项目类型设置为“3D”。 将“位置”设置为适合你的位置(请记住,越接近根目录越好)。 然后,单击“创建项目”。
当 Unity 处于打开状态时,有必要检查默认“脚本编辑器”是否设置为“Visual Studio”。 转到“编辑”>“首选项”,然后在新窗口中导航到“外部工具”。 将外部脚本编辑器更改为 Visual Studio 2017。 关闭“首选项”窗口。
接下来,转到“文件”>“生成设置”,并通过单击“切换平台”按钮将平台切换到“通用 Windows 平台”。
转到“文件”>“生成设置”,并确保:
将“目标设备”设置为“任何设备”。
对于 Microsoft HoloLens,请将“目标设备”设置为“HoloLens”。
将“生成类型”设置为“D3D”
将“SDK”设置为“最新安装的版本”
将“Visual Studio 版本”设置为“最新安装的版本”
将“生成并运行”设置为“本地计算机”
保存场景并将其添加到生成。
通过选择“添加开放场景”来执行此操作。 将出现一个保存窗口。
为此创建新文件夹,并为将来的任何场景创建一个新文件夹,然后选择“新建文件夹”按钮以创建新文件夹,将其命名为“场景”。
打开新创建的“场景”文件夹,然后在“文件名:”文本字段中,键入 FunctionsScene,然后按“保存”。
在“生成设置”中,其余设置目前应保留为默认值。
在“生成设置”窗口中,单击“播放器设置”按钮,这会在检查器所在的空间中打开相关面板。
在此面板中,需要验证一些设置:
在“其他设置”选项卡中:
- 脚本运行时版本应为试验版(等效于 .Net 4.6),这将导致需要重启编辑器。
- “脚本后端”应为 “.NET”
- “API 兼容性级别”应为“.NET 4.6”
在“发布设置”选项卡的“功能”下,检查以下内容:
InternetClient
再往下滑面板,在“XR 设置”(在“发布设置”下方)中,勾选“支持的虚拟现实”,确保已添加“Windows Mixed Reality SDK”。
返回生成设置 Unity C# 项目不再灰显;勾选此框旁边的复选框。
关闭“生成设置”窗口 。
保存场景和项目(“文件”>“保存场景/文件”>“保存项目”)。
第 4 章 - 设置主摄像头
重要
如果要跳过本课程的“Unity 设置”部分并继续直接编写代码,请随意下载此 .unitypackage,并将其作为自定义包导入项目中。 这还将包含下一章中的 DLL。 导入后,请继续学习第 7 章。
在“层次结构面板”中,你将找到一个称为“主摄像头”的对象,一旦“进入”应用程序,此对象就代表你的“头部”视角。
在你面前的 Unity 仪表板上,选择“主摄像头 GameObject”。 你会注意到,“检查器面板”(通常位于仪表板内的右侧)将显示该 GameObject 的各种组件(“转换”位于顶部,其次是“摄像头”和一些其他组件)。 需要重置主摄像头的转换,以便正确定位。
若要实现这一点,请选择摄像头“转换”组件旁边的齿轮图标,并选择“重置”。
然后将“转换”组件更新为如下所示:
转换 - 位置
X | Y | Z |
---|---|---|
0 | 1 | 0 |
转换 - 旋转
X | Y | Z |
---|---|---|
0 | 0 | 0 |
转换 - 缩放
X | Y | Z |
---|---|---|
1 | 1 | 1 |
第 5 章 - 设置 Unity 场景
右键单击层次结构面板的空白区域,在“3D 对象”下添加一个平面。
选中“平面”对象后,在检查器面板中更改以下参数:
转换 - 位置
X | Y | Z |
---|---|---|
0 | 0 | 4 |
转换 - 缩放
X | Y | Z |
---|---|---|
10 | 1 | 10 |
右键单击“层次结构面板”的空白区域,在“3D 对象”下,添加“立方体”。
将“立方体”重命名为“GazeButton”(选中“立方体”后,按“F2”)。
在“检查器面板”中更改“转换位置”的以下参数:
X Y Z 0 3 5 单击“标记”下拉按钮,然后单击“添加标记”以打开“标记和层”窗格。
选择“+ (加号)”按钮,在“新建标记名称”字段中输入“GazeButton”,然后按“保存”。
单击“层次结构面板”中的“GazeButton”对象,在“检查器面板”中,分配新创建的“GazeButton”标记。
在“层次结构面板”中右键单击“GazeButton”对象,并添加一个空的 GameObject(该对象将添加为子对象)。
选择新对象并将其重命名为“ShapeSpawnPoint”。
在“检查器面板”中更改“转换位置”的以下参数:
X Y Z 0 -1 0
接下来,你将创建一个“3D 文本”对象,以提供有关 Azure 服务状态的反馈。
再次右键单击“层次结构面板”中的“GazeButton”,并添加一个“3D 对象”>“3D 文本”对象作为子对象。
将“3D 文本”对象重命名为“AzureStatusText”。
将“AzureStatusText”对象的“转换位置”更改为如下所示内容:
X Y Z 0 0 -0.6 将“AzureStatusText”对象的“转换缩放”更改为如下所示内容:| X | Y | Z | | :---: | :---: | :---: | | 0.1 | 0.1 | 0.1 |
注意
如果它看起来偏离中心,请不要担心,因为在更新下面的文本网格组件时,会修复这种情况。
更改“文本网格”组件以匹配以下内容:
提示
此处所选的颜色是十六进制颜色:000000FF,但可以随意选择自己的颜色,只需确保它是可读的。
层次结构面板结构现在应如下所示:
场景现在应如下所示:
第 6 章 - 导入适用于 Unity 的 Azure 存储
你将使用适用于 Unity 的 Azure 存储(它本身利用的是适用于 Azure 的 .Net SDK)。 有关详细信息,请参阅适用于 Unity 的 Azure 存储一文。
Unity 中当前存在一个已知问题,即需要在导入后重新配置插件。 解决 bug 后不再需要执行这些步骤(本部分的 4-7)。
若要将 SDK 导入到自己的项目中,请确保已从 GitHub 下载最新的“.unitypackage”。 然后执行以下操作:
通过使用“资产”>“导入包”>“自定义包”菜单选项将“.unitypackage”文件添加到 Unity。
在弹出的“导入 Unity 包”框中,可以选择“插件”>“存储”下的所有内容。 取消选中其他所有内容,因为本课程不需要这些内容。
单击“导入”按钮,将项添加到项目。
转到“插件”下的“存储”文件夹中,在“项目”视图中,仅选择以下插件:
Microsoft.Data.Edm
Microsoft.Data.OData
Microsoft.WindowsAzure.Storage
Newtonsoft.Json
System.Spatial
选中这些特定插件后,取消选中“任何平台”,然后取消选中“WSAPlayer”,然后单击“应用”。
注意
我们正在将这些特定插件标记为仅在 Unity 编辑器中使用。 这是因为,在从 Unity 导出项目后,将使用 WSA 文件夹中的相同插件的不同版本。
在“存储”插件文件夹中,仅选择:
Microsoft.Data.Services.Client
选中“平台设置”下的“不处理”框,然后单击“应用”。
注意
我们正在将此插件标记为“不处理”,因为 Unity 程序集修补程序在处理此插件时遇到困难。 即使未处理插件,该插件仍可正常工作。
第 7 章 - 创建 AzureServices 类
要创建的第一个类是 AzureServices 类。
AzureServices 类将负责以下工作:
存储 Azure 帐户凭据。
调用 Azure 应用函数。
在 Azure 云存储中上传和下载数据文件。
若要创建此类,请执行以下操作:
右键单击“资产”文件夹,该文件夹位于“项目”面板中的“创建”>“文件夹”下。 将文件夹命名为“脚本”。
双击刚刚创建的文件夹,打开它。
右键单击文件夹并选择“创建”>“C# 脚本”。 调用脚本 AzureServices。
双击新的 AzureServices 类,以在 Visual Studio 中打开它。
将以下命名空间添加到 AzureServices 的顶部:
using System; using System.Threading.Tasks; using UnityEngine; using Microsoft.WindowsAzure.Storage; using Microsoft.WindowsAzure.Storage.File; using System.IO; using System.Net;
在 AzureServices 类中添加以下检查器字段:
/// <summary> /// Provides Singleton-like behavior to this class. /// </summary> public static AzureServices instance; /// <summary> /// Reference Target for AzureStatusText Text Mesh object /// </summary> public TextMesh azureStatusText;
然后在 AzureServices 类中添加以下成员变量:
/// <summary> /// Holds the Azure Function endpoint - Insert your Azure Function /// Connection String here. /// </summary> private readonly string azureFunctionEndpoint = "--Insert here you AzureFunction Endpoint--"; /// <summary> /// Holds the Storage Connection String - Insert your Azure Storage /// Connection String here. /// </summary> private readonly string storageConnectionString = "--Insert here you AzureStorage Connection String--"; /// <summary> /// Name of the Cloud Share - Hosts directories. /// </summary> private const string fileShare = "fileshare"; /// <summary> /// Name of a Directory within the Share /// </summary> private const string storageDirectory = "storagedirectory"; /// <summary> /// The Cloud File /// </summary> private CloudFile shapeIndexCloudFile; /// <summary> /// The Linked Storage Account /// </summary> private CloudStorageAccount storageAccount; /// <summary> /// The Cloud Client /// </summary> private CloudFileClient fileClient; /// <summary> /// The Cloud Share - Hosts Directories /// </summary> private CloudFileShare share; /// <summary> /// The Directory in the share that will host the Cloud file /// </summary> private CloudFileDirectory dir;
重要
请确保将“终结点”和“连接字符串”值替换为 Azure 存储中的值(可在 Azure 门户中找到)
现在需要添加 Awake() 和 Start() 方法的代码。 类初始化时,将调用这些方法:
private void Awake() { instance = this; } // Use this for initialization private void Start() { // Set the Status text to loading, whilst attempting connection to Azure. azureStatusText.text = "Loading..."; } /// <summary> /// Call to the Azure Function App to request a Shape. /// </summary> public async void CallAzureFunctionForNextShape() { }
重要
将在以后的章节中填写 CallAzureFunctionForNextShape() 的代码。
删除 Update() 方法,因为此类将不会使用它。
在 Visual Studio 中保存更改,然后返回到 Unity。
单击“脚本”文件夹中的 AzureServices 类 ,并将其拖到“层次结构面板”中的主摄像头对象。
选择主摄像头,然后从 GazeButton 对象下抓取 AzureStatusText 子对象,并将其放置在“检查器”的 AzureStatusText 引用目标字段中,以提供对 AzureServices 脚本的引用。
第 8 章 - 创建 ShapeFactory 类
下一个要创建的脚本是 ShapeFactory 类。 此类的作用是在请求时创建一个新的形状,并将创建的形状的历史记录保存在“形状历史记录列表”中。 每次创建形状时,都将在 AzureService 类中更新“形状历史记录列表”,然后将其存储在“Azure 存储”中。 当应用程序启动时,如果在“Azure 存储”中找到存储的文件,则将检索和重播“形状历史记录列表”,其中包含提供生成的形状是来自存储还是新的“3D 文本”对象。
若要创建此类,请执行以下操作:
转到之前创建的“脚本”文件夹。
右键单击文件夹并选择“创建”>“C# 脚本”。 调用脚本 ShapeFactory。
双击新的 ShapeFactory 脚本,以在 Visual Studio 中打开它。
确保 ShapeFactory 类包含以下命名空间:
using System.Collections.Generic; using UnityEngine;
将下面显示的变量添加到 ShapeFactory 类,并将 Start () 和 Awake() 函数替换为以下内容:
/// <summary> /// Provide this class Singleton-like behaviour /// </summary> [HideInInspector] public static ShapeFactory instance; /// <summary> /// Provides an Inspector exposed reference to ShapeSpawnPoint /// </summary> [SerializeField] public Transform spawnPoint; /// <summary> /// Shape History Index /// </summary> [HideInInspector] public List<int> shapeHistoryList; /// <summary> /// Shapes Enum for selecting required shape /// </summary> private enum Shapes { Cube, Sphere, Cylinder } private void Awake() { instance = this; } private void Start() { shapeHistoryList = new List<int>(); }
CreateShape () 方法基于提供的整数参数生成基元形状。 布尔参数用于指定当前创建的形状是来自存储还是新的。 在 ShapeFactory 类中,将以下代码放在前面的方法之下:
/// <summary> /// Use the Shape Enum to spawn a new Primitive object in the scene /// </summary> /// <param name="shape">Enumerator Number for Shape</param> /// <param name="storageShape">Provides whether this is new or old</param> internal void CreateShape(int shape, bool storageSpace) { Shapes primitive = (Shapes)shape; GameObject newObject = null; string shapeText = storageSpace == true ? "Storage: " : "New: "; AzureServices.instance.azureStatusText.text = string.Format("{0}{1}", shapeText, primitive.ToString()); switch (primitive) { case Shapes.Cube: newObject = GameObject.CreatePrimitive(PrimitiveType.Cube); break; case Shapes.Sphere: newObject = GameObject.CreatePrimitive(PrimitiveType.Sphere); break; case Shapes.Cylinder: newObject = GameObject.CreatePrimitive(PrimitiveType.Cylinder); break; } if (newObject != null) { newObject.transform.position = spawnPoint.position; newObject.transform.localScale = new Vector3(0.5f, 0.5f, 0.5f); newObject.AddComponent<Rigidbody>().useGravity = true; newObject.GetComponent<Renderer>().material.color = UnityEngine.Random.ColorHSV(0f, 1f, 1f, 1f, 0.5f, 1f); } }
返回到 Unity 之前,请务必在 Visual Studio 中保存所做的更改。
返回 Unity 编辑器,单击“脚本”文件夹中的 ShapeFactory 类,并将其拖到“层次结构面板”中的主摄像头对象。
选择主摄像头后,你会注意到 ShapeFactory 脚本组件缺少“生成点”引用。 若要修复此问题,请将 ShapeSpawnPoint 对象从“层次结构面板”拖动到“生成点”引用目标。
第 9 章 - 创建 Gaze 类
需要创建的最后一个脚本是 Gaze 类。
此类负责创建从主摄像头正向投影的 Raycast,以检测用户正在注视的对象。 在这种情况下,Raycast 需要识别用户是否正注视场景中的 GazeButton 对象并触发行为。
若要创建此类,请执行以下操作:
转到之前创建的“脚本”文件夹。
右键单击项目面板并选择“创建”>“C# 脚本”。 调用脚本 Gaze。
双击新的 Gaze 脚本,以在 Visual Studio 中打开它。
确保脚本顶部包含以下命名空间:
using UnityEngine;
然后将以下变量添加到 Gaze 类中:
/// <summary> /// Provides Singleton-like behavior to this class. /// </summary> public static Gaze instance; /// <summary> /// The Tag which the Gaze will use to interact with objects. Can also be set in editor. /// </summary> public string InteractibleTag = "GazeButton"; /// <summary> /// The layer which will be detected by the Gaze ('~0' equals everything). /// </summary> public LayerMask LayerMask = ~0; /// <summary> /// The Max Distance the gaze should travel, if it has not hit anything. /// </summary> public float GazeMaxDistance = 300; /// <summary> /// The size of the cursor, which will be created. /// </summary> public Vector3 CursorSize = new Vector3(0.05f, 0.05f, 0.05f); /// <summary> /// The color of the cursor - can be set in editor. /// </summary> public Color CursorColour = Color.HSVToRGB(0.0223f, 0.7922f, 1.000f); /// <summary> /// Provides when the gaze is ready to start working (based upon whether /// Azure connects successfully). /// </summary> internal bool GazeEnabled = false; /// <summary> /// The currently focused object. /// </summary> internal GameObject FocusedObject { get; private set; } /// <summary> /// The object which was last focused on. /// </summary> internal GameObject _oldFocusedObject { get; private set; } /// <summary> /// The info taken from the last hit. /// </summary> internal RaycastHit HitInfo { get; private set; } /// <summary> /// The cursor object. /// </summary> internal GameObject Cursor { get; private set; } /// <summary> /// Provides whether the raycast has hit something. /// </summary> internal bool Hit { get; private set; } /// <summary> /// This will store the position which the ray last hit. /// </summary> internal Vector3 Position { get; private set; } /// <summary> /// This will store the normal, of the ray from its last hit. /// </summary> internal Vector3 Normal { get; private set; } /// <summary> /// The start point of the gaze ray cast. /// </summary> private Vector3 _gazeOrigin; /// <summary> /// The direction in which the gaze should be. /// </summary> private Vector3 _gazeDirection;
重要
其中一些变量能够在编辑器中进行“编辑”。
现在需要添加 Awake() 和 Start() 方法的代码。
/// <summary> /// The method used after initialization of the scene, though before Start(). /// </summary> private void Awake() { // Set this class to behave similar to singleton instance = this; } /// <summary> /// Start method used upon initialization. /// </summary> private void Start() { FocusedObject = null; Cursor = CreateCursor(); }
添加以下代码(它将在开始时创建光标对象),并添加 Update() 方法(将运行 Raycast 方法)以及切换 GazeEnabled 布尔值的位置:
/// <summary> /// Method to create a cursor object. /// </summary> /// <returns></returns> private GameObject CreateCursor() { GameObject newCursor = GameObject.CreatePrimitive(PrimitiveType.Sphere); newCursor.SetActive(false); // Remove the collider, so it doesn't block raycast. Destroy(newCursor.GetComponent<SphereCollider>()); newCursor.transform.localScale = CursorSize; newCursor.GetComponent<MeshRenderer>().material = new Material(Shader.Find("Diffuse")) { color = CursorColour }; newCursor.name = "Cursor"; newCursor.SetActive(true); return newCursor; } /// <summary> /// Called every frame /// </summary> private void Update() { if(GazeEnabled == true) { _gazeOrigin = Camera.main.transform.position; _gazeDirection = Camera.main.transform.forward; UpdateRaycast(); } }
接下来,添加 UpdateRaycast() 方法,该方法将投影 Raycast 并检测命中目标。
private void UpdateRaycast() { // Set the old focused gameobject. _oldFocusedObject = FocusedObject; RaycastHit hitInfo; // Initialise Raycasting. Hit = Physics.Raycast(_gazeOrigin, _gazeDirection, out hitInfo, GazeMaxDistance, LayerMask); HitInfo = hitInfo; // Check whether raycast has hit. if (Hit == true) { Position = hitInfo.point; Normal = hitInfo.normal; // Check whether the hit has a collider. if (hitInfo.collider != null) { // Set the focused object with what the user just looked at. FocusedObject = hitInfo.collider.gameObject; } else { // Object looked on is not valid, set focused gameobject to null. FocusedObject = null; } } else { // No object looked upon, set focused gameobject to null. FocusedObject = null; // Provide default position for cursor. Position = _gazeOrigin + (_gazeDirection * GazeMaxDistance); // Provide a default normal. Normal = _gazeDirection; } // Lerp the cursor to the given position, which helps to stabilize the gaze. Cursor.transform.position = Vector3.Lerp(Cursor.transform.position, Position, 0.6f); // Check whether the previous focused object is this same // object. If so, reset the focused object. if (FocusedObject != _oldFocusedObject) { ResetFocusedObject(); if (FocusedObject != null) { if (FocusedObject.CompareTag(InteractibleTag.ToString())) { // Set the Focused object to green - success! FocusedObject.GetComponent<Renderer>().material.color = Color.green; // Start the Azure Function, to provide the next shape! AzureServices.instance.CallAzureFunctionForNextShape(); } } } }
最后,添加 ResetFocusedObject() 方法,该方法将切换 GazeButton 对象的当前颜色,指示它是否正在创建新形状。
/// <summary> /// Reset the old focused object, stop the gaze timer, and send data if it /// is greater than one. /// </summary> private void ResetFocusedObject() { // Ensure the old focused object is not null. if (_oldFocusedObject != null) { if (_oldFocusedObject.CompareTag(InteractibleTag.ToString())) { // Set the old focused object to red - its original state. _oldFocusedObject.GetComponent<Renderer>().material.color = Color.red; } } }
返回到 Unity 之前,请在 Visual Studio 中“保存所做的更改”。
单击“脚本”文件夹中的 Gaze 类,并将其拖到“层次结构面板”中的主摄像头对象。
第 10 章 - 完成 AzureServices 类
其他脚本就位后,现在可以完成 AzureServices 类。 可以通过以下步骤实现:
添加名为 CreateCloudIdentityAsync() 的新方法,以设置与 Azure 通信所需的身份验证变量。
此方法还将检查是否存在以前存储的包含形状列表的文件。
如果找到该文件,它将禁用用户凝视,并根据存储在 Azure 存储文件中的形状模式触发形状创建。 用户可以看到这一点,因为“文本网格”将提供“存储”或“新建”,具体取决于形状原点。
如果未找到文件,它将启用“凝视”,使用户能够在注视场景中的 GazeButton 对象时创建形状。
/// <summary> /// Create the references necessary to log into Azure /// </summary> private async void CreateCloudIdentityAsync() { // Retrieve storage account information from connection string storageAccount = CloudStorageAccount.Parse(storageConnectionString); // Create a file client for interacting with the file service. fileClient = storageAccount.CreateCloudFileClient(); // Create a share for organizing files and directories within the storage account. share = fileClient.GetShareReference(fileShare); await share.CreateIfNotExistsAsync(); // Get a reference to the root directory of the share. CloudFileDirectory root = share.GetRootDirectoryReference(); // Create a directory under the root directory dir = root.GetDirectoryReference(storageDirectory); await dir.CreateIfNotExistsAsync(); //Check if the there is a stored text file containing the list shapeIndexCloudFile = dir.GetFileReference("TextShapeFile"); if (!await shapeIndexCloudFile.ExistsAsync()) { // File not found, enable gaze for shapes creation Gaze.instance.GazeEnabled = true; azureStatusText.text = "No Shape\nFile!"; } else { // The file has been found, disable gaze and get the list from the file Gaze.instance.GazeEnabled = false; azureStatusText.text = "Shape File\nFound!"; await ReplicateListFromAzureAsync(); } }
下一个代码片段来自 Start() 方法;其中,将调用 CreateCloudIdentityAsync() 方法。 可随意复制当前 Start() 方法,如下所示:
private void Start() { // Disable TLS cert checks only while in Unity Editor (until Unity adds support for TLS) #if UNITY_EDITOR ServicePointManager.ServerCertificateValidationCallback = delegate { return true; }; #endif // Set the Status text to loading, whilst attempting connection to Azure. azureStatusText.text = "Loading..."; //Creating the references necessary to log into Azure and check if the Storage Directory is empty CreateCloudIdentityAsync(); }
填写方法 CallAzureFunctionForNextShape() 的代码。 需使用之前创建的“Azure 函数应用”来请求形状索引。 收到新形状后,此方法将形状发送到 ShapeFactory 类,以在场景中创建新形状。 使用以下代码完成 CallAzureFunctionForNextShape() 的主体。
/// <summary> /// Call to the Azure Function App to request a Shape. /// </summary> public async void CallAzureFunctionForNextShape() { int azureRandomInt = 0; // Call Azure function HttpWebRequest webRequest = WebRequest.CreateHttp(azureFunctionEndpoint); WebResponse response = await webRequest.GetResponseAsync(); // Read response as string using (Stream stream = response.GetResponseStream()) { StreamReader reader = new StreamReader(stream); String responseString = reader.ReadToEnd(); //parse result as integer Int32.TryParse(responseString, out azureRandomInt); } //add random int from Azure to the ShapeIndexList ShapeFactory.instance.shapeHistoryList.Add(azureRandomInt); ShapeFactory.instance.CreateShape(azureRandomInt, false); //Save to Azure storage await UploadListToAzureAsync(); }
添加方法以创建字符串,操作步骤:串联形状历史记录列表中存储的整数,并将其保存到 Azure 存储文件中。
/// <summary> /// Upload the locally stored List to Azure /// </summary> private async Task UploadListToAzureAsync() { // Uploading a local file to the directory created above string listToString = string.Join(",", ShapeFactory.instance.shapeHistoryList.ToArray()); await shapeIndexCloudFile.UploadTextAsync(listToString); }
添加方法以检索存储在 Azure 存储文件下的文件中的文本,并将其反序列化到列表中。
完成此过程后,该方法会重新启用凝视,以便用户可以向场景添加更多形状。
///<summary> /// Get the List stored in Azure and use the data retrieved to replicate /// a Shape creation pattern ///</summary> private async Task ReplicateListFromAzureAsync() { string azureTextFileContent = await shapeIndexCloudFile.DownloadTextAsync(); string[] shapes = azureTextFileContent.Split(new char[] { ',' }); foreach (string shape in shapes) { int i; Int32.TryParse(shape.ToString(), out i); ShapeFactory.instance.shapeHistoryList.Add(i); ShapeFactory.instance.CreateShape(i, true); await Task.Delay(500); } Gaze.instance.GazeEnabled = true; azureStatusText.text = "Load Complete!"; }
返回到 Unity 之前,请在 Visual Studio 中“保存所做的更改”。
第 11 章 - 生成 UWP 解决方案
若要开始生成过程,请执行以下操作:
转到“文件”>“生成设置”。
单击“生成”。 Unity 将启动“文件资源管理器”窗口,你需要在其中创建并选择一个文件夹来生成应用。 现在创建该文件夹,并将其命名为“应用”。 选择“应用”文件夹,然后按“选择文件夹”。
Unity 将开始将项目生成到“应用”文件夹。
Unity 完成生成(可能需要一些时间)后,会在生成位置打开“文件资源管理器”窗口(检查任务栏,因为它可能不会始终显示在窗口上方,但会通知你增加了一个新窗口)。
第 12 章 - 部署应用程序
若要部署应用程序,请执行以下操作:
导航到在上一章中创建的“应用”文件夹。 你将看到一个包含应用名称且扩展名为“.sln”的文件,应双击该文件,以便在 Visual Studio 中打开它。
在“解决方案平台”中,选择“x86,本地计算机”。
在“解决方案配置”中,选择“调试”。
对于 Microsoft HoloLens,你可能会发现,将此选项设置为“远程计算机”会更容易,这样你就不会受限于你的计算机。 不过,还需要执行以下操作:
- 了解 HoloLens 的 IP 地址,该地址可以在“设置”>“网络和 Internet”>“Wi-Fi”>“高级选项”中找到;你应使用 IPv4 地址。
- 确保将“开发人员模式”设置为“开启”;(可在“设置”>“更新和安全”>“开发人员选项”中找到)。
转到“生成”菜单,并单击“部署解决方案”,将应用程序旁加载到计算机。
应用现在应显示在已安装的应用列表中,随时可以启动和测试!
已完成的 Azure Functions 和存储应用程序
恭喜,你生成了一个利用 Azure Functions 和 Azure 存储服务的混合现实应用。 你的应用将能够利用存储的数据,并根据这些数据提供操作。
额外练习
练习 1
创建第二个生成点并记录从中创建对象的生成点。 加载数据文件时,重播从最初创建形状的位置生成的形状。
练习 2
创建一种方法来重启应用,而不必每次都重新打开它。 “加载场景”是一个不错的起点。 执行此操作后,创建一种方法来清除“Azure 存储”中的存储列表,以便可以轻松地从应用重置它。