交互和控制
由于A-Frame能支持各种平台、设备和输入方法。与2D网页不同,虚拟现实的交互方式是无限的。对于电脑、平板和手机,我们只需要考虑鼠标和 触摸输入,对于Cardboard,我们只需要考虑单个按钮,而对于更为复杂的VR设备,我们可以做任何事情:抓、扔、擦、转、戳、伸、压等等动作,就像人在现实世界中一样。 更进一步,混合现实中的跟踪和自定义控制器甚至提供了超出现实的交互方式!
我们在本节中可以做的是过一下现有组件的常用操作。我们还将演示这些组件是如何基于这些输入和交互而构建的,这样我们才知道如何构建自己组件的交互。 我们不能列举所有的操作,所以从某种意义上说,一个好的方式是授之以渔而不是授之以鱼。
事件(Events)
在2D网页中,输入和交互是通过 浏览器事件 (比如,click
, mouseenter
, mouseleave
, touchstart
, touchend
)。每当发生基于输入的事件时,浏览器就会发出我们可以倾听和处理的事件 Element.addEventListener
:
// `click` event emitted by browser on mouse click.document.querySelector('p').addEventListener('click', function (evt) { console.log('This 2D element was clicked!');}); |
和2D网页一样,A-Frame的交互性和动态新也依赖于事件和事件侦听器。然而,因为A-Frame是一个JavaScript框架并且一切是基于WebGL的,A-Frame的事件是模拟自定义事件,可以由任意组件来发出描述任何的事件。
// `collide` event emitted by a component such as some collider or physics component.document.querySelector('a-entity').addEventListener('collide', function (evt) { console.log('This A-Frame entity collided with another entity!');}); |
一个常见的错误观念是认为我们可以添加一个click
事件侦听器给一个A-Frame实体,然后期望我们能够直接用鼠标来点击该实体。 在WebGL中,我们必须提供产生该click
事件的输入和交互。比如,A-Frame的 cursor
组件在“注视”时使用光线跟踪创建一个模拟的click
。或者另外的例子, mouse-cursor 组件在直接点击在实体上时使用光线跟踪创建一个模拟的click
事件。
基于凝视(Gaze-Based)的光标组件交互
我们将首先讨论基于凝视的交互。基于凝视的交互依赖于我们的头部运动和视线观察。这种类型的交互是用于没有控制器的头戴设备。 即使只有旋转控制器(如Daydream,GearVR),交互仍然是相似的。由于A-Frame默认提供鼠标拖动控制,基于凝视的交互在桌面系统上可以某种程度上通过拖动摄像机旋转来预览。
要添加基于凝视的交互控制,我们需要添加或实现一个组件,A-Frame提供了一个光标(cursor)组件,当附加到相机上时能提供基于凝视的交互。
- 显式定义
<a-camera>
实体。之前,我们使用的是A-Frame默认提供的相机。 - 添加一个
<a-cursor>
实体作为camera实体的子元素。 - 配置光标所使用的光线跟踪器(raycaster),这是可选的。
<a-scene> <a-camera> <a-cursor></a-cursor> <!-- Or <a-entity cursor></a-entity> --> </a-camera></a-scene> |
使用event-set组件处理事件
现在我们来处理cursor组件提供的事件。cursor组件发送模拟事件如click
, mouseenter
, mouseleave
, mousedown
, mouseup
, 和 fusing
。我们将它们命名为类似于浏览器的本地事件以便新手能更快熟悉,但请注意它们是模拟的事件。
对于基本事件处理程序,我们在其中监听事件并在响应中设置属性,我们可以使用事件集(event-set)组件。事件集组件为基本事件处理程序提供声明式接口,其API看起来像下面这样:
<a-entity event-set__${id}="_event: ${eventName}; ${someProperty}: ${toValue}"> |
__${id}
部分使得我们在同一个实体上附加多个事件集组件。${eventName}
指定实例将处理哪一个事件。然后,我们传递要在实体事件发生时设置的属性名和值。
例如,当悬停在实体上或查看时使其可见。这个光标组件提供mouseenter
事件:
<a-entity event-set__makevisible="_event: mouseenter; visible: false"> |
如果我们想在鼠标悬停时改变一个盒子的颜色,稍后移开鼠标后再恢复过来:
<script src="https://aframe.io/releases/1.1.0/aframe.min.js"></script><script src="https://unpkg.com/aframe-event-set-component@3.0.3/dist/aframe-event-set-component.min.js"></script><body> <a-scene> <a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9" event-set__enter="_event: mouseenter; color: #8FF7FF" event-set__leave="_event: mouseleave; color: #4CC3D9"></a-box> <a-camera> <a-cursor></a-cursor> </a-camera> </a-scene></body> |
事件集组件还可以针对其他实体,使用_target: ${selector}
。比如我们想在实体悬停时显示一个文本标签:
<script src="https://aframe.io/releases/1.1.0/aframe.min.js"></script><script src="https://unpkg.com/aframe-event-set-component@3.0.3/dist/aframe-event-set-component.min.js"></script><body> <a-scene> <a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D" event-set__enter="_event: mouseenter; _target: #cylinderText; visible: true" event-set__leave="_event: mouseleave; _target: #cylinderText; visible: false"> <a-text value="This is a cylinder" align="center" color="#FFF" visible="false" position="0 -0.55 0.55" geometry="primitive: plane; width: 1.75" material="color: #333"></a-text> </a-cylinder> <a-camera> <a-cursor></a-cursor> </a-camera> </a-scene></body> |
事件集组件还可以和多属性组件一起工作,使用A-Frame组件的点语法(也就是${componentName}.${propertyName}
):
<script src="https://aframe.io/releases/1.1.0/aframe.min.js"></script><script src="https://unpkg.com/aframe-event-set-component@3.0.3/dist/aframe-event-set-component.min.js"></script><body> <a-scene> <a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4" event-set__down="_event: mousedown; material.wireframe: true" event-set__up="_event: mouseup; material.wireframe: false" event-set__leave="_event: mouseleave; material.wireframe: false"></a-plane> <a-camera> <a-cursor></a-cursor> </a-camera> </a-scene></body> |
使用Javascript处理事件
Remix this cursor handler example on Glitch
事件集组件可以用来处理基本操作,但对于复杂的事件响应处理,我们还是要通过JavaScript来完成(例如,进行API调用、存储数据、影响应用程序状态等)。在A-Frame中,我们规定把这些代码放在A-Frame组件中。
为了演示事件集组件的工作原理,让我们来看一个例子,使用JS处理刚才的盒子悬停变色情况:
<script src="https://aframe.io/releases/1.1.0/aframe.min.js"></script><script> AFRAME.registerComponent('change-color-on-hover', { schema: { color: {default: 'red'} }, init: function () { var data = this.data; var el = this.el; // <a-box> var defaultColor = el.getAttribute('material').color; el.addEventListener('mouseenter', function () { el.setAttribute('color', data.color); }); el.addEventListener('mouseleave', function () { el.setAttribute('color', defaultColor); }); } });</script><body> <a-scene> <a-box color="#EF2D5E" position="0 1 -4" change-color-on-hover="color: blue"></a-box> <a-camera><a-cursor></a-cursor></a-camera> </a-scene></body> |
使用简单的.setAttribute
,从技术上讲,我们可以在组件事件响应中处理任何情况,因为我们完全可以访问JavaScript,Three js,和Web API。
接下来我们将描述和实现VR控制器的交互性,但是事件和事件侦听器的概念仍然适用。
VR控制器(Controllers)
控制器对于虚拟现实应用的沉浸体验至关重要。使用六自由度(6DoF)控制器,人们就可以感觉接触到周围的景物,用手与物体进行交互。
A-Frame提供了系列控制器组件,通过各自的网络浏览器来支持 游戏手柄(Gamepad)Web API。有不同的组件来支持Vive, Oculus Touch, Daydream, 和 GearVR控制器。
要检测手柄或者获得游戏手柄对象的ID,我们可以在浏览器命令行调用navigator.getGamepads()
。这将返回一个 手柄列表(GamepadList)
数组包含所有已连接的控制器。
对于高级应用程序,控制器的构建要适配于应用程序(即定制的3D模型、动画、映射、手势)。 例如,一个中世纪的骑士可能有金属护手,或者机器人可能有一个能射出激光或在手腕上显示信息的机器人手。
A-Frame框架所提供的控制器组件主要作为一个默认实现,一个用来起步的组件,我们可以基于此组件派生更多自定义控制器组件。
tracked-controls Component
跟踪控制(tracked-controls)组件是A-Frame的基础控制器组件,为所有A-Frame控制器组件提供如下基础功能:
- 通过一个给定的ID或前缀捕获一个游戏手柄对象。
- 采用姿态(位置和方向)从手柄API读取控制器的运动数据。
- 查看手柄按钮对象值的变化,以便当按钮被按下或触摸时以及轴和触摸板发生变化时触发相应事件。 (i.e.,
axischanged
,buttonchanged
,buttondown
,buttonup
,touchstart
,touchend
).
所有的控制器组件框架建立在顶部的跟踪控制组件:
- 在实体上使用适当的手柄ID(比如
Oculus Touch (Right)
)设置跟踪控制组件。例如,vive-controls 组件调用el.setAttribute('tracked-controls', {idPrefix: 'OpenVR'})
方法。然后跟踪控制将连接到适当的手柄对象来为实体提供姿势和事件。 - 抽象化跟踪控件提供的事件。跟踪控件事件比较底层,我们很难仅仅基于这些事件分辨出哪个按钮是按下的。我们必须事先知道按钮映射。控制器组件可以预先知道各自相应组件的映射,并提供更富语义的事件,如
triggerdown
或xbuttonup
。 - 提供一个模型。单独的跟踪控件并不能提供外部显示。控制器组件可以提供模型,用于在按钮按下或触摸时显示视觉反馈、手势和动画。
对于跟踪控制器组件,只有当它们通过游戏手柄API检测到控制器被发现且被连接时才会被激活。
添加3自由度(3DoF)控制器(daydream-controls, gearvr-controls)
daydream-controls 和 gearvr-controls目前可在A-Frame主干代码上获得。
3自由度控制器(3DoF)只支持有限的旋转跟踪。3DoF控制器没有位置跟踪,这意味着我们不能碰触或者上下左右移动双手,就好比是拥有了一个没有手臂的手和手腕。阅读更多:虚拟现实的自由度。
3DoF控制器组件提供了旋转跟踪、默认的硬件设备匹配模式和抽象按钮映射事件。谷歌Daydream和三星的GearVR都支持3DoF,都是每只手各支持一个控制器。
要给谷歌Daydream添加个控制器,可以使用daydream-controls 组件。然后可以在一个Daydream手机上的Android Chrome上试试:
<a-entity daydream-controls></a-entity> |
要给三星GearVR添加一个控制器,可以使用gearvr-controls 组件。然后可以在Oculus Carmel 或 Samsung GearVR手机浏览器上试试:
<a-entity gearvr-controls></a-entity> |
添加6自由度(6DoF)控制器(vive-controls, oculus-touch-controls)
6自由度(6DoF)控制器同时拥有旋转和位置跟踪功能。和3DoF只能改变方向不同,6DoF还可以改变你的位置,这将使得我们可以在虚拟空间中自由移动。6DoF 让我们可以身前和背后伸展,移动我们的手跨过我们的身体或者靠近我们的脸。有6DoF就像现实里我们拥有双手和手臂,双手可以控制操作方向,手臂控制空间移动。6DoF也适用于头戴等其他设备。有6DoF是一种提供真正身临其境的虚拟现实体验的最低要求。
六自由度控制器组件提供完全跟踪,默认的硬件设备匹配模式和抽象按钮映射事件。HTC Vive和带触摸的Oculus Rift提供6DoF和双手控制器。HTC Vive还提供了跟踪现实世界中的其他对象进入VR。
要添加HTC Vive跟踪控制器,可以使用用于双手的vive-controls组件。然后可以在一个支持WebVR的桌面浏览器上试试:
<a-entity vive-controls="hand: left"></a-entity><a-entity vive-controls="hand: right"></a-entity> |
要给Oculus Touch添加控制器,可以使用用于双手的oculus-touch-controls 组件。 然后可以在一个支持WebVR的桌面浏览器上试试:
<a-entity oculus-touch-controls="hand: left"></a-entity><a-entity oculus-touch-controls="hand: right"></a-entity> |
支持多类型控制器
Web具有支持多个平台的优点。虽然目前还没定义清楚在VR中支持多平台需要具备哪些必要因素。因为3自由度 和6自由度平台提供不同的交互操作并要求不同的用户体验处理。因此这将取决于应用程序怎么来构建VR Web的响应式体验。这里,我们可以展示的是几种不同的方法,但没有一种是普遍适用的。
hand-controls组件
A-Frame通过hand-controls 组件来支持不同类型的6DoF控制器。hand-controls组件主要是用于六自由度 控制器,比较适合房间尺度的交互操作,例如物品抓取。该组件在HTC Vive和Oculus Touch上都可以工作:
- 同时设置了vive-controls和oculus-touch-controls组件
- 用简单的手模型覆盖(override)控制器模型
- 映射Vive和Oculus Touch特有事件到一致的手事件和手势(比如,把
gripdown
和triggerdown
映射到thumbup
)
要添加一个hand-controls组件,使用如下代码:
<a-entity hand-controls="left"></a-entity><a-entity hand-controls="right"></a-entity> |
目前还没有一个支持3DoF的通用控制器(支持Daydream, GearVR)。不过创建这样一个定制控制器并不难,因为交互方式只有简单的旋转。
创建定制(自定义)控制器
如前所述,当控制器根据具体硬件设备来定制的话会获得最好的体验。大多数虚拟现实应用程序都有自己特有的控制器。这意味着不同的模型,动画,手势,视觉反馈和状态。
可以参考hand-controls组件源代码来快速构建一个新的自定义组件,不需要从零开始:
- tracked-controls组件提供姿势(pose)和事件
- vive-controls, oculus-touch-controls, daydream-controls, 或者 gearvr-controls 组件提供控制器特定事件的按键(按钮)映射。
- 而我们的定制控制器组件将基于上述组件,再加上模型、动画、视觉反馈和状态等等的功能覆盖
第一部分是设置哪些控制器组件将被支持。这个控制器组件也将注入跟踪控制组件。例如,要支持所有控制器,设置所有控制器组件并提供手和模型覆盖:
AFRAME.registerComponent('custom-controls', { schema: { hand: {default: ''}, model: {default: 'customControllerModel.gltf'} }, update: function () { var controlConfiguration = { hand: this.data.hand, model: false, rotationOffset: hand === 'left' ? 90 : -90 }; // Build on top of controller components. el.setAttribute('vive-controls', controlConfiguration); el.setAttribute('oculus-touch-controls', controlConfiguration); el.setAttribute('daydream-controls', controlConfiguration); el.setAttribute('gearvr-controls', controlConfiguration); // Set a model. el.setAttribute('gltf-model', this.data.model); }}); |
更加高级和实用的定制组件,请查看A-Painter的paint-controls组件或者A-Blast的shoot-controls组件。
侦听按钮和轴事件
控制器有许多按钮,并发出许多事件。对于每一个按钮,每次按钮按下和释放,或在某些情况下甚至触摸都将发出事件。对于每个轴(例如,触控板,摇杆),每一次触摸都会发出事件。要处理按钮操作,请在各自控制组件文档页的事件表中查找相应名称,然后注册我们想要的事件处理程序:
- daydream-controls事件
- gearvr-controls事件
- hand-controls事件
- oculus-touch-controls事件
- vive-controls事件
例如,我们可以监听Oculus Touch的X按钮并相应切换实体可见性。以组件的形式,如下所示:
AFRAME.registerComponent('x-button-listener', { init: function () { var el = this.el; el.addEventListener('xbuttondown', function (evt) { el.setAttribute('visible', !el.getAttribute('visible')); }); }}); |
然后添加该组件:
<a-entity oculus-touch-controls x-button-listener></a-entity> |
为控制器添加激光交互
激光(或镭射)交互是指从控制器投射一个可见的光线。当实体与光线相交时发生交互。控制器按钮在产生相交,或者和实体不再相交时发生更改。这种交互非常类似于基于凝视(Gaze)的交互,区别是用光线投射器(raycaster)附加在控制器上而不是头戴设备。
有一个社区controller-cursor组件提供了控制器镭射交互。该功能将被合并入光标(cursor)组件中去。用法几乎完全类似于光标组件,只不过是把组件附加到控制器而不是在摄像机下:
<a-entity hand-controls controller-cursor></a-entity> |
光线投射器(raycaster)通过来调节光线投射的长度:
<a-entity hand-controls controller-cursor raycaster="far: 2"></a-entity> |
缺省情况下,controller-cursor组件工作于6DoF控制器的按钮映射(triggerdown
, triggerup
)。由于大多数3DoF控制器拥有不同类型的按钮,我们可以将其配置为3DoF控制器的事件或者通用的downEvents
和upEvents
事件:
<a-entity hand-controls controller-cursor="downEvents: triggerdown, trackpaddown; upEvents: triggerup, trackpadup"></a-entity> |
然后处理事件和交互完全类似于[基于凝视的光标组件交互]。参考前述章节!
为控制器添加空间尺度交互
空间尺度交互很难。这些包括三维空间中的交互,双手相互作用,如抓、掷、伸展、击打,旋转,拉或推。空间尺度交互操作的种类以及复杂性不是我们在这里可以完全覆盖的。这与只有鼠标和触摸屏的2D网页以及只有转动操作的3DoF VR有巨大的差异。不过我们还是可以举一些具体实现的例子以供参考。
除了使用raycasters来检测对象相交外,空间尺度和3D交互还包括碰撞器(colliders)。raycasters是二维的线, 碰撞器是一个三维容器。有不同形状的环绕物体的碰撞器(AABB包围盒,球体和网孔),当这些形状相交时检测到碰撞。
超级手(super-hands)组件
超级手组件提供全功能的自然手控制器交互。该组件将跟踪控制器和碰撞检测组件的输入解释为交互手势,并将这些手势传达给目标实体,以做出响应。
目前已实现的手势有:
- 悬停(Hover): 在实体的碰撞空间中握住控制器
- 抓住(Grab): 按下一个按钮在实体上悬停或移动它
- 拉伸(Stretch): 用双手抓住一个实体并调整大小
- 拖放(Drag-drop): 将实体拖到另一实体上
对于响应超级手手势的实体,它需要添加一个将手势转换为动作的组件。超级手组件包括典型手势反应的组件实现:hoverable,grabbable,stretchable, 和 drag-droppable。
super-hands文档和 实例非常适合入门。
其它例子
其他的例子包括:
- tracked-controls - Interaction through sphere-collider and grab components.
- ball-throw - Grab and throw using aframe-extras and aframe-physics.
- architect - Interaction through cloner, deleter, mover, placer, and scaler components.
- vr-editor - Interaction through a single vr-editor component for cloning, moving, deleting, placing, and scaling.