Rendu à la demande

Le sujet peut sembler évident pour beaucoup, mais au cas où... la plupart des exemples Three.js rendent en continu. En d'autres termes, ils mettent en place une boucle requestAnimationFrame ou "boucle rAF" comme ceci

function render() {
  ...
  requestAnimationFrame(render);
}
requestAnimationFrame(render);

Pour quelque chose qui s'anime, cela a du sens, mais qu'en est-il de quelque chose qui ne s'anime pas ? Dans ce cas, rendre en continu est un gaspillage de la puissance de l'appareil et si l'utilisateur est sur un appareil portable, cela gaspille la batterie de l'utilisateur.

La façon la plus évidente de résoudre ce problème est de rendre une fois au début, puis de ne rendre que lorsque quelque chose change. Les changements incluent le chargement final des textures ou des modèles, l'arrivée de données depuis une source externe, l'ajustement d'un paramètre par l'utilisateur, le changement de caméra ou d'autres entrées pertinentes.

Prenons un exemple de l'article sur la réactivité et modifions-le pour qu'il rende à la demande.

D'abord, nous allons ajouter les OrbitControls afin qu'il y ait quelque chose qui puisse changer et auquel nous puissions réagir en rendant.

import * as THREE from 'three';
+import {OrbitControls} from 'three/addons/controls/OrbitControls.js';

et les configurer

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);
camera.position.z = 2;

+const controls = new OrbitControls(camera, canvas);
+controls.target.set(0, 0, 0);
+controls.update();

Puisque nous n'animerons plus les cubes, nous n'avons plus besoin de les suivre

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

Nous pouvons supprimer le code d'animation des cubes et les appels à requestAnimationFrame

-function render(time) {
-  time *= 0.001;
+function render() {

  if (resizeRendererToDisplaySize(renderer)) {
    const canvas = renderer.domElement;
    camera.aspect = canvas.clientWidth / canvas.clientHeight;
    camera.updateProjectionMatrix();
  }

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

  renderer.render(scene, camera);

-  requestAnimationFrame(render);
}

-requestAnimationFrame(render);

puis nous devons rendre une fois

render();

Nous devons rendre chaque fois que les OrbitControls modifient les paramètres de la caméra. Heureusement, les OrbitControls déclenchent un événement change chaque fois que quelque chose change.

controls.addEventListener('change', render);

Nous devons également gérer le cas où l'utilisateur redimensionne la fenêtre. C'était géré automatiquement auparavant puisque nous rendions en continu, mais maintenant que nous ne le faisons plus, nous devons rendre lorsque la taille de la fenêtre change.

window.addEventListener('resize', render);

Et avec cela, nous obtenons quelque chose qui rend à la demande.

Les OrbitControls ont des options pour ajouter une sorte d'inertie afin de les rendre moins rigides. Nous pouvons l'activer en définissant la propriété enableDamping sur true.

controls.enableDamping = true;

Avec enableDamping activé, nous devons appeler controls.update dans notre fonction de rendu afin que les OrbitControls puissent continuer à nous fournir de nouveaux paramètres de caméra pendant qu'ils lissent le mouvement. Mais cela signifie que nous ne pouvons pas appeler render directement depuis l'événement change car nous nous retrouverions dans une boucle infinie. Les contrôles nous enverraient un événement change et appelleraient render, render appellerait controls.update. controls.update enverrait un autre événement change.

Nous pouvons résoudre cela en utilisant requestAnimationFrame pour appeler render, mais nous devons nous assurer de ne demander une nouvelle image que si une n'a pas déjà été demandée, ce que nous pouvons faire en conservant une variable qui suit si nous avons déjà demandé une image.

+let renderRequested = false;

function render() {
+  renderRequested = false;

  if (resizeRendererToDisplaySize(renderer)) {
    const canvas = renderer.domElement;
    camera.aspect = canvas.clientWidth / canvas.clientHeight;
    camera.updateProjectionMatrix();
  }

  renderer.render(scene, camera);
}
render();

+function requestRenderIfNotRequested() {
+  if (!renderRequested) {
+    renderRequested = true;
+    requestAnimationFrame(render);
+  }
+}

-controls.addEventListener('change', render);
+controls.addEventListener('change', requestRenderIfNotRequested);

Nous devrions probablement aussi utiliser requestRenderIfNotRequested pour le redimensionnement également

-window.addEventListener('resize', render);
+window.addEventListener('resize', requestRenderIfNotRequested);

Il peut être difficile de voir la différence. Essayez de cliquer sur l'exemple ci-dessous et utilisez les touches fléchées pour vous déplacer ou faites glisser pour faire tourner. Ensuite, essayez de cliquer sur l'exemple ci-dessus et faites la même chose, et vous devriez pouvoir faire la différence. Celui d'en haut s'accroche lorsque vous appuyez sur une touche fléchée ou faites glisser, celui d'en bas glisse.

Ajoutons également une simple GUI lil-gui et faisons en sorte que ses modifications rendent à la demande.

import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
+import {GUI} from 'three/addons/libs/lil-gui.module.min.js';

Permettons de définir la couleur et l'échelle x de chaque cube. Pour pouvoir définir la couleur, nous utiliserons le ColorGUIHelper que nous avons créé dans l'article sur les lumières.

Tout d'abord, nous devons créer une GUI

const gui = new GUI();

puis pour chaque cube, nous créerons un dossier et ajouterons 2 contrôles, un pour material.color et un autre pour cube.scale.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;

+  const folder = gui.addFolder(`Cube${x}`);
+  folder.addColor(new ColorGUIHelper(material, 'color'), 'value')
+      .name('color')
+      .onChange(requestRenderIfNotRequested);
+  folder.add(cube.scale, 'x', .1, 1.5)
+      .name('scale x')
+      .onChange(requestRenderIfNotRequested);
+  folder.open();

  return cube;
}

Vous pouvez voir ci-dessus que les contrôles lil-gui ont une méthode onChange à laquelle vous pouvez passer une fonction de rappel à appeler lorsque la GUI modifie une valeur. Dans notre cas, nous avons juste besoin qu'elle appelle requestRenderIfNotRequested. L'appel à folder.open fait que le dossier s'ouvre dès le départ.

J'espère que cela vous donne une idée de la façon de faire en sorte que three.js rende à la demande plutôt qu'en continu. Les applications/pages qui rendent three.js à la demande ne sont pas aussi courantes que la plupart des pages utilisant three.js qui sont soit des jeux, soit de l'art animé en 3D, mais des exemples de pages qui pourraient mieux rendre à la demande seraient, par exemple, une visionneuse de carte, un éditeur 3D, un générateur de graphiques 3D, un catalogue de produits, etc...