Skip to content

Commit 70fe8a8

Browse files
authored
Export JPGs in GLB (#2670)
* export uploaded jpgs as jpgs * select uploaded texture * updated tests and moved logic to MV * updated comment * added test * added event type
1 parent 6fcfae2 commit 70fe8a8

File tree

6 files changed

+81
-22
lines changed

6 files changed

+81
-22
lines changed

packages/model-viewer/src/features/scene-graph.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
*/
1515

1616
import {property} from 'lit-element';
17-
import {Euler, MeshStandardMaterial, RepeatWrapping, sRGBEncoding, Texture, TextureLoader} from 'three';
17+
import {Euler, MeshStandardMaterial, RepeatWrapping, RGBFormat, sRGBEncoding, Texture, TextureLoader} from 'three';
1818
import {GLTFExporter, GLTFExporterOptions} from 'three/examples/jsm/exporters/GLTFExporter';
1919

2020
import ModelViewerElementBase, {$needsRender, $onModelLoad, $renderer, $scene} from '../model-viewer-base.js';
@@ -53,7 +53,7 @@ export interface SceneGraphInterface {
5353
scale: string;
5454
readonly originalGltfJson: GLTF|null;
5555
exportScene(options?: SceneExportOptions): Promise<Blob>;
56-
createTexture(uri: string): Promise<ModelViewerTexture|null>;
56+
createTexture(uri: string, type?: string): Promise<ModelViewerTexture|null>;
5757
}
5858

5959
/**
@@ -113,7 +113,8 @@ export const SceneGraphMixin = <T extends Constructor<ModelViewerElementBase>>(
113113
};
114114
}
115115

116-
async createTexture(uri: string): Promise<ModelViewerTexture|null> {
116+
async createTexture(uri: string, type: string = 'image/png'):
117+
Promise<ModelViewerTexture|null> {
117118
const currentGLTF = this[$currentGLTF];
118119
const texture: Texture = await new Promise<Texture>(
119120
(resolve) => this[$textureLoader].load(uri, resolve));
@@ -125,6 +126,14 @@ export const SceneGraphMixin = <T extends Constructor<ModelViewerElementBase>>(
125126
texture.wrapS = RepeatWrapping;
126127
texture.wrapT = RepeatWrapping;
127128
texture.flipY = false;
129+
// This hack is because GLTFExporter checks if format is RGB vs RGBA to
130+
// decide if it should save as JPEG vs PNG. However, TextureLoader sets
131+
// format based on if the url ends in .jpg, which does not work for an
132+
// ObjectURL like we're passing here. So, to keep from inflating all JPEGs
133+
// to PNGs, we allow the user of the API to specify the type.
134+
if (type === 'image/jpeg') {
135+
texture.format = RGBFormat;
136+
}
128137

129138
return new ModelViewerTexture(this[$getOnUpdateMethod](), texture);
130139
}

packages/model-viewer/src/test/features/scene-graph-spec.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ const expect = chai.expect;
2525

2626
const ASTRONAUT_GLB_PATH = assetPath('models/Astronaut.glb');
2727
const HORSE_GLB_PATH = assetPath('models/Horse.glb');
28-
const CUBES_GLB_PATH = assetPath('models/cubes.gltf');
28+
const CUBES_GLB_PATH = assetPath('models/cubes.gltf'); // has variants
29+
const CUBE_GLB_PATH = assetPath('models/cube.gltf'); // has UV coords
2930
const SUNRISE_IMG_PATH = assetPath('environments/spruit_sunrise_1k_LDR.jpg');
3031

3132
suite('ModelViewerElementBase with SceneGraphMixin', () => {
@@ -101,6 +102,30 @@ suite('ModelViewerElementBase with SceneGraphMixin', () => {
101102
expect(glTFroot.children[1].userData.variantMaterials.size).to.be.eq(3);
102103
});
103104
});
105+
106+
test(
107+
'When loading a new JPEG texture from an ObjectURL, the GLB does not export PNG',
108+
async () => {
109+
element.src = CUBE_GLB_PATH;
110+
await waitForEvent(element, 'load');
111+
await rafPasses();
112+
113+
const url = assetPath(
114+
'models/glTF-Sample-Models/2.0/DamagedHelmet/glTF/Default_albedo.jpg');
115+
const blob = await fetch(url).then(r => r.blob());
116+
const objectUrl = URL.createObjectURL(blob);
117+
const texture = await element.createTexture(objectUrl, 'image/jpeg');
118+
119+
element.model!.materials[0]
120+
.pbrMetallicRoughness.baseColorTexture.setTexture(texture);
121+
122+
const exported = await element.exportScene({binary: true});
123+
expect(exported).to.be.not.undefined;
124+
// The JPEG is ~1 Mb and the equivalent PNG is about ~6 Mb, so this
125+
// just checks we saved an image and it wasn't too big.
126+
expect(exported.size).to.be.greaterThan(0.5e6);
127+
expect(exported.size).to.be.lessThan(1.5e6);
128+
});
104129
});
105130

106131
suite('with a loaded scene graph', () => {

packages/space-opera/src/components/materials_panel/materials_panel.ts

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import {CheckboxElement} from '../shared/checkbox/checkbox.js';
4343
import {ColorPicker} from '../shared/color_picker/color_picker.js';
4444
import {Dropdown} from '../shared/dropdown/dropdown.js';
4545
import {SliderWithInputElement} from '../shared/slider_with_input/slider_with_input.js';
46-
import {TexturePicker} from '../shared/texture_picker/texture_picker.js';
46+
import {FileDetails, TexturePicker} from '../shared/texture_picker/texture_picker.js';
4747
import {ALPHA_BLEND_MODES} from '../utils/gltf_constants.js';
4848
import {checkFinite} from '../utils/reducer_utils.js';
4949

@@ -393,15 +393,19 @@ export class MaterialPanel extends ConnectedLitElement {
393393
}
394394
}
395395

396-
async onTextureUpload(uri: string, textureInfo: TextureInfo) {
397-
if (this.thumbnailsById.has(uri)) {
396+
async onTextureUpload(
397+
detail: FileDetails, texturePicker: TexturePicker,
398+
textureInfo: TextureInfo) {
399+
const {url, type} = detail;
400+
if (this.thumbnailsById.has(url)) {
398401
console.log('URL collision! Texture not updated.');
399402
return;
400403
}
401-
const texture = await getModelViewer()?.createTexture(uri);
404+
const texture = await getModelViewer()?.createTexture(url, type);
402405
if (texture == null) {
403406
return;
404407
}
408+
405409
textureInfo.setTexture(texture);
406410
const id = await pushThumbnail(this.thumbnailsById, textureInfo);
407411
// Trigger async panel update / render
@@ -411,6 +415,7 @@ export class MaterialPanel extends ConnectedLitElement {
411415
// Trigger async texture_picker update / render
412416
this.thumbnailUrls = [...this.thumbnailUrls];
413417
this.thumbnailUrls.push(this.thumbnailsById.get(id)!.objectUrl);
418+
texturePicker.selectedIndex = this.thumbnailIds.indexOf(id);
414419
}
415420
reduxStore.dispatch(dispatchModelDirty());
416421
this.dispatchEvent(new CustomEvent('texture-upload-complete'));
@@ -422,9 +427,11 @@ export class MaterialPanel extends ConnectedLitElement {
422427
this.getMaterial().pbrMetallicRoughness.baseColorTexture);
423428
}
424429

425-
onBaseColorTextureUpload(event: CustomEvent) {
430+
onBaseColorTextureUpload(event: CustomEvent<FileDetails>) {
426431
this.onTextureUpload(
427-
event.detail, this.getMaterial().pbrMetallicRoughness.baseColorTexture);
432+
event.detail,
433+
this.baseColorTexturePicker,
434+
this.getMaterial().pbrMetallicRoughness.baseColorTexture);
428435
}
429436

430437
onMetallicRoughnessTextureChange() {
@@ -433,9 +440,10 @@ export class MaterialPanel extends ConnectedLitElement {
433440
this.getMaterial().pbrMetallicRoughness.metallicRoughnessTexture);
434441
}
435442

436-
onMetallicRoughnessTextureUpload(event: CustomEvent) {
443+
onMetallicRoughnessTextureUpload(event: CustomEvent<FileDetails>) {
437444
this.onTextureUpload(
438445
event.detail,
446+
this.metallicRoughnessTexturePicker,
439447
this.getMaterial().pbrMetallicRoughness.metallicRoughnessTexture);
440448
}
441449

@@ -444,17 +452,23 @@ export class MaterialPanel extends ConnectedLitElement {
444452
this.selectedNormalTextureId, this.getMaterial().normalTexture);
445453
}
446454

447-
onNormalTextureUpload(event: CustomEvent) {
448-
this.onTextureUpload(event.detail, this.getMaterial().normalTexture);
455+
onNormalTextureUpload(event: CustomEvent<FileDetails>) {
456+
this.onTextureUpload(
457+
event.detail,
458+
this.normalTexturePicker,
459+
this.getMaterial().normalTexture);
449460
}
450461

451462
onEmissiveTextureChange() {
452463
this.onTextureChange(
453464
this.selectedEmissiveTextureId, this.getMaterial().emissiveTexture);
454465
}
455466

456-
onEmissiveTextureUpload(event: CustomEvent) {
457-
this.onTextureUpload(event.detail, this.getMaterial().emissiveTexture);
467+
onEmissiveTextureUpload(event: CustomEvent<FileDetails>) {
468+
this.onTextureUpload(
469+
event.detail,
470+
this.emissiveTexturePicker,
471+
this.getMaterial().emissiveTexture);
458472
}
459473

460474
onEmissiveFactorChanged() {
@@ -467,8 +481,11 @@ export class MaterialPanel extends ConnectedLitElement {
467481
this.selectedOcclusionTextureId, this.getMaterial().occlusionTexture);
468482
}
469483

470-
onOcclusionTextureUpload(event: CustomEvent) {
471-
this.onTextureUpload(event.detail, this.getMaterial().occlusionTexture);
484+
onOcclusionTextureUpload(event: CustomEvent<FileDetails>) {
485+
this.onTextureUpload(
486+
event.detail,
487+
this.occlusionTexturePicker,
488+
this.getMaterial().occlusionTexture);
472489
}
473490

474491
onAlphaModeSelect() {

packages/space-opera/src/components/shared/texture_picker/texture_picker.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ import {styles} from './texture_picker.css.js';
3030

3131
const ACCEPT_IMAGE_TYPE = IMAGE_MIME_TYPES.join(',');
3232

33+
export interface FileDetails {
34+
url: string;
35+
type: string;
36+
}
37+
3338
/**
3439
* LitElement for a texture picker which allows user to select one of the
3540
* texture images presented
@@ -130,7 +135,8 @@ export class TexturePicker extends LitElement {
130135
}
131136

132137
const url = createSafeObjectURL(files[0]).unsafeUrl;
133-
this.dispatchEvent(new CustomEvent('texture-uploaded', {detail: url}));
138+
this.dispatchEvent(new CustomEvent<FileDetails>(
139+
'texture-uploaded', {detail: {url, type: files[0].type}}));
134140
}
135141
}
136142

packages/space-opera/src/test/materials_panel/materials_panel_test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ async function checkUpload(
3636

3737
expect(imageList.children.length).toEqual(listLength + 1);
3838

39-
texturePicker.dispatchEvent(
40-
new CustomEvent('texture-uploaded', {detail: TEXTURE_PATH}));
39+
texturePicker.dispatchEvent(new CustomEvent(
40+
'texture-uploaded', {detail: {url: TEXTURE_PATH, type: 'image/png'}}));
4141
await waitForEvent(panel, 'texture-upload-complete');
4242
await panel.updateComplete;
4343

packages/space-opera/src/test/shared/texture_picker/texture_picker_test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import '../../../components/shared/texture_picker/texture_picker.js';
2121
import {IconButton} from '@material/mwc-icon-button';
2222

2323
import {FileModalElement} from '../../../components/file_modal/file_modal.js';
24-
import {TexturePicker} from '../../../components/shared/texture_picker/texture_picker.js';
24+
import {FileDetails, TexturePicker} from '../../../components/shared/texture_picker/texture_picker.js';
2525
import {createSafeObjectURL} from '../../../components/utils/create_object_url.js';
2626
import {generatePngBlob} from '../../utils/test_utils.js';
2727

@@ -81,7 +81,9 @@ describe('texture picker test', () => {
8181
expect(eventListenerSpy).toHaveBeenCalledTimes(1);
8282
const eventListenerArguments = eventListenerSpy.calls.first().args;
8383
expect(eventListenerArguments.length).toBe(1);
84-
expect(eventListenerArguments[0].detail).toBeInstanceOf(String);
84+
const {url, type} = eventListenerArguments[0].detail as FileDetails;
85+
expect(url).toBeInstanceOf(String);
86+
expect(type).toEqual('image/jpeg');
8587
});
8688

8789
it('dispatches an event with undefined selectedIndex on null texture click',

0 commit comments

Comments
 (0)