9. [Three.js] Interactive Object Detection with Raycasting
1. Digest
Raycasting is a fundamental technique for creating interactive 3D applications, enabling detection of which objects intersect with an invisible ray cast through the scene. The THREE.Raycaster class provides two core implementations: automatic mesh detection using a fixed ray direction, and user-driven interaction through mouse click detection.
The first example demonstrates continuous ray collision detection by casting a ray from a fixed origin point and detecting which animated meshes intersect with it in real-time. A visual guide line helps understand how the ray travels through 3D space, changing the color of intersected objects dynamically. The second example advances to practical user interaction, converting 2D mouse coordinates to normalized device coordinates (NDC) and casting rays from the camera through the click position to detect which 3D objects the user has selected.
A critical enhancement addresses a common UX problem: distinguishing between clicks and drags when using OrbitControls. The custom PreventDragClick utility class tracks mouse movement distance and time between mousedown and mouseup events, preventing false click detection when users are actually rotating the camera. This pattern is essential for any interactive 3D application combining camera controls with object selection.
2. What is the purpose
Learning raycasting provides essential interactive 3D techniques that form the foundation of user interaction in Three.js applications. The skills covered include:
- Understanding raycasting fundamentals and how
THREE.Raycasterworks - Implementing continuous object detection using fixed ray directions
- Converting 2D mouse coordinates to 3D ray projections using normalized device coordinates (NDC)
- Detecting which 3D objects users click on using
setFromCamera()andintersectObjects() - Processing intersection results to identify closest objects and handle multiple overlapping objects
- Distinguishing between genuine clicks and camera drag operations with OrbitControls
- Creating reusable utility classes for common interaction patterns
These skills are critical for building interactive 3D experiences like games with clickable objects, product configurators with selectable parts, architectural visualizations with interactive elements, and any application requiring user selection of 3D objects.
3. Some code block and its explanation
Example 1: Basic Raycaster Setup with Fixed Direction
// Raycaster
const raycaster = new THREE.Raycaster();
// Visual guide line to see the ray
const lineMaterial = new THREE.LineBasicMaterial({ color: 'yellow' });
const points = [];
points.push(new THREE.Vector3(0, 0, 100));
points.push(new THREE.Vector3(0, 0, -200));
const lineGeometry = new THREE.BufferGeometry().setFromPoints(points);
const guide = new THREE.Line(lineGeometry, lineMaterial);
scene.add(guide);
// In draw loop
const origin = points[0]; // Starting point (0, 0, 100)
const direction = new THREE.Vector3(0, 0, -100);
raycaster.set(origin, direction.normalize());
const intersects = raycaster.intersectObjects(meshes);
intersects.forEach((intersectedItem) => {
intersectedItem.object.material.color.set('red');
});
This demonstrates the core raycasting mechanism. A Raycaster is created, then configured with an origin point and direction vector. The direction must be normalized (length = 1) to represent pure direction without magnitude. The visual guide line helps debug by showing exactly where the ray travels. intersectObjects() returns an array of intersection results sorted by distance from the origin, each containing the intersected object, intersection point, distance, face index, and UV coordinates. This pattern is useful for laser beams, line-of-sight detection, or automated collision checking.
Example 2: Mouse Click Detection with Camera Ray
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function checkIntersects() {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(meshes);
for (const item of intersects) {
item.object.material.color.set('blue');
break; // Only select the first (closest) object
}
}
canvas.addEventListener('click', (event) => {
// Convert screen coordinates to NDC (Normalized Device Coordinates)
mouse.x = event.clientX / canvas.clientWidth * 2 - 1;
mouse.y = -(event.clientY / canvas.clientHeight * 2 - 1);
checkIntersects();
});
This implements user-driven object selection through mouse clicks. The critical step is converting pixel coordinates (0 to width/height) into normalized device coordinates (NDC) ranging from -1 to +1, where (-1, -1) is bottom-left and (1, 1) is top-right. Note the Y-axis inversion (negative sign) because screen coordinates have Y increasing downward while 3D coordinates have Y increasing upward. setFromCamera() automatically calculates the ray origin and direction from the camera through the mouse position in 3D space. Breaking after the first intersection ensures only the closest visible object is selected, ignoring objects behind it.
Example 3: Preventing False Clicks During Camera Drag
export class PreventDragClick {
constructor(element) {
this.mouseMoved;
let clickStartX;
let clickStartY;
let clickStartTime;
element.addEventListener("mousedown", (event) => {
clickStartX = event.clientX;
clickStartY = event.clientY;
clickStartTime = Date.now();
});
element.addEventListener("mouseup", (event) => {
const xGap = Math.abs(event.clientX - clickStartX);
const yGap = Math.abs(event.clientY - clickStartY);
const timeGap = Date.now() - clickStartTime;
if (xGap > 5 || yGap > 5 || timeGap > 500) {
this.mouseMoved = true;
} else {
this.mouseMoved = false;
}
});
}
}
// Usage
const preventDragClick = new PreventDragClick(canvas);
function checkIntersects() {
if (preventDragClick.mouseMoved) return; // Ignore drags
// ... rest of intersection code
}
This utility class solves a critical UX problem: when using OrbitControls, dragging to rotate the camera technically triggers click events on mouseup. The class tracks mouse position and timing between mousedown and mouseup. If the mouse moved more than 5 pixels in any direction, or the interaction lasted longer than 500ms, it's classified as a drag rather than a click. The checkIntersects() function checks this flag before processing selections, ensuring users don't accidentally select objects while trying to rotate the camera. This pattern is essential for any 3D application combining camera controls with interactive objects.
Example 4: Understanding Normalized Direction Vectors
// Before normalization
const direction = new THREE.Vector3(0, 0, -100);
console.log(direction.length()); // 100
// After normalization
direction.normalize();
console.log(direction.length()); // 1
console.log(direction); // Vector3 { x: 0, y: 0, z: -1 }
Normalization converts a vector to unit length (length = 1) while preserving its direction. This is crucial for raycasting because the Raycaster needs a pure direction vector, not a specific distance. The formula is: normalized = vector / vector.length(). In this example, (0, 0, -100) becomes (0, 0, -1) after normalization. Both vectors point in the same direction (negative Z-axis), but the normalized version has unit length, making it suitable for ray calculations. Always call .normalize() on direction vectors before passing them to raycaster.set().
