GLB export broken under NullEngine in v9 - root cause and fix

Hi, I found a bug with GLB export under NullEngine on the server. It worked correctly in v8, but after upgrading to v9 the following error appears:

Error: Failed to read pixels from texture <name>.
    at GetTextureDataAsync

I traced the regression to an optimization: GetCachedImageAsync in glTFMaterialExporter.ts (and a parallel version in usdzExporter.ts which we already merged).

Root Cause

Both functions contain this guard:

if (internalTexture.invertY) {
    return null;
}

invertY defaults to true for every texture loaded from a URL — so this guard always fires, returns null, and forces fallback to GetTextureDataAsync. That function reads pixels from the GPU, which requires a WebGL context. Under NullEngine there is no GPU → exception.

The intent of the cached path was precisely to avoid GPU readback (faster, works without WebGL). The invertY guard defeats its own purpose.

Why the Guard is Wrong

  • _buffer always stores the original source bytes of the image file — before any GPU upload

  • invertY is a WebGL upload-time operation that has no bearing on the source bytes

  • For export purposes (embedding a PNG/JPEG blob in a glTF/USDZ archive), the original bytes are exactly what we want

  • A glTF/USDZ viewer handles texture orientation itself

Ironically, the GPU fallback path may produce worse results: it reads back pixels after the Y-flip, yielding an upside-down image in the archive.

Fix

Make the invertY guard context-aware:

if (internalTexture.invertY) {
    // On a real engine, the GPU has the texture stored flipped (UNPACK_FLIP_Y_WEBGL),
    // while the glTF loader uploads with invertY=false. Falling back to GPU readback
    // produces bytes that round-trip correctly. NullEngine has no GPU readback path,
    // so the cached URL bytes are the only option.
    const engine = babylonTexture.getScene()?.getEngine();
    if (!(engine instanceof NullEngine)) {
        return null;
    }
}

This preserves correct GPU readback behavior on WebGL engines while allowing NullEngine to use the cached bytes.

PR Status

Both fixes have been submitted:

Implementation: Both GLB and USDZ export now work correctly under NullEngine using the arek-3d/Babylon.js fork.


Note: USDZ didn’t work on server even in v8, but GLB worked properly. See this thread regarding USDZ server-side support.

cc @alexchuber

Thanks for the contributions! Will give this one a review soon.

In the meantime, could you help me understand the details of the regression a bit more? I want to make sure we’re not missing anything else :slight_smile:

To my knowledge, the glTF exporter shouldn’t have been able to export textures at all using NullEngine-- at least not until the alternate GetCachedImageAsync path was introduced in v8.23.2. Were you using a version earlier or later than this?

I’m pretty sure I was using version 8.33 in a Node.js application, where GLB export with textures was working fine. However, USDZ export was throwing an error (readPixels did not work on NullEngine).

I created a topic about it here:

Based on the feedback I received from you, I made a fork and submitted a PR to add USDZ support. That was done on version 9, and it worked correctly for USDZ files.

After upgrading my Node.js project to Babylon.js v9, I noticed that GLB export no longer works. I’m now getting the same readPixels error that previously only occurred with the USDZ exporter in v8.33.

Has something changed in v9 regarding gltf exporter that could affect this function?

Gotcha. Yeah, sounds like it was indeed a gap surrounding NullEngine and both the USDZ and glTF exporters. Makes sense. Thanks for the contribution!