Skip to content

Commit 0099a2d

Browse files
Copilotsyoyo
andcommitted
Add tests for triangulation methods and preserve_quads option
Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com>
1 parent 6017e32 commit 0099a2d

File tree

1 file changed

+273
-0
lines changed

1 file changed

+273
-0
lines changed

tests/tester.cc

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2301,6 +2301,266 @@ main(
23012301
}
23022302
#endif
23032303

2304+
// Tests for triangulation method selection and preserve_quads option.
2305+
2306+
// Helper: parse an inline OBJ string using the v2 API with given config.
2307+
static void ParseInlineObj(const std::string &obj_text,
2308+
const tinyobj::ObjReaderConfig &config,
2309+
tinyobj::ObjReader &reader) {
2310+
bool ok = reader.ParseFromString(obj_text, std::string(""), config);
2311+
if (!reader.Warning().empty()) {
2312+
std::cout << "WARN: " << reader.Warning() << std::endl;
2313+
}
2314+
if (!reader.Error().empty()) {
2315+
std::cerr << "ERR: " << reader.Error() << std::endl;
2316+
}
2317+
TEST_CHECK(ok);
2318+
}
2319+
2320+
void test_triangulation_fan_quad() {
2321+
// A single quad face
2322+
const std::string obj =
2323+
"v 0 0 0\nv 1 0 0\nv 1 1 0\nv 0 1 0\n"
2324+
"f 1 2 3 4\n";
2325+
2326+
tinyobj::ObjReaderConfig config;
2327+
config.triangulate = true;
2328+
config.triangulation_method = "fan";
2329+
config.vertex_color = false;
2330+
2331+
tinyobj::ObjReader reader;
2332+
ParseInlineObj(obj, config, reader);
2333+
TEST_CHECK(1 == reader.GetShapes().size());
2334+
// A quad fan-triangulated should produce 2 triangles
2335+
TEST_CHECK(2 == reader.GetShapes()[0].mesh.num_face_vertices.size());
2336+
TEST_CHECK(3 == reader.GetShapes()[0].mesh.num_face_vertices[0]);
2337+
TEST_CHECK(3 == reader.GetShapes()[0].mesh.num_face_vertices[1]);
2338+
// 6 indices total
2339+
TEST_CHECK(6 == reader.GetShapes()[0].mesh.indices.size());
2340+
// Fan from vertex 0: (0,1,2), (0,2,3)
2341+
TEST_CHECK(0 == reader.GetShapes()[0].mesh.indices[0].vertex_index);
2342+
TEST_CHECK(1 == reader.GetShapes()[0].mesh.indices[1].vertex_index);
2343+
TEST_CHECK(2 == reader.GetShapes()[0].mesh.indices[2].vertex_index);
2344+
TEST_CHECK(0 == reader.GetShapes()[0].mesh.indices[3].vertex_index);
2345+
TEST_CHECK(2 == reader.GetShapes()[0].mesh.indices[4].vertex_index);
2346+
TEST_CHECK(3 == reader.GetShapes()[0].mesh.indices[5].vertex_index);
2347+
}
2348+
2349+
void test_triangulation_fan_pentagon() {
2350+
// A single pentagon face
2351+
const std::string obj =
2352+
"v 0 0 0\nv 1 0 0\nv 1.5 1 0\nv 0.5 1.5 0\nv -0.5 1 0\n"
2353+
"f 1 2 3 4 5\n";
2354+
2355+
tinyobj::ObjReaderConfig config;
2356+
config.triangulate = true;
2357+
config.triangulation_method = "fan";
2358+
config.vertex_color = false;
2359+
2360+
tinyobj::ObjReader reader;
2361+
ParseInlineObj(obj, config, reader);
2362+
TEST_CHECK(1 == reader.GetShapes().size());
2363+
// Pentagon fan-triangulated: 3 triangles
2364+
TEST_CHECK(3 == reader.GetShapes()[0].mesh.num_face_vertices.size());
2365+
TEST_CHECK(9 == reader.GetShapes()[0].mesh.indices.size());
2366+
// Fan: (0,1,2), (0,2,3), (0,3,4)
2367+
TEST_CHECK(0 == reader.GetShapes()[0].mesh.indices[0].vertex_index);
2368+
TEST_CHECK(1 == reader.GetShapes()[0].mesh.indices[1].vertex_index);
2369+
TEST_CHECK(2 == reader.GetShapes()[0].mesh.indices[2].vertex_index);
2370+
TEST_CHECK(0 == reader.GetShapes()[0].mesh.indices[6].vertex_index);
2371+
TEST_CHECK(3 == reader.GetShapes()[0].mesh.indices[7].vertex_index);
2372+
TEST_CHECK(4 == reader.GetShapes()[0].mesh.indices[8].vertex_index);
2373+
}
2374+
2375+
void test_triangulation_earclip_quad() {
2376+
// Default earclip method on quad
2377+
const std::string obj =
2378+
"v 0 0 0\nv 1 0 0\nv 1 1 0\nv 0 1 0\n"
2379+
"f 1 2 3 4\n";
2380+
2381+
tinyobj::ObjReaderConfig config;
2382+
config.triangulate = true;
2383+
config.triangulation_method = "earclip";
2384+
config.vertex_color = false;
2385+
2386+
tinyobj::ObjReader reader;
2387+
ParseInlineObj(obj, config, reader);
2388+
TEST_CHECK(1 == reader.GetShapes().size());
2389+
// Earclip quad => 2 triangles
2390+
TEST_CHECK(2 == reader.GetShapes()[0].mesh.num_face_vertices.size());
2391+
TEST_CHECK(6 == reader.GetShapes()[0].mesh.indices.size());
2392+
}
2393+
2394+
void test_triangulation_earclip_pentagon() {
2395+
// Earclip on pentagon
2396+
const std::string obj =
2397+
"v 0 0 0\nv 1 0 0\nv 1.5 1 0\nv 0.5 1.5 0\nv -0.5 1 0\n"
2398+
"f 1 2 3 4 5\n";
2399+
2400+
tinyobj::ObjReaderConfig config;
2401+
config.triangulate = true;
2402+
config.triangulation_method = "earclip";
2403+
config.vertex_color = false;
2404+
2405+
tinyobj::ObjReader reader;
2406+
ParseInlineObj(obj, config, reader);
2407+
TEST_CHECK(1 == reader.GetShapes().size());
2408+
// Pentagon ear-clipped: 3 triangles
2409+
TEST_CHECK(3 == reader.GetShapes()[0].mesh.num_face_vertices.size());
2410+
TEST_CHECK(9 == reader.GetShapes()[0].mesh.indices.size());
2411+
}
2412+
2413+
void test_preserve_quads() {
2414+
// Mix of triangle, quad, and pentagon faces
2415+
const std::string obj =
2416+
"v 0 0 0\nv 1 0 0\nv 0.5 1 0\n"
2417+
"v 2 0 0\nv 3 0 0\nv 3 1 0\nv 2 1 0\n"
2418+
"v 4 0 0\nv 5 0 0\nv 5.5 1 0\nv 4.5 1.5 0\nv 3.5 1 0\n"
2419+
"f 1 2 3\n"
2420+
"f 4 5 6 7\n"
2421+
"f 8 9 10 11 12\n";
2422+
2423+
tinyobj::ObjReaderConfig config;
2424+
config.triangulate = true;
2425+
config.triangulation_method = "earclip";
2426+
config.preserve_quads = true;
2427+
config.vertex_color = false;
2428+
2429+
tinyobj::ObjReader reader;
2430+
ParseInlineObj(obj, config, reader);
2431+
TEST_CHECK(1 == reader.GetShapes().size());
2432+
const tinyobj::shape_t &shape = reader.GetShapes()[0];
2433+
2434+
// Triangle (3 verts) => kept as-is (1 face, 3 verts)
2435+
// Quad (4 verts) => preserved as quad (1 face, 4 verts)
2436+
// Pentagon (5 verts) => triangulated (3 faces, 3 verts each)
2437+
// Total faces: 1 + 1 + 3 = 5
2438+
TEST_CHECK(5 == shape.mesh.num_face_vertices.size());
2439+
// face 0: triangle
2440+
TEST_CHECK(3 == shape.mesh.num_face_vertices[0]);
2441+
// face 1: quad preserved
2442+
TEST_CHECK(4 == shape.mesh.num_face_vertices[1]);
2443+
// faces 2,3,4: triangulated pentagon
2444+
TEST_CHECK(3 == shape.mesh.num_face_vertices[2]);
2445+
TEST_CHECK(3 == shape.mesh.num_face_vertices[3]);
2446+
TEST_CHECK(3 == shape.mesh.num_face_vertices[4]);
2447+
2448+
// Total indices: 3 + 4 + 9 = 16
2449+
TEST_CHECK(16 == shape.mesh.indices.size());
2450+
}
2451+
2452+
void test_preserve_quads_fan() {
2453+
// Same geometry but with fan triangulation
2454+
const std::string obj =
2455+
"v 0 0 0\nv 1 0 0\nv 0.5 1 0\n"
2456+
"v 2 0 0\nv 3 0 0\nv 3 1 0\nv 2 1 0\n"
2457+
"v 4 0 0\nv 5 0 0\nv 5.5 1 0\nv 4.5 1.5 0\nv 3.5 1 0\n"
2458+
"f 1 2 3\n"
2459+
"f 4 5 6 7\n"
2460+
"f 8 9 10 11 12\n";
2461+
2462+
tinyobj::ObjReaderConfig config;
2463+
config.triangulate = true;
2464+
config.triangulation_method = "fan";
2465+
config.preserve_quads = true;
2466+
config.vertex_color = false;
2467+
2468+
tinyobj::ObjReader reader;
2469+
ParseInlineObj(obj, config, reader);
2470+
TEST_CHECK(1 == reader.GetShapes().size());
2471+
const tinyobj::shape_t &shape = reader.GetShapes()[0];
2472+
2473+
// Triangle: 1 face (3 verts)
2474+
// Quad: preserved (1 face, 4 verts)
2475+
// Pentagon: fan-triangulated (3 faces, 3 verts each)
2476+
TEST_CHECK(5 == shape.mesh.num_face_vertices.size());
2477+
TEST_CHECK(3 == shape.mesh.num_face_vertices[0]);
2478+
TEST_CHECK(4 == shape.mesh.num_face_vertices[1]);
2479+
TEST_CHECK(3 == shape.mesh.num_face_vertices[2]);
2480+
TEST_CHECK(3 == shape.mesh.num_face_vertices[3]);
2481+
TEST_CHECK(3 == shape.mesh.num_face_vertices[4]);
2482+
TEST_CHECK(16 == shape.mesh.indices.size());
2483+
}
2484+
2485+
void test_no_triangulation() {
2486+
// Ensure triangulate=false still works
2487+
const std::string obj =
2488+
"v 0 0 0\nv 1 0 0\nv 1 1 0\nv 0 1 0\n"
2489+
"v 2 0 0\nv 3 0 0\nv 3.5 1 0\nv 2.5 1.5 0\nv 1.5 1 0\n"
2490+
"f 1 2 3 4\n"
2491+
"f 5 6 7 8 9\n";
2492+
2493+
tinyobj::ObjReaderConfig config;
2494+
config.triangulate = false;
2495+
config.vertex_color = false;
2496+
2497+
tinyobj::ObjReader reader;
2498+
ParseInlineObj(obj, config, reader);
2499+
TEST_CHECK(1 == reader.GetShapes().size());
2500+
const tinyobj::shape_t &shape = reader.GetShapes()[0];
2501+
// No triangulation: 2 original faces
2502+
TEST_CHECK(2 == shape.mesh.num_face_vertices.size());
2503+
TEST_CHECK(4 == shape.mesh.num_face_vertices[0]);
2504+
TEST_CHECK(5 == shape.mesh.num_face_vertices[1]);
2505+
TEST_CHECK(9 == shape.mesh.indices.size());
2506+
}
2507+
2508+
void test_triangulation_default_config_is_earclip() {
2509+
// Default ObjReaderConfig should use earclip
2510+
tinyobj::ObjReaderConfig config;
2511+
TEST_CHECK(true == config.triangulate);
2512+
TEST_CHECK(std::string("earclip") == config.triangulation_method);
2513+
TEST_CHECK(false == config.preserve_quads);
2514+
}
2515+
2516+
void test_triangulation_method_string_aliases() {
2517+
// "simple" should be equivalent to "fan"
2518+
const std::string obj =
2519+
"v 0 0 0\nv 1 0 0\nv 1 1 0\nv 0 1 0\n"
2520+
"f 1 2 3 4\n";
2521+
2522+
tinyobj::ObjReaderConfig config;
2523+
config.triangulate = true;
2524+
config.vertex_color = false;
2525+
2526+
// Load with "simple"
2527+
config.triangulation_method = "simple";
2528+
tinyobj::ObjReader reader1;
2529+
ParseInlineObj(obj, config, reader1);
2530+
2531+
// Load with "fan"
2532+
config.triangulation_method = "fan";
2533+
tinyobj::ObjReader reader2;
2534+
ParseInlineObj(obj, config, reader2);
2535+
2536+
// Both should produce identical results
2537+
TEST_CHECK(reader1.GetShapes()[0].mesh.indices.size() ==
2538+
reader2.GetShapes()[0].mesh.indices.size());
2539+
for (size_t i = 0; i < reader1.GetShapes()[0].mesh.indices.size(); i++) {
2540+
TEST_CHECK(reader1.GetShapes()[0].mesh.indices[i].vertex_index ==
2541+
reader2.GetShapes()[0].mesh.indices[i].vertex_index);
2542+
}
2543+
}
2544+
2545+
void test_triangulation_v1_api_backward_compat() {
2546+
// Ensure v1 API (LoadObj with bool triangulate) still works correctly
2547+
tinyobj::attrib_t attrib;
2548+
std::vector<tinyobj::shape_t> shapes;
2549+
std::vector<tinyobj::material_t> materials;
2550+
2551+
std::string warn;
2552+
std::string err;
2553+
bool ret = tinyobj::LoadObj(
2554+
&attrib, &shapes, &materials, &warn, &err,
2555+
"../models/issue-295-trianguation-failure.obj",
2556+
gMtlBasePath, /* triangulate */ true);
2557+
2558+
TEST_CHECK(true == ret);
2559+
TEST_CHECK(1 == shapes.size());
2560+
// 14 quad faces => 28 triangles (same as existing test_face_missing_issue295)
2561+
TEST_CHECK(28 == shapes[0].mesh.num_face_vertices.size());
2562+
}
2563+
23042564
TEST_LIST = {
23052565
{"cornell_box", test_cornell_box},
23062566
{"catmark_torus_creases0", test_catmark_torus_creases0},
@@ -2389,4 +2649,17 @@ TEST_LIST = {
23892649
{"test_parse_error_backward_compat", test_parse_error_backward_compat},
23902650
{"test_split_string_preserves_non_escape_backslash",
23912651
test_split_string_preserves_non_escape_backslash},
2652+
{"test_triangulation_fan_quad", test_triangulation_fan_quad},
2653+
{"test_triangulation_fan_pentagon", test_triangulation_fan_pentagon},
2654+
{"test_triangulation_earclip_quad", test_triangulation_earclip_quad},
2655+
{"test_triangulation_earclip_pentagon", test_triangulation_earclip_pentagon},
2656+
{"test_preserve_quads", test_preserve_quads},
2657+
{"test_preserve_quads_fan", test_preserve_quads_fan},
2658+
{"test_no_triangulation", test_no_triangulation},
2659+
{"test_triangulation_default_config_is_earclip",
2660+
test_triangulation_default_config_is_earclip},
2661+
{"test_triangulation_method_string_aliases",
2662+
test_triangulation_method_string_aliases},
2663+
{"test_triangulation_v1_api_backward_compat",
2664+
test_triangulation_v1_api_backward_compat},
23922665
{NULL, NULL}};

0 commit comments

Comments
 (0)