@driftengine/drft 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.
@@ -0,0 +1,23 @@
1
+ import type { BcFormat } from './drftFormat.ts';
2
+ /** A block-compressed image, as a `CODEC_BC` payload holds it. */
3
+ export interface BcImage {
4
+ readonly format: BcFormat;
5
+ /**
6
+ * What the author's header said of its texels. How a material slot samples them is still the
7
+ * renderer's decision, as for every other codec: a colour map is read as sRGB and a normal map as
8
+ * linear whatever its file claimed, so this is the author's statement rather than an instruction.
9
+ */
10
+ readonly srgb: boolean;
11
+ readonly width: number;
12
+ readonly height: number;
13
+ /** Level 0 first. Views over the payload when read, never copies. */
14
+ readonly levels: readonly Uint8Array[];
15
+ }
16
+ /** How many levels a full chain from `width` by `height` down to 1x1 has. */
17
+ export declare function bcChainLength(width: number, height: number): number;
18
+ /** The bytes level `level` of a `width` by `height` surface takes in `format`. */
19
+ export declare function bcLevelBytes(format: BcFormat, width: number, height: number, level: number): number;
20
+ /** The payload for `image`. Refuses a level whose size is not the one its position requires. */
21
+ export declare function writeBcPayload(image: BcImage): Uint8Array;
22
+ /** A payload read back, its levels as views over `bytes`. */
23
+ export declare function readBcPayload(width: number, height: number, bytes: Uint8Array): BcImage;
package/dist/drftBc.js ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * A `CODEC_BC` texture payload: which block format, what colour space its author declared, and
3
+ * every stored mip level, largest first.
4
+ *
5
+ * **The layout** (FORMAT.md §4.5, 1.24), little-endian after the `TEXS` prefix that already carries
6
+ * the codec, width and height:
7
+ *
8
+ * u32 format the format's own digit: 1, 2, 3, 4, 5 or 7 for BC1 to BC7
9
+ * u32 flags bit 0: the author declared the texels sRGB
10
+ * u32 levels how many mip levels follow, 1 to the full chain
11
+ * u32 0 reserved
12
+ * ... each level's blocks, back to back, largest first
13
+ *
14
+ * **A level's size is derived, never stored.** Level `i` is `max(1, width >> i)` by
15
+ * `max(1, height >> i)` texels, rounded up to whole 4x4 blocks — so a level smaller than a block
16
+ * still costs one, which is how every DDS writer and every GPU lays them out. Storing the sizes
17
+ * would be a second statement of the same fact, free to disagree with the first.
18
+ *
19
+ * **What it gives up**: a chain is whole levels from the top. A file that wanted to drop its top
20
+ * level to save bytes has to say a smaller width and height, which is also what it then is.
21
+ *
22
+ * Unknown flag bits are ignored, which is rule 4 of the compatibility contract; a format number
23
+ * this reader does not know is refused by name. A signed or HDR format would be a new number rather
24
+ * than a flag, because a decoder that ignored it would produce wrong values rather than none.
25
+ */
26
+ import { BC_BLOCK_BYTES, DrftError } from './drftFormat.js';
27
+ const HEADER_BYTES = 16;
28
+ const FLAG_SRGB = 1;
29
+ const FORMAT_NUMBER = {
30
+ bc1: 1,
31
+ bc2: 2,
32
+ bc3: 3,
33
+ bc4: 4,
34
+ bc5: 5,
35
+ bc7: 7,
36
+ };
37
+ const FORMAT_OF = {
38
+ 1: 'bc1',
39
+ 2: 'bc2',
40
+ 3: 'bc3',
41
+ 4: 'bc4',
42
+ 5: 'bc5',
43
+ 7: 'bc7',
44
+ };
45
+ /** How many levels a full chain from `width` by `height` down to 1x1 has. */
46
+ export function bcChainLength(width, height) {
47
+ return Math.floor(Math.log2(Math.max(1, width, height))) + 1;
48
+ }
49
+ /** The bytes level `level` of a `width` by `height` surface takes in `format`. */
50
+ export function bcLevelBytes(format, width, height, level) {
51
+ const w = Math.max(1, width >> level);
52
+ const h = Math.max(1, height >> level);
53
+ return Math.ceil(w / 4) * Math.ceil(h / 4) * BC_BLOCK_BYTES[format];
54
+ }
55
+ /** The payload for `image`. Refuses a level whose size is not the one its position requires. */
56
+ export function writeBcPayload(image) {
57
+ const { format, width, height, levels } = image;
58
+ checkChain(levels.length, width, height);
59
+ let total = HEADER_BYTES;
60
+ for (let i = 0; i < levels.length; i++) {
61
+ const want = bcLevelBytes(format, width, height, i);
62
+ const level = levels[i];
63
+ if (level.length !== want) {
64
+ throw new DrftError(`bc: level ${i} of a ${width}x${height} ${format.toUpperCase()} image is ${want} bytes, and ` +
65
+ `${level.length} were given`);
66
+ }
67
+ total += want;
68
+ }
69
+ const out = new Uint8Array(total);
70
+ const view = new DataView(out.buffer);
71
+ view.setUint32(0, FORMAT_NUMBER[format], true);
72
+ view.setUint32(4, image.srgb ? FLAG_SRGB : 0, true);
73
+ view.setUint32(8, levels.length, true);
74
+ let at = HEADER_BYTES;
75
+ for (const level of levels) {
76
+ out.set(level, at);
77
+ at += level.length;
78
+ }
79
+ return out;
80
+ }
81
+ /** A payload read back, its levels as views over `bytes`. */
82
+ export function readBcPayload(width, height, bytes) {
83
+ if (bytes.length < HEADER_BYTES) {
84
+ throw new DrftError(`bc: a payload of ${bytes.length} bytes is shorter than its header`);
85
+ }
86
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
87
+ const number = view.getUint32(0, true);
88
+ const format = FORMAT_OF[number];
89
+ if (format === undefined) {
90
+ throw new DrftError(`bc: block format ${number} is not one this reader knows; it reads 1, 2, 3, 4, 5 and 7 ` +
91
+ '(BC1 to BC5 and BC7)');
92
+ }
93
+ const count = view.getUint32(8, true);
94
+ checkChain(count, width, height);
95
+ let needed = HEADER_BYTES;
96
+ for (let i = 0; i < count; i++)
97
+ needed += bcLevelBytes(format, width, height, i);
98
+ if (bytes.length < needed) {
99
+ throw new DrftError(`bc: ${count} levels of a ${width}x${height} ${format.toUpperCase()} image needs ` +
100
+ `${needed} bytes and the payload is ${bytes.length}`);
101
+ }
102
+ const levels = [];
103
+ let at = HEADER_BYTES;
104
+ for (let i = 0; i < count; i++) {
105
+ const size = bcLevelBytes(format, width, height, i);
106
+ levels.push(bytes.subarray(at, at + size));
107
+ at += size;
108
+ }
109
+ return { format, srgb: (view.getUint32(4, true) & FLAG_SRGB) !== 0, width, height, levels };
110
+ }
111
+ function checkChain(count, width, height) {
112
+ const most = bcChainLength(width, height);
113
+ if (width < 1 || height < 1 || count < 1 || count > most) {
114
+ throw new DrftError(`bc: ${count} levels for a ${width}x${height} image, which has at least 1 and at most ${most}`);
115
+ }
116
+ }
@@ -20,7 +20,7 @@ export declare const DRFT_VERSION_MAJOR = 1;
20
20
  * Additive revisions within a generation: new optional chunks, new attribute bits, new
21
21
  * enum values with a defined fallback. Never a changed meaning.
22
22
  */
23
- export declare const DRFT_VERSION_MINOR = 23;
23
+ export declare const DRFT_VERSION_MINOR = 24;
24
24
  /** Bytes before the chunk table. */
25
25
  export declare const HEADER_BYTES = 32;
26
26
  /** Bytes per chunk table entry. */
@@ -365,6 +365,20 @@ export declare const ATTR_LAYERS: number;
365
365
  * before this one, and draws the mesh planted, sunlit and opaque — the absent lane's own meaning.
366
366
  */
367
367
  export declare const ATTR_CHANNEL: number;
368
+ /**
369
+ * Added in 1.24: a vertex's fifth to eighth influences, which joints and how much. See
370
+ * `MeshData.joints2`.
371
+ *
372
+ * Two bits for two arrays, both-or-neither like the first pair, and neither without the first pair.
373
+ * Appended after the channel, as every attribute since 1.20 has been, and that has a consequence
374
+ * worth stating rather than implying: indices follow the last array, so **a 1.23 reader does not
375
+ * skip these** — it reads their floats where it expects indices, finds indices far past the vertex
376
+ * count, and refuses the mesh by name. A loud refusal rather than a mesh drawn with four of its
377
+ * eight influences, which is the same answer `layers` and the channel give an older reader.
378
+ */
379
+ export declare const ATTR_JOINTS2: number;
380
+ /** How much each of `ATTR_JOINTS2`' four influences moves the vertex. */
381
+ export declare const ATTR_WEIGHTS2: number;
368
382
  /** Everything this version defines, so a reader can spot bits from a later writer. */
369
383
  export declare const ATTR_KNOWN: number;
370
384
  /**
@@ -380,6 +394,28 @@ export declare const CODEC_PNG = 1;
380
394
  export declare const CODEC_JPEG = 2;
381
395
  export declare const CODEC_WEBP = 3;
382
396
  export declare const CODEC_RAW = 4;
397
+ /**
398
+ * Added in 1.24: block-compressed images, kept as the blocks they were authored in.
399
+ *
400
+ * **Uploaded as they are where the device can sample them**, so a texture authored as BC7 costs a
401
+ * byte a texel on the GPU rather than four, and the mip chain the author built rather than one the
402
+ * engine generates. Where the device cannot — most phones, which sample ASTC and ETC2 instead — the
403
+ * loader decodes them to RGBA, which is the cost every texture paid before this. The payload — block
404
+ * format, colour space and the stored mip chain — is `drftBc.ts`'s.
405
+ */
406
+ export declare const CODEC_BC = 5;
407
+ /**
408
+ * The block formats a `CODEC_BC` payload may hold: what one 4x4 block of texels encodes.
409
+ *
410
+ * BC1 is colour with punch-through alpha in eight bytes; BC2 and BC3 colour with explicit and
411
+ * interpolated alpha in sixteen; BC4 one channel and BC5 two (a normal map) in eight and sixteen;
412
+ * BC7 colour and alpha in sixteen, with eight modes chosen per block. BC6H, signed BC4/BC5 and the
413
+ * other families are not among them: an HDR or signed texture is a different contract, refused by
414
+ * name rather than mis-decoded.
415
+ */
416
+ export type BcFormat = 'bc1' | 'bc2' | 'bc3' | 'bc4' | 'bc5' | 'bc7';
417
+ /** Bytes per 4x4 block of each format. */
418
+ export declare const BC_BLOCK_BYTES: Readonly<Record<BcFormat, number>>;
383
419
  /** What a codec is called, for a message naming what was not understood. */
384
420
  export declare function codecName(codec: number): string;
385
421
  /**
@@ -20,7 +20,7 @@ export const DRFT_VERSION_MAJOR = 1;
20
20
  * Additive revisions within a generation: new optional chunks, new attribute bits, new
21
21
  * enum values with a defined fallback. Never a changed meaning.
22
22
  */
23
- export const DRFT_VERSION_MINOR = 23;
23
+ export const DRFT_VERSION_MINOR = 24;
24
24
  /** Bytes before the chunk table. */
25
25
  export const HEADER_BYTES = 32;
26
26
  /** Bytes per chunk table entry. */
@@ -406,6 +406,20 @@ export const ATTR_LAYERS = 1 << 9;
406
406
  * before this one, and draws the mesh planted, sunlit and opaque — the absent lane's own meaning.
407
407
  */
408
408
  export const ATTR_CHANNEL = 1 << 10;
409
+ /**
410
+ * Added in 1.24: a vertex's fifth to eighth influences, which joints and how much. See
411
+ * `MeshData.joints2`.
412
+ *
413
+ * Two bits for two arrays, both-or-neither like the first pair, and neither without the first pair.
414
+ * Appended after the channel, as every attribute since 1.20 has been, and that has a consequence
415
+ * worth stating rather than implying: indices follow the last array, so **a 1.23 reader does not
416
+ * skip these** — it reads their floats where it expects indices, finds indices far past the vertex
417
+ * count, and refuses the mesh by name. A loud refusal rather than a mesh drawn with four of its
418
+ * eight influences, which is the same answer `layers` and the channel give an older reader.
419
+ */
420
+ export const ATTR_JOINTS2 = 1 << 11;
421
+ /** How much each of `ATTR_JOINTS2`' four influences moves the vertex. */
422
+ export const ATTR_WEIGHTS2 = 1 << 12;
409
423
  /** Everything this version defines, so a reader can spot bits from a later writer. */
410
424
  export const ATTR_KNOWN = ATTR_SPECULAR |
411
425
  ATTR_UVS |
@@ -417,7 +431,9 @@ export const ATTR_KNOWN = ATTR_SPECULAR |
417
431
  ATTR_JOINTS |
418
432
  ATTR_WEIGHTS |
419
433
  ATTR_LAYERS |
420
- ATTR_CHANNEL;
434
+ ATTR_CHANNEL |
435
+ ATTR_JOINTS2 |
436
+ ATTR_WEIGHTS2;
421
437
  /**
422
438
  * How a `TEXS` payload is encoded.
423
439
  *
@@ -431,6 +447,25 @@ export const CODEC_PNG = 1;
431
447
  export const CODEC_JPEG = 2;
432
448
  export const CODEC_WEBP = 3;
433
449
  export const CODEC_RAW = 4;
450
+ /**
451
+ * Added in 1.24: block-compressed images, kept as the blocks they were authored in.
452
+ *
453
+ * **Uploaded as they are where the device can sample them**, so a texture authored as BC7 costs a
454
+ * byte a texel on the GPU rather than four, and the mip chain the author built rather than one the
455
+ * engine generates. Where the device cannot — most phones, which sample ASTC and ETC2 instead — the
456
+ * loader decodes them to RGBA, which is the cost every texture paid before this. The payload — block
457
+ * format, colour space and the stored mip chain — is `drftBc.ts`'s.
458
+ */
459
+ export const CODEC_BC = 5;
460
+ /** Bytes per 4x4 block of each format. */
461
+ export const BC_BLOCK_BYTES = Object.freeze({
462
+ bc1: 8,
463
+ bc2: 16,
464
+ bc3: 16,
465
+ bc4: 8,
466
+ bc5: 16,
467
+ bc7: 16,
468
+ });
434
469
  /** What a codec is called, for a message naming what was not understood. */
435
470
  export function codecName(codec) {
436
471
  return codec === CODEC_PNG
@@ -441,7 +476,9 @@ export function codecName(codec) {
441
476
  ? 'WEBP'
442
477
  : codec === CODEC_RAW
443
478
  ? 'RAW'
444
- : `codec ${codec}`;
479
+ : codec === CODEC_BC
480
+ ? 'BC'
481
+ : `codec ${codec}`;
445
482
  }
446
483
  /** Floats in a `MATL` entry, then the signed texture indices that follow them. */
447
484
  export const MATERIAL_FLOATS = 20;
@@ -23,7 +23,7 @@
23
23
  * compares vertices bit for bit, or one that cannot afford the decode's copy; `writeDrft`'s
24
24
  * `quantise: false` writes `MESH` as before.
25
25
  */
26
- import { ATTR_EMISSIVE_COLOR, ATTR_GRAIN, ATTR_JOINTS, ATTR_RELIEF, ATTR_ROUGHNESS, ATTR_SPECULAR, ATTR_TANGENT, ATTR_UVS, ATTR_WEIGHTS, ATTR_LAYERS, ATTR_CHANNEL, DrftError, align, } from './drftFormat.js';
26
+ import { ATTR_EMISSIVE_COLOR, ATTR_GRAIN, ATTR_JOINTS, ATTR_JOINTS2, ATTR_WEIGHTS2, ATTR_RELIEF, ATTR_ROUGHNESS, ATTR_SPECULAR, ATTR_TANGENT, ATTR_UVS, ATTR_WEIGHTS, ATTR_LAYERS, ATTR_CHANNEL, DrftError, align, } from './drftFormat.js';
27
27
  const RAW = 0;
28
28
  const CONSTANT = 1;
29
29
  const RANGE16 = 2;
@@ -49,6 +49,8 @@ const ORDER = [
49
49
  /* Whole numbers naming an image, so exact: a layer quantised onto its neighbour is another picture. */
50
50
  { name: 'layers', width: 1, bit: ATTR_LAYERS, kind: 'exact' },
51
51
  { name: 'channel', width: 4, bit: ATTR_CHANNEL, kind: 'value' },
52
+ { name: 'joints2', width: 4, bit: ATTR_JOINTS2, kind: 'exact' },
53
+ { name: 'weights2', width: 4, bit: ATTR_WEIGHTS2, kind: 'value' },
52
54
  ];
53
55
  /** A growing little-endian byte list, for the writer: offline, so it may allocate. */
54
56
  class Bytes {
package/dist/drftRead.js CHANGED
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import { readClip, readMorph, readNodes, readSkin } from './drftSkin.js';
18
18
  import { validateMeshData } from './meshData.js';
19
- import { ATTR_EMISSIVE_COLOR, ATTR_GRAIN, ATTR_JOINTS, ATTR_RELIEF, ATTR_TANGENT, ATTR_ROUGHNESS, ATTR_SPECULAR, ATTR_UVS, ATTR_WEIGHTS, ATTR_LAYERS, ATTR_CHANNEL, CHUNK_ENTRY_BYTES, CHUNK_HEAD, CHUNK_LODM, CHUNK_LODF, CHUNK_DTEX, CHUNK_ENTS, CHUNK_NAVM, CHUNK_SPLT, SPLAT_BLOCK_PREFIX, CHUNK_MESH, CHUNK_REQUIRED, DRFT_MAGIC, DRFT_VERSION_MAJOR, DrftError, HEADER_BYTES, CHUNK_MATL, CHUNK_TEXS, KNOWN_CHUNKS, align, fourCCName, CHUNK_ANIM, CHUNK_NODE, CHUNK_INST, CHUNK_REGN, CHUNK_LVOL, CHUNK_LITE, CHUNK_MSHQ, CHUNK_KITS, CHUNK_MSHC, CHUNK_SKIN, CHUNK_MORP, CHUNK_NNET, CHUNK_NGRF, CHUNK_SDFV, CHUNK_SUBS, CHUNK_COLL, } from './drftFormat.js';
19
+ import { ATTR_EMISSIVE_COLOR, ATTR_GRAIN, ATTR_JOINTS, ATTR_RELIEF, ATTR_TANGENT, ATTR_ROUGHNESS, ATTR_SPECULAR, ATTR_UVS, ATTR_WEIGHTS, ATTR_LAYERS, ATTR_CHANNEL, ATTR_JOINTS2, ATTR_WEIGHTS2, CHUNK_ENTRY_BYTES, CHUNK_HEAD, CHUNK_LODM, CHUNK_LODF, CHUNK_DTEX, CHUNK_ENTS, CHUNK_NAVM, CHUNK_SPLT, SPLAT_BLOCK_PREFIX, CHUNK_MESH, CHUNK_REQUIRED, DRFT_MAGIC, DRFT_VERSION_MAJOR, DrftError, HEADER_BYTES, CHUNK_MATL, CHUNK_TEXS, KNOWN_CHUNKS, align, fourCCName, CHUNK_ANIM, CHUNK_NODE, CHUNK_INST, CHUNK_REGN, CHUNK_LVOL, CHUNK_LITE, CHUNK_MSHQ, CHUNK_KITS, CHUNK_MSHC, CHUNK_SKIN, CHUNK_MORP, CHUNK_NNET, CHUNK_NGRF, CHUNK_SDFV, CHUNK_SUBS, CHUNK_COLL, } from './drftFormat.js';
20
20
  import { readSubs } from './drftSubs.js';
21
21
  import { readSdfv } from './sdfv.js';
22
22
  import { readDtex } from './dtex.js';
@@ -104,6 +104,9 @@ export function readMesh(buffer, chunk) {
104
104
  const layers = (attributes & ATTR_LAYERS) !== 0 ? floats(vertices, 'layers') : undefined;
105
105
  /* Sway, sky and opacity, 1.23, four floats a vertex and last. See ATTR_CHANNEL. */
106
106
  const channel = (attributes & ATTR_CHANNEL) !== 0 ? floats(vertices * 4, 'channel') : undefined;
107
+ /* The fifth to eighth influences, 1.24, last. See ATTR_JOINTS2. */
108
+ const joints2 = (attributes & ATTR_JOINTS2) !== 0 ? floats(vertices * 4, 'joints2') : undefined;
109
+ const weights2 = (attributes & ATTR_WEIGHTS2) !== 0 ? floats(vertices * 4, 'weights2') : undefined;
107
110
  const indexBytes = indexCount * 4;
108
111
  if (at + indexBytes > limit) {
109
112
  throw new DrftError(`indices need ${indexBytes} bytes but only ${limit - at} remain at ${at}`);
@@ -126,6 +129,8 @@ export function readMesh(buffer, chunk) {
126
129
  ...(weights === undefined ? {} : { weights }),
127
130
  ...(layers === undefined ? {} : { layers }),
128
131
  ...(channel === undefined ? {} : { channel }),
132
+ ...(joints2 === undefined ? {} : { joints2 }),
133
+ ...(weights2 === undefined ? {} : { weights2 }),
129
134
  };
130
135
  /*
131
136
  * Checked again on the way in, even though the writer checked on the way out. The file
package/dist/drftWrite.js CHANGED
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import { buildClip, buildMorph, buildNodes, buildSkin } from './drftSkin.js';
12
12
  import { validateMeshData } from './meshData.js';
13
- import { ALIGNMENT, ATTR_EMISSIVE_COLOR, ATTR_GRAIN, ATTR_JOINTS, ATTR_RELIEF, ATTR_TANGENT, ATTR_ROUGHNESS, ATTR_SPECULAR, ATTR_UVS, ATTR_WEIGHTS, ATTR_LAYERS, ATTR_CHANNEL, CHUNK_ENTRY_BYTES, CHUNK_HEAD, CHUNK_ANIM, CHUNK_DTEX, CHUNK_ENTS, CHUNK_NAVM, CHUNK_LODM, CHUNK_LODF, CHUNK_MORP, CHUNK_NODE, CHUNK_INST, CHUNK_REGN, CHUNK_LVOL, CHUNK_LITE, CHUNK_MSHQ, CHUNK_KITS, CHUNK_MSHC, CHUNK_SKIN, CHUNK_SPLT, SPLAT_BLOCK_PREFIX, CHUNK_MATL, CHUNK_MESH, CHUNK_REQUIRED, CHUNK_COLL, CHUNK_NNET, CHUNK_NGRF, CHUNK_SDFV, CHUNK_SUBS, CHUNK_TEXS, MATERIAL_ENTRY_BYTES, DRFT_MAGIC, DRFT_VERSION_MAJOR, DRFT_VERSION_MINOR, DrftError, FLAG_LITTLE_ENDIAN, HEADER_BYTES, align, } from './drftFormat.js';
13
+ import { ALIGNMENT, ATTR_EMISSIVE_COLOR, ATTR_GRAIN, ATTR_JOINTS, ATTR_RELIEF, ATTR_TANGENT, ATTR_ROUGHNESS, ATTR_SPECULAR, ATTR_UVS, ATTR_WEIGHTS, ATTR_LAYERS, ATTR_CHANNEL, ATTR_JOINTS2, ATTR_WEIGHTS2, CHUNK_ENTRY_BYTES, CHUNK_HEAD, CHUNK_ANIM, CHUNK_DTEX, CHUNK_ENTS, CHUNK_NAVM, CHUNK_LODM, CHUNK_LODF, CHUNK_MORP, CHUNK_NODE, CHUNK_INST, CHUNK_REGN, CHUNK_LVOL, CHUNK_LITE, CHUNK_MSHQ, CHUNK_KITS, CHUNK_MSHC, CHUNK_SKIN, CHUNK_SPLT, SPLAT_BLOCK_PREFIX, CHUNK_MATL, CHUNK_MESH, CHUNK_REQUIRED, CHUNK_COLL, CHUNK_NNET, CHUNK_NGRF, CHUNK_SDFV, CHUNK_SUBS, CHUNK_TEXS, MATERIAL_ENTRY_BYTES, DRFT_MAGIC, DRFT_VERSION_MAJOR, DRFT_VERSION_MINOR, DrftError, FLAG_LITTLE_ENDIAN, HEADER_BYTES, align, } from './drftFormat.js';
14
14
  import { coarseFirstOrder } from './coarseFirst.js';
15
15
  import { buildSubs } from './drftSubs.js';
16
16
  import { buildSdfv } from './sdfv.js';
@@ -280,6 +280,10 @@ function buildMesh(mesh, code = CHUNK_MESH, flags = CHUNK_REQUIRED) {
280
280
  attributes |= ATTR_LAYERS;
281
281
  if (mesh.channel !== undefined)
282
282
  attributes |= ATTR_CHANNEL;
283
+ if (mesh.joints2 !== undefined)
284
+ attributes |= ATTR_JOINTS2;
285
+ if (mesh.weights2 !== undefined)
286
+ attributes |= ATTR_WEIGHTS2;
283
287
  /* Order is frozen: mandatory arrays, then optional ones by ascending bit. */
284
288
  const arrays = [
285
289
  mesh.positions,
@@ -313,6 +317,11 @@ function buildMesh(mesh, code = CHUNK_MESH, flags = CHUNK_REQUIRED) {
313
317
  /* And newer still, 1.23. See ATTR_CHANNEL. */
314
318
  if (mesh.channel !== undefined)
315
319
  arrays.push(mesh.channel);
320
+ /* And 1.24's second four influences, last. See ATTR_JOINTS2. */
321
+ if (mesh.joints2 !== undefined)
322
+ arrays.push(mesh.joints2);
323
+ if (mesh.weights2 !== undefined)
324
+ arrays.push(mesh.weights2);
316
325
  arrays.push(mesh.indices);
317
326
  const PREFIX = 16;
318
327
  let total = PREFIX;
package/dist/index.d.ts CHANGED
@@ -46,7 +46,10 @@ export type { DrftManifest, DrftStreamHandlers, DrftStreamPacing } from './drftS
46
46
  export { writeDrft } from './drftWrite.ts';
47
47
  export type { DrftSource, DrftTextureSource } from './drftWrite.ts';
48
48
  export { DrftError, DRFT_VERSION_MAJOR, DRFT_VERSION_MINOR } from './drftFormat.ts';
49
- export { CODEC_PNG, CODEC_JPEG, CODEC_WEBP, CODEC_RAW, codecName } from './drftFormat.ts';
49
+ export { CODEC_PNG, CODEC_JPEG, CODEC_WEBP, CODEC_RAW, CODEC_BC, BC_BLOCK_BYTES, codecName, } from './drftFormat.ts';
50
+ export type { BcFormat } from './drftFormat.ts';
51
+ export { bcChainLength, bcLevelBytes, readBcPayload, writeBcPayload } from './drftBc.ts';
52
+ export type { BcImage } from './drftBc.ts';
50
53
  export type { DrftHead, DrftMaterial, DrftSplatBlock, DrftSplats } from './drftFormat.ts';
51
54
  export { coarseFirstOrder } from './coarseFirst.ts';
52
55
  export { MAX_CLUSTERS, buildMeshlets, readMeshlets } from './drftMeshlets.ts';
package/dist/index.js CHANGED
@@ -54,7 +54,8 @@ export { DEFAULT_STREAM_SLICE_MS, DrftStream, streamDrft } from './drftStream.js
54
54
  export { writeDrft } from './drftWrite.js';
55
55
  export { DrftError, DRFT_VERSION_MAJOR, DRFT_VERSION_MINOR } from './drftFormat.js';
56
56
  /* A consumer decoding an embedded texture has to know which codec it is holding. */
57
- export { CODEC_PNG, CODEC_JPEG, CODEC_WEBP, CODEC_RAW, codecName } from './drftFormat.js';
57
+ export { CODEC_PNG, CODEC_JPEG, CODEC_WEBP, CODEC_RAW, CODEC_BC, BC_BLOCK_BYTES, codecName, } from './drftFormat.js';
58
+ export { bcChainLength, bcLevelBytes, readBcPayload, writeBcPayload } from './drftBc.js';
58
59
  /* The order a capture is written in, so a baker outside this package can lay one out the same
59
60
  way — it is the whole reason a capture streams into a recognisable place. */
60
61
  export { coarseFirstOrder } from './coarseFirst.js';
@@ -163,9 +163,12 @@ export interface MeshData {
163
163
  * nothing and uploads nothing. Absent means unskinned, and an unskinned mesh takes exactly the
164
164
  * draw path it took before skinning existed.
165
165
  *
166
- * **Four influences and not eight.** Four is what glTF's `JOINTS_0` carries, what every DCC tool
167
- * exports without being asked, and what fits one attribute. Eight doubles the attribute cost of
168
- * every skinned vertex to serve a case that rarely survives an export.
166
+ * **Four here, and four more in `joints2` for a mesh that has them.** Four is what glTF's
167
+ * `JOINTS_0` carries and what most exports write, and it fits one attribute; a character authored
168
+ * for eight — a face, a shoulder — loses the shape of its deformation when the four lightest are
169
+ * dropped, so 4.8.4 added a second set rather than widening this one. A mesh without it pays
170
+ * nothing for it, and importers sort heaviest first, so this set always holds the four that
171
+ * matter most.
169
172
  *
170
173
  * **`Float32Array` and not `Uint8Array`, which costs twelve bytes a vertex.** Every attribute in
171
174
  * this format is float32 and the absent-attribute mechanism depends on it: `mesh.ts` attaches
@@ -188,6 +191,19 @@ export interface MeshData {
188
191
  * is refused below rather than drawn.
189
192
  */
190
193
  weights?: Float32Array;
194
+ /**
195
+ * Four floats per vertex: the fifth to eighth joints that move this vertex, for a rig authored with
196
+ * eight influences. The same encoding as `joints`, and the same both-or-neither with `weights2`.
197
+ *
198
+ * **Present only with `joints`**, because the importers sort a vertex's influences heaviest first:
199
+ * the first set is always the four that matter most, and a second set alone would be four of the
200
+ * lightest with the heaviest missing. What it costs is thirty-two bytes a vertex on a mesh that
201
+ * carries it, and nothing on one that does not — a renderer draws such a mesh through a vertex
202
+ * variant that reads two more attributes, and every other mesh through the variant it always had.
203
+ */
204
+ joints2?: Float32Array;
205
+ /** Four floats per vertex: how much each of `joints2`' influences moves the vertex. */
206
+ weights2?: Float32Array;
191
207
  /**
192
208
  * Morph target position deltas: `morphTargetCount` targets, three floats a vertex each.
193
209
  *
package/dist/meshData.js CHANGED
@@ -73,6 +73,17 @@ export function validateMeshData(data) {
73
73
  check('tangents', data.tangents, 4);
74
74
  check('joints', data.joints, 4);
75
75
  check('weights', data.weights, 4);
76
+ check('joints2', data.joints2, 4);
77
+ check('weights2', data.weights2, 4);
78
+ if ((data.joints2 === undefined) !== (data.weights2 === undefined)) {
79
+ throw new Error(data.joints2 === undefined
80
+ ? 'Mesh: weights2 without joints2 — the second four influences need both'
81
+ : 'Mesh: joints2 without weights2 — the second four influences need both');
82
+ }
83
+ if (data.joints2 !== undefined && data.joints === undefined) {
84
+ throw new Error('Mesh: a second set of influences without the first — importers put the four heaviest in ' +
85
+ '`joints`, so `joints2` alone would be the lightest four with the heaviest missing');
86
+ }
76
87
  /*
77
88
  * Both or neither. The shader multiplies four matrices by four weights, so a missing half means
78
89
  * either every weight or every index is the absent-attribute constant — and both collapse the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/drft",
3
- "version": "4.8.2",
3
+ "version": "4.8.4",
4
4
  "description": "The .drft container: a streaming format for baked 3D scenes",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -27,9 +27,9 @@
27
27
  ],
28
28
  "sideEffects": false,
29
29
  "devDependencies": {
30
- "@driftengine/core": "4.8.2",
31
- "@driftengine/entities": "4.8.2",
32
- "@driftengine/texture": "4.8.2"
30
+ "@driftengine/core": "4.8.4",
31
+ "@driftengine/entities": "4.8.4",
32
+ "@driftengine/texture": "4.8.4"
33
33
  },
34
34
  "author": "Drift Technologies",
35
35
  "repository": {
package/src/drftBc.ts ADDED
@@ -0,0 +1,151 @@
1
+ /**
2
+ * A `CODEC_BC` texture payload: which block format, what colour space its author declared, and
3
+ * every stored mip level, largest first.
4
+ *
5
+ * **The layout** (FORMAT.md §4.5, 1.24), little-endian after the `TEXS` prefix that already carries
6
+ * the codec, width and height:
7
+ *
8
+ * u32 format the format's own digit: 1, 2, 3, 4, 5 or 7 for BC1 to BC7
9
+ * u32 flags bit 0: the author declared the texels sRGB
10
+ * u32 levels how many mip levels follow, 1 to the full chain
11
+ * u32 0 reserved
12
+ * ... each level's blocks, back to back, largest first
13
+ *
14
+ * **A level's size is derived, never stored.** Level `i` is `max(1, width >> i)` by
15
+ * `max(1, height >> i)` texels, rounded up to whole 4x4 blocks — so a level smaller than a block
16
+ * still costs one, which is how every DDS writer and every GPU lays them out. Storing the sizes
17
+ * would be a second statement of the same fact, free to disagree with the first.
18
+ *
19
+ * **What it gives up**: a chain is whole levels from the top. A file that wanted to drop its top
20
+ * level to save bytes has to say a smaller width and height, which is also what it then is.
21
+ *
22
+ * Unknown flag bits are ignored, which is rule 4 of the compatibility contract; a format number
23
+ * this reader does not know is refused by name. A signed or HDR format would be a new number rather
24
+ * than a flag, because a decoder that ignored it would produce wrong values rather than none.
25
+ */
26
+ import { BC_BLOCK_BYTES, DrftError } from './drftFormat.ts';
27
+ import type { BcFormat } from './drftFormat.ts';
28
+
29
+ /** A block-compressed image, as a `CODEC_BC` payload holds it. */
30
+ export interface BcImage {
31
+ readonly format: BcFormat;
32
+ /**
33
+ * What the author's header said of its texels. How a material slot samples them is still the
34
+ * renderer's decision, as for every other codec: a colour map is read as sRGB and a normal map as
35
+ * linear whatever its file claimed, so this is the author's statement rather than an instruction.
36
+ */
37
+ readonly srgb: boolean;
38
+ readonly width: number;
39
+ readonly height: number;
40
+ /** Level 0 first. Views over the payload when read, never copies. */
41
+ readonly levels: readonly Uint8Array[];
42
+ }
43
+
44
+ const HEADER_BYTES = 16;
45
+ const FLAG_SRGB = 1;
46
+
47
+ const FORMAT_NUMBER: Readonly<Record<BcFormat, number>> = {
48
+ bc1: 1,
49
+ bc2: 2,
50
+ bc3: 3,
51
+ bc4: 4,
52
+ bc5: 5,
53
+ bc7: 7,
54
+ };
55
+ const FORMAT_OF: Readonly<Record<number, BcFormat>> = {
56
+ 1: 'bc1',
57
+ 2: 'bc2',
58
+ 3: 'bc3',
59
+ 4: 'bc4',
60
+ 5: 'bc5',
61
+ 7: 'bc7',
62
+ };
63
+
64
+ /** How many levels a full chain from `width` by `height` down to 1x1 has. */
65
+ export function bcChainLength(width: number, height: number): number {
66
+ return Math.floor(Math.log2(Math.max(1, width, height))) + 1;
67
+ }
68
+
69
+ /** The bytes level `level` of a `width` by `height` surface takes in `format`. */
70
+ export function bcLevelBytes(
71
+ format: BcFormat,
72
+ width: number,
73
+ height: number,
74
+ level: number,
75
+ ): number {
76
+ const w = Math.max(1, width >> level);
77
+ const h = Math.max(1, height >> level);
78
+ return Math.ceil(w / 4) * Math.ceil(h / 4) * BC_BLOCK_BYTES[format];
79
+ }
80
+
81
+ /** The payload for `image`. Refuses a level whose size is not the one its position requires. */
82
+ export function writeBcPayload(image: BcImage): Uint8Array {
83
+ const { format, width, height, levels } = image;
84
+ checkChain(levels.length, width, height);
85
+ let total = HEADER_BYTES;
86
+ for (let i = 0; i < levels.length; i++) {
87
+ const want = bcLevelBytes(format, width, height, i);
88
+ const level = levels[i] as Uint8Array;
89
+ if (level.length !== want) {
90
+ throw new DrftError(
91
+ `bc: level ${i} of a ${width}x${height} ${format.toUpperCase()} image is ${want} bytes, and ` +
92
+ `${level.length} were given`,
93
+ );
94
+ }
95
+ total += want;
96
+ }
97
+ const out = new Uint8Array(total);
98
+ const view = new DataView(out.buffer);
99
+ view.setUint32(0, FORMAT_NUMBER[format], true);
100
+ view.setUint32(4, image.srgb ? FLAG_SRGB : 0, true);
101
+ view.setUint32(8, levels.length, true);
102
+ let at = HEADER_BYTES;
103
+ for (const level of levels) {
104
+ out.set(level, at);
105
+ at += level.length;
106
+ }
107
+ return out;
108
+ }
109
+
110
+ /** A payload read back, its levels as views over `bytes`. */
111
+ export function readBcPayload(width: number, height: number, bytes: Uint8Array): BcImage {
112
+ if (bytes.length < HEADER_BYTES) {
113
+ throw new DrftError(`bc: a payload of ${bytes.length} bytes is shorter than its header`);
114
+ }
115
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
116
+ const number = view.getUint32(0, true);
117
+ const format = FORMAT_OF[number];
118
+ if (format === undefined) {
119
+ throw new DrftError(
120
+ `bc: block format ${number} is not one this reader knows; it reads 1, 2, 3, 4, 5 and 7 ` +
121
+ '(BC1 to BC5 and BC7)',
122
+ );
123
+ }
124
+ const count = view.getUint32(8, true);
125
+ checkChain(count, width, height);
126
+ let needed = HEADER_BYTES;
127
+ for (let i = 0; i < count; i++) needed += bcLevelBytes(format, width, height, i);
128
+ if (bytes.length < needed) {
129
+ throw new DrftError(
130
+ `bc: ${count} levels of a ${width}x${height} ${format.toUpperCase()} image needs ` +
131
+ `${needed} bytes and the payload is ${bytes.length}`,
132
+ );
133
+ }
134
+ const levels: Uint8Array[] = [];
135
+ let at = HEADER_BYTES;
136
+ for (let i = 0; i < count; i++) {
137
+ const size = bcLevelBytes(format, width, height, i);
138
+ levels.push(bytes.subarray(at, at + size));
139
+ at += size;
140
+ }
141
+ return { format, srgb: (view.getUint32(4, true) & FLAG_SRGB) !== 0, width, height, levels };
142
+ }
143
+
144
+ function checkChain(count: number, width: number, height: number): void {
145
+ const most = bcChainLength(width, height);
146
+ if (width < 1 || height < 1 || count < 1 || count > most) {
147
+ throw new DrftError(
148
+ `bc: ${count} levels for a ${width}x${height} image, which has at least 1 and at most ${most}`,
149
+ );
150
+ }
151
+ }
package/src/drftFormat.ts CHANGED
@@ -23,7 +23,7 @@ export const DRFT_VERSION_MAJOR = 1;
23
23
  * Additive revisions within a generation: new optional chunks, new attribute bits, new
24
24
  * enum values with a defined fallback. Never a changed meaning.
25
25
  */
26
- export const DRFT_VERSION_MINOR = 23;
26
+ export const DRFT_VERSION_MINOR = 24;
27
27
 
28
28
  /** Bytes before the chunk table. */
29
29
  export const HEADER_BYTES = 32;
@@ -448,6 +448,21 @@ export const ATTR_LAYERS = 1 << 9;
448
448
  */
449
449
  export const ATTR_CHANNEL = 1 << 10;
450
450
 
451
+ /**
452
+ * Added in 1.24: a vertex's fifth to eighth influences, which joints and how much. See
453
+ * `MeshData.joints2`.
454
+ *
455
+ * Two bits for two arrays, both-or-neither like the first pair, and neither without the first pair.
456
+ * Appended after the channel, as every attribute since 1.20 has been, and that has a consequence
457
+ * worth stating rather than implying: indices follow the last array, so **a 1.23 reader does not
458
+ * skip these** — it reads their floats where it expects indices, finds indices far past the vertex
459
+ * count, and refuses the mesh by name. A loud refusal rather than a mesh drawn with four of its
460
+ * eight influences, which is the same answer `layers` and the channel give an older reader.
461
+ */
462
+ export const ATTR_JOINTS2 = 1 << 11;
463
+ /** How much each of `ATTR_JOINTS2`' four influences moves the vertex. */
464
+ export const ATTR_WEIGHTS2 = 1 << 12;
465
+
451
466
  /** Everything this version defines, so a reader can spot bits from a later writer. */
452
467
  export const ATTR_KNOWN =
453
468
  ATTR_SPECULAR |
@@ -460,7 +475,9 @@ export const ATTR_KNOWN =
460
475
  ATTR_JOINTS |
461
476
  ATTR_WEIGHTS |
462
477
  ATTR_LAYERS |
463
- ATTR_CHANNEL;
478
+ ATTR_CHANNEL |
479
+ ATTR_JOINTS2 |
480
+ ATTR_WEIGHTS2;
464
481
 
465
482
  /**
466
483
  * How a `TEXS` payload is encoded.
@@ -476,6 +493,38 @@ export const CODEC_JPEG = 2;
476
493
  export const CODEC_WEBP = 3;
477
494
  export const CODEC_RAW = 4;
478
495
 
496
+ /**
497
+ * Added in 1.24: block-compressed images, kept as the blocks they were authored in.
498
+ *
499
+ * **Uploaded as they are where the device can sample them**, so a texture authored as BC7 costs a
500
+ * byte a texel on the GPU rather than four, and the mip chain the author built rather than one the
501
+ * engine generates. Where the device cannot — most phones, which sample ASTC and ETC2 instead — the
502
+ * loader decodes them to RGBA, which is the cost every texture paid before this. The payload — block
503
+ * format, colour space and the stored mip chain — is `drftBc.ts`'s.
504
+ */
505
+ export const CODEC_BC = 5;
506
+
507
+ /**
508
+ * The block formats a `CODEC_BC` payload may hold: what one 4x4 block of texels encodes.
509
+ *
510
+ * BC1 is colour with punch-through alpha in eight bytes; BC2 and BC3 colour with explicit and
511
+ * interpolated alpha in sixteen; BC4 one channel and BC5 two (a normal map) in eight and sixteen;
512
+ * BC7 colour and alpha in sixteen, with eight modes chosen per block. BC6H, signed BC4/BC5 and the
513
+ * other families are not among them: an HDR or signed texture is a different contract, refused by
514
+ * name rather than mis-decoded.
515
+ */
516
+ export type BcFormat = 'bc1' | 'bc2' | 'bc3' | 'bc4' | 'bc5' | 'bc7';
517
+
518
+ /** Bytes per 4x4 block of each format. */
519
+ export const BC_BLOCK_BYTES: Readonly<Record<BcFormat, number>> = Object.freeze({
520
+ bc1: 8,
521
+ bc2: 16,
522
+ bc3: 16,
523
+ bc4: 8,
524
+ bc5: 16,
525
+ bc7: 16,
526
+ });
527
+
479
528
  /** What a codec is called, for a message naming what was not understood. */
480
529
  export function codecName(codec: number): string {
481
530
  return codec === CODEC_PNG
@@ -486,7 +535,9 @@ export function codecName(codec: number): string {
486
535
  ? 'WEBP'
487
536
  : codec === CODEC_RAW
488
537
  ? 'RAW'
489
- : `codec ${codec}`;
538
+ : codec === CODEC_BC
539
+ ? 'BC'
540
+ : `codec ${codec}`;
490
541
  }
491
542
 
492
543
  /**
@@ -27,6 +27,8 @@ import {
27
27
  ATTR_EMISSIVE_COLOR,
28
28
  ATTR_GRAIN,
29
29
  ATTR_JOINTS,
30
+ ATTR_JOINTS2,
31
+ ATTR_WEIGHTS2,
30
32
  ATTR_RELIEF,
31
33
  ATTR_ROUGHNESS,
32
34
  ATTR_SPECULAR,
@@ -64,7 +66,9 @@ type Name =
64
66
  | 'joints'
65
67
  | 'weights'
66
68
  | 'layers'
67
- | 'channel';
69
+ | 'channel'
70
+ | 'joints2'
71
+ | 'weights2';
68
72
 
69
73
  /** The frozen order `MESH` uses, with each optional array's bit: `FORMAT.md` §4.3. */
70
74
  const ORDER: readonly {
@@ -89,6 +93,8 @@ const ORDER: readonly {
89
93
  /* Whole numbers naming an image, so exact: a layer quantised onto its neighbour is another picture. */
90
94
  { name: 'layers', width: 1, bit: ATTR_LAYERS, kind: 'exact' },
91
95
  { name: 'channel', width: 4, bit: ATTR_CHANNEL, kind: 'value' },
96
+ { name: 'joints2', width: 4, bit: ATTR_JOINTS2, kind: 'exact' },
97
+ { name: 'weights2', width: 4, bit: ATTR_WEIGHTS2, kind: 'value' },
92
98
  ];
93
99
 
94
100
  /** A growing little-endian byte list, for the writer: offline, so it may allocate. */
package/src/drftRead.ts CHANGED
@@ -33,6 +33,8 @@ import {
33
33
  ATTR_WEIGHTS,
34
34
  ATTR_LAYERS,
35
35
  ATTR_CHANNEL,
36
+ ATTR_JOINTS2,
37
+ ATTR_WEIGHTS2,
36
38
  CHUNK_ENTRY_BYTES,
37
39
  CHUNK_HEAD,
38
40
  CHUNK_LODM,
@@ -324,6 +326,10 @@ export function readMesh(buffer: ArrayBuffer, chunk: DrftChunk): MeshData {
324
326
  const layers = (attributes & ATTR_LAYERS) !== 0 ? floats(vertices, 'layers') : undefined;
325
327
  /* Sway, sky and opacity, 1.23, four floats a vertex and last. See ATTR_CHANNEL. */
326
328
  const channel = (attributes & ATTR_CHANNEL) !== 0 ? floats(vertices * 4, 'channel') : undefined;
329
+ /* The fifth to eighth influences, 1.24, last. See ATTR_JOINTS2. */
330
+ const joints2 = (attributes & ATTR_JOINTS2) !== 0 ? floats(vertices * 4, 'joints2') : undefined;
331
+ const weights2 =
332
+ (attributes & ATTR_WEIGHTS2) !== 0 ? floats(vertices * 4, 'weights2') : undefined;
327
333
 
328
334
  const indexBytes = indexCount * 4;
329
335
  if (at + indexBytes > limit) {
@@ -348,6 +354,8 @@ export function readMesh(buffer: ArrayBuffer, chunk: DrftChunk): MeshData {
348
354
  ...(weights === undefined ? {} : { weights }),
349
355
  ...(layers === undefined ? {} : { layers }),
350
356
  ...(channel === undefined ? {} : { channel }),
357
+ ...(joints2 === undefined ? {} : { joints2 }),
358
+ ...(weights2 === undefined ? {} : { weights2 }),
351
359
  };
352
360
 
353
361
  /*
package/src/drftWrite.ts CHANGED
@@ -27,6 +27,8 @@ import {
27
27
  ATTR_WEIGHTS,
28
28
  ATTR_LAYERS,
29
29
  ATTR_CHANNEL,
30
+ ATTR_JOINTS2,
31
+ ATTR_WEIGHTS2,
30
32
  CHUNK_ENTRY_BYTES,
31
33
  CHUNK_HEAD,
32
34
  CHUNK_ANIM,
@@ -523,6 +525,8 @@ function buildMesh(mesh: MeshData, code = CHUNK_MESH, flags = CHUNK_REQUIRED): P
523
525
  if (mesh.weights !== undefined) attributes |= ATTR_WEIGHTS;
524
526
  if (mesh.layers !== undefined) attributes |= ATTR_LAYERS;
525
527
  if (mesh.channel !== undefined) attributes |= ATTR_CHANNEL;
528
+ if (mesh.joints2 !== undefined) attributes |= ATTR_JOINTS2;
529
+ if (mesh.weights2 !== undefined) attributes |= ATTR_WEIGHTS2;
526
530
 
527
531
  /* Order is frozen: mandatory arrays, then optional ones by ascending bit. */
528
532
  const arrays: (Float32Array | Uint32Array)[] = [
@@ -546,6 +550,9 @@ function buildMesh(mesh: MeshData, code = CHUNK_MESH, flags = CHUNK_REQUIRED): P
546
550
  if (mesh.layers !== undefined) arrays.push(mesh.layers);
547
551
  /* And newer still, 1.23. See ATTR_CHANNEL. */
548
552
  if (mesh.channel !== undefined) arrays.push(mesh.channel);
553
+ /* And 1.24's second four influences, last. See ATTR_JOINTS2. */
554
+ if (mesh.joints2 !== undefined) arrays.push(mesh.joints2);
555
+ if (mesh.weights2 !== undefined) arrays.push(mesh.weights2);
549
556
  arrays.push(mesh.indices);
550
557
 
551
558
  const PREFIX = 16;
package/src/index.ts CHANGED
@@ -140,7 +140,18 @@ export { writeDrft } from './drftWrite.ts';
140
140
  export type { DrftSource, DrftTextureSource } from './drftWrite.ts';
141
141
  export { DrftError, DRFT_VERSION_MAJOR, DRFT_VERSION_MINOR } from './drftFormat.ts';
142
142
  /* A consumer decoding an embedded texture has to know which codec it is holding. */
143
- export { CODEC_PNG, CODEC_JPEG, CODEC_WEBP, CODEC_RAW, codecName } from './drftFormat.ts';
143
+ export {
144
+ CODEC_PNG,
145
+ CODEC_JPEG,
146
+ CODEC_WEBP,
147
+ CODEC_RAW,
148
+ CODEC_BC,
149
+ BC_BLOCK_BYTES,
150
+ codecName,
151
+ } from './drftFormat.ts';
152
+ export type { BcFormat } from './drftFormat.ts';
153
+ export { bcChainLength, bcLevelBytes, readBcPayload, writeBcPayload } from './drftBc.ts';
154
+ export type { BcImage } from './drftBc.ts';
144
155
  export type { DrftHead, DrftMaterial, DrftSplatBlock, DrftSplats } from './drftFormat.ts';
145
156
  /* The order a capture is written in, so a baker outside this package can lay one out the same
146
157
  way — it is the whole reason a capture streams into a recognisable place. */
package/src/meshData.ts CHANGED
@@ -164,9 +164,12 @@ export interface MeshData {
164
164
  * nothing and uploads nothing. Absent means unskinned, and an unskinned mesh takes exactly the
165
165
  * draw path it took before skinning existed.
166
166
  *
167
- * **Four influences and not eight.** Four is what glTF's `JOINTS_0` carries, what every DCC tool
168
- * exports without being asked, and what fits one attribute. Eight doubles the attribute cost of
169
- * every skinned vertex to serve a case that rarely survives an export.
167
+ * **Four here, and four more in `joints2` for a mesh that has them.** Four is what glTF's
168
+ * `JOINTS_0` carries and what most exports write, and it fits one attribute; a character authored
169
+ * for eight — a face, a shoulder — loses the shape of its deformation when the four lightest are
170
+ * dropped, so 4.8.4 added a second set rather than widening this one. A mesh without it pays
171
+ * nothing for it, and importers sort heaviest first, so this set always holds the four that
172
+ * matter most.
170
173
  *
171
174
  * **`Float32Array` and not `Uint8Array`, which costs twelve bytes a vertex.** Every attribute in
172
175
  * this format is float32 and the absent-attribute mechanism depends on it: `mesh.ts` attaches
@@ -189,6 +192,19 @@ export interface MeshData {
189
192
  * is refused below rather than drawn.
190
193
  */
191
194
  weights?: Float32Array;
195
+ /**
196
+ * Four floats per vertex: the fifth to eighth joints that move this vertex, for a rig authored with
197
+ * eight influences. The same encoding as `joints`, and the same both-or-neither with `weights2`.
198
+ *
199
+ * **Present only with `joints`**, because the importers sort a vertex's influences heaviest first:
200
+ * the first set is always the four that matter most, and a second set alone would be four of the
201
+ * lightest with the heaviest missing. What it costs is thirty-two bytes a vertex on a mesh that
202
+ * carries it, and nothing on one that does not — a renderer draws such a mesh through a vertex
203
+ * variant that reads two more attributes, and every other mesh through the variant it always had.
204
+ */
205
+ joints2?: Float32Array;
206
+ /** Four floats per vertex: how much each of `joints2`' influences moves the vertex. */
207
+ weights2?: Float32Array;
192
208
  /**
193
209
  * Morph target position deltas: `morphTargetCount` targets, three floats a vertex each.
194
210
  *
@@ -281,6 +297,21 @@ export function validateMeshData(data: MeshData): void {
281
297
  check('tangents', data.tangents, 4);
282
298
  check('joints', data.joints, 4);
283
299
  check('weights', data.weights, 4);
300
+ check('joints2', data.joints2, 4);
301
+ check('weights2', data.weights2, 4);
302
+ if ((data.joints2 === undefined) !== (data.weights2 === undefined)) {
303
+ throw new Error(
304
+ data.joints2 === undefined
305
+ ? 'Mesh: weights2 without joints2 — the second four influences need both'
306
+ : 'Mesh: joints2 without weights2 — the second four influences need both',
307
+ );
308
+ }
309
+ if (data.joints2 !== undefined && data.joints === undefined) {
310
+ throw new Error(
311
+ 'Mesh: a second set of influences without the first — importers put the four heaviest in ' +
312
+ '`joints`, so `joints2` alone would be the lightest four with the heaviest missing',
313
+ );
314
+ }
284
315
 
285
316
  /*
286
317
  * Both or neither. The shader multiplies four matrices by four weights, so a missing half means