@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/bodies.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { combineMassProperties, createMassProperties, shapeMassProperties } from './mass.js';
|
|
2
|
+
/**
|
|
3
|
+
* Rigid body state, structure-of-arrays, indexed by a dense body index.
|
|
4
|
+
*
|
|
5
|
+
* **`Float32Array` for state and doubles for every intermediate.** A store rounds through
|
|
6
|
+
* `Math.fround`, which ECMAScript specifies exactly, while the arithmetic between stores stays in
|
|
7
|
+
* doubles — so the solver computes at higher precision than it keeps and halves its memory
|
|
8
|
+
* bandwidth. Both halves of that are deliberate, and JavaScript has no SIMD to spend the bandwidth
|
|
9
|
+
* on instead.
|
|
10
|
+
*
|
|
11
|
+
* **Indices are dense and removal swaps the last body in**, so a caller holding an index must be
|
|
12
|
+
* told when one moves. `remove` returns the index that moved, or −1, and every structure keyed by
|
|
13
|
+
* body index has to act on it.
|
|
14
|
+
*/
|
|
15
|
+
export const BODY_STATIC = 0;
|
|
16
|
+
export const BODY_KINEMATIC = 1;
|
|
17
|
+
export const BODY_DYNAMIC = 2;
|
|
18
|
+
/** Set on a body whose island is asleep. Woken bodies clear it together. */
|
|
19
|
+
export const BODY_SLEEPING = 1;
|
|
20
|
+
/**
|
|
21
|
+
* A sensor reports overlap and resolves none of it.
|
|
22
|
+
*
|
|
23
|
+
* Narrowphase still runs — an overlap that is never computed cannot be reported — and the solver is
|
|
24
|
+
* what skips it. **What this costs** is a sensor being as dear as a solid body in the broadphase and
|
|
25
|
+
* narrowphase, which is where most of the cost is anyway.
|
|
26
|
+
*/
|
|
27
|
+
export const BODY_SENSOR = 2;
|
|
28
|
+
const SCRATCH_MASS = createMassProperties();
|
|
29
|
+
export class BodySet {
|
|
30
|
+
count = 0;
|
|
31
|
+
posX;
|
|
32
|
+
posY;
|
|
33
|
+
posZ;
|
|
34
|
+
rotX;
|
|
35
|
+
rotY;
|
|
36
|
+
rotZ;
|
|
37
|
+
rotW;
|
|
38
|
+
velX;
|
|
39
|
+
velY;
|
|
40
|
+
velZ;
|
|
41
|
+
angX;
|
|
42
|
+
angY;
|
|
43
|
+
angZ;
|
|
44
|
+
invMass;
|
|
45
|
+
/** Body-frame inverse inertia, six symmetric terms per body: xx, yy, zz, xy, xz, yz. */
|
|
46
|
+
invInertia;
|
|
47
|
+
/** Centre of mass in body frame, which a shape built off-centre has. */
|
|
48
|
+
comX;
|
|
49
|
+
comY;
|
|
50
|
+
comZ;
|
|
51
|
+
friction;
|
|
52
|
+
restitution;
|
|
53
|
+
type;
|
|
54
|
+
flags;
|
|
55
|
+
sleepTicks;
|
|
56
|
+
layer;
|
|
57
|
+
mask;
|
|
58
|
+
shape = [];
|
|
59
|
+
/** The parts of a compound body, or undefined where it has a single `shape`. */
|
|
60
|
+
shapes = [];
|
|
61
|
+
capacity;
|
|
62
|
+
constructor(capacity = 64) {
|
|
63
|
+
this.capacity = Math.max(1, capacity | 0);
|
|
64
|
+
const f = (n = 1) => new Float32Array(this.capacity * n);
|
|
65
|
+
this.posX = f();
|
|
66
|
+
this.posY = f();
|
|
67
|
+
this.posZ = f();
|
|
68
|
+
this.rotX = f();
|
|
69
|
+
this.rotY = f();
|
|
70
|
+
this.rotZ = f();
|
|
71
|
+
this.rotW = f();
|
|
72
|
+
this.velX = f();
|
|
73
|
+
this.velY = f();
|
|
74
|
+
this.velZ = f();
|
|
75
|
+
this.angX = f();
|
|
76
|
+
this.angY = f();
|
|
77
|
+
this.angZ = f();
|
|
78
|
+
this.invMass = f();
|
|
79
|
+
this.invInertia = f(6);
|
|
80
|
+
this.comX = f();
|
|
81
|
+
this.comY = f();
|
|
82
|
+
this.comZ = f();
|
|
83
|
+
this.friction = f();
|
|
84
|
+
this.restitution = f();
|
|
85
|
+
this.type = new Uint8Array(this.capacity);
|
|
86
|
+
this.flags = new Uint8Array(this.capacity);
|
|
87
|
+
this.sleepTicks = new Uint16Array(this.capacity);
|
|
88
|
+
this.layer = new Uint32Array(this.capacity);
|
|
89
|
+
this.mask = new Uint32Array(this.capacity);
|
|
90
|
+
}
|
|
91
|
+
add(desc) {
|
|
92
|
+
if (this.count === this.capacity)
|
|
93
|
+
this.grow();
|
|
94
|
+
const i = this.count++;
|
|
95
|
+
this.posX[i] = desc.x ?? 0;
|
|
96
|
+
this.posY[i] = desc.y ?? 0;
|
|
97
|
+
this.posZ[i] = desc.z ?? 0;
|
|
98
|
+
this.rotX[i] = desc.qx ?? 0;
|
|
99
|
+
this.rotY[i] = desc.qy ?? 0;
|
|
100
|
+
this.rotZ[i] = desc.qz ?? 0;
|
|
101
|
+
this.rotW[i] = desc.qw ?? 1;
|
|
102
|
+
this.velX[i] = 0;
|
|
103
|
+
this.velY[i] = 0;
|
|
104
|
+
this.velZ[i] = 0;
|
|
105
|
+
this.angX[i] = 0;
|
|
106
|
+
this.angY[i] = 0;
|
|
107
|
+
this.angZ[i] = 0;
|
|
108
|
+
this.type[i] = desc.type;
|
|
109
|
+
this.flags[i] = desc.sensor ? BODY_SENSOR : 0;
|
|
110
|
+
this.sleepTicks[i] = 0;
|
|
111
|
+
this.friction[i] = desc.friction ?? 0.5;
|
|
112
|
+
this.restitution[i] = desc.restitution ?? 0;
|
|
113
|
+
this.layer[i] = desc.layer ?? 1;
|
|
114
|
+
this.mask[i] = desc.mask ?? 0xffffffff;
|
|
115
|
+
this.shape[i] = desc.shape;
|
|
116
|
+
this.shapes[i] = desc.shapes;
|
|
117
|
+
/*
|
|
118
|
+
* A static or kinematic body has zero inverse mass and zero inverse inertia, which is what
|
|
119
|
+
* makes it immovable *arithmetically* rather than by a branch in the solver. Every constraint
|
|
120
|
+
* then treats it uniformly and the hot loop has one fewer test in it.
|
|
121
|
+
*/
|
|
122
|
+
if (desc.type === BODY_DYNAMIC) {
|
|
123
|
+
const density = desc.density ?? 1000;
|
|
124
|
+
const m = desc.shapes
|
|
125
|
+
? combineMassProperties(desc.shapes, density, SCRATCH_MASS)
|
|
126
|
+
: shapeMassProperties(desc.shape, density, SCRATCH_MASS);
|
|
127
|
+
const mass = m.volume * density;
|
|
128
|
+
this.invMass[i] = mass > 0 ? 1 / mass : 0;
|
|
129
|
+
this.comX[i] = m.comX;
|
|
130
|
+
this.comY[i] = m.comY;
|
|
131
|
+
this.comZ[i] = m.comZ;
|
|
132
|
+
invertSymmetric(m.ixx, m.iyy, m.izz, m.ixy, m.ixz, m.iyz, this.invInertia, i * 6);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
this.invMass[i] = 0;
|
|
136
|
+
this.comX[i] = 0;
|
|
137
|
+
this.comY[i] = 0;
|
|
138
|
+
this.comZ[i] = 0;
|
|
139
|
+
for (let k = 0; k < 6; k++)
|
|
140
|
+
this.invInertia[i * 6 + k] = 0;
|
|
141
|
+
}
|
|
142
|
+
return i;
|
|
143
|
+
}
|
|
144
|
+
/** Swap-remove. Returns the index that moved into `index`, or −1 if it was the last. */
|
|
145
|
+
remove(index) {
|
|
146
|
+
const last = --this.count;
|
|
147
|
+
if (index === last) {
|
|
148
|
+
this.shape[index] = undefined;
|
|
149
|
+
this.shapes[index] = undefined;
|
|
150
|
+
return -1;
|
|
151
|
+
}
|
|
152
|
+
const copy = (a, stride = 1) => {
|
|
153
|
+
for (let k = 0; k < stride; k++)
|
|
154
|
+
a[index * stride + k] = a[last * stride + k] ?? 0;
|
|
155
|
+
};
|
|
156
|
+
for (const a of [
|
|
157
|
+
this.posX,
|
|
158
|
+
this.posY,
|
|
159
|
+
this.posZ,
|
|
160
|
+
this.rotX,
|
|
161
|
+
this.rotY,
|
|
162
|
+
this.rotZ,
|
|
163
|
+
this.rotW,
|
|
164
|
+
this.velX,
|
|
165
|
+
this.velY,
|
|
166
|
+
this.velZ,
|
|
167
|
+
this.angX,
|
|
168
|
+
this.angY,
|
|
169
|
+
this.angZ,
|
|
170
|
+
this.invMass,
|
|
171
|
+
this.comX,
|
|
172
|
+
this.comY,
|
|
173
|
+
this.comZ,
|
|
174
|
+
this.friction,
|
|
175
|
+
this.restitution,
|
|
176
|
+
])
|
|
177
|
+
copy(a);
|
|
178
|
+
copy(this.invInertia, 6);
|
|
179
|
+
copy(this.type);
|
|
180
|
+
copy(this.flags);
|
|
181
|
+
copy(this.sleepTicks);
|
|
182
|
+
copy(this.layer);
|
|
183
|
+
copy(this.mask);
|
|
184
|
+
this.shape[index] = this.shape[last];
|
|
185
|
+
this.shape[last] = undefined;
|
|
186
|
+
this.shapes[index] = this.shapes[last];
|
|
187
|
+
this.shapes[last] = undefined;
|
|
188
|
+
return last;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Fill `out` with the world-frame inverse inertia, six symmetric terms.
|
|
192
|
+
*
|
|
193
|
+
* `R · I⁻¹ · Rᵀ`, a congruence, rather than a diagonalisation. Storing three principal moments
|
|
194
|
+
* would need an eigen decomposition, and Jacobi is iterative — a fixed sweep count would be
|
|
195
|
+
* deterministic but is one more thing to get exactly right for no gain. **What this gives up** is
|
|
196
|
+
* six floats per body instead of three and a congruence per substep. **What would make it wrong**
|
|
197
|
+
* is a measurement showing that congruence dominates the tick.
|
|
198
|
+
*/
|
|
199
|
+
worldInverseInertia(index, out, at = 0) {
|
|
200
|
+
const x = this.rotX[index] ?? 0;
|
|
201
|
+
const y = this.rotY[index] ?? 0;
|
|
202
|
+
const z = this.rotZ[index] ?? 0;
|
|
203
|
+
const w = this.rotW[index] ?? 1;
|
|
204
|
+
// Rotation matrix, row-major.
|
|
205
|
+
const r00 = 1 - 2 * (y * y + z * z);
|
|
206
|
+
const r01 = 2 * (x * y - z * w);
|
|
207
|
+
const r02 = 2 * (x * z + y * w);
|
|
208
|
+
const r10 = 2 * (x * y + z * w);
|
|
209
|
+
const r11 = 1 - 2 * (x * x + z * z);
|
|
210
|
+
const r12 = 2 * (y * z - x * w);
|
|
211
|
+
const r20 = 2 * (x * z - y * w);
|
|
212
|
+
const r21 = 2 * (y * z + x * w);
|
|
213
|
+
const r22 = 1 - 2 * (x * x + y * y);
|
|
214
|
+
const b = index * 6;
|
|
215
|
+
const mxx = this.invInertia[b] ?? 0;
|
|
216
|
+
const myy = this.invInertia[b + 1] ?? 0;
|
|
217
|
+
const mzz = this.invInertia[b + 2] ?? 0;
|
|
218
|
+
const mxy = this.invInertia[b + 3] ?? 0;
|
|
219
|
+
const mxz = this.invInertia[b + 4] ?? 0;
|
|
220
|
+
const myz = this.invInertia[b + 5] ?? 0;
|
|
221
|
+
// T = R · M, then out = T · Rᵀ, keeping only the six symmetric terms.
|
|
222
|
+
const t00 = r00 * mxx + r01 * mxy + r02 * mxz;
|
|
223
|
+
const t01 = r00 * mxy + r01 * myy + r02 * myz;
|
|
224
|
+
const t02 = r00 * mxz + r01 * myz + r02 * mzz;
|
|
225
|
+
const t10 = r10 * mxx + r11 * mxy + r12 * mxz;
|
|
226
|
+
const t11 = r10 * mxy + r11 * myy + r12 * myz;
|
|
227
|
+
const t12 = r10 * mxz + r11 * myz + r12 * mzz;
|
|
228
|
+
const t20 = r20 * mxx + r21 * mxy + r22 * mxz;
|
|
229
|
+
const t21 = r20 * mxy + r21 * myy + r22 * myz;
|
|
230
|
+
const t22 = r20 * mxz + r21 * myz + r22 * mzz;
|
|
231
|
+
out[at] = t00 * r00 + t01 * r01 + t02 * r02;
|
|
232
|
+
out[at + 1] = t10 * r10 + t11 * r11 + t12 * r12;
|
|
233
|
+
out[at + 2] = t20 * r20 + t21 * r21 + t22 * r22;
|
|
234
|
+
out[at + 3] = t00 * r10 + t01 * r11 + t02 * r12;
|
|
235
|
+
out[at + 4] = t00 * r20 + t01 * r21 + t02 * r22;
|
|
236
|
+
out[at + 5] = t10 * r20 + t11 * r21 + t12 * r22;
|
|
237
|
+
}
|
|
238
|
+
grow() {
|
|
239
|
+
const next = this.capacity * 2;
|
|
240
|
+
const wider = (a, stride = 1) => {
|
|
241
|
+
const out = new a.constructor(next * stride);
|
|
242
|
+
out.set(a);
|
|
243
|
+
return out;
|
|
244
|
+
};
|
|
245
|
+
this.posX = wider(this.posX);
|
|
246
|
+
this.posY = wider(this.posY);
|
|
247
|
+
this.posZ = wider(this.posZ);
|
|
248
|
+
this.rotX = wider(this.rotX);
|
|
249
|
+
this.rotY = wider(this.rotY);
|
|
250
|
+
this.rotZ = wider(this.rotZ);
|
|
251
|
+
this.rotW = wider(this.rotW);
|
|
252
|
+
this.velX = wider(this.velX);
|
|
253
|
+
this.velY = wider(this.velY);
|
|
254
|
+
this.velZ = wider(this.velZ);
|
|
255
|
+
this.angX = wider(this.angX);
|
|
256
|
+
this.angY = wider(this.angY);
|
|
257
|
+
this.angZ = wider(this.angZ);
|
|
258
|
+
this.invMass = wider(this.invMass);
|
|
259
|
+
this.invInertia = wider(this.invInertia, 6);
|
|
260
|
+
this.comX = wider(this.comX);
|
|
261
|
+
this.comY = wider(this.comY);
|
|
262
|
+
this.comZ = wider(this.comZ);
|
|
263
|
+
this.friction = wider(this.friction);
|
|
264
|
+
this.restitution = wider(this.restitution);
|
|
265
|
+
this.type = wider(this.type);
|
|
266
|
+
this.flags = wider(this.flags);
|
|
267
|
+
this.sleepTicks = wider(this.sleepTicks);
|
|
268
|
+
this.layer = wider(this.layer);
|
|
269
|
+
this.mask = wider(this.mask);
|
|
270
|
+
this.capacity = next;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Invert a symmetric 3×3 in closed form, writing six terms.
|
|
275
|
+
*
|
|
276
|
+
* A singular tensor — a shape with no volume — inverts to zero rather than to infinity, which makes
|
|
277
|
+
* it behave as a point mass instead of producing NaN in every constraint that touches it. An
|
|
278
|
+
* unwritten uniform is zero and zero is a real value, which is the failure this engine has recorded
|
|
279
|
+
* twice; here zero is the *correct* value and the comment is what says so.
|
|
280
|
+
*/
|
|
281
|
+
function invertSymmetric(xx, yy, zz, xy, xz, yz, out, at) {
|
|
282
|
+
const c00 = yy * zz - yz * yz;
|
|
283
|
+
const c01 = xz * yz - xy * zz;
|
|
284
|
+
const c02 = xy * yz - xz * yy;
|
|
285
|
+
const det = xx * c00 + xy * c01 + xz * c02;
|
|
286
|
+
if (det === 0) {
|
|
287
|
+
for (let k = 0; k < 6; k++)
|
|
288
|
+
out[at + k] = 0;
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const inv = 1 / det;
|
|
292
|
+
out[at] = c00 * inv;
|
|
293
|
+
out[at + 1] = (xx * zz - xz * xz) * inv;
|
|
294
|
+
out[at + 2] = (xx * yy - xy * xy) * inv;
|
|
295
|
+
out[at + 3] = c01 * inv;
|
|
296
|
+
out[at + 4] = c02 * inv;
|
|
297
|
+
out[at + 5] = (xy * xz - xx * yz) * inv;
|
|
298
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What water does to a body that falls in.
|
|
3
|
+
*
|
|
4
|
+
* **Two things, and both are about momentum rather than about floating.** A kinematic body has no
|
|
5
|
+
* mass to displace, so there is no buoyant force to compute — what a consumer actually wants is
|
|
6
|
+
* that falling in *costs* something, and that the drop settles instead of accelerating into the
|
|
7
|
+
* seabed.
|
|
8
|
+
*
|
|
9
|
+
* **Drag on every axis, not only the vertical one.** Killing horizontal momentum is the whole
|
|
10
|
+
* reason falling in is a setback rather than a shortcut: a body that kept its speed underwater
|
|
11
|
+
* would be taking a faster line.
|
|
12
|
+
*
|
|
13
|
+
* Zero game nouns, which is why it is here. What stays with a consumer is where the water is.
|
|
14
|
+
*/
|
|
15
|
+
export interface BuoyancyOptions {
|
|
16
|
+
/**
|
|
17
|
+
* The surface height. Default −Infinity, which is a world with no water in it.
|
|
18
|
+
*
|
|
19
|
+
* A consumer that has not asked reaches none of this and pays for none of it.
|
|
20
|
+
*/
|
|
21
|
+
level?: number;
|
|
22
|
+
/** How much of a body's speed a second of water takes, as a fraction. */
|
|
23
|
+
drag?: number;
|
|
24
|
+
/** The fastest a submerged body sinks, metres a second. Negative is downward. */
|
|
25
|
+
sinkSpeed?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface Buoyancy {
|
|
28
|
+
readonly level: number;
|
|
29
|
+
readonly drag: number;
|
|
30
|
+
readonly sinkSpeed: number;
|
|
31
|
+
}
|
|
32
|
+
/** Anything with a velocity. Declared structurally so nothing is imported in either direction. */
|
|
33
|
+
export interface BuoyantState {
|
|
34
|
+
velX: number;
|
|
35
|
+
velY: number;
|
|
36
|
+
velZ: number;
|
|
37
|
+
}
|
|
38
|
+
export declare function createBuoyancy(options?: BuoyancyOptions): Buoyancy;
|
|
39
|
+
/**
|
|
40
|
+
* Apply it, and report whether the body is submerged.
|
|
41
|
+
*
|
|
42
|
+
* @param y Where the body is. Under `level` is submerged.
|
|
43
|
+
* @returns Whether it was, so a consumer can count the time or drown somebody — which is a game's
|
|
44
|
+
* decision and not this function's.
|
|
45
|
+
*/
|
|
46
|
+
export declare function applyBuoyancy(water: Buoyancy, y: number, state: BuoyantState, dt: number): boolean;
|
package/dist/buoyancy.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What water does to a body that falls in.
|
|
3
|
+
*
|
|
4
|
+
* **Two things, and both are about momentum rather than about floating.** A kinematic body has no
|
|
5
|
+
* mass to displace, so there is no buoyant force to compute — what a consumer actually wants is
|
|
6
|
+
* that falling in *costs* something, and that the drop settles instead of accelerating into the
|
|
7
|
+
* seabed.
|
|
8
|
+
*
|
|
9
|
+
* **Drag on every axis, not only the vertical one.** Killing horizontal momentum is the whole
|
|
10
|
+
* reason falling in is a setback rather than a shortcut: a body that kept its speed underwater
|
|
11
|
+
* would be taking a faster line.
|
|
12
|
+
*
|
|
13
|
+
* Zero game nouns, which is why it is here. What stays with a consumer is where the water is.
|
|
14
|
+
*/
|
|
15
|
+
export function createBuoyancy(options = {}) {
|
|
16
|
+
return {
|
|
17
|
+
level: options.level ?? -Infinity,
|
|
18
|
+
drag: options.drag ?? 0,
|
|
19
|
+
sinkSpeed: options.sinkSpeed ?? 0,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Apply it, and report whether the body is submerged.
|
|
24
|
+
*
|
|
25
|
+
* @param y Where the body is. Under `level` is submerged.
|
|
26
|
+
* @returns Whether it was, so a consumer can count the time or drown somebody — which is a game's
|
|
27
|
+
* decision and not this function's.
|
|
28
|
+
*/
|
|
29
|
+
export function applyBuoyancy(water, y, state, dt) {
|
|
30
|
+
if (y >= water.level)
|
|
31
|
+
return false;
|
|
32
|
+
/*
|
|
33
|
+
* Clamped at zero: `1 − drag·dt` goes negative past `dt = 1/drag`, and a negative factor does
|
|
34
|
+
* not slow a body, it throws it backwards. The worst a long step can do is stop it dead.
|
|
35
|
+
*/
|
|
36
|
+
const kept = Math.max(0, 1 - water.drag * dt);
|
|
37
|
+
state.velX *= kept;
|
|
38
|
+
state.velY *= kept;
|
|
39
|
+
state.velZ *= kept;
|
|
40
|
+
/* A floor on the fall rather than a speed it is driven to: a body drifting down slower than
|
|
41
|
+
terminal is left alone, and one falling faster is settled to it. */
|
|
42
|
+
if (state.velY < water.sinkSpeed)
|
|
43
|
+
state.velY = water.sinkSpeed;
|
|
44
|
+
return true;
|
|
45
|
+
}
|
package/dist/cloth.d.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import type { PhysicsWorld } from './world.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Cloth, as position-based dynamics with compliance.
|
|
4
|
+
*
|
|
5
|
+
* **A separate solver from the rigid bodies, deliberately, and it is §5's decision in a different
|
|
6
|
+
* place.** Thousands of rotationless particles are the wrong data layout for a rigid constraint
|
|
7
|
+
* graph: every particle would carry an orientation it never uses and an inertia tensor that is
|
|
8
|
+
* always zero. Two solvers with two jobs, not two implementations of one decision.
|
|
9
|
+
*
|
|
10
|
+
* **Compliance rather than a stiffness constant**, for the same reason §7 states contact stiffness
|
|
11
|
+
* as a frequency: `α = compliance / h²` makes a constraint's stiffness independent of the step
|
|
12
|
+
* length, so the iteration count is a quality dial rather than a behaviour change. A raw
|
|
13
|
+
* position-based solver's stiffness is a function of how many times you iterated, which means
|
|
14
|
+
* tuning it is tuning the solver rather than the cloth.
|
|
15
|
+
*
|
|
16
|
+
* **Two-way coupled since 2026-08-27, by an impulse exchange rather than by a merged island.** The
|
|
17
|
+
* refusal this replaces was right about the price and wrong about the only way to pay it. Putting
|
|
18
|
+
* particles into the rigid solver's constraint graph *would* merge islands — a character in a cloak
|
|
19
|
+
* becomes one island with everything the cloak brushes, and merged islands are what §8a's
|
|
20
|
+
* parallelism is made of. So that is still refused. What ships instead is the exchange the two
|
|
21
|
+
* solvers can make **without either learning about the other's structure**: the collision pass
|
|
22
|
+
* already moves a particle out of a body, and a particle moved by `d` in a tick of `h` has had its
|
|
23
|
+
* momentum changed by `m·d/h`; the body receives the opposite of that at the contact point, through
|
|
24
|
+
* the same `applyImpulse` any consumer would call.
|
|
25
|
+
*
|
|
26
|
+
* **What that costs, said plainly.** The exchange is *explicit* — the body responds on the next
|
|
27
|
+
* tick rather than in the same solve — so it is a spring with a one-tick delay in it, and a very
|
|
28
|
+
* heavy sheet landing on a very light body can oscillate. `coupling` is a scale from 0 to 1 and
|
|
29
|
+
* **0 is the default**, so no existing consumer's world moves by a millimetre until it asks. *What
|
|
30
|
+
* would reverse the remaining refusal:* a consumer who needs the sheet and the body to converge
|
|
31
|
+
* together in one solve, at which point the price is the island structure and it is a track rather
|
|
32
|
+
* than a field.
|
|
33
|
+
*/
|
|
34
|
+
export interface ClothOptions {
|
|
35
|
+
/** Metres of stretch per newton. Zero is inextensible; larger is stretchier. */
|
|
36
|
+
stretchCompliance?: number;
|
|
37
|
+
/** The same, for the constraints that resist folding. */
|
|
38
|
+
bendCompliance?: number;
|
|
39
|
+
/** Solver passes per tick. A quality dial: compliance keeps stiffness meaningful across it. */
|
|
40
|
+
iterations?: number;
|
|
41
|
+
/**
|
|
42
|
+
* How close two unconnected particles may come before they push each other apart, in metres.
|
|
43
|
+
*
|
|
44
|
+
* **0 is off and is the default**, because self-collision is the half of cloth whose *tuning*
|
|
45
|
+
* decides whether it ships: a sheet that resists itself at a distance larger than its own
|
|
46
|
+
* spacing cannot lie flat, and one that resists at a distance far smaller than a fold's
|
|
47
|
+
* thickness passes through itself anyway. A consumer sets this from the sheet they built, and
|
|
48
|
+
* the honest starting point is **a little under the grid spacing** — `makeClothGrid` at 5 cm
|
|
49
|
+
* wants about 3 to 4 cm.
|
|
50
|
+
*
|
|
51
|
+
* **Particles joined by a constraint are exempt**, and that is not an optimisation. A stretch
|
|
52
|
+
* link's rest length is the spacing, so a self-collision distance anywhere near it would have
|
|
53
|
+
* every neighbour pair fighting its own link at every step — the sheet would inflate rather than
|
|
54
|
+
* drape, which is the failure that reads as a solver bug. `ClothBody` builds the adjacency once
|
|
55
|
+
* at construction for exactly this.
|
|
56
|
+
*/
|
|
57
|
+
selfDistance?: number;
|
|
58
|
+
/** Per-second velocity retained, as a fraction. */
|
|
59
|
+
damping?: number;
|
|
60
|
+
/** How far a particle is held off a surface it meets. */
|
|
61
|
+
thickness?: number;
|
|
62
|
+
/**
|
|
63
|
+
* How much of the momentum a pushed particle gave up is handed back to the body, 0 to 1.
|
|
64
|
+
*
|
|
65
|
+
* **0 by default, which is one-way coupling and is what every consumer had before this existed.**
|
|
66
|
+
* 1 is the whole exchange; anything between is a sheet that pushes less than its mass says it
|
|
67
|
+
* should, which is the dial a consumer reaches for when the delay in the exchange makes a light
|
|
68
|
+
* body jitter.
|
|
69
|
+
*/
|
|
70
|
+
coupling?: number;
|
|
71
|
+
/**
|
|
72
|
+
* What one particle weighs, in kilograms. Only the coupling reads it.
|
|
73
|
+
*
|
|
74
|
+
* **Separate from `invMass`, and the separation is deliberate.** That array is a *relative
|
|
75
|
+
* weight* in the position solve — every free particle is 1 and a pinned one is 0 — and the
|
|
76
|
+
* compliance is tuned against it, so redefining it as an inverse mass in kilograms would move
|
|
77
|
+
* every sheet any consumer has ever tuned. The coupling needs kilograms and nothing else does,
|
|
78
|
+
* so it takes them here.
|
|
79
|
+
*
|
|
80
|
+
* The default is fifty grams: a square metre of ordinary cloth is two to three hundred grams,
|
|
81
|
+
* and `makeClothGrid` at a five-centimetre spacing puts four hundred particles in that square
|
|
82
|
+
* metre. A consumer building a tarpaulin or a chainmail cape sets it.
|
|
83
|
+
*/
|
|
84
|
+
particleMass?: number;
|
|
85
|
+
gravityX?: number;
|
|
86
|
+
gravityY?: number;
|
|
87
|
+
gravityZ?: number;
|
|
88
|
+
}
|
|
89
|
+
export declare class ClothBody {
|
|
90
|
+
/** xyz-packed particle positions. */
|
|
91
|
+
readonly position: Float32Array;
|
|
92
|
+
/** xyz-packed velocities. */
|
|
93
|
+
readonly velocity: Float32Array;
|
|
94
|
+
/** Inverse mass per particle. Zero pins it. */
|
|
95
|
+
readonly invMass: Float32Array;
|
|
96
|
+
readonly count: number;
|
|
97
|
+
/** Two particle indices per constraint. */
|
|
98
|
+
private readonly link;
|
|
99
|
+
private readonly rest;
|
|
100
|
+
/** Compliance per constraint, so stretch and bend can differ. */
|
|
101
|
+
private readonly compliance;
|
|
102
|
+
private readonly previous;
|
|
103
|
+
private readonly lambda;
|
|
104
|
+
private readonly linkCount;
|
|
105
|
+
readonly iterations: number;
|
|
106
|
+
readonly damping: number;
|
|
107
|
+
readonly thickness: number;
|
|
108
|
+
/** See `ClothOptions.coupling`. 0 is one-way, which is the default. */
|
|
109
|
+
readonly coupling: number;
|
|
110
|
+
/** See `ClothOptions.particleMass`. Kilograms, read only by the coupling. */
|
|
111
|
+
readonly particleMass: number;
|
|
112
|
+
/** See `ClothOptions.selfDistance`. 0 is off, which is the default. */
|
|
113
|
+
readonly selfDistance: number;
|
|
114
|
+
/**
|
|
115
|
+
* Who is linked to whom, as CSR: `neighbourStart[i]` to `neighbourStart[i + 1]` indexes
|
|
116
|
+
* `neighbour`.
|
|
117
|
+
*
|
|
118
|
+
* **Built once, because the link set never changes** and the self-collision pass asks "are these
|
|
119
|
+
* two linked" once per candidate pair per tick. A `Set` of packed pairs would answer the same
|
|
120
|
+
* question with a hash probe and an allocation at construction the size of the link list; a
|
|
121
|
+
* sorted adjacency answers it with a scan over the eight or so neighbours a grid particle has,
|
|
122
|
+
* which is shorter than the probe.
|
|
123
|
+
*
|
|
124
|
+
* Empty when `selfDistance` is 0, so a sheet that does not collide with itself pays neither the
|
|
125
|
+
* memory nor the build.
|
|
126
|
+
*/
|
|
127
|
+
private readonly neighbourStart;
|
|
128
|
+
private readonly neighbour;
|
|
129
|
+
/**
|
|
130
|
+
* The spatial hash the self-collision pass rebuilds each tick: a cell list over the particles.
|
|
131
|
+
*
|
|
132
|
+
* **A hash rather than a grid, because a sheet has no bounds worth allocating for.** A curtain
|
|
133
|
+
* hanging in a room occupies a sliver of it, and a dense grid over the room's extent would be
|
|
134
|
+
* mostly empty cells rebuilt every tick. The table is sized once from the particle count and
|
|
135
|
+
* reused; `cellHead` and `cellNext` are the standard two arrays, so a rebuild is one pass that
|
|
136
|
+
* writes and allocates nothing.
|
|
137
|
+
*/
|
|
138
|
+
private readonly cellHead;
|
|
139
|
+
private readonly cellNext;
|
|
140
|
+
gravityX: number;
|
|
141
|
+
gravityY: number;
|
|
142
|
+
gravityZ: number;
|
|
143
|
+
constructor(positions: Float32Array, links: Int32Array, bendLinks: Int32Array, options?: ClothOptions);
|
|
144
|
+
/** Pin a particle where it is, or release it. */
|
|
145
|
+
pin(index: number, pinned?: boolean): void;
|
|
146
|
+
/**
|
|
147
|
+
* Advance one fixed tick.
|
|
148
|
+
*
|
|
149
|
+
* Integrate, project every constraint `iterations` times, then derive velocity from how far each
|
|
150
|
+
* particle actually moved — which is what makes it position-based: a constraint that moves a
|
|
151
|
+
* particle also, by construction, changes its velocity, so nothing has to reconcile the two.
|
|
152
|
+
*/
|
|
153
|
+
step(world: PhysicsWorld | null, dt: number): void;
|
|
154
|
+
/**
|
|
155
|
+
* One distance constraint, with its compliance.
|
|
156
|
+
*
|
|
157
|
+
* `α = compliance / h²` is what makes the stiffness independent of the step and of the iteration
|
|
158
|
+
* count. At zero compliance this reduces exactly to an ordinary position-based projection, which
|
|
159
|
+
* is why an inextensible sheet costs nothing extra.
|
|
160
|
+
*/
|
|
161
|
+
private project;
|
|
162
|
+
/**
|
|
163
|
+
* Keep the sheet out of itself: a spatial hash over the particles, and a symmetric push apart.
|
|
164
|
+
*
|
|
165
|
+
* **The hash is rebuilt every tick and that is the cheap half.** One pass over the particles
|
|
166
|
+
* writing two integers each, against a structure that tried to be incremental and would have to
|
|
167
|
+
* know which particles moved — which in a cloth solver is all of them.
|
|
168
|
+
*
|
|
169
|
+
* The cell size is the self-collision distance, so a particle's neighbours are in its own cell
|
|
170
|
+
* and the twenty-six around it. **The 3x3x3 scan is what makes the answer complete**: a pair at
|
|
171
|
+
* the distance exactly cannot be in cells more than one apart, and a scan of the own cell alone
|
|
172
|
+
* would miss every pair straddling a boundary — which is most of them, since a folded sheet lies
|
|
173
|
+
* along one.
|
|
174
|
+
*
|
|
175
|
+
* **Each pair is resolved once, by index order.** Visiting both halves would apply the
|
|
176
|
+
* correction twice and turn a resting fold into a buzzing one; the `other > i` test is what
|
|
177
|
+
* makes the pass a sweep over pairs rather than over particles.
|
|
178
|
+
*
|
|
179
|
+
* A projection rather than a constraint with compliance, unlike the distance links: a contact is
|
|
180
|
+
* an inequality — it does nothing until it is violated — and XPBD's accumulated multiplier is
|
|
181
|
+
* for a constraint that holds all tick. *What that gives up* is that the stiffness of a fold is
|
|
182
|
+
* the iteration count's again, for this one term. *What would make it wrong* is a consumer
|
|
183
|
+
* wanting a fold to have a measurable resistance, at which point it needs its own multiplier
|
|
184
|
+
* array and a sign clamp on it.
|
|
185
|
+
*/
|
|
186
|
+
private collideSelf;
|
|
187
|
+
/** Whether a link joins these two. A scan over the eight or so a grid particle has. */
|
|
188
|
+
private linked;
|
|
189
|
+
/** Push two particles apart to exactly `radius`, split by their inverse masses. */
|
|
190
|
+
private separate;
|
|
191
|
+
/**
|
|
192
|
+
* Push every particle out of anything it has entered, and hand the body back what it took.
|
|
193
|
+
*
|
|
194
|
+
* **The impulse is derived from the correction rather than measured**, which is what makes the
|
|
195
|
+
* exchange cost one multiply: the constraint solve has already decided where the particle would
|
|
196
|
+
* be, the push says where it must be instead, and the difference over the tick is the velocity
|
|
197
|
+
* the collision removed from it. `m·Δ/h` is that momentum, and the body gets its negative at the
|
|
198
|
+
* point of contact — a push straight down on a plank's end tips the plank, because
|
|
199
|
+
* `applyImpulse` takes the point.
|
|
200
|
+
*
|
|
201
|
+
* A static body is skipped for the impulse rather than for the push: scenery does not move, and
|
|
202
|
+
* `applyImpulse` would return without doing anything anyway.
|
|
203
|
+
*/
|
|
204
|
+
private collideParticles;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* A rectangular sheet: positions, its stretch links, and the bend links across them.
|
|
208
|
+
*
|
|
209
|
+
* Bend links skip a particle rather than joining diagonals, because a diagonal resists *shear* and a
|
|
210
|
+
* skip resists *bowing*, and a sheet that cannot shear drapes like cardboard.
|
|
211
|
+
*
|
|
212
|
+
* **A skip-one distance constraint resists curvature and not orientation**, which is worth knowing
|
|
213
|
+
* before tuning it: three collinear particles satisfy it at any angle, so a flat sheet and a sheet
|
|
214
|
+
* hanging straight down are equally happy. Measured — a cantilever pinned along one edge settles at
|
|
215
|
+
* exactly vertical whether the bend compliance is 1e-8 or 1e6. What it *does* buy is 12.5
|
|
216
|
+
* millimetres less bow on a strip pinned at both ends with slack.
|
|
217
|
+
*
|
|
218
|
+
* **What would make it wrong** is a consumer wanting a cloak to stand out from a shoulder, and the
|
|
219
|
+
* answer then is a dihedral-angle constraint over the two triangles either side of an edge, which
|
|
220
|
+
* knows a rest angle and therefore knows the difference between flat and folded.
|
|
221
|
+
*/
|
|
222
|
+
export declare function makeClothGrid(columns: number, rows: number, spacing: number): {
|
|
223
|
+
positions: Float32Array;
|
|
224
|
+
links: Int32Array;
|
|
225
|
+
bendLinks: Int32Array;
|
|
226
|
+
};
|