threejs中的拾取交互

Threejs是一个非常棒的图形库,可是对于一些交互逻辑,实现起来就没有那么容易了,这篇我们来讲解下如何实现一些常见的交互操作,比如点击、mouseover等效果。

首先来看看Threejs是如何实现点选交互的。在3d的世界中,我们选取一个物体不像2d平面上那么简单,因为我们所对应的是一个3维的世界,而鼠标所在的屏幕是一个2d的世界。如何2d的鼠标屏幕点选与3d的webgl世界关联起来呢?我们有两种方式达到鼠标拾取3d物体的目的。

raycast射线拾取

使用three.js自带的光线投射器(Raycaster)选取物体非常简单,代码如下所示

var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();

function onMouseMove(event) {
 // 计算鼠标所在位置的设备坐标
 // 三个坐标分量都是-1到1
 mouse.x = event.clientX / window.innerWidth * 2 - 1;
 mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
}

function pick() {
 // 使用相机和鼠标位置更新选取光线
 raycaster.setFromCamera(mouse, camera);

 // 计算与选取光线相交的物体
 var intersects = raycaster.intersectObjects(scene.children);
}

它是采用包围盒过滤,计算投射光线与每个三角面元是否相交实现的。

但是,当模型非常大,比如说有40万个面,通过遍历的方法选取物体和计算碰撞点位置将非常慢,用户体验不好。

但是使用gpu选取物体不存在这个问题。无论场景和模型有多大,都可以在一帧内获取到鼠标所在点的物体和交点的位置。

使用GPU拾取

实现方法也很简单:

  1. 创建选取几何体,给每个几何体顶点赋值不同的颜色
  2. 读取鼠标的位置像素颜色,根据颜色判断当前的位置
  3. 根据物体位置信息映射出当前的物体

具体实现:

  1. 传递进来一个需要交互的mesh物体集合
  2. 遍历集合,把初始的物体信息添加到字典中做映射关系
  3. clone所有物体的geometry,并且把位置从初始坐标赋值给它们
  4. 添加顶点颜色,添加到geometriesPicking集合中
  5. 遍历完之后,用BufferGeometryUtils.mergeBufferGeometries()方法合并成一个mesh,添加到pickingScene场景中,渲染即可
apply(meshList) {
    const color = new THREE.Color();
    let quaternion = new THREE.Quaternion();
    const matrix = new THREE.Matrix4();
    for (let i = 0, item; item = meshList[i++];) {
      this.pickingData[i] = {
        position: item.userData.position,
        rotation: item.userData.rotation,
        scale: item.userData.scale,
        object: item
      };

      let geometry = item.geometry.clone();
      quaternion.setFromEuler(item.rotation);
      matrix.compose(item.position, item.quaternion, item.scale);
      geometry.applyMatrix4(matrix);
      this.applyVertexColors(geometry, color.setHex(i));
      this.geometriesPicking.push(geometry);
    }
    let mesh = new THREE.Mesh(BufferGeometryUtils.mergeBufferGeometries(this.geometriesPicking), this.pickingMaterial)
    this.pickingScene.add(mesh);
    this.highlightBox = new THREE.Mesh(
      new THREE.BoxGeometry(),
      new THREE.MeshLambertMaterial({
        color: 0xffff00
      }));
    this.scene.add(this.highlightBox)
  }
applyVertexColors(geometry, color) {
    const position = geometry.attributes.position;
    const colors = [];
    for (let i = 0; i < position.count; i++) {
      colors.push(color.r, color.g, color.b);
    }
    geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
  }

渲染函数

  render() {
    this.camera.setViewOffset(this.renderer.domElement.width, this.renderer.domElement.height, this.pointer.x * window.devicePixelRatio | 0, this.pointer.y * window.devicePixelRatio | 0, 1, 1);
    this.renderer.setRenderTarget(this.pickingTexture);
    this.renderer.render(this.pickingScene, this.camera);
    this.camera.clearViewOffset();
    const pixelBuffer = new Uint8Array(4);
    this.renderer.readRenderTargetPixels(this.pickingTexture, 0, 0, 1, 1, pixelBuffer);
    const id = (pixelBuffer[0] << 16) | (pixelBuffer[1] << 8) | (pixelBuffer[2]);
    const data = this.pickingData[id];
    if (!this.highlightBox) return;
    if (data) {
      if (data.position && data.rotation && data.scale) {
        // data.object.material.color=new THREE.Color("")
        this.highlightBox.position.copy(data.position);
        this.highlightBox.rotation.copy(data.rotation);
        this.highlightBox.scale.copy(data.scale).add(this.offset);
        this.highlightBox.visible = true;
      }
    } else {
      this.highlightBox.visible = false;
    }
  }

示例demo

© 版权声明
THE END
喜欢就支持一下吧
点赞12 分享
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容