Compressing a texture: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: source (line 34), encode (line 50), mips (line 55), report (line 81), check (line 110).
1/// Compressing a texture: four block encoders, a mip chain, and a KTX2
2/// container to hold the result.
3///
4/// Quoted by `texture_compression.md` and shown whole in the Source tab.
5library;
7import 'dart:typed_data';
9import 'package:flutter/widgets.dart';
10import 'package:flutter3d/flutter3d.dart' hide VkFormat;
11import 'package:flutter3d_core/formats.dart'
12 show
13 Rgba8Image,
14 VkFormat,
15 buildMipChain,
16 encodeAstc4x4,
17 encodeBc1,
18 encodeBc3,
19 encodeEtc2Rgb8,
20 writeKtx2;
21import 'package:flutter3d_showcase/src/demo/demo.dart';
22import 'package:vector_math/vector_math.dart';
24final class TextureCompressionDemo extends ShowcaseDemo {
25 late final Rgba8Image _source;
26 late final Uint8List _bc1;
27 late final Uint8List _bc3;
28 late final Uint8List _etc2;
29 late final Uint8List _astc;
30 late final Uint8List _ktx2;
33 Scene build(DemoContext context) {
34 // A 16x16 checkerboard, whole 4x4 blocks in both directions, which
35 // every encoder here requires.
36 final pixels = Uint8List(16 * 16 * 4);
37 for (var y = 0; y < 16; y++) {
38 for (var x = 0; x < 16; x++) {
39 final lit = ((x ~/ 4) + (y ~/ 4)).isEven;
40 final o = (y * 16 + x) * 4;
41 final value = lit ? 220 : 40;
42 pixels[o] = value;
43 pixels[o + 1] = value;
44 pixels[o + 2] = lit ? 60 : 200;
45 pixels[o + 3] = 255;
46 }
47 }
48 _source = Rgba8Image(width: 16, height: 16, pixels: pixels);
50 _bc1 = encodeBc1(_source);
51 _bc3 = encodeBc3(_source);
52 _etc2 = encodeEtc2Rgb8(_source);
53 _astc = encodeAstc4x4(_source);
55 // A block encoder needs whole 4x4 blocks, so the chain stops at the
56 // last level that still is one: 16, 8, 4, and no further.
57 final chain = buildMipChain(
58 _source,
59 ).where((level) => level.width % 4 == 0 && level.height % 4 == 0);
60 _ktx2 = writeKtx2(
61 vkFormat: VkFormat.bc1RgbaUNormBlock,
62 pixelWidth: _source.width,
63 pixelHeight: _source.height,
64 levels: <Uint8List>[for (final level in chain) encodeBc1(level)],
65 );
67 return Scene()
68 ..add(
69 MeshNode(
70 DeviceMesh.upload(context.device, CuboidShape().build()),
71 Material(baseColor: Vector4(0.7, 0.7, 0.75, 1.0)),
72 name: 'block',
73 ),
74 )
75 ..add(
76 LightNode(name: 'sun', intensity: 3.0)
77 ..setLocalForward(Vector3(-0.4, -1.0, -0.3)),
78 );
79 }
81 int get _rawBytes => _source.pixels.length;
83 String _report() =>
84 'raw RGBA8: $_rawBytes bytes\n'
85 'BC1: ${_bc1.length} bytes (${(_rawBytes / _bc1.length).toStringAsFixed(1)}x)\n'
86 'BC3: ${_bc3.length} bytes (${(_rawBytes / _bc3.length).toStringAsFixed(1)}x)\n'
87 'ETC2: ${_etc2.length} bytes (${(_rawBytes / _etc2.length).toStringAsFixed(1)}x)\n'
88 'ASTC 4x4: ${_astc.length} bytes '
89 '(${(_rawBytes / _astc.length).toStringAsFixed(1)}x)\n'
90 'KTX2 with a mip chain: ${_ktx2.length} bytes';
93 Widget? customBody(BuildContext buildContext, DemoContext context) =>
94 Container(
95 color: const Color(0xFF14161A),
96 padding: const EdgeInsets.all(24),
97 alignment: Alignment.topLeft,
98 child: DefaultTextStyle(
99 style: const TextStyle(
100 color: Color(0xFFE8E8EC),
101 fontSize: 16,
102 fontFamily: 'monospace',
103 ),
104 child: Text(_report()),
105 ),
106 );
109 void verify(Scene scene, FrameResult frame) {
110 // Every block format above packs a 4x4 patch of pixels into fewer
111 // bytes than the raw pixels take, or it is not compression.
112 if (_bc1.length >= _rawBytes ||
113 _bc3.length >= _rawBytes ||
114 _etc2.length >= _rawBytes ||
115 _astc.length >= _rawBytes) {
116 throw StateError('a compressed block is not smaller than the raw one');
117 }
118 if (_ktx2.length < 12) {
119 throw StateError('writeKtx2 did not produce a file');
120 }
121 if (frame.drawCalls < 1) {
122 throw StateError('the block was not drawn');
123 }
124 }
125}