Unity的UI系统让开发者很容易创建用户界面,但是我们能将其用于VR应用吗?答案是肯定的。VR应用常用两种输入模式,项目的组件都支持:
第一种是GazePointer,用户通过头部、鼠标指针来与物体或者UI元素进行交互来控制指针。“点击”事件可能来自于游戏手柄的按钮或者敲击Gear VR触摸板。
第二种是与常规鼠标指针相似的一种指针,它移动穿过一个世界坐标平面(如悬浮在空中的UI面板)或者虚拟世界中的一个计算机模拟器。
我们首先简要介绍一下Unity UI系统是如何工作?
Unity的UI系统
Unity的UI系统包括一些重要组件:
–EventSystem
–InputModules
–RayCasters
–GraphicComponents: 按钮、开关、滑块等。
EventSystem是贯穿所有事件流的核心。它与其他一些组件关系紧密,包括InputModule组件,它是被EventSystem控制的主要事件来源。在Unity UI系统中,在场景中一次只有一个InputModule是活跃的。内置的鼠标输入模块和触摸输入模块的实现处理与指向源的状态(也就是说鼠标或者触摸)有关,而且它们负责检测这些指针事件与图形用户界面组件的交集。实际检测是在如GraphicRaycaster这样的射线投射类中实现的。
射线投射器负责一些不活跃的组件。当一个InputModule处理指针移动或者触摸时,它检测它知道的所有射线投射器,并且每个射线投射器会检测是否事件触发了自己的任意组件。在Unity的UI系统中有两种内置射线投射器:GraphicRaycaster(针对画布)和PhysicsRaycaster(针对物理对象)。
在一个鼠标驱动或者触摸驱动的应用中,用户触摸或者点击屏幕上的一些点,这些点对应应用视口上的点。从相机角度,视口中的一个点对应空间中的一道光线。因为用户可能打算沿着这条路线单击任何对象,InputModule负责从所有的光线交叉点中选择最接近的结果,交点是在场景中各种各样的射线投射器中找到的。
简单回答就是VR中没有屏幕,因此没有表面来让鼠标移动。
VR中提供GUI控制的一种方法是在世界坐标中为鼠标指针遍历创建一个虚拟的屏幕。在这种方法中,头部的运动并不控制指针,指针根据鼠标移动在虚拟屏幕上移动。注意这不同于Unity的世界坐标UI系统,在其中点击和触摸来自显示用户点击图片的摄像头,即使UI是在世界坐标中。
与VR应用程序交互的另一个常用工具是GazePointer,它总是发生在视图的用户视野当中,并且由用户头部运动控制。GazePointer也作为光线投射,但是来自你的双眼,不来源于UI系统期望的摄像头。如果你有一个跟踪的输入装置,射线可能甚至来自一个你手中的指针。
与鼠标指向和触摸不同,这些指针不能被描述成源自摄像头的射线。Unity的UI系统大多数面向整个屏幕的各个位置。
一线希望
幸运的是,修改UI系统不是太难,让事件系统在世界坐标中与射线相互作用,而不是使用与摄像头相联的屏幕位置。在未来,Unity的用户界面系统可能与射线工作在更深的层面,但现在我们采取在射线投射代码中使用射线的方法,为了与Unity的UI系统剩余部分相兼容,我们之后再把它转换回屏幕位置。
如果你打开博客结尾的项目小样,你会发现我们加入了源于内置Unity UI类的如下类:
我们稍后会检查每个类中的代码,在进一步讨论之前,现在是打开示例项目的好时机。
运行示例项目
项目是Unity 5.2项目,所以我们推荐你使用这个版本的Unity来打开这个项目。但是,所有的代码也可以在Unity 4.6和Unity 5.1上运行。
你也会需要OculusUnity实用工具的最新版本,如果你已经下载了集成环境,把它导入项目。
在项目中你会在场景目录中发现两个场景,Pointers和VRPointers。Pointers是使用正常的Unity UI画布和普通摄像头的场景。VRPointers和前者一样,但是存在OVRCameraRig和UI在VR中工作的安装程序。在我们继续之前可以随便试用这些场景,但请记住,您将设置"虚拟现实支持"选项关闭或打开来分别运行这些场景,你可以在播放器设置中找到此选项。
现在让我们看一下如何使用OVRInputModule,OVRRaycaster,和OVRPhysicsRaycaster(和几个其他helper类)来从非VR版本走向VR版本。一旦我们跨过了这道程序,我们将深入了解这些类是如何工作的。
一步一步UI转换
打开指针场景然后按下播放,在编辑器中运行它。请注意,它有如一个常规非VR应用程序——你可以在屏幕上移动你的鼠标并用它移动滑块和点击复选框。
现在让我们看看如何转换把这一幕转换成虚拟现实,请确保在继续之前你已经取消播放模式。
第一步:用OVRCameraRig取代摄像头
从场景中删除摄像头,从OVR->Prefabs目录中找到OVRCameraRig预制件,用它替换摄像头。(如果在你的项目视图中看不到列出的 OVRCameraRig,请确保您已经导入Oculus集成包了)。您可以选择将其放置在相机之前的位置,或其他任何你觉得提供良好UI视点的位置。
如果您使用的是Unity 5.1或之后的版本,那么在这一点上你应该也确保“虚拟现实支持”在独立播放器设置中是开启的。
第二步:改变InputModule
在层次结构视图中选择EventSystem。在监视器中,请注意它有一个StandaloneInputModule组件,这是正常的鼠标输入的输入模块。删除此组件(右击并选择删除组件)并且从Assets->Scripts目录中添加新的OVRInputModule。OVRInputModule处理基于射线的指向,因此我们需要设置次属性的光线变换属性。通过从OVRCameraRig拖动CentreEyeAnchor到此位置上做到这一点——这意味着你会用你的头进行指向。
为了启用游戏手柄支持,你应该也把OVRGamepadController组件加到EventSystem对象中。
第三步:添加一个GazePointer
我们想要在世界中添加一个围绕你的视角移动的视觉指针。在Assets->Prefabs目录中找到GazePointerRing预制件,并把它扔到场景中。OVRInputModule会自动查找它,当你环顾四周时,OVRInputModule会围绕你的视角移动它。请注意这个预制件上有一些其它的脚本做颗粒效果,这是一切都是可选的——与OVRInputModule工作唯一必需的部分是OVRGazePointer组件。
将OVRCameraRig对象拖到CameraRig位置上,那么OVRGazePointer组件才得以了解 CameraRig。
第四步:搭建画布
在VR中,任何世界坐标画布对象可以用几个变化来操纵。在此场景中确实有三个画布,所以您需要为每个画布重复此步骤。首先,让我们找到并选择下面的Computer对象下的JointsCanvas对象。
4a:您会注意到的第一件事是画布组件不会再有事件摄像头参考。因为我们删除了那个摄像头,所以现在让我们在OVRCameraRig中添加对相机之一的引用。在Unity 4.6你可以选择LeftEyeAnchor或RightEyeAnchor相机。在5.1以上版本,唯一的选择是CenterEyeAnchor。
4b:在使其中,你会发现画布上有一个GraphicRaycaster组件,它用来检测你的鼠标什么时候和GUI组件交织在一起。让我们把它删除,并用OVRRaycaster组件(在脚本目录中)替换,这会起到相同效果,但它适用于光线而不是鼠标的位置。
4c:在新的OVRRaycaster对象上,把Blocking Objects下拉框的选择改为All,这将确保我们的目光在场景中会被杠杆这样的对象遮挡。
UI目光指向准备好了!
在整个过程的这一点上,你应该能够运行场景并使用你的目光和空格键来与GUI元素进行交互。您可以把更改目光从空格的“单击”键改成OVRInputModule监视器面板中的任何其他键。您还可以配置一个游戏手柄按钮来充当目光-"点击"。请记住︰如果你只完成为 JointsCanvas的第4步,那么此时你将只能够凝视-点击那个帆布(有垂直滑块的粉红色那个)。
第五步:与物理对象交互
将OVRPhysicsRaycaster组件添加到OVRCameraRig。此新组件看起来与Unity的内置PhysicsRaycaster非常类似。在监视器中,你会注意到,它有一个事件掩码属性。此筛选器指定该射线投射器会检测到场景中哪个物体。将此设置为"Gazable"层。场景已设置,以便所有交互组件是在"Gazable"层。再次运行场景,现在试着目光点击场景中央的杠杆。
第六步:世界坐标鼠标指针
让我们将世界坐标指针添加到场景中。这个指针将采取行动,好像它是一个虚拟监视器上的鼠标,只有当你用你的GazePointer看画布时该指针才会被激活,这是一个在VR中提供熟悉输入设备的很好的方式。你想要为任何画布添加一个鼠标指针,都应该遵循这些步骤。现在,让我们选择JointsCanvas。
6a:找到 CanvasPointer预制件,将它作为画布的孩子实例化。此对象上没有花哨的脚本,现有的纯粹作为指针的可视化表示形式。你可以把这个换用任何一种你喜欢的2D或3D指针表示。
6b:把新加的指针拽到画布的OVRRaycaster指针参考位置上,这让它知道它应该使用此作为指针。
6c:向画布对象添加OVRMousePointer,它负责在画布上移动指针。
就是这样!现在如果你运行场景,你会发现你仍然可以使用凝视指针与UI交互,现在当你看着它时,你可以使用鼠标来控制画布上的虚拟鼠标指针。
这里需要特别注意的是,初始场景只载有正常Unity UI组件。所以你可以对VR启用的任何现与场景程执行完全相同的过程。
那么这些都是怎么运作的?
在这一节我们将看看我们使用上一节中的脚本如何允许您将现有的Unity用户界面转换为VR UI。脚本已经写了,按照上述步骤,它们可用于在您自己的项目中,我们不非要了解内部运行,但这一节是为对技术感兴趣的人设计的。
若要使奇迹发生,我们扩展的一些核心Unity UI类。咱们先来看看核心类处理我们输入的扩展……
OVRInputModule
Unity的StandaloneInput模块有很多指针交互和GUI元素导航代码,它本可以是我们继承的一个很好的类,但是不幸的是StandaloneInputModule的核心功能不是私有的就是受保护的,所以我们不能在我们的新类中用到所有的好东西。假如是这种情况,我们有三种选择:
1、将Unity的UI系统分支,建立我们自己的UI版本。在一个私有项目中,这本应是最好的选择,但是因为我们想要你能够尽可能在使用时少大惊小怪,我们不想要你安装一个新的UI DLL,所以这个方法不采用。
2、让Unity把这些功能改成受保护的。这会花费时间,但这确实是我们所追求的,多亏了Unity的合作,这些改变会发生。在未来,这个教程中讨论的扩展会变得简单些。
3、相反,从基类进一步继承,我们只需要从StandaloneInputModule将需要的代码复制粘贴到我们的类中。因为我们想让你能够尽快运行这个示例,并且尽可能在多的Unity版本中运行,我们选项了此项目。
组织
如果你看看OVRInputModule.cs,你会看到类从PointerInputModule继承,并在代码中有两个区域我们放置了StandaloneInputModule代码︰
#regionStandaloneInputModule code
#region ModifiedStandaloneInputModule methods
第一段是逐字移动的代码,如果它们不是私有的,第二段是我们已重写的功能。总的来说,OVRInputModule的变化都是 StandaloneInputModule的简单扩展。理解的最佳方式是看看代码。然而,以下的是关键更改的总结︰处理目光和世界坐标指针。
GetGazePointerData()和GetCanvasPointerData()两个新函数做GetMousePointerEventData在PointerEventData中做的事情,但是是针对我们新的类型的指针。这些都是我们处理指针输入的状态的扩展,例如:把空间视为GazePointer的"点击"键,为指针的方向使用赋值的射线变换。这些函数也调用OVRRaycaster/OVRPhysicsRaycaster找出GUI/ Physics的交叉点。但我们已经改变了我们与射线投射器说话的方式
用光线指向
我们已经取得的重要的变化是让子类PointerEventData产生OVRRayPointerEventData。这个新的类具有额外的成员变量︰ public Ray worldSpaceRay;因为它继承了PointerEventData,整个现有的包括EventSystem的UI系统可以把它看作任何其他类型的PointerEventData。重要的是我们新的射线投射对象了解此新成员,并且用它来纠正世界坐标光线交叉点。
用世界坐标指针指向OVRInputModule有如下成员变量:public OVRRaycasteractiveGraphicRaycaster;
它跟踪了它所认为的"活跃"的raycaster。你可以采取的各种计划,决定哪些raycaster处于活动状态(没有理由,你只需要一个),但在此示例中,当GazePointer进入时,OVRRaycaster组件声明自己处于活跃状态。其中OVRRaycaster当前处于活动状态是很重要的因为,因为OVRInputModule就是允许它检测画布世界坐标指针和GUI元素之间的交集。在此例中,你可以看到这种行为,事实上,当你通过GazePointer使画布活跃时,你可以为画布移动鼠标指针。
带回屏幕空间
可能OVRInputModule最重要的工作就是隐藏这样一个事实:我们用来自GUI系统的大量VR指针工作,比如:按钮、开关、滑块、编辑域,在这些元素中的大量逻辑都依赖于指针事件的屏幕位置。我们的指针是基于世界坐标,但是幸运的是,我们可以很容易将这些世界坐标位置转换成相对VR摄像头之一的屏幕位置(在示例中,我们随机选择了左眼)。这种策略施加的约束是你不能与不在摄像头前的UI元素进行交互——这看起来没什么不合理。
由于这种转换,以及OVRRayPointerEventData类只是PointerEventData的子类的事实,UI系统的剩余部分可以与PointerEventData对象交互,无需知道这些对象是来自于一个鼠标指针、GazePointer还是任何其他指针。
正在更新GazePointer
上述变化在技术上足以使目光指向在虚拟现实中起作用。然而,如果场景中没有你目光的视觉呈现,这就不会非常直观。OVRGazePointer是照顾这方面的单例组件,OVRInputModule有责任将其保持在适当的地点和正确的方向。
保持在正确的地方是足够简单——从射线投射回来的世界位置转发到 OVRGazePointer。稍微涉及到方向;一个幼稚的做法是给GazePointer定位,那么它就会总是面向用户(通过定位它朝向摄像头装置的 CenterEyeAnchor)。这其实就是当没有检测无交叉时OVRGazePointer做什么。但当有交叉时,也就是说你的GazePointer是实际上正对对象或UI画布时,OVRInputModule就会找到的GUI组件正常或物理对象的常规示范,并且用这将GazePointer与表面对齐。这就使得GazePointer感觉更附着在表面。
目光光标与UI表面对齐
总结
结果是,利用几个扩展Unity UI系统的新类,在虚拟现实中使UI 100%有效是可能的。这里的例子只是抛砖引玉——有很多方法可以扩展这个例子,用以实现在VR中与UI交互的新方式。GazePointer可以用跟踪控制器的指针替换;世界坐标的指针可以用游戏手柄移动;甚至可以使用世界坐标指针直接追踪跟踪输入控制器的位置——例子不胜枚举。
Unity采用越来越大特定的虚拟现实的功能,此示例中的一些代码可能会变得冗余,但现在就是现在。我们希望这篇文章可以帮助您现在就开始使用Unity伟大的UI系统。
(本文译者:赵箐箐)