// Unit tests para gltf_load_mesh (issue: gltf_load_mesh_cpp_gfx). // Cubre: reject magic invalido, triangulo con POSITION+indices sin NORMAL // (normales generadas correctamente), quad (2 tris), load desde memoria. // No requiere contexto GL — logica CPU pura. #define CATCH_CONFIG_MAIN #include "catch_amalgamated.hpp" #include "gfx/gltf_load_mesh.h" #include #include #include #include // --------------------------------------------------------------------------- // GLB builder helpers (minimal — construye GLB en memoria para tests) // --------------------------------------------------------------------------- // nlohmann is in vendor/ but test includes functions/ and framework/ by default. // Build a GLB manually via byte helpers to avoid adding another include path. static void write_u32le(std::vector& buf, uint32_t v) { buf.push_back(v & 0xFF); buf.push_back((v >> 8) & 0xFF); buf.push_back((v >> 16) & 0xFF); buf.push_back((v >> 24) & 0xFF); } static void write_f32le(std::vector& buf, float v) { uint32_t u; std::memcpy(&u, &v, 4); write_u32le(buf, u); } // Align 'buf' to 4-byte boundary by appending 0x20 (space) padding. static void align4(std::vector& buf) { while (buf.size() % 4 != 0) buf.push_back(0x20); } // Build a minimal GLB with: // positions: flat float xyz array (length = nv*3) // indices: uint16 array (length = ni) // normals: optional float xyz array (length = nv*3, nullptr = omit) // Returns the complete GLB byte vector. static std::vector build_glb(const float* positions, size_t nv, const uint16_t* indices, size_t ni, const float* normals = nullptr) { // BIN chunk: positions | indices | (normals) std::vector bin; size_t pos_offset = 0; size_t pos_byteLen = nv * 3 * 4; for (size_t i = 0; i < nv*3; ++i) write_f32le(bin, positions[i]); // pad before indices so they start at 4-byte alignment while (bin.size() % 4 != 0) bin.push_back(0); size_t idx_offset = bin.size(); size_t idx_byteLen = ni * 2; for (size_t i = 0; i < ni; ++i) { bin.push_back(indices[i] & 0xFF); bin.push_back((indices[i] >> 8) & 0xFF); } size_t nrm_offset = 0, nrm_byteLen = 0; if (normals) { while (bin.size() % 4 != 0) bin.push_back(0); nrm_offset = bin.size(); nrm_byteLen = nv * 3 * 4; for (size_t i = 0; i < nv*3; ++i) write_f32le(bin, normals[i]); } // GLB chunk length must be multiple of 4 while (bin.size() % 4 != 0) bin.push_back(0); // Build JSON // accessor 0: POSITION (vec3 float, bufferView 0) // accessor 1: indices (scalar uint16, bufferView 1) // accessor 2: NORMAL (vec3 float, bufferView 2) — if normals present std::string json = "{"; json += "\"asset\":{\"version\":\"2.0\"},"; json += "\"buffers\":[{\"byteLength\":" + std::to_string(bin.size()) + "}],"; // bufferViews json += "\"bufferViews\":["; json += "{\"buffer\":0,\"byteOffset\":" + std::to_string(pos_offset) + ",\"byteLength\":" + std::to_string(pos_byteLen) + "}"; json += ",{\"buffer\":0,\"byteOffset\":" + std::to_string(idx_offset) + ",\"byteLength\":" + std::to_string(idx_byteLen) + "}"; if (normals) { json += ",{\"buffer\":0,\"byteOffset\":" + std::to_string(nrm_offset) + ",\"byteLength\":" + std::to_string(nrm_byteLen) + "}"; } json += "],"; // accessors json += "\"accessors\":["; json += "{\"bufferView\":0,\"byteOffset\":0,\"componentType\":5126,\"count\":" + std::to_string(nv) + ",\"type\":\"VEC3\"}"; json += ",{\"bufferView\":1,\"byteOffset\":0,\"componentType\":5123,\"count\":" + std::to_string(ni) + ",\"type\":\"SCALAR\"}"; if (normals) { json += ",{\"bufferView\":2,\"byteOffset\":0,\"componentType\":5126,\"count\":" + std::to_string(nv) + ",\"type\":\"VEC3\"}"; } json += "],"; // meshes / primitives std::string attrs = "\"POSITION\":0"; if (normals) attrs += ",\"NORMAL\":2"; json += "\"meshes\":[{\"primitives\":[{\"attributes\":{" + attrs + "},\"indices\":1}]}]"; json += "}"; // Pad JSON to 4-byte boundary while (json.size() % 4 != 0) json += ' '; // Assemble GLB std::vector glb; uint32_t json_chunk_len = (uint32_t)json.size(); uint32_t bin_chunk_len = (uint32_t)bin.size(); uint32_t total = 12 + 8 + json_chunk_len + 8 + bin_chunk_len; // Header write_u32le(glb, 0x46546C67u); // magic "glTF" write_u32le(glb, 2u); // version write_u32le(glb, total); // Chunk 0: JSON write_u32le(glb, json_chunk_len); write_u32le(glb, 0x4E4F534Au); // "JSON" for (char c : json) glb.push_back((uint8_t)c); // Chunk 1: BIN write_u32le(glb, bin_chunk_len); write_u32le(glb, 0x004E4942u); // "BIN\0" for (uint8_t b : bin) glb.push_back(b); return glb; } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- TEST_CASE("invalid magic -> empty Mesh + last_error set", "[gltf][reject]") { std::vector bad(12, 0); bad[0] = 0xDE; bad[1] = 0xAD; bad[2] = 0xBE; bad[3] = 0xEF; // version=2, total=12 bad[4]=2; bad[8]=12; auto m = fn::gfx::gltf_load_mesh_from_memory(bad.data(), bad.size()); REQUIRE(m.positions.empty()); REQUIRE(m.indices.empty()); std::string err = fn::gfx::gltf_load_last_error(); REQUIRE(!err.empty()); INFO("last_error: " << err); // Should mention magic or "not a GLB" REQUIRE((err.find("magic") != std::string::npos || err.find("GLB") != std::string::npos)); } TEST_CASE("too-small buffer -> empty Mesh + last_error set", "[gltf][reject]") { std::vector tiny = {0x67, 0x6C, 0x54, 0x46}; // only 4 bytes auto m = fn::gfx::gltf_load_mesh_from_memory(tiny.data(), tiny.size()); REQUIRE(m.positions.empty()); std::string err = fn::gfx::gltf_load_last_error(); REQUIRE(!err.empty()); } TEST_CASE("triangle without NORMAL -> normals generated, correct count", "[gltf][triangle][normals]") { // One triangle in XY plane (z=0): (0,0,0), (1,0,0), (0,1,0) // Face normal = (0,0,1) → all vertices should get approx (0,0,1) float pos[] = { 0,0,0, 1,0,0, 0,1,0 }; uint16_t idx[] = { 0, 1, 2 }; auto glb = build_glb(pos, 3, idx, 3, /*normals=*/nullptr); auto m = fn::gfx::gltf_load_mesh_from_memory(glb.data(), glb.size()); REQUIRE(m.positions.size() == 9); // 3 vertices * 3 floats REQUIRE(m.indices.size() == 3); REQUIRE(m.normals.size() == m.positions.size()); // Check positions REQUIRE(m.positions[0] == Catch::Approx(0.0f)); REQUIRE(m.positions[1] == Catch::Approx(0.0f)); REQUIRE(m.positions[2] == Catch::Approx(0.0f)); REQUIRE(m.positions[3] == Catch::Approx(1.0f)); REQUIRE(m.positions[6] == Catch::Approx(0.0f)); REQUIRE(m.positions[7] == Catch::Approx(1.0f)); // Check indices REQUIRE(m.indices[0] == 0u); REQUIRE(m.indices[1] == 1u); REQUIRE(m.indices[2] == 2u); // Generated normals should point toward +Z for all 3 vertices for (int v = 0; v < 3; ++v) { REQUIRE(m.normals[v*3+0] == Catch::Approx(0.0f).margin(1e-5f)); REQUIRE(m.normals[v*3+1] == Catch::Approx(0.0f).margin(1e-5f)); REQUIRE(m.normals[v*3+2] == Catch::Approx(1.0f).margin(1e-5f)); } } TEST_CASE("quad (2 triangles) -> positions.size()==12, indices.size()==6", "[gltf][quad]") { // Quad in XY: (0,0,0),(1,0,0),(1,1,0),(0,1,0) split into 2 tris float pos[] = { 0,0,0, 1,0,0, 1,1,0, 0,1,0 }; uint16_t idx[] = { 0,1,2, 0,2,3 }; auto glb = build_glb(pos, 4, idx, 6, nullptr); auto m = fn::gfx::gltf_load_mesh_from_memory(glb.data(), glb.size()); REQUIRE(m.positions.size() == 12); // 4 * 3 REQUIRE(m.normals.size() == 12); REQUIRE(m.indices.size() == 6); // All normals should be (0,0,1) — flat XY plane for (int v = 0; v < 4; ++v) { REQUIRE(m.normals[v*3+2] == Catch::Approx(1.0f).margin(1e-5f)); } } TEST_CASE("explicit normals -> passed through unchanged", "[gltf][normals]") { float pos[] = { 0,0,0, 1,0,0, 0,1,0 }; uint16_t idx[] = { 0, 1, 2 }; // Provide normals pointing in -Z (unusual, but should be respected) float nrm[] = { 0,0,-1, 0,0,-1, 0,0,-1 }; auto glb = build_glb(pos, 3, idx, 3, nrm); auto m = fn::gfx::gltf_load_mesh_from_memory(glb.data(), glb.size()); REQUIRE(m.positions.size() == 9); REQUIRE(m.normals.size() == 9); for (int v = 0; v < 3; ++v) { REQUIRE(m.normals[v*3+0] == Catch::Approx(0.0f).margin(1e-5f)); REQUIRE(m.normals[v*3+1] == Catch::Approx(0.0f).margin(1e-5f)); REQUIRE(m.normals[v*3+2] == Catch::Approx(-1.0f).margin(1e-5f)); } } TEST_CASE("nonexistent file -> empty Mesh + last_error set", "[gltf][file]") { auto m = fn::gfx::gltf_load_mesh_from_file("/tmp/does_not_exist_gltf_test_abc123.glb"); REQUIRE(m.positions.empty()); std::string err = fn::gfx::gltf_load_last_error(); REQUIRE(!err.empty()); }