@driftengine/physics 3.61.0
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/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +503 -0
- package/dist/bodies.d.ts +121 -0
- package/dist/bodies.js +298 -0
- package/dist/buoyancy.d.ts +46 -0
- package/dist/buoyancy.js +45 -0
- package/dist/cloth.d.ts +226 -0
- package/dist/cloth.js +587 -0
- package/dist/collide/aabb.d.ts +10 -0
- package/dist/collide/aabb.js +11 -0
- package/dist/collide/body.d.ts +202 -0
- package/dist/collide/body.js +142 -0
- package/dist/collide/eject.d.ts +39 -0
- package/dist/collide/eject.js +142 -0
- package/dist/collide/index.d.ts +18 -0
- package/dist/collide/index.js +5 -0
- package/dist/collide/scratch.d.ts +8 -0
- package/dist/collide/scratch.js +9 -0
- package/dist/collide/segment.d.ts +12 -0
- package/dist/collide/segment.js +109 -0
- package/dist/collide/sweep.d.ts +3 -0
- package/dist/collide/sweep.js +1036 -0
- package/dist/colliderSet.d.ts +239 -0
- package/dist/colliderSet.js +546 -0
- package/dist/compoundContact.d.ts +70 -0
- package/dist/compoundContact.js +221 -0
- package/dist/controller.d.ts +351 -0
- package/dist/controller.js +641 -0
- package/dist/cylinder.d.ts +78 -0
- package/dist/cylinder.js +294 -0
- package/dist/cylinderContact.d.ts +7 -0
- package/dist/cylinderContact.js +704 -0
- package/dist/decompose.d.ts +105 -0
- package/dist/decompose.js +979 -0
- package/dist/events.d.ts +43 -0
- package/dist/events.js +95 -0
- package/dist/executor.d.ts +147 -0
- package/dist/executor.js +173 -0
- package/dist/faces.d.ts +26 -0
- package/dist/faces.js +34 -0
- package/dist/fingerprint.d.ts +61 -0
- package/dist/fingerprint.js +139 -0
- package/dist/groundMotor.d.ts +34 -0
- package/dist/groundMotor.js +41 -0
- package/dist/heightfieldShape.d.ts +128 -0
- package/dist/heightfieldShape.js +328 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.js +47 -0
- package/dist/island.d.ts +55 -0
- package/dist/island.js +227 -0
- package/dist/islandSolve.d.ts +70 -0
- package/dist/islandSolve.js +171 -0
- package/dist/islandWorker.d.ts +18 -0
- package/dist/islandWorker.js +125 -0
- package/dist/joints.d.ts +180 -0
- package/dist/joints.js +1094 -0
- package/dist/manifold.d.ts +89 -0
- package/dist/manifold.js +608 -0
- package/dist/mass.d.ts +53 -0
- package/dist/mass.js +353 -0
- package/dist/meshContact.d.ts +57 -0
- package/dist/meshContact.js +393 -0
- package/dist/meshShape.d.ts +76 -0
- package/dist/meshShape.js +175 -0
- package/dist/pairs.d.ts +62 -0
- package/dist/pairs.js +154 -0
- package/dist/query.d.ts +71 -0
- package/dist/query.js +683 -0
- package/dist/ragdoll.d.ts +135 -0
- package/dist/ragdoll.js +489 -0
- package/dist/shape.d.ts +177 -0
- package/dist/shape.js +533 -0
- package/dist/solver.d.ts +136 -0
- package/dist/solver.js +601 -0
- package/dist/stage.d.ts +167 -0
- package/dist/stage.js +350 -0
- package/dist/stallEscape.d.ts +141 -0
- package/dist/stallEscape.js +254 -0
- package/dist/sweptStep.d.ts +173 -0
- package/dist/sweptStep.js +472 -0
- package/dist/tree.d.ts +61 -0
- package/dist/tree.js +348 -0
- package/dist/vehicle.d.ts +154 -0
- package/dist/vehicle.js +343 -0
- package/dist/workerPool.d.ts +107 -0
- package/dist/workerPool.js +239 -0
- package/dist/workers.d.ts +37 -0
- package/dist/workers.js +35 -0
- package/dist/world.d.ts +298 -0
- package/dist/world.js +712 -0
- package/package.json +55 -0
- package/src/bodies.ts +369 -0
- package/src/buoyancy.ts +78 -0
- package/src/cloth.ts +734 -0
- package/src/collide/aabb.ts +28 -0
- package/src/collide/body.ts +336 -0
- package/src/collide/eject.ts +156 -0
- package/src/collide/index.ts +26 -0
- package/src/collide/scratch.ts +10 -0
- package/src/collide/segment.ts +127 -0
- package/src/collide/sweep.ts +1132 -0
- package/src/colliderSet.ts +637 -0
- package/src/compoundContact.ts +284 -0
- package/src/controller.ts +817 -0
- package/src/cylinder.ts +340 -0
- package/src/cylinderContact.ts +825 -0
- package/src/decompose.ts +1198 -0
- package/src/events.ts +108 -0
- package/src/executor.ts +224 -0
- package/src/faces.ts +37 -0
- package/src/fingerprint.ts +146 -0
- package/src/groundMotor.ts +53 -0
- package/src/heightfieldShape.ts +395 -0
- package/src/index.ts +164 -0
- package/src/island.ts +219 -0
- package/src/islandSolve.ts +218 -0
- package/src/islandWorker.ts +141 -0
- package/src/joints.ts +1367 -0
- package/src/manifold.ts +798 -0
- package/src/mass.ts +424 -0
- package/src/meshContact.ts +443 -0
- package/src/meshShape.ts +261 -0
- package/src/pairs.ts +169 -0
- package/src/query.ts +844 -0
- package/src/ragdoll.ts +662 -0
- package/src/shape.ts +656 -0
- package/src/solver.ts +873 -0
- package/src/stage.ts +406 -0
- package/src/stallEscape.ts +362 -0
- package/src/sweptStep.ts +609 -0
- package/src/tree.ts +366 -0
- package/src/vehicle.ts +489 -0
- package/src/workerPool.ts +283 -0
- package/src/workers.ts +38 -0
- package/src/world.ts +961 -0
package/dist/cloth.js
ADDED
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
import { BODY_STATIC } from './bodies.js';
|
|
2
|
+
import { LOCAL_POSE, closestOnCylinder, isCylinder, readCylinder } from './cylinder.js';
|
|
3
|
+
import { faceCount } from './faces.js';
|
|
4
|
+
export class ClothBody {
|
|
5
|
+
/** xyz-packed particle positions. */
|
|
6
|
+
position;
|
|
7
|
+
/** xyz-packed velocities. */
|
|
8
|
+
velocity;
|
|
9
|
+
/** Inverse mass per particle. Zero pins it. */
|
|
10
|
+
invMass;
|
|
11
|
+
count;
|
|
12
|
+
/** Two particle indices per constraint. */
|
|
13
|
+
link;
|
|
14
|
+
rest;
|
|
15
|
+
/** Compliance per constraint, so stretch and bend can differ. */
|
|
16
|
+
compliance;
|
|
17
|
+
previous;
|
|
18
|
+
lambda;
|
|
19
|
+
linkCount;
|
|
20
|
+
iterations;
|
|
21
|
+
damping;
|
|
22
|
+
thickness;
|
|
23
|
+
/** See `ClothOptions.coupling`. 0 is one-way, which is the default. */
|
|
24
|
+
coupling;
|
|
25
|
+
/** See `ClothOptions.particleMass`. Kilograms, read only by the coupling. */
|
|
26
|
+
particleMass;
|
|
27
|
+
/** See `ClothOptions.selfDistance`. 0 is off, which is the default. */
|
|
28
|
+
selfDistance;
|
|
29
|
+
/**
|
|
30
|
+
* Who is linked to whom, as CSR: `neighbourStart[i]` to `neighbourStart[i + 1]` indexes
|
|
31
|
+
* `neighbour`.
|
|
32
|
+
*
|
|
33
|
+
* **Built once, because the link set never changes** and the self-collision pass asks "are these
|
|
34
|
+
* two linked" once per candidate pair per tick. A `Set` of packed pairs would answer the same
|
|
35
|
+
* question with a hash probe and an allocation at construction the size of the link list; a
|
|
36
|
+
* sorted adjacency answers it with a scan over the eight or so neighbours a grid particle has,
|
|
37
|
+
* which is shorter than the probe.
|
|
38
|
+
*
|
|
39
|
+
* Empty when `selfDistance` is 0, so a sheet that does not collide with itself pays neither the
|
|
40
|
+
* memory nor the build.
|
|
41
|
+
*/
|
|
42
|
+
neighbourStart;
|
|
43
|
+
neighbour;
|
|
44
|
+
/**
|
|
45
|
+
* The spatial hash the self-collision pass rebuilds each tick: a cell list over the particles.
|
|
46
|
+
*
|
|
47
|
+
* **A hash rather than a grid, because a sheet has no bounds worth allocating for.** A curtain
|
|
48
|
+
* hanging in a room occupies a sliver of it, and a dense grid over the room's extent would be
|
|
49
|
+
* mostly empty cells rebuilt every tick. The table is sized once from the particle count and
|
|
50
|
+
* reused; `cellHead` and `cellNext` are the standard two arrays, so a rebuild is one pass that
|
|
51
|
+
* writes and allocates nothing.
|
|
52
|
+
*/
|
|
53
|
+
cellHead;
|
|
54
|
+
cellNext;
|
|
55
|
+
gravityX;
|
|
56
|
+
gravityY;
|
|
57
|
+
gravityZ;
|
|
58
|
+
constructor(positions, links, bendLinks, options = {}) {
|
|
59
|
+
this.count = positions.length / 3;
|
|
60
|
+
this.position = new Float32Array(positions);
|
|
61
|
+
this.velocity = new Float32Array(positions.length);
|
|
62
|
+
this.invMass = new Float32Array(this.count).fill(1);
|
|
63
|
+
this.previous = new Float32Array(positions.length);
|
|
64
|
+
this.iterations = Math.max(1, options.iterations ?? 8);
|
|
65
|
+
this.damping = options.damping ?? 0.02;
|
|
66
|
+
this.thickness = options.thickness ?? 0.02;
|
|
67
|
+
this.coupling = Math.min(1, Math.max(0, options.coupling ?? 0));
|
|
68
|
+
this.particleMass = Math.max(0, options.particleMass ?? 0.05);
|
|
69
|
+
this.selfDistance = Math.max(0, options.selfDistance ?? 0);
|
|
70
|
+
this.gravityX = options.gravityX ?? 0;
|
|
71
|
+
this.gravityY = options.gravityY ?? -9.81;
|
|
72
|
+
this.gravityZ = options.gravityZ ?? 0;
|
|
73
|
+
const stretch = options.stretchCompliance ?? 0;
|
|
74
|
+
const bend = options.bendCompliance ?? 1e-4;
|
|
75
|
+
const total = links.length / 2 + bendLinks.length / 2;
|
|
76
|
+
this.linkCount = total;
|
|
77
|
+
this.link = new Int32Array(total * 2);
|
|
78
|
+
this.rest = new Float32Array(total);
|
|
79
|
+
this.compliance = new Float32Array(total);
|
|
80
|
+
this.lambda = new Float32Array(total);
|
|
81
|
+
let at = 0;
|
|
82
|
+
for (const [set, value] of [
|
|
83
|
+
[links, stretch],
|
|
84
|
+
[bendLinks, bend],
|
|
85
|
+
]) {
|
|
86
|
+
for (let i = 0; i + 1 < set.length; i += 2) {
|
|
87
|
+
const a = set[i] ?? 0;
|
|
88
|
+
const b = set[i + 1] ?? 0;
|
|
89
|
+
this.link[at * 2] = a;
|
|
90
|
+
this.link[at * 2 + 1] = b;
|
|
91
|
+
this.rest[at] = distance(this.position, a, b);
|
|
92
|
+
this.compliance[at] = value;
|
|
93
|
+
at++;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/*
|
|
97
|
+
* The adjacency and the hash, built only where self-collision is on.
|
|
98
|
+
*
|
|
99
|
+
* Two counting passes rather than an array of arrays: the first counts each particle's links
|
|
100
|
+
* and the second fills them, which is the standard CSR build and allocates two typed arrays
|
|
101
|
+
* instead of one per particle.
|
|
102
|
+
*
|
|
103
|
+
* The table has one bucket a particle. A hash with as many buckets as things in it averages
|
|
104
|
+
* one occupant a bucket, and the cost of a collision is a longer scan rather than a wrong
|
|
105
|
+
* answer — the cell key is part of the comparison below, so two particles that hash together
|
|
106
|
+
* from different cells are separated by the distance test they were going to take anyway.
|
|
107
|
+
*/
|
|
108
|
+
if (this.selfDistance > 0) {
|
|
109
|
+
this.neighbourStart = new Int32Array(this.count + 1);
|
|
110
|
+
for (let k = 0; k < total; k++) {
|
|
111
|
+
this.neighbourStart[(this.link[k * 2] ?? 0) + 1]++;
|
|
112
|
+
this.neighbourStart[(this.link[k * 2 + 1] ?? 0) + 1]++;
|
|
113
|
+
}
|
|
114
|
+
for (let i = 0; i < this.count; i++) {
|
|
115
|
+
this.neighbourStart[i + 1] =
|
|
116
|
+
(this.neighbourStart[i + 1] ?? 0) + (this.neighbourStart[i] ?? 0);
|
|
117
|
+
}
|
|
118
|
+
this.neighbour = new Int32Array(total * 2);
|
|
119
|
+
const cursor = new Int32Array(this.neighbourStart.subarray(0, this.count));
|
|
120
|
+
for (let k = 0; k < total; k++) {
|
|
121
|
+
const a = this.link[k * 2] ?? 0;
|
|
122
|
+
const b = this.link[k * 2 + 1] ?? 0;
|
|
123
|
+
this.neighbour[cursor[a]++] = b;
|
|
124
|
+
this.neighbour[cursor[b]++] = a;
|
|
125
|
+
}
|
|
126
|
+
this.cellHead = new Int32Array(Math.max(1, this.count)).fill(-1);
|
|
127
|
+
this.cellNext = new Int32Array(this.count);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
this.neighbourStart = EMPTY_INDEX;
|
|
131
|
+
this.neighbour = EMPTY_INDEX;
|
|
132
|
+
this.cellHead = EMPTY_INDEX;
|
|
133
|
+
this.cellNext = EMPTY_INDEX;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Pin a particle where it is, or release it. */
|
|
137
|
+
pin(index, pinned = true) {
|
|
138
|
+
this.invMass[index] = pinned ? 0 : 1;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Advance one fixed tick.
|
|
142
|
+
*
|
|
143
|
+
* Integrate, project every constraint `iterations` times, then derive velocity from how far each
|
|
144
|
+
* particle actually moved — which is what makes it position-based: a constraint that moves a
|
|
145
|
+
* particle also, by construction, changes its velocity, so nothing has to reconcile the two.
|
|
146
|
+
*/
|
|
147
|
+
step(world, dt) {
|
|
148
|
+
const n = this.count;
|
|
149
|
+
const retain = 1 / (1 + this.damping * dt);
|
|
150
|
+
for (let i = 0; i < n; i++) {
|
|
151
|
+
const im = this.invMass[i] ?? 0;
|
|
152
|
+
this.previous[i * 3] = this.position[i * 3] ?? 0;
|
|
153
|
+
this.previous[i * 3 + 1] = this.position[i * 3 + 1] ?? 0;
|
|
154
|
+
this.previous[i * 3 + 2] = this.position[i * 3 + 2] ?? 0;
|
|
155
|
+
if (im === 0)
|
|
156
|
+
continue;
|
|
157
|
+
const vx = ((this.velocity[i * 3] ?? 0) + this.gravityX * dt) * retain;
|
|
158
|
+
const vy = ((this.velocity[i * 3 + 1] ?? 0) + this.gravityY * dt) * retain;
|
|
159
|
+
const vz = ((this.velocity[i * 3 + 2] ?? 0) + this.gravityZ * dt) * retain;
|
|
160
|
+
this.position[i * 3] = (this.position[i * 3] ?? 0) + vx * dt;
|
|
161
|
+
this.position[i * 3 + 1] = (this.position[i * 3 + 1] ?? 0) + vy * dt;
|
|
162
|
+
this.position[i * 3 + 2] = (this.position[i * 3 + 2] ?? 0) + vz * dt;
|
|
163
|
+
}
|
|
164
|
+
this.lambda.fill(0);
|
|
165
|
+
const invH2 = 1 / (dt * dt);
|
|
166
|
+
for (let pass = 0; pass < this.iterations; pass++) {
|
|
167
|
+
for (let k = 0; k < this.linkCount; k++)
|
|
168
|
+
this.project(k, invH2);
|
|
169
|
+
}
|
|
170
|
+
/*
|
|
171
|
+
* Self-collision after the constraints and before the bodies, which is the order that makes
|
|
172
|
+
* both of them mean what they say. A sheet resolved against itself *first* would then be
|
|
173
|
+
* stretched back into its own fold by the distance constraints; one resolved against the world
|
|
174
|
+
* first would be pushed back into a body by its own self-collision. The world is what the
|
|
175
|
+
* sheet may not enter under any circumstances, so it goes last.
|
|
176
|
+
*/
|
|
177
|
+
if (this.selfDistance > 0)
|
|
178
|
+
this.collideSelf();
|
|
179
|
+
if (world)
|
|
180
|
+
this.collideParticles(world, dt);
|
|
181
|
+
const invDt = 1 / dt;
|
|
182
|
+
for (let i = 0; i < n; i++) {
|
|
183
|
+
if ((this.invMass[i] ?? 0) === 0) {
|
|
184
|
+
this.velocity[i * 3] = 0;
|
|
185
|
+
this.velocity[i * 3 + 1] = 0;
|
|
186
|
+
this.velocity[i * 3 + 2] = 0;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
this.velocity[i * 3] = ((this.position[i * 3] ?? 0) - (this.previous[i * 3] ?? 0)) * invDt;
|
|
190
|
+
this.velocity[i * 3 + 1] =
|
|
191
|
+
((this.position[i * 3 + 1] ?? 0) - (this.previous[i * 3 + 1] ?? 0)) * invDt;
|
|
192
|
+
this.velocity[i * 3 + 2] =
|
|
193
|
+
((this.position[i * 3 + 2] ?? 0) - (this.previous[i * 3 + 2] ?? 0)) * invDt;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* One distance constraint, with its compliance.
|
|
198
|
+
*
|
|
199
|
+
* `α = compliance / h²` is what makes the stiffness independent of the step and of the iteration
|
|
200
|
+
* count. At zero compliance this reduces exactly to an ordinary position-based projection, which
|
|
201
|
+
* is why an inextensible sheet costs nothing extra.
|
|
202
|
+
*/
|
|
203
|
+
project(k, invH2) {
|
|
204
|
+
const a = this.link[k * 2] ?? 0;
|
|
205
|
+
const b = this.link[k * 2 + 1] ?? 0;
|
|
206
|
+
const wa = this.invMass[a] ?? 0;
|
|
207
|
+
const wb = this.invMass[b] ?? 0;
|
|
208
|
+
const w = wa + wb;
|
|
209
|
+
if (w === 0)
|
|
210
|
+
return;
|
|
211
|
+
const dx = (this.position[b * 3] ?? 0) - (this.position[a * 3] ?? 0);
|
|
212
|
+
const dy = (this.position[b * 3 + 1] ?? 0) - (this.position[a * 3 + 1] ?? 0);
|
|
213
|
+
const dz = (this.position[b * 3 + 2] ?? 0) - (this.position[a * 3 + 2] ?? 0);
|
|
214
|
+
const length = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
215
|
+
if (length < 1e-9)
|
|
216
|
+
return;
|
|
217
|
+
const c = length - (this.rest[k] ?? 0);
|
|
218
|
+
const alpha = (this.compliance[k] ?? 0) * invH2;
|
|
219
|
+
const old = this.lambda[k] ?? 0;
|
|
220
|
+
const delta = (-c - alpha * old) / (w + alpha);
|
|
221
|
+
this.lambda[k] = old + delta;
|
|
222
|
+
const nx = (dx / length) * delta;
|
|
223
|
+
const ny = (dy / length) * delta;
|
|
224
|
+
const nz = (dz / length) * delta;
|
|
225
|
+
this.position[a * 3] = (this.position[a * 3] ?? 0) - nx * wa;
|
|
226
|
+
this.position[a * 3 + 1] = (this.position[a * 3 + 1] ?? 0) - ny * wa;
|
|
227
|
+
this.position[a * 3 + 2] = (this.position[a * 3 + 2] ?? 0) - nz * wa;
|
|
228
|
+
this.position[b * 3] = (this.position[b * 3] ?? 0) + nx * wb;
|
|
229
|
+
this.position[b * 3 + 1] = (this.position[b * 3 + 1] ?? 0) + ny * wb;
|
|
230
|
+
this.position[b * 3 + 2] = (this.position[b * 3 + 2] ?? 0) + nz * wb;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Keep the sheet out of itself: a spatial hash over the particles, and a symmetric push apart.
|
|
234
|
+
*
|
|
235
|
+
* **The hash is rebuilt every tick and that is the cheap half.** One pass over the particles
|
|
236
|
+
* writing two integers each, against a structure that tried to be incremental and would have to
|
|
237
|
+
* know which particles moved — which in a cloth solver is all of them.
|
|
238
|
+
*
|
|
239
|
+
* The cell size is the self-collision distance, so a particle's neighbours are in its own cell
|
|
240
|
+
* and the twenty-six around it. **The 3x3x3 scan is what makes the answer complete**: a pair at
|
|
241
|
+
* the distance exactly cannot be in cells more than one apart, and a scan of the own cell alone
|
|
242
|
+
* would miss every pair straddling a boundary — which is most of them, since a folded sheet lies
|
|
243
|
+
* along one.
|
|
244
|
+
*
|
|
245
|
+
* **Each pair is resolved once, by index order.** Visiting both halves would apply the
|
|
246
|
+
* correction twice and turn a resting fold into a buzzing one; the `other > i` test is what
|
|
247
|
+
* makes the pass a sweep over pairs rather than over particles.
|
|
248
|
+
*
|
|
249
|
+
* A projection rather than a constraint with compliance, unlike the distance links: a contact is
|
|
250
|
+
* an inequality — it does nothing until it is violated — and XPBD's accumulated multiplier is
|
|
251
|
+
* for a constraint that holds all tick. *What that gives up* is that the stiffness of a fold is
|
|
252
|
+
* the iteration count's again, for this one term. *What would make it wrong* is a consumer
|
|
253
|
+
* wanting a fold to have a measurable resistance, at which point it needs its own multiplier
|
|
254
|
+
* array and a sign clamp on it.
|
|
255
|
+
*/
|
|
256
|
+
collideSelf() {
|
|
257
|
+
const n = this.count;
|
|
258
|
+
const radius = this.selfDistance;
|
|
259
|
+
const inverseCell = 1 / radius;
|
|
260
|
+
this.cellHead.fill(-1);
|
|
261
|
+
const buckets = this.cellHead.length;
|
|
262
|
+
for (let i = 0; i < n; i++) {
|
|
263
|
+
const key = cellKey(Math.floor((this.position[i * 3] ?? 0) * inverseCell), Math.floor((this.position[i * 3 + 1] ?? 0) * inverseCell), Math.floor((this.position[i * 3 + 2] ?? 0) * inverseCell), buckets);
|
|
264
|
+
this.cellNext[i] = this.cellHead[key] ?? -1;
|
|
265
|
+
this.cellHead[key] = i;
|
|
266
|
+
}
|
|
267
|
+
for (let i = 0; i < n; i++) {
|
|
268
|
+
const wi = this.invMass[i] ?? 0;
|
|
269
|
+
const px = this.position[i * 3] ?? 0;
|
|
270
|
+
const py = this.position[i * 3 + 1] ?? 0;
|
|
271
|
+
const pz = this.position[i * 3 + 2] ?? 0;
|
|
272
|
+
const cx = Math.floor(px * inverseCell);
|
|
273
|
+
const cy = Math.floor(py * inverseCell);
|
|
274
|
+
const cz = Math.floor(pz * inverseCell);
|
|
275
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
276
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
277
|
+
for (let dz = -1; dz <= 1; dz++) {
|
|
278
|
+
let other = this.cellHead[cellKey(cx + dx, cy + dy, cz + dz, buckets)] ?? -1;
|
|
279
|
+
while (other >= 0) {
|
|
280
|
+
/* Index order, so a pair is met once however many cells they share a bucket with. */
|
|
281
|
+
if (other > i && !this.linked(i, other)) {
|
|
282
|
+
this.separate(i, other, wi, px, py, pz, radius);
|
|
283
|
+
}
|
|
284
|
+
other = this.cellNext[other] ?? -1;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/** Whether a link joins these two. A scan over the eight or so a grid particle has. */
|
|
292
|
+
linked(a, b) {
|
|
293
|
+
const from = this.neighbourStart[a] ?? 0;
|
|
294
|
+
const to = this.neighbourStart[a + 1] ?? from;
|
|
295
|
+
for (let at = from; at < to; at++) {
|
|
296
|
+
if (this.neighbour[at] === b)
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
/** Push two particles apart to exactly `radius`, split by their inverse masses. */
|
|
302
|
+
separate(a, b, wa, ax, ay, az, radius) {
|
|
303
|
+
const wb = this.invMass[b] ?? 0;
|
|
304
|
+
const w = wa + wb;
|
|
305
|
+
if (w === 0)
|
|
306
|
+
return;
|
|
307
|
+
const dx = (this.position[b * 3] ?? 0) - ax;
|
|
308
|
+
const dy = (this.position[b * 3 + 1] ?? 0) - ay;
|
|
309
|
+
const dz = (this.position[b * 3 + 2] ?? 0) - az;
|
|
310
|
+
const squared = dx * dx + dy * dy + dz * dz;
|
|
311
|
+
if (squared >= radius * radius)
|
|
312
|
+
return;
|
|
313
|
+
const length = Math.sqrt(squared);
|
|
314
|
+
/*
|
|
315
|
+
* Two particles at exactly the same point have no direction to separate along, and picking one
|
|
316
|
+
* arbitrarily would pick the same one for every such pair — so a whole collapsed fold would
|
|
317
|
+
* open along one axis. Left alone instead: the next tick's integration moves them apart, and a
|
|
318
|
+
* pair genuinely coincident is a sheet that has already failed.
|
|
319
|
+
*/
|
|
320
|
+
if (length < 1e-9)
|
|
321
|
+
return;
|
|
322
|
+
const push = (radius - length) / length / w;
|
|
323
|
+
const mx = dx * push;
|
|
324
|
+
const my = dy * push;
|
|
325
|
+
const mz = dz * push;
|
|
326
|
+
this.position[a * 3] = ax - mx * wa;
|
|
327
|
+
this.position[a * 3 + 1] = ay - my * wa;
|
|
328
|
+
this.position[a * 3 + 2] = az - mz * wa;
|
|
329
|
+
this.position[b * 3] = (this.position[b * 3] ?? 0) + mx * wb;
|
|
330
|
+
this.position[b * 3 + 1] = (this.position[b * 3 + 1] ?? 0) + my * wb;
|
|
331
|
+
this.position[b * 3 + 2] = (this.position[b * 3 + 2] ?? 0) + mz * wb;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Push every particle out of anything it has entered, and hand the body back what it took.
|
|
335
|
+
*
|
|
336
|
+
* **The impulse is derived from the correction rather than measured**, which is what makes the
|
|
337
|
+
* exchange cost one multiply: the constraint solve has already decided where the particle would
|
|
338
|
+
* be, the push says where it must be instead, and the difference over the tick is the velocity
|
|
339
|
+
* the collision removed from it. `m·Δ/h` is that momentum, and the body gets its negative at the
|
|
340
|
+
* point of contact — a push straight down on a plank's end tips the plank, because
|
|
341
|
+
* `applyImpulse` takes the point.
|
|
342
|
+
*
|
|
343
|
+
* A static body is skipped for the impulse rather than for the push: scenery does not move, and
|
|
344
|
+
* `applyImpulse` would return without doing anything anyway.
|
|
345
|
+
*/
|
|
346
|
+
collideParticles(world, dt) {
|
|
347
|
+
const bodies = world.bodies;
|
|
348
|
+
const exchange = this.coupling > 0 ? (this.particleMass * this.coupling) / dt : 0;
|
|
349
|
+
for (let i = 0; i < this.count; i++) {
|
|
350
|
+
if ((this.invMass[i] ?? 0) === 0)
|
|
351
|
+
continue;
|
|
352
|
+
const px = this.position[i * 3] ?? 0;
|
|
353
|
+
const py = this.position[i * 3 + 1] ?? 0;
|
|
354
|
+
const pz = this.position[i * 3 + 2] ?? 0;
|
|
355
|
+
for (let b = 0; b < bodies.count; b++) {
|
|
356
|
+
const shape = bodies.shape[b];
|
|
357
|
+
if (!shape)
|
|
358
|
+
continue;
|
|
359
|
+
// A cheap reject before anything expensive: the shape's own bounding sphere.
|
|
360
|
+
const bx = bodies.posX[b] ?? 0;
|
|
361
|
+
const by = bodies.posY[b] ?? 0;
|
|
362
|
+
const bz = bodies.posZ[b] ?? 0;
|
|
363
|
+
const dx = px - bx;
|
|
364
|
+
const dy = py - by;
|
|
365
|
+
const dz = pz - bz;
|
|
366
|
+
const reach = shape.boundRadius + this.thickness;
|
|
367
|
+
if (dx * dx + dy * dy + dz * dz > reach * reach)
|
|
368
|
+
continue;
|
|
369
|
+
POSE.x = bx;
|
|
370
|
+
POSE.y = by;
|
|
371
|
+
POSE.z = bz;
|
|
372
|
+
POSE.qx = bodies.rotX[b] ?? 0;
|
|
373
|
+
POSE.qy = bodies.rotY[b] ?? 0;
|
|
374
|
+
POSE.qz = bodies.rotZ[b] ?? 0;
|
|
375
|
+
POSE.qw = bodies.rotW[b] ?? 1;
|
|
376
|
+
if (!pushOut(shape, POSE, px, py, pz, this.thickness, OUT))
|
|
377
|
+
continue;
|
|
378
|
+
this.position[i * 3] = OUT[0];
|
|
379
|
+
this.position[i * 3 + 1] = OUT[1];
|
|
380
|
+
this.position[i * 3 + 2] = OUT[2];
|
|
381
|
+
/*
|
|
382
|
+
* The other half of the exchange. Negated, because the push moved the particle *away*
|
|
383
|
+
* from the body and the body is what pushed it — Newton's third law with the tick length
|
|
384
|
+
* folded in. At the contact point rather than at the centre, so a sheet landing on one end
|
|
385
|
+
* of a plank tips it.
|
|
386
|
+
*/
|
|
387
|
+
if (exchange > 0 && bodies.type[b] !== BODY_STATIC) {
|
|
388
|
+
world.applyImpulse(b, -(OUT[0] - px) * exchange, -(OUT[1] - py) * exchange, -(OUT[2] - pz) * exchange, px, py, pz);
|
|
389
|
+
}
|
|
390
|
+
// A static body is scenery, so one push settles it; a moving one is resolved next tick.
|
|
391
|
+
if (bodies.type[b] === BODY_STATIC)
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/** What a sheet with no self-collision carries instead of four arrays it will never read. */
|
|
398
|
+
const EMPTY_INDEX = new Int32Array(0);
|
|
399
|
+
const POSE = { x: 0, y: 0, z: 0, qx: 0, qy: 0, qz: 0, qw: 1 };
|
|
400
|
+
const OUT = new Float64Array(3);
|
|
401
|
+
const LOCAL = new Float64Array(3);
|
|
402
|
+
const CYLINDER = new Float64Array(11);
|
|
403
|
+
const SURFACE = new Float64Array(7);
|
|
404
|
+
/**
|
|
405
|
+
* Move a point to the outside of a shape, if it is inside. Returns whether it moved.
|
|
406
|
+
*
|
|
407
|
+
* Works in the shape's own frame so a rotated body costs one rotation rather than a transformed
|
|
408
|
+
* copy of its geometry. A faceless shape is a sphere or a capsule and is handled by its radius; a
|
|
409
|
+
* polytope is pushed out along its least-penetrating face.
|
|
410
|
+
*/
|
|
411
|
+
function pushOut(shape, pose, px, py, pz, thickness, out) {
|
|
412
|
+
unrotate(pose, px - pose.x, py - pose.y, pz - pose.z, LOCAL);
|
|
413
|
+
const lx = LOCAL[0];
|
|
414
|
+
const ly = LOCAL[1];
|
|
415
|
+
const lz = LOCAL[2];
|
|
416
|
+
const faces = faceCount(shape);
|
|
417
|
+
if (isCylinder(shape)) {
|
|
418
|
+
/*
|
|
419
|
+
* A cylinder in its own coordinates, which is where this function already is. `closestOnCylinder`
|
|
420
|
+
* answers all four regions — beside the side, past a cap, past a rim, inside — so a particle
|
|
421
|
+
* that has entered near the rim comes out at the rim rather than being pushed through the solid
|
|
422
|
+
* along whichever of the two surfaces the code happened to test first.
|
|
423
|
+
*/
|
|
424
|
+
readCylinder(shape, LOCAL_POSE, CYLINDER);
|
|
425
|
+
closestOnCylinder(CYLINDER, lx, ly, lz, SURFACE);
|
|
426
|
+
const want = shape.radius + thickness;
|
|
427
|
+
if (SURFACE[6] >= want)
|
|
428
|
+
return false;
|
|
429
|
+
rotate(pose, SURFACE[0] + SURFACE[3] * want, SURFACE[1] + SURFACE[4] * want, SURFACE[2] + SURFACE[5] * want, out);
|
|
430
|
+
out[0] += pose.x;
|
|
431
|
+
out[1] += pose.y;
|
|
432
|
+
out[2] += pose.z;
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
if (faces === 0) {
|
|
436
|
+
// The segment the shape is built on, then its radius.
|
|
437
|
+
const count = shape.vertices.length / 3;
|
|
438
|
+
const ax = shape.vertices[0] ?? 0;
|
|
439
|
+
const ay = shape.vertices[1] ?? 0;
|
|
440
|
+
const az = shape.vertices[2] ?? 0;
|
|
441
|
+
const bx = count > 1 ? (shape.vertices[3] ?? 0) : ax;
|
|
442
|
+
const by = count > 1 ? (shape.vertices[4] ?? 0) : ay;
|
|
443
|
+
const bz = count > 1 ? (shape.vertices[5] ?? 0) : az;
|
|
444
|
+
const ex = bx - ax;
|
|
445
|
+
const ey = by - ay;
|
|
446
|
+
const ez = bz - az;
|
|
447
|
+
const ee = ex * ex + ey * ey + ez * ez;
|
|
448
|
+
let t = 0;
|
|
449
|
+
if (ee > 1e-12) {
|
|
450
|
+
t = ((lx - ax) * ex + (ly - ay) * ey + (lz - az) * ez) / ee;
|
|
451
|
+
t = t < 0 ? 0 : t > 1 ? 1 : t;
|
|
452
|
+
}
|
|
453
|
+
const cx = ax + ex * t;
|
|
454
|
+
const cy = ay + ey * t;
|
|
455
|
+
const cz = az + ez * t;
|
|
456
|
+
let nx = lx - cx;
|
|
457
|
+
let ny = ly - cy;
|
|
458
|
+
let nz = lz - cz;
|
|
459
|
+
const d = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
460
|
+
const want = shape.radius + thickness;
|
|
461
|
+
if (d >= want)
|
|
462
|
+
return false;
|
|
463
|
+
if (d < 1e-9) {
|
|
464
|
+
nx = 0;
|
|
465
|
+
ny = 1;
|
|
466
|
+
nz = 0;
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
nx /= d;
|
|
470
|
+
ny /= d;
|
|
471
|
+
nz /= d;
|
|
472
|
+
}
|
|
473
|
+
rotate(pose, cx + nx * want, cy + ny * want, cz + nz * want, out);
|
|
474
|
+
out[0] += pose.x;
|
|
475
|
+
out[1] += pose.y;
|
|
476
|
+
out[2] += pose.z;
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
let bestFace = 0;
|
|
480
|
+
let best = -Infinity;
|
|
481
|
+
for (let f = 0; f < faces; f++) {
|
|
482
|
+
const d = lx * (shape.facePlanes[f * 4] ?? 0) +
|
|
483
|
+
ly * (shape.facePlanes[f * 4 + 1] ?? 0) +
|
|
484
|
+
lz * (shape.facePlanes[f * 4 + 2] ?? 0) -
|
|
485
|
+
(shape.facePlanes[f * 4 + 3] ?? 0) -
|
|
486
|
+
shape.radius;
|
|
487
|
+
if (d > best) {
|
|
488
|
+
best = d;
|
|
489
|
+
bestFace = f;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (best >= thickness)
|
|
493
|
+
return false;
|
|
494
|
+
const move = thickness - best;
|
|
495
|
+
rotate(pose, lx + (shape.facePlanes[bestFace * 4] ?? 0) * move, ly + (shape.facePlanes[bestFace * 4 + 1] ?? 0) * move, lz + (shape.facePlanes[bestFace * 4 + 2] ?? 0) * move, out);
|
|
496
|
+
out[0] += pose.x;
|
|
497
|
+
out[1] += pose.y;
|
|
498
|
+
out[2] += pose.z;
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
function rotate(pose, x, y, z, out) {
|
|
502
|
+
const tx = 2 * (pose.qy * z - pose.qz * y);
|
|
503
|
+
const ty = 2 * (pose.qz * x - pose.qx * z);
|
|
504
|
+
const tz = 2 * (pose.qx * y - pose.qy * x);
|
|
505
|
+
out[0] = x + pose.qw * tx + (pose.qy * tz - pose.qz * ty);
|
|
506
|
+
out[1] = y + pose.qw * ty + (pose.qz * tx - pose.qx * tz);
|
|
507
|
+
out[2] = z + pose.qw * tz + (pose.qx * ty - pose.qy * tx);
|
|
508
|
+
}
|
|
509
|
+
function unrotate(pose, x, y, z, out) {
|
|
510
|
+
const qx = -pose.qx;
|
|
511
|
+
const qy = -pose.qy;
|
|
512
|
+
const qz = -pose.qz;
|
|
513
|
+
const qw = pose.qw;
|
|
514
|
+
const tx = 2 * (qy * z - qz * y);
|
|
515
|
+
const ty = 2 * (qz * x - qx * z);
|
|
516
|
+
const tz = 2 * (qx * y - qy * x);
|
|
517
|
+
out[0] = x + qw * tx + (qy * tz - qz * ty);
|
|
518
|
+
out[1] = y + qw * ty + (qz * tx - qx * tz);
|
|
519
|
+
out[2] = z + qw * tz + (qx * ty - qy * tx);
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Three cell coordinates into one bucket, by the standard three-prime mix.
|
|
523
|
+
*
|
|
524
|
+
* The primes are the ones every published spatial hash for particles uses, and the reason to take
|
|
525
|
+
* them rather than invent three is that a poor choice shows up as a *distribution* rather than as
|
|
526
|
+
* an error: buckets that fill unevenly, a scan that lengthens, and a sheet that costs more the
|
|
527
|
+
* further it is from the origin. `>>> 0` before the modulo, because a negative cell coordinate is
|
|
528
|
+
* ordinary — a sheet hanging below the origin has them — and a negative bucket index would read
|
|
529
|
+
* `undefined` out of the table and start every chain empty.
|
|
530
|
+
*/
|
|
531
|
+
function cellKey(x, y, z, buckets) {
|
|
532
|
+
return (((x * 73856093) ^ (y * 19349663) ^ (z * 83492791)) >>> 0) % buckets;
|
|
533
|
+
}
|
|
534
|
+
function distance(p, a, b) {
|
|
535
|
+
const dx = (p[b * 3] ?? 0) - (p[a * 3] ?? 0);
|
|
536
|
+
const dy = (p[b * 3 + 1] ?? 0) - (p[a * 3 + 1] ?? 0);
|
|
537
|
+
const dz = (p[b * 3 + 2] ?? 0) - (p[a * 3 + 2] ?? 0);
|
|
538
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* A rectangular sheet: positions, its stretch links, and the bend links across them.
|
|
542
|
+
*
|
|
543
|
+
* Bend links skip a particle rather than joining diagonals, because a diagonal resists *shear* and a
|
|
544
|
+
* skip resists *bowing*, and a sheet that cannot shear drapes like cardboard.
|
|
545
|
+
*
|
|
546
|
+
* **A skip-one distance constraint resists curvature and not orientation**, which is worth knowing
|
|
547
|
+
* before tuning it: three collinear particles satisfy it at any angle, so a flat sheet and a sheet
|
|
548
|
+
* hanging straight down are equally happy. Measured — a cantilever pinned along one edge settles at
|
|
549
|
+
* exactly vertical whether the bend compliance is 1e-8 or 1e6. What it *does* buy is 12.5
|
|
550
|
+
* millimetres less bow on a strip pinned at both ends with slack.
|
|
551
|
+
*
|
|
552
|
+
* **What would make it wrong** is a consumer wanting a cloak to stand out from a shoulder, and the
|
|
553
|
+
* answer then is a dihedral-angle constraint over the two triangles either side of an edge, which
|
|
554
|
+
* knows a rest angle and therefore knows the difference between flat and folded.
|
|
555
|
+
*/
|
|
556
|
+
export function makeClothGrid(columns, rows, spacing) {
|
|
557
|
+
const positions = new Float32Array(columns * rows * 3);
|
|
558
|
+
const links = [];
|
|
559
|
+
const bend = [];
|
|
560
|
+
const at = (c, r) => r * columns + c;
|
|
561
|
+
for (let r = 0; r < rows; r++) {
|
|
562
|
+
for (let c = 0; c < columns; c++) {
|
|
563
|
+
const i = at(c, r);
|
|
564
|
+
positions[i * 3] = c * spacing;
|
|
565
|
+
positions[i * 3 + 1] = 0;
|
|
566
|
+
positions[i * 3 + 2] = r * spacing;
|
|
567
|
+
if (c + 1 < columns)
|
|
568
|
+
links.push(i, at(c + 1, r));
|
|
569
|
+
if (r + 1 < rows)
|
|
570
|
+
links.push(i, at(c, r + 1));
|
|
571
|
+
// Shear, which a grid needs or it folds flat along a diagonal.
|
|
572
|
+
if (c + 1 < columns && r + 1 < rows)
|
|
573
|
+
links.push(i, at(c + 1, r + 1));
|
|
574
|
+
if (c + 1 < columns && r + 1 < rows)
|
|
575
|
+
links.push(at(c + 1, r), at(c, r + 1));
|
|
576
|
+
if (c + 2 < columns)
|
|
577
|
+
bend.push(i, at(c + 2, r));
|
|
578
|
+
if (r + 2 < rows)
|
|
579
|
+
bend.push(i, at(c, r + 2));
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return {
|
|
583
|
+
positions,
|
|
584
|
+
links: new Int32Array(links),
|
|
585
|
+
bendLinks: new Int32Array(bend),
|
|
586
|
+
};
|
|
587
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** An axis-aligned box, and the one operation that builds one. */
|
|
2
|
+
export interface Aabb {
|
|
3
|
+
minX: number;
|
|
4
|
+
minY: number;
|
|
5
|
+
minZ: number;
|
|
6
|
+
maxX: number;
|
|
7
|
+
maxY: number;
|
|
8
|
+
maxZ: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function aabbFromCenter(cx: number, cy: number, cz: number, hx: number, hy: number, hz: number): Aabb;
|