Créer de la 3D dans le navigateur de zéro : scène, modèles STL/GLTF, animation au scroll, shaders, post-processing et portfolio 3D animé complet.
WebGL est une API JavaScript qui donne accès direct au GPU pour dessiner en 2D et 3D dans un <canvas>. Sans abstraction, voici ce qu'il faut écrire pour afficher un triangle :
// ~150 lignes de WebGL brut pour un triangle
const gl = canvas.getContext('webgl')
const vertexShader = gl.createShader(gl.VERTEX_SHADER)
// ... compiler le shader GLSL
// ... créer le program
// ... lier les buffers
// ... configurer les attributs
// ... enfin : gl.drawArrays(gl.TRIANGLES, 0, 3)Three.js réduit ça à quelques lignes. Il gère le contexte WebGL, la compilation des shaders, les buffers GPU — en exposant une API orientée objet lisible.
npm install threeTypes TypeScript :
npm install -D @types/threeToute scène Three.js repose sur trois objets obligatoires.
Conteneur de tout ce qui existe dans le monde 3D. On y ajoute les objets, les lumières, les caméras.
import * as THREE from 'three'
const scene = new THREE.Scene()
scene.background = new THREE.Color('#0a0a0a')Définit le point de vue. La plus courante est PerspectiveCamera — elle simule la vision humaine avec perspective.
const camera = new THREE.PerspectiveCamera(
75, // FOV (field of view) en degrés
window.innerWidth / window.innerHeight, // aspect ratio
0.1, // near plane — rien de plus proche n'est rendu
1000 // far plane — rien de plus loin n'est rendu
)
camera.position.set(0, 0, 5) // reculer la caméra sur l'axe ZLe FOV contrôle l'angle de vision. 50-75° est naturel. Au-delà de 90° l'effet fish-eye devient prononcé.
Dessine la scène dans un <canvas> en utilisant le GPU.
const renderer = new THREE.WebGLRenderer({
antialias: true, // lissage des bords
alpha: true, // fond transparent (pour superposer sur du HTML)
})
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) // max 2x pour la perf
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
document.body.appendChild(renderer.domElement) // ajouter le canvas au DOMsetPixelRatio(Math.min(devicePixelRatio, 2)) — les écrans Retina ont un ratio de 2 ou 3. À 3, le renderer trace 9 pixels par pixel "CSS". On plafonne à 2 pour garder de bonnes performances.
Un objet Three.js = une Geometry (forme) + un Material (apparence) = un Mesh.
// Géométrie — les données de forme
const geometry = new THREE.BoxGeometry(1, 1, 1)
// Matériau — apparence visuelle
const material = new THREE.MeshStandardMaterial({
color: '#cdff4f',
roughness: 0.3,
metalness: 0.1,
})
// Mesh = geometry + material
const cube = new THREE.Mesh(geometry, material)
scene.add(cube)MeshStandardMaterial est un matériau PBR (Physically Based Rendering) — il réagit aux lumières de façon réaliste. Sans lumière dans la scène, ce matériau rend tout noir.
// Lumière directionnelle — comme le soleil
const light = new THREE.DirectionalLight('#ffffff', 2)
light.position.set(5, 5, 5)
scene.add(light)
// Lumière ambiante — éclairage de base pour éviter le noir total
const ambient = new THREE.AmbientLight('#ffffff', 0.5)
scene.add(ambient)Three.js ne re-dessine pas automatiquement. Il faut une boucle qui appelle renderer.render() à chaque frame.
function animate() {
requestAnimationFrame(animate)
// Faire tourner le cube
cube.rotation.x += 0.01
cube.rotation.y += 0.01
renderer.render(scene, camera)
}
animate()requestAnimationFrame appelle la fonction avant le prochain repaint du navigateur — typiquement 60 fois par seconde, ou selon le taux de rafraîchissement de l'écran.
Sur un écran 60hz, la rotation de 0.01 par frame = 0.6 radians/seconde. Sur un écran 120hz, ça double. La solution : utiliser le temps écoulé.
const clock = new THREE.Clock()
function animate() {
requestAnimationFrame(animate)
const delta = clock.getDelta() // secondes depuis le dernier frame
cube.rotation.y += delta * 0.5 // 0.5 radian par seconde, peu importe le fps
renderer.render(scene, camera)
}Quand la fenêtre est redimensionnée, l'aspect ratio de la caméra et la taille du renderer doivent être mis à jour.
window.addEventListener('resize', () => {
// Mettre à jour la caméra
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix() // obligatoire après changement d'aspect
// Mettre à jour le renderer
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})Sans updateProjectionMatrix(), la caméra continue d'utiliser l'ancien aspect ratio — les objets apparaissent étirés.
import * as THREE from 'three'
// Scène
const scene = new THREE.Scene()
scene.background = new THREE.Color('#111111')
// Caméra
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
)
camera.position.z = 5
// Renderer
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
document.body.appendChild(renderer.domElement)
// Lumières
scene.add(new THREE.AmbientLight('#ffffff', 0.5))
const dirLight = new THREE.DirectionalLight('#ffffff', 2)
dirLight.position.set(5, 5, 5)
scene.add(dirLight)
// Objet
const cube = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshStandardMaterial({ color: '#cdff4f' })
)
scene.add(cube)
// Resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
})
// Boucle de rendu
const clock = new THREE.Clock()
function animate() {
requestAnimationFrame(animate)
cube.rotation.y += clock.getDelta() * 0.5
renderer.render(scene, camera)
}
animate()Three.js utilise un système de coordonnées main droite :
Y
│
│
└────── X
/
ZX positif → droiteY positif → hautZ positif → vers vous (hors de l'écran)La caméra regarde vers -Z par défaut. C'est pourquoi on recule la caméra à z = 5 pour voir un objet placé à l'origine.
Pour explorer librement la scène avec la souris pendant le développement.
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true // inertie au relâchement
controls.dampingFactor = 0.05
// Dans la render loop — obligatoire avec damping
function animate() {
requestAnimationFrame(animate)
controls.update() // ← nécessaire pour le damping
renderer.render(scene, camera)
}OrbitControls n'est pas dans le core de Three.js — c'est dans three/examples/jsm/. Ces helpers sont dans le package npm mais dans un sous-dossier séparé.
La scène de base est en place. Le chapitre suivant explore les géométries, matériaux et lumières en détail — les outils pour construire des scènes visuellement riches.