# 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.

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

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.

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

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.

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

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...