@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
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A vertex's joint influences, sorted, cut to eight and normalised into the two sets a mesh carries.
|
|
3
|
+
*
|
|
4
|
+
* **Eight and not four, since 4.8.4.** A rig authored for eight — a face, a shoulder, a hand — loses
|
|
5
|
+
* the shape of its deformation when the four lightest are dropped, and both importers dropped them.
|
|
6
|
+
* Heaviest first is the contract the format relies on: the first set always holds the four that
|
|
7
|
+
* matter most, so a renderer or an older pipeline reading only that set keeps the best four, and
|
|
8
|
+
* the second set holds what is left. Shared by the glTF and FBX readers so both cut and order the
|
|
9
|
+
* same way.
|
|
10
|
+
*
|
|
11
|
+
* **What it gives up** is anything past eight, renormalised over what is kept, as four was before.
|
|
12
|
+
* Runs at import, off the frame, so it allocates where that is clearer.
|
|
13
|
+
*/
|
|
14
|
+
/** The most influences a vertex keeps: two sets of four. */
|
|
15
|
+
export const MAX_INFLUENCES = 8;
|
|
16
|
+
const NONE = Object.freeze({ dropped: false, rescaled: false, second: false });
|
|
17
|
+
/**
|
|
18
|
+
* Write vertex `at`'s influences into `out`, heaviest first: the four heaviest into `joints` and
|
|
19
|
+
* `weights`, the next four into `joints2` and `weights2`, normalised to sum to one over all kept.
|
|
20
|
+
* A vertex with no positive weight is left all zero and reported as nothing, because inventing an
|
|
21
|
+
* influence would move geometry nobody rigged.
|
|
22
|
+
*/
|
|
23
|
+
export function writeInfluences(influences, out, at) {
|
|
24
|
+
if (influences === undefined || influences.length === 0)
|
|
25
|
+
return NONE;
|
|
26
|
+
/* Stable, so equal weights keep the order they arrived in: set 0 before set 1. */
|
|
27
|
+
const sorted = influences
|
|
28
|
+
.map((influence, order) => ({ ...influence, order }))
|
|
29
|
+
.filter((influence) => influence.weight > 0)
|
|
30
|
+
.sort((a, b) => b.weight - a.weight || a.order - b.order);
|
|
31
|
+
const kept = sorted.slice(0, MAX_INFLUENCES);
|
|
32
|
+
let sum = 0;
|
|
33
|
+
for (const influence of kept)
|
|
34
|
+
sum += influence.weight;
|
|
35
|
+
if (sum <= 0)
|
|
36
|
+
return NONE;
|
|
37
|
+
for (let i = 0; i < kept.length; i++) {
|
|
38
|
+
const influence = kept[i];
|
|
39
|
+
const joints = i < 4 ? out.joints : out.joints2;
|
|
40
|
+
const weights = i < 4 ? out.weights : out.weights2;
|
|
41
|
+
joints[at * 4 + (i % 4)] = influence.joint;
|
|
42
|
+
weights[at * 4 + (i % 4)] = influence.weight / sum;
|
|
43
|
+
}
|
|
44
|
+
let total = 0;
|
|
45
|
+
for (const influence of sorted)
|
|
46
|
+
total += influence.weight;
|
|
47
|
+
return {
|
|
48
|
+
dropped: sorted.length > MAX_INFLUENCES,
|
|
49
|
+
/* A tenth of a per cent, which is wider than float noise and narrower than an authoring slip. */
|
|
50
|
+
rescaled: Math.abs(total - 1) > 1e-3,
|
|
51
|
+
second: kept.length > 4,
|
|
52
|
+
};
|
|
53
|
+
}
|
package/dist/weld.js
CHANGED
|
@@ -52,6 +52,9 @@ const ATTRIBUTES = {
|
|
|
52
52
|
tangents: 4,
|
|
53
53
|
joints: 4,
|
|
54
54
|
weights: 4,
|
|
55
|
+
/* The fifth to eighth influences, which are per vertex like the first four. */
|
|
56
|
+
joints2: 4,
|
|
57
|
+
weights2: 4,
|
|
55
58
|
/*
|
|
56
59
|
* The four-lane per-vertex channel. It joins the key above as well as the copy below, which is
|
|
57
60
|
* the behaviour that matters: two vertices at one position whose sway or sky exposure differ
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/assets",
|
|
3
|
-
"version": "4.8.
|
|
3
|
+
"version": "4.8.4",
|
|
4
4
|
"description": "Model readers and the streaming loader for the .drft container",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
],
|
|
28
28
|
"sideEffects": false,
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@driftengine/drft": "4.8.
|
|
31
|
-
"@driftengine/texture": "4.8.
|
|
30
|
+
"@driftengine/drft": "4.8.4",
|
|
31
|
+
"@driftengine/texture": "4.8.4"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@driftengine/core": "4.8.
|
|
34
|
+
"@driftengine/core": "4.8.4"
|
|
35
35
|
},
|
|
36
36
|
"author": "Drift Technologies",
|
|
37
37
|
"repository": {
|
package/src/bc7Decode.ts
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One BC7 block into sixteen RGBA texels, exactly as the format defines it.
|
|
3
|
+
*
|
|
4
|
+
* **BC7 is the format to get bit-exact, and the one with no slack.** Eight modes, each a different
|
|
5
|
+
* split of 128 bits into partitions, endpoints, p-bits and indices; a field read one bit early
|
|
6
|
+
* shifts every field after it, and the picture that comes out is plausible noise rather than an
|
|
7
|
+
* error. Unlike BC1 to BC5, its interpolation is specified to the integer, so a decoder either
|
|
8
|
+
* agrees with the format to the byte or is wrong — `bcDecode.test.ts` holds it to that against an
|
|
9
|
+
* independent implementation, two random blocks a mode.
|
|
10
|
+
*
|
|
11
|
+
* A reserved mode — a first byte with no bit set — decodes to transparent black, which is what the
|
|
12
|
+
* format says a decoder returns for it.
|
|
13
|
+
*
|
|
14
|
+
* **Scratch lives at module scope**, because a phone without BC sampling decodes every block of
|
|
15
|
+
* every texture through here at load: a 2048² surface is 262,144 calls, and per-call arrays would
|
|
16
|
+
* be five allocations each. Not re-entrant, which nothing here needs; a worker has its own module.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Each mode's layout: subsets, then the bit widths of its partition, rotation, index selection,
|
|
21
|
+
* colour and alpha fields, whether it has a p-bit per endpoint or one shared per subset, and the
|
|
22
|
+
* widths of its primary and secondary indices.
|
|
23
|
+
*/
|
|
24
|
+
type Bc7Mode = readonly [
|
|
25
|
+
subsets: number,
|
|
26
|
+
partitionBits: number,
|
|
27
|
+
rotationBits: number,
|
|
28
|
+
selectionBits: number,
|
|
29
|
+
colourBits: number,
|
|
30
|
+
alphaBits: number,
|
|
31
|
+
endpointP: number,
|
|
32
|
+
sharedP: number,
|
|
33
|
+
index1: number,
|
|
34
|
+
index2: number,
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const BC7_MODES: readonly Bc7Mode[] = [
|
|
38
|
+
[3, 4, 0, 0, 4, 0, 1, 0, 3, 0],
|
|
39
|
+
[2, 6, 0, 0, 6, 0, 0, 1, 3, 0],
|
|
40
|
+
[3, 6, 0, 0, 5, 0, 0, 0, 2, 0],
|
|
41
|
+
[2, 6, 0, 0, 7, 0, 1, 0, 2, 0],
|
|
42
|
+
[1, 0, 2, 1, 5, 6, 0, 0, 2, 3],
|
|
43
|
+
[1, 0, 2, 0, 7, 8, 0, 0, 2, 2],
|
|
44
|
+
[1, 0, 0, 0, 7, 7, 1, 0, 4, 0],
|
|
45
|
+
[2, 6, 0, 0, 5, 5, 1, 0, 2, 0],
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/** Interpolation weights out of 64, for two-, three- and four-bit indices. */
|
|
49
|
+
const BC7_WEIGHTS: readonly (readonly number[])[] = [
|
|
50
|
+
[0, 21, 43, 64],
|
|
51
|
+
[0, 9, 18, 27, 37, 46, 55, 64],
|
|
52
|
+
[0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 60, 64],
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The two-subset partitions, a bit a pixel (pixel 0 lowest), 1 for the second subset.
|
|
57
|
+
*
|
|
58
|
+
* The format's own table, as published in the D3D11 BC7 specification; transcribed from
|
|
59
|
+
* `texture2ddecoder` 0.1.2 (MIT OR Apache-2.0) and held by `bcDecode.test.ts` to what the format
|
|
60
|
+
* says of it: pixel 0 in subset 0, and every anchor inside the subset it anchors.
|
|
61
|
+
*/
|
|
62
|
+
export const BC7_PARTITION2: readonly number[] = [
|
|
63
|
+
0xcccc, 0x8888, 0xeeee, 0xecc8, 0xc880, 0xfeec, 0xfec8, 0xec80, 0xc800, 0xffec, 0xfe80, 0xe800,
|
|
64
|
+
0xffe8, 0xff00, 0xfff0, 0xf000, 0xf710, 0x008e, 0x7100, 0x08ce, 0x008c, 0x7310, 0x3100, 0x8cce,
|
|
65
|
+
0x088c, 0x3110, 0x6666, 0x366c, 0x17e8, 0x0ff0, 0x718e, 0x399c, 0xaaaa, 0xf0f0, 0x5a5a, 0x33cc,
|
|
66
|
+
0x3c3c, 0x55aa, 0x9696, 0xa55a, 0x73ce, 0x13c8, 0x324c, 0x3bdc, 0x6996, 0xc33c, 0x9966, 0x0660,
|
|
67
|
+
0x0272, 0x04e4, 0x4e40, 0x2720, 0xc936, 0x936c, 0x39c6, 0x639c, 0x9336, 0x9cc6, 0x817e, 0xe718,
|
|
68
|
+
0xccf0, 0x0fcc, 0x7744, 0xee22,
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
/** The three-subset partitions, two bits a pixel (pixel 0 lowest). From the same source. */
|
|
72
|
+
export const BC7_PARTITION3: readonly number[] = [
|
|
73
|
+
0xaa685050, 0x6a5a5040, 0x5a5a4200, 0x5450a0a8, 0xa5a50000, 0xa0a05050, 0x5555a0a0, 0x5a5a5050,
|
|
74
|
+
0xaa550000, 0xaa555500, 0xaaaa5500, 0x90909090, 0x94949494, 0xa4a4a4a4, 0xa9a59450, 0x2a0a4250,
|
|
75
|
+
0xa5945040, 0x0a425054, 0xa5a5a500, 0x55a0a0a0, 0xa8a85454, 0x6a6a4040, 0xa4a45000, 0x1a1a0500,
|
|
76
|
+
0x0050a4a4, 0xaaa59090, 0x14696914, 0x69691400, 0xa08585a0, 0xaa821414, 0x50a4a450, 0x6a5a0200,
|
|
77
|
+
0xa9a58000, 0x5090a0a8, 0xa8a09050, 0x24242424, 0x00aa5500, 0x24924924, 0x24499224, 0x50a50a50,
|
|
78
|
+
0x500aa550, 0xaaaa4444, 0x66660000, 0xa5a0a5a0, 0x50a050a0, 0x69286928, 0x44aaaa44, 0x66666600,
|
|
79
|
+
0xaa444444, 0x54a854a8, 0x95809580, 0x96969600, 0xa85454a8, 0x80959580, 0xaa141414, 0x96960000,
|
|
80
|
+
0xaaaa1414, 0xa05050a0, 0xa0a5a5a0, 0x96000000, 0x40804080, 0xa9a8a9a8, 0xaaaaaa44, 0x2a4a5254,
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
/** The anchor pixel of the second subset of each two-subset partition. From the same source. */
|
|
84
|
+
export const BC7_ANCHOR2: readonly number[] = [
|
|
85
|
+
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 2, 8, 2, 2, 8, 8, 15, 2, 8, 2,
|
|
86
|
+
2, 8, 8, 2, 2, 15, 15, 6, 8, 2, 8, 15, 15, 2, 8, 2, 2, 2, 15, 15, 6, 6, 2, 6, 8, 15, 15, 2, 2, 15,
|
|
87
|
+
15, 15, 15, 15, 2, 2, 15,
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
/** The anchor pixels of the second and third subsets of each three-subset partition. */
|
|
91
|
+
export const BC7_ANCHOR3: readonly (readonly number[])[] = [
|
|
92
|
+
[
|
|
93
|
+
3, 3, 15, 15, 8, 3, 15, 15, 8, 8, 6, 6, 6, 5, 3, 3, 3, 3, 8, 15, 3, 3, 6, 10, 5, 8, 8, 6, 8, 5,
|
|
94
|
+
15, 15, 8, 15, 3, 5, 6, 10, 8, 15, 15, 3, 15, 5, 15, 15, 15, 15, 3, 15, 5, 5, 5, 8, 5, 10, 5,
|
|
95
|
+
10, 8, 13, 15, 12, 3, 3,
|
|
96
|
+
],
|
|
97
|
+
[
|
|
98
|
+
15, 8, 8, 3, 15, 15, 3, 8, 15, 15, 15, 15, 15, 15, 15, 8, 15, 8, 15, 3, 15, 8, 15, 8, 3, 15, 6,
|
|
99
|
+
10, 15, 15, 10, 8, 15, 3, 15, 10, 10, 8, 9, 10, 6, 15, 8, 15, 3, 6, 6, 8, 15, 3, 15, 15, 15, 15,
|
|
100
|
+
15, 15, 15, 15, 15, 15, 3, 15, 15, 8,
|
|
101
|
+
],
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
/** `n` bits of a block from bit `pos`, least significant first, as the format orders them. */
|
|
105
|
+
function bits(bytes: Uint8Array, at: number, pos: number, n: number): number {
|
|
106
|
+
let value = 0;
|
|
107
|
+
for (let i = 0; i < n; i++) {
|
|
108
|
+
const p = pos + i;
|
|
109
|
+
value |= (((bytes[at + (p >> 3)] as number) >> (p & 7)) & 1) << i;
|
|
110
|
+
}
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** A quantised endpoint widened to eight bits by replicating its top bits into the bottom. */
|
|
115
|
+
function expandEndpoint(value: number, width: number): number {
|
|
116
|
+
const shifted = (value << (8 - width)) & 0xff;
|
|
117
|
+
return shifted | (shifted >> width);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The subset and anchor of each pixel, for a partition. */
|
|
121
|
+
function subsetOf(subsets: number, partition: number, pixel: number): number {
|
|
122
|
+
if (subsets === 2) return ((BC7_PARTITION2[partition] as number) >> pixel) & 1;
|
|
123
|
+
if (subsets === 3) return ((BC7_PARTITION3[partition] as number) >>> (2 * pixel)) & 3;
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isAnchor(subsets: number, partition: number, pixel: number): boolean {
|
|
128
|
+
if (pixel === 0) return true;
|
|
129
|
+
if (subsets === 2) return BC7_ANCHOR2[partition] === pixel;
|
|
130
|
+
if (subsets === 3) {
|
|
131
|
+
return BC7_ANCHOR3[0]?.[partition] === pixel || BC7_ANCHOR3[1]?.[partition] === pixel;
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const RAW = new Uint8Array(24);
|
|
137
|
+
const PBITS = new Uint8Array(6);
|
|
138
|
+
const COLOURS = new Uint8Array(24);
|
|
139
|
+
const PRIMARY = new Uint8Array(16);
|
|
140
|
+
const SECONDARY = new Uint8Array(16);
|
|
141
|
+
|
|
142
|
+
/** One BC7 block at `bytes[at]` into sixteen RGBA texels in `out`. */
|
|
143
|
+
export function decodeBc7Block(bytes: Uint8Array, at: number, out: Uint8Array): void {
|
|
144
|
+
let mode = 0;
|
|
145
|
+
while (mode < 8 && bits(bytes, at, mode, 1) === 0) mode++;
|
|
146
|
+
const layout = BC7_MODES[mode];
|
|
147
|
+
if (layout === undefined) {
|
|
148
|
+
out.fill(0, 0, 64);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const [
|
|
152
|
+
subsets,
|
|
153
|
+
partitionBits,
|
|
154
|
+
rotationBits,
|
|
155
|
+
selectionBits,
|
|
156
|
+
colourBits,
|
|
157
|
+
alphaBits,
|
|
158
|
+
endpointP,
|
|
159
|
+
sharedP,
|
|
160
|
+
index1,
|
|
161
|
+
index2,
|
|
162
|
+
] = layout;
|
|
163
|
+
let pos = mode + 1;
|
|
164
|
+
const partition = bits(bytes, at, pos, partitionBits);
|
|
165
|
+
pos += partitionBits;
|
|
166
|
+
const rotation = bits(bytes, at, pos, rotationBits);
|
|
167
|
+
pos += rotationBits;
|
|
168
|
+
const selection = bits(bytes, at, pos, selectionBits);
|
|
169
|
+
pos += selectionBits;
|
|
170
|
+
|
|
171
|
+
/* Endpoints channel by channel, two per subset: red for every subset, then green, blue, alpha. */
|
|
172
|
+
const endpoints = 2 * subsets;
|
|
173
|
+
const channels = alphaBits > 0 ? 4 : 3;
|
|
174
|
+
for (let c = 0; c < channels; c++) {
|
|
175
|
+
const width = c < 3 ? colourBits : alphaBits;
|
|
176
|
+
for (let e = 0; e < endpoints; e++) {
|
|
177
|
+
RAW[e * 4 + c] = bits(bytes, at, pos, width);
|
|
178
|
+
pos += width;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/* P-bits: one per endpoint, or one per subset shared by its two, appended below each value. */
|
|
182
|
+
const hasP = endpointP !== 0 || sharedP !== 0;
|
|
183
|
+
for (let e = 0; e < endpoints; e++) {
|
|
184
|
+
if (endpointP !== 0) PBITS[e] = bits(bytes, at, pos++, 1);
|
|
185
|
+
else if (sharedP !== 0 && e % 2 === 0) PBITS[e] = PBITS[e + 1] = bits(bytes, at, pos++, 1);
|
|
186
|
+
}
|
|
187
|
+
for (let e = 0; e < endpoints; e++) {
|
|
188
|
+
for (let c = 0; c < 4; c++) {
|
|
189
|
+
if (c === 3 && alphaBits === 0) {
|
|
190
|
+
COLOURS[e * 4 + 3] = 255;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const stored = RAW[e * 4 + c] as number;
|
|
194
|
+
const width = (c < 3 ? colourBits : alphaBits) + (hasP ? 1 : 0);
|
|
195
|
+
COLOURS[e * 4 + c] = expandEndpoint(
|
|
196
|
+
hasP ? (stored << 1) | (PBITS[e] as number) : stored,
|
|
197
|
+
width,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* Indices: primary for all sixteen pixels, each anchor one bit short; then secondary, if any. */
|
|
203
|
+
for (let pixel = 0; pixel < 16; pixel++) {
|
|
204
|
+
const width = index1 - (isAnchor(subsets, partition, pixel) ? 1 : 0);
|
|
205
|
+
PRIMARY[pixel] = bits(bytes, at, pos, width);
|
|
206
|
+
pos += width;
|
|
207
|
+
}
|
|
208
|
+
for (let pixel = 0; index2 > 0 && pixel < 16; pixel++) {
|
|
209
|
+
const width = index2 - (pixel === 0 ? 1 : 0);
|
|
210
|
+
SECONDARY[pixel] = bits(bytes, at, pos, width);
|
|
211
|
+
pos += width;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const weights1 = BC7_WEIGHTS[index1 - 2] as readonly number[];
|
|
215
|
+
const weights2 = BC7_WEIGHTS[Math.max(index2, 2) - 2] as readonly number[];
|
|
216
|
+
for (let pixel = 0; pixel < 16; pixel++) {
|
|
217
|
+
/* One index for both unless the mode has two; mode 4's selection bit says which drives alpha. */
|
|
218
|
+
const first = weights1[PRIMARY[pixel] as number] as number;
|
|
219
|
+
const second = index2 > 0 ? (weights2[SECONDARY[pixel] as number] as number) : first;
|
|
220
|
+
const colourWeight = selection === 0 ? first : second;
|
|
221
|
+
const alphaWeight = selection === 0 ? second : first;
|
|
222
|
+
const e0 = subsetOf(subsets, partition, pixel) * 8;
|
|
223
|
+
const texel = pixel * 4;
|
|
224
|
+
for (let c = 0; c < 4; c++) {
|
|
225
|
+
const w = c < 3 ? colourWeight : alphaWeight;
|
|
226
|
+
out[texel + c] =
|
|
227
|
+
((COLOURS[e0 + c] as number) * (64 - w) + (COLOURS[e0 + 4 + c] as number) * w + 32) >> 6;
|
|
228
|
+
}
|
|
229
|
+
/* Rotation: the stored alpha was one of the colour channels, and that channel was alpha. */
|
|
230
|
+
if (rotation !== 0) {
|
|
231
|
+
const swap = texel + rotation - 1;
|
|
232
|
+
const alpha = out[texel + 3] as number;
|
|
233
|
+
out[texel + 3] = out[swap] as number;
|
|
234
|
+
out[swap] = alpha;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
package/src/bcAnswer.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The BC worker's whole job, as a function: one request's level 0 decoded, or the reason it could
|
|
3
|
+
* not be. Its own module so the worker imports the decoders and nothing else — not the loader, and
|
|
4
|
+
* not the engine's barrel behind it.
|
|
5
|
+
*
|
|
6
|
+
* Decoded **as a GPU samples it** for the loader's own textures, so a decode at load looks like an
|
|
7
|
+
* upload of the same blocks: BC4 red alone, BC5 red and green, the lit shader rebuilding a normal's
|
|
8
|
+
* z either way. Decoded **as an ordinary image** (`asImage`) for a consumer taking images through
|
|
9
|
+
* `onImage`, who builds textures the engine does not see. An error is answered rather than thrown,
|
|
10
|
+
* so one bad texture fails on its own and the worker lives on.
|
|
11
|
+
*/
|
|
12
|
+
import type { BcFormat } from '@driftengine/drft';
|
|
13
|
+
|
|
14
|
+
import { decodeBc } from './bcDecode.ts';
|
|
15
|
+
import { decodeBcImage } from './bcImage.ts';
|
|
16
|
+
|
|
17
|
+
/** What the loader asks the worker: one level's blocks, handed over as a buffer of their own. */
|
|
18
|
+
export interface BcRequest {
|
|
19
|
+
readonly id: number;
|
|
20
|
+
readonly format: BcFormat;
|
|
21
|
+
readonly width: number;
|
|
22
|
+
readonly height: number;
|
|
23
|
+
readonly blocks: ArrayBuffer;
|
|
24
|
+
readonly asImage: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The answer: RGBA, eight bits a channel, or why not. */
|
|
28
|
+
export interface BcReply {
|
|
29
|
+
readonly id: number;
|
|
30
|
+
readonly rgba?: ArrayBuffer;
|
|
31
|
+
readonly error?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function answerBcRequest(request: BcRequest): BcReply {
|
|
35
|
+
try {
|
|
36
|
+
const blocks = new Uint8Array(request.blocks);
|
|
37
|
+
const { format, width, height } = request;
|
|
38
|
+
const rgba = request.asImage
|
|
39
|
+
? decodeBcImage(format, width, height, blocks)
|
|
40
|
+
: decodeBc(format, width, height, blocks);
|
|
41
|
+
return { id: request.id, rgba: rgba.buffer as ArrayBuffer };
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return { id: request.id, error: error instanceof Error ? error.message : String(error) };
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/bcDecode.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block-compressed texels to RGBA: BC1, BC2, BC3, BC4, BC5 and BC7.
|
|
3
|
+
*
|
|
4
|
+
* **Two callers, one decoder.** The baker turns a DDS it is told to re-encode into RGBA here, and the
|
|
5
|
+
* loader decodes a container's BC payload here on a device that cannot sample the format — most
|
|
6
|
+
* phones, which have ASTC and ETC2 instead. Both must read a block the same way, so the block
|
|
7
|
+
* routines live in one place rather than in the DDS reader that grew them first.
|
|
8
|
+
*
|
|
9
|
+
* **What a channel the format does not store reads as** is what a GPU returns for it, so a texture
|
|
10
|
+
* looks the same whether it was uploaded compressed or decoded here: BC4 is `(r, 0, 0, 255)` and BC5
|
|
11
|
+
* `(r, g, 0, 255)`. A BC5 normal map therefore arrives with blue at zero on both paths, and the lit
|
|
12
|
+
* shader rebuilds z from the two it has.
|
|
13
|
+
*
|
|
14
|
+
* **What it gives up.** BC1 to BC5 interpolants are rounded here where some decoders truncate, so a
|
|
15
|
+
* texel may differ by one from another decoder's; the format allows either, and hardware differs too.
|
|
16
|
+
* BC7 is defined exactly and decodes to the byte, in `bc7Decode.ts`.
|
|
17
|
+
*
|
|
18
|
+
* Runs off the frame, in the baker or the loader's worker, but a phone decodes every block of every
|
|
19
|
+
* texture through here at load, so the per-block scratch lives at module scope.
|
|
20
|
+
*/
|
|
21
|
+
import type { BcFormat } from '@driftengine/drft';
|
|
22
|
+
import { BC_BLOCK_BYTES, DrftError } from '@driftengine/drft';
|
|
23
|
+
|
|
24
|
+
import { decodeBc7Block } from './bc7Decode.ts';
|
|
25
|
+
|
|
26
|
+
export type { BcFormat } from '@driftengine/drft';
|
|
27
|
+
|
|
28
|
+
/** A colour block's four entries, and a BC4 block's eight values, rewritten for every block. */
|
|
29
|
+
const PALETTE = new Uint8Array(16);
|
|
30
|
+
const VALUES = new Uint8Array(8);
|
|
31
|
+
const TEXELS = new Uint8Array(64);
|
|
32
|
+
|
|
33
|
+
/** RGB565 into the palette at `at`, opaque, with the low bits replicated so white stays white. */
|
|
34
|
+
function expand565(value: number, at: number): void {
|
|
35
|
+
const r = (value >> 11) & 0x1f;
|
|
36
|
+
const g = (value >> 5) & 0x3f;
|
|
37
|
+
const b = value & 0x1f;
|
|
38
|
+
PALETTE[at] = (r << 3) | (r >> 2);
|
|
39
|
+
PALETTE[at + 1] = (g << 2) | (g >> 4);
|
|
40
|
+
PALETTE[at + 2] = (b << 3) | (b >> 2);
|
|
41
|
+
PALETTE[at + 3] = 255;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* One BC1 colour block into sixteen RGBA texels.
|
|
46
|
+
*
|
|
47
|
+
* **The endpoint comparison chooses the mode and is the thing to get right.** With `c0 > c1` the
|
|
48
|
+
* block has four opaque colours; otherwise it has three and its fourth index is transparent black.
|
|
49
|
+
* A decoder that always takes the first branch produces an image that looks correct until
|
|
50
|
+
* something relies on the punch-through alpha, which for foliage and grilles is everything.
|
|
51
|
+
*
|
|
52
|
+
* `punchthrough` is false for BC2 and BC3, whatever the endpoint order: those carry alpha in their
|
|
53
|
+
* own half of the block, so there is no index left over to mean transparent and the colour half is
|
|
54
|
+
* always the four-colour opaque mode. That was a real defect once: reached unconditionally, a BC3
|
|
55
|
+
* block written with `c0 <= c1` decoded its fourth index to transparent black — invisible in most
|
|
56
|
+
* real data, because an encoder has no reason to order the endpoints the low way round.
|
|
57
|
+
*/
|
|
58
|
+
function decodeColourBlock(
|
|
59
|
+
bytes: Uint8Array,
|
|
60
|
+
at: number,
|
|
61
|
+
out: Uint8Array,
|
|
62
|
+
punchthrough: boolean,
|
|
63
|
+
): void {
|
|
64
|
+
const c0 = (bytes[at] as number) | ((bytes[at + 1] as number) << 8);
|
|
65
|
+
const c1 = (bytes[at + 2] as number) | ((bytes[at + 3] as number) << 8);
|
|
66
|
+
expand565(c0, 0);
|
|
67
|
+
expand565(c1, 4);
|
|
68
|
+
const four = c0 > c1 || !punchthrough;
|
|
69
|
+
for (let c = 0; c < 3; c++) {
|
|
70
|
+
const e0 = PALETTE[c] as number;
|
|
71
|
+
const e1 = PALETTE[4 + c] as number;
|
|
72
|
+
PALETTE[8 + c] = four ? Math.round((2 * e0 + e1) / 3) : Math.round((e0 + e1) / 2);
|
|
73
|
+
PALETTE[12 + c] = four ? Math.round((e0 + 2 * e1) / 3) : 0;
|
|
74
|
+
}
|
|
75
|
+
PALETTE[11] = 255;
|
|
76
|
+
PALETTE[15] = four ? 255 : 0;
|
|
77
|
+
|
|
78
|
+
const indices =
|
|
79
|
+
(bytes[at + 4] as number) |
|
|
80
|
+
((bytes[at + 5] as number) << 8) |
|
|
81
|
+
((bytes[at + 6] as number) << 16) |
|
|
82
|
+
((bytes[at + 7] as number) << 24);
|
|
83
|
+
for (let texel = 0; texel < 16; texel++) {
|
|
84
|
+
const index = (indices >>> (texel * 2)) & 0x3;
|
|
85
|
+
out[texel * 4] = PALETTE[index * 4] as number;
|
|
86
|
+
out[texel * 4 + 1] = PALETTE[index * 4 + 1] as number;
|
|
87
|
+
out[texel * 4 + 2] = PALETTE[index * 4 + 2] as number;
|
|
88
|
+
out[texel * 4 + 3] = PALETTE[index * 4 + 3] as number;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* One BC4-style eight-value block, which is what BC3's alpha and both halves of BC5 are, written
|
|
94
|
+
* into channel `offset` of each texel `stride` bytes apart.
|
|
95
|
+
*
|
|
96
|
+
* The same endpoint comparison as the colour block: with `a0 > a1` there are six interpolants, and
|
|
97
|
+
* otherwise four plus a hard 0 and 255.
|
|
98
|
+
*/
|
|
99
|
+
function decodeAlphaBlock(
|
|
100
|
+
bytes: Uint8Array,
|
|
101
|
+
at: number,
|
|
102
|
+
out: Uint8Array,
|
|
103
|
+
stride: number,
|
|
104
|
+
offset: number,
|
|
105
|
+
): void {
|
|
106
|
+
const a0 = bytes[at] as number;
|
|
107
|
+
const a1 = bytes[at + 1] as number;
|
|
108
|
+
const values = VALUES;
|
|
109
|
+
values[0] = a0;
|
|
110
|
+
values[1] = a1;
|
|
111
|
+
if (a0 > a1) {
|
|
112
|
+
for (let i = 1; i <= 6; i++) values[i + 1] = Math.round(((7 - i) * a0 + i * a1) / 7);
|
|
113
|
+
} else {
|
|
114
|
+
for (let i = 1; i <= 4; i++) values[i + 1] = Math.round(((5 - i) * a0 + i * a1) / 5);
|
|
115
|
+
values[6] = 0;
|
|
116
|
+
values[7] = 255;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Six bytes of 3-bit indices, least significant first, as one 48-bit run. */
|
|
120
|
+
let low =
|
|
121
|
+
(bytes[at + 2] as number) |
|
|
122
|
+
((bytes[at + 3] as number) << 8) |
|
|
123
|
+
((bytes[at + 4] as number) << 16);
|
|
124
|
+
let high =
|
|
125
|
+
(bytes[at + 5] as number) |
|
|
126
|
+
((bytes[at + 6] as number) << 8) |
|
|
127
|
+
((bytes[at + 7] as number) << 16);
|
|
128
|
+
for (let texel = 0; texel < 8; texel++) {
|
|
129
|
+
out[texel * stride + offset] = values[low & 0x7] as number;
|
|
130
|
+
low >>>= 3;
|
|
131
|
+
}
|
|
132
|
+
for (let texel = 8; texel < 16; texel++) {
|
|
133
|
+
out[texel * stride + offset] = values[high & 0x7] as number;
|
|
134
|
+
high >>>= 3;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* BC2's alpha: eight bytes of explicit four-bit values, one nibble a texel, low nibble first.
|
|
140
|
+
*
|
|
141
|
+
* Scaled by *replication* rather than by a shift: `0xF` has to come out 255 and not 240, or a texel
|
|
142
|
+
* the author wrote as fully opaque arrives at 94% and every alpha test sees a different picture.
|
|
143
|
+
*/
|
|
144
|
+
function decodeExplicitAlpha(bytes: Uint8Array, at: number, out: Uint8Array): void {
|
|
145
|
+
for (let texel = 0; texel < 16; texel++) {
|
|
146
|
+
const byte = bytes[at + (texel >> 1)] as number;
|
|
147
|
+
const nibble = texel % 2 === 0 ? byte & 0xf : byte >> 4;
|
|
148
|
+
out[texel * 4 + 3] = nibble * 17;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** One block of any format into sixteen RGBA texels. */
|
|
153
|
+
function decodeBlock(format: BcFormat, bytes: Uint8Array, at: number, out: Uint8Array): void {
|
|
154
|
+
out.fill(0, 0, 64);
|
|
155
|
+
if (format === 'bc1') {
|
|
156
|
+
decodeColourBlock(bytes, at, out, true);
|
|
157
|
+
} else if (format === 'bc2') {
|
|
158
|
+
decodeColourBlock(bytes, at + 8, out, false);
|
|
159
|
+
decodeExplicitAlpha(bytes, at, out);
|
|
160
|
+
} else if (format === 'bc3') {
|
|
161
|
+
decodeColourBlock(bytes, at + 8, out, false);
|
|
162
|
+
decodeAlphaBlock(bytes, at, out, 4, 3);
|
|
163
|
+
} else if (format === 'bc4') {
|
|
164
|
+
decodeAlphaBlock(bytes, at, out, 4, 0);
|
|
165
|
+
for (let t = 0; t < 16; t++) out[t * 4 + 3] = 255;
|
|
166
|
+
} else if (format === 'bc5') {
|
|
167
|
+
decodeAlphaBlock(bytes, at, out, 4, 0);
|
|
168
|
+
decodeAlphaBlock(bytes, at + 8, out, 4, 1);
|
|
169
|
+
for (let t = 0; t < 16; t++) out[t * 4 + 3] = 255;
|
|
170
|
+
} else {
|
|
171
|
+
decodeBc7Block(bytes, at, out);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* A whole surface of blocks into RGBA, eight bits a channel, `width` by `height`. A surface that is
|
|
177
|
+
* not a whole number of blocks is decoded block by block and cropped, which is what the format does:
|
|
178
|
+
* the blocks cover a padded rectangle and the texels past the declared size are padding.
|
|
179
|
+
*/
|
|
180
|
+
export function decodeBc(
|
|
181
|
+
format: BcFormat,
|
|
182
|
+
width: number,
|
|
183
|
+
height: number,
|
|
184
|
+
blocks: Uint8Array,
|
|
185
|
+
out: Uint8Array = new Uint8Array(width * height * 4),
|
|
186
|
+
): Uint8Array {
|
|
187
|
+
const blockBytes = BC_BLOCK_BYTES[format];
|
|
188
|
+
const across = Math.max(1, Math.ceil(width / 4));
|
|
189
|
+
const down = Math.max(1, Math.ceil(height / 4));
|
|
190
|
+
const needed = across * down * blockBytes;
|
|
191
|
+
if (blocks.length < needed) {
|
|
192
|
+
throw new DrftError(
|
|
193
|
+
`bc: a ${width}x${height} ${format.toUpperCase()} surface needs ${needed} bytes of blocks and ` +
|
|
194
|
+
`${blocks.length} were given`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
const texels = TEXELS;
|
|
198
|
+
for (let by = 0; by < down; by++) {
|
|
199
|
+
for (let bx = 0; bx < across; bx++) {
|
|
200
|
+
decodeBlock(format, blocks, (by * across + bx) * blockBytes, texels);
|
|
201
|
+
for (let ty = 0; ty < 4; ty++) {
|
|
202
|
+
const y = by * 4 + ty;
|
|
203
|
+
if (y >= height) break;
|
|
204
|
+
for (let tx = 0; tx < 4; tx++) {
|
|
205
|
+
const x = bx * 4 + tx;
|
|
206
|
+
if (x >= width) break;
|
|
207
|
+
const from = (ty * 4 + tx) * 4;
|
|
208
|
+
const to = (y * width + x) * 4;
|
|
209
|
+
out[to] = texels[from] as number;
|
|
210
|
+
out[to + 1] = texels[from + 1] as number;
|
|
211
|
+
out[to + 2] = texels[from + 2] as number;
|
|
212
|
+
out[to + 3] = texels[from + 3] as number;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return out;
|
|
218
|
+
}
|
package/src/bcFlip.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A BC5 normal map's green turned over in its blocks — `255 − g` at every texel — without decoding.
|
|
3
|
+
*
|
|
4
|
+
* **Why in the blocks.** A DirectX normal map points green down where glTF points it up, and the
|
|
5
|
+
* baker turns such a map over on the way in. For a PNG that is a pass over the texels; for BC5 the
|
|
6
|
+
* same pass would mean decoding and re-encoding, and this engine has no encoder — so the map would
|
|
7
|
+
* leave the bake uncompressed, and BC5 is what normal maps are stored as. Turning a BC4 half over
|
|
8
|
+
* is exact in its own terms instead: swap the two endpoints and turn each over, then renumber the
|
|
9
|
+
* indices so each texel names the value that is now its opposite.
|
|
10
|
+
*
|
|
11
|
+
* **Swapping keeps the mode**, which is the point of swapping: `a0 > a1` selects six interpolants
|
|
12
|
+
* and `a0 ≤ a1` four plus a hard 0 and 255, and `255 − a1 > 255 − a0` exactly when `a0 > a1`.
|
|
13
|
+
* Within a mode, index `i` of the interpolants counts from the first endpoint, so it becomes the
|
|
14
|
+
* one counting the same distance from the other end; the hard 0 and 255 trade places.
|
|
15
|
+
*
|
|
16
|
+
* **What it gives up**: an interpolant that falls on a half rounds the same way both before and
|
|
17
|
+
* after, so a texel at such a value lands one away from `255 − g`. That is inside the error the
|
|
18
|
+
* format already carries. Runs in the baker, so it copies.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Six-value mode: endpoints trade places, interpolant `i` becomes `9 − i`. */
|
|
22
|
+
const SIX = [1, 0, 7, 6, 5, 4, 3, 2];
|
|
23
|
+
/** Four-value mode: endpoints trade, interpolant `i` becomes `7 − i`, and 0 and 255 trade. */
|
|
24
|
+
const FOUR = [1, 0, 5, 4, 3, 2, 7, 6];
|
|
25
|
+
|
|
26
|
+
/** A copy of `blocks`, a level of BC5, with every block's green half turned over. */
|
|
27
|
+
export function flipBc5Green(blocks: Uint8Array): Uint8Array {
|
|
28
|
+
const out = new Uint8Array(blocks);
|
|
29
|
+
for (let at = 8; at + 8 <= out.length; at += 16) flipHalf(out, at);
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function flipHalf(bytes: Uint8Array, at: number): void {
|
|
34
|
+
const a0 = bytes[at] as number;
|
|
35
|
+
const a1 = bytes[at + 1] as number;
|
|
36
|
+
const map = a0 > a1 ? SIX : FOUR;
|
|
37
|
+
bytes[at] = 255 - a1;
|
|
38
|
+
bytes[at + 1] = 255 - a0;
|
|
39
|
+
/* Sixteen three-bit indices in two 24-bit runs, least significant first. */
|
|
40
|
+
for (let half = 0; half < 2; half++) {
|
|
41
|
+
const base = at + 2 + half * 3;
|
|
42
|
+
let run =
|
|
43
|
+
(bytes[base] as number) |
|
|
44
|
+
((bytes[base + 1] as number) << 8) |
|
|
45
|
+
((bytes[base + 2] as number) << 16);
|
|
46
|
+
let flipped = 0;
|
|
47
|
+
for (let texel = 0; texel < 8; texel++) {
|
|
48
|
+
flipped |= (map[run & 7] as number) << (texel * 3);
|
|
49
|
+
run >>>= 3;
|
|
50
|
+
}
|
|
51
|
+
bytes[base] = flipped & 0xff;
|
|
52
|
+
bytes[base + 1] = (flipped >> 8) & 0xff;
|
|
53
|
+
bytes[base + 2] = (flipped >> 16) & 0xff;
|
|
54
|
+
}
|
|
55
|
+
}
|