Skip to content

fix: enable export of scenes with KTX2 compressed textures#5142

Merged
diegoteran merged 3 commits intogoogle:masterfrom
douglasholm:fix/ktx2-export-compressed-texture
Apr 7, 2026
Merged

fix: enable export of scenes with KTX2 compressed textures#5142
diegoteran merged 3 commits intogoogle:masterfrom
douglasholm:fix/ktx2-export-compressed-texture

Conversation

@douglasholm
Copy link
Copy Markdown
Contributor

Reference Issue

Fixes #5010 — Cannot export scene with KTX2 compressed model

Summary

exportScene() fails when the scene contains KTX2 compressed textures because GLTFExporter encounters CompressedTexture instances it can't serialize. Three.js requires setTextureUtils() to be called with a decompress function to handle these. This was noted as a known limitation in #5141.

This PR wires up Three.js's WebGLTextureUtils.decompress() on the exporter, reusing model-viewer's existing shared renderer. The wrapper also preserves the original texture's flipY property, which WebGLTextureUtils.decompress() doesn't copy — without this, exported KTX2 textures would be vertically flipped since CompressedTexture uses flipY = false while the decompressed CanvasTexture defaults to true.

Changes

  • scene-graph.ts: Call setTextureUtils() on GLTFExporter with a decompress wrapper that preserves flipY
  • texture-spec.ts: Added round-trip test — create KTX2 texture, export, re-import, verify model loads

Testing

  • Chromium: 121 passed, 0 failed
  • Firefox/WebKit: all pass (new test included)
  • Pre-existing Firefox flaky failures unrelated to this change

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 google#5010.
Copy link
Copy Markdown
Collaborator

@diegoteran diegoteran left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested this change locally and the model disappears once the export is requested.

This is a regression, can you look into this?

Thank you

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.
@douglasholm
Copy link
Copy Markdown
Contributor Author

douglasholm commented Apr 6, 2026

Thank you for catching that @diegoteran, and sorry for the delayed fix!

Root cause: The original implementation used Three.js's built-in WebGLTextureUtils.decompress(), which renders to the renderer's main canvas. This caused three issues:

  1. decompress() calls setSize(), clear(), and render() on the shared renderer without restoring state — trashing the canvas and making the model disappear
  2. Rendering to the DOM canvas instead of an off-screen target produced texture distortion (banding artifacts)
  3. The renderer's toneMapping leaked into the decompressed pixels, degrading texture colors on re-import

Fix: Replaced decompress() with a custom decompressTexture() that renders to a WebGLRenderTarget instead of the main canvas. This avoids all renderer state corruption, handles sRGB color space conversion correctly in the shader, and properly disposes GPU resources.

Also strengthened the test to verify the scene remains intact after export (materials and dimensions unchanged) before re-importing.

Tested across all CI browser contexts (2x WebKit, 2x Chromium) and manually verified the full export/re-import round trip with a production KTX2 model.

image

Copy link
Copy Markdown
Collaborator

@diegoteran diegoteran left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Douglas, thanks for the fix! I do appreciate the effort and descriptive messages you add.

Can we simplify the KTX2 export by passing null as the renderer to Three's decompress() instead of using the custom shader approach?

Passing null forces Three to create a temporary, isolated WebGL context, which avoids the canvas corruption without needing custom code in our repo. This would allow us to drop the custom decompressTexture function entirely. The trade-off is a performance hit for creating the context, but since export is a one-off action, it seems worth it to reduce maintenance.

…ted 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.
@douglasholm
Copy link
Copy Markdown
Contributor Author

Hello again, hopefully this is our last round on this fingers crossed!

The commit passed all tests locally, and worked as intended when tested manually on the same production model as I used previously.

Changes: Replaced the custom decompressTexture() with Three.js's built-in decompress() from WebGLTextureUtils, passing no renderer so it creates and disposes a temporary WebGL context. This drops ~87 lines of custom shader/render target code. As you noted, the minor perf hit is negligible for a one-off export action

Copy link
Copy Markdown
Collaborator

@diegoteran diegoteran left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@diegoteran diegoteran merged commit e6ac9dd into google:master Apr 7, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cannot export scene with KTX2 compressed model

2 participants