Principes fondamentaux

Ceci est le premier article d'une série d'articles sur three.js. Three.js est une bibliothèque 3D qui essaie de rendre aussi facile que possible l'affichage de contenu 3D sur une page web.

Three.js est souvent confondu avec WebGL car la plupart du temps, mais pas toujours, three.js utilise WebGL pour dessiner en 3D. WebGL est un système de très bas niveau qui ne dessine que des points, des lignes et des triangles. Pour faire quoi que ce soit d'utile avec WebGL, cela nécessite généralement beaucoup de code et c'est là que three.js intervient. Il gère des choses comme les scènes, les lumières, les ombres, les matériaux, les textures, les mathématiques 3D, toutes choses que vous auriez à écrire vous-même si vous utilisiez WebGL directement.

Ces tutoriels supposent que vous connaissez déjà JavaScript et, pour la plupart, ils utiliseront le style ES6. Voir ici pour une liste concise des choses que vous êtes censé déjà connaître. La plupart des navigateurs qui supportent three.js sont mis à jour automatiquement, donc la plupart des utilisateurs devraient pouvoir exécuter ce code. Si vous souhaitez faire fonctionner ce code sur de très vieux navigateurs, penchez-vous sur un transpiler comme Babel. Bien sûr, les utilisateurs qui exécutent de très vieux navigateurs ont probablement des machines qui ne peuvent pas exécuter three.js.

Lors de l'apprentissage de la plupart des langages de programmation, la première chose que les gens font est de faire afficher "Hello World!" par l'ordinateur. Pour la 3D, l'une des premières choses les plus courantes à faire est de créer un cube 3D. Alors commençons par "Hello Cube !"

Avant de commencer, essayons de vous donner une idée de la structure d'une application three.js. Une application three.js vous demande de créer un tas d'objets et de les connecter ensemble. Voici un diagramme qui représente une petite application three.js

Points à noter concernant le diagramme ci-dessus.

  • Il y a un Renderer. C'est sans doute l'objet principal de three.js. Vous passez une Scene et une Camera à un Renderer et il rend (dessine) la partie de la scène 3D qui se trouve à l'intérieur du frustum de la caméra en tant qu'image 2D sur un canevas.

  • Il y a un graphe de scène (scenegraph) qui est une structure arborescente, composée de divers objets comme un objet Scene, plusieurs objets Mesh, des objets Light, Group, Object3D, et des objets Camera. Un objet Scene définit la racine du graphe de scène et contient des propriétés comme la couleur de fond et le brouillard. Ces objets définissent une structure arborescente hiérarchique parent/enfant et représentent où les objets apparaissent et comment ils sont orientés. Les enfants sont positionnés et orientés par rapport à leur parent. Par exemple, les roues d'une voiture pourraient être les enfants de la voiture de sorte que déplacer et orienter l'objet voiture déplace automatiquement les roues. Vous pouvez en savoir plus à ce sujet dans l'article sur les graphes de scène.

    Notez dans le diagramme que la Camera est à moitié dedans et à moitié dehors du graphe de scène. Cela représente qu'en three.js, contrairement aux autres objets, une Camera n'a pas besoin d'être dans le graphe de scène pour fonctionner. Tout comme les autres objets, une Camera, en tant qu'enfant d'un autre objet, se déplacera et s'orientera par rapport à son objet parent. Il y a un exemple de mise en place de plusieurs objets Camera dans un graphe de scène à la fin de l'article sur les graphes de scène.

  • Les objets Mesh représentent le dessin d'une Geometry spécifique avec un Material spécifique.

    Les objets Material et les objets Geometry peuvent être utilisés par plusieurs objets Mesh. Par exemple, pour dessiner deux cubes bleus à différents endroits, nous aurions besoin de deux objets Mesh pour représenter la position et l'orientation de chaque cube. Nous n'aurions besoin que d'une seule Geometry pour stocker les données de sommet d'un cube et nous n'aurions besoin que d'un seul Material pour spécifier la couleur bleue. Les deux objets Mesh pourraient référencer le même objet Geometry et le même objet Material.

  • Les objets Geometry représentent les données de sommet d'une pièce de géométrie comme une sphère, un cube, un plan, un chien, un chat, un humain, un arbre, un bâtiment, etc... Three.js fournit de nombreux types de primitives de géométrie intégrées. Vous pouvez également créer une géométrie personnalisée ainsi que charger de la géométrie à partir de fichiers.

  • Les objets Material représentent les propriétés de surface utilisées pour dessiner la géométrie y compris des choses comme la couleur à utiliser et à quel point elle est brillante. Un Material peut également référencer un ou plusieurs objets Texture qui peuvent être utilisés, par exemple, pour envelopper une image sur la surface d'une géométrie.

  • Les objets Texture représentent généralement des images soit chargées à partir de fichiers image, générées à partir d'un canevas, soit rendues à partir d'une autre scène.

  • Les objets Light représentent différents types de lumières.

Étant donné tout cela, nous allons créer la configuration *« Hello Cube »* la plus simple qui ressemble à ceci

Tout d'abord, chargeons three.js

<script type="module">
import * as THREE from 'three';
</script>

Il est important de mettre type="module" dans la balise script. Cela nous permet d'utiliser le mot-clé import pour charger three.js. À partir de r147, c'est la seule façon de charger properly three.js. Les modules ont l'avantage de pouvoir facilement importer d'autres modules dont ils ont besoin. Cela nous évite d'avoir à charger manuellement les scripts supplémentaires dont ils dépendent.

Ensuite, nous avons besoin d'une balise <canvas>, donc...

<body>
  <canvas id="c"></canvas>
</body>

Nous allons demander à three.js de dessiner dans ce canevas, nous devons donc le rechercher.

<script type="module">
import * as THREE from 'three';

+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
+  ...
</script>

Après avoir trouvé le canevas, nous créons un WebGLRenderer. Le renderer est la chose responsable de prendre toutes les données que vous fournissez et de les rendre sur le canevas.

Notez qu'il y a quelques détails ésotériques ici. Si vous ne passez pas de canevas à three.js, il en créera un pour vous, mais vous devrez ensuite l'ajouter à votre document. L'endroit où l'ajouter peut changer en fonction de votre cas d'utilisation et vous devrez changer votre code. Je trouve que passer un canevas à three.js est un peu plus flexible. Je peux placer le canevas n'importe où et le code le trouvera, alors que si j'avais du code pour insérer le canevas dans le document, je devrais probablement changer ce code si mon cas d'utilisation changeait.

Ensuite, nous avons besoin d'une caméra. Nous allons créer une PerspectiveCamera.

const fov = 75;
const aspect = 2;  // the canvas default
const near = 0.1;
const far = 5;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);

fov est l'abréviation de field of view (champ de vision). Dans ce cas, 75 degrés dans la dimension verticale. Notez que la plupart des angles en three.js sont en radians, mais pour une raison quelconque, la caméra perspective prend des degrés.

aspect est le rapport d'aspect (display aspect) du canevas. Nous aborderons les détails dans un autre article mais par défaut, un canevas est de 300x150 pixels, ce qui donne un rapport d'aspect de 300/150, soit 2.

near et far représentent l'espace devant la caméra qui sera rendu. Tout ce qui se trouve avant cette plage ou après cette plage sera écrêté (non dessiné).

Ces quatre paramètres définissent un *« frustum »*.

Un *frustum* est le nom d'une forme 3D qui ressemble à une pyramide dont la pointe est tranchée.

En d'autres termes, considérez le mot "frustum" comme une autre forme 3D comme une sphère, un cube, un prisme, un frustum.

La hauteur des plans near et far est déterminée par le champ de vision. La largeur des deux plans est déterminée par le champ de vision et l'aspect.

Tout ce qui se trouve à l'intérieur du frustum défini sera dessiné. Tout ce qui se trouve à l'extérieur ne le sera pas.

La caméra est orientée par défaut vers l'axe -Z avec +Y vers le haut. Nous allons placer notre cube à l'origine, nous devons donc reculer légèrement la caméra par rapport à l'origine afin de voir quelque chose.

camera.position.z = 2;

Voici ce que nous visons.

Dans le diagramme ci-dessus, nous pouvons voir que notre caméra est à z = 2. Elle regarde vers l'axe -Z. Notre frustum commence à 0.1 unité de l'avant de la caméra et va jusqu'à 5 unités devant la caméra. Parce que dans ce diagramme nous regardons vers le bas, le champ de vision est affecté par l'aspect. Notre canevas est deux fois plus large qu'il n'est haut, donc sur la largeur du canevas, le champ de vision sera beaucoup plus large que nos 75 degrés spécifiés, qui correspondent au champ de vision vertical.

Ensuite, nous créons une Scene. Une Scene dans three.js est la racine d'une forme de graphe de scène. Tout ce que vous voulez que three.js dessine doit être ajouté à la scène. Nous allons couvrir plus de détails sur le fonctionnement des scènes dans un futur article.

const scene = new THREE.Scene();

Ensuite, nous créons une BoxGeometry qui contient les données pour une boîte. Presque tout ce que nous voulons afficher dans Three.js nécessite une géométrie qui définit les sommets qui composent notre objet 3D.

const boxWidth = 1;
const boxHeight = 1;
const boxDepth = 1;
const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);

Nous créons ensuite un matériau de base et définissons sa couleur. Les couleurs peuvent être spécifiées en utilisant les valeurs hexadécimales à 6 chiffres de style CSS standard.

const material = new THREE.MeshBasicMaterial({color: 0x44aa88});

Nous créons ensuite un Mesh. Un Mesh en three.js représente la combinaison de trois choses

  1. Une Geometry (la forme de l'objet)
  2. Un Material (comment dessiner l'objet, brillant ou plat, quelle couleur, quelle(s) texture(s) appliquer. Etc.)
  3. La position, l'orientation et l'échelle de cet objet dans la scène par rapport à son parent. Dans le code ci-dessous, ce parent est la scène.
const cube = new THREE.Mesh(geometry, material);

Et enfin, nous ajoutons ce maillage à la scène

scene.add(cube);

Nous pouvons ensuite rendre la scène en appelant la fonction de rendu du renderer et en lui passant la scène et la caméra

renderer.render(scene, camera);

Voici un exemple fonctionnel

Il est un peu difficile de voir qu'il s'agit d'un cube 3D puisque nous le visualisons directement le long de l'axe -Z et que le cube lui-même est aligné sur les axes, donc nous ne voyons qu'une seule face.

Animons-le en rotation et, espérons-le, cela montrera clairement qu'il est dessiné en 3D. Pour l'animer, nous allons le rendre dans une boucle de rendu en utilisant requestAnimationFrame.

Voici notre boucle

function render(time) {
  time *= 0.001;  // convert time to seconds

  cube.rotation.x = time;
  cube.rotation.y = time;

  renderer.render(scene, camera);

  requestAnimationFrame(render);
}
requestAnimationFrame(render);

requestAnimationFrame est une requête au navigateur indiquant que vous souhaitez animer quelque chose. Vous lui passez une fonction à appeler. Dans notre cas, cette fonction est render. Le navigateur appellera votre fonction et si vous mettez à jour quoi que ce soit lié à l'affichage de la page, le navigateur re-rendrera la page. Dans notre cas, nous appelons la fonction renderer.render de three, qui dessinera notre scène.

requestAnimationFrame passe le temps écoulé depuis le chargement de la page à notre fonction. Ce temps est exprimé en millisecondes. Je trouve beaucoup plus facile de travailler avec des secondes, donc ici nous convertissons cela en secondes.

Nous définissons ensuite les rotations X et Y du cube à l'heure actuelle. Ces rotations sont en radians. Il y a 2 pi radians dans un cercle, donc notre cube devrait faire un tour sur chaque axe en environ 6,28 secondes.

Nous rendons ensuite la scène et demandons une autre frame d'animation pour continuer notre boucle.

En dehors de la boucle, nous appelons requestAnimationFrame une seule fois pour démarrer la boucle.

C'est un peu mieux, mais il est toujours difficile de voir le 3D. Ce qui aiderait, c'est d'ajouter un peu d'éclairage, alors ajoutons une lumière. Il existe de nombreux types de lumières dans three.js que nous aborderons dans un futur article. Pour l'instant, créons une lumière directionnelle.

const color = 0xFFFFFF;
const intensity = 3;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(-1, 2, 4);
scene.add(light);

Les lumières directionnelles ont une position et une cible. Les deux sont par défaut à 0, 0, 0. Dans notre cas, nous définissons la position de la lumière à -1, 2, 4, de sorte qu'elle est légèrement sur la gauche, au-dessus et derrière notre caméra. La cible est toujours 0, 0, 0, elle brillera donc vers l'origine.

Nous devons également changer le matériau. Le MeshBasicMaterial n'est pas affecté par les lumières. Changeons-le pour un MeshPhongMaterial qui est affecté par les lumières.

-const material = new THREE.MeshBasicMaterial({color: 0x44aa88});  // greenish blue
+const material = new THREE.MeshPhongMaterial({color: 0x44aa88});  // greenish blue

Voici la structure de notre nouveau programme

Et le voici en fonctionnement.

Maintenant, il devrait être assez clairement en 3D.

Juste pour le plaisir, ajoutons 2 cubes de plus.

Nous utiliserons la même géométrie pour chaque cube mais créerons un matériau différent afin que chaque cube puisse avoir une couleur différente.

Tout d'abord, nous allons créer une fonction qui crée un nouveau matériau avec la couleur spécifiée. Ensuite, elle crée un maillage en utilisant la géométrie spécifiée et l'ajoute à la scène et définit sa position en X.

function makeInstance(geometry, color, x) {
  const material = new THREE.MeshPhongMaterial({color});

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

  cube.position.x = x;

  return cube;
}

Ensuite, nous l'appellerons 3 fois avec 3 couleurs et positions X différentes en stockant les instances Mesh dans un tableau.

const cubes = [
  makeInstance(geometry, 0x44aa88,  0),
  makeInstance(geometry, 0x8844aa, -2),
  makeInstance(geometry, 0xaa8844,  2),
];

Enfin, nous allons faire tourner les 3 cubes dans notre fonction de rendu. Nous calculons une rotation légèrement différente pour chacun.

function render(time) {
  time *= 0.001;  // convert time to seconds

  cubes.forEach((cube, ndx) => {
    const speed = 1 + ndx * .1;
    const rot = time * speed;
    cube.rotation.x = rot;
    cube.rotation.y = rot;
  });

  ...

et voici le résultat.

Si vous le comparez au diagramme vu de dessus ci-dessus, vous pouvez voir qu'il correspond à nos attentes.

Avec les cubes à X = -2 et X = +2, ils sont partiellement en dehors de notre frustum.

Ils sont également quelque peu exagérément déformés car le champ de vision à travers le canevas est si extrême.

Notre programme a maintenant cette structure

Comme vous pouvez le voir, nous avons 3 objets Mesh, chacun référençant la même BoxGeometry. Chaque Mesh référence un MeshPhongMaterial unique afin que chaque cube puisse avoir une couleur différente.

J'espère que cette courte introduction vous aidera à démarrer. Ensuite, nous verrons comment rendre notre code réactif afin qu'il soit adaptable à plusieurs situations.

modules es6, three.js et structure de dossiers

À partir de la version r147, la manière préférée d'utiliser three.js est via les modules es6 et les cartes d'importation (import maps).

Les modules es6 peuvent être chargés via le mot-clé import dans un script ou en ligne via une balise <script type="module">. Voici un exemple

<script type="module">
import * as THREE from 'three';

...

</script>

Notez le spécificateur 'three' ici. Si vous le laissez tel quel, il produira probablement une erreur. Une *carte d'importation* doit être utilisée pour indiquer au navigateur où trouver three.js

<script type="importmap">
{
  "imports": {
    "three": "./path/to/three.module.js"
  }
}
</script>

Notez que le spécificateur de chemin ne peut commencer qu'avec ./ ou ../.

Pour importer des extensions (addons) comme OrbitControls.js, utilisez ce qui suit

import {OrbitControls} from 'three/addons/controls/OrbitControls.js';

N'oubliez pas d'ajouter les extensions (addons) à la carte d'importation comme ceci

<script type="importmap">
{
  "imports": {
    "three": "./path/to/three.module.js",
    "three/addons/": "./different/path/to/examples/jsm/"
  }
}
</script>

Vous pouvez également utiliser un CDN

<script type="importmap">
{
  "imports": {
    "three": "https://cdn.jsdelivr.net/npm/three@<version>/build/three.module.js",
    "three/addons/": "https://cdn.jsdelivr.net/npm/three@<version>/examples/jsm/"
  }
}
</script>

En conclusion, la manière recommandée d'utiliser three.js est

<script type="importmap">
{
  "imports": {
    "three": "./path/to/three.module.js",
    "three/addons/": "./different/path/to/examples/jsm/"
  }
}
</script>

<script type="module">
import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';

...

</script>