Exploration de Three.js : Personnalisation et Contrôle Avancé des Scènes Immersives
Introduction
Bienvenue dans cette leçon dédiée à l'exploration approfondie de Three.js ! Dans le cadre de notre cours "Développer des Expériences Immersives : Web AR et Web VR avec A-Frame et Three.js", nous avons déjà abordé les bases de la création de scènes 3D interactives. Aujourd'hui, nous allons pousser les limites en nous concentrant sur la personnalisation et le contrôle avancé de ces scènes.
Three.js est une bibliothèque JavaScript incroyablement puissante qui nous permet de rendre des graphiques 3D directement dans le navigateur. Si A-Frame simplifie grandement le développement d'expériences Web XR en offrant un framework déclaratif, comprendre et maîtriser Three.js nous ouvre la porte à une flexibilité inégalée pour :
- Créer des géométries uniques et complexes.
- Concevoir des matériaux et des effets visuels sur mesure via des shaders.
- Implémenter des interactions utilisateur sophistiquées.
- Optimiser les performances pour des scènes riches.
L'objectif de cette leçon est de vous donner les outils nécessaires pour transcender les primitives et les comportements standards, et bâtir des expériences 3D véritablement uniques et immersives.
Rappel des Fondamentaux de Three.js
Avant de plonger dans les aspects avancés, rappelons brièvement les composants essentiels d'une scène Three.js :
Scene: Le conteneur racine pour tous vos objets 3D (modèles, lumières, caméras).Camera: Le point de vue de l'utilisateur sur la scène. Les types courants incluentPerspectiveCamera(pour un rendu réaliste) etOrthographicCamera(pour des vues isométriques ou 2D).Renderer: Le moteur qui prend la scène et la caméra, et les dessine sur un élément<canvas>du navigateur.WebGLRendererest le plus utilisé.Mesh: Représente un objet 3D. Il est composé d'uneGeometry(la forme) et d'unMaterial(l'apparence).Light: Source de lumière qui illumine la scène et affecte l'apparence des matériaux.
Ces briques de base sont la fondation sur laquelle nous allons construire nos personnalisations avancées.
Personnalisation des Objets et Matériaux
Géométries Personnalisées : Introduction à BufferGeometry
Lorsque les géométries primitives fournies par Three.js (CubeGeometry, SphereGeometry, etc.) ne suffisent plus, BufferGeometry devient votre meilleur allié. BufferGeometry est la base de toutes les géométries dans Three.js et permet de définir des formes 3D en spécifiant directement leurs sommets (vertices), normales (normals) pour l'éclairage, coordonnées UV pour le texturage, et d'autres attributs.
Pourquoi utiliser BufferGeometry ?
- Performance : Stocke les données des sommets de manière très efficace, optimisé pour le GPU. Indispensable pour les scènes complexes ou les objets avec un grand nombre de polygones.
- Flexibilité : Permet de créer n'importe quelle forme 3D imaginable, de l'objet le plus simple au plus complexe.
- Contrôle fin : Vous avez un contrôle total sur chaque aspect de la géométrie.
Création d'une BufferGeometry Simple
Pour créer une BufferGeometry, vous devez définir au minimum les positions des sommets. Les normales et les UVs sont également cruciaux pour un rendu correct.
Considérons la création d'un simple triangle :
// 1. Initialiser une BufferGeometry vide
const customGeometry = new THREE.BufferGeometry();
// 2. Définir les positions des sommets
// Chaque groupe de 3 nombres (x, y, z) représente un sommet
const vertices = new Float32Array([
-1.0, -1.0, 0.0, // Sommet 1 (x, y, z)
1.0, -1.0, 0.0, // Sommet 2
0.0, 1.0, 0.0 // Sommet 3
]);
// 3. Ajouter les positions comme un attribut à la géométrie
// 'position' est un nom d'attribut standard
// 3 indique que chaque sommet a 3 composantes (x, y, z)
customGeometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
// (Optionnel) Définir les normales pour un éclairage correct
// Pour un simple triangle plat, les normales pointent toutes dans la même direction
const normals = new Float32Array([
0.0, 0.0, 1.0, // Normale du sommet 1
0.0, 0.0, 1.0, // Normale du sommet 2
0.0, 0.0, 1.0 // Normale du sommet 3
]);
customGeometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
// (Optionnel) Définir les UVs pour le texturage
const uvs = new Float32Array([
0.0, 0.0, // UV du sommet 1
1.0, 0.0, // UV du sommet 2
0.5, 1.0 // UV du sommet 3
]);
customGeometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
// 4. Créer un matériau et un maillage
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00, side: THREE.DoubleSide });
// Pour que l'éclairage fonctionne, utilisez MeshStandardMaterial ou MeshPhongMaterial et ajoutez des lumières.
// const material = new THREE.MeshStandardMaterial({ color: 0x00ff00, side: THREE.DoubleSide });
const triangleMesh = new THREE.Mesh(customGeometry, material);
// Ajouter le maillage à la scène
// scene.add(triangleMesh);
Explication du code :
- Nous commençons par créer une instance de
BufferGeometry. - Un
Float32Arrayest utilisé pour stocker les coordonnées des sommets. Chaque triplet (x, y, z) représente un point dans l'espace 3D. customGeometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3))attache ces données à l'attributpositionde la géométrie.3indique que chaque "item" dans l'array pour cet attribut est composé de 3 valeurs.- Les normales et les UVs sont définis de manière similaire. Les normales sont des vecteurs qui indiquent la direction "extérieure" de la surface à chaque sommet, essentiel pour le calcul de l'éclairage. Les UVs sont des coordonnées 2D (u, v) utilisées pour mapper une texture sur la surface de l'objet.
- Enfin, un
Meshest créé avec cette géométrie personnalisée et un matériau.
Matériaux Avancés et Shaders (GLSL)
Les matériaux par défaut de Three.js (MeshStandardMaterial, MeshPhongMaterial, MeshBasicMaterial, etc.) couvrent un large éventail de besoins. Cependant, pour des effets visuels réellement uniques, des rendus non photoréalistes, ou une optimisation poussée, les shaders sont indispensables.
Les shaders sont de petits programmes écrits en GLSL (OpenGL Shading Language) qui s'exécutent directement sur le GPU. Ils déterminent comment chaque pixel d'un objet est rendu. Il existe deux types principaux :
Vertex Shader: Traite les informations de chaque sommet de la géométrie (position, normale, UV). Il est responsable de la transformation des coordonnées 3D de l'espace objet vers l'espace de l'écran, et peut manipuler la position des sommets (ex: ondulations, explosions).Fragment Shader: S'exécute pour chaque "fragment" (qui correspond souvent à un pixel) de la surface de l'objet. Il est responsable de la détermination de la couleur finale du pixel, en tenant compte de la lumière, des textures, des reflets, etc.
Three.js offre deux classes pour travailler avec les shaders :
ShaderMaterial: Permet de définir des shaders GLSL personnalisés. Il fournit également des mécanismes pour passer des données (uniforms, attributes) du JavaScript aux shaders.RawShaderMaterial: Une version encore plus basique qui ne fournit aucune fonctionnalité par défaut de Three.js (comme la gestion des matrices de projection/vue). Vous devez tout gérer vous-même, offrant un contrôle maximal mais une complexité accrue.
Concepts Clés des Shaders :
Uniforms: Variables globales dont la valeur est la même pour tous les sommets ou fragments d'un objet. Elles sont passées du code JavaScript au shader (ex: couleur globale, temps écoulé, position de la lumière).Attributes: Variables par sommet qui définissent les propriétés de chaque sommet (ex: position, normale, UVs). Elles sont définies dansBufferGeometry.Varyings: Variables utilisées pour passer des données duVertex ShaderauFragment Shader. Elles sont interpolées sur la surface de l'objet.
Structure d'un ShaderMaterial (Exemple Conceptuel)
Bien qu'un exemple de code GLSL complet soit trop long pour cette introduction, voici comment vous structureriez un ShaderMaterial en JavaScript et les bases du GLSL :
// Définition des uniforms (variables passées de JS au shader)
const uniforms = {
time: { value: 0.0 },
color: { value: new THREE.Color(0xff0000) }
};
// Vertex Shader (GLSL)
const vertexShader = `
uniform float time;
attribute vec3 position; // Attribut de position des sommets
// Autres attributs (normal, uv)
varying vec3 vColor; // Variable passant au fragment shader
void main() {
// Manipuler la position des sommets ici
vec3 transformedPosition = position;
// Exemple simple d'animation de la position en Y
transformedPosition.y += sin(transformedPosition.x * 5.0 + time) * 0.1;
// Calculer la position finale projetée
gl_Position = projectionMatrix * modelViewMatrix * vec4(transformedPosition, 1.0);
// Passer une couleur ou autre info au fragment shader
vColor = transformedPosition * 0.5 + 0.5; // Couleur basée sur la position
}
`;
// Fragment Shader (GLSL)
const fragmentShader = `
uniform vec3 color; // Couleur passée du JS
varying vec3 vColor; // Couleur interpolée du vertex shader
void main() {
// Calculer la couleur finale du pixel
gl_FragColor = vec4(vColor * color, 1.0); // Multiplier les couleurs
}
`;
// Création du ShaderMaterial
const customShaderMaterial = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: vertexShader,
fragmentShader: fragmentShader,
transparent: true,
wireframe: false
});
// Créer un maillage avec cette géométrie et ce matériau
// const planeGeometry = new THREE.PlaneGeometry(5, 5, 10, 10); // Un plan avec des subdivisions
// const customMesh = new THREE.Mesh(planeGeometry, customShaderMaterial);
// scene.add(customMesh);
// Dans votre boucle d'animation, mettez à jour l'uniform 'time'
// uniforms.time.value += 0.01;
Explication du code (conceptuel) :
- Le
vertexShaderreçoit lapositionde chaque sommet et peut la modifier (ici, une simple ondulation en Y basée surtime). Il calcule ensuite la position finalegl_Positionet passevColoraufragmentShader. - Le
fragmentShaderreçoit lavColorinterpolée pour chaque fragment et leuniform colorglobal. Il utilise ces informations pour déterminer lagl_FragColor, la couleur finale du pixel. - Le
ShaderMaterialest instancié avec ces shaders et les uniforms définis en JavaScript. Les uniforms sont des ponts entre votre logique JavaScript et le code GLSL.
Les shaders sont un domaine vaste et complexe, mais leur maîtrise ouvre des possibilités illimitées pour des effets visuels personnalisés qui ne seraient pas réalisables avec les matériaux standards.
Contrôle Avancé de la Scène
Au-delà de l'apparence, le contrôle des interactions et de l'animation est essentiel pour des expériences immersives.
Interactions Utilisateur : Raycasting
Le Raycasting est une technique fondamentale pour détecter les intersections entre un rayon (généralement issu de la caméra et pointant dans la direction de la souris) et les objets de la scène. C'est le mécanisme derrière la sélection d'objets ou les interactions contextuelles.
Comment fonctionne le Raycasting ?
- Définir la position de la souris : Convertir les coordonnées de l'écran (pixels) en coordonnées normalisées de l l'appareil (-1 à 1 pour X et Y).
- Créer un rayon : Utiliser un
THREE.Raycasterpour générer un rayon depuis la caméra, passant par les coordonnées de la souris dans l'espace 3D. - Tester les intersections : Le
Raycasterpeut ensuite tester ce rayon contre un ensemble d'objets de la scène pour trouver ceux qu'il intersecte. - Traiter les résultats : La méthode
intersectObjectsretourne un tableau d'objets intersectés, triés par distance.
Exemple de Raycasting pour la Sélection d'Objets
// Initialisation du Raycaster et du vecteur de la souris
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
// Un objet pour stocker l'objet actuellement survolé
let intersectedObject = null;
let previousColor = null;
// Fonction de gestionnaire d'événement pour le mouvement de la souris
function onMouseMove(event) {
// Calcul des coordonnées normalisées de la souris (-1 à +1)
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
}
// Ajouter l'écouteur d'événement
window.addEventListener('mousemove', onMouseMove, false);
// Dans votre boucle d'animation (animate() function) :
function animate() {
requestAnimationFrame(animate);
// 1. Mettre à jour le raycaster avec la caméra et les coordonnées de la souris
raycaster.setFromCamera(mouse, camera);
// 2. Calculer les intersections avec les objets de la scène
// Remplacez 'scene.children' par un tableau spécifique d'objets à tester si nécessaire.
const intersects = raycaster.intersectObjects(scene.children, true); // true pour tester les enfants des objets
if (intersects.length > 0) {
// Il y a des intersections
if (intersectedObject !== intersects[0].object) {
// Un nouvel objet est survolé
// Rétablir la couleur de l'objet précédemment survolé
if (intersectedObject) {
intersectedObject.material.color.set(previousColor);
}
// Stocker le nouvel objet et sa couleur d'origine
intersectedObject = intersects[0].object;
previousColor = intersectedObject.material.color.getHex();
// Mettre en évidence le nouvel objet
intersectedObject.material.color.set(0xff00ff); // Couleur de surbrillance
}
} else {
// Aucune intersection
if (intersectedObject) {
// Rétablir la couleur de l'objet précédemment survolé
intersectedObject.material.color.set(previousColor);
intersectedObject = null;
previousColor = null;
}
}
renderer.render(scene, camera);
}
// N'oubliez pas d'appeler animate() une première fois
// animate();
Explication du code :
THREE.RaycasteretTHREE.Vector2sont initialisés une seule fois.- La fonction
onMouseMovemet à jourmouse.xetmouse.ypour qu'ils soient dans l'intervalle[-1, 1], nécessaire pour le raycaster. - Dans la boucle d'animation,
raycaster.setFromCamera(mouse, camera)met à jour le rayon. raycaster.intersectObjects(scene.children, true)cherche les intersections.trueindique de vérifier les enfants des objets (par exemple, si vous avez un groupe d'objets).- Si des objets sont trouvés, le premier (le plus proche) est sélectionné. Son matériau est temporairement modifié pour le mettre en évidence. Si aucun objet n'est survolé, la couleur de l'objet précédemment sélectionné est restaurée.
Ce mécanisme est la pierre angulaire de nombreuses interactions utilisateur, comme les menus interactifs en VR, les sélections d'éléments en AR, ou les jeux 3D.
Gestion de l'Animation : requestAnimationFrame et Boucles Personnalisées
L'animation dans Three.js est principalement gérée par la fonction globale requestAnimationFrame(). Elle demande au navigateur d'exécuter une fonction donnée avant le prochain rafraîchissement de l'écran, assurant une animation fluide et une utilisation efficace des ressources.
Boucle d'Animation Standard
let rotationSpeed = 0.01;
// Supposons que 'myObject' est un THREE.Mesh déjà ajouté à la scène.
function animate() {
requestAnimationFrame(animate); // Demande la prochaine frame
// Mettre à jour la logique de votre scène ici
// Exemple : Rotation d'un objet
if (myObject) {
myObject.rotation.x += rotationSpeed;
myObject.rotation.y += rotationSpeed * 0.5;
}
// Rendu de la scène
renderer.render(scene, camera);
}
// Appeler la fonction une première fois pour démarrer l'animation
// animate();
Explication du code :
requestAnimationFrame(animate)est la ligne clé. Elle crée une boucle récursive où la fonctionanimateest appelée à chaque rafraîchissement d'écran.- Dans cette boucle, vous placez toute la logique qui doit être mise à jour constamment : les mouvements d'objets, les calculs de physique, la mise à jour des uniforms de shaders, etc.
renderer.render(scene, camera)est toujours la dernière étape pour dessiner la scène mise à jour.
Tweening et Animations Complexes
Pour des animations plus sophistiquées (mouvements fluides entre deux états, animations basées sur des courbes), des bibliothèques de tweening comme GSAP ou TWEEN.js sont très utiles. Elles simplifient la gestion des interpolations de valeurs au fil du temps.
Gestion des Événements et Contrôles de Caméra
- Événements Clavier/Souris : Vous pouvez attacher des écouteurs d'événements (
addEventListener) pour capturer les interactions classiques (clics, touches, défilement de la molette) et les traduire en mouvements de caméra, d'objets, ou en déclencheurs d'actions. OrbitControls: Three.js fournit des utilitaires commeTHREE.OrbitControlsqui offrent des contrôles de caméra prêts à l'emploi (rotation, zoom, panoramique) via la souris, très utiles pour la navigation de débogage ou les visualisations interactives. Pour des expériences immersives, des contrôles plus spécifiques sont souvent nécessaires.- Contrôles Personnalisés : Pour des expériences VR/AR, des contrôles personnalisés sont essentiels. Ils peuvent impliquer des manettes de contrôleur VR, des gestes de la main (pour la RA), ou des mouvements de la tête. Ces contrôles interagissent généralement avec la position et l'orientation de la caméra, ou la transformation d'objets "curseurs" pour les interactions raycasting.
Optimisation et Performance
Pour des scènes immersives complexes, l'optimisation est cruciale pour maintenir un framerate élevé et une expérience fluide.
Frustum Culling: Three.js gère cela automatiquement. Seuls les objets dont le volume de délimitation (bounding box ou bounding sphere) intersecte le frustum (le volume visible par la caméra) sont rendus.InstancedMesh: Pour afficher de très nombreux objets identiques avec des transformations différentes (position, rotation, échelle),InstancedMeshest une optimisation majeure. Au lieu de créer des milliers deMeshindividuels, vous utilisez une seuleBufferGeometryet un seulMaterial, et vous passez les transformations de chaque instance au GPU via des attributs.Level of Detail (LOD): Affiche des versions plus détaillées des objets lorsqu'ils sont proches de la caméra, et des versions simplifiées lorsqu'ils sont éloignés. Cela réduit la charge de rendu pour les objets moins visibles.- Gestion des Ressources :
- Réduire le nombre de Draw Calls : Combinez les géométries avec le même matériau pour qu'elles soient rendues en un seul appel de tirage si possible.
- Optimiser les textures : Utilisez des textures de résolution appropriée, compressez-les, utilisez des atlas de textures.
- Supprimer les objets inutiles : Retirez de la scène les objets qui ne sont plus visibles ou nécessaires.
- Réutiliser les géométries et matériaux : Évitez de créer de nouvelles instances si une ressource existante peut être réutilisée.
Intégration dans des Expériences Immersives (Web AR/VR)
Ce que nous avons exploré aujourd'hui est directement applicable au développement d'expériences Web AR et Web VR, que vous utilisiez A-Frame ou Three.js directement.
- A-Frame et Three.js : A-Frame est construit sur Three.js. Chaque entité A-Frame (
<a-entity>) a un objetTHREE.Object3Dsous-jacent accessible viaentity.object3D. Vous pouvez écrire des composants A-Frame qui manipulent directement les objets Three.js, créent desBufferGeometryou appliquent desShaderMaterialpersonnalisés. C'est le pont parfait pour combiner la facilité d'A-Frame avec la puissance brute de Three.js. - WebXR API : Les expériences Web AR/VR sont rendues possibles par l'API WebXR. Three.js fournit des adaptateurs pour cette API, permettant au
WebGLRendererde fonctionner en mode VR (stéréoscopique) ou AR (superposition sur la caméra réelle). Les techniques de personnalisation et de contrôle que nous avons vues sont essentielles pour construire des interfaces, des interactions et des visuels spécifiques à ces environnements immersifs.
En maîtrisant ces concepts avancés de Three.js, vous êtes en mesure de créer non seulement des scènes 3D statiques, mais aussi des mondes dynamiques, interactifs et visuellement riches qui peuvent servir de base à des applications Web AR/VR de pointe.
Conclusion
Nous avons fait un tour d'horizon des techniques avancées pour la personnalisation et le contrôle des scènes Three.js. De la création de géométries uniques avec BufferGeometry à la manipulation des pixels via les shaders GLSL, en passant par l'implémentation d'interactions complexes avec le Raycaster et l'optimisation des performances, vous avez désormais une boîte à outils plus robuste.
Points clés à retenir :
BufferGeometryest indispensable pour des formes 3D complexes et des performances optimales.- Les shaders GLSL (
ShaderMaterial) sont la clé des effets visuels uniques et du rendu personnalisé, offrant un contrôle sans précédent sur l'apparence. - Le
Raycasterest l'outil principal pour permettre l'interaction de l'utilisateur avec les objets 3D (clic, survol). - La boucle
requestAnimationFrameest le cœur de toute animation fluide. - L'optimisation est une étape cruciale pour des expériences immersives performantes, notamment via
InstancedMeshet la gestion des ressources.
Ces compétences sont directement transférables au développement d'expériences Web AR et Web VR, vous permettant d'aller au-delà des limites des frameworks déclaratifs pour construire des mondes virtuels et augmentés qui reflètent votre vision créative. Continuez à expérimenter et à explorer les possibilités infinies qu'offre Three.js !