@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.
Files changed (136) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +503 -0
  4. package/dist/bodies.d.ts +121 -0
  5. package/dist/bodies.js +298 -0
  6. package/dist/buoyancy.d.ts +46 -0
  7. package/dist/buoyancy.js +45 -0
  8. package/dist/cloth.d.ts +226 -0
  9. package/dist/cloth.js +587 -0
  10. package/dist/collide/aabb.d.ts +10 -0
  11. package/dist/collide/aabb.js +11 -0
  12. package/dist/collide/body.d.ts +202 -0
  13. package/dist/collide/body.js +142 -0
  14. package/dist/collide/eject.d.ts +39 -0
  15. package/dist/collide/eject.js +142 -0
  16. package/dist/collide/index.d.ts +18 -0
  17. package/dist/collide/index.js +5 -0
  18. package/dist/collide/scratch.d.ts +8 -0
  19. package/dist/collide/scratch.js +9 -0
  20. package/dist/collide/segment.d.ts +12 -0
  21. package/dist/collide/segment.js +109 -0
  22. package/dist/collide/sweep.d.ts +3 -0
  23. package/dist/collide/sweep.js +1036 -0
  24. package/dist/colliderSet.d.ts +239 -0
  25. package/dist/colliderSet.js +546 -0
  26. package/dist/compoundContact.d.ts +70 -0
  27. package/dist/compoundContact.js +221 -0
  28. package/dist/controller.d.ts +351 -0
  29. package/dist/controller.js +641 -0
  30. package/dist/cylinder.d.ts +78 -0
  31. package/dist/cylinder.js +294 -0
  32. package/dist/cylinderContact.d.ts +7 -0
  33. package/dist/cylinderContact.js +704 -0
  34. package/dist/decompose.d.ts +105 -0
  35. package/dist/decompose.js +979 -0
  36. package/dist/events.d.ts +43 -0
  37. package/dist/events.js +95 -0
  38. package/dist/executor.d.ts +147 -0
  39. package/dist/executor.js +173 -0
  40. package/dist/faces.d.ts +26 -0
  41. package/dist/faces.js +34 -0
  42. package/dist/fingerprint.d.ts +61 -0
  43. package/dist/fingerprint.js +139 -0
  44. package/dist/groundMotor.d.ts +34 -0
  45. package/dist/groundMotor.js +41 -0
  46. package/dist/heightfieldShape.d.ts +128 -0
  47. package/dist/heightfieldShape.js +328 -0
  48. package/dist/index.d.ts +73 -0
  49. package/dist/index.js +47 -0
  50. package/dist/island.d.ts +55 -0
  51. package/dist/island.js +227 -0
  52. package/dist/islandSolve.d.ts +70 -0
  53. package/dist/islandSolve.js +171 -0
  54. package/dist/islandWorker.d.ts +18 -0
  55. package/dist/islandWorker.js +125 -0
  56. package/dist/joints.d.ts +180 -0
  57. package/dist/joints.js +1094 -0
  58. package/dist/manifold.d.ts +89 -0
  59. package/dist/manifold.js +608 -0
  60. package/dist/mass.d.ts +53 -0
  61. package/dist/mass.js +353 -0
  62. package/dist/meshContact.d.ts +57 -0
  63. package/dist/meshContact.js +393 -0
  64. package/dist/meshShape.d.ts +76 -0
  65. package/dist/meshShape.js +175 -0
  66. package/dist/pairs.d.ts +62 -0
  67. package/dist/pairs.js +154 -0
  68. package/dist/query.d.ts +71 -0
  69. package/dist/query.js +683 -0
  70. package/dist/ragdoll.d.ts +135 -0
  71. package/dist/ragdoll.js +489 -0
  72. package/dist/shape.d.ts +177 -0
  73. package/dist/shape.js +533 -0
  74. package/dist/solver.d.ts +136 -0
  75. package/dist/solver.js +601 -0
  76. package/dist/stage.d.ts +167 -0
  77. package/dist/stage.js +350 -0
  78. package/dist/stallEscape.d.ts +141 -0
  79. package/dist/stallEscape.js +254 -0
  80. package/dist/sweptStep.d.ts +173 -0
  81. package/dist/sweptStep.js +472 -0
  82. package/dist/tree.d.ts +61 -0
  83. package/dist/tree.js +348 -0
  84. package/dist/vehicle.d.ts +154 -0
  85. package/dist/vehicle.js +343 -0
  86. package/dist/workerPool.d.ts +107 -0
  87. package/dist/workerPool.js +239 -0
  88. package/dist/workers.d.ts +37 -0
  89. package/dist/workers.js +35 -0
  90. package/dist/world.d.ts +298 -0
  91. package/dist/world.js +712 -0
  92. package/package.json +55 -0
  93. package/src/bodies.ts +369 -0
  94. package/src/buoyancy.ts +78 -0
  95. package/src/cloth.ts +734 -0
  96. package/src/collide/aabb.ts +28 -0
  97. package/src/collide/body.ts +336 -0
  98. package/src/collide/eject.ts +156 -0
  99. package/src/collide/index.ts +26 -0
  100. package/src/collide/scratch.ts +10 -0
  101. package/src/collide/segment.ts +127 -0
  102. package/src/collide/sweep.ts +1132 -0
  103. package/src/colliderSet.ts +637 -0
  104. package/src/compoundContact.ts +284 -0
  105. package/src/controller.ts +817 -0
  106. package/src/cylinder.ts +340 -0
  107. package/src/cylinderContact.ts +825 -0
  108. package/src/decompose.ts +1198 -0
  109. package/src/events.ts +108 -0
  110. package/src/executor.ts +224 -0
  111. package/src/faces.ts +37 -0
  112. package/src/fingerprint.ts +146 -0
  113. package/src/groundMotor.ts +53 -0
  114. package/src/heightfieldShape.ts +395 -0
  115. package/src/index.ts +164 -0
  116. package/src/island.ts +219 -0
  117. package/src/islandSolve.ts +218 -0
  118. package/src/islandWorker.ts +141 -0
  119. package/src/joints.ts +1367 -0
  120. package/src/manifold.ts +798 -0
  121. package/src/mass.ts +424 -0
  122. package/src/meshContact.ts +443 -0
  123. package/src/meshShape.ts +261 -0
  124. package/src/pairs.ts +169 -0
  125. package/src/query.ts +844 -0
  126. package/src/ragdoll.ts +662 -0
  127. package/src/shape.ts +656 -0
  128. package/src/solver.ts +873 -0
  129. package/src/stage.ts +406 -0
  130. package/src/stallEscape.ts +362 -0
  131. package/src/sweptStep.ts +609 -0
  132. package/src/tree.ts +366 -0
  133. package/src/vehicle.ts +489 -0
  134. package/src/workerPool.ts +283 -0
  135. package/src/workers.ts +38 -0
  136. package/src/world.ts +961 -0
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@driftengine/physics",
3
+ "version": "3.61.0",
4
+ "description": "Collision shapes, spatial queries and swept kinematic collision",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "drift-source": "./src/index.ts",
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./package.json": "./package.json",
16
+ "./*": "./*"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src",
21
+ "!src/**/*.test.ts",
22
+ "!src/**/*.test.mjs",
23
+ "!src/**/__snapshots__",
24
+ "README.md",
25
+ "LICENSE",
26
+ "NOTICE"
27
+ ],
28
+ "sideEffects": false,
29
+ "author": "Drift Technologies",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/drftrun/driftengine.git",
33
+ "directory": "packages/physics"
34
+ },
35
+ "homepage": "https://github.com/drftrun/driftengine#readme",
36
+ "bugs": "https://github.com/drftrun/driftengine/issues",
37
+ "keywords": [
38
+ "driftengine",
39
+ "3d",
40
+ "webgl",
41
+ "webgpu",
42
+ "typescript",
43
+ "physics",
44
+ "collision",
45
+ "rigid-body",
46
+ "raycast",
47
+ "character-controller"
48
+ ],
49
+ "engines": {
50
+ "node": ">=22.12.0"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ }
55
+ }
package/src/bodies.ts ADDED
@@ -0,0 +1,369 @@
1
+ import { combineMassProperties, createMassProperties, shapeMassProperties } from './mass.ts';
2
+ import type { ConvexShape } from './shape.ts';
3
+
4
+ /**
5
+ * Rigid body state, structure-of-arrays, indexed by a dense body index.
6
+ *
7
+ * **`Float32Array` for state and doubles for every intermediate.** A store rounds through
8
+ * `Math.fround`, which ECMAScript specifies exactly, while the arithmetic between stores stays in
9
+ * doubles — so the solver computes at higher precision than it keeps and halves its memory
10
+ * bandwidth. Both halves of that are deliberate, and JavaScript has no SIMD to spend the bandwidth
11
+ * on instead.
12
+ *
13
+ * **Indices are dense and removal swaps the last body in**, so a caller holding an index must be
14
+ * told when one moves. `remove` returns the index that moved, or −1, and every structure keyed by
15
+ * body index has to act on it.
16
+ */
17
+
18
+ export const BODY_STATIC = 0;
19
+ export const BODY_KINEMATIC = 1;
20
+ export const BODY_DYNAMIC = 2;
21
+ /** Static never moves, kinematic moves but is not pushed, dynamic is solved. */
22
+ export type BodyType = 0 | 1 | 2;
23
+
24
+ /** Set on a body whose island is asleep. Woken bodies clear it together. */
25
+ export const BODY_SLEEPING = 1;
26
+ /**
27
+ * A sensor reports overlap and resolves none of it.
28
+ *
29
+ * Narrowphase still runs — an overlap that is never computed cannot be reported — and the solver is
30
+ * what skips it. **What this costs** is a sensor being as dear as a solid body in the broadphase and
31
+ * narrowphase, which is where most of the cost is anyway.
32
+ */
33
+ export const BODY_SENSOR = 2;
34
+
35
+ export interface BodyDesc {
36
+ readonly type: BodyType;
37
+ /**
38
+ * The body's shape, or absent where it is made of several. Exactly one of these two.
39
+ *
40
+ * A body carrying both is refused at `addBody`, because there is no sensible reading of it: the
41
+ * narrow phase would have to decide which one it meant and either choice is a body that collides
42
+ * as something the caller did not describe.
43
+ */
44
+ readonly shape?: ConvexShape;
45
+ /**
46
+ * Several shapes, as one rigid body. What `decomposeConvex` produces, and where it goes.
47
+ *
48
+ * **No offset and no rotation per part**, because a `ConvexShape` is already a point cloud in the
49
+ * body's frame — see `vertices` above. A decomposition's parts are where they belong the moment
50
+ * they exist, and a field that would always be zero is a field somebody eventually sets wrong. The
51
+ * kinematic `Body.parts` carries offsets because it wraps shapes built at the origin and placed
52
+ * afterwards; nothing here does that.
53
+ *
54
+ * Mass is `combineMassProperties`: volumes added, the centre weighted, each tensor moved to the
55
+ * body's centre by the parallel axis theorem. Contacts are one manifold per touching part, with
56
+ * the part pair folded into every feature id so warm starting cannot confuse two of them.
57
+ *
58
+ * Refused above `MAX_BODY_PARTS`, at the call rather than in the tick.
59
+ */
60
+ readonly shapes?: readonly ConvexShape[];
61
+ readonly density?: number;
62
+ readonly x?: number;
63
+ readonly y?: number;
64
+ readonly z?: number;
65
+ /** Unit quaternion. Defaults to identity. */
66
+ readonly qx?: number;
67
+ readonly qy?: number;
68
+ readonly qz?: number;
69
+ readonly qw?: number;
70
+ readonly friction?: number;
71
+ readonly restitution?: number;
72
+ /** Which layers this body is on, and which it layersInteract with. 32 bits each. */
73
+ readonly layer?: number;
74
+ readonly mask?: number;
75
+ /** A sensor reports overlap and is never pushed out of it. */
76
+ readonly sensor?: boolean;
77
+ }
78
+
79
+ const SCRATCH_MASS = createMassProperties();
80
+
81
+ export class BodySet {
82
+ count = 0;
83
+
84
+ posX: Float32Array;
85
+ posY: Float32Array;
86
+ posZ: Float32Array;
87
+ rotX: Float32Array;
88
+ rotY: Float32Array;
89
+ rotZ: Float32Array;
90
+ rotW: Float32Array;
91
+ velX: Float32Array;
92
+ velY: Float32Array;
93
+ velZ: Float32Array;
94
+ angX: Float32Array;
95
+ angY: Float32Array;
96
+ angZ: Float32Array;
97
+ invMass: Float32Array;
98
+ /** Body-frame inverse inertia, six symmetric terms per body: xx, yy, zz, xy, xz, yz. */
99
+ invInertia: Float32Array;
100
+ /** Centre of mass in body frame, which a shape built off-centre has. */
101
+ comX: Float32Array;
102
+ comY: Float32Array;
103
+ comZ: Float32Array;
104
+ friction: Float32Array;
105
+ restitution: Float32Array;
106
+ type: Uint8Array;
107
+ flags: Uint8Array;
108
+ sleepTicks: Uint16Array;
109
+ layer: Uint32Array;
110
+ mask: Uint32Array;
111
+ shape: (ConvexShape | undefined)[] = [];
112
+ /** The parts of a compound body, or undefined where it has a single `shape`. */
113
+ shapes: (readonly ConvexShape[] | undefined)[] = [];
114
+
115
+ private capacity: number;
116
+
117
+ constructor(capacity = 64) {
118
+ this.capacity = Math.max(1, capacity | 0);
119
+ const f = (n = 1) => new Float32Array(this.capacity * n);
120
+ this.posX = f();
121
+ this.posY = f();
122
+ this.posZ = f();
123
+ this.rotX = f();
124
+ this.rotY = f();
125
+ this.rotZ = f();
126
+ this.rotW = f();
127
+ this.velX = f();
128
+ this.velY = f();
129
+ this.velZ = f();
130
+ this.angX = f();
131
+ this.angY = f();
132
+ this.angZ = f();
133
+ this.invMass = f();
134
+ this.invInertia = f(6);
135
+ this.comX = f();
136
+ this.comY = f();
137
+ this.comZ = f();
138
+ this.friction = f();
139
+ this.restitution = f();
140
+ this.type = new Uint8Array(this.capacity);
141
+ this.flags = new Uint8Array(this.capacity);
142
+ this.sleepTicks = new Uint16Array(this.capacity);
143
+ this.layer = new Uint32Array(this.capacity);
144
+ this.mask = new Uint32Array(this.capacity);
145
+ }
146
+
147
+ add(desc: BodyDesc): number {
148
+ if (this.count === this.capacity) this.grow();
149
+ const i = this.count++;
150
+ this.posX[i] = desc.x ?? 0;
151
+ this.posY[i] = desc.y ?? 0;
152
+ this.posZ[i] = desc.z ?? 0;
153
+ this.rotX[i] = desc.qx ?? 0;
154
+ this.rotY[i] = desc.qy ?? 0;
155
+ this.rotZ[i] = desc.qz ?? 0;
156
+ this.rotW[i] = desc.qw ?? 1;
157
+ this.velX[i] = 0;
158
+ this.velY[i] = 0;
159
+ this.velZ[i] = 0;
160
+ this.angX[i] = 0;
161
+ this.angY[i] = 0;
162
+ this.angZ[i] = 0;
163
+ this.type[i] = desc.type;
164
+ this.flags[i] = desc.sensor ? BODY_SENSOR : 0;
165
+ this.sleepTicks[i] = 0;
166
+ this.friction[i] = desc.friction ?? 0.5;
167
+ this.restitution[i] = desc.restitution ?? 0;
168
+ this.layer[i] = desc.layer ?? 1;
169
+ this.mask[i] = desc.mask ?? 0xffffffff;
170
+ this.shape[i] = desc.shape;
171
+ this.shapes[i] = desc.shapes;
172
+
173
+ /*
174
+ * A static or kinematic body has zero inverse mass and zero inverse inertia, which is what
175
+ * makes it immovable *arithmetically* rather than by a branch in the solver. Every constraint
176
+ * then treats it uniformly and the hot loop has one fewer test in it.
177
+ */
178
+ if (desc.type === BODY_DYNAMIC) {
179
+ const density = desc.density ?? 1000;
180
+ const m = desc.shapes
181
+ ? combineMassProperties(desc.shapes, density, SCRATCH_MASS)
182
+ : shapeMassProperties(desc.shape as ConvexShape, density, SCRATCH_MASS);
183
+ const mass = m.volume * density;
184
+ this.invMass[i] = mass > 0 ? 1 / mass : 0;
185
+ this.comX[i] = m.comX;
186
+ this.comY[i] = m.comY;
187
+ this.comZ[i] = m.comZ;
188
+ invertSymmetric(m.ixx, m.iyy, m.izz, m.ixy, m.ixz, m.iyz, this.invInertia, i * 6);
189
+ } else {
190
+ this.invMass[i] = 0;
191
+ this.comX[i] = 0;
192
+ this.comY[i] = 0;
193
+ this.comZ[i] = 0;
194
+ for (let k = 0; k < 6; k++) this.invInertia[i * 6 + k] = 0;
195
+ }
196
+ return i;
197
+ }
198
+
199
+ /** Swap-remove. Returns the index that moved into `index`, or −1 if it was the last. */
200
+ remove(index: number): number {
201
+ const last = --this.count;
202
+ if (index === last) {
203
+ this.shape[index] = undefined;
204
+ this.shapes[index] = undefined;
205
+ return -1;
206
+ }
207
+ const copy = (a: Float32Array | Uint8Array | Uint16Array | Uint32Array, stride = 1) => {
208
+ for (let k = 0; k < stride; k++) a[index * stride + k] = a[last * stride + k] ?? 0;
209
+ };
210
+ for (const a of [
211
+ this.posX,
212
+ this.posY,
213
+ this.posZ,
214
+ this.rotX,
215
+ this.rotY,
216
+ this.rotZ,
217
+ this.rotW,
218
+ this.velX,
219
+ this.velY,
220
+ this.velZ,
221
+ this.angX,
222
+ this.angY,
223
+ this.angZ,
224
+ this.invMass,
225
+ this.comX,
226
+ this.comY,
227
+ this.comZ,
228
+ this.friction,
229
+ this.restitution,
230
+ ])
231
+ copy(a);
232
+ copy(this.invInertia, 6);
233
+ copy(this.type);
234
+ copy(this.flags);
235
+ copy(this.sleepTicks);
236
+ copy(this.layer);
237
+ copy(this.mask);
238
+ this.shape[index] = this.shape[last];
239
+ this.shape[last] = undefined;
240
+ this.shapes[index] = this.shapes[last];
241
+ this.shapes[last] = undefined;
242
+ return last;
243
+ }
244
+
245
+ /**
246
+ * Fill `out` with the world-frame inverse inertia, six symmetric terms.
247
+ *
248
+ * `R · I⁻¹ · Rᵀ`, a congruence, rather than a diagonalisation. Storing three principal moments
249
+ * would need an eigen decomposition, and Jacobi is iterative — a fixed sweep count would be
250
+ * deterministic but is one more thing to get exactly right for no gain. **What this gives up** is
251
+ * six floats per body instead of three and a congruence per substep. **What would make it wrong**
252
+ * is a measurement showing that congruence dominates the tick.
253
+ */
254
+ worldInverseInertia(index: number, out: Float32Array, at = 0): void {
255
+ const x = this.rotX[index] ?? 0;
256
+ const y = this.rotY[index] ?? 0;
257
+ const z = this.rotZ[index] ?? 0;
258
+ const w = this.rotW[index] ?? 1;
259
+ // Rotation matrix, row-major.
260
+ const r00 = 1 - 2 * (y * y + z * z);
261
+ const r01 = 2 * (x * y - z * w);
262
+ const r02 = 2 * (x * z + y * w);
263
+ const r10 = 2 * (x * y + z * w);
264
+ const r11 = 1 - 2 * (x * x + z * z);
265
+ const r12 = 2 * (y * z - x * w);
266
+ const r20 = 2 * (x * z - y * w);
267
+ const r21 = 2 * (y * z + x * w);
268
+ const r22 = 1 - 2 * (x * x + y * y);
269
+
270
+ const b = index * 6;
271
+ const mxx = this.invInertia[b] ?? 0;
272
+ const myy = this.invInertia[b + 1] ?? 0;
273
+ const mzz = this.invInertia[b + 2] ?? 0;
274
+ const mxy = this.invInertia[b + 3] ?? 0;
275
+ const mxz = this.invInertia[b + 4] ?? 0;
276
+ const myz = this.invInertia[b + 5] ?? 0;
277
+
278
+ // T = R · M, then out = T · Rᵀ, keeping only the six symmetric terms.
279
+ const t00 = r00 * mxx + r01 * mxy + r02 * mxz;
280
+ const t01 = r00 * mxy + r01 * myy + r02 * myz;
281
+ const t02 = r00 * mxz + r01 * myz + r02 * mzz;
282
+ const t10 = r10 * mxx + r11 * mxy + r12 * mxz;
283
+ const t11 = r10 * mxy + r11 * myy + r12 * myz;
284
+ const t12 = r10 * mxz + r11 * myz + r12 * mzz;
285
+ const t20 = r20 * mxx + r21 * mxy + r22 * mxz;
286
+ const t21 = r20 * mxy + r21 * myy + r22 * myz;
287
+ const t22 = r20 * mxz + r21 * myz + r22 * mzz;
288
+
289
+ out[at] = t00 * r00 + t01 * r01 + t02 * r02;
290
+ out[at + 1] = t10 * r10 + t11 * r11 + t12 * r12;
291
+ out[at + 2] = t20 * r20 + t21 * r21 + t22 * r22;
292
+ out[at + 3] = t00 * r10 + t01 * r11 + t02 * r12;
293
+ out[at + 4] = t00 * r20 + t01 * r21 + t02 * r22;
294
+ out[at + 5] = t10 * r20 + t11 * r21 + t12 * r22;
295
+ }
296
+
297
+ private grow(): void {
298
+ const next = this.capacity * 2;
299
+ const wider = <T extends Float32Array | Uint8Array | Uint16Array | Uint32Array>(
300
+ a: T,
301
+ stride = 1,
302
+ ): T => {
303
+ const out = new (a.constructor as new (n: number) => T)(next * stride);
304
+ out.set(a);
305
+ return out;
306
+ };
307
+ this.posX = wider(this.posX);
308
+ this.posY = wider(this.posY);
309
+ this.posZ = wider(this.posZ);
310
+ this.rotX = wider(this.rotX);
311
+ this.rotY = wider(this.rotY);
312
+ this.rotZ = wider(this.rotZ);
313
+ this.rotW = wider(this.rotW);
314
+ this.velX = wider(this.velX);
315
+ this.velY = wider(this.velY);
316
+ this.velZ = wider(this.velZ);
317
+ this.angX = wider(this.angX);
318
+ this.angY = wider(this.angY);
319
+ this.angZ = wider(this.angZ);
320
+ this.invMass = wider(this.invMass);
321
+ this.invInertia = wider(this.invInertia, 6);
322
+ this.comX = wider(this.comX);
323
+ this.comY = wider(this.comY);
324
+ this.comZ = wider(this.comZ);
325
+ this.friction = wider(this.friction);
326
+ this.restitution = wider(this.restitution);
327
+ this.type = wider(this.type);
328
+ this.flags = wider(this.flags);
329
+ this.sleepTicks = wider(this.sleepTicks);
330
+ this.layer = wider(this.layer);
331
+ this.mask = wider(this.mask);
332
+ this.capacity = next;
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Invert a symmetric 3×3 in closed form, writing six terms.
338
+ *
339
+ * A singular tensor — a shape with no volume — inverts to zero rather than to infinity, which makes
340
+ * it behave as a point mass instead of producing NaN in every constraint that touches it. An
341
+ * unwritten uniform is zero and zero is a real value, which is the failure this engine has recorded
342
+ * twice; here zero is the *correct* value and the comment is what says so.
343
+ */
344
+ function invertSymmetric(
345
+ xx: number,
346
+ yy: number,
347
+ zz: number,
348
+ xy: number,
349
+ xz: number,
350
+ yz: number,
351
+ out: Float32Array,
352
+ at: number,
353
+ ): void {
354
+ const c00 = yy * zz - yz * yz;
355
+ const c01 = xz * yz - xy * zz;
356
+ const c02 = xy * yz - xz * yy;
357
+ const det = xx * c00 + xy * c01 + xz * c02;
358
+ if (det === 0) {
359
+ for (let k = 0; k < 6; k++) out[at + k] = 0;
360
+ return;
361
+ }
362
+ const inv = 1 / det;
363
+ out[at] = c00 * inv;
364
+ out[at + 1] = (xx * zz - xz * xz) * inv;
365
+ out[at + 2] = (xx * yy - xy * xy) * inv;
366
+ out[at + 3] = c01 * inv;
367
+ out[at + 4] = c02 * inv;
368
+ out[at + 5] = (xy * xz - xx * yz) * inv;
369
+ }
@@ -0,0 +1,78 @@
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
+
16
+ export interface BuoyancyOptions {
17
+ /**
18
+ * The surface height. Default −Infinity, which is a world with no water in it.
19
+ *
20
+ * A consumer that has not asked reaches none of this and pays for none of it.
21
+ */
22
+ level?: number;
23
+ /** How much of a body's speed a second of water takes, as a fraction. */
24
+ drag?: number;
25
+ /** The fastest a submerged body sinks, metres a second. Negative is downward. */
26
+ sinkSpeed?: number;
27
+ }
28
+
29
+ export interface Buoyancy {
30
+ readonly level: number;
31
+ readonly drag: number;
32
+ readonly sinkSpeed: number;
33
+ }
34
+
35
+ /** Anything with a velocity. Declared structurally so nothing is imported in either direction. */
36
+ export interface BuoyantState {
37
+ velX: number;
38
+ velY: number;
39
+ velZ: number;
40
+ }
41
+
42
+ export function createBuoyancy(options: BuoyancyOptions = {}): Buoyancy {
43
+ return {
44
+ level: options.level ?? -Infinity,
45
+ drag: options.drag ?? 0,
46
+ sinkSpeed: options.sinkSpeed ?? 0,
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Apply it, and report whether the body is submerged.
52
+ *
53
+ * @param y Where the body is. Under `level` is submerged.
54
+ * @returns Whether it was, so a consumer can count the time or drown somebody — which is a game's
55
+ * decision and not this function's.
56
+ */
57
+ export function applyBuoyancy(
58
+ water: Buoyancy,
59
+ y: number,
60
+ state: BuoyantState,
61
+ dt: number,
62
+ ): boolean {
63
+ if (y >= water.level) return false;
64
+
65
+ /*
66
+ * Clamped at zero: `1 − drag·dt` goes negative past `dt = 1/drag`, and a negative factor does
67
+ * not slow a body, it throws it backwards. The worst a long step can do is stop it dead.
68
+ */
69
+ const kept = Math.max(0, 1 - water.drag * dt);
70
+ state.velX *= kept;
71
+ state.velY *= kept;
72
+ state.velZ *= kept;
73
+
74
+ /* A floor on the fall rather than a speed it is driven to: a body drifting down slower than
75
+ terminal is left alone, and one falling faster is settled to it. */
76
+ if (state.velY < water.sinkSpeed) state.velY = water.sinkSpeed;
77
+ return true;
78
+ }