@driftengine/assets 3.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +9 -0
  4. package/dist/assetPath.d.ts +51 -0
  5. package/dist/assetPath.js +59 -0
  6. package/dist/coarseLevel.d.ts +94 -0
  7. package/dist/coarseLevel.js +606 -0
  8. package/dist/dds.d.ts +41 -0
  9. package/dist/dds.js +425 -0
  10. package/dist/drawKey.d.ts +64 -0
  11. package/dist/drawKey.js +42 -0
  12. package/dist/drftLoader.d.ts +420 -0
  13. package/dist/drftLoader.js +996 -0
  14. package/dist/drftTextures.d.ts +87 -0
  15. package/dist/drftTextures.js +138 -0
  16. package/dist/fbx.d.ts +86 -0
  17. package/dist/fbx.js +792 -0
  18. package/dist/fbxHarness.d.ts +68 -0
  19. package/dist/fbxHarness.js +185 -0
  20. package/dist/fbxInflate.d.ts +60 -0
  21. package/dist/fbxInflate.js +204 -0
  22. package/dist/fbxMaterial.d.ts +87 -0
  23. package/dist/fbxMaterial.js +345 -0
  24. package/dist/fbxRig.d.ts +130 -0
  25. package/dist/fbxRig.js +601 -0
  26. package/dist/gltf.d.ts +242 -0
  27. package/dist/gltf.js +904 -0
  28. package/dist/gltfAccessor.d.ts +2 -0
  29. package/dist/gltfAccessor.js +106 -0
  30. package/dist/gltfSkin.d.ts +32 -0
  31. package/dist/gltfSkin.js +144 -0
  32. package/dist/iesPhotometry.d.ts +44 -0
  33. package/dist/iesPhotometry.js +94 -0
  34. package/dist/imageInfo.d.ts +31 -0
  35. package/dist/imageInfo.js +119 -0
  36. package/dist/index.d.ts +58 -0
  37. package/dist/index.js +132 -0
  38. package/dist/kn5.d.ts +158 -0
  39. package/dist/kn5.js +557 -0
  40. package/dist/loadProgress.d.ts +118 -0
  41. package/dist/loadProgress.js +1 -0
  42. package/dist/localise.d.ts +35 -0
  43. package/dist/localise.js +253 -0
  44. package/dist/obj.d.ts +54 -0
  45. package/dist/obj.js +335 -0
  46. package/dist/orient.d.ts +104 -0
  47. package/dist/orient.js +241 -0
  48. package/dist/radianceHdr.d.ts +36 -0
  49. package/dist/radianceHdr.js +172 -0
  50. package/dist/readModel.d.ts +153 -0
  51. package/dist/readModel.js +386 -0
  52. package/dist/recognise.d.ts +38 -0
  53. package/dist/recognise.js +97 -0
  54. package/dist/stl.d.ts +22 -0
  55. package/dist/stl.js +131 -0
  56. package/dist/tangentOrder.d.ts +24 -0
  57. package/dist/tangentOrder.js +31 -0
  58. package/dist/threemf.d.ts +46 -0
  59. package/dist/threemf.js +254 -0
  60. package/dist/uploadBudget.d.ts +82 -0
  61. package/dist/uploadBudget.js +86 -0
  62. package/dist/usd.d.ts +49 -0
  63. package/dist/usd.js +542 -0
  64. package/dist/weld.d.ts +44 -0
  65. package/dist/weld.js +237 -0
  66. package/dist/zip.d.ts +35 -0
  67. package/dist/zip.js +110 -0
  68. package/package.json +61 -0
  69. package/src/assetPath.ts +79 -0
  70. package/src/coarseLevel.ts +720 -0
  71. package/src/dds.ts +507 -0
  72. package/src/drawKey.ts +91 -0
  73. package/src/drftLoader.ts +1197 -0
  74. package/src/drftTextures.ts +153 -0
  75. package/src/fbx.ts +934 -0
  76. package/src/fbxHarness.ts +215 -0
  77. package/src/fbxInflate.ts +258 -0
  78. package/src/fbxMaterial.ts +410 -0
  79. package/src/fbxRig.ts +652 -0
  80. package/src/gltf.ts +1161 -0
  81. package/src/gltfAccessor.ts +127 -0
  82. package/src/gltfSkin.ts +210 -0
  83. package/src/iesPhotometry.ts +136 -0
  84. package/src/imageInfo.ts +130 -0
  85. package/src/index.ts +162 -0
  86. package/src/kn5.ts +705 -0
  87. package/src/loadProgress.ts +131 -0
  88. package/src/localise.ts +275 -0
  89. package/src/obj.ts +386 -0
  90. package/src/orient.ts +262 -0
  91. package/src/radianceHdr.ts +195 -0
  92. package/src/readModel.ts +515 -0
  93. package/src/recognise.ts +124 -0
  94. package/src/stl.ts +147 -0
  95. package/src/tangentOrder.ts +32 -0
  96. package/src/threemf.ts +303 -0
  97. package/src/uploadBudget.ts +90 -0
  98. package/src/usd.ts +642 -0
  99. package/src/weld.ts +264 -0
  100. package/src/zip.ts +130 -0
package/dist/dds.js ADDED
@@ -0,0 +1,425 @@
1
+ /**
2
+ * DDS surfaces to RGBA: BC1, BC2, BC3 and BC5 in either header spelling, and the uncompressed
3
+ * layouts, by the masks their own header carries.
4
+ *
5
+ * **It exists because a third of a shipped vehicle's textures are in it.** The car this was built
6
+ * against carries 81 textures and 34 of them are DDS — 25 DXT5 and 9 DXT1 — which are its paint,
7
+ * its interior and most of its normal maps. `imageInfo.ts` identifies PNG, JPEG and WEBP, so
8
+ * without this the model imports correctly shaped and visibly half-painted.
9
+ *
10
+ * **It runs in the baker and never in a frame.** RGBA is larger than the compressed source it came
11
+ * from, and the bake re-encodes to whatever the texture pipeline wants; nothing ships a block
12
+ * decoder into a running game. That is what makes the cost acceptable and it is the only reason
13
+ * decoding to RGBA rather than uploading the blocks directly is the right call here.
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.
18
+ *
19
+ * **Both header spellings, as of 3.30.2.** A `DX10` header names the format in a 20-byte extension
20
+ * rather than in the four characters at byte 84, and the blocks behind it are the same blocks. This
21
+ * reader took the classic spelling and refused the other, which cost one consumer 20 textures of a
22
+ * single car — its paint, its wheels, its lamps, its plate and seven interior maps — and cost them
23
+ * as a *lighting* bug rather than as a missing texture: their baker substitutes a 1x1 white pixel,
24
+ * and white in an ORM map is roughness 1 **and** metallic 1, which in the flat shader leaves no
25
+ * 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.
208
+ */
209
+ /** `DDPF_ALPHAPIXELS`, `DDPF_RGB`, `DDPF_LUMINANCE`: what the pixel format says it describes. */
210
+ const DDPF_ALPHAPIXELS = 0x1;
211
+ const DDPF_RGB = 0x40;
212
+ const DDPF_LUMINANCE = 0x20000;
213
+ /** `DDSD_PITCH`: the header's row stride is meant to be read rather than derived. */
214
+ const DDSD_PITCH = 0x8;
215
+ /**
216
+ * One channel's mask turned into the shift and the scale that read it.
217
+ *
218
+ * **Scaled by replication rather than by a shift, which is the one place this goes quietly wrong.**
219
+ * Five bits of `0x1F` is white; shifting it left by three gives 248, so a surface that should be
220
+ * pure white comes back very slightly grey on every texel and nothing raises. Multiplying by
221
+ * `255 / (2^bits - 1)` is the same thing replication does and is exact at both ends.
222
+ */
223
+ function channelFromMask(mask) {
224
+ if (mask === 0)
225
+ return null;
226
+ let shift = 0;
227
+ while (((mask >>> shift) & 1) === 0)
228
+ shift += 1;
229
+ let bits = 0;
230
+ for (let at = shift; at < 32 && ((mask >>> at) & 1) === 1; at += 1)
231
+ bits += 1;
232
+ const max = (1 << bits) - 1;
233
+ return { shift, max, scale: 255 / max };
234
+ }
235
+ /** One channel out of a texel, as eight bits. */
236
+ function take(texel, channel) {
237
+ return Math.round(((texel >>> channel.shift) & channel.max) * channel.scale);
238
+ }
239
+ /**
240
+ * An uncompressed surface into RGBA, by the masks its own header carries.
241
+ *
242
+ * **No per-format branch, because the masks are the format.** The six vehicle bundles this was
243
+ * written against carry five shapes between them — 32-bit BGRA and RGBA, 24-bit BGR, and 8- and
244
+ * 16-bit luminance with and without alpha — and one mask walk reads all five. A reader with a case
245
+ * per layout is a reader with a case missing.
246
+ *
247
+ * **A luminance surface fills all three colour channels from the one it has**, which is what
248
+ * `DDPF_LUMINANCE` means: a single-channel image is grey, not red.
249
+ *
250
+ * **A row stride is read where the header declares one.** Rows can be padded, and a decoder that
251
+ * assumes `width * bytes` walks diagonally through a padded surface — an image that is recognisably
252
+ * the right picture and progressively more sheared down the frame, which reads as a bad texture
253
+ * rather than as a bad reader.
254
+ */
255
+ function uncompressedToRgba(bytes, view, width, height, flags) {
256
+ const bits = view.getUint32(88, true);
257
+ if (bits !== 8 && bits !== 16 && bits !== 24 && bits !== 32) {
258
+ throw new DrftError(`dds: this surface is ${bits} bits a texel, and 8, 16, 24 and 32 are what this reader walks.`);
259
+ }
260
+ const bytesPerTexel = bits / 8;
261
+ const luminance = (flags & DDPF_LUMINANCE) !== 0;
262
+ const red = channelFromMask(view.getUint32(92, true));
263
+ const green = luminance ? red : channelFromMask(view.getUint32(96, true));
264
+ const blue = luminance ? red : channelFromMask(view.getUint32(100, true));
265
+ const alpha = (flags & DDPF_ALPHAPIXELS) === 0 ? null : channelFromMask(view.getUint32(104, true));
266
+ if (red === null) {
267
+ throw new DrftError('dds: this uncompressed surface declares no channel mask, so nothing says where its ' +
268
+ 'colours are. Convert it to PNG.');
269
+ }
270
+ const declaredPitch = view.getUint32(20, true);
271
+ const stride = (view.getUint32(8, true) & DDSD_PITCH) !== 0 && declaredPitch >= width * bytesPerTexel
272
+ ? declaredPitch
273
+ : width * bytesPerTexel;
274
+ const needed = stride * (height - 1) + width * bytesPerTexel;
275
+ if (bytes.length - HEADER_BYTES < needed) {
276
+ throw new DrftError(`dds: the surface is short — ${width}x${height} at ${bits} bits a texel needs ${needed} ` +
277
+ `bytes and ${bytes.length - HEADER_BYTES} follow the header.`);
278
+ }
279
+ const rgba = new Uint8Array(width * height * 4);
280
+ for (let y = 0; y < height; y++) {
281
+ for (let x = 0; x < width; x++) {
282
+ const at = HEADER_BYTES + y * stride + x * bytesPerTexel;
283
+ /* Little-endian, however many bytes wide, so one expression reads all four widths. */
284
+ let texel = 0;
285
+ for (let b = 0; b < bytesPerTexel; b++)
286
+ texel |= bytes[at + b] << (b * 8);
287
+ texel >>>= 0;
288
+ const to = (y * width + x) * 4;
289
+ rgba[to] = take(texel, red);
290
+ rgba[to + 1] = green === null ? 0 : take(texel, green);
291
+ rgba[to + 2] = blue === null ? 0 : take(texel, blue);
292
+ rgba[to + 3] = alpha === null ? 255 : take(texel, alpha);
293
+ }
294
+ }
295
+ return { width, height, rgba };
296
+ }
297
+ /**
298
+ * Which blocks the file holds, and where they start.
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.
305
+ *
306
+ * **What a `DX10` header can say that a reader must refuse.** `resourceDimension` other than 3
307
+ * (`TEXTURE2D`), `miscFlag` bit 2 (a cube map) or an `arraySize` above 1. The classic header has no
308
+ * way to state any of them, so taking one would hand the caller six faces or twenty slices claiming
309
+ * to be a single surface — a picture rather than an error.
310
+ */
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
+ export function ddsToRgba(bytes) {
346
+ if (!isDds(bytes))
347
+ throw new DrftError('dds: these bytes do not begin "DDS "');
348
+ if (bytes.length < HEADER_BYTES)
349
+ throw new DrftError('dds: the file is shorter than its own header');
350
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
351
+ const height = view.getUint32(12, true);
352
+ const width = view.getUint32(16, true);
353
+ const pixelFlags = view.getUint32(80, true);
354
+ const fourcc = String.fromCharCode(bytes[84], bytes[85], bytes[86], bytes[87]);
355
+ /*
356
+ * A pixel format that describes channels rather than naming a compression. There is no block to
357
+ * decode, so it is a bit count, four masks and a copy — and it is 289 of one consumer's 766
358
+ * surfaces, which is why it stopped being worth refusing.
359
+ */
360
+ if ((pixelFlags & DDPF_FOURCC) === 0) {
361
+ if ((pixelFlags & (DDPF_RGB | DDPF_LUMINANCE)) === 0) {
362
+ throw new DrftError(`dds: this surface's pixel format declares flags ${pixelFlags} — neither a compression, ` +
363
+ 'nor RGB, nor luminance — so nothing says what its bytes hold. Convert it to PNG.');
364
+ }
365
+ return uncompressedToRgba(bytes, view, width, height, pixelFlags);
366
+ }
367
+ const { format, dataAt } = resolveSurface(bytes, view, fourcc);
368
+ const blockBytes = format === 'bc1' ? 8 : 16;
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;
372
+ if (bytes.length - dataAt < needed) {
373
+ throw new DrftError(`dds: the surface is short — ${width}x${height} of ${fourcc} needs ${needed} bytes and ` +
374
+ `${bytes.length - dataAt} follow the header.`);
375
+ }
376
+ const rgba = new Uint8Array(width * height * 4);
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 };
425
+ }
@@ -0,0 +1,64 @@
1
+ import type { DrftMaterial } from '@driftengine/drft';
2
+ /**
3
+ * What decides whether two parts of one model become a single draw call.
4
+ *
5
+ * **This lives here rather than inside `DrftLoader` because a consumer needs it and was copying
6
+ * it.** The loader merges parts whose material would set the same GPU state, and a baker that
7
+ * reports how many draws a baked part will cost has to ask the same question the loader asks. One
8
+ * consumer answered it by rebuilding the key by hand, field for field, with a comment saying so —
9
+ * and that copy went stale the day `cutout` joined the key in 3.26.0, which is a defect nothing
10
+ * could see: a stale key does not throw, it under-counts.
11
+ *
12
+ * So the loader and anyone counting its draws now call the same function. A field added here is
13
+ * added for both, and the copy that could drift no longer exists.
14
+ */
15
+ /** The override `DrftLoaderOptions.surface` may return for a material. */
16
+ export interface DrawSurfaceOverride {
17
+ readonly opacity?: number;
18
+ readonly reflectivity?: number;
19
+ }
20
+ /**
21
+ * The fields a merged group carries, resolved from a material and its override.
22
+ *
23
+ * Named for what they are on a group rather than for what they are called on a material —
24
+ * `ormMap` is `orm` here — because these are the values the renderer is handed, and the group is
25
+ * what holds them between the loader and the draw.
26
+ */
27
+ export interface DrawGrouping {
28
+ readonly albedo: number;
29
+ readonly orm: number;
30
+ readonly normal: number;
31
+ readonly emissive: number;
32
+ readonly opacity: number;
33
+ readonly reflectivity: number;
34
+ readonly roughnessScale: number;
35
+ readonly metallicScale: number;
36
+ readonly occlusionStrength: number;
37
+ readonly cutout: number;
38
+ }
39
+ /**
40
+ * Resolve what a part will actually be drawn with.
41
+ *
42
+ * **The defaults are the loader's and are not arbitrary.** A missing texture index is `-1` rather
43
+ * than `0`, because `0` is a real image. Opacity and the two scales default to 1 and the rest to
44
+ * 0, which is the identity that lets a file saying nothing about a property draw the same as one
45
+ * that did not have the property to say.
46
+ *
47
+ * The override wins over the material for the two fields it may carry, because a consumer dressing
48
+ * a surface by name knows more than a default an exporter wrote. A cutout is not among them: it is
49
+ * a fact about the *image* a material wears, so nothing outside the file has anything to say
50
+ * about it.
51
+ */
52
+ export declare function resolveDrawGrouping(material: DrftMaterial | undefined, override?: DrawSurfaceOverride): DrawGrouping;
53
+ /**
54
+ * The key two parts must share to be merged into one draw.
55
+ *
56
+ * Grouped by opacity and reflectivity as well as by image, because two surfaces can share a map
57
+ * and differ in blend: merging on the image alone means one cannot be made translucent without
58
+ * taking the other with it. **And by the ORM map and its three scales**, for the same reason one
59
+ * step further out — two surfaces sharing a colour map and differing in roughness scale are not
60
+ * one draw, and merging them would silently give one of them the other's material.
61
+ *
62
+ * A string because it is a `Map` key and nothing reads it back apart.
63
+ */
64
+ export declare function drawKeyOf(material: DrftMaterial | undefined, override?: DrawSurfaceOverride): string;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Resolve what a part will actually be drawn with.
3
+ *
4
+ * **The defaults are the loader's and are not arbitrary.** A missing texture index is `-1` rather
5
+ * than `0`, because `0` is a real image. Opacity and the two scales default to 1 and the rest to
6
+ * 0, which is the identity that lets a file saying nothing about a property draw the same as one
7
+ * that did not have the property to say.
8
+ *
9
+ * The override wins over the material for the two fields it may carry, because a consumer dressing
10
+ * a surface by name knows more than a default an exporter wrote. A cutout is not among them: it is
11
+ * a fact about the *image* a material wears, so nothing outside the file has anything to say
12
+ * about it.
13
+ */
14
+ export function resolveDrawGrouping(material, override) {
15
+ return {
16
+ albedo: material?.albedo ?? -1,
17
+ orm: material?.ormMap ?? -1,
18
+ normal: material?.normalMap ?? -1,
19
+ emissive: material?.emissiveMap ?? -1,
20
+ opacity: override?.opacity ?? material?.opacity ?? 1,
21
+ reflectivity: override?.reflectivity ?? material?.reflectivity ?? 0,
22
+ roughnessScale: material?.roughnessScale ?? 1,
23
+ metallicScale: material?.metallicScale ?? 1,
24
+ occlusionStrength: material?.occlusionStrength ?? 0,
25
+ cutout: material?.cutout ?? 0,
26
+ };
27
+ }
28
+ /**
29
+ * The key two parts must share to be merged into one draw.
30
+ *
31
+ * Grouped by opacity and reflectivity as well as by image, because two surfaces can share a map
32
+ * and differ in blend: merging on the image alone means one cannot be made translucent without
33
+ * taking the other with it. **And by the ORM map and its three scales**, for the same reason one
34
+ * step further out — two surfaces sharing a colour map and differing in roughness scale are not
35
+ * one draw, and merging them would silently give one of them the other's material.
36
+ *
37
+ * A string because it is a `Map` key and nothing reads it back apart.
38
+ */
39
+ export function drawKeyOf(material, override) {
40
+ const g = resolveDrawGrouping(material, override);
41
+ return `${g.albedo}:${g.orm}:${g.normal}:${g.emissive}:${g.opacity}:${g.reflectivity}:${g.roughnessScale}:${g.metallicScale}:${g.occlusionStrength}:${g.cutout}`;
42
+ }