Les applications Three.js utilisent souvent beaucoup de mémoire. Un modèle 3D peut occuper de 1 à 20 Mo de mémoire pour l'ensemble de ses sommets. Un modèle peut utiliser de nombreuses textures qui, même si elles sont compressées en fichiers jpg, doivent être décompressées pour être utilisées. Chaque texture 1024x1024 prend 4 à 6 Mo de mémoire.
La plupart des applications three.js chargent les ressources au moment de l'initialisation et les utilisent ensuite indéfiniment jusqu'à ce que la page soit fermée. Mais que se passe-t-il si vous souhaitez charger et modifier des ressources au fil du temps ?
Contrairement à la plupart des codes JavaScript, three.js ne peut pas nettoyer automatiquement ces ressources. Le navigateur les nettoiera si vous changez de page, mais sinon, c'est à vous de les gérer. C'est un problème lié à la conception de WebGL, et three.js n'a donc d'autre choix que de vous confier la responsabilité de libérer les ressources.
Vous libérez les ressources three.js en appelant la fonction dispose
sur
les textures,
les géométries, et les
matériaux.
Vous pourriez le faire manuellement. Au début, vous pourriez créer certaines de ces ressources
const boxGeometry = new THREE.BoxGeometry(...); const boxTexture = textureLoader.load(...); const boxMaterial = new THREE.MeshPhongMaterial({map: texture});
puis, lorsque vous avez terminé avec elles, vous les libéreriez
boxGeometry.dispose(); boxTexture.dispose(); boxMaterial.dispose();
À mesure que vous utilisez de plus en plus de ressources, cela deviendrait de plus en plus fastidieux.
Pour aider à réduire cette tâche fastidieuse, créons une classe pour suivre les ressources. Nous demanderons ensuite à cette classe de faire le nettoyage pour nous.
Voici une première ébauche d'une telle classe
class ResourceTracker { constructor() { this.resources = new Set(); } track(resource) { if (resource.dispose) { this.resources.add(resource); } return resource; } untrack(resource) { this.resources.delete(resource); } dispose() { for (const resource of this.resources) { resource.dispose(); } this.resources.clear(); } }
Utilisons cette classe avec le premier exemple de l'article sur les textures. Nous pouvons créer une instance de cette classe
const resTracker = new ResourceTracker();
et pour faciliter son utilisation, créons une fonction liée pour la méthode track
const resTracker = new ResourceTracker(); +const track = resTracker.track.bind(resTracker);
Maintenant, pour l'utiliser, il suffit d'appeler track
pour chaque géométrie, texture, et matériau
que nous créons
const boxWidth = 1; const boxHeight = 1; const boxDepth = 1; -const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth); +const geometry = track(new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth)); const cubes = []; // an array we can use to rotate the cubes const loader = new THREE.TextureLoader(); -const material = new THREE.MeshBasicMaterial({ - map: loader.load('resources/images/wall.jpg'), -}); +const material = track(new THREE.MeshBasicMaterial({ + map: track(loader.load('resources/images/wall.jpg')), +})); const cube = new THREE.Mesh(geometry, material); scene.add(cube); cubes.push(cube); // add to our list of cubes to rotate
Et ensuite, pour les libérer, nous voudrions retirer les cubes de la scène
et appeler resTracker.dispose
for (const cube of cubes) { scene.remove(cube); } cubes.length = 0; // clears the cubes array resTracker.dispose();
Cela fonctionnerait, mais je trouve fastidieux de devoir retirer les cubes de la
scène. Ajoutons cette fonctionnalité au ResourceTracker
.
class ResourceTracker { constructor() { this.resources = new Set(); } track(resource) { - if (resource.dispose) { + if (resource.dispose || resource instanceof THREE.Object3D) { this.resources.add(resource); } return resource; } untrack(resource) { this.resources.delete(resource); } dispose() { for (const resource of this.resources) { - resource.dispose(); + if (resource instanceof THREE.Object3D) { + if (resource.parent) { + resource.parent.remove(resource); + } + } + if (resource.dispose) { + resource.dispose(); + } + } this.resources.clear(); } }
Et maintenant nous pouvons suivre les cubes
const material = track(new THREE.MeshBasicMaterial({ map: track(loader.load('resources/images/wall.jpg')), })); const cube = track(new THREE.Mesh(geometry, material)); scene.add(cube); cubes.push(cube); // add to our list of cubes to rotate
Nous n'avons plus besoin du code pour retirer les cubes de la scène.
-for (const cube of cubes) { - scene.remove(cube); -} cubes.length = 0; // clears the cube array resTracker.dispose();
Organisons ce code afin de pouvoir rajouter le cube, la texture et le matériau.
const scene = new THREE.Scene(); *const cubes = []; // just an array we can use to rotate the cubes +function addStuffToScene() { const resTracker = new ResourceTracker(); const track = resTracker.track.bind(resTracker); const boxWidth = 1; const boxHeight = 1; const boxDepth = 1; const geometry = track(new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth)); const loader = new THREE.TextureLoader(); const material = track(new THREE.MeshBasicMaterial({ map: track(loader.load('resources/images/wall.jpg')), })); const cube = track(new THREE.Mesh(geometry, material)); scene.add(cube); cubes.push(cube); // add to our list of cubes to rotate + return resTracker; +}
Et ensuite, écrivons du code pour ajouter et supprimer des choses au fil du temps.
function waitSeconds(seconds = 0) { return new Promise(resolve => setTimeout(resolve, seconds * 1000)); } async function process() { for (;;) { const resTracker = addStuffToScene(); await wait(2); cubes.length = 0; // remove the cubes resTracker.dispose(); await wait(1); } } process();
Ce code va créer le cube, la texture et le matériau, attendre 2 secondes, puis les libérer et attendre 1 seconde et répéter.
Cela semble donc fonctionner.
Cependant, pour un fichier chargé, le travail est un peu plus conséquent. La plupart des chargeurs ne renvoient qu'un Object3D
comme racine de la hiérarchie des objets qu'ils chargent, nous devons donc découvrir toutes les ressources
qu'il contient.
Mettons à jour notre ResourceTracker
pour essayer de faire cela.
Nous allons d'abord vérifier si l'objet est un Object3D
, puis suivre sa géométrie, son matériau et ses enfants.
class ResourceTracker { constructor() { this.resources = new Set(); } track(resource) { if (resource.dispose || resource instanceof THREE.Object3D) { this.resources.add(resource); } + if (resource instanceof THREE.Object3D) { + this.track(resource.geometry); + this.track(resource.material); + this.track(resource.children); + } return resource; } ... }
Maintenant, comme resource.geometry
, resource.material
et resource.children
peuvent être nuls ou indéfinis, nous allons vérifier en haut de track
.
class ResourceTracker { constructor() { this.resources = new Set(); } track(resource) { + if (!resource) { + return resource; + } if (resource.dispose || resource instanceof THREE.Object3D) { this.resources.add(resource); } if (resource instanceof THREE.Object3D) { this.track(resource.geometry); this.track(resource.material); this.track(resource.children); } return resource; } ... }
De plus, comme resource.children
est un tableau et que resource.material
peut être
un tableau, vérifions s'il s'agit de tableaux.
class ResourceTracker { constructor() { this.resources = new Set(); } track(resource) { if (!resource) { return resource; } * // handle children and when material is an array of materials. * // Gérer les enfants et lorsque le matériau est un tableau de matériaux. if (Array.isArray(resource)) { resource.forEach(resource => this.track(resource)); return resource; } if (resource.dispose || resource instanceof THREE.Object3D) { this.resources.add(resource); } if (resource instanceof THREE.Object3D) { this.track(resource.geometry); this.track(resource.material); this.track(resource.children); } return resource; } ... }
Et enfin, nous devons parcourir les propriétés et les uniformes d'un matériau à la recherche de textures.
class ResourceTracker { constructor() { this.resources = new Set(); } track(resource) { if (!resource) { return resource; } * // handle children and when material is an array of materials or * // uniform is array of textures * // Gérer les enfants et lorsque le matériau est un tableau de matériaux ou * // qu'un uniforme est un tableau de textures if (Array.isArray(resource)) { resource.forEach(resource => this.track(resource)); return resource; } if (resource.dispose || resource instanceof THREE.Object3D) { this.resources.add(resource); } if (resource instanceof THREE.Object3D) { this.track(resource.geometry); this.track(resource.material); this.track(resource.children); - } + } else if (resource instanceof THREE.Material) { + // We have to check if there are any textures on the material + // Nous devons vérifier s'il y a des textures sur le matériau + for (const value of Object.values(resource)) { + if (value instanceof THREE.Texture) { + this.track(value); + } + } + // We also have to check if any uniforms reference textures or arrays of textures + // Nous devons aussi vérifier si des uniformes font référence à des textures ou à des tableaux de textures + if (resource.uniforms) { + for (const value of Object.values(resource.uniforms)) { + if (value) { + const uniformValue = value.value; + if (uniformValue instanceof THREE.Texture || + Array.isArray(uniformValue)) { + this.track(uniformValue); + } + } + } + } + } return resource; } ... }
Et avec cela, prenons un exemple de l'article sur le chargement de fichiers gltf et faisons-le charger et libérer des fichiers.
const gltfLoader = new GLTFLoader(); function loadGLTF(url) { return new Promise((resolve, reject) => { gltfLoader.load(url, resolve, undefined, reject); }); } function waitSeconds(seconds = 0) { return new Promise(resolve => setTimeout(resolve, seconds * 1000)); } const fileURLs = [ 'resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', 'resources/models/3dbustchallange_submission/scene.gltf', 'resources/models/mountain_landscape/scene.gltf', 'resources/models/simple_house_scene/scene.gltf', ]; async function loadFiles() { for (;;) { for (const url of fileURLs) { const resMgr = new ResourceTracker(); const track = resMgr.track.bind(resMgr); const gltf = await loadGLTF(url); const root = track(gltf.scene); scene.add(root); // compute the box that contains all the stuff // from root and below // calculer la boîte qui contient tout le contenu // à partir de la racine et en dessous const box = new THREE.Box3().setFromObject(root); const boxSize = box.getSize(new THREE.Vector3()).length(); const boxCenter = box.getCenter(new THREE.Vector3()); // set the camera to frame the box // définir la caméra pour cadrer la boîte frameArea(boxSize * 1.1, boxSize, boxCenter, camera); await waitSeconds(2); renderer.render(scene, camera); resMgr.dispose(); await waitSeconds(1); } } } loadFiles();
et nous obtenons
Quelques notes sur le code.
Si nous voulions charger 2 fichiers ou plus à la fois et les libérer à
tout moment, nous utiliserions un ResourceTracker
par fichier.
Ci-dessus, nous suivons uniquement gltf.scene
juste après le chargement.
Sur la base de notre implémentation actuelle de ResourceTracker
,
cela suivra toutes les ressources juste chargées. Si nous ajoutions plus
d'éléments à la scène, nous devrions décider de les suivre ou non.
Par exemple, disons qu'après avoir chargé un personnage, nous mettons un outil dans sa main en faisant de l'outil un enfant de sa main. Tel quel, cet outil ne sera pas libéré. Je suppose que la plupart du temps, c'est ce que nous voulons.
Cela soulève un point. À l'origine, lorsque j'ai écrit pour la première fois le ResourceTracker
ci-dessus, je parcourais tout à l'intérieur de la méthode dispose
au lieu de track
.
Ce n'est que plus tard, en réfléchissant au cas de l'outil en tant qu'enfant de la main ci-dessus,
qu'il est devenu clair que suivre exactement ce qu'il faut libérer dans track
était plus
flexible et sans doute plus correct, car nous pouvions alors suivre ce qui avait été chargé
depuis le fichier plutôt que de simplement libérer l'état du graphe de scène plus tard.
Honnêtement, je ne suis pas satisfait à 100% de ResourceTracker
. Faire les choses de cette
manière n'est pas courant dans les moteurs 3D. Nous ne devrions pas avoir à deviner quelles
ressources ont été chargées, nous devrions le savoir. Il serait bien que three.js
change de sorte que tous les chargeurs de fichiers renvoient un objet standard avec
des références à toutes les ressources chargées. Du moins pour l'instant,
three.js ne nous donne pas plus d'informations lors du chargement d'une scène, donc cette
solution semble fonctionner.
J'espère que vous trouverez cet exemple utile ou du moins une bonne référence pour ce qui est nécessaire pour libérer des ressources dans three.js