WebGL之物體選擇

原文地址: WebGL之物體選擇
使用WebGL將圖形繪製到畫布後,如何與外部進行交互?這其中最關鍵的就是如何實現物體的選擇。好比鼠標點擊後判斷是否選中了某個圖形或圖形的某個部分。javascript

本節實現的效果: WebGL選中物體
WebGL選中物體
html

如何實現選中物體

顏色區分法

《WebGL編程指南》中提出了一個原理很簡單的解決方案,步驟以下:java

  1. 鼠標按下時物體重繪爲紅色或其餘能區分的顏色git

  2. 讀取鼠標點擊處像素的顏色github

    gl.readPixels(x,y,width,height,format,type,pixels)
  3. 使用物體原來的顏色進行重繪,以恢復物體原本顏色web

  4. 判斷第2步讀取到的顏色是否與預設的顏色值相等,相等則表示點擊中物體算法

能夠說這是個很是容易實現的方案,不過要爲每一個物體分別設置不一樣的區分顏色倒是個隱患,同時也不夠友好。編程

光線投射法

這是使用最普遍也最精確的一種方案了,Three.js 中的光線投射器 (Raycaster) 就實現了這種方案,能夠看裏面的源代碼。
光線投射
它的基本原理: 從視點出發的光線首先投射到近截面,最後投射到遠截面,結合鼠標點擊的位置 (x, y) 和視圖投影矩陣 (viewProjection)。能夠得出由近截面座標 (x1, y1, z1) 和遠截面座標 (x2, y2, z2) 組成的射線向量。而後咱們就能夠將物體座標構成的面逐個與這個向量進行對比。這涉及到線性代數中的向量,點積,叉積,矩陣等概念,比較複雜。主要分兩個步驟:canvas

  1. 建立物體的包圍盒,判斷射線是否穿過該物體包圍盒
  2. 判斷射線是否穿過該物體的某個三角形面,若是通過便可判斷選中了該物體

下面就分步實現光線投射算法的上面兩個步驟webgl

包圍盒

包圍盒算法原理以下:

首先用視圖投影模型矩陣 (mvp) 對圖形座標進行變換,獲得在屏幕中的繪製座標[x,y,z]

遍歷每一個座標得出一個由最大最小xy座標 [xmax, xmin, ymax, ymin] 構成的二維包圍盒

鼠標位置 (x, y) 與包圍盒邊界進行比較,若是座標處於盒子邊界以內,那麼就可判斷選中了該物體

核心代碼以下:

canvas.addEventListener('mousemove', function(e) {
    //座標轉換爲webgl表示區間
    const pos = util.windowToWebgl(tCanvas,e.clientX,e.clientY);
    const ps = [];
    Polygons.forEach((p,i)=>{
        //重置狀態
        p.select = false;
        //mvp矩陣
        const matrix = m4.translate(viewProjection, p.pos);
                let xmax, ymax, xmin, ymin, zmax, zmin;//包圍盒邊界
        //遍歷頂點獲取包圍盒的邊界
        for(let j = 0; j < p.position.length; j = j+3){
            //對座標進行矩陣轉換
            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];
        }
        // 射線處於包圍盒內
        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;
        //獲取最靠近視點的圖形
    const sel = ps.length == 1? ps[0]: ps.sort((a,b)=> a.coord[2] - b.coord[2])[0];
    sel.select = true;
},false);

射線與三角形相交

可是包圍盒算法判斷地不是很精準,在物體形狀不是很規則或物體間靠攏的比較緊時表現得尤爲明顯。

咱們知道WebGL圖形是由三角形構成的,那麼進一步判斷射線是否相交該物體某個三角形面就會很是精確了。

數學原理以下:

三角形內的任意一點均可以用它相對於三角形的頂點的位置來定義:

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

其中 u >= 0, v >= 0, u + v <= 1 ,稱爲重心座標

射線能夠用參數方程表示爲:

T(t) = P + td

其中P爲起始點,d爲方向向量

所以計算直線與三角的交點的等式爲:

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

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

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

根據克萊姆法則求解,其中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] )

具體實現代碼以下:

// 射線處於包圍盒內
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];//射線起始點
   const d = [0,0,1];//射線方向

   for(let j = 0; j < p.position.length; j = j + 9){
       //三角形頂點
       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;
       }
   }
}
相關文章
相關標籤/搜索