@driftengine/assets 4.8.2 → 4.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/src/bcImage.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block-compressed texels decoded into an ordinary image: what a PNG of them has to hold.
|
|
3
|
+
*
|
|
4
|
+
* **Not what a GPU samples, on purpose.** `decodeBc` returns what a device would — BC4 as red alone,
|
|
5
|
+
* BC5 as red and green with blue at zero — because the loader's fallback must look like a compressed
|
|
6
|
+
* upload. A baked image is read later as an image, by a slot that reads three channels, so the two
|
|
7
|
+
* formats that store fewer are completed here: BC4's one value fills all three, as a luminance
|
|
8
|
+
* surface does, and BC5's missing z is rebuilt from the two it has.
|
|
9
|
+
*
|
|
10
|
+
* Shared by the DDS reader and the baker's texture pass, which decode the same blocks for the same
|
|
11
|
+
* reason. Runs in the baker, so it allocates the image it returns.
|
|
12
|
+
*/
|
|
13
|
+
import type { BcFormat } from '@driftengine/drft';
|
|
14
|
+
|
|
15
|
+
import { decodeBc } from './bcDecode.ts';
|
|
16
|
+
|
|
17
|
+
/** `blocks` as an RGBA image, eight bits a channel, every channel meaning what an image's does. */
|
|
18
|
+
export function decodeBcImage(
|
|
19
|
+
format: BcFormat,
|
|
20
|
+
width: number,
|
|
21
|
+
height: number,
|
|
22
|
+
blocks: Uint8Array,
|
|
23
|
+
): Uint8Array {
|
|
24
|
+
const rgba = decodeBc(format, width, height, blocks);
|
|
25
|
+
if (format === 'bc5') rebuildNormalZ(rgba);
|
|
26
|
+
if (format === 'bc4') greyFromRed(rgba);
|
|
27
|
+
return rgba;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* BC5 is two BC4 blocks, red then green, and it is how a normal map is stored: x and y, with z
|
|
32
|
+
* implied by the normal's unit length. **A baked image has three channels and the lit shader reads
|
|
33
|
+
* three**, so z is rebuilt here as `(√(1 − x² − y²) + 1) / 2`. Until 4.8.4 the DDS reader said z
|
|
34
|
+
* belonged in the shader that consumes the map, and no shader rebuilt it: every BC5 normal map baked
|
|
35
|
+
* to PNG arrived with blue at zero, a normal lying in the surface it was meant to stand out of.
|
|
36
|
+
*
|
|
37
|
+
* Outside the unit disc — compression error at a grazing normal — z is clamped to zero rather than
|
|
38
|
+
* reflected, which would turn the normal back to face the viewer.
|
|
39
|
+
*
|
|
40
|
+
* What it gives up: a BC5 surface that is not a normal map gets a blue channel derived as though it
|
|
41
|
+
* were. Red and green are untouched, so anything reading the two channels it stored loses nothing.
|
|
42
|
+
*/
|
|
43
|
+
function rebuildNormalZ(rgba: Uint8Array): void {
|
|
44
|
+
for (let at = 0; at < rgba.length; at += 4) {
|
|
45
|
+
const x = ((rgba[at] as number) / 255) * 2 - 1;
|
|
46
|
+
const y = ((rgba[at + 1] as number) / 255) * 2 - 1;
|
|
47
|
+
const z = Math.sqrt(Math.max(0, 1 - x * x - y * y));
|
|
48
|
+
rgba[at + 2] = Math.round((z * 0.5 + 0.5) * 255);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** BC4 is one channel, and a one-channel image is grey. */
|
|
53
|
+
function greyFromRed(rgba: Uint8Array): void {
|
|
54
|
+
for (let at = 0; at < rgba.length; at += 4) {
|
|
55
|
+
rgba[at + 1] = rgba[at] as number;
|
|
56
|
+
rgba[at + 2] = rgba[at] as number;
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/bcLoad.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
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 type { CompressedTextureFormat } from '@driftengine/core';
|
|
18
|
+
import type { BcImage } from '@driftengine/drft';
|
|
19
|
+
|
|
20
|
+
import { answerBcRequest } from './bcAnswer.ts';
|
|
21
|
+
import type { BcReply, BcRequest } from './bcAnswer.ts';
|
|
22
|
+
|
|
23
|
+
export type { BcReply, BcRequest } from './bcAnswer.ts';
|
|
24
|
+
export { answerBcRequest } from './bcAnswer.ts';
|
|
25
|
+
|
|
26
|
+
/** What happens to one BC texture. */
|
|
27
|
+
export type BcPlan = 'blocks' | 'decode';
|
|
28
|
+
|
|
29
|
+
/** Blocks where this device takes the format in the colour space the slot reads, with a chain. */
|
|
30
|
+
export function bcPlan(
|
|
31
|
+
image: BcImage,
|
|
32
|
+
srgb: boolean,
|
|
33
|
+
available: readonly CompressedTextureFormat[],
|
|
34
|
+
): BcPlan {
|
|
35
|
+
const chained = image.levels.length > 1 || Math.max(image.width, image.height) <= 4;
|
|
36
|
+
return chained && uploadsCompressed(image.format, srgb, image.width, image.height, available)
|
|
37
|
+
? 'blocks'
|
|
38
|
+
: 'decode';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The part of a worker the decoder uses. A browser `Worker` satisfies it. */
|
|
42
|
+
export interface BcWorker {
|
|
43
|
+
postMessage(request: BcRequest, transfer?: Transferable[]): void;
|
|
44
|
+
terminate(): void;
|
|
45
|
+
onmessage: ((event: { data: unknown }) => void) | null;
|
|
46
|
+
onerror?: ((event: unknown) => void) | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Level 0 of a BC image decoded to RGBA, off the main thread where one is to be had. */
|
|
50
|
+
export interface BcDecoder {
|
|
51
|
+
decode(image: BcImage, asImage: boolean): Promise<Uint8Array>;
|
|
52
|
+
/** Empty while a worker decodes; otherwise why the main thread does. */
|
|
53
|
+
readonly reason: string;
|
|
54
|
+
dispose(): void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface Pending {
|
|
58
|
+
readonly image: BcImage;
|
|
59
|
+
readonly asImage: boolean;
|
|
60
|
+
resolve(rgba: Uint8Array): void;
|
|
61
|
+
reject(error: Error): void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function request(id: number, image: BcImage, asImage: boolean): BcRequest {
|
|
65
|
+
/* A copy, so the transfer detaches nothing the loader still holds: a level is a view over the
|
|
66
|
+
fetched file, and every other view over that file would read as empty afterwards. */
|
|
67
|
+
const blocks = (image.levels[0] as Uint8Array).slice().buffer;
|
|
68
|
+
return { id, format: image.format, width: image.width, height: image.height, blocks, asImage };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function settle(reply: BcReply, pending: Pending): void {
|
|
72
|
+
if (reply.rgba !== undefined) pending.resolve(new Uint8Array(reply.rgba));
|
|
73
|
+
else pending.reject(new Error(reply.error ?? 'bc: the decode answered nothing'));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The worker this package ships, built the way a bundler understands, or null where there is no
|
|
78
|
+
* `Worker` at all. `createBcDecoder`'s `spawn` is the way past either.
|
|
79
|
+
*/
|
|
80
|
+
const defaultSpawn: (() => BcWorker) | null =
|
|
81
|
+
// platform: feature probe — no `Worker` means BC decodes on the main thread, with a reason
|
|
82
|
+
typeof Worker === 'undefined'
|
|
83
|
+
? null
|
|
84
|
+
: (): BcWorker =>
|
|
85
|
+
// platform: browser default — `createBcDecoder(spawn)` is the seam a host supplies
|
|
86
|
+
new Worker(new URL('./bcWorker.ts', import.meta.url), {
|
|
87
|
+
type: 'module',
|
|
88
|
+
}) as unknown as BcWorker;
|
|
89
|
+
|
|
90
|
+
export function createBcDecoder(spawn: (() => BcWorker) | null = defaultSpawn): BcDecoder {
|
|
91
|
+
let worker: BcWorker | null = null;
|
|
92
|
+
let reason = '';
|
|
93
|
+
const pending = new Map<number, Pending>();
|
|
94
|
+
let next = 0;
|
|
95
|
+
|
|
96
|
+
const onMainThread = (why: string): void => {
|
|
97
|
+
reason = `BC textures decode on the main thread: ${why}`;
|
|
98
|
+
worker?.terminate();
|
|
99
|
+
worker = null;
|
|
100
|
+
for (const [id, waiting] of pending)
|
|
101
|
+
settle(answerBcRequest(request(id, waiting.image, waiting.asImage)), waiting);
|
|
102
|
+
pending.clear();
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
if (spawn === null) {
|
|
106
|
+
reason = 'BC textures decode on the main thread: this runtime has no Worker';
|
|
107
|
+
} else {
|
|
108
|
+
try {
|
|
109
|
+
worker = spawn();
|
|
110
|
+
worker.onmessage = (event): void => {
|
|
111
|
+
const reply = event.data as BcReply;
|
|
112
|
+
const waiting = pending.get(reply.id);
|
|
113
|
+
if (waiting === undefined) return;
|
|
114
|
+
pending.delete(reply.id);
|
|
115
|
+
settle(reply, waiting);
|
|
116
|
+
};
|
|
117
|
+
worker.onerror = (event): void => {
|
|
118
|
+
const message = (event as { message?: unknown } | null)?.message;
|
|
119
|
+
onMainThread(`the worker failed (${typeof message === 'string' ? message : 'no message'})`);
|
|
120
|
+
};
|
|
121
|
+
} catch (error) {
|
|
122
|
+
onMainThread(
|
|
123
|
+
`the worker would not start (${error instanceof Error ? error.message : String(error)})`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
get reason(): string {
|
|
130
|
+
return reason;
|
|
131
|
+
},
|
|
132
|
+
decode(image: BcImage, asImage: boolean): Promise<Uint8Array> {
|
|
133
|
+
const id = next++;
|
|
134
|
+
const target = worker;
|
|
135
|
+
if (target === null) {
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
settle(answerBcRequest(request(id, image, asImage)), { image, asImage, resolve, reject });
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
pending.set(id, { image, asImage, resolve, reject });
|
|
142
|
+
const message = request(id, image, asImage);
|
|
143
|
+
target.postMessage(message, [message.blocks]);
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
dispose(): void {
|
|
147
|
+
worker?.terminate();
|
|
148
|
+
worker = null;
|
|
149
|
+
for (const waiting of pending.values())
|
|
150
|
+
waiting.reject(new Error('bc: the decoder was disposed'));
|
|
151
|
+
pending.clear();
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
package/src/bcWorker.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
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.ts';
|
|
10
|
+
import type { BcRequest } from './bcAnswer.ts';
|
|
11
|
+
|
|
12
|
+
const scope = globalThis as unknown as {
|
|
13
|
+
onmessage: ((event: { data: BcRequest }) => void) | null;
|
|
14
|
+
postMessage(reply: unknown, transfer: ArrayBuffer[]): void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
scope.onmessage = (event): void => {
|
|
18
|
+
const reply = answerBcRequest(event.data);
|
|
19
|
+
scope.postMessage(reply, reply.rgba === undefined ? [] : [reply.rgba]);
|
|
20
|
+
};
|
package/src/dds.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,212 +19,16 @@
|
|
|
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
|
-
|
|
34
|
-
import { DrftError } from '@driftengine/drft';
|
|
35
|
-
|
|
36
|
-
const HEADER_BYTES = 128;
|
|
37
|
-
/** `DDS_HEADER_DXT10`: the extension a `DX10` FourCC puts between the header and the surface. */
|
|
38
|
-
const DX10_EXTENSION_BYTES = 20;
|
|
39
|
-
/** `DDPF_FOURCC`: the pixel format names a compression rather than describing channels. */
|
|
40
|
-
const DDPF_FOURCC = 0x4;
|
|
41
|
-
|
|
42
|
-
/** Whether these bytes are a DDS at all. The magic, and nothing inferred from a file name. */
|
|
43
|
-
export function isDds(bytes: Uint8Array): boolean {
|
|
44
|
-
return (
|
|
45
|
-
bytes.length >= 4 &&
|
|
46
|
-
bytes[0] === 0x44 &&
|
|
47
|
-
bytes[1] === 0x44 &&
|
|
48
|
-
bytes[2] === 0x53 &&
|
|
49
|
-
bytes[3] === 0x20
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** RGB565 to three bytes, with the low bits replicated so white stays white. */
|
|
54
|
-
function expand565(value: number): [number, number, number] {
|
|
55
|
-
const r = (value >> 11) & 0x1f;
|
|
56
|
-
const g = (value >> 5) & 0x3f;
|
|
57
|
-
const b = value & 0x1f;
|
|
58
|
-
return [(r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2)];
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* One BC1 colour block into a 16-entry RGBA palette lookup.
|
|
63
|
-
*
|
|
64
|
-
* **The endpoint comparison chooses the mode and is the thing to get right.** With `c0 > c1` the
|
|
65
|
-
* block has four opaque colours; otherwise it has three and its fourth index is transparent black.
|
|
66
|
-
* A decoder that always takes the first branch produces an image that looks correct until
|
|
67
|
-
* something relies on the punch-through alpha, which for foliage and grilles is everything.
|
|
68
|
-
*/
|
|
69
|
-
function decodeColourBlock(
|
|
70
|
-
bytes: Uint8Array,
|
|
71
|
-
at: number,
|
|
72
|
-
out: Uint8Array,
|
|
73
|
-
/**
|
|
74
|
-
* Whether the block may take BC1's three-colour mode.
|
|
75
|
-
*
|
|
76
|
-
* **False for BC2 and BC3, whatever the endpoint order, and that is the correction of a real
|
|
77
|
-
* defect.** Those two carry alpha in their own half of the block, so there is no index left over
|
|
78
|
-
* to mean transparent and the colour half is always the four-colour opaque mode. This function
|
|
79
|
-
* was reached unconditionally, so a BC3 block written with `c0 <= c1` decoded its fourth index to
|
|
80
|
-
* transparent black instead of to the interpolant — invisible in every test here and in most real
|
|
81
|
-
* data, because an encoder that has no reason to order the endpoints the low way usually does not.
|
|
82
|
-
* Found while adding BC2, whose specification says the same thing about the same bytes.
|
|
83
|
-
*/
|
|
84
|
-
punchthrough: boolean,
|
|
85
|
-
): void {
|
|
86
|
-
const c0 = bytes[at]! | (bytes[at + 1]! << 8);
|
|
87
|
-
const c1 = bytes[at + 2]! | (bytes[at + 3]! << 8);
|
|
88
|
-
const [r0, g0, b0] = expand565(c0);
|
|
89
|
-
const [r1, g1, b1] = expand565(c1);
|
|
90
|
-
|
|
91
|
-
const palette = new Uint8Array(16);
|
|
92
|
-
palette.set([r0, g0, b0, 255], 0);
|
|
93
|
-
palette.set([r1, g1, b1, 255], 4);
|
|
94
|
-
if (c0 > c1 || !punchthrough) {
|
|
95
|
-
palette.set(
|
|
96
|
-
[
|
|
97
|
-
Math.round((2 * r0 + r1) / 3),
|
|
98
|
-
Math.round((2 * g0 + g1) / 3),
|
|
99
|
-
Math.round((2 * b0 + b1) / 3),
|
|
100
|
-
255,
|
|
101
|
-
Math.round((r0 + 2 * r1) / 3),
|
|
102
|
-
Math.round((g0 + 2 * g1) / 3),
|
|
103
|
-
Math.round((b0 + 2 * b1) / 3),
|
|
104
|
-
255,
|
|
105
|
-
],
|
|
106
|
-
8,
|
|
107
|
-
);
|
|
108
|
-
} else {
|
|
109
|
-
palette.set(
|
|
110
|
-
[
|
|
111
|
-
Math.round((r0 + r1) / 2),
|
|
112
|
-
Math.round((g0 + g1) / 2),
|
|
113
|
-
Math.round((b0 + b1) / 2),
|
|
114
|
-
255,
|
|
115
|
-
0,
|
|
116
|
-
0,
|
|
117
|
-
0,
|
|
118
|
-
0,
|
|
119
|
-
],
|
|
120
|
-
8,
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const indices =
|
|
125
|
-
bytes[at + 4]! | (bytes[at + 5]! << 8) | (bytes[at + 6]! << 16) | (bytes[at + 7]! << 24);
|
|
126
|
-
for (let texel = 0; texel < 16; texel++) {
|
|
127
|
-
const index = (indices >>> (texel * 2)) & 0x3;
|
|
128
|
-
out[texel * 4] = palette[index * 4]!;
|
|
129
|
-
out[texel * 4 + 1] = palette[index * 4 + 1]!;
|
|
130
|
-
out[texel * 4 + 2] = palette[index * 4 + 2]!;
|
|
131
|
-
out[texel * 4 + 3] = palette[index * 4 + 3]!;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* One BC4-style eight-value block, which is what BC3's alpha and both halves of BC5 are.
|
|
137
|
-
*
|
|
138
|
-
* The same endpoint comparison as the colour block, and the same consequence for getting it
|
|
139
|
-
* backwards: with `a0 > a1` there are six interpolants, and otherwise four plus a hard 0 and 255.
|
|
140
|
-
*/
|
|
141
|
-
function decodeAlphaBlock(
|
|
142
|
-
bytes: Uint8Array,
|
|
143
|
-
at: number,
|
|
144
|
-
out: Uint8Array,
|
|
145
|
-
stride: number,
|
|
146
|
-
offset: number,
|
|
147
|
-
): void {
|
|
148
|
-
const a0 = bytes[at]!;
|
|
149
|
-
const a1 = bytes[at + 1]!;
|
|
150
|
-
const values = new Uint8Array(8);
|
|
151
|
-
values[0] = a0;
|
|
152
|
-
values[1] = a1;
|
|
153
|
-
if (a0 > a1) {
|
|
154
|
-
for (let i = 1; i <= 6; i++) values[i + 1] = Math.round(((7 - i) * a0 + i * a1) / 7);
|
|
155
|
-
} else {
|
|
156
|
-
for (let i = 1; i <= 4; i++) values[i + 1] = Math.round(((5 - i) * a0 + i * a1) / 5);
|
|
157
|
-
values[6] = 0;
|
|
158
|
-
values[7] = 255;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/* Six bytes of 3-bit indices, least significant first, as one 48-bit run. */
|
|
162
|
-
let low = bytes[at + 2]! | (bytes[at + 3]! << 8) | (bytes[at + 4]! << 16);
|
|
163
|
-
let high = bytes[at + 5]! | (bytes[at + 6]! << 8) | (bytes[at + 7]! << 16);
|
|
164
|
-
for (let texel = 0; texel < 8; texel++) {
|
|
165
|
-
out[texel * stride + offset] = values[low & 0x7]!;
|
|
166
|
-
low >>>= 3;
|
|
167
|
-
}
|
|
168
|
-
for (let texel = 8; texel < 16; texel++) {
|
|
169
|
-
out[texel * stride + offset] = values[high & 0x7]!;
|
|
170
|
-
high >>>= 3;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* BC2's alpha: eight bytes of explicit four-bit values, one nibble a texel, in row order.
|
|
176
|
-
*
|
|
177
|
-
* **No interpolation and no mode**, which is the whole difference from BC3 and the reason this is
|
|
178
|
-
* four lines rather than the twenty above. Sixteen texels, low nibble of each byte first.
|
|
179
|
-
*
|
|
180
|
-
* The nibble is scaled by *replication* rather than by a shift: `0xF` has to come out 255 and not
|
|
181
|
-
* 240, or a texel the author wrote as fully opaque arrives at 94% and every alpha test in the
|
|
182
|
-
* pipeline sees a different picture from the one that was exported.
|
|
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.
|
|
183
24
|
*/
|
|
184
|
-
function decodeExplicitAlpha(bytes: Uint8Array, at: number, out: Uint8Array): void {
|
|
185
|
-
for (let texel = 0; texel < 16; texel++) {
|
|
186
|
-
const byte = bytes[at + (texel >> 1)]!;
|
|
187
|
-
const nibble = texel % 2 === 0 ? byte & 0xf : byte >> 4;
|
|
188
|
-
out[texel * 4 + 3] = nibble * 17;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
25
|
|
|
192
|
-
|
|
193
|
-
type BlockFormat = 'bc1' | 'bc2' | 'bc3' | 'bc5';
|
|
26
|
+
import { DrftError, BC_BLOCK_BYTES } from '@driftengine/drft';
|
|
194
27
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
*
|
|
198
|
-
* **A `DX10` header states the format in a 20-byte extension instead of in the four characters at
|
|
199
|
-
* byte 84, and that is the whole of the difference.** BC3 announced as `DXGI_FORMAT_BC3_UNORM_SRGB`
|
|
200
|
-
* is bit for bit the BC3 announced as `DXT5`: the same blocks, the same mip chain, and sRGB is a
|
|
201
|
-
* statement about how the values are *read* rather than about how they are stored. Measured from
|
|
202
|
-
* outside on one bundle's paint texture — 5,592,580 bytes against 5,592,560 for the classic
|
|
203
|
-
* spelling of the same 2048² chain, which is 148 + 5,592,432 against 128 + 5,592,432.
|
|
204
|
-
*
|
|
205
|
-
* The typeless and sRGB spellings sit beside the plain `UNORM` ones for that reason.
|
|
206
|
-
*
|
|
207
|
-
* **What is deliberately absent.** BC4 (79–81), which this reader does not decode at all; and
|
|
208
|
-
* `BC5_SNORM` (84), whose values are signed — mapping it would decode to wrong numbers rather than
|
|
209
|
-
* to an error, which is the one failure this file is written to avoid.
|
|
210
|
-
*/
|
|
211
|
-
const DXGI_BLOCK: Readonly<Record<number, BlockFormat>> = {
|
|
212
|
-
70: 'bc1',
|
|
213
|
-
71: 'bc1',
|
|
214
|
-
72: 'bc1',
|
|
215
|
-
73: 'bc2',
|
|
216
|
-
74: 'bc2',
|
|
217
|
-
75: 'bc2',
|
|
218
|
-
76: 'bc3',
|
|
219
|
-
77: 'bc3',
|
|
220
|
-
78: 'bc3',
|
|
221
|
-
82: 'bc5',
|
|
222
|
-
83: 'bc5',
|
|
223
|
-
};
|
|
28
|
+
import { decodeBcImage } from './bcImage.ts';
|
|
29
|
+
import { DDPF_FOURCC, HEADER_BYTES, isDds, readDdsSurface } from './ddsHeader.ts';
|
|
224
30
|
|
|
225
|
-
|
|
226
|
-
DXT1: 'bc1',
|
|
227
|
-
DXT3: 'bc2',
|
|
228
|
-
DXT5: 'bc3',
|
|
229
|
-
ATI2: 'bc5',
|
|
230
|
-
BC5U: 'bc5',
|
|
231
|
-
};
|
|
31
|
+
export { isDds } from './ddsHeader.ts';
|
|
232
32
|
|
|
233
33
|
/** What a decoded surface is: level 0, RGBA, eight bits a channel. */
|
|
234
34
|
export interface DdsImage {
|
|
@@ -237,14 +37,6 @@ export interface DdsImage {
|
|
|
237
37
|
readonly rgba: Uint8Array;
|
|
238
38
|
}
|
|
239
39
|
|
|
240
|
-
/**
|
|
241
|
-
* Decode a DDS surface's level 0 into RGBA.
|
|
242
|
-
*
|
|
243
|
-
* A surface whose dimensions are not multiples of four is decoded block by block and cropped, which
|
|
244
|
-
* is what the format itself does: the blocks cover a padded rectangle and the texels outside the
|
|
245
|
-
* declared size are padding rather than image.
|
|
246
|
-
*/
|
|
247
|
-
|
|
248
40
|
/** `DDPF_ALPHAPIXELS`, `DDPF_RGB`, `DDPF_LUMINANCE`: what the pixel format says it describes. */
|
|
249
41
|
const DDPF_ALPHAPIXELS = 0x1;
|
|
250
42
|
const DDPF_RGB = 0x40;
|
|
@@ -354,72 +146,12 @@ function uncompressedToRgba(
|
|
|
354
146
|
}
|
|
355
147
|
|
|
356
148
|
/**
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
* **Two spellings of one thing.** A classic header names the format in the four characters at byte
|
|
360
|
-
* 84 and the surface follows at 128; a `DX10` header puts `DX10` there instead and states the real
|
|
361
|
-
* format in a 20-byte extension, so the surface starts at 148. Nothing else differs — the blocks
|
|
362
|
-
* are the same blocks — which is why this resolves both to one name and one offset rather than
|
|
363
|
-
* growing a second decoder.
|
|
149
|
+
* Decode a DDS surface's level 0 into RGBA.
|
|
364
150
|
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
* to be a single surface — a picture rather than an error.
|
|
151
|
+
* A surface whose dimensions are not multiples of four is decoded block by block and cropped, which
|
|
152
|
+
* is what the format itself does: the blocks cover a padded rectangle and the texels outside the
|
|
153
|
+
* declared size are padding rather than image.
|
|
369
154
|
*/
|
|
370
|
-
function resolveSurface(
|
|
371
|
-
bytes: Uint8Array,
|
|
372
|
-
view: DataView,
|
|
373
|
-
fourcc: string,
|
|
374
|
-
): { format: BlockFormat; dataAt: number } {
|
|
375
|
-
if (fourcc !== 'DX10') {
|
|
376
|
-
const format = FOURCC_BLOCK[fourcc];
|
|
377
|
-
if (format === undefined) {
|
|
378
|
-
throw new DrftError(
|
|
379
|
-
`dds: "${fourcc}" is not a format this reader decodes. It reads BC1 (DXT1), BC2 (DXT3), ` +
|
|
380
|
-
'BC3 (DXT5) and BC5 (ATI2). Convert the texture to PNG, or re-export it as one of those.',
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
return { format, dataAt: HEADER_BYTES };
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const dataAt = HEADER_BYTES + DX10_EXTENSION_BYTES;
|
|
387
|
-
if (bytes.length < dataAt) {
|
|
388
|
-
throw new DrftError(
|
|
389
|
-
'dds: a DX10 header is declared and the twenty-byte extension is not there',
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const dxgi = view.getUint32(HEADER_BYTES, true);
|
|
394
|
-
const dimension = view.getUint32(HEADER_BYTES + 4, true);
|
|
395
|
-
const miscFlag = view.getUint32(HEADER_BYTES + 8, true);
|
|
396
|
-
const arraySize = view.getUint32(HEADER_BYTES + 12, true);
|
|
397
|
-
|
|
398
|
-
if (dimension !== 3) {
|
|
399
|
-
throw new DrftError(
|
|
400
|
-
`dds: this DX10 surface declares resourceDimension ${dimension}, and only 3 (a plain 2D ` +
|
|
401
|
-
'texture) is read here. A 1D or volume texture is a different thing wearing the same header.',
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
if ((miscFlag & 0x4) !== 0) {
|
|
405
|
-
throw new DrftError('dds: this is a cube map, and only a plain 2D texture is read here.');
|
|
406
|
-
}
|
|
407
|
-
if (arraySize > 1) {
|
|
408
|
-
throw new DrftError(
|
|
409
|
-
`dds: this is a texture array of ${arraySize}, and only a plain 2D texture is read here.`,
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const format = DXGI_BLOCK[dxgi];
|
|
414
|
-
if (format === undefined) {
|
|
415
|
-
throw new DrftError(
|
|
416
|
-
`dds: DXGI format ${dxgi} is not one this reader decodes. It reads BC1, BC2, BC3 and BC5 ` +
|
|
417
|
-
'(unsigned). Convert the texture to PNG, or re-export it as one of those.',
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
return { format, dataAt };
|
|
421
|
-
}
|
|
422
|
-
|
|
423
155
|
export function ddsToRgba(bytes: Uint8Array): DdsImage {
|
|
424
156
|
if (!isDds(bytes)) throw new DrftError('dds: these bytes do not begin "DDS "');
|
|
425
157
|
if (bytes.length < HEADER_BYTES)
|
|
@@ -429,7 +161,6 @@ export function ddsToRgba(bytes: Uint8Array): DdsImage {
|
|
|
429
161
|
const height = view.getUint32(12, true);
|
|
430
162
|
const width = view.getUint32(16, true);
|
|
431
163
|
const pixelFlags = view.getUint32(80, true);
|
|
432
|
-
const fourcc = String.fromCharCode(bytes[84]!, bytes[85]!, bytes[86]!, bytes[87]!);
|
|
433
164
|
|
|
434
165
|
/*
|
|
435
166
|
* A pixel format that describes channels rather than naming a compression. There is no block to
|
|
@@ -446,62 +177,15 @@ export function ddsToRgba(bytes: Uint8Array): DdsImage {
|
|
|
446
177
|
return uncompressedToRgba(bytes, view, width, height, pixelFlags);
|
|
447
178
|
}
|
|
448
179
|
|
|
449
|
-
const { format, dataAt } =
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
const blocksX = Math.max(1, Math.ceil(width / 4));
|
|
453
|
-
const blocksY = Math.max(1, Math.ceil(height / 4));
|
|
454
|
-
const needed = blocksX * blocksY * blockBytes;
|
|
180
|
+
const { format, dataAt, named } = readDdsSurface(bytes, view);
|
|
181
|
+
const needed =
|
|
182
|
+
Math.max(1, Math.ceil(width / 4)) * Math.max(1, Math.ceil(height / 4)) * BC_BLOCK_BYTES[format];
|
|
455
183
|
if (bytes.length - dataAt < needed) {
|
|
456
184
|
throw new DrftError(
|
|
457
|
-
`dds: the surface is short — ${width}x${height} of ${
|
|
185
|
+
`dds: the surface is short — ${width}x${height} of ${named} needs ${needed} bytes and ` +
|
|
458
186
|
`${bytes.length - dataAt} follow the header.`,
|
|
459
187
|
);
|
|
460
188
|
}
|
|
461
189
|
|
|
462
|
-
|
|
463
|
-
const block = new Uint8Array(16 * 4);
|
|
464
|
-
for (let by = 0; by < blocksY; by++) {
|
|
465
|
-
for (let bx = 0; bx < blocksX; bx++) {
|
|
466
|
-
const at = dataAt + (by * blocksX + bx) * blockBytes;
|
|
467
|
-
block.fill(0);
|
|
468
|
-
if (format === 'bc1') {
|
|
469
|
-
decodeColourBlock(bytes, at, block, true);
|
|
470
|
-
} else if (format === 'bc2') {
|
|
471
|
-
/* Eight bytes of explicit alpha, then a colour block that is always four-colour. */
|
|
472
|
-
decodeColourBlock(bytes, at + 8, block, false);
|
|
473
|
-
decodeExplicitAlpha(bytes, at, block);
|
|
474
|
-
} else if (format === 'bc3') {
|
|
475
|
-
decodeColourBlock(bytes, at + 8, block, false);
|
|
476
|
-
decodeAlphaBlock(bytes, at, block, 4, 3);
|
|
477
|
-
} else {
|
|
478
|
-
/*
|
|
479
|
-
* BC5 is two BC4 blocks, red then green, and it is how a normal map is stored. Blue is
|
|
480
|
-
* left at zero rather than reconstructed: `z = sqrt(1 - x² - y²)` belongs in the shader
|
|
481
|
-
* that consumes the map, where the reconstruction can use the same convention the
|
|
482
|
-
* renderer's own tangent basis does.
|
|
483
|
-
*/
|
|
484
|
-
decodeAlphaBlock(bytes, at, block, 4, 0);
|
|
485
|
-
decodeAlphaBlock(bytes, at + 8, block, 4, 1);
|
|
486
|
-
for (let texel = 0; texel < 16; texel++) block[texel * 4 + 3] = 255;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
for (let ty = 0; ty < 4; ty++) {
|
|
490
|
-
const y = by * 4 + ty;
|
|
491
|
-
if (y >= height) break;
|
|
492
|
-
for (let tx = 0; tx < 4; tx++) {
|
|
493
|
-
const x = bx * 4 + tx;
|
|
494
|
-
if (x >= width) break;
|
|
495
|
-
const from = (ty * 4 + tx) * 4;
|
|
496
|
-
const to = (y * width + x) * 4;
|
|
497
|
-
rgba[to] = block[from]!;
|
|
498
|
-
rgba[to + 1] = block[from + 1]!;
|
|
499
|
-
rgba[to + 2] = block[from + 2]!;
|
|
500
|
-
rgba[to + 3] = block[from + 3]!;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
return { width, height, rgba };
|
|
190
|
+
return { width, height, rgba: decodeBcImage(format, width, height, bytes.subarray(dataAt)) };
|
|
507
191
|
}
|