KTX2 and Basis

KTX2 and Basis Universal Texture Compression

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:

Encoding a cover as ETC1S and as UASTC, and a whole model as ETC1SJavaScript
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 texture

The 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):

The same cover as PNG, UASTC and ETC1S, with the GPU format each becameHTMLLive
<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>
Browser output of Listing 7.60
Browser output of 60

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.