Add support for loading 1D flex components from OBJ line segments.

The obj_decoder plugin is updated to parse line segments ('l') from OBJ files and represent them as degenerate triangles. The user_flexcomp compiler now supports flex dimensions of 1, using these parsed line segments to define the 1D flex elements. The compile sample's error reporting is also improved.

PiperOrigin-RevId: 928566870
Change-Id: I6920729d1b1d5aeb391401c3245ebb1d4784c190
This commit is contained in:
Alessio Quaglino
2026-06-08 08:03:04 -07:00
committed by Copybara-Service
parent ac7f198536
commit 7a7dc7ccad
6 changed files with 107 additions and 33 deletions
+1
View File
@@ -16,6 +16,7 @@ General
- Added :ref:`mjs_makeFlex`, a new C API function equivalent to the :ref:`flexcomp<body-flexcomp>` element for
programmatically creating flex objects with auto-generated bodies, joints, and equality constraints. Exposed as
``body.make_flex()`` in Python.
- Added support for loading 1D flex components from OBJ line segments
- Significantly improved the quality of coarse convex hulls produced by the :ref:`maxhullvert<asset-mesh-maxhullvert>`
attribute by invoking Qhull's `Q9 <http://www.qhull.org/html/qh-optq.htm#Q9>`__ option.
+72 -31
View File
@@ -53,41 +53,80 @@ mjSpec* Decode(mjResource* resource, const mjVFS* vfs) {
std::vector<int> userfacetexcoord;
if (!obj_reader.GetShapes().empty()) {
const auto& obj_mesh = obj_reader.GetShapes()[0].mesh;
const auto& obj_shape = obj_reader.GetShapes()[0];
if (!obj_shape.mesh.indices.empty()) {
const auto& obj_mesh = obj_shape.mesh;
std::vector<tinyobj::index_t> face_indices;
for (size_t face = 0, idx = 0; idx < obj_mesh.indices.size();) {
int nfacevert = obj_mesh.num_face_vertices[face];
if (nfacevert < 3 || nfacevert > 4) {
mju_warning(
"obj_decoder: only tri or quad meshes are supported (file '%s')",
resource->name);
mj_deleteSpec(spec);
return nullptr;
size_t num_face_indices = 0;
for (size_t face = 0; face < obj_mesh.num_face_vertices.size(); ++face) {
int nfacevert = obj_mesh.num_face_vertices[face];
if (nfacevert == 3) {
num_face_indices += 3;
} else if (nfacevert == 4) {
num_face_indices += 6;
}
}
face_indices.push_back(obj_mesh.indices[idx]);
face_indices.push_back(obj_mesh.indices[idx + 1]);
face_indices.push_back(obj_mesh.indices[idx + 2]);
if (nfacevert == 4) {
face_indices.push_back(obj_mesh.indices[idx]);
face_indices.push_back(obj_mesh.indices[idx + 2]);
face_indices.push_back(obj_mesh.indices[idx + 3]);
}
idx += nfacevert;
++face;
}
for (const auto& mesh_index : face_indices) {
userface.push_back(mesh_index.vertex_index);
std::vector<tinyobj::index_t> face_indices;
face_indices.reserve(num_face_indices);
userface.reserve(num_face_indices);
if (!usernormal.empty()) {
userfacenormal.push_back(mesh_index.normal_index);
userfacenormal.reserve(num_face_indices);
}
if (!usertexcoord.empty()) {
userfacetexcoord.reserve(num_face_indices);
}
if (!usertexcoord.empty()) {
userfacetexcoord.push_back(mesh_index.texcoord_index);
for (size_t face = 0, idx = 0; idx < obj_mesh.indices.size();) {
int nfacevert = obj_mesh.num_face_vertices[face];
if (nfacevert < 3 || nfacevert > 4) {
mju_warning(
"obj_decoder: only tri or quad meshes are supported (file '%s')",
resource->name);
mj_deleteSpec(spec);
return nullptr;
}
face_indices.push_back(obj_mesh.indices[idx]);
face_indices.push_back(obj_mesh.indices[idx + 1]);
face_indices.push_back(obj_mesh.indices[idx + 2]);
if (nfacevert == 4) {
face_indices.push_back(obj_mesh.indices[idx]);
face_indices.push_back(obj_mesh.indices[idx + 2]);
face_indices.push_back(obj_mesh.indices[idx + 3]);
}
idx += nfacevert;
++face;
}
for (const auto& mesh_index : face_indices) {
userface.push_back(mesh_index.vertex_index);
if (!usernormal.empty()) {
userfacenormal.push_back(mesh_index.normal_index);
}
if (!usertexcoord.empty()) {
userfacetexcoord.push_back(mesh_index.texcoord_index);
}
}
} else if (!obj_shape.lines.indices.empty()) {
// encode line segments as degenerate triangles (i1, i2, i2) in userface,
// since mjsMesh has no native edge field; decoded back by flexcomp
const auto& obj_lines = obj_shape.lines;
userface.reserve(3 * (obj_lines.indices.size() - obj_lines.num_line_vertices.size()));
size_t idx = 0;
for (size_t l = 0; l < obj_lines.num_line_vertices.size(); l++) {
int nlinevert = obj_lines.num_line_vertices[l];
for (int v = 0; v < nlinevert - 1; v++) {
int i1 = obj_lines.indices[idx + v].vertex_index;
int i2 = obj_lines.indices[idx + v + 1].vertex_index;
userface.push_back(i1);
userface.push_back(i2);
userface.push_back(i2);
}
idx += nlinevert;
}
}
}
@@ -101,8 +140,10 @@ mjSpec* Decode(mjResource* resource, const mjVFS* vfs) {
mjs_setFloat(mesh->usernormal, usernormal.data(), usernormal.size());
mjs_setFloat(mesh->usertexcoord, usertexcoord.data(), usertexcoord.size());
mjs_setInt(mesh->userface, userface.data(), userface.size());
mjs_setInt(mesh->userfacenormal, userfacenormal.data(), userfacenormal.size());
mjs_setInt(mesh->userfacetexcoord, userfacetexcoord.data(), userfacetexcoord.size());
mjs_setInt(mesh->userfacenormal, userfacenormal.data(),
userfacenormal.size());
mjs_setInt(mesh->userfacetexcoord, userfacetexcoord.data(),
userfacetexcoord.size());
return spec;
}
+11 -2
View File
@@ -1334,8 +1334,8 @@ bool mjCFlexcomp::MakeMesh(mjCModel* model, mjsCompiler* compiler, char* error,
}
// check dim
if (def.spec.flex->dim < 2) {
return comperr(error, "Flex dim must be at least 2 for mesh", error_sz);
if (def.spec.flex->dim < 1) {
return comperr(error, "Flex dim must be at least 1 for mesh", error_sz);
}
// load resource
@@ -1381,6 +1381,15 @@ bool mjCFlexcomp::MakeMesh(mjCModel* model, mjsCompiler* compiler, char* error,
// copy faces or create 3D mesh
if (def.spec.flex->dim == 2) {
element = mesh.Face();
} else if (def.spec.flex->dim == 1) {
// extract edge pairs from degenerate triangles (i1, i2, i2)
const std::vector<int>& face = mesh.Face();
element.clear();
element.reserve(face.size() * 2 / 3);
for (size_t i = 0; i < face.size(); i += 3) {
element.push_back(face[i]);
element.push_back(face[i+1]);
}
} else {
point.insert(point.begin() + 0, origin[0]);
point.insert(point.begin() + 1, origin[1]);
+5
View File
@@ -0,0 +1,5 @@
<mujoco>
<worldbody>
<flexcomp name="line" type="mesh" file="line.obj" dim="1" radius="0.1"/>
</worldbody>
</mujoco>
+6
View File
@@ -0,0 +1,6 @@
v 0 0 0
v 2 0 0
v 1 0 0
v 3 0 0
l 1 3 2
l 2 4
+12
View File
@@ -1399,6 +1399,18 @@ TEST_F(UserFlexTest, Vert0RotationInvariant) {
mj_deleteModel(m2);
}
TEST_F(UserFlexTest, Load1DFlexFromOBJ) {
const std::string xml_path =
GetTestDataFilePath("user/testdata/flex_line_obj.xml");
std::array<char, 1024> error;
mjModel* m = mj_loadXML(xml_path.c_str(), 0, error.data(), error.size());
ASSERT_THAT(m, NotNull()) << error.data();
EXPECT_EQ(m->nflexvert, 4);
EXPECT_EQ(m->nflexelem, 3);
EXPECT_EQ(m->flex_dim[0], 1);
mj_deleteModel(m);
}
} // namespace
} // namespace mujoco