@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.
- package/package.json +1 -1
- 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 -578
- package/src/engine/physics/REVIEW_003.md +166 -0
- package/src/engine/physics/constraint/solve_constraints.d.ts +24 -2
- package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
- package/src/engine/physics/constraint/solve_constraints.js +402 -165
- package/src/engine/physics/ecs/Joint.d.ts +115 -0
- package/src/engine/physics/ecs/Joint.d.ts.map +1 -1
- package/src/engine/physics/ecs/Joint.js +168 -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/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,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
|
-
*
|
|
4
|
-
*
|
|
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":"
|
|
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"}
|