fce88032ca
- .mcp.json - bash/functions/infra/write_mcp_jupyter_config.md - bash/functions/infra/write_mcp_jupyter_config.sh - cpp/CMakeLists.txt - cpp/apps/chart_demo - cpp/apps/shaders_lab - cpp/functions/gfx/gl_framebuffer.cpp - cpp/functions/gfx/gl_framebuffer.h - cpp/functions/gfx/gl_framebuffer.md - cpp/functions/gfx/mesh_gpu.md - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
244 lines
9.2 KiB
C++
244 lines
9.2 KiB
C++
// 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 <cstdint>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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<uint8_t>& 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<uint8_t>& 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<uint8_t>& 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<uint8_t> 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<uint8_t> 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<uint8_t> 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<uint8_t> 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<uint8_t> 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());
|
|
}
|