10ac4c74db
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
242 lines
8.4 KiB
C++
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
|