Skip to content

physics: port Visual Pinball's recent physics fixes - #599

Merged
freezy merged 12 commits into
masterfrom
fix/physics-alignment
Nov 7, 2026
Merged

freezy merged 12 commits into
masterfrom
fix/physics-alignment

Conversation

@freezy

@freezy freezy commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

Ports physics fixes Visual Pinball made in the last weeks (mostly its FIX_PHYSICS path, which is enabled in VPX) that VPE didn't have yet. One commit per area:

  • Wall touch band (vpinball#4088): slow touches inside a line segment's touch band got a remapped hit time that never fits a step, so they were dropped. They are contacts now.
  • Contact friction (vpinball#4086, #4088, 8c4cf05d): mass-independent normal force and surface acceleration, friction cone bounded by the contact's own normal impulse instead of gravity alone (nothing on walls and the glass), the 0.025 static gate restricted to supports (it always picked static friction on walls), and the full slip-removing impulse instead of a step-length fraction of it.
  • Flipper contact (vpinball#4053, 8c4cf05d): static friction inertia sign, and the gap acceleration uses the ball's center, so a spinning ball keeps its support.
  • Flipper inertia (a75d4da2d): moment of inertia of the real flipper shape, coil torque rescaled so the free swing stays the one tables are tuned on.
  • Ball/ball collision normal (vpinball#4051): taken between both centers at hit time.
  • Ball/ball resting contacts (vpinball#4087, #4088, 8c4cf05d): ball pairs were never contacts, so resting balls kept colliding and popping apart. Contacts with relative approach and friction, inelastic slow impacts, ball contacts resolved first and static contacts cancelling what earlier contacts did to the ball (stack and row jitter, Newton's cradle).
  • Ramp entrance (vpinball#4079): the start edge had its joint twice.
  • Reversed winding (vpinball#4085 and follow-ups): walls drawn clockwise collide from both sides like in VPX, and get their top back (the ear clipping found no triangles, so the top collider was missing and collider generation threw). Curved triggers no longer swap hit and unhit.
  • Playfield (8c4cf05d): a slow ball embedded deeper than the touch layer collides instead of sinking through.

Where VPE differs from VPX on purpose:

  • Static contact friction works from a snapshot of the ball's motion taken before the contact pass (so it doesn't take another contact's normal response as slip). With VPX's full slip-removing impulse, each later contact of a ball now sees the friction of the earlier ones, or two contacts sharing a slip would both remove it. Spring hinges are left out, they recompute their normal. Ball/ball contacts use the live motion of both balls, as in VPX.
  • The bnd > -PHYS_TOUCH limit on triangle contacts from 8c4cf05d is not ported: as a collision, a cover embedded in the ball from above would get the displacement correction pushing the ball into the floor again. VPE's contact recovery, which never pushes along gravity, handles deep triangle contacts.
  • VPX's small overlap drain is applied to ball/ball contacts only. VPE's static contacts keep their own recovery.
  • A ball/ball contact cancels at most the pair's own approach plus a step of load: VPX braked the trailing one of two balls moving together.
  • A static contact may take back an earlier contact's lift that is larger than the step's load (short steps), and passes the take-back on so two supports don't take it twice.

Tests: new PhysicsAlignmentTests (38 cases, several ported from VPX's test-physics-defects.cpp). The EditMode suite has no new failures compared to master; the 43 failures on master (mesh Z offsets, VPX write float noise, play-mode fixtures, Kiki ABI) are unchanged. Not play-tested on a table yet.

A slow ball inside a line segment's touch band got the legacy hit time
bnd * 10 + 0.5, which never fits a 0.1 step, so the touch was dropped
and the wall only reacted once the ball had sunk into it. It is a
contact at hit time 0 now, as in Visual Pinball's FIX_PHYSICS path.
- The normal force of a contact is the velocity change that cancels the
  approach and the load for the step, whatever the ball's mass, and the
  ball's surface acceleration no longer divides gravity by its mass.
  Heavier balls were thrown off their support.
- The friction cone is bounded by the normal impulse the contact applied
  instead of the gravity component pressing the ball on the surface,
  which is next to nothing on walls and nothing on the glass.
- The 0.025 static friction gate only applies to supports. After the
  normal solve it tested the orientation of the normal and picked the
  static branch on every wall, even with real slip.
- Sliding friction requests the full slip-removing impulse instead of a
  step-length fraction of it.

Contact friction works from a snapshot of the ball's motion, so with the
full impulse each later contact of the same ball now sees the friction
of the earlier ones, or two contacts sharing a slip would each remove
all of it. Spring hinges recompute their normal and are left out.
Ported from Visual Pinball:

- The static friction case subtracted the flipper's inertia term from
  the ball's instead of adding it, as the dynamic case does.
- The contact's gap acceleration uses the ball's center acceleration.
  The centripetal term of the material contact point is positive along
  the normal for any spin, so a spinning ball read as accelerating away
  and lost its support. The friction budget gets the same fix.
Ported from Visual Pinball: the flipper's moment of inertia is the one of
the convex hull of its base and end circles instead of a rod's. The coil
torque is scaled by the ratio of the two, so the free swing stays the one
tables were tuned on, while ball impacts now see the shape's inertia.
Degenerate shapes, one circle inside the other, keep the rod.
Ported from Visual Pinball: the normal is the direction between the two
ball centers at the time of the hit. It added the relative motion to the
moving ball with the wrong sign and left the hit ball where it was, so an
oblique hit pushed the balls apart in a wrong direction.
Ported from Visual Pinball (ball/ball contacts and the stack fixes of its
FIX_PHYSICS path). Ball pairs were never contacts, so balls resting
against each other kept colliding, popping apart and bouncing back.

- A slow pair inside the touch band is a contact, also a bit deeper
  where a resting stack hovers, and only a deep overlap is a collision.
- The contact cancels the approach relative to the other ball, up to a
  contact-scale velocity, so an impact still passes on through a row of
  balls. A small capped nudge drains the overlap.
- Contact friction acts on the slip relative to the other ball, which
  takes its share of the impulse, with a steel on steel coefficient.
- Slow impacts are inelastic and skip the displacement correction.
- Ball/ball contacts are resolved first, and static contacts cancel
  what earlier contacts did to the ball along their normal, so the
  friction of a ball pressed against another one no longer lifts it.
Ported from Visual Pinball: the starting edge of the ramp floor got a
joint for the first triangle and the same one again when the first
triangle was checked against its (missing) predecessor. A degenerate
floor triangle now also resets the predecessor, so the next triangle
gets its own edge joint.
Ported from Visual Pinball. Dragging a point across the others reverses
the winding of an outline, which code generating from it assumes to be
counterclockwise.

- A wall drawn clockwise had no top: the ear-clipping triangulation of
  its top mesh, which also gives the top collider, found no triangles,
  and generating the colliders threw. The top is triangulated in the
  canonical order now.
- The side colliders are one sided. Tables relied on a clockwise wall
  facing inward, so like Visual Pinball, such a wall collides from both
  sides.
- A curved trigger drawn clockwise swapped its hit and unhit events.
Ported from Visual Pinball: a slow ball embedded deeper than the touch
layer in the playfield plane was neither a contact nor a collision, so
it could sink through. It collides now, which pushes it out, and only a
touch inside the touch layer stays a contact.
@greptile-apps

greptile-apps Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[High impact] The PR appears safe to merge; the previous floor-support issue is fixed, and no new blocking issue was found.

Summary

Ports recent Visual Pinball physics fixes and adds regression tests.

  • Slow ball pairs settle into contacts and pass momentum along.
  • Surface contacts use their own load to set friction.
  • Slow wall touches and embedded playfield balls enter the right response path.
  • Flippers keep spinning balls supported and use their real shape for inertia.
  • Reversed outlines keep their wall tops and trigger sides.
  • Ramp entrance joints are added once when collider edges are built.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Find hits and contacts] --> B[Put ball contacts first]
  B --> C[Resolve collisions]
  C --> D[Save support from static impacts]
  D --> E[Prepare contacts and mark backing support]
  E --> F[Resolve ball contacts once per pair]
  F --> G[Pass both balls' changes to later contacts]
  G --> H[Resolve static contacts]
Loading

Reviews (4) · Last reviewed commit: "physics: preserve backing support for ba..." · Reviewed by Greptile

- A ball/ball contact cancels at most the pair's own approach plus the
  step of load. Two touching balls moving together along the normal
  don't close in on each other, but the trailing one was braked by up to
  the contact velocity against the leading one (as in Visual Pinball).
  Resting stacks keep their support from the load.
- A static contact takes back the lift of the contacts resolved before
  it even when that lift is larger than the step's load, which happens
  on short steps since friction removes the slip at once. It still
  never pulls a ball nobody lifted towards the surface, and passes the
  take-back on, so two supports don't take the same lift back twice.
Resolve free ball contacts once per pair with mass-weighted normal and friction impulses. Preserve the stack response when a solid or frozen ball supports the pair, and cover both cases through the physics cycle.
Retain support from a resolved collider impact even after the collision event is cleared. Follow the load direction through backed ball contacts so sloped queues and vertical stacks settle. Pass both sides of a free contact on to static supports.
@freezy
freezy merged commit f57207f into master Nov 7, 2026
15 checks passed
@freezy
freezy deleted the fix/physics-alignment branch November 7, 2026 13:12
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.

1 participant