Three.js Double Click Raycaster Intersection Tutorial

This article explains how to detect double-click events on 3D objects within a Three.js scene using a Raycaster. You will learn how to capture the mouse coordinates during a double-click event, convert those coordinates into normalized device coordinates, and cast a ray from the camera to identify which 3D objects in your scene were clicked.

Step-by-Step Implementation

To trigger a Raycaster intersection on a double-click, you need to set up a standard DOM event listener for the dblclick event, convert the mouse coordinates, and update the Three.js Raycaster.

1. Initialize the Raycaster and Pointer Vectors

First, instantiate a global Raycaster and a Vector2 to store the normalized mouse coordinates.

import * as THREE from 'three';

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

2. Create the Double-Click Event Listener

Add an event listener for dblclick to your window or the specific canvas rendering your Three.js scene.

Inside the listener, calculate the mouse’s normalized device coordinates (NDC). This maps the 2D screen coordinates of the double-click (spanning from top-left to bottom-right) to a 3D coordinate system ranging from -1 to 1 on both axes.

window.addEventListener('dblclick', onDoubleMouseClick);

function onDoubleMouseClick(event) {
    // Calculate pointer position in normalized device coordinates (-1 to +1)
    pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
    pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;

    // Trigger the raycast calculation
    checkIntersections();
}

3. Cast the Ray and Detect Intersections

With the pointer coordinates updated, configure the raycaster to point from the camera through the mouse coordinates. Then, use intersectObjects to find which meshes in your scene the ray passes through.

function checkIntersections() {
    // Update the picking ray with the camera and pointer position
    raycaster.setFromCamera(pointer, camera);

    // Calculate objects intersecting the picking ray
    // Set the second parameter to true to check all descendants (recursive)
    const intersects = raycaster.intersectObjects(scene.children, true);

    if (intersects.length > 0) {
        // The first object in the array is the closest intersected object
        const targetObject = intersects[0].object;
        
        console.log('Double-clicked on:', targetObject);
        
        // Example action: Change the color of the double-clicked object
        if (targetObject.material && targetObject.material.color) {
            targetObject.material.color.set(0xff0000); // Turn red
        }
    }
}

Complete Code Example

Here is how the code fits together inside a typical Three.js setup:

import * as THREE from 'three';

// Standard Scene, Camera, and Renderer Setup
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// Add a test cube to the scene
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

camera.position.z = 5;

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

// Event Listener
window.addEventListener('dblclick', (event) => {
    // 1. Convert screen coordinates to normalized device coordinates
    pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
    pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;

    // 2. Set the raycaster
    raycaster.setFromCamera(pointer, camera);

    // 3. Test for intersections
    const intersects = raycaster.intersectObjects(scene.children);

    if (intersects.length > 0) {
        // Handle double-click action
        intersects[0].object.material.color.set(0xff00ff); // Change color to magenta
    }
});

// Animation Loop
function animate() {
    requestAnimationFrame(animate);
    renderer.render(scene, camera);
}
animate();