@driftengine/assets 4.8.2 → 4.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +1 -1
  2. package/dist/bc7Decode.d.ts +33 -0
  3. package/dist/bc7Decode.js +197 -0
  4. package/dist/bcAnswer.d.ts +28 -0
  5. package/dist/bcAnswer.js +15 -0
  6. package/dist/bcDecode.d.ts +28 -0
  7. package/dist/bcDecode.js +175 -0
  8. package/dist/bcFlip.d.ts +21 -0
  9. package/dist/bcFlip.js +52 -0
  10. package/dist/bcImage.d.ts +15 -0
  11. package/dist/bcImage.js +38 -0
  12. package/dist/bcLoad.d.ts +26 -0
  13. package/dist/bcLoad.js +113 -0
  14. package/dist/bcWorker.d.ts +1 -0
  15. package/dist/bcWorker.js +14 -0
  16. package/dist/dds.d.ts +16 -18
  17. package/dist/dds.js +20 -293
  18. package/dist/ddsBlocks.d.ts +3 -0
  19. package/dist/ddsBlocks.js +54 -0
  20. package/dist/ddsHeader.d.ts +32 -0
  21. package/dist/ddsHeader.js +119 -0
  22. package/dist/drftLoader.d.ts +18 -0
  23. package/dist/drftLoader.js +98 -8
  24. package/dist/fbx.js +32 -13
  25. package/dist/fbxRig.d.ts +0 -18
  26. package/dist/fbxRig.js +0 -37
  27. package/dist/gltf.js +74 -12
  28. package/dist/index.d.ts +4 -0
  29. package/dist/index.js +7 -2
  30. package/dist/influences.d.ts +40 -0
  31. package/dist/influences.js +53 -0
  32. package/dist/weld.js +3 -0
  33. package/package.json +4 -4
  34. package/src/bc7Decode.ts +237 -0
  35. package/src/bcAnswer.ts +45 -0
  36. package/src/bcDecode.ts +218 -0
  37. package/src/bcFlip.ts +55 -0
  38. package/src/bcImage.ts +58 -0
  39. package/src/bcLoad.ts +154 -0
  40. package/src/bcWorker.ts +20 -0
  41. package/src/dds.ts +21 -337
  42. package/src/ddsBlocks.ts +63 -0
  43. package/src/ddsHeader.ts +160 -0
  44. package/src/drftLoader.ts +115 -9
  45. package/src/fbx.ts +34 -18
  46. package/src/fbxRig.ts +0 -41
  47. package/src/gltf.ts +75 -13
  48. package/src/index.ts +7 -2
  49. package/src/influences.ts +77 -0
  50. package/src/weld.ts +3 -0
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Model readers for nine formats — `.blend` among them, read from the file's own DNA — and the streaming loader that uploads them.
4
4
 
5
- **Cost: 22.5 KB gzipped on top of core.** Measured by `scripts/size-gate.test.mjs`, which fails if it drifts more than
5
+ **Cost: 26.3 KB gzipped on top of core.** Measured by `scripts/size-gate.test.mjs`, which fails if it drifts more than
6
6
  3% — the number is a fact about the build rather than a claim in a document.
7
7
 
8
8
  Part of [DriftEngine](../../README.md). See [`docs/ARCHITECTURE.md`](../../docs/ARCHITECTURE.md)
@@ -0,0 +1,33 @@
1
+ /**
2
+ * One BC7 block into sixteen RGBA texels, exactly as the format defines it.
3
+ *
4
+ * **BC7 is the format to get bit-exact, and the one with no slack.** Eight modes, each a different
5
+ * split of 128 bits into partitions, endpoints, p-bits and indices; a field read one bit early
6
+ * shifts every field after it, and the picture that comes out is plausible noise rather than an
7
+ * error. Unlike BC1 to BC5, its interpolation is specified to the integer, so a decoder either
8
+ * agrees with the format to the byte or is wrong — `bcDecode.test.ts` holds it to that against an
9
+ * independent implementation, two random blocks a mode.
10
+ *
11
+ * A reserved mode — a first byte with no bit set — decodes to transparent black, which is what the
12
+ * format says a decoder returns for it.
13
+ *
14
+ * **Scratch lives at module scope**, because a phone without BC sampling decodes every block of
15
+ * every texture through here at load: a 2048² surface is 262,144 calls, and per-call arrays would
16
+ * be five allocations each. Not re-entrant, which nothing here needs; a worker has its own module.
17
+ */
18
+ /**
19
+ * The two-subset partitions, a bit a pixel (pixel 0 lowest), 1 for the second subset.
20
+ *
21
+ * The format's own table, as published in the D3D11 BC7 specification; transcribed from
22
+ * `texture2ddecoder` 0.1.2 (MIT OR Apache-2.0) and held by `bcDecode.test.ts` to what the format
23
+ * says of it: pixel 0 in subset 0, and every anchor inside the subset it anchors.
24
+ */
25
+ export declare const BC7_PARTITION2: readonly number[];
26
+ /** The three-subset partitions, two bits a pixel (pixel 0 lowest). From the same source. */
27
+ export declare const BC7_PARTITION3: readonly number[];
28
+ /** The anchor pixel of the second subset of each two-subset partition. From the same source. */
29
+ export declare const BC7_ANCHOR2: readonly number[];
30
+ /** The anchor pixels of the second and third subsets of each three-subset partition. */
31
+ export declare const BC7_ANCHOR3: readonly (readonly number[])[];
32
+ /** One BC7 block at `bytes[at]` into sixteen RGBA texels in `out`. */
33
+ export declare function decodeBc7Block(bytes: Uint8Array, at: number, out: Uint8Array): void;
@@ -0,0 +1,197 @@
1
+ /**
2
+ * One BC7 block into sixteen RGBA texels, exactly as the format defines it.
3
+ *
4
+ * **BC7 is the format to get bit-exact, and the one with no slack.** Eight modes, each a different
5
+ * split of 128 bits into partitions, endpoints, p-bits and indices; a field read one bit early
6
+ * shifts every field after it, and the picture that comes out is plausible noise rather than an
7
+ * error. Unlike BC1 to BC5, its interpolation is specified to the integer, so a decoder either
8
+ * agrees with the format to the byte or is wrong — `bcDecode.test.ts` holds it to that against an
9
+ * independent implementation, two random blocks a mode.
10
+ *
11
+ * A reserved mode — a first byte with no bit set — decodes to transparent black, which is what the
12
+ * format says a decoder returns for it.
13
+ *
14
+ * **Scratch lives at module scope**, because a phone without BC sampling decodes every block of
15
+ * every texture through here at load: a 2048² surface is 262,144 calls, and per-call arrays would
16
+ * be five allocations each. Not re-entrant, which nothing here needs; a worker has its own module.
17
+ */
18
+ const BC7_MODES = [
19
+ [3, 4, 0, 0, 4, 0, 1, 0, 3, 0],
20
+ [2, 6, 0, 0, 6, 0, 0, 1, 3, 0],
21
+ [3, 6, 0, 0, 5, 0, 0, 0, 2, 0],
22
+ [2, 6, 0, 0, 7, 0, 1, 0, 2, 0],
23
+ [1, 0, 2, 1, 5, 6, 0, 0, 2, 3],
24
+ [1, 0, 2, 0, 7, 8, 0, 0, 2, 2],
25
+ [1, 0, 0, 0, 7, 7, 1, 0, 4, 0],
26
+ [2, 6, 0, 0, 5, 5, 1, 0, 2, 0],
27
+ ];
28
+ /** Interpolation weights out of 64, for two-, three- and four-bit indices. */
29
+ const BC7_WEIGHTS = [
30
+ [0, 21, 43, 64],
31
+ [0, 9, 18, 27, 37, 46, 55, 64],
32
+ [0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 60, 64],
33
+ ];
34
+ /**
35
+ * The two-subset partitions, a bit a pixel (pixel 0 lowest), 1 for the second subset.
36
+ *
37
+ * The format's own table, as published in the D3D11 BC7 specification; transcribed from
38
+ * `texture2ddecoder` 0.1.2 (MIT OR Apache-2.0) and held by `bcDecode.test.ts` to what the format
39
+ * says of it: pixel 0 in subset 0, and every anchor inside the subset it anchors.
40
+ */
41
+ export const BC7_PARTITION2 = [
42
+ 0xcccc, 0x8888, 0xeeee, 0xecc8, 0xc880, 0xfeec, 0xfec8, 0xec80, 0xc800, 0xffec, 0xfe80, 0xe800,
43
+ 0xffe8, 0xff00, 0xfff0, 0xf000, 0xf710, 0x008e, 0x7100, 0x08ce, 0x008c, 0x7310, 0x3100, 0x8cce,
44
+ 0x088c, 0x3110, 0x6666, 0x366c, 0x17e8, 0x0ff0, 0x718e, 0x399c, 0xaaaa, 0xf0f0, 0x5a5a, 0x33cc,
45
+ 0x3c3c, 0x55aa, 0x9696, 0xa55a, 0x73ce, 0x13c8, 0x324c, 0x3bdc, 0x6996, 0xc33c, 0x9966, 0x0660,
46
+ 0x0272, 0x04e4, 0x4e40, 0x2720, 0xc936, 0x936c, 0x39c6, 0x639c, 0x9336, 0x9cc6, 0x817e, 0xe718,
47
+ 0xccf0, 0x0fcc, 0x7744, 0xee22,
48
+ ];
49
+ /** The three-subset partitions, two bits a pixel (pixel 0 lowest). From the same source. */
50
+ export const BC7_PARTITION3 = [
51
+ 0xaa685050, 0x6a5a5040, 0x5a5a4200, 0x5450a0a8, 0xa5a50000, 0xa0a05050, 0x5555a0a0, 0x5a5a5050,
52
+ 0xaa550000, 0xaa555500, 0xaaaa5500, 0x90909090, 0x94949494, 0xa4a4a4a4, 0xa9a59450, 0x2a0a4250,
53
+ 0xa5945040, 0x0a425054, 0xa5a5a500, 0x55a0a0a0, 0xa8a85454, 0x6a6a4040, 0xa4a45000, 0x1a1a0500,
54
+ 0x0050a4a4, 0xaaa59090, 0x14696914, 0x69691400, 0xa08585a0, 0xaa821414, 0x50a4a450, 0x6a5a0200,
55
+ 0xa9a58000, 0x5090a0a8, 0xa8a09050, 0x24242424, 0x00aa5500, 0x24924924, 0x24499224, 0x50a50a50,
56
+ 0x500aa550, 0xaaaa4444, 0x66660000, 0xa5a0a5a0, 0x50a050a0, 0x69286928, 0x44aaaa44, 0x66666600,
57
+ 0xaa444444, 0x54a854a8, 0x95809580, 0x96969600, 0xa85454a8, 0x80959580, 0xaa141414, 0x96960000,
58
+ 0xaaaa1414, 0xa05050a0, 0xa0a5a5a0, 0x96000000, 0x40804080, 0xa9a8a9a8, 0xaaaaaa44, 0x2a4a5254,
59
+ ];
60
+ /** The anchor pixel of the second subset of each two-subset partition. From the same source. */
61
+ export const BC7_ANCHOR2 = [
62
+ 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 2, 8, 2, 2, 8, 8, 15, 2, 8, 2,
63
+ 2, 8, 8, 2, 2, 15, 15, 6, 8, 2, 8, 15, 15, 2, 8, 2, 2, 2, 15, 15, 6, 6, 2, 6, 8, 15, 15, 2, 2, 15,
64
+ 15, 15, 15, 15, 2, 2, 15,
65
+ ];
66
+ /** The anchor pixels of the second and third subsets of each three-subset partition. */
67
+ export const BC7_ANCHOR3 = [
68
+ [
69
+ 3, 3, 15, 15, 8, 3, 15, 15, 8, 8, 6, 6, 6, 5, 3, 3, 3, 3, 8, 15, 3, 3, 6, 10, 5, 8, 8, 6, 8, 5,
70
+ 15, 15, 8, 15, 3, 5, 6, 10, 8, 15, 15, 3, 15, 5, 15, 15, 15, 15, 3, 15, 5, 5, 5, 8, 5, 10, 5,
71
+ 10, 8, 13, 15, 12, 3, 3,
72
+ ],
73
+ [
74
+ 15, 8, 8, 3, 15, 15, 3, 8, 15, 15, 15, 15, 15, 15, 15, 8, 15, 8, 15, 3, 15, 8, 15, 8, 3, 15, 6,
75
+ 10, 15, 15, 10, 8, 15, 3, 15, 10, 10, 8, 9, 10, 6, 15, 8, 15, 3, 6, 6, 8, 15, 3, 15, 15, 15, 15,
76
+ 15, 15, 15, 15, 15, 15, 3, 15, 15, 8,
77
+ ],
78
+ ];
79
+ /** `n` bits of a block from bit `pos`, least significant first, as the format orders them. */
80
+ function bits(bytes, at, pos, n) {
81
+ let value = 0;
82
+ for (let i = 0; i < n; i++) {
83
+ const p = pos + i;
84
+ value |= ((bytes[at + (p >> 3)] >> (p & 7)) & 1) << i;
85
+ }
86
+ return value;
87
+ }
88
+ /** A quantised endpoint widened to eight bits by replicating its top bits into the bottom. */
89
+ function expandEndpoint(value, width) {
90
+ const shifted = (value << (8 - width)) & 0xff;
91
+ return shifted | (shifted >> width);
92
+ }
93
+ /** The subset and anchor of each pixel, for a partition. */
94
+ function subsetOf(subsets, partition, pixel) {
95
+ if (subsets === 2)
96
+ return (BC7_PARTITION2[partition] >> pixel) & 1;
97
+ if (subsets === 3)
98
+ return (BC7_PARTITION3[partition] >>> (2 * pixel)) & 3;
99
+ return 0;
100
+ }
101
+ function isAnchor(subsets, partition, pixel) {
102
+ if (pixel === 0)
103
+ return true;
104
+ if (subsets === 2)
105
+ return BC7_ANCHOR2[partition] === pixel;
106
+ if (subsets === 3) {
107
+ return BC7_ANCHOR3[0]?.[partition] === pixel || BC7_ANCHOR3[1]?.[partition] === pixel;
108
+ }
109
+ return false;
110
+ }
111
+ const RAW = new Uint8Array(24);
112
+ const PBITS = new Uint8Array(6);
113
+ const COLOURS = new Uint8Array(24);
114
+ const PRIMARY = new Uint8Array(16);
115
+ const SECONDARY = new Uint8Array(16);
116
+ /** One BC7 block at `bytes[at]` into sixteen RGBA texels in `out`. */
117
+ export function decodeBc7Block(bytes, at, out) {
118
+ let mode = 0;
119
+ while (mode < 8 && bits(bytes, at, mode, 1) === 0)
120
+ mode++;
121
+ const layout = BC7_MODES[mode];
122
+ if (layout === undefined) {
123
+ out.fill(0, 0, 64);
124
+ return;
125
+ }
126
+ const [subsets, partitionBits, rotationBits, selectionBits, colourBits, alphaBits, endpointP, sharedP, index1, index2,] = layout;
127
+ let pos = mode + 1;
128
+ const partition = bits(bytes, at, pos, partitionBits);
129
+ pos += partitionBits;
130
+ const rotation = bits(bytes, at, pos, rotationBits);
131
+ pos += rotationBits;
132
+ const selection = bits(bytes, at, pos, selectionBits);
133
+ pos += selectionBits;
134
+ /* Endpoints channel by channel, two per subset: red for every subset, then green, blue, alpha. */
135
+ const endpoints = 2 * subsets;
136
+ const channels = alphaBits > 0 ? 4 : 3;
137
+ for (let c = 0; c < channels; c++) {
138
+ const width = c < 3 ? colourBits : alphaBits;
139
+ for (let e = 0; e < endpoints; e++) {
140
+ RAW[e * 4 + c] = bits(bytes, at, pos, width);
141
+ pos += width;
142
+ }
143
+ }
144
+ /* P-bits: one per endpoint, or one per subset shared by its two, appended below each value. */
145
+ const hasP = endpointP !== 0 || sharedP !== 0;
146
+ for (let e = 0; e < endpoints; e++) {
147
+ if (endpointP !== 0)
148
+ PBITS[e] = bits(bytes, at, pos++, 1);
149
+ else if (sharedP !== 0 && e % 2 === 0)
150
+ PBITS[e] = PBITS[e + 1] = bits(bytes, at, pos++, 1);
151
+ }
152
+ for (let e = 0; e < endpoints; e++) {
153
+ for (let c = 0; c < 4; c++) {
154
+ if (c === 3 && alphaBits === 0) {
155
+ COLOURS[e * 4 + 3] = 255;
156
+ continue;
157
+ }
158
+ const stored = RAW[e * 4 + c];
159
+ const width = (c < 3 ? colourBits : alphaBits) + (hasP ? 1 : 0);
160
+ COLOURS[e * 4 + c] = expandEndpoint(hasP ? (stored << 1) | PBITS[e] : stored, width);
161
+ }
162
+ }
163
+ /* Indices: primary for all sixteen pixels, each anchor one bit short; then secondary, if any. */
164
+ for (let pixel = 0; pixel < 16; pixel++) {
165
+ const width = index1 - (isAnchor(subsets, partition, pixel) ? 1 : 0);
166
+ PRIMARY[pixel] = bits(bytes, at, pos, width);
167
+ pos += width;
168
+ }
169
+ for (let pixel = 0; index2 > 0 && pixel < 16; pixel++) {
170
+ const width = index2 - (pixel === 0 ? 1 : 0);
171
+ SECONDARY[pixel] = bits(bytes, at, pos, width);
172
+ pos += width;
173
+ }
174
+ const weights1 = BC7_WEIGHTS[index1 - 2];
175
+ const weights2 = BC7_WEIGHTS[Math.max(index2, 2) - 2];
176
+ for (let pixel = 0; pixel < 16; pixel++) {
177
+ /* One index for both unless the mode has two; mode 4's selection bit says which drives alpha. */
178
+ const first = weights1[PRIMARY[pixel]];
179
+ const second = index2 > 0 ? weights2[SECONDARY[pixel]] : first;
180
+ const colourWeight = selection === 0 ? first : second;
181
+ const alphaWeight = selection === 0 ? second : first;
182
+ const e0 = subsetOf(subsets, partition, pixel) * 8;
183
+ const texel = pixel * 4;
184
+ for (let c = 0; c < 4; c++) {
185
+ const w = c < 3 ? colourWeight : alphaWeight;
186
+ out[texel + c] =
187
+ (COLOURS[e0 + c] * (64 - w) + COLOURS[e0 + 4 + c] * w + 32) >> 6;
188
+ }
189
+ /* Rotation: the stored alpha was one of the colour channels, and that channel was alpha. */
190
+ if (rotation !== 0) {
191
+ const swap = texel + rotation - 1;
192
+ const alpha = out[texel + 3];
193
+ out[texel + 3] = out[swap];
194
+ out[swap] = alpha;
195
+ }
196
+ }
197
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The BC worker's whole job, as a function: one request's level 0 decoded, or the reason it could
3
+ * not be. Its own module so the worker imports the decoders and nothing else — not the loader, and
4
+ * not the engine's barrel behind it.
5
+ *
6
+ * Decoded **as a GPU samples it** for the loader's own textures, so a decode at load looks like an
7
+ * upload of the same blocks: BC4 red alone, BC5 red and green, the lit shader rebuilding a normal's
8
+ * z either way. Decoded **as an ordinary image** (`asImage`) for a consumer taking images through
9
+ * `onImage`, who builds textures the engine does not see. An error is answered rather than thrown,
10
+ * so one bad texture fails on its own and the worker lives on.
11
+ */
12
+ import type { BcFormat } from '@driftengine/drft';
13
+ /** What the loader asks the worker: one level's blocks, handed over as a buffer of their own. */
14
+ export interface BcRequest {
15
+ readonly id: number;
16
+ readonly format: BcFormat;
17
+ readonly width: number;
18
+ readonly height: number;
19
+ readonly blocks: ArrayBuffer;
20
+ readonly asImage: boolean;
21
+ }
22
+ /** The answer: RGBA, eight bits a channel, or why not. */
23
+ export interface BcReply {
24
+ readonly id: number;
25
+ readonly rgba?: ArrayBuffer;
26
+ readonly error?: string;
27
+ }
28
+ export declare function answerBcRequest(request: BcRequest): BcReply;
@@ -0,0 +1,15 @@
1
+ import { decodeBc } from './bcDecode.js';
2
+ import { decodeBcImage } from './bcImage.js';
3
+ export function answerBcRequest(request) {
4
+ try {
5
+ const blocks = new Uint8Array(request.blocks);
6
+ const { format, width, height } = request;
7
+ const rgba = request.asImage
8
+ ? decodeBcImage(format, width, height, blocks)
9
+ : decodeBc(format, width, height, blocks);
10
+ return { id: request.id, rgba: rgba.buffer };
11
+ }
12
+ catch (error) {
13
+ return { id: request.id, error: error instanceof Error ? error.message : String(error) };
14
+ }
15
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Block-compressed texels to RGBA: BC1, BC2, BC3, BC4, BC5 and BC7.
3
+ *
4
+ * **Two callers, one decoder.** The baker turns a DDS it is told to re-encode into RGBA here, and the
5
+ * loader decodes a container's BC payload here on a device that cannot sample the format — most
6
+ * phones, which have ASTC and ETC2 instead. Both must read a block the same way, so the block
7
+ * routines live in one place rather than in the DDS reader that grew them first.
8
+ *
9
+ * **What a channel the format does not store reads as** is what a GPU returns for it, so a texture
10
+ * looks the same whether it was uploaded compressed or decoded here: BC4 is `(r, 0, 0, 255)` and BC5
11
+ * `(r, g, 0, 255)`. A BC5 normal map therefore arrives with blue at zero on both paths, and the lit
12
+ * shader rebuilds z from the two it has.
13
+ *
14
+ * **What it gives up.** BC1 to BC5 interpolants are rounded here where some decoders truncate, so a
15
+ * texel may differ by one from another decoder's; the format allows either, and hardware differs too.
16
+ * BC7 is defined exactly and decodes to the byte, in `bc7Decode.ts`.
17
+ *
18
+ * Runs off the frame, in the baker or the loader's worker, but a phone decodes every block of every
19
+ * texture through here at load, so the per-block scratch lives at module scope.
20
+ */
21
+ import type { BcFormat } from '@driftengine/drft';
22
+ export type { BcFormat } from '@driftengine/drft';
23
+ /**
24
+ * A whole surface of blocks into RGBA, eight bits a channel, `width` by `height`. A surface that is
25
+ * not a whole number of blocks is decoded block by block and cropped, which is what the format does:
26
+ * the blocks cover a padded rectangle and the texels past the declared size are padding.
27
+ */
28
+ export declare function decodeBc(format: BcFormat, width: number, height: number, blocks: Uint8Array, out?: Uint8Array): Uint8Array;
@@ -0,0 +1,175 @@
1
+ import { BC_BLOCK_BYTES, DrftError } from '@driftengine/drft';
2
+ import { decodeBc7Block } from './bc7Decode.js';
3
+ /** A colour block's four entries, and a BC4 block's eight values, rewritten for every block. */
4
+ const PALETTE = new Uint8Array(16);
5
+ const VALUES = new Uint8Array(8);
6
+ const TEXELS = new Uint8Array(64);
7
+ /** RGB565 into the palette at `at`, opaque, with the low bits replicated so white stays white. */
8
+ function expand565(value, at) {
9
+ const r = (value >> 11) & 0x1f;
10
+ const g = (value >> 5) & 0x3f;
11
+ const b = value & 0x1f;
12
+ PALETTE[at] = (r << 3) | (r >> 2);
13
+ PALETTE[at + 1] = (g << 2) | (g >> 4);
14
+ PALETTE[at + 2] = (b << 3) | (b >> 2);
15
+ PALETTE[at + 3] = 255;
16
+ }
17
+ /**
18
+ * One BC1 colour block into sixteen RGBA texels.
19
+ *
20
+ * **The endpoint comparison chooses the mode and is the thing to get right.** With `c0 > c1` the
21
+ * block has four opaque colours; otherwise it has three and its fourth index is transparent black.
22
+ * A decoder that always takes the first branch produces an image that looks correct until
23
+ * something relies on the punch-through alpha, which for foliage and grilles is everything.
24
+ *
25
+ * `punchthrough` is false for BC2 and BC3, whatever the endpoint order: those carry alpha in their
26
+ * own half of the block, so there is no index left over to mean transparent and the colour half is
27
+ * always the four-colour opaque mode. That was a real defect once: reached unconditionally, a BC3
28
+ * block written with `c0 <= c1` decoded its fourth index to transparent black — invisible in most
29
+ * real data, because an encoder has no reason to order the endpoints the low way round.
30
+ */
31
+ function decodeColourBlock(bytes, at, out, punchthrough) {
32
+ const c0 = bytes[at] | (bytes[at + 1] << 8);
33
+ const c1 = bytes[at + 2] | (bytes[at + 3] << 8);
34
+ expand565(c0, 0);
35
+ expand565(c1, 4);
36
+ const four = c0 > c1 || !punchthrough;
37
+ for (let c = 0; c < 3; c++) {
38
+ const e0 = PALETTE[c];
39
+ const e1 = PALETTE[4 + c];
40
+ PALETTE[8 + c] = four ? Math.round((2 * e0 + e1) / 3) : Math.round((e0 + e1) / 2);
41
+ PALETTE[12 + c] = four ? Math.round((e0 + 2 * e1) / 3) : 0;
42
+ }
43
+ PALETTE[11] = 255;
44
+ PALETTE[15] = four ? 255 : 0;
45
+ const indices = bytes[at + 4] |
46
+ (bytes[at + 5] << 8) |
47
+ (bytes[at + 6] << 16) |
48
+ (bytes[at + 7] << 24);
49
+ for (let texel = 0; texel < 16; texel++) {
50
+ const index = (indices >>> (texel * 2)) & 0x3;
51
+ out[texel * 4] = PALETTE[index * 4];
52
+ out[texel * 4 + 1] = PALETTE[index * 4 + 1];
53
+ out[texel * 4 + 2] = PALETTE[index * 4 + 2];
54
+ out[texel * 4 + 3] = PALETTE[index * 4 + 3];
55
+ }
56
+ }
57
+ /**
58
+ * One BC4-style eight-value block, which is what BC3's alpha and both halves of BC5 are, written
59
+ * into channel `offset` of each texel `stride` bytes apart.
60
+ *
61
+ * The same endpoint comparison as the colour block: with `a0 > a1` there are six interpolants, and
62
+ * otherwise four plus a hard 0 and 255.
63
+ */
64
+ function decodeAlphaBlock(bytes, at, out, stride, offset) {
65
+ const a0 = bytes[at];
66
+ const a1 = bytes[at + 1];
67
+ const values = VALUES;
68
+ values[0] = a0;
69
+ values[1] = a1;
70
+ if (a0 > a1) {
71
+ for (let i = 1; i <= 6; i++)
72
+ values[i + 1] = Math.round(((7 - i) * a0 + i * a1) / 7);
73
+ }
74
+ else {
75
+ for (let i = 1; i <= 4; i++)
76
+ values[i + 1] = Math.round(((5 - i) * a0 + i * a1) / 5);
77
+ values[6] = 0;
78
+ values[7] = 255;
79
+ }
80
+ /* Six bytes of 3-bit indices, least significant first, as one 48-bit run. */
81
+ let low = bytes[at + 2] |
82
+ (bytes[at + 3] << 8) |
83
+ (bytes[at + 4] << 16);
84
+ let high = bytes[at + 5] |
85
+ (bytes[at + 6] << 8) |
86
+ (bytes[at + 7] << 16);
87
+ for (let texel = 0; texel < 8; texel++) {
88
+ out[texel * stride + offset] = values[low & 0x7];
89
+ low >>>= 3;
90
+ }
91
+ for (let texel = 8; texel < 16; texel++) {
92
+ out[texel * stride + offset] = values[high & 0x7];
93
+ high >>>= 3;
94
+ }
95
+ }
96
+ /**
97
+ * BC2's alpha: eight bytes of explicit four-bit values, one nibble a texel, low nibble first.
98
+ *
99
+ * Scaled by *replication* rather than by a shift: `0xF` has to come out 255 and not 240, or a texel
100
+ * the author wrote as fully opaque arrives at 94% and every alpha test sees a different picture.
101
+ */
102
+ function decodeExplicitAlpha(bytes, at, out) {
103
+ for (let texel = 0; texel < 16; texel++) {
104
+ const byte = bytes[at + (texel >> 1)];
105
+ const nibble = texel % 2 === 0 ? byte & 0xf : byte >> 4;
106
+ out[texel * 4 + 3] = nibble * 17;
107
+ }
108
+ }
109
+ /** One block of any format into sixteen RGBA texels. */
110
+ function decodeBlock(format, bytes, at, out) {
111
+ out.fill(0, 0, 64);
112
+ if (format === 'bc1') {
113
+ decodeColourBlock(bytes, at, out, true);
114
+ }
115
+ else if (format === 'bc2') {
116
+ decodeColourBlock(bytes, at + 8, out, false);
117
+ decodeExplicitAlpha(bytes, at, out);
118
+ }
119
+ else if (format === 'bc3') {
120
+ decodeColourBlock(bytes, at + 8, out, false);
121
+ decodeAlphaBlock(bytes, at, out, 4, 3);
122
+ }
123
+ else if (format === 'bc4') {
124
+ decodeAlphaBlock(bytes, at, out, 4, 0);
125
+ for (let t = 0; t < 16; t++)
126
+ out[t * 4 + 3] = 255;
127
+ }
128
+ else if (format === 'bc5') {
129
+ decodeAlphaBlock(bytes, at, out, 4, 0);
130
+ decodeAlphaBlock(bytes, at + 8, out, 4, 1);
131
+ for (let t = 0; t < 16; t++)
132
+ out[t * 4 + 3] = 255;
133
+ }
134
+ else {
135
+ decodeBc7Block(bytes, at, out);
136
+ }
137
+ }
138
+ /**
139
+ * A whole surface of blocks into RGBA, eight bits a channel, `width` by `height`. A surface that is
140
+ * not a whole number of blocks is decoded block by block and cropped, which is what the format does:
141
+ * the blocks cover a padded rectangle and the texels past the declared size are padding.
142
+ */
143
+ export function decodeBc(format, width, height, blocks, out = new Uint8Array(width * height * 4)) {
144
+ const blockBytes = BC_BLOCK_BYTES[format];
145
+ const across = Math.max(1, Math.ceil(width / 4));
146
+ const down = Math.max(1, Math.ceil(height / 4));
147
+ const needed = across * down * blockBytes;
148
+ if (blocks.length < needed) {
149
+ throw new DrftError(`bc: a ${width}x${height} ${format.toUpperCase()} surface needs ${needed} bytes of blocks and ` +
150
+ `${blocks.length} were given`);
151
+ }
152
+ const texels = TEXELS;
153
+ for (let by = 0; by < down; by++) {
154
+ for (let bx = 0; bx < across; bx++) {
155
+ decodeBlock(format, blocks, (by * across + bx) * blockBytes, texels);
156
+ for (let ty = 0; ty < 4; ty++) {
157
+ const y = by * 4 + ty;
158
+ if (y >= height)
159
+ break;
160
+ for (let tx = 0; tx < 4; tx++) {
161
+ const x = bx * 4 + tx;
162
+ if (x >= width)
163
+ break;
164
+ const from = (ty * 4 + tx) * 4;
165
+ const to = (y * width + x) * 4;
166
+ out[to] = texels[from];
167
+ out[to + 1] = texels[from + 1];
168
+ out[to + 2] = texels[from + 2];
169
+ out[to + 3] = texels[from + 3];
170
+ }
171
+ }
172
+ }
173
+ }
174
+ return out;
175
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * A BC5 normal map's green turned over in its blocks — `255 − g` at every texel — without decoding.
3
+ *
4
+ * **Why in the blocks.** A DirectX normal map points green down where glTF points it up, and the
5
+ * baker turns such a map over on the way in. For a PNG that is a pass over the texels; for BC5 the
6
+ * same pass would mean decoding and re-encoding, and this engine has no encoder — so the map would
7
+ * leave the bake uncompressed, and BC5 is what normal maps are stored as. Turning a BC4 half over
8
+ * is exact in its own terms instead: swap the two endpoints and turn each over, then renumber the
9
+ * indices so each texel names the value that is now its opposite.
10
+ *
11
+ * **Swapping keeps the mode**, which is the point of swapping: `a0 > a1` selects six interpolants
12
+ * and `a0 ≤ a1` four plus a hard 0 and 255, and `255 − a1 > 255 − a0` exactly when `a0 > a1`.
13
+ * Within a mode, index `i` of the interpolants counts from the first endpoint, so it becomes the
14
+ * one counting the same distance from the other end; the hard 0 and 255 trade places.
15
+ *
16
+ * **What it gives up**: an interpolant that falls on a half rounds the same way both before and
17
+ * after, so a texel at such a value lands one away from `255 − g`. That is inside the error the
18
+ * format already carries. Runs in the baker, so it copies.
19
+ */
20
+ /** A copy of `blocks`, a level of BC5, with every block's green half turned over. */
21
+ export declare function flipBc5Green(blocks: Uint8Array): Uint8Array;
package/dist/bcFlip.js ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * A BC5 normal map's green turned over in its blocks — `255 − g` at every texel — without decoding.
3
+ *
4
+ * **Why in the blocks.** A DirectX normal map points green down where glTF points it up, and the
5
+ * baker turns such a map over on the way in. For a PNG that is a pass over the texels; for BC5 the
6
+ * same pass would mean decoding and re-encoding, and this engine has no encoder — so the map would
7
+ * leave the bake uncompressed, and BC5 is what normal maps are stored as. Turning a BC4 half over
8
+ * is exact in its own terms instead: swap the two endpoints and turn each over, then renumber the
9
+ * indices so each texel names the value that is now its opposite.
10
+ *
11
+ * **Swapping keeps the mode**, which is the point of swapping: `a0 > a1` selects six interpolants
12
+ * and `a0 ≤ a1` four plus a hard 0 and 255, and `255 − a1 > 255 − a0` exactly when `a0 > a1`.
13
+ * Within a mode, index `i` of the interpolants counts from the first endpoint, so it becomes the
14
+ * one counting the same distance from the other end; the hard 0 and 255 trade places.
15
+ *
16
+ * **What it gives up**: an interpolant that falls on a half rounds the same way both before and
17
+ * after, so a texel at such a value lands one away from `255 − g`. That is inside the error the
18
+ * format already carries. Runs in the baker, so it copies.
19
+ */
20
+ /** Six-value mode: endpoints trade places, interpolant `i` becomes `9 − i`. */
21
+ const SIX = [1, 0, 7, 6, 5, 4, 3, 2];
22
+ /** Four-value mode: endpoints trade, interpolant `i` becomes `7 − i`, and 0 and 255 trade. */
23
+ const FOUR = [1, 0, 5, 4, 3, 2, 7, 6];
24
+ /** A copy of `blocks`, a level of BC5, with every block's green half turned over. */
25
+ export function flipBc5Green(blocks) {
26
+ const out = new Uint8Array(blocks);
27
+ for (let at = 8; at + 8 <= out.length; at += 16)
28
+ flipHalf(out, at);
29
+ return out;
30
+ }
31
+ function flipHalf(bytes, at) {
32
+ const a0 = bytes[at];
33
+ const a1 = bytes[at + 1];
34
+ const map = a0 > a1 ? SIX : FOUR;
35
+ bytes[at] = 255 - a1;
36
+ bytes[at + 1] = 255 - a0;
37
+ /* Sixteen three-bit indices in two 24-bit runs, least significant first. */
38
+ for (let half = 0; half < 2; half++) {
39
+ const base = at + 2 + half * 3;
40
+ let run = bytes[base] |
41
+ (bytes[base + 1] << 8) |
42
+ (bytes[base + 2] << 16);
43
+ let flipped = 0;
44
+ for (let texel = 0; texel < 8; texel++) {
45
+ flipped |= map[run & 7] << (texel * 3);
46
+ run >>>= 3;
47
+ }
48
+ bytes[base] = flipped & 0xff;
49
+ bytes[base + 1] = (flipped >> 8) & 0xff;
50
+ bytes[base + 2] = (flipped >> 16) & 0xff;
51
+ }
52
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Block-compressed texels decoded into an ordinary image: what a PNG of them has to hold.
3
+ *
4
+ * **Not what a GPU samples, on purpose.** `decodeBc` returns what a device would — BC4 as red alone,
5
+ * BC5 as red and green with blue at zero — because the loader's fallback must look like a compressed
6
+ * upload. A baked image is read later as an image, by a slot that reads three channels, so the two
7
+ * formats that store fewer are completed here: BC4's one value fills all three, as a luminance
8
+ * surface does, and BC5's missing z is rebuilt from the two it has.
9
+ *
10
+ * Shared by the DDS reader and the baker's texture pass, which decode the same blocks for the same
11
+ * reason. Runs in the baker, so it allocates the image it returns.
12
+ */
13
+ import type { BcFormat } from '@driftengine/drft';
14
+ /** `blocks` as an RGBA image, eight bits a channel, every channel meaning what an image's does. */
15
+ export declare function decodeBcImage(format: BcFormat, width: number, height: number, blocks: Uint8Array): Uint8Array;
@@ -0,0 +1,38 @@
1
+ import { decodeBc } from './bcDecode.js';
2
+ /** `blocks` as an RGBA image, eight bits a channel, every channel meaning what an image's does. */
3
+ export function decodeBcImage(format, width, height, blocks) {
4
+ const rgba = decodeBc(format, width, height, blocks);
5
+ if (format === 'bc5')
6
+ rebuildNormalZ(rgba);
7
+ if (format === 'bc4')
8
+ greyFromRed(rgba);
9
+ return rgba;
10
+ }
11
+ /**
12
+ * BC5 is two BC4 blocks, red then green, and it is how a normal map is stored: x and y, with z
13
+ * implied by the normal's unit length. **A baked image has three channels and the lit shader reads
14
+ * three**, so z is rebuilt here as `(√(1 − x² − y²) + 1) / 2`. Until 4.8.4 the DDS reader said z
15
+ * belonged in the shader that consumes the map, and no shader rebuilt it: every BC5 normal map baked
16
+ * to PNG arrived with blue at zero, a normal lying in the surface it was meant to stand out of.
17
+ *
18
+ * Outside the unit disc — compression error at a grazing normal — z is clamped to zero rather than
19
+ * reflected, which would turn the normal back to face the viewer.
20
+ *
21
+ * What it gives up: a BC5 surface that is not a normal map gets a blue channel derived as though it
22
+ * were. Red and green are untouched, so anything reading the two channels it stored loses nothing.
23
+ */
24
+ function rebuildNormalZ(rgba) {
25
+ for (let at = 0; at < rgba.length; at += 4) {
26
+ const x = (rgba[at] / 255) * 2 - 1;
27
+ const y = (rgba[at + 1] / 255) * 2 - 1;
28
+ const z = Math.sqrt(Math.max(0, 1 - x * x - y * y));
29
+ rgba[at + 2] = Math.round((z * 0.5 + 0.5) * 255);
30
+ }
31
+ }
32
+ /** BC4 is one channel, and a one-channel image is grey. */
33
+ function greyFromRed(rgba) {
34
+ for (let at = 0; at < rgba.length; at += 4) {
35
+ rgba[at + 1] = rgba[at];
36
+ rgba[at + 2] = rgba[at];
37
+ }
38
+ }
@@ -0,0 +1,26 @@
1
+ import type { CompressedTextureFormat } from '@driftengine/core';
2
+ import type { BcImage } from '@driftengine/drft';
3
+ import type { BcRequest } from './bcAnswer.ts';
4
+ export type { BcReply, BcRequest } from './bcAnswer.ts';
5
+ export { answerBcRequest } from './bcAnswer.ts';
6
+ /** What happens to one BC texture. */
7
+ export type BcPlan = 'blocks' | 'decode';
8
+ /** Blocks where this device takes the format in the colour space the slot reads, with a chain. */
9
+ export declare function bcPlan(image: BcImage, srgb: boolean, available: readonly CompressedTextureFormat[]): BcPlan;
10
+ /** The part of a worker the decoder uses. A browser `Worker` satisfies it. */
11
+ export interface BcWorker {
12
+ postMessage(request: BcRequest, transfer?: Transferable[]): void;
13
+ terminate(): void;
14
+ onmessage: ((event: {
15
+ data: unknown;
16
+ }) => void) | null;
17
+ onerror?: ((event: unknown) => void) | null;
18
+ }
19
+ /** Level 0 of a BC image decoded to RGBA, off the main thread where one is to be had. */
20
+ export interface BcDecoder {
21
+ decode(image: BcImage, asImage: boolean): Promise<Uint8Array>;
22
+ /** Empty while a worker decodes; otherwise why the main thread does. */
23
+ readonly reason: string;
24
+ dispose(): void;
25
+ }
26
+ export declare function createBcDecoder(spawn?: (() => BcWorker) | null): BcDecoder;