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
- Re-export the texture as KTX2 (toktx --t2 or gltf-transform etc.) and confirm the 'KTX 20' magic bytes
- Check the served file's first bytes match the KTX2 identifier §0xAB 'KTX' ' ' '2' '0' 'xBB'
- Ensure the download is complete (compare Content-Length)
- 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
- Check the KTX2 magic bytes before passing buffers
- Confirm fetched content-type is application/octet-stream, not text/html
- Standardize exports through toktx or gltf-transform pipelines
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
- Failed to decode the KTX2 file: expected ${layerCount} layer
- Failed to decode the KTX2 file. ${decodedData.errors}
- Draco: Cannot decode invalid geometry type ${type}
- Sample2DRgbaToRef: widthPx and heightPx must be positive.
- At least one mesh is needed to create the nav mesh.
AI-assisted analysis of BabylonJS/Babylon.js@0592b347b8 (2026-08-30).
Data as JSON: /api/errors/508b1f86e44312a8.
Report an issue: GitHub.