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