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
Geometry
(la forme de l'objet)Material
(comment dessiner l'objet, brillant ou plat, quelle couleur, quelle(s) texture(s) appliquer. Etc.)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.