@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.
- package/README.md +1 -1
- package/dist/clothBatches.d.ts +34 -0
- package/dist/clothBatches.js +91 -0
- package/dist/clothControl.d.ts +55 -0
- package/dist/clothControl.js +106 -0
- package/dist/clothLimits.d.ts +33 -0
- package/dist/clothLimits.js +108 -0
- package/dist/clothPose.d.ts +36 -0
- package/dist/clothPose.js +225 -0
- package/dist/clothTargets.d.ts +44 -0
- package/dist/clothTargets.js +124 -0
- package/dist/exact.d.ts +80 -0
- package/dist/exact.js +331 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +19 -0
- package/dist/ragdoll.js +126 -11
- package/dist/skinnedCloth.d.ts +63 -0
- package/dist/skinnedCloth.js +214 -0
- package/dist/skinnedClothSetup.d.ts +160 -0
- package/dist/skinnedClothSetup.js +120 -0
- package/dist/skinnedClothSolve.d.ts +22 -0
- package/dist/skinnedClothSolve.js +230 -0
- package/package.json +1 -1
- package/src/clothBatches.ts +115 -0
- package/src/clothControl.ts +131 -0
- package/src/clothLimits.ts +133 -0
- package/src/clothPose.ts +272 -0
- package/src/clothTargets.ts +146 -0
- package/src/exact.ts +344 -0
- package/src/index.ts +32 -0
- package/src/ragdoll.ts +142 -11
- package/src/skinnedCloth.ts +323 -0
- package/src/skinnedClothSetup.ts +273 -0
- package/src/skinnedClothSolve.ts +283 -0
|
@@ -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
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|