aframe
Version:
A web framework for building virtual reality experiences.
262 lines (233 loc) • 7.64 kB
JavaScript
import * as THREE from 'three';
import { registerSystem } from '../core/system.js';
import * as utils from '../utils/index.js';
import { setTextureProperties, createCompatibleTexture } from '../utils/material.js';
var debug = utils.debug;
var error = debug('components:texture:error');
var warn = debug('components:texture:warn');
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} sourceCache - Texture source cache for, Image, Video and Canvas sources
*/
export var System = registerSystem('material', {
init: function () {
this.materials = {};
this.sourceCache = {};
},
clearTextureSourceCache: function () {
this.sourceCache = {};
},
/**
* Loads and creates a texture for a given `src`.
*
* @param {string|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|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') {
sourceLoaded(new THREE.Source(src));
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); }
}
function sourceLoaded (sourcePromise) {
sourceCache[hash] = Promise.resolve(sourcePromise);
sourceCache[hash].then(cb);
}
},
/**
* Load the six individual sides and construct a cube texture, then call back.
*
* @param {Array<string|Element>} srcs - Array of six texture URLs or elements.
* @param {function} cb - Callback to pass cube texture to.
*/
loadCubeMapTexture: function (srcs, cb) {
var self = this;
var loaded = 0;
var cube = new THREE.CubeTexture();
cube.colorSpace = THREE.SRGBColorSpace;
function loadSide (index) {
self.loadTextureSource(srcs[index], function (source) {
cube.images[index] = source.data;
loaded++;
if (loaded === 6) {
cube.needsUpdate = true;
cb(cube);
}
});
}
if (srcs.length !== 6) {
warn('Cube map texture requires exactly 6 sources, got only %s sources', srcs.length);
return;
}
for (var i = 0; i < srcs.length; i++) {
loadSide(i);
}
},
/**
* High-level function for loading image textures (THREE.Texture).
*
* @param {string|Element} src - Texture source.
* @param {function} cb - Callback to pass texture to.
*/
loadImage: function (src, cb) {
// Image element provided
if (typeof src !== 'string') {
cb(new THREE.Source(src));
return;
}
cb(loadImageUrl(src));
},
/**
* 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 {string|Element} src - Texture source.
* @param {function} cb - Callback to pass texture to.
*/
loadVideo: function (src, cb) {
var videoEl;
// Video element provided.
if (typeof src !== 'string') {
// Check cache before creating texture.
videoEl = src;
// 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);
cb(new THREE.Source(videoEl));
},
/**
* Create a hash for a given source.
*/
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 src;
},
/**
* Keep track of material in case an update trigger is needed (e.g., fog).
*
* @param {object} material
*/
registerMaterial: function (material) {
this.materials[material.uuid] = 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];
}
});
/**
* Load image from a given URL.
*
* @private
* @param {string} src - An url to an image file.
* @returns {Promise} Resolves once texture is loaded.
*/
function loadImageUrl (src) {
return new Promise(doLoadImageUrl);
function doLoadImageUrl (resolve, reject) {
// Request and load texture from src string. THREE will create underlying element.
ImageLoader.load(
src,
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));
}
}
}
/**
* Create video element to be used as a texture.
*
* @param {string} src - Url to a video file.
* @returns {Element} Video element.
*/
function createVideoEl (src) {
var videoEl = document.createElement('video');
// Support inline videos for iOS webviews.
videoEl.setAttribute('playsinline', '');
videoEl.setAttribute('webkit-playsinline', '');
videoEl.autoplay = true;
videoEl.loop = true;
videoEl.crossOrigin = 'anonymous';
videoEl.addEventListener('error', function () {
warn('`%s` is not a valid video', src);
}, true);
videoEl.src = src;
return videoEl;
}
/**
* Fixes a video element's attributes to prevent developers from accidentally passing the
* wrong attribute values to commonly misused video attributes.
*
* <video> does not treat `autoplay`, `controls`, `crossorigin`, `loop`, and `preload` as
* as booleans. Existence of those attributes will mean truthy.
*
* For example, translates <video loop="false"> to <video>.
*
* @see https://developer.mozilla.org/docs/Web/HTML/Element/video#Attributes
* @param {Element} videoEl - Video element.
* @returns {Element} Video element with the correct properties updated.
*/
function fixVideoAttributes (videoEl) {
videoEl.autoplay = videoEl.hasAttribute('autoplay') && videoEl.getAttribute('autoplay') !== 'false';
videoEl.controls = videoEl.hasAttribute('controls') && videoEl.getAttribute('controls') !== 'false';
if (videoEl.getAttribute('loop') === 'false') {
videoEl.removeAttribute('loop');
}
if (videoEl.getAttribute('preload') === 'false') {
videoEl.preload = 'none';
}
videoEl.crossOrigin = videoEl.crossOrigin || 'anonymous';
// To support inline videos in iOS webviews.
videoEl.setAttribute('playsinline', '');
videoEl.setAttribute('webkit-playsinline', '');
return videoEl;
}