diff --git a/src/libslic3r/Format/OBJ.cpp b/src/libslic3r/Format/OBJ.cpp index de91efb64d..8242f2c0c9 100644 --- a/src/libslic3r/Format/OBJ.cpp +++ b/src/libslic3r/Format/OBJ.cpp @@ -141,7 +141,7 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s its.indices.emplace_back(indices[0], indices[1], indices[2]); int face_index =its.indices.size() - 1; RGBA face_color; - auto set_face_color = [&uvs, &data, &mtl_data, &obj_info, &face_color](int face_index, const std::string mtl_name) { + auto set_face_color = [&uvs, &data, &mtl_data, &obj_info, &face_color](int face_index, const std::string mtl_name, const std::array &corners) { if (mtl_data.new_mtl_unmap.find(mtl_name) != mtl_data.new_mtl_unmap.end()) { bool is_merge_ka_kd = true; for (size_t n = 0; n < 3; n++) { @@ -174,7 +174,7 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s return Vec2f::Zero(); return Vec2f(data.textureCoordinates[idx * OBJ_TEXCOORD_LENGTH], data.textureCoordinates[idx * OBJ_TEXCOORD_LENGTH + 1]); }; - std::array uv_array{uv_at(uvs[0]), uv_at(uvs[1]), uv_at(uvs[2])}; + std::array uv_array{uv_at(uvs[corners[0]]), uv_at(uvs[corners[1]]), uv_at(uvs[corners[2]])}; obj_info.uvs.emplace_back(uv_array); } obj_info.face_colors.emplace_back(face_color); @@ -185,27 +185,27 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s } } }; - auto set_face_color_by_mtl = [&data, &set_face_color](int face_index) { + auto set_face_color_by_mtl = [&data, &set_face_color](int face_index, const std::array &corners) { if (data.usemtls.size() == 1) { - set_face_color(face_index, data.usemtls[0].name); + set_face_color(face_index, data.usemtls[0].name, corners); } else { for (size_t k = 0; k < data.usemtls.size(); k++) { auto mtl = data.usemtls[k]; if (face_index >= mtl.face_start && face_index <= mtl.face_end) { - set_face_color(face_index, data.usemtls[k].name); + set_face_color(face_index, data.usemtls[k].name, corners); break; } } } }; if (exist_mtl) { - set_face_color_by_mtl(face_index); + set_face_color_by_mtl(face_index, {0, 1, 2}); } if (cnt == 4) { its.indices.emplace_back(indices[0], indices[2], indices[3]); int face_index = its.indices.size() - 1; if (exist_mtl) { - set_face_color_by_mtl(face_index); + set_face_color_by_mtl(face_index, {0, 2, 3}); } } } @@ -217,8 +217,12 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s message = _L("This OBJ file couldn't be read because it's empty."); return false; } - if (meshptr->volume() < 0) + if (meshptr->volume() < 0) { meshptr->flip_triangles(); + // Flipping swaps corners 1 and 2 of every face, so the UVs have to follow. + for (std::array &uv : obj_info.uvs) + std::swap(uv[1], uv[2]); + } // Hand the parsed material table back so callers can build a TexturedMesh from it. if (out_mtl) *out_mtl = mtl_data; diff --git a/tests/libslic3r/test_obj.cpp b/tests/libslic3r/test_obj.cpp index d198a69c8c..2cbf57ff04 100644 --- a/tests/libslic3r/test_obj.cpp +++ b/tests/libslic3r/test_obj.cpp @@ -19,9 +19,8 @@ struct LoadedObj ObjInfo info; }; -// A tetrahedron with a material and two texture coordinates, (0.25, 0.5) and (0.75, 1). -// Only the first face and the vt lines are varied; the other three faces reference vt 1. -LoadedObj load_textured_tetrahedron(const std::string &first_face, const std::string &vts = "vt 0.25 0.5\nvt 0.75 1\n") +// Loads an OBJ made of the given lines, with a material library that defines material "a". +LoadedObj load_textured_obj(const std::string &body) { ScopedTemporaryFile obj(".obj"); ScopedTemporaryFile mtl(".mtl"); @@ -31,12 +30,7 @@ LoadedObj load_textured_tetrahedron(const std::string &first_face, const std::st } { boost::nowide::ofstream out(obj.string()); - out << "mtllib " << mtl.path().filename().string() << "\n" - << "v 0 0 0\nv 10 0 0\nv 0 10 0\nv 0 0 10\n" - << vts - << "usemtl a\n" - << first_face << "\n" - << "f 1/1 2/1 4/1\nf 1/1 4/1 3/1\nf 2/1 3/1 4/1\n"; + out << "mtllib " << mtl.path().filename().string() << "\n" << body; } LoadedObj loaded; std::string message; @@ -44,6 +38,21 @@ LoadedObj load_textured_tetrahedron(const std::string &first_face, const std::st return loaded; } +// A tetrahedron with a material and two texture coordinates, (0.25, 0.5) and (0.75, 1). +// Only the first face and the vt lines are varied; the other three faces reference vt 1. +LoadedObj load_textured_tetrahedron(const std::string &first_face, const std::string &vts = "vt 0.25 0.5\nvt 0.75 1\n") +{ + return load_textured_obj("v 0 0 0\nv 10 0 0\nv 0 10 0\nv 0 0 10\n" + vts + "usemtl a\n" + first_face + "\n" + + "f 1/1 2/1 4/1\nf 1/1 4/1 3/1\nf 2/1 3/1 4/1\n"); +} + +// Texture coordinate n is (n / 10, n / 20), so a UV identifies the vt it came from. +void check_uv_is_vt(const Vec2f &uv, int vt) +{ + CHECK_THAT(uv.x(), WithinAbs(vt / 10., 1e-6)); + CHECK_THAT(uv.y(), WithinAbs(vt / 20., 1e-6)); +} + } // namespace TEST_CASE("An out-of-range texture index falls back to a zero UV and keeps the geometry", "[OBJ][Regression]") @@ -120,3 +129,39 @@ TEST_CASE("A texture coordinate with w does not shift the indices of the ones af CHECK_THAT(uv[2].x(), WithinAbs(0.75, 1e-6)); CHECK_THAT(uv[2].y(), WithinAbs(1., 1e-6)); } + +TEST_CASE("Both triangles of a quad take the texture coordinates of their own corners", "[OBJ][Regression]") +{ + const LoadedObj loaded = load_textured_obj("v 0 0 0\nv 10 0 0\nv 10 10 0\nv 0 10 0\n" + "vt 0.1 0.05\nvt 0.2 0.1\nvt 0.3 0.15\nvt 0.4 0.2\n" + "usemtl a\n" + "f 1/1 2/2 3/3 4/4\n"); + + REQUIRE(loaded.ok); + REQUIRE(loaded.mesh.facets_count() == 2); + REQUIRE(loaded.info.uvs.size() == 2); + check_uv_is_vt(loaded.info.uvs[0][0], 1); + check_uv_is_vt(loaded.info.uvs[0][1], 2); + check_uv_is_vt(loaded.info.uvs[0][2], 3); + check_uv_is_vt(loaded.info.uvs[1][0], 1); + check_uv_is_vt(loaded.info.uvs[1][1], 3); + check_uv_is_vt(loaded.info.uvs[1][2], 4); +} + +TEST_CASE("Texture coordinates follow the corners of a mesh that is flipped on load", "[OBJ][Regression]") +{ + // The faces wind inwards, so the loader flips them. Vertex n uses vt n. + const LoadedObj loaded = load_textured_obj("v 0 0 0\nv 10 0 0\nv 0 10 0\nv 0 0 10\n" + "vt 0.1 0.05\nvt 0.2 0.1\nvt 0.3 0.15\nvt 0.4 0.2\n" + "usemtl a\n" + "f 1/1 2/2 3/3\nf 1/1 4/4 2/2\nf 1/1 3/3 4/4\nf 2/2 4/4 3/3\n"); + + REQUIRE(loaded.ok); + const indexed_triangle_set &its = loaded.mesh.its; + CHECK(its_volume(its) > 0.f); + REQUIRE(its.indices.size() == 4); + REQUIRE(loaded.info.uvs.size() == 4); + for (size_t face = 0; face < its.indices.size(); ++face) + for (int corner = 0; corner < 3; ++corner) + check_uv_is_vt(loaded.info.uvs[face][corner], its.indices[face][corner] + 1); +}