@woosh/meep-engine 2.139.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/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.d.ts +3 -3
- package/src/core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.d.ts.map +1 -1
- package/src/core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.js +4 -4
- package/src/{engine/physics/broadphase/aabb_transform_oriented.d.ts → core/geom/3d/aabb/aabb3_transform_oriented.d.ts} +2 -2
- package/src/core/geom/3d/aabb/aabb3_transform_oriented.d.ts.map +1 -0
- package/src/{engine/physics/broadphase/aabb_transform_oriented.js → core/geom/3d/aabb/aabb3_transform_oriented.js} +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/core/geom/3d/quaternion/quat3_to_matrix3.d.ts +54 -0
- package/src/core/geom/3d/quaternion/quat3_to_matrix3.d.ts.map +1 -0
- package/src/core/geom/3d/quaternion/quat3_to_matrix3.js +69 -0
- package/src/core/geom/3d/shape/AbstractShape3D.d.ts +24 -2
- package/src/core/geom/3d/shape/AbstractShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/AbstractShape3D.js +24 -1
- package/src/core/geom/3d/shape/HeightMapShape3D.d.ts +148 -0
- package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -0
- package/src/core/geom/3d/shape/HeightMapShape3D.js +451 -0
- package/src/core/geom/3d/shape/MeshShape3D.d.ts +210 -0
- package/src/core/geom/3d/shape/MeshShape3D.d.ts.map +1 -0
- package/src/core/geom/3d/shape/MeshShape3D.js +593 -0
- package/src/core/geom/3d/shape/TransformedShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/TransformedShape3D.js +46 -2
- package/src/core/geom/3d/shape/Triangle3D.d.ts +95 -0
- package/src/core/geom/3d/shape/Triangle3D.d.ts.map +1 -0
- package/src/core/geom/3d/shape/Triangle3D.js +318 -0
- package/src/core/geom/3d/shape/UnionShape3D.js +13 -0
- package/src/core/geom/3d/shape/shape_mesh_from_geometry.d.ts +30 -0
- package/src/core/geom/3d/shape/shape_mesh_from_geometry.d.ts.map +1 -0
- package/src/core/geom/3d/shape/shape_mesh_from_geometry.js +64 -0
- package/src/core/geom/3d/tetrahedra/prototype_tetrahedrize_mesh.js +9 -11
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.d.ts +28 -0
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.d.ts.map +1 -0
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.js +48 -0
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_improve_quality.d.ts.map +1 -1
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_improve_quality.js +40 -18
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.d.ts +9 -5
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.d.ts.map +1 -1
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.js +38 -10
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.d.ts +14 -5
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.d.ts.map +1 -1
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.js +47 -5
- package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.d.ts +19 -0
- package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.js +75 -13
- package/src/core/geom/3d/triangle/v3_compute_triangle_normal.d.ts +2 -2
- package/src/core/geom/3d/triangle/v3_compute_triangle_normal.d.ts.map +1 -1
- package/src/core/geom/3d/triangle/v3_compute_triangle_normal.js +1 -1
- package/src/core/geom/vec3/v3_dot_array_array.d.ts +3 -3
- package/src/core/geom/vec3/v3_dot_array_array.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_dot_array_array.js +2 -2
- package/src/core/geom/vec3/v3_negate_array.d.ts +3 -3
- package/src/core/geom/vec3/v3_negate_array.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_negate_array.js +2 -2
- package/src/core/geom/vec3/v3_quat3_apply.d.ts +29 -0
- package/src/core/geom/vec3/v3_quat3_apply.d.ts.map +1 -0
- package/src/core/geom/vec3/v3_quat3_apply.js +39 -0
- package/src/core/geom/vec3/v3_quat3_apply_inverse.d.ts +30 -0
- package/src/core/geom/vec3/v3_quat3_apply_inverse.d.ts.map +1 -0
- package/src/core/geom/vec3/v3_quat3_apply_inverse.js +41 -0
- package/src/core/geom/vec3/v3_triple_cross_product.d.ts +32 -0
- package/src/core/geom/vec3/v3_triple_cross_product.d.ts.map +1 -0
- package/src/core/geom/vec3/v3_triple_cross_product.js +45 -0
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +16 -3
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerController.js +211 -211
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +72 -8
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +37 -5
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +101 -3
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +1789 -1416
- package/src/engine/control/first-person/TODO.md +173 -127
- package/src/engine/control/first-person/abilities/Slide.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/Slide.js +9 -1
- package/src/engine/control/first-person/prototype_first_person_controller.js +88 -2
- package/src/engine/control/first-person/test/buildTestPlayer.d.ts.map +1 -1
- package/src/engine/control/first-person/test/buildTestPlayer.js +9 -1
- package/src/engine/graphics/geometry/CapsuleGeometry.d.ts +42 -0
- package/src/engine/graphics/geometry/CapsuleGeometry.d.ts.map +1 -0
- package/src/engine/graphics/geometry/CapsuleGeometry.js +171 -0
- package/src/engine/physics/BULLET_REVIEW.md +945 -0
- package/src/engine/physics/CANNON_REVIEW.md +1300 -0
- package/src/engine/physics/JOLT_REVIEW.md +913 -0
- package/src/engine/physics/PLAN.md +578 -236
- package/src/engine/physics/RAPIER_REVIEW.md +934 -0
- package/src/engine/physics/REVIEW_001_ACTION_PLAN.md +642 -0
- package/src/engine/physics/REVIEW_002.md +151 -0
- package/src/engine/physics/broadphase/compute_fat_world_aabb.js +2 -2
- 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/contact/ManifoldStore.d.ts +83 -10
- package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
- package/src/engine/physics/contact/ManifoldStore.js +608 -499
- package/src/engine/physics/ecs/ColliderObserverSystem.d.ts +2 -2
- package/src/engine/physics/ecs/ColliderObserverSystem.d.ts.map +1 -1
- 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 +180 -20
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +1423 -1159
- 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.js +1 -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 +30 -6
- 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 +44 -18
- package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts +6 -6
- package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts.map +1 -1
- package/src/engine/physics/gjk/expanding_polytope_algorithm.js +68 -22
- package/src/engine/physics/gjk/gjk.d.ts +28 -2
- package/src/engine/physics/gjk/gjk.d.ts.map +1 -1
- package/src/engine/physics/gjk/gjk.js +421 -378
- package/src/engine/physics/gjk/minkowski_support.d.ts +37 -0
- package/src/engine/physics/gjk/minkowski_support.d.ts.map +1 -0
- package/src/engine/physics/gjk/minkowski_support.js +75 -0
- package/src/engine/physics/gjk/mpr.d.ts +56 -0
- package/src/engine/physics/gjk/mpr.d.ts.map +1 -0
- package/src/engine/physics/gjk/mpr.js +344 -0
- package/src/engine/physics/inertia/world_inverse_inertia.d.ts +20 -5
- package/src/engine/physics/inertia/world_inverse_inertia.d.ts.map +1 -1
- package/src/engine/physics/inertia/world_inverse_inertia.js +36 -38
- package/src/engine/physics/integration/integrate_position.d.ts +25 -7
- package/src/engine/physics/integration/integrate_position.d.ts.map +1 -1
- package/src/engine/physics/integration/integrate_position.js +43 -12
- package/src/engine/physics/integration/integrate_velocity.d.ts +30 -0
- package/src/engine/physics/integration/integrate_velocity.d.ts.map +1 -1
- package/src/engine/physics/integration/integrate_velocity.js +82 -1
- 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/PosedShape.d.ts +0 -8
- package/src/engine/physics/narrowphase/PosedShape.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/PosedShape.js +28 -30
- package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/box_box_manifold.js +140 -18
- package/src/engine/physics/narrowphase/box_triangle_contact.d.ts +30 -0
- package/src/engine/physics/narrowphase/box_triangle_contact.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/box_triangle_contact.js +811 -0
- package/src/engine/physics/narrowphase/capsule_contacts.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/capsule_contacts.js +10 -56
- package/src/engine/physics/narrowphase/capsule_triangle_contact.d.ts +71 -0
- package/src/engine/physics/narrowphase/capsule_triangle_contact.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/capsule_triangle_contact.js +375 -0
- package/src/engine/physics/narrowphase/compute_penetration.d.ts +91 -0
- package/src/engine/physics/narrowphase/compute_penetration.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/compute_penetration.js +396 -0
- package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.d.ts +35 -0
- package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.js +80 -0
- package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.d.ts +31 -0
- package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.js +55 -0
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts +42 -0
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.js +204 -0
- package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.d.ts +42 -0
- package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.js +94 -0
- package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.d.ts +37 -0
- package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.js +37 -0
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts +41 -2
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/narrowphase_step.js +1497 -382
- package/src/engine/physics/narrowphase/sphere_box_contact.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/sphere_box_contact.js +16 -23
- package/src/engine/physics/narrowphase/sphere_triangle_contact.d.ts +48 -0
- package/src/engine/physics/narrowphase/sphere_triangle_contact.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/sphere_triangle_contact.js +143 -0
- package/src/engine/physics/queries/overlap_shape.d.ts +51 -0
- package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -0
- package/src/engine/physics/queries/overlap_shape.js +183 -0
- package/src/engine/physics/queries/shape_cast.d.ts +56 -0
- package/src/engine/physics/queries/shape_cast.d.ts.map +1 -0
- package/src/engine/physics/queries/shape_cast.js +387 -0
- package/src/engine/physics/solver/solve_contacts.d.ts +146 -32
- package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
- package/src/engine/physics/solver/solve_contacts.js +809 -223
- package/src/engine/physics/broadphase/aabb_transform_oriented.d.ts.map +0 -1
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.d.ts +0 -20
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.d.ts.map +0 -1
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.js +0 -83
|
@@ -0,0 +1,945 @@
|
|
|
1
|
+
# Bullet vs. meep rigid-body physics — deep technical review
|
|
2
|
+
|
|
3
|
+
Subject: `engine/physics/` (pure-JS rigid-body engine, ECS-attached) vs.
|
|
4
|
+
[Bullet Physics 3](https://github.com/bulletphysics/bullet3) (C++).
|
|
5
|
+
|
|
6
|
+
Bullet is a mature, multi-platform physics SDK with a notoriously deep class
|
|
7
|
+
hierarchy and broad simulation feature set. meep is a deliberately scoped
|
|
8
|
+
pure-JS engine: deterministic same-runtime, no WASM/SIMD, no shared-memory
|
|
9
|
+
workers, designed around an active-list / mostly-sleeping model with up to
|
|
10
|
+
millions of bodies. The two projects share core algorithmic ideas (Catto-style
|
|
11
|
+
sequential impulse, `btPersistentManifold`-style 4-point manifold cache,
|
|
12
|
+
SAT+clipping for box-box, GJK+EPA for general convex pairs) but diverge sharply
|
|
13
|
+
on dispatch shape, hierarchy depth, and feature surface.
|
|
14
|
+
|
|
15
|
+
This review is organised in four sections, per the original brief.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 1. Overall architecture
|
|
20
|
+
|
|
21
|
+
### 1.1 Pipeline shape
|
|
22
|
+
|
|
23
|
+
| Phase | meep (`PhysicsSystem.fixedUpdate`) | Bullet (`btDiscreteDynamicsWorld::stepSimulation`) |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| Integrate velocity | `integrate_velocity` — semi-implicit Euler, gravity + accum force/torque, implicit damping | `predictUnconstrainMotion` — same scheme, explicit damping with `exp` |
|
|
26
|
+
| Broadphase refit | `compute_fat_world_aabb` + `node_move_aabb`, per active collider leaf | `updateAabbs` + `btDbvtBroadphase::setAabb`, deferred update queue |
|
|
27
|
+
| Pair generation | `generate_pairs` — leaf-by-leaf query of both BVHs, dedup via manifold "touched" bit | `calculateOverlappingPairs` — incremental, persists pair cache across frames, percentage-revalidated each tick |
|
|
28
|
+
| Narrowphase | `narrowphase_step` — closed-form fast paths then GJK+EPA fallback | `btCollisionDispatcher` — virtual algorithm matrix per `(shape_type_A, shape_type_B)` lookup |
|
|
29
|
+
| Island build | `IslandBuilder` — union-find rebuilt every frame from touched non-sensor contacts | `btSimulationIslandManager` — also union-find based, but on persistent pairs |
|
|
30
|
+
| Constraint solve | `solve_contacts` (Catto SI, 10 iters, in-place per-island) | `btSequentialImpulseConstraintSolver`, 10 iters default, optional split-impulse position iters |
|
|
31
|
+
| Position integrate | `integrate_position` + quaternion integrator | `integrateTransforms`, with optional CCD shape-cast for fast objects |
|
|
32
|
+
| Sleep test | `__sleep_test` — per-island atomic | Per-body `m_deactivationTime` accumulator with `gDeactivationTime` (default 2 s) |
|
|
33
|
+
| Manifold diff → events | `diff_manifolds` + `__dispatch_contact_events` (Begin / Stay / End signals) | `internalTickCallback` user-driven, or `contactStartedCallback` / `contactEndedCallback` globals |
|
|
34
|
+
| Frame roll | `manifolds.advance_frame` — touched → prev, evict on grace | `btPersistentManifold::refreshContactPoints` — per-manifold, by world-distance |
|
|
35
|
+
|
|
36
|
+
**Where the two are most similar**: both run a velocity-only solver iteration
|
|
37
|
+
sweep with warm-starting on a persistent manifold cache keyed on body-pair, and
|
|
38
|
+
both build islands via union-find so the solver iterates one connected
|
|
39
|
+
component at a time.
|
|
40
|
+
|
|
41
|
+
**Where meep diverges deliberately**:
|
|
42
|
+
|
|
43
|
+
1. **No split-impulse architecture.** Bullet's solver runs separate position-
|
|
44
|
+
correction iterations (`solveGroupCacheFriendlySplitImpulseIterations`)
|
|
45
|
+
over a *pseudo-velocity* that does not contaminate the real velocity.
|
|
46
|
+
meep folds position correction into the velocity bias via Baumgarte
|
|
47
|
+
(`solve_contacts.js:444`). `PLAN.md` already lists this as the
|
|
48
|
+
architectural blocker for TGS substepping; the trade-off is documented
|
|
49
|
+
and intentional.
|
|
50
|
+
2. **Stateless pair regeneration vs. Bullet's persistent overlapping-pair
|
|
51
|
+
cache.** Bullet keeps `m_overlappingPairCache` between frames and only
|
|
52
|
+
re-validates a percentage of pairs each step (`gOverlappingPairsPercentageToUpdate`).
|
|
53
|
+
meep regenerates every pair every step from the BVH leaves
|
|
54
|
+
(`generate_pairs.js:50-100`), relying on the `ManifoldStore`'s touched-bit
|
|
55
|
+
for dedup. Simpler, less per-frame state, slightly higher recurrent cost.
|
|
56
|
+
3. **Two BVHs (static + dynamic) rather than Bullet's two-stage single
|
|
57
|
+
structure with "moving objects → fixed set" promotion.** meep classifies
|
|
58
|
+
at insert (`PhysicsSystem.js:325`); Bullet shuffles between stages over
|
|
59
|
+
time. meep's two-tree model is cleaner; Bullet's is closer to Jolt.
|
|
60
|
+
4. **Atomic per-island sleep with sleep-group chain wakeup.** Bullet sleeps
|
|
61
|
+
per body and bodies in the same island wake one-by-one as contacts
|
|
62
|
+
propagate via the broadphase over multiple frames. meep threads a
|
|
63
|
+
circular `sleep_group_next` / `sleep_group_prev` doubly-linked list
|
|
64
|
+
through all members of an atomically-sleeping island
|
|
65
|
+
(`PhysicsSystem.js:754-786`) so any wake on any member wakes the whole
|
|
66
|
+
chain in one call. This is meep's most distinctive architectural choice
|
|
67
|
+
and the PLAN.md rationale (the 100-block-stack wake wave) is well-founded.
|
|
68
|
+
|
|
69
|
+
### 1.2 Data layout
|
|
70
|
+
|
|
71
|
+
meep uses a deliberate Structure-of-Arrays (`BodyStorage` — six typed arrays
|
|
72
|
+
indexed by body slot) and an external sparse `__bodies[]` / `__transforms[]` /
|
|
73
|
+
`__body_collider_lists[]` indexed the same way. The hot iteration is
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
for (let i = 0; i < count; i++) {
|
|
77
|
+
const idx = this.storage.awake_at(i);
|
|
78
|
+
const rb = this.__bodies[idx];
|
|
79
|
+
...
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
— sequential awake-list, sparse sidetable dereference. The manifold store has
|
|
84
|
+
its own SoA `Float64Array` plus a `Uint32Array` meta buffer (`ManifoldStore.js`,
|
|
85
|
+
lines 96-115). Solver scratch is a per-call typed array
|
|
86
|
+
(`solver/solve_contacts.js:88-91`).
|
|
87
|
+
|
|
88
|
+
Bullet uses standard AoS objects (`btRigidBody`, `btCollisionObject`,
|
|
89
|
+
`btPersistentManifold`) plus a separate "solver body" SoA (`btSolverBody`,
|
|
90
|
+
`btSolverConstraint`) inside the constraint solver only. The
|
|
91
|
+
narrowphase / broadphase paths are AoS.
|
|
92
|
+
|
|
93
|
+
meep is **more uniformly SoA** end-to-end. That's the right call for JS — V8
|
|
94
|
+
treats typed arrays as backing for off-heap buffers and the hot loops here
|
|
95
|
+
avoid the property-load cost that would dominate equivalent Bullet-style AoS
|
|
96
|
+
JS code.
|
|
97
|
+
|
|
98
|
+
### 1.3 Broadphase
|
|
99
|
+
|
|
100
|
+
| Aspect | meep | Bullet (`btDbvtBroadphase`) |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| Tree count | 2 (`staticBvh`, `dynamicBvh`) | 2 (`m_sets[0]` dynamic / `m_sets[1]` fixed) |
|
|
103
|
+
| Fat-AABB constant | `FAT_LINEAR = 0.05` + 2 × velocity × dt | `gDbvtMargin = 0.05`, velocity-scaled internally |
|
|
104
|
+
| Pair generation | Re-query every active leaf every frame | Incremental: only re-validate a percentage |
|
|
105
|
+
| Pair store | `PairUint32Map` (Robin Hood, Fibonacci hash) → manifold slot | `btHashedOverlappingPairCache` |
|
|
106
|
+
|
|
107
|
+
Bullet's two-set design migrates bodies between sets as they go inactive — a
|
|
108
|
+
true two-stage tree. meep's two-tree split is by *body kind* (static or
|
|
109
|
+
dynamic) at insert. The Bullet model is closer to Jolt's static/dynamic split
|
|
110
|
+
than to meep's, but in practice meep's is simpler and matches PLAN.md's stated
|
|
111
|
+
Jolt-inspired rationale.
|
|
112
|
+
|
|
113
|
+
**Fat-AABB strategy** in `broadphase/compute_fat_world_aabb.js:34`:
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
const speed = Math.sqrt(vx * vx + vy * vy + vz * vz);
|
|
117
|
+
const pad = FAT_LINEAR + FAT_VELOCITY_MULTIPLIER * speed * dt;
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This is Box2D-style padding (constant + velocity look-ahead). Bullet's
|
|
121
|
+
equivalent is `setAabbForceUpdate` with `gDbvtVelocityScale = 1.0` and
|
|
122
|
+
`gDbvtExtraMargin = 0.05`. Both engines bound the fat AABB by *predicted next
|
|
123
|
+
position* + slack — same idea, slightly different constants.
|
|
124
|
+
|
|
125
|
+
### 1.4 Island structure
|
|
126
|
+
|
|
127
|
+
meep's `IslandBuilder` is union-find with deterministic union-by-min-index +
|
|
128
|
+
path halving (`island/union_find.js:65-76`), producing CSR-style output sorted
|
|
129
|
+
ascending within and across islands. Static / kinematic bodies are **constraint
|
|
130
|
+
anchors only** — they do not enlarge islands, so two disjoint piles on the
|
|
131
|
+
same floor remain separate islands. This is identical to Bullet's
|
|
132
|
+
`btSimulationIslandManager` (which also marks static bodies as
|
|
133
|
+
`ISLAND_SLEEPING` and treats them as graph-cut points), and to Jolt.
|
|
134
|
+
|
|
135
|
+
The CSR output design (`body_offsets[]` + `body_data[]`,
|
|
136
|
+
`contact_offsets[]` + `contact_data[]`) is cleaner than Bullet's, which uses
|
|
137
|
+
a per-body `m_islandTag1` field and a `getIslandId()` post-pass — meep's is
|
|
138
|
+
both more cache-friendly to iterate and simpler to reason about for parallel
|
|
139
|
+
solving in the future.
|
|
140
|
+
|
|
141
|
+
**Determinism guarantee**: meep's "smallest-body-index wins as root" gives a
|
|
142
|
+
canonical island id that's independent of pair encounter order. Bullet's
|
|
143
|
+
island ids depend on `findUnion()` call order, so the same scene can produce
|
|
144
|
+
different island indices across runs — fine for Bullet's use case, but
|
|
145
|
+
problematic for a deterministic replay engine. PLAN.md is right to call this
|
|
146
|
+
out explicitly.
|
|
147
|
+
|
|
148
|
+
### 1.5 Solver architecture
|
|
149
|
+
|
|
150
|
+
| | meep | Bullet |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| Solver | Sequential Impulse (Catto / Box2D) | Sequential Impulse (Catto / Box2D) |
|
|
153
|
+
| Iterations | 10 (`DEFAULT_VELOCITY_ITERATIONS`) | 10 (`m_numIterations`) |
|
|
154
|
+
| Warm-start | Yes — `j_n / j_t1 / j_t2` stored per contact, replayed pre-iter | Yes — `cp.m_appliedImpulse` scaled by `m_warmstartingFactor = 0.85` |
|
|
155
|
+
| Warm-start factor | 1.0 (no scale-down) | 0.85 |
|
|
156
|
+
| Friction model | 2-axis tangent + **disk-cone clamp** (`friction_cone_clamp`) | 2-axis (optional) with **box clamp** (`m_lowerLimit = -mu * j_n`) |
|
|
157
|
+
| Restitution | Velocity-bias inside the iteration loop, clamped above threshold (1.0 m/s) | One-shot velocity target injected into iter-0 RHS |
|
|
158
|
+
| Position correction | Baumgarte velocity bias `β/dt * pen`, capped at 3 m/s | Optional split-impulse pseudo-velocity pass (default on) |
|
|
159
|
+
| Per-island iteration | Yes (`solve_contacts` walks `islands.contact_offsets`) | Yes (per-island group inside `solveGroupCacheFriendly...`) |
|
|
160
|
+
|
|
161
|
+
**Notable substantive differences:**
|
|
162
|
+
|
|
163
|
+
- meep's friction model is **better** than Bullet's default. Bullet's
|
|
164
|
+
`m_lowerLimit = -mu * j_n` independent-axis clamp produces the classic
|
|
165
|
+
"friction stronger along one tangent than the other" anisotropy on
|
|
166
|
+
diagonally-sliding bodies. meep's 2-D disk projection
|
|
167
|
+
(`solver/friction_cone.js:25-36`) is the correct Coulomb cone projection
|
|
168
|
+
and matches Box2D Lite's recipe.
|
|
169
|
+
|
|
170
|
+
- meep's **restitution is a velocity bias inside the iter loop**, which is
|
|
171
|
+
what Box2D Lite does. Bullet runs it as a one-shot target velocity in
|
|
172
|
+
iter-0's RHS. meep's approach is documented in `PLAN.md` as one of the
|
|
173
|
+
three problems blocking TGS, because the inside-the-loop bias interacts
|
|
174
|
+
badly with the `j_n ≥ 0` warm-start clamp on second and subsequent
|
|
175
|
+
substeps. Bullet's one-shot approach is robust under substepping; meep's
|
|
176
|
+
isn't. This is the right call-out — PLAN.md is fully aware of the
|
|
177
|
+
consequence.
|
|
178
|
+
|
|
179
|
+
- meep **does not have split-impulse**. Bullet's
|
|
180
|
+
`solveGroupCacheFriendlySplitImpulseIterations` runs a separate
|
|
181
|
+
pseudo-velocity pass for position correction. meep's Baumgarte bias is
|
|
182
|
+
capped at `MAX_BAUMGARTE_BIAS = 3` m/s (`solve_contacts.js:62-63`) as a
|
|
183
|
+
guard against EPA-degenerate-depth blow-ups, which is sound but does mean
|
|
184
|
+
deep penetrations resolve more slowly. The cap is conservative — Bullet's
|
|
185
|
+
default `erp` is also 0.2 but it gets applied via split impulse so
|
|
186
|
+
saturation is less common.
|
|
187
|
+
|
|
188
|
+
### 1.6 Sleep system
|
|
189
|
+
|
|
190
|
+
- **meep**: per-island atomic. `__sleep_test` (`PhysicsSystem.js:942-1006`)
|
|
191
|
+
computes `max(|v|² + |ω|²)` across an island; when all members have been
|
|
192
|
+
below the threshold for `sleepTimeThreshold` (default 0.5 s), the entire
|
|
193
|
+
island sleeps in one frame via `__atomic_sleep_island_range`, which threads
|
|
194
|
+
the circular sleep-group chain.
|
|
195
|
+
- **Bullet**: per-body. `btRigidBody::updateDeactivation` increments
|
|
196
|
+
`m_deactivationTime` per body; sleeps when ≥ `gDeactivationTime` (default
|
|
197
|
+
2 s). Islands wake one body at a time as contacts propagate.
|
|
198
|
+
|
|
199
|
+
meep's atomic sleep is **architecturally superior for stacks** — a 100-block
|
|
200
|
+
stack hit at the base wakes the whole stack in one frame, where Bullet would
|
|
201
|
+
take ~100 frames of broadphase propagation. The cost is more bookkeeping (the
|
|
202
|
+
sleep-group chain), and the precondition is the per-island design described
|
|
203
|
+
above. PLAN.md explains the chain mechanism well.
|
|
204
|
+
|
|
205
|
+
### 1.7 Threading model
|
|
206
|
+
|
|
207
|
+
Both engines are single-threaded by default. Bullet has the optional
|
|
208
|
+
`btMultiThreadedDynamicsWorld` + worker-pool wrapper; meep is explicitly
|
|
209
|
+
single-threaded with `SharedArrayBuffer` cited in PLAN.md as a future seam.
|
|
210
|
+
This is in scope of the documented design bets and not in scope for review.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## 2. Specific algorithms and tradeoffs
|
|
215
|
+
|
|
216
|
+
For each touchpoint: meep file:line | Bullet header/cpp | tradeoff.
|
|
217
|
+
|
|
218
|
+
### 2.1 Broadphase (DBVT)
|
|
219
|
+
|
|
220
|
+
- **meep**: `core/bvh2/bvh3/BVH.js` (external to physics) + `broadphase/generate_pairs.js:50-100`. Two BVHs, per-leaf re-query each frame, manifold-store dedup.
|
|
221
|
+
- **Bullet**: `BulletCollision/BroadphaseCollision/btDbvtBroadphase.{h,cpp}` + `btDbvt.{h,cpp}`. One DBVT with two `m_sets` (dynamic + fixed), incremental update queue, `m_deferedcollide`.
|
|
222
|
+
|
|
223
|
+
| Trade-off | meep | Bullet |
|
|
224
|
+
|---|---|---|
|
|
225
|
+
| Per-frame cost | O(active leaves × log N) every frame | O(active leaves × log N) amortised over multiple frames |
|
|
226
|
+
| State complexity | Low — just two trees + manifold store | Higher — deferred queue, stage promotion, pair cache |
|
|
227
|
+
| Determinism | Strong — leaf order is deterministic | Weaker — depends on internal queue eviction order |
|
|
228
|
+
|
|
229
|
+
meep gives up Bullet's amortised cost to gain determinism and simplicity.
|
|
230
|
+
Reasonable trade-off given PLAN.md's design priorities.
|
|
231
|
+
|
|
232
|
+
### 2.2 GJK
|
|
233
|
+
|
|
234
|
+
- **meep**: `gjk/gjk.js`, 64 iterations, simplex stored as Float64Array slots.
|
|
235
|
+
- **Bullet**: `BulletCollision/NarrowPhaseCollision/btGjkPairDetector.cpp` + `btVoronoiSimplexSolver.cpp`. 1000-iteration cap. Cached separating axis across frames (`m_cachedSeparatingAxis`).
|
|
236
|
+
|
|
237
|
+
Major differences:
|
|
238
|
+
|
|
239
|
+
1. **Frame coherence.** Bullet caches the last separating axis per
|
|
240
|
+
`btGjkPairDetector` instance and uses it as the next step's initial search
|
|
241
|
+
direction — typically converging in 1-3 iterations on quiescent pairs.
|
|
242
|
+
meep starts every call with `(1, 0, 0)` (`gjk/gjk.js:41`) regardless of
|
|
243
|
+
prior frames' result. **This is a real performance gap on stable
|
|
244
|
+
contacts** — Bullet's cached-axis trick gives roughly an order of
|
|
245
|
+
magnitude fewer support calls per frame in steady state.
|
|
246
|
+
2. **Simplex management.** meep uses Kevin Moran's slot-shuffling simplex
|
|
247
|
+
(`gjk.js:163-265`), which is direct and readable. Bullet uses
|
|
248
|
+
`btVoronoiSimplexSolver` which computes the closest point to the origin
|
|
249
|
+
within the simplex and prunes by Voronoi region — more complex but more
|
|
250
|
+
numerically robust.
|
|
251
|
+
3. **Convergence criterion.** Bullet's "previousSquaredDistance −
|
|
252
|
+
squaredDistance ≤ ε × previousSquaredDistance" is a relative-improvement
|
|
253
|
+
check. meep uses absolute fall-off (the new support point's dot product
|
|
254
|
+
with the search direction is < 0, i.e. it didn't pass the origin). Both
|
|
255
|
+
correct; Bullet's is slightly more conservative on near-touching pairs.
|
|
256
|
+
|
|
257
|
+
### 2.3 EPA
|
|
258
|
+
|
|
259
|
+
- **meep**: `gjk/expanding_polytope_algorithm.js`, 64 iterations, 64-face cap, 32-loose-edge cap. Linear scan to find closest face each iter.
|
|
260
|
+
- **Bullet**: `BulletCollision/NarrowPhaseCollision/btGjkEpa2.cpp`. 255-iteration cap, doubly-linked-list hull with neighbour pointers per face, recursive horizon expansion.
|
|
261
|
+
|
|
262
|
+
Comparison points:
|
|
263
|
+
|
|
264
|
+
| | meep | Bullet |
|
|
265
|
+
|---|---|---|
|
|
266
|
+
| Face data structure | Flat Float64Array `[ax,ay,az, bx,by,bz, cx,cy,cz, nx,ny,nz]` × 64 | Linked list of `sFace` (3 verts, 3 neighbour pointers) |
|
|
267
|
+
| Closest-face lookup | Linear scan over `num_faces` every iter | Linear scan over hull list every iter |
|
|
268
|
+
| Horizon edge handling | Loose-edge list — for each removed face, three edges added; matching edges in the list cancel | Recursive `expand()` walks neighbour pointers — O(visible-face count), no extra storage |
|
|
269
|
+
| Iteration cap | 64 | 255 |
|
|
270
|
+
| Smooth-shape degenerate handling | Documented in code, returns closest-face approximation | `eStatus::Degenerated` set, fallback returns best-so-far |
|
|
271
|
+
|
|
272
|
+
meep's flat-array approach is **easier to read and trivially deterministic**.
|
|
273
|
+
Bullet's linked-list-with-neighbours is asymptotically better when the
|
|
274
|
+
horizon is large (visible-face traversal is O(neighbours) instead of O(faces))
|
|
275
|
+
but the constant factor difference is negligible at the face counts we
|
|
276
|
+
actually see (≤ 64).
|
|
277
|
+
|
|
278
|
+
**The biggest issue in meep's EPA** is the same one Bullet has — neither
|
|
279
|
+
engine converges cleanly on smooth shapes, because both rely on the polytope
|
|
280
|
+
having flat faces to expand onto. meep mitigates with closed-form fast paths
|
|
281
|
+
for sphere/box/capsule and the documented MPR alternative; Bullet does the
|
|
282
|
+
same with closed-form pair handlers and `btMprPenetration`.
|
|
283
|
+
|
|
284
|
+
### 2.4 Box-box manifold
|
|
285
|
+
|
|
286
|
+
- **meep**: `narrowphase/box_box_manifold.js`. SAT over 15 axes, Sutherland-Hodgman face clip, deepest-then-spread reduction to 4 contacts.
|
|
287
|
+
- **Bullet**: `BulletCollision/CollisionDispatch/btBoxBoxDetector.cpp`. SAT over 15 axes (Open Dynamics Engine port), custom `intersectRectQuad2()` polygon-clip, `cullPoints2()` reduction (deepest + angular distribution to 4).
|
|
288
|
+
|
|
289
|
+
Both engines:
|
|
290
|
+
- Use SAT with the canonical 15 axes (3 face normals A, 3 face normals B, 9
|
|
291
|
+
edge-cross pairs).
|
|
292
|
+
- Project the incident face into the reference face's 2-D frame and clip.
|
|
293
|
+
- Reduce to ≤ 4 contacts.
|
|
294
|
+
|
|
295
|
+
Differences:
|
|
296
|
+
|
|
297
|
+
| | meep | Bullet |
|
|
298
|
+
|---|---|---|
|
|
299
|
+
| Clipping | Sutherland-Hodgman, 4 successive axis-aligned half-planes | `intersectRectQuad2` — same idea, hand-unrolled |
|
|
300
|
+
| Reduction | Deepest at slot 0, then greedy maximise-min-distance | Deepest at slot 0, then 3 by angular position around the centroid |
|
|
301
|
+
| Edge-edge case | Single midpoint contact (PLAN.md known limitation) | Single closest-edge-point contact via `dLineClosestApproach` |
|
|
302
|
+
|
|
303
|
+
meep's edge-edge fallback is **less accurate** than Bullet's. PLAN.md lists
|
|
304
|
+
"edge-edge multi-point manifold" as a backlog item; the cheaper improvement
|
|
305
|
+
would be to first match Bullet's "closest points on the two edges" formula —
|
|
306
|
+
a few lines of segment-segment math — rather than the midpoint
|
|
307
|
+
(`box_box_manifold.js:221-230`). This would close most of the gap without
|
|
308
|
+
needing the full multi-point edge-pair contact.
|
|
309
|
+
|
|
310
|
+
meep's spread-by-max-min-distance reduction is a *better* default than
|
|
311
|
+
Bullet's angular distribution when the clipped polygon is convex but not
|
|
312
|
+
near-circular — both work, but meep's is theoretically slightly more
|
|
313
|
+
solver-stable for elongated contact regions. The two are practically
|
|
314
|
+
indistinguishable on cube-on-cube.
|
|
315
|
+
|
|
316
|
+
### 2.5 Sphere-sphere, sphere-box, capsule pairs
|
|
317
|
+
|
|
318
|
+
- meep has closed-form solvers for each (`sphere_sphere_contact.js`,
|
|
319
|
+
`sphere_box_contact.js`, `capsule_contacts.js`).
|
|
320
|
+
- Bullet, surprisingly, **does not** dispatch sphere-sphere and sphere-box to
|
|
321
|
+
closed-form paths in the default `btConvexConvexAlgorithm` — they go
|
|
322
|
+
through GJK ([source](https://github.com/bulletphysics/bullet3/blob/master/src/BulletCollision/CollisionDispatch/btConvexConvexAlgorithm.cpp)).
|
|
323
|
+
Bullet only specialises capsule-capsule and capsule-sphere
|
|
324
|
+
(`btCapsuleCapsuleAlgorithm`, added in Bullet 2.75 for ragdoll
|
|
325
|
+
performance).
|
|
326
|
+
|
|
327
|
+
This is a **real advantage** for meep: GJK for sphere-sphere does maybe 10
|
|
328
|
+
iterations of cross-products + dot-products to compute what one
|
|
329
|
+
`d² < (rA+rB)²` test gets you in closed form. meep's `sphere_sphere_contact`
|
|
330
|
+
is fewer than 20 numeric ops; Bullet's GJK route is closer to 200. The same
|
|
331
|
+
factor-of-10 gap applies to sphere-box.
|
|
332
|
+
|
|
333
|
+
meep's `sphere_box_contact` (`narrowphase/sphere_box_contact.js`) correctly
|
|
334
|
+
handles the centre-inside-box singular case with smallest-overlap face
|
|
335
|
+
tie-breaking, with X > Y > Z deterministic tie-break — robust and
|
|
336
|
+
deterministic.
|
|
337
|
+
|
|
338
|
+
### 2.6 Sequential impulse solver
|
|
339
|
+
|
|
340
|
+
- **meep**: `solver/solve_contacts.js`, 10 iters default, Baumgarte bias inside the velocity loop, disk-cone friction clamp, warm-start factor = 1.0.
|
|
341
|
+
- **Bullet**: `BulletDynamics/ConstraintSolver/btSequentialImpulseConstraintSolver.cpp`, 10 iters default, split-impulse pseudo-velocity pass for position, box-clamp friction (default) or 2-direction with optional cone, warm-start factor = 0.85.
|
|
342
|
+
|
|
343
|
+
Side-by-side:
|
|
344
|
+
|
|
345
|
+
| Concern | meep | Bullet |
|
|
346
|
+
|---|---|---|
|
|
347
|
+
| Warm-start `j ≥ 0` clamp | Yes (`solve_contacts.js:533`) | Yes |
|
|
348
|
+
| Tangent basis construction | Least-aligned-axis (`build_tangents`, lines 115-143) | `btPlaneSpace1` (essentially the same thing) OR velocity-aligned if `SOLVER_DISABLE_VELOCITY_DEPENDENT_FRICTION_DIRECTION` is off |
|
|
349
|
+
| Bias clamp | `MAX_BAUMGARTE_BIAS = 3` m/s | `m_maxErrorReduction = 20` per Bullet defaults |
|
|
350
|
+
| Restitution threshold | 1.0 m/s | 2.0 m/s (`m_restingContactRestitutionThreshold = 2`) |
|
|
351
|
+
| Friction coefficient combine | Geometric mean | Default: `mat0.friction * mat1.friction` (product) |
|
|
352
|
+
| Restitution combine | Max | Default: `mat0.restitution * mat1.restitution` (product) |
|
|
353
|
+
|
|
354
|
+
meep's combine functions are **closer to Unity / PhysX defaults** (geometric
|
|
355
|
+
mean for friction, max for restitution). Bullet's products are a quirk that
|
|
356
|
+
produces low-friction surfaces from `(0.5, 0.5)` pairs (gives 0.25). meep's
|
|
357
|
+
choice is more conventional.
|
|
358
|
+
|
|
359
|
+
### 2.7 Manifold caching
|
|
360
|
+
|
|
361
|
+
- **meep**: `contact/ManifoldStore.js`. Stride-13 contact record (positions A, positions B, normal, depth, three impulses). 4 contacts max. Grace-2 frame eviction. Per-frame touched flag. PairUint32Map for `(idA, idB) → slot`.
|
|
362
|
+
- **Bullet**: `btPersistentManifold.h/cpp`. Stride is larger (local/world positions on both bodies, normal, two lateral friction directions, two lateral impulses, distance, applied impulses, lifetime counter, partIds, friction-anchor flag). 4 contacts max. Distance-threshold eviction via `validContactDistance()`. No grace.
|
|
363
|
+
|
|
364
|
+
meep's manifold record is **leaner**, intentionally — it stores only what's
|
|
365
|
+
needed for the solver (world positions, normal, depth, accumulated impulses).
|
|
366
|
+
Bullet's adds:
|
|
367
|
+
|
|
368
|
+
1. **Local-space contact positions** on each body. Bullet uses these to
|
|
369
|
+
re-validate contacts as bodies rotate without rerunning narrowphase
|
|
370
|
+
(`refreshContactPoints` projects local-to-world and tests world-distance).
|
|
371
|
+
meep doesn't do incremental refresh — narrowphase runs every frame for
|
|
372
|
+
every touched manifold. This is fine for meep's pipeline because the
|
|
373
|
+
broadphase is also rerun every frame; Bullet's persistence is what lets
|
|
374
|
+
it amortise narrowphase cost over multiple frames.
|
|
375
|
+
|
|
376
|
+
2. **Cached lateral friction directions** per contact. Bullet's tangent
|
|
377
|
+
basis is constructed once and persisted; meep recomputes via
|
|
378
|
+
`build_tangents` every iter per contact. The recompute cost is ~6
|
|
379
|
+
multiplies + 1 normalise per tangent → negligible for low contact counts;
|
|
380
|
+
becomes measurable at 1000+ active contacts.
|
|
381
|
+
|
|
382
|
+
3. **Lifetime counter** per contact. Bullet uses it for `m_frictionAnchor`
|
|
383
|
+
handling. meep doesn't have friction anchors yet.
|
|
384
|
+
|
|
385
|
+
meep's eviction strategy (`DEFAULT_GRACE = 2`, in `ManifoldStore.js:59`) lets
|
|
386
|
+
a one-frame separation survive — jitter-friendly. Bullet's distance-threshold
|
|
387
|
+
eviction (`m_contactBreakingThreshold`, world-space) is more physically
|
|
388
|
+
grounded but requires tuning. meep's frame-count grace is simpler.
|
|
389
|
+
|
|
390
|
+
### 2.8 Sleep
|
|
391
|
+
|
|
392
|
+
- **meep**: per-island atomic, threshold `0.01` on `|v|² + |ω|²`, time `0.5 s`. Sleep-group chain for atomic wake.
|
|
393
|
+
- **Bullet**: per-body, threshold `gLinearSleepingThreshold = 0.8`, `gAngularSleepingThreshold = 1.0`, time `gDeactivationTime = 2.0`.
|
|
394
|
+
|
|
395
|
+
meep's threshold (0.01) is *much* tighter than Bullet's (0.8 and 1.0). Both
|
|
396
|
+
are reasonable — Bullet uses 0.8 as a wide noise floor; meep is tuned for
|
|
397
|
+
clean game-physics inputs. Bullet's deactivation time (2 s) is also longer
|
|
398
|
+
than meep's (0.5 s).
|
|
399
|
+
|
|
400
|
+
### 2.9 CCD
|
|
401
|
+
|
|
402
|
+
- **meep**: speculative margin via fat AABB only (PLAN.md known limitation).
|
|
403
|
+
- **Bullet**: optional per-body `m_ccdMotionThreshold` + `m_ccdSweptSphereRadius` triggers `btSubsimplexConvexCast` conservative advancement.
|
|
404
|
+
|
|
405
|
+
This is a real gap (PLAN.md acknowledges) but the documented use case — 1km
|
|
406
|
+
drop onto a 1cm floor losing 180/1000 bodies to tunnelling — is exactly the
|
|
407
|
+
scenario where shape-cast CCD would help. Backlog item is well-scoped.
|
|
408
|
+
|
|
409
|
+
### 2.10 Concave / triangle-mesh narrowphase
|
|
410
|
+
|
|
411
|
+
- **meep**: `narrowphase/narrowphase_step.js:368-578` + `narrowphase/decomposition/`. Decompose concave shape into triangles in convex's AABB → per-triangle GJK + EPA → one-sided face-normal rejection → contact-normal dedup. **Known limitation: `Triangle3D`'s support function is degenerate along its face-normal axis.**
|
|
412
|
+
- **Bullet**: `BulletCollision/CollisionShapes/btBvhTriangleMeshShape` + `btBvhTriangleMeshShape::processAllTriangles` callback → per-triangle GJK via `btConvexTriangleCallback`. Same per-triangle approach in spirit; Bullet doesn't suffer the degenerate-support issue because `btTriangleShape` extends `btPolyhedralConvexShape` with a thin extruded depth, not a true zero-volume primitive.
|
|
413
|
+
|
|
414
|
+
meep's documented `Triangle3D` degenerate support is a real correctness
|
|
415
|
+
issue — and the PLAN.md "closed-form triangle-vs-primitive solvers" backlog
|
|
416
|
+
item is the right fix. The half-space pre-test in `compute_penetration`
|
|
417
|
+
(noted in PLAN.md) is a clean workaround for that one entry point.
|
|
418
|
+
|
|
419
|
+
A subtle observation about meep's concave dispatch: the sign-check at
|
|
420
|
+
`narrowphase_step.js:506` validates the MTV against `(convex_centre −
|
|
421
|
+
triangle_centroid)`, then rejects if the MTV opposes the triangle's
|
|
422
|
+
outward face normal (one-sided). This is a **better-than-Bullet** rejection
|
|
423
|
+
strategy. Bullet's per-triangle GJK has no such face-normal sanity check, so
|
|
424
|
+
a body pushed *through* a wall by external forces can resolve as if it's
|
|
425
|
+
contacting from the inside, producing the classic "popping out the wrong
|
|
426
|
+
side" artifact. meep explicitly avoids this. Strong design.
|
|
427
|
+
|
|
428
|
+
### 2.11 MPR
|
|
429
|
+
|
|
430
|
+
- **meep**: `gjk/mpr.js` — XenoCollide / Snethen GDC 2009 implementation, MTV output format matches EPA (drop-in compatible). 64-iter cap. Not wired into the dispatch.
|
|
431
|
+
- **Bullet**: `btMprPenetration.h` — also XenoCollide / Snethen. 1000-iter cap (`BT_MPR_MAX_ITERATIONS`), 1e-6 tolerance.
|
|
432
|
+
|
|
433
|
+
Both implementations are textbook XenoCollide. Bullet's portal-discovery
|
|
434
|
+
loop handles `BT_MPR_TOLERANCE` (1e-6) more aggressively than meep's
|
|
435
|
+
`MPR_TOLERANCE` (1e-4) — meep's tolerance is the order of typical
|
|
436
|
+
narrowphase noise, which is fine. meep's also handles the
|
|
437
|
+
`MPR_EPSILON = 1e-10` collinearity check inline.
|
|
438
|
+
|
|
439
|
+
The MPR result-convention match-EPA design choice in meep is *excellent* —
|
|
440
|
+
it means the swap from EPA to MPR is a one-line change at any call site.
|
|
441
|
+
Bullet's MPR is wired into a separate code path with its own result format,
|
|
442
|
+
so adopting it as a primary narrowphase would be a bigger refactor.
|
|
443
|
+
|
|
444
|
+
### 2.12 Queries (raycast / shape-cast / overlap)
|
|
445
|
+
|
|
446
|
+
- **meep**: `queries/raycast.js` (broadphase-only), `queries/shape_cast.js`
|
|
447
|
+
(broadphase swept-AABB + GJK bisection ToI + EPA normal at kiss point),
|
|
448
|
+
`queries/overlap_shape.js` (broadphase + per-candidate GJK with concave
|
|
449
|
+
triangle-decomposition routing).
|
|
450
|
+
- **Bullet**: `btCollisionWorld::rayTest` (broadphase + per-shape narrowphase
|
|
451
|
+
ray test), `btCollisionWorld::convexSweepTest`
|
|
452
|
+
(`btSubsimplexConvexCast` / `btContinuousConvexCollision` per candidate),
|
|
453
|
+
`btCollisionWorld::contactTest` (broadphase + per-pair manifold collection).
|
|
454
|
+
|
|
455
|
+
meep's shape-cast implementation uses **GJK bisection on the time interval**
|
|
456
|
+
plus EPA at the kiss point to recover the surface normal. Bullet's
|
|
457
|
+
`btSubsimplexConvexCast` uses Brian Mirtich's conservative advancement,
|
|
458
|
+
which is faster (no bisection) but produces less precise normals at the
|
|
459
|
+
moment of contact. meep's approach is more conservative; the bisection's
|
|
460
|
+
extra GJK calls per cast are negligible for the use case (player movement,
|
|
461
|
+
AOE casts).
|
|
462
|
+
|
|
463
|
+
The `shape_cast` "rerun EPA at best_t on the winning candidate to recover
|
|
464
|
+
true normal" trick is a clean way to get accurate contact normals — better
|
|
465
|
+
than what most game engines bother with. The fallback to `-ray.direction`
|
|
466
|
+
only on EPA degeneracies (NaN / zero depth) is sound.
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## 3. In-depth comparison: 8 consequential touchpoints
|
|
471
|
+
|
|
472
|
+
For each: same quantity? bug or simplification? specific opportunity.
|
|
473
|
+
|
|
474
|
+
### 3.1 GJK — frame coherence
|
|
475
|
+
|
|
476
|
+
**meep** (`gjk/gjk.js:41`): every call seeds the initial direction with `(1, 0, 0)`:
|
|
477
|
+
|
|
478
|
+
```js
|
|
479
|
+
minkowski_support(simplex, 6, shape_a, shape_b, 1, 0, 0);
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
**Bullet** (`btGjkPairDetector.cpp`): caches the converged separating axis
|
|
483
|
+
per-pair:
|
|
484
|
+
|
|
485
|
+
```cpp
|
|
486
|
+
m_cachedSeparatingAxis.setValue(0, 1, 0); // first frame
|
|
487
|
+
// ... after convergence:
|
|
488
|
+
m_cachedSeparatingAxis = newCachedSeparatingAxis;
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
**Diagnosis**: not a bug — meep is computing the same answer. But a
|
|
492
|
+
**performance opportunity worth pursuing**: caching the last successful
|
|
493
|
+
search direction (or, equivalently, the EPA-converged MTV normal) on the
|
|
494
|
+
manifold cache slot and seeding the next frame's GJK with it would reduce
|
|
495
|
+
GJK iterations from 10-20 down to 3-5 on quiescent contacts. Storage would
|
|
496
|
+
be 3 floats per manifold slot — bumping `CONTACT_STRIDE` from 13 to 16, or
|
|
497
|
+
adding a per-slot meta field.
|
|
498
|
+
|
|
499
|
+
**Improvement**: store last separating axis on manifold acquire/touch path,
|
|
500
|
+
seed `simplex_buf` from it on next call. Estimated 30-50% reduction in GJK
|
|
501
|
+
support calls in stable scenes.
|
|
502
|
+
|
|
503
|
+
### 3.2 Box-box edge-edge contact
|
|
504
|
+
|
|
505
|
+
**meep** (`narrowphase/box_box_manifold.js:221-230`):
|
|
506
|
+
|
|
507
|
+
```js
|
|
508
|
+
if (best_source >= 6) {
|
|
509
|
+
const mx = (ax + bx) * 0.5;
|
|
510
|
+
const my = (ay + by) * 0.5;
|
|
511
|
+
const mz = (az + bz) * 0.5;
|
|
512
|
+
out[3] = 1;
|
|
513
|
+
out[4] = mx; out[5] = my; out[6] = mz;
|
|
514
|
+
out[7] = mx; out[8] = my; out[9] = mz;
|
|
515
|
+
out[10] = best_overlap;
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
**Bullet** (`btBoxBoxDetector.cpp`): finds the closest points on the two
|
|
521
|
+
edges via `dLineClosestApproach` and uses their midpoint as the contact —
|
|
522
|
+
much more accurate.
|
|
523
|
+
|
|
524
|
+
**Diagnosis**: simplification. Both engines emit one contact for edge-edge,
|
|
525
|
+
but Bullet's at the actual closest-edge-point pair; meep's at the body
|
|
526
|
+
centres' midpoint. For a stack of skewed cubes (45° offset), meep's contact
|
|
527
|
+
point is consistently off by ~0.5 × half-extent — solver still resolves but
|
|
528
|
+
with a wrong lever arm.
|
|
529
|
+
|
|
530
|
+
**Improvement**: replace the midpoint with the segment-segment closest-pair
|
|
531
|
+
formula on the two contributing edges (`best_source - 6` gives the
|
|
532
|
+
edge-pair index; the edges are `(ta[i], length 2*ahx/y/z)` and
|
|
533
|
+
`(tb[j], length 2*bhx/y/z)`). The helper already exists in the codebase as
|
|
534
|
+
`core/geom/3d/line/line3_closest_points_segment_segment.js` per PLAN.md
|
|
535
|
+
"Bonus utilities". This is a 10-line change that closes the documented
|
|
536
|
+
limitation.
|
|
537
|
+
|
|
538
|
+
### 3.3 EPA — closest-face linear scan
|
|
539
|
+
|
|
540
|
+
**meep** (`gjk/expanding_polytope_algorithm.js:115-127`):
|
|
541
|
+
|
|
542
|
+
```js
|
|
543
|
+
let min_dist = v3_dot_array_array(faces, 0, faces, 3 * 3);
|
|
544
|
+
closest_face = 0;
|
|
545
|
+
for (let i = 1; i < num_faces; i++) {
|
|
546
|
+
const dist = v3_dot_array_array(faces, i * FACE_ELEMENT_COUNT, faces, i * FACE_ELEMENT_COUNT + 3 * 3);
|
|
547
|
+
if (dist < min_dist) { min_dist = dist; closest_face = i; }
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
**Bullet** (`btGjkEpa2.cpp::findbest()`): same linear scan over hull faces.
|
|
552
|
+
|
|
553
|
+
**Diagnosis**: both engines scan linearly. This is fine for 64-face caps;
|
|
554
|
+
a priority queue would only pay off above ~200 faces. No improvement
|
|
555
|
+
needed.
|
|
556
|
+
|
|
557
|
+
Minor: meep also re-computes the face's distance-to-origin (`v3_dot` of the
|
|
558
|
+
face's vertex against its own normal) on every iteration. Caching it on
|
|
559
|
+
the face record would skip an N-multiply pass. Not significant at 64 faces.
|
|
560
|
+
|
|
561
|
+
### 3.4 EPA — `debugger` statement on zero-normal degeneracy
|
|
562
|
+
|
|
563
|
+
**meep** (`gjk/expanding_polytope_algorithm.js:135-137`):
|
|
564
|
+
|
|
565
|
+
```js
|
|
566
|
+
if (search_dir_x === 0 && search_dir_y === 0 && search_dir_z === 0) {
|
|
567
|
+
debugger;
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
**Diagnosis**: **bug / inappropriate left-in debug aid**. A live `debugger;`
|
|
572
|
+
statement in a hot path will halt the entire app under devtools and is dead
|
|
573
|
+
code under production V8 (no observable effect, but signals "this code
|
|
574
|
+
shouldn't reach here, but didn't decide what to do if it does"). The
|
|
575
|
+
function continues with `(0, 0, 0)` as search direction, which produces a
|
|
576
|
+
support point identical to whatever the support function returns for a
|
|
577
|
+
zero direction — undefined behaviour per the `support()` contract.
|
|
578
|
+
|
|
579
|
+
**Improvement**: replace with a defensive bail — return whatever the current
|
|
580
|
+
`closest_face` approximation says (mirror the iteration-cap exit at line 338).
|
|
581
|
+
The closest-face approximation is what the narrowphase consumer already
|
|
582
|
+
expects on degenerate EPA, and the upstream `if (!(depth > 0))` check filters
|
|
583
|
+
it out anyway.
|
|
584
|
+
|
|
585
|
+
### 3.5 Solver bias clamp
|
|
586
|
+
|
|
587
|
+
**meep** (`solver/solve_contacts.js:62-63, 444-449`):
|
|
588
|
+
|
|
589
|
+
```js
|
|
590
|
+
const MAX_BAUMGARTE_BIAS = 3;
|
|
591
|
+
// ...
|
|
592
|
+
if (depth > PENETRATION_SLOP) {
|
|
593
|
+
bias = -BAUMGARTE_BETA / dt * (depth - PENETRATION_SLOP);
|
|
594
|
+
if (bias < -MAX_BAUMGARTE_BIAS) bias = -MAX_BAUMGARTE_BIAS;
|
|
595
|
+
}
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
**Bullet** (no equivalent absolute bias clamp): Bullet relies on
|
|
599
|
+
**split-impulse** to prevent position correction contaminating real
|
|
600
|
+
velocity. The bias-cap meep applies is a workaround for not having split
|
|
601
|
+
impulse.
|
|
602
|
+
|
|
603
|
+
**Diagnosis**: deliberate simplification. The cap (3 m/s) is well-motivated
|
|
604
|
+
(prevents EPA-degenerate-depth blow-ups from launching bodies) but it does
|
|
605
|
+
cap real depth resolution at `cap × dt / β = 3 × 0.016 / 0.2 = 0.24 m/step`
|
|
606
|
+
worth of penetration. Past that, the body has to wait for narrowphase to
|
|
607
|
+
keep producing a contact across multiple steps. For normal game-physics
|
|
608
|
+
inputs this is fine.
|
|
609
|
+
|
|
610
|
+
**Improvement**: this becomes moot when split-impulse / TGS lands per the
|
|
611
|
+
backlog. Until then, the cap is the right choice.
|
|
612
|
+
|
|
613
|
+
### 3.6 Warm-start factor
|
|
614
|
+
|
|
615
|
+
**meep** (`solver/solve_contacts.js`): warm-start replays the cached impulse
|
|
616
|
+
verbatim:
|
|
617
|
+
|
|
618
|
+
```js
|
|
619
|
+
const j_n = data[off + 10];
|
|
620
|
+
const j_t1 = data[off + 11];
|
|
621
|
+
const j_t2 = data[off + 12];
|
|
622
|
+
// ... applied as-is
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
**Bullet**: scales by `m_warmstartingFactor = 0.85`:
|
|
626
|
+
|
|
627
|
+
```cpp
|
|
628
|
+
solverConstraint.m_appliedImpulse = cp.m_appliedImpulse * infoGlobal.m_warmstartingFactor;
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
**Diagnosis**: trade-off. Bullet's 0.85 factor is a well-known empirical
|
|
632
|
+
choice — it slightly under-shoots so the first iteration of the solver
|
|
633
|
+
*adds* impulse rather than potentially overshooting and having to subtract.
|
|
634
|
+
meep's 1.0 (full replay) is more aggressive — gives faster steady-state
|
|
635
|
+
convergence on stable stacks but can briefly over-impulse on transient
|
|
636
|
+
contacts that have changed normal/depth significantly between frames.
|
|
637
|
+
|
|
638
|
+
**Improvement candidate**: parameterise the factor (default still 1.0, or
|
|
639
|
+
move to 0.85 as a softer recommendation). Low-priority; both choices are
|
|
640
|
+
defensible. Worth measuring on the existing benchmark scenes.
|
|
641
|
+
|
|
642
|
+
### 3.7 Tangent basis construction
|
|
643
|
+
|
|
644
|
+
**meep** (`solver/solve_contacts.js:115-143`):
|
|
645
|
+
|
|
646
|
+
```js
|
|
647
|
+
function build_tangents(out, off, nx, ny, nz) {
|
|
648
|
+
// Pick the world axis least aligned with n.
|
|
649
|
+
const ax = nx < 0 ? -nx : nx;
|
|
650
|
+
// ...
|
|
651
|
+
if (ax <= ay && ax <= az) { rx = 1; ry = 0; rz = 0; }
|
|
652
|
+
else if (ay <= az) { rx = 0; ry = 1; rz = 0; }
|
|
653
|
+
else { rx = 0; ry = 0; rz = 1; }
|
|
654
|
+
// t1 = normalize(n × r)
|
|
655
|
+
// ...
|
|
656
|
+
}
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
Recomputed every iter per contact.
|
|
660
|
+
|
|
661
|
+
**Bullet**: `btPlaneSpace1(n, t1, t2)` — equivalent algorithm, but
|
|
662
|
+
**cached on the contact point** between solver iterations (and between
|
|
663
|
+
frames). Optional velocity-aligned variant
|
|
664
|
+
(`SOLVER_DISABLE_VELOCITY_DEPENDENT_FRICTION_DIRECTION`).
|
|
665
|
+
|
|
666
|
+
**Diagnosis**: meep recomputes tangents per contact per iteration. The
|
|
667
|
+
algorithm itself is identical and correct, but the recompute is wasted
|
|
668
|
+
work. At 10 iterations × N contacts per island, the recompute cost is
|
|
669
|
+
10× higher than necessary.
|
|
670
|
+
|
|
671
|
+
**Improvement**: hoist `build_tangents` to the pre-step loop
|
|
672
|
+
(`solve_contacts.js` lines 379-465 are already the pre-step), store the
|
|
673
|
+
two tangents in the `pre[]` scratch (slots 6-11 already do this — verify
|
|
674
|
+
they aren't being overwritten per iter; reading the code, slots 6-11 are
|
|
675
|
+
indeed pre-step-only, so the data IS hoisted; the velocity loop reads
|
|
676
|
+
`pre[pre_off + 6..11]`). **Actually, this is already correctly factored.**
|
|
677
|
+
Withdraw this improvement.
|
|
678
|
+
|
|
679
|
+
Re-read confirms: the tangents are computed once per contact in the
|
|
680
|
+
pre-step pass (line 402) and read from `pre[]` in the iteration loop
|
|
681
|
+
(lines 501-502). Good.
|
|
682
|
+
|
|
683
|
+
### 3.8 Manifold contact persistence
|
|
684
|
+
|
|
685
|
+
**meep** (`narrowphase/narrowphase_step.js:687-700`): `clear_contacts(slot)`
|
|
686
|
+
called every frame, then contacts re-written from this frame's narrowphase.
|
|
687
|
+
The accumulated impulses (`j_n`, `j_t1`, `j_t2`) are zeroed by
|
|
688
|
+
`clear_contacts` (lines 232-239 in `ManifoldStore.js`).
|
|
689
|
+
|
|
690
|
+
```js
|
|
691
|
+
clear_contacts(slot) {
|
|
692
|
+
const meta_off = slot * SLOT_META_STRIDE;
|
|
693
|
+
this.__meta[meta_off + 2] = this.__meta[meta_off + 2] & ~COUNT_MASK;
|
|
694
|
+
const data_off = slot * SLOT_DATA_STRIDE;
|
|
695
|
+
this.__data.fill(0, data_off, data_off + SLOT_DATA_STRIDE);
|
|
696
|
+
}
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
**Bullet** (`btPersistentManifold::addManifoldPoint`): looks up an existing
|
|
700
|
+
contact within a threshold of the new one (`getCacheEntry`), and
|
|
701
|
+
**preserves the cached impulse** for the matching contact. Adds new
|
|
702
|
+
contacts only when no match. Replaces contacts via `replaceContactPoint`
|
|
703
|
+
which keeps `m_appliedImpulse`.
|
|
704
|
+
|
|
705
|
+
**Diagnosis**: **meep is throwing away warm-start data every frame.** Look
|
|
706
|
+
at `narrowphase_step.js:687-700`:
|
|
707
|
+
|
|
708
|
+
```js
|
|
709
|
+
manifolds.clear_contacts(slot); // ← wipes impulses
|
|
710
|
+
if (cand_count === 0) continue;
|
|
711
|
+
const kept = reduce_candidates(cand_count);
|
|
712
|
+
for (let k = 0; k < kept; k++) {
|
|
713
|
+
// writes the new candidate's positions, normal, depth
|
|
714
|
+
// — but j_n / j_t1 / j_t2 stay at 0 because clear_contacts zeroed them
|
|
715
|
+
manifolds.set_contact(slot, k, ...);
|
|
716
|
+
}
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
The `clear_contacts` call zeros the *entire slot data*, including all
|
|
720
|
+
twelve `j_*` impulse fields. Then `set_contact` writes only the first ten
|
|
721
|
+
floats per contact (positions × 2, normal, depth), so `j_n`, `j_t1`,
|
|
722
|
+
`j_t2` are left at the zeroed values.
|
|
723
|
+
|
|
724
|
+
**This means warm-starting is effectively disabled across frames.** Look at
|
|
725
|
+
the pre-step in `solve_contacts.js:468-476`:
|
|
726
|
+
|
|
727
|
+
```js
|
|
728
|
+
const j_n = data[off + 10];
|
|
729
|
+
const j_t1 = data[off + 11];
|
|
730
|
+
const j_t2 = data[off + 12];
|
|
731
|
+
const Px = nx * j_n + t1x * j_t1 + t2x * j_t2;
|
|
732
|
+
// ...
|
|
733
|
+
apply_impulse_to_body(rbA, trA, invMA, rax, ray, raz, Px, Py, Pz, +1, scratch_inertia_a);
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
Within a single frame, the iteration loop accumulates impulse correctly
|
|
737
|
+
(line 535 updates `data[off + 10]`). But across frames, every `set_contact`
|
|
738
|
+
call has wiped the previous frame's `j_n / j_t1 / j_t2`, so the warm-start
|
|
739
|
+
on this frame reads zeros, applies a zero impulse delta, and the solver
|
|
740
|
+
starts cold every step.
|
|
741
|
+
|
|
742
|
+
**This is a substantive correctness/performance bug.** Warm-start in a
|
|
743
|
+
Box2D-style SI solver typically buys ~30% iteration count reduction at
|
|
744
|
+
equivalent stability; without it, 10 iterations may be insufficient for
|
|
745
|
+
tall stacks. The 16-cube short-window test PLAN.md references probably
|
|
746
|
+
hides this because (a) short tower, (b) atomic-island sleep takes them out
|
|
747
|
+
of the solver quickly, but it would explain any "jitter on settle" the
|
|
748
|
+
team has seen.
|
|
749
|
+
|
|
750
|
+
**Improvement** (this is the highest-priority finding in the review):
|
|
751
|
+
either
|
|
752
|
+
|
|
753
|
+
1. Don't call `clear_contacts` — instead, match incoming candidates to
|
|
754
|
+
existing manifold contacts by world-distance (Bullet's
|
|
755
|
+
`getCacheEntry()` approach: keep the contact slot if a new candidate is
|
|
756
|
+
within ~0.02 m), preserving their `j_*`. New contacts get fresh slots.
|
|
757
|
+
This is the proper Bullet-style persistence and would also unlock
|
|
758
|
+
`local_a / local_b` storage for cross-frame refresh.
|
|
759
|
+
|
|
760
|
+
2. Alternatively, change `clear_contacts` to wipe only positions / normal /
|
|
761
|
+
depth and preserve `j_n / j_t1 / j_t2` (a 10-float overwrite instead of
|
|
762
|
+
13). Combined with re-pairing the new candidates to the *closest old
|
|
763
|
+
contact's impulse* by world-position, this gives warm-start without the
|
|
764
|
+
full Bullet refresh mechanism. Simpler delta.
|
|
765
|
+
|
|
766
|
+
Either way, the comment in `ManifoldStore.js:266-270` is consistent with
|
|
767
|
+
the intent ("`// j_n, j_t1, j_t2 are warm-start; preserved across calls.`")
|
|
768
|
+
— the `set_contact` API correctly does NOT touch them. But the upstream
|
|
769
|
+
`clear_contacts` zero pass at line 238 wipes them anyway. **This is a bug
|
|
770
|
+
that contradicts the documented invariant.**
|
|
771
|
+
|
|
772
|
+
---
|
|
773
|
+
|
|
774
|
+
## 4. Simplicity & uniformity
|
|
775
|
+
|
|
776
|
+
### 4.1 Code reuse and uniform abstractions
|
|
777
|
+
|
|
778
|
+
meep's code is markedly more uniform than Bullet's at every level:
|
|
779
|
+
|
|
780
|
+
| Aspect | meep | Bullet |
|
|
781
|
+
|---|---|---|
|
|
782
|
+
| Shape hierarchy | `AbstractShape3D` flat-ish; subclasses implement `support`, `compute_bounding_box`, `signed_distance`, `is_convex`. ~6 levels deep. | `btCollisionShape` → `btConvexShape` → `btPolyhedralConvexShape` → `btConvexHullShape` / `btBoxShape` / `btTriangleShape` ... + soft-body shapes + compound shapes. 4+ levels with multiple inheritance. |
|
|
783
|
+
| Narrowphase dispatch | Linear `if` ladder in `narrowphase_step.js:204-353`, ~10 cases | Virtual `getCollisionAlgorithm(...)` matrix lookup + per-cell dispatch class (`btSphereSphereCollisionAlgorithm`, `btSphereBoxCollisionAlgorithm`, ...) — `btCollisionDispatcher` holds a `m_doubleDispatch[NUM_SHAPE_TYPES][NUM_SHAPE_TYPES]` array of algorithm factories |
|
|
784
|
+
| Solver | One file, one function, three nested loops | `btSequentialImpulseConstraintSolver` ~3000 lines + helpers, virtual `btConstraintSolver` base, optional MLCP variant (`btMLCPSolver`) |
|
|
785
|
+
| Manifold | One class, ~500 lines, all in one file | `btPersistentManifold` + `btManifoldResult` + `btCollisionWorld` event hooks + ContactProcessedCallback / ContactDestroyedCallback ... |
|
|
786
|
+
| Broadphase | Two BVHs + one pair generator | `btBroadphaseInterface` virtual base + `btDbvtBroadphase` + `btSimpleBroadphase` + `btAxisSweep3` + `btMultiSapBroadphase`. With separate pair caches: `btHashedOverlappingPairCache` / `btSortedOverlappingPairCache`. |
|
|
787
|
+
|
|
788
|
+
**meep wins handily** on simplicity. The classic Bullet pain points:
|
|
789
|
+
|
|
790
|
+
- **Virtual call per support.** `btConvexShape::localGetSupportingVertex` is
|
|
791
|
+
virtual; every GJK iteration pays a vtable indirection. meep uses
|
|
792
|
+
duck-typed `shape.support(...)` which V8 monomorphises if the shape type
|
|
793
|
+
is hot.
|
|
794
|
+
|
|
795
|
+
- **Algorithm dispatch matrix.** Bullet looks up
|
|
796
|
+
`m_doubleDispatch[shape_a_type][shape_b_type]`, instantiates an
|
|
797
|
+
`btCollisionAlgorithm` (which may allocate), invokes a virtual method.
|
|
798
|
+
meep's `if (isSphereA && isSphereB) ... else if ...` is straight-line
|
|
799
|
+
and trivially profile-able.
|
|
800
|
+
|
|
801
|
+
- **AoS + virtual everything**: most C++ engines' style; meep's typed-array
|
|
802
|
+
SoA is JS-native and cache-coherent without trying.
|
|
803
|
+
|
|
804
|
+
### 4.2 Ease of adding a new shape pair
|
|
805
|
+
|
|
806
|
+
In **meep**, to add (say) `cylinder ↔ box` closed-form:
|
|
807
|
+
|
|
808
|
+
1. Add a `Cylinder3D` shape with `support` / `compute_bounding_box`.
|
|
809
|
+
2. Write a `cylinder_box_contact.js` closed-form solver.
|
|
810
|
+
3. Add the `isCylinderA && isBoxB` (and symmetric) branch to
|
|
811
|
+
`narrowphase_step.js`'s dispatch.
|
|
812
|
+
|
|
813
|
+
Three files. The dispatch entry is ~20 lines. Total work fits in one
|
|
814
|
+
commit.
|
|
815
|
+
|
|
816
|
+
In **Bullet**, the same task requires:
|
|
817
|
+
|
|
818
|
+
1. Add a `btCylinderShape` (subclass of `btPolyhedralConvexShape`).
|
|
819
|
+
2. Implement `btConvexShape::localGetSupportingVertex` + `getAabb`.
|
|
820
|
+
3. Register a `btCylinderBoxCollisionAlgorithm` factory with
|
|
821
|
+
`btCollisionDispatcher::registerCollisionCreateFunc(SHAPE_CYLINDER,
|
|
822
|
+
SHAPE_BOX, ...)`.
|
|
823
|
+
4. Implement the algorithm class extending `btCollisionAlgorithm` with
|
|
824
|
+
`processCollision`, `calculateTimeOfImpact`, and `getAllContactManifolds`
|
|
825
|
+
virtual overrides.
|
|
826
|
+
|
|
827
|
+
Five or six files, two or three classes, registration boilerplate. Bullet
|
|
828
|
+
is well-known for this overhead.
|
|
829
|
+
|
|
830
|
+
### 4.3 Ease of adding a new constraint
|
|
831
|
+
|
|
832
|
+
**meep**: not yet supported (joints are backlog). The solver loop is
|
|
833
|
+
already written to iterate `contacts ∪ joints` in PLAN.md's words; adding
|
|
834
|
+
a joint type would mean adding a pre-step builder, an iter applicator,
|
|
835
|
+
and a position-correction hook. With the existing `pre[]` scratch design,
|
|
836
|
+
this is reasonable.
|
|
837
|
+
|
|
838
|
+
**Bullet**: requires extending `btTypedConstraint` and implementing
|
|
839
|
+
`getInfo1`, `getInfo2`, `solveConstraintObsolete`, plus registering with
|
|
840
|
+
the constraint solver. More machinery, more constraints come pre-built
|
|
841
|
+
(distance, hinge, generic-6dof, slider, cone-twist, gear...).
|
|
842
|
+
|
|
843
|
+
For meep's target use case (game physics with simple joints), the simpler
|
|
844
|
+
add path is right. For Bullet's audience (robotics, complex articulated
|
|
845
|
+
mechanisms), the abstraction earns its weight.
|
|
846
|
+
|
|
847
|
+
### 4.4 Ease of adding a new query
|
|
848
|
+
|
|
849
|
+
**meep**: `queries/` is three independent files
|
|
850
|
+
(`raycast.js`, `shape_cast.js`, `overlap_shape.js`), each ~100-300 lines.
|
|
851
|
+
Adding a new query is "write a new file + add a method to
|
|
852
|
+
`PhysicsSystem`". Each query has its own BVH traversal — no shared
|
|
853
|
+
infrastructure beyond the BVH's leaf-iterator API. This is good — Bullet
|
|
854
|
+
in contrast has a complex `btCollisionWorld` callback interface
|
|
855
|
+
(`RayResultCallback`, `ConvexResultCallback`, `ContactResultCallback`)
|
|
856
|
+
that callers have to implement to consume results. meep's "fill a buffer,
|
|
857
|
+
return a count" idiom is simpler and JS-native.
|
|
858
|
+
|
|
859
|
+
### 4.5 Where to simplify further
|
|
860
|
+
|
|
861
|
+
The codebase is generally clean. Specific spots worth flagging:
|
|
862
|
+
|
|
863
|
+
1. **The `debugger;` statement in EPA** (`expanding_polytope_algorithm.js:135-137`)
|
|
864
|
+
is the single piece of debug-leftover code in the package. Already
|
|
865
|
+
covered above.
|
|
866
|
+
|
|
867
|
+
2. **`narrowphase_step.js`'s `dispatch_pair`** is a 350-line function with
|
|
868
|
+
ten if/else-if branches. Could be split into `dispatch_convex_convex`,
|
|
869
|
+
`dispatch_concave`, `dispatch_gjk_fallback` private functions — but
|
|
870
|
+
the current shape is readable and JIT-friendly (a single function with
|
|
871
|
+
monomorphic call sites is the inlining sweet-spot in V8). Leaving as-is
|
|
872
|
+
is defensible.
|
|
873
|
+
|
|
874
|
+
3. **`narrowphase_step.js`'s concave-side quaternion rotation** (lines
|
|
875
|
+
468-501) re-computes the q · v · q⁻¹ rotation inline for face-normal
|
|
876
|
+
and centroid. The helper `world_inverse_inertia_apply` already
|
|
877
|
+
demonstrates the same identity factored cleanly. A `v3_rotate_by_quat`
|
|
878
|
+
helper would make the concave dispatch ~30 lines shorter and easier to
|
|
879
|
+
audit. Low-priority readability win.
|
|
880
|
+
|
|
881
|
+
4. **Body / solver coupling via `__primary_collider` and friction-on-first-
|
|
882
|
+
collider** (`solver/solve_contacts.js:364`) — known limitation, backlog
|
|
883
|
+
has it. The clean way is per-contact source-collider tracking in the
|
|
884
|
+
manifold's stride. Mentioned in PLAN.md.
|
|
885
|
+
|
|
886
|
+
### 4.6 Where Bullet is a cautionary tale
|
|
887
|
+
|
|
888
|
+
- **Narrowphase dispatch matrix**: 14×14 grid of algorithm classes per
|
|
889
|
+
shape-type pair. Adding a new shape multiplies the algorithm count by
|
|
890
|
+
N. meep's linear if-ladder scales as N+1 per new shape — much better.
|
|
891
|
+
|
|
892
|
+
- **Virtual support()**: every iteration of GJK / EPA / MPR pays a virtual
|
|
893
|
+
call. meep's monomorphic-when-hot dispatch is a real performance
|
|
894
|
+
advantage.
|
|
895
|
+
|
|
896
|
+
- **The btIDebugDraw class hierarchy + debug-rendering hooks**: Bullet
|
|
897
|
+
threads debug-draw calls through every collision algorithm. meep doesn't
|
|
898
|
+
do this — debug rendering is a separate system that reads the physics
|
|
899
|
+
state, not a callback into it. Right architectural choice.
|
|
900
|
+
|
|
901
|
+
- **The compound-shape vs. compound-body confusion**: Bullet has both
|
|
902
|
+
`btCompoundShape` (multi-shape single-body) and the option of a
|
|
903
|
+
multi-collider rigid body. meep collapses this into "compound body" with
|
|
904
|
+
N attached colliders — simpler and arguably more game-physics-correct
|
|
905
|
+
(separate transforms on child entities, no nested-shape transform stack).
|
|
906
|
+
|
|
907
|
+
---
|
|
908
|
+
|
|
909
|
+
## Headline findings
|
|
910
|
+
|
|
911
|
+
1. **Warm-start is effectively disabled across frames** because
|
|
912
|
+
`clear_contacts` in `ManifoldStore.js:233-239` wipes `j_n / j_t1 / j_t2`
|
|
913
|
+
along with positions. Documented invariant contradicted by upstream
|
|
914
|
+
wipe. **Highest-priority fix.** (Section 3.8.)
|
|
915
|
+
|
|
916
|
+
2. **`debugger;` statement** in `expanding_polytope_algorithm.js:136` is a
|
|
917
|
+
bug — leftover debug aid in a hot path. Replace with the same
|
|
918
|
+
closest-face-approximation fallback used at the iteration-cap exit.
|
|
919
|
+
(Section 3.4.)
|
|
920
|
+
|
|
921
|
+
3. **No GJK separating-axis cache.** Significant per-frame performance gap
|
|
922
|
+
vs Bullet on quiescent contacts. Store the converged direction on the
|
|
923
|
+
manifold slot, seed next frame's GJK with it. (Section 3.1.)
|
|
924
|
+
|
|
925
|
+
4. **Box-box edge-edge contact uses body-centre midpoint** rather than
|
|
926
|
+
closest-edge-pair. Simple ~10-line improvement using the existing
|
|
927
|
+
`line3_closest_points_segment_segment` helper. (Section 3.2.)
|
|
928
|
+
|
|
929
|
+
5. **Multi-sided strengths over Bullet**: closed-form fast paths for
|
|
930
|
+
sphere-sphere and sphere-box (Bullet routes both through GJK), better
|
|
931
|
+
friction-cone disk clamp (Bullet defaults to box clamp), atomic-island
|
|
932
|
+
sleep with sleep-group chains (Bullet wakes one body per frame),
|
|
933
|
+
one-sided face-normal rejection on concave dispatch (Bullet doesn't
|
|
934
|
+
sanity-check MTV direction against face normal), deterministic union-
|
|
935
|
+
by-min-index island roots (Bullet's island ids depend on call order).
|
|
936
|
+
|
|
937
|
+
6. **meep's documented simplifications are mostly well-reasoned**: missing
|
|
938
|
+
split-impulse, missing per-body CCD, GJK on triangle-with-degenerate-
|
|
939
|
+
support — all enumerated in PLAN.md, all blocking specific test cases,
|
|
940
|
+
all with clear architectural paths to resolution.
|
|
941
|
+
|
|
942
|
+
7. **Code organisation is dramatically simpler than Bullet's** — flat
|
|
943
|
+
shape hierarchy, linear narrowphase dispatch, SoA throughout, no
|
|
944
|
+
virtual-per-support dispatch. The trade-off vs. Bullet's algorithm
|
|
945
|
+
matrix is real and meep is on the right side of it for game physics.
|