@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
package/README.md
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
# @driftengine/assets
|
|
2
2
|
|
|
3
|
-
Model readers
|
|
3
|
+
Model readers, the loader that streams a `.drft` file onto the screen, and the textures a phone
|
|
4
|
+
samples.
|
|
4
5
|
|
|
5
|
-
**Cost:
|
|
6
|
-
3% — the number is a fact about the build rather than a claim in a document.
|
|
6
|
+
**Cost: 27.6 KB gzipped on top of core.** Measured by `scripts/size-gate.test.mjs`, which fails if it
|
|
7
|
+
drifts more than 3% — the number is a fact about the build rather than a claim in a document.
|
|
8
|
+
|
|
9
|
+
- **Readers.** glTF and GLB, OBJ, STL, USD and 3MF, and experimentally FBX and Blender's `.blend`,
|
|
10
|
+
read from the file's own DNA. `readModel` picks the reader by extension and `MODEL_FORMATS` lists
|
|
11
|
+
what each one promises.
|
|
12
|
+
- **`DrftLoader`** streams a container nearest first: an outline, then parts within a frame budget,
|
|
13
|
+
then images decoded off the main thread, with progress that reaches ready only once the picture is
|
|
14
|
+
right. A BC texture goes up as its blocks wherever the device samples BC.
|
|
15
|
+
- **Phones.** On a device that samples ETC2 and not BC, a BC texture is shown decoded first and then
|
|
16
|
+
re-encoded as ETC2 or EAC in a worker and swapped in behind the handle it was drawn with — half a
|
|
17
|
+
byte a texel for colour, where the decoded image holds four. Pass `bcWorker: spawnBcWorker`, from
|
|
18
|
+
`@driftengine/assets/bcWorkers`; without a worker nothing is encoded and the textures stay RGBA.
|
|
19
|
+
The encode costs about 0.7 s for a 2048² image on a desktop processor, and several times that on a
|
|
20
|
+
phone's, on a core of its own.
|
|
21
|
+
- **Textures compressed offline.** `readKtx2` takes an uncompressed KTX2 file — ASTC, ETC2, EAC or
|
|
22
|
+
BC — as the blocks it carries, for `createSurfaceTexture`. `encodeEtc2Chain` makes an ETC2 or EAC
|
|
23
|
+
chain from pixels a caller holds; run it in a worker.
|
|
7
24
|
|
|
8
25
|
Part of [DriftEngine](../../README.md). See [`docs/ARCHITECTURE.md`](../../docs/ARCHITECTURE.md)
|
|
9
|
-
for how the packages divide, and
|
|
26
|
+
for how the packages divide, and [`docs/HANDBOOK.md`](../../docs/HANDBOOK.md) for working with
|
|
27
|
+
imported models.
|
package/dist/bcLoad.d.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import type { CompressedTextureFormat } from '@driftengine/core';
|
|
2
2
|
import type { BcImage } from '@driftengine/drft';
|
|
3
3
|
import type { BcRequest } from './bcAnswer.ts';
|
|
4
|
+
import type { Etc2Request } from './etc2Answer.ts';
|
|
4
5
|
export type { BcReply, BcRequest } from './bcAnswer.ts';
|
|
5
6
|
export { answerBcRequest } from './bcAnswer.ts';
|
|
6
7
|
/** What happens to one BC texture. */
|
|
7
8
|
export type BcPlan = 'blocks' | 'decode';
|
|
8
9
|
/** Blocks where this device takes the format in the colour space the slot reads, with a chain. */
|
|
9
10
|
export declare function bcPlan(image: BcImage, srgb: boolean, available: readonly CompressedTextureFormat[]): BcPlan;
|
|
10
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* The part of a worker the decoder and the ETC2 encoder use (`etc2Load.ts`), each starting one of
|
|
13
|
+
* its own from the same factory. A browser `Worker` satisfies it.
|
|
14
|
+
*/
|
|
11
15
|
export interface BcWorker {
|
|
12
|
-
postMessage(request: BcRequest, transfer?: Transferable[]): void;
|
|
16
|
+
postMessage(request: BcRequest | Etc2Request, transfer?: Transferable[]): void;
|
|
13
17
|
terminate(): void;
|
|
14
18
|
onmessage: ((event: {
|
|
15
19
|
data: unknown;
|
|
@@ -25,6 +29,7 @@ export interface BcDecoder {
|
|
|
25
29
|
}
|
|
26
30
|
/**
|
|
27
31
|
* A decoder over a worker `spawn` starts, or on the main thread where it is null — the default, since
|
|
28
|
-
* the worker's factory lives behind its own specifier (
|
|
32
|
+
* the worker's factory lives behind its own specifier (`@driftengine/assets/bcWorkers`) and a caller
|
|
33
|
+
* names it.
|
|
29
34
|
*/
|
|
30
35
|
export declare function createBcDecoder(spawn?: (() => BcWorker) | null): BcDecoder;
|
package/dist/bcLoad.js
CHANGED
|
@@ -38,7 +38,8 @@ function settle(reply, pending) {
|
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
40
|
* A decoder over a worker `spawn` starts, or on the main thread where it is null — the default, since
|
|
41
|
-
* the worker's factory lives behind its own specifier (
|
|
41
|
+
* the worker's factory lives behind its own specifier (`@driftengine/assets/bcWorkers`) and a caller
|
|
42
|
+
* names it.
|
|
42
43
|
*/
|
|
43
44
|
export function createBcDecoder(spawn = null) {
|
|
44
45
|
let worker = null;
|
|
@@ -56,7 +57,8 @@ export function createBcDecoder(spawn = null) {
|
|
|
56
57
|
if (spawn === null) {
|
|
57
58
|
reason =
|
|
58
59
|
'BC textures decode on the main thread: no worker was named. Pass `bcWorker: spawnBcWorker`, ' +
|
|
59
|
-
|
|
60
|
+
"exported by '@driftengine/assets/bcWorkers', to decode them off it — and, on a device " +
|
|
61
|
+
'that samples ETC2 and not BC, to re-encode them as ETC2 rather than hold them as RGBA';
|
|
60
62
|
}
|
|
61
63
|
else {
|
|
62
64
|
try {
|
package/dist/bcWorker.js
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The worker `bcLoad.ts`
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The worker `bcLoad.ts` and `etc2Load.ts` each start one of: decodes BC blocks off the main
|
|
3
|
+
* thread, or re-encodes them as an ETC2 chain, one request at a time, and hands each answer back
|
|
4
|
+
* rather than copying it. Imports the two answers alone, so its bundle is the codecs and nothing
|
|
5
|
+
* else.
|
|
5
6
|
*
|
|
6
7
|
* No top-level await, for the reason `islandWorker.ts` gives: a consumer's bundler compiles every
|
|
7
8
|
* `new Worker(new URL(…))` it finds, and an IIFE worker cannot carry one.
|
|
8
9
|
*/
|
|
9
10
|
import { answerBcRequest } from './bcAnswer.js';
|
|
11
|
+
import { answerEtc2Request } from './etc2Answer.js';
|
|
10
12
|
const scope = globalThis;
|
|
11
13
|
scope.onmessage = (event) => {
|
|
12
|
-
const
|
|
14
|
+
const request = event.data;
|
|
15
|
+
if ('kind' in request && request.kind === 'etc2') {
|
|
16
|
+
const reply = answerEtc2Request(request);
|
|
17
|
+
scope.postMessage(reply, reply.levels ?? []);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const reply = answerBcRequest(request);
|
|
13
21
|
scope.postMessage(reply, reply.rgba === undefined ? [] : [reply.rgba]);
|
|
14
22
|
};
|
package/dist/bcWorkers.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The BC decode
|
|
2
|
+
* The BC worker — the decode, and on a device that samples ETC2 and not BC the re-encode as ETC2
|
|
3
|
+
* (`etc2Load.ts`), each in a worker of its own from this factory — behind its own specifier, for
|
|
4
|
+
* the reason `physics/src/workers.ts` gives.
|
|
3
5
|
*
|
|
4
6
|
* **A bundler emits a worker before it decides the code reaching it is unreachable.** The loader's
|
|
5
7
|
* decoder built its worker with `new Worker(new URL('./bcWorker.ts', import.meta.url))` inside the
|
|
@@ -13,15 +15,21 @@
|
|
|
13
15
|
*
|
|
14
16
|
* ```ts
|
|
15
17
|
* import { DrftLoader } from '@driftengine/assets';
|
|
16
|
-
* import { spawnBcWorker } from '@driftengine/assets/
|
|
18
|
+
* import { spawnBcWorker } from '@driftengine/assets/bcWorkers';
|
|
17
19
|
*
|
|
18
20
|
* const loader = new DrftLoader(renderer, { bcWorker: spawnBcWorker });
|
|
19
21
|
* ```
|
|
20
22
|
*
|
|
23
|
+
* **A declared subpath, so it resolves the way the barrel does**: to the build by default and to
|
|
24
|
+
* this file under `drift-source`. The path into `src/` that 4.8.7 documented still resolves, through
|
|
25
|
+
* the manifest's `./*` passthrough, and hands every consumer TypeScript inside `node_modules`, which
|
|
26
|
+
* is the default the 2026-09-13 flip took away from the barrels; a resolver that maps a package's
|
|
27
|
+
* subpaths into `src/` also doubled it, to `src/src/bcWorkers.ts`.
|
|
28
|
+
*
|
|
21
29
|
* Without it BC textures decode on the main thread, and the loader says so once. **What that costs**
|
|
22
30
|
* is the decode of a texture the device cannot take as blocks, which on a phone is every BC texture:
|
|
23
31
|
* a 2048² BC7 image is 262,144 blocks.
|
|
24
32
|
*/
|
|
25
33
|
import type { BcWorker } from './bcLoad.ts';
|
|
26
|
-
/** A BC
|
|
34
|
+
/** A BC worker, as a bundler understands one. Throws where the runtime has no `Worker`. */
|
|
27
35
|
export declare function spawnBcWorker(): BcWorker;
|
package/dist/bcWorkers.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** A BC
|
|
1
|
+
/** A BC worker, as a bundler understands one. Throws where the runtime has no `Worker`. */
|
|
2
2
|
export function spawnBcWorker() {
|
|
3
3
|
// platform: browser default — `DrftLoaderOptions.bcWorker` is the seam a host supplies
|
|
4
4
|
return new Worker(new URL('./bcWorker.js', import.meta.url), {
|
package/dist/drftLoader.d.ts
CHANGED
|
@@ -51,9 +51,15 @@ export interface DrftLoaderOptions {
|
|
|
51
51
|
readonly fetchImpl?: FetchLike;
|
|
52
52
|
/**
|
|
53
53
|
* What starts the worker a BC texture the device cannot take as blocks is decoded in:
|
|
54
|
-
* `spawnBcWorker`, from `@driftengine/assets/
|
|
54
|
+
* `spawnBcWorker`, from `@driftengine/assets/bcWorkers`. Absent, they decode on the main
|
|
55
55
|
* thread and the loader says so once — the factory is behind its own specifier so a consumer that
|
|
56
56
|
* never names it has no worker in its build. See `bcWorkers.ts`.
|
|
57
|
+
*
|
|
58
|
+
* **On a device that samples ETC2 and not BC**, which is a phone, a second worker from the same
|
|
59
|
+
* factory then re-encodes each such texture as ETC2 or EAC and swaps the chain in behind the
|
|
60
|
+
* handle its decoded image went up under, at half a byte a texel, or a byte with alpha, where the
|
|
61
|
+
* image holds four. Nothing arrives later for it; see `etc2Load.ts` for what it costs. Without a worker
|
|
62
|
+
* the textures stay RGBA.
|
|
57
63
|
*/
|
|
58
64
|
readonly bcWorker?: () => BcWorker;
|
|
59
65
|
/**
|
|
@@ -276,6 +282,11 @@ export declare class DrftLoader {
|
|
|
276
282
|
/** Started at the first BC texture that has to be decoded, and only then. See `bcLoad.ts`. */
|
|
277
283
|
private bcDecoder;
|
|
278
284
|
private warnedBc;
|
|
285
|
+
/** Started at the first BC texture this device takes better as ETC2. See `etc2Load.ts`. */
|
|
286
|
+
private etc2Encoder;
|
|
287
|
+
private warnedEtc2;
|
|
288
|
+
/** Encoded chains whose image has not gone up yet, swapped in by `update` once it has. */
|
|
289
|
+
private readonly swaps;
|
|
279
290
|
private readonly chunks;
|
|
280
291
|
private materials;
|
|
281
292
|
/**
|
|
@@ -530,6 +541,20 @@ export declare class DrftLoader {
|
|
|
530
541
|
* Like every image, a BC texture fails on its own and its surfaces draw untextured.
|
|
531
542
|
*/
|
|
532
543
|
private acceptBc;
|
|
544
|
+
/**
|
|
545
|
+
* A BC texture just decoded for the picture, re-encoded as ETC2 or EAC where this device samples
|
|
546
|
+
* that and not BC, and swapped in behind its handle when the chain comes back — at once where the
|
|
547
|
+
* image has gone up, and from `update` where it is still queued. See `etc2Load.ts`.
|
|
548
|
+
*/
|
|
549
|
+
private encodeLater;
|
|
550
|
+
/**
|
|
551
|
+
* An encoded chain into the texture its image went up as: false where that texture is not made
|
|
552
|
+
* yet. **The decoded image is let go**, and the BC blocks it came from held in its place — a
|
|
553
|
+
* view over the file the loader keeps anyway — so a texture made again from them later decodes
|
|
554
|
+
* as it first did. A chain the texture refuses, where the slot came to read a colour space the
|
|
555
|
+
* blocks have no twin in, leaves the image where it is.
|
|
556
|
+
*/
|
|
557
|
+
private swapIn;
|
|
533
558
|
/**
|
|
534
559
|
* What goes to `createSurfaceTexture`: the image as it is, unless it is blocks this device cannot
|
|
535
560
|
* take in the colour space the slot reads — a material read after its texture arrived can change
|
package/dist/drftLoader.js
CHANGED
|
@@ -3,6 +3,7 @@ import { MeshBuilder, concatMeshes, placeMesh } from '@driftengine/core';
|
|
|
3
3
|
import { placeFields } from './fieldPlacement.js';
|
|
4
4
|
import { TextureSet, textureColorSpaces } from './drftTextures.js';
|
|
5
5
|
import { bcPlan, createBcDecoder } from './bcLoad.js';
|
|
6
|
+
import { createEtc2Encoder, etc2Plan } from './etc2Load.js';
|
|
6
7
|
import { decodeBc } from './bcDecode.js';
|
|
7
8
|
import { isCompressedSource, uploadsCompressed } from '@driftengine/core';
|
|
8
9
|
import { CODEC_BC, codecName, readBcPayload } from '@driftengine/drft';
|
|
@@ -106,6 +107,11 @@ export class DrftLoader {
|
|
|
106
107
|
/** Started at the first BC texture that has to be decoded, and only then. See `bcLoad.ts`. */
|
|
107
108
|
bcDecoder = null;
|
|
108
109
|
warnedBc = false;
|
|
110
|
+
/** Started at the first BC texture this device takes better as ETC2. See `etc2Load.ts`. */
|
|
111
|
+
etc2Encoder = null;
|
|
112
|
+
warnedEtc2 = false;
|
|
113
|
+
/** Encoded chains whose image has not gone up yet, swapped in by `update` once it has. */
|
|
114
|
+
swaps = [];
|
|
109
115
|
chunks = [];
|
|
110
116
|
materials = [];
|
|
111
117
|
/**
|
|
@@ -745,6 +751,18 @@ export class DrftLoader {
|
|
|
745
751
|
changed = true;
|
|
746
752
|
begun++;
|
|
747
753
|
}
|
|
754
|
+
/* An encoded chain whose image has gone up since it arrived; one a frame, as images go. */
|
|
755
|
+
if (this.swaps.length > 0 && mayBeginMore(begun, performance.now() - startedMs, msBudget)) {
|
|
756
|
+
for (let at = 0; at < this.swaps.length; at++) {
|
|
757
|
+
const swap = this.swaps[at];
|
|
758
|
+
if (!this.swapIn(swap.ordinal, swap.blocks, swap.source))
|
|
759
|
+
continue;
|
|
760
|
+
this.swaps.splice(at, 1);
|
|
761
|
+
changed = true;
|
|
762
|
+
begun++;
|
|
763
|
+
break;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
748
766
|
const revealSec = this.options.revealSec ?? DEFAULT_REVEAL_SEC;
|
|
749
767
|
for (const part of this.revealed) {
|
|
750
768
|
if (part.reveal >= 1)
|
|
@@ -802,6 +820,9 @@ export class DrftLoader {
|
|
|
802
820
|
this.regionStore.dispose();
|
|
803
821
|
this.bcDecoder?.dispose();
|
|
804
822
|
this.bcDecoder = null;
|
|
823
|
+
this.etc2Encoder?.dispose();
|
|
824
|
+
this.etc2Encoder = null;
|
|
825
|
+
this.swaps.length = 0;
|
|
805
826
|
/*
|
|
806
827
|
* Everything this class ever made, each disposed once.
|
|
807
828
|
*
|
|
@@ -1264,9 +1285,62 @@ export class DrftLoader {
|
|
|
1264
1285
|
}
|
|
1265
1286
|
const pixels = imageDataOf(rgba, image.width, image.height);
|
|
1266
1287
|
done(asImage ? await createImageBitmap(pixels, AS_AUTHORED) : pixels);
|
|
1288
|
+
if (!asImage)
|
|
1289
|
+
this.encodeLater(image, ordinal, srgb);
|
|
1267
1290
|
})
|
|
1268
1291
|
.catch(failed);
|
|
1269
1292
|
}
|
|
1293
|
+
/**
|
|
1294
|
+
* A BC texture just decoded for the picture, re-encoded as ETC2 or EAC where this device samples
|
|
1295
|
+
* that and not BC, and swapped in behind its handle when the chain comes back — at once where the
|
|
1296
|
+
* image has gone up, and from `update` where it is still queued. See `etc2Load.ts`.
|
|
1297
|
+
*/
|
|
1298
|
+
encodeLater(image, ordinal, srgb) {
|
|
1299
|
+
const spawn = this.options.bcWorker;
|
|
1300
|
+
if (spawn === undefined || !etc2Plan(image, srgb, this.renderer.compressedFormats))
|
|
1301
|
+
return;
|
|
1302
|
+
if (this.etc2Encoder === null)
|
|
1303
|
+
this.etc2Encoder = createEtc2Encoder(spawn);
|
|
1304
|
+
const encoder = this.etc2Encoder;
|
|
1305
|
+
encoder
|
|
1306
|
+
.encode(image, srgb)
|
|
1307
|
+
.then((blocks) => {
|
|
1308
|
+
if (this.disposed)
|
|
1309
|
+
return;
|
|
1310
|
+
if (!this.swapIn(ordinal, blocks, image))
|
|
1311
|
+
this.swaps.push({ ordinal, blocks, source: image });
|
|
1312
|
+
})
|
|
1313
|
+
.catch((error) => {
|
|
1314
|
+
/* Said once: the texture stays the image it already is, which is a cost and not a fault. */
|
|
1315
|
+
if (this.disposed || this.warnedEtc2)
|
|
1316
|
+
return;
|
|
1317
|
+
this.warnedEtc2 = true;
|
|
1318
|
+
console.warn(`DrftLoader: ${encoder.reason !== '' ? encoder.reason : `image ${ordinal} did not re-encode as ETC2 and stays RGBA`}`, error);
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* An encoded chain into the texture its image went up as: false where that texture is not made
|
|
1323
|
+
* yet. **The decoded image is let go**, and the BC blocks it came from held in its place — a
|
|
1324
|
+
* view over the file the loader keeps anyway — so a texture made again from them later decodes
|
|
1325
|
+
* as it first did. A chain the texture refuses, where the slot came to read a colour space the
|
|
1326
|
+
* blocks have no twin in, leaves the image where it is.
|
|
1327
|
+
*/
|
|
1328
|
+
swapIn(ordinal, blocks, source) {
|
|
1329
|
+
const held = this.textureSet?.at(ordinal) ?? null;
|
|
1330
|
+
if (held === null)
|
|
1331
|
+
return false;
|
|
1332
|
+
try {
|
|
1333
|
+
this.renderer.updateSurfaceTexture(held, blocks);
|
|
1334
|
+
this.images[ordinal] = source;
|
|
1335
|
+
}
|
|
1336
|
+
catch (error) {
|
|
1337
|
+
if (!this.warnedEtc2) {
|
|
1338
|
+
this.warnedEtc2 = true;
|
|
1339
|
+
console.warn(`DrftLoader: image ${ordinal} stays RGBA`, error);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
return true;
|
|
1343
|
+
}
|
|
1270
1344
|
/**
|
|
1271
1345
|
* What goes to `createSurfaceTexture`: the image as it is, unless it is blocks this device cannot
|
|
1272
1346
|
* take in the colour space the slot reads — a material read after its texture arrived can change
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The encode worker's whole job, as a function: one BC texture's level 0 decoded as a GPU samples
|
|
3
|
+
* it and made into an ETC2 or EAC chain (`etc2Chain.ts`), or the reason it could not be. Its own
|
|
4
|
+
* module beside `bcAnswer.ts`, which the same worker answers decode requests with.
|
|
5
|
+
*
|
|
6
|
+
* **The blocks cross, not the pixels**: the loader has already decoded this texture once, for the
|
|
7
|
+
* picture it shows while this runs, and hands its blocks over again rather than a copy of that
|
|
8
|
+
* image — a quarter to an eighth of the bytes, decoded a second time here at about a fifth of the
|
|
9
|
+
* encode's cost. An error is answered rather than thrown, so one bad texture fails on its own.
|
|
10
|
+
*/
|
|
11
|
+
import type { BcFormat } from '@driftengine/drft';
|
|
12
|
+
import type { Etc2Format } from './etc2Encode.ts';
|
|
13
|
+
/** What the loader asks the encode worker: one level's BC blocks, and how the texture is read. */
|
|
14
|
+
export interface Etc2Request {
|
|
15
|
+
readonly kind: 'etc2';
|
|
16
|
+
readonly id: number;
|
|
17
|
+
readonly format: BcFormat;
|
|
18
|
+
readonly width: number;
|
|
19
|
+
readonly height: number;
|
|
20
|
+
readonly blocks: ArrayBuffer;
|
|
21
|
+
/** Whether the slot reads colour, so the chain is averaged in light. */
|
|
22
|
+
readonly srgb: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** The answer: every level of the chain in one format, or why not. */
|
|
25
|
+
export interface Etc2Reply {
|
|
26
|
+
readonly id: number;
|
|
27
|
+
readonly format?: Etc2Format;
|
|
28
|
+
readonly levels?: ArrayBuffer[];
|
|
29
|
+
readonly error?: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function answerEtc2Request(request: Etc2Request): Etc2Reply;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { decodeBc } from './bcDecode.js';
|
|
2
|
+
import { encodeEtc2Chain, etc2FormatFor } from './etc2Chain.js';
|
|
3
|
+
export function answerEtc2Request(request) {
|
|
4
|
+
try {
|
|
5
|
+
const { width, height } = request;
|
|
6
|
+
const rgba = decodeBc(request.format, width, height, new Uint8Array(request.blocks));
|
|
7
|
+
const format = etc2FormatFor(request.format, rgba);
|
|
8
|
+
const colour = format === 'etc2-rgb8' || format === 'etc2-rgba8';
|
|
9
|
+
const levels = encodeEtc2Chain(format, rgba, width, height, colour && request.srgb);
|
|
10
|
+
return { id: request.id, format, levels: levels.map((level) => level.buffer) };
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
return { id: request.id, error: error instanceof Error ? error.message : String(error) };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A decoded texture as ETC2 or EAC blocks with its whole chain, down to 1x1: what the loader's
|
|
3
|
+
* encode worker makes of a BC texture on a device that samples ETC2 and not BC.
|
|
4
|
+
*
|
|
5
|
+
* **The format follows what the BC format stores**: BC4's one channel as `eac-r11` and BC5's two as
|
|
6
|
+
* `eac-rg11`, so a roughness or normal map keeps eleven bits a channel where colour would round it
|
|
7
|
+
* to a brightness step; every colour format as `etc2-rgb8`, or `etc2-rgba8` where any texel is less
|
|
8
|
+
* than opaque. **Opaque is 254 and above**, because a BC7 encoder storing an opaque image in its
|
|
9
|
+
* mode with a seven-bit alpha and a shared low bit can land every texel on 254, and that is no
|
|
10
|
+
* reason to spend twice the memory: what it gives up is an alpha of 254 read as 255, a 0.4% change
|
|
11
|
+
* no blend or cutout can show. **And what the choice gives up** is `etc2-rgb8a1`, half the size of
|
|
12
|
+
* `etc2-rgba8` for a cutout whose alpha is only ever 0 or 255, which needs ETC2's punch-through
|
|
13
|
+
* modes and this encoder writes none of them.
|
|
14
|
+
*
|
|
15
|
+
* **Each level is a box average of the one above, colour in linear light where the texture is
|
|
16
|
+
* colour**, as a GPU builds a chain for an sRGB image: averaged as stored, a fine bright detail on
|
|
17
|
+
* a dark ground fades darker with distance than it should. Alpha and data are averaged as stored.
|
|
18
|
+
* Allocates every level, once; this runs in a worker, once a texture.
|
|
19
|
+
*/
|
|
20
|
+
import type { BcFormat } from '@driftengine/drft';
|
|
21
|
+
import type { Etc2Format } from './etc2Encode.ts';
|
|
22
|
+
/** The ETC2 or EAC format a BC texture decoded to `rgba` becomes. */
|
|
23
|
+
export declare function etc2FormatFor(source: BcFormat, rgba: Uint8Array): Etc2Format;
|
|
24
|
+
/** `rgba` at `width` by `height`, and each level below it to 1x1, as `format` blocks. */
|
|
25
|
+
export declare function encodeEtc2Chain(format: Etc2Format, rgba: Uint8Array, width: number, height: number, srgb: boolean): Uint8Array[];
|
|
26
|
+
/**
|
|
27
|
+
* The next level down: each texel the average of the two by two above it, or of the one or two an
|
|
28
|
+
* odd edge leaves. Colour in linear light where `srgb`; alpha, and every channel of data, as stored.
|
|
29
|
+
*/
|
|
30
|
+
export declare function halve(rgba: Uint8Array, width: number, height: number, srgb: boolean): Uint8Array;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { encodeEtc2 } from './etc2Encode.js';
|
|
2
|
+
/** The ETC2 or EAC format a BC texture decoded to `rgba` becomes. */
|
|
3
|
+
export function etc2FormatFor(source, rgba) {
|
|
4
|
+
if (source === 'bc4')
|
|
5
|
+
return 'eac-r11';
|
|
6
|
+
if (source === 'bc5')
|
|
7
|
+
return 'eac-rg11';
|
|
8
|
+
for (let at = 3; at < rgba.length; at += 4) {
|
|
9
|
+
if (rgba[at] < 254)
|
|
10
|
+
return 'etc2-rgba8';
|
|
11
|
+
}
|
|
12
|
+
return 'etc2-rgb8';
|
|
13
|
+
}
|
|
14
|
+
/** `rgba` at `width` by `height`, and each level below it to 1x1, as `format` blocks. */
|
|
15
|
+
export function encodeEtc2Chain(format, rgba, width, height, srgb) {
|
|
16
|
+
const levels = [];
|
|
17
|
+
let level = rgba;
|
|
18
|
+
let w = width;
|
|
19
|
+
let h = height;
|
|
20
|
+
for (;;) {
|
|
21
|
+
levels.push(encodeEtc2(format, level, w, h));
|
|
22
|
+
if (w === 1 && h === 1)
|
|
23
|
+
return levels;
|
|
24
|
+
level = halve(level, w, h, srgb);
|
|
25
|
+
w = Math.max(1, w >> 1);
|
|
26
|
+
h = Math.max(1, h >> 1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Each eight-bit sRGB value in linear light. */
|
|
30
|
+
const LINEAR = new Float64Array(256);
|
|
31
|
+
for (let v = 0; v < 256; v++) {
|
|
32
|
+
const c = v / 255;
|
|
33
|
+
LINEAR[v] = c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
34
|
+
}
|
|
35
|
+
/** The eight-bit sRGB value whose light is nearest `light`, found in `LINEAR` and not by a power. */
|
|
36
|
+
function fromLinear(light) {
|
|
37
|
+
let lo = 0;
|
|
38
|
+
let hi = 255;
|
|
39
|
+
while (hi - lo > 1) {
|
|
40
|
+
const mid = (lo + hi) >> 1;
|
|
41
|
+
if (LINEAR[mid] <= light)
|
|
42
|
+
lo = mid;
|
|
43
|
+
else
|
|
44
|
+
hi = mid;
|
|
45
|
+
}
|
|
46
|
+
return light - LINEAR[lo] <= LINEAR[hi] - light ? lo : hi;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The next level down: each texel the average of the two by two above it, or of the one or two an
|
|
50
|
+
* odd edge leaves. Colour in linear light where `srgb`; alpha, and every channel of data, as stored.
|
|
51
|
+
*/
|
|
52
|
+
export function halve(rgba, width, height, srgb) {
|
|
53
|
+
const w = Math.max(1, width >> 1);
|
|
54
|
+
const h = Math.max(1, height >> 1);
|
|
55
|
+
const out = new Uint8Array(w * h * 4);
|
|
56
|
+
for (let y = 0; y < h; y++) {
|
|
57
|
+
const y0 = Math.min(y * 2, height - 1);
|
|
58
|
+
const y1 = Math.min(y * 2 + 1, height - 1);
|
|
59
|
+
for (let x = 0; x < w; x++) {
|
|
60
|
+
const x0 = Math.min(x * 2, width - 1);
|
|
61
|
+
const x1 = Math.min(x * 2 + 1, width - 1);
|
|
62
|
+
const a = (y0 * width + x0) * 4;
|
|
63
|
+
const b = (y0 * width + x1) * 4;
|
|
64
|
+
const c = (y1 * width + x0) * 4;
|
|
65
|
+
const d = (y1 * width + x1) * 4;
|
|
66
|
+
const to = (y * w + x) * 4;
|
|
67
|
+
for (let channel = 0; channel < 4; channel++) {
|
|
68
|
+
const pa = rgba[a + channel];
|
|
69
|
+
const pb = rgba[b + channel];
|
|
70
|
+
const pc = rgba[c + channel];
|
|
71
|
+
const pd = rgba[d + channel];
|
|
72
|
+
out[to + channel] =
|
|
73
|
+
srgb && channel < 3
|
|
74
|
+
? fromLinear((LINEAR[pa] +
|
|
75
|
+
LINEAR[pb] +
|
|
76
|
+
LINEAR[pc] +
|
|
77
|
+
LINEAR[pd]) /
|
|
78
|
+
4)
|
|
79
|
+
: (pa + pb + pc + pd + 2) >> 2;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
/** The formats this encodes: colour, colour with alpha, and one or two channels of data. */
|
|
25
|
+
export type Etc2Format = 'etc2-rgb8' | 'etc2-rgba8' | 'eac-r11' | 'eac-rg11';
|
|
26
|
+
/** `rgba` at `width` by `height` as `format` blocks, row of blocks after row of blocks. */
|
|
27
|
+
export declare function encodeEtc2(format: Etc2Format, rgba: Uint8Array, width: number, height: number): Uint8Array;
|
|
28
|
+
/**
|
|
29
|
+
* One level of a block back to RGBA, for the modes this encoder writes and EAC: what a device
|
|
30
|
+
* decodes, stated here so the encoder can be held to a number. A block in T, H or planar mode is
|
|
31
|
+
* refused by name, since nothing here writes one.
|
|
32
|
+
*/
|
|
33
|
+
export declare function decodeEtc2(format: Etc2Format, blocks: Uint8Array, width: number, height: number): Uint8Array;
|