Valentin Dupas

💡 If this is the first course you read from me, please read this small thing : about my courses

ThreeJS

On va voir comment faire ceci (Espace pour changer le modèle et t pour changer l'environnement)

Pourquoi?

Parce que c'est aussi stylé de s'affranchir des interfaces 2D pour des gens qui veulent faire dans le numérique mais ça l'est aussi pour des gens qui veulent faire dans le physique de pouvoir mettre des modés en avant sur vos sites portfolios (qui plus est en AR/VR si on pousse plus loin que ce cours). Et puis une occasion de plus de regarder sous le capot de ce genre de site c'est bon pour sa culture numérique.

Back to the basics

L'essentiel de ce que l'on va voir dans un premier temps va ressembler à ça. On va voir une petite explication de concepts de bases et on verra qu'en fait c'est tout simple.

const camera = new THREE.PerspectiveCamera(70, width / height, 0.01, 10);
camera.position.z = 1;

const scene = new THREE.Scene();

const geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
const material = new THREE.MeshNormalMaterial();

const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
renderer.setAnimationLoop(animation);
document.body.appendChild(renderer.domElement);

renderer.render(scene, camera);

(si vous arrivez à deviner ce qu'il se passe vous pouvez sautez au chapitre 2 "threejs")

Tout est information

Quand on programme, tout est information, je veux dire, on parle quand même du domaine qui s'appelle "l'informatique". On peut classifier ces informations en deux catégories, les données, et les procédures. Le Quoi et le Comment. Pour la suite je vais simplement me reposer sur les termes "variables" et "fonctions" même si ce n'est pas rigoureusement correct.

Je vous invite à ouvrir le projet de base pour que vous puissiez taper du code dans le fichier "script.js" et observer ce que ca donne, à mesure que je vous l'explique.

Pour ouvrir un projet avec le live server de VSCode il suffit d'installer l'extension (Ctrl+Shift+X) "live server" puis d'aller dans "File" > "open folder..." de sélectionner le dossier du projet et d'appuyer sur le bouton "Go Live" tout en bas à droite de la fenêtre de VSCode.

Les fonctions

Les fonctions représentent le "Comment", comment on fait les choses, pour utiliser une fonction il suffit de spécifier son nom suivi par une paire de parenthèses. C'est parce qu'on le fait via son nom que c'est interchangeable de dire que l'on "appelle", ou "invoque" la fonction lorsqu'on l'utilise.

Vous pouvez essayer ceci par exemple :

alert("hello");

Essayez de changer le texte qui est entre les parenthèses. On est d'accord que ça change ce que le navigateur vous affiche?

Ce que vous mettez entre parenthèses sont des valeurs que la fonction a besoin pour faire son job. Ici notre fonction alert() sert à afficher du texte dans une boîte d'alerte, il va bien falloir lui fournir le texte en question.

Comment savoir quoi fournir? En regardant sa documentation. Vous verrez que dans le chapitre "parameters" de cette page on vous décrit ce que la fonction prend et dans quel ordre, d'ailleurs on voit que le paramètre "message" est optionnel et donc on pourrait ne rien mettre entre parenthèses. Je ne suis pas certain de l'intérêt mais on peut 🤷‍♂.

Comment savoir où chercher? Pour ce qui nous interesse, soit sur MDN , soit sur la documentation de threejs. Ici vu que c'est une alerte à lancer par le navigateur sans aucune question de 3D c'est logique que ce soit chez MDN mais vous verrez quand on va attaquer threejs, c'est encore plus simple que ca. Si ça commence par THREE. je vous laisse deviner où regarder.

Mais donc tout bêtement,

"Un magicien est aussi compétent qu'il a mis de sort dans son grimoire."

nan, je veux dire, "Un technicien est aussi efficace qu'il a d'outils à sa ceinture."

hhm, presque. "Quelqu'un qui programme est aussi capable qu'il connaît de fonctions."

L'important c'est d'y aller mollo et de chercher à s'approprier un truc à la fois, là typiquement, on arrive à se servir de la fonction alert(), on sait comment l'écrire, et on peut imaginer ce qu'elle va faire sans se tromper, elle fait maintenant partie de ce que vous savez faire. Quand on apprend ce genre de chose c'est l'essentiel, cherchez à limiter le nombre de fonctions que vous ne connaissez pas, exécutez le code avec différentes valeurs, lisez la doc et en principe ça devrait venir, et si ce n'est pas le cas c'est cool, on est dans une école, profitez en, posez des questions. (Pro-tip: si vous arrivez à poser des questions fermées c'est que vous êtes en train de faire un super job.)

Le code s'exécute ligne par ligne de haut en bas à chaque fois que l'on recharge la page, essayez ça pour voir

alert("3");
alert("2");
alert("1");
alert("GO!");

Le seul truc un peu déroutant avec ce que je viens de vous donner est que sous certaines conditions le code ne s'exécute pas. Comme dans ce cas-ci, par exemple

function countdown() {
  alert("3");
  alert("2");
  alert("1");
  alert("GO!");
}

Vous l'avez essayé?

Eh bien en fait je vous ai menti, il se passe quelque chose, mais rien de visible. Ce code permet de créer une fonction qu'on peut utiliser par la suite, cette fonction s'appelle countdown, parce qu'il n'y a rien entre ses parenthèse c'est qu'elle ne prend rien et le code qu'elle fait lorsqu'on l'appelle est entre ses accolades {}.

Donc si j'essaye le code suivant, on peut voir que ma fonction s'exécute 3 fois.

function countdown() {
  alert("3");
  alert("2");
  alert("1");
  alert("GO!");
}

countdown();
countdown();
countdown();

Ce qui est plutôt utile pour pouvoir répéter une procédure un certain nombre de fois sans avoir à la réécrire à chaque fois, et ça nous donne aussi qu'un seul endroit dans notre projet à toucher si on veut mettre à jour cette procédure. Les gens qui ont déjà créé des composants sur figma sauront à quel point c'est sympa.

Les variable

Une variable permet beaucoup de choses, notamment de garder une information en mémoire et de la faire évoluer.

let count = 5;
count = 12;
count = 42;

Dans ce code, j'ai créé une variable du nom de count en lui donnant une valeur initiale de 5. Puis je lui ai donné la valeur 12 pour ensuite lui donner la valeur 42. Le symbole = est un ordre ici. Quand j'écris count = 12; ce que ça communique c'est qu'à partir de cette ligne count vaudra 12.

alert() quand on fournit une valeur à alert() on est pas obligé de lui donner une valeur directement, on peut lui donner une variable et ce sera la valeur qu'a la variable au moment d'exécuter la fonction qui sera utilisée.

let count = 3;

alert(count);

Vous remarquerez que ce coup-ci il n'y a pas de guillemets dans le alert() parce que pour une fois on ne lui donne pas du texte en tant que valeur mais un nom de variable.

Mais si on met bout à bout le fait que notre variable peut changer on peut commencer à faire de la logique.

let count = 3;

alert(count);
count = count - 1;

alert(count);
count = count - 1;

alert(count);
count = count - 1;

Vous savez ce qui rendrait tout ça instantanément plus intéressant? Vraiment données.

Ce qu'une fonction peut faire lorsqu'on l'appelle, c'est aussi de nous fournir une valeur en retour.

prompt("What is your name?");

Si vous exécutez ce code vous verrez une nouvelle boîte de dialogue un peu différente. Ce qu'il se passe est qu'une fois exécutée l'ordinateur remplace cette fonction par la valeur que vous avez donnée avant de reprendre l'exécution du code.

Mais du coup on s'en sert pas, pas plus que si j'exécute ce code.

"just a string, by itself";

Par contre ce que je peux faire, c'est de créer une variable et de mettre le résultat de prompt() dedans, puis je peux me servir de cette variable.

let name = prompt("what is your name?");
alert("hello");
alert(name);
alert("how are you?");

et en sachant qu'avec le symbole + on peut coller deux bout de textes ensemble on peut faire un peu mieux.

let name = prompt("what is your name?");
alert("hello " + name + ", how are you?");

Exceptionnellement je vais vous demander de survoler le bout de code suivant et vous expliquer l'intérêt après. Aussi, accessoirement, c'est le dernier truc à voir avant d'attaquer threejs.

class Person {
  constructor(name, age, height) {
    this.name = name;
    this.age = age;
    this.height = height;
  }

  sayHello() {
    alert("Hello, I am " + this.name + " and I am " + this.age + " years old");
  }
}

let a = new Person("Rigoberto", 14, 163);
let b = new Person("Gabriela", 23, 177);

b.sayHello();
a.sayHello();

alert(a.height);

a.height = 174;

alert(a.height);

Tout ce qui est class Person{...} est essentiel, mais tout ce que je veux que vous compreniez est que dans ce code une Person contient 3 valeurs et une fonction.

L'intérêt est que je peux créer des personnes avec le mot clef new. Ce qui me permet de stocker tout un compte utilisateur dans une variable. Pour accéder à une valeur ou une fonction il me suffit de mettre un point . comme ceci a.height ce qui est équivalent à dire la height de a.

Ce qui explique que b.sayHello() et a.sayHello() sont toutes les deux la fonction sayHello() mais dans le premier cas c'est celle de b avec les infos de b et dans le deuxième cas c'est celle de a avec les infos de a ce qui explique leur comportement à la fois similaire (fais la meme chose) mais diffèrent (pas avec les même données).

Eh bien c'est pareil pour le gros bout de code du début, si je vous écris ...

const scene = new THREE.Scene();
const geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
const material = new THREE.MeshNormalMaterial();
const mesh = new THREE.Mesh(geometry, material);

scene.add(mesh);

... ça devrait être beaucoup plus lisible maintenant.

// on creer une Scene et on la stocke dans une varibable appellee scene
const scene = new THREE.Scene();
// on creer une BoxGeometry et on la stocke dans une varibable appellee geometry
const geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
// on creer un MeshNormalMaterial et on la stocke dans une varibable appellee material
const material = new THREE.MeshNormalMaterial();
// on creer un Mesh grace a geometry et material et on la stocke dans une variable appellee mesh
const mesh = new THREE.Mesh(geometry, material);

// on utilise la fonction add() de notre Scene pour ajouter le mesh
scene.add(mesh);

En soit tout ça nous créer une scène, des points de la forme d'une une boîte, une matière, une boîte à partir de la matière étendue sur les points et qui ajoute cette boîte dans notre scène.

Ne vous inquiétez pas si les concepts de boîte, scène, matière sont flou, c'est toute la partie croustillante de ce cours.

Pour passer au chapitre suivant il était important que vous compreniez que...

const scene = new THREE.Scene();

créer une scène

const geometry = new THREE.BoxGeometry();

créer une "box geometry", peut importe ce que c'est.

etc... etc...

Si vous êtes vraiment en "super galère"™️ j'ai un cours d'intro à javascript qui reprend tout ça un peu plus calmement. /courses/JS/intro.html

Mais honnêtement je suis sûr que vous êtes plus capable de sauter sur threejs que vous ne le croyez.

Threejs

Créer un élément Threejs sur sa page web

// on importe toutes les fonctionnalitee de threejs
import * as THREE from "three";

// creation d'une fenetre de dessin
const renderer = new THREE.WebGLRenderer();

// utilisation de la fonction de sa fonction setSize
// pour lui faire prendre la taille de la fenetre
renderer.setSize(window.innerWidth, window.innerHeight);

// ajout de l'espace de dessin sur la page web
document.body.appendChild(renderer.domElement);

Ajouter les essentiels pour faire notre premier rendu 3D

import * as THREE from "three";

const renderer = new THREE.WebGLRenderer();

// on creer une scene
const scene = new THREE.Scene();
// on creen une camera qui prend en compte la perspective (plutot qu'une camera orthographique)
// avec 75 degrees de FOV, le meme aspect-ratio que notre page web,
// et faisant le rendu de tous les objets entre 0.1 et 1000 unitees de distance
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);

// parce qu'on va vouloir regarder des objets en position (0,0,0) on va se reculer un peu
camera.position.z = 5;

renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// on demande a l'espace de rendu threejs de nous afficher la scene
// depuis le point de vue de la camera
renderer.render(scene, camera);

Ajouter un truc à regarder

import * as THREE from "three";

const renderer = new THREE.WebGLRenderer();

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);

camera.position.z = 5;

// on creer un objet de forme cubique de 1 de long, 1 de large et 1 de profondeur
const geometry = new THREE.BoxGeometry(1, 1, 1);
// on creer un materiau de couleur verte
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
// on creer un object 3D qui est l'application du materiau vert sur la forme de cube
const cube = new THREE.Mesh(geometry, material);
// on ajoute cet objet 3D a la scene
scene.add(cube);

renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

renderer.render(scene, camera);

Ajouter un peu de mouvement

import * as THREE from "three";

const renderer = new THREE.WebGLRenderer();

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);

camera.position.z = 5;

const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// on creer une fonction qui met a jour notre scene
function update() {
  // on change un peu la rotation de notre cube
  cube.rotation.x = cube.rotation.x + 0.01;
  cube.rotation.y = cube.rotation.y + 0.01;

  // on oublie pas de redessiner un rendu sur notre page
  // sinon on verra pas les changements
  renderer.render(scene, camera);

  // requestAnimationFrame permet de demander au navigateur d'executer une fonction
  // aussi vite que la page le peu sans pour autant ralentir les interaction de l'utilisateur
  // donc on se remet un tour de la fonction update
  // creeant une boucle continue de mise a jour -> rendu -> mise a jour -> rendu etc...
  requestAnimationFrame(update);
}

// on lance la premiere mise a jour de notre scene
// qui lancera toute les autres
update();

Challenge : utilisez le MeshToonMaterial plutôt que le MeshBasicMaterial (indice: il va vous falloir une lumière pour voir la différence)

https://threejs.org/docs/index.html?q=toon#api/en/materials/MeshToonMaterial

Je vous recommande une HemisphereLight: https://threejs.org/docs/index.html?q=hemi#api/en/lights/HemisphereLight

Ajouter le plugin pour le contrôle de la caméra

import * as THREE from "three";
// si vous regarder index.html vous verrez une importmap
// grace a elle on peut juste ecrire ca pour importer l'addon dans notre projet
// sa documentation etant ici : https://threejs.org/docs/index.html?q=orbit#examples/en/controls/OrbitControls
import { OrbitControls } from "three/addons/controls/OrbitControls.js";

const renderer = new THREE.WebGLRenderer();

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);

camera.position.z = 5;

// on creer un objet qui contiendra les parametre de notre controleur
// evidemment, il est pour notre seule camera et notre espace de dessi
const controls = new OrbitControls(camera, renderer.domElement);
// synchronise le controleur et la camera, si on se penche sur la doc
// on voit qu'il ne faudra pas oublier d'appeler cette fonction si on bouge la camera
// mais vu qu'on va lui laisser controle dessus, on le fait une fois et on y pense plus
controls.update();

const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

function update() {
  cube.rotation.x = cube.rotation.x + 0.01;
  cube.rotation.y = cube.rotation.y + 0.01;

  renderer.render(scene, camera);

  requestAnimationFrame(update);
}

update();

Comment réagir aux touches du clavier

import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";

const renderer = new THREE.WebGLRenderer();

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);

camera.position.z = 5;

const controls = new OrbitControls(camera, renderer.domElement);
controls.update();

const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

function update() {
  // on va arreter de tourner automatiquement

  renderer.render(scene, camera);

  requestAnimationFrame(update);
}

function handleKeyDown(event) {
  // si on appuie sur la touche espace...
  if (event.key == " ") {
    // ... on augmente la rotation sur l'axe x et y
    cube.rotation.x = cube.rotation.x + 0.1;
    cube.rotation.y = cube.rotation.y + 0.1;
  }
}

// plus d'infos ici : https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event
// le navigateur va executer en boucle la fonction fournie tant que l'on reste appuyer sur une touche
// ici cette fonction s'appelle "handleKeyDown"
window.addEventListener("keydown", handleKeyDown);

update();

Challenge : en utilisant "keypress" plutôt que "keydown" est-ce que vous pouvez changer la couleur du cube?

Importer un environement HDR

lien pour télécharger l'environnement

import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
// import d'un autre addon, celui ci nous permet de charger des fichiers au format HDR
import { RGBELoader } from "three/addons/loaders/RGBELoader.js";

const renderer = new THREE.WebGLRenderer();

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);

camera.position.z = 5;

const controls = new OrbitControls(camera, renderer.domElement);
controls.update();

const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

function update() {
  renderer.render(scene, camera);

  requestAnimationFrame(update);
}

function handleKeyDown(event) {
  if (event.key == " ") {
    cube.rotation.x = cube.rotation.x + 0.1;
    cube.rotation.y = cube.rotation.y + 0.1;
  }
}

function onSkyboxReady(texture) {
  // quand on viens de charger une texture d'environement
  // aussi dite "skybox"

  // on definit que ce n'est pas une texture ordinaire mais une skybox
  texture.mapping = THREE.EquirectangularReflectionMapping;

  // puis on la fournit a la scene en tant qu'image de background
  scene.background = texture;
  // mais aussi d'image de reference pour la lumiere ambiante
  scene.environment = texture;
}

// cette paire de lignes charge la texture "royal_esplanade_1k.hdr"
// en partant du principe qu'elle est dans le meme dossier que script.js
// puis appelle la fonction "onSkyboxReady" en lui donnant la texture chargee
let loader = new RGBELoader();
loader.load("royal_esplanade_1k.hdr", onSkyboxReady);

window.addEventListener("keydown", handleKeyDown);

update();

Importer un modèle gltf/glb

lien pour telecharger le casque lien pour telecharger le flamand rose lien pour telecharger le robot

import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { RGBELoader } from "three/addons/loaders/RGBELoader.js";
// on importe un nouveau loader pour prendre en charge un nouveau format de fichier
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";

const renderer = new THREE.WebGLRenderer();

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);

camera.position.z = 5;

const controls = new OrbitControls(camera, renderer.domElement);
controls.update();

const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

function update() {
  renderer.render(scene, camera);

  requestAnimationFrame(update);
}

function handleKeyDown(event) {
  if (event.key == " ") {
    cube.rotation.x = cube.rotation.x + 0.1;
    cube.rotation.y = cube.rotation.y + 0.1;
  }
}

function onSkyboxReady(texture) {
  texture.mapping = THREE.EquirectangularReflectionMapping;

  scene.background = texture;
  scene.environment = texture;
}

let loader = new RGBELoader();
loader.load("royal_esplanade_1k.hdr", onSkyboxReady);

function onGltfModelReady(gltfContent) {
  // lorsqu'on a un gltf charge...

  // on recupere ce qui nous interesse a l'interieur
  // mais en regle generale c'est toute la scene qui est a l'interieur
  // on pourrait ne recuperer que les textures, les lumieres, etc... mais bon...
  const model = gltfContent.scene;

  // dans le cas particulier de notre modele de flammand, il est un peu gros
  // donc on va le reduire
  model.scale.set(0.02, 0.02, 0.02);

  // puis on ajoute tout ca a notre scene
  scene.add(model);
}

// cette paire de lignes charge le modele "Flamingo.glb"
// en partant du principe qu'il est dans le meme dossier que script.js
// puis appelle la fonction "onGltfModelReady" en lui donnant le contenu du fichier charge
let gltfLoader = new GLTFLoader();
gltfLoader.load("Flamingo.glb", onGltfModelReady);

window.addEventListener("keydown", handleKeyDown);

update();

cacher/afficher le modèle gltf/glb

import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { RGBELoader } from "three/addons/loaders/RGBELoader.js";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";

const renderer = new THREE.WebGLRenderer();

// on se creer une variable tout en haut pour pouvoir s'en servir partout
let mainModel = null;

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);

camera.position.z = 5;

const controls = new OrbitControls(camera, renderer.domElement);
controls.update();

const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

function update() {
  renderer.render(scene, camera);

  requestAnimationFrame(update);
}

function handleKeyDown(event) {
  if (event.key == " ") {
    // on arrete de faire tourner le cube quand on appuie sur espace
    // mais a la place ...

    if (mainModel) {
      // ... si le mainModel existe (et donc c'est pas null)
      if (mainModel.visible) {
        // ... si il est visible
        mainModel.visible = false; // alors il ne l'est plus
      } else {
        // sinon...
        mainModel.visible = true; // il le deviens
      }
    }
  }
}

function onSkyboxReady(texture) {
  texture.mapping = THREE.EquirectangularReflectionMapping;

  scene.background = texture;
  scene.environment = texture;
}

let loader = new RGBELoader();
loader.load("royal_esplanade_1k.hdr", onSkyboxReady);

function onGltfModelReady(gltfContent) {
  // ici on remarquera qu'on a change pour mainModel
  // donc il passera de null a une vraie valeur une fois charge
  mainModel = gltfContent.scene;

  mainModel.scale.set(0.02, 0.02, 0.02);

  scene.add(mainModel);
}

let gltfLoader = new GLTFLoader();
gltfLoader.load("Flamingo.glb", onGltfModelReady);

window.addEventListener("keydown", handleKeyDown);

update();

Challenge : essayez de reproduire la démo montrée au début du document