pub struct ContactMaterial {
pub stiffness: f64,
pub damping: f64,
pub friction: f64,
pub restitution: f64,
pub soft_cfm: f64,
pub soft_erp: f64,
pub solref: SolRef,
pub solimp: SolImp,
pub margin: f64,
}Expand description
Material properties for contact interactions.
Fields§
§stiffness: f64Contact stiffness (N/m).
damping: f64Contact damping (N·s/m).
friction: f64Coefficient of friction (dimensionless).
restitution: f64Coefficient of restitution (0 = inelastic, 1 = elastic).
Was bounce, a field no solver ever read — ContactMaterial::bouncy()
did nothing. It is now honoured by the convex solve, entering as a
target normal velocity rather than a post-solve velocity reset. See
docs/design/differentiable-contact.md §4.3.
soft_cfm: f64Constraint force mixing (for numerical stability).
soft_erp: f64Error reduction parameter (for constraint drift correction).
solref: SolRefReference response of the contact constraint (MuJoCo solref).
This is what gives the convex solve a penetration-recovery bias.
Without it ContactRow::depth was carried around and never read, so
penetration was never paid back and a stack crept forever.
solimp: SolImpDepth-dependent constraint impedance (MuJoCo solimp).
Supersedes a single scalar ContactSolverConfig::regularization as the
softness knob: the regularizer is now R = (1-d)/d * diag(A) per row,
with the config value acting as a floor.
margin: f64Contact margin, in metres: how far apart surfaces may be and still be included in the solve.
This used to be inert (“the narrow phase owns detection”). It is not:
with soft contact a candidate at exactly zero depth still carries the
full solimp.dmin share of the load, so a detection predicate that cuts
off at zero gap cuts off a contact that was carrying real force. Measured
on a humanoid stance, the least-loaded foot corner was carrying 22.3 N —
11% of body weight — on the step before it left the contact set, and went
to 0 N the moment it rose a nanometre above the plane.
The margin is the band above the surface over which a contact is still
generated, with a negative penetration_depth, and its impedance
tapers to zero — see ContactMaterial::impedance_at. That turns the
step into a ramp.
The default is SolImp::default’s width, i.e. the same 1 mm scale
over which solimp already varies the impedance on the penetrating side.
Picking the same scale makes impedance one continuous, C^1 function of
signed gap across the whole band [-margin, +width] rather than two
curves glued at zero.
Combines with max (MuJoCo’s rule): the larger margin is the one asking
for earlier detection, and min would silently defeat it.
Implementations§
Source§impl ContactMaterial
impl ContactMaterial
Sourcepub fn new(
stiffness: f64,
damping: f64,
friction: f64,
restitution: f64,
) -> Self
pub fn new( stiffness: f64, damping: f64, friction: f64, restitution: f64, ) -> Self
Create a new contact material with custom parameters.
Sourcepub fn impedance_at(&self, depth: f64) -> f64
pub fn impedance_at(&self, depth: f64) -> f64
Constraint impedance for a contact at signed penetration depth,
where negative means the surfaces are separated by -depth metres.
On the penetrating side (depth >= 0) this is exactly
SolImp::impedance and nothing has changed. Inside the margin band
(-margin < depth < 0) it is dmin scaled by a smoothstep that reaches
zero at depth = -margin.
Why impedance, and why this makes the force continuous. The
regularizer is R = (1-d)/d * A_nn (see
phyz_contact::convex::regularization_diag), so a single contact resolving a
free approach velocity v carries a normal impulse of about
v / (A_nn + R) = d * v / A_nn — linear in d. Taking d smoothly to
zero therefore takes the normal force smoothly to zero, and the contact
leaves the set carrying nothing instead of carrying 11% of body weight.
It also cannot pull: d -> 0 means “infinitely soft”, not “attractive”.
The normal impulse stays in the friction cone’s f_n >= 0 half-line, and
phyz_contact’s ContactRow::from_material clamps the stabilization bias at
depth.max(0.0), so a separated contact has no bias to push or pull
with. Friction rides on mu * f_n and vanishes with it.
The join at depth = 0 is C^1: smoothstep has zero slope at its top
end, and the solimp sigmoid has zero slope at x = 0 for power > 1.
Sourcepub fn dimpedance_ddepth(&self, depth: f64) -> f64
pub fn dimpedance_ddepth(&self, depth: f64) -> f64
d(impedance)/d(depth), the derivative of Self::impedance_at.
This is not a nicety. Inside the margin band the stabilization bias is
identically zero, so impedance is the only channel by which depth
reaches the impulses — and it is a strong one: the measured force ramps
from 17.66 N to 0 across a 1 mm band, roughly 1.8e4 N/m. A gradient
that dropped this term would report exactly zero sensitivity across the
whole region where contact activation is differentiable, which is
precisely the region a contact-timing gradient lives in.
On the penetrating side it is the solimp sigmoid’s own slope, worth up
to (dmax-dmin)/dmin of the primary term. That used to be dropped as a
“second-order path”; it is included now, so there is one expression
rather than two regimes with different honesty.
Both branches are exact, and they agree at depth = 0 (both zero for
power > 1), which is what makes Self::impedance_at C^1 there.
Sourcepub fn combine(a: &Self, b: &Self) -> Self
pub fn combine(a: &Self, b: &Self) -> Self
Combine the two materials of a contacting pair into the one material the contact is actually solved with.
A contact belongs to two surfaces, so using one side’s material — as
assemble did, always taking body_i’s — makes the physics depend on
which body the narrow phase happened to list first. Rubber on ice slid
or gripped depending on collision-pair ordering.
The rules, and why each one:
-
Friction:
max. This is MuJoCo’s rule (mj_contactParamtakes the elementwise maximum of the two geoms’ friction when neither has priority), and it is the conservative choice for a simulator: a sticky surface stays sticky against a slippery one, so a foot does not silently lose grip because the floor was authored with a low default. Note this ismax, notmin— the mixing rule matters more than any single material’s number, so it follows MuJoCo rather than the intuition that the slipperier surface should win.This is the rule that decides which body you attach a material to, and it is asymmetric in a way that bites. A material lives on a
crate::Bodyand applies to every contact that body is part of, somaxmeans a grippy body grips everything it touches. Measured on a skateboarding robot: grip tape (mu = 1.5) put on the deck correctly grips the shoes — but the deck’s bare underside then also grips the asphalt during the tail scrape, which braked the board from 0.6 to 0.39 m/s and killed the trick. The same 1.5 on the shoes raises only the foot-on-deck contacts and leaves deck-on-ground at wood’s 0.6. Put the material on the part whose surface it describes, then check what else that part touches. -
Margin:
max. Also MuJoCo’s. The larger margin is the one that wants contacts generated earlier; taking the min would silently defeat a surface that asked for early detection. -
Restitution:
max. Same argument: a bouncy ball stays bouncy on an inelastic floor. -
Everything stiffness-like —
stiffness,damping,soft_cfm,soft_erp, and everysolref/solimpscalar: geometric mean. These span orders of magnitude (soft_cfmfrom1e-5to1e-3, stiffness from1e3to5e4), and the arithmetic mean of two such numbers is just the larger one — soft rubber against a rigid floor would come out rigid. The geometric mean is the mean that respects a log-scaled quantity, and it also makes the combination invariant to the units the quantity is expressed in. MuJoCo uses asolmix- weighted arithmetic average here; with no per-material mix weight to honour, the geometric mean is the better-behaved default.
The rule is commutative in both arguments, which is the property that makes the result independent of narrow-phase ordering.
Trait Implementations§
Source§impl Clone for ContactMaterial
impl Clone for ContactMaterial
Source§fn clone(&self) -> ContactMaterial
fn clone(&self) -> ContactMaterial
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more