原文地址: WebGL之物體選擇
使用WebGL將圖形繪制到畫布后,如何與外部進(jìn)行交互?這其中最關(guān)鍵的就是如何實(shí)現(xiàn)物體的選擇。比如鼠標(biāo)點(diǎn)擊后判斷是否選中了某個(gè)圖形或圖形的某個(gè)部分。
本節(jié)實(shí)現(xiàn)的效果: WebGL選中物體
如何實(shí)現(xiàn)選中物體
顏色區(qū)分法
《WebGL編程指南》中提出了一個(gè)原理很簡(jiǎn)單的解決方案,步驟如下:
鼠標(biāo)按下時(shí)物體重繪為紅色或其他能區(qū)分的顏色
-
讀取鼠標(biāo)點(diǎn)擊處像素的顏色
gl.readPixels(x,y,width,height,format,type,pixels)
使用物體原來(lái)的顏色進(jìn)行重繪,以恢復(fù)物體本來(lái)顏色
判斷第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è)步驟:
- 創(chuàng)建物體的包圍盒,判斷射線是否穿過(guò)該物體包圍盒
- 判斷射線是否穿過(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;
}
}
}