BabylonJS/Babylon.js · error · Error

The provided data is not a valid KTX2 file.

Error message

The provided data is not a valid KTX2 file.

What it means

CreateTexture2DArrayFromKTX2Async validates the buffer with KhronosTextureContainer2.IsValid before decoding. If the data does not carry the KTX2 container signature/structure, it throws rather than letting the transcoder fail opaquely.

Source

Thrown at packages/dev/core/src/Materials/Textures/rawTexture2DArray.functions.ts:151

 * @param options defines optional creation settings
 * @returns a promise resolved with the created RawTexture2DArray
 */
export async function CreateTexture2DArrayFromKTX2Async(scene: Scene, data: string | ArrayBufferView, options?: ICreateTexture2DArrayFromKTX2Options): Promise<RawTexture2DArray> {
    let buffer: ArrayBufferView;
    if (typeof data === "string") {
        const response = await fetch(data);
        if (!response.ok) {
            throw new Error(`Failed to fetch KTX2 file "${data}": ${response.status} ${response.statusText}`);
        }
        buffer = new Uint8Array(await response.arrayBuffer());
    } else {
        buffer = data;
    }

    const { KhronosTextureContainer2 } = await import("../../Misc/khronosTextureContainer2");

    if (!KhronosTextureContainer2.IsValid(buffer)) {
        throw new Error("The provided data is not a valid KTX2 file.");
    }

    const container = new KhronosTextureContainer2(scene.getEngine());

    // forceRGBA: the transcoded compressed formats cannot be uploaded to an array texture yet (see above).
    const decodedData = await container._decodeAsync(buffer, { forceRGBA: true });

    if (decodedData.errors) {
        throw new Error("Failed to decode the KTX2 file. " + decodedData.errors);
    }

    const layerCount = Math.max(decodedData.layerCount ?? 1, 1);
    // mipmaps are ordered level by level, and within a level layer by layer, so the base level occupies
    // the first layerCount entries and its layers are already in ascending order.
    const baseLevel = decodedData.mipmaps.slice(0, layerCount);

    if (baseLevel.length !== layerCount) {
        throw new Error(`Failed to decode the KTX2 file: expected ${layerCount} layers for the base mip level but got ${baseLevel.length}.`);

View on GitHub (pinned to 0592b347b8)

Solutions

  1. Re-export the texture as KTX2 (toktx --t2 or gltf-transform etc.) and confirm the 'KTX 20' magic bytes
  2. Check the served file's first bytes match the KTX2 identifier §0xAB 'KTX' ' ' '2' '0' 'xBB'
  3. Ensure the download is complete (compare Content-Length)
  4. Verify the file is KTX2, not legacy KTX1

Example fix

// before
const data = new Uint8Array(await file.arrayBuffer());
const tex = await CreateTexture2DArrayFromKTX2Async(scene, data);
// after
const data = new Uint8Array(await file.arrayBuffer());
const isKtx2 = data[0] === 0xab && data[1] === 0x4b && data[2] === 0x54 && data[3] === 0x58 && data[4] === 0x20 && data[5] === 0x32 && data[6] === 0x30 && data[7] === 0xbb;
if (!isKtx2) throw new Error(`${file.name} is not KTX2`);
const tex = await CreateTexture2DArrayFromKTX2Async(scene, data);
Defensive patterns

Strategy: validation

Validate before calling

function isKtx2Buffer(buf: Uint8Array): boolean {
  return buf.length >= 12
    && buf[0] === 0xab && buf[1] === 0x4b && buf[2] === 0x54 && buf[3] === 0x58
    && buf[4] === 0x20 && buf[5] === 0x32 && buf[6] === 0x30 && buf[7] === 0xbb;
}
if (!isKtx2Buffer(data)) throw new Error("Not a KTX2 file");

Type guard

function isKtx2(b: Uint8Array): b is Uint8Array {
  return b.length >= 12 && b[0] === 0xab && b[1] === 0x4b && b[2] === 0x54 && b[3] === 0x58 && b[4] === 0x20 && b[5] === 0x32 && b[6] === 0x30 && b[7] === 0xbb;
}

Try / catch

try {
  const tex = await CreateTexture2DArrayFromKTX2Async(scene, data);
} catch (e) {
  if (e instanceof Error && e.message.includes("not a valid KTX2")) {
    console.error("Asset is not KTX2 — re-export or check the fetched content");
  } else throw e;
}

Prevention

When it happens

Trigger: Passing a buffer that is not KTX2 — a KTX v1 file, a PNG/JPEG mislabeled as .ktx2, a truncated download, or an HTML error page saved as .ktx2.

Common situations: Exporting with an old tool producing KTX (v1) instead of KTX2, fetching a 404 page that got cached, or passing a URL string when the server returns non-KTX2 content.

Related errors


AI-assisted analysis of BabylonJS/Babylon.js@0592b347b8 (2026-08-30). Data as JSON: /api/errors/508b1f86e44312a8. Report an issue: GitHub.