@driftengine/physics 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.
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Where a skeleton puts a skinned cloth: each particle's skinned position and normal, each
3
+ * collider's ends, and the share of the character's own motion a free particle is carried by.
4
+ *
5
+ * **From joint globals and inverse binds, not from a skin palette.** A palette is `global × inverse
6
+ * bind` and is all skinning needs, but a collider sits on a joint's own frame, which only the
7
+ * global gives; taking both pieces lets one pose serve both. Model space throughout, then the model
8
+ * matrix, so the cloth simulates in the world and a turn swings it.
9
+ *
10
+ * **Normals are carried by each joint's upper 3×3**, not its inverse transpose: right for the
11
+ * rotations and uniform scales a rig is made of, and wrong for a sheared or non-uniformly scaled
12
+ * joint, which a garment's rig does not have. Renormalised after blending.
13
+ *
14
+ * Allocation-free: everything writes into arrays handed in.
15
+ */
16
+ import { exactCos } from './exact.ts';
17
+
18
+ /** `a × b` into `out` at `at`, column-major 4×4. `out` may not alias either. */
19
+ export function multiply4(
20
+ a: ArrayLike<number>,
21
+ aAt: number,
22
+ b: ArrayLike<number>,
23
+ bAt: number,
24
+ out: Float32Array,
25
+ at: number,
26
+ ): void {
27
+ for (let c = 0; c < 4; c++) {
28
+ for (let r = 0; r < 4; r++) {
29
+ let sum = 0;
30
+ for (let k = 0; k < 4; k++) {
31
+ sum += (a[aAt + k * 4 + r] as number) * (b[bAt + c * 4 + k] as number);
32
+ }
33
+ out[at + c * 4 + r] = sum;
34
+ }
35
+ }
36
+ }
37
+
38
+ /** `model × global_j × inverseBind_j` for every joint, into `skin`. */
39
+ export function skinMatrices(
40
+ globals: Float32Array,
41
+ inverseBind: Float32Array,
42
+ model: Float32Array,
43
+ scratch: Float32Array,
44
+ skin: Float32Array,
45
+ ): void {
46
+ const joints = inverseBind.length / 16;
47
+ for (let j = 0; j < joints; j++) {
48
+ multiply4(globals, j * 16, inverseBind, j * 16, scratch, 0);
49
+ multiply4(model, 0, scratch, 0, skin, j * 16);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Each particle's skinned position and normal, by up to eight influences, into `position` and
55
+ * `normal`. Without a rig (`skin` null), the rest pose under the model matrix alone.
56
+ */
57
+ export function skinParticles(
58
+ rest: Float32Array,
59
+ restNormal: Float32Array | null,
60
+ joints: Float32Array | null,
61
+ weights: Float32Array | null,
62
+ joints2: Float32Array | null,
63
+ weights2: Float32Array | null,
64
+ skin: Float32Array | null,
65
+ model: Float32Array,
66
+ position: Float32Array,
67
+ normal: Float32Array,
68
+ ): void {
69
+ const count = rest.length / 3;
70
+ for (let i = 0; i < count; i++) {
71
+ const x = rest[i * 3] as number;
72
+ const y = rest[i * 3 + 1] as number;
73
+ const z = rest[i * 3 + 2] as number;
74
+ const nx = restNormal === null ? 0 : (restNormal[i * 3] as number);
75
+ const ny = restNormal === null ? 0 : (restNormal[i * 3 + 1] as number);
76
+ const nz = restNormal === null ? 1 : (restNormal[i * 3 + 2] as number);
77
+ let px = 0;
78
+ let py = 0;
79
+ let pz = 0;
80
+ let qx = 0;
81
+ let qy = 0;
82
+ let qz = 0;
83
+ if (skin === null || joints === null || weights === null) {
84
+ const m = model;
85
+ px = (m[0] as number) * x + (m[4] as number) * y + (m[8] as number) * z + (m[12] as number);
86
+ py = (m[1] as number) * x + (m[5] as number) * y + (m[9] as number) * z + (m[13] as number);
87
+ pz = (m[2] as number) * x + (m[6] as number) * y + (m[10] as number) * z + (m[14] as number);
88
+ qx = (m[0] as number) * nx + (m[4] as number) * ny + (m[8] as number) * nz;
89
+ qy = (m[1] as number) * nx + (m[5] as number) * ny + (m[9] as number) * nz;
90
+ qz = (m[2] as number) * nx + (m[6] as number) * ny + (m[10] as number) * nz;
91
+ } else {
92
+ for (let set = 0; set < 2; set++) {
93
+ const js = set === 0 ? joints : joints2;
94
+ const ws = set === 0 ? weights : weights2;
95
+ if (js === null || ws === null) continue;
96
+ for (let k = 0; k < 4; k++) {
97
+ const w = ws[i * 4 + k] as number;
98
+ if (w === 0) continue;
99
+ const m = (js[i * 4 + k] as number) * 16;
100
+ px +=
101
+ w *
102
+ ((skin[m] as number) * x +
103
+ (skin[m + 4] as number) * y +
104
+ (skin[m + 8] as number) * z +
105
+ (skin[m + 12] as number));
106
+ py +=
107
+ w *
108
+ ((skin[m + 1] as number) * x +
109
+ (skin[m + 5] as number) * y +
110
+ (skin[m + 9] as number) * z +
111
+ (skin[m + 13] as number));
112
+ pz +=
113
+ w *
114
+ ((skin[m + 2] as number) * x +
115
+ (skin[m + 6] as number) * y +
116
+ (skin[m + 10] as number) * z +
117
+ (skin[m + 14] as number));
118
+ qx +=
119
+ w *
120
+ ((skin[m] as number) * nx +
121
+ (skin[m + 4] as number) * ny +
122
+ (skin[m + 8] as number) * nz);
123
+ qy +=
124
+ w *
125
+ ((skin[m + 1] as number) * nx +
126
+ (skin[m + 5] as number) * ny +
127
+ (skin[m + 9] as number) * nz);
128
+ qz +=
129
+ w *
130
+ ((skin[m + 2] as number) * nx +
131
+ (skin[m + 6] as number) * ny +
132
+ (skin[m + 10] as number) * nz);
133
+ }
134
+ }
135
+ }
136
+ position[i * 3] = px;
137
+ position[i * 3 + 1] = py;
138
+ position[i * 3 + 2] = pz;
139
+ const length = Math.sqrt(qx * qx + qy * qy + qz * qz);
140
+ const inverse = length > 1e-12 ? 1 / length : 0;
141
+ normal[i * 3] = qx * inverse;
142
+ normal[i * 3 + 1] = qy * inverse;
143
+ normal[i * 3 + 2] = qz * inverse;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Each collider's two ends in the world, six floats a collider: `model × global × frame` applied to
149
+ * the frame's origin and to `length` along its +Z. A sphere's two ends are one point.
150
+ */
151
+ export function placeColliders(
152
+ colliders: readonly {
153
+ readonly joint: number;
154
+ readonly length?: number;
155
+ readonly frame?: Float32Array;
156
+ }[],
157
+ globals: Float32Array,
158
+ model: Float32Array,
159
+ scratch: Float32Array,
160
+ ends: Float32Array,
161
+ ): void {
162
+ for (let k = 0; k < colliders.length; k++) {
163
+ const collider = colliders[k] as (typeof colliders)[number];
164
+ multiply4(model, 0, globals, collider.joint * 16, scratch, 0);
165
+ const frame = collider.frame;
166
+ if (frame !== undefined) {
167
+ multiply4(scratch, 0, frame, 0, scratch, 16);
168
+ scratch.copyWithin(0, 16, 32);
169
+ }
170
+ const length = collider.length ?? 0;
171
+ for (let c = 0; c < 3; c++) {
172
+ const origin = scratch[12 + c] as number;
173
+ ends[k * 6 + c] = origin;
174
+ ends[k * 6 + 3 + c] = origin + (scratch[8 + c] as number) * length;
175
+ }
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Carry free particles by the share of the character's motion they do not keep as their own.
181
+ *
182
+ * The character moved by `next × previous⁻¹`: a rotation about its old origin and a move of that
183
+ * origin. A particle is moved by `1 − angularInertia` of the first and `1 − linearInertia` of the
184
+ * second, its velocity turned by the same share — so inertia 1 leaves the cloth in the world and 0
185
+ * moves it as if it were simulated in the character's space. **A blend of the rotated point, not a
186
+ * fraction of the angle**, which is exact at the two ends and, for the few degrees a character turns
187
+ * in a step, indistinguishable between them; it needs no trigonometry, so it is the same bits on
188
+ * every engine. Both matrices rigid.
189
+ */
190
+ export function carryParticles(
191
+ position: Float32Array,
192
+ velocity: Float32Array,
193
+ inverseMass: Float32Array,
194
+ previous: Float32Array,
195
+ next: Float32Array,
196
+ linearInertia: number,
197
+ angularInertia: number,
198
+ ): void {
199
+ const turn = 1 - angularInertia;
200
+ const move = 1 - linearInertia;
201
+ if (turn === 0 && move === 0) return;
202
+ /* R = R_next × R_previousᵀ, row r column c at r + c·3. */
203
+ const r = ROTATION;
204
+ for (let row = 0; row < 3; row++) {
205
+ for (let col = 0; col < 3; col++) {
206
+ let sum = 0;
207
+ for (let k = 0; k < 3; k++)
208
+ sum += (next[k * 4 + row] as number) * (previous[k * 4 + col] as number);
209
+ r[row + col * 3] = sum;
210
+ }
211
+ }
212
+ const ox = previous[12] as number;
213
+ const oy = previous[13] as number;
214
+ const oz = previous[14] as number;
215
+ const dx = ((next[12] as number) - ox) * move;
216
+ const dy = ((next[13] as number) - oy) * move;
217
+ const dz = ((next[14] as number) - oz) * move;
218
+ for (let i = 0; i < inverseMass.length; i++) {
219
+ if ((inverseMass[i] as number) === 0) continue;
220
+ const at = i * 3;
221
+ const x = (position[at] as number) - ox;
222
+ const y = (position[at + 1] as number) - oy;
223
+ const z = (position[at + 2] as number) - oz;
224
+ position[at] =
225
+ (position[at] as number) +
226
+ ((r[0] as number) * x + (r[3] as number) * y + (r[6] as number) * z - x) * turn +
227
+ dx;
228
+ position[at + 1] =
229
+ (position[at + 1] as number) +
230
+ ((r[1] as number) * x + (r[4] as number) * y + (r[7] as number) * z - y) * turn +
231
+ dy;
232
+ position[at + 2] =
233
+ (position[at + 2] as number) +
234
+ ((r[2] as number) * x + (r[5] as number) * y + (r[8] as number) * z - z) * turn +
235
+ dz;
236
+ const vx = velocity[at] as number;
237
+ const vy = velocity[at + 1] as number;
238
+ const vz = velocity[at + 2] as number;
239
+ velocity[at] =
240
+ vx + ((r[0] as number) * vx + (r[3] as number) * vy + (r[6] as number) * vz - vx) * turn;
241
+ velocity[at + 1] =
242
+ vy + ((r[1] as number) * vx + (r[4] as number) * vy + (r[7] as number) * vz - vy) * turn;
243
+ velocity[at + 2] =
244
+ vz + ((r[2] as number) * vx + (r[5] as number) * vy + (r[8] as number) * vz - vz) * turn;
245
+ }
246
+ }
247
+
248
+ const ROTATION = new Float64Array(9);
249
+
250
+ /**
251
+ * How far the character jumped between two poses: its origin's distance and its rotation's angle.
252
+ * The angle through `trace`, `cos θ = (tr R − 1) / 2`, compared as a cosine, through `exactCos` so
253
+ * every engine draws the line in one place.
254
+ */
255
+ export function jumped(
256
+ previous: Float32Array,
257
+ next: Float32Array,
258
+ distance: number,
259
+ angle: number,
260
+ ): boolean {
261
+ const dx = (next[12] as number) - (previous[12] as number);
262
+ const dy = (next[13] as number) - (previous[13] as number);
263
+ const dz = (next[14] as number) - (previous[14] as number);
264
+ if (dx * dx + dy * dy + dz * dz > distance * distance) return true;
265
+ if (!(angle < Math.PI)) return false;
266
+ let trace = 0;
267
+ for (let d = 0; d < 3; d++) {
268
+ for (let k = 0; k < 3; k++)
269
+ trace += (next[k * 4 + d] as number) * (previous[k * 4 + d] as number);
270
+ }
271
+ return (trace - 1) / 2 < exactCos(angle);
272
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * A skinned cloth's pose: where skinning puts every particle at the last two poses, where it puts
3
+ * them at a step between, and where the colliders are.
4
+ *
5
+ * **Interpolated across the steps of one frame.** A frame that runs two steps moves its kinematic
6
+ * particles half way at the first and the rest at the second, rather than all at once — a fixed
7
+ * step must not turn a 120 Hz animation into a 60 Hz one with a jump in it. The model matrix is
8
+ * blended the same way for the inertia carry; a blend of two rigid matrices is not quite rigid, by
9
+ * the few thousandths of a degree a character turns between two steps.
10
+ *
11
+ * Before any pose the rest positions stand in, under an identity model, so a cloth that is never
12
+ * posed is the plain solver it was in H1's tests.
13
+ *
14
+ * **The particles' skinned positions can be left to a device.** A GPU solver skins them itself, from
15
+ * `skin`, so `particlesOnCpu` false keeps only what is per joint or per collider — the model
16
+ * matrices, the skin matrices and the colliders' ends — and `position`, `normal` and `latest` then
17
+ * hold the rest pose and are not to be read.
18
+ */
19
+ import { placeColliders, skinMatrices, skinParticles } from './clothPose.ts';
20
+ import type { ClothCollider, SkinnedClothSetup } from './skinnedClothSetup.ts';
21
+
22
+ const IDENTITY = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
23
+
24
+ /** Skinned positions and normals from the last two poses, and the model matrices between. */
25
+ export class ClothTargets {
26
+ /** Where skinning puts each particle at the step being taken. */
27
+ readonly position: Float32Array;
28
+ /** Its skinned normal at the latest pose. */
29
+ readonly normal: Float32Array;
30
+ /** The model matrix at the step being taken, and at the one before it. */
31
+ readonly modelNow = new Float32Array(IDENTITY);
32
+ readonly modelBefore = new Float32Array(IDENTITY);
33
+ /** Each collider's two ends, six floats a collider, at the latest pose. */
34
+ readonly colliderEnds: Float32Array;
35
+ /** Each collider's two radii. */
36
+ readonly colliderRadii: Float32Array;
37
+ /** Whether a pose has been given at all. */
38
+ posed = false;
39
+
40
+ /** `model × global × inverse bind` a joint at the latest pose, or null for a cloth with no rig. */
41
+ readonly skin: Float32Array | null;
42
+
43
+ private readonly from: Float32Array;
44
+ private readonly to: Float32Array;
45
+ private readonly modelFrom = new Float32Array(IDENTITY);
46
+ private readonly modelTo = new Float32Array(IDENTITY);
47
+ private readonly scratch = new Float32Array(32);
48
+ private readonly colliders: readonly ClothCollider[];
49
+
50
+ constructor(
51
+ private readonly setup: SkinnedClothSetup,
52
+ private readonly particlesOnCpu = true,
53
+ ) {
54
+ const count = setup.positions.length;
55
+ this.from = new Float32Array(setup.positions);
56
+ this.to = new Float32Array(setup.positions);
57
+ this.position = new Float32Array(setup.positions);
58
+ this.normal = new Float32Array(count);
59
+ const bones = (setup.inverseBind?.length ?? 0) / 16;
60
+ this.skin = setup.joints === undefined || bones === 0 ? null : new Float32Array(bones * 16);
61
+ this.colliders = setup.colliders ?? [];
62
+ this.colliderEnds = new Float32Array(this.colliders.length * 6);
63
+ this.colliderRadii = new Float32Array(this.colliders.length * 2);
64
+ this.colliders.forEach((collider, k) => {
65
+ this.colliderRadii[k * 2] = collider.radius;
66
+ this.colliderRadii[k * 2 + 1] = collider.radius2 ?? collider.radius;
67
+ });
68
+ this.skinAt(new Float32Array(Math.max(16, bones * 16)), this.modelTo);
69
+ }
70
+
71
+ /** The model matrix the latest pose was given at. */
72
+ get model(): Float32Array {
73
+ return this.modelTo;
74
+ }
75
+
76
+ /** Take a new pose: the old one becomes where this frame's steps start from. */
77
+ pose(globals: Float32Array, model: Float32Array): void {
78
+ this.from.set(this.to);
79
+ this.modelFrom.set(this.modelTo);
80
+ this.modelTo.set(model);
81
+ this.skinAt(globals, model);
82
+ if (this.colliders.length > 0) {
83
+ placeColliders(this.colliders, globals, model, this.scratch, this.colliderEnds);
84
+ }
85
+ this.posed = true;
86
+ }
87
+
88
+ /** Forget the pose before the latest: steps start where the latest one is. */
89
+ settle(): void {
90
+ this.from.set(this.to);
91
+ this.position.set(this.to);
92
+ this.modelFrom.set(this.modelTo);
93
+ this.modelNow.set(this.modelTo);
94
+ this.modelBefore.set(this.modelTo);
95
+ }
96
+
97
+ /**
98
+ * The steps of a frame have run: the next frame's start from this pose, not the one before it, or
99
+ * a frame given no new pose would interpolate back from the old one and jump the cloth.
100
+ */
101
+ consume(): void {
102
+ this.from.set(this.to);
103
+ this.modelFrom.set(this.modelTo);
104
+ }
105
+
106
+ /** The skinned positions and the model matrix `fraction` of the way from the last pose to this one. */
107
+ at(fraction: number): void {
108
+ const { from, to, position } = this;
109
+ if (this.particlesOnCpu) {
110
+ for (let i = 0; i < to.length; i++) {
111
+ const a = from[i] as number;
112
+ position[i] = a + ((to[i] as number) - a) * fraction;
113
+ }
114
+ }
115
+ this.modelBefore.set(this.modelNow);
116
+ for (let i = 0; i < 16; i++) {
117
+ const a = this.modelFrom[i] as number;
118
+ this.modelNow[i] = a + ((this.modelTo[i] as number) - a) * fraction;
119
+ }
120
+ }
121
+
122
+ /** The latest skinned positions, which `reset` puts every particle at. */
123
+ get latest(): Float32Array {
124
+ return this.to;
125
+ }
126
+
127
+ private skinAt(globals: Float32Array, model: Float32Array): void {
128
+ const { setup, skin } = this;
129
+ if (skin !== null && setup.inverseBind !== undefined) {
130
+ skinMatrices(globals, setup.inverseBind, model, this.scratch, skin);
131
+ }
132
+ if (!this.particlesOnCpu) return;
133
+ skinParticles(
134
+ setup.positions,
135
+ setup.normals ?? null,
136
+ setup.joints ?? null,
137
+ setup.weights ?? null,
138
+ setup.joints2 ?? null,
139
+ setup.weights2 ?? null,
140
+ skin,
141
+ model,
142
+ this.to,
143
+ this.normal,
144
+ );
145
+ }
146
+ }