Cardboard与gaze注视
Cardboard可以说是手机VR头显的元老了,狭义上指的是Google推出的一个带有双凸透镜的盒子,广义上则表示智能手机+盒子的VR体验平台。
它的交互方式较为简单,利用了手机的陀螺仪,采用gaze注视行为来触发场景里的事件,比如用户在虚拟商店中注视一款商品时,弹出这个商品的价格信息。
注视事件是WebVR最基本的交互方式,用户通过头部运动改变视线朝向,当用户视线正对着物体时,触发物体绑定的事件,具体分为三个基本事件,分别是gazeEnter
,gazeTrigger
,gazeLeave
。
我们可以设置一个位于相机中心的准心来描述这三个基本事件(准确的说,在VR模式下是两个,分别位于左右相机的中心)
- gazeEnter:当准心进入物体时,即用户注视了物体,触发一次
- gazeLeave:当准心离开物体时,即用户停止注视该物体时,触发一次
- gazeTrigger:当准心处于物体时触发,不同于gazeEnter,gazeTrigger会在每一帧刷触发,直到准心离开物体
注视事件原理
注视事件触发条件其实就是物体被用户视线“击中”。在每帧动画渲染中,从准心处沿z轴负方向发出射线,如果射线与物体相交,即物体被射线击中,说明前方的物体被用户注视,这里使用Three提供的raycaster对象,对场景里的3d物体进行射线拾取。
下面是使用THREE.Raycaster
拾取物体的简单例子:
1 | // 创建射线发射器实例raycaster |
主要分为三步:
new THREE.Raycaster()
创建一个射线发射器;- 调用
.setFromCamera(origin,camera)
设置射线发射源位置,第一个参数origin传入NDC标准化设备坐标,即归一化的屏幕坐标,第二个参数传入相机,此时射线将在屏幕的origin处,沿垂直于相机的近切面的方向进行投射; - 调用
.intersectObjects(targetList)
检测targetList的物体是否相交
Raycaster
借鉴了光线投射法进行物体拾取,更多用法可参考three.js官方文档
gazeEnter, gazeLeave, gazeTrigger实现
根据上文对gaze基本事件的描述,现在开始创建注视监听器Gazer
类,提供事件绑定on
、解绑off
、更新update
的公用方法,物体可注册gazeEnter
,gazeLeave
,gazeTrigger
事件回调,以下是完整代码。
1 | // 注视事件监听器 |
下面一起来看Gazer
实现的三步曲,这里用“击中”表示射线与物体相交。
第一步,使用构造函数constructor
初始化:
- 初始化射线发射器
raycaster
实例; - 创建
rayList
以记录注册gaze事件的物体对象; - 创建
lastTarget
记录前一帧被射线击中的物体,初始为null。
第二步,创建on
方法提供事件绑定API
通过调用gazer.on(target,eventType,callback)
方式,传入绑定事件的Obect3D对象target
,绑定事件类型eventType
以及事件回调callback
三个参数。
- 判断这个target是否存在,不存在,则创建一个监听对象,存在则更新对象里的事件函数。这个对象包括传入的target本身,以及三个基本事件的回调函数(初始值为空方法):
1 | this.rayList[target.id] = { |
将这个对象以键值对形式赋值给raylist[target.id]
监听序列对象;
- 将
raylist
对象处理成[ target1, ..., targetN ]
的形式赋值给this.targetList
,作为raycaster.intersectObjects
的入参。
第三步,创建update
方法,在动画帧中监听三个基本事件是否触发
- 调用
raycaster.setFromCamera
更新射线起点与方向; - 调用
raycaster.intersectObjects
检测监听序列this.targetList
是否有物体与射线相交; - 根据
gazeEnter
和gazeLeave
和gazeTrigger
实现的情况,总结了以下这三个事件触发的逻辑图。
逻辑图里的三个条件用代码表示如下:
- 当前帧射线是否击中物体:
if (intersects.length > 0)
- 上一帧射线是否击中物体:
if (this._lastTarget)
- 当前帧射线击中物体是否与上一帧不同:
if (this._lastTarget.id !== currentTarget.id)
1 | if (intersects.length > 0) { // 当前帧射线击中物体 |
最后,我们需要更新this._lastTarget
值,供下一帧进行逻辑判断,如果当前帧有物体击中,则this._lastTarget = currentTarget
,否则执行this._lastTarget = null
。
事件绑定示例
接下来,我们调用前面定义的Gazer
类开发gaze交互,实现一个简单例子:随机创建100个cube立方体,当用户注视立方体时,立方体半透明。
首先创建准心,设置为一个圆点作为展现给用户的光标,当然你可以创建其它准心形状,比如十字形或环形等。
1 | // 创建准心 |
接下来,在start()
方法创建物体并绑定事件,在update
监听事件。
1 | // 场景物体初始化 |
在示例中,我们遵循上一期WebVRApp的代码结构,在start
方法里增加了一个准心,为100个cube立方体绑定gazeEnter
事件和gazeLeave
事件,触发gazeEnter
时,立方体半透明,触发gazeLeave
时,立方体恢复不透明。
演示地址:yonechen.github.io/WebVR-helloworld/cardboard.html
源码地址:github.com/YoneChen/WebVR-helloworld/blob/master/cardboard.html
注视事件除了以上三种基本事件外,还衍生了像注视延迟事件和注视点击事件,这些gaze事件都可以在gazeTrigger
里进行拓展。
注视点击事件
cardboard二代在盒子上提供了一个按钮,当用户通过注视物体并点击按钮,由按钮点击屏幕触发。
实现思路:在window
绑定click事件,触发click时改变标志位,在gazeTrigger
方法内根据标志位来判断是否执行回调,关键代码如下:
1 | //按钮事件监听 |
注视延迟事件
当准心在物体上超过一定时间时触发,一般会在准心处设置一个进度条动画。
实现思路:在gazeEnter
时记录开始时间点,在gazeTrigger
计算出时间差是否超过预设延迟时间,如果是则执行回调,关键代码如下:
1 | //准心进入物体,开启事件触发计时 |
这里准心计时进度条loader动画使用了Tween.js
,这里就不展开了,更多可在源码地址查看。
源码地址:github.com/YoneChen/WebVR-helloworld/blob/master/cardboard2.html
小结
以上介绍了Cardboard的gaze事件概念与原理,以及三个基本事件的开发过程,通过例子展示gaze交互实现方法,最后文末补充了gaze事件的扩展。
上文提及的注视点击也是Gear VR最常用的交互方式,不过Gear VR提供了更为丰富的touchpad而不是按钮,下一期将详细介绍Gear VR与touchpad的事件开发,敬请期待。