@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.
- 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/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/skinnedCloth.ts +323 -0
- package/src/skinnedClothSetup.ts +273 -0
- package/src/skinnedClothSolve.ts +283 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A skinned cloth on the CPU: the reference solver, and the one WebGL2 and Node run.
|
|
3
|
+
*
|
|
4
|
+
* **Whole fixed steps, whatever the frame.** The set-up names its step (a garment's is 1/60); a
|
|
5
|
+
* caller in the engine's own fixed-step loop calls `step` once a tick, and any other calls `advance`
|
|
6
|
+
* with the frame's time and the remainder waits for the next frame. The cloth therefore behaves the
|
|
7
|
+
* same at 60, 120 and 144 frames a second, and a frame draws it on `alpha` between its last two
|
|
8
|
+
* steps — the engine's rule, and what keeps a high refresh rate from drawing the same cloth twice.
|
|
9
|
+
*
|
|
10
|
+
* **What it gives up**: an `advance` of a long time runs that many steps, with no ceiling; a caller
|
|
11
|
+
* that stalls for a second gets a second of cloth. The engine's loop clamps its own catch-up, and a
|
|
12
|
+
* caller outside it is expected to clamp the time it hands over.
|
|
13
|
+
*
|
|
14
|
+
* Every step allocates nothing. The WebGPU solver in `@driftengine/core` runs the same kernels over
|
|
15
|
+
* the same data (`skinnedClothSolve.ts`, `clothLimits.ts`) under the same `ClothControl`, and is held
|
|
16
|
+
* to this one by a parity check on the device.
|
|
17
|
+
*/
|
|
18
|
+
import { batchesOf } from './clothBatches.ts';
|
|
19
|
+
import { ClothControl } from './clothControl.ts';
|
|
20
|
+
import type { ClothDevice } from './clothControl.ts';
|
|
21
|
+
import { applyColliders, applyMaxDistance, applyStops } from './clothLimits.ts';
|
|
22
|
+
import { carryParticles } from './clothPose.ts';
|
|
23
|
+
import type { ClothTargets } from './clothTargets.ts';
|
|
24
|
+
import { exactExp } from './exact.ts';
|
|
25
|
+
import type { ClothBatches } from './clothBatches.ts';
|
|
26
|
+
import {
|
|
27
|
+
finishParticles,
|
|
28
|
+
predictParticles,
|
|
29
|
+
solveBendingBatch,
|
|
30
|
+
solveDistanceBatch,
|
|
31
|
+
solveTethers,
|
|
32
|
+
} from './skinnedClothSolve.ts';
|
|
33
|
+
import { validateClothSetup } from './skinnedClothSetup.ts';
|
|
34
|
+
import type {
|
|
35
|
+
ClothLimits,
|
|
36
|
+
ResolvedClothParameters,
|
|
37
|
+
SkinnedClothSetup,
|
|
38
|
+
TetherConstraints,
|
|
39
|
+
} from './skinnedClothSetup.ts';
|
|
40
|
+
|
|
41
|
+
/** One kind of constraint, ready to solve: its arrays, its multipliers and its batches. */
|
|
42
|
+
interface Solvable {
|
|
43
|
+
readonly indices: Uint32Array;
|
|
44
|
+
readonly rest: Float32Array;
|
|
45
|
+
readonly compliance: Float32Array;
|
|
46
|
+
readonly lambda: Float32Array;
|
|
47
|
+
readonly batches: ClothBatches;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function solvable(
|
|
51
|
+
name: string,
|
|
52
|
+
indices: Uint32Array,
|
|
53
|
+
arity: number,
|
|
54
|
+
rest: Float32Array,
|
|
55
|
+
compliance: Float32Array,
|
|
56
|
+
inverseMass: Float32Array,
|
|
57
|
+
given: Uint32Array | undefined,
|
|
58
|
+
): Solvable {
|
|
59
|
+
const batches = batchesOf(name, indices, arity, inverseMass, given);
|
|
60
|
+
return { indices, rest, compliance, lambda: new Float32Array(rest.length), batches };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A garment's particles, constraints and solver state, stepped at the set-up's fixed step. Read
|
|
65
|
+
* `positions` after a step, or `interpolate` between the last two for a frame's `alpha`. The
|
|
66
|
+
* schedule — poses, steps, resets, the blend — is `ClothControl`'s; this is its CPU device.
|
|
67
|
+
*/
|
|
68
|
+
export class SkinnedCloth {
|
|
69
|
+
/** How many particles. */
|
|
70
|
+
readonly count: number;
|
|
71
|
+
/** Where each particle is now, three floats a particle. */
|
|
72
|
+
readonly positions: Float32Array;
|
|
73
|
+
/** Metres a second, three floats a particle. */
|
|
74
|
+
readonly velocities: Float32Array;
|
|
75
|
+
readonly inverseMass: Float32Array;
|
|
76
|
+
/** The parameters with their defaults filled in. */
|
|
77
|
+
readonly parameters: ResolvedClothParameters;
|
|
78
|
+
|
|
79
|
+
/** Where each particle was when the last step began, for drawing on `alpha`. */
|
|
80
|
+
private readonly stepStart: Float32Array;
|
|
81
|
+
/** Where each particle was when the current substep began, for its velocity. */
|
|
82
|
+
private readonly substepStart: Float32Array;
|
|
83
|
+
private readonly distance: Solvable;
|
|
84
|
+
private readonly bending: Solvable | null;
|
|
85
|
+
private readonly tethers: TetherConstraints | null;
|
|
86
|
+
private readonly control: ClothControl;
|
|
87
|
+
private readonly targets: ClothTargets;
|
|
88
|
+
private readonly limits: ClothLimits;
|
|
89
|
+
private readonly colliders: number;
|
|
90
|
+
/** The air the cloth drags toward, metres a second: the set-up's until `setWind` says otherwise. */
|
|
91
|
+
private readonly wind: [number, number, number];
|
|
92
|
+
|
|
93
|
+
constructor(setup: SkinnedClothSetup) {
|
|
94
|
+
validateClothSetup(setup);
|
|
95
|
+
this.count = setup.positions.length / 3;
|
|
96
|
+
this.positions = new Float32Array(setup.positions);
|
|
97
|
+
this.velocities = new Float32Array(setup.positions.length);
|
|
98
|
+
this.stepStart = new Float32Array(setup.positions);
|
|
99
|
+
this.substepStart = new Float32Array(setup.positions.length);
|
|
100
|
+
this.inverseMass = new Float32Array(setup.inverseMass);
|
|
101
|
+
const { distance, bending } = setup;
|
|
102
|
+
this.distance = solvable(
|
|
103
|
+
'distance',
|
|
104
|
+
distance.pairs,
|
|
105
|
+
2,
|
|
106
|
+
distance.rest,
|
|
107
|
+
distance.compliance,
|
|
108
|
+
this.inverseMass,
|
|
109
|
+
distance.batches,
|
|
110
|
+
);
|
|
111
|
+
this.bending =
|
|
112
|
+
bending === undefined
|
|
113
|
+
? null
|
|
114
|
+
: solvable(
|
|
115
|
+
'bending',
|
|
116
|
+
bending.quads,
|
|
117
|
+
4,
|
|
118
|
+
bending.rest,
|
|
119
|
+
bending.compliance,
|
|
120
|
+
this.inverseMass,
|
|
121
|
+
bending.batches,
|
|
122
|
+
);
|
|
123
|
+
this.tethers = setup.tethers ?? null;
|
|
124
|
+
this.limits = setup.limits ?? {};
|
|
125
|
+
this.colliders = setup.colliders?.length ?? 0;
|
|
126
|
+
/* Made once, so a step builds nothing: the control calls back into the private step. */
|
|
127
|
+
const device: ClothDevice = {
|
|
128
|
+
targetsChanged: () => {},
|
|
129
|
+
rest: () => {
|
|
130
|
+
this.rest();
|
|
131
|
+
},
|
|
132
|
+
step: (fraction, blend) => {
|
|
133
|
+
this.stepAt(fraction, blend);
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
this.control = new ClothControl(setup, device);
|
|
137
|
+
this.parameters = this.control.parameters;
|
|
138
|
+
this.targets = this.control.targets;
|
|
139
|
+
this.wind = [...this.parameters.wind];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The air the steps to come drag the cloth toward, world space, metres a second: the frame's
|
|
144
|
+
* sample of the scene's one wind, so a garment moves with the smoke and the grass rather than
|
|
145
|
+
* with a second wind of its own. See `AGENTS.md`, "One wind, sampled once".
|
|
146
|
+
*/
|
|
147
|
+
setWind(x: number, y: number, z: number): void {
|
|
148
|
+
this.wind[0] = x;
|
|
149
|
+
this.wind[1] = y;
|
|
150
|
+
this.wind[2] = z;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** See `ClothControl.setPose`. */
|
|
154
|
+
setPose(globals: Float32Array, model: Float32Array): void {
|
|
155
|
+
this.control.setPose(globals, model);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** See `ClothControl.reset`. */
|
|
159
|
+
reset(): void {
|
|
160
|
+
this.control.reset();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** See `ClothControl.advance`. */
|
|
164
|
+
advance(dt: number): number {
|
|
165
|
+
return this.control.advance(dt);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** How far the time handed to `advance` reaches into the next step, 0 to 1. */
|
|
169
|
+
get alpha(): number {
|
|
170
|
+
return this.control.alpha;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Each particle between the start and the end of the last step, at `alpha`, into `out`. */
|
|
174
|
+
interpolate(alpha: number, out: Float32Array): void {
|
|
175
|
+
const from = this.stepStart;
|
|
176
|
+
const to = this.positions;
|
|
177
|
+
for (let i = 0; i < to.length; i++) {
|
|
178
|
+
const a = from[i] as number;
|
|
179
|
+
out[i] = a + ((to[i] as number) - a) * alpha;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** One fixed step, all the way to the latest pose: for a caller in the engine's fixed-step loop. */
|
|
184
|
+
step(): void {
|
|
185
|
+
this.control.step();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Every particle at its latest skinned position, at rest. */
|
|
189
|
+
private rest(): void {
|
|
190
|
+
this.positions.set(this.targets.latest);
|
|
191
|
+
this.stepStart.set(this.positions);
|
|
192
|
+
this.velocities.fill(0);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* One fixed step, `fraction` of the way from the last pose to the latest: carried by the share of
|
|
197
|
+
* the character's motion it does not keep, its kinematic particles placed, then its substeps —
|
|
198
|
+
* each predicted, constrained, limited and turned into velocities.
|
|
199
|
+
*/
|
|
200
|
+
private stepAt(fraction: number, blend: number): void {
|
|
201
|
+
/* Where the last step ended, kinematic particles included, before this one moves anything. */
|
|
202
|
+
this.stepStart.set(this.positions);
|
|
203
|
+
const { step, substeps, iterations, damping, drag, gravity } = this.parameters;
|
|
204
|
+
const wind = this.wind;
|
|
205
|
+
const targets = this.targets;
|
|
206
|
+
carryParticles(
|
|
207
|
+
this.positions,
|
|
208
|
+
this.velocities,
|
|
209
|
+
this.inverseMass,
|
|
210
|
+
targets.modelBefore,
|
|
211
|
+
targets.modelNow,
|
|
212
|
+
this.parameters.linearInertia,
|
|
213
|
+
this.parameters.angularInertia,
|
|
214
|
+
);
|
|
215
|
+
placeKinematic(this.positions, this.inverseMass, targets.position);
|
|
216
|
+
const h = step / substeps;
|
|
217
|
+
const invH = 1 / h;
|
|
218
|
+
const invH2 = invH * invH;
|
|
219
|
+
const keep = exactExp(damping * h);
|
|
220
|
+
const dragKeep = exactExp(drag * h);
|
|
221
|
+
for (let s = 0; s < substeps; s++) {
|
|
222
|
+
predictParticles(
|
|
223
|
+
this.positions,
|
|
224
|
+
this.substepStart,
|
|
225
|
+
this.velocities,
|
|
226
|
+
this.inverseMass,
|
|
227
|
+
h,
|
|
228
|
+
keep,
|
|
229
|
+
dragKeep,
|
|
230
|
+
gravity,
|
|
231
|
+
wind,
|
|
232
|
+
);
|
|
233
|
+
this.distance.lambda.fill(0);
|
|
234
|
+
this.bending?.lambda.fill(0);
|
|
235
|
+
for (let pass = 0; pass < iterations; pass++) {
|
|
236
|
+
this.solve(this.distance, solveDistanceBatch, invH2);
|
|
237
|
+
if (this.bending !== null) this.solve(this.bending, solveBendingBatch, invH2);
|
|
238
|
+
const tethers = this.tethers;
|
|
239
|
+
if (tethers !== null) {
|
|
240
|
+
solveTethers(this.positions, tethers.particles, tethers.anchors, tethers.lengths);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
this.limit();
|
|
244
|
+
finishParticles(this.positions, this.substepStart, this.velocities, this.inverseMass, invH);
|
|
245
|
+
}
|
|
246
|
+
if (blend > 0) this.blend(blend);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** The limits and the colliders, after the constraints, so a garment pulling into the body loses. */
|
|
250
|
+
private limit(): void {
|
|
251
|
+
const { positions, inverseMass, targets, limits } = this;
|
|
252
|
+
const { maxDistanceScale, margin } = this.parameters;
|
|
253
|
+
if (limits.maxDistance !== undefined) {
|
|
254
|
+
applyMaxDistance(
|
|
255
|
+
positions,
|
|
256
|
+
inverseMass,
|
|
257
|
+
targets.position,
|
|
258
|
+
limits.maxDistance,
|
|
259
|
+
maxDistanceScale,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
if (limits.backstop !== undefined) {
|
|
263
|
+
applyStops(positions, inverseMass, targets.position, targets.normal, limits.backstop, -1);
|
|
264
|
+
}
|
|
265
|
+
if (limits.frontstop !== undefined) {
|
|
266
|
+
applyStops(positions, inverseMass, targets.position, targets.normal, limits.frontstop, 1);
|
|
267
|
+
}
|
|
268
|
+
if (this.colliders > 0) {
|
|
269
|
+
applyColliders(
|
|
270
|
+
positions,
|
|
271
|
+
inverseMass,
|
|
272
|
+
limits.thickness ?? null,
|
|
273
|
+
margin,
|
|
274
|
+
targets.colliderEnds,
|
|
275
|
+
targets.colliderRadii,
|
|
276
|
+
this.colliders,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** After a reset, the share of the simulation drawn grows from nothing to all over the blend. */
|
|
282
|
+
private blend(share: number): void {
|
|
283
|
+
const target = this.targets.position;
|
|
284
|
+
for (let i = 0; i < this.positions.length; i++) {
|
|
285
|
+
const t = target[i] as number;
|
|
286
|
+
this.positions[i] = t + ((this.positions[i] as number) - t) * share;
|
|
287
|
+
this.velocities[i] = (this.velocities[i] as number) * share;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Every batch of one kind, in colour order. */
|
|
292
|
+
private solve(kind: Solvable, kernel: typeof solveDistanceBatch, invH2: number): void {
|
|
293
|
+
const { batches, order } = kind.batches;
|
|
294
|
+
for (let b = 0; b + 1 < batches.length; b++) {
|
|
295
|
+
kernel(
|
|
296
|
+
this.positions,
|
|
297
|
+
this.inverseMass,
|
|
298
|
+
kind.indices,
|
|
299
|
+
kind.rest,
|
|
300
|
+
kind.compliance,
|
|
301
|
+
kind.lambda,
|
|
302
|
+
order,
|
|
303
|
+
batches[b] as number,
|
|
304
|
+
batches[b + 1] as number,
|
|
305
|
+
invH2,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Every kinematic particle exactly where skinning puts it at this step. */
|
|
312
|
+
function placeKinematic(
|
|
313
|
+
position: Float32Array,
|
|
314
|
+
inverseMass: Float32Array,
|
|
315
|
+
target: Float32Array,
|
|
316
|
+
): void {
|
|
317
|
+
for (let i = 0; i < inverseMass.length; i++) {
|
|
318
|
+
if ((inverseMass[i] as number) !== 0) continue;
|
|
319
|
+
position[i * 3] = target[i * 3] as number;
|
|
320
|
+
position[i * 3 + 1] = target[i * 3 + 1] as number;
|
|
321
|
+
position[i * 3 + 2] = target[i * 3 + 2] as number;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
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
|
+
|
|
15
|
+
/** Distance constraints: two particles, a rest length, a compliance (metres per newton). */
|
|
16
|
+
export interface DistanceConstraints {
|
|
17
|
+
/** Two particle indices a constraint. */
|
|
18
|
+
readonly pairs: Uint32Array;
|
|
19
|
+
readonly rest: Float32Array;
|
|
20
|
+
readonly compliance: Float32Array;
|
|
21
|
+
/**
|
|
22
|
+
* Graph-colour batches, as offsets into the constraints: batch `b` is constraints
|
|
23
|
+
* `batches[b]` to `batches[b + 1]`, none of which moves a particle another in the batch moves.
|
|
24
|
+
* Absent, the solver colours them itself. Given, they are checked and refused if they overlap.
|
|
25
|
+
*/
|
|
26
|
+
readonly batches?: Uint32Array;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Dihedral bending: four particles — the shared edge, then each triangle's far corner. */
|
|
30
|
+
export interface BendingConstraints {
|
|
31
|
+
readonly quads: Uint32Array;
|
|
32
|
+
/** The angle between the two triangles' normals at rest, in radians: π is flat. */
|
|
33
|
+
readonly rest: Float32Array;
|
|
34
|
+
readonly compliance: Float32Array;
|
|
35
|
+
readonly batches?: Uint32Array;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Long-range attachments: a particle may be no further than a length from a kinematic anchor. */
|
|
39
|
+
export interface TetherConstraints {
|
|
40
|
+
readonly particles: Uint32Array;
|
|
41
|
+
readonly anchors: Uint32Array;
|
|
42
|
+
readonly lengths: Float32Array;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Where a particle may not go, measured from where skinning puts it. Each optional, one entry a
|
|
47
|
+
* particle (two for a stop), and `Infinity` is none — which is how cooked data says it.
|
|
48
|
+
*/
|
|
49
|
+
export interface ClothLimits {
|
|
50
|
+
/** Metres from the skinned position a particle may not leave: a sphere around it. */
|
|
51
|
+
readonly maxDistance?: Float32Array;
|
|
52
|
+
/** A distance and a radius a particle: a sphere behind the skinned position, along its normal. */
|
|
53
|
+
readonly backstop?: Float32Array;
|
|
54
|
+
/** The same in front. */
|
|
55
|
+
readonly frontstop?: Float32Array;
|
|
56
|
+
/** Metres a particle keeps from a collider's surface. */
|
|
57
|
+
readonly thickness?: Float32Array;
|
|
58
|
+
/**
|
|
59
|
+
* Which particles collide with each other, one flag each. **Accepted and not yet simulated**: a
|
|
60
|
+
* garment held by its colliders and its backstops does not need it to look right, and the GPU
|
|
61
|
+
* solver has no neighbour search yet. Carried so a set-up need not be rewritten when it arrives.
|
|
62
|
+
*/
|
|
63
|
+
readonly selfCollision?: Uint8Array;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A collider on a joint: a sphere, or a tapered capsule along its frame's +Z.
|
|
68
|
+
*
|
|
69
|
+
* Its frame is in the joint's own space, so it rides the joint whatever the animation does.
|
|
70
|
+
*/
|
|
71
|
+
export interface ClothCollider {
|
|
72
|
+
readonly joint: number;
|
|
73
|
+
readonly radius: number;
|
|
74
|
+
/** The radius at the far end; the same as `radius` where absent. */
|
|
75
|
+
readonly radius2?: number;
|
|
76
|
+
/** Metres along the frame's +Z; zero or absent is a sphere. */
|
|
77
|
+
readonly length?: number;
|
|
78
|
+
/** Sixteen floats, column-major, in the joint's space; identity where absent. */
|
|
79
|
+
readonly frame?: Float32Array;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** What the solver runs at. Every field has a default; the defaults are a garment's, not a sheet's. */
|
|
83
|
+
export interface SkinnedClothParameters {
|
|
84
|
+
/** The fixed step, in seconds. The solver runs whole steps whatever the frame. Default 1/60. */
|
|
85
|
+
readonly step?: number;
|
|
86
|
+
/** Substeps a step. Default 1. */
|
|
87
|
+
readonly substeps?: number;
|
|
88
|
+
/** Constraint passes a substep. Default 4. */
|
|
89
|
+
readonly iterations?: number;
|
|
90
|
+
/** Metres a second squared, world space. Default [0, −9.81, 0]. */
|
|
91
|
+
readonly gravity?: readonly [number, number, number];
|
|
92
|
+
/**
|
|
93
|
+
* The log of the velocity kept after a second: `ln 0.25` keeps a quarter. Zero is none; it is
|
|
94
|
+
* applied as `exp(damping · h)` a substep, so it means one thing at every step length.
|
|
95
|
+
*/
|
|
96
|
+
readonly damping?: number;
|
|
97
|
+
/** The same, of the velocity relative to the wind: how fast the air takes the cloth with it. */
|
|
98
|
+
readonly drag?: number;
|
|
99
|
+
/**
|
|
100
|
+
* Metres a second, world space: the air the cloth starts in. Default still air. A garment in a
|
|
101
|
+
* scene follows the scene's wind a frame at a time, through `setWind`.
|
|
102
|
+
*/
|
|
103
|
+
readonly wind?: readonly [number, number, number];
|
|
104
|
+
/**
|
|
105
|
+
* How much of the character's own movement and turning the cloth keeps as its own, 0 to 1: 1 is
|
|
106
|
+
* simulated wholly in the world, so a dash leaves the hem behind; 0 moves it with the character.
|
|
107
|
+
* Default 1.
|
|
108
|
+
*/
|
|
109
|
+
readonly linearInertia?: number;
|
|
110
|
+
readonly angularInertia?: number;
|
|
111
|
+
/** Scales every max distance, for a garment worn looser or tighter than cooked. Default 1. */
|
|
112
|
+
readonly maxDistanceScale?: number;
|
|
113
|
+
/** Metres added to every collider's radius, beyond a particle's own thickness. Default 0. */
|
|
114
|
+
readonly margin?: number;
|
|
115
|
+
/** A move of the character's origin past this, between two poses, resets the cloth. Default none. */
|
|
116
|
+
readonly teleportDistance?: number;
|
|
117
|
+
/** A turn past this many radians between two poses resets it too. Default none. */
|
|
118
|
+
readonly teleportAngle?: number;
|
|
119
|
+
/** Steps run at once when the cloth is reset, so it starts hanging rather than falling. */
|
|
120
|
+
readonly settleSteps?: number;
|
|
121
|
+
/** Steps over which a reset cloth blends from its skinned pose into its simulation. */
|
|
122
|
+
readonly blendSteps?: number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Everything a skinned cloth is made from. */
|
|
126
|
+
export interface SkinnedClothSetup {
|
|
127
|
+
/** Rest positions, three floats a particle, in the space the skinning starts from. */
|
|
128
|
+
readonly positions: Float32Array;
|
|
129
|
+
/** One a particle: zero is kinematic, following its skinned position exactly. */
|
|
130
|
+
readonly inverseMass: Float32Array;
|
|
131
|
+
readonly distance: DistanceConstraints;
|
|
132
|
+
readonly bending?: BendingConstraints;
|
|
133
|
+
readonly tethers?: TetherConstraints;
|
|
134
|
+
readonly parameters: SkinnedClothParameters;
|
|
135
|
+
/** Rest normals, three floats a particle: what a backstop and a frontstop are measured along. */
|
|
136
|
+
readonly normals?: Float32Array;
|
|
137
|
+
/** Up to eight joint influences a particle, as `MeshData` carries them. Absent: not skinned. */
|
|
138
|
+
readonly joints?: Float32Array;
|
|
139
|
+
readonly weights?: Float32Array;
|
|
140
|
+
readonly joints2?: Float32Array;
|
|
141
|
+
readonly weights2?: Float32Array;
|
|
142
|
+
/** Sixteen floats a joint: each joint's inverse bind matrix, column-major. */
|
|
143
|
+
readonly inverseBind?: Float32Array;
|
|
144
|
+
readonly limits?: ClothLimits;
|
|
145
|
+
readonly colliders?: readonly ClothCollider[];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The parameters with every default filled in. */
|
|
149
|
+
export interface ResolvedClothParameters {
|
|
150
|
+
readonly step: number;
|
|
151
|
+
readonly substeps: number;
|
|
152
|
+
readonly iterations: number;
|
|
153
|
+
readonly gravity: readonly [number, number, number];
|
|
154
|
+
readonly damping: number;
|
|
155
|
+
readonly drag: number;
|
|
156
|
+
readonly wind: readonly [number, number, number];
|
|
157
|
+
readonly linearInertia: number;
|
|
158
|
+
readonly angularInertia: number;
|
|
159
|
+
readonly maxDistanceScale: number;
|
|
160
|
+
readonly margin: number;
|
|
161
|
+
readonly teleportDistance: number;
|
|
162
|
+
readonly teleportAngle: number;
|
|
163
|
+
readonly settleSteps: number;
|
|
164
|
+
readonly blendSteps: number;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function resolveClothParameters(
|
|
168
|
+
parameters: SkinnedClothParameters,
|
|
169
|
+
): ResolvedClothParameters {
|
|
170
|
+
return {
|
|
171
|
+
step: parameters.step ?? 1 / 60,
|
|
172
|
+
substeps: Math.max(1, Math.floor(parameters.substeps ?? 1)),
|
|
173
|
+
iterations: Math.max(1, Math.floor(parameters.iterations ?? 4)),
|
|
174
|
+
gravity: parameters.gravity ?? [0, -9.81, 0],
|
|
175
|
+
damping: parameters.damping ?? 0,
|
|
176
|
+
drag: parameters.drag ?? 0,
|
|
177
|
+
wind: parameters.wind ?? [0, 0, 0],
|
|
178
|
+
linearInertia: Math.min(1, Math.max(0, parameters.linearInertia ?? 1)),
|
|
179
|
+
angularInertia: Math.min(1, Math.max(0, parameters.angularInertia ?? 1)),
|
|
180
|
+
maxDistanceScale: parameters.maxDistanceScale ?? 1,
|
|
181
|
+
margin: parameters.margin ?? 0,
|
|
182
|
+
teleportDistance: parameters.teleportDistance ?? Infinity,
|
|
183
|
+
teleportAngle: parameters.teleportAngle ?? Infinity,
|
|
184
|
+
settleSteps: Math.max(0, Math.floor(parameters.settleSteps ?? 0)),
|
|
185
|
+
blendSteps: Math.max(0, Math.floor(parameters.blendSteps ?? 0)),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Refuse a set-up whose arrays disagree, naming the array and both lengths. */
|
|
190
|
+
export function validateClothSetup(setup: SkinnedClothSetup): void {
|
|
191
|
+
const count = setup.positions.length / 3;
|
|
192
|
+
const fail = (what: string): never => {
|
|
193
|
+
throw new Error(`skinned cloth: ${what}`);
|
|
194
|
+
};
|
|
195
|
+
if (!Number.isInteger(count) || count === 0) {
|
|
196
|
+
fail(
|
|
197
|
+
`positions hold ${setup.positions.length} floats, which is not a whole number of particles`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
if (setup.inverseMass.length !== count) {
|
|
201
|
+
fail(`inverseMass has ${setup.inverseMass.length} entries for ${count} particles`);
|
|
202
|
+
}
|
|
203
|
+
const indices = (name: string, values: Uint32Array, arity: number, constraints: number): void => {
|
|
204
|
+
if (values.length !== constraints * arity) {
|
|
205
|
+
fail(`${name} has ${values.length} indices for ${constraints} constraints of ${arity}`);
|
|
206
|
+
}
|
|
207
|
+
for (let i = 0; i < values.length; i++) {
|
|
208
|
+
if ((values[i] as number) >= count)
|
|
209
|
+
fail(`${name}[${i}] names particle ${values[i]} of ${count}`);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
const { distance, bending, tethers, parameters } = setup;
|
|
213
|
+
const links = distance.rest.length;
|
|
214
|
+
if (distance.compliance.length !== links)
|
|
215
|
+
fail(`distance compliance has ${distance.compliance.length} for ${links}`);
|
|
216
|
+
indices('distance.pairs', distance.pairs, 2, links);
|
|
217
|
+
if (bending !== undefined) {
|
|
218
|
+
const quads = bending.rest.length;
|
|
219
|
+
if (bending.compliance.length !== quads)
|
|
220
|
+
fail(`bending compliance has ${bending.compliance.length} for ${quads}`);
|
|
221
|
+
indices('bending.quads', bending.quads, 4, quads);
|
|
222
|
+
}
|
|
223
|
+
if (tethers !== undefined) {
|
|
224
|
+
const n = tethers.lengths.length;
|
|
225
|
+
indices('tethers.particles', tethers.particles, 1, n);
|
|
226
|
+
indices('tethers.anchors', tethers.anchors, 1, n);
|
|
227
|
+
for (let i = 0; i < n; i++) {
|
|
228
|
+
if ((setup.inverseMass[tethers.anchors[i] as number] as number) !== 0) {
|
|
229
|
+
fail(`tether ${i} is anchored to particle ${tethers.anchors[i]}, which is not kinematic`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const step = parameters.step ?? 1 / 60;
|
|
234
|
+
if (!(step > 0)) fail(`a step of ${step} seconds`);
|
|
235
|
+
const per = (name: string, values: ArrayLike<number> | undefined, width: number): void => {
|
|
236
|
+
if (values !== undefined && values.length !== count * width) {
|
|
237
|
+
fail(`${name} has ${values.length} entries for ${count} particles of ${width}`);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
per('normals', setup.normals, 3);
|
|
241
|
+
per('joints', setup.joints, 4);
|
|
242
|
+
per('weights', setup.weights, 4);
|
|
243
|
+
per('joints2', setup.joints2, 4);
|
|
244
|
+
per('weights2', setup.weights2, 4);
|
|
245
|
+
per('limits.maxDistance', setup.limits?.maxDistance, 1);
|
|
246
|
+
per('limits.backstop', setup.limits?.backstop, 2);
|
|
247
|
+
per('limits.frontstop', setup.limits?.frontstop, 2);
|
|
248
|
+
per('limits.thickness', setup.limits?.thickness, 1);
|
|
249
|
+
per('limits.selfCollision', setup.limits?.selfCollision, 1);
|
|
250
|
+
if ((setup.joints === undefined) !== (setup.weights === undefined))
|
|
251
|
+
fail('joints without weights, or weights without joints');
|
|
252
|
+
if ((setup.joints2 === undefined) !== (setup.weights2 === undefined))
|
|
253
|
+
fail('joints2 without weights2, or the reverse');
|
|
254
|
+
if (setup.joints2 !== undefined && setup.joints === undefined)
|
|
255
|
+
fail('a second set of influences without the first');
|
|
256
|
+
const bones = (setup.inverseBind?.length ?? 0) / 16;
|
|
257
|
+
if (!Number.isInteger(bones))
|
|
258
|
+
fail(`inverseBind holds ${setup.inverseBind?.length} floats, not a whole number of matrices`);
|
|
259
|
+
if (setup.joints !== undefined && bones === 0)
|
|
260
|
+
fail('joints without inverseBind matrices to skin them by');
|
|
261
|
+
for (const set of [setup.joints, setup.joints2]) {
|
|
262
|
+
if (set === undefined) continue;
|
|
263
|
+
for (let i = 0; i < set.length; i++) {
|
|
264
|
+
if ((set[i] as number) >= bones) fail(`a particle names joint ${set[i]} of ${bones}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
(setup.colliders ?? []).forEach((collider, k) => {
|
|
268
|
+
if (collider.joint >= bones) fail(`collider ${k} is on joint ${collider.joint} of ${bones}`);
|
|
269
|
+
if (!(collider.radius > 0)) fail(`collider ${k} has radius ${collider.radius}`);
|
|
270
|
+
if (collider.frame !== undefined && collider.frame.length !== 16)
|
|
271
|
+
fail(`collider ${k}'s frame is not sixteen floats`);
|
|
272
|
+
});
|
|
273
|
+
}
|