Trois flèches au tableau
Avant d'écrire du code pour vivre, j'ai enseigné les maths et la physique. Chaque année, le premier cours de mécanique commençait pareil : je dessinais trois flèches au tableau, je les nommais x, y et z, et je disais « tant qu'on n'est pas d'accord sur ces trois flèches, personne ne peut dire où est quoi ».
Des années plus tard, j'ai voulu fabriquer une carte de Noël en 3D : un sapin, un chat roux qui se prend pour l'étoile tout en haut, des boules qui tombent quand on secoue l'arbre. Elle est dans le Labo 3D du site. Et la première chose que three.js m'a demandée, avant même le sapin, c'était exactement ça : trois flèches, et un point de vue.
Le premier écran que j'ai obtenu était entièrement noir. Pas d'erreur dans la console, rien. Le sapin existait bien, la caméra aussi, mais la caméra regardait ailleurs. Toute la 3D commence là : savoir où sont les choses, et d'où on les regarde.
Le repère : x à droite, y en haut, z vers toi
Une scène three.js vit dans un repère à trois axes. L'axe x va vers la droite de l'écran, l'axe y monte, et l'axe z sort de l'écran pour venir vers toi. Une position, c'est trois nombres : (2, 0, -1) veut dire « deux pas à droite, au niveau du sol, un pas vers le fond ».
C'est un repère dit « main droite ». Tends la main droite, pouce vers la droite (x), index vers le haut (y) : ton majeur, plié vers toi, montre z. Si tu te trompes de main, tous tes objets se retrouvent en miroir.
Piège classique : dans three.js, le haut est y. Mais Blender, le logiciel de modélisation que beaucoup utilisent pour fabriquer leurs modèles, met le haut sur z. Un modèle importé sans conversion arrive couché sur le dos. Les exportateurs glTF corrigent ça tout seuls, mais si ton chat dort sur le flanc sans raison, regarde d'abord les axes.
En code, placer un objet revient à écrire ces trois nombres. Un objet visible, un Mesh, c'est une forme (la géométrie) habillée d'une matière (le matériau) :
const scene = new THREE.Scene();
const cone = new THREE.Mesh(
new THREE.ConeGeometry(1.3, 1.2, 8), // rayon, hauteur, nombre de côtés
new THREE.MeshStandardMaterial({ color: 0x2f8a4e })
);
cone.position.set(-1.2, 0.7, -0.5); // x, y, z
scene.add(cone);
Rien n'apparaît encore. Une scène sans caméra, c'est un décor de théâtre sans spectateur : tout est là, mais personne ne le voit.
La caméra perspective : Thalès en action
La caméra la plus courante s'appelle PerspectiveCamera. Elle imite un œil : plus un objet est loin, plus il paraît petit. Et ce « plus petit », c'est du Thalès pur. Un objet deux fois plus loin paraît deux fois plus petit, parce que la caméra divise les coordonnées par la profondeur. C'est ce qu'on appelle la division perspective.
// fov, rapport largeur/hauteur, near, far
const camera = new THREE.PerspectiveCamera(38, innerWidth / innerHeight, 0.1, 60);
camera.position.set(0, 2.5, 7);
camera.lookAt(0, 1, 0); // vise le pied du sapin
renderer.render(scene, camera);
Cette ligne vient directement de la scène du chat étoile : un angle de 38 degrés, assez serré pour que le sapin remplisse l'image sans déformer le chat. Le premier paramètre, le fov (field of view), est l'angle d'ouverture vertical de la caméra, en degrés.
Petit fov, c'est un téléobjectif : on voit peu de chose, en gros, et la profondeur s'écrase. Grand fov, c'est un grand-angle : on voit beaucoup, les objets rapetissent et les bords se déforment. Et il existe une formule simple pour savoir combien de scène tient dans l'image.
La hauteur visible : à une distance d de la caméra, la hauteur de scène qui tient à l'écran vaut h = 2 × d × tan(fov / 2). Avec un fov de 38° et un sapin à 6 unités, on voit environ 2 × 6 × tan(19°) ≈ 4,1 unités de haut. Le sapin mesure 2,9 unités : il occupe environ 70 % de la hauteur de l'image.
Le deuxième paramètre est le rapport largeur sur hauteur de l'image. Si tu l'oublies quand la fenêtre change de taille, tout s'étire comme une photo mal recadrée. C'est pour ça qu'on met à jour camera.aspect puis qu'on appelle camera.updateProjectionMatrix() à chaque redimensionnement.
À toi de piloter la caméra
Voici un sapin et un chat simplifiés, posés sur une grille. La flèche rouge est l'axe x, la verte l'axe y, la bleue l'axe z. Joue avec les curseurs, et essaie surtout ces trois expériences : monte le fov à 110°, puis recule la caméra jusqu'à retrouver la même taille de sapin qu'avec 30°. Ensuite, baisse le plan lointain jusqu'à voir le décor se faire couper.
Ce que tu aurais dû remarquer
Avec un grand fov et une caméra proche, le sapin garde à peu près sa taille, mais la grille au sol s'étale et le chat se déforme sur les bords : c'est la perspective forcée des grands-angles. Avec un petit fov et une caméra loin, tout s'aplatit, comme sur une photo prise au téléobjectif. Et quand far descend sous la distance d'un objet, l'objet disparaît net, sans fondu : il est sorti de la boîte que voit la caméra.
near et far : la boîte de la caméra
Les deux derniers paramètres, near et far, découpent une tranche dans l'espace. La caméra ne voit que ce qui se trouve entre ces deux distances. Ce volume a la forme d'une pyramide tronquée, qu'on appelle le frustum. Ce qui est plus près que near ou plus loin que far n'est tout simplement pas dessiné.
Pourquoi ne pas mettre near à presque zéro et far à l'infini, pour tout voir ? Parce que la carte graphique range la profondeur de chaque pixel dans un nombre de précision limitée, et que cette précision se concentre près de near. Plus l'écart entre les deux est énorme, plus deux surfaces proches finissent par avoir la même profondeur « arrondie ».
Le z-fighting : avec near: 0.00001 et far: 100000, deux surfaces presque collées (un tapis posé sur un plancher, par exemple) clignotent l'une à travers l'autre quand la caméra bouge. Ce scintillement en zébrures n'est pas un bug de ton code : c'est la précision de profondeur qui craque.
Règle d'or : choisis near aussi grand que possible et far aussi petit que possible pour ta scène. Une pièce de quelques mètres n'a pas besoin de plus que 0.1 et 60, exactement les valeurs du chat étoile.
Astuce de débogage : devant un écran noir, ajoute scene.add(new THREE.AxesHelper(2)). Si les trois flèches n'apparaissent pas, le problème vient de la caméra (position, cible ou near/far), pas de tes objets.
Three arrows on the board
Before I wrote code for a living, I taught maths and physics. Every year, the first mechanics class started the same way: I drew three arrows on the board, named them x, y and z, and said "until we agree on these three arrows, nobody can say where anything is".
Years later, I wanted to build a 3D Christmas card: a tree, a ginger cat who thinks he is the star at the very top, baubles that fall when you shake the tree. It lives in the site's 3D Lab. And the first thing three.js asked me for, before the tree even, was exactly that: three arrows, and a point of view.
The first screen I got was completely black. No error in the console, nothing. The tree existed, so did the camera, but the camera was looking elsewhere. All of 3D starts there: knowing where things are, and from where you look at them.
The frame: x to the right, y up, z towards you
A three.js scene lives in a frame with three axes. The x axis goes to the right of the screen, the y axis goes up, and the z axis comes out of the screen towards you. A position is three numbers: (2, 0, -1) means "two steps right, at floor level, one step towards the back".
This is called a "right-handed" frame. Hold out your right hand, thumb to the right (x), index finger up (y): your middle finger, bent towards you, shows z. Use the wrong hand and every object ends up mirrored.
Classic trap: in three.js, up is y. But Blender, the modelling tool many people use to build their models, puts up on z. A model imported without conversion arrives lying on its back. glTF exporters fix this on their own, but if your cat sleeps on its side for no reason, check the axes first.
In code, placing an object means writing those three numbers. A visible object, a Mesh, is a shape (the geometry) dressed in a material:
const scene = new THREE.Scene();
const cone = new THREE.Mesh(
new THREE.ConeGeometry(1.3, 1.2, 8), // radius, height, number of sides
new THREE.MeshStandardMaterial({ color: 0x2f8a4e })
);
cone.position.set(-1.2, 0.7, -0.5); // x, y, z
scene.add(cone);
Nothing shows up yet. A scene without a camera is a theatre set without an audience: everything is there, but nobody sees it.
The perspective camera: Thales at work
The most common camera is the PerspectiveCamera. It mimics an eye: the further away an object is, the smaller it looks. And that "smaller" is pure intercept theorem. An object twice as far looks half as big, because the camera divides coordinates by depth. That step is called the perspective divide.
// fov, width/height ratio, near, far
const camera = new THREE.PerspectiveCamera(38, innerWidth / innerHeight, 0.1, 60);
camera.position.set(0, 2.5, 7);
camera.lookAt(0, 1, 0); // aim at the foot of the tree
renderer.render(scene, camera);
This line comes straight from the star cat scene: a 38 degree angle, tight enough for the tree to fill the picture without distorting the cat. The first parameter, the fov (field of view), is the camera's vertical opening angle, in degrees.
A small fov is a telephoto lens: you see little, in close-up, and depth gets flattened. A large fov is a wide-angle lens: you see a lot, objects shrink and the edges distort. And there is a simple formula to know how much of the scene fits in the picture.
Visible height: at a distance d from the camera, the height of scene that fits on screen is h = 2 × d × tan(fov / 2). With a 38° fov and a tree 6 units away, you see about 2 × 6 × tan(19°) ≈ 4.1 units of height. The tree is 2.9 units tall: it takes up about 70% of the picture height.
The second parameter is the picture's width over height ratio. Forget to update it when the window changes size and everything stretches like a badly cropped photo. That is why you update camera.aspect and then call camera.updateProjectionMatrix() on every resize.
Your turn to drive the camera
Here is a simplified tree and cat, standing on a grid. The red arrow is the x axis, the green one y, the blue one z. Play with the sliders, and above all try these three experiments: raise the fov to 110°, then move the camera back until the tree is the same size as at 30°. Then lower the far plane until you see the set get cut off.
What you should have noticed
With a large fov and a close camera, the tree keeps roughly its size, but the floor grid spreads out and the cat distorts at the edges: that is the forced perspective of wide-angle lenses. With a small fov and a distant camera, everything flattens, like a telephoto shot. And when far drops below an object's distance, the object vanishes sharply, with no fade: it has left the box the camera sees.
near and far: the camera's box
The last two parameters, near and far, cut a slice out of space. The camera only sees what lies between those two distances. That volume is shaped like a truncated pyramid, called the frustum. Anything closer than near or further than far is simply not drawn.
Why not set near to almost zero and far to infinity, to see everything? Because the graphics card stores each pixel's depth in a number of limited precision, and that precision is concentrated near near. The wider the gap between the two, the more two close surfaces end up with the same "rounded" depth.
Z-fighting: with near: 0.00001 and far: 100000, two nearly touching surfaces (a rug on a wooden floor, say) flicker through each other as the camera moves. That striped shimmer is not a bug in your code: it is depth precision giving way.
Golden rule: pick near as large as possible and far as small as possible for your scene. A room a few metres wide needs no more than 0.1 and 60, exactly the star cat's values.
Debugging tip: facing a black screen, add scene.add(new THREE.AxesHelper(2)). If the three arrows do not show up, the problem is the camera (position, target or near/far), not your objects.
🎯 Pratique
S'entraîner (clique pour ouvrir) :
💬 Ré-explique sans regarder
Explique à un collègue pourquoi doubler le fov et rapprocher la caméra ne donne pas la même image, même si le sapin garde la même taille.
2 × d × tan(fov / 2). On peut garder ce rapport en jouant sur d ou sur le fov. Mais la perspective change : de près avec un grand angle, les objets proches grossissent beaucoup plus vite que les lointains, donc le fond s'éloigne et les bords se déforment. De loin avec un petit angle, la profondeur s'aplatit.🧠 Rappel libre
Sans remonter : avec un fov de 90° et un objet à 5 unités de la caméra, quelle hauteur de scène tient dans l'image ?
h = 2 × d × tan(fov / 2). Ici tan(45°) = 1, donc h = 2 × 5 × 1 = 10 unités. Un fov de 90° montre, à chaque distance, une hauteur égale au double de cette distance.⚖️ Juge le code de l'IA
Ta scène est une pièce de cinq mètres. Le tapis clignote quand la caméra bouge. L'IA propose : « Mets new THREE.PerspectiveCamera(50, aspect, 0.00001, 100000), comme ça tu es sûr de tout voir. » Tu acceptes, ou tu rejettes ?
far / near de dix milliards écrase la précision du tampon de profondeur, donc le tapis et le plancher se confondent encore plus. Pour une pièce de cinq mètres, near: 0.1 et far: 30 suffisent largement. Si le tapis clignote encore, on le décolle de quelques millimètres du sol.fov de 40° à 80° sans bouger la caméra. Que devient le sapin à l'écran ?far: 40. Un sapin se trouve à 50 unités. Que se passe-t-il ?near à 0.00001 pour être tranquille ?Une position, c'est trois nombres. Mais pour faire tourner la tête d'un chat vers ton pointeur, il faut savoir soustraire deux positions, mesurer une distance et savoir si deux directions se regardent. Leçon 2 : les vecteurs, et le produit scalaire.
Leçon 2 : Les vecteurs : position, direction, distance →