@driftengine/drft 3.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +9 -0
- package/dist/animationData.d.ts +53 -0
- package/dist/animationData.js +13 -0
- package/dist/coarseFirst.d.ts +27 -0
- package/dist/coarseFirst.js +118 -0
- package/dist/drftColliders.d.ts +53 -0
- package/dist/drftColliders.js +137 -0
- package/dist/drftFormat.d.ts +454 -0
- package/dist/drftFormat.js +350 -0
- package/dist/drftRead.d.ts +121 -0
- package/dist/drftRead.js +487 -0
- package/dist/drftSkin.d.ts +40 -0
- package/dist/drftSkin.js +270 -0
- package/dist/drftStream.d.ts +170 -0
- package/dist/drftStream.js +315 -0
- package/dist/drftSubs.d.ts +18 -0
- package/dist/drftSubs.js +70 -0
- package/dist/drftWrite.d.ts +103 -0
- package/dist/drftWrite.js +478 -0
- package/dist/fixtures/v1-0.d.ts +9 -0
- package/dist/fixtures/v1-0.js +9 -0
- package/dist/fixtures/v1-1.d.ts +16 -0
- package/dist/fixtures/v1-1.js +16 -0
- package/dist/fixtures/v1-11.d.ts +14 -0
- package/dist/fixtures/v1-11.js +14 -0
- package/dist/fixtures/v1-2.d.ts +19 -0
- package/dist/fixtures/v1-2.js +19 -0
- package/dist/fixtures/v1-3.d.ts +18 -0
- package/dist/fixtures/v1-3.js +18 -0
- package/dist/fixtures/v1-4.d.ts +13 -0
- package/dist/fixtures/v1-4.js +13 -0
- package/dist/fixtures/v1-5.d.ts +14 -0
- package/dist/fixtures/v1-5.js +14 -0
- package/dist/fixtures/v1-6.d.ts +9 -0
- package/dist/fixtures/v1-6.js +9 -0
- package/dist/fixtures/v1-7.d.ts +9 -0
- package/dist/fixtures/v1-7.js +9 -0
- package/dist/fixtures/v1-9.d.ts +9 -0
- package/dist/fixtures/v1-9.js +9 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +29 -0
- package/dist/meshData.d.ts +210 -0
- package/dist/meshData.js +105 -0
- package/package.json +57 -0
- package/src/animationData.ts +58 -0
- package/src/coarseFirst.ts +113 -0
- package/src/drftColliders.ts +153 -0
- package/src/drftFormat.ts +537 -0
- package/src/drftRead.ts +652 -0
- package/src/drftSkin.ts +326 -0
- package/src/drftStream.ts +436 -0
- package/src/drftSubs.ts +86 -0
- package/src/drftWrite.ts +613 -0
- package/src/fixtures/v1-0.ts +10 -0
- package/src/fixtures/v1-1.ts +17 -0
- package/src/fixtures/v1-11.ts +15 -0
- package/src/fixtures/v1-2.ts +20 -0
- package/src/fixtures/v1-3.ts +19 -0
- package/src/fixtures/v1-4.ts +14 -0
- package/src/fixtures/v1-5.ts +15 -0
- package/src/fixtures/v1-6.ts +10 -0
- package/src/fixtures/v1-7.ts +10 -0
- package/src/fixtures/v1-9.ts +10 -0
- package/src/index.ts +56 -0
- package/src/meshData.ts +301 -0
package/src/drftSkin.ts
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import type { AnimationClip, DrftSkin, Joint, JointTrack, TrackPath } from './animationData.ts';
|
|
2
|
+
import { align, DrftError } from './drftFormat.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `NODE`, `SKIN` and `ANIM`: reading and writing the three chunks a rig needs.
|
|
6
|
+
*
|
|
7
|
+
* **All three are optional, which is what makes this additive.** Rule 2 of docs/FORMAT.md §4.4
|
|
8
|
+
* says an optional chunk a reader does not understand is skipped in silence, so a 1.6 file opens
|
|
9
|
+
* in a 1.5 reader as the static geometry it also holds — the right degradation, since a reader
|
|
10
|
+
* that has never heard of a skin cannot deform anything.
|
|
11
|
+
*
|
|
12
|
+
* **`NODE` was specified in §4.3 and implemented nowhere.** `CHUNK_NODE` has been declared and in
|
|
13
|
+
* `KNOWN_CHUNKS` since v1 with no writer and no reader; rigid TRS animation is the first thing to
|
|
14
|
+
* need a hierarchy, so this is a chunk the format has claimed for its whole life finally being
|
|
15
|
+
* built. Its payload is what §4.3 already promised — parent index, TRS, mesh index, name — rather
|
|
16
|
+
* than whatever would be convenient now.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** A node in the asset's own hierarchy. `mesh` is -1 for a node that draws nothing. */
|
|
20
|
+
export interface DrftNode {
|
|
21
|
+
readonly parent: number;
|
|
22
|
+
readonly translation: readonly [number, number, number];
|
|
23
|
+
readonly rotation: readonly [number, number, number, number];
|
|
24
|
+
readonly scale: readonly [number, number, number];
|
|
25
|
+
readonly mesh: number;
|
|
26
|
+
readonly name: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** i32 parent, 3 floats, 4 floats, 3 floats, i32 mesh. */
|
|
30
|
+
const NODE_ENTRY_BYTES = 4 + 12 + 16 + 12 + 4;
|
|
31
|
+
/** u32 joint, u32 path, u32 keyCount. */
|
|
32
|
+
const TRACK_ENTRY_BYTES = 12;
|
|
33
|
+
|
|
34
|
+
const PATHS: readonly TrackPath[] = ['translation', 'rotation', 'scale'];
|
|
35
|
+
|
|
36
|
+
const encoder = new TextEncoder();
|
|
37
|
+
const decoder = new TextDecoder();
|
|
38
|
+
|
|
39
|
+
/** Length-prefixed and padded to four, so the block that follows a name stays aligned. */
|
|
40
|
+
function encodeString(value: string): Uint8Array {
|
|
41
|
+
const text = encoder.encode(value);
|
|
42
|
+
const bytes = new Uint8Array(align(4 + text.length));
|
|
43
|
+
new DataView(bytes.buffer).setUint32(0, text.length, true);
|
|
44
|
+
bytes.set(text, 4);
|
|
45
|
+
return bytes;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Reads one, and answers where the next begins. */
|
|
49
|
+
function decodeString(view: DataView, at: number): { value: string; next: number } {
|
|
50
|
+
const length = view.getUint32(at, true);
|
|
51
|
+
const start = view.byteOffset + at + 4;
|
|
52
|
+
if (start + length > view.byteOffset + view.byteLength) {
|
|
53
|
+
throw new DrftError('a name runs past the end of its chunk');
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
value: decoder.decode(new Uint8Array(view.buffer, start, length)),
|
|
57
|
+
next: at + align(4 + length),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* --- NODE --- */
|
|
62
|
+
|
|
63
|
+
export function buildNodes(nodes: readonly DrftNode[]): Uint8Array {
|
|
64
|
+
const names = nodes.map((node) => encodeString(node.name));
|
|
65
|
+
let nameBytes = 0;
|
|
66
|
+
for (const name of names) nameBytes += name.length;
|
|
67
|
+
|
|
68
|
+
const bytes = new Uint8Array(align(8 + nodes.length * NODE_ENTRY_BYTES + nameBytes));
|
|
69
|
+
const view = new DataView(bytes.buffer);
|
|
70
|
+
view.setUint32(0, nodes.length, true);
|
|
71
|
+
view.setUint32(4, NODE_ENTRY_BYTES, true);
|
|
72
|
+
|
|
73
|
+
let at = 8;
|
|
74
|
+
for (const node of nodes) {
|
|
75
|
+
view.setInt32(at, node.parent, true);
|
|
76
|
+
for (let c = 0; c < 3; c++)
|
|
77
|
+
view.setFloat32(at + 4 + c * 4, node.translation[c] as number, true);
|
|
78
|
+
for (let c = 0; c < 4; c++) view.setFloat32(at + 16 + c * 4, node.rotation[c] as number, true);
|
|
79
|
+
for (let c = 0; c < 3; c++) view.setFloat32(at + 32 + c * 4, node.scale[c] as number, true);
|
|
80
|
+
view.setInt32(at + 44, node.mesh, true);
|
|
81
|
+
at += NODE_ENTRY_BYTES;
|
|
82
|
+
}
|
|
83
|
+
for (const name of names) {
|
|
84
|
+
bytes.set(name, at);
|
|
85
|
+
at += name.length;
|
|
86
|
+
}
|
|
87
|
+
return bytes;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function readNodes(buffer: ArrayBuffer, offset: number, byteLength: number): DrftNode[] {
|
|
91
|
+
if (byteLength < 8) throw new DrftError('NODE is too short to hold its header');
|
|
92
|
+
const view = new DataView(buffer, offset, byteLength);
|
|
93
|
+
const count = view.getUint32(0, true);
|
|
94
|
+
/*
|
|
95
|
+
* The stride is read rather than assumed, exactly as `MATL` reads its own: a later minor version
|
|
96
|
+
* may widen an entry, and a reader that assumed the old width would walk into the middle of the
|
|
97
|
+
* next one and produce a hierarchy of noise rather than a refusal.
|
|
98
|
+
*/
|
|
99
|
+
const stride = view.getUint32(4, true);
|
|
100
|
+
if (stride < NODE_ENTRY_BYTES) {
|
|
101
|
+
throw new DrftError(
|
|
102
|
+
`NODE entry stride ${stride} is shorter than this reader's ${NODE_ENTRY_BYTES}`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (8 + count * stride > byteLength) throw new DrftError('NODE names more nodes than it holds');
|
|
106
|
+
|
|
107
|
+
const nodes: DrftNode[] = [];
|
|
108
|
+
let namesAt = 8 + count * stride;
|
|
109
|
+
for (let i = 0; i < count; i++) {
|
|
110
|
+
const at = 8 + i * stride;
|
|
111
|
+
const name = decodeString(view, namesAt);
|
|
112
|
+
namesAt = name.next;
|
|
113
|
+
nodes.push({
|
|
114
|
+
parent: view.getInt32(at, true),
|
|
115
|
+
translation: [
|
|
116
|
+
view.getFloat32(at + 4, true),
|
|
117
|
+
view.getFloat32(at + 8, true),
|
|
118
|
+
view.getFloat32(at + 12, true),
|
|
119
|
+
],
|
|
120
|
+
rotation: [
|
|
121
|
+
view.getFloat32(at + 16, true),
|
|
122
|
+
view.getFloat32(at + 20, true),
|
|
123
|
+
view.getFloat32(at + 24, true),
|
|
124
|
+
view.getFloat32(at + 28, true),
|
|
125
|
+
],
|
|
126
|
+
scale: [
|
|
127
|
+
view.getFloat32(at + 32, true),
|
|
128
|
+
view.getFloat32(at + 36, true),
|
|
129
|
+
view.getFloat32(at + 40, true),
|
|
130
|
+
],
|
|
131
|
+
mesh: view.getInt32(at + 44, true),
|
|
132
|
+
name: name.value,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return nodes;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* --- SKIN --- */
|
|
139
|
+
|
|
140
|
+
export function buildSkin(skin: DrftSkin): Uint8Array {
|
|
141
|
+
const count = skin.joints.length;
|
|
142
|
+
if (skin.inverseBind.length !== count * 16) {
|
|
143
|
+
throw new DrftError(
|
|
144
|
+
`SKIN: ${count} joints but ${skin.inverseBind.length} inverse-bind floats; expected ${count * 16}`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
const names = skin.joints.map((joint) => encodeString(joint.name));
|
|
148
|
+
let nameBytes = 0;
|
|
149
|
+
for (const name of names) nameBytes += name.length;
|
|
150
|
+
|
|
151
|
+
const parentsAt = 8;
|
|
152
|
+
const matricesAt = parentsAt + count * 4;
|
|
153
|
+
const namesStart = matricesAt + count * 64;
|
|
154
|
+
const bytes = new Uint8Array(align(namesStart + nameBytes));
|
|
155
|
+
const view = new DataView(bytes.buffer);
|
|
156
|
+
view.setUint32(0, count, true);
|
|
157
|
+
/* Reserved, and it must be ignored on read — rule 6 of §4.4, so it can become a field later. */
|
|
158
|
+
view.setUint32(4, 0, true);
|
|
159
|
+
|
|
160
|
+
skin.joints.forEach((joint, i) => view.setInt32(parentsAt + i * 4, joint.parent, true));
|
|
161
|
+
for (let f = 0; f < count * 16; f++) {
|
|
162
|
+
view.setFloat32(matricesAt + f * 4, skin.inverseBind[f] as number, true);
|
|
163
|
+
}
|
|
164
|
+
let at = namesStart;
|
|
165
|
+
for (const name of names) {
|
|
166
|
+
bytes.set(name, at);
|
|
167
|
+
at += name.length;
|
|
168
|
+
}
|
|
169
|
+
return bytes;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function readSkin(buffer: ArrayBuffer, offset: number, byteLength: number): DrftSkin {
|
|
173
|
+
if (byteLength < 8) throw new DrftError('SKIN is too short to hold its header');
|
|
174
|
+
const view = new DataView(buffer, offset, byteLength);
|
|
175
|
+
const count = view.getUint32(0, true);
|
|
176
|
+
const parentsAt = 8;
|
|
177
|
+
const matricesAt = parentsAt + count * 4;
|
|
178
|
+
const namesStart = matricesAt + count * 64;
|
|
179
|
+
if (namesStart > byteLength) throw new DrftError('SKIN names more joints than it holds');
|
|
180
|
+
|
|
181
|
+
const inverseBind = new Float32Array(count * 16);
|
|
182
|
+
for (let f = 0; f < count * 16; f++) inverseBind[f] = view.getFloat32(matricesAt + f * 4, true);
|
|
183
|
+
|
|
184
|
+
const joints: Joint[] = [];
|
|
185
|
+
let namesAt = namesStart;
|
|
186
|
+
for (let i = 0; i < count; i++) {
|
|
187
|
+
const name = decodeString(view, namesAt);
|
|
188
|
+
namesAt = name.next;
|
|
189
|
+
const parent = view.getInt32(parentsAt + i * 4, true);
|
|
190
|
+
/*
|
|
191
|
+
* Checked on the way in rather than trusted. A palette is resolved in index order, so a joint
|
|
192
|
+
* whose parent is not already resolved produces a rig wrong in one limb — which reads as a bad
|
|
193
|
+
* animation rather than as a bad file, and is exactly the failure a reader can turn loud.
|
|
194
|
+
*/
|
|
195
|
+
if (parent >= i) {
|
|
196
|
+
throw new DrftError(
|
|
197
|
+
`SKIN: joint ${i} ("${name.value}") names parent ${parent}, which is not before it. ` +
|
|
198
|
+
`Joints are written parents-first.`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
joints.push({ parent, name: name.value });
|
|
202
|
+
}
|
|
203
|
+
return { joints, inverseBind };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* --- ANIM --- */
|
|
207
|
+
|
|
208
|
+
export function buildClip(clip: AnimationClip): Uint8Array {
|
|
209
|
+
const name = encodeString(clip.name);
|
|
210
|
+
const tracks = clip.tracks;
|
|
211
|
+
let floats = 0;
|
|
212
|
+
for (const track of tracks) floats += track.times.length + track.values.length;
|
|
213
|
+
|
|
214
|
+
const headerAt = 16 + name.length;
|
|
215
|
+
const dataAt = headerAt + tracks.length * TRACK_ENTRY_BYTES;
|
|
216
|
+
const bytes = new Uint8Array(align(dataAt + floats * 4));
|
|
217
|
+
const view = new DataView(bytes.buffer);
|
|
218
|
+
view.setUint32(0, tracks.length, true);
|
|
219
|
+
view.setFloat32(4, clip.durationSec, true);
|
|
220
|
+
view.setUint32(8, name.length, true);
|
|
221
|
+
view.setUint32(12, 0, true);
|
|
222
|
+
bytes.set(name, 16);
|
|
223
|
+
|
|
224
|
+
let at = headerAt;
|
|
225
|
+
let data = dataAt;
|
|
226
|
+
for (const track of tracks) {
|
|
227
|
+
const path = PATHS.indexOf(track.path);
|
|
228
|
+
if (path < 0) throw new DrftError(`ANIM: unknown track path "${track.path}"`);
|
|
229
|
+
view.setUint32(at, track.joint, true);
|
|
230
|
+
view.setUint32(at + 4, path, true);
|
|
231
|
+
view.setUint32(at + 8, track.times.length, true);
|
|
232
|
+
at += TRACK_ENTRY_BYTES;
|
|
233
|
+
|
|
234
|
+
for (const value of track.times) {
|
|
235
|
+
view.setFloat32(data, value, true);
|
|
236
|
+
data += 4;
|
|
237
|
+
}
|
|
238
|
+
for (const value of track.values) {
|
|
239
|
+
view.setFloat32(data, value, true);
|
|
240
|
+
data += 4;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return bytes;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function readClip(buffer: ArrayBuffer, offset: number, byteLength: number): AnimationClip {
|
|
247
|
+
if (byteLength < 16) throw new DrftError('ANIM is too short to hold its header');
|
|
248
|
+
const view = new DataView(buffer, offset, byteLength);
|
|
249
|
+
const count = view.getUint32(0, true);
|
|
250
|
+
const durationSec = view.getFloat32(4, true);
|
|
251
|
+
const nameBytes = view.getUint32(8, true);
|
|
252
|
+
const name = decodeString(view, 16).value;
|
|
253
|
+
|
|
254
|
+
const headerAt = 16 + nameBytes;
|
|
255
|
+
let at = headerAt;
|
|
256
|
+
let data = headerAt + count * TRACK_ENTRY_BYTES;
|
|
257
|
+
const tracks: JointTrack[] = [];
|
|
258
|
+
for (let i = 0; i < count; i++) {
|
|
259
|
+
const joint = view.getUint32(at, true);
|
|
260
|
+
const pathIndex = view.getUint32(at + 4, true);
|
|
261
|
+
const keys = view.getUint32(at + 8, true);
|
|
262
|
+
at += TRACK_ENTRY_BYTES;
|
|
263
|
+
|
|
264
|
+
const path = PATHS[pathIndex];
|
|
265
|
+
if (path === undefined) throw new DrftError(`ANIM: track ${i} names path ${pathIndex}`);
|
|
266
|
+
const components = path === 'rotation' ? 4 : 3;
|
|
267
|
+
if (data + keys * 4 + keys * components * 4 > byteLength) {
|
|
268
|
+
throw new DrftError(`ANIM: track ${i} reads past the end of its chunk`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const times = new Float32Array(keys);
|
|
272
|
+
for (let k = 0; k < keys; k++) {
|
|
273
|
+
times[k] = view.getFloat32(data, true);
|
|
274
|
+
data += 4;
|
|
275
|
+
}
|
|
276
|
+
const values = new Float32Array(keys * components);
|
|
277
|
+
for (let v = 0; v < keys * components; v++) {
|
|
278
|
+
values[v] = view.getFloat32(data, true);
|
|
279
|
+
data += 4;
|
|
280
|
+
}
|
|
281
|
+
tracks.push({ joint, path, times, values });
|
|
282
|
+
}
|
|
283
|
+
return { name, durationSec, tracks };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/* --- MORP --- */
|
|
287
|
+
|
|
288
|
+
/** One mesh's morph deltas, and which mesh they belong to. */
|
|
289
|
+
export interface DrftMorph {
|
|
290
|
+
/** The ordinal of the mesh these deform. */
|
|
291
|
+
readonly mesh: number;
|
|
292
|
+
readonly targetCount: number;
|
|
293
|
+
/** Three floats a vertex per target, interleaved by vertex. See `MeshData.morphTargets`. */
|
|
294
|
+
readonly deltas: Float32Array;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function buildMorph(morph: DrftMorph): Uint8Array {
|
|
298
|
+
const bytes = new Uint8Array(align(8 + morph.deltas.length * 4));
|
|
299
|
+
const view = new DataView(bytes.buffer);
|
|
300
|
+
view.setUint32(0, morph.targetCount, true);
|
|
301
|
+
/*
|
|
302
|
+
* The mesh this deforms, **in the payload rather than in the chunk table's `index`**.
|
|
303
|
+
*
|
|
304
|
+
* That field is the ordinal *within a FourCC* — the writer derives it and a caller cannot set
|
|
305
|
+
* it — so for a file where only the second and fifth meshes have targets, the two `MORP` chunks
|
|
306
|
+
* would carry 0 and 1 and a reader pairing by it would deform the wrong geometry. The table's
|
|
307
|
+
* own convention is right and this is the field that has to move.
|
|
308
|
+
*/
|
|
309
|
+
view.setUint32(4, morph.mesh, true);
|
|
310
|
+
for (let f = 0; f < morph.deltas.length; f++) {
|
|
311
|
+
view.setFloat32(8 + f * 4, morph.deltas[f] as number, true);
|
|
312
|
+
}
|
|
313
|
+
return bytes;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function readMorph(buffer: ArrayBuffer, offset: number, byteLength: number): DrftMorph {
|
|
317
|
+
if (byteLength < 8) throw new DrftError('MORP is too short to hold its header');
|
|
318
|
+
const view = new DataView(buffer, offset, byteLength);
|
|
319
|
+
const targetCount = view.getUint32(0, true);
|
|
320
|
+
const mesh = view.getUint32(4, true);
|
|
321
|
+
if (targetCount < 1) throw new DrftError(`MORP for mesh ${mesh} declares ${targetCount} targets`);
|
|
322
|
+
const floats = Math.floor((byteLength - 8) / 4);
|
|
323
|
+
const deltas = new Float32Array(floats);
|
|
324
|
+
for (let f = 0; f < floats; f++) deltas[f] = view.getFloat32(8 + f * 4, true);
|
|
325
|
+
return { mesh, targetCount, deltas };
|
|
326
|
+
}
|