Add 2D membrane elasticity for interpolated flex shell mode

When elastic2d="stretch" is set on an interpolated flexcomp, treat the bounding box boundary as membrane elements rather than volumetric cells. This computes plane-stress stiffness over the boundary faces and updates the runtime force/derivative kernels accordingly.

Interior vertex tracking (moving vertices that follow the deforming shell) is not yet implemented so all mesh vertices need to be on the bounding box surface or the background grid should have no interior nodes (i.e. cellcount should be 1 on at least one axis).

PiperOrigin-RevId: 907654080
Change-Id: I51b90e2f6a1d1b036f9604e42de20e377dc5d3f9
This commit is contained in:
Alessio Quaglino
2026-04-29 10:16:38 -07:00
committed by Copybara-Service
parent 517c113656
commit 9c6a4f76eb
15 changed files with 1378 additions and 349 deletions
+36 -16
View File
@@ -884,27 +884,47 @@ bool mjCFlexcomp::Make(mjsBody* body, char* error, int error_sz, const mjVFS* vf
pe->active = true;
mjs_setString(pe->name1, name.c_str());
} else if (equality == 3) {
// create one strain constraint per cell, storing cell index in eq_data
// create one strain constraint per finite element, storing element index
flex->has_strain_eq = true;
int cell_cx = flex->spec.cellcount[0];
int cell_cy = flex->spec.cellcount[1];
int cell_cz = flex->spec.cellcount[2];
for (int ci = 0; ci < cell_cx; ci++) {
for (int cj = 0; cj < cell_cy; cj++) {
for (int ck = 0; ck < cell_cz; ck++) {
// skip empty cells
if (!flex->cell_empty.empty() &&
flex->cell_empty[ci * cell_cy * cell_cz + cj * cell_cz + ck]) {
continue;
bool shell = (doftype == mjFCOMPDOF_TRILINEAR ||
doftype == mjFCOMPDOF_QUADRATIC) &&
flex->spec.elastic2d;
if (shell) {
// shell mode: one constraint per boundary face element
int nelem_fe = 2*(cell_cy*cell_cz + cell_cx*cell_cz + cell_cx*cell_cy);
for (int fe = 0; fe < nelem_fe; fe++) {
mjsEquality* pe = mjs_addEquality(&model->spec, &def.spec);
mjs_setDefault(pe->element, &model->Default()->spec);
pe->type = mjEQ_FLEXSTRAIN;
pe->active = true;
mjs_setString(pe->name1, name.c_str());
pe->data[0] = fe;
pe->data[1] = -1; // sentinel: shell mode
pe->data[2] = -1;
}
} else {
// volume mode: one constraint per 3D cell
for (int ci = 0; ci < cell_cx; ci++) {
for (int cj = 0; cj < cell_cy; cj++) {
for (int ck = 0; ck < cell_cz; ck++) {
// skip empty cells
if (!flex->cell_empty.empty() &&
flex->cell_empty[ci * cell_cy * cell_cz + cj * cell_cz + ck]) {
continue;
}
mjsEquality* pe = mjs_addEquality(&model->spec, &def.spec);
mjs_setDefault(pe->element, &model->Default()->spec);
pe->type = mjEQ_FLEXSTRAIN;
pe->active = true;
mjs_setString(pe->name1, name.c_str());
pe->data[0] = ci;
pe->data[1] = cj;
pe->data[2] = ck;
}
mjsEquality* pe = mjs_addEquality(&model->spec, &def.spec);
mjs_setDefault(pe->element, &model->Default()->spec);
pe->type = mjEQ_FLEXSTRAIN;
pe->active = true;
mjs_setString(pe->name1, name.c_str());
pe->data[0] = ci;
pe->data[1] = cj;
pe->data[2] = ck;
}
}
}
+215 -44
View File
@@ -3807,6 +3807,98 @@ void inline ComputeLinearStiffness(std::vector<double>& K,
}
// compute the linear stiffness matrix for a flat 2D quad face element (membrane)
// K: output stiffness matrix, size 3*npe x 3*npe, npe = (order+1)^2
// pos: node positions (3*npe doubles), ordered row-major in 2D parametric domain
// E, nu: Young's modulus and Poisson's ratio
// order: interpolation order (1 or 2)
// thickness: shell thickness
// normal_axis: axis perpendicular to the face (0=x, 1=y, 2=z)
void inline ComputeLinearStiffness2D(std::vector<double>& K,
const double* pos,
double E, double nu, int order,
double thickness, int normal_axis) {
int nbasis = order + 1;
int npe = nbasis * nbasis; // nodes per face element
int ndof = 3 * npe;
// in-plane axes
int axis0 = (normal_axis + 1) % 3; // slow-varying
int axis1 = (normal_axis + 2) % 3; // fast-varying
// compute quadrature points
std::vector<double> points(nbasis);
std::vector<double> weight(nbasis);
quadratureGaussLegendre(points.data(), weight.data(), nbasis, 0, 1);
// compute element transformation (diagonal Jacobian on flat face)
double d0 = (pos + 3*(npe-1))[axis0] - pos[axis0]; // extent along axis0
double d1 = (pos + 3*(npe-1))[axis1] - pos[axis1]; // extent along axis1
if (d0 == 0 || d1 == 0) {
throw mjCError(nullptr, "degenerate 2D element with zero extent");
}
double detJ = d0 * d1;
double invJ0 = 1.0 / d0;
double invJ1 = 1.0 / d1;
// plane-stress Lamé parameter: lambda* = E*nu/(1 - nu^2)
double la = E * nu / (1.0 - nu * nu);
double mu = E / (2.0 * (1.0 + nu));
// basis function gradients (2-component)
std::vector<std::array<double, 2>> F(npe);
// loop over quadrature points (2D)
for (int ps = 0; ps < nbasis; ps++) {
for (int pt = 0; pt < nbasis; pt++) {
double s = points[ps];
double t = points[pt];
double dvol = weight[ps] * weight[pt] * detJ * thickness;
int dof = 0;
// cartesian product of 2D basis functions
for (int b0 = 0; b0 < nbasis; b0++) {
for (int b1 = 0; b1 < nbasis; b1++) {
F[dof][0] = dphi(s, b0, order) * phi(t, b1, order);
F[dof][1] = phi(s, b0, order) * dphi(t, b1, order);
dof++;
}
}
if (dof != npe) {
throw mjCError(nullptr, "incorrect number of 2D basis functions");
}
// tensor contraction (same structure as 3D but with zero normal column)
for (int i = 0; i < npe; i++) {
for (int j = 0; j < npe; j++) {
Matrix du;
Matrix dv;
du.fill({0, 0, 0});
dv.fill({0, 0, 0});
for (int k = 0; k < 3; k++) {
for (int l = 0; l < 3; l++) {
// du[k] has non-zero entries only at in-plane axes
du[k][axis0] = invJ0 * F[i][0];
du[k][axis1] = invJ1 * F[i][1];
// du[k][normal_axis] = 0 (already zero)
dv[l][axis0] = invJ0 * F[j][0];
dv[l][axis1] = invJ1 * F[j][1];
// dv[l][normal_axis] = 0 (already zero)
K[ndof*(3*i+k) + 3*j+l] -= la * trace(du) * trace(dv) * dvol;
// mu (not 2*mu): same convention as 3D ComputeLinearStiffness
K[ndof*(3*i+k) + 3*j+l] -= mu * trace(inner(sym(du), sym(dv))) * dvol;
mjuu_zerovec(du[k].data(), 3);
mjuu_zerovec(dv[l].data(), 3);
}
}
}
}
}
}
}
// Eigendecompose cell stiffness matrix and store scaled eigenvectors.
// K_cell is n×n stored (negative convention: K_stored = -K_physical).
// Output layout in `out`:
@@ -3964,7 +4056,8 @@ void mjCFlex::ResolveReferences(const mjCModel* m) {
mjCBody* pbody = static_cast<mjCBody*>(m->FindObject(mjOBJ_BODY, vertbody));
if (pbody) {
vertbodyid.push_back(pbody->id);
if (pbody->joints.size() != 3 && dim == 2 && (elastic2d == 1 || elastic2d == 3)) {
if (pbody->joints.size() != 3 && dim == 2 &&
(elastic2d == 1 || elastic2d == 3) && !interpolated) {
// TODO(quaglino): add support for pins
throw mjCError(this, "pins are not supported for bending");
}
@@ -4105,8 +4198,8 @@ void mjCFlex::Compile(const mjVFS* vfs) {
if (thickness <= 0) {
throw mjCError(this, "2d elasticity requires positive thickness");
}
if (interpolated) {
throw mjCError(this, "interpolated flex does not yet support 2d elasticity");
if (interpolated && elastic2d != 2) {
mju_warning("bending passive force is not implemented for interpolated flex");
}
if (dim != 2 && !interpolated) {
throw mjCError(this, "2d elasticity requires 2d flex");
@@ -4141,6 +4234,11 @@ void mjCFlex::Compile(const mjVFS* vfs) {
if (spec.cellcount[0] == 0 || spec.cellcount[1] == 0 || spec.cellcount[2] == 0) {
throw mjCError(this, "cellcount cannot be 0 in any dimension when interpolation order > 0");
}
if (elastic2d && !(spec.cellcount[0] == 1 || spec.cellcount[1] == 1 || spec.cellcount[2] == 1)) {
throw mjCError(this,
"shell trilinear flex requires at least one dimension "
"with cell count equal to one (no interior nodes)");
}
int expected_nodes = (spec.cellcount[0] * spec.order + 1) *
(spec.cellcount[1] * spec.order + 1) *
(spec.cellcount[2] * spec.order + 1);
@@ -4354,7 +4452,7 @@ void mjCFlex::Compile(const mjVFS* vfs) {
}
// bending stiffness (2D only)
if (dim == 2 && (elastic2d == 1 || elastic2d == 3)) {
if (dim == 2 && (elastic2d == 1 || elastic2d == 3) && !interpolated) {
bending.assign(nedge*17, 0);
for (unsigned int e = 0; e < nedge; e++) {
@@ -4392,57 +4490,130 @@ void mjCFlex::Compile(const mjVFS* vfs) {
double K_young = has_strain_eq ? 1e1 : young;
double K_poisson = has_strain_eq ? 0.3 : poisson;
int npc = pow(spec.order + 1, 3); // nodes per cell
int ndof_cell = 3 * npc;
int cx = spec.cellcount[0], cy = spec.cellcount[1], cz = spec.cellcount[2];
int ncells = cx * cy * cz;
int ny_global = cy * spec.order + 1;
int nz_global = cz * spec.order + 1;
// total stiffness = ncells * ndof_cell^2
stiffness.resize(ncells * ndof_cell * ndof_cell, 0);
// determine element type: 2D boundary quads (shell) or 3D cells (volume)
bool shell_mode = elastic2d != 0;
int npe; // nodes per element
int nelem_fe; // total finite elements
// compute stiffness per cell
for (int ci = 0; ci < cx; ci++) {
for (int cj = 0; cj < cy; cj++) {
for (int ck = 0; ck < cz; ck++) {
int cell_idx = ci * cy * cz + cj * cz + ck;
if (shell_mode) {
npe = pow(spec.order + 1, 2); // (order+1)^2 for 2D quads
nelem_fe = 2*(cy*cz + cx*cz + cx*cy);
} else {
npe = pow(spec.order + 1, 3); // (order+1)^3 for 3D cells
nelem_fe = cx * cy * cz;
}
int ndof_elem = 3 * npe;
// skip stiffness computation for empty cells (no mesh content)
if (!cell_empty.empty() && cell_empty[cell_idx]) {
continue;
// total stiffness = nelem_fe * ndof_elem^2
stiffness.resize(nelem_fe * ndof_elem * ndof_elem, 0);
// face layout for shell mode:
// face 0: x=0 (cy*cz quads, normal=0, in-plane=(1,2))
// face 1: x=max (cy*cz quads, normal=0, in-plane=(1,2))
// face 2: y=0 (cx*cz quads, normal=1, in-plane=(0,2))
// face 3: y=max (cx*cz quads, normal=1, in-plane=(0,2))
// face 4: z=0 (cx*cy quads, normal=2, in-plane=(0,1))
// face 5: z=max (cx*cy quads, normal=2, in-plane=(0,1))
// face_sizes = {cy*cz, cy*cz, cx*cz, cx*cz, cx*cy, cx*cy}
int face_sizes[6] = {cy*cz, cy*cz, cx*cz, cx*cz, cx*cy, cx*cy};
int face_normal[6] = {0, 0, 1, 1, 2, 2};
// cell counts along each in-plane axis for each face
int face_count1[6] = {cz, cz, cx, cx, cy, cy}; // fast axis count
// fixed axis value (in grid node units, 0 or max)
int face_fixed[6] = {0, cx*spec.order, 0, cy*spec.order, 0, cz*spec.order};
// compute stiffness per element
for (int fe = 0; fe < nelem_fe; fe++) {
// gather element node positions
std::vector<double> elem_pos(3 * npe);
int normal_axis = -1;
if (shell_mode) {
// determine which face and quad within face
int face_id = 0, within_face = fe;
int cumul = 0;
for (int f = 0; f < 6; f++) {
if (fe < cumul + face_sizes[f]) {
face_id = f;
within_face = fe - cumul;
break;
}
cumul += face_sizes[f];
}
// gather cell's local node positions
std::vector<double> cell_pos(3 * npc);
int local = 0;
for (int li = 0; li <= spec.order; li++) {
for (int lj = 0; lj <= spec.order; lj++) {
for (int lk = 0; lk <= spec.order; lk++) {
int gi = ci * spec.order + li;
int gj = cj * spec.order + lj;
int gk = ck * spec.order + lk;
int global = gi * ny_global * nz_global + gj * nz_global + gk;
mjuu_copyvec(cell_pos.data() + 3*local, nodexpos_local.data() + 3*global, 3);
local++;
}
}
}
normal_axis = face_normal[face_id];
int na0 = (normal_axis + 1) % 3; // slow in-plane axis
int na1 = (normal_axis + 2) % 3; // fast in-plane axis
int c1 = face_count1[face_id]; // cell count along fast axis
int g_fixed = face_fixed[face_id]; // grid index along normal axis
int q0 = within_face / c1; // quad index along slow in-plane axis
int q1 = within_face % c1; // quad index along fast in-plane axis
// compute per-cell stiffness
std::vector<double> K_cell(ndof_cell * ndof_cell, 0);
ComputeLinearStiffness(K_cell, cell_pos.data(), K_young, K_poisson, spec.order);
double* out = stiffness.data() + cell_idx * ndof_cell * ndof_cell;
if (has_strain_eq) {
// eigendecompose: store [neig, sqrt(λ)*v_1, sqrt(λ)*v_2, ...]
std::fill(out, out + ndof_cell * ndof_cell, 0.0);
EigendecomposeStiffness(K_cell.data(), out, ndof_cell);
} else {
// store raw K for passive forces
std::copy(K_cell.begin(), K_cell.end(), out);
// gather 2D face element nodes
int local = 0;
for (int l0 = 0; l0 <= spec.order; l0++) {
for (int l1 = 0; l1 <= spec.order; l1++) {
// build global node index from 3 axis values
int g[3];
g[normal_axis] = g_fixed;
g[na0] = q0 * spec.order + l0;
g[na1] = q1 * spec.order + l1;
int global = g[0] * ny_global * nz_global + g[1] * nz_global + g[2];
mjuu_copyvec(elem_pos.data() + 3*local,
nodexpos_local.data() + 3*global, 3);
local++;
}
}
} else {
// 3D cell: convert flat index to (ci, cj, ck)
int ci = fe / (cy * cz);
int cj = (fe / cz) % cy;
int ck = fe % cz;
// skip stiffness computation for empty cells (no mesh content)
if (!cell_empty.empty() && cell_empty[fe]) {
continue;
}
// gather cell's local node positions
int local = 0;
for (int li = 0; li <= spec.order; li++) {
for (int lj = 0; lj <= spec.order; lj++) {
for (int lk = 0; lk <= spec.order; lk++) {
int gi = ci * spec.order + li;
int gj = cj * spec.order + lj;
int gk = ck * spec.order + lk;
int global = gi * ny_global * nz_global + gj * nz_global + gk;
mjuu_copyvec(elem_pos.data() + 3*local,
nodexpos_local.data() + 3*global, 3);
local++;
}
}
}
}
// compute per-element stiffness
std::vector<double> K_elem(ndof_elem * ndof_elem, 0);
if (shell_mode) {
ComputeLinearStiffness2D(K_elem, elem_pos.data(), K_young, K_poisson,
spec.order, thickness, normal_axis);
} else {
ComputeLinearStiffness(K_elem, elem_pos.data(), K_young, K_poisson,
spec.order);
}
double* out = stiffness.data() + fe * ndof_elem * ndof_elem;
if (has_strain_eq) {
// eigendecompose: store [neig, sqrt(λ)*v_1, sqrt(λ)*v_2, ...]
std::fill(out, out + ndof_elem * ndof_elem, 0.0);
EigendecomposeStiffness(K_elem.data(), out, ndof_elem);
} else {
// store raw K for passive forces
std::copy(K_elem.begin(), K_elem.end(), out);
}
}
}
+44 -16
View File
@@ -2191,12 +2191,20 @@ void mjCModel::SetSizes() {
nflexevpair += (int)flexes_[i]->evpair.size()/2;
nflextexcoord += (flexes_[i]->HasTexcoord() ? flexes_[i]->get_texcoord().size()/2 : 0);
if (flexes_[i]->spec.order != 0) {
int npc = (int)pow(flexes_[i]->spec.order + 1, 3);
int ndof_cell = 3 * npc;
int ncells = flexes_[i]->spec.cellcount[0] *
flexes_[i]->spec.cellcount[1] *
flexes_[i]->spec.cellcount[2];
extra_stiffness_size += ncells * ndof_cell * ndof_cell;
int cx = flexes_[i]->spec.cellcount[0];
int cy = flexes_[i]->spec.cellcount[1];
int cz = flexes_[i]->spec.cellcount[2];
bool shell = (flexes_[i]->elastic2d != 0);
int npe, nelem;
if (shell) {
npe = (int)pow(flexes_[i]->spec.order + 1, 2);
nelem = 2*(cy*cz + cx*cz + cx*cy);
} else {
npe = (int)pow(flexes_[i]->spec.order + 1, 3);
nelem = cx * cy * cz;
}
int ndof_elem = 3 * npe;
extra_stiffness_size += nelem * ndof_elem * ndof_elem;
}
if (flexes_[i]->interpolated || flexes_[i]->rigid) {
continue;
@@ -3476,10 +3484,20 @@ void mjCModel::CopyObjects(mjModel* m) {
m->flex_stiffnessadr[i] = 21 * elem_adr;
} else {
m->flex_stiffnessadr[i] = current_extra_stiffness_adr;
int npc = (int)pow(pfl->spec.order + 1, 3);
int ndof_cell = 3 * npc;
int ncells = pfl->spec.cellcount[0] * pfl->spec.cellcount[1] * pfl->spec.cellcount[2];
current_extra_stiffness_adr += ncells * ndof_cell * ndof_cell;
int pcx = pfl->spec.cellcount[0];
int pcy = pfl->spec.cellcount[1];
int pcz = pfl->spec.cellcount[2];
bool shell = (pfl->elastic2d != 0);
int npe, nelem;
if (shell) {
npe = (int)pow(pfl->spec.order + 1, 2);
nelem = 2*(pcy*pcz + pcx*pcz + pcx*pcy);
} else {
npe = (int)pow(pfl->spec.order + 1, 3);
nelem = pcx * pcy * pcz;
}
int ndof_elem = 3 * npe;
current_extra_stiffness_adr += nelem * ndof_elem * ndof_elem;
}
if (!pfl->stiffness.empty()) {
@@ -3490,10 +3508,20 @@ void mjCModel::CopyObjects(mjModel* m) {
if (pfl->spec.order == 0) {
stiff_size = 21 * pfl->nelem;
} else {
int npc = (int)pow(pfl->spec.order + 1, 3);
int ndof_cell = 3 * npc;
int ncells = pfl->spec.cellcount[0] * pfl->spec.cellcount[1] * pfl->spec.cellcount[2];
stiff_size = ncells * ndof_cell * ndof_cell;
int scx = pfl->spec.cellcount[0];
int scy = pfl->spec.cellcount[1];
int scz = pfl->spec.cellcount[2];
bool shell = (pfl->elastic2d != 0);
int npe, sncells;
if (shell) {
npe = (int)pow(pfl->spec.order + 1, 2);
sncells = 2*(scy*scz + scx*scz + scx*scy);
} else {
npe = (int)pow(pfl->spec.order + 1, 3);
sncells = scx * scy * scz;
}
int ndof_elem = 3 * npe;
stiff_size = sncells * ndof_elem * ndof_elem;
}
mjuu_zerovec(m->flex_stiffness + m->flex_stiffnessadr[i], stiff_size);
}
@@ -3629,8 +3657,8 @@ void mjCModel::CopyObjects(mjModel* m) {
memcpy(m->flex_nodebodyid + node_adr, pfl->nodebodyid.data(), pfl->nnode*sizeof(int));
}
// set interpolation type, only two types for now
m->flex_interp[i] = pfl->spec.order;
// set interpolation type: positive = volumetric, negative = shell mode
m->flex_interp[i] = pfl->spec.elastic2d ? -pfl->spec.order : pfl->spec.order;
// set cell count for multi-cell finite cell method
m->flex_cellnum[3*i+0] = pfl->spec.cellcount[0];