基于Unity的ARCore开发实战详解

作者: 汪祥春
译者:
编辑: 张涛
分类: Unity

图书目录:

详情

第1章ARCore的技术原理;第2章学习运动跟踪技术及3D音效;第3章Shader知识;第4章学习光照估计技术;第5章学习图像识别技术;第6章学习ARCore计算机视觉相关技术;第7章学习AR与AI结合的相关技术;第8章实现多人AR体验共享;第9章学习ARCore人脸增强技术;第10章学习开发AR应用;第11章学习AR性能问题;第12章通过实例提高实际运用转化能力。

图书摘要

版权信息

书名:抢读版-基于Unity的ARCore开发实战详解

ISBN:978-7-115-52803-2

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

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

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

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

编  著 汪祥春

责任编辑 张 涛

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

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

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

读者服务热线:(010)81055410

反盗版热线:(010)81055315


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

本书的主要内容:第1章作为入门章节,介绍了ARCore的技术原理及开发Unity AR应用的环境准备及调试方法;第2章学习运动跟踪技术及3D音效;第3章学习环境理解技术及Shader知识;第4章学习光照估计技术及相应的光照模型知识;第5章学习图像识别技术;第6章学习ARCore计算机视觉相关技术;第7章学习AR与AI结合的相关技术,特别是神经网络知识;第8章学习共享云锚点技术,实现多人AR体验共享;第9章学习ARCore人脸增强技术;第10章学习开发AR应用的设计原则与设计指南;第11章学习AR开发时的性能问题排查及优化技术;第12章通过综合运用ARCore技术完成若干实例,提高实际运用转化能力。

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


近年来,增强现实(AR)技术得到了研究人员、普通民众的广泛关注。在计算机视觉与人工智能技术的推动下,增强现实技术表现出了强劲的发展势头,无论是跟踪精度、设备性能,还是人机交互自然性上,都有很大提高。据权威机构预测,增强现实会成为下一个 10年改变人们生活、工作的最重要的技术之一,并将在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)结构完备。本书可分3部分:第一部分为基础篇(包含第1章至第9章),对ARCore各个技术点进行全面深入的剖析;第二部分为提高篇(第10章至第11章),主要从高层次对AR开发中的原则及性能优化进行讲解;第三部分为实践篇(第12章),通过对基础篇中技术的综合运用,提升实际应用的能力。从技术深度到技术运用,降低技术转化成实际能力的难度。

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

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

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

本书各章节案例源代码可在人民邮电出版社异步社区网站上下载。

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

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

作 者


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

本书配套资源包括书中示例的源代码。

要获得以上配套资源,请在异步社区本书页面中单击,跳转到下载界面,按提示进行操作即可。注意,为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

如果您是教师,希望获得教学配套资源,请在社区本书页面中直接联系本书的责任编辑。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可,如下图所示。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/selfpublish/submission即可)。

如果您所在的学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


AR技术是下一个10年最重要的技术之一。时至今日,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年代初发明的。其后,经过几代人的努力,最早将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)是一种佩戴在前额上的显示装置。头显将物理世界和虚拟物体的图像放置在用户的眼球视场上,现代头显经常使用传感器进行六自由度监控,允许系统将虚拟信息与物理世界对齐,并根据用户头部运动相应地调整虚拟信息;眼镜是另一个常见的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系统具有3个突出的特点:①真实世界和虚拟信息融合;②具有实时交互性;③在三维尺度空间中增添定位虚拟物体。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年8月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的读取速度大约为1 000次/s,并且是基于加速度的(用户的移动)。采用航迹推算法(Dead Reckoning)测量IMU读数之间的设备移动,但这种方法推算是一种估算,就像向前走一步,然后猜测走了多远一样,此时会用航迹推算法来估计距离。惯导系统中的误差也会随时间累积,所以IMU帧率越长,惯导系统从视觉系统中复位后的时间就越长,追踪位置距离真实位置偏差也就越多。

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

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

为了获得精确的定位,VIO需要获取两张有差异的场景图像,然后对当前位置进行立体计算。我们眼睛就是这样看到3D效果的,一些跟踪器也因此而依赖立体相机。如果采用两台相机就很容易计算,知道两个相机之间的距离,同时捕获帧进行视差计算;如果只有一个相机,可以先捕捉一次画面,然后移动到下一个位置进行第二次捕捉,最后进行视差计算。使用IMU航迹推算可以计算两次数据读取位置之间的距离然后正常进行立体计算(也可以多捕获几次使计算更加准确)。为了获得较高的精度,系统依赖IMU的精确航迹推算,从IMU读取的加速度和时间测量中,可以向后合并以计算速度并且再次向后合并以获取画面之间设备的实际距离(公式S = 0.5 × a × t2),但是困难的是从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)特征提取:特征主要包括特征点、特征线和区域。大多数情况下都是以特征点为匹配基元,特征点以何种形式提取与用何种匹配策略紧密联系。因此在进行特征点的提取时需要先确定用哪种匹配方法。特征点提取算法可以总结为3种:基于方向导数的方法,基于图像亮度对比关系的方法,基于数学形态学的方法。

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

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

ARCore使用3个主要功能将虚拟内容与通过手机摄像头看到的现实世界整合:运动跟踪、环境理解、光照估计。这3个功能是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描述了真实世界二维平面的信息,如平面的中心点、平面的x轴和z轴、组成平面多边形的顶点。检测到的平面还分为3种状态,分别是正在跟踪、可恢复跟踪和永不恢复跟踪。如果是非正在跟踪的平面,包含的平面信息可能不准确。两个或者多个平面还可以被自动合并成一个父平面。

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这10款手机在美国地区是支持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,可以在其官网中下载。请确保下载的版本在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)是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。目前Unity最新稳定版本是v2018.3.0f2,我们下载其下载助手后,利用下载助手帮我们下载我们需要的组件。因为我们需要发布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或者Wi-Fi来调试应用,同时本节还将介绍使用计算机监视手机运行的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接口损坏,而且长期给手机充放电也会损害手机电池性能。下面我们介绍利用Wi-Fi来调试AR应用。

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

设置手机Wi-Fi调试的步骤如下。

(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)手机连接Wi-Fi并记下手机的IP地址。

在手机界面,依次单击设置→关于手机→状态项,然后在打开的界面上可以查看手机连接Wi-Fi时分配的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

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

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

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

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

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

首先通过USB或者Wi-Fi连接到移动设备,然后打开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应用信息

注意

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

至此,我们可以构建、部署和远程调试我们的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所示。

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

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

提示

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

至此,我们的场景已经搭建得差不多了,是不是感觉场景中没东西?这就是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,可以找到Detected PlaneVisualizer,将这个脚本挂载到我们的平面上,如图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,transform);
22.       
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所示。

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

提示

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

在图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,可以找到Detected Plane Generator,将这个脚本挂载到Detected Place Generater对象上,如图1-58所示。

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

在挂载Detected Plane Generator脚本组件后,将我们之前制作的Visual Detected Plane平面Prefab拖到挂载脚本的Detected Place Prefab属性框中,这样就完成了平面可视化工作,如图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.
12.  void Start () {
13.    OnCheckDevice();
14.  }
15.
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所示。

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

▲图1-67 设置Rigidbody属性

在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三角剖分算法。

为实现三角化,为我们需要新建一个ARSurface.cs脚本,该脚本负责处理三角化的具体事务,ARSurface代码如代码清单1-14所示。

代码清单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

  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可以打开这个可视化预制体Detected Plane Visualizer属性面板,为实现碰撞检测,我们需要给这个DetectedPlaneVisualizer Prefab挂载Mesh Collider组件,后面脚本中会用到这个组件,挂载后的属性面板如图1-70所示。

▲图1-70 Detected Plane Visualizer属性面板

图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所示。

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

▲图1-71 Andy Collider属性面板

修改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与平面碰撞效果图


相关图书

Unity游戏开发入门经典(第4版)
Unity游戏开发入门经典(第4版)
Unity  3D游戏开发技术详解与典型案例
Unity 3D游戏开发技术详解与典型案例
从零开始:快速入门Unity 3D游戏开发
从零开始:快速入门Unity 3D游戏开发
Unity 3D脚本编程与游戏开发
Unity 3D脚本编程与游戏开发
AR开发权威指南:基于AR Foundation
AR开发权威指南:基于AR Foundation
VR与AR开发高级教程:基于Unity(第2版)
VR与AR开发高级教程:基于Unity(第2版)

相关文章

相关课程