Ball-and-socket and hinge joints with angle limits hold a capsule chain, and training dummies hang in the first map #22

Merged
agra merged 5 commits from task/3903-ball-and-socket-and-hinge-joints-with-an into master 2026-10-30 12:21:53 +02:00
Owner

Tracker: #3903

Step 4 of plan #3865. The one solver gains joints, and the first map gains two training dummies the player can knock about.

What it delivers

  • Joints (src/physics/joints.sx, solved in src/physics/solver.sx). A joint joins two bodies, or a body and a fixed frame in the world, at a frame on each; addJoint gives its index.
    • Ball-and-socket: its points stay together; optional swing cone about the twist axis (the frame's +Y), an ellipse of two half-angles about the frame's X and Z, and a twist range.
    • Hinge: its points stay together and its axes (the frame's +X) stay aligned; optional angle range.
    • Soft constraints at 60 Hz, damping ratio 2, in the biased pass and rigid in the relax (JOINT_HERTZ, JOINT_DAMPING_RATIO beside the contact constants). Limits are speculative: while within, a limit lets the joint close at most its room in the substep.
    • Pass order: the warm start, the biased pass and the relax each go through the joints between two bodies, the contacts between two bodies (players' included), the joints to the world, and the map's contacts last (solvePass).
    • Angles come from atan2f, declared extern as the stage declares it. Every angle between two axes is atan2(|a×b|, a·b), exact near 0, so no acosf is needed: an acos of a dot product that rounds to 1 while the cross product does not gave the cone a zero gradient and a NaN.
    • Two bodies a joint joins do not collide; a joint joins its bodies' island; a sleeping body a joint ties to an awake one wakes.
  • Joined bodies end the tick together (src/physics/dynamics.sx, sweepGroups). Bodies joined through joints form a group; a hit ends the tick for the whole group at its earliest hit, each body going to its pose there and keeping its velocity less the gravity the rest of the tick added. A body with no joint is a group of its own, so every figure the existing physics and sim tests print reads as master's: verbose copies of 20 of them, run in git archive arms of master d962aee and this head, printed the same 2 451 lines.
  • The dummy (src/sim/dummy.sx): 12 capsules, 75.5 kg, 1.81 m tall, 11 joints (neck 40°/±45°, spine 25°/±20°, shoulders 80°/±60°, hips 60°/±30°, elbows and knees hinged 0–140°), hung by the top of its head from a fixed point by one more ball-and-socket without limits. Its parts damp at 0.5, linear and angular: with a new Blender body's 0.04 and 0.1, a dummy a player walked into took 178 s to sleep again; at 0.5 it takes 14 s.
  • Markers. An Empty whose hulken_dummy is true marks where a dummy hangs. The extract records it (intermediate version 7) and refuses one that is not an empty; the stage writes it into the world file (version 6) in hulken's axes; the server hangs a dummy at each after the world file's bodies, so the snapshot lists the world's bodies then each dummy's parts in the template's order.
  • Drawing (src/render/dummies.sx): the app builds a capsule mesh for each part and draws the dummies at the snapshot's poses with the body vertex stage, in one flat colour.
  • The first map (assets/scenes/first_map_dummies.py, run once): a post-and-beam frame beside the props, on the clearing's level ground, two dummy markers 2.25 m above the ground under its beam, and a passable rope from each up to the beam, so a swinging dummy's head never meets the beam. first_map.blend.md and the README record it.

Tests

  • tests/physics_joints.sx: pendulum, swing cone (struck from rest and at 8 phases of a tick, each reaching the cone at 20 rad/s; an elliptic cone held along each axis), twist, hinge, hinged drop at 8 phases off the floor's diagonal, the 10-capsule chain dropped across a lone crate at 8 phases, joined bodies, waking, a joint joining islands, and determinism.
  • tests/sim_dummy.sx: the hung dummy sleeps, a player walking into it swings it and it sleeps again, the snapshots list its parts after the world's bodies, and two servers send the same bytes.
  • tests/dummy_draws.sx: the capsule meshes lie on their capsules and face out, and a hung dummy matches its golden, tests/pixels/dummy-metal.png.
  • Cases in tests/bake_extract.py, tests/bake_stage.sx and tests/first_map_world.sx for the markers.

Figures at this head

Case Figure Bound
Pendulum point 0.07 mm apart; period 1.5426 s against 1.5419 s 1 mm; 2%
Swing cone, from rest at 20 rad/s 1.01° past 2°
Swing cone, 8 phases 0.02–1.31° past 2°
Elliptic cone 20°/45° holds at 19.99° and 45.29° within 2°
Twist ±15° stops at 15.00° each way within 2°
Hinge axes 0.04° apart; free turn within 0.003%; stops at 60.00° each way 1°; 1%; 2°
Hinged drop, 8 phases opens 19.5–19.9 mm, 1 tick past 1 cm, its last tick past 2 mm the 3rd after a landing; rebounds 0.396–0.415 m, landing again 36 ticks after the first landing (reopening 6.7–7.2 mm) and a third time 46–48 ticks after it (2.5–3.2 mm); sphere at most 5.39–5.49 mm into the map; axes aligned 3 cm; 3 ticks; 6 ticks; 0.6 m; 8 mm; 1°
Chain, 8 phases 1.24–1.78° past a limit; 0.98–1.86 mm into the crate or ground; asleep after 1.27–1.33 s 3°; 5 mm; 6 s
Hung dummy asleep after 0.53 s, 0° past a limit; walked into, its pelvis swings 1.08 m and it sleeps again 14 s later 5 s, 2°

The hinged pair lands at (1.3, −0.7), off the diagonal along which flatGround splits its quad, so the sphere has one map manifold, as the task's 1D case has. Dropped at the origin, on the diagonal, it had two, one from each triangle, which doubled the map's stiffness: it sank 0.31 mm, opened 22.9–23.3 mm and rebounded 0.48–0.50 m. Off it, the figures match plan runs 1704 and 1711's models (19.5–20.4 mm, 0.395–0.429 m, 5.5 mm, landings 35–36 and 46–49 ticks on).

The chain's figures match plan runs 1721 and 1727's models (1.18–1.80°, 1.44–2.10 mm, 1.27–1.35 s), and the swing cone's from-rest strike matches their 1.06° and 1.01°.

Mutants

Each rule's mutant, applied alone in a git archive arm of the head, fails the cases named:

Mutant Fails
no cone limit Swing cone (147–149° past)
no twist limit Twist (176°), the dummy at rest
no hinge range Hinge (172°)
limits act only once passed Swing cone (4.75° from rest; up to 5.75° at the phases)
joined bodies collide Joined bodies (0.08 m apart), Hinged drop, the dummy
joints join no islands the box joined to the swinging sphere sleeps at tick 29, the chain and the dummy never sleep
no hinge alignment Hinge (axes 176° apart)
only the hit body rewinds, or groups without joints Hinged drop: 27.9–112.6 mm open, past 3 cm at 6 of 8 phases, rebound up to 1.48 m
the map's contacts before the joints Hinged drop: the sphere 11.1–11.6 mm into the map at every phase
a joint rigid in the biased pass Hinged drop: rebounds 1.13–1.21 m at every phase
a joint wakes no sleeping body Waking
the server hangs no dummy; a dummy ignores its marker's yaw; it hangs from its head's middle sim_dummy
the extract refuses no marked mesh; records no marker bake_extract
the stage drops the yaw; the world file drops the markers bake_stage
capsule triangles wound clockwise dummy_draws

These survive every case, none of them among the rules the task holds to a mutant:

  • the twist rate's axis not lengthened by the swing: no case twists a joint that is swung far;
  • the cone closing along the swing's own axis, and its room measured radially: both equal the head's along the ellipse's axes, where the cases strike;
  • the joints to the world solved before the contacts between bodies: no case has a body held by a world joint while it touches another body.

Merge

Master's d962aee (#3930, the trees reseated on the first map) is merged in. The merge takes master's first_map.blend and runs first_map_dummies.py over it again; a Blender dump of objects, matrices, mesh hashes, materials and collections shows the run adds to master's .blend exactly what the branch added to its base's: Dummy 1, Dummy 2, Dummy Frame and Dummy Ropes, the frame_wood and rope materials and the Dummies collection. The README takes master's paragraph with the dummies' script named after the props'.

Gates

  • full, gate run 936, at 7a25f3a: passed, 39 of 39 tests.
  • full, gate run 923, at e5fdbcd, before the merge: passed, 39 of 39 tests.
  • fast, gate run 922, at 769ced1: passed.

Agra's check in the app is still to do: the dummies hang beside the props and swing when walked into. A probe build of the app walked the player from the first spawn marker into the nearer dummy: its pelvis swung about 0.8 m, and it slept again 13 s after the player walked away.

Tracker: [#3903](https://sak.swipelab.com/tasks/3903) Step 4 of plan #3865. The one solver gains joints, and the first map gains two training dummies the player can knock about. ## What it delivers - **Joints** (`src/physics/joints.sx`, solved in `src/physics/solver.sx`). A joint joins two bodies, or a body and a fixed frame in the world, at a frame on each; `addJoint` gives its index. - **Ball-and-socket:** its points stay together; optional swing cone about the twist axis (the frame's +Y), an ellipse of two half-angles about the frame's X and Z, and a twist range. - **Hinge:** its points stay together and its axes (the frame's +X) stay aligned; optional angle range. - Soft constraints at 60 Hz, damping ratio 2, in the biased pass and rigid in the relax (`JOINT_HERTZ`, `JOINT_DAMPING_RATIO` beside the contact constants). Limits are speculative: while within, a limit lets the joint close at most its room in the substep. - **Pass order:** the warm start, the biased pass and the relax each go through the joints between two bodies, the contacts between two bodies (players' included), the joints to the world, and the map's contacts last (`solvePass`). - Angles come from `atan2f`, declared `extern` as the stage declares it. Every angle between two axes is `atan2(|a×b|, a·b)`, exact near 0, so no `acosf` is needed: an `acos` of a dot product that rounds to 1 while the cross product does not gave the cone a zero gradient and a NaN. - Two bodies a joint joins do not collide; a joint joins its bodies' island; a sleeping body a joint ties to an awake one wakes. - **Joined bodies end the tick together** (`src/physics/dynamics.sx`, `sweepGroups`). Bodies joined through joints form a group; a hit ends the tick for the whole group at its earliest hit, each body going to its pose there and keeping its velocity less the gravity the rest of the tick added. A body with no joint is a group of its own, so every figure the existing physics and sim tests print reads as master's: verbose copies of 20 of them, run in `git archive` arms of master d962aee and this head, printed the same 2 451 lines. - **The dummy** (`src/sim/dummy.sx`): 12 capsules, 75.5 kg, 1.81 m tall, 11 joints (neck 40°/±45°, spine 25°/±20°, shoulders 80°/±60°, hips 60°/±30°, elbows and knees hinged 0–140°), hung by the top of its head from a fixed point by one more ball-and-socket without limits. Its parts damp at 0.5, linear and angular: with a new Blender body's 0.04 and 0.1, a dummy a player walked into took 178 s to sleep again; at 0.5 it takes 14 s. - **Markers.** An Empty whose `hulken_dummy` is true marks where a dummy hangs. The extract records it (intermediate version 7) and refuses one that is not an empty; the stage writes it into the world file (version 6) in hulken's axes; the server hangs a dummy at each after the world file's bodies, so the snapshot lists the world's bodies then each dummy's parts in the template's order. - **Drawing** (`src/render/dummies.sx`): the app builds a capsule mesh for each part and draws the dummies at the snapshot's poses with the body vertex stage, in one flat colour. - **The first map** (`assets/scenes/first_map_dummies.py`, run once): a post-and-beam frame beside the props, on the clearing's level ground, two dummy markers 2.25 m above the ground under its beam, and a passable rope from each up to the beam, so a swinging dummy's head never meets the beam. `first_map.blend.md` and the README record it. ## Tests - `tests/physics_joints.sx`: pendulum, swing cone (struck from rest and at 8 phases of a tick, each reaching the cone at 20 rad/s; an elliptic cone held along each axis), twist, hinge, hinged drop at 8 phases off the floor's diagonal, the 10-capsule chain dropped across a lone crate at 8 phases, joined bodies, waking, a joint joining islands, and determinism. - `tests/sim_dummy.sx`: the hung dummy sleeps, a player walking into it swings it and it sleeps again, the snapshots list its parts after the world's bodies, and two servers send the same bytes. - `tests/dummy_draws.sx`: the capsule meshes lie on their capsules and face out, and a hung dummy matches its golden, `tests/pixels/dummy-metal.png`. - Cases in `tests/bake_extract.py`, `tests/bake_stage.sx` and `tests/first_map_world.sx` for the markers. ### Figures at this head | Case | Figure | Bound | |---|---|---| | Pendulum | point 0.07 mm apart; period 1.5426 s against 1.5419 s | 1 mm; 2% | | Swing cone, from rest at 20 rad/s | 1.01° past | 2° | | Swing cone, 8 phases | 0.02–1.31° past | 2° | | Elliptic cone 20°/45° | holds at 19.99° and 45.29° | within 2° | | Twist ±15° | stops at 15.00° each way | within 2° | | Hinge | axes 0.04° apart; free turn within 0.003%; stops at 60.00° each way | 1°; 1%; 2° | | Hinged drop, 8 phases | opens 19.5–19.9 mm, 1 tick past 1 cm, its last tick past 2 mm the 3rd after a landing; rebounds 0.396–0.415 m, landing again 36 ticks after the first landing (reopening 6.7–7.2 mm) and a third time 46–48 ticks after it (2.5–3.2 mm); sphere at most 5.39–5.49 mm into the map; axes aligned | 3 cm; 3 ticks; 6 ticks; 0.6 m; 8 mm; 1° | | Chain, 8 phases | 1.24–1.78° past a limit; 0.98–1.86 mm into the crate or ground; asleep after 1.27–1.33 s | 3°; 5 mm; 6 s | | Hung dummy | asleep after 0.53 s, 0° past a limit; walked into, its pelvis swings 1.08 m and it sleeps again 14 s later | 5 s, 2° | The hinged pair lands at (1.3, −0.7), off the diagonal along which `flatGround` splits its quad, so the sphere has one map manifold, as the task's 1D case has. Dropped at the origin, on the diagonal, it had two, one from each triangle, which doubled the map's stiffness: it sank 0.31 mm, opened 22.9–23.3 mm and rebounded 0.48–0.50 m. Off it, the figures match plan runs 1704 and 1711's models (19.5–20.4 mm, 0.395–0.429 m, 5.5 mm, landings 35–36 and 46–49 ticks on). The chain's figures match plan runs 1721 and 1727's models (1.18–1.80°, 1.44–2.10 mm, 1.27–1.35 s), and the swing cone's from-rest strike matches their 1.06° and 1.01°. ### Mutants Each rule's mutant, applied alone in a `git archive` arm of the head, fails the cases named: | Mutant | Fails | |---|---| | no cone limit | Swing cone (147–149° past) | | no twist limit | Twist (176°), the dummy at rest | | no hinge range | Hinge (172°) | | limits act only once passed | Swing cone (4.75° from rest; up to 5.75° at the phases) | | joined bodies collide | Joined bodies (0.08 m apart), Hinged drop, the dummy | | joints join no islands | the box joined to the swinging sphere sleeps at tick 29, the chain and the dummy never sleep | | no hinge alignment | Hinge (axes 176° apart) | | only the hit body rewinds, or groups without joints | Hinged drop: 27.9–112.6 mm open, past 3 cm at 6 of 8 phases, rebound up to 1.48 m | | the map's contacts before the joints | Hinged drop: the sphere 11.1–11.6 mm into the map at every phase | | a joint rigid in the biased pass | Hinged drop: rebounds 1.13–1.21 m at every phase | | a joint wakes no sleeping body | Waking | | the server hangs no dummy; a dummy ignores its marker's yaw; it hangs from its head's middle | `sim_dummy` | | the extract refuses no marked mesh; records no marker | `bake_extract` | | the stage drops the yaw; the world file drops the markers | `bake_stage` | | capsule triangles wound clockwise | `dummy_draws` | These survive every case, none of them among the rules the task holds to a mutant: - the twist rate's axis not lengthened by the swing: no case twists a joint that is swung far; - the cone closing along the swing's own axis, and its room measured radially: both equal the head's along the ellipse's axes, where the cases strike; - the joints to the world solved before the contacts between bodies: no case has a body held by a world joint while it touches another body. ## Merge Master's d962aee (#3930, the trees reseated on the first map) is merged in. The merge takes master's `first_map.blend` and runs `first_map_dummies.py` over it again; a Blender dump of objects, matrices, mesh hashes, materials and collections shows the run adds to master's `.blend` exactly what the branch added to its base's: `Dummy 1`, `Dummy 2`, `Dummy Frame` and `Dummy Ropes`, the `frame_wood` and `rope` materials and the `Dummies` collection. The README takes master's paragraph with the dummies' script named after the props'. ## Gates - `full`, gate run 936, at 7a25f3a: passed, 39 of 39 tests. - `full`, gate run 923, at e5fdbcd, before the merge: passed, 39 of 39 tests. - `fast`, gate run 922, at 769ced1: passed. Agra's check in the app is still to do: the dummies hang beside the props and swing when walked into. A probe build of the app walked the player from the first spawn marker into the nearer dummy: its pelvis swung about 0.8 m, and it slept again 13 s after the player walked away.
agra merged commit ef9407650a into master 2026-10-30 12:21:53 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
sx/hulken!22
No description provided.