ZontarZon
ZontarZon

Reputation: 753

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

I have a functioning Raycaster for a simple painting app. I use it for a "bucket tool" in which the user can click on an object and change its color. It works for geometry objects such as BoxGeometry and CircleGeometry, but I'm struggling to apply it to the children of an ArrowHelper object. Because ArrowHelper isn't a shape and does not possess a geometry attribute, Raycaster does not detect collision with its position when checking scene.children for intersections. However, the children of ArrowHelper objects are always two things: a line and a cone, both of which have geometry, material, and position attributes.

I HAVE TRIED:

  1. Toggling the recursive boolean of the function .intersectObjects(objects: Array, recursive: Boolean, optionalTarget: Array ) to true, so that it includes the children of the objects in the array.
  2. Circumventing the ArrowHelper parent by iterating through scene.children for ArrowHelper objects and adding their lines and cones into a separate array of objects. From there I attempted to check for intersections with only the list of lines and cones, but no intersections were detected.

Raycaster setup:

  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);
      }
    }
  };

Here's my attempt to check the lines and cones individually, without the ArrowHelper parent:

    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);

SOME NOTES:

  1. Every ArrowHelper, its line, and its cone have uniquely identifiable names so they can be recolored/repositioned/deleted later.
  2. The Raycaster runs with every onMouseDown and onMouseMove event.
  3. Notably, the line and cone children of ArrowHelpers are BufferGeometry and CylinderBufferGeometry, respectively, rather than variations of Geometry. I'm wondering if this has anything to do with it. According to this example from the Three.JS documentation website, BufferGeometry can be detected by Raycaster in a similar fashion.

Upvotes: 0

Views: 952

Answers (2)

ZontarZon
ZontarZon

Reputation: 753

After a closer inspection, it was a matter of the set position, not necessarily the arrow. The position of the arrow varied based on user mouse click to specify the start point. However, it still presented several problems: It was very difficult to select the line because the lineWidth value of LineBasicMaterial cannot have any other value besides 1, despite being editable. This is due to a limitation in the OpenGL Core Profile, as addressed in the docs and in this question. Similarly, the cone would not respond to setLength. This limits the customization of the ArrowHelper tool pretty badly.

Because of this, I decided to entirely replace ArrowHelper with two objects coupled together: tubeGeometry and coneGeometry, both assigned a MeshBasicMaterial, in a way which can be accessed by Raycasters out of the box.

... // 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';

When placing the arrow, the user will click and drag to specify the start and end point of the arrow, so the arrow and its tip have to be updated with onMouseMove. We have to use Math.atan2 to get the angle in degrees between v1 and v2, with v1 as the center. Subtracting 90 orients the rotation to the default position.

... // 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);
  };

Out of the box, this "arrow" allows me to select and edit it with Raycaster without a problem. No worrying about line positioning, line thickness, or line length.

Upvotes: 0

TheJim01
TheJim01

Reputation: 8896

Setting recursion = true worked for me. Run the simple code below, and click on the arrow head. You will see the intersection information printed to the console. (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>

Upvotes: 1

Related Questions