flutter3d
Showcase Changelog 38 packages API reference

KTX2 and Basis textures

since 0.4.2 Formats and I/O

KTX2 is the container most compressed-texture pipelines write to. Some files carry a real GPU block format directly; others carry Basis Universal, a format transcoded at load time into whatever the device actually samples. This page reads one of the second kind and puts it on a cube.

Step 1: A real Basis Universal file #

Eight by eight pixels, four flat-coloured quadrants, produced by a real build of basisu. vkFormat is left undefined, which is how KTX2 says "these pixels are Basis Universal" rather than naming a block format directly; the container's own data format descriptor says which flavour, ETC1S under Basis-LZ here.

// An 8x8 image, four flat-coloured quadrants, encoded by a real build of
// `basisu`. `vkFormat` is left undefined, KTX2's own way of saying "these
// pixels are Basis Universal", with the actual flavour named in the
// container's data format descriptor.
static const String _fixtureBase64 =
    'q0tUWCAyMLsNChoKAAAAAAEAAAAIAAAACAAAAAAAAAAAAAAAAQAAAAEAAAABAAAAaAAAACw'
    'AAACUAAAAJAAAALgAAAAAAAAAkQAAAAAAAABJAQAAAAAAAAIAAAAAAAAAAAAAAAAAAAAsA'
    'AAAAAAAAAIAKACjAQEAAwMAAAgAAAAAAAAAAAA/AAAAAAAAAAAA/////x8AAABLVFh3cml'
    '0ZXIAQmFzaXMgVW5pdmVyc2FsIDIuNTAAAAQABAAsAAAAEQAAACwAAAAAAAAAAAAAAAAAA'
    'AACAAAAAAAAAAAAAAAgQEQAAAAAAAgjRAATAQAAAAAASAIDwASAAAAAAADiAGACAAAAAAA'
    'AATkjAKyqUlWtqqqqrKqqqlJVVVUFAMFEAAAAAAAA8l+NAJgBAAAAAABAQgkQIUAAAAAAU'
    'pgiAJgAAAAAAABAAAEEEw==';

Step 2: Parse it #

Ktx2Texture.parse transcodes the ETC1S data to plain RGBA8, checked against a real basisu -unpack decode of the same file rather than only against this reader's own understanding of the format.

final bytes = base64Decode(_fixtureBase64.replaceAll('\n', ''));
_texture = Ktx2Texture.parse(bytes);

Note. UASTC is the other Basis flavour this reader knows. A file naming a real block format directly, BC1 or ETC2 among them, skips transcoding altogether: its bytes are already what a device samples, once the device says it can.

Step 3: Put it on something #

The transcoded level is already plain RGBA8, the same shape any other decoded image arrives in, so it uploads the same way.

// The level this reader hands back is already plain RGBA8, so it goes
// straight onto a device texture the same way any other decoded image
// would.
final albedo = context.device.createTextureFromPixels(
  width: _texture.pixelWidth,
  height: _texture.pixelHeight,
  format: TextureFormat.r8g8b8a8UNormInt,
  pixels: _texture.levels.first,
);

Step 4: Check the claim #

This fixture is always 8x8, always transcodes to RGBA8, and its top-left quadrant is a near-pure red.

if (_texture.pixelWidth != 8 || _texture.pixelHeight != 8) {
  throw StateError('this fixture is always 8x8');
}
if (_texture.vkFormat != VkFormat.r8g8b8a8UNorm) {
  throw StateError('a Basis file should transcode to plain RGBA8');
}
// The top-left quadrant is a flat, near-pure red.
final topLeft = _texture.levels.first.getUint32(0, Endian.little);
final red = topLeft & 0xff;
final green = (topLeft >> 8) & 0xff;
if (red < 240 || green > 15) {
  throw StateError('the top-left quadrant should transcode close to red');
}
if (frame.drawCalls < 1) {
  throw StateError('the block was not drawn');
}