#include "gfx/mesh_obj_load.h" #include #include #include #include #include #include #include namespace fn::gfx { namespace { // Parse one face vertex token "v", "v/t", "v//n", or "v/t/n". // Returns 0-based positive index for v_idx and n_idx (or -1 if not present). // .obj indices are 1-based; negative indices are relative-to-end. struct FaceVert { int v = -1; int n = -1; }; FaceVert parse_face_vert(const char* tok, int n_pos, int n_nrm) { FaceVert r; int v = 0, t = 0, n = 0; int slashes = 0; const char* p = tok; // crude split by '/' char buf[3][32]; int bi = 0; int bj = 0; buf[0][0] = buf[1][0] = buf[2][0] = '\0'; while (*p && *p != '\0' && *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n') { if (*p == '/') { buf[bi][bj] = '\0'; bi++; bj = 0; if (bi > 2) break; slashes++; } else if (bj < 31) { buf[bi][bj++] = *p; } p++; } if (bi <= 2) buf[bi][bj] = '\0'; if (buf[0][0]) v = std::atoi(buf[0]); if (slashes >= 1 && buf[1][0]) t = std::atoi(buf[1]); if (slashes == 2 && buf[2][0]) n = std::atoi(buf[2]); (void)t; // resolve 1-based / negative if (v > 0) r.v = v - 1; else if (v < 0) r.v = n_pos + v; if (n > 0) r.n = n - 1; else if (n < 0) r.n = n_nrm + n; return r; } 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]; } void sub3(const float a[3], const float b[3], float out[3]) { out[0] = a[0]-b[0]; out[1] = a[1]-b[1]; out[2] = a[2]-b[2]; } void normalize3(float v[3]) { float l = std::sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]); if (l > 0) { v[0] /= l; v[1] /= l; v[2] /= l; } } } // namespace Mesh mesh_obj_parse(const char* obj_text, size_t len) { Mesh m; if (!obj_text || len == 0) return m; // Raw arrays from the file. std::vector raw_pos; // stride 3 std::vector raw_nrm; // stride 3 // Per-output-vertex: dedup by (v_idx, n_idx) tuple. Map "v_n" -> out index. // For tiny meshes a linear scan of pairs suffices, but we use a vector + // simple hash map via std::string key (KISS, ~MB-scale meshes still ok). struct Key { int v; int n; }; std::vector out_keys; auto find_or_add = [&](int v_idx, int n_idx) -> uint32_t { // linear scan — fine for inspection meshes (<<100k unique verts). for (uint32_t i = 0; i < out_keys.size(); ++i) { if (out_keys[i].v == v_idx && out_keys[i].n == n_idx) return i; } out_keys.push_back({v_idx, n_idx}); return (uint32_t)(out_keys.size() - 1); }; // Iterate lines. const char* p = obj_text; const char* end = obj_text + len; // Stash face vertices for second pass (if normals are missing globally // we generate per face). struct FaceTri { FaceVert v[3]; }; std::vector faces; auto skip_ws = [](const char*& q, const char* qend) { while (q < qend && (*q == ' ' || *q == '\t')) q++; }; while (p < end) { // skip leading whitespace skip_ws(p, end); if (p >= end) break; if (*p == '#') { while (p < end && *p != '\n') p++; if (p < end) p++; continue; } if (*p == '\n' || *p == '\r') { p++; continue; } // tag const char* tag_start = p; while (p < end && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') p++; size_t tag_len = (size_t)(p - tag_start); auto eat_line = [&](std::string& out) { skip_ws(p, end); const char* line_start = p; while (p < end && *p != '\n' && *p != '\r') p++; out.assign(line_start, p); while (p < end && (*p == '\n' || *p == '\r')) p++; }; if (tag_len == 1 && tag_start[0] == 'v') { std::string body; eat_line(body); float x = 0, y = 0, z = 0; std::sscanf(body.c_str(), "%f %f %f", &x, &y, &z); raw_pos.push_back(x); raw_pos.push_back(y); raw_pos.push_back(z); } else if (tag_len == 2 && tag_start[0] == 'v' && tag_start[1] == 'n') { std::string body; eat_line(body); float x = 0, y = 0, z = 0; std::sscanf(body.c_str(), "%f %f %f", &x, &y, &z); raw_nrm.push_back(x); raw_nrm.push_back(y); raw_nrm.push_back(z); } else if (tag_len == 1 && tag_start[0] == 'f') { std::string body; eat_line(body); // Split body into space-delimited tokens. std::vector fv; const char* bp = body.c_str(); const char* be = bp + body.size(); int npos = (int)(raw_pos.size() / 3); int nnrm = (int)(raw_nrm.size() / 3); while (bp < be) { while (bp < be && (*bp == ' ' || *bp == '\t')) bp++; if (bp >= be) break; fv.push_back(parse_face_vert(bp, npos, nnrm)); while (bp < be && *bp != ' ' && *bp != '\t') bp++; } if (fv.size() == 3) { faces.push_back({fv[0], fv[1], fv[2]}); } else if (fv.size() == 4) { faces.push_back({fv[0], fv[1], fv[2]}); faces.push_back({fv[0], fv[2], fv[3]}); } // n-gons silently dropped; documented in .md } else { // unknown tag — skip to EOL while (p < end && *p != '\n') p++; if (p < end) p++; } } if (raw_pos.empty() || faces.empty()) return Mesh{}; // Build output vertices/normals/indices. // If any face vertex lacks a normal, we'll generate face normals. bool need_face_normals = raw_nrm.empty(); if (!need_face_normals) { for (auto& f : faces) { for (int k = 0; k < 3; ++k) { if (f.v[k].n < 0) { need_face_normals = true; break; } } if (need_face_normals) break; } } // First pass: build deduped vertex list with explicit normals (when present). // If we need face normals, we'll emit unique vertices per face (no dedup // across faces for normals; positions can still be shared but we just // duplicate to keep code simple — this is flat shading by design). if (need_face_normals) { m.positions.reserve(faces.size() * 3 * 3); m.normals.reserve(faces.size() * 3 * 3); m.indices.reserve(faces.size() * 3); uint32_t next = 0; for (auto& f : faces) { const float* p0 = &raw_pos[f.v[0].v * 3]; const float* p1 = &raw_pos[f.v[1].v * 3]; const float* p2 = &raw_pos[f.v[2].v * 3]; float e1[3], e2[3], n[3]; sub3(p1, p0, e1); sub3(p2, p0, e2); cross3(e1, e2, n); normalize3(n); for (int k = 0; k < 3; ++k) { const float* pp = &raw_pos[f.v[k].v * 3]; m.positions.push_back(pp[0]); m.positions.push_back(pp[1]); m.positions.push_back(pp[2]); m.normals.push_back(n[0]); m.normals.push_back(n[1]); m.normals.push_back(n[2]); m.indices.push_back(next++); } } } else { m.indices.reserve(faces.size() * 3); for (auto& f : faces) { for (int k = 0; k < 3; ++k) { uint32_t out = find_or_add(f.v[k].v, f.v[k].n); m.indices.push_back(out); } } m.positions.resize(out_keys.size() * 3); m.normals.resize(out_keys.size() * 3); for (size_t i = 0; i < out_keys.size(); ++i) { int vi = out_keys[i].v; int ni = out_keys[i].n; m.positions[i*3 + 0] = raw_pos[vi*3 + 0]; m.positions[i*3 + 1] = raw_pos[vi*3 + 1]; m.positions[i*3 + 2] = raw_pos[vi*3 + 2]; m.normals[i*3 + 0] = raw_nrm[ni*3 + 0]; m.normals[i*3 + 1] = raw_nrm[ni*3 + 1]; m.normals[i*3 + 2] = raw_nrm[ni*3 + 2]; } } return m; } Mesh mesh_obj_load(const char* path) { if (!path || !*path) return Mesh{}; std::ifstream f(path, std::ios::binary); if (!f) return Mesh{}; std::ostringstream ss; ss << f.rdbuf(); std::string s = ss.str(); return mesh_obj_parse(s.data(), s.size()); } } // namespace fn::gfx