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
+47 -29
View File
@@ -1724,10 +1724,10 @@ int mj_broadphase(const mjModel* m, mjData* d, int* bfpair, int maxpair) {
//----------------------------- narrow-phase collision detection -----------------------------------
// compute contact condim, solref, solimp, friction
// compute contact condim, solref, solimp, friction, adhesion
static void mj_contactParam(const mjModel* m, int* condim,
mjtNum* solref, mjtNum* solimp, mjtNum* friction,
int g1, int g2, int f1, int f2) {
mjtNum* adhesion, int g1, int g2, int f1, int f2) {
mjtNum fri[3];
// get parameters from geom1 or flex1
@@ -1737,6 +1737,7 @@ static void mj_contactParam(const mjModel* m, int* condim,
const mjtNum* solref1 = (f1 < 0) ? m->geom_solref+g1*mjNREF : m->flex_solref+f1*mjNREF;
const mjtNum* solimp1 = (f1 < 0) ? m->geom_solimp+g1*mjNIMP : m->flex_solimp+f1*mjNIMP;
const mjtNum* friction1 = (f1 < 0) ? m->geom_friction+g1*3 : m->flex_friction+f1*3;
mjtNum adhesion1 = (f1 < 0) ? m->geom_adhesion[g1] : 0;
// get parameters from geom2 or flex2
int priority2 = (f2 < 0) ? m->geom_priority[g2] : m->flex_priority[f2];
@@ -1745,6 +1746,10 @@ static void mj_contactParam(const mjModel* m, int* condim,
const mjtNum* solref2 = (f2 < 0) ? m->geom_solref+g2*mjNREF : m->flex_solref+f2*mjNREF;
const mjtNum* solimp2 = (f2 < 0) ? m->geom_solimp+g2*mjNIMP : m->flex_solimp+f2*mjNIMP;
const mjtNum* friction2 = (f2 < 0) ? m->geom_friction+g2*3 : m->flex_friction+f2*3;
mjtNum adhesion2 = (f2 < 0) ? m->geom_adhesion[g2] : 0;
// adhesion: each surface contributes its own attraction
*adhesion = adhesion1 + adhesion2;
// different priority: copy from item with higher priority
if (priority1 > priority2) {
@@ -1752,12 +1757,14 @@ static void mj_contactParam(const mjModel* m, int* condim,
mju_copy(solref, solref1, mjNREF);
mju_copy(solimp, solimp1, mjNIMP);
mju_copy(fri, friction1, 3);
*adhesion = adhesion1;
}
else if (priority1 < priority2) {
*condim = condim2;
mju_copy(solref, solref2, mjNREF);
mju_copy(solimp, solimp2, mjNIMP);
mju_copy(fri, friction2, 3);
*adhesion = adhesion2;
}
// same priority
@@ -1820,17 +1827,27 @@ static void mj_contactParam(const mjModel* m, int* condim,
static void mj_setContact(const mjModel* m, mjContact* con,
int condim, mjtNum includemargin,
const mjtNum* solref, const mjtNum* solreffriction,
const mjtNum* solimp, const mjtNum* friction) {
const mjtNum* solimp, const mjtNum* friction,
mjtNum adhesion) {
// set parameters
con->dim = condim;
con->includemargin = includemargin;
con->adhesion = adhesion;
mj_assignRef(m, con->solref, solref);
mj_assignRef(m, con->solreffriction, solreffriction);
mj_assignImp(m, con->solimp, solimp);
mj_assignFriction(m, con->friction, friction);
// exclude in gap
con->exclude = (con->dist >= includemargin);
// exclude in gap; adhesive contacts remain active in the gap, producing
// rows with positive violation whose reference acceleration pulls (tether)
con->exclude = (con->dist >= includemargin) && !adhesion;
// in-gap adhesive contacts have no surface contact and therefore no
// friction: reduce to normal-only rows (with tangential rows present, fast
// slip would recruit normal force and brake tangential motion at a distance)
if (adhesion && con->dist >= includemargin) {
con->dim = 1;
}
// complete frame
mju_makeFrame(con->frame);
@@ -2010,7 +2027,7 @@ static void mj_narrowphase(const mjModel* m, mjData* d, const mjcPair* buffer, i
continue;
int condim;
mjtNum friction[5], solref[mjNREF], solimp[mjNIMP];
mjtNum friction[5], solref[mjNREF], solimp[mjNIMP], adhesion;
mjtNum solreffriction[mjNREF] = {0};
int g1 = pairbuffer[i].geom_geom.g1;
int g2 = pairbuffer[i].geom_geom.g2;
@@ -2021,11 +2038,12 @@ static void mj_narrowphase(const mjModel* m, mjData* d, const mjcPair* buffer, i
mju_copy(solref, m->pair_solref+mjNREF*ipair, mjNREF);
mju_copy(solimp, m->pair_solimp+mjNIMP*ipair, mjNIMP);
mju_copy(friction, m->pair_friction+5*ipair, 5);
adhesion = m->pair_adhesion[ipair];
if (m->pair_solreffriction[mjNREF*ipair] || m->pair_solreffriction[mjNREF*ipair + 1]) {
mju_copy(solreffriction, m->pair_solreffriction+mjNREF*ipair, mjNREF);
}
} else {
mj_contactParam(m, &condim, solref, solimp, friction, g1, g2, -1, -1);
mj_contactParam(m, &condim, solref, solimp, friction, &adhesion, g1, g2, -1, -1);
}
mjPreContact* bc = arg.conbuffer + pairbuffer[i].conpos;
@@ -2044,7 +2062,7 @@ static void mj_narrowphase(const mjModel* m, mjData* d, const mjcPair* buffer, i
c->elem[1] = -1;
c->vert[0] = -1;
c->vert[1] = -1;
mj_setContact(m, c, condim, margin, solref, solreffriction, solimp, friction);
mj_setContact(m, c, condim, margin, solref, solreffriction, solimp, friction, adhesion);
}
conpos += ncon;
}
@@ -2065,9 +2083,9 @@ static void mj_collidePlaneFlex(const mjModel* m, mjData* d, int g, int f) {
int condim;
int flex_vertnum = m->flex_vertnum[f];
mjtNum gap = m->geom_gap[g] + m->flex_gap[f];
mjtNum solref[mjNREF], solimp[mjNIMP], friction[5];
mjtNum solref[mjNREF], solimp[mjNIMP], friction[5], adhesion;
mjtNum solreffriction[mjNREF] = {0};
mj_contactParam(m, &condim, solref, solimp, friction, g, -1, -1, f);
mj_contactParam(m, &condim, solref, solimp, friction, &adhesion, g, -1, -1, f);
// collide all flex vertices with plane
for (int i=0; i < flex_vertnum; i++) {
@@ -2099,7 +2117,7 @@ static void mj_collidePlaneFlex(const mjModel* m, mjData* d, int g, int f) {
con.vert[1] = i;
// set remaining contact parameters
mj_setContact(m, &con, condim, margin, solref, solreffriction, solimp, friction);
mj_setContact(m, &con, condim, margin, solref, solreffriction, solimp, friction, adhesion);
// add to mjData, abort if too many contacts
if (mj_addContact(m, d, &con)) {
@@ -2120,9 +2138,9 @@ static void mj_collideSdfFlex(const mjModel* m, mjData* d, int g, int f) {
mjtNum margin = mj_assignMargin(m, m->geom_margin[g] + m->flex_margin[f]);
mjtNum gap = m->geom_gap[g] + m->flex_gap[f];
int condim;
mjtNum solref[mjNREF], solimp[mjNIMP], friction[5];
mjtNum solref[mjNREF], solimp[mjNIMP], friction[5], adhesion;
mjtNum solreffriction[mjNREF] = {0};
mj_contactParam(m, &condim, solref, solimp, friction, g, -1, -1, f);
mj_contactParam(m, &condim, solref, solimp, friction, &adhesion, g, -1, -1, f);
// allocate temporary contact array on stack (zero-initialized)
mj_markStack(d);
@@ -2153,7 +2171,7 @@ static void mj_collideSdfFlex(const mjModel* m, mjData* d, int g, int f) {
con.vert[1] = -1;
// set remaining contact parameters
mj_setContact(m, &con, condim, margin, solref, solreffriction, solimp, friction);
mj_setContact(m, &con, condim, margin, solref, solreffriction, solimp, friction, adhesion);
// add to mjData, abort if too many contacts
if (mj_addContact(m, d, &con)) {
@@ -2221,9 +2239,9 @@ static void mj_collideFlexInternal(const mjModel* m, mjData* d, int f) {
int condim;
int flex_elemnum = m->flex_elemnum[f];
mjtNum radius = m->flex_radius[f];
mjtNum solref[mjNREF], solimp[mjNIMP], friction[5];
mjtNum solref[mjNREF], solimp[mjNIMP], friction[5], adhesion;
mjtNum solreffriction[mjNREF] = {0};
mj_contactParam(m, &condim, solref, solimp, friction, -1, -1, f, f);
mj_contactParam(m, &condim, solref, solimp, friction, &adhesion, -1, -1, f, f);
condim = 1;
// process all elements
@@ -2240,7 +2258,7 @@ static void mj_collideFlexInternal(const mjModel* m, mjData* d, int f) {
mju_copy3(con.pos, precon.pos);
mju_copy3(con.frame, precon.normal);
mju_copy3(con.frame + 3, precon.tangent);
mj_setContact(m, &con, condim, 0, solref, solreffriction, solimp, friction);
mj_setContact(m, &con, condim, 0, solref, solreffriction, solimp, friction, adhesion);
if (mj_addContact(m, d, &con)) return;
}
@@ -2251,7 +2269,7 @@ static void mj_collideFlexInternal(const mjModel* m, mjData* d, int f) {
mju_copy3(con.pos, precon.pos);
mju_copy3(con.frame, precon.normal);
mju_copy3(con.frame + 3, precon.tangent);
mj_setContact(m, &con, condim, 0, solref, solreffriction, solimp, friction);
mj_setContact(m, &con, condim, 0, solref, solreffriction, solimp, friction, adhesion);
if (mj_addContact(m, d, &con)) return;
}
@@ -2262,7 +2280,7 @@ static void mj_collideFlexInternal(const mjModel* m, mjData* d, int f) {
mju_copy3(con.pos, precon.pos);
mju_copy3(con.frame, precon.normal);
mju_copy3(con.frame + 3, precon.tangent);
mj_setContact(m, &con, condim, 0, solref, solreffriction, solimp, friction);
mj_setContact(m, &con, condim, 0, solref, solreffriction, solimp, friction, adhesion);
if (mj_addContact(m, d, &con)) return;
}
@@ -2273,7 +2291,7 @@ static void mj_collideFlexInternal(const mjModel* m, mjData* d, int f) {
mju_copy3(con.pos, precon.pos);
mju_copy3(con.frame, precon.normal);
mju_copy3(con.frame + 3, precon.tangent);
mj_setContact(m, &con, condim, 0, solref, solreffriction, solimp, friction);
mj_setContact(m, &con, condim, 0, solref, solreffriction, solimp, friction, adhesion);
if (mj_addContact(m, d, &con)) return;
}
}
@@ -2439,9 +2457,9 @@ void mj_collideGeomElem(const mjModel* m, mjData* d, int g, int f, int e) {
// get contact parameters
int condim;
mjtNum friction[5], solref[mjNREF], solimp[mjNIMP];
mjtNum friction[5], solref[mjNREF], solimp[mjNIMP], adhesion;
mjtNum solreffriction[mjNREF] = {0};
mj_contactParam(m, &condim, solref, solimp, friction, g, -1, -1, f);
mj_contactParam(m, &condim, solref, solimp, friction, &adhesion, g, -1, -1, f);
// allocate mjContact[num] on the arena
mjContact* con =
@@ -2475,7 +2493,7 @@ void mj_collideGeomElem(const mjModel* m, mjData* d, int g, int f, int e) {
}
// set remaining contact parameters
mj_setContact(m, con + i, condim, margin, solref, solreffriction, solimp, friction);
mj_setContact(m, con + i, condim, margin, solref, solreffriction, solimp, friction, adhesion);
}
// add to ncon
@@ -2548,9 +2566,9 @@ void mj_collideElems(const mjModel* m, mjData* d, int f1, int e1, int f2, int e2
// get contact parameters
int condim;
mjtNum friction[5], solref[mjNREF], solimp[mjNIMP];
mjtNum friction[5], solref[mjNREF], solimp[mjNIMP], adhesion;
mjtNum solreffriction[mjNREF] = {0};
mj_contactParam(m, &condim, solref, solimp, friction, -1, -1, f1, f2);
mj_contactParam(m, &condim, solref, solimp, friction, &adhesion, -1, -1, f1, f2);
mjContact* con = (mjContact*) mj_arenaAllocByte(d, sizeof(mjContact) * num, _Alignof(mjContact));
if (!con) {
@@ -2577,7 +2595,7 @@ void mj_collideElems(const mjModel* m, mjData* d, int f1, int e1, int f2, int e2
mju_copy3(con[i].frame + 3, precon[i].tangent);
// set remaining contact parameters
mj_setContact(m, con + i, condim, margin, solref, solreffriction, solimp, friction);
mj_setContact(m, con + i, condim, margin, solref, solreffriction, solimp, friction, adhesion);
}
// add to ncon
@@ -2638,9 +2656,9 @@ void mj_collideElemVert(const mjModel* m, mjData* d, int f, int e, int v) {
// get contact parameters
int condim;
mjtNum friction[5], solref[mjNREF], solimp[mjNIMP];
mjtNum friction[5], solref[mjNREF], solimp[mjNIMP], adhesion;
mjtNum solreffriction[mjNREF] = {0};
mj_contactParam(m, &condim, solref, solimp, friction, -1, -1, f, f);
mj_contactParam(m, &condim, solref, solimp, friction, &adhesion, -1, -1, f, f);
// allocate mjContact[num] on the arena
@@ -2671,7 +2689,7 @@ void mj_collideElemVert(const mjModel* m, mjData* d, int f, int e, int v) {
mju_copy3(con[i].frame + 3, precon[i].tangent);
// set remaining contact parameters
mj_setContact(m, con + i, condim, 0, solref, solreffriction, solimp, friction);
mj_setContact(m, con + i, condim, 0, solref, solreffriction, solimp, friction, adhesion);
}
// add to ncon
+35
View File
@@ -3199,6 +3199,38 @@ static void mj_addSurfaceVel(const mjModel* m, mjData* d) {
// bias aref of adhesive contact rows: makes the compression branch of the net
// force-penetration curve independent of adhesion (exact translated-cone semantics)
static void mj_adhesionRef(const mjModel* m, mjData* d) {
// no adhesion on any geom or pair: quick return
if (!m->flg_adhesion) {
return;
}
int ispyramid = mj_isPyramidal(m), ncon = d->ncon;
for (int i=0; i < ncon; i++) {
const mjContact* con = d->contact + i;
if (!con->adhesion || con->efc_address < 0) {
continue;
}
int adr = con->efc_address, dim = con->dim;
if (dim == 1 || !ispyramid) {
// normal row
d->efc_aref[adr] += d->efc_R[adr] * con->adhesion;
} else {
// pyramid rows: the normal decomposes equally over the 2*(dim-1) edges
mjtNum edge = con->adhesion / (2*(dim-1));
for (int j=0; j < 2*(dim-1); j++) {
d->efc_aref[adr+j] += d->efc_R[adr+j] * edge;
}
}
}
}
// compute efc_vel, efc_aref
void mj_referenceConstraint(const mjModel* m, mjData* d) {
int nefc = d->nefc;
@@ -3216,6 +3248,9 @@ void mj_referenceConstraint(const mjModel* m, mjData* d) {
-KBIP[4*i]*KBIP[4*i+2]*(d->efc_pos[i]-d->efc_margin[i]);
}
// bias adhesive contact rows
mj_adhesionRef(m, d);
// subtract Jdot*v correction for connect/weld equality constraints
if (d->ne > 0) {
mj_Jdotv(m, d, d->efc_aref);
+3
View File
@@ -1077,6 +1077,9 @@ void mj_contactForce(const mjModel* m, const mjData* d, int id, mjtNum result[6]
} else {
mju_copy(result, d->efc_force + con->efc_address, con->dim);
}
// report the net interface force: the solver's cone force minus the adhesive pull
result[0] -= con->adhesion;
}
}
+78
View File
@@ -962,6 +962,71 @@ int mj_contactPassive(const mjModel* m, mjData* d) {
}
// adhesion forces: constant attraction along the normals of adhesive contacts
int mj_adhesion(const mjModel* m, mjData* d) {
int ncon = d->ncon, issparse = mj_isSparse(m);
int NV, nv = m->nv, *chain = NULL;
mjtNum *jac, *jacdif, *jac1p, *jac2p, *qfrc;
mjContact* con;
int has_adhesion = 0;
if (!m->flg_adhesion || mjDISABLED(mjDSBL_CONTACT) || ncon == 0 || nv == 0) {
return 0;
}
// early return if no adhesive contact
for (int i=0; i < ncon; i++) {
if (d->contact[i].adhesion && d->contact[i].exclude <= 1) {
has_adhesion = 1;
break;
}
}
if (!has_adhesion) {
return 0;
}
// allocate Jacobian; contacts are normal-only (dim 1): no rotational buffers
mj_markStack(d);
jac = mjSTACKALLOC(d, nv, mjtNum);
jacdif = mjSTACKALLOC(d, 3*nv, mjtNum);
jac1p = mjSTACKALLOC(d, 3*nv, mjtNum);
jac2p = mjSTACKALLOC(d, 3*nv, mjtNum);
qfrc = mjSTACKALLOC(d, nv, mjtNum);
if (issparse) {
chain = mjSTACKALLOC(d, nv, int);
}
// pull adhesive contacts together along the contact normal
for (int i=0; i < ncon; i++) {
con = d->contact + i;
if (!con->adhesion || con->exclude > 1) {
continue;
}
// normal Jacobian
NV = mj_contactJacobian(m, d, con, 1, jac, jacdif, jacdif, NULL,
jac1p, jac2p, NULL, NULL, chain);
if (NV == 0) {
continue;
}
mju_mulMatMat(jac, con->frame, jacdif, 1, 3, NV);
// accumulate qfrc_adhesion += jacN' * (-adhesion)
if (!issparse) {
mju_addToScl(d->qfrc_adhesion, jac, -con->adhesion, nv);
} else {
mju_scl(qfrc, jac, -con->adhesion, NV);
for (int j=0; j < NV; j++) {
d->qfrc_adhesion[chain[j]] += qfrc[j];
}
}
}
mj_freeStack(d);
return 1;
}
// all passive forces
void mj_passive(const mjModel* m, mjData* d) {
int sleep_filter = mjENABLED(mjENBL_SLEEP) && d->nv_awake < m->nv;
@@ -974,12 +1039,14 @@ void mj_passive(const mjModel* m, mjData* d) {
mju_zeroInd(d->qfrc_damper, nv, dof_awake_ind);
mju_zeroInd(d->qfrc_gravcomp, nv, dof_awake_ind);
mju_zeroInd(d->qfrc_fluid, nv, dof_awake_ind);
mju_zeroInd(d->qfrc_adhesion, nv, dof_awake_ind);
mju_zeroInd(d->qfrc_passive, nv, dof_awake_ind);
} else {
mju_zero(d->qfrc_spring, nv);
mju_zero(d->qfrc_damper, nv);
mju_zero(d->qfrc_gravcomp, nv);
mju_zero(d->qfrc_fluid, nv);
mju_zero(d->qfrc_adhesion, nv);
mju_zero(d->qfrc_passive, nv);
}
@@ -1000,6 +1067,9 @@ void mj_passive(const mjModel* m, mjData* d) {
// contact forces
mj_contactPassive(m, d);
// adhesion forces
int has_adhesion = mj_adhesion(m, d);
// add passive forces into qfrc_passive
if (sleep_filter) {
mju_addInd(d->qfrc_passive, d->qfrc_spring, d->qfrc_damper, dof_awake_ind, nv);
@@ -1015,6 +1085,14 @@ void mj_passive(const mjModel* m, mjData* d) {
}
}
if (has_adhesion) {
if (sleep_filter) {
mju_addToInd(d->qfrc_passive, d->qfrc_adhesion, dof_awake_ind, nv);
} else {
mju_addTo(d->qfrc_passive, d->qfrc_adhesion, nv);
}
}
if (has_gravcomp) {
int ndof = sleep_filter ? d->nv_awake : nv;
for (int v=0; v < ndof; v++) {
+3
View File
@@ -28,6 +28,9 @@ extern "C" {
// all passive forces
MJAPI void mj_passive(const mjModel* m, mjData* d);
// adhesion forces: constant attraction along the normals of adhesive contacts
int mj_adhesion(const mjModel* m, mjData* d);
+15
View File
@@ -223,6 +223,21 @@ static void setFixed(mjModel* m, mjData* d) {
}
m->flg_surfacevel = flg_surfacevel;
// compute flg_adhesion: whether any geom or pair has nonzero adhesion
mjtBool flg_adhesion = 0;
for (int i=0; i < m->ngeom; i++) {
if (m->geom_adhesion[i]) {
flg_adhesion = 1;
break;
}
}
for (int i=0; i < m->npair && !flg_adhesion; i++) {
if (m->pair_adhesion[i]) {
flg_adhesion = 1;
}
}
m->flg_adhesion = flg_adhesion;
// set jnt_actuatorid and tendon_actuatorid
mju_fillInt(m->jnt_actuatorid, -1, m->njnt);
mju_fillInt(m->tendon_actuatorid, -1, m->ntendon);