Skip to content

Commit e6ac9dd

Browse files
authored
fix: enable export of scenes with KTX2 compressed textures (#5142)
* fix: enable export of scenes with KTX2 compressed textures Call setTextureUtils() on GLTFExporter to decompress CompressedTexture instances before serialization. The decompress wrapper preserves the original texture's flipY to prevent orientation issues during export. Fixes #5010. * fix: enable export of scenes with KTX2 compressed textures Replace Three.js's decompress() with a custom implementation that renders compressed textures to a WebGLRenderTarget instead of the renderer's main canvas. This avoids corrupting model-viewer's shared renderer state, which caused the model to disappear after export. The custom decompressTexture() function: - Uses an off-screen WebGLRenderTarget (no canvas corruption) - Saves and restores renderer toneMapping state - Handles sRGB color space conversion in the shader - Preserves the original texture's flipY property Also strengthen the KTX2 export test to verify the scene remains intact (materials, dimensions) after export, before re-importing. * fix: simplify KTX2 export using Three.js native decompress with isolated context Replace custom decompressTexture() (shaders, render targets, pixel readback) with Three.js's built-in decompress() from WebGLTextureUtils, passing no renderer so Three creates and disposes a temporary WebGL context. This avoids corrupting model-viewer's shared canvas without maintaining custom GPU code. --------- Co-authored-by: Douglas Holm <161740744+MyrqvistDouglas@users.noreply.github.com>
1 parent 6fa0f87 commit e6ac9dd

2 files changed

Lines changed: 39 additions & 0 deletions

File tree

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import {property} from 'lit/decorators.js';
1717
import {CanvasTexture, Object3D, RepeatWrapping, SRGBColorSpace, Texture, VideoTexture} from 'three';
1818
import {GLTFExporter, GLTFExporterOptions} from 'three/examples/jsm/exporters/GLTFExporter.js';
19+
import {decompress} from 'three/examples/jsm/utils/WebGLTextureUtils.js';
1920

2021
import ModelViewerElementBase, {$needsRender, $onModelLoad, $progressTracker, $renderer, $scene} from '../model-viewer-base.js';
2122
import {GLTF} from '../three-components/gltf-instance/gltf-defaulted.js';
@@ -289,6 +290,13 @@ export const SceneGraphMixin = <T extends Constructor<ModelViewerElementBase>>(
289290
.register(
290291
(writer: any) =>
291292
new GLTFExporterMaterialsVariantsExtension(writer));
293+
294+
exporter.setTextureUtils({
295+
decompress: (texture: Texture, maxTextureSize?: number) => {
296+
return decompress(texture, maxTextureSize ?? Infinity, undefined);
297+
}
298+
});
299+
292300
let exportTarget: Object3D;
293301
if (scene.models.length > 1) {
294302
exportTarget = new Object3D();

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,37 @@ suite('scene-graph/texture', () => {
7575
.to.be.equal('image/png');
7676
});
7777

78+
test(
79+
'exports and re-imports a model with KTX2 compressed texture',
80+
async () => {
81+
const ktx2Texture = await element.createTexture(KTX2_TEXTURE_PATH);
82+
element.model!.materials[0]
83+
.pbrMetallicRoughness.baseColorTexture!.setTexture(ktx2Texture);
84+
85+
const materialCount = element.model!.materials.length;
86+
const dim = element.getDimensions();
87+
88+
const exported = await element.exportScene({binary: true});
89+
expect(exported).to.be.not.undefined;
90+
expect(exported.size).to.be.greaterThan(500);
91+
92+
// Verify scene is still intact after export (no renderer
93+
// corruption).
94+
expect(element.model).to.not.be.null;
95+
expect(element.model!.materials.length).to.equal(materialCount);
96+
const dimAfter = element.getDimensions();
97+
expect(dimAfter.x).to.be.closeTo(dim.x, 0.001);
98+
expect(dimAfter.y).to.be.closeTo(dim.y, 0.001);
99+
expect(dimAfter.z).to.be.closeTo(dim.z, 0.001);
100+
101+
const url = URL.createObjectURL(exported);
102+
element.src = url;
103+
await waitForEvent(element, 'load');
104+
105+
expect(element.model).to.not.be.null;
106+
expect(element.model!.materials.length).to.be.greaterThan(0);
107+
});
108+
78109
test('Verify legacy correlatedObjects are updated.', async () => {
79110
const newUUID: string|undefined = texture?.source[$threeTexture]?.uuid;
80111

0 commit comments

Comments
 (0)