@driftengine/physics 4.8.3 → 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,131 @@
1
+ /**
2
+ * When a skinned cloth steps: its poses, whole fixed steps, the reset a teleport forces, the steps
3
+ * a reset settles and the blend back into the simulation after it — apart from what a step does to
4
+ * the particles, which a `ClothDevice` owns.
5
+ *
6
+ * **Apart so that two solvers take one decision.** `SkinnedCloth` here moves particles in arrays on
7
+ * the CPU; `@driftengine/core` moves them in buffers on a GPU. Both are driven by this, so a frame
8
+ * runs the same steps at the same fractions on either, a teleport resets both at the same pose, and
9
+ * a blend reaches the simulation over the same steps — which is what lets the device parity check
10
+ * compare positions rather than schedules.
11
+ *
12
+ * **What it gives up**: a device sees a step as a fraction and a blend share, not as the arrays it
13
+ * came from, so a device that wanted a different schedule — substeps chosen per frame, say — would
14
+ * have to grow this rather than decide it alone.
15
+ */
16
+ import { jumped } from './clothPose.ts';
17
+ import { ClothTargets } from './clothTargets.ts';
18
+ import { resolveClothParameters } from './skinnedClothSetup.ts';
19
+ import type { ResolvedClothParameters, SkinnedClothSetup } from './skinnedClothSetup.ts';
20
+
21
+ /** What moves a skinned cloth's particles: the CPU's arrays, or a GPU's buffers. */
22
+ export interface ClothDevice {
23
+ /**
24
+ * The targets changed what they hold — took a pose, settled on it, or consumed the one a frame
25
+ * stepped toward. A device that keeps its own copy of the skinned positions follows here.
26
+ */
27
+ targetsChanged(change: 'posed' | 'settled' | 'consumed'): void;
28
+ /** Every particle at its latest skinned position, at rest, its step start with it. */
29
+ rest(): void;
30
+ /**
31
+ * One fixed step, `fraction` of the way from the last pose to the latest. `blend` is the share of
32
+ * the simulation kept against the skinned pose after it, or 0 for no blend at all — not 1, which
33
+ * is a real last step of a blend and is applied.
34
+ */
35
+ step(fraction: number, blend: number): void;
36
+ }
37
+
38
+ /** A skinned cloth's schedule, over one device. */
39
+ export class ClothControl {
40
+ readonly parameters: ResolvedClothParameters;
41
+ readonly targets: ClothTargets;
42
+ private accumulator = 0;
43
+ /** Steps left in the blend from the skinned pose after a reset. */
44
+ private blendLeft = 0;
45
+
46
+ /**
47
+ * `particlesOnCpu` false leaves the skinned particle positions to the device: the targets keep
48
+ * the model matrices, the skin matrices and the colliders, which is all a GPU needs from them.
49
+ */
50
+ constructor(
51
+ setup: SkinnedClothSetup,
52
+ private readonly device: ClothDevice,
53
+ particlesOnCpu = true,
54
+ ) {
55
+ this.parameters = resolveClothParameters(setup.parameters);
56
+ this.targets = new ClothTargets(setup, particlesOnCpu);
57
+ }
58
+
59
+ /**
60
+ * The skeleton's pose for the steps to come: each joint's global matrix, model space, sixteen
61
+ * floats a joint, and the model matrix. The first pose, and one past the teleport thresholds,
62
+ * resets the cloth to it; any other is reached across the steps of the next `advance`.
63
+ */
64
+ setPose(globals: Float32Array, model: Float32Array): void {
65
+ const { teleportDistance, teleportAngle } = this.parameters;
66
+ const first = !this.targets.posed;
67
+ const jump = !first && jumped(this.targets.model, model, teleportDistance, teleportAngle);
68
+ this.targets.pose(globals, model);
69
+ this.device.targetsChanged('posed');
70
+ if (first || jump) this.reset();
71
+ }
72
+
73
+ /**
74
+ * Every particle at its skinned position, at rest; then the set-up's settle steps at once, and its
75
+ * blend steps from the skinned pose into the simulation over the steps that follow.
76
+ */
77
+ reset(): void {
78
+ this.targets.settle();
79
+ this.device.targetsChanged('settled');
80
+ this.device.rest();
81
+ this.accumulator = 0;
82
+ this.blendLeft = 0;
83
+ for (let s = 0; s < this.parameters.settleSteps; s++) this.stepAt(1);
84
+ this.blendLeft = this.parameters.blendSteps;
85
+ }
86
+
87
+ /**
88
+ * Advance by `dt` seconds of the caller's time, in whole fixed steps; how many ran. The remainder
89
+ * is kept for the next call, and `alpha` says how far into the next step it reaches.
90
+ */
91
+ advance(dt: number): number {
92
+ this.accumulator += dt;
93
+ const step = this.parameters.step;
94
+ /* A millionth of a step of slack, so 1/30 is two steps of 1/60 and not one and a remainder. */
95
+ const steps = Math.floor((this.accumulator + step * 1e-6) / step);
96
+ for (let k = 0; k < steps; k++) {
97
+ this.accumulator -= step;
98
+ this.stepAt((k + 1) / steps);
99
+ }
100
+ if (steps > 0) this.consume();
101
+ if (this.accumulator < 0) this.accumulator = 0;
102
+ return steps;
103
+ }
104
+
105
+ /** How far the time handed to `advance` reaches into the next step, 0 to 1. */
106
+ get alpha(): number {
107
+ return Math.min(1, this.accumulator / this.parameters.step);
108
+ }
109
+
110
+ /** One fixed step, all the way to the latest pose: for a caller in the engine's fixed-step loop. */
111
+ step(): void {
112
+ this.stepAt(1);
113
+ this.consume();
114
+ }
115
+
116
+ private consume(): void {
117
+ this.targets.consume();
118
+ this.device.targetsChanged('consumed');
119
+ }
120
+
121
+ /** The targets moved to `fraction`, the share a blend keeps, then the device's step. */
122
+ private stepAt(fraction: number): void {
123
+ this.targets.at(fraction);
124
+ let blend = 0;
125
+ if (this.blendLeft > 0) {
126
+ blend = (this.parameters.blendSteps - this.blendLeft + 1) / this.parameters.blendSteps;
127
+ this.blendLeft--;
128
+ }
129
+ this.device.step(fraction, blend);
130
+ }
131
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Where a skinned cloth's particles may not go: the limits measured from each particle's skinned
3
+ * position, and the colliders its skeleton carries.
4
+ *
5
+ * **Projections, not constraints with compliance.** A limit is a statement about the body — a
6
+ * garment does not pass through the hip it is cut for — so a particle past one is put back on its
7
+ * surface, at once and wholly. Applied after the constraint iterations of every substep, so a stiff
8
+ * garment pulling a particle into the body loses.
9
+ *
10
+ * Allocation-free, and each function a kernel the GPU solver mirrors.
11
+ */
12
+
13
+ /** A particle further than `maxDistance[i] · scale` from its skinned position is put back on that sphere. */
14
+ export function applyMaxDistance(
15
+ position: Float32Array,
16
+ inverseMass: Float32Array,
17
+ target: Float32Array,
18
+ maxDistance: Float32Array,
19
+ scale: number,
20
+ ): void {
21
+ for (let i = 0; i < inverseMass.length; i++) {
22
+ if ((inverseMass[i] as number) === 0) continue;
23
+ const limit = (maxDistance[i] as number) * scale;
24
+ if (!(limit < Infinity)) continue;
25
+ const at = i * 3;
26
+ const dx = (position[at] as number) - (target[at] as number);
27
+ const dy = (position[at + 1] as number) - (target[at + 1] as number);
28
+ const dz = (position[at + 2] as number) - (target[at + 2] as number);
29
+ const d2 = dx * dx + dy * dy + dz * dz;
30
+ if (d2 <= limit * limit) continue;
31
+ const s = limit / Math.sqrt(d2);
32
+ position[at] = (target[at] as number) + dx * s;
33
+ position[at + 1] = (target[at + 1] as number) + dy * s;
34
+ position[at + 2] = (target[at + 2] as number) + dz * s;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * A sphere the particle may not enter, on one side of its skinned position: behind it along the
40
+ * skinned normal for a backstop (`side` −1), in front for a frontstop (`side` +1). `stops` holds a
41
+ * distance and a radius a particle; the centre is `distance + radius` from the skinned position,
42
+ * so the sphere's near surface is `distance` from it. An infinite distance is no stop.
43
+ */
44
+ export function applyStops(
45
+ position: Float32Array,
46
+ inverseMass: Float32Array,
47
+ target: Float32Array,
48
+ normal: Float32Array,
49
+ stops: Float32Array,
50
+ side: number,
51
+ ): void {
52
+ for (let i = 0; i < inverseMass.length; i++) {
53
+ if ((inverseMass[i] as number) === 0) continue;
54
+ const distance = stops[i * 2] as number;
55
+ const radius = stops[i * 2 + 1] as number;
56
+ if (!(distance < Infinity) || !(radius > 0)) continue;
57
+ const at = i * 3;
58
+ const reach = (distance + radius) * side;
59
+ const cx = (target[at] as number) + (normal[at] as number) * reach;
60
+ const cy = (target[at + 1] as number) + (normal[at + 1] as number) * reach;
61
+ const cz = (target[at + 2] as number) + (normal[at + 2] as number) * reach;
62
+ pushOut(position, at, cx, cy, cz, radius);
63
+ }
64
+ }
65
+
66
+ /** Put the point at `at` on the sphere's surface if it is inside. A point at the centre stays. */
67
+ function pushOut(
68
+ position: Float32Array,
69
+ at: number,
70
+ cx: number,
71
+ cy: number,
72
+ cz: number,
73
+ radius: number,
74
+ ): void {
75
+ const dx = (position[at] as number) - cx;
76
+ const dy = (position[at + 1] as number) - cy;
77
+ const dz = (position[at + 2] as number) - cz;
78
+ const d2 = dx * dx + dy * dy + dz * dz;
79
+ if (d2 >= radius * radius || d2 < 1e-18) return;
80
+ const s = radius / Math.sqrt(d2);
81
+ position[at] = cx + dx * s;
82
+ position[at + 1] = cy + dy * s;
83
+ position[at + 2] = cz + dz * s;
84
+ }
85
+
86
+ /**
87
+ * Every particle out of every collider: a sphere, or a tapered capsule from `ends[k·6]` to
88
+ * `ends[k·6+3]` whose radius runs from `radii[k·2]` to `radii[k·2+1]` — plus the particle's
89
+ * thickness and the cloth's margin.
90
+ *
91
+ * **A tapered capsule is pushed out from the nearest point on its axis**, at the radius interpolated
92
+ * there. That is the usual approximation of a cone-swept sphere and what character cloth solvers
93
+ * ship: exact where the two radii are equal, and where they differ it puts a particle a little off
94
+ * the true surface on the narrowing side — by a fraction of the taper over the capsule's length,
95
+ * which for a limb is millimetres. What would make it wrong is a collider tapering steeply over a
96
+ * short length, a cone more than a limb; the exact distance to a swept sphere is the fix then.
97
+ */
98
+ export function applyColliders(
99
+ position: Float32Array,
100
+ inverseMass: Float32Array,
101
+ thickness: Float32Array | null,
102
+ margin: number,
103
+ ends: Float32Array,
104
+ radii: Float32Array,
105
+ colliders: number,
106
+ ): void {
107
+ for (let i = 0; i < inverseMass.length; i++) {
108
+ if ((inverseMass[i] as number) === 0) continue;
109
+ const at = i * 3;
110
+ const pad = (thickness === null ? 0 : (thickness[i] as number)) + margin;
111
+ for (let k = 0; k < colliders; k++) {
112
+ const ax = ends[k * 6] as number;
113
+ const ay = ends[k * 6 + 1] as number;
114
+ const az = ends[k * 6 + 2] as number;
115
+ const ex = (ends[k * 6 + 3] as number) - ax;
116
+ const ey = (ends[k * 6 + 4] as number) - ay;
117
+ const ez = (ends[k * 6 + 5] as number) - az;
118
+ const e2 = ex * ex + ey * ey + ez * ez;
119
+ let t = 0;
120
+ if (e2 > 1e-18) {
121
+ t =
122
+ (((position[at] as number) - ax) * ex +
123
+ ((position[at + 1] as number) - ay) * ey +
124
+ ((position[at + 2] as number) - az) * ez) /
125
+ e2;
126
+ t = Math.min(1, Math.max(0, t));
127
+ }
128
+ const r0 = radii[k * 2] as number;
129
+ const r1 = radii[k * 2 + 1] as number;
130
+ pushOut(position, at, ax + ex * t, ay + ey * t, az + ez * t, r0 + (r1 - r0) * t + pad);
131
+ }
132
+ }
133
+ }
@@ -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
+ }