How to "hover" only one object using raycastr

Hello, I have the following code which works great to “hover” objects, however it is possible for multiple objects to be hovered at once if their meshes overlap at the point of the mouse. I understand that the intersections array is sorted such that the first element is the closest, but I can’t figure out how to use this to my advantage given the following code. Any help is appreciated.


const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();

var raycastLayer = [];
let hovered = {};
let intersects = [];

window.addEventListener('pointermove', (e) => {

    pointer.set((e.clientX / window.innerWidth) * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1)
    raycaster.setFromCamera(pointer, camera)
    intersects = raycaster.intersectObjects(raycastLayer, true)


    // if a previously hovered item is not among the hits we must call onPointerOut
    Object.keys(hovered).forEach((key) => {

        const hit = intersects.find((hit) => hit.object.uuid === key)

        if (hit === undefined) {
            const hoveredItem = hovered[key]
            if (hoveredItem.object.onPointerOver) hoveredItem.object.onPointerOut(hoveredItem)
            delete hovered[key]
        }
    })

    intersects.forEach((hit) => {

        // if a hit has not been flagged as hovered we must call onPointerOver
        if (!hovered[hit.object.uuid]) {
            hovered[hit.object.uuid] = hit
            if (hit.object.onPointerOver) hit.object.onPointerOver(hit)
        }

        // call onPointerMove
        if (hit.object.onPointerMove) hit.object.onPointerMove(hit)
    })

    render();
})

For a super-quick fix without much changes, you should be able to just change this line:

intersects.forEach((hit) => {

into:

[ intersects[0] ].forEach((hit) => {

And you’re done :sweat_smile:

(If you’d prefer the longer, and also a bit cleaner way, all you need to do is to remove .forEach entirely, and just work with intersects[0] - no loops involved. Just check if value of hovered is the same as intersects[0], call onPointerOut if it’s not, then call onPointerOver for intersects[0], set hovered to intersects[0].)

1 Like

Thanks for your help. I’ve been stuck on this for a couple of hours and tried a bunch of different things, to no avail! I’m sure it’s very simple if someone is able to take a glance and help me out:

I realize I am still using the full array for the hovered section, but whatever I did before didn’t work so I just put it back.

https://playcode.io/1436752

import * as THREE from 'three';
import { OrbitControls } from 'three-controls';

// ----------------------- SCENE
const scene = new THREE.Scene();

// ----------------------- CAMERA
const camera = new THREE.PerspectiveCamera(
  60,
  window.innerWidth / window.innerHeight,
  0.1,
  10000
);
camera.position.set(0, 0, 5);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xfff000);
document.body.appendChild(renderer.domElement);
renderer.setPixelRatio(window.devicePixelRatio);

// ----------------------- CONTROLS
const controls = new OrbitControls(camera, renderer.domElement);
controls.addEventListener('change', render);

// ----------------------- RAYCASTING
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();

var raycastLayer = [];
let hovered = {};
let intersects = [];

window.addEventListener('pointermove', e => {
  pointer.set(
    (e.clientX / window.innerWidth) * 2 - 1,
    -(e.clientY / window.innerHeight) * 2 + 1
  );
  raycaster.setFromCamera(pointer, camera);
  intersects = raycaster.intersectObjects(raycastLayer, true);

  // if a previously hovered item is not among the hits we must call onPointerOut
  Object.keys(hovered).forEach(key => {
    const hit = intersects.find(hit => hit.object.uuid === key);

    if (hit === undefined) {
      const hoveredItem = hovered[key];
      if (hoveredItem.object.onPointerOver)
        hoveredItem.object.onPointerOut(hoveredItem);
      delete hovered[key];
    }
  });

  if (intersects.length > 0) {
    [intersects[0]].forEach(hit => {
      // if a hit has not been flagged as hovered we must call onPointerOver
      if (!hovered[hit.object.uuid]) {
        hovered[hit.object.uuid] = hit;
        if (hit.object.onPointerOver) hit.object.onPointerOver(hit);
      }

      // call onPointerMove
      if (hit.object.onPointerMove) hit.object.onPointerMove(hit);
    });
  }

  render();
});

window.addEventListener('click', e => {
  if (e.target == renderer.domElement) {
    // update the picking ray with the camera and pointer position
    raycaster.setFromCamera(pointer, camera);
    const intersects = raycaster.intersectObjects(raycastLayer, true);

    intersects.forEach(hit => {
      if (hit.object.onClick) {
        hit.object.onClick(hit);
      }
    });
  }
});

// ----------------------- LIGHTING
const ambLight = new THREE.AmbientLight(0xffffff, 0.7); // soft white light
scene.add(ambLight);
scene.background = new THREE.Color(0xffff00);

// ----------------------- GLYPHS
const activeGlyphScale = 1.25;

class Glyph extends THREE.Mesh {
  constructor() {
    super();

    this.position.set(
      Math.random() * 5 - 2.5,
      Math.random() * 5 - 2.5,
      Math.random() * 5 - 2.5
    );

    this.isActive = false;

    let randCol = new THREE.Color(0xffffff);
    randCol.setHex(Math.random() * 0xffffff);

    this.geometry = new THREE.PlaneGeometry(1, 1);

    this.material = new THREE.MeshStandardMaterial({
      side: 2,
      color: randCol,
      emissiveIntensity: 1,
    });

    this.scale.setScalar(1);
  }

  onPointerOver() {
    this.scale.setScalar(1.5);
  }

  onPointerOut() {
    this.scale.setScalar(1);
  }

  onClick(e) {}
}
let glyphs = [];
for (let i = 0; i < 15; i++) {
  glyphs[i] = new Glyph();
  scene.add(glyphs[i]);
  raycastLayer.push(glyphs[i]);
}

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

animate();
function animate() {
  requestAnimationFrame(animate);
}

you essentially want something like the doms event.stopPropagation() which requires event bubbling. just picking the first imo is not enough and will result in buggy behaviour. this all gets worse and worse and worse with having to manage pointerOut, capture once the pointer goes out the window etc. if you want, here’s a full pointer event layer: react-three-fiber/events.ts at 75521d21511b3523dbd8f692af8211710f036006 · pmndrs/react-three-fiber · GitHub but imo it makes no sense to re-implement this in vanilla, just use three with react where all of this is battle tested and taken care of.

for reference: React Three Fiber Documentation