mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-29 11:51:24 +00:00
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.
This commit is contained in:
@@ -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<Vec2f, 3> 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<int>(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<Vec2f, 3> 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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/Format/OBJ.hpp"
|
||||
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
|
||||
#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<Vec2f, 3> &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<Vec2f, 3> &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));
|
||||
}
|
||||
Reference in New Issue
Block a user