Skip to content

feat(world): a blast inside a shape pushes it toward its center of mass - #418

Merged
siracozmen01 merged 1 commit into
mainfrom
feat/blast-push-to-center
Nov 5, 2026
Merged

siracozmen01 merged 1 commit into
mainfrom
feat/blast-push-to-center

Conversation

@siracozmen01

Copy link
Copy Markdown
Collaborator

Problem

When the blast of m3World_Explode sits inside a shape, its rounding included, PushShape in src/destruction.c pushed the body from the blast toward a point of the shape and applied the impulse there. That point was a sphere's centre, the middle of a capsule's segment, or the average of a hull's vertices (#417). It is not the body's centre of mass when a hull is lopsided or the body has more than one shape, so the push spun the body for a reason with no physical meaning. A blast exactly at that point pushed along the shape's local x axis, a direction nothing about the blast picks.

docs/user/destruction.md said the shape "is pushed out through its centroid", which matched neither case. maul2d pushes such a body toward its centre of mass, and the family should behave the same.

Change

A shape with the blast inside it, its rounding included, now pushes its body in the direction from the blast to the body's centre of mass. The impulse is linear, along a line through the centre of mass, so that push spins nothing. A blast inside it at the centre of mass itself, within 1e-5 m, pushes nothing; the body still wakes. The impulse's size is computed as before: impulsePerArea times the area the shape shows along the push.

What changes for which calls:

  • m3World_Explode, and a replay of a journaled explosion, can give a different velocity to a body one of whose shapes holds the blast. A shape the blast is outside of is pushed as before.
  • No other call changes. The golden scenes, the warm start scenes and the bench scenes keep every line of test/hashes.txt.

M3_SOLVER_REV stays at 21: an explosion is an API call that acts at once, so a snapshot written before this change still steps to the same bits.

docs/user/destruction.md now says: "A shape with the blast inside it, its rounding included, is pushed in the direction from the blast to its body's centre of mass, so that push spins nothing; a blast inside it at the centre of mass itself pushes nothing."

Verification

  • TestBlastInside in test/test_explosion.c has three cases:
    • a square pyramid hull, whose vertex average sits at y = 0.4 and its centre of mass at y = 0.5, with the blast at 0.3, 0.1, 0: it moves toward its centre of mass with no spin;
    • a blast at that hull's centre of mass leaves the world hash as it was;
    • a capsule with a ball above it on the same body, so the centre of mass is off the capsule, with the blast in the capsule's rounding past the segment's end: it moves toward the body's centre of mass with no spin.
  • With main's src/destruction.c, all three fail.
  • test_golden, test_warmhold and test_bench from a gcc-14 Release build give all 20 lines of test/hashes.txt unchanged, before the two helpers were split out of PushShape; the split moves no arithmetic, and the golden tests pass on this head.
  • tools/check.sh pr passes in the main checkout on this head: 78 and 76 tests, and fuzz with no finding.

Closes #417

@siracozmen01 siracozmen01 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review at 6f52206: ready. No findings. Read, not run.

  • Inside matches the sentence. inside = !(out.distance > 1e-6f && surface > 1e-6f): the blast is in the core (distance about 0), or within the rounding (surface = distance - radius about 0 or below). That is "inside it, its rounding included", with the 1e-6 m tolerance the old branch already used.
  • The push is the sentence's.
    • BlastToCenter takes body position + rotated localCenter - blast, the vector from the blast to the body's center of mass, and normalizes it.
    • m3ApplyLinearImpulseInternal applies it with no lever arm, so it spins nothing.
    • Its size still uses ShapeProjectedArea along that direction, brought into the shape's frame with m3InvRotateVec3(xf.q, ...), and FalloffScale is the old arithmetic moved out unchanged.
    • "At the center of mass itself" is a 1e-5 m ball (dot(d, d) <= 1e-10). The body says so, and that is a fair reading of "itself" for a float position.
    • The body is woken before the branch, so even the no-push case wakes it, as "every body the blast reaches wakes" says.
  • Multi-shape bodies: PushShape still runs once per shape and pushes that shape's body once.
    • A shape the blast is inside pushes toward the body's center of mass.
    • A shape it is outside of pushes from its nearest point exactly as before (the !inside branch is the old code).
    • So a blast inside one shape of a two-shape body gives one center-of-mass push plus one ordinary push. A blast at the center of mass inside one shape gives that shape nothing and the other its ordinary push.
  • The M3_SOLVER_REV reasoning holds for what the rev guards. Its comment says it is in the snapshot config hash, so that a snapshot does not silently diverge. A snapshot holds state, not a pending explosion, and the step itself is unchanged, so every snapshot steps to the same bits. A journal has its own version and no config hash, so a tape recorded before this change replays an explosion with the new push. The body states that, and the feat changelog entry is where a user sees it.

@siracozmen01
siracozmen01 merged commit 6445e89 into main Nov 5, 2026
28 checks passed
@siracozmen01
siracozmen01 deleted the feat/blast-push-to-center branch November 5, 2026 13:57
@rawframe-release rawframe-release Bot mentioned this pull request Nov 5, 2026
siracozmen01 pushed a commit that referenced this pull request Nov 8, 2026
🤖 I have created a release *beep* *boop*
---


## [1.1.0](v1.0.5...v1.1.0) (2026-11-05)

### Changed

- **1.1.0 refuses snapshots, tapes and containers from 1.0.x.** The
  snapshot keeps format 6, the journal tape format 7 and the replay
  container format 4, but they carry `M3_ABI_VERSION`, which now names
  1.1, so 1.1.0 refuses a snapshot, tape or container that 1.0.x wrote
  with `m3_errorVersion`. `m3CreateWorld` refuses a def from 1.0
  headers the same way: rebuild against the 1.1 headers. The shared
  library's `SOVERSION` is `1.1`.
- **A blast inside a shape pushes its body toward its centre of mass.**
  When the blast of `m3World_Explode` lay inside a shape, its rounding
  included, the body was pushed from the blast toward a point of that
  shape, with the impulse applied there. That point was a sphere's centre,
  the middle of a capsule's segment, or the average of a hull's
  vertices. A lopsided hull, or a body with several shapes, therefore
  spun for no physical reason. A blast exactly at that point pushed
  along the shape's own x axis. Now the push runs from the blast to the
  body's centre of mass, as maul2d's does, so it spins nothing, and a
  blast at the centre of mass itself pushes nothing; the body still
  wakes. `m3World_Explode`, and a replay of a journaled explosion, can
  give a different velocity to a body one of whose shapes holds the
  blast. A shape the blast is outside of is pushed as before.
  ([#418](#418))

### Fixed

- **A mesh create no longer takes seconds on a fan.** The edge flags a
  mesh bakes on create found each edge's neighbor by scanning every
  triangle around one of its vertices, so a fan of triangles around one
  vertex took time quadratic in its size: about 5.4 s for 65533
  triangles. The bake now takes time linear in the mesh, about 0.025 s
  for the same fan, and gives the same flags byte for byte, so no
  contact or result changes. The same holds for a mesh create that
  `m3World_ReplayJournal` replays.
  ([#413](#413))
- **Restore refuses a voxel chunk whose cells disagree.** An occupied
  cell always has a fill of 1 to 255, and an empty cell has fill 0 and
  payload 0. `m3World_Restore` refused neither an occupied cell with
  fill 0 nor an empty cell with a fill or a payload, and
  `m3World_ReplayJournal` took the same states from a chunk create
  record. Both now refuse such a chunk with `m3_errorFormat`, and
  restore leaves the world as it was. No chunk the library writes is
  refused.
  ([#409](#409))
- **Restore refuses a wheel direction that is not unit length.** A
  vehicle create takes a wheel direction whose squared length lies
  between 0.81 and 1.21 and stores it normalized, but restore took any
  stored direction in that band, so a restored wheel could push along
  a scaled direction. Restore now requires the squared length within
  1e-5 of 1, about 24 times what normalizing leaves, and refuses
  anything else with `m3_errorFormat`.
  ([#415](#415))
- **Restore refuses mesh edge flags the mesh would not bake.** A mesh's
  edge flags are derived from its geometry, and contact welding reads
  them, but restore took them as stored. Restore now bakes each mesh's
  flags again and refuses a snapshot whose flags differ, with
  `m3_errorFormat`, or `m3_errorCapacity` when it cannot allocate the
  bake's scratch. No snapshot the library writes is refused. On the
  largest mesh the limits allow, 65522 triangles, a restore took about
  26 to 29 ms with the check against 22 ms without it.
  ([#416](#416))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

a blast inside a shape pushes toward a shape point, not the center of mass

1 participant