#include "gfx/gltf_load_mesh.h" #include #include #include #include #include #include #include // nlohmann/json vendored #include "nlohmann/json.hpp" namespace fn::gfx { // --------------------------------------------------------------------------- // Thread-local last error // --------------------------------------------------------------------------- static thread_local char s_last_error[512] = ""; static void set_error(const char* msg) { std::strncpy(s_last_error, msg, sizeof(s_last_error) - 1); s_last_error[sizeof(s_last_error) - 1] = '\0'; } const char* gltf_load_last_error() { return s_last_error; } // --------------------------------------------------------------------------- // GLB binary format constants (spec glTF 2.0) // --------------------------------------------------------------------------- static constexpr uint32_t GLB_MAGIC = 0x46546C67u; // "glTF" static constexpr uint32_t GLB_VERSION = 2u; static constexpr uint32_t CHUNK_JSON = 0x4E4F534Au; // "JSON" static constexpr uint32_t CHUNK_BIN = 0x004E4942u; // "BIN\0" // glTF accessor componentType static constexpr int CT_UNSIGNED_BYTE = 5121; static constexpr int CT_UNSIGNED_SHORT = 5123; static constexpr int CT_UNSIGNED_INT = 5125; static constexpr int CT_FLOAT = 5126; // --------------------------------------------------------------------------- // Math helpers // --------------------------------------------------------------------------- static void cross3(const float a[3], const float b[3], float out[3]) { out[0] = a[1]*b[2] - a[2]*b[1]; out[1] = a[2]*b[0] - a[0]*b[2]; out[2] = a[0]*b[1] - a[1]*b[0]; } static float dot3(const float a[3], const float b[3]) { return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]; } static float len3(const float a[3]) { return std::sqrt(dot3(a, a)); } // Multiply 4x4 column-major matrix by vec3 (point, w=1) static void mat4_mul_point(const float m[16], const float p[3], float out[3]) { out[0] = m[0]*p[0] + m[4]*p[1] + m[8] *p[2] + m[12]; out[1] = m[1]*p[0] + m[5]*p[1] + m[9] *p[2] + m[13]; out[2] = m[2]*p[0] + m[6]*p[1] + m[10]*p[2] + m[14]; } // Multiply 4x4 column-major matrix by vec3 (direction, w=0 — for normals use // inverse-transpose, which here is computed at call site) static void mat4_mul_dir(const float m[16], const float v[3], float out[3]) { out[0] = m[0]*v[0] + m[4]*v[1] + m[8] *v[2]; out[1] = m[1]*v[0] + m[5]*v[1] + m[9] *v[2]; out[2] = m[2]*v[0] + m[6]*v[1] + m[10]*v[2]; } // 3x3 inverse-transpose (for normal transform) extracted from upper-left of 4x4. // Returns false if matrix is singular (scale 0). static bool compute_normal_matrix(const float m[16], float out[9]) { // Extract upper-left 3x3 (column-major from 4x4) float a00=m[0], a10=m[1], a20=m[2]; float a01=m[4], a11=m[5], a21=m[6]; float a02=m[8], a12=m[9], a22=m[10]; float det = a00*(a11*a22 - a21*a12) - a01*(a10*a22 - a20*a12) + a02*(a10*a21 - a20*a11); if (std::fabs(det) < 1e-12f) return false; float inv = 1.0f / det; // Inverse of 3x3, then transpose → inverse-transpose columns become rows out[0] = inv * (a11*a22 - a21*a12); out[1] = inv * (a21*a02 - a01*a22); out[2] = inv * (a01*a12 - a11*a02); out[3] = inv * (a20*a12 - a10*a22); out[4] = inv * (a00*a22 - a20*a02); out[5] = inv * (a10*a02 - a00*a12); out[6] = inv * (a10*a21 - a20*a11); out[7] = inv * (a20*a01 - a00*a21); out[8] = inv * (a00*a11 - a10*a01); return true; } static void nrm3x3_mul(const float m[9], const float v[3], float out[3]) { out[0] = m[0]*v[0] + m[3]*v[1] + m[6]*v[2]; out[1] = m[1]*v[0] + m[4]*v[1] + m[7]*v[2]; out[2] = m[2]*v[0] + m[5]*v[1] + m[8]*v[2]; } // TRS → column-major 4x4 matrix // translation=[tx,ty,tz], rotation quaternion=[qx,qy,qz,qw], scale=[sx,sy,sz] static void trs_to_mat4(const float t[3], const float q[4], const float s[3], float out[16]) { float qx=q[0], qy=q[1], qz=q[2], qw=q[3]; float x2=qx+qx, y2=qy+qy, z2=qz+qz; float xx=qx*x2, xy=qx*y2, xz=qx*z2; float yy=qy*y2, yz=qy*z2, zz=qz*z2; float wx=qw*x2, wy=qw*y2, wz=qw*z2; out[0] = (1-(yy+zz))*s[0]; out[1] = (xy+wz)*s[0]; out[2] = (xz-wy)*s[0]; out[3] = 0; out[4] = (xy-wz)*s[1]; out[5] = (1-(xx+zz))*s[1]; out[6] = (yz+wx)*s[1]; out[7] = 0; out[8] = (xz+wy)*s[2]; out[9] = (yz-wx)*s[2]; out[10] = (1-(xx+yy))*s[2]; out[11] = 0; out[12] = t[0]; out[13] = t[1]; out[14] = t[2]; out[15] = 1; } // --------------------------------------------------------------------------- // Accessor reading helpers // --------------------------------------------------------------------------- struct BufView { const uint8_t* base = nullptr; size_t total = 0; }; // Read a single element of 'count' components from accessor at element index 'idx'. // component_type: CT_FLOAT, CT_UNSIGNED_BYTE, CT_UNSIGNED_SHORT, CT_UNSIGNED_INT // components_per_element: 1 (SCALAR) or 3 (VEC3) etc. // Returns false if out-of-bounds. static bool read_float_vec(const BufView& bin, int component_type, int components_per_element, size_t byte_offset, // accessor.byteOffset + bufferView.byteOffset int byte_stride, // bufferView.byteStride (0 = tightly packed) size_t idx, float out[4]) { size_t comp_size = 0; switch (component_type) { case CT_UNSIGNED_BYTE: comp_size = 1; break; case CT_UNSIGNED_SHORT: comp_size = 2; break; case CT_UNSIGNED_INT: comp_size = 4; break; case CT_FLOAT: comp_size = 4; break; default: return false; } size_t element_size = comp_size * (size_t)components_per_element; size_t stride = (byte_stride > 0) ? (size_t)byte_stride : element_size; size_t off = byte_offset + idx * stride; if (off + element_size > bin.total) return false; const uint8_t* p = bin.base + off; for (int c = 0; c < components_per_element; ++c) { const uint8_t* cp = p + (size_t)c * comp_size; switch (component_type) { case CT_UNSIGNED_BYTE: out[c] = (float)*cp; break; case CT_UNSIGNED_SHORT: { uint16_t v; std::memcpy(&v, cp, 2); out[c] = (float)v; break; } case CT_UNSIGNED_INT: { uint32_t v; std::memcpy(&v, cp, 4); out[c] = (float)v; break; } case CT_FLOAT: { float v; std::memcpy(&v, cp, 4); out[c] = v; break; } default: return false; } } return true; } static bool read_index(const BufView& bin, int component_type, size_t byte_offset, size_t idx, uint32_t& out) { float v[1] = {}; if (!read_float_vec(bin, component_type, 1, byte_offset, 0, idx, v)) return false; out = static_cast(v[0]); return true; } // --------------------------------------------------------------------------- // Core GLB parser // --------------------------------------------------------------------------- static Mesh parse_glb(const uint8_t* data, size_t size) { s_last_error[0] = '\0'; // --- 1. Validate header (12 bytes) --- if (size < 12) { set_error("file too small for GLB header"); return {}; } uint32_t magic, version, total_len; std::memcpy(&magic, data, 4); std::memcpy(&version, data + 4, 4); std::memcpy(&total_len, data + 8, 4); if (magic != GLB_MAGIC) { set_error("not a GLB file (bad magic)"); return {}; } if (version != GLB_VERSION){ set_error("unsupported GLB version (expected 2)"); return {}; } if (total_len > size) { set_error("GLB total_length > buffer size"); return {}; } // --- 2. Walk chunks --- const uint8_t* json_data = nullptr; size_t json_len = 0; const uint8_t* bin_data = nullptr; size_t bin_len = 0; size_t pos = 12; while (pos + 8 <= total_len) { uint32_t chunk_len, chunk_type; std::memcpy(&chunk_len, data + pos, 4); std::memcpy(&chunk_type, data + pos + 4, 4); pos += 8; if (pos + chunk_len > total_len) { set_error("chunk extends past file end"); return {}; } if (chunk_type == CHUNK_JSON) { json_data = data + pos; json_len = chunk_len; } else if (chunk_type == CHUNK_BIN) { bin_data = data + pos; bin_len = chunk_len; } pos += chunk_len; } if (!json_data) { set_error("no JSON chunk found"); return {}; } // --- 3. Parse JSON --- nlohmann::json j; try { j = nlohmann::json::parse(json_data, json_data + json_len); } catch (const std::exception& e) { std::snprintf(s_last_error, sizeof(s_last_error), "JSON parse error: %s", e.what()); return {}; } // --- 4. Find first mesh / first primitive --- if (!j.contains("meshes") || j["meshes"].empty()) { set_error("no meshes in glTF"); return {}; } auto& prim = j["meshes"][0]["primitives"][0]; auto& attrs = prim["attributes"]; if (!attrs.contains("POSITION")) { set_error("primitive has no POSITION attribute"); return {}; } auto& accessors = j["accessors"]; auto& bufferViews = j["bufferViews"]; BufView bin_view { bin_data, bin_len }; // Helper: resolve accessor index → (byte_offset, byte_stride, component_type, count, components_per_elem) struct AccInfo { size_t byte_offset; int byte_stride; int comp_type; size_t count; int ncomp; }; auto resolve_accessor = [&](int acc_idx, AccInfo& out) -> bool { if (acc_idx < 0 || acc_idx >= (int)accessors.size()) return false; auto& acc = accessors[acc_idx]; int bv_idx = acc.value("bufferView", -1); size_t acc_offset = acc.value("byteOffset", 0); out.comp_type = acc.value("componentType", 0); out.count = acc.value("count", 0u); std::string type_str = acc.value("type", "SCALAR"); out.ncomp = 1; if (type_str == "VEC2") out.ncomp = 2; else if (type_str == "VEC3") out.ncomp = 3; else if (type_str == "VEC4") out.ncomp = 4; if (bv_idx >= 0 && bv_idx < (int)bufferViews.size()) { auto& bv = bufferViews[bv_idx]; size_t bv_offset = bv.value("byteOffset", 0u); out.byte_stride = bv.value("byteStride", 0); out.byte_offset = acc_offset + bv_offset; } else { out.byte_offset = acc_offset; out.byte_stride = 0; } return out.count > 0 && out.comp_type != 0; }; // --- 5. Read POSITION --- AccInfo pos_info{}; if (!resolve_accessor(attrs["POSITION"].get(), pos_info)) { set_error("failed to resolve POSITION accessor"); return {}; } if (pos_info.ncomp != 3 || pos_info.comp_type != CT_FLOAT) { set_error("POSITION must be float vec3"); return {}; } if (!bin_data && pos_info.count > 0) { set_error("POSITION accessor requires BIN chunk, which is missing"); return {}; } size_t nv = pos_info.count; std::vector positions(nv * 3); for (size_t i = 0; i < nv; ++i) { float v[4]{}; if (!read_float_vec(bin_view, CT_FLOAT, 3, pos_info.byte_offset, pos_info.byte_stride, i, v)) { set_error("out-of-bounds read in POSITION"); return {}; } positions[i*3+0] = v[0]; positions[i*3+1] = v[1]; positions[i*3+2] = v[2]; } // --- 6. Read NORMAL (optional) --- std::vector normals; bool has_normals = false; if (attrs.contains("NORMAL")) { AccInfo nrm_info{}; if (resolve_accessor(attrs["NORMAL"].get(), nrm_info) && nrm_info.ncomp == 3 && nrm_info.comp_type == CT_FLOAT && nrm_info.count == nv) { normals.resize(nv * 3); for (size_t i = 0; i < nv; ++i) { float v[4]{}; if (!read_float_vec(bin_view, CT_FLOAT, 3, nrm_info.byte_offset, nrm_info.byte_stride, i, v)) { set_error("out-of-bounds read in NORMAL"); return {}; } normals[i*3+0] = v[0]; normals[i*3+1] = v[1]; normals[i*3+2] = v[2]; } has_normals = true; } } // --- 7. Read indices --- std::vector indices; if (prim.contains("indices") && !prim["indices"].is_null()) { AccInfo idx_info{}; int idx_acc = prim["indices"].get(); if (!resolve_accessor(idx_acc, idx_info)) { set_error("failed to resolve indices accessor"); return {}; } if (!bin_data && idx_info.count > 0) { set_error("indices accessor requires BIN chunk, which is missing"); return {}; } indices.resize(idx_info.count); for (size_t i = 0; i < idx_info.count; ++i) { if (!read_index(bin_view, idx_info.comp_type, idx_info.byte_offset, i, indices[i])) { set_error("out-of-bounds read in indices"); return {}; } } } else { // No indices: interpret as sequential triangle list indices.resize(nv); for (size_t i = 0; i < nv; ++i) indices[i] = (uint32_t)i; } // --- 8. Generate normals if missing (smooth, area-weighted) --- if (!has_normals) { normals.assign(nv * 3, 0.0f); size_t ntri = indices.size() / 3; for (size_t t = 0; t < ntri; ++t) { uint32_t i0 = indices[t*3+0]; uint32_t i1 = indices[t*3+1]; uint32_t i2 = indices[t*3+2]; if (i0 >= nv || i1 >= nv || i2 >= nv) continue; float e1[3] = { positions[i1*3+0] - positions[i0*3+0], positions[i1*3+1] - positions[i0*3+1], positions[i1*3+2] - positions[i0*3+2] }; float e2[3] = { positions[i2*3+0] - positions[i0*3+0], positions[i2*3+1] - positions[i0*3+1], positions[i2*3+2] - positions[i0*3+2] }; float face_n[3]; cross3(e1, e2, face_n); // face_n magnitude = 2 * area → area weighting automatic for (uint32_t vi : {i0, i1, i2}) { normals[vi*3+0] += face_n[0]; normals[vi*3+1] += face_n[1]; normals[vi*3+2] += face_n[2]; } } // Normalize per-vertex for (size_t i = 0; i < nv; ++i) { float* n = &normals[i*3]; float l = len3(n); if (l > 1e-8f) { n[0]/=l; n[1]/=l; n[2]/=l; } else { n[0]=0; n[1]=1; n[2]=0; } // degenerate fallback } } // --- 9. Apply node transform (first node referencing this mesh) --- bool applied_transform = false; if (j.contains("nodes") && !j["nodes"].empty()) { auto& nodes = j["nodes"]; for (size_t ni = 0; ni < nodes.size() && !applied_transform; ++ni) { auto& node = nodes[ni]; if (!node.contains("mesh") || node["mesh"].get() != 0) continue; float mat[16] = { 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 }; // identity column-major if (node.contains("matrix") && node["matrix"].is_array() && node["matrix"].size() == 16) { for (int k = 0; k < 16; ++k) mat[k] = node["matrix"][k].get(); applied_transform = true; } else { float t[3] = {0,0,0}, q[4] = {0,0,0,1}, s[3] = {1,1,1}; bool has_trs = false; if (node.contains("translation") && node["translation"].size() == 3) { for (int k = 0; k < 3; ++k) t[k] = node["translation"][k].get(); has_trs = true; } if (node.contains("rotation") && node["rotation"].size() == 4) { for (int k = 0; k < 4; ++k) q[k] = node["rotation"][k].get(); has_trs = true; } if (node.contains("scale") && node["scale"].size() == 3) { for (int k = 0; k < 3; ++k) s[k] = node["scale"][k].get(); has_trs = true; } if (has_trs) { trs_to_mat4(t, q, s, mat); applied_transform = true; } } if (applied_transform) { // Check if matrix is non-trivially identity const float id[16] = {1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1}; bool is_identity = true; for (int k = 0; k < 16; ++k) if (std::fabs(mat[k] - id[k]) > 1e-6f) { is_identity = false; break; } if (!is_identity) { float nrm_mat[9]; bool has_nrm_mat = compute_normal_matrix(mat, nrm_mat); for (size_t vi = 0; vi < nv; ++vi) { float p[3] = { positions[vi*3+0], positions[vi*3+1], positions[vi*3+2] }; float tp[3]; mat4_mul_point(mat, p, tp); positions[vi*3+0] = tp[0]; positions[vi*3+1] = tp[1]; positions[vi*3+2] = tp[2]; if (has_nrm_mat) { float n[3] = { normals[vi*3+0], normals[vi*3+1], normals[vi*3+2] }; float tn[3]; nrm3x3_mul(nrm_mat, n, tn); float l = len3(tn); if (l > 1e-8f) { tn[0]/=l; tn[1]/=l; tn[2]/=l; } normals[vi*3+0] = tn[0]; normals[vi*3+1] = tn[1]; normals[vi*3+2] = tn[2]; } } } } } } Mesh m; m.positions = std::move(positions); m.normals = std::move(normals); m.indices = std::move(indices); return m; } // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- Mesh gltf_load_mesh_from_memory(const unsigned char* data, size_t size) { return parse_glb(reinterpret_cast(data), size); } Mesh gltf_load_mesh_from_file(const char* path) { std::ifstream f(path, std::ios::binary | std::ios::ate); if (!f) { std::snprintf(s_last_error, sizeof(s_last_error), "cannot open file: %s", path); return {}; } auto file_size = f.tellg(); if (file_size <= 0) { set_error("file is empty"); return {}; } f.seekg(0); std::vector buf((size_t)file_size); if (!f.read(reinterpret_cast(buf.data()), file_size)) { set_error("file read failed"); return {}; } return parse_glb(buf.data(), buf.size()); } } // namespace fn::gfx