@driftengine/assets 4.8.3 → 4.8.5
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 +1 -1
- package/dist/bc7Decode.d.ts +33 -0
- package/dist/bc7Decode.js +197 -0
- package/dist/bcAnswer.d.ts +28 -0
- package/dist/bcAnswer.js +15 -0
- package/dist/bcDecode.d.ts +28 -0
- package/dist/bcDecode.js +175 -0
- package/dist/bcFlip.d.ts +21 -0
- package/dist/bcFlip.js +52 -0
- package/dist/bcImage.d.ts +15 -0
- package/dist/bcImage.js +38 -0
- package/dist/bcLoad.d.ts +26 -0
- package/dist/bcLoad.js +113 -0
- package/dist/bcWorker.d.ts +1 -0
- package/dist/bcWorker.js +14 -0
- package/dist/dds.d.ts +16 -18
- package/dist/dds.js +20 -293
- package/dist/ddsBlocks.d.ts +3 -0
- package/dist/ddsBlocks.js +54 -0
- package/dist/ddsHeader.d.ts +32 -0
- package/dist/ddsHeader.js +119 -0
- package/dist/drftLoader.d.ts +18 -0
- package/dist/drftLoader.js +98 -8
- package/dist/fbx.js +32 -13
- package/dist/fbxRig.d.ts +0 -18
- package/dist/fbxRig.js +0 -37
- package/dist/gltf.js +74 -12
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -2
- package/dist/influences.d.ts +40 -0
- package/dist/influences.js +53 -0
- package/dist/weld.js +3 -0
- package/package.json +4 -4
- package/src/bc7Decode.ts +237 -0
- package/src/bcAnswer.ts +45 -0
- package/src/bcDecode.ts +218 -0
- package/src/bcFlip.ts +55 -0
- package/src/bcImage.ts +58 -0
- package/src/bcLoad.ts +154 -0
- package/src/bcWorker.ts +20 -0
- package/src/dds.ts +21 -337
- package/src/ddsBlocks.ts +63 -0
- package/src/ddsHeader.ts +160 -0
- package/src/drftLoader.ts +115 -9
- package/src/fbx.ts +34 -18
- package/src/fbxRig.ts +0 -41
- package/src/gltf.ts +75 -13
- package/src/index.ts +7 -2
- package/src/influences.ts +77 -0
- package/src/weld.ts +3 -0
package/dist/bcLoad.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BC textures as the loader receives them: which go up as their blocks, and the decode of the rest
|
|
3
|
+
* off the main thread.
|
|
4
|
+
*
|
|
5
|
+
* **Blocks where the device takes them**, through `uploadsCompressed`, and only with a chain: no
|
|
6
|
+
* chain can be generated for blocks on the device, and a large texture with no mips crawls at a
|
|
7
|
+
* distance, so a single stored level is decoded and given generated mips instead. **Decoded
|
|
8
|
+
* otherwise**, which on a phone is every BC texture: in a worker, because a 2048² BC7 image is
|
|
9
|
+
* 262,144 blocks and a phone's main thread spends the better part of a second on them.
|
|
10
|
+
*
|
|
11
|
+
* **Never silently slower.** Where there is no `Worker`, or the one this module starts fails to
|
|
12
|
+
* load — a bundler that does not understand `new Worker(new URL(…))` — the decode moves to the main
|
|
13
|
+
* thread and `reason` says so, and the loader prints it once. A request in flight when the worker
|
|
14
|
+
* fails is decoded again there rather than left waiting.
|
|
15
|
+
*/
|
|
16
|
+
import { uploadsCompressed } from '@driftengine/core';
|
|
17
|
+
import { answerBcRequest } from './bcAnswer.js';
|
|
18
|
+
export { answerBcRequest } from './bcAnswer.js';
|
|
19
|
+
/** Blocks where this device takes the format in the colour space the slot reads, with a chain. */
|
|
20
|
+
export function bcPlan(image, srgb, available) {
|
|
21
|
+
const chained = image.levels.length > 1 || Math.max(image.width, image.height) <= 4;
|
|
22
|
+
return chained && uploadsCompressed(image.format, srgb, image.width, image.height, available)
|
|
23
|
+
? 'blocks'
|
|
24
|
+
: 'decode';
|
|
25
|
+
}
|
|
26
|
+
function request(id, image, asImage) {
|
|
27
|
+
/* A copy, so the transfer detaches nothing the loader still holds: a level is a view over the
|
|
28
|
+
fetched file, and every other view over that file would read as empty afterwards. */
|
|
29
|
+
const blocks = image.levels[0].slice().buffer;
|
|
30
|
+
return { id, format: image.format, width: image.width, height: image.height, blocks, asImage };
|
|
31
|
+
}
|
|
32
|
+
function settle(reply, pending) {
|
|
33
|
+
if (reply.rgba !== undefined)
|
|
34
|
+
pending.resolve(new Uint8Array(reply.rgba));
|
|
35
|
+
else
|
|
36
|
+
pending.reject(new Error(reply.error ?? 'bc: the decode answered nothing'));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The worker this package ships, built the way a bundler understands, or null where there is no
|
|
40
|
+
* `Worker` at all. `createBcDecoder`'s `spawn` is the way past either.
|
|
41
|
+
*/
|
|
42
|
+
const defaultSpawn =
|
|
43
|
+
// platform: feature probe — no `Worker` means BC decodes on the main thread, with a reason
|
|
44
|
+
typeof Worker === 'undefined'
|
|
45
|
+
? null
|
|
46
|
+
: () =>
|
|
47
|
+
// platform: browser default — `createBcDecoder(spawn)` is the seam a host supplies
|
|
48
|
+
new Worker(new URL('./bcWorker.js', import.meta.url), {
|
|
49
|
+
type: 'module',
|
|
50
|
+
});
|
|
51
|
+
export function createBcDecoder(spawn = defaultSpawn) {
|
|
52
|
+
let worker = null;
|
|
53
|
+
let reason = '';
|
|
54
|
+
const pending = new Map();
|
|
55
|
+
let next = 0;
|
|
56
|
+
const onMainThread = (why) => {
|
|
57
|
+
reason = `BC textures decode on the main thread: ${why}`;
|
|
58
|
+
worker?.terminate();
|
|
59
|
+
worker = null;
|
|
60
|
+
for (const [id, waiting] of pending)
|
|
61
|
+
settle(answerBcRequest(request(id, waiting.image, waiting.asImage)), waiting);
|
|
62
|
+
pending.clear();
|
|
63
|
+
};
|
|
64
|
+
if (spawn === null) {
|
|
65
|
+
reason = 'BC textures decode on the main thread: this runtime has no Worker';
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
try {
|
|
69
|
+
worker = spawn();
|
|
70
|
+
worker.onmessage = (event) => {
|
|
71
|
+
const reply = event.data;
|
|
72
|
+
const waiting = pending.get(reply.id);
|
|
73
|
+
if (waiting === undefined)
|
|
74
|
+
return;
|
|
75
|
+
pending.delete(reply.id);
|
|
76
|
+
settle(reply, waiting);
|
|
77
|
+
};
|
|
78
|
+
worker.onerror = (event) => {
|
|
79
|
+
const message = event?.message;
|
|
80
|
+
onMainThread(`the worker failed (${typeof message === 'string' ? message : 'no message'})`);
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
onMainThread(`the worker would not start (${error instanceof Error ? error.message : String(error)})`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
get reason() {
|
|
89
|
+
return reason;
|
|
90
|
+
},
|
|
91
|
+
decode(image, asImage) {
|
|
92
|
+
const id = next++;
|
|
93
|
+
const target = worker;
|
|
94
|
+
if (target === null) {
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
settle(answerBcRequest(request(id, image, asImage)), { image, asImage, resolve, reject });
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
pending.set(id, { image, asImage, resolve, reject });
|
|
101
|
+
const message = request(id, image, asImage);
|
|
102
|
+
target.postMessage(message, [message.blocks]);
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
dispose() {
|
|
106
|
+
worker?.terminate();
|
|
107
|
+
worker = null;
|
|
108
|
+
for (const waiting of pending.values())
|
|
109
|
+
waiting.reject(new Error('bc: the decoder was disposed'));
|
|
110
|
+
pending.clear();
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/bcWorker.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The worker `bcLoad.ts` starts: decodes BC blocks off the main thread, one request at a time, and
|
|
3
|
+
* hands each image back rather than copying it. Imports `bcAnswer.ts` alone, so its bundle is the
|
|
4
|
+
* decoders and nothing else.
|
|
5
|
+
*
|
|
6
|
+
* No top-level await, for the reason `islandWorker.ts` gives: a consumer's bundler compiles every
|
|
7
|
+
* `new Worker(new URL(…))` it finds, and an IIFE worker cannot carry one.
|
|
8
|
+
*/
|
|
9
|
+
import { answerBcRequest } from './bcAnswer.js';
|
|
10
|
+
const scope = globalThis;
|
|
11
|
+
scope.onmessage = (event) => {
|
|
12
|
+
const reply = answerBcRequest(event.data);
|
|
13
|
+
scope.postMessage(reply, reply.rgba === undefined ? [] : [reply.rgba]);
|
|
14
|
+
};
|
package/dist/dds.d.ts
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DDS surfaces to RGBA: BC1
|
|
3
|
-
*
|
|
2
|
+
* DDS surfaces to RGBA: BC1 to BC5 and BC7 in either header spelling, and the uncompressed layouts,
|
|
3
|
+
* by the masks their own header carries.
|
|
4
4
|
*
|
|
5
5
|
* **It exists because a third of a shipped vehicle's textures are in it.** The car this was built
|
|
6
6
|
* against carries 81 textures and 34 of them are DDS — 25 DXT5 and 9 DXT1 — which are its paint,
|
|
7
7
|
* its interior and most of its normal maps. `imageInfo.ts` identifies PNG, JPEG and WEBP, so
|
|
8
8
|
* without this the model imports correctly shaped and visibly half-painted.
|
|
9
9
|
*
|
|
10
|
-
* **
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* **Only the mips are dropped, deliberately.** A DDS carries a full chain and the container
|
|
16
|
-
* generates its own, so decoding level 0 and discarding the rest is not a loss — it is declining
|
|
17
|
-
* to carry two answers to the same question.
|
|
10
|
+
* **Since 4.8.4 the baker keeps a block-compressed DDS as blocks**, through `ddsBlocks.ts`, and this
|
|
11
|
+
* is the path for what it cannot keep — an uncompressed surface, a chain it refuses — and for a bake
|
|
12
|
+
* asked to decode. It decodes level 0 only: the container generates its own chain for an image it
|
|
13
|
+
* did not receive one for.
|
|
18
14
|
*
|
|
19
15
|
* **Both header spellings, as of 3.30.2.** A `DX10` header names the format in a 20-byte extension
|
|
20
16
|
* rather than in the four characters at byte 84, and the blocks behind it are the same blocks. This
|
|
@@ -23,19 +19,21 @@
|
|
|
23
19
|
* as a *lighting* bug rather than as a missing texture: their baker substitutes a 1x1 white pixel,
|
|
24
20
|
* and white in an ORM map is roughness 1 **and** metallic 1, which in the flat shader leaves no
|
|
25
21
|
* diffuse and no sun term at all. The bodywork became a blurred mirror of the field it was parked
|
|
26
|
-
* in, and the report said so in those words.
|
|
27
|
-
*
|
|
28
|
-
* What it refuses, by name: BC7, BC6H, BC4, signed BC5, cube maps, arrays and volumes. A format
|
|
29
|
-
* identified and declined is worth more than one half-decoded, which is `recognise.ts`'s principle
|
|
30
|
-
* applied one level down — and the last three of those are refused *because* a classic header has
|
|
31
|
-
* no way to state them, so accepting one would answer with a picture instead of an error.
|
|
22
|
+
* in, and the report said so in those words. The header is `ddsHeader.ts`'s now, shared with the
|
|
23
|
+
* reader that keeps the blocks.
|
|
32
24
|
*/
|
|
33
|
-
|
|
34
|
-
export declare function isDds(bytes: Uint8Array): boolean;
|
|
25
|
+
export { isDds } from './ddsHeader.ts';
|
|
35
26
|
/** What a decoded surface is: level 0, RGBA, eight bits a channel. */
|
|
36
27
|
export interface DdsImage {
|
|
37
28
|
readonly width: number;
|
|
38
29
|
readonly height: number;
|
|
39
30
|
readonly rgba: Uint8Array;
|
|
40
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Decode a DDS surface's level 0 into RGBA.
|
|
34
|
+
*
|
|
35
|
+
* A surface whose dimensions are not multiples of four is decoded block by block and cropped, which
|
|
36
|
+
* is what the format itself does: the blocks cover a padded rectangle and the texels outside the
|
|
37
|
+
* declared size are padding rather than image.
|
|
38
|
+
*/
|
|
41
39
|
export declare function ddsToRgba(bytes: Uint8Array): DdsImage;
|
package/dist/dds.js
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DDS surfaces to RGBA: BC1
|
|
3
|
-
*
|
|
2
|
+
* DDS surfaces to RGBA: BC1 to BC5 and BC7 in either header spelling, and the uncompressed layouts,
|
|
3
|
+
* by the masks their own header carries.
|
|
4
4
|
*
|
|
5
5
|
* **It exists because a third of a shipped vehicle's textures are in it.** The car this was built
|
|
6
6
|
* against carries 81 textures and 34 of them are DDS — 25 DXT5 and 9 DXT1 — which are its paint,
|
|
7
7
|
* its interior and most of its normal maps. `imageInfo.ts` identifies PNG, JPEG and WEBP, so
|
|
8
8
|
* without this the model imports correctly shaped and visibly half-painted.
|
|
9
9
|
*
|
|
10
|
-
* **
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* **Only the mips are dropped, deliberately.** A DDS carries a full chain and the container
|
|
16
|
-
* generates its own, so decoding level 0 and discarding the rest is not a loss — it is declining
|
|
17
|
-
* to carry two answers to the same question.
|
|
10
|
+
* **Since 4.8.4 the baker keeps a block-compressed DDS as blocks**, through `ddsBlocks.ts`, and this
|
|
11
|
+
* is the path for what it cannot keep — an uncompressed surface, a chain it refuses — and for a bake
|
|
12
|
+
* asked to decode. It decodes level 0 only: the container generates its own chain for an image it
|
|
13
|
+
* did not receive one for.
|
|
18
14
|
*
|
|
19
15
|
* **Both header spellings, as of 3.30.2.** A `DX10` header names the format in a 20-byte extension
|
|
20
16
|
* rather than in the four characters at byte 84, and the blocks behind it are the same blocks. This
|
|
@@ -23,189 +19,13 @@
|
|
|
23
19
|
* as a *lighting* bug rather than as a missing texture: their baker substitutes a 1x1 white pixel,
|
|
24
20
|
* and white in an ORM map is roughness 1 **and** metallic 1, which in the flat shader leaves no
|
|
25
21
|
* diffuse and no sun term at all. The bodywork became a blurred mirror of the field it was parked
|
|
26
|
-
* in, and the report said so in those words.
|
|
27
|
-
*
|
|
28
|
-
* What it refuses, by name: BC7, BC6H, BC4, signed BC5, cube maps, arrays and volumes. A format
|
|
29
|
-
* identified and declined is worth more than one half-decoded, which is `recognise.ts`'s principle
|
|
30
|
-
* applied one level down — and the last three of those are refused *because* a classic header has
|
|
31
|
-
* no way to state them, so accepting one would answer with a picture instead of an error.
|
|
32
|
-
*/
|
|
33
|
-
import { DrftError } from '@driftengine/drft';
|
|
34
|
-
const HEADER_BYTES = 128;
|
|
35
|
-
/** `DDS_HEADER_DXT10`: the extension a `DX10` FourCC puts between the header and the surface. */
|
|
36
|
-
const DX10_EXTENSION_BYTES = 20;
|
|
37
|
-
/** `DDPF_FOURCC`: the pixel format names a compression rather than describing channels. */
|
|
38
|
-
const DDPF_FOURCC = 0x4;
|
|
39
|
-
/** Whether these bytes are a DDS at all. The magic, and nothing inferred from a file name. */
|
|
40
|
-
export function isDds(bytes) {
|
|
41
|
-
return (bytes.length >= 4 &&
|
|
42
|
-
bytes[0] === 0x44 &&
|
|
43
|
-
bytes[1] === 0x44 &&
|
|
44
|
-
bytes[2] === 0x53 &&
|
|
45
|
-
bytes[3] === 0x20);
|
|
46
|
-
}
|
|
47
|
-
/** RGB565 to three bytes, with the low bits replicated so white stays white. */
|
|
48
|
-
function expand565(value) {
|
|
49
|
-
const r = (value >> 11) & 0x1f;
|
|
50
|
-
const g = (value >> 5) & 0x3f;
|
|
51
|
-
const b = value & 0x1f;
|
|
52
|
-
return [(r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2)];
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* One BC1 colour block into a 16-entry RGBA palette lookup.
|
|
56
|
-
*
|
|
57
|
-
* **The endpoint comparison chooses the mode and is the thing to get right.** With `c0 > c1` the
|
|
58
|
-
* block has four opaque colours; otherwise it has three and its fourth index is transparent black.
|
|
59
|
-
* A decoder that always takes the first branch produces an image that looks correct until
|
|
60
|
-
* something relies on the punch-through alpha, which for foliage and grilles is everything.
|
|
61
|
-
*/
|
|
62
|
-
function decodeColourBlock(bytes, at, out,
|
|
63
|
-
/**
|
|
64
|
-
* Whether the block may take BC1's three-colour mode.
|
|
65
|
-
*
|
|
66
|
-
* **False for BC2 and BC3, whatever the endpoint order, and that is the correction of a real
|
|
67
|
-
* defect.** Those two carry alpha in their own half of the block, so there is no index left over
|
|
68
|
-
* to mean transparent and the colour half is always the four-colour opaque mode. This function
|
|
69
|
-
* was reached unconditionally, so a BC3 block written with `c0 <= c1` decoded its fourth index to
|
|
70
|
-
* transparent black instead of to the interpolant — invisible in every test here and in most real
|
|
71
|
-
* data, because an encoder that has no reason to order the endpoints the low way usually does not.
|
|
72
|
-
* Found while adding BC2, whose specification says the same thing about the same bytes.
|
|
73
|
-
*/
|
|
74
|
-
punchthrough) {
|
|
75
|
-
const c0 = bytes[at] | (bytes[at + 1] << 8);
|
|
76
|
-
const c1 = bytes[at + 2] | (bytes[at + 3] << 8);
|
|
77
|
-
const [r0, g0, b0] = expand565(c0);
|
|
78
|
-
const [r1, g1, b1] = expand565(c1);
|
|
79
|
-
const palette = new Uint8Array(16);
|
|
80
|
-
palette.set([r0, g0, b0, 255], 0);
|
|
81
|
-
palette.set([r1, g1, b1, 255], 4);
|
|
82
|
-
if (c0 > c1 || !punchthrough) {
|
|
83
|
-
palette.set([
|
|
84
|
-
Math.round((2 * r0 + r1) / 3),
|
|
85
|
-
Math.round((2 * g0 + g1) / 3),
|
|
86
|
-
Math.round((2 * b0 + b1) / 3),
|
|
87
|
-
255,
|
|
88
|
-
Math.round((r0 + 2 * r1) / 3),
|
|
89
|
-
Math.round((g0 + 2 * g1) / 3),
|
|
90
|
-
Math.round((b0 + 2 * b1) / 3),
|
|
91
|
-
255,
|
|
92
|
-
], 8);
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
palette.set([
|
|
96
|
-
Math.round((r0 + r1) / 2),
|
|
97
|
-
Math.round((g0 + g1) / 2),
|
|
98
|
-
Math.round((b0 + b1) / 2),
|
|
99
|
-
255,
|
|
100
|
-
0,
|
|
101
|
-
0,
|
|
102
|
-
0,
|
|
103
|
-
0,
|
|
104
|
-
], 8);
|
|
105
|
-
}
|
|
106
|
-
const indices = bytes[at + 4] | (bytes[at + 5] << 8) | (bytes[at + 6] << 16) | (bytes[at + 7] << 24);
|
|
107
|
-
for (let texel = 0; texel < 16; texel++) {
|
|
108
|
-
const index = (indices >>> (texel * 2)) & 0x3;
|
|
109
|
-
out[texel * 4] = palette[index * 4];
|
|
110
|
-
out[texel * 4 + 1] = palette[index * 4 + 1];
|
|
111
|
-
out[texel * 4 + 2] = palette[index * 4 + 2];
|
|
112
|
-
out[texel * 4 + 3] = palette[index * 4 + 3];
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* One BC4-style eight-value block, which is what BC3's alpha and both halves of BC5 are.
|
|
117
|
-
*
|
|
118
|
-
* The same endpoint comparison as the colour block, and the same consequence for getting it
|
|
119
|
-
* backwards: with `a0 > a1` there are six interpolants, and otherwise four plus a hard 0 and 255.
|
|
120
|
-
*/
|
|
121
|
-
function decodeAlphaBlock(bytes, at, out, stride, offset) {
|
|
122
|
-
const a0 = bytes[at];
|
|
123
|
-
const a1 = bytes[at + 1];
|
|
124
|
-
const values = new Uint8Array(8);
|
|
125
|
-
values[0] = a0;
|
|
126
|
-
values[1] = a1;
|
|
127
|
-
if (a0 > a1) {
|
|
128
|
-
for (let i = 1; i <= 6; i++)
|
|
129
|
-
values[i + 1] = Math.round(((7 - i) * a0 + i * a1) / 7);
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
for (let i = 1; i <= 4; i++)
|
|
133
|
-
values[i + 1] = Math.round(((5 - i) * a0 + i * a1) / 5);
|
|
134
|
-
values[6] = 0;
|
|
135
|
-
values[7] = 255;
|
|
136
|
-
}
|
|
137
|
-
/* Six bytes of 3-bit indices, least significant first, as one 48-bit run. */
|
|
138
|
-
let low = bytes[at + 2] | (bytes[at + 3] << 8) | (bytes[at + 4] << 16);
|
|
139
|
-
let high = bytes[at + 5] | (bytes[at + 6] << 8) | (bytes[at + 7] << 16);
|
|
140
|
-
for (let texel = 0; texel < 8; texel++) {
|
|
141
|
-
out[texel * stride + offset] = values[low & 0x7];
|
|
142
|
-
low >>>= 3;
|
|
143
|
-
}
|
|
144
|
-
for (let texel = 8; texel < 16; texel++) {
|
|
145
|
-
out[texel * stride + offset] = values[high & 0x7];
|
|
146
|
-
high >>>= 3;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* BC2's alpha: eight bytes of explicit four-bit values, one nibble a texel, in row order.
|
|
151
|
-
*
|
|
152
|
-
* **No interpolation and no mode**, which is the whole difference from BC3 and the reason this is
|
|
153
|
-
* four lines rather than the twenty above. Sixteen texels, low nibble of each byte first.
|
|
154
|
-
*
|
|
155
|
-
* The nibble is scaled by *replication* rather than by a shift: `0xF` has to come out 255 and not
|
|
156
|
-
* 240, or a texel the author wrote as fully opaque arrives at 94% and every alpha test in the
|
|
157
|
-
* pipeline sees a different picture from the one that was exported.
|
|
158
|
-
*/
|
|
159
|
-
function decodeExplicitAlpha(bytes, at, out) {
|
|
160
|
-
for (let texel = 0; texel < 16; texel++) {
|
|
161
|
-
const byte = bytes[at + (texel >> 1)];
|
|
162
|
-
const nibble = texel % 2 === 0 ? byte & 0xf : byte >> 4;
|
|
163
|
-
out[texel * 4 + 3] = nibble * 17;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* The DXGI block formats, as the same blocks this reader already decodes.
|
|
168
|
-
*
|
|
169
|
-
* **A `DX10` header states the format in a 20-byte extension instead of in the four characters at
|
|
170
|
-
* byte 84, and that is the whole of the difference.** BC3 announced as `DXGI_FORMAT_BC3_UNORM_SRGB`
|
|
171
|
-
* is bit for bit the BC3 announced as `DXT5`: the same blocks, the same mip chain, and sRGB is a
|
|
172
|
-
* statement about how the values are *read* rather than about how they are stored. Measured from
|
|
173
|
-
* outside on one bundle's paint texture — 5,592,580 bytes against 5,592,560 for the classic
|
|
174
|
-
* spelling of the same 2048² chain, which is 148 + 5,592,432 against 128 + 5,592,432.
|
|
175
|
-
*
|
|
176
|
-
* The typeless and sRGB spellings sit beside the plain `UNORM` ones for that reason.
|
|
177
|
-
*
|
|
178
|
-
* **What is deliberately absent.** BC4 (79–81), which this reader does not decode at all; and
|
|
179
|
-
* `BC5_SNORM` (84), whose values are signed — mapping it would decode to wrong numbers rather than
|
|
180
|
-
* to an error, which is the one failure this file is written to avoid.
|
|
181
|
-
*/
|
|
182
|
-
const DXGI_BLOCK = {
|
|
183
|
-
70: 'bc1',
|
|
184
|
-
71: 'bc1',
|
|
185
|
-
72: 'bc1',
|
|
186
|
-
73: 'bc2',
|
|
187
|
-
74: 'bc2',
|
|
188
|
-
75: 'bc2',
|
|
189
|
-
76: 'bc3',
|
|
190
|
-
77: 'bc3',
|
|
191
|
-
78: 'bc3',
|
|
192
|
-
82: 'bc5',
|
|
193
|
-
83: 'bc5',
|
|
194
|
-
};
|
|
195
|
-
const FOURCC_BLOCK = {
|
|
196
|
-
DXT1: 'bc1',
|
|
197
|
-
DXT3: 'bc2',
|
|
198
|
-
DXT5: 'bc3',
|
|
199
|
-
ATI2: 'bc5',
|
|
200
|
-
BC5U: 'bc5',
|
|
201
|
-
};
|
|
202
|
-
/**
|
|
203
|
-
* Decode a DDS surface's level 0 into RGBA.
|
|
204
|
-
*
|
|
205
|
-
* A surface whose dimensions are not multiples of four is decoded block by block and cropped, which
|
|
206
|
-
* is what the format itself does: the blocks cover a padded rectangle and the texels outside the
|
|
207
|
-
* declared size are padding rather than image.
|
|
22
|
+
* in, and the report said so in those words. The header is `ddsHeader.ts`'s now, shared with the
|
|
23
|
+
* reader that keeps the blocks.
|
|
208
24
|
*/
|
|
25
|
+
import { DrftError, BC_BLOCK_BYTES } from '@driftengine/drft';
|
|
26
|
+
import { decodeBcImage } from './bcImage.js';
|
|
27
|
+
import { DDPF_FOURCC, HEADER_BYTES, isDds, readDdsSurface } from './ddsHeader.js';
|
|
28
|
+
export { isDds } from './ddsHeader.js';
|
|
209
29
|
/** `DDPF_ALPHAPIXELS`, `DDPF_RGB`, `DDPF_LUMINANCE`: what the pixel format says it describes. */
|
|
210
30
|
const DDPF_ALPHAPIXELS = 0x1;
|
|
211
31
|
const DDPF_RGB = 0x40;
|
|
@@ -295,53 +115,12 @@ function uncompressedToRgba(bytes, view, width, height, flags) {
|
|
|
295
115
|
return { width, height, rgba };
|
|
296
116
|
}
|
|
297
117
|
/**
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
* **Two spellings of one thing.** A classic header names the format in the four characters at byte
|
|
301
|
-
* 84 and the surface follows at 128; a `DX10` header puts `DX10` there instead and states the real
|
|
302
|
-
* format in a 20-byte extension, so the surface starts at 148. Nothing else differs — the blocks
|
|
303
|
-
* are the same blocks — which is why this resolves both to one name and one offset rather than
|
|
304
|
-
* growing a second decoder.
|
|
118
|
+
* Decode a DDS surface's level 0 into RGBA.
|
|
305
119
|
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
* to be a single surface — a picture rather than an error.
|
|
120
|
+
* A surface whose dimensions are not multiples of four is decoded block by block and cropped, which
|
|
121
|
+
* is what the format itself does: the blocks cover a padded rectangle and the texels outside the
|
|
122
|
+
* declared size are padding rather than image.
|
|
310
123
|
*/
|
|
311
|
-
function resolveSurface(bytes, view, fourcc) {
|
|
312
|
-
if (fourcc !== 'DX10') {
|
|
313
|
-
const format = FOURCC_BLOCK[fourcc];
|
|
314
|
-
if (format === undefined) {
|
|
315
|
-
throw new DrftError(`dds: "${fourcc}" is not a format this reader decodes. It reads BC1 (DXT1), BC2 (DXT3), ` +
|
|
316
|
-
'BC3 (DXT5) and BC5 (ATI2). Convert the texture to PNG, or re-export it as one of those.');
|
|
317
|
-
}
|
|
318
|
-
return { format, dataAt: HEADER_BYTES };
|
|
319
|
-
}
|
|
320
|
-
const dataAt = HEADER_BYTES + DX10_EXTENSION_BYTES;
|
|
321
|
-
if (bytes.length < dataAt) {
|
|
322
|
-
throw new DrftError('dds: a DX10 header is declared and the twenty-byte extension is not there');
|
|
323
|
-
}
|
|
324
|
-
const dxgi = view.getUint32(HEADER_BYTES, true);
|
|
325
|
-
const dimension = view.getUint32(HEADER_BYTES + 4, true);
|
|
326
|
-
const miscFlag = view.getUint32(HEADER_BYTES + 8, true);
|
|
327
|
-
const arraySize = view.getUint32(HEADER_BYTES + 12, true);
|
|
328
|
-
if (dimension !== 3) {
|
|
329
|
-
throw new DrftError(`dds: this DX10 surface declares resourceDimension ${dimension}, and only 3 (a plain 2D ` +
|
|
330
|
-
'texture) is read here. A 1D or volume texture is a different thing wearing the same header.');
|
|
331
|
-
}
|
|
332
|
-
if ((miscFlag & 0x4) !== 0) {
|
|
333
|
-
throw new DrftError('dds: this is a cube map, and only a plain 2D texture is read here.');
|
|
334
|
-
}
|
|
335
|
-
if (arraySize > 1) {
|
|
336
|
-
throw new DrftError(`dds: this is a texture array of ${arraySize}, and only a plain 2D texture is read here.`);
|
|
337
|
-
}
|
|
338
|
-
const format = DXGI_BLOCK[dxgi];
|
|
339
|
-
if (format === undefined) {
|
|
340
|
-
throw new DrftError(`dds: DXGI format ${dxgi} is not one this reader decodes. It reads BC1, BC2, BC3 and BC5 ` +
|
|
341
|
-
'(unsigned). Convert the texture to PNG, or re-export it as one of those.');
|
|
342
|
-
}
|
|
343
|
-
return { format, dataAt };
|
|
344
|
-
}
|
|
345
124
|
export function ddsToRgba(bytes) {
|
|
346
125
|
if (!isDds(bytes))
|
|
347
126
|
throw new DrftError('dds: these bytes do not begin "DDS "');
|
|
@@ -351,7 +130,6 @@ export function ddsToRgba(bytes) {
|
|
|
351
130
|
const height = view.getUint32(12, true);
|
|
352
131
|
const width = view.getUint32(16, true);
|
|
353
132
|
const pixelFlags = view.getUint32(80, true);
|
|
354
|
-
const fourcc = String.fromCharCode(bytes[84], bytes[85], bytes[86], bytes[87]);
|
|
355
133
|
/*
|
|
356
134
|
* A pixel format that describes channels rather than naming a compression. There is no block to
|
|
357
135
|
* decode, so it is a bit count, four masks and a copy — and it is 289 of one consumer's 766
|
|
@@ -364,62 +142,11 @@ export function ddsToRgba(bytes) {
|
|
|
364
142
|
}
|
|
365
143
|
return uncompressedToRgba(bytes, view, width, height, pixelFlags);
|
|
366
144
|
}
|
|
367
|
-
const { format, dataAt } =
|
|
368
|
-
const
|
|
369
|
-
const blocksX = Math.max(1, Math.ceil(width / 4));
|
|
370
|
-
const blocksY = Math.max(1, Math.ceil(height / 4));
|
|
371
|
-
const needed = blocksX * blocksY * blockBytes;
|
|
145
|
+
const { format, dataAt, named } = readDdsSurface(bytes, view);
|
|
146
|
+
const needed = Math.max(1, Math.ceil(width / 4)) * Math.max(1, Math.ceil(height / 4)) * BC_BLOCK_BYTES[format];
|
|
372
147
|
if (bytes.length - dataAt < needed) {
|
|
373
|
-
throw new DrftError(`dds: the surface is short — ${width}x${height} of ${
|
|
148
|
+
throw new DrftError(`dds: the surface is short — ${width}x${height} of ${named} needs ${needed} bytes and ` +
|
|
374
149
|
`${bytes.length - dataAt} follow the header.`);
|
|
375
150
|
}
|
|
376
|
-
|
|
377
|
-
const block = new Uint8Array(16 * 4);
|
|
378
|
-
for (let by = 0; by < blocksY; by++) {
|
|
379
|
-
for (let bx = 0; bx < blocksX; bx++) {
|
|
380
|
-
const at = dataAt + (by * blocksX + bx) * blockBytes;
|
|
381
|
-
block.fill(0);
|
|
382
|
-
if (format === 'bc1') {
|
|
383
|
-
decodeColourBlock(bytes, at, block, true);
|
|
384
|
-
}
|
|
385
|
-
else if (format === 'bc2') {
|
|
386
|
-
/* Eight bytes of explicit alpha, then a colour block that is always four-colour. */
|
|
387
|
-
decodeColourBlock(bytes, at + 8, block, false);
|
|
388
|
-
decodeExplicitAlpha(bytes, at, block);
|
|
389
|
-
}
|
|
390
|
-
else if (format === 'bc3') {
|
|
391
|
-
decodeColourBlock(bytes, at + 8, block, false);
|
|
392
|
-
decodeAlphaBlock(bytes, at, block, 4, 3);
|
|
393
|
-
}
|
|
394
|
-
else {
|
|
395
|
-
/*
|
|
396
|
-
* BC5 is two BC4 blocks, red then green, and it is how a normal map is stored. Blue is
|
|
397
|
-
* left at zero rather than reconstructed: `z = sqrt(1 - x² - y²)` belongs in the shader
|
|
398
|
-
* that consumes the map, where the reconstruction can use the same convention the
|
|
399
|
-
* renderer's own tangent basis does.
|
|
400
|
-
*/
|
|
401
|
-
decodeAlphaBlock(bytes, at, block, 4, 0);
|
|
402
|
-
decodeAlphaBlock(bytes, at + 8, block, 4, 1);
|
|
403
|
-
for (let texel = 0; texel < 16; texel++)
|
|
404
|
-
block[texel * 4 + 3] = 255;
|
|
405
|
-
}
|
|
406
|
-
for (let ty = 0; ty < 4; ty++) {
|
|
407
|
-
const y = by * 4 + ty;
|
|
408
|
-
if (y >= height)
|
|
409
|
-
break;
|
|
410
|
-
for (let tx = 0; tx < 4; tx++) {
|
|
411
|
-
const x = bx * 4 + tx;
|
|
412
|
-
if (x >= width)
|
|
413
|
-
break;
|
|
414
|
-
const from = (ty * 4 + tx) * 4;
|
|
415
|
-
const to = (y * width + x) * 4;
|
|
416
|
-
rgba[to] = block[from];
|
|
417
|
-
rgba[to + 1] = block[from + 1];
|
|
418
|
-
rgba[to + 2] = block[from + 2];
|
|
419
|
-
rgba[to + 3] = block[from + 3];
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
return { width, height, rgba };
|
|
151
|
+
return { width, height, rgba: decodeBcImage(format, width, height, bytes.subarray(dataAt)) };
|
|
425
152
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A block-compressed DDS kept as it was authored: its blocks, untouched, and every mip level its
|
|
3
|
+
* author stored, ready for a `CODEC_BC` payload.
|
|
4
|
+
*
|
|
5
|
+
* **Why keep them rather than decode them.** A BC7 texture is a byte a texel on the GPU where RGBA
|
|
6
|
+
* is four, and its chain is the one its author built — often filtered with care a box filter does
|
|
7
|
+
* not take, and for a normal map renormalised at every level. Decoding to RGBA and letting the
|
|
8
|
+
* engine generate mips throws both away, and costs a phone's memory and a desktop's bandwidth for
|
|
9
|
+
* the privilege. Where a device cannot sample a format, the loader decodes the same blocks there.
|
|
10
|
+
*
|
|
11
|
+
* **What it refuses, so the baker can fall back to decoding level 0:** a chain longer than the
|
|
12
|
+
* image has levels for, a chain whose bytes stop before its last level, a volume texture (each of
|
|
13
|
+
* whose levels holds several slices, so its layout is not a chain of images), and an uncompressed
|
|
14
|
+
* surface, which has no blocks to keep. Bytes after the last level are left where they are; a
|
|
15
|
+
* classic cube map's other five faces are such bytes, so its first face is what is kept.
|
|
16
|
+
*/
|
|
17
|
+
import { bcChainLength, bcLevelBytes, DrftError } from '@driftengine/drft';
|
|
18
|
+
import { DDPF_FOURCC, HEADER_BYTES, isDds, readDdsSurface } from './ddsHeader.js';
|
|
19
|
+
/** `DDSCAPS2_VOLUME`: each level is a stack of slices rather than one image. */
|
|
20
|
+
const DDSCAPS2_VOLUME = 0x200000;
|
|
21
|
+
/** The surface's blocks and stored levels, as views over `bytes`. */
|
|
22
|
+
export function ddsBlocks(bytes) {
|
|
23
|
+
if (!isDds(bytes))
|
|
24
|
+
throw new DrftError('dds: these bytes do not begin "DDS "');
|
|
25
|
+
if (bytes.length < HEADER_BYTES) {
|
|
26
|
+
throw new DrftError('dds: the file is shorter than its own header');
|
|
27
|
+
}
|
|
28
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
29
|
+
if ((view.getUint32(80, true) & DDPF_FOURCC) === 0) {
|
|
30
|
+
throw new DrftError('dds: this surface is uncompressed, so it has no blocks to keep');
|
|
31
|
+
}
|
|
32
|
+
if ((view.getUint32(112, true) & DDSCAPS2_VOLUME) !== 0) {
|
|
33
|
+
throw new DrftError('dds: this is a volume texture, whose levels are stacks of slices rather than a chain');
|
|
34
|
+
}
|
|
35
|
+
const surface = readDdsSurface(bytes, view);
|
|
36
|
+
const { format, width, height, mipCount } = surface;
|
|
37
|
+
const most = bcChainLength(width, height);
|
|
38
|
+
if (mipCount > most) {
|
|
39
|
+
throw new DrftError(`dds: ${surface.named} declares ${mipCount} levels for a ${width}x${height} image, which ` +
|
|
40
|
+
`has at most ${most}`);
|
|
41
|
+
}
|
|
42
|
+
const levels = [];
|
|
43
|
+
let at = surface.dataAt;
|
|
44
|
+
for (let i = 0; i < mipCount; i++) {
|
|
45
|
+
const size = bcLevelBytes(format, width, height, i);
|
|
46
|
+
if (at + size > bytes.length) {
|
|
47
|
+
throw new DrftError(`dds: level ${i} of ${mipCount} needs ${size} bytes and ${Math.max(0, bytes.length - at)} ` +
|
|
48
|
+
'are left');
|
|
49
|
+
}
|
|
50
|
+
levels.push(bytes.subarray(at, at + size));
|
|
51
|
+
at += size;
|
|
52
|
+
}
|
|
53
|
+
return { format, srgb: surface.srgb, width, height, levels };
|
|
54
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { BcFormat } from '@driftengine/drft';
|
|
2
|
+
export declare const HEADER_BYTES = 128;
|
|
3
|
+
/** `DDPF_FOURCC`: the pixel format names a compression rather than describing channels. */
|
|
4
|
+
export declare const DDPF_FOURCC = 4;
|
|
5
|
+
/** Whether these bytes are a DDS at all. The magic, and nothing inferred from a file name. */
|
|
6
|
+
export declare function isDds(bytes: Uint8Array): boolean;
|
|
7
|
+
/** What a block-compressed DDS holds, read from its header alone. */
|
|
8
|
+
export interface DdsSurface {
|
|
9
|
+
readonly format: BcFormat;
|
|
10
|
+
readonly srgb: boolean;
|
|
11
|
+
readonly width: number;
|
|
12
|
+
readonly height: number;
|
|
13
|
+
/**
|
|
14
|
+
* The levels the header says follow. Zero is read as one, which is what Microsoft's own loader
|
|
15
|
+
* does: many writers leave the count zero, and its flag unset, on a surface with no chain.
|
|
16
|
+
*/
|
|
17
|
+
readonly mipCount: number;
|
|
18
|
+
/** Where level 0's first block starts. */
|
|
19
|
+
readonly dataAt: number;
|
|
20
|
+
/** How the header named the format, for a message: a FourCC, or `DXGI <n>`. */
|
|
21
|
+
readonly named: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The surface a block-compressed DDS declares. The caller has checked the magic, the length of the
|
|
25
|
+
* classic header and that the pixel format names a compression.
|
|
26
|
+
*
|
|
27
|
+
* **What a `DX10` header can say that a reader must refuse.** `resourceDimension` other than 3
|
|
28
|
+
* (`TEXTURE2D`), `miscFlag` bit 2 (a cube map) or an `arraySize` above 1. Taking one would hand the
|
|
29
|
+
* caller six faces or twenty slices claiming to be a single surface — a picture rather than an
|
|
30
|
+
* error.
|
|
31
|
+
*/
|
|
32
|
+
export declare function readDdsSurface(bytes: Uint8Array, view: DataView): DdsSurface;
|