# 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

cliquez ici pour ouvrir dans une fenêtre séparée

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.

cliquez ici pour ouvrir dans une fenêtre séparée

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.

cliquez ici pour ouvrir dans une fenêtre séparée

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.

cliquez ici pour ouvrir dans une fenêtre séparée

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>