aframe
Version:
A web framework for building virtual reality experiences.
326 lines (279 loc) • 10.3 kB
JavaScript
/* global HTMLCanvasElement, HTMLVideoElement */
import * as THREE from 'three';
import * as srcLoader from './src-loader.js';
import debug from './debug.js';
var warn = debug('utils:material:warn');
var COLOR_MAPS = new Set([
'emissiveMap',
'envMap',
'map',
'specularMap'
]);
/**
* Set texture properties such as repeat and offset.
*
* @param {THREE.Texture} texture - a Texture instance.
* @param {object} data - With keys like `repeat`.
*/
export function setTextureProperties (texture, data) {
var offset = data.offset || {x: 0, y: 0};
var repeat = data.repeat || {x: 1, y: 1};
var npot = data.npot || false;
var anisotropy = data.anisotropy || THREE.Texture.DEFAULT_ANISOTROPY;
var wrapS = texture.wrapS;
var wrapT = texture.wrapT;
var magFilter = texture.magFilter;
var minFilter = texture.minFilter;
// To support NPOT textures, wrap must be ClampToEdge (not Repeat),
// and filters must not use mipmaps (i.e. Nearest or Linear).
if (npot) {
wrapS = THREE.ClampToEdgeWrapping;
wrapT = THREE.ClampToEdgeWrapping;
magFilter = THREE.LinearFilter;
minFilter = THREE.LinearFilter;
}
// Set wrap mode to repeat only if repeat isn't 1/1. Power-of-two is required to repeat.
if (repeat.x !== 1 || repeat.y !== 1) {
wrapS = THREE.RepeatWrapping;
wrapT = THREE.RepeatWrapping;
}
// Apply texture properties
texture.offset.set(offset.x, offset.y);
texture.repeat.set(repeat.x, repeat.y);
if (texture.wrapS !== wrapS || texture.wrapT !== wrapT ||
texture.magFilter !== magFilter || texture.minFilter !== minFilter ||
texture.anisotropy !== anisotropy) {
texture.wrapS = wrapS;
texture.wrapT = wrapT;
texture.magFilter = magFilter;
texture.minFilter = minFilter;
texture.anisotropy = anisotropy;
texture.needsUpdate = true;
}
}
/**
* Update `material` texture property (usually but not always `map`)
* from `data` property (usually but not always `src`).
*
* @param {string} materialName
* @param {string} dataName
* @param {object} shader - A-Frame shader instance.
* @param {object} data
*/
export function updateMapMaterialFromData (materialName, dataName, shader, data) {
var el = shader.el;
var material = shader.material;
var rendererSystem = el.sceneEl.systems.renderer;
var src = data[dataName];
// Because a single material / shader may have multiple textures,
// we need to remember the source value for this data property
// to avoid redundant operations which can be expensive otherwise
// (e.g. video texture loads).
if (!shader.materialSrcs) { shader.materialSrcs = {}; }
if (!src) {
// Forget the prior material src.
delete shader.materialSrcs[materialName];
// Remove the texture from the material.
setMap(null);
return;
}
// If material src hasn't changed, and we already have a texture,
// just update properties, but don't reload the texture.
if (src === shader.materialSrcs[materialName] &&
material[materialName]) {
setTextureProperties(material[materialName], data);
return;
}
// Remember the new src for this texture (there may be multiple).
shader.materialSrcs[materialName] = src;
// If the new material src is already a texture, just use it.
if (src instanceof THREE.Texture) { setMap(src); } else {
// Load texture source for the new material src.
// (And check if we should still use it once available in callback.)
el.sceneEl.systems.material.loadTextureSource(src, updateTexture);
}
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) {
// 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);
}
}
/**
* Update `material.map` given `data.src`. For standard and flat shaders.
*
* @param {object} shader - A-Frame shader instance.
* @param {object} data
*/
export function updateMap (shader, data) {
return updateMapMaterialFromData('map', 'src', shader, data);
}
/**
* Updates the material's maps which give the illusion of extra geometry.
*
* @param {string} longType - The friendly name of the map from the component e.g. ambientOcclusionMap becomes aoMap in THREE.js
* @param {object} shader - A-Frame shader instance
* @param {object} data
*/
export function updateDistortionMap (longType, shader, data) {
var shortType = longType;
if (longType === 'ambientOcclusion') { shortType = 'ao'; }
var info = {};
info.src = data[longType + 'Map'];
// Pass through the repeat and offset to be handled by the material loader.
info.offset = data[longType + 'TextureOffset'];
info.repeat = data[longType + 'TextureRepeat'];
info.wrap = data[longType + 'TextureWrap'];
return updateMapMaterialFromData(shortType + 'Map', 'src', shader, info);
}
// Cache env map results as promises
var envMapPromises = {};
/**
* Updates the material's environment map providing reflections or refractions.
*
* @param {object} shader - A-Frame shader instance
* @param {object} data
*/
export function updateEnvMap (shader, data) {
var material = shader.material;
var el = shader.el;
var materialName = 'envMap';
var src = data.envMap;
var sphericalEnvMap = data.sphericalEnvMap;
var refract = data.refract;
if (sphericalEnvMap) {
src = sphericalEnvMap;
warn('`sphericalEnvMap` property is deprecated, using spherical map as equirectangular map instead. ' +
'Use `envMap` property with a CubeMap or Equirectangular image instead.');
}
if (!shader.materialSrcs) { shader.materialSrcs = {}; }
// EnvMap has been removed
if (!src) {
// Forget the prior material src.
delete shader.materialSrcs[materialName];
material.envMap = null;
material.needsUpdate = true;
return;
}
// Remember the new src for this env map.
shader.materialSrcs[materialName] = src;
// Env map is already loading. Wait on promise.
if (envMapPromises[src]) {
envMapPromises[src].then(checkSetMap);
return;
}
// First time loading this env map.
envMapPromises[src] = new Promise(function (resolve) {
srcLoader.validateEnvMapSrc(src, function loadCubeMap (srcs) {
el.sceneEl.systems.material.loadCubeMapTexture(srcs, function (texture) {
texture.mapping = refract ? THREE.CubeRefractionMapping : THREE.CubeReflectionMapping;
checkSetMap(texture);
resolve(texture);
});
}, function loadEquirectMap (src) {
el.sceneEl.systems.material.loadTexture(src, {src: src}, function (texture) {
texture.mapping = refract ? THREE.EquirectangularRefractionMapping : THREE.EquirectangularReflectionMapping;
checkSetMap(texture);
resolve(texture);
});
});
});
function checkSetMap (texture) {
if (shader.materialSrcs[materialName] !== src) { return; }
material.envMap = texture;
material.needsUpdate = true;
handleTextureEvents(el, texture);
}
}
/**
* Emit event on entities on texture-related events.
*
* @param {Element} el - Entity.
* @param {object} texture - three.js Texture.
*/
export function handleTextureEvents (el, texture) {
if (!texture) { return; }
el.emit('materialtextureloaded', {src: texture.image, texture: texture});
// Video events.
if (!texture.image || texture.image.tagName !== 'VIDEO') { return; }
texture.image.addEventListener('loadeddata', emitVideoTextureLoadedDataAll);
texture.image.addEventListener('ended', emitVideoTextureEndedAll);
function emitVideoTextureLoadedDataAll () {
el.emit('materialvideoloadeddata', {src: texture.image, texture: texture});
}
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);
});
}
/**
* 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
*/
export function isCompatibleTexture (texture, source) {
if (texture.source !== source) {
return false;
}
if (source.data instanceof HTMLCanvasElement) {
return texture.isCanvasTexture;
}
if (source.data instanceof HTMLVideoElement) {
return texture.isVideoTexture;
}
return texture.isTexture && !texture.isCanvasTexture && !texture.isVideoTexture;
}
export 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;
}