Skip to main content

ContactMaterial

Struct ContactMaterial 

Source
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: f64

Contact stiffness (N/m).

§damping: f64

Contact damping (N·s/m).

§friction: f64

Coefficient of friction (dimensionless).

§restitution: f64

Coefficient 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: f64

Constraint force mixing (for numerical stability).

§soft_erp: f64

Error reduction parameter (for constraint drift correction).

§solref: SolRef

Reference 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: SolImp

Depth-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: f64

Contact 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

Source

pub fn new( stiffness: f64, damping: f64, friction: f64, restitution: f64, ) -> Self

Create a new contact material with custom parameters.

Source

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.

Source

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.

Source

pub fn bouncy() -> Self

Create a bouncy material (high restitution).

Source

pub fn soft() -> Self

Create a soft material (low stiffness).

Source

pub fn rigid() -> Self

Create a rigid material (high stiffness).

Source

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_contactParam takes 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 is max, not min — 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::Body and applies to every contact that body is part of, so max means 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 every solref/solimp scalar: geometric mean. These span orders of magnitude (soft_cfm from 1e-5 to 1e-3, stiffness from 1e3 to 5e4), 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 a solmix- 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

Source§

fn clone(&self) -> ContactMaterial

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for ContactMaterial

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for ContactMaterial

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.