@driftengine/physics 4.8.3 → 4.8.5

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,160 @@
1
+ /**
2
+ * What a skinned cloth is made from: particles, their limits, the constraints between them and the
3
+ * numbers the solver runs at — plain arrays, so a port decoding a middleware's cooked data hands it
4
+ * over as it decoded it, and the GPU solver uploads the same arrays the CPU one reads.
5
+ *
6
+ * **Skinned** because its anchor is a skeleton rather than a world: a kinematic particle (inverse
7
+ * mass 0) is placed by its joint influences every step, and every limit is measured from where
8
+ * skinning would put a particle. `ClothBody` is the other kind — a sheet in a physics world — and
9
+ * stays that.
10
+ *
11
+ * Validated at construction and refused by name: a cloth whose arrays disagree in length would read
12
+ * past an end on the GPU, where nothing says so.
13
+ */
14
+ /** Distance constraints: two particles, a rest length, a compliance (metres per newton). */
15
+ export interface DistanceConstraints {
16
+ /** Two particle indices a constraint. */
17
+ readonly pairs: Uint32Array;
18
+ readonly rest: Float32Array;
19
+ readonly compliance: Float32Array;
20
+ /**
21
+ * Graph-colour batches, as offsets into the constraints: batch `b` is constraints
22
+ * `batches[b]` to `batches[b + 1]`, none of which moves a particle another in the batch moves.
23
+ * Absent, the solver colours them itself. Given, they are checked and refused if they overlap.
24
+ */
25
+ readonly batches?: Uint32Array;
26
+ }
27
+ /** Dihedral bending: four particles — the shared edge, then each triangle's far corner. */
28
+ export interface BendingConstraints {
29
+ readonly quads: Uint32Array;
30
+ /** The angle between the two triangles' normals at rest, in radians: π is flat. */
31
+ readonly rest: Float32Array;
32
+ readonly compliance: Float32Array;
33
+ readonly batches?: Uint32Array;
34
+ }
35
+ /** Long-range attachments: a particle may be no further than a length from a kinematic anchor. */
36
+ export interface TetherConstraints {
37
+ readonly particles: Uint32Array;
38
+ readonly anchors: Uint32Array;
39
+ readonly lengths: Float32Array;
40
+ }
41
+ /**
42
+ * Where a particle may not go, measured from where skinning puts it. Each optional, one entry a
43
+ * particle (two for a stop), and `Infinity` is none — which is how cooked data says it.
44
+ */
45
+ export interface ClothLimits {
46
+ /** Metres from the skinned position a particle may not leave: a sphere around it. */
47
+ readonly maxDistance?: Float32Array;
48
+ /** A distance and a radius a particle: a sphere behind the skinned position, along its normal. */
49
+ readonly backstop?: Float32Array;
50
+ /** The same in front. */
51
+ readonly frontstop?: Float32Array;
52
+ /** Metres a particle keeps from a collider's surface. */
53
+ readonly thickness?: Float32Array;
54
+ /**
55
+ * Which particles collide with each other, one flag each. **Accepted and not yet simulated**: a
56
+ * garment held by its colliders and its backstops does not need it to look right, and the GPU
57
+ * solver has no neighbour search yet. Carried so a set-up need not be rewritten when it arrives.
58
+ */
59
+ readonly selfCollision?: Uint8Array;
60
+ }
61
+ /**
62
+ * A collider on a joint: a sphere, or a tapered capsule along its frame's +Z.
63
+ *
64
+ * Its frame is in the joint's own space, so it rides the joint whatever the animation does.
65
+ */
66
+ export interface ClothCollider {
67
+ readonly joint: number;
68
+ readonly radius: number;
69
+ /** The radius at the far end; the same as `radius` where absent. */
70
+ readonly radius2?: number;
71
+ /** Metres along the frame's +Z; zero or absent is a sphere. */
72
+ readonly length?: number;
73
+ /** Sixteen floats, column-major, in the joint's space; identity where absent. */
74
+ readonly frame?: Float32Array;
75
+ }
76
+ /** What the solver runs at. Every field has a default; the defaults are a garment's, not a sheet's. */
77
+ export interface SkinnedClothParameters {
78
+ /** The fixed step, in seconds. The solver runs whole steps whatever the frame. Default 1/60. */
79
+ readonly step?: number;
80
+ /** Substeps a step. Default 1. */
81
+ readonly substeps?: number;
82
+ /** Constraint passes a substep. Default 4. */
83
+ readonly iterations?: number;
84
+ /** Metres a second squared, world space. Default [0, −9.81, 0]. */
85
+ readonly gravity?: readonly [number, number, number];
86
+ /**
87
+ * The log of the velocity kept after a second: `ln 0.25` keeps a quarter. Zero is none; it is
88
+ * applied as `exp(damping · h)` a substep, so it means one thing at every step length.
89
+ */
90
+ readonly damping?: number;
91
+ /** The same, of the velocity relative to the wind: how fast the air takes the cloth with it. */
92
+ readonly drag?: number;
93
+ /**
94
+ * Metres a second, world space: the air the cloth starts in. Default still air. A garment in a
95
+ * scene follows the scene's wind a frame at a time, through `setWind`.
96
+ */
97
+ readonly wind?: readonly [number, number, number];
98
+ /**
99
+ * How much of the character's own movement and turning the cloth keeps as its own, 0 to 1: 1 is
100
+ * simulated wholly in the world, so a dash leaves the hem behind; 0 moves it with the character.
101
+ * Default 1.
102
+ */
103
+ readonly linearInertia?: number;
104
+ readonly angularInertia?: number;
105
+ /** Scales every max distance, for a garment worn looser or tighter than cooked. Default 1. */
106
+ readonly maxDistanceScale?: number;
107
+ /** Metres added to every collider's radius, beyond a particle's own thickness. Default 0. */
108
+ readonly margin?: number;
109
+ /** A move of the character's origin past this, between two poses, resets the cloth. Default none. */
110
+ readonly teleportDistance?: number;
111
+ /** A turn past this many radians between two poses resets it too. Default none. */
112
+ readonly teleportAngle?: number;
113
+ /** Steps run at once when the cloth is reset, so it starts hanging rather than falling. */
114
+ readonly settleSteps?: number;
115
+ /** Steps over which a reset cloth blends from its skinned pose into its simulation. */
116
+ readonly blendSteps?: number;
117
+ }
118
+ /** Everything a skinned cloth is made from. */
119
+ export interface SkinnedClothSetup {
120
+ /** Rest positions, three floats a particle, in the space the skinning starts from. */
121
+ readonly positions: Float32Array;
122
+ /** One a particle: zero is kinematic, following its skinned position exactly. */
123
+ readonly inverseMass: Float32Array;
124
+ readonly distance: DistanceConstraints;
125
+ readonly bending?: BendingConstraints;
126
+ readonly tethers?: TetherConstraints;
127
+ readonly parameters: SkinnedClothParameters;
128
+ /** Rest normals, three floats a particle: what a backstop and a frontstop are measured along. */
129
+ readonly normals?: Float32Array;
130
+ /** Up to eight joint influences a particle, as `MeshData` carries them. Absent: not skinned. */
131
+ readonly joints?: Float32Array;
132
+ readonly weights?: Float32Array;
133
+ readonly joints2?: Float32Array;
134
+ readonly weights2?: Float32Array;
135
+ /** Sixteen floats a joint: each joint's inverse bind matrix, column-major. */
136
+ readonly inverseBind?: Float32Array;
137
+ readonly limits?: ClothLimits;
138
+ readonly colliders?: readonly ClothCollider[];
139
+ }
140
+ /** The parameters with every default filled in. */
141
+ export interface ResolvedClothParameters {
142
+ readonly step: number;
143
+ readonly substeps: number;
144
+ readonly iterations: number;
145
+ readonly gravity: readonly [number, number, number];
146
+ readonly damping: number;
147
+ readonly drag: number;
148
+ readonly wind: readonly [number, number, number];
149
+ readonly linearInertia: number;
150
+ readonly angularInertia: number;
151
+ readonly maxDistanceScale: number;
152
+ readonly margin: number;
153
+ readonly teleportDistance: number;
154
+ readonly teleportAngle: number;
155
+ readonly settleSteps: number;
156
+ readonly blendSteps: number;
157
+ }
158
+ export declare function resolveClothParameters(parameters: SkinnedClothParameters): ResolvedClothParameters;
159
+ /** Refuse a set-up whose arrays disagree, naming the array and both lengths. */
160
+ export declare function validateClothSetup(setup: SkinnedClothSetup): void;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * What a skinned cloth is made from: particles, their limits, the constraints between them and the
3
+ * numbers the solver runs at — plain arrays, so a port decoding a middleware's cooked data hands it
4
+ * over as it decoded it, and the GPU solver uploads the same arrays the CPU one reads.
5
+ *
6
+ * **Skinned** because its anchor is a skeleton rather than a world: a kinematic particle (inverse
7
+ * mass 0) is placed by its joint influences every step, and every limit is measured from where
8
+ * skinning would put a particle. `ClothBody` is the other kind — a sheet in a physics world — and
9
+ * stays that.
10
+ *
11
+ * Validated at construction and refused by name: a cloth whose arrays disagree in length would read
12
+ * past an end on the GPU, where nothing says so.
13
+ */
14
+ export function resolveClothParameters(parameters) {
15
+ return {
16
+ step: parameters.step ?? 1 / 60,
17
+ substeps: Math.max(1, Math.floor(parameters.substeps ?? 1)),
18
+ iterations: Math.max(1, Math.floor(parameters.iterations ?? 4)),
19
+ gravity: parameters.gravity ?? [0, -9.81, 0],
20
+ damping: parameters.damping ?? 0,
21
+ drag: parameters.drag ?? 0,
22
+ wind: parameters.wind ?? [0, 0, 0],
23
+ linearInertia: Math.min(1, Math.max(0, parameters.linearInertia ?? 1)),
24
+ angularInertia: Math.min(1, Math.max(0, parameters.angularInertia ?? 1)),
25
+ maxDistanceScale: parameters.maxDistanceScale ?? 1,
26
+ margin: parameters.margin ?? 0,
27
+ teleportDistance: parameters.teleportDistance ?? Infinity,
28
+ teleportAngle: parameters.teleportAngle ?? Infinity,
29
+ settleSteps: Math.max(0, Math.floor(parameters.settleSteps ?? 0)),
30
+ blendSteps: Math.max(0, Math.floor(parameters.blendSteps ?? 0)),
31
+ };
32
+ }
33
+ /** Refuse a set-up whose arrays disagree, naming the array and both lengths. */
34
+ export function validateClothSetup(setup) {
35
+ const count = setup.positions.length / 3;
36
+ const fail = (what) => {
37
+ throw new Error(`skinned cloth: ${what}`);
38
+ };
39
+ if (!Number.isInteger(count) || count === 0) {
40
+ fail(`positions hold ${setup.positions.length} floats, which is not a whole number of particles`);
41
+ }
42
+ if (setup.inverseMass.length !== count) {
43
+ fail(`inverseMass has ${setup.inverseMass.length} entries for ${count} particles`);
44
+ }
45
+ const indices = (name, values, arity, constraints) => {
46
+ if (values.length !== constraints * arity) {
47
+ fail(`${name} has ${values.length} indices for ${constraints} constraints of ${arity}`);
48
+ }
49
+ for (let i = 0; i < values.length; i++) {
50
+ if (values[i] >= count)
51
+ fail(`${name}[${i}] names particle ${values[i]} of ${count}`);
52
+ }
53
+ };
54
+ const { distance, bending, tethers, parameters } = setup;
55
+ const links = distance.rest.length;
56
+ if (distance.compliance.length !== links)
57
+ fail(`distance compliance has ${distance.compliance.length} for ${links}`);
58
+ indices('distance.pairs', distance.pairs, 2, links);
59
+ if (bending !== undefined) {
60
+ const quads = bending.rest.length;
61
+ if (bending.compliance.length !== quads)
62
+ fail(`bending compliance has ${bending.compliance.length} for ${quads}`);
63
+ indices('bending.quads', bending.quads, 4, quads);
64
+ }
65
+ if (tethers !== undefined) {
66
+ const n = tethers.lengths.length;
67
+ indices('tethers.particles', tethers.particles, 1, n);
68
+ indices('tethers.anchors', tethers.anchors, 1, n);
69
+ for (let i = 0; i < n; i++) {
70
+ if (setup.inverseMass[tethers.anchors[i]] !== 0) {
71
+ fail(`tether ${i} is anchored to particle ${tethers.anchors[i]}, which is not kinematic`);
72
+ }
73
+ }
74
+ }
75
+ const step = parameters.step ?? 1 / 60;
76
+ if (!(step > 0))
77
+ fail(`a step of ${step} seconds`);
78
+ const per = (name, values, width) => {
79
+ if (values !== undefined && values.length !== count * width) {
80
+ fail(`${name} has ${values.length} entries for ${count} particles of ${width}`);
81
+ }
82
+ };
83
+ per('normals', setup.normals, 3);
84
+ per('joints', setup.joints, 4);
85
+ per('weights', setup.weights, 4);
86
+ per('joints2', setup.joints2, 4);
87
+ per('weights2', setup.weights2, 4);
88
+ per('limits.maxDistance', setup.limits?.maxDistance, 1);
89
+ per('limits.backstop', setup.limits?.backstop, 2);
90
+ per('limits.frontstop', setup.limits?.frontstop, 2);
91
+ per('limits.thickness', setup.limits?.thickness, 1);
92
+ per('limits.selfCollision', setup.limits?.selfCollision, 1);
93
+ if ((setup.joints === undefined) !== (setup.weights === undefined))
94
+ fail('joints without weights, or weights without joints');
95
+ if ((setup.joints2 === undefined) !== (setup.weights2 === undefined))
96
+ fail('joints2 without weights2, or the reverse');
97
+ if (setup.joints2 !== undefined && setup.joints === undefined)
98
+ fail('a second set of influences without the first');
99
+ const bones = (setup.inverseBind?.length ?? 0) / 16;
100
+ if (!Number.isInteger(bones))
101
+ fail(`inverseBind holds ${setup.inverseBind?.length} floats, not a whole number of matrices`);
102
+ if (setup.joints !== undefined && bones === 0)
103
+ fail('joints without inverseBind matrices to skin them by');
104
+ for (const set of [setup.joints, setup.joints2]) {
105
+ if (set === undefined)
106
+ continue;
107
+ for (let i = 0; i < set.length; i++) {
108
+ if (set[i] >= bones)
109
+ fail(`a particle names joint ${set[i]} of ${bones}`);
110
+ }
111
+ }
112
+ (setup.colliders ?? []).forEach((collider, k) => {
113
+ if (collider.joint >= bones)
114
+ fail(`collider ${k} is on joint ${collider.joint} of ${bones}`);
115
+ if (!(collider.radius > 0))
116
+ fail(`collider ${k} has radius ${collider.radius}`);
117
+ if (collider.frame !== undefined && collider.frame.length !== 16)
118
+ fail(`collider ${k}'s frame is not sixteen floats`);
119
+ });
120
+ }
@@ -0,0 +1,22 @@
1
+ /** A particle's predicted position: damping and drag, then gravity, then a substep of travel. */
2
+ export declare function predictParticles(position: Float32Array, previous: Float32Array, velocity: Float32Array, inverseMass: Float32Array, h: number, keep: number, dragKeep: number, gravity: readonly [number, number, number], wind: readonly [number, number, number]): void;
3
+ /** Distance constraints `order[from]` to `order[to]`, one batch. */
4
+ export declare function solveDistanceBatch(position: Float32Array, inverseMass: Float32Array, pairs: Uint32Array, rest: Float32Array, compliance: Float32Array, lambda: Float32Array, order: Uint32Array, from: number, to: number, invH2: number): void;
5
+ /**
6
+ * Dihedral bending constraints of one batch: Müller et al.'s position-based bending constraint, the
7
+ * angle between the two triangles' normals held at its rest value (π is flat).
8
+ *
9
+ * The constraint is `acos(n1 · n2) − rest`, whose gradient is the paper's `q` vectors over
10
+ * `sin θ`: **the paper's `q` are the negative of the derivative of `n1 · n2`**, measured against a
11
+ * finite difference at every component, which is why its update carries a minus sign where a reader
12
+ * taking `q` for the derivative would put a plus — and why a first version here folded a hinge
13
+ * further instead of unfolding it. **Divided rather than multiplied through**, because near flat the `q` vectors vanish
14
+ * with `sin θ` and their ratio stays finite: the paper's own form, `sin θ · q`, slows to nothing just
15
+ * where a garment spends its time. A configuration within a millionth of flat or of folded shut is
16
+ * left for the next iteration rather than divided by.
17
+ */
18
+ export declare function solveBendingBatch(position: Float32Array, inverseMass: Float32Array, quads: Uint32Array, rest: Float32Array, compliance: Float32Array, lambda: Float32Array, order: Uint32Array, from: number, to: number, invH2: number): void;
19
+ /** Tethers: a particle further than its length from its kinematic anchor is brought back to it. */
20
+ export declare function solveTethers(position: Float32Array, particles: Uint32Array, anchors: Uint32Array, lengths: Float32Array): void;
21
+ /** Velocities from where the substep moved each free particle. */
22
+ export declare function finishParticles(position: Float32Array, previous: Float32Array, velocity: Float32Array, inverseMass: Float32Array, invH: number): void;
@@ -0,0 +1,230 @@
1
+ /**
2
+ * The skinned cloth's kernels: one substep's prediction, each kind of constraint over one batch, and
3
+ * the velocities a substep leaves.
4
+ *
5
+ * **Plain functions over flat arrays, one per GPU dispatch.** The WebGPU solver runs the same steps
6
+ * as compute passes over the same arrays, and its WGSL is written to be read beside this file: a
7
+ * kernel here is a thread's body there, the batch loop is the dispatch. Keeping them that shape is
8
+ * what lets the device parity check compare like with like.
9
+ *
10
+ * **XPBD throughout**: a constraint's compliance becomes `α = compliance / h²`, and its multiplier
11
+ * accumulates over the substep's iterations, so stiffness is a property of the cloth rather than of
12
+ * how many times it was iterated — §7 of the physics design, as `cloth.ts` states it.
13
+ *
14
+ * Allocation-free: every kernel writes into arrays it is handed.
15
+ */
16
+ import { exactAcos } from './exact.js';
17
+ /** A particle's predicted position: damping and drag, then gravity, then a substep of travel. */
18
+ export function predictParticles(position, previous, velocity, inverseMass, h, keep, dragKeep, gravity, wind) {
19
+ const count = inverseMass.length;
20
+ for (let i = 0; i < count; i++) {
21
+ const at = i * 3;
22
+ previous[at] = position[at];
23
+ previous[at + 1] = position[at + 1];
24
+ previous[at + 2] = position[at + 2];
25
+ if (inverseMass[i] === 0)
26
+ continue;
27
+ for (let c = 0; c < 3; c++) {
28
+ const air = wind[c];
29
+ const v = air + (velocity[at + c] * keep - air) * dragKeep + gravity[c] * h;
30
+ velocity[at + c] = v;
31
+ position[at + c] = position[at + c] + v * h;
32
+ }
33
+ }
34
+ }
35
+ /** Distance constraints `order[from]` to `order[to]`, one batch. */
36
+ export function solveDistanceBatch(position, inverseMass, pairs, rest, compliance, lambda, order, from, to, invH2) {
37
+ for (let n = from; n < to; n++) {
38
+ const k = order[n];
39
+ const a = pairs[k * 2];
40
+ const b = pairs[k * 2 + 1];
41
+ const wa = inverseMass[a];
42
+ const wb = inverseMass[b];
43
+ const w = wa + wb;
44
+ if (w === 0)
45
+ continue;
46
+ const dx = position[b * 3] - position[a * 3];
47
+ const dy = position[b * 3 + 1] - position[a * 3 + 1];
48
+ const dz = position[b * 3 + 2] - position[a * 3 + 2];
49
+ const length = Math.sqrt(dx * dx + dy * dy + dz * dz);
50
+ if (length < 1e-9)
51
+ continue;
52
+ const alpha = compliance[k] * invH2;
53
+ const old = lambda[k];
54
+ const delta = (-(length - rest[k]) - alpha * old) / (w + alpha);
55
+ lambda[k] = old + delta;
56
+ const s = delta / length;
57
+ position[a * 3] = position[a * 3] - dx * s * wa;
58
+ position[a * 3 + 1] = position[a * 3 + 1] - dy * s * wa;
59
+ position[a * 3 + 2] = position[a * 3 + 2] - dz * s * wa;
60
+ position[b * 3] = position[b * 3] + dx * s * wb;
61
+ position[b * 3 + 1] = position[b * 3 + 1] + dy * s * wb;
62
+ position[b * 3 + 2] = position[b * 3 + 2] + dz * s * wb;
63
+ }
64
+ }
65
+ /* Scratch for one bending constraint: relative positions, normals and the four gradients. */
66
+ const E = new Float64Array(3);
67
+ const P3 = new Float64Array(3);
68
+ const P4 = new Float64Array(3);
69
+ const N1 = new Float64Array(3);
70
+ const N2 = new Float64Array(3);
71
+ const Q = new Float64Array(12);
72
+ const W = new Float64Array(4);
73
+ const IDS = new Uint32Array(4);
74
+ /** A vector's length, through `Math.sqrt`, which every engine rounds the same. */
75
+ function length3(v) {
76
+ const x = v[0];
77
+ const y = v[1];
78
+ const z = v[2];
79
+ return Math.sqrt(x * x + y * y + z * z);
80
+ }
81
+ function cross(a, b, out, at = 0) {
82
+ out[at] = a[1] * b[2] - a[2] * b[1];
83
+ out[at + 1] = a[2] * b[0] - a[0] * b[2];
84
+ out[at + 2] = a[0] * b[1] - a[1] * b[0];
85
+ }
86
+ /**
87
+ * Dihedral bending constraints of one batch: Müller et al.'s position-based bending constraint, the
88
+ * angle between the two triangles' normals held at its rest value (π is flat).
89
+ *
90
+ * The constraint is `acos(n1 · n2) − rest`, whose gradient is the paper's `q` vectors over
91
+ * `sin θ`: **the paper's `q` are the negative of the derivative of `n1 · n2`**, measured against a
92
+ * finite difference at every component, which is why its update carries a minus sign where a reader
93
+ * taking `q` for the derivative would put a plus — and why a first version here folded a hinge
94
+ * further instead of unfolding it. **Divided rather than multiplied through**, because near flat the `q` vectors vanish
95
+ * with `sin θ` and their ratio stays finite: the paper's own form, `sin θ · q`, slows to nothing just
96
+ * where a garment spends its time. A configuration within a millionth of flat or of folded shut is
97
+ * left for the next iteration rather than divided by.
98
+ */
99
+ export function solveBendingBatch(position, inverseMass, quads, rest, compliance, lambda, order, from, to, invH2) {
100
+ for (let n = from; n < to; n++) {
101
+ const k = order[n];
102
+ const i1 = quads[k * 4];
103
+ const i2 = quads[k * 4 + 1];
104
+ const i3 = quads[k * 4 + 2];
105
+ const i4 = quads[k * 4 + 3];
106
+ const w1 = inverseMass[i1];
107
+ const w2 = inverseMass[i2];
108
+ const w3 = inverseMass[i3];
109
+ const w4 = inverseMass[i4];
110
+ if (w1 + w2 + w3 + w4 === 0)
111
+ continue;
112
+ for (let c = 0; c < 3; c++) {
113
+ const origin = position[i1 * 3 + c];
114
+ E[c] = position[i2 * 3 + c] - origin;
115
+ P3[c] = position[i3 * 3 + c] - origin;
116
+ P4[c] = position[i4 * 3 + c] - origin;
117
+ }
118
+ cross(E, P3, N1);
119
+ cross(E, P4, N2);
120
+ const l1 = length3(N1);
121
+ const l2 = length3(N2);
122
+ if (l1 < 1e-12 || l2 < 1e-12)
123
+ continue;
124
+ for (let c = 0; c < 3; c++) {
125
+ N1[c] = N1[c] / l1;
126
+ N2[c] = N2[c] / l2;
127
+ }
128
+ let d = N1[0] * N2[0] + N1[1] * N2[1];
129
+ d += N1[2] * N2[2];
130
+ d = Math.min(1, Math.max(-1, d));
131
+ const sine = Math.sqrt(1 - d * d);
132
+ if (sine < 1e-6)
133
+ continue;
134
+ /* q3 = (e × n2 + (n1 × e) d) / |e × p3|, q4 = (e × n1 + (n2 × e) d) / |e × p4|. */
135
+ crossInto(E, N2, N1, E, d, l1, Q, 6);
136
+ crossInto(E, N1, N2, E, d, l2, Q, 9);
137
+ /* q2 = −(p3 × n2 + (n1 × p3) d) / |e × p3| − (p4 × n1 + (n2 × p4) d) / |e × p4|. */
138
+ crossInto(P3, N2, N1, P3, d, l1, Q, 3);
139
+ const a0 = Q[3];
140
+ const a1 = Q[4];
141
+ const a2 = Q[5];
142
+ crossInto(P4, N1, N2, P4, d, l2, Q, 3);
143
+ Q[3] = -(a0 + Q[3]);
144
+ Q[4] = -(a1 + Q[4]);
145
+ Q[5] = -(a2 + Q[5]);
146
+ for (let c = 0; c < 3; c++) {
147
+ Q[c] = -(Q[3 + c] + Q[6 + c] + Q[9 + c]);
148
+ }
149
+ /* The gradient of acos(d) is q / sin θ (q being −∂d/∂p); its weighted squared length. */
150
+ const weights = W;
151
+ weights[0] = w1;
152
+ weights[1] = w2;
153
+ weights[2] = w3;
154
+ weights[3] = w4;
155
+ let sum = 0;
156
+ for (let j = 0; j < 4; j++) {
157
+ const qx = Q[j * 3];
158
+ const qy = Q[j * 3 + 1];
159
+ const qz = Q[j * 3 + 2];
160
+ sum += weights[j] * (qx * qx + qy * qy + qz * qz);
161
+ }
162
+ sum /= sine * sine;
163
+ if (sum < 1e-12)
164
+ continue;
165
+ const alpha = compliance[k] * invH2;
166
+ const old = lambda[k];
167
+ const delta = (-(exactAcos(d) - rest[k]) - alpha * old) / (sum + alpha);
168
+ lambda[k] = old + delta;
169
+ const scale = delta / sine;
170
+ const ids = IDS;
171
+ ids[0] = i1;
172
+ ids[1] = i2;
173
+ ids[2] = i3;
174
+ ids[3] = i4;
175
+ for (let j = 0; j < 4; j++) {
176
+ const w = weights[j];
177
+ if (w === 0)
178
+ continue;
179
+ const p = ids[j] * 3;
180
+ position[p] = position[p] + w * scale * Q[j * 3];
181
+ position[p + 1] = position[p + 1] + w * scale * Q[j * 3 + 1];
182
+ position[p + 2] = position[p + 2] + w * scale * Q[j * 3 + 2];
183
+ }
184
+ }
185
+ }
186
+ /** `(a × b + (c × e) · d) / length` into `out` at `at`. */
187
+ function crossInto(a, b, c, e, d, length, out, at) {
188
+ const x1 = a[1] * b[2] - a[2] * b[1];
189
+ const y1 = a[2] * b[0] - a[0] * b[2];
190
+ const z1 = a[0] * b[1] - a[1] * b[0];
191
+ const x2 = c[1] * e[2] - c[2] * e[1];
192
+ const y2 = c[2] * e[0] - c[0] * e[2];
193
+ const z2 = c[0] * e[1] - c[1] * e[0];
194
+ out[at] = (x1 + x2 * d) / length;
195
+ out[at + 1] = (y1 + y2 * d) / length;
196
+ out[at + 2] = (z1 + z2 * d) / length;
197
+ }
198
+ /** Tethers: a particle further than its length from its kinematic anchor is brought back to it. */
199
+ export function solveTethers(position, particles, anchors, lengths) {
200
+ for (let k = 0; k < lengths.length; k++) {
201
+ const p = particles[k] * 3;
202
+ const a = anchors[k] * 3;
203
+ const dx = position[p] - position[a];
204
+ const dy = position[p + 1] - position[a + 1];
205
+ const dz = position[p + 2] - position[a + 2];
206
+ const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
207
+ const length = lengths[k];
208
+ if (distance <= length || distance < 1e-9)
209
+ continue;
210
+ const s = length / distance;
211
+ position[p] = position[a] + dx * s;
212
+ position[p + 1] = position[a + 1] + dy * s;
213
+ position[p + 2] = position[a + 2] + dz * s;
214
+ }
215
+ }
216
+ /** Velocities from where the substep moved each free particle. */
217
+ export function finishParticles(position, previous, velocity, inverseMass, invH) {
218
+ for (let i = 0; i < inverseMass.length; i++) {
219
+ const at = i * 3;
220
+ if (inverseMass[i] === 0) {
221
+ velocity[at] = 0;
222
+ velocity[at + 1] = 0;
223
+ velocity[at + 2] = 0;
224
+ continue;
225
+ }
226
+ velocity[at] = (position[at] - previous[at]) * invH;
227
+ velocity[at + 1] = (position[at + 1] - previous[at + 1]) * invH;
228
+ velocity[at + 2] = (position[at + 2] - previous[at + 2]) * invH;
229
+ }
230
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/physics",
3
- "version": "4.8.3",
3
+ "version": "4.8.5",
4
4
  "description": "Collision shapes, spatial queries and swept kinematic collision",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Graph-colour batches: constraints grouped so that no batch moves a particle twice.
3
+ *
4
+ * **What makes a batch parallel.** Every constraint in a batch reads and writes particles no other
5
+ * constraint in it writes, so the batch can be solved in any order — one GPU thread a constraint —
6
+ * and give the answer it gives in sequence. The batches themselves still run one after another,
7
+ * which keeps the solve a Gauss–Seidel one: each batch sees what the last one moved.
8
+ *
9
+ * **A kinematic particle may be shared freely.** Nothing writes it, so two constraints holding the
10
+ * same pinned particle cannot race; colouring around it would only add batches.
11
+ *
12
+ * Greedy, in the order the constraints are given: each takes the first colour none of its moving
13
+ * particles has yet. Not the fewest colours there are, which is a hard problem; a chain gets two and
14
+ * a triangle mesh's edges about six to nine, which is what a solver colouring cooked data gets too.
15
+ */
16
+
17
+ /** Constraints reordered by colour, and where each colour's run starts. */
18
+ export interface ClothBatches {
19
+ /** Constraint indices, batch by batch. */
20
+ readonly order: Uint32Array;
21
+ /** Offsets into `order`: batch `b` is `order[batches[b]]` up to `order[batches[b + 1]]`. */
22
+ readonly batches: Uint32Array;
23
+ }
24
+
25
+ /** Colour `indices`, `arity` particles a constraint, around the particles `inverseMass` pins. */
26
+ export function colourConstraints(
27
+ indices: Uint32Array,
28
+ arity: number,
29
+ inverseMass: Float32Array,
30
+ ): ClothBatches {
31
+ const constraints = indices.length / arity;
32
+ const colour = new Int32Array(constraints);
33
+ /* For each particle, a bit set of the colours already holding it, grown as colours are added. */
34
+ const used: Set<number>[] = [];
35
+ let colours = 0;
36
+ for (let k = 0; k < constraints; k++) {
37
+ let chosen = 0;
38
+ for (; ; chosen++) {
39
+ let free = true;
40
+ for (let j = 0; j < arity && free; j++) {
41
+ const p = indices[k * arity + j] as number;
42
+ if ((inverseMass[p] as number) === 0) continue;
43
+ if (used[p]?.has(chosen) === true) free = false;
44
+ }
45
+ if (free) break;
46
+ }
47
+ colour[k] = chosen;
48
+ colours = Math.max(colours, chosen + 1);
49
+ for (let j = 0; j < arity; j++) {
50
+ const p = indices[k * arity + j] as number;
51
+ if ((inverseMass[p] as number) === 0) continue;
52
+ (used[p] ??= new Set()).add(chosen);
53
+ }
54
+ }
55
+ const batches = new Uint32Array(colours + 1);
56
+ for (let k = 0; k < constraints; k++) batches[(colour[k] as number) + 1]++;
57
+ for (let c = 0; c < colours; c++)
58
+ batches[c + 1] = (batches[c + 1] as number) + (batches[c] as number);
59
+ const cursor = batches.slice(0, colours);
60
+ const order = new Uint32Array(constraints);
61
+ for (let k = 0; k < constraints; k++) order[cursor[colour[k] as number]++] = k;
62
+ return { order, batches };
63
+ }
64
+
65
+ /**
66
+ * Given batches, checked: they cover every constraint once, in order, and none moves a particle
67
+ * twice. Refused by name, because an overlap is a race on the GPU and a wrong answer nowhere else.
68
+ */
69
+ export function givenBatches(
70
+ name: string,
71
+ indices: Uint32Array,
72
+ arity: number,
73
+ inverseMass: Float32Array,
74
+ batches: Uint32Array,
75
+ ): ClothBatches {
76
+ const constraints = indices.length / arity;
77
+ if (batches[0] !== 0 || batches[batches.length - 1] !== constraints) {
78
+ throw new Error(
79
+ `skinned cloth: ${name} batches run from ${batches[0]} to ${batches[batches.length - 1]}, ` +
80
+ `and must cover constraints 0 to ${constraints}`,
81
+ );
82
+ }
83
+ const seen = new Int32Array(inverseMass.length).fill(-1);
84
+ for (let b = 0; b + 1 < batches.length; b++) {
85
+ for (let k = batches[b] as number; k < (batches[b + 1] as number); k++) {
86
+ for (let j = 0; j < arity; j++) {
87
+ const p = indices[k * arity + j] as number;
88
+ if ((inverseMass[p] as number) === 0) continue;
89
+ if (seen[p] === b) {
90
+ throw new Error(
91
+ `skinned cloth: ${name} batch ${b} moves particle ${p} twice, so it cannot run in parallel`,
92
+ );
93
+ }
94
+ seen[p] = b;
95
+ }
96
+ }
97
+ }
98
+ return { order: Uint32Array.from({ length: constraints }, (_, k) => k), batches };
99
+ }
100
+
101
+ /**
102
+ * A kind of constraint's batches: the set-up's own, checked, where it gives them, and coloured here
103
+ * where it does not. One decision for both solvers, which must batch a garment the same way.
104
+ */
105
+ export function batchesOf(
106
+ name: string,
107
+ indices: Uint32Array,
108
+ arity: number,
109
+ inverseMass: Float32Array,
110
+ given: Uint32Array | undefined,
111
+ ): ClothBatches {
112
+ return given === undefined
113
+ ? colourConstraints(indices, arity, inverseMass)
114
+ : givenBatches(name, indices, arity, inverseMass, given);
115
+ }