chore: auto-commit (43 archivos)
- .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>
This commit is contained in:
@@ -14,9 +14,17 @@ static void create_tex(Framebuffer& f) {
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
static void create_depth_rbo(Framebuffer& f) {
|
||||
glGenRenderbuffers(1, &f.depth_rbo);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, f.depth_rbo);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, f.width, f.height);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||
}
|
||||
|
||||
void fb_init(Framebuffer& f) {
|
||||
f.width = 1;
|
||||
f.height = 1;
|
||||
f.width = 1;
|
||||
f.height = 1;
|
||||
f.has_depth = false;
|
||||
create_tex(f);
|
||||
glGenFramebuffers(1, &f.fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, f.fbo);
|
||||
@@ -24,23 +32,50 @@ void fb_init(Framebuffer& f) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
void fb_init_depth(Framebuffer& f) {
|
||||
f.width = 1;
|
||||
f.height = 1;
|
||||
f.has_depth = true;
|
||||
create_tex(f);
|
||||
create_depth_rbo(f);
|
||||
glGenFramebuffers(1, &f.fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, f.fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, f.tex, 0);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, f.depth_rbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
void fb_resize(Framebuffer& f, int w, int h) {
|
||||
if (w == f.width && h == f.height) return;
|
||||
f.width = w;
|
||||
f.width = w;
|
||||
f.height = h;
|
||||
|
||||
// Recreate color texture.
|
||||
if (f.tex) glDeleteTextures(1, &f.tex);
|
||||
f.tex = 0;
|
||||
create_tex(f);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, f.fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, f.tex, 0);
|
||||
|
||||
// Resize depth renderbuffer in-place (no need to recreate).
|
||||
if (f.has_depth && f.depth_rbo) {
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, f.depth_rbo);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, f.width, f.height);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||
// Re-attach in case it was lost (should be stable across storage resize, but be safe).
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, f.depth_rbo);
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
void fb_destroy(Framebuffer& f) {
|
||||
if (f.fbo) { glDeleteFramebuffers(1, &f.fbo); f.fbo = 0; }
|
||||
if (f.tex) { glDeleteTextures(1, &f.tex); f.tex = 0; }
|
||||
f.width = 0;
|
||||
f.height = 0;
|
||||
if (f.fbo) { glDeleteFramebuffers(1, &f.fbo); f.fbo = 0; }
|
||||
if (f.tex) { glDeleteTextures(1, &f.tex); f.tex = 0; }
|
||||
if (f.depth_rbo) { glDeleteRenderbuffers(1, &f.depth_rbo); f.depth_rbo = 0; }
|
||||
f.width = 0;
|
||||
f.height = 0;
|
||||
f.has_depth = false;
|
||||
}
|
||||
|
||||
} // namespace fn::gfx
|
||||
|
||||
@@ -3,14 +3,17 @@
|
||||
namespace fn::gfx {
|
||||
|
||||
struct Framebuffer {
|
||||
unsigned int fbo = 0;
|
||||
unsigned int tex = 0; // GL_RGBA8, clamp, linear
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
unsigned int fbo = 0;
|
||||
unsigned int tex = 0; // GL_RGBA8 color
|
||||
unsigned int depth_rbo = 0; // GL_DEPTH_COMPONENT24 renderbuffer, 0 si sin depth
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
bool has_depth = false;
|
||||
};
|
||||
|
||||
void fb_init(Framebuffer& f); // crea fbo+tex 1x1 iniciales
|
||||
void fb_resize(Framebuffer& f, int w, int h); // no-op si w,h iguales
|
||||
void fb_destroy(Framebuffer& f);
|
||||
void fb_init(Framebuffer& f); // crea fbo+tex 1x1 (color-only, retro-compat)
|
||||
void fb_init_depth(Framebuffer& f); // crea fbo+tex+depth_rbo 1x1
|
||||
void fb_resize(Framebuffer& f, int w, int h); // redimensiona color y depth (si has_depth); no-op si iguales
|
||||
void fb_destroy(Framebuffer& f); // libera fbo, tex y depth_rbo si existen
|
||||
|
||||
} // namespace fn::gfx
|
||||
|
||||
@@ -3,11 +3,11 @@ name: gl_framebuffer
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: gfx
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
purity: impure
|
||||
signature: "void fb_init(Framebuffer& f); void fb_resize(Framebuffer& f, int w, int h); void fb_destroy(Framebuffer& f)"
|
||||
description: "CRUD de un framebuffer OpenGL (FBO + textura RGBA8). fb_resize es no-op si las dimensiones no cambian. Listo para uso con ImGui::Image."
|
||||
tags: [opengl, framebuffer, fbo, texture, gfx, offscreen]
|
||||
signature: "void fb_init(Framebuffer& f); void fb_init_depth(Framebuffer& f); void fb_resize(Framebuffer& f, int w, int h); void fb_destroy(Framebuffer& f)"
|
||||
description: "CRUD de un framebuffer OpenGL (FBO + textura RGBA8, opcionalmente con depth renderbuffer GL_DEPTH_COMPONENT24). fb_init es color-only (retro-compat); fb_init_depth añade depth. fb_resize redimensiona color y depth si has_depth. Listo para uso con ImGui::Image."
|
||||
tags: [opengl, framebuffer, fbo, texture, gfx, offscreen, depth, cpp-dashboard-viz]
|
||||
uses_functions: ["gl_loader_cpp_gfx"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
@@ -21,23 +21,23 @@ file_path: "cpp/functions/gfx/gl_framebuffer.cpp"
|
||||
framework: opengl
|
||||
params:
|
||||
- name: f
|
||||
desc: "Struct Framebuffer con campos fbo, tex (GL ids), width, height. Inicializar a {0} antes de fb_init."
|
||||
desc: "Struct Framebuffer con campos fbo, tex, depth_rbo (GL ids), width, height, has_depth. Inicializar a {0} antes de fb_init/fb_init_depth."
|
||||
- name: w
|
||||
desc: "Ancho deseado en pixels (fb_resize)"
|
||||
- name: h
|
||||
desc: "Alto deseado en pixels (fb_resize)"
|
||||
output: "Modifica f in-place. Después de fb_init, f.fbo y f.tex son IDs GL válidos. fb_destroy pone todos los campos a 0."
|
||||
output: "Modifica f in-place. Después de fb_init/fb_init_depth, f.fbo y f.tex son IDs GL válidos. Si fb_init_depth: f.depth_rbo != 0 y f.has_depth == true. fb_destroy pone todos los campos a 0."
|
||||
---
|
||||
|
||||
# gl_framebuffer
|
||||
|
||||
FBO con textura color RGBA8 (GL_CLAMP_TO_EDGE, GL_LINEAR). Diseñado para renderizado offscreen y posterior display via `ImGui::Image`.
|
||||
FBO con textura color RGBA8 (GL_CLAMP_TO_EDGE, GL_LINEAR). Opcionalmente con depth renderbuffer GL_DEPTH_COMPONENT24. Diseñado para renderizado offscreen y posterior display via `ImGui::Image`.
|
||||
|
||||
## Ciclo de vida
|
||||
## Ciclo de vida — color-only (retro-compat)
|
||||
|
||||
```cpp
|
||||
fn::gfx::Framebuffer fb{};
|
||||
fn::gfx::fb_init(fb); // fbo + tex 1x1
|
||||
fn::gfx::fb_init(fb); // fbo + tex 1x1, has_depth=false
|
||||
|
||||
// En el render loop:
|
||||
fn::gfx::fb_resize(fb, w, h); // no-op si mismas dimensiones
|
||||
@@ -46,6 +46,23 @@ fn::gfx::fb_resize(fb, w, h); // no-op si mismas dimensiones
|
||||
fn::gfx::fb_destroy(fb);
|
||||
```
|
||||
|
||||
## Ciclo de vida — con depth renderbuffer
|
||||
|
||||
```cpp
|
||||
fn::gfx::Framebuffer fb{};
|
||||
fn::gfx::fb_init_depth(fb); // fbo + tex 1x1 + depth_rbo 1x1, has_depth=true
|
||||
|
||||
// En el render loop (antes de glDrawElements):
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LESS);
|
||||
|
||||
fn::gfx::fb_resize(fb, w, h); // redimensiona color Y depth_rbo
|
||||
|
||||
// Al destruir:
|
||||
fn::gfx::fb_destroy(fb); // libera fbo, tex y depth_rbo
|
||||
```
|
||||
|
||||
## Uso con ImGui::Image
|
||||
|
||||
```cpp
|
||||
@@ -59,4 +76,10 @@ ImGui::Image(
|
||||
|
||||
## Notas
|
||||
|
||||
`fb_resize` recrea solo la textura (no el FBO) cuando las dimensiones cambian, reattachando la nueva textura al FBO existente. Esto minimiza el overhead de resize.
|
||||
`fb_resize` recrea solo la textura (no el FBO) cuando las dimensiones cambian, reattachando la nueva textura al FBO existente. Para el depth renderbuffer, llama `glRenderbufferStorage` in-place (sin recrear el RBO). Esto minimiza el overhead de resize.
|
||||
|
||||
`fb_init` (sin depth) se mantiene idéntico al comportamiento pre-v1.1.0 — no rompe consumidores existentes (`shader_canvas`, `graph_renderer`).
|
||||
|
||||
## Capability growth log
|
||||
|
||||
v1.1.0 (2026-05-28) — fb_init_depth opcional + depth en fb_resize/fb_destroy
|
||||
|
||||
@@ -0,0 +1,510 @@
|
||||
#include "gfx/gltf_load_mesh.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// 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<uint32_t>(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<int>(), 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<float> 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<float> normals;
|
||||
bool has_normals = false;
|
||||
if (attrs.contains("NORMAL")) {
|
||||
AccInfo nrm_info{};
|
||||
if (resolve_accessor(attrs["NORMAL"].get<int>(), 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<uint32_t> indices;
|
||||
if (prim.contains("indices") && !prim["indices"].is_null()) {
|
||||
AccInfo idx_info{};
|
||||
int idx_acc = prim["indices"].get<int>();
|
||||
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<int>() != 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<float>();
|
||||
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<float>();
|
||||
has_trs = true;
|
||||
}
|
||||
if (node.contains("rotation") && node["rotation"].size() == 4) {
|
||||
for (int k = 0; k < 4; ++k) q[k] = node["rotation"][k].get<float>();
|
||||
has_trs = true;
|
||||
}
|
||||
if (node.contains("scale") && node["scale"].size() == 3) {
|
||||
for (int k = 0; k < 3; ++k) s[k] = node["scale"][k].get<float>();
|
||||
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<const uint8_t*>(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<uint8_t> buf((size_t)file_size);
|
||||
if (!f.read(reinterpret_cast<char*>(buf.data()), file_size)) {
|
||||
set_error("file read failed");
|
||||
return {};
|
||||
}
|
||||
return parse_glb(buf.data(), buf.size());
|
||||
}
|
||||
|
||||
} // namespace fn::gfx
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include "gfx/mesh_obj_load.h" // fn::gfx::Mesh
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace fn::gfx {
|
||||
|
||||
// Carga el primer mesh (primera primitive del primer mesh) de un archivo GLB 2.0.
|
||||
//
|
||||
// Soporta:
|
||||
// - POSITION (vec3 float, obligatorio)
|
||||
// - NORMAL (vec3 float, opcional — si falta se generan normales smooth
|
||||
// area-weighted promediando las normales de cara de cada vertice)
|
||||
// - indices (ubyte/ushort/uint, escalares) — sin indices se interpreta como
|
||||
// lista de triangulos directa.
|
||||
//
|
||||
// Node transform: si el primer nodo que referencia el mesh tiene matrix o TRS,
|
||||
// se aplica a posiciones y normales (normales se transforman con la inversa transpuesta).
|
||||
//
|
||||
// Limitaciones (documentadas):
|
||||
// - Solo GLB (binario). .gltf+.bin separado y data-URIs base64 no soportados.
|
||||
// - Solo el primer mesh / primera primitive.
|
||||
// - Sin texturas ni materiales (mesh viewer usa color uniforme).
|
||||
// - Asume buffer 0 embebido en el chunk BIN.
|
||||
//
|
||||
// Retorna Mesh vacio (positions.empty()) si el parse falla.
|
||||
// El detalle del error esta disponible via gltf_load_last_error().
|
||||
Mesh gltf_load_mesh_from_file(const char* path);
|
||||
|
||||
// Variante pura (salvo el buffer): parsea GLB desde un bloque de memoria.
|
||||
// 'data' debe vivir al menos mientras dure la llamada.
|
||||
// Retorna Mesh vacio en fallo; gltf_load_last_error() da el detalle.
|
||||
Mesh gltf_load_mesh_from_memory(const unsigned char* data, size_t size);
|
||||
|
||||
// Descripcion del ultimo error de gltf_load_mesh_from_file /
|
||||
// gltf_load_mesh_from_memory. Valida hasta la siguiente llamada a cualquiera
|
||||
// de las dos funciones. Nunca retorna nullptr (puede ser "").
|
||||
const char* gltf_load_last_error();
|
||||
|
||||
} // namespace fn::gfx
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
name: gltf_load_mesh
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: gfx
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "Mesh gltf_load_mesh_from_file(const char* path); Mesh gltf_load_mesh_from_memory(const unsigned char* data, size_t size); const char* gltf_load_last_error()"
|
||||
description: "Parser GLB 2.0 (glTF binario): carga el primer mesh/primitive a CPU como fn::gfx::Mesh. Soporta POSITION+NORMAL (vec3 float), indices ubyte/ushort/uint, node transform TRS/matrix. Genera normales smooth area-weighted si faltan. Sin dependencias externas — BIN chunk + nlohmann JSON vendored."
|
||||
tags: [mesh, gltf, glb, 3d, loader, geometry, gfx, mesh-3d]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [gfx/mesh_obj_load.h, nlohmann/json.hpp, fstream, cstring, cmath]
|
||||
tested: true
|
||||
tests:
|
||||
- "invalid magic -> empty Mesh + last_error set"
|
||||
- "too-small buffer -> empty Mesh + last_error set"
|
||||
- "triangle without NORMAL -> normals generated, correct count"
|
||||
- "quad (2 triangles) -> positions.size()==12, indices.size()==6"
|
||||
- "explicit normals -> passed through unchanged"
|
||||
- "nonexistent file -> empty Mesh + last_error set"
|
||||
test_file_path: "cpp/tests/test_gltf_load_mesh.cpp"
|
||||
file_path: "cpp/functions/gfx/gltf_load_mesh.cpp"
|
||||
framework: opengl
|
||||
params:
|
||||
- name: path
|
||||
desc: "Ruta al archivo .glb. Solo GLB binario — .gltf+.bin separado y data-URI base64 no soportados."
|
||||
- name: data
|
||||
desc: "Puntero al buffer GLB en memoria. Debe vivir mientras dure la llamada."
|
||||
- name: size
|
||||
desc: "Longitud del buffer en bytes."
|
||||
output: "fn::gfx::Mesh con positions/normals (stride 3, mismo length) y indices uint32 (tri-list). Mesh vacio (positions.empty()==true) si parse falla. gltf_load_last_error() devuelve descripcion del error."
|
||||
notes: |
|
||||
Usa fn::gfx::Mesh de mesh_obj_load.h — mismo struct que consume mesh_gpu_upload().
|
||||
nlohmann vendored en cpp/vendor/nlohmann/json.hpp.
|
||||
El parser no aloca heap mas alla del Mesh de salida + JSON temporal.
|
||||
gltf_load_last_error() usa thread_local — seguro en multihilo siempre que
|
||||
cada hilo llame sus propias funciones.
|
||||
---
|
||||
|
||||
# gltf_load_mesh
|
||||
|
||||
Loader GLB 2.0 minimal para el registry. Parsea el contenedor GLB binario a mano
|
||||
(header 12 bytes + chunks JSON + BIN) usando nlohmann para el JSON. KISS: sin
|
||||
tinygltf ni dependencias extra.
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```cpp
|
||||
// Cargar .glb generado por TripoSR/trimesh y subir a GPU:
|
||||
#include "gfx/gltf_load_mesh.h"
|
||||
#include "gfx/mesh_gpu.h"
|
||||
|
||||
auto cpu = fn::gfx::gltf_load_mesh_from_file("model.glb");
|
||||
if (cpu.positions.empty()) {
|
||||
fprintf(stderr, "gltf load failed: %s\n", fn::gfx::gltf_load_last_error());
|
||||
return;
|
||||
}
|
||||
|
||||
// Subir a GPU (requiere contexto GL activo):
|
||||
auto gpu = fn::gfx::mesh_gpu_upload(cpu);
|
||||
if (!gpu.ok()) { /* fallo de upload GL */ return; }
|
||||
|
||||
glUseProgram(prog);
|
||||
glBindVertexArray(gpu.vao);
|
||||
glDrawElements(GL_TRIANGLES, gpu.index_count, GL_UNSIGNED_INT, 0);
|
||||
glBindVertexArray(0);
|
||||
|
||||
fn::gfx::mesh_gpu_destroy(gpu);
|
||||
```
|
||||
|
||||
```cpp
|
||||
// Desde memoria (ej. respuesta HTTP o embedding):
|
||||
std::vector<unsigned char> glb_buf = download_glb(...);
|
||||
auto cpu = fn::gfx::gltf_load_mesh_from_memory(glb_buf.data(), glb_buf.size());
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando recibes un `.glb` (binario glTF 2.0) de un backend Python (TripoSR,
|
||||
trimesh, open3d) y necesitas renderizarlo en una app ImGui via `mesh_gpu_upload`.
|
||||
Tambien util para inspeccionar geometria en CPU sin subir a GPU.
|
||||
|
||||
## Limitaciones
|
||||
|
||||
- **Solo GLB binario**. `.gltf + .bin` separado: no soportado. Data URIs base64: no soportados.
|
||||
- **Primer mesh, primera primitive**. Archivos con multiples meshes o materiales: solo se carga el primero.
|
||||
- **Sin texturas ni materiales**. El Mesh solo contiene geometria (posicion + normal). El shader del viewer usa color uniforme.
|
||||
- **Buffer unico embebido** (chunk BIN). Referencias a buffers externos: no soportadas.
|
||||
- **Modo solo triangulos** (`"mode": 4`, default). Puntos, lineas, triangle-strip: no soportados.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- `gltf_load_last_error()` es `thread_local`. Si usas multihilo, cada hilo tiene su propio error buffer — no compartas el puntero entre hilos.
|
||||
- El puntero que devuelve `gltf_load_last_error()` se sobreescribe en la siguiente llamada a `gltf_load_mesh_from_*`. Copia el string si lo necesitas despues.
|
||||
- Un `Mesh` retornado con `positions.empty() == true` es la senal de fallo — **no** lanzamos excepciones.
|
||||
- Para archivos grandes (>50 MB) la lectura es un `std::vector<uint8_t>` completo en memoria. Para streaming, usa `gltf_load_mesh_from_memory` con tu propio buffer.
|
||||
- El parser no valida que `indices` sean menores que `nv` en cada vertice — indices fuera de rango se saltan silenciosamente durante la generacion de normales pero pueden producir geometria incorrecta.
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "MeshGpu mesh_gpu_upload(const Mesh&); void mesh_gpu_destroy(MeshGpu&)"
|
||||
description: "Sube un Mesh CPU a OpenGL como VAO + VBO interleaved (pos.xyz, normal.xyz) + EBO uint32. Layout: location 0 = a_pos vec3, location 1 = a_normal vec3, stride 6 floats."
|
||||
tags: [opengl, mesh, vao, vbo, ebo, gpu, gfx]
|
||||
tags: [opengl, mesh, vao, vbo, ebo, gpu, gfx, mesh-3d]
|
||||
uses_functions: ["gl_loader_cpp_gfx", "mesh_obj_load_cpp_gfx"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "Mesh mesh_obj_parse(const char* obj_text, size_t len); Mesh mesh_obj_load(const char* path)"
|
||||
description: "Parser minimal de Wavefront .obj — soporta v, vn, f (tris y quads). Genera normales por face si faltan. mesh_obj_parse es puro; mesh_obj_load es helper impuro que lee fichero y delega."
|
||||
tags: [obj, mesh, parser, wavefront, loader, geometry, 3d]
|
||||
tags: [obj, mesh, parser, wavefront, loader, geometry, 3d, mesh-3d]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
|
||||
@@ -105,7 +105,7 @@ GLuint compile_program() {
|
||||
void ensure_init(Cache& c) {
|
||||
if (c.initialized) return;
|
||||
fn::gfx::gl_loader_init();
|
||||
fn::gfx::fb_init(c.fb);
|
||||
fn::gfx::fb_init_depth(c.fb);
|
||||
c.program = compile_program();
|
||||
if (c.program) {
|
||||
c.loc_view = glGetUniformLocation(c.program, "u_view");
|
||||
@@ -145,10 +145,9 @@ void mesh_viewer(const char* id, const MeshViewerConfig& cfg) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, c.fb.fbo);
|
||||
glViewport(0, 0, w, h);
|
||||
glClearColor(0.10f, 0.10f, 0.13f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
// No depth attachment in our FBO — fall back to back-to-front-ish via
|
||||
// GL_DEPTH_TEST off. For inspection meshes this is fine; documented.
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LESS);
|
||||
|
||||
glUseProgram(c.program);
|
||||
auto m = fn::core::orbit_camera_matrices(*cfg.cam);
|
||||
@@ -183,7 +182,7 @@ void mesh_viewer(const char* id, const MeshViewerConfig& cfg) {
|
||||
// Restore GL state.
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, (GLuint)prev_fbo);
|
||||
glViewport(prev_vp[0], prev_vp[1], prev_vp[2], prev_vp[3]);
|
||||
if (prev_depth) glEnable(GL_DEPTH_TEST);
|
||||
if (prev_depth) glEnable(GL_DEPTH_TEST); else glDisable(GL_DEPTH_TEST);
|
||||
}
|
||||
|
||||
// Display.
|
||||
|
||||
@@ -3,11 +3,11 @@ name: mesh_viewer
|
||||
kind: component
|
||||
lang: cpp
|
||||
domain: viz
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
purity: impure
|
||||
signature: "void mesh_viewer(const char* id, const MeshViewerConfig& cfg)"
|
||||
description: "Renderiza un MeshGpu (3D) en un FBO interno cacheado por id, con orbit camera, iluminacion Lambert headlight, opcion wireframe. Drag/wheel del mouse mueven la camara."
|
||||
tags: [imgui, opengl, mesh, 3d, viewer, viz, fbo, pendiente-usar]
|
||||
description: "Renderiza un MeshGpu (3D) en un FBO interno cacheado por id, con orbit camera, iluminacion Lambert headlight, depth test correcto (GL_DEPTH_COMPONENT24), opcion wireframe. Drag/wheel del mouse mueven la camara."
|
||||
tags: [imgui, opengl, mesh, 3d, viewer, viz, fbo, cpp-dashboard-viz]
|
||||
uses_functions: ["gl_framebuffer_cpp_gfx", "gl_loader_cpp_gfx", "gl_shader_cpp_gfx", "mesh_gpu_cpp_gfx", "orbit_camera_cpp_core"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
@@ -20,6 +20,11 @@ test_file_path: ""
|
||||
file_path: "cpp/functions/viz/mesh_viewer.cpp"
|
||||
framework: imgui
|
||||
emits: ["camera_drag", "camera_zoom"]
|
||||
params:
|
||||
- name: id
|
||||
desc: "ID estable de ImGui para cachear el FBO y el programa shader. Cambiar el id entre frames acumula recursos (leak). Usar IDs constantes."
|
||||
- name: cfg
|
||||
desc: "MeshViewerConfig con mesh (MeshGpu*), cam (OrbitCamera*), size (ImVec2, -1 = full width), wireframe (bool), color (ImU32 RGBA)."
|
||||
output: "Renderiza una imagen del mesh dentro del frame ImGui actual; muta cfg.cam in-place segun drag/wheel del mouse cuando el panel esta active/hovered."
|
||||
---
|
||||
|
||||
@@ -28,8 +33,8 @@ output: "Renderiza una imagen del mesh dentro del frame ImGui actual; muta cfg.c
|
||||
Componente de viz para inspeccionar geometria 3D dentro de cualquier panel ImGui. Internamente:
|
||||
|
||||
1. Compila/cachea (por `id`) un programa shader Lambert headlight (vertex + fragment).
|
||||
2. Cachea un `Framebuffer` por `id` y lo redimensiona segun `cfg.size`.
|
||||
3. Cada frame: bind FBO, draw `cfg.mesh`, mostrar la textura via `ImGui::Image`.
|
||||
2. Cachea un `Framebuffer` con depth renderbuffer por `id` y lo redimensiona segun `cfg.size`.
|
||||
3. Cada frame: bind FBO, clear color+depth, draw `cfg.mesh` con depth test activo, mostrar la textura via `ImGui::Image`.
|
||||
4. Si el panel esta active → llama `orbit_camera_handle_drag` con `MouseDelta`.
|
||||
5. Si el panel esta hovered y hay scroll → ajusta zoom.
|
||||
|
||||
@@ -46,10 +51,23 @@ cfg.color = IM_COL32(160, 200, 255, 255);
|
||||
fn::viz::mesh_viewer("##teapot_view", cfg);
|
||||
```
|
||||
|
||||
## Notas
|
||||
## Cuando usarla
|
||||
|
||||
- **Sin depth buffer**: el FBO solo tiene attachment color (sigue el patron de `gl_framebuffer`). Para meshes complejos con auto-oclusion, esto produce artefactos. Issue futuro puede añadir depth/stencil renderbuffer.
|
||||
- **Wireframe**: usa `glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)` (no disponible en GL ES; protegido con `#ifndef __EMSCRIPTEN__`).
|
||||
- **Cache por id**: si el `id` cambia dinamicamente entre frames, se acumulan FBOs y programas en memoria (leak). Usar IDs estables.
|
||||
Cuando necesites inspeccionar geometria 3D (OBJ, STL, cualquier MeshGpu) dentro de un panel ImGui existente, con orbit camera interactiva y auto-oclusion correcta de caras.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Cache por id**: si el `id` cambia dinamicamente entre frames, se acumulan FBOs y programas en memoria (leak). Usar IDs estables (`"##nombre_fijo"`).
|
||||
- **Wireframe**: usa `glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)` — no disponible en GL ES; protegido con `#ifndef __EMSCRIPTEN__`.
|
||||
- **Iluminacion**: Lambert con luz fija en `+Z` view-space ("headlight"), suficiente para inspeccion. Sin specular, sin sombras.
|
||||
- **Matrices**: row-major desde `orbit_camera_matrices`; se suben con `transpose=GL_TRUE` (GL espera column-major).
|
||||
- **Estado GL**: salva y restaura `GL_FRAMEBUFFER_BINDING`, `GL_VIEWPORT` y `GL_DEPTH_TEST` antes/despues del render. No contamina el estado del frame ImGui principal.
|
||||
|
||||
## Notas
|
||||
|
||||
- **Depth renderbuffer activo** (GL_DEPTH_COMPONENT24): auto-oclusion correcta en meshes solidos. `glEnable(GL_DEPTH_TEST)` + `glDepthFunc(GL_LESS)` dentro del render del FBO.
|
||||
- Usa `fb_init_depth` de `gl_framebuffer_cpp_gfx` (v1.1.0+).
|
||||
|
||||
## Capability growth log
|
||||
|
||||
v1.1.0 (2026-05-28) — depth renderbuffer via fb_init_depth, fix auto-oclusion en meshes solidos
|
||||
|
||||
Reference in New Issue
Block a user