WebGL之物體選擇

原文地址: WebGL之物體選擇

使用WebGL將圖形繪制到畫布后,如何與外部進(jìn)行交互?這其中最關(guān)鍵的就是如何實(shí)現(xiàn)物體的選擇。比如鼠標(biāo)點(diǎn)擊后判斷是否選中了某個(gè)圖形或圖形的某個(gè)部分。

本節(jié)實(shí)現(xiàn)的效果: WebGL選中物體

WebGL選中物體

如何實(shí)現(xiàn)選中物體

顏色區(qū)分法

《WebGL編程指南》中提出了一個(gè)原理很簡(jiǎn)單的解決方案,步驟如下:

  1. 鼠標(biāo)按下時(shí)物體重繪為紅色或其他能區(qū)分的顏色

  2. 讀取鼠標(biāo)點(diǎn)擊處像素的顏色

    gl.readPixels(x,y,width,height,format,type,pixels)
    
  3. 使用物體原來(lái)的顏色進(jìn)行重繪,以恢復(fù)物體本來(lái)顏色

  4. 判斷第2步讀取到的顏色是否與預(yù)設(shè)的顏色值相等,相等則表示點(diǎn)擊中物體

可以說(shuō)這是個(gè)非常容易實(shí)現(xiàn)的方案,不過(guò)要為每個(gè)物體分別設(shè)置不同的區(qū)分顏色卻是個(gè)隱患,同時(shí)也不夠友好。

光線投射法

這是使用最廣泛也最精確的一種方案了,Three.js 中的光線投射器 (Raycaster) 就實(shí)現(xiàn)了這種方案,可以看里面的源代碼。

光線投射

它的基本原理: 從視點(diǎn)出發(fā)的光線首先投射到近截面,最后投射到遠(yuǎn)截面,結(jié)合鼠標(biāo)點(diǎn)擊的位置 (x, y) 和視圖投影矩陣 (viewProjection)。可以得出由近截面坐標(biāo) (x1, y1, z1) 和遠(yuǎn)截面坐標(biāo) (x2, y2, z2) 組成的射線向量。然后我們就可以將物體坐標(biāo)構(gòu)成的面逐個(gè)與這個(gè)向量進(jìn)行對(duì)比。這涉及到線性代數(shù)中的向量,點(diǎn)積,叉積,矩陣等概念,比較復(fù)雜。主要分兩個(gè)步驟:

  1. 創(chuàng)建物體的包圍盒,判斷射線是否穿過(guò)該物體包圍盒
  2. 判斷射線是否穿過(guò)該物體的某個(gè)三角形面,如果經(jīng)過(guò)即可判斷選中了該物體

下面就分步實(shí)現(xiàn)光線投射算法的上面兩個(gè)步驟

包圍盒

包圍盒算法原理如下:

首先用視圖投影模型矩陣 (mvp) 對(duì)圖形坐標(biāo)進(jìn)行變換,得到在屏幕中的繪制坐標(biāo)[x,y,z]

遍歷每個(gè)坐標(biāo)得出一個(gè)由最大最小xy坐標(biāo) [xmax, xmin, ymax, ymin] 構(gòu)成的二維包圍盒

鼠標(biāo)位置 (x, y) 與包圍盒邊界進(jìn)行比較,如果坐標(biāo)處于盒子邊界之內(nèi),那么就可判斷選中了該物體

核心代碼如下:

canvas.addEventListener('mousemove', function(e) {
    //坐標(biāo)轉(zhuǎn)換為webgl表示區(qū)間
    const pos = util.windowToWebgl(tCanvas,e.clientX,e.clientY);
    const ps = [];
    Polygons.forEach((p,i)=>{
        //重置狀態(tài)
        p.select = false;
        //mvp矩陣
        const matrix = m4.translate(viewProjection, p.pos);
        let xmax, ymax, xmin, ymin, zmax, zmin;//包圍盒邊界
        //遍歷頂點(diǎn)獲取包圍盒的邊界
        for(let j = 0; j < p.position.length; j = j+3){
            //對(duì)坐標(biāo)進(jìn)行矩陣轉(zhuǎn)換
            const s = m4.transformPoint(matrix, p.position.slice(j,j+3));
            if(j == 0){
                xmax = s[0];
                xmin = s[0];
                ymax = s[1];
                ymin = s[1];
                zmax = s[2];
                zmin = s[2];
                continue;
            }
            if(s[0]>xmax) xmax = s[0];
            if(s[0]<xmin) xmin = s[0];
            if(s[1]>ymax) ymax = s[1];
            if(s[1]<ymin) ymin = s[1];
            if(s[2]>zmax) zmax = s[2];
            if(s[2]<zmin) zmin = s[2];
        }
        // 射線處于包圍盒內(nèi)
        if(pos.x >= xmin && pos.x <= xmax && pos.y >= ymin && pos.y <= ymax){
            p.coord = [(xmax+xmin)/2,(ymax+ymin)/2,(zmax+zmin)/2];
            ps.push(p);
        }
    });
    if(!ps.length) return;
        //獲取最靠近視點(diǎn)的圖形
    const sel = ps.length == 1? ps[0]: ps.sort((a,b)=> a.coord[2] - b.coord[2])[0];
    sel.select = true;
},false);

射線與三角形相交

但是包圍盒算法判斷地不是很精準(zhǔn),在物體形狀不是很規(guī)則或物體間靠攏的比較緊時(shí)表現(xiàn)得尤其明顯。

我們知道WebGL圖形是由三角形構(gòu)成的,那么進(jìn)一步判斷射線是否相交該物體某個(gè)三角形面就會(huì)非常精確了。

數(shù)學(xué)原理如下:

三角形內(nèi)的任意一點(diǎn)都可以用它相對(duì)于三角形的頂點(diǎn)的位置來(lái)定義:

T(u,v) = (1 - u - v)V0 + uV1 + vV2

其中 u >= 0, v >= 0, u + v <= 1 ,稱為重心坐標(biāo)

射線可以用參數(shù)方程表示為:

T(t) = P + td

其中P為起始點(diǎn),d為方向向量

因此計(jì)算直線與三角的交點(diǎn)的等式為:

P + td = (1-u-v)V0 + uV1 + vV2

整理后最終得到一個(gè)齊次線性方程組,其中[t u v] 為1 x 3 的矩陣,(t,u,v) 是它的解

[-d V1-V0 V2-V0] [t u v] = [P-V0]

根據(jù)克萊姆法則求解,其中T = P - V0, E1 = V1 - V0, E2 = V2 - V0,( [(T x E1) ? E2] [(d x E2) ? T] [(T x E1) ? d] ) 為 3 x 3 矩陣,等式最終可以寫成如下:

(t,u,v) = 1/((d x E2) ? E1) ( [(T x E1) ? E2] [(d x E2) ? T] [(T x E1) ? d] )

具體實(shí)現(xiàn)代碼如下:

// 射線處于包圍盒內(nèi)
if(pos.x >= xmin && pos.x <= xmax && pos.y >= ymin && pos.y <= ymax){
   p.coord = [(xmax+xmin)/2,(ymax+ymin)/2,(zmax+zmin)/2];
   const P = [pos.x,pos.y,0.5];//射線起始點(diǎn)
   const d = [0,0,1];//射線方向

   for(let j = 0; j < p.position.length; j = j + 9){
       //三角形頂點(diǎn)
       const V0 = m4.transformPoint(matrix, p.position.slice(j,j+3));
       const V1 = m4.transformPoint(matrix, p.position.slice(j+3,j+6));
       const V2 = m4.transformPoint(matrix, p.position.slice(j+6,j+9));

       const T = v3.subtract(P,V0);
       const E1 = v3.subtract(V1,V0);
       const E2 = v3.subtract(V2,V0);
       const M = v3.cross(d,E2);
       const det = v3.dot(M,E1);

       if(det == 0) continue;
       const K = v3.cross(T,E1);
       const t = v3.dot(K,E2)/det;
       const u = v3.dot(M,T)/det;
       const v = v3.dot(K,d)/det;
       //射線與三角形相交
       if(u >= 0 && v >= 0 && u+v<=1 ){
           ps.push(p);
           break;
       }
   }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,185評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,446評(píng)論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,951評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,189評(píng)論 0 287
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,718評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,602評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,800評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,045評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評(píng)論 1 281
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,420評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,755評(píng)論 2 371