@woosh/meep-engine 2.140.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.
- 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/control/first-person/prototype_first_person_controller.js +5 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts.map +1 -1
- package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js +67 -42
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.d.ts +12 -22
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.d.ts.map +1 -1
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.js +340 -186
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.d.ts +44 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.d.ts.map +1 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.js +151 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.d.ts +14 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.d.ts.map +1 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.js +78 -0
- package/src/engine/physics/PLAN.md +705 -461
- package/src/engine/physics/REVIEW_002.md +151 -0
- package/src/engine/physics/REVIEW_003.md +166 -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 +38 -0
- package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -0
- package/src/engine/physics/constraint/solve_constraints.js +673 -0
- package/src/engine/physics/ecs/Joint.d.ts +294 -0
- package/src/engine/physics/ecs/Joint.d.ts.map +1 -0
- package/src/engine/physics/ecs/Joint.js +402 -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/narrowphase/ray_shapes.d.ts +66 -0
- package/src/engine/physics/narrowphase/ray_shapes.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/ray_shapes.js +187 -0
- package/src/engine/physics/narrowphase/refine_ray_concave.d.ts +16 -0
- package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/refine_ray_concave.js +145 -0
- package/src/engine/physics/narrowphase/refine_ray_hit.d.ts +39 -0
- package/src/engine/physics/narrowphase/refine_ray_hit.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/refine_ray_hit.js +78 -0
- package/src/engine/physics/queries/raycast.d.ts +11 -9
- package/src/engine/physics/queries/raycast.d.ts.map +1 -1
- package/src/engine/physics/queries/raycast.js +108 -159
- 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/src/engine/physics/vehicle/RaycastVehicle.d.ts +114 -0
- package/src/engine/physics/vehicle/RaycastVehicle.d.ts.map +1 -0
- package/src/engine/physics/vehicle/RaycastVehicle.js +333 -0
|
@@ -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,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.
|
|
@@ -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,38 @@
|
|
|
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;
|
|
23
|
+
/**
|
|
24
|
+
* Solve every joint for one substep: recompute geometry at the current poses,
|
|
25
|
+
* derive each DOF's row parameters from its mode, replay the per-substep
|
|
26
|
+
* warm-start, and run `iters` velocity iterations.
|
|
27
|
+
*
|
|
28
|
+
* Called once per substep from `PhysicsSystem.fixedUpdate`, after the contact
|
|
29
|
+
* solve so the two share the substep / warm-start cadence.
|
|
30
|
+
*
|
|
31
|
+
* @param {Joint[]} joints live joints (sparse array; holes skipped)
|
|
32
|
+
* @param {PhysicsSystem} system reads `__bodies` / `__transforms` / index map
|
|
33
|
+
* @param {number} dt_sub substep size in seconds (the SPOOK gain is derived
|
|
34
|
+
* from it, matching the contact solver's per-substep position stiffness)
|
|
35
|
+
* @param {number} iters velocity iterations
|
|
36
|
+
*/
|
|
37
|
+
export function solve_joints(joints: Joint[], system: PhysicsSystem, dt_sub: number, iters: number): void;
|
|
38
|
+
//# 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":"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"}
|