Files
Mujoco_WASM/src/user/user_mesh.cc
T
Alessio Quaglino fb8f77df74 Throw error if qhull is called with NaNs.
PiperOrigin-RevId: 601745538
Change-Id: I590673ca7975e4abc171f98704a6c8f8d08f8327
2024-01-26 06:13:27 -08:00

2715 lines
72 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 <cmath>
#include <csetjmp>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#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 <MC.h>
#if defined(__clang__)
#pragma clang diagnostic pop
#elif defined(__GNUC__)
#pragma GCC diagnostic pop
#endif
#include <mujoco/mjmacro.h>
#include <mujoco/mjmodel.h>
#include <mujoco/mjtnum.h>
#include <mujoco/mjplugin.h>
#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 <tiny_obj_loader.h>
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<mjMINVAL) {
return 0;
}
// normalize
normal[0] /= len;
normal[1] /= len;
normal[2] /= len;
// return area
return len/2;
}
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;
}
}
// 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));
}
//------------------ 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<std::string>&& content_type) {
if (content_type.has_value()) {
content_type_ = std::move(content_type.value());
}
}
void mjCMesh::set_file(std::optional<std::string>&& file) {
if (file.has_value()) {
file_ = std::move(file.value());
}
}
void mjCMesh::set_refpos(std::optional<std::array<double, 3>> refpos) {
if (refpos.has_value()) {
std::copy(refpos.value().begin(), refpos.value().end(), refpos_);
}
}
void mjCMesh::set_refquat(std::optional<std::array<double, 4>> refquat) {
if (refquat.has_value()) {
std::copy(refquat.value().begin(), refquat.value().end(), refquat_);
}
}
void mjCMesh::set_scale(std::optional<std::array<double, 3>> scale) {
if (scale.has_value()) {
set_scale(scale.value());
}
}
void mjCMesh::set_uservert(std::optional<std::vector<float>>&& uservert) {
if (uservert.has_value()) {
uservert_ = std::move(uservert.value());
}
}
void mjCMesh::set_usernormal(std::optional<std::vector<float>>&& usernormal) {
if (usernormal.has_value()) {
usernormal_ = std::move(usernormal.value());
}
}
void mjCMesh::set_usertexcoord(std::optional<std::vector<float>>&& usertexcoord) {
if (usertexcoord.has_value()) {
usertexcoord_ = std::move(usertexcoord.value());
}
}
void mjCMesh::set_userface(std::optional<std::vector<int>>&& 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<double, 3> 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<mjtNum> attributes(plugin->nattribute, 0);
std::vector<const char*> names(plugin->nattribute, 0);
std::vector<const char*> 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<float> uservert;
std::vector<float> usernormal;
std::vector<int> 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<userface_.size(); i++) {
if (userface_[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; i<userface_.size()/3; i++) {
int v0 = userface_[3*i+0];
int v1 = userface_[3*i+1];
int v2 = userface_[3*i+2];
mjtNum normal[3];
if (_triangle(normal, nullptr, vert_+3*v0, vert_+3*v1, vert_+3*v2)>sqrt(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; i<nface_; i++) {
SetBoundingVolume(i);
}
tree_.CreateBVH();
}
}
// get bounding volume
void mjCMesh::SetBoundingVolume(int faceid) {
mjCBoundingVolume* node = tree_.GetBoundingVolume(faceid);
node->SetId(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; i<nvert_; i++) {
double v[3] = {vert_[3*i], vert_[3*i+1], vert_[3*i+2]};
double dst = mjuu_dist3(v, cen);
geom->size[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; i<nvert_; i++) {
double v[3] = {vert_[3*i], vert_[3*i+1], vert_[3*i+2]};
double dst = sqrt((v[0]-cen[0])*(v[0]-cen[0]) +
(v[1]-cen[1])*(v[1]-cen[1]));
geom->size[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; i<nvert_; i++) {
// get distance in XY and Z
double v[3] = {vert_[3*i], vert_[3*i+1], vert_[3*i+2]};
double dst = sqrt((v[0]-cen[0])*(v[0]-cen[0]) +
(v[1]-cen[1])*(v[1]-cen[1]));
double dst2 = fabs(v[2]-cen[2]);
// get spherical elevation at horizontal distance dst
double h = geom->size[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<int[]>(new int[nvert_]);
auto redirect = std::unique_ptr<int[]>(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_; i++) {
int j = i;
while (redirect[j]!=j) {
j = redirect[j];
}
redirect[i] = j;
}
// find good vertices, compress, reuse index to save compressed position
int j = 0;
for (int i=0; i<nvert_; i++) {
if (redirect[i]==i) {
index[i] = j;
memcpy(vert_+3*j, vert_+3*i, 3*sizeof(float));
j++;
} else {
index[i] = -1;
}
}
// recompute face data to reflect compressed vertices
for (int i=0; i<3*nface_; i++) {
face_[i] = index[redirect[face_[i]]];
// sanity check, SHOULD NOT OCCUR
if (face_[i]<0 || face_[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<tinyobj::index_t> 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; i<usertexcoord_.size()/2; i++) {
usertexcoord_[2*i+1] = 1-usertexcoord_[2*i+1];
}
}
// load STL binary mesh
void mjCMesh::LoadSTL(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 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; i<nface_; i++) {
for (int j=0; j<3; j++) {
// read vertex coordinates
float v[3];
ReadFromBuffer(&v, stl+50*i+12*(j+1));
for (int k=0; k < 3; k++) {
if (std::isnan(v[k]) || std::isinf(v[k])) {
throw mjCError(this, "STL file '%s' contains invalid vertices.",
resource->name);
}
// 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<UnalignedFloat*>(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; i<nface_; i++) {
int tmp = face_[3*i+1];
face_[3*i+1] = face_[3*i+2];
face_[3*i+2] = tmp;
}
}
}
void mjCMesh::ComputeVolume(double CoM[3], mjtMeshType type,
const double facecen[3], bool exactmeshinertia) {
double nrm[3];
double cen[3];
GetVolumeRef(type) = 0;
mjuu_zerovec(CoM, 3);
for (int i=0; i<nface_; i++) {
// get area, normal and center
double a = _triangle(nrm, cen, vert_+3*face_[3*i], vert_+3*face_[3*i+1], vert_+3*face_[3*i+2]);
// compute and add volume
const double vec[3] = {cen[0]-facecen[0], cen[1]-facecen[1], cen[2]-facecen[2]};
double vol = type==mjSHELL_MESH ? a : mjuu_dot3(vec, nrm) * a / 3;
// if legacy computation requested, then always positive
if (!exactmeshinertia && type==mjVOLUME_MESH) {
vol = fabs(vol);
}
// add pyramid com
GetVolumeRef(type) += vol;
for (int j=0; j<3; j++) {
CoM[j] += vol*(cen[j]*3.0/4.0 + facecen[j]/4.0);
}
}
}
// apply transformations
void mjCMesh::ApplyTransformations() {
// translate
if (refpos_[0]!=0 || refpos_[1]!=0 || refpos_[2]!=0) {
// prepare translation
float rp[3] = {(float)refpos_[0], (float)refpos_[1], (float)refpos_[2]};
// process vertices
for (int i=0; i<nvert_; i++) {
vert_[3*i] -= rp[0];
vert_[3*i+1] -= rp[1];
vert_[3*i+2] -= rp[2];
}
}
// rotate
if (refquat_[0]!=1 || refquat_[1]!=0 || refquat_[2]!=0 || refquat_[3]!=0) {
// prepare rotation
mjtNum quat[4] = {refquat_[0], refquat_[1], refquat_[2], refquat_[3]};
mjtNum mat[9];
mju_normalize4(quat);
mju_quat2Mat(mat, quat);
// process vertices
for (int i=0; i<nvert_; i++) {
mjtNum p1[3], p0[3] = {vert_[3*i], vert_[3*i+1], vert_[3*i+2]};
mju_rotVecMatT(p1, p0, mat);
vert_[3*i] = (float) p1[0];
vert_[3*i+1] = (float) p1[1];
vert_[3*i+2] = (float) p1[2];
}
// process normals
for (int i=0; i<nnormal_; i++) {
mjtNum n1[3], n0[3] = {normal_[3*i], normal_[3*i+1], normal_[3*i+2]};
mju_rotVecMatT(n1, n0, mat);
normal_[3*i] = (float) n1[0];
normal_[3*i+1] = (float) n1[1];
normal_[3*i+2] = (float) n1[2];
}
}
// scale
if (scale_[0]!=1 || scale_[1]!=1 || scale_[2]!=1) {
for (int i=0; i<nvert_; i++) {
vert_[3*i] *= scale_[0];
vert_[3*i+1] *= scale_[1];
vert_[3*i+2] *= scale_[2];
}
for (int i=0; i<nnormal_; i++) {
normal_[3*i] *= scale_[0];
normal_[3*i+1] *= scale_[1];
normal_[3*i+2] *= scale_[2];
}
}
// normalize normals
for (int i=0; i<nnormal_; i++) {
// compute length
float len = normal_[3*i]*normal_[3*i] + normal_[3*i+1]*normal_[3*i+1] + normal_[3*i+2]*normal_[3*i+2];
// rescale
if (len>mjMINVAL) {
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<nface_; i++) {
// check vertex indices
for (int j=0; j<3; j++) {
if (face_[3*i+j]<0 || face_[3*i+j]>=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; i<nvert_; i++) {
for (int j=0; j<3; j++) {
vert_[3*i+j] -= CoM[j];
}
}
}
// accumulate products of inertia, recompute volume
const int k[6][2] = {{0, 0}, {1, 1}, {2, 2}, {0, 1}, {0, 2}, {1, 2}};
double P[6] = {0, 0, 0, 0, 0, 0};
GetVolumeRef(type) = 0;
for (int i=0; i<nface_; i++) {
float* D = vert_+3*face_[3*i];
float* E = vert_+3*face_[3*i+1];
float* F = vert_+3*face_[3*i+2];
// get area, normal and center; update volume
double a = _triangle(nrm, cen, D, E, F);
double vol = type==mjSHELL_MESH ? a : mjuu_dot3(cen, nrm) * a / 3;
// if legacy computation requested, then always positive
if (!exactmeshinertia && type==mjVOLUME_MESH) {
vol = fabs(vol);
}
// apply formula, accumulate
GetVolumeRef(type) += vol;
for (int j=0; j<6; j++) {
P[j] += def->geom.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<nvert_; i++) {
// vertices
const double vec[3] = {vert_[3*i], vert_[3*i+1], vert_[3*i+2]};
double res[3];
mjuu_mulvecmat(res, vec, mat);
for (int j=0; j<3; j++) {
vert_[3*i+j] = (float) res[j];
// axis-aligned bounding box
aamm_[j+0] = mju_min(aamm_[j+0], res[j]);
aamm_[j+3] = mju_max(aamm_[j+3], res[j]);
}
}
for (int i=0; i<nnormal_; i++) {
// normals
const double nrm[3] = {normal_[3*i], normal_[3*i+1], normal_[3*i+2]};
double res[3];
mjuu_mulvecmat(res, nrm, mat);
for (int j=0; j<3; j++) {
normal_[3*i+j] = (float) res[j];
}
}
}
}
// check that the mesh is valid
void mjCMesh::CheckMesh(mjtMeshType type) {
if (!processed_) {
return;
}
if (invalidorientation_.first>=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; j++)
if (pid1==edge_localid[j]) {
break;
}
// not found: insert
if (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<numvert+3*numface; i++) {
if (edge_localid[i]>=0) {
// search vert_globalid for match
int adr;
for (adr=0; adr<numvert; adr++) {
if (vert_globalid[adr]==edge_localid[i]) {
edge_localid[i] = adr;
break;
}
}
// make sure we found a match: SHOULD NOT OCCUR
if (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; i<nface_; i++) {
// address in graph
int j = 2 + 3*numvert + 3*nface_ + 3*i;
// copy
face_[3*i] = graph_[j];
face_[3*i+1] = graph_[j+1];
face_[3*i+2] = graph_[j+2];
}
}
// compute vertex normals
void mjCMesh::MakeNormal(void) {
// only if normal data is missing
if (normal_) {
return;
}
// allocate and clear normals
nnormal_ = nvert_;
normal_ = (float*) mju_malloc(3*nnormal_*sizeof(float));
memset(normal_, 0, 3*nnormal_*sizeof(float));
if (!facenormal_) {
facenormal_ = (int*) mju_malloc(3*nface_*sizeof(int));
memset(facenormal_, 0, 3*nface_*sizeof(int));
}
// loop over faces, accumulate vertex normals
for (int i=0; i<nface_; i++) {
// get vertex ids
int vertid[3];
for (int j=0; j<3; j++) {
vertid[j] = face_[3*i+j];
}
// get triangle edges
mjtNum vec01[3], vec02[3];
for (int j=0; j<3; j++) {
vec01[j] = vert_[3*vertid[1]+j] - vert_[3*vertid[0]+j];
vec02[j] = vert_[3*vertid[2]+j] - vert_[3*vertid[0]+j];
}
// compute face normal
mjtNum nrm[3];
mju_cross(nrm, vec01, vec02);
mjtNum area = mju_normalize3(nrm);
// add normal to each vertex with weight = area
for (int j=0; j<3; j++) {
for (int k=0; k<3; k++) {
normal_[3*vertid[j]+k] += nrm[k]*area;
}
facenormal_[3*i+j] = vertid[j];
}
}
// remove large-angle faces
if (!smoothnormal_) {
// allocate removal and clear
float* nremove = (float*) mju_malloc(3*nnormal_*sizeof(float));
memset(nremove, 0, 3*nnormal_*sizeof(float));
// remove contributions from faces at large angles with vertex normal
for (int i=0; i<nface_; i++) {
// get vertex ids
int vertid[3];
for (int j=0; j<3; j++) {
vertid[j] = face_[3*i+j];
}
// get triangle edges
mjtNum vec01[3], vec02[3];
for (int j=0; j<3; j++) {
vec01[j] = vert_[3*vertid[1]+j] - vert_[3*vertid[0]+j];
vec02[j] = vert_[3*vertid[2]+j] - vert_[3*vertid[0]+j];
}
// compute face normal
mjtNum nrm[3];
mju_cross(nrm, vec01, vec02);
mjtNum area = mju_normalize3(nrm);
// compare to vertex normal, subtract contribution if dot product too small
for (int j=0; j<3; j++) {
// normalized vertex normal
mjtNum vnrm[3] = {normal_[3*vertid[j]], normal_[3*vertid[j]+1], normal_[3*vertid[j]+2]};
mju_normalize3(vnrm);
// dot too small: remove
if (mju_dot3(nrm, vnrm)<0.8) {
for (int k=0; k<3; k++) {
nremove[3*vertid[j]+k] += nrm[k]*area;
}
}
}
}
// apply removal, free nremove
for (int i=0; i<3*nnormal_; i++) {
normal_[i] -= nremove[i];
}
mju_free(nremove);
}
// normalize normals
for (int i=0; i<nnormal_; i++) {
// compute length
float len = sqrtf(normal_[3*i]*normal_[3*i] +
normal_[3*i+1]*normal_[3*i+1] +
normal_[3*i+2]*normal_[3*i+2]);
// divide by length
if (len>mjMINVAL)
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; i<nface_; i++) {
// get vertex ids
int* vertid = face_ + 3*i;
// get triangle edges
mjtNum a[3], b[3];
for (int j=0; j<3; j++) {
a[j] = vert_[3*vertid[0]+j] - vert_[3*vertid[2]+j];
b[j] = vert_[3*vertid[1]+j] - vert_[3*vertid[2]+j];
}
// compute face normal
mjtNum nrm[3];
mju_cross(nrm, a, b);
// compute circumradius
mjtNum norm_a_2 = mju_dot3(a, a);
mjtNum norm_b_2 = mju_dot3(b, b);
mjtNum area = mju_norm3(nrm);
// compute circumcenter
mjtNum res[3], vec[3] = {
norm_a_2 * b[0] - norm_b_2 * a[0],
norm_a_2 * b[1] - norm_b_2 * a[1],
norm_a_2 * b[2] - norm_b_2 * a[2]
};
mju_cross(res, vec, nrm);
center_[3*i+0] = res[0]/(2*area*area) + vert_[3*vertid[2]+0];
center_[3*i+1] = res[1]/(2*area*area) + vert_[3*vertid[2]+1];
center_[3*i+2] = res[2]/(2*area*area) + vert_[3*vertid[2]+2];
}
}
//------------------ class mjCSkin implementation --------------------------------------------------
// constructor
mjCSkin::mjCSkin(mjCModel* _model) {
// set model pointer
model = _model;
// clear data
file.clear();
material.clear();
rgba[0] = rgba[1] = rgba[2] = 0.5f;
rgba[3] = 1.0f;
inflate = 0;
group = 0;
vert.clear();
texcoord.clear();
face.clear();
bodyname.clear();
bindpos.clear();
bindquat.clear();
vertid.clear();
vertweight.clear();
bodyid.clear();
matid = -1;
}
// destructor
mjCSkin::~mjCSkin() {
file.clear();
material.clear();
vert.clear();
texcoord.clear();
face.clear();
bodyname.clear();
bindpos.clear();
bindquat.clear();
vertid.clear();
vertweight.clear();
bodyid.clear();
}
// compiler
void mjCSkin::Compile(const mjVFS* vfs) {
// load file
if (!file.empty()) {
// make sure data is not present
if (!vert.empty() ||
!texcoord.empty() ||
!face.empty() ||
!bodyname.empty() ||
!bindpos.empty() ||
!bindquat.empty() ||
!vertid.empty() ||
!vertweight.empty() ||
!bodyid.empty()) {
throw mjCError(this, "Data already exists, trying to load from skin file: %s", file.c_str());
}
// remove path from file if necessary
if (model->strippath) {
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; i<nbone; i++) {
mjCBase* pbody = model->FindObject(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<float> 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<nbone; i++) {
// make sure bone has vertices and sizes match
size_t nbv = vertid[i].size();
if (vertweight[i].size()!=nbv || nbv==0) {
throw mjCError(this, "vertid and vertweight must have same non-zero size in skin");
}
// accumulate weights in global array
for (int j=0; j<nbv; j++) {
// get index and check range
int jj = vertid[i][j];
if (jj<0 || jj>=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; i<nvert; i++) {
if (vw[i]<=mjMINVAL) {
throw mjCError(this, "vertex %d must have positive total weight in skin", NULL, i);
}
}
// normalize vertex weights
for (int i=0; i<nbone; i++) {
for (int j=0; j<vertid[i].size(); j++) {
vertweight[i][j] /= vw[vertid[i][j]];
}
}
// normalize bindquat
for (int i=0; i<nbone; i++) {
mjtNum quat[4] = {
(mjtNum)bindquat[4*i],
(mjtNum)bindquat[4*i+1],
(mjtNum)bindquat[4*i+2],
(mjtNum)bindquat[4*i+3]
};
mju_normalize4(quat);
bindquat[4*i] = (float) quat[0];
bindquat[4*i+1] = (float) quat[1];
bindquat[4*i+2] = (float) quat[2];
bindquat[4*i+3] = (float) quat[3];
}
}
// load skin in SKN BIN format
void mjCSkin::LoadSKN(mjResource* resource) {
char* buffer = 0;
int buffer_sz = mju_readResource(resource, (const void**) &buffer);
if (buffer_sz < 0) {
throw mjCError(this, "could not read SKN file '%s'", resource->name);
} 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; i<nbone; i++) {
// check size
if (buffer_sz/4-4-cnt < 18) {
throw mjCError(this, "insufficient data in SKN file '%s', bone %d", resource->name, 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 <class T1, class T2>
std::size_t operator() (const std::pair<T1, T2>& pair) const {
return std::hash<T1>()(pair.first) ^ std::hash<T2>()(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<dim+1) {
throw mjCError(this, "not enough vertices");
}
// check elem vertex ids
for (int i=0; i<(int)elem.size(); i++) {
if (elem[i]<0 || elem[i]>=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<int> 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<dim; k++) {
if (el[k]==el[k+1]) {
throw mjCError(this, "repeated vertex in element");
}
}
}
// determine rigid if not already set
if (!rigid) {
rigid = true;
for (int i=1; i<(int)vertbodyid.size(); i++) {
if (vertbodyid[i]!=vertbodyid[0]) {
rigid = false;
break;
}
}
}
// determine centered if not already set
if (!centered) {
centered = true;
for (int i=0; i<(int)vert.size(); i++) {
if (vert[i]!=0) {
centered = false;
break;
}
}
}
// compute global vertex positions
vertxpos = vector<mjtNum> (3*nvert);
for (int i=0; i<nvert; i++) {
// get body id, set vertxpos = body.xpos0
int b = rigid ? vertbodyid[0] : vertbodyid[i];
mju_copy3(vertxpos.data()+3*i, model->bodies[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; e<nelem; e++) {
const int* edata = elem.data() + e*(dim+1);
mjtNum* v0 = vertxpos.data() + 3*edata[0];
mjtNum* v1 = vertxpos.data() + 3*edata[1];
mjtNum* v2 = vertxpos.data() + 3*edata[2];
mjtNum* v3 = vertxpos.data() + 3*edata[3];
mjtNum v01[3] = {v1[0]-v0[0], v1[1]-v0[1], v1[2]-v0[2]};
mjtNum v02[3] = {v2[0]-v0[0], v2[1]-v0[1], v2[2]-v0[2]};
mjtNum v03[3] = {v3[0]-v0[0], v3[1]-v0[1], v3[2]-v0[2]};
// detect wrong orientation
mjtNum nrm[3];
mju_cross(nrm, v01, v02);
if (mju_dot3(nrm, v03)>0) {
// 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<int> edgeidx(elem.size()*kNumEdges[dim-1]);
// map from edge vertices to their index in `edges` vector
std::unordered_map<std::pair<int, int>, 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<nelem; e++) {
const int* edata = elem.data() + e*(dim+1);
// skip inactive in 3D
if (dim==3 && elemlayer[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<vector<int>> fragspec(nelem*(dim+1)); // [sorted frag vertices, elem, original frag vertices]
vector<vector<int>> connectspec; // [elem1, elem2, common sorted frag vertices]
vector<bool> border(nelem, false); // is element on the border
vector<bool> borderfrag(nelem*(dim+1), false); // is fragment on the border
// make fragspec
for (int e=0; e<nelem; e++) {
int n = e*(dim+1);
// element vertices in original (unsorted) order
vector<int> 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<nelem*(dim+1); n++) {
std::sort(fragspec[n].begin(), fragspec[n].begin()+dim);
}
}
// sort fragspec
std::sort(fragspec.begin(), fragspec.end());
// make border and connectspec, record borderfrag
int cnt = 1;
for (int n=1; n<nelem*(dim+1); n++) {
// extract frag vertices, without elem
vector<int> previous = {fragspec[n-1].begin(), fragspec[n-1].begin()+dim};
vector<int> current = {fragspec[n].begin(), fragspec[n].begin()+dim};
// same sequential fragments
if (previous==current) {
// found pair of elements connected by common fragment
vector<int> 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<int> (nelem, 0);
}
else {
elemlayer = vector<int> (nelem, nelem+1); // init with greater than max value
for (int e=0; e<nelem; e++) {
if (border[e]) {
elemlayer[e] = 0; // set border elements to 0
}
}
bool change = true;
while (change) { // repeat while changes are happening
change = false;
// process edges of element connectivity graph
for (int i=0; i<(int)connectspec.size(); i++) {
int e1 = connectspec[i][0]; // get element pair for this edge
int e2 = connectspec[i][1];
if (elemlayer[e1]>elemlayer[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<int> 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;
}
}
}
}
}
}
}