feat(shaders_lab): scaffold C++ app with GLSL live-reload canvas

- cpp/functions/gfx: gl_shader, gl_framebuffer, fullscreen_quad, shader_canvas
- cpp/apps/shaders_lab: main + 3 seed shaders (plasma, circle, checker)
- ImGui docking layout: Code | Canvas | Controls

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-24 20:32:14 +02:00
parent d7c3daaa6b
commit 3008b56e76
17 changed files with 868 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
---
name: shaders_lab
lang: cpp
domain: gfx
description: "Live GLSL fragment shader editor con preview en tiempo real. Layout de 3 paneles: editor de código, canvas de preview y controles."
tags: [gui, shaders, opengl, glsl, imgui]
uses_functions:
- gl_shader_cpp_gfx
- gl_framebuffer_cpp_gfx
- fullscreen_quad_cpp_gfx
- shader_canvas_cpp_gfx
- fps_overlay_cpp_core
uses_types: []
framework: "imgui + opengl3"
entry_point: "cpp/build/linux/apps/shaders_lab/shaders_lab"
dir_path: "cpp/apps/shaders_lab"
---
## Descripción
Editor interactivo de fragment shaders GLSL con preview en tiempo real. Incluye 3 presets (Plasma, Circle, Checker) y recompila automáticamente con debounce de 250ms al editar el código.
## Layout
- **Code** (izquierda): editor de texto con botones de preset y footer de errores de compilación
- **Canvas** (centro): preview del shader renderizado a FBO y mostrado via ImGui::Image
- **Controls** (derecha): placeholder para fase 2 + FPS overlay
## Build
```bash
./fn run build_cpp_linux_bash_infra shaders_lab
```
Binario: `cpp/build/linux/apps/shaders_lab/shaders_lab`
## Uniforms disponibles en los shaders
- `u_resolution`: vec2 — dimensiones del canvas en pixels
- `u_time`: float — segundos desde inicio de la app (ImGui::GetTime())
- `u_mouse`: vec2 — posición del mouse relativa al canvas, origen bottom-left
## Notas
No requiere dependencias externas más allá de lo ya vendorizado (GLFW, ImGui, OpenGL). Los fragment shaders se escriben sin `#version`, sin `out vec4 fragColor` ni declaraciones de uniforms — `compile_fragment()` los prepende automáticamente.
+5
View File
@@ -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)
+11
View File
@@ -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}
)
+115
View File
@@ -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 <chrono>
#include <string>
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<std::chrono::milliseconds>(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<float>(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;
}
+95
View File
@@ -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";
+30
View File
@@ -0,0 +1,30 @@
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glext.h>
#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
+14
View File
@@ -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
+45
View File
@@ -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);
```
+49
View File
@@ -0,0 +1,49 @@
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glext.h>
#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
+16
View File
@@ -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
+62
View File
@@ -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<ImTextureID>(static_cast<uintptr_t>(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.
+119
View File
@@ -0,0 +1,119 @@
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glext.h>
#include "gfx/gl_shader.h"
#include <cstdio>
#include <cstring>
#include <regex>
#include <string>
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:<line>:" format
std::regex re1(R"(ERROR:\s*\d+:(\d+):)");
// Try "0(<line>)" 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<size_t>(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<size_t>(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
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <string>
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
+66
View File
@@ -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` + `<GL/gl.h>` + `<GL/glext.h>` (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.
+85
View File
@@ -0,0 +1,85 @@
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glext.h>
#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<int>(avail.x);
int h = static_cast<int>(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<float>(w), static_cast<float>(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<float>(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<GLuint>(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
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <string>
#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
+61
View File
@@ -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.