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() 源码。
原清理函数需要调整哪些地方?
- 同一个资源只释放一次。
APP.cubeMesh.geometry与APP.cubeGeom指向同一个对象,APP.cubeMesh.material与APP.cubeMat也是如此。重复调用不会补上内部 render target 的清理。 - 删除手动调用
renderer.properties.dispose()的代码。它只会清空内部属性映射。释放纹理和 render target 时需要从该映射读取 WebGL 资源句柄,提前清空会破坏后续清理所需的信息。WebGLProperties 源码、WebGLTextures 释放逻辑 - 让
renderer.dispose()处理内部清理流程,不需要提前调用renderer.renderLists.dispose()。 - 不要把
renderer.domElement设为null。从 DOM 中移除 canvas,再清除应用持有的引用即可。 - 检查 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。
也可以用其他版本运行相同的复现代码,核对源码和结果后再决定是否升级;这里没有确认某个后续版本已经修复。
备注:内容仅供参考。
