安卓增强现实应用开发-全-

安卓增强现实应用开发(全)

原文:zh.annas-archive.org/md5/95678E82316924655B17444823E77DA0

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

增强现实技术将物理世界与虚拟世界融合,产生魔幻效果,并将应用从屏幕带到你的手中。增强现实技术彻底重新定义了广告、游戏以及教育的方式;它将成为移动应用开发者需要掌握的技术。本书使你能够实际在 Android 上实现基于传感器和计算机视觉的增强现实应用。了解实施增强现实应用程序的理论基础和实践细节。通过动手实践,你可以快速开发和部署新颖的增强现实应用。

本书涵盖的内容

第一章,增强现实概念与工具,介绍两种主要的增强现实方法:基于传感器和基于计算机视觉的增强现实。

第二章,观察世界,介绍构建增强现实应用程序的第一步基础:在设备上捕捉和显示真实世界。

第三章,叠加世界,帮助你使用 JMonkeyEngine 将高保真的 3D 模型覆盖在物理世界上。

第四章,在世界中定位,提供使用传感器和 GPS 实现你自己的增强现实浏览器的基本构建块。

第五章,与好莱坞相同 - 在物理对象上的虚拟现实,为你讲解基于计算机视觉的 AR 中 Vuforia^(TM) SDK 的强大功能。

第六章,使其互动 - 创建用户体验,解释如何让增强现实应用具有互动性。具体来说,你将学习如何开发射线拣选、基于接近度的互动以及基于 3D 动作手势的互动。

第七章,进一步阅读与技巧,介绍更多高级技术,以提升任何 AR 应用程序的开发。

你需要为这本书准备的内容

如果你想要为 Android 开发增强现实应用,你可以与常规 Android 开发者共享大部分工具。特别是,你可以利用广泛支持的Android 开发者工具包ADT Bundle)。这包括:

  • Eclipse 集成开发环境IDE

  • 用于 Eclipse 的Android 开发者工具ADT)插件

  • 针对目标设备的 Android 平台(可以下载其他平台)

  • 配备最新系统镜像的 Android 模拟器

除了许多 Android 开发环境共有的这个标准包之外,你还需要:

  • JMonkeyEngine (JME),版本 3 或更高版本的快照

  • 高通® Vuforia^(TM) 软件开发包Vuforia^(TM)),版本 2.6 或更高

  • 安卓原生开发工具包Android NDK),版本 r9 或更高

本书的目标读者

如果您是 Android 移动应用开发者,并且想要使用增强现实技术将移动应用开发提升到下一个层次,这本书就是为您准备的。我们假设您熟悉 Android 开发工具和部署。如果您有使用 Android 外部库的经验,这将非常有用,因为我们会用到 JMonkeyEngine 和 Vuforia^(TM) SDK。如果您已经使用过 Android N,这很棒,但不是必须的。

约定

在这本书中,您会发现多种文本样式,用于区分不同类型的信息。以下是一些样式示例及其含义的解释。

文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名会以下面的形式显示:"最后,您在CameraPreview类的onSurfaceChanged()方法中注册了Camera.PreviewCallback接口的实现。"

代码块设置如下:

public static Camera getCameraInstance() {
  Camera c = null;
  try {
    c = Camera.open(0);
  } catch (Exception e) { ...  }
  return c;
}

新术语重要词汇会用粗体显示。您在屏幕上看到的词,比如菜单或对话框中的,会在文本中以这样的形式出现:"在弹出菜单中,选择运行方式 | 1 安卓应用。"

注意

警告或重要信息会以这样的框显示。

小贴士

提示和技巧会像这样出现。

读者反馈

我们始终欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢或可能不喜欢的内容。读者的反馈对我们来说非常重要,它帮助我们开发出您真正能从中获益最多的书籍。

如果您想要给我们发送一般性反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件的主题中提及书名。

如果您在某个主题上有专业知识,并且有兴趣撰写或为书籍做贡献,请查看我们在www.packtpub.com/authors上的作者指南。

客户支持

既然您已经拥有了 Packt 的一本书,我们有许多方法可以帮助您充分利用您的购买。

下载示例代码

您可以从您的账户www.packtpub.com下载您购买的所有 Packt 书籍的示例代码文件。如果您在别处购买了这本书,可以访问www.packtpub.com/support注册,我们会直接将文件通过电子邮件发送给您。您也可以在github.com/arandroidbook/ar4android找到代码文件。

勘误

尽管我们已经竭尽全力确保内容的准确性,但错误仍然在所难免。如果你在我们的书中发现了一个错误——可能是文本或代码中的错误——我们非常感激你能向我们报告。这样做可以避免其他读者产生困扰,并帮助我们改进本书后续版本。如果你发现任何勘误信息,请通过访问www.packtpub.com/submit-errata,选择你的书籍,点击勘误提交表单链接,并输入你的勘误详情。一旦你的勘误信息被核实,你的提交将被接受,并且勘误信息将被上传到我们的网站,或添加到该标题勘误部分现有的勘误列表中。任何现有的勘误信息可以通过选择你的标题,在www.packtpub.com/support进行查看。

盗版

互联网上版权资料的盗版问题在所有媒体中持续存在。在 Packt,我们非常重视保护我们的版权和许可。如果你在任何形式下,在互联网上遇到我们作品的非法副本,请立即提供我们该位置地址或网站名称,以便我们可以寻求补救措施。

如果发现疑似盗版资料,请通过<copyright@packtpub.com>联系我们,并提供一个链接。

我们感谢你帮助保护我们的作者,以及我们为你提供有价值内容的能力。

问题

如果你在这本书的任何方面遇到问题,可以通过<questions@packtpub.com>联系我们,我们将尽力解决。

第一章:增强现实概念与工具

增强现实AR)为我们提供了一种与物理(或真实)世界互动的新方式。它在我们桌面电脑或移动设备的屏幕上,创建了一个融入了数字(或虚拟)信息的现实世界的改良版本。将虚拟与现实融合在一起可以开创一系列全新的用户体验,超越常见应用的能力范围。你能想象在自家附近玩第一人称射击游戏,街角突然冒出怪物吗(正如澳大利亚南澳大学的Bruce Thomas开发的 ARQuake 中所示,见下截图左侧)?在自然历史博物馆看到尘封的恐龙骨架在你眼前虚拟复活,血肉丰满,难道不是一件激动人心的事情吗?或者,当你给孩子讲故事时,看到一只骄傲的公鸡出现在书上并走在页面之上(正如Gavin Bishop所著的《杰克建的房子》AR 版所示,见下截图右侧)。在这本书中,我们将向您展示如何在 Android 平台上实际实现这些体验。

增强现实概念与工具

十年前,有经验的研发人员是少数能够创建这类应用程序的人群。它们通常仅限于演示原型或者在有限时间内运行的临时项目中。现在,开发增强现实(AR)体验已经成为广泛移动软件开发者的现实。在过去的几年里,我们见证了计算能力、传感器小型化以及多媒体库的易用性和功能性的巨大进步。这些进展使得开发者能够比以往更容易地制作 AR 应用程序。这已经导致在诸如 Google Play 之类的移动应用商店中涌现出越来越多的 AR 应用。尽管热情的程序员可以轻松地将一些基本的代码片段拼凑起来,创建一个基本 AR 应用的外观,但它们通常设计粗糙,功能有限,几乎不具备可重用性。为了能够创建复杂的 AR 应用,我们必须真正理解增强现实(Augmented Reality)是什么。

在本章中,我们将引导您更深入地理解 AR。我们将描述 AR 的一些主要概念。然后,我们将从这些例子过渡到 AR 的基础软件组件。最后,我们将介绍本书将使用的开发工具,这些工具将支持我们构建高效且模块化的 AR 软件架构的旅程。

准备好为了增强现实而改变你的现实了吗?让我们开始吧。

AR 概念快速概览

随着近年来增强现实在媒体上变得越来越流行,不幸的是,一些关于增强现实的扭曲观念也产生了。任何与真实世界有关,并涉及一些计算的活动,比如站在商店前观看 3D 模型穿着最新时尚,都变成了增强现实。增强现实从几十年前的实验室研究中出现,产生了不同的定义。随着越来越多研究领域(例如,计算机视觉、计算机图形学、人机交互、医学、人文和艺术)将增强现实作为一个技术、应用或概念进行研究,现在存在多个重叠的增强现实定义。我们不会为您提供详尽的定义列表,而是介绍任何增强现实应用中存在的一些主要概念。

感官增强

“增强现实”这个词本身包含了现实的概念。增强通常指的是通过附加信息影响你的一个人类感官系统,如视觉或听觉的方面。这种信息通常被定义为数字或虚拟的,并由计算机生成。目前的技术使用显示来叠加和融合物理信息与数字信息。为了增强你的听觉,配备麦克风的改良耳机或耳塞能够实时将你周围的声音与计算机生成的声音混合。在这本书中,我们将主要关注视觉增强。

显示技术

家中的电视屏幕是感知虚拟内容的理想设备,无论是来自广播的流媒体还是播放的 DVD 内容。不幸的是,大多数常见的电视屏幕无法捕捉现实世界并将其增强。增强现实显示需要同时展示真实世界和虚拟世界。

增强现实最早期的显示技术之一是由Ivan Sutherland在 1964 年生产的(名为“达摩克利斯之剑”)。该系统被刚性安装在天花板上,并使用一些 CRT 屏幕和一个透明显示来创建视觉上融合真实和虚拟的感觉。

从那时起,我们在增强现实显示中看到了不同的趋势,从静态显示发展到可穿戴和手持显示。其中一个主要趋势是使用光学透视OST)技术。这个想法是仍然通过半透明屏幕看到真实世界,并在屏幕上投射一些虚拟内容。真实世界和虚拟世界的融合不是在电脑屏幕上发生的,而是直接在你的视网膜上,如下面的图所示:

显示技术

AR 显示的另一个主要趋势是我们所说的视频透视VST)技术。你可以想象不是直接感知世界,而是通过显示器上的视频来感知。视频图像与一些虚拟内容混合(正如你在电影中看到的)并返回到一些标准显示设备,如桌面屏幕、移动电话或如下图中即将出现的一代头戴式显示器:

显示设备

在本书中,我们将使用安卓驱动的移动电话,因此只讨论 VST 系统;所使用的摄像机将是手机背面的那一个。

3D 注册

手持显示设备(OST 或 VST),你已经能够将真实世界中的事物叠加在上面,正如你在电视广告中看到屏幕底部有文字横幅。然而,任何虚拟内容(如文本或图像)将保持其在屏幕上的固定位置。这种叠加实际上是静态的,你的 AR 显示屏将充当抬头显示HUD),但并不是如下图所示的那种真正的 AR:

3D 注册

Google Glass 是一个 HUD 的例子。尽管它使用像 OST 那样的半透明屏幕,但数字内容保持在一个静态位置。

增强现实(AR)需要更多地了解真实内容和虚拟内容。它需要知道物体在空间中的位置(注册)并跟踪它们的移动(追踪)。

注册基本上是将虚拟和真实内容在同一空间对齐的想法。如果你喜欢看电影或体育,你会注意到 2D 或 3D 图形经常被叠加到物理世界的场景中。在冰球中,球通常用彩色轨迹突出显示。在诸如沃尔特·迪士尼的《创》(1982 年版)等电影中,真实和虚拟元素无缝融合。然而,AR 与这些效果不同,因为它基于以下所有方面(由Ronald T. Azuma在 1997 年提出):

  • 它是 3D 的:在早期,有些电影是通过手动编辑将虚拟视觉效果与真实内容合并的。一个著名的例子是《星球大战》,其中所有的光剑效果都是由数百名艺术家手工绘制的,因此是逐帧制作的。如今,更复杂的技术支持将数字 3D 内容(如角色或汽车)与视频图像合并(这称为匹配移动)。AR 本质上始终在 3D 空间中完成这一工作。

  • 注册是实时发生的:在电影中,一切都是预先录制并在工作室中生成的;你只需播放媒体。在 AR 中,一切都是实时的,因此你的应用程序需要在每个实例中将现实与虚拟性合并。

  • 它是交互式的:在电影中,你只是被动地从拍摄场景的地方观看。在 AR 中,你可以主动地四处移动,前进和后退,并转动你的 AR 显示屏——你仍然会看到两个世界之间的对齐。

与环境的互动

构建丰富的 AR 应用需要环境之间的交互;否则,你最终会得到很快就变得无聊的漂亮 3D 图形。AR 交互是指选择和操作数字和物理对象,并在增强的场景中导航。丰富的 AR 应用允许你使用桌面上的对象,移动一些虚拟角色,在街上行走时用手选择一些漂浮的虚拟对象,或者与出现在你的手表上的虚拟代理交谈,安排当天晚些时候的会议。在第六章 让它互动——创造用户体验中,我们将讨论移动 AR 的交互。我们将探讨一些标准的移动交互技术如何也应用于 AR。我们还将深入研究涉及操作现实世界的特定技术。

选择你的风格——基于传感器和计算机视觉的增强现实(AR)

在本章前面的内容中,我们讨论了增强现实(AR)的定义,并详细阐述了显示、注册和交互。由于本书中的一些概念同样适用于任何 AR 开发,我们将特别关注移动 AR

移动 AR 有时指的是任何可携带、可穿戴的 AR 系统,这种系统可以在室内外使用。在本书中,我们将探讨目前最流行的移动 AR 含义——使用手持移动设备,如智能手机或平板电脑。借助当前一代的智能手机,可以实现两种主要的 AR 系统方法。这些系统以其特定的注册技术和交互范围为特征,同时它们也支持不同范围的应用。基于传感器的 AR 和基于计算机视觉的 AR 系统都使用视频透视显示,依赖于手机的摄像头和屏幕。

基于传感器的 AR

第一类系统称为基于传感器的 AR,通常被称为 GPS 加惯性 AR(有时也称为户外 AR 系统)。基于传感器的 AR 使用移动设备的位置传感器和方向传感器。结合位置和方向传感器可以提供用户在物理世界中的全球位置。

位置传感器主要支持全球导航卫星系统GNSS)接收器。最流行的 GNSS 接收器之一是 GPS(由美国维护),它几乎存在于所有智能手机上。

注意事项

其他系统目前正在(或即将)部署,例如 GLONASS(俄罗斯)、Galileo(欧洲,2020 年)或 Compass(中国,2020 年)。

手持设备上有几种可能的定向传感器,如加速度计、磁力计和陀螺仪。你的手持设备测量的位置和方向提供了追踪信息,这些信息用于在物理场景上注册虚拟对象。由 GPS 模块报告的位置可能不准确,且更新速度比你移动的速度慢。这可能导致滞后现象,即当你快速移动时,虚拟元素似乎会飘在后面。基于传感器的系统中最受欢迎的 AR 应用类型之一是 AR 浏览器,它们可以可视化兴趣点POI),即关于你周围事物的简单图形信息。如果你尝试一些最受欢迎的产品,如 Junaio、Layar 或 Wikitude,你可能会观察到这种滞后效应。

这种技术的优点在于,基于传感器的增强现实(AR)可以在全球范围内普遍适用,几乎在任何物理户外位置都能工作(比如你身处于沙漠中央或是一座城市中)。这种系统的一个限制是它们无法在室内工作(或者工作效果差),或者在任意遮挡区域工作(与天空没有视线,如在森林中或四周高楼林立的街道上)。我们将在第四章《在世界中定位》中进一步讨论这种类型的移动 AR 系统。

基于计算机视觉的 AR

另一种流行的 AR 系统是基于计算机视觉的 AR。这里的想法是利用内置相机的力量,不仅仅是为了捕捉和显示物理世界(如基于传感器的 AR 所做的)。这项技术通常与图像处理和计算机视觉算法一起工作,分析图像以检测相机可见的任何物体。这种分析可以提供关于不同物体位置的信息,因此也能提供关于用户的信息(更多内容请见第五章,《好莱坞风格——虚拟物体叠加在物理物体上》)。

这种技术的优点是事物似乎完美对齐。当前技术允许你识别不同类型的平面图像内容,比如特别设计的标记(基于标记的追踪)或更自然的内容(无标记追踪)。一个缺点是,基于视觉的 AR 在处理上较为繁重,并且可能非常快地耗尽电池。最近几代的智能手机更适合处理这类问题,因为它们针对能源消耗进行了优化。

增强现实(AR)架构概念

因此,让我们探讨如何支持之前描述的概念以及两种通用 AR 系统的发展。与开发任何其他应用程序一样,一些软件工程中的知名概念可以应用于 AR 应用程序的开发。我们将先看看 AR 应用程序的结构方面(软件组件),然后是行为方面(控制流程)。

AR 软件组件

增强现实应用程序可以结构化为三层:应用层、AR 层和操作系统/第三方层。

应用层对应于应用程序的领域逻辑。如果您想开发一个 AR 游戏,与游戏资源管理(角色、场景、物体)或游戏逻辑相关的任何内容都将在这个特定层中实现。AR 层对应于我们之前描述的概念的实例化。我们之前提出的每个 AR 概念和理念(显示、注册和交互)在软件层面都可以被视为一个模块元素、组件或 AR 层的服务。

你可以注意到,在图中我们将跟踪与注册分离开了,使得跟踪成为 AR 应用程序的一个主要软件组件。在任何 AR 应用程序中,提供空间信息给注册服务的跟踪都是一个复杂且计算密集型的过程。操作系统/第三方层对应于现有的工具和库,它们本身不提供任何 AR 功能,但将支持 AR 层。例如,移动应用程序的显示模块将与操作系统层通信,访问摄像头以创建物理世界的视图。在 Android 上,Google Android API 就是这一层的一部分。一些额外的库,如处理图形的 JMonkeyEngine,也是这一层的一部分。

在本书的其余部分,我们将向您展示如何实现 AR 层的不同模块,这也涉及到与操作系统/第三方层的通信。AR 应用程序的主要层(见以下图的右侧),以及它们的应用模块(以下图的左侧),在以下图中描绘:

AR 软件组件

AR 控制流程

在了解了软件层次和组件的概念之后,我们现在可以看看在典型的增强现实(AR)应用程序中信息的流动方式。在这里,我们将重点描述 AR 层的每个组件随时间如何相互关联,以及它们与操作系统/第三方层之间的连接是什么。

在过去十年中,AR 研究人员和开发人员趋向于使用一种广泛采用的方法来组合这些组件,并采用类似的执行顺序——AR 控制流程。在这里,我们介绍了社区总结并在以下图中概述的一般 AR 控制流程:

AR 控制流程

前面的图表,从下往上阅读,展示了 AR 应用程序的主要活动。这个顺序在 AR 应用程序中无限重复;它可以被视为典型的AR 主循环(请注意,我们在这里排除了领域逻辑以及操作系统活动)。每个活动对应我们之前展示的相同模块。因此,AR 层和 AR 控制流程的结构非常对称。

了解这个控制流程是开发 AR 应用程序的关键,因此我们将在本书的剩余部分回到这个主题并使用它。下一章我们将详细讲解各个组件和步骤。

因此,查看前面的图表,你的应用程序中的主要活动和步骤如下:

  • 首先管理显示:对于移动 AR 来说,这意味着访问视频摄像头并在屏幕上显示捕获的图像(你物理世界的视图)。我们将在第二章,观察世界中讨论这个问题。这也涉及到在物理摄像头和渲染你的数字对象的虚拟摄像头之间匹配相机参数(第三章,覆盖世界)。

  • 注册并跟踪你的对象:分析你手机上的传感器(方法 1)或分析视频图像(方法 2),并检测你的世界中的每个元素的位置(如相机或物体)。我们将在第四章,在世界中定位和第五章,与好莱坞相同 - 在物理对象上的虚拟中讨论这个方面。

  • 互动:一旦你的内容正确注册,你就可以开始与它互动,我们将在第六章,让它互动 - 创建用户体验中讨论这一点。

开发和部署的系统要求

如果你想要为 Android 开发增强现实应用程序,你可以与常规 Android 开发者共享大部分工具。特别是,你可以利用广泛支持的谷歌 Android 开发者工具包ADT Bundle)。它包括以下内容:

  • Eclipse 集成开发环境IDE

  • 用于 Eclipse 的谷歌 Android 开发者工具ADT)插件

  • 针对你的目标设备的 Android 平台(可以下载其他平台)

  • 带有最新系统映像的 Android 模拟器

除了这个许多 Android 开发环境共有的标准包之外,你还需要以下内容:

  • JMonkeyEngineJME)的快照,版本 3 或更高

  • 高通® Vuforia^(TM) 软件开发包Vuforia^(TM)),版本 2.6 或更高

  • Android 原生开发工具包Android NDK),版本 r9 或更高

JME Java OpenGL®游戏引擎是一个免费的工具集,可以让你的程序中的 3D 图形生动起来。它提供了 3D 图形和游戏中间件,使你无需专门使用低级OpenGL® ES (OpenGL® for Embedded Systems,例如)进行编码,通过提供导入模型的资源系统、预定义的照明、物理和特效组件。

高通® Vuforia^(TM) SDK 集成了先进的计算机视觉算法,旨在识别和跟踪各种对象,包括校准标记(框架标记)、图像目标和甚至 3D 对象。虽然它对于基于传感器的 AR 不是必需的,但它可以让你方便地实现基于计算机视觉的 AR 应用程序。

谷歌 Android NDK 是用于性能关键型应用程序的工具集。它允许应用程序的部分内容用本地代码语言(C/C++)编写。虽然你不需要用 C 或 C++编码,但 Vuforia^(TM) SDK 需要这个工具集。

当然,你不必局限于特定的 IDE,也可以使用命令行工具。我们在这本书中提供的代码片段并不依赖于特定 IDE 的使用。然而,在本书中,我们将为你提供针对流行的 Eclipse IDE 的设置说明。此外,所有开发工具都可以在 Windows(XP 或更高版本)、Linux 和 Mac OS X(10.7 或更高版本)上使用。

在接下来的页面中,我们将指导你完成 Android 开发者工具包、NDK、JME 和 Vuforia^(TM) SDK 的安装过程。虽然开发工具可以分散在系统中,但我们建议你为开发工具和示例代码使用一个共同的基目录;我们称之为AR4Android(例如,在 Windows 下的C:/AR4Android或在 Linux 或 Mac OS X 下的/opt/AR4Android)。

安装 Android 开发者工具包和 Android NDK

你可以通过以下两个简单步骤安装 ADT Bundle:

  1. developer.android.com/sdk/index.html下载 ADT Bundle。

  2. 下载后,将adt-bundle-<os_platform>.zip解压到AR4Android基础目录。

然后,你可以通过启动AR4Android/adt-bundle-<os_platform>/eclipse/eclipse(.exe)来启动 Eclipse IDE。

提示

请注意,根据你使用的设备,你可能需要安装额外的系统映像(例如,版本 2.3.5 或 4.0.1)。你可以按照以下网站的说明操作:developer.android.com/tools/help/sdk-manager.html

对于 Android NDK(版本 r9 或更高),你可以按照以下类似的步骤操作:

  1. developer.android.com/tools/sdk/ndk/index.html下载。

  2. 下载后,将android-ndk-r<version>Y-<os_platform>.(zip|bz2)解压到AR4Android基础目录。

安装 JMonkeyEngine

JME 是一个基于 Java 的强大 3D 游戏引擎。它自带开发环境(基于 NetBeans 的 JME IDE),主要针对桌面游戏的开发。尽管 JME IDE 也支持部署到 Android 设备,但在本书撰写之时,它还没有集成像 ADT Bundle 中那样的便捷 Android SDK 工具,例如Android 调试桥adb)、Dalvik 调试监控服务器视图DDMS)或 Android 模拟器的集成。因此,我们将不使用 JME IDE,而是将基本库集成到我们在 Eclipse 中的 AR 项目中。获取 JME 库的最简单方法是,从 jmonkeyengine.org/downloads 下载适用于你操作系统的 SDK 并安装到 AR4Android 基础目录(或你自己的开发者目录;只要确保稍后在你的项目中能轻松访问即可)。在本书出版之时,有三个软件包:Windows、GNU/Linux 和 Mac OS X。

提示

你也可以从 updates.jmonkeyengine.org/nightly/3.0/engine/ 获取最新版本。

对于使用 ADT Bundle 进行 AR 开发,你只需要 JME 的 Java 库(.jar)。如果你在 Windows 或 Linux 上工作,可以通过执行以下步骤将它们包含在任何现有的 Eclipse 项目中:

  1. 在 Eclipse 资源管理器中右键点击你的 AR 项目(我们将在下一章创建)或其他任何项目,然后选择构建路径 | 添加外部存档

  2. JAR 选择对话框中,浏览至 AR4Android/jmonkeyplatform/jmonkeyplatform/libs 目录。

  3. 你可以选中 lib 目录中的所有 JAR 文件,然后点击打开

如果你使用的是 Mac OS X,在应用与 Windows 或 Linux 前一节描述的相同步骤之前,应该从 jmonkeyplatform.app 中提取库。要提取库,你需要右键点击 jmonkeyplatform.app 应用并选择显示包内容,你会在 /Applications/jmonkeyplatform.app/Contents/Resources/ 找到库。

请注意,在本书的背景下,我们只使用其中的一部分。在随书附带的 Eclipse 项目中,你会发现必要的 JAR 文件已经位于本地 lib 目录中,其中包含了运行示例所需的 Java 库子集。你还可以在构建路径中引用它们。

注意

使用 JME 与 Android 相关的参考文档可以在 hub.jmonkeyengine.org/wiki/doku.php/jme3:android 找到。

安装 Vuforia^(TM)

Vuforia^(TM) 是一个最先进的计算机视觉识别和自然特征跟踪库。

为了下载和安装 Vuforia^(TM),你首先需要在developer.vuforia.com/user/register进行注册。注册后,你可以从developer.vuforia.com/resources/sdk/android下载 SDK(支持 Windows、Linux 或 Mac OS X)。创建一个名为AR4Android/ThirdParty的文件夹。然后通过选择文件 | 新建 | 项目 ...,创建一个名为ThirdParty的 Eclipse 项目,并将位置选择为文件夹AR4Android/ThirdParty(也见第二章中的创建 Eclipse 项目一节,观察世界)。接着在AR4Android/ThirdParty/vuforia-sdk-android-<VERSION>安装 Vuforia^(TM) SDK。对于第五章,好莱坞级——实物上的虚拟和第六章,互动体验——创建用户体验中的示例,你需要引用这个ThirdParty Eclipse项目。

你应该使用哪些 Android 设备?

你将学习的增强现实应用程序将在各种 Android 智能手机和平板电脑上运行。然而,根据特定的算法,我们将引入一些硬件要求。具体来说,Android 设备需要具备以下特性:

  • 用于本书所有示例的后置摄像头

  • 用于基于传感器的 AR 示例的 GPS 模块

  • 用于基于传感器的 AR 示例的陀螺仪或线性加速度计

在手机上实现增强现实可能会具有挑战性,因为许多集成传感器必须在应用程序运行期间保持活跃,并执行计算密集型算法。因此,我们建议在双核处理器(或多核)上部署以获得最佳的 AR 体验。最早应部署的 Android 版本应为 2.3.3(API 10,姜饼)。这使你的 AR 应用有可能覆盖大约 95%的所有 Android 设备。

注意事项

访问developer.android.com/about/dashboards/index.html获取最新的数据。

请确保按照developer.android.com/tools/device.html的描述为开发设置你的设备。

此外,大多数 AR 应用程序,尤其是基于计算机视觉的应用程序(使用 Vuforia^(TM)),需要足够的处理能力。

总结

在本章中,我们介绍了 AR 的基础背景。我们展示了 AR 的一些主要概念,如感官增强、专用的显示技术、物理和数字信息的实时空间注册以及与内容的互动。

我们还介绍了基于计算机视觉和传感器的增强现实(AR)系统,这是移动设备上架构的两个主要趋势。同时,我们还描述了一个 AR 应用程序的基本软件架构块,并将以此作为本书后续内容的指导。至此,你应该已经安装了接下来章节中将使用的第三方工具。在下一章中,你将开始学习如何查看虚拟世界并使用 JME 实现相机访问。

第二章:观察世界

在本章中,我们将学习如何开发任何移动 AR 应用程序的第一个元素:真实世界的视图。为了理解真实世界视图的概念,我们将看看你安装在手机上的摄像头应用程序。打开你预装在安卓设备上的任何照片拍摄应用程序(摄像头应用),或者你可能从谷歌应用商店下载的应用(如 Camera Zoom FX、Vignette 等)。你在应用程序取景器上看到的是摄像头捕获的实时视频流,并显示在你的屏幕上。

在运行应用程序时移动设备,仿佛你通过设备看到了真实世界。实际上,摄像头就像是设备的眼睛,感知你周围的环境。这个过程也用于移动 AR 开发,以创建真实世界的视图。这是我们在前一章中介绍的透视视频的概念。

显示真实世界需要两个主要步骤:

  • 从摄像头捕获图像(摄像头访问)

  • 使用图形库在屏幕上显示这个图像(在 JME 中显示摄像头)

这个过程通常在一个无限循环中重复,创建了物理世界视图的实时特性。在本章中,我们将讨论如何使用两个不同的图形库实现这两种技术:一个低级别的(Android 库)和一个高级的(JME 3D 场景图库)。虽然 Android 库能让你快速显示摄像头图像,但它并非设计为与你想在视频流上增强的 3D 图形结合使用。因此,你也将使用 JME 库实现摄像头显示。我们还将介绍处理各种 Android 智能手机及其内置摄像头的挑战和提示。

理解摄像头

手机制造商总是在竞相为你的智能手机配备最先进的摄像头传感器,加入更多功能,如更高的分辨率、更好的对比度、更快的视频捕捉、新的自动对焦模式等等。结果是,不同智能手机型号或品牌之间的手机摄像头功能(特性)可能存在显著差异。幸运的是,谷歌 Android API 为底层摄像头硬件提供了一个通用封装,统一了开发者的访问方式:即 Android 摄像头 API。在开发过程中,高效地访问摄像头需要对 API 提供的摄像头功能(参数和函数)有清晰的理解。忽视这一点会导致应用程序运行缓慢或图像像素化,影响你的应用程序用户体验。

摄像头特性

现今智能手机上的相机与数码傻瓜相机有许多共同特性。它们通常支持两种操作模式:静态图像模式(即瞬间捕捉的单个图像),或者视频模式(即连续实时捕捉图像)。

视频和图像模式在功能上有所不同:例如,图像捕捉总是具有比视频更高的分辨率(更多像素)。虽然现代智能手机在静态图像模式下轻松实现 800 万像素,但视频模式限制在 1080p(约 200 万像素)左右。在增强现实(AR)中,我们通常使用较低分辨率如 VGA(640 x 480)的视频模式以提高效率。与标准数码相机不同,我们不将任何内容存储在外部存储卡上;我们只是在屏幕上显示图像。这个模式在 Android API 中有一个特殊的名字:预览模式。

预览模式的一些常见设置(参数)包括:

  • 分辨率:这是捕捉到的图像的大小,可以在你的屏幕上显示。在 Android 相机 API 中也被称为尺寸。分辨率以像素为单位定义图像的宽(x)和高(y)。它们之间的比率称为宽高比,这给出了图像与电视分辨率(如 1:1、4:3 或 16:9)相似程度的感知。

  • 帧率:它定义了图像可以捕捉的速度。这也被称为每秒帧数FPS)。

  • 白平衡:它决定了图像上的白色会是什么样子,主要取决于你的环境光线(例如,户外情况下是日光,家里是白炽灯,工作场所是荧光灯等)。

  • 焦点:它定义了图像中哪部分会显得清晰,哪部分不容易被辨识(失焦)。与其他相机一样,智能手机相机也支持自动对焦模式。

  • 像素格式:捕捉到的图像被转换成特定的图像格式,每个像素的颜色(和亮度)以特定格式存储。像素格式不仅定义了颜色通道的类型(如 RGB 与 YCbCr),还定义了每个分量的存储大小(例如 5 位、8 位或 16 位)。一些流行的像素格式包括 RGB888、RGB565 或 YCbCr422。在下面的图中,你可以看到从左到右常见的相机参数:图像分辨率、捕捉图像流的帧率、相机焦点、存储图像的像素格式以及白平衡:相机特性

与相机工作流程相关的重要设置还有:

  • 播放控制:定义了你可以开始、暂停、停止或获取相机图像内容的时间。

  • 缓冲控制:捕捉到的图像被复制到内存中,以便你的应用程序可以访问。存储这个图像有不同的方法,例如,使用缓冲系统。

正确配置这些设置是 AR 应用的基本要求。尽管流行的相机应用仅使用预览模式来捕捉视频或图像,但预览模式是 AR 中现实世界视图的基础。你需要记住的一些关于配置这些相机参数的事情包括:

  • 分辨率越高,帧率越低,这意味着如果你的应用中图像内容移动不快,它看起来可能会更美观,但运行会更缓慢。相比之下,你可以让应用运行得更快,但图像看起来会变得“块状”(像素化效果)。

  • 如果白平衡设置不当,叠加在视频图像上的数字模型的外观将不匹配,AR 体验将大打折扣。

  • 如果焦点不断变化(自动对焦),你可能无法分析图像的内容,应用的其他部分(如追踪)可能无法正确工作。

  • 移动设备上的相机使用压缩图像格式,并且通常不具备高端桌面网络摄像头相同的性能。当你将视频图像(通常是 RGB565 格式与使用 RGB8888 格式的 3D 渲染内容结合)结合在一起时,你可能会注意到它们之间的颜色差异。

  • 如果你在图像上进行大量处理,那可能会造成应用延迟。此外,如果你的应用同时运行多个进程,将图像捕获过程与其他进程同步是非常重要的。

我们建议你:

  • 获取并测试各种 Android 设备和它们的相机,以了解相机的功能和性能。

  • 在分辨率和帧率之间找到平衡点。桌面 AR 上使用的标准分辨率/帧率组合是 640 x 480,30 fps。将此作为你移动 AR 应用的基础,并在此基础上优化,以获得适用于新型设备的更高质量的 AR 应用。

  • 如果你的 AR 应用仅计划在特定环境中运行,例如白天户外应用,请优化白平衡。

  • 控制焦点一直是 Android 智能手机的限制因素之一(始终开启自动对焦或无法配置)。如果开发的是桌面或室内 AR 应用(近焦)相对于户外 AR 应用(远焦),请优先选择固定焦点,并优化焦点范围。

  • 实验不同的像素格式,以与你的渲染内容达到最佳匹配。

  • 如果目标设备支持,尝试使用先进的缓冲系统。

还有一些通过 API 无法获取的(或在部分手持设备上才能获取的)相机主要特性,在开发 AR 应用时也应当考虑。这些特性包括视场、曝光时间和光圈。

在这里我们只讨论其中之一:视野。视野对应于相机从现实世界中看到的内容,比如你的眼睛可以从左到右、从上到下看到多少(人类的双眼视觉大约是 120 度)。视野以度数测量,不同相机之间差异很大(15 度至 60 度,无变形)。

你的视野越大,捕获的现实世界视野就越广,体验也越好。视野取决于相机的硬件特性(传感器尺寸和焦距)。可以使用额外的工具来估算这个视野;我们稍后会进行探讨。

相机与屏幕特性对比

在你的移动平台上,相机和屏幕特性通常不完全相同。例如,相机图像可能比屏幕分辨率大。屏幕的宽高比也可能与其中一个相机不同。在增强现实(AR)中,这是一个技术挑战,因为你想找到最好的方法将相机图像适配到屏幕上,以创建 AR 显示的感觉。你希望尽可能多地将在相机图像上的信息显示在屏幕上。在电影行业,他们有类似的问题,因为记录的格式可能与播放媒体不同(例如,在 4:3 的移动设备上播放宽银幕电影,在 1080p 的电视屏幕上播放 4K 电影分辨率等)。为了解决这个问题,你可以使用两种全屏方法:拉伸和裁剪,如下图所示:

相机与屏幕特性对比

拉伸会根据屏幕特性调整相机图像,这可能会导致图像原始格式(主要是宽高比)的变形。裁剪会选择图像的一个子区域进行显示,你会丢失一些信息(基本上是将图像放大,直到整个屏幕被填满)。另一种方法是改变图像的缩放比例,使得屏幕和图像的一个尺寸(宽度或高度)相同。这里的缺点是,你将无法全屏显示相机图像(图像边缘将出现黑边)。这些技术都不完美,因此你需要实验哪种方法对你的应用和目标设备更方便。

在 Android 中访问相机

首先,我们将创建一个简单的相机活动,以了解在 Android 中访问相机的原理。尽管有方便的 Android 应用程序可以通过 Android 意图快速抓拍照片或录制视频,但我们将会亲自动手,使用 Android 相机 API 为我们的第一个应用程序获得定制化的相机访问。

我们将一步一步指导你创建你的第一个显示实时相机预览的应用程序。这将包括:

  • 创建一个 Eclipse 项目

  • 在 Android Manifest 文件中请求相关权限

  • 创建 SurfaceView 以捕获相机的预览帧

  • 创建一个显示相机预览帧的活动

  • 设置相机参数

提示

下载示例代码

你可以从你在 www.packtpub.com 的账户下载你所购买的 Packt 书籍的示例代码文件。如果你在别处购买了这本书,可以访问 www.packtpub.com/support 注册,直接通过电子邮件发送文件给你。你还可以在 github.com/arandroidbook/ar4android 找到代码文件。

创建一个 Eclipse 项目

我们的第一步是在 Eclipse 中创建 Android 项目的设置过程。我们将第一个项目命名为 CameraAccessAndroid。请注意,本小节的描述将与其他将在本书中展示的示例类似。

启动你的 Eclipse 项目并转到 文件 | 新建 | Android 应用程序项目。在接下来的配置对话框中,请按照以下截图填写适当的字段:

创建一个 Eclipse 项目

然后,点击另外两个对话框(配置项目 选择到你的项目文件路径,启动器图标),接受默认值。接着,在 创建活动 对话框中,选中 创建活动 复选框和 空白活动 选项。在接下来的 新建空白活动 对话框中,在 活动名称 文本框中填写,例如 CameraAccessAndroidActivity 并将 布局名称 文本框保留为默认值。最后,点击 完成 按钮,你的项目应该会被创建并在项目浏览器中可见。

Android Manifest 中的权限

对于我们将要创建的每个 AR 应用程序,我们将使用相机。使用 Android API,你需要在应用程序的 Android Manifest 声明中明确允许相机访问。在你的 CameraAccessAndroid 项目的顶级文件夹中,以文本视图打开 AndroidManifest.xml 文件。然后添加以下权限:

<uses-permission android:name="android.permission.CAMERA" />

除了此权限,应用程序还需要至少声明使用相机功能:

<uses-feature android:name="android.hardware.camera" />

由于我们希望以全屏模式运行 AR 应用程序(以获得更好的沉浸感),请在活动标签中添加以下选项:

android:theme="@android:style/Theme.NoTitleBar.Fullscreen"

创建一个显示相机的活动

在最基本的形式中,我们的 Activity 类负责设置 Camera 实例。作为一个类成员,你需要声明一个 Camera 类的实例:

public class CameraAccessAndroidActivity extends Activity {
private Camera mCamera;

}

下一步是打开相机。为此,我们定义一个 getCameraInstance() 方法:

public static Camera getCameraInstance() {
  Camera c = null;
  try {
    c = Camera.open(0);
  } catch (Exception e) { ...  }
  return c;
}

open() 调用被 try{}catch{} 块包围非常重要,因为相机可能当前正被其他进程使用而不可用。此方法在 Activity 类的 onResume() 方法中调用:

public void onResume() {
  super.onResume();  
  stopPreview = false;
  mCamera = getCameraInstance();
  ...
}

当你暂停或退出程序时,正确释放摄像头同样重要。否则,如果你打开另一个(或相同的)程序,摄像头将被占用。我们为此定义了一个releaseCamera()方法:

private void releaseCamera() {
  if (mCamera != null) {

    mCamera.release();
    mCamera = null;
  }
}

然后,你可以在Activity类的onPause()方法中调用这个方法。

注意

在某些设备上,打开摄像头可能会很慢。在这种情况下,你可以使用AsyncTask类来缓解这个问题。

设置摄像头参数

现在,你已经有了启动和停止摄像头的的基本工作流程。Android 摄像头 API 还允许你查询和设置本章开始时讨论的各种摄像头参数。特别是,你应该注意不要使用分辨率非常高的图像,因为它们会消耗大量处理能力。对于典型的移动 AR 应用,你不需要高于 640 x 480(VGA)的视频分辨率。

由于摄像头模块可能存在很大差异,不建议硬编码视频分辨率。相反,查询摄像头传感器可用的分辨率,并根据你的应用程序只使用最合适的分辨率,这是一个好的做法(如果支持的话)。

假设你已经在变量mDesiredCameraPreviewWidth中预定义了想要的视频宽度。你可以通过以下方法检查该宽度分辨率(以及相关的视频高度)是否被摄像头支持:

private void initializeCameraParameters() {
  Camera.Parameters parameters = mCamera.getParameters();
  List<Camera.Size> sizes = parameters.getSupportedPreviewSizes();
  int currentWidth = 0;
  int currentHeight = 0;
  boolean foundDesiredWidth = false;
  for(Camera.Size s: sizes) {
    if (s.width == mDesiredCameraPreviewWidth)  {             
      currentWidth = s.width;
      currentHeight = s.height;
      foundDesiredWidth = true;
      break;
    }
  }
  if(foundDesiredWidth) 
    parameters.setPreviewSize( currentWidth, currentHeight );

  mCamera.setParameters(parameters);
}     

mCamera.getParameters()方法用于查询当前的摄像头参数。mCamera.getParameters()getSupportedPreviewSizes()方法返回可用的预览尺寸的子集,而parameters.setPreviewSize方法用于设置新的预览尺寸。最后,你需要调用mCamera.setParameters(parameters)方法,以便实施请求的更改。这个initializeCameraParameters()方法也可以在Activity类的onResume()方法中调用。

创建 SurfaceView

对于你的增强现实应用,你希望将后置摄像头的实时图像流显示在屏幕上。在标准应用中,获取视频和显示视频是两个独立的程序。使用 Android API,你还需要一个单独的 SurfaceView 来显示摄像头流。SurfaceView类是一个专用的绘图区域,你可以将其嵌入到你的应用程序中。

所以,对于我们的示例,我们需要从 Android 的SurfaceView类派生一个新类(我们称之为CameraPreview),并实现SurfaceHolder.Callback接口。这个接口用于响应与表面相关的任何事件,例如表面的创建、更改和销毁。通过Camera类访问移动摄像头。在构造函数中,传递了之前定义的 Android Camera实例:

public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {
  private static final String TAG = "CameraPreview";
  private SurfaceHolder mHolder;
  private Camera mCamera;
  public CameraPreview(Context context, Camera camera) {
    super(context);
    mCamera = camera;
    mHolder = getHolder();
    mHolder.addCallback(this);
    mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
  }

surfaceChanged方法中,你需要处理传递一个初始化的SurfaceHolder实例(这是持有显示表面的实例)并开始相机的预览流,你稍后想在你的应用程序中显示(和处理)。停止相机预览流同样重要:

public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
  if (mHolder.getSurface() == null){
    return;
  }
  try {
    mCamera.stopPreview();
  } catch (Exception e){ ...}
  try {       
    mCamera.setPreviewDisplay(mHolder);
    mCamera.startPreview();
  } catch (Exception e){ ... }
}

继承的方法surfaceCreated()surfaceDestroyed()保持为空。

有了我们定义的CameraPreview类,我们可以在Activity类中声明它:

private CameraPreview mPreview;

然后,在onResume()方法中实例化它:

mPreview = new CameraPreview(this, mCamera);
setContentView(mPreview);

要测试你的应用程序,你可以对你的其他项目做同样的操作:请通过 USB 线将你的测试设备连接到电脑上。在 Eclipse 中,右键点击你的项目文件夹CameraAccessAndroid,在弹出菜单中选择运行方式 | 1 Android 应用程序。现在你应该能在应用程序上传并启动后,在手机屏幕上看到实时的相机视图。

JME 中的实时相机视图

在前面的示例中,你已经了解了如何使用低级图形库(标准的 Android 库)访问 Android 相机。由于我们想要执行增强现实,我们将需要另一种技术将虚拟内容覆盖在视频视图上。有不同的方法可以实现这一点,最好的方法无疑是使用一个公共视图,它将很好地整合虚拟和视频内容。一个强大的技术是使用基于场景图模型的托管 3D 图形库。场景图基本上是一个数据结构,它可以帮助你比在纯 OpenGL®中更容易地构建复杂的 3D 场景,通过逻辑地组织基本构建块,如几何或空间变换。由于你在第一章中安装了 JME,我们将使用这个特定的库,它提供了我们进行 AR 开发所需的所有特性。在本小节中,我们将探讨如何使用 JME 显示视频。与前面的示例不同,相机视图将被整合到 3D 场景图中。为了实现这一点,你需要执行以下步骤:

  1. 创建一个支持 JME 的项目。

  2. 创建一个设置 JME 的活动。

  3. 创建 JME 应用程序,它实际渲染我们的 3D 场景。

要创建支持 JME 的项目,你可以按照第一章中安装 JMonkeyEngine一节的说明进行操作,增强现实概念和工具。我们将创建一个名为CameraAccessJME的新项目。

创建 JME 活动

作为一名 Android 开发者,您知道 Android 活动是创建应用程序的主要入口点。然而,JME 是一个平台无关的游戏引擎,可以在支持 Java 的许多平台上运行。JME 的创建者希望尽可能简单地将现有的(和新的)JME 应用程序集成到 Android 中。因此,他们明确区分了实际渲染场景的 JME 应用程序(也可以在其他平台上使用)和 JME 活动中的 Android 特定部分,以设置环境允许 JME 应用程序运行。他们实现这一目标的方法是使用一个特定的类AndroidHarness,它减轻了开发者正确配置 Android 活动的负担。例如,它将屏幕上的触摸事件映射到 JME 应用程序中的鼠标事件。这种方法中的一个挑战是将 Android 特定的事件转发到 JME 应用程序中,这些事件在其他平台上并不常见。别担心,我们将向您展示如何为相机图像实现这一点。

您要做的第一件事是创建一个派生自AndroidHarness类的 Android 活动,我们将其称为CameraAccessJMEActivity方法。它与CameraAccessAndroidActivity类相似,包含CameraCameraPreview类的实例。不同之处在于,它还将包含您实际的 JME 应用程序实例(将在本章下一节中讨论),负责渲染场景。您尚未提供类的实际实例,只提供了完整的类路径名。在AndroidHarness超类中通过反射技术在运行时构造类的实例:

public CameraAccessJMEActivity() {    
  appClass = "com.ar4android.CameraAccessJME";
}

在运行时,您可以通过将通用的 JME 应用程序类转换为特定的类来访问实际实例,AndroidHarness将其存储在app变量中,例如,通过(com.ar4android.CameraAccessJME) app。

如本章开头所讨论的,相机可以以各种像素格式提供图像。大多数渲染引擎(JME 也不例外)无法处理各种像素格式,但期望某些格式,如 RGB565。RGB565 格式以 5 位存储红色和蓝色分量,以 6 位存储绿色分量,从而在 16 位每像素的情况下显示 65536 种颜色。您可以在initializeCameraParameters方法中通过添加以下代码来检查相机是否支持此格式:

List<Integer> pixelFormats = parameters.getSupportedPreviewFormats();
  for (Integer format : pixelFormats) {
    if (format == ImageFormat.RGB_565) {    
      pixelFormatConversionNeeded = false;
      parameters.setPreviewFormat(format);
      break;
  }
}

在这段代码中,我们查询所有可用的像素格式(遍历parameters.getSupportedPreviewFormats())并在支持的情况下设置 RGB565 模型的像素格式(记住,我们是通过对pixelFormatConversionNeeded标志进行设置来完成这一步的)。

如前所述,与上一个示例不同,我们不会直接渲染SurfaceView类。相反,我们将在每一帧中从摄像头复制预览图像。为此,我们定义了preparePreviewCallbackBuffer()方法,你可以在创建摄像头并设置其参数后,在onResume()方法中调用它。它分配缓冲区以复制摄像头图像并将其转发给 JME:

public void preparePreviewCallbackBuffer() {    

  mPreviewWidth = mCamera.getParameters().getPreviewSize().width;
  mPreviewHeight = mCamera.getParameters().getPreviewSize().height;
  int bufferSizeRGB565 = mPreviewWidth * mPreviewHeight * 2 + 4096;
  mPreviewBufferRGB565 = null;
  mPreviewBufferRGB565 = new byte[bufferSizeRGB565];
  mPreviewByteBufferRGB565 = ByteBuffer.allocateDirect(mPreviewBufferRGB565.length);
  cameraJMEImageRGB565 = new Image(Image.Format.RGB565, mPreviewWidth, mPreviewHeight, mPreviewByteBufferRGB565);
}

如果你的摄像头不支持 RGB565 格式,它可能会以 YCbCr 格式(亮度、蓝色差、红色差)输出帧,这就需要你将其转换为 RGB565 格式。为此,我们将使用一种在 AR 和图像处理中非常常见的色彩空间转换方法。我们在示例项目中提供了一个此方法的实现(yCbCrToRGB565(…))。使用这个方法的基本步骤是创建不同的图像缓冲区,你将在其中复制源图像、中间图像和最终转换后的图像。

因此,在进行转换时,通过在preparePreviewCallbackBuffer()方法中调用摄像头实例的getParameters()方法来查询mPreviewWidthmPreviewHeightbitsPerPixel变量,并确定持有图像数据的字节数组的大小。你将一个 JME 图像(cameraJMEImageRGB565)传递给 JME 应用程序,这个图像是由 Java 的ByteBuffer类构造的,它只是简单地包装了 RGB565 字节数组。

准备好图像缓冲区后,我们现在需要访问实际图像的内容。在 Android 中,你通过实现Camera.PreviewCallback接口来完成这个操作。在这个对象的onPreviewFrame(byte[] data, Camera c)方法中,你可以获取到实际存储为字节数组的摄像头图像:

private final Camera.PreviewCallback mCameraCallback = new Camera.PreviewCallback() {
    public void onPreviewFrame(byte[] data, Camera c) {

      mPreviewByteBufferRGB565.clear();
      if(pixelFormatConversionNeeded) {
        yCbCrToRGB565(data, mPreviewWidth, mPreviewHeight, mPreviewBufferRGB565);
        mPreviewByteBufferRGB565.put(mPreviewBufferRGB565);
      }

      cameraJMEImageRGB565.setData(mPreviewByteBufferRGB565);
      if ((com.ar4android.CameraAccessJME) app != null) {
        ((com.ar4android.CameraAccessJME) app).setTexture(cameraJMEImageRGB565);
      }

    }
  }

CameraAccessJME类的setTexture方法仅仅是将传入的数据复制到一个本地图像对象中。

最后,你需要在CameraPreview类的onSurfaceChanged()方法中注册你的Camera.PreviewCallback接口实现:

mCamera.setPreviewCallback(mCameraPreviewCallback);

注意

一种更快的方法来获取摄像头图像,避免在每一帧都创建新缓冲区,是在之前分配一个缓冲区,并使用mCamera.addCallbackBuffer()mCamera.setPreviewCallbackWithBuffer()方法。请注意,这种方法可能与某些设备不兼容。

创建 JME 应用程序

如前一部分所述,JME 应用程序是实际进行场景渲染的地方。它不应该关注前面描述的 Android 系统的细节。JME 为你提供了一种方便的方法,使用许多默认设置初始化你的应用程序。你需要做的就是从SimpleApplication类继承,在simpleInitApp()中初始化你的自定义变量,并在simpleUpdate()方法中在渲染新帧之前更新它们。为了我们的渲染相机背景的目的,我们将在initVideoBackground方法中创建一个自定义ViewPort(显示窗口内的视图)和一个虚拟Camera(用于渲染观察到的场景)。在 JME 这样的场景图中显示视频的常见方法是将视频图像作为纹理,放置在一个四边形网格上:

public void initVideoBackground(int screenWidth, int screenHeight){
  Quad videoBGQuad = new Quad(1, 1, true);
  mVideoBGGeom = new Geometry("quad", videoBGQuad);
  float newWidth = 1.f * screenWidth / screenHeight;
  mVideoBGGeom.setLocalTranslation(-0.5f * newWidth, -0.5f, 0.f);mVideoBGGeom.setLocalScale(1.f * newWidth, 1.f, 1);
  mvideoBGMat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
  mVideoBGGeom.setMaterial(mvideoBGMat);
  mCameraTexture = new Texture2D();

  Camera videoBGCam = cam.clone();
  videoBGCam.setParallelProjection(true);
  ViewPort videoBGVP = renderManager.createMainView("VideoBGView",
    videoBGCam);
  videoBGVP.attachScene(mVideoBGGeom);
  mSceneInitialized = true;
}

让我们更详细地看看这个为设置视频背景渲染场景图的基本方法。首先,你创建一个四边形形状,并将其分配给一个 JME Geometry对象。为了确保屏幕与相机之间的正确映射,你会根据设备屏幕的尺寸缩放和重新定位几何图形。你为四边形分配一个材质,并为它创建一个纹理。由于我们进行的是 3D 渲染,我们需要定义一个观察这个四边形的相机。由于我们希望相机仅在没有失真的情况下,很好地放置在相机前方的四边形,因此我们创建了一个自定义视口和一个正交相机(这种正交相机没有透视缩短效果)。最后,我们将四边形几何添加到这个视口中。

现在,我们的相机观察的是全屏渲染的带纹理四边形。剩下的就是每次相机有新的视频帧可用时,更新四边形的纹理。我们将在simpleUpdate()方法中这样做,该方法由 JME 渲染引擎定期调用:

public void simpleUpdate(float tpf) {
if(mNewCameraFrameAvailable) {
  mCameraTexture.setImage(mCameraImage);
  mvideoBGMat.setTexture("ColorMap", mCameraTexture)
}

} 

你可能注意到了对mNewCameraFrameAvailable变量的条件测试的使用。由于场景图以其内容的不同刷新率(在现代智能手机上高达 60 fps)进行渲染,而移动相机通常能提供的刷新率(通常是 20-30 fps)不同,我们使用mNewCameraFrameAvailable标志来更新纹理,仅当有新图像可用时。

这就是全部内容。实施这些步骤后,你可以编译并上传你的应用程序,并应得到与下图类似的结果:

创建 JME 应用程序

总结

在本章中,您了解了 Android 相机访问的世界以及如何在 JME 3D 渲染引擎中显示相机图像。您学习了各种相机参数以及为了获得有效的相机访问所做出的妥协(例如,在图像大小和每秒帧数之间)。我们还介绍了在 Android 活动中显示相机视图的最简单方法,但也解释了为什么需要超越这个简单示例,将相机视图和 3D 图形在单一应用程序中集成。最后,我们帮助您实现了渲染相机背景的 JME 应用程序。您在本章中获得的知识是为相机视图叠加第一个 3D 对象的有益基础——这是我们将在下一章讨论的主题。

第三章:叠加现实世界

现在你已经在屏幕上看到了物理世界的视图,我们下一个目标是在其上叠加数字 3D 模型。在增强现实中使用的 3D 叠加与使用 Adobe Photoshop 或类似绘图应用程序的基本 2D 叠加(我们仅调整两个 2D 图层的位置)不同。3D 叠加的概念涉及具有六个自由度(在三维度上进行平移和旋转)的内容管理和渲染,如下图所示:

叠加现实世界

在本章中,我们将引导你了解不同的概念,并为你提供最佳的方式将真实和虚拟内容叠加。我们将依次介绍真实和虚拟相机的概念,如何使用我们的场景图引擎进行叠加,以及如何创建高质量的叠加。首先,让我们讨论 3D 世界和虚拟相机。

3D 渲染的构建块

表示和渲染虚拟 3D 内容的方式与你在物理世界中用数码相机拍照的方式相同。如果你给朋友或风景拍照,你首先会用肉眼检查你的拍摄对象,然后通过相机的取景器观察,最后才会拍照。这三种不同的步骤与虚拟 3D 内容相同。你没有用物理相机拍照,而是使用虚拟相机来渲染场景。你的虚拟相机可以看作是真实相机的数字表示,并且可以以类似的方式进行配置;你可以定位相机,改变其视野等。对于虚拟 3D 内容,你操作的是几何 3D 场景的数字表示,我们简单地称之为你的虚拟 3D 场景或虚拟世界。

使用 3D 计算机图形渲染场景的三个基本步骤如下所示,包括:

  • 配置你的虚拟 3D 场景(对象的位置和外观)

  • 配置你的虚拟相机

  • 使用虚拟相机渲染 3D 场景

3D 渲染的构建块

由于我们进行增强现实(AR)的实时渲染,你将需要循环执行这些步骤;在每一帧(通常为 20-30 FPS)都可以移动对象或相机。

在场景中定位物体或相机时,我们需要一种表示物体位置(以及方向)相对于彼此的方法。为此,我们通常使用基于几何数学模型的场景空间表示。最常见的方法是使用欧几里得几何坐标系。一个坐标系定义了一种引用空间中物体(或点)的方法,使用数值表示来定义这个位置(坐标)。你的场景中的所有内容都可以在坐标系中定义,而坐标系之间可以通过变换相互关联。

以下是最常见的坐标系:

  • 世界坐标系:这是你引用所有事物的地面。

  • 相机坐标系:它放置在世界坐标系中,用于从这个特定的视角渲染你的场景。有时也被称为视点坐标系。

  • 局部坐标系:例如,一个物体坐标系,用于表示一个物体的 3D 点。传统上,你可以使用物体的(几何)中心来定义你的局部坐标系。

3D 渲染的构建块

提示

坐标系的方向有左右手两种约定:在两种约定中,X 轴都在右侧,Y 轴向上。在右手坐标系中,Z 轴指向你;而在左手坐标系中,Z 轴远离你。

另一个常见的坐标系在这里没有说明,是图像坐标系。如果你编辑图片,你可能对这个很熟悉。它定义了从参考原点(通常是图像的左上角或左下角)开始的每个像素的位置。当你进行 3D 图形渲染时,概念是相同的。现在我们将关注虚拟相机的特性。

实际相机与虚拟相机。

用于 3D 图形渲染的虚拟相机通常由两组主要参数表示:外参内参。外参定义了相机在虚拟世界中的位置(从世界坐标系到相机坐标系的变换以及反之)。内参定义了相机的投影属性,包括其视场(焦距)、图像中心和倾斜。这两种参数可以用不同的数据结构表示,最常见的是矩阵。

如果您开发的是一款 3D 移动游戏,通常可以自由配置摄像头;您可以将摄像头放置在在地形上奔跑的 3D 角色上方(外置)或设置一个大的视场角以获得角色和地形的大范围视图(内置)。然而,当您进行增强现实(AR)时,选择会受到手机中真实摄像头属性的制约。在 AR 中,我们希望虚拟摄像头的属性与真实摄像头相匹配:视场角和摄像头位置。这是 AR 的一个重要元素,我们将在本章中进一步解释如何实现它。

摄像头参数(内在方向)

我们将在后续章节中探讨虚拟摄像头的 extrinsic 参数;它们用于增强现实中的 3D 注册。对于我们的 3D 叠加,我们现在将探讨摄像头的 intrinsic 参数。

有不同的计算模型可以表示虚拟摄像头(及其参数),我们将使用最流行的模型:针孔摄像头模型。针孔摄像头模型是对物理摄像头的简化模型,在这里您认为只有一个点(针孔)光线进入摄像头图像。有了这个假设,计算机视觉研究者简化了内在参数的描述如下:

  • (物理或虚拟)镜头的焦距:这和摄像头中心的尺寸一起决定了摄像头的视场角FOV)——也称为视角。FOV 是摄像头可以看到的对象空间的范围,用弧度(或度)表示。它可以确定摄像头传感器的水平、垂直和斜向的视角。

  • 图像中心(主点):这适用于传感器从中心位置的任何位移。

  • 倾斜因子:这用于非方形像素。

注意

在非移动摄像头中,您还应该考虑镜头畸变,比如径向畸变和切向畸变。它们可以通过先进的软件算法进行建模和校正。手机摄像头上的镜头畸变通常在硬件中进行校正。

有了这些概念,现在让我们进行一些实践操作。

使用场景图将 3D 模型叠加到摄像头视图中

在上一章中,你学习了如何设置单个视口和相机来渲染视频背景。虚拟相机决定了你的 3D 图形如何投射到 2D 图像平面上,而视口定义了将此图像平面映射到应用程序实际运行窗口的一部分(或智能手机的全屏,如果应用以全屏模式运行)。它决定了应用程序窗口中渲染图形的部分。多个视口可以堆叠并覆盖相同的或不同的屏幕区域,如下所示。对于基本的 AR 应用,你通常有两个视口。一个与渲染背景视频的相机相关联,另一个与渲染 3D 对象的相机一起使用。通常,这些视口覆盖整个屏幕。

使用场景图将 3D 模型覆盖到相机视图上

视口大小不是以像素为单位定义的,而是无单位的,从 0 到 1 定义宽度和高度,以便能够轻松适应窗口大小的变化。一次只将一个相机关联到一个视口。

请记住,对于视频背景,我们使用了正交相机以避免视频图像的透视缩短。然而,这种透视对于正确视觉感受 3D 对象至关重要。正交(平行)投影(在以下图的左侧)和透视投影(在以下图的右侧)决定了如何将 3D 体积投射到 2D 图像平面上,如下所示:

使用场景图将 3D 模型覆盖到相机视图上

JME 使用右手坐标系(OpenGL®约定,x 在右侧,y 向上,z 朝向你)。你当然希望随着相机靠近 3D 对象,它们看起来会更大,远离时则更小。那么我们该如何操作呢?没错,你只需添加第二个相机——这次是透视相机——以及一个关联的视口,这个视口也覆盖整个应用程序窗口。

在本章相关的SuperimposeJME项目中,我们同样拥有一个 Android 活动(SuperimposeJMEActivity.java)和一个 JME 应用程序类(SuperimposeJME.java)。应用程序无需对我们的上一个项目进行重大更改;你只需继承 JME 的SimpleApplication类即可。在其simpleInitApp()启动方法中,我们现在明确区分场景几何(视频背景:initVideoBackground();3D 前景场景:initForegroundScene())及其相关相机和视口的初始化:

private float mForegroundCamFOVY = 30;
…
public void simpleInitApp() {
…
initVideoBackground(settings.getWidth(), settings.getHeight());
initForegroundScene();	
initBackgroundCamera();
initForegroundCamera(mForegroundCamFOVY);
…
}

请注意,初始化摄像头和视口(viewport)的顺序很重要。只有当我们首先添加视频背景的摄像头和视口(initBackgroundCamera()),然后添加前景摄像头和视口(initForegroundCamera()),才能确保我们的 3D 对象渲染在视频背景之上;否则,你只能看到视频背景。

现在,我们使用initForegroundScene()将你的第一个 3D 模型添加到场景中。JME 的一个便捷特性是它支持加载外部资源——例如 Wavefront 文件(.obj)或 Ogre3D 文件(.mesh.xml/.scene)——包括动画。我们将加载并动画化一个绿色忍者,这是 JME 自带的一个默认资源。

private AnimControl mAniControl;
private AnimChannel mAniChannel;
…
public void initForegroundScene() {
Spatial ninja = assetManager.loadModel("Models/Ninja/Ninja.mesh.xml");
ninja.scale(0.025f, 0.025f, 0.025f);
ninja.rotate(0.0f, -3.0f, 0.0f);
ninja.setLocalTranslation(0.0f, -2.5f, 0.0f);
rootNode.attachChild(ninja);

DirectionalLight sun = new DirectionalLight();
sun.setDirection(new Vector3f(-0.1f, -0.7f, -1.0f));
rootNode.addLight(sun);

mAniControl = ninja.getControl(AnimControl.class);
mAniControl.addListener(this);
mAniChannel = mAniControl.createChannel();
mAniChannel.setAnim("Walk");
mAniChannel.setLoopMode(LoopMode.Loop);
mAniChannel.setSpeed(1f);
}

在这个方法中,你相对于项目的root/asset文件夹加载一个模型。如果你想加载其他模型,也请将它们放在这个asset文件夹中。你对模型进行缩放、平移和定位,然后将其添加到根场景图节点。为了使模型可见,你还需要添加一个从顶部前方照射到模型的方向光(你可以尝试不添加光看看结果)。对于动画,访问模型中存储的“Walk”动画序列。为此,你的类需要实现AnimEventListener接口,并使用AnimControl实例来访问该模型中的动画序列。最后,你将“Walk”序列分配给一个AnimChannel实例,告诉它循环动画,并设置动画速度。

很好,现在你已经加载了你的第一个 3D 模型,但你仍然需要在屏幕上显示它。

接下来在initForegroundCamera(fovY)中你会这样做。它负责为你的 3D 模型设置透视摄像头和相关视口。由于透视摄像头由其能看到的物体空间范围(即视场角 FOV)来定义,我们将存储在mForegroundCamFOVY中的垂直视角传递给该方法。然后它将包含 3D 模型的场景根节点附着到前景视口。

public void initForegroundCamera(float fovY) {
  Camera fgCam = new Camera(settings.getWidth(), settings.getHeight());
  fgCam.setLocation(new Vector3f(0f, 0f, 10f));
  fgCam.setAxes(new Vector3f(-1f,0f,0f), new Vector3f(0f,1f,0f), new Vector3f(0f,0f,-1f));
  fgCam.setFrustumPerspective(fovY,  settings.getWidth()/settings.getHeight(), 1, 1000);

  ViewPort fgVP = renderManager.createMainView("ForegroundView", fgCam);
  fgVP.attachScene(rootNode);
  fgVP.setBackgroundColor(ColorRGBA.Blue);
  fgVP.setClearFlags(false, true, false);
}

尽管你可以直接复制一些默认摄像头的标准参数(类似于我们对视频背景摄像头所做的),但了解实际上需要执行哪些步骤来初始化新摄像头是很有好处的。在用窗口宽度和高度初始化透视摄像头后,你需要设置摄像头的位置(setLocation())和旋转(setAxes())。JME 使用右手坐标系,我们的摄像头配置为沿着负 z 轴看向原点,正如前一个图所示。此外,我们将传递给setFrustumPerspective()的垂直视角设置为 30 度,这大约对应于人类看起来自然的视野(与非常宽或非常窄的视野相比)。

之后,我们像为视频背景相机那样设置视口。此外,我们告诉视口删除其深度缓冲区,但保留颜色和模板缓冲区,使用setClearFlags(false, true, false)。我们这样做是为了确保我们的 3D 模型始终在持有视频纹理的四边形前面渲染,无论它们在实际对象空间中是位于四边形前面还是后面(请注意,我们所有的图形对象都引用在同一个世界坐标系中)。我们不清理颜色缓冲区,否则,先前渲染到颜色缓冲区的视频背景的颜色值将被删除,我们将只能看到这个视口(蓝色)的背景颜色。如果你现在运行你的应用程序,你应该能够看到你的视频背景前有一个行走的忍者,如下面这个相当酷的截图所示:

使用场景图将 3D 模型叠加到相机视图中

改进叠加效果

在上一节中,你创建了一个透视相机,该相机以 30 度的垂直视场角渲染你的模型。然而,为了增加场景的真实感,实际上你希望尽可能匹配虚拟相机和物理相机的视场角。在像你的手机相机这样的通用成像系统中,这个视场角取决于相机传感器的尺寸和光学器件的焦距。焦距是衡量相机镜头将入射的平行光线弯曲到聚焦(在传感器平面上)的强度,基本上就是传感器平面与镜头的光学元件之间的距离。

视场(FOV)可以通过公式 α = 2 arctan d/2f 计算,其中 d 是相机传感器的(垂直、水平或对角线)范围,而 2 是焦距。听起来很简单,对吧?这里只有一个小挑战。你通常并不知道手机相机的(物理)传感器尺寸或焦距。前面公式的好处在于,你不需要知道传感器的物理范围或其焦距,但可以用任意坐标系(如像素)来计算。至于传感器尺寸,我们可以轻松使用相机的图像分辨率,这你在第二章《观察世界》中已经学会了如何查询。

最棘手的部分是估计你的相机焦距。有一些工具可以帮助你通过一组从已知物体拍摄的图片来完成这个任务;它们被称为相机重定工具(或几何相机校准工具)。我们将向你展示如何使用一个名为 GML C++相机校准工具箱的工具来实现这一点,你可以从graphics.cs.msu.ru/en/node/909下载它。

安装工具后,在您的安卓手机上打开标准相机应用。在静态图像设置下选择与您在 JME 应用中使用的相机分辨率,例如,640 x 480,如下截图所示:

改善叠加

打印出 GML 校准模式子目录中的checkerboard_8x5_A4.pdf文件,大小为 A4。用相机应用从不同的视角至少拍摄四张照片(6 到 8 张会更好)。尽量避免非常尖锐的角度,并尽量使棋盘格在图像中最大化。示例图像如下所示:

改善叠加

完成后,将图像传输到计算机上的文件夹(例如,AR4Android\calibration-images)。之后,在计算机上启动 GML 相机校准应用并创建一个新项目。在新建项目对话框中输入黑白方格的正确数量(例如,58),如下截图所示:

改善叠加

实际测量方格大小也至关重要,因为您的打印机可能会将 PDF 缩放到纸张大小。然后,点击确定,开始添加您刚才拍摄的照片(导航至对象检测 | 添加图片)。添加所有图片后,导航至对象检测 | 检测所有,然后是校准 | 校准。如果校准成功,您应该在结果标签中看到相机参数。我们主要对焦距部分感兴趣。虽然 x 轴和 y 轴有两个不同的焦距,但使用第一个即可。在三星 Galaxy SII 拍摄的样本图像案例中,得到的焦距为 522 像素。

然后,您可以将这个数字与您的垂直图像分辨率一起代入前面的公式,得到视角的垂直角度(以弧度为单位)。由于 JME 需要以度为单位的角度,您只需应用这个因子:180/PI进行转换。如果您也在使用三星 Galaxy SII,应该得到大约 50 度的垂直视角,这相当于 35 毫米胶片格式中的大约 28 毫米焦距(广角镜头)。如果您将这个值代入mForegroundCamFOVY变量并上传应用,行走忍者应该会显示得更小,如下所示。当然,您可以通过调整相机位置再次增加其大小。

请注意,您无法在 JME 中模拟物理相机的所有参数。例如,您不能轻松地将物理相机的主点与 JME 相机设置对齐。

注意

JME 也不支持直接的镜头畸变校正。您可以通过高级镜头校正技术来考虑这些影响,例如,这里介绍的技术:paulbourke.net/miscellaneous/lenscorrection/

改善叠加效果

总结

在本章中,我们向您介绍了 3D 渲染的概念、3D 虚拟相机以及增强现实中的 3D 叠加观念。我们阐述了虚拟相机的定义及其特性,并描述了内在相机参数对于精确增强现实的重要性。您还有机会开发了您的第一个 3D 叠加,并对移动相机进行校准以提高逼真度。然而,当您移动手机时,视频背景会发生变化,而 3D 模型保持原位。在下一章中,我们将解决增强现实应用程序的一个基本组成部分:注册。

第四章:在世界中的定位

在上一章中,您学习了如何将数字内容叠加在物理世界的视图上。然而,如果您带着设备四处移动,将其指向其他地方,虚拟内容将始终停留在屏幕上的同一位置。这并不是 AR 中真正发生的情况。虚拟内容应该相对于物理世界保持在同一位置(你可以围绕它移动),而不是固定在屏幕上。

在本章中,我们将探讨如何实现数字内容与物理空间之间的动态注册。如果我们每次都更新应用程序中移动对象的位置,我们将创造出数字内容与物理世界紧密相连的感觉。跟随场景中移动元素的位置可以定义为追踪,这正是我们将在本章中使用和实现的内容。我们将使用基于传感器的 AR 来更新数字内容与物理空间之间的注册。由于这些传感器通常质量不佳,我们将向您展示如何使用一种名为传感器融合的技术来改善从它们获得的测量结果。为了更具实用性,我们将向您展示如何开发一个简单的原型,这是最常见基于全局追踪的 AR 应用程序的基本构建块:一个 AR 浏览器(例如 Junaio、Layar 或 Wikitude)。

知道你的位置——处理 GPS

在本节中,我们将探讨移动 AR 和基于传感器的 AR(见第一章,增强现实概念和工具)的一种主要方法,该方法使用全局追踪。全局追踪指的是在全球参考框架(世界坐标系)中的追踪,可以涵盖整个地球。我们首先会看看位置方面,然后是手机上用于 AR 的位置传感器。我们将学习如何使用 Android API 从中获取信息,并将其位置信息整合到 JME 中。

GPS 和 GNSS

因此,我们需要追踪用户的位置,以了解他在现实世界中的位置。当我们说追踪用户时,手持 AR 应用程序实际上追踪的是设备的位置。

注意

用户追踪与设备追踪

要创建一个完全沉浸式的 AR 应用程序,理想情况下,您需要知道设备的位置,用户相对于设备的位置,以及用户眼睛相对于身体的位置。这种方法在过去已经被探索过,尤其是在头戴式显示器中。为此,您需要追踪用户头部、身体,并拥有它们之间的所有静态变换(校准)。在移动 AR 中,我们离这还很远;也许将来,用户会佩戴或穿着配备传感器的眼镜或衣物,这将允许创建更精确的注册和追踪。

那么我们如何在一个全球坐标系统中追踪设备的位置呢?你或者你的朋友们肯定使用过 GPS 进行汽车导航、跑步或远足。GPS 是一种用于全球追踪的常见技术,参照地球坐标系统,如下面的图所示:

GPS 和 GNSS

现在大多数手机都配备了 GPS,因此它似乎成为 AR 中全球追踪的理想技术。GPS 是美国版的全球导航卫星系统GNSS)。这项技术依赖于一系列地理参考卫星,它们可以使用地理坐标在全球任何地方给出你的位置。GPS 并不是唯一的 GNSS,一个俄罗斯版本(GLONASS)目前也在运行中,而一个欧洲版本(Galileo)将在 2020 年左右生效。然而,GPS 是目前在移动设备上支持最广泛的 GNSS,因此在本书的其余部分,当我们讨论使用 GNSS 进行追踪时,我们将使用这个术语。

对于依赖 GPS 的常见 AR 应用,你需要考虑两件事:数字内容的位置和设备的位置。如果它们都在参照地球的同一坐标系统中定义,你将能够了解它们相对于彼此的位置(见下面图中的椭圆形图案)。有了这些知识,你可以在用户坐标系统中建模 3D 内容的位置,并通过 GPS 传感器的每次位置更新来更新它。因此,如果你向一个物体移动(从下到上),物体将显得更近(在图像中也更大),复现你在现实世界中的行为。

GPS 和 GNSS

我们在使用这项技术时遇到的一个小问题是与 GPS 使用的坐标系统有关。使用纬度和经度坐标(基本 GPS 提供的数据)并不是使用 AR 的最适应表示。当我们进行 3D 图形处理时,我们习惯于使用欧几里得坐标系统来定位数字内容;使用笛卡尔坐标系统来定义位置,即 X、Y 和 Z 坐标。因此,我们需要通过将这些 GPS 坐标转换成更适应的形式来解决这一问题。

JME 和 GPS——追踪你的设备位置

谷歌的 Android API 通过位置管理器服务提供了对 GPS 的访问。位置管理器可以提供你 GPS 数据,但它也可以使用网络(例如 Wi-Fi 和手机网络)来精确你的位置,并给出一个大致的估计。在 Android 术语中,这被称为位置提供者。要使用位置管理器,你需要应用基于监听器类的 Android 通知的标准 Android 机制;在这种情况下是LocationListener

所以打开与本章关联的LocationAccessJME项目,这是SuperimposeJME项目(第三章,覆盖世界)的修改版本。

首先,我们需要修改 Android 清单,以允许访问 GPS 传感器。关于 GPS 有不同的质量模式(估计位置的质量),我们将授权所有这些模式。因此,请将这两个权限添加到您的AndroidManifest.xml文件中:

  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

该项目与之前一样,有一个 JME 类(LocationAccessJME),一个活动类(LocationAccessJMEActivity),以及CameraPreview。我们需要做的是创建一个LocationListener类和一个LocationManager类,并将它们添加到我们的LocationAccessJMEActivity类中:

private LocationManager locationManager;

LocationListener类中,我们需要重写不同的回调函数:

private LocationListener locListener= new LocationListener() {
  …

  @Override
  public void onLocationChanged(Location location) {
    Log.d(TAG, "onLocation: " + location.toString());
    if ((com.ar4android.LocationAccessJME) app != null) {
      ((com.ar4android.LocationAccessJME) app)
        .setUserLocation(xyzposition);
    }
  }
  …
}

onLocationChanged回调是当用户位置发生变化时的调用;位置参数包含测量的纬度和经度(以度为单位)。为了将转换后的数据传递给我们的 JME,我们将使用与前一个相同的原则:使用位置作为参数调用 JME 类中的方法。因此,每次用户位置更新时都会调用setUserLocation,新的值将对 JME 类可用。

接下来,我们需要访问定位管理服务,并使用requestLocationUpdates函数向其注册我们的位置监听器:

  public void onResume() {
    super.onResume();
    …
    locationManager = (LocationManager)getSystemService(LOCATION_SERVICE);
    locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 500, 0, locListener);
  }

requestLocationUpdates方法的参数包括我们想要使用的定位提供者类型(GPS 或网络),更新频率(以毫秒为单位),以及作为我们监听器的位置变化阈值(以米为单位)。

在 JME 方面,我们需要为我们的LocationAccessJME类定义两个新变量:

  //the User position which serves as intermediate storage place for the Android
  //Location listener position update
  private Vector3f mUserPosition;

  //A flag indicating if a new Location is available
  private boolean mNewUserPositionAvailable =false;

我们还需要定义我们的setUserLocation函数,该函数从LocationListener中的回调中被调用:

    public void setUserLocation(Vector3f location) {
    if (!mSceneInitialized) {
      return;
    }
    WSG84toECEF(location,mUserPosition);
    //update your POI location in reference to the user position
    ….
    mNewUserPositionAvailable =true;
  }

在这个函数中,我们需要将摄像头的位置从纬度/经度格式转换成笛卡尔坐标系。实现这一转换有不同的技术;我们将使用 SatSleuth 网站上的转换算法(www.satsleuth.com/GPS_ECEF_Datum_transformation.htm),将我们的数据转换为ECEF地球中心,地球固定)格式。现在我们的 JME 类中有了以 ECEF 格式可用的mUserPosition。每次用户位置发生变化时,都会调用onLocationChange方法和setUserLocation,我们将得到mUserPosition的更新值。现在的问题是我们如何将这个变量用于我们的场景图并与地理参考数字内容(例如,POI)相关联?

使用的办法是从您的当前位置本地引用内容。为此,我们需要使用一个附加的坐标系:东-北-上(ENU)坐标系。对于您拥有的每个数据(例如,从您位置出发 5 公里半径内的若干个兴趣点),您需要从当前的位置计算其位置。下面让我们看看如何对我们的忍者模型进行这样的操作,如下面的代码所示:

    Vector3f ECEFNinja=new Vector3f();
    Vector3f ENUNinja=new Vector3f();
    WSG84toECEF(locationNinja,ECEFNinja);
    ECEFtoENU(location,mUserPosition,ECEFNinja,ENUNinja);
    mNinjaPosition.set(ENUNinja.x,0,ENUNinja.y);

忍者位置的纬度-经度格式(locationNinja)也被转换成地心地固坐标系(ECEFNinja)格式。从那里,使用当前的 GPS 位置(以纬度-经度格式和地心地固坐标系格式,即位置mUserPosition),我们计算忍者在本地方坐标系(ENUNinja)中的位置。每次用户移动时,他的 GPS 位置将被更新,转换为地心地固坐标系格式,并且将更新内容的本地位置,这将触发不同的渲染。就是这样!我们已经实现了基于 GPS 的追踪。不同坐标系之间的关系说明如图所示:

JME 和 GPS——追踪您设备的位置

剩下的唯一部分就是使用新的本地位置来更新模型的位置。我们可以通过在simpleUpdate函数中添加以下代码来实现这一点:

    if (mNewUserPositionAvailable) {
      Log.d(TAG,"update user location");
      ninja.setLocalTranslation(mNinjaPosition.x+0.0f,mNinjaPosition.y-2.5f,mNinjaPosition.z+0.0f);
      mNewUserPositionAvailable=false;
    }

在一个真正的 AR 应用中,您可能会在 GPS 坐标系中以您当前位置为中心放置一些 3D 内容,例如在纽约第五街放置一个虚拟的忍者,或者在巴黎的埃菲尔铁塔前。

由于我们希望确保无论您在何处进行测试和阅读本书(从纽约到廷巴克图),都可以独立运行这个示例。出于教育目的,我们将稍微修改这个演示。我们将要做的是在setUserLocation中添加以下调用,在您的初始 GPS 位置 10 米处添加忍者模型(即第一次 GPS 更新时):

    if (firstTimeLocation) {
      //put it at 10 meters
      locationNinja.setLatitude(location.getLatitude()+0.0001);
      locationNinja.setLongitude(location.getLongitude());
      firstTimeLocation=false;
    }

测试时间:将应用程序部署到您的移动设备上,并前往一个可以获得良好 GPS 信号的位置(您应该能够看到天空,并避免在非常多云的日子进行测试)。不要忘记在设备上激活 GPS。启动应用程序,四处移动,您应该能看到忍者位置的变化。恭喜您,您已经开发出了您的第一个增强现实(AR)应用的追踪实例!

了解您的注视方向——处理惯性传感器

通过前面的示例和访问 GPS 位置,我们现在可以更新用户的位置,并能够在增强现实中进行基本的追踪。然而,这种追踪只考虑了用户的位置,而没有考虑他的方向。例如,如果用户旋转手机,将不会发生任何变化,只有在他移动时变化才有效。为此,我们需要能够检测用户方向的变化;这时就需要用到惯性传感器。惯性传感器可以用来检测方向的变化。

理解传感器

在当前一代的手机中,有三种类型的传感器可用于定位:

  • 加速度计:这些传感器检测手机的正加速度,也称为g 力加速度。手机通常配备有多轴模型,可以提供三个轴上的加速度:手机的俯仰、翻滚和倾斜。它们是手机上最早使用的传感器,用于基于传感器的游戏,生产成本低廉。通过加速度计和一点基础物理知识,你可以计算出手机的方向。然而,它们相当不准确,测量数据非常嘈杂(可能导致你的 AR 应用出现抖动)。

  • 磁力计:它们可以检测地球的磁场,就像指南针一样。理想情况下,你可以通过测量三维磁场来获取北方方向,从而知道手机指向哪里。磁力计的挑战在于,它们很容易受到周围金属物体的影响,比如用户手腕上的手表,进而指示出错误的北方方向。

  • 陀螺仪:它们通过科里奥利效应测量角速度。手机中使用的是多轴微型机械系统MEMS),采用振动机制。它们比之前的传感器更准确,但主要问题是漂移:测量精度随时间降低;短时间后,测量开始变得非常不准确。

你可以将它们的测量值结合起来,以解决它们的局限性,我们将在本章后面看到。在手机使用之前,惯性传感器已经被广泛使用,最著名的应用是在飞机上测量其方向或速度,用作惯性测量单元IMU)。由于制造商总是试图降低成本,不同移动设备之间的传感器质量差异很大。噪声、漂移和不准确的影响将导致你的 AR 内容在你没有移动手机的情况下跳跃或移动,或者可能导致内容定位在错误的方向。如果你想要商业部署你的应用,确保你测试了它们的一系列性能。

JME 中的传感器

在 Google Android API 中,通过SensorManager访问传感器,并使用SensorListener获取测量值。SensorManager不仅提供对惯性传感器的访问,还提供对所有手机上传感器的访问。在 Android API 中,传感器分为三类:运动传感器、环境传感器和位置传感器。加速度计和陀螺仪被定义为运动传感器;磁力计被定义为位置传感器。Android API 还实现了一些软件传感器,这些传感器结合了这些不同传感器的值(可能包括位置传感器)来提供运动和方向信息。可用的五个运动传感器包括:

  • TYPE_ACCELEROMETER

  • TYPE_GRAVITY

  • TYPE_GYROSCOPE

  • TYPE_LINEAR_ACCELERATION

  • TYPE_ROTATION_VECTOR

请参考谷歌开发者安卓网站developer.android.com/guide/topics/sensors/sensors_overview.html,了解更多关于它们各自特性的信息。现在让我们打开SensorAccessJME项目。像之前一样,我们定义了一个SensorManager类,并为每个这些运动传感器添加一个Sensor类:

  private SensorManager sensorManager;
  Sensor rotationVectorSensor;
  Sensor gyroscopeSensor;
  Sensor magneticFieldSensor;
  Sensor accelSensor;
  Sensor linearAccelSensor; 

我们还需要定义SensorListener,它将处理来自运动传感器的任何传感器变化:

private SensorEventListener sensorListener = new SensorEventListener() {    
    …
@Override
public void onSensorChanged(SensorEvent event) {
  switch(event.sensor.getType()) {
      …
      case Sensor.TYPE_ROTATION_VECTOR:
  float[] rotationVector = {event.values[0],event.values[1], event.values[2]};
  float[] quaternion = {0.f,0.f,0.f,0.f};
  sensorManager.getQuaternionFromVector(quaternion,rotationVector);
  float qw = quaternion[0]; float qx = quaternion[1];
  float qy = quaternion[2];float qz = quaternion[3];
    double headingQ = Math.atan2(2*qy*qw-2*qx*qz , 1 - 2*qy*qy - 2*qz*qz);
  double pitchQ = Math.asin(2*qx*qy + 2*qz*qw); 
  double rollQ = Math.atan2(2*qx*qw-2*qy*qz ,1 - 2*qx*qx - 2*qz*qz);
  if ((com.ar4android.SensorAccessJME) app != null) {
  ((com.ar4android.SensorAccessJME) app).setRotation((float)pitchQ, (float)rollQ, (float)headingQ);
  }
  }
}
};

注意事项

旋转变化也可以仅使用四元数来处理,但我们明确使用欧拉角以便更直观地理解。优先使用四元数,因为它们组合旋转更容易,并且不会出现“万向节死锁”。

我们的监听器重写了两个回调:onAccuracyChangedonSensorChanged。当我们注册到SensorManager的传感器有任何变化时,将调用onSensorChanged通道。这里我们通过查询event.sensor.getType()来确定是哪种类型的传感器发生了变化。对于每种类型的传感器,你可以使用生成的测量值来计算设备的新方向。在这个例子中,我们只向你展示如何使用TYPE_ROTATION_VECTOR传感器的值(软件传感器)。这个传感器提供的方向需要映射到与虚拟相机坐标帧相匹配。我们将欧拉角(偏航,俯仰和翻滚)传递给 JME 应用程序,在 JME 应用程序的setRotation函数中实现这一点(欧拉角只是方向的另一种表示,可以从传感器事件中提供的四元数和轴角表示计算得出)。

现在,有了我们的SensorListener,我们需要查询SensorManager以获取传感器服务并初始化我们的传感器。在你的onCreate方法中添加:

  // sensor setup
  sensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
  List<Sensor> deviceSensors = sensorManager.getSensorList(Sensor.TYPE_ALL);
  Log.d(TAG, "Integrated sensors:");
  for(int i = 0; i < deviceSensors.size(); ++i ) {
    Sensor curSensor = deviceSensors.get(i);
    Log.d(TAG, curSensor.getName() + "\t" + curSensor.getType() + "\t" + curSensor.getMinDelay() / 1000.0f);
  }
initSensors();

在获取传感器服务访问权限后,我们查询所有可用传感器的列表,并在我们的 logcat 上显示结果。为了初始化传感器,我们调用我们的initSensors方法,并定义如下:

    protected void initSensors(){
      //look specifically for the gyroscope first and then for the rotation_vector_sensor (underlying sensors vary from platform to platform)
    gyroscopeSensor = initSingleSensor(Sensor.TYPE_GYROSCOPE, "TYPE_GYROSCOPE");
    rotationVectorSensor = initSingleSensor(Sensor.TYPE_ROTATION_VECTOR, "TYPE_ROTATION_VECTOR");
    accelSensor = initSingleSensor(Sensor.TYPE_ACCELEROMETER, "TYPE_ACCELEROMETER");
      linearAccelSensor = initSingleSensor(Sensor.TYPE_LINEAR_ACCELERATION, "TYPE_LINEAR_ACCELERATION");
    magneticFieldSensor = initSingleSensor(Sensor.TYPE_MAGNETIC_FIELD, "TYPE_MAGNETIC_FIELD");
    }

initSingleSensor函数将创建一个Sensor实例,并注册我们之前创建的监听器,传递特定类型的传感器参数:

    protected Sensor initSingleSensor( int type, String name ){
    Sensor newSensor = sensorManager.getDefaultSensor(type);
    if(newSensor != null){
      if(sensorManager.registerListener(sensorListener, newSensor, SensorManager.SENSOR_DELAY_GAME)) {
        Log.i(TAG, name + " successfully registered default");
      } else {
        Log.e(TAG, name + " not registered default");
      }
    } …
    return newSensor;
    }

当我们退出应用程序时,我们不应该忘记注销监听器,因此按照以下方式修改你的onStop方法:

    public void onStop() {
      super.onStop();
      sensorManager.unregisterListener(sensorListener);
    }

因此,我们现在在Activity中设置好了。在我们的SensorAccessJME类中,我们添加以下变量:

  private Quaternion mRotXYZQ;
  private Quaternion mInitialCamRotation;
  private Quaternion mCurrentCamRotation; 

变量mInitialCamRotation保存初始摄像头方向,mRotXYZQ保存映射到摄像头坐标系的传感器方向,mCurrentCamRotation存储最终摄像头旋转,它是由mInitialCamRotationmRotXYZQ相乘得到的。setRotation函数从 Android 活动中获取传感器值并将它们映射到摄像头坐标系。最后,它将当前旋转值与初始摄像头方向相乘。

  public void setRotation(float pitch, float roll, float heading) {
    if (!mSceneInitialized) {
      return;
    }
    mRotXYZQ.fromAngles(pitch , roll - FastMath.HALF_PI, 0);
    mCurrentCamRotation = mInitialCamRotation.mult(mRotXYZQ);
    mNewCamRotationAvailable = true;

作为最后一步,我们需要将这个旋转值用于我们的虚拟摄像头,就像我们在 GPS 示例中所做的那样。在simpleUpdate中,你现在需要添加:

    if (mNewCamRotationAvailable) {
      fgCam.setAxes(mCurrentCamRotation);
      mNewCamRotationAvailable = false;
    }

现在,我们准备运行应用程序。重要的是要考虑设备的自然方向,它定义了运动传感器的坐标系,并不是所有设备都相同。如果你的设备默认是纵向模式,而你将其改为横向模式,坐标系将会旋转。在我们的示例中,我们明确将设备方向设置为横向。使用此默认方向模式将你的应用程序部署到设备上。你可能需要旋转设备,以便在屏幕上看到忍者的移动,如下面的截图所示:

JME 中的传感器JME 中的传感器

改进方向追踪——处理传感器融合

基于传感器的追踪的一个限制是传感器本身。正如我们之前所介绍的,一些传感器不准确、有噪声或存在漂移。一种补偿它们各自问题的技术是将它们的值结合起来,以提高你可以获得的整体旋转。这种技术称为传感器融合。融合传感器有不同的方法,我们将使用Paul Lawitzki提出的方法,并提供一个在 MIT 许可下的源代码,可访问www.thousand-thoughts.com/2012/03/android-sensor-fusion-tutorial/。在本节中,我们将简要解释这项技术是如何工作的,以及如何将传感器融合集成到我们的 JME AR 应用程序中。

传感器融合简述

Paul Lawitzki提出的融合算法将加速度计、磁力计和陀螺仪传感器数据融合在一起。类似于 Android API 中的软件传感器处理方式,首先将加速度计和磁力计融合在一起,以获得绝对方向(磁力计作为指南针,为你提供真正的北方向)。为了补偿两者的噪声和不准确,使用陀螺仪。陀螺仪精确但随时间漂移,在系统中以高频率使用;加速度计和磁力计则考虑更长的周期。以下是算法的概述:

传感器融合简述

你可以在Paul Lawitzki的网页上找到更多关于算法(补充滤波器)细节的信息。

JME 中的传感器融合

打开SensorFusionJME项目。传感器融合使用了一定数量的内部变量,你在SensorFusionJMEActivity的开始部分声明这些变量。

// angular speeds from gyro
private float[] gyro = new float[3]; …

还要添加算法使用的不同子程序的代码:

  • calculateAccMagOrientation:从加速度计和磁力计的测量值计算方向角度

  • getRotationVectorFromGyro:从陀螺仪角速度测量计算旋转矢量

  • gyroFunction:将基于陀螺仪的方向写入gyroOrientation

  • 两个矩阵变换函数getRotationMatrixFromOrientationmatrixMultiplication

处理的主要部分在calculatedFusedOrientationTask函数中完成。这个函数作为TimerTask的一部分生成新的融合方向,TimerTask是一个可以在特定时间安排的任务。在这个函数的末尾,我们将生成的数据传递给我们的 JME 类:

  if ((com.ar4android.SensorFusionJME) app != null) {
        ((com.ar4android.SensorFusionJME) app).setRotationFused((float)(fusedOrientation[2]), (float)(-fusedOrientation[0]), (float)(fusedOrientation[1]));
      }
  }

传递给我们的 JME 活动桥接函数(setRotationFused)的参数是在欧拉角格式中定义的融合方向。

我们还需要修改我们的onSensorChanged回调,以调用calculatedFusedOrientationTask使用的子程序:

public void onSensorChanged(SensorEvent event) {
  switch(event.sensor.getType()) {
  case Sensor.TYPE_ACCELEROMETER:
    System.arraycopy(event.values, 0, accel, 0, 3);
    calculateAccMagOrientation();
    break;
  case Sensor.TYPE_MAGNETIC_FIELD:  
    System.arraycopy(event.values, 0, magnet, 0, 3);
    break;
  case Sensor.TYPE_GYROSCOPE:
    gyroFunction(event)
    break;
}

对于我们的活动类,最后的更改是指定一个定时器的任务,指定计划速率以及首次执行前的延迟。我们在调用initSensors之后,在onCreate方法中添加这个:

fuseTimer.scheduleAtFixedRate(new calculateFusedOrientationTask(), 1000, TIME_CONSTANT);

在 JME 方面,我们定义了一个新的桥接函数用于更新旋转(再次将传感器方向转换为虚拟相机的适当方向):

public void setRotationFused(float pitch, float roll, float heading) {
  if (!mSceneInitialized) {
    return;
  } // pitch: cams x axis roll: cams y axisheading: cams z axis
  mRotXYZQ.fromAngles(pitch + FastMath.HALF_PI , roll - FastMath.HALF_PI, 0);
  mCurrentCamRotationFused = mInitialCamRotation.mult(mRotXYZQ);
  mNewUserRotationFusedAvailable = true;
}

最后,我们在simpleUpdate中与setRotation一样使用这个函数,通过fgCam.setAxes(mCurrentCamRotationFused)更新相机方向。你现在可以部署应用程序并在你的设备上查看结果。

如果你将LocationAccessJMESensorAccessJME示例结合起来,你现在将获得完整的 6 自由度(6DOF)跟踪,这是基于经典传感器增强现实应用的基础。

为你的增强现实浏览器获取内容——使用 Google Places API

在知道如何获取你的 GPS 位置和手机的方向之后,你现在可以准备将优秀的内容集成到相机的实时视图中。如果能够物理探索你周围的兴趣点,如地标和商店,岂不是很酷?现在我们将向你展示如何集成流行的基于位置的服务,如 Google Places API,以实现这一点。为了成功集成到你的应用程序中,你需要执行以下步骤:

  • 查询你当前位置周围的兴趣点(POIs)

  • 解析结果并提取属于 POIs 的信息。

  • 在你的增强现实视图中可视化信息

在开始之前,你必须确保你的应用程序有一个有效的 API 密钥。为此,你需要一个 Google 账户。你可以通过使用你的 Google 账户登录code.google.com/apis/console来获取。

为了测试你的应用程序,你可以使用默认项目API Project,或者创建一个新的。要创建新的 API 密钥,你需要:

  1. 点击左侧菜单中的服务链接。

  2. 激活 Places API 状态开关。

  3. 通过点击左侧菜单中的API 访问菜单项,并查看简单 API 访问区域来获取你的密钥。

你可以在LocationAccessJME项目中将密钥存储在String mPlacesKey = "<YOUR API KEY HERE>"变量中。

接下来,我们将向你展示如何查询设备位置周围的 POI,并获得一些基本信息,例如它们的名称和位置。将这些信息集成到 AR 视图中的原则与JME 和 GPS——追踪你的设备位置一节中描述的原则相同。

查询你当前位置周围的 POI

在本章前面,你已经学习了如何获取你在世界上的当前位置(纬度和经度)。你现在可以使用这些信息来获取你周围的 POI 位置。Google Places API 允许你通过 HTTP 请求查询用户周边的地标和商家,并以 JSON 或 XML 字符串的形式返回结果。所有查询都将指向以maps.googleapis.com/maps/api/place/开头的 URL。

尽管你可以在网页浏览器中轻松地进行查询,但你会希望在你的 Android 应用程序内部发送请求并处理响应。由于调用 URL 并等待响应可能需要花费数秒钟,因此你需要以不阻塞主程序执行的方式来实现这种请求-响应处理。下面我们将展示如何使用线程来实现这一点。

在你的LocationAccessJME项目中,你定义了一些新的成员变量,它们负责与 Google Places API 的交互。具体来说,你创建了一个HttpClient来发送请求,以及一个List<POI> mPOIs列表,用于存储关于 POIs 的最重要信息。POI类是一个简单的帮助类,用于存储 Google Places 引用字符串(在 Google Places 数据库中的唯一标识符,POI 名称,纬度和经度):

private class POI {
  public String placesReference;
  public String name;
  public Location location;
…
}

当然,你可以轻松地扩展这个类以保存其他信息,例如街道地址或图片 URL。为了查询 POI,你调用了sendPlacesQuery函数。我们在程序启动时进行调用,但你可以很容易地在固定时间间隔内进行(例如,当用户移动一定距离时)或明确地在按钮点击时进行。

public void sendPlacesQuery(final Location location,  final Handler guiHandler) throws Exception  {
Thread t = new Thread() {
public void run() {
  Looper.prepare();
  BufferedReader in = null;
  try {
    String url = "https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=" + location.getLatitude() + "," + location.getLongitude() + "&radius=" +  mPlacesRadius + "&sensor=true&key=" + mPlacesKey;
    HttpConnectionParams.setConnectionTimeout(mHttpClient.getParams(), 10000);
    HttpResponse response;
    HttpGet get = new HttpGet(url);
    response = mHttpClient.execute(get);
    Message toGUI = guiHandler.obtainMessage();
…
guiHandler.sendMessage(toGUI);
…

在此方法中,我们为每次对 Google Places 服务的查询创建一个新线程。这对于不阻塞主程序的执行非常重要。Places API 的响应应该是一个 JSON 字符串,我们将它传递给主线程中的Handler实例来解析 JSON 结果,接下来我们将讨论这一点。

解析 Google Places API 的结果

Google Places 以轻量级的 JSON 格式(XML 是另一种选择)返回结果。您可以使用作为标准 Android 包提供的org.json库方便地解析这些结果。

您查询的典型 JSON 结果将如下所示:

{
   …
   "results" : [
      {
         "geometry" : {
            "location" : {
               "lat" : 47.07010720,
               "lng" : 15.45455070
            },
   …
         },
         "name" : "Sankt Leonhard",
         "reference" : "CpQBiQAAADXt6JM47sunYZ8vZvt0GViZDLICZi2JLRdfhHGbtK-ekFMjkaceN6GmECaynOnR69buuDZ6t-PKow-J98l2tFyg3T50P0Fr39DRV3YQMpqW6YGhu5sAzArNzipS2tUY0ocoMNHoNSGPbuuYIDX5QURVgncFQ5K8eQL8OkPST78A_lKTN7icaKQV7HvvHkEQJBIQrx2r8IxIYuaVhL1mOZOsKBoUQjlsuuhqa1k7OCtxThYqVgfGUGw",
         …
      },
…
}

在我们的处理程序placesPOIQueryHandlerhandleMessage中,我们将解析这个 JSON 字符串到一个 POI 列表,然后可以在您的 AR 视图中进行可视化:

public void handleMessage(Message msg) {
  try {
    JSONObject response = new JSONObject(msg.obj.toString());
    JSONArray results = response.getJSONArray("results");
    for(int i = 0; i < results.length(); ++i) {
      JSONObject curResult = results.getJSONObject(i);
      String poiName = curResult.getString("name");
      String poiReference = curResult.getString("reference");
      double lat = curResult.getJSONObject("geometry").getJSONObject("location").getDouble("lat");
      double lng = curResult.getJSONObject("geometry").getJSONObject("location").getDouble("lng");
      Location refLoc = new Location(LocationManager.GPS_PROVIDER);
      refLoc.setLatitude(lat);
      refLoc.setLongitude(lng);
      mPOIs.add(new POI(poiReference, poiName, refLoc));
      …
    }
  …
  }
} 

就这样。现在您已经有了基本的 POI 信息,并且有了纬度和经度信息,您可以在 JME 中轻松实例化新的 3D 对象,并将它们相对于相机位置正确地定位,就像您对忍者所做的那样。您还可以查询有关 POI 的更多详细信息,或者根据各种标准对它们进行过滤。有关 Google Places API 的更多信息,请访问developers.google.com/places/documentation/

提示

如果您想在 3D 场景中包含文本,我们建议避免使用 3D 文本对象,因为它们会导致需要渲染的额外多边形数量增多。您可以使用位图文本替代,将其渲染为可以在网格上生成的纹理。

总结

在本章中,我们向您介绍了移动 AR 的第一种流行方法:基于 GPS 和传感器的增强现实。我们介绍了跟踪设备在全球参考框架中的位置的基本构建块,动态确定设备方向,提高方向跟踪的鲁棒性,并最终使用流行的 Google Places API 获取关于用户周围 POI 的信息,然后可以将这些信息集成到 AR 视图中。

在下一章中,我们将向您介绍实现移动 AR 的第二种流行方式:基于计算机视觉的增强现实。

第五章:与好莱坞相同——物理对象上的虚拟效果

在前一章中,你已经学习了实现基于 GPS 和传感器的 AR 应用程序的基本构建块。如果你尝试了我们提供的不同示例,你可能会注意到将数字对象放置在真实空间中的感觉(注册)是可行的,但可能会变得粗糙且不稳定。这主要是由于智能手机或平板电脑中使用的传感器(如 GPS、加速度计等)的准确性问题,以及这些技术的特性(例如,陀螺仪漂移、GPS 对卫星可见性的依赖等)。在本章中,我们将介绍一种更健壮的解决方案,这是支持移动 AR 的第二种主要方法:基于计算机视觉的 AR

基于计算机视觉的 AR 不依赖于任何外部传感器,而是使用摄像头图像的内容来支持跟踪,这是通过不同算法流程的分析。使用基于计算机视觉的 AR,你可以在数字和物理世界之间获得更好的注册效果,尽管在处理上的成本会稍微高一些。

可能你甚至已经不知不觉中见过基于计算机视觉的注册效果。如果你去看一个充满电影特效的大片动作电影,有时你会注意到一些数字内容已经覆盖在物理录制场景上(例如,假的爆炸、假的背景和假的奔跑角色)。与 AR 一样,电影行业也必须处理数字和物理内容之间的注册问题,依靠分析录制的图像来恢复跟踪和相机信息(例如,使用匹配移动技术)。然而,与增强现实相比,它是离线完成的,不是实时完成,通常依赖于重型工作站进行注册和视觉整合。

在本章中,我们将向你介绍不同类型的基于计算机视觉的 AR 跟踪。我们还将为你描述一个广泛使用且高质量的移动 AR 跟踪库——高通公司®的Vuforia^(TM)的集成。使用这个库,我们将能够实现我们的第一个基于计算机视觉的 AR 应用程序。

介绍基于计算机视觉的跟踪和 Vuforia^(TM)

迄今为止,你一直将手机的摄像头专门用于渲染真实世界的视图,作为你模型的背景。基于计算机视觉的 AR 更进了一步,它处理每一帧图像,寻找摄像头图像中熟悉的模式(或图像特征)。

在典型的基于计算机视觉的 AR 应用中,平面对象如帧标记自然特征追踪目标被用来在局部坐标系中定位摄像头(请参阅第三章,覆盖世界显示三个最常见的坐标系统的图)。这与基于传感器的 AR 中使用的全球坐标系(地球)相对立,但允许在此局部坐标框架内更精确和稳定地覆盖虚拟内容。与之前类似,获取追踪信息允许我们更新 3D 图形渲染引擎中虚拟摄像头的相关信息,并自动为我们提供注册。

选择物理对象

为了成功实现基于计算机视觉的增强现实(AR),你需要了解哪些物理对象可以用来追踪摄像头。目前主要有两种方法可以实现这一点:帧标记(Fiducials)和平面纹理对象作为自然特征追踪目标,如下所示。在下一节中,我们将讨论这两种方法。

选择物理对象

理解帧标记

在移动增强现实技术的早期,使用计算效率高的算法至关重要。传统上,计算机视觉算法要求较高,因为它们通常依赖于图像分析、复杂的几何算法和数学变换,所有这些操作需要在每一帧内完成(为了保持 30 赫兹的恒定帧率,你只有 33 毫秒的时间)。因此,基于计算机视觉的 AR 的最初方法之一是使用相对简单的对象类型,这些对象可以用计算要求较低的算法检测,例如 Fiducial 标记。这些标记通常只在灰度级别定义,简化了在传统物理世界中的分析和识别(类似于 3D 中的二维码)。

下图展示了一个典型的检测这类标记的算法流程,接下来将对其进行简要说明:

理解帧标记

获取的摄像机图像转换为灰度图像后,将应用阈值,即灰度级别被转换为纯黑白图像。下一步是矩形检测,在简化后的图像中搜索边缘,然后通过检测封闭轮廓的过程,可能是平行四边形形状。进一步的步骤是为了确保检测到的轮廓确实是一个平行四边形(即它恰好有四个点以及几条平行线)。一旦确认形状,就会分析标记的内容。在模式检查步骤中提取标记边框内的(二进制)模式以识别标记。这对于能够在不同的标记上叠加不同的虚拟内容非常重要。对于帧标记,使用一个简单的位编码,支持 512 种不同的组合(因此也支持 512 个不同的标记)。

在最后一步中,通过姿态估计步骤计算姿态(即摄像机在标记局部坐标系统中的平移和旋转,反之亦然)。

注意

姿态计算,在其最简单的形式中是homography(两个平面上点之间的映射),可以与内在参数一起使用来恢复摄像机的平移和旋转。

在实际应用中,这不是一次性的计算,而是一个迭代过程,初始姿态会经过多次细化以获得更准确的结果。为了可靠地估计摄像机姿态,至少需要让系统知道标记的一边(宽度或高度)的长度;这通常在加载标记描述时的配置步骤中完成。否则,系统可能无法可靠地判断一个小的标记是近还是大的标记是远(由于透视投影的影响)。

理解自然特征跟踪目标

尽管帧标记可以有效地用于许多应用中跟踪摄像机姿态,但你可能希望用不那么显眼的物体进行跟踪。通过使用更高级(但也计算成本更高)的算法,你可以实现这一点。自然特征跟踪的一般思想是使用目标上的多个(理论上只需三个,实际上则需要数十个或数百个)局部点来计算摄像机姿态。挑战在于这些点必须是可靠的,能够健壮地检测并跟踪。这是通过先进的计算机视觉算法来检测和描述兴趣点(或特征点)的局部邻域来实现的。兴趣点具有清晰的细节(如角落),例如,使用梯度方向,这适用于由黄色十字标记的特征点。圆形或直线没有清晰的细节,不适合作为兴趣点:

理解自然特征跟踪目标

在纹理丰富的图像上可以找到许多特征点(比如本章中使用的街道图像):

理解自然特征跟踪目标

请注意,在颜色均匀区域或边缘柔和的图像上(如蓝天或一些计算机图形渲染的图片),特征点无法被很好地识别。

Vuforia^(TM)架构

Vuforia^(TM)是由高通公司®分发的增强现实库。该库在非商业或商业项目中免费使用。库支持帧标记和自然特征目标跟踪以及多目标,这是多个目标的组合。库还具备基本的渲染功能(视频背景和 OpenGL® 3D 渲染)、线性代数(矩阵/向量变换)以及交互能力(虚拟按钮)。实际上,该库在 iOS 和 Android 平台上都可以使用,并且在配备高通®芯片组的移动设备上性能有所提升。以下图展示了库架构的概览:

VuforiaTM 架构

从客户端视角来看(如前图左边的应用程序框),架构为开发者提供了一个状态对象,其中包含有关已识别目标以及相机内容的信息。这里我们不会详细介绍,因为他们的网站上有一系列示例,以及完整的文档和一个活跃的论坛,请访问developer.vuforia.com/。你需要知道的是,该库使用Android NDK进行集成,因为它是用 C++开发的。

这主要是因为使用 C++进行图像分析或计算机视觉的高性能计算收益,而不是用 Java(并发技术也采用相同的方法)。这对于我们来说是一个缺点(因为我们只使用 JME 和 Java),但对于你在应用程序中获得性能来说是一个收益。

要使用这个库,你通常需要遵循以下三个步骤:

  • 训练并创建你的目标或标记

  • 在你的应用程序中集成库

  • 部署你的应用程序

在下一节中,我们将介绍如何创建和训练你的目标。

配置 Vuforia^(TM)以识别对象

要使用带有自然特征跟踪目标的 Vuforia^(TM)工具包,首先你需要创建它们。在库的最新版本(2.0)中,你可以在应用程序运行时(在线)自动创建你的目标,或者在部署应用程序之前(离线)预先定义它们。我们将向你展示如何离线创建。首先访问 Vuforia^(TM)开发者网站developer.vuforia.com

你需要做的第一件事是登录到网站,以访问创建你目标的工具。点击右上角,如果你之前没有做过,请注册。登录后,你可以点击目标管理器,这是创建目标的培训计划。目标管理器以数据库的形式组织(可以对应你的项目),对于数据库,你可以创建一个目标列表,如下截图所示:

配置 VuforiaTM 以识别物体

让我们创建第一个数据库。点击创建数据库,并输入VuforiaJME。你的数据库应该会出现在设备数据库列表中。选择它进入下一页:

配置 VuforiaTM 以识别物体

点击添加新目标以创建第一个目标。会出现一个对话框,包含不同文本字段以填写,如下截图所示:

配置 VuforiaTM 以识别物体

首先你需要为你的目标选择一个名字;在我们的例子中,我们将其称为VuforiaJMETarget。Vuforia^(TM)允许你创建以下不同类型的目标:

  • 单张图片:你只创建一个平面表面,并且只使用一张图片。目标通常用于在页面、杂志的一部分等上打印。

  • 立方体:你定义多个表面(带有多张图片),将用于追踪一个 3D 立方体。这可以用于游戏、包装等。

  • 长方体:这是立方体类型的变化,具有非正方形面的平行六面体。

选择单张图片目标类型。目标尺寸为你的标记定义了一个相对比例。单位没有定义,因为它对应于你的虚拟对象的大小。一个很好的建议是考虑所有尺寸都是以厘米或毫米为单位,这通常是你的物理标记的大小(例如,打印在 A4 或信纸上)。在我们的例子中,我们以厘米为单位输入尺寸。最后,你需要选择一个将用于目标的图像。例如,你可以选择stones.jpg图像,这是 Vuforia^(TM)示例发行版中提供的(在 Vuforia^(TM)网站上的ImageTargets示例的媒体目录中)。为验证你的配置,点击添加,然后等待图像处理。处理完成后,你应该会看到一个如下所示的屏幕:

配置 VuforiaTM 以识别物体

星级会告诉你追踪目标的品质如何。这个例子有五颗星,意味着它将非常好用。你可以在 Vuforia^(TM)网站上获取更多信息,了解如何为追踪目标创建一个好的图像:developer.vuforia.com/resources/dev-guide/natural-features-and-rating

现在的最后一步是导出已创建的目标。因此,选择目标(勾选VuforiaJMETarget旁边的框),然后点击下载选择的目标。在出现的对话框中,选择SDK作为导出,VuforiaJME作为我们的数据库名称,然后保存。

配置 VuforiaTM 以识别对象

解压你压缩的文件。你会看到两个文件:一个.dat文件和一个.xml文件。这两个文件都用于在运行时操作 Vuforia^(TM)追踪。.dat文件指定了你的图像中的特征点,而.xml文件是一个配置文件。有时你可能想要更改标记的大小或进行一些基本编辑,而不必重新启动或进行训练;你可以直接在 XML 文件上进行修改。现在我们已经准备好实现我们的第一个 Vuforia^(TM)项目的目标了!

将它们组合在一起——Vuforia^(TM)与 JME

在本节中,我们将向你展示如何将 Vuforia^(TM)与 JME 集成。我们将使用自然特征追踪目标来实现这一目的。因此,在 Eclipse 中打开VuforiaJME项目以开始。正如你已经可以观察到的,与我们的前一个项目相比,有两个主要变化:

  • 相机预览类已移除

  • 项目根目录中有一个名为jni的新目录。

第一次更改是由于 Vuforia(TM)管理相机的方式。Vuforia(TM)使用自己的相机句柄和集成在库中的相机预览。因此,我们需要通过 Vuforia^(TM)库查询视频图像,以便在我们的场景图中显示(使用与第二章相同的原理,观看世界)。

jni文件夹包含 C++源代码,这是 Vuforia^(TM)所需的。为了将 Vuforia^(TM)与 JME 集成,我们需要互操作 Vuforia^(TM)的低级别部分(C++)和高级别部分(Java)。这意味着我们将需要编译 C++和 Java 代码并在它们之间传输数据。如果你做到了,你将需要在继续之前下载并安装 Android NDK(如第一章所述,增强现实概念和工具)。

C++集成

C++层基于 Vuforia^(TM)网站上提供的ImageTargets示例的修改版本。jni文件夹包含以下文件:

  • MathUtils.cppMathUtils.h:用于数学计算的实用功能函数

  • VuforiaNative.cpp:这是与我们的 Java 层交互的主要 C++类

  • Android.mkApplication.mk:这些包含编译配置文件

打开Android.mk文件,检查到你的 Vuforia^(TM)安装路径在QCAR_DIR目录中是否正确。使用相对路径使其跨平台(在 MacOS 上使用 android ndk r9 或更高版本,绝对路径将与当前目录拼接,导致不正确的目录路径)。

现在打开VuforiNative.cpp文件。文件中定义了很多函数,但只有三个与我们有关系:

  • Java_com_ar4android_VuforiaJMEActivity_loadTrackerData(JNIEnv *, jobject): 这是用于加载我们特定目标(在上一节中创建)的函数

  • virtual void QCAR_onUpdate(QCAR::State& state): 这是查询相机图像并将其传递给 Java 层的函数

  • Java_com_ar4android_VuforiaJME_updateTracking(JNIEnv *env, jobject obj): 这个函数用于查询目标的位置并将其传递给 Java 层

第一步将是在我们的应用程序中使用特定目标以及第一个函数。因此,将VuforiaJME.datVuforiaJME.xml文件复制并粘贴到你的资产目录中(应该已经有两个目标配置)。Vuforia^(TM)根据 XML 配置文件配置将使用的目标。loadTrackerData首先访问TrackerManagerimageTracker(用于非自然特征的追踪器):

JNIEXPORT int JNICALL
Java_com_ar4android_VuforiaJMEActivity_loadTrackerData(JNIEnv *, jobject)
{
    LOG("Java_com_ar4android_VuforiaJMEActivity_ImageTargets_loadTrackerData");

    // Get the image tracker:
    QCAR::TrackerManager& trackerManager = QCAR::TrackerManager::getInstance();
    QCAR::ImageTracker* imageTracker = static_cast<QCAR::ImageTracker*>(trackerManager.getTracker(QCAR::Tracker::IMAGE_TRACKER));
    if (imageTracker == NULL)
    {
        LOG("Failed to load tracking data set because the ImageTracker has not been initialized.");
        return 0;
    }

下一步是创建一个特定的目标,比如实例化一个数据集。在这个例子中,创建了一个名为dataSetStonesAndChips的数据集:

    // Create the data sets:
    dataSetStonesAndChips = imageTracker->createDataSet();
    if (dataSetStonesAndChips == 0)
    {
        LOG("Failed to create a new tracking data.");
        return 0;
    }

在创建的实例中加载目标的配置后,这里是我们设置 VuforiaJME 目标的地方:

    // Load the data sets:
    if (!dataSetStonesAndChips->load("VuforiaJME.xml", QCAR::DataSet::STORAGE_APPRESOURCE))
    {
        LOG("Failed to load data set.");
        return 0;
    }

我们可以通过调用activateDataSet函数来激活数据集。如果你不激活数据集,目标将在追踪器中加载和初始化,但在激活之前不会被追踪:

    // Activate the data set:
    if (!imageTracker->activateDataSet(dataSetStonesAndChips))
    {
        LOG("Failed to activate data set.");
        return 0;
    }

    LOG("Successfully loaded and activated data set.");
    return 1;
}

一旦我们初始化了目标,就需要使用 Vuforia^(TM)获取现实世界的真实视图。这个概念与我们之前看到的相同:在 JME 类中使用视频背景相机并使用图像更新它。然而,在这里,图像不是来自 Java 的Camera.PreviewCallback,而是来自 Vuforia^(TM)。在 Vuforia^(TM)中获取视频图像的最佳位置是在QCAR_onUpdate函数中。这个函数在追踪器更新后立即被调用。可以通过查询 Vuforia^(TM)的状态对象的帧来获取图像,使用getFrame()。一个帧可能包含多个图像,因为相机图像有不同的格式(例如,YUV、RGB888、GREYSCALE、RGB565 等)。在之前的例子中,我们在 JME 类中使用了 RGB565 格式。这里我们也将这样做。所以我们的类将从这里开始。

class ImageTargets_UpdateCallback : public QCAR::UpdateCallback
{   
    virtual void QCAR_onUpdate(QCAR::State& state)
    {
       //inspired from:
       //https://developer.vuforia.com/forum/faq/android-how-can-i-access-camera-image

 QCAR::Image *imageRGB565 = NULL;
        QCAR::Frame frame = state.getFrame();

        for (int i = 0; i < frame.getNumImages(); ++i) {
              const QCAR::Image *image = frame.getImage(i);
              if (image->getFormat() == QCAR::RGB565) {
                  imageRGB565 = (QCAR::Image*)image;

                  break;
              }
        }

该函数解析帧中的图像列表并获取RGB565图像。一旦我们得到这个图像,我们需要将其传递给Java 层。为此,你可以使用 JNI 函数:

        if (imageRGB565) {
            JNIEnv* env = 0;

            if ((javaVM != 0) && (activityObj != 0) && (javaVM->GetEnv((void**)&env, JNI_VERSION_1_4) == JNI_OK)) {

                const short* pixels = (const short*) imageRGB565->getPixels();
                int width = imageRGB565->getWidth();
                int height = imageRGB565->getHeight();
                int numPixels = width * height;

                jbyteArray pixelArray = env->NewByteArray(numPixels * 2);
                env->SetByteArrayRegion(pixelArray, 0, numPixels * 2, (const jbyte*) pixels);
                jclass javaClass = env->GetObjectClass(activityObj);
                jmethodID method = env-> GetMethodID(javaClass, "setRGB565CameraImage", "([BII)V");
                env->CallVoidMethod(activityObj, method, pixelArray, width, height);

                env->DeleteLocalRef(pixelArray);

            }
        }

};

在这个例子中,我们获取关于图像大小以及图像原始数据的指针。我们使用名为setRGB565CameraImage的 JNI 函数,该函数在我们的Java Activity类中定义。我们从 C++中调用这个函数,并传入图像内容(pixelArray)作为图像的widthheight。因此,每次追踪器更新时,我们都会获取新的摄像头图像,并通过调用setRGB565CameraImage函数将其发送到 Java 层。JNI 机制非常有用,你可以使用它来传递任何数据,从复杂的计算过程回到你的 Java 类(例如,物理,数值模拟等)。

下一步是从追踪中获取目标的位置。我们将在updateTracking函数中执行此操作。像之前一样,我们从 Vuforia^(TM)获取 State 对象的实例。State 对象包含TrackableResults,这是视频图像中识别的目标列表(在这里被识别为目标及其位置):

JNIEXPORT void JNICALL
Java_com_ar4android_VuforiaJME_updateTracking(JNIEnv *env, jobject obj)
{
    //LOG("Java_com_ar4android_VuforiaJMEActivity_GLRenderer_renderFrame");

    //Get the state from QCAR and mark the beginning of a rendering section
    QCAR::State state = QCAR::Renderer::getInstance().begin();

    // Did we find any trackables this frame?
    for(int tIdx = 0; tIdx < state.getNumTrackableResults(); tIdx++)
    {
        // Get the trackable:
        const QCAR::TrackableResult* result = state.getTrackableResult(tIdx);

在我们的例子中,只有一个目标被激活,所以如果我们得到一个结果,它显然将是我们的标记。然后我们可以直接查询它的位置信息。如果你有多个激活的标记,你将需要通过调用result->getTrackable()从结果中获取信息,以确定哪个是哪个。

通过调用result->getPose()来查询trackable的位置,这将返回一个定义线性变换的矩阵。这个变换可以给出标记相对于摄像头位置的位置。Vuforia^(TM)使用的是计算机视觉坐标系(x 向左,y 向下,z 远离你),这与 JME 不同,因此我们稍后需要进行一些转换。现在,我们首先要做的是反转变换,以得到相对于标记的摄像头位置;这将使标记成为我们虚拟内容的参考坐标系。所以你将进行以下一些基本的数学运算:

        QCAR::Matrix44F modelViewMatrix = QCAR::Tool::convertPose2GLMatrix(result->getPose());

        QCAR::Matrix44F inverseMV = MathUtil::Matrix44FInverse(modelViewMatrix);
        QCAR::Matrix44F invTranspMV = MathUtil::Matrix44FTranspose(inverseMV);

        float cam_x = invTranspMV.data[12];
        float cam_y = invTranspMV.data[13];
        float cam_z = invTranspMV.data[14];

        float cam_right_x = invTranspMV.data[0];
        float cam_right_y = invTranspMV.data[1];
        float cam_right_z = invTranspMV.data[2];
        float cam_up_x = invTranspMV.data[4];
        float cam_up_y = invTranspMV.data[5];
        float cam_up_z = invTranspMV.data[6];
        float cam_dir_x = invTranspMV.data[8];
        float cam_dir_y = invTranspMV.data[9];
        float cam_dir_z = invTranspMV.data[10];

现在我们有了摄像头的位置(cam_x,y,z)以及摄像头的方向(cam_right_/cam_up_/cam_dir_x,y,z)。

最后一步是将这些信息传递到 Java 层。我们将再次使用 JNI 进行此操作。我们还需要的是关于我们摄像头内部参数的信息。这与第三章中讨论的内容相似,叠加世界,但现在这里使用 Vuforia^(TM)完成。为此,你可以从CameraDevice访问CameraCalibration对象:

float nearPlane = 1.0f;
float farPlane = 1000.0f;
const QCAR::CameraCalibration& cameraCalibration = QCAR::CameraDevice::getInstance().getCameraCalibration();
QCAR::Matrix44F projectionMatrix = QCAR::Tool::getProjectionGL(cameraCalibration, nearPlane, farPlane);

我们可以轻松地将投影变换转换为更易于阅读的摄像头配置格式,比如其视场(fovDegrees),我们也必须调整它以适应摄像头传感器和屏幕的宽高比差异:

        QCAR::Vec2F size = cameraCalibration.getSize();
        QCAR::Vec2F focalLength = cameraCalibration.getFocalLength();
        float fovRadians = 2 * atan(0.5f * size.data[1] / focalLength.data[1]);
        float fovDegrees = fovRadians * 180.0f / M_PI;
        float aspectRatio=(size.data[0]/size.data[1]);

        float viewportDistort=1.0;
        if (viewportWidth != screenWidth)     {
        	viewportDistort = viewportWidth / (float) screenWidth;
            fovDegrees=fovDegrees*viewportDistort;
            aspectRatio=aspectRatio/viewportDistort;
        }
        if (viewportHeight != screenHeight)  {
        	viewportDistort = viewportHeight / (float) screenHeight;
            fovDegrees=fovDegrees/viewportDistort;
            aspectRatio=aspectRatio*viewportDistort;
        }

然后,我们调用三个 JNI 函数,将视场(setCameraPerspectiveNative)、摄像头位置(setCameraPoseNative)和摄像头方向(setCameraOrientationNative)传输到我们的 Java 层。这三个函数在 VuforiaJME 类中有定义,这使得我们可以快速修改我们的虚拟摄像头:

jclass activityClass = env->GetObjectClass(obj);
        jmethodID setCameraPerspectiveMethod = env->GetMethodID(activityClass,"setCameraPerspectiveNative", "(FF)V");
        env->CallVoidMethod(obj,setCameraPerspectiveMethod,fovDegrees,aspectRatio);
        jmethodID setCameraViewportMethod = env->GetMethodID(activityClass,"setCameraViewportNative", "(FFFF)V");
        env->CallVoidMethod(obj,setCameraViewportMethod,viewportWidth,viewportHeight,cameraCalibration.getSize().data[0],cameraCalibration.getSize().data[1]);
       // jclass activityClass = env->GetObjectClass(obj);
        jmethodID setCameraPoseMethod = env->GetMethodID(activityClass,"setCameraPoseNative", "(FFF)V");
        env->CallVoidMethod(obj,setCameraPoseMethod,cam_x,cam_y,cam_z);

        //jclass activityClass = env->GetObjectClass(obj);
        jmethodID setCameraOrientationMethod = env->GetMethodID(activityClass,"setCameraOrientationNative", "(FFFFFFFFF)V");
        env->CallVoidMethod(obj,setCameraOrientationMethod,cam_right_x,cam_right_y,cam_right_z,
        cam_up_x,cam_up_y,cam_up_z,cam_dir_x,cam_dir_y,cam_dir_z);

    }

    QCAR::Renderer::getInstance().end();
}

最后一步将是编译程序。所以,运行一个命令行窗口,前往包含文件的 jni 目录。从那里你需要调用 ndk-build 函数。该函数在你的 android-ndk-r9d 目录中定义,所以确保它可以从你的路径中访问。如果一切顺利,你应该会看到以下内容:

Install        : libQCAR.so => libs/armeabi-v7a/libQCAR.so
Compile++ arm  : VuforiaNative <= VuforiaNative.cpp
SharedLibrary  : libVuforiaNative.so
Install        : libVuforiaNative.so => libs/armeabi-v7a/libVuforiaNative.so

是时候回到 Java 了!

Java 集成

Java 层定义了之前使用与我们的 Superimpose 示例相似的类调用的函数。第一个函数是 setRGB565CameraImage,它处理视频图像,如之前的例子所示。

其他 JNI 函数将修改我们前台摄像头的特性。具体来说,我们会调整 JME 摄像头的左侧轴,以匹配 Vuforia^(TM) 使用的坐标系(如图中选择物理对象一节所示)。

  public void setCameraPerspectiveNative(float fovY,float aspectRatio) {
            fgCam.setFrustumPerspective(fovY,aspectRatio, 1, 1000);
  }  
  public void setCameraPoseNative(float cam_x,float cam_y,float cam_z){
           fgCam.setLocation(new Vector3f(cam_x,cam_y,cam_z));
  }

  public void setCameraOrientationNative(float cam_right_x,float cam_right_y,float cam_right_z,
  float cam_up_x,float cam_up_y,float cam_up_z,float cam_dir_x,float cam_dir_y,float cam_dir_z) {
       //left,up,direction
       fgCam.setAxes(new Vector3f(-cam_right_x,-cam_right_y,-cam_right_z), 
         new Vector3f(-cam_up_x,-cam_up_y,-cam_up_z), 
         new Vector3f(cam_dir_x,cam_dir_y,cam_dir_z));
  } 

最后,我们必须调整显示摄像头图像的背景摄像头的视口,以防止 3D 对象漂浮在物理目标之上:

public void setCameraViewportNative(float viewport_w,float viewport_h,float size_x,float size_y) {		
      float newWidth = 1.f;
      float newHeight = 1.f;

      if (viewport_h != settings.getHeight())
      {
        newWidth=viewport_w/viewport_h;
        newHeight=1.0f;
        videoBGCam.resize((int)viewport_w,(int)viewport_h,true);
        videoBGCam.setParallelProjection(true);
      }
      float viewportPosition_x =  (((int)(settings.getWidth()  - viewport_w)) / (int) 2);//+0
      float viewportPosition_y =  (((int)(settings.getHeight() - viewport_h)) / (int) 2);//+0
      float viewportSize_x = viewport_w;//2560
      float viewportSize_y = viewport_h;//1920

      //transform in normalized coordinate
      viewportPosition_x =  (float)viewportPosition_x/(float)viewport_w;
      viewportPosition_y =  (float)viewportPosition_y/(float)viewport_h;
      viewportSize_x = viewportSize_x/viewport_w;
      viewportSize_y = viewportSize_y/viewport_h;

    //adjust for viewport start (modify video quad)
        mVideoBGGeom.setLocalTranslation(-0.5f*newWidth+viewportPosition_x,-0.5f*newHeight+viewportPosition_y,0.f);
    //adust for viewport size (modify video quad)
    mVideoBGGeom.setLocalScale(newWidth, newHeight, 1.f);
  }

就这么多。我们再次想要强调的是背后的概念:

  • 你的跟踪器中使用的摄像头模型与你的虚拟摄像头(在这个例子中,Vuforia^(TM) 的 CameraCalibration 与我们的 JME 虚拟摄像头)相匹配。这将保证我们正确的注册。

  • 你在摄像头坐标系中跟踪一个目标(在这个例子中,是来自 Vuforia^(TM) 的自然特征目标)。这种跟踪取代了我们之前看到的 GPS,并使用了一个局部坐标系。

  • 这个目标的位置被用来修改你的虚拟摄像头的姿态(在这个例子中,通过 JNI 将检测到的位置从 C++ 传输到 Java,并更新我们的 JME 虚拟摄像头)。由于我们对每一帧都重复这个过程,因此在物理(目标)和虚拟(我们的 JME 场景)之间有一个完整的 6DOF 注册。

你的结果应该与下面这幅图类似:

Java 集成

总结

在本章中,我们介绍了基于计算机视觉的 AR。我们使用 Vuforia^(TM) 库开发了一个应用程序,并展示了如何将其与 JME 集成。你现在可以创建基于自然特征跟踪的 AR 应用程序了。在这个演示中,你可以围绕标记移动你的设备,并从各个方向看到虚拟内容。在下一章中,我们将学习如何进行更多交互。比如能够选择模型并与之互动怎么样?

第六章:让它互动——创建用户体验

在前面的章节中,我们已经学习了使用两种最常见的 AR 方法创建增强现实的要点:基于传感器和基于计算机视觉的 AR。我们现在能够将数字内容叠加在物理世界的视图上,支持 AR 跟踪,以及处理账户注册(在目标上或户外)。

然而,我们仅仅能在增强的世界中导航。如果能让用户以直观的方式与虚拟内容互动,岂不是很好吗?用户互动是任何应用程序开发的重要组成部分。由于我们这里专注于用户与 3D 内容(3D 交互)的互动,以下是三种主要的交互技术类别,可以加以开发:

  • 导航:在场景中移动并选择一个特定的视角。在增强现实(AR)中,这种导航是通过物理移动来完成的(例如,在街上行走或转动桌子),并且可以辅以额外的虚拟功能(例如,地图视图,导航路径,冻结模式等)。

  • 操作:选择、移动和修改对象。在 AR 中,这可以应用于物理和虚拟元素,通过一系列传统方法(例如,射线选择),以及新颖的交互范式(例如,有形用户界面)。

  • 系统控制:调整应用程序的参数,包括渲染、轮询过程和依赖于应用程序的内容。在 AR 中,它可以对应于调整跟踪参数或可视化技术(例如,在 AR 浏览器中显示到您的兴趣点(POI)的距离)。

在本章中,我们将向您展示一些常用的 AR 交互技术的一个子集。我们将向您展示如何开发三种交互技术,包括射线选择、基于接近度的交互和基于 3D 运动手势的交互。这是设计 AR 应用程序的下一步,也是我们 AR 层的基本构建块(请参阅第一章,增强现实概念和工具)。

拿起棍子——使用射线选择进行 3D 选择

在台式计算机上,3D 交互使用的是一组有限的设备,包括键盘、鼠标或游戏操纵杆。在智能手机(或平板电脑)上,交互主要由触摸或传感器输入驱动。从交互输入(传感器数据,如在屏幕上的 x 和 y 坐标,或事件类型,如点击或悬停)开始,您可以开发不同的交互技术,如射线选择、转向导航等。对于移动 AR,可以使用大量交互技术进行 2D 或 3D 交互。在本节中,我们将探讨结合触摸输入和名为射线选择的技术。

射线拣选的概念是使用一个从你的设备到你的环境(即目标)的虚拟射线,并检测沿途它击中了什么。当你在某个对象上得到一个击中(例如,射线与你的虚拟角色之一相交),你可以认为这个对象已被拣选(选中)并开始操作它。在这里,我们只看如何在 JME 中拣选一个对象。在示例代码中,你可以扩展对象以支持进一步的操作,例如,当一个对象被击中并拣选,你可以检测滑动触摸动作并平移对象,让它爆炸,或者将击中用作某些游戏的射击射线,等等。

那么让我们开始。在 JME 中,你可以使用特定于 Android 的输入功能(通过AndroidInput)或与桌面应用程序相同的输入(MouseInput)。默认情况下,JME 在 Android 上将任何触摸事件映射为鼠标事件,这允许我们在 Android 和桌面几乎使用相同的代码。我们将为这个项目选择以下解决方案;作为一个练习,你可以尝试使用AndroidInput(查看AndroidTouchInputListener以使用AndroidInput)。

打开RayPickingJME示例。它使用与VuforiaJME相同的基代码,我们的拣选方法基于 JME 的一个示例,对于这种拣选方法,请访问以下链接:jmonkeyengine.org/wiki/doku.php/jme3:beginner:hello_picking

首先要做的就是在我们的RayPickingJME类中添加不同的射线拣选所需的包:

import com.jme3.math.Ray;
import com.jme3.collision.CollisionResult;
import com.jme3.collision.CollisionResults;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseButtonTrigger;

为了能够拣选一个对象,我们需要在RayPicking类的范围内声明一些全局变量:

  • Node shootables

  • Geometry geom

下一步是向我们的类中添加一个监听器。如果你从未做过 Android 或 JME 编程,你可能不知道监听器是什么。监听器是一种事件处理技术,可以监听类中发生的任何活动,并提供特定方法来处理任何事件。例如,如果你有一个鼠标按钮点击事件,你可以为它创建一个监听器,该监听器有一个onPushEvent()方法,你可以在其中安装你自己的代码。在 JME 中,事件管理和监听器被组织成两个组件,通过使用InputManager类进行控制。

  • 触发器映射:使用这个你可以将设备输入与一个触发器名称关联起来,例如,点击鼠标可以与PressShootMoveEntity等关联。

  • 监听器:使用这个你可以将触发器名称与特定的监听器关联起来;ActionListener(用于离散事件,如“按钮按下”)或AnalogListener(用于连续事件,如操纵杆移动的幅度)。

因此,在你的simpleInitApp过程中,添加以下代码:

  inputManager.addMapping("Shoot",      // Declare...
    newKeyTrigger(KeyInput.KEY_SPACE), // trigger 1: spacebar, or
    newMouseButtonTrigger(0));         // trigger 2: left-button click
  inputManager.addListener(actionListener, "Shoot");

所以这里,我们将按下空格键(即使使用虚拟键盘)和鼠标点击(这是在我们移动设备上的触摸动作)映射到触发器名称 Shoot。这个触发器名称与名为 actionListenerActionListener 事件监听器相关联。动作监听器将是我们进行射线拾取的地方;因此,在触摸屏设备上,通过触摸屏幕,你可以激活 actionListener(使用触发器 Shoot)。

我们下一步是定义可能被我们的射线拾取命中的对象列表。一个不错的技术是将它们重新组合在一个特定的组节点下。在下面的代码中,我们将创建一个盒子对象并将其放置在名为 shootables 的组节点下:

Box b = new Box(7, 4, 6); // create cube shape at the origin
geom = new Geometry("Box", b);  // create cube geometry from the shape
Material mat = new Material(assetManager,
"Common/MatDefs/Misc/Unshaded.j3md");  // create a simple material
mat.setColor("Color", ColorRGBA.Red);   // set color of material to blue
geom.setMaterial(mat);        // set the cube's material
geom.setLocalTranslation(new Vector3f(0.0f,0.0f,6.0f));

shootables = new Node("Shootables");
shootables.attachChild(geom);
rootNode.attachChild(shootables);

现在我们有了触摸映射和可以被击中的对象。我们只需要实现我们的监听器。在 JME 中进行射线投射的方式与许多其他库相似;我们使用击中坐标(在屏幕坐标中定义),通过我们的摄像机进行变换,创建一个射线,并进行击中测试。在我们的 AR 示例中,我们将使用由基于计算机视觉的追踪器 fgCam 更新的 AR 摄像机。因此,在 AR 中的代码与另一个虚拟游戏中的相同,不同之处在于,这里的摄像机位置是由追踪器更新的。

我们创建一个 Ray 对象并通过调用 collideWith 来对我们的可击中对象列表(shootables)进行拾取测试(击中测试)。碰撞结果将被存储在一个 CollisionResults 对象中。因此,我们的监听器的代码如下所示:

  privateActionListeneractionListener = new ActionListener() {

  public void onAction(String name, booleankeyPressed, float tpf) {
      Log.d(TAG,"Shooting.");

      if (name.equals("Shoot") && !keyPressed) {

        // 1\. Reset results list.
        CollisionResults results = new CollisionResults();

        // 2\. Mode 1: user touch location.
        Vector2f click2d = inputManager.getCursorPosition();
        Vector3f click3d = fgCam.getWorldCoordinates(
        new Vector2f(click2d.x, click2d.y), 0f).clone();
        Vector3f dir = fgCam.getWorldCoordinates(
        new Vector2f(click2d.x, click2d.y), 1f).subtractLocal(click3d).normalizeLocal();
        Ray ray = new Ray(click3d, dir);

        // 2\. Mode 2: using screen center
        //Aim the ray from fgcamloc to fgcam direction.
        //Ray ray = new Ray(fgCam.getLocation(), fgCam.getDirection());

        // 3\. Collect intersections between Ray and Shootables in results list.
        shootables.collideWith(ray, results);
…

那么,我们应该如何处理这个结果呢?正如本书前面所解释的,你可以以不同的方式操作它。这里我们将做一件简单的事情;我们会检测我们的盒子是否被选中,如果被选中,在没有交点的情况下将其颜色变为红色,如果存在交点则变为绿色。我们首先打印结果以便调试,你可以使用 getCollision() 函数来检测哪个对象被击中(getGeometry()),在什么距离(getDistance())以及接触点(getContactPoint()):

  for (int i = 0; i<results.size(); i++) {
    // For each hit, we know distance, impact point, name of geometry.
    floatdist = results.getCollision(i).getDistance();
    Vector3f pt = results.getCollision(i).getContactPoint();
    String hit = results.getCollision(i).getGeometry().getName();

    Log.d(TAG,"* Collision #" + i + hit);
    //         Log.d(TAG,"  You shot " + hit + " at " + pt + ", " + dist + "wu away.");
  }

因此,使用前面的代码我们可以检测是否有任何结果,由于我们的场景中只有一个对象,我们认为如果我们有击中,那就是我们的对象,所以我们把对象的颜色变为绿色。如果我们没有任何击中,因为只有我们的对象,我们将其变为红色:

  if (results.size() > 0) {
    // The closest collision point is what was truly hit:
  CollisionResult closest = results.getClosestCollision();

  closest.getGeometry().getMaterial().setColor("Color", ColorRGBA.Green);
  } else {
    geom.getMaterial().setColor("Color", ColorRGBA.Red);
  }

你应该得到一个类似于以下截图所示的结果(击中:左,未击中:右):

选择棒子 - 使用射线拾取的 3D 选择

现在你可以部署并运行这个例子;在屏幕上触摸对象,看看我们的盒子颜色变化!

基于邻近的交互

AR 中的另一种交互方式是利用相机与物理对象之间的关系。如果你在桌子上放置了一个目标,并且你带着设备围绕它移动以从不同角度观察虚拟对象,你也可以使用这种方式来创建交互。这个想法很简单:你可以检测到移动设备上的相机(你的设备)与放在桌子上的目标之间的空间变换的任何变化,并触发一些事件。例如,你可以检测相机是否处于特定角度,是否从上方看向目标,等等。

在本例中,我们将实现一种接近性技术,该技术可用于创建一些酷炫的动画和效果。接近性技术利用了 AR 相机与基于计算机视觉的目标之间的距离。

因此,请在你的 Eclipse 中打开ProximityBasedJME项目。同样,这个项目也是基于VuforiaJME示例的。

首先,我们使用三种不同的颜色——红色、绿色和蓝色,创建三个对象——一个盒子、一个球体和一个圆环,如下所示:

    Box b = new Box(7, 4, 6); // create cube shape at the origin
    geom1 = new Geometry("Box", b);  // create cube geometry from the shape
    Material mat = new Material(assetManager,"Common/MatDefs/Misc/Unshaded.j3md");  // create a simple material
    mat.setColor("Color", ColorRGBA.Red);   // set color of material to red
    geom1.setMaterial(mat);                   // set the cube's material

    geom1.setLocalTranslation(new Vector3f(0.0f,0.0f,6.0f));

    rootNode.attachChild(geom1);              // make the cube appear in the scene

    Sphere s = new Sphere(12,12,6);
    geom2 = new Geometry("Sphere", s);  // create sphere geometry from the shape
    Material mat2 = new Material(assetManager,"Common/MatDefs/Misc/Unshaded.j3md");  // create a simple material
    mat2.setColor("Color", ColorRGBA.Green);   // set color of material to green
    geom2.setMaterial(mat2);                   // set the sphere's material

    geom2.setLocalTranslation(new Vector3f(0.0f,0.0f,6.0f));

    rootNode.attachChild(geom2);              // make the sphere appear in the scene

    Torus= new Torus(12, 12, 2, 6); // create torus shape at the origin
    geom3 = new Geometry("Torus", t);  // create torus geometry from the shape
    Material mat3 = new Material(assetManager,"Common/MatDefs/Misc/Unshaded.j3md");  // create a simple material
    mat3.setColor("Color", ColorRGBA.Blue);   // set color of material to blue
    geom3.setMaterial(mat3);                   // set the torus material
    geom3.setLocalTranslation(new Vector3f(0.0f,0.0f,6.0f));

    rootNode.attachChild(geom3);              // make the torus appear in the scene

在许多场景图库中,你经常会找到一个允许基于某些参数(如对象到相机的距离)切换对象表示的开关节点。JME 没有开关节点,因此我们将模拟其行为。我们将根据物体与相机的距离来改变显示的对象(盒子、球体或圆环)。实现这一点的简单方法是,在特定距离处添加或移除不应该显示的对象。

要实现接近性技术,我们查询 AR 相机(fgCam.getLocation())的位置。从该位置,你可以计算到某些对象或仅是目标的距离。根据定义,到目标的距离类似于相机的位置(用三维向量表示)的距离。所以,我们要做的是为我们的对象定义三个范围,如下所示:

  • 相机距离 50 及以上:显示立方体

  • 相机距离 40-50:显示球体

  • 相机距离 40 以下:显示圆环

simpleUpdate方法中的生成代码相当简单:

      Vector3f pos=new Vector3f();

      pos=fgCam.getLocation();

      if (pos.length()>50.)
      {
        rootNode.attachChild(geom1);           
        rootNode.detachChild(geom2);       
        rootNode.detachChild(geom3); 

      }
      else
        if (pos.length()>40.)
        {
          rootNode.detachChild(geom1);           
          rootNode.attachChild(geom2);       
          rootNode.detachChild(geom3); 
        },
        else
        {
          rootNode.detachChild(geom1);           
          rootNode.detachChild(geom2);       
          rootNode.attachChild(geom3); 
        }

运行你的示例,并改变设备与追踪目标之间的距离。这将影响所呈现的对象。当你远离时(如下图左侧所示)会出现立方体,靠近时(如下图右侧所示)会出现圆环,在中间距离时(如下图中心所示)会出现球体:

基于接近性的交互

使用加速度计的简单手势识别

在第四章《在世界中定位》中,你已经了解了典型的 Android 设备内置的各种传感器。你学会了如何使用它们来推导出设备方向。然而,你可以用这些传感器做更多的事情,特别是加速度计。如果你玩过 Wii 游戏,你肯定会对通过挥动 Wiimote 实现自然交互而着迷(例如,在玩网球或高尔夫 Wii 游戏时)。有趣的是,Wiimote 使用的加速度计与许多 Android 智能手机类似,因此你实际上可以实现与 Wiimote 类似的交互方法。对于复杂的 3D 运动手势(如在空中画八字),你将需要一些机器学习背景或使用以下链接中的库:www.dfki.de/~rnessel/tools/gesture_recognition/gesture_recognition.html。但是,如果你只想识别简单的手势,你可以很容易地在几行代码中实现。接下来,我们将向你展示如何识别简单的手势,比如摇动手势,即快速地前后挥动手机几次。

如果你查看示例项目ShakeItJME,你会发现它很大程度上与第四章《在世界中定位》中的SensorFusionJME项目相同。实际上,我们只需执行几个简单的步骤,就可以扩展任何已经使用加速度计的应用程序。在ShakeItJMEActivity中,你首先添加一些与摇动检测相关的变量,主要包括存储加速度计事件时间戳的变量(mTimeOfLastShakemTimeOfLastForcemLastTime),存储过去的加速度力的变量(mLastAccelValXmLastAccelValYmLastAccelValZ),以及用于摇动持续时间、超时(SHAKE_DURATION_THRESHOLDTIME_BETWEEN_ACCEL_EVENTS_THRESHOLDSHAKE_TIMEOUT)以及加速度力和传感器样本的最小数量(ACCEL_FORCE_THRESHOLDACCEL_EVENT_COUNT_THRESHOLD)的阈值。

接下来,你只需在你的SensorEventListener::onSensorChanged方法中的Sensor.TYPE_ACCELEROMETER代码部分,简单地添加对detectShake()方法的调用。

detectShake()方法是你的摇动检测的核心:

public void detectShake(SensorEvent event) {
…
  floatcurAccForce = Math.abs(event.values[2] - mLastAccelValZ) / timeDiff;
  if (curAccForce> ACCEL_FORCE_THRESHOLD) {
    mShakeCount++;
    if ((mShakeCount>= ACCEL_EVENT_COUNT_THRESHOLD) && (now - mTimeOfLastShake> SHAKE_DURATION_THRESHOLD)) {
      mTimeOfLastShake = now;mShakeCount = 0;			
      if ((com.ar4android.ShakeItJME) app != null) {
        ((com.ar4android.ShakeItJME) app).onShake();
      }
    }
…
  }    
}

在这种方法中,你基本上是检查在特定时间框架内的加速度计数值是否大于阈值。如果是,就调用你的 JME 应用程序的onShake()方法,并将事件整合到你的应用程序逻辑中。注意,在这个例子中,我们只使用了沿 z 轴的加速度计数值,即与摄像头指向的方向平行。你可以很容易地扩展这一点,也包括侧向摇动运动,通过在curAccForce的计算中纳入加速度计的 x 和 y 值。以下是如何使用摇动检测来触发事件的示例,在你的 JME 应用程序的onShake()`方法中,我们触发了走路忍者的新动画:

public void onShake() {
  mAniChannel.setAnim("Spin");
}

为了避免忍者现在一直旋转;我们将在旋转动画结束后切换到行走动画:

public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) {
if(animName.contains("Spin")) {
    mAniChannel.setAnim("Walk");
  }
}

如果你现在启动应用程序并在观看方向上摇动设备,你应该会看到忍者停止行走并做了一个轻柔的旋转,正如下面这幅图所示:

使用加速度计的简单手势识别

概述

在本章中,我们向你介绍了三种交互技术,适用于各种增强现实(AR)应用。选择技术允许你通过触摸屏幕来选择 3D 对象,就像你在 2D 选择中所做的那样。基于接近度的摄像头技术允许你通过改变设备的距离和方向来触发应用事件。最后,我们向你展示了一个简单的 3D 手势检测方法示例,为你的应用程序添加更多交互可能性。这些技术应作为你创建针对特定应用场景的交互方法的构建块。在最后一章,我们将介绍一些高级技术以及进一步阅读材料,帮助你充分利用增强现实应用。

第七章:进一步阅读与技巧

在最后一章中,我们将为您提供更多高级技巧以及相关链接,以帮助提升任何 AR 应用的开发。我们将介绍内容管理技巧,如多目标和云识别,以及高级交互技术。

管理你的内容

对于基于计算机视觉的 AR,我们向你展示了如何使用单一目标构建应用。然而,在某些情况下,你可能需要同时使用多个标记。想象一下增强一个房间,你需要在每面墙上至少有一个目标,或者你可能希望你的应用能够识别并增强数百个不同的产品包装。前者可以通过追踪具有公共坐标框架的多个目标来实现,后者可以通过使用云识别的力量来实现。我们将在以下各节简要讨论这两种情况。

多目标

多目标不仅仅是几个单独图像的集合。它们实现了一个单一且连贯的坐标系统,手持设备可以在其中被追踪。只要至少有一个目标可见,这就允许场景的持续增强。创建多目标的主要挑战在于定义公共坐标系统(只需做一次)以及在设备运行期间保持这些目标的相对位置。

要创建公共坐标系统,你必须指定所有图像目标相对于公共原点的平移和方向。Vuforia^(TM)为你提供了一个选项,即使不涉及指定整个目标变换的细节,也可以构建常用的多目标,如立方体或长方体。在 Vuforia^(TM)目标管理器中,你可以简单地为具有立方体(长度、高度和宽度相等)或长方体(长度、高度和宽度不同)的目标添加一个立方体,该目标的坐标原点在(不可见)长方体的中心。你需要做的就是指定一个扩展到三个扩展的长方体,并为你的目标的每一面添加单独的图像,如下图所示:

多目标

如果你想要创建更复杂的多目标,例如,追踪整个房间,你必须采取略有不同的方法。首先,你需要在 Vuforia^(TM)目标管理器中的单个设备数据库内上传所有想要用于多目标的多张图片。之后,将设备数据库下载到你的开发机上,然后你可以修改下载的<database>.xml文件,以添加单个图像目标的名字以及它们相对于坐标原点的平移和方向。你可以在 Vuforia^(TM)知识库中找到示例 XML 文件,链接为developer.vuforia.com/resources/dev-guide/creating-multi-target-xml-file

请注意,您的设备数据库中最多只能有 100 个目标,因此,您的多目标最多只能由这么多图像目标组成。还要注意,在运行时更改图像目标的位置(例如,打开产品包装)将阻碍对您的坐标系的一致跟踪,即,各个目标元素之间定义的空间关系将不再有效。这甚至可能导致跟踪完全失败。如果您想将单个移动元素作为应用程序的一部分使用,除了多目标之外,您还必须将它们定义为单独的图像目标。

云端识别

如前所述,在您的 Vuforia^(TM)应用程序中,同时只能使用多达 100 个图像。通过使用云数据库,可以克服这一限制。这里的基本思想是,您使用摄像头图像查询云服务,如果在云端识别到目标,则可以在本地设备上处理已识别目标的跟踪。这种方法的主要优点是,您可以识别多达一百万张图像,这对于大多数应用场景来说应该是足够的。然而,这种好处并非没有代价。由于识别过程在云端进行,您的客户端必须连接到互联网,响应时间可能需要几秒钟(通常大约两到三秒)。

与云端识别不同,存储在设备上的图像数据库通常只需要大约 60 到 100 毫秒。为了更容易上传大量图像以供云端识别,您甚至不需要使用 Vuforia^(TM)在线目标管理网站,而可以使用特定的 Web API——Vuforia^(TM) Web 服务 API,该 API 可以在以下 URL 找到:developer.vuforia.com/resources/dev-guide/managing-targets-cloud-database-using-developer-api。您可以通过访问developer.vuforia.com/resources/dev-guide/cloud-targets在 Vuforia^(TM)知识库中找到有关使用云端识别的更多信息。

提高识别和跟踪

如果您想创建自己的自然特征跟踪目标,重要的是要设计它们,以便 AR 系统能够很好地识别和跟踪。第五章《与好莱坞相同——虚拟物体上的物理对象》中的了解自然特征跟踪目标部分介绍了自然特征目标的基础。可良好追踪的目标的基本要求是它们具有大量的局部特征。但如果您的目标识别不佳怎么办?在一定程度上,您可以通过使用以下技巧来改善跟踪。

首先,你想要确保图像具有足够的局部对比度。在 GIMP 或 Photoshop 等任何照片编辑软件中查看目标灰度表示的直方图是判断目标整体对比度的一个好方法。你通常希望直方图分布广泛,而不是像下面图中那样只有少数尖峰:

提高识别和追踪

要增加图像的局部对比度,你可以使用你选择的照片编辑器,并应用去锐化蒙版滤镜或清晰度滤镜,如在 Adobe Lightroom 中。

提示

为了避免在 Vuforia^(TM)目标创建过程中出现重采样伪影,请确保上传的单个图像具有精确的 320 像素图像宽度。这样可以避免因服务器端自动调整图像大小而导致的走样效果和局部特征数量减少。通过改善渲染效果,Vuforia^(TM)会将你的图像重新缩放到最长图像边最大为 320 像素。

在本书的示例应用中,我们使用了不同类型的 3D 模型,包括基本图元(如我们的彩色立方体或球体)或更高级的 3D 模型(如忍者模型)。对于所有这些模型,我们并没有真正考虑真实感,包括光线条件。任何桌面或移动 3D 应用程序都会考虑渲染看起来如何真实。这种真实感追求始终通过模型几何质量、外观定义(材质反射属性)以及它们如何与光线交互(着色和照明)来体现。

真实感渲染将展示诸如遮挡(某物的前后)阴影(来自光照)对一系列真实材质的支持(使用着色器技术开发)或更高级的属性,例如支持全局照明。

当你开发 AR 应用时,还应该考虑真实感渲染。然而,事情会有些复杂,因为在 AR 中,你不仅要考虑虚拟方面(例如,桌面 3D 游戏),还要考虑真实方面。在 AR 中支持真实感意味着你需要考虑真实(R)环境和虚拟(V)环境在渲染过程中如何交互,可以通过以下四种不同情况简化如下:

  1. V→V

  2. V→R

  3. R→V

  4. R→R

最简单的事情是支持 V→V,这意味着你可以在 3D 渲染引擎中启用任何高级渲染技术。对于基于计算机视觉的应用程序,这意味着目标上的所有东西看起来都是真实的。对于基于传感器的应用程序,这意味着你的虚拟对象在彼此之间看起来是真实的。

对于基于计算机视觉的应用程序,第二步是使用平面技术支持 V→R(虚拟到现实)。如果你有一个目标,可以创建一个半透明的版本并将其添加到你的虚拟场景中。如果你启用了阴影,那么看起来阴影会投射到你的目标上,从而创建一个简单的 V→R(虚拟到现实)的错觉。你可以参考以下论文,其中提供了一些解决此问题的技术方案:

  • 请参考Michael HallerStephan DrabWerner Hartmann所著的《一种用于增强现实应用的实时阴影方法。VRST 2003: 56-65》。

处理 R→V(现实到虚拟)要复杂一些,仍然是一个困难的研究课题。例如,支持虚拟对象由物理光源照明需要付出大量的努力。

相反,对于 R→V 来说,遮挡是容易实现的。如果例如一个物理对象(如一个罐头)放在你的虚拟对象前面,那么 R→V 情况下的遮挡就会发生。在标准 AR(增强现实)中,你总是将虚拟内容渲染在视频前面,所以即使你的罐头实际上在目标前面,它看起来也会在后面。

产生这种效果的一个简单技术有时被称为幻影对象。你需要创建一个物理对象的虚拟对应物,比如一个圆柱体来代表你的罐头。将这个虚拟对应物放置在与物理对象相同的位置,并进行仅深度渲染。仅深度渲染在许多库中都可以使用,它与颜色遮罩有关,当你渲染任何东西时,你可以决定要渲染哪个通道。通常,你有红色、绿色、蓝色和深度的组合。因此,你需要关闭前三个通道,只激活深度。它将渲染一种幻影对象(没有颜色,只有深度),通过标准的渲染管线,在你有真实对象的地方,视频将不再被遮挡,遮挡看起来会非常真实;例如,请参阅hal.inria.fr/docs/00/53/75/15/PDF/occlusionCollaborative.pdf

这是一种简单的情况;当你有一个动态对象时,事情要复杂得多,你需要能够跟踪你的对象,更新它们的幻影模型,并获得逼真的渲染效果。

高级交互技术

在前一章中,我们研究了一些简单的交互技术,包括光线选择(通过触摸交互)、传感器交互,或者摄像头与目标的接近性。在增强现实中,还有大量其他的交互技术可以使用。

我们在其他移动用户界面也会发现的一种标准技术是虚拟控制面板。由于移动电话限制了访问额外的控制设备,比如游戏手柄或操纵杆,你可以通过触摸界面模拟它们的行为。使用这项技术,你可以在屏幕上显示一个虚拟控制器,并将这一区域的触摸分析等同于控制一个控制面板。它易于实现并增强基本的射线投射技术。控制面板通常显示在屏幕边缘附近,适应形态因素,并捕捉到你持握设备时的手势,这样你可以用手握住设备,并在屏幕上自然地移动手指。

增强现实(AR)中另一种非常流行的技术是有形用户界面TUI)。当我们使用摄像头对准近距离的概念来创建示例时,实际上我们实现了有形用户界面。TUI 的理念是使用物理对象来支持交互。这一概念主要由麻省理工学院的 Tangible Media Group 的Iroshi Ishii教授大力开发和丰富——相关网站可以参考tangible.media.mit.edu/Mark Billinghurst在他的博士研究中将这一概念应用于增强现实,并展示了一系列专用的交互技术。

TUI AR 的第一种类型是本地交互,例如,你可以使用两个目标进行交互。类似于我们在ProximityBasedJME项目中检测摄像头与目标之间的距离的方式,你可以用两个目标复制相同的想法。你可以检测两个目标是否彼此靠近,是否在同一方向上对齐,并触发一些动作。当你在卡片游戏中希望卡片之间进行交互,或者包含谜题的游戏需要用户组合不同的卡片时,你可以使用这种类型的交互。

第二种类型的 TUI AR 是全局交互,这种交互方式也需要使用两个或更多目标,但其中一个目标会变得特殊。在这种情况下,你需要定义一个目标作为基础目标,其他所有目标都参照这个基础目标。实现时,你只需计算其他目标相对于基础目标的局部变换,以基础目标作为原点并在其背后定义。这样,将目标放置在主目标上就变得非常简单,某种程度上定义了一个地面平面,并且可以与它进行一系列不同类型的交互。《Mark Billinghurst》介绍了一个著名的衍生版本,用于执行基于拨片的交互。在这种情况下,其中一个目标被用作拨片,可以用来在地面平面上进行交互——你可以触摸地面平面,让拨片在地面平面的特定位置,甚至用它来检测简单的手势(摇动拨片、倾斜拨片等)。为了设置移动 AR,你需要考虑到最终用户手持设备并不能执行复杂的手势,但使用手机时,单手交互仍然是可能的。参考以下技术论文:

  • 有形的增强现实。ACM SIGGRAPH ASIA(2008):1-10,作者Mark BillinghurstHirokazu KatoIvan Poupyrev

  • 设计增强现实界面。ACM Siggraph Computer Graphics 39.1(2005):17-22,作者Mark BillinghurstRaphael GrassetJulian Looser

从某种意义上说,与 TUI 的全局交互可以被定义为屏幕背后的交互,而虚拟控制面板可以被看作是屏幕前方的交互。这是与移动设备交互的另一种分类方式,它将我们引向了第三类交互技术:目标上的触摸交互。例如,Vuforia^(TM)库实现了虚拟按钮的概念。目标上的特定区域可以用来放置控制器(例如,按钮、滑块和旋钮),用户可以将手指放在这个区域上并控制这些元素。这个概念背后的原理是基于时间的;如果你将手指长时间放在这个区域,它会模拟你在电脑上可能进行的点击,或者在触摸屏上进行的轻敲。例如,参考

研究实验室正在探索其他技术,这些技术很快就会成为未来移动增强现实(AR)的可用技术,因此在你考虑它们何时可用时,也应该将它们考虑在内。一个趋势是朝向 3D 手势交互,也被称为空中交互。你不再需要触摸屏幕或目标,可以想象在设备和目标之间进行手势操作。对于 3D 建模来说,移动 AR 将是一种适当的技巧。3D 手势面临许多挑战,比如识别手、手指、手势,以及可能导致疲劳的身体参与等等。在不久的将来,这种已经在智能家居设备(如微软的 Kinect)上普及的交互方式,将配备 3D 传感器的设备上得到应用。

总结

在本章中,我们向您展示了如何通过使用多目标或云识别来超越标准的 AR 应用,基于计算机视觉的 AR。我们还向您展示了如何提高图像目标的跟踪性能。此外,我们还向您介绍了一些用于 AR 应用的高级渲染技术。最后,我们还展示了一些新颖的交互技术,您可以使用它们来创造出色的 AR 体验。这一章为您介绍了 Android 增强现实开发世界的入门。我们希望您已经准备好进入 AR 应用开发的新层次。

posted @ 2024-05-22 15:12  绝不原创的飞龙  阅读(22)  评论(0编辑  收藏  举报