@@ -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\n v 1 0 0\n v 1 1 0\n v 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\n v 1 0 0\n v 1.5 1 0\n v 0.5 1.5 0\n v -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\n v 1 0 0\n v 1 1 0\n v 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\n v 1 0 0\n v 1.5 1 0\n v 0.5 1.5 0\n v -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\n v 1 0 0\n v 0.5 1 0\n "
2417+ " v 2 0 0\n v 3 0 0\n v 3 1 0\n v 2 1 0\n "
2418+ " v 4 0 0\n v 5 0 0\n v 5.5 1 0\n v 4.5 1.5 0\n v 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\n v 1 0 0\n v 0.5 1 0\n "
2456+ " v 2 0 0\n v 3 0 0\n v 3 1 0\n v 2 1 0\n "
2457+ " v 4 0 0\n v 5 0 0\n v 5.5 1 0\n v 4.5 1.5 0\n v 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\n v 1 0 0\n v 1 1 0\n v 0 1 0\n "
2489+ " v 2 0 0\n v 3 0 0\n v 3.5 1 0\n v 2.5 1.5 0\n v 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\n v 1 0 0\n v 1 1 0\n v 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+
23042564TEST_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