The first map's crates, barrels and cannon balls are rigid bodies the server steps and the app draws #18

Merged
agra merged 20 commits from task/3901-the-first-map-s-crates-barrels-and-canno into master 2026-10-30 07:34:39 +02:00
Owner

Tracker: #3901

Step 2 of plan #3865 (Physics). Blender rigid bodies bake into the first map, the server steps them every tick, each snapshot carries their poses, and the client draws them between snapshots. The first map gains crates, barrels and cannon balls in front of its first spawn marker, which settle as the map loads. The player still walks through them until step 3.

What it delivers

  • Prefabs. assets/prefabs/kenney.py takes --centre, which puts a prefab's origin at its mesh's bounds' centre, where Blender centres a Box or Sphere body. It imported Kenney's Pirate Kit 2.1 crate.glb, barrel.glb and cannon-ball.glb (CC0, kenney_pirate-kit.zip, 3154665 bytes, SHA-256 667ed2ca…c943, downloaded again on 2026-10-30) into assets/prefabs/crate.blend, barrel.blend and cannon-ball.blend, each with its .md. The kit's colormap.png holds the texels of assets/textures/ship-wreck_colormap.png, so kenney.py now finds a texture already under assets/textures/ and the three draw that file. It also names the joined mesh for the model, which a scene links by name.

  • The map. assets/scenes/first_map_bodies.py, run once over first_map.blend:

    • cuts a clearing level into the terrain 13.5 m in front of Spawn 1, along the way it faces. The ground there rose about 5° away from the marker, where a ball rolls off for good. Every vertex within 7.5 m of the clearing's centre takes the lowest of their heights, those out to 10.5 m blend back, and every prop stands a grid cell's diagonal inside the level ground. The marker's own ground is untouched;
    • makes the terrain a Passive Mesh body at a new body's values, which makes the rigid body world;
    • adds 20 props as objects of their own in a Props collection, each mesh linked by relative path from its prefab: 10 crates as Box bodies of 20 kg (a tower of 3, a pyramid of 6, one 3 m up), 5 barrels as Convex Hull bodies of 60 kg (2 upright, 2 lying, one 2.5 m up) and 5 cannon balls as Sphere bodies of 30 kg (4 on the ground, one 2 m up).

    first_map.blend.md records them. first_map_prefabs.py's clear now removes only its own prefabs' libraries, so rerunning it keeps the props: a rerun in an arm kept all 20 and placed the same 900 trees.

  • The extract (tools/bake/extract.py, intermediate version 6) takes a mesh object with an Active rigid body in the world's collection as a body. It records the body's shape sized as Blender sizes it, its mass, friction, restitution and both dampings, its evaluated pose, and its evaluated triangles in its own space with its scale applied. It reads the scene's gravity and use_gravity. It takes a Passive Mesh body as map. It compares each rigid body with one made new in a reference scene by rigidbody.object_add of its type, its shape then set to the body's. It refuses, naming the datablock and the setting, every case D9 lists, a rigid body, Active or Passive, marked as water or as a collision proxy, a body its scale, its delta scale and its constraints together mirror, read by its world matrix's determinant, which the refusal names, and a Sphere or Capsule scaled unevenly in the world, by its scale, its delta scale or a constraint: Blender's simulation scales a Sphere's radius by the X scale alone and a Capsule's radius by its Y scale and its length by its Z scale, all read from the world matrix, so such a body would collide otherwise in hulken. A scale counts as even while its parts differ by at most 1e-5 of the largest, since a turned, evenly scaled object decomposes from its world matrix uneven by rounding (up to 5.3e-7 of its scale over 4000 random turns). rigid_body, rigid_body_constraint, rigidbody_world, gravity and use_gravity have left the lists it passes unread.

  • The stage builds each hull with hullOf. It writes each body's shape, mass, material, damping and start in hulken's axes (Blender's local Z becomes local +Y) and the scene's gravity into the world file (version 5), and each body's meshes in its own space into the render file (version 5). A body's triangles go into neither the collision world nor the static meshes.

  • The server builds a PhysicsWorld over its world's collision from the world file's bodies and gravity, and each tick steps its players, then the bodies, then posts the snapshot. The snapshot carries each body's index, position and unit quaternion (src/net/messages.sx documents the bytes). The client gives each body between its two kept snapshots, the position lerped and the orientation nlerped along the shorter arc.

  • The renderer draws each body's meshes in the opaque pass, its vertex stage placing them by a rigid transform set per draw with setVertexConstants and turning their normals by its rotation. It shares the scene's fragment stage. The app loads the body meshes and draws them at the client's poses.

  • README. The Scenes section says what a rigid body is in a scene.

Tests

  • tests/bake_extract.py: a case for each shape as Blender sizes it, a Capsule shorter than it is wide sized as a sphere, a Sphere scaled 1000 and turned, whose world scale decomposes as (1000, 1000, 999.99994), uneven by 6e-5 though only by 6e-8 of its scale, taken, a Capsule scaled evenly, a scaled and turned Box, a Convex Hull wedge scaled (1, 1, 2) whose normals are those of its scaled faces, what is read, the scene's gravity changed and off, a body whose mesh is linked from crate.blend, a Passive Mesh ground, the Passive Box, Passive Convex Hull, friction and restitution refusals, a baked cache, a Sphere scaled (1, 1, 1.00002), uneven by twice the tolerance, a Sphere of delta scale (1, 1, 2) and a Capsule a Limit Scale constraint holds at 2 along z refused, a body marked as water and the Passive Mesh ground marked as a collision proxy refused, a body scaled -1 along x and one of delta scale (-1, 1, 1) refused, the latter naming its world matrix's determinant, and each other refusal.
  • tests/bake_stage.sx: bodies through the stage and both files, a Capsule turned in Blender lying along hulken's +Z by its local +Y, a hull built, the bodies' triangles kept out of the collision world and the static meshes, the gravity, both files read back, and a flat hull refused.
  • tests/net_messages.sx: a snapshot with bodies round-trips, and one cut short in its last body is refused.
  • tests/sim_bodies.sx: a crate dropped 3 m on flat ground falls, lands and sleeps in the server's snapshots; two servers fed the same commands send the same bytes for 600 ticks; the client's pose is lerped and nlerped along the shorter arc.
  • tests/body_draws.sx: bodies turned about each axis, and a turned card drawing a cut-out checker, draw the same pixels as the same boxes and card placed statically, every pixel opaque, the card holding both texels and holes.
  • tests/first_map_bodies.sx: the baked first map's props and shapes, each asleep within 10 s above the terrain under it, the tower and the pyramid within 5 cm of their starts, and each crate settled on the terrain drawing within 1 cm of it.

Mutants

Each rule's code was changed in a git archive arm of the head, one change at a time, and the tests that cover it were rerun. 85 mutants:

  • Extract (48): each refusal dropped (an instanced, parented, modified or mirrored body, any Active shape, a baked cache, a body or a Passive body outside the world, a hulken_collide false material, a water or proxy marker on a rigid body, an unevenly scaled Sphere or Capsule, the Sphere or the Capsule alone left out of it), only the water or only the proxy marker refused, the markers refused on Active bodies alone, the mirror read from the object's own scale, the evenness read from the object's own scale, from its scale times its delta scale, or on x and y alone, compared exactly, absolutely rather than relative to the scale, or within 1e-8, 1e-4, 1e-3, 1e-2, 1e-1 or half the scale, every scaled Sphere or Capsule refused, each size rule changed (box by whole dimensions, sphere by the least, capsule length without its caps, a capsule's half segment without its floor at 0), the scale left off the corners and the points, normals left unnormalised, normals only normalised and not divided by the scale, the rotation's components reordered, rigid_body, rigid_body_constraint or rigidbody_world passed unread again, gravity or mass compared rather than read, use_gravity ignored, the preview settings compared, and the reference body's shape, type or world wrong. All killed by tests/bake_extract.py. The markers' ran at 689103d; the mirror's and the evenness's, and 689103d's extract, which fails the new cases on their names, ran against the tests at 6f5b54a.
  • Stage and files (15): box extents, capsule radius and half, hull points, rotation, position and gravity left in Blender's axes; friction and restitution swapped; a body's triangles into the static meshes; gravity not written; each hull check and the shape-kind check dropped; damping swapped; bodies not written to the render file. All killed by tests/bake_stage.sx, tests/first_map_bodies.sx or tests/first_map_views.sx.
  • Server, messages, client (11): bodies not stepped, poses not refreshed, the world's gravity or friction not taken, the snapshot without bodies, bodies not encoded, rotation components read swapped, the latest pose only, the fraction from the wrong end, nlerp along the longer arc. All killed by tests/sim_bodies.sx or tests/net_messages.sx. One survives as equivalent: the bodies stepped before the players, which nothing distinguishes until step 3 lets players touch bodies.
  • Renderer and loader (9): normals not turned, normals or corners turned by the inverse rotation, bodies not drawn, the body transform without its move, body meshes not loaded, the cut-out body stage built without CUTOUT, every body mesh drawn in the passes that are not cut-out, the cut-out body stage over the static vertex stage. All killed by tests/body_draws.sx or tests/first_map_views.sx; the three cut-out ones by tests/body_draws.sx alone.
  • Assets (2): the crate re-imported with its origin at its volume's centroid draws 11.6 cm above the terrain, and a level disc that stops short of the props' footprints leaves cannon balls rolling past 10 s; both fail tests/first_map_bodies.sx.

Gates

  • fast at 1c96f32: passed (gate run 888).
  • full at 3d2ac98: passed, 34 of 34 (gate run 889).
  • full at 22f4af3: passed, 34 of 34 (gate run 893).
  • full at 689103d: passed, 34 of 34 (gate run 895).
  • full at 6f5b54a: passed, 34 of 34 (gate run 898).

For Agra's check

In the app, the props stand in the clearing in front of the first spawn marker; the crate, barrel and ball in the air fall and land; nothing should jitter at rest. A run of the built app with a print after its frame loop showed the server stepping 20 bodies, all asleep after about 5 s, and 20 drawn.

Tracker: [#3901](https://sak.swipelab.com/tasks/3901) Step 2 of plan [#3865](https://sak.swipelab.com/tasks/3865) (Physics). Blender rigid bodies bake into the first map, the server steps them every tick, each snapshot carries their poses, and the client draws them between snapshots. The first map gains crates, barrels and cannon balls in front of its first spawn marker, which settle as the map loads. The player still walks through them until step 3. ## What it delivers - **Prefabs.** `assets/prefabs/kenney.py` takes `--centre`, which puts a prefab's origin at its mesh's bounds' centre, where Blender centres a Box or Sphere body. It imported Kenney's Pirate Kit 2.1 `crate.glb`, `barrel.glb` and `cannon-ball.glb` (CC0, `kenney_pirate-kit.zip`, 3154665 bytes, SHA-256 667ed2ca…c943, downloaded again on 2026-10-30) into `assets/prefabs/crate.blend`, `barrel.blend` and `cannon-ball.blend`, each with its `.md`. The kit's `colormap.png` holds the texels of `assets/textures/ship-wreck_colormap.png`, so kenney.py now finds a texture already under `assets/textures/` and the three draw that file. It also names the joined mesh for the model, which a scene links by name. - **The map.** `assets/scenes/first_map_bodies.py`, run once over `first_map.blend`: - cuts a clearing level into the terrain 13.5 m in front of `Spawn 1`, along the way it faces. The ground there rose about 5° away from the marker, where a ball rolls off for good. Every vertex within 7.5 m of the clearing's centre takes the lowest of their heights, those out to 10.5 m blend back, and every prop stands a grid cell's diagonal inside the level ground. The marker's own ground is untouched; - makes the terrain a Passive Mesh body at a new body's values, which makes the rigid body world; - adds 20 props as objects of their own in a `Props` collection, each mesh linked by relative path from its prefab: 10 crates as Box bodies of 20 kg (a tower of 3, a pyramid of 6, one 3 m up), 5 barrels as Convex Hull bodies of 60 kg (2 upright, 2 lying, one 2.5 m up) and 5 cannon balls as Sphere bodies of 30 kg (4 on the ground, one 2 m up). `first_map.blend.md` records them. `first_map_prefabs.py`'s clear now removes only its own prefabs' libraries, so rerunning it keeps the props: a rerun in an arm kept all 20 and placed the same 900 trees. - **The extract** (`tools/bake/extract.py`, intermediate version 6) takes a mesh object with an Active rigid body in the world's collection as a body. It records the body's shape sized as Blender sizes it, its mass, friction, restitution and both dampings, its evaluated pose, and its evaluated triangles in its own space with its scale applied. It reads the scene's gravity and `use_gravity`. It takes a Passive Mesh body as map. It compares each rigid body with one made new in a reference scene by `rigidbody.object_add` of its type, its shape then set to the body's. It refuses, naming the datablock and the setting, every case D9 lists, a rigid body, Active or Passive, marked as water or as a collision proxy, a body its scale, its delta scale and its constraints together mirror, read by its world matrix's determinant, which the refusal names, and a Sphere or Capsule scaled unevenly in the world, by its scale, its delta scale or a constraint: Blender's simulation scales a Sphere's radius by the X scale alone and a Capsule's radius by its Y scale and its length by its Z scale, all read from the world matrix, so such a body would collide otherwise in hulken. A scale counts as even while its parts differ by at most 1e-5 of the largest, since a turned, evenly scaled object decomposes from its world matrix uneven by rounding (up to 5.3e-7 of its scale over 4000 random turns). `rigid_body`, `rigid_body_constraint`, `rigidbody_world`, `gravity` and `use_gravity` have left the lists it passes unread. - **The stage** builds each hull with `hullOf`. It writes each body's shape, mass, material, damping and start in hulken's axes (Blender's local Z becomes local +Y) and the scene's gravity into the world file (version 5), and each body's meshes in its own space into the render file (version 5). A body's triangles go into neither the collision world nor the static meshes. - **The server** builds a `PhysicsWorld` over its world's collision from the world file's bodies and gravity, and each tick steps its players, then the bodies, then posts the snapshot. **The snapshot** carries each body's index, position and unit quaternion (`src/net/messages.sx` documents the bytes). **The client** gives each body between its two kept snapshots, the position lerped and the orientation nlerped along the shorter arc. - **The renderer** draws each body's meshes in the opaque pass, its vertex stage placing them by a rigid transform set per draw with `setVertexConstants` and turning their normals by its rotation. It shares the scene's fragment stage. The app loads the body meshes and draws them at the client's poses. - **README.** The Scenes section says what a rigid body is in a scene. ## Tests - `tests/bake_extract.py`: a case for each shape as Blender sizes it, a Capsule shorter than it is wide sized as a sphere, a Sphere scaled 1000 and turned, whose world scale decomposes as (1000, 1000, 999.99994), uneven by 6e-5 though only by 6e-8 of its scale, taken, a Capsule scaled evenly, a scaled and turned Box, a Convex Hull wedge scaled (1, 1, 2) whose normals are those of its scaled faces, what is read, the scene's gravity changed and off, a body whose mesh is linked from `crate.blend`, a Passive Mesh ground, the Passive Box, Passive Convex Hull, friction and restitution refusals, a baked cache, a Sphere scaled (1, 1, 1.00002), uneven by twice the tolerance, a Sphere of delta scale (1, 1, 2) and a Capsule a Limit Scale constraint holds at 2 along z refused, a body marked as water and the Passive Mesh ground marked as a collision proxy refused, a body scaled -1 along x and one of delta scale (-1, 1, 1) refused, the latter naming its world matrix's determinant, and each other refusal. - `tests/bake_stage.sx`: bodies through the stage and both files, a Capsule turned in Blender lying along hulken's +Z by its local +Y, a hull built, the bodies' triangles kept out of the collision world and the static meshes, the gravity, both files read back, and a flat hull refused. - `tests/net_messages.sx`: a snapshot with bodies round-trips, and one cut short in its last body is refused. - `tests/sim_bodies.sx`: a crate dropped 3 m on flat ground falls, lands and sleeps in the server's snapshots; two servers fed the same commands send the same bytes for 600 ticks; the client's pose is lerped and nlerped along the shorter arc. - `tests/body_draws.sx`: bodies turned about each axis, and a turned card drawing a cut-out checker, draw the same pixels as the same boxes and card placed statically, every pixel opaque, the card holding both texels and holes. - `tests/first_map_bodies.sx`: the baked first map's props and shapes, each asleep within 10 s above the terrain under it, the tower and the pyramid within 5 cm of their starts, and each crate settled on the terrain drawing within 1 cm of it. ## Mutants Each rule's code was changed in a `git archive` arm of the head, one change at a time, and the tests that cover it were rerun. 85 mutants: - **Extract** (48): each refusal dropped (an instanced, parented, modified or mirrored body, any Active shape, a baked cache, a body or a Passive body outside the world, a `hulken_collide` false material, a water or proxy marker on a rigid body, an unevenly scaled Sphere or Capsule, the Sphere or the Capsule alone left out of it), only the water or only the proxy marker refused, the markers refused on Active bodies alone, the mirror read from the object's own scale, the evenness read from the object's own scale, from its scale times its delta scale, or on x and y alone, compared exactly, absolutely rather than relative to the scale, or within 1e-8, 1e-4, 1e-3, 1e-2, 1e-1 or half the scale, every scaled Sphere or Capsule refused, each size rule changed (box by whole dimensions, sphere by the least, capsule length without its caps, a capsule's half segment without its floor at 0), the scale left off the corners and the points, normals left unnormalised, normals only normalised and not divided by the scale, the rotation's components reordered, `rigid_body`, `rigid_body_constraint` or `rigidbody_world` passed unread again, gravity or mass compared rather than read, `use_gravity` ignored, the preview settings compared, and the reference body's shape, type or world wrong. All killed by `tests/bake_extract.py`. The markers' ran at 689103d; the mirror's and the evenness's, and 689103d's extract, which fails the new cases on their names, ran against the tests at 6f5b54a. - **Stage and files** (15): box extents, capsule radius and half, hull points, rotation, position and gravity left in Blender's axes; friction and restitution swapped; a body's triangles into the static meshes; gravity not written; each hull check and the shape-kind check dropped; damping swapped; bodies not written to the render file. All killed by `tests/bake_stage.sx`, `tests/first_map_bodies.sx` or `tests/first_map_views.sx`. - **Server, messages, client** (11): bodies not stepped, poses not refreshed, the world's gravity or friction not taken, the snapshot without bodies, bodies not encoded, rotation components read swapped, the latest pose only, the fraction from the wrong end, nlerp along the longer arc. All killed by `tests/sim_bodies.sx` or `tests/net_messages.sx`. One survives as equivalent: the bodies stepped before the players, which nothing distinguishes until step 3 lets players touch bodies. - **Renderer and loader** (9): normals not turned, normals or corners turned by the inverse rotation, bodies not drawn, the body transform without its move, body meshes not loaded, the cut-out body stage built without CUTOUT, every body mesh drawn in the passes that are not cut-out, the cut-out body stage over the static vertex stage. All killed by `tests/body_draws.sx` or `tests/first_map_views.sx`; the three cut-out ones by `tests/body_draws.sx` alone. - **Assets** (2): the crate re-imported with its origin at its volume's centroid draws 11.6 cm above the terrain, and a level disc that stops short of the props' footprints leaves cannon balls rolling past 10 s; both fail `tests/first_map_bodies.sx`. ## Gates - `fast` at 1c96f32: passed (gate run 888). - `full` at 3d2ac98: passed, 34 of 34 (gate run 889). - `full` at 22f4af3: passed, 34 of 34 (gate run 893). - `full` at 689103d: passed, 34 of 34 (gate run 895). - `full` at 6f5b54a: passed, 34 of 34 (gate run 898). ## For Agra's check In the app, the props stand in the clearing in front of the first spawn marker; the crate, barrel and ball in the air fall and land; nothing should jitter at rest. A run of the built app with a print after its frame loop showed the server stepping 20 bodies, all asleep after about 5 s, and 20 drawn.
agra added 10 commits 2026-10-30 06:21:31 +02:00
agra merged commit 80e2ed5ded into master 2026-10-30 07:34:39 +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!18
No description provided.