KTX2 and Basis textures: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: bitstream (line 21), parse (line 36), upload (line 39), check (line 65).
1/// Reading a KTX2 container that carries Basis Universal ETC1S pixels, and
2/// putting the transcoded texture on a cube.
3///
4/// Quoted by `ktx2.md` and shown whole in the Source tab.
5library;
7import 'dart:convert';
8import 'dart:typed_data';
10import 'package:flutter3d/flutter3d.dart' hide Ktx2Texture;
11// The engine's own `Ktx2Texture` (from `flutter3d/flutter3d.dart`) answers
12// a `TextureFormat`, already resolved against a device. This page reads
13// the container itself, which answers the file's own `vkFormat`.
14import 'package:flutter3d_core/formats.dart' show Ktx2Texture, VkFormat;
15import 'package:flutter3d_showcase/src/demo/demo.dart';
16import 'package:vector_math/vector_math.dart';
18final class Ktx2Demo extends ShowcaseDemo {
19 late final Ktx2Texture _texture;
21 // An 8x8 image, four flat-coloured quadrants, encoded by a real build of
22 // `basisu`. `vkFormat` is left undefined, KTX2's own way of saying "these
23 // pixels are Basis Universal", with the actual flavour named in the
24 // container's data format descriptor.
25 static const String _fixtureBase64 =
26 'q0tUWCAyMLsNChoKAAAAAAEAAAAIAAAACAAAAAAAAAAAAAAAAQAAAAEAAAABAAAAaAAAACw'
27 'AAACUAAAAJAAAALgAAAAAAAAAkQAAAAAAAABJAQAAAAAAAAIAAAAAAAAAAAAAAAAAAAAsA'
28 'AAAAAAAAAIAKACjAQEAAwMAAAgAAAAAAAAAAAA/AAAAAAAAAAAA/////x8AAABLVFh3cml'
29 '0ZXIAQmFzaXMgVW5pdmVyc2FsIDIuNTAAAAQABAAsAAAAEQAAACwAAAAAAAAAAAAAAAAAA'
30 'AACAAAAAAAAAAAAAAAgQEQAAAAAAAgjRAATAQAAAAAASAIDwASAAAAAAADiAGACAAAAAAA'
31 'AATkjAKyqUlWtqqqqrKqqqlJVVVUFAMFEAAAAAAAA8l+NAJgBAAAAAABAQgkQIUAAAAAAU'
32 'pgiAJgAAAAAAABAAAEEEw==';
35 Scene build(DemoContext context) {
36 final bytes = base64Decode(_fixtureBase64.replaceAll('\n', ''));
37 _texture = Ktx2Texture.parse(bytes);
39 // The level this reader hands back is already plain RGBA8, so it goes
40 // straight onto a device texture the same way any other decoded image
41 // would.
42 final albedo = context.device.createTextureFromPixels(
43 width: _texture.pixelWidth,
44 height: _texture.pixelHeight,
45 format: TextureFormat.r8g8b8a8UNormInt,
46 pixels: _texture.levels.first,
47 );
49 return Scene()
50 ..add(
51 MeshNode(
52 DeviceMesh.upload(context.device, CuboidShape().build()),
53 Material(albedo: albedo, roughness: 0.7),
54 name: 'block',
55 ),
56 )
57 ..add(
58 LightNode(name: 'sun', intensity: 3.0)
59 ..setLocalForward(Vector3(-0.4, -1.0, -0.3)),
60 );
61 }
64 void verify(Scene scene, FrameResult frame) {
65 if (_texture.pixelWidth != 8 || _texture.pixelHeight != 8) {
66 throw StateError('this fixture is always 8x8');
67 }
68 if (_texture.vkFormat != VkFormat.r8g8b8a8UNorm) {
69 throw StateError('a Basis file should transcode to plain RGBA8');
70 }
71 // The top-left quadrant is a flat, near-pure red.
72 final topLeft = _texture.levels.first.getUint32(0, Endian.little);
73 final red = topLeft & 0xff;
74 final green = (topLeft >> 8) & 0xff;
75 if (red < 240 || green > 15) {
76 throw StateError('the top-left quadrant should transcode close to red');
77 }
78 if (frame.drawCalls < 1) {
79 throw StateError('the block was not drawn');
80 }
81 }
82}