Three.js 使用 Raycaster 检测 ArrowHelper 的线和圆锥子项

Three.js Using Raycaster to detect line and cone children of ArrowHelper

我有一个用于简单绘画应用程序的正常运行的 Raycaster。我将它用作“桶工具”,用户可以在其中单击一个对象并更改其颜色。它适用于 BoxGeometry 和 CircleGeometry 等几何对象,但我很难将其应用于 ArrowHelper 对象的子对象。因为 ArrowHelper 不是形状并且不具有几何属性,所以 Raycaster 在检查 scene.children 交叉点时不会检测与其位置的碰撞。但是,ArrowHelper 对象的子对象总是两种东西:一条线和一个圆锥体,它们都具有几何、material 和位置属性。

我试过:

  1. 将函数 .intersectObjects(objects: Array, recursive: Boolean, optionalTarget: Array ) recursive 布尔值切换为真,以便它包含数组中对象的子对象。
  2. 通过遍历 ArrowHelper 对象 scene.children 并将其线条和圆锥体添加到单独的对象数组中,从而绕过 ArrowHelper 父对象。从那里我试图检查仅与直线和圆锥体列表的交叉点,但未检测到交叉点。

Raycaster 设置:

  const runRaycaster = (mouseEvent) => {

... // sets mouse and canvas bounds here

    const raycaster = new THREE.Raycaster();
    raycaster.setFromCamera(mouse, camera);

    const intersects = raycaster.intersectObjects(scene.children, true);

    if (intersects.length > 0) {
      for (let i = 0; i < intersects.length; i++) {
        // works for GEOMETRY ONLY
        // needs modifications for checking ArrowHelpers
        intersects[i].object.material.color.set(currentColor);
      }
    }
  };

这是我在没有 ArrowHelper 父级的情况下单独检查线条和圆锥体的尝试:

    let arrowObjectsList = [];
      for (let i = 0; i < scene.children.length; i++) {
        if (scene.children[i].type === 'ArrowHelper') {
          arrowObjectsList.push(scene.children[i].line);
          arrowObjectsList.push(scene.children[i].cone);
        } else {
          console.log(scene.children[i].type);
        }
      }
      console.log(arrowObjectsList); // returns 2 objects per arrow on the canvas
    // intersectsArrows always returns empty
    const intersectsArrows = raycaster.intersectObjects(arrowObjectsList, true);

一些注意事项:

  1. 每个 ArrowHelper、它的线和它的锥体都有唯一可识别的名称,因此它们可以 recolored/repositioned/deleted 以后。
  2. Raycaster 随每个 onMouseDown 和 onMouseMove 事件一起运行。
  3. 值得注意的是,ArrowHelpers 的线和圆锥体子项分别是 BufferGeometry 和 CylinderBufferGeometry,而不是 Geometry 的变体。我想知道这是否与它有关。根据 Three.JS 文档网站的 this example,BufferGeometry 可以用类似的方式被 Raycaster 检测到。

设置recursion = true对我有用。 运行下面的简单代码,然后点击箭头。您将看到打印到控制台的路口信息。 (three.js r125)

let W = window.innerWidth;
let H = window.innerHeight;

const renderer = new THREE.WebGLRenderer({
  antialias: true,
  alpha: true
});
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();

const camera = new THREE.PerspectiveCamera(28, 1, 1, 1000);
camera.position.set(5, 5, 5);
camera.lookAt(scene.position);
scene.add(camera);

const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(0, 0, -1);
camera.add(light);

const mesh = new THREE.ArrowHelper(
  new THREE.Vector3(0, 0, 1),
  new THREE.Vector3(0, 0, 0),
  2,
  0xff0000,
  1,
  1
);
scene.add(mesh);

function render() {
  renderer.render(scene, camera);
}

function resize() {
  W = window.innerWidth;
  H = window.innerHeight;
  renderer.setSize(W, H);
  camera.aspect = W / H;
  camera.updateProjectionMatrix();
  render();
}

window.addEventListener("resize", resize);

resize();
render();

// RAYCASTER STUFF
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

renderer.domElement.addEventListener('mousedown', function(e) {

  mouse.set(
    (event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1
  );

  raycaster.setFromCamera(mouse, camera);

  const intersects = raycaster.intersectObjects(scene.children, true);

  console.log(intersects);

});
html,
body {
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;
  overflow: hidden;
  background: skyblue;
}
<script src="https://threejs.org/build/three.min.js"></script>

仔细一看,是设置位置的问题,不一定是箭头。箭头的位置根据用户单击鼠标以指定起点而变化。但是,它仍然存在几个问题: select 该行非常困难,因为 LineBasicMateriallineWidth 值除了 1 之外不能有任何其他值,尽管它是可编辑的。这是由于 OpenGL 核心配置文件中的限制,如 the docs and in this question 中所述。同样,圆锥体不会响应 setLength。这严重限制了 ArrowHelper 工具的自定义。

因此,我决定用两个耦合在一起的对象完全替换 ArrowHelper:tubeGeometry 和 coneGeometry,它们都指定了 MeshBasicMaterial,Raycaster 可以开箱即用。

... // the pos Float32Array is set according to user mouse coordinates.

  const v1 = new THREE.Vector3(pos[0], pos[1], pos[2]);
  const v2 = new THREE.Vector3(pos[3], pos[4], pos[5]);

  const material = new THREE.MeshBasicMaterial({
    color: color,
    side: THREE.DoubleSide,
  });
  // Because there are only two vectors, no actual curve occurs.
  // Therefore, it's our straight line.
  const tubeGeometry = new THREE.TubeBufferGeometry(
      new THREE.CatmullRomCurve3([v1, v2]), 1, 3, 3, false);
  const coneGeometry = new THREE.ConeGeometry(10, 10, 3, 1, false);
  arrowLine = new THREE.Mesh(tubeGeometry, material);
  arrowTip = new THREE.Mesh(coneGeometry, material);
  // needs names to be updated later.
  arrowLine.name = 'arrowLineName';
  arrowTip.name = 'arrowTipName';

放置箭头时,用户会通过点击和拖动来指定箭头的起点和终点,所以箭头和它的尖端必须更新为onMouseMove。我们必须使用 Math.atan2 来获取 v1 和 v2 之间的角度,以 v1 为中心。减去 90 将旋转定向到默认位置。

... // on the onMouseMove event, pos is updated with new coords.

const setDirection = () => {
    const v1 = new THREE.Vector3(pos[0], pos[1], pos[2]);
    const v2 = new THREE.Vector3(pos[3], pos[4], pos[5]);
    
    // copying the v2 pos ensures that the arrow tip is always at the end.
    arrowTip.position.copy(v2);

    // rotating the arrow tip according to the angle between start and end
    // points, v1 and v2.
    let angleDegrees = 180 - (Math.atan2(pos[1] - pos[4], pos[3] - pos[0]) * 180 / Math.PI - 90);
    const angleRadians = angleDegrees * Math.PI / 180;
    arrowTip.rotation.set(0, 0, angleRadians);
    
    // NOT VERY EFFICIENT, but it does the job to "update" the curve.
    arrowLine.geometry.copy( new THREE.TubeBufferGeometry(new THREE.CatmullRomCurve3([v1, v2]),1,3,3,false));
    scene.add(arrowLine);
    scene.add(arrowTip);
  };

开箱即用,这个“箭头”允许我 select 并使用 Raycaster 毫无问题地对其进行编辑。不用担心线条定位、线条粗细或线条长度。