Skip to content

Add friction and rolling resistance to sustained ball contacts#571

Draft
freezy wants to merge 6 commits into
masterfrom
physics/friction-rolling-resistance
Draft

Add friction and rolling resistance to sustained ball contacts#571
freezy wants to merge 6 commits into
masterfrom
physics/friction-rolling-resistance

Conversation

@freezy

@freezy freezy commented Jul 16, 2026

Copy link
Copy Markdown
Owner

This PR refines sustained ball contact friction into a consistent impulse-based solve and adds opt-in rolling resistance for table and ball materials. Fixes #363.

Changes

  • Normalize static and kinetic contact friction around per-tick support impulses.
  • Carry validated sustained-contact data into one rolling-resistance pass per ball.
  • Apply a coupled linear and angular impulse that preserves pure rolling while dissipating speed without reversing direction.
  • Select one deterministic contact at seams and corners to avoid multiplying the supported weight.
  • Add rolling-resistance authoring, packaging, validation, and zero-value defaults across collider and ball materials.
  • Add analytic, regression, serialization, aggregation, transformed-contact, and behavior coverage.
  • Document the model, calibration workflow, limitations, and migration guidance.

Testing

  • Unity 6000.5 EditMode: 45/45 focused physics tests passed across BallFrictionTests, BallContactRegressionTests, PhysicsMaterialTests, and BallRollingResistanceTests.
  • DocFX 2.78.5 documentation build completed successfully.

Notes

Rolling resistance defaults to zero, so existing tables retain their current behavior until explicitly tuned. Impact collisions, flipper contacts, and ball-to-ball contacts remain outside this first implementation. Physical material presets remain unchanged pending table-specific measurement and calibration.

freezy added 6 commits July 16, 2026 02:54
Exercise sustained contact through the public handler using production gravity
and contact-step ordering.

Cover analytic rolling transitions, incline acceleration, invariance, slip
classification, and energy behavior. The legacy solver fails seven of nine
cases, establishing the Phase 1 regression baseline.
Separate normal correction from steady support and express Coulomb changes
as impulses with gravity consistently treated as acceleration.

Select static or kinetic friction from tangential slip, validate ball mass
and radius at creation boundaries, and add flipper, rubber, separation, and
invalid-state regressions.
Add dimensionless Crr values to runtime materials, assets, and supported
collider overrides while keeping unsupported specialized contacts inert.

Expose authoring controls, preserve values through .vpe package serializers,
and verify old JSON defaults plus nonzero round trips. Existing and imported
tables retain a zero default.
Apply a coupled linear and angular impulse once per ball after sustained
Coulomb contacts have been solved. Use steady support load and relative surface
motion so no-slip rolling decelerates analytically without affecting impacts.

Select a deterministic eligible representative across seam, corner, and mixed
material contacts to avoid multiplying gravity support. Keep flipper and
ball-to-ball rolling resistance disabled until both bodies can be coupled.

Cover analytic level rolling, no-slip preservation, contact aggregation, and
mixed-contact eligibility with deterministic EditMode tests.
Extend deterministic sustained-contact coverage across incline losses,
near-stop clamping, monotonic energy, zero-coefficient equivalence, and
relative kinematic surface motion.

Prove rolling resistance remains independent of impact response, mass, radius,
and substep partitioning. Isolate incline support loading with a differential
zero-coefficient comparison that pins the cosine term.
Explain how Coulomb friction and rolling resistance differ and document where
the new authoring value is supported.

Provide fixture setup, inverse formulas, data-sheet fields, fit and validation
criteria, and rollout rules. Keep all defaults at zero until measured physical
evidence and an HDRP table smoke support a nonzero preset.

Record runtime limitations and the sustained-contact correction in the
changelog.
@greptile-apps

greptile-apps Bot commented Jul 16, 2026

Copy link
Copy Markdown

Greptile Summary

This PR refactors sustained ball-contact friction into an impulse-based solve and adds an opt-in rolling-resistance coefficient (Crr) to table and ball materials. The support impulse returned from contact resolution is now used to bound the Coulomb friction limit, a spurious /ball.Mass division in SurfaceAcceleration is removed, and a new single-contact-per-ball rolling-resistance pass is appended after the contact loop.

  • Contact physics refactor (BallCollider.cs, ContactPhysics.cs): HandleStaticContact now returns the steady support impulse; friction is clamped against that impulse instead of mass * g_n; static friction converts the surface acceleration to an impulse with a * dTime factor that was previously missing.
  • Rolling resistance system (ContactBufferElement.cs, BallCollider.cs, ContactPhysics.cs, PhysicsCycle.cs): A new RollingContactData struct carries per-contact information; SelectRollingContact picks one representative contact per ball; ApplyRollingResistance applies a coupled linear/angular impulse that preserves the no-slip condition.
  • RollingResistance authoring (PhysicsMaterialAsset.cs, all *ColliderComponent.cs, ICollidableComponent.cs, ColliderComponent.cs, packables): The new property is wired through the full authoring, packaging, and serialization stack with zero-value defaults so existing tables are unaffected.

Confidence Score: 4/5

Safe to merge; rolling resistance defaults to zero so existing tables see no behavioral change, and the friction/support-impulse refactor is validated by 45 focused unit tests.

The physics derivations for the coupled linear/angular rolling-resistance impulse are sound and match the solid-sphere effective-mass formula. The SurfaceAcceleration division-by-mass fix is correct and well-tested. The one logic concern is that the absolute PhysicsConstants.Precision (0.01) tolerance used to gate rolling resistance on angularRollingSpeed vs centerSpeed can disagree with the tighter Precision-squared slip-speed gate already applied, meaning rolling resistance may silently skip for a legitimately rolling ball at low speeds.

ContactPhysics.cs (ball-to-ball branch dead code) and BallCollider.cs (TryGetRollingState absolute tolerance for angularRollingSpeed).

Important Files Changed

Filename Overview
VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/ContactPhysics.cs Core contact solve refactored to return support impulse, populate RollingContactData, and add ApplyRollingResistance/SelectRollingContact helpers; logic is sound but ball-to-ball branch contains a no-op RollingResistance zeroing
VisualPinball.Unity/VisualPinball.Unity/VPT/Ball/BallCollider.cs HandleStaticContact decomposed into SolveNormalContact + ApplyCoulombContactImpulse; new TryGetRollingState and ApplyRollingResistance implement the coupled impulse model; physics derivations look correct
VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/ContactBufferElement.cs New RollingContactData struct added with IsValid, IsPreferredOver, and RollingImpulseLimit; tie-breaking uses exact float comparison which is deterministic for same-source values
VisualPinball.Unity/VisualPinball.Unity/VPT/Ball/BallState.cs SurfaceAcceleration bug fix: removed spurious /ball.Mass that treated an acceleration vector as a force; change is correct and well-documented in comments
VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/PhysicsMaterialData.cs RollingResistance field added with SanitizeRollingResistance helper; Equals and GetHashCode updated correctly
VisualPinball.Unity/VisualPinball.Unity/Game/PhysicsCycle.cs Single line addition to call ApplyRollingResistance after the contact loop; correctly placed before the contact buffer is cleared
VisualPinball.Unity/VisualPinball.Unity/VPT/ColliderComponent.cs PhysicsRollingResistance abstract property added to base class and correctly wired into GetPhysicsMaterialData for both material-reference and override paths
VisualPinball.Unity/VisualPinball.Unity/Packaging/CommonPackables.cs RollingResistance added to PhysicalMaterialPackable with symmetric pack/unpack; old JSON without the field defaults to zero (confirmed by PhysicsMaterialTests)

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant PC as PhysicsCycle
    participant CP as ContactPhysics
    participant BC as BallCollider
    participant BS as BallState

    PC->>CP: Update(contact, ball, state, hitTime)
    CP->>BC: HandleStaticContact(ball, collEvent, material, dTime, gravity, colliderVelocity)
    BC->>BC: SolveNormalContact → correctionImpulse + supportImpulse
    BC->>BS: "ball.Velocity += correction"
    BC->>BC: ApplyCoulombContactImpulse(supportImpulse, friction)
    BC->>BS: ball.ApplySurfaceImpulse(frictionImpulse)
    BC-->>CP: return supportImpulse
    CP->>CP: Populate RollingContactData (IsContact, Crr, supportImpulse, normal, colliderVel)
    Note over CP: repeat for each contact / ball

    PC->>CP: ApplyRollingResistance(contacts, state)
    CP->>CP: SelectRollingContact(contacts[first..end], ball)
    loop per ball's contacts
        CP->>BC: IsRollingContact(ball, candidate) — pure-rolling guard
    end
    CP->>BC: ApplyRollingResistance(ball, selectedContact)
    BC->>BC: TryGetRollingState → tangentDir, rollingAxis, speeds, effectiveMass
    BC->>BC: "deltaSpeed = min(Crr·Jn / effectiveMass, centerSpeed, angularRollingSpeed)"
    BC->>BS: "ball.Velocity -= deltaSpeed · tangentDirection"
    BC->>BS: "ball.AngularMomentum -= (I/R)·deltaSpeed · rollingAxis"
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 PC as PhysicsCycle
    participant CP as ContactPhysics
    participant BC as BallCollider
    participant BS as BallState

    PC->>CP: Update(contact, ball, state, hitTime)
    CP->>BC: HandleStaticContact(ball, collEvent, material, dTime, gravity, colliderVelocity)
    BC->>BC: SolveNormalContact → correctionImpulse + supportImpulse
    BC->>BS: "ball.Velocity += correction"
    BC->>BC: ApplyCoulombContactImpulse(supportImpulse, friction)
    BC->>BS: ball.ApplySurfaceImpulse(frictionImpulse)
    BC-->>CP: return supportImpulse
    CP->>CP: Populate RollingContactData (IsContact, Crr, supportImpulse, normal, colliderVel)
    Note over CP: repeat for each contact / ball

    PC->>CP: ApplyRollingResistance(contacts, state)
    CP->>CP: SelectRollingContact(contacts[first..end], ball)
    loop per ball's contacts
        CP->>BC: IsRollingContact(ball, candidate) — pure-rolling guard
    end
    CP->>BC: ApplyRollingResistance(ball, selectedContact)
    BC->>BC: TryGetRollingState → tangentDir, rollingAxis, speeds, effectiveMass
    BC->>BC: "deltaSpeed = min(Crr·Jn / effectiveMass, centerSpeed, angularRollingSpeed)"
    BC->>BS: "ball.Velocity -= deltaSpeed · tangentDirection"
    BC->>BS: "ball.AngularMomentum -= (I/R)·deltaSpeed · rollingAxis"
Loading

Reviews (1): Last reviewed commit: "doc: document rolling resistance calibra..." | Re-trigger Greptile

Comment thread VisualPinball.Unity/VisualPinball.Unity/VPT/Ball/BallCollider.cs
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.

Physics: Properly handle friction

1 participant