ARCore之路——Unity开发从入门到实践

作者: 汪祥春
译者:
编辑: 张涛

图书目录:

详情

本书旨在教会读者如何使用ARCore,更重要的是帮助读者学习ARCore一些底层技术原理及机制,希望本书可以为读者打开迈向AR技术的大门。

图书摘要

版权信息

书名:ARCore之路——Unity开发从入门到实践

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。


编  著 汪祥春

责任编辑 张 涛

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


汪祥春,计算机科学与技术专业硕士,曾供职于中国人民解放军,从事部队信息化软件开发研制工作,今供职于北京某科技公司,从事项目管理及技术研发工作。拥有深厚的软件工程专业背景,省级科技项目研发管理经验还兼任CSDN专栏作家。拥有十余年软件开发及项目管理的经历,100余万行代码量的开发与积累,编程经验丰富。


本书旨在教会读者如何使用ARCore,更重要的是帮助读者学习ARCore一些底层技术原理及机制,希望本书可以为读者打开迈向AR技术的大门。

本书介绍了ARCore的技术原理和开发Unity AR应用的环境准备及其调试方法,学习运动跟踪技术及3D音效、环境理解技术及Shader知识、光照估计技术及其相应的光照模型知识、图像识别技术、ARCore计算机视觉相关技术、AR与AI结合的相关技术,特别是神经网络知识;共享点云技术,实现多人AR体验共享; ARCore人脸增强技术、开发AR应用的设计原则与设计指南、AR开发时的性能问题排查及优化技术,最后通过综合运用ARCore技术完成若干实例,提高实际运用转化能力。

本书适合ARCore初学者、Unity开发人员、程序员、科研人员,同时也可以作为高校、大专院校相关专业师生的学习用书,以及培训学校的培训教材。


近年来,增强现实(AR)技术受到了专业技术人员和普通民众的广泛关注。在计算机视觉与人工智能技术的推动下,增强现实技术表现出了强劲的发展势头,无论是在跟踪精度、设备性能上,还是在人机交互自然性上都有很大提高。据权威机构预测,增强现实技术会成为下个十年改变人们生活、工作的最重要的技术之一,并即将在5G技术的助力下出现需求爆发。

虽然增强现实从概念提出到现在已有几十年的历史,但增强现实进入到普通大众视野的时间并不长。这其中的原因是多方面的,但ARKit与ARCore的出现彻底改变了这种印象。借助ARCore,我们不再需要单独且昂贵的设备就可以体验到AR带来的奇妙,移动手机一夜之间具备了另一种崭新的应用形式。

ARCore是集当前最前沿、最尖端多学科领域技术于一体的AR开发SDK,一经推出就受到了专业技术人员的广泛关注,其紧凑的模块化设计、易用性极强的良好封装大大降低AR的开发难度,使得个人开发复杂AR应用成为可能。但由于AR是一门前沿技术,ARCore也处在前期的高速发展中,当前可供开发者参考的国内外技术资料非常匮乏,更没有成体系的完整学习资料。本书旨在为ARCore技术开发人员提供一个相对完善的成体系的学习材料,解决AR发展早期参考资料缺乏的问题。

本书关注对ARCore技术的应用,但在讲解ARCore技术点的同时对其原理、技术背景进行了较深入的探究,采取循序渐进的方式,使读者知其然更能知其所以然,一步步地将读者带入AR的殿堂。

本书面向ARCore初学者与程序员,尽量采用通俗易懂的语言从基础入门讲起,但仍然希望读者能具备以下前置知识。

1.有一定的编程经验。尽管ARCore有良好的代码封装集成,但仍然需要编写代码去实现特定功能和效果。学习过C#、Java之类高级语言的读者会更加容易理解接口及方法调用,同时,如果读者有一定的Shader语言基础更佳,但这不是必需的。

2.对Unity引擎操作界面比较熟悉,对Unity的基础操作比较熟练。如创建场景、脚本、挂载组件等。

3.有一定的数据基础。数字三维空间就是用数学精确描述的虚拟世界,如果读者对矩阵、向量及基本的代数运算有所了解会对理解ARCore工作原理、渲染管线有很大帮助,但本书中我们已尽力将数学的影响降到最小,读者不用太担心。

本书属于技术类书籍,预期读者人群包括:

1.高等院及对计算机技术有浓厚兴趣的专科学校学生;

2.对AR技术有兴趣的科技工作者;

3.向AR方向转行的程序员、工程师;

4.研究讲授AR技术的教师;

5.渴望利用新技术的自由职业者或者其他行业人员。

1.结构连贯。本书共分三个部分:第一部分为基础篇(第1~9章),对ARCore各个技术点进行全面深入地剖析;第二部分为提高篇(第10~11章),主要从高层次对AR开发中的原则及性能优化进行讲解;第三部分为实践篇(第12章),通过对基础篇中的技术进行综合运用,提升运用技术点的能力。本书从技术深度到技术运用,使技术转化成实际能力的学习曲线达到最佳。

2.循序渐进。本书充分考虑不同知识背景读者的需求,按知识点循序渐进,利用大量配图、实例进行详细讲解,即使是毫无Unity使用经验的读者也能轻松上手。

3.难易兼顾。在讲解ARCore技术点的同时对其原理、背景进行了较深入的探究,不仅仅拘泥于基础,而且让读者知其然更能知其所以然。同时为防止陷入数学的泥淖,本书使用通俗易懂的语言解释深奥枯燥的数学原理,非常便于数学基础不扎实的读者理解。

4.实用性强。本书实例丰富,每一章都有一到两个实例,还有充分利用各技术点的综合实例章节,很多实例中的技术都可以应用到实际的项目中,实用性非常强。

本书各章节案例源代码可在http://fdflksflsdlkfds.com/download.html上下载。

尽管我们在本书的编写过程中多次对内容、语言描述的连贯一致性和叙述的准确性进行审查、校正,但由于作者能力水平有限,书中仍然难免会出现一些错误,欢迎读者在发现这些问题时及时批评指正。可以发邮件(yolon3000@163.com)联系作者。本书讨论QQ群为190304915。

谨以此书献给我的妻子欧阳女士、孩子妍妍和轩轩,是你们的支持让我能勇往直前,永远爱你们。

作者

二〇一九年六月


AR技术是下个十年最重要的技术之一。时至今日,AR技术的发展已到达临界期,各行业应用需求将随着时间推移而持续增长。2017年,随着AR概念迅速在民众中普及,国际国内大厂先继推出各类AR SDK,其中苹果公司和谷歌公司推出的ARKit和ARCore掀起了AR开发的大幕,并成功吸引了民众与资本的关注,为AR的推广普及打下了坚实的基础。

VR、AR、MR这些英文缩写有时让初学者感到困惑。VR是Virtual Reality的缩写,即虚拟现实技术,是一种能够创建和体验虚拟世界的计算机仿真技术。它利用计算机生成交互式的全数字三维视场,能够营造全虚拟的环境。MR是Mix Reality的缩写,即混合现实,指的是结合真实和虚拟世界创造的全新环境和可视化三维世界。在MR中,物理实体和数字对象共存并实时相互作用,是虚拟现实技术的进一步发展。本书我们主要关注AR技术,并将在后面详细讲述如何用ARCore来开发构建移动端AR。

1.什么是AR

增强现实技术(Augmented Reality,AR),是一种实时计算相机位置和角度(姿态)并在真实环境上叠加文字、图像、视频、3D模型的技术。这种技术的目标是在屏幕上把虚拟信息无缝叠加在真实世界之上从而增强现实,如图1-1所示。

图1-1 AR技术是将虚拟信息叠加在真实环境之上从而达到增强现实的目的

早在1901年,作家L. Frank Baum就提出将电子数据叠加在实现之上产生虚拟与现实混合的思想,当时他把这种技术称之为“字符标识”,这是有记载的最早的虚拟现实的设想。但是,碍于当时的软硬件技术及整体科技水平,这也只能是一种设想。第一个为用户提供沉浸式混合现实体验功能的AR系统是在20世纪90年代初发明的,虚拟装置及系统于1992年在美国空军阿姆斯特朗实验室开发。其后,经过无数代人的努力,最早将AR技术带到普通大众视野的是谷歌公司的Google Glass增强现实眼镜。虽然Google Glass项目最终并未能继续下去,但它给整个AR行业带来了生机和活力。AR研究及应用进入了蓬勃发展时期。Oculus Rift、微软HoloLens、Magic Leap和HTC vive相继推出眼镜产品,特别是2017年苹果公司的ARKit和谷歌公司的ARCore SDK的推出,把AR从专门的硬件中剥离了出来,使得普通手机也可以体验到AR带来的奇妙感受。AR越来越受到各大公司的重视,其技术也是日新月异,百花齐放。

增强现实顾名思义是对现实世界环境的一种增强。在这种环境中,现实世界中的物体被计算机生成的虚拟信息“增强”,甚至可以跨越视觉、听觉、触觉、体感和嗅觉等多种感官模式。叠加的虚拟信息既可以是建设性的(即对现实环境的附加),也可以是破坏性的(即对现实环境的掩蔽),并与现实世界无缝地交织在一起,让人产生身临其境的感觉,分不清虚实。通过这种方式,增强现实可以改变用户对真实世界环境的持续感知,与虚拟现实完全取代用户的真实世界环境完全不一样。

增强现实的主要价值在于它将数字世界带入到个人对现实世界的感知中,而不是简单的数据显示,通过与被视为环境的自然部分的沉浸式集成来实现对现实的增强。借助先进的AR技术(例如添加计算机视觉和物体识别),用户周围真实世界的信息变得可交互和可操作。用简单的话来说,AR就是将虚拟信息放在现实中展现,并且让用户和虚拟信息进行互动。AR通过技术手段能够将现实与虚拟信息进行无缝对接,将在现实中不存在的事物构建在与真实环境一致的同一个三维场景予以展现,与现实生活衔接融合。增强现实技术的发展将改变我们观察世界的方式,想象用户行走在或者驱车行驶在路上,通过增强现实显示器(AR眼镜或者全透明挡风玻璃显示器),信息化图像将出现在用户的视野之内(如路标、导航、提示),并且所播放的声音将与用户所看到的景象保持同步。这些增强信息将实时更新,从而带来全新的观察世界的方式。

2.AR技术AR技术是一门交叉综合技术,涉及数学、物理、工程、信息技术、计算机技术等多领域的知识,相关专业术语、概念也非常多,其中最重要的概念术语主要有以下几个。

(1)硬件:硬件是AR的物质基础。增强现实需要的硬件主要包括处理器、显示器、传感器和输入设备。有些需要一些特殊的硬件,如深度传感器、眼镜等。通常这类AR产品价格昂贵。有些则不需要专门的硬件,普通的移动终端如智能手机和平板电脑就能满足。通常也包括照相机和MEMS传感器,如加速度计、GPS和固态电子罗盘等。

(2)显示设备:在增强现实中叠加的虚拟信息需要借助显示设备以便反馈到人脑中去。这些显示设备包括光学投影系统、显示器、手持设备和佩戴在人体上的显示系统。头显(HMD)是一种佩戴在前额上的显示装置。HMD将物理世界和虚拟物体的图像放置在用户的眼球视场上,现代HMD经常使用传感器进行六自由度监控,允许系统将虚拟信息与物理世界对齐,并根据用户头部运动相应地调整虚拟信息;眼镜是另一个常见的AR显示设备,眼镜相对来说更便携也更轻巧;移动终端如手机屏幕也是AR常见的显示设备。

(3)眼镜:眼镜(Glasses)这里特指类似近视眼镜的AR显示器,但它远比近视眼镜复杂。它使用相机采集真实环境场景,通过处理器对环境进行跟踪并叠加虚拟信息,并将增强的虚拟信息投射在目镜上。

(4)HUD:平视显示器(HUD)是一种透明的显示器,显示数据而不需要用户远离他们通常的观点。HUD是增强现实技术的先驱技术,在20世纪50年代首次为飞行员开发,将简单的飞行数据投射到他们的视线中,从而让他们保持“抬头”而不用低头看仪器设备。因为HUD可以显示数据、信息和图像,同时允许用户查看真实世界,也是一种AR显示设备。

(5)SAR:空间增强现实(Spatial Augmented Reality,SAR)增强现实世界的对象和场景。SAR利用数字投影仪在物理对象上显示图形信息。SAR系统的虚拟内容直接投影在现实世界中。任何物理表面,如墙、桌、泡沫、木块甚至是人体都可以成为可交互的显示屏。随着投影设备尺寸、成本、功耗的降低以及3D投影的不断进步,各种全新的交互及显示形式正在不断涌现。SAR系统最大的优点在于虚拟信息能够以实际的比例和大小呈现在眼前。

(6)跟踪:跟踪是AR实现定位的基础,增强现实系统使用以下跟踪技术中的一种或多种:数码相机和/或其他光学传感器、加速度计、GPS、陀螺仪、固态罗盘、RFID、深度相机。这些技术提供了不同的测量方面和精度水平。跟踪最重要的是需要跟踪用户头部或者虚拟现实设备的姿态,跟踪用户的手或手持式输入设备,可以提供六自由度交互。

(7)网络:因为移动设备、可穿戴设备的广泛普及,AR正在越来越受到欢迎。但是,虚拟现实往往依赖于计算密集型计算机视觉算法,所以对处理及传输延迟具有非常高的要求。为了弥补单台设备计算能力的不足,通常需要将数据上传到中心机器上处理,这在延迟和带宽方面对网络的传输速度提出了非常高的要求。5G技术的发展将能有效地满足这种需求。

(8)输入设备:输入技术包括普通的屏幕输入、手柄输入、将用户的声音翻译成计算机指令的语音识别系统、通过视觉检测或从嵌在外围设备中的传感器来解释用户的身体运动的手势识别系统等。

(9)处理器:增强现实使用处理器融合生成的图像,处理器负责与增强现实相关的图形及算法运算。处理器接收来自传感器的数据、接收扫描的环境信息,这些传感器确定物体表面的相对位置,叠加虚拟信息到合适的地方,然后生成图像或视频,并将其放在接收器上,以便用户查看。物体表面上的固定标记被存储在处理器的存储器中,处理器也从硬盘或者数据库中读取信息,随着技术和处理器的改进,处理器的运算速度越快,增强现实能处理的信息就越多,也更能增强现实。

(10)软件与算法:AR系统的一个关键度量是虚拟信息与现实世界的结合度,AR系统从摄像机图像中获取与摄像机无关的真实世界坐标,这个过程被称为图像配准。通常这些方法由两个阶段组成:第一阶段是在摄像机图像中检测兴趣点、基准标记或光流。该步骤可以使用特征检测方法,如角点检测、斑点检测、边缘检测或阈值处理等图像处理方法;第二阶段从第一阶段获得的数据恢复真实世界坐标系,某些方法假设场景中存在已知几何(或基准标记)的对象。在某些情况下,场景三维结构应预先计算,如果部分场景是未知的,即时定位和映射(SLAM)可以映射相对位置。第二阶段的数学方法包括射影(极线)几何、几何代数、指数映射旋转表示、卡尔曼滤波和粒子滤波、非线性优化、稳健统计等。在AR中,软件与算法大多与计算机视觉相关,主要与图像识别跟踪相关,增强现实的许多计算机视觉方法是从视觉测径法继承的。

(11)交互:AR中叠加的虚拟信息应该支持与用户的交互,增强现实技术最令人兴奋的因素也是3D虚拟空间的引入能力,并在现实中与虚拟信息进行交互,如果一个产品的设计中没有交互,那将大大降低其可玩性、娱乐性、实用性。 这个交互不仅仅包括在用户的操作下的被动交互,也包括程序自发的主动交互,如随着距离的不同显示不同的细节信息。

3.AR技术应用

AR系统具有三个突出的特点:①真实世界和虚拟信息融合;②具有实时交互性;③在三维尺度空间中增添定位虚拟物体。AR技术因为可以将虚拟的信息叠加到现实世界之上,因而在很多领域都有很广泛的应用前景,在相当多的领域都有发展潜力。AR技术可广泛应用于数个领域,有数百种可能的应用,其中游戏和娱乐是最显而易见的应用领域。AR游戏最早的起源并非手机,而是NDS上的AR游戏。此类游戏大多数的玩法都是在桌面上摆放识别卡,识别卡片后通过手机屏幕与识别出来的内容进行交互。2011年任天堂3DS主机内置的《AR游戏》是利用摄像头拍摄AR卡片来玩的游戏。通过利用AR技术,将摄像头拍摄到的内容以另外一种形式展现在屏幕内;由Niantic Lab出品的Ingress与Pokemon GO也是增强现实类游戏;2019年4月11日,国内AR探索手游《一起来捉妖》上线,这是与Pokemon GO差不多的捉怪游戏。目前,在娱乐、游戏领域AR正在快速地发展起来。

除此之外,AR技术在文学、考古、建筑、视觉艺术、商业、应急管理/搜救、教育、社会互动、工业设计、医学、空间沉浸与互动、飞行训练、导航、旅游观光、音乐、零售、虚拟装潢等领域都有很广阔的应用前景。

提示

在本书中:①虚拟对象、虚拟信息、虚拟物体均指在真实环境上叠加的由计算机处理生成的文字、图像、3D模型、视频等虚拟非真实信息,严格来讲这三者是差别的,但有时我们在描述时并不严格区分这三者之间的差异;②Unity、Unity3D均指Unity 3D引擎软件;③Vertex Shader、顶点着色器、顶点Shader均为顶点Cg代码,Fragment Shader、片元着色器、片元Shader均指片元Cg代码。

利用ARCore能将普通手机变成一款AR设备,在移动端实现AR功能。那么什么是ARCore?

1.什么是ARCore

2017年6月6日,苹果公司发布ARKit SDK,它能够帮助用户快速实现AR功能。ARKit框架提供了两种AR技术,一种是基于3D场景(SceneKit)实现的增强现实,另一种是基于2D场景(SpriktKit)实现的增强现实。ARKit的发布大大推动了AR概念的普及,同时也刺激了谷歌公司,广大Android 开发者们也无时不在期待着 谷歌公司进军AR领域,因为没有一个企业比谷歌公司在Android手机系统上开发AR SDK更合适。谷歌公司也并没有让广大开发者失望,2017年08月29日,谷歌公司在Android 官方博客上正式发布了ARCore SDK预览版,开始向AR领域发力。ARCore是一套用来创建AR的SDK。该工具包可以为现有及未来的Android手机提供AR功能。ARCore SDK可以在现下多种流行开发平台中使用。ARCore利用不同的API让手机能够感知其环境、了解现实世界并与虚拟信息进行交互,ARCore还为Android和iOS同时提供API支持共享AR体验。图1-2所示为利用ARCore在水平与垂直平面上放置虚拟物体。

图1-2  利用ARCore SDK在水平和垂直平面上锚定虚拟物体

从本质上讲,ARCore在做两件事:在用户设备移动时跟踪它的姿态和构建自己对现实世界的理解。ARCore的运动跟踪技术使用手机摄像头标识兴趣点(称为特征点),并跟踪这些点随着时间变化的移动,将这些点的移动与手机惯性传感器的读数组合,ARCore可以在手机移动时确定它的位置和屏幕方向。除了标识关键点,ARCore还会检测平坦的表面(例如桌子或地面),并估测周围区域的平均光照强度。这些功能共同让ARCore可以构建自己对周围世界的理解。借助ARCore对现实世界的理解,我们能够以一种与现实世界无缝整合的方式添加物体、注释或其他信息。例如可以将一只打盹的小猫放在咖啡桌的一角,或者利用艺术家的生平信息为一幅画添加注释。运动跟踪意味着可以移动和从任意角度查看这些物体,即使当我们转身离开房间,再回来后,小猫或注释还会在其最初添加定位它们的地方。

2.ARCore的前生今世

2014年,谷歌公司的Tango项目首次展示。Tango是谷歌公司基于FlyBy授权的VIO技术发展起来的AR技术,而且技术也比现在的ARKit和ARCore更复杂,比如有深度感知技术和相关硬件,但它需要用到额外的感应器和其他硬件来辅助增强现实,过高的门槛使得很多消费者甚至开发者难以触及,最后应用Tango的只有联想的Phab 2 Pro和华硕Zenfone手机。因为Tango需要借助更多的传感器辅助现实增强,而在当时AR功能却不是用户的必选项,这导致Tango并未能大规模推广开来。

苹果的ARKit SDK上线后,借助于ARKit,开发者可以轻松地打造AR应用,而所有升级到iOS 11的苹果设备都能享受其带来的便利,而不需要额外的硬件,因此ARKit SDK取得了良好的市场反响。为抢占技术高点,谷歌公司也非常迅速地在Tango基础上提取了ARCore,提供了Android、iOS、Unity、Unreal、Java等多个开发平台的API,使其可以将Android上的AR应用方便地移植到苹果手机。除了让ARCore适配Unreal及Unity等主流3D引擎,谷歌公司还开发了能够将网页延伸至真实世界的浏览器版SDK,通过Java代码将网页中的三维虚拟物件推入真实世界。

ARCore是在当前移动手机广泛普及的情况下,Google适应时代潮流推出的AR开发工具。但其可以追溯到Tango这个已经研究很长时间的AR技术项目,ARKit也是采用的FlyBy技术,因此,尽管技术上有所差异,但从来源上来讲,ARKit和ARCore都是基于FlyBy开发的技术,是苹果与Google在AR领域的最新研发成果。在使用时,ARKit和ARCore都需要扫描环境以检测可用的平面,进而放置与跟踪虚拟物体,图1-3所示为ARCore扫描环境示意图。

图1-3  ARCore扫描环境示意图

ARCore带给用户奇妙体验的背后是数学、物理、计算机技术的支持,对我们来讲,了解其技术原理有助于理解ARCore整个运行生命周期,了解其优势与不足,并能更好地利用它来做应用开发。

谈到位置追踪,不得不说SLAM(Simultaneous Localization And Mapping),中文意思是即时定位与地图映射,SLAM最早由科学家Smith、Self、Cheeseman于1988年提出,SLAM 问题可以描述为:将一个机器人放入未知环境中的未知位置,是否有办法让机器人逐步绘制出此环境的完全地图,所谓完全地图(Consistent Map)是指不受障碍行进到可进入的每个角落的地图。SLAM作为一种基础技术,从最早的军事用途(核潜艇海底定位就有了SLAM的雏形)到今天逐步走入大众的视野。当前,在室外我们可以利用GPS、北斗等导航系统实现非常高精度的定位,甚至可以利用RTK的实时相位差分技术,实现厘米级的定位精度,基本上解决了室外的定位和定姿问题。但室内定位的发展则缓慢得多,为解决室内的定位定姿问题,SLAM技术逐渐脱颖而出。SLAM一般处理流程包括Track和Map两部分。所谓的Track是用来估计相机的位姿,也叫前端,而Map部分(后端)则是深度的构建,通过前端的跟踪模块估计得到相机的位姿,采用三角法(Triangulation)计算相应特征点的深度,然后进行当前环境Map的重建,重建出的Map同时为前端提供更好的姿态估计,并可以用于闭环检测。SLAM是机器进行自主导航的基础技术,近年来进步非常快,图1-4是Kumar教授进行SLAM实验的图示。

图1-4  Kumar 实验中的SLAM

定位与跟踪是AR应用必须解决的问题,定位与跟踪也是AR的基础,目前,从技术角度来看,解决室内定位与定姿主要采用视觉惯性测距系统(Visual Inertial Odometry,VIO)和惯性导航系统。VIO意味着可以通过软件实时追踪用户的空间位置(用户在空间上的六自由度姿态)。VIO在帧刷新之间计算用户的位置,为保持应用流畅,VIO速度必须达到每秒30次及以上,这些计算要并行完成两次,通过视觉(摄像)系统将现实世界中的一个点与摄像机传感器上的一帧像素相匹配,从而追踪用户的姿势。惯性导航系统(用户的加速度计和陀螺仪跟踪统称为惯性测量单元,Inertial Measurement Unit,简称IMU)也可以追踪用户的姿势。在计算完上述过程之后,卡尔曼滤波器(Kalman Filter)结合两个系统的输出结果,决定哪一个系统提供的估测更接近用户的“真实”位置并通过软件更新当前位置。VIO系统追踪用户的设备在三维空间里的移动,好比用户汽车里的里程表测量车的行驶距离一样。

提示

自由度(Degrees Of Freedom,DOF)是指物理学当中描述一个物理状态,独立对物理状态结果产生影响的变量的数量。运动自由度是确定一个系统在空间中的位置所需要的最小坐标数。在三维坐标系描述一个物体在空间中的位置和朝向信息需要6个自由度数据,即6DOF,指 xyz 方向上的三维运动(移动)加上俯仰/偏转/滚动(旋转)。

惯性导航系统带来的最大好处是IMU的读取速度大约为1000次/s,并且是基于加速度的(用户的移动)。采用航迹推算法(Dead Reckoning)测量 IMU 读数之间的设备移动,但这种方法推算是一种估算,就像向前走一步,然后猜测走了多远一样,此时会用航迹推算法来估计距离。惯导系统中的误差也会随时间累积,所以IMU帧率越长,惯导系统从视觉系统中复位后的时间越长,追踪位置距离真实位置偏差就越多。

VIO使用摄像头来采集视觉信息,设备帧率通常为30FPS并且依赖场景复杂度(不同的场景帧率也有所不同)。光学系统通常随着距离的增大误差也不断增大(时间也会有轻度影响),所以用户移动得越远,误差就越大。

惯性导航系统与视觉测量系统各有各的优势和不足,但视觉测量和惯性导航是基于完全不同的测量方式,他们之间并没有相互依赖。这意味着在遮蔽摄像头或者只看到一个具有很少光学特征的场景(比如白墙)时惯性系统照样可以正常工作,或者设备在完全静止的条件下,视觉系统可以呈现出一个比惯性系统更加稳定的姿态。卡尔曼滤波器不断地选择最佳姿态,从而实现稳定跟踪。VIO系统已经存在很多年,在业界已获得了广泛认可,并且在应用市场上已经有相当一部分应用。

为了获得精确的定位,VIO需要获取两张有差异的场景图像,然后对当前位置进行立体计算。我们眼睛就是这样看到3D效果的,一些跟踪器也因此而依赖立体相机。如果采用两台相机就很容易计算,知道两个相机之间的距离,同时捕获帧进行视差计算;如果只有一个相机,可以先捕捉一次画面,然后移动到下一个位置进行第二次捕捉,然后进行视差计算。使用IMU航迹推算可以计算两次数据读取位置之间的距离然后正常进行立体计算(也可以多捕获几次使计算更加准确)。为了获得较高的精度,系统依赖IMU的精确航迹推算,从IMU读取的加速度和时间测量中,可以向后合并以计算速度并且再次向后合并以获取画面之间设备的实际距离(公式S=0.5at2),但是困难的是从IMU中除去误差以获得精确的加速度测量,在设备移动的几秒钟之内,一个微小的错误每秒运行一千次,就会造成非常大程度的误差积累。

深度相机可以帮助提高设备对环境的理解。在低特征场景中,深度相机对提高地面实况、度量标度以及边界追踪的精度都有很大的帮助。但是它们非常耗能,因此只有在非常低的帧率以及帧间使用深度相机和VIO结合才是有意义,深度相机在户外也不能正常运行,因为来自太阳光的红外散射会过滤掉深度相机中的红外线。移动设备深度相机的拍摄范围也比较有限,这意味它们只适合在手机上的短距离范围内使用(几米的范围),另外深度相机在BOM成本方面也是非常昂贵的,因此OEM厂商目前都避免在手机上大量采用,在移动手机上深度相机目前应用并不广泛。

立体RGB或鱼眼镜头也有助于看到更大范围的场景(因为立体RGB和鱼眼镜头具有潜在的光学特性。例如普通镜头可能只会看到白色的墙壁,但是一个鱼眼设备可以在画面中看到有图案的天花板和地毯,Tango和Hololens就使用这种方法)。并且相对VIO而言,它们可以以更低的计算成本来获取深度信息,尽管VIO使用较低的BOM和功率也可以达到同样的精度。由于手机立体摄像头(即使是HMD)之间的距离非常近,因此手机上深度计算的精度范围也受到限制(相隔数厘米距离的手机相机在深度计算的误差上可以达到数米)。

从当前的移动设备软件硬件来看,VIO结合IMU做位置跟踪仍将是以后一段时间内的主流做法。

为了使软件能够精确地匹配摄像机传感器上的像素与现实世界中的点,摄像机系统需要进行精密地校准。

几何校准:使用相机的针孔模型来校正镜头的视野和镜筒效果等。基本上由于镜头的形状缘故所有的图像都会产生变形,大多数软件开发人员可以在没有OEM(Original Equipment Manufacturer,原始设备制造商) 帮助的情况下使用棋盘格和基本公开的相机规格进行几何校正,如图1-5所示。

图1-5 对视觉信息进行校准示意图

光度校准:光度校准涉及很多底层的东西,通常要求OEM厂商参与。因为光度校准涉及图像传感器本身的细节特征以及内部透镜所用的涂层材料特性等,光度校准一般用于处理色彩和强度的映射。例如,正在拍摄遥远星星的望远镜连接的摄像机需要知道传感器上一个像素光强度的轻微变化是否确实是源于星星的光强变化,或者仅仅来源于传感器或透镜中的像差。光度校准对于AR跟踪器而言其好处是提高了传感器上的像素和真实世界中的点的匹配度,因此可使视觉跟踪具有更强的鲁棒性以及更少的错误。

当我们在考虑IMU时,一定要记住IMU是用来测量加速度而不是距离或速度的,这点很重要。距离是时间的二次方,IMU读取错误而得到的计算结果会随着时间的推移快速积累。校准和建模的目标就是确保距离测量具有足够的精度。理想情况下,使用IMU可以使摄像机具有足够长的时间来弥补由于用户覆盖镜头或者场景中发生其他事情所造成的视频帧跟踪的丢失。使用IMU测量距离的航迹推算是一种估测,但是通过对IMU的行为进行建模,找出产生错误的所有方式,然后通过编写过滤器来减少这些错误,可以使这个估测更加精确。想象一下如果用户向前走一步,然后就猜测用户走了几米的场景,仅凭一步就去估算会有很大的误差,但是如果重复上千步,那么对用户每步的估测与实际行走距离的估测最终会变得非常准确,这基本上就是IMU校准和建模的原理。

在IMU中会有很多错误来源,这都需要分析捕获并过滤。假设一个机器臂通常用于以完全相同的方式重复地移动设备,来自其IMU的输出会一直被捕获和过滤,直到IMU的输出能够和机器臂的实况移动十分精确地匹配,这就是一种校准与建模的过程。要想获得真正的精确度会比它听起来要更难,对于设备厂商而言,他们必须在它所有组合的全部设备中解决这些问题。谷歌公司和微软公司甚至将它们的设备发送到太空微重力环境中来消除额外的错误。

3D重建(3D Reconstruction)系统能够找出场景中真实物体的形状和结构,并且允许虚拟对象与真实物体发生碰撞并隐藏在真实物体的背后,如图1-6所示。要将虚拟对象隐藏在真实物体之后,前提是必须要对真实物体进行识别与重建。3D重建目前来看还有很多难点需要克服,当前很多AR Demos都不支持3D重建,因此AR中的虚拟对象看起来仅仅是在镜头中真实物体的前面移动而已。3D重建通过从场景中捕获密集的点云(使用深度相机或者RGB相机),然后将其转换为网格,并将隐形网格传递给3D引擎(连同真实世界的坐标),之后将真实世界网格精准地放置在相机所捕获的场景上,重建后虚拟对象就可以与现实世界互动。

图1-6  Magic Leap演示的遮挡效果

提示

3D重建在Hololens术语中叫作空间映射,在Tango术语中叫作深度感知。

三维重建的步骤如下。

(1) 图像获取:在进行图像处理之前,先要用摄像机获取三维物体的二维图像。光照条件、相机的几何特性等对后续的图像处理会造成很大的影响。

(2)摄像机标定:通过摄像机标定来建立有效的成像模型,求解出摄像机的内外参数,这样就可以结合图像的匹配结果得到空间中的三维点坐标。

(3)特征提取:特征主要包括特征点、特征线和区域。大多数情况下都是以特征点为匹配基元,特征点以何种形式提取与用何种匹配策略紧密联系。因此在进行特征点的提取时需要先确定用哪种匹配方法。特征点提取算法可以总结为:基于方向导数的方法,基于图像亮度对比关系的方法,基于数学形态学的方法三种。

(4)立体匹配:立体匹配是指根据所提取的特征来建立图像对之间的一种对应关系,也就是将同一物理空间点在两幅不同图像中的成像点一一对应起来。在进行匹配时要注意场景中一些因素的干扰,比如光照条件、噪声、景物几何形状畸变、表面物理特性以及摄像机机特性等诸多因素。

(5)三维重建:有了比较精确的匹配结果,结合摄像机标定的内外参数,就可以恢复出三维场景信息。由于三维重建精度受匹配精度、摄像机的内外参数误差等因素的影响,只有重建前各个环节的精度高,误差小,这样才能设计出一个比较精确的立体视觉系统。这里我们只了解一下3D重建的概念,本书中我们不会用到3D重建方面相关的知识。但3D重建在构建AR系统深度信息方面非常重要,3D重建是实现真实物体与虚拟物体间相互遮挡关系的基础。

ARCore 使用三个主要功能将虚拟内容与通过手机摄像头看到的现实世界整合:运动跟踪、 环境理解、光照估计。这三个功能是ARCore完成其他各项AR应用的重要的基础,也是ARCore技术框架中重要的支柱。

1.运动跟踪

在2D和3D空间中跟踪用户的运动并最终定位他们的位置是所有AR应用程序的基础。当我们的移动设备在现实世界中移动时,ARCore会通过一个名为并行测距与映射(Concurrent Odometry and Mapping,COM)的过程来分析手机相对于周围世界的位置。 ARCore会检测捕获到的摄像头图像中的视觉差异特征(称为特征点),并使用这些点来计算其位置变化。这些视觉信息将与设备IMU的惯性测量结果结合,一起用于估测摄像头随着时间推移而相对于周围世界的姿态(位置和方向)。如图1-7所示,在图中,当用户沿着白线移动时,沙发上特征点在屏幕中的位置也会发生变化,但通过COM可以反向计算出用户移动的位置。以前为了跟踪运动(位置),我们需要预先训练特征点,现在ARCore会实时的自动进行计算,这种跟踪技术是非常先进的,当然,它也有它的不足,后面我们会谈到。

图1-7 ARCore运动跟踪示意图

在开发中,通过将渲染3D内容的虚拟摄像头的姿态与ARCore提供的手机设备摄像头的姿态对齐,开发者就能够从正确的透视角度渲染虚拟内容,渲染的虚拟图像可以叠加到从设备摄像头获取的图像上,让虚拟内容看起来就像现实世界的一部分一样。

2.环境理解

ARCore通过检测特征点和平面来不断改进它对现实世界环境的理解。ARCore可以查找看起来位于常见水平或垂直表面(例如桌子或墙)上的成簇特征点,并让这些表面用于应用程序的工作平面,ARCore也可以确定每个平面的边界,并将该信息提供给应用,使用此信息将可以把虚拟物体置于平坦的表面上,如图1-8所示。由于ARCore使用特征点来检测平面,因此它无法正确检测像白墙一样没有纹理的平坦表面,这一点是由算法的底层设计决定的,除非附加其他算法,否则这个问题不可解。在后续的文章中我们将详细讨论环境理解的细节。

图1-8  ARCore通过检测特征点和平面来改进对环境的理解

在图1-9所示中我们看到的是一个通过网格识别的真实环境表面,这个平面由白点标识。在场景中,当ARCore识别出平面后,我们就可以将各种虚拟物体放置在表面上,看起来就像是真的把虚拟物体放置到了真实环境表面上一样。环境理解对于创建AR视觉表现来说是必不可少的。

图1-9  ARCore运动跟踪示意图

3.光照估计

ARCore可以检测设备所在空间环境光的相关信息,并提供给定摄像头图像的平均光照强度和色彩校正,利用这些光照信息,我们可以使用与周围环境相同的光照来照亮虚拟物体,让虚拟物体与周边真实物体光照看起来一致,提升虚拟物体的真实感。在图1-10所示中,处于强光中的猫与处于阴影中的猫颜色保持了与真实场景中光照的一致。利用ARCore,还可以估计光源的位置和光照方向,可以让虚拟物体产生与真实光照一样的阴影效果,进一步提升虚拟物体的真实感。

图1-10  ARCore放置在不同光照环境中的虚拟物体光照表现不一样

ARCore作为一个AR SDK,除了常见的AR术语,还有一些特定的概念。

1.手势操作

ARCore利用命中测试来获取对应于手机屏幕坐标(通过点按或应用支持的任何其他交互操作方式)的虚拟3D空间坐标,方法是通过射线检测将一条射线投射到摄像头的视野中,返回这条射线贯穿的任何平面或特征点,以及交叉位置在现实世界空间中的姿态。这让用户可以选择环境中的物体或者与它们互动。

2.特征点

借助特征点,可以将虚拟物体置于倾斜的表面上。当执行返回特征点的命中测试时,ARCore将查看附近的特征点并使用这些特征点估算表面在给定特征点处的角度,然后,ARCore会返回一个将该角度考虑在内的姿态。由于ARCore使用成簇特征点来检测表面的角度,因此无法正确检测像白墙一样没有纹理的表面。

3.锚点和可跟踪对象

姿态会随着ARCore改进它对自身位置和环境的理解而变化,当我们要放置一个虚拟物体时,需要定义一个锚点来确保ARCore可以跟踪物体随时间推移的位置,很多时候,需要基于命中测试返回的姿态创建一个锚点,以此来绑定虚拟物体与真实环境的位置关系。姿态会发生变化,这就意味着ARCore需要更新平面和特征点等环境物体随时间推移的位置。平面和特征点是一种特殊类型的物体,称为可跟踪对象,ARCore可以随着时间推移跟踪这些物体。我们可以将虚拟物体锚定到特定的可跟踪对象,确保虚拟物体与可跟踪对象之间的关系即使在设备移动时也能保持稳定。这意味着,如果将一个虚拟的Android机器人放在书桌上,即使ARCore稍后调整了与书桌关联的平面的姿态,Android 机器人仍会看起来像位于书桌上。

注意

处理器会实时地跟踪锚点并处理与锚点相关的大量计算,因此,应该尽可能重用锚点并在不需要时分离锚点,以减少CPU开销。

锚点描述了现实世界中的一个固定的位置和方向信息。为了保持在物理空间的固定位置,这个位置的数值描述将会随着ARCore对空间的理解的改进而更新。使用getPose()方法可以获取这个Anchor的当前数值信息,这些信息每次update()被调用的时候都可能改变,但不会自发地改变。

4.增强图像

使用增强图像可以构建能够响应特定2D图像(如产品包装或电影海报)的AR应用,用户可以将手机的摄像头对准特定图像时触发AR体验,例如,我们可以将手机的摄像头对准电影海报,使虚拟人物弹出,然后引发一个场景。ARCore可离线编译图像以创建图像数据库,也可以在应用运行时实时添加参考图像信息。

5.共享

借助ARCore的Cloud Anchors API,开发人员可以创建适用于Android和iOS设备的协作性或多人游戏应用。使用云锚点,一台设备可以将锚点和附近的特征点发送到云端进行托管,可以将这些锚点与处于同一环境中Android或iOS设备上的其他用户共享。这使应用可以渲染连接到这些锚点的相同3D对象,从而让用户能够同步拥有相同的AR体验。由于国内网络环境问题,我们可能无法正常使用Cloud Anchors API。

6.帧

帧(Frame)最直观的理解是照相机获取的一帧图像,ARCore背景渲染的画面就来自摄像头获取的图像帧。在ARCore中,Frame还包含更丰富的内容,它提供了某一个时刻AR的状态。这些状态包括:当前帧中的环境光照,如在绘制内容的时候根据光照控制虚拟物体绘制的颜色,使其更真实;当前Frame中检测到的特征点云和它的姿态;当前Frame中包含的Anchor和检测到的平面集合用于绘制内容和平面;手机设备当前的姿态、帧获取的时间戳、AR跟踪状态和摄像头的视矩阵等。

7.点云

ARCore在检测平面的时候,显示的一个个小白点,就是特征点云。特征点云包含了被观察到的3D点和可信值的集合,它还包含被ARCore检测时的时间戳。

8.平面

为了提高真实感,ARCore应用一般都会依托于平面(Plane)类进行渲染。如ARCore示例中的Android机器人,只有在检测到平面网格的地方才能放置。ARCore中平面可分为水平朝上、朝下、垂直平面类型,Plane描述了真实世界二维平面的信息,如平面的中心点、平面的xz轴、组成平面多边形的顶点。检测到的平面还分为三种状态,分别是正在跟踪、可恢复跟踪和永不恢复跟踪。如果是非正在跟踪的平面,包含的平面信息可能不准确。两个或者多个平面还可以被自动合并成一个父平面。

9.碰撞点

单击手机屏幕时,从设备单击处朝手机屏幕面向方向发出一条射线,射线检测与平面是否有碰撞点(PointCloudHitResult)。HitResult存储了射线检测的结果集,我们可以从中获取当前设备到有碰撞几何体的距离、碰撞点的姿态。如果是平面交集就是PlaneHitResult,如果是点云就是Point CludHitResult,PlaneHitResult中可以判断交集点是否在被检测的集合范围内,是否在平面的正面等。

10.姿态

姿态(Pose)表示一个对象的方向与位置,也用来描述从一个坐标系到另一个坐标系的转换。在所有的ARCore API中,姿态变换通常描述从物体的局部坐标系到世界坐标系的变换,也就是说,来自ARCore API的Pose可以被认为等同于OpenGL的模型矩阵或DirectX的世界矩阵。随着ARCore对环境的了解不断变化,它将调整坐标系模式以便与真实世界保持一致。这时,摄像机和锚点的位置(坐标)可能会发生明显的变化,以便使它们所代表的物体处在恰当的位置。因此,每一帧图像都应被认为是在一个完全独立的世界坐标空间中。

11.光照估计

光照估计(Light Estimate)给我们提供了一个接口来查询当前帧的光照环境。我们可以获取当前相机视图的像素强度,一个范围在[0.0,1.0]的值,0代表黑色,1代表白色。使用该光照属性绘制虚拟内容,可以使虚拟物体更真实。

12.会话

会话(Session)在ARCore中重要的功能是管理AR应用的状态、处理AR应用生命周期,是ARCore API的主要入口。在开始使用ARCore API的时候,通过设置的Config来检查当前设备是否支持ARCore。在Unity对应的生命周期方法中需要处理Session的生命周期,这样AR应用会根据需要开始或暂停相机帧的采集,初始化或释放相关的资源。

Session是ARCore API的一个类com.google.ar.core.Session,有自己的生命周期,处理开始和停止摄像头图像帧的获取。Session管理AR应用的全部状态,包括通过session.add(Pose)和session.removeAnchor(anchors)保存和删除跟踪的Anchor信息;session.getAllPlanes()返回被检测到的平面、当前投影矩阵等。当ARCore App退至后台,Untiy调用onPause()方法时,也需要通过session.pause()暂停Session来停止对摄像机图像的获取,当App呈现在前台时,onResume()方法中调用session.resume(Config)可以重新启用Session获取摄像机图像等。可以通过调用session.update()方法来获取最新的相机帧、更新设备的位置、更新被跟踪的Anchor信息、更新被检查的平面。在AR应用每一帧画面的渲染过程中,我们可以从Session中获取当前相机反馈的Frame,根据需要保存、获取和删除Anchor,获取系统检测到的所有平面集,设置纵横缩放比,获取投影矩阵(渲染3D效果)等用于渲染相关工作。

13.配置

在当前市场上,并不是所有的Android设备都支持ARCore,也并不是所有AR应用都需要用到ARCore提供的全部功能,这时候我们就要用到Config,它保存了用于配置Session的相关设置。在使用ARCore之前,需要创建Config,检查当前设备是否支持ARCore。默认Config开启平面检测、光照估计、帧速率适配。配置文件主要包括是否与摄像机帧速匹配Match Camera Framerate;平面检测系统的行为Config.PlaneFindingMode,包含开启和禁止平面检测、水平与垂直平面检测等;是否开启光照估计Enable Light Estimation,包含开启或者禁止光照估计;增强图像数据库Augmented Image Database,这主要用于图像识别;摄像机对焦模式Camera Focus Mode,用于提高平面检测速度和平台计算机视觉的图像分辨率;是否启用云锚点Enable Cloud Anchor;是否启用人脸增强Augmented Face Mode。

在介绍完前面的基础知识之后,从本节开始,我们将正式开始从无到有开发我们的AR应用,但是在开始我们的创作之前,还需要先把开发环境搭建好,这是基础中的基础。我们将采用Unity平台、利用ARCore SDK来开发AR应用。

由于Android系统的碎片化和之前我们讨论过的要对设备的摄像头模块与IMU进行校准的缘故,ARCore并非完全支持所有型号的手机,手机厂商需要对手机进行校准与适配才能支持ARCore,目前支持ARCore的手机及平板已有近百款,但国产手机支持的还比较少,当前只有华为和小米的若干款手机支持,支持列表见表1-1。与Android手机不同的是,iPhone SE及以上的所有型号手机、平板都支持ARCore。

表1-1 国内支持ARCore的手机

制造商 型号
小米 Mix 2S
小米8、小米8 SE
Mix 3
华为 P20、P20Pro
P30、P30Pro
Porsche Design Mate RS、Porsche Design Mate 20 RS
荣耀10、荣耀Magic 2.荣耀v20、
麦芒7
Nova3、Nova 3i
Mate 20、Mate20 Pro、Mate 20 X
Nova 4
三星 Galaxy Note 9
Galaxy S9、Galaxy S9+

目前国内手机上支持ARCore还是比较小众的。当然,由于Google特别是Android的巨大影响力,相信过不了一两年,支持ARCore的手机将会出现爆发性地增长(注:华为Honor 8X; 小米Pocophone F1;Vivo的NEX Dual Display Edtion、NEX S;一加的3T、5.5T、6.6T;Oppo R17 Pro这十款手机在美国地区是支持ARCore的) 。

详细的手机支持列表,读者可以在ARCore支持设备页面查看。这是一个及时更新的页面,随着时间的推移,支持的手机设备肯定会越来越多。

开发AR应用需要众多的软件协作,为了演示需要,我们的项目也需要些模型,建议读者将所有内容下载到一个目录中,以便下一步操作。我们接下来的开发基于Windows操作系统,所有软件请选择Windows相应版本。  

1.ARCore

谷歌公司 对ARCore的更新速度非常快,基本保持两个月一更新的频率,目前已到了v1.9版本了,ARCore可以在Google Play中下载,国内用户可以在华为的应用市场中搜索ARCore找到。目前也是v1.9版,华为应用市场ARCore下载界面如图1-11所示。

图1-11 华为应用市场ARCore SDK下载页面 

提示

ARCore_vxx.apk文件也可以从ARCore官方GitHub上直接下载,ARCore_vxx.apk包会随ARCore SDK for Android一并发布。选择所需版本下载即可。

2.Android Studio

我们可以通过安装Android Studio来使用Andriod SDK,安装Andriod SDK后还可以使用Android Studio的模拟器,最新的Android Studio版本是v3.2.1,可以在其官网中下载,https://developer.android.google.cn/studio/。请下载的版本确保在v3.0以上,因为ARCore需要Android SDK v7.0 (API Level 24) 或者更高版本上运行,否则将会出现不能运行ARCore的情况。Android Studio打开后的运行界面如图1-12所示。

图1-12  Android Studio界面

3.JDK

JDK(JavaSE Development Kit),JDK是Sun Microsystems为Java开发的产品,是用于构建应用程序、applet程序和运行Java编程言语的组成部分,现如今JDK已经成为运用最广泛的Java SDK,JDK是整个Java的核心,包含了Java运行环境,Java工具和Java基础的类库。用户可以在其官网下载,下载时勾选Accept License Agreement,选择所使用的版本,如图1-13所示。

图1-13 JDK下载选择界面

4.Unity

我们所采用的开发平台为Unity,所以我们需要下载Unity,目前的最新稳定版本是v2018.3.0f2,官网的下载地址是https://store.unity.com/cn/download?ref=personal,我们下载其下载助手后,利用下载助手帮我们下载我们需要的组件。因为我们需要发布Android应用,所以在Unity下载助手中选择下载内容时,务必选中Android Build Support复选框,其他项读者根据需要选择(从Unity v2019开始,Unity使用了Unity Hub来下载安装程序,Unity Hub作用与Unity Assistant类似,操作也相似)。图1-14所示为利用Unity Assistant下载安装Unity图示,图1-15为Unity功能组件选择示意图,开发Android应用,需要勾选Android Build Support,开发IOS应用,则需要勾选iOS Build Support。

图1-14 利用Unity Download Assistant下载Unity及其组件

图1-15 在Unity Download Assistant中选择所需的组件

5.ARCore Unity SDK

目前Uinty平台的ARCore SDK也是v1.9,我们可以选择Clone or download打包下载成ZIP文件,如图1-16所示。

图1-16 选择下载成ZIP压缩包

也可以采用命令行的方式直接下载到本地:

代码清单1-1

1. git clone https://github.com/google-ar/ARCore-unity-sdk.git

该命令在执行命令时所在的文件夹下新建ARCore-unity-sdk文件夹,并将GitHub上的ARCore Unity SDK下载到该文件夹内。

也可以从ARCore官方GitHub上直接下载ARCore-unity-sdk-vxxx.unitypackage,选择所需要的版本下载即可,如图1-17所示。

图1-17 直接下载ARCore-unity-sdk-vxxx.unitypackage

6.模型文件

狐狸模型下载地址:https://download.csdn.net/download/yolon3000/10599469。

在前一节准备好的6个资源中,ARCore是基础构件,需要安装在手机上(部分支持ARCore的手机预装了ARCore,而另外一些则没有预装,这就需要用户在运行任何ARCore应用之前先安装ARCore),ARCore Unity SDK是在Unity开发中使用的SDK,模型也是在开发中使用的。所以需要安装的软件是Android Studio、JDK和Unity。

1.Android Studio的安装

在Windows中安装Android Studio很简单,双击下载好的Android Studio exe文件即可开始安装,然后只需要根据提示一步一步操作即可,需要注意的一点是Android Studio的安装路径,因为在后面的Unity开发中我们需要使用Android SDK,需要知道具体的路径(Unity也会自动检查),如图1-18所示。

图1-18  Android Studio安装路径选择界面

2.JDK的安装

在Windows中安装JDK也很简单,先解压下载好的JDK.zip文件,双击解压后的.exe文件即可开始安装,需要注意JDK的安装路径,后面设置Unity时需要这个路径地址,如图1-19所示。

图1-19 JDK安装路径选择界面

3.Unity的安装

Unity的安装可以与下载一并进行,Unity的Unity Assistant简化了这个过程,下载并启动Unity Assistant后会自动下载安装Unity及选择的功能组件,还要提醒大家的是,我们一定要选择Android Build Support,这样才能开发Android应用。Unity的安装界面如图1-20所示。

图1-20 使用Unity Download Assistant下载并安装Unity及其组件示意图

在前一节中,我们已经安装了所需要的软件,但在编译发布一个项目之前,我们还需要再设置一些参数、作一些配置以确保我们的AR应用能在Android手机上正确地运行。

现在我们从头开始创建一个新的项目,并设置相应的ARCore开发参数来启动和运行AR项目。在开始菜单或者桌面找到Unity图标,双击启动Unity程序,在启动后的主界面单击New新建一个项目,并命名项目为Fox,然后单击Create project按钮,如图1-21所示,这将新建一个Unity项目。稍等片刻便会打开Unity主界面(我们假定读者熟悉Unity的基本操作,如读者理解有困难,请查阅Unity相关资料)。

图1-21 在Unity中新建项目

待Unity主窗口打开后,按Ctrl+Shift+B快捷键,或者在菜单栏中选择File→Build Settings,打开设置窗口。选择Platfor下的Android选项,然后单击Switch Platform按钮切换到Android平台。当Unity标志出现在Android选项旁边时,发布平台就切换成Android了。单击Player Settings按钮继续后续设置,如图1-22所示。

图1-22 在将目标平台切换成Android后,单击Player Settings…进行后续设置

查看Unity Inspector窗口,单击 Android小图标选项卡,在Other Settings下,单击Multithreaded Rendering复选框,以确保它不被选中的(取消多线程渲染),如图1-23所示。

图1-23 取消Multithreaded Rendering勾选 

如图1-24所示,在Company Name和Product Name中输入公司和程序包名,同时,在Identification→Package Name中要输入一样的公司和程序包名,这个值要求唯一,因为如果它与另一个应用程序具有相同的包名,可能会导致问题发生。另外,我们还需要设置与ARCore兼容的Android最低版本,找到Minimun API level选项,单击其下拉菜单,选择Android7.0 nougat (API level 24)或以上,正如这个选项名字一样,应用程序与ARCore将不会在Nougat版本之前的Android设备上运行。同时,我们还需要设置一下Target API Level,这里设置的是 Android 8.0 Oreo (API level 26),因为笔者的测试手机就是这个版本,读者可以根据自己的需要设置,但目标版本不得低于Nougat,不然开发的AR应用将无法运行,如图1-24所示。

图1-24 设置Package Name、Minimum API Level和Target API Level

在完成以上设置后,单击Other Settings文字以收起Other Settings设置折叠栏,然后单击XR Settings展开,选中ARCore Supported复选框以确保应用得到ARCore的支持,如图1-25所示。

图1-25 勾选ARCore Support,确保开发的AR应用能得到ARCore的支持

完成上述设置后,在Unity菜单中单击 Edit→Preferences,打开Unity Preferences对话框,如图1-26所示,选择External Tools选项卡。在这里,我们不仅可以设置使用的代码开发IDE、图片编辑器,最重要的是还可以设置Android SDK和JDK的路径。在设置时确保路径正确(即前文我们强调过的Andrid Studio与JDK安装路径),否则将无法正确编译生成Android应用(这个设置只需要操作一次,与应用开发项目无关,即设置完一次后再创建新应用也不必再次设置)。

图1-26 设置代码编辑器、SDK和JDK

在完成开发环境设置后,还需要导入ARCore Unity SDK工具包。保持Unity处于打开状态,找到我们之前下载的 ARCore-unity-sdk-v1.x.0.unitypackage(x代表子版本号),双击它打开资源导入对话框,如图1-27所示。或者在Unity主界面Project窗口中,在Project→Assets上单击鼠标右键,选择Import Package→Custom Package,选择ARCore-unity-sdk-v1.x.0.unitypackage,也一样可以打开资源导入对话框。直接选择Import导入全部资源,稍候片刻,Unity将会把我们需要的ARCore SDK导入到Unity项目中。

图1-27 导入ARCore Untiy SDK

至此,我们已经将软件环境及开发设置都处理好了,下步我们将真正开始利用ARCore开发AR应用了。

在上一节中,我们已经做好了所有的前期准备工作。这一节先来编译运行一下ARCore自带的示例,一方面是测试一下我们的开发环境看有没有问题,另一方面是实际体验一下AR给我们带来的奇妙感受。打开上节Unity Fox项目,在Project窗口中,找到Project→Assets→GoogleARCore→Examples→HelloAR→Scenes,打开HelloAR.unity,不做任何修改,直接按Ctrl+Shift+B组合键(或者选择File→ Build Settings)打开Build Settings对话框。在打开的对话框中保证选中当前场景,单击Build按钮,设置发布后的程序名,最后单击“保存”按钮开始编译生成apk文件,如图1-28所示。

图1-28 生成apk文件

如果编译没有错误将生成apk文件,将发布后的apk复制到手机上安装运行(手机上需要安装我们上节下载的ARCore.apk),移动一下手机,在检测到的平面上放置Andy机器人,将看到如图1-29所示的运行效果。

图1-29 在水平平面与垂直平面上放置Andy机器人

上节中,我们将发布后生成的.apk文件复制到手机上运行,这样非常不方便,也不利于快速调试。本节中,我们将设置手机的开发者模式,这样可以使用USB或者WiFi来调试应用,同时本节还将介绍使用计算机监视手机运行的AR应用,这也是查找、排除问题的有效手段。

为了方便将我们编译发布的Android包文件(.apk文件)发布到手机中并测试它,我们需要启用手机的Developer Option,即开发者选项。开发者选项,顾名思义,就是供应用开发者开发调试应用的选项,通过这个功能开发者就可以用USB连接手机,直接在手机硬件上安装、调试自己的应用程序。通过开发者选项可以在计算机和设备之间复制数据,在设备上安装应用而不发送通知以及读取日志数据。当然,也可以通过USB,查看到手机中Android系统的一些数据和信息。默认情况下,开发者选项”是隐藏的,但是可以通过连续7次单击手机中的“设置→关于手机→软件信息→编译编号”来解锁“开发者选项”(需要输入手机密码才能完成)。单击时会有“您还差x步就可以打开开发者模式”的提示。在“开发者选项”中,我们还需要打开“USB调试”,这样我们才能使用USB在手机上调试运行的AR应用,如图1-30所示,通过连续单击图1-30a中的“编译编号”解锁显示开发者选项,单击图1-30b中的开发者选项并输入密码打开“开发者选项”,打开图1-30c中的“USB调试”启用USB调试功能。

注意

不同的手机打开“开发者选项”的方式不一样,这个跟手机厂商有关,但操作大致相同,可以查看手机使用说明获取相关帮助。

图1-30 在“开发者选项”中打开“USB调试”选项单击单击

“开发者选项”中有很多参数,这些设置对应用开发者了解应用的运行状态很有帮助,详细的参数及功能说明超出本书的范围,读者可以自行探索并深入了解在后面用到的时候我们也会作简单介绍。打开“开发者选项”之后我们就可以通过USB连接手机与计算机来调试我们的应用了。

使用USB来调试AR应用确实方便了很多,但AR应用测试需要四处移动手机,使用有线的方式还是会有束缚,同时,频繁的插拔USB线还会导致USB接口损坏,而且长期给手机充放电也会损害手机电池性能,下面我们介绍利用WiFi来调试AR应用。

首先我们需要使用的工具是adb,这是个应用工具,它在Android SDK安装目录下的platform-tools子目录内,adb基于TCP协议,使用WiFi来调试应用需要手机操作系统的ROOT权限,我们可以在手机上下载安装Android Terminal Emulator作为辅助。

设置手机WiFi调试的步骤如下。

(1)首先打开Android手机对指定端口的监听。

这一步需要使用Shell,因此手机上要有终端模拟器,打开安装的Android Terminal Emulator终端,依次输入下列几行命令,如代码清单1-2所示。

代码清单1-2

1.su           //获取root权限
2.setprop service.adb.tcp.port 7890  //设置监听的端口,端口可以自定义,如7890,5555是默认的
3.stop adbd    //关闭adbd
4.start adbd   //重新启动adbd

(2)手机连接WiFi并记下手机的IP地址。

在手机界面,依次单击设置→关于手机→状态项,然后在打开的界面上可以查看手机连接WiFi时分配的IP地址,笔者手机IP为192.168.2.107,如图1-31所示。

图1-31 在手机上查看分配的IP信息

(3)在计算机上打开命令提示符,然后输入如代码清单1-3所示的命令。

代码清单1-3

1.adb connect 192.168.2.107:7890    //如果不输入端口号,默认是5555,自定义的端口号必须写明,对应第1步中自定义的端口号,例如:192.168.2.107:7890

(4)如果命令行显示:“connected to XXXXXXX”,说明配置成功了,如代码清单1-4所示。然后就可以调试程序了。

代码清单1-4

1.C:\Users\Root>adb connect 192.168.2.107:7890
2.connected to 192.168.2.107:7890

如果需要关闭WiFi调试,只需将端口号设置为-1,并且重复第一步即可。

有了WiFi调试,我们就可以摆脱USB线的束缚了,这样可以更方便我们调试AR应用。

在使用USB或者WiFi调试应用时,将AR应用编译并推送到手机上需要花费很长的时间。但AR应用在手机端运行之后,我们只能看到运行的结果而不能确切地知道在运行过程中发生的事情。作为开发者,我们需要知道AR应用在运行过程中到底发生了什么,并利用这些信息来调试我们的应用。本节将介绍远程调试AR应用的方法。但在开始之前,为便于以后的工作,我们将Android SDK目录添加到操作系统的环境变量中,打开Windows控制面板,导航到控制面板→系统和安全→系统→高级系统设置,打开“高级系统设置”对话框,将Android SDK和JDK目录添加到环境变量中(这是个可选步骤,但建议设置,防止在应用编译过程中发生找不到相应的动态连接库的问题),如图1-32所示。

图1-32 将Android SDK和JDK目录添加到环境变量中

接下来,通过执行以下操作,就能使用计算机远程查看运行中的AR应用程序运行状况了。

首先通过USB或者WiFi连接到移动设备,然后打开Android SDK安装目录,进入到SDK目录下的Tools子目录内(或者在C:\Users\Administrator\AppData\Local\Android\Sdk\tools目录,本路径仅供参考)。双击monitor.bat文件,打开Android Device Monitor(Android设备监视器),其左侧列表中的设备即为此时连接到计算机的移动设备,选择需要调试的设备,在logcat窗口将看到设备的日志输出流,拖动logcat窗口,使其成为主窗口中的一个选项卡,如图1-33所示。

图1-33 使用Android Device Monitor查看AR应用信息

注意

如果采用WiFi的模式调试,需要确保开发计算机与手机设备在同一子网中。

至此,我们可以构建、部署和远程调试我们的AR应用了,这给我们开发应用带来了足够的灵活性和弹性,在Logcat窗口中我们可以看到应用程序的Debug.log输出。远程调试连接将与Android Studio一起工作为我们带来足够的方便。但是,这里的输出信息太多了,而且很多信息我们都不关心,因此我们需要设置一下以获取特定的我们关心的日志消息。

为了得到我们关心的信息并屏蔽掉其他无用信息,我们对日志消息进行过滤,过滤的操作步骤如下所示。

(1)打开Android设备监视器窗口。

(2)单击Logcat →Saved Filters 面板中的绿色加号按钮来创建过滤器。

(3)通过输入过滤器名称(Unity)和日志标记(Unity)创建一个新的过滤器。

(4)单击“确定”添加筛选器。

通过过滤器,我们将不关心的信息过滤掉了,日志消息将更加清爽,如图1-34所示。在图上我们还可以看到,除了使用日志标记(Log Tag)创建过滤器外,我们还可以通过日志消息(Log Message)、PID、应用名字(Application)、日志等级(Log Level)等来创建过滤器,或者联合其中的两项或多项创建复杂的过滤器,使过滤出来的信息更能符合我们的要求。

图1-34 使用过滤器过滤日志消息

现在我们有了一个带有远程连接和调试支持的Unity工作环境了,这将使我们的工作更容易进行,至此,我们已经做好了一切准备,即将进入AR开发的殿堂。

在前面,我们已经运行了ARCore提供的示例,虽然示例演示很成功,但其实我们对ARCore一无所知,不知其然也不知其所以然,所以,从本节开始,是时候为Android设备构建增强现实应用程序的框架了,我们将一步一步地建立我们自己的AR应用,开始探索ARCore带来的神奇世界。学习如何开发AR应用的旅程可能是一条漫长而艰难的道路,特别是AR应用涉及太多的3D数学、渲染管线、计算机视觉、图形处理方面的知识,幸运的是,先驱们已经为我们做了很多工作,将很多原本复杂烦琐的细节给封装了起来,提供了良好的开发接口,AR开发的门槛已经大大降低了。了解底层的细节可以让我们的开发进行得更加得心应手,但这并不是必须的,虽然AR应用开发是一个非常复杂的系统集成,但现在正处于一个每个人都可以进行AR开发的阶段,也正是学习AR开发的绝佳时间,接下来我们将会一步一步地带领大家开发属于我们自己的AR应用。

Unity的场景(Scene),顾名思义,就是存储在一个场景中所有资源的集合,一个应用可以根据其需要构建单个场景或多个场景,一个场景就像话剧的一幕,也是我们经常玩的游戏中的一个关卡。所有的工作都必须依托于场景来构建。Unity它虽然是作为一个游戏引擎来创建的,但随着增强现实和虚拟现实已经开始渗透到开发的方方面面,现在最好把Unity看作一个3D引擎而不仅仅是一个游戏引擎。

下面我们来创建场景,打开我们在上节中新建的Fox工程,为了便于统一管理,我们在Project窗口中,右键单击Assets选项,在弹出的菜单中依次选择Create→Folder选项,将会新建一个文件夹,我们把文件夹命名为Scenes,如图1-35所示。

图1-35 在Unity中新建一个文件夹

提示

为了更好的管理项目中的各类资源,最好将资源按照分类存放在不同的文件夹中,如Scenes、Prefabs、Audio、Video、Materials、Textures、Sprites、Scripts、Models等等,特别是如果项目很复杂,还应该在这些文件夹下建子文件夹,层次化管理资源会给后面的开发省很多事。一种比较省事的做法是在一个空项目里建好各类文件夹并打包导出Unitypackage,以后在需要的时候直接导入这个包,这样就不用每次都手工去创建这些文件夹了。

在Unity中,选择菜单File →New Scene(或者直接按Ctrl+N组合键),这时Unity会新建一个空的场景。新建的场景并没有保存,最好先将其保存到文件夹中,选择菜单File→Save Scene,这将打开保存场景对话框,选择好要保持的路径(保存到我们刚建的Scenes文件夹中),并命名场景为Fox.unity,如图1-36所示。

图1-36 在Unity中新建并保存一个场景

通过上面的操作场景建好了,在建好的场景中默认有一个主摄像机和一个平行光,除此之处什么都没有,下面我们将添加ARCore SDK的关键模块。

在Unity中,有一种对象叫Prefabs预制件,所谓预制件其实就是一个模块,这个模块包含了GameObjects及其相关联的组件、属性、动作、模型、纹理等,这是一个功能块,这种设计使得模块可以非常方便地被复用。在我们引入ARCore的相关Prefab前,我们把场景中的Main Camera和Directional Light删除,因为ARCore中已经有这两个GameObject了。在Hierarchy窗口,选中Main Camera、Directional Light,单击鼠标右键选择Delete选项(或者在选择后直接按键盘上的Delete键删除),如图1-37所示。

图1-37 删除新场景中的默认的Main Camera、Directional Light

在Project窗口中,依次展开GoogleARCore → Prefabs,在Prefabs目录下,选择ARCore Device和 Environmental Light,并将这两个prefabs拖动到Hierarchy窗口中,如图1-38所示。

图1-38 添加ARCore预制件ARCore Device、Environmental Light

现在我们已经把ARCore两个最重要的Prefabs加入到场景中了。ARCore Device负责处理所有与设备相关的事宜,包括更新Device的位置、更新Anchor的位置、更新检测到的平面等。Enviromental Light则处理所有光照相关事宜,如光照强度估计、光源方向估计等。在添加完这两个Prefabs后我们还需要添加事件系统以使我们的应用能监听和处理事件信息。在Hierarchy窗口的空白处单击右键,在弹出的菜单中依次选择GameObects→UI →Event System,这就为我们的场景添加了事件系统,如图1-39所示。

图1-39 在场景中添加Event System,以便应用能响应各类事件

在本示例中我们将使用我们自己的模型,所以我们需要导入之前下载的狐狸模型。在Project窗口中,单击鼠标右键,在弹出的菜单中依次选择Assets →Import Package →Custom Package,如图1-40所示,在打开的对话框中定位到之前下载的Fox.unitypackage文件,单击确认后会打开资源导入窗口,不做任何修改,直接单击Import,这将把狐狸模型导入到工程中,如图1-41所示。

提示

Unity支持多种模型文件格式,如.max、.mb、.jasl、.dae、.fbx、.3ds等,但它并不是对每一种外部模型的属性都支持,如有的格式材质或者动画Unity不支持,通常建议模型使用.fbx格式。

图1-40 导入自定义用户包

图1-41 导入自定义用户包界面

至此,我们的场景已经搭建得差不多了,是不是感觉场景中没东西?这就是AR与其他应用开发的最大不同,AR的场景就是取自于摄像头的实景,也正因为如此,在Game窗口中,我们没办法测试开发中的AR应用。

我们已经在场景中添加了AR应用开发必要的支持组件,但现在场景中什么也没有,本节中,我们将要编写应用控制器(App Controller),利用ARCore提供的功能来做平面检测。当然,这是一个循序渐进的过程,我们首先要确保设备支持ARCore,并进行一些必要的检查以便我们后续工作的开展。本节中,我们的目标是创建一个简单的AppController,主要是进行各种检查及错误处理。当然主要的工作还是建立在ARCore基础之上,ARCore已经帮助我们处理了很多细节问题,场景中的ARCore Device主要处理了两大类问题:跟踪用户手机姿态和Session管理。跟踪用户手机姿态处理手机的类型、姿态并实时更新。Session管理AR应用系统的状态。ARCore Device包含了一个第一人称摄像头。

既然我们要编写控制器,那么肯定要用到程序语言,如今市面上至少也有几百种程序语言。当前,在AR领域,C#是最佳的编程语言,当然也还有很多如JavaScript、C++、Java也都可以用来开发AR应用。本书将主要以C#作为我们的代码编写语言,而且C#也是Unity中主要的编程语言,而Unity又是应用最广泛的AR开发引擎。

在上一节中,我们创建了一个场景,现在我们要创建一个控制器,以便来开发和测试应用,我们将从头开始一步一步的创建它。首先,我们要创建一个C#类。

在创建C#类之前,我们先建一个文件夹用于存放管理代码文件。Project窗口中,在Assets上单击右键,在弹出的菜单中依次选择Create →Foldert选项,如图1-42所示,这将创建一个文件夹,将其命名为Scripts,以后所有的代码文件我们都放在这个文件夹里。

图1-42 新建Scripts文件夹

然后我们创建脚本文件(AppController类),在我们创建的Scripts文件夹上单击鼠标右键,在弹出的菜单中依次选择Create →C# Scripts选项,如图1-43所示,将创建的脚本文件命名为AppController.cs。

图1-43 新建AppController C#脚本文件

好了,现在我们已经创建完AppController类,双击AppController.cs会在 IDE(Integrated Development Environment,集成开发环境)中打开这个类,IDE可以在Edit→Preferences→External Tools→External Script Editor中设置(如不清楚请查阅前文设置章节),默认设置是monodevelop,我们使用Visual Studio 2015来开发。

在打开的代码编辑器中,IDE默认已经生成了一部分代码,如代码清单1-5所示。

代码清单1-5

1.using System.Collections;
2.using System.Collections.Generic;
3.using UnityEngine;
4.public class AppController : MonoBehaviour {
5.    // Use this for initialization
6.    void Start () {     
7.    }   
8.    // Update is called once per frame
9.    void Update () {        
10.    }
11.}

上述代码中,Start()方法会在挂载脚本的物体开始时执行一次,而Update ()方法则会在AR运行的每帧都更新。首先,我们需要添加ARCore的命名空间,如代码清单1-6所示。

代码清单1-6

1.using GoogleARCore;

添加这个命名空间后,代码看起来应该如图1-44所示,在引用这个命名空间后,我们就能调用Google ARCore SDK提供的功能模块了。

图1-44 在脚本中引用ARCore的命名空间

代码清单1-5第4行是定义了一个类,这个类继承自MonoBehaviour,不必太在意这个,大部分的Unity类都会继承自这个类,这个基类包含了很多Unity独特的方法和功能定义。

如前所述,Start()方法会在挂载脚本的物体开始时执行一次,所以我们可以将设备检查工作的代码写在这里,为了更好地管理我们的代码,我们另建一个方法专门负责设备检查工作,例如让用户授权摄像头的使用、弹出检查结果提示、遇到错误退出等。这样我们的代码逻辑将会更清晰、AR应用更友好和健壮。在AppController.cs中我们新建一个方法OnCheckDevice(),如代码清单1-7所示。

代码清单1-7

1.using System.Collections;
2.using System.Collections.Generic;
3.using UnityEngine;
4.using GoogleARCore;
5.public class AppController : MonoBehaviour {
6.    // Use this for initialization
7.    void Start () {
8.        OnCheckDevice();
9.    }
10.    // Update is called once per frame
11.    void Update () {
12.    }
13.    /// <summary>
14.    /// 检查设备
15.    /// </summary>
16.    private void OnCheckDevice()
17.    {
18.        if(Session.Status == SessionStatus.ErrorSessionConfigurationNotSupported)
19.        {
20.            ShowAndroidToastMessage("ARCore在本机上不受支持或配置错误!");
21.            Invoke("DoQuit", 0.5f);
22.        }
23.        else if (Session.Status == SessionStatus.ErrorPermissionNotGranted)
24.        {
25.            ShowAndroidToastMessage("AR应用的运行需要使用摄像头,现无法获取到摄像头授权信息,请允许使用摄像头!");
26.            Invoke("DoQuit", 0.5f);
27.        }
28.        else if (Session.Status.IsError())
29.        {
30.            ShowAndroidToastMessage("ARCore运行时出现错误,请重新启动本程序!");
31.            Invoke("DoQuit", 0.5f);
32.        }
33.    }
34.    /// <summary>
35.    /// 退出程序
36.    /// </summary>
37.    private void DoQuit()
38.    {
39.        Application.Quit();
40.    }
41.    /// <summary>
42.    /// 弹出信息提示
43.    /// </summary>
44.    /// <param name="message">要弹出的信息</param>
45.    private void ShowAndroidToastMessage(string message)
46.    {
47.        AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
48.        AndroidJavaObject unityActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
49.        if (unityActivity != null)
50.        {
51.            AndroidJavaClass toastClass = new AndroidJavaClass("android.widget.Toast");
52.            unityActivity.Call("runOnUiThread", new AndroidJavaRunnable(() =>
53.            {
54.                AndroidJavaObject toastObject = toastClass.CallStatic<AndroidJavaObject>("makeText", unityActivity,message, 0);
55.                toastObject.Call("show");
56.            }));
57.        }
58.    }
59.}

简单解释一下代码逻辑,在OnCheckDevice()方法中,我们首先检查用户手机设备是否支持ARCore,然后检查是否获取到摄像头的授权,最后再确认一下有没有发生错误。在检查执行中,我们用ShowAndroidToastMessage()方法弹出检查中需要让用户知道的信息,如果出现严重错误导致应用不能再进行下去,我们则使用DoQuit()方法主动退出应用。

ShowAndroidToastMessage()方法的主要功能就是在用户屏幕上显示信息。

经过设备检查,如果没有出现问题,那么我将进入AR应用循环,在Update()中,我们将开始应用的主流程。同样为了更清晰地管理我们的应用流程,我们另建一个方法来管理应用的生命周期。新建一个方法UpdateApplicationLifecycle()。在UpdateApplicationLifecycle()中,我们主要做两件事,一件事是检查当前设备是否是处于有效跟踪状态,如果当前设备目前不是正在跟踪,那么我们让应用休眠一小段时间再检查,如果当前设备目前正处于有效跟踪状态,说明应用正在有效执行中,我们则不要让应用休眠,而是一直跟踪用户设备。当然,如果当前应用处于正处在退出状态,那我们则不再进行下一步的操作,如果应用正常,那我们则进入到应用的主处理环节中,具体代码如代码清单1-8所示(注意新加了一个私有变量mIsQuitting,用来标识当前应用程序状态):

代码清单1-8

1.using System.Collections;
2.using System.Collections.Generic;
3.using UnityEngine;
4.using GoogleARCore;
5.public class AppController : MonoBehaviour {
6.    private bool mIsQuitting = false;
7.    // Use this for initialization
8.    void Start () {
9.        OnCheckDevice();
10.    }
11.    // Update is called once per frame
12.    void Update () {
13.        UpdateApplicationLifecycle();
14.    }
15.    /// <summary>
16.    /// 检查设备
17.    /// </summary>
18.    private void OnCheckDevice()
19.    {
20.        if(Session.Status == SessionStatus.ErrorSessionConfigurationNotSupported)
21.        {
22.            ShowAndroidToastMessage("ARCore在本机上不受支持或配置错误!");
23.            mIsQuitting = true;
24.            Invoke("DoQuit", 0.5f);
25.        }
26.        else if (Session.Status == SessionStatus.ErrorPermissionNotGranted)
27.        {
28.            ShowAndroidToastMessage("AR应用的运行需要使用摄像头,现无法获取到摄像头授权信息,请允许使用摄像头!");
29.            mIsQuitting = true;
30.            Invoke("DoQuit", 0.5f);
31.        }
32.        else if (Session.Status.IsError())
33.        {
34.            ShowAndroidToastMessage("ARCore运行时出现错误,请重新启动本程序!");
35.            mIsQuitting = true;
36.            Invoke("DoQuit", 0.5f);
37.        }
38.    }
39.    /// <summary>
40.    /// 管理应用的生命周期
41.    /// </summary>
42.    private void UpdateApplicationLifecycle()
43.    {
44.        if (Session.Status != SessionStatus.Tracking)
45.        {
46.            const int lostTrackingSleepTimeout = 15;
47.            Screen.sleepTimeout = lostTrackingSleepTimeout;
48.        }
49.        else
50.        {
51.            Screen.sleepTimeout = SleepTimeout.NeverSleep;
52.        }
53.
54.            if (mIsQuitting)
55.        {
56.            return;
57.        }
58.    }
59.    /// <summary>
60.    /// 退出程序
61.    /// </summary>
62.    private void DoQuit()
63.    {
64.        Application.Quit();
65.    }
66.    /// <summary>
67.    /// 弹出信息提示
68.    /// </summary>
69.    /// <param name="message">要弹出的信息</param>
70.    private void ShowAndroidToastMessage(string message)
71.    {
72.        AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
73.        AndroidJavaObject unityActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
74.        if (unityActivity != null)
75.        {
76.            AndroidJavaClass toastClass = new AndroidJavaClass("android.widget.Toast");
77.            unityActivity.Call("runOnUiThread", new AndroidJavaRunnable(() =>
78.            {
79.                AndroidJavaObject toastObject = toastClass.CallStatic<AndroidJavaObject>("makeText", unityActivity,message, 0);
80.                toastObject.Call("show");
81.            }));
82.        }
83.    }
84.}

至此,我们已经利用ARCore提供的功能搭建了应用程序框架,下节我们将开始做平面检测以及在合适的位置放置模型。

在前一节中,我们创建了一个App Controller控制脚本,构建了我们的AR应用框架来做应用程序的整体流程处理。但是,如果此刻运行我们前面的框架,什么也不会看到,本节中,我们将使用摄像机生成的点云数据来检测和创建平面,同时我们还要可视化检测出来的平面,帮助用户确认一个可用的工作面。

前面我们介绍过Prefabs预制件,当检测到真实世界中的平面时,我们需要一种在虚拟空间中表示这一平面的方法,这就是可视化虚拟平面,即将ARCore检测发现的平面以一种人眼可见的方式展现出来,帮助用户放置虚拟物体和进行下一步操作。为了使用代码来创建平面,我们也要制作一个平面的Prefabs,当检测到更多真实世界的平面时,我们还要实例化并附加更多的Prefabs,以使可用的虚拟平面更大。

在Hierarchy窗口的空白处单击鼠标右键,在弹出的菜单中,依次选择Create → 3D Object→Plane选项,新建一个平面,命名为VisualDetectedPlane,如图1-45所示。

图1-45 创建一个平面对象

在这里,我们需要特别提醒的是,在VisualDetectedPlane平面的Inspector窗口中, Position的值一定要归0,同时还要确保Scale值为(1,1,1),如果这里有少许偏移,那么在用代码实例化平面后也同样会出现偏移,如图1-46所示。

图1-46 设置平面对象Position、Rotation、Scale的值

有了平面,我们还要给平面赋一个材质,以便在实例化后用户能看到这个平面。保持当前VisualDetectedPlane处于被选中状态,在Inspector窗口中,单击Mesh Renderer组件左侧的箭头展开Mesh Renderer展卷栏,在信息栏展开后单击Element0选项框后面的小圆圈,打开材质选择面板。然后在材质选择面板中,选择PlaneGrid,这就为我们的VisualDetectedPlane平面赋上了一个漂亮的可视材质纹理了,如图1-47所示。

图1-47 为创建的平面对象赋一个材质

有了平面,也有了材质纹理了,但我们还需要一个渲染器来将检测到或扩展的平面渲染出来,如果我们自己去写这个渲染器将不会是一件愉快的工作。好在ARCore已经为我们写好了,我们只需要将这个写好的类挂载到我们的平面上即可。在Inspector窗口下方单击Add Component按钮,在搜索框中输入detected,可以找到DetectedPlaneVisualizer,将这个脚本挂载到我们的平面上,如图1-48所示。

图1-48 为创建的平面对象挂载可视化渲染脚本

在完成VisualDetectedPlane平面制作后,我们需要将其转化为Prefabs预制件格式方便程序调用。制作预制件非常简单,在Hierarchy窗口中,将VisualDetectedPlane平面拖动到Projects窗口中的Prefabs文件夹中,这样我们就做好了VisualDetectedPlane平面的Prefabs,然后删除Hierarchy窗口中的VisualDetectedPlane平面对象,如图1-49所示。

图1-49 制作平面对象的Prefabs

提示

当游戏对象(GameObject)被制作成了预制件(Prefabs)后,该对象在Hierarchy窗口中的颜色会从黑色变为蓝色。通过观察Hierarchy窗口中游戏对象的颜色也可以判断其类型。

在制作完可视化平面Prefabs后,我们现在需要更新一下App Controller脚本,以便处理检测到的平面可视化问题。打开App Controller脚本,因为平面中我们添加的DetectedPlaneVisualizer是由ARCore提供的,我们需要在我们的代码中引用其命名空间GoogleARCore.Examples.Common,命名空间引用请参见1.7节。

然后我们再申明一个GameObject类型的DetectedPlanePrefab用来保存平面预制件的引用,注意这个变量是public型的,等挂载这个脚本后,这个变量会出现在Inspector窗口中,另外,我们还实例化了两个list用来存放我们新检测到的平面和已检测到的所有平面,见代码清单1-9。

代码清单1-9

1.public GameObject DetectedPlanePrefab;
2.private List<DetectedPlane> mNewPlanes = new List<DetectedPlane>();
3.private List<DetectedPlane> mAllPlanes = new List<DetectedPlane>();

接下来我们在update()方法中添加代码清单1-10的代码。

代码清单1-10

1.Session.GetTrackables<DetectedPlane>(mNewPlanes, TrackableQueryFilter.New);
2.for (int i = 0; i < mNewPlanes.Count; i++)
3.{
4.    GameObject planeObject = Instantiate(DetectedPlanePrefab, Vector3.zero, Quaternion.identity, transform);
5.                  planeObject.GetComponent<DetectedPlaneVisualizer>().Initialize(mNewPlanes[i]);
6.}
7.Session.GetTrackables<DetectedPlane>(mAllPlanes);

这段代码的逻辑是:首先我们从Session中得到标记为new的DetectedPlane,并将这些检测到的平面赋给mNewPlanes list表,然后我们根据新检测到的mNewPlanes数量,为每一个新检测到的平面实例化一个我们之前制作的VisualDetectedPlane平面Prefabs,并将新实例化的平面赋给planeObject以便显示和利用。最后我们还保留一份所有检测到的平面的副本。为方便阅读,这里将完整的AppController代码列写出来,如代码清单1-11所示。

代码清单1-11

1.using System.Collections;
2.using System.Collections.Generic;
3.using UnityEngine;
4.using GoogleARCore;
5.using GoogleARCore.Examples.Common;
6.public class AppController : MonoBehaviour {
7.    private bool mIsQuitting = false;
8.    public GameObject DetectedPlanePrefab;
9.    private List<DetectedPlane> mNewPlanes = new List<DetectedPlane>();
10.    private List<DetectedPlane> mAllPlanes = new List<DetectedPlane>();
11.    // Use this for initialization
12.    void Start () {
13.        OnCheckDevice();
14.    }
15.    // Update is called once per frame
16.    void Update () {
17.        UpdateApplicationLifecycle();
18.        Session.GetTrackables<DetectedPlane>(mNewPlanes, TrackableQueryFilter.New);
19.        for (int i = 0; i < mNewPlanes.Count; i++)
20.        {
21.            GameObject planeObject = Instantiate(DetectedPlanePrefab, Vector3.zero, Quaternion.identity,
22.                transform);
23.            planeObject.GetComponent<DetectedPlaneVisualizer>().Initialize(mNewPlanes[i]);
24.        }
25.        Session.GetTrackables<DetectedPlane>(mAllPlanes);
26.    }
27.    /// <summary>
28.    /// 检查设备
29.    /// </summary>
30.    private void OnCheckDevice()
31.    {
32.        if(Session.Status == SessionStatus.ErrorSessionConfigurationNotSupported)
33.        {
34.            ShowAndroidToastMessage("ARCore在本机上不受支持或配置错误!");
35.            mIsQuitting = true;
36.            Invoke("DoQuit", 0.5f);
37.        }
38.        else if (Session.Status == SessionStatus.ErrorPermissionNotGranted)
39.        {
40.            ShowAndroidToastMessage("AR应用的运行需要使用摄像头,现无法获取到摄像头授权信息,请允许使用摄像头!");
41.            mIsQuitting = true;
42.            Invoke("DoQuit", 0.5f);
43.        }
44.        else if (Session.Status.IsError())
45.        {
46.            ShowAndroidToastMessage("ARCore运行时出现错误,请重新启动本程序!");
47.            mIsQuitting = true;
48.            Invoke("DoQuit", 0.5f);
49.        }
50.    }
51.    /// <summary>
52.    /// 管理应用的生命周期
53.    /// </summary>
54.    private void UpdateApplicationLifecycle()
55.    {
56.        if (Session.Status != SessionStatus.Tracking)
57.        {
58.            const int lostTrackingSleepTimeout = 15;
59.            Screen.sleepTimeout = lostTrackingSleepTimeout;
60.        }
61.        else
62.        {
63.            Screen.sleepTimeout = SleepTimeout.NeverSleep;
64.        }
65.
66.            if (mIsQuitting)
67.        {
68.            return;
69.        }
70.    }
71.    /// <summary>
72.    /// 退出程序
73.    /// </summary>
74.    private void DoQuit()
75.    {
76.        Application.Quit();
77.    }
78.    /// <summary>
79.    /// 弹出信息提示
80.    /// </summary>
81.    /// <param name="message">要弹出的信息</param>
82.    private void ShowAndroidToastMessage(string message)
83.    {
84.        AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
85.        AndroidJavaObject unityActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
86.        if (unityActivity != null)
87.        {
88.            AndroidJavaClass toastClass = new AndroidJavaClass("android.widget.Toast");
89.            unityActivity.Call("runOnUiThread", new AndroidJavaRunnable(() =>
90.            {
91.                AndroidJavaObject toastObject = toastClass.CallStatic<AndroidJavaObject>("makeText", unityActivity,message, 0);
92.                toastObject.Call("show");
93.            }));
94.        }
95.    }
96.}

至此,我们已经更新了App Controller,也制作了我们需要的可视化平面,现在我们要把这个脚本挂载到场景中让其运行起来,在Hierarchy窗口的空白处单击鼠标右键,在弹出的菜单中选择Create Empty,新建一个空对象,并命名为AppController,如图1-50所示。

图1-50 在Hierarchy窗口中创建一个空对象

然后将Project窗口的Scripts文件夹中的AppController.cs拖到Hierarchy窗口中的AppController对象上(或者在Hierarchy窗口选中AppController对象,然后在Inspector窗口中,选择Add Component,在搜索框中输入AppController,将其挂载到该物体上,如图1-51所示)。

图1-51 为空对象挂载脚本

在AppController对象上挂载AppController.cs脚本后,我们还需要将制作好的VisualDetectedPlane预制体赋给脚本变量。保持AppController对象处于选中状态,在Inspector窗口中,单击Detected Plane Prefab文本框后的小圆圈打开对象选择面板,选择VisualDetectedPlane,如图1-52所示。

图1-52 将平面Prefabs预制体赋给脚本

至此,平面检测功能开发完毕,在进行联机测试前,确保手机连接到开发计算机上,按Ctrl+Shift+B组合键打开Build Settings对话框,在对话框中,确保选中我们的开发场景,如图1-53所示。

提示

在Scenes In Build框出现的场景中,将需要生成的场景前面的勾选上,不需要生成的则不必勾选。如果想要生成的场景在框中没有出现,单击右下方的Add Open Scenes添加当前正在编辑的场景。在勾选的场景后面会生成一个下标值,如果是多场景,要确保应用启动后的第一个场景下标值为0。

图1-53 选中需要生成的场景

在图1-53中,选择好场景后,单击单击Build And Run按钮,保存生成的.apk文件到指定的路径,如图1-54所示,即开始编译生成我们的第一个AR应用,如图1-55所示。

图1-54 保存生成的应用

图1-55 检查设备,编译、推送应用

等待片刻,如果没有错误,编译完后的AR应用程序会自动下载安装到连接计算机的手机上。在应用打开后,拿着手机前后左右移动一下,如果检测到平面,我们将会看到检测到的平面以可视化的形式展现出来,如图1-56所示。

图1-56 可视化的平面检测效果

本节我们以另一种更简洁的方式实现平面可视化,并讨论如何在检测到的平面上放置虚拟物体。

在上一节中, 我们已经实现了对检测到的平面进行可视化显示,运行后效果良好。通过查看代码,我们更清楚地了解了ARCore是如何将检测到的平面可视化的,这对于我们理解ARCore的工作方式会有很大帮助。其实,ARCore已经简化了这个过程,下面我们来看看实现可视化平面的另一种方式。打开Fox工程,在Hierarchy窗口空白处单击单击鼠标右键,选择Create Empty,并将生成的空对象命名为DetectedPlaceGenerator,如图1-57所示。

图1-57 可视化的平面检测效果

保持选中DetectedPlaceGenerator,在Inspector窗口单击单击 Add Component按钮,在搜索框中输入detected,可以找到DetectedPlaneGenerator,将这个脚本挂载到DetectedPlaceGenerater对象上,如图1-58所示。

图1-58 在空对象上挂载Detected Plane Generator脚本

在挂载DetectedPlaneGenerator脚本组件后,将我们之前制作的VisualDetectedPlane平面Prefab拖到挂载脚本的DetectedPlacePrefab属性框中,这样就完成了平面可视化工作,如图1-59所示。

图1-59 将制作的平面Prefabs拖拽到Detected Plane Generator脚本的属性框中

这是另一种实现检测平面可视化的方式,这种方式下,ARCore已经帮我们实现了全部代码,我们一行脚本代码也不用编写,这比前面我们自己写代码来实现要简单得多。

不管采用哪种方式,我们已经可以将检测到的平面进行可视化显示。有了可供我们放置物体的平面,下一步我们将在平面上放置虚拟物体对象,但在场景中什么位置放置虚拟物体呢?我们知道检测到的平面是三维的,而我们的手机屏幕却是二维的,如何在二维的平面上放置操作三维的虚拟对象?解决这个问题通常的做法是作射线检测(Raycast),这与我们在VR中用鼠标拾取物体一样。

射线检测(Raycast)的基本思路是在三维世界中从一个点沿一个方向发射出一条无限长的射线,在射线的方向上,一旦射线与添加了碰撞器的对象发生碰撞,则会产生一个碰撞检测对象,因此我们可以利用射线实现子弹击中目标的检测,也可以用射线来检测发生碰撞的位置。例如,我们可以从屏幕上用户单击单击鼠标的点和摄像机(AR中就是我们的手机设备)的位置来构建一条射线,与场景中的平面进行碰撞检测,如果发生碰撞则返回碰撞的位置,这样,我们就可以在检测到平面的碰撞点上放置我们的虚拟对象了。

ARCore在Frame类中为我们实现了4种发射射线检测物体的方法,利用这些方法,不仅可以检测到第一个碰撞对象,还可以检测到所有与射线发生碰撞的对象,Frame类的射线检测方法如表1-2所示。

表1-2 Raycast方法

Frame中的公有静态方法

描述

Raycast(float x, float y, TrackableHitFlags filter, out TrackableHit hitResult)

对ARCore跟踪的物理对象执行射线检测,参数1.2为屏幕坐标点,一旦发生碰撞则返回,返回值为Bool型,true表示发生碰撞,false表示未发生碰撞

Raycast(Vector3 origin, Vector3 direction, out TrackableHit hitResult, float maxDistance, TrackableHitFlags filter)

对ARCore跟踪的物理对象执行射线检测,参数1为射线起点,参数2为射线方向,一旦发生碰撞则返回,返回值为Bool型,true表示发生碰撞,false表示未发生碰撞

RaycastAll(float x, float y, TrackableHitFlags filter, List< TrackableHit > hitResults)

对ARCore跟踪的物理对象执行射线检测,与所有对象进行检测,返回值为Bool型,true表示发生碰撞,false表示未发生碰撞

RaycastAll(Vector3 origin, Vector3 direction, List< TrackableHit > hitResults, float maxDistance, TrackableHitFlags filter)

对ARCore跟踪的物理对象执行射线检测,与所有对象进行检测,返回值为Bool型,true表示发生碰撞,false表示未发生碰撞

在Raycast方法中,TrackableHitFlags用来过滤需要进行碰撞检测的对象类型,其值既可以是表1-3所示属性值的一个,也可以是几个。

表1-3 TrackableHitFlags类

TrackableHitFlags属性

描述

Default

这个值用于与放置的所有物体发生碰撞检测。如果我们填写这个值,那么在ARCore中,我们发射的射线将与场景中的所有平面、包围多边形、带法线的特征点进行碰撞检测

FeaturePoint

与当前帧点云中所有的特征点进行碰撞检测

FeaturePointWithSurfaceNormal

与当前帧点云中带有表面法线估计(方向)的特征点进行碰撞检测

None

此值用来表示trackableHit返回中没有碰撞发生,如果将此值传递给raycast,则不会得到任何碰撞结果

PlaneWithinBounds

与当前帧中已检测平面内的包围盒进行碰撞检测

PlaneWithinInfinity

与已检测到的平面进行碰撞检测,但这个检测不仅仅局限于包围盒或者多边形,而是可以与已检测到的平面的延展平面进行碰撞检测

PlaneWithinPolygon

与已检测平面内的凸边界多边形进行碰撞检测

TrackableHit类保存的是发生碰撞检测时检测到的相关信息,其相关属性信息如表1-4所示。

表1-4 TrackableHit类

TrackableHit属性

描述

Distance

float类型,获取从射线源到命中点的距离

Flags

TrackableHitFlags类型,获取一个位掩码,设置trackablehitmark标志对应于命中所属对象的类别

Pose

Pose类型,获取光线投射击中的物体在Unity世界坐标中的姿态

Trackable

Trackable类型,获取命中的可跟踪对象

有了上面的基础,我们重写AppContoller脚本(因为我们现在不用自己实现平面可视化代码了),完整的功能代码如代码清单1-12所示。

代码清单1-12

1.using System.Collections;
2.using System.Collections.Generic;
3.using UnityEngine;
4.using GoogleARCore;
5.using GoogleARCore.Examples.Common;
6.public class AppController : MonoBehaviour {
7.    public Camera FirstPersonCamera;
8.    public GameObject prefab;
9.    private bool mIsQuitting = false;
10.    private const float mModelRotation = 180.0f;
11.    // Use this for initialization
12.    void Start () {
13.        OnCheckDevice();
14.    }
15.    // Update is called once per frame
16.    void Update () {
17.        UpdateApplicationLifecycle();
18.        Touch touch;
19.        if (Input.touchCount < 1 || (touch = Input.GetTouch(0)).phase != TouchPhase.Began)
20.        {
21.            return;
22.        }
23.        TrackableHit hit;
24.        TrackableHitFlags raycastFilter = TrackableHitFlags.PlaneWithinPolygon | TrackableHitFlags.PlaneWithinBounds;
25.        if (Frame.Raycast(touch.position.x, touch.position.y, raycastFilter, out hit))
26.        {
27.            if ((hit.Trackable is DetectedPlane) && Vector3.Dot(FirstPersonCamera.transform.position - hit.Pose.position, hit.Pose.rotation * Vector3.up) < 0)
28.            {
29.                Debug.Log("射线击中了DetectedPlane的背面!");
30.            }
31.            else
32.            {
33.                var FoxObject = Instantiate(prefab, hit.Pose.position, hit.Pose.rotation);
34.                FoxObject.transform.Rotate(0, mModelRotation, 0, Space.Self);
35.                var anchor = hit.Trackable.CreateAnchor(hit.Pose);
36.                FoxObject.transform.parent = anchor.transform;
37.            }
38.        }
39.    }

为了放置我们自己的虚拟对象,我们还需要对AppController进行设置,在Hierarchy窗口中选中AppController对象,在Inspector窗口中,将狐狸模型赋给AppController脚本的Prefab属性,操作如图1-60所示。

图1-60 将狐狸模型赋给AppController脚本的Prefab属性

对代码清单1-12的关键代码摘出来如代码清单1-13所示。

代码清单1-13

1.TrackableHit hit;
2.TrackableHitFlags raycastFilter = TrackableHitFlags.PlaneWithinPolygon | TrackableHitFlags.PlaneWithinBounds;
3.if (Frame.Raycast(touch.position.x, touch.position.y, raycastFilter, out hit))
4.{
5.     if ((hit.Trackable is DetectedPlane) && Vector3.Dot(FirstPersonCamera.transform.position - hit.Pose.position, hit.Pose.rotation * Vector3.up) < 0)
6.     {
7.          Debug.Log("射线击中了DetectedPlane的背面!");
8.     }
9.     else
10.     {
11.          var FoxObject = Instantiate(prefab, hit.Pose.position, hit.Pose.rotation);
12.          FoxObject.transform.Rotate(0, mModelRotation, 0, Space.Self);
13.          var anchor = hit.Trackable.CreateAnchor(hit.Pose);
14.            FoxObject.transform.parent = anchor.transform;
15.       }
16.  }

我们对这段代码进行一下分析解释,有了前文的基础,这段代码逻辑也很清楚。

首先,我们定义一个TrackableHitFlags过滤器,因为我们只想检测在多边形内与边界内的平面。然后我们以用户在屏幕上的单击位置与摄像头的位置构建射线做碰撞检测。如果发生了碰撞,我们则对碰撞情况进行分析,如果是击中了检测到的平面并且又不是平面的背面,则实例化我们的Fox Prefab,同时生成一个Anchor锚点,并将实例化后的Fox对象挂载到这个Anchor上,以便ARCore实时更新这个对象的姿态。在代码中Vector3.Dot(FirstPersonCamera.transform.position - hit.Pose.position, hit.Pose.rotation * Vector3.up) < 0),这句代码的意思是对从摄像机发射到碰撞点的向量与碰撞点的法向量做点积,小于0,说明角度大于90度,因此击中的是检测平面的背面。

编译生成AR应用程序,如果没有错误,在检测到平面上单击会生成狐狸模型,效果如图1-61所示。

图1-61 在检测到的平面上放置虚拟对象效果图

在前面的章节里,新建AR工程后,我们最先做的工作是把ARCore的两个Prefabs——ARCore Device 和 Environmental Light 拖到了Hierarchy场景窗口中,如图1-62所示。那么这两个ARCore的Prefabs都是做什么的呢?本节我们将详细了解一下这两个ARCore Prefabs的作用以加深对ARCore的理解,为后面的学习打下良好的基础。

图1-62 场景中的ARCore Device和Environmental Light

大致来说,ARCore Device负责处理与设备相关的事宜,具体来说就是管理ARCore Session。ARCore利用Session来管理AR应用整个生命周期,因此,在场景中引入这个Prefab就是引入了AR应用的生命周期管理功能。

在场景加载激活后,ARCore Device将创建并初始化一个ARCore Session,并从一个跟随设备位置和方向的Unity摄像机渲染出背景图像。因此AR应用必须要有用户手机相机的使用调取权限,如果应用程序在初始化时没有访问相机的权限,则将弹出提示向用户请求相机调取权限。ARCore Device负责管理ARCore Session,而一个ARCore应用只允许有且只有一个Session,所以ARCore Device在一个场景中只能允许有且只有一个存在,默认情况下,销毁ARCore Device也会破坏Session。如果要在不同的场景中重用相同的 Session,保持跟踪状态,则需要在游戏对象上调用DontDestroyOnLoad()方法,不然,之前 Session中的所有信息都将会丢失,相当于重启了应用,重置了Session。

  在Hierarchy窗口中选中ARCore Device,然后在右侧的Inspector窗口中,可以看到ARCore Device包含了一个ARCore Session的脚本,如图1-63所示。这个脚本需要一个Session Config的ARCoreSessionConfig对象,这是个Session 配置Scriptable对象,如图1-64所示,在这个Scriptable对象可以设置ARCore的基本配置信息,其主要属性如表1-5所示。

图1-63 场景中的ARCore Device及其Session配置

图1-64 Session Config配置界面

表1-5 ARCoreSessionConfig属性

ARCoreSessionConfig主要属性(默认值)

描述

MatchCameraFramerate = true

bool类型,用于设置ARCore是否可以在Unity的帧更新中引入延迟,以与摄像机传感器传送帧的速率相匹配(在大多数设备上,这是30帧/s)

PlaneFindingMode = DetectedPlaneFindingMode.HorizontalAndVertical

DetectedPlaneFindingMode类型,设置检测的平面类型,可以是单个值,也可以是一个组合值

EnableLightEstimation = true

bool类型, 是否开启光估计,用于光估计渲染

EnableCloudAnchor = false

bool类型,是否开启云锚点,以便在不同设备间共享锚点数据

CameraFocusMode = CameraFocusMode.Fixed

CameraFocusMode类型,选择ARCore相机的对焦模式,默认是固定模式,也可以选择自动模式

AugmentedImageDatabase

AugmentedImageDatabase类型,是一个用来对检测图像做对比的图像库

AugmentedFaceMode

选择是否使用增强人脸模式,默认是disabled,也可以选择Mesh

在表1-5中,需要特别说明的是MatchCameraFramerate这个属性选项。启用这个设置可以减少因在同一帧中多次渲染相同的背景纹理而导致的无效损耗。启用这个设置还会设置QualitySetting.vSyncCount=0,这样,整个Unity应用(包括动画、UI)都将以相机帧速率来进行刷新,这可以降低性能消耗,但需要注意的是,启用此设置并不能保证每个Unity帧都有一个新的、唯一的摄像机背景纹理,因为ARCore将会等待一个新的相机帧(当前是66ms)成为可用才会进行纹理更新以避免死锁。其他的属性等我们用到的时候再进行详细说明。

在ARCore Device下有一个First Person Camera子对象(第一人称摄像机,这个摄像机其实就是模拟的用户眼睛,也是AR设备相机),这个摄像机下挂载了Tracked Pose Driver 和 ARCore Backgroud Renderer两个脚本,如图1-65所示。从名字可以看出,第一个脚本是用来跟踪用户设备状态的,第二个脚本则主要是负责将从设备相机获取的图像渲染成场景背景。Tracked Pose Driver脚本将跟踪设备的当前姿态,Tracked Pose Driver可以跟踪多种类型的设备,包括通用XR设备、通用控制器和通用遥控器,姿态来源可以是RGB相机、HD设备的左右相机等,还可以设置跟踪类型是只跟踪位置还是只跟踪旋转或者两者兼之,然后也可以设置更新模式等。

图1-65 ARCore Device下的First Person Camera属性面板

相比ARCore Device复杂的功能,Environmental Light只做一件事,那就是通过相机抓取的图像进行分析,设置_GlobalColorCorrection和_GlobalLightEstimation这两个Shader变量以便在Shader中调整虚拟物体的颜色(Shader是GPU上执行的渲染脚本,后面我们会学到)。这两个变量都可以在Shader中使用,_GlobalColorCorrection是在伽马空间中使用归一化像素强度时进行的颜色校正,颜色校正使用了一个RGB缩放因子来控制校正量,这个缩放因子也是通过ARCore对光照进行估计得出来的;_GlobalLightEstimation也是一个全局变量,ARCore会根据当前相机图像进行分析并设置这个值,通过这个变量可以调整虚拟物体的光照。_GlobalColorCorrection与_GlobalLightEstimation的区别是,_GlobalColorCorrection使用了一个RGB缩放因子而_GlobalLightEstimation没有,_GlobalLightEstimation主要是为了前向兼容, Environmental Light属性面板如图1-66所示。

图1-66 Environmental Light属性面板

通过前面的学习,我们已经可以将虚拟物体放置到ARCore检测到的平面上了,但现在还有一个问题,我们目前放置的虚拟物体没有设置质量和使用重力,如果设置了相关属性,虚拟物体还能立在检测到的平面上吗?

在前面的章节中,我们在制作VisualDetectedPlane时,这个Prefab是有Mesh Collider的,理论上这个平面可以与其他碰撞体发生碰撞。打开Fox工程,我们在之前导入的狐狸模型Fox这个Prefab下挂载Rigidbody和Mesh Collider组件,然后我们设置Rigidbody下的Mass为1.勾上Use Gravity,即设置其质量为1,使用重力,如图1-67所示。

图1-67 设置Rigidbody属性

按照我们的预期,实例化后的狐狸应该会与检测到的平面进行碰撞,然后可以稳稳地立在平面上。但编译运行后,我们得到的事实是这个狐狸会掉下去,也就是说我们想要的碰撞并没有发生。通过调整不同的设置(不同的碰撞器、质量等),测试发现皆达不到预期效果,即有质量的虚拟对象不能按照预期立在检测到的平面上。这是个比较大的问题,希望ARCore以后会内建这个支持,但目前来看我们只能自己处理。

在AR场景中,如果虚拟物体不能与检测到的平面发生碰撞将会严重影响到用户体验,既然ARCore目前不能给我们提供一个便捷的方式来解决这个问题,我们就自己动手来解决它,解决的思路是在可视化检测到的平面时,对每一个检测到的平面进行三角化并挂载Mesh Collider组件。具体实施步骤如下。

(1)获取ARCore检测到的所有平面。

(2)对每一个平面进行处理,获取到该平面最边沿的点。

(3)对获取到的这个平面进行网格化,也就是三角化。

(4)将材质与Mesh Collider附加到这个三角化的网格上。

在计算机中我们用网格(Mesh)来描述表面,所有的网格又全部都由三角形组成,网格由图形硬件(Graphics Processing Unit,GPU)进行处理来达到渲染显示的目的。目前,所有的GPU都能处理点、直线、三角形,也就是任何表面要想能被GPU处理则必须要先将其转化为点、直线、三角形组成的网格才行。使用三角形来构建网格,是因为三角形是平坦的并且拥有直边,所以他们可以完美地被用来显示平坦和连续的表面,而且数据结构上简单便于处理。三角形可以构建任何平面、曲面或者是圆面(圆面可以由大量很小的三角形来近似组成),如果三角面数足够多,曲面或圆面也可以非常平滑,使用三角形来描述的网格如图1-68所示。

图1-68 使用三角形来描述的网格

对一个不规则平面三角化是一个比较麻烦的过程,也超出了本书的范畴,我们只简单作一介绍。对于给定点集,我们需要对点集进行处理,去掉空间位置太近的点。在构成三角形时还需要考虑点连成三角形的顺序(顺时针)、中间空间的划分等。三角化这个过程就是对给定点集进行处理以得到一个由三角形组成的网格,在得到网格后就可以进行下一步的处理了。

提示

在进行三维渲染时,底层图形接口API会根据三角形的顶点顺序来区分三角形的正反面,这个也可由开发人员指定。点集的三角剖分(Triangulation)对图形学来说是非常重要的一项预处理技术,通常采用Delaunay三角剖分算法。

代码清单1-14

1.using GoogleARCore;
2.using System.Collections.Generic;
3.using UnityEngine;
4.public class ARSurface : MonoBehaviour
5.{
6.    TrackedPlane m_trackedPlane;
7.    MeshCollider m_meshCollider;
8.    MeshFilter m_meshFilter;
9.    MeshRenderer m_meshRenderer;
10.    List<Vector3> m_points = new List<Vector3>();
11.    List<Vector3> m_previousFramePoints = new List<Vector3>();
12.    Mesh m_mesh;
13.    void Awake()
14.    {
15.        m_meshCollider = gameObject.AddComponent<MeshCollider>();
16.        m_meshFilter = gameObject.AddComponent<MeshFilter>();
17.        m_meshRenderer = gameObject.AddComponent<MeshRenderer>();
18.        m_mesh = new Mesh();
19.        m_meshFilter.mesh = m_mesh;
20.        m_meshCollider.sharedMesh = m_mesh;
21.        Vector3 oneCentimeterUp = Vector3.up * 0.01f;
22.        transform.Translate(oneCentimeterUp, Space.Self);
23.    }
24.    public void SetTrackedPlane(TrackedPlane plane, Material material)
25.    {
26.        m_trackedPlane = plane;
27.        m_meshRenderer.material = material;
28.        Update();
29.    }
30.    void Update()
31.    {
32.        if (m_trackedPlane == null)
33.        {
34.            return;
35.        }
36.        else if (m_trackedPlane.SubsumedBy != null)
37.        {
38.            Destroy(gameObject);
39.            return;
40.        }
41.        else if (Session.Status != SessionStatus.Tracking)
42.        {
43.            m_meshRenderer.enabled = false;
44.            m_meshCollider.enabled = false;
45.            return;
46.        }
47.        m_meshRenderer.enabled = true;
48.        m_meshCollider.enabled = true;
49.        UpdateMeshIfNeeded();
50.    }
51.
52.        void UpdateMeshIfNeeded()
53.    {
54.        m_trackedPlane.GetBoundaryPolygon(m_points);
55.
56.            if (AreVertexListsEqual(m_previousFramePoints, m_points))
57.        {
58.            return;
59.        }
60.        int[] indices = TriangulatorXZ.Triangulate(m_points); //实际执行三角化
61.        m_mesh.Clear();
62.        m_mesh.SetVertices(m_points);
63.        m_mesh.SetIndices(indices, MeshTopology.Triangles, 0);
64.        m_mesh.RecalculateBounds();
65.
66.            m_meshCollider.sharedMesh = null;
67.        m_meshCollider.sharedMesh = m_mesh;
68.    }
69.    bool AreVertexListsEqual(List<Vector3> firstList, List<Vector3> secondList)
70.    {
71.        if (firstList.Count != secondList.Count)
72.        {
73.            return false;
74.        }
75.
76.            for (int i = 0; i < firstList.Count; i++)
77.        {
78.            if (firstList[i] != secondList[i])
79.            {
80.                return false;
81.            }
82.        }
83.
84.            return true;
85.    }
86.

ARSurface脚本实现了三角化的全部流程,为了应用ARSurface脚本,我们需要对AppController.cs脚本进行修改,添加三角化的处理代码。

(1)获取ARCore检测到的所有平面。

这一步比较简单,我们只需要从Session中取出所有检测到的平面即可,即代码清单1-15所示的语句。

代码清单1-15

1.Session.GetTrackables(m_newPlanes, TrackableQueryFilter.New);

(2)对每一个平面进行处理,获取该平面最边沿的点。

在获取平面边沿点之前,对每一个检测到的平面,我们都生成一个ARSurface空对象(这个对象就是我们将要三角化并附加材质与碰撞体的对象),如代码清单1-16所示。

代码清单1-16

1.foreach (var plane in m_newPlanes)
2.{
3.     var surfaceObj = new GameObject("ARSurface");
4.     var arSurface = surfaceObj.AddComponent<ARSurface>();
5.     arSurface.SetTrackedPlane(plane, m_surfaceMaterial);
6.}

ARCore DetectedPlane提供了GetBoundaryPolygon(List< Vector3 > boundaryPolygonPoints)方法,利用这个方法可以获取在Unity世界空间中表示平面的边界多边形的点列表(顺时针方向)。

(3)对获取到的这个平面进行网格化,也就是三角化。

这步在ARSurface脚本中进行了处理,对点集进行三角化我们直接写成TriangulatorXZ类供调用,这个类详细的算法解释超出了本书的范围,这里不详述。

(4)将材质与Mesh Collider附加到这个三角化的网格上。

为了让我们的网格可视化,我们还需要设置材质。另外,这步操作最主要的是添加Mesh Collider碰撞体,加上这个碰撞体后,平面才会与刚体对象发生碰撞,这样才能托住我们的虚拟对象让其不往下掉。这步也是在ARSurface脚本中进行了处理。 经过对检测到的平面进行三角化处理,现在生成的可视化平面能达到预期的效果,可以托住有质量和使用了重力的物体不让其往下坠(由于篇幅有限,详细代码请参见随书源码)。

在上节中,我们自己实现了虚拟对象与检测到的平面之的碰撞,但这种解决问题的方式还不太优雅,原因在于我们对检测到的平面进行了两次三角化,造成了不必要的性能开销。

回顾上节解决碰撞问题的思想,我们的解决方式是在ARCore检测到相应平面后,再对这个平面进行三角化并挂载Mesh Collider组件以解决碰撞的问题。这看似没有问题,但仔细分析一下,既然ARCore都已经检测出了平面并也可视化了检测到的平面,说明ARCore已经对这个平面进行了三角化处理,不然也不可能可视化显示。那么问题就来了,既然ARCore已经对检测到的平面进行了一遍三角化,我们为了解决碰撞问题又进行了一遍三角化,很显然做了重复的工作,我们何不在ARCore进行三角化处理的时候就给平面挂载Mesh Collider呢?毕竟三角化是一件耗时费劲的工作。 有了这个思路,我们的具体实施步骤如下:

(1)找到ARCore三角化检测平面的代码,在三角化后为平面挂载Mesh Collider; (2)对修改后的代码进行测试验证。

在Fox工程中,假设我们是直接使用Detected Plane Generator来可视化平面的,可以看到该脚本需要一个可视化平面的Prefab,ARCore默认的是使用DetectedPlaneVisualizer,如图1-69所示。

图1-69 DetectedPlaneVisualizer

在Inspector窗口中,鼠标双击图1-69所示的DetectedPlaneVisualizer可以打开这个可视化预制体DetectedPlaneVisualizer属性面板,为实现碰撞检测,我们需要给这个DetectedPlaneVisualizer Prefab挂载Mesh Collider组件,后面脚本中会用到这个组件,挂载后的属性面板如图1-70所示。

图1-70 DetectedPlaneVisualizer属性面板

图1-70中的Detected Plane Visualizer脚本组件就是ARCore对检测到的平面进行三角化的脚本,双击在IDE中打开该脚本,我们需要对这个脚本进行修改。

提示

如果读者没有直接使用ARCore提供的Detected Plane Generator来可视化检测到的平面,而是使用自己实现的可视化Prefab,那就直接找到自己实现的可视化Prefab,目标就是修改Detected Plane Visualizer脚本,其原理与步骤基本一致。

在IDE中打开Detected Plane Visualizer脚本后,我们在第66行定义两个变量。一个是用来存储MeshCollider,另一个作为控制开关,控制是否要启用碰撞器,如代码清单1-17所示。

代码清单1-17

1.private MeshCollider m_meshCollider;
2.public bool mIsCollider = true;

在完成上述步骤后,在该脚本中找到ARCore的三角化函数_UpdateMeshIfNeeded(),然后在该函数末尾添加碰撞器,即在原212行后(由于篇幅有限,详细代码请参见随书源码)添加代码清单1-18所示代码。

代码清单1-18

1.if (mIsCollider)
2.{
3.    if (m_meshCollider == null)
4.    {
5.        m_meshCollider = GetComponent<MeshCollider>();
6.    }
7.    m_meshCollider.sharedMesh = null;
8.    m_meshCollider.enabled = false;
9.    m_meshCollider.enabled = true;
10.    m_meshCollider.sharedMesh = m_Mesh;
11.}

这段代码逻辑很清晰,在需要碰撞器时给检测到的Mesh添加MeshCollider组件,以达到实现碰撞的目标。

现在我们已经给ARCore检测到的平面添加了Mesh Collider,为了验证其是否可用,我们对AndyBlue预制体进行一下处理。在Project窗口中,将GoogleARCore → Examples → Common → Prefabs目录下的AndyBlue拖到Hierarchy窗口中,为其挂载Rigidbody组件,并设置Mass1为1, Angular为0.05,勾选Use Gravity;挂载Capsule Collider组件,并设置其Radius为0.1, height为0.2, Center→Y为0.1,如图1-71所示。

图1-71 Andy Collider属性面板

在完成上述设置后我们将AndyBlue从Hierarchy窗口拖动到Project窗口中的Prefab文件平夹下,即新创建一个Prefab,命名为AndyCollider,并将Hierarchy窗口中的AndyCollider删除。

修改AppController控制脚本,允许放置多个虚拟对象,核心代码如代码清单1-19所示。

代码清单1-19

1.void Update () {
2.    UpdateApplicationLifecycle();
3.    Touch touch;
4.    if (Input.touchCount < 1 || (touch = Input.GetTouch(0)).phase != TouchPhase.Began)
5.    {
6.        return;
7.    }
8.    TrackableHit hit;
9.    TrackableHitFlags raycastFilter = TrackableHitFlags.PlaneWithinPolygon | TrackableHitFlags.PlaneWithinBounds;
10.    if (Frame.Raycast(touch.position.x, touch.position.y, raycastFilter, out hit) )
11.    {
12.        if ((hit.Trackable is DetectedPlane) && Vector3.Dot(FirstPersonCamera.transform.position - hit.Pose.position, hit.Pose.rotation * Vector3.up) < 0)
13.        {
14.            Debug.Log("射线击中了DetectedPlane的背面!");
15.        }
16.        else
17.        {
18.            var AndyColliderObject= Instantiate(prefab, hit.Pose.position, hit.Pose.rotation);
19.            AndyColliderObject.transform.Rotate(0.0f, mModelRotation, 0, Space.Self);
20.            AndyColliderObject.transform.Translate(0.0f, 0.2f, 0.0f);
21.            var anchor = hit.Trackable.CreateAnchor(hit.Pose);
22.            AndyColliderObject.transform.parent = anchor.transform;
23.        }
24.    }
25.
26.    }

这段代码与之前的AppController脚本基本没有什么区别,只是在实例化Andy机器人后我们将其向上移动了一小段距离,这是为了能更明显地观察Andy机器人下落、与平面发生碰撞的过程。

编译运行,测试结果显示检测是有效的,平面能托住加了Rigidbody的AndyCollider对象(AndyCollider相互间也会发生碰撞),效果如图1-72所示。

图1-72 Andy Collider与平面碰撞效果图


经过第1章的学习,我们已经可以将虚拟物体放置在合适的位置了,代码也可以很好地工作,并且也对部分ARCore技术点进行了比较深入的探讨。事实上到目前为止我们反倒更愿意再深入一步了解ARCore实现的技术细节,以便更好地利用它来为我们服务。前面我们讨论过,ARCore最主要的三大功能:运动跟踪、环境理解、光照估计,那我们就从运动跟踪开始来深入探究一下ARCore的技术细节。在本节中,我们将深入了解ARCore运动跟踪的工作原理并分析一下ARCore目前在运动跟踪方面的局限性。

从前面章节我们已经知道,ARCore使用视觉惯性测距系统(VIO)算法和IMU来实现运动跟踪。ARCore将从设备摄像机中识别的图像特征与内部运动传感器结合起来,以跟踪设备的相对位置和方向(姿态)。通过跟踪方向和位置,我们即可获取到设备在6自由度(6 Degrees of Freedom,简称6DoF)范围内的位置,或者我们通常所说的设备/物体的姿态。

自由度(DoF)与刚体在空间内的运动相关,可以解释为“物体移动的不同基本方式”。在客观世界或者虚拟世界中,我们都采用三维坐标系来精确定位一个物体的位置,在虚拟世界中,我们采用如图2-1所示的坐标系来衡量世界坐标或者物体局部坐标。

图2-1 笛卡儿三维坐标系

假如一个有大小的物体放置在坐标系的原点,那么这个物体的运动整体上可以分为平移与旋转两类(刚体不考虑缩放),同时,平移又可以分为三个度:前后(沿Z轴移动)、左右(沿X轴移动)、上下(沿Y轴移动);旋转也可以分三个度:俯仰(围绕X轴旋转)、偏航(围绕Y轴旋转)、翻滚(围绕Z轴旋转),如图2-2所示。通过分析计算,刚体物体的任何运动方式均可由这6个基本运动方式来表达,即6DoF的刚体物体可以做到所有的运动方式,具有6DoF的刚体物体在空间中的运动是不受限的。

图2-2 6DoF刚体运动示意图

具有6自由度的刚体可以到达三维坐标系的任何位置并且可以面向任何方向。平移相对来说比较好理解,即是刚体沿XYZ三个轴之一运动,旋转其实也是以XYZ三个轴之一为旋转轴进行旋转。在计算机图形学中,我们常用一些术语来表示特定的运动,这些术语如表2-1所示。

表2-1 刚体运动术语

术语

描述

Down

向下

Up

向上

Strafe

左右

Walk

前进后退

Picth

围绕X轴旋转,即上下打量,也叫俯仰角

Rotate

围绕Y轴旋转,即左右打量,也叫偏航角(yaw)

Roll

围绕Z轴旋转,即翻滚,也叫翻滚角

我们在AR中跟踪物体的位置和方向时经常使用姿态这个术语,姿态的数学表示就是矩阵,既可以用矩阵来表示物体平移,又可以用矩阵来表示物体的旋转。为了更好地平滑及优化内存使用,我们通常还会使用四元数来操作旋转,四元数允许我们以简单的形式定义3D旋转的所有方面。在本书中不用担心矩阵及四元数的使用,Unity已经为我们封装好了大部分的操作方法,需要用到时我们再进一步说明。

在计算机渲染管线中,为了将虚拟物体显示在投影平面上,我们需要将物体从自身局部空间(Local Space)变换到世界空间(World Space),接着变换到视空间(View Space),再变换到齐次裁剪空间(Homogeneous Clip Space),最后变换到规范设备空间(Normalized Device Space),这样物体顶点信息才最终能被GPU处理。Unity悄悄处理了所有的变换,但我们还是有必须了解一下虚拟物体到底是如何投影到手机显示屏上的,具体的渲染管线介绍超出了本书的范围,我们只简单了解一下关于投影矩阵的知识。

在ARCore Device Prefab下有个First Person Camera,这个摄像机模拟的就是用户的眼睛,手机屏幕就是投影面,图2-3所示说明了在计算机图形学中是如何定义3D摄像机或投影的,而图2-4所示则以剖面的形式展示近平面、远平面及投影面之间的关系。

图2-3 虚拟物体成像示意图

图2-4 投影平面与近平面、远平面及视场角关系图

图像处理的主要任务是将3D虚拟物体投影成2D图像,然后显示在设备上。投影即是将物体从视空间变换到齐次裁剪空间,所有的变换都可以用矩阵来表示,那么这个矩阵是如何得到的?

通过数学分析,投影矩阵可以通过视场角(Field of View,即图2-4中的α角)、投影面宽高比(可以由设备显示屏的宽高比得到)、近平面(Clipping Planes Near)、远平面(Clipping Planes Far)这4个参数构建,在First Person Camera的Inspector面板中可以看到这其中的3个参数,如图2-5所示(另一个参数可以从设备初始化时得到)。

图2-5 First Person Camera投影矩阵参数

近平面和远平面连同视场角构成了一个视平截头体,只有这个平截头体里的对象可以被渲染,非视平截头体内的对象将被剔除。在AR场景中,我们通常使用一个60º的视场角,这个角度大小与人眼视场角基本一致,用这个视场角观察场景更自然。

在AR应用中,摄像机模拟的是用户眼睛,对齐的摄像机它也恰好代表了在AR应用中通过设备投射的视图,这样我们可以通过跟踪摄像机的位置来等效跟踪用户,跟踪摄像机可以通过VIO和IMU进行。我们可以在上一章Fox工程的AppController脚本的update()方法里添加代码清单2-1中的代码进行测试。

代码清单2-1

1.var pos = FirstPersonCamera.transform.position;
2.var rot = FirstPersonCamera.transform.rotation;
3.Debug.Log("Device position (X:" + pos.x + ",Y:" + pos.y + ",Z:" + pos.z + ")");
4.Debug.Log("Device orientation (pitch:" + rot.x + ",yaw:" + rot.y + ",roll:" + rot.z + ")");

在上面的代码中,我们将第一人称摄像机的位置和方向(旋转)提取到pos和rot辅助变量中,然后,用Debug.Log()方法将其值输出到控制台,如前所述,摄像机的位置就是用户的位置。使用Android Device Monitor可以截获到这些位置和方向信息,如图2-6所示。

图2-6 使用Android Device Monitor查看应用程序控制台输出信息

利用ARCore提供给我们的运动跟踪功能,我们可以方便地跟踪用户的姿态,从跟踪效果来看,ARCore的运动跟踪质量还是比较高的,也比较稳定。但从ARCore使用的VIO和IMU技术可以推测出,ARCore的跟踪在以下情况下会失效。

(1)在运动中做运动跟踪

例如,如果用户是在火车上使用ARCore,这时IMU(Inertial Measurement Unit)获取的不仅包括用户的移动数据(实际是加速度),也包括火车的移动数据(实际是加速度),这样将导致跟踪失败。

(2)跟踪动态的环境

例如,如果用户对着波光粼粼的湖面,这时从摄像机获取到的图像信息是不稳定的,这将导致特征点提取出错进而导致跟踪失败。

(3)热飘移

相机感光元件与 IMU 都是对温度敏感的元器件,之前我们说过相机和IMU的OEM校准,但这个校准通常都会在某一个或者几个特定温度下进行,但在用户设备使用过程中,随着时间的延长会导致设备发热,发热就会影响到相机获取图像的颜色信息和IMU测量的加速度信息的准确性,表现出来就是跟踪的物体会飘移。

(4)昏暗环境 基于VIO的跟踪效果与环境中光照有很大关系,昏暗的环境采集的环境图像信息对比度低,这对提取特征点信息非常不利,因此会大大影响到跟踪的准确性,这也会导致基于VIO的跟踪失败。

在前文中,我们学习了如何定位追踪用户(实际是定位用户的手机设备)的位置与方向,然后通过摄像机的投影矩阵将虚拟物体投影到用户手机屏幕。如果用户移动了,我们可以通过VIO和IMU更新用户的位置与方向信息、更新投影矩阵,这样就可以把虚拟物体固定在空间的某点上(这个点就是锚点),从而达到以假乱真的视觉体验。

在真实世界中,我们不仅利用视觉信息,也利用听觉信息来定位3D物体。为达到更好的沉浸式体验效果,在AR应用中定位也不仅仅包括虚拟物体的位置定位,还应该包括声音的3D定位。3D音效处理目的是让用户进一步相信AR应用虚拟生成的世界是真实的。事实上,3D音效在电影、电视、电子游戏中被广泛使用。但AR 3D声音的处理有其特别之处,类似于电影采用的技术并不能很好地解决AR中3D音效的问题。

在电影院中,观众的位置是固定的,因此我们可以在影院的四周都加装上音响设备,通过设计不同位置音响设备上声音的大小和延迟,就能给观众营造逼真的3D声音效果。经过大量的研究与努力,人们根据人耳的结构与声音的传播特性也设计出了很多技术,可以只用两个音响或者耳机就能模拟出3D音效,这种技术叫双耳声(Binaural Sound),它的技术原理如图2-7所示。

图2-7 大脑通过双耳对来自声源的直接信号与间接信号进行分析,可以计算出声源的位置

在图2-7中,从声源发出来的声音会直接传播到左耳和右耳,但因为左耳离声源近,所以声音会先到达左耳再到达右耳,由于在传播过程中的衰减,左耳听到的声音要比右耳大,这是直接的声音信号,大脑会接收到两只耳朵传过来的信号。同时,从声源发出的声音也会被周围的物体反射,这些反射与直接信号相比有一定的延迟并且音量更小,这些是间接的声音信号。大脑会采集到直接信号与所有的间接信号并比较从左耳与右耳采集的信号,经过分析计算,从而达到定位声音源的效果。在了解大脑的工作模式后,我们就可以通过算法控制两个音响或者耳机的音量与延迟来达到模拟3D声源的效果,让大脑产生出虚拟的3D声音场景。

3D声场,也称为三维音频、虚拟3D音频、双耳音频等,它是根据人耳对声音信号的感知特性,使用信号处理的方法对到达两耳的声音信号进行模拟,以重建复杂的空间声场。通俗地说就是把耳朵以外的世界看作一个系统,那么任意一个声音源,在耳膜处接收到信号后,三维声场重建就是把两个耳朵接收到的声音尽可能准确地模拟出来,让人产生听到三维音频的感觉。

如前所述,当人耳在接收到声源发出的声音时,人的耳廓、耳道、头盖骨、肩部等对声波的折射、绕射和衍射以及鼓膜接收到的信息会被大脑所接受,大脑通过经验来对声音的方位进行判断。在计算机中,通过信号处理的数学方法,构建头部相关传输函数HRTF(Head Related Transfer Functions),根据多组的滤波器来计算人耳接收到的声源的“位置信息”,其原理如图2-8所示。

图2-8 通过信号处理的数学方法可以模拟3D音效

目前的3D声场重建技术已经非常成熟,人们不仅想出了如何录制3D音频,而且还知道如何播放这些3D音频,让大脑产生逼真的3D声场信息,产生与现实相同的声场效果。然而,目前的大多数3D声场重建技术都假设用户是静止的(或者说与用户位置无关),而在AR应用中,情况却有很大不同,AR应用的用户是随时移动的,这意味着用户周围的3D声音也需要调整,这一特殊情况导致目前的3D声场重建技术在AR应用时失效。幸运的是,谷歌公司已经考虑到这个问题,并为AR和VR开发了一个3D声音API,称为共振音频(Resonance Audio),我们将在下面的学习中探讨更多关于共振音频(Resonance Audio)的内容。

为何我们能够根据听到的声音确定声源的位置呢?上节我们对这个问题进行了简单阐述,根据听到的声音信号,我们可以将声音归成两类:直接信号、间接信号。

在耳朵接收到这两类信号后,大脑会进行处理并根据以往经验来对声源和环境进行确定,在真实世界中,我们通过耳朵与声音信号的交互来确定声源的水平与垂直位置,如图2-9所示,共振音频(Resonance Audio)技术完全模拟了这个交互过程,也是通过模拟这两类信号差异来让大脑产生与真实一致的声音体验。

图2-9 人耳与声音的交互示意图

1.抵达双耳的时间差

抵达双耳的时间差(Interaural Time Differences,ITD),由于声源到左耳与右耳的距离不同(当然也可以相同),如图2-10所示,因此声音信号传播到两只耳朵的时间也会有细微的差距,这样的时间差可以帮助我们了解声源的水平(Horizontal)位置,特别是在低频声源的情况下,大脑定位会更准确一些。这个时间差取决于声源相对于用户的水平位置,当一个声源离用户的左右耳差距越大,时差就越大。

图2-10 声音信号抵达左右耳有细微的时间差

2.抵达双耳的强度差

我们知道,声音是源于物体受力产生震动,声波的频率指的是一秒内物理震动的次数,如物体一秒震动480次,那该声波的频率就为480Hz。自然界声音极少是由单一频率组成,声波多是由数个频率叠加组成复合波。由于人类耳朵对声源频率的接收频率有限,对高频声源无法仅仅由时间差(Interaural Time Differences,ITD)获知它的水平位置,另外实际上声波常常会受到阻挡而无法继续传递、扩散,进而产生声音的阴影区(Acoustic Shadow),例如声波遇到建筑物、人的头部都会有这种现象,这会影响到左右耳听到不同的声音大小并接收到不同的频率分布。这些差异能帮助耳朵判断声源的水平位置,当频率越高时,双耳接收到的大小声甚至会差更多,最高可以差到8~10 dB。

3.耳廓效应

时间和强度的差异可以帮助我们定位声源的水平位置,声音进入外耳时会有不同的入射角和折射角,来自不同方向的声音以不同的方式从我们的外耳反射进入内耳,这都会造成声波频率的改变,从而产生差异,如图2-11所示。我们的耳朵就用这种差异去辨别声源的垂直高度(Elevation),确认声源的垂直位置。

图2-11 耳廓效应示意图

为了模拟真实的声波与耳朵之间的相互作用,共振音频(Resonance Audio)技术使用了头部关联传导函数(Head-Related Transfer Functions,HRTFs)音效定位算法。HRTFs包括我们用来确定声音位置的时间差和强度差的影响,以及我们用来确定声音位置的频谱效应,该算法计算声波从发射、反射后经头部、耳廓的种种效应,模拟人的神经系统如何去判断声源位置,尤其是声源的垂直高度。通过耳机接收HRTFs处理后的音频可以让大脑产生一种错觉,即声音在他们周围的虚拟世界中有一个特定的位置(这就达到了声源模拟定位的目标)。除此之外,共振音频技术不仅能模拟声波与我们的耳朵的相互作用,还能模拟声波与其周围环境的相互作用。

对我们来说,我们更关心的是用户头部的运动(其实是手机设备的姿态),因为对AR应用而言,这个非常关键。在真实生活中,移动我们的头部会导致声波与双耳间的相互作用发生变化,大脑可以帮助我们感知音频位置的相对变化,如图2-12所示。共振音频技术也反映了这种情况,以便维持一个声源在声场范围内的位置。ARCore可以跟踪用户设备的姿态,共振音频技术使用这些信息在相反的方向旋转声场(通常是声音的作用球体),这样就能保持虚拟声音相对于用户的位置。

图2-12 转动头部会改变声音与双耳的相互作用

在现实世界中,当声波在空气中传播时,除了透过空气进入耳朵外,还会经由墙壁或是环境中的介质反射后进入耳内,混合成我们听到的声音,这就产生了一个复杂的反射组合。共振音频技术将声波的混合分为以下三个部分。

(1)直达音

从声源直接传入耳朵的声波我们称为直达音(Direct Sound),直达音随着传递的距离增加,能量会逐渐减少,这也是为何声源离我们越远声音会越小的原因,如图2-13所示。

图2-13 声音的强度会随着距离的增大而减小

(2)早期反射音

除了直达音外,声波遇到介质会不断反射,如果仅经过一次反射后就进入耳朵我们称作早期反射音( Early Reflections)。通过耳朵听见的反射音会帮助我们了解身处的空间大小与形状,如图2-14所示。共振音频技术会实时地对早期反射进行空间化,并为每个反射呈现模拟源,这样,我们就能模拟声源与环境的交互过程,即感知所处空间的大小。

图2-14 早期反射音有助于了解所处空间的大小

(3)晚期反射音

声波会在环境中不断的反射,反射音随着时间不断增加形成更多也更密集的反射音,会密集到我们无法分辨,直到单个声波无法区分为止。这种现象我们称之为后期混响(Late Reverb)。共振音频技术内建了一个强大的混响引擎(Reverb Engine),能真实地还原声音在空间内的反射,开发者可以设定空间大小与反射的材质去模拟不同的声场情况。通过调整空间大小或墙壁的表面材质,混响引擎会实时做出反应,并调整声波以适应新的环境,更好的模拟不断变化的用户环境。

在真实环境中,当耳朵与声源之间有障碍物时,声音的高低频都会被影响,如图2-15所示,由波的衍射可知,高频率的声音会更容易被障碍物阻挡。为了进一步增加真实感,共振音频技术还模拟了声源和耳朵之间传播遇到障碍物时的遮挡效果,它通过处理高频和低频的不同分量来模拟这些环境遮挡效应,通过阻塞更多高频部分,模拟现实世界中发生的事情。

图2-15 声音遮挡示意图

声源的指向性与声音遮挡密切相关,指向性指声音沿不同方向传播的特性。从波的传播方式可知,声波的频率越高其直线传播的指向性越好,换句话说,就是高频部分绕过障碍物的能力越差。根据声源的指向性和用户相对于声源的位置,感受到的声音是不同的。如图2-16所示,当我们围着一个弹吉他的人绕圈走时,在吉他正前面时,听到声音要响亮得多,当绕到吉他后面时,吉他和演奏者的身体挡住了来自琴弦的声音,我们听到的声音就要小一些,这就是声音的指向性。

图2-16 声音指向性示意图

共振音频技术也充分考虑了这些因素,并提供了相关的参数来模拟声音的指向性,其提供的控制参数如表2-2所示。

表2-2 共振音频(Resonance Audio)提供的指向性参数

术语

描述

Alpha

用于设定不同的声源指向性,例如心形指向(cardiod)、圆形指向( circular)和八字型指向(figure-eight shapes)

Sharpness

设定每种指向性的宽度

Ambisonics是一种录制和播放声音的技术,是专门用来模拟原始三维声场效果的声音系统,它通过由4个指向不同方向的心形麦克风组成“四面体阵列”实现三维全覆盖的360º沉浸式全景环绕声音录制,与普通环绕声不同,播放效果更类似于Dobly Atoms,Ambisonics除了水平环绕声音,还包括水平与上下垂直方向的声源。共振音频(Resonance Audio)采用用Ambisonics技术处理声波与环境的互动,在此项技术中Ambisonic order是影响模拟声波的重要因素,当Ambisonic order增加,模拟的结果就会越清晰准确,反之则模糊。

共振音频技术其实是一整套关于AR/VR 中3D声源定位、混音、处理、多音轨合成的高性能音频处理技术,其更详细的技术细节超出了本书范围,不再详述。Unity2018版集成了共振音频(Resonance Audio)引擎但并没有提供相应组件,所以我们要使用它,还得下载Resonance Audio Unity SDK。目前,SDK版本是1.2.1,我们只需下载ResonanceAudioForUnity_1.2.1.unitypackage文件即可,如图2-17所示。

图2-17 下载共振音频(Resonance Audio)Unity SDK

打开本章配套源码Fox工程,导入下载的 Resonance Audio SDK文件,导入后如图2-18所示。

图2-18 导入共振音频(Resonance Audio)Unity SDK

在Project窗口中打开导入的ResonanceAudio→Prefabs文件夹,可以看到4个Prefabs。这4个Prefabs提供了音效处理最基本和常用的组件,详细描述见表2-3。

表2-3 共振音频(Resonance Audio)提供的常用组件

组件名

描述

ResonanceAudioSource

通过引入额外的可选参数,如方向性模式、遮挡和渲染质量,增强了Untiy可控的音频源特性。在同一个游戏对象中需要一个Untiy内置组件AudioSource

ResonanceAudioSoundfield

通过在用户周围的虚拟声场球体上编码声波来呈现完整的360°空间音频

ResonanceAudioRoom

通过引入动态的早期反射和后期混响来模拟特定空间的房间效果。利用游戏对象的Transform组件相应地应用房间效果。当用户设备(或者音频接受者,AudioListener)在房间模型的指定范围内时,就启用相应的房间效果

ResonanceAudioReverbProbe

提供了一个更精细的空间建模和更细微的混响效果的高级选项

现实环境中,声源在不同的环境中进入到人脑后会呈现完全不一样的听觉感受,比如,同一首曲子在空旷的原野上、在地下室里、在大演播厅里、在浴室里、在小房间里会给人带来完全不同的感觉。共振音频使用一个叫ResonanceAudioRoom的组件来模拟不同的环境。

可以把ResonanceAudioRoom想象成一个长方体,用来模拟声源周围的环境。这个Room共有六面墙,上下左右前后,每一面墙都可以设置其材质,共振音频共提供了23种不同的材质,如图2-19所示,有了这23种材质的组合,可以非常灵活的定义任何想要的房间效果。而且ResonanceAudioRoom不仅可以用来描述一个房间,其也可以用来描述一个室外空间或者半开放空间,在使用transparent 选项时就意味着声音会沿着那个方向穿过虚拟墙,如果6个方向全部设置为transparent 那就是一个完全空旷的空间。这个组合对我们来说也很重要。

图2-19 Resonance Audio Room提供了大量的环境模拟选项

现在我们已经了解了足够多的知识来使用Resonance Audio SDK,Google工程师为方便我们使用做了很大的努力,让复杂的东西变得简单、易操作。

(1)配置工程。

在使用共振音频之前,我们需要配置一下Unity,让Unity使用共振音频引擎而不是自带声音引擎来处理音频问题。在菜单中依次选择Edit → Project Settings → Audio,打开AudioManager settings面板。在Spatializer Plugin中选择Resonance Audio,在Ambisonic Decoder Plugin中选择Resonance Audio,如图2-20所示。

图2-20 配置应用音频引擎

(2)添加AudioListener。

为了使用音频,场景中需要挂载一个AudioListener,通常把它挂载在Camera下,这里我们将其挂载在First Person Camera下。对这个组件没有特别的要求,可以直接使用Unity的内置Audio Listener组件,也可以使用共振音频中的ResonanceAudioListener组件(提供了更精细的控制特性),如图2-21所示。我们这里直接使用内置的Audio Listener组件。

图2-21 可以直接添加ResonanceAudioListener组件,也可以添加Unity内置AudioListener组件

(3)添加ResonanceAudioRoom。

我们需要修改一下Fox这个Prefab,将其从Fox→Prefab中拖到Hierarchy窗口中,并将Project窗口中ResonanceAudio→Prefabs下的ResonanceAudioRoom Prefab拖到Hierarchy窗口中的Fox下,然后对ResonanceAudioRoom的大小与材质进行调整,如图2-22所示。

图2-22 在Fox Prefabs上添加ResonanceAudioRoom并调整参数

(4)添加ResonanceAudioSource。

我们还需要挂载一个Audio Source,这样才能将音频绑定到Fox对象上模拟声音从Fox发出的效果。选中Hierarchy窗口中的Fox,我们可以直接使用Unity的内置Audio Source组件,也可以使用共振音频中的ResonanceAudioSource组件(能提供更精细的控制特性),我们这里直接使用内置的AudioSource组件,并为AudioClip选择一个音频,如图2-23所示。

图2-23 调整Audio Source相应参数

除此之外,我们还需要对其他属性进行简单设置,保持选中Hierarchy窗口中的Fox对象,在Inspector窗口中做如下操作。

(1)在Project窗口,将ResonanceAudio→Resources→ResonanceAudioMixer→Master混音器赋给AudioSource组件的Output属性。

(2)将Spatial Blend滑到3D。

(3)展开3D Sound Settings,勾选Spatialize和Spatialize Post Effect复选框。

(4)如果需要可以勾选Play On Awake和Loop复选框,以便在场景加载后循环播放音频。操作如2-24所示。

图2-24 设置Audio Source其他参数

最后,选中Hierarchy窗口中的Fox,然后单击单击Inspector窗口中的Apply按钮将修改应用到Prefab中,删除Hierarchy窗口中的Fox,编译运行AR应用,使用耳机(注意耳机上的左右耳塞勿戴反,一般会标有L和R)或者双通道音响体验3D音效,移动手机或者旋转手机朝向,体验在AR中声源定位的效果。

上节我们已经利用Resonance Audio SDK实现了3D音效,这只是共振音频最简单的应用,共振音频提供了远比示例中高级的功能技术特性,特别是模拟房间的参数,可以模拟很多生活中的真实环境特性,本节主要对Room参数、API进行更深入详细的探究。

如图2-25所示,Resonance Audio Room属性面板中主要包括Surface Materials、Reflectivity、Reverb Properties、Size这4大主要属性。合理利用这4个属性,可以模拟不同环境下的声源音效表现,营造出非常逼真的虚拟声场效果。

图2-25 Resonance Audio Room属性面板

该面板的其详细信息如下。

(1)Surface materials:利用该属性可以对Room6个声学表面赋与不同的声学材质,共振音频共提供了23种不同的材质,我们可以直接选择需要的声学材质。每一种声学材料定义了在不同的频率下声波吸收或反射率等声学特性。例如,Heavy Curtain(厚窗帘) 吸收高频部分,因此可以模拟一个更干燥、温暖房间的声音特性;Polished Concrete(抛光混凝土)在所有频率上都会反射更多的声音能量,从而产生更明亮和更有回声的房间特性。

(2)Reflectivity:该参数用来对Resonance Audio Room中声源的早期反射强度进行调节,这样用户就能对他们周围房间的大小和形状有一个大致的印象。例如,减少这个参数的值来用来模拟受限小空间的声音,增加这个值用来模拟空旷空间的声音。

(3)Reverb properties:影响Resonance Audio Room的后期混响。它主要包括以下三个参数。

Resonance Audio SDK提供了丰富的声效API,其主要的API类如表2-4所示。

表2-4 共振音频(Resonance Audio)主要API类

类名

描述

ResonanceAudio

这是共振音频(Resonance Audio)的主要类,它负责共振音频(Resonance Audio)与操作系统及硬件的交互

ResonanceAudioAcousticMesh

这个类用来处理Mesh,保存mesh filter或者terrain的复制风格信息,以及分配给三角形的表面材质

ResonanceAudioListener

对Unity AudioListener的增强,提供更多高级的3D声场的调节参数

ResonanceAudioMaterialMapper

Resonance Audio material mapper可编程对象,负责处理GUID到表面材质的映射(这些材质定义了表面的声学特性)

ResonanceAudioMaterialMapperUpdater

Resonance Audio material mapper updater

ResonanceAudioReverbProbe

共振音频(Resonance Audio)混响Probe,作为音效的采样点,这个采样点会向其周围的环境发射射线,以此来感知周围环境的声学特性并利用这些信息来计算混响音效

ResonanceAudioRoom

模拟房间,利用这个模拟房间来模拟真实的音效环境

ResonanceAudioRoomManager

模拟房间的管理类,管理模拟房间的3D声场

SurfaceMaterialDictionary

一个可序列化的字典类,它负责从表面材质到GUID的映射

ResonanceAudioSource

对Unity AudioSource的增强,提供更多高级的3D声场的调节参数

此外,共振音频技术还提供了对声音的混响烘焙(Reverb Baking),与对光照进行烘焙一样,通过设置探针(Probe)可以将声音特性直接烘焙到环境中,省去实时的音频处理,降低处理器的压力,这对移动设备有限的资源来说是非常有利的。

在前面的学习中,我们已经对ARCore运动跟踪进行了深入地探究,对其原理、机制、局限性都有了比较深刻的认识。本节我们将结合ARCore的运动跟踪技术展示其在空间的定位和跟踪能力。

在Google ARCore开发者演示中,有一款在空间绘画的应用叫ARCore Drawing,如图2-26所示。这是一款充分运用ARCore定位与跟踪技术的应用,接下来我们就来实现其核心技术。

图2-26 ARCore Drawing演示效果

我们的目标是实现用户利用手机在空间中画线的功能(用户轨迹),主要利用ARCore对用户位置的空间跟踪能力。具体实现思路如下。

(1)建一个材质用于渲染线条。

(2)随时间获取用户当前的位置,我们每隔一秒获取一次用户位置(手机设备位置)。

(3)将获取到的位置信息保存起来,利用lineRender来渲染位置点之间的连线。

1.创建材质

新建一个Unity工程,配置好相关设置(具体请参见第1章),在Project窗口的Assets→Materials文件夹下新建材质LineRender,Shader选择Particles/Additive(选择其他Shader也可以),设置好Particle Texture,如图2-27所示。

图2-27 设置材质属性

2.创建AppController

在Assets→Scripts文件夹下新建C# 脚本AppController.cs(代码附后),在Hierarchy窗口中新建一个空对象,并为其挂载Appcontroller脚本,设置脚本参数如图2-28所示。

图2-28 设置AppController脚本属性

3.实现代码

打开AppController脚本,编写实现代码,关键代码如代码清单2-2所示。

注意

在代码清单2-2中,我们获取位置点的周期设置为每一秒获取一次用户位置信息。这主要是为了演示和减轻处理器压力。获取位置信息越频繁对处理器的要求就越高,当然,渲染出来的线条也会越平滑。

代码清单2-2

1.public Material mMaterial;
2.public Camera FirstPersonCamera;
3.private LineRenderer lineRender;
4.private int PointCount = 0;
5.private bool mIsQuitting = false;
6.private float mDeltaTime = 1.0f;
7.private float mNextPointTime = 0.0f;
8.private int mMaxPoint = 100;
9.// Use this for initialization
10.void Start () {
11.    OnCheckDevice();
12.    lineRender = gameObject.AddComponent<LineRenderer>();
13.    lineRender.material = mMaterial;
14.    lineRender.startWidth = 0.005f;
15.    lineRender.endWidth = 0.002f;
16.    lineRender.startColor = Color.red;
17.    lineRender.endColor = Color.yellow;
18.    mNextPointTime = Time.time;
19. }    
20.   // Update is called once per frame
21.void Update () {
22.   UpdateApplicationLifecycle();
23.   if (Time.time > mNextPointTime && PointCount <= mMaxPoint)
24.   {
25.        AddLinePoint();
26.        mNextPointTime += mDeltaTime;
27.    }        
28.}
29.void AddLinePoint()
30.{
31.   lineRender.positionCount = PointCount + 1;
32.   lineRender.SetPosition(PointCount, FirstPersonCamera.transform.position);
33.   PointCount++;
34.}

在代码中,我们先设置lineRender相关参数,每隔一秒获取一次用户位置信息数据并将位置信息赋给lineRender,利用lineRender画线连接位置点。AppController代码沿用了之前我们写的代码结构,重复部分不赘述。本代码实现了最基础也是最核心的空间运动跟踪及可视化功能,对代码稍作扩展即可以实现空中手写、空中绘画、空中标点等酷炫的应用。

编译运行,运行效果如图2-29所示。

图2-29 ARCore空间定位跟踪示例运行效果


相关图书

推荐系统:产品与算法解析
推荐系统:产品与算法解析
深入理解Prometheus监控系统
深入理解Prometheus监控系统
流式系统
流式系统
程序员的制胜技
程序员的制胜技
Verilog HDL程序设计教程(第2版)
Verilog HDL程序设计教程(第2版)
面向电子鼻的复合光气体传感方法
面向电子鼻的复合光气体传感方法

相关文章

相关课程