diff --git a/cpp/functions/gfx/gl_loader.cpp b/cpp/functions/gfx/gl_loader.cpp index d0493b9c..2fc0ccca 100644 --- a/cpp/functions/gfx/gl_loader.cpp +++ b/cpp/functions/gfx/gl_loader.cpp @@ -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); diff --git a/cpp/functions/gfx/gl_loader.h b/cpp/functions/gfx/gl_loader.h index 499d1629..eeebda9a 100644 --- a/cpp/functions/gfx/gl_loader.h +++ b/cpp/functions/gfx/gl_loader.h @@ -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 diff --git a/cpp/functions/viz/mesh_viewer.cpp b/cpp/functions/viz/mesh_viewer.cpp new file mode 100644 index 00000000..c6017b4c --- /dev/null +++ b/cpp/functions/viz/mesh_viewer.cpp @@ -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 +#include + +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& caches() { + static std::unordered_map 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 diff --git a/cpp/functions/viz/mesh_viewer.h b/cpp/functions/viz/mesh_viewer.h new file mode 100644 index 00000000..51964091 --- /dev/null +++ b/cpp/functions/viz/mesh_viewer.h @@ -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 diff --git a/cpp/functions/viz/mesh_viewer.md b/cpp/functions/viz/mesh_viewer.md new file mode 100644 index 00000000..dff986d3 --- /dev/null +++ b/cpp/functions/viz/mesh_viewer.md @@ -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).