WebVR開發教程——交互事件(三)Cardboard與注視

Cardboard能夠說是手機VR頭顯的元老了,狹義上指的是Google推出的一個帶有雙凸透鏡的盒子,廣義上則表示智能手機+盒子的VR體驗平臺。html

Cardboard與gaze注視

cardboard

它的交互方式較爲簡單,利用了手機的陀螺儀,採用gaze注視行爲來觸發場景裏的事件,好比用戶在虛擬商店中注視一款商品時,彈出這個商品的價格信息。git

gaze交互

注視事件是WebVR最基本的交互方式,用戶經過頭部運動改變視線朝向,當用戶視線正對着物體時,觸發物體綁定的事件,具體分爲三個基本事件,分別是gazeEnter,gazeTrigger,gazeLeave
咱們能夠設置一個位於相機中心的準心來描述這三個基本事件(準確的說,在VR模式下是兩個,分別位於左右相機的中心)github

  • gazeEnter:當準心進入物體時,即用戶注視了物體,觸發一次
  • gazeLeave:當準心離開物體時,即用戶中止注視該物體時,觸發一次
  • gazeTrigger:當準心處於物體時觸發,不一樣於gazeEnter,gazeTrigger會在每一幀刷觸發,直到準心離開物體

注視事件原理

注視事件觸發條件其實就是物體被用戶視線「擊中」。在每幀動畫渲染中,從準心處沿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

  1. new THREE.Raycaster()建立一個射線發射器;
  2. 調用.setFromCamera(origin,camera)設置射線發射源位置,第一個參數origin傳入NDC標準化設備座標,即歸一化的屏幕座標,第二個參數傳入相機,此時射線將在屏幕的origin處,沿垂直於相機的近切面的方向進行投射;
  3. 調用.intersectObjects(targetList)檢測targetList的物體是否相交
    Raycaster借鑑了光線投射法進行物體拾取,更多用法可參考three.js官方文檔

gazeEnter, gazeLeave, gazeTrigger實現

根據上文對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初始化:
  1. 初始化射線發射器raycaster實例;
  2. 建立rayList以記錄註冊gaze事件的物體對象;
  3. 建立lastTarget記錄前一幀被射線擊中的物體,初始爲null。
第二步,建立on方法提供事件綁定API

經過調用gazer.on(target,eventType,callback)方式,傳入綁定事件的Obect3D對象target,綁定事件類型eventType以及事件回調callback三個參數。動畫

  1. 判斷這個target是否存在,不存在,則建立一個監聽對象,存在則更新對象裏的事件函數。這個對象包括傳入的target自己,以及三個基本事件的回調函數(初始值爲空方法):this

    this.rayList[target.id] = { 
       target, 
       gazeEnter, 
       gazeTrigger, 
       gazeLeave
    }

    將這個對象以鍵值對形式賦值給raylist[target.id]監聽序列對象;

  2. raylist對象處理成[ target1, ..., targetN ]的形式賦值給this.targetList,做爲raycaster.intersectObjects的入參。
第三步,建立update方法,在動畫幀中監聽三個基本事件是否觸發
  1. 調用raycaster.setFromCamera更新射線起點與方向;
  2. 調用raycaster.intersectObjects檢測監聽序列this.targetList是否有物體與射線相交;
  3. 根據gazeEntergazeLeavegazeTrigger實現的狀況,總結了如下這三個事件觸發的邏輯圖。

gaze基本事件邏輯圖

邏輯圖裏的三個條件用代碼表示以下:

當前幀射線是否擊中物體: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時,立方體恢復不透明。

gaze注視交互

演示地址: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開發教程——交互事件(一)頭顯與手柄
WebVR開發教程——交互事件(二)使用Gamepad
WebVR開發教程——深度剖析 關於WebVR的開發調試方案以及原理機制
WebVR開發教程——標準入門 使用Three.js開發WebVR場景的入門教程

相關文章
相關標籤/搜索