ARCore-学习指南-全-

ARCore 学习指南(全)

原文:zh.annas-archive.org/md5/dbc674bb4ce5ab1b45ac0a050122632e

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

增强现实应用已经从新奇转变为现实,随着 ARKit 和 ARCore 的发布,它们对普通开发者来说变得更加容易接触。现在,几乎任何掌握编程语言的人都可以快速使用各种平台构建 AR 体验。随着 ARCore 的发布,谷歌现在使这一过程更加简单,并为多个开发平台提供支持。本书将指导您使用 JavaScript 和 Web 在移动端(Java/Android)以及移动端(C# / Unity)构建 AR 应用。在这个过程中,您将学习为您的用户构建高质量 AR 体验的基础知识。

本书面向的对象

本书面向任何想要使用 ARCore 深入构建增强现实应用的开发者,但没有任何游戏或图形编程背景。尽管本书仅假设读者具备基本的中学水平数学知识,但读者仍应至少掌握以下编程语言之一:JavaScript、Java 或 C#。

本书涵盖的内容

第一章,入门,涵盖了任何现代 AR 应用都需要解决的基本概念,以便为用户提供良好的体验。我们将学习运动跟踪、环境理解和光估计的基本概念。

第二章,Android 上的 ARCore,是使用 Android Studio 进行 Android 开发的入门指南,其中我们向您展示如何安装 Android Studio 并设置您的第一个 ARCore 应用。

第三章,Unity 上的 ARCore,讨论了如何使用 Unity 安装和构建 ARCore 应用。本章还向您展示如何使用 Android 开发工具远程调试应用。

第四章,Web 上的 ARCore,跳入使用 JavaScript 的 Web 开发,重点介绍如何使用 Node.js 设置您自己的简单 Web 服务器。然后,本章将浏览各种 ARCore 示例模板,并讨论如何扩展这些模板以进行进一步开发。

第五章,现实世界运动跟踪,扩展了上一章的学习内容,并将一个 Web 示例扩展到添加现实世界的运动跟踪。这不仅将展示与 3D 概念一起工作的几个基本原理,还将演示 ARCore 如何跟踪用户的运动。

第六章,理解环境,回到 Android 平台,处理 ARCore 如何理解用户的环境。我们将了解 ARCore 如何识别环境中的平面或表面并将它们网格化以供用户交互和可视化。在这里,我们将探讨如何修改着色器以测量和着色用户的数据点。

第七章,光线估计,解释了光照和阴影在向用户销售 AR 体验中所扮演的角色。我们学习了 ARCore 如何提供光线估计以及它是如何用于照亮用户放置到 AR 世界中的虚拟模型的。

第八章,识别环境,介绍了机器学习的基础以及这项技术对 AR 革命成功的重要性。然后我们转向构建一个简单的神经网络,通过监督训练使用称为反向传播的技术进行学习。在了解了神经网络和深度学习的基础之后,我们转向一个更复杂的示例,展示了各种机器学习的形式。

第九章,为建筑设计融合光线,介绍了构建一个 AR 设计应用程序,允许用户在客厅或他们需要的任何地方放置虚拟家具。我们还介绍了如何使用触摸在 AR 中放置和移动对象,以及如何识别对象已被选中。然后,我们将从第七章,光线估计,扩展我们的光照和阴影,并在虚拟对象上提供实时阴影。

第十章,混合现实中的融合,介绍了通过使用低成本的 MR 头戴式设备来引入混合现实。ARCore 非常适合用于这些低成本的耳机,因为它已经内部跟踪用户并监控其环境。我们将概述如何将我们的应用程序从使用 Unity 的 3D WRLD API 的传统地图应用程序转变为 AR 地图应用程序,同时我们还将提供一个选项来切换到 MR 和 MR 耳机。

第十一章,性能提示和故障排除,涵盖了我们在所有处理的开发平台上测量应用程序性能的技术。然后我们讨论了性能的重要性及其对各种系统可能产生的影响。之后,我们介绍了通用的调试和故障排除技巧,最后用一个表格总结了用户在此书中可能遇到的常见错误。

为了充分利用这本书

为了最大限度地利用这本书,以下是需要记住的事项:

  • 读者需要精通以下编程语言之一:JavaScript、Java 或 C#

  • 高中数学的记忆

  • 支持 ARCore 的 Android 设备;以下链接可以查看设备列表:developers.google.com/ar/discover/

  • 一台可以运行 Android Studio 和 Unity 的桌面电脑;不需要明确要求专门的 3D 显卡

下载示例代码文件

您可以从www.packtpub.com的账户下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以按照以下步骤下载代码文件:

  1. www.packtpub.com登录或注册。

  2. 选择 SUPPORT 标签。

  3. 点击代码下载与勘误。

  4. 在搜索框中输入书籍名称,并遵循屏幕上的说明。

文件下载后,请确保使用最新版本的软件解压或提取文件夹:

  • 适用于 Windows 的 WinRAR/7-Zip

  • 适用于 Mac 的 Zipeg/iZip/UnRarX

  • 适用于 Linux 的 7-Zip/PeaZip

本书代码包也托管在 GitHub 上,地址为github.com/PacktPublishing/Learn-ARCore-Fundamentals-of-Google-ARCore。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还有来自我们丰富的书籍和视频目录中的其他代码包,可在github.com/PacktPublishing/找到。查看它们吧!

下载彩色图像

我们还提供包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/LearnARCoreFundamentalsofGoogleARCore_ColorImages.pdf

使用的约定

本书使用了多种文本约定。

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“向下滚动到draw方法,并在指定的行下方添加以下代码。”

代码块设置如下:

void main() {
   float t = length(a_Position)/u_FurthestPoint;
   v_Color = vec4(t, 1.0-t,t,1.0);
   gl_Position = u_ModelViewProjection * vec4(a_Position.xyz, 1.0);
   gl_PointSize = u_PointSize;
}

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

uniform mat4 u_ModelViewProjection;
uniform vec4 u_Color;
uniform float u_PointSize;
uniform float u_FurthestPoint;

任何命令行输入或输出都按以下方式编写:

cd c:\Android
npm install http-server -g

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“从管理面板中选择系统信息。”

警告或重要注意事项如下所示。

小贴士和技巧如下所示。

联系我们

我们欢迎读者的反馈。

一般反馈:请将电子邮件发送至feedback@packtpub.com,并在邮件主题中提及书名。如果您对本书的任何方面有疑问,请通过电子邮件联系我们questions@packtpub.com

勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告这一点。请访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版:如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过发送链接至 copyright@packtpub.com 与我们联系。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com

评论

请留下评论。一旦您阅读并使用了这本书,为何不在您购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,我们 Packt 可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!

如需更多关于 Packt 的信息,请访问 packtpub.com

第一章:入门

欢迎来到 Google ARCore 的沉浸计算和增强现实的世界。在本书中,我们将从基础知识开始。首先,我们将介绍增强现实(AR)的一些重要核心概念。从那里,我们将介绍本书中将使用的三个开发平台(Android、Web 和 Unity)的安装和基础知识。接下来,我们将更深入地探讨 AR 开发者面临的技术挑战,包括解决这些挑战的各种技术和方法。在本书的最后几章中,我们将通过开发三个示例 AR 和混合现实MR)应用来扩展这些技能,我们将构建一个机器学习物体识别器、一个 AR 设计应用以及一个从 AR 过渡到 MR 的应用。

我们决定从本书中省略 Unreal 平台,并不是因为它是一个低级的平台,恰恰相反。Unreal 是一个经过验证且处于前沿的游戏引擎,非常适合经验丰富的图形和游戏开发者。然而,Unreal 和 Unity 在开发功能上基本上是相等的。因此,专注于 Unity 更有意义,它更适合学习游戏和图形开发。

在本章中,我们将首先快速介绍沉浸式计算和增强现实的基本概念。然后,我们将探讨 ARCore 旨在解决的核心理问题(运动跟踪、环境理解和光估计)。以下是本章我们将涉及的主题快速浏览:

  • 沉浸计算

  • ARCore 和 AR

    • 运动跟踪

    • 环境理解

    • 光估计

  • 前方的道路

本书是在 ARCore 的测试版中编写的。如果您发现有任何不同之处或需要更改的内容,请联系 Packt 并提供勘误表。

沉浸计算

沉浸计算是一个新术语,用来描述为用户提供沉浸式体验的应用。这可能以增强现实或虚拟现实体验的形式出现。虽然本书的重点将主要集中在构建增强现实体验上,但我们也会突出可用于 VR 的技术。为了更好地理解沉浸计算的范围,让我们看一下这个图表:

图片

沉浸计算谱系

上述图表说明了沉浸程度如何影响用户体验,图表的左侧代表沉浸程度较低或几乎没有的传统应用,而右侧则代表完全沉浸的虚拟现实应用。对我们来说,我们将保持在中间的甜蜜点,致力于开发增强现实应用。在下一节中,我们将更详细地介绍 AR 和 ARCore。

AR 和 ARCore

增强现实应用是独特的,因为它们会注释或增强用户的现实。这通常是通过 AR 应用将计算机图形叠加到现实世界的视图上视觉完成的。ARCore 主要是为了为用户提供这种类型的视觉注释而设计的。这里展示了一个 ARCore 演示应用的例子:

图片

谷歌 ARCore 演示应用;狗是真的

当你意识到这个截图是在移动设备上实时渲染的时候,它甚至更加令人印象深刻。这不是使用 Photoshop 或其他媒体效果库耗时数小时的结果。你在这张图片中看到的是虚拟物体,狮子,在用户现实中的整个叠加。更令人印象深刻的是沉浸感的质量。注意细节,比如狮子上的光照和阴影,地面上的阴影,以及物体在现实中保持位置的方式,尽管它实际上并不在那里。如果没有这些视觉增强,你看到的将只是一个浮现在屏幕上的狮子。正是这些视觉细节提供了沉浸感。谷歌开发了 ARCore,作为一种帮助开发者将这种视觉增强融入构建 AR 应用的方法。

谷歌开发了 ARCore 用于 Android,作为一种与苹果的 iOS ARKit 竞争的方式。今天,两大科技巨头在 AR 领域争夺地位的事实表明了构建新的和创新的沉浸式应用的推动力。

ARCore 起源于 Tango,这是一个更先进的 AR 工具包,它使用内置在设备中的特殊传感器。为了使 AR 更加易于访问和主流,谷歌开发了 ARCore,这是一个为没有配备任何特殊传感器的 Android 设备设计的 AR 工具包。Tango 依赖于特殊传感器,而 ARCore 则使用软件来尝试完成相同的核心增强。对于 ARCore,谷歌确定了三个核心领域,通过这个工具包来解决,如下所示:

  • 运动追踪

  • 环境理解

  • 光估计

在接下来的三个部分中,我们将更详细地探讨这些核心领域,并了解它们如何增强用户体验。

运动追踪

跟踪用户在 2D 和 3D 空间中的运动和最终位置是任何 AR 应用的基础。ARCore 允许我们通过识别和跟踪设备摄像头图像中的视觉特征点来跟踪位置变化。这种工作原理的例子如图所示:

图片

ARCore 中的特征点跟踪

在图中,我们可以看到用户的位置是如何相对于在真实沙发上识别的特征点进行跟踪的。之前,为了成功跟踪运动(位置),我们需要预先注册或预先训练我们的特征点。如果你曾经使用过 Vuforia AR 工具,你将非常熟悉需要训练图像或目标标记。现在,ARCore 为我们自动完成所有这些,实时进行,无需任何训练。然而,这种跟踪技术非常新,存在一些限制。在本书的后期部分,特别是在第五章,真实世界运动跟踪中,我们将为我们的 AR 助手添加一个功能,允许我们使用 GPS 实时跟踪来自多个设备的多个对象的位置。然后,在第十章,混合现实中的混合中,我们将扩展我们的跟踪,包括增强地图。

环境理解

AR 应用越好地理解用户的现实或他们周围的环境,沉浸感就越强。我们已经看到了 ARCore 如何使用特征识别来跟踪用户的运动。然而,跟踪运动只是第一步。我们需要的是一种方法来识别用户现实中的物理对象或表面。ARCore 通过一种称为网格化的技术来实现这一点。

我们将在后面的章节中详细介绍网格化的更多细节,但现在,请看一下谷歌提供的以下图像,它展示了网格化操作的实际应用:

图片

展示网格化操作的谷歌图像

在前面的图像中,我们看到的是一个通过网格化识别了真实世界表面的 AR 应用。平面通过白色点被识别。在背景中,我们可以看到用户已经在表面上放置了各种虚拟对象。环境理解和网格化对于创建融合现实的幻觉至关重要。运动跟踪使用识别的特征来跟踪用户的位置,而环境理解使用网格化来跟踪用户现实中的虚拟对象。在第八章,识别环境中,我们将探讨如何训练我们自己的机器学习对象识别器,这将使我们能够将网格化扩展到包括环境中可自动识别的对象或区域。

光度估计

魔术师努力成为欺诈和视觉错觉的大师。他们明白,在伟大的错觉中,透视和良好的光照是至关重要的,而在开发优秀的 AR 应用中,这一点也不例外。请花一秒钟时间回到有虚拟狮子的场景。注意狮子和地面上的阴影的照明和细节。你注意到狮子在地面上投下了影子,尽管它实际上并不在那里吗?这种额外的照明细节是通过结合用户位置的追踪、虚拟对象位置的环境理解以及读取光水平的方式才得以实现的。幸运的是,ARCore 为我们提供了一种读取或估计场景中光照的方法。然后我们可以使用这些照明信息来照亮和投射虚拟 AR 对象的阴影。这是一张显示 ARCore 演示应用上低调照明的图片:

Google 展示低调照明的 ARCore 演示应用图片

随着我们开始开发我们的初创应用程序,光照效果或缺乏光照的影响将变得更加明显。稍后,在第九章 Blending Light for Architectural Design 中,我们将更深入地探讨 3D 光照,甚至构建一些简单的着色器效果。

在本章中,我们没有深入探讨任何详细内容;我们将在稍后进行,但你现在应该已经很好地掌握了 ARCore 旨在解决的核心理念。在下一节中,我们将更详细地探讨如何最好地使用本书中的材料。

前方的道路

在本书的剩余部分,我们将采取非常实际的方法。毕竟,没有比实践更好的学习方法了。虽然本书旨在整体阅读,但并非所有读者都有时间或需要这样做。因此,以下表格提供了一个关于书中剩余章节的平台、工具、技术和难度级别的快速总结:

章节 重点 难度 平台 工具和技术
第二章,Android 中的 ARCore Android 基础 基础 Android (Java) 安装 Android 的工具和环境。
第三章,Unity 中的 ARCore Unity 基础 基础 Android/Unity (C#) Unity 示例的安装、设置和部署。
第四章,Web 中的 ARCore 构建 ARCore Web 应用 中等 Web (JavaScript) 支持 Web 开发和托管工具的安装和设置。
第五章,现实世界运动追踪 3D 空间音频和 Firebase 中等 Web (JavaScript) 使用带有音频的移动设备进行运动追踪,与 Google Firebase 集成,并在 AR 中追踪多个对象和/或用户。
第六章, 理解环境 EU 和网格化的介绍 中级 Android (Java) 学习 ARCore API 的 Java 版本,以及创建一个新的 ARCore Android 项目,对环境进行网格化,并使用 OpenGL ES 与对象交互。
第七章,光估计 Unity 中光估计和光照的介绍 高级 Unity (C#, Cg/HLSL) 了解光照的重要性以及如何使用它来使 AR 对象看起来更真实。
第八章,识别环境 AR 和机器学习(ML)的介绍及其应用 高级 Android (Java), Unity (C#) 查看各种机器学习平台,以便更好地理解它们在 AR 应用中的使用方法。
第九章,为建筑设计融合光线 3D 光照和着色器 高级 Unity (C#) Unity 中光照和着色器的深入介绍,包括编写 HLSL/ Cg 着色器代码。
第十章,混合现实中的混合 将所有元素结合在一起。 高级+ Unity (C#), Android (Java) 我们将通过引入混合现实来扩展 ARCore 平台,并允许应用从 AR 过渡到 MR。
第十一章,性能和故障排除 性能和故障排除技巧 基础 所有 提供了一些关于性能的有用技巧,并有一个专门的部分来解决你在处理示例时可能遇到的问题。

此外,第十章,混合现实中的混合,是在读者回顾了所有前面的章节之后使用的。

虽然一些读者可能更喜欢只通过那些特定章节来探索单个 ARCore 平台,但你被强烈鼓励完成这本书中的所有示例。鉴于 ARCore API 在各个平台之间如此相似,你学到的技术应该可以很好地转移到另一个平台。另外,不要因为不同的平台或编程语言而感到害怕。如果你在 C 语言方面有良好的知识基础,学习这个家族中的任何其他语言只需要最小的努力。开发者、程序员、软件工程师,或者你想要称自己为什么,你都可以从学习另一种编程语言中受益。

摘要

在本章中,我们简要地探讨了沉浸式计算和增强现实(AR)的基本概念。我们了解到,增强现实覆盖了沉浸式计算光谱的中间地带,AR 只是精心混合的幻觉,用来欺骗用户相信他们的现实已经与虚拟现实相结合。毕竟,谷歌开发了 ARCore,作为一种提供构建这些幻觉的更好工具的方式,并保持 Android 在 AR 市场中的竞争力。之后,我们学习了 ARCore 旨在解决的核心概念,并逐一进行了更详细的探讨:运动跟踪、环境理解和光估计。最后,我们为那些希望在最短的时间内从本书中获得最大收益的用户提供了一个有用的路线图。

在下一章中,我们将开始深入探讨,通过设置和调整满足我们需求的样本 Android 项目来“动手实践”。

第二章:Android 上的 ARCore

Google 开发了 ARCore,使其可以从多个开发平台(Android [Java],Web [JavaScript],Unreal [C++],和 Unity [C#])访问,从而为开发者提供了大量的灵活性和选项,以便在各种平台上构建应用程序。虽然每个平台都有其优势和劣势,我们将在后面讨论,但所有平台本质上都扩展了最初作为 Tango 构建的本地 Android SDK。这意味着无论你选择哪个平台,你都需要安装并熟悉使用 Android 开发工具。

在本章中,我们将专注于设置 Android 开发工具和构建 Android 的 ARCore 应用程序。以下是本章我们将涵盖的主要主题摘要:

  • 安装 Android Studio

  • 安装 ARCore

  • 构建和部署

  • 探索代码

如果你已经有经验使用 Android 工具并且已经安装了 SDK,你可能只想快速浏览前三部分。否则,请确保跟随本章的练习,因为这些步骤将在本书的许多其他领域的练习中是必需的。

在撰写本文时,为了执行本书中的任何练习,你需要一个支持 ARCore 的设备。支持设备的列表可以在developers.google.com/ar/discover/#supported_devices找到。其他人已经做了一些工作来支持早期设备,所以如果你有一个不支持设备,这可能是一个选择。你可以在github.com/tomthecarrot/arcore-for-all找到更多关于ARCore for All项目的详细信息。

安装 Android Studio

Android Studio 是一个用于编码和部署 Android 应用程序的开发环境。因此,它包含了我们构建和部署应用程序到 Android 设备所需的核心工具集。毕竟,ARCore 需要安装到物理设备上以便测试。按照以下说明在你的开发环境中安装 Android Studio:

  1. 在你的开发计算机上打开浏览器到developer.android.com/studio

  2. 点击绿色的“下载 Android Studio”按钮。

  3. 同意条款和条件并按照说明下载。

  4. 文件下载完成后,运行你的系统安装程序。

  5. 按照安装对话框上的说明进行操作。如果你在 Windows 上安装,请确保设置一个容易记住的安装路径,以便稍后轻松找到,如下例所示:

图片

设置 Windows 的安装路径

  1. 点击剩余的对话框以完成安装。

  2. 安装完成后,您将可以选择启动程序。确保选中启动 Android Studio 的选项,然后点击“完成”。

Android Studio 内置了 OpenJDK。这意味着我们至少在 Windows 上可以省略安装 Java 的步骤。如果您在 Windows 上进行任何严肃的 Android 开发,那么您应该自己执行步骤来安装完整的 Java JDK 1.7 和/或 1.8,尤其是如果您计划使用较旧的 Android 版本。

在 Windows 上,我们将把所有内容安装到 C:\Android;这样,我们可以把所有 Android 工具放在一个地方。如果您使用的是其他操作系统,请使用类似的已知路径。

现在我们已经安装了 Android Studio,但我们还没有完成。我们还需要安装构建和部署所必需的 SDK 工具。按照下一项练习中的说明完成安装:

  1. 如果您之前没有安装 Android SDK,那么当 Android Studio 首次启动时,您将被提示安装 SDK,如下所示:

图片

为 Windows 设置 SDK 安装路径

  1. 选择 SDK 组件,并确保您将安装路径设置为已知位置,再次,如前一张截图所示。

  2. 目前请保持 Android Studio 欢迎对话框开启。我们将在稍后的练习中返回它。

这样就完成了 Android Studio 的安装。在下一节中,我们将开始安装 ARCore。

安装 ARCore

当然,为了使用或构建任何 ARCore 应用程序,我们需要为我们的选择平台安装 SDK。按照以下说明安装 ARCore SDK:

我们将使用 Git 直接从源代码拉取所需的代码。您可以在 git-scm.com/book/en/v2/Getting-Started-Installing-Git 上了解更多关于 Git 以及如何在您的平台上安装 Git 的信息,或者使用 Google 搜索:getting started installing Git。确保当您在 Windows 上安装时,选择默认选项,并让安装程序设置 PATH 环境变量。

  1. 打开命令提示符或 Windows 命令行,导航到 Android (C:\Android 在 Windows 上) 安装文件夹。

  2. 输入以下命令:

git clone https://github.com/google-ar/arcore-android-sdk.git
  1. 这将下载并安装 ARCore SDK 到一个名为 arcore-android-sdk 的新文件夹中,如下面的截图所示:

图片

显示 ARCore 安装的命令窗口

  1. 确保您保持命令窗口开启。我们稍后还会再次使用它。

在设备上安装 ARCore 服务

现在,随着 ARCore SDK 已安装在我们的开发环境中,我们可以继续在测试设备上安装 ARCore 服务。使用以下步骤在您的设备上安装 ARCore 服务:

注意:此步骤仅在处理 ARCore 预览 SDK 时需要。当 Google ARCore 1.0 发布时,你将不需要执行此步骤。

  1. 拿起你的移动设备,通过以下步骤启用开发者调试选项:

    1. 打开设置应用

    2. 选择“系统”

    3. 滚动到页面底部并选择“关于手机”

    4. 再次滚动到页面底部并连续点击“构建号”七次

    5. 返回到上一屏幕并选择底部的“开发者选项”

    6. 选择“USB 调试”

  2. github.com/google-ar/arcore-android-sdk/releases/download/sdk-preview/arcore-preview.apk下载 ARCore 服务 APK 到 Android 安装文件夹(C:\Android)。此外,请注意,此 URL 可能会在未来发生变化。

  3. 使用 USB 线连接你的移动设备。如果你是第一次连接,你可能需要等待几分钟以安装驱动程序。然后,你将被提示开启设备以允许连接。选择“允许”以启用连接。

  4. 返回到命令提示符或 Windows shell,并运行以下命令:

adb install -r -d arcore-preview.apk
//ON WINDOWS USE:
sdk\platform-tools\adb install -r -d arcore-preview.apk 

命令运行后,你会看到“成功”这个词。如果你在这一阶段遇到了错误,请确保查阅第十一章,性能提示和故障排除,以获取更多帮助。

这就完成了 Android 平台 ARCore 的安装。在下一节中,我们将构建我们的第一个 ARCore 示例应用。

构建和部署

现在我们已经完成了所有繁琐的安装步骤,是时候构建并部署一个示例应用到你的 Android 设备上了。让我们回到 Android Studio,按照给定的步骤开始操作:

  1. 从 Android Studio 的欢迎窗口中选择“打开现有 Android Studio 项目”选项。如果你意外关闭了 Android Studio,只需再次启动它。

  2. 按照以下步骤导航并选择Android\arcore-android-sdk\samples\java_arcore_hello_ar文件夹:

图片

选择 ARCore 示例项目文件夹

  1. 点击“确定”。如果你是第一次运行此项目,你可能会遇到一些依赖错误,例如这里所示:

图片

依赖错误信息

  1. 为了解决错误,只需点击错误信息底部的链接。这将打开一个对话框,你将被提示接受并下载所需的依赖项。继续点击链接,直到不再出现错误。

  2. 确保你的移动设备已连接,然后从菜单中选择“运行”-“运行”。这应该在设备上启动应用,但你可能仍然需要解决一些依赖错误。只需记住点击链接来解决错误。

  3. 这将打开一个小对话框。选择应用选项。如果你看不到应用选项,从菜单中选择构建 - 构建项目。再次,通过点击链接解决任何依赖错误。

"你的耐心将得到回报。"

  • 阿尔顿·布朗
  1. 从下一个对话框中选择你的设备并点击“确定”。这将启动你的设备上的应用。确保你允许应用访问设备的相机。以下是一个显示应用运行状态的截图:

图片

运行中的示例 Android ARCore 应用;这只狗是真的

太好了,我们一起构建并部署了我们的第一个 Android ARCore 应用。在下一节中,我们将快速查看 Java 源代码。

探索代码

现在,让我们通过深入研究源代码来更仔细地查看应用的主要部分。按照以下步骤在 Android Studio 中打开应用的代码:

  1. 从项目窗口中,找到并双击HelloArActivity,如图所示:

图片

项目窗口中显示的 HelloArActivity

  1. 在源代码加载后,滚动到以下部分:
private void showLoadingMessage() {
 runOnUiThread(new Runnable() {
  @Override
  public void run() {
   mLoadingMessageSnackbar = Snackbar.make(
    HelloArActivity.this.findViewById(android.R.id.content),
    "Searching for surfaces...",
    Snackbar.LENGTH_INDEFINITE);
   mLoadingMessageSnackbar.getView().setBackgroundColor(0xbf323232);
   mLoadingMessageSnackbar.show();
  }
 });
}
  1. 注意高亮显示的文本——"Searching for surfaces.."。选择此文本并将其更改为"Searching for ARCore surfaces.."showLoadingMessage函数是一个用于显示加载信息的辅助函数。内部,这个函数调用runOnUIThread,它反过来创建一个新的Runnable实例,然后添加一个内部的run函数。我们这样做是为了避免在 UI 上阻塞线程,这是一个大忌。在run函数内部设置消息,并显示消息Snackbar

  2. 从菜单中选择运行 - 运行 'app' 以在设备上启动应用。当然,确保你的设备通过 USB 连接。

  3. 在你的设备上运行应用并确认消息已更改。

太好了,现在我们有一个包含我们自己的代码的工作应用。这当然不是飞跃,但在跑之前先走一走是有帮助的。在这个阶段,返回并审查代码,特别注意注释和流程。如果你从未开发过 Android 应用,代码可能看起来相当令人畏惧,确实如此。不必担心,我们将在第五章现实世界运动跟踪和第六章理解环境中分解并重用这个示例应用的一些元素。

摘要

在本章中,我们通过为 Android 平台构建和部署一个 AR 应用开始了对 ARCore 的探索。我们首先安装了 Android Studio,这将成为我们 Android 开发的集成开发环境IDE)。然后,我们在测试移动设备上安装了 ARCore SDK 和 ARCore 服务。接下来,我们加载了示例 ARCore 应用,并耐心地安装了各种所需的构建和部署依赖项。在成功构建后,我们将应用部署到我们的设备上并进行了测试。最后,我们测试了对代码进行微小修改后再次部署应用。这样做确保了我们的 Android 开发环境完全可用,我们现在可以继续阅读本书的其余部分。

我们的故事将在下一章继续,我们将使用 Unity 平台构建和部署一个 ARCore 应用。Unity 是一个领先的免费/商业游戏引擎,我们将在第十章混合现实混合的最终项目中使用。

第三章:Unity 上的 ARCore

我们接下来要搭建的平台是 Unity。Unity 是一个领先的跨平台游戏引擎,它特别易于快速构建游戏和图形应用程序。因此,当我们第十章“混合现实中的混合”中构建最终应用程序时,它将成为我们使用的平台。

近年来,Unity 因其被过度用于低质量游戏而获得了一些坏名声。这不是因为 Unity 不能制作高质量的游戏,它当然可以。然而,快速创建游戏的能力经常被开发者滥用,他们为了盈利而发布廉价游戏。

在本章中,我们将学习如何为 Android 安装、构建和部署 Unity ARCore 应用程序。然后,我们将设置远程调试,最后我们将探索对示例应用程序进行一些修改。以下是本章我们将涵盖的主题摘要:

  • 安装 Unity 和 ARCore

  • 构建和部署到 Android

  • 远程调试

  • 探索代码

我们已经在第二章“Android 上的 ARCore”中介绍了设置 Android 工具。如果您跳过了这一章,您需要回到前面几节去做练习,然后再继续。如果您是一个已经设置了 Android 环境的经验丰富的 Unity 开发者,您仍然应该阅读这一章,因为它可能包含一些有用的提示或设置。

安装 Unity 和 ARCore

安装 Unity 编辑器相对简单。然而,我们将使用的 Unity 版本可能仍在 beta 测试中。因此,在安装 Unity 时,您需要特别注意以下说明:

  1. 使用网络浏览器导航到unity3d.com/unity/beta

在撰写本文时,我们将使用 Unity 的最新 beta 版本,因为 ARCore 也仍在 beta 预览中。请务必注意您正在下载和安装的版本。如果您在使用 ARCore 时遇到问题,这将有所帮助。

  1. 点击“下载安装程序”按钮。这将下载UnityDownloadAssistant

  2. 启动UnityDownloadAssistant

  3. 点击“下一步”,然后同意服务条款。再次点击下一步

  4. 选择组件,如图所示:

图片

选择要安装的组件

  1. 将 Unity 安装在可以识别版本的文件夹中,如下所示:

图片

设置 Unity 安装路径

  1. 点击“下一步”下载并安装 Unity。这可能需要一些时间,所以请起身活动一下,喝点饮料。

  2. 点击“完成”按钮,并确保 Unity 设置为自动启动。让 Unity 启动并保持窗口打开。我们很快就会回来。

Unity 安装完成后,我们希望下载 Unity 的 ARCore SDK。由于我们已经安装了 Git,现在这会变得容易。按照给定的说明安装 SDK:

  1. 打开一个 shell 或命令提示符。

  2. 导航到您的Android文件夹。在 Windows 上,使用以下路径:

cd C:\Android
  1. 输入并执行以下命令:
git clone https://github.com/google-ar/arcore-unity-sdk.git
  1. git命令完成后,您将看到一个名为arcore-unity-sdk的新文件夹。

如果这是您第一次使用 Unity,您需要上网到unity3d.com/并创建一个 Unity 用户账户。Unity 编辑器将要求您在首次使用和之后不时登录。

现在我们已经安装了 Unity 和 ARCore,是时候通过以下步骤打开示例项目了:

  1. 如果您关闭了 Unity 窗口,请启动 Unity 编辑器。在 Windows 上的路径将是C:\Unity 2017.3.0b8\Editor\Unity.exe。您可以创建一个带有版本号的快捷方式,以便稍后更容易启动特定的 Unity 版本。

  2. 切换到 Unity 项目窗口,并点击打开按钮。

  3. 选择Android/arcore-unity-sdk文件夹。这是我们之前使用git命令安装 SDK 的文件夹,如下面的对话框所示:

图片

打开示例 ARCore Unity 项目

  1. 点击选择文件夹按钮。这将启动编辑器并加载项目。

  2. 在项目窗口中打开Assets/GoogleARCore/HelloARExample/Scenes文件夹,如图所示:

图片

打开场景文件夹

  1. 双击 HelloAR 场景,如图所示在项目窗口和前面的截图。这将把我们的 AR 场景加载到 Unity 中。

在任何时候,如果您在底部状态栏看到红色控制台或错误消息,这通常意味着您有一个版本冲突。您可能需要安装 Unity 的不同版本。有关更多帮助,请参阅第十一章,性能提示和故障排除

现在我们已经安装了 Unity 和 ARCore,我们将在下一节构建项目并将应用部署到 Android 设备。

在 Android 上构建和部署

在大多数 Unity 开发中,我们只需在编辑器中运行场景进行测试。不幸的是,当开发 ARCore 应用程序时,我们需要将应用部署到设备进行测试。幸运的是,我们正在打开的项目应该已经大部分配置好了。所以,让我们按照下一个练习中的步骤开始吧:

  1. 打开 Unity 编辑器到示例 ARCore 项目,并打开 HelloAR 场景。如果您在上一个练习中留下了 Unity 打开,请忽略此步骤。

  2. 通过 USB 连接您的设备。

  3. 从菜单中选择文件 | 构建设置。确认设置与以下对话框匹配:

图片

构建设置对话框

  1. 确认 HelloAR 场景已添加到构建中。如果场景缺失,请点击添加打开场景按钮将其添加。

  2. 点击构建和运行。请耐心等待,首次构建可能需要一些时间。

  3. 在应用被推送到设备后,您可以自由地测试它,就像您测试 Android 版本时那样。

太好了!现在我们有一个 Unity 版本的示例 ARCore 项目正在运行。在下一节中,我们将探讨如何远程调试我们的应用。

远程调试

每次都要连接 USB 来推送应用是不方便的。更不用说,如果我们想进行任何调试,我们就需要始终维护与我们的开发机器的物理 USB 连接。幸运的是,有一种方法可以通过 Wi-Fi 将我们的 Android 设备连接到我们的开发机器。使用以下步骤建立 Wi-Fi 连接:

  1. 确保设备通过 USB 连接。

  2. 打开命令提示符或 shell。

在 Windows 上,我们将只为我们在工作的提示符添加C:\Android\sdk\platform-tools到路径。建议您将此路径添加到您的环境变量中。如果您不确定这是什么意思,请谷歌搜索。

  1. 输入以下命令:
//WINDOWS ONLY
path C:\Android\sdk\platform-tools

//FOR ALL 
adb devices
adb tcpip 5555
  1. 如果成功了,您将看到重新启动 TCP 模式端口:5555。如果您遇到错误,请断开连接并重新连接设备。

  2. 断开您的设备。

  3. 按照以下步骤查找您的设备 IP 地址:

    1. 打开您的手机并转到设置,然后是关于手机。

    2. 点击状态注意记录 IP 地址。

  4. 返回您的 shell 或命令提示符并输入以下内容:

adb connect [IP Address]
  1. 确保您使用从您的设备记下的 IP 地址。

  2. 您应该看到连接到[IP 地址]:5555。如果您遇到问题,只需再次运行这些步骤。

测试连接

现在我们已经与我们的设备建立了远程连接,我们应该测试它以确保它正常工作。让我们通过以下步骤来测试我们的连接:

  1. 打开 Unity 到示例 AR 项目。

  2. 在层次结构窗口中展开 Canvas 对象,直到您看到 SearchingText 对象并选择它,就像以下摘录中所示:

图片

展示已选择 SearchingText 对象的层次结构窗口

  1. 将您的注意力转向默认位于右侧的检查器窗口。在窗口中向下滚动,直到您看到文本“正在搜索表面…”

  2. 将文本修改为“正在搜索 ARCore 表面…”,就像我们在上一章中为 Android 所做的。

  3. 从菜单中选择文件 | 构建和运行。

  4. 打开您的设备并测试您的应用。

远程调试运行中的应用程序

现在,通过这种方式构建和推送应用到您的设备将花费更长的时间,但这种方式要方便得多。接下来,让我们看看我们如何可以通过以下步骤远程调试运行中的应用程序:

  1. 返回您的 shell 或命令提示符。

  2. 输入以下命令:

adb logcat
  1. 您将看到覆盖屏幕的日志流,这不是非常有用的事情。

  2. Ctrl + C (command + C 在 Mac 上)来终止进程。

  3. 输入以下命令:

//ON WINDOWS
C:\Android\sdk\tools\monitor.bat

//ON LINUX/MACcd android-sdk/tools/
monitor
  1. 这将打开 Android 设备监控器。您应该在左侧的列表中看到您的设备。确保您选择它。您将在LogCat窗口中看到日志输出开始流式传输。将 LogCat 窗口拖动到主窗口中的一个标签页,如图所示:

显示 LogCat 窗口的 Android 设备监控器

  1. 保持 Android 设备监控器窗口打开并运行。我们稍后会回来。

现在我们可以远程构建、部署和调试。这将给我们提供足够的灵活性,以便我们想要变得更加移动。当然,我们通过 adb 设置的远程连接也将与 Android Studio 一起工作。然而,我们实际上还没有跟踪任何日志输出。我们将在下一节输出一些日志消息。

探索代码

与 Android 不同,我们能够轻松地在编辑器中修改我们的 Unity 应用,而无需编写代码。实际上,给定正确的 Unity 扩展,你可以在 Unity 中不写任何代码就制作出可工作的游戏。然而,对于我们来说,我们想要深入了解 ARCore 的细节,这需要编写一些代码。回到 Unity 编辑器,让我们看看我们如何通过实现以下练习来修改一些代码:

  1. 从层次窗口中选择 ExampleController 对象。这将使对象在检查器窗口中显示出来。

  2. 在 Hello AR Controller (Script) 旁边的齿轮图标上选择,然后从上下文菜单中选择编辑脚本,如下面的摘录所示:

在 Unity 中编辑脚本

  1. 这将打开你的脚本编辑器并加载脚本,默认情况下,MonoDevelop

Unity 支持许多用于编写 C# 脚本的集成开发环境(IDE)。一些流行的选项是 Visual Studio 2015-2017(Windows)、VS Code(所有)、JetBrains Rider(Mac)以及甚至 Notepad++(所有)。为了自己好,尝试为你操作系统列出的选项之一。

  1. 在脚本中向下滚动,直到你看到以下代码块:
public void Update ()
{
    _QuitOnConnectionErrors();
  1. _QuitOnConnectionErrors(); 代码行之后,添加以下代码:
Debug.Log("Unity Update Method");
  1. 保存文件,然后返回 Unity。Unity 将自动重新编译文件。如果你犯了任何错误,你将在状态栏或控制台看到红色错误消息。

  2. 从菜单中选择文件 | 构建和运行。只要你的设备仍然通过 TCP/IP 连接,这将有效。如果你的连接中断,只需回到上一节并重置它。

  3. 在设备上运行应用程序。

  4. 将注意力转向 Android 设备监控器,看看你是否能找到那些日志消息。

Unity 更新方法

Unity 的 Update 方法是一个在帧更新或渲染之前/期间运行的特殊方法。对于你典型的每秒 60 帧的游戏,这意味着 Update 方法将每秒被调用 60 次,所以你应该会看到很多标记为 Unity 的消息。你可以通过以下方式过滤这些消息:

  1. 跳转到 Android 设备监控器窗口。

  2. 在已保存的过滤器面板中单击绿色加号按钮,如下面的摘录所示:

添加新的标签过滤器

  1. 通过输入过滤器名称(使用 Unity)和日志标签(使用 Unity),创建一个新的过滤器,如前一张截图所示。

  2. 点击 确定以添加过滤器。

  3. 选择新的 Unity 过滤器。当应用在设备上运行时,您将看到针对 Unity 平台特定过滤的消息列表。如果您没有看到任何消息,请检查您的连接并尝试重新构建。确保您已在 MonoDevelop 中保存了您编辑的代码文件。

干得好。我们现在有一个带有远程构建和调试支持的正常工作的 Unity 设置,这无疑将使我们的工作更加容易。现在您已经设置好了一切,回到 Unity 平台并熟悉界面。尽量不要更改任何设置,因为我们将在后面的章节中使用示例项目作为我们的基础。

摘要

在本章中,我们为我们的 ARCore 开发设置了一个新的平台,称为 Unity。正如我们所学的,Unity 是一个领先、强大、灵活且简单的游戏/图形引擎,我们将在后面的章节中广泛使用它。然而,我们现在安装了 Unity 和 ARCore SDK for Unity。然后,我们通过设置使用 TCP/IP 通过 Wi-Fi 到设备的远程构建和调试连接进行了一点点偏离。接下来,我们通过添加一些调试日志输出测试了我们在 Unity 中修改 C# 脚本的能力。最后,我们使用 Android 设备监控工具测试了我们的代码更改,以过滤和跟踪部署到设备上的 Unity 应用程序的日志消息。

我们将在下一章继续努力,为我们的 Web ARCore 开发设置环境。Web ARCore 开发与 Android 和 Unity 实际上有很大不同。然而,我们仍将涵盖一些我们将用于 第十章,混合现实中的混合 的基本设置,所以即使您不做 Web 开发,也不要跳过下一章。

第四章:ARCore 在网页上

以前,大多数 AR 开发都需要在本地安装的应用中进行。然而,随着 ARCore 的出现,谷歌增加了对网页 AR 开发的支持,这使用户可以通过浏览器访问 AR 应用。当然,AR 网页应用可能永远不会像使用 Android 或 Unity 开发的类似应用那样强大或功能丰富。但 ARCore 扩展了其浏览器支持,包括 iOS 和 Android。因此,如果您需要一个跨平台 AR 应用,那么您可能需要专注于 ARCore 网页开发。

在本章中,我们继续为 ARCore 网页开发设置我们的环境。以下是本章我们将涵盖的主要主题:

  • 安装 WebARonARCore

  • 安装 Node.js

  • 探索示例

  • 在 Android 上调试网页应用

  • 3D 和 three.js

即使您对网页开发没有兴趣,您也应该阅读这一章。我们将在最终项目第十章,混合现实中的混合中使用这一章的内容。

安装 WebARonARCore

为了从网页上运行 ARCore,我们还需要一个支持 ARCore 或 ARKit 的浏览器。截至撰写时(beta 预览),没有浏览器支持 ARCore 或 ARKit,因此我们需要安装一个特殊或实验性浏览器。我们将安装的实验性浏览器称为 WebARonARCore。

截至撰写时,Google ARCore 处于 beta 预览阶段。如果 Google ARCore 已全面发布(1.0)并在您的设备上的浏览器中得到支持,那么您可以跳过这一部分。

安装 WebARonARCore 非常简单,只需在您的设备上打开浏览器并安装一个 APK。按照以下步骤安装 WebARonARCore:

  1. 将您的设备上的浏览器指向github.com/google-ar/WebARonARCore或直接在 Google 上搜索git WebARonARCore

  2. 按照 README 文件中的说明查找并点击 WebARonARCore APK 下载链接。这将下载 APK 到您的设备。如果您收到关于 APK 文件类型的警告,只需忽略它即可。

  3. 文件下载后,点击“打开”。如果您的设备设置为阻止从未知来源安装应用,您将收到警告。为了绕过警告,请执行以下操作:

    1. 点击“设置”。

    2. 点击“未知来源”以启用它。

  4. 点击“安装”以将 APK 安装到您的设备。

  5. 在您的设备上找到 WebARonARCore 应用并打开它。

  6. 点击“允许”,通过安全警告。

这将启动 WebARCore 实验性浏览器并将其指向我们从那里拉取 APK 的同一 GitHub 页面。请保持应用在您的设备上打开,因为我们将在接下来的部分中使用它。在下一节中,我们将学习如何安装 Node.js。

您可以通过安装 WebARonARKit 在 iOS 设备上测试您的网页开发。不幸的是,WebARonARKit 源代码必须手动构建、编译和部署。这些步骤在本书中没有涵盖,但如果您对为 iOS 设备设置感兴趣,请遵循 github.com/google-ar/WebARonARKit

安装 Node.js

与其他平台不同,我们不需要在设备上安装任何其他东西来使用 AR 网页应用。然而,我们确实需要一个方法来将我们的网页应用程序页面提供给设备。通常,这是通过一个网页服务器,如 IIS、Tomcat、Jetty、Node 或其他服务器来完成的。对于我们的目的,我们只需要一个简单的 HTTP 服务器来提供静态 HTML 内容。幸运的是,Node 提供了一个仅用于从文件夹运行简单 HTTP 服务器的包。为了获取此包,我们首先需要安装 Node。按照以下步骤安装 Node:

  1. Nodejs.org 下载并安装 Node.js 的 长期支持 (LTS) 版本。只需遵循页面上的说明和安装程序。确保在安装到 Windows 时设置 PATH

Node.js 是一个基于 Chrome 的 JavaScript 运行时之上的轻量级、非阻塞和事件驱动的 JavaScript 运行时。由于其庞大的模块或包库,它已经变得非常流行。我们安装 Node.js 只是为了使用 Node.js 包。

  1. 打开命令提示符或 shell,并输入以下内容:
npm
  1. 如果一切安装正确,你应该会看到一个显示 npm 用法的消息。

节点包管理器

Node 包管理器 (npm) 是一个用于安装 Node.js 包的命令行工具。我们将使用此工具下载并安装我们的简单 HTTP 服务器。按照以下步骤安装 HTTP 服务器:

  1. 从您的设备打开命令提示符或 shell,并输入以下内容:
npm install http-server -g
  1. 这将下载并安装 http-server 作为全局工具。现在,让我们测试它。

  2. 使用您的命令提示符或 shell,将文件夹更改为 Android,如下所示:

//WINDOWS
cd c:\Android
  1. Android 文件夹中运行 http-server,输入以下内容:
http-server -p 9999
  1. 你将看到一个端点 URL 的列表。选择一个与您的 Wi-Fi 相同子网、与您的设备相同的子网的端点。复制或写下端点的文本,如下面的摘录所示:

选择端点 URL

  1. 在您的设备上打开网页浏览器,并输入上一步骤中选择的端点。开始建立连接后,你将看到前面屏幕截图中显示的日志输出。

如果您无法与您的设备连接,请确保您输入了完整的端点,包括协议,例如示例中的 http://192.168.1.118:9999,但您的端点可能不同。确保您允许防火墙中端口 9999 的任何异常。或者,您可以关闭防火墙进行测试。只是不要将其关闭。

  1. 您应该在浏览器中看到Android文件夹的列表,因为我们已经配置了我们的服务器,只列出Android文件夹的内容。以下是在您的浏览器中它将看起来如何的示例:

浏览器显示 Android 文件夹列表

好的!现在我们有一种简单的方式来提供我们需要的任何静态网页。在下一节中,我们将下载 Web ARCore 示例并对其进行审查。

探索样本

现在我们有了支持 AR 的网页浏览器,我们可以继续探索一些示例。按照提到的步骤中的说明来探索样本:

  1. 打开命令提示符或 shell 到Android文件夹,并输入以下内容:
git clone https://github.com/google-ar/three.ar.js.git
  1. 确保您的http-server网页浏览器从Android文件夹中运行。如果您需要再次启动服务器,只需运行最后一个练习中的命令。

  2. 将您的设备上支持 Web AR 的浏览器(WebARCore)指向一个有效的端点 URL。再次检查最后一个练习,以防您忘记了如何操作。如果页面变黑或无响应,您可能需要重置应用程序。只需关闭 WebARCore 浏览器应用并重新启动。

  3. 浏览到three.ar.js/examples/文件夹。在这个文件夹中,您将找到使用three.jsthree.ar.js开发的 AR 应用程序的示例 HTML 页面集合。以下表格概述了每个示例,以及它们的功能描述:

页面 描述 概念
boilerplate.html 一个用于构建的简单项目 基础
graffiti.html 在 AR 中进行触摸交互和绘图 触摸,环境
record-at-camera.html 在一个点上录制 3D 空间音频 触摸,空间音频
reticle.html 跟踪表面的姿态 运动,姿态跟踪 – 环境
spawn-at-camera.html 在相机位置触摸生成对象 触摸,环境
spawn-at-surface.html 在识别的表面或平面上触摸生成对象 触摸,环境
surfaces.html 识别环境中的表面或平面 环境

在撰写本文时,这些是可用的示例。可能还会添加一些具有一些新功能或其他操作方式的新的样本。请确保检查您的文件夹,并花些时间探索每个样本。

  1. 在您的设备上浏览每个样本。这些样本是我们将在后续章节中涵盖的概念的优秀示例。

如果在运行 WebAR 浏览器时屏幕变黑,只需强制关闭应用并重新启动。通常发生的情况是 Chrome 的开发者工具DevTools)和应用程序失去了同步,只需要重新启动。

我们现在在我们的开发机器上运行一个 HTTP 服务器,为我们的设备提供 Web AR 应用。这很好,但我们如何编辑代码和调试呢?能够调试代码在我们开始编写新代码时也将对我们成功至关重要。因此,在下一节中,我们将学习如何设置远程 Web 调试到 Android 设备。

在 Android 上调试 Web 应用

如我们在上一节末尾提到的,当我们开始编写新代码时,调试/记录对我们来说将至关重要。如果你曾经尝试在没有日志或调试能力的情况下盲目修复问题,那么你很快就会欣赏到一个好的调试器的价值。碰巧的是,Chrome 有一个很棒的工具集,可以帮助我们做到这一点。按照以下步骤在你的 Android 设备上设置远程 Web 调试:

  1. 使用 USB 将您的设备连接到您的计算机。

  2. 打开命令提示符窗口。

  3. 通过输入以下内容来验证您的连接:

adb devices
  1. 那个命令的输出应该显示你的连接设备。你可能可以绕过这一步,但通过运行这个简单的检查,你可以避免以后的很多挫折。

  2. 确保您的 Android 设备上所有 Chrome 浏览器实例都已关闭。

  3. 打开 WebARCore 浏览器的一个实例。记住,这个浏览器只是 Chrome 的一个实验性扩展。

  4. 使用打开的浏览器导航到一个示例。现在还真的不重要。这个例子将使用 spawn-at-camera.html

连接 Chrome 开发者工具

所以,信不信由你,我们现在已经连接并准备好调试了。现在,我们只需要在开发机器上设置我们的调试工具:

  1. 在你的开发机器上打开 Chrome。如果你没有安装 Chrome,你需要做这个。当然,如果你正在阅读一本关于 Google ARCore 的书,你很可能已经安装了 Chrome,对吧?

  2. 通过按 command + option + I(Mac),Ctrl + Shift + I(Windows、Linux)或从菜单中选择更多工具 | 开发者工具来打开 Chrome 开发者工具。

  3. 从 Chrome 的开发者工具菜单中选择更多工具 | 远程设备,如图所示:

图片

定位远程调试菜单选项

  1. 将会打开一个新的标签页,远程设备,并应显示你的连接设备,如下所示:

图片

显示连接设备和页面的远程设备标签

  1. 在标签页底部,你应该能看到您当前在设备上指向的地址。如果不是这样,可能有一个文本框允许您手动输入它,然后连接。

  2. 点击检查按钮。这将打开一个新的 Chrome 窗口,一侧是开发者工具,另一侧是您的设备图像。

使用 Chrome 调试

到目前为止,如果你有使用 Chrome DevTools 的经验,你可以开始调试了。当然,如果你对这一切都相对陌生,请按照以下步骤学习如何在 DevTools 中调试:

  1. 切换到上一节中我们打开的 Chrome 窗口。

  2. 点击 DevTools 窗口的“源”标签。

  3. 选择spawn-at-camera.html或你在测试中使用的文件。

  4. 在 HTML 和 JavaScript 中向下滚动,直到看到onClick()函数。

  5. 点击行号 229(示例中为 229,但你的可能不同),在突出显示的代码左侧设置断点。这也在以下摘录中演示:

图片

设置 JavaScript 断点

  1. 切换回运行应用的设备。触摸屏幕以生成一个对象。当你这样做时,你的应用应该在顶部显示“调试器暂停”消息,然后优雅地冻结。

  2. 切换回你的开发机器和开发者工具窗口。你会看到应用在断点处暂停。现在,你可以使用鼠标悬停在代码上以检查变量和任何其他你可能正在调试的内容。

随意探索设置其他断点,甚至逐步执行代码。我们将把探索更多 DevTools 功能留给读者自行探索。

现在你可以远程调试在设备上运行的 AR Web 应用。这也完成了我们的大部分初始基本设置。我们现在可以深入了解在 3D 中与 AR 合作,从下一节开始。

3D 和 three.js

我们生活在一个三维世界中。因此,为了让我们用户相信他们的现实正在被增强或改变,我们需要以三维的方式与他们所处的世界合作。现在,我们正在合作的每个平台(Web、Android 和 Unity),都有我们将要使用的 3D 引擎。在 Unity 的情况下,它是 3D 引擎,毫无疑问,它是最容易使用的,几乎不需要编程或数学知识。Android 和 OpenGL ES 是第二选择,因为它将需要一些 3D 数学知识。第三和最后一个选择是我们用于 Web 的 3D 引擎,它将是three.js库。three.js将是使用 3D 时最难的平台,这使得它成为我们理想的起点。

如我们在第一章“入门”中提到的,Unreal 平台是另一个 ARCore 平台选项。Unreal 在提供用于 3D 工作的优秀工具方面与 Unity 相似,尽管这些工具更技术化,并且需要理解 3D 数学才能成功。

与前几章不同,我们不会仅仅进行简单的文本更改来测试我们更改和部署代码的能力。相反,在本节中,我们将修改我们生成的 3D 对象。这将是一次深入 3D 的好机会,并为我们准备本书的其余部分。让我们按照以下步骤开始:

  1. 使用记事本、Notepad++、vi 或其他文本编辑器打开位于Android/three.ar.js/example文件夹中的spawn-at-camera.html文件。

  2. 在代码中向下滚动,直到看到以下部分:

var geometry = new THREE.BoxGeometry( 0.05, 0.05, 0.05 );
var faceIndices = ['a', 'b', 'c'];
for (var i = 0; i < geometry.faces.length; i++) 
{
  var f  = geometry.faces[i];
  for (var j = 0; j < 3; j++) 
{
    var vertexIndex = f[faceIndices[ j ]];
    f.vertexColors[j] = colors[vertexIndex];
  }
}
var material = new THREE.MeshBasicMaterial({ vertexColors:       
                                           THREE.VertexColors });
  1. 将整个代码部分注释掉或删除。使用 // 将一行转换为注释。

  2. 在高亮行之前输入新代码:

var geometry = new THREE.TorusGeometry( 10, 3, 16, 100 );
var material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
cube = new THREE.Mesh(geometry, material);
  1. 这第一行新代码将几何形状替换为环面。TorusGeometry 是一个用于创建环面的辅助函数。还有许多其他辅助函数用于创建许多其他几何形状或甚至加载网格对象。第二行创建了一个新的基本单色材质。然后,它将这个材质包裹在几何形状周围,并创建我们的对象(网格),现在我们仍然将其称为立方体。如果你觉得需要更改变量名,那么请随意进行更改,但请务必小心。

  2. 保存你的代码更改。

  3. 切换回你的设备并刷新页面。然后,轻触屏幕以生成新对象。一开始,你可能认为没有任何效果;走开并四处移动。你可能会看到一个非常大的亮黄色环面的边缘。如果你仍然有一些问题,只需确保你已保存更改并尝试重新连接一切。

在这一点上,我们有一系列问题需要理解和解决,如下列所示:

  • 物体太大或比例不合适

  • 物体的方向或旋转错误

  • 需要将物体移动或变换到摄像头正前方

  • 我们想要改变颜色

理解左手系或右手系坐标系统

虽然对 3D 数学的良好理解当然会有所帮助,但它并不是完全必要的。目前你所需要知道的是,我们使用 x、y 和 z 的通用符号定义三维(3D)中的对象,其中 x 是沿x轴的位置,y 是沿y轴,z 是沿z轴。此外,我们使用左手系或右手系坐标系统这个术语来定义这些轴的位置,如下面的图所示:

图片

左手系和右手系坐标系统的定义

按照前面的图示,举起你的左手,并将你的中指指向屏幕。现在,你的大拇指指向正 x 轴,食指指向正 y 轴,中指指向正 z 轴。很多时候,为了避免左手系或右手系之间的混淆,我们只需用它们指向的方向来表示轴。因此,右用于正 x 轴,上用于正 y 轴,前用于正 z 轴。幸运的是,对于我们的所有平台,我们将使用左手系坐标系统。

3D 缩放、旋转和变换

我们接下来需要理解的是如何将缩放、旋转和变换应用于对象,从而解决我们确定的问题。不深入数学,让我们只理解这些术语的含义:

  • 缩放:它允许我们定义对象的大小。在我们的例子中,我们的对象太大,因此我们需要缩小对象。我们很快就会学习如何做到这一点。

  • 旋转:它定义了物体的方向或姿态。我们将这两个术语交替使用。旋转稍微复杂一些,我们在这个例子中不会担心它。

  • 变换:它定义了物体的位置,其中位置 0,0,0 代表原点。在我们的例子中,我们希望将环面稍微放置在相机前方。

我们使用一个称为矩阵的数学概念来在三维空间中对三维物体应用缩放、旋转和变换操作。矩阵的酷之处在于它们可以同时表示缩放、旋转和变换的所有三种操作。然而,这也意味着我们必须注意这些操作的顺序。让我们回到代码中,看看我们如何将每个操作应用到我们的环面上:

  1. 打开你的文本编辑器到spawn-at-camera.html示例。

  2. 滚动到高亮显示的代码,并在其后输入以下行:

scene.add(clone); //near the bottom of the file
clone.scale.copy(new THREE.Vector3(.15,.15,.15));
clone.position.copy(new THREE.Vector3(0,0,10));
  1. 将下面那行代码注释掉,就像这样:
//clone.position.copy(pos);
  1. 保存你的工作,并在你的设备上运行应用程序。你现在可以看到我们如何移动和缩放生成的对象。你可以随意尝试进一步移动、缩放甚至旋转对象。

至于将颜色从那刺眼的黄色改为更吸引人的颜色,我们留给读者作为他们的家庭作业。以下是需要更改的代码行:

var material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );

如果你在上一个部分中遇到了任何困难,你真的应该找一本书,阅读博客/维基百科,或者参加 3D 和/或 3D 数学的课程。学习 3D 概念的另一个好方法是使用 Blender、SketchUp、Max 等 3D 建模软件。

当然,我们将在本书中以及在后几章中更详细地介绍更多的 3D 概念。不过,如果你是第一次接触 3D 编程,欢迎加入我们,并准备好迎接一段颠簸的旅程。

摘要

在本章中,我们完成了我们在后续章节中将要探索的 ARCore 环境的主要设置任务中的最后一个。我们首先跳进去安装了预置的 AR 启用实验性 Chrome 浏览器。然后,我们下载并安装了 Node.js,作为运行简单 HTTP 服务器的需求。这使得我们能够将three.ar.js源中的示例拉到我们的本地机器上。然后,我们使用 HTTP 服务器为我们设备上的样本 AR 网络启用应用程序提供服务。接下来,我们解决了远程调试 JavaScript 代码到 Android 设备的问题。在那之后,我们简要地游览了 3D 世界,并探讨了在 AR 场景中缩放和变换 3D 对象的方法。然后,我们最终了解到,对 3D 概念和/或数学的良好了解对于我们的 AR 开发者成功至关重要。

现在我们完成了基本的设置任务,是时候开始构建我们自己的 AR 应用程序了。在下一章中,我们将探索使用我们的网络平台进行运动跟踪的 AR 概念。

第五章:实际世界运动跟踪

现在我们已经设置并准备好了所有有趣的东西,我们可以开始构建一些实际的 AR 应用。为了做到这一点,我们将从样本中挑选和选择我们需要的各种组件。样本是很好的例子,但大部分只是样板代码。这意味着我们没有理由重写已经运行良好的代码部分。相反,我们将专注于添加新的代码来解决 AR 问题。在本章中,我们将深入了解 ARCore 运动跟踪的工作原理。我们将学习 ARCore 运动跟踪的当前局限性,并开发一种克服这些局限性的技术。以下是本章我们将涵盖的主要主题:

  • 深度运动跟踪

  • 3D 声音

  • Resonance Audio

  • 使用 Firebase 的跟踪服务

  • 可视化跟踪运动

为了成功完成本章的练习,读者需要完成到第四章,ARCore on the Web的设置。回顾该章节的一些练习可能会有所帮助。

深度运动跟踪

ARCore 使用称为视觉惯性里程计VIO)的算法来实现运动跟踪。VIO 结合了从设备摄像头识别图像特征与内部运动传感器,以跟踪设备相对于起始点的方向和位置。通过跟踪方向和位置,我们能够理解设备在 6 个自由度中的位置,或者我们经常所说的设备/物体的姿态。让我们看看以下图中姿态的样子:

图片

6 自由度,姿态

我们在识别物体在 3D 空间中的位置和方向时,会频繁使用术语“姿态”。如果你还记得第四章,ARCore on the Web,姿态也可以用一种称为矩阵的数学符号来表示。我们还可以提到一种特殊形式的复数数学中的旋转,称为四元数。四元数允许我们以简单形式定义 3D 旋转的所有方面。再次强调,我们不会担心这里的特定数学;我们只是提及它的用法。

如果我们能通过修改后的 ARCore 样本看到它是如何工作的,可能会更有帮助。在文本编辑器中打开Android/three.ar.js/examples文件夹中的spawn-at-surface.html示例,并按照给定的步骤进行操作:

  1. 滚动或搜索update函数。

  2. 定位以下代码行:

camera.updateProjectionMatrix();
  1. 在高亮显示的行之后添加以下代码行:
var pos = camera.position;
var rot = camera.rotation;
console.log("Device position (X:" + pos.x + ",Y:" + pos.y + ",Z:" + pos.z + ")");
console.log("Device orientation (pitch:" + rot._x + ",yaw:" + rot._y + ",roll:" + rot._z + ")");
  1. 保存文件。我们添加的代码只是将摄像头的位置和方向(旋转)提取到一些辅助变量中:posrot。然后,使用console.log函数将这些值输出到控制台。碰巧的是,摄像头也代表了设备的视图。

  2. 打开命令提示符或 shell 窗口。

  3. 在你的android文件夹中通过输入以下命令启动http-server

cd /android
http-server -d -p 9999
  1. 启动 Chrome 调试工具并远程连接到你的设备。

  2. 使用设备上的 WebARCore 浏览器应用打开spawn-at-surface.html文件。

  3. 切换回 Chrome 工具并点击检查。

  4. 等待新窗口打开并点击控制台。在运行 AR 应用(spawn-at-surface.html)的同时移动你的设备,你应该会看到控制台标签更新,显示有关设备位置和方向的消息。以下是如何显示的示例:

图片

显示设备位置和方向跟踪的输出控制台

我们在这个例子中添加的代码跟踪摄像机,它恰好代表了 AR 应用中通过设备投影的视图。我们称摄像机为 3D 场景的视图。一个 3D 场景可以有多台摄像机,但在 AR 中,我们通常只使用一台。以下是我们如何定义 3D 中的摄像机或视图投影的示意图:

图片

3D 摄像机的视图视锥体

摄像机的主要任务是投影或展平 3D 虚拟对象到 2D 图像中,然后该图像在设备上显示。如果你在spawn-at-surface.html文件的中间附近滚动,你会看到以下代码,它为场景创建摄像机:

camera = new THREE.ARPerspectiveCamera(
    vrDisplay,
    60,
    window.innerWidth / window.innerHeight,
    vrDisplay.depthNear,
    vrDisplay.depthFar
  );  

在这里,vrDisplay是设备的实际摄像机,60代表视野,window.innerWidth / window.innerHeight代表宽高比vrDisplay.depthNearvrDisplay.depthFar代表近平面和远平面的深度距离。近、远平面以及视野共同构成了视图视锥体。视锥体内的所有对象都将被渲染。你可以自由尝试更改这些参数,看看它们在运行应用时对场景视图有什么影响。

在这个设置中,我们使用 60 度的视野来给场景中的对象提供更自然的视角。你可以自由地尝试更大的和更小的角度,看看这对场景对象有什么视觉效果。

现在我们对如何在场景周围跟踪我们的设备有了更好的理解,我们将扩展我们的例子。在下一节中,我们将介绍 3D 空间声音。

3D 声音

3D 声音是我们向听众施加的另一种错觉,以进一步欺骗他们相信我们的虚拟生成世界是真实的。实际上,3D 声音多年来在电影、电视和当然,视频游戏中被广泛使用,以欺骗听众获得更沉浸式的体验。例如,在电影中,听众是静止的,因此可以通过设置多个扬声器来模仿 3D 声音。然而,在 AR 或 VR 移动应用中,声音需要来自单个(单声道)或双声道(立体声,耳机)源。幸运的是,许多聪明的人通过使用称为双耳声音的技术来了解我们人类耳朵的听觉方式,从而在 3D 中绘制声音。下一张图将更详细地介绍双耳音频的工作原理:

3D 声音可视化

自那以后,我们不仅弄清楚了如何录制双耳音频,还弄清楚了如何回放它,从而让我们能够播放让大脑误以为声音来源与现实不同的声音。然而,目前的大多数技术都假设用户是静止的,但在 AR 应用中,当然并非如此。在 AR 应用中,我们的用户(听众)在我们的虚拟世界中移动,这意味着听众周围的 3D 声音也需要调整。幸运的是,谷歌再次伸出援手,为 AR 和 VR 开发了一个 3D 声音 API,称为Resonance Audio。我们将在下一节中更深入地探讨 Resonance Audio 及其使用方法。

Resonance Audio

谷歌开发了 Resonance Audio 作为开发者工具,用于在他们的 AR 和 VR 应用中包含 3D 空间音频。我们将使用这个工具在我们的演示应用中添加 3D 声音。让我们开始吧,首先在您喜欢的文本编辑器中打开spawn-at-surface.html文件,然后按照给定的步骤进行:

  1. 定位到 JavaScript 的开始部分,并在变量声明中添加以下行:
var cube***;** * //after this line
var audioContext;
var resonanceAudioScene;
var audioElement;
var audioElementSource;
var audio;
  1. 现在,向下滚动到update函数之前,开始一个新的函数,称为initAudio,如下所示:
function initAudio(){

}

function update(){ //before this function
  1. 接下来,我们需要初始化一个AudioContext,它代表设备的立体声音。在initAudio函数中,输入以下内容:
audioContext = new AudioContext();
  1. 然后,我们在Resonance中设置音频场景,并通过添加以下内容将双耳音频输出到设备的立体声音频输出:
resonanceAudioScene = new ResonanceAudio(audioContext); 
resonanceAudioScene.output.connect(audioContext.destination);
  1. 之后,我们通过添加以下代码为用户周围的虚拟空间定义一些属性:
let roomDimensions = {   width: 10, height: 100, depth: 10 };
let roomMaterials = {
   // Room wall materials
   left: 'brick-bare',
   right: 'curtain-heavy',
   front: 'marble',
   back: 'glass-thin',
   // Room floor
   down: 'grass',
   // Room ceiling
   up: 'transparent' };
  1. 如您所见,这里有很多灵活性来定义任何您想要的room。在这个例子中,我们描述了一个房间,但那个房间也可以描述为一个户外空间。底部有一个关于向上方向的例子,其中使用了透明选项。透明意味着声音会穿过虚拟墙的那个方向,您可以通过将所有方向设置为透明来表示户外。

  2. 现在,我们通过写入以下内容将room添加到音频场景中:

resonanceAudioScene.setRoomProperties(roomDimensions,       
                                      roomMaterials);
  1. 现在,room已经完成,让我们通过输入以下内容添加音频源:
audioElement = document.createElement('audio');
audioElement.src = 'cube-sound.wav'; 

audioElementSource = audioContext.createMediaElementSource(audioElement);
audio = resonanceAudioScene.createSource();
audioElementSource.connect(audio.input);
  1. audioElement是与 HTML audio标签的连接。本质上,我们在这里所做的就是用通过共振路由的音频替换 HTML 的默认音频,为我们提供空间声音。

  2. 最后,当我们生成盒子并播放声音时,我们需要添加我们的audio对象。在onClick函数中THREE.ARUtils.placeObjectAtHit函数调用之后输入给定的代码:

audio.setPosition(cube.position.x,cube.position.y,cube.position.z);

audioElement.play();

在我们运行示例之前,我们需要下载cube-sound.wav文件并将其放入我们的示例文件夹中。打开你下载本书源代码的文件夹,并将文件从Chapter_5/Resources/cube-sound.wav复制到你的Android/three.ar.js/examples文件夹中。

双耳立体声之所以得名,是因为我们用两只耳朵听声音。为了从本章的音频示例中获得最大收益,请确保你戴上立体声耳机。你将能够用你设备的单声道扬声器听到一些差异,但没有耳机就不会一样。

现在你准备好运行应用程序时,保存spawn-at-surface.html页面,启动你的设备,关闭并重新打开 WebARCore 应用程序。在应用程序中玩耍,并通过轻触表面生成一个盒子。现在当盒子生成时,你会听到立方体声音。在场景中四处移动,看看声音是如何移动的。

不是你预期的结果?没错,声音仍然随着用户移动。那么问题是什么?问题是我们的音频场景和 3D 对象场景在两个不同的虚拟空间或维度中。以下是一个希望进一步解释这一点的图表:

图片

音频和虚拟 3D 对象空间中的差异

我们遇到的问题是我们的音频空间随着用户移动。我们想要做的是将音频空间与我们的相机相同的参考系对齐,然后移动听众。现在,这听起来可能像是一项大量工作,如果不是因为 ARCore,那么很可能就是这样。所以,幸运的是,我们可以在之前放入的那几个控制台行之后添加一行代码来实现这一点,就像这样:

  1. 找到我们在上一节中添加的两个console.log行,并像这样注释掉:

如果你省略了前面的部分,你需要回去完成它。本节中使用的代码需要它*。

//console.log("Device position (X:" + pos.x + ",Y:" + pos.y + ",Z:" + pos.z + ")");
//console.log("Device orientation (pitch:" + rot._x + ",yaw:" + rot._y + ",roll:" + rot._z + ")");
  1. 添加我们新的代码行:
audio.setPosition(pos.x-cube.position.x,pos.y-cube.position.y,pos.z-cube.position.z);
  1. 这行代码所做的只是调整音频位置相对于用户(相机)。它是通过减去位置向量的XYZ值来实现的。我们也可以同样容易地减去向量。

  2. 再次运行示例。生成一些盒子并四处移动。

注意,当你放置一个盒子并四处移动时,声音会改变,正如你所期望的那样。这是由于我们能够跟踪用户在相对于虚拟声音的 3D 空间中的位置。在下一节中,我们将探讨通过设置跟踪服务来扩展我们跟踪用户的能力。

使用 Firebase 的跟踪服务

现在,能够跟踪用户的动作固然很好,但如果我们想要跨应用或同时跟踪多个用户呢?这将需要我们编写服务器,设置数据库,创建模式等等,这当然不是一件容易的事情,也不能在仅仅一章中轻易解释。然而,如果有一个更简单的方法呢?好吧,确实有,而且,又是 Google 带着 Firebase 来帮助我们。

Firebase 是一个使用简单且跨平台的优秀应用工具和存储服务集合。我们将使用 Firebase 数据库,一个实时数据库服务,来跟踪用户的位置。打开一个网络浏览器并按照以下步骤操作:

  1. 浏览到 firebase.google.com

  2. 点击 GET STARTED 按钮。

  3. 使用你的 Google(Gmail)账户登录。如果你没有,是的,你需要创建一个账户才能继续。

  4. 点击 Add project 按钮。

  5. 将你的项目命名为ARCore并选择你自己的国家/地区,如以下摘录所示:

设置 ARCore 项目

  1. 点击 CREATE PROJECT。这将创建你的项目并打开 Firebase 控制台。

  2. 点击 Add Firebase to your web app,它可以在项目概览页面的顶部找到。这将打开一个类似于以下对话框:

复制你项目的设置代码

  1. 点击 COPY。这应该会将两个脚本标签及其内容复制到你的剪贴板中。

如果你看到的密钥和 URL 不同,不用担心;它们应该是不同的。

  1. 在你喜欢的文本编辑器中打开spawn-at-surface.html文件。滚动到最后一行<script>标签之前,即带有大块代码的那一行。粘贴你之前复制的代码(Ctrl + Vcommand + V 在 Mac 上)。

设置数据库

这样,我们就已经设置了 ARCore Firebase 项目。现在我们想要创建我们的实时数据库并为我们设置连接。返回 Firebase 控制台并按照以下步骤设置数据库:

  1. 关闭我们之前留下的配置对话框。

  2. 点击左侧菜单中的 Database。

  3. 点击 GET STARTED。这将创建一个带有默认安全设置开启的 Firebase 实时数据库。在这个阶段,我们实际上不需要身份验证,所以让我们将其关闭。

  4. 点击 RULES 标签页。默认的安全规则是用 JSON 定义的。我们想要更改它,以便我们的数据库具有公开访问权限。将 JSON 替换为以下内容:

{  "rules": {    ".read": true,    ".write": true  }}
  1. 点击 PUBLISH。你现在应该会看到一个以下安全警告:

打开公共访问后的安全警告

  1. 点击 DATA 标签页。保持这个标签页和浏览器窗口打开。

关闭安全设置适用于开发原型。然而,一旦您超出原型,您需要重新启用安全设置。未能这样做可能会给您带来各种痛苦和您可能无法想象的事情。

测试连接时间

信不信由你,我们的实时数据库服务正在运行;现在我们只想通过从我们的 AR Web 应用向数据库写入单个值来测试我们的连接。在文本编辑器中打开spawn-at-surface.html,并按照以下步骤操作:

  1. 滚动到我们之前添加的 Firebase 脚本。在最后一行之后添加以下代码:
var database = firebase.database();
  1. 上一行代码创建了对数据库的引用。现在,让我们使用以下代码设置一些数据:
firebase.database().ref('pose/' + 1).set({x: 12,y: 1,z: 0});
  1. 保存文件。

您可以在本书下载的源代码中的Chapter_5/Examples部分找到spawn-at-surface.html页面的各种版本。

  1. 使用 http://localhost:9999/three.ar.js/examples/spawn-at-surface.html URL 在您的桌面浏览器上运行页面。在这个阶段,我们只是在页面开始时设置一个数据点作为测试,因此我们不需要 AR。当然,确保在运行任何测试之前启动http-server

  2. 页面加载后,您将看到 ARCore 警告信息,但不必担心,这只是一个对实时数据库服务的测试。

  3. 返回我们之前留下的 Firebase 控制台 (console.firebase.google.com/u/0/?pli=1) 窗口。确保您正在查看数据库页面和 DATA 选项卡,如下所示:

图片

检查在 Firebase 数据库上设置的数据

  1. 展开姿态及其子对象,如前文摘录所示。如果一切正常,您应该看到我们为模拟姿态(位置)设置的值。

我们现在有一个服务在位,能够跟踪我们想要跟踪的任何数据。Firebase 允许我们在实时中对我们的数据和模式进行建模,这在原型设计中非常有用。它还有额外的优势,即免费、公开,并且可以从我们稍后将要工作的其他平台访问。在下一节中,我们将通过实时跟踪用户来使用我们的跟踪服务。

可视化跟踪运动

现在我们已经了解了如何跟踪运动并且有一个服务在位,让我们看看我们如何使用这个服务并在我们的 AR 应用中可视化跟踪数据。在文本编辑器中打开spawn-at-surface.html页面,并按照以下步骤操作:

  1. 找到我们上次练习中添加的最后一行代码并将其删除:
firebase.database().ref('pose/' + 1).set({x: 12,y: 1,z : 0}); //delete me
  1. 将该行替换为以下代码:
var idx = 1;
setInterval(function(){
 idx = idx + 1;
 if(camera){
  camera.updateProjectionMatrix();
  var pos = camera.position;
  var rot = camera.rotation;
  firebase.database().ref('pose/' + idx).set({x: pos.x,y: pos.y,z : pos.z, roll: rot._z, pitch: rot._x, yaw: rot._y });
 }  }, 1000);
  1. 前一个代码片段中的第一行是设置索引或计数变量。然后,我们使用setInterval函数设置一个每秒(1000 毫秒)调用匿名函数的重复计时器。我们这样做是为了确保我们每秒只追踪一次移动。我们当然可以在像多人游戏那样的每一帧追踪移动,但现阶段,一秒就足够了。其余的代码,你已经在之前的练习中见过。

  2. 保存文件。

  3. 在你的浏览器设备上运行示例。现在,用设备移动。

  4. 前往 Firebase 控制台。你现在应该会看到一个数据流被输入到数据库中。你可以自由地扩展数据点,并查看捕获的值。

太好了,我们现在可以看到我们的数据正在被收集。当然,除非我们能将其在 2D 或 3D 中可视化,否则对我们人类来说,要轻松理解这些数据是有点困难的,这意味着我们有几种选择。我们可以构建一个单独的网页来跟踪地图上的用户。但这听起来更像是一个标准的网络练习,所以我们把它留给有此兴趣的读者。相反,我们将使用发送到我们数据库的相同数据来绘制用户旅行的 3D 路径。再次打开那个文本编辑器并加载spawn-at-camera.html以继续:

  1. 定位到我们在上一个练习中添加的setInterval函数调用。我们需要更改一些代码来创建从点到线的线条。

  2. 在识别的行之后输入以下代码:

firebase.database().ref('pose/' + ... //after this lineif(lastPos){   
  var material = new THREE.LineBasicMaterial({ color: 0x0000ff   });
  var geometry = new THREE.Geometry();
  geometry.vertices.push(
     new THREE.Vector3( pos.x, pos.y, pos.z ),
   new THREE.Vector3( lastPos.x, lastPos.y, lastPos.z )
  );
  var line = new THREE.Line( geometry, material );
  scene.add( line );
}
lastPos = { x: pos.x, y: pos.y, z: pos.z};
  1. 此代码首先检查lastPos是否已定义。在setInterval计时器循环的第一次运行中,lastPos将是未定义的;它随后在if语句之后被设置。然后,在lastPos被定义后,我们通过调用THREE.LineBasicMaterial并传入十六进制颜色值来创建一个基本的material线条。接下来,我们使用当前的poslastPos变量以及material创建我们的geometry,即一条线。我们这样做是通过首先使用每个位置xyz的值构造一个Vector3对象。最后,我们通过调用scene.add(line)line添加到场景中。

向量不过是有序数字的集合,其中每个数字代表一个维度。向量有许多有趣的数学特性,了解这些特性是有用的。然而,现在,我们可以将Vector3视为在xyz坐标上表示 3D 空间中的一个点。我们使用术语顶点来指代线、表面或网格上的向量或点。

  1. 保存文件,并在你的设备上的 WebARCore 浏览器中运行它。现在当你移动时,你会看到一条蓝色线条的轨迹跟随你,如下面的图片所示:

图片

示例显示追踪路径为蓝色线条

随意继续使用这个应用。当开发一个简单的单页网页应用时,开发周期(构建、部署和运行)非常快,这为你提供了很多快速更改、运行并轻松调试的机会。

练习

在每一章的结尾或接近结尾的地方,将有一个练习部分来测试你的知识,并让你在 ARCore 上获得更多经验。请独立完成以下练习:

  1. 将追踪线的颜色从蓝色改为红色,或另一种颜色。

  2. 将直线段替换为SplineCurve。提示——你需要跟踪多个之前的位置。

  3. 让立方体和/或音频沿着追踪路径跟随用户。提示——你可以使用另一个setInterval定时器函数,每 1.1 秒(1100 毫秒)沿着路径移动盒子。

摘要

有了这些,我们完成了对 ARCore 运动追踪的探讨。正如我们所学的,ARCore 通过使用与设备运动传感器相关的特征识别来给我们提供跟踪位置和旋转或设备姿态的能力。然后我们学习了为什么在构建带有 3D 声音的 AR 应用时跟踪用户的位置很重要。这教会了我们我们的音频和虚拟(3D)场景之间的区别以及如何进行转换。然后我们通过设置 Firebase 实时数据库并连接到我们的 AR 应用来扩展跟踪用户的能力。通过这样做,我们现在可以全球范围内跟踪单个用户或多个用户。当然,我们没有足够的时间在这里进一步构建。现在,我们通过在设备在区域内移动时绘制用户的旅行路径来完成应用。

在下一章中,我们将回到使用 Android(Java)的工作,学习更多关于环境理解和各种相关 3D 概念,这是基础 AR 主题列表中的下一个主题。

第六章:理解环境

增强现实应用程序都是关于增强或扩展用户的现实。为了做到这一点,作为 AR 应用程序开发者,我们需要一套能够理解用户环境的工具。正如我们在上一章中看到的,ARCore 使用 视觉惯性里程计VIO)来识别环境中的对象和特征,然后它可以利用这些信息来获取设备的姿态并跟踪运动。然而,这项技术也可以帮助我们使用相同的工具包来识别对象及其姿态。在本章中,我们将探讨如何使用 ARCore API 更好地理解用户的环境。以下是本章我们将涵盖的主要主题的简要概述:

  • 跟踪点云

  • 网格化和环境

  • 与环境交互

  • 使用 OpenGL ES 绘制

  • 着色器编程

如果你还没有从 GitHub 下载源代码,你需要为这一章这样做。当然,你还需要完成 第二章 中涵盖的 Android 设置和安装,即 ARCore on Android

跟踪点云

正如我们讨论的那样,ARCore 中的运动跟踪是通过识别和跟踪用户周围的可见特征来完成的。然后,它使用这些点以及设备的方向和加速度计传感器来保持跟踪更新。如果不这样做,准确跟踪的能力会迅速下降。此外,我们还获得了跟踪 ARCore 识别为对象点的多个点的优势。让我们通过再次启动示例 ARCore Android 应用程序来查看这些跟踪点的外观。按照以下步骤开始:

  1. 打开 Android Studio。如果你还没有打开其他项目,那么它应该会立即加载 Android ARCore 示例项目。如果不是这样,请在 Android/arcore-android-sdk/samples/java_arcore_hello_ar 文件夹中加载项目。

  2. 打开 HelloArActivity.java 文件,并向下滚动到 OnDrawFrame 方法,如下面的摘录所示:

图片

在 Android Studio 中打开 HelloArActivity.java 文件

  1. OnDrawFrame 是渲染方法,就像我们在网络示例中看到的 update 函数一样。这个方法每帧被调用一次,在典型的 3D 应用程序中,通常每秒大约 60 帧。我们也将 60 fps 称为帧率。帧率将根据你每帧执行的操作量而变化。因此,我们希望我们的 render 函数和其中的代码尽可能快。我们将在 第十一章 中更多地讨论性能和渲染,性能提示和故障排除

  2. 在此方法的第一行,从 GLES20.glClear 开始,清除渲染缓冲区并准备绘图。

根据你正在使用的 3D 平台,你可能不需要担心一些特定的细节,比如清除渲染缓冲区。例如,Unity 会隐藏许多这些细节,对开发者来说这可能既有好的一面也有不好的一面。只需理解所有 3D 平台通常都会遵循相同的原理。

  1. 向下滚动一点,直到try块内部,并添加以下行:
Frame frame = mSession.update();
  1. Frame代表从设备摄像头捕获的当前 AR 视图。我们通过调用mSession.update()来获取frame的实例;mSession是在之前初始化的,代表我们的 ARCore 会话服务。

  2. Frame还公开了一些辅助方法;向下滚动直到你看到以下行:

mPointCloud.update(frame.getPointCloud());
mPointCloud.draw(frame.getPointCloudPose(), viewmtx, projmtx);
  1. mPointCloud.update()开始,这个调用获取当前frame中的可见点。然后,mPointCloud.draw()根据云的姿态绘制点,使用当前的视图(viewmtx)和投影(projmtx)矩阵。

视图和投影矩阵代表相机或组合场景视图。使用three.js时,这些由我们处理。同样,当我们到达 Unity 时,我们也不需要担心设置这些矩阵。

  1. 将你的设备连接到你的机器,无论是通过 USB 还是远程连接。然后,在你的设备上构建并运行应用。特别注意点云的绘制。

注意,当你保持设备在一个方向上更长的时间时,点的数量会增加。这些点代表用于跟踪和解释环境的可识别和可识别的特征点。这些点将帮助我们识别环境中的物体或表面。在下一节中,我们将探讨如何识别和渲染表面。

网格化和环境

因此,能够识别物体的特征或角点实际上只是我们想要了解用户环境信息的起点。我们真正想要做的是利用这些特征点来帮助我们识别平面、表面或已知物体及其姿态。ARCore 通过一种称为网格化的技术自动为我们识别平面或表面。我们已经多次在高级示例中看到网格化是如何工作的,当 ARCore 跟踪表面时。现在,在我们自己领先之前,让我们通过以下图表来想象一下点云和网格在 3D 中的样子:

网格化和环境

3D 中的点云和网格

如果你注意观察图示,你会看到一个嵌入的图形,展示了一个多边形及其组成的有序顶点集。注意点的顺序是逆时针的。是的,我们连接点的顺序会影响网格光照和着色时的表面朝向。当场景渲染时,我们只能看到面向摄像机的表面。远离摄像机的表面会被移除或背面裁剪。我们连接点的顺序被称为绕行,除非你计划手动创建网格,否则你不必担心这个问题。

网格化是将一组特征点组合起来并从中构建网格的过程。生成的网格随后通常会被着色并渲染到场景中。如果我们现在运行这个示例并观察,我们会看到 ARCore 生成的表面或平面网格被生成并放置。我们何不再次在 Android Studio 中打开 Android 示例项目,看看网格化发生在哪里:

  1. 确保你的代码是开放的,以便我们上次离开的地方。你应该正在查看带有 mPointCloud 的行。

  2. 向下滚动一点,直到你看到这段代码块:

if (messageSnackbar != null) {
  for (Plane plane : session.getAllTrackables(Plane.class)) {
    if (plane.getType() == com.google.ar.core.Plane.Type.HORIZONTAL_UPWARD_FACING
        && plane.getTrackingState() == TrackingState.TRACKING) {
      hideLoadingMessage();
      break;
    }
  }
}
  1. 这段代码块只是遍历会话中识别出的类型为 Plane(一个平面网格)的跟踪对象。当它识别出一个正确类型的跟踪平面时,它会隐藏加载信息并跳出循环。

  2. 然后,它将识别出的任何平面渲染成这条线:

planeRenderer.drawPlanes(
    session.getAllTrackables(Plane.class), camera.getDisplayOrientedPose(), projmtx);
  1. planeRenderer 辅助类用于绘制平面。它使用 drawPlanes 方法渲染 ARCore 会话通过视图和投影矩阵识别出的任何识别出的平面。你会注意到它通过调用 getAllTrackables(Plane.class) 将所有平面传递进去。

  2. 将光标放在 drawPlanes 上,输入 Ctrl + B (command + B 在 Mac 上) 以跳转到定义。

  3. 现在,你应该能在 PlaneRenderer.java 文件中看到 drawPlanes 方法——不要慌张。是的,这里有很多令人害怕的代码,幸运的是,这些代码已经为我们写好了。作为一个练习,只需滚动并阅读代码。我们没有时间深入分析,但阅读这段代码将使你对渲染过程有更深入的了解。

  4. 从菜单中选择运行 - 运行 'HelloArActivity'。现在,当应用程序运行时,请特别注意表面的渲染方式以及你如何与之交互。

好的,现在我们了解了表面是如何创建和渲染的。我们还需要了解如何与环境中的这些表面或其他对象交互。

与环境交互

我们知道 ARCore 会为我们提供用户周围识别的特征点和平面/表面。从这些识别的点或平面,我们可以附加虚拟对象。由于 ARCore 为我们跟踪这些点和平面,因此当用户移动对象时,附加到平面上的对象保持固定。但是,我们如何确定用户试图放置对象的位置呢?为了做到这一点,我们使用一种称为射线投射的技术。射线投射将触摸点在二维空间中的点投射到场景中。然后,该射线被用于测试场景中其他对象的碰撞。以下图表显示了这是如何工作的:

从设备屏幕到 3D 空间的射线投射示例

当然,你可能已经看到过无数次这样的工作了。不仅是在示例应用中,几乎所有 3D 应用都使用射线投射进行对象交互和碰撞检测。现在我们了解了射线投射是如何工作的,让我们看看它在代码中的样子:

  1. 打开 Android Studio,示例项目和HelloArActivity.java文件。

  2. 滚动到以下代码块:

MotionEvent tap = queuedSingleTaps.poll();
if (tap != null && camera.getTrackingState() == TrackingState.TRACKING) {
  for (HitResult hit : frame.hitTest(tap)) {
    // Check if any plane was hit, and if it was hit inside the plane 
       polygon
    Trackable trackable = hit.getTrackable();
    // Creates an anchor if a plane or an oriented point was hit.
    if ((trackable instanceof Plane && ((Plane) trackable).isPoseInPolygon(hit.getHitPose()))
 || (trackable instanceof Point
 && ((Point) trackable).getOrientationMode()
 == OrientationMode.ESTIMATED_SURFACE_NORMAL)) {
      // Hits are sorted by depth. Consider only closest hit on a plane 
         or oriented point.
      // Cap the number of objects created. This avoids overloading 
         both the
      // rendering system and ARCore.
      if (anchors.size() >= 20) {
        anchors.get(0).detach();
        anchors.remove(0);
      }
      // Adding an Anchor tells ARCore that it should track this          
        position in
      // space. This anchor is created on the Plane to place the 3D 
         model
      // in the correct position relative both to the world and to the 
         plane.
      anchors.add(hit.createAnchor());
      break;
    }
  }
}
  1. 阅读注释并注意高亮显示的代码行。第一行高亮显示的代码基于在场景中使用frame.hitTest(tap)检测到的击中次数开始循环。那个调用正在进行射线投射以确定哪些对象可能被点击。点击代表二维空间中的屏幕触摸。

    下一个高亮显示的行在检查哪个 ARCore 识别的平面被触摸的if语句内部。如果有击中,我们首先检查anchors的数量是否小于 20,其中每个锚点代表一个附加点。然后,我们使用hit.createAnchoranchors ArrayList集合中添加一个新的Anchor,并使用对新锚点的引用。

  2. 向下滚动一些,直到onDrawFrame中的以下代码块:

// Visualize anchors created by touch.
float scaleFactor = 1.0f;
for (Anchor anchor : anchors) {
  if (anchor.getTrackingState() != TrackingState.TRACKING) {
    continue;
  }
  // Get the current pose of an Anchor in world space. The Anchor pose is updated
  // during calls to session.update() as ARCore refines its estimate of the world.
  anchor.getPose().toMatrix(anchorMatrix, 0);

  // Update and draw the model and its shadow.
  virtualObject.updateModelMatrix(anchorMatrix, scaleFactor);
  virtualObjectShadow.updateModelMatrix(anchorMatrix, scaleFactor);
  virtualObject.draw(viewmtx, projmtx, lightIntensity);
  virtualObjectShadow.draw(viewmtx, projmtx, lightIntensity);
  1. 快速阅读一下代码。第一行高亮显示的代码首先遍历anchors列表中的anchor。然后,我们检查该锚点是否正在被跟踪;如果是,我们在第二行高亮显示的代码中获取其姿态。然后,我们在代码的最后几行中绘制我们的virtualObject(安迪)。注意,在这种情况下,我们还在绘制阴影。

  2. 将第一行代码更改为以下内容:

float scaleFactor = 2.0f;
  1. 此更改将使安迪的大小加倍。在您的设备上运行应用,等待一些表面出现。然后,触摸屏幕放下安迪。现在,他看起来是原来的两倍大小。

触摸进行手势检测

因此,这涵盖了简单的交互。我们再添加一个手势,允许用户清除所有附加点,从而从场景中移除安迪机器人。按照以下步骤添加另一个触摸手势:

  1. 滚动到以下代码部分:
// Set up tap listener.
gestureDetector =
    new GestureDetector(
        this,
        new GestureDetector.SimpleOnGestureListener() {
          @Override
          public boolean onSingleTapUp(MotionEvent e) {
            onSingleTap(e);
            return true;
          }

          @Override
          public boolean onDown(MotionEvent e) {
            return true;
          }
        });

surfaceView.setOnTouchListener(
    new View.OnTouchListener() {
      @Override
      public boolean onTouch(View v, MotionEvent event) {
        return gestureDetector.onTouchEvent(event);
      }
    });
  1. 上述代码位于HelloArActivityonCreate方法中。它首先为解释选定的触摸事件设置了gestureDetector。然后,我们通过setOnTouchListener设置一个监听器来捕获触摸事件并将它们发送到手势检测器。只需记住,监听器监听触摸,而手势检测器解释触摸的类型。所以我们要做的是捕获用户另一种形式的触摸手势。

  2. 在高亮部分之后立即添加以下代码:

@Override
public boolean onDown(MotionEvent e) { return true;} //after this section@Overridepublic void onLongPress(MotionEvent e) {
 onLongPressDown(e);
}

  1. 这将我们的事件发送到新的方法onLongPressDown。让我们在其他的处理手势的方法下面添加这个新方法,代码如下:
private void onSingleTap(MotionEvent e) {
    // Queue tap if there is space. Tap is lost if queue is full. mQueuedSingleTaps.offer(e);
} //after this block of codeprivate void onLongPressDown(MotionEvent e) {
    mTouches.clear();
}
  1. onLongPressDown内部发生的一切只是收集anchorsanchors被清除。通过清除anchors,我们清除了附着点,因此 Andy 的任何渲染都将消失。

  2. 保存文件,连接您的设备,并运行示例。尝试在场景周围放置几个大型的 Andy。然后,使用新的长按手势来移除它们。

好的,现在我们基本了解了如何与环境交互。在下一节中,我们将介绍 OpenGL ES 的一些基础知识,这是我们用于 Android 的 3D 渲染框架。

使用 OpenGL ES 进行绘图

OpenGL ES 或简称 GLES 是 OpenGL 的精简版移动版本。OpenGL 是一个类似于 DirectX 的低级且强大的 2D 和 3D 绘图 API。由于它是一个低级库,因此确实需要大量的 2D/3D 数学知识。再次强调,为了我们的目的,我们将避免大多数复杂的数学,只是修改一些绘图代码来改变示例应用程序的功能。我们将修改示例应用程序,改变对象绘制的方式。加载 Android Studio 中的示例项目,让我们开始吧:

  1. 滚动到PointCloudRenderer.java的底部,查看以下屏幕摘录中标识的代码部分:

图片

PointCloudRenderer.java中打开 draw 方法

  1. 现在代码很简单,但其中很多内容都假设开发者有很好的 3D 数学和图形渲染基础。我们没有时间逐一解释每个步骤,但本质上,代码所做的只是绘制标识的点云特征(那些蓝色点)。

当我们到达 Unity 章节时,你可能会开始想知道为什么有人会愿意忍受编写 OpenGL ES AR 应用程序的痛苦。这是一个好问题。渲染逼真的 3D 图形完全是关于速度和性能。虽然 Unity 在渲染方面做得很好,但它仍然是 OpenGL ES 之上的另一层软件。这意味着 Unity 通常会比其本地的 OpenGL ES 版本运行得慢。具体慢多少,取决于你试图做什么。

  1. 查看以下摘录中标识的行,如图所示:
GLES20.glUniform4f(colorUniform, 31.0f / 255.0f, 188.0f / 255.0f, 210.0f / 255.0f, 1.0f);
  1. 这行代码设置了渲染点云的颜色。它是通过将31.0188.0210.0的 RGB 颜色值除以255.0来归一化这些值的,从而创建一个从 0 到 1 的均匀或归一化颜色向量。最后一个值1.0代表 alpha 或透明度,其中1.0表示颜色不透明,而0.0表示它透明

  2. 让我们通过将那行代码更改为以下内容进行一点实验:

GLES20.glUniform4f(colorUniform, 255.0f / 255.0f, 255.0f / 255.0f, 255.0f / 255.0f, 1.0f); 
  1. 接下来,我们将改变我们绘制的点的尺寸,以便它们清晰可见,通过更改以下行代码:
GLES20.glUniform1f(pointSizeUniform, 25.0f);
  1. 保存文件,连接您的设备,然后部署并运行应用程序。当应用程序运行时,注意现在点的颜色。这是您预期的吗?

现在,我们可以清楚地看到特征点是如何以及在哪里被识别的。然而,我们仍然从点数据中得不到很多信息。如果我们根据点到观察者的距离给点着色会怎样?这将使我们能够以一些深度信息可视化我们的环境点云。在像 OpenGL ES 这样的低级 API 中手动通过颜色子集点将需要大量的代码更改。幸运的是,我们甚至可以更进一步,编写一个名为着色器的程序,在绘制点之前改变点的颜色。我们将在下一节深入探讨着色器编程。

着色器编程

着色器编程可能是作为图形程序员你能做的最困难且最底层的发展任务之一。它需要卓越的 3D 数学和图形渲染过程的知识。此外,编写好的着色器是一项可能需要数年才能掌握的技能。那么,为什么我们要在一本介绍基础知识的书中介绍这个?简单地说,编写一个好的着色器可能很困难,但它也是极其有益的,而且这是一项对于任何严肃的 3D 程序员来说必不可少的技能集。

我们将在本书的其余部分使用着色器来完成许多事情。如果你现在开始感到不知所措,那么请休息一下,学习一些 3D 数学或跳到下一章。有时候,你需要时间让事情沉淀下来,才能有那种“啊哈”的顿悟时刻。

着色器程序直接在设备或计算机的图形处理单元GPU)上运行。如果设备没有 GPU,则程序在 CPU 上执行,这是一个速度慢得多的过程。毕竟,GPU 已经针对运行着色器代码进行了优化,并且执行得非常好。实际上,几乎在 GPU 上进行的所有 3D 渲染都运行着色器代码。当我们使用 Unity 这样的高级游戏引擎时,我们仍然会编写自己的着色器,因为它给我们提供了强大的功能和灵活性。

那么,着色器程序看起来是什么样子?以下是一个用OpenGL 着色语言GLSL)编写的着色器示例:

uniform mat4 u_ModelViewProjection;
uniform vec4 u_Color;
uniform float u_PointSize;

attribute vec4 a_Position;

varying vec4 v_Color;

void main() {
   v_Color = u_Color;
   gl_Position = u_ModelViewProjection * vec4(a_Position.xyz, 1.0);
   gl_PointSize = u_PointSize;
}

这是我们用于渲染点云点或顶点的着色器程序。具体来说,这个着色器负责在每次调用main时渲染单个顶点,它被称为顶点着色器。在渲染过程的后期,在 3D 场景通过顶点着色器被展平成 2D 图像之后,我们有运行片段或像素着色器的机会。片段着色器是为需要渲染的每个像素/片段运行的。

着色器程序有多种变体,但既然它们都源自 C 语言并且共享许多相似的功能,从一种语言切换到另一种语言并不像你想的那么困难。实际上,我们将学习一些 GLSL 的基础知识以及 Unity 中使用的称为高级着色语言HLSL)的形式,它起源于 DirectX。

如果你查看main函数,你会看到我们设置了三个变量:v_Colorgl_Positiongl_PointSize。这些变量是全局的,仅用于确定顶点的颜色、大小和位置。第一行将颜色设置为输入变量——u_Color。然后,通过将u_ModelViewProjection矩阵与表示位置的新的向量相乘来计算位置。这个操作将我们的顶点从世界空间转换为屏幕空间。最后,我们使用另一个输入——u_PointSize来设置点的大小。

我们想要做的是修改这个着色器程序,使其根据用户距离来着色点。不过,在我们这样做之前,先看看着色器是如何获取这些输入的。打开 Android Studio 中的PointCloudRenderer.java文件,并按照以下步骤操作:

  1. 滚动到createOnGUIThread方法的底部,查找以下行:
positionAttribute = GLES20.glGetAttribLocation(programName, "a_Position");
colorUniform = GLES20.glGetUniformLocation(programName, "u_Color");
modelViewProjectionUniform = GLES20.glGetUniformLocation(programName, "u_ModelViewProjection");
pointSizeUniform = GLES20.glGetUniformLocation(programName, "u_PointSize");
  1. 这些代码行设置了我们的着色器输入位置。我们在这里所做的就是确定我们需要用于将数据注入传递给着色器的数组缓冲区的索引。我们需要添加另一个输入,所以请在前面代码片段的末尾添加以下行:
furthestPoint = GLES20.glGetUniformLocation(programName, "u_FurthestPoint");
  1. 这行代码添加了一个名为u_FurthestPoint的另一个输入变量。我们需要计算用户(相机)到最远点的距离,以便在渐变上着色点。在此之前,回到文件顶部,并在识别的行下面声明以下新变量:
private int numPoints = 0; //after this line
private int furthestPoint;
private float furthestPointLength;
  1. 记住furthestPoint是一个变量的索引,而furthestPointLength将用于存储到最远点的距离。

  2. 滚动到update方法,在识别的行之后输入以下代码:

numPoints = lastPointCloud.getPoints().remaining() / FLOATS_PER_POINT;  //after me

furthestPointLength = 1;
if(numPoints > 0) {
    for(int i=0; i<numPoints*FLOATS_PER_POINT;i= i+FLOATS_PER_POINT) {
        float x = lastPointCloud.getPoints().get(i);
        float y = lastPointCloud.getPoints().get(i+1);
        float z = lastPointCloud.getPoints().get(i+2);
        double len = Math.sqrt(x*x+y*y+z*z);
        furthestPointLength = Math.max(furthestPointLength, (float)len);
    } 
 }
}
  1. 这段代码首先将我们的最小距离(1)设置为mFurthestPointLength。然后,我们检查是否有任何观测到的点。如果有,我们遍历点云中的点。在循环中,我们使用get方法索引到点缓冲区并提取点的xyz。这使得我们可以用点的xyz来测量向量的长度。你可能认出这个方程是勾股定理,但是在三维空间中,而不是你习惯的二维空间中。然后我们检查这个新的长度(距离)是否大于当前的最长长度,使用Math.max。请注意,这段代码在update方法中运行,因此每帧渲染都会执行。

我们使用以下公式计算三维空间中两点之间的距离:

由于我们的相机(用户)是原点,我们可以假设我们的一个点是(0,0,0),这等于以下内容:

这变成了以下内容:

  1. 滚动到draw方法,并在指定的行下面添加以下代码:
GLES20.glUniform1f(mPointSizeUniform, 25.0f); //after me

GLES20.glUniform1f(furthestPoint, furthestPointLength);
  1. 这个调用将我们在update方法中计算的furthestPointLength设置到着色器程序中。

编辑着色器

好的,这就是我们为了计算和设置新的距离变量而需要编写的所有 Java 代码。接下来,我们想要打开着色器程序并修改代码以满足我们的需求。按照以下步骤修改着色器程序:

  1. res/raw文件夹下打开point_cloud_vertex.shader文件,如图所示:

打开point_cloud_vertex.shader

  1. 按照以下方式对高亮显示的代码进行更改:
uniform mat4 u_ModelViewProjection;
uniform vec4 u_Color;
uniform float u_PointSize;
uniform float u_FurthestPoint;

attribute vec4 a_Position;

varying vec4 v_Color;

void main() {
   float t = length(a_Position)/u_FurthestPoint;
   v_Color = vec4(t, 1.0-t,t,1.0);
   gl_Position = u_ModelViewProjection * vec4(a_Position.xyz, 1.0);
   gl_PointSize = u_PointSize;
}
  1. 代码的第一行是新的。我们只是获取a_Position向量的长度,确定其长度或到相机的距离,然后将该值归一化到 0 和 1 之间。第二行根据我们对t变量的计算创建一个新的vec4来表示颜色。这个新向量代表以红蓝绿 alphaRGBA)形式存在的颜色,其中 alpha 被设置为常量1.0

  2. 保存文件,连接您的设备,并在设备上构建和运行应用程序。现在,你应该会看到按到相机的距离着色的云点,如下所示:

按深度着色的点云点截图

想象一下,如果我们不得不编写 Java 代码来完成相同颜色的点着色。我们肯定需要比我们写的代码多得多的代码。此外,我们使用的任何 Java 代码肯定比着色器慢得多。现在,对于我们的示例,应用程序的性能不太关键,但当你开发一个真正的 AR 应用程序时,你将希望榨取所有可用的性能;这就是为什么我们的讨论和对着色器的了解如此重要的原因。

练习

以下练习旨在测试你的技能和刚刚获得的知识,以便在我们刚刚完成的工作上继续前进。请在自己的设备上完成以下练习:

  1. 将跟踪线的颜色从蓝色改为红色,或另一种颜色。

  2. 将直线段替换为SplineCurve。提示:你需要跟踪多个之前的位置。

  3. 让立方体和/或音频沿着跟踪路径跟随用户。提示——你可以使用另一个setInterval定时器函数,每 1.1 秒(1100 毫秒)沿着路径移动盒子。

摘要

我们从回顾环境跟踪的一些概念开始本章,探讨了 ARCore 如何跟踪环境。然后,我们转向网格化以及它是如何用于生成平面和表面的。从那里,我们转向与环境交互,我们看到触摸手势是如何被解释并转换为 3D 场景中的位置的。之后,我们学习了 OpenGL ES 的一些基础知识以及我们的点云是如何渲染的。然后,我们深入探讨了着色器的低级渲染过程。有了这个,我们就修改了点云顶点着色器,以便根据距离着色点。

灯光是增强现实整体错觉的关键元素。在下一章中,我们将再次深入 Unity,学习关于光估计的内容。

第七章:光估计

魔术师们会在镜子前花费数小时,观察并研究他们表演的每一个角度,以确保完美无缺。他们意识到,每一个细节都必须完美,才能让观众相信这个幻觉。即使是小小的错误,也可能毁掉整个幻觉,甚至魔术师的整个表演和可信度。虽然这听起来很苛刻,但这与构建 AR 应用的过程并无二致。如果你的应用要让用户沉浸在你的世界中,你需要尽可能地让它看起来可信。这包括确保场景中的所有虚拟物体看起来就像它们属于那里一样。魔术师们使用照明和透视技巧来欺骗用户,让他们相信某物是真实的。我们已经看到了我们如何使用透视,现在我们需要涵盖并增强我们对照明的使用。

在本章中,我们将介绍 ARCore 如何使用光估计技术来让 AR 体验对用户来说更加可信。然后我们将继续扩展一些基本技术,以改进我们未来的 AR 应用。以下是本章我们将涵盖的主要主题:

  • 3D 渲染

  • 3D 照明

  • 光估计

  • Cg/HLSL 着色器

  • 估计光方向

我们将在本章中使用 Unity,因为它提供了一个更容易的平台来学习渲染过程、照明以及更多关于着色器程序的内容。Unity 中的着色器程序是不同类型的,绝对值得一看。

虽然这一章还不到书的一半,但读者应该将其视为高级章节。我们还将再次涵盖更多关于着色器程序和 3D 数学概念的内容。对于那些想要复习或只是想对 3D 数学有一个基本了解的人来说,这里有一个很好的网站,通过这个教程,3D Math: Vector Math for 3D Computer Graphics chortle.ccsu.edu/vectorlessons/vectorindex.html。这是一个由Bradley Kjell授权的极好网站。

3D 渲染

在我们讨论 AR 中的光估计之前,让我们回顾一下 3D 模型的渲染过程。看看以下这个图表,它从高层次解释了渲染过程:

图片

3D 模型的典型渲染过程

现在,这个图表只是从视觉上展示了渲染过程。几何和顶点着色器实际上永远不会渲染线框模型。相反,它们只定位和着色顶点和表面,然后这些信息被输入到像素/片段和照明着色器中。这一步被称为光栅化,代表了生成或光栅化 2D 图像的最终步骤。

我们在这里讨论的渲染过程是在设备 GPU 上使用 DirectX 或 OpenGL 进行的标准实时渲染。请记住,还有其他用于实时(体素)和非实时(光线追踪)渲染的渲染过程。

Euclideon 开发了一种类似体素渲染技术,他们声称如下:

"这里有了真正可用的全息技术。"

  • Euclideon

这听起来非常有前景,并且是 AR 和 VR 的一个变革性技术。然而,这项技术因在帧率损失的情况下渲染万亿个点而受到极大的质疑,有些人认为这是荒谬的声明。

构建测试场景

如往常一样,让我们看看这在我们工具中的样子。打开 Unity,使用我们已安装的示例 ARCore 项目,并执行以下步骤:

  1. 从菜单中选择文件 | 新建场景。这将在 Unity 中为我们创建一个新的空场景。

  2. 从项目窗口中,将安迪预制件从Assets/GoogleARCore/HelloARExample/Prefabs文件夹拖动到层次窗口中,如以下屏幕截图中所示:

图片

显示 Unity 界面,安迪预制件被拖动到场景中

  1. 安迪(Andy)相当小,因此我们将调整他的大小和相机,以便更好地适应场景和游戏窗口。选择安迪,并将变换缩放修改为 X 为25,Y 为25,Z 为25。然后,选择主相机,并将其变换位置修改为 Y 为4。这在上面的屏幕截图中显示:

图片

设置安迪和主相机的变换

  1. 点击游戏和场景标签页以切换视图,查看安迪模型在每个视图中的样子。

Unity 中的场景窗口用于组合场景对象。你将在 Unity 中大部分工作都在这里完成。游戏窗口表示游戏中的视图,尽可能接近渲染效果。不幸的是,对于 ARCore 应用,我们只能限制在设备上进行测试,因此无法生成准确的游戏视图。这就是为什么,至少目前,我们将为探索目的在单独的场景中工作。

  1. 从菜单中选择 GameObject | 3D Object | Plane。这将向场景添加一个新的平面。确保通过在检查器窗口中点击变换组件旁边的齿轮图标并从菜单中选择重置位置,使平面定位在0,0,0。完成此操作后,安迪将在平面上投射阴影。

  2. 再次切换视图。展开场景标签页下方的着色下拉菜单,如以下摘录所示:

图片

绘制模式菜单

  1. 此菜单表示 Unity 可以支持的各种绘制模式。其中一些可能是有意义的,例如线框,而其他则不太有意义。无论如何,运行每个选项的列表,看看它们的作用。

材质、着色器和纹理

好的,现在我们已经看到了 Unity 如何渲染场景以及各种绘制模式。然而,我们仍然需要了解对象是如何着色或纹理化的。在 Unity 中,我们通常使用材质、着色器和纹理来渲染 3D 对象。材质本质上是一个着色器、其依赖的纹理和其他设置的封装。让我们通过以下步骤查看 AndyMaterial 在 Unity 中的样子:

  1. 在项目窗口中打开 Assets/GoogleARCore/HelloARExample/Materials/Andy 文件夹,并选择 AndyMaterial。查看检查器窗口,注意顶部的着色器名称(ARCoreDiffuseWithLightEstimation)。当前着色器使用简单的光照模型,并针对移动 AR 进行了优化,而我们目前不需要,因此我们将更改它。

  2. 在 AndyMaterial 中展开着色器下拉菜单,选择标准。这将使材质切换到使用标准着色器,如下面的截图所示:

将 Andy 切换到使用标准 Unity 着色器

  1. 你会立即注意到 Andy 变得非常暗。这是因为金属和光滑度被调得非常高。使用鼠标调整各种值,使其更加愉快,如前一张截图中的红色箭头所示。也许一个金属闪亮的 Andy?

在调整材质时要注意的一点是,你对材质所做的任何更改都将自动保存并持久化,即使在播放或演示模式下运行也是如此。有时,保留设置的备份很有用,尤其是如果你发现它们很难实现。

  1. 在项目窗口中选择 AndyMaterial 并按 Ctrl + D 或在 Mac 上按 command + D 复制 AndyMaterial。将新材质重命名为 StandardAndyMaterial。

  2. 再次选择 AndyMaterial。将着色器改回 ARCore/DiffuseWithLightEstimation。注意 Andy 的外观如何迅速改变。

  3. 从菜单中选择文件 | 保存场景。将场景保存到 Assets/GoogleARCore/HelloARExample/Scenes 文件夹,命名为 RenderingTest.scene

如你所见,有很多选项和设置可以用于渲染 3D 对象。请自由探索标准着色器上的每个材质设置。在下一节中,我们将通过讨论光照来扩展我们对渲染的理解。

3D 光照

到目前为止,我们已经了解了渲染过程的基础以及 3D 模型的渲染方式。然而,在第一部分中,我们省略了光照如何影响这个过程。为了了解光照在 3D 场景中的重要性,我们不妨先关闭灯光。打开 Unity,回到第一部分结束的地方,并按照以下步骤操作:

  1. 在层次结构窗口中选择方向光对象。

  2. 在检查器窗口中通过取消选中对象名称旁边的复选框来禁用灯光。这将关闭或禁用灯光。然而,你将注意到并非所有的灯光都关闭了。这是因为我们有一个环境光或全局光,用于解释一般的光散射。

  3. 现在,你面前的是一个没有灯光和阴影的暗物体。通过点击复选框重新打开方向光。查看检查器窗口中光的属性,如图所示:

图片

检查器窗口中的方向光属性

  1. 在检查器窗口中调整类型、颜色、模式和阴影类型属性。你可以使用四种不同类型的灯光。方向光类型代表如太阳这样的光源,因此我们只需要确定光的方向。对于其他灯光类型,如点光聚光灯,你需要正确地将灯光放置在场景中才能看到任何效果。

我们可以使用以下方程计算简单的 3D 漫反射照明:

图片

这里:

图片 是光的方向

图片 是表面的法线

图片 是光的强度 [0 到 1]

图片 然后被乘以颜色,以确定最终的照明颜色。

  1. 我们之前查看的Standard着色器使用基于物理的渲染(PBR)或照明模型,这是一个相当复杂的模型。不幸的是,PBR 着色器目前在移动平台上有限制,并且通常无法正常工作或性能不佳。通常,设备的 GPU 无法支持 PBR 着色器所需的额外指令。因此,我们将局限于编写我们自己的自定义照明着色器。

  2. 让我们探索在 AndyMaterial 上切换着色器,以便我们可以看到不同的照明模型会产生什么效果。在Assets/GoogleARCore/HelloARExample/Materials/Andy文件夹中找到 AndyMaterial 并选择它。

  3. ARCore/DiffuseWithLightEstimationMobile DiffuseStandard着色器之间切换,以查看效果或不同的照明模型,如图所示:

图片

三个不同着色器的照明模型比较

  1. 显然,Standard 着色器看起来最自然,但正如我们所学的,PBR 着色器目前在移动平台上不受支持。另一个选择是 Mobile Diffuse 着色器;让我们看看这个着色器在我们的 AR 示例应用中的样子。

  2. 将着色器切换到 Mobile Diffuse,然后保存项目(文件 | 保存项目)。

  3. 将你的设备连接并输入 *Ctrl *+ B,Mac 上为 *command *+ B。这将构建并在你的设备上运行应用。玩玩这个应用,等待一个表面可见,然后点击并放置 Andy。

注意我们的朋友有什么不同吗?没错,他看起来就像加拿大炎热的夏天一样突出。这是因为移动漫反射着色器假设了一个一致的光源,这意味着我们的模型总是接收到相同的光(方向和强度),但在现实世界中,随着用户的移动,光的方向和强度可以发生显著变化。你的设备相机会尝试补偿这一点,但你仍然可以观察到明显的光照变化,尤其是在用户周围的光照发生显著变化时。你可以通过再次运行应用程序来观察这一点,这次,仔细观察我们的模型及其周围的光照有何不同。ARCore 通过执行一个称为光估计的过程来解决不一致光照的问题。我们将在下一节中详细介绍光估计。

光估计

光估计是一种复制现实世界光照条件并将其应用于我们的 3D 虚拟对象的技术。理想情况下,我们希望能够复制确切的光照条件,但当然,我们还没有达到那个水平。ARCore 目前使用图像分析算法根据设备当前图像确定光强度。然后,将其作为全局光照应用于场景中的 3D 对象。再次打开 Unity,让我们通过遵循给定的步骤来了解这是如何完成的:

  1. 再次定位 AndyMaterial 并将其着色器还原为ARCore/DiffuseWithLightEstimation

  2. 保存项目(文件 | 保存项目)。

  3. 连接你的设备,并输入Ctrl + B (Mac 上的command* + B)来构建和运行设备上的应用程序。放置几个 Andy 模型并改变光照条件。注意我们的物体是如何对光照变化做出反应的。

  4. 返回 Unity,双击Assets/GoogleARCore/HelloARExample/Scenes文件夹中的HelloAR场景以打开场景。请随意保存你的RenderingTest场景。

  5. 将你的注意力转向层级窗口,双击方向光以在场景窗口中聚焦并突出显示它。注意在场景窗口中光是如何直指下方的。在检查器窗口中,你会看到阴影类型设置为无阴影,并且强度调低到0.7,这实际上将光变成了方向性环境光或全局光。

  6. 将你的注意力转回到层级窗口,并选择环境光。进入检查器窗口,点击环境光(脚本)组件旁边的齿轮图标。然后,从上下文菜单中选择编辑脚本选项,如图所示:

图片

编辑环境光脚本

  1. 这将打开你的脚本编辑器中的脚本。默认情况下,Unity 安装了MonoDevelop,如果没有安装或设置了不同的编辑器,它将打开脚本。向下滚动到Update方法,如下所示:
public void Update()
{
#if UNITY_EDITOR
        // Set _GlobalLightEstimation to 1 in editor, if the value is not set, all materials
        // using light estimation shaders will be black.
        Shader.SetGlobalFloat("_GlobalLightEstimation", 1.0f);
#else
 if (Frame.TrackingState != FrameTrackingState.Tracking)
        {
            return;
        }

        // Use the following function to compute color scale:
        // * linear growth from (0.0, 0.0) to (1.0, LinearRampThreshold)
        // * slow growth from (1.0, LinearRampThreshold)
            const float LinearRampThreshold = 0.8f;
            const float MiddleGray = 0.18f;
            const float Inclination = 0.4f;

        float normalizedIntensity = Frame.LightEstimate.PixelIntensity / MiddleGray;
        float colorScale = 1.0f;

        if (normalizedIntensity < 1.0f)
        {
            colorScale = normalizedIntensity * LinearRampThreshold;
        }
        else
        {
            float b = LinearRampThreshold / Inclination - 1.0f;
            float a = (b + 1.0f) / b * LinearRampThreshold;
            colorScale = a * (1.0f - (1.0f / (b * normalizedIntensity + 1.0f)));
        }

        Shader.SetGlobalFloat("_GlobalLightEstimation", colorScale);
#endif
    }
}
  1. #if UNITY_EDITOR是一个编译器指令,用于检查代码是否在编辑器中运行。我们这样做的原因是,当代码在 Unity 编辑器中运行时,我们希望它忽略任何光估计计算。当代码在编辑器中运行时,它将执行下一行;_GlobalLightEstimation着色器变量被设置为1。这意味着当代码在编辑器中运行时,它所做的只是将我们的光照设置为1.0

在进行移动开发时,你经常会遇到#if UNITY_EDITOR指令。这个指令允许你编写仅在代码在编辑器中运行时执行的测试代码。这允许我们模拟对象在编辑器中的运行,而无需担心 ARCore 服务或设备限制。

  1. 将你的注意力转向代码中的#else块。这是在设备上执行的代码,首先检查Frame是否正在跟踪。我们已经在 Android 中看到了这个检查。其余的代码基本上只是数学运算,但如果你查看最后高亮的那一行,你会看到一个对Frame.LightEstimate.PixelIntensity的调用。这就是 ARCore 从相机读取图像并确定当前像素强度的调用;一个从完全黑色的图像的0到完全白色的1的浮点值。强度是根据一个称为MiddleGray的常量进行归一化的。MiddleGray颜色或光强度为0.18f大致对应于我们人类停止识别颜色的点。

  2. 然后,我们使用normalizedIntensity来确定我们是否想要线性变化的光照,当normalizedIntensity小于1.0时,或者当强度大于1.0时,变化更缓慢。这就是其余数学运算所做的一切,只是在某个阈值之后使光照变化更加缓慢。

  3. MiddleGray常量更改为以下行:

const float MiddleGray = 1.0f;
  1. 这将把我们的光估计转换为现在使用线性模型。保存代码更改并返回 Unity。Unity 将自动重新编译代码,并在编辑器底部的状态栏中通知你任何错误。

  2. 连接你的设备并构建运行。将一个 Andy 放在表面上。注意这个图形有多暗;这是因为光照模型太突然。

我们使用的是单通道的颜色,或者你也可以称之为灰度。这就是为什么我们称这些值为颜色,但实际上它只是一个单一的浮点数。灰度颜色0.18f相当于 RGB 颜色(0.18f0.18f0.18f),或者 ARCore 称之为MiddleGray

  1. MiddleGray常量改回0.18f,保存项目,并运行应用。注意光照的变化。

这涵盖了 ARCore 如何使用图像分析技术从相机的图像中读取光强度,并将其转换为全局光强度或颜色。光照值是在着色器上设置的,我们将在下一节中了解这个值是如何被使用的。

Cg/HLSL 着色器

Unity 中使用的着色语言是 HLSL 的多种变体,有时也称为 Cg。这种着色变体提供了两种不同形式的着色器:表面顶点/片段着色器。现在,从 Android 过来,这可能会让人感到困惑,因为 GLSL 对顶点和片段着色器处理不同。然而,Unity 中的 HLSL 变体将顶点和片段着色器视为相同,因为它们位于同一文件中,并且处于相同的流程中。处理我们模型光照的表面着色器可以是简单的,也可以非常复杂。标准的 Unity 表面着色器使用 PBR 光照模型,这是一个相当先进的模型,并且不支持大多数移动设备。这个问题,加上我们有限的场景光照跟踪能力,使我们不得不编写自己的着色器,以正确地获取我们的对象光照。ARCore 为我们提供了一个非常简单的表面着色器,用于在示例中为 Andy 模型提供光照。让我们打开 Unity,按照以下步骤查看这个着色器的样子:

  1. 加载HelloAR示例项目和场景。

  2. Assets/GoogleARCore/HelloARExample/Materials/Andy文件夹中选择 AndyMaterial。确保着色器设置为ARCore/DiffuseWithLightEstimation。如果你更改了它,请将其切换回来。

  3. 点击齿轮图标,从上下文菜单中选择编辑着色器。这将打开你的代码编辑器中的着色器,这里也为了参考而展示:

Shader "ARCore/DiffuseWithLightEstimation"
{
    Properties
    {
        _MainTex ("Base (RGB)", 2D) = "white" {}
    }

    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 150

        CGPROGRAM
    #pragma surface surf Lambert noforwardadd finalcolor:lightEstimation

        sampler2D _MainTex;
        fixed _GlobalLightEstimation;

        struct Input
        {
            float2 uv_MainTex;
        };

    void lightEstimation(Input IN, SurfaceOutput o, inout fixed4   
                         color)
    {
        color *= _GlobalLightEstimation;
    }

    void surf (Input IN, inout SurfaceOutput o)
    {
        fixed4 c = tex2D(_MainTex, IN.uv_MainTex);
        o.Albedo = c.rgb;
        o.Alpha = c.a;
    }
    ENDCG
  }

    Fallback "Mobile/VertexLit"
}
  1. 这是一个相当简单的漫反射光照着色器,它使用了我们之前计算的全球光照估计。它首先通过以下行定义自己:
Shader "ARCore/DiffuseWithLightEstimation"
  1. 接下来,它定义了下一个代码块中的Properties,其中_MainTex代表基础纹理,被称为"Base (RGB)",并设置为2D。如果你快速回顾 Unity,你可以在检查器窗口中看到这个属性。

  2. SubShader开始的代码块是动作发生的地方。我们首先定义Tags,这是一组键/值对,用于设置渲染顺序和类型参数。在我们的例子中,我们将此设置为Opaque。然后,我们有以下行:

LOD 150
  1. 这决定了着色器的细节级别LOD指令用于确定着色器的复杂度或性能要求。你可以将值设置为任何值,但以下列表显示了典型值:

    • 顶点照明类型着色器 = 100

    • 贴图,反射式顶点照明 = 150

    • 漫反射 = 200

    • 漫反射细节,反射式凸起无光照,反射式凸起顶点照明 = 250

    • 凸起,高光 = 300

    • 凸起高光 = 400

    • 视差 = 500

    • 视差高光 = 600

  2. 从列表中可以看出,简单的着色器代表的是低细节级别。这意味着低级硬件应该能够无任何问题地运行这个着色器。你可以为每个着色器或全局设置最大着色器 LOD;查看 Unity 文档以获取更多详细信息。

  3. 我们从CGPROGRAM开始编写实际的着色器代码,然后使用#pragma指令定义表面着色器的形式,如下所示代码:

#pragma surface surf Lambert noforwardadd finalcolor:lightEstimation

#pragma surface surfaceFunction lightModel [optionalparams]

  1. 指令的第一部分,surface,将其定义为表面着色器。然后,我们看到surf函数名称指的是主表面函数。接着是光照模型,本例中为Lambert。之后,选项设置为noforwardadd,这是一种简单的方式来限制灯光数量为单一。最后,我们使用一个名为lightEstimation的自定义修改函数,并通过finalcolor:lightEstimation进行设置。

此着色器使用 Lambert 光照模型。你可以在docs.unity3d.com/Manual/SL-SurfaceShaderLightingExamples.html找到 Unity 支持的光照模型的大量示例或如何编写自己的模型。

  1. #pragma指令的内部,我们看到着色器输入的定义:_MainTex_GlobalLightEstimationstruct Input。如果你还记得,_GlobalLightEstimation是我们设置在EnvironmentalLight脚本中的变量,用来表示我们的全局光。

  2. 接下来,我们将向下跳几行到surf函数,如下所示:

void surf (Input IN, inout SurfaceOutput o)
{
 fixed4 c = tex2D(_MainTex, IN.uv_MainTex);
 o.Albedo = c.rgb;
 o.Alpha = c.a;
 }
  1. 此函数简单地使用tex2D和输入的uv坐标从我们的_MainTex中采样颜色。然后,它从查找中设置颜色(Albedo)和Alpha。这个函数首先被调用以确定表面的颜色,然后,其输出被传递到 Lambert 光照模型,之后最终颜色由lightEstimation函数设置。

    标记为inout的输入表示一个可以修改的值,并且将自动返回。

  2. 向上滚动一点到lightEstimation函数。在这个函数内部,以下所示的代码根据为_GlobalLightEstimation设置的值修改颜色:

color *= _GlobalLightEstimation;
  1. 将颜色乘以全局光照估计相当于调整亮度开关。

  2. 最后,我们通过Fallback和另一个着色器的名称来完成着色器的设置。这将为当前着色器无法运行时设置一个回退或备用着色器。着色器可能会因为编译错误或硬件限制而失败。

现在我们已经清楚地了解了之前生成的光照估计值在着色器中的使用方式,我们可以转向可能增强我们的光照。如果你还记得,我们当前的光线是直接向下的,但理想情况下,我们希望将光线定位到最强的光源处。在下一节中,我们将探讨一种简单但有效的方法来跟踪和定位 AR 中的光线。

估计光照方向

Google 通过 ARCore 为我们提供了一个强大的解决方案,用于估计 AR 场景中的光照量。正如我们所学的,光方向是场景照明的一个重要组成部分。Google 并没有故意忽略使用 ARCore 估计光方向;只是这个问题真的很难做对。然而,Google 确实在 ARCore 中为我们提供了足够多的工具,使我们能够估计光方向,并做出一些简单的假设。首先,我们需要假设我们的用户,至少目前,将保持在同一个房间或区域。其次,我们的用户需要至少在视野中 180 度的弧度内查看,或者更简单地说,用户只需要四处看看。第三,如果真实世界环境由一个远处的单一明亮光源照亮,例如太阳,那么效果最好。基于这些假设,我们可以简单地存储用户看到最亮图像的方向,并使用它来反向计算我们的光方向。这可能听起来比实际要复杂,所以希望以下图表可以进一步解释这一点:

图片

从相机像素强度计算光方向

现在,这个技术听起来可能相当复杂,但实际上并不复杂。我们实际上只需要几行代码就能完成这个任务。打开 Unity 并跟随步骤编写我们的方向光检测器:

  1. 确保样本应用中的HelloAR场景已加载。

  2. 在层次结构窗口中选择环境光对象。

  3. 检查器窗口中点击环境光(脚本)组件旁边的齿轮图标,并在上下文菜单中选择编辑脚本。

  4. 在类声明下方,添加以下行以声明新变量:

public class EnvironmentalLight : MonoBehaviour
{ //after me
  public GameObject SceneCamera;
  public GameObject SceneLight;
  private float maxGlobal = float.MinValue;
  private Vector3 maxLightDirection;
  1. 这些变量将保存对场景相机、光、找到的最大全局强度以及找到的方向的引用。

  2. 在代码中向下滚动,直到看到Update方法中标识的行,并添加以下行:

const float Inclination = 0.4f; //after me
var pi = Frame.LightEstimate.PixelIntensity;
if(pi > maxGlobal)
{
  maxGlobal = pi;
  SceneLight.transform.rotation = Quaternion.LookRotation(-SceneCamera.transform.forward);
}

  1. 所有这些代码所做的只是使用Frame.LightEstimate.PixelIntensity读取当前相机方向的光强度。然后,我们检查这个值是否高于之前看到的任何值(maxGlobal)。如果是,我们设置一个新的最大值并将光(SceneLight)旋转到与相机相反的方向,这意味着光将面向相机。

在编辑#if UNITY_EDITOR指令之外的代码时要小心。这段代码只有在为平台运行构建时才会编译,这意味着代码中的任何错误都会被识别为构建错误。这可能会令人困惑,所以请小心避免在编写这些部分时出现语法错误。

  1. 保存文件;这就是我们为了调整光方向需要编写的所有代码。如果你还记得上一节,我们使用的漫反射着色器并没有考虑光方向。然而,ARCore 为我们提供了一个考虑了光方向的着色器。

  2. 返回编辑器以在 Assets/GoogleARCore/HelloARExample/Materials/Andy 文件夹中找到并选择 Andy 材质。

  3. 将材质更改为使用ARCore/SpecularWithLightEstimation着色器。这种材质能更好地显示光的方向。

  4. 在层级窗口中选择环境光照对象。注意我们已向环境光照(脚本)组件添加了两个新属性。这些新属性(场景相机和场景光)被添加是因为我们在类中将它们声明为公共字段。

  5. 点击场景相机属性旁边的类似靶心的图标。然后,如以下摘录所示,从选择游戏对象对话框中选择 First Person Camera 对象:

图片

设置组件的场景相机和场景光属性

  1. 关闭选择游戏对象对话框。

  2. 重复相同的步骤来设置方向光作为场景光

  3. 连接您的设备并构建运行。在一个只有一个明亮光源的区域运行应用,看看放置 Andy 后他的样子。

更新环境照明

现在,Andy 应该被看起来区域中最亮的光源照亮。然而,因为我们目前没有跟踪光方向的变化,如果你更换房间或照明发生变化,那么这种幻觉就会被打破。光跟踪很困难,而且比跟踪用户更困难,尽管我们可以实施一个简单的黑客手段来不跟踪照明,目前这个手段是永久的,如果你没有注意的话。跟随以下步骤将这个简单的黑客手段放入我们刚刚编写的代码中:

  1. 在你选择的文本编辑器中打开EnvrionmentalLight.cs脚本。如果你忘记了如何做,只需回顾几页。

  2. 在指定的行之后和之前添加以下行:

var pi = Frame.LightEstimate.PixelIntensity; //after me
maxGlobal *= .98f;
if(pi > maxGlobal){ //before me
  1. 那一行是maxGlobal变量的降级函数。记住maxGlobal是我们识别为最强光源的值。这个简单的函数,是的,函数,随时间降低这个值。.98f的值设置了衰减速度。.98f代表一个相当快的衰减率,而.9999f则代表一个慢衰减率。

  2. 保存文件,是的,就这样。

  3. 返回 Unity。连接、构建并运行应用。现在当你放置一个 Andy 时,你应该很快看到应用识别的最强光源的变化。你可以随意返回并更改衰减率或修改函数,并使用你自己的方法;实验。

我们组合的方法是一种简单的跟踪和估计光方向的方式。正如我们提到的,这种方法是有效的,但当然并非没有局限性。无论如何,这应该为好奇的读者提供足够的资料来继续并进一步扩展。我们还完全避免了关于阴影的适当讨论。幸运的是,我们将在第九章混合光用于建筑设计中有很多时间来讨论这一点,我们将允许用户转换他们自己的居住空间。

练习

请独立完成以下练习

  1. 改变maxGlobal衰减率。你决定是让它更快还是更慢。

  2. 根据用户的移动量增加或减少maxGlobal衰减率。提示——回想一下我们是如何追踪用户的,并利用这一点来确定他们走了多远或有多快。使用这些信息来设置衰减率。

  3. 编写你自己的自定义光照表面着色器。这个任务可能有些困难,但付出努力是值得的。

摘要

当然,随着你成为 AR 领域的专家,你会意识到光照对增强现实的重要性。它如此重要,以至于谷歌开发了内置光估计的 ARCore,这就是为什么我们花费了整个章节来讨论这个主题。首先,我们更深入地了解了渲染过程;然后,我们介绍了 3D 光照,这是我们理解 AR 中光照增加的复杂性所必需的基本知识。这引导我们研究 ARCore 如何通过仔细查看 Unity Cg/HLSL 着色器和,更具体地说,表面着色器来估计区域的光照水平或全局光照。最后,我们实施了一个简单但有效的技巧来跟踪和估计场景中的光方向,我们将这个任务留给读者在他们的时间里去改进。

估计环境中的实际光照条件将是 AR 需要克服的主要障碍。然而,随着 AI 和机器学习的惊人进步,我们可能会很快看到一些更好的解决方案出现。我们将在下一章更详细地探讨机器学习如何帮助 AR。

第八章:环境识别

在整本书中,我们探讨了我们的设备如何借助 ARCore 的多种方式来跟踪用户、理解用户的世界,并渲染一个替代现实。ARCore 使用设备的传感器和摄像头作为输入,不断更新它感知到的用户真实世界。然而,如果我们想为用户提供更多功能;比如识别某个物体、标志或地标?这将需要一套更高级的工具。即使是在 5 年前,这也会被视为一项极其艰巨的任务。随着 OpenAI 的出现,多亏了马斯克先生,许多其他公司也开始开源并使他们的工具可用。这导致了这些技术(俗称为机器学习ML))的爆炸性增长,并使它们对每个人更加易于访问。幸运的是,对于那些对开发 AR 应用感兴趣的人来说,这是一个好事。当我们需要识别和理解用户的环境时,我们希望得到所有可能的帮助。

在本章中,我们将介绍机器学习,并探讨我们如何利用它为我们的用户提供更好的 AR 应用。在本章中,我们将涵盖以下主题:

  • 机器学习简介

  • 深度强化学习

  • 编程神经网络

  • 训练神经网络

  • TensorFlow

机器学习是一个非常高级的主题,要掌握它可能需要多年的学习。然而,为了我们的目的,我们将学习一些基本技术,读者可以在以后通过更多的学习或实现自己的解决方案来扩展。

如果你已经对神经网络、卷积神经网络和 TensorFlow 有深入的了解,你可以自由地跳过这一章。

机器学习简介

机器学习是一个广泛使用的术语,用来指代人工智能和相关计算机预测分析模型。虽然“机器学习”这个名字可能过于笼统,但它比“人工智能”这个术语更合适。然而,机器学习本身是一个非常广泛的术语,可能需要进一步的解释和澄清。机器显然指的是计算机或其他设备,而学习通常表示一个随着时间的推移会演变或学习的算法或模型。然而,在许多机器学习模型中,情况往往并非如此。因此,为了我们的目的,我们将使用更广泛的“机器学习”这个术语来指代任何可以训练以识别 AR 中的环境或环境部分的工具或算法,从而让我们,开发者,更好地增强用户的世界。

数据科学和机器学习密不可分。数据科学是关于理解数据、提取模式和做出预测的。本质上,当你开始编写机器学习模型以识别物体或环境时,你实际上只是在分析数据,这意味着你也可以非常松散地称自己为数据科学家。

机器学习是一个很大的领域,并且每天都在不断扩大,所以让我们具体分析一下我们希望机器学习帮助我们解决的问题:

  • 目标检测:目标在 AR 中已经使用了一段时间。它曾是许多 AR 应用在 ARCore 之前的跟踪和参考点。

  • 图像识别:这衍生出一系列子应用,我们将在后面详细讨论。

  • 物体检测:从点云数据中检测 3D 物体并非易事,但这已经实现,并且正在变得更好。

  • 人脸检测:在图像中检测人脸已经存在多年,并在许多应用中得到了很好的应用。

  • 人员检测:检测人或动作具有很大的可能性。想想看,Kinect 进入 AR。

  • /手势检测:不要与触摸手势混淆。这是我们检测用户在设备摄像头前手的动作或手势的地方。

  • 物体姿态检测:与物体检测相关,但现在我们还在检测物体的位置和方向。

  • 光源检测:能够在场景中放置逼真的光源,使虚拟对象渲染更加逼真。我们已经在第七章中探讨了光照的重要性,光照估计

  • 环境检测:识别用户移动到的环境在地图制作或 GPS 不可用的建筑或其他位置有很好的应用,这适用于大多数内部空间。

每个问题可能需要不同的工具和技术来解决这些问题。在机器学习中,不仅仅关于使用工具,而是最终的答案和什么有效。在构建您应用所需的任何机器学习时,请考虑这一点。尝试各种机器学习工具和技术;机器学习模型的大小和性能差异可能至关重要,这是您需要考虑的。

一位机器学习算法走进了一家餐厅。

服务员问道:“您要点什么?”

算法说:“其他人都在吃什么?”

  • 未知

在下表中总结了当前主要机器学习提供商及其可以解决的 AR 问题类型:

工具集 优点/缺点 机器学习任务
目标/图像 物体/姿态 人脸
Vuforia 成熟且易于使用。需要互联网连接。
XZIMG 支持 Unity 和其他平台的人脸和图像/目标跟踪。
ARToolkit 成熟的开源平台,用于图像跟踪和特征检测。
EasyAR 专业许可证获得对象和特征跟踪。
Google Face Detection API 低级 Android API。
OpenCV Android 的一个成熟的底层 API,商业版本已移植到 Unity。仍需要底层知识。
Google TensorFlow 尚处于起步阶段,但迅速成为 CNN 的平台标准。需要底层和高级机器学习知识。
Google ARCore 目前识别平面、特征点和光线。

我们只包括为支持移动 ARCore 设备的 AR 平台构建了主要平台的参与者。由于这些技术的局限性,我们排除了 Web 技术,尽管许多提到的技术需要互联网连接并支持 Web 平台。如果你快速浏览一下表格,你也可以清楚地看到两个有潜力主导整个空间的主要竞争者;这是因为这些技术都是底层技术,通常支持像 Vuforia 这样的更大平台。这两个平台现在都支持移动预训练网络,以便在移动设备上进行快速识别。这现在可能看起来不是什么大事,但当我们开始训练自己的模型时,你就会明白为什么。

线性回归解释

让我们讨论一下机器学习的基本原理以及它试图实现的目标。看看以下图表,它显示了为你下一个应用程序的一些虚构的销售数据:

图表

虚构的销售数据图表

现在,只需看一下图表,你就可以看到,随着x值(可能是销售天数)的增加,我们的销售额似乎也在增加:y值(销售额)。仅凭观察图表,我们自己就可以通过跟随点的趋势来做出预测。试试看;当x值(底部轴)为 25 时,销售额是多少?给出你的猜测,并写下它。在你确定了猜测之后,我们将使用一种称为线性回归的技术来找到一个好的答案。

线性回归已经存在多年,被认为是许多统计数据分析方法的基础。它是今天在数据科学和预测分析中使用的许多其他机器学习算法的基础。这种技术通过找到一个最佳拟合点(一条线、曲线或任何其他形状)的解决方案来实现。从这个解决方案中,我们可以确定未来的或过去的事件或发生。由于这种方法已经非常成熟,你只需打开 Excel,就可以让它直接在图表上绘制线性回归解决方案。以下是一个带有趋势线和方程的线性回归图表的示例:

图表

带有线性回归趋势线的图表

请记住,这个例子使用的是二维点,但同样的概念也适用于三维,你只需要考虑额外的维度,这虽然不是一件 trivial 的事情,但仍然可行。

不深入数学的细节,只需理解,这条线是为了最小化线与点之间的误差,这通常被称为最佳拟合线或最小误差线,在这种情况下,它被表示为 R 平方值()。的值从 1.0(最佳拟合)到 0.0(在黑暗中射击)不等。你可以看到我们的并不完美,但它是 1 或 91.25%的正确率;它并不完美,但可能足够好了。

概率和统计学在所有形式的机器学习中都起着重要作用。如果你没有良好的统计学背景,你仍然可以通过选择第三方提供商来获取统计数据。唯一的例外是如果你有关于该技术的难题;那么,拥有一些你自己的背景知识会有所帮助,如果你已经在努力追赶你的 3D 数学技能,这可能不是你想要听到的。

以我们刚才看到的例子为例,现在考虑在 3D 空间中的问题,它不是一个线,而是一个我们想要识别或预测的 3D 对象。显然,使用统计模型,事情可以很快变得复杂,计算成本也很高。幸运的是,有一种更好的方法来做这件事,使用一种使用监督学习来模拟人脑的技术,称为神经网络NN)。

在下一节中,我们将深入了解监督学习,并探讨我们可以使用神经网络进行数据分析和使用深度学习DL)的一些技术。

深度学习

正如我们讨论的那样,更传统的预测模型,如线性回归,扩展性不好,因为它们总是需要使用所有可用的点或数据来计算整个解决方案。这些类型的技巧或模型没有记忆、学习和改进的能力,它们通常被归类为监督模型。这导致了更高级的学习模型的演变,称为强化学习RL)技术,用于解决机器学习问题。实际上,深度学习和深度强化学习技术在性能和准确性上已经超越了统计方法几个数量级。然而,情况并非总是如此,统计方法也在每天以同样的速度显著改进。这确实是一个进入机器学习的激动人心的时刻。

下面的图示展示了强化学习过程:

图片

强化学习过程

在图中,你可以看到有一个代理(假设为计算机)和环境(游戏或现实世界)。代理根据环境观察采取行动,这些行动可能基于或可能不基于奖励。使用奖励的 RL 系统被称为强化学习。我们在本章中使用的学习方法被称为监督学习,因为我们正在对特定输出类别进行标记或训练。无监督学习是一种不标记数据但仅使用技术进行分类或分组的数据训练方法。

我们通常识别出三类训练:无监督学习、监督学习和强化学习。强化学习在监督或无监督系统之上使用基于奖励的系统作为学习增强。RL 系统可以以这种方式学习,基本上不需要初始训练。使用深度 RL 模型的 AlphaGo Zero 在能够从头开始击败经过训练的版本,且没有人类干预后,目前正成为新闻焦点。

定义所有这些 ML 概念的问题之一是它们经常交织在一起,其中一种学习算法或技术可能被层叠在另一种之上,可能使用带有或不带有监督的 RL。正如我们将看到的,使用多个不同的技术层来产生准确的答案是很常见的。这种层叠的好处是能够快速尝试多种不同的方法,或者稍后用更好的技术替换。

深度学习是我们用来描述这种层叠过程的术语。DL 可以使用我们讨论过的任何训练方法进行训练。在任何情况下,我们需要停止泛泛而谈,真正地看看 DL 的过程。

深度强化学习近年来变得非常流行,从玩 Atari 游戏到快速击败早期监督训练版本的成功案例有很多。如果你对这个领域的训练感兴趣,确保你搜索 AlphaGo Zero。

神经网络——深度学习的基础

当我们谈论 DL 时,我们通常想到的是一种称为神经网络的 ML 技术。神经网络是通过尝试模拟人脑而构思的。神经网络的核心是神经元,之所以称为神经元,是因为它代表了一个单一的人类脑细胞。以下是人类和计算机神经元的图像:

人类和计算机神经元

就像大脑中数十亿个神经元在层中连接一样,我们以类似的方式在层中连接神经元。每个神经元都与层中其他所有神经元的输入和输出相连,其中第一层接收我们的输入,而最后一层或单个神经元输出我们的答案。以下是一个典型的例子:

带有层的神经网络

在我们继续之前,我们应该澄清一点,我们讨论的深度学习中的层并不对应于神经网络中的层。将神经网络想象为 DL 系统中的一个层。

在这里,图中的每个圆圈代表一个单独的神经元。每个神经元在其所有输入的总和超过某个阈值或激活函数时都会触发。这个过程会持续对所有神经元进行,最终层输出答案。当然,这是一个非常简单的例子,但直到你开始用它们编程,你很难看到神经网络的威力。因此,在下一节中,我们将编写一个神经网络,我们计划用它来识别环境中的物体。

当你第一次遇到神经网络时,你可能会认为这不可能工作。毕竟,自动驾驶汽车怎么可能只使用一串相互连接的神经元来识别人呢?答案确实如此。我们实际上才开始理解神经网络是如何工作的,而且,我们经常发现我们需要回到起点。在这种情况下,起点是人的大脑,而神经网络的一些最新进展是进一步大脑研究的结果。

编程神经网络

学习某事物的最佳方式是去实践,因此在本节中,我们将编写一个简单的神经网络,然后对其进行训练以执行各种任务。这个网络将具有固定数量的层——输入层、隐藏层和输出层——但我们将在每一层中允许设置一定数量的神经元。我们将使用 Unity 编写此代码,以便在第十章,混合现实中使用。

编写神经网络是一个高级示例,需要与数学讨论才能正确解释。如果你在任何时候感到不知所措,你总是可以打开完成的项目并检查最终结果。当然,如果你之前已经编写过神经网络,那么你可能也想跳过这一部分。

对于这个例子,我们将从源 Unity 模板创建一个新的项目,所以让我们开始通过打开命令提示符来开始:

  1. 使用以下命令在根文件夹(Windows 上的C:\)下创建一个名为ARCore的新文件夹:
mkdir ARCore
cd ARCore
  1. 这组命令创建了一个新文件夹,然后导航到它。

  2. 执行以下命令:

git clone https://github.com/google-ar/arcore-unity-sdk.git ARCoreML
  1. 这将 Unity ARCore 模板从 GitHub 拖入一个名为ARCoreML的新文件夹中。

  2. 打开 Unity 的新实例,并在项目页面点击“打开”。这将打开选择项目文件夹对话框。选择您刚刚将模板拖入的新文件夹,ARCoreML,以打开项目。等待项目在 Unity 编辑器中打开。

  3. 在项目窗口中,右键单击(在 Mac 上按Ctrl + 点击)Assets文件夹。从上下文菜单中选择创建 | 文件夹。将新文件夹命名为Scripts

  4. 通过在项目窗口中双击它,从Assets/GoogleARCore/Examples/HelloAR文件夹打开HelloAR场景。

  5. 从菜单中选择文件 | 构建设置。确保 Android 设置为目标平台,并将HelloAR场景设置为构建中的场景0

  6. 连接您的设备并构建运行。只需确保示例在您的设备上按预期运行。

编写神经网络脚本

在新项目设置完成后,我们现在可以开始编写脚本以构建神经网络。回到 Unity 并执行以下步骤:

  1. 打开ARCoreML/Scripts文件夹,然后从菜单中选择 Assets | Create | C# Script。将脚本命名为Neuron,双击以在您选择的编辑器中打开它。

这个示例的代码最初来源于github.com/Blueteak/Unity-Neural-Network.git,它展示了为 Unity 开发的一个简单且简洁的神经网络示例,并明确进行了训练。我们将根据我们的需求修改原始代码,但如果您感兴趣,请随意查看并贡献原始源代码。这段代码非常适合学习,但当然,这并不是您可能希望在生产中使用的东西。我们将在 TensorFlow 部分查看生产就绪神经网络的选项。

  1. 删除所有代码,留下using语句,然后添加以下内容:
using System.Linq; //add after other using's

public class Neuron
{
  private static readonly System.Random Random = new System.Random();
  public List<Synapse> InputSynapses;
  public List<Synapse> OutputSynapses;
  public double Bias;
  public double BiasDelta;
  public double Gradient;
  public double Value;
}
  1. 注意这个类没有继承MonoBehaviour,因此不会是一个游戏对象,这意味着我们将在这个脚本中加载这个类。然后,我们为Random创建一个占位符;我们这样做是因为我们使用的是System.Random而不是Unity.RandomUnity.Random仅支持生成随机float,但我们需要double的精度。其余的都是我们将随着相关代码部分的出现而讨论的属性。

  2. 在最后一个属性声明之后但在类的结束花括号之前输入以下内容:

public static double GetRandom()
{
 return 2 * Random.NextDouble() - 1;
}
  1. 我们创建这个static辅助方法是为了生成从-1.01.0double随机数。这允许有更高的精度,并确保我们的值总是围绕0生成。将值保持在0附近可以避免舍入错误,并且通常使计算变得更简单。

  2. 接下来,在static方法之后输入以下代码:

public Neuron()
{
  InputSynapses = new List<Synapse>();
  OutputSynapses = new List<Synapse>();
  Bias = GetRandom();
}

public Neuron(IEnumerable<Neuron> inputNeurons) : this()
{
  foreach (var inputNeuron in inputNeurons)
  {
    var synapse = new Synapse(inputNeuron, this);
    inputNeuron.OutputSynapses.Add(synapse);
    InputSynapses.Add(synapse);
  }
}
  1. 在这里,我们设置了基类构造函数和单个参数构造函数。基类构造函数为神经元的输入和输出连接创建了一个List<Synapse>Synapse代表一个连接。另一个构造函数调用基类(this)并接受一个IEnumerable<Neuron>的神经元,然后将其连接回。这样,网络可以自下而上构建;当我们到达NeuralNet类时,我们将看到这是如何工作的。

  2. 接下来,我们将添加Neuron类的其余方法:

public virtual double CalculateValue()
{
  return Value = Sigmoid.Output(InputSynapses.Sum(a => a.Weight *  
                                a.InputNeuron.Value) + Bias);
}

public double CalculateError(double target)
{
  return target - Value;
}

public double CalculateGradient(double? target = null)
{
  if (target == null)
    return Gradient = OutputSynapses.Sum(a =>    
    a.OutputNeuron.Gradient * a.Weight) * Sigmoid.Derivative(Value);
    return Gradient = CalculateError(target.Value) * Sigmoid.Derivative(Value);
}

public void UpdateWeights(double learnRate, double momentum)
{
  var prevDelta = BiasDelta;
  BiasDelta = learnRate * Gradient;
  Bias += BiasDelta + momentum * prevDelta;
  foreach (var synapse in InputSynapses)
  {
    prevDelta = synapse.WeightDelta;
    synapse.WeightDelta = learnRate * Gradient * synapse.InputNeuron.Value;
    synapse.Weight += synapse.WeightDelta + momentum * prevDelta;
  }
}
  1. 我们在这里添加了四个方法:CalculateValueCalculateErrorCalculateGradientUpdateWeightsCalculateValue 用于根据我们在 Sigmoid 中定义的激活函数确定神经元的输出。我们将在稍后介绍 Sigmoid。其他方法用于训练神经元。训练神经元是我们将在下一节中介绍的内容。

  2. 保持在同一文件中,并在 Neuron 类外部添加以下三个新的辅助类:

} // end of Neuron class definition
public class Synapse
{
  public Neuron InputNeuron;
  public Neuron OutputNeuron;
  public double Weight;
  public double WeightDelta;
  public Synapse(Neuron inputNeuron, Neuron outputNeuron)
  {
    InputNeuron = inputNeuron;
    OutputNeuron = outputNeuron;
    Weight = Neuron.GetRandom();
  }
}

public static class Sigmoid
{
  public static double Output(double x)
  {
    return x < -45.0 ? 0.0 : x > 45.0 ? 1.0 : 1.0 / (1.0 +    
    Mathf.Exp((float)-x));
  }
  public static double Derivative(double x)
  {
    return x * (1 - x);
  }
}
public class DataSet
{
  public double[] Values;
  public double[] Targets;
  public DataSet(double[] values, double[] targets)
  {
    Values = values;
    Targets = targets;
  }
}
  1. 第一个类 Synapse,正如我们已知的,定义了神经元之间的连接。接下来是 Sigmoid,它恰好是一个用于我们使用的 sigmoid 激活函数的包装类。注意,值被限制在 -45.0+45.0。这限制了我们的网络大小,但我们可以手动更改它。然后是 DataSet,它只是我们训练数据的持有者。

这样就完成了 Neuron 类。在 Unity 中创建另一个脚本,这次命名为 NeuralNet;在您选择的编辑器中打开它并执行以下步骤:

  1. 再次删除起始代码,但保留 using 语句,并输入以下内容:
public class NeuralNet
{
  public double LearnRate;
  public double Momentum;
  public List<Neuron> InputLayer;
  public List<Neuron> HiddenLayer;
  public List<Neuron> OutputLayer;

}  //be sure to add ending brace
  1. 再次,这是一组定义 LearnRate 网络和 Momentum 的公共属性。然后是三个 List<Neuron>,用于存储输入、隐藏(中间)和输出层中的神经元集合。在这个例子中,我们使用单个隐藏层,但更复杂的网络通常支持更多层。你可以猜到,LearnRateMomentum 将在训练部分进行介绍。

我们通常在 Unity 中不倾向于使用带有获取器和设置器的属性。为什么?主要是因为 Unity 编辑器与公共字段配合得更好。其次,游戏编程完全是关于性能的,尽可能避免获取器和设置器的开销是有意义的。使用列表也是不允许的,但在这个情况下,它使得代码更容易理解。

  1. 接下来,让我们为我们的 NeuralNet 添加一个构造函数:
public NeuralNet(int inputSize, int hiddenSize, int outputSize, 
              double? learnRate = null, double? momentum = null)
{
  LearnRate = learnRate ?? .4;
  Momentum = momentum ?? .9;
  InputLayer = new List<Neuron>();
  HiddenLayer = new List<Neuron>();
  OutputLayer = new List<Neuron>();
  for (var i = 0; i < inputSize; i++){
    InputLayer.Add(new Neuron());
  }

  for (var i = 0; i < hiddenSize; i++){
    HiddenLayer.Add(new Neuron(InputLayer));
  }

  for (var i = 0; i < outputSize; i++){
    OutputLayer.Add(new Neuron(HiddenLayer));
  }
}
  1. 此构造函数期望几个输入,包括输入、隐藏和输出层中的神经元数量,以及 learnRatemomentum 的值。在构造函数内部,属性根据输入值进行初始化。注意,第一层使用默认的 Neuron 构造函数,而后续层使用带有前一层作为输入的单参数构造函数。记得从构建 Neuron 类中,这是添加神经元层之间所有突触连接的地方。

  2. 接下来,我们将添加一些用于训练的方法:

public void Train(List<DataSet> dataSets, int numEpochs)
{
  for (var i = 0; i < numEpochs; i++)
  {
    foreach (var dataSet in dataSets)
    {
      ForwardPropagate(dataSet.Values);
      BackPropagate(dataSet.Targets);
    }
  }
}

public void Train(List<DataSet> dataSets, double minimumError)
{
  var error = 1.0;
  var numEpochs = 0;
  while (error > minimumError && numEpochs < int.MaxValue)
  {
    var errors = new List<double>();
    foreach (var dataSet in dataSets)
    {
      ForwardPropagate(dataSet.Values);
      BackPropagate(dataSet.Targets);
      errors.Add(CalculateError(dataSet.Targets));
    }
    error = errors.Average();
    numEpochs++;
  }
}
  1. 然后,我们将添加方法来正向和反向传播网络:
private void ForwardPropagate(params double[] inputs)
{
  var i = 0;
  InputLayer.ForEach(a => a.Value = inputs[i++]);
  HiddenLayer.ForEach(a => a.CalculateValue());
  OutputLayer.ForEach(a => a.CalculateValue());
}

private void BackPropagate(params double[] targets)
{
  var i = 0;
  OutputLayer.ForEach(a => a.CalculateGradient(targets[i++]));
  HiddenLayer.ForEach(a => a.CalculateGradient());
  HiddenLayer.ForEach(a => a.UpdateWeights(LearnRate, Momentum));
  OutputLayer.ForEach(a => a.UpdateWeights(LearnRate, Momentum));
}
  1. 最后,添加以下方法来计算整个网络和计算错误:
public double[] Compute(params double[] inputs)
{
  ForwardPropagate(inputs);
  return OutputLayer.Select(a => a.Value).ToArray();
}

private double CalculateError(params double[] targets)
{
  var i = 0;
  return OutputLayer.Sum(a => Mathf.Abs((float)a.CalculateError(targets[i++])));
}

这样就完成了神经网络代码。我们在下一节关于训练神经网络的训练部分留下了许多讨论区域。

训练神经网络

如您可能已经总结的那样,神经网络在训练之前基本上是无用的。在我们开始训练之前,我们应该更多地讨论一下神经元是如何被激活的。再次打开 Neuron 类,并查看 CalculateValue 函数。此方法根据其内部设置的权重计算输出,如下所述:

这里:

同时,请注意以下几点:

n = 作为输入连接的总神经元数

I = 传递给 Neuron 类的信号输入

O = 计算的输出

S = 具有如下图的 sigmoid 函数:

Sigmoid 函数

Sigmoid 函数基本上根据类似于前述图表的曲线(函数)在 0 和 1 之间分配值的加权总和。我们这样做是为了使每个神经元的输出均匀加权。同样,当我们考虑将输入数据输入到网络中时,我们也希望将值规范化到 0 和 1 之间。如果我们不这样做,单个神经元或输入可能会偏置我们的整个网络。这就像用锤子敲你的大拇指,接下来的几秒钟内只能感觉到大拇指的疼痛,但我们不希望我们的网络对这种野外的输入做出反应。相反,我们希望用 sigmoid 函数来平缓我们的网络。

激活警告

让我们进一步推迟对训练的讨论,并准备一个简单的例子来看看它是如何工作的。再次打开 Unity 并执行以下步骤:

  1. Assets/ARCoreML/Scripts 文件夹中创建一个新的 C# 脚本,名为 EnvironmentScanner。然后,在您的编辑器中打开该脚本。

  2. 将如下所示的代码添加到类定义中:

[RequireComponent(typeof(AudioSource))]
public class EnvironmentalScanner : MonoBehaviour  //before me
  1. RequireComponent 是一个自定义的 Unity 属性,它强制一个 GameObject 在添加此组件之前需要特定的类。在这个例子中,我们需要一个 AudioSource 组件。

  2. 将以下新的属性/字段和方法添加到类中;不要删除任何内容:

public NeuralNet net;
public List<DataSet> dataSets;

private float min = float.MaxValue;
private float maxRange = float.MinValue;
private float[] inputs;
private double[] output;
private double temp;
private bool warning;
private AudioSource audioSource;
private double lastTimestamp;

public void Awake()
{ 
    int numInputs, numHiddenLayers, numOutputs;
    numInputs = 1; numHiddenLayers = 4; numOutputs = 1;
    net = new NeuralNet(numInputs, numHiddenLayers, numOutputs);
    dataSets = new List<DataSet>();
}
  1. 在 Unity 中,Awake 方法是特殊的,因为它在对象首次唤醒或变为活动状态时被调用。AwakeStart 的不同之处在于它在对象的初始化时被调用,而 Start 在对象渲染第一帧之前被调用。这种差异很微妙,通常只有在您担心对象加载时间时才相关。

    接下来,我们创建了一些临时输入变量来设置 输入隐藏输出神经元的数量。在这个例子中,我们将使用一个输入、四个隐藏和一个输出。这些输入用于在下一行创建 NeuralNet,随后初始化 dataSets 列表。

  2. 接下来,让我们修改 Start 方法,使其类似于以下内容:

void Start()
{ 
  dataSets.Add(new DataSet(new double[]{ 1,.1,0.0}, new double[] { 0.0,1.0,1.0 } ));
  net.Train(dataSets, .001);
  audioSource = GetComponent<AudioSource>();
}
  1. Start内部的第一行创建了一个非常简单的DataSet,具有输入和输出。由于我们使用单个输入和输出神经元,这些输入和输出映射 1 到 1,因此产生以下图表:

图片

训练输入图表

  1. 然后,net.Train使用最小误差.001训练神经网络。之后,它获取所需的AudioSource,记住RequireComponent属性,并将其设置为私有的audioSource字段。我们将使用声音来警告用户当他们太靠近时。考虑一下这些点作为函数描述的是什么。

  2. 最后,修改Update方法以包含以下内容:

void Update()
{
  if (warning)
  { 
    audioSource.Play();
  }
  else
  {
    audioSource.Stop();
  }
  // Do not update if ARCore is not tracking.
  if (Frame.TrackingState != FrameTrackingState.Tracking)
  {
    return;
  }

  min = float.MaxValue; 
  PointCloud pointCloud = Frame.PointCloud;
  if (pointCloud.PointCount > 0 && pointCloud.Timestamp > lastTimestamp)
  {
  lastTimestamp = pointCloud.Timestamp;
  //find min
    for (int i = 0; i < pointCloud.PointCount; i++)
    {
      var rng = Mathf.Clamp01((pointCloud.GetPoint(i)- transform.parent.parent.transform.position).magnitude);
      min = Mathf.Min(rng, min);
    }

    //compute output
    output = net.Compute(new double[] { (double)min });
    if(output.Length > 0)
    {       
      warning = output[0] > .001;
    }
    else
    {
      warning = false;
    }
  }  
}
  1. 这里有很多事情在进行中,所以让我们分解一下。我们首先检查warning是否为true。如果是,我们播放声音,否则我们停止播放;warning将是我们表示神经网络发出信号的标志。接下来,我们确保Frame正在跟踪,使用与之前看到的相同的代码。然后,我们重置min并从Frame获取当前点云。

    之后,我们确保pointCloud有点,并且是最新的。这是通过测试时间戳来检查的。然后,在if块内部,我们通过遍历所有点来计算当前的min。然后,我们通过net.Compute将这个值(最小点)推送到我们的神经网络中,这返回我们的信号或神经元输出。在这种情况下,我们正在测试.001以确定神经元是否发出激活信号。这会将警告设置为truefalse

  2. 保存代码并返回 Unity;确保您没有看到编译错误。

添加环境扫描器

现在我们有一个使用该组件的脚本,让我们将其添加到场景中作为一个新对象。返回到我们上次离开的编辑器,并继续以下步骤:

  1. 打开HelloAR场景。从菜单中选择文件 | 保存为,并将场景保存为MainAssets/ARCoreML文件夹中。

  2. 在层次结构窗口中找到并选择第一人称摄像机。请记住,您可以使用搜索面板。

  3. 右键单击(Ctrl + 点击 Mac)第一人称摄像机,并从上下文菜单中选择创建空对象。将对象命名为Environmental Scanner

  4. 选择新对象,在Inspector窗口中添加一个新的AudioSource组件。

  5. 在项目窗口中,在Assets/ARCoreML路径下创建一个新的文件夹,命名为Audio

  6. 从下载的代码文件夹中打开Resources文件夹,并将tone-beep.wav文件复制到您刚刚创建的Assets/ARCoreML/Audio文件夹中。

  7. Inspector窗口中打开Environmental Scanner对象,并设置AudioSource属性,如图所示:

图片

Inspector中设置AudioSource属性

  1. Inspector窗口中选中Environmental Scanner,然后点击添加组件按钮。添加我们之前编写的Environmental Scanner脚本。

  2. 打开构建设置对话框,确保将当前场景(Main)添加到构建中。确保从构建中删除任何其他场景。

  3. 连接、构建和运行。在房间内移动。那么,当您接近物体时会发生什么?在什么距离?

很好,所以我们已经有效地创建了一个备份或警告蜂鸣器,以便在您接近物体时通知您。显然,我们也可以简单地编写一个简单的阈值测试来测试当min接近时的情况。然而,这个简单的例子为我们理解训练工作提供了一个良好的基础。

反向传播解释

在这个例子中,我们正在对模型(监督学习)进行预训练,使其执行一个简单的函数,该函数由一系列输入(1.0, 0.1, 0)和预期的输出(0, 1.0, 1.0)描述,这在我们之前看到的图表/图中表示。本质上,我们希望我们的神经网络能够学习由这些点定义的函数,并能够输出这些结果。我们通过调用net.Train,传入datasets和最小预期误差来实现这一点。这样,网络通过反向传播错误通过网络中的每个神经元来训练,直到达到最小误差。然后,训练停止,网络声明自己已准备好。

反向传播使用一个简单的迭代优化算法,称为梯度下降,它使用最小误差来最小化每个神经元的输入权重,以便达到全局最小误差。为了完全理解这一点,我们需要进入一些微分学和导数的知识。相反,我们将走捷径,只看看NeuralNet类的Train方法中的代码在做什么:

public void Train(List<DataSet> dataSets, double minimumError)
{
  var error = 1.0;
  var numEpochs = 0;
  while (error > minimumError && numEpochs < int.MaxValue)
  { 
    var errors = new List<double>();
    foreach (var dataSet in dataSets)
    {
      ForwardPropagate(dataSet.Values);
      BackPropagate(dataSet.Targets);
      errors.Add(CalculateError(dataSet.Targets));
    }
    error = errors.Average();
    numEpochs++;
  }
}

这里的代码相对简单。我们设置了一个errornumEpochs。然后,我们启动一个while循环,该循环在error大于minimumError(全局)并且numEpochs小于最大int值时结束。在循环内部,我们遍历dataSets中的每个dataSet。首先,使用ForwardPropagate对数据集值的输入进行操作以确定输出。然后,使用BackPropagate对数据集的目标值进行调整,使用梯度下降法调整每个神经元的权重。让我们看看BackPropagate方法内部的情况:

private void BackPropagate(params double[] targets)
{
    var i = 0;
    OutputLayer.ForEach(a => a.CalculateGradient(targets[i++]));
    HiddenLayer.ForEach(a => a.CalculateGradient());
    HiddenLayer.ForEach(a => a.UpdateWeights(LearnRate, Momentum));
    OutputLayer.ForEach(a => a.UpdateWeights(LearnRate, Momentum));
}

这个方法优雅地使用System.Linq中的ForEach遍历每个神经元层。首先,它计算输出和隐藏层的梯度,然后以相反的顺序调整权重:首先是隐藏层,然后是输出层。接下来,我们将剖析CalculateGradient方法:

public double CalculateGradient(double? target = null)
{
  if (target == null)
    return Gradient = OutputSynapses.Sum(a => a.OutputNeuron.Gradient * a.Weight) * Sigmoid.Derivative(Value);

  return Gradient = CalculateError(target.Value) * Sigmoid.Derivative(Value);
}

我们可以看到CalculateGradient方法接受一个名为target的可空double。如果targetnull,则通过将先前梯度乘以输入权重来计算Gradient。否则,Gradient通过将误差乘以Sigmoid的导数来计算。记住,sigmoid 是我们的激活函数,这基本上是我们试图最小化的。如果你从微积分中回忆起来,我们可以通过对函数求导来确定其最小值或最大值。实际上,为了使用梯度下降法进行反向传播,你的激活函数必须是可导的。

梯度下降法解释

梯度下降法使用损失或误差函数的偏导数来将更新传播回神经元权重。在这个例子中,我们的成本函数是 Sigmoid 函数,这与我们的激活函数相关。为了找到输出神经元的梯度,我们需要对 Sigmoid 函数求偏导数。以下图表显示了梯度下降法如何沿着导数下降以找到最小值:

图表

梯度下降算法可视化

如果你计划花更多时间学习神经网络、深度学习或机器学习,你肯定会更深入地学习梯度下降和反向传播的数学。然而,你不太可能进一步接触到编程神经网络的基礎概念,因此这一章将是一个很好的未来参考。

让我们看看CalculateError函数,它简单地从神经元的输出值中减去其应有的值:

public double CalculateError(double target)
{
    return target - Value;
}

然后,滚动到以下代码中的UpdateWeights方法:

public void UpdateWeights(double learnRate, double momentum)
{
    var prevDelta = BiasDelta;
    BiasDelta = learnRate * Gradient;
    Bias += BiasDelta + momentum * prevDelta;

    foreach (var synapse in InputSynapses)
    {
        prevDelta = synapse.WeightDelta;
        synapse.WeightDelta = learnRate * Gradient *         
                               synapse.InputNeuron.Value;
        synapse.Weight += synapse.WeightDelta + momentum * prevDelta;
    }
}

UpdateWeights随后根据learnRatemomentum调整每个神经元的权重;learnRatemomentum设置了神经网络学习的速度。我们通常希望控制算法的学习率,以防止过拟合和陷入局部最小值或最大值。之后,代码相对简单,它通过循环突触连接并使用新值更新权重。Bias用于控制 Sigmoid 激活函数的截距,从而允许神经元调整其初始激活函数。我们可以在以下图表中看到Bias如何改变激活函数:

图表

偏置对 Sigmoid 激活函数的影响

调整Bias允许神经元在除了 0 以外的值开始放电或激活,如前图所示。因此,如果Bias的值为 2,那么神经元将在-2 处开始激活,如图所示。

定义网络架构

我们刚刚学习了如何编写和使用一个简单的神经网络来警告用户当他们离物体太近时。当你查看代码时,请注意,这些值中的大多数都是在训练过程中内部调整的。在使用神经网络时,理解这些基本原理非常重要:

  • 激活函数: 如果你没有使用 sigmoid 函数,那么你还需要找到你的激活函数的偏导数,以便在使用反向传播时使用梯度下降。

  • # 输入神经元: 这不仅会设置网络的复杂性,还会确定隐藏或中间层的神经元数量。

  • # 输出神经元: 你需要你的网络有多少个输出或分类方式?

  • # 隐藏层/神经元: 作为一条好的经验法则,你希望使用输入和输出神经元的平均值,或者就是 输入+输出/2。我们将在下一个示例中应用这个规则。

  • 训练方法: 我们的神经网络支持两种训练方法:最小误差或按 epoch 或迭代次数。我们更倾向于使用最小误差,因为这能更好地量化我们的模型。

本章源代码下载中包含了一个工作示例,在资产包中展示了我们的简单神经网络被用作环境或对象识别器。回到 Unity,执行以下步骤来设置此示例:

在开始之前,请确保保存你的现有项目或下载一个新的 ARCore 模板。资产导入将覆盖你的现有文件,所以如果你想在继续之前保留任何早期工作,你应该先进行备份。

  1. 从菜单中选择“Assets | 导入包 | 自定义包”。使用文件对话框导航到书籍下载源代码的Code/Chapter_8文件夹,并导入Chapter_8_Final.unitypackage

  2. Assets/ARCoreML文件夹中打开主场景。

  3. 打开构建设置对话框,确保主场景被添加到构建中并且是激活的。

  4. 连接、构建和运行。现在当你运行应用时,你将在界面顶部看到两个按钮:一个写着“训练 0”,另一个写着“训练 1”。

  5. 将你的设备对准你希望神经网络识别的区域。确保 ARCore 正在屏幕上识别大量的蓝色点,然后按下“训练 1”按钮;这将向网络发出信号,表明你希望它识别这个特征集。

  6. 将设备对准你不想神经网络识别的区域,并按下“训练 0”按钮;这将向网络强化你不想它识别这个区域。

  7. 在原地保持不动,继续这个过程。将你的设备对准你希望重复识别的区域,并按下“训练 1”按钮。同样,对于你不想识别的区域,也这样做,但确保按下“训练 0”按钮。训练大约 10 次后,你应该开始听到警告蜂鸣声,这表明神经网络已经识别了你的区域。

  8. 如果你开始听到警告音,那将是一个指标,表明你的神经网络(NN)开始学习。继续在那个地方旋转,训练网络,确保通过按下适当的按钮来纠正网络。你可能需要做几次(可能是 20 到 50 次左右)才能注意到 NN 识别了你想要区域。

确保在训练网络时,你能看到很多蓝色点。如果你看不到任何点,你实际上就是在用空数据训练。

  1. 最后,当你的网络完全训练完成后,你应该能够慢慢地绕着房间转一圈,并听到当你的设备识别到你选择区域时。

使用我们简单的神经网络(NN),我们能够构建一个对象/特征识别器,可以训练它识别特定的特征、地点或对象。这个例子相当简单,并不非常稳健或准确。然而,考虑到有限的训练数据集,它能够很好地在实时识别特征。打开环境扫描器脚本,我们将看看网络是如何配置的:

  1. 滚动到唤醒方法,看看网络是如何创建的:
public void Awake()
{ 
  int numInputs, numHiddenLayers, numOutputs;
  numInputs = 25; numHiddenLayers = 13; numOutputs = 1;
  net = new NeuralNet(numInputs, numHiddenLayers, numOutputs);
  dataSets = new List<DataSet>();
  normInputs = new double[numInputs];
}
  1. 注意这次我们创建了一个包含25个神经元的输入层和1个输出。如果我们坚持我们的隐藏层是输入和输出的平均值的通用规则,那么这就等于13个神经元((25+1)/2=13)。

  2. 我们从Start方法中移除了初始的 NN 设置和训练,并将其移动到新的Train方法底部:

private void Train()
{ 
  net.Train(dataSets, 100);
  trained = dataSets.Count > 10;
}
  1. 这次,我们使用了一种不同的训练形式,称为时代。当我们不确定预期的误差是什么,或者它需要改变,就像在这个例子中一样,我们会使用这种训练形式。想想看——当我们用一个非常有限的数据集开始训练我们的网络时,由于我们的数据不足,我们的错误率会很高。这意味着我们永远无法将我们的网络训练到最小误差。因此,对于每个训练周期,只运行我们的训练算法一定数量的迭代或时代似乎更有意义。

  2. Train方法之前是TrainNetwork,如下所示:

public void TrainNetwork(float expected)
{
  this.expected = expected;
  training = true;
}
  1. TrainNetwork是一个公共方法,我们用它来向环境扫描器发出信号,以启动一个具有预期结果的训练周期。这允许我们在 UI 按钮上设置事件处理器,以调用此方法并传递预期值。当你按下“训练 0”按钮时,TrainNetwork会传递0.0,而在按下“训练 1”按钮后,会传递1.0

  2. 滚动到更新方法,看看以下代码段:

if (training)
{ 
  dataSets.Add(new DataSet(normInputs, new double[] { expected }));
  training = false;
  Train();
}
  1. 这是检查训练标志的代码块。如果它被设置,它会收集归一化的输入并将它们添加到dataSets中,并带有预期的结果。然后我们关闭标志并调用Train

  2. 滚动到上面的代码块,你可以看到我们是如何对训练输入进行归一化的:

for (int i = 0; i < normInputs.Length; i++)
{
  if (i < pointCloud.PointCount)
  {
    //normalize the inputs
    normInputs[i] = inputs[i] / max;
  }
  else
  {
    normInputs[i] = 0;
  }
}
  1. 在这里,我们正在归一化inputs。一个input代表一个识别点与相机(用户)之间的距离或大小。归一化是将您的数据缩放到01范围内的值。在这种情况下,我们通过找到每个点的最大距离,然后使用它来除以所有其他输入来实现这一点。循环中的测试是为了确保我们始终为每个输入神经元设置一个值。

其余的代码与我们之前写的类似,不值得再次讨论。

网络视图的世界

那么,这里到底发生了什么,网络到底在识别什么?本质上,我们正在将我们对世界的 3D 视图展平成 2D 线或曲线。以下是如何看起来归一化的典型示例:

图片

归一化输入点

这些输入代表了神经网络正在训练或可能对抗的归一化视图。如果您训练网络识别那条线,那么当它检测到那条线时,警告声音应该响起。当然,您添加的点越多,您的识别器可能工作得越好,也可能不会。我们将把它留给您自己进一步测试网络。

神经网络在 1990 年代末和 2000 年代初非常受游戏和图形开发者的欢迎。神经网络在各种 AI 场景中取得了一些成功,尤其是在驱动游戏方面,但最终,其他专门设计的技巧胜出,即直到最近,随着卷积神经网络等新技术的出现。这些新的成功导致了深度学习技术和平台的大幅增长。

这个简单的神经网络可以扩展以识别您想要的其他简单函数或模式。然而,如果我们尝试将其用于我们之前确定为 AR 关键的其他任何识别任务,它将表现得很差。因此,在下一节中,我们将探讨如何使用谷歌开发的新平台 TensorFlow 来解决我们的识别问题。

练习

独立完成以下练习:

  1. 解释无监督学习、监督学习和强化学习之间的区别。这更多的是一种思维练习,但真正理解这些区别将是有益的。

  2. 修改原始神经网络示例,当检测到超过一定距离的物体时发出警告。

  3. 在第二个示例中,如果您按长度对输入进行排序会发生什么?它仍然有效吗?

  4. 在第二个示例中,向网络添加一个额外的输出神经元。您还需要一个新的训练按钮,并需要修改TrainNetwork函数以接受两个inputs

TensorFlow

在机器学习领域,有一个新出现的名字叫做 TensorFlow,它也是由 Google 开发的,正在掀起一股令人印象深刻的浪潮。TensorFlow 是一个完整的机器学习平台,实际上它不仅仅是一个带有大量内置工具的执行引擎。更令人印象深刻的是,你可以在离线状态下使用大规模数据集训练高级神经网络、卷积神经网络、胶囊网络或你需要的一切。然后,你将这些训练好的网络放在一个称为 MobileNet 的移动设备上,以便快速识别和分类复杂对象。在本节中,我们将暂时放下 ARCore,来看看即将到来的 TensorFlow 的强大功能。

TensorFlow 是一个高级的机器学习资源库和工具包,值得你花时间去学习,了解你是否需要进行任何高级的识别任务。然而,请记住,这个工具需要你在数学方面有高级知识,并且对 Python 有实际的操作经验。

我们将运行 TensorFlow 的 Android 示例,不仅是为了了解工具的强大功能,也是为了理解可能实现的内容。尽管 Google 正在构建 TensorFlow 和 ARCore,但我们只能假设未来将构建新的集成工具。然而,目前,让我们打开命令提示符或 shell 并开始吧:

  1. 从你的用户文件夹或根目录运行以下命令:
mkdir TensorFlow
cd TensorFlow
  1. 创建 TensorFlow 目录并导航到它。然后,输入以下命令:
git clone https://github.com/tensorflow/tensorflow
  1. 打开 Android Studio。从欢迎界面,选择“打开现有的 Android Studio 项目”。

  2. 使用对话框导航到,选择 TensorFlow/tensorflow/examples/android 文件夹,然后点击“确定”。

  3. 如果提示你进行 Gradle 同步,请点击“确定”。

  4. 从项目侧面板下的 Gradle 脚本中打开 build.gradle 文件,并将 nativeBuildSystem 变量设置为 none,如图所示:

def nativeBuildSystem = 'none'
  1. 连接你的设备并点击运行按钮,顶部的绿色箭头图标。遵循任何必要的构建步骤,并让应用推送到你的设备。

  2. 构建完成后,Studio 将推送四个应用到你的设备:TFClassifyTFDetectTFSpeechTFStylize。尝试每个示例,并观察一些在设备上运行的网络的强大功能。

以下是一个 TFDetect 应用运行并非常准确地分类狗和人的示例:

图片

TFDetect 正确分类狗和人的功能

很遗憾,运行 TensorFlow 与 ARCore 需要的组件尚未完全准备好,因此在撰写本文时,我们无法完成一个完整的示例。然而,AR 应用程序的机器学习未来无疑将与 TensorFlow 或其他第三方解决方案相结合,在 TensorFlow 的基础上进行。谷歌在人工智能/机器学习领域拥有多年的经验,从开发自动驾驶汽车到 Google Home。它将这些年的知识融入 TensorFlow,使其对全世界开放。如果你计划构建自己的机器学习对象/特征识别,不花时间学习 TensorFlow 算是愚蠢之举。

我们计划构建一个在 ARCore 中运行的经过训练的 MobileNet 的示例。遗憾的是,组件尚未完全准备好,这导致了一个过于复杂的示例。大约在本书出版时,我们可能会看到更多工具的开发,以使将 TensorFlow 集成到 ARCore 中变得更加容易。

摘要

在本章中,我们深入探讨了机器学习的深层次——或者说深度学习的深层次——领域。我们首先讨论了机器学习的重要性以及我们可以在 AR 中使用它的应用。然后,我们探讨了机器学习如何通过无监督学习、监督学习和强化学习等不同学习方法来教授机器学习代理进行学习。接着,我们查看了一个特定的学习机器学习算法的例子,称为神经网络,通常被称为深度学习。这引导我们构建了一个简单的神经网络,你也可以用它来自己学习神经网络的复杂性。神经网络非常复杂,不太直观,因此了解它们的基本结构非常重要。然后,我们在一个非常简单的数据集上训练了这个网络,以通知用户他们是否离物体太近。这进一步讨论了神经网络如何使用梯度下降算法进行反向传播训练。之后,我们查看了一个增强的例子,它允许你训练网络来识别区域或对象。最后,我们探讨了当前机器学习的王者 TensorFlow,并查看了一个快速示例,展示了可能性和即将到来的是什么。

在下一章中,我们将回到使用 ARCore 构建实际示例。我们将构建一个简单的设计应用程序,让用户能够虚拟装饰他们的生活空间。

第九章:建筑设计的灯光融合

这是两个章节中的第一个,我们将构建现实世界的 AR 应用,您可以从中学习并向朋友和家人展示。与前面的章节不同,这次我们将从零开始构建我们的 AR 应用。这样,我们可以学习将 ARCore 集成到 Unity 项目中所需的具体细节。在本章中,我们将有很多内容要介绍,所以让我们开始吧。以下是我们将涵盖的主要主题的快速总结:

  • 设置项目

  • 放置内容

  • 构建 UI

  • 与虚拟交互

  • 灯光和阴影

我们应用的前提是一个用于建筑和设计的 AR 工具。目前,设计应用在 AR 领域非常受欢迎,并且非常适合 ARCore 提供的工具集。

能够在或超过真实世界物体中虚拟放置对象,并立即看到它的样子,这对设计师和建筑师来说有巨大的好处。现在,使用 AR 应用的设计师可以立即用他们的愿景改变空间。想象一下,再也不用移动沙发 15 次才能把它放得恰到好处。

设置项目

我们将使用示例项目作为创建新项目的模板。在撰写本文时,使用 beta 版本进行 ARCore 资产导入仍然需要相当的项目设置。理想情况下,我们希望从头开始创建项目,但我们将做下一件最好的事情。下一件最好的事情是将项目从 GitHub 克隆到我们选择的新的文件夹中。你可以通过打开命令提示符并按照以下步骤开始:

  1. 在您的根文件夹或工作文件夹中创建一个新的文件夹,并执行以下命令来下载 ARCore 模板:
mkdir ARCore
cd ARCore
git clone https://github.com/google-ar/arcore-unity-sdk.git ARCoreDesign
  1. 这将创建一个新的文件夹。切换到它,并从 GitHub 下载项目模板。

  2. 打开 Unity 到项目对话框,并点击打开

  3. 使用文件夹对话框找到并选择我们刚刚将代码下载到的ARCoreDesign文件夹,如下所示:

图片

打开 ARCoreDesign 项目

  1. 等待 Unity 加载。确保注意编辑器状态栏底部的任何编译器错误。如果您看到它们,这意味着您可能有一个版本冲突,或者某些东西已经改变。检查您的版本,并根据需要尝试升级或降级。

  2. 我们首先要做的是组织我们的文件夹结构。在项目窗口中,通过右键单击(Ctrl + 点击 Mac)Assets文件夹,并从上下文菜单中选择创建 | 文件夹来创建一个名为ARCore_Design的新文件夹。

  3. 在新文件夹下方,添加ScriptsPrefabsScenesMaterialsModels文件夹,如图所示:

图片

设置文件夹结构

我们刚才用于设置项目的技术,当您处理其他您想要自己定制的样本项目时非常有用。Unity 通过文件夹来管理项目,名称与文件夹名称相对应。我们不会担心设置源代码控制,因为您可以根据自己的需要自行完成。

如果您正在设置这个项目不是为了学习,那么您现在应该考虑一个源代码解决方案。Dropbox 或其他文件共享解决方案在紧急情况下可以工作,但这不是适合多个开发者的解决方案。有很多免费且相对简单的解决方案与 Unity 一起工作,所以花些时间选择一个适合您的方案。

构建场景

为了节省我们一些时间,我们将加载 HelloAR 场景并将其修改以满足我们的需求。按照以下步骤操作:

  1. 通过双击它来打开 Assets/GoogleARCore/HelloARExample/Scenes 文件夹中的 HelloAR 场景。

  2. 从菜单中选择文件 | 保存场景为,将场景保存在新的 Assets/ARCore_Design/Scenes 文件夹中,并将其命名为 Main

除了我们之前使用的样本之外,从现在开始,如果我们需要修改一个文件,我们将将其复制到一个新的适当文件夹中,并重命名它。在修改外部资产时,这是一个很好的实践。这样,当你用新版本更新资产时,你的更改不会被覆盖。

  1. 从菜单中选择 编辑 | 项目设置 | 播放器

  2. 在检查器窗口中,点击 Android 选项卡,并将包名修改为 com.Packt.ARCoreDesign,如以下屏幕截图所示:

图片

在播放器设置中编辑包名

  1. 从菜单中选择文件 | 构建设置。

  2. 点击 HelloAR 场景上的复选框以将其关闭。然后,点击 添加打开的场景 以将新的 Main 场景添加到构建中。确保平台选项为 Android,并确认一切设置正确,如以下摘录所示:

图片

设置构建设置

  1. 连接您的设备,然后点击 构建并运行。您将提示保存 APK。输入您用于包的相同名称(com.Packt.ARCoreDesign)并点击 保存。这将启动构建。第一次构建可能需要一些时间,所以拿一杯饮料或稍作休息。

  2. 在您的设备上运行应用程序,并确认一切按预期运行。如果出现任何问题,请参考第十一章,性能提示和故障排除,以获取帮助。

在您完成本章的练习时,尽可能经常构建。构建可以快速告诉您是否有任何重大问题。

修改基础场景

我们接下来要做的就是修改基础场景以满足我们的需求。打开 Unity 并按照以下步骤操作:

  1. 在层次结构窗口中选择并拖动 PointCloud 对象,并将其拖放到项目窗口中的Assets/ARCoreDesign/Prefabs文件夹中,如下所示:

图片

使用 PointCloud 对象创建预制件

  1. 这将创建 PointCloud 对象的预制件。将预制件想象成一个模板或几乎就像一个类。任何时候我们想要重复使用 PointCloud 对象,我们都可以将其拖入场景或实例化它。

  2. 在层次结构窗口中选择 PointCloud 对象,并按 Delete 键。找到它,并点击它。这将删除该对象;我们现在不需要它。

  3. 在层次结构窗口中将ExampleController对象重命名为SceneController

  4. Assets/ARCoreDesign/Scripts文件夹中选择,从菜单中选择 Assets | Create | C# Script。将脚本命名为SceneController。然后双击它以在最喜欢的代码编辑器中打开脚本。

  5. 现在,回到 Unity。在项目搜索面板中输入helloarcontroller以过滤窗口到脚本。双击脚本以在代码编辑器中打开它。

  6. 将整个HelloARController.cs脚本复制并粘贴到SceneController.cs文件的内容上;是的,全部。我们实际上是在制作一个副本。重命名你的类并更改命名空间,如下所示:

namespace Packt.ARCoreDesign
{
...  //code omitted
public class SceneController : MonoBehaviour  //rename me
***...*** //code omitted
}  // don't forget the closing brace at the end
  1. 我们用namespace包装所有新的代码文件,以避免命名冲突。如果你使用了大量资产,Unity 中命名冲突发生的频率更高。一般来说,如果你是 Unity 的新手,你将使用很多第三方资产。

  2. 确保以下所有新的using语句都已识别,如下所示:

 using System.Collections.Generic;
 using GoogleARCore;
 using UnityEngine;
 using UnityEngine.Rendering; 
 using GoogleARCore.HelloAR;

#if UNITY_EDITOR
    using Input = GoogleARCore.InstantPreviewInput; 
#endif
  1. 保存文件并返回 Unity。务必注意查看任何编译错误。

  2. 在层次结构窗口中选择SceneController对象,并在检查器窗口中点击添加组件按钮。

  3. 在搜索面板中输入scene,然后选择以下摘录中的场景控制器脚本:

图片

添加场景控制器脚本作为组件

  1. 点击靶心图标以设置场景控制器的属性。确保它们与 Hello AR 控制器(脚本)匹配。当所有属性匹配时,点击 Hello AR 控制器(脚本)旁边的齿轮图标,并在上下文菜单中选择移除组件。你现在应该只剩下具有相同属性设置的场景控制器(场景)组件。

  2. 在你的设备上连接、构建并运行应用。如果你遇到任何问题,检查编译错误并确保你正确设置了组件。

当然,我们可以为所有主要脚本创建副本,但暂时这样就可以了。显然,我们还有很多工作要做,但这是一个良好的开始。确保保存场景和你的项目。在下一节中,我们将查看允许用户放置的内容,并选择放置的位置。

环境和放置内容

我们已经介绍了如何与环境交互以放置内容的基本知识。我们现在想做的交换和添加新内容(对不起,安迪)。毕竟,我们设计应用程序的整个前提是在 AR 中可视化新家具或其他物品在空间中的样子。让我们通过打开您最喜欢的网络浏览器并跟随操作来开始吧:

  1. 浏览到turbosquid.comTurboSquid是一个优秀的 3D 模型资源网站,提供免费和付费模型。

对于 AR / VR 和混合应用程序,您通常希望模型细节较少。例如,Android 等移动设备无法很好地渲染精细的模型。在购买任何模型之前,请确保您了解设备的渲染限制。

  1. 在网站上搜索ligne roset

当然,您可以使用任何喜欢的 FBX 模型,但第一次尝试使用建议的模型。如果您不确定自己在做什么,与 3D 模型一起工作可能会很令人沮丧。

  1. 将搜索过滤为免费模型,并选择 Ligne Roset Citta 沙发和扶手椅,如下所示:

从 TurboSquid 下载模型

  1. 点击下载按钮。您可能需要先创建账户并登录。

  2. 点击标记为Ligne_Roset_Citta_FBX.zip的链接。这将下载 zip 文件。

  3. 将文件解压到新文件夹中,然后打开该文件夹。选择并拖动mpm_vol.07_p24.FBX文件到 Unity 中,并将其拖放到Assets/ARCore/Models文件夹中,如下所示:

将模型拖入模型文件夹

  1. 选择模型,然后在检查器窗口中确认 Model | 比例因子设置正确,如下所示:

在导入模型后检查模型比例

  1. 在本例中,模型使用的是文件比例,设置为0.001。您可能需要根据模型使用的比例进行调整。目前,这个比例是合适的。

  2. 我们提供的模型包含chairsofa。幸运的是,我们可以相对容易地将它们分开。将模型拖放到层次结构窗口的开放区域。您应该会看到chairsofa被添加到场景中。

  3. 再次在层次结构窗口的空白区域单击以禁用模型选择。

  4. 从菜单中选择 GameObject | 创建空对象;将对象重命名为sofa。再次执行此操作以创建另一个新对象,并将其命名为armchair。确保armchairsofa游戏对象设置在原点姿态,位置为(0, 0, 0),旋转为(0, 0, 0)。如有需要,选择对象并检查检查器窗口。

  5. 展开 mpm_vol.07_p24 模型,将子对象armchair拖动到新的armchair游戏对象上。为沙发部件重复此过程,您的层次结构窗口应类似于以下内容:

创建两个新模型

  1. 我们刚才所做的就是创建新的锚点,然后分解我们的模型。锚点允许我们根据固定的锚点调整模型。在建模软件使用不同参考的情况下,您通常需要这样做。我们的模型就是这样。选择 24 Ligne Roset Citta 扶手椅子对象,并检查检查器窗口。

  2. 将“扶手椅”变换的位置更改为(00.250),如图所示:

设置扶手椅的位置变换

  1. 确保位置设置为 X=0和 Y=-.25,并保持旋转不变。我们正在将“扶手椅”的位置从原位置向下偏移一点。这是因为 ARCore 目前倾向于跟踪过高的平面;希望到发布时这个问题能够得到解决。无论如何,你可以在以后任何时候以任何方式调整“椅子”的位置偏移。

  2. 从“层次”窗口拖动“扶手椅”对象并将其放入Assets/ARCoreDesign/Prefabs文件夹。同样,对“沙发”对象重复此过程。这将创建“扶手椅”和“沙发”的预制件。

  3. 从“层次”窗口删除“扶手椅”、“沙发”和原始 mpm_vol.07_p24 对象。

  4. 在“层次”窗口中选择SceneController对象,然后在检查器窗口中,将 Andy Android Prefab 设置为“扶手椅”预制件,如下所示:

设置 SceneController 的预制件槽

  1. 保存项目,连接,并在您的设备上运行应用程序。让一些表面出现,然后放置一把或两把椅子。您可以随时返回并交换沙发。请注意,您可能还需要调整“沙发”模型的定位。

好的,现在我们可以放置一些家具了,但您很快就会意识到,现在平面更碍事了。让我们看看在下一节中如何通过添加一些 UI 来开启和关闭平面。

构建 UI

在这个阶段,我们希望用户能够清除场景并关闭平面。平面有助于识别我们可以放置对象的表面,但它们实际上会分散用户的注意力。我们将通过构建一个非常简单的 UI 并添加几个按钮来实现这一点。幸运的是,Unity 有一个非常强大的 UI 系统,称为uGUI,它将允许我们快速完成这项工作。打开 Unity 编辑器的Main场景并按照以下步骤操作:

  1. 点击“层次”窗口的空白区域以确保您的选择已清除。我们这样做是为了避免错误地将对象附加到其他对象上。

  2. 从菜单中选择 GameObject | UI | Canvas。将新对象命名为UI,并确保此对象的属性与以下摘录中的检查器窗口相匹配:

设置新 UI 画布的属性

  1. 我们在这个画布上使用的设置允许我们的子UI对象根据特定分辨率自动与屏幕大小缩放。如果我们不这样做,我们的UI控件在每台设备上的缩放方式都会不同。这使我们能够保持一致的外观,这是一个好事。

  2. 选择 UI 画布,从菜单中选择 GameObject | UI | Panel 以向画布添加一个新的子面板。

  3. 选择新的面板对象。在检查器窗口中,点击“添加组件”,然后搜索并添加一个网格布局组组件。然后,设置此组件的属性以匹配以下屏幕摘录:

图片

设置组布局网格(脚本)属性

  1. 网格布局组是一个用于自动布局对象的实用工具。布局将自动调整大小并调整其子网格组件。

  2. 在仍然选择面板的情况下,将图像组件的颜色属性更改为透明。通过点击颜色属性旁边的颜色选择区域,并将颜色设置为#FFFFFF00Alpha 0来完成此操作。

  3. 在层次窗口中选择面板对象,从菜单中选择 GameObject | UI | Button。将按钮重命名为“清除”。

  4. 展开清除按钮并选择子对象。将文本组件的文本属性更改为清除。

  5. 重复第六步和第七步,为一个新的按钮“平面”操作。完成之后,你的层次窗口和游戏窗口应该类似于以下摘录:

图片

2D 视图中的完成按钮和面板

  1. 通过点击场景窗口顶部的 2D 按钮,你可以以 2D 视图查看你的场景。这对于预览你正在构建的 UI 元素非常有用。之后,你可以使用鼠标和键盘自行调整视图。

  2. 连接、构建和运行。按钮目前还不能工作,但改变方向看看按钮如何缩放。

随意设计这些按钮,因为毕竟这也是你的应用。你也可以添加一个滑动菜单。关于 Unity uGUI 开发,有许多优秀的资源和好书可以指导你如何扩展 UI 以符合你的外观和感觉。

编写按钮脚本

显然下一步是让这些按钮工作。当然,我们需要添加一些脚本,如下所示:

  1. 在代码编辑器中打开我们之前创建的SceneController脚本。在Update方法之前,插入以下代码段:
private List<GameObject> m_sceneObjects = new List<GameObject>();
private List<GameObject> m_scenePlanes = new List<GameObject>();
private bool m_planeOnState;
public void ClearScene()
{
  foreach(var obj in m_sceneObjects)
  {
    Destroy(obj);
  }
  m_sceneObjects.Clear();
}
public void Planes()
{
  m_planeOnState = !m_planeOnState;
  //turn plane visibility on or off
  foreach(var plane in m_scenePlanes)
  {
    plane.SetActive(m_planeOnState);
  }
}
  1. 在此代码中,我们首先创建一些列表来存储场景中的物体(m_sceneObjects)和平面(m_scenePlanes),并添加一个新的布尔值来跟踪平面的状态m_planeOnState(可见或不可见)。接下来,我们添加两个新的方法(ClearScenePlanes)。ClearScene使用foreach遍历m_sceneObjects,并使用Destroy方法从场景中删除对象。Destroy是用于从场景中删除和清理游戏对象的方法。Planes方法翻转m_planeOnState的状态,然后遍历平面并使用SetActive设置它们的状态。如果一个对象是活动的,这意味着它在场景中是可见的并且正在更新。一个非活动的对象是被禁用的,并且不会渲染。

在这个例子中,我们保持与相同的命名约定一致,以匹配代码风格。如果你不习惯使用m_来表示私有成员变量,请不要使用它。你可能还想要重构此代码,并将andyObject等名称替换为更合适的名称。Visual Studio 有一套出色的重构工具,可以轻松完成此类任务。

  1. Update方法中向下滚动,并在指定的行之后添加一行:
var andyObject = Instantiate... //after me
m_sceneObjects.Add(andyObject);
  1. 这行代码只是将andyObject(现在命名不佳)添加到我们的场景对象列表中。andyObject首先使用Instantiate方法实例化。将Instantiate视为Destroy的相反操作。

  2. 向上滚动并添加到指定的行之后的一行:

GameObject planeObject = Instantiate... //after mem_scenePlanes.Add(planeObject);
  1. 这里也是一样,我们正在将新实例化的planeObject添加到场景平面的列表中。

  2. 保存文件并返回 Unity。我们现在需要连接按钮。像往常一样,等待编译器完成以确保你没有创建语法错误。

  3. 选择清除按钮,并在检查器窗口中滚动到按钮组件。点击底部的+按钮添加一个新的事件处理程序,然后设置处理程序的属性如下:

添加按钮事件处理程序

  1. 为平面按钮重复此过程。这次,连接Planes方法。

  2. 连接、构建并运行。尝试放置一个对象,然后使用按钮清除它。

现在,你应该能够打开和关闭平面的可见性,并清除你创建的任何对象。在下一节中,我们将扩展我们的 UI,以便用户可以与对象交互。

与虚拟交互

我们希望用户能够根据需要放置、移动或调整他们的对象姿态。如果你还记得,姿态代表了一个物体在 3D 空间中可以表示的六个自由度。在我们开始设置物体之前,我们需要能够选择一个物体。选择物体后,我们希望能够勾勒出它,以便向用户标识为已选择。由于勾勒物体听起来像是一个基本的第一步,让我们先解决这个问题。按照以下步骤创建物体勾勒:

  1. 返回 Unity。在Assets/ARCoreDesign/Materials文件夹中创建一个新的文件夹,并将其命名为Shaders

  2. 在项目窗口中,在新的文件夹内右键单击(Ctrl + 点击 Mac),从上下文菜单中选择创建 | 着色器 | 标准表面着色器。将新着色器的名称命名为ARMobileSpecularOutline

  3. 双击ARMobileSpecularOutline着色器,在您的代码编辑器中打开它。

  4. 删除文件的内容。我们将用之前使用的 ARCore 移动高光着色器来替换它。

  5. 在您的文本编辑器中打开MobileSpecularWithLightEstimation.shader文件,并将整个内容复制到剪贴板。该文件位于Assets/GoogleARCore/HelloARExample/Materials/Shaders文件夹中。

  6. 将剪贴板的内容粘贴到我们刚刚创建的ARMobileSpecularOutline.shader文件中。再次,我们正在复制示例源并将其转换为我们的代码。

虽然这个着色器是我们光估计着色器的副本,并将使用光估计,但我们希望尽可能保持变量名称简洁。通常,我们将光估计添加到着色器的名称中。然而,在这个例子中,我们将使用 AR 前缀来提醒我们这个着色器使用光估计,并且针对 AR 进行了优化。

  1. 将着色器的名称(顶部行)更改为以下内容:
Shader "ARCoreDesgin/ARMobileSpecularOutline"
  1. 接下来,我们需要在文件顶部进行几个编辑。将属性部分更改为以下内容,并添加未高亮的新的行:
Properties
{
  _Albedo ("Albedo", Color) = (1, 1, 1, 1)
  ***_***Shininess ("Shininess", Range (0.03, 1)) = 0.078125
 _MainTex ("Base (RGB) Gloss (A)", 2D) = "white" {}
 [NoScaleOffset] _BumpMap ("Normalmap", 2D) = "bump" {}
  _Outline ("_Outline", Range(0,0.1)) = 0
  _OutlineColor ("Color", Color) = (1, 1, 1, 1)
}
  1. 这添加了三个新的属性:_Albedo_Outline_OutlineColor。我们添加_Albedo是为了在我们的材质上设置颜色而不使用纹理。_Outline定义了轮廓的大小,而_OutlineColor则指的是颜色。

  2. 在标识的行之后,注入以下代码块:

Tags { "RenderType"="Opaque" }
LOD 250 //after me
Pass {
  Tags { "RenderType"="Opaque" }
  Cull Front

  CGPROGRAM

  #pragma vertex vert
  #pragma fragment frag
  #include "UnityCG.cginc"

  struct v2f {
    float4 pos : SV_POSITION;
  };
  float _Outline;
  float4 _OutlineColor;

  float4 vert(appdata_base v) : SV_POSITION {
    v2f o;
    o.pos = UnityObjectToClipPos(v.vertex);
    float3 normal = mul((float3x3) UNITY_MATRIX_MV, v.normal);
    normal.x *= UNITY_MATRIX_P[0][0];
    normal.y *= UNITY_MATRIX_P[1][1];
    o.pos.xy += normal.xy * _Outline;
      return o.pos;
    }

    half4 frag(v2f i) : COLOR {
      return _OutlineColor;
    }

    ENDCG
  }
  1. 这段代码块是创建轮廓的部分,它通过第二次渲染来实现这一点。它使用Pass关键字来完成这项工作。在Pass内部,我们可以看到更多标签的定义,以及一个使用CGPROGRAM开始的着色器程序的新开始。第二个块是一个vertex/fragment着色器,如果你查看vert函数,可以看到轮廓是如何计算的。它是通过将模型的顶点normal投影到一个由_Outline确定的距离来做到这一点的。然后,在frag函数中,我们只返回轮廓颜色。再次强调,如果这看起来令人生畏,那么它确实是。

  2. 我们需要做的最后一件事是将新的_Albedo属性添加到我们的表面着色器中,并添加代码来使用它。向下滚动,并在标识的行之后添加以下行:

fixed _GlobalLightEstimation;  //after me
float4 _Albedo;
  1. 向下滚动到surf函数并修改以下行:
from o.Albedo = tex.rgb;

to o.Albedo = tex.rgb * _Albedo;
  1. 所有这些操作都是为了将Albedo颜色应用到纹理上。如果没有纹理,则使用1.0的值,这意味着只显示Albedo颜色。我们需要添加这部分代码,因为我们的导入模型没有纹理,我们不想不得不使用纹理。

  2. 保存文件并返回 Unity。确保您没有看到任何编译错误。

这样就完成了轮廓着色器的构建,但当然,我们还想测试它的工作效果。让我们创建一个新的材质并将其设置在我们的模型上,看看效果如何:

  1. Assets/ARCoreDesign/Materials文件夹中创建一个名为ARMobileSpecularOutline_Green的新材质。

  2. 将新材质的着色器更改为使用新创建的着色器ARCoreDesign | ARMobileSpecularOutline

  3. Albedo颜色设置为一种愉快的绿色,例如#09D488FF。将光泽度设置为大约0.5左右,您自己决定。

实际的布料材质颜色是#8F8E2A;如果您不想有如此明显的差异,请使用该颜色。

  1. _Outline设置为0.02,这仍然相当厚,但很明显。现在使用这个值,以后您可以更改它。

  2. Assets/ARCoreDesign/Prefabs文件夹中选择sofa预制件,并将其fabric材质替换为新的ARMobileSpecularOutline_Green,如图所示:

将沙发预制件更改为使用新材质

  1. 保存您的项目。连接、构建然后运行。放置一个chair并看看效果。

我们已经有了轮廓着色器,但现在我们需要在用户选择对象时以编程方式打开轮廓。

构建对象轮廓

我们将构建一个ObjectOutliner类来为我们处理轮廓。跟随我们的步骤,构建以下部分以在用户选择对象时打开和关闭轮廓:

  1. Assets/ARCoreDesign/Scripts文件夹中创建一个名为ObjectOutliner的新 C#脚本。

  2. 用以下内容替换所有预生成的脚本:

namespace Packt.ARCoreDesign
{
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    public class ObjectOutliner : MonoBehaviour
    {
        public int MaterialSlot;
        public Material DefaultMaterial;
        public Material OutlineMaterial;
        public bool outlineOn; 
        public void Outline()
        {
            outlineOn = !outlineOn;
            var renderer = GetComponent<MeshRenderer>();
            Material[] mats = renderer.materials;
            if (outlineOn)
            {
                mats[MaterialSlot] = OutlineMaterial;
            }
            else
            {
                mats[MaterialSlot] = DefaultMaterial;
            }
            renderer.materials = mats;
        }
    }
}
  1. 这个类基本上每次调用Outline时都会交换对象的材质与其轮廓或默认材质。

  2. 接下来,在您的代码编辑器中打开SceneController.cs脚本。我们必须在Update方法中用我们自己的Physics Raycast包装Session Raycast调用。按照以下方式在突出显示的代码部分周围添加以下代码:

RaycastHit rayHit;
if (Physics.Raycast(FirstPersonCamera.ScreenPointToRay(touch.position), out rayHit, 2))
  {
    var outliner = rayHit.collider.gameObject.GetComponent<ObjectOutliner>();
    if (outliner != null)
    {                    
      outliner.Outline();
    }
  }
  else
  {
    // Raycast against the location the player touched to search for planes. 
 TrackableHit hit;
 TrackableHitFlags raycastFilter = TrackableHitFlags.PlaneWithinPolygon |
 TrackableHitFlags.FeaturePointWithSurfaceNormal;

 if (Frame.Raycast(touch.position.x, touch.position.y, raycastFilter, out hit))
 {
 var andyObject = Instantiate(AndyAndroidPrefab, hit.Pose.position, hit.Pose.rotation);
 m_sceneObjects.Add(andyObject);
 // Create an anchor to allow ARCore to track the hitpoint as understanding of the physical
 // world evolves.
 var anchor = hit.Trackable.CreateAnchor(hit.Pose);

 // Andy should look at the camera but still be flush with the plane.
 if ((hit.Flags & TrackableHitFlags.PlaneWithinPolygon) != TrackableHitFlags.None)
 {
 // Get the camera position and match the y-component with the hit position.
 Vector3 cameraPositionSameY = FirstPersonCamera.transform.position;
 cameraPositionSameY.y = hit.Pose.position.y;

 // Have Andy look toward the camera respecting his "up" perspective, which may be from ceiling.
 andyObject.transform.LookAt(cameraPositionSameY, andyObject.transform.up);
 }

 // Make Andy model a child of the anchor.
 andyObject.transform.parent = anchor.transform;
 }/end of Frame.Raycast
  }
  1. 这段代码使用了Physics对象的Raycast方法。Physics是封装 Unity 物理引擎的对象。Raycast是我们使用的方法,就像我们之前看到的Frame.Raycast一样,用来发射射线并检查是否有碰撞。通常,在运行射线投射操作之前,您会过滤出要测试的对象,因为这非常昂贵。您可以通过SessionraycastFilter的设置中看到这是如何完成的,其中过滤器设置为测试平面,但您也可以设置这个点。这将允许您轻松地应用墙纸等。在我们的案例中,因为我们使用Physics进行Raycast,我们可以确保您只能得到物理对象。ARCore 平面没有附加物理对象。

  2. 保存文件并返回 Unity。

  3. Assets/ARCoreDesign/Prefabs文件夹中找到armchair预制件并将其展开以查看内部模型。

  4. 选择扶手椅模型,然后在检查器窗口中单击添加组件。将盒式碰撞器添加到对象中;盒式碰撞器将自动调整其大小以包围模型。Physics引擎仅测试碰撞器与对象的碰撞,而不是对象本身。这就是为什么我们不必担心我们的 ARCore 平面和点。如果您添加其他模型并希望它们可选中,那么始终使用最适合您形状的最简单碰撞器。简单意味着多边形更少。例如,当盒式碰撞器可以做到时,不要使用球体碰撞器。

  5. 再次单击添加组件按钮,这次将我们的新对象大纲脚本添加到对象中,并将其属性设置为以下摘录中所示:

图片

设置对象大纲属性

  1. 默认材质代表模型的基色。然后,我们将轮廓材质设置为之前创建的轮廓材质。最后,我们设置要替换的槽位。我们想要替换的元素是元素 1,所以我们在材质槽位属性中输入1

  2. 保存项目,构建并运行。放置一把椅子,然后选择它。

现在,您可以放置一把椅子,选择它,然后取消选择。如果您发现选择对象很困难,请确保检查碰撞器是否足够大以包围该对象。在我们的例子中,自动创建的扶手椅碰撞器略有偏差;也许我们可以通过其中一个练习问题来解决这个问题。

放置椅子

最后一步是允许用户在选择椅子后移动它。幸运的是,我们可以在代码中完成所有这些操作。打开您的代码编辑器到SceneController.cs文件,并按照以下步骤进行:

  1. 在指定的行之后,在类顶部添加一个新的public变量:
public GameObject m_andyAndroidPrefab; //after me
public float MoveSpeed = .1f;
  1. 这个新的float MoveSpeed设置了用户移动对象的速度。您也可以将其视为移动灵敏度。我们在这里将其设置为默认值.1f,但您可以在测试时在检查器中自由更改它。

  2. 定位以下突出显示的代码部分,并将其替换为以下内容:

if (Input.touchCount < 1 || (touch = Input.GetTouch(0)).phase != TouchPhase.Began)
{
 return;
} //replace me with

if (Input.touchCount < 1) return;
touch = Input.GetTouch(0);
if (touch.phase == TouchPhase.Began) //handle a single touch
{   //starting single touch
  1. 之前的代码确保只测试起始触摸。现在,我们想要检查触摸开始时以及用户移动手指时。由于我们之前的Physics射线投射代码包装了Session射线投射代码,我们现在需要再次用测试第一次触摸和移动事件的代码包装它,这正是我们的第二个if语句所做的事情。

  2. 滚动到指定的行,并在Update方法末尾之前添加以下代码:

    // Make Andy model a child of the anchor.
    andyObject.transform.parent = anchor.transform;
 }
} //after me
}  //be sure to add the brace
else if (touch.phase == TouchPhase.Moved)
{
  var change = FirstPersonCamera.transform.forward * touch.deltaPosition.y;
  change += FirstPersonCamera.transform.right * touch.deltaPosition.x;
  change *= Time.deltaTime * MoveSpeed;

  foreach (var obj in m_sceneObjects)
  {
    var outliner = obj.GetComponentInChildren<ObjectOutliner>();
    if (outliner != null && outliner.outlineOn)
    {
      obj.transform.position += change;
    }
  }
}
  1. 我们在这里添加的代码处理用户移动手指的情况。然后我们计算相对于相机位置的 change 向量。通过在 2D 中沿 y 轴的增量位置变换相机相对于的 forwardz 轴,这大致意味着当用户在屏幕上上下移动手指时,对象将在相对于相机的 forward 轴上移动进退。然后,我们将相对于相机的 rightx 轴向量添加到 change 向量中,并修改用户在 2D 中沿 x 轴的增量。因此,当用户在屏幕上左右移动手指时,模型将相对于相机沿 right 轴左右移动。

  2. 向上滚动并更改 **if** 语句,添加以 **&&** 开头的新突出显示代码:

if (outliner != null && outliner.outlineOn == false)
{
  outliner.Outline();
}
  1. 这个更改只是确保如果对象被突出显示并再次选中,不会调用 Outline 方法。我们不再想要切换选择,但为了方便使用,我们将保持 Outline 方法不变。接下来,我们想要处理用户触摸到对象之外的情况。在这种情况下,我们想要禁用所有突出显示的对象。

    如果在任何时候你失去了方向或感到沮丧,请查看作为代码一部分提供的完成项目。

  2. 向下滚动到指定的代码,并插入以下新代码以清除选中对象上的轮廓:

else
{  //after me
  //touched outside, reset all outlined objects
  foreach (var obj in m_sceneObjects)
  {
    var outliner = obj.GetComponentInChildren<ObjectOutliner>();
    if (outliner != null && outliner.outlineOn)
    {
      outliner.Outline();
    }
  }

TrackableHit hit;  //before me
  1. 这段代码遍历游戏 m_scene_Objects,找到 ObjectOutliner 组件,然后使用它来测试轮廓是否开启。如果轮廓开启,它将通过调用 Outline 来将其关闭,现在可能这个名字起得不太合适。

  2. 连接、构建并运行。等待表面跟踪完成,然后放置一个 chair。触摸以选择,然后使用手指移动 chair。你还可以调整相对于 chair 的位置,并观察对象如何实时响应。

  3. 同时按下音量下键和电源按钮来截取屏幕截图。将你的图片与以下图片进行比较:

图片

放置并移动的虚拟扶手椅

还不错,但我们可能做得更好。在下一节中,我们将回到光照,并处理对象的光照和阴影。

光照和阴影

光照是我们场景中的基本元素,但正如我们已经看到的,要正确设置它需要一些工作。在本节中,我们将重新审视光照,并解决添加阴影的问题。给我们的对象添加阴影将使它们看起来真的在那里。我们将从添加阴影开始,所以打开 Unity 并跟随操作:

  1. Assets/ARCoreDesign/Materials/Shaders 文件夹中创建一个新的着色器,名为 UnlitShadowReceiver

  2. 双击新的着色器,在代码编辑器中打开它。

  3. 选择所有自动生成的代码并将其删除。然后,添加以下代码:

Shader "ARCoreDesign/UnlitShadowReceiver" 
{
 Properties
 { 
  _Color("Main Color", Color) = (1,1,1,1) 
  _MainTex("Base (RGB)", 2D) = "white" {} 
  _Cutoff("Cutout", Range(0,1)) = 0.5 
 }
 SubShader
 { 
  Pass
  { 
   Alphatest Greater[_Cutoff] SetTexture[_MainTex] 
  } 

  Pass
  { 
   Blend DstColor Zero Tags{ "LightMode" = "ForwardBase" }

   CGPROGRAM
   #pragma vertex vert
   #pragma fragment frag
   #include "UnityCG.cginc"
   #pragma multi_compile_fwdbase
   #include "AutoLight.cginc"

   struct v2f 
   {
    float4 pos : SV_POSITION; LIGHTING_COORDS(0,1)
   };
   v2f vert(appdata_base v) 
   {
    v2f o; 
    o.pos = UnityObjectToClipPos(v.vertex);
    TRANSFER_VERTEX_TO_FRAGMENT(o);
    return o; 
   }
   fixed4 frag(v2f i) : COLOR 
   {
    float attenuation = LIGHT_ATTENUATION(i);
    return attenuation;
   } 
  ENDCG
  }
 }
 Fallback "Transparent/Cutout/VertexLit" 
}
  1. 这个着色器是一个透明阴影接收器的例子。着色器分为两个阶段。在第一阶段,我们基于截止 alpha 值清除纹理。这允许我们将对象变为透明,同时仍然接收阴影。在第二阶段,使用顶点和片段着色器绘制阴影。请随意花时间进一步研究这个着色器。

随着 ARCore 的成熟,可能会提供更多版本的透明阴影接收器。计划在将来搜索其他选项或改进这种着色器形式的其他方法。

  1. 保存文件并返回 Unity。

  2. Assets/ARCoreDesign/Materials文件夹中创建一个新的材质,并将其命名为UnlitShadowReceiver。设置材质的属性,如下面的摘录所示:

图片

在 UnlitShadowReceiver 材质上设置属性

  1. 项目窗口中,从Assets/ARCoreDesign/Materials文件夹中选择并拖动armchair预制件到层次结构窗口的空白区域。我们想要稍微调整预制件,这是最简单的方法。

  2. 从菜单中选择GameObject | 3D | Plane。展开armchair对象,并将平面拖动到 24 Ligne Roset Citta 扶手椅子对象上。

  3. 选择平面,并将位置重置为(0, 0, 0),比例设置为(0.1, 1, 0.1)在 Transform.Set。将材质设置为新的 UnlitShadowReceiver,如图所示:

图片

将平面材质设置为 UnlitShadowReceiver

  1. 层次结构窗口中选择扶手椅对象,然后在检查器窗口中,点击 Prefab 属性旁边的应用按钮以保存预制件。现在将预制件留在场景中,但稍后我们将想要删除它。

我们刚刚创建了一个透明的阴影接收器着色器,并将其设置在我们添加到预制件中的平面上。我们需要这样做,以便我们的对象armchair能够正确地在我们新的透明接收器上投射阴影。接下来,我们需要打开阴影,因为 ARCore 示例默认禁用了阴影。

打开阴影

按照以下步骤重新打开阴影:

  1. 在层次结构窗口中选择方向光,并设置灯光属性,如图所示:

图片

打开方向光的阴影

  1. 一旦你更改了阴影类型,你应该立即在场景窗口中看到变化,扶手椅现在在下面显示阴影。如果你还没有看到阴影,不要慌张,我们可能只需要调整质量设置。

ARCore 示例使用 Blob 纹理为 Andy 模型添加阴影。通过更新以使用着色器,我们现在可以自动支持任何你想要添加的对象。只需记住调整平面以适应对象。如果你想添加一幅画或其他挂在墙上的物品,你需要将平面设置为垂直,并与对象对齐。

  1. 从菜单中选择编辑 | 项目设置 | 质量。通过点击 Android 级别下的箭头图标,将 Android 构建设置为默认使用最高质量设置。这在上面的摘录中显示:

设置构建的质量设置

  1. 确保再次将更改应用到预制件上。这只是为了确保在我们从场景中删除预制件之前,我们的更改已经保存。

  2. 从场景中选取并删除扶手椅对象。

在这个示例中,我们使用了最高的质量设置。对于大多数情况,ARCore 应用将在相对较新的设备上运行,这意味着我们可以尝试推动极限。如果你发现质量设置会导致设备崩溃或无法正确渲染,那么尝试在构建中降低一个级别的质量。你可能无论如何都想要这样做,以改善你的应用性能。

  1. 连接、构建和运行。放置一把椅子,看看差异,如图所示:

带有光照和阴影的完整应用的示例

在本章中,我们将到此为止。请随意增强应用,并花时间独立完成一些可选练习。如果阴影没有正确显示,请返回并编辑灯光和质量的阴影设置。

练习

独立回答以下问题

  1. 将应用中的模型更改为沙发或另一个对象。

  2. 将我们对环境光照脚本所做的更改添加到应用中,以便跟踪光照方向。

  3. 将其他对象添加到应用中,并允许用户选择放置哪个对象。

  4. 允许用户放置垂直对象。提示——你现在需要渲染垂直平面,是的,ARCore 确实可以识别垂直平面。

  5. 允许用户旋转模型。提示——你可能需要添加一些控制手柄。

摘要

这样,我们就完成了我们简单的设计应用示例。我们能够完成我们想要完成的所有主要技术项目。我们从使用 ARCore 示例作为模板设置一个新的 Unity 项目开始。这为我们节省了一些时间,否则这将变成一个非常长的章节。接下来,我们学习了如何从像TurboSquid这样的网站导入新模型,以及如何将它们设置为预制件以供以后使用。然后,我们构建了一个简单的用户界面,使我们能够从视图中清除跟踪平面和清除任何模型。之后,我们添加了用户在 AR 场景中选择和移动对象的能力。这要求我们增强 ARCore 示例中的着色器,并对SceneController脚本进行了大量修改。最后,我们通过打开灯光并添加透明阴影接收器到我们的对象预制件来解决阴影问题。

ARCore 非常适合下一波 HoloLens 或低成本混合现实头戴式设备。在下一章中,我们将从 AR 中稍作休息,深入混合现实,我们将构建一个名为HoloCore的多玩家应用。

第十章:在混合现实中混合

混合现实MR)是将增强现实虚拟现实结合到同一体验或应用中的演变。MR 通常使用可穿戴设备在用户的现实世界之上叠加虚拟世界。这个概念最初是在微软推出 HoloLens 时开始受到关注的。HoloLens 是一种可穿戴眼镜设备,允许你通过手势将你的现实世界与虚拟内容叠加,这与我们在整本书中用 ARCore 所做的不太一样,只是可穿戴部分和,当然,价格标签不同。

微软目前正领导混合现实开发,他们的同名平台为整个 AR/VR 和现在的 MR 空间提供了很好的曝光。微软是一家大型的科技公司,就像许多大型科技巨头一样,已经决定重新定义混合现实的概念,使其也包括虚拟现实。

允许用户体验混合现实的可穿戴设备传统上相当昂贵,直到最近。通过众筹和其他举措,现在有很多价格低廉、不到 30 美元的美国可穿戴设备可以让你体验混合现实。这对于任何想要深入了解并学习如何开发混合现实应用的人来说是完美的。当然,并非所有混合现实平台都为移动设备设计,或者与 ARCore 兼容。幸运的是,一个名为HoloKit的开源项目发布了一个纸盒混合现实头戴设备,它设计用于与 ARCore 一起使用。

“我没有困惑,我只是混合得很好。”

  • 罗伯特·弗罗斯特

在本章中,我们将构建一个结合 AR / MR ARCore 应用,它将作为一个技术和学习演示,展示 AR 和 MR 的强大功能。当然,我们还需要在 VR 方面稍微尝试一下,这应该会使事情变得有趣。以下是本章我们将重点关注的主要项目列表:

  • 混合现实和 HoloKit

  • 介绍 WRLD

  • 设置 WRLD 以进行 MR

  • 导航地图

  • 地图绘制、GIS 和 GPS

  • 接下来是什么

这是一个内容非常丰富的章节,有很多内容需要讲解。遗憾的是,由于版权问题,我们无法将其内容作为一个完整的包提供。然而,我们已经尽力将本章的每个部分都写成可以独立使用的样子,几乎就像一本烹饪书。这将允许你挑选和选择你想要和不需要使用的组件。

为了最好地体验本章的练习,建议你获得一个 HoloKit。你应该能够以大约 30 美元的价格获得这个设备。如果你感到好奇,甚至有计划可以自己制作。以下是一个链接,你可以在这里了解更多关于 HoloKit 的信息并订购自己的设备:holokit.io/

混合现实和 HoloKit

HoloKit 是由 Botau Hu 创建的,他是一位才华横溢的新技术创新者,必将在行业中取得巨大成功。这是一个可穿戴设备,可以将你的移动设备屏幕投射成 3D 全息投影。然后,这个全息投影叠加到用户的视野上,从而使用户能够体验一个更加沉浸的环境,这种环境通常处于 VR 的边缘。以下是一张完全组装的 HoloKit 的插图:

图片

完全组装的 HoloKit

如从图中所示,该设备在构造上与谷歌 Cardboard 非常相似。Cardboard 是谷歌使 VR 大众化的方式,并且它成功了。如果你无法快速获得 HoloKit,你也可以使用修改后的谷歌 Cardboard。只需在纸板上为设备的摄像头切一个槽,并确保不要过多地移动。

你会首先注意到大多数混合现实耳机的一个特点就是用户可以看到他们的环境。这使用户仍然能够对周围的空间有意识,同时体验可能是一种几乎完全虚拟的经历。由于用户更加警觉,MR 设备通常被认为更安全,用户很少会经历运动病和/或跌倒。目前,由于这些问题,VR 设备被认为不适合 13 岁以下的人。

VR 运动病通常更多是应用程序性能不佳或分辨率低的结果。实际上,由延迟的应用程序或低分辨率引起的视觉伪影会对用户的头脑造成额外的压力。这种压力会以剧烈头痛或恶心等形式表现出来。在 VR 的早期,这是一个大问题,但现在技术已经足够成熟,大多数用户可以连续使用应用程序数小时。

Mirage Solo耳机是由联想为迪士尼的一款名为绝地挑战的游戏开发的。绝地挑战实际上更多的是一个混合现实概念的证明和展示,以及可能实现的内容。它很可能也会成为收藏品,因为它与新的星球大战系列相关联,并且恰好与即将到来的技术革命相吻合。这个项目唯一真正不幸的事情是联想从未发布开发者套件;希望他们将来能纠正这一点。

以下是一张联想 Mirage Solo 耳机的图片:

图片

绝地挑战混合现实游戏

为了完成本章的练习,你不需要 HoloKit。HoloKit 允许你通过按按钮从 AR 切换到 MR/VR 模式。这意味着你仍然可以完成本章的所有练习。然而,这也意味着你将无法体验到 MR 的神奇体验。在下一节中,我们将设置 HoloKit 与 ARCore 一起工作,并准备好构建我们的技术演示。

设置 HoloKit

HoloKit 的伟大之处在于它自带完整的 Unity 模板项目。这使得我们使用 HoloKit 的过程变得非常轻松。打开命令提示符或 shell 窗口,执行以下操作:

  1. 如果你还没有这样做,从根目录创建一个名为ARCore的新文件夹,并导航到它:
mkdir ARCore
cd ARCore
  1. 将 HoloKit 仓库克隆到其中:
git clone -b android https://github.com/holokit/holokitsdk.git
  1. 该命令克隆了特定的Android分支,我们将使用它。HoloKit 也支持iOS上的ARKit

  2. 打开一个新的 Unity 编辑器实例。在ARCore文件夹中创建并打开一个名为HoloCore的新项目。

  3. 在项目窗口中,在 Assets 下创建一个名为HoloCore的新文件夹。在该新文件夹下,创建我们标准的五个新文件夹(ScriptsScenesMaterialsModelsPrefabs)。

  4. 使用文件资源管理器打开ARCore/holokitsdk/Assets文件夹。将HoloKitSDK文件夹复制并放置在ARCore/HoloCore/Assets文件夹中。完成后,返回到编辑器,你应该会看到资产正在导入和编译。导入完成后,确认你的项目窗口如下所示:

图片

显示 HoloKitSDK 的项目窗口文件夹

  1. 如果你被提示切换到 Android,通过点击 OK 选择这样做。

  2. 从菜单中选择编辑 | 项目设置 | 玩家。这将打开玩家(即应用玩家)设置面板。选择 Android 选项卡,取消选择多线程渲染选项,并设置包名、API 级别和 ARCore 支持,如下所示:

图片

设置 Android 的 Player 设置

  1. Assets/HoloKitSDK/Examples文件夹中打开 HoloKit 示例场景CubeOnTheFloor

  2. 从菜单中选择文件 | 保存场景为,并将场景保存为Assets/HoloCore/Scenes文件夹中的 Main。

  3. 打开构建设置并将当前场景添加到构建中。

  4. 连接、构建和运行。你应该在右上角看到一个带有字母 C 的小按钮。按下该按钮即可从 AR 模式切换到 MR 模式。准备好后,将你的设备放入 HoloKit 头戴式设备中,享受你的第一个 MR 应用。

与 Google Cardboard 不同,HoloKit 需要让相机查看用户的环境以便跟踪。因此,你可能需要通过为设备摄像头切割更大的孔来修改头戴式设备。这是一张需要修改以适应三星 Galaxy S8 的 HoloKit 图片:

图片

修改了 HoloKit 以允许相机可见地跟踪

如果你还有其他你想破解的设备,比如 Cardboard,那么只需确保你切割出一个空间,以便相机不会被遮挡。一些与移动设备兼容的其他混合现实头戴式设备已经有了相机扩展。这些相机扩展可能支持鱼眼镜头,这允许设备看到更宽的区域。这效果相当不错,因为它本质上将相机转换成了一个具有广角镜头的传感器。

它是如何工作的?

在我们走得太远之前,让我们打开 HoloKit 项目,看看它是如何或做什么的。打开 Unity 编辑器并完成以下操作:

  1. 在层次结构窗口中找到 HoloKitCameraRig,然后选择并展开它。展开子对象的子对象,等等,直到你可以看到左眼和右眼对象,如下面的截图所示:

图片

在层次结构窗口中查看场景的 3 个相机视图

  1. 当应用处于 AR 模式时,VideoSeeThroughCamera 是主要使用的相机。当应用处于 MR 模式时,左右眼相机被用来创建立体 3D 视觉。仔细观察眼相机,你会注意到它们在x轴上的位置略有调整。对于右相机,调整量为 0.032,对于左相机则是-0.032。这就是我们如何通过为每只眼睛使用偏移相机来生成 3D 立体投影。

  2. 其他组件如下:

    • HoloKitAmbientLight:它只是一个带有ARCore Environmental Light脚本的普通方向性光源。

    • HoloKitPlaneGenerator:它是HelloARController脚本的基础对象,我们之前已经见过很多次了。

    • HoloKitPlacementRoot:它是场景中虚拟对象的主要锚点。

    • HoloKitCameraRig:它是控制应用视图的东西。

    • HoloKitGazeManager:它是新的,允许用户通过定位他们的注视点或视图在目标上选择对象。你现在可以用当前场景和球来尝试这个功能。将你的注视点固定在球上,看看会发生什么。

    • HoloKitPointCloud:它与其在 ARCore 中的对应物具有相同的功能。

  3. 继续浏览并检查场景中其余对象的其余部分。

  4. 连接、构建并再次运行场景。这次,注意细节,看看你是否能让注视点工作。

希望这相对容易。现在,有了 HoloKit 的设置,我们为我们的 AR 和 MR 应用组合建立了框架。我们应该扩展我们的技术演示将做什么。我们的技术演示的前提将是一个允许用户在传统地图界面和 AR 或 MR 界面之间移动的应用。HoloCore 这个名字是对用户能够钻入地图并在 AR 或 MR 中渲染 3D 视图的能力的一种戏谑。这也很好地与 ARCore 的名字相呼应。在下一节中,我们将看看如何将世界 3D 地图添加到我们的应用中。

介绍 WRLD

混合现实应用程序,因为它们为用户提供空间感知,非常适合查看大型物体或区域,如地图。与虚拟现实不同,混合现实提供了更直观和自然的界面进行移动,因为用户也可以物理移动他们的位置。所以,还有什么比使用它来查看世界的 3D 地图来完全探索混合现实更好的方法呢。幸运的是,有一个相对较新的名为 WRLD 的平台,它已经开始在 AR / VR 和 MR 领域产生重大影响,因为它提供了一个优秀且简单的解决方案来渲染相当好的 3D 地图。

WRLD 是一个优秀的通用 3D 映射和可视化平台。它目前不支持更强大的后端 GIS 服务,但肯定可以。对于那些可以访问 Esri CityEngine 的专业 GIS 开发者,也有一些将 CE 模型引入 Unity 的优秀工作流程。这意味着你还可以在混合现实中实验 CE 模型。

WRLD 作为 Unity 资产直接在资产商店中提供,因此安装非常简单。然而,在我们安装之前,我们需要访问 WRLD 网站,并获取一个开发者账户。WRLD 是一项按使用量收费的商业服务。幸运的是,他们为有限的时间提供免费的开发者访问权限,这对于我们的技术演示来说非常完美。打开浏览器并完成以下操作:

  1. 浏览到 wrld3d.com 并注册账户。确保通过电子邮件验证账户。

  2. 返回网站并登录。

  3. 在页面顶部找到并点击“开发者”链接。这将带你去到开发者页面。

  4. 点击页面顶部的“大访问 API 密钥”按钮。

  5. 为你的密钥输入名称 HoloCore,然后点击“创建 API 密钥”以创建密钥,如图所示:

创建 WRLD API 密钥

  1. 点击“复制 API 密钥”以将密钥复制到剪贴板。我们很快就会用到它。

  2. 返回 Unity 编辑器,从菜单中选择 Window | Asset Store。这将打开编辑器内的浏览器页面。

  3. 在搜索框中输入 WRLD 并点击搜索按钮。这将打开 WRLD 的资产页面,提供下载资产。点击下载按钮,如图所示:

从资产商店下载 WRLD 资产

  1. 这将下载包。下载完成后,你将看到一个资产导入对话框。只需点击“导入”即可导入所有内容。这可能需要一些时间,所以请伸展一下腿脚,拿些点心。

在某些情况下,你可能想要更小心地选择将哪些内容带入你的项目中。例如,如果你正在构建一个非技术演示或概念验证,你可能会从项目中移除任何示例场景或其他多余内容。我们将在第十一章性能提示和故障排除中更多地讨论保持项目精简。

  1. 你可能会收到一个警告,提示你的版本与 Unity 的版本不匹配。接受警告并继续。

  2. 在你导入WRLD后,被提示获取密钥时,只需点击“稍后”。毕竟,我们已经有了一个密钥。

  3. 接下来,你可能会被提示通过以下对话框增加阴影距离:

图片

跳过阴影设置对话框

  1. 点击跳过按钮。我们稍后需要手动调整光照、材质和阴影。

这将WRLD资产导入到我们的项目中。在下一节中,我们将介绍如何设置并运行WRLD以用于我们的 MR 应用。

为 WRLD 设置 MR 环境

导入资产后,我们现在可以开始设置WRLD以在 MR 中工作。设置需要一些定制,所以回到 Unity 并完成以下步骤:

  1. 从菜单中选择Assets | Setup WRLD Resources For | Android。这将确保资产针对 Android 进行了优化。我们还会在稍后的部分讨论如何通过更新或创建自己的着色器来手动优化材质。

  2. 确保主场景已加载,然后选择并展开HoloKitPlacementRoot。禁用 DebugCube 和 GazeTargetExample 子对象。如果你忘记了如何操作,请检查检查器窗口。

  3. 创建一个名为WRLD的新子GameObject,属于 HoloKitPlacementRoot。转到检查器窗口,并使用添加组件来添加Wrld Map组件到对象。

  4. 设置Wrld Map组件的属性,如图所示:

图片

设置 Wrld Map 组件的属性

  1. 选择并拖动新的WRLD对象到你的Assets/HoloCore/Prefabs文件夹中,以创建一个我们可以稍后使用的预制件。

  2. 从层次结构窗口中选择 HoloKitCameraRig,并将变换的 Y 位置设置为300。由于我们的地图在000,我们希望观察者从大约 300 米或约 1000 英尺的高度向下看。然后展开对象,直到你看到所有的子对象。

  3. 选择每个相机,VideoSeeThroughCamera,左眼和右眼,然后在检查器窗口中,将远裁剪平面设置为5000,如图所示:

图片

设置远平面裁剪距离

  1. 调整远裁剪平面实际上将我们的视图扩展到包括距离5000的所有对象。之前,这个值设置为1000。你可能还想将近裁剪平面增加到更大的值;110效果很好。如果你注意到地图上略有闪烁,这很可能是由于裁剪平面设置得太近造成的。

  2. 连接、构建和运行。通过点击 C 按钮并将设备插入 HoloKit 来设置应用在 MR 中运行。享受在混合现实中查看地图的体验。

WRLD 在 Unity 和其他平台上使用其 API 有几个优秀的示例。我们构建这个示例是为了展示现实混合,而不是重新创建他们的示例。因此,我们省略了将地图放置在表面上的操作,但这是因为 WRLD 已经有一个很好的 ARKit 示例,并且很可能会在未来提供。

你刚才所体验的相当有趣,尤其是考虑到这个示例在设置上所花费的精力如此之小,除了有一些东西缺失。当然,我们希望能够移动并放大缩小我们的地图,因此我们将在下一节中介绍移动和导航。

导航地图

在传统的 AR 应用中,你很少移动用户或玩家。用户或玩家自己移动,AR 应用则围绕这一点工作。我们在本书中花了很多时间来理解 ARCore 如何跟踪用户并理解他们的环境,这在处理像 Andy 这样的小物体时效果相当不错。但是,如果我们想要渲染大量的虚拟物体或者嵌入新的环境,那么我们需要一种让用户也能导航这些物体的方式。因此,在本节中,我们将探讨实现从标准触摸界面到 AR 和 MR 版本的混合导航方法。如果你没有 HoloKit 或者对尝试 MR 不感兴趣,那么你可以只专注于使用 AR。

在向我们的应用添加导航之前,我们可能应该看看 WRLD 默认是如何处理导航的。打开 Unity 编辑器并跟随操作:

  1. 保存你的当前场景。

  2. 创建一个新的场景。将其命名为Navigation并在Assets/HoloCore/Scenes文件夹中保存场景。

  3. Assets/HoloCore/Prefabs文件夹中,拖动我们之前创建的WRLD预制体并将其放入场景中。设置Wrld Map上的属性,如图所示:

图片

设置 WRLD 预制体的属性

  1. 这基本上是你将用于在非 AR 界面中将地图渲染到设备上的默认设置。

  2. 打开构建设置对话框并将你的新场景添加到构建中。取消选中主场景,但不要删除它;我们稍后会再次启用它。

  3. 连接、构建和运行。你现在会看到地图成为你视图中的主要元素。你可以使用触摸手势来移动、平移和缩放地图。

如您通过操作应用所见,使用触摸界面进行地图导航非常流畅。我们将使用这个场景作为我们的起始场景,并使用我们的主场景让用户切换到 AR 或 MR 模式以查看更详细的项目。为此,我们将使用我们刚刚创建的场景作为起始场景,并使用我们的主场景让用户切换到 AR 或 MR。

能够在常规触摸驱动的 UI、AR 或 MR 等界面类型之间切换始终是可行的。当然,一个很好的例子是来自 Niantic Labs 的流行游戏 Pokémon Go。这也恰好使用地图,并允许用户切换到 AR 来捕捉宝可梦。如果你对 Pokémon Go 是如何构建的感到好奇,可以查看由 Micheal Lanham 编写的书籍 Augmented Reality Game Development,该书也来自 Packt

从 AR 切换到 MR

能够切换场景并保持状态是一个常见的任务,但在 Unity 中似乎需要做一点工作。打开 Unity 编辑器到 Navigation 场景并完成以下操作:

  1. 打开 Assets/HoloCore/Scripts 文件夹,创建一个新的脚本,命名为 Singleton。转到书籍下载的源代码文件夹 Code/Chapter_10,复制 Singleton.cs 文件的全部内容,并将其粘贴到你的新脚本中。Singleton 是 Unity 中创建只希望有一个对象且永远不希望该对象被销毁的常见模式。如果你对 Singleton 比较陌生,最好花些时间复习这个类。

  2. 在同一文件夹中创建一个新的脚本,命名为 SceneController 并替换生成的代码为以下内容:

using System;
using UnityEngine;
using UnityEngine.SceneManagement;
using Wrld;
using Wrld.Space;
namespace Packt.HoloCore
{
  public class SceneController : Singleton<SceneController>
  { 
    protected SceneController() { }
  }
}
  1. SceneController 是一个具有 SceneControllerSingleton。这种循环引用可能有点令人困惑,所以最好将其视为一个 SceneController,它是一个 Singleton,持有 SceneController 类型。在类内部,我们需要定义一个 protected 默认构造函数,以便通过 Instance 强制访问。我们很快就会看看如何使用 Instance

  2. 在构造函数之后输入以下内容:

public LatLongAltitude position;
  1. 接下来,我们将添加一个单独的属性来保存相机最后固定的位置。这样,当我们切换场景时,我们只需将 position 属性传递回场景,以便它可以确定设置的位置。LatLongAltitude 是一种空间数据类型,它保存了相机的纬度、经度和高度。

  2. 添加以下新的方法,LoadScene,并使用以下代码:

public void LoadScene(string scene, Camera mapCamera)
{
   if (Api.Instance.CameraApi.HasControlledCamera)
   {
     mapCamera = Api.Instance.CameraApi.GetControlledCamera(); 
   }
   if(mapCamera == null) throw new ArgumentNullException("Camera", "Camera must be set, if map is not controlled.");

  position = Api.Instance.CameraApi.ScreenToGeographicPoint(new Vector3(mapCamera.pixelHeight/2, mapCamera.pixelWidth/2, mapCamera.nearClipPlane), mapCamera);

  SceneManager.LoadScene(scene, LoadSceneMode.Single);
}
  1. LoadScene,这是所有工作发生的地方。我们将在SceneController上调用LoadScene,传入我们想要在当前map或 WRLD 相机中加载的scene名称。在方法内部,我们首先测试当前map是否正在被控制;如果是,我们只需忽略相机并使用被控制的相机。接下来,我们测试mapCamera是否为 null;如果是,我们希望带错误退出。否则,我们使用ScreenToGeographicPoint提取当前位置。此方法提取相机的主屏幕焦点,我们假设它位于屏幕宽度和高度的一半像素处;mapCamera.nearClipPlane设置视图锥体或相机的正面,如果您还记得我们之前的讨论,这等于相机相对于地面高度,或者在这个例子中是地图。在方法结束时,我们使用SceneManager,这是 Unity 加载场景的辅助类。我们使用LoadSceneMode.Single选项调用LoadScene

这就完成了我们的SceneController。现在,作为Singleton的好处是,我们永远不需要物理添加组件,因为它现在始终被认为是可用的。WRLD 的大部分 Unity API 也是基于这个模式。我们仍然需要添加一些可以从我们的场景激活的额外代码。

构建场景切换器

让我们添加另一个脚本/组件,它将仅激活我们的SceneController。打开编辑器并完成以下操作:

  1. 创建一个新的 C#脚本,命名为SceneSwitcher,并用以下代码替换所有预生成的代码:
using UnityEngine;
namespace Packt.HoloCore
{
  public class SceneSwitcher : MonoBehaviour {

  }
}
  1. 在类内部创建以下属性:
public Camera mapCamera;
  1. 这是mapCamera的占位符,即用于渲染Wrld 地图的相机。当地图不是由相机控制时,我们需要这个,这种情况发生在用户处于 AR / MR 时。

  2. 然后,创建以下方法:

public void SwitchScenes(string sceneName)
{
   SceneController.Instance.LoadScene(sceneName, mapCamera);
}
  1. 此方法将负责在SceneController上使用LoadScene。注意类和方法调用之间使用Instance。请记住,我们的SceneController是一个Singleton,它是一个对象而不是静态类。因此,我们需要一个实例,这由Singleton中的辅助属性Instance提供,因此当在SceneController上调用方法时,我们总是通过Instance调用它。

  2. 如果您还没有这样做,请保存所有文件,并返回 Unity。确保您没有编译错误。

创建场景切换器预制件

代码完成后,现在是我们构建SceneSwitcher预制件的时候了。打开编辑器到Navigation场景并完成以下操作:

  1. 从菜单中选择 GameObject | UI | Canvas。将SceneSwitcher组件(脚本)添加到画布中,并将其重命名为SceneSwitcher。将Scene Switcher上的地图相机属性设置为使用主相机。

  2. 在“Hierarchy”窗口中选择SceneSwitcher对象,然后从菜单中选择 GameObject | UI | Panel。设置面板的属性,如下面的摘录所示:

图片

在面板上设置属性

  1. 通过点击按钮设置锚点,然后当“Anchor Presets”菜单打开时,同时按下旋转和位置键(Windows 上的Shift + Alt)并点击左上角。这将使面板锚定到左上角。您还需要添加一个网格布局组组件并设置指定的属性。

  2. 选择面板,从菜单中选择 GameObject | UI | Button。将按钮重命名为 Switch 并设置按钮文本为 Switch。

  3. 为 Switch 按钮设置 OnClick 处理程序,如下所示:

图片

添加按钮的 OnClick 处理程序

  1. 我们将参数设置为字符串类型的“Main”。Main 是我们想要切换到的场景名称,当用户点击按钮时,将切换到该场景。

  2. 从“Hierarchy”窗口拖动SceneSwitcher对象到“Project”窗口的Assets/HoloCore/Prefabs文件夹中。这将为我们创建一个新的预制件,以便在主场景中使用。

  3. 双击Assets/HoloCore/Scenes文件夹中的“Main”场景。当提示保存Navigation场景更改时,当然要保存。

  4. Assets/HoloCore/Prefabs文件夹拖动SceneSwitcher预制件并将其拖放到“Hierarchy”窗口的空白区域。

  5. SceneSwitcher组件(在SceneSwitcher对象上)上设置地图相机属性为 VideoSeeThroughCamera。

  6. 展开 SceneSwitcher 对象并找到 Switch 按钮。将 OnClick 事件处理程序更改为传递Navigation,这是我们想要从主场景加载的场景。请记住,场景名称必须完全匹配,所以要注意大小写。

  7. 保存场景。

修改 Wrld 地图脚本

我们几乎完成了;我们最后需要做的是让Wrld Map脚本从我们的单例SceneController中获取最后一个摄像机的位置。这意味着我们不幸地必须修改Wrld Map脚本的源代码。通常,我们想要避免修改第三方 API,除非我们有源代码,并且它真的是我们唯一的选项。打开位于Assets/Wrld/API文件夹中的WrldMap脚本,并按照以下步骤操作:

  1. 在指定的行之间插入以下内容:
using Wrld.Scripts.Utilities; //after me
using Packt.HoloCore;
#if UNITY_EDITOR //before me
  1. 滚动到SetupApi方法并在指定的行之间插入以下代码:
config.Collisions.BuildingCollision = m_buildingCollisions; //after meconfig.DistanceToInterest = SceneController.Instance.position.GetAltitude();
config.LatitudeDegrees = SceneController.Instance.position.GetLatitude();
config.LongitudeDegrees = SceneController.Instance.position.GetLongitude();
Transform rootTransform = null; //before me
  1. 所有这些做的只是将地图设置到相机最后指向的位置。你可以看到我们在这里使用SceneController单例来访问相机最后已知的位置。你可以在SetupApi方法中看到定义和设置配置对象的地方。希望在未来,Wrld允许将此配置传递到脚本中。如果那样可能,我们就可以在传递给WrldMap脚本之前修改该配置,从而消除我们在这个类中添加自己代码的需要。

  2. 保存文件并返回 Unity。检查是否有任何错误。

  3. 打开构建设置对话框,确保两个场景都已添加、激活,并且顺序如以下摘录所示:

在构建设置对话框中设置场景和场景顺序

  1. 连接、构建并运行应用。由于我们从0开始,即纬度和经度空间坐标的0,地图将从非洲海岸开始,这是00。使用捏合触摸手势放大,直到你看到全球视图。使用触摸滑动手势将地图平移到北美洲,目前是查看WRLD数据的最佳位置。选择一个熟悉的地方并放大,直到你开始看到 3D 对象。然后,按一下切换按钮以切换界面到 MR 和 AR。你可以再次按切换按钮返回主视图。以下是一张显示增强现实模式和另一个用户使用 HoloKit 头戴式设备混合现实模式的图片:

应用运行时的增强现实视图

  1. 我们现在有一个应用,允许用户在地图上导航,然后切换到 AR 或 MR 模式查看感兴趣的区域。这工作得很好,但如果用户从当前位置开始会更好。为了做到这一点,我们需要对地图、GIS 和 GPS 有更多的了解,我们将在下一节中介绍。

地图、GIS 和 GPS

如我们已学到的,Unity 使用一个带有笛卡尔坐标参考系统(x、y 和 z)的点在 3D 空间中跟踪其对象。当我们在一个世界地图上绘制一个点时,情况并无不同;我们需要参考这个点,但现在我们需要使用球面或地理参考系统来表示地球上的一个位置,因为我们都知道地球是球形的。然而,在地理系统和笛卡尔系统之间转换是昂贵的。因此,许多地图应用使用一个称为地球中心地球固定ECEF)的中间参考系统,它在一个地球固定的笛卡尔坐标参考系统上表示地图数据。以下是一个显示笛卡尔、地理和 ECEF 坐标参考系统之间差异的图表:

坐标参考系统比较

现在,你可能已经注意到 WRLD 默认支持 ECEF。正如我们提到的,由于 ECEF 已经在笛卡尔坐标系中,转换要容易得多,也快得多。然而,对我们来说,我们只想将相机定位在用户的地理坐标参考上,这可以通过使用 GPS 从用户的设备中轻松获得。

访问用户设备上的 GPS 需要一些工作,但幸运的是,我们可以在一个地方完成所有这些操作。让我们打开SceneController脚本并做出以下修改:

  1. 在类顶部添加两个新属性:
public bool isLoaded;
public string status;
  1. 在构造函数下方创建一个新方法:
void Awake()
{
  StartCoroutine(GetLocationPoint());
}
  1. Awake方法是一个特殊的 Unity 方法,当GameObject首次初始化时运行。在方法内部,我们调用StartCoroutineStartCoroutine是 Unity 中的另一个特殊方法,允许你创建coroutineCoroutines是一种中断或打断你的代码流,做其他事情,然后返回以完成原始任务的方式。在调用中,我们传递一个方法调用GetLocationPoint(),将此方法设置为coroutine

  2. 添加以下方法来创建coroutine

IEnumerator GetLocationPoint()
{
}
  1. 协程必须返回IEnumerator。通过添加返回类型,该方法现在可以使用返回YieldInstructionyield或中断其执行的yield return语句。我们很快就会看到如何做到这一点。

  2. GetLocationPoint内部添加以下行:

AndroidPermissionsManager.RequestPermission(new string[] { "android.permission.ACCESS_FINE_LOCATION" });
  1. 这行代码提示用户获取对location服务的访问权限,也称为 GPS。我们这样做是为了明确识别用户的location,前提是他们的设备 GPS 没有被阻止或者用户已经禁用了location服务。

Google 通过将无线端点 MAC 地址映射到地理坐标的方式本质上开发了他们自己的location服务。Google 通过本质上使用其自动驾驶街景汽车进行 war driving 来实现这一点。当这些汽车自动驾驶时,它们也在抓取在映射到 GPSlocation时可以检测到的每个无线设备的 MAC 地址。结果证明,这个服务实际上在提供location方面可以更准确,尤其是在 GPS 视线难以到达的更密集的都市地区。

  1. 然后,添加以下内容:
if (Input.location.isEnabledByUser == false)
{
  isLoaded = true;
  yield return SetStatus("Location not authorized, starting at 0,0", 1.0f);
  yield break;
}
  1. 这段代码检查用户是否启用了 GPS;如果没有,我们无法做任何事情。我们将isLoaded设置为true,这将作为一个标志,让外部方法知道我们找到了或没有找到location。然后,我们yield return调用SetStatus的结果。记住,因为我们处于coroutine中,yield return意味着我们想要在此处中断代码执行。

  2. GetLocationPoint方法下方向下滚动并添加以下新方法:

public YieldInstruction SetStatus(string status, float time)
{
  this.status = status;
  return new WaitForSeconds(time);
}
  1. 在方法内部,我们正在设置我们的status文本,这将是我们想要显示给用户的消息。然后,我们返回一个新的WaitForSeconds(time),其中time代表要等待的秒数。有许多不同的YieldInstruction形式可以用来中断你的代码。这里的YieldInstruction只是等待设定数量的秒数,然后返回继续执行代码的当前位置。请注意,在yield过期后,无论出于什么原因,代码将从确切的位置继续执行。

  2. 返回到GetLocationPoint中我们离开的地方。在yield return SetStatus调用之后,我们正在执行yield break。这一行会中断coroutine并退出方法,这在普通方法中相当于返回。

  3. 现在我们已经理解了coroutines,让我们进入下一部分的代码:

yield return SetStatus("-----STARTING LOCATION SERVICE-----", 1);
Input.location.Start();

// Wait until service initializes
int maxWait = 30;
while (Input.location.status == LocationServiceStatus.Initializing && maxWait > 0)
{
  yield return new WaitForSeconds(1);
  maxWait--;
}
  1. 首先,我们设置一个status消息并让用户知道我们正在启动服务,然后我们这样做。之后,我们不断地循环,每秒使用yield return new WaitForSeconds(1)中断一次,每次迭代调整我们的计数器maxWait。我们需要等待location服务初始化;有时这可能需要一段时间。

  2. 输入以下代码以处理当我们的计数器已过期(maxWait<1)的情况:

// Service didn't initialize in 20 seconds
 if (maxWait < 1)
 {
   yield return SetStatus("ERROR - Location service timed out, setting to 0,0,0", 10.0f);
   isLoaded = true;
   yield break;
 }
  1. if块内部,我们设置statusloaded标志。然后,我们使用yield breakcoroutine返回。

  2. 接下来,我们想要处理服务失败或启动的情况,如下所示:

if (Input.location.status == LocationServiceStatus.Failed)
{
  yield return SetStatus("ERROR - Unable to determine device location.", 10.0f);
  isLoaded = true;
  yield break;
}
else
{
  //set the position
  yield return SetStatus("-----SETTING LOCATION----", 10.0f);
  position = new LatLongAltitude(Input.location.lastData.latitude, Input.location.lastData.longitude, Input.location.lastData.altitude);
  isLoaded = true; 
}
  1. 此代码处理服务失败或成功。在失败路径中,我们设置一个错误消息并退出。否则,我们设置一个status并等待10秒。我们这样做是为了让用户能够阅读消息。然后,我们根据设备提供的地理坐标设置位置。

  2. 最后,我们使用以下代码停止服务:

 Input.location.Stop();
  1. 我们停止服务是因为我们不需要不断地获取location更新。

    如果你想要保持服务开启并使用它来跟踪用户的location,例如像《精灵宝可梦 GO》这样的应用,那么只需确保在对象被销毁时停止服务。你可以在名为OnDisable()的方法中这样做,这是另一个特殊的 Unity 方法,用于清理对象。

  2. 在这一点上,我们还想更新并重载LoadScene方法,如下所示:

public void LoadScene(string scene)
{
  SceneManager.LoadScene(scene, LoadSceneMode.Single);
}

public void LoadScene(string scene, Camera mapCamera)
{ 
  if (Api.Instance.CameraApi.HasControlledCamera)
  {
    mapCamera = Api.Instance.CameraApi.GetControlledCamera();
  }
  else if (mapCamera == null) throw new ArgumentNullException("Camera", "Camera must be set, if map is not controlled.");
  position = Api.Instance.CameraApi.ScreenToGeographicPoint(new Vector3(mapCamera.pixelHeight / 2, mapCamera.pixelWidth / 2, mapCamera.nearClipPlane), mapCamera);

  Debug.LogFormat("cam position set {0}:{1}:{2}", position.GetLatitude(), position.GetLongitude(), position.GetAltitude());
  SceneManager.LoadScene(scene, LoadSceneMode.Single);
}
  1. 我们对方法进行了重载,以便在切换场景时允许两种不同的行为。我们添加的新方法不会担心设置摄像机的position。我们还添加了一些日志记录,这样我们可以在运行应用时通过查看 Android 调试工具来查看正在设置的值。

  2. 完成后保存文件。

我们刚刚设置的代码最初是从 Unity 示例中派生出来的,但已经修改过以供你重用。由于访问location服务可能需要一段时间,我们将添加一个新的场景来处理location服务的启动。这将是一个可以在以后让它更美观的启动画面。

制作 Splash 场景

我们正在构建的Splash场景目前非常基础,只有一些状态消息。当然,您可以稍后对其进行样式化并添加您喜欢的任何图像。打开编辑器并完成以下操作:

  1. 创建一个名为Splash的新场景,并将其保存到Assets/HoloCore/Scenes文件夹中。

  2. 从菜单中选择 GameObject | UI | Panel。这将添加一个新的 Canvas,其中包含一个子面板和 EventSystem。将面板的背景颜色设置为深灰色。

  3. 选择面板,从菜单中选择 GameObject | UI | Text。将对象的名称更改为Status,并在检查器窗口中设置其属性,如下所示:

设置状态文本属性

  1. 这是我们将向用户显示那些状态消息的地方,这意味着我们需要一个可以更新状态消息并知道服务何时加载以及应用何时可以开始的脚本。

  2. Assets/HoloCore/Scripts文件夹中创建一个新的 C#脚本名为SceneLoader,并用以下代码替换预生成的代码:

using UnityEngine;
using UnityEngine.UI;

namespace Packt.HoloCore
{
  public class SceneLoader : MonoBehaviour
  {
    public string sceneName;
    public Text statusText;

    void Update()
    {
      if (SceneController.Instance.isLoaded)
      {
        SceneController.Instance.LoadScene(sceneName);
      }
      else
      {
        statusText.text = SceneController.Instance.status;
      }
    }
  }
}
  1. 这个简单的类是我们用来跟踪我们的SceneController的状态的。所有的动作都在Update方法中发生。我们首先通过测试isLoaded来检查SceneController是否已加载。如果场景未加载,我们在statusText.text对象中显示状态文本。记住,Update方法在每个渲染帧中运行,所以我们每秒测试这个条件几次。保存脚本,然后,我们需要将其添加为场景的组件。

  2. 返回 Unity 编辑器并等待新类编译。

  3. 创建一个名为ScreenLoader的新对象,并将新的ScreenLoader脚本添加到其中。然后,将SceneLoader的属性设置如下所示:

设置 SceneLoader 组件属性

  1. 将状态文本属性设置为Status对象。您可以使用瞄准器图标从场景中选择对象,或者只需从层次结构窗口中拖动对象并将其放入槽中。

  2. 保存场景。

  3. 打开构建设置,将Splash场景添加到构建中,并确保它是第一个场景,如下所示:

将 Splash 场景添加到构建中

  1. 继续连接、构建和运行。现在,您将被带到由Location服务识别的位置,即,如果您允许服务连接的话。

解决高度问题

如果您居住在海平面以上 500 米的地方,您可能会遇到一个问题。这个问题发生是因为我们的 AR 相机固定在 500 米的高度。问题是我们的 AR 相机在固定高度;我们现在需要根据相机的高度进行调整。重新打开编辑器并完成以下操作:

  1. 创建一个新的 C#脚本,并用以下代码替换代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Packt.HoloCore
{
  public class SceneCameraMover : MonoBehaviour
  {
    void Awake()
    {
      var altitude = SceneController.Instance.position.GetAltitude();
      transform.position = new Vector3(0f, (float)altitude, 0f);
    }
  }
}
  1. 这个脚本创建了一个名为SceneCameraMover的新类。SceneCameraMover的职责是在视图切换到 AR / MR 时将 AR 摄像头移动到位。

  2. 保存脚本并返回 Unity。

  3. Assets/HoloCore/Scenes文件夹中打开主场景。

  4. 展开 HoloKitCameraRig 并选择 VideoSeeThroughCamera。然后,使用添加组件并搜索 SceneCameraMover 来将脚本添加到组件中。

  5. 将 HoloKitCameraRig 的变换设置为000。现在我们将让脚本将摄像头移动到所需的位子。

  6. 保存场景和项目。

  7. 连接、构建和运行。前往海拔较高的地区,比如山区,并切换到 AR / MR 视图。现在摄像头应该根据你观看场景的海拔高度正确定位。

确保你探索世界各地其他感兴趣的区域。在下一节中,我们将完成本章和我们对 AR 和 ARCore 的讨论,并讨论下一步行动,看看你可以在哪里构建你自己的令人难以置信的科技演示或商业应用。

WRLD 的在线示例演示建议在 ARCore 或 ARKit 应用中使用备用流式传输摄像头进行 AR 视觉效果。然而,我们发现添加备用摄像头,加上 HoloKit 已经有的两个额外摄像头,使得应用比之前更加不稳定。如果你不打算使用 MR 或 HoloKit,你可能想尝试备用流式传输摄像头。

接下来是什么?

我们开发的科技演示是跨用户体验集成技术可能性的绝佳例子。这已经得到了广泛的应用,一个流行的例子就是《精灵宝可梦 GO》。事实上,你可以说《精灵宝可梦 GO》将 AR 带入了我们的词汇。那么,你接下来会用 ARCore 开发什么下一个大型的 AR 应用?你还在尝试想出一些想法或可能性吗?以下是一些应用想法或正在大力投资 AR 的行业列表:

  • 娱乐(游戏):游戏和娱乐是你能进入的最具竞争性的领域。为这个领域开发应用需要辛勤工作和一点运气。这个领域已经取得了一些巨大的成功,但这是在一些艰苦的工作和相当大的支持之后。

  • 医疗保健(紧急服务):医疗保健行业正全力投入 AR / MR 和 VR 世界。由于这个行业资金雄厚,现在在这些技术领域处于领先地位。如果你想进入尖端现实开发领域,这就是你应该所在的空间。进入这个领域可能更具挑战性,因为这个行业传统上更加孤立,但现在随着增长爆炸,有很多机会。

  • 市场营销(零售):随着增强现实变得更加主流和易于获取,我们将遇到在这个领域开发的新应用。已经有一些伟大的创新概念被用来促进销售,并且效果显著,但增强现实在这个领域最近已经变成了一种新奇事物。然而,如果你和市场营销领域的人交谈,他们会同意总有一天大部分广告将通过增强现实提供。在此之前,也许你可以想想下一个伟大的应用,它将卖出汉堡。

  • 教育(知识转移):这是一个非常大的行业,进入可能很困难,尤其是如果你计划将你的应用放入教室。或者,你可以构建一个教育应用,也许教你如何烹饪,但通过应用商店提供。无论如何,这是一个可能很难进入但非常有益的行业,尤其是如果你喜欢教学或学习。

  • 军事:除非你有军事背景或其他既定资历,否则进入这个领域非常困难。这很可能意味着你需要有强大的教育背景。如果你能进入这个领域,这确实是一个有趣的选择,但绝对不是适合每个人。如果你选择了这个方向,你肯定会从事最前沿的应用或工具的开发工作。

  • 旅游与旅游业(历史):这个领域与教育有所交叉,因为可能适用一些相同的原则。也许是通过增强现实向某人展示该地区的历史战役。对于所有技能水平的开发者来说,在这个领域构建增强现实/混合现实应用有很多机会。

  • 设计(所有):这个领域可以与零售应用紧密相连。也许是在某人身上叠加一套服装,或者尝试确定一把椅子是否适合房间。我们把这个领域放在列表的后面,因为我们的专家调查也把这一项列得较低。然而,正如我们所展示的,ARCore 有很多优秀的设计应用。

  • 工业(制造业):增强现实的应用可以帮助人类用户,同时也为未来系统或其它流程的自动化提供更好的基础。这意味着我们现在为人类构建的增强现实系统也将帮助我们使未来的制造机器人变得更智能。

  • 汽车:我们已经看到增强现实系统在汽车中的应用有几年了。从抬头显示到 GPS 设备,这个行业已经接受了增强现实,尽管为这个行业开发嵌入式增强现实应用可能并不合理。大多数用户,即驾驶员,可能会更愿意使用他们设备上的增强现实。也许对于汽车行业来说,在车辆中提供一个带有增强现实界面的移动设备底座更有意义?

  • 音乐:这更多的是针对音乐家而不是听众。这是一套帮助音乐家创作和工作的 AR 工具。可能并不适合所有人,也许将来 ARCore 会嵌入语音识别或其他音频识别技术。

无论你计划构建什么作为你的下一个应用程序,我们都衷心祝愿你一切顺利,并热切地想了解任何伟大的应用程序。请务必与作者联系,分享你的伟大应用程序概念。

练习

请独立完成以下练习:

  1. 返回到 HoloCore 示例,并使用一个方块或球体追踪用户的位置。提示——本示例的第一部分包含在代码下载中。

  2. 追踪用户在地图上的移动位置。提示——你现在需要根据最新的 GPS 读数更新用户的位置。

  3. 追踪你周围多个用户的位置。提示——你可以使用 Firebase 实时数据库来追踪用户的地理位置坐标。

摘要

对于本章,我们稍微偏离了 AR,探索了混合增强和混合现实。我们发现,我们可以通过一个简单的设备 HoloKit 或其他廉价的头盔轻松体验混合现实应用程序。ARCore 很好地追踪用户,非常适合添加 MR 体验。谁知道呢,在未来的某一天,当每个人都戴着 MR 眼镜时,我们甚至能区分 AR 和 MR 的不同吗?然后我们设置了 HoloKit 模板应用程序,开始构建一个快速的 MR 演示。之后,我们通过添加 WRLD 扩展了我们的演示。正如我们所学的,WRLD 是一个有趣且易于使用的 API,可以快速为我们提供一些大型的、令人印象深刻的 3D 场景,这些场景代表了用户所在区域。从那里,我们为所有用户开发了一系列场景,将地图触摸界面从地图的全混合现实视角移动,在那里我们能够从他们的设备 GPS 中获取用户的地理坐标,并将它们放置在 WRLD 中的相同位置。最后,我们展望未来,并关注你可以专注于应用程序开发的行业。

我们将在下一章中完成我们的旅程,讨论性能和故障排除,这两者都将有助于你在提升技能成为更好的 AR 开发者时有所帮助。

第十一章:性能技巧和故障排除

这将是探索ARCore和增强现实之旅的结束。在本章中,我们将探讨 AR 和移动应用程序的一般性能技巧。然后,我们将介绍一些故障排除解决方案,以备你遇到任何问题时使用。我们将讨论你可能会遇到的具体问题,以及如果你遇到问题时应遵循的更一般模式。以下是本章我们将涵盖的主要主题摘要:

  • 诊断性能

    • Chrome DevTools

    • Android Profiler

    • Unity 编辑器

  • 提高性能的技巧

  • 一般故障排除

  • 故障排除技巧

如你很可能在本书中多次注意到的,AR 应用程序需要高水平的性能才能提供引人入胜的用户体验。在下一节中,我们将探讨我们如何使用每个平台来诊断性能。

诊断性能

在本节中,我们将查看你需要采取的具体步骤来诊断我们每个开发平台(Web、Android 和 Unity)的性能。在处理新或不熟悉的技术时,很容易失去对性能的跟踪。因此,你通常希望将某种形式的性能评估作为开发过程的一部分,甚至可能为当你的应用程序以低于标准的性能或帧率渲染时实施一些最低帧率警告。然而,在我们开始设计性能测试之前,我们想要了解如何在每个平台上跟踪性能,从下一节开始,我们将使用Chrome DevTools来跟踪 Web 平台。

Chrome DevTools

当你使用 ARCore 开发 Web 项目时,你将享受到使用 Chrome 进行调试的便利。实际上,如果你进行比较,由于 Chrome DevTools 的功能,Web 项目性能工具在我们的平台列表中排名第二。让我们打开来自第五章,“现实世界运动跟踪”的spawn-at-surface.html Web 示例,并执行以下步骤:

  1. Android文件夹中启动http-server,端口为9999,就像我们之前做的那样。

  2. 选择一个与你的本地网络匹配的端点,并为其编写或复制以便稍后使用。记住,你的设备和开发机器需要处于同一网络中才能使这生效。

  3. 在你的设备上启动 WebARCore 应用程序,并导航到你的选定端点。这通常看起来像http://192.168.*.*:9999,其中*.*将被你的开发机器的特定 IP 地址替换。

  4. 使用 WebARCore,导航到http://[YOUR IP]:9999/three.ar.js/examples/spawn-at-surface.html

  5. 将你的设备连接到你的开发机器,无论是通过远程连接还是通过 USB 线缆。

  6. 返回你的机器并启动 Chrome。使用Ctrl + Shift + I(在 Mac 上为command + option + I)打开开发者工具。

  7. 点击远程设备选项卡并选择你的设备。然后,点击检查按钮以打开另一个包含设备上运行的应用 WebView 的 Chrome 窗口。

  8. 点击性能选项卡,然后选择记录按钮以开始性能分析,如下截图所示:

使用 DevTools 开始性能分析

  1. 让应用在你的设备上运行,同时性能分析器运行大约 30 秒,然后点击停止。停止捕获数据后,配置会话将在时间轴窗口中展开。

如果你发现性能分析会话不断崩溃,请通过取消勾选窗口顶部的复选框来禁用截图功能。

  1. 在摘要窗口顶部点击调用树选项卡,如下所示:

性能分析会话时间轴

  1. 调用树选项卡是你可以快速识别可能导致性能问题的函数调用或代码段的地方。在我们的例子中,我们深入到了 update 函数,并可以看到在这个函数内部花费的大部分时间是用 updateProjectionMatrix 调用来构建投影矩阵。由于这个调用位于 three.ar.js 库中,这不是我们关心的东西。

  2. 随意继续测试和性能分析。尝试设置几个 Andy 模型并看看这对性能有什么影响。

在任何性能分析中,你希望快速识别的是峰值或数据波峰出现的区域。确定这些峰值发生的原因将帮助你理解哪些活动可能影响性能。例如,放置一个 Andy 将由于将模型实例化到场景中而引起峰值。你还需要密切关注应用如何从峰值中恢复。例如,应用是完全恢复,还是只部分恢复?

如果你正在你的 Web 应用中进行数据传输或执行 AJAX 调用,那么你还需要监控网络性能。网络选项卡具有与性能选项卡类似的工具界面。

在识别到峰值后,你将希望扩展你的视图以覆盖整个会话。然后,你可以展开调用树并识别最耗时的方法。如果你的应用在单个函数中花费了 80% 的时间,那么你需要非常小心该函数中进行的操作。找到并优化昂贵的函数通常可以让你在应用性能上获得快速提升。虽然工具不同,但相同的原理适用于我们所有的开发平台。

我们只是刚刚开始探索 DevTools 的可能性。如果你进行任何数量的 Web 开发,你将很快熟悉这些工具。在下一节中,我们将介绍 Android 性能分析工具。

Android 性能分析器

Android Studio 拥有出色的性能分析工具;毕竟,它提供了与你的移动 Android 设备最接近的金属到金属接口。然而,它的使用并不像 DevTools 那样简单,因此在与其他性能分析工具的比较中排名第三。我们将使用我们合作过的其中一个 Android 示例项目。打开 Android Studio,并选择 java_arcore_hello_arandroid (TensorFlow 示例) 示例项目之一,并执行以下步骤:

  1. 连接你的设备并将应用程序构建到设备上。等待应用程序在设备上开始运行。

  2. 从菜单中选择视图 | 工具窗口 | Android Profiler。这将打开一个性能分析工具窗口,如下面的截图所示:

图片

Android 性能分析器捕获实时会话

  1. 当应用程序运行时,观察内存和 CPU 使用情况。你可以点击图表中的任何一点来扩展视图,查看调用堆栈和代码执行的多种其他视图,如下面的截图所示:

图片

检查实时性能分析会话

  1. 你也可以通过在性能分析窗口顶部按下记录按钮来记录会话以供后续检查。

在这一点上,你可以使用 Android Profiler 寻找性能峰值或各种函数调用的总体性能,就像你在 Chrome 中做的那样。Android 工具的学习和使用可能比较困难,但如果你在进行任何严肃的 Android/Java 开发,它们是值得努力的。在下一节中,我们将探讨使用 Unity 的最后一种性能分析方式。

Unity 性能分析器

Unity 是一个功能强大的工具,拥有一个非常强大的性能分析工具,使用起来非常愉快,不仅可以用于性能分析,还可以深入了解 Unity 的内部工作原理。打开 Unity 编辑器中的一个我们合作过的示例项目。对于这个例子,我们将使用来自第十章 混合现实中的混合的 HoloCore,但如果你更喜欢其他应用程序,请随意使用。在打开编辑器后,执行以下步骤:

  1. 从菜单中选择窗口 | 性能分析器。窗口将未停靠打开。通过标签拖动窗口,并将其拖到游戏窗口标签旁边以停靠。通常,我们会将性能分析器停靠在检查器旁边,以便你在编辑器中运行游戏时可以观察性能分析。由于我们无法在编辑器中运行 ARCore 应用程序,因此现在我们将通过将其停靠在游戏窗口旁边为性能分析器腾出更多空间。

  2. 打开构建设置对话框,检查是否启用了开发构建和自动连接性能分析器设置,如下面的截图所示:

图片

设置开发构建设置

  1. 使用 USB 连接你的设备,构建并运行。让应用程序在设备上继续运行。

  2. 返回编辑器并打开活动播放器下拉菜单,选择 AndroidPlayer(ADB@127.0.0.1:someport),如图所示:

Unity Profiler 从 Android 设备捕获会话

  1. 点击前一个屏幕截图所示的一个峰值。当 CPU 面板被选中时,将注意力集中在底部的详细面板上。

  2. 使用下拉菜单选择时间线,如下所示:

检查性能分析会话的详细信息

  1. 这里有很多有用的信息,一开始可能会让人感到不知所措。幸运的是,Unity 界面是自文档化的,你可以快速了解什么是好的或坏的。我们将在稍后更详细地介绍需要注意哪些区域,但现在,请注意渲染时间和总分配内存。对于渲染时间,你通常会看到以毫秒(ms)或每秒毫秒(milliseconds)为单位的时间,以及每秒帧数(FPS)或每秒帧数。一个好的规则是确保你的帧率保持在 30 FPS 以上。当为移动应用构建时,内存同样可能至关重要。

  2. 当你在进行性能分析时,如果你使用的是HoloCore,例如,通过改变现实模式来对应用施加压力。然后,继续深入到各个详细面板,并观察应用会话中不同点的值如何变化。

Unity 工具提供了用于性能分析应用的最强大和直观的界面。虽然我们几乎只是触及了我们查看的所有工具的威力,但你将注意到它们都具有强烈的相似性。当然,这不是偶然的,在你学会在一个平台上对应用进行性能分析的所有细节之后,其中许多技能将会迁移。在下一节中,我们将查看一系列关于提高应用性能的技巧。

管理更好性能的技巧

现在我们已经掌握了如何对应用进行性能分析,让我们来看看将影响性能的主要项目。这些项目的顺序按一般重要性排序,但你的应用的具体需求可能会改变这些优先级。下次你需要或想要对应用进行性能分析时,请随时参考以下清单:

  • 渲染(包括负责渲染帧的所有 CPU 和内存资源)

    • 渲染循环(CPU 性能):检查render函数的执行时间,并留意任何昂贵的调用。确保最小化任何对象实例化、日志记录或内部循环。记住,渲染函数,通常称为Update,每秒将被调用 30 次或更多。我们查看的所有工具都将让您执行这项至关重要的任务。

    • 帧率(渲染时间):除了优化代码之外,帧率通常由我们渲染的复杂性和对象数量决定。因此,你可能想要尽可能优化着色器,但很多时候,通过减少三角形的数量或模型的复杂性,你可以获得很大的性能提升。在移动应用中,这意味着寻找低多边形简单模型作为资源。另一个有用的选项是为你的模型构建各种细节级别LOD),并使用适合相应细节级别的适当版本。Unity 提供了一套免费和付费的 LOD 优化资源,可以简化这项任务。

    • 光照材质:不仅模型的复杂性会影响性能,你用来渲染模型的纹理或材质(着色器)以及灯光也会影响。确保你限制纹理的大小,或者确保所有着色器都有回退或简化版本。你还可以尽可能简化光照。

    • 内存(图形):一般来说,你的应用使用的内存越多,渲染一帧的成本就越高。当然,也有例外,但监控内存可以定位潜在的问题或甚至内存泄漏。高内存消耗通常指向可能需要优化的模型、纹理或其他资源。

  • 加载(在场景中添加、替换或更新新内容的过程)

    • 对象实例化:具有多个详细纹理的大型复杂网格将需要额外的加载时间。你通常会想要缓存或预加载对象,以减少加载过程中的中断。在我们的大多数示例中,这并不是一个问题,但一个很好的例子是在第十章,在混合现实中的混合,我们使用了 3D 地图。

    • 流式传输:流式传输是一种将媒体资源(如音频或视频)加载到播放所需内容的绝佳方式。在 Unity 中,将资源设置为流而不是完全加载相对简单,可以在资源定义时完成,如下面的截图所示:

图片

在音频资源上启用流式传输

    • 垃圾回收:在我们所有的平台上,当应用运行时,都会通过某种形式的垃圾回收来管理对象的生命周期。将创建和销毁的对象数量保持在最低限度可以减轻垃圾回收的压力。如果垃圾回收很快填满,这通常会触发一个昂贵的回收操作,这可能会冻结你的应用。你可以通过创建对象池来减少对象的实例化和回收。对象池就是你在内存中保留一批对象,根据需要从场景中添加和移除对象。
  • 交互(包括用户或环境的任何活动,无论是物理的还是人工的)

    • 环境检测:这是一个更具体于 AR 应用程序的要求,对于 ARCore 来说是至关重要的。如果你计划增强点云或平面的检测,请确保尽可能优化此代码。

    • 对象交互(物理):限制你需要测试的用于射线投射或碰撞的对象数量。你可以通过标记你的对象然后过滤标签来实现这一点。在 Unity 中,这个功能是内置的,但对于其他平台来说实现起来相当简单。

    • AI(机器学习):如果你的应用程序需要为非玩家角色(NPC)或其他代理执行任何 AI,那么你可能想限制任何昂贵的 AI 或学习调用。例如,你可能会想将 AI 限制在每第五或第十帧运行,而不是每帧都运行。通常,这还有额外的优势,即使 AI 更真实或更智能,因为它似乎在采取行动之前会短暂地思考。

在寻找可能存在的性能问题时,前面的列表是一个很好的起点,并且它应该很好地作为你需要分析的平台指南。在下一节中,我们将介绍一些通用的故障排除技巧,这些技巧可以在开发过程中应用于每个平台。

一般故障排除

我们学习了每个平台的调试过程的基础,但我们从未介绍过任何调试或故障排除的技术。就像分析一样,有一个基本的指南或列表可以遵循,以使你在故障排除时更加高效。使用以下步骤列表来帮助你解决下一个问题:

  1. 控制台:首先需要查看的是控制台报告的任何错误。我们所有的平台都提供了控制台,你应该熟悉在你的选择平台上如何访问它。错误有意义吗?你能否确定导致问题的代码部分或项目?

  2. Google: 如果你看到一条晦涩的控制台消息,并且不确定它是什么意思,那么就去谷歌搜索。你不需要搜索整个消息,只需提取短语中的五到六个关键词并使用这些关键词。你可能还需要添加一些词来覆盖你的平台;例如,Java、Android 或 Unity C#。

  3. 日志记录:通过在代码的关键区域注入日志语句来对你的代码进行工具化。如果你的代码没有向控制台报告错误,请添加日志以便你知道代码的流向。这可以帮助你确定关键代码部分是否被运行。

  4. 复现:隔离问题并尝试在一个新的项目或测试应用中复现它。如果你无法隔离代码,你可能有一个更大的问题,你可能需要重构。通常,除非问题需要解决方案或更严重,否则复现项目可以巩固你对问题的理解。复现问题不仅可以帮助你解决问题,还可以帮助你重构和清理代码。

  5. 发布它:如果你在复制问题后仍然没有找到解决方案,那么请寻找合适的论坛并发布你的问题。确保你在发布问题时提供复制的样本。这通常会是人们首先要求你的东西,特别是如果问题很复杂。此外,展示你已经花费时间复制问题会使你的帖子更具可信度,并避免回答简单问题所浪费的时间。

  6. 绕过它:如果你无法解决你的问题,那么就绕过它。有时,解决问题是不可能的,或者成本太高,耗时太长。那么,你需要想出另一种方法来构建功能或修改它。这通常需要回到设计师或愿景者那里,如果你的项目有这样的人的话,并咨询他们可能的解决方案。

当你遇到问题时,上述列表又是一个好的起点。如果你已经开发软件一段时间了,你可能会有自己的流程,但上述列表可能与你的大致相同。

故障排除代码

对于那些在代码故障排除方面经验较少的你们,请遵循以下简单练习:

  1. 打开 Unity 编辑器到一个新的空白项目和起始场景。

  2. 在场景中创建一个立方体对象。

  3. 选择立方体,在检查器窗口中,点击“添加组件”。选择“新脚本”,将名称设置为Test,然后点击“创建并添加”将脚本添加到对象中,如下所示:

图片

使用“添加组件”创建一个新的脚本。

  1. 这将在根Assets文件夹中创建一个脚本。这不是放置脚本的最佳位置,但这种方法对于创建快速测试脚本很有用。

尽量避免在你的主要开发项目中编写复制/测试或概念验证代码。尽量保持你的主要项目尽可能干净。如果你正在构建任何商业产品,你肯定会想通过额外的努力验证你的项目中的每个资产或资源,至少是你负责的那些。定期检查你的参考和资产是一个有用的团队练习,可能每月一次,如果你正在做出多次更改,可能更频繁。

  1. 在你选择的编辑器中打开Test脚本,并添加以下突出显示的代码行:
using UnityEngine;
public class Test : MonoBehaviour {
  public GameObject monster;  //add me
  // Use this for initialization
  void Start () {

  }

  // Update is called once per frame
  void Update () {
    if(monster.transform.position.x > 5)  //and add this section
 {
 Destroy(this);
 }
  }
}
  1. 此脚本简单地跟踪一个名为monsterGameObject,并确定其x位置是否超过5。当它超过时,脚本使用Destroy(this)销毁其父对象。

  2. 保存文件并返回 Unity。

  3. 在场景中添加另一个立方体并将其重命名为Monster

  4. 在编辑器的顶部按下播放按钮以开始场景。

  5. 点击控制台窗口将其带到最前面。观察错误流,如下截图所示:

图片

显示错误流的控制台窗口

  1. 因此,我们看到的通用错误信息是 UnassignedReferenceException。快速谷歌搜索这个文本,看看结果中有什么。这给你带来了比控制台上的消息更多的洞察力吗?

很可能你已经解决了这个问题,但让我们继续假设我们仍然困惑。比如说,谷歌的结果不太有帮助。继续跟随我们的故障排除(调试)过程:

  1. 我们下一步是记录日志。在Update方法中添加以下代码行:
Debug.LogFormat("Monster is at position ({0})", monster.transform.position);
  1. 这行代码将调试信息输出到控制台。

  2. 当然,再次运行代码将重现相同的问题,这也意味着你只是复制了问题,并在一行代码中覆盖了下一步。

虽然日志记录是好的,但它也可能不好,对性能和试图破解你的游戏的人来说都是如此。你通常可以控制为每个环境输出你想要的日志级别。然而,作为一般规则,除非信息是必需的或有用的,否则尽量避免过多的日志记录。

在我们的示例中,此时应该很明显知道问题是什么,但当然,情况并不总是如此。当你已经尝试了所有其他途径时,如果问题仍未解决,那么你可以将问题发布到适当的论坛。如果长时间没有回复,那么你可能需要继续前进并绕过这个问题。奇怪的是,在编写绕过问题的解决方案一半时意识到错误的情况并不少见。这种情况会发生,最好的建议就是继续前进。失败是学习的好方法,你失败得越多,你学到的就越多。

在加拿大,你通过去停车场旋转并失去控制来学习如何在冰雪中驾驶。虽然这确实可以带来很多乐趣,但它教会了驾驶员如何在可控的恶劣天气条件下失去控制。这不仅给了驾驶员更多的信心,还加强了在高速度下车辆失去牵引力时的控制方法。然后,当驾驶员真的失去控制时,他们可以尝试避免或最小化损害。对你的代码进行单元测试并不像学习如何在冰雪中驾驶。它测试了代码的极限,这样你就可以确信如果某件事成功或失败会发生什么。

大多数开发者都难以理解在游戏或图形项目中添加单元测试代码的概念。实际上,由于缺乏工具或知识,这种做法是被劝阻的。单元测试或严格测试你的代码永远不会是浪费时间,而且为你的平台使用测试框架将大大简化这项任务。现在,无论你决定是否为你的代码编写单元测试,这完全取决于你,但你应该学习如何进行单元测试。仅仅学习如何测试你的代码就会让你眼界大开,看到一片新的可能性世界。

你编写的代码越多,开发的游戏或其他应用程序越多,你在故障排除错误方面就会变得越好。实践经验是无法替代的。在下一节中,我们将探讨您在阅读本书过程中可能遇到的更多具体故障排除项目。

练习

独立完成以下练习:

  1. 修改检查怪物位置的if语句,以便代码完全避免错误。

  2. 你能修复代码中的未分配引用问题吗?提示——查看GameObject.Find方法。

  3. 编写一个使用键盘或鼠标作为输入来移动其块的怪物脚本。

故障排除技巧

在使用任何新技术时,可能会出现很多问题,这不仅是因为你对它的不熟悉,还可能是因为这项技术可能没有准备好做它声称能做的事情。以下是在阅读本书时可能会遇到的一些常见问题的表格:

平台 问题 解决方案
Web 无法加载页面或找到服务器 检查您是否正在使用适合您机器的正确端点。如果您有几个选择,尝试不同的选项。确认您的系统没有运行可能阻止通信的防火墙。尝试暂时禁用防火墙并再次尝试。如果这解决了问题,那么在防火墙中为端口9999或您使用的任何端口创建一个例外。
Web ARCore 在页面上显示错误消息 确保 ARCore 服务已安装,并且您正在使用适用于您平台的 WebARCore 启用浏览器。
Web 缺少引用 确保您检查的内容或脚本加载路径是否正确。您可以在 Chrome 中通过检查“源”标签轻松完成此操作。
Android 无法构建或缺少引用 Android Studio 非常有帮助,但它有时需要加载大量的引用。在这种情况下,您只需要有耐心,加载项目所需的所有内容。如果您是从头开始构建项目,您需要参考关于 Android 项目设置的优质教程来正确完成。如果您发现您仍然缺少引用,那么请创建一个新的项目并再次尝试。
Android/Unity 无法连接到设备 这种情况很少发生,但偶尔也会发生。请拔掉并重新插入您的设备,或在控制台或 shell 窗口中运行adb devices。如果您正在远程连接,您可能需要通过重新连接 USB 并重置连接来重新配置设备。
Unity 在进入播放模式之前必须修复所有编译错误 检查控制台是否有任何红色错误消息。双击任何消息将跳转到代码中的语法错误。尽力解决或删除语法错误。
Unity 无法构建 检查是否有任何编译错误,并确保您的场景已通过构建设置对话框添加到构建中。
Unity 构建停滞 如果你连接到设备并且电缆暂时断开,这可能会导致构建锁定或停止。通常,只需点击“取消”即可退出构建过程,然后你可以重新开始。偶尔,非常罕见的情况下,你可能需要重新启动 Unity。

上述表格应该能帮助你解决你在阅读本书过程中可能遇到的更多常见问题。如果你遇到的问题不在列表中,当然可以咨询 Google 或其他你喜欢的搜索引擎。你经常会发现,仅仅重新构建项目就能教会你哪里出了错。

摘要

这本书的最后一章,我们花了很多时间学习性能和故障排除。我们首先介绍了你可以在每个平台上使用的各种性能分析工具。然后,我们介绍了一系列非常基本的提高应用程序性能的技巧,涵盖了从帧率到资产大小的一切。这使我们能够介绍解决基本问题的技巧,以及更具体地说,是编码问题。最后,我们提供了一个有用的故障排除技巧表,如果你遇到更具体的问题,可以参考这个表。

现在你已经完成了这本书,你刚刚开始踏上探索增强现实(AR)和混合现实(MR)的旅程。Packt 出版社在 AR、网络开发、Android 开发和当然还有 Unity 方面有更多优秀的书籍。我们也鼓励读者们寻找你们当地的 AR/VR 聚会,如果没有的话,也可以自己创建一个。看到别人在 AR 或甚至 VR 开发中所做的事情可以激发新的想法和最佳实践。我们所有人,实际上都刚刚开始了一段激动人心的旅程,进入一个将彻底改变我们未来生活的全新计算界面。随着可穿戴主流商用眼镜的持续发展,你也将准备好迎接 AR 领域更多的变化。

posted @ 2025-09-28 09:13  绝不原创的飞龙  阅读(3)  评论(0)    收藏  举报