@driftengine/assets 4.8.7 → 4.10.0
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.
- package/README.md +22 -4
- package/dist/bcLoad.d.ts +8 -3
- package/dist/bcLoad.js +4 -2
- package/dist/bcWorker.js +12 -4
- package/dist/bcWorkers.d.ts +11 -3
- package/dist/bcWorkers.js +1 -1
- package/dist/drftLoader.d.ts +26 -1
- package/dist/drftLoader.js +74 -0
- package/dist/etc2Answer.d.ts +31 -0
- package/dist/etc2Answer.js +15 -0
- package/dist/etc2Chain.d.ts +30 -0
- package/dist/etc2Chain.js +84 -0
- package/dist/etc2Encode.d.ts +33 -0
- package/dist/etc2Encode.js +531 -0
- package/dist/etc2Load.d.ts +20 -0
- package/dist/etc2Load.js +124 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +9 -0
- package/dist/ktx2.d.ts +10 -0
- package/dist/ktx2.js +134 -0
- package/package.json +9 -4
- package/src/bcLoad.ts +10 -4
- package/src/bcWorker.ts +14 -5
- package/src/bcWorkers.ts +11 -3
- package/src/drftLoader.ts +89 -2
- package/src/etc2Answer.ts +48 -0
- package/src/etc2Chain.ts +113 -0
- package/src/etc2Encode.ts +566 -0
- package/src/etc2Load.ts +154 -0
- package/src/index.ts +11 -0
- package/src/ktx2.ts +151 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ETC2 and EAC blocks made from pixels: what a phone samples, at half a byte a texel for colour and
|
|
3
|
+
* a byte with alpha, where RGBA costs four. The loader encodes a texture this way where the device
|
|
4
|
+
* takes ETC2 and not the BC its file holds (`etc2Plan`), and a caller holding pixels of its own can
|
|
5
|
+
* call `encodeEtc2` directly — off the main thread, since a 2048² image is 262,144 blocks.
|
|
6
|
+
*
|
|
7
|
+
* **ETC2's colour, through the modes it shares with ETC1**: each 4x4 block split in two halves,
|
|
8
|
+
* side by side or one above the other, each half a base colour and one of eight tables of
|
|
9
|
+
* brightness steps, each texel the step nearest its own colour. Both splits and both ways of storing
|
|
10
|
+
* the two bases — each on its own at four bits, or the second as a small difference from the first
|
|
11
|
+
* at five — are tried, and the least error kept. **What it gives up** is ETC2's three further modes,
|
|
12
|
+
* T, H and planar, which every ETC2 device decodes and this encoder never chooses: a smooth gradient
|
|
13
|
+
* across a block and a sharp edge between two colours come out a little coarser than a full search
|
|
14
|
+
* would make them. What would make that worth changing is a texture whose banding shows at the size
|
|
15
|
+
* it is seen.
|
|
16
|
+
*
|
|
17
|
+
* **EAC for everything that is not colour**: an alpha channel beside ETC2's colour, and one or two
|
|
18
|
+
* channels of data on their own (`eac-r11`, `eac-rg11`) — a roughness map, a normal map's x and y.
|
|
19
|
+
* Each block a base, a step size and one of sixteen tables of eight steps, searched per table.
|
|
20
|
+
*
|
|
21
|
+
* Texels past the edge of a level smaller than a block repeat the last row and column, so a 2x2 or
|
|
22
|
+
* 1x1 level is one block whose other texels match. Allocates the blocks it returns, once a level.
|
|
23
|
+
*/
|
|
24
|
+
/** ETC1's eight tables of brightness steps: the small and the large, each added and subtracted. */
|
|
25
|
+
const STEPS = [
|
|
26
|
+
[2, 8],
|
|
27
|
+
[5, 17],
|
|
28
|
+
[9, 29],
|
|
29
|
+
[13, 42],
|
|
30
|
+
[18, 60],
|
|
31
|
+
[24, 80],
|
|
32
|
+
[33, 106],
|
|
33
|
+
[47, 183],
|
|
34
|
+
];
|
|
35
|
+
/** A texel's step by its two-bit index: the small added, the large added, then both subtracted. */
|
|
36
|
+
function step(table, index) {
|
|
37
|
+
const [small, large] = STEPS[table];
|
|
38
|
+
return index === 0 ? small : index === 1 ? large : index === 2 ? -small : -large;
|
|
39
|
+
}
|
|
40
|
+
/** EAC's sixteen tables of eight steps. */
|
|
41
|
+
const EAC = [
|
|
42
|
+
[-3, -6, -9, -15, 2, 5, 8, 14],
|
|
43
|
+
[-3, -7, -10, -13, 2, 6, 9, 12],
|
|
44
|
+
[-2, -5, -8, -13, 1, 4, 7, 12],
|
|
45
|
+
[-2, -4, -6, -13, 1, 3, 5, 12],
|
|
46
|
+
[-3, -6, -8, -12, 2, 5, 7, 11],
|
|
47
|
+
[-3, -7, -9, -11, 2, 6, 8, 10],
|
|
48
|
+
[-4, -7, -8, -11, 3, 6, 7, 10],
|
|
49
|
+
[-3, -5, -8, -11, 2, 4, 7, 10],
|
|
50
|
+
[-2, -6, -8, -10, 1, 5, 7, 9],
|
|
51
|
+
[-2, -5, -8, -10, 1, 4, 7, 9],
|
|
52
|
+
[-2, -4, -8, -10, 1, 3, 7, 9],
|
|
53
|
+
[-2, -5, -7, -10, 1, 4, 6, 9],
|
|
54
|
+
[-3, -4, -7, -10, 2, 3, 6, 9],
|
|
55
|
+
[-1, -2, -3, -10, 0, 1, 2, 9],
|
|
56
|
+
[-4, -6, -8, -9, 3, 5, 7, 8],
|
|
57
|
+
[-3, -5, -7, -9, 2, 4, 6, 8],
|
|
58
|
+
];
|
|
59
|
+
/** Per EAC table, the index of the step nearest each whole offset from -16 to 15. */
|
|
60
|
+
const NEAREST = EAC.map((steps) => {
|
|
61
|
+
const lookup = new Int8Array(32);
|
|
62
|
+
for (let u = -16; u < 16; u++) {
|
|
63
|
+
let chosen = 0;
|
|
64
|
+
for (let index = 1; index < 8; index++) {
|
|
65
|
+
if (Math.abs(steps[index] - u) < Math.abs(steps[chosen] - u)) {
|
|
66
|
+
chosen = index;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
lookup[u + 16] = chosen;
|
|
70
|
+
}
|
|
71
|
+
return lookup;
|
|
72
|
+
});
|
|
73
|
+
/** Bytes a block of each format takes. */
|
|
74
|
+
const BYTES = {
|
|
75
|
+
'etc2-rgb8': 8,
|
|
76
|
+
'etc2-rgba8': 16,
|
|
77
|
+
'eac-r11': 8,
|
|
78
|
+
'eac-rg11': 16,
|
|
79
|
+
};
|
|
80
|
+
const clamp255 = (v) => (v < 0 ? 0 : v > 255 ? 255 : v);
|
|
81
|
+
/** One block's texels, RGBA, in ETC's own order: down each column, then across. */
|
|
82
|
+
const texels = new Int32Array(64);
|
|
83
|
+
/** `rgba` at `width` by `height` as `format` blocks, row of blocks after row of blocks. */
|
|
84
|
+
export function encodeEtc2(format, rgba, width, height) {
|
|
85
|
+
const across = Math.max(1, Math.ceil(width / 4));
|
|
86
|
+
const down = Math.max(1, Math.ceil(height / 4));
|
|
87
|
+
const bytes = BYTES[format];
|
|
88
|
+
const out = new Uint8Array(across * down * bytes);
|
|
89
|
+
for (let by = 0; by < down; by++) {
|
|
90
|
+
for (let bx = 0; bx < across; bx++) {
|
|
91
|
+
gather(rgba, width, height, bx * 4, by * 4);
|
|
92
|
+
const at = (by * across + bx) * bytes;
|
|
93
|
+
if (format === 'etc2-rgb8')
|
|
94
|
+
encodeColour(out, at);
|
|
95
|
+
else if (format === 'etc2-rgba8') {
|
|
96
|
+
encodeEac(out, at, 3, false);
|
|
97
|
+
encodeColour(out, at + 8);
|
|
98
|
+
}
|
|
99
|
+
else if (format === 'eac-r11')
|
|
100
|
+
encodeEac(out, at, 0, true);
|
|
101
|
+
else {
|
|
102
|
+
encodeEac(out, at, 0, true);
|
|
103
|
+
encodeEac(out, at + 8, 1, true);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
/** The block at `(x0, y0)` into `texels`, the edge repeated past a level smaller than a block. */
|
|
110
|
+
function gather(rgba, width, height, x0, y0) {
|
|
111
|
+
for (let x = 0; x < 4; x++) {
|
|
112
|
+
for (let y = 0; y < 4; y++) {
|
|
113
|
+
const sx = Math.min(x0 + x, width - 1);
|
|
114
|
+
const sy = Math.min(y0 + y, height - 1);
|
|
115
|
+
const from = (sy * width + sx) * 4;
|
|
116
|
+
const to = (x * 4 + y) * 4;
|
|
117
|
+
texels[to] = rgba[from];
|
|
118
|
+
texels[to + 1] = rgba[from + 1];
|
|
119
|
+
texels[to + 2] = rgba[from + 2];
|
|
120
|
+
texels[to + 3] = rgba[from + 3];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** Whether texel `i` (ETC order) is in the first half, for a split `flip` names. */
|
|
125
|
+
const firstHalf = (i, flip) => (flip === 0 ? i < 8 : i % 4 < 2);
|
|
126
|
+
/** The eight texels of each half: `HALF_TEXELS[flip * 2 + half]`. */
|
|
127
|
+
const HALF_TEXELS = [0, 1, 2, 3].map((k) => {
|
|
128
|
+
const list = new Int32Array(8);
|
|
129
|
+
let count = 0;
|
|
130
|
+
for (let i = 0; i < 16; i++) {
|
|
131
|
+
if (firstHalf(i, k >> 1) === ((k & 1) === 0))
|
|
132
|
+
list[count++] = i;
|
|
133
|
+
}
|
|
134
|
+
return list;
|
|
135
|
+
});
|
|
136
|
+
/** Each table's small and large step, as numbers the search reads without unpacking a pair. */
|
|
137
|
+
const SMALL = new Int32Array(STEPS.map(([small]) => small));
|
|
138
|
+
const LARGE = new Int32Array(STEPS.map(([, large]) => large));
|
|
139
|
+
const halves = [
|
|
140
|
+
{ error: 0, table: 0, indices: new Int32Array(8) },
|
|
141
|
+
{ error: 0, table: 0, indices: new Int32Array(8) },
|
|
142
|
+
];
|
|
143
|
+
const best = { error: Infinity, high: 0, low: 0 };
|
|
144
|
+
const base = new Int32Array(6);
|
|
145
|
+
const quantized = new Int32Array(6);
|
|
146
|
+
const offsets = new Float64Array(8);
|
|
147
|
+
/** The least-error ETC1-mode block for `texels`, written at `at`. */
|
|
148
|
+
function encodeColour(out, at) {
|
|
149
|
+
best.error = Infinity;
|
|
150
|
+
const [first, second] = halves;
|
|
151
|
+
for (let flip = 0; flip < 2; flip++) {
|
|
152
|
+
let fitted = false;
|
|
153
|
+
for (let differential = 1; differential >= 0; differential--) {
|
|
154
|
+
/* Five bits where the pair's difference fits, which is nearly always the better; four bits
|
|
155
|
+
each only where it does not, so a split pays for one search and not two. */
|
|
156
|
+
if (differential === 0 && fitted)
|
|
157
|
+
continue;
|
|
158
|
+
fitted = bases(flip, differential === 1);
|
|
159
|
+
if (!fitted)
|
|
160
|
+
continue;
|
|
161
|
+
/* Each half searched only as far as it could still beat the best block so far. */
|
|
162
|
+
searchHalf(flip, 0, first, best.error);
|
|
163
|
+
if (first.error >= best.error)
|
|
164
|
+
continue;
|
|
165
|
+
searchHalf(flip, 1, second, best.error - first.error);
|
|
166
|
+
const error = first.error + second.error;
|
|
167
|
+
if (error >= best.error)
|
|
168
|
+
continue;
|
|
169
|
+
best.error = error;
|
|
170
|
+
let high = 0;
|
|
171
|
+
if (differential === 1) {
|
|
172
|
+
for (let c = 0; c < 3; c++) {
|
|
173
|
+
const q = quantized[c];
|
|
174
|
+
const delta = quantized[c + 3] - q;
|
|
175
|
+
high |= ((q << 3) | (delta & 7)) << (24 - c * 8);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
for (let c = 0; c < 3; c++) {
|
|
180
|
+
high |= ((quantized[c] << 4) | quantized[c + 3]) << (24 - c * 8);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
high |= (first.table << 5) | (second.table << 2) | (differential << 1) | flip;
|
|
184
|
+
let low = 0;
|
|
185
|
+
for (let half = 0; half < 2; half++) {
|
|
186
|
+
const list = HALF_TEXELS[flip * 2 + half];
|
|
187
|
+
const indices = halves[half].indices;
|
|
188
|
+
for (let k = 0; k < 8; k++) {
|
|
189
|
+
const i = list[k];
|
|
190
|
+
const index = indices[k];
|
|
191
|
+
low |= ((index >> 1) << (i + 16)) | ((index & 1) << i);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
best.high = high >>> 0;
|
|
195
|
+
best.low = low >>> 0;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
write32(out, at, best.high);
|
|
199
|
+
write32(out, at + 4, best.low);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Each half's average, quantised as the mode stores it and expanded back, into `base` — and false
|
|
203
|
+
* where the differential mode cannot hold the second as a step of -4 to 3 from the first.
|
|
204
|
+
*/
|
|
205
|
+
function bases(flip, differential) {
|
|
206
|
+
for (let half = 0; half < 2; half++) {
|
|
207
|
+
const list = HALF_TEXELS[flip * 2 + half];
|
|
208
|
+
let r = 0;
|
|
209
|
+
let g = 0;
|
|
210
|
+
let b = 0;
|
|
211
|
+
for (let k = 0; k < 8; k++) {
|
|
212
|
+
const i = list[k];
|
|
213
|
+
r += texels[i * 4];
|
|
214
|
+
g += texels[i * 4 + 1];
|
|
215
|
+
b += texels[i * 4 + 2];
|
|
216
|
+
}
|
|
217
|
+
for (let c = 0; c < 3; c++) {
|
|
218
|
+
const average = (c === 0 ? r : c === 1 ? g : b) / 8;
|
|
219
|
+
if (differential) {
|
|
220
|
+
const q = Math.round((average * 31) / 255);
|
|
221
|
+
quantized[half * 3 + c] = q;
|
|
222
|
+
base[half * 3 + c] = (q << 3) | (q >> 2);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
const q = Math.round((average * 15) / 255);
|
|
226
|
+
quantized[half * 3 + c] = q;
|
|
227
|
+
base[half * 3 + c] = (q << 4) | q;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (!differential)
|
|
232
|
+
return true;
|
|
233
|
+
for (let c = 0; c < 3; c++) {
|
|
234
|
+
const delta = quantized[c + 3] - quantized[c];
|
|
235
|
+
if (delta < -4 || delta > 3)
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* The table with the least error for one half against its base, and each texel's step for it —
|
|
242
|
+
* or `into.error` left at infinity where no table comes under `bound`.
|
|
243
|
+
*
|
|
244
|
+
* **A texel's step is found, not searched.** Every step moves all three channels by one amount, so
|
|
245
|
+
* the amount that brings a texel nearest its colour is its average offset from the base, and the
|
|
246
|
+
* step to take is the one nearest that. Unclamped, a texel's error at step `s` is its spread about
|
|
247
|
+
* that average offset `o` plus `3(s − o)²`, so a table's error is the half's spread, summed once,
|
|
248
|
+
* plus that term — exact, with no channel read again. **Only a table whose large step could carry a
|
|
249
|
+
* channel past 0 or 255 is checked texel by texel**, with the clamp a device applies and the
|
|
250
|
+
* nearest step's neighbour as a second candidate, since a clamped channel can make the nearest step
|
|
251
|
+
* no longer the closest. On a photograph that is a dark or a bright block's few tables.
|
|
252
|
+
*
|
|
253
|
+
* What it gives up is the error a weighting by channel would trade; the steps are one brightness for
|
|
254
|
+
* all three, so a weight changes which of two near steps wins and little else.
|
|
255
|
+
*/
|
|
256
|
+
function searchHalf(flip, half, into, bound) {
|
|
257
|
+
const list = HALF_TEXELS[flip * 2 + half];
|
|
258
|
+
const r0 = base[half * 3];
|
|
259
|
+
const g0 = base[half * 3 + 1];
|
|
260
|
+
const b0 = base[half * 3 + 2];
|
|
261
|
+
const lowest = Math.min(r0, g0, b0);
|
|
262
|
+
const highest = Math.max(r0, g0, b0);
|
|
263
|
+
let spread = 0;
|
|
264
|
+
for (let k = 0; k < 8; k++) {
|
|
265
|
+
const i = list[k];
|
|
266
|
+
const dr = texels[i * 4] - r0;
|
|
267
|
+
const dg = texels[i * 4 + 1] - g0;
|
|
268
|
+
const db = texels[i * 4 + 2] - b0;
|
|
269
|
+
const offset = (dr + dg + db) / 3;
|
|
270
|
+
offsets[k] = offset;
|
|
271
|
+
spread += dr * dr + dg * dg + db * db - 3 * offset * offset;
|
|
272
|
+
}
|
|
273
|
+
into.error = Infinity;
|
|
274
|
+
let chosen = -1;
|
|
275
|
+
let least = bound;
|
|
276
|
+
for (let table = 0; table < 8; table++) {
|
|
277
|
+
const small = SMALL[table];
|
|
278
|
+
const large = LARGE[table];
|
|
279
|
+
const error = lowest - large >= 0 && highest + large <= 255
|
|
280
|
+
? spread + 3 * nearestSum(small, large, null)
|
|
281
|
+
: clampedError(list, r0, g0, b0, small, large, least, null);
|
|
282
|
+
if (error < least) {
|
|
283
|
+
least = error;
|
|
284
|
+
chosen = table;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (chosen < 0)
|
|
288
|
+
return;
|
|
289
|
+
into.error = least;
|
|
290
|
+
into.table = chosen;
|
|
291
|
+
const small = SMALL[chosen];
|
|
292
|
+
const large = LARGE[chosen];
|
|
293
|
+
if (lowest - large >= 0 && highest + large <= 255)
|
|
294
|
+
nearestSum(small, large, into.indices);
|
|
295
|
+
else
|
|
296
|
+
clampedError(list, r0, g0, b0, small, large, Infinity, into.indices);
|
|
297
|
+
}
|
|
298
|
+
/** `Σ (s − o)²` over the half's offsets at each one's nearest step, the steps into `write`. */
|
|
299
|
+
function nearestSum(small, large, write) {
|
|
300
|
+
const middle = (small + large) / 2;
|
|
301
|
+
let sum = 0;
|
|
302
|
+
for (let k = 0; k < 8; k++) {
|
|
303
|
+
const offset = offsets[k];
|
|
304
|
+
let index;
|
|
305
|
+
let d;
|
|
306
|
+
if (offset >= 0) {
|
|
307
|
+
index = offset > middle ? 1 : 0;
|
|
308
|
+
d = (index === 1 ? large : small) - offset;
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
index = offset < -middle ? 3 : 2;
|
|
312
|
+
d = (index === 3 ? -large : -small) - offset;
|
|
313
|
+
}
|
|
314
|
+
if (write !== null)
|
|
315
|
+
write[k] = index;
|
|
316
|
+
sum += d * d;
|
|
317
|
+
}
|
|
318
|
+
return sum;
|
|
319
|
+
}
|
|
320
|
+
/** A table's error with every channel clamped as a device does, stopping once past `bound`. */
|
|
321
|
+
function clampedError(list, r0, g0, b0, small, large, bound, write) {
|
|
322
|
+
let error = 0;
|
|
323
|
+
for (let k = 0; k < 8; k++) {
|
|
324
|
+
const i = list[k];
|
|
325
|
+
const offset = offsets[k];
|
|
326
|
+
/* The nearest step by the offset, and its neighbour toward zero. */
|
|
327
|
+
let near;
|
|
328
|
+
let other;
|
|
329
|
+
if (offset >= 0) {
|
|
330
|
+
near = offset > (small + large) / 2 ? 1 : 0;
|
|
331
|
+
other = near === 1 ? 0 : 2;
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
near = offset < -(small + large) / 2 ? 3 : 2;
|
|
335
|
+
other = near === 3 ? 2 : 0;
|
|
336
|
+
}
|
|
337
|
+
const e1 = texelError(i, r0, g0, b0, stepOf(near, small, large));
|
|
338
|
+
const e2 = texelError(i, r0, g0, b0, stepOf(other, small, large));
|
|
339
|
+
if (write !== null)
|
|
340
|
+
write[k] = e1 <= e2 ? near : other;
|
|
341
|
+
error += e1 <= e2 ? e1 : e2;
|
|
342
|
+
if (error >= bound)
|
|
343
|
+
return error;
|
|
344
|
+
}
|
|
345
|
+
return error;
|
|
346
|
+
}
|
|
347
|
+
/** A step by its two-bit index, from a table's two amounts. */
|
|
348
|
+
function stepOf(index, small, large) {
|
|
349
|
+
return index === 0 ? small : index === 1 ? large : index === 2 ? -small : -large;
|
|
350
|
+
}
|
|
351
|
+
/** One texel's squared error against a base moved by `s`, every channel clamped as a device does. */
|
|
352
|
+
function texelError(i, r0, g0, b0, s) {
|
|
353
|
+
const dr = texels[i * 4] - clamp255(r0 + s);
|
|
354
|
+
const dg = texels[i * 4 + 1] - clamp255(g0 + s);
|
|
355
|
+
const db = texels[i * 4 + 2] - clamp255(b0 + s);
|
|
356
|
+
return dr * dr + dg * dg + db * db;
|
|
357
|
+
}
|
|
358
|
+
const eacIndices = new Int32Array(16);
|
|
359
|
+
const eacBest = new Int32Array(16);
|
|
360
|
+
/**
|
|
361
|
+
* One EAC block of channel `channel` at `at`: eight-bit, as alpha beside colour is, or eleven-bit,
|
|
362
|
+
* as `eac-r11` stores data. A block of one value is stored exactly, through the one table with a
|
|
363
|
+
* zero step.
|
|
364
|
+
*/
|
|
365
|
+
function encodeEac(out, at, channel, eleven) {
|
|
366
|
+
let min = 255;
|
|
367
|
+
let max = 0;
|
|
368
|
+
for (let i = 0; i < 16; i++) {
|
|
369
|
+
const v = texels[i * 4 + channel];
|
|
370
|
+
if (v < min)
|
|
371
|
+
min = v;
|
|
372
|
+
if (v > max)
|
|
373
|
+
max = v;
|
|
374
|
+
}
|
|
375
|
+
/* Targets in the stored range: eight bits, or eleven with 255 at 2047. */
|
|
376
|
+
const scale = eleven ? 2047 / 255 : 1;
|
|
377
|
+
let bestError = Infinity;
|
|
378
|
+
let bestBase = 0;
|
|
379
|
+
let bestMultiplier = 1;
|
|
380
|
+
let bestTable = 13;
|
|
381
|
+
for (let table = 0; table < 16; table++) {
|
|
382
|
+
const steps = EAC[table];
|
|
383
|
+
let lo = Infinity;
|
|
384
|
+
let hi = -Infinity;
|
|
385
|
+
for (const s of steps) {
|
|
386
|
+
if (s < lo)
|
|
387
|
+
lo = s;
|
|
388
|
+
if (s > hi)
|
|
389
|
+
hi = s;
|
|
390
|
+
}
|
|
391
|
+
const span = (max - min) * scale;
|
|
392
|
+
const grain = eleven ? 8 : 1;
|
|
393
|
+
const ideal = span / ((hi - lo) * grain);
|
|
394
|
+
for (let round = 0; round < 2; round++) {
|
|
395
|
+
const m = Math.min(15, Math.max(1, round === 0 ? Math.floor(ideal) : Math.ceil(ideal)));
|
|
396
|
+
/* The base that puts the table's middle on the block's. */
|
|
397
|
+
const middle = ((min + max) / 2) * scale - ((hi + lo) / 2) * m * grain;
|
|
398
|
+
const b = Math.min(255, Math.max(0, Math.round(eleven ? (middle - 4) / 8 : middle)));
|
|
399
|
+
let error = 0;
|
|
400
|
+
const nearest = NEAREST[table];
|
|
401
|
+
const origin = eleven ? b * 8 + 4 : b;
|
|
402
|
+
const unit = m * grain;
|
|
403
|
+
for (let i = 0; i < 16; i++) {
|
|
404
|
+
const target = texels[i * 4 + channel] * scale;
|
|
405
|
+
/* The step nearest the texel's offset, looked up, and checked exactly with the clamp. */
|
|
406
|
+
const u = Math.round((target - origin) / unit);
|
|
407
|
+
const chosen = nearest[(u < -16 ? -16 : u > 15 ? 15 : u) + 16];
|
|
408
|
+
const d = eacValue(b, m, steps[chosen], eleven) - target;
|
|
409
|
+
eacIndices[i] = chosen;
|
|
410
|
+
error += d * d;
|
|
411
|
+
if (error >= bestError)
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
if (error < bestError) {
|
|
415
|
+
bestError = error;
|
|
416
|
+
bestBase = b;
|
|
417
|
+
bestMultiplier = m;
|
|
418
|
+
bestTable = table;
|
|
419
|
+
eacBest.set(eacIndices);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
out[at] = bestBase;
|
|
424
|
+
out[at + 1] = (bestMultiplier << 4) | bestTable;
|
|
425
|
+
/* Forty-eight bits of three-bit indices, the first texel in the highest: two words of 24. */
|
|
426
|
+
let first = 0;
|
|
427
|
+
let second = 0;
|
|
428
|
+
for (let i = 0; i < 8; i++)
|
|
429
|
+
first = (first << 3) | eacBest[i];
|
|
430
|
+
for (let i = 8; i < 16; i++)
|
|
431
|
+
second = (second << 3) | eacBest[i];
|
|
432
|
+
out[at + 2] = (first >>> 16) & 255;
|
|
433
|
+
out[at + 3] = (first >>> 8) & 255;
|
|
434
|
+
out[at + 4] = first & 255;
|
|
435
|
+
out[at + 5] = (second >>> 16) & 255;
|
|
436
|
+
out[at + 6] = (second >>> 8) & 255;
|
|
437
|
+
out[at + 7] = second & 255;
|
|
438
|
+
}
|
|
439
|
+
/** What an EAC block decodes a step to: eight-bit, or eleven-bit as `eac-r11` stores. */
|
|
440
|
+
function eacValue(base, multiplier, step, eleven) {
|
|
441
|
+
if (!eleven)
|
|
442
|
+
return clamp255(base + step * multiplier);
|
|
443
|
+
const v = base * 8 + 4 + step * multiplier * 8;
|
|
444
|
+
return v < 0 ? 0 : v > 2047 ? 2047 : v;
|
|
445
|
+
}
|
|
446
|
+
function write32(out, at, value) {
|
|
447
|
+
out[at] = (value >>> 24) & 255;
|
|
448
|
+
out[at + 1] = (value >>> 16) & 255;
|
|
449
|
+
out[at + 2] = (value >>> 8) & 255;
|
|
450
|
+
out[at + 3] = value & 255;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* One level of a block back to RGBA, for the modes this encoder writes and EAC: what a device
|
|
454
|
+
* decodes, stated here so the encoder can be held to a number. A block in T, H or planar mode is
|
|
455
|
+
* refused by name, since nothing here writes one.
|
|
456
|
+
*/
|
|
457
|
+
export function decodeEtc2(format, blocks, width, height) {
|
|
458
|
+
const across = Math.max(1, Math.ceil(width / 4));
|
|
459
|
+
const bytes = BYTES[format];
|
|
460
|
+
const out = new Uint8Array(width * height * 4);
|
|
461
|
+
for (let y = 0; y < height; y++) {
|
|
462
|
+
for (let x = 0; x < width; x++) {
|
|
463
|
+
const at = (Math.floor(y / 4) * across + Math.floor(x / 4)) * bytes;
|
|
464
|
+
const i = (x % 4) * 4 + (y % 4);
|
|
465
|
+
const to = (y * width + x) * 4;
|
|
466
|
+
out[to + 3] = 255;
|
|
467
|
+
if (format === 'etc2-rgb8' || format === 'etc2-rgba8') {
|
|
468
|
+
decodeColourTexel(blocks, format === 'etc2-rgba8' ? at + 8 : at, i, out, to);
|
|
469
|
+
if (format === 'etc2-rgba8')
|
|
470
|
+
out[to + 3] = decodeEacTexel(blocks, at, i, false);
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
out[to] = Math.round((decodeEacTexel(blocks, at, i, true) * 255) / 2047);
|
|
474
|
+
out[to + 1] =
|
|
475
|
+
format === 'eac-rg11'
|
|
476
|
+
? Math.round((decodeEacTexel(blocks, at + 8, i, true) * 255) / 2047)
|
|
477
|
+
: 0;
|
|
478
|
+
out[to + 2] = 0;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return out;
|
|
483
|
+
}
|
|
484
|
+
function read32(blocks, at) {
|
|
485
|
+
return (((blocks[at] << 24) |
|
|
486
|
+
(blocks[at + 1] << 16) |
|
|
487
|
+
(blocks[at + 2] << 8) |
|
|
488
|
+
blocks[at + 3]) >>>
|
|
489
|
+
0);
|
|
490
|
+
}
|
|
491
|
+
function decodeColourTexel(blocks, at, i, out, to) {
|
|
492
|
+
const high = read32(blocks, at);
|
|
493
|
+
const low = read32(blocks, at + 4);
|
|
494
|
+
const flip = high & 1;
|
|
495
|
+
const differential = (high >> 1) & 1;
|
|
496
|
+
const half = firstHalf(i, flip) ? 0 : 1;
|
|
497
|
+
const table = half === 0 ? (high >> 5) & 7 : (high >> 2) & 7;
|
|
498
|
+
const index = (((low >>> (i + 16)) & 1) << 1) | ((low >>> i) & 1);
|
|
499
|
+
for (let c = 0; c < 3; c++) {
|
|
500
|
+
let value;
|
|
501
|
+
if (differential === 1) {
|
|
502
|
+
const first = (high >>> (27 - c * 8)) & 31;
|
|
503
|
+
let delta = (high >>> (24 - c * 8)) & 7;
|
|
504
|
+
if (delta >= 4)
|
|
505
|
+
delta -= 8;
|
|
506
|
+
const q = half === 0 ? first : first + delta;
|
|
507
|
+
if (q < 0 || q > 31) {
|
|
508
|
+
throw new Error('decodeEtc2: a block in T, H or planar mode, which this does not decode');
|
|
509
|
+
}
|
|
510
|
+
value = (q << 3) | (q >> 2);
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
const q = (high >>> (half === 0 ? 28 - c * 8 : 24 - c * 8)) & 15;
|
|
514
|
+
value = (q << 4) | q;
|
|
515
|
+
}
|
|
516
|
+
out[to + c] = clamp255(value + step(table, index));
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
function decodeEacTexel(blocks, at, i, eleven) {
|
|
520
|
+
const b = blocks[at];
|
|
521
|
+
const multiplier = blocks[at + 1] >> 4;
|
|
522
|
+
const table = blocks[at + 1] & 15;
|
|
523
|
+
const first = (blocks[at + 2] << 16) |
|
|
524
|
+
(blocks[at + 3] << 8) |
|
|
525
|
+
blocks[at + 4];
|
|
526
|
+
const second = (blocks[at + 5] << 16) |
|
|
527
|
+
(blocks[at + 6] << 8) |
|
|
528
|
+
blocks[at + 7];
|
|
529
|
+
const index = i < 8 ? (first >>> (21 - i * 3)) & 7 : (second >>> (21 - (i - 8) * 3)) & 7;
|
|
530
|
+
return eacValue(b, multiplier, EAC[table][index], eleven);
|
|
531
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CompressedTextureFormat, CompressedTextureSource } from '@driftengine/core';
|
|
2
|
+
import type { BcImage } from '@driftengine/drft';
|
|
3
|
+
import type { BcWorker } from './bcLoad.ts';
|
|
4
|
+
export type { Etc2Reply, Etc2Request } from './etc2Answer.ts';
|
|
5
|
+
export { answerEtc2Request } from './etc2Answer.ts';
|
|
6
|
+
/**
|
|
7
|
+
* Whether `image` can go up as ETC2 or EAC on a device offering `available`, read in the colour
|
|
8
|
+
* space the slot reads: every format it could become, since which colour format is decided by the
|
|
9
|
+
* decoded alpha. EAC has no sRGB twin, so a BC4 or BC5 texture read as colour stays an image.
|
|
10
|
+
*/
|
|
11
|
+
export declare function etc2Plan(image: BcImage, srgb: boolean, available: readonly CompressedTextureFormat[]): boolean;
|
|
12
|
+
/** BC textures to ETC2 or EAC chains, one at a time, on a worker. */
|
|
13
|
+
export interface Etc2Encoder {
|
|
14
|
+
encode(image: BcImage, srgb: boolean): Promise<CompressedTextureSource>;
|
|
15
|
+
/** Empty while the worker lives; otherwise why the textures stay RGBA. */
|
|
16
|
+
readonly reason: string;
|
|
17
|
+
dispose(): void;
|
|
18
|
+
}
|
|
19
|
+
/** An encoder over a worker `spawn` starts: the same factory the decoder is given. */
|
|
20
|
+
export declare function createEtc2Encoder(spawn: () => BcWorker): Etc2Encoder;
|
package/dist/etc2Load.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BC textures re-encoded as ETC2 or EAC for a device that samples those and not BC — which is a
|
|
3
|
+
* phone — and the worker that does it.
|
|
4
|
+
*
|
|
5
|
+
* **After the picture, not before it.** Such a texture is decoded and shown as RGBA first, exactly
|
|
6
|
+
* as it was before this existed (`bcLoad.ts`), and its blocks then go to an encode worker of their
|
|
7
|
+
* own; the chain that comes back replaces the image behind the handle every draw already holds
|
|
8
|
+
* (`updateSurfaceTexture`). So nothing arrives later than it did, and the texture ends at half a
|
|
9
|
+
* byte a texel, or a byte with alpha, where it held four. **What it costs** is the encode, on a core
|
|
10
|
+
* of its own: measured at 0.7 s for a 2048² photograph on a desktop processor, and several times
|
|
11
|
+
* that on a phone's, one texture at a time. **What it gives up** is the memory a load peaks at,
|
|
12
|
+
* which is still every texture as RGBA until the encodes catch up: what would make that wrong is a
|
|
13
|
+
* device that cannot hold the RGBA at all, and the answer then is blocks it samples in the file,
|
|
14
|
+
* which a KTX2 texture carries through as they are.
|
|
15
|
+
*
|
|
16
|
+
* **A worker of its own, not the decode worker**, so a texture waiting to be decoded never waits
|
|
17
|
+
* behind a second of somebody else's encode; and one job in flight at a time, so a model's hundred
|
|
18
|
+
* textures are a hundred small messages queued here rather than a hundred copies of their blocks
|
|
19
|
+
* queued there. **Never on the main thread**: where no worker was named, or the one started fails,
|
|
20
|
+
* the textures stay RGBA and the loader says why once — a texture held at four bytes a texel is
|
|
21
|
+
* the state this improves on, never a stall.
|
|
22
|
+
*/
|
|
23
|
+
import { uploadsCompressed } from '@driftengine/core';
|
|
24
|
+
export { answerEtc2Request } from './etc2Answer.js';
|
|
25
|
+
/**
|
|
26
|
+
* Whether `image` can go up as ETC2 or EAC on a device offering `available`, read in the colour
|
|
27
|
+
* space the slot reads: every format it could become, since which colour format is decided by the
|
|
28
|
+
* decoded alpha. EAC has no sRGB twin, so a BC4 or BC5 texture read as colour stays an image.
|
|
29
|
+
*/
|
|
30
|
+
export function etc2Plan(image, srgb, available) {
|
|
31
|
+
const { format, width, height } = image;
|
|
32
|
+
if (format === 'bc4')
|
|
33
|
+
return uploadsCompressed('eac-r11', srgb, width, height, available);
|
|
34
|
+
if (format === 'bc5')
|
|
35
|
+
return uploadsCompressed('eac-rg11', srgb, width, height, available);
|
|
36
|
+
return (uploadsCompressed('etc2-rgb8', srgb, width, height, available) &&
|
|
37
|
+
uploadsCompressed('etc2-rgba8', srgb, width, height, available));
|
|
38
|
+
}
|
|
39
|
+
/** An encoder over a worker `spawn` starts: the same factory the decoder is given. */
|
|
40
|
+
export function createEtc2Encoder(spawn) {
|
|
41
|
+
let worker = null;
|
|
42
|
+
let reason = '';
|
|
43
|
+
let running = null;
|
|
44
|
+
const waiting = [];
|
|
45
|
+
let next = 0;
|
|
46
|
+
const stop = (why) => {
|
|
47
|
+
reason = why;
|
|
48
|
+
worker?.terminate();
|
|
49
|
+
worker = null;
|
|
50
|
+
const error = new Error(why);
|
|
51
|
+
running?.reject(error);
|
|
52
|
+
running = null;
|
|
53
|
+
for (const job of waiting)
|
|
54
|
+
job.reject(error);
|
|
55
|
+
waiting.length = 0;
|
|
56
|
+
};
|
|
57
|
+
const pump = () => {
|
|
58
|
+
const target = worker;
|
|
59
|
+
if (running !== null || target === null)
|
|
60
|
+
return;
|
|
61
|
+
const job = waiting.shift();
|
|
62
|
+
if (job === undefined)
|
|
63
|
+
return;
|
|
64
|
+
running = job;
|
|
65
|
+
const { image } = job;
|
|
66
|
+
/* A copy, for the reason `bcLoad.ts` gives: a level is a view over the fetched file. */
|
|
67
|
+
const blocks = image.levels[0].slice().buffer;
|
|
68
|
+
const request = {
|
|
69
|
+
kind: 'etc2',
|
|
70
|
+
id: job.id,
|
|
71
|
+
format: image.format,
|
|
72
|
+
width: image.width,
|
|
73
|
+
height: image.height,
|
|
74
|
+
blocks,
|
|
75
|
+
srgb: job.srgb,
|
|
76
|
+
};
|
|
77
|
+
target.postMessage(request, [blocks]);
|
|
78
|
+
};
|
|
79
|
+
try {
|
|
80
|
+
const started = spawn();
|
|
81
|
+
worker = started;
|
|
82
|
+
started.onmessage = (event) => {
|
|
83
|
+
const reply = event.data;
|
|
84
|
+
const job = running;
|
|
85
|
+
if (job === null || reply.id !== job.id)
|
|
86
|
+
return;
|
|
87
|
+
running = null;
|
|
88
|
+
if (reply.format !== undefined && reply.levels !== undefined) {
|
|
89
|
+
const levels = reply.levels.map((level) => new Uint8Array(level));
|
|
90
|
+
const { width, height } = job.image;
|
|
91
|
+
job.resolve({ format: reply.format, width, height, levels });
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
job.reject(new Error(reply.error ?? 'etc2: the encode answered nothing'));
|
|
95
|
+
}
|
|
96
|
+
pump();
|
|
97
|
+
};
|
|
98
|
+
started.onerror = (event) => {
|
|
99
|
+
const message = event?.message;
|
|
100
|
+
stop(`the ETC2 worker failed (${typeof message === 'string' ? message : 'no message'}), so ` +
|
|
101
|
+
'BC textures stay RGBA on this device');
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
stop(`the ETC2 worker would not start (${error instanceof Error ? error.message : String(error)})` +
|
|
106
|
+
', so BC textures stay RGBA on this device');
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
get reason() {
|
|
110
|
+
return reason;
|
|
111
|
+
},
|
|
112
|
+
encode(image, srgb) {
|
|
113
|
+
if (worker === null)
|
|
114
|
+
return Promise.reject(new Error(reason));
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
waiting.push({ id: next++, image, srgb, resolve, reject });
|
|
117
|
+
pump();
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
dispose() {
|
|
121
|
+
stop('etc2: the encoder was disposed');
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -98,6 +98,11 @@ export { ddsBlocks } from './ddsBlocks.ts';
|
|
|
98
98
|
export { decodeBc } from './bcDecode.ts';
|
|
99
99
|
export { decodeBcImage } from './bcImage.ts';
|
|
100
100
|
export { flipBc5Green } from './bcFlip.ts';
|
|
101
|
+
export { readKtx2 } from './ktx2.ts';
|
|
102
|
+
export type { Ktx2Texture } from './ktx2.ts';
|
|
103
|
+
export { encodeEtc2 } from './etc2Encode.ts';
|
|
104
|
+
export type { Etc2Format } from './etc2Encode.ts';
|
|
105
|
+
export { encodeEtc2Chain } from './etc2Chain.ts';
|
|
101
106
|
export { buildClusters } from './bake/cluster.ts';
|
|
102
107
|
export type { ClusterOptions, ClusterSet } from './bake/cluster.ts';
|
|
103
108
|
export { buildClusterDag } from './bake/clusterLod.ts';
|