Raycaster triggers all tiles without mouse moving

Hello!
I am stuck on this problem where I would like to have an individual tile turn pink if I hover over it. However, all of the tiles turn pink, immediately upon refreshing the page. I am not sure what I might be doing incorrectly.

Here is what the viewport looks like:

And here is my main.js code:

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

let camera, scene, renderer, controls;

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

init();
animate();

function init() {
  
  renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } );
  renderer.setPixelRatio( window.devicePixelRatio );
  renderer.setSize( window.innerWidth, window.innerHeight );
  document.body.appendChild( renderer.domElement );

  camera = new THREE.PerspectiveCamera( 20, window.innerWidth / window.innerHeight, 0.1, 10000 );
  camera.position.set( 0, 50, 30 );

  scene = new THREE.Scene();

  const light = new THREE.HemisphereLight( 0xffffff, 0x888888, 3 );
  light.position.set( 0, 1, 0 );
  scene.add( light );

  const geometry = new THREE.BoxGeometry(1, 0.5, 1.5);
  const material = new THREE.MeshPhongMaterial( {color: 0xffff00} );

  for (let rowIndex = 0; rowIndex < 4; rowIndex++) {

    for (let tileIndex = 0; tileIndex < 4; tileIndex++) {

      const tile = new THREE.Mesh( geometry, material );
      tile.position.set(-4 + 2.5 * tileIndex, 0, -3 + 2.75 * rowIndex - 1)
      scene.add(tile);

    }

  }

  controls = new OrbitControls( camera, renderer.domElement );
  controls.enableDamping = true;
  controls.enableZoom = false;
  controls.enablePan = false;
  controls.maxPolarAngle = Math.PI/2;

  window.addEventListener( 'resize', onWindowResize );
  document.addEventListener( 'mousemove', onMouseMove );

}

function onWindowResize() {

  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();

  renderer.setSize( window.innerWidth, window.innerHeight );

}

function onMouseMove( event ) {

  event.preventDefault();

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

}

function animate() {

  requestAnimationFrame( animate );

  controls.update();

  raycaster.setFromCamera( mouse, camera );

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

  for ( let i = 0; i < intersects.length; i ++ ) {

    intersects[ i ].object.material.color.set( 0xff3060 );

  }

  render();

}

function render() {

  renderer.render( scene, camera );

}

If you have any suggestions, please let me know.

Thank you!

All tiles share the same material. When you change its color, all tiles will use the new color. To solve the issue, create a separate material for each tile (the geometry is OK to be shared).

About having tiles colored without mouse moving → the raycaster uses mouse as the last normalized position of the mouse pointer. By default it has some value (maybe (0,0)), so even if the mouse is not moved, the raycaster will work and will use this value.

1 Like

Thank you for your suggestions, PavelBoytchev!

Based on them I’ve added some changes in the code that made a huge difference.
The commented sections mention the changes:

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

let camera, scene, renderer, controls;

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2(100, 100);
// setting the mouse's value to (100, 100) 
// to start all tiles off as yellow,
// otherwise, the top left tile will be pink.

init();
animate();

function init() {
  
  renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } );
  renderer.setPixelRatio( window.devicePixelRatio );
  renderer.setSize( window.innerWidth, window.innerHeight );
  document.body.appendChild( renderer.domElement );

  camera = new THREE.PerspectiveCamera( 20, window.innerWidth / window.innerHeight, 0.1, 10000 );
  camera.position.set( 0, 50, 30 );

  scene = new THREE.Scene();

  const light = new THREE.HemisphereLight( 0xffffff, 0x888888, 3 );
  light.position.set( 0, 1, 0 );
  scene.add( light );

  const geometry = new THREE.BoxGeometry(1, 0.5, 1.5);
  // const material = new THREE.MeshPhongMaterial( {color: 0xffff00} );
  // ^ moved this to the nested for loop that creates each tile ^

  for (let rowIndex = 0; rowIndex < 4; rowIndex++) {

    for (let tileIndex = 0; tileIndex < 4; tileIndex++) {

      const material = new THREE.MeshPhongMaterial( {color: 0xffff00} );
      const tile = new THREE.Mesh( geometry, material );
      tile.position.set(-4 + 2.5 * tileIndex, 0, -3 + 2.75 * rowIndex - 1)
      scene.add(tile);

    }

  }

  controls = new OrbitControls( camera, renderer.domElement );
  controls.enableDamping = true;
  controls.enableZoom = false;
  controls.enablePan = false;
  controls.maxPolarAngle = Math.PI/2;

  window.addEventListener( 'resize', onWindowResize );
  document.addEventListener( 'mousemove', onMouseMove );

}

function onWindowResize() {

  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();

  renderer.setSize( window.innerWidth, window.innerHeight );

}

function onMouseMove( event ) {

  event.preventDefault();

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

}

function animate() {

  requestAnimationFrame( animate );

  controls.update();

  raycaster.setFromCamera( mouse, camera );

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


  // replaced this  
  // for ( let i = 0; i < intersects.length; i ++ ) {

  //   intersects[ i ].object.material.color.set( 0xff3060 );

  // }

  // with this:
  if (intersects.length > 0) {

    intersects[0].object.material.color.set(0xff3060);

  }

  render();

}

function render() {

  renderer.render( scene, camera );

}

Thank you so much for your help! It is now working.

1 Like