Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
030a30ea4This file is pinned to the selected release. Download the original to open it in your design tool.
// 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 <climits>
#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>
#include "user/user_api.h"
#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_cache.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) {
mjs_defaultMesh(spec);
elemtype = mjOBJ_MESH;
// 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));
// in case this body is not compiled
CopyFromSpec();
// point to local
PointToLocal();
}
mjCMesh::mjCMesh(const mjCMesh& other) {
*this = other;
}
mjCMesh& mjCMesh::operator=(const mjCMesh& other) {
if (this != &other) {
this->spec = other.spec;
*static_cast<mjCMesh_*>(this) = static_cast<const mjCMesh_&>(other);
*static_cast<mjsMesh*>(this) = static_cast<const mjsMesh&>(other);
if (other.vert_) {
size_t nvert = 3*other.nvert_*sizeof(float);
this->vert_ = (float*)mju_malloc(nvert);
memcpy(this->vert_, other.vert_, nvert);
} else {
this->vert_ = NULL;
}
if (other.normal_) {
size_t nnormal = 3*other.nnormal_*sizeof(float);
this->normal_ = (float*)mju_malloc(nnormal);
memcpy(this->normal_, other.normal_, nnormal);
} else {
this->normal_ = NULL;
}
if (other.center_) {
size_t ncenter = 3*other.nface_*sizeof(double);
this->center_ = (double*)mju_malloc(ncenter);
memcpy(this->center_, other.center_, ncenter);
} else {
this->center_ = NULL;
}
if (other.texcoord_) {
size_t ntexcoord = 2*other.ntexcoord_*sizeof(float);
this->texcoord_ = (float*)mju_malloc(ntexcoord);
memcpy(this->texcoord_, other.texcoord_, ntexcoord);
} else {
this->texcoord_ = NULL;
}
if (other.face_) {
size_t nface = 3*other.nface_*sizeof(int);
this->face_ = (int*)mju_malloc(nface);
memcpy(this->face_, other.face_, nface);
} else {
this->face_ = NULL;
}
if (other.facenormal_) {
size_t nfacenormal = 3*other.nface_*sizeof(int);
this->facenormal_ = (int*)mju_malloc(nfacenormal);
memcpy(this->facenormal_, other.facenormal_, nfacenormal);
} else {
this->facenormal_ = NULL;
}
if (other.facetexcoord_) {
size_t nfacetexcoord = 3*other.nface_*sizeof(int);
this->facetexcoord_ = (int*)mju_malloc(nfacetexcoord);
memcpy(this->facetexcoord_, other.facetexcoord_, nfacetexcoord);
} else {
this->facetexcoord_ = NULL;
}
if (other.graph_) {
size_t szgraph = szgraph_*sizeof(int);
this->graph_ = (int*)mju_malloc(szgraph);
memcpy(this->graph_, other.graph_, szgraph);
} else {
this->graph_ = NULL;
}
if (other.plugin.instance) {
mjCPlugin* new_plugin = new mjCPlugin(*static_cast<mjCPlugin*>(other.plugin.instance));
plugin = new_plugin->spec;
model->plugins.push_back(new_plugin);
}
}
PointToLocal();
return *this;
}
void mjCMesh::PointToLocal() {
spec.element = static_cast<mjElement*>(this);
spec.name = (mjString)&name;
spec.classname = (mjString)&classname;
spec.file = (mjString)&spec_file_;
spec.content_type = (mjString)&spec_content_type_;
spec.uservert = (mjFloatVec)&spec_uservert_;
spec.usernormal = (mjFloatVec)&spec_usernormal_;
spec.userface = (mjIntVec)&spec_userface_;
spec.usertexcoord = (mjFloatVec)&spec_usertexcoord_;
spec.plugin.name = (mjString)&plugin_name;
spec.plugin.instance_name = (mjString)&plugin_instance_name;
spec.info = (mjString)&info;
}
void mjCMesh::CopyFromSpec() {
*static_cast<mjsMesh*>(this) = spec;
file_ = spec_file_;
content_type_ = spec_content_type_;
uservert_ = spec_uservert_;
usernormal_ = spec_usernormal_;
userface_ = spec_userface_;
usertexcoord_ = spec_usertexcoord_;
file = (mjString)&file_;
content_type = (mjString)&content_type_;
uservert = (mjFloatVec)&uservert_;
usernormal = (mjFloatVec)&usernormal_;
userface = (mjIntVec)&userface_;
usertexcoord = (mjFloatVec)&usertexcoord_;
plugin.active = spec.plugin.active;
plugin.instance = spec.plugin.instance;
plugin.name = spec.plugin.name;
plugin.instance_name = spec.plugin.instance_name;
// clear precompiled asset. TODO: use asset cache
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_);
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;
}
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_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);
}
mjCPlugin* plugin_instance = static_cast<mjCPlugin*>(plugin.instance);
model->ResolvePlugin(this, plugin_name, plugin_instance_name, &plugin_instance);
plugin.instance = plugin_instance;
const mjpPlugin* pplugin = mjp_getPluginAtSlot(plugin_instance->spec.plugin_slot);
if (!(pplugin->capabilityflags & mjPLUGIN_SDF)) {
throw mjCError(this, "plugin '%s' does not support signed distance fields", pplugin->name);
}
std::vector<mjtNum> attributes(pplugin->nattribute, 0);
std::vector<const char*> names(pplugin->nattribute, 0);
std::vector<const char*> values(pplugin->nattribute, 0);
for (int i=0; i < pplugin->nattribute; i++) {
names[i] = pplugin->attributes[i];
values[i] = plugin_instance->config_attribs[names[i]].c_str();
}
if (pplugin->sdf_attribute) {
pplugin->sdf_attribute(attributes.data(), names.data(), values.data());
}
mjtNum aabb[6] = {0};
pplugin->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] = pplugin->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));
}
uservert_ = std::move(uservert);
usernormal_ = std::move(usernormal);
userface_ = std::move(userface);
delete[] field;
}
// compiler
void mjCMesh::Compile(const mjVFS* vfs) {
CopyFromSpec();
visual_ = true;
// 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 (plugin.active) {
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, "in face %d, vertex index %d does not exist",
nullptr, i / 3, userface_[i]);
}
}
// 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(mjtGeomInertia type) {
if (type==mjINERTIA_SHELL) {
return pos_surface_;
} else {
return pos_volume_;
}
}
// get orientation
double* mjCMesh::GetQuatPtr(mjtGeomInertia type) {
if (type==mjINERTIA_SHELL) {
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;
// try loading from cache
mjCCache *cache = reinterpret_cast<mjCCache*>(mj_globalCache());
if (cache) {
auto asset = cache->Get(resource->name);
if (asset.has_value() &&
!mju_isModifiedResource(resource, asset->Timestamp().c_str())) {
if (LoadCachedOBJ(asset.value())) {
return;
}
}
}
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];
}
// try caching asset
if (cache) {
mjCAsset asset("", resource->name, resource->timestamp);
asset.AddVector("uservert_", uservert_);
asset.AddVector("usernormal_", usernormal_);
asset.AddVector("usertexcoord_", usertexcoord_);
if (!objReader.GetShapes().empty()) {
const auto& mesh = objReader.GetShapes()[0].mesh;
std::vector<int> vertex_index;
vertex_index.reserve(mesh.indices.size());
std::vector<int> normal_index;
normal_index.reserve(mesh.indices.size());
std::vector<int> texcoord_index;
texcoord_index.reserve(mesh.indices.size());
for (tinyobj::index_t index : mesh.indices) {
vertex_index.push_back(index.vertex_index);
normal_index.push_back(index.normal_index);
texcoord_index.push_back(index.texcoord_index);
}
asset.AddVector("num_face_vertices", mesh.num_face_vertices);
asset.AddVector("vertex_index", vertex_index);
asset.AddVector("normal_index", normal_index);
asset.AddVector("texcoord_index", texcoord_index);
} else {
asset.AddVector("num_face_vertices", std::vector<unsigned char>());
asset.AddVector("vertex_index", std::vector<int>());
asset.AddVector("normal_index", std::vector<int>());
asset.AddVector("texcoord_index", std::vector<int>());
}
cache->Insert(std::move(asset));
}
}
// load OBJ from cached asset, return true on success
bool mjCMesh::LoadCachedOBJ(const mjCAsset& asset) {
// check that asset has all data
if (!asset.HasData("uservert_") || !asset.HasData("usernormal_")
|| !asset.HasData("usertexcoord_") || !asset.HasData("num_face_vertices")
|| !asset.HasData("vertex_index") || !asset.HasData("normal_index")
|| !asset.HasData("texcoord_index")) {
return false;
}
uservert_ = asset.GetVector<float>("uservert_").value();
usernormal_ = asset.GetVector<float>("usernormal_").value();
usertexcoord_ = asset.GetVector<float>("usertexcoord_").value();
vector<int> vertex_index = asset.GetVector<int>("vertex_index").value();
vector<int> normal_index = asset.GetVector<int>("normal_index").value();
vector<int> texcoord_index = asset.GetVector<int>("texcoord_index").value();
vector<unsigned char> num_face_vertices =
asset.GetVector<unsigned char>("num_face_vertices").value();
bool righthand = (scale[0] * scale[1] * scale[2]) > 0;
for (int face = 0, i = 0; i < vertex_index.size();) {
int nfacevert = num_face_vertices[face];
if (nfacevert < 3 || nfacevert > 4) {
throw mjCError(
this, "only tri or quad meshes are supported for OBJ (file '%s')",
asset.Id().c_str());
}
userface_.push_back(vertex_index[i]);
userface_.push_back(vertex_index[i + (righthand == 1 ? 1 : 2)]);
userface_.push_back(vertex_index[i + (righthand == 1 ? 2 : 1)]);
if (!usernormal_.empty()) {
userfacenormal_.push_back(normal_index[i]);
userfacenormal_.push_back(normal_index[i + (righthand == 1 ? 1 : 2)]);
userfacenormal_.push_back(normal_index[i + (righthand == 1 ? 2 : 1)]);
}
if (!usertexcoord_.empty()) {
userfacetexcoord_.push_back(texcoord_index[i]);
userfacetexcoord_.push_back(texcoord_index[i + (righthand == 1 ? 1 : 2)]);
userfacetexcoord_.push_back(texcoord_index[i + (righthand == 1 ? 2 : 1)]);
}
if (nfacevert == 4) {
userface_.push_back(vertex_index[i]);
userface_.push_back(vertex_index[i + (righthand == 1 ? 2 : 3)]);
userface_.push_back(vertex_index[i + (righthand == 1 ? 3 : 2)]);
if (!usernormal_.empty()) {
userfacenormal_.push_back(normal_index[i]);
userfacenormal_.push_back(normal_index[i + (righthand == 1 ? 1 : 2)]);
userfacenormal_.push_back(normal_index[i + (righthand == 1 ? 2 : 1)]);
}
if (!usertexcoord_.empty()) {
userfacetexcoord_.push_back(texcoord_index[i]);
userfacetexcoord_.push_back(texcoord_index[i + (righthand == 1 ? 1 : 2)]);
userfacetexcoord_.push_back(texcoord_index[i + (righthand == 1 ? 2 : 1)]);
}
}
i += nfacevert;
++face;
}
return true;
}
// 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);
}
if (nvert_ >= INT_MAX / sizeof(float) / 3 ||
nnormal_ >= INT_MAX / sizeof(float) / 3 ||
ntexcoord_ >= INT_MAX / sizeof(float) / 2 ||
nface_ >= INT_MAX / sizeof(int) / 3) {
throw mjCError(this, "too large 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], mjtGeomInertia 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==mjINERTIA_SHELL ? a : mjuu_dot3(vec, nrm) * a / 3;
// if legacy computation requested, then always positive
if (!exactmeshinertia && type==mjINERTIA_VOLUME) {
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 : { mjtGeomInertia::mjINERTIA_VOLUME, mjtGeomInertia::mjINERTIA_SHELL } ) {
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==mjINERTIA_VOLUME || 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==mjINERTIA_SHELL ? a : mjuu_dot3(cen, nrm) * a / 3;
// if legacy computation requested, then always positive
if (!exactmeshinertia && type==mjINERTIA_VOLUME) {
vol = fabs(vol);
}
// apply formula, accumulate
GetVolumeRef(type) += vol;
for (int j=0; j<6; j++) {
P[j] += def->geom.density*vol /
(type==mjINERTIA_SHELL ? 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==mjINERTIA_SHELL && validvolume_>0) {
mju_copy4(GetQuatPtr(type), GetQuatPtr(mjINERTIA_VOLUME));
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(mjtGeomInertia type) {
if (!processed_) {
return;
}
if ((invalidorientation_.first>=0 || invalidorientation_.second>=0) && model->exactmeshinertia)
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==mjINERTIA_SHELL)
throw mjCError(this, "mesh surface area is too small: %s", name.c_str());
if (validvolume_<0 && type==mjINERTIA_VOLUME)
throw mjCError(this, "mesh volume is negative (misoriented triangles): %s", name.c_str());
if (!validvolume_ && type==mjINERTIA_VOLUME)
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(mjtGeomInertia type) {
CheckMesh(type);
return type==mjINERTIA_SHELL ? boxsz_surface_ : boxsz_volume_;
}
double& mjCMesh::GetVolumeRef(mjtGeomInertia type) {
CheckMesh(type);
return type==mjINERTIA_SHELL ? 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) {
mjs_defaultSkin(spec);
elemtype = mjOBJ_SKIN;
// set model pointer
model = _model;
// clear data
spec_file_.clear();
spec_material_.clear();
spec_vert_.clear();
spec_texcoord_.clear();
spec_face_.clear();
spec_bodyname_.clear();
spec_bindpos_.clear();
spec_bindquat_.clear();
spec_vertid_.clear();
spec_vertweight_.clear();
bodyid.clear();
matid = -1;
// point to local
PointToLocal();
// in case this camera is not compiled
CopyFromSpec();
}
mjCSkin::mjCSkin(const mjCSkin& other) {
*this = other;
}
mjCSkin& mjCSkin::operator=(const mjCSkin& other) {
if (this != &other) {
this->spec = other.spec;
*static_cast<mjCSkin_*>(this) = static_cast<const mjCSkin_&>(other);
*static_cast<mjsSkin*>(this) = static_cast<const mjsSkin&>(other);
}
PointToLocal();
return *this;
}
void mjCSkin::PointToLocal() {
spec.element = static_cast<mjElement*>(this);
spec.name = (mjString)&name;
spec.classname = (mjString)&classname;
spec.file = (mjString)&spec_file_;
spec.material = (mjString)&spec_material_;
spec.vert = (mjFloatVec)&spec_vert_;
spec.texcoord = (mjFloatVec)&spec_texcoord_;
spec.face = (mjIntVec)&spec_face_;
spec.bodyname = (mjStringVec)&spec_bodyname_;
spec.bindpos = (mjFloatVec)&spec_bindpos_;
spec.bindquat = (mjFloatVec)&spec_bindquat_;
spec.vertid = (mjIntVecVec)&spec_vertid_;
spec.vertweight = (mjFloatVecVec)&spec_vertweight_;
spec.info = (mjString)&info;
}
void mjCSkin::NameSpace(const mjCModel* m) {
for (int i=0; i<(int)spec_bodyname_.size(); i++) {
spec_bodyname_[i] = m->prefix + spec_bodyname_[i] + m->suffix;
}
}
void mjCSkin::CopyFromSpec() {
*static_cast<mjsSkin*>(this) = spec;
file_ = spec_file_;
material_ = spec_material_;
vert_ = spec_vert_;
texcoord_ = spec_texcoord_;
face_ = spec_face_;
bodyname_ = spec_bodyname_;
bindpos_ = spec_bindpos_;
bindquat_ = spec_bindquat_;
vertid_ = spec_vertid_;
vertweight_ = spec_vertweight_;
file = (mjString)&spec_file_;
material = (mjString)&spec_material_;
vert = (mjFloatVec)&spec_vert_;
texcoord = (mjFloatVec)&spec_texcoord_;
face = (mjIntVec)&spec_face_;
bodyname = (mjStringVec)&spec_bodyname_;
bindpos = (mjFloatVec)&spec_bindpos_;
bindquat = (mjFloatVec)&spec_bindquat_;
vertid = (mjIntVecVec)&spec_vertid_;
vertweight = (mjFloatVecVec)&spec_vertweight_;
}
// destructor
mjCSkin::~mjCSkin() {
spec_file_.clear();
spec_material_.clear();
spec_vert_.clear();
spec_texcoord_.clear();
spec_face_.clear();
spec_bodyname_.clear();
spec_bindpos_.clear();
spec_bindquat_.clear();
spec_vertid_.clear();
spec_vertweight_.clear();
bodyid.clear();
}
void mjCSkin::ResolveReferences(const mjCModel* m) {
size_t nbone = bodyname_.size();
bodyid.resize(nbone);
for (int i=0; i<nbone; i++) {
mjCBase* pbody = m->FindObject(mjOBJ_BODY, bodyname_[i]);
if (!pbody) {
throw mjCError(this, "unknown body '%s' in skin", bodyname_[i].c_str());
}
bodyid[i] = pbody->id;
}
}
// compiler
void mjCSkin::Compile(const mjVFS* vfs) {
CopyFromSpec();
// 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
ResolveReferences(model);
// 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) {
mjs_defaultFlex(spec);
elemtype = mjOBJ_FLEX;
// set model
model = _model;
// clear internal variables
nvert = 0;
nedge = 0;
nelem = 0;
matid = -1;
rigid = false;
centered = false;
PointToLocal();
CopyFromSpec();
}
mjCFlex::mjCFlex(const mjCFlex& other) {
*this = other;
}
mjCFlex& mjCFlex::operator=(const mjCFlex& other) {
if (this != &other) {
this->spec = other.spec;
*static_cast<mjCFlex_*>(this) = static_cast<const mjCFlex_&>(other);
*static_cast<mjsFlex*>(this) = static_cast<const mjsFlex&>(other);
}
PointToLocal();
return *this;
}
void mjCFlex::PointToLocal() {
spec.element = static_cast<mjElement*>(this);
spec.name = (mjString)&name;
spec.classname = (mjString)&classname;
spec.material = (mjString)&spec_material_;
spec.vertbody = (mjStringVec)&spec_vertbody_;
spec.vert = (mjDoubleVec)&spec_vert_;
spec.texcoord = (mjFloatVec)&spec_texcoord_;
spec.elem = (mjIntVec)&spec_elem_;
spec.info = (mjString)&info;
}
void mjCFlex::NameSpace(const mjCModel* m) {
for (int i=0; i<(int)spec_vertbody_.size(); i++) {
spec_vertbody_[i] = m->prefix + spec_vertbody_[i] + m->suffix;
}
}
void mjCFlex::CopyFromSpec() {
*static_cast<mjsFlex*>(this) = spec;
spec.info = (mjString)&info;
material_ = spec_material_;
vertbody_ = spec_vertbody_;
vert_ = spec_vert_;
texcoord_ = spec_texcoord_;
elem_ = spec_elem_;
material = (mjString)&material_;
vertbody = (mjStringVec)&vertbody_;
vert = (mjDoubleVec)&vert_;
texcoord = (mjFloatVec)&texcoord_;
elem = (mjIntVec)&elem_;
// clear precompiled asset. TODO: use asset cache
nedge = 0;
edge.clear();
shell.clear();
evpair.clear();
}
bool mjCFlex::HasTexcoord() const {
return !texcoord_.empty();
}
void mjCFlex::DelTexcoord() {
texcoord_.clear();
}
void mjCFlex::ResolveReferences(const mjCModel* m) {
for (int i=0; i<(int)vertbody_.size(); i++) {
mjCBase* pbody = m->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());
}
}
}
// compiler
void mjCFlex::Compile(const mjVFS* vfs) {
CopyFromSpec();
// 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
ResolveReferences(model);
// 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) {
mjCPlugin* plugin_instance =
static_cast<mjCPlugin*>(model->bodies[vertbodyid[i]]->plugin.instance);
plugin_instance->config_attribs["face"] = userface;
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;
}
}
}
}
}
}
}