Three.js Raycaster Tutorial for Mouse Picking

In Three.js, creating interactive 3D applications requires a way to detect which objects a user is pointing at or clicking on. This article explains the fundamental concepts of the Raycaster class, explores how it enables mouse picking, and provides a step-by-step implementation guide to handle user interaction in your 3D scenes.

What is the Raycaster Class?

The THREE.Raycaster class is a tool designed to perform raycasting. Raycasting is the process of projecting a 3D line (a ray) from a specific origin point in a specific direction and calculating which objects in the 3D space intersect with that line.

In the context of user interaction, the ray’s origin is typically the virtual camera, and its direction is determined by the position of the 2D mouse cursor on the screen. By casting this ray into the 3D scene, Three.js can identify precisely which objects the cursor is hovering over or clicking.

How Mouse Picking Works

Mouse picking with Raycaster involves three main steps:

  1. Convert Screen Coordinates to Normalized Device Coordinates (NDC): The mouse position is originally captured in 2D screen pixels (from top-left 0,0 to bottom-right width, height). Three.js requires these coordinates to be converted into a normalized 2D coordinate system where both the X and Y axes range from -1 to 1.
  2. Update the Raycaster: The raycaster is updated with the mouse’s normalized coordinates and the active camera. This calculates the correct origin and direction of the ray in 3D space.
  3. Calculate Intersections: The raycaster tests for intersections against the objects in your scene, returning an array of intersected objects sorted by distance (closest first).

Step-by-Step Implementation

1. Set Up the Mouse Coordinates Vector

First, create a 2D vector to store the normalized mouse coordinates.

import * as THREE from 'three';

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

2. Track Mouse Movement

Add an event listener to update the mouse coordinates whenever the user moves the mouse. This converts the pixel values into the -1 to 1 range.

window.addEventListener('mousemove', (event) => {
    // Convert mouse position to normalized device coordinates (-1 to +1)
    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
});

3. Cast the Ray in the Render Loop

To make the interaction dynamic, update the raycaster and check for intersections within your animation/render loop.

function animate() {
    requestAnimationFrame(animate);

    // 1. Update the ray with the camera and mouse position
    raycaster.setFromCamera(mouse, camera);

    // 2. Calculate objects intersecting the picking ray
    // 'scene.children' checks all objects in the scene. 
    // Set the second parameter to 'true' to check recursively (child objects).
    const intersects = raycaster.intersectObjects(scene.children, true);

    // 3. Handle the intersection results
    if (intersects.length > 0) {
        // The first element is the closest intersected object
        const closestObject = intersects[0].object;
        
        // Example action: Change the hovered object's color to red
        closestObject.material.color.set(0xff0000);
    }

    renderer.render(scene, camera);
}

animate();

Handling Click Events

If you only want to trigger an action when the user clicks (rather than hovers), move the raycasting logic into a click event listener.

window.addEventListener('click', () => {
    // Update the raycaster with the current mouse and camera state
    raycaster.setFromCamera(mouse, camera);

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

    if (intersects.length > 0) {
        const clickedObject = intersects[0].object;
        console.log('Clicked on:', clickedObject.name || 'an unnamed object');
        
        // Perform click action (e.g., scale up the clicked object)
        clickedObject.scale.multiplyScalar(1.2);
    }
});

Performance Best Practices