A-Frame ECS and Three.js: GPU Memory Disposal in Custom Components
0 reputation · 22 Sept 2021, 03:25 UTC
0 reputation · 22 Sept 2021, 03:25 UTC
A-Frame utilizes an Entity-Component System (ECS) to wrap Three.js objects. While the framework manages the lifecycle of HTML entities, the underlying Three.js geometries and materials reside in GPU memory and are not automatically garbage collected when an entity is removed from the DOM.
Custom components typically instantiate Three.js objects within the init handler. To prevent memory leaks, developers must manually call .dispose() on these resources. However, there is uncertainty regarding the optimal sequence for disposing of shared materials across multiple entities versus unique geometries tied to a single component instance.
Given the lifecycle sequence of init → update → tick → remove, how should the remove handler be structured to ensure all associated Three.js buffers are cleared without affecting other entities sharing the same material references?
33275 reputation · 22 Sept 2021, 03:56 UTC
The remove handler should explicitly dispose of every Three.js object that the component created, but only dispose of shared resources when no other entity still references them. A minimal pattern looks like this:
AFRAME.registerComponent('dynamic-geometry', {
schema: { /* your schema */ },
init: function () {
// Create geometry and material
this.geometry = new THREE.BufferGeometry();
this.material = new THREE.MeshBasicMaterial({ color: this.data.color });
this.mesh = new THREE.Mesh(this.geometry, this.material);
this.el.setObject3D('mesh', this.mesh);
// If you want to share a material, store a reference counter
if (!this.material.userData.refCount) {
this.material.userData.refCount = 0;
}
this.material.userData.refCount++;
},
remove: function () {
// 1. Dispose geometry – unique to this entity
if (this.geometry) {
this.geometry.dispose();
this.geometry = null;
}
// 2. Dispose material only if this was the last user
if (this.material) {
this.material.userData.refCount--;
if (this.material.userData.refCount === 0) {
this.material.dispose();
}
this.material = null;
}
// 3. Remove the mesh from the entity
this.el.removeObject3D('mesh');
}
});
.dispose() frees GPU buffers, textures, and shader programs. Simply dropping references does not reclaim GPU memory..dispose() on custom Three.js objects.When a material is shared (e.g., you pass the same material instance to several entities), you must track how many entities use it. The snippet above uses a refCount stored in userData. If you prefer a global registry, create a lightweight map keyed by material UUID.
geometry, material, texture, and any ShaderMaterial programs.removeObject3D before disposing.renderer.info.memory or the browser’s Performance panel after removal.Do you create a single material instance that is reused across multiple entities, or does each entity create its own material?
Use comments to ask for clarification. Post a solution as an answer.
33,275 reputation · 22 Sept 2021, 14:36 UTC
While reference counting effectively manages the material itself, it is important to remember that THREE.Material often holds references to THREE.Texture objects. Calling material.dispose() does not automatically dispose of the associated textures.
To ensure a complete cleanup, the remove handler should also check for textures. For materials with multiple maps (e.g., diffuse, normal, and roughness maps), you can iterate through the material properties:
// Inside the refCount === 0 block
const textures = [];
for (const key in this.material) {
if (this.material[key] && this.material[key].isTexture) {
textures.push(this.material[key]);
}
}
textures.forEach(t => t.dispose());
this.material.dispose();
Verification of this process can be performed by monitoring renderer.info.memory.textures before and after the removal of entities to confirm the count actually decreases.