Cardboard能夠說是手機VR頭顯的元老了,狹義上指的是Google推出的一個帶有雙凸透鏡的盒子,廣義上則表示智能手機+盒子的VR體驗平臺。html
它的交互方式較爲簡單,利用了手機的陀螺儀,採用gaze注視行爲來觸發場景裏的事件,好比用戶在虛擬商店中注視一款商品時,彈出這個商品的價格信息。git
注視事件是WebVR最基本的交互方式,用戶經過頭部運動改變視線朝向,當用戶視線正對着物體時,觸發物體綁定的事件,具體分爲三個基本事件,分別是gazeEnter
,gazeTrigger
,gazeLeave
。
咱們能夠設置一個位於相機中心的準心來描述這三個基本事件(準確的說,在VR模式下是兩個,分別位於左右相機的中心)github
注視事件觸發條件其實就是物體被用戶視線「擊中」。在每幀動畫渲染中,從準心處沿z軸負方向發出射線,若是射線與物體相交,即物體被射線擊中,說明前方的物體被用戶注視,這裏使用Three提供的raycaster對象,對場景裏的3d物體進行射線拾取。api
下面是使用THREE.Raycaster
拾取物體的簡單例子:dom
// 建立射線發射器實例raycaster const raycaster = new THREE.Raycaster(); raycaster.setFromCamera(origin,camera); // 設置射線源點 raycaster.intersectObjects(targetList); // 檢測targetList的object物體是否與射線相交 if (intersects.length > 0) { // 獲取從源點觸發,與射線相交的首個物體 const target = intersects[0].object; // TODO }
主要分爲三步:ssh
new THREE.Raycaster()
建立一個射線發射器;.setFromCamera(origin,camera)
設置射線發射源位置,第一個參數origin傳入NDC標準化設備座標,即歸一化的屏幕座標,第二個參數傳入相機,此時射線將在屏幕的origin處,沿垂直於相機的近切面的方向進行投射;.intersectObjects(targetList)
檢測targetList的物體是否相交Raycaster
借鑑了光線投射法進行物體拾取,更多用法可參考three.js官方文檔 根據上文對gaze基本事件的描述,如今開始建立注視監聽器Gazer
類,提供事件綁定on
、解綁off
、更新update
的公用方法,物體可註冊gazeEnter
,gazeLeave
,gazeTrigger
事件回調,如下是完整代碼。函數
// 注視事件監聽器 class Gazer { constructor() { // 初始化射線發射源 this.raycaster = new THREE.Raycaster(); this._center = new THREE.Vector2(); this.rayList = {},this.targetList = []; this._lastTarget = null; } /** 物體綁定gaze事件的公用方法 * @param {THREE.Object3D} target 監聽的3d網格 * @param {String} eventType 事件類型 * @param {Function} callback 事件回調 **/ on(target, eventType, callback) { const noop = () => {}; // target首次綁定事件,則建立監聽對象,加入raylist監聽列表,並將三個基本事件的回調初始爲空方法 if (!this.rayList[target.id]) this.rayList[target.id] = { target, gazeEnter: noop, gazeTrigger: noop, gazeLeave: noop }; // 根據傳入的 eventType與callback更新事件回調 this.rayList[target.id][eventType] = callback; this.targetList = Object.keys(this.rayList).map(key => this.rayList[key].target); } off(target) { delete this.rayList[target.id]; this.targetList = Object.keys(this.rayList).map(key => this.rayList[key].target); } update(camera) { if (this.targetList.length <= 0) return; //更新射線位置 this.raycaster.setFromCamera(this._center,camera); const intersects = this.raycaster.intersectObjects(this.targetList); if (intersects.length > 0) { // 當前幀射線擊中物體 const currentTarget = intersects[0].object; if (this._lastTarget) { // 上一幀射線擊中物體 if (this._lastTarget.id !== currentTarget.id) { // 上一幀射線擊中物體與當前幀不一樣 this.rayList[this._lastTarget.id].gazeLeave(); this.rayList[currentTarget.id].gazeEnter(); } } else { // 上一幀射線未擊中物體 this.rayList[currentTarget.id].gazeEnter(); // 觸發當前幀物體的gazeEnter事件 } this.rayList[currentTarget.id].gazeTrigger(); // 當前幀射線擊中物體,觸發物體的gazeTrigger事件 this._lastTarget = currentTarget; } else { // 當前幀我擊中物體 if ( this._lastTarget ) this.rayList[this._lastTarget.id].gazeLeave(); // 觸發上一幀物體gazeLeave this._lastTarget = null; } } }
下面一塊兒來看Gazer
實現的三步曲,這裏用「擊中」表示射線與物體相交。oop
constructor
初始化:raycaster
實例;rayList
以記錄註冊gaze事件的物體對象;lastTarget
記錄前一幀被射線擊中的物體,初始爲null。on
方法提供事件綁定API經過調用gazer.on(target,eventType,callback)
方式,傳入綁定事件的Obect3D對象target
,綁定事件類型eventType
以及事件回調callback
三個參數。動畫
判斷這個target是否存在,不存在,則建立一個監聽對象,存在則更新對象裏的事件函數。這個對象包括傳入的target自己,以及三個基本事件的回調函數(初始值爲空方法):this
this.rayList[target.id] = { target, gazeEnter, gazeTrigger, gazeLeave }
將這個對象以鍵值對形式賦值給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)
if (intersects.length > 0) { // 當前幀射線擊中物體 const currentTarget = intersects[0].object; if (this._lastTarget) { // 上一幀射線擊中物體 if (this._lastTarget.id !== currentTarget.id) { // 上一幀射線擊中物體與當前幀不一樣,觸發上一幀物體的gazeLeave事件,觸發當前幀物體的gazeEnter事件 this.rayList[this._lastTarget.id].gazeLeave(); this.rayList[currentTarget.id].gazeEnter(); } } else { // 上一幀射線未擊中物體 this.rayList[currentTarget.id].gazeEnter(); // 上一幀射線沒有擊中物體,觸發當前幀物體的gazeEnter事件 } this.rayList[currentTarget.id].gazeTrigger(); // 當前幀射線擊中物體,觸發物體的gazeTrigger事件 this._lastTarget = currentTarget; } else { // 當前幀我擊中物體 if ( this._lastTarget ) this.rayList[this._lastTarget.id].gazeLeave(); // 上一幀射線擊中物體,觸發上一幀物體gazeLeave this._lastTarget = null; }
最後,咱們須要更新this._lastTarget
值,供下一幀進行邏輯判斷,若是當前幀有物體擊中,則this._lastTarget = currentTarget
,不然執行this._lastTarget = null
。
接下來,咱們調用前面定義的Gazer
類開發gaze交互,實現一個簡單例子:隨機建立100個cube立方體,當用戶注視立方體時,立方體半透明。
首先建立準心,設置爲一個圓點做爲展示給用戶的光標,固然你能夠建立其它準心形狀,好比十字形或環形等。
// 建立準心 createCrosshair () { const geometry = new THREE.CircleGeometry( 0.002, 16 ); const material = new THREE.MeshBasicMaterial({ color: 0xffffff, opacity: 0.5, transparent: true }); const crosshair = new THREE.Mesh(geometry,material); crosshair.position.z = -0.5; return crosshair; }
接下來,在start()
方法建立物體並綁定事件,在update
監聽事件。
// 場景物體初始化 start() { const { scene, camera } = this; ... 建立燈光、地板等 // 添加準心到相機 camera.add(this.createCrosshair()); this.gazer = new Gazer(); // 建立立方體 for (let i = 0; i < 100; i++) { const cube = this.createCube(2,2,2 ); cube.position.set( 100*Math.random() - 50, 50*Math.random() -10, 100*Math.random() - 50 ); scene.add(cube); // 綁定注視事件 this.gazer.on(cube,'gazeEnter',() => { cube.material.opacity = 0.5; }); this.gazer.on(cube,'gazeLeave',() => { cube.material.opacity = 1; }); } } // 動畫更新 update() { const { scene, camera, renderer, gazer } = this; gazer.update(camera); renderer.render(scene, camera); }
在示例中,咱們遵循上一期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
方法內根據標誌位來判斷是否執行回調,關鍵代碼以下:
//按鈕事件監聽 window.addEventListener('click', e => this.state._clicked = true); this.gazer.on(cube,'gazeTrigger',() => { // 當用戶點擊時觸發 if (this.state._clicked) { this.state._clicked = false; // 重置點擊標誌位 cube.scale.set(1.5,1.5,1.5); // TODO } });
當準心在物體上超過必定時間時觸發,通常會在準心處設置一個進度條動畫。
實現思路:在gazeEnter
時記錄開始時間點,在gazeTrigger
計算出時間差是否超過預設延遲時間,若是是則執行回調,關鍵代碼以下:
//準心進入物體,開啓事件觸發計時 this.gazer.on(cube,'gazeEnter',() => { this.state._wait = true; // 計時已開始 this.animate.loader.start(); // 開啓準心進度條動畫 this.state.gazeEnterTime = Date.now(); // 記錄計時開始時間點 }); this.gazer.on(cube,'gazeTrigger',() => { // 當計時已開始,且延遲時長超過1.5秒觸發 if (this.state._wait && Date.now() - this.state.gazeEnterTime > 1500) { this.animate.loader.stop(); // 中止準心進度條動畫 this.state._wait = false; // 計時結束 cube.material.opacity = 0.5; // TODO } }); this.gazer.on(cube,'gazeLeave',() => { this.animate.loader.stop(); // 中止準心進度條動畫 this.state._wait = false; // 計時結束 ... });
這裏準心計時進度條loader動畫使用了Tween.js
,這裏就不展開了,更多可在源碼地址查看。
演示地址:yonechen.github.io/WebVR-helloworld/cardboard2.html
源碼地址:github.com/YoneChen/WebVR-helloworld/blob/master/cardboard2.html
以上介紹了Cardboard的gaze事件概念與原理,以及三個基本事件的開發過程,經過例子展現gaze交互實現方法,最後文末補充了gaze事件的擴展。
上文說起的注視點擊也是Gear VR最經常使用的交互方式,不過Gear VR提供了更爲豐富的touchpad而不是按鈕,下一期將詳細介紹Gear VR與touchpad的事件開發,敬請期待。
WebVR開發教程——交互事件(一)頭顯與手柄
WebVR開發教程——交互事件(二)使用Gamepad
WebVR開發教程——深度剖析 關於WebVR的開發調試方案以及原理機制
WebVR開發教程——標準入門 使用Three.js開發WebVR場景的入門教程