KTX2 and Basis textures
Open the live demo · Read the source · View on GitHub
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');
}