第5章编程基础 本章首先介绍Unity3D编程基础知识,然后分别介绍HTC VIVE开发环境,投影式VR系统开发环境以及HoloLens开发环境等常见的VR编程环境。 5.1Unity3D编程基础 5.1.1Unity3D简介 Unity3D是目前最为流行的游戏引擎之一,具有强大的跨平台发布功能,同时Unity3D对各类虚拟现实、混合现实硬件设备提供了良好的开发接口,可以方便地对Oculus Rift、HTC VIVE 及HoloLens等主流VR/MR硬件设备进行集成开发,适合进行虚拟现实应用开发。本书统一采用Unity3D作为实例开发平台。 本章介绍Unity3D引擎开发基础知识,包括Unity3D的基本操作、动画角色的基本控制及虚拟相机的设置与使用,在后续的实战篇章节中将结合虚拟现实实例开发,详细讲述Unity3D开发知识。 Unity3D由Unity Technology公司开发,2004年推出最初版本。近年来,Unity3D强化了对各类交互硬件设备的开发支持,从传统的游戏开发逐步扩展到各类虚拟现实及人机交互等系统开发。经过十余年的发展,目前全球已有数百万开发者使用Unity3D进行产品开发。 Unity3D具有如下特点。 1. 支持跨平台发布 Unity3D支持跨平台发布,开发者不用过多考虑各平台间的差异,只需开发一套工程,就可以在Windows、Linux、Mac OS X、iOS、Android、Xbox One和PS4等不同系统下跨平台发布运行。在Unity3D中选择菜单File→Build Settings,在弹出窗口中可以选择不同的发布平台,无须额外的二次开发与移植工作,可以节省大量的开发时间和精力。 2. 丰富的插件支持 在Unity3D官方资源商店Asset Store中提供了大量的材质、粒子特效及物理仿真方面的插件,由此可实现许多视觉特效制作,节省开发时间,提高视觉效果。同时Unity3D也具有各类VR设备及交互设备的插件支持,例如免费的Steam VR插件及Kinect with MSSDK插件,支持在Unity3D平台中使用Oculus Rift、HTC VIVE等头戴式VR设备的开发及Kinect交互设备的开发。 3. 良好的集成开发界面 Unity3D提供了图形化的集成开发界面,用户可以通过鼠标拖曳完成代码、材质及各类组件与游戏模型之间的绑定工作。同时Unity3D提供了与主流三维动画平台Maya及3ds Max类似的三维模型交互操作界面,可以方便地将三维模型资源组合成所需游戏场景,如图5.1所示。 图5.1Unity3D的编辑器 4. 方便的代码开发与调试 早期版本的Unity(如Unity 5.x)支持采用JavaScript、Boo及C#语言进行代码开发,新版本Unity(如Unity 2019.x)只支持C#语言代码开发。其中,C#语言封装性好,学习上手快,便于快速开发和第三方代码移植,是进行Unity3D代码开发的首选。本书采用C#语言进行代码示例与开发。Unity3D的代码编辑与调试工作可以在微软的Visual Studio平台 图5.2层次视图 中进行,具有良好的代码输入提示功能,同时具有良好的断点跟踪及变量检查等代码调试功能,便于开发与调试。 5.1.2节 5.1.2Unity3D集成开发界面基本操作 本章主要介绍Unity3D的主要操作界面以及脚本编辑器。 1. 主要操作界面 1) 层次视图 层次视图(见图5.2)显示当前打开场景文件(Scene)在场景视图(Scene View)中显示或隐藏的所有游戏物体。 2) 场景视图 场景视图(Scene View)(见图5.3)可视化显示游戏场景中的所有对象,可以在此视图中操纵所有物体的位置、旋转和尺寸。在层次视图(Hierarchy)中选择某对象后按F键(Frame Selected),可在场景视图中快速找到该物体。 图5.3场景视图 3) 工程视图 工程视图(Project)(见图5.4)是用于存储所有资源文件的地方,无须担心数据量,只有在导出场景中用到的资源才会被打包编译。 4) 检视面板 检视面板(Inspector)(见图5.5)用于显示当前物体附带的所有组件(脚本也属于组件)。展开组件显示所有组件的属性数值。 图5.4工程视图 图5.5检视面板 5) 游戏视图 游戏视图(Game View)(见图5.6)可以把场景视图中所有相机看到的视角在这里显示,并执行所有物体中运行的组件和脚本。单击“播放”按钮进入播放模式。 2. 脚本编辑器 Unity3D采用C#作为脚本语言,在编写脚本时,使用VS作为默认开发环境。VS功能强大,可以同时支持以上三种脚本语言,图5.7是一段简单的脚本结构分析。 图5.6游戏视图 图5.7一段简单的脚本结构分析 图5.7中列举了Unity3D脚本中常用函数Start()和Update()。Unity3D脚本中的重要函数均为回调函数,Unity在执行过程中会自动调用相关函数。就本例而言,图5.7中的Start()函数仅在Update()函数第一次被调用前调用。而Update()函数在其所在脚本启用后,将在之后的每一帧被调用。Unity3D还提供其他大量在不同时刻调用的回调函数,读者可自行查阅相关资料。 5.1.3节 5.1.3动画角色控制 在虚拟现实应用开发中,虚拟动画角色的导入与动作控制是最基本的环节,用户可以通过各类体感输入设备控制虚拟动画角色完成不同的动作,本节以一个官方实例角色的导入与动作控制为例介绍相关基础知识。在本节中,用户仍使用传统的键盘交互进行角色控制,在后面章节介绍的系统中,用户将进一步通过体感输入设备进行更为自然的交互与控制,但基本的编程开发流程是一样的。 对于虚拟动画角色的导入与动作控制,Unity3D官方提供了一组标准资源,包括若干基本动画角色与基本特效,供用户进行学习与测试。自Unity 2018版本开始,官方标准资源(Standard Assets)需要开发者在官方资源商店网站Asset Store中进行下载和导入。 在Window菜单中选择Asset Store选项,在场景窗口上方的面板栏就出现了Asset Store栏,进入该栏后Unity3D将自动链接到官方Asset Store网站,用户可以下载各类资源,如图5.8所示。 图5.8链接到官方Asset Store网站 在搜索栏中输入Standard Assets关键字进行搜索,选择并进入官方标准资源包的下载页面,如图5.9所示,这一资源包是完全免费供开发者学习和使用的。 图5.9进入官方Standard Assets资源包下载页面 在页面中单击Import按钮,官方标准资源包将下载和导入至本地Unity3D工程目录中。 现在回到场景面板栏,在Project窗口中找到并进入Standard Assets→Characters→ThirdPersonCharacter→Models目录,将其中名为Ethan的项目用鼠标左键拖动到Hierarchy窗口中,将Unity3D官方提供的男孩模型Ethan导入场景之中,如图5.10所示。 图5.10将Unity3D官方提供的模型Ethan导入场景 接下来将为动画角色Ethan添加运动控制与动画控制。首先要说明的是,Unity3D导入的动画角色模型,一般包含一组基本动画供开发者调用。动画控制就是开发者通过代码来控制何时播放哪一种动画动作及动画动作之间的转换。这不仅是游戏控制的基本环节,也是虚拟现实交互的基本环节。首先让Ethan做一个简单的游戏动画(Idle),即一个角色在无输入控制时自然站立、身体自然轻微运动的动画,表示角色在等待用户进行动作控制。 首先在Assets窗口中右击出现浮动菜单,选择Create→Animator Controller命令,新建一个动画控制器元件,将名字对应改为Ethan,并将该项拖动至角色Ethan的Inspector面板中Animator组件下的Controller栏中,表明将通过该动画控制器控制角色Ethan的动画运动,如图5.11所示。 图5.11为角色创建一个动画控制器 接下来在动画控制器中进行动画控制的设置,在Inspector栏中双击Animator组件下的Controller命令,场景窗口自动切换至Animator栏,这是一个以节点图方式呈现的动画控制界面,每一个方块形的节点表示一个动画状态,通过节点设置及代码控制可以实现不同动画之间的切换,使角色根据用户输入完成指定的动画动作。 首先添加第一个动画节点,右击Create State→Empty,新建一个空的动画节点,在右侧Inspect栏中将节点名称改为Idle,表示此节点对应于角色等待动画。随后单击Motion栏,在出现的弹出菜单栏中,可以看到角色Ethan附带的一系列动画动作,选择其中的HumanoidIdle动画,如图5.12所示,运行游戏会发现角色Ethan开始执行等待动作。 图5.12在动画控制面板中设置节点与动画 接下来的控制分为两个步骤: 首先实现通过键盘上的上下左右箭头四个按键控制角色Ethan在场景中移动,控制动画角色在虚拟场景中的漫游。其中,上下箭头控制角色位置前进与后退,左右箭头控制角色的朝向进行左右转动; 随后在动画控制器中添加相应的动画动作,控制角色执行前后走动(跑动)及转向走动(跑动)。经过这两个步骤,就可以控制角色Ethan进行场景漫游。 在Assets窗口中新建一个C#脚本,更名为EthanControl.cs,将脚本拖动至Ethan角色,并双击脚本图标在Visual Studio中打开,并在Update()函数体中输入如下代码。 在上述代码中,前两行是Unity3D提供的标准键盘输入代码。变量h的取值为-1~+1,表示按上下箭头的强度: 按上箭头时,h取值为0~1,取值大小表示前进幅度; 按下箭头时,h取值为0~-1,取值大小表示后退幅度; 不按键时h取值为0,表示角色静止。同样地,变量v的取值也为-1~+1,表示按下左右箭头的强度: 按右箭头时,v取值为0~1,取值大小表示右转幅度; 按左箭头时,v取值为0~-1,取值大小表示左转幅度; 不按键时v取值为0,表示角色朝向正前方。在下面的步骤中,将通过键盘输入的v值与h值控制角色的位置、朝向及动作。 此时运行游戏,可以通过上下箭头控制角色前进后退,通过左右箭头控制角色转向。但此时角色在移动过程中,只是执行等待动画,呈现一种“漂浮”移动的感觉。接下来需要在动画控制器中为角色添加相应动画控制,使角色在受键盘控制移动漫游时,能够执行相应的走动和跑动动画。 首先分析一下动作控制的目标,轻按上箭头时,角色Ethan位置慢速前进,应相应执行向前走的动画,同时按左右箭头时,角色Ethan位置慢速转向,应相应执行向左走及向右走的动画; 长按上箭头时,角色Ethan位置快速前进,应相应执行向前跑的动画,同时按左右箭头时,角色Ethan位置快速转向,应相应执行向左跑及向右跑的动画; 最后按下下箭头时,角色Ethan位置后退,应相应执行向后走的动画。为实现上述动画控制,在Animator面板中设置混合树节点(Blend Tree),用于处理上下左右四个箭头按键的混合控制。 打开Animator面板,单击鼠标右键新建一个动画节点,更名为Move,用于控制向前、向左、向右三个方向上的走动及跑动的动画控制。再新建一个动画节点,更名为WalkBack,相应的Motion选项设置为Humanoid Walk Back,用于控制后退走动的动画控制,如图5.13所示。 图5.13新建两个动画节点 鼠标单击Move节点,在弹出菜单中选择Create New BlendTree in State命令,为Move节点创建一个混合树(Blender Tree)结构,用于混合控制向前走/跑、向左走/跑及向右走/跑共6组动画的切换。这6组动画通过两级混合树进行控制,第一级根据上箭头按键强度(变量h的取值大小),决定角色Ethan执行走还是跑的动画,第二级根据左右箭头按键强度(变量v的取值大小), 决定角色Ethan执行左转还是右转的动画,如图5.14所示。 图5.14设置两级混合树控制 可以看到,现在Animator面板中的混合树可以通过节点上的两个滑块控制6种动画之间进行切换。两个滑块分别名为Speed与Direction: Speed滑块对应键盘前进速度,Direction滑块对应键盘左右转向角度。后面可以通过代码中的v变量取值及h变量取值对应控制Speed与Direction两个滑块的取值,实现代码控制。 现在回到Animator面板的根界面,在静止等待节点Idle与移动动画节点Move之间设置两条往返箭头,进行动画状态转换设置: 由静止等待切换到移动状态的条件是Speed>0.1,即一旦按下上箭头,角色Ethan就开始向前、左、右三个方向移动; 由移动状态反向切换到静止等待的条件是Speed<0.1,即一旦松开上箭头,角色Ethan就开始执行静止等待动画。 接下来在静止等待节点Idle与后退动画节点WalkBack之间同样设置两条往返箭头。当Speed<-0.1时,即一旦按下箭头,角色Ethan就开始倒退行走,反之松开下箭头,角色Ethan就开始执行静止等待动画,如图5.15所示。 图5.15设置动画节点之间的转换路径 接下来在动画控制脚本EthanControl.cs中的Update()函数体中添加如下代码。 分别将变量v与变量h的取值赋给Animator面板中的动画切换滑块Speed与Direction,从而通过用户的键盘操作控制角色Ethan的动画切换过程。 最后在Hierarchy面板栏中将主相机Main Camera拖动到角色Ethan的里面。该操作的意义是将主相机设置为角色Ethan的子物体,即主相机将跟随角色Ethan前后移动及转向。由于现在主相机位于角色脑后方向,因此执行效果是以第三人称视角进行场景漫游。 现在可以通过上、下、左、右箭头按键控制角色进行动画并漫游。 本节通过官方自带动画角色,实现了一种简单的第三人称漫游控制。在例子中用户是通过键盘进行控制的,在后面章节中可以替换为使用深度相机、交互手柄进行控制,但动画切换控制的原理基本上相同,只不过更换了输入手段。 5.1.4节 5.1.4虚拟相机设置 1. 相机参数设置 在Hierarchy窗口中选择场景中的相机,在右侧Inspector窗口中就显示出相机的各项设置,如图5.16所示。 图5.16相机的各项参数设置 其中第一栏Clear Flags设置了虚拟相机背景画面类型,单击下拉箭头可以看到对应选项,默认选项为Sky Box,即设置相机渲染背景为天空盒图像; 第2选项为Solid Color,设置相机渲染背景为用户指定的单色图像,背景色在第二项Background栏中设置; 第3选项为Depth Only,设置相机渲染输出为表示场景3D深度次序的灰度图像,应用该项设置时相机输出画面用于特效叠加处理; 第4选项为 Dont Clear,设置相机的输出图像为前后帧叠加效果,用于产生运动模糊特效。通常情况下采用默认的Sky Box选项。 第三栏Culling Mask设置相机的渲染掩模,即设置相机可以看到虚拟场景中的哪些物体与角色。其默认选项为Everything,设置相机可以看到所有物体。单击下拉箭头可以按照虚拟场景物体的分层设置情况,具体选择相机只渲染指定层中的物体,而不处理其他层中的物体。这一设置的意义是可以根据漫游情况忽略过远处的物体,提高实时渲染效率。Culling Mask的设置既可以手动调整,也可以在游戏运行过程中通过代码实时调整。 第四栏Projection项设置相机的视景体模式。其默认选项为Perspective,相机设置为透视相机,渲染画面呈现近大远小的透视效果,用于显示3D场景; 如果设置为Orthographic,相机被设置为垂直相机,渲染画面无透视效果,用于渲染显示2D视图。 第五栏Field of View项用于设置透视相机的视角大小。视角越大相机视野也就越大,通常设置为60~90°,超过90°则会出现广角镜头效果。 第七栏Clipping Planes用于设置透视相机的近切面与远切面。透视相机只对近切面与远切面之间的物体进行渲染,超出范围的物体则不予处理。 第八栏Viewport Rect用于设置相机渲染输出画面在整个屏幕中的占比,具体有四个选项,其中,X与Y选项设置渲染画面左上角在屏幕中的位置,例如(X,Y)=(0,0)表示画面左上角位于屏幕左上角,(X,Y)=(0.5,0.5)表示画面左上角位于屏幕中心; W与H选项设置渲染画面的宽度与高度,例如(W,H)=(1,1)表示渲染画面的宽度、高度与屏幕尺寸相同,(W,H)=(0.5,1)表示渲染画面的宽度为屏幕一半,高度与屏幕相同。 第九栏Depth用于设置相机深度序号,用于处理场景中多个相机画面的深度叠加。Depth数值大的相机为前景相机,Depth数值小的相机为背景相机。前景相机画面会遮挡背景相机画面。 第十栏Rendering Path用于设置相机的渲染顺序,影响相机对场景中透明物体的渲染效果。 第十一栏Target Texture用于指定相机的渲染目标纹理,即相机的渲染输出可以不直接输出到屏幕,而是暂存于指定的目标纹理区域中,以便进行合成处理后再输出至屏幕。该选项常用来进行立体视频中左右眼画面的合成。 2. 渲染次序控制 在游戏开发场景中通常只有一台主相机负责画面实时渲染,但在VR应用中经常需要在同一个场景中使用多台虚拟相机,例如,实现立体显示时需要两台虚拟相机来模拟用户的左右眼,或者在虚拟漫游过程中需要从不同角度观察虚拟环境,都需要设置多台虚拟相机。本节介绍如何使用多台相机并控制其渲染次序。 在5.1.3节的角色控制实例中添加一台虚拟相机,该相机作为角色Ethan的子物体,放置在角色头部正中跟随角色运动。该相机以角色自身眼睛的视角观察场景,称之为第一人称相机。而之前主相机被放置在角色头部后上方跟随角色运动,不仅能看到前方的虚拟环境,同时也能看到虚拟角色的动作,称之为第三人称相机。下面以此为例讲述一下如何在场景中设置多台相机,从不同视角渲染场景并进行视角切换控制。后续章节中设置立体投影双目相机的基本原理与此相同,只是需要进一步细致设置双目相机位置绑定及调整视景体。 找到绑定在角色Ethan上的场景主相机MainCamera,通过菜单GameObject→Camera,在场景中新建两台相机作为第一人称相机与第三人称相机,分别命名为FirstPersonCamera与ThirdPersonCamera。将这两台相机拖动至MainCamera下,成为主相机的子物体,第一人称相机位于角色Ethan头部,第三人称相机位于角色Ethan头部后上方,选择这三台相机观察各种渲染视角,如图5.17所示。 图5.17选择这三台相机观察各种渲染视角 现在运行游戏程序,发现虽然场景中存在三台相机,但运行时输出的只有主相机视角画面,现在通过代码脚本控制第一人称相机与第三人称相机进行渲染输出,并使用户可以对两个视角进行切换显示控制。 为主相机MainCamera添加一个新建的C#脚本CameraControl.cs,在脚本类声明之后,添加两个Public型的Camera变量,用于访问和控制第一人称相机与第三人称相机。将两台相机图标拖到Inspector窗口中脚本对应的Public项中,建立相机对象与Public型变量之间的关联,如图5.18所示。 随后在脚本中再声明两个RenderTexture型变量。RenderTexture是指一种特定的内存空间,可以暂存虚拟相机的渲染画面,也就是说,虚拟相机的渲染结果可以不必直接输出至显示设备,而是暂存于指定内存区域中,待进行后续处理之后再推送至显示设备。脚本中定义的两个RenderTexture型变量分别用于存储第一人称相机与第三人称相机的渲染结果。然后根据用户控制选择其中一个视角画面进行显示输出。随后再定义一个int型变量ViewFlag用于记录当前视角选择,初始值设为3表示以第三人称视角显示输出,如图5.19所示。 图5.18在相机控制脚本中添加 Public型变量 图5.19在相机控制脚本中声明 RenderTexture型变量 接下来,在脚本的Start()函数体中添加如下代码,如图5.20所示。其中,第1、2行代码分别为声明的RenderTexture变量申请内存区域,内存区域大小与显示设备屏幕分辨率相同; 第3、4行分别将第一人称相机与第三人称相机的渲染输出指向对应的RenderTexture内存区; 第5、6行分别禁止第一人称相机与第三人称相机进行自动渲染,由用户指定某一个视角相机进行受控渲染。 图5.20对第一人称相机与第三人称相机进行设置 在Update函数体中添加如图5.21所示的代码,使用户可以通过键盘(F1键/F3键)控制输出视角。 最后在脚本中新建一个名为OnRenderImage的函数体,如图5.22所示。OnRenderImage()函数体与Update()函数体一样,在每一个游戏帧中进行调用,作用是在相机渲染输出至显示设备之前对渲染结果进行再次处理。其调用顺序是在Update函数体之后。 图5.21用户通过键盘控制输出视角 图5.22在OnRenderImage()函数体中控制两台相机渲染输出 其中第一个if判断的作用是,如果当前用户指定第一人称视角画面输出,则控制第一人称相机进行一次渲染,并调用Blit()函数将存放于RenderTexture内存区中的渲染画面输出至显示器destination; 第二个if判断的作用是,如果当前用户指定第三人称视角画面输出,则控制第三人称相机进行一次渲染,并调用Blit()函数将存放于RenderTexture内存区中的渲染画面输出至显示器destination,从而实现两台相机画面的切换输出。 保存脚本执行游戏,按F1键屏幕上显示第一人称视角画面,按F3键屏幕上显示第三人称视角画面,用户可以选择切换使用第一人称视角或第三人称视角进行虚拟漫游,如图5.23所示。 图5.23用户控制虚拟漫游视角 5.1.5节 5.1.5Unity3D中函数体的执行顺序 在游戏和VR应用中,虚拟环境中的各种事件总是并行出现的,虚拟角色本身会进行各种运动,而用户的交互也会改变虚拟环境与角色的状态,因此游戏引擎和VR开发都需要具有并行处理各类事件的机制,本节介绍Unity3D中并行处理各类事件时函数体的执行顺序,理清这一问题对深入理解Unity3D开发有着重要意义。 虽然各类操作系统中的多线程技术已非常成熟,但Unity3D仍然是以单线程运行为主的,实际上目前大多数游戏引擎也都是基于单线程的,其原因何在呢?多线程的一个重要优点是可以利用CPU空闲时间处理多个任务,提高资源利用率,但在游戏和VR应用中,角色事件处理与画面更新在响应时间上需要有很强的确定性与实时性,如果在逻辑更新与画面更新处理中使用多线程模式,那么处理多线程同步会大大增加开发消耗,所以目前大多数游戏引擎及VR开发的主逻辑循环部分都是单线程的。 在Unity3D中,每个角色对象都可以通过所绑定的代码脚本来控制和更新其行为,一个对象可以绑定多个脚本,共同控制该对象的多个或一个行为属性,在一个对象的脚本中也可以去控制和更新其他对象的行为属性。在每个脚本中最重要也最常用的就是Update()函数体,Unity3D主逻辑循环会根据硬件实际运行速率对每个脚本中的Update()函数体进行调用,在Update()函数体中来响应各种交互指令,并更新所属对象的行为属性,这样虚拟环境中的所有对象、所有角色看起来都像是在并行地运作了。 在代码脚本中除了Update()之外还有其他重要的函数体,图5.24中列出了脚本的基本类MonoBehaviour中的若干重要成员函数体的执行顺序,下面结合该图来进行介绍。 图5.24Unity3D中MonoBehaviour类成员函数执行流程图 图5.24中列出的函数体都会在游戏或VR应用运行过程的特定时刻,被自动地调用执行,这里着重介绍下面几个常用的函数体: Awake()函数体与Start()函数体在整个游戏运行过程中只会被执行一次,用于对所属对象进行初始化工作。FixedUpdate()、Update()及LateUpdate()函数体则会被不断反复调用执行,用于对所属对象的属性进行实时更新,从而控制所属对象的行为。OnPreRender()、OnPostRender()及OnRenderImage()函数体只能绑定在虚拟相机对象上,用于在每一帧中进行实时渲染的不同阶段进行相机设置、绘图处理及图像特效处理。 图5.24中列出的是一个脚本中各函数体的执行顺序,看起来简单明了,但要深入了解其中的机制并正确使用,就需要在多个对象多个脚本并行执行的背景下来理解。在虚拟场景中存在多个对象(例如多种角色及多个道具),而每一个对象又可以绑定多个脚本。下面介绍一下多个对象、多个脚本中的这些函数体的执行原则。 首先看用作初始化操作的Awake()与Start()两个函数体,我们已经知道在单个脚本中是先执行Awake()之后,再执行Start()。但在多对象多脚本情况下,一个脚本中的Start()并不是在该脚本中的Awake()执行之后就会被立即执行,而是要等到其他所有脚本中的Awake()都被执行完毕之后,才会开始调用执行,也就是说,所有脚本中的Awake()要在首轮中全部执行完毕,才会开始下一轮Start()的执行,如此设置是为了规范和调整各对象、各脚本之间的初始化顺序。下面来解释一下。 各脚本中的Awake()函数体是在场景中所有对象都被实例化创建之后被自动调用执行的,但各脚本中的Awake()的执行顺序是随机选定的。如果在Awake()中进行初始化的属性变量并不依赖于其他对象或其他脚本,就没有问题,但如果一个属性变量的初始取值依赖于其他对象或脚本中的某个属性取值,就可能会出现错误。例如,需要把场景中所有大精灵B的初始速度设置为小精灵A初始速度的一半,那么在大精灵B脚本中的Awake()函数体里进行这一设置时,小精灵A脚本中的Awake()可能还没有被调用,其初始速度值也没有初始化,这样就导致大精灵B的速度被初始化为错误数值(见图5.25)。此时就需要把大精灵B的初始速度设置工作放在Start()函数体中,这是因为当大精灵B脚本的Start()函数体开始执行时,所有脚本的Awake()函数体都已经执行完毕了,大精灵B的初始速度设置也就不会出现问题了(见图5.26)。 图5.25在Awake()函数体中进行初始化存在潜在错误可能性 需要补充说明的是,由于Awake()函数体是在场景中所有对象都实例化创建之后才开始执行,所以在Awake()之中可以正确安全地使用GameObject.Find()或GameObject.FindWithTag()函数去查找和获取其他对象的引用,但是通过引用去控制和改变其他对象的属性时,最好是放在Start()函数体或更后面的Update()函数体中执行才是安全的(见图5.26)。 图5.26利用Awake()与Start()的执行先后顺序正确进行初始化 理解了Awake()函数体与Start()函数体之间的执行顺序机制之后,可以类推地理解FixedUpdate()、Update()及LateUpdate()三个函数体的执行顺序。首先,Unity3D自动调用执行所有脚本中的FixedUpdate()(按照随机顺序),完毕之后再自动调用执行所有脚本中的Update()(按照随机顺序),完毕之后再自动调用执行所有脚本中的LateUpdate()(按照随机顺序)。这三个函数体对应了游戏对象属性的三个更新轮次: 首先第一轮是FixedUpdate(),该函数体是按照固定时间间隔反复自动调用的,两次调用之间的时间间隔不受硬件情况影响,在该函数体中适于完成对精确物理属性的更新工作。例如,动力学模拟中的受力更新或速度更新。后续两轮Update()与LateUpdate()的调用时间间隔则受到硬件情况影响,无法保证完全恒定。设置LateUpdate()的一个重要作用是为了正确处理对象之间的多重影响,例如在运行过程中,对象C的位置要同时受到对象A与B的影响,而对象D需要始终瞄准对象C。如果上述操作都在各对象的Update()函数体中执行,那么在同一帧中,对象D瞄准对象C在前,对象B调整对象C的位置在后,于是造成了对象D没有瞄准本帧中对象C的最终位置(见图5.27)。为此应该在对象A与B的Update()中去更新对象C的位置属性,等到所有脚本的Update()函数体都执行完毕后,在相机D的LateUpdate()函数体中再获取对象C的位置,并指向C,实现正确的瞄准效果(见图5.28)。 需要指出的是,Unity3D提供了通过手动或代码设置Script Execution Order(脚本执行顺序)属性的方式,允许用户自行设置和规定多个代码脚本的先后顺序,也就相应设置了各脚本之间Update()及LateUpdate()函数体的执行顺序。但对于场景中有众多角色对象的情况,利用Update()与LateUpdate()的先后轮次顺序来处理对象间的多重影响,避免造成冲突,这是一种合理而方便的开发技巧。 图5.27仅使用Update()函数体,无法确保正确处理对象间的多重影响 图5.28利用Update()与LateUpdate()的执行轮次顺序,正确处理对象间的多重影响 最后再介绍一下OnPreRender()、OnPostRender()及OnRenderImage()三个函数体的作用。这三个函数体所属脚本必须绑定在场景中的某个虚拟相机对象上,它们的调用顺序同样是按照先后轮次来执行的。OnPreRender()函数体在相机开始渲染之前被自动调用,在其中可以更改设置相机的某些参数,如开/关相机的雾效渲染功能,而OnPostRender()则在相机完成渲染之后被自动调用,用户可以在其中调用图形库函数,在渲染画面上附加绘制图标图形,如果这一操作放在OnPreRender()函数体中,那么用户绘制的图标图形就会被渲染画面所覆盖。所有渲染绘制操作完成之后,在将结果输出到显示设备之前,OnRenderImage()函数体被自动调用,允许用户在其中对渲染画面进行后期特效处理,例如对画面进行高斯平滑、运动模糊或画面泛光(Bloom)处理。 5.2投影式VR系统开发环境 在第3章和第4章中介绍了立体显示的基本原理,本节中进一步介绍如何通过Unity3D 平台实现立体投影显示功能。首先分析立体图像视差与立体显示效果的关系,在此基础上再介绍如何正确渲染生成立体图像,最后介绍如何按120Hz刷新率顺序显示立体图像,形成立体视频流。 5.2.1视差与立体显示效果的关系 左右眼图像中的视差使用户产生了立体视觉。在目前常用的立体显示技术中,只有水平视差,而没有垂直视差,虚拟场景中的一个物体在左眼图像中的水平位置为L,在右眼图像中的水平位置为R,则定义该物体的视差P=R-L。当物体距离较远时,会穿入屏幕出现在屏幕后方,称为入屏效果; 当物体距离较近时,会穿出屏幕出现在屏幕前方,称为出屏效果。出屏还是入屏取决于物体的视差,而视差有零视差、正视差、负视差及发散视差四种情况。下面结合图5.29进行具体分析。 图5.29视差与立体显示效果 零视差: 如图5.29(a)所示,物体在左右眼图像中的水平位置重合,视差P=0,此时物体出现在屏幕上。 正视差: 如图5.29(b)所示,当左右眼图像叠放在一起时,物体在右眼图像中的水平位置R位于左眼图像中的水平位置L的右侧,视差P>0。这时,物体出现在屏幕后方,即产生入屏效果。此时物体的距离与视差成正比——视差越大物体越远,当视差与用户眼间距相等时,物体出现在无穷远处。 负视差: 如图5.29(c)所示,当左右眼图像叠放在一起时,物体在右眼图像中的水平位置R位于左眼图像中的水平位置L的左侧,与用户的左右眼呈现交叉,视差P<0。这时,物体出现在屏幕前方,即产生出屏效果。此时物体的距离与视差的绝对值成反比——视差绝对值越大物体越近。当视差与用户眼间距相等时,物体恰好出现在用户到屏幕距离的一半处。 发散视差: 如图5.29(d)所示,当视差值大于两眼的瞳孔距时会产生发散视差。在真实世界中,该情况是不存在的。在立体显示时,此类情况即使存在很短的一段时间,也会使眼睛产生极为不舒服的感觉。因此,在立体显示时应该避免此类情况。 5.2.2渲染立体图像 5.2.1节中分析了视差与立体显示效果之间的关系,本节介绍如何在虚拟场景中设置立体相机的透视投影矩阵,保证立体相机能够渲染生成正确的立体图像。 普通虚拟场景中只需要设置一台相机,而为了立体渲染,就需要设置左、右两台相机,对应于观众的左、右眼。左、右两台相机在水平方向上有一定的间隔距离,相当于人的眼间距(Interaxial),此时左、右眼图像中就出现了视差。 需要注意的是,如果左、右相机仅进行单纯平移,两台相机的视锥体之间就不存在一个公共的截面(如图5.30(a)中的左右相机单纯平移和图5.30(c)中左右相机满足零视差面约束的情况下)。此时渲染出的左、右图像中就只有负视差情况,而没有零视差和正视差情况 详细推导见附录C。。为了修正这一问题,就需要为左、右相机的视锥体指定一个公共的截面(如图5.30(b)中的左右相机单纯平移后的视锥体和图5.30(d)中左右相机满足零视差面约束时的视锥体),即零视差面。虚拟场景中位于零视差面前面的点产生了负视差,位于零视差面后面的点产生了正视差,而恰好位于零视差上面的点产生了零视差 详细推导见附录C。,即位于零视差面上的点在左、右图像中汇聚为一点,因此称相机到零视差面的距离为汇聚(Convergence)距离。 图5.30立体相机的视锥体结构 左右相机的视锥体在零视差面上重合,称为零视差面约束,对比图5.30(a)与图5.30(b), 在满足零视差面约束条件下,两台相机的视锥体从原先的对称情况变成了非对称情况,这就需要重新计算左、右相机的透视投影矩阵。在前面介绍过一台虚拟相机的透视投影矩阵是由该相机近切面的上(Top)、下(Bottom)、左(Left)、右(Right),及近切面距离(Near)和远切面距离(Far)共6个参数决定的,而在左、右两台相机组成立体相机时,相机的透视投影矩阵还与两台相机之间的间距(Interaxial)及汇聚距离(Convergence)两个参数相关,左、右相机的透视投影矩阵参数分别为详细推导见附录C。: 左相机: Top=Near×tanFov2 Bottom=-Near×tanFov2 Left=-Aspect×Top+Near×Interaxial2×Convergence Right=Aspect×Top+Near×Interaxial2×Convergence 右相机: Top=Near×tanFov2 Bottom=-Near×tanFov2 Left=-Aspect×Top-Near×Interaxial2×Convergence Right=Aspect×Top-Near×Interaxial2×Convergence 其中,Fov是相机的视角,Aspect是显示设备的高宽比。 5.2.3播放立体视频 在VR开发中,通过设置左、右相机可以实时渲染生成立体视频,立体视频中的每一帧包含左眼和右眼两幅图像,需要根据投影仪中不同的立体播放模式,编写立体视频播放代码。 在前面章节中介绍过目前常用立体投影技术分为偏振式立体投影与主动式立体投影两类,偏振式立体投影主要在电影院环境中使用,而主动式立体投影更适合于搭建VR环境。目前商用投影仪基本都具备主动立体投影功能,并支持帧序列(见图5.31(a))、左右并列(见图5.31(b))、上下并列(见图5.31(c))三种播放模式。帧序列模式是指按照左—右—左—右…顺序依次播放左眼画面与右眼画面,图像刷新率为120Hz,即每秒各播放60帧左眼画面与60帧右眼画面。但帧序列模式不支持高清分辨率投影。左右并列格式是指左、右眼图像水平并列成一个双倍宽度的画面,例如,对于1080p分辨率的立体视频,每一帧图像的分辨率为3840×1080,并按60Hz的刷新率发送到投影仪,投影仪端自动将画面一分为二,按左—右—左—右…顺序播放,因此实际图像刷新率仍为120Hz。上下并列格式与左右并列格式类似,只是左、右眼图像垂直排列成一个双倍高度的画面。目前,左右并列、上下并列两种模式都支持1080p分辨率的立体视频播放。 图5.31常用的三种立体视频格式比较 在Unity3D平台中,开发者可以根据需要自行编写上述三种模式的立体视频播放代码。下面给出Unity3D实现左右并列格式立体视频播放的实现方法与代码示例。 (1) 左右相机摆置: 为左右相机设置一个共同的父物体,父物体对应于虚拟角色双眼连线的中心位置,父物体跟随虚拟角色进行位移和旋转变换。左、右相机在父物体坐标系下,按眼间距参数在x轴水平方向分别向左和向右进行平移。父子层级设置保证从不同位置和不同视角观看(渲染)虚拟场景时,左、右眼相机始终保持固定的眼间距取值,父子层级结构的设置在Hierarchy栏中完成。 (2) 设置相机透视投影矩阵: 根据用户指定的眼间距与汇聚距离(Convergence)两个参数,按照5.2.2节中的公式,分别计算左、右相机的透视投影矩阵。在编写代码脚本时,眼间距与汇聚距离作为脚本的public型变量,可供用户自行指定设置,具体见本节的代码示例。 (3) 左右相机渲染控制: 由于Unity3D中默认场景中主相机自动进行渲染输出,因此在进行立体渲染时,就需要通过代码控制左、右相机交替进行渲染,分别生成每一帧的左、右眼图像。在Unity3D中可以通过调用Camera.Render()函数控制指定相机完成一次渲染。 (4) 左右相机画面并列: 左、右相机渲染完成后,生成的左、右眼画面各自存放于显存中的RenderTexture区域中,可以通过调用Graphics.CopyTexutre()函数将两幅画面复制到一个双倍宽度的RenderTexture区域中,形成左右并列格式立体画面,推送到投影仪端进行播放。这一过程可在OnRenderImage()函数体中完成。该函数体每一帧被自动调用一次,用于在最终显示之前对相机渲染结果进行特效处理。 以左右并列格式为例,VR立体视频的实时渲染与播放流程图如图5.32所示。 图5.32VR立体视频的实时渲染与播放流程图(左右并列格式) 下面给出Unity3D中左右并列格式的立体视频实时渲染与播放的代码实例。对立体相机进行初始化设置的代码如下。 在立体相机初始化过程中,通过调用函数体GetProjectionMatrix()计算立体相机透视投影矩阵,该函数体代码如下。 控制立体相机实时渲染,生成立体视频流的代码如下。 根据上述代码实例的实现原理,读者也可以自己编写帧顺序及上下并列格式的立体视频播放代码。 5.3HTC VIVE开发环境 HTC VIVE是目前常用的头戴式VR设备,不仅具有良好的沉浸式立体视觉体验,同时也为用户提供了良好的定位跟踪及交互功能。本节介绍基于HTC VIVE进行系统开发的基本过程,包括环境配置、头盔显示以及手柄开发等内容。 5.3.1节 5.3.1环境配置 1. HTC VIVE的安装 可在官网上下载VIVEPORT按照安装教程进行HTC VIVE安装,下载过程中会安装SteamVR。SteamVR是HTC VIVE的运行插件,如图5.33所示。 图5.33安装HTC VIVE 也可以在计算机中安装Steam以及SteamVR。首先从官网下载并安装Steam(见图5.34),安装完成后进入Steam的商店界面,搜索并下载SteamVR。安装成功后,在Steam界面的右上角会有VR标志(见图5.35)。在对HTC VIVE进行开发时启动SteamVR设备,第一次启动时设置VR设备的房间环境,按照房间设置的一步一步提示完成即可。 图5.34安装Steam 图5.35SteamVR安装成功 2. Unity3D 中SteamVR SDK插件的下载与配置 在Unity3D中下载和配置SteamVR SDK。SteamVR SDK 是一个由 VIVE 提供的官方库,以简化VIVE开发。在布置好的场景中,单击Window→Asset Store(见图5.36),打开官网商店,搜索SteamVR Plugin,下载即可(见图5.37); 然后单击Import按钮,并单击编辑器中的Accept All按钮,如图5.38所示。 图5.36Asset Store 图5.37下载SteamVR Plugin 图5.38SteamVR Plugin的导入以及编辑器设置 注意在使用VIVE调试时,先对项目进行设置,单击菜单栏中的Edit→Project Settings→Player,找到Other Settings,勾选Virtual Reality Supported复选框,查看Virtual Reality SDKs中是否有OpenVR。如果没有,单击右下角的“+”进行添加,如图5.39所示。启用 OpenVR后,即可运行虚拟场景。 图5.39Unity3D设置 3. Unity3D中手柄开发插件VIVE Input Utility的下载 VIVE手柄开发可使用多种方式多种插件,这里介绍VIVE Input Utility插件的使用。首先在Asset Store中下载并导入插件VIVE Input Utility,如图5.40所示。VIVE Input Utility是一个基于SteamVR插件的开发工具,使开发者更方便地控制VIVE设备。本案例中使用该插件简单有效地开发VIVE手柄。 图5.40VIVE Input Utility的下载 4. 场景中虚拟相机以及手柄的设置 首先,由于HTC VIEV场景中需要的是3D相机,所以将场景中自动生成的Main Camera删除。然后选择SteamVR/Prefabs/[CameraRig]预制体并添加到场景中(见图5.41)。这个预制体是SteamVR提供的3D虚拟相机,与VIVE头盔直接绑定,可以直接在HTC VIVE中提供3D效果。为了方便后续设置,将它在场景中的位置设置为(0, 0, 0)。 图5.41CameraRig预制体 然后将VIVE Input Utility插件中Prefabs中的VivePointers预制体(见图5.42)添加到场景中,以备开发手柄功能使用。 图5.42VivePointers预制体 5.3.2HTC VIVE头盔 HTC VIVE头盔通过精确地跟踪定位以及逼真地呈现虚拟场景,给玩家带来真实的、高品质的沉浸式体验。VIVE头盔的跟踪定位是通过使用HTC VIVE定位器捕获头盔的位置信息与头部转向等信息,将信息映射到虚拟场景中,为虚拟场景中相应玩家的位置和动作提供数据,确定当前视场中的目标物、用户视点的位置和朝向,实现虚拟现实空间定位并提供浸入式体验。 HTC VIVE头盔与虚拟相机[CameraRig]直接绑定,头盔移动时的位置和旋转角度等信息也会反映在虚拟相机上, 图5.43[CameraRig]的 层次展开 所以头盔位置移动时相机位置也会随之移动。虚拟相机[CameraRig]可控制头盔的位置和旋转角度,包含的子物体如图5.43所示。其中,Controller(left)和Controller(right)是左右手柄,对应玩家的左右手,Camera(head)是玩家的头部,Camera(eye)对应玩家的眼睛视角。给[CameraRig]添加相关脚本设置其位置以及角度,控制相机的位置和旋转角度,使玩家的Head(即场景中视角)达到系统设定的Transform信息。可在C#脚本的Update()函数中调用方法transform.position或者GetComponent<Transform> ().position获取相机的位置,调用transform.rotation或者GetComponent<Transform>().rotation获取角度信息。 5.3.3HTC VIVE手柄交互 HTC VIVE手柄(见图5.44)可以像鼠标一样选中某个物体,也可以对物体进行抓取,也可以根据系统需要进行交互语义定义。 图5.44HTC VIVE手柄 一套VIVE设备中有两个手柄,分左右,开发的时候也是分左右的。每个手柄上面有一个圆盘和4个按钮。 系统按钮: 用来打开手柄。这个按钮不可以开发(默认)。在游戏中按下该按钮是调出系统默认的菜单,用来关闭和切换游戏用。 menu按钮: 默认用来打开游戏菜单。 grip按钮: 每个手柄有两个grip按钮,左右侧各有一个。 trigger按钮: 扳机按钮,用的最多,可以有力度等级区别。 pad: 触摸屏+鼠标的功能,可触摸,可点击。 后面三种是开发时常用的。 1. 按钮开发 首先,需要在C#脚本中进行引用,具体代码为using HTC.UnityPlugin.Vive。然后在该脚本的 Update()代码段中调用相关按钮的相关方法,实现特定的功能。每个按钮都有GetPress(按住时一直返回true)、GetPressDown(按下时触发事件)、GetPressUp(放开时触发事件)三种方法,用HandRole枚举来确定左右手柄,用ControllerButton枚举来确定是哪个按钮。具体实现代码如图5.45所示。 图5.45手柄按钮开发实现 其中,trigger按钮除了上述常见方法,还可以通过GetTriggerValue方法获得其模拟值triggervalue,范围是0~1,不同数值代表不同程度的按压,具体数值对应如表5.1所示。 pad有接触、按下两组方法,可返回事件点的位置等信息,如GetPadTouchAxis是返回接触点的位置信息,常用到的有六种方法: GetPadTouchAxis,GetPadTouchDelta,GetPadTouchVector,GetPadToPressAxis,GetPadToPressDelta,GetPadToPressVector。 其中,Axis是坐标位置,Delta是最后一帧移动位置,Vector是移动的向量。 表5.1triggervalue数值对应按压力度 triggervalue=0 没按 无 triggervalue=0.1~0.2 轻按 HairTrigger triggervalue>0.5 中度按 Trigger triggervalue=1 全部按下 FullTrigger 2. UGUI开发 在场景中添加预制体[CameraRig]和[VivePointers]后,新建一个UI按钮,如图5.46所示。 图5.46添加Button 禁用Canvas对象下的两个脚本,并设置模式为World Space,为Canvas添加Canvas Raycast Target脚本,如图5.47所示。 图5.47设置Canvas并添加脚本 将Canvas和Button调整至合适的大小和位置后,运行场景。运行以后,手柄会发出射线,当射线照射到按钮时,会有一个黄色的球,如图5.48所示。此时按Trigger按钮,就可以实现单击按钮的动作,按钮触发的具体事件可自行定义。 3. 通过射线远距离拖动物体 手柄射线照射到远处的3D物体时,可以通过Trigger按钮抓住物体并拖动。同样是在场景中添加预制体[CameraRig]和[VivePointers]后,新建一个3D物体,如Cube,为其添加脚本Draggable,若不能自动添加Rigidbody刚体组件,便手动添加上,如图5.49所示。 图5.48手柄点击按钮场景实例 图5.49为手柄点击按钮添加脚本 4. 触碰和拾取 在场景中添加预制体[CameraRig]和[VivePointers]后,新建一个3D对象,默认可以被触碰,再为其添加Rigidbody组件和Basic Grabbable,如图5.50所示,则该对象可以被拾取。 在3D物体上添加脚本Material Changer,可自行设置该物体在被触碰和拾取时的效果,设置参数如图5.51所示。 图5.50为可拖曳物体添加刚体组件和拖曳脚本 图5.51Material Changer脚本的参数设置 其中,Normal是默认贴图,Heightlight是触碰后的贴图,Pressed是按下按钮时的贴图,Dragged是拖曳时的贴图,Heightlight Button是指定的可响应按钮,默认是Trigger。实例如图5.52所示。 图5.52HTC VIVE手柄触碰和拾取物体实例 5.4HoloLens开发环境 HoloLens允许开发者在Unity3D中创建自己的混合现实应用程序,并部署在HoloLens中运行。本节将介绍如何在Unity3D中开发HoloLens应用程序并使其运行在HoloLens上。 图5.53Visual Studio 2017安装 详细信息 5.4.1环境配置 1. 安装Visual Studio 从官网下载Visual Studio,安装时需要勾选“通用Windows平台开发”“使用Unity3D的游戏开发”“使用C++的游戏开发”以及“Visual Studio扩展开发”。如图5.53所示为安装界面右侧安装组件的详细信息。使用Unity3D的游戏开发有一个可选的Unity3D编辑器,由于HoloLens官方建议使用LTS版本的Unity3D编辑器,因此此处不勾选。 安装Unity3D 2. 安装Unity3D 下载HoloLens官方建议的LST版本,安装时需要勾选支持UWP平台打包的组件,如图5.54所示。 图5.54Unity3D安装过程中的组件选择 3. 安装模拟器(可选) 微软提供了HoloLens模拟器,允许开发人员在PC上测试HoloLens应用程序。模拟器的运行对于PC的硬件配置有一定要求,要求如下。 (1) 操作系统: Windows 10专业版、企业版或教育版。 (2) CPU: 4核及以上64位。 (3) 内存: 8GB及以上。 (4) GPU: 支持 DirectX 11.0 或以上,驱动为WDDM 1.2及以上。 其安装过程如下。 1) Windows 10启用开发人员模式 Windows 10系统中启用开发人员模式的过程如图5.55所示: 在“设置”窗口中选择“更新和安全”→“开发者选项”,选中“开发人员模式”单选按钮。 图5.55Windows中设置开发人员模式 2) 设置虚拟化 如图5.56所示,重启计算机打开BISO界面,找到虚拟化选项并打开。 图5.56BIOS开启虚拟化 3) 启用HyperV 选择“控制面板”→“程序”→“程序和功能”→“启用或关闭Windows功能”,勾选HyperV及其子项前面的复选框,如图5.57所示。 图5.57启用HyperV 4) HoloLens模拟器安装 下载HoloLens模拟器,安装。模拟器运行效果如图5.58所示。 图5.58HoloLens模拟器运行界面 4. 引入MixedRealityToolkit MixedRealityToolkit(MRTK)是微软提供的混合现实开发工具包,旨在加速针对Microsoft HoloLens和Windows混合现实沉浸式设备应用程序的开发。其源码在GitHub上开放。 由于MRTK基于Windows 10 SDK 18362+,因此需要先下载安装相应的Windows 10 SDK。在Unity3D中引入MRTK的步骤如下。 (1) GitHub下载MixedRealityToolkitUnity。网址https://github.com /microsoft/MixedRealityToolkitUnity/releases提供了MRTK的各种发布版本。目前已经更新到v2.3.0。其中,Microsoft.Mixed Reality.Toolkit. Unity.Foundation.2.3.0.unitypackage必选,其他包可选,如图5.59所示。 图5.59MixedRealityToolkitUnity资源包下载页 (2) 资源包导入Unity3D。如图5.60所示,在Unity3D中新建工程,选择Assets→Import Package→Custom Package命令,然后浏览到资源包下载位置,选中之后进行导入。导入完成后弹出MRTK配置选项,单击Apply按钮。 图5.60MRTK导入过程 导入成功后,Assets目录结构如图5.61所示。 5.4.2节 图5.61MRTK导入后的文件结构 5.4.2开发实例 本节介绍如何在Unity3D中构建简单的HoloLens混合现实应用实例。 1. MR场景及配置 新建场景,在编辑器导航栏选择Mixed Reality Toolkit→Add to Scene and Configure命令,如图5.62所示。 图5.62场景中添加Mixed Reality Toolkit配置 该操作完成后场景中包含以下物体,如图5.63所示,主相机成为MixedRealityPlayspace的子物体。 图5.63场景配置结果 在场景中简单放置一个Cube用于测试,位置在设备正前方1m处,大小为25cm×25cm×25cm,分别绕x,y,z轴旋转45°,如图5.64所示。 图5.64场景中全息物体的设置 2. 工程打包 如图5.65所示,选择File→Build Settings命令,将目标平台改为UWP,选中Universal Windows Platform后单击Switch Platform按钮。单击Add Open Scenes按钮添加场景,然后单击Player Settings按钮进行项目配置。 在PlayerSettings中输入Company Name、Product Name和Version,其中Product Name为HoloLens中显示的应用程序的名称,如图5.66所示。 单击Build Settings中的Build按钮,在弹出的文件资源管理器中新建文件夹,将程序打包在该文件夹中,如图5.67所示。 图5.65打包平台配置 图5.66Play Settings中项目设置 图5.67新建文件夹保存打包文件 3. 应用程序部署 要将应用程序部署在HoloLens上,首先需要在HoloLens中打开开发者模式,打开过程类似于在Windows 10中打开开发者模式。程序打包完成后,用VS打开后缀为sln的文件,编译平台选择Release x86,如图5.68所示。开发人员可以选择将程序部署在HoloLens真机或者HoloLens模拟器上。 图5.68VS中调试选项 (1) 若在真机中调试,调试器选择Device。真机调试状态下,HoloLens通过USB连接到PC,在第一次进行部署时需要将PC和HoloLens进行配对。如图5.69所示,在HoloLens开发者选项中单击Pair按钮,会显示一个六位数字PIN码,单击VS中的Device开始部署程序,部署过程中按提示输入PIN,如图5.70所示,即可将程序部署在HoloLens中。 图5.69HoloLens中配对 图5.70VS中提示输入PIN HoloLens中运行效果如图5.71所示。 (2) 若在HoloLens模拟器中调试,调试器选择HoloLens Emulator。运行结果如图5.72所示。 图5.71HoloLens真机中的运行效果 图5.72HoloLens模拟器中的运行效果 5.4.3节 5.4.3交互实现 本节介绍如何在Unity3D中使用MRTK进行HoloLens的交互操作开发。MRTK对于不同的交互方式提供了不同的接口,接口定义在Microsoft.MixedReality. Toolkit.Input命名空间中。开发过程中只需实现这些接口即可进行不同交互方式的开发。以下将介绍HoloLens的三种基本的交互操作开发,分别是凝视、手势和语音交互。如5.4.2节所述,建立场景,在场景中放置一个cube作为交互对象,在场景中放置3D Text用于显示交互状态。 1. 凝视 凝视功能由IMixedRealityFocusHandler接口提供。本节实现凝视点进入和离开物体,物体颜色改变的效果,使用文字显示凝视点状态。图5.73为凝视交互的实现代码。首先,引入Microsoft.MixedReality.Toolkit.Input命名空间,然后实现IMixedRealityFocusHandler接口中OnFocusEnter()和OnFocusExit()两个函数。函数中为设置cube颜色和3D Text文本的代码。 图5.73凝视交互的代码 图5.74模拟器中的运行效果 图5.74为HoloLens模拟器中的运行效果。 2. 手势 手势功能由IMixedRealityInputHandler接口提供。本节实现用户凝视物体并执行Airtap手势时物体颜色改变,手势执行结束后恢复初始颜色,文字显示用户交互状态。图5.75为手势交互实现的代码。首先,引入Microsoft.MixedReality.Toolkit.Input命名空间,然后实现IMixedRealityInputHandler接口中OnInputUp()和OnInputDown()两个函数。函数中为设置cube颜色和3D Text文本的代码。 图5.75手势交互的代码 图5.76HoloLens模拟器中的 运行效果 图5.76为HoloLens模拟器中的运行效果。 3. 语音 语音交互实现过程如下。 1) 语音指令设置 选中场景中MixedRealityToolkit物体,在物体上的MRTK组件中选择Input选项。在Input选项下配置Speech交互方式,单击Add a New Speech Command按钮,输入新的语音指令。如图5.77所示,此处以改变物体颜色为例,加入Change Color指令。 图5.77语音指令设置 2) 绑定语音交互事件 加入指令后,在cube上添加Speech Input Handler脚本,添加语音指令,并绑定触发事件,如图5.78所示。 图5.78语音交互事件绑定 触发事件在脚本中定义,并绑定在场景中的物体上,图5.79为触发事件的代码,代码控制物体颜色改变和交互状态的改变。 图5.79语音交互事件代码 图5.80为HoloLens模拟器中的运行效果。 图5.80HoloLens模拟器中的运行效果 习题 1. 用Unity3D进行编程,实现一个基于HTC VIVE的赛车VR游戏。 2. 用Unity3D进行编程,实现一个基于HoloLens的混合现实系统,对房间中的汽车进行观察。 3. 分别编写Unity3D代码,实现帧顺序与上下并列格式的立体视频播放。