From e0dddcd3d9a439baf75afe316ebdc0dcb2e520ea Mon Sep 17 00:00:00 2001 From: Noeri Huisman <8823461+mrxz@users.noreply.github.com> Date: Fri, 9 Feb 2024 23:21:23 +0100 Subject: [PATCH] Move envMap loading into material utils and system (#5454) Co-authored-by: Noeri Huisman --- src/core/a-cubemap.js | 13 ++- src/core/propertyTypes.js | 2 +- src/shaders/phong.js | 84 +++------------- src/shaders/standard.js | 59 +---------- src/systems/material.js | 33 +++++++ src/utils/material.js | 71 ++++++++++++++ src/utils/src-loader.js | 101 ++++++++++++------- tests/shaders/phong.test.js | 172 +++++++++++++++++++++++++++++++++ tests/shaders/standard.test.js | 13 ++- tests/utils/src-loader.test.js | 93 ++++++++++++++++++ 10 files changed, 470 insertions(+), 171 deletions(-) create mode 100644 tests/shaders/phong.test.js create mode 100644 tests/utils/src-loader.test.js diff --git a/src/core/a-cubemap.js b/src/core/a-cubemap.js index 7bdc890d36c..db9e250f894 100644 --- a/src/core/a-cubemap.js +++ b/src/core/a-cubemap.js @@ -4,7 +4,7 @@ var debug = require('../utils/debug'); var warn = debug('core:cubemap:warn'); /** - * Cubemap element that handles validation and exposes list of URLs. + * Cubemap element that handles validation and exposes list of six image sources (URL or ). * Does not listen to updates. */ class ACubeMap extends HTMLElement { @@ -38,10 +38,9 @@ class ACubeMap extends HTMLElement { /** * Checks for exactly six elements with [src]. - * Does not check explicitly for s in case user does not want - * prefetching. + * When s are used they will be prefetched. * - * @returns {Array|null} - six URLs if valid, else null. + * @returns {Array|null} - six URLs or elements if valid, else null. */ validate () { var elements = this.querySelectorAll('[src]'); @@ -49,7 +48,11 @@ class ACubeMap extends HTMLElement { var srcs = []; if (elements.length === 6) { for (i = 0; i < elements.length; i++) { - srcs.push(elements[i].getAttribute('src')); + if (elements[i].tagName === 'IMG') { + srcs.push(elements[i]); + } else { + srcs.push(elements[i].getAttribute('src')); + } } return srcs; } diff --git a/src/core/propertyTypes.js b/src/core/propertyTypes.js index e899ba2b1ce..5e62a9cbe34 100644 --- a/src/core/propertyTypes.js +++ b/src/core/propertyTypes.js @@ -6,7 +6,7 @@ var warn = debug('core:propertyTypes:warn'); var propertyTypes = module.exports.propertyTypes = {}; var nonCharRegex = /[,> .[\]:]/; -var urlRegex = /\url\((.+)\)/; +var urlRegex = /url\((.+)\)/; // Built-in property types. registerPropertyType('audio', '', assetParse); diff --git a/src/shaders/phong.js b/src/shaders/phong.js index b279ec281e7..541a3ea8ab9 100644 --- a/src/shaders/phong.js +++ b/src/shaders/phong.js @@ -2,9 +2,6 @@ var registerShader = require('../core/shader').registerShader; var THREE = require('../lib/three'); var utils = require('../utils/'); -var CubeLoader = new THREE.CubeTextureLoader(); -var texturePromises = {}; - /** * Phong shader using THREE.MeshPhongMaterial. */ @@ -55,6 +52,16 @@ module.exports.Shader = registerShader('phong', { this.materialData = { color: new THREE.Color(), specular: new THREE.Color(), emissive: new THREE.Color() }; getMaterialData(data, this.materialData); this.material = new THREE.MeshPhongMaterial(this.materialData); + var sceneEl = this.el.sceneEl; + // Fallback to scene environment when no envMap is defined (matching behaviour of standard material) + Object.defineProperty(this.material, 'envMap', { + get: function () { + return this._envMap || sceneEl.object3D.environment; + }, + set: function (value) { + this._envMap = value; + } + }); }, update: function (data) { @@ -64,7 +71,7 @@ module.exports.Shader = registerShader('phong', { utils.material.updateDistortionMap('displacement', this, data); utils.material.updateDistortionMap('ambientOcclusion', this, data); utils.material.updateDistortionMap('bump', this, data); - this.updateEnvMap(data); + utils.material.updateEnvMap(this, data); }, /** @@ -78,73 +85,6 @@ module.exports.Shader = registerShader('phong', { for (key in this.materialData) { this.material[key] = this.materialData[key]; } - }, - - /** - * Handle environment cubemap. Textures are cached in texturePromises. - */ - updateEnvMap: function (data) { - var self = this; - var material = this.material; - var envMap = data.envMap; - var sphericalEnvMap = data.sphericalEnvMap; - var refract = data.refract; - var sceneEl = this.el.sceneEl; - - // No envMap defined or already loading. - if ((!envMap && !sphericalEnvMap) || this.isLoadingEnvMap) { - Object.defineProperty(material, 'envMap', { - get: function () { - return sceneEl.object3D.environment; - }, - set: function (value) { - delete this.envMap; - this.envMap = value; - } - }); - material.needsUpdate = true; - return; - } - this.isLoadingEnvMap = true; - delete material.envMap; - - // if a spherical env map is defined then use it. - if (sphericalEnvMap) { - this.el.sceneEl.systems.material.loadTexture(sphericalEnvMap, { src: sphericalEnvMap }, function textureLoaded (texture) { - self.isLoadingEnvMap = false; - texture.mapping = refract ? THREE.EquirectangularRefractionMapping : THREE.EquirectangularReflectionMapping; - - material.envMap = texture; - utils.material.handleTextureEvents(self.el, texture); - material.needsUpdate = true; - }); - return; - } - - // Another material is already loading this texture. Wait on promise. - if (texturePromises[envMap]) { - texturePromises[envMap].then(function (cube) { - self.isLoadingEnvMap = false; - material.envMap = cube; - utils.material.handleTextureEvents(self.el, cube); - material.needsUpdate = true; - }); - return; - } - - // Material is first to load this texture. Load and resolve texture. - texturePromises[envMap] = new Promise(function (resolve) { - utils.srcLoader.validateCubemapSrc(envMap, function loadEnvMap (urls) { - CubeLoader.load(urls, function (cube) { - // Texture loaded. - self.isLoadingEnvMap = false; - material.envMap = cube; - cube.mapping = refract ? THREE.CubeRefractionMapping : THREE.CubeReflectionMapping; - utils.material.handleTextureEvents(self.el, cube); - resolve(cube); - }); - }); - }); } }); @@ -192,7 +132,7 @@ function getMaterialData (data, materialData) { } if (data.bumpMap) { - materialData.aoMapIntensity = data.bumpMapScale; + materialData.bumpScale = data.bumpMapScale; } if (data.displacementMap) { diff --git a/src/shaders/standard.js b/src/shaders/standard.js index 2db6798ab3e..853f730552e 100755 --- a/src/shaders/standard.js +++ b/src/shaders/standard.js @@ -2,9 +2,6 @@ var registerShader = require('../core/shader').registerShader; var THREE = require('../lib/three'); var utils = require('../utils/'); -var CubeLoader = new THREE.CubeTextureLoader(); -var texturePromises = {}; - /** * Standard (physically-based) shader using THREE.MeshStandardMaterial. */ @@ -74,7 +71,7 @@ module.exports.Shader = registerShader('standard', { utils.material.updateDistortionMap('ambientOcclusion', this, data); utils.material.updateDistortionMap('metalness', this, data); utils.material.updateDistortionMap('roughness', this, data); - this.updateEnvMap(data); + utils.material.updateEnvMap(this, data); }, /** @@ -90,60 +87,6 @@ module.exports.Shader = registerShader('standard', { for (key in this.materialData) { material[key] = this.materialData[key]; } - }, - - /** - * Handle environment cubemap. Textures are cached in texturePromises. - */ - updateEnvMap: function (data) { - var self = this; - var material = this.material; - var envMap = data.envMap; - var sphericalEnvMap = data.sphericalEnvMap; - - // No envMap defined or already loading. - if ((!envMap && !sphericalEnvMap) || this.isLoadingEnvMap) { - material.envMap = null; - material.needsUpdate = true; - return; - } - this.isLoadingEnvMap = true; - - // if a spherical env map is defined then use it. - if (sphericalEnvMap) { - this.el.sceneEl.systems.material.loadTexture(sphericalEnvMap, {src: sphericalEnvMap}, function textureLoaded (texture) { - self.isLoadingEnvMap = false; - texture.mapping = THREE.EquirectangularReflectionMapping; - material.envMap = texture; - utils.material.handleTextureEvents(self.el, texture); - material.needsUpdate = true; - }); - return; - } - - // Another material is already loading this texture. Wait on promise. - if (texturePromises[envMap]) { - texturePromises[envMap].then(function (cube) { - self.isLoadingEnvMap = false; - material.envMap = cube; - utils.material.handleTextureEvents(self.el, cube); - material.needsUpdate = true; - }); - return; - } - - // Material is first to load this texture. Load and resolve texture. - texturePromises[envMap] = new Promise(function (resolve) { - utils.srcLoader.validateCubemapSrc(envMap, function loadEnvMap (urls) { - CubeLoader.load(urls, function (cube) { - // Texture loaded. - self.isLoadingEnvMap = false; - material.envMap = cube; - utils.material.handleTextureEvents(self.el, cube); - resolve(cube); - }); - }); - }); } }); diff --git a/src/systems/material.js b/src/systems/material.js index 23b685958c3..bf9f994c6d2 100755 --- a/src/systems/material.js +++ b/src/systems/material.js @@ -69,6 +69,39 @@ module.exports.System = registerSystem('material', { function loadVideoCb (src) { self.loadVideo(src, data, cb); } }, + /** + * Load the six individual sides and construct a cube texture, then call back. + * + * @param {Array} 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.loadTexture(srcs[index], {src: srcs[index]}, function (texture) { + cube.images[index] = texture.image; + 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). * diff --git a/src/utils/material.js b/src/utils/material.js index 2e76e9ae892..df7189f8e75 100644 --- a/src/utils/material.js +++ b/src/utils/material.js @@ -1,4 +1,7 @@ var THREE = require('../lib/three'); +var srcLoader = require('./src-loader'); +var debug = require('./debug'); +var warn = debug('utils:material:warn'); var COLOR_MAPS = new Set([ 'emissiveMap', @@ -148,6 +151,74 @@ module.exports.updateDistortionMap = function (longType, shader, data) { return module.exports.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 + */ +module.exports.updateEnvMap = function (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. * diff --git a/src/utils/src-loader.js b/src/utils/src-loader.js index e0e3d0fb5ed..cd705831071 100644 --- a/src/utils/src-loader.js +++ b/src/utils/src-loader.js @@ -25,50 +25,84 @@ function validateSrc (src, isImageCb, isVideoCb) { } /** - * Validates six images as a cubemap, either as selector or comma-separated - * URLs. + * Validates either six images as a cubemap or one image as an Equirectangular image. * - * @param {string} src - A selector or comma-separated image URLs. Image URLs - must be wrapped by `url()`. - * @param {string} src - A selector or comma-separated image URLs. Image URLs + * @param {string} src - A selector, image URL or comma-separated image URLs. Image URLS must be wrapped by `url()`. + * @param {*} isCubemapCb - callback if src is a cubemap. + * @param {*} isEquirectCb - callback is src is a singular equirectangular image. */ -function validateCubemapSrc (src, cb) { - var aCubemap; +function validateEnvMapSrc (src, isCubemapCb, isEquirectCb) { + var el; var cubemapSrcRegex = ''; var i; var urls; var validatedUrls = []; - for (i = 0; i < 5; i++) { - cubemapSrcRegex += '(url\\((?:[^\\)]+)\\),\\s*)'; - } - cubemapSrcRegex += '(url\\((?:[^\\)]+)\\)\\s*)'; - urls = src.match(new RegExp(cubemapSrcRegex)); - - // `src` is a comma-separated list of URLs. - // In this case, re-use validateSrc for each side of the cube. - function isImageCb (url) { - validatedUrls.push(url); - if (validatedUrls.length === 6) { - cb(validatedUrls); + if (typeof src === 'string') { + for (i = 0; i < 5; i++) { + cubemapSrcRegex += '(url\\((?:[^\\)]+)\\),\\s*)'; } - } - if (urls) { - for (i = 1; i < 7; i++) { - validateSrc(parseUrl(urls[i]), isImageCb); + cubemapSrcRegex += '(url\\((?:[^\\)]+)\\)\\s*)'; + urls = src.match(new RegExp(cubemapSrcRegex)); + + // `src` is a comma-separated list of URLs. + // In this case, re-use validateSrc for each side of the cube. + function isImageCb (url) { + validatedUrls.push(url); + if (validatedUrls.length === 6) { + isCubemapCb(validatedUrls); + } } - return; + if (urls) { + for (i = 1; i < 7; i++) { + validateSrc(parseUrl(urls[i]), isImageCb); + } + return; + } + + // Single URL src + if (!src.startsWith('#')) { + var parsedSrc = parseUrl(src); + if (parsedSrc) { + validateSrc(parsedSrc, isEquirectCb); + } else { + validateSrc(src, isEquirectCb); + } + return; + } + } + + // `src` is either an element or a query selector to an element ( or ). + if (src.tagName) { + el = src; + } else { + el = validateAndGetQuerySelector(src); } - // `src` is a query selector to containing six $([src])s. - aCubemap = validateAndGetQuerySelector(src); - if (!aCubemap) { return; } - if (aCubemap.tagName === 'A-CUBEMAP' && aCubemap.srcs) { - return cb(aCubemap.srcs); + if (!el) { return; } + if (el.tagName === 'A-CUBEMAP' && el.srcs) { + return isCubemapCb(el.srcs); } - // Else if aCubeMap is not a . - warn('Selector "%s" does not point to ', src); + if (el.tagName === 'IMG') { + return isEquirectCb(el); + } + // Else if el is not a valid element, either or . + warn('Selector "%s" does not point to or ', src); +} + +/** + * Validates six images as a cubemap, either as selector or comma-separated + * URLs. + * + * @param {string} src - A selector or comma-separated image URLs. Image URLs + must be wrapped by `url()`. + * @param {function} cb - callback if src is a cubemap. + */ +function validateCubemapSrc (src, cb) { + return validateEnvMapSrc(src, cb, function isEquirectCb () { + warn('Expected cubemap but got image'); + }); } /** @@ -77,7 +111,7 @@ function validateCubemapSrc (src, cb) { * @return {string} The parsed src, if parseable. */ function parseUrl (src) { - var parsedSrc = src.match(/\url\((.+)\)/); + var parsedSrc = src.match(/url\((.+)\)/); if (!parsedSrc) { return; } return parsedSrc[1]; } @@ -153,5 +187,6 @@ function validateAndGetQuerySelector (selector) { module.exports = { parseUrl: parseUrl, validateSrc: validateSrc, - validateCubemapSrc: validateCubemapSrc + validateCubemapSrc: validateCubemapSrc, + validateEnvMapSrc: validateEnvMapSrc }; diff --git a/tests/shaders/phong.test.js b/tests/shaders/phong.test.js new file mode 100644 index 00000000000..69c29b5c053 --- /dev/null +++ b/tests/shaders/phong.test.js @@ -0,0 +1,172 @@ +/* global assert, process, setup, suite, test */ +var entityFactory = require('../helpers').entityFactory; +var THREE = require('index').THREE; + +suite('phong material', function () { + setup(function (done) { + var el = this.el = entityFactory(); + el.sceneEl.systems.material.clearTextureCache(); + el.setAttribute('geometry', ''); + el.setAttribute('material', {shader: 'phong'}); + if (el.hasLoaded) { done(); } + el.addEventListener('loaded', function () { + done(); + }); + }); + + test('can unset fog', function () { + var el = this.el; + assert.ok(el.getObject3D('mesh').material.fog); + el.setAttribute('material', 'fog', false); + assert.notOk(el.getObject3D('mesh').material.fog); + }); + + test('can use ambient occlusion maps', function (done) { + var el = this.el; + var imageUrl = 'base/tests/assets/test.png'; + assert.isNull(el.getObject3D('mesh').material.aoMap); + el.setAttribute('material', { + ambientOcclusionMapIntensity: 0.4, + ambientOcclusionMap: `url(${imageUrl})` + }); + assert.equal(el.getObject3D('mesh').material.aoMapIntensity, 0.4); + el.addEventListener('materialtextureloaded', function (evt) { + assert.equal(el.getObject3D('mesh').material.aoMap, evt.detail.texture); + done(); + }); + }); + + test('can use normal maps', function (done) { + var el = this.el; + var imageUrl = 'base/tests/assets/test.png'; + assert.isNull(el.getObject3D('mesh').material.normalMap); + el.setAttribute('material', { + normalScale: {x: 0.3, y: -0.4}, + normalMap: `url(${imageUrl})`, + normalTextureRepeat: {x: 2, y: 2}, + normalTextureOffset: {x: 0.1, y: 0.1} + }); + assert.equal(el.getObject3D('mesh').material.normalScale.x, 0.3); + assert.equal(el.getObject3D('mesh').material.normalScale.y, -0.4); + el.addEventListener('materialtextureloaded', function (evt) { + assert.equal(el.getObject3D('mesh').material.normalMap, evt.detail.texture); + assert.equal(evt.detail.texture.repeat.x, 2); + assert.equal(evt.detail.texture.offset.x, 0.1); + done(); + }); + }); + + test('can use displacement maps', function (done) { + var el = this.el; + var imageUrl = 'base/tests/assets/test.png'; + assert.isNull(el.getObject3D('mesh').material.displacementMap); + el.setAttribute('material', { + displacementScale: 0.3, + displacementBias: 0.2, + displacementMap: `url(${imageUrl})`, + displacementTextureRepeat: {x: 2, y: 2}, + displacementTextureOffset: {x: 0.1, y: 0.1} + }); + assert.equal(el.getObject3D('mesh').material.displacementScale, 0.3); + assert.equal(el.getObject3D('mesh').material.displacementBias, 0.2); + el.addEventListener('materialtextureloaded', function (evt) { + assert.equal(el.getObject3D('mesh').material.displacementMap, evt.detail.texture); + assert.equal(evt.detail.texture.repeat.x, 2); + assert.equal(evt.detail.texture.offset.x, 0.1); + done(); + }); + }); + + test('can use bump maps', function (done) { + var el = this.el; + var imageUrl = 'base/tests/assets/test.png'; + assert.isNull(el.getObject3D('mesh').material.bumpMap); + el.setAttribute('material', { + bumpMapScale: 0.4, + bumpMap: `url(${imageUrl})` + }); + assert.equal(el.getObject3D('mesh').material.bumpScale, 0.4); + el.addEventListener('materialtextureloaded', function (evt) { + assert.equal(el.getObject3D('mesh').material.bumpMap, evt.detail.texture); + done(); + }); + }); + + [ + { dataName: 'normalMap', materialName: 'normalMap' }, + { dataName: 'displacementMap', materialName: 'displacementMap' }, + { dataName: 'ambientOcclusionMap', materialName: 'aoMap' }, + { dataName: 'bumpMap', materialName: 'bumpMap' } + ].forEach(function (names) { + test(`can unset ${names.dataName}`, function (done) { + var el = this.el; + var imageUrl = 'base/tests/assets/test.png'; + assert.isNull(el.getObject3D('mesh').material[names.materialName]); + el.setAttribute('material', names.dataName, `url(${imageUrl})`); + el.addEventListener('materialtextureloaded', function (evt) { + assert.equal(el.getObject3D('mesh').material[names.materialName], evt.detail.texture); + el.setAttribute('material', names.dataName, ''); + assert.isNull(el.getObject3D('mesh').material[names.materialName]); + done(); + }); + }); + }); + + test('can use spherical env maps', function (done) { + var el = this.el; + var imageUrl = 'base/tests/assets/test.png'; + el.setAttribute('material', 'sphericalEnvMap: url(' + imageUrl + ');'); + el.addEventListener('materialtextureloaded', function (evt) { + assert.equal(evt.detail.texture.mapping, THREE.EquirectangularReflectionMapping); + assert.equal(el.getObject3D('mesh').material.envMap, evt.detail.texture); + done(); + }); + }); + + test('can use cube env maps', function (done) { + var el = this.el; + var imageUrl = 'base/tests/assets/test.png'; + el.setAttribute('material', 'envMap: url(' + imageUrl + '), url(' + imageUrl + '), url(' + imageUrl + '), url(' + imageUrl + '), url(' + imageUrl + '), url(' + imageUrl + ');'); + el.addEventListener('materialtextureloaded', function (evt) { + assert.equal(evt.detail.texture.mapping, THREE.CubeReflectionMapping); + assert.equal(el.getObject3D('mesh').material.envMap, evt.detail.texture); + done(); + }); + }); + + test('can use equirectangular env maps', function (done) { + var el = this.el; + var imageUrl = 'base/tests/assets/test.png'; + el.setAttribute('material', 'envMap: url(' + imageUrl + ');'); + el.addEventListener('materialtextureloaded', function (evt) { + assert.equal(evt.detail.texture.mapping, THREE.EquirectangularReflectionMapping); + assert.equal(el.getObject3D('mesh').material.envMap, evt.detail.texture); + done(); + }); + }); + + test('can use equirectangular env maps for refraction', function (done) { + var el = this.el; + var imageUrl = 'base/tests/assets/test2.png'; + el.setAttribute('material', 'envMap: url(' + imageUrl + '); refract: true'); + el.addEventListener('materialtextureloaded', function (evt) { + assert.equal(evt.detail.texture.mapping, THREE.EquirectangularRefractionMapping); + assert.equal(el.getObject3D('mesh').material.envMap, evt.detail.texture); + done(); + }); + }); + + test('falls back to scene.environment for envMap', function () { + var el = this.el; + el.sceneEl.object3D.environment = new THREE.CubeTexture(); + assert.equal(el.getObject3D('mesh').material.envMap, el.sceneEl.object3D.environment); + }); + + test('can use wireframes', function () { + var el = this.el; + assert.notOk(el.getObject3D('mesh').material.wireframe); + el.setAttribute('material', 'wireframe', true); + assert.ok(el.getObject3D('mesh').material.wireframe); + assert.equal(el.getObject3D('mesh').material.wireframeLinewidth, 2); + }); +}); diff --git a/tests/shaders/standard.test.js b/tests/shaders/standard.test.js index 45d270e244d..bd72788729c 100644 --- a/tests/shaders/standard.test.js +++ b/tests/shaders/standard.test.js @@ -103,7 +103,6 @@ suite('standard material', function () { var el = this.el; var imageUrl = 'base/tests/assets/test.png'; el.setAttribute('material', 'sphericalEnvMap: url(' + imageUrl + ');'); - assert.ok(el.components.material.shader.isLoadingEnvMap); el.addEventListener('materialtextureloaded', function (evt) { assert.equal(evt.detail.texture.mapping, THREE.EquirectangularReflectionMapping); assert.equal(el.getObject3D('mesh').material.envMap, evt.detail.texture); @@ -115,7 +114,6 @@ suite('standard material', function () { var el = this.el; var imageUrl = 'base/tests/assets/test.png'; el.setAttribute('material', 'envMap: url(' + imageUrl + '), url(' + imageUrl + '), url(' + imageUrl + '), url(' + imageUrl + '), url(' + imageUrl + '), url(' + imageUrl + ');'); - assert.ok(el.components.material.shader.isLoadingEnvMap); el.addEventListener('materialtextureloaded', function (evt) { assert.equal(evt.detail.texture.mapping, THREE.CubeReflectionMapping); assert.equal(el.getObject3D('mesh').material.envMap, evt.detail.texture); @@ -123,6 +121,17 @@ suite('standard material', function () { }); }); + test('can use equirectangular env maps', function (done) { + var el = this.el; + var imageUrl = 'base/tests/assets/test.png'; + el.setAttribute('material', 'envMap: url(' + imageUrl + ');'); + el.addEventListener('materialtextureloaded', function (evt) { + assert.equal(evt.detail.texture.mapping, THREE.EquirectangularReflectionMapping); + assert.equal(el.getObject3D('mesh').material.envMap, evt.detail.texture); + done(); + }); + }); + test('can use wireframes', function () { var el = this.el; assert.notOk(el.getObject3D('mesh').material.wireframe); diff --git a/tests/utils/src-loader.test.js b/tests/utils/src-loader.test.js new file mode 100644 index 00000000000..c94b20e5d1f --- /dev/null +++ b/tests/utils/src-loader.test.js @@ -0,0 +1,93 @@ +/* global assert, suite, setup, test, Image */ +var srcLoader = require('utils/src-loader'); +var entityFactory = require('../helpers').entityFactory; + +suite('utils.src-loader', function () { + suite('validateEnvMapSrc', function () { + setup(function (done) { + var el; + var imgAsset = document.createElement('img'); + imgAsset.setAttribute('id', 'image'); + imgAsset.setAttribute('src', 'base/tests/assets/test.png'); + var imgCubemap = constructACubemap('img'); + imgCubemap.setAttribute('id', 'cubemap-imgs'); + var assetCubemap = constructACubemap('a-asset-item'); + assetCubemap.setAttribute('id', 'cubemap-assets'); + + el = this.el = entityFactory({assets: [imgAsset, imgCubemap, assetCubemap]}); + if (el.hasLoaded) { done(); } + el.addEventListener('loaded', function () { + done(); + }); + }); + + test('validates six urls as cubemap', function (done) { + const srcs = `url(base/tests/assets/test.png), + url(base/tests/assets/test.png), + url(base/tests/assets/test.png), + url(base/tests/assets/test.png), + url(base/tests/assets/test.png), + url(base/tests/assets/test.png)`; + srcLoader.validateEnvMapSrc(srcs, function isCubemapCb () { + done(); + }, function isEquirectCb () { + assert.fail(); + }); + }); + + test('validates one url as equirectangular map', function (done) { + const srcs = 'url(base/tests/assets/test.png)'; + srcLoader.validateEnvMapSrc(srcs, function isCubemapCb () { + assert.fail(); + }, function isEquirectCb () { + done(); + }); + }); + + test('validates selector to as equirectangular map', function (done) { + const srcs = '#image'; + srcLoader.validateEnvMapSrc(srcs, function isCubemapCb () { + assert.fail(); + }, function isEquirectCb () { + done(); + }); + }); + + test('validates selector to (with children) as cubemap', function (done) { + const srcs = '#cubemap-imgs'; + srcLoader.validateEnvMapSrc(srcs, function isCubemapCb () { + done(); + }, function isEquirectCb () { + assert.fail(); + }); + }); + + test('validates selector to (without children) as cubemap', function (done) { + const srcs = '#cubemap-assets'; + srcLoader.validateEnvMapSrc(srcs, function isCubemapCb () { + done(); + }, function isEquirectCb () { + assert.fail(); + }); + }); + + test('validates single non-wrapped URL as equirectangular map', function (done) { + const srcs = 'base/tests/assets/test.png'; + srcLoader.validateEnvMapSrc(srcs, function isCubemapCb () { + assert.fail(); + }, function isEquirectCb () { + done(); + }); + }); + }); +}); + +function constructACubemap (childTag) { + var aCubemap = document.createElement('a-cubemap'); + for (let i = 0; i < 6; i++) { + var child = document.createElement(childTag); + child.setAttribute('src', 'base/tests/assets/test.png'); + aCubemap.appendChild(child); + } + return aCubemap; +}