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 5546ce6453
commit 042bb43b37
16 changed files with 823 additions and 0 deletions
+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.