Les langages du web > Le Javascript

Three.js

(En construction)

jour 2 - Learn Three

Dédiée à l'affichage d'objets 3D dans un navigateur web, la première version de cette library JS est apparue en 2010.

Les versions de three.js

Ce mini-cours s'inspire de ce livre (sur Internet) qui utilise la version 132 à 136.

Attention. Lorsqu'on fait des copier/coller de bouts de code, toujours s'assurer que ces bouts de code correspondent à la même version de la librairie.

/* const geometry = new BoxBufferGeometry(); // valide si from "https://cdn.skypack.dev/three@0.132.2" */
const geometry = new BoxGeometry(); // valide avec la version 157

Avec ou sans Buffer

De même qu'on ne mélange pas du code de différentes versions, on utilise la documentation correspondante (à la version du code).

La version utilisée dans ce mini-cours est la version 157 (r157; r = release (sortie, en français))

Autres tutoriels

Voici une liste de tutoriels pour apprendre à utiliser **Three.js**:

1. **Chapitre 1 - Les concepts de base de Three.js**: Ce chapitre détaille les concepts de base utilisés dans un projet Three.js. Il présente la hiérarchie entre les différents concepts que nous aborderons, tels que Scene, Mesh, Geometry, Material, Light, Camera et Renderer.

2. **3D Application & Game Development on Three.js**: Ce cours en ligne gratuit sur le développement d'applications et de jeux avec Three.js vous donnera les connaissances et les techniques nécessaires pour développer des jeux 3D et des applications à l'aide de Three.js.

3. **Introduction et mise en place**: Ce tutoriel three.js vous guide à travers les étapes de la mise en place de Three.js.

4. **Apprendre three.js**: Dans cette vidéo, vous pouvez voir un exemple de panorama 360° interactif créé à l'aide de la librairie Three.js.

5. **Chapitre 2 - Un Hello World avec Three.js**: Dans ce chapitre, vous découvrirez comment configurer en détail les différents concepts que nous avons présentés dans le chapitre 1.

J'espère que cela vous aidera à commencer à utiliser Three.js!

Source : conversation avec Bing, 29/12/2023

Visualisation

La visualisation des exemples ci-dessous ne peut se faire que sur un serveur (local ou distant).

Navigateur Edge > (...) > Paramètres > Système et Performance (menu de gauche), section "Système" (partie droite), y cocher "Utiliser l'accélération matérielle" (pour éviter les avertissements dans la console).

Si Python est installé sur votre ordinateur, vous disposez d'un mini serveur web local.

Quelques commandes en ligne pour lancer un mini-serveur. Les langages associés doivent être préalablement installés :
npx http-server (Node.js)
npx five-server (Node.js)
python -m SimpleHTTPServer (Python 2.x)
python -m http.server (Python 3.x)
php -S localhost:8000 (PHP 5.4+)

Exemples

Ces exemples ne s'afficheront que si l'URL, dans la barre du navigateur, commence par http.

Pré-requis

Éditeur recommandé

Visual Studio Code (fonctionne sous Window, Mac et Linux)

Code HTML

Code du fichier index.htm :

Ce fichier est identique dans tous les exemples (situés dans les dossiers : 3D_*),
hormis le contenu de la balise title qui est le nom du dossier de l'exemple en cours.

<!DOCTYPE html>
<html lang="fr-BE" xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr-BE">
  <head>
    <meta charset="UTF-8" />
    <title>
      3D_01
    </title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="stylesheet" href="main.css" />
    <script type="module" src="m_main.js"></script>
  </head>
  <body>
    <noscript>Le JavaScript n'est pas activé. Son activation est nécessaire pour visualiser un objet 3D.</noscript>
    <p id="noServer"></p>
    <div id="scene-container"></div>
    <script src="index.js"></script>
  </body>
</html>

L'animation 3D sur une page web se fait dans un container :
<div id="scene-container"></div>

Dans le code HTML de cette page contient un appel à un fichier CSS (dans la section <head>) :
<link rel="stylesheet" href="main.css" />

Ce fichier main.css gère l'apparence du container.

Dans le code HTML de cette page contient un appel à un fichier JS (dans la section <head>) :
<script src="m_main.js" type="module"></script>

À partir de maintenant, tout module ne peut appeler qu'un module.
Un module ne peut qu'exporter du code ou qu'en importer. Plus d'info : du livre, w3schools, MDN

L'objet 3D ne peut apparaître que si le JS est activé.
=> <noscript>...</noscript> juste après la balise body.

La page web appelle un fichier JS classique, index.js, qui informe l'utilisateur (ici, le développeur) que le fichier ne provient pas d'un serveur.

D'où le code HTML juste après la balise noscript : <p id="noServer"></p>

index.js

Ce fichier est identique dans tous les dossiers : 3D_*

if (location.protocol.substr(0,4)!="http") document.getElementById("noServer").innerHTML
  ="Pas connecté à un serveur !"

Il ne sert qu'à rappeler aux distraits que ce qu'ils voient ne provient pas d'un serveur.

main.css

Ce fichier est identique dans tous les dossiers : 3D_*

#scene-container {
  /* Tell our scene container to take up the full page */
  position: absolute;
  width: 100%;
  height: 100%;

  /* Set the container's background color to the same as the scene's background
    (cfr scene.js) to prevent flashing on load  */
  background-color: skyblue;
}

Chemins relatifs

Le chemin est relatif par rapport au fichier appelant.

Ici, le fichier appelant est souvent un fichier JS qui appelle (importe) un autre fichier JS ...

Le chemin des importations des Relative references must start with either "/", "./", or "../".
Plus d'info

Lorsque tout.js est dans le même dossier que m_main.js,
et lorsque m_main.js fait appel à tout.js,
le chemin est alors (dans m_main.js) : ./tout.js.

Les modules tout.js et m_main.js seront abordés plus loin.

La librairie, three.module.min.js (ici, version 157), se situe dans le dossier parent des dossiers 3D_*.

Consulter la liste des versions (pour trouver le numéro de la dernière)

=> Les modules JS - scene.js, camera.js, lights.js, ... - la trouvent via : ../three.module.min.js.

Toute application 3D a besoin d'une scène, d'une camera et d'un rendu.

scene.js

Il existe toujours une scène. => Tous les exemples disposent d'une scène.
=> Ce fichier est identique dans tous les dossiers : 3D_*

import { Color, Scene} from "../three.module.min.js"

function createScene() {

  const scene = new Scene();

  /* même couleur dans main.css */
  scene.background = new Color('skyblue'); /* Rappel : doit correspondre à celle de main.css */

  return scene;
}

export { createScene };

La modification de la couleur de fond de la scène se fait dans le fichier scene.js. Toutefois, pour éviter un effet "flash", la même couleur est indiquée dans le fichier main.css.

The scene is the container that holds all the objects (meshes, cameras, and lights), the camera determines what part of the scene is shown when it is rendered, and the renderer takes care of creating the output on the screen, taking into account all the information from the meshes, cameras, and lights in the scene.

mesh = geometry + material (= le maillage + la peau)

camera.js

Il existe toujours une caméra. => Tous les exemples disposent d'une caméra.
=> Ce fichier est identique dans tous les dossiers : 3D_*

import { PerspectiveCamera } from "../three.module.min.js"

function createCamera() {

  const camera = new PerspectiveCamera(
    35,  /* fov = Field Of View        */
    1,   /* aspect ratio (dummy value) */
    0.1, /* near clipping plane        */
    100, /* far clipping plane         */
  );

  /* move the camera back so we can view the scene */
  camera.position.set(0, 0, 10);

  return camera;
}

export { createCamera };

lights.js

Quasi tous les exemples disposent de lumières.
=> Ce fichier est identique dans les dossiers : 3D_02 à 3D_06 (un seul spot)

import { DirectionalLight } from "../three.module.min.js"

function createLights() {

  /* Create a directional light */
  const light = new DirectionalLight('white', 8);

  /* move the light right, up, and towards us */
  light.position.set(10, 10, 10);

  return light;
}

export { createLights };

Dans le premier exemple, la scène n'est pas éclairée. À partir de l'exemple 7, il existe une lumière d'ambiance. Voir lights7.js

release r155 will contain a major change in context of lighting. Source.

renderer.js

Tous les exemples disposent d'un rendu (pour renvoyer le résultat à l'écran)
=> Ce fichier est identique dans tous les dossiers : 3D_*

import { WebGLRenderer } from "../three.module.min.js"

function createRenderer() {

  const renderer = new WebGLRenderer({ antialias: true });

  /* turn on the physically correct lighting model */
  renderer.physicallyCorrectLights = true;

  return renderer;
}

export { createRenderer };

Resizer.js

Cette class tient compte des dimensions de la zone dans laquelle devra s'afficher l'objet 3D.

class Resizer {

  constructor(container, camera, renderer) {

    /* Set the camera's aspect ratio */
    camera.aspect = container.clientWidth / container.clientHeight;

    /* update the camera's frustum */
    camera.updateProjectionMatrix();

    /* update the size of the renderer AND the canvas */
    renderer.setSize(container.clientWidth, container.clientHeight);

    /* set the pixel ratio (for mobile devices) */
    renderer.setPixelRatio(window.devicePixelRatio);
  }
}

export { Resizer };

Version de 3D_03 et suivantes : Resizer3.js

tout.js

Le contenu de ce fichier varie d'un dossier à l'autre.

Ce module contient toutes les importations nécessaires et exporte la classe Tout.

Un module importé peut lui-même importer un ou plusieurs modules ...

Cette classe Tout contient 2 méthodes : constructor() et render().

Le constructeur de la class Tout reçoit comme unique paramètre la référence à la div du code HTML. C'est dans cette zone HTML que tout sera affiché (si l'objet est dans le champ de la caméra et ...).

/* Ce module importe les modules nécessaires */

import { createCamera } from './camera.js';
import { createCube } from './cube.js';
/*import { createTorus } from './torus.js';*/
import { createScene } from './scene.js';

import { createRenderer } from './renderer.js';
import { Resizer } from './Resizer.js';

/* These variables are module-scoped
   => we cannot access them from outside the module */
let camera;
let renderer;
let scene;

class Tout {

  constructor(container) {

    camera = createCamera();
    scene = createScene();

    renderer = createRenderer();
    container.append(renderer.domElement);

    const cube = createCube();
    scene.add(cube);

    /* D'autres objets peuvent être créés ici : */

    /*const cube2 = createCube(); // de même dimension que le premier ...
    cube2.position.set(3,1,2)
    scene.add(cube2);*/

    /*const torus = createTorus();
    scene.add(torus);*/

    const resizer = new Resizer(container, camera, renderer);

    resizer.onResize = () => {
      this.render();
    };

  }

  render() { /* draw a single frame */
    renderer.render(scene, camera);
  }
}

export { Tout };

C'est dans ce module qu'on peut créer plusieurs objets 3D.

m_main.js

Ce module est le seul appelé par le fichier HTML.

Ce code JS contient une fonction main() qui est appelée après avoir été créée.

La fonction main() :

  1. Crée la référence au container
  2. Crée l'objet (ici, tout) qui contiendra tous les objets nécessaires à l'animation (et qui a reçu la référence du container)
  3. Lance la méthode de l'objet (ici, tout) qui lance le rendu.

La fonction main() fait appel à la classe Tout qui, donc, doit être importée.
Les importations se font au début du code JS.

import { Tout } from './tout.js';

/* create the main function */
function main() {

  /* 1. Create a reference to the container element */
  const container = document.querySelector('#scene-container');

  /* 2. Create a scene */
  const tout = new Tout(container);

  /* 3. Render the scene */
  tout.render();

  /* 4. start the loop (produce a stream of frames, pour une animation en continu) */
  tout.start();

}

/* Call main to start the app */
main();

Ce fichier est (quasi) identique dans tous les dossiers : 3D_*.
tout.start(); n'est lié qu'à l'animation => dossiers 3D_04 et +.

Résumé

En gros, au minimum, dans un dossier,

Dans les dossiers, le nom des modules et leur contenu peuvent être légèrement différents.


Ci-dessous, le cours reste au stade de développement


Les objets sur la scène

cube.js

Documentation : BoxGeometry
Cette documentation ci-dessus ne convient pas au code ci-dessous !

import { BoxBufferGeometry, Mesh,  MeshBasicMaterial } from "https://cdn.skypack.dev/three@0.132.2"

function createCube(floatWidth=2, floatHeight=2, floatDepth=2) {

  /* create a geometry */
  const geometry = new BoxBufferGeometry(floatWidth, floatHeight, floatDepth);

  /* create a default (white) Basic material */
  const material = new MeshBasicMaterial();

  /* create a Mesh containing the geometry and material */
  const cube = new Mesh(geometry, material);

  return cube;
}

export { createCube };

Pour chaque objet de géométrie différente, il devrait avoir un fichier JS : cube.js, torus.js, ...

Chacun de ces fichiers devrait avoir une fonction createXXX() avec les paramètres du constructeur de la géométrie et la couleur du matériel

torus.js

import { TorusGeometry, Mesh,  MeshBasicMaterial } from "https://cdn.skypack.dev/three@0.132.2"

function createTorus(floatRadius=2, floatTube=1, intRadialSegments=16, intTubularSegments=30,
                     floatArc=6.283185307179586, strColor="0x00ff00") {

  /* create a geometry */
  const geometry = new TorusGeometry(floatRadius, floatTube, intRadialSegments, intTubularSegments, floatArc);

  /* create Basic material (By default, white) */
  const material = new MeshBasicMaterial({color: strColor});

  /* create a Mesh containing the geometry and material */
  const torus = new Mesh(geometry, material);

  return torus;
}

export { createTorus };

Ci-dessus, ce sont les bases.

Voir le résultat. S'affiche un carré blanc centré sur fond bleu qui ne s'adapte pas à la taille de la fenêtre lorsqu'elle est redimmensionnée. En réalité, elle n'est redimmensionnée qu'une fois, lors de la première frame.


Il reste à éclairer la scène. (=> l'objet est visible)

cube2.js

Améliorations du code de l'objet cube :

La couleur est indiquée sous la forme d'une string. La valeur de cette string est soit un nom de couleur, soit # suivi de 6 chiffres hexadécimaux.

import { BoxBufferGeometry, Mesh,  MeshBasicMaterial } from "https://cdn.skypack.dev/three@0.132.2"
import { BoxBufferGeometry, Mesh, MeshStandardMaterial } from "https://cdn.skypack.dev/three@0.132.2";

function createCube(floatWidth=2, floatHeight=2, floatDepth=2, strColor="#00ff00") {

  /* create a geometry */
  const geometry = new BoxBufferGeometry(floatWidth, floatHeight, floatDepth);

  /* create a default (white) Basic material */
  const material = new MeshBasicMaterial();
  const material = new MeshStandardMaterial({ color: strColor });

  /* create a Mesh containing the geometry and material */
  const cube = new Mesh(geometry, material);

  cube.rotation.set(-0.5, -0.1, 0.8);

  return cube;
}

export { createCube };

renderer2.js

import { WebGLRenderer} from "https://cdn.skypack.dev/three@0.132.2"

function createRenderer() {

  const renderer = new WebGLRenderer();

  /* turn on the physically correct lighting model */
  renderer.physicallyCorrectLights = true;

  return renderer;
}

export { createRenderer };

tout2.js

/* Ce module importe les modules nécessaires */

import { createCamera } from './camera.js';
import { createCube } from './cube2.js';
import { createScene } from './scene.js';
import { createLights } from './lights.js';

import { createRenderer } from './renderer2.js';
import { Resizer } from './Resizer.js';

/* These variables are module-scoped
   => we cannot access them from outside the module */
let camera;
let renderer;
let scene;

class Tout {

  constructor(container) {

    camera = createCamera();
    scene = createScene();

    renderer = createRenderer();
    container.append(renderer.domElement);

    const cube = createCube();
    scene.add(cube);

    const light = createLights();
    scene.add(cube, light);

    const resizer = new Resizer(container, camera, renderer);

    resizer.onResize = () => {
      this.render();
    };

  }

  render() { /* draw a single frame */
    renderer.render(scene, camera);
  }
}

export { Tout };

Ci-dessus, avec ces améliorations : lumière/ombre, couleur de la peau et rotation initiale de l'objet placé sur la scène.

Voir le résultat (avec des objets visibles).


Il reste à redessiner la scène chaque fois que la dimension de la fenêtre du navigateur varie.

Ci-dessus, avec ces améliorations :

renderer3.js

import { WebGLRenderer} from "https://cdn.skypack.dev/three@0.132.2"

function createRenderer() {

  const renderer = new WebGLRenderer({ antialias: true });

  /* turn on the physically correct lighting model */
  renderer.physicallyCorrectLights = true;

  return renderer;
}

export { createRenderer };

Resizer3.js

Version antérieure

const setSize = (container, camera, renderer) => {
  camera.aspect = container.clientWidth / container.clientHeight;
  camera.updateProjectionMatrix();

  renderer.setSize(container.clientWidth, container.clientHeight);
  renderer.setPixelRatio(window.devicePixelRatio);
};

class Resizer {
  constructor(container, camera, renderer) {

    /* set initial size on load */
    setSize(container, camera, renderer);

    window.addEventListener('resize', () => {

      /* set the size again if a resize occurs */
      setSize(container, camera, renderer);

      /* perform any custom actions */
      this.onResize();
    });
  }

  onResize() {}
}

export { Resizer };

tout3.js

/* Ce module importe les modules nécessaires */

import { createCamera } from './camera.js';
import { createCube } from './cube4.js';
import { createScene } from './scene.js';
import { createLights } from './lights.js';

import { createRenderer } from './renderer3.js';
import { Resizer } from './Resizer3.js';

...

Voir le résultat (avec un redimensionnement de la fenêtre).


Il reste à animer l'objet.

Ci-dessus, avec ces améliorations :

Loop.js

import { Clock } from "https://cdn.skypack.dev/three@0.132.2";

const clock = new Clock();

class Loop {
  /* Contient un constructeur et 3 méthodes : .start(), .stop() et .tick() */

  constructor(camera, scene, renderer) {

    this.camera = camera;
    this.scene = scene;
    this.renderer = renderer;
    this.updatables = [];
  }

  start() {
    this.renderer.setAnimationLoop( () => {

      /* tell every animated object to tick forward one frame */
      this.tick();

      /* render a frame */
      this.renderer.render(this.scene, this.camera);
    });
  }

  stop() {
    this.renderer.setAnimationLoop(null);
  }

  tick() {
    /* only call the getDelta function once per frame ! */
    const delta = clock.getDelta();

    for (const object of this.updatables) {
      object.tick(delta);
    }
  }

}

export { Loop };

cube4.js

import {
  BoxBufferGeometry,
  MathUtils,
  Mesh,
  MeshStandardMaterial,
} from "https://cdn.skypack.dev/three@0.132.2";

function createCube(floatWidth=2, floatHeight=2, floatDepth=2, strColor="#00ff00") {

  const geometry = new BoxBufferGeometry(floatWidth, floatHeight, floatDepth);
  const material = new MeshStandardMaterial({ color: strColor });
  const cube = new Mesh(geometry, material);

  cube.rotation.set(-0.5, -0.1, 0.8);

  const radiansPerSecond = MathUtils.degToRad(30);

  /* this method will be called once per frame */
  cube.tick = (delta) => {
    /*increase the cube's rotation each frame */
    cube.rotation.z += radiansPerSecond * delta;
    cube.rotation.x += radiansPerSecond * delta;
    cube.rotation.y += radiansPerSecond * delta;
  };

  return cube;
}

export { createCube };

tout4.js

/* Ce module importe les modules nécessaires */

import { createCamera } from './camera.js';
import { createCube } from './cube4.js';
import { createScene } from './scene.js';
import { createLights } from './lights.js';

import { createRenderer } from './renderer3.js';
import { Resizer } from './Resizer3.js';
import { Loop } from './Loop.js';


/* These variables are module-scoped
   => we cannot access them from outside the module */
let camera;
let renderer;
let scene;
let loop;

class Tout {
...

  render() { /* draw a single frame */
    renderer.render(scene, camera);
  }

  start() {
    loop.start();
  }

  stop() {
    loop.stop();
  }

}

export { Tout };

main.js

Le lancement de l'animation se fait dans main.js

import { Tout } from './tout4.js';

/* create the main function */
function main() {

  /* 1. Create a reference to the container element */
  const container = document.querySelector('#scene-container');

  /* 2. Create a scene */
  const tout = new Tout(container);

  /* 3. Render the scene */
  tout.render();

  /* 4. start the loop (produce a stream of frames, pour une animation en continu) */
  tout.start();

}

/* Call main to start the app */
main();

Voir le résultat (avec une animation).


Il reste à donner une peau à l'objet.

Toute image au format PNG, JPG, GIF, BMP ... (mais pas WEBP) peut être une peau.

Sur un cube, l'image doit être carrée pour éviter la déformation.

Ci-dessus, avec ces améliorations :

cube5.js

import {
  BoxBufferGeometry,
  MathUtils,
  Mesh,
  MeshStandardMaterial,
  TextureLoader,
} from "https://cdn.skypack.dev/three@0.132.2";

function createMaterial() {

  /* create a texture loader. */
  const textureLoader = new TextureLoader();

  /* load a texture
  any image format that your browser supports, such as PNG, JPG, GIF, BMP ... not WEBP */
  const texture = textureLoader.load('uv-test-col.png');

  /* create a "standard" material */
  const material = new MeshStandardMaterial({ map: texture });

  return material;
}

function createCube(floatWidth=2, floatHeight=2, floatDepth=2) {

  const geometry = new BoxBufferGeometry(floatWidth, floatHeight, floatDepth);
  const material = createMaterial();
  const cube = new Mesh(geometry, material);

  cube.rotation.set(-0.5, -0.1, 0.8);

  const radiansPerSecond = MathUtils.degToRad(30);

  /* this method will be called once per frame */
  cube.tick = (delta) => {
    /* increase the cube's rotation each frame */
    cube.rotation.z += radiansPerSecond * delta;
    cube.rotation.x += radiansPerSecond * delta;
    cube.rotation.y += radiansPerSecond * delta;
  };

  return cube;
}

export { createCube };

Voir le résultat (avec une peau)

Voir le résultat idem (mais via CDN) avec obscurcissement du code JS du premier fichier JS appelé (ex-main.js, qui a été modifié pour tenir compte du fait que tout5.js a été renommé 0x502d75.js).