Add geom adhesion: contacts that pull, via translated friction cones.

https://youtu.be/GioWwB36XHI

The new geom attribute adhesion (units of force, signed; pair-level
override) translates the contact friction cone along its normal so
that the force origin lies strictly inside it. Consequences: each
contact can pull with up to the given force before breaking, and the
tangential friction budget becomes mu*(f_N + adhesion) -- the
Mohr-Coulomb yield condition with cohesion c = mu*adhesion -- so
lightly-squeezed grasps retain a guaranteed friction floor.

A translated cone factors exactly into {constant attractive force}
+ {original cone}, so no solver kernels change. The implementation is
this factorization: a constant attraction along contact normals
accumulated into the new mjData.qfrc_adhesion (summed into
qfrc_passive), plus a bias of adhesive contact rows' reference
acceleration (aref += R*adhesion), which makes resting penetration
exactly independent of adhesion. Contacts of adhesive pairs remain
active throughout the gap zone, producing rows with positive violation
whose reference acceleration pulls: a tether that resists pull-off
smoothly, captures objects released within the band into steady
contact, and detaches at the specified force. Adhesion values of the
two geoms combine by sum; explicit pairs override.

mj_contactForce reports the net interface force (cone force minus the
adhesive pull), whose normal component can now be negative. Negative
adhesion is allowed and produces a repulsive offset (air hockey).

PiperOrigin-RevId: 950858148
Change-Id: I879c08eba7ae501e5c0f8c2f807167344da4c2bc
This commit is contained in:
Yuval Tassa
2026-07-20 08:35:17 -07:00
committed by Copybara-Service
parent b942c9f922
commit a264d0bc8b
28 changed files with 784 additions and 50 deletions
+12 -7
View File
@@ -238,7 +238,8 @@ std::vector<const char*> MJCF[nMJCF] = {
{"geom", "?", "type", "pos", "quat", "contype", "conaffinity", "condim",
"group", "priority", "size", "material", "friction", "mass", "density",
"shellinertia", "solmix", "solref", "solimp",
"margin", "gap", "surfacevel", "fromto", "axisangle", "xyaxes", "zaxis", "euler",
"margin", "gap", "surfacevel", "adhesion", "fromto", "axisangle", "xyaxes", "zaxis",
"euler",
"hfield", "mesh", "fitscale", "rgba", "fluidshape", "fluidcoef", "user"},
{"site", "?", "type", "group", "pos", "quat", "material",
"size", "fromto", "axisangle", "xyaxes", "zaxis", "euler", "rgba", "user"},
@@ -249,7 +250,7 @@ std::vector<const char*> MJCF[nMJCF] = {
"directional", "type", "castshadow", "active", "attenuation", "cutoff", "exponent",
"ambient", "diffuse", "specular", "mode"},
{"pair", "?", "condim", "friction", "solref", "solreffriction", "solimp",
"gap", "margin"},
"gap", "margin", "adhesion"},
{"equality", "?", "active", "solref", "solimp"},
{"tendon", "?", "group", "limited", "range",
"solreflimit", "solimplimit", "solreffriction", "solimpfriction",
@@ -349,9 +350,9 @@ std::vector<const char*> MJCF[nMJCF] = {
{"geom", "*", "name", "class", "type", "contype", "conaffinity", "condim",
"group", "priority", "size", "material", "friction", "mass", "density",
"shellinertia", "solmix", "solref", "solimp",
"margin", "gap", "surfacevel", "fromto", "pos", "quat", "axisangle", "xyaxes",
"zaxis", "euler", "hfield", "mesh", "fitscale", "rgba", "fluidshape", "fluidcoef",
"user"},
"margin", "gap", "surfacevel", "adhesion", "fromto", "pos", "quat", "axisangle",
"xyaxes", "zaxis", "euler", "hfield", "mesh", "fitscale", "rgba", "fluidshape",
"fluidcoef", "user"},
{"<"},
{"plugin", "*", "plugin", "instance"},
{"<"},
@@ -381,7 +382,8 @@ std::vector<const char*> MJCF[nMJCF] = {
{"skin", "?", "texcoord", "material", "group", "rgba", "inflate", "subgrid"},
{"geom", "?", "type", "contype", "conaffinity", "condim",
"group", "priority", "size", "material", "rgba", "friction", "mass",
"density", "solmix", "solref", "solimp", "margin", "gap", "surfacevel"},
"density", "solmix", "solref", "solimp", "margin", "gap", "surfacevel",
"adhesion"},
{"site", "?", "group", "size", "material", "rgba"},
{"plugin", "*", "plugin", "instance"},
{"<"},
@@ -427,7 +429,7 @@ std::vector<const char*> MJCF[nMJCF] = {
{"contact", "*"},
{"<"},
{"pair", "*", "name", "class", "geom1", "geom2", "condim", "friction",
"solref", "solreffriction", "solimp", "gap", "margin"},
"solref", "solreffriction", "solimp", "gap", "margin", "adhesion"},
{"exclude", "*", "name", "body1", "body2"},
{">"},
@@ -1975,6 +1977,7 @@ void mjXReader::OneGeom(XMLElement* elem, mjsGeom* geom) {
ReadAttr(elem, "margin", 1, &geom->margin, text);
ReadAttr(elem, "gap", 1, &geom->gap, text);
ReadAttr(elem, "surfacevel", 6, geom->surfacevel, text, false, false);
ReadAttr(elem, "adhesion", 1, &geom->adhesion, text);
if (ReadAttrTxt(elem, "hfield", hfieldname)) {
mjs_setString(geom->hfieldname, hfieldname.c_str());
}
@@ -2197,6 +2200,7 @@ void mjXReader::OnePair(XMLElement* elem, mjsPair* pair) {
ReadAttr(elem, "solimp", mjNIMP, pair->solimp, text, false, false);
ReadAttr(elem, "margin", 1, &pair->margin, text);
ReadAttr(elem, "gap", 1, &pair->gap, text);
ReadAttr(elem, "adhesion", 1, &pair->adhesion, text);
ReadAttr(elem, "friction", 5, pair->friction, text, false, false);
// write error info
@@ -2785,6 +2789,7 @@ void mjXReader::OneComposite(XMLElement* elem, mjsBody* body, mjsFrame* frame, c
ReadAttr(egeom, "margin", 1, &dgeom.margin, text);
ReadAttr(egeom, "gap", 1, &dgeom.gap, text);
ReadAttr(egeom, "surfacevel", 6, dgeom.surfacevel, text, false, false);
ReadAttr(egeom, "adhesion", 1, &dgeom.adhesion, text);
if (ReadAttrTxt(egeom, "material", material)) {
mjs_setString(dgeom.material, material.c_str());
}