Skip to content

Commit a8d9ad0

Browse files
committed
Implement HDR image support (fixes #65) and transitions to using
scene.background for skybox (fixes #196).
1 parent 55fdd5e commit a8d9ad0

File tree

16 files changed

+573
-217
lines changed

16 files changed

+573
-217
lines changed

examples/assets/attributions.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,32 +18,32 @@ Damaged Helmet by <a href="https://sketchfab.com/theblueturtle_">theblueturtle_<
1818
licensed under Creative Commons Attribution-NonCommercial
1919
(<a href="https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/DamagedHelmet">source</a>)
2020

21-
## equirectangular.png
22-
23-
equirectangular.png by <a href="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/WestLangley">WestLangley</a>
24-
licensed under <a href="https://github.com/mrdoob/three.js/blob/dev/LICENSE">MIT</a>
25-
(<a href="https://github.com/mrdoob/three.js/blob/dev/examples/textures/equirectangular.png">source</a>)
26-
2721
## Shishkebab.*
2822

2923
Shishkebab by <a href="https://poly.google.com/user/4aEd8rQgKu2">Poly</a>,
3024
licensed under <a href="https://creativecommons.org/licenses/by/2.0/">CC-BY</a>
3125
(<a href="https://poly.google.com/view/6uTsH2jqgVn">source</a>)
3226

33-
# whipple_creek_regional_park_04
27+
## whipple_creek_regional_park_04
3428

3529
whipple_creek_regional_park_04_1k.jpg by <a href="https://hdrihaven.com">HDRI Haven</a>
3630
licensed under <a href="https://hdrihaven.com/p/license.php">CC0</a>
3731
(<a href="https://hdrihaven.com/hdri/?h=whipple_creek_regional_park_04">source</a>)
3832
Originally in HDR format
3933

40-
# small_hangar_01
34+
## small_hangar_01
4135

4236
small_hangar_01_1k.jpg by <a href="https://hdrihaven.com">HDRI Haven</a>
4337
licensed under <a href="https://hdrihaven.com/p/license.php">CC0</a>
4438
(<a href="https://hdrihaven.com/hdri/?h=small_hangar_01">source</a>)
4539
Originally in HDR format
4640

47-
## cube.gltf, offcenter-cube.gltf, pbr-spheres.glb, reflective-sphere.gltf
41+
## spruit_sunrise_2k
42+
43+
spruit_sunrise_2k.hdr, spruit_sunrise_2k.jpg by <a href="https://hdrihaven.com">HDRI Haven</a>
44+
licensed under <a href="https://hdrihaven.com/p/license.php">CC0</a>
45+
(<a href="https://hdrihaven.com/hdri/?c=outdoor&h=spruit_sunrise">source</a>)
46+
47+
## cube.gltf, offcenter-cube.gltf, pbr-spheres.glb, radiance.glb, reflective-sphere.gltf
4848

4949
Contributed by <a href="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/jsantell">jsantell</a>
-643 KB
Binary file not shown.

examples/assets/radiance.glb

467 KB
Binary file not shown.
5.66 MB
Binary file not shown.
668 KB
Loading

examples/background-image.html

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,9 @@ <h2>An equirectangular <span class="attribute">background-image</span></h2>
6666
<template>
6767
<model-viewer
6868
controls
69-
background-image="assets/whipple_creek_regional_park_04_1k.jpg"
70-
alt="A 3D model of a damaged helmet with a forest in the background"
71-
src="assets/DamagedHelmet/DamagedHelmet.gltf">
72-
</model-viewer>
69+
background-image="assets/spruit_sunrise_2k.jpg"
70+
alt="A 3D model of metal spheres at varying degrees of roughness"
71+
src="assets/radiance.glb"></model-viewer>
7372
</template>
7473
</example-snippet>
7574
</div>
@@ -81,14 +80,14 @@ <h2>An equirectangular <span class="attribute">background-image</span></h2>
8180
<div class="content">
8281
<div class="wrapper">
8382
<div class="index">2</div>
84-
<h2>An equirectangular <span class="attribute">background-image</span> with a very reflective model</h2>
83+
<h2>An equirectangular HDR <span class="attribute">background-image</span></h2>
8584
<example-snippet stamp-to="demo-container-2" highlight-as="html">
8685
<template>
8786
<model-viewer
88-
controls
89-
background-image="assets/small_hangar_01_1k.jpg"
90-
alt="A 3D model of a reflective sphere depicted within a hangar"
91-
src="assets/reflective-sphere.gltf"></model-viewer>
87+
controls
88+
background-image="assets/spruit_sunrise_2k.hdr"
89+
alt="A 3D model of metal spheres at varying degrees of roughness"
90+
src="assets/radiance.glb"></model-viewer>
9291
</template>
9392
</example-snippet>
9493
</div>
@@ -144,7 +143,7 @@ <h2>Cycling between <span class="attribute">background-image</span> and <span cl
144143
<template>
145144
<model-viewer
146145
id="toggle-image"
147-
backgroud-color="#ff0077"
146+
background-color="#ff0077"
148147
src="assets/DamagedHelmet/DamagedHelmet.gltf"
149148
alt="A 3D model of a damaged helmet depicted within changing environments"
150149
controls auto-rotate></model-viewer>

src/features/environment.js

Lines changed: 25 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@
1313
* limitations under the License.
1414
*/
1515

16-
import {Color} from 'three';
16+
import {BackSide, BoxBufferGeometry, Color, Mesh, ShaderLib, ShaderMaterial, UniformsUtils} from 'three';
1717

1818
import {$needsRender, $onModelLoad, $renderer, $scene, $tick} from '../model-viewer-base.js';
19+
1920
const DEFAULT_BACKGROUND_COLOR = '#ffffff';
20-
const GAMMA_TO_LINEAR = 2.2;
2121

2222
const WHITE = new Color('#ffffff');
2323

24-
const $currentCubemap = Symbol('currentCubemap');
24+
const $currentEnvironmentMap = Symbol('currentEnvironmentMap');
2525
const $setEnvironmentImage = Symbol('setEnvironmentImage');
2626
const $setEnvironmentColor = Symbol('setEnvironmentColor');
2727
const $setShadowLightColor = Symbol('setShadowLightColor');
@@ -75,17 +75,11 @@ export const EnvironmentMixin = (ModelViewerElement) => {
7575
}
7676
}
7777

78-
[$tick](time, delta) {
79-
super[$tick](time, delta);
80-
const camera = this[$scene].getCamera();
81-
this[$scene].skysphere.position.copy(camera.position);
82-
}
83-
8478
[$onModelLoad](e) {
8579
super[$onModelLoad](e);
8680

87-
if (this[$currentCubemap]) {
88-
this[$scene].model.applyEnvironmentMap(this[$currentCubemap]);
81+
if (this[$currentEnvironmentMap]) {
82+
this[$scene].model.applyEnvironmentMap(this[$currentEnvironmentMap]);
8983
this[$needsRender]();
9084
}
9185
}
@@ -100,7 +94,7 @@ export const EnvironmentMixin = (ModelViewerElement) => {
10094
return;
10195
}
10296

103-
const textures = await textureUtils.toCubemapAndEquirect(url);
97+
const textures = await textureUtils.generateEnvironmentTextures(url);
10498

10599
// If the background image has changed
106100
// while fetching textures, abort and defer to that
@@ -118,13 +112,12 @@ export const EnvironmentMixin = (ModelViewerElement) => {
118112
return;
119113
}
120114

121-
const {cubemap, equirect} = textures;
115+
const {skybox, environmentMap} = textures;
122116

123-
this[$scene].skysphere.material.color = new Color(0xffffff);
124-
this[$scene].skysphere.material.map = equirect;
125-
this[$scene].skysphere.material.needsUpdate = true;
126-
this[$currentCubemap] = cubemap;
127-
this[$scene].model.applyEnvironmentMap(cubemap);
117+
this[$scene].background = skybox;
118+
119+
this[$currentEnvironmentMap] = environmentMap;
120+
this[$scene].model.applyEnvironmentMap(environmentMap);
128121

129122
this[$setShadowLightColor](WHITE);
130123

@@ -143,18 +136,16 @@ export const EnvironmentMixin = (ModelViewerElement) => {
143136

144137
this[$deallocateTextures]();
145138

146-
const skysphereColor = this[$scene].skysphere.material.color =
147-
new Color(color);
148-
skysphereColor.convertGammaToLinear(GAMMA_TO_LINEAR);
149-
this[$setShadowLightColor](skysphereColor);
139+
const parsedColor = new Color(color);
140+
141+
this[$scene].background = parsedColor;
150142

151-
this[$scene].skysphere.material.map = null;
152-
this[$scene].skysphere.material.needsUpdate = true;
143+
this[$setShadowLightColor](parsedColor);
153144

154-
// TODO can cache this per renderer and color
155-
const cubemap = textureUtils.generateDefaultEnvMap();
156-
this[$currentCubemap] = cubemap;
157-
this[$scene].model.applyEnvironmentMap(this[$currentCubemap]);
145+
// TODO(#336): can cache this per renderer and color
146+
const environmentMap = textureUtils.generateDefaultEnvironmentMap();
147+
this[$currentEnvironmentMap] = environmentMap;
148+
this[$scene].model.applyEnvironmentMap(this[$currentEnvironmentMap]);
158149

159150
this[$needsRender]();
160151
}
@@ -165,13 +156,13 @@ export const EnvironmentMixin = (ModelViewerElement) => {
165156
}
166157

167158
[$deallocateTextures]() {
168-
if (this[$scene].skysphere.material.map) {
169-
this[$scene].skysphere.material.map.dispose();
170-
this[$scene].skysphere.material.map = null;
159+
const background = this[$scene].background;
160+
if (background && background.dispose) {
161+
background.dispose();
171162
}
172-
if (this[$currentCubemap]) {
173-
this[$currentCubemap].dispose();
174-
this[$currentCubemap] = null;
163+
if (this[$currentEnvironmentMap]) {
164+
this[$currentEnvironmentMap].dispose();
165+
this[$currentEnvironmentMap] = null;
175166
}
176167
}
177168
}

src/test/features/environment-spec.js

Lines changed: 48 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,20 @@ import {assetPath, textureMatchesMeta, timePasses, waitForEvent} from '../helper
1919
import {BasicSpecTemplate} from '../templates.js';
2020

2121
const expect = chai.expect;
22-
const BG_IMAGE_URL = assetPath('equirectangular.png');
22+
const BG_IMAGE_URL = assetPath('spruit_sunrise_2k.jpg');
2323
const MODEL_URL = assetPath('reflective-sphere.gltf');
2424

25-
const skysphereUsingMap =
25+
const backgroundHasMap =
2626
(scene, url) => {
27-
const material = scene.skysphere.material;
28-
const color = material.color.getHexString();
29-
return textureMatchesMeta(material.map, {url: url}) && color === 'ffffff';
27+
return textureMatchesMeta(scene.background.texture, {url: url});
3028
}
3129

32-
const skysphereUsingColor =
30+
const backgroundHasColor =
3331
(scene, hex) => {
34-
const {color, map} = scene.skysphere.material;
35-
// Invert gamma correct to match passed in hex
36-
const gammaCorrectedColor = color.clone().convertLinearToGamma(2.2);
37-
38-
return map == null && gammaCorrectedColor.getHexString() === hex;
32+
if (!scene.background || !scene.background.isColor) {
33+
return false;
34+
}
35+
return scene.background.getHexString() === hex;
3936
}
4037

4138
/**
@@ -47,7 +44,7 @@ const skysphereUsingColor =
4744
* @param {THREE.Scene} scene
4845
* @param {Object} meta
4946
*/
50-
const modelUsingEnvmap = (scene, meta) => {
47+
const modelUsingEnvMap = (scene, meta) => {
5148
let found = false;
5249
scene.model.traverse(object => {
5350
if (!object.material || !object.material.envMap) {
@@ -71,18 +68,22 @@ const modelUsingEnvmap = (scene, meta) => {
7168
* @param {Model} model
7269
* @param {Object} meta
7370
*/
74-
const waitForEnvmap = (model, meta) => waitForEvent(
75-
model,
76-
'envmap-change',
77-
e => textureMatchesMeta(e.value, {...meta, type: 'EnvironmentMap'}));
71+
const waitForEnvMap = (model, meta) =>
72+
waitForEvent(model, 'envmap-change', event => {
73+
return textureMatchesMeta(event.value, {...meta});
74+
});
7875

7976
/**
8077
* Returns a promise that resolves when a given element is loaded
8178
* and has an environment map set that matches the passed in meta.
8279
* @see textureMatchesMeta
8380
*/
84-
const waitForLoadAndEnvmap = (scene, element, meta) => Promise.all(
85-
[waitForEvent(element, 'load'), waitForEnvmap(scene.model, meta)]);
81+
const waitForLoadAndEnvMap =
82+
(scene, element, meta) => {
83+
const load = waitForEvent(element, 'load');
84+
const envMap = waitForEnvMap(scene.model, meta);
85+
return Promise.all([load, envMap]);
86+
}
8687

8788
suite('ModelViewerElementBase with EnvironmentMixin', () => {
8889
let nextId = 0;
@@ -109,34 +110,34 @@ suite('ModelViewerElementBase with EnvironmentMixin', () => {
109110
BasicSpecTemplate(() => ModelViewerElement, () => tagName);
110111

111112
test(
112-
'has default skysphere if no background-image or background-color',
113+
'has default background if no background-image or background-color',
113114
() => {
114-
expect(skysphereUsingColor(scene, 'ffffff')).to.be.equal(true);
115+
expect(backgroundHasColor(scene, 'ffffff')).to.be.equal(true);
115116
});
116117

117118
test(
118-
'has default skysphere if no background-image or background-color when in DOM',
119+
'has default background if no background-image or background-color when in DOM',
119120
async () => {
120121
document.body.appendChild(element);
121122
await timePasses();
122-
expect(skysphereUsingColor(scene, 'ffffff')).to.be.equal(true);
123+
expect(backgroundHasColor(scene, 'ffffff')).to.be.equal(true);
123124
});
124125

125126
suite('with a background-image property', () => {
126127
suite('and a src property', () => {
127128
setup(async () => {
128-
let onLoad = waitForLoadAndEnvmap(scene, element, {url: BG_IMAGE_URL});
129+
let onLoad = waitForLoadAndEnvMap(scene, element, {url: BG_IMAGE_URL});
129130
element.src = MODEL_URL;
130131
element.backgroundImage = BG_IMAGE_URL;
131132
await onLoad;
132133
});
133134

134-
test('displays skysphere with the correct map', async function() {
135-
expect(skysphereUsingMap(scene, element.backgroundImage)).to.be.ok;
135+
test('displays background with the correct map', async function() {
136+
expect(backgroundHasMap(scene, element.backgroundImage)).to.be.ok;
136137
});
137138

138139
test('applies the image as an environment map', async function() {
139-
expect(modelUsingEnvmap(scene, {
140+
expect(modelUsingEnvMap(scene, {
140141
url: element.backgroundImage
141142
})).to.be.ok;
142143
});
@@ -159,26 +160,30 @@ suite('ModelViewerElementBase with EnvironmentMixin', () => {
159160
suite('with a background-color property', () => {
160161
suite('and a src property', () => {
161162
setup(async () => {
162-
let onLoad = waitForLoadAndEnvmap(scene, element, {url: null});
163+
let onLoad = waitForLoadAndEnvMap(scene, element, {
164+
url: null,
165+
});
163166
element.src = MODEL_URL;
164167
element.backgroundColor = '#ff0077';
165168
await onLoad;
166169
});
167170

168-
test('displays skysphere with the correct color', async function() {
169-
expect(skysphereUsingColor(scene, 'ff0077')).to.be.ok;
171+
test('displays background with the correct color', async function() {
172+
expect(backgroundHasColor(scene, 'ff0077')).to.be.ok;
170173
});
171174

172175
test('applies a generated environment map on model', async function() {
173-
expect(modelUsingEnvmap(scene, {url: null})).to.be.ok;
176+
expect(modelUsingEnvMap(scene, {
177+
url: null,
178+
})).to.be.ok;
174179
});
175180

176181
test(
177-
'displays skysphere with correct color after attaching to DOM',
182+
'displays background with correct color after attaching to DOM',
178183
async function() {
179184
document.body.appendChild(element);
180185
await timePasses();
181-
expect(skysphereUsingColor(scene, 'ff0077')).to.be.ok;
186+
expect(backgroundHasColor(scene, 'ff0077')).to.be.ok;
182187
});
183188
test('the directional light is tinted', () => {
184189
const lightColor = scene.shadowLight.color.getHexString().toLowerCase();
@@ -189,34 +194,34 @@ suite('ModelViewerElementBase with EnvironmentMixin', () => {
189194

190195
suite('with background-color and background-image properties', () => {
191196
setup(async () => {
192-
let onLoad = waitForLoadAndEnvmap(scene, element, {url: BG_IMAGE_URL});
197+
let onLoad = waitForLoadAndEnvMap(scene, element, {url: BG_IMAGE_URL});
193198
element.setAttribute('src', MODEL_URL);
194199
element.setAttribute('background-color', '#ff0077');
195200
element.setAttribute('background-image', BG_IMAGE_URL);
196201
await onLoad;
197202
});
198203

199-
test('displays skysphere with background-image', async function() {
200-
expect(skysphereUsingMap(scene, element.backgroundImage)).to.be.ok;
204+
test('displays background with background-image', async function() {
205+
expect(backgroundHasMap(scene, element.backgroundImage)).to.be.ok;
201206
});
202207

203-
test('applies background-image envmap on model', async function() {
204-
expect(modelUsingEnvmap(scene, {url: element.backgroundImage})).to.be.ok;
208+
test('applies background-image environment map on model', async function() {
209+
expect(modelUsingEnvMap(scene, {url: element.backgroundImage})).to.be.ok;
205210
});
206211

207212
suite('and background-image subsequently removed', () => {
208213
setup(async () => {
209-
let envmapChanged = waitForEnvmap(scene.model, {url: null});
214+
let envMapChanged = waitForEnvMap(scene.model, {url: null});
210215
element.removeAttribute('background-image');
211-
await envmapChanged;
216+
await envMapChanged;
212217
});
213218

214-
test('displays skysphere with background-color', async function() {
215-
expect(skysphereUsingColor(scene, 'ff0077')).to.be.ok;
219+
test('displays background with background-color', async function() {
220+
expect(backgroundHasColor(scene, 'ff0077')).to.be.ok;
216221
});
217222

218-
test('reapplies generated envmap on model', async function() {
219-
expect(modelUsingEnvmap(scene, {url: null})).to.be.ok;
223+
test('reapplies generated environment map on model', async function() {
224+
expect(modelUsingEnvMap(scene, {url: null})).to.be.ok;
220225
});
221226
});
222227
});

0 commit comments

Comments
 (0)