# 清除

Three.js应用经常使用大量的内存。一个3D模型的所有节点，可能占用1-20M内存。 一个模型可能会使用很多纹理，即使它们被压缩成了图片文件，也必须被展开成为未压缩的形态来使用。每个 1024 x 1024 大小的纹理会占用4-5M内存。

大多数的three.js应用在初始化的时候加载资源，并且一直使用这些资源直到页面关闭。但是，如果你想随时间的变动加载和改变资源怎么办呢？

不像大多数的JavaScript库，three.js不能自动的清除这些资源。 如果你切换页面，浏览器会清除这些资源，其它时候如何管理它们取决于你。这是WebGL设计的问题，three.js没有追索权只能将释放资源的责任托付给你。

通过在纹理、 图元和 材质对象上调用`dispose`方法来释放资源

你可以手动来处理。起初，你可能创建了一些资源。

    const boxGeometry = new THREE.BoxGeometry(...);
    const boxTexture = textureLoader.load(...);
    const boxMaterial = new THREE.MeshPhongMaterial({map: texture});

然后，当你处理完了它们，可以释放它们

    boxGeometry.dispose();
    boxTexture.dispose();
    boxMaterial.dispose();

随着你使用越来越多的资源，这将会变得越来越乏味。

为了减少一些乏味的工作，让我们创建一个类来跟踪这些资源。我们会请求这个类来帮我们做清除的工作。

这个类一开始是这个样子。

    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();
      }
    }

让我们在纹理文章中的例子中使用这个类。我们可以创建一个这个类的实例。

    const resTracker = new ResourceTracker();

然后为了让这个类的使用更加地简单，让我们来为`track`方法创建一个绑定函数。

    const resTracker = new ResourceTracker();
    +const track = resTracker.track.bind(resTracker);

现在，我们只需要在我们创建的每个图元、纹理、材质对象上调用`track`方法就可以使用它。

    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

然后，我们从场景中移除这些立方体，再然后调用`resTracker.dispose`来释放它们。

    for (const cube of cubes) {
      scene.remove(cube);
    }
    cubes.length = 0;  // clears the cubes array
    resTracker.dispose();

这起作用了，但是我发现必须从场景中移除立方体有些乏味。让我们给`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();
      }
    }

现在我们可以跟踪这些立方体了

    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

我们不再需要编码从场景中移除这些立方体了。

    -for (const cube of cubes) {
    -  scene.remove(cube);
    -}
    cubes.length = 0;  // clears the cube array
    resTracker.dispose();

让我们来调整一下代码，这样我们可以重新添加立方体、纹理和材质。

    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;
    +}

让我们来编写一些随着时间变动添加和移除物体的代码。

    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();

代码将会创建立方体、纹理和材质，等待2秒，然后释放它们，然后等待1秒，重复这个过程。

点击此处打开独立窗口

这好像能工作了。

对于加载文件来说，还需要一点额外的工作。大多数的加载器仅仅返回一个`Object3D` 对象，作为它加载的层次对象的根节点，因此我们需要去发现所有的这些资源是哪些。

让我们更新`ResourceTracker`来试着去实现它。

首先，我们来检测这个物体是否是一个`Object3D`，然后跟踪它的图元、材质和子对象。

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

现在，因为任意的`resource.geometry`、`resource.material`和`resource.children`有可能为null或undefined， 我们将在`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;
      }
      ...
    }

同时，因为`resource.children`是一个数组， 同时`resource.material`也可能是数组，让我们对数组做检测。

    class ResourceTracker {
      constructor() {
        this.resources = new Set();
      }
      track(resource) {
        if (!resource) {
          return resource;
        }

    +    // handle children and when material is an array of materials.
    +    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;
      }
      ...
    }

最后我们需要遍历这些材质的属性和uniforms来寻找纹理。

    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
        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
    +      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
    +      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;
      }
      ...
    }

让我们来使用“加载gltf文件文章“中的例子，让它能够加载和释放文件。

    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
          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
          frameArea(boxSize * 1.1, boxSize, boxCenter, camera);

          await waitSeconds(2);
          renderer.render(scene, camera);

          resMgr.dispose();

          await waitSeconds(1);

        }
      }
    }
    loadFiles();

然后我们得到了

点击此处在独立窗口中打开

关于代码的一些注释。

如果我们想要加载2个或者更多的文件，并且想要随时地释放它们，我们将要每个文件使用一个`ResourceTracker`。

在上面中，我们只是在场景加载之后跟踪了`gltf.scene`。 根据`ResourceTracker`的当前实现版本，它会跟踪刚刚加载的所有资源。如果我们向场景中添加了更多的资源，我们需要决定是否要跟踪它们。

举例来说，在我们加载了一个角色之后，我们把一个工具放入它的手中，这是通过把工具成为手的子对象来实现的。因此工具将不会被释放。我猜更多的时候，这不是我们想要的。

这带来了一个问题。当我起初写上面的`ResourceTracker`的时候， 我是在`dispose`方法中遍历所有对象而不是在 `track`方法中。稍后，我就想到了上面的成为手的子对象的工具的这个例子， 在`track`方法中确切地跟踪哪些对象需要被释放更加地灵活，按理来说也更加地准确，因为我们可以跟踪从文件中加载了什么，而不是稍后从资源图中释放状态。

诚实地说，我对ResourceTracker并不是100%满意。在3D引擎中，做这样的事情并不是很常见。我们不应该去猜测什么资源被加载了，我们应该知道。 如果three.js能做出改变，所有的文件加载器返回能够引用所有加载资源的标准对象，就太好了。至少在这个时候，three.js在场景加载的时候并没有提供给我们其它的信息。因此，这个方法是有效的。

我希望这个例子能给你带来帮助或者至少成为在three.js中如何释放资源的一份好的参考