@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.
Files changed (67) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +9 -0
  4. package/dist/animationData.d.ts +53 -0
  5. package/dist/animationData.js +13 -0
  6. package/dist/coarseFirst.d.ts +27 -0
  7. package/dist/coarseFirst.js +118 -0
  8. package/dist/drftColliders.d.ts +53 -0
  9. package/dist/drftColliders.js +137 -0
  10. package/dist/drftFormat.d.ts +454 -0
  11. package/dist/drftFormat.js +350 -0
  12. package/dist/drftRead.d.ts +121 -0
  13. package/dist/drftRead.js +487 -0
  14. package/dist/drftSkin.d.ts +40 -0
  15. package/dist/drftSkin.js +270 -0
  16. package/dist/drftStream.d.ts +170 -0
  17. package/dist/drftStream.js +315 -0
  18. package/dist/drftSubs.d.ts +18 -0
  19. package/dist/drftSubs.js +70 -0
  20. package/dist/drftWrite.d.ts +103 -0
  21. package/dist/drftWrite.js +478 -0
  22. package/dist/fixtures/v1-0.d.ts +9 -0
  23. package/dist/fixtures/v1-0.js +9 -0
  24. package/dist/fixtures/v1-1.d.ts +16 -0
  25. package/dist/fixtures/v1-1.js +16 -0
  26. package/dist/fixtures/v1-11.d.ts +14 -0
  27. package/dist/fixtures/v1-11.js +14 -0
  28. package/dist/fixtures/v1-2.d.ts +19 -0
  29. package/dist/fixtures/v1-2.js +19 -0
  30. package/dist/fixtures/v1-3.d.ts +18 -0
  31. package/dist/fixtures/v1-3.js +18 -0
  32. package/dist/fixtures/v1-4.d.ts +13 -0
  33. package/dist/fixtures/v1-4.js +13 -0
  34. package/dist/fixtures/v1-5.d.ts +14 -0
  35. package/dist/fixtures/v1-5.js +14 -0
  36. package/dist/fixtures/v1-6.d.ts +9 -0
  37. package/dist/fixtures/v1-6.js +9 -0
  38. package/dist/fixtures/v1-7.d.ts +9 -0
  39. package/dist/fixtures/v1-7.js +9 -0
  40. package/dist/fixtures/v1-9.d.ts +9 -0
  41. package/dist/fixtures/v1-9.js +9 -0
  42. package/dist/index.d.ts +26 -0
  43. package/dist/index.js +29 -0
  44. package/dist/meshData.d.ts +210 -0
  45. package/dist/meshData.js +105 -0
  46. package/package.json +57 -0
  47. package/src/animationData.ts +58 -0
  48. package/src/coarseFirst.ts +113 -0
  49. package/src/drftColliders.ts +153 -0
  50. package/src/drftFormat.ts +537 -0
  51. package/src/drftRead.ts +652 -0
  52. package/src/drftSkin.ts +326 -0
  53. package/src/drftStream.ts +436 -0
  54. package/src/drftSubs.ts +86 -0
  55. package/src/drftWrite.ts +613 -0
  56. package/src/fixtures/v1-0.ts +10 -0
  57. package/src/fixtures/v1-1.ts +17 -0
  58. package/src/fixtures/v1-11.ts +15 -0
  59. package/src/fixtures/v1-2.ts +20 -0
  60. package/src/fixtures/v1-3.ts +19 -0
  61. package/src/fixtures/v1-4.ts +14 -0
  62. package/src/fixtures/v1-5.ts +15 -0
  63. package/src/fixtures/v1-6.ts +10 -0
  64. package/src/fixtures/v1-7.ts +10 -0
  65. package/src/fixtures/v1-9.ts +10 -0
  66. package/src/index.ts +56 -0
  67. package/src/meshData.ts +301 -0
@@ -0,0 +1,58 @@
1
+ /**
2
+ * What a rig and its clips are, as data.
3
+ *
4
+ * **Here for the reason `MeshData` is here**, and stated in that file's header: these are the
5
+ * boundary objects between the container and everything that uses one. `SKIN` and `ANIM` are
6
+ * chunks, so what they carry belongs to the format package — which also means the two packages
7
+ * that meet over a rig, `@driftengine/assets` producing one and `@driftengine/animation` consuming
8
+ * one, do so without either depending on the other. Both already depend on this.
9
+ *
10
+ * Nothing here has behaviour. `Skeleton`, `Pose` and `sampleClip` live in the animation package,
11
+ * which re-exports these types so a consumer imports from one place.
12
+ */
13
+
14
+ /** One joint: where it hangs and what it is called. */
15
+ export interface Joint {
16
+ /**
17
+ * Index of this joint's parent, or -1 for a root. **Always less than this joint's own index.**
18
+ *
19
+ * Parents-first is a requirement rather than a convention: a palette is resolved in index order
20
+ * and reads each joint's parent as it goes, so a nearly-sorted rig is wrong in one limb — which
21
+ * reads as a bad animation rather than as a data error. The importer sorts, because it is also
22
+ * the layer that can remap every index naming a joint; `Skeleton` refuses an unsorted one.
23
+ */
24
+ readonly parent: number;
25
+ /** The name the source rig gave it, which is what retargeting matches on. */
26
+ readonly name: string;
27
+ }
28
+
29
+ /** Which of a joint's three transforms a track drives. */
30
+ export type TrackPath = 'translation' | 'rotation' | 'scale';
31
+
32
+ /** One joint's keys for one of its three transforms. */
33
+ export interface JointTrack {
34
+ readonly joint: number;
35
+ readonly path: TrackPath;
36
+ /** Ascending, in seconds. One entry per key. */
37
+ readonly times: Float32Array;
38
+ /** Three floats a key for translation and scale, four for rotation. */
39
+ readonly values: Float32Array;
40
+ }
41
+
42
+ export interface AnimationClip {
43
+ readonly name: string;
44
+ readonly durationSec: number;
45
+ readonly tracks: readonly JointTrack[];
46
+ }
47
+
48
+ /**
49
+ * A skin: the joints, their inverse bind matrices, and nothing about how it is played.
50
+ *
51
+ * `inverseBind` is sixteen floats a joint, column-major, in the same order as `joints`. An entry
52
+ * takes a vertex from model space into that joint's bind-pose space, which is what makes a palette
53
+ * identity at the bind pose whatever the bind pose is.
54
+ */
55
+ export interface DrftSkin {
56
+ readonly joints: readonly Joint[];
57
+ readonly inverseBind: Float32Array;
58
+ }
@@ -0,0 +1,113 @@
1
+ /** The order splats are written to the container in, so any prefix of the file is the whole place. */
2
+
3
+ /**
4
+ * Bits per axis in the spatial key. Ten, so three of them fit a 30-bit integer sort key.
5
+ *
6
+ * A thousand and twenty-four cells an axis over the capture's own extent, which for a room-sized
7
+ * capture is under a centimetre. What it gives up is that two splats inside one cell are ordered
8
+ * arbitrarily against each other, which is invisible at any block size a stream uses. What would
9
+ * make it wrong is a capture whose extent is dominated by one distant outlier, squeezing the rest
10
+ * into a handful of cells — the same failure any quantisation has.
11
+ */
12
+ const AXIS_BITS = 10;
13
+ const AXIS_CELLS = (1 << AXIS_BITS) - 1;
14
+
15
+ /** Spread ten bits out to every third bit, so three of them interleave into one integer. */
16
+ function spread(value: number): number {
17
+ let bits = value & AXIS_CELLS;
18
+ bits = (bits | (bits << 16)) & 0x030000ff;
19
+ bits = (bits | (bits << 8)) & 0x0300f00f;
20
+ bits = (bits | (bits << 4)) & 0x030c30c3;
21
+ bits = (bits | (bits << 2)) & 0x09249249;
22
+ return bits;
23
+ }
24
+
25
+ /**
26
+ * The order to write a capture's splats in, so that **every prefix of the file is a complete
27
+ * sparse capture** rather than a finished corner of one.
28
+ *
29
+ * **This is the reason the container is worth having at all.** Everything above it is a splat
30
+ * viewer; a load that opens on a recognisable place and densifies is what a viewer streaming from
31
+ * a plain file cannot do by accident. It is the same trick `coarseLevel.ts` plays for meshes,
32
+ * where a decimated whole body reads as a car and one finished wheel does not.
33
+ *
34
+ * Two steps, and both are needed:
35
+ *
36
+ * 1. **Sort by Morton code**, so index order becomes spatial order. Without this the sequence
37
+ * below decimates whatever order the trainer happened to leave — which is usually fine and is
38
+ * a slab of one axis when it is not, and the difference is invisible until somebody opens a
39
+ * capture that was written out in scan order. `coarseFirst.test.ts` builds exactly that case.
40
+ * 2. **Walk that order bit-reversed**, which is the van der Corput sequence and is what makes
41
+ * each *block* a sparse capture rather than only the first. Reversing the bits of a counter
42
+ * visits 0, half way, quarter, three quarters, and so on, so consecutive slots are maximally
43
+ * far apart in the spatial order and every block is an even scattering over the whole thing.
44
+ *
45
+ * What it costs is a sort at bake time — `O(n log n)` once, on a machine with a keyboard — and
46
+ * nothing at all at load. **What would make it wrong** is a capture meant to be read in its
47
+ * authored order, which this format has no notion of: a `.drft` splat chunk is a set, and a
48
+ * consumer that needed the original indices would need them stored.
49
+ */
50
+ export function coarseFirstOrder(positions: Float32Array, count: number): Uint32Array {
51
+ const order = new Uint32Array(Math.max(0, count));
52
+ if (count <= 0) return order;
53
+ if (count === 1) return order;
54
+
55
+ let minX = Infinity;
56
+ let minY = Infinity;
57
+ let minZ = Infinity;
58
+ let maxX = -Infinity;
59
+ let maxY = -Infinity;
60
+ let maxZ = -Infinity;
61
+ for (let index = 0; index < count; index++) {
62
+ const at = index * 3;
63
+ const x = positions[at] ?? 0;
64
+ const y = positions[at + 1] ?? 0;
65
+ const z = positions[at + 2] ?? 0;
66
+ if (x < minX) minX = x;
67
+ if (y < minY) minY = y;
68
+ if (z < minZ) minZ = z;
69
+ if (x > maxX) maxX = x;
70
+ if (y > maxY) maxY = y;
71
+ if (z > maxZ) maxZ = z;
72
+ }
73
+ /* A capture with no extent on an axis quantises every splat to cell zero there, which is the
74
+ right answer: with no range there is no spatial order along it to preserve. */
75
+ const scaleX = maxX > minX ? AXIS_CELLS / (maxX - minX) : 0;
76
+ const scaleY = maxY > minY ? AXIS_CELLS / (maxY - minY) : 0;
77
+ const scaleZ = maxZ > minZ ? AXIS_CELLS / (maxZ - minZ) : 0;
78
+
79
+ const spatial = new Uint32Array(count);
80
+ const keys = new Uint32Array(count);
81
+ for (let index = 0; index < count; index++) {
82
+ const at = index * 3;
83
+ const x = Math.round(((positions[at] ?? 0) - minX) * scaleX);
84
+ const y = Math.round(((positions[at + 1] ?? 0) - minY) * scaleY);
85
+ const z = Math.round(((positions[at + 2] ?? 0) - minZ) * scaleZ);
86
+ keys[index] = (spread(x) | (spread(y) << 1) | (spread(z) << 2)) >>> 0;
87
+ spatial[index] = index;
88
+ }
89
+ /*
90
+ * A comparison sort, deliberately. This runs once per bake on a workstation, against a counting
91
+ * sort's 4 MB of buckets for a 30-bit key; the frame-time sort in `@driftengine/splats` is the
92
+ * one where that trade goes the other way, and it says so.
93
+ */
94
+ const sorted = Array.from(spatial).sort((a, b) => (keys[a] ?? 0) - (keys[b] ?? 0));
95
+
96
+ /*
97
+ * Bit-reversal over the spatial order. `width` is the number of bits the count needs, so the
98
+ * counter covers a power of two at least as large; reversed values landing past the end are
99
+ * skipped, which keeps the sequence's spread and costs at most one extra pass.
100
+ */
101
+ let width = 0;
102
+ while (1 << width < count) width++;
103
+ let slot = 0;
104
+ for (let counter = 0; counter < 1 << width; counter++) {
105
+ let reversed = 0;
106
+ for (let bit = 0; bit < width; bit++) {
107
+ if ((counter & (1 << bit)) !== 0) reversed |= 1 << (width - 1 - bit);
108
+ }
109
+ if (reversed >= count) continue;
110
+ order[slot++] = sorted[reversed] ?? 0;
111
+ }
112
+ return order;
113
+ }
@@ -0,0 +1,153 @@
1
+ import { DrftError, align } from './drftFormat.ts';
2
+
3
+ /**
4
+ * `COLL`: the convex hulls an asset collides as.
5
+ *
6
+ * **A FourCC that was claimed and undefined for the whole of v1.** `docs/FORMAT.md` §4.3 listed the
7
+ * row and `KNOWN_CHUNKS` held the code while no writer emitted one and no reader consumed one.
8
+ * `readModel.ts` said why in as many words when it declined to put a vehicle's collision hull inside
9
+ * the model's own file: *"a format decision about whether a hull is triangle soup or a set of convex
10
+ * hulls, and not one an import should make in passing."* This is that decision, made — **a set of
11
+ * convex hulls**.
12
+ *
13
+ * **Points, not shapes, because this package depends on nothing.** `boundaries.test.mjs` asserts
14
+ * that import graph and `drft-only` measures what it is worth. A serialised `ConvexShape` would mean
15
+ * the format package knowing what a face plane and a separating axis are, and it would freeze a
16
+ * representation the physics package is still free to change. A hull here is the points whose hull
17
+ * it is, which is exactly what `hullShape` takes:
18
+ *
19
+ * ```ts
20
+ * const shapes = asset.colliders.map((points) => hullShape(points));
21
+ * ```
22
+ *
23
+ * **One chunk for the file**, pairing with nothing. An asset's collision is a fact about the whole
24
+ * asset the way `SUBS` is a fact about all its materials, not a thing that belongs to mesh three.
25
+ *
26
+ * ```
27
+ * u32 hullCount
28
+ * u32 pointTotal
29
+ * u32 starts[hullCount + 1] offsets in points; starts[0] = 0, starts[n] = pointTotal
30
+ * f32 points[pointTotal * 3] xyz, in the asset's own space
31
+ * ```
32
+ *
33
+ * The starts table is written whole, with its closing entry, so a reader takes every hull's extent
34
+ * from one subtraction and never has to special-case the last one. Everything is four-byte aligned,
35
+ * so the points come back as one `Float32Array` view over the fetched buffer and each hull is a
36
+ * `subarray` of it — the same property `MESH` exists for.
37
+ */
38
+
39
+ /** How many points one hull may carry, which is what `hullShape` accepts. */
40
+ export const MAX_COLLIDER_POINTS = 64;
41
+
42
+ /**
43
+ * A cap on hulls per asset, so a malformed length cannot ask a reader for gigabytes.
44
+ *
45
+ * Matches `MAX_BODY_PARTS` in `@driftengine/physics` by intent rather than by import — this package
46
+ * depends on nothing, and the two are checked against each other by a test in the engine instead.
47
+ */
48
+ export const MAX_COLLIDER_HULLS = 32;
49
+
50
+ export function buildColliders(hulls: readonly Float32Array[]): Uint8Array {
51
+ if (hulls.length > MAX_COLLIDER_HULLS) {
52
+ throw new DrftError(`${hulls.length} collision hulls exceeds ${MAX_COLLIDER_HULLS}`);
53
+ }
54
+ let pointTotal = 0;
55
+ for (const hull of hulls) {
56
+ if (hull.length % 3 !== 0) {
57
+ throw new DrftError(`a collision hull has ${hull.length} floats, which is not whole points`);
58
+ }
59
+ const points = hull.length / 3;
60
+ if (points === 0) throw new DrftError('a collision hull has no points');
61
+ if (points > MAX_COLLIDER_POINTS) {
62
+ throw new DrftError(
63
+ `a collision hull has ${points} points, over the ${MAX_COLLIDER_POINTS} cap`,
64
+ );
65
+ }
66
+ pointTotal += points;
67
+ }
68
+
69
+ const startsBytes = (hulls.length + 1) * 4;
70
+ const size = 8 + startsBytes + pointTotal * 3 * 4;
71
+ const bytes = new Uint8Array(align(size));
72
+ const view = new DataView(bytes.buffer);
73
+ view.setUint32(0, hulls.length, true);
74
+ view.setUint32(4, pointTotal, true);
75
+
76
+ let start = 0;
77
+ for (let i = 0; i < hulls.length; i++) {
78
+ view.setUint32(8 + i * 4, start, true);
79
+ start += (hulls[i] as Float32Array).length / 3;
80
+ }
81
+ view.setUint32(8 + hulls.length * 4, pointTotal, true);
82
+
83
+ const points = new Float32Array(bytes.buffer, 8 + startsBytes, pointTotal * 3);
84
+ let at = 0;
85
+ for (const hull of hulls) {
86
+ points.set(hull, at);
87
+ at += hull.length;
88
+ }
89
+ return bytes;
90
+ }
91
+
92
+ /**
93
+ * The hulls a `COLL` chunk carries, as views over the fetched buffer.
94
+ *
95
+ * Views rather than copies, exactly like the vertex arrays, so a load allocates nothing beyond the
96
+ * array holding them. They keep the file alive for as long as they are held, which is the same trade
97
+ * `DrftTexture.bytes` makes and for the same reason.
98
+ */
99
+ export function readColliders(
100
+ buffer: ArrayBuffer,
101
+ offset: number,
102
+ byteLength: number,
103
+ ): readonly Float32Array[] {
104
+ if (byteLength < 8) throw new DrftError('COLL is too short to hold its counts');
105
+ const view = new DataView(buffer, offset, byteLength);
106
+ const hullCount = view.getUint32(0, true);
107
+ const pointTotal = view.getUint32(4, true);
108
+ if (hullCount > MAX_COLLIDER_HULLS) {
109
+ throw new DrftError(`COLL claims ${hullCount} hulls, over the ${MAX_COLLIDER_HULLS} cap`);
110
+ }
111
+
112
+ const startsBytes = (hullCount + 1) * 4;
113
+ const needed = 8 + startsBytes + pointTotal * 3 * 4;
114
+ if (byteLength < needed) {
115
+ throw new DrftError(
116
+ `COLL claims ${hullCount} hulls and ${pointTotal} points, which runs past the chunk`,
117
+ );
118
+ }
119
+
120
+ /*
121
+ * The starts are read and checked before a single point is handed out. A table that runs backwards
122
+ * or past the total would otherwise produce a `subarray` of a length nobody wrote, which is a hull
123
+ * of arbitrary geometry rather than an error.
124
+ */
125
+ const starts = new Uint32Array(hullCount + 1);
126
+ for (let i = 0; i <= hullCount; i++) starts[i] = view.getUint32(8 + i * 4, true);
127
+ if ((starts[0] as number) !== 0)
128
+ throw new DrftError('COLL does not start its first hull at zero');
129
+ if ((starts[hullCount] as number) !== pointTotal) {
130
+ throw new DrftError('COLL closes its table at a point count it did not declare');
131
+ }
132
+ for (let i = 0; i < hullCount; i++) {
133
+ const from = starts[i] as number;
134
+ const to = starts[i + 1] as number;
135
+ if (to < from) throw new DrftError(`COLL hull ${i} runs backwards`);
136
+ const points = to - from;
137
+ if (points === 0) throw new DrftError(`COLL hull ${i} has no points`);
138
+ if (points > MAX_COLLIDER_POINTS) {
139
+ throw new DrftError(
140
+ `COLL hull ${i} has ${points} points, over the ${MAX_COLLIDER_POINTS} cap`,
141
+ );
142
+ }
143
+ }
144
+
145
+ const all = new Float32Array(buffer, offset + 8 + startsBytes, pointTotal * 3);
146
+ const hulls: Float32Array[] = [];
147
+ for (let i = 0; i < hullCount; i++) {
148
+ const from = (starts[i] as number) * 3;
149
+ const to = (starts[i + 1] as number) * 3;
150
+ hulls.push(all.subarray(from, to));
151
+ }
152
+ return hulls;
153
+ }