PAYATHON 2026

MeshPhysicalMaterial 为何无法完全释放?

数据小许

结论

这个现象涉及 three.js r180 内部 transmission render target 的生命周期。renderer.info.memory.textures 没有归零,并不能说明 MeshPhysicalMaterial.dispose() 调用失败。

r180 在 transmission 渲染时会创建内部 WebGLRenderTarget。释放材质不会销毁它;该版本的 WebGLRenderStates.dispose() 也只会替换保存状态的 WeakMap,不会逐个调用内部目标的 dispose(),因此存在显式清理的缺口。r180 渲染器源码、r180 渲染状态源码

如果像示例中那样彻底结束渲染,可以释放应用资源、销毁 renderer,并在扩展可用时主动丢失 context。context 丢失后,旧 renderer 的统计值仍不一定归零。

为什么开启 transmission 后会多出纹理?

r180 的 renderTransmissionPass() 会创建额外的渲染目标,用来生成 transmission 所需的背景图像。这个目标保存在当前渲染状态的 transmissionRenderTarget[camera.id] 中,不属于 cubeMat.map 之类的材质贴图。

遍历材质属性或反复调用 material.dispose() 都无法直接清理这个目标。创建逻辑见 renderTransmissionPass() 源码。

原清理函数需要调整哪些地方?

  1. 同一个资源只释放一次。APP.cubeMesh.geometry 与 APP.cubeGeom 指向同一个对象,APP.cubeMesh.material 与 APP.cubeMat 也是如此。重复调用不会补上内部 render target 的清理。
  2. 删除手动调用 renderer.properties.dispose() 的代码。它只会清空内部属性映射。释放纹理和 render target 时需要从该映射读取 WebGL 资源句柄,提前清空会破坏后续清理所需的信息。WebGLProperties 源码、WebGLTextures 释放逻辑
  3. 让 renderer.dispose() 处理内部清理流程,不需要提前调用 renderer.renderLists.dispose()。
  4. 不要把 renderer.domElement 设为 null。从 DOM 中移除 canvas,再清除应用持有的引用即可。
  5. 检查 context 丢失扩展是否存在。原代码直接执行 ext.loseContext(),扩展不可用时会抛出异常。

彻底销毁示例时怎样清理?

下面的函数可以替换原来的 dispose()。使用前提是 renderer 和 WebGL context 后续都不再使用,且 geometry、material 没有与其他对象共享。

function dispose() {
  if (!APP || !APP.renderer) return;

  cancelAnimationFrame(rafId);
  rafId = null;

  const renderer = APP.renderer;
  const canvas = renderer.domElement;
  const gl = renderer.getContext();
  const loseContext = gl.getExtension('WEBGL_lose_context');

  console.log('before', JSON.stringify(renderer.info.memory));

  APP.scene.remove(APP.cubeMesh);
  APP.cubeGeom.dispose();
  APP.cubeMat.dispose();

  renderer.dispose();

  // 仅在不再使用此 context 时执行。
  if (loseContext) {
    loseContext.loseContext();
  }

  canvas.remove();

  // 这是旧 renderer 的内部计数,不是 context 销毁后的显存读数。
  console.log('after', JSON.stringify(renderer.info.memory));

  APP.cubeMesh = null;
  APP.cubeGeom = null;
  APP.cubeMat = null;
  APP.scene = null;
  APP.camera = null;
  APP.renderer = null;
  APP = null;
}

这段代码简化了销毁流程,但没有修补 r180 内部 render target 的显式释放逻辑,因此不能保证执行后 textures 一定变成 0。

为什么丢失 context 后统计还可能不归零?

renderer.info.memory 是 three.js 维护的对象计数,并非浏览器实时提供的显存用量。纹理释放路径会显式减少计数,而 context 丢失不会自动重写这些 JavaScript 数值。renderer.info.reset() 也只重置渲染统计,不重置 memory。WebGLTextures 源码、WebGLInfo 源码

旧 renderer 中残留的计数不能单独证明 context 丢失后仍占用对应的 GPU 资源。也不要通过手动将计数改为 0 来判断问题已经解决。

如果需要继续复用 renderer 呢?

继续渲染时不要主动丢失 context。对于 r180,可以在同一个 renderer 上重复加载、卸载场景,观察纹理数量是否持续增加。一次残留和持续增长需要分别判断。

如果确认内部 transmission render target 持续累积,就需要修补其生命周期:在渲染器内部跟踪创建的目标,在适当的销毁阶段调用每个目标的 dispose(),并确保调用发生在 properties.dispose() 之前。仅清空 WeakMap 无法替代这一步。这个目标没有可供应用直接清理的公开属性,不应假设存在 material.transmissionRenderTarget。

也可以用其他版本运行相同的复现代码,核对源码和结果后再决定是否升级;这里没有确认某个后续版本已经修复。

备注:内容仅供参考。