From 674458275a93b04150caf54181fe918be027aadf Mon Sep 17 00:00:00 2001 From: Noeri Huisman Date: Sat, 10 Feb 2024 15:50:20 +0100 Subject: [PATCH] Cache Source instead of Texture instances in material system --- src/components/material.js | 9 + src/core/shader.js | 3 - src/systems/material.js | 265 ++++++++---------------------- src/systems/renderer.js | 5 +- src/utils/material.js | 105 ++++++++++-- tests/components/material.test.js | 44 ++++- tests/shaders/phong.test.js | 2 +- tests/systems/material.test.js | 169 ++++++------------- 8 files changed, 266 insertions(+), 336 deletions(-) diff --git a/src/components/material.js b/src/components/material.js index bd17ea0cf44..118905aecd7 100644 --- a/src/components/material.js +++ b/src/components/material.js @@ -250,4 +250,13 @@ function parseBlending (blending) { function disposeMaterial (material, system) { material.dispose(); system.unregisterMaterial(material); + + // Dispose textures on this material + Object.keys(material) + .filter(function (propName) { + return material[propName] && material[propName].isTexture; + }) + .forEach(function (mapName) { + material[mapName].dispose(); + }); } diff --git a/src/core/shader.js b/src/core/shader.js index e28b977a558..14173943d00 100755 --- a/src/core/shader.js +++ b/src/core/shader.js @@ -135,9 +135,6 @@ Shader.prototype = { color = new THREE.Color(value); return new THREE.Vector3(color.r, color.g, color.b); } - case 'map': { - return THREE.ImageUtils.loadTexture(value); - } default: { return value; } diff --git a/src/systems/material.js b/src/systems/material.js index bf9f994c6d2..7b902a801d7 100755 --- a/src/systems/material.js +++ b/src/systems/material.js @@ -2,71 +2,78 @@ var registerSystem = require('../core/system').registerSystem; var THREE = require('../lib/three'); var utils = require('../utils/'); var setTextureProperties = require('../utils/material').setTextureProperties; +var createCompatibleTexture = require('../utils/material').createCompatibleTexture; -var bind = utils.bind; var debug = utils.debug; var error = debug('components:texture:error'); -var TextureLoader = new THREE.TextureLoader(); var warn = debug('components:texture:warn'); - -TextureLoader.setCrossOrigin('anonymous'); +var ImageLoader = new THREE.ImageLoader(); /** * System for material component. * Handle material registration, updates (for fog), and texture caching. * * @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. + * @member {object} sourceCache - Texture source cache for, Image, Video and Canvas sources */ module.exports.System = registerSystem('material', { init: function () { this.materials = {}; - this.textureCounts = {}; - this.textureCache = {}; - - this.sceneEl.addEventListener( - 'materialtextureloaded', - bind(this.onMaterialTextureLoaded, this) - ); + this.sourceCache = {}; }, - clearTextureCache: function () { - this.textureCache = {}; + clearTextureSourceCache: function () { + this.sourceCache = {}; }, /** - * Determine whether `src` is a image or video. Then try to load the asset, then call back. + * Loads and creates a texture for a given `src`. * - * @param {string, or element} src - Texture URL or element. - * @param {string} data - Relevant texture data used for caching. - * @param {function} cb - Callback to pass texture to. + * @param {string, or element} src - URL or element + * @param {object} data - Relevant texture properties + * @param {function} cb - Callback to pass texture to */ loadTexture: function (src, data, cb) { + this.loadTextureSource(src, function sourceLoaded (source) { + var texture = createCompatibleTexture(source); + setTextureProperties(texture, data); + cb(texture); + }); + }, + + /** + * Determine whether `src` is an image or video. Then try to load the asset, then call back. + * + * @param {string, or element} src - URL or element. + * @param {function} cb - Callback to pass texture source to. + */ + loadTextureSource: function (src, cb) { var self = this; + var sourceCache = this.sourceCache; + + var hash = this.hash(src); + if (sourceCache[hash]) { + sourceCache[hash].then(cb); + return; + } // Canvas. if (src.tagName === 'CANVAS') { - this.loadCanvas(src, data, cb); + sourceLoaded(new THREE.Source(src)); return; } - // Video element. - if (src.tagName === 'VIDEO') { - if (!src.src && !src.srcObject && !src.childElementCount) { - warn('Video element was defined with neither `source` elements nor `src` / `srcObject` attributes.'); - } - this.loadVideo(src, data, cb); - return; + sourceLoaded(new Promise(doSourceLoad)); + function doSourceLoad (resolve, reject) { + utils.srcLoader.validateSrc(src, loadImageCb, loadVideoCb); + function loadImageCb (src) { self.loadImage(src, resolve); } + function loadVideoCb (src) { self.loadVideo(src, resolve); } } - utils.srcLoader.validateSrc(src, loadImageCb, loadVideoCb); - function loadImageCb (src) { self.loadImage(src, data, cb); } - function loadVideoCb (src) { self.loadVideo(src, data, cb); } + function sourceLoaded (sourcePromise) { + sourceCache[hash] = Promise.resolve(sourcePromise); + sourceCache[hash].then(cb); + } }, /** @@ -82,8 +89,8 @@ module.exports.System = registerSystem('material', { cube.colorSpace = THREE.SRGBColorSpace; function loadSide (index) { - self.loadTexture(srcs[index], {src: srcs[index]}, function (texture) { - cube.images[index] = texture.image; + self.loadTextureSource(srcs[index], function (source) { + cube.images[index] = source; loaded++; if (loaded === 6) { cube.needsUpdate = true; @@ -106,108 +113,54 @@ module.exports.System = registerSystem('material', { * High-level function for loading image textures (THREE.Texture). * * @param {Element|string} src - Texture source. - * @param {object} data - Texture data. * @param {function} cb - Callback to pass texture to. */ - loadImage: function (src, data, handleImageTextureLoaded) { - var hash = this.hash(data); - var textureCache = this.textureCache; - - // Texture already being loaded or already loaded. Wait on promise. - if (textureCache[hash]) { - textureCache[hash].then(handleImageTextureLoaded); + loadImage: function (src, cb) { + // Image element provided + if (typeof src !== 'string') { + cb(new THREE.Source(src)); return; } - // Texture not yet being loaded. Start loading it. - textureCache[hash] = loadImageTexture(src, data); - textureCache[hash].then(handleImageTextureLoaded); + cb(loadImageUrl(src)); }, /** - * High-level function for loading canvas textures (THREE.Texture). - * - * @param {Element|string} src - Texture source. - * @param {object} data - Texture data. - * @param {function} cb - Callback to pass texture to. - */ - loadCanvas: function (src, data, cb) { - var texture; - texture = new THREE.CanvasTexture(src); - setTextureProperties(texture, data); - cb(texture); - }, - - /** * Load video texture (THREE.VideoTexture). * Which is just an image texture that RAFs + needsUpdate. * Note that creating a video texture is synchronous unlike loading an image texture. * Made asynchronous to be consistent with image textures. * * @param {Element|string} src - Texture source. - * @param {object} data - Texture data. * @param {function} cb - Callback to pass texture to. */ - loadVideo: function (src, data, cb) { - var hash; - var texture; - var textureCache = this.textureCache; + loadVideo: function (src, cb) { var videoEl; - var videoTextureResult; - - function handleVideoTextureLoaded (result) { - result.texture.needsUpdate = true; - cb(result.texture, result.videoEl); - } // Video element provided. if (typeof src !== 'string') { // Check cache before creating texture. videoEl = src; - hash = this.hashVideo(data, videoEl); - if (textureCache[hash]) { - textureCache[hash].then(handleVideoTextureLoaded); - return; - } - // If not in cache, fix up the attributes then start to create the texture. + + // Fix up the attributes then start to create the texture. fixVideoAttributes(videoEl); } // Only URL provided. Use video element to create texture. - videoEl = videoEl || createVideoEl(src, data.width, data.height); - - // Generated video element already cached. Use that. - hash = this.hashVideo(data, videoEl); - if (textureCache[hash]) { - textureCache[hash].then(handleVideoTextureLoaded); - return; - } + videoEl = videoEl || createVideoEl(src); - // Create new video texture. - texture = new THREE.VideoTexture(videoEl); - texture.minFilter = THREE.LinearFilter; - setTextureProperties(texture, data); - - // Cache as promise to be consistent with image texture caching. - videoTextureResult = {texture: texture, videoEl: videoEl}; - textureCache[hash] = Promise.resolve(videoTextureResult); - handleVideoTextureLoaded(videoTextureResult); + cb(new THREE.Source(videoEl)); }, /** - * Create a hash of the material properties for texture cache key. + * Create a hash for a given source. */ - hash: function (data) { - if (data.src.tagName) { - // Since `data.src` can be an element, parse out the string if necessary for the hash. - data = utils.extendDeep({}, data); - data.src = data.src.src; + hash: function (src) { + if (src.tagName) { + // Prefer element's ID or source, otherwise fallback to the element itself + return src.id || src.src || src; } - return JSON.stringify(data); - }, - - hashVideo: function (data, videoEl) { - return calculateVideoCacheHash(data, videoEl); + return src; }, /** @@ -227,104 +180,34 @@ module.exports.System = registerSystem('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(); - } - }); - }, - - /** - * 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]++; } }); /** - * Calculates consistent hash from a video element using its attributes. - * If the video element has an ID, use that. - * Else build a hash that looks like `src:myvideo.mp4;height:200;width:400;`. - * - * @param data {object} - Texture data such as repeat. - * @param videoEl {Element} - Video element. - * @returns {string} - */ -function calculateVideoCacheHash (data, videoEl) { - var i; - var id = videoEl.getAttribute('id'); - var hash; - var videoAttributes; - - if (id) { return id; } - - // Calculate hash using sorted video attributes. - hash = ''; - videoAttributes = data || {}; - for (i = 0; i < videoEl.attributes.length; i++) { - videoAttributes[videoEl.attributes[i].name] = videoEl.attributes[i].value; - } - Object.keys(videoAttributes).sort().forEach(function (name) { - hash += name + ':' + videoAttributes[name] + ';'; - }); - - return hash; -} - -/** - * Load image texture. + * Load image from a given URL. * * @private - * @param {string|object} src - An element or url to an image file. - * @param {object} data - Data to set texture properties like `repeat`. + * @param {string} src - An url to an image file. * @returns {Promise} Resolves once texture is loaded. */ -function loadImageTexture (src, data) { - return new Promise(doLoadImageTexture); - - function doLoadImageTexture (resolve, reject) { - var isEl = typeof src !== 'string'; - - function resolveTexture (texture) { - setTextureProperties(texture, data); - texture.needsUpdate = true; - resolve(texture); - } - - // Create texture from an element. - if (isEl) { - resolveTexture(new THREE.Texture(src)); - return; - } +function loadImageUrl (src) { + return new Promise(doLoadImageUrl); + function doLoadImageUrl (resolve, reject) { // Request and load texture from src string. THREE will create underlying element. - // Use THREE.TextureLoader (src, onLoad, onProgress, onError) to load texture. - TextureLoader.load( + ImageLoader.load( src, - resolveTexture, + resolveSource, function () { /* no-op */ }, function (xhr) { error('`$s` could not be fetched (Error code: %s; Response: %s)', xhr.status, xhr.statusText); } ); + + function resolveSource (data) { + resolve(new THREE.Source(data)); + } } } @@ -332,14 +215,10 @@ function loadImageTexture (src, data) { * Create video element to be used as a texture. * * @param {string} src - Url to a video file. - * @param {number} width - Width of the video. - * @param {number} height - Height of the video. * @returns {Element} Video element. */ -function createVideoEl (src, width, height) { +function createVideoEl (src) { var videoEl = document.createElement('video'); - videoEl.width = width; - videoEl.height = height; // Support inline videos for iOS webviews. videoEl.setAttribute('playsinline', ''); videoEl.setAttribute('webkit-playsinline', ''); @@ -347,7 +226,7 @@ function createVideoEl (src, width, height) { videoEl.loop = true; videoEl.crossOrigin = 'anonymous'; videoEl.addEventListener('error', function () { - warn('`$s` is not a valid video', src); + warn('`%s` is not a valid video', src); }, true); videoEl.src = src; return videoEl; diff --git a/src/systems/renderer.js b/src/systems/renderer.js index 758e2f8410b..58cebb2914a 100644 --- a/src/systems/renderer.js +++ b/src/systems/renderer.js @@ -78,8 +78,11 @@ module.exports.System = registerSystem('renderer', { applyColorCorrection: function (texture) { if (!this.data.colorManagement || !texture) { return; - } else if (texture.isTexture) { + } + + if (texture.isTexture && texture.colorSpace !== THREE.SRGBColorSpace) { texture.colorSpace = THREE.SRGBColorSpace; + texture.needsUpdate = true; } }, diff --git a/src/utils/material.js b/src/utils/material.js index df7189f8e75..fe93cbd34f9 100644 --- a/src/utils/material.js +++ b/src/utils/material.js @@ -1,3 +1,4 @@ +/* global HTMLCanvasElement, HTMLImageElement, HTMLVideoElement */ var THREE = require('../lib/three'); var srcLoader = require('./src-loader'); var debug = require('./debug'); @@ -14,7 +15,7 @@ var COLOR_MAPS = new Set([ * Set texture properties such as repeat and offset. * * @param {object} data - With keys like `repeat`. -*/ + */ function setTextureProperties (texture, data) { var offset = data.offset || {x: 0, y: 0}; var repeat = data.repeat || {x: 1, y: 1}; @@ -79,7 +80,7 @@ module.exports.updateMapMaterialFromData = function (materialName, dataName, sha if (!src) { // Forget the prior material src. delete shader.materialSrcs[materialName]; - // Remove the texture. + // Remove the texture from the material. setMap(null); return; } @@ -97,24 +98,55 @@ module.exports.updateMapMaterialFromData = function (materialName, dataName, sha // If the new material src is already a texture, just use it. if (src instanceof THREE.Texture) { setMap(src); } else { - // Load texture for the new material src. + // Load texture source for the new material src. // (And check if we should still use it once available in callback.) - el.sceneEl.systems.material.loadTexture(src, - {src: src, repeat: data.repeat, offset: data.offset, npot: data.npot, anisotropy: data.anisotropy}, - checkSetMap); + el.sceneEl.systems.material.loadTextureSource(src, updateTexture); } - function checkSetMap (texture) { + function updateTexture (source) { // If the source has been changed, don't use loaded texture. if (shader.materialSrcs[materialName] !== src) { return; } + + var texture = material[materialName]; + + // Handle removal or texture type change + if (texture && (source === null || !isCompatibleTexture(texture, source))) { + texture = null; + } + + // Create texture if needed + if (!texture && source) { + texture = createCompatibleTexture(source); + } + + // Update texture source and properties + if (texture) { + if (texture.source !== source) { + texture.source = source; + texture.needsUpdate = true; + } + if (COLOR_MAPS.has(materialName)) { + rendererSystem.applyColorCorrection(texture); + } + setTextureProperties(texture, data); + } + + // Set map property on the material setMap(texture); } function setMap (texture) { - material[materialName] = texture; - if (texture && COLOR_MAPS.has(materialName)) { - rendererSystem.applyColorCorrection(texture); + // Nothing to do if texture is the same + if (material[materialName] === texture) { + return; + } + + // Dispose old texture if present + if (material[materialName]) { + material[materialName].dispose(); } + + material[materialName] = texture; material.needsUpdate = true; handleTextureEvents(el, texture); } @@ -233,12 +265,59 @@ function handleTextureEvents (el, texture) { // Video events. if (!texture.image || texture.image.tagName !== 'VIDEO') { return; } - texture.image.addEventListener('loadeddata', function emitVideoTextureLoadedDataAll () { + texture.image.addEventListener('loadeddata', emitVideoTextureLoadedDataAll); + texture.image.addEventListener('ended', emitVideoTextureEndedAll); + function emitVideoTextureLoadedDataAll () { el.emit('materialvideoloadeddata', {src: texture.image, texture: texture}); - }); - texture.image.addEventListener('ended', function emitVideoTextureEndedAll () { + } + function emitVideoTextureEndedAll () { // Works for non-looping videos only. el.emit('materialvideoended', {src: texture.image, texture: texture}); + } + + // Video source can outlive texture, so cleanup event listeners when texture is disposed + texture.addEventListener('dispose', function cleanupListeners () { + texture.image.removeEventListener('loadeddata', emitVideoTextureLoadedDataAll); + texture.image.removeEventListener('ended', emitVideoTextureEndedAll); }); } module.exports.handleTextureEvents = handleTextureEvents; + +/** + * Checks if a given texture type is compatible with a given source. + * + * @param {THREE.Texture} texture - The texture to check compatibility with + * @param {THREE.Source} source - The source to check compatibility with + * @returns {boolean} True if the texture is compatible with the source, false otherwise + */ +function isCompatibleTexture (texture, source) { + if (source.data instanceof HTMLCanvasElement) { + return texture.isCanvasTexture; + } + + if (source.data instanceof HTMLVideoElement) { + // VideoTexture can't have its source changed after initial user + return texture.isVideoTexture && texture.source === source; + } + + return texture.isTexture && !texture.isCanvasTexture && !texture.isVideoTexture; +} +module.exports.isCompatibleTexture = isCompatibleTexture; + +function createCompatibleTexture (source) { + var texture; + + if (source.data instanceof HTMLCanvasElement) { + texture = new THREE.CanvasTexture(); + } else if (source.data instanceof HTMLVideoElement) { + // Pass underlying video to constructor to ensure requestVideoFrameCallback is setup + texture = new THREE.VideoTexture(source.data); + } else { + texture = new THREE.Texture(); + } + + texture.source = source; + texture.needsUpdate = true; + return texture; +} +module.exports.createCompatibleTexture = createCompatibleTexture; diff --git a/tests/components/material.test.js b/tests/components/material.test.js index 487a2394d4b..7a86c1ee2ff 100644 --- a/tests/components/material.test.js +++ b/tests/components/material.test.js @@ -44,6 +44,32 @@ suite('material', function () { assert.ok(disposeSpy.called); }); + test('disposes material when removing material', function () { + var material = el.getObject3D('mesh').material; + var disposeSpy = this.sinon.spy(material, 'dispose'); + el.removeAttribute('material'); + assert.ok(disposeSpy.called); + }); + + test('disposes textures when removing material', function () { + var material = el.getObject3D('mesh').material; + var texture1 = {uuid: 'tex1', isTexture: true, dispose: sinon.spy()}; + var texture2 = {uuid: 'tex2', isTexture: true, dispose: sinon.spy()}; + material.map = texture1; + material.normalMap = texture2; + el.removeAttribute('material'); + assert.ok(texture1.dispose.called); + assert.ok(texture2.dispose.called); + }); + + test('disposes texture when removing texture', function () { + var material = el.getObject3D('mesh').material; + var texture1 = {uuid: 'tex1', isTexture: true, dispose: sinon.spy()}; + material.map = texture1; + el.setAttribute('material', 'map', ''); + assert.ok(texture1.dispose.called); + }); + test('defaults to standard material', function () { el.removeAttribute('material'); // setup creates a non-default component el.setAttribute('material', ''); @@ -158,11 +184,11 @@ suite('material', function () { }); test('invokes XHR if not cached', function (done) { - var textureLoaderSpy = this.sinon.spy(THREE.TextureLoader.prototype, 'load'); + var imageLoaderSpy = this.sinon.spy(THREE.ImageLoader.prototype, 'load'); el.addEventListener('materialtextureloaded', function () { - assert.ok(textureLoaderSpy.called); + assert.ok(imageLoaderSpy.called); assert.ok(IMG_SRC in THREE.Cache.files); - THREE.TextureLoader.prototype.load.restore(); + THREE.ImageLoader.prototype.load.restore(); done(); }); el.setAttribute('material', 'src', IMG_SRC); @@ -247,14 +273,14 @@ suite('material', function () { assert.equal(el.getObject3D('mesh').material.side, THREE.DoubleSide); }); - test('sets material.needsUpdate true if side switchs from/to double', function () { + test('sets material.needsUpdate true if side switches from/to double', function () { var oldMaterialVersion = el.getObject3D('mesh').material.version; el.setAttribute('material', 'side: front'); assert.equal(el.getObject3D('mesh').material.version, oldMaterialVersion); el.setAttribute('material', 'side: double'); - assert.equal(el.getObject3D('mesh').material.version, oldMaterialVersion + 2); + assert.equal(el.getObject3D('mesh').material.version, oldMaterialVersion + 1); el.setAttribute('material', 'side: front'); - assert.equal(el.getObject3D('mesh').material.version, oldMaterialVersion + 4); + assert.equal(el.getObject3D('mesh').material.version, oldMaterialVersion + 2); }); }); @@ -308,8 +334,8 @@ suite('material', function () { el.setAttribute('material', 'alphaTest: 0.0'); assert.equal(el.getObject3D('mesh').material.version, oldMaterialVersion); el.setAttribute('material', 'alphaTest: 1.0'); - // A-Frame sets needsUpdate twice and THREE one more internaly when setting alphaTest. - assert.equal(el.getObject3D('mesh').material.version, oldMaterialVersion + 3); + // A-Frame sets needsUpdate once and THREE one more internally when setting alphaTest. + assert.equal(el.getObject3D('mesh').material.version, oldMaterialVersion + 2); }); }); @@ -323,7 +349,7 @@ suite('material', function () { var oldMaterialVersion = el.getObject3D('mesh').material.version; el.setAttribute('material', 'vertexColorsEnabled', true); assert.equal(el.components.material.material.vertexColors, true); - assert.equal(el.components.material.material.version, oldMaterialVersion + 2); + assert.equal(el.components.material.material.version, oldMaterialVersion + 1); }); }); diff --git a/tests/shaders/phong.test.js b/tests/shaders/phong.test.js index 69c29b5c053..70db0ae7a63 100644 --- a/tests/shaders/phong.test.js +++ b/tests/shaders/phong.test.js @@ -5,7 +5,7 @@ var THREE = require('index').THREE; suite('phong material', function () { setup(function (done) { var el = this.el = entityFactory(); - el.sceneEl.systems.material.clearTextureCache(); + el.sceneEl.systems.material.clearTextureSourceCache(); el.setAttribute('geometry', ''); el.setAttribute('material', {shader: 'phong'}); if (el.hasLoaded) { done(); } diff --git a/tests/systems/material.test.js b/tests/systems/material.test.js index 591f0e946e0..4247ae4c782 100644 --- a/tests/systems/material.test.js +++ b/tests/systems/material.test.js @@ -17,7 +17,7 @@ suite('material system', function () { }); suite('registerMaterial', function () { - test('registers material to scene', function () { + test('registers material to system', function () { var el = this.el; var material; var system; @@ -45,44 +45,33 @@ suite('material system', function () { }); suite('unregisterMaterial', function () { - test('disposes of unused textures', function () { + test('unregisters material from system', 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()}; + var material = {uuid: 'material' }; - 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.registerMaterial(material); + assert.equal(system.materials[material.uuid], material); - system.unregisterMaterial(material2); - assert.ok(texture1.dispose.called); - assert.equal(texture2.dispose.callCount, 1); + system.unregisterMaterial(material); + assert.notOk(system.materials[material.uuid]); }); }); suite('texture caching', function () { setup(function () { - this.system.clearTextureCache(); + this.system.clearTextureSourceCache(); }); - suite('loadImage', function () { - test('loads image texture', function (done) { + suite('loadTextureSource', function () { + test('loads image texture source', function (done) { var system = this.system; var src = IMAGE1; - var data = {src: IMAGE1}; - var hash = system.hash(data); + var hash = system.hash(src); - system.loadImage(src, data, function (texture) { - system.textureCache[hash].then(function (texture2) { - assert.equal(texture, texture2); + system.loadTextureSource(src, function (source) { + system.sourceCache[hash].then(function (source2) { + assert.equal(source, source2); done(); }); }); @@ -90,172 +79,120 @@ suite('material system', function () { test('loads image given an element', function (done) { var img = document.createElement('img'); + img.setAttribute('src', IMAGE1); + var system = this.system; - var data = {src: IMAGE1}; - var hash = system.hash(data); + var hash = system.hash(img); - img.setAttribute('src', IMAGE1); - system.loadImage(img, data, function (texture) { - assert.equal(texture.image, img); - system.textureCache[hash].then(function (texture2) { - assert.equal(texture, texture2); + system.loadTextureSource(img, function (source) { + assert.equal(source.data, img); + system.sourceCache[hash].then(function (source2) { + assert.equal(source, source2); done(); }); }); }); - test('caches identical image textures', function (done) { + test('caches identical image texture sources', function (done) { var system = this.system; var src = IMAGE1; - var data = {src: src}; - var hash = system.hash(data); + var hash = system.hash(src); Promise.all([ - new Promise(function (resolve) { system.loadImage(src, data, resolve); }), - new Promise(function (resolve) { system.loadImage(src, data, resolve); }) + new Promise(function (resolve) { system.loadTextureSource(src, resolve); }), + new Promise(function (resolve) { system.loadTextureSource(src, resolve); }) ]).then(function (results) { assert.equal(results[0], results[1]); - assert.ok(system.textureCache[hash]); - assert.equal(Object.keys(system.textureCache).length, 1); + assert.ok(system.sourceCache[hash]); + assert.equal(Object.keys(system.sourceCache).length, 1); done(); }); }); - test('caches different textures for different images', function (done) { + test('caches different texture sources for different images', function (done) { var system = this.system; var src1 = IMAGE1; var src2 = IMAGE2; - var data1 = {src: src1}; - var data2 = {src: src2}; Promise.all([ - new Promise(function (resolve) { system.loadImage(src1, data1, resolve); }), - new Promise(function (resolve) { system.loadImage(src2, data2, resolve); }) + new Promise(function (resolve) { system.loadTextureSource(src1, resolve); }), + new Promise(function (resolve) { system.loadTextureSource(src2, resolve); }) ]).then(function (results) { assert.notEqual(results[0].uuid, results[1].uuid); done(); }); }); - - test('caches different textures for different repeat', function (done) { - var system = this.system; - var src = IMAGE1; - var data1 = {src: src}; - var data2 = {src: src, repeat: {x: 5, y: 5}}; - var hash1 = system.hash(data1); - var hash2 = system.hash(data2); - - Promise.all([ - new Promise(function (resolve) { system.loadImage(src, data1, resolve); }), - new Promise(function (resolve) { system.loadImage(src, data2, resolve); }) - ]).then(function (results) { - assert.notEqual(results[0].uuid, results[1].uuid); - assert.shallowDeepEqual(results[0].repeat, {x: 1, y: 1}); - assert.shallowDeepEqual(results[1].repeat, {x: 5, y: 5}); - assert.equal(Object.keys(system.textureCache).length, 2); - assert.ok(system.textureCache[hash1]); - assert.ok(system.textureCache[hash2]); - done(); - }); - }); }); suite('loadVideo', function () { test('loads video texture', function (done) { var system = this.system; var src = VIDEO1; - var data = {src: VIDEO1}; - system.loadVideo(src, data, function (texture) { - var hash = Object.keys(system.textureCache)[0]; - system.textureCache[hash].then(function (result) { - assert.equal(texture, result.texture); - assert.equal(texture.image, result.videoEl); + system.loadTextureSource(src, function (source) { + var hash = Object.keys(system.sourceCache)[0]; + system.sourceCache[hash].then(function (result) { + assert.equal(source, result); done(); }); }); }); - test('loads image given a