Files
fn_registry/cpp/functions/gfx/mesh_obj_load.cpp
T
egutierrez 10ac4c74db feat(gfx): mesh_obj_load — minimal Wavefront .obj parser
mesh_obj_parse (pure) + mesh_obj_load (impure file helper).
Soporta v / vn / f (tris y quads). Genera normales per-face si
faltan (flat shading). Quads se parten en 2 tris; n-gons (>4) se
descartan silenciosamente. Indices 1-based positivos y negativos.

issue 0029
2026-04-25 21:51:05 +02:00

242 lines
8.4 KiB
C++

#include "gfx/mesh_obj_load.h"
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <sstream>
#include <string>
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<float> raw_pos; // stride 3
std::vector<float> 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<Key> 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<FaceTri> 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<FaceVert> 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