From 7a7dc7ccad4d96c99d47f5400d986106671b150a Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Mon, 8 Jun 2026 08:03:04 -0700 Subject: [PATCH] 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 --- doc/changelog.rst | 1 + plugin/obj_decoder/obj_decoder.cc | 103 +++++++++++++++++++-------- src/user/user_flexcomp.cc | 13 +++- test/user/testdata/flex_line_obj.xml | 5 ++ test/user/testdata/line.obj | 6 ++ test/user/user_flex_test.cc | 12 ++++ 6 files changed, 107 insertions(+), 33 deletions(-) create mode 100644 test/user/testdata/flex_line_obj.xml create mode 100644 test/user/testdata/line.obj diff --git a/doc/changelog.rst b/doc/changelog.rst index e7bd560a..abf1bd26 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -16,6 +16,7 @@ General - Added :ref:`mjs_makeFlex`, a new C API function equivalent to the :ref:`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` attribute by invoking Qhull's `Q9 `__ option. diff --git a/plugin/obj_decoder/obj_decoder.cc b/plugin/obj_decoder/obj_decoder.cc index 929e959a..8390d363 100644 --- a/plugin/obj_decoder/obj_decoder.cc +++ b/plugin/obj_decoder/obj_decoder.cc @@ -53,41 +53,80 @@ mjSpec* Decode(mjResource* resource, const mjVFS* vfs) { std::vector 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 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 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; } diff --git a/src/user/user_flexcomp.cc b/src/user/user_flexcomp.cc index 4a62d6d3..e5e8d494 100644 --- a/src/user/user_flexcomp.cc +++ b/src/user/user_flexcomp.cc @@ -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& 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]); diff --git a/test/user/testdata/flex_line_obj.xml b/test/user/testdata/flex_line_obj.xml new file mode 100644 index 00000000..d25f5f72 --- /dev/null +++ b/test/user/testdata/flex_line_obj.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/test/user/testdata/line.obj b/test/user/testdata/line.obj new file mode 100644 index 00000000..a9396851 --- /dev/null +++ b/test/user/testdata/line.obj @@ -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 diff --git a/test/user/user_flex_test.cc b/test/user/user_flex_test.cc index 3342bbd8..f5a31d0e 100644 --- a/test/user/user_flex_test.cc +++ b/test/user/user_flex_test.cc @@ -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 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