// 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 #include #include #include #include #include #include #include #include #include #include #include #include #ifdef MUJOCO_TINYOBJLOADER_IMPL #define TINYOBJLOADER_IMPLEMENTATION #endif #if defined(__clang__) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wgnu-anonymous-struct" #pragma clang diagnostic ignored "-Wnested-anon-types" #elif defined(__GNUC__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpedantic" #endif #include #if defined(__clang__) #pragma clang diagnostic pop #elif defined(__GNUC__) #pragma GCC diagnostic pop #endif #include #include #include #include #include "engine/engine_crossplatform.h" #include "engine/engine_io.h" #include "engine/engine_plugin.h" #include "engine/engine_resource.h" #include "engine/engine_util_blas.h" #include "engine/engine_util_errmem.h" #include "engine/engine_util_misc.h" #include "engine/engine_util_solve.h" #include "engine/engine_util_spatial.h" #include "user/user_model.h" #include "user/user_objects.h" #include "user/user_util.h" #include "xml/xml_util.h" #include extern "C" { #include "qhull_ra.h" } using std::string; using std::vector; // compute triangle area, surface normal, center static mjtNum _triangle(mjtNum* normal, mjtNum* center, const float* v1, const float* v2, const float* v3) { // center if (center) { for (int i=0; i<3; i++) { center[i] = (v1[i] + v2[i] + v3[i])/3; } } // normal = (v2-v1) cross (v3-v1) double b[3] = { v2[0]-v1[0], v2[1]-v1[1], v2[2]-v1[2] }; double c[3] = { v3[0]-v1[0], v3[1]-v1[1], v3[2]-v1[2] }; mju_cross(normal, b, c); // get length double len = mju_norm3(normal); // ignore small faces if (len static T* VecToArray(std::vector& 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; } } // Read data of type T from a potentially unaligned buffer pointer. template static void ReadFromBuffer(T* dst, const char* src) { std::memcpy(dst, src, sizeof(T)); } //------------------ class mjCMesh implementation -------------------------------------------------- mjCMesh::mjCMesh(mjCModel* _model, mjCDef* _def) { // set defaults mjuu_setvec(refpos_, 0, 0, 0); mjuu_setvec(refquat_, 1, 0, 0, 0); mjuu_setvec(scale_, 1, 1, 1); smoothnormal_ = false; // clear internal variables mjuu_setvec(pos_surface_, 0, 0, 0); mjuu_setvec(pos_volume_, 0, 0, 0); mjuu_setvec(quat_surface_, 1, 0, 0, 0); mjuu_setvec(quat_volume_, 1, 0, 0, 0); mjuu_setvec(pos_, 0, 0, 0); mjuu_setvec(quat_, 1, 0, 0, 0); mjuu_setvec(boxsz_surface_, 0, 0, 0); mjuu_setvec(boxsz_volume_, 0, 0, 0); mjuu_setvec(aamm_, 1e10, 1e10, 1e10); mjuu_setvec(aamm_+3, -1e10, -1e10, -1e10); nvert_ = 0; nnormal_ = 0; ntexcoord_ = 0; nface_ = 0; szgraph_ = 0; vert_ = NULL; normal_ = NULL; center_ = NULL; texcoord_ = NULL; face_ = NULL; facenormal_ = NULL; facetexcoord_ = NULL; graph_ = NULL; needhull_ = false; invalidorientation_.first = -1; invalidorientation_.second = -1; validarea_ = true; validvolume_ = 1; valideigenvalue_ = true; validinequality_ = true; processed_ = false; visual_ = true; // reset to default if given if (_def) { *this = _def->mesh; } // set model, def model = _model; def = (_def ? _def : (_model ? _model->defaults[0] : 0)); } mjCMesh::~mjCMesh() { if (vert_) mju_free(vert_); if (normal_) mju_free(normal_); if (texcoord_) mju_free(texcoord_); if (center_) mju_free(center_); if (face_) mju_free(face_); if (facenormal_) mju_free(facenormal_); if (facetexcoord_) mju_free(facetexcoord_); if (graph_) mju_free(graph_); } void mjCMesh::set_content_type(std::optional&& content_type) { if (content_type.has_value()) { content_type_ = std::move(content_type.value()); } } void mjCMesh::set_file(std::optional&& file) { if (file.has_value()) { file_ = std::move(file.value()); } } void mjCMesh::set_refpos(std::optional> refpos) { if (refpos.has_value()) { std::copy(refpos.value().begin(), refpos.value().end(), refpos_); } } void mjCMesh::set_refquat(std::optional> refquat) { if (refquat.has_value()) { std::copy(refquat.value().begin(), refquat.value().end(), refquat_); } } void mjCMesh::set_scale(std::optional> scale) { if (scale.has_value()) { set_scale(scale.value()); } } void mjCMesh::set_uservert(std::optional>&& uservert) { if (uservert.has_value()) { uservert_ = std::move(uservert.value()); } } void mjCMesh::set_usernormal(std::optional>&& usernormal) { if (usernormal.has_value()) { usernormal_ = std::move(usernormal.value()); } } void mjCMesh::set_usertexcoord(std::optional>&& usertexcoord) { if (usertexcoord.has_value()) { usertexcoord_ = std::move(usertexcoord.value()); } } void mjCMesh::set_userface(std::optional>&& userface) { if (userface.has_value()) { userface_ = std::move(userface.value()); } } void mjCMesh::set_file(const std::string& file) { file_ = file; } void mjCMesh::set_scale(std::array scale) { std::copy(scale.begin(), scale.end(), scale_); } void mjCMesh::set_smoothnormal(bool smoothnormal) { smoothnormal_ = smoothnormal; } void mjCMesh::set_needhull(bool needhull) { needhull_ = needhull; } // generate mesh using marching cubes void mjCMesh::LoadSDF() { if (plugin_name.empty() && plugin_instance_name.empty()) { throw mjCError( this, "neither 'plugin' nor 'instance' is specified for mesh '%s', (id = %d)", name.c_str(), id); } if (scale_[0] != 1 || scale_[1] != 1 || scale_[2] != 1) { throw mjCError(this, "attribute scale is not compatible with SDFs in mesh '%s', (id = %d)", name.c_str(), id); } model->ResolvePlugin(this, plugin_name, plugin_instance_name, &plugin_instance); const mjpPlugin* plugin = mjp_getPluginAtSlot(plugin_instance->plugin_slot); if (!(plugin->capabilityflags & mjPLUGIN_SDF)) { throw mjCError(this, "plugin '%s' does not support signed distance fields", plugin->name); } std::vector attributes(plugin->nattribute, 0); std::vector names(plugin->nattribute, 0); std::vector values(plugin->nattribute, 0); for (int i=0; i < plugin->nattribute; i++) { names[i] = plugin->attributes[i]; values[i] = plugin_instance->config_attribs[names[i]].c_str(); } if (plugin->sdf_attribute) { plugin->sdf_attribute(attributes.data(), names.data(), values.data()); } mjtNum aabb[6] = {0}; plugin->sdf_aabb(aabb, attributes.data()); mjtNum total = aabb[3] + aabb[4] + aabb[5]; const mjtNum n = 300; int nx, ny, nz; nx = floor(n / total * aabb[3]) + 1; ny = floor(n / total * aabb[4]) + 1; nz = floor(n / total * aabb[5]) + 1; MC::MC_FLOAT* field = new MC::MC_FLOAT[nx * ny * nz]; for (int i = 0; i < nx; i++) { for (int j = 0; j < ny; j++) { for (int k = 0; k < nz; k++) { mjtNum point[] = {aabb[0]-aabb[3] + 2 * aabb[3] * i / (nx-1), aabb[1]-aabb[4] + 2 * aabb[4] * j / (ny-1), aabb[2]-aabb[5] + 2 * aabb[5] * k / (nz-1)}; field[(k * ny + j) * nx + i] = plugin->sdf_staticdistance(point, attributes.data()); } } } MC::mcMesh mesh; MC::marching_cube(field, nx, ny, nz, mesh); std::vector uservert; std::vector usernormal; std::vector userface; for (size_t i = 0; i < mesh.vertices.size(); i++) { uservert.push_back(2*aabb[3]*mesh.vertices.at(i).x/(nx-1) + aabb[0]-aabb[3]); uservert.push_back(2*aabb[4]*mesh.vertices.at(i).y/(ny-1) + aabb[1]-aabb[4]); uservert.push_back(2*aabb[5]*mesh.vertices.at(i).z/(nz-1) + aabb[2]-aabb[5]); } for (size_t i = 0; i < mesh.normals.size(); i++) { usernormal.push_back(mesh.normals.at(i).x); usernormal.push_back(mesh.normals.at(i).y); usernormal.push_back(mesh.normals.at(i).z); } for (size_t i = 0; i < mesh.indices.size(); i++) { userface.push_back(mesh.indices.at(i)); } set_uservert(uservert); set_usernormal(usernormal); set_userface(userface); delete[] field; } // compiler void mjCMesh::Compile(const mjVFS* vfs) { // load file if (!file_.empty()) { // remove path from file if necessary if (model->strippath) { file_ = mjuu_strippath(file_); } std::string asset_type = GetAssetContentType(file_, content_type_); if (asset_type.empty()) { throw mjCError(this, "unknown mesh content type for file: '%s'", file_.c_str()); } if (asset_type != "model/stl" && asset_type != "model/obj" && asset_type != "model/vnd.mujoco.msh") { throw mjCError(this, "unsupported content type: '%s'", asset_type.c_str()); } string filename = mjuu_makefullname(model->modelfiledir, model->meshdir, file_); mjResource* resource = LoadResource(filename, vfs); try { if (asset_type == "model/stl") { LoadSTL(resource); } else if (asset_type == "model/obj") { LoadOBJ(resource); } else { LoadMSH(resource); } mju_closeResource(resource); } catch (mjCError err) { mju_closeResource(resource); throw err; } } // create using marching cubes else if (is_plugin) { LoadSDF(); } // copy user vertex if (!uservert_.empty()) { // check repeated if (vert_) { throw mjCError(this, "repeated vertex specification"); } // check size if (uservert_.size()<12) { throw mjCError(this, "at least 4 vertices required"); } if (uservert_.size()%3) { throw mjCError(this, "vertex data must be a multiple of 3"); } // copy from user nvert_ = (int)uservert_.size()/3; vert_ = VecToArray(uservert_, !file_.empty()); } // copy user normal if (!usernormal_.empty()) { // check repeated if (normal_) { throw mjCError(this, "repeated normal specification"); } // check size if (usernormal_.size()%3) { throw mjCError(this, "normal data must be a multiple of 3"); } // copy from user nnormal_ = (int)usernormal_.size()/3; normal_ = VecToArray(usernormal_, !file_.empty()); } // copy user texcoord if (!usertexcoord_.empty()) { // check repeated if (texcoord_) { throw mjCError(this, "repeated texcoord specification"); } // check size if (usertexcoord_.size()%2) { throw mjCError(this, "texcoord must be a multiple of 2"); } // check size if no face texcoord indices are given if (usertexcoord_.size() != 2*nvert_ && userfacetexcoord_.empty()) { throw mjCError(this, "texcoord must be 2*nv if face texcoord indices are not provided in an OBJ file"); } // copy from user ntexcoord_ = (int)usertexcoord_.size()/2; texcoord_ = VecToArray(usertexcoord_, !file_.empty()); } // copy user face if (!userface_.empty()) { // check repeated if (face_) { throw mjCError(this, "repeated face specification"); } // check size if (userface_.size()%3) { throw mjCError(this, "face data must be a multiple of 3"); } // check vertices exist for (int i=0; i= nvert_ || userface_[i] < 0) { throw mjCError(this, "index in face does not exist in vertex array"); } } // create half-edge structure (if mesh was in XML) if (useredge_.empty()) { for (int i=0; isqrt(mjMINVAL)) { useredge_.push_back(std::pair(v0, v1)); useredge_.push_back(std::pair(v1, v2)); useredge_.push_back(std::pair(v2, v0)); } else { // TODO(b/255525326) } } } // copy from user nface_ = (int)userface_.size()/3; face_ = VecToArray(userface_, !file_.empty()); // check vertices exist for (auto vertex_index : userface_) { if (vertex_index>=nvert_ || vertex_index < 0) { throw mjCError(this, "found index in userface that exceeds uservert size."); } } } // check for inconsistent face orientations if (!useredge_.empty()) { std::stable_sort(useredge_.begin(), useredge_.end()); auto iterator = std::adjacent_find(useredge_.begin(), useredge_.end()); if (iterator != useredge_.end()) { invalidorientation_.first = iterator->first+1; invalidorientation_.second = iterator->second+1; } } // require vertices if (!vert_) { throw mjCError(this, "no vertices"); } // make graph describing convex hull if ((model->convexhull && needhull_) || !face_) { MakeGraph(); } // no faces: copy from convex hull if (!face_) { CopyGraph(); } // no normals: make if (!normal_) { MakeNormal(); } // copy user normal indices if (!userfacenormal_.empty()) { // check repeated if (facenormal_) { throw mjCError(this, "repeated facenormal specification"); } if (userfacenormal_.size()!=3*nface_) { throw mjCError(this, "face data must have the same size as face normal data"); } facenormal_ = VecToArray(userfacenormal_, !file_.empty()); } // copy user texcoord if (!userfacetexcoord_.empty()) { // check repeated if (facetexcoord_) { throw mjCError(this, "repeated facetexcoord specification"); } facetexcoord_ = VecToArray(userfacetexcoord_, !file_.empty()); } // no facetexcoord: copy from faces if (!facetexcoord_ && texcoord_) { facetexcoord_ = (int*) mju_malloc(3*nface_*sizeof(int)); memcpy(facetexcoord_, face_, 3*nface_*sizeof(int)); } // facenormal might not exist if usernormal was specified if (!facenormal_) { facenormal_ = (int*) mju_malloc(3*nface_*sizeof(int)); memcpy(facenormal_, face_, 3*nface_*sizeof(int)); } // scale, center, orient, compute mass and inertia Process(); processed_ = true; // no radii: make if (!center_) { MakeCenter(); } // make bounding volume hierarchy if (tree_.bvh.empty()) { face_aabb_.assign(6*nface_, 0); tree_.AllocateBoundingVolumes(nface_); for (int i=0; iSetId(faceid); node->conaffinity = 1; node->contype = 1; node->pos = center_ + 3*faceid; node->quat = NULL; mjtNum face_aamm[6] = {1E+10, 1E+10, 1E+10, -1E+10, -1E+10, -1E+10}; for (int j=0; j<3; j++) { int vertid = face_[3*faceid+j]; face_aamm[0] = mjMIN(face_aamm[0], vert_[3*vertid+0]); face_aamm[1] = mjMIN(face_aamm[1], vert_[3*vertid+1]); face_aamm[2] = mjMIN(face_aamm[2], vert_[3*vertid+2]); face_aamm[3] = mjMAX(face_aamm[3], vert_[3*vertid+0]); face_aamm[4] = mjMAX(face_aamm[4], vert_[3*vertid+1]); face_aamm[5] = mjMAX(face_aamm[5], vert_[3*vertid+2]); } face_aabb_[6*faceid+0] = .5 * (face_aamm[0] + face_aamm[3]); face_aabb_[6*faceid+1] = .5 * (face_aamm[1] + face_aamm[4]); face_aabb_[6*faceid+2] = .5 * (face_aamm[2] + face_aamm[5]); face_aabb_[6*faceid+3] = .5 * (face_aamm[3] - face_aamm[0]); face_aabb_[6*faceid+4] = .5 * (face_aamm[4] - face_aamm[1]); face_aabb_[6*faceid+5] = .5 * (face_aamm[5] - face_aamm[2]); node->aabb = face_aabb_.data() + 6*faceid; } // get position double* mjCMesh::GetPosPtr(mjtMeshType type) { if (type==mjSHELL_MESH) { return pos_surface_; } else { return pos_volume_; } } // get orientation double* mjCMesh::GetQuatPtr(mjtMeshType type) { if (type==mjSHELL_MESH) { return quat_surface_; } else { return quat_volume_; } } double* mjCMesh::GetOffsetPosPtr() { return pos_; } double* mjCMesh::GetOffsetQuatPtr() { return quat_; } bool mjCMesh::HasTexcoord() const { return texcoord_ != nullptr; } void mjCMesh::CopyVert(float* arr) const { std::copy(vert_, vert_+3*nvert_, arr); } void mjCMesh::CopyNormal(float* arr) const { std::copy(normal_, normal_+3*nnormal_, arr); } void mjCMesh::CopyFace(int* arr) const { std::copy(face_, face_+3*nface_, arr); } void mjCMesh::CopyFaceTexcoord(int* arr) const { std::copy(facetexcoord_, facetexcoord_+3*nface_, arr); } void mjCMesh::CopyFaceNormal(int* arr) const { std::copy(facenormal_, facenormal_+3*nface_, arr); } void mjCMesh::CopyTexcoord(float* arr) const { std::copy(texcoord_, texcoord_+2*ntexcoord_, arr); } void mjCMesh::CopyGraph(int* arr) const { std::copy(graph_, graph_+szgraph_, arr); } void mjCMesh::DelTexcoord() { if (texcoord_) mju_free(texcoord_); ntexcoord_ = 0; } // set geom size to match mesh void mjCMesh::FitGeom(mjCGeom* geom, double* meshpos) { // copy mesh pos into meshpos mjuu_copyvec(meshpos, GetPosPtr(geom->typeinertia), 3); // use inertial box if (!model->fitaabb) { // get inertia box type (shell or volume) double* boxsz = GetInertiaBoxPtr(geom->typeinertia); switch (geom->type) { case mjGEOM_SPHERE: geom->size[0] = (boxsz[0] + boxsz[1] + boxsz[2])/3; break; case mjGEOM_CAPSULE: geom->size[0] = (boxsz[0] + boxsz[1])/2; geom->size[1] = mju_max(0, boxsz[2] - geom->size[0]/2); break; case mjGEOM_CYLINDER: geom->size[0] = (boxsz[0] + boxsz[1])/2; geom->size[1] = boxsz[2]; break; case mjGEOM_ELLIPSOID: case mjGEOM_BOX: geom->size[0] = boxsz[0]; geom->size[1] = boxsz[1]; geom->size[2] = boxsz[2]; break; default: throw mjCError(this, "invalid geom type in fitting mesh %s", name.c_str()); } } // use aamm else { // find aabb box center double cen[3] = {(aamm_[0]+aamm_[3])/2, (aamm_[1]+aamm_[4])/2, (aamm_[2]+aamm_[5])/2}; // add box center into meshpos meshpos[0] += cen[0]; meshpos[1] += cen[1]; meshpos[2] += cen[2]; // compute depending on type switch (geom->type) { case mjGEOM_SPHERE: // find maximum distance geom->size[0] = 0; for (int i=0; isize[0] = mju_max(geom->size[0], dst); } break; case mjGEOM_CAPSULE: case mjGEOM_CYLINDER: // find maximum distance in XY, separately in Z geom->size[0] = 0; geom->size[1] = 0; for (int i=0; isize[0] = mju_max(geom->size[0], dst); // proceed with z: valid for cylinder double dst2 = fabs(v[2]-cen[2]); geom->size[1] = mju_max(geom->size[1], dst2); } // special handling of capsule: consider curved cap if (geom->type==mjGEOM_CAPSULE) { geom->size[1] = 0; for (int i=0; isize[0] * sin(acos(dst/geom->size[0])); geom->size[1] = mju_max(geom->size[1], dst2-h); } } break; case mjGEOM_ELLIPSOID: case mjGEOM_BOX: geom->size[0] = aamm_[3] - cen[0]; geom->size[1] = aamm_[4] - cen[1]; geom->size[2] = aamm_[5] - cen[2]; break; default: throw mjCError(this, "invalid fittype in mesh %s", name.c_str()); } } // rescale size geom->size[0] *= geom->fitscale; geom->size[1] *= geom->fitscale; geom->size[2] *= geom->fitscale; } // comparison function for vertex sorting quicksortfunc(vertcompare, context, el1, el2) { float* vert = (float*) context; float x1 = vert[3*(*(int*)el1)] + 1e-2*vert[1+3*(*(int*)el1)] + 1e-4*vert[2+3*(*(int*)el1)]; float x2 = vert[3*(*(int*)el2)] + 1e-2*vert[1+3*(*(int*)el2)] + 1e-4*vert[2+3*(*(int*)el2)]; if (x1 < x2) { return -1; } else if (x1 == x2) { return 0; } else { return 1; } } // remove repeated vertices void mjCMesh::RemoveRepeated() { int repeated = 0; // allocate sort and redirection indices, set to identity auto index = std::unique_ptr(new int[nvert_]); auto redirect = std::unique_ptr(new int[nvert_]); for (int i=0; i < nvert_; i++) { index[i] = redirect[i] = i; } // sort vertices mjQUICKSORT(index.get(), nvert_, sizeof(int), vertcompare, vert_); // find repeated vertices, set redirect for (int i=1; i < nvert_; i++) { if (vert_[3*index[i]] == vert_[3*index[i-1]] && vert_[3*index[i]+1] == vert_[3*index[i-1]+1] && vert_[3*index[i]+2] == vert_[3*index[i-1]+2]) { redirect[index[i]] = index[i-1]; repeated++; } } // compress vertices, change face data if (repeated) { // track redirections until non-redirected vertex, set for (int i=0; i=nvert_-repeated) { throw mjCError( this, "error removing vertices from mesh '%s'", name.c_str()); } } } // correct vertex count nvert_ -= repeated; // resize vert if any vertices were removed if (repeated) { float* old = vert_; vert_ = (float*) mju_malloc(3*nvert_*sizeof(float)); memcpy(vert_, old, 3*nvert_*sizeof(float)); mju_free(old); } } // load OBJ mesh void mjCMesh::LoadOBJ(mjResource* resource) { tinyobj::ObjReader objReader; const void* bytes = nullptr; int buffer_sz = mju_readResource(resource, &bytes); if (buffer_sz < 0) { throw mjCError(this, "could not read OBJ file '%s'", resource->name); } // TODO(etom): support .mtl files? const char* buffer = (const char*) bytes; objReader.ParseFromString(std::string(buffer, buffer_sz), std::string()); if (!objReader.Valid()) { throw mjCError(this, "could not parse OBJ file '%s'", resource->name); } const auto& attrib = objReader.GetAttrib(); uservert_ = attrib.vertices; // copy from one std::vector to another usernormal_ = attrib.normals; usertexcoord_ = attrib.texcoords; if (!objReader.GetShapes().empty()) { const auto& mesh = objReader.GetShapes()[0].mesh; bool righthand = (scale_[0]*scale_[1]*scale_[2] > 0); // iterate over mesh faces std::vector face_indices; for (int face = 0, idx = 0; idx < mesh.indices.size();) { int nfacevert = mesh.num_face_vertices[face]; if (nfacevert < 3 || nfacevert > 4) { throw mjCError( this, "only tri or quad meshes are supported for OBJ (file '%s')", resource->name); } face_indices.push_back(mesh.indices[idx]); face_indices.push_back(mesh.indices[idx + (righthand==1 ? 1 : 2)]); face_indices.push_back(mesh.indices[idx + (righthand==1 ? 2 : 1)]); if (nfacevert == 4) { face_indices.push_back(mesh.indices[idx]); face_indices.push_back(mesh.indices[idx + (righthand==1 ? 2 : 3)]); face_indices.push_back(mesh.indices[idx + (righthand==1 ? 3 : 2)]); } idx += nfacevert; ++face; } // for each vertex, store index, normal, and texcoord for (const auto& mesh_index : face_indices) { userface_.push_back(mesh_index.vertex_index); if (!usernormal_.empty()) { userfacenormal_.push_back(mesh_index.normal_index); } if (!usertexcoord_.empty()) { userfacetexcoord_.push_back(mesh_index.texcoord_index); } } } // flip the second texcoord for (int i=0; i0); // get file data in buffer char* buffer = 0; int buffer_sz = mju_readResource(resource, (const void**) &buffer); // still not found if (buffer_sz < 0) { throw mjCError(this, "could not read STL file '%s'", resource->name); } else if (!buffer_sz) { throw mjCError(this, "STL file '%s' is empty", resource->name); } // make sure there is enough data for header if (buffer_sz<84) { throw mjCError(this, "invalid header in STL file '%s'", resource->name); } // get number of triangles, check bounds ReadFromBuffer(&nface_, buffer + 80); if (nface_<1 || nface_>200000) { throw mjCError(this, "number of faces should be between 1 and 200000 in STL file '%s';" " perhaps this is an ASCII file?", resource->name); } // check remaining buffer size if (nface_*50 != buffer_sz-84) { throw mjCError(this, "STL file '%s' has wrong size; perhaps this is an ASCII file?", resource->name); } // assign stl data pointer const char* stl = buffer + 84; // allocate face and vertex data face_ = (int*) mju_malloc(3*nface_*sizeof(int)); vert_ = (float*) mju_malloc(9*nface_*sizeof(float)); // add vertices and faces, including repeated for now for (int i=0; iname); } // check if vertex coordinates can be cast to an int safely if (fabs(v[k])>pow(2, 30)) { throw mjCError(this, "vertex coordinates in STL file '%s' exceed maximum bounds", resource->name); } } // add vertex address in face; change order if scale makes it lefthanded if (righthand || j==0) { face_[3*i+j] = nvert_; } else { face_[3*i+3-j] = nvert_; } // add vertex data memcpy(vert_+3*nvert_, v, 3*sizeof(float)); nvert_++; } } RemoveRepeated(); } // load MSH binary mesh void mjCMesh::LoadMSH(mjResource* resource) { bool righthand = (scale_[0]*scale_[1]*scale_[2]>0); // get file data in buffer char* buffer = 0; int buffer_sz = mju_readResource(resource, (const void**) &buffer); // still not found if (buffer_sz < 0) { throw mjCError(this, "could not read MSH file '%s'", resource->name); } else if (!buffer_sz) { throw mjCError(this, "MSH file '%s' is empty", resource->name); } // make sure header is present if (buffer_sz<4*sizeof(int)) { throw mjCError(this, "missing header in MSH file '%s'", resource->name); } // get sizes from header ReadFromBuffer(&nvert_, buffer); ReadFromBuffer(&nnormal_, buffer + sizeof(int)); ReadFromBuffer(&ntexcoord_, buffer + 2*sizeof(int)); ReadFromBuffer(&nface_, buffer + 3*sizeof(int)); // check sizes if (nvert_<4 || nface_<0 || nnormal_<0 || ntexcoord_<0 || (nnormal_>0 && nnormal_!=nvert_) || (ntexcoord_>0 && ntexcoord_!=nvert_)) { throw mjCError(this, "invalid sizes in MSH file '%s'", resource->name); } // check file size if (buffer_sz != 4*sizeof(int) + 3*nvert_*sizeof(float) + 3*nnormal_*sizeof(float) + 2*ntexcoord_*sizeof(float) + 3*nface_*sizeof(int)) { throw mjCError(this, "unexpected file size in MSH file '%s'", resource->name); } // allocate and copy using UnalignedFloat = char[sizeof(float)]; auto fdata = reinterpret_cast(buffer + 4*sizeof(int)); if (nvert_) { vert_ = (float*) mju_malloc(3*nvert_*sizeof(float)); memcpy(vert_, fdata, 3*nvert_*sizeof(float)); fdata += 3*nvert_; } if (nnormal_) { normal_ = (float*) mju_malloc(3*nvert_*sizeof(float)); memcpy(normal_, fdata, 3*nvert_*sizeof(float)); fdata += 3*nvert_; } if (ntexcoord_) { texcoord_ = (float*) mju_malloc(2*nvert_*sizeof(float)); memcpy(texcoord_, fdata, 2*nvert_*sizeof(float)); fdata += 2*nvert_; } if (nface_) { face_ = (int*) mju_malloc(3*nface_*sizeof(int)); facenormal_ = (int*) mju_malloc(3*nface_*sizeof(int)); memcpy(face_, fdata, 3*nface_*sizeof(int)); memcpy(facenormal_, fdata, 3*nface_*sizeof(int)); } if (nface_ && texcoord_) { facetexcoord_= (int*) mju_malloc(3*nface_*sizeof(int)); memcpy(facetexcoord_, fdata, 3*nface_*sizeof(int)); } // rearrange face data if left-handed scaling if (nface_ && !righthand) { for (int i=0; imjMINVAL) { float scl = 1/sqrtf(len); normal_[3*i] *= scl; normal_[3*i+1] *= scl; normal_[3*i+2] *= scl; } else { normal_[3*i] = 0; normal_[3*i+1] = 0; normal_[3*i+2] = 1; } } } // find centroid of faces void mjCMesh::ComputeFaceCentroid(double facecen[3]) { double area = 0; double nrm[3]; double cen[3]; for (int i=0; i=nvert_) { throw mjCError(this, "vertex index out of range in %s (index = %d)", name.c_str(), i); } } // get area and center double a = _triangle(nrm, cen, vert_+3*face_[3*i], vert_+3*face_[3*i+1], vert_+3*face_[3*i+2]); // accumulate for (int j=0; j<3; j++) { facecen[j] += a*cen[j]; } area += a; } // require positive area if (area < mjMINVAL) { validarea_ = false; return; } // finalize centroid of faces for (int j=0; j<3; j++) { facecen[j] /= area; } } void mjCMesh::Process() { double facecen[3] = {0, 0, 0}; double nrm[3]; double cen[3]; // user offset, rotation, scaling ApplyTransformations(); // find centroid of faces ComputeFaceCentroid(facecen); // compute inertial properties for both inertia types for ( const auto type : { mjtMeshType::mjVOLUME_MESH, mjtMeshType::mjSHELL_MESH } ) { double CoM[3] = {0, 0, 0}; double inert[6] = {0, 0, 0, 0, 0, 0}; bool exactmeshinertia = model->exactmeshinertia; // compute CoM and volume from pyramid volumes ComputeVolume(CoM, type, facecen, model->exactmeshinertia); // perform computation with convex mesh if volume is negative if (GetVolumeRef(type) <= 0 && exactmeshinertia) { mju_warning("Malformed mesh %s, computing mesh inertia from convex hull", name.c_str()); exactmeshinertia = false; ComputeVolume(CoM, type, facecen, exactmeshinertia); } // if volume is still invalid, skip the rest of the computations if (GetVolumeRef(type) < mjMINVAL) { validvolume_ = GetVolumeRef(type) < 0 ? -1 : 0; continue; } // finalize CoM, save as mesh center for (int j=0; j<3; j++) { CoM[j] /= GetVolumeRef(type); } mjuu_copyvec(GetPosPtr(type), CoM, 3); // re-center mesh at CoM if (type==mjVOLUME_MESH || validvolume_<=0) { for (int i=0; igeom.density*vol / (type==mjSHELL_MESH ? 12 : 20) * ( 2*(D[k[j][0]] * D[k[j][1]] + E[k[j][0]] * E[k[j][1]] + F[k[j][0]] * F[k[j][1]]) + D[k[j][0]] * E[k[j][1]] + D[k[j][1]] * E[k[j][0]] + D[k[j][0]] * F[k[j][1]] + D[k[j][1]] * F[k[j][0]] + E[k[j][0]] * F[k[j][1]] + E[k[j][1]] * F[k[j][0]]); } } // convert from products of inertia to moments of inertia inert[0] = P[1] + P[2]; inert[1] = P[0] + P[2]; inert[2] = P[0] + P[1]; inert[3] = -P[3]; inert[4] = -P[4]; inert[5] = -P[5]; // get quaternion and diagonal inertia mjtNum eigval[3], eigvec[9], quattmp[4]; mjtNum full[9] = { inert[0], inert[3], inert[4], inert[3], inert[1], inert[5], inert[4], inert[5], inert[2] }; mju_eig3(eigval, eigvec, quattmp, full); // check eigval - SHOULD NOT OCCUR if (eigval[2]<=0) { valideigenvalue_ = false; return; } if (eigval[0] + eigval[1] < eigval[2] || eigval[0] + eigval[2] < eigval[1] || eigval[1] + eigval[2] < eigval[0]) { validinequality_ = false; return; } // compute sizes of equivalent inertia box double mass = GetVolumeRef(type) * def->geom.density; double* boxsz = GetInertiaBoxPtr(type); boxsz[0] = sqrt(6*(eigval[1]+eigval[2]-eigval[0])/mass)/2; boxsz[1] = sqrt(6*(eigval[0]+eigval[2]-eigval[1])/mass)/2; boxsz[2] = sqrt(6*(eigval[0]+eigval[1]-eigval[2])/mass)/2; // if volume was valid, copy volume quat to shell and stop, // otherwise use shell quat for coordinate transformations if (type==mjSHELL_MESH && validvolume_>0) { mju_copy4(GetQuatPtr(type), GetQuatPtr(mjVOLUME_MESH)); continue; } // rotate vertices and normals into axis-aligned frame mju_copy4(GetQuatPtr(type), quattmp); double neg[4] = {quattmp[0], -quattmp[1], -quattmp[2], -quattmp[3]}; double mat[9]; mjuu_quat2mat(mat, neg); for (int i=0; i=0 || invalidorientation_.second>=0) throw mjCError(this, "faces of mesh '%s' have inconsistent orientation. Please check the " "faces containing the vertices %d and %d.", name.c_str(), invalidorientation_.first, invalidorientation_.second); if (!validarea_ && type==mjSHELL_MESH) throw mjCError(this, "mesh surface area is too small: %s", name.c_str()); if (validvolume_<0 && type==mjVOLUME_MESH) throw mjCError(this, "mesh volume is negative (misoriented triangles): %s", name.c_str()); if (!validvolume_ && type==mjVOLUME_MESH) throw mjCError(this, "mesh volume is too small: %s", name.c_str()); if (!valideigenvalue_) throw mjCError(this, "eigenvalue of mesh inertia must be positive: %s", name.c_str()); if (!validinequality_) throw mjCError(this, "eigenvalues of mesh inertia violate A + B >= C: %s", name.c_str()); } // get inertia pointer double* mjCMesh::GetInertiaBoxPtr(mjtMeshType type) { CheckMesh(type); return type==mjSHELL_MESH ? boxsz_surface_ : boxsz_volume_; } double& mjCMesh::GetVolumeRef(mjtMeshType type) { CheckMesh(type); return type==mjSHELL_MESH ? surface_ : volume_; } // make graph describing convex hull void mjCMesh::MakeGraph(void) { int adr, ok, curlong, totlong, exitcode; double* data; facetT* facet, **facetp; vertexT* vertex, *vertex1, **vertex1p; char qhopt[10] = "qhull Qt"; // graph not needed for small meshes if (nvert_ < 4) { return; } // convert mesh data to double data = (double*) mju_malloc(3*nvert_*sizeof(double)); if (!data) { throw mjCError(this, "could not allocate data for qhull"); } for (int i=0; i<3*nvert_; i++) { if (!std::isfinite(vert_[i])) { mju_free(data); throw mjCError(this, "vertex coordinate %d is not finite", NULL, i); } data[i] = (double)vert_[i]; } qhT qh_qh; qhT* qh = &qh_qh; qh_zero(qh, stderr); // qhull basic init qh_init_A(qh, stdin, stdout, stderr, 0, NULL); // install longjmp error handler exitcode = setjmp(qh->errexit); qh->NOerrexit = false; if (!exitcode) { // actual init qh_initflags(qh, qhopt); qh_init_B(qh, data, nvert_, 3, False); // construct convex hull qh_qhull(qh); qh_triangulate(qh); qh_vertexneighbors(qh); // allocate graph: // numvert, numface, vert_edgeadr[numvert], vert_globalid[numvert], // edge_localid[numvert+3*numface], face_globalid[3*numface] int numvert = qh->num_vertices; int numface = qh->num_facets; szgraph_ = 2 + 3*numvert + 6*numface; graph_ = (int*) mju_malloc(szgraph_*sizeof(int)); graph_[0] = numvert; graph_[1] = numface; // pointers for convenience int* vert_edgeadr = graph_ + 2; int* vert_globalid = graph_ + 2 + numvert; int* edge_localid = graph_ + 2 + 2*numvert; int* face_globalid = graph_ + 2 + 3*numvert + 3*numface; // fill in graph data int i = adr = 0; ok = 1; FORALLvertices { // point id of this vertex, check int pid = qh_pointid(qh, vertex->point); if (pid<0 || pid>=nvert_) { ok = 0; break; } // save edge address and global id of this vertex vert_edgeadr[i] = adr; vert_globalid[i] = pid; // process neighboring faces and their vertices int start = adr; FOREACHsetelement_(facetT, vertex->neighbors, facet) { int cnt = 0; FOREACHsetelement_(vertexT, facet->vertices, vertex1) { cnt++; // point id of face vertex, check int pid1 = qh_pointid(qh, vertex1->point); if (pid1<0 || pid1>=nvert_) { ok = 0; break; } // if different from vertex id, try to insert if (pid!=pid1) { // check for previous record int j; for (j=start; j=adr) { edge_localid[adr++] = pid1; } } } // make sure we have triangle: SHOULD NOT OCCUR if (cnt!=3) { mju_error("Qhull did not return triangle"); } } // insert separator, advance to next vertex edge_localid[adr++] = -1; i++; } // size check: SHOULD NOT OCCUR if (adr!=numvert+3*numface) { mju_error("Wrong size in convex hull graph"); } // add triangle data, reorient faces if flipped adr = 0; FORALLfacets { int ii = 0; int ind[3] = {0, 1, 2}; if (facet->toporient) { ind[0] = 1; ind[1] = 0; } // copy triangle data FOREACHsetelement_(vertexT, facet->vertices, vertex1) { // make sure we have triangle: SHOULD NOT OCCUR if (ii>=3) { mju_error("Qhull did not return triangle"); } face_globalid[adr + ind[ii++]] = qh_pointid(qh, vertex1->point); } // advance to next triangle adr += 3; } // free all qh_freeqhull(qh, !qh_ALL); qh_memfreeshort(qh, &curlong, &totlong); mju_free(data); // bad graph: delete if (!ok) { szgraph_ = 0; mju_free(graph_); graph_ = 0; mju_warning("Could not construct convex hull graph"); } // replace global ids with local ids in edge data for (int i=0; i=0) { // search vert_globalid for match int adr; for (adr=0; adr=numvert) { mju_error("Vertex id not found in convex hull"); } } } } // longjmp error handler else { // free all qh_freeqhull(qh, !qh_ALL); qh_memfreeshort(qh, &curlong, &totlong); mju_free(data); if (graph_) { mju_free(graph_); szgraph_ = 0; } throw mjCError(this, "qhull error"); } } // copy graph into face data void mjCMesh::CopyGraph(void) { // only if face data is missing if (face_) { return; } // get info from graph, allocate int numvert = graph_[0]; nface_ = graph_[1]; face_ = (int*) mju_malloc(3*nface_*sizeof(int)); // copy faces for (int i=0; imjMINVAL) for (int j=0; j<3; j++) { normal_[3*i+j] /= len; } else { normal_[3*i] = normal_[3*i+1] = 0; normal_[3*i+2] = 1; } } } // compute face circumradii void mjCMesh::MakeCenter(void) { if (center_) { return; } // allocate and clear center_ = (double*) mju_malloc(3*nface_*sizeof(double)); memset(center_, 0, 3*nface_*sizeof(double)); for (int i=0; istrippath) { file = mjuu_strippath(file); } // load SKN string ext = mjuu_getext(file); if (strcasecmp(ext.c_str(), ".skn")) { throw mjCError(this, "Unknown skin file type: %s", file.c_str()); } string filename = mjuu_makefullname(model->modelfiledir, model->meshdir, file); mjResource* resource = LoadResource(filename, vfs); try { LoadSKN(resource); mju_closeResource(resource); } catch(mjCError err) { mju_closeResource(resource); throw err; } } // make sure all data is present if (vert.empty() || face.empty() || bodyname.empty() || bindpos.empty() || bindquat.empty() || vertid.empty() || vertweight.empty()) { throw mjCError(this, "Missing data in skin"); } // check mesh sizes if (vert.size()%3) { throw mjCError(this, "Vertex data must be multiple of 3"); } if (!texcoord.empty() && texcoord.size()!=2*vert.size()/3) { throw mjCError(this, "Vertex and texcoord data incompatible size"); } if (face.size()%3) { throw mjCError(this, "Face data must be multiple of 3"); } // check bone sizes size_t nbone = bodyname.size(); if (bindpos.size()!=3*nbone) { throw mjCError(this, "Unexpected bindpos size in skin"); } if (bindquat.size()!=4*nbone) { throw mjCError(this, "Unexpected bindquat size in skin"); } if (vertid.size()!=nbone) { throw mjCError(this, "Unexpected vertid size in skin"); } if (vertweight.size()!=nbone) { throw mjCError(this, "Unexpected vertweight size in skin"); } // resolve body names bodyid.resize(nbone); for (int i=0; iFindObject(mjOBJ_BODY, bodyname[i]); if (!pbody) { throw mjCError(this, "unknown body '%s' in skin", bodyname[i].c_str()); } bodyid[i] = pbody->id; } // resolve material name mjCBase* pmat = model->FindObject(mjOBJ_MATERIAL, material); if (pmat) { matid = pmat->id; } else if (!material.empty()) { throw mjCError(this, "unkown material '%s' in skin", material.c_str()); } // set total vertex weights to 0 vector vw; size_t nvert = vert.size()/3; vw.resize(nvert); fill(vw.begin(), vw.end(), 0.0f); // accumulate vertex weights from all bones for (int i=0; i=nvert) { throw mjCError(this, "vertid %d out of range in skin", NULL, jj); } // accumulate vw[jj] += vertweight[i][j]; } } // check coverage for (int i=0; iname); } else if (!buffer_sz) { throw mjCError(this, "SKN file '%s' is empty", resource->name); } // make sure header is present if (buffer_sz<16) { throw mjCError(this, "missing header in SKN file '%s'", resource->name); } // get sizes from header int nvert = ((int*)buffer)[0]; int ntexcoord = ((int*)buffer)[1]; int nface = ((int*)buffer)[2]; int nbone = ((int*)buffer)[3]; // negative sizes not allowed if (nvert<0 || ntexcoord<0 || nface<0 || nbone<0) { throw mjCError(this, "negative size in header of SKN file '%s'", resource->name); } // make sure we have data for vert, texcoord, face if (buffer_sz < 16 + 12*nvert + 8*ntexcoord + 12*nface) { throw mjCError(this, "insufficient data in SKN file '%s'", resource->name); } // data pointer and counter float* pdata = (float*)(buffer+16); int cnt = 0; // copy vert if (nvert) { vert.resize(3*nvert); memcpy(vert.data(), pdata+cnt, 3*nvert*sizeof(float)); cnt += 3*nvert; } // copy texcoord if (ntexcoord) { texcoord.resize(2*ntexcoord); memcpy(texcoord.data(), pdata+cnt, 2*ntexcoord*sizeof(float)); cnt += 2*ntexcoord; } // copy face if (nface) { face.resize(3*nface); memcpy(face.data(), pdata+cnt, 3*nface*sizeof(int)); cnt += 3*nface; } // allocate bone arrays bodyname.clear(); bindpos.resize(3*nbone); bindquat.resize(4*nbone); vertid.resize(nbone); vertweight.resize(nbone); // read bones for (int i=0; iname, i); } // read name char txt[40]; strncpy(txt, (char*)(pdata+cnt), 39); txt[39] = '\0'; cnt += 10; bodyname.push_back(txt); // read bindpos memcpy(bindpos.data()+3*i, pdata+cnt, 3*sizeof(float)); cnt += 3; // read bind quat memcpy(bindquat.data()+4*i, pdata+cnt, 4*sizeof(float)); cnt += 4; // read vertex count int vcount = *(int*)(pdata+cnt); cnt += 1; // check for negative if (vcount<1) { throw mjCError(this, "vertex count must be positive in SKN file '%s', bone %d", resource->name, i); } // check size if (buffer_sz/4-4-cnt < 2*vcount) { throw mjCError(this, "insufficient vertex data in SKN file '%s', bone %d", resource->name, i); } // read vertid vertid[i].resize(vcount); memcpy(vertid[i].data(), (int*)(pdata+cnt), vcount*sizeof(int)); cnt += vcount; // read vertweight vertweight[i].resize(vcount); memcpy(vertweight[i].data(), (int*)(pdata+cnt), vcount*sizeof(int)); cnt += vcount; } // check final size if (buffer_sz != 16+4*cnt) { throw mjCError(this, "unexpected buffer size in SKN file '%s'", resource->name); } } //------------------ class mjCFlex implementation -------------------------------------------------- // hash function for std::pair struct PairHash { template std::size_t operator() (const std::pair& pair) const { return std::hash()(pair.first) ^ std::hash()(pair.second); } }; // simplex connectivity constexpr int kNumEdges[3] = {1, 3, 6}; constexpr int eledge[3][6][2] = {{{ 0, 1}, {-1, -1}, {-1, -1}, {-1, -1}, {-1, -1}, {-1, -1}}, {{ 1, 2}, { 2, 0}, { 0, 1}, {-1, -1}, {-1, -1}, {-1, -1}}, {{ 0, 1}, { 1, 2}, { 2, 0}, { 2, 3}, { 0, 3}, { 1, 3}}}; // constructor mjCFlex::mjCFlex(mjCModel* _model) { // set model model = _model; // set contact defaults contype = 1; conaffinity = 1; condim = 3; priority = 0; mjuu_setvec(friction, 1, 0.005, 0.0001); solmix = 1.0; mj_defaultSolRefImp(solref, solimp); margin = 0; gap = 0; // set other defaults dim = 2; radius = 0.005; internal = true; flatskin = false; selfcollide = mjFLEXSELF_AUTO; activelayers = 1; group = 0; edgestiffness = 0; edgedamping = 0; material.clear(); rgba[0] = rgba[1] = rgba[2] = 0.5f; rgba[3] = 1.0f; // clear internal variables nvert = 0; nedge = 0; nelem = 0; matid = -1; rigid = false; centered = false; } bool mjCFlex::HasTexcoord() const { return !texcoord.empty(); } void mjCFlex::DelTexcoord() { texcoord.clear(); } // compiler void mjCFlex::Compile(const mjVFS* vfs) { // set nelem; check sizes if (dim<1 || dim>3) { throw mjCError(this, "dim must be 1, 2 or 3"); } if (elem.empty()) { throw mjCError(this, "elem is empty"); } if (elem.size() % (dim+1)) { throw mjCError(this, "elem size must be multiple of (dim+1)"); } if (vertbody.empty()) { throw mjCError(this, "vertbody is empty"); } if (vert.size() % 3) { throw mjCError(this, "vert size must be a multiple of 3"); } if (edgestiffness>0 && dim>1) { throw mjCError(this, "edge stiffness only available for dim=1, please use elasticity plugins"); } nelem = (int)elem.size()/(dim+1); // set nvert, rigid, centered; check size if (vert.empty()) { centered = true; nvert = (int)vertbody.size(); } else { nvert = (int)vert.size()/3; if (vertbody.size()==1) { rigid = true; } } if (nvert=nvert) { throw mjCError(this, "elem vertex id out of range"); } } // check texcoord if (!texcoord.empty() && texcoord.size()!=2*nvert) { throw mjCError(this, "two texture coordinates per vertex expected"); } // resolve material name mjCBase* pmat = model->FindObject(mjOBJ_MATERIAL, material); if (pmat) { matid = pmat->id; } else if (!material.empty()) { throw mjCError(this, "unkown material '%s' in flex", material.c_str()); } // resolve body ids for (int i=0; i<(int)vertbody.size(); i++) { mjCBase* pbody = model->FindObject(mjOBJ_BODY, vertbody[i]); if (pbody) { vertbodyid.push_back(pbody->id); } else { throw mjCError(this, "unkown body '%s' in flex", vertbody[i].c_str()); } } // process elements for (int e=0; e<(int)elem.size()/(dim+1); e++) { // make sorted copy of element vector el; el.assign(elem.begin()+e*(dim+1), elem.begin()+(e+1)*(dim+1)); std::sort(el.begin(), el.end()); // check for repeated vertices for (int k=0; k (3*nvert); for (int i=0; ibodies[b]->xpos0); // add vertex offset within body if not centered if (!centered) { mjtNum offset[3]; mju_rotVecQuat(offset, vert.data()+3*i, model->bodies[b]->xquat0); mju_addTo3(vertxpos.data()+3*i, offset); } } // reorder tetrahedra so right-handed face orientation is outside // faces are (0,1,2); (0,2,3); (0,3,1); (1,3,2) if (dim==3) { for (int e=0; e0) { // flip orientation int tmp = elem[e*(dim+1)+1]; elem[e*(dim+1)+1] = elem[e*(dim+1)+2]; elem[e*(dim+1)+2] = tmp; } } } // create edges std::vector edgeidx(elem.size()*kNumEdges[dim-1]); // map from edge vertices to their index in `edges` vector std::unordered_map, int, PairHash> edge_indices; // insert local edges into global vector for (int f = 0; f < (int)elem.size()/(dim+1); f++) { int* v = elem.data() + f*(dim+1); for (int e = 0; e < kNumEdges[dim-1]; e++) { auto pair = std::pair( std::min(v[eledge[dim-1][e][0]], v[eledge[dim-1][e][1]]), std::max(v[eledge[dim-1][e][0]], v[eledge[dim-1][e][1]]) ); // if edge is already present in the vector only store its index auto [it, inserted] = edge_indices.insert({pair, nedge}); if (inserted) { edge.push_back(pair); edgeidx[f*kNumEdges[dim-1]+e] = nedge++; } else { edgeidx[f*kNumEdges[dim-1]+e] = it->second; } } } // set size nedge = (int)edge.size(); // add plugins std::string userface, useredge; mjXUtil::Vector2String(userface, elem); mjXUtil::Vector2String(useredge, edgeidx); for (int i=0; i<(int)vertbodyid.size(); i++) { if (model->bodies[vertbodyid[i]]->plugin_instance) { model->bodies[vertbodyid[i]]->plugin_instance->config_attribs["face"] = userface; model->bodies[vertbodyid[i]]->plugin_instance->config_attribs["edge"] = useredge; } } // create shell fragments and element-vertex collision pairs CreateShellPair(); // create bounding volume hierarchy CreateBVH(); } // create flex BVH void mjCFlex::CreateBVH(void) { int nbvh = 0; // allocate element bounding boxes elemaabb.resize(6*nelem); tree.AllocateBoundingVolumes(nelem); // construct element bounding boxes, add to hierarchy for (int e=0; e=activelayers) { continue; } // compute min and max along each global axis mjtNum xmin[3], xmax[3]; mju_copy3(xmin, vertxpos.data() + 3*edata[0]); mju_copy3(xmax, vertxpos.data() + 3*edata[0]); for (int i=1; i<=dim; i++) { for (int j=0; j<3; j++) { xmin[j] = mjMIN(xmin[j], vertxpos[3*edata[i]+j]); xmax[j] = mjMAX(xmax[j], vertxpos[3*edata[i]+j]); } } // compute aabb (center, size) elemaabb[6*e+0] = 0.5*(xmax[0]+xmin[0]); elemaabb[6*e+1] = 0.5*(xmax[1]+xmin[1]); elemaabb[6*e+2] = 0.5*(xmax[2]+xmin[2]); elemaabb[6*e+3] = 0.5*(xmax[0]-xmin[0]) + radius; elemaabb[6*e+4] = 0.5*(xmax[1]-xmin[1]) + radius; elemaabb[6*e+5] = 0.5*(xmax[2]-xmin[2]) + radius; // add bounding volume for this element mjCBoundingVolume* bv = tree.GetBoundingVolume(nbvh++); bv->contype = contype; bv->conaffinity = conaffinity; bv->quat = NULL; bv->SetId(e); bv->aabb = elemaabb.data() + 6*e; bv->pos = bv->aabb; } // create hierarchy tree.RemoveInactiveVolumes(nbvh); tree.CreateBVH(); } // create shells and element-vertex collision pairs void mjCFlex::CreateShellPair(void) { vector> fragspec(nelem*(dim+1)); // [sorted frag vertices, elem, original frag vertices] vector> connectspec; // [elem1, elem2, common sorted frag vertices] vector border(nelem, false); // is element on the border vector borderfrag(nelem*(dim+1), false); // is fragment on the border // make fragspec for (int e=0; e el; el.assign(elem.begin()+n, elem.begin()+n+dim+1); // line: 2 vertex fragments if (dim==1) { fragspec[n].push_back(el[0]); fragspec[n].push_back(e); fragspec[n].push_back(el[0]); fragspec[n+1].push_back(el[1]); fragspec[n+1].push_back(e); fragspec[n+1].push_back(el[1]); } // triangle: 3 edge fragments else if (dim==2) { fragspec[n].push_back(el[0]); fragspec[n].push_back(el[1]); fragspec[n].push_back(e); fragspec[n].push_back(el[0]); fragspec[n].push_back(el[1]); fragspec[n+2].push_back(el[1]); fragspec[n+2].push_back(el[2]); fragspec[n+2].push_back(e); fragspec[n+2].push_back(el[1]); fragspec[n+2].push_back(el[2]); fragspec[n+1].push_back(el[2]); fragspec[n+1].push_back(el[0]); fragspec[n+1].push_back(e); fragspec[n+1].push_back(el[2]); fragspec[n+1].push_back(el[0]); } // tetrahedron: 4 face fragments else { fragspec[n].push_back(el[0]); fragspec[n].push_back(el[1]); fragspec[n].push_back(el[2]); fragspec[n].push_back(e); fragspec[n].push_back(el[0]); fragspec[n].push_back(el[1]); fragspec[n].push_back(el[2]); fragspec[n+2].push_back(el[0]); fragspec[n+2].push_back(el[2]); fragspec[n+2].push_back(el[3]); fragspec[n+2].push_back(e); fragspec[n+2].push_back(el[0]); fragspec[n+2].push_back(el[2]); fragspec[n+2].push_back(el[3]); fragspec[n+1].push_back(el[0]); fragspec[n+1].push_back(el[3]); fragspec[n+1].push_back(el[1]); fragspec[n+1].push_back(e); fragspec[n+1].push_back(el[0]); fragspec[n+1].push_back(el[3]); fragspec[n+1].push_back(el[1]); fragspec[n+3].push_back(el[1]); fragspec[n+3].push_back(el[3]); fragspec[n+3].push_back(el[2]); fragspec[n+3].push_back(e); fragspec[n+3].push_back(el[1]); fragspec[n+3].push_back(el[3]); fragspec[n+3].push_back(el[2]); } } // sort first segment of each fragspec if (dim>1) { for (int n=0; n previous = {fragspec[n-1].begin(), fragspec[n-1].begin()+dim}; vector current = {fragspec[n].begin(), fragspec[n].begin()+dim}; // same sequential fragments if (previous==current) { // found pair of elements connected by common fragment vector connect; connect.insert(connect.end(), fragspec[n-1][dim]); connect.insert(connect.end(), fragspec[n][dim]); connect.insert(connect.end(), fragspec[n].begin(), fragspec[n].begin()+dim); connectspec.push_back(connect); // count same sequential fragments cnt++; } // different sequential fragments else { // found border fragment if (cnt==1) { border[fragspec[n-1][dim]] = true; borderfrag[n-1] = true; } // reset count cnt = 1; } } // last fragment is border if (cnt==1) { int n = nelem*(dim+1); border[fragspec[n-1][dim]] = true; borderfrag[n-1] = true; } // create shell for (int i=0; i<(int)borderfrag.size(); i++) { if (borderfrag[i]) { // add fragment vertices, in original order shell.insert(shell.end(), fragspec[i].begin()+dim+1, fragspec[i].end()); } } // compute elemlayer (distance from border) via value iteration in 3D if (dim<3) { elemlayer = vector (nelem, 0); } else { elemlayer = vector (nelem, nelem+1); // init with greater than max value for (int e=0; eelemlayer[e2]+1) { elemlayer[e1] = elemlayer[e2]+1; // better value found for e1: update change = true; } else if (elemlayer[e2]>elemlayer[e1]+1) { elemlayer[e2] = elemlayer[e1]+1; // better value found for e2: update change = true; } } } } // create evpairs in 1D and 2D if (dim<3) { // process connected element pairs containing a border element for (int n=0; n<(int)connectspec.size(); n++) { if (border[connectspec[n][0]] || border[connectspec[n][1]]) { // extract common fragment vector frag = {connectspec[n].begin()+2, connectspec[n].end()}; // process both elements for (int ei=0; ei<2; ei++) { const int* edata = elem.data() + connectspec[n][ei]*(dim+1); // find element vertex that is not in the common fragment for (int i=0; i<=dim; i++) { if (frag.end() == std::find(frag.begin(), frag.end(), edata[i])) { // add ev pair, involving the other element in connectspec evpair.push_back(connectspec[n][1-ei]); evpair.push_back(edata[i]); // one such vertex exists break; } } } } } } }