Keep OBJ Texture Coordinates That Carry a W Component

The vt parser stopped reading the optional third component when texture
coordinates were cut down to u and v, but the check that nothing is left
on the line stayed. A legal "vt u v w" line was therefore rejected and
silently dropped, shifting every later texture index. The w component is
parsed again and discarded.

The texture coordinate stride is now a named constant, OBJ_TEXCOORD_LENGTH,
used by the parser and the importer, so the relative-index rebase cannot
drift from the storage layout again.
This commit is contained in:
Hanif Koh
2026-09-28 13:00:25 +08:00
parent 468fa3be86
commit 377034104c
4 changed files with 41 additions and 11 deletions
+2 -2
View File
@@ -168,11 +168,11 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s
if (data.textureCoordinates.size() > 0) { if (data.textureCoordinates.size() > 0) {
// A face vertex may omit vt or reference a missing one. Fall back to (0, 0) rather than // 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. // skipping the face, so obj_info.uvs stays aligned with the face indices.
const int uv_count = static_cast<int>(data.textureCoordinates.size() / 2); const int uv_count = static_cast<int>(data.textureCoordinates.size() / OBJ_TEXCOORD_LENGTH);
auto uv_at = [&data, uv_count](int idx) -> Vec2f { auto uv_at = [&data, uv_count](int idx) -> Vec2f {
if (idx < 0 || idx >= uv_count) if (idx < 0 || idx >= uv_count)
return Vec2f::Zero(); return Vec2f::Zero();
return Vec2f(data.textureCoordinates[idx * 2], data.textureCoordinates[idx * 2 + 1]); return Vec2f(data.textureCoordinates[idx * OBJ_TEXCOORD_LENGTH], data.textureCoordinates[idx * OBJ_TEXCOORD_LENGTH + 1]);
}; };
std::array<Vec2f, 3> uv_array{uv_at(uvs[0]), uv_at(uvs[1]), uv_at(uvs[2])}; 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.uvs.emplace_back(uv_array);
+4 -5
View File
@@ -51,19 +51,18 @@ static bool obj_parseline(const char *line, ObjData &data)
line = endptr; line = endptr;
EATWS(); EATWS();
} }
/*double w = 0; // The optional w is accepted but not stored: only u and v are used.
if (*line != 0) { if (*line != 0) {
w = strtod(line, &endptr); strtod(line, &endptr);
if (endptr == 0 || (*endptr != ' ' && *endptr != '\t' && *endptr != 0)) if (endptr == 0 || (*endptr != ' ' && *endptr != '\t' && *endptr != 0))
return false; return false;
line = endptr; line = endptr;
EATWS(); EATWS();
}*/ }
if (*line != 0) if (*line != 0)
return false; return false;
data.textureCoordinates.push_back((float)u); data.textureCoordinates.push_back((float)u);
data.textureCoordinates.push_back((float)v); data.textureCoordinates.push_back((float)v);
//data.textureCoordinates.push_back((float)w);
break; break;
} }
case 'n': case 'n':
@@ -245,7 +244,7 @@ static bool obj_parseline(const char *line, ObjData &data)
else else
-- vertex.normalIdx; -- vertex.normalIdx;
if (vertex.textureCoordIdx < 0) if (vertex.textureCoordIdx < 0)
vertex.textureCoordIdx += (int)data.textureCoordinates.size() / 2; vertex.textureCoordIdx += (int)data.textureCoordinates.size() / OBJ_TEXCOORD_LENGTH;
else else
-- vertex.textureCoordIdx; -- vertex.textureCoordIdx;
data.vertices.push_back(vertex); data.vertices.push_back(vertex);
+2 -1
View File
@@ -92,6 +92,7 @@ inline bool operator==(const ObjSmoothingGroup &v1, const ObjSmoothingGroup &v2)
} }
#define OBJ_VERTEX_COLOR_ALPHA 6 #define OBJ_VERTEX_COLOR_ALPHA 6
#define OBJ_VERTEX_LENGTH 7 // x, y, z, color_x,color_y,color_z,color_w #define OBJ_VERTEX_LENGTH 7 // x, y, z, color_x,color_y,color_z,color_w
#define OBJ_TEXCOORD_LENGTH 2 // u, v
#define ONE_FACE_SIZE 4//ONE_FACE format: f 8/4/6 7/3/6 6/2/6 -1/-1/-1 #define ONE_FACE_SIZE 4//ONE_FACE format: f 8/4/6 7/3/6 6/2/6 -1/-1/-1
struct ObjData { struct ObjData {
// Version of the data structure for load / store in the private binary format. // Version of the data structure for load / store in the private binary format.
@@ -100,7 +101,7 @@ struct ObjData {
// x, y, z, color_x,color_y,color_z,color_w // x, y, z, color_x,color_y,color_z,color_w
std::vector<float> coordinates; std::vector<float> coordinates;
bool has_vertex_color{false}; bool has_vertex_color{false};
// u, v, w // u, v
std::vector<float> textureCoordinates; std::vector<float> textureCoordinates;
// x, y, z // x, y, z
std::vector<float> normals; std::vector<float> normals;
+33 -3
View File
@@ -20,8 +20,8 @@ struct LoadedObj
}; };
// A tetrahedron with a material and two texture coordinates, (0.25, 0.5) and (0.75, 1). // 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. // 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) LoadedObj load_textured_tetrahedron(const std::string &first_face, const std::string &vts = "vt 0.25 0.5\nvt 0.75 1\n")
{ {
ScopedTemporaryFile obj(".obj"); ScopedTemporaryFile obj(".obj");
ScopedTemporaryFile mtl(".mtl"); ScopedTemporaryFile mtl(".mtl");
@@ -33,7 +33,7 @@ LoadedObj load_textured_tetrahedron(const std::string &first_face)
boost::nowide::ofstream out(obj.string()); boost::nowide::ofstream out(obj.string());
out << "mtllib " << mtl.path().filename().string() << "\n" out << "mtllib " << mtl.path().filename().string() << "\n"
<< "v 0 0 0\nv 10 0 0\nv 0 10 0\nv 0 0 10\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" << vts
<< "usemtl a\n" << "usemtl a\n"
<< first_face << "\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"; << "f 1/1 2/1 4/1\nf 1/1 4/1 3/1\nf 2/1 3/1 4/1\n";
@@ -90,3 +90,33 @@ TEST_CASE("A negative texture index counts back from the last texture coordinate
CHECK_THAT(uv[1].x(), WithinAbs(0.75, 1e-6)); CHECK_THAT(uv[1].x(), WithinAbs(0.75, 1e-6));
CHECK_THAT(uv[1].y(), WithinAbs(1., 1e-6)); CHECK_THAT(uv[1].y(), WithinAbs(1., 1e-6));
} }
TEST_CASE("Texture coordinates with a w component are kept", "[OBJ][Regression]")
{
const LoadedObj loaded = load_textured_tetrahedron("f 1/1 3/2 2/2", "vt 0.25 0.5 0\nvt 0.75 1 0\n");
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));
}
TEST_CASE("A texture coordinate with w does not shift the indices of the ones after it", "[OBJ][Regression]")
{
// The w on the first vt used to drop that line, so vt 2 resolved to the third coordinate.
const LoadedObj loaded = load_textured_tetrahedron("f 1/2 3/3 2/-1", "vt 0.1 0.2 0\nvt 0.25 0.5\nvt 0.75 1\n");
REQUIRE(loaded.ok);
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));
CHECK_THAT(uv[2].x(), WithinAbs(0.75, 1e-6));
CHECK_THAT(uv[2].y(), WithinAbs(1., 1e-6));
}