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:
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
```
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user