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:
- Convert Screen Coordinates to Normalized Device Coordinates
(NDC): The mouse position is originally captured in 2D screen
pixels (from top-left
0,0to bottom-rightwidth, height). Three.js requires these coordinates to be converted into a normalized 2D coordinate system where both the X and Y axes range from-1to1. - 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.
- 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
- Limit Target Objects: Avoid passing the entire
scene.childrenarray tointersectObjectsif your scene contains thousands of static background objects. Instead, maintain a dedicated array of “interactive” meshes and pass only that array to the raycaster. - Simplify Geometries: Raycasting against highly complex high-poly meshes can cause performance lag. To optimize, you can raycast against invisible, low-poly helper meshes (bounding boxes or spheres) that mimic the shape of the complex objects.
- Throttle Event Listeners: If performing heavy logic on hover, throttle your mouse movement event listener so the raycasting calculation doesn’t fire more often than necessary.