Friction impulses on speculative contacts use the midpoint of the two contact points, which adds `separation/2` to the lever arm and breaks angular-momentum conservation at the contact point

Author: AliveverComCreated Sep 11, 2026Updated Sep 11, 2026

Version: Jolt Physics v5.3.0 (single-threaded job system, deterministic configuration)

Observed on v5.3.0 (the version pinned in our project); not yet re-run on v5.6.0.

Summary

For a speculative contact (the two shapes are still separated by δ > 0 when the constraint is built), WorldContactPoint::TemplatedCalculateFrictionAndNonPenetrationConstraintProperties computes a single application point p = 0.5 * (inWorldSpacePosition1 + inWorldSpacePosition2) and uses the resulting r1 / r2 for both the non-penetration constraint and the two friction axes. Translating the application point along the contact normal is harmless for the normal axis ((r + α·n) × n = r × n), but it changes the friction axes ((r + α·n) × t = r × t + α·(n × t)). For a sphere resting/bouncing on a plane this makes the effective friction lever arm R + δ/2 instead of R, so every sliding speculative impact injects a spurious angular impulse.

Reproduction

  • Sphere: radius R = 0.1 m, mass m = 1 kg, solid-sphere inertia I = (2/5)mR² = 0.004 kg·m².
  • Initial state: centre at z = 1.35 m, linear velocity (+2, 0, 0) m/s, zero angular velocity.
  • Static box floor, top face at z = 0, well clear of the edges.
  • Materials: combined friction μ = 0.5, combined restitution e = 0.8; gravity 9.81 m/s².
  • Stepping: Update(dt = 50 ms, collision_steps = 10)Δt_sub = 5 ms; mSpeculativeContactDistance = 0.02 m (default); mMinVelocityForRestitution = 0.140071 m/s.
  • Let the sphere bounce until it rolls without slipping, then measure the steady horizontal speed.

Expected

Both the normal impulse and the friction impulse act at the sphere's contact point, so the angular momentum about that point, L = m·v_x·R + I·ω_y, is conserved across every impact and every flight phase. With L₀ = 0.2 kg·m²/s the pure-rolling end state is v_x = L₀ / (mR + I/R) = 5/7 · v₀ = 1.428571 m/s (and ω = 14.285714 rad/s), independent of μ, of the solver iteration count, and of how many impacts the de-slipping takes.

Actual

v_x = 1.4503364 m/s, i.e. +1.52 %. The whole error appears at the first (large-slip) impact, where L jumps from 0.200000000 to 0.203026097 kg·m²/s (+1.513 %); all subsequent impacts conserve L to within 0.02 pp.

Analysis

The narrow phase uses the sub-step start position, so the contact is created while the sphere is still δ = 11.4875 mm above the floor (gap(n) = 1.25 − g·Δt_sub²·n(n+1)/2, first value ≤ 0.02 m at n = 100). With mCombinedRestitution > 0 the whole bounce (incoming 4.95 m/s, outgoing 3.92 m/s) is applied in that single sub-step, and friction is applied in the same sub-step at the same point p. The closed form for the spurious angular momentum is

ΔL = |J_t| · δ / 2

With the measured tangential impulse |J_t| = 0.527 N·s this predicts ΔL/L₀ = +1.513 % against a measured +1.513 % (agreement to 0.02 %). Two independent consequences confirm the mechanism:

  • Reducing mSpeculativeContactDistance to 0.01 m or 0.005 m makes the contact be detected one sub-step later, at δ = −13.2775 mm (i.e. penetrating), so the lever arm becomes R − |δ|/2 and the error changes sign; both settings give the same ≈ −2.1 % because they select the same sub-step.
  • The error is independent of μ (the friction impulse is not Coulomb-clamped here: μ·J_n = 4.44 N·s ≫ 0.527 N·s), so it is not a friction-model issue — any tangential impulse on a speculative contact is affected.

Suggested fix

Derive the friction lever arms from each body's own contact point instead of the midpoint, leaving the non-penetration axis untouched (the normal axis is provably invariant under a translation along the normal, so this keeps the change bit-exact for frictionless contacts):

cpp
// inside the `if (inSettings.mCombinedFriction > 0.0f)` block
Vec3 rf1 = Vec3(inWorldSpacePosition1 - inBody1.GetCenterOfMassPosition());
Vec3 rf2 = Vec3(inWorldSpacePosition2 - inBody2.GetCenterOfMassPosition());
// pass rf1 / rf2 to both friction AxisConstraintPart instances

A fuller variant uses the per-body contact points for r1 / r2 everywhere; on convex-vs-convex manifolds p₁ − p₂ is parallel to the normal, so the normal Jacobian and effective mass are mathematically unchanged and only the (physically correct) tangential relative-velocity term differs.

Effect of the suggested fix (measured)

Observable Before After
ΔL/L₀ at the first impact (three drop heights) +1.513 % −0.00027 %
Steady rolling speed vs 5/7·v₀ +1.52 % 0.0 %
CONTACT_BEGIN / CONTACT_END counts for the case above 10 / 9 10 / 9 (unchanged)
Byte-identical regression baselines (5 recorded cases) 4 of 5 unchanged; only the rolling case (μ > 0 with an off-normal lever arm) changes

Contact detection, manifold reduction, the contact cache and warm starting are untouched by the change; only the friction lever arm moves.