diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 3b931617..a9bae6da 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -99,6 +99,11 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/chart_demo/CMakeLists.txt) add_subdirectory(apps/chart_demo) endif() +# --- Shaders Lab --- +if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/shaders_lab/CMakeLists.txt) + add_subdirectory(apps/shaders_lab) +endif() + # --- Registry Dashboard (lives in projects/fn_monitoring/apps/) --- set(_DASH_DIR ${CMAKE_SOURCE_DIR}/../projects/fn_monitoring/apps/registry_dashboard) if(EXISTS ${_DASH_DIR}/CMakeLists.txt) diff --git a/cpp/apps/shaders_lab/CMakeLists.txt b/cpp/apps/shaders_lab/CMakeLists.txt new file mode 100644 index 00000000..66dfe9f2 --- /dev/null +++ b/cpp/apps/shaders_lab/CMakeLists.txt @@ -0,0 +1,11 @@ +add_imgui_app(shaders_lab + main.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/gl_shader.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/gl_framebuffer.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/fullscreen_quad.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/shader_canvas.cpp + ${CMAKE_SOURCE_DIR}/functions/core/fps_overlay.cpp +) +target_include_directories(shaders_lab PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} +) diff --git a/cpp/apps/shaders_lab/main.cpp b/cpp/apps/shaders_lab/main.cpp new file mode 100644 index 00000000..e996433c --- /dev/null +++ b/cpp/apps/shaders_lab/main.cpp @@ -0,0 +1,115 @@ +#include "app_base.h" +#include "imgui.h" + +#include "gfx/shader_canvas.h" +#include "gfx/gl_shader.h" +#include "core/fps_overlay.h" +#include "seed_shaders.h" + +#include +#include + +static fn::gfx::ShaderCanvas g_canvas; +static std::string g_source = PLASMA; +static std::string g_last_err; +static int g_last_err_line = -1; +static std::chrono::steady_clock::time_point g_last_edit; +static bool g_dirty = true; + +static void try_compile() { + auto r = fn::gfx::compile_fragment(g_source); + if (r.ok) { + fn::gfx::canvas_set_program(g_canvas, r.program); + g_last_err.clear(); + g_last_err_line = -1; + } else { + g_last_err = r.err_msg; + g_last_err_line = r.err_line; + } +} + +static void mark_dirty() { + g_last_edit = std::chrono::steady_clock::now(); + g_dirty = true; +} + +static void load_preset(const char* src) { + g_source = src; + mark_dirty(); +} + +static void render() { + if (!g_canvas.initialized) fn::gfx::canvas_init(g_canvas); + + if (g_dirty) { + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - g_last_edit).count(); + if (elapsed > 250) { + try_compile(); + g_dirty = false; + } + } + + ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport()); + + // --- Code panel --- + if (ImGui::Begin("Code")) { + if (ImGui::Button("Plasma")) { load_preset(PLASMA); } + ImGui::SameLine(); + if (ImGui::Button("Circle")) { load_preset(CIRCLE); } + ImGui::SameLine(); + if (ImGui::Button("Checker")) { load_preset(CHECKER); } + + ImVec2 avail = ImGui::GetContentRegionAvail(); + float footer_height = g_last_err.empty() ? 0.0f : ImGui::GetTextLineHeightWithSpacing() + 8.0f; + ImVec2 editor_size(avail.x, avail.y - footer_height); + + char buf[1 << 16]; + size_t copy_len = g_source.size() < sizeof(buf) - 1 ? g_source.size() : sizeof(buf) - 1; + memcpy(buf, g_source.c_str(), copy_len); + buf[copy_len] = '\0'; + + ImGui::PushFont(nullptr); // use default monospace-ish font + if (ImGui::InputTextMultiline("##code", buf, sizeof(buf), editor_size, + ImGuiInputTextFlags_AllowTabInput)) { + g_source = buf; + mark_dirty(); + } + ImGui::PopFont(); + + if (!g_last_err.empty()) { + ImGui::Separator(); + if (g_last_err_line > 0) { + ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "line %d: %s", + g_last_err_line, g_last_err.c_str()); + } else { + ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s", g_last_err.c_str()); + } + } + } + ImGui::End(); + + // --- Canvas panel --- + if (ImGui::Begin("Canvas")) { + fn::gfx::canvas_render(g_canvas, static_cast(ImGui::GetTime())); + } + ImGui::End(); + + // --- Controls panel --- + if (ImGui::Begin("Controls")) { + ImGui::TextDisabled("Controls (fase 2)"); + ImGui::Spacing(); + fps_overlay(); + } + ImGui::End(); +} + +int main() { + fn::AppConfig cfg; + cfg.title = "shaders_lab"; + cfg.width = 1400; + cfg.height = 860; + int rc = fn::run_app(cfg, render); + fn::gfx::canvas_destroy(g_canvas); + return rc; +} diff --git a/cpp/apps/shaders_lab/seed_shaders.h b/cpp/apps/shaders_lab/seed_shaders.h new file mode 100644 index 00000000..0eab0412 --- /dev/null +++ b/cpp/apps/shaders_lab/seed_shaders.h @@ -0,0 +1,95 @@ +#pragma once + +// GLSL 330 fragment shader bodies (no #version, no out, no uniform declarations). +// compile_fragment() prepends those automatically. + +static const char* PLASMA = R"glsl( +void main() { + vec2 uv = gl_FragCoord.xy / u_resolution; + float t = u_time * 0.5; + + float v1 = sin(uv.x * 10.0 + t); + float v2 = sin(uv.y * 10.0 + t * 1.3); + float v3 = sin((uv.x + uv.y) * 10.0 + t * 0.7); + float v4 = sin(length(uv - 0.5) * 20.0 - t * 2.0); + + float v = (v1 + v2 + v3 + v4) * 0.25; + + vec3 col = vec3( + sin(v * 3.14159 + 0.0) * 0.5 + 0.5, + sin(v * 3.14159 + 2.094) * 0.5 + 0.5, + sin(v * 3.14159 + 4.188) * 0.5 + 0.5 + ); + + fragColor = vec4(col, 1.0); +} +)glsl"; + +static const char* CIRCLE = R"glsl( +void main() { + vec2 uv = gl_FragCoord.xy / u_resolution; + vec2 center = vec2(0.5); + float t = u_time; + + // Animated center + center += vec2(sin(t * 0.7), cos(t * 0.5)) * 0.2; + + float d = length(uv - center); + + // Concentric rings + float rings = sin(d * 40.0 - t * 3.0) * 0.5 + 0.5; + + // Radial glow + float glow = exp(-d * 4.0); + + vec3 col = mix( + vec3(0.05, 0.1, 0.3), + vec3(0.2, 0.7, 1.0), + rings * glow + glow * 0.4 + ); + + fragColor = vec4(col, 1.0); +} +)glsl"; + +static const char* CHECKER = R"glsl( +void main() { + vec2 uv = gl_FragCoord.xy / u_resolution; + float t = u_time; + + // Animated scale and rotation + float scale = 8.0 + sin(t * 0.4) * 3.0; + float angle = t * 0.2; + float ca = cos(angle), sa = sin(angle); + vec2 p = uv - 0.5; + p = vec2(ca * p.x - sa * p.y, sa * p.x + ca * p.y); + p = p * scale + 0.5; + + vec2 cell = floor(p); + float checker = mod(cell.x + cell.y, 2.0); + + // Color gradient per cell + float hue = fract((cell.x + cell.y) * 0.1 + t * 0.05); + vec3 col_a = vec3(hue, 0.7, 0.9); + vec3 col_b = vec3(fract(hue + 0.5), 0.5, 0.7); + + // Simple HSV to RGB + vec3 col = mix(col_b, col_a, checker); + // hue is already [0,1], apply saturation/value manually + float h = col.x * 6.0; + int i = int(h); + float f = h - float(i); + float p2 = col.z * (1.0 - col.y); + float q2 = col.z * (1.0 - col.y * f); + float t2 = col.z * (1.0 - col.y * (1.0 - f)); + vec3 rgb; + if (i == 0) rgb = vec3(col.z, t2, p2); + else if (i == 1) rgb = vec3(q2, col.z, p2); + else if (i == 2) rgb = vec3(p2, col.z, t2); + else if (i == 3) rgb = vec3(p2, q2, col.z); + else if (i == 4) rgb = vec3(t2, p2, col.z); + else rgb = vec3(col.z, p2, q2); + + fragColor = vec4(rgb, 1.0); +} +)glsl"; diff --git a/cpp/functions/gfx/fullscreen_quad.cpp b/cpp/functions/gfx/fullscreen_quad.cpp new file mode 100644 index 00000000..a3bb1897 --- /dev/null +++ b/cpp/functions/gfx/fullscreen_quad.cpp @@ -0,0 +1,30 @@ +#define GL_GLEXT_PROTOTYPES +#include +#include + +#include "gfx/fullscreen_quad.h" + +namespace fn::gfx { + +void quad_init(Quad& q) { + glGenVertexArrays(1, &q.vao); + glGenBuffers(1, &q.vbo); + // Vertex shader generates positions from gl_VertexID — VBO stays empty. + glBindVertexArray(q.vao); + glBindBuffer(GL_ARRAY_BUFFER, q.vbo); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); +} + +void quad_draw(const Quad& q) { + glBindVertexArray(q.vao); + glDrawArrays(GL_TRIANGLES, 0, 6); + glBindVertexArray(0); +} + +void quad_destroy(Quad& q) { + if (q.vao) { glDeleteVertexArrays(1, &q.vao); q.vao = 0; } + if (q.vbo) { glDeleteBuffers(1, &q.vbo); q.vbo = 0; } +} + +} // namespace fn::gfx diff --git a/cpp/functions/gfx/fullscreen_quad.h b/cpp/functions/gfx/fullscreen_quad.h new file mode 100644 index 00000000..75471e75 --- /dev/null +++ b/cpp/functions/gfx/fullscreen_quad.h @@ -0,0 +1,14 @@ +#pragma once + +namespace fn::gfx { + +struct Quad { + unsigned int vao = 0; + unsigned int vbo = 0; +}; + +void quad_init(Quad& q); +void quad_draw(const Quad& q); +void quad_destroy(Quad& q); + +} // namespace fn::gfx diff --git a/cpp/functions/gfx/fullscreen_quad.md b/cpp/functions/gfx/fullscreen_quad.md new file mode 100644 index 00000000..8e21dbce --- /dev/null +++ b/cpp/functions/gfx/fullscreen_quad.md @@ -0,0 +1,45 @@ +--- +name: fullscreen_quad +kind: function +lang: cpp +domain: gfx +version: "1.0.0" +purity: impure +signature: "void quad_init(Quad& q); void quad_draw(const Quad& q); void quad_destroy(Quad& q)" +description: "VAO/VBO para un fullscreen quad de 6 vértices. El vertex shader genera las posiciones via gl_VertexID, por lo que el VBO queda vacío. quad_draw emite glDrawArrays(GL_TRIANGLES, 0, 6)." +tags: [opengl, quad, fullscreen, vao, vbo, gfx] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [GL/gl.h, GL/glext.h] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/gfx/fullscreen_quad.cpp" +framework: opengl +params: + - name: q + desc: "Struct Quad con campos vao, vbo (GL ids). Inicializar a {0} antes de quad_init." +output: "Modifica q in-place. quad_draw necesita un programa GL activo con vertex shader que genere posiciones desde gl_VertexID." +--- + +# fullscreen_quad + +VAO + VBO vacío para dibujar un quad de pantalla completa. El vertex shader de `gl_shader` ya embebe las 6 posiciones via `gl_VertexID`, por lo que no se necesitan datos en el VBO. + +## Ejemplo + +```cpp +fn::gfx::Quad quad{}; +fn::gfx::quad_init(quad); + +// En el render loop (con program activo): +glUseProgram(program); +fn::gfx::quad_draw(quad); +glUseProgram(0); + +// Al destruir: +fn::gfx::quad_destroy(quad); +``` diff --git a/cpp/functions/gfx/gl_framebuffer.cpp b/cpp/functions/gfx/gl_framebuffer.cpp new file mode 100644 index 00000000..fb28b855 --- /dev/null +++ b/cpp/functions/gfx/gl_framebuffer.cpp @@ -0,0 +1,49 @@ +#define GL_GLEXT_PROTOTYPES +#include +#include + +#include "gfx/gl_framebuffer.h" + +namespace fn::gfx { + +static void create_tex(Framebuffer& f) { + glGenTextures(1, &f.tex); + glBindTexture(GL_TEXTURE_2D, f.tex); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, f.width, f.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glBindTexture(GL_TEXTURE_2D, 0); +} + +void fb_init(Framebuffer& f) { + f.width = 1; + f.height = 1; + create_tex(f); + glGenFramebuffers(1, &f.fbo); + glBindFramebuffer(GL_FRAMEBUFFER, f.fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, f.tex, 0); + 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.height = h; + 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); + 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; +} + +} // namespace fn::gfx diff --git a/cpp/functions/gfx/gl_framebuffer.h b/cpp/functions/gfx/gl_framebuffer.h new file mode 100644 index 00000000..e4c1c853 --- /dev/null +++ b/cpp/functions/gfx/gl_framebuffer.h @@ -0,0 +1,16 @@ +#pragma once + +namespace fn::gfx { + +struct Framebuffer { + unsigned int fbo = 0; + unsigned int tex = 0; // GL_RGBA8, clamp, linear + int width = 0; + int height = 0; +}; + +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); + +} // namespace fn::gfx diff --git a/cpp/functions/gfx/gl_framebuffer.md b/cpp/functions/gfx/gl_framebuffer.md new file mode 100644 index 00000000..b8c3840b --- /dev/null +++ b/cpp/functions/gfx/gl_framebuffer.md @@ -0,0 +1,62 @@ +--- +name: gl_framebuffer +kind: function +lang: cpp +domain: gfx +version: "1.0.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] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [GL/gl.h, GL/glext.h] +tested: false +tests: [] +test_file_path: "" +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." + - 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." +--- + +# gl_framebuffer + +FBO con textura color RGBA8 (GL_CLAMP_TO_EDGE, GL_LINEAR). Diseñado para renderizado offscreen y posterior display via `ImGui::Image`. + +## Ciclo de vida + +```cpp +fn::gfx::Framebuffer fb{}; +fn::gfx::fb_init(fb); // fbo + tex 1x1 + +// En el render loop: +fn::gfx::fb_resize(fb, w, h); // no-op si mismas dimensiones + +// Al destruir: +fn::gfx::fb_destroy(fb); +``` + +## Uso con ImGui::Image + +```cpp +// Flip Y porque OpenGL tiene origen bottom-left +ImGui::Image( + reinterpret_cast(static_cast(fb.tex)), + size, + ImVec2(0, 1), ImVec2(1, 0) +); +``` + +## 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. diff --git a/cpp/functions/gfx/gl_shader.cpp b/cpp/functions/gfx/gl_shader.cpp new file mode 100644 index 00000000..d06f9cd4 --- /dev/null +++ b/cpp/functions/gfx/gl_shader.cpp @@ -0,0 +1,119 @@ +#define GL_GLEXT_PROTOTYPES +#include +#include + +#include "gfx/gl_shader.h" + +#include +#include +#include +#include + +namespace fn::gfx { + +static const char* k_vert_src = R"glsl( +#version 330 core +const vec2 verts[6] = vec2[]( + vec2(-1,-1), vec2(1,-1), vec2(-1,1), + vec2(1,-1), vec2(1,1), vec2(-1,1) +); +void main() { gl_Position = vec4(verts[gl_VertexID], 0.0, 1.0); } +)glsl"; + +static const char* k_frag_preamble = + "#version 330 core\n" + "out vec4 fragColor;\n" + "uniform vec2 u_resolution;\n" + "uniform float u_time;\n" + "uniform vec2 u_mouse;\n"; + +static int parse_err_line(const char* log) { + // Try "ERROR: 0::" format + std::regex re1(R"(ERROR:\s*\d+:(\d+):)"); + // Try "0()" format + std::regex re2(R"(\d+\((\d+)\))"); + std::cmatch m; + if (std::regex_search(log, m, re1)) { + return std::stoi(m[1].str()); + } + if (std::regex_search(log, m, re2)) { + return std::stoi(m[1].str()); + } + return -1; +} + +static unsigned int compile_shader(GLenum type, const char** srcs, int count, std::string& out_err, int& out_line) { + unsigned int s = glCreateShader(type); + glShaderSource(s, count, srcs, nullptr); + glCompileShader(s); + GLint ok = 0; + glGetShaderiv(s, GL_COMPILE_STATUS, &ok); + if (!ok) { + GLint len = 0; + glGetShaderiv(s, GL_INFO_LOG_LENGTH, &len); + out_err.resize(static_cast(len)); + glGetShaderInfoLog(s, len, nullptr, &out_err[0]); + out_line = parse_err_line(out_err.c_str()); + glDeleteShader(s); + return 0; + } + return s; +} + +CompileResult compile_fragment(const std::string& user_fragment_src) { + CompileResult result; + + // Vertex shader (no preamble needed — it's a standalone complete shader) + std::string vert_err; + int vert_line = -1; + unsigned int vert = compile_shader(GL_VERTEX_SHADER, &k_vert_src, 1, vert_err, vert_line); + if (!vert) { + result.err_msg = "vertex: " + vert_err; + result.err_line = vert_line; + return result; + } + + // Fragment shader — prepend preamble, then user body + const char* frag_srcs[2] = { k_frag_preamble, user_fragment_src.c_str() }; + std::string frag_err; + int frag_line = -1; + unsigned int frag = compile_shader(GL_FRAGMENT_SHADER, frag_srcs, 2, frag_err, frag_line); + if (!frag) { + glDeleteShader(vert); + result.err_msg = frag_err; + // Adjust line: subtract preamble lines (4 lines) + if (frag_line > 4) result.err_line = frag_line - 4; + else result.err_line = frag_line; + return result; + } + + // Link + unsigned int prog = glCreateProgram(); + glAttachShader(prog, vert); + glAttachShader(prog, frag); + glLinkProgram(prog); + glDeleteShader(vert); + glDeleteShader(frag); + + GLint ok = 0; + glGetProgramiv(prog, GL_LINK_STATUS, &ok); + if (!ok) { + GLint len = 0; + glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &len); + result.err_msg.resize(static_cast(len)); + glGetProgramInfoLog(prog, len, nullptr, &result.err_msg[0]); + result.err_line = parse_err_line(result.err_msg.c_str()); + glDeleteProgram(prog); + return result; + } + + result.program = prog; + result.ok = true; + return result; +} + +void delete_program(unsigned int program) { + if (program) glDeleteProgram(program); +} + +} // namespace fn::gfx diff --git a/cpp/functions/gfx/gl_shader.h b/cpp/functions/gfx/gl_shader.h new file mode 100644 index 00000000..ba4ac159 --- /dev/null +++ b/cpp/functions/gfx/gl_shader.h @@ -0,0 +1,22 @@ +#pragma once +#include + +namespace fn::gfx { + +struct CompileResult { + unsigned int program = 0; // GL program id, 0 si falla + bool ok = false; + int err_line = -1; // línea parseada del infoLog, -1 si no + std::string err_msg; +}; + +// Compila un fragment shader GLSL (sólo el cuerpo del usuario). +// Prepends automáticamente: version, out vec4 fragColor, y uniforms u_resolution/u_time/u_mouse. +// Usa un vertex shader fijo que genera un fullscreen quad via gl_VertexID. +// Si falla, program = 0. Si ok, program es una id válida de glProgram lista para usar. +CompileResult compile_fragment(const std::string& user_fragment_src); + +// Libera el programa. Seguro con id = 0. +void delete_program(unsigned int program); + +} // namespace fn::gfx diff --git a/cpp/functions/gfx/gl_shader.md b/cpp/functions/gfx/gl_shader.md new file mode 100644 index 00000000..2efed100 --- /dev/null +++ b/cpp/functions/gfx/gl_shader.md @@ -0,0 +1,66 @@ +--- +name: gl_shader +kind: function +lang: cpp +domain: gfx +version: "1.0.0" +purity: impure +signature: "CompileResult compile_fragment(const std::string& user_fragment_src)" +description: "Compila un cuerpo de fragment shader GLSL 330 y retorna un GL program listo para usar. Prepende automáticamente version, out vec4 fragColor y uniforms u_resolution/u_time/u_mouse. Usa GL_GLEXT_PROTOTYPES + GL/glext.h." +tags: [opengl, shader, glsl, compile, fragment, gfx] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [GL/gl.h, GL/glext.h] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/gfx/gl_shader.cpp" +framework: opengl +params: + - name: user_fragment_src + desc: "Cuerpo del fragment shader GLSL sin #version, sin 'out vec4 fragColor' ni declaraciones de uniforms. Solo el void main() y funciones auxiliares." +output: "CompileResult con program=GL id si ok=true, o err_msg/err_line si falla. program=0 indica error." +--- + +# gl_shader + +Compila y enlaza un fragment shader GLSL 330 contra un vertex shader fijo que genera un fullscreen quad via `gl_VertexID`. + +## Vertex shader fijo + +```glsl +#version 330 core +const vec2 verts[6] = vec2[]( + vec2(-1,-1), vec2(1,-1), vec2(-1,1), + vec2(1,-1), vec2(1,1), vec2(-1,1) +); +void main() { gl_Position = vec4(verts[gl_VertexID], 0.0, 1.0); } +``` + +## Preamble prepended al fragment + +```glsl +#version 330 core +out vec4 fragColor; +uniform vec2 u_resolution; +uniform float u_time; +uniform vec2 u_mouse; +``` + +## Ejemplo + +```cpp +auto r = fn::gfx::compile_fragment("void main() { fragColor = vec4(1,0,0,1); }"); +if (r.ok) { + glUseProgram(r.program); +} else { + fprintf(stderr, "line %d: %s\n", r.err_line, r.err_msg.c_str()); +} +``` + +## Notas + +Usa `#define GL_GLEXT_PROTOTYPES` + `` + `` (mismo patrón que `graph_renderer`). El loader de ImGui ya ha inicializado los symbols GL antes de que esta función sea llamada. El err_line del fragment se ajusta restando las 4 líneas del preamble. diff --git a/cpp/functions/gfx/shader_canvas.cpp b/cpp/functions/gfx/shader_canvas.cpp new file mode 100644 index 00000000..4cc671d1 --- /dev/null +++ b/cpp/functions/gfx/shader_canvas.cpp @@ -0,0 +1,85 @@ +#define GL_GLEXT_PROTOTYPES +#include +#include + +#include "gfx/shader_canvas.h" +#include "imgui.h" + +namespace fn::gfx { + +void canvas_init(ShaderCanvas& c) { + if (c.initialized) return; + fb_init(c.fb); + quad_init(c.quad); + c.initialized = true; +} + +void canvas_set_program(ShaderCanvas& c, unsigned int program) { + if (c.program) delete_program(c.program); + c.program = program; +} + +void canvas_render(ShaderCanvas& c, float time_seconds) { + ImVec2 avail = ImGui::GetContentRegionAvail(); + int w = static_cast(avail.x); + int h = static_cast(avail.y); + if (w < 1) w = 1; + if (h < 1) h = 1; + + 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); + + // Render to FBO + glBindFramebuffer(GL_FRAMEBUFFER, c.fb.fbo); + glViewport(0, 0, w, h); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + if (c.program) { + glUseProgram(c.program); + + GLint loc_res = glGetUniformLocation(c.program, "u_resolution"); + GLint loc_time = glGetUniformLocation(c.program, "u_time"); + GLint loc_mouse = glGetUniformLocation(c.program, "u_mouse"); + + if (loc_res >= 0) glUniform2f(loc_res, static_cast(w), static_cast(h)); + if (loc_time >= 0) glUniform1f(loc_time, time_seconds); + if (loc_mouse >= 0) { + ImVec2 mouse = ImGui::GetMousePos(); + ImVec2 canvas_pos = ImGui::GetCursorScreenPos(); + // cursor pos is AFTER content region starts + float mx = mouse.x - canvas_pos.x; + float my = static_cast(h) - (mouse.y - canvas_pos.y); + glUniform2f(loc_mouse, mx, my); + } + + quad_draw(c.quad); + glUseProgram(0); + } + + // Restore GL state + glBindFramebuffer(GL_FRAMEBUFFER, static_cast(prev_fbo)); + glViewport(prev_vp[0], prev_vp[1], prev_vp[2], prev_vp[3]); + + // Draw texture in ImGui panel (flip V: OpenGL origin is bottom-left) + ImGui::Image( + (ImTextureID)(intptr_t)c.fb.tex, + avail, + ImVec2(0, 1), ImVec2(1, 0) + ); +} + +void canvas_destroy(ShaderCanvas& c) { + if (!c.initialized) return; + if (c.program) { delete_program(c.program); c.program = 0; } + quad_destroy(c.quad); + fb_destroy(c.fb); + c.initialized = false; +} + +} // namespace fn::gfx diff --git a/cpp/functions/gfx/shader_canvas.h b/cpp/functions/gfx/shader_canvas.h new file mode 100644 index 00000000..f481ccc4 --- /dev/null +++ b/cpp/functions/gfx/shader_canvas.h @@ -0,0 +1,28 @@ +#pragma once +#include +#include "gfx/gl_framebuffer.h" +#include "gfx/gl_shader.h" +#include "gfx/fullscreen_quad.h" + +namespace fn::gfx { + +struct ShaderCanvas { + Framebuffer fb; + Quad quad; + unsigned int program = 0; + bool initialized = false; +}; + +// Inicializa recursos GL (idempotente). +void canvas_init(ShaderCanvas& c); + +// Sustituye el programa activo (borra el anterior). Acepta program=0 para pantalla en negro. +void canvas_set_program(ShaderCanvas& c, unsigned int program); + +// Renderiza el shader al FBO y dibuja la textura resultante como contenido del panel ImGui. +// Llamar DENTRO de un ImGui::Begin/End. Ocupa GetContentRegionAvail(). +void canvas_render(ShaderCanvas& c, float time_seconds); + +void canvas_destroy(ShaderCanvas& c); + +} // namespace fn::gfx diff --git a/cpp/functions/gfx/shader_canvas.md b/cpp/functions/gfx/shader_canvas.md new file mode 100644 index 00000000..94663bce --- /dev/null +++ b/cpp/functions/gfx/shader_canvas.md @@ -0,0 +1,61 @@ +--- +name: shader_canvas +kind: component +lang: cpp +domain: gfx +version: "1.0.0" +purity: impure +signature: "void canvas_render(ShaderCanvas& c, float time_seconds)" +description: "Componente ImGui que renderiza un fragment shader GLSL a un FBO y lo muestra en el panel actual. Compone gl_framebuffer, fullscreen_quad y gl_shader. Gestiona resize automático y coordenadas de mouse." +tags: [opengl, shader, canvas, imgui, fbo, gfx, component] +uses_functions: + - gl_shader_cpp_gfx + - gl_framebuffer_cpp_gfx + - fullscreen_quad_cpp_gfx +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [imgui, GL/gl.h, GL/glext.h] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/gfx/shader_canvas.cpp" +framework: imgui +params: + - name: c + desc: "ShaderCanvas con estado GL interno (fb, quad, program). Inicializar con canvas_init() antes del primer frame." + - name: time_seconds + desc: "Tiempo en segundos para el uniform u_time. Usar ImGui::GetTime() o clock propio." +output: "Dibuja ImGui::Image con la textura del FBO renderizado. El panel ImGui debe estar abierto (entre Begin/End). Ocupa GetContentRegionAvail()." +--- + +# shader_canvas + +Componente que encapsula el ciclo render-to-FBO + ImGui::Image. Llama a `canvas_render()` dentro de un `ImGui::Begin/End` activo. + +## Ciclo de vida + +```cpp +fn::gfx::ShaderCanvas canvas{}; + +// En el render loop: +if (!canvas.initialized) fn::gfx::canvas_init(canvas); + +// Cargar un shader compilado: +auto r = fn::gfx::compile_fragment(src); +if (r.ok) fn::gfx::canvas_set_program(canvas, r.program); + +// Dentro de ImGui::Begin/End: +fn::gfx::canvas_render(canvas, (float)ImGui::GetTime()); + +// Al destruir: +fn::gfx::canvas_destroy(canvas); +``` + +## Notas + +- `canvas_set_program` borra el programa anterior automáticamente. +- `canvas_set_program(c, 0)` deja la pantalla en negro (glClear sin draw call). +- El flip de coordenadas UV (`ImVec2(0,1)` / `ImVec2(1,0)`) corrige el origen OpenGL bottom-left vs ImGui top-left. +- Guarda y restaura `GL_FRAMEBUFFER_BINDING` y `GL_VIEWPORT` para compatibilidad con el render loop de ImGui.