@driftengine/assets 4.8.3 → 4.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +1 -1
  2. package/dist/bc7Decode.d.ts +33 -0
  3. package/dist/bc7Decode.js +197 -0
  4. package/dist/bcAnswer.d.ts +28 -0
  5. package/dist/bcAnswer.js +15 -0
  6. package/dist/bcDecode.d.ts +28 -0
  7. package/dist/bcDecode.js +175 -0
  8. package/dist/bcFlip.d.ts +21 -0
  9. package/dist/bcFlip.js +52 -0
  10. package/dist/bcImage.d.ts +15 -0
  11. package/dist/bcImage.js +38 -0
  12. package/dist/bcLoad.d.ts +26 -0
  13. package/dist/bcLoad.js +113 -0
  14. package/dist/bcWorker.d.ts +1 -0
  15. package/dist/bcWorker.js +14 -0
  16. package/dist/dds.d.ts +16 -18
  17. package/dist/dds.js +20 -293
  18. package/dist/ddsBlocks.d.ts +3 -0
  19. package/dist/ddsBlocks.js +54 -0
  20. package/dist/ddsHeader.d.ts +32 -0
  21. package/dist/ddsHeader.js +119 -0
  22. package/dist/drftLoader.d.ts +18 -0
  23. package/dist/drftLoader.js +98 -8
  24. package/dist/fbx.js +32 -13
  25. package/dist/fbxRig.d.ts +0 -18
  26. package/dist/fbxRig.js +0 -37
  27. package/dist/gltf.js +74 -12
  28. package/dist/index.d.ts +4 -0
  29. package/dist/index.js +7 -2
  30. package/dist/influences.d.ts +40 -0
  31. package/dist/influences.js +53 -0
  32. package/dist/weld.js +3 -0
  33. package/package.json +4 -4
  34. package/src/bc7Decode.ts +237 -0
  35. package/src/bcAnswer.ts +45 -0
  36. package/src/bcDecode.ts +218 -0
  37. package/src/bcFlip.ts +55 -0
  38. package/src/bcImage.ts +58 -0
  39. package/src/bcLoad.ts +154 -0
  40. package/src/bcWorker.ts +20 -0
  41. package/src/dds.ts +21 -337
  42. package/src/ddsBlocks.ts +63 -0
  43. package/src/ddsHeader.ts +160 -0
  44. package/src/drftLoader.ts +115 -9
  45. package/src/fbx.ts +34 -18
  46. package/src/fbxRig.ts +0 -41
  47. package/src/gltf.ts +75 -13
  48. package/src/index.ts +7 -2
  49. package/src/influences.ts +77 -0
  50. package/src/weld.ts +3 -0
@@ -0,0 +1,119 @@
1
+ /**
2
+ * A DDS header, read as far as which blocks follow it: the block format and the colour space its
3
+ * author declared, in either header spelling, the size, the stored mip count, and where the surface
4
+ * starts. Shared by `dds.ts`, which decodes the blocks, and `ddsBlocks.ts`, which keeps them.
5
+ *
6
+ * **Two spellings of one thing.** A classic header names the format in the four characters at byte
7
+ * 84 and the surface follows at 128; a `DX10` header puts `DX10` there instead and states the real
8
+ * format in a 20-byte extension, so the surface starts at 148. Nothing else differs — the blocks are
9
+ * the same blocks — which is why both resolve to one name and one offset.
10
+ *
11
+ * What it refuses, by name: BC6H, signed BC4 and BC5, the premultiplied DXT2 and DXT4, and from a
12
+ * `DX10` header a cube map, an array or anything but a plain 2D texture. A format identified and
13
+ * declined is worth more than one half-decoded, which is `recognise.ts`'s principle applied one
14
+ * level down.
15
+ */
16
+ import { DrftError } from '@driftengine/drft';
17
+ export const HEADER_BYTES = 128;
18
+ /** `DDS_HEADER_DXT10`: the extension a `DX10` FourCC puts between the header and the surface. */
19
+ const DX10_EXTENSION_BYTES = 20;
20
+ /** `DDPF_FOURCC`: the pixel format names a compression rather than describing channels. */
21
+ export const DDPF_FOURCC = 0x4;
22
+ /** Whether these bytes are a DDS at all. The magic, and nothing inferred from a file name. */
23
+ export function isDds(bytes) {
24
+ return (bytes.length >= 4 &&
25
+ bytes[0] === 0x44 &&
26
+ bytes[1] === 0x44 &&
27
+ bytes[2] === 0x53 &&
28
+ bytes[3] === 0x20);
29
+ }
30
+ /**
31
+ * The DXGI block formats, and which of them declare sRGB.
32
+ *
33
+ * **sRGB is a statement about how the values are *read* rather than about how they are stored.**
34
+ * BC3 announced as `DXGI_FORMAT_BC3_UNORM_SRGB` is bit for bit the BC3 announced as `DXT5`: the
35
+ * same blocks and the same mip chain. Measured from outside on one bundle's paint texture —
36
+ * 5,592,580 bytes against 5,592,560 for the classic spelling of the same 2048² chain, which is
37
+ * 148 + 5,592,432 against 128 + 5,592,432. The typeless spellings read as linear.
38
+ *
39
+ * **What is deliberately absent.** `BC4_SNORM` (81) and `BC5_SNORM` (84), whose values are signed,
40
+ * and BC6H (94–96), whose values are half floats — mapping any of them would decode to wrong numbers
41
+ * rather than to an error, which is the one failure this reader is written to avoid.
42
+ */
43
+ const DXGI_BLOCK = {
44
+ 70: ['bc1', false],
45
+ 71: ['bc1', false],
46
+ 72: ['bc1', true],
47
+ 73: ['bc2', false],
48
+ 74: ['bc2', false],
49
+ 75: ['bc2', true],
50
+ 76: ['bc3', false],
51
+ 77: ['bc3', false],
52
+ 78: ['bc3', true],
53
+ 79: ['bc4', false],
54
+ 80: ['bc4', false],
55
+ 82: ['bc5', false],
56
+ 83: ['bc5', false],
57
+ 97: ['bc7', false],
58
+ 98: ['bc7', false],
59
+ 99: ['bc7', true],
60
+ };
61
+ /** The classic spellings. BC7 has none: only a `DX10` header can name it. */
62
+ const FOURCC_BLOCK = {
63
+ DXT1: 'bc1',
64
+ DXT3: 'bc2',
65
+ DXT5: 'bc3',
66
+ ATI1: 'bc4',
67
+ BC4U: 'bc4',
68
+ ATI2: 'bc5',
69
+ BC5U: 'bc5',
70
+ };
71
+ /**
72
+ * The surface a block-compressed DDS declares. The caller has checked the magic, the length of the
73
+ * classic header and that the pixel format names a compression.
74
+ *
75
+ * **What a `DX10` header can say that a reader must refuse.** `resourceDimension` other than 3
76
+ * (`TEXTURE2D`), `miscFlag` bit 2 (a cube map) or an `arraySize` above 1. Taking one would hand the
77
+ * caller six faces or twenty slices claiming to be a single surface — a picture rather than an
78
+ * error.
79
+ */
80
+ export function readDdsSurface(bytes, view) {
81
+ const height = view.getUint32(12, true);
82
+ const width = view.getUint32(16, true);
83
+ const mipCount = Math.max(1, view.getUint32(28, true));
84
+ const fourcc = String.fromCharCode(bytes[84], bytes[85], bytes[86], bytes[87]);
85
+ if (fourcc !== 'DX10') {
86
+ const format = FOURCC_BLOCK[fourcc];
87
+ if (format === undefined) {
88
+ throw new DrftError(`dds: "${fourcc}" is not a format this reader decodes. It reads BC1 (DXT1), BC2 (DXT3), ` +
89
+ 'BC3 (DXT5), BC4 (ATI1) and BC5 (ATI2), and BC7 behind a DX10 header. Convert the ' +
90
+ 'texture to PNG, or re-export it as one of those.');
91
+ }
92
+ return { format, srgb: false, width, height, mipCount, dataAt: HEADER_BYTES, named: fourcc };
93
+ }
94
+ const dataAt = HEADER_BYTES + DX10_EXTENSION_BYTES;
95
+ if (bytes.length < dataAt) {
96
+ throw new DrftError('dds: a DX10 header is declared and the twenty-byte extension is not there');
97
+ }
98
+ const dxgi = view.getUint32(HEADER_BYTES, true);
99
+ const dimension = view.getUint32(HEADER_BYTES + 4, true);
100
+ const miscFlag = view.getUint32(HEADER_BYTES + 8, true);
101
+ const arraySize = view.getUint32(HEADER_BYTES + 12, true);
102
+ if (dimension !== 3) {
103
+ throw new DrftError(`dds: this DX10 surface declares resourceDimension ${dimension}, and only 3 (a plain 2D ` +
104
+ 'texture) is read here. A 1D or volume texture is a different thing wearing the same header.');
105
+ }
106
+ if ((miscFlag & 0x4) !== 0) {
107
+ throw new DrftError('dds: this is a cube map, and only a plain 2D texture is read here.');
108
+ }
109
+ if (arraySize > 1) {
110
+ throw new DrftError(`dds: this is a texture array of ${arraySize}, and only a plain 2D texture is read here.`);
111
+ }
112
+ const entry = DXGI_BLOCK[dxgi];
113
+ if (entry === undefined) {
114
+ throw new DrftError(`dds: DXGI format ${dxgi} is not one this reader decodes. It reads BC1 to BC5 and BC7, ` +
115
+ 'unsigned. Convert the texture to PNG, or re-export it as one of those.');
116
+ }
117
+ const [format, srgb] = entry;
118
+ return { format, srgb, width, height, mipCount, dataAt, named: `DXGI ${dxgi}` };
119
+ }
@@ -265,6 +265,9 @@ export declare class DrftLoader {
265
265
  private boundCount;
266
266
  private readonly singles;
267
267
  private readonly images;
268
+ /** Started at the first BC texture that has to be decoded, and only then. See `bcLoad.ts`. */
269
+ private bcDecoder;
270
+ private warnedBc;
268
271
  private readonly chunks;
269
272
  private materials;
270
273
  /**
@@ -510,6 +513,21 @@ export declare class DrftLoader {
510
513
  * silently reassigning an image to a different surface.
511
514
  */
512
515
  private rebuildTextures;
516
+ /**
517
+ * A BC texture: its blocks kept, to go up as they are where this device samples them, or decoded
518
+ * off the main thread where it does not — which on a phone is every one. See `bcLoad.ts`.
519
+ *
520
+ * **A consumer taking images through `onImage` gets a decoded image either way**, as an ordinary
521
+ * picture: what it builds from them is its own, and a texture array of bitmaps cannot hold blocks.
522
+ * Like every image, a BC texture fails on its own and its surfaces draw untextured.
523
+ */
524
+ private acceptBc;
525
+ /**
526
+ * What goes to `createSurfaceTexture`: the image as it is, unless it is blocks this device cannot
527
+ * take in the colour space the slot reads — a material read after its texture arrived can change
528
+ * that — in which case they are decoded here, on the spot.
529
+ */
530
+ private uploadable;
513
531
  /** One place that writes the progress, so every field stays consistent with the phase. */
514
532
  private set;
515
533
  }
@@ -2,6 +2,10 @@
2
2
  import { MeshBuilder, concatMeshes, placeMesh } from '@driftengine/core';
3
3
  import { placeFields } from './fieldPlacement.js';
4
4
  import { TextureSet, textureColorSpaces } from './drftTextures.js';
5
+ import { bcPlan, createBcDecoder } from './bcLoad.js';
6
+ import { decodeBc } from './bcDecode.js';
7
+ import { isCompressedSource, uploadsCompressed } from '@driftengine/core';
8
+ import { CODEC_BC, codecName, readBcPayload } from '@driftengine/drft';
5
9
  import { drawKeyOf, resolveDrawGrouping } from './drawKey.js';
6
10
  import { streamDrft, CODEC_JPEG, CODEC_PNG, CODEC_RAW, CODEC_WEBP, DrftError, expandAssembly, } from '@driftengine/drft';
7
11
  import { RegionStore, fitCopies } from './regionStore.js';
@@ -99,6 +103,9 @@ export class DrftLoader {
99
103
  boundCount = 0;
100
104
  singles = [];
101
105
  images = [];
106
+ /** Started at the first BC texture that has to be decoded, and only then. See `bcLoad.ts`. */
107
+ bcDecoder = null;
108
+ warnedBc = false;
102
109
  chunks = [];
103
110
  materials = [];
104
111
  /**
@@ -567,6 +574,10 @@ export class DrftLoader {
567
574
  * visibly wrong, which a consumer's loading bar reported to a person as finished.
568
575
  */
569
576
  this.set({ phase: 'textures' });
577
+ if (texture.codec === CODEC_BC) {
578
+ this.acceptBc(texture, ordinal);
579
+ return;
580
+ }
570
581
  /*
571
582
  * Decoded through the browser's own image path, so the engine ships no decoder, and
572
583
  * **each one fails on its own**: a texture is the part of an asset most likely to be
@@ -716,7 +727,7 @@ export class DrftLoader {
716
727
  if (take === undefined)
717
728
  this.rebuildTextures(ordinal);
718
729
  else
719
- take(this.chunks[ordinal]?.name ?? '', this.images[ordinal] ?? null);
730
+ take(this.chunks[ordinal]?.name ?? '', bitmapOf(this.images[ordinal] ?? null));
720
731
  /*
721
732
  * Counted here, once, and only for the image itself.
722
733
  *
@@ -789,6 +800,8 @@ export class DrftLoader {
789
800
  return;
790
801
  this.disposed = true;
791
802
  this.regionStore.dispose();
803
+ this.bcDecoder?.dispose();
804
+ this.bcDecoder = null;
792
805
  /*
793
806
  * Everything this class ever made, each disposed once.
794
807
  *
@@ -1175,7 +1188,8 @@ export class DrftLoader {
1175
1188
  */
1176
1189
  const existing = previous?.at(ordinal) ?? null;
1177
1190
  const arrived = this.images[ordinal] ?? null;
1178
- if (existing !== null && arrived !== null) {
1191
+ /* Blocks never arrive as a replacement: a BC texture has no preview to replace. */
1192
+ if (existing !== null && arrived !== null && !isCompressedSource(arrived)) {
1179
1193
  this.renderer.updateSurfaceTexture(existing, arrived);
1180
1194
  return;
1181
1195
  }
@@ -1188,18 +1202,85 @@ export class DrftLoader {
1188
1202
  const held = previous?.at(at) ?? null;
1189
1203
  if (held !== null)
1190
1204
  return held;
1191
- const bitmap = this.images[at] ?? null;
1192
- if (bitmap === null)
1205
+ const image = this.images[at] ?? null;
1206
+ if (image === null)
1193
1207
  return null;
1194
1208
  const anisotropy = this.options.anisotropy;
1195
1209
  const wrap = this.options.textureWrap;
1196
- return this.renderer.createSurfaceTexture(bitmap, {
1210
+ const colorSpace = spaces[at] ?? 'linear';
1211
+ return this.renderer.createSurfaceTexture(this.uploadable(image, colorSpace === 'srgb'), {
1197
1212
  ...(anisotropy === undefined ? {} : { anisotropy }),
1198
1213
  ...(wrap === undefined ? {} : { wrap }),
1199
- colorSpace: spaces[at] ?? 'linear',
1214
+ colorSpace,
1200
1215
  });
1201
1216
  });
1202
1217
  }
1218
+ /**
1219
+ * A BC texture: its blocks kept, to go up as they are where this device samples them, or decoded
1220
+ * off the main thread where it does not — which on a phone is every one. See `bcLoad.ts`.
1221
+ *
1222
+ * **A consumer taking images through `onImage` gets a decoded image either way**, as an ordinary
1223
+ * picture: what it builds from them is its own, and a texture array of bitmaps cannot hold blocks.
1224
+ * Like every image, a BC texture fails on its own and its surfaces draw untextured.
1225
+ */
1226
+ acceptBc(texture, ordinal) {
1227
+ const done = (image) => {
1228
+ if (this.disposed)
1229
+ return;
1230
+ this.images[ordinal] = image;
1231
+ this.sharp[ordinal] = true;
1232
+ this.imageQueue.push(ordinal);
1233
+ };
1234
+ const failed = (error) => {
1235
+ console.warn(`DrftLoader: image ${ordinal} did not decode; its surfaces stay untextured` +
1236
+ ` (codec BC, ${texture.bytes.length} bytes, ${texture.width}x${texture.height})`, error);
1237
+ done(null);
1238
+ };
1239
+ let image;
1240
+ try {
1241
+ image = readBcPayload(texture.width, texture.height, texture.bytes);
1242
+ }
1243
+ catch (error) {
1244
+ failed(error);
1245
+ return;
1246
+ }
1247
+ const asImage = this.options.onImage !== undefined;
1248
+ const srgb = textureColorSpaces(this.materials, ordinal + 1)[ordinal] === 'srgb';
1249
+ if (!asImage && bcPlan(image, srgb, this.renderer.compressedFormats) === 'blocks') {
1250
+ done(image);
1251
+ return;
1252
+ }
1253
+ if (this.bcDecoder === null)
1254
+ this.bcDecoder = createBcDecoder();
1255
+ const decoder = this.bcDecoder;
1256
+ decoder
1257
+ .decode(image, asImage)
1258
+ .then(async (rgba) => {
1259
+ /* Said once, where a worker could not be had or failed: a slower load nobody could
1260
+ otherwise attribute. */
1261
+ if (decoder.reason !== '' && !this.warnedBc) {
1262
+ this.warnedBc = true;
1263
+ console.warn(`DrftLoader: ${decoder.reason}`);
1264
+ }
1265
+ const pixels = imageDataOf(rgba, image.width, image.height);
1266
+ done(asImage ? await createImageBitmap(pixels, AS_AUTHORED) : pixels);
1267
+ })
1268
+ .catch(failed);
1269
+ }
1270
+ /**
1271
+ * What goes to `createSurfaceTexture`: the image as it is, unless it is blocks this device cannot
1272
+ * take in the colour space the slot reads — a material read after its texture arrived can change
1273
+ * that — in which case they are decoded here, on the spot.
1274
+ */
1275
+ uploadable(image, srgb) {
1276
+ if (!isCompressedSource(image))
1277
+ return image;
1278
+ const { format, width, height } = image;
1279
+ if (uploadsCompressed(format, srgb, width, height, this.renderer.compressedFormats)) {
1280
+ return image;
1281
+ }
1282
+ return imageDataOf(decodeBc(format, width, height, image.levels[0]), width, height);
1283
+ }
1203
1284
  /** One place that writes the progress, so every field stays consistent with the phase. */
1204
1285
  set(patch) {
1205
1286
  const next = { ...this.state, ...patch };
@@ -1283,6 +1364,14 @@ const AS_AUTHORED = {
1283
1364
  premultiplyAlpha: 'premultiply',
1284
1365
  colorSpaceConversion: 'none',
1285
1366
  };
1367
+ /** RGBA as an `ImageData`, which every upload path takes as it takes a bitmap. */
1368
+ function imageDataOf(rgba, width, height) {
1369
+ return new ImageData(new Uint8ClampedArray(rgba.buffer, rgba.byteOffset, rgba.length), width, height);
1370
+ }
1371
+ /** The bitmap `onImage` is handed. Only bitmaps reach that path, BC decoded to one on the way. */
1372
+ function bitmapOf(image) {
1373
+ return image === null || isCompressedSource(image) || 'data' in image ? null : image;
1374
+ }
1286
1375
  async function decodeImage(texture, longestSide) {
1287
1376
  /*
1288
1377
  * **A raw texture has no decoder, because it is already decoded.**
@@ -1308,8 +1397,9 @@ async function decodeImage(texture, longestSide) {
1308
1397
  }
1309
1398
  const type = imageTypeFor(texture.codec);
1310
1399
  if (type === null) {
1311
- throw new DrftError(`texture codec ${texture.codec} is not one this loader can decode; bake it again with a ` +
1312
- 'current baker');
1400
+ throw new DrftError(`texture codec ${codecName(texture.codec)} is not one this loader reads: a newer baker ` +
1401
+ 'wrote it, or the file is damaged. Update @driftengine/assets, or bake with the baker ' +
1402
+ 'that matches it');
1313
1403
  }
1314
1404
  const blob = new Blob([texture.bytes.slice()], { type });
1315
1405
  if (longestSide === undefined)
package/dist/fbx.js CHANGED
@@ -27,7 +27,8 @@
27
27
  import { DrftError } from '@driftengine/drft';
28
28
  import { parseUpAxis } from './orient.js';
29
29
  import { FALLBACK_MATERIAL, readMaterials } from './fbxMaterial.js';
30
- import { bareName, indexObjects, readClips, readSkin, writeInfluences } from './fbxRig.js';
30
+ import { bareName, indexObjects, readClips, readSkin } from './fbxRig.js';
31
+ import { writeInfluences } from './influences.js';
31
32
  /** One value repeated per vertex, for a material constant this format has nowhere else to put. */
32
33
  function repeatTriple(count, value) {
33
34
  const out = new Float32Array(count * 3);
@@ -575,6 +576,16 @@ export function fbxToMeshes(buffer, inflate) {
575
576
  const outUvs = uvLayer === null ? null : [];
576
577
  const outJoints = skin === null ? null : [];
577
578
  const outWeights = skin === null ? null : [];
579
+ /* The fifth to eighth influences, kept only if some vertex needed them. See influences.ts. */
580
+ const outJoints2 = skin === null ? null : [];
581
+ const outWeights2 = skin === null ? null : [];
582
+ let secondInfluences = false;
583
+ const scratch = {
584
+ joints: new Float32Array(4),
585
+ weights: new Float32Array(4),
586
+ joints2: new Float32Array(4),
587
+ weights2: new Float32Array(4),
588
+ };
578
589
  const indices = [];
579
590
  /*
580
591
  * A mesh with no normals is not deduplicated, and that is the geometry speaking rather than
@@ -677,18 +688,23 @@ export function fbxToMeshes(buffer, inflate) {
677
688
  outColors.push(r, g, b);
678
689
  if (outUvs !== null)
679
690
  outUvs.push(u, v);
680
- if (outJoints !== null && outWeights !== null && skin !== null) {
681
- outJoints.push(0, 0, 0, 0);
682
- outWeights.push(0, 0, 0, 0);
683
- const scratchJoints = new Float32Array(4);
684
- const scratchWeights = new Float32Array(4);
685
- const report = writeInfluences(skin.influences.get(vertexIndex), scratchJoints, scratchWeights, 0);
686
- for (let k = 0; k < 4; k++) {
687
- outJoints[emitted * 4 + k] = scratchJoints[k];
688
- outWeights[emitted * 4 + k] = scratchWeights[k];
689
- }
691
+ if (outJoints !== null &&
692
+ outWeights !== null &&
693
+ outJoints2 !== null &&
694
+ outWeights2 !== null &&
695
+ skin !== null) {
696
+ scratch.joints.fill(0);
697
+ scratch.weights.fill(0);
698
+ scratch.joints2.fill(0);
699
+ scratch.weights2.fill(0);
700
+ const report = writeInfluences(skin.influences.get(vertexIndex), scratch, 0);
701
+ outJoints.push(...scratch.joints);
702
+ outWeights.push(...scratch.weights);
703
+ outJoints2.push(...scratch.joints2);
704
+ outWeights2.push(...scratch.weights2);
690
705
  droppedInfluences ||= report.dropped;
691
706
  rescaledInfluences ||= report.rescaled;
707
+ secondInfluences ||= report.second;
692
708
  }
693
709
  }
694
710
  const count = positions.length / 3;
@@ -699,8 +715,8 @@ export function fbxToMeshes(buffer, inflate) {
699
715
  computeFaceNormals(outPositions, normalsOut);
700
716
  }
701
717
  if (droppedInfluences) {
702
- warnings.push(`${name}: a vertex is held by more than four joints; the four largest are kept and ` +
703
- `renormalised, because four is what the skinning palette reads.`);
718
+ warnings.push(`${name}: a vertex is held by more than eight joints; the eight largest are kept and ` +
719
+ `renormalised, because eight is what the skinning palette reads.`);
704
720
  }
705
721
  if (rescaledInfluences) {
706
722
  warnings.push(`${name}: skin weights did not sum to one and were normalised.`);
@@ -724,6 +740,9 @@ export function fbxToMeshes(buffer, inflate) {
724
740
  ...(outJoints === null || outWeights === null
725
741
  ? {}
726
742
  : { joints: Float32Array.from(outJoints), weights: Float32Array.from(outWeights) }),
743
+ ...(outJoints2 === null || outWeights2 === null || !secondInfluences
744
+ ? {}
745
+ : { joints2: Float32Array.from(outJoints2), weights2: Float32Array.from(outWeights2) }),
727
746
  });
728
747
  perMesh.push(material);
729
748
  }
package/dist/fbxRig.d.ts CHANGED
@@ -95,24 +95,6 @@ export declare function readSkin(objects: FbxObjects, geometryId: number,
95
95
  * readers of this format settle on, and for the same reason.
96
96
  */
97
97
  meshWorld: readonly number[], warnings: string[]): FbxSkin | null;
98
- /**
99
- * One control point's four influences, largest first, normalised, written into a caller's arrays.
100
- *
101
- * **Four is the shader's number and the file's is unbounded.** A rig authored with six influences
102
- * on a shoulder is ordinary, so the four largest are kept and renormalised rather than the first
103
- * four taken: dropping by *order* would discard whichever the exporter happened to write last,
104
- * which on a shoulder is as likely to be the dominant one as not.
105
- *
106
- * Returns true where something was dropped or rescaled, so the caller can say so once for the mesh
107
- * rather than once per vertex.
108
- */
109
- export declare function writeInfluences(influences: readonly {
110
- joint: number;
111
- weight: number;
112
- }[] | undefined, joints: Float32Array, weights: Float32Array, at: number): {
113
- dropped: boolean;
114
- rescaled: boolean;
115
- };
116
98
  /**
117
99
  * Every take in the file, as clips over the joints a skin named.
118
100
  *
package/dist/fbxRig.js CHANGED
@@ -25,8 +25,6 @@
25
25
  */
26
26
  /** One second, in the integer unit `KeyTime` counts. FBX's own constant. */
27
27
  const KTIME_PER_SECOND = 46186158000;
28
- /** The most influences a vertex may carry, which is what the palette shader reads. */
29
- const MAX_INFLUENCES = 4;
30
28
  const IDENTITY = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
31
29
  function findChild(node, name) {
32
30
  return node.children.find((child) => child.name === name);
@@ -296,41 +294,6 @@ meshWorld, warnings) {
296
294
  }
297
295
  return { joints, inverseBind, influences, modelOfJoint: order };
298
296
  }
299
- /**
300
- * One control point's four influences, largest first, normalised, written into a caller's arrays.
301
- *
302
- * **Four is the shader's number and the file's is unbounded.** A rig authored with six influences
303
- * on a shoulder is ordinary, so the four largest are kept and renormalised rather than the first
304
- * four taken: dropping by *order* would discard whichever the exporter happened to write last,
305
- * which on a shoulder is as likely to be the dominant one as not.
306
- *
307
- * Returns true where something was dropped or rescaled, so the caller can say so once for the mesh
308
- * rather than once per vertex.
309
- */
310
- export function writeInfluences(influences, joints, weights, at) {
311
- if (influences === undefined || influences.length === 0)
312
- return { dropped: false, rescaled: false };
313
- const sorted = [...influences].sort((a, b) => b.weight - a.weight);
314
- const kept = sorted.slice(0, MAX_INFLUENCES);
315
- let sum = 0;
316
- for (const influence of kept)
317
- sum += influence.weight;
318
- if (sum <= 0)
319
- return { dropped: false, rescaled: false };
320
- for (let i = 0; i < kept.length; i++) {
321
- const influence = kept[i];
322
- joints[at * MAX_INFLUENCES + i] = influence.joint;
323
- weights[at * MAX_INFLUENCES + i] = influence.weight / sum;
324
- }
325
- let total = 0;
326
- for (const influence of sorted)
327
- total += influence.weight;
328
- return {
329
- dropped: sorted.length > MAX_INFLUENCES,
330
- /* A tenth of a per cent, which is wider than float noise and narrower than an authoring slip. */
331
- rescaled: Math.abs(total - 1) > 1e-3,
332
- };
333
- }
334
297
  /** Euler degrees in the file's default XYZ order, as a quaternion. */
335
298
  function eulerToQuaternion(x, y, z) {
336
299
  const half = Math.PI / 360;
package/dist/gltf.js CHANGED
@@ -21,6 +21,7 @@ import { generateTangents } from '@driftengine/core';
21
21
  import { MAX_MORPH_TARGETS } from '@driftengine/core';
22
22
  import { accessorFloats } from './gltfAccessor.js';
23
23
  import { readGltfSkins } from './gltfSkin.js';
24
+ import { writeInfluences } from './influences.js';
24
25
  /** The engine's own roughness, for a material whose stated one cannot be believed. */
25
26
  const DEFAULT_ROUGHNESS = 0.4277;
26
27
  /** `TRIANGLES`. The only primitive mode a `MeshData` can express. */
@@ -959,20 +960,80 @@ deriveTangents = true) {
959
960
  }
960
961
  let joints;
961
962
  let weights;
962
- const jointIndex = primitive.attributes['JOINTS_0'];
963
- const weightIndex = primitive.attributes['WEIGHTS_0'];
964
- if (skin !== undefined && jointIndex !== undefined && weightIndex !== undefined) {
965
- joints = accessorFloats(doc, buffers, jointIndex, `${label} JOINTS_0`);
966
- weights = accessorFloats(doc, buffers, weightIndex, `${label} WEIGHTS_0`);
967
- for (let i = 0; i < joints.length; i++) {
968
- const old = joints[i];
969
- const moved = skin.remap[old];
970
- if (moved === undefined) {
971
- throw new DrftError(`gltf: ${label} weights a vertex to joint ${old}, and its skin has ${skin.remap.length}`);
963
+ let joints2;
964
+ let weights2;
965
+ if (skin !== undefined) {
966
+ /*
967
+ * Every `JOINTS_n`/`WEIGHTS_n` pair the primitive carries, remapped onto the sorted joints. One
968
+ * pair is what nearly every export writes and keeps the path it always had; more go through
969
+ * `writeInfluences`, which keeps the eight heaviest, heaviest first, across the two sets a mesh
970
+ * carries. See influences.ts.
971
+ */
972
+ const sets = [];
973
+ for (let n = 0;; n++) {
974
+ const jointIndex = primitive.attributes[`JOINTS_${n}`];
975
+ const weightIndex = primitive.attributes[`WEIGHTS_${n}`];
976
+ if (jointIndex === undefined || weightIndex === undefined)
977
+ break;
978
+ const set = {
979
+ joints: accessorFloats(doc, buffers, jointIndex, `${label} JOINTS_${n}`),
980
+ weights: accessorFloats(doc, buffers, weightIndex, `${label} WEIGHTS_${n}`),
981
+ };
982
+ for (let i = 0; i < set.joints.length; i++) {
983
+ const old = set.joints[i];
984
+ const moved = skin.remap[old];
985
+ if (moved === undefined) {
986
+ throw new DrftError(`gltf: ${label} weights a vertex to joint ${old}, and its skin has ${skin.remap.length}`);
987
+ }
988
+ set.joints[i] = moved;
989
+ }
990
+ sets.push(set);
991
+ }
992
+ const only = sets.length === 1 ? sets[0] : undefined;
993
+ if (only !== undefined) {
994
+ joints = only.joints;
995
+ weights = only.weights;
996
+ normaliseWeights(weights, label, warnings);
997
+ }
998
+ else if (sets.length > 1) {
999
+ const vertices = sets[0].joints.length / 4;
1000
+ const out = {
1001
+ joints: new Float32Array(vertices * 4),
1002
+ weights: new Float32Array(vertices * 4),
1003
+ joints2: new Float32Array(vertices * 4),
1004
+ weights2: new Float32Array(vertices * 4),
1005
+ };
1006
+ let dropped = false;
1007
+ let rescaled = false;
1008
+ let second = false;
1009
+ for (let v = 0; v < vertices; v++) {
1010
+ const influences = [];
1011
+ for (const set of sets) {
1012
+ for (let k = 0; k < 4; k++) {
1013
+ influences.push({
1014
+ joint: set.joints[v * 4 + k],
1015
+ weight: set.weights[v * 4 + k],
1016
+ });
1017
+ }
1018
+ }
1019
+ const report = writeInfluences(influences, out, v);
1020
+ dropped ||= report.dropped;
1021
+ rescaled ||= report.rescaled;
1022
+ second ||= report.second;
1023
+ }
1024
+ joints = out.joints;
1025
+ weights = out.weights;
1026
+ if (second) {
1027
+ joints2 = out.joints2;
1028
+ weights2 = out.weights2;
1029
+ }
1030
+ if (dropped) {
1031
+ warnings.push(`${label}: a vertex is held by more than eight joints; the eight largest are kept`);
1032
+ }
1033
+ if (rescaled) {
1034
+ warnings.push(`${label}: vertex weight sets did not sum to one and were rescaled`);
972
1035
  }
973
- joints[i] = moved;
974
1036
  }
975
- normaliseWeights(weights, label, warnings);
976
1037
  }
977
1038
  return {
978
1039
  mesh: {
@@ -987,6 +1048,7 @@ deriveTangents = true) {
987
1048
  ...(uvs === undefined ? {} : { uvs }),
988
1049
  ...(tangents === undefined ? {} : { tangents }),
989
1050
  ...(joints === undefined || weights === undefined ? {} : { joints, weights }),
1051
+ ...(joints2 === undefined || weights2 === undefined ? {} : { joints2, weights2 }),
990
1052
  ...(morphTargets === undefined || morphTargetCount === undefined
991
1053
  ? {}
992
1054
  : { morphTargets, morphTargetCount }),
package/dist/index.d.ts CHANGED
@@ -94,6 +94,10 @@ export type { Localised } from './localise.ts';
94
94
  export { colliderBeside, levelsBeside } from './readModel.ts';
95
95
  export { ddsToRgba, isDds } from './dds.ts';
96
96
  export type { DdsImage } from './dds.ts';
97
+ export { ddsBlocks } from './ddsBlocks.ts';
98
+ export { decodeBc } from './bcDecode.ts';
99
+ export { decodeBcImage } from './bcImage.ts';
100
+ export { flipBc5Green } from './bcFlip.ts';
97
101
  export { buildClusters } from './bake/cluster.ts';
98
102
  export type { ClusterOptions, ClusterSet } from './bake/cluster.ts';
99
103
  export { buildClusterDag } from './bake/clusterLod.ts';
package/dist/index.js CHANGED
@@ -170,10 +170,15 @@ export { localiseNodes } from './localise.js';
170
170
  */
171
171
  export { colliderBeside, levelsBeside } from './readModel.js';
172
172
  /*
173
- * DDS block decoding, for the block-compressed textures a vehicle model carries. Baker work: RGBA
174
- * is larger than the source and nothing ships a block decoder into a frame.
173
+ * DDS for the baker: `ddsBlocks` keeps a block-compressed surface's blocks and stored chain for a
174
+ * `CODEC_BC` texture, and `ddsToRgba` decodes level 0 for what cannot be kept. `decodeBc` is the
175
+ * block decoder both share with the loader, which runs it on a device that cannot sample BC.
175
176
  */
176
177
  export { ddsToRgba, isDds } from './dds.js';
178
+ export { ddsBlocks } from './ddsBlocks.js';
179
+ export { decodeBc } from './bcDecode.js';
180
+ export { decodeBcImage } from './bcImage.js';
181
+ export { flipBc5Green } from './bcFlip.js';
177
182
  /*
178
183
  * **The offline half of the GPU-driven pipeline, which nothing outside this package could reach.**
179
184
  *
@@ -0,0 +1,40 @@
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
+ /** One joint and how much of a vertex it moves. */
15
+ export interface Influence {
16
+ readonly joint: number;
17
+ readonly weight: number;
18
+ }
19
+ /** The four arrays a mesh's influences land in, four floats per vertex each. */
20
+ export interface InfluenceSets {
21
+ readonly joints: Float32Array;
22
+ readonly weights: Float32Array;
23
+ readonly joints2: Float32Array;
24
+ readonly weights2: Float32Array;
25
+ }
26
+ /** What a vertex's influences needed: dropping past eight, rescaling, and a second set at all. */
27
+ export interface InfluenceReport {
28
+ readonly dropped: boolean;
29
+ readonly rescaled: boolean;
30
+ readonly second: boolean;
31
+ }
32
+ /** The most influences a vertex keeps: two sets of four. */
33
+ export declare const MAX_INFLUENCES = 8;
34
+ /**
35
+ * Write vertex `at`'s influences into `out`, heaviest first: the four heaviest into `joints` and
36
+ * `weights`, the next four into `joints2` and `weights2`, normalised to sum to one over all kept.
37
+ * A vertex with no positive weight is left all zero and reported as nothing, because inventing an
38
+ * influence would move geometry nobody rigged.
39
+ */
40
+ export declare function writeInfluences(influences: readonly Influence[] | undefined, out: InfluenceSets, at: number): InfluenceReport;