Skip to content

General physics fixes#562

Merged
freezy merged 11 commits into
masterfrom
fix/physics
Jul 10, 2026
Merged

General physics fixes#562
freezy merged 11 commits into
masterfrom
fix/physics

Conversation

@freezy

@freezy freezy commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Fixes several transform and collision bugs uncovered while investigating the MG plunger ramp, where increasing plunger momentum could make the ball rebound earlier instead of traveling farther up the ramp.

The original issue was caused by an incorrectly transformed Line3D hit normal. A broader physics audit found related transform, event, and equality problems, which are also addressed here with regression coverage.

Changes

  • Transform Line3D hit normals back into world space correctly.
  • Keep transformed point-collider AABBs aligned with their geometry.
  • Transform LineZ endpoints using the complete matrix.
  • Extract scale from matrix basis columns so rotation does not mix scale axes.
  • Reject rotated, non-uniform XY scale for circle colliders.
  • Scale ball radius when moving through uniformly scaled collider space.
  • Preserve the hidden Z component of hit velocity during coordinate transforms.
  • Fire Line3D hit events for approaching balls above the configured threshold.
  • Delegate boxed Aabb and ColliderHeader equality to their typed overloads
    instead of recursing through Equals(object).
  • Expose runtime internals to the Unity test assembly for collider-level tests.

Ball radius scaling is deliberately limited to uniform transforms. A non-uniformly transformed sphere becomes an ellipsoid and cannot be represented faithfully by the engine's scalar radius.

freezy added 9 commits July 10, 2026 08:46
The hit normal was transformed back to world coordinates using the
matrix directly instead of its transpose. Since the forward transform
into local space uses the transpose, the inverse rotation must use the
non-transposed matrix — use math.transpose() to correct this.

Add a test verifying the returned hit normal points into world space.
Add PhysicsRegressionTests covering known transform and collision
bugs: point/lineZ/circle collider transforms, matrix scale
extraction, ball radius scaling, collision event round-trips,
Line3D hit event firing, and boxed Equals delegation. Includes an
IL-walking helper to assert Equals(object) forwards to the typed
overload.
PointCollider transformed its position and then rebuilt its AABB by
applying the same matrix to that already-transformed point. Translations
and other transforms therefore affected broad-phase bounds twice, leaving
the collider geometry and bounds in different places.

Build the zero-size bounds directly from the final transformed point. Also
expose runtime internals to the Unity test assembly so the collider-level
regressions compile and can verify the broad-phase position alongside the
narrow-phase point.
LineZCollider reconstructed transforms from a translation plus a separately
extracted Z scale. That skipped legal rotation around Z, ignored XY scale,
and expanded the height around a translated midpoint instead of transforming
the original endpoints.

Transform the low and high endpoints with the complete matrix, take their
shared XY position, and order the resulting Z values. The collider and its
bounds now follow translation, Z rotation, axis scale, and negative Z scale
exactly.
GetScale measured matrix rows even though Unity.Mathematics stores each
transformed basis axis in a column. Once a non-uniform scale was rotated,
row lengths blended the axes and could report a false uniform scale.

Measure the XYZ length of each basis column instead. Scale extraction is
now rotation-invariant, and collider transformability checks correctly
reject rotated non-uniform XY scale instead of baking an ellipse as a
circle.
The non-transformable-collider fallback moved a ball into local space but
left its radius in world units. Uniformly scaled items therefore tested a
correctly transformed center against a sphere of the wrong size, shifting
collision times and contact depth.

Apply the matrix scale to the radius when all three axes are uniform, where
a sphere remains exactly representable by one scalar and the inverse
transform restores the original value. Non-uniform transforms remain
unchanged because representing those faithfully requires ellipsoid
collision support.
CollisionEventData exposes the legacy XY hit velocity used by flippers and
plungers, but a tilted-space transform can rotate part of that vector into
Z. The old transform discarded that component immediately, so applying the
inverse matrix returned a smaller XY velocity.

Keep the transformed Z component privately alongside the public XY value
and include it in subsequent transforms. Existing collider code retains its
two-dimensional API while world-to-local round trips preserve the complete
velocity needed by tilted plungers.
The line-3D event gate compared normal dot velocity directly with a positive
threshold. An approaching ball produces a negative dot product, so ordinary
impacts resolved physically but never crossed the event threshold; receding
motion had the sign the event code expected.

Convert the pre-collision dot product to a positive impact speed before
comparing it with the threshold. This matches the convention used by the
other static colliders and restores hit events for primitive, ramp, rubber,
and wire-guide line segments.
Aabb.Equals(object) and ColliderHeader.Equals(object) checked the boxed type
but then passed the original object back to Equals. Overload resolution
selected the same object overload, causing unbounded recursion and a
process-ending stack overflow.

Capture the typed value in each pattern match and call the corresponding
IEquatable overload explicitly. The boxed paths now share the established
field comparisons, and the regression suite description no longer labels
the completed cases as intentionally red.
@freezy freezy self-assigned this Jul 10, 2026
@greptile-apps

greptile-apps Bot commented Jul 10, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a cluster of physics correctness bugs discovered while investigating anomalous plunger-ramp behavior: a ball gaining more momentum could counterintuitively rebound earlier than a slower one. The root cause was an incorrectly applied normal transform in Line3DCollider, and a broader audit surfaced six additional coordinate-system, equality, and event-firing defects.

  • Hit normal world-space transform: Line3DCollider.HitTest now applies transpose(_matrix) (= the inverse rotation) rather than _matrix itself, correctly mapping the cylinder-local normal back to world space.
  • GetScale column-vs-row fix: The helper was reading rows of the upper-left 3×3 instead of columns, giving wrong scale values for any matrix that combines rotation with non-uniform scale; ball radius scaling, circle-collider rejection, and HasEqualScale's relative tolerance all depend on this being correct.
  • Other fixes: PointCollider AABB is now built from the already-updated P; LineZCollider applies the full matrix to both endpoints; CollisionEventData preserves the hidden Z component of hit velocity across round-trip transforms; Line3D.Collide negates the velocity-normal dot so the threshold fires for approaching (not receding) balls; Aabb/ColliderHeader.Equals(object) no longer recurse infinitely.
  • Test coverage: A new PhysicsRegressionTests suite and Line3DColliderTests cover every fix, including an IL-introspection guard against re-introducing the Equals recursion.

Confidence Score: 5/5

All changes are targeted bug fixes with dedicated regression tests; no new APIs or data-model changes that could break callers.

Every fix is mathematically sound (transpose = inverse for orthogonal rotation, column magnitudes for TRS scale, relative tolerance for large-scale uniformity checks), the Aabb/ColliderHeader Equals recursion crash is definitively resolved, and the new test suite guards each fix independently. No behavioral regressions were identified.

No files require special attention; the most impactful changes in Line3DCollider.cs and MathExtensions.cs are well-covered by the accompanying tests.

Important Files Changed

Filename Overview
VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/Line3DCollider.cs Hit normal correctly transformed back to world space using transpose (= inverse) of rotation matrix; Collide event direction bug fixed by negating the dot product to yield a positive impact speed for approaching balls.
VisualPinball.Unity/VisualPinball.Unity/Extensions/MathExtensions.cs GetScale now reads column magnitudes (c0.xyz, c1.xyz, c2.xyz) instead of row magnitudes — the previous code was extracting rows of the upper-left 3×3 and produced wrong scale values for any matrix combining rotation with non-uniform scale.
VisualPinball.Unity/VisualPinball.Unity/VPT/Ball/BallState.cs Transform() now scales Radius by the uniform scale factor extracted from the matrix; non-uniform transforms are silently skipped (documented in PR); relies on the corrected GetScale().
VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/CollisionEventData.cs Adds private _hitVelocityZ to preserve the Z component of hit velocity across round-trip 3D transforms; ClearCollider() and the new ClearHitVelocity() both zero it; KickerApi updated accordingly.
VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/Collider.cs Adds HasEqualScale() with relative tolerance comparison, replacing the previous absolute comparison that would incorrectly flag large-scale uniform matrices as non-uniform.
VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/LineZCollider.cs Transform() now applies the full matrix to both endpoints via MultiplyPoint instead of separately extracting translation and scale; ZLow/ZHigh correctly ordered with min/max.
VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/PointCollider.cs Transform() now builds the AABB directly from the already-updated P instead of calling TransformAabb(matrix), which was recomputing from the un-transformed source and produced stale bounds.
VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/Aabb.cs Fixes infinite recursion in Equals(object): pattern-matches to Aabb and delegates to the typed Equals(Aabb) overload instead of calling itself.
VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/ColliderHeader.cs Same infinite-recursion fix as Aabb.cs: Equals(object) now correctly delegates to Equals(ColliderHeader) via the is-pattern cast.
VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/CircleCollider.cs IsTransformable() now uses the relative HasEqualScale() for the uniformity check, preventing false rejects for large-scale circles.
VisualPinball.Unity/VisualPinball.Unity.Test/Physics/PhysicsRegressionTests.cs Comprehensive regression suite covering: PointCollider AABB alignment, LineZCollider full-matrix transform, GetScale correctness, CircleCollider rejection of rotated non-uniform scale, ball radius round-trip, CollisionEvent Z-velocity preservation, Line3D hit event threshold, and Equals delegation for Aabb and ColliderHeader.
VisualPinball.Unity/VisualPinball.Unity.Test/Physics/Line3DColliderTests.cs New test verifies that the hit normal for a diagonal approach to a horizontal line is correctly rotated back from cylinder space to world space (expected (0, √0.5, √0.5)).
VisualPinball.Unity/VisualPinball.Unity/Common/AssemblyInfo.cs Adds InternalsVisibleTo("VisualPinball.Unity.Test") so the test assembly can access internal collider types for the new regression tests.
VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs Replaces manual HitVelocity = float2.zero with the new ClearHitVelocity() call to also zero the hidden _hitVelocityZ field.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant BallMover
    participant Line3DCollider
    participant LineZCollider
    participant CollisionEvent

    BallMover->>Line3DCollider: HitTest(ball, dTime)
    Line3DCollider->>Line3DCollider: "hitTestBall.Position = _matrix * ball.Position"
    Line3DCollider->>Line3DCollider: "hitTestBall.Velocity = _matrix * ball.Velocity"
    Line3DCollider->>LineZCollider: HitTest(collEvent, hitTestBall)
    LineZCollider-->>Line3DCollider: hitTime, collEvent.HitNormal (local space)
    Note over Line3DCollider: FIX: n_world = transpose(_matrix) * n_local
    Line3DCollider-->>BallMover: hitTime, HitNormal (world space)

    BallMover->>Line3DCollider: Collide(ball, collEvent)
    Note over Line3DCollider: FIX: impactSpeed = -dot(HitNormal, Velocity)
    Line3DCollider->>Line3DCollider: BallCollider.Collide3DWall(...)
    alt "impactSpeed >= Threshold"
        Line3DCollider->>CollisionEvent: FireHitEvent(ball, hitEvents, Header)
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant BallMover
    participant Line3DCollider
    participant LineZCollider
    participant CollisionEvent

    BallMover->>Line3DCollider: HitTest(ball, dTime)
    Line3DCollider->>Line3DCollider: "hitTestBall.Position = _matrix * ball.Position"
    Line3DCollider->>Line3DCollider: "hitTestBall.Velocity = _matrix * ball.Velocity"
    Line3DCollider->>LineZCollider: HitTest(collEvent, hitTestBall)
    LineZCollider-->>Line3DCollider: hitTime, collEvent.HitNormal (local space)
    Note over Line3DCollider: FIX: n_world = transpose(_matrix) * n_local
    Line3DCollider-->>BallMover: hitTime, HitNormal (world space)

    BallMover->>Line3DCollider: Collide(ball, collEvent)
    Note over Line3DCollider: FIX: impactSpeed = -dot(HitNormal, Velocity)
    Line3DCollider->>Line3DCollider: BallCollider.Collide3DWall(...)
    alt "impactSpeed >= Threshold"
        Line3DCollider->>CollisionEvent: FireHitEvent(ball, hitEvents, Header)
    end
Loading

Reviews (3): Last reviewed commit: "physics(balls): compare transform scale ..." | Re-trigger Greptile

freezy added 2 commits July 10, 2026 10:40
Extract ClearHitVelocity() so clearing the collision event's hit
velocity also zeroes the transformed Z component, not just the
2D vector. ClearCollider() now clears it too, and KickerApi uses
the shared helper. Add a regression test covering transform after
clear.
Ball transforms decide whether a sphere's radius can follow a collider
matrix by comparing the three extracted axis scales. The previous absolute
tolerance grew stricter as the matrix scale increased, allowing a matrix
and its inverse to disagree and permanently shrink the persistent ball.

Compare scale values relative to their magnitude and share the predicate
with circle collider validation. Cover a rotated uniform scale of ten with
an inverse/forward regression that previously changed radius 25 to 2.5.
@freezy
freezy merged commit 680d43c into master Jul 10, 2026
15 checks passed
@freezy
freezy deleted the fix/physics branch July 10, 2026 09:10
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