Introduction à three.js


Les beaux jours reviennent, les feuilles commencent à pousser sur les arbres, mais surtout les Vive et Rift commencent à envahir les foyers. Les quoi ? Chez qui ? Les premiers casques de réalité virtuelle grand public réellement performants ! Après des mois d’attentes, les modèles de chez Oculus (qui appartient à Facebook) et du duo Valve / HTC arrivent chez les premiers acheteurs, tout comme leur support dans les dernières versions des navigateurs. Fort de ce constat, je me décidais alors de me fendre d’un petit article d’introduction à three.js.

Mais, qu’est-ce que three.js ?

Three.js est une bibliothèque légère, réalisée par Ricardo Cabello, dont la fonction principale est de rendre accessible au plus grand nombre le rendu 3D dans les navigateurs. Elle propose une couche d’abstraction à WebGL incluant la gestion de graphes de scène et la facilitation de la plupart des opérations mathématiques utiles dans ce domaine.

Je vous invite à récupérer sur votre machine préférée le contenu de ce dépôt où vous retrouverez le code nécessaire à la compréhension de l’article.

Une version live des scènes de cet article est également disponible sur ce lien.

Contrôles de la démo:

  • A – déplacement automatique
  • P – Scène précédente
  • N – Scène suivante

En mode caméra manuelle ou (inclusif) mode réalité virtuelle :

  • ZSQD – Déplacement avant arrière gauche droite (grand classique)
  • CTRL – Descendre
  • ESPACE – Monter

Parce qu’il faut bien commencer par quelque chose, traçons un cube…

three.js scene 1

Le minimum vital pour pouvoir rendre une scène est constitué des éléments suivants :

  • Un renderer
  • Une caméra
  • Une scène

Pour un affichage en temps réel du renderer, on utilisera le mécanisme de requestAnimationFrame auquel les personnes ayant déjà manipulé des canvas pour faire de l’animation sont habituées.

Une scène peut contenir une ou plusieurs caméras.

Une fois ces éléments en place, il est intéressant d’ajouter à notre scène du contenu, histoire d’avoir quelque chose à voir. Nous allons donc ajouter un cube dans une nouvelle scène.

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

Un objet manipulable dans le graphe de scène est constitué de deux éléments :

  • Une géométrie, qui définit ses sommets et leurs liaisons
  • Une matière qui définit son aspect visuel

Il est possible de positionner très facilement un objet grâce à ses propriétés position et rotation qui permettent d’agir sur les axes x,y et z. Il est aussi possible de passer par des opérations utilisant des quaternions.

À retenir : si on considère l’écran comme l’origine, z négatif est devant, z positif derrière. C’est important, car on peut se demander où sont les objets si on ne fait pas attention… Vous savez, le genre de bugs où l’on a honte d’avoir passé 2 heures à comprendre…

Certains esprits chagrins pourraient ici douter de l’aspect tridimensionnel de ce cube, ce à quoi, je serais tenté de répondre que dans tous les cas, ce n’est qu’une projection sur un écran en deux dimensions, et donc par nature, de la 2D. Cependant, dans ma grande mansuétude, je les inviterais plutôt à passer à l’étape suivante :

Mouvement, dynamisme, action : transformons un monde inerte en une orgie visuelle !

three.js scene 2

Dans l’étape précédente, nous avons vu qu’il existait des propriétés permettant de positionner un élément. Il est possible d’utiliser ces propriétés pour déplacer la caméra lors du passage dans la boucle d’animation.

Il est également possible de déplacer un objet selon sa position locale avec les opérations translate* et rotate* où * est à remplacer par l’axe souhaité. Ces fonctions sont particulièrement utiles pour établir les contrôles d’un joueur ou d’un véhicule.

À retenir : tous les objets ajoutés au graphe de scène peuvent être manipulés de la même manière que la caméra.

Je veux pouvoir interagir avec le monde !

three.js Scene 03

Hé bien voilà qui tombe fort à propos, car c’est justement le sujet de cette scène. Certains se souviennent peut-être avec nostalgie de leurs cours de maths et de géométrie vectorielle, de transformations de matrices et autres produits scalaires. Hélas, three.js a décidé de simplifier les choses en fournissant un Raycaster. Savoir si un objet est dans votre ligne de mire ou sous votre souris est aussi simple que de demander à cet objet de tirer un rayon à partir de cette direction.

var rayCaster = new THREE.Raycaster();
function onMouseClick(event) {
   var mouse = new THREE.Vector2();
   mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
   mouse.y = -( event.clientY / window.innerHeight ) * 2 + 1;
   rayCaster.setFromCamera(mouse, camera);
   var intersects = rayCaster.intersectObjects(scene.children);
   for (var i = 0; i < intersects.length; i++) {
       intersects[i].object.material.color.set(0xE84F0E);
   }
}

Dans cet exemple, on utilise les coordonnées normalisées (entre –1 et 1) de la souris pour pouvoir changer la couleur des objets traversés par le rayon lancé passant par la caméra et le plan de la souris.

À retenir : le raycasting a la possibilité d’être récursif, ce qui permet de traverser des hiérarchies d’objets, ce qui n’est pas le cas par défaut (par exemple, pour toucher un élément spécifique au sein d’un groupe). Les coordonnées cible du THREE.Vector2 doivent être comprises entre –1 et 1.

Oh ? Une question ?

“Le MeshBasicMaterial, c’est moche, pourquoi tu n’utilises que ça ?”

Pour des raisons pédagogiques, jeune malotru. En effet, ce matériau ne nécessite pas de source lumineuse pour être visible ce qui est plutôt pratique pour simplifier nos premiers exemples. Cependant, ta question très pertinente m’amène à traiter le point suivant, les lumières. Alors…

Fiat lux* !

three.js scene 4

*locution latine n’ayant aucun rapport avec une quelconque gamme automobile

Ah oui, c’est tout de suite moins moche. Observons.

Tout d’abord, nous avons abandonné les matières basiques pour passer à d’autres, sensibles à l’éclairage. Ce qui signifie aussi que sans éclairage, les boules apparaîtraient complètement noires. Le fond, lui, serait visible, car non sensible aux lumières.

Dans cette scène, 4 sources lumineuses sont utilisées :

var sun = new THREE.DirectionalLight(new THREE.Color(0xffffff), 0.3);

Similaire aux rayons du soleil, cette source lumineuse est directionnelle et d’intensité constante.

var ambientLight = new THREE.AmbientLight(0xffffff, 0.2);

Considérée généralement comme une des plus grosses astuces du rendu 3D, cette source de lumière éclaire uniformément tous les objets, elle évite le noir total. Si toute la scène était éclairée avec cette source, le résultat serait similaire à ce qu’on obtenait avec une matière basique.

var hemisphereLight = new THREE.HemisphereLight(0xcccccc, 0x333333, 0.25);

Cette source permet de simuler un éclairage venant du ciel et du sol, par exemple pour ajouter l’illusion de reflet de la couleur de ceux-ci sur un objet

var pointLight = new THREE.PointLight(0xffffff, 1.2, 60);

Une lumière omnidirectionnelle et ponctuelle, sa puissance s’estompe avec la distance.

Pour les objets à l’aspect mat, on utilisera une matière de type MeshLambertMaterial qui éclaire selon les sommets. Pour des matériaux brillants, on se tournera plutôt vers du MeshPhongMaterial qui éclaire en fonction des pixels.

Dans cette scène, d’autres matériaux ont également été employés, on notera notamment l’usage d’une texture d’environnement pour simuler la réfraction et la réflexion.

À retenir : three.js propose de nombreuses matières, la plupart basées sur des shaders. Il offre aussi de nombreux helpers pour visualiser graphiquement des choses invisibles telles que des caméras ou des sources lumineuses.

Que ? Ah, votre sens de l’observation m’emplit d’une immense fierté. Oui, les lumières peuvent aussi projeter des ombres, laissez-moi vous expliquer…

Carried away, by a moonlight shadow

three.js scene 5

Les ombres. Un sujet de recherche constant dans le monde du rendu 3D. À ce jour, il n’existe pas de façon efficace et ultime de les calculer, alors on se contente d’approximations de plus en plus convaincantes. Une fois de plus, three.js va vous masquer tout l’aspect calculatoire de l’opération.

var light = new THREE.DirectionalLight(new THREE.Color(0xFFFFFF), 1);
light.position.set(0, 20, 0);
light.castShadow = true;

Oui. Il suffit de demander à une source de lumière directionnelle de tracer les ombres pour que la bibliothèque s’en charge. Cependant votre intervention va être requise pour différents ajustements :

  • vous devez définir la taille de la texture destinée au rendu des ombres avec raison
  • vous devez définir la zone dans laquelle sont rendues les ombres, en longueur, largeur et profondeur
  • vous devez régler le biais d’approximation pour éviter que vos ombres soient constellées de trous (qu’on appelle acné, oui, c’est sale)

Vous devrez également signaler au renderer de tracer les ombres :

renderer.shadowMap.enabled = true;
renderer.shadowMap.cullFace = THREE.CullFaceBack;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

Le type des ombres reste à votre discrétion. Mais, à mon humble avis, les soft shadows sont plus jolies. Notons que le back face culling évite pas mal d’artefacts visuels.

À retenir : s’il est très simple d’ajouter des ombres à une scène, il faut néanmoins penser à bien régler les différents paramètres, au risque de ne rien voir ou de voir son plaisir gâché par quelques artefacts aberrants.

La magie du cinéma des groupes !

three.js scene 6

Une personne ayant bien observé, ou mieux, joué avec les exemples précédents, aura noté l’aspect rébarbatif du placement d’éléments les uns par rapport aux autres. Imaginez maintenant devoir représenter une scène comme celle de l’illustration bloc par bloc… Oui, ça en fait des calculs de décalages et autres bidules qu’on aimerait éviter. Heureusement, nous sommes face à un graphe de scène, il est donc possible de construire des ensembles d’éléments se comportant comme un tout grâce aux groupes.

Un groupe se construit très facilement grâce à la syntaxe suivante:

var myNiceGroup = new THREE.Group();

Les éléments s’ajoutent au groupe de la même façon qu’ils s’ajouteraient à une scène. Un groupe doit être ajouté à une scène pour être rendu.

À retenir : un groupe est une structure hiérarchique qui peut donc être traversée récursivement pour accéder à un élément donné. Vous pouvez également “accrocher” à la racine vos objets les plus important pour un accès plus rapide. Un groupe se manipulera comme un objet classique pour ce qui est de ses mouvements, positions et rotations.

Quoi ? Une autre question ? On vous avait promis des chats dans cet article ? OKAY.

Les textures, chat c’est bien !

three.js Scene 07

Roooh, mais c’est qu’ils sont trop choupinous tous ces minous ! Ahem. Je m’égare.

Plus sérieusement, il est tout simplement possible d’assigner une texture préalablement chargée à une matière en utilisant sa propriété map :

var catPictureTexture = new THREE.TextureLoader().load("textures/cats/" + cat.picture);
var catPictureGeometry = new THREE.PlaneGeometry(1, 1, 1);
var catPictureMaterial = new THREE.MeshBasicMaterial({
   color: 0xffffff,
   map: catPictureTexture,
   transparent: true,
   opacity: 0.95
});

À retenir : dans cet exemple, les textures sont utilisées “à la barbare” – ce ne sont ni des puissances de deux, ni des textures destinées à du mapping. Lorsque l’on cherche à texturer un objet, des cordonnées en uv correspondants à l’emplacement des textures en fonction des sommets doivent êtres également fournies.

Les textures vidéo, chat c’est encore mieux !

three.js Scene 08

De la même façon que la propriété map a été utilisée pour ajouter une texture statique à un objet, il est possible de l’utiliser pour ajouter une texture dynamique :

var texture = new THREE.VideoTexture(video);
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;
texture.format = THREE.RGBFormat;
var screenMaterial = new THREE.MeshBasicMaterial({color: 0xffffff, map: texture});

À retenir : utilisez des formats supportés par la balise vidéo HTML5 si vous souhaitez voir quelque chose.

Et les shaders pompaient…

three.js Scene 09

Une présentation de three.js ne serait pas complète sans un mot sur les shaders. En effet, three.js vous permet d’utiliser les capacités de la carte graphique de la machine pour traiter du code embarqué et vectorisé pour gérer sommets, fragments et autres.

Ce mécanisme est très puissant et pourrait faire l’objet de mois entiers de discussions et d’études.

Un excellent endroit pour apprendre est le site de Íñigo Quílez, ancien des effets spéciaux de chez Pixar et travaillant aujourd’hui dans la division cinéma de chez Oculus. Vous pouvez également vous entraîner sur shadertoy.com et voir les choses impressionnantes que les développeurs de la scène démo arrivent à faire !

Beaucoup plus humblement, les deux shaders proposés à titre d’illustration dans cette scène ajoutent au sol un effet de damier bruité et un effet de changement de couleur aux canapés en fonction de la position dans la face et celle de la caméra.

À retenir : Íñigo Quílez est le maître des shaders.

« Mais, je ne comprends pas, c’est quoi le rapport entre ton article et ton intro ? »

Ah, oui ! Et la réalité virtuelle dans tout ça ?

[Photo embarrassante de l’auteur en train de caresser des chats virtuels]REDACTED

Fidèle à sa réputation, three.js propose un wrapper (utilisé dans cette démo) au renderer, qui permet l’utilisation d’un casque de réalité virtuelle de façon transparente.

new THREE.VRControls(camera);
new THREE.VREffect(renderer, function (error) {});

En utilisant ces fonctions, votre scène pourra être automatiquement basculée sur le casque de VR utilisé par le navigateur. La position aussi bien que les rotations seront transférées à la caméra lors des mises à jour de la scène.

DISCLAIMER: utilisez la démo en mode réalité virtuelle dans un endroit sûr et à l’abri des personnes à l’affût d’une photo compromettante à poster sur leurs réseaux sociaux préférés.

À retenir : il est possible de wrapper différents effets spéciaux au renderer, la réalité virtuelle en est un.

Le mot de la fin

Les possibilités offertes par three.js sont immenses, cette introduction n’a fait qu’effleurer le potentiel de cette bibliothèque ou de WebGL. L’arrivée récente des casques de réalité virtuelle, et le début de leur support natif dans les versions de développement des navigateurs, laissent entrevoir d’intéressantes perspectives pour le Web. Une visite en ligne ? Du commerce ? De la réalité augmentée ? De l’éducation ? Du serious gaming ? Des plateformes de discussions ? Impossible de fournir une liste exhaustive des applications pouvant être impactées par ces nouveautés. Les besoins risquent d’émerger beaucoup plus vite que prévu, même si, il ne faut pas l’oublier, le taux d’équipement devait malheureusement rester très faible pendant encore quelques années.

Vous aimerez aussi...

  • bonjour
    super tuto clair
    il y a un exemple que personne ne traite reellement : une piece contenant des objets dans laquelle on peut se deplacer en mode first person controls et dans laquelle on ne passe pas au travers des murs mais on peut cliquer sur les objets pour obtenir une info… je bloque sur ce sujet depuis des semaines ;) un tuto serait le bienvenu car je ne comprends toujours pas ou placer les raycaster, en particulier quand la piece est importer en objmtl…
    merci pour les reponses. cordialement