feat(viz): mesh_viewer — componente 3D con FBO + Lambert headlight

Compila/cachea por id un programa GLSL (vertex+fragment) con
iluminacion Lambert (luz=camara), gestiona Framebuffer cacheado por
id, dibuja MeshGpu con orbit camera, muestra via ImGui::Image y
maneja drag (mouse) + wheel (zoom). Wireframe opcional via
glPolygonMode.

gl_loader: añade glUniformMatrix4fv (proc requerido en Windows para
subir las matrices view/proj del mesh_viewer).

issue 0029
This commit is contained in:
2026-04-25 21:51:22 +02:00
parent 4e19583a22
commit b557433388
5 changed files with 290 additions and 0 deletions
+2
View File
@@ -31,6 +31,7 @@ PFNGLUNIFORM2FPROC fn_glUniform2f = nullptr;
PFNGLUNIFORM3FPROC fn_glUniform3f = nullptr;
PFNGLUNIFORM4FPROC fn_glUniform4f = nullptr;
PFNGLUNIFORM4FVPROC fn_glUniform4fv = nullptr;
PFNGLUNIFORMMATRIX4FVPROC fn_glUniformMatrix4fv = nullptr;
PFNGLUSEPROGRAMPROC fn_glUseProgram = nullptr;
PFNGLACTIVETEXTUREPROC fn_glActiveTexture = nullptr;
PFNGLGENERATEMIPMAPPROC fn_glGenerateMipmap = nullptr;
@@ -82,6 +83,7 @@ bool gl_loader_init() {
LOAD(glUniform3f);
LOAD(glUniform4f);
LOAD(glUniform4fv);
LOAD(glUniformMatrix4fv);
LOAD(glUseProgram);
LOAD(glActiveTexture);
LOAD(glGenerateMipmap);
+2
View File
@@ -37,6 +37,7 @@
extern PFNGLUNIFORM3FPROC fn_glUniform3f;
extern PFNGLUNIFORM4FPROC fn_glUniform4f;
extern PFNGLUNIFORM4FVPROC fn_glUniform4fv;
extern PFNGLUNIFORMMATRIX4FVPROC fn_glUniformMatrix4fv;
extern PFNGLUSEPROGRAMPROC fn_glUseProgram;
// Texture (gl_texture_load — issue 0026)
extern PFNGLACTIVETEXTUREPROC fn_glActiveTexture;
@@ -84,6 +85,7 @@
#define glUniform3f fn_glUniform3f
#define glUniform4f fn_glUniform4f
#define glUniform4fv fn_glUniform4fv
#define glUniformMatrix4fv fn_glUniformMatrix4fv
#define glUseProgram fn_glUseProgram
#define glActiveTexture fn_glActiveTexture
#define glGenerateMipmap fn_glGenerateMipmap
+207
View File
@@ -0,0 +1,207 @@
#include "viz/mesh_viewer.h"
#include "gfx/gl_loader.h"
#include "gfx/gl_framebuffer.h"
#include "gfx/gl_shader.h"
#include <string>
#include <unordered_map>
namespace fn::viz {
namespace {
// Vertex+fragment shader with Lambert headlight (light = camera direction).
// We bypass gl_shader::compile_fragment because that helper assumes a
// fullscreen-quad pipeline; here we need explicit attribs and a vertex shader
// that consumes mvp + normalMatrix.
const char* kVert = R"glsl(
#version 330 core
layout(location = 0) in vec3 a_pos;
layout(location = 1) in vec3 a_normal;
uniform mat4 u_view;
uniform mat4 u_proj;
out vec3 v_normal_view;
void main() {
vec4 view_pos = u_view * vec4(a_pos, 1.0);
// For pure rotation in the view part of orbit-camera, the upper-left 3x3
// suffices as the normal matrix.
v_normal_view = mat3(u_view) * a_normal;
gl_Position = u_proj * view_pos;
}
)glsl";
const char* kFrag = R"glsl(
#version 330 core
in vec3 v_normal_view;
out vec4 frag;
uniform vec4 u_color;
void main() {
// Headlight: light dir = +Z in view space (toward camera in right-handed view).
vec3 N = normalize(v_normal_view);
vec3 L = vec3(0.0, 0.0, 1.0);
float ndl = max(dot(N, L), 0.0);
// ambient 0.2 + diffuse 0.8
float lighting = 0.2 + 0.8 * ndl;
frag = vec4(u_color.rgb * lighting, u_color.a);
}
)glsl";
// Per-id cached state.
struct Cache {
fn::gfx::Framebuffer fb{};
GLuint program = 0;
GLint loc_view = -1;
GLint loc_proj = -1;
GLint loc_color = -1;
bool initialized = false;
};
std::unordered_map<std::string, Cache>& caches() {
static std::unordered_map<std::string, Cache> m;
return m;
}
GLuint compile_program() {
// We need vertex+fragment, but gl_shader::compile_fragment only does
// fragment with a fixed vertex. Inline a small compile here.
auto compile = [](GLenum type, const char* src) -> GLuint {
GLuint sh = glCreateShader(type);
glShaderSource(sh, 1, &src, nullptr);
glCompileShader(sh);
GLint ok = 0;
glGetShaderiv(sh, GL_COMPILE_STATUS, &ok);
if (!ok) {
char log[512];
glGetShaderInfoLog(sh, sizeof(log), nullptr, log);
(void)log; // could log via ImGui
glDeleteShader(sh);
return 0;
}
return sh;
};
GLuint v = compile(GL_VERTEX_SHADER, kVert);
if (!v) return 0;
GLuint f = compile(GL_FRAGMENT_SHADER, kFrag);
if (!f) { glDeleteShader(v); return 0; }
GLuint p = glCreateProgram();
glAttachShader(p, v);
glAttachShader(p, f);
glLinkProgram(p);
glDeleteShader(v);
glDeleteShader(f);
GLint ok = 0;
glGetProgramiv(p, GL_LINK_STATUS, &ok);
if (!ok) { glDeleteProgram(p); return 0; }
return p;
}
void ensure_init(Cache& c) {
if (c.initialized) return;
fn::gfx::gl_loader_init();
fn::gfx::fb_init(c.fb);
c.program = compile_program();
if (c.program) {
c.loc_view = glGetUniformLocation(c.program, "u_view");
c.loc_proj = glGetUniformLocation(c.program, "u_proj");
c.loc_color = glGetUniformLocation(c.program, "u_color");
}
c.initialized = true;
}
} // namespace
void mesh_viewer(const char* id, const MeshViewerConfig& cfg) {
if (!id) id = "##mesh_viewer";
// Resolve panel size.
ImVec2 size = cfg.size;
if (size.x <= 0) size.x = ImGui::GetContentRegionAvail().x;
if (size.y <= 0) size.y = 400.0f;
int w = (int)size.x; if (w < 1) w = 1;
int h = (int)size.y; if (h < 1) h = 1;
auto& c = caches()[id];
ensure_init(c);
if (cfg.mesh && cfg.mesh->ok() && cfg.cam && c.program) {
cfg.cam->aspect = (float)w / (float)h;
fn::gfx::fb_resize(c.fb, w, h);
// Save GL state.
GLint prev_fbo = 0;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prev_fbo);
GLint prev_vp[4];
glGetIntegerv(GL_VIEWPORT, prev_vp);
GLboolean prev_depth = glIsEnabled(GL_DEPTH_TEST);
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);
glUseProgram(c.program);
auto m = fn::core::orbit_camera_matrices(*cfg.cam);
// Row-major -> upload with transpose=GL_TRUE.
if (c.loc_view >= 0) glUniformMatrix4fv(c.loc_view, 1, GL_TRUE, m.view);
if (c.loc_proj >= 0) glUniformMatrix4fv(c.loc_proj, 1, GL_TRUE, m.proj);
if (c.loc_color >= 0) {
float r = ((cfg.color >> 0) & 0xFF) / 255.0f;
float g = ((cfg.color >> 8) & 0xFF) / 255.0f;
float b = ((cfg.color >> 16) & 0xFF) / 255.0f;
float a = ((cfg.color >> 24) & 0xFF) / 255.0f;
glUniform4f(c.loc_color, r, g, b, a);
}
#ifndef __EMSCRIPTEN__
if (cfg.wireframe) {
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
} else {
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}
#endif
glBindVertexArray(cfg.mesh->vao);
glDrawElements(GL_TRIANGLES, cfg.mesh->index_count, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
#ifndef __EMSCRIPTEN__
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
#endif
glUseProgram(0);
// 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);
}
// Display.
ImGui::Image((ImTextureID)(intptr_t)c.fb.tex,
ImVec2((float)w, (float)h),
ImVec2(0, 1), ImVec2(1, 0));
if (ImGui::IsItemActive() && cfg.cam) {
ImVec2 d = ImGui::GetIO().MouseDelta;
fn::core::orbit_camera_handle_drag(*cfg.cam, d, 0.0f);
}
if (ImGui::IsItemHovered() && cfg.cam) {
float w_scroll = ImGui::GetIO().MouseWheel;
if (w_scroll != 0.0f) {
ImVec2 zero{0, 0};
fn::core::orbit_camera_handle_drag(*cfg.cam, zero, w_scroll);
}
}
}
} // namespace fn::viz
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include "core/orbit_camera.h"
#include "gfx/mesh_gpu.h"
#include "imgui.h"
namespace fn::viz {
struct MeshViewerConfig {
const fn::gfx::MeshGpu* mesh = nullptr;
fn::core::OrbitCamera* cam = nullptr; // mutable: drag/wheel handler
ImVec2 size = {-1.0f, 400.0f}; // -1 = stretch X
ImU32 color = IM_COL32(180, 180, 200, 255);
bool wireframe = false;
};
// Renderiza un MeshGpu en un FBO interno (cacheado por id) y muestra
// la textura via ImGui::Image. Maneja drag/wheel sobre el panel.
//
// El `id` debe ser estable para reusar el mismo FBO entre frames.
void mesh_viewer(const char* id, const MeshViewerConfig& cfg);
} // namespace fn::viz
+55
View File
@@ -0,0 +1,55 @@
---
name: mesh_viewer
kind: component
lang: cpp
domain: viz
version: "1.0.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]
uses_functions: [mesh_gpu_cpp_gfx, orbit_camera_cpp_core, gl_framebuffer_cpp_gfx]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [imgui, GL/gl.h]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/mesh_viewer.cpp"
framework: imgui
emits: ["camera_drag", "camera_zoom"]
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."
---
# mesh_viewer
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`.
4. Si el panel esta active → llama `orbit_camera_handle_drag` con `MouseDelta`.
5. Si el panel esta hovered y hay scroll → ajusta zoom.
## Ejemplo
```cpp
static fn::core::OrbitCamera cam;
fn::viz::MeshViewerConfig cfg{};
cfg.mesh = &gpu; // MeshGpu valido
cfg.cam = &cam;
cfg.size = {-1, 480};
cfg.wireframe = false;
cfg.color = IM_COL32(160, 200, 255, 255);
fn::viz::mesh_viewer("##teapot_view", cfg);
```
## Notas
- **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.
- **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).