WebGL之物体选择

原文地址: WebGL之物体选择

使用WebGL将图形绘制到画布后,如何与外部进行交互?这其中最关键的就是如何实现物体的选择。比如鼠标点击后判断是否选中了某个图形或图形的某个部分。

本节实现的效果: WebGL选中物体

WebGL选中物体

如何实现选中物体

颜色区分法

《WebGL编程指南》中提出了一个原理很简单的解决方案,步骤如下:

  1. 鼠标按下时物体重绘为红色或其他能区分的颜色

  2. 读取鼠标点击处像素的颜色

    gl.readPixels(x,y,width,height,format,type,pixels)
    
  3. 使用物体原来的颜色进行重绘,以恢复物体本来颜色

  4. 判断第2步读取到的颜色是否与预设的颜色值相等,相等则表示点击中物体

可以说这是个非常容易实现的方案,不过要为每个物体分别设置不同的区分颜色却是个隐患,同时也不够友好。

光线投射法

这是使用最广泛也最精确的一种方案了,Three.js 中的光线投射器 (Raycaster) 就实现了这种方案,可以看里面的源代码。

光线投射

它的基本原理: 从视点出发的光线首先投射到近截面,最后投射到远截面,结合鼠标点击的位置 (x, y) 和视图投影矩阵 (viewProjection)。可以得出由近截面坐标 (x1, y1, z1) 和远截面坐标 (x2, y2, z2) 组成的射线向量。然后我们就可以将物体坐标构成的面逐个与这个向量进行对比。这涉及到线性代数中的向量,点积,叉积,矩阵等概念,比较复杂。主要分两个步骤:

  1. 创建物体的包围盒,判断射线是否穿过该物体包围盒
  2. 判断射线是否穿过该物体的某个三角形面,如果经过即可判断选中了该物体

下面就分步实现光线投射算法的上面两个步骤

包围盒

包围盒算法原理如下:

首先用视图投影模型矩阵 (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;
       }
   }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 225,151评论 6 523
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 96,465评论 3 405
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 172,429评论 0 368
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 61,147评论 1 301
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 70,149评论 6 400
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,614评论 1 315
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,965评论 3 429
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,950评论 0 279
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,486评论 1 324
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,524评论 3 347
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,640评论 1 355
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,228评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,976评论 3 340
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,407评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,552评论 1 277
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 50,215评论 3 381
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,714评论 2 366