第 3章 基本特性操作与开发 HoloLens 2设备运行在 Windows Holographic Operating System全息操作系统上,是一种全新的硬件形态,对开发人员而言, HoloLens 2设备操作使用和软件开发均与传统桌面计算机、移动手机不相同,包括设备系统信息采集、软件安装方式、硬件运行状况监视等都非常不一样。另外, HoloLens 2设备对开发人员开放了硬件传感器数据,我们可以通过一定方式获取这些底层传感器的原始数据进行研究。本章主要介绍 HoloLens 2设备门户、获取硬件原始数据、远端渲染、全息图像和视频拍摄等基本功能的使用与相关技术特性的开发。 3.1 HoloLens 2设备门户 每个 HoloLens 2设备运行的系统都自带一个 Web服务器(Web Server),这是一个功能强大的设备门户( HoloLens Device Portal),通过这个门户,我们可以直接在 PC端通过 WiFi 或 USB 远程配置和管理设备,包括拍照、录像、第一视角显示设备画面、安装卸载 App、查看系统性能、调试和优化应用等。 3.1.1连接设备门户 设备门户在 HoloLens 2设备中默认为关闭状态,该功能属于开发者功能,正常情况下不应当在生产部署环境中开启。在需要使用该功能时,从 HoloLens 2设备的开始菜单中,依次选择 Setting → Update & Security → For developers,启用 Developers Mode(开发人员模式),然后在该面板中,将滚动条滑动到最下方,勾选并启用 Device Portal。 在开启设备门户后,可以通过两种方式连接计算机与 HoloLens 2设备。 1. 通过 USB进行连接 首先确保计算机中已安装了 Windows 10开发工具 Visual Studio 2019,将 HoloLens 2设备通过 USB连接到计算机,然后在计算机的 Web浏览器中访问 http://127.0.0.1:10080,以便打开设备门户。 HoloLens 2开发入门精要——基于 Unity和 MRTK 如果在实际操作中发现无法连接,则请检查 Visual Studio 2019安装配置中是否已勾选了 “USB设备连接性”复选框,可参见图 1-7。 2. 通过 WiFi进行连接 在通过 WiFi进行连接前,首先应当确保计算机与 HoloLens 2设备处于同一个局域网。在 HoloLens 2设备中打开开始菜单,依次选择 Setting → Network&Internet → WiFi,在该面板中, 勾选并启用 WiFi,这时设备会自动检查环境中的无线网络,在列出的可用无线网络中选择一 个网络并单击 Connect按钮进行连接,输入对应密码即可连接到该网络。 在连接到无线网络后,我们还需要获取分配给该 HoloLens 2设备的 IP地址,最简单的 方法是查看所连接的无线网络,单击该网络下方的 Advanced options(高级选项)a,查看获取 IPv4地址,假设为 192.168.1.1。也可以通过开始菜单,依次选择 Setting → Network&Internet, 然后选择 Hardware properties(硬件属性)查看 IP地址。 获取了 HoloLens 2设备的 IP地址后,在计算机 Web浏览器中访问 bhttp://192.168.1.1,便 可打开设备门户,这时,可能会出现“你的计算机不信任此网站的安全证书”之类的安全提示, 这是因为设备门户 Web服务器使用的是测试证书,可以暂时忽略该提示并选择继续。 首次访问设备门户时需要创建用户名和密码,在打开的设备门户页面中单击 Request PIN(请求 PIN码)按钮,这时 HoloLens 2设备的显示设备上会出现一个 7位 PIN码,在计算机 浏览器中输入该 PIN码,同时设置好用户名与密码,然后单击 Pair(配对)按钮就可以连接 计算机与 HoloLens 2设备门户 c了。 3.1.2功能简介 在打开设备门户后,其主界面如图 3-1所示。整个设备门户系统界面分成 3部分,上部为操作与状态栏、左侧为菜单栏、右侧为内容面板。 操作与状态栏显示 HoloLens 2设备的运行状态,包括系统运行状态、 WiFi连接状态、硬件温度、电池电量情况,还有运行反馈与帮助按钮(单击可以直接跳转到微软公司官方网站帮助页面)。除此之外,我们还可以在这里直接关闭、重启 HoloLens 2设备。 HoloLens 2 a 可能需要将滚动条滑动到面板最下方才能看到。 b 该地址由无线路由器自动分配,请以实际分配的 IP地址为准。 c 如果在后续的使用过程中忘记了用户名和密码,则可通过设备门户 https://IP/devicepair.htm页面进行重置。 设备门户是一个定制化程度很高的 Web网站系统,我们可以通过操作状态栏左侧的下拉菜单(“三”字形图标)添加工作页面、设置每个工作页面显示的内容、导出及导入工作页面布局等。 图 3-1 设备门户主界面 左侧菜单栏列出了一些常用功能,主要分为视图( Views)、性能( Performance)、系统 (System)3部分,其中视图栏下主要有主页系统信息、硬件环境感知 3D模型、程序安装与卸载、图像视频捕获相关功能;性能栏下主要包括设备系统进程、性能跟踪、硬件性能表现等功能;系统栏下主要包括硬件文件管理、日志管理、网络管理、研究模式等。 右侧内容面板主要用于展示某特定功能的详细情况,根据功能的不同该面板布局与内容也会不同。 HoloLens 2设备门户功能很强大,下面我们只对经常用的主要功能进行详细阐述,性能部分内容可参见本书第 14章,其他功能读者可以自行查看了解。 1. 应用管理 使用设备门户可以直接管理 HoloLens 2设备中运行的应用,包括查看、安装、卸载应用。在左侧菜单栏中依次选择 Views → Apps将打开应用管理界面,如图 3-2所示。应用管理主要由 3部分功能组成:Deploy Apps(安装应用)、Installed Apps(已安装应用)、Running Apps(正在运行应用),当然,内容面板显示的功能可以自行定制。 在安装应用功能区,我们可以直接将计算机本地的 .appxbundle、.appx程序包、网络 URI定位的 .appxbundle、.appx程序包安装到 HoloLens 2设备上,还可以安装应用 .cer证书。在已安装应用功能区,下拉列表列出了当前 HoloLens 2设备中已安装的所有应用,我们可以直接启动某个应用,也可以在这里卸载并删除某个应用。正在运行应用功能区列出了当前 HoloLens 2设备系统正在运行的应用程序,可以在这里直接关闭并终止某个应用的运行。 HoloLens 2开发入门精要——基于 Unity和 MRTK 图 3-2 设备门户应用管理界面 2. MR照片与视频捕获 在 HoloLens 2设备的实际使用中,我们有时需要获取系统运行时的 MR全息图像或者视频,或者希望在设备外部以第一视角观察 MR应用的运行情况,这时最简单的方式就是使用图像和视频捕获功能。在左侧菜单栏中依次选择 Views → Mixed Reality Capture(混合现实捕获),这将打开图像与视频捕获界面,如图 3-3所示。 图 3-3 设备门户 MR照片与视频捕获界面 在打开的内容面板混合现实捕获( Mixed Reality Capture)功能区,我们可以以第一人称 视角观察应用运行( Live Preview)、拍摄 MR照片( Take Photo)、录制视频( Record),在捕获视频时,还可以指定捕获视频的各项参数,例如:开启 Holograms(全息),用于捕获虚拟渲染的全息内容;启用 PV Camera(主摄像头),从摄像头捕获视频流;启用 Mic Audio(话筒声频):捕获话筒阵列采集的数据;启用 App Audio(应用声频),捕获当前运行应用中播放的声频数据。 在以第一人称视角捕获视频时,我们还可以通过 Live Preview Quality(直播预览质量)下拉菜单,选择实时预览视频的分辨率、帧率和码流。 图像与视频( Videos and Photos)功能区列出了所有已拍摄的照片和录制的视频,我们可以直接预览这些照片和视频,也可以将其下载到计算机本地磁盘中,或者删除它们。 3. 系统性能 通过设备门户可以直观地查看 HoloLens 2设备各硬件性能的消耗情况,在左侧菜单栏中依次选择 Performance → System Performance(系统性能),这将打开系统性能界面,如图 3-4所示。 图 3-4 设备门户系统性能界面 从图 3-4可以看到,在系统性能内容面板中,分为若干功能区: CPU、I/O、Memory、 Network,分别显示实时的 CPU、I/O吞吐量、内存、网络性能情况; Power功能区显示电量消耗情况,其中 SoC为片上系统的瞬时耗电量,System为 HoloLens 2设备系统的瞬时耗电量; GPU与 Frame Rate功能区显示 GPU利用率和系统刷新帧率; Memory功能区显示硬件内存使用情况,包括总计、已用、已提交、已分页和未分页等数据。 HoloLens 2开发入门精要——基于 Unity和 MRTK 4. 文件浏览器 文件浏览器( File Explorer)用于管理 HoloLens 2设备中的文件和在计算机与 HoloLens 2设备之间传输文件,在左侧菜单栏中依次选择 System → File Explorer,打开文件浏览器界面,如图 3-5所示。 图 3-5 设备门户文件浏览器界面 文件浏览器内容面板主要包括:目录内容列表、路径导航栏、上传文件功能区。目录内容列表以目录树的形式列出 HoloLens 2设备中的所有文件,在列表中,我们可以选择特定文件将其下载到计算机本地,也可以修改文件名或者直接删除文件。 路径导航栏会显示当前目录内容列表所在的路径,可以非常方便地在不同路径之间跳转。选择好某个目录,我们可以将计算机本地文件上传到 HoloLens 2设备的该目录下。 除以上介绍的功能,利用设备门户还可以捕获应用运行日志( Logging)、崩溃记录( App Crash Dumps)、管理设备运行的应用进程等,使用非常简单便捷。 设备门户事实上是一个运行在 HoloLens 2设备上的 Web服务器,而且所有的内容都构建在 REST API基础之上,因此,我们也可以脱离其提供的可视化界面而使用编程的方式访问数据、控制设备,如下载 HoloLens 2设备文件、安装及卸载应用等,具体可查阅官方文档。 3.2 研究模式 出于安全因素的考虑,在使用 HoloLens 2设备时,开发者并不能直接获取设备硬件传感器的原始数据,但有时我们可能需要这些传感器的数据,如深度传感器数据、加速度计数据、陀螺仪数据等,鉴于此,微软公司提供了一个 HoloLens 2研究模式(Research Mode),很显然,微软公司希望开发者只在研究或者验证功能的情况下使用该模式,而不是在大规模部署时开启该模式。 HoloLens 1设备中已引入了研究模式,可以访问设备上的关键传感器, HoloLens 2设备则在第 1代的基础上开放了更多的传感器数据,具体如表 3-1所示。 表 3-1 HoloLens 2开放的传感器 硬件传感器名称 描  述 RGB相机 HoloLens 2使用了 5颗 RGB相机,其中 4颗用于跟踪使用者头部和构建环境地图 深度相机 HoloLens 2使用 ToF(Time of Flight,飞行时间)深度相机检测手势和场景深度,由于检测目标的不同,为节约资源,使用了两种频率模式:高频模式( 45FPS)用于手势检测,检测深度为 0.15~ 0.95m;低频模式( 1~ 5FPS)用于环境深度检测,检测深度为 0.95~ 3.52m 加速度计 测量沿 X轴、Y轴、Z轴和重力方向的线性加速度 陀螺仪 测量旋转角度 磁力仪 估计设备绝对方位 需要注意的是,在开启研究模式后, HoloLens 2设备会消耗更多的资源和电能,即使这些特性没有被使用,另外,不正确地使用传感器数据可能会给应用程序和整个系统带来不稳定甚至引发安全风险。微软公司也不保证研究模式会在以后的硬件升级或者系统版本中肯定被支持,因此,研究模式更适合科研或者计算机视觉前沿探索而不适合商业部署。 HoloLens 2设备默认不开启研究模式,在需要时,可以通过设备门户开启。在设备门户中,依次选择 System → Research Mode,打开研究模式内容面板,如图 3-6所示,在该面板中,勾选 Allow access to sensor streams(允许获取传感器流)属性前的复选框,然后重启 HoloLens 2设备即可开启研究模式 a。 研究模式为直接获取设备传感器的原始数据打开了通道,通常使用这些传感器数据是为了进行计算机视觉、 SLAM、运动跟踪等方面的研究,由于这些领域的专业性比较强,鉴于本书的目的,我们不展开详细讨论 b。 a 开启 HoloLens 2设备的研究模式需要将固件升级到 19041.1356以上,否则需要申请加入内部预览(Insider Preview)项目。 b 微软公司官方提供了名为 SensorVisualization、HoloLens2ForCV的工程案例可供参考。 HoloLens 2开发入门精要——基于 Unity和 MRTK 图 3-6 设备门户中开启研究模式界面 3.3 图像与视频捕获 截屏是移动手机用户经常使用的一项功能,也是一项特别方便用户保存、分享屏幕信息的方式。移动设备(包括使用 iOS和 Android操作系统的设备)都有方便且高效的截屏快捷键。在 HoloLens 2设备的 MR应用中,我们也经常需要获取摄像机、全息图像数据,如获取摄像头采集的原始图像或者实时全息图像,但由于 HoloLens 2设备采用的并非传统矩形框显示设备,传统截屏操作方法并不适合 MR全息图像的捕获。考虑到 HoloLens 2用户的实际需求,微软公司实际上提供了多种捕获设备摄像头图像数据和全息图像数据的方式。 3.3.1在设备中直接操作 在 HoloLens 2设备启动(或者使用手势呼出开始菜单)后,可以看到开始菜单下部列有 Camera和 Video两个按钮(或者是相机与录像机图标),其中 Camera按钮用于拍摄当前场景的全息图像,而 Video按钮则用于录制全息视频。使用这种方式拍摄全息照片或者录制视频与手机移动设备截屏和录屏完全一致,非常简单易用,但不能调整拍摄照片的尺寸,也不能设置视频录制分辨率。所有拍摄的照片和视频都存储在 HoloLens 2本地存储器上,可以通过系统自带的 Photo应用程序进行查看和回放。 3.3.2在设备门户中操作 使用设备门户也可以非常方便捕获全息图像和视频,在图像与视频捕获功能区,可以精 确地设置捕获图像或视频的质量和分辨率(可参见图 3-3),Photo Resolution属性下拉菜单中列出了所有可用的分辨率格式, Video Resolution属性下拉菜单列出了所有可用的分辨率及帧率。所有捕获的全息照片或者录制的视频都会在 Video and Photos功能区列出,可以查看、回放、删除,也可以下载到计算机的本地存储中。 当然,使用这种方式需要开启 HoloLens 2设备的开发者模式,不适合大规模产品部署。 3.3.3代码操作 使用 3.3.1节和 3.3.2节提供的方法进行图像和视频捕获,方便快捷,但也有很多局限性,特别是对于需要后续处理的图像和视频捕获,如通过网络传输、进行计算机视觉处理等场合则显得力不从心。对开发人员而言,我们更希望能完全控制图像和视频的捕获时机、质量、帧率等以适应更高的应用需求。本节我们将从实际应用出发,探讨在 HoloLens 2设备中以代码的方式进行图像和视频捕获的各类方法。 在 HoloLens 2设备中使用脚本代码捕获图像或者视频时会独占设备主摄像头( Photo Video Camera),这意味着拍照与录像不能同时进行,而且相同的操作也相互排斥(如图像检测识别、二维码扫描等使用主摄像头的任务与图像视频捕获会相互排斥),因此,在进行图像或者视频捕获之前应当先检测当前主摄像头的可用性。 由于图像或者视频捕获需要使用设备主摄像头,视频捕获还有可能会使用设备话筒阵列,因此,首先必须确保主摄像头和话筒阵列的特性可用,在 Unity菜单中,依次选择 Edit → Project Settings → Player,选择 Universal Windows Platform Settings(UWP设置)选项卡,并依次选择 Publishing Settings → Capabilities功能图 3-7 在工程中勾选 WebCam和 Microphone 设置区,勾选 WebCam和 Microphone两个复选框,如图 3-7所示。两个复选框 为方便开发人员进行图像和视频捕获操作, MRTK提供了专门的 API,所有与图像和视频捕获相关的操作都位于 UnityEngine.Windows.WebCam命名空间中,以图像捕获为例,典型的使用流程如下: (1)创建一个 PhotoCapture对象。 (2)创建一个 CameraParameters参数结构体,该结构体用于定义捕获图像的分辨率、图像格式、全息图像透明度等。 (3)使用 PhotoCapture对象的 StartPhotoModeAsync()方法开启图像捕获模式。(4)捕获图像并进行相关个性化操作。 (5)使用 PhotoCapture对象的 StopPhotoModeAsync()方法关闭图像捕获模式,清理并释放资源。 1. 捕获图像并存储到磁盘 HoloLens 2开发入门精要——基于 Unity和 MRTK 捕获图像并存储到磁盘是最典型的应用场景,遵照图像捕获流程,典型的使用代码如下: //第 3章 /3-1.cs private PhotoCapture photoCaptureObject = null; private Texture2D targetTexture = null; public void TakePhoto() { var cameraMode = WebCam.Mode; if (cameraMode == WebCamMode.None) { PhotoCapture.CreateAsync(true, OnPhotoCaptureCreated); } else Debug.LogError("当前相机不可用! "); } void OnPhotoCaptureCreated(PhotoCapture captureObject) { photoCaptureObject = captureObject; Resolution cameraResolution = PhotoCapture.SupportedResolutions. OrderByDescending((res) => res.width * res.height).First(); CameraParameters cameraParm = new CameraParameters(); cameraParm.hologramOpacity = 1.0f; cameraParm.cameraResolutionWidth = cameraResolution.width; cameraParm.cameraResolutionHeight = cameraResolution.height; cameraParm.pixelFormat = CapturePixelFormat.BGRA32; captureObject.StartPhotoModeAsync(cameraParm, OnPhotoModeStarted); } private void OnPhotoModeStarted(PhotoCapture.PhotoCaptureResult result) { if (result.success) { string .lename = string.Format(@"Image{0}_0.jpg", Time.time); string .lePath = System.IO.Path.Combine(Application.persistentDataPath, .lename); photoCaptureObject.TakePhotoAsync(.lePath, PhotoCaptureFileOutputFormat. JPG, OnCapturedPhotoToDisk); } else { Debug.LogError("无法启动相机! "); } } void OnCapturedPhotoToDisk(PhotoCapture.PhotoCaptureResult result) { if (result.success) { //保存照片到磁盘成功 photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode); } else { Debug.Log("保存照片失败! "); } } void OnStoppedPhotoMode(PhotoCapture.PhotoCaptureResult result) { photoCaptureObject.Dispose(); photoCaptureObject = null; } TakePhoto()方法是图像捕获的总入口,可以使用按钮事件或者定时方式调用,在该方法中,我们首先检查主摄像头设备是否可用,利用 WebCam.Mode属性可以获取当前主摄像头的使用状态,该属性返回一个 WebCamMode类型枚举,其枚举值如表 3-2所示。 表 3-2 WebCamMode枚举值 枚举值 描  述 None 当前主摄像头可用 PhotoMode 当前主摄像头正处于图像捕获状态 VideoMode 当前主摄像头正处于视频捕获状态 在确保主摄像头可用时,调用 CreateAsync()方法创建 PhotoCapture对象,该方法有两个参数:第 1个参数为布尔值,用于指定是否捕获全息影像,其值为 true时表示捕获,其值为 false时则表示不捕获全息图像(只捕获实景原始图像),当该值被设置为 true值时, OnPhotoCaptureCreated()方法中的 CameraParameters.hologramOpacity属性需要设置一个大于 0的值才能在捕获的图像中显示全息图像 a;第 2个参数为 PhotoCapture对象创建成功时的回调函数。 在 OnPhotoCaptureCreated()方法中, PhotoCapture.SupportedResolutions属性返回当前主摄像头支持的所有静态图像分辨率类型,我们可以选择合适的分辨率格式,这里直接选取了受主摄像头支持的第 1个分辨率格式,HoloLens 2设备主摄像头支持的所有分辨率如表 3-3所示。 表 3-3 HoloLens 2设备支持的静态图像捕获分辨率 序 号 分辨率 / px 1 3904×2196 2 1952×1100 3 1920×1080 4 1280×720 a hologramOpacity属性取值范围为 [0,1],0为全透明,1为完全不透明。 HoloLens 2开发入门精要——基于 Unity和 MRTK 在 OnPhotoModeStarted()方法中,我们使用 JPG图像编码格式将捕获的图像直接存储到 Application.persistentDataPath指定的磁盘路径中,该路径位于应用程序安装路径下的 LocalState目录中 a,可以通过设备门户查看这些捕获的图像文件或者使用脚本代码读取这些文件。 2. 捕获图像到 Texture2D 在将捕获的图像存储到磁盘的方式中,无法直接获取捕获的图像数据(可以重新从磁盘中读取),很多时候我们可能需要对捕获的图像进行后续处理,如裁剪、通过网络传输等,这时可以直接将图像捕获到内存中更方便后续操作,使用代码如下: //第 3章 /3-2.cs private PhotoCapture photoCaptureObject = null; private Texture2D targetTexture = null; public void TakePhoto() { var cameraMode = WebCam.Mode; if (cameraMode == WebCamMode.None) { PhotoCapture.CreateAsync(true, OnPhotoCaptureCreated); } else Debug.LogError("当前相机不可用! "); } void OnPhotoCaptureCreated(PhotoCapture captureObject) { photoCaptureObject = captureObject; Resolution cameraResolution = PhotoCapture.SupportedResolutions. OrderByDescending((res) => res.width * res.height).First(); targetTexture = new Texture2D(cameraResolution.width, cameraResolution. height); CameraParameters cameraParm = new CameraParameters(); cameraParm.hologramOpacity = 1.0f; cameraParm.cameraResolutionWidth = cameraResolution.width; cameraParm.cameraResolutionHeight = cameraResolution.height; cameraParm.pixelFormat = CapturePixelFormat.BGRA32; captureObject.StartPhotoModeAsync(cameraParm, OnPhotoModeStarted); } private void OnPhotoModeStarted(PhotoCapture.PhotoCaptureResult result) { if (result.success) a 存储路径如: User Folders\LocalAppData\[AppName]\LocalState\,其中 [AppName]为应用程序包名,看起来类似于 Template3D_1.0.0.0_ARM64__am1avaz9ccc48。 { photoCaptureObject.TakePhotoAsync(OnCapturedPhotoToMemory); } else { Debug.LogError("无法开启拍照模式 !"); } } void OnCapturedPhotoToMemory(PhotoCapture.PhotoCaptureResult result, PhotoCaptureFrame photoCaptureFrame) { if (result.success) { Resolution cameraResolution = PhotoCapture.SupportedResolutions. OrderByDescending((res) => res.width * res.height).First(); photoCaptureFrame.UploadImageDataToTexture(targetTexture); //应用 Texture2D string .lename = string.Format(@"Image{0}_1.jpg", Time.time); string .lePath = System.IO.Path.Combine(Application.persistentDataPath, .lename); StartCoroutine(saveTexture2DtoFile(targetTexture, .lePath)); } photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode); } private IEnumerator saveTexture2DtoFile(Texture2D texture, string path) { //等待渲染线程结束 yield return new WaitForEndOfFrame(); Byte[] textureData = texture.EncodeToJPG(); System.IO.File.WriteAllBytes(path, textureData); } void OnStoppedPhotoMode(PhotoCapture.PhotoCaptureResult result) { photoCaptureObject.Dispose(); photoCaptureObject = null; } 可以看到,将图像捕获到内存中与将图像捕获并保存到磁盘中的操作非常相似,只是在 OnCapturedPhotoToMemory()方法中,使用 photoCaptureFrame.UploadImageDataToTexture()方法将图像数据保存到 Texture2D类型对象中,在获取图像数据后,我们就可以进行对应的后续操作了(示例中我们只是将其存储到磁盘)。 3. 捕获设备原始图像数据 利用以上两种方式捕获的是混合现实场景图像数据,这些图像数据经过 MRTK处理,在进行计算机视觉处理时,我们更希望能直接从主摄像头采集硬件的原始图像数据,一方面这 HoloLens 2开发入门精要——基于 Unity和 MRTK 些原始数据未经过加工处理,另一方面可以节省设备资源。下面我们以基本的边缘检测算法为例,演示从设备中采集原始图像数据进行处理,代码如下(为节约篇幅略去了相同的代码): //第 3章 /3-3.cs void OnPhotoCaptureCreated(PhotoCapture captureObject) { photoCaptureObject = captureObject; Resolution cameraResolution = PhotoCapture.SupportedResolutions. OrderByDescending((res) => res.width * res.height).First(); targetTexture = new Texture2D(cameraResolution.width, cameraResolution. height,TextureFormat.R8,false); CameraParameters cameraParm = new CameraParameters(); cameraParm.hologramOpacity = 0.0f; cameraParm.cameraResolutionWidth = cameraResolution.width; cameraParm.cameraResolutionHeight = cameraResolution.height; cameraParm.pixelFormat = CapturePixelFormat.BGRA32; captureObject.StartPhotoModeAsync(cameraParm, OnPhotoModeStarted); } private void OnPhotoModeStarted(PhotoCapture.PhotoCaptureResult result) { if (result.success) { photoCaptureObject.TakePhotoAsync(OnCapturedPhotoToMemory); } else { Debug.LogError("无法开启拍照模式 !"); } } void OnCapturedPhotoToMemory(PhotoCapture.PhotoCaptureResult result, PhotoCaptureFrame photoCaptureFrame) { if (result.success) { List imageBu.erList = new List(); photoCaptureFrame.CopyRawImageDataIntoBu.er(imageBu.erList); Byte[] imageGray = new Byte[targetTexture.width * targetTexture.height]; int imageGrayIndex = 0; int stride = 4; List imageFliped = FlipVertical(imageBu.erList, targetTexture. width, targetTexture.height, 4); for (int i = 0; i< imageFliped.Count - 1; i += stride) { //将图像从 BGRA32彩色格式转换成灰度格式 imageGray[imageGrayIndex++] = (Byte)(((int)((int)(imageFliped[i + 3]) * 0.299 + (int)(imageFliped[i + 2]) * 0.587 + (int)(imageFliped[i + 1]) * 0.114)) & 0xFF); } imageFliped.Clear(); imageBu.erList.Clear(); Byte[] .nalImage = new Byte[targetTexture.width * targetTexture.height]; Sobel(.nalImage, imageGray, targetTexture.width, targetTexture.height); targetTexture.LoadRawTextureData(.nalImage); targetTexture.Apply(); imageGray = null; .nalImage = null; //应用 Texture2D string .lename = string.Format(@"Image{0}_2.jpg", Time.time); string .lePath = System.IO.Path.Combine(Application.persistentDataPath, .lename); StartCoroutine(saveTexture2DtoFile(targetTexture, .lePath)); } photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode); } //垂直翻转图像 private List FlipVertical(List src, int width, int height, int stride) { Byte[] dst = new Byte[src.Count]; for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { int invY = (height - 1) - y; int pxel = (y * width + x) * stride; int invPxel = (invY * width + x) * stride; for (int i = 0; i < stride; ++i) { dst[invPxel + i] = src[pxel + i]; } } } return new List(dst); } //Sobel算子边缘检测 private static void Sobel(Byte[] outputImage, Byte[] mImageBu.er, int width, int height) { //边缘检测的阈值 int threshold = 128 * 128; for (int j = 1; j < height - 1; j++) { for (int i = 1; i < width - 1; i++) HoloLens 2开发入门精要——基于 Unity和 MRTK { //将处理中心移动到指定位置 int o.set = (j * width) + i; //获取9个采样点的像素值 int a00 = mImageBu.er[o.set - width - 1]; int a01 = mImageBu.er[o.set - width]; int a02 = mImageBu.er[o.set - width + 1]; int a10 = mImageBu.er[o.set - 1]; int a12 = mImageBu.er[o.set + 1]; int a20 = mImageBu.er[o.set + width - 1]; int a21 = mImageBu.er[o.set + width]; int a22 = mImageBu.er[o.set + width + 1]; int xSum = -a00 - (2 * a10) - a20 + a02 + (2 * a12) + a22; int ySum = a00 + (2 * a01) + a02 - a20 - (2 * a21) - a22; if ((xSum * xSum) + (ySum * ySum) > threshold) { outputImage[(j * width) + i] = 0xFF; //是边缘 } else { outputImage[(j * width) + i] = 0x00; //不是边缘 } } } } 因为是直接采集设备硬件图像数据,因此在使用 CreateAsync()方法创建 PhotoCapture对象时将第 1个参数的值设置为 false,并且由于边缘检测处理完成后返回灰度图像,创建 Texture2D对象时使用了 TextureFormat.R8格式。 在代码 3-3.cs中,使用 photoCaptureFrame.CopyRawImageDataIntoBu.er()方法直接从设备硬件中采集原始的图像数据,由于原始图像数据在垂直方向上与观察方向相反,我们使用 FlipVertical()方法垂直翻转图像,然后将图像从 BGRA32彩色格式转换成灰度,最后使用 Sobel算子进行边缘检测。 Sobel算子是一种常见的边缘检测卷积核,该算子的计算速度相比其他算子慢,但其较大的卷积核在很大程度上平滑了输入图像,使算子对噪声的敏感性降低,同时也对Gx Gy像素位置的影响做了加权,可以降低边缘模糊程度,因此图 3-8 Sobel算子效果更好。Sobel算子是一个离散的一阶差分算子,用来计算图像亮度函数的一阶梯度近似值,该算子如图 3-8所示。在图像的任何一点使用此算子,将会产生该点对应的梯度矢量或法矢量。 -1 0 +1 -2 0 +2 -1 0 +1 +1 +2 +1 0 0 0 -1 -2 -1 Sobel算子包含两组 3×3的矩阵,分别用于横向和纵向计算,将其与图像做平面卷积,即可分别得出横向和纵向的亮度差分近似值。如果以 A代表原始图像, Gx及 Gy分别代表经横向和纵向边缘检测的图像灰度值,则其公式如图 3-9所示。 -1 0 +1 +1 +2 +1Gx = ×A -1 0 +1 ×A 和 Gy = -2 0 +2 00 0 -1 -2 -1 图 3-9 Sobel算子对图像进行处理的计算公式 具体计算公式如下: Gx =(-1)× f(x-1, y-1)+ 0×f(x, y-1)+1×f(x-1, y-1)+ (-2)× f(x-1, y)+ 0×f(x, y)+ 2×f(x+1, y)+ (-1)× f(x-1, y+1)+ 0×f(x, y +1)+1×f(x+1, y+1)(3-1) =[f(x+1, y-1)+2×f(x+1, y)+f(x+1, y+1)]-[f(x-1, y-1)+ 2×f(x-1, y)+f(x-1, y+1)] Gy = 1×f(x-1, y-1)+ 2×f(x, y-1)+1×f(x+1, y-1)+ 0×f(x-1, y)+ 0×f(x, y)+ 0×f(x+1, y)+(-1)× f(x-1, y+1)+(-2)× f(x, y +1)+(-1)× f(x+1, y+1)(3-2) =[f(x-1, y-1)+2×f(x, y-1)+f(x+1, y-1)]-[f(x-1, y+1)+ 2×f(x, y+1)+f(x+1, y+1)] 其中 f(a, b)表示原始图像(a, b)点的灰度值,处理后图像每像素梯度值的大小由横向和纵向灰度值平方和开根号决定,公式如下: G = Gx 2+Gy 2 通常,为了提高效率也可以使用不开平方的近似值公式: G = |Gx| + |Gy| (3-3)如果梯度值 G大于某一阈值则认为该点( x, y)为边缘点。 Sobel算子也根据像素上、下、左、右相邻点灰度加权差在边缘处达到极值这一原理来检测边缘,对噪声具有平滑作用,能提供较为精确的边缘方向信息,但边缘定位精度不够高,当对精度要求不是很高时,是一种较为常用的边缘检测方法。在代码 3-3.cs中,我们最后将边缘检测结果使用 JPG图像编码格式存储到磁盘,如果直接打开可以看到蓝绿色的边缘检测图像,这是因为虽然我们将黑白的边缘检测结果图像数据存储到 Texture2D对象时是灰度图,但在最后编码成 JPG图像格式时只在 R通道复制了灰度数据,而 G和 B通道默认填充了白色(255),使最终的图像成为蓝绿色(可以使用 Photoshop之类的图像处理软件查看各通道的图像数据)。 4. 视频捕获 在 HoloLens 2设备中捕获视频的流程与捕获图像的流程极为相似,典型的使用流程如下:(1)创建一个 VideoCapture对象。(2)创建一个 CameraParameters参数结构体,该结构体用于定义捕获视频的分辨率、视 HoloLens 2开发入门精要——基于 Unity和 MRTK 频帧率(fps)、全息图像透明度等。 (3)使用 VideoCapture对象的 StartVideoModeAsync()方法开启视频捕获模式。 (4)使用 VideoCapture对象的 StartRecordingAsync()方法开始录制视频。 (5)使用 VideoCapture对象的 StopRecordingAsync()方法结束录制视频。 (6)使用 VideoCapture对象的 StopVideoModeAsync方法关闭视频录制模式,清理并释放资源。 但与图像捕获不同的是,在录制视频时必须设置视频帧率( fps)属性,而且录制的视频只能使用 .mp4格式并直接存储到磁盘中。典型的使用代码如下: //第 3章 /3-4.cs private VideoCapture videoCaptureObject = null; public void StartRecordingVideo() { var cameraMode = WebCam.Mode; if (cameraMode == WebCamMode.None) { VideoCapture.CreateAsync(true, OnVideoCaptureCreated); } else Debug.LogError("当前相机不可用! "); } void OnVideoCaptureCreated(VideoCapture videoCapture) { if (videoCapture != null) { videoCaptureObject = videoCapture; Resolution cameraResolution = VideoCapture.SupportedResolutions. OrderByDescending((res) => res.width * res.height).First(); .oat cameraFramerate = VideoCapture.GetSupportedFrameRatesForResolution (cameraResolution).OrderByDescending((fps) => fps).First(); CameraParameters cameraParameters = new CameraParameters(); cameraParameters.hologramOpacity = 1.0f; cameraParameters.frameRate = cameraFramerate; cameraParameters.cameraResolutionWidth = cameraResolution.width; cameraParameters.cameraResolutionHeight = cameraResolution.height; cameraParameters.pixelFormat = CapturePixelFormat.BGRA32; videoCaptureObject.StartVideoModeAsync(cameraParameters,VideoCapture. AudioState.MicAudio, OnStartedVideoCaptureMode); } else { Debug.LogError("无法创建视频对象! "); } } void OnStartedVideoCaptureMode(VideoCapture.VideoCaptureResult result) { if (result.success) { string .lename = string.Format("Video{0}.mp4", Time.time); string .lepath = System.IO.Path.Combine(Application.persistentDataPath, .lename); videoCaptureObject.StartRecordingAsync(.lepath, OnStartedRecordingVideo); } } void OnStartedRecordingVideo(VideoCapture.VideoCaptureResult result) { //更新 UI、允许停止录制、定时录制等操作 Debug.Log("视频录制开始! "); } //The user has indicated to stop recording public void StopRecordingVideo() { videoCaptureObject.StopRecordingAsync(OnStoppedRecordingVideo); } void OnStoppedRecordingVideo(VideoCapture.VideoCaptureResult result) { Debug.Log("停止视频录制! "); videoCaptureObject.StopVideoModeAsync(OnStoppedVideoCaptureMode); } void OnStoppedVideoCaptureMode(VideoCapture.VideoCaptureResult result) { videoCaptureObject.Dispose(); videoCaptureObject = null; } 在 OnVideoCaptureCreated()方法中, VideoCapture.SupportedResolutions属性返回当前主摄像头支持的所有视频录制分辨率类型,我们可以选择合适的分辨率格式,这里直接选取了受主摄像头支持的第 1个分辨率格式, HoloLens 2设备主摄像头支持的所有视频录制分辨率格式如表 3-4所示,HoloLens 2设备支持以 5、15、30、60共 4种帧率录制视频,但具体的分辨率所支持的帧率有所不同,可使用 VideoCapture.GetSupportedFrameRatesForResolution()方法获取某种分辨率所支持的帧率。 HoloLens 2开发入门精要——基于 Unity和 MRTK 表 3-4 HoloLens 2设备支持的视频录制分辨率 序 号 分辨率 / px 序号 分辨率 / px 1 2272×1278 8 1280×720 2 896×504 9 1128×636 3 1952×1100 10 960×540 4 1504×846 11 760×428 5 1952×1100 12 640×360 6 1504×846 13 500×282 7 1920×1080 14 424×240 代码 3-4.cs其余部分操作与图像捕获基本一致,录制的视频文件也同样放置在应用程序的安装路径下,可通过设备门户查看,不再赘述。 由于 HoloLens 2设备主摄像头( Photo Video Camera)安装的位置与使用者眼睛的位置并不在同一个地方,默认在渲染场景时 HoloLens 2设备会从使用者眼睛的视角进行渲染,这样可以提供给使用者虚实贴合的最佳体验,但在使用本节方法捕获图像或者视频时,则会从主摄像头视角进行捕获,这样就导致捕获的图像与使用者实际看到的图像存在细微差异 a。为纠正这种差异,我们可以设置配置文件下 Camera子配置文件中的 Camera Settings Providers(相机设置提供者)子配置文件,勾选 Render from PV Camera(Align holograms)b功能属性,如图 3-10所示,从主摄像头视角进行场景渲染,从而保证使用者看到的场景与捕获的图像一致。 图 3-10 使用主摄像头视角渲染场景 a 这种差异在录制手势操作视频时会表现得很明显,能清楚地看到手势与所操作的虚拟对象并未在空间位置上吻合。 b 从 PV相机渲染(对齐全息图像)。 3.4 全息远端呈现 HoloLens 2设备是一台移动的可穿戴智能设备,其本身配备的硬件设备可以满足一般 MR应用运行的需求,但在追求高质量渲染的场合,如复杂工业 CAD模型展示、高精度模型渲染等,这种情况下 HoloLens 2设备自身有限的计算处理能力就不足以实时完成渲染需求。另外,虽然在 Unity中使用 MRTK进行应用开发时有非常方便的各类输入模拟器可以加速开发迭代过程,但 MR应用最终还是需要部署到真机设备上进行各类测试,而进行真机部署测试是一项耗时且费力的工作,会严重影响应用的开发速度。 为解决这两个问题, MRTK设计了全息远端呈现( Holographic Remoting)工具,利用该工具,我们可以利用 PC端强大的计算处理能力进行全息渲染,然后将渲染完成的全息影像以流的形式输出到 HoloLens 2设备端,从而极大地降低了 HoloLens 2设备端的渲染压力。我们还可以在开发阶段直接在 Unity编辑器中连接 HoloLens 2设备,测试 MR应用在真机设备上的真实表现,这样便可以大大加速应用的开发测试速度。 使用全息远端呈现功能首先需要通过 USB、WiFi连接 PC与 HoloLens 2设备,为确保数据传输速率,最好选择高带宽的路由设备,并且保证 PC与 HoloLens 2设备处在同一 WiFi网络下。除此之外, HoloLens 2设备上还需要安装 Remoting Player软件,该软件可以通过设备中自带的 Microsoft Store应用商店下载并安装。 3.4.1在 Unity编辑器中使用全息远端呈现 在 Unity编辑器中使用全息远端呈现功能可以极大地加快 MR应用开发测试,但对于不同的 Unity版本 a,由于架构不同,使用全息远端呈现功能也略有差异,我们分两种情况进行阐述: (1)对于本书使用的 Unity 2019.4 LTS或者之前的版本,在导入 MRTK相关插件包之后,可以通过 Unity菜单 Window → XR → Holographic Emulation打开全息仿真窗口。 (2)对于 Unity 2020及更高版本或者通过 Windows XR Plugin使用 MRTK的方式,可以通过 Unity菜单 Window → XR → Windows XR Plugin Remoting打开全息仿真窗口。打开的界面分别如图 3-11(a)和图 3-11(b)所示。 图 3-11 Holographic Emulation和 Windows XR Plugin Remoting界面 a Unity 2020、Unity 2019版本对原渲染管线进行了比较大幅度的调整,各版本使用并不完全相同。 HoloLens 2开发入门精要——基于 Unity和 MRTK 然后在 HoloLens 2设备上启动 Remoting Player应用程序,该应用程序启动时会显示 HoloLens 2设备当前的 IP地址,将该 IP地址填入全息仿真窗口中的Remote Machine(远端设备) 属性栏中,单击 Connect按钮开始连接,如果连接成功则会显示 Connected字样,如果没有连 接成功,需检查 USB连接线或者 WiFi网络情况。 在成功连接后,单击 Unity编辑器的 Play按钮进入运行状态,这时就可以在 Unity编辑 器中查看 HoloLens 2设备中运行应用的情况了。 在 HoloLens 2设备上使用全息远端呈现时,支持手部关节检测和眼动跟踪,但在使用 Holographic Emulation(全息模拟)方式时需要安装 DotNetWinRT插件 a。在安装完 DotNetWinRT插件后,还需要进行设置,最简单的办法是通过在 Unity菜单中依次选择 Mixed Reality Toolkit → Utilities → Windows Mixed Reality → Check Con.guration进行自动配置。另一种方法是在 Unity菜单中依次选择 Edit → Project Settings打开工程设置窗口,选择 Player → Universal Windows Platform → Other Settings卷展栏,在 Scripting De.ne Symbols属性栏中添加 DOTNETWINRT_PRESENT定义符。 通常而言,使用 USB连接能确保更好的通信传输速率和稳定性,在使用 USB连接时, 需要先在 HoloLens 2设备中断开 WiFi连接(默认使用 WiFi连接),然后启动 Remoting Player应用程序,该应用程序会显示一个以 192.168或 169开始的 IP地址,使用该 IP地址进行连接(在通过 USB连接成功后,可以重新开启 WiFi连接)。在使用全息远端呈现时,可能会出现 Unity编辑器被挂起而无法正常使用的情况,这种情况很多时候是由于上次使用完全息远端呈现后没有关闭连接或者退出 Unity时没有关闭全息仿真窗口引起的,确保全息仿真窗口被关闭 后重启 Unity会解决该问题。 3.4.2MR应用程序使用全息远端呈现 为充分利用 PC的强大计算处理能力,我们可以将 MR应用程序构建并运行在 PC端,通 过全息远端呈现功能将渲染结果以流的形式输出到 HoloLens 2设备上,同时将 HoloLens 2设 备检测到的手势操作、语音命令、眼动跟踪等输入数据传输到 PC端进行处理,这样就可以实 现无缝的高品质渲染和交互。 在实际使用时,通过 USB或者 WiFi连接 PC与 HoloLens 2设备后,我们可以通过脚本 代码在 PC与 HoloLens 2设备之间建立数据连接,在数据连接建立后,后续的数据传输则由 全息远端呈现功能接管,开发人员无须进行处理,代码如下: //第 3章 /3-5.cs using System.Collections; using UnityEngine; using UnityEngine.XR; using UnityEngine.XR.WSA; a 该插件需要使用 NuGet安装,在安装好 NuGet后,可以通过在 NuGet客户端搜索 DotNetWinRT并安装它。 public class HolographicRemoteConnect : MonoBehaviour { [SerializeField] private string IP; private bool connected = false; //开始连接 public void Connect() { if (HolographicRemoting.ConnectionState != HolographicStreamerConnectionState. Connected) { //对 HoloLens 1设备使用 HolographicRemoting.Connect(IP) HolographicRemoting.Connect(IP, 99999, RemoteDeviceVersion.V2); } } //断开连接 public void DisConnect() { HolographicRemoting.Disconnect(); connected = false; } void Update() { if (!connected && HolographicRemoting.ConnectionState == HolographicS treamerConnectionState.Connected) { connected = true; StartCoroutine(LoadDevice("WindowsMR")); } } IEnumerator LoadDevice(string newDevice) { XRSettings.LoadDeviceByName(newDevice); yield return null; XRSettings.enabled = true; } } 如果在使用全息远端呈现时需要进行语音命令操作,由于语音命令传输需要使用因特网客户端和个人网络客户服务器端的功能特性,因此需要在项目中开启这两个功能特性,在 Unity菜单中,依次选择 Edit → Project Settings → Player,选择 Universal Windows Platform Settings(UWP设置)选项卡,展开 Publishing Settings卷展栏,在 Capabilities功能设置区,勾选 InternetClient和 PrivateNetworkClientServer复选框,如图 3-12(a)所示,然后打开 XR Settings卷展栏,勾选 WSA Holographic Remoting Supported(WSA全息远端呈现支持)复选框启用全息远端呈现功能,如图 3-12(b)所示。 HoloLens 2开发入门精要——基于 Unity和 MRTK 图 3-12 启用远端呈现功能 使用该功能时, Unity按正常流程构建 Visual Studio工程,在 Visual Studio发布 MR应用时选择 Release、x64、Local Machine生成 PC端应用程序,然后在 PC端启动该应用程序。 随后,在 HoloLens 2设备中启动 Remoting Player应用程序,该应用程序会显示一个 IP地址,确保 PC端 MR应用程序所使用的 IP地址与该地址一致,一旦数据连接成功,就可以通过 HoloLens 2设备呈现 PC端渲染的内容。 3.5 诊断系统 MRTK提供了一个用于性能分析的诊断系统( Diagnostic System),该诊断系统使用图形化方式直观地显示当前应用资源的消耗和帧率等信息,是 MR应用开发性能分析的有力工具,建议开发者在整个应用开发迭代过程中都开启该诊断系统,时刻监视 MR应用的性能消耗情况,便于进行性能优化,只在应用真正发布前关闭它 a。 诊断系统由配置文件进行配置,打开主配置文件下诊断系统的子配置文件,界面如图 3-13所示。 图 3-13 诊断系统配置界面 a MRTK配置文件默认开启诊断系统。 在 MRTK中,Diagnostics System Type(诊断系统类型)属性需选择为 Microsoft.Mixed-Reality.Toolkit.Diagnostics → MixedRealityDiagnosticsSystem,其余各属性描述如表 3-5所示。 表 3-5 诊断系统各属性描述 属性名称 描述 Enable Verbose Logging(开启日志) 这实质上是一个独立的日志记录功能开关,与诊断系统相互不影响,设置该值会影响整个 MR应用日志记录。该值用于开启 MRTK日志系统,默认未勾选,当勾选时, MR应用详细的日志信息将被记录,通常在性能分析时使用,通过详细的日志文件可以加速性能问题的定位和排除 Enable Diagnostics System(开启诊断系统) 诊断系统开关 Show Diagnostics(显示诊断值) 配置值显示开关,取消勾选时所有配置选项均不显示 Show Pro.ler(显示分析器) 诊断系统图形界面开关,取消勾选时不显示诊断系统图形界面 Frame Sample rate(帧率) 计算帧率使用的时长单位,范围为 [0, 5] Window Anchor(窗口锚定) 诊断系统图形界面显示位置 Window O.set(窗口偏移) 诊断系统图像界面显示位置偏移量 Window Scale(窗口缩放) 诊断系统图像界面缩放值 Window Follow Speed(窗口跟随速度) 诊断系统图像界面跟随使用者的速度 Show Pro.ler During MRC(MRC时显示分析器) 在进行图像捕获或者视频录制时是否显示诊断系统图像界面 除了在开发阶段可以静态地启用 /停用诊断系统,我们也可以在 MR应用运行时动态地启用或者关闭诊断系统,典型代码如下: //第 3章 /3-6.cs CoreServices.DiagnosticsSystem.Disable(); CoreServices.DiagnosticsSystem.ShowDiagnostics = false; CoreServices.DiagnosticsSystem.ShowPro.ler = false; 诊断系统图形界面如图 3-14所示,它以直观的方式展现了 MR应用的实时性能数据。 图 3-14 诊断系统图像界面示意图 图 3-14左上角显示了 MR应用的实时帧率和每帧所用时间,对于 HoloLens 2设备,为了保证流畅运行,帧率的典型值为 60fps,即每帧用时不超过 16.7ms。中间红绿相间的滚动图形 HoloLens 2开发入门精要——基于 Unity和 MRTK 指示 MR应用每帧运行的情况,红色部分表示该帧未能达到指定帧率,也就是需要优化的帧。界面下部区域为内存使用情况,包括当前内存占用量、峰值内存占用量、最大内容可用量等信息,内存使用量也实时刷新,我们可以通过内存使用量直观了解资源的使用情况。 虽然我们可以在 Unity编辑器中使用诊断系统,但 MR应用最终需要运行在 HoloLens 2设备上,因此,为获得准确的性能数据,一定需要在真机上进行性能测试,另外,为排除 Debug模式的影响,最好使用 Release模式进行性能测试。 3.6 动态 GLTF格式模型加载 GLTF(GL Transmission Format,GL传输格式)是一种专门设计用于网络传输的模型格式,它能够更加高效地通过网络传输场景或者模型,并且简化了模型解包和处理,有良好的可伸缩性设计,可以使用 JSON格式描述模型,也可以使用二进制 GLB形式组织模型文件以降低模型的传输数据量,目前已广泛应用于工业和商业中。 MRTK对 GLTF格式文件提供直接支持 a,可以在运行时加载本地或者网络端模型文件,所有与 GLTF模型操作相关的脚本放置于 Microsoft.MixedReality.Toolkit.Utilities.Gltf命名空间下。本节简单介绍在 MR应用中动态加载 GLTF模型文件的一般处理流程,从 StreamingAssets文件夹下动态加载 GLTF文件的代码如下: //第 3章 /3-7.cs //需要引入 Microsoft.MixedReality.Toolkit.Utilities.Gltf.Schema;Microsoft.MixedReality. Toolkit.Utilities.Gltf.Serialization命名空间 public class TestGltfLoading : MonoBehaviour { private string relativePath = "model.gltf"; public string AbsolutePath => Path.Combine(Path.GetFullPath(Application. streamingAssetsPath), relativePath); private .oat ScaleFactor = 1.0f; private async void Start() { var path = AbsolutePath; if (!File.Exists(path)) { Debug.LogError($"在路径 {path}中找不到 GLTF文件 "); return; } GltfObject gltfObject = null; try { gltfObject = await GltfUtility.ImportGltfObjectFromPathAsync(path); a 也提供了其他模型格式与 GLTF格式的转换工具 https://github.com/Microsoft/glTF-Toolkit/releases。 gltfObject.GameObjectReference.transform.position = new Vector3(0.0f, 0.0f, 1.0f); gltfObject.GameObjectReference.transform.localScale *= this.ScaleFactor; } catch (Exception e) { Debug.LogError($"加载模型文件失败 - {e.Message}\n{e.StackTrace}"); } if (gltfObject != null) { Debug.Log("加载模型文件成功 "); } } } 除了从本地端加载 GLTF格式文件, MRTK也支持直接从网络服务器上加载 GLTF格式模型,从网络端加载 GLTF格式模型的一般流程的代码如下: //第 3章 /3-8.cs //需要引入 Microsoft.MixedReality.Toolkit.Utilities;Microsoft.MixedReality.Toolkit. Utilities.Gltf.Serialization命名空间 public class TestGlbLoading : MonoBehaviour { private string uri = "https://www.baidu.com/model.glb"; private async void Start() { Response response = new Response(); try { response = await Rest.GetAsync(uri, readResponseData: true); } catch (Exception e) { Debug.LogError(e.Message); } if (!response.Successful) { Debug.LogError($"从 URL:{uri}加载模型失败 "); return; } var gltfObject = GltfUtility.GetGltfObjectFromGlb(response.ResponseData); try { await gltfObject.ConstructAsync(); } catch (Exception e) { HoloLens 2开发入门精要——基于 Unity和 MRTK Debug.LogError($"模型解释失败:{e.Message}\n{e.StackTrace}"); return; } if (gltfObject != null) { Debug.Log("模型加载成功 "); } } } 3.7 多场景管理 在 MR应用运行时,可能需要动态地加载各种不同类型的虚拟对象,如模型、文字、视频等,一般采取的方法是加载不同的预制体,加载预制体的方式对单个对象的使用非常友好,但在有相互位置关系的多对象情形下,使用 MRTK提供的场景系统( Scene System)可以更加高效地进行管理,场景系统主要为以下情形而设计: (1)工程包含多场景,并且需要在运行时切换。(2)在 MR应用中切换场景,但需要保持体验的一致性。(3)使用一种更简便的方式利用多个场景构建 MR体验。(4)简化监视场景加载或者激活过程。(5)在场景切换时保持 MR场景光照效果的一致性。 3.7.1场景系统配置 使用 MRTK提供的场景系统可以极大地方便多场景管理,并且保持一致连续的 MR体验,场景系统默认不会开启,在需要场景系统时我们首先应当在配置文件中开启该功能,在 MRTK主配置文件下单击 Scene System子配置文件,勾选其 Enable Scene System(开启场景系统)复选框开启场景系统功能,将 Scene System Type(场景系统类型)属性设置为 Microsoft.MixedReality.Toolkit.SceneSystem → MixedRealitySceneSystem,如图 3-15所示。 图 3-15 场景系统配置文件面板(局部) 为简化管理,MRTK将场景分为以下 3类。 (1)管理场景:管理场景是一个用于管理其他场景加载 /卸载的唯一实例场景,它包含一个 MixedRealityToolkit实例,该场景在应用程序启动后首先被加载,并且在整个应用程序生命周期内将保持运行,因此,该场景中的所有对象都不会在场景切换时被销毁。 (2)内容场景:内容场景即为需要进行切换的工作场景,一个工程中可以包含一个或者多个内容场景,这些内容场景也可以组合加载。 (3)光照场景:光照场景是为保持在内容场景切换时光照的一致性而设计的,一个工程中可以包含多个光照场景,但某个时刻只能有一个光照场景被激活,当光照场景切换时,光照效果会平滑过渡。 在开启场景系统后,默认会启动很多与场景管理相关的功能特性。 (1)Editor Manage Build Settings(编辑器管理构建设置):用于场景系统自动更新工程设置以确保所有管理器场景、光照场景、内容场景都正确加载。 (2)Editor Manage Loaded Scenes(编辑器管理加载场景):用于强制加载所有管理器场景、光照场景和内容场景。 (3)Editor Enforce Scene Order(编辑器强制场景顺序):用于强制按管理器场景、光照场景、内容场景顺序加载。 (4)Editor Enforce Lighting Scene Types(编辑器强制光照场景类型):用于控制光照场景中可以加载的对象类型,勾选该值后只有在 Permitted Lighting Scene Component Types(允许的光照场景组件类型)属性中定义的类型才可以被加载到光照场景中。 以上功能可以辅助、简单化多场景管理,开发者如果需要自行控制所有场景管理过程,可以取消对应功能特性的选择。 当在配置文件中开启场景系统后, MRTK会在 Unity层级( Hierarchy)窗口中生成 3个场景对象: DefaultManagerScene、DefaultLightingScene、MultiScene,分别对应配置文件中的 Manager Scene Settings(管理场景设置)、Lighting Scene Settings(光照场景设置)、Content Scene Settings(内容场景设置)配置内容,其中管理场景设置用于配置首先加载的管理场景,开发者如果不使用管理场景,取消勾选 Use Manager Scene复选框即可。光照场景设置用于配置场景光照,对于 MR应用,由于 MRTK会自动采样环境光照情况,可以取消勾选 Use Lighting Scene复选框。内容场景设置用于配置 MR应用需要使用的所有内容场景,每个内容场景都会生成一个构建索引(Build Index),也可以指定一个标签(Tag),如图 3-16所示。 图 3-16 内容场景设置界面 HoloLens 2开发入门精要——基于 Unity和 MRTK 内容场景设置中配置的内容场景是我们主要的操作对象,这些内容场景可以在任何时候被安全地加载、卸载、组合。 3.7.2场景加载与卸载 利用场景系统加载的场景是叠加性的,即新加入的场景不会影响原场景内容,因此我们可以一次加载 /卸载一个场景或者多个场景,示例代码如下: //第 3章 /3-9.cs private async void LoadOrUnLoadScene() { IMixedRealitySceneSystem sceneSystem = MixedRealityToolkit.Instance.GetService (); await sceneSystem.LoadContent("MyContentScene1"); await sceneSystem.LoadContent("MyContentScene1",LoadSceneMode.Single); await sceneSystem.LoadContent(new string[] { "MyContentScene1", "MyContentScene2", "MyContentScene3" }); if (sceneSystem.IsContentLoaded("MyContentScene1")) await sceneSystem.UnloadContent("MyContentScene1"); } LoadContent()方法有多个重载,可以指定加载内容场景的模式( mode),如果使用 LoadSceneMode.Single模式,则会在加载本内容场景之前先卸载场景中原来所有的内容场景,该方法可以加载单个场景,也可以一次性加载多个场景。 我们也可以使用场景标签加载内容场景,由于不同的内容场景可以使用同一个标签,因此利用标签加载也可以一次加载一个或者多个场景,使用标签方式加载的另一个好处是对美术人员特别友好,他们可以通过为场景设定不同标签来控制内容场景的加载,而不必改动脚本代码,典型代码如下: //第 3章 /3-10.cs private async void LoadOrUnLoadScene() { IMixedRealitySceneSystem sceneSystem = MixedRealityToolkit.Instance.GetService (); await LoadContentByTag("MyContentScene1"); if (sceneSystem.IsContentLoaded("MyContentScene1")) await UnloadContentByTag("MyContentScene1"); } 每个内容场景都有对应的构建索引,索引是有序整型数字,因此我们也可以直接加载上一个或者下一个场景而无须指定内容场景名,代码如下: //第 3章 /3-11.cs private async void LoadScene() { IMixedRealitySceneSystem sceneSystem = MixedRealityToolkit.Instance.GetService (); if (sceneSystem.NextContentExists) { await sceneSystem.LoadNextContent(); } if (sceneSystem.PrevContentExists) { await sceneSystem.LoadPrevContent(); } await sceneSystem.LoadNextContent(true); } 在加载下一个内容场景时需要通过 NextContentExists属性值检查下一个内容场景是否存在,同样,加载上一个内容场景时需要通过 PrevContentExists属性值检查上一个内容场景是否存在。 LoadNextContent()方法也有若干默认参数,如果指定 wrap参数值为 true时,则会循环构建索引,构建最后一个内容场景后会返回第 1个内容场景,同样,该方法也可以指定加载模式。 3.7.3场景加载进度与事件 当内容场景正在加载或者卸载时, SceneOperationInProgress属性的返回值为 true,这时我们可以通过 SceneOperationProgress属性获取加载 /卸载进度数值,该属性取值范围为 [0,1],用于指示场景加载或者卸载进度,在一次性加载多个场景时,该值为整体进度。典型的加载 /卸载进度代码如下: //第 3章 /3-12.cs public class ProgressDialog : MonoBehaviour { private void Update() { IMixedRealitySceneSystem sceneSystem = MixedRealityToolkit.Instance. GetService(); if (sceneSystem.SceneOperationInProgress) { //显示进度条 DisplayProgressIndicator(sceneSystem.SceneOperationProgress); } else { //隐藏进度条 HideProgressIndicator(); HoloLens 2开发入门精要——基于 Unity和 MRTK } } //后续操作 } 除了使用进度条,场景系统也提供了若干事件用于监视内容场景的加载 /卸载情况,所有内容场景的加载事件如表 3-6所示。 表 3-6 场景系统内容场景的加载事件 事件名称 描  述 适用场景 OnWillLoadContent 内容场景加载前触发 内容场景 OnContentLoaded 所有内容场景加载完成后触发 内容场景 OnWillUnloadContent 内容场景卸载前触发 内容场景 OnContentUnloaded 所有内容场景完全卸载后触发 内容场景 OnWillLoadLighting 光照场景加载前触发 光照场景 OnLightingLoaded 光照场景加载完成后触发 光照场景 OnWillUnloadLighting 光照场景卸载前触发 光照场景 OnLightingUnloaded 光照场景完全卸载后触发 光照场景 OnWillLoadScene 场景加载前触发 所有场景 OnSceneLoaded 所有场景加载完成后触发 所有场景 OnWillUnloadScene 场景卸载前触发 所有场景 OnSceneUnloaded 所有场景完全卸载后触发 所有场景 由于一次操作可以加载 /卸载一个或多个内容场景,当涉及多个内容场景时,每个场景在加载 /卸载时都会触发对应的事件,但也有一些事件会在全部场景加载 /卸载完成后同时触发,因此,建议使用 OnWillUnload事件侦测场景卸载操作,而不是使用 OnUnloaded事件,相应地, OnLoaded事件会在全部场景加载完成之后触发,使用 OnLoaded事件会比 OnWillLoadContent事件更安全,典型场景事件代码如下: //第 3章 /3-13.cs public class ProgressDialog : MonoBehaviour { private bool displayingProgress = false; private void Start() { IMixedRealitySceneSystem sceneSystem = MixedRealityToolkit.Instance. GetService(); sceneSystem.OnWillLoadContent += HandleSceneOperation; sceneSystem.OnWillUnloadContent += HandleSceneOperation; } private void HandleSceneOperation (string sceneName) { if (displayingProgress) { return; } displayingProgress = true; StartCoroutine(DisplayProgress()); } private IEnumerator DisplayProgress() { IMixedRealitySceneSystem sceneSystem = MixedRealityToolkit.Instance. GetService(); while (sceneSystem.SceneOperationInProgress) { DisplayProgressIndicator(sceneSystem.SceneOperationProgress); yield return null; } HideProgressIndicator(); displayingProgress = false; } //后续操作 } 默认情况下,内容场景一旦加载完成就会马上激活,但我们也可以使用 Scene-ActivationToken机制控制其激活时机,当一次操作加载多个内容场景时,这个激活令牌(Token)会应用到所有场景,典型控制代码如下: //第 3章 /3-14.cs IMixedRealitySceneSystem sceneSystem = MixedRealityToolkit.Instance.GetService (); SceneActivationToken activationToken = new SceneActivationToken(); //加载场景时传递激活令牌 sceneSystem.LoadContent(new string[] { "ContentScene1", "ContentScene2", "ContentScene3" }, LoadSceneMode.Additive, activationToken); //示例:如等待所有玩家加入 while (!AllUsersHaveJoinedExperience()) { await Task.Yield(); } //准备激活所有场景 activationToken.AllowSceneActivation = true; //加载并激活所有场景 while (sceneSystem.SceneOperationInProgress) HoloLens 2开发入门精要——基于 Unity和 MRTK { await Task.Yield(); } 场景系统的 ContentSceneNames属性包含当前已加载的所有内容场景名,按构建索引排序,我们可以通过 IsContentLoaded(string contentName)检查对应场景是否已加载,典型代码如下: //第 3章 /3-15.cs IMixedRealitySceneSystem sceneSystem = MixedRealityToolkit.Instance.GetService (); string[] contentSceneNames = sceneSystem.ContentSceneNames; bool[] loadStatus = new bool[contentSceneNames.Length]; for (int i = 0; i < contentSceneNames.Length; i++>) { loadStatus[i] = sceneSystem.IsContentLoaded(contentSceneNames[i]); }