关于前端:一文学会threejs鼠标交互Raycaster拾取物体

58次阅读

共计 3228 个字符,预计需要花费 9 分钟才能阅读完成。

版权申明:此文首发于我的集体博客 Keyon Y,转载请注明出处。

对场景内的模型增加事件监听,实现鼠标交互,须要用到Raycaster(光线投射)类。

拾取物体的原理

webGL 中获取鼠标交互物体的原理:通过三维空间中 相机视点 鼠标在屏幕上的地位 的连线,造成一条直线,捕捉与此直线相交的空间中的物体,即为交互对象物体。

在 three 中,Raycaster为咱们封装了大量的逻辑代码,包含生成相机到鼠标的射线、射线与空间物体的碰撞检测、射线相交物体深度计算、相交物体列表等等。应用起来十分不便。

获取鼠标所在位置的物体

先放代码

//...
    constructor: function () {
        INTERSECTED: null // 暂存射线相交的物体(交互的模型对象)mouse = new THREE.Vector2();}
//...
/**
   * 作者: Keyon
   * 性能: 渲染时的回调
   * 形容:
   * @param mesh {Object} Object3D、Mesh、Group 均可,可不传默认应用初始化实例时传入的 mesh 对象
   * @returns {Null} 默认返回 null
   */
render: function (mesh = null) {if (!mesh) mesh = this.mesh;
    // 通过摄像机和鼠标地位更新射线
    this.raycaster.setFromCamera(this.mouse, app.camera);
    // 计算物体和射线的焦点
    let intersects = this.raycaster.intersectObject(mesh, true);
    if (intersects.length > 0) {if ( this.INTERSECTED !== intersects[ 0].object ) {if ( this.INTERSECTED) this.INTERSECTED.material.emissive.setHex(this.INTERSECTED.currentHex);
        // 记录以后对象
        this.INTERSECTED = intersects[0].object;
        // 记录以后对象自身色彩
        this.INTERSECTED.currentHex = this.INTERSECTED.material.emissive.getHex();
        // 设置色彩为灰色
        this.INTERSECTED.material.emissive.setHex(0x333333);

      }

    } else {
      // 复原上一个对象色彩并置空变量
      if (this.INTERSECTED) this.INTERSECTED.material.emissive.setHex(this.INTERSECTED.currentHex);

      this.INTERSECTED = null;

    }
  },

上面来解释一下

  1. Raycaster类的 .intersectObject() 办法:检测所有在射线与物体之间,包含或不包含后辈的相交局部。返回后果时,相交局部将按间隔进行排序,最近的位于第一个。
  2. 对物体进行材质变换,调整emissive(材质的自发光)的 Hex 色彩,实现 hover 类型的交互成果。

鼠标坐标的转换

先上最终的代码

  onMouseMove: function (event) {event.preventDefault();
    // 将鼠标地位归一化为设施坐标。x 和 y 方向的取值范畴是 (-1 to +1)
    // renderer 为 three 的渲染器
    let px = renderer.domElement.getBoundingClientRect().left;
    let py = renderer.domElement.getBoundingClientRect().top;
    this.mouse.x = ((event.clientX - px) / (renderer.domElement.offsetWidth) ) * 2 - 1;
    this.mouse.y = - ((event.clientY - py) / (renderer.domElement.offsetHeight) ) * 2 + 1;
  },
  /*
   * 作者: Keyon
   * 性能: 增加鼠标挪动监听,拾取物体
   * 形容:
   * @param callback {Function} 拾取物体的回调,参数是拾取的 mesh
   * @returns {Null} 默认返回 null
   */
  listenOnMouseMove: function (callback = null) {if (typeof callback === 'function') callback();
    let fn = (e) => {this.onMouseMove(e); };
    window.addEventListener('mousemove', fn, false);
  }

增加鼠标挪动监听,就是一个很常见的监听事件,不做解释。

重点是将鼠标坐标转化成设施坐标,从而影响三维空间中发射射线方向的问题。

对于鼠标地位转换,官网案例中给出的代码,是如下这样的:

    this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    this.mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;

这样的计算形式会有个问题,如果 canvas 元素并不是全屏的,即 canvas 元素的
domCanvas.clientWidth !== document.documentElement.clientWidth || domCanvas.clientHeight !== document.documentElement.clientHeight 为 true,那么上边的转换就会有问题,鼠标拾取物体的地位就会产生偏移。

坐标转换原理

// 失去
 mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
 mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
  
 推导过程:设 A 点为点击点(x1,y1),x1=e.clintX, y1=e.clientY
 设 A 点在世界坐标中的坐标值为 B(x2,y2);
 
 因为 A 点的坐标值的原点是以屏幕左上角为(0,0);
 咱们能够计算可得以屏幕核心为原点的 B' 值
 x2' = x1 - innerWidth/2
 y2' = innerHeight/2 - y1
 又因为在世界坐标的范畴是[-1,1], 要失去正确的 B 值咱们必须要将坐标标准化
 x2 = (x1 -innerWidth/2)/(innerwidth/2) = (x1/innerWidth)*2-1
 同理得 y2 = -(y1/innerHeight)*2 +1

具体推导过程可参考:
threejs 对象拾取

提炼进去就是

mouse.x = (< 鼠标绝对于可视区域的横坐标 > / < 可视区域的宽 >) * 2 - 1;
mouse.y = -(< 鼠标绝对于可视区域的纵坐标 > / < 可视区域的高 >) * 2 + 1;

因为 canvas 并非全屏的元素,所以咱们须要从新计算这几个值值。

  1. 首先, renderer.domElement的大小就是 three 场景可视区域的范畴。所以通过 renderer.domElement 推算即可。
  2. 鼠标绝对于可视区域的横坐标 推算为e.clientX - renderer.domElement.getBoundingClientRect().left
  3. 可视区域的宽 推算为renderer.domElement.offsetWidth
  4. 鼠标绝对于可视区域的纵坐标 推算为e.clientY - renderer.domElement.getBoundingClientRect().top
  5. 可视区域的高 推算为renderer.domElement.offsetHeight

那么,就失去了最终的代码。完满实现拾取物体的鼠标交互。

都看到这里了,还不给个赞,加个珍藏吗~
点赞珍藏的马上升职加薪[手动滑稽]

参考资料

ThreeJS 中的点击与交互——Raycaster 的用法

threejs 对象拾取

正文完
 0