e6639f1e29
PiperOrigin-RevId: 866518766 Change-Id: I5f042f52a27d2a6c66fe9642bccc73328fe14603
4672 lines
132 KiB
C++
4672 lines
132 KiB
C++
// Copyright 2021 DeepMind Technologies Limited
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <climits>
|
|
#include <cmath>
|
|
#include <csetjmp>
|
|
#include <cstddef>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <deque>
|
|
#include <functional>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <set>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include <mujoco/mjspec.h>
|
|
#include "user/user_api.h"
|
|
#include <TriangleMeshDistance/include/tmd/TriangleMeshDistance.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/mjplugin.h>
|
|
#include <mujoco/mjtnum.h>
|
|
#include "engine/engine_crossplatform.h" // IWYU pragma: keep
|
|
#include "engine/engine_plugin.h"
|
|
#include "engine/engine_util_errmem.h"
|
|
#include "user/user_cache.h"
|
|
#include "user/user_model.h"
|
|
#include "user/user_objects.h"
|
|
#include "user/user_resource.h"
|
|
#include "user/user_util.h"
|
|
|
|
extern "C" {
|
|
#include "qhull_ra.h"
|
|
}
|
|
|
|
namespace {
|
|
using mujoco::user::FilePath;
|
|
using std::max;
|
|
using std::min;
|
|
using std::sin;
|
|
using std::cos;
|
|
using std::pow;
|
|
|
|
// Parametrized linear/quintic interpolated nonlinearity.
|
|
double Fovea(double x, double gamma) {
|
|
// Quick return.
|
|
if (!gamma) return x;
|
|
|
|
// Foveal deformation.
|
|
double g = mjMAX(0, mjMIN(1, gamma));
|
|
return g * pow(x, 5) + (1 - g) * x;
|
|
}
|
|
|
|
// Evenly spaced numbers over a specified interval.
|
|
void LinSpace(double lower, double upper, int n, double array[]) {
|
|
double increment = n > 1 ? (upper - lower) / (n - 1) : 0;
|
|
for (int i = 0; i < n; ++i) {
|
|
*array = lower;
|
|
++array;
|
|
lower += increment;
|
|
}
|
|
}
|
|
|
|
// Make bin edges.
|
|
void BinEdges(double* x_edges, double* y_edges, int size[2], double fov[2],
|
|
double gamma) {
|
|
// Make unit bin edges.
|
|
LinSpace(-1, 1, size[0] + 1, x_edges);
|
|
LinSpace(-1, 1, size[1] + 1, y_edges);
|
|
|
|
// Apply foveal deformation.
|
|
for (int i = 0; i < size[0] + 1; i++) {
|
|
x_edges[i] = Fovea(x_edges[i], gamma);
|
|
}
|
|
for (int i = 0; i < size[1] + 1; i++) {
|
|
y_edges[i] = Fovea(y_edges[i], gamma);
|
|
}
|
|
|
|
// Scale by field-of-view.
|
|
mjuu_scalevec(x_edges, x_edges, fov[0] * mjPI / 180, size[0] + 1);
|
|
mjuu_scalevec(y_edges, y_edges, fov[1] * mjPI / 180, size[1] + 1);
|
|
}
|
|
|
|
// Transform spherical (azimuth, elevation, radius) to Cartesian (x,y,z).
|
|
void SphericalToCartesian(const double aer[3], float xyz[3]) {
|
|
double a = aer[0], e = aer[1], r = aer[2];
|
|
xyz[0] = r * cos(e) * sin(a);
|
|
xyz[1] = r * sin(e);
|
|
xyz[2] = -r * cos(e) * cos(a);
|
|
}
|
|
|
|
// Tangent frame in Cartesian coordinates.
|
|
void TangentFrame(const double aer[3], float mat[9]) {
|
|
double a = aer[0], e = aer[1], r = aer[2];
|
|
double ta[3] = {r * cos(e) * cos(a), 0, r * cos(e) * sin(a)};
|
|
double te[3] = {-r * sin(e) * sin(a), r * cos(e), r * sin(e) * cos(a)};
|
|
double n[3];
|
|
mjuu_normvec(ta, 3);
|
|
mjuu_normvec(te, 3);
|
|
mjuu_copyvec(mat + 3, ta, 3);
|
|
mjuu_copyvec(mat + 6, te, 3);
|
|
mjuu_crossvec(n, te, ta);
|
|
mjuu_copyvec(mat, n, 3);
|
|
}
|
|
|
|
// parametric superellipsoid/supertoroid helper functions
|
|
double aux_c(double omega, double m) {
|
|
return std::copysign(pow(std::abs(cos(omega)), m), cos(omega));
|
|
}
|
|
double aux_s(double omega, double m) {
|
|
return std::copysign(pow(std::abs(sin(omega)), m), sin(omega));
|
|
}
|
|
} // namespace
|
|
|
|
// compute triangle area, surface normal, center
|
|
static double triangle(double* normal, double* center,
|
|
const double* v1, const double* v2, const double* v3) {
|
|
double normal_local[3]; // if normal is nullptr
|
|
double* normal_ptr = (normal) ? normal : normal_local;
|
|
// center
|
|
if (center) {
|
|
center[0] = (v1[0] + v2[0] + v3[0])/3;
|
|
center[1] = (v1[1] + v2[1] + v3[1])/3;
|
|
center[2] = (v1[2] + v2[2] + v3[2])/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] };
|
|
mjuu_crossvec(normal_ptr, b, c);
|
|
|
|
// get length
|
|
double len = sqrt(mjuu_dot3(normal_ptr, normal_ptr));
|
|
|
|
// ignore small faces
|
|
if (len < mjMINVAL) {
|
|
return 0;
|
|
}
|
|
|
|
// normalize
|
|
if (normal) {
|
|
normal_ptr[0] /= len;
|
|
normal_ptr[1] /= len;
|
|
normal_ptr[2] /= len;
|
|
}
|
|
|
|
// return area
|
|
return 0.5 * len;
|
|
}
|
|
|
|
|
|
// 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_, 0, 0, 0);
|
|
mjuu_setvec(quat_, 1, 0, 0, 0);
|
|
|
|
mjuu_setvec(boxsz_, 0, 0, 0);
|
|
mjuu_setvec(aamm_, 1e10, 1e10, 1e10);
|
|
mjuu_setvec(aamm_+3, -1e10, -1e10, -1e10);
|
|
szgraph_ = 0;
|
|
center_ = nullptr;
|
|
graph_ = nullptr;
|
|
needhull_ = false;
|
|
maxhullvert_ = -1;
|
|
processed_ = false;
|
|
visual_ = true;
|
|
needreorient_ = true;
|
|
|
|
// reset to default if given
|
|
if (_def) {
|
|
*this = _def->Mesh();
|
|
}
|
|
|
|
// set model, def
|
|
model = _model;
|
|
if (_model) compiler = &_model->spec.compiler;
|
|
classname = (_def ? _def->name : (_model ? "main" : ""));
|
|
|
|
// 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.center_) {
|
|
size_t ncenter = 3*other.nface()*sizeof(double);
|
|
this->center_ = (double*)mju_malloc(ncenter);
|
|
memcpy(this->center_, other.center_, ncenter);
|
|
} else {
|
|
this->center_ = nullptr;
|
|
}
|
|
if (other.graph_) {
|
|
size_t szgraph = szgraph_*sizeof(int);
|
|
this->graph_ = (int*)mju_malloc(szgraph);
|
|
memcpy(this->graph_, other.graph_, szgraph);
|
|
} else {
|
|
this->graph_ = nullptr;
|
|
}
|
|
}
|
|
PointToLocal();
|
|
return *this;
|
|
}
|
|
|
|
|
|
|
|
void mjCMesh::PointToLocal() {
|
|
spec.element = static_cast<mjsElement*>(this);
|
|
spec.file = &spec_file_;
|
|
spec.content_type = &spec_content_type_;
|
|
spec.uservert = &spec_vert_;
|
|
spec.usernormal = &spec_normal_;
|
|
spec.userface = &spec_face_;
|
|
spec.userfacenormal = &spec_facenormal_;
|
|
spec.usertexcoord = &spec_texcoord_;
|
|
spec.userfacetexcoord = &spec_facetexcoord_;
|
|
spec.material = &spec_material_;
|
|
spec.plugin.plugin_name = &plugin_name;
|
|
spec.plugin.name = &plugin_instance_name;
|
|
spec.info = &info;
|
|
file = nullptr;
|
|
content_type = nullptr;
|
|
uservert = nullptr;
|
|
usernormal = nullptr;
|
|
userface = nullptr;
|
|
userfacenormal = nullptr;
|
|
usertexcoord = nullptr;
|
|
userfacetexcoord = nullptr;
|
|
}
|
|
|
|
|
|
|
|
void mjCMesh::NameSpace(const mjCModel* m) {
|
|
if (name.empty()) {
|
|
std::string stripped = mjuu_strippath(spec_file_);
|
|
name = mjuu_stripext(stripped);
|
|
}
|
|
mjCBase::NameSpace(m);
|
|
if (modelfiledir_.empty()) {
|
|
modelfiledir_ = FilePath(m->spec_modelfiledir_);
|
|
}
|
|
if (!plugin_instance_name.empty()) {
|
|
plugin_instance_name = m->prefix + plugin_instance_name + m->suffix;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void mjCMesh::CopyFromSpec() {
|
|
*static_cast<mjsMesh*>(this) = spec;
|
|
file_ = spec_file_;
|
|
content_type_ = spec_content_type_;
|
|
normal_ = spec_normal_;
|
|
face_ = spec_face_;
|
|
material_ = spec_material_;
|
|
ProcessVertices(spec_vert_);
|
|
texcoord_ = spec_texcoord_;
|
|
facetexcoord_ = spec_facetexcoord_;
|
|
maxhullvert_ = spec.maxhullvert;
|
|
plugin.active = spec.plugin.active;
|
|
plugin.element = spec.plugin.element;
|
|
plugin.plugin_name = spec.plugin.plugin_name;
|
|
plugin.name = spec.plugin.name;
|
|
|
|
// clear precompiled asset. TODO: use asset cache
|
|
if (center_) mju_free(center_);
|
|
if (graph_) mju_free(graph_);
|
|
szgraph_ = 0;
|
|
center_ = nullptr;
|
|
graph_ = nullptr;
|
|
|
|
// use filename if name is missing
|
|
if (name.empty()) {
|
|
std::string stripped = mjuu_strippath(file_);
|
|
name = mjuu_stripext(stripped);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void mjCMesh::CopyPlugin() {
|
|
model->CopyExplicitPlugin(this);
|
|
}
|
|
|
|
|
|
|
|
mjCMesh::~mjCMesh() {
|
|
if (center_) mju_free(center_);
|
|
if (graph_) mju_free(graph_);
|
|
}
|
|
|
|
|
|
|
|
// 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.element);
|
|
model->ResolvePlugin(this, plugin_name, plugin_instance_name, &plugin_instance);
|
|
plugin.element = plugin_instance;
|
|
const mjpPlugin* pplugin = mjp_getPluginAtSlot(plugin_instance->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 double 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;
|
|
|
|
uservert.reserve(mesh.vertices.size() * 3);
|
|
usernormal.reserve(mesh.normals.size() * 3);
|
|
userface.reserve(mesh.indices.size());
|
|
|
|
for (const auto& vertex : mesh.vertices) {
|
|
uservert.push_back(2*aabb[3]*vertex.x/(nx-1) + aabb[0]-aabb[3]);
|
|
uservert.push_back(2*aabb[4]*vertex.y/(ny-1) + aabb[1]-aabb[4]);
|
|
uservert.push_back(2*aabb[5]*vertex.z/(nz-1) + aabb[2]-aabb[5]);
|
|
}
|
|
|
|
for (const auto& normal : mesh.normals) {
|
|
usernormal.push_back(normal.x);
|
|
usernormal.push_back(normal.y);
|
|
usernormal.push_back(normal.z);
|
|
}
|
|
|
|
for (unsigned int index : mesh.indices) {
|
|
userface.push_back(index);
|
|
}
|
|
|
|
needreorient_ = false;
|
|
needsdf = false;
|
|
normal_ = std::move(usernormal);
|
|
face_ = std::move(userface);
|
|
ProcessVertices(uservert);
|
|
delete[] field;
|
|
}
|
|
|
|
|
|
|
|
void mjCMesh::CacheMesh(mjCCache* cache, const mjResource* resource) {
|
|
if (cache == nullptr) return;
|
|
|
|
// cache mesh data into new mesh object
|
|
mjCMesh *mesh = new mjCMesh();
|
|
|
|
// mesh properties
|
|
mesh->maxhullvert_ = maxhullvert_;
|
|
mesh->inertia = inertia;
|
|
std::copy(scale, scale + 3, mesh->scale);
|
|
|
|
// mesh processed data
|
|
mesh->processed_ = processed_;
|
|
mesh->vert_ = vert_;
|
|
mesh->normal_ = normal_;
|
|
mesh->texcoord_ = texcoord_;
|
|
mesh->face_ = face_;
|
|
mesh->facenormal_ = facenormal_;
|
|
mesh->facetexcoord_ = facetexcoord_;
|
|
mesh->halfedge_ = halfedge_;
|
|
mesh->szgraph_ = szgraph_;
|
|
if (szgraph_) {
|
|
mesh->graph_ = (int*)mju_malloc(szgraph_*sizeof(int));
|
|
std::copy(graph_, graph_ + szgraph_, mesh->graph_);
|
|
}
|
|
mesh->polygons_ = polygons_;
|
|
mesh->polygon_normals_ = polygon_normals_;
|
|
mesh->polygon_map_ = polygon_map_;
|
|
mesh->surface_ = surface_;
|
|
mesh->volume_ = volume_;
|
|
mesh->material_ = material_;
|
|
std::copy(boxsz_, boxsz_ + 3, mesh->boxsz_);
|
|
std::copy(aamm_, aamm_ + 6, mesh->aamm_);
|
|
std::copy(pos_, pos_ + 3, mesh->pos_);
|
|
std::copy(quat_, quat_ + 4, mesh->quat_);
|
|
int ncenter = face_.size();
|
|
if (ncenter) {
|
|
mesh->center_ = (double*)mju_malloc(ncenter * sizeof(double));
|
|
std::copy(center_, center_ + ncenter, mesh->center_);
|
|
}
|
|
mesh->tree_ = tree_;
|
|
mesh->face_aabb_ = face_aabb_;
|
|
mesh->octree_ = octree_;
|
|
|
|
// calculate estimated size of mesh
|
|
std::size_t size = sizeof(mjCMesh)
|
|
+ (sizeof(double) * vert_.size())
|
|
+ (sizeof(float) * normal_.size())
|
|
+ (sizeof(float) * texcoord_.size())
|
|
+ (sizeof(int) * face_.size())
|
|
+ (sizeof(int) * facenormal_.size())
|
|
+ (sizeof(int) * facetexcoord_.size())
|
|
+ (sizeof(int) * 2 * halfedge_.size())
|
|
+ (sizeof(int) * szgraph_)
|
|
+ (sizeof(int) * npolygonvert())
|
|
+ (sizeof(double) * polygon_normals_.size())
|
|
+ (sizeof(int) * npolygonmap())
|
|
+ (sizeof(double) * 18)
|
|
+ (sizeof(int) * ncenter)
|
|
+ tree_.Size()
|
|
+ octree_.Size()
|
|
+ (sizeof(double) * face_aabb_.size());
|
|
|
|
std::shared_ptr<const void> cached_data(mesh, +[] (const void* data) {
|
|
const mjCMesh* mesh = static_cast<const mjCMesh*>(data);
|
|
delete mesh;
|
|
});
|
|
cache->Insert("", resource->name, resource, cached_data, size);
|
|
}
|
|
|
|
namespace {
|
|
|
|
// vertex key for hash map
|
|
struct VertexKey {
|
|
float v[3];
|
|
|
|
bool operator==(const VertexKey& other) const {
|
|
return (v[0] == other.v[0] && v[1] == other.v[1] && v[2] == other.v[2]);
|
|
}
|
|
|
|
std::size_t operator()(const VertexKey& vertex) const {
|
|
// combine all three hash values into a single hash value
|
|
return ((std::hash<float>()(vertex.v[0])
|
|
^ (std::hash<float>()(vertex.v[1]) << 1)) >> 1)
|
|
^ (std::hash<float>()(vertex.v[2]) << 1);
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
// convert vertices to double precision and remove repeated vertices if requested
|
|
void mjCMesh::ProcessVertices(const std::vector<float>& vert, bool remove_repeated) {
|
|
vert_.clear();
|
|
int nvert = vert.size();
|
|
|
|
if (nvert % 3) {
|
|
throw mjCError(this, "vertex data must be a multiple of 3");
|
|
}
|
|
if (face_.size() % 3) {
|
|
throw mjCError(this, "face data must be a multiple of 3");
|
|
}
|
|
|
|
// convert vertices to double precision, may contain repeated vertices
|
|
if (!remove_repeated) {
|
|
vert_.reserve(nvert);
|
|
for (int i = 0; i < nvert / 3; ++i) {
|
|
const float* v = &vert[3 * i];
|
|
if (!std::isfinite(v[0]) || !std::isfinite(v[1]) || !std::isfinite(v[2])) {
|
|
throw mjCError(this, "vertex coordinate %d is not finite", nullptr, i);
|
|
}
|
|
vert_.push_back(v[0]);
|
|
vert_.push_back(v[1]);
|
|
vert_.push_back(v[2]);
|
|
}
|
|
return;
|
|
}
|
|
|
|
int index = 0;
|
|
std::unordered_map<VertexKey, int, VertexKey> vertex_map;
|
|
|
|
// populate vertex map with new vertex indices
|
|
for (int i = 0; i < nvert; i += 3) {
|
|
const float* v = &vert[i];
|
|
|
|
if (!std::isfinite(v[0]) || !std::isfinite(v[1]) || !std::isfinite(v[2])) {
|
|
throw mjCError(this, "vertex coordinate %d is not finite", nullptr, i);
|
|
}
|
|
|
|
VertexKey key = {v[0], v[1], v[2]};
|
|
if (vertex_map.find(key) == vertex_map.end()) {
|
|
vertex_map.insert({key, index});
|
|
++index;
|
|
}
|
|
}
|
|
|
|
// no repeated vertices (just copy vertex data)
|
|
if (3*index == nvert) {
|
|
vert_.reserve(nvert);
|
|
for (float v : vert) {
|
|
vert_.push_back(v);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// update face vertex indices
|
|
for (int i = 0; i < face_.size(); ++i) {
|
|
VertexKey key = {vert[3*face_[i]], vert[3*face_[i] + 1],
|
|
vert[3*face_[i] + 2]};
|
|
face_[i] = vertex_map[key];
|
|
}
|
|
|
|
// repopulate vertex data
|
|
vert_.resize(3 * index);
|
|
for (const auto& pair : vertex_map) {
|
|
const VertexKey& key = pair.first;
|
|
int index = pair.second;
|
|
|
|
// double precision
|
|
vert_[3*index + 0] = key.v[0];
|
|
vert_[3*index + 1] = key.v[1];
|
|
vert_[3*index + 2] = key.v[2];
|
|
}
|
|
}
|
|
|
|
bool mjCMesh::IsObj(std::string_view filename, std::string_view ct) {
|
|
std::string asset_type = GetAssetContentType(filename, ct);
|
|
return asset_type == "model/obj";
|
|
}
|
|
|
|
bool mjCMesh::IsSTL(std::string_view filename, std::string_view ct) {
|
|
std::string asset_type = GetAssetContentType(filename, ct);
|
|
return asset_type == "model/stl";
|
|
}
|
|
|
|
bool mjCMesh::IsMSH(std::string_view filename, std::string_view ct) {
|
|
std::string asset_type = GetAssetContentType(filename, ct);
|
|
return asset_type == "model/vnd.mujoco.msh";
|
|
}
|
|
|
|
bool mjCMesh::IsObj() const {
|
|
return content_type_ == "model/obj";
|
|
}
|
|
|
|
bool mjCMesh::IsSTL() const {
|
|
return content_type_ == "model/stl";
|
|
}
|
|
|
|
bool mjCMesh::IsMSH() const {
|
|
return content_type_ == "model/vnd.mujoco.msh";
|
|
}
|
|
|
|
|
|
|
|
// load mesh from resource; throw error on failure
|
|
void mjCMesh::LoadFromResource(mjResource* resource, bool remove_repeated) {
|
|
// set content type from resource name
|
|
std::string asset_type = GetAssetContentType(resource->name, content_type_);
|
|
if (asset_type.empty()) {
|
|
if (!content_type_.empty()) {
|
|
throw mjCError(this, "invalid content type: '%s'", content_type_.c_str());
|
|
}
|
|
throw mjCError(this, "unknown or unsupported mesh file: '%s'", resource->name);
|
|
}
|
|
content_type_ = asset_type;
|
|
|
|
if (IsSTL()) {
|
|
LoadSTL(resource);
|
|
} else if (IsObj()) {
|
|
LoadOBJ(resource, remove_repeated);
|
|
} else if (IsMSH()) {
|
|
LoadMSH(resource, remove_repeated);
|
|
} else {
|
|
throw mjCError(this, "unsupported mesh type: '%s'", asset_type.c_str());
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// compiler wrapper
|
|
void mjCMesh::Compile(const mjVFS* vfs) {
|
|
try {
|
|
TryCompile(vfs);
|
|
} catch (mjCError err) {
|
|
if (resource_ != nullptr) {
|
|
mju_closeResource(resource_);
|
|
resource_ = nullptr;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// compiler
|
|
void mjCMesh::TryCompile(const mjVFS* vfs) {
|
|
bool fromCache = false;
|
|
CopyFromSpec();
|
|
visual_ = true;
|
|
mjCCache *cache = reinterpret_cast<mjCCache*>(mj_getCache()->impl_);
|
|
|
|
// load file
|
|
if (!file_.empty()) {
|
|
vert_.clear();
|
|
face_.clear();
|
|
normal_.clear();
|
|
texcoord_.clear();
|
|
facenormal_.clear();
|
|
facetexcoord_.clear();
|
|
if (resource_ != nullptr) {
|
|
mju_closeResource(resource_);
|
|
resource_ = nullptr;
|
|
}
|
|
|
|
// copy paths from model if not already defined
|
|
mujoco::user::FilePath meshdir_;
|
|
meshdir_ = FilePath(mjs_getString(compiler->meshdir));
|
|
|
|
if (modelfiledir_.empty()) {
|
|
modelfiledir_ = FilePath(model->modelfiledir_);
|
|
}
|
|
|
|
// remove path from file if necessary
|
|
if (model->strippath) {
|
|
file_ = mjuu_strippath(file_);
|
|
}
|
|
|
|
FilePath filename = meshdir_ + FilePath(file_);
|
|
resource_ = LoadResource(modelfiledir_.Str(), filename.Str(), vfs);
|
|
|
|
// try loading from cache
|
|
if (cache != nullptr && LoadCachedMesh(cache, resource_)) {
|
|
mju_closeResource(resource_);
|
|
resource_ = nullptr;
|
|
fromCache = true;
|
|
}
|
|
|
|
if (!fromCache) {
|
|
LoadFromResource(resource_);
|
|
|
|
// check repeated mesh data
|
|
if (!normal_.empty() && !spec_normal_.empty()) {
|
|
throw mjCError(this, "repeated normal specification");
|
|
} else if (normal_.empty()) {
|
|
normal_ = spec_normal_;
|
|
}
|
|
if (!texcoord_.empty() && !spec_texcoord_.empty()) {
|
|
throw mjCError(this, "repeated texcoord specification");
|
|
} else if (texcoord_.empty()) {
|
|
texcoord_ = spec_texcoord_;
|
|
}
|
|
if (!face_.empty() && !spec_face_.empty()) {
|
|
throw mjCError(this, "repeated face specification");
|
|
} else if (face_.empty()) {
|
|
face_ = spec_face_;
|
|
}
|
|
if (!vert_.empty() && !spec_vert_.empty()) {
|
|
throw mjCError(this, "repeated vertex specification");
|
|
} else if (vert_.empty()) {
|
|
ProcessVertices(spec_vert_);
|
|
}
|
|
if (!facenormal_.empty() && !spec_normal_.empty()) {
|
|
throw mjCError(this, "repeated facenormal specification");
|
|
} else if (facenormal_.empty()) {
|
|
facenormal_ = spec_facenormal_;
|
|
}
|
|
if (!facetexcoord_.empty() && !spec_facetexcoord_.empty()) {
|
|
throw mjCError(this, "repeated facetexcoord specification");
|
|
} else if (facetexcoord_.empty()) {
|
|
facetexcoord_ = spec_facetexcoord_;
|
|
}
|
|
}
|
|
} else if (plugin.active) {
|
|
LoadSDF(); // create using marching cubes
|
|
}
|
|
|
|
CheckInitialMesh();
|
|
|
|
// compute mesh properties
|
|
if (!fromCache) {
|
|
Process();
|
|
}
|
|
|
|
// make octree
|
|
if (!needsdf) {
|
|
octree_.Clear(); // this occurs when a non-SDF mesh is loaded from a cached SDF mesh
|
|
} else if (octree_.NumNodes() == 0) {
|
|
octree_.SetFace(vert_, face_);
|
|
octree_.CreateOctree(aamm_);
|
|
|
|
// compute sdf coefficients
|
|
if (!plugin.active) {
|
|
tmd::TriangleMeshDistance sdf(vert_.data(), nvert(), face_.data(), nface());
|
|
|
|
std::vector<double> coeffs(octree_.NumVerts());
|
|
std::vector<bool> processed(octree_.NumVerts(), false);
|
|
std::deque<int> queue;
|
|
|
|
if (octree_.NumNodes() > 0) {
|
|
queue.push_back(0); // start traversal from the root node
|
|
}
|
|
|
|
while (!queue.empty()) {
|
|
int node_idx = queue.front();
|
|
queue.pop_front();
|
|
|
|
for (int j = 0; j < 8; ++j) {
|
|
int vert_id = octree_.VertId(node_idx, j);
|
|
if (processed[vert_id]) {
|
|
continue;
|
|
}
|
|
if (octree_.Hang(vert_id).empty()) {
|
|
coeffs[vert_id] = sdf.signed_distance(octree_.Vert(vert_id)).distance;
|
|
} else {
|
|
double sum_coeff = 0;
|
|
for (int dep_id : octree_.Hang(vert_id)) {
|
|
sum_coeff += coeffs[dep_id];
|
|
if (!processed[dep_id]) {
|
|
throw mjCError(this, "sdf coefficient computation failed");
|
|
}
|
|
}
|
|
coeffs[vert_id] = sum_coeff / octree_.Hang(vert_id).size();
|
|
}
|
|
processed[vert_id] = true;
|
|
}
|
|
|
|
for (int child_idx : octree_.Children(node_idx)) {
|
|
if (child_idx != -1) {
|
|
queue.push_back(child_idx);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < octree_.NumNodes(); ++i) {
|
|
for (int j = 0; j < 8; j++) {
|
|
octree_.AddCoeff(i, j, coeffs[octree_.VertId(i, j)]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// cache mesh
|
|
if (!fromCache && !file_.empty()) {
|
|
CacheMesh(cache, resource_);
|
|
}
|
|
|
|
// close resource
|
|
if (resource_ != nullptr) {
|
|
mju_closeResource(resource_);
|
|
resource_ = nullptr;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// get bounding volume
|
|
void mjCMesh::SetBoundingVolume(int faceid) {
|
|
constexpr double kMaxVal = std::numeric_limits<double>::max();
|
|
double face_aamm[6] = {kMaxVal, kMaxVal, kMaxVal, -kMaxVal, -kMaxVal, -kMaxVal};
|
|
|
|
for (int j = 0; j < 3; j++) {
|
|
int vertid = face_[3*faceid + j];
|
|
face_aamm[0] = std::min(face_aamm[0], vert_[3*vertid + 0]);
|
|
face_aamm[1] = std::min(face_aamm[1], vert_[3*vertid + 1]);
|
|
face_aamm[2] = std::min(face_aamm[2], vert_[3*vertid + 2]);
|
|
face_aamm[3] = std::max(face_aamm[3], vert_[3*vertid + 0]);
|
|
face_aamm[4] = std::max(face_aamm[4], vert_[3*vertid + 1]);
|
|
face_aamm[5] = std::max(face_aamm[5], vert_[3*vertid + 2]);
|
|
}
|
|
|
|
face_aabb_.push_back(.5 * (face_aamm[0] + face_aamm[3]));
|
|
face_aabb_.push_back(.5 * (face_aamm[1] + face_aamm[4]));
|
|
face_aabb_.push_back(.5 * (face_aamm[2] + face_aamm[5]));
|
|
face_aabb_.push_back(.5 * (face_aamm[3] - face_aamm[0]));
|
|
face_aabb_.push_back(.5 * (face_aamm[4] - face_aamm[1]));
|
|
face_aabb_.push_back(.5 * (face_aamm[5] - face_aamm[2]));
|
|
|
|
tree_.AddBoundingVolume(faceid, 1, 1, center_ + 3*faceid, nullptr,
|
|
&face_aabb_[6*faceid]);
|
|
}
|
|
|
|
|
|
|
|
double* mjCMesh::GetPosPtr() {
|
|
return pos_;
|
|
}
|
|
|
|
|
|
|
|
double* mjCMesh::GetQuatPtr() {
|
|
return quat_;
|
|
}
|
|
|
|
|
|
|
|
bool mjCMesh::HasTexcoord() const {
|
|
return !texcoord_.empty();
|
|
}
|
|
|
|
|
|
|
|
void mjCMesh::CopyVert(float* arr) const {
|
|
for (int i = 0; i < vert_.size(); ++i) {
|
|
arr[i] = (float)vert_[i];
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void mjCMesh::CopyNormal(float* arr) const {
|
|
std::copy(normal_.begin(), normal_.end(), arr);
|
|
}
|
|
|
|
|
|
|
|
void mjCMesh::CopyFace(int* arr) const {
|
|
std::copy(face_.begin(), face_.end(), arr);
|
|
}
|
|
|
|
|
|
|
|
void mjCMesh::CopyFaceTexcoord(int* arr) const {
|
|
std::copy(facetexcoord_.begin(), facetexcoord_.end(), arr);
|
|
}
|
|
|
|
|
|
|
|
void mjCMesh::CopyFaceNormal(int* arr) const {
|
|
std::copy(facenormal_.begin(), facenormal_.end(), arr);
|
|
}
|
|
|
|
|
|
|
|
void mjCMesh::CopyTexcoord(float* arr) const {
|
|
std::copy(texcoord_.begin(), texcoord_.end(), arr);
|
|
}
|
|
|
|
|
|
|
|
void mjCMesh::CopyGraph(int* arr) const {
|
|
std::copy(graph_, graph_+szgraph_, arr);
|
|
}
|
|
|
|
|
|
|
|
void mjCMesh::CopyPolygons(int* verts, int* adr, int* num, int poly_adr) const {
|
|
int n = polygons_.size(), count = 0;
|
|
for (int i = 0; i < n; ++i) {
|
|
int m = num[i] = polygons_[i].size();
|
|
adr[i] = poly_adr + count;
|
|
count += m;
|
|
for (int j = 0; j < m; ++j) {
|
|
verts[adr[i] + j - poly_adr] = polygons_[i][j];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void mjCMesh::CopyPolygonMap(int* faces, int* adr, int* num, int poly_adr) const {
|
|
int n = polygon_map_.size(), count = 0;
|
|
for (int i = 0; i < n; ++i) {
|
|
int m = num[i] = polygon_map_[i].size();
|
|
adr[i] = poly_adr + count;
|
|
count += m;
|
|
for (int j = 0; j < m; ++j) {
|
|
faces[adr[i] + j - poly_adr] = polygon_map_[i][j];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void mjCMesh::CopyPolygonNormals(mjtNum* arr) {
|
|
for (int i = 0; i < polygon_normals_.size(); i += 3) {
|
|
arr[i + 0] = (mjtNum)polygon_normals_[i + 0];
|
|
arr[i + 1] = (mjtNum)polygon_normals_[i + 1];
|
|
arr[i + 2] = (mjtNum)polygon_normals_[i + 2];
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void mjCMesh::DelTexcoord() {
|
|
texcoord_.clear();
|
|
}
|
|
|
|
|
|
|
|
// set geom size to match mesh
|
|
void mjCMesh::FitGeom(mjCGeom* geom, double center[3]) {
|
|
// use inertial box
|
|
if (!model->compiler.fitaabb) {
|
|
// get inertia box type (shell or volume)
|
|
double* boxsz = GetInertiaBoxPtr();
|
|
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] = max(0.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 and size
|
|
center[0] = (aamm_[0]+aamm_[3])/2;
|
|
center[1] = (aamm_[1]+aamm_[4])/2;
|
|
center[2] = (aamm_[2]+aamm_[5])/2;
|
|
double size[3] = {aamm_[3] - center[0], aamm_[4] - center[1], aamm_[5] - center[2]};
|
|
|
|
// compute smallest geom whose aabb contains the mesh aabb
|
|
switch (geom->type) {
|
|
case mjGEOM_SPHERE:
|
|
geom->size[0] = max(max(size[0], size[1]), size[2]);
|
|
break;
|
|
|
|
case mjGEOM_CAPSULE:
|
|
case mjGEOM_CYLINDER:
|
|
// find maximum distance in XY, separately in Z
|
|
geom->size[0] = max(size[0], size[1]);
|
|
geom->size[1] = size[2];
|
|
|
|
// special handling of capsule: consider curved cap
|
|
if (geom->type == mjGEOM_CAPSULE) {
|
|
geom->size[1] -= geom->size[0];
|
|
}
|
|
break;
|
|
|
|
case mjGEOM_ELLIPSOID:
|
|
case mjGEOM_BOX:
|
|
geom->size[0] = size[0];
|
|
geom->size[1] = size[1];
|
|
geom->size[2] = size[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;
|
|
}
|
|
|
|
|
|
|
|
// load OBJ mesh
|
|
void mjCMesh::LoadOBJ(mjResource* resource, bool remove_repeated) {
|
|
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();
|
|
normal_ = attrib.normals;
|
|
texcoord_ = attrib.texcoords;
|
|
facenormal_.clear();
|
|
facetexcoord_.clear();
|
|
|
|
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) {
|
|
face_.push_back(mesh_index.vertex_index);
|
|
|
|
if (!normal_.empty()) {
|
|
facenormal_.push_back(mesh_index.normal_index);
|
|
}
|
|
|
|
if (!texcoord_.empty()) {
|
|
facetexcoord_.push_back(mesh_index.texcoord_index);
|
|
}
|
|
}
|
|
}
|
|
|
|
// flip the second texcoord
|
|
for (int i=0; i < texcoord_.size()/2; i++) {
|
|
texcoord_[2*i+1] = 1-texcoord_[2*i+1];
|
|
}
|
|
|
|
// copy vertex data
|
|
ProcessVertices(attrib.vertices, remove_repeated);
|
|
}
|
|
|
|
|
|
|
|
// load mesh from cached asset, return true on success
|
|
bool mjCMesh::LoadCachedMesh(mjCCache *cache, const mjResource* resource) {
|
|
auto process_mesh = [&](const void* data) {
|
|
const mjCMesh* mesh = static_cast<const mjCMesh*>(data);
|
|
// check if maxhullvert is different
|
|
if (maxhullvert_ != mesh->maxhullvert_) {
|
|
return false;
|
|
}
|
|
|
|
// check if inertia is different
|
|
if (inertia != mesh->inertia) {
|
|
return false;
|
|
}
|
|
|
|
// check if scale is different
|
|
if (scale[0] != mesh->scale[0] ||
|
|
scale[1] != mesh->scale[1] ||
|
|
scale[2] != mesh->scale[2]) {
|
|
return false;
|
|
}
|
|
|
|
// check if need hull
|
|
if (needhull_ && !mesh->szgraph_) {
|
|
return false;
|
|
}
|
|
|
|
processed_ = mesh->processed_;
|
|
vert_ = mesh->vert_;
|
|
normal_ = mesh->normal_;
|
|
texcoord_ = mesh->texcoord_;
|
|
face_ = mesh->face_;
|
|
facenormal_ = mesh->facenormal_;
|
|
facetexcoord_ = mesh->facetexcoord_;
|
|
halfedge_ = mesh->halfedge_;
|
|
|
|
// only copy graph if needed
|
|
if (needhull_ || mesh->face_.empty()) {
|
|
szgraph_ = mesh->szgraph_;
|
|
graph_ = nullptr;
|
|
if (szgraph_) {
|
|
graph_ = (int*)mju_malloc(szgraph_*sizeof(int));
|
|
std::copy(mesh->graph_, mesh->graph_ + szgraph_, graph_);
|
|
}
|
|
}
|
|
|
|
polygons_ = mesh->polygons_;
|
|
polygon_normals_ = mesh->polygon_normals_;
|
|
polygon_map_ = mesh->polygon_map_;
|
|
surface_ = mesh->surface_;
|
|
volume_ = mesh->volume_;
|
|
std::copy(mesh->boxsz_, mesh->boxsz_ + 3, boxsz_);
|
|
std::copy(mesh->aamm_, mesh->aamm_ + 6, aamm_);
|
|
std::copy(mesh->pos_, mesh->pos_ + 3, pos_);
|
|
std::copy(mesh->quat_, mesh->quat_ + 4, quat_);
|
|
|
|
center_ = nullptr;
|
|
int ncenter = mesh->face_.size();
|
|
if (ncenter) {
|
|
center_ = (double*)mju_malloc(ncenter * sizeof(double));
|
|
std::copy(mesh->center_, mesh->center_ + ncenter, center_);
|
|
}
|
|
tree_ = mesh->tree_;
|
|
face_aabb_ = mesh->face_aabb_;
|
|
octree_ = mesh->octree_;
|
|
return true;
|
|
};
|
|
|
|
// check that cached asset has all data
|
|
return cache->PopulateData(resource->name, resource, process_mesh);
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
int nfaces = 0;
|
|
ReadFromBuffer(&nfaces, buffer + 80);
|
|
if (nfaces < 1 || nfaces > 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 (nfaces*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_.assign(3*nfaces, 0);
|
|
std::vector<float> vert;
|
|
|
|
// add vertices and faces, including repeated for now
|
|
for (int i=0; i < nfaces; i++) {
|
|
for (int j=0; j < 3; j++) {
|
|
// read vertex coordinates
|
|
float v[3];
|
|
ReadFromBuffer(&v, stl + 50*i + 12*(j + 1));
|
|
|
|
// check if vertex can be cast to an int safely
|
|
if (fabs(v[0]) > pow(2, 30) || fabs(v[1]) > pow(2, 30) || fabs(v[2]) > pow(2, 30)) {
|
|
throw mjCError(this, "vertex 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] = vert.size() / 3;
|
|
} else {
|
|
face_[3*i + 3 - j] = vert.size() / 3;
|
|
}
|
|
|
|
// add vertex data
|
|
vert.push_back(v[0]);
|
|
vert.push_back(v[1]);
|
|
vert.push_back(v[2]);
|
|
}
|
|
}
|
|
ProcessVertices(vert, true);
|
|
}
|
|
|
|
|
|
|
|
// load MSH binary mesh
|
|
void mjCMesh::LoadMSH(mjResource* resource, bool remove_repeated) {
|
|
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
|
|
int nvbuf = 0, nfbuf = 0, nnbuf = 0, ntbuf = 0;
|
|
ReadFromBuffer(&nvbuf, buffer);
|
|
ReadFromBuffer(&nnbuf, buffer + sizeof(int));
|
|
ReadFromBuffer(&ntbuf, buffer + 2*sizeof(int));
|
|
ReadFromBuffer(&nfbuf, buffer + 3*sizeof(int));
|
|
|
|
// check sizes
|
|
if (nvbuf < 4 || nfbuf < 0 || nnbuf < 0 || ntbuf < 0 ||
|
|
(nnbuf > 0 && nnbuf != nvbuf) ||
|
|
(ntbuf > 0 && ntbuf != nvbuf)) {
|
|
throw mjCError(this, "invalid sizes in MSH file '%s'", resource->name);
|
|
}
|
|
|
|
if (nvbuf >= INT_MAX / sizeof(float) / 3 ||
|
|
nnbuf >= INT_MAX / sizeof(float) / 3 ||
|
|
ntbuf >= INT_MAX / sizeof(float) / 2 ||
|
|
nfbuf >= 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*nvbuf*sizeof(float) + 3*nnbuf*sizeof(float) +
|
|
2*ntbuf*sizeof(float) + 3*nfbuf*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));
|
|
std::vector<float> vert;
|
|
int nvert = 0;
|
|
if (nvbuf) {
|
|
vert.assign(3*nvbuf, 0);
|
|
nvert = 3*nvbuf;
|
|
memcpy(vert.data(), fdata, nvert*sizeof(float));
|
|
fdata += nvert;
|
|
}
|
|
if (nnbuf) {
|
|
normal_.assign(nvert, 0);
|
|
memcpy(normal_.data(), fdata, nvert*sizeof(float));
|
|
fdata += nvert;
|
|
}
|
|
if (ntbuf) {
|
|
texcoord_.assign(2*(nvert / 3), 0);
|
|
memcpy(texcoord_.data(), fdata, 2*(nvert/3)*sizeof(float));
|
|
fdata += 2*(nvert / 3);
|
|
}
|
|
if (nfbuf) {
|
|
face_.assign(3*nfbuf, 0);
|
|
facenormal_.assign(3*nfbuf, 0);
|
|
memcpy(face_.data(), fdata, 3*nfbuf*sizeof(int));
|
|
memcpy(facenormal_.data(), fdata, 3*nfbuf*sizeof(int));
|
|
}
|
|
if (nfbuf && !texcoord_.empty()) {
|
|
facetexcoord_.assign(3*nfbuf, 0);
|
|
memcpy(facetexcoord_.data(), fdata, 3*nfbuf*sizeof(int));
|
|
}
|
|
|
|
// rearrange face data if left-handed scaling
|
|
if (nfbuf && !righthand) {
|
|
for (int i=0; i < nfbuf; i++) {
|
|
int tmp = face_[3*i+1];
|
|
face_[3*i+1] = face_[3*i+2];
|
|
face_[3*i+2] = tmp;
|
|
}
|
|
}
|
|
ProcessVertices(vert, remove_repeated);
|
|
}
|
|
|
|
|
|
|
|
// compute the volume and center-of-mass of the mesh given the face centroid
|
|
double mjCMesh::ComputeVolume(double CoM[3], const double facecen[3]) const {
|
|
double normal[3], center[3], total_volume = 0;
|
|
CoM[0] = CoM[1] = CoM[2] = 0;
|
|
int nf = (inertia == mjMESH_INERTIA_CONVEX) ? graph_[1] : nface();
|
|
const int* f = (inertia == mjMESH_INERTIA_CONVEX) ? GraphFaces() : face_.data();
|
|
|
|
for (int i = 0; i < nf; i++) {
|
|
// get area, normal and center
|
|
double area = triangle(normal, center, &vert_[3*f[3*i]], &vert_[3*f[3*i + 1]],
|
|
&vert_[3*f[3*i + 2]]);
|
|
|
|
// compute and add volume
|
|
double vec[3] = {center[0] - facecen[0], center[1] - facecen[1], center[2] - facecen[2]};
|
|
double volume = mjuu_dot3(vec, normal) * area / 3;
|
|
|
|
// if legacy computation requested, then always positive
|
|
if (inertia == mjMESH_INERTIA_LEGACY) {
|
|
volume = std::abs(volume);
|
|
}
|
|
|
|
// add pyramid com
|
|
total_volume += volume;
|
|
CoM[0] += volume*(center[0]*3.0/4.0 + facecen[0]/4.0);
|
|
CoM[1] += volume*(center[1]*3.0/4.0 + facecen[1]/4.0);
|
|
CoM[2] += volume*(center[2]*3.0/4.0 + facecen[2]/4.0);
|
|
}
|
|
|
|
// if volume is valid normalize CoM
|
|
if (total_volume >= mjMINVAL) {
|
|
CoM[0] /= total_volume;
|
|
CoM[1] /= total_volume;
|
|
CoM[2] /= total_volume;
|
|
}
|
|
return total_volume;
|
|
}
|
|
|
|
|
|
|
|
// compute the surface area and center-of-mass of the mesh given the face centroid
|
|
double mjCMesh::ComputeSurfaceArea(double CoM[3], const double facecen[3]) const {
|
|
double surface = 0;
|
|
CoM[0] = CoM[1] = CoM[2] = 0;
|
|
for (int i = 0; i < nface(); i++) {
|
|
// get area and center
|
|
double area, center[3];
|
|
area = triangle(nullptr, center, &vert_[3*face_[3*i]],
|
|
&vert_[3*face_[3*i + 1]], &vert_[3*face_[3*i + 2]]);
|
|
|
|
// add pyramid com
|
|
surface += area;
|
|
CoM[0] += area*(center[0]*3.0/4.0 + facecen[0]/4.0);
|
|
CoM[1] += area*(center[1]*3.0/4.0 + facecen[1]/4.0);
|
|
CoM[2] += area*(center[2]*3.0/4.0 + facecen[2]/4.0);
|
|
}
|
|
|
|
// if area is valid normalize CoM
|
|
if (surface >= mjMINVAL) {
|
|
CoM[0] /= surface;
|
|
CoM[1] /= surface;
|
|
CoM[2] /= surface;
|
|
}
|
|
return surface;
|
|
}
|
|
|
|
|
|
|
|
// apply transformations
|
|
void mjCMesh::ApplyTransformations() {
|
|
// translate
|
|
if (refpos[0] != 0 || refpos[1] != 0 || refpos[2] != 0) {
|
|
for (int i = 0; i < nvert(); i++) {
|
|
vert_[3*i + 0] -= refpos[0];
|
|
vert_[3*i + 1] -= refpos[1];
|
|
vert_[3*i + 2] -= refpos[2];
|
|
}
|
|
}
|
|
|
|
// rotate
|
|
if (refquat[0] != 1 || refquat[1] != 0 || refquat[2] != 0 || refquat[3] != 0) {
|
|
// prepare rotation
|
|
double quat[4] = {refquat[0], refquat[1], refquat[2], refquat[3]};
|
|
double mat[9];
|
|
mjuu_normvec(quat, 4);
|
|
mjuu_quat2mat(mat, quat);
|
|
|
|
// process vertices
|
|
for (int i = 0; i < nvert(); i++) {
|
|
mjuu_mulvecmatT(&vert_[3*i], &vert_[3*i], mat);
|
|
}
|
|
|
|
// process normals
|
|
for (int i = 0; i < nnormal(); i++) {
|
|
double n1[3], n0[3] = {normal_[3*i], normal_[3*i+1], normal_[3*i+2]};
|
|
mjuu_mulvecmatT(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 + 0] *= scale[0];
|
|
vert_[3*i + 1] *= scale[1];
|
|
vert_[3*i + 2] *= scale[2];
|
|
}
|
|
|
|
for (int i = 0; i < nnormal(); i++) {
|
|
normal_[3*i + 0] *= 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 + 0] *= scl;
|
|
normal_[3*i + 1] *= scl;
|
|
normal_[3*i + 2] *= scl;
|
|
} else {
|
|
normal_[3*i + 0] = 0;
|
|
normal_[3*i + 1] = 0;
|
|
normal_[3*i + 2] = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// find centroid of faces, return total area
|
|
double mjCMesh::ComputeFaceCentroid(double facecen[3]) const {
|
|
double total_area = 0;
|
|
|
|
for (int i = 0; i < nface(); i++) {
|
|
// get area and center
|
|
double area, center[3];
|
|
area = triangle(nullptr, center, &vert_[3*face_[3*i]],
|
|
&vert_[3*face_[3*i + 1]], &vert_[3*face_[3*i + 2]]);
|
|
|
|
// accumulate
|
|
facecen[0] += area * center[0];
|
|
facecen[1] += area * center[1];
|
|
facecen[2] += area * center[2];
|
|
total_area += area;
|
|
}
|
|
|
|
// finalize centroid of faces
|
|
if (total_area >= mjMINVAL) {
|
|
facecen[0] /= total_area;
|
|
facecen[1] /= total_area;
|
|
facecen[2] /= total_area;
|
|
}
|
|
return total_area;
|
|
}
|
|
|
|
|
|
|
|
void mjCMesh::Process() {
|
|
// create half-edge structure (if mesh was in XML)
|
|
if (halfedge_.empty()) {
|
|
for (int i = 0; i < nface(); i++) {
|
|
int v0 = face_[3*i + 0];
|
|
int v1 = face_[3*i + 1];
|
|
int v2 = face_[3*i + 2];
|
|
if (triangle(nullptr, nullptr, &vert_[3*v0], &vert_[3*v1], &vert_[3*v2]) > sqrt(mjMINVAL)) {
|
|
halfedge_.push_back({v0, v1});
|
|
halfedge_.push_back({v1, v2});
|
|
halfedge_.push_back({v2, v0});
|
|
} else {
|
|
// TODO(b/255525326)
|
|
}
|
|
}
|
|
}
|
|
|
|
// check for inconsistent face orientations
|
|
if (!halfedge_.empty()) {
|
|
std::stable_sort(halfedge_.begin(), halfedge_.end());
|
|
auto iterator = std::adjacent_find(halfedge_.begin(), halfedge_.end());
|
|
if (iterator != halfedge_.end() && inertia == mjMESH_INERTIA_EXACT) {
|
|
throw mjCError(this,
|
|
"faces of mesh '%s' have inconsistent orientation. Please check the "
|
|
"faces containing the vertices %d and %d.",
|
|
name.c_str(), iterator->first + 1, iterator->second + 1);
|
|
}
|
|
}
|
|
|
|
// make graph describing convex hull
|
|
if (needhull_ || face_.empty()) {
|
|
MakeGraph();
|
|
}
|
|
|
|
// no faces: copy from convex hull
|
|
if (face_.empty()) {
|
|
CopyGraph();
|
|
}
|
|
|
|
// no normals: make
|
|
if (normal_.empty()) {
|
|
MakeNormal();
|
|
}
|
|
|
|
// check facenormal size
|
|
if (!facenormal_.empty() && facenormal_.size() != face_.size()) {
|
|
throw mjCError(this, "face data must have the same size as face normal data");
|
|
}
|
|
|
|
// no facetexcoord: copy from faces
|
|
if (facetexcoord_.empty() && !texcoord_.empty()) {
|
|
facetexcoord_ = face_;
|
|
}
|
|
|
|
// facenormal might not exist if usernormal was specified
|
|
if (facenormal_.empty()) {
|
|
int normal_per_vertex = normal_.size() / vert_.size();
|
|
facenormal_.assign(face_.size(), 0);
|
|
for (int i = 0; i < face_.size(); i++) {
|
|
facenormal_[i] = normal_per_vertex * face_[i];
|
|
}
|
|
}
|
|
|
|
MakePolygons();
|
|
|
|
// user offset, rotation, scaling
|
|
ApplyTransformations();
|
|
|
|
// find centroid of faces
|
|
double facecen[3] = {0, 0, 0};
|
|
if (ComputeFaceCentroid(facecen) < mjMINVAL) {
|
|
throw mjCError(this, "mesh surface area is too small: %s", name.c_str());
|
|
}
|
|
|
|
// compute inertia and transform mesh. The mesh is transformed such that it is
|
|
// centered at the CoM and the axes are the principle axes of inertia
|
|
double CoM[3] = {0, 0, 0};
|
|
double inert[6] = {0, 0, 0, 0, 0, 0};
|
|
|
|
// compute CoM and volume/area
|
|
if (inertia == mjMESH_INERTIA_SHELL) {
|
|
surface_ = ComputeSurfaceArea(CoM, facecen);
|
|
if (surface_ < mjMINVAL) {
|
|
throw mjCError(this, "mesh surface area is too small: %s", name.c_str());
|
|
}
|
|
} else {
|
|
if ((volume_ = ComputeVolume(CoM, facecen)) < mjMINVAL) {
|
|
if (volume_ < 0) {
|
|
throw mjCError(this, "mesh volume is negative (misoriented triangles): %s", name.c_str());
|
|
} else {
|
|
throw mjCError(this, "mesh volume is too small: %s . Try setting inertia to shell",
|
|
name.c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
// compute inertia
|
|
double total_volume = ComputeInertia(inert, CoM);
|
|
if (inertia == mjMESH_INERTIA_SHELL) {
|
|
surface_ = total_volume;
|
|
} else {
|
|
volume_ = total_volume;
|
|
}
|
|
|
|
// get quaternion and diagonal inertia
|
|
double eigval[3], eigvec[9], quattmp[4];
|
|
double full[9] = {
|
|
inert[0], inert[3], inert[4],
|
|
inert[3], inert[1], inert[5],
|
|
inert[4], inert[5], inert[2]
|
|
};
|
|
mjuu_eig3(eigval, eigvec, quattmp, full);
|
|
|
|
constexpr double inequality_atol = 1e-9;
|
|
constexpr double inequality_rtol = 1e-6;
|
|
|
|
// check eigval - SHOULD NOT OCCUR
|
|
if (eigval[2] <= 0) {
|
|
throw mjCError(this, "eigenvalue of mesh inertia must be positive: %s", name.c_str());
|
|
}
|
|
|
|
if (eigval[0] + eigval[1] < eigval[2] * (1.0 - inequality_rtol) - inequality_atol ||
|
|
eigval[0] + eigval[2] < eigval[1] * (1.0 - inequality_rtol) - inequality_atol ||
|
|
eigval[1] + eigval[2] < eigval[0] * (1.0 - inequality_rtol) - inequality_atol) {
|
|
throw mjCError(this, "eigenvalues of mesh inertia violate A + B >= C: %s", name.c_str());
|
|
}
|
|
|
|
// compute sizes of equivalent inertia box
|
|
double volume = GetVolumeRef();
|
|
boxsz_[0] = 0.5 * std::sqrt(6*(eigval[1] + eigval[2] - eigval[0])/volume);
|
|
boxsz_[1] = 0.5 * std::sqrt(6*(eigval[0] + eigval[2] - eigval[1])/volume);
|
|
boxsz_[2] = 0.5 * std::sqrt(6*(eigval[0] + eigval[1] - eigval[2])/volume);
|
|
|
|
// prevent reorientation if the mesh was autogenerated using marching cubes
|
|
if (!needreorient_) {
|
|
mjuu_setvec(CoM, 0, 0, 0);
|
|
mjuu_setvec(quattmp, 1, 0, 0, 0);
|
|
}
|
|
|
|
// transform CoM to origin
|
|
for (int i=0; i < nvert(); i++) {
|
|
vert_[3*i + 0] -= CoM[0];
|
|
vert_[3*i + 1] -= CoM[1];
|
|
vert_[3*i + 2] -= CoM[2];
|
|
}
|
|
Rotate(quattmp);
|
|
|
|
// save the pos and quat that was used to transform the mesh
|
|
mjuu_copyvec(pos_, CoM, 3);
|
|
mjuu_copyvec(quat_, quattmp, 4);
|
|
|
|
processed_ = true;
|
|
|
|
// no radii: make
|
|
if (!center_) {
|
|
MakeCenter();
|
|
}
|
|
|
|
// recompute polygon normals
|
|
MakePolygonNormals();
|
|
|
|
// make bounding volume hierarchy
|
|
if (tree_.Bvh().empty()) {
|
|
face_aabb_.clear();
|
|
face_aabb_.reserve(3*face_.size());
|
|
tree_.AllocateBoundingVolumes(nface());
|
|
for (int i = 0; i < nface(); i++) {
|
|
SetBoundingVolume(i);
|
|
}
|
|
tree_.CreateBVH();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// compute abstract (unitless) inertia, recompute area / volume
|
|
double mjCMesh::ComputeInertia(double inert[6], const double CoM[3]) const {
|
|
double total_volume = 0;
|
|
|
|
// copy vertices to avoid modifying the original mesh
|
|
std::vector<double> vert_centered;
|
|
vert_centered.reserve(3*nvert());
|
|
|
|
// translate vertices to origin in order to compute inertia
|
|
for (int i = 0; i < nvert(); i++) {
|
|
vert_centered.push_back(vert_[3*i + 0] - CoM[0]);
|
|
vert_centered.push_back(vert_[3*i + 1] - CoM[1]);
|
|
vert_centered.push_back(vert_[3*i + 2] - CoM[2]);
|
|
}
|
|
|
|
// 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};
|
|
int nf = (inertia == mjMESH_INERTIA_CONVEX) ? graph_[1] : nface();
|
|
const int* f = (inertia == mjMESH_INERTIA_CONVEX) ? GraphFaces() : face_.data();
|
|
for (int i=0; i < nf; i++) {
|
|
const double* D = &vert_centered[3*f[3*i + 0]];
|
|
const double* E = &vert_centered[3*f[3*i + 1]];
|
|
const double* F = &vert_centered[3*f[3*i + 2]];
|
|
|
|
// get area, normal and center; update volume
|
|
double normal[3], center[3];
|
|
double volume, area = triangle(normal, center, D, E, F);
|
|
if (inertia == mjMESH_INERTIA_SHELL) {
|
|
volume = area;
|
|
} else {
|
|
volume = mjuu_dot3(center, normal) * area / 3;
|
|
}
|
|
|
|
// if legacy computation requested, then always positive
|
|
if (inertia == mjMESH_INERTIA_LEGACY) {
|
|
volume = abs(volume);
|
|
}
|
|
|
|
// apply formula, accumulate
|
|
total_volume += volume;
|
|
|
|
int C = (inertia == mjMESH_INERTIA_SHELL) ? 12 : 20;
|
|
for (int j = 0; j < 6; j++) {
|
|
P[j] += volume /
|
|
C * (
|
|
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];
|
|
return total_volume;
|
|
}
|
|
|
|
|
|
|
|
void mjCMesh::Rotate(double quat[4]) {
|
|
// rotate vertices and normals of mesh by quaternion
|
|
double neg[4] = {quat[0], -quat[1], -quat[2], -quat[3]};
|
|
double mat[9];
|
|
mjuu_quat2mat(mat, neg);
|
|
for (int i = 0; i < nvert(); i++) {
|
|
mjuu_mulvecmat(&vert_[3*i], &vert_[3*i], mat);
|
|
|
|
// axis-aligned bounding box
|
|
aamm_[0] = std::min(aamm_[0], vert_[3*i + 0]);
|
|
aamm_[3] = std::max(aamm_[3], vert_[3*i + 0]);
|
|
aamm_[1] = std::min(aamm_[1], vert_[3*i + 1]);
|
|
aamm_[4] = std::max(aamm_[4], vert_[3*i + 1]);
|
|
aamm_[2] = std::min(aamm_[2], vert_[3*i + 2]);
|
|
aamm_[5] = std::max(aamm_[5], vert_[3*i + 2]);
|
|
}
|
|
|
|
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];
|
|
}
|
|
}
|
|
}
|
|
void mjCMesh::CheckInitialMesh() const {
|
|
if (vert_.size() < 12) {
|
|
throw mjCError(this, "at least 4 vertices required");
|
|
}
|
|
if (vert_.size() % 3) {
|
|
throw mjCError(this, "vertex data must be a multiple of 3");
|
|
}
|
|
if (normal_.size() % 3) {
|
|
throw mjCError(this, "normal data must be a multiple of 3");
|
|
}
|
|
if (texcoord_.size() % 2) {
|
|
throw mjCError(this, "texcoord must be a multiple of 2");
|
|
}
|
|
if (face_.size() % 3) {
|
|
throw mjCError(this, "face data must be a multiple of 3");
|
|
}
|
|
|
|
// check texcoord size if no face texcoord indices are given
|
|
if (!texcoord_.empty() && texcoord_.size() != 2 * nvert() &&
|
|
facetexcoord_.empty() && !IsObj()) {
|
|
throw mjCError(this,
|
|
"texcoord must be 2*nv if face texcoord indices are not provided in an OBJ file");
|
|
}
|
|
|
|
// require vertices
|
|
if (vert_.empty()) {
|
|
throw mjCError(this, "no vertices");
|
|
}
|
|
|
|
// check vertices exist
|
|
for (int i = 0; i < face_.size(); i++) {
|
|
if (face_[i] >= nvert() || face_[i] < 0) {
|
|
throw mjCError(this, "in face %d, vertex index %d does not exist",
|
|
nullptr, i / 3, face_[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// return inertia pointer
|
|
double* mjCMesh::GetInertiaBoxPtr() {
|
|
return boxsz_;
|
|
}
|
|
|
|
|
|
|
|
// return volume or surface area
|
|
double mjCMesh::GetVolumeRef() const {
|
|
return (inertia == mjMESH_INERTIA_SHELL) ? surface_ : volume_;
|
|
}
|
|
|
|
|
|
|
|
// make graph describing convex hull
|
|
void mjCMesh::MakeGraph() {
|
|
int adr, ok, curlong, totlong, exitcode;
|
|
facetT* facet, **facetp;
|
|
vertexT* vertex, *vertex1, **vertex1p;
|
|
|
|
std::string qhopt = "qhull Qt";
|
|
if (maxhullvert_ > -1) {
|
|
// qhull "TA" actually means "number of vertices added after the initial simplex"
|
|
qhopt += " TA" + std::to_string(maxhullvert_ - 4);
|
|
}
|
|
|
|
// graph not needed for small meshes
|
|
if (nvert() < 4) {
|
|
return;
|
|
}
|
|
|
|
qhT qh_qh;
|
|
qhT* qh = &qh_qh;
|
|
qh_zero(qh, stderr);
|
|
|
|
// qhull basic init
|
|
qh_init_A(qh, stdin, stdout, stderr, 0, nullptr);
|
|
|
|
// install longjmp error handler
|
|
exitcode = setjmp(qh->errexit);
|
|
qh->NOerrexit = false;
|
|
if (!exitcode) {
|
|
// actual init
|
|
qh_initflags(qh, const_cast<char*>(qhopt.c_str()));
|
|
qh_init_B(qh, vert_.data(), nvert(), 3, qh_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);
|
|
|
|
// 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);
|
|
if (graph_) {
|
|
mju_free(graph_);
|
|
szgraph_ = 0;
|
|
}
|
|
|
|
throw mjCError(this, "qhull error");
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// copy graph into face data
|
|
void mjCMesh::CopyGraph() {
|
|
// only if face data is missing
|
|
if (!face_.empty()) {
|
|
return;
|
|
}
|
|
|
|
// get info from graph, allocate
|
|
int numvert = graph_[0];
|
|
face_.assign(3*graph_[1], 0);
|
|
|
|
// 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 + 0] = graph_[j + 0];
|
|
face_[3*i + 1] = graph_[j + 1];
|
|
face_[3*i + 2] = graph_[j + 2];
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// make a mesh of a hemisphere (quad projected)
|
|
void mjCMesh::MakeHemisphere(int res, bool make_faces, bool make_cap) {
|
|
constexpr double kNorthPole[3] = {0, 0, 1};
|
|
constexpr double kEquator[4][3] = {
|
|
{1, 0, 0},
|
|
{0, 1, 0},
|
|
{-1, 0, 0},
|
|
{0, -1, 0},
|
|
};
|
|
|
|
// allocate vertices
|
|
int nvert = 1 + 2 * (res + 1) * (res + 2);
|
|
nvert += make_cap && make_faces; // add center vertex for bottom cap faces
|
|
std::vector<float> vert(3 * nvert);
|
|
|
|
// north pole
|
|
vert[0] = kNorthPole[0];
|
|
vert[1] = kNorthPole[1];
|
|
vert[2] = kNorthPole[2];
|
|
|
|
// iterate through rows from north pole to equator, compute vertices
|
|
int v = 1;
|
|
for (int row = 0; row <= res; row++) {
|
|
// iterate through the four sides
|
|
for (int side = 0; side < 4; side++) {
|
|
double factor = static_cast<double>(row + 1) / (res + 1);
|
|
double start[3], end[3];
|
|
|
|
// start and end points of current arc
|
|
for (int i = 0; i < 3; i++) {
|
|
start[i] = kNorthPole[i] + factor * (kEquator[side][i] - kNorthPole[i]);
|
|
end[i] = kNorthPole[i] + factor * (kEquator[(side + 1) % 4][i] - kNorthPole[i]);
|
|
}
|
|
|
|
// step size for interpolation along the arc
|
|
double delta[3];
|
|
for (int i = 0; i < 3; i++) {
|
|
delta[i] = (end[i] - start[i]) / (row + 1);
|
|
}
|
|
|
|
// interpolate points along the arc
|
|
for (int i = 0; i < row + 1; i++) {
|
|
double p[3];
|
|
for (int j = 0; j < 3; j++) {
|
|
p[j] = start[j] + i * delta[j];
|
|
}
|
|
|
|
// normalize point to lie on hemisphere surface
|
|
double norm = std::sqrt(p[0] * p[0] + p[1] * p[1] + p[2] * p[2]);
|
|
vert[3 * v + 0] = p[0] / norm;
|
|
vert[3 * v + 1] = p[1] / norm;
|
|
vert[3 * v + 2] = p[2] / norm;
|
|
v++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// optional center vertex for bottom cap (for symmetry)
|
|
if (make_faces && make_cap) {
|
|
vert[3 * (nvert - 1) + 0] = 0;
|
|
vert[3 * (nvert - 1) + 1] = 0;
|
|
vert[3 * (nvert - 1) + 2] = 0;
|
|
}
|
|
|
|
// save vertices
|
|
mjs_setFloat(spec.uservert, vert.data(), 3 * nvert);
|
|
|
|
if (make_faces) {
|
|
// allocate faces
|
|
int nface = 4 * (res + 1) * (res + 1);
|
|
nface += make_cap * (4 * (res + 1)); // bottom cap faces
|
|
std::vector<int> face(3 * nface);
|
|
|
|
// faces connected to north pole
|
|
int f = 0;
|
|
face[f++] = 0; face[f++] = 1; face[f++] = 2;
|
|
face[f++] = 0; face[f++] = 2; face[f++] = 3;
|
|
face[f++] = 0; face[f++] = 3; face[f++] = 4;
|
|
face[f++] = 0; face[f++] = 4; face[f++] = 1;
|
|
|
|
// faces on the hemisphere from north pole to equator
|
|
for (int row = 0; row < res; row++) {
|
|
const int start_curr = 2 * row * (row + 1) + 1;
|
|
const int count_curr = 4 * (row + 1);
|
|
const int start_next = start_curr + count_curr;
|
|
const int count_next = 4 * (row + 2);
|
|
|
|
for (int side = 0; side < 4; side++) {
|
|
for (int i = 0; i < row + 2; i++) {
|
|
const int v_curr = i + (row + 1) * side;
|
|
const int v_next = i + (row + 2) * side;
|
|
face[f++] = start_curr + v_curr % count_curr;
|
|
face[f++] = start_next + v_next % count_next;
|
|
face[f++] = start_next + (v_next + 1) % count_next;
|
|
|
|
if (i < row + 1) {
|
|
face[f++] = start_curr + v_curr % count_curr;
|
|
face[f++] = start_next + (v_next + 1) % count_next;
|
|
face[f++] = start_curr + (v_curr + 1) % count_curr;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (make_cap) {
|
|
// add faces for the bottom cap
|
|
const int start = 2 * res * (res + 1) + 1;
|
|
const int count = 4 * (res + 1);
|
|
for (int i = 0; i < count; i++) {
|
|
face[f++] = start + i;
|
|
face[f++] = nvert - 1;
|
|
face[f++] = start + (i + 1) % count;
|
|
}
|
|
}
|
|
|
|
// save faces
|
|
mjs_setInt(spec.userface, face.data(), 3 * nface);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// make a mesh of a sphere using icosaheral subdivision
|
|
void mjCMesh::MakeSphere(int subdiv, bool make_faces) {
|
|
// make icosahedron
|
|
const float phi = (1.0 + std::sqrt(5.0)) / 2.0;
|
|
std::vector<float> vert = {
|
|
-1.0, phi, 0.0,
|
|
1.0, phi, 0.0,
|
|
-1.0, -phi, 0.0,
|
|
1.0, -phi, 0.0,
|
|
|
|
0.0, -1.0, phi,
|
|
0.0, 1.0, phi,
|
|
0.0, -1.0, -phi,
|
|
0.0, 1.0, -phi,
|
|
|
|
phi, 0.0, -1.0,
|
|
phi, 0.0, 1.0,
|
|
-phi, 0.0, -1.0,
|
|
-phi, 0.0, 1.0,
|
|
};
|
|
|
|
// normalize vertices to be on a unit sphere
|
|
const double norm = std::sqrt(1.0 + phi * phi);
|
|
for (float& v : vert) {
|
|
v /= norm;
|
|
}
|
|
|
|
std::vector<int> face = {
|
|
0, 11, 5, 0, 5, 1, 0, 1, 7, 0, 7, 10, 0, 10, 11,
|
|
1, 5, 9, 5, 11, 4, 11, 10, 2, 10, 7, 6, 7, 1, 8,
|
|
3, 9, 4, 3, 4, 2, 3, 2, 6, 3, 6, 8, 3, 8, 9,
|
|
4, 9, 5, 2, 4, 11, 6, 2, 10, 8, 6, 7, 9, 8, 1
|
|
};
|
|
|
|
// subdivision
|
|
if (subdiv > 0) {
|
|
// helper to get or create a midpoint vertex
|
|
auto get_midpoint = [&vert](
|
|
int v1_idx, int v2_idx, std::map<std::pair<int, int>, int>& cache) -> int {
|
|
// key is the pair of vertex indices, sorted
|
|
std::pair<int, int> key = std::minmax(v1_idx, v2_idx);
|
|
|
|
// if midpoint is already in cache, return its index
|
|
auto it = cache.find(key);
|
|
if (it != cache.end()) {
|
|
return it->second;
|
|
}
|
|
|
|
// otherwise, create it
|
|
const float* v1 = &vert[v1_idx * 3];
|
|
const float* v2 = &vert[v2_idx * 3];
|
|
|
|
float mid_x = (v1[0] + v2[0]) / 2.0f;
|
|
float mid_y = (v1[1] + v2[1]) / 2.0f;
|
|
float mid_z = (v1[2] + v2[2]) / 2.0f;
|
|
|
|
// normalize the new vertex to put it on the sphere
|
|
float mid_norm = std::sqrt(mid_x * mid_x + mid_y * mid_y + mid_z * mid_z);
|
|
mid_x /= mid_norm;
|
|
mid_y /= mid_norm;
|
|
mid_z /= mid_norm;
|
|
|
|
// add the new vertex to the list
|
|
int new_idx = vert.size() / 3;
|
|
vert.push_back(mid_x);
|
|
vert.push_back(mid_y);
|
|
vert.push_back(mid_z);
|
|
|
|
// add to cache
|
|
cache[key] = new_idx;
|
|
|
|
return new_idx;
|
|
};
|
|
|
|
// subdivision loop
|
|
for (int i = 0; i < subdiv; ++i) {
|
|
std::map<std::pair<int, int>, int> midpoint_cache;
|
|
std::vector<int> new_face;
|
|
new_face.reserve(face.size() * 4);
|
|
for (size_t j = 0; j < face.size(); j += 3) {
|
|
int v1 = face[j];
|
|
int v2 = face[j+1];
|
|
int v3 = face[j+2];
|
|
|
|
int m12 = get_midpoint(v1, v2, midpoint_cache);
|
|
int m23 = get_midpoint(v2, v3, midpoint_cache);
|
|
int m31 = get_midpoint(v3, v1, midpoint_cache);
|
|
|
|
new_face.insert(new_face.end(), {v1, m12, m31});
|
|
new_face.insert(new_face.end(), {v2, m23, m12});
|
|
new_face.insert(new_face.end(), {v3, m31, m23});
|
|
new_face.insert(new_face.end(), {m12, m23, m31});
|
|
}
|
|
face = std::move(new_face);
|
|
}
|
|
}
|
|
|
|
// save vertices and maybe faces
|
|
mjs_setFloat(spec.uservert, vert.data(), vert.size());
|
|
if (make_faces) mjs_setInt(spec.userface, face.data(), face.size());
|
|
}
|
|
|
|
|
|
|
|
// make a mesh of a supersphere
|
|
void mjCMesh::MakeSupersphere(int res, double e, double n) {
|
|
// allocate vertices and faces
|
|
int nvert = (res - 1) * res + 2;
|
|
int nface = 2 * res * (res - 1);
|
|
std::vector<float> vert;
|
|
vert.reserve(3 * nvert);
|
|
std::vector<int> face;
|
|
face.reserve(3 * nface);
|
|
|
|
// south pole
|
|
vert.insert(vert.end(), {0.0f, 0.0f, -1.0f});
|
|
|
|
// rings
|
|
for (int i = 1; i < res; i++) {
|
|
double v = -mjPI/2 + i * mjPI / res;
|
|
for (int j = 0; j < res; j++) {
|
|
double u = -mjPI + j * 2 * mjPI / res;
|
|
vert.push_back(aux_c(v, n) * aux_c(u, e));
|
|
vert.push_back(aux_c(v, n) * aux_s(u, e));
|
|
vert.push_back(aux_s(v, n));
|
|
}
|
|
}
|
|
|
|
// north pole
|
|
vert.insert(vert.end(), {0.0f, 0.0f, 1.0f});
|
|
|
|
// south pole faces
|
|
for (int j = 0; j < res; j++) {
|
|
int v2 = 1 + j;
|
|
int v3 = 1 + (j + 1) % res;
|
|
face.insert(face.end(), {0, v3, v2});
|
|
}
|
|
|
|
// ring faces
|
|
for (int i = 0; i < res - 2; i++) {
|
|
for (int j = 0; j < res; j++) {
|
|
int v1 = 1 + i * res + j;
|
|
int v2 = 1 + i * res + (j + 1) % res;
|
|
int v4 = 1 + (i + 1) * res + j;
|
|
int v3 = 1 + (i + 1) * res + (j + 1) % res;
|
|
face.insert(face.end(), {v1, v2, v4});
|
|
face.insert(face.end(), {v2, v3, v4});
|
|
}
|
|
}
|
|
|
|
// north pole faces
|
|
int north_pole_idx = nvert - 1;
|
|
int last_ring_start_idx = 1 + (res - 2) * res;
|
|
for (int j = 0; j < res; j++) {
|
|
int v1 = last_ring_start_idx + j;
|
|
int v2 = last_ring_start_idx + (j + 1) % res;
|
|
face.insert(face.end(), {v1, v2, north_pole_idx});
|
|
}
|
|
|
|
// save vertices and faces
|
|
mjs_setFloat(spec.uservert, vert.data(), vert.size());
|
|
mjs_setInt(spec.userface, face.data(), face.size());
|
|
}
|
|
|
|
|
|
|
|
// make a mesh of a torus (subsumed by supertorus, kept for reference only)
|
|
void mjCMesh::MakeTorus(int res, double radius) {
|
|
// allocate vertices and faces
|
|
int nvert = res * res;
|
|
int nface = res * res * 2;
|
|
std::vector<float> vert(3 * nvert);
|
|
std::vector<int> face(3 * nface);
|
|
|
|
// generate vertices
|
|
for (int i = 0; i < res; ++i) {
|
|
for (int j = 0; j < res; ++j) {
|
|
double u = 2 * mjPI * i / res;
|
|
double v = 2 * mjPI * j / res;
|
|
int vidx = i * res + j;
|
|
vert[3 * vidx + 0] = (1 + radius * cos(v)) * cos(u);
|
|
vert[3 * vidx + 1] = (1 + radius * cos(v)) * sin(u);
|
|
vert[3 * vidx + 2] = radius * sin(v);
|
|
}
|
|
}
|
|
|
|
// generate faces
|
|
int fidx = 0;
|
|
for (int i = 0; i < res; ++i) {
|
|
for (int j = 0; j < res; ++j) {
|
|
int i_next = (i + 1) % res;
|
|
int j_next = (j + 1) % res;
|
|
|
|
int v1 = i * res + j;
|
|
int v2 = i_next * res + j;
|
|
int v3 = i_next * res + j_next;
|
|
int v4 = i * res + j_next;
|
|
|
|
// first triangle
|
|
face[3 * fidx + 0] = v1;
|
|
face[3 * fidx + 1] = v2;
|
|
face[3 * fidx + 2] = v4;
|
|
fidx++;
|
|
|
|
// second triangle
|
|
face[3 * fidx + 0] = v2;
|
|
face[3 * fidx + 1] = v3;
|
|
face[3 * fidx + 2] = v4;
|
|
fidx++;
|
|
}
|
|
}
|
|
|
|
// save vertices and faces
|
|
mjs_setFloat(spec.uservert, vert.data(), vert.size());
|
|
mjs_setInt(spec.userface, face.data(), face.size());
|
|
}
|
|
|
|
|
|
|
|
// make a mesh of a supertoroid, see https://en.wikipedia.org/wiki/Supertoroid
|
|
void mjCMesh::MakeSupertorus(int res, double radius, double s, double t) {
|
|
// allocate vertices and faces
|
|
int nvert = res * res;
|
|
int nface = res * res * 2;
|
|
std::vector<float> vert(3 * nvert);
|
|
std::vector<int> face(3 * nface);
|
|
|
|
// generate vertices
|
|
for (int i = 0; i < res; ++i) {
|
|
for (int j = 0; j < res; ++j) {
|
|
double u = 2 * mjPI * i / res;
|
|
double v = 2 * mjPI * j / res;
|
|
int vidx = i * res + j;
|
|
vert[3 * vidx + 0] = (1 + radius * aux_c(v, s)) * aux_c(u, t);
|
|
vert[3 * vidx + 1] = (1 + radius * aux_c(v, s)) * aux_s(u, t);
|
|
vert[3 * vidx + 2] = radius * aux_s(v, s);
|
|
}
|
|
}
|
|
|
|
// generate faces
|
|
int fidx = 0;
|
|
for (int i = 0; i < res; ++i) {
|
|
for (int j = 0; j < res; ++j) {
|
|
int i_next = (i + 1) % res;
|
|
int j_next = (j + 1) % res;
|
|
|
|
int v1 = i * res + j;
|
|
int v2 = i_next * res + j;
|
|
int v3 = i_next * res + j_next;
|
|
int v4 = i * res + j_next;
|
|
|
|
// first triangle
|
|
face[3 * fidx + 0] = v1;
|
|
face[3 * fidx + 1] = v2;
|
|
face[3 * fidx + 2] = v4;
|
|
fidx++;
|
|
|
|
// second triangle
|
|
face[3 * fidx + 0] = v2;
|
|
face[3 * fidx + 1] = v3;
|
|
face[3 * fidx + 2] = v4;
|
|
fidx++;
|
|
}
|
|
}
|
|
|
|
// save vertices and faces
|
|
mjs_setFloat(spec.uservert, vert.data(), vert.size());
|
|
mjs_setInt(spec.userface, face.data(), face.size());
|
|
}
|
|
|
|
|
|
|
|
// make a mesh of a spherical wedge
|
|
void mjCMesh::MakeWedge(int resolution[2], double fov[2], double gamma) {
|
|
std::vector<double> x_edges(resolution[0] + 1, 0);
|
|
std::vector<double> y_edges(resolution[1] + 1, 0);
|
|
BinEdges(x_edges.data(), y_edges.data(), resolution, fov, gamma);
|
|
std::vector<float> uservert(3 * resolution[0] * resolution[1], 0);
|
|
std::vector<float> usernormal(9 * resolution[0] * resolution[1], 0);
|
|
|
|
for (int i = 0; i < resolution[0]; i++) {
|
|
for (int j = 0; j < resolution[1]; j++) {
|
|
double aer[3];
|
|
aer[0] = 0.5 * (x_edges[i + 1] + x_edges[i]);
|
|
aer[1] = 0.5 * (y_edges[j + 1] + y_edges[j]);
|
|
aer[2] = 1;
|
|
SphericalToCartesian(aer, uservert.data() + 3 * (i * resolution[1] + j));
|
|
TangentFrame(aer, usernormal.data() + 9 * (i * resolution[1] + j));
|
|
}
|
|
}
|
|
|
|
mjs_setFloat(spec.uservert, uservert.data(),
|
|
3 * resolution[0] * resolution[1]);
|
|
mjs_setFloat(spec.usernormal, usernormal.data(),
|
|
9 * resolution[0] * resolution[1]);
|
|
}
|
|
|
|
|
|
|
|
// make a mesh of a rectangle
|
|
void mjCMesh::MakeRect(int resolution[2]) {
|
|
std::vector<double> x_edges(resolution[0] + 1, 0);
|
|
std::vector<double> y_edges(resolution[1] + 1, 0);
|
|
LinSpace(-1, 1, resolution[0] + 1, x_edges.data());
|
|
LinSpace(-1, 1, resolution[1] + 1, y_edges.data());
|
|
std::vector<float> uservert(3 * resolution[0] * resolution[1], 0);
|
|
std::vector<float> usernormal(9 * resolution[0] * resolution[1], 0);
|
|
std::vector<int> userface(6 * (resolution[0] - 1) * (resolution[1] - 1), 0);
|
|
spec.inertia = mjMESH_INERTIA_SHELL;
|
|
|
|
for (int i = 0; i < resolution[0]; i++) {
|
|
for (int j = 0; j < resolution[1]; j++) {
|
|
int vert = i * resolution[1] + j;
|
|
mjtNum dx = 2. / resolution[0];
|
|
mjtNum dy = 2. / resolution[1];
|
|
uservert[3 * vert + 0] = -1 + (i + 0.5) * dx;
|
|
uservert[3 * vert + 1] = -1 + (j + 0.5) * dy;
|
|
uservert[3 * vert + 2] = -1;
|
|
usernormal[9 * vert + 0] = 1;
|
|
usernormal[9 * vert + 4] = 1;
|
|
usernormal[9 * vert + 8] = 1;
|
|
if (i > 0 && j > 0) {
|
|
int cell = (i - 1) * (resolution[1] - 1) + j - 1;
|
|
userface[6 * cell + 0] = (i - 1) * resolution[1] + j - 1;
|
|
userface[6 * cell + 1] = (i - 0) * resolution[1] + j - 1;
|
|
userface[6 * cell + 2] = (i - 1) * resolution[1] + j - 0;
|
|
userface[6 * cell + 3] = (i - 0) * resolution[1] + j - 0;
|
|
userface[6 * cell + 4] = (i - 1) * resolution[1] + j - 0;
|
|
userface[6 * cell + 5] = (i - 0) * resolution[1] + j - 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
mjs_setFloat(spec.uservert, uservert.data(),
|
|
3 * resolution[0] * resolution[1]);
|
|
mjs_setFloat(spec.usernormal, usernormal.data(),
|
|
9 * resolution[0] * resolution[1]);
|
|
mjs_setInt(spec.userface, userface.data(),
|
|
6 * (resolution[0] - 1) * (resolution[1] - 1));
|
|
}
|
|
|
|
|
|
|
|
// make a mesh of a generalized discrete cone
|
|
void mjCMesh::MakeCone(int nedge, double radius) {
|
|
int n = 3 * (nedge + (radius > 0 ? nedge : 1));
|
|
std::vector<float> uservert(n, 0);
|
|
|
|
// bottom face
|
|
for (int i = 0; i < nedge; i++) {
|
|
uservert[3 * i + 0] = cos(2 * i * mjPI / nedge);
|
|
uservert[3 * i + 1] = sin(2 * i * mjPI / nedge);
|
|
uservert[3 * i + 2] = -1;
|
|
}
|
|
|
|
// top face or single point
|
|
if (radius > 0) {
|
|
for (int i = nedge; i < 2 * nedge; i++) {
|
|
uservert[3 * i + 0] = radius * cos(2 * i * mjPI / nedge);
|
|
uservert[3 * i + 1] = radius * sin(2 * i * mjPI / nedge);
|
|
uservert[3 * i + 2] = 1;
|
|
}
|
|
} else {
|
|
uservert[3 * nedge + 2] = 1;
|
|
}
|
|
|
|
mjs_setFloat(spec.uservert, uservert.data(), n);
|
|
}
|
|
|
|
|
|
|
|
// compute vertex normals
|
|
void mjCMesh::MakeNormal() {
|
|
// only if normal data is missing
|
|
if (!normal_.empty()) {
|
|
return;
|
|
}
|
|
|
|
// allocate and clear normals
|
|
normal_.assign(3*nvert(), 0);
|
|
|
|
if (facenormal_.empty()) {
|
|
facenormal_.assign(3*nface(), 0);
|
|
}
|
|
|
|
// 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
|
|
double 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
|
|
double nrm[3];
|
|
mjuu_crossvec(nrm, vec01, vec02);
|
|
double area = mjuu_normvec(nrm, 3);
|
|
|
|
// 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
|
|
double 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
|
|
double nrm[3];
|
|
mjuu_crossvec(nrm, vec01, vec02);
|
|
double area = mjuu_normvec(nrm, 3);
|
|
|
|
// compare to vertex normal, subtract contribution if dot product too small
|
|
for (int j=0; j < 3; j++) {
|
|
// normalized vertex normal
|
|
double vnrm[3] = {normal_[3*vertid[j]], normal_[3*vertid[j]+1], normal_[3*vertid[j]+2]};
|
|
mjuu_normvec(vnrm, 3);
|
|
|
|
// dot too small: remove
|
|
if (mjuu_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() {
|
|
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_.data() + 3*i;
|
|
|
|
// get triangle edges
|
|
double 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
|
|
double nrm[3];
|
|
mjuu_crossvec(nrm, a, b);
|
|
|
|
// compute circumradius
|
|
double norm_a_2 = mjuu_dot3(a, a);
|
|
double norm_b_2 = mjuu_dot3(b, b);
|
|
double area = sqrt(mjuu_dot3(nrm, nrm));
|
|
|
|
// compute circumcenter
|
|
double 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]
|
|
};
|
|
mjuu_crossvec(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];
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// compute the normals of the polygons
|
|
void mjCMesh::MakePolygonNormals() {
|
|
for (int i = 0; i < polygons_.size(); ++i) {
|
|
double n[3];
|
|
mjuu_makenormal(n, &vert_[3*polygons_[i][0]], &vert_[3*polygons_[i][1]],
|
|
&vert_[3*polygons_[i][2]]);
|
|
polygon_normals_[3*i + 0] = n[0];
|
|
polygon_normals_[3*i + 1] = n[1];
|
|
polygon_normals_[3*i + 2] = n[2];
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// helper class to compute the polygons of a mesh
|
|
class MeshPolygon {
|
|
public:
|
|
// constructors (need starting face)
|
|
MeshPolygon(const double v1[3], const double v2[3], const double v3[3],
|
|
int v1i, int v2i, int v3i);
|
|
MeshPolygon() = delete;
|
|
MeshPolygon(const MeshPolygon&) = delete;
|
|
MeshPolygon& operator=(const MeshPolygon&) = delete;
|
|
|
|
void InsertFace(int v1, int v2, int v3); // insert a face into the polygon
|
|
std::vector<std::vector<int>> Paths() const; // return trace of the polygons
|
|
const double* Normal() const { return normal_; } // return the normal of the polygon
|
|
|
|
// return the ith component of the normal of the polygon
|
|
double Normal(int i) const { return normal_[i]; }
|
|
|
|
private:
|
|
std::vector<std::pair<int, int>> edges_;
|
|
|
|
// inserted faces do not necessarily share edges with the current polygon, so they're grouped as
|
|
// islands until they can be combined with later face insertions
|
|
std::vector<int> islands_;
|
|
int nisland_ = 0;
|
|
double normal_[3] = {0.0, 0.0, 0.0};
|
|
void CombineIslands(int& island1, int& island2);
|
|
};
|
|
|
|
|
|
|
|
MeshPolygon::MeshPolygon(const double v1[3], const double v2[3], const double v3[3],
|
|
int v1i, int v2i, int v3i) {
|
|
mjuu_makenormal(normal_, v1, v2, v3);
|
|
edges_ = {{v1i, v2i}, {v2i, v3i}, {v3i, v1i}};
|
|
nisland_ = 1;
|
|
islands_ = {0, 0, 0};
|
|
}
|
|
|
|
|
|
|
|
// comparison operator for std::set
|
|
bool PolygonCmp(const MeshPolygon& p1, const MeshPolygon& p2) {
|
|
const double* n1 = p1.Normal();
|
|
const double* n2 = p2.Normal();
|
|
double dot3 = n1[0] * n2[0] + n1[1] * n2[1] + n1[2] * n2[2];
|
|
|
|
// TODO(kylebayes): The tolerance should be a parameter set the user, as it should be optimized
|
|
// from mesh to mesh.
|
|
if (dot3 > 0.99999872) {
|
|
return false;
|
|
}
|
|
|
|
if (std::abs(n1[0] - n2[0]) > mjMINVAL) {
|
|
return n1[0] > n2[0];
|
|
}
|
|
if (std::abs(n1[1] - n2[1]) > mjMINVAL) {
|
|
return n1[1] > n2[1];
|
|
}
|
|
if (std::abs(n1[2] - n2[2]) > mjMINVAL) {
|
|
return n1[2] > n2[2];
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
|
|
// combine two islands when a newly inserted face connects them
|
|
void MeshPolygon::CombineIslands(int& island1, int& island2) {
|
|
// pick the smaller island
|
|
if (island2 < island1) {
|
|
int tmp = island1;
|
|
island1 = island2;
|
|
island2 = tmp;
|
|
}
|
|
|
|
// renumber the islands
|
|
for (int k = 0; k < islands_.size(); ++k) {
|
|
if (islands_[k] == island2) {
|
|
islands_[k] = island1;
|
|
} else if (islands_[k] > island2) {
|
|
islands_[k]--;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// insert a triangular face into the polygon
|
|
void MeshPolygon::InsertFace(int v1, int v2, int v3) {
|
|
int add1 = 1, add2 = 1, add3 = 1;
|
|
int island = -1;
|
|
|
|
// check if face can be attached via edge v1v2
|
|
for (int i = 0; i < edges_.size(); ++i) {
|
|
if (edges_[i].first == v2 && edges_[i].second == v1) {
|
|
add1 = 0;
|
|
island = islands_[i];
|
|
edges_.erase(edges_.begin() + i);
|
|
islands_.erase(islands_.begin() + i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// check if face can be attached via edge v2v3
|
|
for (int i = 0; i < edges_.size(); ++i) {
|
|
if (edges_[i].first == v3 && edges_[i].second == v2) {
|
|
int island2 = islands_[i];
|
|
if (island == -1) {
|
|
island = island2;
|
|
} else if (island2 != island) {
|
|
nisland_--;
|
|
CombineIslands(island, island2);
|
|
}
|
|
add2 = 0;
|
|
edges_.erase(edges_.begin() + i);
|
|
islands_.erase(islands_.begin() + i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// check if face can be attached via edge v3v1
|
|
for (int i = 0; i < edges_.size(); ++i) {
|
|
if (edges_[i].first == v1 && edges_[i].second == v3) {
|
|
int island3 = islands_[i];
|
|
if (island == -1) {
|
|
island = island3;
|
|
} else if (island3 != island) {
|
|
nisland_--;
|
|
CombineIslands(island, island3);
|
|
}
|
|
add3 = 0;
|
|
edges_.erase(edges_.begin() + i);
|
|
islands_.erase(islands_.begin() + i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (island == -1) {
|
|
island = nisland_++;
|
|
}
|
|
|
|
// add only new edges to the polygon
|
|
|
|
if (add1) {
|
|
edges_.push_back({v1, v2});
|
|
islands_.push_back(island);
|
|
}
|
|
if (add2) {
|
|
edges_.push_back({v2, v3});
|
|
islands_.push_back(island);
|
|
}
|
|
if (add3) {
|
|
edges_.push_back({v3, v1});
|
|
islands_.push_back(island);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// return the transverse vertices of the polygon, multiple paths possible if not connected
|
|
std::vector<std::vector<int> > MeshPolygon::Paths() const {
|
|
std::vector<std::vector<int> > paths;
|
|
// shortcut if polygon is just a triangular face
|
|
if (edges_.size() == 3) {
|
|
return {{edges_[0].first, edges_[1].first, edges_[2].first}};
|
|
}
|
|
|
|
// go through each connected component of the polygon
|
|
for (int i = 0; i < nisland_; ++i) {
|
|
std::vector<int> path;
|
|
|
|
// find starting vertex
|
|
for (int j = 0; j < edges_.size(); ++j) {
|
|
if (islands_[j] == i) {
|
|
path.push_back(edges_[j].first);
|
|
path.push_back(edges_[j].second);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// SHOULD NOT OCCUR (See logic in MeshPolygon::CombineIslands)
|
|
if (path.empty()) {
|
|
continue;
|
|
}
|
|
|
|
// visit the next vertex given the current edge
|
|
int next = path.back();
|
|
for (int l = 0; l < edges_.size(); ++l) {
|
|
int finished = 0;
|
|
for (int k = 1; k < edges_.size(); ++k) {
|
|
if (islands_[k] == i && edges_[k].first == next) {
|
|
next = edges_[k].second;
|
|
if (next == path[0]) {
|
|
paths.push_back(path);
|
|
finished = 1;
|
|
break;
|
|
}
|
|
path.push_back(next);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// back at start
|
|
if (finished) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return paths;
|
|
}
|
|
|
|
|
|
|
|
// merge coplanar mesh triangular faces into polygonal sides to represent the geometry of the mesh
|
|
void mjCMesh::MakePolygons() {
|
|
std::set<MeshPolygon, decltype(PolygonCmp)*> polygons(PolygonCmp);
|
|
polygons_.clear();
|
|
polygon_normals_.clear();
|
|
polygon_map_.clear();
|
|
|
|
// initialize polygon map
|
|
for (int i = 0; i < nvert(); i++) {
|
|
polygon_map_.push_back(std::vector<int>());
|
|
}
|
|
|
|
// use graph data if available
|
|
int *faces, nfaces;
|
|
if (graph_) {
|
|
nfaces = graph_[1];
|
|
faces = GraphFaces();
|
|
} else {
|
|
nfaces = nface();
|
|
faces = face_.data();
|
|
}
|
|
|
|
// process each face
|
|
for (int i = 0; i < nfaces; i++) {
|
|
double* v1 = &vert_[3*faces[3*i + 0]];
|
|
double* v2 = &vert_[3*faces[3*i + 1]];
|
|
double* v3 = &vert_[3*faces[3*i + 2]];
|
|
|
|
MeshPolygon face(v1, v2, v3, faces[3*i + 0], faces[3*i + 1], faces[3*i + 2]);
|
|
auto it = polygons.find(face);
|
|
if (it == polygons.end()) {
|
|
polygons.emplace(v1, v2, v3, faces[3*i + 0], faces[3*i + 1], faces[3*i + 2]);
|
|
} else {
|
|
MeshPolygon& p = const_cast<MeshPolygon&>(*it);
|
|
p.InsertFace(faces[3*i + 0], faces[3*i + 1], faces[3*i + 2]);
|
|
}
|
|
}
|
|
|
|
for (const auto& polygon : polygons) {
|
|
std::vector<std::vector<int> > paths = polygon.Paths();
|
|
|
|
// separate the polygons if they were grouped together
|
|
for (const auto& path : paths) {
|
|
if (path.size() < 3) continue;
|
|
polygons_.push_back(path);
|
|
polygon_normals_.push_back(polygon.Normal(0));
|
|
polygon_normals_.push_back(polygon.Normal(1));
|
|
polygon_normals_.push_back(polygon.Normal(2));
|
|
}
|
|
}
|
|
|
|
// populate the polygon map
|
|
for (int i = 0; i < polygons_.size(); i++) {
|
|
for (int j = 0; j < polygons_[i].size(); ++j) {
|
|
polygon_map_[polygons_[i][j]].push_back(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
//------------------ class mjCSkin implementation --------------------------------------------------
|
|
|
|
// constructor
|
|
mjCSkin::mjCSkin(mjCModel* _model) {
|
|
mjs_defaultSkin(&spec);
|
|
elemtype = mjOBJ_SKIN;
|
|
|
|
// set model pointer
|
|
model = _model;
|
|
if (model) compiler = &model->spec.compiler;
|
|
|
|
// 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<mjsElement*>(this);
|
|
spec.file = &spec_file_;
|
|
spec.material = &spec_material_;
|
|
spec.vert = &spec_vert_;
|
|
spec.texcoord = &spec_texcoord_;
|
|
spec.face = &spec_face_;
|
|
spec.bodyname = &spec_bodyname_;
|
|
spec.bindpos = &spec_bindpos_;
|
|
spec.bindquat = &spec_bindquat_;
|
|
spec.vertid = &spec_vertid_;
|
|
spec.vertweight = &spec_vertweight_;
|
|
spec.info = &info;
|
|
file = nullptr;
|
|
material = nullptr;
|
|
vert = nullptr;
|
|
texcoord = nullptr;
|
|
face = nullptr;
|
|
bodyname = nullptr;
|
|
bindpos = nullptr;
|
|
bindquat = nullptr;
|
|
vertid = nullptr;
|
|
vertweight = nullptr;
|
|
}
|
|
|
|
|
|
|
|
void mjCSkin::NameSpace(const mjCModel* m) {
|
|
// use filename if name is missing
|
|
if (name.empty()) {
|
|
std::string stripped = mjuu_strippath(spec_file_);
|
|
name = mjuu_stripext(stripped);
|
|
}
|
|
for (auto& name : spec_bodyname_) {
|
|
name = m->prefix + name + m->suffix;
|
|
}
|
|
if (modelfiledir_.empty()) {
|
|
modelfiledir_ = FilePath(m->spec_modelfiledir_);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
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_;
|
|
|
|
// use filename if name is missing
|
|
if (name.empty()) {
|
|
std::string stripped = mjuu_strippath(file_);
|
|
name = mjuu_stripext(stripped);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 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 (!spec_vert_.empty() ||
|
|
!spec_texcoord_.empty() ||
|
|
!spec_face_.empty() ||
|
|
!spec_bodyname_.empty() ||
|
|
!spec_bindpos_.empty() ||
|
|
!spec_bindquat_.empty() ||
|
|
!spec_vertid_.empty() ||
|
|
!spec_vertweight_.empty()) {
|
|
throw mjCError(this, "Both skin data and file were specified: %s", file_.c_str());
|
|
}
|
|
|
|
// remove path from file if necessary
|
|
if (model->strippath) {
|
|
file_ = mjuu_strippath(file_);
|
|
}
|
|
|
|
// load SKN
|
|
std::string ext = mjuu_getext(file_);
|
|
if (strcasecmp(ext.c_str(), ".skn")) {
|
|
throw mjCError(this, "Unknown skin file type: %s", file_.c_str());
|
|
}
|
|
|
|
// copy paths from model if not already defined
|
|
if (modelfiledir_.empty()) {
|
|
modelfiledir_ = FilePath(model->modelfiledir_);
|
|
}
|
|
mujoco::user::FilePath meshdir_;
|
|
meshdir_ = FilePath(mjs_getString(compiler->meshdir));
|
|
|
|
FilePath filename = meshdir_ + FilePath(file_);
|
|
mjResource* resource = LoadResource(modelfiledir_.Str(), filename.Str(), 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, "unknown material '%s' in skin", material_.c_str());
|
|
}
|
|
|
|
// set total vertex weights to 0
|
|
std::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", nullptr, 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", nullptr, 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++) {
|
|
double quat[4] = {
|
|
(double)bindquat_[4*i],
|
|
(double)bindquat_[4*i+1],
|
|
(double)bindquat_[4*i+2],
|
|
(double)bindquat_[4*i+3]
|
|
};
|
|
mjuu_normvec(quat, 4);
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
//-------------------------- nonlinear elasticity --------------------------------------------------
|
|
|
|
// 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 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}}};
|
|
|
|
struct Stencil2D {
|
|
static constexpr int kNumEdges = 3;
|
|
static constexpr int kNumVerts = 3;
|
|
static constexpr int kNumFaces = 2;
|
|
static constexpr int edge[kNumEdges][2] = {{1, 2}, {2, 0}, {0, 1}};
|
|
static constexpr int face[kNumVerts][2] = {{1, 2}, {2, 0}, {0, 1}};
|
|
static constexpr int edge2face[kNumEdges][2] = {{1, 2}, {2, 0}, {0, 1}};
|
|
int vertices[kNumVerts];
|
|
int edges[kNumEdges];
|
|
};
|
|
|
|
struct Stencil3D {
|
|
static constexpr int kNumEdges = 6;
|
|
static constexpr int kNumVerts = 4;
|
|
static constexpr int kNumFaces = 3;
|
|
static constexpr int edge[kNumEdges][2] = {{0, 1}, {1, 2}, {2, 0},
|
|
{2, 3}, {0, 3}, {1, 3}};
|
|
static constexpr int face[kNumVerts][3] = {{2, 1, 0}, {0, 1, 3},
|
|
{1, 2, 3}, {2, 0, 3}};
|
|
static constexpr int edge2face[kNumEdges][2] = {{2, 3}, {1, 3}, {2, 1},
|
|
{1, 0}, {0, 2}, {0, 3}};
|
|
int vertices[kNumVerts];
|
|
int edges[kNumEdges];
|
|
};
|
|
|
|
template <typename T>
|
|
inline double ComputeVolume(const double* x, const int v[T::kNumVerts]);
|
|
|
|
template <>
|
|
inline double ComputeVolume<Stencil2D>(const double* x,
|
|
const int v[Stencil2D::kNumVerts]) {
|
|
double normal[3];
|
|
const double* x0 = x + 3*v[0];
|
|
const double* x1 = x + 3*v[1];
|
|
const double* x2 = x + 3*v[2];
|
|
double edge1[3] = {x1[0]-x0[0], x1[1]-x0[1], x1[2]-x0[2]};
|
|
double edge2[3] = {x2[0]-x0[0], x2[1]-x0[1], x2[2]-x0[2]};
|
|
mjuu_crossvec(normal, edge1, edge2);
|
|
return mjuu_normvec(normal, 3) / 2;
|
|
}
|
|
|
|
template<>
|
|
inline double ComputeVolume<Stencil3D>(const double* x,
|
|
const int v[Stencil3D::kNumVerts]) {
|
|
double normal[3];
|
|
const double* x0 = x + 3*v[0];
|
|
const double* x1 = x + 3*v[1];
|
|
const double* x2 = x + 3*v[2];
|
|
const double* x3 = x + 3*v[3];
|
|
double edge1[3] = {x1[0]-x0[0], x1[1]-x0[1], x1[2]-x0[2]};
|
|
double edge2[3] = {x2[0]-x0[0], x2[1]-x0[1], x2[2]-x0[2]};
|
|
double edge3[3] = {x3[0]-x0[0], x3[1]-x0[1], x3[2]-x0[2]};
|
|
mjuu_crossvec(normal, edge1, edge2);
|
|
return mjuu_dot3(normal, edge3) / 6;
|
|
}
|
|
|
|
// compute metric tensor of edge lengths inner product
|
|
template <typename T>
|
|
void inline MetricTensor(double* metric, int idx, double mu,
|
|
double la, const double basis[T::kNumEdges][9]) {
|
|
double trE[T::kNumEdges] = {0};
|
|
double trEE[T::kNumEdges*T::kNumEdges] = {0};
|
|
double k[T::kNumEdges*T::kNumEdges];
|
|
|
|
// compute first invariant i.e. trace(strain)
|
|
for (int e = 0; e < T::kNumEdges; e++) {
|
|
for (int i = 0; i < 3; i++) {
|
|
trE[e] += basis[e][4*i];
|
|
}
|
|
}
|
|
|
|
// compute second invariant i.e. trace(strain^2)
|
|
for (int ed1 = 0; ed1 < T::kNumEdges; ed1++) {
|
|
for (int ed2 = 0; ed2 < T::kNumEdges; ed2++) {
|
|
for (int i = 0; i < 3; i++) {
|
|
for (int j = 0; j < 3; j++) {
|
|
trEE[T::kNumEdges*ed1+ed2] += basis[ed1][3*i+j] * basis[ed2][3*j+i];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// assembly of strain metric tensor
|
|
for (int ed1 = 0; ed1 < T::kNumEdges; ed1++) {
|
|
for (int ed2 = 0; ed2 < T::kNumEdges; ed2++) {
|
|
k[T::kNumEdges*ed1 + ed2] = mu * trEE[T::kNumEdges * ed1 + ed2] +
|
|
la * trE[ed2] * trE[ed1];
|
|
}
|
|
}
|
|
|
|
// copy to triangular representation
|
|
int id = 0;
|
|
for (int ed1 = 0; ed1 < T::kNumEdges; ed1++) {
|
|
for (int ed2 = ed1; ed2 < T::kNumEdges; ed2++) {
|
|
metric[21*idx + id++] = k[T::kNumEdges*ed1 + ed2];
|
|
}
|
|
}
|
|
|
|
if (id != T::kNumEdges*(T::kNumEdges+1)/2) {
|
|
mju_error("incorrect stiffness matrix size");
|
|
}
|
|
}
|
|
|
|
// compute local basis
|
|
template <typename T>
|
|
void inline ComputeBasis(double basis[9], const double* x,
|
|
const int v[T::kNumVerts],
|
|
const int faceL[T::kNumFaces],
|
|
const int faceR[T::kNumFaces], double volume);
|
|
|
|
template <>
|
|
void inline ComputeBasis<Stencil2D>(double basis[9], const double* x,
|
|
const int v[Stencil2D::kNumVerts],
|
|
const int faceL[Stencil2D::kNumFaces],
|
|
const int faceR[Stencil2D::kNumFaces],
|
|
double volume) {
|
|
double basisL[3], basisR[3];
|
|
double normal[3];
|
|
|
|
const double* xL0 = x + 3*v[faceL[0]];
|
|
const double* xL1 = x + 3*v[faceL[1]];
|
|
const double* xR0 = x + 3*v[faceR[0]];
|
|
const double* xR1 = x + 3*v[faceR[1]];
|
|
double edgesL[3] = {xL0[0]-xL1[0], xL0[1]-xL1[1], xL0[2]-xL1[2]};
|
|
double edgesR[3] = {xR1[0]-xR0[0], xR1[1]-xR0[1], xR1[2]-xR0[2]};
|
|
|
|
mjuu_crossvec(normal, edgesR, edgesL);
|
|
mjuu_normvec(normal, 3);
|
|
mjuu_crossvec(basisL, normal, edgesL);
|
|
mjuu_crossvec(basisR, edgesR, normal);
|
|
|
|
// we use as basis the symmetrized tensor products of the edge normals of the
|
|
// other two edges; this is shown in Weischedel "A discrete geometric view on
|
|
// shear-deformable shell models" in the remark at the end of section 4.1;
|
|
// equivalent to linear finite elements but in a coordinate-free formulation.
|
|
|
|
for (int i = 0; i < 3; i++) {
|
|
for (int j = 0; j < 3; j++) {
|
|
basis[3*i+j] = (basisL[i]*basisR[j] +
|
|
basisR[i]*basisL[j]) / (8*volume*volume);
|
|
}
|
|
}
|
|
}
|
|
|
|
// compute local basis
|
|
template <>
|
|
void inline ComputeBasis<Stencil3D>(double basis[9], const double* x,
|
|
const int v[Stencil3D::kNumVerts],
|
|
const int faceL[Stencil3D::kNumFaces],
|
|
const int faceR[Stencil3D::kNumFaces],
|
|
double volume) {
|
|
const double* xL0 = x + 3*v[faceL[0]];
|
|
const double* xL1 = x + 3*v[faceL[1]];
|
|
const double* xL2 = x + 3*v[faceL[2]];
|
|
const double* xR0 = x + 3*v[faceR[0]];
|
|
const double* xR1 = x + 3*v[faceR[1]];
|
|
const double* xR2 = x + 3*v[faceR[2]];
|
|
double edgesL[6] = {xL1[0] - xL0[0], xL1[1] - xL0[1], xL1[2] - xL0[2],
|
|
xL2[0] - xL0[0], xL2[1] - xL0[1], xL2[2] - xL0[2]};
|
|
double edgesR[6] = {xR1[0] - xR0[0], xR1[1] - xR0[1], xR1[2] - xR0[2],
|
|
xR2[0] - xR0[0], xR2[1] - xR0[1], xR2[2] - xR0[2]};
|
|
|
|
double normalL[3], normalR[3];
|
|
mjuu_crossvec(normalL, edgesL, edgesL+3);
|
|
mjuu_crossvec(normalR, edgesR, edgesR+3);
|
|
|
|
// we use as basis the symmetrized tensor products of the area normals of the
|
|
// two faces not adjacent to the edge; this is the 3D equivalent to the basis
|
|
// proposed in Weischedel "A discrete geometric view on shear-deformable shell
|
|
// models" in the remark at the end of section 4.1. This is also equivalent to
|
|
// linear finite elements but in a coordinate-free formulation.
|
|
|
|
for (int i = 0; i < 3; i++) {
|
|
for (int j = 0; j < 3; j++) {
|
|
basis[3*i+j] = (normalL[i]*normalR[j] +
|
|
normalR[i]*normalL[j]) / (36*2*volume*volume);
|
|
}
|
|
}
|
|
}
|
|
|
|
// compute stiffness for a single element
|
|
template <typename T>
|
|
void inline ComputeStiffness(std::vector<double>& stiffness,
|
|
const std::vector<double>& body_pos,
|
|
const int* v, int t, double E,
|
|
double nu, double thickness = 4) {
|
|
// triangles area
|
|
double volume = ComputeVolume<T>(body_pos.data(), v);
|
|
|
|
// material parameters
|
|
double mu = E / (2*(1+nu)) * std::abs(volume) / 4 * thickness;
|
|
double la = E*nu / ((1+nu)*(1-2*nu)) * std::abs(volume) / 4 * thickness;
|
|
|
|
// local geometric quantities
|
|
double basis[T::kNumEdges][9] = {{0}};
|
|
|
|
// compute edge basis
|
|
for (int e = 0; e < T::kNumEdges; e++) {
|
|
ComputeBasis<T>(basis[e], body_pos.data(), v,
|
|
T::face[T::edge2face[e][0]],
|
|
T::face[T::edge2face[e][1]], volume);
|
|
}
|
|
|
|
// compute metric tensor
|
|
MetricTensor<T>(stiffness.data(), t, mu, la, basis);
|
|
}
|
|
|
|
// local tetrahedron numbering
|
|
constexpr int kNumEdges = Stencil2D::kNumEdges;
|
|
constexpr int kNumVerts = Stencil2D::kNumVerts;
|
|
constexpr int edge[kNumEdges][2] = {{1, 2}, {2, 0}, {0, 1}};
|
|
|
|
// create map from triangles to vertices and edges and from edges to vertices
|
|
static void CreateFlapStencil(std::vector<StencilFlap>& flaps,
|
|
const std::vector<int>& simplex,
|
|
const std::vector<int>& edgeidx) {
|
|
// populate stencil
|
|
int ne = 0;
|
|
int nt = simplex.size() / kNumVerts;
|
|
std::vector<Stencil2D> elements(nt);
|
|
for (int t = 0; t < nt; t++) {
|
|
for (int v = 0; v < kNumVerts; v++) {
|
|
elements[t].vertices[v] = simplex[kNumVerts * t + v];
|
|
}
|
|
}
|
|
|
|
// map from edge vertices to their index in `edges` vector
|
|
std::unordered_map<std::pair<int, int>, int, PairHash> edge_indices;
|
|
|
|
// loop over all triangles
|
|
for (int t = 0; t < nt; t++) {
|
|
int* v = elements[t].vertices;
|
|
|
|
// compute edges to vertices map for fast computations
|
|
for (int e = 0; e < kNumEdges; e++) {
|
|
auto pair = std::pair(std::min(v[edge[e][0]], v[edge[e][1]]),
|
|
std::max(v[edge[e][0]], v[edge[e][1]]));
|
|
|
|
// if edge is already present in the vector only store its index
|
|
auto [it, inserted] = edge_indices.insert({pair, ne});
|
|
|
|
if (inserted) {
|
|
StencilFlap flap;
|
|
flap.vertices[0] = v[edge[e][0]];
|
|
flap.vertices[1] = v[edge[e][1]];
|
|
flap.vertices[2] = v[(edge[e][1] + 1) % 3];
|
|
flap.vertices[3] = -1;
|
|
flaps.push_back(flap);
|
|
elements[t].edges[e] = ne++;
|
|
} else {
|
|
elements[t].edges[e] = it->second;
|
|
flaps[it->second].vertices[3] = v[(edge[e][1] + 1) % 3];
|
|
}
|
|
|
|
// double check that the edge indices are consistent
|
|
if (!edgeidx.empty()) {
|
|
if (elements[t].edges[e] != edgeidx[kNumEdges * t + e]) {
|
|
mju_error("edge indices do not match in CreateFlapStencil");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// cotangent between two edges
|
|
double inline cot(const double* x, int v0, int v1, int v2) {
|
|
double normal[3];
|
|
double edge1[3] = {x[3*v1]-x[3*v0], x[3*v1+1]-x[3*v0+1], x[3*v1+2]-x[3*v0+2]};
|
|
double edge2[3] = {x[3*v2]-x[3*v0], x[3*v2+1]-x[3*v0+1], x[3*v2+2]-x[3*v0+2]};
|
|
|
|
mjuu_crossvec(normal, edge1, edge2);
|
|
return mjuu_dot3(edge1, edge2) / sqrt(mjuu_dot3(normal, normal));
|
|
}
|
|
|
|
// area of a triangle
|
|
double inline ComputeVolume(const double* x, const int v[Stencil2D::kNumVerts]) {
|
|
double normal[3];
|
|
double edge1[3] = {x[3*v[1]]-x[3*v[0]], x[3*v[1]+1]-x[3*v[0]+1], x[3*v[1]+2]-x[3*v[0]+2]};
|
|
double edge2[3] = {x[3*v[2]]-x[3*v[0]], x[3*v[2]+1]-x[3*v[0]+1], x[3*v[2]+2]-x[3*v[0]+2]};
|
|
|
|
mjuu_crossvec(normal, edge1, edge2);
|
|
return sqrt(mjuu_dot3(normal, normal)) / 2;
|
|
}
|
|
|
|
// compute bending stiffness for a single edge
|
|
template <typename T>
|
|
void inline ComputeBending(double* bending, double* pos, const int v[4], double mu,
|
|
double thickness) {
|
|
int vadj[3] = {v[1], v[0], v[3]};
|
|
|
|
if (v[3]== -1) {
|
|
// skip boundary edges
|
|
return;
|
|
}
|
|
|
|
// cotangent operator from Wardetzky at al., "Discrete Quadratic Curvature
|
|
// Energies", https://cims.nyu.edu/gcl/papers/wardetzky2007dqb.pdf
|
|
|
|
double a01 = cot(pos, v[0], v[1], v[2]);
|
|
double a02 = cot(pos, v[0], v[3], v[1]);
|
|
double a03 = cot(pos, v[1], v[2], v[0]);
|
|
double a04 = cot(pos, v[1], v[0], v[3]);
|
|
double c[4] = {a03 + a04, a01 + a02, -(a01 + a03), -(a02 + a04)};
|
|
double volume = ComputeVolume(pos, v) + ComputeVolume(pos, vadj);
|
|
double stiffness = 3 * mu * pow(thickness, 3) / (24 * volume);
|
|
|
|
// Garg et al., "Cubic Shells", https://cims.nyu.edu/gcl/papers/garg2007cs.pdf
|
|
const double* v0 = pos + 3*v[0];
|
|
const double* v1 = pos + 3*v[1];
|
|
const double* v2 = pos + 3*v[2];
|
|
const double* v3 = pos + 3*v[3];
|
|
double e0[3] = {v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]};
|
|
double e1[3] = {v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]};
|
|
double e2[3] = {v3[0] - v0[0], v3[1] - v0[1], v3[2] - v0[2]};
|
|
double e3[3] = {v2[0] - v1[0], v2[1] - v1[1], v2[2] - v1[2]};
|
|
double e4[3] = {v3[0] - v1[0], v3[1] - v1[1], v3[2] - v1[2]};
|
|
double t0[3] = {-(a03*e1[0] + a01*e3[0]), -(a03*e1[1] + a01*e3[1]), -(a03*e1[2] + a01*e3[2])};
|
|
double t1[3] = {-(a04*e2[0] + a02*e4[0]), -(a04*e2[1] + a02*e4[1]), -(a04*e2[2] + a02*e4[2])};
|
|
double sqr = mjuu_dot3(e0, e0);
|
|
double cos_theta = -mjuu_dot3(t0, t1) / sqr;
|
|
|
|
for (int v1 = 0; v1 < T::kNumVerts; v1++) {
|
|
for (int v2 = 0; v2 < T::kNumVerts; v2++) {
|
|
bending[4 * v1 + v2] += c[v1] * c[v2] * cos_theta * stiffness;
|
|
}
|
|
}
|
|
|
|
double n[3];
|
|
mjuu_crossvec(n, e0, e1);
|
|
bending[16] = mjuu_dot3(n, e2) * (a01 - a03) * (a04 - a02) * stiffness / (sqr * sqrt(sqr));
|
|
}
|
|
|
|
//----------------------------- linear elasticity --------------------------------------------------
|
|
|
|
// Gauss Legendre quadrature points in 1 dimension on the interval [a, b]
|
|
void quadratureGaussLegendre(double* points, double* weights,
|
|
const int order, const double a, const double b) {
|
|
if (order > 3)
|
|
mju_error("Integration order > 3 not yet supported.");
|
|
|
|
// x is on [-1, 1], p on [a, b]
|
|
double p0 = (a+b)/2.;
|
|
double dpdx = (b-a)/2;
|
|
|
|
if (order == 2) {
|
|
points[0] = -dpdx / sqrt(3) + p0;
|
|
points[1] = dpdx / sqrt(3) + p0;
|
|
weights[0] = dpdx;
|
|
weights[1] = dpdx;
|
|
} else {
|
|
points[0] = p0;
|
|
points[1] = -dpdx / sqrt(3. / 5.) + p0;
|
|
points[2] = dpdx / sqrt(3. / 5.) + p0;
|
|
weights[0] = 8. / 9. * dpdx;
|
|
weights[1] = 5. / 9. * dpdx;
|
|
weights[2] = 5. / 9. * dpdx;
|
|
}
|
|
}
|
|
|
|
// evaluate 1-dimensional basis function
|
|
double phi(const double s, const int i, const int order) {
|
|
if (order == 1) {
|
|
return i == 0 ? 1 - s : s;
|
|
} else if (order == 2) {
|
|
switch (i) {
|
|
case 0:
|
|
return 2 * s * s - 3 * s + 1;
|
|
case 1:
|
|
return 4 * (s - s * s);
|
|
case 2:
|
|
return 2 * s * s - s;
|
|
default:
|
|
mjERROR("invalid index %d", i);
|
|
return 0;
|
|
}
|
|
} else {
|
|
mju_error("Order must be 1 or 2.");
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// evaluate gradient of 1-dimensional basis function
|
|
double dphi(const double s, const int i, const int order) {
|
|
if (order == 1) {
|
|
return i == 0 ? -1 : 1;
|
|
} else if (order == 2) {
|
|
switch (i) {
|
|
case 0:
|
|
return 4 * s - 3;
|
|
case 1:
|
|
return 4 * (1 - 2 * s);
|
|
case 2:
|
|
return 4 * s - 1;
|
|
default:
|
|
mjERROR("invalid index %d, must be 0, 1, or 2", i);
|
|
return 0;
|
|
}
|
|
} else {
|
|
mju_error("Order must be 1 or 2.");
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
typedef std::array<std::array<double, 3>, 3> Matrix;
|
|
|
|
// symmetrize a tensor
|
|
Matrix inline sym(const Matrix& tensor) {
|
|
Matrix eps;
|
|
for (int i = 0; i < 3; i++) {
|
|
for (int j = 0; j < 3; j++) {
|
|
eps[i][j] = (tensor[i][j] + tensor[j][i]) / 2;
|
|
}
|
|
}
|
|
return eps;
|
|
}
|
|
|
|
// compute tensor inner product
|
|
Matrix inline inner(const Matrix& tensor1, const Matrix& tensor2) {
|
|
Matrix inner;
|
|
for (int i = 0; i < 3; i++) {
|
|
for (int j = 0; j < 3; j++) {
|
|
inner[i][j] = tensor1[i][0] * tensor2[0][j] +
|
|
tensor1[i][1] * tensor2[1][j] +
|
|
tensor1[i][2] * tensor2[2][j];
|
|
}
|
|
}
|
|
return inner;
|
|
}
|
|
|
|
// compute trace of a tensor
|
|
double inline trace(const Matrix& tensor) {
|
|
return tensor[0][0] + tensor[1][1] + tensor[2][2];
|
|
}
|
|
|
|
void inline ComputeLinearStiffness(std::vector<double>& K,
|
|
const double* pos,
|
|
double E, double nu, int order) {
|
|
int nbasis = order + 1;
|
|
int n = pow(nbasis, 3);
|
|
int ndof = 3*n;
|
|
|
|
// compute quadrature points
|
|
std::vector<double> points(nbasis); // quadrature points
|
|
std::vector<double> weight(nbasis); // quadrature weights
|
|
quadratureGaussLegendre(points.data(), weight.data(), nbasis, 0, 1);
|
|
|
|
// compute element transformation
|
|
double dx = (pos+3*(n-1))[0] - pos[0];
|
|
double dy = (pos+3*(n-1))[1] - pos[1];
|
|
double dz = (pos+3*(n-1))[2] - pos[2];
|
|
double detJ = dx * dy * dz;
|
|
double invJ[3] = {1.0 / dx, 1.0 / dy, 1.0 / dz};
|
|
|
|
// compute stiffness matrix
|
|
std::vector<std::array<double, 3> > F(n);
|
|
double la = E * nu / (1 + nu) / (1 - 2 * nu);
|
|
double mu = E / (2 * (1 + nu));
|
|
|
|
// loop over quadrature points
|
|
for (int ps=0; ps < nbasis; ps++) {
|
|
for (int pt=0; pt < nbasis; pt++) {
|
|
for (int pu=0; pu < nbasis; pu++) {
|
|
double s = points[ps];
|
|
double t = points[pt];
|
|
double u = points[pu];
|
|
double dvol = weight[ps] * weight[pt] * weight[pu] * detJ;
|
|
int dof = 0;
|
|
|
|
// cartesian product of basis functions
|
|
for (int bx=0; bx < nbasis; bx++) {
|
|
for (int by=0; by < nbasis; by++) {
|
|
for (int bz=0; bz < nbasis; bz++) {
|
|
std::array<double, 3> gradient;
|
|
gradient[0] = dphi(s, bx, order) * phi(t, by, order) * phi(u, bz, order);
|
|
gradient[1] = phi(s, bx, order) * dphi(t, by, order) * phi(u, bz, order);
|
|
gradient[2] = phi(s, bx, order) * phi(t, by, order) * dphi(u, bz, order);
|
|
F[dof++] = gradient;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (dof != n) { // SHOULD NOT OCCUR
|
|
throw mjCError(nullptr, "incorrect number of basis functions");
|
|
}
|
|
|
|
// tensor contraction of the gradients of elastic strains
|
|
// (d(F+F')/dx : d(F+F')/dx)
|
|
for (int i=0; i < n; i++) {
|
|
for (int j=0; j < n; j++) {
|
|
Matrix du;
|
|
Matrix dv;
|
|
du.fill({0, 0, 0});
|
|
dv.fill({0, 0, 0});
|
|
for (int k=0; k < 3; k++) {
|
|
for (int l=0; l < 3; l++) {
|
|
du[k][0] = invJ[0] * F[i][0];
|
|
du[k][1] = invJ[1] * F[i][1];
|
|
du[k][2] = invJ[2] * F[i][2];
|
|
dv[l][0] = invJ[0] * F[j][0];
|
|
dv[l][1] = invJ[1] * F[j][1];
|
|
dv[l][2] = invJ[2] * F[j][2];
|
|
K[ndof*(3*i+k) + 3*j+l] -= la * trace(du) * trace(dv) * dvol;
|
|
K[ndof*(3*i+k) + 3*j+l] -= mu * trace(inner(sym(du), sym(dv))) * dvol;
|
|
mjuu_zerovec(du[k].data(), 3);
|
|
mjuu_zerovec(dv[l].data(), 3);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//------------------ class mjCFlex implementation --------------------------------------------------
|
|
|
|
// constructor
|
|
mjCFlex::mjCFlex(mjCModel* _model) {
|
|
mjs_defaultFlex(&spec);
|
|
elemtype = mjOBJ_FLEX;
|
|
|
|
// set model
|
|
model = _model;
|
|
if (_model) compiler = &_model->spec.compiler;
|
|
|
|
// clear internal variables
|
|
nvert = 0;
|
|
nnode = 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<mjsElement*>(this);
|
|
spec.material = &spec_material_;
|
|
spec.vertbody = &spec_vertbody_;
|
|
spec.nodebody = &spec_nodebody_;
|
|
spec.vert = &spec_vert_;
|
|
spec.node = &spec_node_;
|
|
spec.texcoord = &spec_texcoord_;
|
|
spec.elemtexcoord = &spec_elemtexcoord_;
|
|
spec.elem = &spec_elem_;
|
|
spec.info = &info;
|
|
material = nullptr;
|
|
vertbody = nullptr;
|
|
nodebody = nullptr;
|
|
vert = nullptr;
|
|
node = nullptr;
|
|
texcoord = nullptr;
|
|
elemtexcoord = nullptr;
|
|
elem = nullptr;
|
|
}
|
|
|
|
|
|
|
|
void mjCFlex::NameSpace(const mjCModel* m) {
|
|
for (auto& name : spec_vertbody_) {
|
|
name = m->prefix + name + m->suffix;
|
|
}
|
|
for (auto& name : spec_nodebody_) {
|
|
name = m->prefix + name + m->suffix;
|
|
}
|
|
if (!spec_material_.empty() && model != m) {
|
|
spec_material_ = m->prefix + spec_material_ + m->suffix;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void mjCFlex::CopyFromSpec() {
|
|
*static_cast<mjsFlex*>(this) = spec;
|
|
spec.info = &info;
|
|
material_ = spec_material_;
|
|
vertbody_ = spec_vertbody_;
|
|
nodebody_ = spec_nodebody_;
|
|
vert_ = spec_vert_;
|
|
node_ = spec_node_;
|
|
texcoord_ = spec_texcoord_;
|
|
elemtexcoord_ = spec_elemtexcoord_;
|
|
elem_ = spec_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) {
|
|
vertbodyid.clear();
|
|
nodebodyid.clear();
|
|
for (const auto& vertbody : vertbody_) {
|
|
mjCBody* pbody = static_cast<mjCBody*>(m->FindObject(mjOBJ_BODY, vertbody));
|
|
if (pbody) {
|
|
vertbodyid.push_back(pbody->id);
|
|
if (pbody->joints.size() != 3 && dim == 2 && (elastic2d == 1 || elastic2d == 3)) {
|
|
// TODO(quaglino): add support for pins
|
|
throw mjCError(this, "pins are not supported for bending");
|
|
}
|
|
} else {
|
|
throw mjCError(this, "unknown body '%s' in flex", vertbody.c_str());
|
|
}
|
|
}
|
|
for (const auto& nodebody : nodebody_) {
|
|
mjCBase* pbody = m->FindObject(mjOBJ_BODY, nodebody);
|
|
if (pbody) {
|
|
nodebodyid.push_back(pbody->id);
|
|
} else {
|
|
throw mjCError(this, "unknown body '%s' in flex", nodebody.c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// compiler
|
|
void mjCFlex::Compile(const mjVFS* vfs) {
|
|
CopyFromSpec();
|
|
interpolated = !nodebody_.empty();
|
|
|
|
// 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() && !interpolated) {
|
|
throw mjCError(this, "vertbody and nodebody are both 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");
|
|
}
|
|
if (interpolated && selfcollide != mjFLEXSELF_NONE) {
|
|
throw mjCError(this, "trilinear interpolation cannot do self-collision");
|
|
}
|
|
if (interpolated && internal) {
|
|
throw mjCError(this, "trilinear interpolation cannot do internal collisions");
|
|
}
|
|
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;
|
|
} else if (vertbody_.size() != nvert) {
|
|
throw mjCError(this, "vertbody size must be 1 or nvert");
|
|
}
|
|
}
|
|
if (nvert < dim+1) {
|
|
throw mjCError(this, "not enough vertices");
|
|
}
|
|
|
|
// set nnode
|
|
nnode = static_cast<int>(nodebody_.size());
|
|
if (nnode && !order_) {
|
|
order_ = std::pow(nnode, 1.0 / 3) - 1;
|
|
if (nnode != std::pow(order_ + 1, 3)) {
|
|
throw mjCError(this, "number of nodes must be %d^3 but it is %d", nullptr, order_, nnode);
|
|
}
|
|
}
|
|
|
|
// check elem vertex ids
|
|
for (const auto& elem : elem_) {
|
|
if (elem < 0 || elem >= nvert) {
|
|
throw mjCError(this, "elem vertex id out of range");
|
|
}
|
|
}
|
|
|
|
// check texcoord
|
|
if (!texcoord_.empty() && texcoord_.size() != 2*nvert && elemtexcoord_.empty()) {
|
|
throw mjCError(this, "two texture coordinates per vertex expected");
|
|
}
|
|
|
|
// no elemtexcoord: copy from faces
|
|
if (elemtexcoord_.empty() && !texcoord_.empty()) {
|
|
elemtexcoord_.assign(3*nelem, 0);
|
|
memcpy(elemtexcoord_.data(), elem_.data(), 3*nelem*sizeof(int));
|
|
}
|
|
|
|
// resolve material name
|
|
mjCBase* pmat = model->FindObject(mjOBJ_MATERIAL, material_);
|
|
if (pmat) {
|
|
matid = pmat->id;
|
|
} else if (!material_.empty()) {
|
|
throw mjCError(this, "unknown 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
|
|
std::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 && !interpolated) {
|
|
rigid = true;
|
|
for (unsigned i=1; i < vertbodyid.size(); i++) {
|
|
if (vertbodyid[i] != vertbodyid[0]) {
|
|
rigid = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// determine centered if not already set
|
|
if (!centered && !interpolated) {
|
|
centered = true;
|
|
for (const auto& vert : vert_) {
|
|
if (vert != 0) {
|
|
centered = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!centered && interpolated) {
|
|
centered = true;
|
|
for (const auto& node : node_) {
|
|
if (node != 0) {
|
|
centered = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// compute global vertex positions
|
|
vertxpos = std::vector<double> (3*nvert);
|
|
for (int i=0; i < nvert; i++) {
|
|
// get body id, set vertxpos = body.xpos0
|
|
int b = rigid ? vertbodyid[0] : vertbodyid[i];
|
|
mjuu_copyvec(vertxpos.data()+3*i, model->Bodies()[b]->xpos0, 3);
|
|
|
|
// add vertex offset within body if not centered
|
|
if (!centered || interpolated) {
|
|
double offset[3];
|
|
mjuu_rotVecQuat(offset, vert_.data()+3*i, model->Bodies()[b]->xquat0);
|
|
mjuu_addtovec(vertxpos.data()+3*i, offset, 3);
|
|
}
|
|
|
|
if (interpolated) {
|
|
// this should happen in ResolveReferences but we need a body id in this loop to compute
|
|
// the global vertex position, this is a hack since it is the id of the parent body
|
|
vertbodyid[i] = -1;
|
|
}
|
|
}
|
|
|
|
// compute global node positions
|
|
std::vector<double> nodexpos = std::vector<double> (3*nnode);
|
|
for (int i=0; i < nnode; i++) {
|
|
// get body id, set nodexpos = body.xpos0
|
|
int b = nodebodyid[i];
|
|
mjuu_copyvec(nodexpos.data()+3*i, model->Bodies()[b]->xpos0, 3);
|
|
|
|
// add node offset within body if not centered
|
|
if (!centered) {
|
|
double offset[3];
|
|
mjuu_rotVecQuat(offset, node_.data()+3*i, model->Bodies()[b]->xquat0);
|
|
mjuu_addtovec(nodexpos.data()+3*i, offset, 3);
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
double* v0 = vertxpos.data() + 3*edata[0];
|
|
double* v1 = vertxpos.data() + 3*edata[1];
|
|
double* v2 = vertxpos.data() + 3*edata[2];
|
|
double* v3 = vertxpos.data() + 3*edata[3];
|
|
double v01[3] = {v1[0]-v0[0], v1[1]-v0[1], v1[2]-v0[2]};
|
|
double v02[3] = {v2[0]-v0[0], v2[1]-v0[1], v2[2]-v0[2]};
|
|
double v03[3] = {v3[0]-v0[0], v3[1]-v0[1], v3[2]-v0[2]};
|
|
|
|
// detect wrong orientation
|
|
double nrm[3];
|
|
mjuu_crossvec(nrm, v01, v02);
|
|
if (mjuu_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
|
|
edgeidx_.assign(elem_.size()*kNumEdges[dim-1]/(dim+1), 0);
|
|
|
|
// 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 (unsigned f = 0; f < 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(
|
|
min(v[eledge[dim-1][e][0]], v[eledge[dim-1][e][1]]),
|
|
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();
|
|
|
|
// create flap stencil
|
|
if (dim == 2) {
|
|
CreateFlapStencil(flaps, elem_, edgeidx_);
|
|
}
|
|
|
|
// compute elasticity
|
|
if (young > 0) {
|
|
if (poisson < 0 || poisson >= 0.5) {
|
|
throw mjCError(this, "Poisson ratio must be in [0, 0.5)");
|
|
}
|
|
|
|
// linear elasticity
|
|
stiffness.assign(21*nelem, 0);
|
|
if (interpolated) {
|
|
int min_size = ceil(nodexpos.size()*nodexpos.size() / 21);
|
|
if (min_size > nelem) {
|
|
throw mjCError(this, "Trilinear dofs are require at least %d elements", "", min_size);
|
|
}
|
|
ComputeLinearStiffness(stiffness, nodexpos.data(), young, poisson, order_);
|
|
}
|
|
|
|
// geometrically nonlinear elasticity
|
|
for (unsigned int t = 0; t < nelem; t++) {
|
|
if (interpolated) {
|
|
continue;
|
|
}
|
|
if (dim == 2 && elastic2d >= 2 && thickness > 0) {
|
|
ComputeStiffness<Stencil2D>(stiffness, vertxpos,
|
|
elem_.data() + (dim + 1) * t, t, young,
|
|
poisson, thickness);
|
|
} else if (dim == 3) {
|
|
ComputeStiffness<Stencil3D>(stiffness, vertxpos,
|
|
elem_.data() + (dim + 1) * t, t, young,
|
|
poisson);
|
|
}
|
|
}
|
|
|
|
// bending stiffness (2D only)
|
|
if (dim == 2 && (elastic2d == 1 || elastic2d == 3)) {
|
|
if (thickness < 0) {
|
|
throw mjCError(this, "thickness must be positive for bending stiffness");
|
|
}
|
|
bending.assign(nedge*17, 0);
|
|
|
|
for (unsigned int e = 0; e < nedge; e++) {
|
|
ComputeBending<StencilFlap>(bending.data() + 17 * e, vertxpos.data(), flaps[e].vertices,
|
|
young / (2 * (1 + poisson)), thickness);
|
|
}
|
|
}
|
|
}
|
|
|
|
// placeholder for setting plugins parameters, currently not used
|
|
for (const auto& vbodyid : vertbodyid) {
|
|
if (vbodyid < 0) {
|
|
continue;
|
|
}
|
|
if (model->Bodies()[vbodyid]->plugin.element) {
|
|
mjCPlugin* plugin_instance =
|
|
static_cast<mjCPlugin*>(model->Bodies()[vbodyid]->plugin.element);
|
|
if (!plugin_instance) {
|
|
throw mjCError(this, "plugin instance not found");
|
|
}
|
|
}
|
|
}
|
|
|
|
// create shell fragments and element-vertex collision pairs
|
|
CreateShellPair();
|
|
|
|
// create bounding volume hierarchy
|
|
CreateBVH();
|
|
|
|
// compute bounding box coordinates
|
|
vert0_.assign(3*nvert, 0);
|
|
const mjtNum* bvh = tree.Bvh().data();
|
|
size[0] = bvh[3] - radius;
|
|
size[1] = bvh[4] - radius;
|
|
size[2] = bvh[5] - radius;
|
|
for (int j=0; j < nvert; j++) {
|
|
for (int k=0; k < 3; k++) {
|
|
if (size[k] > mjMINVAL) {
|
|
vert0_[3*j+k] = (vertxpos[3*j+k] - bvh[k]) / (2*size[k]) + 0.5;
|
|
} else {
|
|
vert0_[3*j+k] = 0.5;
|
|
}
|
|
}
|
|
}
|
|
|
|
// store node cartesian positions
|
|
node0_.assign(3*nnode, 0);
|
|
for (int i=0; i < nnode; i++) {
|
|
mjuu_copyvec(node0_.data()+3*i, nodexpos.data()+3*i, 3);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// create flex BVH
|
|
void mjCFlex::CreateBVH() {
|
|
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
|
|
double xmin[3], xmax[3];
|
|
mjuu_copyvec(xmin, vertxpos.data() + 3*edata[0], 3);
|
|
mjuu_copyvec(xmax, vertxpos.data() + 3*edata[0], 3);
|
|
for (int i=1; i <= dim; i++) {
|
|
for (int j=0; j < 3; j++) {
|
|
xmin[j] = std::min(xmin[j], vertxpos[3*edata[i]+j]);
|
|
xmax[j] = std::max(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
|
|
// contype and conaffinity are set to nonzero to force bvh generation
|
|
const double* aabb = elemaabb_.data() + 6*e;
|
|
tree.AddBoundingVolume(e, 1, 1, aabb, nullptr, aabb);
|
|
nbvh++;
|
|
}
|
|
|
|
// create hierarchy
|
|
tree.RemoveInactiveVolumes(nbvh);
|
|
tree.CreateBVH();
|
|
}
|
|
|
|
|
|
|
|
// create shells and element-vertex collision pairs
|
|
void mjCFlex::CreateShellPair(void) {
|
|
std::vector<std::vector<int> > fragspec(nelem*(dim+1)); // [sorted frag vertices, elem, original frag vertices]
|
|
std::vector<std::vector<int> > connectspec; // [elem1, elem2, common sorted frag vertices]
|
|
std::vector<bool> border(nelem, false); // is element on the border
|
|
std::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
|
|
std::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
|
|
std::vector<int> previous = {fragspec[n-1].begin(), fragspec[n-1].begin()+dim};
|
|
std::vector<int> current = {fragspec[n].begin(), fragspec[n].begin()+dim};
|
|
|
|
// same sequential fragments
|
|
if (previous == current) {
|
|
// found pair of elements connected by common fragment
|
|
std::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 (unsigned i=0; i < 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 = std::vector<int> (nelem, 0);
|
|
}
|
|
else {
|
|
elemlayer = std::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 (const auto& connect : connectspec) {
|
|
int e1 = connect[0]; // get element pair for this edge
|
|
int e2 = connect[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 (const auto& connect : connectspec) {
|
|
if (border[connect[0]] || border[connect[1]]) {
|
|
// extract common fragment
|
|
std::vector<int> frag = {connect.begin()+2, connect.end()};
|
|
|
|
// process both elements
|
|
for (int ei=0; ei < 2; ei++) {
|
|
const int* edata = elem_.data() + connect[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(connect[1-ei]);
|
|
evpair.push_back(edata[i]);
|
|
|
|
// one such vertex exists
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|