WebVR開發(fā)教程——交互事件(三)Cardboard與gaze

Cardboard與gaze注視

Cardboard可以說是手機(jī)VR頭顯的元老了掰烟,狹義上指的是Google推出的一個帶有雙凸透鏡的盒子一姿,廣義上則表示智能手機(jī)+盒子的VR體驗平臺。

cardboard

它的交互方式較為簡單登渣,利用了手機(jī)的陀螺儀,采用gaze注視行為來觸發(fā)場景里的事件,比如用戶在虛擬商店中注視一款商品時始绍,彈出這個商品的價格信息。

gaze交互

注視事件是WebVR最基本的交互方式话侄,用戶通過頭部運動改變視線朝向亏推,當(dāng)用戶視線正對著物體時,觸發(fā)物體綁定的事件年堆,具體分為三個基本事件吞杭,分別是gazeEnter,gazeTrigger,gazeLeave
我們可以設(shè)置一個位于相機(jī)中心的準(zhǔn)心來描述這三個基本事件(準(zhǔn)確的說变丧,在VR模式下是兩個芽狗,分別位于左右相機(jī)的中心)

  • gazeEnter:當(dāng)準(zhǔn)心進(jìn)入物體時,即用戶注視了物體痒蓬,觸發(fā)一次
  • gazeLeave:當(dāng)準(zhǔn)心離開物體時童擎,即用戶停止注視該物體時,觸發(fā)一次
  • gazeTrigger:當(dāng)準(zhǔn)心處于物體時觸發(fā)攻晒,不同于gazeEnter顾复,gazeTrigger會在每一幀刷觸發(fā),直到準(zhǔn)心離開物體

注視事件原理

注視事件觸發(fā)條件其實就是物體被用戶視線“擊中”鲁捏。在每幀動畫渲染中芯砸,從準(zhǔn)心處沿z軸負(fù)方向發(fā)出射線,如果射線與物體相交给梅,即物體被射線擊中假丧,說明前方的物體被用戶注視,這里使用Three提供的raycaster對象,對場景里的3d物體進(jìn)行射線拾取动羽。

下面是使用THREE.Raycaster拾取物體的簡單例子:

// 創(chuàng)建射線發(fā)射器實例raycaster
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(origin,camera); // 設(shè)置射線源點
raycaster.intersectObjects(targetList); // 檢測targetList的object物體是否與射線相交
if (intersects.length > 0) {
    // 獲取從源點觸發(fā)包帚,與射線相交的首個物體
    const target = intersects[0].object;
    // TODO
}

主要分為三步:

  1. new THREE.Raycaster()創(chuàng)建一個射線發(fā)射器;
  2. 調(diào)用.setFromCamera(origin,camera)設(shè)置射線發(fā)射源位置曹质,第一個參數(shù)origin傳入NDC標(biāo)準(zhǔn)化設(shè)備坐標(biāo)婴噩,即歸一化的屏幕坐標(biāo)擎场,第二個參數(shù)傳入相機(jī),此時射線將在屏幕的origin處几莽,沿垂直于相機(jī)的近切面的方向進(jìn)行投射迅办;
  3. 調(diào)用.intersectObjects(targetList)檢測targetList的物體是否相交
    Raycaster借鑒了光線投射法進(jìn)行物體拾取,更多用法可參考three.js官方文檔

gazeEnter, gazeLeave, gazeTrigger實現(xiàn)

根據(jù)上文對gaze基本事件的描述章蚣,現(xiàn)在開始創(chuàng)建注視監(jiān)聽器Gazer類站欺,提供事件綁定on、解綁off纤垂、更新update的公用方法矾策,物體可注冊gazeEnter,gazeLeave,gazeTrigger事件回調(diào),以下是完整代碼峭沦。

// 注視事件監(jiān)聽器
class Gazer {
    constructor() {
        // 初始化射線發(fā)射源
        this.raycaster = new THREE.Raycaster();
        this._center = new THREE.Vector2();
        this.rayList = {},this.targetList = [];
        this._lastTarget = null;
    }
    /** 物體綁定gaze事件的公用方法
     * @param {THREE.Object3D} target 監(jiān)聽的3d網(wǎng)格
     * @param {String} eventType 事件類型 
     * @param {Function} callback 事件回調(diào)
     **/
    on(target, eventType, callback) {
        const noop = () => {};
        // target首次綁定事件贾虽,則創(chuàng)建監(jiān)聽對象,加入raylist監(jiān)聽列表吼鱼,并將三個基本事件的回調(diào)初始為空方法
        if (!this.rayList[target.id]) this.rayList[target.id] = { target, gazeEnter: noop, gazeTrigger: noop, gazeLeave: noop };
        // 根據(jù)傳入的 eventType與callback更新事件回調(diào)
        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) { // 當(dāng)前幀射線擊中物體
            const currentTarget = intersects[0].object;
            if (this._lastTarget) { // 上一幀射線擊中物體
                if (this._lastTarget.id !== currentTarget.id) { // 上一幀射線擊中物體與當(dāng)前幀不同
                    this.rayList[this._lastTarget.id].gazeLeave(); 
                    this.rayList[currentTarget.id].gazeEnter();
                }
            } else { // 上一幀射線未擊中物體
                this.rayList[currentTarget.id].gazeEnter(); // 觸發(fā)當(dāng)前幀物體的gazeEnter事件
            }
            this.rayList[currentTarget.id].gazeTrigger(); // 當(dāng)前幀射線擊中物體蓬豁,觸發(fā)物體的gazeTrigger事件
            this._lastTarget = currentTarget;
        } else { // 當(dāng)前幀我擊中物體
            if ( this._lastTarget ) this.rayList[this._lastTarget.id].gazeLeave(); // 觸發(fā)上一幀物體gazeLeave
            this._lastTarget = null;
        }
    }
}

下面一起來看Gazer實現(xiàn)的三步曲,這里用“擊中”表示射線與物體相交菇肃。

第一步地粪,使用構(gòu)造函數(shù)constructor初始化:
  1. 初始化射線發(fā)射器raycaster實例;
  2. 創(chuàng)建rayList以記錄注冊gaze事件的物體對象琐谤;
  3. 創(chuàng)建lastTarget記錄前一幀被射線擊中的物體蟆技,初始為null。
第二步斗忌,創(chuàng)建on方法提供事件綁定API

通過調(diào)用gazer.on(target,eventType,callback)方式质礼,傳入綁定事件的Obect3D對象target,綁定事件類型eventType以及事件回調(diào)callback三個參數(shù)飞蹂。

  1. 判斷這個target是否存在几苍,不存在,則創(chuàng)建一個監(jiān)聽對象陈哑,存在則更新對象里的事件函數(shù)。這個對象包括傳入的target本身伸眶,以及三個基本事件的回調(diào)函數(shù)(初始值為空方法):
this.rayList[target.id] = { 
   target, 
   gazeEnter, 
   gazeTrigger, 
   gazeLeave
}

將這個對象以鍵值對形式賦值給raylist[target.id]監(jiān)聽序列對象惊窖;

  1. raylist對象處理成[ target1, ..., targetN ]的形式賦值給this.targetList,作為raycaster.intersectObjects的入?yún)ⅰ?/li>
第三步厘贼,創(chuàng)建update方法界酒,在動畫幀中監(jiān)聽三個基本事件是否觸發(fā)
  1. 調(diào)用raycaster.setFromCamera更新射線起點與方向;
  2. 調(diào)用raycaster.intersectObjects檢測監(jiān)聽序列this.targetList是否有物體與射線相交嘴秸;
  3. 根據(jù)gazeEntergazeLeavegazeTrigger實現(xiàn)的情況毁欣,總結(jié)了以下這三個事件觸發(fā)的邏輯圖庇谆。
gaze基本事件邏輯圖

邏輯圖里的三個條件用代碼表示如下:

當(dāng)前幀射線是否擊中物體:if (intersects.length > 0)
上一幀射線是否擊中物體:if (this._lastTarget)
當(dāng)前幀射線擊中物體是否與上一幀不同:if (this._lastTarget.id !== currentTarget.id)

if (intersects.length > 0) { // 當(dāng)前幀射線擊中物體
    const currentTarget = intersects[0].object;
    if (this._lastTarget) { // 上一幀射線擊中物體
        if (this._lastTarget.id !== currentTarget.id) { 
            // 上一幀射線擊中物體與當(dāng)前幀不同,觸發(fā)上一幀物體的gazeLeave事件凭疮,觸發(fā)當(dāng)前幀物體的gazeEnter事件
            this.rayList[this._lastTarget.id].gazeLeave(); 
            this.rayList[currentTarget.id].gazeEnter();
        }
    } else { // 上一幀射線未擊中物體
        this.rayList[currentTarget.id].gazeEnter(); // 上一幀射線沒有擊中物體饭耳,觸發(fā)當(dāng)前幀物體的gazeEnter事件
    }
    this.rayList[currentTarget.id].gazeTrigger(); // 當(dāng)前幀射線擊中物體,觸發(fā)物體的gazeTrigger事件
    this._lastTarget = currentTarget;
} else { // 當(dāng)前幀我擊中物體
    if ( this._lastTarget ) this.rayList[this._lastTarget.id].gazeLeave(); // 上一幀射線擊中物體执解,觸發(fā)上一幀物體gazeLeave
    this._lastTarget = null;
}

最后寞肖,我們需要更新this._lastTarget值,供下一幀進(jìn)行邏輯判斷衰腌,如果當(dāng)前幀有物體擊中新蟆,則this._lastTarget = currentTarget,否則執(zhí)行this._lastTarget = null右蕊。

事件綁定示例

接下來琼稻,我們調(diào)用前面定義的Gazer類開發(fā)gaze交互,實現(xiàn)一個簡單例子:隨機(jī)創(chuàng)建100個cube立方體饶囚,當(dāng)用戶注視立方體時欣簇,立方體半透明。
首先創(chuàng)建準(zhǔn)心坯约,設(shè)置為一個圓點作為展現(xiàn)給用戶的光標(biāo)熊咽,當(dāng)然你可以創(chuàng)建其它準(zhǔn)心形狀,比如十字形或環(huán)形等闹丐。

// 創(chuàng)建準(zhǔn)心
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()方法創(chuàng)建物體并綁定事件,在update監(jiān)聽事件卿拴。

// 場景物體初始化
start() {
    const { scene, camera } = this;
    ... 創(chuàng)建燈光衫仑、地板等
    // 添加準(zhǔn)心到相機(jī)
    camera.add(this.createCrosshair());
    this.gazer = new Gazer();
    // 創(chuàng)建立方體
    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的代碼結(jié)構(gòu)堕花,在start方法里增加了一個準(zhǔn)心文狱,為100個cube立方體綁定gazeEnter事件和gazeLeave事件,觸發(fā)gazeEnter時缘挽,立方體半透明瞄崇,觸發(fā)gazeLeave時,立方體恢復(fù)不透明壕曼。

gaze注視交互

演示地址:yonechen.github.io/WebVR-helloworld/cardboard.html
源碼地址:github.com/YoneChen/WebVR-helloworld/blob/master/cardboard.html


注視事件除了以上三種基本事件外苏研,還衍生了像注視延遲事件和注視點擊事件,這些gaze事件都可以在gazeTrigger里進(jìn)行拓展腮郊。

注視點擊事件

cardboard二代在盒子上提供了一個按鈕摹蘑,當(dāng)用戶通過注視物體并點擊按鈕,由按鈕點擊屏幕觸發(fā)轧飞。
實現(xiàn)思路:在window綁定click事件衅鹿,觸發(fā)click時改變標(biāo)志位撒踪,在gazeTrigger方法內(nèi)根據(jù)標(biāo)志位來判斷是否執(zhí)行回調(diào),關(guān)鍵代碼如下:

//按鈕事件監(jiān)聽
window.addEventListener('click', e => this.state._clicked = true);
this.gazer.on(cube,'gazeTrigger',() => {
    // 當(dāng)用戶點擊時觸發(fā)
    if (this.state._clicked) {
        this.state._clicked = false; // 重置點擊標(biāo)志位
        cube.scale.set(1.5,1.5,1.5); // TODO
    }
});
注視延遲事件

當(dāng)準(zhǔn)心在物體上超過一定時間時觸發(fā)大渤,一般會在準(zhǔn)心處設(shè)置一個進(jìn)度條動畫制妄。

注視延遲事件

實現(xiàn)思路:在gazeEnter時記錄開始時間點,在gazeTrigger計算出時間差是否超過預(yù)設(shè)延遲時間兼犯,如果是則執(zhí)行回調(diào)忍捡,關(guān)鍵代碼如下:

//準(zhǔn)心進(jìn)入物體,開啟事件觸發(fā)計時
this.gazer.on(cube,'gazeEnter',() => {
    this.state._wait = true; // 計時已開始
    this.animate.loader.start(); // 開啟準(zhǔn)心進(jìn)度條動畫
    this.state.gazeEnterTime = Date.now(); // 記錄計時開始時間點
});
this.gazer.on(cube,'gazeTrigger',() => {
    // 當(dāng)計時已開始切黔,且延遲時長超過1.5秒觸發(fā)
    if (this.state._wait && Date.now() - this.state.gazeEnterTime > 1500) {
        this.animate.loader.stop(); // 停止準(zhǔn)心進(jìn)度條動畫
        this.state._wait = false; // 計時結(jié)束
        cube.material.opacity = 0.5; // TODO
    }
});
this.gazer.on(cube,'gazeLeave',() => {
    this.animate.loader.stop(); // 停止準(zhǔn)心進(jìn)度條動畫
    this.state._wait = false; // 計時結(jié)束
    ...
});

這里準(zhǔn)心計時進(jìn)度條loader動畫使用了Tween.js砸脊,這里就不展開了,更多可在源碼地址查看纬霞。

演示地址:yonechen.github.io/WebVR-helloworld/cardboard2.html
源碼地址:github.com/YoneChen/WebVR-helloworld/blob/master/cardboard2.html


小結(jié)

本文介紹了VR注視事件gaze原理以及開發(fā)過程凌埂,核心是通過raycaster來實現(xiàn)3d場景的物體拾取。為了方便調(diào)用诗芜,我將上述的gaze事件監(jiān)聽機(jī)制封裝了一個插件 gaze-event 瞳抓,歡迎查看。


WebVR開發(fā)傳送門:

WebVR開發(fā)教程——交互事件(二)使用Gamepad
WebVR開發(fā)教程——深度剖析 關(guān)于WebVR的開發(fā)調(diào)試方案以及原理機(jī)制
WebVR開發(fā)教程——標(biāo)準(zhǔn)入門 使用Three.js開發(fā)WebVR場景的入門教程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末伏恐,一起剝皮案震驚了整個濱河市孩哑,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌翠桦,老刑警劉巖横蜒,帶你破解...
    沈念sama閱讀 211,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異销凑,居然都是意外死亡丛晌,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,347評論 3 385
  • 文/潘曉璐 我一進(jìn)店門斗幼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來澎蛛,“玉大人砾省,你說我怎么就攤上這事杆煞。” “怎么了染苛?”我有些...
    開封第一講書人閱讀 157,435評論 0 348
  • 文/不壞的土叔 我叫張陵渠羞,是天一觀的道長斤贰。 經(jīng)常有香客問我,道長次询,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,509評論 1 284
  • 正文 為了忘掉前任瓷叫,我火速辦了婚禮屯吊,結(jié)果婚禮上送巡,老公的妹妹穿的比我還像新娘。我一直安慰自己盒卸,他們只是感情好骗爆,可當(dāng)我...
    茶點故事閱讀 65,611評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蔽介,像睡著了一般摘投。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上虹蓄,一...
    開封第一講書人閱讀 49,837評論 1 290
  • 那天犀呼,我揣著相機(jī)與錄音,去河邊找鬼薇组。 笑死外臂,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的律胀。 我是一名探鬼主播宋光,決...
    沈念sama閱讀 38,987評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼炭菌!你這毒婦竟也來了罪佳?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,730評論 0 267
  • 序言:老撾萬榮一對情侶失蹤黑低,失蹤者是張志新(化名)和其女友劉穎赘艳,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體投储,經(jīng)...
    沈念sama閱讀 44,194評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡第练,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,525評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了玛荞。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片娇掏。...
    茶點故事閱讀 38,664評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖勋眯,靈堂內(nèi)的尸體忽然破棺而出婴梧,到底是詐尸還是另有隱情,我是刑警寧澤客蹋,帶...
    沈念sama閱讀 34,334評論 4 330
  • 正文 年R本政府宣布塞蹭,位于F島的核電站,受9級特大地震影響讶坯,放射性物質(zhì)發(fā)生泄漏番电。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,944評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望漱办。 院中可真熱鬧这刷,春花似錦、人聲如沸娩井。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,764評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽洞辣。三九已至咐刨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間扬霜,已是汗流浹背定鸟。 一陣腳步聲響...
    開封第一講書人閱讀 31,997評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留畜挥,地道東北人仔粥。 一個月前我還...
    沈念sama閱讀 46,389評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像蟹但,于是被迫代替她去往敵國和親躯泰。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,554評論 2 349

推薦閱讀更多精彩內(nèi)容