Files
Mujoco_WASM/src/user/user_flexcomp.cc
T
Alessio Quaglino 9c6a4f76eb 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
2026-04-29 10:17:21 -07:00

2241 lines
67 KiB
C++

// Copyright 2021 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <algorithm>
#include <array>
#include <climits>
#include <cmath>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <queue>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
#include <mujoco/mjmacro.h>
#include <mujoco/mjmodel.h>
#include <mujoco/mjtnum.h>
#include <mujoco/mjplugin.h>
#include "cc/array_safety.h"
#include "engine/engine_crossplatform.h"
#include "engine/engine_util_errmem.h"
#include "user/user_flexcomp.h"
#include <mujoco/mjspec.h>
#include "user/user_api.h"
#include "user/user_model.h"
#include "user/user_objects.h"
#include "user/user_resource.h"
#include "user/user_util.h"
namespace {
namespace mju = ::mujoco::util;
using std::vector;
using std::stringstream;
} // namespace
// strncpy with 0, return false
static bool comperr(char* error, const char* msg, int error_sz) {
mju_strncpy(error, msg, error_sz);
return false;
}
// Read data of type T from a potentially unaligned buffer pointer.
template <typename T>
static void ReadFromBuffer(T* dst, const char* src) {
std::memcpy(dst, src, sizeof(T));
}
static void ReadStrFromBuffer(char* dest, const char* src, int maxlen) {
std::strncpy(dest, src, maxlen);
}
bool IsValidElementOrNodeHeader22(const std::string& line) {
// making sure characters are numbers
for (char c : line) {
if (!std::isdigit(c)) {
return false;
}
}
return true;
}
// constructor: set defaults outside mjCDef
mjCFlexcomp::mjCFlexcomp(void) {
type = mjFCOMPTYPE_GRID;
count[0] = count[1] = count[2] = 10;
cellcount[0] = cellcount[1] = cellcount[2] = -1;
mjuu_setvec(spacing, 0.02, 0.02, 0.02);
mjuu_setvec(scale, 1, 1, 1);
mass = 1;
inertiabox = 0.005;
equality = 0;
mjuu_setvec(pos, 0, 0, 0);
mjuu_setvec(quat, 1, 0, 0, 0);
rigid = false;
centered = false;
doftype = mjFCOMPDOF_FULL;
mjs_defaultPlugin(&plugin);
mjs_defaultOrientation(&alt);
plugin_name = "";
plugin_instance_name = "";
plugin.plugin_name = (mjString*)&plugin_name;
plugin.name = (mjString*)&plugin_instance_name;
}
// identify empty cells and pin nodes exclusively in empty cells
void mjCFlexcomp::MarkEmptyCells(mjCFlex* flex, const double* points,
int npnt, const double minmax[6],
int nx, int ny, int nz) {
int cx = flex->spec.cellcount[0];
int cy = flex->spec.cellcount[1];
int cz = flex->spec.cellcount[2];
int ncells = cx * cy * cz;
int order = flex->spec.order;
// determine which cells contain mesh elements (not just vertices)
// for each element, compute its AABB and mark all overlapping cells
std::vector<bool> has_element(ncells, false);
double dx = minmax[3] - minmax[0];
double dy = minmax[4] - minmax[1];
double dz = minmax[5] - minmax[2];
// vertices per element: dim+1 (edges=2, triangles=3, tets=4)
int nvpe = flex->spec.dim + 1;
if (nvpe > 0 && !element.empty()) {
int nelem = element.size() / nvpe;
for (int e = 0; e < nelem; e++) {
// compute element AABB
double elo[3] = {1e30, 1e30, 1e30};
double ehi[3] = {-1e30, -1e30, -1e30};
for (int v = 0; v < nvpe; v++) {
int vid = element[nvpe * e + v];
for (int j = 0; j < 3; j++) {
elo[j] = std::min(elo[j], points[3 * vid + j]);
ehi[j] = std::max(ehi[j], points[3 * vid + j]);
}
}
// map element AABB to cell range
auto cellIdx = [](double coord, double lo, double d, int nc) {
if (d <= 0) return 0;
int c = (int)((coord - lo) / d * nc);
return std::max(0, std::min(nc - 1, c));
};
int ci0 = cellIdx(elo[0], minmax[0], dx, cx);
int ci1 = cellIdx(ehi[0], minmax[0], dx, cx);
int cj0 = cellIdx(elo[1], minmax[1], dy, cy);
int cj1 = cellIdx(ehi[1], minmax[1], dy, cy);
int ck0 = cellIdx(elo[2], minmax[2], dz, cz);
int ck1 = cellIdx(ehi[2], minmax[2], dz, cz);
// mark all overlapping cells as containing elements
for (int ci = ci0; ci <= ci1; ci++) {
for (int cj = cj0; cj <= cj1; cj++) {
for (int ck = ck0; ck <= ck1; ck++) {
has_element[ci * cy * cz + cj * cz + ck] = true;
}
}
}
}
}
// default: all cells non-empty (only exterior cells will be empty)
flex->cell_empty.assign(ncells, false);
// for dim=2 (surface mesh): check watertightness and flood-fill
if (flex->spec.dim == 2 && nvpe == 3 && !element.empty()) {
// flood-fill from grid boundary to find exterior cells
// cells reachable from the boundary through non-element cells
// are outside the mesh volume; cells NOT reachable are interior
std::vector<bool> visited(ncells, false);
std::queue<std::array<int, 3>> bfs;
// seed BFS from boundary cells that have no elements
for (int ci = 0; ci < cx; ci++) {
for (int cj = 0; cj < cy; cj++) {
for (int ck = 0; ck < cz; ck++) {
if (ci == 0 || ci == cx - 1 ||
cj == 0 || cj == cy - 1 ||
ck == 0 || ck == cz - 1) {
int idx = ci * cy * cz + cj * cz + ck;
if (!has_element[idx] && !visited[idx]) {
visited[idx] = true;
flex->cell_empty[idx] = true;
bfs.push({ci, cj, ck});
}
}
}
}
}
// BFS: spread through non-element cells
const int dirs[6][3] = {
{-1, 0, 0}, {1, 0, 0}, {0, -1, 0},
{0, 1, 0}, {0, 0, -1}, {0, 0, 1}};
while (!bfs.empty()) {
auto [ci, cj, ck] = bfs.front();
bfs.pop();
for (auto& d : dirs) {
int ni = ci + d[0], nj = cj + d[1], nk = ck + d[2];
if (ni < 0 || ni >= cx ||
nj < 0 || nj >= cy ||
nk < 0 || nk >= cz) {
continue;
}
int nidx = ni * cy * cz + nj * cz + nk;
if (!visited[nidx] && !has_element[nidx]) {
visited[nidx] = true;
flex->cell_empty[nidx] = true;
bfs.push({ni, nj, nk});
}
}
}
} else {
// dim!=2 (e.g., tet mesh): cells without element overlap are empty
for (int c = 0; c < ncells; c++) {
flex->cell_empty[c] = !has_element[c];
}
}
// pin nodes that belong exclusively to empty cells
for (int gi = 0; gi < nx; gi++) {
for (int gj = 0; gj < ny; gj++) {
for (int gk = 0; gk < nz; gk++) {
// find all cells that reference this node
bool all_empty = true;
int ci_min = std::max(0, gi == 0 ? 0 : (gi - 1) / order);
int ci_max = std::min(cx - 1, gi / order);
int cj_min = std::max(0, gj == 0 ? 0 : (gj - 1) / order);
int cj_max = std::min(cy - 1, gj / order);
int ck_min = std::max(0, gk == 0 ? 0 : (gk - 1) / order);
int ck_max = std::min(cz - 1, gk / order);
for (int ci = ci_min; ci <= ci_max && all_empty; ci++) {
for (int cj = cj_min; cj <= cj_max && all_empty; cj++) {
for (int ck = ck_min; ck <= ck_max && all_empty; ck++) {
if (!flex->cell_empty[ci * cy * cz + cj * cz + ck]) {
all_empty = false;
}
}
}
}
if (all_empty) {
int idx = gi * ny * nz + gj * nz + gk;
pinned[idx] = true;
}
}
}
}
}
// make flexcomp object
bool mjCFlexcomp::Make(mjsBody* body, char* error, int error_sz, const mjVFS* vfs) {
mjCModel* model = static_cast<mjCBody*>(body->element)->model;
mjsCompiler* compiler = static_cast<mjCBody*>(body->element)->compiler;
mjsFlex* dflex = def.spec.flex;
bool direct = (type == mjFCOMPTYPE_DIRECT ||
type == mjFCOMPTYPE_MESH ||
type == mjFCOMPTYPE_GMSH);
// check parent body name
if (mjs_getName(body->element)->empty()) {
return comperr(error, "Parent body must have name", error_sz);
}
// check dim
if (dflex->dim < 1 || dflex->dim > 3) {
return comperr(error, "Invalid dim, must be between 1 and 3", error_sz);
}
// check counts
for (int i=0; i < 3; i++) {
if (count[i] < 1 || ((doftype == mjFCOMPDOF_RADIAL && count[i] < 2) && dflex->dim == 3)) {
return comperr(error, "Count too small", error_sz);
}
}
// check spacing
double minspace = 2*dflex->radius + dflex->margin;
if (!direct) {
if (spacing[0] < minspace ||
spacing[1] < minspace ||
spacing[2] < minspace) {
return comperr(error, "Spacing must be larger than geometry size", error_sz);
}
}
// check scale
if (scale[0] < mjMINVAL || scale[1] < mjMINVAL || scale[2] < mjMINVAL) {
return comperr(error, "Scale must be larger than mjMINVAL", error_sz);
}
// check mass and inertia
if (mass < mjMINVAL || inertiabox < mjMINVAL) {
return comperr(error, "Mass and inertiabox must be larger than mjMINVAL", error_sz);
}
// compute orientation
const char* alterr = mjs_resolveOrientation(quat, compiler->degree, compiler->eulerseq, &alt);
if (alterr) {
return comperr(error, alterr, error_sz);
}
// type-specific constructor: populate point and element, possibly set dim
bool res;
switch (type) {
case mjFCOMPTYPE_GRID:
case mjFCOMPTYPE_CIRCLE:
res = MakeGrid(error, error_sz);
break;
case mjFCOMPTYPE_BOX:
case mjFCOMPTYPE_CYLINDER:
case mjFCOMPTYPE_ELLIPSOID:
res = MakeBox(error, error_sz, dflex->dim);
break;
case mjFCOMPTYPE_SQUARE:
case mjFCOMPTYPE_DISC:
res = MakeSquare(error, error_sz);
break;
case mjFCOMPTYPE_MESH:
res = MakeMesh(model, compiler, error, error_sz, vfs);
break;
case mjFCOMPTYPE_GMSH:
res = MakeGMSH(model, compiler, error, error_sz, vfs);
break;
case mjFCOMPTYPE_DIRECT:
res = true;
break;
default:
return comperr(error, "Unknown flexcomp type", error_sz);
}
if (!res) {
return false;
}
// force flatskin shading for box, cylinder and 3D grid
if (type == mjFCOMPTYPE_BOX || type == mjFCOMPTYPE_CYLINDER ||
(type == mjFCOMPTYPE_GRID && dflex->dim == 3)) {
dflex->flatskin = true;
}
// check pin sizes
if (pinrange.size()%2) {
return comperr(error, "Pin range number must be multiple of 2", error_sz);
}
if (pingrid.size()%dflex->dim) {
return comperr(error, "Pin grid number must be multiple of dim", error_sz);
}
if (pingridrange.size()%(2*dflex->dim)) {
return comperr(error, "Pin grid range number of must be multiple of 2*dim", error_sz);
}
if (type != mjFCOMPTYPE_GRID && !(pingrid.empty() && pingridrange.empty()) &&
doftype != mjFCOMPDOF_TRILINEAR && doftype != mjFCOMPDOF_QUADRATIC) {
return comperr(error, "Pin grid(range) can only be used with grid or interpolated", error_sz);
}
if (dflex->dim == 1 && !(pingrid.empty() && pingridrange.empty())) {
return comperr(error, "Pin grid(range) cannot be used with dim=1", error_sz);
}
// require element and point
if (point.empty() || element.empty()) {
return comperr(error, "Point and element required", error_sz);
}
// check point size
if (point.size()%3) {
return comperr(error, "Point size must be a multiple of 3", error_sz);
}
// check element size
if (element.size()%(dflex->dim+1)) {
return comperr(error, "Element size must be a multiple of dim+1", error_sz);
}
// get number of points
int npnt = point.size()/3;
// check elem vertex ids
for (int i=0; i < (int)element.size(); i++) {
if (element[i] < 0 || element[i] >= npnt) {
char msg[100];
snprintf(msg, sizeof(msg), "element %d has point id %d, number of points is %d", i,
element[i], npnt);
return comperr(error, msg, error_sz);
}
}
// apply scaling for direct types
if (direct && (scale[0] != 1 || scale[1] != 1 || scale[2] != 1)) {
for (int i=0; i < npnt; i++) {
point[3*i] *= scale[0];
point[3*i+1] *= scale[1];
point[3*i+2] *= scale[2];
}
}
// apply pose transform to points
for (int i=0; i < npnt; i++) {
double newp[3], oldp[3] = {point[3*i], point[3*i+1], point[3*i+2]};
mjuu_trnVecPose(newp, pos, quat, oldp);
point[3*i] = newp[0];
point[3*i+1] = newp[1];
point[3*i+2] = newp[2];
}
// compute bounding box of points
double minmax[6] = {mjMAXVAL, mjMAXVAL, mjMAXVAL, -mjMAXVAL, -mjMAXVAL, -mjMAXVAL};
for (int i=0; i < npnt; i++) {
for (int j=0; j < 3; j++) {
minmax[j+0] = std::min(minmax[j+0], point[3*i+j]);
minmax[j+3] = std::max(minmax[j+3], point[3*i+j]);
}
}
// construct pinned array
int nnode = 0;
if (doftype == mjFCOMPDOF_TRILINEAR || doftype == mjFCOMPDOF_QUADRATIC) {
int order = doftype == mjFCOMPDOF_TRILINEAR ? 1 : 2;
// multi-cell count for mesh/direct/gmsh, else single cell
int cx = 1, cy = 1, cz = 1;
if (type == mjFCOMPTYPE_MESH || type == mjFCOMPTYPE_DIRECT ||
type == mjFCOMPTYPE_GMSH) {
if (cellcount[0] >= 0) {
cx = cellcount[0];
cy = cellcount[1];
cz = cellcount[2];
}
}
nnode = (cx*order+1) * (cy*order+1) * (cz*order+1);
}
pinned = vector<bool>(std::max(npnt, nnode), rigid);
// handle pins if user did not specify rigid
if (!rigid) {
// process pinid
for (int i=0; i < (int)pinid.size(); i++) {
// check range
if (pinid[i] < 0 || pinid[i] >= npnt) {
return comperr(error, "pinid out of range", error_sz);
}
// set
pinned[pinid[i]] = true;
}
// process pinrange
for (int i=0; i < (int)pinrange.size(); i+=2) {
// check range
if (pinrange[i] < 0 || pinrange[i] >= npnt ||
pinrange[i+1] < 0 || pinrange[i+1] >= npnt) {
return comperr(error, "pinrange out of range", error_sz);
}
// set
for (int k=pinrange[i]; k <= pinrange[i+1]; k++) {
pinned[k] = true;
}
}
// process pingrid
for (int i=0; i < (int)pingrid.size(); i+=dflex->dim) {
// check range
int count_check[3] = {count[0], count[1], count[2]};
if (type != mjFCOMPTYPE_GRID && (doftype == mjFCOMPDOF_TRILINEAR ||
doftype == mjFCOMPDOF_QUADRATIC)) {
int dim = (doftype == mjFCOMPDOF_TRILINEAR) ? 2 : 3;
count_check[0] = count_check[1] = count_check[2] = dim;
}
for (int k=0; k < dflex->dim; k++) {
if (pingrid[i+k] < 0 || pingrid[i+k] >= count_check[k]) {
return comperr(error, "pingrid out of range", error_sz);
}
}
// set
if (dflex->dim == 2) {
pinned[GridID(pingrid[i], pingrid[i+1])] = true;
}
else if (dflex->dim == 3) {
pinned[GridID(pingrid[i], pingrid[i+1], pingrid[i+2])] = true;
}
}
// process pingridrange
for (int i=0; i < (int)pingridrange.size(); i+=2*dflex->dim) {
// check range
for (int k=0; k < 2*dflex->dim; k++) {
if (pingridrange[i+k] < 0 || pingridrange[i+k] >= count[k%dflex->dim]) {
return comperr(error, "pingridrange out of range", error_sz);
}
}
// set
if (dflex->dim == 2) {
for (int ix=pingridrange[i]; ix <= pingridrange[i+2]; ix++) {
for (int iy=pingridrange[i+1]; iy <= pingridrange[i+3]; iy++) {
pinned[GridID(ix, iy)] = true;
}
}
}
else if (dflex->dim == 3) {
for (int ix=pingridrange[i]; ix <= pingridrange[i+3]; ix++) {
for (int iy=pingridrange[i+1]; iy <= pingridrange[i+4]; iy++) {
for (int iz=pingridrange[i+2]; iz <= pingridrange[i+5]; iz++) {
pinned[GridID(ix, iy, iz)] = true;
}
}
}
}
}
// center of radial body is always pinned
if (doftype == mjFCOMPDOF_RADIAL) {
pinned[0] = true;
}
// check if all or none are pinned
bool allpin = true, nopin = true;
for (int i=0; i < npnt; i++) {
if (pinned[i]) {
nopin = false;
}
else {
allpin = false;
}
}
// adjust rigid and centered
if (allpin) {
rigid = true;
}
else if (nopin) {
centered = true;
}
}
// remove unreferenced for direct, mesh, gmsh
if (direct) {
// find used
used = std::vector<bool> (npnt, false);
for (int i=0; i < (int)element.size(); i++) {
used[element[i]] = true;
}
// construct reindex
bool hasunused = false;
std::vector<int> reindex (npnt, 0);
for (int i=0; i < npnt; i++) {
if (!used[i]) {
hasunused = true;
for (int k=i+1; k < npnt; k++) {
reindex[k]--;
}
}
}
// reindex elements if unused present
if (hasunused) {
for (int i=0; i < (int)element.size(); i++) {
element[i] += reindex[element[i]];
}
// compact point, texcoord, pinned arrays
int new_npnt = 0;
for (int i=0; i < npnt; i++) {
if (used[i]) {
point[3*new_npnt+0] = point[3*i+0];
point[3*new_npnt+1] = point[3*i+1];
point[3*new_npnt+2] = point[3*i+2];
if (!texcoord.empty()) {
texcoord[2*new_npnt+0] = texcoord[2*i+0];
texcoord[2*new_npnt+1] = texcoord[2*i+1];
}
pinned[new_npnt] = pinned[i];
new_npnt++;
}
}
// resize arrays
point.resize(3*new_npnt);
if (!texcoord.empty()) {
texcoord.resize(2*new_npnt);
}
pinned.resize(std::max(new_npnt, nnode));
used.assign(new_npnt, true);
// update count
npnt = new_npnt;
}
}
// nothing to remove for auto-generated types
else {
used = std::vector<bool> (npnt, true);
}
// create flex, copy parameters
mjCFlex* flex = model->AddFlex();
mjsFlex* pf = &flex->spec;
int id = flex->id;
*flex = def.Flex();
flex->PointToLocal();
flex->model = model;
flex->id = id;
mjs_setName(pf->element, name.c_str());
mjs_setInt(pf->elem, element.data(), element.size());
mjs_setFloat(pf->texcoord, texcoord.data(), texcoord.size());
mjs_setInt(pf->elemtexcoord, elemtexcoord.data(), elemtexcoord.size());
if (!centered) {
mjs_setDouble(pf->vert, point.data(), point.size());
}
// rigid: set parent name, nothing else to do
if (rigid) {
mjs_appendString(pf->vertbody, mjs_getName(body->element)->c_str());
return true;
}
// compute body mass and inertia matching specs
double bodymass = mass/npnt;
double bodyinertia = bodymass*(2.0*inertiabox*inertiabox)/3.0;
// overwrite plugin name
if (plugin.active && plugin_instance_name.empty()) {
plugin_instance_name = "flexcomp_" + name;
static_cast<mjCPlugin*>(plugin.element)->name = plugin_instance_name;
}
// create bodies, construct flex vert and vertbody
for (int i=0; i < npnt; i++) {
// not used: skip
if (!used[i]) {
continue;
}
// pinned or trilinear or quadratic: parent body
if (pinned[i] || doftype == mjFCOMPDOF_TRILINEAR || doftype == mjFCOMPDOF_QUADRATIC) {
mjs_appendString(pf->vertbody, mjs_getName(body->element)->c_str());
// add plugin
if (plugin.active) {
mjsPlugin* pplugin = &body->plugin;
pplugin->active = true;
pplugin->element = static_cast<mjsElement*>(plugin.element);
mjs_setString(pplugin->plugin_name, mjs_getString(plugin.plugin_name));
mjs_setString(pplugin->name, plugin_instance_name.c_str());
}
}
// not pinned and not trilinear: new body
else {
// add new body at vertex coordinates
mjsBody* pb = mjs_addBody(body, 0);
// set frame and inertial
pb->pos[0] = point[3*i];
pb->pos[1] = point[3*i+1];
pb->pos[2] = point[3*i+2];
mjuu_zerovec(pb->ipos, 3);
pb->mass = bodymass;
pb->inertia[0] = bodyinertia;
pb->inertia[1] = bodyinertia;
pb->inertia[2] = bodyinertia;
pb->explicitinertial = true;
// add radial slider
if (doftype == mjFCOMPDOF_RADIAL) {
mjsJoint* jnt = mjs_addJoint(pb, 0);
// set properties
jnt->type = mjJNT_SLIDE;
mjuu_setvec(jnt->pos, 0, 0, 0);
mjuu_copyvec(jnt->axis, pb->pos, 3);
mjuu_normvec(jnt->axis, 3);
}
// add three orthogonal sliders
else if (doftype == mjFCOMPDOF_FULL) {
for (int j=0; j < 3; j++) {
// add joint to body
mjsJoint* jnt = mjs_addJoint(pb, 0);
// set properties
jnt->type = mjJNT_SLIDE;
mjuu_setvec(jnt->pos, 0, 0, 0);
mjuu_setvec(jnt->axis, 0, 0, 0);
jnt->axis[j] = 1;
}
}
// add two orthogonal sliders (x and y only)
else if (doftype == mjFCOMPDOF_2D) {
for (int j=0; j < 2; j++) {
mjsJoint* jnt = mjs_addJoint(pb, 0);
jnt->type = mjJNT_SLIDE;
mjuu_setvec(jnt->pos, 0, 0, 0);
mjuu_setvec(jnt->axis, 0, 0, 0);
jnt->axis[j] = 1;
}
}
// construct body name, add to vertbody
char txt[100];
mju::sprintf_arr(txt, "%s_%d", name.c_str(), i);
mjs_setName(pb->element, txt);
mjs_appendString(pf->vertbody, mjs_getName(pb->element)->c_str());
// clear flex vertex coordinates if allocated
if (!centered) {
point[3*i] = 0;
point[3*i+1] = 0;
point[3*i+2] = 0;
}
// add plugin
if (plugin.active) {
mjsPlugin* pplugin = &pb->plugin;
pplugin->active = true;
pplugin->element = static_cast<mjsElement*>(plugin.element);
mjs_setString(pplugin->plugin_name, mjs_getString(plugin.plugin_name));
mjs_setString(pplugin->name, plugin_instance_name.c_str());
}
}
}
// create nodal mesh for trilinear/quadratic interpolation
if (doftype == mjFCOMPDOF_TRILINEAR || doftype == mjFCOMPDOF_QUADRATIC) {
flex->spec.order = doftype == mjFCOMPDOF_TRILINEAR ? 1 : 2;
if (cellcount[0] >= 0) {
flex->spec.cellcount[0] = cellcount[0];
flex->spec.cellcount[1] = cellcount[1];
flex->spec.cellcount[2] = cellcount[2];
}
// total number of nodes with shared boundaries
int nx = flex->spec.cellcount[0] * flex->spec.order + 1;
int ny = flex->spec.cellcount[1] * flex->spec.order + 1;
int nz = flex->spec.cellcount[2] * flex->spec.order + 1;
int nnode = nx * ny * nz;
// mark empty cells and pin nodes exclusively in empty cells
MarkEmptyCells(flex, point.data(), npnt, minmax, nx, ny, nz);
// if MarkEmptyCells pinned any nodes, force centered=false
// so that pf->node (local positions) is saved to the model
if (centered) {
for (int i = 0; i < nnode; i++) {
if (pinned[i]) {
centered = false;
break;
}
}
}
std::vector<double> node(3 * nnode, 0);
int idx = 0;
// Simpson's rule weights for quadratic mass distribution
double massP2[3] = {1. / 6., 2. / 3., 1. / 6.};
// collect created bodies for mass normalization
std::vector<mjsBody*> node_bodies;
for (int gi = 0; gi < nx; gi++) {
for (int gj = 0; gj < ny; gj++) {
for (int gk = 0; gk < nz; gk++) {
// parametric position in [0, 1]^3
double s = (double)gi / (flex->spec.cellcount[0] * flex->spec.order);
double t = (double)gj / (flex->spec.cellcount[1] * flex->spec.order);
double u = (double)gk / (flex->spec.cellcount[2] * flex->spec.order);
// physical position
double px = minmax[0] + s * (minmax[3] - minmax[0]);
double py = minmax[1] + t * (minmax[4] - minmax[1]);
double pz = minmax[2] + u * (minmax[5] - minmax[2]);
if (pinned[idx]) {
node[3*idx+0] = px;
node[3*idx+1] = py;
node[3*idx+2] = pz;
mjs_appendString(pf->nodebody,
mjs_getName(body->element)->c_str());
idx++;
continue;
}
mjsBody* pb = mjs_addBody(body, 0);
pb->pos[0] = px;
pb->pos[1] = py;
pb->pos[2] = pz;
mjuu_zerovec(pb->ipos, 3);
// mass distribution
if (doftype == mjFCOMPDOF_TRILINEAR) {
pb->mass = 1.0;
} else {
// local index within the cell for mass computation
int li = gi % flex->spec.order;
int lj = gj % flex->spec.order;
int lk = gk % flex->spec.order;
// boundary nodes: average mass contribution
int ncells_i = (gi > 0 && gi < nx-1 && li == 0) ? 2 : 1;
int ncells_j = (gj > 0 && gj < ny-1 && lj == 0) ? 2 : 1;
int ncells_k = (gk > 0 && gk < nz-1 && lk == 0) ? 2 : 1;
// use Simpson weights
double wi = massP2[li == 0 ? 0 : li];
double wj = massP2[lj == 0 ? 0 : lj];
double wk = massP2[lk == 0 ? 0 : lk];
pb->mass = wi * wj * wk * ncells_i * ncells_j * ncells_k;
}
node_bodies.push_back(pb);
pb->inertia[0] = pb->mass*(2.0*inertiabox*inertiabox)/3.0;
pb->inertia[1] = pb->mass*(2.0*inertiabox*inertiabox)/3.0;
pb->inertia[2] = pb->mass*(2.0*inertiabox*inertiabox)/3.0;
pb->explicitinertial = true;
for (int d=0; d < 3; d++) {
mjsJoint* jnt = mjs_addJoint(pb, 0);
jnt->type = mjJNT_SLIDE;
mjuu_setvec(jnt->pos, 0, 0, 0);
mjuu_setvec(jnt->axis, 0, 0, 0);
jnt->axis[d] = 1;
}
// construct node name, add to nodebody
char txt[100];
mju::sprintf_arr(txt, "%s_%d_%d_%d", name.c_str(), gi, gj, gk);
mjs_setName(pb->element, txt);
mjs_appendString(pf->nodebody, mjs_getName(pb->element)->c_str());
idx++;
}
}
}
// normalize masses so total equals prescribed mass
double total_mass = 0;
for (mjsBody* pb : node_bodies) {
total_mass += pb->mass;
}
if (total_mass > 0) {
double scale = mass / total_mass;
for (mjsBody* pb : node_bodies) {
pb->mass *= scale;
pb->inertia[0] *= scale;
pb->inertia[1] *= scale;
pb->inertia[2] *= scale;
}
}
if (!centered) {
mjs_setDouble(pf->node, node.data(), node.size());
}
}
if (!centered || doftype == mjFCOMPDOF_TRILINEAR || doftype == mjFCOMPDOF_QUADRATIC) {
mjs_setDouble(pf->vert, point.data(), point.size());
}
// create equality constraints
if (equality) {
// equality 1=edge(mjEQ_FLEX), 2=vert(mjEQ_FLEXVERT), 3=strain(mjEQ_FLEXSTRAIN)
if (equality == 1 || equality == 2) {
mjsEquality* pe = mjs_addEquality(&model->spec, &def.spec);
mjs_setDefault(pe->element, &model->Default()->spec);
pe->type = (equality == 1) ? mjEQ_FLEX : mjEQ_FLEXVERT;
pe->active = true;
mjs_setString(pe->name1, name.c_str());
} else if (equality == 3) {
// 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];
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;
}
}
}
}
}
}
return true;
}
// get point id from grid coordinates
int mjCFlexcomp::GridID(int ix, int iy) {
return ix*count[1] + iy;
}
int mjCFlexcomp::GridID(int ix, int iy, int iz) {
return ix*count[1]*count[2] + iy*count[2] + iz;
}
// make grid
bool mjCFlexcomp::MakeGrid(char* error, int error_sz) {
int dim = def.Flex().spec.dim;
bool needtex = texcoord.empty() && mjs_getString(def.spec.flex->material)[0];
// 1D
if (dim == 1) {
for (int ix=0; ix < count[0]; ix++) {
if (type == mjFCOMPTYPE_CIRCLE) {
if (ix >= count[0]-1) {
continue;
}
// add point
double theta = 2*mjPI/(count[0]-1);
double radius = spacing[0]/std::sin(theta/2)/2;
point.push_back(radius*std::cos(theta*ix));
point.push_back(radius*std::sin(theta*ix));
point.push_back(0);
// add element
element.push_back(ix);
element.push_back(ix == count[0]-2 ? 0 : ix+1);
} else {
// add point
point.push_back(spacing[0]*(ix - 0.5*(count[0]-1)));
point.push_back(0);
point.push_back(0);
// add element
if (ix < count[0]-1) {
element.push_back(ix);
element.push_back(ix+1);
}
}
}
}
// 2D
else if (dim == 2) {
for (int ix=0; ix < count[0]; ix++) {
for (int iy=0; iy < count[1]; iy++) {
int quad2tri[2][3] = {{0, 1, 2}, {0, 2, 3}};
// add point
double pos[2] = {spacing[0]*(ix - 0.5*(count[0]-1)),
spacing[1]*(iy - 0.5*(count[1]-1))};
point.push_back(pos[0]);
point.push_back(pos[1]);
point.push_back(0);
// add texture coordinates, if not specified explicitly
if (needtex) {
texcoord.push_back(ix/(double)std::max(count[0]-1, 1));
texcoord.push_back(iy/(double)std::max(count[1]-1, 1));
}
// flip triangles if radial projection is requested
if (((pos[0] < -mjEPS && pos[1] > -mjEPS) ||
(pos[0] > -mjEPS && pos[1] < -mjEPS)) &&
type == mjFCOMPTYPE_DISC) {
quad2tri[0][2] = 3;
quad2tri[1][0] = 1;
}
// add elements
if (ix < count[0]-1 && iy < count[1]-1) {
int vert[4] = {
count[2]*count[1]*(ix+0) + count[2]*(iy+0),
count[2]*count[1]*(ix+1) + count[2]*(iy+0),
count[2]*count[1]*(ix+1) + count[2]*(iy+1),
count[2]*count[1]*(ix+0) + count[2]*(iy+1),
};
for (int s =0; s < 2; s++) {
for (int v=0; v < 3; v++) {
element.push_back(vert[quad2tri[s][v]]);
}
}
}
}
}
}
// 3D
else {
int cube2tets[6][4] = {{0, 3, 1, 7}, {0, 1, 4, 7},
{1, 3, 2, 7}, {1, 2, 6, 7},
{1, 5, 4, 7}, {1, 6, 5, 7}};
for (int ix=0; ix < count[0]; ix++) {
for (int iy=0; iy < count[1]; iy++) {
for (int iz=0; iz < count[2]; iz++) {
// add point
point.push_back(spacing[0]*(ix - 0.5*(count[0]-1)));
point.push_back(spacing[1]*(iy - 0.5*(count[1]-1)));
point.push_back(spacing[2]*(iz - 0.5*(count[2]-1)));
// add texture coordinates, if not specified explicitly
if (needtex) {
texcoord.push_back(ix/(float)std::max(count[0]-1, 1));
texcoord.push_back(iy/(float)std::max(count[1]-1, 1));
}
// add elements
if (ix < count[0]-1 && iy < count[1]-1 && iz < count[2]-1) {
int vert[8] = {
count[2]*count[1]*(ix+0) + count[2]*(iy+0) + iz+0,
count[2]*count[1]*(ix+1) + count[2]*(iy+0) + iz+0,
count[2]*count[1]*(ix+1) + count[2]*(iy+1) + iz+0,
count[2]*count[1]*(ix+0) + count[2]*(iy+1) + iz+0,
count[2]*count[1]*(ix+0) + count[2]*(iy+0) + iz+1,
count[2]*count[1]*(ix+1) + count[2]*(iy+0) + iz+1,
count[2]*count[1]*(ix+1) + count[2]*(iy+1) + iz+1,
count[2]*count[1]*(ix+0) + count[2]*(iy+1) + iz+1,
};
for (int s=0; s < 6; s++) {
for (int v=0; v < 4; v++) {
element.push_back(vert[cube2tets[s][v]]);
}
}
}
}
}
}
}
// check elements
if (element.empty()) {
return comperr(error, "No elements were created in grid", error_sz);
}
return true;
}
// get point id from box coordinates and side
int mjCFlexcomp::BoxID(int ix, int iy, int iz) {
// side iz=0
if (iz == 0) {
return ix*count[1] + iy + 1;
}
// side iz=max
else if (iz == count[2]-1) {
return count[0]*count[1] + ix*count[1] + iy + 1;
}
// side iy=0
else if (iy == 0) {
return 2*count[0]*count[1] + ix*(count[2]-2) + iz - 1 + 1;
}
// side iy=max
else if (iy == count[1]-1) {
return 2*count[0]*count[1] + count[0]*(count[2]-2) + ix*(count[2]-2) + iz - 1 + 1;
}
// side ix=0
else if (ix == 0) {
return 2*count[0]*count[1] + 2*count[0]*(count[2]-2) + (iy-1)*(count[2]-2) + iz - 1 + 1;
}
// side ix=max
else {
return 2*count[0]*count[1] + 2*count[0]*(count[2]-2) + (count[1]-2)*(count[2]-2) +
(iy-1)*(count[2]-2) + iz - 1 + 1;
}
}
// project from box to other shape
void mjCFlexcomp::BoxProject(double* pos, int ix, int iy, int iz) {
// init point
pos[0] = 2.0*ix/(count[0]-1) - 1;
pos[1] = 2.0*iy/(count[1]-1) - 1;
pos[2] = 2.0*iz/(count[2]-1) - 1;
// determine sizes
double size[3] = {
0.5*spacing[0]*(count[0]-1),
0.5*spacing[1]*(count[1]-1),
0.5*spacing[2]*(count[2]-1)
};
// box
if (type == mjFCOMPTYPE_BOX) {
pos[0] *= size[0];
pos[1] *= size[1];
pos[2] *= size[2];
}
// cylinder
else if (type == mjFCOMPTYPE_CYLINDER) {
double L0 = std::max(std::abs(pos[0]), std::abs(pos[1]));
mjuu_normvec(pos, 2);
pos[0] *= size[0]*L0;
pos[1] *= size[1]*L0;
pos[2] *= size[2];
}
// ellipsoid
else if (type == mjFCOMPTYPE_ELLIPSOID) {
mjuu_normvec(pos, 3);
pos[0] *= size[0];
pos[1] *= size[1];
pos[2] *= size[2];
}
}
// make 2d square or disc
bool mjCFlexcomp::MakeSquare(char* error, int error_sz) {
// set 2D
def.spec.flex->dim = 2;
// create square
if (!MakeGrid(error, error_sz)) {
return false;
}
// do projection
if (type == mjFCOMPTYPE_DISC) {
double size[2] = {
0.5*spacing[0]*(count[0]-1),
0.5*spacing[1]*(count[1]-1),
};
for (int i=0; i < point.size()/3; i++) {
double* pos = point.data() + i*3;
double L0 = std::max(std::abs(pos[0]), std::abs(pos[1]));
mjuu_normvec(pos, 2);
pos[0] *= size[0]*L0;
pos[1] *= size[1]*L0;
}
}
return true;
}
static int mat2lin(int ix, int iy, int iz, const int count[3]) {
return ix*count[1]*count[2] + iy*count[2] + iz;
}
// make 3d box, ellipsoid or cylinder
bool mjCFlexcomp::MakeBox(char* error, int error_sz, int dim, bool open) {
double pos[3];
bool needtex = texcoord.empty() && mjs_getString(def.spec.flex->material)[0];
// set dimension
def.spec.flex->dim = dim;
// add center point
if (dim == 3) {
point.push_back(0);
point.push_back(0);
point.push_back(0);
}
// add texture coordinates, if not specified explicitly
if (needtex) {
texcoord.push_back(0);
texcoord.push_back(0);
}
// add points
int n = 0;
std::vector<int> idx(count[0]*count[1]*count[2]);
// iz=0/max
for (int iz=0; iz < count[2]; iz+=count[2]-1) {
for (int ix=0; ix < count[0]; ix++) {
for (int iy=0; iy < count[1]; iy++) {
if (open && dim == 2 && iz != 0) {
continue;
}
// add point
BoxProject(pos, ix, iy, iz);
point.push_back(pos[0]);
point.push_back(pos[1]);
point.push_back(pos[2]);
idx[mat2lin(ix, iy, iz, count)] = n++;
// add texture coordinates, if not specified explicitly
if (needtex) {
texcoord.push_back(ix/(float)std::max(count[0]-1, 1));
texcoord.push_back(iy/(float)std::max(count[1]-1, 1));
}
}
}
}
// iy=0/max
for (int iy=0; iy < count[1]; iy+=count[1]-1) {
for (int ix=0; ix < count[0]; ix++) {
for (int iz=0; iz < count[2]; iz++) {
// add point
if (iz > 0 && ((open && dim == 2) || (iz < count[2]-1))) {
BoxProject(pos, ix, iy, iz);
point.push_back(pos[0]);
point.push_back(pos[1]);
point.push_back(pos[2]);
idx[mat2lin(ix, iy, iz, count)] = n++;
// add texture coordinates
if (needtex) {
texcoord.push_back(ix/(float)std::max(count[0]-1, 1));
texcoord.push_back(iz/(float)std::max(count[2]-1, 1));
}
}
}
}
}
// ix=0/max
for (int ix=0; ix < count[0]; ix+=count[0]-1) {
for (int iy=0; iy < count[1]; iy++) {
for (int iz=0; iz < count[2]; iz++) {
// add point
if (iz > 0 && ((open && dim == 2) || (iz < count[2]-1)) && iy > 0 && iy < count[1]-1) {
BoxProject(pos, ix, iy, iz);
point.push_back(pos[0]);
point.push_back(pos[1]);
point.push_back(pos[2]);
idx[mat2lin(ix, iy, iz, count)] = n++;
// add texture coordinates
if (needtex) {
texcoord.push_back(iy/(float)std::max(count[1]-1, 1));
texcoord.push_back(iz/(float)std::max(count[2]-1, 1));
}
}
}
}
}
// add elements
// iz=0/max
for (int iz=0; iz < count[2]; iz+=count[2]-1) {
for (int ix=0; ix < count[0]; ix++) {
for (int iy=0; iy < count[1]; iy++) {
if (open && dim == 2 && iz != 0) {
continue;
}
if (ix < count[0]-1 && iy < count[1]-1) {
if (dim==3) {
element.push_back(0);
element.push_back(BoxID(ix, iy, iz));
element.push_back(BoxID(ix+1, iy, iz));
element.push_back(BoxID(ix+1, iy+1, iz));
element.push_back(0);
element.push_back(BoxID(ix, iy, iz));
element.push_back(BoxID(ix, iy+1, iz));
element.push_back(BoxID(ix+1, iy+1, iz));
} else {
int step1 = iz == 0 ? 1 : 0;
int step2 = iz == 0 ? 0 : 1;
element.push_back(idx[mat2lin(ix, iy, iz, count)]);
element.push_back(idx[mat2lin(ix+1, iy+step1, iz, count)]);
element.push_back(idx[mat2lin(ix+1, iy+step2, iz, count)]);
element.push_back(idx[mat2lin(ix, iy, iz, count)]);
element.push_back(idx[mat2lin(ix+step2, iy+1, iz, count)]);
element.push_back(idx[mat2lin(ix+step1, iy+1, iz, count)]);
}
}
}
}
}
// iy=0/max
for (int iy=0; iy < count[1]; iy+=count[1]-1) {
for (int ix=0; ix < count[0]; ix++) {
for (int iz=0; iz < count[2]; iz++) {
if (ix < count[0]-1 && iz < count[2]-1) {
if (dim==3) {
element.push_back(0);
element.push_back(BoxID(ix, iy, iz));
element.push_back(BoxID(ix+1, iy, iz));
element.push_back(BoxID(ix+1, iy, iz+1));
element.push_back(0);
element.push_back(BoxID(ix, iy, iz));
element.push_back(BoxID(ix, iy, iz+1));
element.push_back(BoxID(ix+1, iy, iz+1));
} else {
int ix0 = iy == 0 ? ix : ix+1;
int dx = iy == 0 ? 1 : -1;
element.push_back(idx[mat2lin(ix0, iy, iz, count)]);
element.push_back(idx[mat2lin(ix0+dx, iy, iz, count)]);
element.push_back(idx[mat2lin(ix0+dx, iy, iz+1, count)]);
element.push_back(idx[mat2lin(ix0, iy, iz, count)]);
element.push_back(idx[mat2lin(ix0+dx, iy, iz+1, count)]);
element.push_back(idx[mat2lin(ix0, iy, iz+1, count)]);
}
}
}
}
}
// ix=0/max
for (int ix=0; ix < count[0]; ix+=count[0]-1) {
for (int iy=0; iy < count[1]; iy++) {
for (int iz=0; iz < count[2]; iz++) {
if (iy < count[1]-1 && iz < count[2]-1) {
if (dim==3) {
element.push_back(0);
element.push_back(BoxID(ix, iy, iz));
element.push_back(BoxID(ix, iy+1, iz));
element.push_back(BoxID(ix, iy+1, iz+1));
element.push_back(0);
element.push_back(BoxID(ix, iy, iz));
element.push_back(BoxID(ix, iy, iz+1));
element.push_back(BoxID(ix, iy+1, iz+1));
} else {
int iy0 = ix != 0 ? iy : iy+1;
int dy = ix != 0 ? 1 : -1;
element.push_back(idx[mat2lin(ix, iy0, iz, count)]);
element.push_back(idx[mat2lin(ix, iy0+dy, iz, count)]);
element.push_back(idx[mat2lin(ix, iy0+dy, iz+1, count)]);
element.push_back(idx[mat2lin(ix, iy0, iz, count)]);
element.push_back(idx[mat2lin(ix, iy0+dy, iz+1, count)]);
element.push_back(idx[mat2lin(ix, iy0, iz+1, count)]);
}
}
}
}
}
return true;
}
// copied from user_mesh.cc
template <typename T> static T* VecToArray(std::vector<T>& vector, bool clear = true){
if (vector.empty())
return nullptr;
else {
int n = (int)vector.size();
T* cvec = (T*) mju_malloc(n*sizeof(T));
memcpy(cvec, vector.data(), n*sizeof(T));
if (clear) {
vector.clear();
}
return cvec;
}
}
// make mesh
bool mjCFlexcomp::MakeMesh(mjCModel* model, mjsCompiler* compiler, char* error, int error_sz, const mjVFS* vfs) {
// strip path
if (!file.empty() && model->spec.strippath) {
file = mjuu_strippath(file);
}
// file is required
if (file.empty()) {
return comperr(error, "File is required", error_sz);
}
// check dim
if (def.spec.flex->dim < 2) {
return comperr(error, "Flex dim must be at least 2 for mesh", error_sz);
}
// load resource
std::string filename = mjuu_combinePaths(mjs_getString(compiler->meshdir), file);
mjResource* resource = nullptr;
if (mjCMesh::IsMSH(filename)) {
return comperr(error, "legacy MSH files are not supported in flexcomp", error_sz);
}
try {
resource = mjCBase::LoadResource(mjs_getString(model->spec.modelfiledir),
filename, vfs);
} catch (mjCError err) {
return comperr(error, err.message, error_sz);
}
// load mesh
mjCMesh mesh;
try {
mesh.LoadFromResource(resource, true);
mju_closeResource(resource);
} catch (mjCError err) {
mju_closeResource(resource);
return comperr(error, err.message, error_sz);
}
// check sizes
if (mesh.Vert().empty() || mesh.Face().empty()) {
return comperr(error, "Vertex and face data required", error_sz);
}
// copy vertices
point.assign(mesh.Vert().begin(), mesh.Vert().end());
if (mesh.HasTexcoord()) {
texcoord = mesh.Texcoord();
elemtexcoord = mesh.FaceTexcoord();
}
// copy faces or create 3D mesh
if (def.spec.flex->dim == 2) {
element = mesh.Face();
} else {
point.insert(point.begin() + 0, origin[0]);
point.insert(point.begin() + 1, origin[1]);
point.insert(point.begin() + 2, origin[2]);
for (int i=0; i < mesh.Face().size(); i+=3) {
// only add tetrahedra with positive volume
int tet[3] = {mesh.Face()[i+0]+1,
mesh.Face()[i+1]+1,
mesh.Face()[i+2]+1};
double edge1[3], edge2[3], edge3[3];
for (int i=0; i < 3; i++) {
edge1[i] = point[3*tet[0]+i] - origin[i];
edge2[i] = point[3*tet[1]+i] - origin[i];
edge3[i] = point[3*tet[2]+i] - origin[i];
}
double normal[3];
mjuu_crossvec(normal, edge1, edge2);
if (mjuu_dot3(normal, edge3) < mjMINVAL) {
continue;
}
element.push_back(0);
element.push_back(tet[0]);
element.push_back(tet[1]);
element.push_back(tet[2]);
}
}
return true;
}
// find string in buffer, return position or -1 if not found
static int findstring(const char* buffer, int buffer_sz, const char* str) {
int len = (int)strlen(str);
// scan buffer
for (int i=0; i < buffer_sz-len; i++) {
// check for string at position i
bool found = true;
for (int k=0; k < len; k++) {
if (buffer[i+k] != str[k]) {
found = false;
break;
}
}
// string found
if (found) {
return i;
}
}
// not found
return -1;
}
// load points and elements from GMSH file
bool mjCFlexcomp::MakeGMSH(mjCModel* model, mjsCompiler* compiler, char* error, int error_sz, const mjVFS* vfs) {
// strip path
if (!file.empty() && model->spec.strippath) {
file = mjuu_strippath(file);
}
// file is required
if (file.empty()) {
return comperr(error, "File is required", error_sz);
}
// open resource
mjResource* resource = nullptr;
try {
std::string filename = mjuu_combinePaths(mjs_getString(compiler->meshdir), file);
resource = mjCBase::LoadResource(mjs_getString(model->spec.modelfiledir),
filename, vfs);
} catch (mjCError err) {
return comperr(error, err.message, error_sz);
}
// try to load, close resource properly
try {
LoadGMSH(model, resource);
mju_closeResource(resource);
} catch (mjCError err) {
mju_closeResource(resource);
return comperr(error, err.message, error_sz);
} catch (...) {
mju_closeResource(resource);
return comperr(error, "exception while reading GMSH file", error_sz);
}
return true;
}
// load GMSH format 4.1
void mjCFlexcomp::LoadGMSH41(char* buffer, int binary, int nodeend,
int nodebegin, int elemend, int elembegin){
// header size
constexpr int kGmsh41HeaderSize = 52;
// base for node tags, to be subtracted from element data
size_t minNodeTag, numEntityBlocks, numNodes, maxNodeTag, numNodesInBlock, tag;
int entityDim, entityTag, parametric;
// ascii nodes
if (binary == 0) {
// convert node char buffer to stringstream
stringstream ss(std::string(buffer + nodebegin, nodeend - nodebegin));
// read header
ss >> numEntityBlocks >> numNodes >> minNodeTag >> maxNodeTag;
ss >> entityDim >> entityTag >> parametric >> numNodesInBlock;
if (!ss.good()) {
throw mjCError(NULL, "Error reading Nodes header");
}
// check number of nodes is a positive number
if (numNodes < 0) {
throw mjCError(NULL, "Invalid number of nodes");
}
// require single block
if (numEntityBlocks != 1 || numNodes != numNodesInBlock) {
throw mjCError(NULL, "All nodes must be in single block");
}
// require maximum number of nodes be equal to maximum number of nodes in a block
if (maxNodeTag != numNodesInBlock){
throw mjCError(NULL, "Maximum number of nodes must be equal to number of nodes in a block");
}
// check dimensionality and save
if (entityDim < 1 || entityDim > 3) {
throw mjCError(NULL, "Entity must be 1D, 2D or 3D");
}
def.spec.flex->dim = entityDim;
// read and discard node tags; require range from minNodeTag to maxNodeTag
for (size_t i=0; i < numNodes; i++) {
size_t tag;
ss >> tag;
if (!ss.good()) {
throw mjCError(NULL, "Error reading node tags");
}
if (tag != i+minNodeTag) {
throw mjCError(NULL, "Node tags must be sequential");
}
}
// read points
if (numNodes < 0 || numNodes >= INT_MAX / 3) {
throw mjCError(NULL, "Invalid number of nodes.");
}
point.reserve(3*numNodes);
for (size_t i=0; i < 3*numNodes; i++) {
double x;
ss >> x;
if (!ss.good()) {
throw mjCError(NULL, "Error reading node coordinates");
}
point.push_back(x);
}
}
// binary nodes
else {
// check header size
if (nodeend-nodebegin < kGmsh41HeaderSize) {
throw mjCError(NULL, "Invalid nodes header");
}
// read header
ReadFromBuffer(&numEntityBlocks, buffer + nodebegin);
ReadFromBuffer(&numNodes, buffer + nodebegin + 8);
ReadFromBuffer(&minNodeTag, buffer + nodebegin + 16);
ReadFromBuffer(&maxNodeTag, buffer + nodebegin + 24);
ReadFromBuffer(&entityDim, buffer + nodebegin + 32);
ReadFromBuffer(&entityTag, buffer + nodebegin + 36);
ReadFromBuffer(&parametric, buffer + nodebegin + 40);
ReadFromBuffer(&numNodesInBlock, buffer + nodebegin + 44);
// require single block
if (numEntityBlocks != 1 || numNodes != numNodesInBlock) {
throw mjCError(NULL, "All nodes must be in single block");
}
// check number of nodes is a positive number
if (numNodes < 0) {
throw mjCError(NULL, "Invalid number of nodes");
}
// check dimensionality and save
if (entityDim < 1 || entityDim > 3) {
throw mjCError(NULL, "Entity must be 1D, 2D or 3D");
}
def.spec.flex->dim = entityDim;
// nodeData: node tag and 3 nodes
constexpr int numNodeComponents = 4;
constexpr int componentSize = 8;
int nodeDataSize = numNodeComponents*componentSize;
// check section byte size
if (nodeend-nodebegin < kGmsh41HeaderSize + numNodes*nodeDataSize) {
throw mjCError(NULL, "Insufficient byte size of Nodes");
}
// check node tags: must range from minNodeTag to maxNodeTag
const char* tagbuffer = buffer + nodebegin + kGmsh41HeaderSize;
for (size_t i=0; i < numNodes; i++) {
ReadFromBuffer(&tag, tagbuffer + i*componentSize);
if (tag != i+minNodeTag) {
throw mjCError(NULL, "Node tags must be sequential");
}
}
// read points
if (numNodes < 0 || numNodes >= INT_MAX / 3) {
throw mjCError(NULL, "Invalid number of nodes.");
}
point.reserve(3*numNodes);
const char* pointbuffer = buffer + nodebegin + kGmsh41HeaderSize + componentSize*numNodes;
for (size_t i=0; i < 3*numNodes; i++) {
double x;
ReadFromBuffer(&x, pointbuffer + i*componentSize);
point.push_back(x);
}
}
size_t numElements, minElementTag, maxElementTag, numElementsInBlock;
int elementType;
// ascii elements
if (binary == 0) {
// convert element char buffer to stringstream
buffer[elemend] = 0;
stringstream ss(std::string(buffer + elembegin, elemend - elembegin));
// read header
ss >> numEntityBlocks >> numElements >> minElementTag >> maxElementTag;
ss >> entityDim >> entityTag >> elementType >> numElementsInBlock;
if (!ss.good()) {
throw mjCError(NULL, "Error reading Elements header");
}
// require single block
if (numEntityBlocks != 1 || numElements != numElementsInBlock) {
throw mjCError(NULL, "All elements must be in single block");
}
// check number of elements is a positive number
if (numElements < 0) {
throw mjCError(NULL, "Invalid number of elements");
}
// dimensionality must be same as nodes
if (entityDim != def.spec.flex->dim) {
throw mjCError(NULL, "Inconsistent dimensionality in Elements");
}
if (numElements < 0 || numElements >= INT_MAX / 4) {
throw mjCError(NULL, "Invalid numElements.");
}
// type must be consistent with dimensionality
if ((entityDim == 1 && elementType != 1) ||
(entityDim == 2 && elementType != 2) ||
(entityDim == 3 && elementType != 4)) {
throw mjCError(NULL, "Element type inconsistent with dimensionality");
}
// read elements, discard tags
element.reserve((entityDim+1)*numElements);
for (size_t i=0; i < numElements; i++) {
size_t tag, nodeid;
ss >> tag;
for (int k=0; k <= entityDim; k++) {
ss >> nodeid;
if (!ss.good()) {
throw mjCError(NULL, "Error reading Elements");
}
element.push_back((int)(nodeid-minNodeTag));
}
}
}
// binary elements
else {
// check header size
if (elemend-elembegin < kGmsh41HeaderSize) {
throw mjCError(NULL, "Invalid elements header");
}
// read header
ReadFromBuffer(&numEntityBlocks, buffer + elembegin);
ReadFromBuffer(&numElements, buffer + elembegin + 8);
ReadFromBuffer(&minElementTag, buffer + elembegin + 16);
ReadFromBuffer(&maxElementTag, buffer + elembegin + 24);
ReadFromBuffer(&entityDim, buffer + elembegin + 32);
ReadFromBuffer(&entityTag, buffer + elembegin + 36);
ReadFromBuffer(&elementType, buffer + elembegin + 40);
ReadFromBuffer(&numElementsInBlock, buffer + elembegin + 44);
// require single block
if (numEntityBlocks != 1 || numElements != numElementsInBlock) {
throw mjCError(NULL, "All elements must be in single block");
}
// check number of elements is a positive number
if (numElements < 0) {
throw mjCError(NULL, "Invalid number of elements");
}
// dimensionality must be same as nodes
if (entityDim != def.spec.flex->dim) {
throw mjCError(NULL, "Inconsistent dimensionality in Elements");
}
// type must be consistent with dimensionality
if ((entityDim == 1 && elementType != 1) ||
(entityDim == 2 && elementType != 2) ||
(entityDim == 3 && elementType != 4)) {
throw mjCError(NULL, "Element type inconsistent with dimensionality");
}
if (numElements < 0 || numElements >= INT_MAX / 4) {
throw mjCError(NULL, "Invalid numElements.");
}
// elementData: element tag and n node tags
int numElementComponents = (entityDim+2);
constexpr int componentSize = 8;
int elementDataSize = numElementComponents*componentSize;
// check section byte size
if (elemend - elembegin < kGmsh41HeaderSize + numElements*elementDataSize) {
throw mjCError(NULL, "Insufficient byte size of Elements");
}
// read elements, discard tags
element.reserve((entityDim+1)*numElements);
const char* elembuffer = buffer + elembegin + kGmsh41HeaderSize;
for (size_t i=0; i < numElements; i++) {
// skip element tag
elembuffer += componentSize;
// read vertex ids
size_t elemid;
for (int k=0; k <= entityDim; k++) {
ReadFromBuffer(&elemid, elembuffer);
int elementid = elemid - minNodeTag;
element.push_back(elementid);
elembuffer += componentSize;
}
}
}
}
// load GMSH format 2.2
void mjCFlexcomp::LoadGMSH22(char* buffer, int binary, int nodeend,
int nodebegin, int elemend, int elembegin) {
// number of nodes
size_t numNodes = 0;
// ascii nodes
if (binary == 0) {
// convert node char buffer to stringstream
stringstream ss(std::string(buffer + nodebegin, nodeend - nodebegin));
std::string line;
// checking header template
std::getline(ss, line);
if (!IsValidElementOrNodeHeader22(line)) {
throw mjCError(NULL, "Invalid node header");
}
ss.seekg(-(line.size()+1), std::ios::cur);
// read header
size_t maxNodeTag = 0;
ss >> maxNodeTag;
if (!ss.good()) {
throw mjCError(NULL, "Error reading Nodes header");
}
numNodes = maxNodeTag;
if (numNodes < 0 || numNodes >= INT_MAX / 3) {
throw mjCError(NULL, "Invalid number of nodes.");
}
// read points, discard tag
point.reserve(3*numNodes);
for (size_t i=0; i < numNodes; i++) {
size_t tag;
double x;
ss >> tag;
if (!ss.good()) {
throw mjCError(NULL, "Error reading node tags");
}
// reading nodes
for (int k=0; k < 3; k++) {
ss >> x;
if (!ss.good()) {
throw mjCError(NULL, "Error reading node coordinates");
}
point.push_back(x);
}
}
}
// binary nodes
else {
// header size for gmshApp
constexpr int nodeHeaderSizeGmshApp = 5;
// header size compatible with both gmshApp and Ftetwild
constexpr int nodeHeaderSize = nodeHeaderSizeGmshApp - 1;
// check header size
if (nodeend-nodebegin < nodeHeaderSize) {
throw mjCError(NULL, "Invalid nodes header");
}
// parse maxNodeTag and then cast it to int
char maxNodeTagChar[11] = {0};
ReadStrFromBuffer(maxNodeTagChar, buffer + nodebegin, std::min(10, nodeend - nodebegin));
size_t measuredHeaderSize = strnlen(maxNodeTagChar, 10) - 1;
size_t maxNodeTag;
try {
maxNodeTag = std::stoi(maxNodeTagChar);
} catch (const std::out_of_range& e) {
throw mjCError(NULL, "Invalid number of nodes");
}
numNodes = maxNodeTag;
// check number of nodes is a positive number
if (numNodes < 0) {
throw mjCError(NULL, "Invalid number of nodes");
}
// node data: node tag and 3 nodes
int nodeSize = sizeof(double);
int indexSize = sizeof(int);
int nodeDataSize = indexSize + 3*nodeSize;
// check section byte size
if (nodeend - nodebegin < nodeHeaderSize + numNodes*nodeDataSize) {
throw mjCError(NULL, "Insufficient byte size of Nodes");
}
// read point, discard tag
if (numNodes < 0 || numNodes >= INT_MAX / 3) {
throw mjCError(NULL, "Invalid number of nodes.");
}
point.reserve(3*numNodes);
// beginning of buffer containing node info
const char* tagBuffer = buffer + nodebegin + measuredHeaderSize;
for (int i=0; i < numNodes; i++) {
int tag;
int offset = i*(sizeof(int) + sizeof(double)*3);
ReadFromBuffer(&tag, tagBuffer + offset);
for (int k=0; k < 3; k++) {
double x;
const char* nodeBuffer = tagBuffer + sizeof(int) + sizeof(double)*k;
ReadFromBuffer(&x, nodeBuffer + offset);
point.push_back(x);
}
}
}
// ascii elements
if (binary == 0) {
// convert element char buffer to stringstream
buffer[elemend] = 0;
stringstream ss(std::string(buffer + elembegin, elemend - elembegin));
std::string line;
// checking header template
std::getline(ss, line);
if (!IsValidElementOrNodeHeader22(line)) {
throw mjCError(NULL, "Invalid elements header");
}
ss.seekg(-(line.size()+1), std::ios::cur);
// read header
size_t maxElementTag = 0;
ss >> maxElementTag;
if (!ss.good()) {
throw mjCError(NULL, "Error reading Elements header");
}
size_t numElements = maxElementTag;
if (numElements < 0 || numElements >= INT_MAX / 4) {
throw mjCError(NULL, "Invalid number of elements.");
}
// check number of elements is a positive number
if (numElements < 0) {
throw mjCError(NULL, "Invalid number of elements");
}
// reading first element's type
int tag = 0, elementType = 0, numTags = 0;
ss >> tag >> elementType >> numTags;
if (!ss.good()) {
throw mjCError(NULL, "Error reading Elements");
}
size_t entityDim = 0;
int numNodeTags = 0;
// surface
if (elementType == 2) {
entityDim = 2;
numNodeTags = 3;
}
// tetrahedral
else if (elementType == 4) {
entityDim = 3;
numNodeTags = 4;
}
if (numNodeTags < 1 || numNodeTags > 4) {
throw mjCError(NULL, "Invalid number of node tags");
}
// setting entityDim
def.spec.flex->dim = entityDim;
// read elements, discard all tags
element.reserve(numNodeTags*numElements);
for (size_t i=0; i < numElements; i++) {
int nodeTag = 0, physicalEntityTag = 0, elementModelEntityTag = 0;
if (i != 0) {
ss >> tag >> elementType >> numTags;
if (!ss.good()) {
throw mjCError(NULL, "Error reading Elements");
}
}
if (numTags > 0) {
ss >> physicalEntityTag >> elementModelEntityTag;
if (!ss.good()) {
throw mjCError(NULL, "Error reading Elements");
}
}
for (int k=0; k < numNodeTags; k++) {
ss >> nodeTag;
if (!ss.good()) {
throw mjCError(NULL, "Error reading Elements");
}
if (nodeTag > numNodes || nodeTag < 1) {
throw mjCError(NULL, "Invalid node tag");
}
element.push_back((int)(nodeTag-1));
}
}
}
// binary elements
else {
// header size for gmshApp
constexpr int elementHeaderSizeGmshApp = 4;
// header size for Ftetwild
constexpr int elementHeaderSizeFtetwild = 17;
// check header size
if (elemend - elembegin < elementHeaderSizeGmshApp) {
throw mjCError(NULL, "Invalid elements header");
}
// reading elements
char maxElementTagChar[11] = {0};
ReadStrFromBuffer(maxElementTagChar, buffer + elembegin, std::min(10, elemend - elembegin));
int measuredHeaderSize = strnlen(maxElementTagChar, 10) - 1;
int maxElementTag;
try {
maxElementTag = std::stoi(maxElementTagChar);
} catch (const std::out_of_range& e) {
throw mjCError(NULL, "Invalid number of elements");
}
int numElements = maxElementTag;
int tag, numTags;
int nodeTag;
int elementType;
// check number of elements is a positive number
if (numElements < 0) {
throw mjCError(NULL, "Invalid number of elements");
}
// size of single component in element data
int componentSize = sizeof(int);
// element buffer
const char* elementsBuffer = buffer + elembegin + measuredHeaderSize;
ReadFromBuffer(&elementType, elementsBuffer);
ReadFromBuffer(&numTags, elementsBuffer + componentSize*2);
ReadFromBuffer(&tag, elementsBuffer + componentSize*3);
// tetrahedral has 4 node tags and surface has 3
int numNodeTags = 0;
size_t entityDim = 0;
// surface
if (elementType == 2) {
entityDim = 2;
numNodeTags = 3;
}
// tetrahedral
else if (elementType == 4) {
entityDim = 3;
numNodeTags = 4;
}
if (numNodeTags < 1 || numNodeTags > 4) {
throw mjCError(NULL, "Invalid number of node tags");
}
def.spec.flex->dim = entityDim;
// element data(Ftetwild): tag and 4 nodeTag
constexpr int numComponentsFtetwild = 5;
// element data(gmshApp): 4 Info components, 2 entity tag and entityDim+1 nodeTags
constexpr int numInfoComponents = 4;
constexpr int numEntityTagComponents = 2;
int numComponentsGmshApp = numInfoComponents + numEntityTagComponents + numNodeTags;
// single element data size
int elementDataSizeFtetwild = numComponentsFtetwild*componentSize;
int elementDataSizeGmshApp = numComponentsGmshApp*componentSize;
// elements section buffer size
int elementsBufferSizeFtetwild = elementHeaderSizeFtetwild +
numElements*elementDataSizeFtetwild;
int elementsBufferSizeGmshApp = elementHeaderSizeGmshApp +
numElements*elementDataSizeGmshApp;
// check section byte size for ftetwild
if (elemend - elembegin < elementsBufferSizeFtetwild) {
throw mjCError(NULL, "Insufficient byte size of Elements");
}
// Handling elements produced by gmsh
if (numTags > 0) {
// check section byte size for gmsh
if (elemend - elembegin < elementsBufferSizeGmshApp) {
throw mjCError(NULL, "Insufficient byte size of Elements");
}
// read first element
for (int k =0; k < numNodeTags; k++) {
ReadFromBuffer(&nodeTag, elementsBuffer + componentSize*(6+k));
if (nodeTag > numNodes || nodeTag < 1) {
throw mjCError(NULL, "Invalid node tag");
}
element.push_back(nodeTag-1);
}
// read every other element
for (int i=1; i < numElements; i++) {
const char* numTagsBuffer = elementsBuffer + componentSize*2;
const char* tagBuffer = elementsBuffer + componentSize*3;
int offset = i*elementDataSizeGmshApp;
ReadFromBuffer(&numTags, numTagsBuffer + offset);
ReadFromBuffer(&tag, tagBuffer+offset);
for (int k =0; k < numNodeTags; k++) {
const char* nodeTagBuffer = elementsBuffer + componentSize*(6+k);
ReadFromBuffer(&nodeTag, nodeTagBuffer + offset);
if (nodeTag > numElements || nodeTag < 1) {
throw mjCError(NULL, "Invalid node tag");
}
element.push_back(nodeTag-1);
}
}
}
// Handling elements produced by ftetwild
else {
// read first element
for (int k = 0; k < numNodeTags; k++) {
const char* nodeTagBuffer = elementsBuffer + componentSize*(4+k);
ReadFromBuffer(&nodeTag, nodeTagBuffer);
if (nodeTag > numNodes || nodeTag < 1) {
throw mjCError(NULL, "Invalid node tag");
}
element.push_back(nodeTag-1);
}
// read every other element
for (int i=0; i < numElements-1; i++) {
int offset = componentSize*(4+2) + i*elementDataSizeFtetwild;
const char* tagBuffer = elementsBuffer + componentSize*2;
ReadFromBuffer(&tag, tagBuffer + offset);
for (int k=0; k < numNodeTags; k++) {
const char* nodeTagBuffer = elementsBuffer + componentSize*(3+k);
ReadFromBuffer(&nodeTag, nodeTagBuffer + offset);
if (nodeTag > numElements || nodeTag < 1) {
throw mjCError(NULL, "Invalid node tag");
}
element.push_back(nodeTag-1);
}
}
}
}
}
// load GMSH file from resource
void mjCFlexcomp::LoadGMSH(mjCModel* model, mjResource* resource) {
// get buffer from resource
char* buffer = 0;
int buffer_sz = mju_readResource(resource, (const void**) &buffer);
// check buffer
if (buffer_sz < 0) {
throw mjCError(NULL, "Could not read GMSH file");
} else if (buffer_sz == 0) {
throw mjCError(NULL, "Empty GMSH file");
} else if (buffer_sz < 11 || strncmp(buffer, "$MeshFormat", 11)) {
throw mjCError(NULL, "GMSH file must begin with $MeshFormat");
}
// check version, determine ascii or binary
double version;
int binary;
if (sscanf(buffer + 11, "%lf %d", &version, &binary) != 2) {
throw mjCError(NULL, "Could not read GMSH file header");
}
if (mju_round(100*version) != 220 && mju_round(100*version) != 410) {
throw mjCError(NULL, "Only GMSH file format versions 4.1 and 2.2 are supported");
}
// find section begin/end
int nodebegin = findstring(buffer, buffer_sz, "$Nodes");
int nodeend = findstring(buffer, buffer_sz, "$EndNodes");
int elembegin = findstring(buffer, buffer_sz, "$Elements");
int elemend = findstring(buffer, buffer_sz, "$EndElements");
// correct begin for string size, +1 for LF in binary (CRLF in Win ascii works)
nodebegin += (int)strlen("$Nodes") + 1;
elembegin += (int)strlen("$Elements") + 1;
// check sections
if (nodebegin < 0) {
throw mjCError(NULL, "GMSH file missing $Nodes");
}
if (nodeend < nodebegin) {
throw mjCError(NULL, "GMSH file missing $EndNodes after $Nodes");
}
if (elembegin < 0) {
throw mjCError(NULL, "GMSH file missing $Elements");
}
if (elemend < elembegin) {
throw mjCError(NULL, "GMSH file missing $EndElements after $Elements");
}
// Support for 4.1
if (mju_round(100*version) == 410) {
LoadGMSH41(buffer, binary, nodeend, nodebegin, elemend, elembegin);
}
// Support for 2.2
else if (mju_round(100*version) == 220) {
LoadGMSH22(buffer, binary, nodeend, nodebegin, elemend, elembegin);
} else {
throw mjCError(NULL, "Unsupported GMSH file format version");
}
}