书名:Google Cardboard App 开发指南
ISBN:978-7-115-45303-7
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
• 著 [美] 迈克尔•沃西耶(Michael Vaissiere)
迈克•帕萨莫尼克(Mike Pasamonik)
亚历山大•波波夫(Oleksandr Popov)
彼得•比克霍夫(Peter Bickhofe)
译 吴亚峰
责任编辑 吴晋瑜
• 人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
• 读者服务热线:(010)81055410
反盗版热线:(010)81055315
Copyright © 2015 Bleeding Edge Press. All rights reserved. First published in the English
language under the title “Google Cardboard Apps Deconstructed A Developer’s Guide By: Michael Vaissiere. Mike Pasamonik. Oleksandr Popov and Peter Bickhofe” by Bleeding Edge Press, an imprint of Backstop Media.
本书中文简体版由Backstop Media LLC授权人民邮电出版社出版,未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。
版权所有,侵权必究。
本书循序渐进地引导开发人员学习用谷歌魔镜开发工具包(Cardboard Android SDK)以及Unity开发包进行VR应用开发的技术,讲解了创建令人沉浸其中的虚拟景观应用、开发使用智能手机拍摄3D全景照片的应用,以及采集和控制语音到优化画面的技术。
本书内容分为5章,包括初始VR、VR应用——Alien Apartment、VR应用——Glitcher VR、Lanterns和Village案例以及谷歌魔镜——面向大众的VR。
本书适合VR虚拟现实开发者、游戏开发者、程序员学习使用,也可作为大专院校相关专业师生的学习用书和培训学校的教材。
最近5年,我一直在从事移动平台下3D游戏及应用开发的相关工作,对虚拟现实(VR)也颇感兴趣。早期的虚拟现实可穿戴设备价格高昂,很难实际体验。自谷歌于2014年夏天发布了谷歌魔镜(Google Cardboard)之后,体验虚拟现实的成本才大大下降,“旧时王谢堂前燕”终于“飞入寻常百姓家”。
我也在第一时间购买并进行了体验,通过谷歌魔镜真正感受到了虚拟现实的魅力。在那之后,我也在安卓手机上基于谷歌魔镜的SDK开发了一些简单的虚拟现实场景。恰逢今年人民邮电出版社要引进Deconstructing Google Cardboard Apps一书,我有幸承担了本书的翻译工作。
拿到这本书后,我第一时间进行了阅读。由于内容非常精彩,原本打算抽时间见缝插针进行阅读的计划被打破,我用整整两天时间读完了这本书。虽然这本书不太厚,但书中提供了很多作者在实际开发中积累的处理各方面问题的经验,参考价值很高,令我受益匪浅。
翻译过程中,我的学生刘建雄、罗星辰、王淳鹤帮我完成了不少辅助工作,在这里对他们表示感谢。另外,由于本人学识有限,书中难免会有不足之处,敬请广大读者批评指正!
吴亚峰
谷歌魔镜(Google Cardboard)是体验虚拟现实最廉价的方式之一,并且被人们称为体验虚拟现实的一种“先导药”。谷歌魔镜廉价且使用便捷,再加上它是通过与手机的协作(由手机负责画面渲染、传感器数据拾取与处理等方面的工作,谷歌魔镜负责将手机固定于双眼之前并负责近距离成像)来实现虚拟现实,这意味着几乎每个人都能通过它方便地进入虚拟现实的世界。谷歌魔镜于2015年的谷歌I/O大会上升级到能够与iPhone系列手机兼容,同时还开始支持YouTube与GoPro等许多公司的产品。这意味着,即便你不愿出门,也能通过虚拟现实技术以“身临其境”的方式体验世界各地的风情。
本书面向具有Unity 3D开发经验的移动应用开发人员。如果你了解以下几种知识,将有助于更好地掌握本书涉及的内容。
你应该了解顶点、网格等与3D建模相关的基本概念,能熟练地使用Blender、Sketches或者其他类似的3D建模工具。
你应该已经学习了Unity的基本使用方法,并知道如何利用它建立一个简单的场景;你应该已经了解什么是预制件(Prefab)、什么是着色器(Shader),并且已经掌握如何将用Unity开发的应用部署到手机等移动设备上。
你应该了解什么是Apk,并且知道如何利用adb来进行错误调试。
过去15年里,Michael Vaissiere(又被誉为VR领域的Ryan Conrad)一直从事IT行业,担任过开发人员、企业架构师和项目经理。曾作为谷歌魔镜应用程序的独立开发人员,开发了当前非常流行的外星公寓VR应用。他喜欢创新、听音乐、读书,并且对科学与天文学充满激情。
Mike Pasamonik是Glitcher VR的创造者,他不仅是一名计算机科学专业的研究生,还担任圣迭戈州立大学语言资源采集中心的全职工程师,负责编写和维护网络、移动应用程序,主要学习与研究人工智能、计算机视觉、机器人技术和虚拟现实技术。
他是孔明灯(Lanterns)和村庄VR及其他众多应用程序的创作者之一。Oleksandr则担任ELEKS Software的产品经理与开发工程师。
Peter Bickhofe是“哇!当心小行星!”(WAA! When Asteroids Attack!)的创作者,作为一名游戏开发者,他致力于Unity 3D、VR和基于位置的游戏等领域的学习与研究,现担任Highscore Brothers的CEO。
Raka Mahesa是一个小型游戏工作室的程序员,开发过游戏Chocoarts。他负责探索游戏开发过程中所需要的新技术,如实现联网功能、设计图形编辑器以及使程序支持虚拟现实。工作之余,他也开发自己的游戏,并且对VR技术很感兴趣,他相信VR将会在未来世界里扮演重要的角色。
Scott Harber从事视频游戏产业将近12年,曾担任《横冲直撞3》《极品飞车14热力追踪》和《战地硬仗》等知名游戏的技术美工。从2014年开始,他成为一名从事移动手机端、PC、Mac和Wii U游戏开发的独立开发者,并对虚拟现实产生了兴趣,还发布了一款支持Oculus Rift(一种头戴式显示器)的游戏和一款支持谷歌魔镜的手机应用。
他是第一个公开分享能可靠支持谷歌魔镜磁性触控按钮(本书中后面将简称为磁钮)的可重用代码的Unity开发者。直至今天,许多在线商店中的游戏依然在使用这些代码。后来,随着谷歌魔镜开发包的整合,该应用成了谷歌的一项业务。2014年年底,他结束了独立开发者生涯,回到游戏产业工作。
Casey Borders是一名拥有十几年开发经验的专业开发者,几乎在他的整个职业生涯中,他都在研究实时交互式3D软件与手机应用的开发。
Raul Acosta和Eduardo Acosta在Raiz专门从事创建和优化身临其境的环境以及更广泛的设备上增强现实的实现。他们喜欢创建、修饰画面以及探索在设计过程中加入虚拟现实的新方式。
迈克尔·沃西耶(Michael Vaissiere)
远在YouTube支持360°全景视频之前,我就使用过类似的应用。开始我也想过自己制作VR眼镜,最后还是决定偷懒,买一个算了。之后我购入了一个谷歌魔镜,来进一步提高游戏体验。那天,包裹被送了过来,从打开包装到组装完毕,共花了5分钟。
接着,我下载了谷歌魔镜的官方应用,插入手机。但这个应用一直在循环播放视频,后来发现是因为手机应用需要检测到谷歌魔镜里NFC的信号才开始正常工作。我把NFC标签贴好,打开了手机的NFC功能并重新插入手机。此时,一切都恢复正常了。
提示:
谷歌魔镜里的NFC标签通过与手机触碰发送信号可实现快速下载App、快速打开谷歌应用等便捷功能。
你也许从很多人那里听说过,第一次观看谷歌魔镜所呈现的画面时,你会惊呼“哇”!对我而言,我认为这主要是因为谷歌魔镜所带来的令人惊叹的效果。我开始非常怀疑,仅有两个廉价镜片的谷歌魔镜再加上手机中的几个传感器能否真的将虚拟现实的世界呈现在我的眼前。
但谷歌魔镜确实做到了,你看到的风景会跟真实的景象一样是 360°全景的,会随着你头部的转动而变化。难以想象这就是真正的VR,所以大家都会惊叹“哇”。
接着15分钟过去了。在这15分钟里,我在YouTube上观看了3D电影,体验了几次虚拟世界里的雨天效果,在谷歌地球中欣赏了许多地方的风景。然后我对自己说:“这很棒,但是我还没看够,我要去探索更多东西,我想在其中实现互动,我想要如同在真实世界里一样行走。”
于是我开始这样想:谷歌的官方应用应该具有代表性,毕竟谷歌魔镜是如此酷炫,谷歌应用商店里应该有许多更好的应用。但事实并非如此,下载完应用商店里的所有 VR 应用后,我才发现应用商店中支持谷歌魔镜的应用基本都处在测试阶段。这些应用当中没有一个能够让我实现互动,或者是不使用蓝牙手柄就可以操作的。
短暂的失望过后,我就到架子上找出我的蓝牙手柄来实现与虚拟现实世界的交互。然而在感到眩晕后,我的想法就落空了。在虚拟现实世界里通过游戏手柄来控制移动会让我感到晕眩。真正的VR怎么可能让人如此失望呢?
接着我开始思考:虽然人类的头部并没有像手和腿那样灵活,但我们平时也经常用头部动作来进行简单的信息传递,如点头代表“是”、摇头代表“否”。为什么不能用头部动作来对应用进行操控呢?我开始进行这方面的尝试,同时还试验了许多其他方法,结果证明通过头部动作进行操控的方式更好一些。
这种操控方式甚至影响了谷歌应用商店里人们对应用的评价。除了一些蓝牙手柄的铁杆粉丝还在坚持VR应用一定要支持蓝牙手柄外,其他人群的反响非常好。于是最终我找到了一种能够同时支持这两种控制方式的方法,并且将来还会支持更多控制方式。
我为了测试我的头部控制方式而开辟的专区变得广受好评。我开始收到不少用户的来信,信中说应用里的音乐很棒并且令人感到舒适,他们通过享受专区里的应用缓解了一天的辛劳(后续的章节里我会分析为什么用户会有这样的体验)。我开辟的专区逐渐打破了仅通过蓝牙控制器进行VR应用操作的固有模式。
最重要的是,谷歌商店里的VR应用非常少。难以想象,在一个拥有数十亿用户的应用商店中,VR应用却如此之少。因此在VR类别的应用中,这是获得巨大下载量的绝佳方式。最终事实也证明我的确是对的,Alien Apartment(这是笔者开发的一款用头部动作进行操控的VR应用)在几周内达到了数千次的下载量。尽管不及Flappy bird,但还是比较可观的。同时,谷歌将继续推广这个平台。
我参与VR的开发纯属偶然,但这并不影响我参与的热情,因为VR像音乐一样能带给我乐趣。研究VR并不仅仅是为了开发游戏,也是在设计一个新世界。如同你可以变换音乐的旋律,聆听它、享受它一样,你可以设计自己的世界,并沉浸其中。这和Minecraft(我的世界)的高速成长十分相似,不禁让我想到了VR的市场前景以及什么样的机会将首先出现。
假设你已经购买了这本书,也已经有一些开发谷歌魔镜和Oculus Rift(一种专用的VR头盔,价格较贵)上VR应用的经验,那么我们来比较一下基于上述两种平台开发VR应用的区别,具体情况如表1-1所列。
表1-1 两个平台开发VR应用的区别
项目 |
Oculus VR |
谷歌魔镜 |
得出的结论 |
---|---|---|---|
花费 |
这个头戴式显示器需要与一个高配置的电脑协同工作(电脑负责运行程序、处理图像,其价格在1000美元,Oculus VR则负责显示功能) |
需要买一部性能和功能类似Nexus5的手机(300美元左右)和谷歌魔镜(5~100美元) |
2014年最后一个季度大约售出了4亿部手机和8000万台电脑。这个数据应该可以帮助你从市场规模的角度得出结论 |
质量 |
具体最终性能怎样并不知晓,但很明显Oculus VR是高端产品 |
中低端产品,低分辨率,低功耗,而且显示器并非特制的虚拟现实显示器 |
如果你期望开发出像大卫雕像那样的顶级艺术画面,那你买错书了 |
体验 |
潜力无限 |
潜力无限 |
虽然通过Oculus不仅可以体验FPS类型的游戏或应用,也可以更好地渲染更大数量的多边形以取得更好的视觉效果,但同时,当下人们也非常喜爱复古风格的像素游戏,这些游戏的体验和音乐的吸引力同样很大。对于VR领域而言,更是如此。相信你应该已经知道这一点,这就是你购买本书的原因 |
发展水平 |
如果玩家已经拥有高配置的能与Oculus VR 协同提供流畅顺滑虚拟现实体验的主机,则不需要再购买新的主机,因而总花销不太高(作者认为这仅仅是针对骨灰级玩家而言的) |
个人认为谷歌正在大力发展与谷歌魔镜相关的业务,准备占领下一代的大众市场 |
我个人认为由于Oculus定价不菲,所以目前还仅仅是骨灰级玩家案头的昂贵外设,仍然未被大众玩家所接受。而只要拥有安卓手机便可以轻松购得谷歌魔镜,通过其方便地进入虚拟现实的世界 |
总而言之,如果谷歌拿出大力发展谷歌魔镜的战略计划,就会有更广阔的市场和更大的发展潜力。此外,Oculus 是一个正在高速发展的、性能更强劲的平台,其能够全速支持更丰富的VR理念。
近期Oculus推出的Gear VR(与三星合作研发)使得VR的市场路线更加模糊,前景也不很明朗。但可以确定,未来的VR设备不会仍只是骨灰级玩家案头的专有外设或者是仅有两个廉价镜片的纸板盒。
可以预见,性能优良、价格合理的VR设备必将出现。那时,肯定可以以一款中端智能手机的价格购得一套完整的VR设备。这些VR设备可以方便地与你拥有的智能手机、智能手表、平板电脑或笔记本电脑配合使用。时间会证明我在谷歌虚拟现实上下的赌注是正确的。
有过软件开发经验的开发人员都了解,开发工具是生产效率的基石。合理的工具链能够支撑合理的工作流,因此下面将介绍几种在开发谷歌魔镜VR应用时可以配套使用的几个工具。
Unity可以说是开发Android 3D应用最简便、最快捷的工具。这与其非常成熟以及完全支持用户自定义密不可分。此外,谷歌还提供了支持Unity平台的谷歌魔镜SDK,这为开发提供了极大的方便。到目前为止,Unity已经发布了很多版本,本书为什么选用5.1版呢?原因如下。
完整版是可以免费使用的(但是有商业限制,如果你想用于商业计划,请提前仔细阅读条款)。
尽管Unity 4的稳定性很好,但由于其免费版本有一些功能上的限制,实际使用时不如完整版好用。
Unity从5.1版本开始提供对VR设备(包括Oculus和Gear VR)的原生支持,估计不久的将来也可能会原生支持谷歌魔镜。我认为至少谷歌SDK将会和Unity子程序之间能够协同工作。下载网址unity3d.com。
实际谷歌魔镜VR应用的开发中,尽管理论上可以有多种选择,你甚至可以选择创建自己的立体渲染框架,但我认为只有使用谷歌魔镜开发工具包进行开发是正确的,原因如下。
支持触发器管理(谷歌魔镜在2014版增加了磁钮,在2015版增加了按钮)。
图像失真校正。由于使用谷歌魔镜体验虚拟现实的过程中需要通过透镜观察屏幕,实际看到的画面是扭曲失真的(就像我们透过玻璃碗看世界一样),通过这个开发工具包可以很快修复此问题(因为它是原生的)。
符合Unity的一贯开发风格。
良好地兼容iOS。
谷歌原厂支持。
可以方便地从“developers.google.com/cardboard/unity/”下载。
我不太擅长3D模型的创作,因此花了很长时间去选择合适的模型制作工具。在尝试了多种不同的工具后,最终我选择了Blender,主要原因如下。
它是开源的解决方案。
社区论坛很强大也很活跃,里面有免费的案例和教程。
能很好地兼容Unity。
其用户自定义能力很强,并且对于一款3D建模软件而言是非常易用的了。
可以从网站“www.blender.org”进行下载。
如果没有漂亮的纹理图,就不可能渲染出具有吸引力的场景,所以一款优秀的图像处理软件对应用中的所有3D场景都是非常关键的。我之所以喜欢用GIMP工具处理图像,是因为它简单且功能强大。GMIP 不断提供各种处理工具(包括色彩曲线、多参数调整、规格化、着色、色相/饱和度、色彩深度、alpha 通道处理、选择、图层、遮罩等)。这些处理工具正在迅速地发展,而且使用起来非常简便明了。当然,GIMP也是开源的,互联网上有大量的插件(诸如生成法向量贴图的工具等)。下载地址https://www.gimp.org。
这里你说了算!无论你是不是一个拥有自己家庭工作室的专职音乐制作人,都可以从网络上海量的开源音效素材中找到你最需要的音效。除此之外,还有一点我不吐不快:Single Cell Software(一个软件开发商)开发的Caustic是一款非常值得使用的音效制作软件,它虽然很小但功能强大,可以说是过去10年中MOD音轨制作软件非常值得一提的继承者。
说明:
MOD是一种较为古老的用来存储计算机音乐的文件格式,也是第一个模块化的音乐文件格式,其文件以“.mod”扩展名结尾,一度非常流行。
Caustic同时兼容Android和iOS平台,可以随时将灵光一现飘过你耳畔的旋律制作出来。当然,通过Caustic,你也可以在台式机上(包括Windows和MacOS)导出.wav或.mid格式的音频。即使你从来没有制作过音乐,你也应该尝试一下!下载地址:singlecellsoftware.com/caustic。
虽然我还没有使用过这款软件,但是在这里我必须要提一下。Makehuman 是一款基于大量人类学形态特征数据开发的,且有大量可自定义选项的开源3D人物角色建模软件,还符合Unity rig模型标准。下载地址:www.makehuman.org。
我的建议很简单:开发中的模型、音效等最好使用对应制作软件原本的格式存储,使用时导出为Unity友好的格式。到目前为止,我认为最好的经验如下。
三维模型采用.fbx格式(在Blender的输出菜单中选择FBX选项导出即可)。
一般纹理图采用.jpg格式,如果需要alpha通道的纹理图,则采用.png格式。
音效采用.wav格式并且使用Unity进行压缩。
如果你也是一位独立开发者,硬件拥有量是一个非常大的制约因素,因为很难有充裕的资金购买各种各样的手机和头盔来检查应用程序的兼容性。因此,实际的选择只能是“做能力范围内的事情”。以我为例,我最终只做了两种设备的测试,具体情况如下。
由于最初的谷歌魔镜本身没有提供头部固定装置,在测试时我使用了自己制作的绑带将其固定在我的头部。这对于检查我的场景对性能的要求和基本的平台兼容性(包括视野的范围、焦距、舒适度等)很有帮助。
这种组合的首要目的就是使眼睛和身体更加舒适。因为在体验诸如Alien Apartment这样的场景时,一不留神就会花费很长的时间。而长时间佩戴使用松紧绑带的一些VR设备后,你的脸部会被压出很深的皱痕,那是令人十分郁闷的一件事!在这种情况下,Freely VR这类的设备是一个很好的选择。另外,对我的眼睛而言,Nexus 5的屏幕像素密度能达到的效果已经基本和最新的Oculus原型设备相同,这也是能良好体验大范围虚拟现实场景所需达到的最低要求了。
其实这个问题没有标准答案,这取决于你自身的体会。我不可能一一列举,但可以给出一些帮助和提示。其实,最具革命性的是VR开创了新的纪元。从最早的游戏(Pong)到目前最新版的游戏(黑暗旅行)都可以被重新策划、制作,带来完全不同的体验!
对于传统屏幕而言,创作一款与市面上游戏类似的作品非常容易。但是如果让你开发一款VR游戏,估计你会无从下手。不要灰心!虽然我们刚刚登上这片新大陆,但是有足够的时间来慢慢开拓这片蕴含极大潜力的处女地。
说明:
Pong是世界上公认的第一款成功的商用计算机游戏,由Atari(阿塔里)在 20世纪70年代推出,可以说是计算机游戏的鼻祖了。
试想一下这样的情景:你拥有一家能做出世界上最美味食物的餐厅,但是餐厅装修得非常糟糕而且坐落在偏僻的地方,生意应该十分惨淡。之后,你又开了一家新的餐厅,由于主厨辞职了,餐厅只能提供一些基本的食物。但是如果这家餐厅坐落在科罗拉多大峡谷的顶部,有着漂亮的玻璃屋顶,餐桌都是用周围的天然石材制作,用餐环境特色突出、新奇好玩,你将会赚得盆满钵满。
说明:
科罗拉多大峡谷是一个举世闻名的自然奇观,位于美国亚利桑那州西北部的凯巴布高原上,大峡谷全长446千米。由于科罗拉多河穿流其中,风景优美,是联合国教科文组织选为受保护的天然遗产之一,也是美国的旅游胜地之一。
明白我的意思了么?这听起来确实有点荒谬,但也不是不可能。如果将餐厅比作一款游戏,食物可以看作画面的质量,餐厅的装修可以看作画面的吸引力,而地理位置可以看作游戏的可玩性。通常,高端的画面并不意味着一定受欢迎。这一点从最近一段时间游戏的流行情况就可以看出来,不管是像素化的游戏、黑白界面的游戏以及彩色画面的游戏都有流行的。
手机的GPU无论如何也不可能达到同时代PC显卡的处理能力,因此很难期望手机游戏通过渲染数以百万计的多边形组成的复杂场景来吸引玩家。但就像前面餐厅的例子那样,可以通过提高游戏的可玩性、创造良好的游戏氛围来吸引玩家。
我并不想假模假样地解释一下如何营造身临其境的氛围,但是我曾做过一些有益的尝试。其中很多做法是非常有效而且简单的,诸如利用云、极光、视角、背景音乐和音效增强游戏氛围等。身临其境的氛围对于一个游戏的成功来说十分重要,后面我还会回到这个话题。
我们有必要确定一下讨论的是同一件事情,虚拟世界需要做到以下几点才能带来沉浸式的体验。
你有在上面花费时间的欲望。
能让你产生情感反应。
一段时间内,你对现实世界中发生的事情不感兴趣。
即便什么都不做,你也会感到很放松。
不过,想要让每个人都有完全沉浸式的体验是非常困难的。以我为例,当不需要手柄进行控制时,我很容易沉浸到游戏场景中,但是当我要思考应该按下哪个按钮时,我就会立刻回到现实中。对于需要使用磁钮的情况也是如此,我总是不断地在想:“磁钮在哪个位置啊?”再加上拨动磁钮时会带来那萦绕在耳边的糟糕声音,这就更难使我忘记现实世界而沉浸到虚拟世界中了。
然而,大部分人似乎并没有对这样的情况不满,我认为应该是有些人更容易融入虚拟世界中。值得高兴的是,如果你成功解决了某些人难以融入虚拟世界的问题,剩下的事情就会比较容易解决了。
我觉得营造一个身临其境的氛围理解起来比较容易。沉浸式的体验已经有点抽象了,而真实感和依赖性是更复杂的沉浸式体验的延伸。让我们先来谈谈依赖性,如果你玩过Alien Apartment这款游戏,就会发现没有太大的可重玩性。虽然刚上手时有点小小的困难,可一旦你通关了这个游戏,短期内就不会有再玩一次的欲望了。不过这并没有对这个游戏产生任何负面影响,许多人都给我来信说他们非常喜欢这款游戏,每次完成游戏都能让他们的身心得以放松。
我很难轻易理解为什么会出现这种情况,于是我做了一个测试,比较一下现实世界的我躺在床上和虚拟世界的我躺在床上有什么不同,但并没有什么特别的感觉。有一次,我闲来无事打开了游戏,在选项中关闭了游戏的背景音乐,然后随便选择了一个关卡。当时我有一种灵光突现的感觉,我很难用语言来形容它,如果你也进行过相同的体验后就会明白我当时的感受了。
我真的在里面!就像我平时做的那样,站在窗前,风扇嗖嗖地转着,四周环绕着 Sade的歌《Sweetest taboo》,我沉浸在美好的回忆中,极光映在天空中显得格外美妙。从那时起,我去Alien Apartment的目的就变成了从起初的做测试到如今的享受音乐,这就是我所说的依赖性。
我觉得只有真实感才能带来依赖性,那么下面再来谈谈真实感。这个时刻我感觉现实世界的我真真正正在Alien Apartment里面。并不像传统的沉浸式体验完毕后回到现实世界时会有昏昏欲睡的感觉,而是现实世界中的你真的处在虚拟世界中。这很难用语言描述,最好是自己亲自去尝试、体验。以下方式可以帮助你更好地体会到这种感觉:
把现实世界中你喜欢的东西放进虚拟世界中(如音乐、照片等)。
做一些现实生活中常做的动作(如躺在床上、坐着等)。
谷歌魔镜的设计非常简单,这种设计能让每个人都很容易上手。通过谷歌魔镜能让更多的人开始接触虚拟现实,进而实现虚拟现实的大众化。
我非常喜欢虚拟现实,但我认为“虚拟现实”这个说法并不是十分恰当。因为虚拟现实不一定必须是真实的,同样也没有“真实现实”与之对应。那么我们应该称其为什么比较好呢?其实我也没有更好的命名,不过我想称其为“交互现实(intertwined reality)”等类似的名称。
因为这不仅能说明虚拟世界中的体验是和现实生活紧密相连的,还能体现出你在虚拟世界中的感受和在现实世界中是一模一样的。此外,我可以告诉你,迄今为止你所有的体验都是真实的。不论这些体验是在虚拟世界还是在真实世界中。只不过一种是机器产生的体验,而另一种是现实生活产生的体验。这也正是虚拟现实最大的缺点:机器产生的体验效果远远没有现实生活产生的体验效果好。
之前一直在讨论虚拟现实和现实世界的问题,下面我们来说说一个虚拟现实实践中存在的现实问题。最近许多PC端游戏的场景做得十分逼真,但我们很难直接将这些逼真的场景融入谷歌魔镜进行应用,主要原因如下。
尽管最新的手机GPU性能已经十分强劲,但手机的续航问题制约了其性能的全部发挥。
新款手机往往都有着很高的屏幕分辨率(如 Quad HD)。尽管这一点对虚拟现实十分有利,但由于需要对大量像素点进行渲染和处理,会消耗GPU较多的渲染能力。
虽然为了支持虚拟现实已经不需要对场景进行二次渲染,但是只要Unity不能原生支持谷歌魔镜,任何预料之外的事情都有可能发生。
VR 游戏中,性能是首先要考虑的问题,因为较大的延迟会严重影响虚拟现实使用者的体验。无论视觉上多么细腻真实,只要有较大的时延,真实感就无从谈起了。
我并不是说不能渲染非常逼真的场景,只要你的场景足够简单就可以(笔者这里的意思应该是场景中组成3D物体的多边形总量不能很多)。例如,你的场景中可以使用应用恰当纹理的天空穹作为背景,再包含数量不多的3D物体,这样不仅视觉效果良好,时延也较小,可以带来良好的用户体验。反之,如果你使用由大量3D模型组成的城市作为虚拟现实的场景,整体效果可能就远远达不到预期了。
依我拙见,大多数移动平台的用户都可以归为以下三类。
初级玩家:偶尔玩一下游戏。
中级玩家:把手机作为游戏的平台。
骨灰级玩家:但凡有空闲,都要玩游戏。
我并没有关于谷歌魔镜用户的准确数据,我接触的大部分用户都是一些游戏开发者、电脑朋克族或者仅仅是出于好奇才接触谷歌魔镜的人。但对于Alien Apartment这款游戏,还有其他两类用户:全景影院应用爱好者以及类似过山车那种非手动操控游戏的爱好者。因此开发VR应用时应该注意满足不同用户的各种需求,如游戏的操控方式、耗电程度等方面。
接着值得一提的是硬件,你可能想知道谷歌魔镜的用户都使用什么样的手机,答案是有很多种。出现在谷歌游戏开发后台的各种设备类型和数量是惊人的(约有1700种不同的设备)。尽管数据不完整,但我还是想和你分享在Alien Apartment中被频繁使用的手机种类。
图1-1所示的数据可能对你的项目计划有所帮助。
图1-1 不同型号手机的使用情况
实际开发时,不要总想着高配置的专用头盔能够良好运行你的VR应用就满足了。我的建议是,要充分考虑低端组合的设备能否顺利运行你的VR应用。每当我的游戏需要更新时,我总是用最低配置的设备组合(Nexus 4+谷歌魔镜)来测试场景。因为这样的组合很可能由于屏幕范围的缩小(被纸盒挡住了屏幕的一部分)而难以完整显示游戏中的菜单项或HUD项目(一种将玩家需要实时获悉的与游戏直接相关的信息显示在游戏画面上的方式),这非常有助于调试以提高游戏的设备兼容性。
现在面临的问题是不知道有多少用户拥有独立的操控设备(比如游戏手柄或蓝牙键盘),但从谷歌应用商店我了解到以下信息。
VR应用程序若只能靠外部控制器控制,会有差评。
VR应用程序若不支持外部控制器,会有差评。
VR应用程序若不支持低端VR设备,也会收到差评。
你明白我的意思吗?你应该为应对不同的情况做好准备!我曾试图在Google+发布调查来收集更多信息,但只收到80个回复,统计结果如下。
25%的玩家使用磁钮。
30%的玩家使用控制器。
45%的玩家不使用手。
根据项目的具体情况,你可以得出自己的结论。在我的项目中,用户更倾向于用游戏手柄操控的实现,同时这些用户也喜欢能够通过目光触发动作的设计。对于使用磁钮操控的应用,用户也希望射击类的动作能够通过目光来操控。
对于谷歌魔镜平台而言,你需要注意几点。首先是帧速率(FPS),专家建议FPS应该在60到120之间,这样才能带来良好的体验。实际在手机平台上运行时最高也就是60FPS,最低会到45~50FPS——这远远达不到专家建议的水平。当然,这很大程度上依赖于玩家手机的硬件情况。能够维持较低的延迟对于保持用户的舒适性是非常重要的,否则用户在体验了几分钟后就会感觉非常糟糕。对于某些人群来说,即使是保持在60FPS,游戏体验仍然不佳。值得庆幸的是,这类人群是小众群体。
提示:
并不是说智能手机的硬件能力肯定达不到60FPS以上,而是出于续航等方面的考量,大部分厂商都将智能手机等移动设备的最高FPS限制在60FPS。就笔者了解到的情况而言,目前没有一部智能手机的垂直同步设置高于60FPS。
其次,需要注意避免产生晕动症。我有过类似的经验,可以通过特定的场景画面在几秒钟内让所有人体验到晕动症的感觉。因此,在实际应用的开发中,下面的一些情况需要避免。
我曾有过一次这样的体验:我刚刚连上了手机,准备体验支持游戏手柄的新版Alien Apartment应用。由于代码的逻辑错误,将场景中的Y轴和Z轴倒置了。仅仅30ms后,我就有了晕动症的感觉,真的很难受。
偶尔丢掉几帧画面一般不会被用户注意到,但如果这种情况持续几秒后,情况就不容乐观了。
大家都有这样的经历,在平稳行驶的火车上我们很难意识到火车的运动,这种情况在VR体验中更加突出。研究建议在VR场景中的摄像机前面绘制一个虚拟的鼻子可以帮助解决这个问题,我也认为这种策略的确能起到一定的作用。但我认为基本没有这个必要,重要的是要确保离用户较近物体的运动符合经典的物理学并选择恰当的参照物。
最后一点也很重要,要注意用户交互界面的设计。我的意思是,即使你的游戏在使用过程中并不需要设置菜单,但在游戏开始之前总会有一些选项需要供用户选择。而当这些选项呈现于触控屏幕上时,会迫使用户摘下VR设备进行选择。总是辗转于虚拟世界与现实世界一般会引起用户的反感。好消息是,最近发布的谷歌魔镜SDK提供了一个通过目光进行输入的模块用以解决此类问题(这个模块的发布晚于我的应用,因此我还没有进行测试)。
对于点击类游戏(如打蚊子、打地鼠)的操控而言,鼠标并不是最好的选择。而智能手机采用的屏幕触控的交互方式在此类游戏中表现更佳。但鼠标具有精准定位、点击、拉动与拖曳等能力,对于没有触控屏幕的情况而言,用于玩此类游戏也是一个不错的选择。现在我们面对的是类似的情况,对于虚拟现实和谷歌魔镜而言最佳的操控方式还没有出现。然而,基于头部运动的控制可以完成很多操作,如通过点头、注视、倾斜头部等进行游戏操控和菜单选择。
到这里为止,读者应该对虚拟现实技术有了大体的了解。下一章将通过介绍如何创建Alien Apartment虚拟现实应用带领读者真正进入VR的世界。
Alien Apartment运行效果
开发者迈克尔·沃西耶
应用名称Alien Apartment
下载地址https://play.google.com/store/apps/details?id=com.software.mick.fargods.wip.demo
开发工具Unity
如果读者没有阅读前言以及出于其他需要,请先了解如下概要。
从小我就知道VR很强大并且前景广阔。
目前市场不大,内容碎片化较为严重,大的公司基本还没有参与进来。
尽管磁钮的设计很聪明,但很难通过它流畅地控制你在虚拟世界中的化身去完成很多动作,无法做到沉浸式的体验。
初步使用过谷歌魔镜后,我很快就从最开始的觉得VR很有潜力变为对于技术不成熟的失望了。一段时间后,我再次进入应用商店,重新尝试我曾看好的应用。我发现了如下所列的现状:
目前还没有一个应用程序可以做到不使用控制器就能真正使用。
的确有一些非常有创意的应用出现。
从应用商店的评论里可以看到,的确有些人喜欢使用游戏手柄进行操控。
本章案例中的代码是使用C#开源工具Mono进行编辑的,但语言是相通的,只要会Java、C++等就足够了,因为本章内容并不会涉及C #特有的技术。
现在打开Unity,我们来了解一下需要关注的内容。本章的目的是先来熟悉一下Unity,并使用Alien Apartment中的头部移动技术实现一个可以行走于其中的虚拟场景。最后一点也相当重要,尝试着去感受一下Alien Apartment身临其境般的现场感和代入感。
开始学习代码和案例之前,先来进行Unity的相关准备工作。我将会提供所有的基础步骤,并且为了便于突出重点,其间只会使用一些基本的操作与要素。
对初学者的提示:
可以通过鼠标右键单击Hierarchy面板在Unity中创建物体。当我使用术语transform时,指的是一个空的GameObject。开发者可以通过在Hierarchy面板中单击鼠标右键,然后选择Create Empty菜单项来创建空的GameObject。
首先创建一个新的Unity项目。要注意的是:Unity场景中一般默认包含摄像机和环境光,但本案例中摄像机是作为玩家的视野来使用的,所以需要先删除场景中的主摄像机(Main Camera),然后导入谷歌魔镜的开发包(从Unity菜单中依次选择Assets>Import Package>Custom Package进行导入)。
创建一个平面(plane),设置其位置(Position)为[0,0,0],缩放值(Scale)为[100,1,100],然后在“Mesh Renderer”组件中将“Cast Shadows”选项设置为“Off”。(创建的Plane尺寸太大的话,其投射阴影无用并且会影响性能,因此要关闭此选项。)
创建一个空的transform,修改其名称为“player”。然后右键单击其添加一个摄像机,命名为“Camera”,最后将“Camrea”作为“player”的子对象。此摄像机将作为玩家的“眼睛”,为玩家提供观察的视野。
将“player”对象移动到近似人类的身高,如[0,2,0]。最后在运行时的构建设置(Build Setting)中选择Android或iOS平台,然后保存场景。
说明:
此时各位读者的屏幕可能看起来和图2-1中的界面有所不同,此截图中的窗口对应的是我的首选布局,读者也可以将首选布局设置成此截图中的组合。
图2-1 笔者开发时的Unity界面
有时开发者可能希望混合使用头部运动和游戏手柄(或鼠标)来实现相关的控制。开始的时候,我认为在谷歌魔镜的 Demo 中实现这样的控制很简单。后来发现事情远远没有开始时想得那么简单,问题一遍一遍地重复,逐渐变得一团糟。对我而言,这是在有控制器支持的情况下发生的。在VR模式下,一些开发者想要使用游戏手柄来控制替身在虚拟环境下的左右旋转。我也曾经在将游戏手柄控制的坐标轴与头部运动控制的坐标轴混合时遇到了很大的困难。但是,大多数时候可以用一个简单的方法来解决这个问题。那就是在每一次输入的时候进行相应的虚拟变换,下面是具体的步骤。
在场景中创建一个空的transform对象,并命名为Framework。接着再创建另一个空的transform对象(命名为AxisManager)作为其子对象。
创建两个空的transform对象并分别命名为AlternagerAxis和CardboardAxis,然后将这两个对象拖动到刚刚创建的AxisManager下,作为AxisManager的子对象。
创建一个C#脚本并命名为“AlternateAxisControl.cs”,然后将其挂载到AlternateAxis对象上。
双击打开脚本,稍后将介绍该脚本的编写。
图2-2中给出的是经过上述步骤后的Unity界面。
图2-2 经过几个操作步骤后的Unity界面
接着给出的是MonoDevelop中的情况,如图2-3所示。
首先删除AlternateAxisControl.cs脚本中的Start()方法和Update()方法。这是因为在该脚本中实际并不需要使用这两个方法,出于性能考虑推荐删除它们。接着创建一个新的方法LateUpdate(),其在每次渲染摄像机视图(也就是屏幕上用户看到的画面)之前被调用(关于MonoBehaviour相关方法的细节将在后面的章节中给出),作用为管理运行时的输入(若在开发环境中运行则是鼠标的输入,若在Android中运行则是游戏手柄的输入)。该脚本的代码如下。
图2-3 MonoDevelop中的情况
1 using UnityEngine;
2 public class AlternateAxisControl : MonoBehaviour {
3 protected float y;
4 void LateUpdate(){
5 #if UNITY_EDITOR
6 y=Input.GetAxis("Mouse X"); //若在开发环境中运行,则使用鼠标输入信息进行旋转
7 #else
8 y=Input.GetAxis("Horizontal"); //若不是在开发环境中运行,则使用游戏手柄进行旋转
9 #endif
10 transform.Rotate(0,y,0);
11 //SimpleController.updatePlayer(); //现在先注释掉此行代码,将留在后面使用
12 }
13 }
单击Unity中的“运行”按钮,然后可以观察到AlternateAxis对象下transform对象中的y角度值会随着鼠标位置的变化而改变,接着再次单击“运行”按钮停止运行。
水平坐标轴的相关信息可以在Unity的Input中进行配置(Edit>Project Setting>Input),默认情况下为游戏手柄的坐标轴。
接着需要通过挂载的C#脚本来实现谷歌魔镜坐标轴的相关功能。首先创建一个新的C#脚本并将其挂载到CardboardAxis对象上,然后将脚本命名为“CardboardAxisControl.cs”。编写此脚本的代码之前,需要先创建一个代理类——APIProxy(在Unity中的asset目录下单击鼠标右键,然后选择Create>C# Script命令)。通过一个代理类来组织SDK相关API的调用可以帮助我们在未来更容易地集成新版本的谷歌魔镜SDK。代码如下。
1 using UnityEngine;
2 public class APIProxy{
3 public static void CardbboardUpdateState() {
4 Cardboard.SDK.UpdateState();
5 }
6 public static Quaternion CardbboardRotation() {
7 return Cardboard.SDK.HeadPose.Orientation;
8 }
9 public static Vector3 CardbboardPosition() {
10 return Cardboard.SDK.HeadPose.Position;
11 }}
因为Google在谷歌魔镜SDK中已经提供了大部分的基础代码,所以谷歌魔镜坐标轴相关功能的开发就比较简单了。但是基于SDK中提供的代码进行开发也会有一个小弊端,那就是每次SDK升级后,我们可能需要花费数小时来升级相关的代码。因此我们将调用SDK中API的代码集中到APIProxy类中,这样就可以大大减轻每次升级的负担。APIProxy类的代码如下。
1 using UnityEngine;
2 using System.Collections;
3 public class CardboardAxisControl : MonoBehaviour {
4 private static CardboardAxisControl _this = null;
5 void Awake() {
6 if (_this == null)
7 _this = this;
8 if (_this != this) {
9 Debug.LogWarning("this should be a singleton");
10 return;
11 }}
12 //当需要根据头部的位置更新其他transform对象时,首先要调用此方法
13 public static void RequestHeadUpdate() {
14 _this.UpdateHead();
15 }
16 private bool updated;
17 void Update() { //每帧执行一次更新
18 updated = false;
19 }
20 void LateUpdate() {
21 UpdateHead(); //默认情况下在此更新头部的位置
22 }
23 private void UpdateHead(){ //计算出头部的新姿势
24 if (updated) return;
25 updated = true;
26 APIProxy.CardbboardUpdateState();
27 transform.localRotation = APIProxy.CardbboardRotation();
28 //SimpleController.updatePlayer(); //现在先注释掉此行代码,将留在后面使用
29 }}
接下来单击Unity的“运行”按钮,一边按住Ctrl键(或Alt键)一边移动鼠标,然后观察CardboardAxis对象下transform中的角度值,该值会随着鼠标的移动而不断变化。这是SDK为了模仿头部的运动而提供的一种非常便利的解决方案。读者通过界面中白色的竖条可能也注意到了SDK已经被调用(虽然目前还没有立体视图,但是在案例正常工作之前已经没有需要再添加的内容了),再次按下“运行”按钮停止运行。
到这里读者可能发现在单击“运行”按钮后出现了一个白色的竖条和一个齿轮图标,那就代表着谷歌魔镜SDK已经成功运行了。但是读者可能会有一个疑问:有了谷歌魔镜SDK的支持后,画面不是应该分为左右眼独立渲染吗?你的疑问是对的,我们还需要完成下面的步骤:
选中Camera对象,然后为其添加一个StereoController组件(添加组件的按钮在Inspector面板中),然后单击新添加组件中的Update Stereo Camera按钮。
单击该按钮后会立即出现一个左摄像机和一个右摄像机对象。选中这两个对象,可以看到这两个对象下都挂载有CardboaedHead组件,接着将CardboardHead组件从刚刚生成的这两个摄像机对象上删除。
再次单击“运行”按钮,在谷歌魔镜SDK的加持下,画面就分为左右眼独立渲染了(见图2-4),很简单吧!
图2-4 左右眼独立渲染
现在读者已经能够从两个摄像机中管理坐标轴的数据输入,接下来就需要做一些更实际的工作了。例如要开发第一人称视角的游戏,可能希望通过绕 Y轴旋转来实现玩家替身在虚拟世界中的转身。同时希望用另外一个坐标轴来旋转摄像机,下面来介绍这部分功能的开发。
首先,我们需要合并两个坐标轴。创建一个新的C#脚本“AxisManager.cs”,并将其添加到AxisManager对象下的transform上。该脚本代码如下。
1 using UnityEngine;
2 public class AxisManager : MonoBehaviour{
3 private static Transform unityaxis;
4 private static Transform cardbboardaxis;
5 private static AxisManager _this = null;
6 void Awake() {
7 if (_this != null)
8 Debug.LogError(this + "shuould be a singleton!");
9 _this = this;
10 unityaxis = transform.FindChild("AlternateAxis");
11 cardbboardaxis = transform.FindChild("CardboardAxis");
12 }
13 //获取方位角值(Azimuth或Yaw)
14 public static float getAzimuth() {
15 return (unityaxis.rotation.eulerAngles.y + cardbboardaxis.eulerAngles.y)
% 360;
16 }
17 //获取俯仰角值(Elevation 或pitch)
18 public static float getElevation() {
19 return (unityaxis.rotation.eulerAngles.x + cardbboardaxis.eulerAngles.x)
% 360;
20 }
21 //获取翻滚角值(tilt 或roll)
22 public static float getTilt() {
23 return (unityaxis .rotation .eulerAngles.z+cardbboardaxis .eulerAngles
.z)%360;
24 }}
然后需要创建一个游戏控制器,具体步骤如下:
创建一个空的transform,并命名为SimpleController。然后将其拖动到Framework对象下作为其transform子对象。
创建一个C#脚本,并命名为“SimpleController.cs”,然后将其挂载到SimpleController上。
选中Player的transform对象,然后在Inspector面板中添加一个Tag,命名为“Player”,接着将选中transform对象的标签从“Untagged”修改为“Player”。
下面开始编写SimpleController.cs脚本,其具体代码如下。
1 using UnityEngine;
2 public class SimpleController : MonoBehaviour {
3 private static GameObject player;
4 private static Camera playercamera;
5 private static bool trace = true;
6 private static SimpleController _this = null;
7 Vector3 direction = new Vector3();
8 bool walk = false;
9 void Awake() {
10 if (_this != null)
11 Debug.LogError(this +"should be a singleton!");
12 _this = this;
13 player = GameObject.FindWithTag("Player");
14 playercamera=player.GetComponentInChildren<Camera>();
15 if (trace)
16 Debug.Log("Connected controller to"+player+"."+playercamera);
17 }
18 public static void updatePlayer() {
19 playercamera.transform.parent.localEulerAngles =
20 new Vector3(0f,AxisManager.getAzimuth(),0f);
21 playercamera.transform.localEulerAngles =
22 new Vector3(AxisManager.getElevation(),0f,AxisManager.getTilt());
23 }
24 void Update() {
25 if (!walk && GetComponent<AxisProcessor>().islefttilt)
26 walk = true;
27 if (walk) {
28 direction = player.transform.rotation * Vector3.forward;
29 player.GetComponent<CharacterController>().Move(direction * 0.01f);
30 }}}
读者应该已经发现在AlernateAxisControl.cs和CardboardAxisControl.cs两个脚本中,都将调用updatePlayer()方法的相关代码注释掉了。现在可以将这两句代码前面的注释符号去掉,然后运行程序。此时按住Ctrl和Alt键,并拖动鼠标模仿头部的旋转,同时应该可以观察到player对象的y轴以及摄像机的X轴、Z轴是一起跟着变化的。运行时的界面如图2-5所示。
图2-5 运行时的界面
读者可能会想了解为什么使用updatePlayer()方法,这是因为当坐标轴一旦发生变化时需要更新摄像机对象的数据,而使用该方法来完成可以在最大程度上减少延迟。这样也可以防止和Player对象以及摄像机相关的物体在移动的时候产生抖动。
到现在为止,相关的开发工作比较简单,下面将开始复杂一些的部分,功能为使玩家能够通过轻摆头部在虚拟世界中到处走动。下面首先给出的是这部分功能开发所需了解的原理,着急的读者第一次阅读时可以先看后面的内容。但如果希望在应用里实现相关的头部运动检测功能,就需要返回这里仔细地进行学习。
从第一次完成Alien Apartment到最新版本在发展中的Whispering Eons上的更新,我已经尝试了很多次,并且保证这是目前我所发现的最好方法。那么,应该怎样检测出使用者头部的运动情况呢?我们可以选择一根特定的坐标轴,然后在特定的时间来进行测量并获取数据。接着通过两次测量来推导出运动速度。还可以进一步通过三次测量来推导出两个运动速度,进而根据这两个运动速度推导出加速度。实际我们可以采用如下伪代码所表示的算法:
1 if reading1 then statex elseif reading2 then statey else statez
最终我发现,较为快速简单的解决策略就是使用采样法(我不能保证这绝对是最好的方法,因此如果读者发现更好的方法,请进行尝试)。采样法的原理非常简单:以固定的时间间隔读取X、Y、Z轴的值,将这些值陆续存入缓冲替代旧值。同时对缓冲中的数据进行一定的数学处理,分析出玩家头部的运动情况。比如每125ms采样一次,缓冲中共存储8组数据,那么通过缓冲中的8组数据就可以得到1s内玩家头部的原始运动信息。根据运动信息的不同,可以分析出玩家头部的不同运动情况,常见的几种如下所列。
图2-6 玩家正在凝视某些内容时传感器3个轴读数的变化情况
图2-7 玩家向左倾斜头部时传感器3个轴读数的变化情况
图2-8 玩家点头时传感器3个轴读数的变化情况
到目前为止并没有什么高大上的内容出现,仅仅是把玩家 1s内头部运动基于 3 个坐标轴的测量值绘制成了折线图。读者也应该发现,自然人从折线图中比较容易分析出玩家的头部在1s内做了什么动作,但是用代码实现这一点就不太容易了。下面我们一起来试试!
首先,要对数据进行采样,并且将获取的坐标轴数据存储起来。具体做法为,在SimpleController对象上添加一个C#脚本,命名为“AxisProcessor.cs”,部分代码如下。
1 using UnityEngine;
2 public class AxisProcessor : MonoBehaviour {
3 //可以存储的采样数据数量
4 private static int samplesize = 6;
5 //当前事件采样值的索引
6 private int ibuffer=0;
7 //存储3个轴事件采样值的缓冲
8 private float[] xbuffer = new float[samplesize];
9 private float[] ybuffer = new float[samplesize];
10 private float[] zbuffer = new float[samplesize];
11 //是否需要采样新值的标志位
12 private bool trackingisdirty = true;
13 //FixUpdate函数的调用次数
14 private int fucount=0;
15 //当前读取到的坐标轴数据
16 float x,y,z;
17 //FixUpdate函数会被固定时间间隔调用
18 //默认为20ms一次,可以在Project Settings>Time中修改
19 void FixedUpdate(){
20 //每120ms获取一次样本数据,也可根据自己的需要修改
21 if (fucount > 5 && !trackingisdirty)
22 trackingisdirty = true;
23 fucount++;
24 }
25 //update方法每一帧都被调用
26 void update(){
27 x = AxisManager.getElevation ();
28 y = AxisManager.getAzimuth ();
29 z = AxisManager.getTilt ();
30 //负值代表视线低于水平线
31 if (x > 180) x = 360 - x;
32 else x = -x;
33 //负值代表头部向左歪
34 if (z > 180) z = 360 - z;
35 else z = -z;
36 if (trackingisdirty) {
37 sample(x,y,z);
38 trackingisdirty=false;
39 fucount=0;
40 }}
41 private void sample(float x,float y,float z){
42 if (++ibuffer >= samplesize) ibuffer = 0;
43 xbuffer [ibuffer] = x;
44 ybuffer [ibuffer] = y;
45 zbuffer [ibuffer] = z;
46 }
47 }
目前脚本中的代码十分简单,但现在还不能用它实现前面原理中介绍的非常有趣的动作分析。因为其中还缺少几个重要的数据处理函数,让我们将这几个函数添加到脚本中,具体内容如下。
1 private float sum,avg;
2 private int cnt;
3 //求数组中数据的平均值
4 private float aavg(float[] array){
5 sum = 0;
6 foreach (float f in array) sum += f;
7 return sum / array.Length;
8 }
9 //求标准差
10 private float astddev(float[] array){
11 avg = aavg (array);
12 sum = 0;
13 foreach (float f in array)
14 sum += (f - avg) * (f - avg);
15 return sum / array.Length;
16 }
17 //计算数据围绕一个值的波动
18 //“last”为最新添加的数据的索引,避免对以前的值进行重复计算
19 private int across(float[] array, float value,int last){
20 cnt = 0;
21 for (int i,j,k=last+1; k<array.Length+last; k++) {
22 i=k%array.Length;
23 j=(k+1)%array.Length;
24 if ((array [i] < value && array [j] > value) || (array [i] > value && array [j] < value))
25 cnt++;
26 }
27 return cnt;
28 }
简而言之,这将平滑读取到的坐标轴数据。玩家运动的幅度越大,标准偏差将越高。同时数值波动的次数可以帮助开发人员了解玩家的头部运动有多少次超过了给定的阈值。从中我们可以检测出一些有用的信息吗?是的!请看下面的3个例子。在AxisProcessor类中添加3个新的属性并进一步完善其中的sample函数。
1 public bool isstarring{ get; private set;}
2 public bool islefttilt{ get; private set;}
3 public bool isnodyes{ get; private set;}
4 private void sample(float x,float y,float z){
5 if (++ibuffer >= samplesize) ibuffer = 0;
6 xbuffer [ibuffer] = x;
7 ybuffer [ibuffer] = y;
8 zbuffer [ibuffer] = z;
9 //如果判断玩家的头部几乎没有运动,就设置isstarring为true
10 //本例中为3个坐标缓冲中的数据标准差都小于1度即可
11 //也可以根据需要自行调整
12 isstarring=(astddev(xbuffer)4&& //振幅为正负两度
21 across(xbuffer,aavg(xbuffer),ibuffer)>2);
22 }
现在我们有一个小型的缓冲用来存储读取的坐标轴数据,这足以进行复杂的运动检测。后继的内容中,我们将给出具体的用法。
提示:
很明显,sample函数可以进行进一步的优化以避免多次相同的重复调用,但这里我更希望优先保证代码的可读性。
首先我们需要向场景中添加一些物体,如图2-9所示。创建一个简单的Cube对象,使其成为Plane的子对象,并设置Cube的Position为[0.07,5,0.12]、Scale为[0.1,1,0.1]。另外,需要保证这个Cube对象能够被摄像机捕捉到(在摄像机的视野范围内),读者也可以添加一些其他的物体。
接下来再进一步丰富Player对象,在Hierarchy面板中选中“Player”,为其添加Character Controller组件。然后打开SimpleController脚本,向其中添加Update函数。具体代码如下:
图2-9 向场景中添加物体
1 Vector3 direction=new Vector3();
2 bool walk=false;
3 void Update(){
4 //下面的代码仅仅是一个简单的演示,读者可以根据具体需求进行更改
5 //注意不要多次反复的调用GetComponent函数,会影响性能
6 if (!walk && GetComponent<AxisProcessor> ().islefttilt)
7 walk = true;
8 if (walk) {
9 direction=player.transform.rotation*Vector3.forward;
10 player.GetComponent<CharacterController>().Move(direction*0.1f);
11 }}
现在就可以构建这个程序,并在自己的手机上运行测试了。
读者应该很快会注意到,现实中玩家的一次动作可能在程序运行时被多次检出。其实这也不奇怪,一次动作有持续的时间,如果在动作过程中程序进行了多次检测,自然会返回多个相同的结果。假定玩家刚刚结束了一次向左倾斜头部的动作,但是缓冲中的数据经数学处理后可能还是会得到向左倾斜头部的动作检测结果。
如果开发人员希望程序可以通过玩家向左倾斜头部的动作来控制虚拟世界中人物的行走和停止,那么就需要开发一个计时器来保证程序两次动作检测之间的最小时间间隔,以避免产生多个不需要的相同检测结果。这样,开发人员就可以充分发挥想象来进行动作检测以开发出新的动作类型并完成特定的操控任务。
前面提到过,在虚拟现实的人机界面中,用户不能使用鼠标这样的设备来方便地操控光标,一般只能通过他们的目光来进行操控。因此,与虚拟世界进行交互时就需要场景中存在可以与玩家交互的对象。一般用如下的方式来实现:首先需要创建两个脚本,一个脚本作为通用脚本(在场景中所有可以参与交互的对象间重用),另一个脚本实现接口,来描述每一个交互物体的具体行为。
首先从接口脚本开始,要注意的是由于需要实现一些普通的逻辑,因此我们开发了一个抽象类。用鼠标右键单击asset文件夹,在其中创建一个C#脚本并命名为“InteractionManager”,具体代码如下。
1 using UnityEngine;
2 public abstract class InteractionManager : MonoBehaviour {
3 //ready用来开启或关闭物体的可交互控制
4 public bool ready{get;set;}
5 public virtual void onAwake(){
6 ready = true;
7 }
8 //仅调用一次
9 public abstract void onStart();
10 //每一帧都会被调用
11 public abstract void onUpdate();
12 //如果这个物体在玩家的视野范围之内就每一帧都被调用
13 public abstract void onLook(long ms);
14 //如果物体存在屏幕上,就会在每一帧都被调用
15 public abstract void onVisible();
16 public abstract void onControl();
17 public abstract void onRelease();
18 }
当需要编码实现一些交互功能时,就需要对这个类进行拓展,并将其作为一个组件添加到场景中的可交互对象上。接下来继续创建另一个C#脚本,并将其命名为“Interactable”,具体代码如下。
1 using System.Collections;
2 using UnityEngine;
3 [RequireComponent(typeof(InteractionManager))]
4 public class Interactable : MonoBehaviour {
5 private static Interactable _current=null;
6 private static bool tracecontrol = false;
7 [Tooltip("Max distance to be looked from.")]
8 [Range(1,100)]
9 public float distance = 3f;
10 [Tooltip("send raycast every?(in ms)")]
11 [Range(0,1000)]
12 public long sampdelay=60;
13 private long cms,ms=0,mslook=0;
14 private bool isLookedAt=false,control=false;
15 private Collider _collider; //当前对象的Collider组件
16 private Renderer _renderer; //当前对象的Renderer组件
17 private Camera _camera; //代表玩家的摄像机
18 private InteractionManager _im;
19 protected RaycastHit hit;
20 //解除对当前对象的控制
21 public static bool releaseControl(Interactable io){
22 if (_current == io) {
23 _current=null;
24 io._release();
25 return true;
26 }else{ return false;}
27 }
28 //游戏对象请求控制函数
29 public static bool requestControl(Interactable io){
30 if (_current == null || !_current.isActiveAndEnabled) {
31 _current = io;
32 return true;
33 //控制距离玩家最近的对象
34 } else if (_current.hit.distance > io.hit.distance) {
35 Interactable.releaseControl (_current);
36 _current = io;
37 return true;
38 } else {
39 if(tracecontrol)
40 Debug.Log("Cannot give control to"+io+" "+_current+" has control");
41 return false;
42 }}
43 //初始化
44 void Awake(){
45 _collider=GetComponent<Collider>();
46 _renderer = GetComponent<Renderer> ();
47 _im=GetComponent<InteractionManager>();
48 _camera = GameObject.FindWithTag ("Player").GetComponentInChildren<Camera>();
49 _im.onAwake ();
50 }
51 //发射射线判断当前玩家是否注视着一个对象
52 private bool raycast(){
53 return _collider.Raycast (new Ray (_camera.transform.position,
54 _camera.transform.forward), out hit, distance);
55 }
56 void Start(){
57 _im.onStart ();
58 }
59 void Update(){
60 //Collider组件用于响应射线检测
61 if (_collider == null) {
62 if(_im.ready){
63 _collider=GetComponent<Collider>();
64 if(_collider==null){
65 Debug.LogWarning("Collider is missing!!");
66 _im.ready=false;
67 }}
68 return;
69 }
70 bool p = _im.ready;
71 _im.onUpdate ();
72 if (p != _im.ready && tracecontrol) {
73 Debug.Log(this+" state changed to ready="+_im.ready);
74 }
75 if (!_im.ready) {
76 if(_current==this){
77 Interactable.releaseControl(this);
78 return;
79 }}
80 //控制射线的发射频率,不需要每一帧都发射
81 cms = Mathf.RoundToInt (Time.fixedTime*1000);
82 if (!(cms - ms > sampdelay)) return;
83 ms = cms;
84 //判断当前的物体是否能够被观察到
85 if (_renderer != null && _renderer.enabled) {
86 if (_renderer.isVisible) {
87 isLookedAt = raycast ();
88 _im.onVisible ();
89 Debug.Log(isLookedAt);
90 } else { isLookedAt = false;}
91 } else { isLookedAt=raycast(); }
92 if (isLookedAt) {
93 if (!control && Interactable.requestControl (this)) {
94 if (tracecontrol)
95 Debug.Log (this + " got control!");
96 _im.onControl ();
97 _control ();
98 }} else if (control)
99 Interactable.releaseControl (this);
100 if (control) {
101 mslook+=sampdelay;
102 _im.onLook(mslook);
103 }}
104 private void _control(){
105 control = true;
106 mslook = 0;
107 }
108 private void _release(){
109 if (tracecontrol)
110 Debug.Log (this+" release!");
111 control = false;
112 mslook = 0;
113 _im.onRelease ();
114 }}
上述Interactable类有两个主要功能,如下所列。
第一,能通过从摄像机发射出的射线来检测进入到玩家视野中的带有Collider组件的物体,没有Collider组件的物体将不会被射线检测到。
第二,这个类能够保证在同一时间内最多只有一个可交互的物体被激活并控制,这样做是因为大部分情况下玩家不希望也不方便同时与多个物体进行交互。
有些时候,你还需要对这个类进行修改使其更加适合特定的程序。因为你的项目可能允许玩家使用目光与多个物体同时进行交互,或者你可能需要丰富其中的requestControl函数(比如在其中加入优先级机制)。你也可以丰富程序对不同人物姿态的处理机制,例如,你可以实现一个onStare函数并根据AxisProcessing类中isstarring变量的值来决定是否调用这个函数。
理论方面已经讲得足够多了,现在让我们来展示一下上述案例中的代码到底能做什么。实际运行前,还需要在场景中添加另外一个对象,具体步骤如下。
鼠标右键单击Hierarchy面板中的Plane对象,为其添加一个Cube作为其子对象。
设置transform组件中的Position参数值为[−0.02,1.5,0.25],Scale参数值为[0.005,3, 0.005]。
给此Cube对象添加一个C#脚本并命名为“DummyIM”,打开其进行编辑。
1 using UnityEngine;
2 public class DummyIM : InteractionManager {
3 public override void onStart(){}
4 public override void onUpdate(){}
5 public override void onLook(long ms){
6 Debug.Log ("you are looking at me!");
7 }
8 public override void onVisible(){
9 Debug.Log ("you can see me!");
10 }
11 public override void onControl(){
12 Debug.Log ("I have control!");
13 }
14 public override void onRelease(){
15 Debug.Log ("I lost control!");
16 }}
接着选中新创建的Cube对象,然后单击Inspector面板中的Add Component按钮,在打开的菜单中选择Interactable脚本,此时Inspector面板如图2-10所示。
图2-10 Inspector面板
需要注意的是:公共变量Distance和Sampdelay的值可以直接在Inspector面板中通过拖动滚动条来进行修改。单击Unity的"播放"按钮来运行程序,接着可以花一点时间来熟悉如何通过视线进行操控以及不同事件的触发方式,如图2-11所示。
图2-11 运行时的情况
当然可以,只是我在制作Ailen Apartment时避免了使用VR菜单。下面有一点提示给C#新手:有时可能会需要通过实例化预制件,使它们作为菜单中的选项,这无疑是一个正确的做法。但是你有可能会被一个问题所困扰,那就是如何让通过同一个预制件实例化而来的不同菜单选项去实现各自特定的功能,比如开灯和关灯。我的建议是创建一个MenuItem类,其构造器如下所示。
1 public MenuItem(string label , string text){
2 //......
3 }
你可能会想到通过像“MenuManager.addElement(new MenuItem("BGM", "Enable/Disable music"));”这样的语句来使用它。但是如何控制特定的菜单项去完成特定的任务呢?让我来告诉你所缺少的重要部分,这就是lambda表达式的使用,具体代码如下:
1 //...
2 public MenuItem(string label , string text , Action what_to_do){
3 //...
4 }
5 //...
6 private do(){
7 what_to_do.Invoke();
8 }
9 //...
10 somewhere(){
11 MenuManager.addElement(new MenuItem("Test" , "trace" , ()=>Debug.Log("Hellow world!")));
12 }
说明:
这个示例的展示并不完整,但是足以给出其中的要点。完整的示例将在后面的“通用协同程序管理”中给出。另外,需要的读者可以在“https://msdn.microsoft.com/en-us/ library/bb397687.aspx”中学习到更多关于lambda表达式的知识。
欢迎来到我最喜欢的部分!在这一部分我将介绍一些能够使用户对场景产生真实感和依赖感的常用技术和策略。
Unity 5.0中有两个很棒的新特性,一个是天空盒的可编程过程化材质,另一个是可根据天空盒来改变环境光的效果。你需要调节 4 个方面的颜色属性,分别为地面、地平线、天空和阳光,具体的操作步骤如下。
在Hierarchy面板中选中Directional Light对象。
在对应的Inspector面板中设置transform下的rotation属性为[20,160,0],并且将光的颜色修改为“#760608FF”。
在Project面板中创建一个新的材质并命名为“mysky”。
在对应的Inspector面板中将材质的Shader更改为Procedual、设置太阳的Size(尺寸)为0.4、Atmosphere Thickness(大气厚度)为2.5、Sky Tint(天空颜色)为“#3F7F6FFF”、Ground(地面颜色)为“#00DDFFF”以及Exposure(曝光强度)为0.4。
在Lighting窗口(可以在Window菜单中打开)中设置Skybox(天空盒)为“mysky”、Sun(太阳光)为“Directional Light”。
设置Ambient intensity(环境光强度值)为0.2、Reflection intensity(反射强度)为0.2以及Ambient GI(全局光照)为“Baked(烘焙)”模式(移动平台上出于性能考量不推荐使用实时光照)。
提示:
一旦完成了天空盒的调整,就可以使用反射探头来对其进行烘焙,这样做可以很大程度地提升程序的运行性能。
由于天空盒的使用,我们可以将落日晚霞和巨大的星体混合在一起,这样就创建出了一个更有吸引力的世界。接下来进一步设置Lighting窗口中的雾特效(Fog),首先选中Fog的复选框来启用雾特效并且设置其颜色值为“#AA2405FF”,再设置雾衰减模式(Fog Mode)为Exponential(指数衰减),设置雾浓度(Density)为0.03。到目前工作还没有完成,接着还需要使用一个更棒的工具——粒子系统(Particle System)。用鼠标右键单击“Plane”对象并且添加一个Particle System,接着选中添加的Particle System,其Inspector窗口如图2-12所示。
图2-12 Inspector窗口
说明:
不要忘记将Plane对象的Cast Shadows设置为“off”,并且取消对Receive Shadows项的勾选。
经过上述步骤的操作就可以轻松便捷地实现萤火虫飞舞的效果,读者也可以进一步根据自己的品位来修改参数已达到不同的效果。在Alien Apartment中,我通过同样的方式实现了极光的效果。你也希望我们能够在Demo里实现这样的效果吗?好的,让我们来一起做。首先将现有的Particle System复制并且重新调整一下其中的参数,具体内容如下:
将Transform中的Rotation修改为[300,180,0],Scale修改为[5,5,100]。
设置Duration(粒子发射时间)为5、Start Lifetime(粒子存活周期)为10、Start Speed(初始速度)为0,Start Size(粒子大小)为1000、Max Particle(粒子最大数量)为200。
修改Shape(粒子发射器形态)为Hemisphere(半球)、勾选Emit from shell项(从球壳发射粒子)并且取消Random direction项(粒子发射方向随机)的勾选。
将Renderer中的Max Particle size修改为150。
修改Color over Lifeime(粒子存活周期中的颜色变化)为渐变色,比如使用60作为alpha通道的最大值,具体情况如图2-13所示。
图2-13 调整颜色
还可能需要修改摄像机的Camera Far Clipping Plane(视锥体远剪裁平面)为10000这样比较大的值,以确保各个粒子都能够被渲染并显示在屏幕上,这是因为有些粒子会出现在离摄像机较远的位置,运行时效果如图2-14所示。
图2-14 运行时的情况
提示:
你可以选择Hierarchy面板中的CardBoard对象并将其对应的Inspector面板中的VR Mode Enabled取消勾选以关闭立体视角。
到目前为止,视觉效果已经足够了,现在需要关注听觉感受来进一步提升用户体验。让我们添加相关的音效,比如风声(或者其他你想要尝试的声音效果)。如果不知道去哪里寻找音效,可以去“https://www.freesound.org/”网站进行挑选。一旦选中了某个音效,有可能还需要截取音效中的一部分以保证它能够无缝地循环播放。
如果一个音效无法实现无缝地循环播放,往往是因为这段音频的开头和结尾的音量以及音高不同而造成的。在这里没有办法去修改音高,但是像Audacity这样的工具就能够帮助你通过音乐包络来调整音频。大部分情况下可以通过对音频最开始的2ms进行增强和最后的2ms进行衰减来完成调整。
完成了音频的调整后,将其导出为“.wav”格式的文件并导入需要的Unity项目中。然后在场景中添加一个Audio Source(音频源),将Audio Source的Audio Clip(音频剪辑)设置为导入的音频并且勾选loop(循环播放)复选框。同时,可根据具体应用的风格调整其他的参数。添加音频后需要试听音频时不需要每次都运行这个程序,可以在Scene(场景)中直接播放音频。
可以选择你喜欢的音乐,当然最好选择一个与整体环境氛围也相适应的音乐。添加一个新的Audio Source并将其Audio Clip设置为选中的音乐,并同时将其设置为2D音效。最后将程序编译并发布到手机上,将手机放入谷歌魔镜,就可以体验这个程序了。
总体来说,案例的整体体验可能并不完美,我的目的是想说明关于沉浸式体验很难用文字来完美地阐述,但是并不一定需要复杂的3D模型、高超的编程技巧以及丰富的场景。
这一节中我将尽力介绍我所知道的一些在开发过程中很实用的制作技巧。当然,我非常希望读者在学习完成后进一步去深入地挖掘这些制作技巧。
Unity 5中提供了强大的光照贴图(light-mapping)工具,并且当程序面向移动平台时,使用光照贴图是不可或缺的。因为当场景中超过了一个光源并且开启阴影渲染时,光照贴图可以大大地减少GPU的运算负担并显著提升FPS。这样做的缺点就是会增加APK包的体积,并且处于可移动状态的物体和光源无法使用光照贴图。图2-15是来自“Whispering Eons trailer”的屏幕截图。
图2-15 “Whispering Eons trailer”的屏幕截图
简而言之,光照贴图在提高场景光照氛围真实度的同时还可以显著提高实时渲染的FPS。
光照贴图既可以提供良好的视觉效果又可以提升性能,但是这并不是唯一的方法。下面介绍一些其他用于提升性能的小技巧。
Unity 场景中的摄像机有一个近平面和一个远平面,这就可以避免离摄像机太远的物体被渲染。主要是因为这些物体对象在最终画面中看起来会比较小,耗费大量的能力进行渲染不是很合算。然而,在某些特殊情况下,比如在渲染天空中的月亮时可能会遇到一些问题。这时可以编写一个简单的脚本,来设置某个层级剪裁平面的值,具体代码如下:
1 using UnityEngine;
2 using System.Collections;
3 [RequireComponent (typeof(Camera))]
4 public class CameraControl : MonoBehaviour{
5 public float[] distances = { 400, 400, 400, 400, 400, 400, 400, 400, 0, 800, 0,
6 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0 };
7 void Start () {
8 GetComponent<Camera>().layerCullDistances = distances;
9 }}
说明:
上述代码中distances数组中存放的是每个对应层级的远平面截距离止值,对于每个特定的层级距离摄像机超过截距离止值的物体不会被渲染。例如,上述代码中对于 0~7层级(这8个层级是Unity的内建层级)中的物体,当距离摄像机超过400后就不会被渲染。而第8层级中物体绘制的截止距离值是0,这表示采用摄像机默认的截止距离值进行判断。
熟悉OpenGL编程的读者都知道,最大限度地减少绘制函数的调用次数可以很大程度上提高性能。每个物体被渲染时都会调用绘制方法,减少绘制方法被调用次数的简单策略就是同时渲染几个采用同一种材质的物体对象。关于这方面,你需要知道以下几点。
动态批处理:运行时,Unity将自动批处理一些物体对象,尽可能复用材质。
静态批处理:Unity引擎在编译时对物体对象批处理进行预计算,选中对象属性面板的static选项即可实现静态批处理。
总而言之,通过批处理可以适当地提高帧速率(FPS)。
Unity的动态批处理有时可能会失败,此时可以通过手动合并模型网格来达到批处理的效果。下面脚本的作用就是将多个模型网格合并为一个整体。
1 using UnityEngine;
2 using System.Collections;
3 using UnityEditor;
4 using System.Collections.Generic;
5 public class CreateMergedMesh:ScriptableObject{
6 [MenuItem ("Prototype/Create Merged Mesh")]
7 static void CreateDataMesh() {
8 var go = Selection.activeGameObject;
9 MeshFilter[] mfs =go.GetComponentsInChildren<MeshFilter>();
10 GameObject newGo = new GameObject("Merged Mesh");
11 var newMf=newGo .AddComponent<MeshFilter>();
12 var newMr = newGo.AddComponent<MeshRenderer>();
13 Mesh masterMesh = new Mesh(); //创建网格
14 masterMesh.name = "Combined Mesh"; //将网格数据合并为一个整体
15 List<CombineInstance> combineInstances = new List<CombineInstance>();
16 foreach (var m in mfs) {
17 CombineInstance c = new CombineInstance();
18 c.mesh = m.sharedMesh;
19 c.transform = m.transform.localToWorldMatrix;
20 combineInstances.Add(c);
21 }
22 masterMesh.CombineMeshes(combineInstances.ToArray(),true ,true );
23 newMf.mesh = masterMesh;
24 newMr.material = new Material(Shader.Find("Diffuse"));
25 }}
提示:
需要注意的是,与3D建模工具提供的二进制操作相比,上述脚本的结果效率并不高,因为被遮挡的顶点与平面并没有被删除。
尽管不在Unity摄像机视野范围内的物体不会被渲染,但是还可以进一步通过对视野范围内的静态物体进行相关预处理来提高运行速度。只需要将静止物体对象设置为静态(Static)的,并在Occlusion(遮挡)窗口中单击烘焙按钮即可实现遮挡烘焙功能。这样在实际运行时,静态物体之间的遮挡将不再进行不断的重复计算,而是直接使用遮挡烘焙的结果。这与光照贴图有异曲同工之妙,渲染效率可以进一步提高。
有时候大脑中感知的景象并不是场景中实际呈现的景象。例如,将一个大象模型放置在离摄像机1m远的位置,并将其设置为实际尺寸的1/10,大脑将会使你错误地认为大象距离你有10m远。但是当画面的立体分离度太高时,这种错觉会消失。立体分离度很高往往意味着被观察的物体离摄像机很近,比如将模型放置于离摄像机25cm处的位置。
实际开发中,掌握好这种技巧是非常有价值的。例如在Alient Apartment项目中,像New Gaea这样的天体并没有采用离摄像机很远的球体模型,而是采用了具有透明度通道的纹理四边形来进行呈现。这个纹理四边形中的内容就是New Gaea天体的平面贴图。当然在这种情况下没有必要去使用由很多三角形构建而成的四边形,只需要采用由两个三角形组成的四边形即可。这样不但操作简单,而且可以一定程度上提高渲染速度。
尽管渲染质量的设置主要取决于项目的种类,但是这里还是给出一些关于渲染质量设置的建议,具体内容如表2-1所列。
表2-1
参数 |
建议 |
---|---|
Pixel Light Count(像素光源数) |
建议为一个,意味着只有一个实时光源采用片元着色器,其他光源采用顶点着色器渲染 |
Anti Aliasing(抗锯齿) |
大多数Open GL 2.0设备支持4倍的抗锯齿采样 |
Real-time Reflection Probes(实时采用反射探头) |
移动平台下建议不选择该选项 |
Shadows(阴影) |
尽管大部分情况下可以使用光照烘焙贴图来实现阴影,但是实时渲染阴影也是必不可少的。建议在渲染质量设置中启用阴影,这样就可以在每个光源中直接设置启用或者禁用阴影 |
V Sync Count(垂直同步) |
建议选择“Every V Blank”选项 |
提示:
渲染质量设置在菜单Edit下的Project settings中的Quality下。
Unity中所有新的标准着色器功能都非常强大,但是当项目的目标平台为移动平台时,必须注意以下两点。
出于性能的考虑,这些着色器能不用就不用。
基于OpenGL ES 2.0进行渲染可能会产生失真。
当需要切换场景或者是移动摄像机到另外一个位置时,尽量避免画面的突然变化。建议对于这种情况采用的处理方式为淡入淡出。
如果计划将Apk上传至应用商店进行发布,要记住相关Apk的签名,否则以后将不能上传对应的更新包。
目前还不清楚默认情况下Unity是如何存储资源信息的,这就使得不大可能在配置中管理Asset文件夹。但是开发人员可以通过切换版本控制模式来改变这一默认情况,需要的话请依次选择Edit→Project Settings→Editor→Editor setting来进行设置。如果不太熟悉,可以访问下述网址了解更多的细节:
http://docs.unity3d.com/Manual/ExternalVersionControlSystemSupport.html
Unity中有一些在运行时被按照一定的规律自动回调的方法,具体情况如表2-2所示。
表2-2
方法名 |
调用规则 |
最适用情况 |
---|---|---|
Awake |
游戏开始时被调用,在Start方法之前 |
初始化变量、组件等 |
Start |
第一帧之前被调用 |
初始化依赖于其他对象的变量 |
Update |
每一帧完成时被调用 |
更新一切与时间无关的对象 |
LateUpdate |
在更新每一帧时被调用 |
适用于较低延迟的对象,例如在VR中头部转动的方向 |
FixedUpdate |
定期(固定时间间隔)被调用 |
适用于与物理相关的对象,比如重力、速度等 |
用Unity中的协同程序(coroutine)触发事件应该是非常方便的,但是使用它需要编写较多行数的代码,对此我自己编写了下面这样一段脚本程序。
1 using UnityEngine;
2 using System.Collections;
3 using System;
4 public class SceneUtils : MonoBehaviour {
5 private static SceneUtils _this = null;
6 public static SceneUtils PROXY {
7 get {
8 if (_this == null) {
9 _this =UnityEngine.Object.FindObjectOfType<SceneUtils>();
10 } if (_this == null) {
11 Debug.Log("Creating sceneutils object");
12 var go = new GameObject("SceneUtils");
13 _this = go.AddComponent<SceneUtils>();
14 go.transform.localPosition = Vector3.zero;
15 }
16 return _this;
17 }}
18 public void runIn(float second, Action action) {
19 StartCoroutine(_runIn(second, action));
20 Invoke("kas",2.0f);
21 }
22 private IEnumerator _runIn(float s, Action a) {
23 yield return new WaitForSeconds(s);
24 a.Invoke();
25 }}
说明:
仅仅通过一次调用就可以进行使用,例如“SceneUtils.PROXY.runIn(1f, () => Debug.Log ("One second later..."));”。读者还应注意的是,这句调用中使用了lambda表达式。
一些构建参数对应用程序有很大的影响,相关的设置笔者建议如表2-3所示。
表2-3
参数 |
建议 |
---|---|
Default Orientation(App的默认屏幕方向) |
选择“Landscape Left” |
Rendering Path(渲染路径) |
大多数智能手机都支持“Forward” |
Static and Dynamic Batching(静态和动态批处理) |
选中该选项 |
Stripping Level(剥离等级) |
建议选择“Strip Byte Code” |
Split Application Binary(拆分App的二进制包) |
Play Store限制单个App不能超过50MB,但是你可以选中该选项,只不过上传时需要上传多个文件 |
Minimum API Level(最低的API版本) |
考虑到AR渲染的需求,建议选择Android 4.1 |
Automatic graphic API(自适应图形API) |
建议强制使用OpenGL ES 2.0,以提高App的兼容性和性能 |
默认情况下,返回键按下事件并没有被处理。但是我们可以自己开发用返回键退出的功能,相关的代码如下:
1 void Update () {
2 if (Input.GetKeyDown(KeyCode.Escape)) {
3 APIGateway.UnityExit();
4 }}
当VR程序运行时,用户一般较长时间不会触摸屏幕,这时屏幕可能会自动进入休眠。为了解决这个问题,应该将屏幕设置为永不自动休眠,具体代码如下:
1 Screen.sleepTimeout = SleepTimeout.NeverSleep;
着色器是在运行时用于渲染模型网格的代码片段,也可以说着色器是渲染任务的主要完成者。尽管Unity游戏引擎已经自带了种类繁多的着色器,但如果开发人员不熟悉着色器程序的基本开发,当需要用到一些着色器代码时,也会感到像陷入泥沼一样痛苦。因此,开发人员应该具备一定的着色器开发能力。不用担心,学习下面的几个案例会帮助你熟悉并学习着色器编程。
半透明可能是开发人员非常需要的特效之一,它可以通过能改变alpha值的简单着色器来实现。Unity自带的着色器中(Unlit/Transparent)可以实现透明效果。不幸的是,它不允许通过程序改变颜色。这意味着如果希望产生不同的色调,则需要不同颜色的纹理图。下面来编写一个带有颜色设置选项的透明特效着色器,在Project面板中单击鼠标右键,在弹出的菜单中依次选择Create→Shader选项以创建一个着色器脚本,如图2-16所示。
图2-16 透明特效着色器
1 Shader "Custom/Unlit/Alpha" { //着色器的菜单路径
2 Properties {
3 _Color ("Color Tint", Color) = (1,1,1,1)
4 _MainTex ("Tecture (RGB) Alpha (A)", 2D) = "white"
5 }
6 Category {
7 Lighting Off //该材质不受场景中灯光的影响
8 ZWrite Off //该材质具有alpha通道,不能写入深度缓冲
9 Cull Back
10 Blend SrcAlpha OneMinusSrcAlpha //混合操作
11 Tags {Queue=Transparent} //渲染队列为透明队列
12 Fog {Mode Off} //材质不接受雾的颜色
13 SubShader {
14 Pass { //可以有多个通道
15 SetTexture [_MainTex] {
16 constantColor [_Color] //颜色赋值
17 combine texture + constant
18 }}}}}
自发光也是开发人员非常需要的特效之一。虽然Unity自带了几种自发光着色器,但是这些着色器都依赖于光照工作。Unity也给出了如何通过自己的材质忽略光照的案例。当然也可以去使用非光照着色器来完成,但同样需要面临的问题是可以自定义的项目不够多。下面要介绍的着色器相对复杂,它是一个片元着色器。
图2-17给出的是用Unity非光照着色器实现的自发光效果。
图2-17 用Unity非光照着色器实现的自发光效果
接着,图2-18给出的是要介绍的自发光着色器的效果。
图2-18 自发光着色器的效果
1 Shader "Custom/Unlit/Selflit texture" {
2 Properties {
3 _Color ("Color", Color) = (1,1,1,1)
4 _MainTex ("Base (RGB)", 2D) = "white" {}
5 _Ambiant ("Ambiant", Range(0,1)) = 0.1
6 }
7 Category {
8 Lighting Off
9 Cull Back
10 SubShader {
11 Pass {
12 CGPROGRAM
13 #pragma vertex vert //这是顶点着色器
14 #pragma fragment frag //这是片元着色器
15 #include "UnityCG.cginc"
16 float _Ambiant; //着色器中的属性变量
17 fixed4 _Color;
18 sampler2D _MainTex;
19 struct v2f { //内部变量
20 float4 pos : SV_POSITION;
21 float2 uv : TEXCOORD0;
22 };
23 float4 _MainTex_ST;
24 //根据法向量以及UV映射应用纹理
25 //这里也是实施物体变形处理的地方
26 v2f vert (appdata_base v){
27 v2f o;
28 o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
29 o.uv = TRANSFORM_TEX (v.texcoord, _MainTex);
30 return o;
31 }
32 //渲染片元
33 //这里是实现混合以及颜色操作的地方
34 fixed4 frag (v2f i) : SV_Target{
35 fixed4 c = tex2D (_MainTex, i.uv);
36 return fixed4((c.rgb * _Color.rgb * (c.rgb+_Ambiant)) * _Color.a,1);
37 }
38 ENDCG
39 }}}}
当需要玻璃特效着色器时,我发现这方面的着色器比较匮乏。当然,Unity中提供了很多相关的着色器,如透明着色器和反射着色器,但是这些着色器不能同时实现折射和反射效果。我们也可以通过标准着色器加反射探头来实现,但是这样处理的话,GPU负载会很大并且反射效果不能精细调整。
因此,就不得不依赖于利用摄像机对象的“Camera.RenderIntoCubemap()”方法来渲染产生的立方图纹理。另外,也可以使用烘焙反射探头来实现。当然,2025年的移动设备GPU或许可以实时处理这种需求。所以我们需要一个能够同时处理透明度的反射效果着色器,具体效果如图2-19所示。
1 Shader "Custom/Reflective/Glass" {
2 Properties {
3 _ReflectColor ("Reflection Color", Color) = (1,1,1,0.5)
4 _Cube ("Reflection Cubemap", Cube) = "" { }
5 }
6 Category {
7 Lighting Off
8 ZWrite Off
9 Cull Back
10 SubShader {
11 Tags {"Queue"="Transparent"}
12 Blend One One
13 CGPROGRAM
14 //这是一个表面着色器
15 #pragma surface surf BlinnPhong nolightmap
16 #pragma target 3.0
17 struct Input {
18 float3 worldRefl;
19 };
20 samplerCUBE _Cube;
21 fixed4 _ReflectColor;
22 void surf (Input IN, inout SurfaceOutput o) {
23 o.Albedo = 0;
24 o.Gloss = 1;
25 fixed4 reflcol = texCUBE (_Cube, IN.worldRefl);
26 o.Emission = (reflcol.rgb * _ReflectColor.a *
27 _ReflectColor.rgb);
28 }
29 ENDCG
30 }}}
图2-19 玻璃特效着色器
这里给出的着色器同时实现了前面两个着色器的功能,既有反射透明效果,又有自发光效果,具体情况如图2-20所示。
图2-20 纹理玻璃特效着色器
1 Shader "Custom/Reflective/Textured Glass" {
2 Properties {
3 _Color ("Color", Color) = (1,1,1,1)
4 _MainTex ("Base (RGB)", 2D) = "white" {}
5 _ReflectColor ("Reflection Color", Color) = (1,1,1,0.5)
6 _Cube ("Reflection Cubemap", Cube) = "" { }
7 }
8 Category {
9 Lighting Off
10 ZWrite Off
11 Cull Back
12 SubShader {
13 Tags {"Queue"="Transparent"}
14 Blend One SrcColor
15 CGPROGRAM
16 #pragma surface surf BlinnPhong nolightmap
17 #pragma target 3.0
18 struct Input {
19 float2 uv_MainTex;
20 float3 worldRefl;
21 };
22 samplerCUBE _Cube;
23 fixed4 _Color;
24 fixed4 _ReflectColor;
25 sampler2D _MainTex;
26 void surf (Input IN, inout SurfaceOutput o) {
27 fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
28 o.Gloss = 1;
29 fixed4 reflcol = texCUBE (_Cube, IN.worldRefl);
30 o.Emission = (reflcol.rgb * _ReflectColor.a *_ReflectColor.rgb);
31 o.Albedo = _Color.a * c.rgb;
32 }
33 ENDCG
34 }}}
读者可以访问“https://www.google.com/get/cardboard/developers/”网址,浏览设计指南界面,并从Cardboard Design Lab中下载Google开发的VR App。希望读者喜欢Alien Apartment,并学会前面讲解的知识。下一章,笔者将讲解Mike Pasamonik开发的VR App——Glitcher VR。