• 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;
           }
       }
    }
    
  • 相关阅读:
    Git命令缩写
    MySQL中varchar(10)和varchar(100)的优缺点
    理解Kafka的Topic、Partion
    DAN 文本分类
    股票问题系列通解(转载翻译)
    leecode每日刷题1
    leecode每日刷题3
    leecode每日刷题2
    VMware Workstation中安装kali2022 残梅殇
    kali安装中文输入法 残梅殇
  • 原文地址:https://www.cnblogs.com/edwardloveyou/p/10943102.html
Copyright © 2020-2023  润新知