@driftengine/assets 4.8.2 → 4.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bc7Decode.d.ts +33 -0
- package/dist/bc7Decode.js +197 -0
- package/dist/bcAnswer.d.ts +28 -0
- package/dist/bcAnswer.js +15 -0
- package/dist/bcDecode.d.ts +28 -0
- package/dist/bcDecode.js +175 -0
- package/dist/bcFlip.d.ts +21 -0
- package/dist/bcFlip.js +52 -0
- package/dist/bcImage.d.ts +15 -0
- package/dist/bcImage.js +38 -0
- package/dist/bcLoad.d.ts +26 -0
- package/dist/bcLoad.js +113 -0
- package/dist/bcWorker.d.ts +1 -0
- package/dist/bcWorker.js +14 -0
- package/dist/dds.d.ts +16 -18
- package/dist/dds.js +20 -293
- package/dist/ddsBlocks.d.ts +3 -0
- package/dist/ddsBlocks.js +54 -0
- package/dist/ddsHeader.d.ts +32 -0
- package/dist/ddsHeader.js +119 -0
- package/dist/drftLoader.d.ts +18 -0
- package/dist/drftLoader.js +98 -8
- package/dist/fbx.js +32 -13
- package/dist/fbxRig.d.ts +0 -18
- package/dist/fbxRig.js +0 -37
- package/dist/gltf.js +74 -12
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -2
- package/dist/influences.d.ts +40 -0
- package/dist/influences.js +53 -0
- package/dist/weld.js +3 -0
- package/package.json +4 -4
- package/src/bc7Decode.ts +237 -0
- package/src/bcAnswer.ts +45 -0
- package/src/bcDecode.ts +218 -0
- package/src/bcFlip.ts +55 -0
- package/src/bcImage.ts +58 -0
- package/src/bcLoad.ts +154 -0
- package/src/bcWorker.ts +20 -0
- package/src/dds.ts +21 -337
- package/src/ddsBlocks.ts +63 -0
- package/src/ddsHeader.ts +160 -0
- package/src/drftLoader.ts +115 -9
- package/src/fbx.ts +34 -18
- package/src/fbxRig.ts +0 -41
- package/src/gltf.ts +75 -13
- package/src/index.ts +7 -2
- package/src/influences.ts +77 -0
- package/src/weld.ts +3 -0
|
@@ -0,0 +1,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
|
+
}
|
package/dist/drftLoader.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/drftLoader.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
1192
|
-
if (
|
|
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
|
-
|
|
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
|
|
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
|
|
1312
|
-
'
|
|
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
|
|
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 &&
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
|
703
|
-
`renormalised, because
|
|
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
|
-
|
|
963
|
-
|
|
964
|
-
if (skin !== undefined
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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
|
|
174
|
-
*
|
|
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;
|