From 824b092a6ed7656daec02c59c0e3a2a078b17f8e Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Mon, 28 Sep 2026 05:59:22 +0800 Subject: [PATCH] Validate OBJ Texture-Coordinate Indices load_obj read the texture coordinates of a face without checking the vt index, so a face referencing a vt past the end of the list read out of bounds and crashed, and a face vertex with no vt read index -1. Out-of-range or missing indices now fall back to a zero UV. The face keeps its entry in the per-face UV list, so the following faces stay aligned, and the geometry loads as before. Negative (relative) vt indices were also rebased by dividing the float count by 3, but each vt stores two floats. --- src/libslic3r/Format/OBJ.cpp | 13 +++-- src/libslic3r/Format/objparser.cpp | 2 +- tests/libslic3r/CMakeLists.txt | 1 + tests/libslic3r/test_obj.cpp | 92 ++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 tests/libslic3r/test_obj.cpp diff --git a/src/libslic3r/Format/OBJ.cpp b/src/libslic3r/Format/OBJ.cpp index 10abe8e4de..56244f480b 100644 --- a/src/libslic3r/Format/OBJ.cpp +++ b/src/libslic3r/Format/OBJ.cpp @@ -166,10 +166,15 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s obj_info.uv_map_pngs[face_index] = png_name; } if (data.textureCoordinates.size() > 0) { - Vec2f uv0(data.textureCoordinates[uvs[0] * 2], data.textureCoordinates[uvs[0] * 2 + 1]); - Vec2f uv1(data.textureCoordinates[uvs[1] * 2], data.textureCoordinates[uvs[1] * 2 + 1]); - Vec2f uv2(data.textureCoordinates[uvs[2] * 2], data.textureCoordinates[uvs[2] * 2 + 1]); - std::array uv_array{uv0, uv1, uv2}; + // A face vertex may omit vt or reference a missing one. Fall back to (0, 0) rather than + // skipping the face, so obj_info.uvs stays aligned with the face indices. + const int uv_count = static_cast(data.textureCoordinates.size() / 2); + auto uv_at = [&data, uv_count](int idx) -> Vec2f { + if (idx < 0 || idx >= uv_count) + return Vec2f::Zero(); + return Vec2f(data.textureCoordinates[idx * 2], data.textureCoordinates[idx * 2 + 1]); + }; + std::array uv_array{uv_at(uvs[0]), uv_at(uvs[1]), uv_at(uvs[2])}; obj_info.uvs.emplace_back(uv_array); } obj_info.face_colors.emplace_back(face_color); diff --git a/src/libslic3r/Format/objparser.cpp b/src/libslic3r/Format/objparser.cpp index 886fa423bd..5ee1a76618 100644 --- a/src/libslic3r/Format/objparser.cpp +++ b/src/libslic3r/Format/objparser.cpp @@ -245,7 +245,7 @@ static bool obj_parseline(const char *line, ObjData &data) else -- vertex.normalIdx; if (vertex.textureCoordIdx < 0) - vertex.textureCoordIdx += (int)data.textureCoordinates.size() / 3; + vertex.textureCoordIdx += (int)data.textureCoordinates.size() / 2; else -- vertex.textureCoordIdx; data.vertices.push_back(vertex); diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 185dce37da..9c6dae471a 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -32,6 +32,7 @@ add_executable(${_TEST_NAME}_tests test_mutable_priority_queue.cpp test_minimum_spanning_tree.cpp test_nozzle_volume_type.cpp + test_obj.cpp test_step.cpp test_stl.cpp test_triangle_selector.cpp diff --git a/tests/libslic3r/test_obj.cpp b/tests/libslic3r/test_obj.cpp new file mode 100644 index 0000000000..2479bb9203 --- /dev/null +++ b/tests/libslic3r/test_obj.cpp @@ -0,0 +1,92 @@ +#include + +#include "libslic3r/TriangleMesh.hpp" +#include "libslic3r/Format/OBJ.hpp" + +#include + +#include "test_utils.hpp" + +using namespace Slic3r; +using Catch::Matchers::WithinAbs; + +namespace { + +struct LoadedObj +{ + bool ok{false}; + TriangleMesh mesh; + ObjInfo info; +}; + +// A tetrahedron with a material and two texture coordinates, (0.25, 0.5) and (0.75, 1). +// Only the first face is varied; the other three reference vt 1. +LoadedObj load_textured_tetrahedron(const std::string &first_face) +{ + ScopedTemporaryFile obj(".obj"); + ScopedTemporaryFile mtl(".mtl"); + { + boost::nowide::ofstream out(mtl.string()); + out << "newmtl a\nKd 1 0 0\n"; + } + { + 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" + << "vt 0.25 0.5\nvt 0.75 1\n" + << "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"; + } + LoadedObj loaded; + std::string message; + loaded.ok = load_obj(obj.string().c_str(), &loaded.mesh, loaded.info, message); + return loaded; +} + +} // namespace + +TEST_CASE("An out-of-range texture index falls back to a zero UV and keeps the geometry", "[OBJ][Regression]") +{ + const LoadedObj loaded = load_textured_tetrahedron("f 1/1000000000 3/1 2/1"); + + REQUIRE(loaded.ok); + CHECK(loaded.mesh.facets_count() == 4); + REQUIRE(loaded.info.uvs.size() == 4); + const std::array &uv = loaded.info.uvs.front(); + CHECK_THAT(uv[0].x(), WithinAbs(0., 1e-6)); + CHECK_THAT(uv[0].y(), WithinAbs(0., 1e-6)); + CHECK_THAT(uv[1].x(), WithinAbs(0.25, 1e-6)); + CHECK_THAT(uv[1].y(), WithinAbs(0.5, 1e-6)); +} + +TEST_CASE("A face without texture indices loads among faces that have them", "[OBJ][Regression]") +{ + const LoadedObj loaded = load_textured_tetrahedron("f 1 3 2"); + + REQUIRE(loaded.ok); + CHECK(loaded.mesh.facets_count() == 4); + // One UV entry per face, so later faces keep their own coordinates. + REQUIRE(loaded.info.uvs.size() == 4); + for (const Vec2f &uv : loaded.info.uvs.front()) { + CHECK_THAT(uv.x(), WithinAbs(0., 1e-6)); + CHECK_THAT(uv.y(), WithinAbs(0., 1e-6)); + } + CHECK_THAT(loaded.info.uvs[1][0].x(), WithinAbs(0.25, 1e-6)); + CHECK_THAT(loaded.info.uvs[1][0].y(), WithinAbs(0.5, 1e-6)); +} + +TEST_CASE("A negative texture index counts back from the last texture coordinate", "[OBJ]") +{ + // -1 is the most recent vt (0.75, 1), -2 the one before it (0.25, 0.5). + const LoadedObj loaded = load_textured_tetrahedron("f 1/-2 3/-1 2/-1"); + + REQUIRE(loaded.ok); + CHECK(loaded.mesh.facets_count() == 4); + REQUIRE(loaded.info.uvs.size() == 4); + const std::array &uv = loaded.info.uvs.front(); + CHECK_THAT(uv[0].x(), WithinAbs(0.25, 1e-6)); + CHECK_THAT(uv[0].y(), WithinAbs(0.5, 1e-6)); + CHECK_THAT(uv[1].x(), WithinAbs(0.75, 1e-6)); + CHECK_THAT(uv[1].y(), WithinAbs(1., 1e-6)); +}