WebGL permet d'exploiter le GPU depuis un navigateur, mais son utilisation directe demande beaucoup de code : création des buffers, compilation des shaders, gestion des matrices et configuration du pipeline graphique. Three.js fournit une couche d'abstraction qui prend en charge une grande partie de ce travail.
L'objectif de cet article est de créer une première scène 3D animée tout en comprenant les principaux concepts de Three.js. Il prolonge naturellement l'article WebGL pour débutants, qui présente le fonctionnement du pipeline graphique et l'affichage d'un premier triangle avec WebGL natif.
Pourquoi utiliser Three.js ?
Three.js est une bibliothèque JavaScript dédiée au rendu 3D dans le navigateur. Elle utilise généralement WebGL en arrière-plan, mais expose une API plus accessible pour manipuler des objets, des caméras, des lumières, des matériaux et des textures.
Avec WebGL natif, afficher un cube éclairé implique notamment de préparer ses sommets, ses indices et ses normales, d'écrire des shaders GLSL, de transmettre des matrices au GPU et de configurer plusieurs états de rendu. Avec Three.js, ces opérations sont regroupées dans des classes cohérentes.
Three.js ne remplace toutefois pas les notions fondamentales de la 3D. Il reste nécessaire de comprendre ce que sont une scène, une caméra, une géométrie, un matériau et une boucle de rendu.
Installer Three.js avec Vite
Pour commencer dans un environnement moderne, créons un projet JavaScript avec Vite :
npm create vite@latest premiere-scene-threejs -- --template vanilla
cd premiere-scene-threejs
npm install
npm install three
npm run dev
Le fichier HTML doit contenir un élément qui accueillera le canvas de rendu :
<main>
<canvas class="webgl" aria-label="Animation 3D décorative"></canvas>
</main>
<script type="module" src="/src/main.js"></script>
Le canvas est l'élément dans lequel Three.js dessinera chaque image. Pour une scène purement décorative, une alternative textuelle n'est généralement pas nécessaire. Pour une visualisation qui transmet une information, il faut également proposer une représentation accessible des données.
Ajoutons quelques styles afin que le canvas occupe toute la fenêtre :
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
min-height: 100%;
overflow: hidden;
}
.webgl {
display: block;
width: 100vw;
height: 100vh;
}
Les quatre éléments d'un premier rendu
Une application Three.js minimale repose sur quatre éléments principaux :
- une scène qui contient les objets ;
- une caméra qui définit le point de vue ;
- un renderer qui dessine la scène dans le canvas ;
- une géométrie associée à un matériau pour former un objet visible.
Importons Three.js et récupérons le canvas :
import * as THREE from 'three'
const canvas = document.querySelector('.webgl')
if (!canvas) {
throw new Error('Canvas .webgl introuvable')
}
L'import avec * as THREE donne accès aux classes de la bibliothèque dans l'espace de noms THREE.
Créer la scène et un premier objet
La scène joue le rôle de conteneur principal. Les objets qui doivent être rendus doivent y être ajoutés :
const scene = new THREE.Scene()
scene.background = new THREE.Color('#111827')
Créons ensuite un cube. Dans Three.js, un objet visible est souvent un Mesh, c'est-à-dire l'association d'une géométrie et d'un matériau.
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshStandardMaterial({
color: '#38bdf8',
roughness: 0.45,
metalness: 0.1
})
const cube = new THREE.Mesh(geometry, material)
scene.add(cube)
BoxGeometry génère les sommets, les faces, les coordonnées de texture et les normales nécessaires au cube. MeshStandardMaterial simule un matériau réagissant à la lumière selon un modèle physiquement cohérent.
Ces abstractions évitent de créer manuellement les buffers et les shaders nécessaires, mais les données finissent toujours par être envoyées au GPU.
Placer une caméra
Une scène 3D ne peut pas être rendue sans caméra. Utilisons une caméra en perspective, adaptée à la majorité des scènes réalistes :
const sizes = {
width: window.innerWidth,
height: window.innerHeight
}
const camera = new THREE.PerspectiveCamera(
60,
sizes.width / sizes.height,
0.1,
100
)
camera.position.set(2, 1.5, 3)
camera.lookAt(0, 0, 0)
scene.add(camera)
Les quatre paramètres du constructeur correspondent au champ de vision vertical, au ratio largeur-hauteur, au plan de coupe proche et au plan de coupe éloigné.
Les objets situés avant 0.1 unité ou après 100 unités ne seront pas affichés. Éviter un intervalle inutilement grand améliore la précision du tampon de profondeur et limite certains artefacts visuels.
Ajouter des lumières
Le matériau MeshStandardMaterial a besoin de lumière. Ajoutons une lumière ambiante faible et une lumière directionnelle plus marquée :
const ambientLight = new THREE.AmbientLight('#ffffff', 0.6)
scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight('#ffffff', 2)
directionalLight.position.set(3, 4, 2)
scene.add(directionalLight)
La lumière ambiante éclaire toutes les faces de manière uniforme. La lumière directionnelle possède une orientation et permet de mieux percevoir les volumes.
Une scène uniquement éclairée par une lumière ambiante paraît souvent plate. À l'inverse, une scène sans lumière indirecte peut produire des zones entièrement noires. Leur combinaison constitue une base simple pour débuter.
Configurer le renderer
Le renderer relie Three.js au canvas et déclenche le rendu WebGL :
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: true
})
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
L'option antialias réduit l'effet d'escalier sur les contours. La limitation du pixel ratio à 2 évite de rendre un nombre excessif de pixels sur certains écrans à forte densité, ce qui pourrait réduire les performances.
Nous pouvons maintenant afficher une image :
renderer.render(scene, camera)
Cette instruction demande au renderer de dessiner la scène depuis le point de vue de la caméra.
Créer une boucle d'animation
Pour faire tourner le cube, il faut mettre à jour ses propriétés avant chaque nouvelle image. requestAnimationFrame synchronise la boucle avec le rythme de rafraîchissement du navigateur.
const clock = new THREE.Clock()
function animate() {
const elapsedTime = clock.getElapsedTime()
cube.rotation.x = elapsedTime * 0.4
cube.rotation.y = elapsedTime * 0.7
renderer.render(scene, camera)
requestAnimationFrame(animate)
}
animate()
L'utilisation du temps écoulé rend la vitesse indépendante du nombre d'images calculées par seconde. Incrémenter directement la rotation avec une valeur fixe à chaque image ferait tourner le cube plus vite sur un écran à fréquence élevée.
Three.js utilise les radians pour les rotations. Un tour complet correspond à Math.PI * 2.
Adapter la scène au redimensionnement
Lorsque la fenêtre change de taille, le ratio de la caméra et la résolution du renderer doivent être recalculés :
window.addEventListener('resize', () => {
sizes.width = window.innerWidth
sizes.height = window.innerHeight
camera.aspect = sizes.width / sizes.height
camera.updateProjectionMatrix()
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})
Modifier camera.aspect ne suffit pas. L'appel à updateProjectionMatrix() indique à Three.js de recalculer la matrice utilisée pour projeter la scène 3D sur l'écran 2D.
Pour des canvas intégrés dans des composants dont la taille ne dépend pas directement de la fenêtre, ResizeObserver constitue une solution plus adaptée. L'article ResizeObserver en JavaScript présente cette API plus en détail.
Comprendre la structure de la scène
Three.js organise les éléments sous forme de graphe de scène. Chaque objet possède notamment une position, une rotation et une échelle :
cube.position.set(0, 0.25, 0)
cube.rotation.set(0.2, 0.4, 0)
cube.scale.set(1, 1.5, 1)
Ces transformations sont exprimées par rapport au parent de l'objet. Il est donc possible de regrouper plusieurs éléments dans un Group et de déplacer l'ensemble :
const group = new THREE.Group()
scene.add(group)
group.add(cube)
group.position.x = 1
En interne, Three.js transforme ces valeurs en matrices. Cette logique correspond aux transformations géométriques que l'on devrait gérer manuellement avec WebGL.
Libérer les ressources inutilisées
La mémoire graphique n'est pas automatiquement libérée dès qu'un objet est retiré de la scène. Dans une application qui crée et détruit régulièrement des contenus, il faut nettoyer les ressources associées :
scene.remove(cube)
geometry.dispose()
material.dispose()
renderer.dispose()
Cette étape devient importante pour les configurateurs, jeux, datavisualisations ou applications monopages dans lesquelles plusieurs scènes se succèdent.
Ce que Three.js simplifie réellement
Notre première scène contient peu de code, mais Three.js effectue de nombreuses opérations : création des programmes WebGL, transfert des sommets, calcul des matrices, gestion des lumières, tri des objets et exécution des commandes de rendu.
La bibliothèque ne supprime donc pas WebGL. Elle fournit un modèle de programmation plus productif au-dessus de son pipeline. Comprendre les bases de WebGL reste utile pour diagnostiquer un problème de performance, écrire des shaders personnalisés ou concevoir des effets avancés.
À partir de cette scène minimale, les prochaines étapes consistent à ajouter des contrôles de caméra, charger des modèles 3D, appliquer des textures, créer des ombres et découvrir les shaders. La structure fondamentale restera la même : préparer la scène, mettre à jour son état, puis demander au renderer de produire une nouvelle image.