Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 42 additions & 3 deletions src/systems/material.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ var THREE = require('../lib/three');
var utils = require('../utils/');
var isHLS = require('../utils/material').isHLS;

var bind = utils.bind;
var debug = utils.debug;
var error = debug('components:texture:error');
var TextureLoader = new THREE.TextureLoader();
Expand All @@ -14,15 +15,24 @@ TextureLoader.setCrossOrigin('anonymous');
* System for material component.
* Handle material registration, updates (for fog), and texture caching.
*
* @member materials {object} - Registered materials.
* @member textureCache {object} - Texture cache for:
* @member {object} materials - Registered materials.
* @member {object} textureCounts - Number of times each texture is used. Tracked
* separately from textureCache, because the cache (1) is populated in
* multiple places, and (2) may be cleared at any time.
* @member {object} textureCache - Texture cache for:
* - Images: textureCache has mapping of src -> repeat -> cached three.js texture.
* - Videos: textureCache has mapping of videoElement -> cached three.js texture.
*/
module.exports.System = registerSystem('material', {
init: function () {
this.materials = {};
this.textureCounts = {};
this.textureCache = {};

this.sceneEl.addEventListener(
'materialtextureloaded',
bind(this.onMaterialTextureLoaded, this)
);
},

clearTextureCache: function () {
Expand Down Expand Up @@ -189,12 +199,26 @@ module.exports.System = registerSystem('material', {
},

/**
* Stop tracking material.
* Stop tracking material, and dispose of any textures not being used by
* another material component.
*
* @param {object} material
*/
unregisterMaterial: function (material) {
delete this.materials[material.uuid];

// If any textures on this material are no longer in use, dispose of them.
var textureCounts = this.textureCounts;
Object.keys(material)
.filter(function (propName) {
return material[propName] && material[propName].isTexture;
})
.forEach(function (mapName) {
textureCounts[material[mapName].uuid]--;
if (textureCounts[material[mapName].uuid] <= 0) {
material[mapName].dispose();
}
});
},

/**
Expand All @@ -205,6 +229,21 @@ module.exports.System = registerSystem('material', {
Object.keys(materials).forEach(function (uuid) {
materials[uuid].needsUpdate = true;
});
},

/**
* Track textures used by material components, so that they can be safely
* disposed when no longer in use. Textures must be registered here, and not
* through registerMaterial(), because textures may not be attached at the
* time the material is registered.
*
* @param {Event} e
*/
onMaterialTextureLoaded: function (e) {
if (!this.textureCounts[e.detail.texture.uuid]) {
this.textureCounts[e.detail.texture.uuid] = 0;
}
this.textureCounts[e.detail.texture.uuid]++;
}
});

Expand Down
24 changes: 24 additions & 0 deletions tests/systems/material.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,30 @@ suite('material system', function () {
});
});

suite('unregisterMaterial', function () {
test('disposes of unused textures', function () {
var el = this.el;
var sinon = this.sinon;
var system = el.sceneEl.systems.material;
var texture1 = {uuid: 'tex1', isTexture: true, dispose: sinon.spy()};
var texture2 = {uuid: 'tex2', isTexture: true, dispose: sinon.spy()};
var material1 = {fooMap: texture1, barMap: texture2, dispose: sinon.spy()};
var material2 = {fooMap: texture1, dispose: sinon.spy()};

el.emit('materialtextureloaded', {texture: texture1});
el.emit('materialtextureloaded', {texture: texture1});
el.emit('materialtextureloaded', {texture: texture2});

system.unregisterMaterial(material1);
assert.notOk(texture1.dispose.called);
assert.ok(texture2.dispose.called);

system.unregisterMaterial(material2);
assert.ok(texture1.dispose.called);
assert.equal(texture2.dispose.callCount, 1);
});
});

suite('texture caching', function () {
setup(function () {
this.system.clearTextureCache();
Expand Down