@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
package/src/ddsBlocks.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A block-compressed DDS kept as it was authored: its blocks, untouched, and every mip level its
|
|
3
|
+
* author stored, ready for a `CODEC_BC` payload.
|
|
4
|
+
*
|
|
5
|
+
* **Why keep them rather than decode them.** A BC7 texture is a byte a texel on the GPU where RGBA
|
|
6
|
+
* is four, and its chain is the one its author built — often filtered with care a box filter does
|
|
7
|
+
* not take, and for a normal map renormalised at every level. Decoding to RGBA and letting the
|
|
8
|
+
* engine generate mips throws both away, and costs a phone's memory and a desktop's bandwidth for
|
|
9
|
+
* the privilege. Where a device cannot sample a format, the loader decodes the same blocks there.
|
|
10
|
+
*
|
|
11
|
+
* **What it refuses, so the baker can fall back to decoding level 0:** a chain longer than the
|
|
12
|
+
* image has levels for, a chain whose bytes stop before its last level, a volume texture (each of
|
|
13
|
+
* whose levels holds several slices, so its layout is not a chain of images), and an uncompressed
|
|
14
|
+
* surface, which has no blocks to keep. Bytes after the last level are left where they are; a
|
|
15
|
+
* classic cube map's other five faces are such bytes, so its first face is what is kept.
|
|
16
|
+
*/
|
|
17
|
+
import { bcChainLength, bcLevelBytes, DrftError } from '@driftengine/drft';
|
|
18
|
+
import type { BcImage } from '@driftengine/drft';
|
|
19
|
+
|
|
20
|
+
import { DDPF_FOURCC, HEADER_BYTES, isDds, readDdsSurface } from './ddsHeader.ts';
|
|
21
|
+
|
|
22
|
+
/** `DDSCAPS2_VOLUME`: each level is a stack of slices rather than one image. */
|
|
23
|
+
const DDSCAPS2_VOLUME = 0x200000;
|
|
24
|
+
|
|
25
|
+
/** The surface's blocks and stored levels, as views over `bytes`. */
|
|
26
|
+
export function ddsBlocks(bytes: Uint8Array): BcImage {
|
|
27
|
+
if (!isDds(bytes)) throw new DrftError('dds: these bytes do not begin "DDS "');
|
|
28
|
+
if (bytes.length < HEADER_BYTES) {
|
|
29
|
+
throw new DrftError('dds: the file is shorter than its own header');
|
|
30
|
+
}
|
|
31
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
32
|
+
if ((view.getUint32(80, true) & DDPF_FOURCC) === 0) {
|
|
33
|
+
throw new DrftError('dds: this surface is uncompressed, so it has no blocks to keep');
|
|
34
|
+
}
|
|
35
|
+
if ((view.getUint32(112, true) & DDSCAPS2_VOLUME) !== 0) {
|
|
36
|
+
throw new DrftError(
|
|
37
|
+
'dds: this is a volume texture, whose levels are stacks of slices rather than a chain',
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const surface = readDdsSurface(bytes, view);
|
|
41
|
+
const { format, width, height, mipCount } = surface;
|
|
42
|
+
const most = bcChainLength(width, height);
|
|
43
|
+
if (mipCount > most) {
|
|
44
|
+
throw new DrftError(
|
|
45
|
+
`dds: ${surface.named} declares ${mipCount} levels for a ${width}x${height} image, which ` +
|
|
46
|
+
`has at most ${most}`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const levels: Uint8Array[] = [];
|
|
50
|
+
let at = surface.dataAt;
|
|
51
|
+
for (let i = 0; i < mipCount; i++) {
|
|
52
|
+
const size = bcLevelBytes(format, width, height, i);
|
|
53
|
+
if (at + size > bytes.length) {
|
|
54
|
+
throw new DrftError(
|
|
55
|
+
`dds: level ${i} of ${mipCount} needs ${size} bytes and ${Math.max(0, bytes.length - at)} ` +
|
|
56
|
+
'are left',
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
levels.push(bytes.subarray(at, at + size));
|
|
60
|
+
at += size;
|
|
61
|
+
}
|
|
62
|
+
return { format, srgb: surface.srgb, width, height, levels };
|
|
63
|
+
}
|
package/src/ddsHeader.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
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
|
+
import type { BcFormat } from '@driftengine/drft';
|
|
18
|
+
|
|
19
|
+
export const HEADER_BYTES = 128;
|
|
20
|
+
/** `DDS_HEADER_DXT10`: the extension a `DX10` FourCC puts between the header and the surface. */
|
|
21
|
+
const DX10_EXTENSION_BYTES = 20;
|
|
22
|
+
/** `DDPF_FOURCC`: the pixel format names a compression rather than describing channels. */
|
|
23
|
+
export const DDPF_FOURCC = 0x4;
|
|
24
|
+
|
|
25
|
+
/** Whether these bytes are a DDS at all. The magic, and nothing inferred from a file name. */
|
|
26
|
+
export function isDds(bytes: Uint8Array): boolean {
|
|
27
|
+
return (
|
|
28
|
+
bytes.length >= 4 &&
|
|
29
|
+
bytes[0] === 0x44 &&
|
|
30
|
+
bytes[1] === 0x44 &&
|
|
31
|
+
bytes[2] === 0x53 &&
|
|
32
|
+
bytes[3] === 0x20
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The DXGI block formats, and which of them declare sRGB.
|
|
38
|
+
*
|
|
39
|
+
* **sRGB is a statement about how the values are *read* rather than about how they are stored.**
|
|
40
|
+
* BC3 announced as `DXGI_FORMAT_BC3_UNORM_SRGB` is bit for bit the BC3 announced as `DXT5`: the
|
|
41
|
+
* same blocks and the same mip chain. Measured from outside on one bundle's paint texture —
|
|
42
|
+
* 5,592,580 bytes against 5,592,560 for the classic spelling of the same 2048² chain, which is
|
|
43
|
+
* 148 + 5,592,432 against 128 + 5,592,432. The typeless spellings read as linear.
|
|
44
|
+
*
|
|
45
|
+
* **What is deliberately absent.** `BC4_SNORM` (81) and `BC5_SNORM` (84), whose values are signed,
|
|
46
|
+
* and BC6H (94–96), whose values are half floats — mapping any of them would decode to wrong numbers
|
|
47
|
+
* rather than to an error, which is the one failure this reader is written to avoid.
|
|
48
|
+
*/
|
|
49
|
+
const DXGI_BLOCK: Readonly<Record<number, readonly [BcFormat, boolean]>> = {
|
|
50
|
+
70: ['bc1', false],
|
|
51
|
+
71: ['bc1', false],
|
|
52
|
+
72: ['bc1', true],
|
|
53
|
+
73: ['bc2', false],
|
|
54
|
+
74: ['bc2', false],
|
|
55
|
+
75: ['bc2', true],
|
|
56
|
+
76: ['bc3', false],
|
|
57
|
+
77: ['bc3', false],
|
|
58
|
+
78: ['bc3', true],
|
|
59
|
+
79: ['bc4', false],
|
|
60
|
+
80: ['bc4', false],
|
|
61
|
+
82: ['bc5', false],
|
|
62
|
+
83: ['bc5', false],
|
|
63
|
+
97: ['bc7', false],
|
|
64
|
+
98: ['bc7', false],
|
|
65
|
+
99: ['bc7', true],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** The classic spellings. BC7 has none: only a `DX10` header can name it. */
|
|
69
|
+
const FOURCC_BLOCK: Readonly<Record<string, BcFormat>> = {
|
|
70
|
+
DXT1: 'bc1',
|
|
71
|
+
DXT3: 'bc2',
|
|
72
|
+
DXT5: 'bc3',
|
|
73
|
+
ATI1: 'bc4',
|
|
74
|
+
BC4U: 'bc4',
|
|
75
|
+
ATI2: 'bc5',
|
|
76
|
+
BC5U: 'bc5',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** What a block-compressed DDS holds, read from its header alone. */
|
|
80
|
+
export interface DdsSurface {
|
|
81
|
+
readonly format: BcFormat;
|
|
82
|
+
readonly srgb: boolean;
|
|
83
|
+
readonly width: number;
|
|
84
|
+
readonly height: number;
|
|
85
|
+
/**
|
|
86
|
+
* The levels the header says follow. Zero is read as one, which is what Microsoft's own loader
|
|
87
|
+
* does: many writers leave the count zero, and its flag unset, on a surface with no chain.
|
|
88
|
+
*/
|
|
89
|
+
readonly mipCount: number;
|
|
90
|
+
/** Where level 0's first block starts. */
|
|
91
|
+
readonly dataAt: number;
|
|
92
|
+
/** How the header named the format, for a message: a FourCC, or `DXGI <n>`. */
|
|
93
|
+
readonly named: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The surface a block-compressed DDS declares. The caller has checked the magic, the length of the
|
|
98
|
+
* classic header and that the pixel format names a compression.
|
|
99
|
+
*
|
|
100
|
+
* **What a `DX10` header can say that a reader must refuse.** `resourceDimension` other than 3
|
|
101
|
+
* (`TEXTURE2D`), `miscFlag` bit 2 (a cube map) or an `arraySize` above 1. Taking one would hand the
|
|
102
|
+
* caller six faces or twenty slices claiming to be a single surface — a picture rather than an
|
|
103
|
+
* error.
|
|
104
|
+
*/
|
|
105
|
+
export function readDdsSurface(bytes: Uint8Array, view: DataView): DdsSurface {
|
|
106
|
+
const height = view.getUint32(12, true);
|
|
107
|
+
const width = view.getUint32(16, true);
|
|
108
|
+
const mipCount = Math.max(1, view.getUint32(28, true));
|
|
109
|
+
const fourcc = String.fromCharCode(
|
|
110
|
+
bytes[84] as number,
|
|
111
|
+
bytes[85] as number,
|
|
112
|
+
bytes[86] as number,
|
|
113
|
+
bytes[87] as number,
|
|
114
|
+
);
|
|
115
|
+
if (fourcc !== 'DX10') {
|
|
116
|
+
const format = FOURCC_BLOCK[fourcc];
|
|
117
|
+
if (format === undefined) {
|
|
118
|
+
throw new DrftError(
|
|
119
|
+
`dds: "${fourcc}" is not a format this reader decodes. It reads BC1 (DXT1), BC2 (DXT3), ` +
|
|
120
|
+
'BC3 (DXT5), BC4 (ATI1) and BC5 (ATI2), and BC7 behind a DX10 header. Convert the ' +
|
|
121
|
+
'texture to PNG, or re-export it as one of those.',
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return { format, srgb: false, width, height, mipCount, dataAt: HEADER_BYTES, named: fourcc };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const dataAt = HEADER_BYTES + DX10_EXTENSION_BYTES;
|
|
128
|
+
if (bytes.length < dataAt) {
|
|
129
|
+
throw new DrftError(
|
|
130
|
+
'dds: a DX10 header is declared and the twenty-byte extension is not there',
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
const dxgi = view.getUint32(HEADER_BYTES, true);
|
|
134
|
+
const dimension = view.getUint32(HEADER_BYTES + 4, true);
|
|
135
|
+
const miscFlag = view.getUint32(HEADER_BYTES + 8, true);
|
|
136
|
+
const arraySize = view.getUint32(HEADER_BYTES + 12, true);
|
|
137
|
+
if (dimension !== 3) {
|
|
138
|
+
throw new DrftError(
|
|
139
|
+
`dds: this DX10 surface declares resourceDimension ${dimension}, and only 3 (a plain 2D ` +
|
|
140
|
+
'texture) is read here. A 1D or volume texture is a different thing wearing the same header.',
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if ((miscFlag & 0x4) !== 0) {
|
|
144
|
+
throw new DrftError('dds: this is a cube map, and only a plain 2D texture is read here.');
|
|
145
|
+
}
|
|
146
|
+
if (arraySize > 1) {
|
|
147
|
+
throw new DrftError(
|
|
148
|
+
`dds: this is a texture array of ${arraySize}, and only a plain 2D texture is read here.`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
const entry = DXGI_BLOCK[dxgi];
|
|
152
|
+
if (entry === undefined) {
|
|
153
|
+
throw new DrftError(
|
|
154
|
+
`dds: DXGI format ${dxgi} is not one this reader decodes. It reads BC1 to BC5 and BC7, ` +
|
|
155
|
+
'unsigned. Convert the texture to PNG, or re-export it as one of those.',
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
const [format, srgb] = entry;
|
|
159
|
+
return { format, srgb, width, height, mipCount, dataAt, named: `DXGI ${dxgi}` };
|
|
160
|
+
}
|
package/src/drftLoader.ts
CHANGED
|
@@ -10,6 +10,12 @@ import { placeFields } from './fieldPlacement.ts';
|
|
|
10
10
|
import type { DrftFieldPlacement } from './fieldPlacement.ts';
|
|
11
11
|
import type { DrftTexture } from '@driftengine/drft';
|
|
12
12
|
import { TextureSet, textureColorSpaces } from './drftTextures.ts';
|
|
13
|
+
import { bcPlan, createBcDecoder } from './bcLoad.ts';
|
|
14
|
+
import type { BcDecoder } from './bcLoad.ts';
|
|
15
|
+
import { decodeBc } from './bcDecode.ts';
|
|
16
|
+
import { isCompressedSource, uploadsCompressed } from '@driftengine/core';
|
|
17
|
+
import { CODEC_BC, codecName, readBcPayload } from '@driftengine/drft';
|
|
18
|
+
import type { BcImage } from '@driftengine/drft';
|
|
13
19
|
import { drawKeyOf, resolveDrawGrouping } from './drawKey.ts';
|
|
14
20
|
import type { DrawGrouping, DrawSurfaceOverride } from './drawKey.ts';
|
|
15
21
|
import {
|
|
@@ -323,7 +329,10 @@ export class DrftLoader {
|
|
|
323
329
|
private readonly bound: boolean[] = [];
|
|
324
330
|
private boundCount = 0;
|
|
325
331
|
private readonly singles: MeshHandle[] = [];
|
|
326
|
-
private readonly images: (
|
|
332
|
+
private readonly images: (LoadedImage | null)[] = [];
|
|
333
|
+
/** Started at the first BC texture that has to be decoded, and only then. See `bcLoad.ts`. */
|
|
334
|
+
private bcDecoder: BcDecoder | null = null;
|
|
335
|
+
private warnedBc = false;
|
|
327
336
|
private readonly chunks: DrftTexture[] = [];
|
|
328
337
|
private materials: readonly DrftMaterial[] = [];
|
|
329
338
|
/**
|
|
@@ -812,6 +821,10 @@ export class DrftLoader {
|
|
|
812
821
|
* visibly wrong, which a consumer's loading bar reported to a person as finished.
|
|
813
822
|
*/
|
|
814
823
|
this.set({ phase: 'textures' });
|
|
824
|
+
if (texture.codec === CODEC_BC) {
|
|
825
|
+
this.acceptBc(texture, ordinal);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
815
828
|
/*
|
|
816
829
|
* Decoded through the browser's own image path, so the engine ships no decoder, and
|
|
817
830
|
* **each one fails on its own**: a texture is the part of an asset most likely to be
|
|
@@ -969,7 +982,7 @@ export class DrftLoader {
|
|
|
969
982
|
if (ordinal !== undefined) {
|
|
970
983
|
const take = this.options.onImage;
|
|
971
984
|
if (take === undefined) this.rebuildTextures(ordinal);
|
|
972
|
-
else take(this.chunks[ordinal]?.name ?? '', this.images[ordinal] ?? null);
|
|
985
|
+
else take(this.chunks[ordinal]?.name ?? '', bitmapOf(this.images[ordinal] ?? null));
|
|
973
986
|
/*
|
|
974
987
|
* Counted here, once, and only for the image itself.
|
|
975
988
|
*
|
|
@@ -1047,6 +1060,8 @@ export class DrftLoader {
|
|
|
1047
1060
|
if (this.disposed) return;
|
|
1048
1061
|
this.disposed = true;
|
|
1049
1062
|
this.regionStore.dispose();
|
|
1063
|
+
this.bcDecoder?.dispose();
|
|
1064
|
+
this.bcDecoder = null;
|
|
1050
1065
|
/*
|
|
1051
1066
|
* Everything this class ever made, each disposed once.
|
|
1052
1067
|
*
|
|
@@ -1444,7 +1459,8 @@ export class DrftLoader {
|
|
|
1444
1459
|
*/
|
|
1445
1460
|
const existing = previous?.at(ordinal) ?? null;
|
|
1446
1461
|
const arrived = this.images[ordinal] ?? null;
|
|
1447
|
-
|
|
1462
|
+
/* Blocks never arrive as a replacement: a BC texture has no preview to replace. */
|
|
1463
|
+
if (existing !== null && arrived !== null && !isCompressedSource(arrived)) {
|
|
1448
1464
|
this.renderer.updateSurfaceTexture(existing, arrived);
|
|
1449
1465
|
return;
|
|
1450
1466
|
}
|
|
@@ -1458,18 +1474,90 @@ export class DrftLoader {
|
|
|
1458
1474
|
this.textureSet = TextureSet.from(this.chunks, (_, at) => {
|
|
1459
1475
|
const held = previous?.at(at) ?? null;
|
|
1460
1476
|
if (held !== null) return held;
|
|
1461
|
-
const
|
|
1462
|
-
if (
|
|
1477
|
+
const image = this.images[at] ?? null;
|
|
1478
|
+
if (image === null) return null;
|
|
1463
1479
|
const anisotropy = this.options.anisotropy;
|
|
1464
1480
|
const wrap = this.options.textureWrap;
|
|
1465
|
-
|
|
1481
|
+
const colorSpace = spaces[at] ?? 'linear';
|
|
1482
|
+
return this.renderer.createSurfaceTexture(this.uploadable(image, colorSpace === 'srgb'), {
|
|
1466
1483
|
...(anisotropy === undefined ? {} : { anisotropy }),
|
|
1467
1484
|
...(wrap === undefined ? {} : { wrap }),
|
|
1468
|
-
colorSpace
|
|
1485
|
+
colorSpace,
|
|
1469
1486
|
});
|
|
1470
1487
|
});
|
|
1471
1488
|
}
|
|
1472
1489
|
|
|
1490
|
+
/**
|
|
1491
|
+
* A BC texture: its blocks kept, to go up as they are where this device samples them, or decoded
|
|
1492
|
+
* off the main thread where it does not — which on a phone is every one. See `bcLoad.ts`.
|
|
1493
|
+
*
|
|
1494
|
+
* **A consumer taking images through `onImage` gets a decoded image either way**, as an ordinary
|
|
1495
|
+
* picture: what it builds from them is its own, and a texture array of bitmaps cannot hold blocks.
|
|
1496
|
+
* Like every image, a BC texture fails on its own and its surfaces draw untextured.
|
|
1497
|
+
*/
|
|
1498
|
+
private acceptBc(texture: DrftTexture, ordinal: number): void {
|
|
1499
|
+
const done = (image: LoadedImage | null): void => {
|
|
1500
|
+
if (this.disposed) return;
|
|
1501
|
+
this.images[ordinal] = image;
|
|
1502
|
+
this.sharp[ordinal] = true;
|
|
1503
|
+
this.imageQueue.push(ordinal);
|
|
1504
|
+
};
|
|
1505
|
+
const failed = (error: unknown): void => {
|
|
1506
|
+
console.warn(
|
|
1507
|
+
`DrftLoader: image ${ordinal} did not decode; its surfaces stay untextured` +
|
|
1508
|
+
` (codec BC, ${texture.bytes.length} bytes, ${texture.width}x${texture.height})`,
|
|
1509
|
+
error,
|
|
1510
|
+
);
|
|
1511
|
+
done(null);
|
|
1512
|
+
};
|
|
1513
|
+
let image: BcImage;
|
|
1514
|
+
try {
|
|
1515
|
+
image = readBcPayload(texture.width, texture.height, texture.bytes);
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
failed(error);
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
const asImage = this.options.onImage !== undefined;
|
|
1521
|
+
const srgb = textureColorSpaces(this.materials, ordinal + 1)[ordinal] === 'srgb';
|
|
1522
|
+
if (!asImage && bcPlan(image, srgb, this.renderer.compressedFormats) === 'blocks') {
|
|
1523
|
+
done(image);
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
if (this.bcDecoder === null) this.bcDecoder = createBcDecoder();
|
|
1527
|
+
const decoder = this.bcDecoder;
|
|
1528
|
+
decoder
|
|
1529
|
+
.decode(image, asImage)
|
|
1530
|
+
.then(async (rgba) => {
|
|
1531
|
+
/* Said once, where a worker could not be had or failed: a slower load nobody could
|
|
1532
|
+
otherwise attribute. */
|
|
1533
|
+
if (decoder.reason !== '' && !this.warnedBc) {
|
|
1534
|
+
this.warnedBc = true;
|
|
1535
|
+
console.warn(`DrftLoader: ${decoder.reason}`);
|
|
1536
|
+
}
|
|
1537
|
+
const pixels = imageDataOf(rgba, image.width, image.height);
|
|
1538
|
+
done(asImage ? await createImageBitmap(pixels, AS_AUTHORED) : pixels);
|
|
1539
|
+
})
|
|
1540
|
+
.catch(failed);
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
/**
|
|
1544
|
+
* What goes to `createSurfaceTexture`: the image as it is, unless it is blocks this device cannot
|
|
1545
|
+
* take in the colour space the slot reads — a material read after its texture arrived can change
|
|
1546
|
+
* that — in which case they are decoded here, on the spot.
|
|
1547
|
+
*/
|
|
1548
|
+
private uploadable(image: LoadedImage, srgb: boolean): ImageBitmap | ImageData | BcImage {
|
|
1549
|
+
if (!isCompressedSource(image)) return image;
|
|
1550
|
+
const { format, width, height } = image;
|
|
1551
|
+
if (uploadsCompressed(format, srgb, width, height, this.renderer.compressedFormats)) {
|
|
1552
|
+
return image;
|
|
1553
|
+
}
|
|
1554
|
+
return imageDataOf(
|
|
1555
|
+
decodeBc(format, width, height, image.levels[0] as Uint8Array),
|
|
1556
|
+
width,
|
|
1557
|
+
height,
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1473
1561
|
/** One place that writes the progress, so every field stays consistent with the phase. */
|
|
1474
1562
|
private set(patch: Partial<DrftLoadProgress>): void {
|
|
1475
1563
|
const next = { ...this.state, ...patch };
|
|
@@ -1554,6 +1642,23 @@ const AS_AUTHORED = {
|
|
|
1554
1642
|
colorSpaceConversion: 'none',
|
|
1555
1643
|
} as const satisfies ImageBitmapOptions;
|
|
1556
1644
|
|
|
1645
|
+
/** What the loader holds for an image: decoded, or blocks to upload as they are. */
|
|
1646
|
+
type LoadedImage = ImageBitmap | ImageData | BcImage;
|
|
1647
|
+
|
|
1648
|
+
/** RGBA as an `ImageData`, which every upload path takes as it takes a bitmap. */
|
|
1649
|
+
function imageDataOf(rgba: Uint8Array, width: number, height: number): ImageData {
|
|
1650
|
+
return new ImageData(
|
|
1651
|
+
new Uint8ClampedArray(rgba.buffer as ArrayBuffer, rgba.byteOffset, rgba.length),
|
|
1652
|
+
width,
|
|
1653
|
+
height,
|
|
1654
|
+
);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
/** The bitmap `onImage` is handed. Only bitmaps reach that path, BC decoded to one on the way. */
|
|
1658
|
+
function bitmapOf(image: LoadedImage | null): ImageBitmap | null {
|
|
1659
|
+
return image === null || isCompressedSource(image) || 'data' in image ? null : image;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1557
1662
|
async function decodeImage(texture: DrftTexture, longestSide?: number): Promise<ImageBitmap> {
|
|
1558
1663
|
/*
|
|
1559
1664
|
* **A raw texture has no decoder, because it is already decoded.**
|
|
@@ -1589,8 +1694,9 @@ async function decodeImage(texture: DrftTexture, longestSide?: number): Promise<
|
|
|
1589
1694
|
const type = imageTypeFor(texture.codec);
|
|
1590
1695
|
if (type === null) {
|
|
1591
1696
|
throw new DrftError(
|
|
1592
|
-
`texture codec ${texture.codec} is not one this loader
|
|
1593
|
-
'
|
|
1697
|
+
`texture codec ${codecName(texture.codec)} is not one this loader reads: a newer baker ` +
|
|
1698
|
+
'wrote it, or the file is damaged. Update @driftengine/assets, or bake with the baker ' +
|
|
1699
|
+
'that matches it',
|
|
1594
1700
|
);
|
|
1595
1701
|
}
|
|
1596
1702
|
const blob = new Blob([texture.bytes.slice()], { type });
|
package/src/fbx.ts
CHANGED
|
@@ -32,7 +32,8 @@ import type { UpAxis } from './orient.ts';
|
|
|
32
32
|
import { FALLBACK_MATERIAL, readMaterials } from './fbxMaterial.ts';
|
|
33
33
|
import type { DrftMaterial } from '@driftengine/drft';
|
|
34
34
|
import type { AssetReference } from './assetPath.ts';
|
|
35
|
-
import { bareName, indexObjects, readClips, readSkin
|
|
35
|
+
import { bareName, indexObjects, readClips, readSkin } from './fbxRig.ts';
|
|
36
|
+
import { writeInfluences } from './influences.ts';
|
|
36
37
|
import type { AnimationClip, DrftSkin } from '@driftengine/drft';
|
|
37
38
|
|
|
38
39
|
/** One value repeated per vertex, for a material constant this format has nowhere else to put. */
|
|
@@ -688,6 +689,16 @@ export function fbxToMeshes(
|
|
|
688
689
|
const outUvs: number[] | null = uvLayer === null ? null : [];
|
|
689
690
|
const outJoints: number[] | null = skin === null ? null : [];
|
|
690
691
|
const outWeights: number[] | null = skin === null ? null : [];
|
|
692
|
+
/* The fifth to eighth influences, kept only if some vertex needed them. See influences.ts. */
|
|
693
|
+
const outJoints2: number[] | null = skin === null ? null : [];
|
|
694
|
+
const outWeights2: number[] | null = skin === null ? null : [];
|
|
695
|
+
let secondInfluences = false;
|
|
696
|
+
const scratch = {
|
|
697
|
+
joints: new Float32Array(4),
|
|
698
|
+
weights: new Float32Array(4),
|
|
699
|
+
joints2: new Float32Array(4),
|
|
700
|
+
weights2: new Float32Array(4),
|
|
701
|
+
};
|
|
691
702
|
const indices: number[] = [];
|
|
692
703
|
/*
|
|
693
704
|
* A mesh with no normals is not deduplicated, and that is the geometry speaking rather than
|
|
@@ -805,23 +816,25 @@ export function fbxToMeshes(
|
|
|
805
816
|
outColors.push(r, g, b);
|
|
806
817
|
if (outUvs !== null) outUvs.push(u, v);
|
|
807
818
|
|
|
808
|
-
if (
|
|
809
|
-
outJoints
|
|
810
|
-
outWeights
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
);
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
819
|
+
if (
|
|
820
|
+
outJoints !== null &&
|
|
821
|
+
outWeights !== null &&
|
|
822
|
+
outJoints2 !== null &&
|
|
823
|
+
outWeights2 !== null &&
|
|
824
|
+
skin !== null
|
|
825
|
+
) {
|
|
826
|
+
scratch.joints.fill(0);
|
|
827
|
+
scratch.weights.fill(0);
|
|
828
|
+
scratch.joints2.fill(0);
|
|
829
|
+
scratch.weights2.fill(0);
|
|
830
|
+
const report = writeInfluences(skin.influences.get(vertexIndex), scratch, 0);
|
|
831
|
+
outJoints.push(...scratch.joints);
|
|
832
|
+
outWeights.push(...scratch.weights);
|
|
833
|
+
outJoints2.push(...scratch.joints2);
|
|
834
|
+
outWeights2.push(...scratch.weights2);
|
|
823
835
|
droppedInfluences ||= report.dropped;
|
|
824
836
|
rescaledInfluences ||= report.rescaled;
|
|
837
|
+
secondInfluences ||= report.second;
|
|
825
838
|
}
|
|
826
839
|
}
|
|
827
840
|
|
|
@@ -834,8 +847,8 @@ export function fbxToMeshes(
|
|
|
834
847
|
}
|
|
835
848
|
if (droppedInfluences) {
|
|
836
849
|
warnings.push(
|
|
837
|
-
`${name}: a vertex is held by more than
|
|
838
|
-
`renormalised, because
|
|
850
|
+
`${name}: a vertex is held by more than eight joints; the eight largest are kept and ` +
|
|
851
|
+
`renormalised, because eight is what the skinning palette reads.`,
|
|
839
852
|
);
|
|
840
853
|
}
|
|
841
854
|
if (rescaledInfluences) {
|
|
@@ -861,6 +874,9 @@ export function fbxToMeshes(
|
|
|
861
874
|
...(outJoints === null || outWeights === null
|
|
862
875
|
? {}
|
|
863
876
|
: { joints: Float32Array.from(outJoints), weights: Float32Array.from(outWeights) }),
|
|
877
|
+
...(outJoints2 === null || outWeights2 === null || !secondInfluences
|
|
878
|
+
? {}
|
|
879
|
+
: { joints2: Float32Array.from(outJoints2), weights2: Float32Array.from(outWeights2) }),
|
|
864
880
|
});
|
|
865
881
|
perMesh.push(material);
|
|
866
882
|
}
|
package/src/fbxRig.ts
CHANGED
|
@@ -30,9 +30,6 @@ import type { FbxNode } from './fbx.ts';
|
|
|
30
30
|
/** One second, in the integer unit `KeyTime` counts. FBX's own constant. */
|
|
31
31
|
const KTIME_PER_SECOND = 46186158000;
|
|
32
32
|
|
|
33
|
-
/** The most influences a vertex may carry, which is what the palette shader reads. */
|
|
34
|
-
const MAX_INFLUENCES = 4;
|
|
35
|
-
|
|
36
33
|
const IDENTITY: readonly number[] = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
37
34
|
|
|
38
35
|
function findChild(node: FbxNode, name: string): FbxNode | undefined {
|
|
@@ -332,44 +329,6 @@ export function readSkin(
|
|
|
332
329
|
return { joints, inverseBind, influences, modelOfJoint: order };
|
|
333
330
|
}
|
|
334
331
|
|
|
335
|
-
/**
|
|
336
|
-
* One control point's four influences, largest first, normalised, written into a caller's arrays.
|
|
337
|
-
*
|
|
338
|
-
* **Four is the shader's number and the file's is unbounded.** A rig authored with six influences
|
|
339
|
-
* on a shoulder is ordinary, so the four largest are kept and renormalised rather than the first
|
|
340
|
-
* four taken: dropping by *order* would discard whichever the exporter happened to write last,
|
|
341
|
-
* which on a shoulder is as likely to be the dominant one as not.
|
|
342
|
-
*
|
|
343
|
-
* Returns true where something was dropped or rescaled, so the caller can say so once for the mesh
|
|
344
|
-
* rather than once per vertex.
|
|
345
|
-
*/
|
|
346
|
-
export function writeInfluences(
|
|
347
|
-
influences: readonly { joint: number; weight: number }[] | undefined,
|
|
348
|
-
joints: Float32Array,
|
|
349
|
-
weights: Float32Array,
|
|
350
|
-
at: number,
|
|
351
|
-
): { dropped: boolean; rescaled: boolean } {
|
|
352
|
-
if (influences === undefined || influences.length === 0)
|
|
353
|
-
return { dropped: false, rescaled: false };
|
|
354
|
-
const sorted = [...influences].sort((a, b) => b.weight - a.weight);
|
|
355
|
-
const kept = sorted.slice(0, MAX_INFLUENCES);
|
|
356
|
-
let sum = 0;
|
|
357
|
-
for (const influence of kept) sum += influence.weight;
|
|
358
|
-
if (sum <= 0) return { dropped: false, rescaled: false };
|
|
359
|
-
for (let i = 0; i < kept.length; i++) {
|
|
360
|
-
const influence = kept[i] as { joint: number; weight: number };
|
|
361
|
-
joints[at * MAX_INFLUENCES + i] = influence.joint;
|
|
362
|
-
weights[at * MAX_INFLUENCES + i] = influence.weight / sum;
|
|
363
|
-
}
|
|
364
|
-
let total = 0;
|
|
365
|
-
for (const influence of sorted) total += influence.weight;
|
|
366
|
-
return {
|
|
367
|
-
dropped: sorted.length > MAX_INFLUENCES,
|
|
368
|
-
/* A tenth of a per cent, which is wider than float noise and narrower than an authoring slip. */
|
|
369
|
-
rescaled: Math.abs(total - 1) > 1e-3,
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
|
|
373
332
|
/** Euler degrees in the file's default XYZ order, as a quaternion. */
|
|
374
333
|
function eulerToQuaternion(x: number, y: number, z: number): [number, number, number, number] {
|
|
375
334
|
const half = Math.PI / 360;
|
package/src/gltf.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { MAX_MORPH_TARGETS } from '@driftengine/core';
|
|
|
26
26
|
import { accessorFloats } from './gltfAccessor.ts';
|
|
27
27
|
import type { GltfSkin } from './gltfSkin.ts';
|
|
28
28
|
import { readGltfSkins } from './gltfSkin.ts';
|
|
29
|
+
import { writeInfluences } from './influences.ts';
|
|
29
30
|
|
|
30
31
|
/* The subset of the schema this reads. Everything else is ignored or refused by name. */
|
|
31
32
|
interface GltfAccessor {
|
|
@@ -1283,22 +1284,82 @@ function buildPrimitive(
|
|
|
1283
1284
|
|
|
1284
1285
|
let joints: Float32Array | undefined;
|
|
1285
1286
|
let weights: Float32Array | undefined;
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
if (skin !== undefined
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1287
|
+
let joints2: Float32Array | undefined;
|
|
1288
|
+
let weights2: Float32Array | undefined;
|
|
1289
|
+
if (skin !== undefined) {
|
|
1290
|
+
/*
|
|
1291
|
+
* Every `JOINTS_n`/`WEIGHTS_n` pair the primitive carries, remapped onto the sorted joints. One
|
|
1292
|
+
* pair is what nearly every export writes and keeps the path it always had; more go through
|
|
1293
|
+
* `writeInfluences`, which keeps the eight heaviest, heaviest first, across the two sets a mesh
|
|
1294
|
+
* carries. See influences.ts.
|
|
1295
|
+
*/
|
|
1296
|
+
const sets: { joints: Float32Array; weights: Float32Array }[] = [];
|
|
1297
|
+
for (let n = 0; ; n++) {
|
|
1298
|
+
const jointIndex = primitive.attributes[`JOINTS_${n}`];
|
|
1299
|
+
const weightIndex = primitive.attributes[`WEIGHTS_${n}`];
|
|
1300
|
+
if (jointIndex === undefined || weightIndex === undefined) break;
|
|
1301
|
+
const set = {
|
|
1302
|
+
joints: accessorFloats(doc, buffers, jointIndex, `${label} JOINTS_${n}`),
|
|
1303
|
+
weights: accessorFloats(doc, buffers, weightIndex, `${label} WEIGHTS_${n}`),
|
|
1304
|
+
};
|
|
1305
|
+
for (let i = 0; i < set.joints.length; i++) {
|
|
1306
|
+
const old = set.joints[i] as number;
|
|
1307
|
+
const moved = skin.remap[old];
|
|
1308
|
+
if (moved === undefined) {
|
|
1309
|
+
throw new DrftError(
|
|
1310
|
+
`gltf: ${label} weights a vertex to joint ${old}, and its skin has ${skin.remap.length}`,
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
set.joints[i] = moved;
|
|
1314
|
+
}
|
|
1315
|
+
sets.push(set);
|
|
1316
|
+
}
|
|
1317
|
+
const only = sets.length === 1 ? sets[0] : undefined;
|
|
1318
|
+
if (only !== undefined) {
|
|
1319
|
+
joints = only.joints;
|
|
1320
|
+
weights = only.weights;
|
|
1321
|
+
normaliseWeights(weights, label, warnings);
|
|
1322
|
+
} else if (sets.length > 1) {
|
|
1323
|
+
const vertices = (sets[0] as { joints: Float32Array }).joints.length / 4;
|
|
1324
|
+
const out = {
|
|
1325
|
+
joints: new Float32Array(vertices * 4),
|
|
1326
|
+
weights: new Float32Array(vertices * 4),
|
|
1327
|
+
joints2: new Float32Array(vertices * 4),
|
|
1328
|
+
weights2: new Float32Array(vertices * 4),
|
|
1329
|
+
};
|
|
1330
|
+
let dropped = false;
|
|
1331
|
+
let rescaled = false;
|
|
1332
|
+
let second = false;
|
|
1333
|
+
for (let v = 0; v < vertices; v++) {
|
|
1334
|
+
const influences: { joint: number; weight: number }[] = [];
|
|
1335
|
+
for (const set of sets) {
|
|
1336
|
+
for (let k = 0; k < 4; k++) {
|
|
1337
|
+
influences.push({
|
|
1338
|
+
joint: set.joints[v * 4 + k] as number,
|
|
1339
|
+
weight: set.weights[v * 4 + k] as number,
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
const report = writeInfluences(influences, out, v);
|
|
1344
|
+
dropped ||= report.dropped;
|
|
1345
|
+
rescaled ||= report.rescaled;
|
|
1346
|
+
second ||= report.second;
|
|
1347
|
+
}
|
|
1348
|
+
joints = out.joints;
|
|
1349
|
+
weights = out.weights;
|
|
1350
|
+
if (second) {
|
|
1351
|
+
joints2 = out.joints2;
|
|
1352
|
+
weights2 = out.weights2;
|
|
1353
|
+
}
|
|
1354
|
+
if (dropped) {
|
|
1355
|
+
warnings.push(
|
|
1356
|
+
`${label}: a vertex is held by more than eight joints; the eight largest are kept`,
|
|
1297
1357
|
);
|
|
1298
1358
|
}
|
|
1299
|
-
|
|
1359
|
+
if (rescaled) {
|
|
1360
|
+
warnings.push(`${label}: vertex weight sets did not sum to one and were rescaled`);
|
|
1361
|
+
}
|
|
1300
1362
|
}
|
|
1301
|
-
normaliseWeights(weights, label, warnings);
|
|
1302
1363
|
}
|
|
1303
1364
|
|
|
1304
1365
|
return {
|
|
@@ -1314,6 +1375,7 @@ function buildPrimitive(
|
|
|
1314
1375
|
...(uvs === undefined ? {} : { uvs }),
|
|
1315
1376
|
...(tangents === undefined ? {} : { tangents }),
|
|
1316
1377
|
...(joints === undefined || weights === undefined ? {} : { joints, weights }),
|
|
1378
|
+
...(joints2 === undefined || weights2 === undefined ? {} : { joints2, weights2 }),
|
|
1317
1379
|
...(morphTargets === undefined || morphTargetCount === undefined
|
|
1318
1380
|
? {}
|
|
1319
1381
|
: { morphTargets, morphTargetCount }),
|