@woosh/meep-engine 2.140.0 → 2.141.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/package.json +1 -1
- package/src/core/geom/3d/quaternion/quat3_multiply.d.ts +21 -0
- package/src/core/geom/3d/quaternion/quat3_multiply.d.ts.map +1 -0
- package/src/core/geom/3d/quaternion/quat3_multiply.js +25 -0
- package/src/engine/physics/PLAN.md +152 -35
- package/src/engine/physics/REVIEW_002.md +151 -0
- package/src/engine/physics/constraint/DofMode.d.ts +28 -0
- package/src/engine/physics/constraint/DofMode.d.ts.map +1 -0
- package/src/engine/physics/constraint/DofMode.js +35 -0
- package/src/engine/physics/constraint/solve_constraints.d.ts +16 -0
- package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -0
- package/src/engine/physics/constraint/solve_constraints.js +436 -0
- package/src/engine/physics/ecs/Joint.d.ts +179 -0
- package/src/engine/physics/ecs/Joint.d.ts.map +1 -0
- package/src/engine/physics/ecs/Joint.js +234 -0
- package/src/engine/physics/ecs/PhysicsSystem.d.ts +52 -0
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +126 -4
- package/src/engine/physics/fluid/FluidField.d.ts +14 -10
- package/src/engine/physics/fluid/FluidField.d.ts.map +1 -1
- package/src/engine/physics/fluid/FluidField.js +14 -10
- package/src/engine/physics/fluid/FluidSimulator.d.ts.map +1 -1
- package/src/engine/physics/fluid/FluidSimulator.js +0 -1
- package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.d.ts +17 -10
- package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.js +18 -11
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.d.ts +13 -10
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.js +18 -13
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.d.ts +4 -3
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.js +15 -11
- package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts +24 -22
- package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.js +26 -22
- package/src/engine/physics/island/IslandBuilder.d.ts +4 -1
- package/src/engine/physics/island/IslandBuilder.d.ts.map +1 -1
- package/src/engine/physics/island/IslandBuilder.js +33 -16
- package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/box_box_manifold.js +27 -1
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts +33 -0
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/narrowphase_step.js +75 -0
- package/src/engine/physics/solver/solve_contacts.d.ts +28 -0
- package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
- package/src/engine/physics/solver/solve_contacts.js +169 -1
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"description": "Pure JavaScript game engine. Fully featured and production ready.",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"author": "Alexander Goldring",
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.141.0",
|
|
10
10
|
"main": "build/meep.module.js",
|
|
11
11
|
"module": "build/meep.module.js",
|
|
12
12
|
"exports": {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hamilton product of two unit quaternions: `out = a ⊗ b`.
|
|
3
|
+
*
|
|
4
|
+
* Composition order is the usual rotation convention: the product `a ⊗ b`
|
|
5
|
+
* applies `b` first, then `a` (i.e. `(a⊗b)·v·(a⊗b)* == a·(b·v·b*)·a*`). So to
|
|
6
|
+
* rotate a vector by `q1` and then by `q2`, compose `q2 ⊗ q1`.
|
|
7
|
+
*
|
|
8
|
+
* Sign convention: `(x, y, z, w)` — `w` last, matching {@link Quaternion} and
|
|
9
|
+
* the rest of the `core/geom/3d/quaternion/` family.
|
|
10
|
+
*
|
|
11
|
+
* `out` may alias neither `a` nor `b` component-wise (the result is computed
|
|
12
|
+
* from all eight inputs before any write, so passing the same backing array
|
|
13
|
+
* via different offsets is safe only if the ranges don't overlap).
|
|
14
|
+
*
|
|
15
|
+
* @param {number[]|Float32Array|Float64Array} out
|
|
16
|
+
* @param {number} out_offset offset into `out`; receives 4 floats (x, y, z, w)
|
|
17
|
+
* @param {number} ax @param {number} ay @param {number} az @param {number} aw
|
|
18
|
+
* @param {number} bx @param {number} by @param {number} bz @param {number} bw
|
|
19
|
+
*/
|
|
20
|
+
export function quat3_multiply(out: number[] | Float32Array | Float64Array, out_offset: number, ax: number, ay: number, az: number, aw: number, bx: number, by: number, bz: number, bw: number): void;
|
|
21
|
+
//# sourceMappingURL=quat3_multiply.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quat3_multiply.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/quaternion/quat3_multiply.js"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,oCALW,MAAM,EAAE,GAAC,YAAY,GAAC,YAAY,cAClC,MAAM,MACN,MAAM,MAAa,MAAM,MAAa,MAAM,MAAa,MAAM,MAC/D,MAAM,MAAa,MAAM,MAAa,MAAM,MAAa,MAAM,QAOzE"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hamilton product of two unit quaternions: `out = a ⊗ b`.
|
|
3
|
+
*
|
|
4
|
+
* Composition order is the usual rotation convention: the product `a ⊗ b`
|
|
5
|
+
* applies `b` first, then `a` (i.e. `(a⊗b)·v·(a⊗b)* == a·(b·v·b*)·a*`). So to
|
|
6
|
+
* rotate a vector by `q1` and then by `q2`, compose `q2 ⊗ q1`.
|
|
7
|
+
*
|
|
8
|
+
* Sign convention: `(x, y, z, w)` — `w` last, matching {@link Quaternion} and
|
|
9
|
+
* the rest of the `core/geom/3d/quaternion/` family.
|
|
10
|
+
*
|
|
11
|
+
* `out` may alias neither `a` nor `b` component-wise (the result is computed
|
|
12
|
+
* from all eight inputs before any write, so passing the same backing array
|
|
13
|
+
* via different offsets is safe only if the ranges don't overlap).
|
|
14
|
+
*
|
|
15
|
+
* @param {number[]|Float32Array|Float64Array} out
|
|
16
|
+
* @param {number} out_offset offset into `out`; receives 4 floats (x, y, z, w)
|
|
17
|
+
* @param {number} ax @param {number} ay @param {number} az @param {number} aw
|
|
18
|
+
* @param {number} bx @param {number} by @param {number} bz @param {number} bw
|
|
19
|
+
*/
|
|
20
|
+
export function quat3_multiply(out, out_offset, ax, ay, az, aw, bx, by, bz, bw) {
|
|
21
|
+
out[out_offset] = aw * bx + ax * bw + ay * bz - az * by;
|
|
22
|
+
out[out_offset + 1] = aw * by - ax * bz + ay * bw + az * bx;
|
|
23
|
+
out[out_offset + 2] = aw * bz + ax * by - ay * bx + az * bw;
|
|
24
|
+
out[out_offset + 3] = aw * bw - ax * bx - ay * by - az * bz;
|
|
25
|
+
}
|
|
@@ -257,21 +257,30 @@ Architectural references for design choices:
|
|
|
257
257
|
the knot already uses the exact closed-form box-triangle solver (P1.1b);
|
|
258
258
|
the problem is purely that TGS freezes *which* feature is in contact across
|
|
259
259
|
substeps. The common concave case — a convex dynamic body on static concave
|
|
260
|
-
terrain — is unaffected (the convex side's feature is stable)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
260
|
+
terrain — is unaffected (the convex side's feature is stable), and that is
|
|
261
|
+
the only concave case the engine targets.
|
|
262
|
+
|
|
263
|
+
**Interim fix (implemented): per-substep concave re-detection.** For
|
|
264
|
+
contact pairs involving a concave body, the substep loop re-runs the
|
|
265
|
+
concave narrowphase geometry at the current substep pose (instead of the
|
|
266
|
+
analytic refresh that freezes the feature) and re-prepares those contacts
|
|
267
|
+
from the fresh witness/normal/depth — so the contact normal tracks the
|
|
268
|
+
rocking body and no energy is pumped in. Convex pairs keep the cheap
|
|
269
|
+
analytic refresh. This is ~Nx narrowphase cost on concave-involved pairs
|
|
270
|
+
(acceptable — they're rare), gated by collider convexity. Un-skips the
|
|
271
|
+
torus-knot dynamic-settle test.
|
|
272
|
+
|
|
273
|
+
**Better long-term fix: convex collision proxies (not raw concave).** Every
|
|
274
|
+
major engine (Box2D, Jolt, PhysX, Rapier) requires dynamic bodies to be
|
|
275
|
+
convex or convex-decomposed; raw concave meshes are static-only. The right
|
|
276
|
+
granularity is a *few* convex pieces — NOT the thousands of tets a
|
|
277
|
+
volumetric mesher produces (tet count ≈ collider/BVH-leaf count, which
|
|
278
|
+
explodes the broadphase for an awake body; tet meshing is for a future
|
|
279
|
+
FEM/soft-body subsystem, not rigid collision). See the "Convex collision
|
|
280
|
+
proxies for dynamic concave bodies" backlog item — a 3D convex hull builder
|
|
281
|
+
(single-hull proxy covers most dynamic objects) plus an optional
|
|
282
|
+
few-hull (V-HACD-style) decomposition. Those supersede the interim
|
|
283
|
+
per-substep re-detection once built.
|
|
275
284
|
|
|
276
285
|
---
|
|
277
286
|
|
|
@@ -331,31 +340,121 @@ scaffolding is in place.
|
|
|
331
340
|
for convex shapes (cheaper, no manifold-lifecycle churn) but is
|
|
332
341
|
only as good as the frozen normal — see the concave caveat below.
|
|
333
342
|
|
|
334
|
-
|
|
335
|
-
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
torus-knot dynamic-settle
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
+
Follow-ups since the core landed:
|
|
344
|
+
- [x] **Box-box SAT reference tie-break deadband** — aligned cube
|
|
345
|
+
stacks (4–10 high) now settle to zero velocity and sleep; the
|
|
346
|
+
reference-face flip-flop that creeped/toppled them is gone.
|
|
347
|
+
- [x] **Per-substep contact re-detection for concave pairs** — lifts
|
|
348
|
+
the dynamic-concave-body limitation; the torus-knot dynamic-settle
|
|
349
|
+
test is un-skipped. Concave pairs re-run narrowphase geometry each
|
|
350
|
+
substep (`redetect_concave_contacts`); convex pairs keep the cheap
|
|
351
|
+
analytic refresh.
|
|
352
|
+
|
|
353
|
+
Remaining (Phases 4–6) — now complete:
|
|
354
|
+
- [x] Regression coverage: heavy-on-light pyramid (10× capstone on two
|
|
355
|
+
light cubes settles + sleeps) and a ragdoll-stub (shoulder
|
|
356
|
+
ball-socket + elbow hinge arm hangs, stays articulated, settles).
|
|
357
|
+
- [x] REVIEW_002 retrospective — `engine/physics/REVIEW_002.md`.
|
|
343
358
|
|
|
344
359
|
References: Catto 2018 ("Soft Constraints" GDC talk + the TGS
|
|
345
360
|
follow-up); Box2D v3 source (`b2ApplyRestitution`, the substep solver
|
|
346
361
|
stages); Rapier as the closest architectural sibling.
|
|
347
362
|
|
|
348
|
-
- [ ] **
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
363
|
+
- [ ] **Constraints / joints — the next major work.** Now unblocked: TGS is
|
|
364
|
+
in (joint-chain convergence is a TGS sweet spot), warm-start +
|
|
365
|
+
per-substep + island machinery is reusable, and the SPOOK compliance
|
|
366
|
+
dial already in the contact solver gives soft/spring constraints
|
|
367
|
+
essentially for free. Target use cases: chains/ropes, ragdolls,
|
|
368
|
+
vehicles (incl. suspension), plus the common mechanical set (doors,
|
|
369
|
+
pistons, welds, grab/drag, winches, drivetrains).
|
|
370
|
+
|
|
371
|
+
**Foundational work (do first): generalise the solver to constraint
|
|
372
|
+
rows.** Today `solver/solve_contacts.js` is hard-coded to the
|
|
373
|
+
contact-shape constraint (normal + 2 friction tangents, ≥0 clamp,
|
|
374
|
+
restitution, penetration bias). Joints are equality / inequality
|
|
375
|
+
constraints on relative velocity at anchors, generally bilateral
|
|
376
|
+
(impulse may be ±) with optional limits and motors. The clean shape —
|
|
377
|
+
and what Jolt / Box2D-v3 do — is a **generic constraint row**: a
|
|
378
|
+
Jacobian (linear + angular parts per body), an effective mass, a bias
|
|
379
|
+
(position error × SPOOK gain, or motor target), and impulse bounds
|
|
380
|
+
`[lo, hi]` (`[0,∞)` for a contact/limit, `(−∞,∞)` for an equality,
|
|
381
|
+
`[−maxForce·h, +maxForce·h]` for a motor). Each joint type just fills
|
|
382
|
+
in its rows; the existing per-body impulse-apply primitive
|
|
383
|
+
(`apply_impulse_to_body` + `world_inverse_inertia_apply`), the
|
|
384
|
+
per-substep warm-start, the islands, and the split-impulse / SPOOK
|
|
385
|
+
position handling are all reused. Contacts become *one* constraint
|
|
386
|
+
type among several rather than the hard-coded path.
|
|
387
|
+
|
|
388
|
+
The specific constraint set, its use-case mapping, and per-type
|
|
389
|
+
architecture-fit assessment are under review (see the constraints
|
|
390
|
+
sketch). High level: ball-socket / distance / spring / weld and the
|
|
391
|
+
grab constraint are near drop-ins on the row machinery; hinge /
|
|
392
|
+
prismatic / cone-twist / motors / limits add angular-row + bounded-row
|
|
393
|
+
mechanics (still within the impulse framework); raycast vehicles,
|
|
394
|
+
conveyor surface-velocity, and gear/pulley coupling are higher-level
|
|
395
|
+
systems or contact modifiers that sit *on top of* the primitives
|
|
396
|
+
rather than being generic rows.
|
|
397
|
+
|
|
398
|
+
**Decision: build ONE configurable 6-DOF constraint** (PhysX D6 / Jolt
|
|
399
|
+
SixDOF), implemented mode-by-mode. The `Joint` ECS component carries
|
|
400
|
+
`dofMode[6]` (3 linear, 3 angular) each `{locked|free|limited|spring|
|
|
401
|
+
motor}` + per-DOF limit/spring/motor config + warm-start accumulators.
|
|
402
|
+
Concrete joints are configs, not new code (ball-socket = lock 3 linear;
|
|
403
|
+
hinge = lock 3 linear + 2 angular; weld = lock 6; cone-twist = lock 3
|
|
404
|
+
linear + limit 3 angular; suspension = spring 1 linear + lock rest).
|
|
405
|
+
|
|
406
|
+
Phasing:
|
|
407
|
+
1. [x] Constraint-row solver as a **parallel row set** in the TGS
|
|
408
|
+
substep loop (contacts left untouched, not ported — lower risk).
|
|
409
|
+
`constraint/solve_constraints.js` reuses `world_inverse_inertia`,
|
|
410
|
+
per-substep warm-start, and the SPOOK position bias; `Joint`
|
|
411
|
+
component + `link_joint`/`unlink_joint` in PhysicsSystem;
|
|
412
|
+
`jointIterations` knob. Bodies need no collider.
|
|
413
|
+
2. [x] **LOCKED linear DOFs → ball-socket.** Pendulum (anchor pinned
|
|
414
|
+
to a world pivot, body swings) and a 2-link chain (body↔body,
|
|
415
|
+
joints stay connected, chain hangs) pass. → **chains, ropes,
|
|
416
|
+
pendulums working.**
|
|
417
|
+
3. [x] LOCKED angular + linear DOFs in the frame basis — **weld,
|
|
418
|
+
hinge, prismatic done**. Joint frame bases
|
|
419
|
+
(`localBasisA`/`localBasisB`); BOTH linear and angular rows now
|
|
420
|
+
resolve in frame A's axes (cleared the world-axis linear debt — the
|
|
421
|
+
solver is fully frame-relative). Angular: relative rotation
|
|
422
|
+
`qD = conj(qA)·qB` → small-angle error, ωB−ωA rows + SPOOK bias.
|
|
423
|
+
Linear: `C·axis` error, vA−vB rows. `asWeld()` / `asHinge(axis)` /
|
|
424
|
+
`asPrismatic(axis)` presets. Verified: weld holds pose + orientation
|
|
425
|
+
against an off-centre torque; hinge swings about its free axis only
|
|
426
|
+
(locked axes < 0.02); prismatic slides along its one free axis,
|
|
427
|
+
locked on the others; all LOCKED-mode tests still green after the
|
|
428
|
+
frame-basis rewrite.
|
|
429
|
+
4. [ ] LIMITED + MOTOR (bounded rows) → doors, pistons, wheel
|
|
430
|
+
spin/drive, joint ROM.
|
|
431
|
+
5. [ ] SPRING (SPOOK soft) → suspension, bungees, soft ragdolls.
|
|
432
|
+
6. [ ] Cone-twist / swing-twist angular limits → ragdolls.
|
|
433
|
+
7. [ ] Vehicle layer — recommend a **raycast-vehicle controller**
|
|
434
|
+
(raycast + suspension force + tire friction; what most games ship)
|
|
435
|
+
on top of the queries, with simulated wheels via the 6-DOF as an
|
|
436
|
+
option. → vehicles.
|
|
437
|
+
8. [ ] Extras: pulley, gear, conveyor (contact surface-velocity),
|
|
438
|
+
breakable-joint flag.
|
|
439
|
+
|
|
440
|
+
Foundation gaps — both now closed:
|
|
441
|
+
- [x] **Island integration.** Jointed dynamic-dynamic bodies are
|
|
442
|
+
unioned into one island (`IslandBuilder` Pass 1b), so a chain /
|
|
443
|
+
ragdoll sleeps and wakes as a unit; `__wake_joints` propagates wake
|
|
444
|
+
across a joint when one side is awake and the other asleep
|
|
445
|
+
(e.g. a kinematic/motor driver pulling a sleeping chain). Verified:
|
|
446
|
+
a damped chain settles and both links sleep in one sleep group.
|
|
447
|
+
- [x] **Generation-checked body references.** `solve_joints`,
|
|
448
|
+
`IslandBuilder` Pass 1b and `__wake_joints` all gate on
|
|
449
|
+
`storage.is_valid(packedId)`, so a joint to an unlinked / slot-reused
|
|
450
|
+
body goes inert instead of attaching to the wrong body or crashing.
|
|
451
|
+
Verified: unlinking a jointed body leaves the joint inert and the
|
|
452
|
+
survivor free.
|
|
453
|
+
|
|
454
|
+
References: Catto / Box2D-v3 joint solvers; Jolt's `Constraint` base
|
|
455
|
+
(`SetupVelocityConstraint` / `WarmStartVelocityConstraint` /
|
|
456
|
+
`SolveVelocityConstraint` / `SolvePositionConstraint`); PhysX D6 /
|
|
457
|
+
ODE joint taxonomy.
|
|
359
458
|
|
|
360
459
|
### Stability
|
|
361
460
|
- [ ] **Closed-form triangle-vs-primitive solvers**
|
|
@@ -391,6 +490,24 @@ scaffolding is in place.
|
|
|
391
490
|
Out-of-scope unless / until SAB is universally usable.
|
|
392
491
|
|
|
393
492
|
### Features
|
|
493
|
+
- [ ] **Convex collision proxies for dynamic concave bodies.** The long-term
|
|
494
|
+
replacement for the interim per-substep concave re-detection (see
|
|
495
|
+
Limitations) — and how every major engine handles dynamic non-convex
|
|
496
|
+
shapes: collide a *few* convex pieces, never the raw concave mesh.
|
|
497
|
+
1. **3D convex hull builder** (meep has only 2D hulls today —
|
|
498
|
+
`core/geom/2d/convex-hull/`). A single hull of a mesh is one
|
|
499
|
+
collider / one broadphase leaf and covers the overwhelming majority
|
|
500
|
+
of dynamic objects (thrown props, debris). Pairs with the existing
|
|
501
|
+
"Convex hull shape + eigen-inertia" item below.
|
|
502
|
+
2. **Few-hull (V-HACD-style) approximate convex decomposition** for
|
|
503
|
+
shapes whose concavity matters (a cup, a chair): ~8–64 fat convex
|
|
504
|
+
hulls = 8–64 colliders, two orders of magnitude below a tet mesh.
|
|
505
|
+
Each hull is convex → stable contact feature → the TGS analytic refresh
|
|
506
|
+
is exact → no per-substep re-detection, no rocking. Granularity is the
|
|
507
|
+
whole point: collider/BVH-leaf count must stay small for an *awake*
|
|
508
|
+
dynamic body (the volumetric tet-mesher under `core/geom/3d/tetrahedra/`
|
|
509
|
+
is the wrong tool here — thousands of pieces — and belongs to a future
|
|
510
|
+
FEM/soft-body subsystem, not rigid collision).
|
|
394
511
|
- [ ] **Convex hull shape** with eigen-based principal-axes inertia
|
|
395
512
|
derivation. Hooks `matrix_eigenvalues_in_place` from the existing
|
|
396
513
|
linalg layer.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# REVIEW_002 — TGS solver, contact robustness, and constraints
|
|
2
|
+
|
|
3
|
+
Retrospective on the "competent → great" push: promoting the solver from
|
|
4
|
+
single-step PGS to substepped TGS, fixing the contact-robustness issues that
|
|
5
|
+
surfaced, and standing up the 6-DOF constraint subsystem. Companion to the
|
|
6
|
+
engine-comparison docs (`CANNON_REVIEW`, `RAPIER_REVIEW`, `JOLT_REVIEW`,
|
|
7
|
+
`BULLET_REVIEW`); this one is a build retrospective, not a comparison.
|
|
8
|
+
|
|
9
|
+
Scope: the work tracked in `PLAN.md` under "Solver quality" + "Constraints /
|
|
10
|
+
joints". What landed, the hard-won lessons, where the implementation
|
|
11
|
+
deviated from the original plan and why, and what's deferred.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## What landed
|
|
16
|
+
|
|
17
|
+
**TGS (Temporal Gauss-Seidel), Phases 1–3.** The solver is now a staged
|
|
18
|
+
pipeline driven by a substep loop:
|
|
19
|
+
`prepare → per substep [redetect-concave / refresh-convex → warm-start →
|
|
20
|
+
solve-velocity → solve-position] → restitution`. Defaults: 4 substeps, 4
|
|
21
|
+
velocity + 1 position iteration per substep. Split-impulse position
|
|
22
|
+
correction (pseudo-velocity folded into the pose, never into real velocity);
|
|
23
|
+
one-shot restitution; SPOOK compliance as the soft-constraint dial.
|
|
24
|
+
|
|
25
|
+
**Contact robustness.**
|
|
26
|
+
- Box-box SAT reference tie-break deadband → aligned cube stacks 4–10 high
|
|
27
|
+
settle to zero velocity and sleep.
|
|
28
|
+
- Per-substep concave re-detection → a dynamic concave mesh body (torus knot)
|
|
29
|
+
settles instead of rocking.
|
|
30
|
+
|
|
31
|
+
**Constraints — 6-DOF configurable joint** (PhysX D6 / Jolt SixDOF model):
|
|
32
|
+
one constraint type, each of 3 linear + 3 angular DOFs independently
|
|
33
|
+
locked/free/limited/spring/motor. Landed modes: LOCKED linear (ball-socket)
|
|
34
|
+
and LOCKED angular (hinge, weld). Solved as a parallel row set inside the
|
|
35
|
+
same substep loop; island-integrated; generation-checked against stale body
|
|
36
|
+
references.
|
|
37
|
+
|
|
38
|
+
Coverage went ~698 → 720+ physics tests, all green. Falling-tower bench
|
|
39
|
+
unchanged (~48 ms / 1000 active bodies).
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Hard-won lessons
|
|
44
|
+
|
|
45
|
+
These cost real debugging and are the reason the code looks the way it does.
|
|
46
|
+
|
|
47
|
+
1. **Per-substep warm-start is mandatory under substepping.** The first
|
|
48
|
+
substep attempt warm-started once per outer step and applied gravity per
|
|
49
|
+
substep. That mismatch — replaying a *full-frame* impulse against a
|
|
50
|
+
*single substep* of gravity — over-pushes resting contacts and **explodes**
|
|
51
|
+
deep stacks (a 10-cube stack hit 9 m of drift / 13 m/s). The fix: gravity
|
|
52
|
+
per substep **and** warm-start per substep, so each substep's gravity and
|
|
53
|
+
each substep's replayed impulse cancel exactly at rest. A resting body
|
|
54
|
+
holds at `v = 0`, `j_n ≈ m·g·h`.
|
|
55
|
+
|
|
56
|
+
2. **Restitution must gate on the running-max normal impulse, not the
|
|
57
|
+
end-of-loop value.** With per-substep warm-start, a transient collision's
|
|
58
|
+
accumulated `j_n` relaxes back to ~0 by the end of the loop (no sustained
|
|
59
|
+
load to hold it), so gating restitution on the final `j_n` silently killed
|
|
60
|
+
every bounce. Tracking `maxNormalImpulse` over the whole step (Box2D-v3's
|
|
61
|
+
trick) is what makes restitution fire.
|
|
62
|
+
|
|
63
|
+
3. **Analytic separation re-derivation is great for convex, wrong for
|
|
64
|
+
dynamic concave.** Re-deriving each substep's penetration from frozen
|
|
65
|
+
witness anchors + current pose (no narrowphase re-run) is cheap and exact
|
|
66
|
+
*while the contact feature is stable* — true for a convex primitive under
|
|
67
|
+
the small per-step motion. For a concave body rocking on a mesh, the
|
|
68
|
+
supporting triangle (and its normal) genuinely changes mid-step, so
|
|
69
|
+
freezing it pumps energy in. Resolution: convex pairs use the cheap
|
|
70
|
+
analytic refresh; concave-involved pairs re-run narrowphase geometry each
|
|
71
|
+
substep (`redetect_concave_contacts`). Hybrid, gated by collider convexity.
|
|
72
|
+
|
|
73
|
+
4. **The aligned-stack instability was not the solver.** Cube stacks crept
|
|
74
|
+
and toppled; the instinct was "solver convergence". Dumping
|
|
75
|
+
`box_box_manifold` across a settling pair showed the normal and contact
|
|
76
|
+
*count* were stable, but the *reference face* flip-flopped between A and B
|
|
77
|
+
each frame (their SAT overlaps are exactly equal for aligned boxes, so
|
|
78
|
+
float noise from a sub-degree wobble decided the winner). That reordered
|
|
79
|
+
the contacts → flipped the Gauss-Seidel sweep order → alternating bias →
|
|
80
|
+
creep. A 6-line relative+absolute tie-break deadband (bias ties toward the
|
|
81
|
+
earlier-tested axis) fixed stacks 4–10. **Lesson: diagnose the manifold
|
|
82
|
+
before blaming the solver.**
|
|
83
|
+
|
|
84
|
+
5. **Granularity is everything for dynamic concave.** The first instinct
|
|
85
|
+
("decompose the mesh into the exact Delaunay tets we already have") is
|
|
86
|
+
wrong: a Suzanne is ~8000 tets → ~8000 colliders → ~8000 dynamic-BVH
|
|
87
|
+
leaves refit every frame. Tet meshing is a *volumetric* (FEM/soft-body)
|
|
88
|
+
tool. Rigid collision wants the *fewest* convex pieces — a single convex
|
|
89
|
+
hull for most dynamic objects, or a few-hull (V-HACD-style) decomposition.
|
|
90
|
+
Recorded as the long-term path; the per-substep re-detection is the
|
|
91
|
+
interim.
|
|
92
|
+
|
|
93
|
+
6. **One configurable 6-DOF constraint beats N joint types.** The row math is
|
|
94
|
+
contact-shaped (Jacobian + effective mass + bias + bounds), so it slots
|
|
95
|
+
into the existing solver. Concrete joints become *config*: ball-socket =
|
|
96
|
+
lock 3 linear; hinge = lock 3 linear + 2 angular; weld = lock 6. This
|
|
97
|
+
minimises distinct code paths — the whole point.
|
|
98
|
+
|
|
99
|
+
7. **Sign conventions: linear A−B, angular B−A.** The linear rows use
|
|
100
|
+
"A minus B" (impulse +to A); the angular rows use "B minus A" (impulse
|
|
101
|
+
+to B). Each is internally consistent — the body-order difference just
|
|
102
|
+
folds the sign in. Derived carefully against the SPOOK Baumgarte target
|
|
103
|
+
(`dC/dt = vrel`); both worked first try once written down.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Deviations from the original plan (and why)
|
|
108
|
+
|
|
109
|
+
- **Joints run as a parallel row set, not by porting contacts onto a shared
|
|
110
|
+
constraint base.** The plan floated either. Porting the working,
|
|
111
|
+
well-tuned contact path carried regression risk for no immediate benefit;
|
|
112
|
+
running joints as a second row family in the same substep loop gets the
|
|
113
|
+
coupling (shared warm-start / islands / substep cadence) at far lower risk.
|
|
114
|
+
Unifying remains optional.
|
|
115
|
+
|
|
116
|
+
- **Convex contacts use analytic refresh, not per-substep match-and-merge.**
|
|
117
|
+
The plan said "substeps refresh contacts via match-and-merge". Analytic
|
|
118
|
+
re-derivation is cheaper and avoids manifold-lifecycle churn for the convex
|
|
119
|
+
majority; match-and-merge-style re-detection is reserved for the concave
|
|
120
|
+
minority (lesson 3).
|
|
121
|
+
|
|
122
|
+
- **Dynamic concave: per-substep re-detection now, convex proxies later** —
|
|
123
|
+
not the tet-compound idea first sketched (lesson 5).
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Deferred (with rationale)
|
|
128
|
+
|
|
129
|
+
- **Constraints**: prismatic (needs frame-basis linear rows — today's linear
|
|
130
|
+
locks use world axes, exact only when all 3 are locked), limits, motors,
|
|
131
|
+
springs (SPOOK soft), swing-twist (ragdoll cone), the raycast-vehicle
|
|
132
|
+
layer, and extras (pulley/gear/conveyor/breakable). Sequenced in `PLAN.md`.
|
|
133
|
+
- **Trajectory accuracy**: gravity is substepped, so ballistic integration is
|
|
134
|
+
at the substep rate — good. No higher-order integrator pursued.
|
|
135
|
+
- **Per-island parallel solve / CCD shape-cast / closed-form
|
|
136
|
+
triangle-vs-primitive remainder**: unchanged backlog items.
|
|
137
|
+
- **Convex hull builder + few-hull decomposition**: the production answer for
|
|
138
|
+
dynamic concave (lesson 5), not yet built.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Bottom line
|
|
143
|
+
|
|
144
|
+
The engine moved from "competent PGS" to a substepped TGS solver with stable
|
|
145
|
+
stacks, accurate restitution, robust mass ratios, settling dynamic concave
|
|
146
|
+
bodies, and a working 6-DOF constraint (ball-socket / hinge / weld) — i.e.
|
|
147
|
+
chains, ropes, and the structural joints for ragdolls and mechanisms. The
|
|
148
|
+
costliest mistakes were all about *substepping invariants* (warm-start and
|
|
149
|
+
restitution-gating cadence) and *not trusting assumptions* (the stack bug was
|
|
150
|
+
narrowphase, not the solver). The remaining constraint work is incremental
|
|
151
|
+
on a validated framework.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-degree-of-freedom mode for a 6-DOF constraint ({@link Joint }).
|
|
3
|
+
*
|
|
4
|
+
* A 6-DOF constraint has 3 linear DOFs (translation of body B's anchor frame
|
|
5
|
+
* relative to body A's, along A's frame axes) and 3 angular DOFs (relative
|
|
6
|
+
* rotation, swing-twist decomposed). Each DOF independently takes one of these
|
|
7
|
+
* modes — which is how a single configurable constraint expresses the whole
|
|
8
|
+
* joint taxonomy (PhysX D6 / Jolt SixDOF / Bullet Generic6DOF):
|
|
9
|
+
*
|
|
10
|
+
* - ball-socket = lock 3 linear
|
|
11
|
+
* - hinge = lock 3 linear + 2 angular
|
|
12
|
+
* - prismatic = lock 2 linear + 3 angular
|
|
13
|
+
* - weld = lock all 6
|
|
14
|
+
* - cone-twist = lock 3 linear + limit 3 angular
|
|
15
|
+
* - suspension = spring 1 linear + lock the rest
|
|
16
|
+
*
|
|
17
|
+
* Implementation lands mode-by-mode: LOCKED first (covers ball-socket / hinge
|
|
18
|
+
* / prismatic / weld), then LIMITED, SPRING, MOTOR.
|
|
19
|
+
*/
|
|
20
|
+
export type DofMode = number;
|
|
21
|
+
export namespace DofMode {
|
|
22
|
+
let LOCKED: number;
|
|
23
|
+
let FREE: number;
|
|
24
|
+
let LIMITED: number;
|
|
25
|
+
let SPRING: number;
|
|
26
|
+
let MOTOR: number;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=DofMode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DofMode.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/constraint/DofMode.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;sBAqBU,MAAM"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-degree-of-freedom mode for a 6-DOF constraint ({@link Joint}).
|
|
3
|
+
*
|
|
4
|
+
* A 6-DOF constraint has 3 linear DOFs (translation of body B's anchor frame
|
|
5
|
+
* relative to body A's, along A's frame axes) and 3 angular DOFs (relative
|
|
6
|
+
* rotation, swing-twist decomposed). Each DOF independently takes one of these
|
|
7
|
+
* modes — which is how a single configurable constraint expresses the whole
|
|
8
|
+
* joint taxonomy (PhysX D6 / Jolt SixDOF / Bullet Generic6DOF):
|
|
9
|
+
*
|
|
10
|
+
* - ball-socket = lock 3 linear
|
|
11
|
+
* - hinge = lock 3 linear + 2 angular
|
|
12
|
+
* - prismatic = lock 2 linear + 3 angular
|
|
13
|
+
* - weld = lock all 6
|
|
14
|
+
* - cone-twist = lock 3 linear + limit 3 angular
|
|
15
|
+
* - suspension = spring 1 linear + lock the rest
|
|
16
|
+
*
|
|
17
|
+
* Implementation lands mode-by-mode: LOCKED first (covers ball-socket / hinge
|
|
18
|
+
* / prismatic / weld), then LIMITED, SPRING, MOTOR.
|
|
19
|
+
*
|
|
20
|
+
* @author Alex Goldring
|
|
21
|
+
* @copyright Company Named Limited (c) 2026
|
|
22
|
+
* @enum {number}
|
|
23
|
+
*/
|
|
24
|
+
export const DofMode = {
|
|
25
|
+
/** Constrained to zero (relative position/angle held at the rest value). */
|
|
26
|
+
LOCKED: 0,
|
|
27
|
+
/** Unconstrained — the DOF moves freely. */
|
|
28
|
+
FREE: 1,
|
|
29
|
+
/** Free within `[lower, upper]`, constrained (one-sided) at the limits. */
|
|
30
|
+
LIMITED: 2,
|
|
31
|
+
/** A soft spring (stiffness + damping) pulling toward a rest target. */
|
|
32
|
+
SPRING: 3,
|
|
33
|
+
/** Driven toward a target velocity, bounded by a maximum force. */
|
|
34
|
+
MOTOR: 4,
|
|
35
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Solve every joint for one substep: recompute geometry at the current poses,
|
|
3
|
+
* replay the per-substep warm-start, and run `iters` velocity iterations of
|
|
4
|
+
* the locked linear DOFs.
|
|
5
|
+
*
|
|
6
|
+
* Called once per substep from `PhysicsSystem.fixedUpdate`, after the contact
|
|
7
|
+
* solve so the two share the substep / warm-start cadence.
|
|
8
|
+
*
|
|
9
|
+
* @param {Joint[]} joints live joints (sparse array; holes skipped)
|
|
10
|
+
* @param {PhysicsSystem} system reads `__bodies` / `__transforms` / index map
|
|
11
|
+
* @param {number} dt_sub substep size in seconds (the SPOOK gain is derived
|
|
12
|
+
* from it, matching the contact solver's per-substep position stiffness)
|
|
13
|
+
* @param {number} iters velocity iterations
|
|
14
|
+
*/
|
|
15
|
+
export function solve_joints(joints: Joint[], system: PhysicsSystem, dt_sub: number, iters: number): void;
|
|
16
|
+
//# sourceMappingURL=solve_constraints.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"solve_constraints.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/constraint/solve_constraints.js"],"names":[],"mappings":"AAyJA;;;;;;;;;;;;;GAaG;AACH,qCANW,OAAO,iCAEP,MAAM,SAEN,MAAM,QA8QhB"}
|