diff --git a/src/user/user_mesh.cc b/src/user/user_mesh.cc index 15b0e220..287748c6 100644 --- a/src/user/user_mesh.cc +++ b/src/user/user_mesh.cc @@ -62,6 +62,7 @@ mjCMesh::mjCMesh(mjCModel* _model, mjCDef* _def) { usernormal.clear(); usertexcoord.clear(); userface.clear(); + useredge.clear(); // clear internal variables mjuu_setvec(pos, 0, 0, 0); @@ -97,6 +98,7 @@ mjCMesh::~mjCMesh() { usernormal.clear(); usertexcoord.clear(); userface.clear(); + useredge.clear(); if (vert) mju_free(vert); if (normal) mju_free(normal); @@ -200,6 +202,26 @@ void mjCMesh::Compile(const mjVFS* vfs) { nface = (int)userface.size()/3; face = (int*) mju_malloc(3*nface*sizeof(int)); memcpy(face, userface.data(), 3*nface*sizeof(int)); + + // create half-edge structure (if mesh was in XML) + if (useredge.empty()) { + for (int i=0; i> faces; - std::array face1 = { - mesh.indices[index_in_mesh_indices], - mesh.indices[index_in_mesh_indices+1], - mesh.indices[index_in_mesh_indices+2]}; + tinyobj::index_t v0 = mesh.indices[index_in_mesh_indices]; + tinyobj::index_t v1 = mesh.indices[index_in_mesh_indices+1]; + tinyobj::index_t v2 = mesh.indices[index_in_mesh_indices+2]; + std::array face1 = {v0, v1, v2}; faces.push_back(face1); - if (mesh.num_face_vertices[face] == 4) { // add second triangle with 4th vertex - std::array face2 = { - mesh.indices[index_in_mesh_indices], - mesh.indices[index_in_mesh_indices+2], - mesh.indices[index_in_mesh_indices+3]}; + // add edges + useredge.push_back(std::pair(v0.vertex_index, v1.vertex_index)); + useredge.push_back(std::pair(v1.vertex_index, v2.vertex_index)); + useredge.push_back(std::pair(v2.vertex_index, v0.vertex_index)); + // handle quad: add second triangle with 4th vertex + if (mesh.num_face_vertices[face] == 4) { + tinyobj::index_t v3 = mesh.indices[index_in_mesh_indices+3]; + std::array face2 = {v0, v2, v3}; faces.push_back(face2); + useredge.push_back(std::pair(v0.vertex_index, v2.vertex_index)); + useredge.push_back(std::pair(v2.vertex_index, v3.vertex_index)); + useredge.push_back(std::pair(v3.vertex_index, v0.vertex_index)); } for (const auto& face_indices : faces) { int index_of_first_vertex = uservert.size()/3; diff --git a/src/user/user_objects.h b/src/user/user_objects.h index 383a391b..32206c6d 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -453,10 +453,11 @@ class mjCMesh: public mjCBase { double scale[3]; // rescale mesh bool smoothnormal; // do not exclude large-angle faces from normals - std::vector uservert; // user vertex data - std::vector usernormal; // user normal data - std::vector usertexcoord; // user texcoord data - std::vector userface; // user face data + std::vector uservert; // user vertex data + std::vector usernormal; // user normal data + std::vector usertexcoord; // user texcoord data + std::vector userface; // user face data + std::vector< std::pair > useredge; // user half-edge data private: mjCMesh(mjCModel* = 0, mjCDef* = 0); // constructor diff --git a/test/user/testdata/malformed_face.obj b/test/user/testdata/malformed_face.obj new file mode 100644 index 00000000..054631e2 --- /dev/null +++ b/test/user/testdata/malformed_face.obj @@ -0,0 +1,9 @@ +v 0 0 0 +v 1 0 0 +v 0 1 0 +v 0 0 1 +f 3 1 4 +f 1 2 4 +f 2 3 4 +# malformed face +f 1 2 3 diff --git a/test/user/testdata/malformed_face.xml b/test/user/testdata/malformed_face.xml new file mode 100644 index 00000000..7539d435 --- /dev/null +++ b/test/user/testdata/malformed_face.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/test/user/user_mesh_test.cc b/test/user/user_mesh_test.cc index 41435841..f7924032 100644 --- a/test/user/user_mesh_test.cc +++ b/test/user/user_mesh_test.cc @@ -43,6 +43,8 @@ static const char* const kTexturedTorusPath = "user/testdata/textured_torus.xml"; static const char* const kDuplicateOBJPath = "user/testdata/duplicate.xml"; +static const char* const kMalformedFaceOBJPath = + "user/testdata/malformed_face.xml"; using ::testing::HasSubstr; @@ -174,5 +176,31 @@ TEST_F(MujocoTest, TinyInertiaFails) { "mass and inertia of moving bodies must be larger than mjMINVAL")); } +TEST_F(MujocoTest, MalformedFaceFails) { + const std::string xml_path = GetTestDataFilePath(kMalformedFaceOBJPath); + std::array error; + mjModel* model = mj_loadXML(xml_path.c_str(), 0, error.data(), error.size()); + ASSERT_THAT(model, testing::IsNull()); + EXPECT_THAT(error.data(), HasSubstr("faces have inconsistent orientation")); +} + +TEST_F(MujocoTest, FlippedFaceFails) { + static constexpr char xml[] = R"( + + + + + + + + + )"; + std::array error; + LoadModelFromString(xml, error.data(), error.size()); + EXPECT_THAT(error.data(), HasSubstr("faces have inconsistent orientation")); +} + } // namespace } // namespace mujoco diff --git a/test/xml/xml_native_writer_test.cc b/test/xml/xml_native_writer_test.cc index 9ffcb72a..248dab2c 100644 --- a/test/xml/xml_native_writer_test.cc +++ b/test/xml/xml_native_writer_test.cc @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -416,10 +417,15 @@ TEST_F(XMLWriterTest, WriteReadCompare) { if (p.path().extension() == ext) { std::string xml = p.path().string(); + // if file is meant to fail, skip it + if (absl::StrContains(p.path().string(), "malformed_")) { + continue; + } + // load model std::array error; mjModel* m = mj_loadXML(xml.c_str(), nullptr, error.data(), error.size()); - ASSERT_THAT(m, NotNull()) << "Failed to load model: " << error.data(); + ASSERT_THAT(m, NotNull()) << "Failed to load " << xml.c_str() << ": " << error.data(); // make data mjData* d = mj_makeData(m);