Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 32

Détection de Clics 3D avec Raycaster

Recommended resources

Bonjour !

Dans la leçon précédente, nous avons découvert comment utiliser GSAP pour créer des animations complexes et séquencées. C'est un outil formidable pour donner vie à nos scènes. Cependant, une application 3D professionnelle est rarement un simple spectacle passif ; elle doit être interactive.

Aujourd'hui, nous allons aborder une question fondamentale : comment faire réagir notre scène 3D aux actions de l'utilisateur, comme un clic de souris ? C'est la dernière brique de notre apprentissage du Three.js "impératif" avant de passer à une approche plus moderne. L'objectif de cette leçon est donc d'implémenter la détection de clics sur des objets 3D de manière impérative avec le Raycaster.

1. Le problème : de la 2D à la 3D

Quand un utilisateur clique sur l'écran, le navigateur vous donne des coordonnées en 2D (les pixels x et y). Mais comment savoir quel objet 3D se trouve "derrière" ce pixel dans notre scène, en tenant compte de la perspective, de la distance et de la position de la caméra ? C'est là que le Raycasting entre en jeu.

Imaginez que vous projetiez un rayon laser invisible depuis l'œil de la caméra, qui passe exactement par le point où se trouve le curseur de la souris sur l'écran, et qui continue sa course à travers votre scène 3D. Le premier objet que ce rayon touche est celui que l'utilisateur a voulu cliquer.

Visualizing the Raycaster Concept in 3D
Cette image illustre parfaitement le concept. Le rayon (en orange) part de la caméra (hors champ) et traverse la scène. Les objets qu'il intercepte (le cylindre, le cube, le tore...) sont les candidats pour une interaction.

Dans Three.js, l'outil qui nous permet de lancer ce "rayon laser" et de détecter les collisions est le THREE.Raycaster.


2. Mettre en œuvre le Raycasting

Le processus pour détecter un clic se décompose en quelques étapes logiques.

  1. Créer les outils : Nous avons besoin d'une instance du Raycaster et d'un Vector2 pour stocker les coordonnées de la souris.
  2. Écouter l'événement : Mettre en place un addEventListener pour l'événement click.
  3. Normaliser les coordonnées : Dans la fonction de rappel de l'événement, il faut convertir les coordonnées de la souris (en pixels, ex: 800x600) en "coordonnées normalisées de l'appareil" (Normalized Device Coordinates ou NDC) que Three.js peut comprendre. Ce système de coordonnées va de -1 à +1 sur les axes X et Y, avec (0, 0) au centre de l'écran.
  4. Lancer le rayon : On utilise la méthode raycaster.setFromCamera(), en lui donnant les coordonnées normalisées et la caméra.
  5. Calculer les intersections : On appelle raycaster.intersectObjects(), en lui passant un tableau des objets que l'on souhaite tester (par exemple, tous les enfants de la scène : scene.children).
  6. Agir sur le résultat : Cette méthode renvoie un tableau des objets touchés, triés par distance (le plus proche en premier). Si le tableau n'est pas vide, c'est que nous avons cliqué sur au moins un objet !

Voyons comment un expert met cela en pratique. La vidéo suivante est un excellent exemple concret.

Tutoriel JavaScript : Panorama 360°

Dans ce tutoriel de Grafikart.fr, vous allez voir comment le Raycaster est utilisé pour ajouter des points d'intérêt interactifs dans un panorama 360°. C'est un cas d'usage parfait pour notre leçon.

Regardez l'extrait de 26:57 à 28:17. L'auteur y explique de manière très claire le concept de "faisceau laser" (raycasting) pour faire le lien entre le clic 2D et la scène 3D.

Maintenant que le concept est clair, nous allons voir le code. L'article suivant présente un exemple très concis et complet, qui rassemble toutes les étapes que nous venons de décrire.

Three.js – Lumière et Intéraction

L'article "Three.js – Lumière et Intéraction" de Syslog contient un bloc de code parfait pour notre apprentissage. Il est clair, en français, et va droit au but.

Étudiez attentivement le bloc de code sous la section "Détection des clics". Analysez chaque ligne en vous référant aux 6 étapes que j'ai décrites plus haut. Vous y retrouverez la création du raycaster, la normalisation des coordonnées de la souris (mouse.x = ..., mouse.y = ...), la mise à jour avec setFromCamera, et enfin l'intersection avec intersectObject.

Ce que vous avez vu dans l'article fonctionne parfaitement pour un seul objet. Mais que se passe-t-il si votre scène en contient plusieurs ? Comment savoir sur lequel on a cliqué ?

Retournons à la vidéo de Grafikart, qui implémente cette logique.

Tutoriel JavaScript : Panorama 360°

Regardons maintenant la suite de la vidéo pour voir comment gérer les clics sur plusieurs objets et identifier la cible.

Regardez attentivement ces deux passages : De 28:23 à 32:43 : L'auteur met en place le listener onClick, normalise les coordonnées et utilise raycaster.intersectObject() pour trouver le point d'intersection sur la sphère. De 38:00 à 40:21 : C'est la partie la plus importante. Il remplace intersectObject par intersectObjects(scene.children) pour tester tous les objets. Ensuite, il parcourt le tableau des intersections pour vérifier si un objet de type Sprite a été touché, et récupère son nom. C'est la méthode standard pour gérer les clics dans une scène complexe.


3. Atelier : Scène de sélection

Il est temps de mettre en pratique. Vous trouverez ci-dessous le code d'une scène simple avec trois objets.

Votre mission :
Modifiez ce code pour implémenter la détection de clic avec Raycaster.

  1. Lorsqu'un objet est cliqué, sa couleur doit devenir jaune.
  2. Si un autre objet est cliqué, le nouvel objet devient jaune et l'ancien (celui qui était précédemment jaune) doit retrouver sa couleur d'origine. Il ne doit y avoir qu'un seul objet jaune à la fois.

Code de départ :

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

// Scène, Caméra, Renderer
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x222222);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 2, 8);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// Contrôles
const controls = new OrbitControls(camera, renderer.domElement);

// Lumière
const light = new THREE.DirectionalLight(0xffffff, 3);
light.position.set(5, 5, 5);
scene.add(light);
scene.add(new THREE.AmbientLight(0xffffff, 1));

// Objets
const sphereGeometry = new THREE.SphereGeometry(1, 32, 32);
const sphereMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.position.x = -3;
scene.add(sphere);

const boxGeometry = new THREE.BoxGeometry(1.5, 1.5, 1.5);
const boxMaterial = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const box = new THREE.Mesh(boxGeometry, boxMaterial);
scene.add(box);

const torusGeometry = new THREE.TorusGeometry(1, 0.4, 16, 100);
const torusMaterial = new THREE.MeshStandardMaterial({ color: 0x0000ff });
const torus = new THREE.Mesh(torusGeometry, torusMaterial);
torus.position.x = 3;
scene.add(torus);

// On garde en mémoire les objets interactifs
const interactiveObjects = [sphere, box, torus];

// ----- DÉBUT DE LA ZONE À MODIFIER -----

// 1. Initialisez le Raycaster et le Vector2 pour la souris.

// 2. Créez une variable pour garder en mémoire l'objet actuellement sélectionné, par exemple `selectedObject`.

// 3. Créez une fonction `onMouseClick(event)`.

// 4. Dans cette fonction, normalisez les coordonnées de la souris.

// 5. Mettez à jour le raycaster depuis la caméra.

// 6. Calculez les intersections avec `interactiveObjects`.

// 7. Ajoutez la logique :
//    - Si un objet est touché (`intersects.length > 0`):
//      a. Si un `selectedObject` existait déjà, restaurez sa couleur d'origine. (Vous pouvez la stocker temporairement).
//      b. Mettez à jour `selectedObject` avec le nouvel objet cliqué (`intersects[0].object`).
//      c. Stockez sa couleur d'origine et changez sa couleur en jaune.
//    - Si on clique dans le vide (`intersects.length === 0`):
//      a. Si un `selectedObject` existait, restaurez sa couleur et réinitialisez la variable `selectedObject`.

// 8. Ajoutez l'écouteur d'événement sur la fenêtre.


// ----- FIN DE LA ZONE À MODIFIER -----

// Boucle de rendu
function animate() {
    requestAnimationFrame(animate);
    controls.update();
    renderer.render(scene, camera);
}

animate();

Cliquez ici pour voir une solution possible
// ----- SOLUTION -----

// 1. Initialisez le Raycaster et le Vector2 pour la souris.
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

// 2. Créez une variable pour garder en mémoire l'objet actuellement sélectionné.
let selectedObject = null;

// 3. Créez une fonction `onMouseClick(event)`.
function onMouseClick(event) {
    // 4. Normalisez les coordonnées de la souris.
    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

    // 5. Mettez à jour le raycaster depuis la caméra.
    raycaster.setFromCamera(mouse, camera);

    // 6. Calculez les intersections avec `interactiveObjects`.
    const intersects = raycaster.intersectObjects(interactiveObjects);

    // 7. Logique de sélection/désélection
    if (intersects.length > 0) {
        const clickedObject = intersects[0].object;

        // Si on clique sur un objet déjà sélectionné, on ne fait rien.
        if (selectedObject === clickedObject) {
            return;
        }

        // Si un autre objet était sélectionné, on restaure sa couleur.
        if (selectedObject) {
            selectedObject.material.color.set(selectedObject.userData.originalColor);
        }

        // On met à jour l'objet sélectionné
        selectedObject = clickedObject;
        
        // On stocke sa couleur d'origine dans `userData` (un espace de stockage pratique)
        selectedObject.userData.originalColor = selectedObject.material.color.clone();
        
        // On applique la couleur de sélection
        selectedObject.material.color.set(0xffff00); // Jaune

    } else {
        // Si on clique dans le vide et qu'un objet est sélectionné, on le désélectionne.
        if (selectedObject) {
            selectedObject.material.color.set(selectedObject.userData.originalColor);
            selectedObject = null;
        }
    }
}

// 8. Ajoutez l'écouteur d'événement sur la fenêtre.
window.addEventListener('click', onMouseClick);


// ----- FIN DE LA SOLUTION -----

Conclusion

Félicitations ! Vous maîtrisez maintenant l'une des briques les plus importantes de l'interactivité en 3D. Le Raycaster est un outil puissant que vous retrouverez dans tous les moteurs 3D.

Les points clés à retenir :

  • Le Raycaster est l'outil qui permet de traduire un clic 2D en une sélection d'objet 3D.
  • La normalisation des coordonnées de la souris (de pixels vers l'intervalle [-1, 1]) est une étape cruciale et obligatoire.
  • La méthode intersectObjects() renvoie un tableau des objets touchés, trié par distance, vous permettant d'identifier précisément la cible.
  • Gérer un état (comme selectedObject) est nécessaire pour créer des interactions complexes comme la sélection unique.

Aperçu de la prochaine leçon :
Nous avons passé beaucoup de temps à écrire du code de manière impérative : "crée ceci", "ajoute cela", "modifie cette propriété". C'est efficace, mais peut devenir lourd à gérer. Dans le prochain module, nous allons faire un grand saut vers le futur du développement web 3D en découvrant React Three Fiber (R3F).

Vous allez voir comment décrire une scène 3D de manière déclarative, en utilisant des composants React. L'interactivité que nous venons de coder en plusieurs étapes se résumera souvent à une simple prop onClick sur un composant. La première leçon de ce nouveau module vous montrera comment initialiser une application R3F avec le composant <Canvas>. Préparez-vous à voir votre productivité décupler