Skip to content

Commit 2cee9eb

Browse files
ngokevindmarcos
authored andcommitted
make shaders handle setting textures (#1399)
1 parent 0a1cd39 commit 2cee9eb

File tree

5 files changed

+327
-233
lines changed

5 files changed

+327
-233
lines changed

‎docs/components/material.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ The material component has only a few base properties, but more properties will
4646

4747
| Event Name | Description |
4848
|-------------------------|--------------------------------------------------------------------------------------------|
49-
| material-texture-loaded | Texture loaded onto material. Or when the first frame is playing for video textures. |
50-
| material-video-ended | For video textures, emitted when the video has reached its end (may not work with `loop`). |
49+
| materialtextureloaded | Texture loaded onto material. Or when the first frame is playing for video textures. |
50+
| materialvideoended | For video textures, emitted when the video has reached its end (may not work with `loop`). |
5151

5252
## Textures
5353

‎src/systems/material.js‎

Lines changed: 100 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ var registerSystem = require('../core/system').registerSystem;
22
var THREE = require('../lib/three');
33
var utils = require('../utils/');
44

5-
var EVENTS = {
6-
TEXTURE_LOADED: 'material-texture-loaded'
7-
};
85
var debug = utils.debug;
96
var error = debug('components:texture:error');
107
var TextureLoader = new THREE.TextureLoader();
@@ -30,58 +27,69 @@ module.exports.System = registerSystem('material', {
3027
},
3128

3229
/**
33-
* High-level function for loading image textures. Meat of logic is in `loadImageTexture`.
30+
* Determine whether `src` is a image or video. Then try to load the asset, then call back.
3431
*
35-
* @param {Element} el - Entity, used to emit event.
36-
* @param {object} material - three.js material, bound by the A-Frame shader.
37-
* @param {object} data - Shader data, bound by the A-Frame shader.
38-
* @param {Element|string} src - Texture source, bound by `src-loader` utils.
32+
* @param {string} src - Texture URL.
33+
* @param {string} data - Relevant texture data used for caching.
34+
* @param {function} cb - Callback to pass texture to.
3935
*/
40-
loadImage: function (el, material, data, src) {
41-
var repeat = data.repeat || '1 1';
42-
var srcString = src;
43-
var textureCache = this.textureCache;
36+
loadTexture: function (src, data, cb) {
37+
var self = this;
38+
utils.srcLoader.validateSrc(src, loadImageCb, loadVideoCb);
39+
function loadImageCb (src) { self.loadImage(src, data, cb); }
40+
function loadVideoCb (src) { self.loadVideo(src, data, cb); }
41+
},
4442

45-
if (typeof src !== 'string') { srcString = src.getAttribute('src'); }
43+
/**
44+
* High-level function for loading image textures (THREE.Texture).
45+
*
46+
* @param {Element|string} src - Texture source.
47+
* @param {object} data - Texture data.
48+
* @param {function} cb - Callback to pass texture to.
49+
*/
50+
loadImage: function (src, data, cb) {
51+
var hash = this.hash(data);
52+
var handleImageTextureLoaded = cb;
53+
var textureCache = this.textureCache;
4654

47-
// Another material is already loading this texture. Wait on promise.
48-
if (textureCache[src] && textureCache[src][repeat]) {
49-
textureCache[src][repeat].then(handleImageTextureLoaded);
55+
// Texture already being loaded or already loaded. Wait on promise.
56+
if (textureCache[hash]) {
57+
textureCache[hash].then(handleImageTextureLoaded);
5058
return;
5159
}
5260

53-
// Material instance is first to try to load this texture. Load it.
54-
textureCache[srcString] = textureCache[srcString] || {};
55-
textureCache[srcString][repeat] = textureCache[srcString][repeat] || {};
56-
textureCache[srcString][repeat] = loadImageTexture(material, src, repeat);
57-
textureCache[srcString][repeat].then(handleImageTextureLoaded);
58-
59-
function handleImageTextureLoaded (texture) {
60-
utils.material.updateMaterialTexture(material, texture);
61-
el.emit(EVENTS.TEXTURE_LOADED, { src: src, texture: texture });
62-
}
61+
// Texture not yet being loaded. Start loading it.
62+
textureCache[hash] = loadImageTexture(src, data);
63+
textureCache[hash].then(handleImageTextureLoaded);
6364
},
6465

65-
/**
66-
* Load video texture.
67-
* Note that creating a video texture is more synchronous than creating an image texture.
66+
/**
67+
* Load video texture (THREE.VideoTexture).
68+
* Which is just an image texture that RAFs + needsUpdate.
69+
* Note that creating a video texture is synchronous unlike loading an image texture.
70+
* Made asynchronous to be consistent with image textures.
6871
*
69-
* @param {Element} el - Entity, used to emit event.
70-
* @param {object} material - three.js material.
71-
* @param data {object} - Shader data, bound by the A-Frame shader.
72-
* @param src {Element|string} - Texture source, bound by `src-loader` utils.
72+
* @param {Element|string} src - Texture source.
73+
* @param {object} data - Texture data.
74+
* @param {function} cb - Callback to pass texture to.
7375
*/
74-
loadVideo: function (el, material, data, src) {
76+
loadVideo: function (src, data, cb) {
7577
var hash;
7678
var texture;
7779
var textureCache = this.textureCache;
7880
var videoEl;
7981
var videoTextureResult;
8082

83+
function handleVideoTextureLoaded (result) {
84+
result.texture.needsUpdate = true;
85+
cb(result.texture, result.videoEl);
86+
}
87+
88+
// Video element provided.
8189
if (typeof src !== 'string') {
8290
// Check cache before creating texture.
8391
videoEl = src;
84-
hash = calculateVideoCacheHash(videoEl);
92+
hash = this.hashVideo(data, videoEl);
8593
if (textureCache[hash]) {
8694
textureCache[hash].then(handleVideoTextureLoaded);
8795
return;
@@ -90,11 +98,11 @@ module.exports.System = registerSystem('material', {
9098
fixVideoAttributes(videoEl);
9199
}
92100

93-
// Use video element to create texture.
94-
videoEl = videoEl || createVideoEl(material, src, data.width, data.height);
101+
// Only URL provided. Use video element to create texture.
102+
videoEl = videoEl || createVideoEl(src, data.width, data.height);
95103

96104
// Generated video element already cached. Use that.
97-
hash = calculateVideoCacheHash(videoEl);
105+
hash = this.hashVideo(data, videoEl);
98106
if (textureCache[hash]) {
99107
textureCache[hash].then(handleVideoTextureLoaded);
100108
return;
@@ -103,28 +111,20 @@ module.exports.System = registerSystem('material', {
103111
// Create new video texture.
104112
texture = new THREE.VideoTexture(videoEl);
105113
texture.minFilter = THREE.LinearFilter;
114+
setTextureProperties(texture, data);
106115

107116
// Cache as promise to be consistent with image texture caching.
108-
videoTextureResult = {
109-
texture: texture,
110-
videoEl: videoEl
111-
};
117+
videoTextureResult = {texture: texture, videoEl: videoEl};
112118
textureCache[hash] = Promise.resolve(videoTextureResult);
113119
handleVideoTextureLoaded(videoTextureResult);
120+
},
114121

115-
function handleVideoTextureLoaded (res) {
116-
texture = res.texture;
117-
videoEl = res.videoEl;
118-
utils.material.updateMaterialTexture(material, texture);
119-
el.emit(EVENTS.TEXTURE_LOADED, { element: videoEl, src: src });
120-
videoEl.addEventListener('loadeddata', function () {
121-
el.emit('material-video-loadeddata', { element: videoEl, src: src });
122-
});
123-
videoEl.addEventListener('ended', function () {
124-
// Works for non-looping videos only.
125-
el.emit('material-video-ended', { element: videoEl, src: src });
126-
});
127-
}
122+
hash: function (data) {
123+
return JSON.stringify(data);
124+
},
125+
126+
hashVideo: function (data, videoEl) {
127+
return calculateVideoCacheHash(data, videoEl);
128128
},
129129

130130
/**
@@ -161,10 +161,11 @@ module.exports.System = registerSystem('material', {
161161
* If the video element has an ID, use that.
162162
* Else build a hash that looks like `src:myvideo.mp4;height:200;width:400;`.
163163
*
164+
* @param data {object} - Texture data such as repeat.
164165
* @param videoEl {Element} - Video element.
165166
* @returns {string}
166167
*/
167-
function calculateVideoCacheHash (videoEl) {
168+
function calculateVideoCacheHash (data, videoEl) {
168169
var i;
169170
var id = videoEl.getAttribute('id');
170171
var hash;
@@ -174,7 +175,7 @@ function calculateVideoCacheHash (videoEl) {
174175

175176
// Calculate hash using sorted video attributes.
176177
hash = '';
177-
videoAttributes = {};
178+
videoAttributes = data || {};
178179
for (i = 0; i < videoEl.attributes.length; i++) {
179180
videoAttributes[videoEl.attributes[i].name] = videoEl.attributes[i].value;
180181
}
@@ -186,84 +187,84 @@ function calculateVideoCacheHash (videoEl) {
186187
}
187188

188189
/**
189-
* Set image texture on material as `map`.
190+
* Load image texture.
190191
*
191192
* @private
192-
* @param {object} el - Entity element.
193-
* @param {object} material - three.js material.
194193
* @param {string|object} src - An <img> element or url to an image file.
195-
* @param {string} repeat - X and Y value for size of texture repeating (in UV units).
194+
* @param {object} data - Data to set texture properties like `repeat`.
196195
* @returns {Promise} Resolves once texture is loaded.
197196
*/
198-
function loadImageTexture (material, src, repeat) {
197+
function loadImageTexture (src, data) {
199198
return new Promise(doLoadImageTexture);
200199

201200
function doLoadImageTexture (resolve, reject) {
202201
var isEl = typeof src !== 'string';
203202

203+
function resolveTexture (texture) {
204+
setTextureProperties(texture, data);
205+
texture.needsUpdate = true;
206+
resolve(texture);
207+
}
208+
204209
// Create texture from an element.
205210
if (isEl) {
206-
createTexture(src);
211+
resolveTexture(new THREE.Texture(src));
207212
return;
208213
}
209214

210215
// Load texture from src string. THREE will create underlying element.
211216
// Use THREE.TextureLoader (src, onLoad, onProgress, onError) to load texture.
212217
TextureLoader.load(
213218
src,
214-
createTexture,
219+
resolveTexture,
215220
function () { /* no-op */ },
216221
function (xhr) {
217222
error('`$s` could not be fetched (Error code: %s; Response: %s)', xhr.status,
218223
xhr.statusText);
219224
}
220225
);
226+
}
227+
}
221228

222-
/**
223-
* Texture loaded. Set it.
224-
*/
225-
function createTexture (texture) {
226-
var repeatXY;
227-
if (!(texture instanceof THREE.Texture)) { texture = new THREE.Texture(texture); }
228-
229-
// Handle UV repeat.
230-
repeatXY = repeat.split(' ');
231-
if (repeatXY.length === 2) {
232-
texture.wrapS = THREE.RepeatWrapping;
233-
texture.wrapT = THREE.RepeatWrapping;
234-
texture.repeat.set(parseInt(repeatXY[0], 10), parseInt(repeatXY[1], 10));
235-
}
229+
/**
230+
* Set texture properties such as repeat.
231+
*
232+
* @param {object} data - With keys like `repeat`.
233+
*/
234+
function setTextureProperties (texture, data) {
235+
// Handle UV repeat.
236+
var repeat = data.repeat || '1 1';
237+
var repeatXY = repeat.split(' ');
236238

237-
resolve(texture);
238-
}
239-
}
239+
// Don't bother setting repeat if it is 1/1. Power-of-two is required to repeat.
240+
if (repeat === '1 1' || repeatXY.length !== 2) { return; }
241+
242+
texture.wrapS = THREE.RepeatWrapping;
243+
texture.wrapT = THREE.RepeatWrapping;
244+
texture.repeat.set(parseInt(repeatXY[0], 10), parseInt(repeatXY[1], 10));
240245
}
241246

242247
/**
243248
* Create video element to be used as a texture.
244249
*
245-
* @param {object} material - three.js material.
246250
* @param {string} src - Url to a video file.
247251
* @param {number} width - Width of the video.
248252
* @param {number} height - Height of the video.
249253
* @returns {Element} Video element.
250254
*/
251-
function createVideoEl (material, src, width, height) {
252-
var el = material.videoEl || document.createElement('video');
253-
el.width = width;
254-
el.height = height;
255-
if (el !== this.videoEl) {
256-
el.setAttribute('webkit-playsinline', ''); // To support inline videos in iOS webviews.
257-
el.autoplay = true;
258-
el.loop = true;
259-
el.crossOrigin = true;
260-
el.addEventListener('error', function () {
261-
warn('`$s` is not a valid video', src);
262-
}, true);
263-
material.videoEl = el;
264-
}
265-
el.src = src;
266-
return el;
255+
function createVideoEl (src, width, height) {
256+
var videoEl = document.createElement('video');
257+
videoEl.width = width;
258+
videoEl.height = height;
259+
videoEl.setAttribute('webkit-playsinline', ''); // Support inline videos for iOS webviews.
260+
videoEl.autoplay = true;
261+
videoEl.loop = true;
262+
videoEl.crossOrigin = true;
263+
videoEl.addEventListener('error', function () {
264+
warn('`$s` is not a valid video', src);
265+
}, true);
266+
videoEl.src = src;
267+
return videoEl;
267268
}
268269

269270
/**
@@ -280,10 +281,8 @@ function createVideoEl (material, src, width, height) {
280281
* @returns {Element} Video element with the correct properties updated.
281282
*/
282283
function fixVideoAttributes (videoEl) {
284+
videoEl.autoplay = videoEl.getAttribute('autoplay') !== 'false';
283285
videoEl.controls = videoEl.getAttribute('controls') !== 'false';
284-
if (videoEl.getAttribute('autoplay') === 'false') {
285-
videoEl.removeAttribute('autoplay');
286-
}
287286
if (videoEl.getAttribute('loop') === 'false') {
288287
videoEl.removeAttribute('loop');
289288
}

0 commit comments

Comments
 (0)