@woosh/meep-engine 2.139.0 → 2.140.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_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 +461 -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/broadphase/compute_fat_world_aabb.js +2 -2
- 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/PhysicsSystem.d.ts +128 -20
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +1301 -1159
- package/src/engine/physics/fluid/FluidSimulator.d.ts.map +1 -1
- package/src/engine/physics/fluid/FluidSimulator.js +2 -1
- package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts +28 -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 +39 -17
- 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/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 +113 -17
- 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 +8 -2
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/narrowphase_step.js +1422 -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 +116 -30
- package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
- package/src/engine/physics/solver/solve_contacts.js +641 -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
|
@@ -3,8 +3,49 @@ import { ColliderFlags } from "../ecs/ColliderFlags.js";
|
|
|
3
3
|
import { CONTACT_STRIDE } from "../contact/ManifoldStore.js";
|
|
4
4
|
import { RigidBodyFlags } from "../ecs/RigidBodyFlags.js";
|
|
5
5
|
import { world_inverse_inertia_apply } from "../inertia/world_inverse_inertia.js";
|
|
6
|
+
import { v3_quat3_apply } from "../../../core/geom/vec3/v3_quat3_apply.js";
|
|
7
|
+
import { v3_quat3_apply_inverse } from "../../../core/geom/vec3/v3_quat3_apply_inverse.js";
|
|
6
8
|
import { friction_cone_clamp } from "./friction_cone.js";
|
|
7
9
|
|
|
10
|
+
/**
|
|
11
|
+
* # TGS split-impulse contact solver (staged)
|
|
12
|
+
*
|
|
13
|
+
* Temporal Gauss-Seidel with Catto-2018 split impulse. The solver runs as a
|
|
14
|
+
* sequence of stages driven by `PhysicsSystem.fixedUpdate`, which owns the
|
|
15
|
+
* substep loop (it has to — velocity/position integration spans every awake
|
|
16
|
+
* body, not just contacts):
|
|
17
|
+
*
|
|
18
|
+
* prepare_contacts(manifolds, system, h) // once per outer step
|
|
19
|
+
* for each substep:
|
|
20
|
+
* (system integrates gravity by h)
|
|
21
|
+
* refresh_contacts(manifolds, system) // re-derive geometry
|
|
22
|
+
* warm_start_contacts(manifolds, system) // replay impulse — per substep!
|
|
23
|
+
* solve_velocity(manifolds, system, iters) // non-penetration + friction
|
|
24
|
+
* solve_position(manifolds, system, pos_iters)
|
|
25
|
+
* (system integrates position by h, folding pseudo-velocity)
|
|
26
|
+
* apply_restitution(manifolds, system) // once, after the loop
|
|
27
|
+
*
|
|
28
|
+
* The three concerns are fully decoupled (Phases 1–2) and substepping
|
|
29
|
+
* (Phase 3) re-runs only the velocity + position solve per substep —
|
|
30
|
+
* narrowphase runs once per outer step. Each substep re-derives the
|
|
31
|
+
* current penetration analytically from the bodies' moved poses and the
|
|
32
|
+
* contact anchors captured at prepare time, so the position correction
|
|
33
|
+
* adapts as the stack settles (the mechanism behind TGS stack stability)
|
|
34
|
+
* without paying for narrowphase N times.
|
|
35
|
+
*
|
|
36
|
+
* Why staged module functions sharing module scratch, rather than one call:
|
|
37
|
+
* the substep loop interleaves solver stages with whole-body integration
|
|
38
|
+
* that lives in the system. The stages communicate through module-scoped
|
|
39
|
+
* scratch + `g_*` state set by `prepare_contacts`; they must be called in
|
|
40
|
+
* order, prepare first, within a single outer step. Single-threaded and
|
|
41
|
+
* deterministic, so the shared state is safe.
|
|
42
|
+
*
|
|
43
|
+
* Contacts from all islands are flattened into one scratch array. Islands
|
|
44
|
+
* are independent (they share no bodies), so a single flat Gauss-Seidel
|
|
45
|
+
* sweep gives the same result as per-island sweeps — the island partition
|
|
46
|
+
* is still built and used by the sleep test, just not needed here.
|
|
47
|
+
*/
|
|
48
|
+
|
|
8
49
|
/**
|
|
9
50
|
* A pair is "sensor-only" when either body OR either body's primary
|
|
10
51
|
* collider carries the IsSensor flag. The manifold still exists (so
|
|
@@ -25,29 +66,18 @@ function pair_is_sensor(rbA, colA, rbB, colB) {
|
|
|
25
66
|
}
|
|
26
67
|
|
|
27
68
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* than the classic PGS default of 10. Total iterations per tick =
|
|
32
|
-
* substeps × iters_per_substep; the recommended pairing is 4 × 2 = 8,
|
|
33
|
-
* which matches Box2D's TGS defaults and outperforms 1 × 10 on tall
|
|
34
|
-
* stacks (impulses get position-integrated between substeps, so info
|
|
35
|
-
* propagation through a contact chain compounds across substeps rather
|
|
36
|
-
* than fighting against accumulating position drift inside one solve).
|
|
37
|
-
*
|
|
38
|
-
* Direct callers that want classic PGS can still pass `iters = 10` and
|
|
39
|
-
* skip substepping by calling once with the full tick `dt`.
|
|
69
|
+
* Velocity-iteration count per substep. With substepping the per-substep
|
|
70
|
+
* count can be lower than a single-step PGS solver would need, because the
|
|
71
|
+
* outer loop revisits the contact set `substeps` times.
|
|
40
72
|
* @type {number}
|
|
41
73
|
*/
|
|
42
|
-
const DEFAULT_VELOCITY_ITERATIONS =
|
|
74
|
+
const DEFAULT_VELOCITY_ITERATIONS = 10;
|
|
43
75
|
|
|
44
76
|
/**
|
|
45
|
-
*
|
|
46
|
-
* push bodies apart but more stable); larger values can introduce jitter.
|
|
47
|
-
* 0.2 is the Catto / Box2D default.
|
|
77
|
+
* Position-iteration count per substep (split-impulse pseudo-velocity pass).
|
|
48
78
|
* @type {number}
|
|
49
79
|
*/
|
|
50
|
-
const
|
|
80
|
+
const DEFAULT_POSITION_ITERATIONS = 2;
|
|
51
81
|
|
|
52
82
|
/**
|
|
53
83
|
* Penetration allowed without applying position correction. Eliminates
|
|
@@ -57,32 +87,107 @@ const BAUMGARTE_BETA = 0.2;
|
|
|
57
87
|
const PENETRATION_SLOP = 0.005;
|
|
58
88
|
|
|
59
89
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
90
|
+
* SPOOK contact stiffness `k`. Effectively infinite for rigid-body
|
|
91
|
+
* contact: what the solver actually sees is the regularization
|
|
92
|
+
* `eps = 4 / (h² · k · (1 + 4d))`, negligible at `k = 1e12` but in place
|
|
93
|
+
* as a continuous compliance dial for future soft contacts. Lacoursière
|
|
94
|
+
* 2007; same formulation as cannon-es / AgX.
|
|
95
|
+
* @type {number}
|
|
96
|
+
*/
|
|
97
|
+
const CONTACT_STIFFNESS = 1e12;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* SPOOK contact relaxation `d`. Chosen so `a = 4 / (h(1 + 4d)) = 0.2 / h`,
|
|
101
|
+
* numerically matching the prior Baumgarte β = 0.2 gain. `4/(1+4d)=0.2` →
|
|
102
|
+
* `d = 4.75`. Note `h` here is the SUBSTEP size: position correction is
|
|
103
|
+
* applied per substep, so the gain is derived from the substep `dt`.
|
|
104
|
+
* @type {number}
|
|
105
|
+
*/
|
|
106
|
+
const CONTACT_RELAXATION = 4.75;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Maximum magnitude of the position-correction velocity bias, in m/s.
|
|
110
|
+
* Belt-and-braces against inflated EPA depths (PLAN.md caveat) driving the
|
|
111
|
+
* bias to tens of m/s. Removed once closed-form triangle solvers land.
|
|
112
|
+
* @type {number}
|
|
113
|
+
*/
|
|
114
|
+
const MAX_POSITION_BIAS = 3;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Velocity below which restitution is suppressed (no micro-bounce buzz on
|
|
118
|
+
* resting stacks).
|
|
62
119
|
* @type {number}
|
|
63
120
|
*/
|
|
64
121
|
const RESTITUTION_VELOCITY_THRESHOLD = 1.0;
|
|
65
122
|
|
|
66
123
|
/**
|
|
67
|
-
*
|
|
68
|
-
* 0..2 :
|
|
69
|
-
* 3..5 :
|
|
70
|
-
* 6..8 :
|
|
71
|
-
* 9..11:
|
|
72
|
-
* 12
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
124
|
+
* Per-contact pre-step scratch stride: 23 doubles.
|
|
125
|
+
* 0..2 : localWA (A's contact witness, in A's LOCAL frame — constant)
|
|
126
|
+
* 3..5 : localWB (B's contact witness, in B's LOCAL frame — constant)
|
|
127
|
+
* 6..8 : rA (lever from A's COM to the contact midpoint — refreshed)
|
|
128
|
+
* 9..11: rB (lever from B's COM to the contact midpoint — refreshed)
|
|
129
|
+
* 12..14: t1 (tangent 1, unit, world — constant)
|
|
130
|
+
* 15..17: t2 (tangent 2, unit, world — constant)
|
|
131
|
+
* 18 : m_eff_n
|
|
132
|
+
* 19 : m_eff_t1
|
|
133
|
+
* 20 : m_eff_t2
|
|
134
|
+
* 21 : rest_bias (restitution: `e · vn_approach`, ≤ 0; 0 if not bouncing)
|
|
135
|
+
* 22 : bias_position (depth-correction bias — refreshed each substep)
|
|
136
|
+
*
|
|
137
|
+
* `localWA` / `localWB` are the per-body contact witnesses expressed in body
|
|
138
|
+
* local frames at prepare time. Each substep, `refresh_contacts` rotates
|
|
139
|
+
* them back to world by the body's current pose to recover the moved contact
|
|
140
|
+
* points, re-derives the current penetration depth (anchored on the trusted
|
|
141
|
+
* prepare-time depth via a delta, so it's sign-robust), and rebuilds the
|
|
142
|
+
* impulse lever arms `rA` / `rB` and the position bias.
|
|
76
143
|
* @type {number}
|
|
77
144
|
*/
|
|
78
|
-
const PRE_STRIDE =
|
|
145
|
+
const PRE_STRIDE = 23;
|
|
79
146
|
|
|
80
147
|
/** Per-contact index list — 4 uint32 per contact: slot, idx, idxA, idxB. */
|
|
81
148
|
const INDEX_STRIDE = 4;
|
|
82
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Per-body pseudo-velocity stride: 3 linear + 3 angular doubles. Owned by
|
|
152
|
+
* PhysicsSystem (`system.__pseudo_velocity`); zeroed each substep before
|
|
153
|
+
* the position pass and folded into the pose by `integrate_position`.
|
|
154
|
+
* @type {number}
|
|
155
|
+
*/
|
|
156
|
+
const PSEUDO_STRIDE = 6;
|
|
157
|
+
|
|
83
158
|
let scratch_pre = new Float64Array(64 * PRE_STRIDE);
|
|
84
159
|
let scratch_idx = new Uint32Array(64 * INDEX_STRIDE);
|
|
85
160
|
let scratch_mu = new Float64Array(64);
|
|
161
|
+
let scratch_pos_jn = new Float64Array(64);
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Per-contact maximum normal impulse seen across the whole outer step's
|
|
165
|
+
* velocity solving (all substeps, all iterations). Reset in
|
|
166
|
+
* {@link prepare_contacts}, updated in {@link solve_velocity}, read by
|
|
167
|
+
* {@link apply_restitution}.
|
|
168
|
+
*
|
|
169
|
+
* Restitution must fire whenever a contact *was* compressive at some point
|
|
170
|
+
* in the step, even if its accumulated impulse later relaxes back to ~0 — a
|
|
171
|
+
* transient collision (ball bouncing, head-on hit) under per-substep
|
|
172
|
+
* warm-start ends the loop with `j_n ≈ 0` because there's no sustained load
|
|
173
|
+
* to hold the impulse up. Gating on this running max (Box2D-v3
|
|
174
|
+
* `maxNormalImpulse`) rather than the end-of-loop `j_n` is what makes
|
|
175
|
+
* bounces fire.
|
|
176
|
+
* @type {Float64Array}
|
|
177
|
+
*/
|
|
178
|
+
let scratch_max_jn = new Float64Array(64);
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Shared cross-stage state, set by {@link prepare_contacts} and read by the
|
|
182
|
+
* per-substep stages within the same outer step. Single-threaded, so plain
|
|
183
|
+
* module variables are safe.
|
|
184
|
+
*/
|
|
185
|
+
let g_contact_count = 0;
|
|
186
|
+
let g_spook_a = 0;
|
|
187
|
+
let g_spook_eps = 0;
|
|
188
|
+
|
|
189
|
+
/** Scratch for re-deriving contact world points in {@link refresh_contacts}. */
|
|
190
|
+
const scratch_cp = new Float64Array(6);
|
|
86
191
|
|
|
87
192
|
function ensure_capacity(n) {
|
|
88
193
|
if (scratch_pre.length < n * PRE_STRIDE) {
|
|
@@ -94,12 +199,16 @@ function ensure_capacity(n) {
|
|
|
94
199
|
if (scratch_mu.length < n) {
|
|
95
200
|
scratch_mu = new Float64Array(n * 2);
|
|
96
201
|
}
|
|
202
|
+
if (scratch_pos_jn.length < n) {
|
|
203
|
+
scratch_pos_jn = new Float64Array(n * 2);
|
|
204
|
+
}
|
|
205
|
+
if (scratch_max_jn.length < n) {
|
|
206
|
+
scratch_max_jn = new Float64Array(n * 2);
|
|
207
|
+
}
|
|
97
208
|
}
|
|
98
209
|
|
|
99
210
|
/**
|
|
100
|
-
* Build an orthonormal tangent basis perpendicular to a unit normal.
|
|
101
|
-
* "least-aligned axis" trick: pick the world axis least aligned with `n`,
|
|
102
|
-
* cross with `n`, then cross again for the second tangent.
|
|
211
|
+
* Build an orthonormal tangent basis perpendicular to a unit normal.
|
|
103
212
|
*
|
|
104
213
|
* @param {Float64Array} out 6 floats: t1.xyz then t2.xyz
|
|
105
214
|
* @param {number} off
|
|
@@ -108,7 +217,6 @@ function ensure_capacity(n) {
|
|
|
108
217
|
* @param {number} nz
|
|
109
218
|
*/
|
|
110
219
|
function build_tangents(out, off, nx, ny, nz) {
|
|
111
|
-
// Pick the world axis least aligned with n.
|
|
112
220
|
const ax = nx < 0 ? -nx : nx;
|
|
113
221
|
const ay = ny < 0 ? -ny : ny;
|
|
114
222
|
const az = nz < 0 ? -nz : nz;
|
|
@@ -121,14 +229,12 @@ function build_tangents(out, off, nx, ny, nz) {
|
|
|
121
229
|
} else {
|
|
122
230
|
rx = 0; ry = 0; rz = 1;
|
|
123
231
|
}
|
|
124
|
-
// t1 = normalize(n × r)
|
|
125
232
|
let t1x = ny * rz - nz * ry;
|
|
126
233
|
let t1y = nz * rx - nx * rz;
|
|
127
234
|
let t1z = nx * ry - ny * rx;
|
|
128
235
|
const inv = 1 / Math.sqrt(t1x * t1x + t1y * t1y + t1z * t1z);
|
|
129
236
|
t1x *= inv; t1y *= inv; t1z *= inv;
|
|
130
237
|
|
|
131
|
-
// t2 = n × t1 (already unit since n and t1 are orthogonal unit vectors)
|
|
132
238
|
const t2x = ny * t1z - nz * t1y;
|
|
133
239
|
const t2y = nz * t1x - nx * t1z;
|
|
134
240
|
const t2z = nx * t1y - ny * t1x;
|
|
@@ -147,8 +253,7 @@ function inv_mass_of(rb) {
|
|
|
147
253
|
}
|
|
148
254
|
|
|
149
255
|
/**
|
|
150
|
-
* Combine two friction coefficients
|
|
151
|
-
* PhysX default and never goes beyond the smaller of the two.
|
|
256
|
+
* Combine two friction coefficients (geometric mean — Bullet / PhysX default).
|
|
152
257
|
* @param {number} a
|
|
153
258
|
* @param {number} b
|
|
154
259
|
* @returns {number}
|
|
@@ -158,8 +263,7 @@ function combine_friction(a, b) {
|
|
|
158
263
|
}
|
|
159
264
|
|
|
160
265
|
/**
|
|
161
|
-
* Combine two restitution coefficients
|
|
162
|
-
* wins; matches Unity / common game-engine default.
|
|
266
|
+
* Combine two restitution coefficients (maximum — Unity / common default).
|
|
163
267
|
* @param {number} a
|
|
164
268
|
* @param {number} b
|
|
165
269
|
* @returns {number}
|
|
@@ -173,28 +277,17 @@ const scratch_inertia_a = new Float64Array(3);
|
|
|
173
277
|
const scratch_inertia_b = new Float64Array(3);
|
|
174
278
|
|
|
175
279
|
/**
|
|
176
|
-
* Apply an impulse `P` at body-relative offset `r
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
* Writes go directly to the typed-array backing of `linearVelocity` and
|
|
180
|
-
* `angularVelocity` — bypassing {@link Vector3#set} on purpose. Each `set()`
|
|
181
|
-
* runs a hasHandlers / sendN dispatch path which is dead weight in the
|
|
182
|
-
* solver inner loop (no observer is subscribed to a body's velocity in
|
|
183
|
-
* normal use, and even when one is, fanning out per-iteration writes is
|
|
184
|
-
* useless — callers want the post-step value).
|
|
280
|
+
* Apply an impulse `P` at body-relative offset `r` to persistent velocity
|
|
281
|
+
* (Δv = P/m, Δω = I⁻¹·(r × P)). Direct typed-array writes bypass
|
|
282
|
+
* {@link Vector3#set}'s observer dispatch (dead weight in the solver loop).
|
|
185
283
|
*
|
|
186
284
|
* @param {RigidBody} rb
|
|
187
285
|
* @param {Transform} transform
|
|
188
286
|
* @param {number} invM
|
|
189
|
-
* @param {number} rx
|
|
190
|
-
* @param {number}
|
|
191
|
-
* @param {number}
|
|
192
|
-
* @param {
|
|
193
|
-
* @param {number} Py
|
|
194
|
-
* @param {number} Pz
|
|
195
|
-
* @param {number} sign +1 for body A, -1 for body B (the contact normal points
|
|
196
|
-
* from B → A, so A receives +P and B receives −P).
|
|
197
|
-
* @param {Float64Array} scratch_inertia 3-vector scratch for the angular term
|
|
287
|
+
* @param {number} rx @param {number} ry @param {number} rz
|
|
288
|
+
* @param {number} Px @param {number} Py @param {number} Pz
|
|
289
|
+
* @param {number} sign +1 for body A, -1 for body B
|
|
290
|
+
* @param {Float64Array} scratch_inertia
|
|
198
291
|
*/
|
|
199
292
|
function apply_impulse_to_body(
|
|
200
293
|
rb, transform, invM,
|
|
@@ -214,12 +307,11 @@ function apply_impulse_to_body(
|
|
|
214
307
|
lv[1] += sPy * invM;
|
|
215
308
|
lv[2] += sPz * invM;
|
|
216
309
|
|
|
217
|
-
// r × P (already-signed)
|
|
218
310
|
const tx = ry * sPz - rz * sPy;
|
|
219
311
|
const ty = rz * sPx - rx * sPz;
|
|
220
312
|
const tz = rx * sPy - ry * sPx;
|
|
221
313
|
|
|
222
|
-
world_inverse_inertia_apply(scratch_inertia, 0, rb, transform, tx, ty, tz);
|
|
314
|
+
world_inverse_inertia_apply(scratch_inertia, 0, rb.inverseInertiaLocal, transform.rotation, tx, ty, tz);
|
|
223
315
|
|
|
224
316
|
const av = rb.angularVelocity;
|
|
225
317
|
av[0] += scratch_inertia[0];
|
|
@@ -227,18 +319,56 @@ function apply_impulse_to_body(
|
|
|
227
319
|
av[2] += scratch_inertia[2];
|
|
228
320
|
}
|
|
229
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Apply a position-pass impulse `P` at body-relative offset `r` to the
|
|
324
|
+
* body's pseudo-velocity (linear + angular) in the `pseudo_velocity` flat
|
|
325
|
+
* buffer. Mirrors {@link apply_impulse_to_body} but the result is consumed
|
|
326
|
+
* by `integrate_position` the same substep and never persists.
|
|
327
|
+
*
|
|
328
|
+
* @param {Float64Array} pseudo_velocity stride = {@link PSEUDO_STRIDE}
|
|
329
|
+
* @param {number} base offset = `body_index * PSEUDO_STRIDE`
|
|
330
|
+
* @param {RigidBody} rb @param {Transform} transform @param {number} invM
|
|
331
|
+
* @param {number} rx @param {number} ry @param {number} rz
|
|
332
|
+
* @param {number} Px @param {number} Py @param {number} Pz
|
|
333
|
+
* @param {number} sign +1 for body A, -1 for body B
|
|
334
|
+
* @param {Float64Array} scratch_inertia
|
|
335
|
+
*/
|
|
336
|
+
function apply_impulse_to_pseudo(
|
|
337
|
+
pseudo_velocity, base,
|
|
338
|
+
rb, transform, invM,
|
|
339
|
+
rx, ry, rz,
|
|
340
|
+
Px, Py, Pz,
|
|
341
|
+
sign,
|
|
342
|
+
scratch_inertia
|
|
343
|
+
) {
|
|
344
|
+
if (invM === 0) return;
|
|
345
|
+
|
|
346
|
+
const sPx = sign * Px;
|
|
347
|
+
const sPy = sign * Py;
|
|
348
|
+
const sPz = sign * Pz;
|
|
349
|
+
|
|
350
|
+
pseudo_velocity[base] += sPx * invM;
|
|
351
|
+
pseudo_velocity[base + 1] += sPy * invM;
|
|
352
|
+
pseudo_velocity[base + 2] += sPz * invM;
|
|
353
|
+
|
|
354
|
+
const tx = ry * sPz - rz * sPy;
|
|
355
|
+
const ty = rz * sPx - rx * sPz;
|
|
356
|
+
const tz = rx * sPy - ry * sPx;
|
|
357
|
+
|
|
358
|
+
world_inverse_inertia_apply(scratch_inertia, 0, rb.inverseInertiaLocal, transform.rotation, tx, ty, tz);
|
|
359
|
+
|
|
360
|
+
pseudo_velocity[base + 3] += scratch_inertia[0];
|
|
361
|
+
pseudo_velocity[base + 4] += scratch_inertia[1];
|
|
362
|
+
pseudo_velocity[base + 5] += scratch_inertia[2];
|
|
363
|
+
}
|
|
364
|
+
|
|
230
365
|
/**
|
|
231
366
|
* Quadratic-form contribution of one body to the constraint effective mass
|
|
232
|
-
* along a unit axis
|
|
367
|
+
* along a unit axis: `(r × axis)^T · I⁻¹_world · (r × axis)`.
|
|
233
368
|
*
|
|
234
|
-
* @param {RigidBody} rb
|
|
235
|
-
* @param {
|
|
236
|
-
* @param {number}
|
|
237
|
-
* @param {number} ry
|
|
238
|
-
* @param {number} rz
|
|
239
|
-
* @param {number} ax axis x
|
|
240
|
-
* @param {number} ay
|
|
241
|
-
* @param {number} az
|
|
369
|
+
* @param {RigidBody} rb @param {Transform} transform
|
|
370
|
+
* @param {number} rx @param {number} ry @param {number} rz
|
|
371
|
+
* @param {number} ax @param {number} ay @param {number} az
|
|
242
372
|
* @param {Float64Array} scratch_inertia
|
|
243
373
|
* @returns {number}
|
|
244
374
|
*/
|
|
@@ -256,7 +386,7 @@ function angular_jacobian_contribution(
|
|
|
256
386
|
const rxay = rz * ax - rx * az;
|
|
257
387
|
const rxaz = rx * ay - ry * ax;
|
|
258
388
|
|
|
259
|
-
world_inverse_inertia_apply(scratch_inertia, 0,
|
|
389
|
+
world_inverse_inertia_apply(scratch_inertia, 0, ii, transform.rotation, rxax, rxay, rxaz);
|
|
260
390
|
|
|
261
391
|
return rxax * scratch_inertia[0]
|
|
262
392
|
+ rxay * scratch_inertia[1]
|
|
@@ -264,97 +394,66 @@ function angular_jacobian_contribution(
|
|
|
264
394
|
}
|
|
265
395
|
|
|
266
396
|
/**
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
* Per-island iteration matters for two reasons:
|
|
272
|
-
* 1. Impulse propagation converges inside an island without waiting for
|
|
273
|
-
* unrelated bodies' Gauss-Seidel updates from previous outer loops.
|
|
274
|
-
* 2. Disconnected awake bodies don't pay each other's solver cost — adding
|
|
275
|
-
* an unrelated active body to the scene scales O(island_size) rather than
|
|
276
|
-
* O(global_active).
|
|
277
|
-
*
|
|
278
|
-
* Coulomb friction is applied as a disk clamp in the contact tangent plane.
|
|
279
|
-
* Position correction is folded into the velocity solve via Baumgarte bias.
|
|
280
|
-
*
|
|
281
|
-
* The `apply_warm_start` flag gates the warm-start impulse application.
|
|
282
|
-
* In a classic PGS call (one solve per tick), pass `true` (default) — the
|
|
283
|
-
* cached impulses from the previous tick get replayed onto the velocity,
|
|
284
|
-
* seeding the iterations near the converged answer. In TGS substepping,
|
|
285
|
-
* the PhysicsSystem calls this once per substep; only substep 0 should
|
|
286
|
-
* apply warm-start, because subsequent substeps continue from the solver
|
|
287
|
-
* state left by the previous substep (the impulses in `data` already
|
|
288
|
-
* reflect the current velocity, so re-applying them would double-count).
|
|
397
|
+
* Flat count of all touched contacts across every island, plus the slot
|
|
398
|
+
* list to iterate. Islands are concatenated densely in `contact_data`;
|
|
399
|
+
* `contact_offsets[island_count]` is the end of the last island.
|
|
289
400
|
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
* correct for arbitrary rotations + diagonal local inertia.
|
|
293
|
-
*
|
|
294
|
-
* @param {ManifoldStore} manifolds
|
|
295
|
-
* @param {PhysicsSystem} system PhysicsSystem; reads `system.islands`.
|
|
296
|
-
* @param {number} dt step duration (full tick `dt` for PGS, sub-tick for TGS)
|
|
297
|
-
* @param {number} [iters]
|
|
298
|
-
* @param {boolean} [apply_warm_start]
|
|
401
|
+
* @param {PhysicsSystem} system
|
|
402
|
+
* @returns {{slot_list: Uint32Array, total_slots: number}|null}
|
|
299
403
|
*/
|
|
300
|
-
|
|
301
|
-
if (dt <= 0) return;
|
|
302
|
-
|
|
404
|
+
function island_slot_range(system) {
|
|
303
405
|
const islands = system.islands;
|
|
304
|
-
if (islands === undefined || islands === null) return;
|
|
305
|
-
|
|
406
|
+
if (islands === undefined || islands === null) return null;
|
|
306
407
|
const island_count = islands.island_count;
|
|
307
|
-
if (island_count === 0) return;
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
for (let i = 0; i < island_count; i++) {
|
|
313
|
-
const start = slot_offsets[i];
|
|
314
|
-
const end = slot_offsets[i + 1];
|
|
315
|
-
if (end === start) continue;
|
|
316
|
-
solve_island(manifolds, system, dt, iters, slot_list, start, end, apply_warm_start);
|
|
317
|
-
}
|
|
408
|
+
if (island_count === 0) return null;
|
|
409
|
+
return {
|
|
410
|
+
slot_list: islands.contact_data,
|
|
411
|
+
total_slots: islands.contact_offsets[island_count],
|
|
412
|
+
};
|
|
318
413
|
}
|
|
319
414
|
|
|
320
415
|
/**
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
416
|
+
* Stage 1 — prepare the contact constraints for an outer step.
|
|
417
|
+
*
|
|
418
|
+
* Packs every touched, non-sensor contact into the flat scratch arrays:
|
|
419
|
+
* local-frame witness anchors, tangent basis, effective masses, friction,
|
|
420
|
+
* the restitution approach velocity, and the warm-start replay (applied
|
|
421
|
+
* once here, not per substep). Computes the SPOOK gains from the SUBSTEP
|
|
422
|
+
* size `dt_sub` because the position correction runs once per substep.
|
|
325
423
|
*
|
|
326
424
|
* @param {ManifoldStore} manifolds
|
|
327
425
|
* @param {PhysicsSystem} system
|
|
328
|
-
* @param {number} dt
|
|
329
|
-
* @
|
|
330
|
-
* @param {Uint32Array} slot_list
|
|
331
|
-
* @param {number} slot_start
|
|
332
|
-
* @param {number} slot_end
|
|
333
|
-
* @param {boolean} apply_warm_start whether to replay cached impulses (see
|
|
334
|
-
* {@link solve_contacts})
|
|
426
|
+
* @param {number} dt_sub substep size `dt / substeps`
|
|
427
|
+
* @returns {number} number of contacts prepared (also stored module-side)
|
|
335
428
|
*/
|
|
336
|
-
function
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
429
|
+
export function prepare_contacts(manifolds, system, dt_sub) {
|
|
430
|
+
g_contact_count = 0;
|
|
431
|
+
if (dt_sub <= 0) return 0;
|
|
432
|
+
|
|
433
|
+
const range = island_slot_range(system);
|
|
434
|
+
if (range === null) return 0;
|
|
435
|
+
const slot_list = range.slot_list;
|
|
436
|
+
const total_slots = range.total_slots;
|
|
437
|
+
|
|
342
438
|
let contact_total = 0;
|
|
343
|
-
for (let i =
|
|
344
|
-
|
|
345
|
-
contact_total += manifolds.contact_count(slot);
|
|
439
|
+
for (let i = 0; i < total_slots; i++) {
|
|
440
|
+
contact_total += manifolds.contact_count(slot_list[i]);
|
|
346
441
|
}
|
|
347
|
-
if (contact_total === 0) return;
|
|
442
|
+
if (contact_total === 0) return 0;
|
|
348
443
|
ensure_capacity(contact_total);
|
|
349
444
|
|
|
445
|
+
const denom = 1 + 4 * CONTACT_RELAXATION;
|
|
446
|
+
g_spook_a = 4 / (dt_sub * denom);
|
|
447
|
+
g_spook_eps = 4 / (dt_sub * dt_sub * CONTACT_STIFFNESS * denom);
|
|
448
|
+
|
|
350
449
|
const data = manifolds.data_buffer;
|
|
351
450
|
const pre = scratch_pre;
|
|
352
451
|
const idx = scratch_idx;
|
|
353
452
|
const mus = scratch_mu;
|
|
453
|
+
const pos_jn = scratch_pos_jn;
|
|
354
454
|
|
|
355
|
-
// 2. Pre-step + warm-start.
|
|
356
455
|
let c = 0;
|
|
357
|
-
for (let i =
|
|
456
|
+
for (let i = 0; i < total_slots; i++) {
|
|
358
457
|
const slot = slot_list[i];
|
|
359
458
|
const idxA = system.__index_of(manifolds.bodyA(slot));
|
|
360
459
|
const idxB = system.__index_of(manifolds.bodyB(slot));
|
|
@@ -362,16 +461,9 @@ function solve_island(manifolds, system, dt, iters, slot_list, slot_start, slot_
|
|
|
362
461
|
const rbB = system.__bodies[idxB];
|
|
363
462
|
const trA = system.__transforms[idxA];
|
|
364
463
|
const trB = system.__transforms[idxB];
|
|
365
|
-
// Material parameters come from the "primary" collider of each body
|
|
366
|
-
// — for single-collider bodies this is the only one; for compound
|
|
367
|
-
// bodies it's a v1 approximation (per-contact source-collider
|
|
368
|
-
// tracking is a follow-up). Fall back to defaults if a body has no
|
|
369
|
-
// attached colliders.
|
|
370
464
|
const colA = system.__primary_collider(idxA);
|
|
371
465
|
const colB = system.__primary_collider(idxB);
|
|
372
466
|
if (colA === null || colB === null) continue;
|
|
373
|
-
// Sensor pairs: contacts persist in the manifold (for events) but
|
|
374
|
-
// produce no impulse. Skip the solver work for this pair entirely.
|
|
375
467
|
if (pair_is_sensor(rbA, colA, rbB, colB)) continue;
|
|
376
468
|
|
|
377
469
|
const invMA = inv_mass_of(rbA);
|
|
@@ -382,35 +474,40 @@ function solve_island(manifolds, system, dt, iters, slot_list, slot_start, slot_
|
|
|
382
474
|
const restitution_combined = combine_restitution(colA.restitution, colB.restitution);
|
|
383
475
|
const friction_combined = combine_friction(colA.friction, colB.friction);
|
|
384
476
|
|
|
477
|
+
const pAx = trA.position.x, pAy = trA.position.y, pAz = trA.position.z;
|
|
478
|
+
const pBx = trB.position.x, pBy = trB.position.y, pBz = trB.position.z;
|
|
479
|
+
const qA = trA.rotation, qB = trB.rotation;
|
|
480
|
+
|
|
385
481
|
for (let k = 0; k < cc; k++) {
|
|
386
482
|
const off = slot_off + k * CONTACT_STRIDE;
|
|
387
483
|
|
|
388
484
|
const wax = data[off], way = data[off + 1], waz = data[off + 2];
|
|
389
485
|
const wbx = data[off + 3], wby = data[off + 4], wbz = data[off + 5];
|
|
390
486
|
const nx = data[off + 6], ny = data[off + 7], nz = data[off + 8];
|
|
391
|
-
const depth = data[off + 9];
|
|
392
487
|
|
|
393
|
-
// Application point: midpoint of the two surface witnesses.
|
|
394
488
|
const px = (wax + wbx) * 0.5;
|
|
395
489
|
const py = (way + wby) * 0.5;
|
|
396
490
|
const pz = (waz + wbz) * 0.5;
|
|
397
491
|
|
|
398
|
-
const rax = px -
|
|
399
|
-
const
|
|
400
|
-
const raz = pz - trA.position.z;
|
|
401
|
-
const rbx = px - trB.position.x;
|
|
402
|
-
const rby = py - trB.position.y;
|
|
403
|
-
const rbz = pz - trB.position.z;
|
|
492
|
+
const rax = px - pAx, ray = py - pAy, raz = pz - pAz;
|
|
493
|
+
const rbx = px - pBx, rby = py - pBy, rbz = pz - pBz;
|
|
404
494
|
|
|
405
495
|
const pre_off = c * PRE_STRIDE;
|
|
406
|
-
pre[pre_off] = rax; pre[pre_off + 1] = ray; pre[pre_off + 2] = raz;
|
|
407
|
-
pre[pre_off + 3] = rbx; pre[pre_off + 4] = rby; pre[pre_off + 5] = rbz;
|
|
408
|
-
build_tangents(pre, pre_off + 6, nx, ny, nz);
|
|
409
496
|
|
|
410
|
-
|
|
411
|
-
|
|
497
|
+
// Per-body witness anchors in LOCAL frame (constant across the
|
|
498
|
+
// outer step). localW = R⁻¹ · (witness − COM).
|
|
499
|
+
v3_quat3_apply_inverse(pre, pre_off, wax - pAx, way - pAy, waz - pAz, qA[0], qA[1], qA[2], qA[3]);
|
|
500
|
+
v3_quat3_apply_inverse(pre, pre_off + 3, wbx - pBx, wby - pBy, wbz - pBz, qB[0], qB[1], qB[2], qB[3]);
|
|
501
|
+
|
|
502
|
+
// Lever arms from COM to the contact midpoint (refreshed each
|
|
503
|
+
// substep; seeded here from prepare-time pose).
|
|
504
|
+
pre[pre_off + 6] = rax; pre[pre_off + 7] = ray; pre[pre_off + 8] = raz;
|
|
505
|
+
pre[pre_off + 9] = rbx; pre[pre_off + 10] = rby; pre[pre_off + 11] = rbz;
|
|
506
|
+
|
|
507
|
+
build_tangents(pre, pre_off + 12, nx, ny, nz);
|
|
508
|
+
const t1x = pre[pre_off + 12], t1y = pre[pre_off + 13], t1z = pre[pre_off + 14];
|
|
509
|
+
const t2x = pre[pre_off + 15], t2y = pre[pre_off + 16], t2z = pre[pre_off + 17];
|
|
412
510
|
|
|
413
|
-
// K = invMA + invMB + (rA×axis)^T·I_w⁻¹_A·(rA×axis) + (rB×axis)^T·I_w⁻¹_B·(rB×axis)
|
|
414
511
|
const k_n = invMA + invMB
|
|
415
512
|
+ angular_jacobian_contribution(rbA, trA, rax, ray, raz, nx, ny, nz, scratch_inertia_a)
|
|
416
513
|
+ angular_jacobian_contribution(rbB, trB, rbx, rby, rbz, nx, ny, nz, scratch_inertia_b);
|
|
@@ -421,15 +518,13 @@ function solve_island(manifolds, system, dt, iters, slot_list, slot_start, slot_
|
|
|
421
518
|
+ angular_jacobian_contribution(rbA, trA, rax, ray, raz, t2x, t2y, t2z, scratch_inertia_a)
|
|
422
519
|
+ angular_jacobian_contribution(rbB, trB, rbx, rby, rbz, t2x, t2y, t2z, scratch_inertia_b);
|
|
423
520
|
|
|
424
|
-
|
|
425
|
-
pre[pre_off +
|
|
426
|
-
pre[pre_off +
|
|
521
|
+
const k_n_eff = k_n + g_spook_eps;
|
|
522
|
+
pre[pre_off + 18] = k_n_eff > 0 ? 1 / k_n_eff : 0;
|
|
523
|
+
pre[pre_off + 19] = k_t1 > 0 ? 1 / k_t1 : 0;
|
|
524
|
+
pre[pre_off + 20] = k_t2 > 0 ? 1 / k_t2 : 0;
|
|
427
525
|
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
// dv = vA_at_contact − vB_at_contact; vn = dv · n.
|
|
431
|
-
// Cache the typed-array views once — Vector3 extends Float64Array,
|
|
432
|
-
// so `lv[0]` is a direct memory read, no signal observer chain.
|
|
526
|
+
// Restitution approach velocity (captured once, this is the
|
|
527
|
+
// closing speed entering the step). n is B → A; vn < 0 closing.
|
|
433
528
|
const lvA = rbA.linearVelocity, avA = rbA.angularVelocity;
|
|
434
529
|
const lvB = rbB.linearVelocity, avB = rbB.angularVelocity;
|
|
435
530
|
const vAx_at = lvA[0] + avA[1] * raz - avA[2] * ray;
|
|
@@ -438,62 +533,195 @@ function solve_island(manifolds, system, dt, iters, slot_list, slot_start, slot_
|
|
|
438
533
|
const vBx_at = lvB[0] + avB[1] * rbz - avB[2] * rby;
|
|
439
534
|
const vBy_at = lvB[1] + avB[2] * rbx - avB[0] * rbz;
|
|
440
535
|
const vBz_at = lvB[2] + avB[0] * rby - avB[1] * rbx;
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const vn_pre = dvx * nx + dvy * ny + dvz * nz;
|
|
445
|
-
|
|
446
|
-
// Baumgarte position correction: gentle separation push when depth
|
|
447
|
-
// exceeds the slop tolerance.
|
|
448
|
-
let bias = 0;
|
|
449
|
-
if (depth > PENETRATION_SLOP) {
|
|
450
|
-
bias = -BAUMGARTE_BETA / dt * (depth - PENETRATION_SLOP);
|
|
451
|
-
// negative because the solver formula is `lambda = m_eff * (-vn + bias_pos)`
|
|
452
|
-
// → bias_pos = beta/dt * pen → we'll flip below by using `lambda = -m_eff * (vn + bias)`
|
|
453
|
-
// For clarity: target vn_new = -bias_pos (i.e. positive separation velocity).
|
|
454
|
-
// Stored sign convention: lambda = -m_eff * (vn + bias) with bias < 0 to push apart.
|
|
455
|
-
}
|
|
456
|
-
// Restitution: if closing fast, push the post-impulse vn to -e * vn_pre.
|
|
536
|
+
const vn_pre = (vAx_at - vBx_at) * nx + (vAy_at - vBy_at) * ny + (vAz_at - vBz_at) * nz;
|
|
537
|
+
|
|
538
|
+
let rest_bias = 0;
|
|
457
539
|
if (vn_pre < -RESTITUTION_VELOCITY_THRESHOLD) {
|
|
458
|
-
|
|
459
|
-
// restitution_combined > 0, vn_pre < 0 → contribution is negative,
|
|
460
|
-
// reinforcing separation.
|
|
540
|
+
rest_bias = restitution_combined * vn_pre;
|
|
461
541
|
}
|
|
462
|
-
pre[pre_off +
|
|
542
|
+
pre[pre_off + 21] = rest_bias;
|
|
543
|
+
pre[pre_off + 22] = 0; // bias_position — filled by refresh_contacts
|
|
463
544
|
|
|
464
545
|
mus[c] = friction_combined;
|
|
546
|
+
pos_jn[c] = 0;
|
|
547
|
+
scratch_max_jn[c] = 0;
|
|
465
548
|
|
|
466
549
|
idx[c * INDEX_STRIDE] = slot;
|
|
467
550
|
idx[c * INDEX_STRIDE + 1] = k;
|
|
468
551
|
idx[c * INDEX_STRIDE + 2] = idxA;
|
|
469
552
|
idx[c * INDEX_STRIDE + 3] = idxB;
|
|
470
553
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
// reflected in the current velocity, re-applying would
|
|
478
|
-
// double-count).
|
|
479
|
-
const j_n = data[off + 10];
|
|
480
|
-
const j_t1 = data[off + 11];
|
|
481
|
-
const j_t2 = data[off + 12];
|
|
482
|
-
const Px = nx * j_n + t1x * j_t1 + t2x * j_t2;
|
|
483
|
-
const Py = ny * j_n + t1y * j_t1 + t2y * j_t2;
|
|
484
|
-
const Pz = nz * j_n + t1z * j_t1 + t2z * j_t2;
|
|
485
|
-
|
|
486
|
-
apply_impulse_to_body(rbA, trA, invMA, rax, ray, raz, Px, Py, Pz, +1, scratch_inertia_a);
|
|
487
|
-
apply_impulse_to_body(rbB, trB, invMB, rbx, rby, rbz, Px, Py, Pz, -1, scratch_inertia_b);
|
|
488
|
-
}
|
|
554
|
+
// Warm-start is NOT applied here: under TGS it must be replayed
|
|
555
|
+
// *every substep* (see warm_start_contacts) so that, per substep,
|
|
556
|
+
// the cached impulse balances exactly one substep of gravity.
|
|
557
|
+
// Applying the cached impulse once against a full frame of
|
|
558
|
+
// gravity (the non-substepped pattern) over-pushes resting
|
|
559
|
+
// contacts and jitters / explodes deep stacks.
|
|
489
560
|
|
|
490
561
|
c++;
|
|
491
562
|
}
|
|
492
563
|
}
|
|
493
564
|
|
|
494
|
-
|
|
565
|
+
g_contact_count = c;
|
|
566
|
+
return c;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Stage 1b (per substep) — warm-start: replay the cached accumulated
|
|
571
|
+
* impulses `(j_n, j_t1, j_t2)` onto persistent velocity using the current
|
|
572
|
+
* (refreshed) lever arms and tangents. Run once per substep, after
|
|
573
|
+
* {@link refresh_contacts} and before {@link solve_velocity}.
|
|
574
|
+
*
|
|
575
|
+
* Per-substep warm-start is the crux of stable TGS: the stored impulse is a
|
|
576
|
+
* per-substep quantity (≈ the impulse to counter one substep of gravity), so
|
|
577
|
+
* replaying it each substep balances that substep's `integrate_velocity_gravity`
|
|
578
|
+
* and a resting contact holds at zero velocity. `solve_velocity` then only
|
|
579
|
+
* has to correct the residual, which converges in a few iterations even for
|
|
580
|
+
* deep chains because each substep carries just `h` of gravity.
|
|
581
|
+
*
|
|
582
|
+
* @param {ManifoldStore} manifolds
|
|
583
|
+
* @param {PhysicsSystem} system
|
|
584
|
+
*/
|
|
585
|
+
export function warm_start_contacts(manifolds, system) {
|
|
586
|
+
const count = g_contact_count;
|
|
587
|
+
if (count === 0) return;
|
|
588
|
+
|
|
589
|
+
const data = manifolds.data_buffer;
|
|
590
|
+
const pre = scratch_pre;
|
|
591
|
+
const idx = scratch_idx;
|
|
592
|
+
|
|
593
|
+
for (let ci = 0; ci < count; ci++) {
|
|
594
|
+
const slot = idx[ci * INDEX_STRIDE];
|
|
595
|
+
const cidx = idx[ci * INDEX_STRIDE + 1];
|
|
596
|
+
const idxA = idx[ci * INDEX_STRIDE + 2];
|
|
597
|
+
const idxB = idx[ci * INDEX_STRIDE + 3];
|
|
598
|
+
const rbA = system.__bodies[idxA];
|
|
599
|
+
const rbB = system.__bodies[idxB];
|
|
600
|
+
const trA = system.__transforms[idxA];
|
|
601
|
+
const trB = system.__transforms[idxB];
|
|
602
|
+
const invMA = inv_mass_of(rbA);
|
|
603
|
+
const invMB = inv_mass_of(rbB);
|
|
604
|
+
|
|
605
|
+
const pre_off = ci * PRE_STRIDE;
|
|
606
|
+
const rax = pre[pre_off + 6], ray = pre[pre_off + 7], raz = pre[pre_off + 8];
|
|
607
|
+
const rbx = pre[pre_off + 9], rby = pre[pre_off + 10], rbz = pre[pre_off + 11];
|
|
608
|
+
const t1x = pre[pre_off + 12], t1y = pre[pre_off + 13], t1z = pre[pre_off + 14];
|
|
609
|
+
const t2x = pre[pre_off + 15], t2y = pre[pre_off + 16], t2z = pre[pre_off + 17];
|
|
610
|
+
|
|
611
|
+
const slot_off = manifolds.slot_data_offset(slot);
|
|
612
|
+
const off = slot_off + cidx * CONTACT_STRIDE;
|
|
613
|
+
const nx = data[off + 6], ny = data[off + 7], nz = data[off + 8];
|
|
614
|
+
|
|
615
|
+
const j_n = data[off + 10];
|
|
616
|
+
const j_t1 = data[off + 11];
|
|
617
|
+
const j_t2 = data[off + 12];
|
|
618
|
+
const Px = nx * j_n + t1x * j_t1 + t2x * j_t2;
|
|
619
|
+
const Py = ny * j_n + t1y * j_t1 + t2y * j_t2;
|
|
620
|
+
const Pz = nz * j_n + t1z * j_t1 + t2z * j_t2;
|
|
621
|
+
apply_impulse_to_body(rbA, trA, invMA, rax, ray, raz, Px, Py, Pz, +1, scratch_inertia_a);
|
|
622
|
+
apply_impulse_to_body(rbB, trB, invMB, rbx, rby, rbz, Px, Py, Pz, -1, scratch_inertia_b);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Stage 2 (per substep) — re-derive each contact's geometry from the
|
|
628
|
+
* bodies' current poses and the local witness anchors captured at prepare.
|
|
629
|
+
*
|
|
630
|
+
* For each contact:
|
|
631
|
+
* - rotate the stored local witnesses back to world by the current pose to
|
|
632
|
+
* get the moved contact points `cpA`, `cpB`;
|
|
633
|
+
* - current penetration `depth_now = depth0 − Δseparation`, where
|
|
634
|
+
* `Δseparation = (cpA − wA − (cpB − wB)) · n` is the change since prepare.
|
|
635
|
+
* Anchoring on the trusted prepare-time depth makes the sign convention
|
|
636
|
+
* irrelevant — only the analytic delta uses the anchors;
|
|
637
|
+
* - rebuild the impulse levers `rA`/`rB` from the moved midpoint;
|
|
638
|
+
* - recompute the position-correction bias from `depth_now`.
|
|
639
|
+
*
|
|
640
|
+
* The contact normal and tangents are held fixed for the outer step (valid
|
|
641
|
+
* for the small per-step rotation), so they are not recomputed here.
|
|
642
|
+
*
|
|
643
|
+
* @param {ManifoldStore} manifolds
|
|
644
|
+
* @param {PhysicsSystem} system
|
|
645
|
+
*/
|
|
646
|
+
export function refresh_contacts(manifolds, system) {
|
|
647
|
+
const count = g_contact_count;
|
|
648
|
+
if (count === 0) return;
|
|
649
|
+
|
|
650
|
+
const data = manifolds.data_buffer;
|
|
651
|
+
const pre = scratch_pre;
|
|
652
|
+
const idx = scratch_idx;
|
|
653
|
+
const cp = scratch_cp;
|
|
654
|
+
const spook_a = g_spook_a;
|
|
655
|
+
|
|
656
|
+
for (let ci = 0; ci < count; ci++) {
|
|
657
|
+
const slot = idx[ci * INDEX_STRIDE];
|
|
658
|
+
const cidx = idx[ci * INDEX_STRIDE + 1];
|
|
659
|
+
const idxA = idx[ci * INDEX_STRIDE + 2];
|
|
660
|
+
const idxB = idx[ci * INDEX_STRIDE + 3];
|
|
661
|
+
const trA = system.__transforms[idxA];
|
|
662
|
+
const trB = system.__transforms[idxB];
|
|
663
|
+
|
|
664
|
+
const slot_off = manifolds.slot_data_offset(slot);
|
|
665
|
+
const off = slot_off + cidx * CONTACT_STRIDE;
|
|
666
|
+
|
|
667
|
+
const wax = data[off], way = data[off + 1], waz = data[off + 2];
|
|
668
|
+
const wbx = data[off + 3], wby = data[off + 4], wbz = data[off + 5];
|
|
669
|
+
const nx = data[off + 6], ny = data[off + 7], nz = data[off + 8];
|
|
670
|
+
const depth0 = data[off + 9];
|
|
671
|
+
|
|
672
|
+
const pre_off = ci * PRE_STRIDE;
|
|
673
|
+
|
|
674
|
+
const pAx = trA.position.x, pAy = trA.position.y, pAz = trA.position.z;
|
|
675
|
+
const pBx = trB.position.x, pBy = trB.position.y, pBz = trB.position.z;
|
|
676
|
+
const qA = trA.rotation, qB = trB.rotation;
|
|
677
|
+
|
|
678
|
+
// Current world contact points: COM + R · localWitness.
|
|
679
|
+
v3_quat3_apply(cp, 0, pre[pre_off], pre[pre_off + 1], pre[pre_off + 2], qA[0], qA[1], qA[2], qA[3]);
|
|
680
|
+
v3_quat3_apply(cp, 3, pre[pre_off + 3], pre[pre_off + 4], pre[pre_off + 5], qB[0], qB[1], qB[2], qB[3]);
|
|
681
|
+
const cpAx = pAx + cp[0], cpAy = pAy + cp[1], cpAz = pAz + cp[2];
|
|
682
|
+
const cpBx = pBx + cp[3], cpBy = pBy + cp[4], cpBz = pBz + cp[5];
|
|
683
|
+
|
|
684
|
+
// Penetration re-derived as a delta from the trusted prepare depth.
|
|
685
|
+
// Δsep = ((cpA − wA) − (cpB − wB)) · n.
|
|
686
|
+
const dsep = ((cpAx - wax) - (cpBx - wbx)) * nx
|
|
687
|
+
+ ((cpAy - way) - (cpBy - wby)) * ny
|
|
688
|
+
+ ((cpAz - waz) - (cpBz - wbz)) * nz;
|
|
689
|
+
const depth_now = depth0 - dsep;
|
|
690
|
+
|
|
691
|
+
// Impulse levers from each COM to the current contact midpoint.
|
|
692
|
+
const mx = (cpAx + cpBx) * 0.5;
|
|
693
|
+
const my = (cpAy + cpBy) * 0.5;
|
|
694
|
+
const mz = (cpAz + cpBz) * 0.5;
|
|
695
|
+
pre[pre_off + 6] = mx - pAx; pre[pre_off + 7] = my - pAy; pre[pre_off + 8] = mz - pAz;
|
|
696
|
+
pre[pre_off + 9] = mx - pBx; pre[pre_off + 10] = my - pBy; pre[pre_off + 11] = mz - pBz;
|
|
697
|
+
|
|
698
|
+
let bias_position = 0;
|
|
699
|
+
if (depth_now > PENETRATION_SLOP) {
|
|
700
|
+
bias_position = -spook_a * (depth_now - PENETRATION_SLOP);
|
|
701
|
+
if (bias_position < -MAX_POSITION_BIAS) bias_position = -MAX_POSITION_BIAS;
|
|
702
|
+
}
|
|
703
|
+
pre[pre_off + 22] = bias_position;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Stage 3 (per substep) — velocity iterations enforcing pure
|
|
709
|
+
* non-penetration (`vn → 0`) plus Coulomb-disk friction. No bias: depth
|
|
710
|
+
* correction is the position pass, restitution is the one-shot pass.
|
|
711
|
+
*
|
|
712
|
+
* @param {ManifoldStore} manifolds
|
|
713
|
+
* @param {PhysicsSystem} system
|
|
714
|
+
* @param {number} iters
|
|
715
|
+
*/
|
|
716
|
+
export function solve_velocity(manifolds, system, iters) {
|
|
717
|
+
const contact_count = g_contact_count;
|
|
718
|
+
if (contact_count === 0) return;
|
|
719
|
+
|
|
720
|
+
const data = manifolds.data_buffer;
|
|
721
|
+
const pre = scratch_pre;
|
|
722
|
+
const idx = scratch_idx;
|
|
723
|
+
const mus = scratch_mu;
|
|
495
724
|
|
|
496
|
-
// 3. Velocity iterations.
|
|
497
725
|
for (let iter = 0; iter < iters; iter++) {
|
|
498
726
|
for (let ci = 0; ci < contact_count; ci++) {
|
|
499
727
|
const slot = idx[ci * INDEX_STRIDE];
|
|
@@ -508,22 +736,18 @@ function solve_island(manifolds, system, dt, iters, slot_list, slot_start, slot_
|
|
|
508
736
|
const invMB = inv_mass_of(rbB);
|
|
509
737
|
|
|
510
738
|
const pre_off = ci * PRE_STRIDE;
|
|
511
|
-
const rax = pre[pre_off],
|
|
512
|
-
const rbx = pre[pre_off +
|
|
513
|
-
const t1x = pre[pre_off +
|
|
514
|
-
const t2x = pre[pre_off +
|
|
515
|
-
const m_eff_n = pre[pre_off +
|
|
516
|
-
const m_eff_t1 = pre[pre_off +
|
|
517
|
-
const m_eff_t2 = pre[pre_off +
|
|
518
|
-
const bias_n = pre[pre_off + 15];
|
|
739
|
+
const rax = pre[pre_off + 6], ray = pre[pre_off + 7], raz = pre[pre_off + 8];
|
|
740
|
+
const rbx = pre[pre_off + 9], rby = pre[pre_off + 10], rbz = pre[pre_off + 11];
|
|
741
|
+
const t1x = pre[pre_off + 12], t1y = pre[pre_off + 13], t1z = pre[pre_off + 14];
|
|
742
|
+
const t2x = pre[pre_off + 15], t2y = pre[pre_off + 16], t2z = pre[pre_off + 17];
|
|
743
|
+
const m_eff_n = pre[pre_off + 18];
|
|
744
|
+
const m_eff_t1 = pre[pre_off + 19];
|
|
745
|
+
const m_eff_t2 = pre[pre_off + 20];
|
|
519
746
|
|
|
520
747
|
const slot_off = manifolds.slot_data_offset(slot);
|
|
521
748
|
const off = slot_off + cidx * CONTACT_STRIDE;
|
|
522
749
|
const nx = data[off + 6], ny = data[off + 7], nz = data[off + 8];
|
|
523
750
|
|
|
524
|
-
// --- Current relative velocity at contact (linear + angular) ---
|
|
525
|
-
// Cache typed-array views once; direct indexing avoids the
|
|
526
|
-
// accessor chain on Vector3.x/y/z.
|
|
527
751
|
const lvA = rbA.linearVelocity, avA = rbA.angularVelocity;
|
|
528
752
|
const lvB = rbB.linearVelocity, avB = rbB.angularVelocity;
|
|
529
753
|
|
|
@@ -537,14 +761,15 @@ function solve_island(manifolds, system, dt, iters, slot_list, slot_start, slot_
|
|
|
537
761
|
const dvy = vAy_at - vBy_at;
|
|
538
762
|
const dvz = vAz_at - vBz_at;
|
|
539
763
|
|
|
540
|
-
// --- Normal impulse ---
|
|
764
|
+
// --- Normal impulse (non-penetration: drive vn → 0) ---
|
|
541
765
|
const vn = dvx * nx + dvy * ny + dvz * nz;
|
|
542
766
|
const j_n_accum = data[off + 10];
|
|
543
|
-
const lambda_n = -m_eff_n *
|
|
767
|
+
const lambda_n = -m_eff_n * vn;
|
|
544
768
|
const sum_n = j_n_accum + lambda_n;
|
|
545
769
|
const new_j_n = sum_n > 0 ? sum_n : 0;
|
|
546
770
|
const delta_j_n = new_j_n - j_n_accum;
|
|
547
771
|
data[off + 10] = new_j_n;
|
|
772
|
+
if (new_j_n > scratch_max_jn[ci]) scratch_max_jn[ci] = new_j_n;
|
|
548
773
|
|
|
549
774
|
if (delta_j_n !== 0) {
|
|
550
775
|
const Pnx = nx * delta_j_n;
|
|
@@ -555,8 +780,6 @@ function solve_island(manifolds, system, dt, iters, slot_list, slot_start, slot_
|
|
|
555
780
|
}
|
|
556
781
|
|
|
557
782
|
// --- Friction impulse (Coulomb disk in tangent plane) ---
|
|
558
|
-
// Recompute relative velocity at contact after the normal impulse.
|
|
559
|
-
// lvA/avA/lvB/avB still alias the same arrays, so we just re-read.
|
|
560
783
|
const vAx2 = lvA[0] + avA[1] * raz - avA[2] * ray;
|
|
561
784
|
const vAy2 = lvA[1] + avA[2] * rax - avA[0] * raz;
|
|
562
785
|
const vAz2 = lvA[2] + avA[0] * ray - avA[1] * rax;
|
|
@@ -596,3 +819,198 @@ function solve_island(manifolds, system, dt, iters, slot_list, slot_start, slot_
|
|
|
596
819
|
}
|
|
597
820
|
}
|
|
598
821
|
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Stage 4 (once, after the substep loop) — one-shot restitution
|
|
825
|
+
* (Box2D-v3 `b2ApplyRestitution`, Catto 2018). Drives `vn → -e · vn_approach`
|
|
826
|
+
* exactly once per closing contact, gated on (a) the contact having been
|
|
827
|
+
* closing faster than the threshold at prepare, and (b) a compressive
|
|
828
|
+
* normal impulse having formed during the velocity solve. The added impulse
|
|
829
|
+
* accumulates into the same normal-impulse slot so it composes with
|
|
830
|
+
* warm-start next frame.
|
|
831
|
+
*
|
|
832
|
+
* @param {ManifoldStore} manifolds
|
|
833
|
+
* @param {PhysicsSystem} system
|
|
834
|
+
*/
|
|
835
|
+
export function apply_restitution(manifolds, system) {
|
|
836
|
+
const contact_count = g_contact_count;
|
|
837
|
+
if (contact_count === 0) return;
|
|
838
|
+
|
|
839
|
+
const data = manifolds.data_buffer;
|
|
840
|
+
const pre = scratch_pre;
|
|
841
|
+
const idx = scratch_idx;
|
|
842
|
+
|
|
843
|
+
for (let ci = 0; ci < contact_count; ci++) {
|
|
844
|
+
const pre_off = ci * PRE_STRIDE;
|
|
845
|
+
const rest_bias = pre[pre_off + 21];
|
|
846
|
+
if (rest_bias === 0) continue;
|
|
847
|
+
|
|
848
|
+
const slot = idx[ci * INDEX_STRIDE];
|
|
849
|
+
const cidx = idx[ci * INDEX_STRIDE + 1];
|
|
850
|
+
// Gate on the running max normal impulse, not the end-of-loop value:
|
|
851
|
+
// a transient collision relaxes back to j_n ≈ 0 under per-substep
|
|
852
|
+
// warm-start, but it WAS compressive, so it should still bounce.
|
|
853
|
+
if (scratch_max_jn[ci] <= 0) continue;
|
|
854
|
+
|
|
855
|
+
const slot_off = manifolds.slot_data_offset(slot);
|
|
856
|
+
const off = slot_off + cidx * CONTACT_STRIDE;
|
|
857
|
+
|
|
858
|
+
const j_n_accum = data[off + 10];
|
|
859
|
+
|
|
860
|
+
const idxA = idx[ci * INDEX_STRIDE + 2];
|
|
861
|
+
const idxB = idx[ci * INDEX_STRIDE + 3];
|
|
862
|
+
const rbA = system.__bodies[idxA];
|
|
863
|
+
const rbB = system.__bodies[idxB];
|
|
864
|
+
const trA = system.__transforms[idxA];
|
|
865
|
+
const trB = system.__transforms[idxB];
|
|
866
|
+
const invMA = inv_mass_of(rbA);
|
|
867
|
+
const invMB = inv_mass_of(rbB);
|
|
868
|
+
|
|
869
|
+
const rax = pre[pre_off + 6], ray = pre[pre_off + 7], raz = pre[pre_off + 8];
|
|
870
|
+
const rbx = pre[pre_off + 9], rby = pre[pre_off + 10], rbz = pre[pre_off + 11];
|
|
871
|
+
const m_eff_n = pre[pre_off + 18];
|
|
872
|
+
const nx = data[off + 6], ny = data[off + 7], nz = data[off + 8];
|
|
873
|
+
|
|
874
|
+
const lvA = rbA.linearVelocity, avA = rbA.angularVelocity;
|
|
875
|
+
const lvB = rbB.linearVelocity, avB = rbB.angularVelocity;
|
|
876
|
+
const vAx_at = lvA[0] + avA[1] * raz - avA[2] * ray;
|
|
877
|
+
const vAy_at = lvA[1] + avA[2] * rax - avA[0] * raz;
|
|
878
|
+
const vAz_at = lvA[2] + avA[0] * ray - avA[1] * rax;
|
|
879
|
+
const vBx_at = lvB[0] + avB[1] * rbz - avB[2] * rby;
|
|
880
|
+
const vBy_at = lvB[1] + avB[2] * rbx - avB[0] * rbz;
|
|
881
|
+
const vBz_at = lvB[2] + avB[0] * rby - avB[1] * rbx;
|
|
882
|
+
const vn = (vAx_at - vBx_at) * nx + (vAy_at - vBy_at) * ny + (vAz_at - vBz_at) * nz;
|
|
883
|
+
|
|
884
|
+
const lambda = -m_eff_n * (vn + rest_bias);
|
|
885
|
+
const new_j_n = (j_n_accum + lambda) > 0 ? (j_n_accum + lambda) : 0;
|
|
886
|
+
const delta = new_j_n - j_n_accum;
|
|
887
|
+
data[off + 10] = new_j_n;
|
|
888
|
+
|
|
889
|
+
if (delta !== 0) {
|
|
890
|
+
const Px = nx * delta, Py = ny * delta, Pz = nz * delta;
|
|
891
|
+
apply_impulse_to_body(rbA, trA, invMA, rax, ray, raz, Px, Py, Pz, +1, scratch_inertia_a);
|
|
892
|
+
apply_impulse_to_body(rbB, trB, invMB, rbx, rby, rbz, Px, Py, Pz, -1, scratch_inertia_b);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Stage 5 (per substep) — split-impulse position correction. Normal-only
|
|
899
|
+
* (friction in the pseudo-velocity pass is ill-defined). Reads the per-body
|
|
900
|
+
* pseudo-velocity, applies a clamped normal impulse driven by the refreshed
|
|
901
|
+
* `bias_position`, and writes the increment back into the pseudo buffer. The
|
|
902
|
+
* pose integrator folds pseudo-velocity into `pos += v · dt` and discards it.
|
|
903
|
+
*
|
|
904
|
+
* The pseudo buffer must be zeroed by the caller before this stage each
|
|
905
|
+
* substep (it's a per-substep correction). The position accumulator
|
|
906
|
+
* `scratch_pos_jn` is likewise reset here per substep.
|
|
907
|
+
*
|
|
908
|
+
* @param {ManifoldStore} manifolds
|
|
909
|
+
* @param {PhysicsSystem} system
|
|
910
|
+
* @param {number} pos_iters
|
|
911
|
+
*/
|
|
912
|
+
export function solve_position(manifolds, system, pos_iters) {
|
|
913
|
+
const contact_count = g_contact_count;
|
|
914
|
+
if (contact_count === 0) return;
|
|
915
|
+
|
|
916
|
+
const data = manifolds.data_buffer;
|
|
917
|
+
const pre = scratch_pre;
|
|
918
|
+
const idx = scratch_idx;
|
|
919
|
+
const pos_jn = scratch_pos_jn;
|
|
920
|
+
const pseudoVel = system.__pseudo_velocity;
|
|
921
|
+
|
|
922
|
+
// Reset the position-impulse accumulator for this substep.
|
|
923
|
+
for (let ci = 0; ci < contact_count; ci++) pos_jn[ci] = 0;
|
|
924
|
+
|
|
925
|
+
for (let iter = 0; iter < pos_iters; iter++) {
|
|
926
|
+
for (let ci = 0; ci < contact_count; ci++) {
|
|
927
|
+
const pre_off = ci * PRE_STRIDE;
|
|
928
|
+
const bias_p = pre[pre_off + 22];
|
|
929
|
+
if (bias_p === 0) continue;
|
|
930
|
+
|
|
931
|
+
const slot = idx[ci * INDEX_STRIDE];
|
|
932
|
+
const cidx = idx[ci * INDEX_STRIDE + 1];
|
|
933
|
+
const idxA = idx[ci * INDEX_STRIDE + 2];
|
|
934
|
+
const idxB = idx[ci * INDEX_STRIDE + 3];
|
|
935
|
+
const rbA = system.__bodies[idxA];
|
|
936
|
+
const rbB = system.__bodies[idxB];
|
|
937
|
+
const trA = system.__transforms[idxA];
|
|
938
|
+
const trB = system.__transforms[idxB];
|
|
939
|
+
const invMA = inv_mass_of(rbA);
|
|
940
|
+
const invMB = inv_mass_of(rbB);
|
|
941
|
+
|
|
942
|
+
const rax = pre[pre_off + 6], ray = pre[pre_off + 7], raz = pre[pre_off + 8];
|
|
943
|
+
const rbx = pre[pre_off + 9], rby = pre[pre_off + 10], rbz = pre[pre_off + 11];
|
|
944
|
+
const m_eff_n = pre[pre_off + 18];
|
|
945
|
+
|
|
946
|
+
const slot_off = manifolds.slot_data_offset(slot);
|
|
947
|
+
const off = slot_off + cidx * CONTACT_STRIDE;
|
|
948
|
+
const nx = data[off + 6], ny = data[off + 7], nz = data[off + 8];
|
|
949
|
+
|
|
950
|
+
const baseA = idxA * PSEUDO_STRIDE;
|
|
951
|
+
const baseB = idxB * PSEUDO_STRIDE;
|
|
952
|
+
|
|
953
|
+
const pslA_x = pseudoVel[baseA], pslA_y = pseudoVel[baseA + 1], pslA_z = pseudoVel[baseA + 2];
|
|
954
|
+
const psaA_x = pseudoVel[baseA + 3], psaA_y = pseudoVel[baseA + 4], psaA_z = pseudoVel[baseA + 5];
|
|
955
|
+
const pslB_x = pseudoVel[baseB], pslB_y = pseudoVel[baseB + 1], pslB_z = pseudoVel[baseB + 2];
|
|
956
|
+
const psaB_x = pseudoVel[baseB + 3], psaB_y = pseudoVel[baseB + 4], psaB_z = pseudoVel[baseB + 5];
|
|
957
|
+
|
|
958
|
+
const psvAx_at = pslA_x + psaA_y * raz - psaA_z * ray;
|
|
959
|
+
const psvAy_at = pslA_y + psaA_z * rax - psaA_x * raz;
|
|
960
|
+
const psvAz_at = pslA_z + psaA_x * ray - psaA_y * rax;
|
|
961
|
+
const psvBx_at = pslB_x + psaB_y * rbz - psaB_z * rby;
|
|
962
|
+
const psvBy_at = pslB_y + psaB_z * rbx - psaB_x * rbz;
|
|
963
|
+
const psvBz_at = pslB_z + psaB_x * rby - psaB_y * rbx;
|
|
964
|
+
|
|
965
|
+
const psdvx = psvAx_at - psvBx_at;
|
|
966
|
+
const psdvy = psvAy_at - psvBy_at;
|
|
967
|
+
const psdvz = psvAz_at - psvBz_at;
|
|
968
|
+
const psvn = psdvx * nx + psdvy * ny + psdvz * nz;
|
|
969
|
+
|
|
970
|
+
const jn_accum = pos_jn[ci];
|
|
971
|
+
const lambda = -m_eff_n * (psvn + bias_p);
|
|
972
|
+
const sum = jn_accum + lambda;
|
|
973
|
+
const new_jn = sum > 0 ? sum : 0;
|
|
974
|
+
const delta = new_jn - jn_accum;
|
|
975
|
+
pos_jn[ci] = new_jn;
|
|
976
|
+
|
|
977
|
+
if (delta !== 0) {
|
|
978
|
+
const Px = nx * delta;
|
|
979
|
+
const Py = ny * delta;
|
|
980
|
+
const Pz = nz * delta;
|
|
981
|
+
apply_impulse_to_pseudo(pseudoVel, baseA, rbA, trA, invMA, rax, ray, raz, Px, Py, Pz, +1, scratch_inertia_a);
|
|
982
|
+
apply_impulse_to_pseudo(pseudoVel, baseB, rbB, trB, invMB, rbx, rby, rbz, Px, Py, Pz, -1, scratch_inertia_b);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* Convenience single-step driver: prepare → refresh → velocity →
|
|
990
|
+
* restitution → position, all at the full `dt` (one substep). Equivalent to
|
|
991
|
+
* the Phase-2 solver. The substepped path in `PhysicsSystem.fixedUpdate`
|
|
992
|
+
* calls the stages directly; this entry point exists for callers/tests that
|
|
993
|
+
* want a one-shot solve.
|
|
994
|
+
*
|
|
995
|
+
* The position pass writes `system.__pseudo_velocity`; the caller must zero
|
|
996
|
+
* that buffer before this call and fold it into the pose afterwards.
|
|
997
|
+
*
|
|
998
|
+
* @param {ManifoldStore} manifolds
|
|
999
|
+
* @param {PhysicsSystem} system
|
|
1000
|
+
* @param {number} dt
|
|
1001
|
+
* @param {number} [iters]
|
|
1002
|
+
* @param {number} [pos_iters]
|
|
1003
|
+
*/
|
|
1004
|
+
export function solve_contacts(manifolds, system, dt,
|
|
1005
|
+
iters = DEFAULT_VELOCITY_ITERATIONS,
|
|
1006
|
+
pos_iters = DEFAULT_POSITION_ITERATIONS) {
|
|
1007
|
+
if (dt <= 0) return;
|
|
1008
|
+
if (prepare_contacts(manifolds, system, dt) === 0) return;
|
|
1009
|
+
refresh_contacts(manifolds, system);
|
|
1010
|
+
warm_start_contacts(manifolds, system);
|
|
1011
|
+
solve_velocity(manifolds, system, iters);
|
|
1012
|
+
apply_restitution(manifolds, system);
|
|
1013
|
+
solve_position(manifolds, system, pos_iters);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
export { DEFAULT_VELOCITY_ITERATIONS, DEFAULT_POSITION_ITERATIONS };
|