From ceee4c28b213702dd9b46843b07fb093054829a8 Mon Sep 17 00:00:00 2001 From: dmarcos Date: Mon, 25 Apr 2016 15:22:21 -0700 Subject: [PATCH 1/3] Add icosahedron geometry --- src/geometries/icosahedron.js | 13 +++++++++++++ src/geometries/index.js | 1 + tests/components/geometry.test.js | 12 ++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 src/geometries/icosahedron.js diff --git a/src/geometries/icosahedron.js b/src/geometries/icosahedron.js new file mode 100644 index 00000000000..fd8b449a612 --- /dev/null +++ b/src/geometries/icosahedron.js @@ -0,0 +1,13 @@ +var registerGeometry = require('../core/geometry').registerGeometry; +var THREE = require('../lib/three'); + +registerGeometry('icosahedron', { + schema: { + detail: {default: 0, min: 0, max: 1}, + radius: {default: 1, min: 0} + }, + + init: function (data) { + this.geometry = new THREE.IcosahedronGeometry(data.radius, data.detail); + } +}); diff --git a/src/geometries/index.js b/src/geometries/index.js index bee12af030e..f9611592981 100644 --- a/src/geometries/index.js +++ b/src/geometries/index.js @@ -2,6 +2,7 @@ require('./box.js'); require('./circle.js'); require('./cone.js'); require('./cylinder.js'); +require('./icosahedron.js'); require('./plane.js'); require('./ring.js'); require('./sphere.js'); diff --git a/tests/components/geometry.test.js b/tests/components/geometry.test.js index 4bef6c91e5f..613859df5ef 100644 --- a/tests/components/geometry.test.js +++ b/tests/components/geometry.test.js @@ -173,6 +173,18 @@ suite('standard geometries', function () { assert.equal(geometry.parameters.thetaLength, degToRad(350)); }); + test('icosahedron', function () { + var el = this.el; + var geometry; + el.setAttribute('geometry', { + buffer: false, primitive: 'icosahedron', detail: 0, radius: 5}); + + geometry = el.getObject3D('mesh').geometry; + assert.equal(geometry.type, 'IcosahedronGeometry'); + assert.equal(geometry.parameters.radius, 5); + assert.equal(geometry.parameters.detail, 0); + }); + test('plane', function () { var el = this.el; var geometry; From 5deb28e6ba0a5a197a646ba2c0f0fcc911c5ea97 Mon Sep 17 00:00:00 2001 From: Kevin Ngo Date: Tue, 26 Apr 2016 22:55:15 -0700 Subject: [PATCH 2/3] make shaders handle setting textures (#1399) --- docs/components/material.md | 4 +- src/systems/material.js | 201 +++++++++++++------------ src/utils/material.js | 55 +++---- tests/components/material.test.js | 61 ++++---- tests/systems/material.test.js | 239 +++++++++++++++++++++--------- 5 files changed, 327 insertions(+), 233 deletions(-) diff --git a/docs/components/material.md b/docs/components/material.md index 82133b13df1..e3d8a1749f3 100644 --- a/docs/components/material.md +++ b/docs/components/material.md @@ -46,8 +46,8 @@ The material component has only a few base properties, but more properties will | Event Name | Description | |-------------------------|--------------------------------------------------------------------------------------------| -| material-texture-loaded | Texture loaded onto material. Or when the first frame is playing for video textures. | -| material-video-ended | For video textures, emitted when the video has reached its end (may not work with `loop`). | +| materialtextureloaded | Texture loaded onto material. Or when the first frame is playing for video textures. | +| materialvideoended | For video textures, emitted when the video has reached its end (may not work with `loop`). | ## Textures diff --git a/src/systems/material.js b/src/systems/material.js index f18d91da8b4..7f19121211f 100755 --- a/src/systems/material.js +++ b/src/systems/material.js @@ -2,9 +2,6 @@ var registerSystem = require('../core/system').registerSystem; var THREE = require('../lib/three'); var utils = require('../utils/'); -var EVENTS = { - TEXTURE_LOADED: 'material-texture-loaded' -}; var debug = utils.debug; var error = debug('components:texture:error'); var TextureLoader = new THREE.TextureLoader(); @@ -30,58 +27,69 @@ module.exports.System = registerSystem('material', { }, /** - * High-level function for loading image textures. Meat of logic is in `loadImageTexture`. + * Determine whether `src` is a image or video. Then try to load the asset, then call back. * - * @param {Element} el - Entity, used to emit event. - * @param {object} material - three.js material, bound by the A-Frame shader. - * @param {object} data - Shader data, bound by the A-Frame shader. - * @param {Element|string} src - Texture source, bound by `src-loader` utils. + * @param {string} src - Texture URL. + * @param {string} data - Relevant texture data used for caching. + * @param {function} cb - Callback to pass texture to. */ - loadImage: function (el, material, data, src) { - var repeat = data.repeat || '1 1'; - var srcString = src; - var textureCache = this.textureCache; + loadTexture: function (src, data, cb) { + var self = this; + utils.srcLoader.validateSrc(src, loadImageCb, loadVideoCb); + function loadImageCb (src) { self.loadImage(src, data, cb); } + function loadVideoCb (src) { self.loadVideo(src, data, cb); } + }, - if (typeof src !== 'string') { srcString = src.getAttribute('src'); } + /** + * 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, cb) { + var hash = this.hash(data); + var handleImageTextureLoaded = cb; + var textureCache = this.textureCache; - // Another material is already loading this texture. Wait on promise. - if (textureCache[src] && textureCache[src][repeat]) { - textureCache[src][repeat].then(handleImageTextureLoaded); + // Texture already being loaded or already loaded. Wait on promise. + if (textureCache[hash]) { + textureCache[hash].then(handleImageTextureLoaded); return; } - // Material instance is first to try to load this texture. Load it. - textureCache[srcString] = textureCache[srcString] || {}; - textureCache[srcString][repeat] = textureCache[srcString][repeat] || {}; - textureCache[srcString][repeat] = loadImageTexture(material, src, repeat); - textureCache[srcString][repeat].then(handleImageTextureLoaded); - - function handleImageTextureLoaded (texture) { - utils.material.updateMaterialTexture(material, texture); - el.emit(EVENTS.TEXTURE_LOADED, { src: src, texture: texture }); - } + // Texture not yet being loaded. Start loading it. + textureCache[hash] = loadImageTexture(src, data); + textureCache[hash].then(handleImageTextureLoaded); }, - /** - * Load video texture. - * Note that creating a video texture is more synchronous than creating an image 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} el - Entity, used to emit event. - * @param {object} material - three.js material. - * @param data {object} - Shader data, bound by the A-Frame shader. - * @param src {Element|string} - Texture source, bound by `src-loader` utils. + * @param {Element|string} src - Texture source. + * @param {object} data - Texture data. + * @param {function} cb - Callback to pass texture to. */ - loadVideo: function (el, material, data, src) { + loadVideo: function (src, data, cb) { var hash; var texture; var textureCache = this.textureCache; 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 = calculateVideoCacheHash(videoEl); + hash = this.hashVideo(data, videoEl); if (textureCache[hash]) { textureCache[hash].then(handleVideoTextureLoaded); return; @@ -90,11 +98,11 @@ module.exports.System = registerSystem('material', { fixVideoAttributes(videoEl); } - // Use video element to create texture. - videoEl = videoEl || createVideoEl(material, src, data.width, data.height); + // 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 = calculateVideoCacheHash(videoEl); + hash = this.hashVideo(data, videoEl); if (textureCache[hash]) { textureCache[hash].then(handleVideoTextureLoaded); return; @@ -103,28 +111,20 @@ module.exports.System = registerSystem('material', { // 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 - }; + videoTextureResult = {texture: texture, videoEl: videoEl}; textureCache[hash] = Promise.resolve(videoTextureResult); handleVideoTextureLoaded(videoTextureResult); + }, - function handleVideoTextureLoaded (res) { - texture = res.texture; - videoEl = res.videoEl; - utils.material.updateMaterialTexture(material, texture); - el.emit(EVENTS.TEXTURE_LOADED, { element: videoEl, src: src }); - videoEl.addEventListener('loadeddata', function () { - el.emit('material-video-loadeddata', { element: videoEl, src: src }); - }); - videoEl.addEventListener('ended', function () { - // Works for non-looping videos only. - el.emit('material-video-ended', { element: videoEl, src: src }); - }); - } + hash: function (data) { + return JSON.stringify(data); + }, + + hashVideo: function (data, videoEl) { + return calculateVideoCacheHash(data, videoEl); }, /** @@ -161,10 +161,11 @@ module.exports.System = registerSystem('material', { * 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 (videoEl) { +function calculateVideoCacheHash (data, videoEl) { var i; var id = videoEl.getAttribute('id'); var hash; @@ -174,7 +175,7 @@ function calculateVideoCacheHash (videoEl) { // Calculate hash using sorted video attributes. hash = ''; - videoAttributes = {}; + videoAttributes = data || {}; for (i = 0; i < videoEl.attributes.length; i++) { videoAttributes[videoEl.attributes[i].name] = videoEl.attributes[i].value; } @@ -186,24 +187,28 @@ function calculateVideoCacheHash (videoEl) { } /** - * Set image texture on material as `map`. + * Load image texture. * * @private - * @param {object} el - Entity element. - * @param {object} material - three.js material. * @param {string|object} src - An element or url to an image file. - * @param {string} repeat - X and Y value for size of texture repeating (in UV units). + * @param {object} data - Data to set texture properties like `repeat`. * @returns {Promise} Resolves once texture is loaded. */ -function loadImageTexture (material, src, repeat) { +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) { - createTexture(src); + resolveTexture(new THREE.Texture(src)); return; } @@ -211,59 +216,55 @@ function loadImageTexture (material, src, repeat) { // Use THREE.TextureLoader (src, onLoad, onProgress, onError) to load texture. TextureLoader.load( src, - createTexture, + resolveTexture, function () { /* no-op */ }, function (xhr) { error('`$s` could not be fetched (Error code: %s; Response: %s)', xhr.status, xhr.statusText); } ); + } +} - /** - * Texture loaded. Set it. - */ - function createTexture (texture) { - var repeatXY; - if (!(texture instanceof THREE.Texture)) { texture = new THREE.Texture(texture); } - - // Handle UV repeat. - repeatXY = repeat.split(' '); - if (repeatXY.length === 2) { - texture.wrapS = THREE.RepeatWrapping; - texture.wrapT = THREE.RepeatWrapping; - texture.repeat.set(parseInt(repeatXY[0], 10), parseInt(repeatXY[1], 10)); - } +/** + * Set texture properties such as repeat. + * + * @param {object} data - With keys like `repeat`. + */ +function setTextureProperties (texture, data) { + // Handle UV repeat. + var repeat = data.repeat || '1 1'; + var repeatXY = repeat.split(' '); - resolve(texture); - } - } + // Don't bother setting repeat if it is 1/1. Power-of-two is required to repeat. + if (repeat === '1 1' || repeatXY.length !== 2) { return; } + + texture.wrapS = THREE.RepeatWrapping; + texture.wrapT = THREE.RepeatWrapping; + texture.repeat.set(parseInt(repeatXY[0], 10), parseInt(repeatXY[1], 10)); } /** * Create video element to be used as a texture. * - * @param {object} material - three.js material. * @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 (material, src, width, height) { - var el = material.videoEl || document.createElement('video'); - el.width = width; - el.height = height; - if (el !== this.videoEl) { - el.setAttribute('webkit-playsinline', ''); // To support inline videos in iOS webviews. - el.autoplay = true; - el.loop = true; - el.crossOrigin = true; - el.addEventListener('error', function () { - warn('`$s` is not a valid video', src); - }, true); - material.videoEl = el; - } - el.src = src; - return el; +function createVideoEl (src, width, height) { + var videoEl = document.createElement('video'); + videoEl.width = width; + videoEl.height = height; + videoEl.setAttribute('webkit-playsinline', ''); // Support inline videos for iOS webviews. + videoEl.autoplay = true; + videoEl.loop = true; + videoEl.crossOrigin = true; + videoEl.addEventListener('error', function () { + warn('`$s` is not a valid video', src); + }, true); + videoEl.src = src; + return videoEl; } /** @@ -280,10 +281,8 @@ function createVideoEl (material, src, width, height) { * @returns {Element} Video element with the correct properties updated. */ function fixVideoAttributes (videoEl) { + videoEl.autoplay = videoEl.getAttribute('autoplay') !== 'false'; videoEl.controls = videoEl.getAttribute('controls') !== 'false'; - if (videoEl.getAttribute('autoplay') === 'false') { - videoEl.removeAttribute('autoplay'); - } if (videoEl.getAttribute('loop') === 'false') { videoEl.removeAttribute('loop'); } diff --git a/src/utils/material.js b/src/utils/material.js index bbc2df0fd96..d5ffeaf2344 100644 --- a/src/utils/material.js +++ b/src/utils/material.js @@ -1,47 +1,52 @@ -var srcLoader = require('./src-loader'); - /** - * Update shader instance's `material.map` given `data.src`. + * Update `material.map` given `data.src`. For standard and flat shaders. * * @param {object} shader - A-Frame shader instance. * @param {object} data */ module.exports.updateMap = function (shader, data) { var el = shader.el; - var src = data.src; var material = shader.material; - var materialSystem = el.sceneEl.systems.material; + var src = data.src; if (src) { - if (src === shader.mapSrc) { return; } + if (src === shader.textureSrc) { return; } // Texture added or changed. - shader.mapSrc = src; - srcLoader.validateSrc( - src, - function loadImageCb (src) { materialSystem.loadImage(el, material, data, src); }, - function loadVideoCb (src) { materialSystem.loadVideo(el, material, data, src); } - ); + shader.textureSrc = src; + el.sceneEl.systems.material.loadTexture(src, {src: src, repeat: data.repeat}, setMap); return; } // Texture removed. - updateMaterialTexture(material, null); + if (!material.map) { return; } + setMap(null); + + function setMap (texture) { + material.map = texture; + material.needsUpdate = true; + handleTextureEvents(el, texture); + } }; /** - * Set material texture and update if necessary. + * Emit event on entities on texture-related events. * - * @param {object} material - * @param {object} texture + * @param {Element} el - Entity. + * @param {object} texture - three.js Texture. */ -function updateMaterialTexture (material, texture) { - var oldMap = material.map; - if (texture) { texture.needsUpdate = true; } - material.map = texture; +function handleTextureEvents (el, texture) { + if (!texture) { return; } - // Only need to update three.js material if presence or not of texture has changed. - if (oldMap === null && material.map || material.map === null && oldMap) { - material.needsUpdate = true; - } + el.emit('materialtextureloaded', {src: texture.image, texture: texture}); + + // Video events. + if (texture.image.tagName !== 'VIDEO') { return; } + texture.image.addEventListener('loadeddata', function emitVideoTextureLoadedDataAll () { + el.emit('materialvideoloadeddata', {src: texture.image, texture: texture}); + }); + texture.image.addEventListener('ended', function emitVideoTextureEndedAll () { + // Works for non-looping videos only. + el.emit('materialvideoended', {src: texture.image, texture: texture}); + }); } -module.exports.updateMaterialTexture = updateMaterialTexture; +module.exports.handleTextureEvents = handleTextureEvents; diff --git a/tests/components/material.test.js b/tests/components/material.test.js index ccd8bebb39f..6a9fa1d6725 100644 --- a/tests/components/material.test.js +++ b/tests/components/material.test.js @@ -1,5 +1,6 @@ /* global assert, process, setup, suite, test, AFRAME */ var entityFactory = require('../helpers').entityFactory; +var shaders = require('core/shader').shaders; var THREE = require('index').THREE; suite('material', function () { @@ -25,6 +26,13 @@ suite('material', function () { assert.shallowDeepEqual(el.getObject3D('mesh').material.side, THREE.DoubleSide); }); + test('updates material shader', function () { + var el = this.el; + assert.equal(el.getObject3D('mesh').material.type, 'MeshBasicMaterial'); + el.setAttribute('material', 'shader', 'standard'); + assert.equal(el.getObject3D('mesh').material.type, 'MeshStandardMaterial'); + }); + test('disposes material when changing to new material', function () { var el = this.el; var material = el.getObject3D('mesh').material; @@ -63,15 +71,24 @@ suite('material', function () { var el = this.el; var imageUrl = 'base/tests/assets/test.png'; el.setAttribute('material', 'src: url(' + imageUrl + ')'); - el.addEventListener('material-texture-loaded', function (evt) { - assert.equal(evt.detail.src, imageUrl); + el.addEventListener('materialtextureloaded', function (evt) { + assert.equal(evt.detail.texture.image.getAttribute('src'), imageUrl); done(); }); }); + + test('sets material to MeshShaderMaterial for custom shaders', function () { + var el = this.el; + delete shaders.test; + AFRAME.registerShader('test', {}); + assert.equal(el.getObject3D('mesh').material.type, 'MeshBasicMaterial'); + el.setAttribute('material', 'shader', 'test'); + assert.equal(el.getObject3D('mesh').material.type, 'ShaderMaterial'); + }); }); suite('updateSchema', function () { - test('updates schema', function () { + test('updates schema for flat shader', function () { var el = this.el; el.components.material.updateSchema({shader: 'flat'}); assert.ok(el.components.material.schema.color); @@ -84,39 +101,21 @@ suite('material', function () { assert.notOk(el.components.material.schema.roughness); assert.notOk(el.components.material.schema.envMap); }); - }); - - suite('updateShader', function () { - test('updates material shader', function () { - var el = this.el; - assert.equal(el.getObject3D('mesh').material.type, 'MeshBasicMaterial'); - el.components.material.updateShader('standard'); - assert.equal(el.getObject3D('mesh').material.type, 'MeshStandardMaterial'); - }); - test('sets material to MeshShaderMaterial for custom shaders', function () { + test('updates schema for custom shader', function () { var el = this.el; + delete shaders.test; AFRAME.registerShader('test', { schema: { - 'luminance': { default: 1 } - }, - - vertexShader: [ - 'varying vec3 vWorldPosition;', - 'void main() {', - 'vec4 worldPosition = modelMatrix * vec4( position, 1.0 );', - 'vWorldPosition = worldPosition.xyz;', - 'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );', - '}' - ].join('\n'), - - fragmentShader: [ - 'void main() { gl_FragColor = vec4(1.0,0.0,1.0,1.0); }' - ].join('\n') + color: {type: 'color'}, + luminance: {default: 1} + } }); - assert.equal(el.getObject3D('mesh').material.type, 'MeshBasicMaterial'); - el.components.material.updateShader('test'); - assert.equal(el.getObject3D('mesh').material.type, 'ShaderMaterial'); + el.setAttribute('material', 'shader', 'test'); + assert.ok(el.components.material.schema.opacity); + assert.ok(el.components.material.schema.color); + assert.ok(el.components.material.schema.luminance); + assert.notOk(el.components.material.schema.src); }); }); diff --git a/tests/systems/material.test.js b/tests/systems/material.test.js index b3bb4968ee6..72e9a8790ac 100644 --- a/tests/systems/material.test.js +++ b/tests/systems/material.test.js @@ -1,11 +1,17 @@ /* global assert, process, setup, suite, test */ var entityFactory = require('../helpers').entityFactory; -var THREE = require('index').THREE; + +var IMAGE1 = 'base/tests/assets/test.png'; +var IMAGE2 = 'base/tests/assets/test2.png'; +var VIDEO1 = 'base/tests/assets/test.mp4'; +var VIDEO2 = 'base/tests/assets/test2.mp4'; suite('material system', function () { setup(function (done) { + var self = this; var el = this.el = entityFactory(); el.addEventListener('loaded', function () { + self.system = el.sceneEl.systems.material; done(); }); }); @@ -38,97 +44,182 @@ suite('material system', function () { suite('texture caching', function () { setup(function () { - this.el.sceneEl.systems.material.clearTextureCache(); + this.system.clearTextureCache(); }); - test('does not cache different image textures', function (done) { - var el = this.el; - var imageUrl = 'base/tests/assets/test.png'; - var imageUrl2 = 'base/tests/assets/test2.png'; - var textureSpy = this.sinon.spy(THREE, 'Texture'); - assert.equal(textureSpy.callCount, 0); - - el.setAttribute('material', 'src: url(' + imageUrl + ')'); - - el.addEventListener('material-texture-loaded', function (evt) { - var el2; - assert.equal(textureSpy.callCount, 1); - - el2 = document.createElement('a-entity'); - el2.setAttribute('material', 'src: url(' + imageUrl2 + ')'); - el.sceneEl.appendChild(el2); - el2.addEventListener('material-texture-loaded', function () { - // Two textures created. - assert.equal(textureSpy.callCount, 2); + suite('loadImage', function () { + test('loads image texture', function (done) { + var system = this.system; + var src = IMAGE1; + var data = {src: IMAGE1}; + var hash = system.hash(data); + + system.loadImage(src, data, function (texture) { + assert.equal(texture.image.getAttribute('src'), src); + system.textureCache[hash].then(function (texture2) { + assert.equal(texture, texture2); + done(); + }); + }); + }); + + test('loads image given an element', function (done) { + var img = document.createElement('img'); + var system = this.system; + var data = {src: IMAGE1}; + var hash = system.hash(data); + + 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); + done(); + }); + }); + }); + + test('caches identical image textures', function (done) { + var system = this.system; + var src = IMAGE1; + var data = {src: src}; + var hash = system.hash(data); + + Promise.all([ + new Promise(function (resolve) { system.loadImage(src, data, resolve); }), + new Promise(function (resolve) { system.loadImage(src, data, resolve); }) + ]).then(function (results) { + assert.equal(results[0], results[1]); + assert.equal(results[0].image.getAttribute('src'), src); + assert.ok(system.textureCache[hash]); + assert.equal(Object.keys(system.textureCache).length, 1); done(); }); }); - }); - test('can cache image textures', function (done) { - var el = this.el; - var imageUrl = 'base/tests/assets/test.png'; - var textureSpy = this.sinon.spy(THREE, 'Texture'); - assert.equal(textureSpy.callCount, 0); - - el.setAttribute('material', 'src: url(' + imageUrl + ')'); - - el.addEventListener('material-texture-loaded', function () { - var el2; - assert.equal(textureSpy.callCount, 1); - - el2 = document.createElement('a-entity'); - el2.setAttribute('material', 'src: url(' + imageUrl + ')'); - el.sceneEl.appendChild(el2); - el2.addEventListener('material-texture-loaded', function () { - // Only one texture created. - assert.equal(textureSpy.callCount, 1); + test('caches different textures 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); }) + ]).then(function (results) { + assert.equal(results[0].image.getAttribute('src'), src1); + assert.equal(results[1].image.getAttribute('src'), src2); + assert.notEqual(results[0].uuid, results[1].uuid); done(); }); }); - }); - test('does not cache different video textures', function (done) { - var el = this.el; - var videoUrl = 'base/tests/assets/test.mp4'; - var videoUrl2 = 'base/tests/assets/test2.mp4'; - var textureSpy = this.sinon.spy(THREE, 'VideoTexture'); - assert.equal(textureSpy.callCount, 0); - - el.setAttribute('material', 'src: url(' + videoUrl + ')'); - - el.addEventListener('material-texture-loaded', function (evt) { - var el2; - assert.equal(textureSpy.callCount, 1); - - el2 = document.createElement('a-entity'); - el2.setAttribute('material', 'src: url(' + videoUrl2 + ')'); - el.sceneEl.appendChild(el2); - el2.addEventListener('material-texture-loaded', function () { - // Two textures created. - assert.equal(textureSpy.callCount, 2); + 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: '5 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(); }); }); }); - test('can cache video textures', function (done) { - var el = this.el; - var videoUrl = 'base/tests/assets/test.mp4'; - var textureSpy = this.sinon.spy(THREE, 'VideoTexture'); - assert.equal(textureSpy.callCount, 0); + 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]; + assert.equal(texture.image.getAttribute('src'), src); + system.textureCache[hash].then(function (result) { + assert.equal(texture, result.texture); + assert.equal(texture.image, result.videoEl); + done(); + }); + }); + }); + + test('loads image given a