A JPEG expands to raw RGBA on the GPU: each of the clock's 1K textures takes 5.6 MB of video memory. GPUs sample block-compressed formats directly (Compressed Textures), but none runs everywhere, so Basis Universal 3,114 stores an intermediate in a KTX2 file that is transcoded at load time to what the GPU supports: ETC1S is tiny but blocky, UASTC larger and near-lossless. glTF uses them through KHR_texture_basisu. Khronos's ktx tool (KTX-Software 4.4.2 1,367 ) encodes an image, glTF Transform a model:
opts='--format R8G8B8A8_SRGB --assign-tf srgb --generate-mipmap'
ktx create $opts --encode basis-lz booknest-cover-3.png booknest-cover-3-etc1s.ktx2
ktx create $opts --encode uastc --zstd 18 booknest-cover-3.png booknest-cover-3-uastc.ktx2
gltf-transform etc1s clock-draco.glb clock-draco-etc1s.glb # calls ktx per textureThe 86,050-byte PNG became 12,398 bytes as ETC1S and 37,468 as UASTC, and the Draco-and-ETC1S clock 1.18 MB (from 3.13 MB). new BABYLON.Texture() accepts .ktx2 URLs, fetching the transcoder from Babylon 89,603 's CDN (KhronosTextureContainer2.URLConfig moves it):
<canvas id="view" style="width: 620px; height: 160px"></canvas>
<p id="log" style="font: 15px sans-serif; text-align: center"></p>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script>
{
const engine = new BABYLON.Engine(view, true, {}, true);
const scene = new BABYLON.Scene(engine);
scene.clearColor = BABYLON.Color4.FromHexString('#eceff1ff');
const center = new BABYLON.Vector3(0, 0, 0);
new BABYLON.ArcRotateCamera('cam', -Math.PI / 2, Math.PI / 2, 3.6, center, scene).fov = 0.5;
const formats = { 5: 'RGBA8', 36492: 'BC7' }; // WebGL format enums
['png', 'uastc.ktx2', 'etc1s.ktx2'].forEach((end, i) => {
const texture = new BABYLON.Texture(`booknest-cover-3${i ? '-' : '.'}${end}`, scene);
if (i) texture.vScale = -1; // KTX2 rows run top-down: flip
texture.onLoadObservable.addOnce(() => { log.textContent +=
` ${end} as ${formats[texture.getInternalTexture().format]} `; });
const cover = BABYLON.MeshBuilder.CreatePlane(end, { width: 1, height: 1.5 }, scene);
cover.position.x = (i - 1) * 1.25;
cover.material = new BABYLON.StandardMaterial(end, scene);
cover.material.emissiveTexture = texture; // unlit, to compare the pixels
cover.material.disableLighting = true;
});
scene.executeWhenReady(() => engine.runRenderLoop(() => scene.render()));
scene.onAfterRenderObservable.add(() => {
window.__done = scene.getFrameId() > 5 && log.textContent.split(' as ').length === 4;
});
}
</script>
The GTX 1650 lacks ETC and ASTC, so both KTX2 files became BC7, a quarter of RGBA8's footprint. Compressed blocks cannot be flipped on upload, so Babylon ignores invertY for KTX2: flip with vScale = -1, or when encoding. glTF needs no flip, since its texture origin is the top left too.