@woosh/meep-engine 2.141.0 → 2.142.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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/engine/control/first-person/prototype_first_person_controller.js +5 -0
  3. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts.map +1 -1
  4. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js +67 -42
  5. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.d.ts +12 -22
  6. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.d.ts.map +1 -1
  7. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.js +340 -186
  8. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.d.ts +44 -0
  9. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.d.ts.map +1 -0
  10. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.js +151 -0
  11. package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.d.ts +14 -0
  12. package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.d.ts.map +1 -0
  13. package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.js +78 -0
  14. package/src/engine/physics/PLAN.md +705 -578
  15. package/src/engine/physics/REVIEW_003.md +166 -0
  16. package/src/engine/physics/constraint/solve_constraints.d.ts +24 -2
  17. package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
  18. package/src/engine/physics/constraint/solve_constraints.js +402 -165
  19. package/src/engine/physics/ecs/Joint.d.ts +115 -0
  20. package/src/engine/physics/ecs/Joint.d.ts.map +1 -1
  21. package/src/engine/physics/ecs/Joint.js +168 -0
  22. package/src/engine/physics/narrowphase/ray_shapes.d.ts +66 -0
  23. package/src/engine/physics/narrowphase/ray_shapes.d.ts.map +1 -0
  24. package/src/engine/physics/narrowphase/ray_shapes.js +187 -0
  25. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts +16 -0
  26. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -0
  27. package/src/engine/physics/narrowphase/refine_ray_concave.js +145 -0
  28. package/src/engine/physics/narrowphase/refine_ray_hit.d.ts +39 -0
  29. package/src/engine/physics/narrowphase/refine_ray_hit.d.ts.map +1 -0
  30. package/src/engine/physics/narrowphase/refine_ray_hit.js +78 -0
  31. package/src/engine/physics/queries/raycast.d.ts +11 -9
  32. package/src/engine/physics/queries/raycast.d.ts.map +1 -1
  33. package/src/engine/physics/queries/raycast.js +108 -159
  34. package/src/engine/physics/vehicle/RaycastVehicle.d.ts +114 -0
  35. package/src/engine/physics/vehicle/RaycastVehicle.d.ts.map +1 -0
  36. package/src/engine/physics/vehicle/RaycastVehicle.js +333 -0
@@ -0,0 +1,166 @@
1
+ # REVIEW_003 — 6-DOF joints, full DOF-mode set, and the raycast vehicle
2
+
3
+ Retrospective on the constraints push: completing the 6-DOF joint across every
4
+ per-DOF mode (lock / free / limit / motor / spring), adding accurate cone-twist
5
+ range-of-motion, and building the raycast-vehicle layer on top. Companion to
6
+ `REVIEW_002` (TGS solver + contact robustness + the first joint modes); this one
7
+ covers `PLAN.md` → "Constraints / joints" phases 3–7.
8
+
9
+ Scope: what landed, the hard-won lessons, where the build deviated from the plan
10
+ and why, and what's deferred.
11
+
12
+ ---
13
+
14
+ ## What landed
15
+
16
+ **One mode-agnostic constraint row.** The joint solver resolves every DOF —
17
+ linear or angular — through a single scalar velocity row parameterised by
18
+ `(effective mass, bias, impulse clamp, regularisation γ)`. The `DofMode` only
19
+ chooses those parameters:
20
+
21
+ - **LOCKED** — bilateral, `bias = β/h · C`, clamp `(−∞, +∞)`. Ball-socket /
22
+ hinge / weld / prismatic are configurations of locked vs free DOFs.
23
+ - **LIMITED** — a *speculative (β=1) one-sided* velocity constraint: `bias =
24
+ (pos − bound)/h`, clamp one-sided. Slider end-stops and joint ROM.
25
+ - **MOTOR** — `bias = −targetVelocity`, clamp `±maxForce·h`. Powered doors,
26
+ wheel drive, pistons.
27
+ - **SPRING** — a regularised (compliant) row: `γ = 1/(h·(c+h·k))`, `bias =
28
+ (k/denom)·C`, softened mass `1/(K+γ)`, plus the `+γ·λ_accum` term. Suspension,
29
+ bungees, soft-return.
30
+
31
+ Adding each mode was a localised change to the row-parameter setup; the
32
+ warm-start and the iteration loop never changed shape. SPRING added exactly one
33
+ term (`+γ·λ_accum`, γ defaulting to 0), so LOCKED/LIMITED/MOTOR stayed
34
+ bit-for-bit identical.
35
+
36
+ **Frame-basis everything.** Both linear and angular rows resolve in frame A's
37
+ basis axes, clearing the early world-axis linear debt — the solver is fully
38
+ frame-relative.
39
+
40
+ **Cone-twist (swing-twist).** Opt-in `Joint.swingTwist` switches the angular
41
+ position measure from the per-axis small-angle vector to a swing-twist
42
+ decomposition (X = twist, Y/Z = swing) with *exact* angles, reusing the LIMITED
43
+ / SPRING rows. Wide ragdoll ROM now holds at the true angle.
44
+
45
+ **Raycast vehicle.** `vehicle/RaycastVehicle.js` — a chassis body + raycast
46
+ wheels, a controller on the public `raycast` + `applyForceAt`/`applyImpulseAt`
47
+ API. Suspension (spring+damper along the contact normal) + tyre friction
48
+ (lateral grip + drive/brake clamped to a friction circle μ·N) + steering. The
49
+ 6-DOF spring+motor is the alternative "simulated wheel" path.
50
+
51
+ Coverage ~720 → **732 physics tests**, all green. Joint tests 9 → 19; vehicle
52
+ tests +6; one swing-twist micro-bench (`test.skip`).
53
+
54
+ ---
55
+
56
+ ## Hard-won lessons
57
+
58
+ 1. **A limit must not bounce — velocity-Baumgarte injects energy.** The first
59
+ LIMITED cut waited for the DOF to cross a stop, then pushed back with a
60
+ Baumgarte bias. On a spin-into-stop with no opposing load the restorative
61
+ *velocity* it added didn't dissipate — the DOF coasted back through the whole
62
+ range. This is the same energy injection the contact solver avoids by using
63
+ split-impulse instead of velocity-Baumgarte. The fix: a **speculative (β=1)
64
+ one-sided velocity constraint** that removes exactly the approach velocity so
65
+ the DOF *lands on* the stop (zero penetration → zero rebound), self-gates to
66
+ a no-op away from the bound, and — because only the push-out side of the bias
67
+ is clamped — eases a teleport out instead of yanking. A loaded stop (gravity
68
+ on a slider) rests cleanly: per-substep load and per-substep warm-start
69
+ cancel, the resting-contact invariant again.
70
+
71
+ 2. **One row, many modes — but only because the *fixed point* is shared.**
72
+ Folding LOCKED and LIMITED (and later MOTOR/SPRING) into one solve worked
73
+ because they differ only in `(bias, clamp, γ)`. The proof it was safe was
74
+ mechanical: LOCKED's prior tests stayed green untouched after each
75
+ generalisation. Resisting the urge to special-case kept the loop tiny and
76
+ made every later mode a few lines.
77
+
78
+ 3. **Spring = regularisation, and it reproduces the analytic equilibrium.** The
79
+ SPOOK soft constraint (`γ` compliance + `(k/denom)·C` bias + softened mass)
80
+ isn't an approximation to dial in — a vertical strut settles at *exactly*
81
+ `m·g/k`, an undamped one oscillates and conserves energy, a stiff one stays
82
+ stable at 4 substeps. Getting the discrete coefficients right (Catto's
83
+ implicit-Euler form) made it behave like a real spring on the first run.
84
+
85
+ 4. **Swing-twist: exact position, first-order Jacobian.** The win is measuring
86
+ the limit *position* as a true angle (a 1.2 rad swing stops at 1.2, not the
87
+ small-angle proxy's ~1.287). The velocity Jacobian stays the cheap `ω·axis`
88
+ first-order one — PhysX/Jolt do the same. Keeping it opt-in means welds and
89
+ tight hinges don't pay the `atan2`/`sqrt` cost.
90
+
91
+ 5. **Benchmark before inlining — but inline when it's 5×.** Rather than assume,
92
+ the swing-twist decomposition was measured inlined (allocation-free scalar
93
+ math) vs the object-based `Quaternion.computeSwingAndTwist`: ~5× faster than
94
+ the method with reused out-params, ~10× vs naive fresh allocation (property
95
+ getters + `normalize` + a quaternion multiply + GC). In a per-substep
96
+ per-joint hot loop that margin justifies the duplicated math; the Quaternion
97
+ method stays for general callers. Making all three variants produce identical
98
+ results (matching sinks) was what made the comparison trustworthy.
99
+
100
+ 6. **The raycast vehicle is a force controller, and its failure modes are
101
+ physical.** Too much drive on a light, high-CG car wheelies and flips; too
102
+ much speed into a hard steer rolls it. These aren't solver bugs — they're the
103
+ model being honest. The tests use a low, heavy car and moderate inputs so the
104
+ *controller* (suspension equilibrium, grip, drive, steer) is what's under
105
+ test, not the operator. Spotting "the car launched, went airborne, then slept
106
+ on its roof" from `contacts=0, y=0.29, sleep=1` saved chasing a non-bug.
107
+
108
+ 7. **An external per-frame controller can't fully honour the substep
109
+ invariant.** Suspension is applied once per frame as a `dt`-force, but gravity
110
+ is per-substep — so a resting chassis carries a ~`g·h` end-of-step
111
+ velocity-sample artifact (it hovers position-stable to sub-cm, but the
112
+ sampled `v_y` isn't zero the way a resting *contact* body's is). Contacts
113
+ avoid this because their warm-start cancels each substep's gravity inside the
114
+ loop; a frame-level controller has no such hook. Documented, not hidden.
115
+
116
+ ---
117
+
118
+ ## Deviations from the plan (and why)
119
+
120
+ - **LIMITED is speculative-velocity, not split-impulse.** The plan sketched
121
+ limits as bounded rows; the bounce lesson pushed it to a β=1 speculative
122
+ velocity constraint, which needs no separate position pass and rests cleanly.
123
+ - **Swing-twist is inlined, not `Quaternion.computeSwingAndTwist`.** The note
124
+ said "reuse the Quaternion method"; the benchmark said inline (lesson 5). The
125
+ method stays for non-hot callers.
126
+ - **Swing-twist is opt-in (`swingTwist` flag), not the default measure.** Keeps
127
+ the cheaper small-angle path for welds/hinges and zero regression risk; the
128
+ flag is a genuine angular-mode choice (PhysX D6 / Jolt SwingTwist), not an
129
+ auto/override sentinel.
130
+ - **Vehicle is a standalone controller, not an ECS component+system.** It rides
131
+ the public query+force API exactly as the plan framed it ("on top of the
132
+ queries"), which keeps it decoupled and matches Bullet/Godot usage.
133
+
134
+ ---
135
+
136
+ ## Deferred (with rationale)
137
+
138
+ - **Raycast narrowphase refinement** — the suspension ray (and every shape
139
+ query) currently resolves to the leaf's inflated AABB: exact for axis-aligned
140
+ box ground, coarse for spheres/capsules/rotated boxes/meshes. This is the next
141
+ item; it sharpens vehicle ride height and all query accuracy. Tracked
142
+ separately.
143
+ - **Vehicle extras** — anti-roll bars, engine/gearbox curves, configurable
144
+ weight transfer / anti-squat, wheel-collision (vs raycast-only). The core is
145
+ in; these are tuning layers.
146
+ - **Elliptical/conical swing limit** — the cone is currently box-shaped
147
+ (independent Y/Z swing limits). A single combined swing-magnitude row would
148
+ give a circular/elliptical cone.
149
+ - **Per-substep joint warm-start for the vehicle** — would remove the `g·h`
150
+ velocity-sample artifact, at the cost of running the controller inside the
151
+ substep loop.
152
+ - Carried from REVIEW_002: convex-hull / few-hull decomposition for production
153
+ dynamic concave; per-island parallel solve; shape-cast CCD.
154
+
155
+ ---
156
+
157
+ ## Bottom line
158
+
159
+ The 6-DOF joint now spans the whole taxonomy from one row solve — ball-socket,
160
+ hinge, weld, prismatic, slider/ROM end-stops, powered doors/wheels, springs/
161
+ suspension, and accurate ragdoll cone-twist — and the raycast vehicle sits on
162
+ top of it and the query API. The costliest lessons were again about *not
163
+ injecting energy* (the limit bounce, mirroring the contact split-impulse
164
+ rationale) and *measuring before optimising* (the swing-twist inline bench). The
165
+ constraints plan (phases 1–7) is complete; the highest-leverage next step is
166
+ raycast narrowphase, which every query — and the vehicle — would sharpen.
@@ -1,7 +1,29 @@
1
+ /**
2
+ * Swing-twist decomposition of the relative rotation `qD = conj(qA)⊗qB`, giving
3
+ * the per-frame-axis angular positions used by wide-cone angular DOFs.
4
+ *
5
+ * Decomposes `qD = swing ⊗ twist`, with **twist** the rotation about frame
6
+ * axis X (the bone / twist axis) and **swing** the residual tilt (a rotation
7
+ * about an axis in the YZ plane that carries X to its new direction). Writes,
8
+ * into `out`:
9
+ * - `out[0]` = signed twist angle about X, in `(−π, π]` — *exact*, so a twist
10
+ * limit engages at the true angle, not the `2·sin(θ/2)` small-angle proxy;
11
+ * - `out[1]`, `out[2]` = the swing angle distributed over Y and Z
12
+ * (`φ·axis`, φ the true swing angle), so independent Y/Z swing limits form
13
+ * an accurate (box-shaped) cone that holds at large tilt.
14
+ * Reduces continuously to the small-angle vector near identity. Pure scratch /
15
+ * locals — no allocation, suitable for the per-substep hot loop. (The Quaternion
16
+ * class has an object-based `computeSwingAndTwist`; this is the inlined,
17
+ * allocation-free form — see the bench that justifies it.)
18
+ *
19
+ * @param {number} dx @param {number} dy @param {number} dz @param {number} dw qD
20
+ * @param {Float64Array} out length-3 destination (frame-axis positions)
21
+ */
22
+ export function swing_twist_error(dx: number, dy: number, dz: number, dw: number, out: Float64Array): void;
1
23
  /**
2
24
  * 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.
25
+ * derive each DOF's row parameters from its mode, replay the per-substep
26
+ * warm-start, and run `iters` velocity iterations.
5
27
  *
6
28
  * Called once per substep from `PhysicsSystem.fixedUpdate`, after the contact
7
29
  * solve so the two share the substep / warm-start cadence.
@@ -1 +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"}
1
+ {"version":3,"file":"solve_constraints.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/constraint/solve_constraints.js"],"names":[],"mappings":"AA+MA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,sCAHW,MAAM,MAAa,MAAM,MAAa,MAAM,MAAa,MAAM,OAC/D,YAAY,QAkCtB;AA0JD;;;;;;;;;;;;;GAaG;AACH,qCANW,OAAO,iCAEP,MAAM,SAEN,MAAM,QAsPhB"}