Skip to content

Commit deb62b1

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

File tree

16 files changed

+569
-217
lines changed

16 files changed

+569
-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
}
@@ -95,7 +89,7 @@ export const EnvironmentMixin = (ModelViewerElement) => {
9589
*/
9690
async[$setEnvironmentImage](url) {
9791
const textureUtils = this[$renderer].textureUtils;
98-
const textures = await textureUtils.toCubemapAndEquirect(url);
92+
const textures = await textureUtils.generateEnvironmentTextures(url);
9993

10094
// If the background image has changed
10195
// while fetching textures, abort and defer to that
@@ -113,13 +107,12 @@ export const EnvironmentMixin = (ModelViewerElement) => {
113107
return;
114108
}
115109

116-
const {cubemap, equirect} = textures;
110+
const {skybox, environmentMap} = textures;
117111

118-
this[$scene].skysphere.material.color = new Color(0xffffff);
119-
this[$scene].skysphere.material.map = equirect;
120-
this[$scene].skysphere.material.needsUpdate = true;
121-
this[$currentCubemap] = cubemap;
122-
this[$scene].model.applyEnvironmentMap(cubemap);
112+
this[$scene].background = skybox;
113+
114+
this[$currentEnvironmentMap] = environmentMap;
115+
this[$scene].model.applyEnvironmentMap(environmentMap);
123116

124117
this[$setShadowLightColor](WHITE);
125118

@@ -134,18 +127,16 @@ export const EnvironmentMixin = (ModelViewerElement) => {
134127

135128
this[$deallocateTextures]();
136129

137-
const skysphereColor = this[$scene].skysphere.material.color =
138-
new Color(color);
139-
skysphereColor.convertGammaToLinear(GAMMA_TO_LINEAR);
140-
this[$setShadowLightColor](skysphereColor);
130+
const parsedColor = new Color(color);
131+
132+
this[$scene].background = parsedColor;
141133

142-
this[$scene].skysphere.material.map = null;
143-
this[$scene].skysphere.material.needsUpdate = true;
134+
this[$setShadowLightColor](parsedColor);
144135

145-
// TODO can cache this per renderer and color
146-
const cubemap = textureUtils.generateDefaultEnvMap();
147-
this[$currentCubemap] = cubemap;
148-
this[$scene].model.applyEnvironmentMap(this[$currentCubemap]);
136+
// TODO(#336): can cache this per renderer and color
137+
const environmentMap = textureUtils.generateDefaultEnvironmentMap();
138+
this[$currentEnvironmentMap] = environmentMap;
139+
this[$scene].model.applyEnvironmentMap(this[$currentEnvironmentMap]);
149140

150141
this[$needsRender]();
151142
}
@@ -156,13 +147,13 @@ export const EnvironmentMixin = (ModelViewerElement) => {
156147
}
157148

158149
[$deallocateTextures]() {
159-
if (this[$scene].skysphere.material.map) {
160-
this[$scene].skysphere.material.map.dispose();
161-
this[$scene].skysphere.material.map = null;
150+
const background = this[$scene].background;
151+
if (background && background.dispose) {
152+
background.dispose();
162153
}
163-
if (this[$currentCubemap]) {
164-
this[$currentCubemap].dispose();
165-
this[$currentCubemap] = null;
154+
if (this[$currentEnvironmentMap]) {
155+
this[$currentEnvironmentMap].dispose();
156+
this[$currentEnvironmentMap] = null;
166157
}
167158
}
168159
}

src/test/features/environment-spec.js

Lines changed: 48 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,20 @@ import ModelViewerElementBase, {$scene} from '../../model-viewer-base.js';
1818
import {assetPath, textureMatchesMeta, timePasses, waitForEvent} from '../helpers.js';
1919

2020
const expect = chai.expect;
21-
const BG_IMAGE_URL = assetPath('equirectangular.png');
21+
const BG_IMAGE_URL = assetPath('spruit_sunrise_2k.jpg');
2222
const MODEL_URL = assetPath('reflective-sphere.gltf');
2323

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

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

4037
/**
@@ -46,7 +43,7 @@ const skysphereUsingColor =
4643
* @param {THREE.Scene} scene
4744
* @param {Object} meta
4845
*/
49-
const modelUsingEnvmap = (scene, meta) => {
46+
const modelUsingEnvMap = (scene, meta) => {
5047
let found = false;
5148
scene.model.traverse(object => {
5249
if (!object.material || !object.material.envMap) {
@@ -70,18 +67,22 @@ const modelUsingEnvmap = (scene, meta) => {
7067
* @param {Model} model
7168
* @param {Object} meta
7269
*/
73-
const waitForEnvmap = (model, meta) => waitForEvent(
74-
model,
75-
'envmap-change',
76-
e => textureMatchesMeta(e.value, {...meta, type: 'EnvironmentMap'}));
70+
const waitForEnvMap = (model, meta) =>
71+
waitForEvent(model, 'envmap-change', event => {
72+
return textureMatchesMeta(event.value, {...meta});
73+
});
7774

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

8687
suite('ModelViewerElementBase with EnvironmentMixin', () => {
8788
let nextId = 0;
@@ -106,34 +107,34 @@ suite('ModelViewerElementBase with EnvironmentMixin', () => {
106107
teardown(() => element.parentNode && element.parentNode.removeChild(element));
107108

108109
test(
109-
'has default skysphere if no background-image or background-color',
110+
'has default background if no background-image or background-color',
110111
() => {
111-
expect(skysphereUsingColor(scene, 'ffffff')).to.be.equal(true);
112+
expect(backgroundHasColor(scene, 'ffffff')).to.be.equal(true);
112113
});
113114

114115
test(
115-
'has default skysphere if no background-image or background-color when in DOM',
116+
'has default background if no background-image or background-color when in DOM',
116117
async () => {
117118
document.body.appendChild(element);
118119
await timePasses();
119-
expect(skysphereUsingColor(scene, 'ffffff')).to.be.equal(true);
120+
expect(backgroundHasColor(scene, 'ffffff')).to.be.equal(true);
120121
});
121122

122123
suite('with a background-image property', () => {
123124
suite('and a src property', () => {
124125
setup(async () => {
125-
let onLoad = waitForLoadAndEnvmap(scene, element, {url: BG_IMAGE_URL});
126+
let onLoad = waitForLoadAndEnvMap(scene, element, {url: BG_IMAGE_URL});
126127
element.src = MODEL_URL;
127128
element.backgroundImage = BG_IMAGE_URL;
128129
await onLoad;
129130
});
130131

131-
test('displays skysphere with the correct map', async function() {
132-
expect(skysphereUsingMap(scene, element.backgroundImage)).to.be.ok;
132+
test('displays background with the correct map', async function() {
133+
expect(backgroundHasMap(scene, element.backgroundImage)).to.be.ok;
133134
});
134135

135136
test('applies the image as an environment map', async function() {
136-
expect(modelUsingEnvmap(scene, {
137+
expect(modelUsingEnvMap(scene, {
137138
url: element.backgroundImage
138139
})).to.be.ok;
139140
});
@@ -156,26 +157,30 @@ suite('ModelViewerElementBase with EnvironmentMixin', () => {
156157
suite('with a background-color property', () => {
157158
suite('and a src property', () => {
158159
setup(async () => {
159-
let onLoad = waitForLoadAndEnvmap(scene, element, {url: null});
160+
let onLoad = waitForLoadAndEnvMap(scene, element, {
161+
url: null,
162+
});
160163
element.src = MODEL_URL;
161164
element.backgroundColor = '#ff0077';
162165
await onLoad;
163166
});
164167

165-
test('displays skysphere with the correct color', async function() {
166-
expect(skysphereUsingColor(scene, 'ff0077')).to.be.ok;
168+
test('displays background with the correct color', async function() {
169+
expect(backgroundHasColor(scene, 'ff0077')).to.be.ok;
167170
});
168171

169172
test('applies a generated environment map on model', async function() {
170-
expect(modelUsingEnvmap(scene, {url: null})).to.be.ok;
173+
expect(modelUsingEnvMap(scene, {
174+
url: null,
175+
})).to.be.ok;
171176
});
172177

173178
test(
174-
'displays skysphere with correct color after attaching to DOM',
179+
'displays background with correct color after attaching to DOM',
175180
async function() {
176181
document.body.appendChild(element);
177182
await timePasses();
178-
expect(skysphereUsingColor(scene, 'ff0077')).to.be.ok;
183+
expect(backgroundHasColor(scene, 'ff0077')).to.be.ok;
179184
});
180185
test('the directional light is tinted', () => {
181186
const lightColor = scene.shadowLight.color.getHexString().toLowerCase();
@@ -186,34 +191,34 @@ suite('ModelViewerElementBase with EnvironmentMixin', () => {
186191

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

196-
test('displays skysphere with background-image', async function() {
197-
expect(skysphereUsingMap(scene, element.backgroundImage)).to.be.ok;
201+
test('displays background with background-image', async function() {
202+
expect(backgroundHasMap(scene, element.backgroundImage)).to.be.ok;
198203
});
199204

200-
test('applies background-image envmap on model', async function() {
201-
expect(modelUsingEnvmap(scene, {url: element.backgroundImage})).to.be.ok;
205+
test('applies background-image environment map on model', async function() {
206+
expect(modelUsingEnvMap(scene, {url: element.backgroundImage})).to.be.ok;
202207
});
203208

204209
suite('and background-image subsequently removed', () => {
205210
setup(async () => {
206-
let envmapChanged = waitForEnvmap(scene.model, {url: null});
211+
let envMapChanged = waitForEnvMap(scene.model, {url: null});
207212
element.removeAttribute('background-image');
208-
await envmapChanged;
213+
await envMapChanged;
209214
});
210215

211-
test('displays skysphere with background-color', async function() {
212-
expect(skysphereUsingColor(scene, 'ff0077')).to.be.ok;
216+
test('displays background with background-color', async function() {
217+
expect(backgroundHasColor(scene, 'ff0077')).to.be.ok;
213218
});
214219

215-
test('reapplies generated envmap on model', async function() {
216-
expect(modelUsingEnvmap(scene, {url: null})).to.be.ok;
220+
test('reapplies generated environment map on model', async function() {
221+
expect(modelUsingEnvMap(scene, {url: null})).to.be.ok;
217222
});
218223
});
219224
});

0 commit comments

Comments
 (0)