@google/model-viewer
Version:
Easily display interactive 3D models on the web and in AR!
403 lines • 17.3 kB
JavaScript
/*
* Copyright 2018 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EnvironmentMixin } from '../../features/environment.js';
import ModelViewerElementBase, { $resetRenderer, $scene } from '../../model-viewer-base.js';
import { assetPath, textureMatchesMeta, timePasses, waitForEvent } from '../helpers.js';
import { BasicSpecTemplate } from '../templates.js';
const expect = chai.expect;
const ALT_BG_IMAGE_URL = assetPath('quick_1k.png');
const BG_IMAGE_URL = assetPath('spruit_sunrise_1k_LDR.jpg');
const HDR_BG_IMAGE_URL = assetPath('spruit_sunrise_1k_HDR.hdr');
const MODEL_URL = assetPath('reflective-sphere.gltf');
const UNLIT_MODEL_URL = assetPath('glTF-Sample-Models/2.0/UnlitTest/glTF-Binary/UnlitTest.glb');
const MULTI_MATERIAL_MODEL_URL = assetPath('Triangle.gltf');
const backgroundHasMap = (scene, url) => {
return textureMatchesMeta(scene.skyboxMaterial().uniforms.envMap.value, { url: url });
};
const backgroundHasColor = (scene, hex) => {
if (!scene.background || !scene.background.isColor) {
return false;
}
return scene.background.getHexString() === hex;
};
/**
* Takes a scene and a meta object and returns a
* boolean indicating whether or not the scene's model has an
* environment map applied that matches the meta object.
*
* @see textureMatchesMeta
*/
const modelUsingEnvMap = (scene, meta) => {
let found = false;
scene.model.traverse(object => {
const mesh = object;
if (Array.isArray(mesh.material)) {
found = found || mesh.material.some((m) => {
return textureMatchesMeta(m.envMap, meta);
});
}
else if (mesh.material && mesh.material.envMap) {
found = found ||
textureMatchesMeta(mesh.material.envMap, meta);
}
});
return found;
};
const modelHasEnvMap = (scene) => {
let found = false;
scene.model.traverse(object => {
const mesh = object;
if (Array.isArray(mesh.material)) {
found = found ||
mesh.material.some((m) => !!m.envMap);
}
else if (mesh.material && mesh.material.envMap) {
found = true;
}
});
return found;
};
/**
* Takes a model object and a meta object and returns
* a promise that resolves when the model's environment map has
* been set to a texture that has `userData` that matches
* the passed in `meta`.
*
* @see textureMatchesMeta
*/
const waitForEnvMap = (model, meta) => waitForEvent(model, 'envmap-change', event => {
return textureMatchesMeta(event.value, Object.assign({}, meta));
});
/**
* Returns a promise that resolves when a given element is loaded
* and has an environment map set that matches the passed in meta.
* @see textureMatchesMeta
*/
const waitForLoadAndEnvMap = (scene, element, meta) => {
const load = waitForEvent(element, 'load');
const envMap = waitForEnvMap(scene.model, meta);
return Promise.all([load, envMap]);
};
suite('ModelViewerElementBase with EnvironmentMixin', () => {
suiteTeardown(() => {
// Reset the renderer once at the end of this spec, to clear out all
// of the heavy cached image buffers:
ModelViewerElementBase[$resetRenderer]();
});
let nextId = 0;
let tagName;
let ModelViewerElement;
let element;
let scene;
setup(() => {
tagName = `model-viewer-environment-${nextId++}`;
ModelViewerElement = class extends EnvironmentMixin(ModelViewerElementBase) {
static get is() {
return tagName;
}
};
customElements.define(tagName, ModelViewerElement);
element = new ModelViewerElement();
scene = element[$scene];
});
teardown(() => element.parentNode && element.parentNode.removeChild(element));
BasicSpecTemplate(() => ModelViewerElement, () => tagName);
test('has default background if no background-image or background-color', () => {
expect(backgroundHasColor(scene, 'ffffff')).to.be.equal(true);
});
test('has default background if no background-image or background-color when in DOM', async () => {
document.body.appendChild(element);
await timePasses();
expect(backgroundHasColor(scene, 'ffffff')).to.be.equal(true);
});
suite('with no background-image property', () => {
let environmentChanges = 0;
suite('and a src property', () => {
setup(async () => {
let onLoad = waitForLoadAndEnvMap(scene, element, { url: null });
element.src = MODEL_URL;
document.body.appendChild(element);
environmentChanges = 0;
scene.model.addEventListener('envmap-update', () => {
environmentChanges++;
});
await onLoad;
});
teardown(() => {
document.body.removeChild(element);
});
test('displays default background', async function () {
expect(backgroundHasColor(scene, 'ffffff')).to.be.equal(true);
});
test('applies a generated environment map on model', async function () {
expect(modelUsingEnvMap(scene, {
url: null,
})).to.be.ok;
});
test('changes the environment exactly once', async function () {
expect(environmentChanges).to.be.eq(1);
});
});
});
suite('with a background-image property', () => {
suite('and a src property', () => {
setup(async () => {
let onLoad = waitForLoadAndEnvMap(scene, element, { url: BG_IMAGE_URL });
element.src = MODEL_URL;
element.backgroundImage = BG_IMAGE_URL;
document.body.appendChild(element);
await onLoad;
});
teardown(() => {
document.body.removeChild(element);
});
test('displays background with the correct map', async function () {
expect(backgroundHasMap(scene, element.backgroundImage)).to.be.ok;
});
test('applies the image as an environment map', async function () {
expect(modelUsingEnvMap(scene, {
url: element.backgroundImage
})).to.be.ok;
});
suite('and a background-color property', () => {
setup(async () => {
element.backgroundColor = '#ff0077';
await timePasses();
});
test('the directional light is white', () => {
const lightColor = scene.shadowLight.color.getHexString().toLowerCase();
expect(lightColor).to.be.equal('ffffff');
});
});
suite('on an unlit model', () => {
setup(async () => {
let onLoad = waitForLoadAndEnvMap(scene, element, {
url: BG_IMAGE_URL,
});
element.src = UNLIT_MODEL_URL;
await onLoad;
});
test('applies no environment map on unlit model', async function () {
expect(modelHasEnvMap(scene)).to.be.false;
});
});
suite('on a model with multi-material meshes', () => {
setup(async () => {
let onLoad = waitForLoadAndEnvMap(scene, element, {
url: BG_IMAGE_URL,
});
element.src = MULTI_MATERIAL_MODEL_URL;
await onLoad;
});
test('applies environment map on model with multi-material meshes', async function () {
expect(modelUsingEnvMap(scene, {
url: element.backgroundImage
})).to.be.ok;
});
});
});
});
suite('with a background-color property', () => {
suite('and a src property', () => {
setup(async () => {
let onLoad = waitForLoadAndEnvMap(scene, element, {
url: null,
});
element.src = MODEL_URL;
element.backgroundColor = '#ff0077';
document.body.appendChild(element);
await onLoad;
});
teardown(() => {
document.body.removeChild(element);
});
test('displays background with the correct color', async function () {
expect(backgroundHasColor(scene, 'ff0077')).to.be.ok;
});
test('applies a generated environment map on model', async function () {
expect(modelUsingEnvMap(scene, {
url: null,
})).to.be.ok;
});
test('displays background with correct color after attaching to DOM', async function () {
document.body.appendChild(element);
await timePasses();
expect(backgroundHasColor(scene, 'ff0077')).to.be.ok;
});
test('the directional light is not tinted', () => {
const lightColor = scene.shadowLight.color.getHexString().toLowerCase();
expect(lightColor).to.be.equal('ffffff');
});
suite('on an unlit model', () => {
setup(async () => {
let onLoad = waitForLoadAndEnvMap(scene, element, {
url: null,
});
element.src = UNLIT_MODEL_URL;
await onLoad;
});
test('applies no environment map on unlit model', async function () {
expect(modelHasEnvMap(scene)).to.be.false;
});
});
});
});
suite('exposure', () => {
setup(async () => {
element.src = MODEL_URL;
document.body.appendChild(element);
await waitForEvent(element, 'load');
scene.isVisible = true;
});
teardown(() => {
document.body.removeChild(element);
});
test('changes the tone mapping exposure of the renderer', async () => {
const originalToneMappingExposure = scene.renderer.renderer.toneMappingExposure;
element.exposure = 2.0;
await timePasses();
scene.renderer.render(performance.now());
const newToneMappingExposure = scene.renderer.renderer.toneMappingExposure;
expect(newToneMappingExposure)
.to.be.greaterThan(originalToneMappingExposure);
});
});
suite('stage-light-intensity', () => {
setup(async () => {
element.src = MODEL_URL;
document.body.appendChild(element);
await waitForEvent(element, 'load');
});
teardown(() => {
document.body.removeChild(element);
});
test('changes model scene light intensity', async () => {
const originalLightIntensity = scene.shadowLight.intensity;
element.stageLightIntensity = 0.5;
await timePasses();
const newLightIntensity = scene.shadowLight.intensity;
expect(newLightIntensity).to.be.greaterThan(originalLightIntensity);
});
});
suite('shadow-intensity', () => {
setup(async () => {
element.src = MODEL_URL;
document.body.appendChild(element);
await waitForEvent(element, 'load');
});
teardown(() => {
document.body.removeChild(element);
});
test('changes the opacity of the static shadow', async () => {
const originalOpacity = scene.shadow.material.opacity;
element.shadowIntensity = 1.0;
await timePasses();
const newOpacity = scene.shadow.material.opacity;
expect(newOpacity).to.be.greaterThan(originalOpacity);
});
});
suite('environment-image', () => {
setup(async () => {
let onLoad = waitForLoadAndEnvMap(scene, element, { url: HDR_BG_IMAGE_URL });
element.setAttribute('src', MODEL_URL);
element.setAttribute('background-color', '#ff0077');
element.setAttribute('environment-image', HDR_BG_IMAGE_URL);
document.body.appendChild(element);
await onLoad;
});
teardown(() => {
document.body.removeChild(element);
});
test('applies environment-image environment map on model', () => {
expect(modelUsingEnvMap(scene, { url: element.environmentImage })).to.be.ok;
});
suite('and environment-image subsequently removed', () => {
setup(async () => {
let envMapChanged = waitForEnvMap(scene.model, { url: null });
element.removeAttribute('environment-image');
await envMapChanged;
});
test('reapplies generated environment map on model', () => {
expect(modelUsingEnvMap(scene, { url: null })).to.be.ok;
});
});
});
suite('with background-color and background-image properties', () => {
setup(async () => {
let onLoad = waitForLoadAndEnvMap(scene, element, { url: HDR_BG_IMAGE_URL });
element.setAttribute('src', MODEL_URL);
element.setAttribute('background-color', '#ff0077');
element.setAttribute('background-image', HDR_BG_IMAGE_URL);
document.body.appendChild(element);
await onLoad;
});
teardown(() => {
document.body.removeChild(element);
});
test('displays background with background-image', async function () {
expect(backgroundHasMap(scene, element.backgroundImage)).to.be.ok;
});
test('applies background-image environment map on model', async function () {
expect(modelUsingEnvMap(scene, { url: element.backgroundImage })).to.be.ok;
});
suite('with an environment-image', () => {
setup(async () => {
const environmentChanged = waitForEvent(element, 'environment-change');
element.setAttribute('environment-image', ALT_BG_IMAGE_URL);
await environmentChanged;
});
test('prefers environment-image as environment map', () => {
expect(modelUsingEnvMap(scene, { url: ALT_BG_IMAGE_URL })).to.be.ok;
});
suite('and environment-image subsequently removed', () => {
setup(async () => {
const environmentChanged = waitForEvent(element, 'environment-change');
element.removeAttribute('environment-image');
await environmentChanged;
});
test('uses background-image as environment map', () => {
expect(modelUsingEnvMap(scene, { url: HDR_BG_IMAGE_URL })).to.be.ok;
});
});
suite('and background-image subsequently removed', () => {
setup(async () => {
const environmentChanged = waitForEvent(element, 'environment-change');
element.removeAttribute('background-image');
await environmentChanged;
});
test('continues using environment-image as environment map', () => {
expect(modelUsingEnvMap(scene, { url: ALT_BG_IMAGE_URL })).to.be.ok;
});
test('displays background with background-color', async function () {
expect(backgroundHasColor(scene, 'ff0077')).to.be.ok;
});
});
});
suite('and background-image subsequently removed', () => {
setup(async () => {
let envMapChanged = waitForEnvMap(scene.model, { url: null });
element.removeAttribute('background-image');
await envMapChanged;
});
test('displays background with background-color', async function () {
expect(backgroundHasColor(scene, 'ff0077')).to.be.ok;
});
test('reapplies generated environment map on model', async function () {
expect(modelUsingEnvMap(scene, { url: null })).to.be.ok;
});
});
});
});
//# sourceMappingURL=environment-spec.js.map