diff --git a/cpp/apps/shaders_lab/CMakeLists.txt b/cpp/apps/shaders_lab/CMakeLists.txt index e3768411..f4769339 100644 --- a/cpp/apps/shaders_lab/CMakeLists.txt +++ b/cpp/apps/shaders_lab/CMakeLists.txt @@ -5,6 +5,8 @@ add_imgui_app(shaders_lab ${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/gfx/uniform_parser.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/uniform_panel.cpp ${CMAKE_SOURCE_DIR}/functions/core/fps_overlay.cpp ) target_include_directories(shaders_lab PRIVATE diff --git a/cpp/apps/shaders_lab/main.cpp b/cpp/apps/shaders_lab/main.cpp index e996433c..934d4c0d 100644 --- a/cpp/apps/shaders_lab/main.cpp +++ b/cpp/apps/shaders_lab/main.cpp @@ -3,11 +3,14 @@ #include "gfx/shader_canvas.h" #include "gfx/gl_shader.h" +#include "gfx/uniform_parser.h" +#include "gfx/uniform_panel.h" #include "core/fps_overlay.h" #include "seed_shaders.h" #include #include +#include static fn::gfx::ShaderCanvas g_canvas; static std::string g_source = PLASMA; @@ -15,10 +18,14 @@ 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 std::vector g_descs; +static fn::gfx::UniformStore g_store; static void try_compile() { auto r = fn::gfx::compile_fragment(g_source); if (r.ok) { + g_descs = fn::gfx::parse_uniforms(g_source); + fn::gfx::uniforms_sync(g_store, g_descs); fn::gfx::canvas_set_program(g_canvas, r.program); g_last_err.clear(); g_last_err_line = -1; @@ -91,13 +98,16 @@ static void render() { // --- Canvas panel --- if (ImGui::Begin("Canvas")) { - fn::gfx::canvas_render(g_canvas, static_cast(ImGui::GetTime())); + fn::gfx::canvas_render(g_canvas, static_cast(ImGui::GetTime()), + [](unsigned int program) { + fn::gfx::uniforms_apply(g_store, g_descs, program); + }); } ImGui::End(); // --- Controls panel --- if (ImGui::Begin("Controls")) { - ImGui::TextDisabled("Controls (fase 2)"); + fn::gfx::uniforms_panel(g_store, g_descs); ImGui::Spacing(); fps_overlay(); } diff --git a/cpp/apps/shaders_lab/seed_shaders.h b/cpp/apps/shaders_lab/seed_shaders.h index 0eab0412..7cf4a3f4 100644 --- a/cpp/apps/shaders_lab/seed_shaders.h +++ b/cpp/apps/shaders_lab/seed_shaders.h @@ -4,24 +4,14 @@ // compile_fragment() prepends those automatically. static const char* PLASMA = R"glsl( +uniform float u_speed; // @slider min=0.1 max=5 default=1 +uniform vec3 u_color; // @color default=0.5,0.2,0.8 + 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); + float t = u_time * u_speed; + vec3 c = u_color * (0.5 + 0.5 * cos(t + uv.xyx + vec3(0.0, 2.0, 4.0))); + fragColor = vec4(c, 1.0); } )glsl"; diff --git a/cpp/functions/gfx/gl_loader.cpp b/cpp/functions/gfx/gl_loader.cpp index 1b8eef09..6c73f818 100644 --- a/cpp/functions/gfx/gl_loader.cpp +++ b/cpp/functions/gfx/gl_loader.cpp @@ -26,7 +26,10 @@ PFNGLGETUNIFORMLOCATIONPROC fn_glGetUniformLocation = nullptr; PFNGLLINKPROGRAMPROC fn_glLinkProgram = nullptr; PFNGLSHADERSOURCEPROC fn_glShaderSource = nullptr; PFNGLUNIFORM1FPROC fn_glUniform1f = nullptr; +PFNGLUNIFORM1IPROC fn_glUniform1i = nullptr; PFNGLUNIFORM2FPROC fn_glUniform2f = nullptr; +PFNGLUNIFORM3FPROC fn_glUniform3f = nullptr; +PFNGLUNIFORM4FPROC fn_glUniform4f = nullptr; PFNGLUSEPROGRAMPROC fn_glUseProgram = nullptr; namespace fn::gfx { @@ -60,7 +63,10 @@ bool gl_loader_init() { LOAD(glLinkProgram); LOAD(glShaderSource); LOAD(glUniform1f); + LOAD(glUniform1i); LOAD(glUniform2f); + LOAD(glUniform3f); + LOAD(glUniform4f); LOAD(glUseProgram); #undef LOAD diff --git a/cpp/functions/gfx/gl_loader.h b/cpp/functions/gfx/gl_loader.h index 3a9ca2db..db36d5ac 100644 --- a/cpp/functions/gfx/gl_loader.h +++ b/cpp/functions/gfx/gl_loader.h @@ -32,7 +32,10 @@ extern PFNGLLINKPROGRAMPROC fn_glLinkProgram; extern PFNGLSHADERSOURCEPROC fn_glShaderSource; extern PFNGLUNIFORM1FPROC fn_glUniform1f; + extern PFNGLUNIFORM1IPROC fn_glUniform1i; extern PFNGLUNIFORM2FPROC fn_glUniform2f; + extern PFNGLUNIFORM3FPROC fn_glUniform3f; + extern PFNGLUNIFORM4FPROC fn_glUniform4f; extern PFNGLUSEPROGRAMPROC fn_glUseProgram; #define glAttachShader fn_glAttachShader @@ -59,7 +62,10 @@ #define glLinkProgram fn_glLinkProgram #define glShaderSource fn_glShaderSource #define glUniform1f fn_glUniform1f + #define glUniform1i fn_glUniform1i #define glUniform2f fn_glUniform2f + #define glUniform3f fn_glUniform3f + #define glUniform4f fn_glUniform4f #define glUseProgram fn_glUseProgram #else #define GL_GLEXT_PROTOTYPES diff --git a/cpp/functions/gfx/shader_canvas.cpp b/cpp/functions/gfx/shader_canvas.cpp index 3b5ddc16..37798a52 100644 --- a/cpp/functions/gfx/shader_canvas.cpp +++ b/cpp/functions/gfx/shader_canvas.cpp @@ -1,6 +1,7 @@ #include "gfx/gl_loader.h" #include "gfx/shader_canvas.h" #include "imgui.h" +#include namespace fn::gfx { @@ -17,7 +18,8 @@ void canvas_set_program(ShaderCanvas& c, unsigned int program) { c.program = program; } -void canvas_render(ShaderCanvas& c, float time_seconds) { +void canvas_render(ShaderCanvas& c, float time_seconds, + const std::function& uniforms_fn) { ImVec2 avail = ImGui::GetContentRegionAvail(); int w = static_cast(avail.x); int h = static_cast(avail.y); @@ -56,6 +58,8 @@ void canvas_render(ShaderCanvas& c, float time_seconds) { glUniform2f(loc_mouse, mx, my); } + if (uniforms_fn) uniforms_fn(c.program); + quad_draw(c.quad); glUseProgram(0); } diff --git a/cpp/functions/gfx/shader_canvas.h b/cpp/functions/gfx/shader_canvas.h index f481ccc4..578dba4d 100644 --- a/cpp/functions/gfx/shader_canvas.h +++ b/cpp/functions/gfx/shader_canvas.h @@ -1,4 +1,5 @@ #pragma once +#include #include #include "gfx/gl_framebuffer.h" #include "gfx/gl_shader.h" @@ -21,7 +22,9 @@ 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); +// uniforms_fn se invoca tras glUseProgram y antes de quad_draw, recibe el program ID. +void canvas_render(ShaderCanvas& c, float time_seconds, + const std::function& uniforms_fn = {}); void canvas_destroy(ShaderCanvas& c); diff --git a/cpp/functions/gfx/shader_canvas.md b/cpp/functions/gfx/shader_canvas.md index 94663bce..558caa0e 100644 --- a/cpp/functions/gfx/shader_canvas.md +++ b/cpp/functions/gfx/shader_canvas.md @@ -3,15 +3,16 @@ name: shader_canvas kind: component lang: cpp domain: gfx -version: "1.0.0" +version: "1.1.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." +signature: "void canvas_render(ShaderCanvas& c, float time_seconds, const std::function& uniforms_fn = {})" +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. Acepta callback opcional uniforms_fn invocado tras glUseProgram para uniforms custom." tags: [opengl, shader, canvas, imgui, fbo, gfx, component] uses_functions: - gl_shader_cpp_gfx - gl_framebuffer_cpp_gfx - fullscreen_quad_cpp_gfx + - uniform_panel_cpp_gfx uses_types: [] returns: [] returns_optional: false @@ -27,6 +28,8 @@ params: 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." + - name: uniforms_fn + desc: "Callback opcional invocado tras glUseProgram y antes de quad_draw. Recibe el program ID. Usar para aplicar uniforms custom (ej: uniforms_apply de uniform_panel)." output: "Dibuja ImGui::Image con la textura del FBO renderizado. El panel ImGui debe estar abierto (entre Begin/End). Ocupa GetContentRegionAvail()." --- diff --git a/cpp/functions/gfx/uniform_panel.cpp b/cpp/functions/gfx/uniform_panel.cpp new file mode 100644 index 00000000..06a50741 --- /dev/null +++ b/cpp/functions/gfx/uniform_panel.cpp @@ -0,0 +1,91 @@ +#include "gfx/uniform_panel.h" +#include "gfx/gl_loader.h" +#include "imgui.h" + +namespace fn::gfx { + +void uniforms_sync(UniformStore& store, const std::vector& descs) { + // Remove keys no longer present + for (auto it = store.values.begin(); it != store.values.end(); ) { + bool found = false; + for (auto& d : descs) if (d.name == it->first) { found = true; break; } + it = found ? std::next(it) : store.values.erase(it); + } + // Add new entries with defaults + for (auto& d : descs) { + if (store.values.count(d.name)) continue; + std::array arr = { + d.defaults[0], d.defaults[1], d.defaults[2], d.defaults[3] + }; + store.values[d.name] = arr; + } +} + +void uniforms_panel(UniformStore& store, const std::vector& descs) { + if (descs.empty()) { + ImGui::TextDisabled("Declare uniforms with @slider ... annotations."); + return; + } + + for (auto& d : descs) { + auto it = store.values.find(d.name); + if (it == store.values.end()) continue; + float* v = it->second.data(); + + switch (d.widget) { + case WidgetKind::Slider: { + if (d.glsl_type == GLSLType::Int) { + int tmp = static_cast(v[0]); + if (ImGui::SliderInt(d.name.c_str(), &tmp, + static_cast(d.min[0]), + static_cast(d.max[0]))) { + v[0] = static_cast(tmp); + } + } else { + ImGuiSliderFlags flags = d.log_scale ? ImGuiSliderFlags_Logarithmic : 0; + ImGui::SliderFloat(d.name.c_str(), &v[0], d.min[0], d.max[0], "%.3f", flags); + } + break; + } + case WidgetKind::Color: { + if (d.glsl_type == GLSLType::Vec4) + ImGui::ColorEdit4(d.name.c_str(), v); + else + ImGui::ColorEdit3(d.name.c_str(), v); + break; + } + case WidgetKind::Toggle: { + bool tmp = v[0] != 0.0f; + if (ImGui::Checkbox(d.name.c_str(), &tmp)) + v[0] = tmp ? 1.0f : 0.0f; + break; + } + case WidgetKind::XY: { + ImGui::SliderFloat2(d.name.c_str(), v, d.min[0], d.max[0]); + break; + } + } + } +} + +void uniforms_apply(const UniformStore& store, const std::vector& descs, unsigned int program) { + for (auto& d : descs) { + auto it = store.values.find(d.name); + if (it == store.values.end()) continue; + const float* v = it->second.data(); + + GLint loc = glGetUniformLocation(program, d.name.c_str()); + if (loc < 0) continue; + + switch (d.glsl_type) { + case GLSLType::Float: glUniform1f(loc, v[0]); break; + case GLSLType::Int: glUniform1i(loc, static_cast(v[0])); break; + case GLSLType::Bool: glUniform1i(loc, v[0] != 0.0f ? 1 : 0); break; + case GLSLType::Vec2: glUniform2f(loc, v[0], v[1]); break; + case GLSLType::Vec3: glUniform3f(loc, v[0], v[1], v[2]); break; + case GLSLType::Vec4: glUniform4f(loc, v[0], v[1], v[2], v[3]); break; + } + } +} + +} // namespace fn::gfx diff --git a/cpp/functions/gfx/uniform_panel.h b/cpp/functions/gfx/uniform_panel.h new file mode 100644 index 00000000..6c9ada42 --- /dev/null +++ b/cpp/functions/gfx/uniform_panel.h @@ -0,0 +1,24 @@ +#pragma once +#include +#include +#include +#include + +#include "gfx/uniform_parser.h" + +namespace fn::gfx { + +struct UniformStore { + std::unordered_map> values; +}; + +// Mantiene el store sincronizado con los descriptores actuales. +void uniforms_sync(UniformStore& store, const std::vector& descs); + +// Renderiza widgets ImGui en el panel actual (llamar entre Begin/End). +void uniforms_panel(UniformStore& store, const std::vector& descs); + +// Llama glUniform* sobre el programa GL activo (el caller debe haber llamado glUseProgram). +void uniforms_apply(const UniformStore& store, const std::vector& descs, unsigned int program); + +} // namespace fn::gfx diff --git a/cpp/functions/gfx/uniform_panel.md b/cpp/functions/gfx/uniform_panel.md new file mode 100644 index 00000000..0530ace0 --- /dev/null +++ b/cpp/functions/gfx/uniform_panel.md @@ -0,0 +1,60 @@ +--- +name: uniform_panel +kind: component +lang: cpp +domain: gfx +version: "1.0.0" +purity: impure +signature: "void uniforms_panel(UniformStore&, const std::vector&)" +description: "Panel ImGui auto-generado a partir de UniformDescriptor. Sincroniza un UniformStore con los descriptores, renderiza widgets (slider/color/toggle/xy) y aplica los valores al programa GL activo vía glUniform*." +tags: [opengl, shader, uniforms, imgui, controls, gfx, component] +uses_functions: + - uniform_parser_cpp_gfx + - gl_loader_cpp_gfx +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [imgui, gl_loader, uniform_parser, array, unordered_map] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/gfx/uniform_panel.cpp" +framework: imgui +params: + - name: store + desc: "Mapa nombre→array con los valores actuales de cada uniform. Se modifica in-place al interactuar con los widgets." + - name: descs + desc: "Descriptores parseados por parse_uniforms(). Determinan qué widgets se renderizan y con qué rangos." + - name: program + desc: "(uniforms_apply) ID del programa GL sobre el que aplicar los valores. Debe estar activo con glUseProgram antes de llamar." +output: "Dibuja los widgets ImGui en el panel actual. uniforms_apply llama glUniform* actualizando los uniforms del programa para el frame actual." +--- + +# uniform_panel + +Tres funciones que forman el ciclo completo de controles de uniforms: + +``` +parse_uniforms(src) → descs +uniforms_sync(store, descs) // al compilar shader +uniforms_panel(store, descs) // cada frame, dentro de ImGui::Begin/End +uniforms_apply(store, descs, program) // cada frame, dentro de canvas_render callback +``` + +## Widgets por tipo + +| GLSLType | WidgetKind | ImGui call | +|----------|------------|-------------------------------------| +| Float | Slider | SliderFloat (+ flag Logarithmic) | +| Int | Slider | SliderInt | +| Bool | Toggle | Checkbox | +| Vec2 | XY | SliderFloat2 | +| Vec3 | Color | ColorEdit3 | +| Vec4 | Color | ColorEdit4 | + +## Notas + +- `uniforms_sync` elimina entradas obsoletas y añade nuevas con sus defaults. Preserva valores actuales para uniforms que siguen presentes. +- Si no hay descriptores, `uniforms_panel` muestra `"Declare uniforms with @slider ... annotations."`. +- `uniforms_apply` usa `glGetUniformLocation` por nombre; si no existe (loc < 0), lo salta silenciosamente. diff --git a/cpp/functions/gfx/uniform_parser.cpp b/cpp/functions/gfx/uniform_parser.cpp new file mode 100644 index 00000000..f7262c28 --- /dev/null +++ b/cpp/functions/gfx/uniform_parser.cpp @@ -0,0 +1,248 @@ +#include "gfx/uniform_parser.h" + +#include +#include +#include +#include +#include +#include + +namespace fn::gfx { + +static const std::unordered_set k_reserved = { + "u_resolution", "u_time", "u_mouse" +}; + +// Parse a comma-separated list of up to 4 floats into out[]. Returns count parsed. +static int parse_floats(const std::string& s, float out[4]) { + int n = 0; + std::istringstream ss(s); + std::string tok; + while (n < 4 && std::getline(ss, tok, ',')) { + try { out[n++] = std::stof(tok); } catch (...) {} + } + return n; +} + +// Parse `key=value` pairs from annotation tail. +static std::unordered_map parse_props(const std::string& tail) { + std::unordered_map props; + std::regex kv_re(R"((\w+)=([^\s]+))"); + auto begin = std::sregex_iterator(tail.begin(), tail.end(), kv_re); + auto end = std::sregex_iterator(); + for (auto it = begin; it != end; ++it) { + props[(*it)[1].str()] = (*it)[2].str(); + } + return props; +} + +static void apply_defaults_for_type(UniformDescriptor& d) { + switch (d.glsl_type) { + case GLSLType::Float: + d.widget = WidgetKind::Slider; + d.min[0] = 0.0f; d.max[0] = 1.0f; + d.defaults[0] = 0.0f; + break; + case GLSLType::Int: + d.widget = WidgetKind::Slider; + d.min[0] = 0.0f; d.max[0] = 10.0f; + d.step = 1.0f; + d.defaults[0] = 0.0f; + break; + case GLSLType::Bool: + d.widget = WidgetKind::Toggle; + d.default_bool = false; + d.defaults[0] = 0.0f; + break; + case GLSLType::Vec2: + d.widget = WidgetKind::XY; + d.min[0] = d.min[1] = 0.0f; + d.max[0] = d.max[1] = 1.0f; + d.defaults[0] = d.defaults[1] = 0.5f; + break; + case GLSLType::Vec3: + d.widget = WidgetKind::Color; + d.defaults[0] = d.defaults[1] = d.defaults[2] = 1.0f; + break; + case GLSLType::Vec4: + d.widget = WidgetKind::Color; + d.defaults[0] = d.defaults[1] = d.defaults[2] = d.defaults[3] = 1.0f; + break; + } +} + +static void apply_props(UniformDescriptor& d, const std::unordered_map& props) { + auto get = [&](const char* k) -> const std::string* { + auto it = props.find(k); + return it != props.end() ? &it->second : nullptr; + }; + + if (auto* v = get("min")) { + float tmp[4] = {d.min[0], d.min[1], d.min[2], d.min[3]}; + int n = parse_floats(*v, tmp); + for (int i = 0; i < 4; i++) d.min[i] = (i < n) ? tmp[i] : tmp[n > 0 ? n-1 : 0]; + } + if (auto* v = get("max")) { + float tmp[4] = {d.max[0], d.max[1], d.max[2], d.max[3]}; + int n = parse_floats(*v, tmp); + for (int i = 0; i < 4; i++) d.max[i] = (i < n) ? tmp[i] : tmp[n > 0 ? n-1 : 0]; + } + if (auto* v = get("default")) { + if (d.glsl_type == GLSLType::Bool) { + d.default_bool = (*v == "true" || *v == "1"); + d.defaults[0] = d.default_bool ? 1.0f : 0.0f; + } else { + float tmp[4] = {}; + int n = parse_floats(*v, tmp); + for (int i = 0; i < 4; i++) d.defaults[i] = (i < n) ? tmp[i] : 0.0f; + } + } + if (auto* v = get("step")) { + try { d.step = std::stof(*v); } catch (...) {} + } + if (auto* v = get("log")) { + d.log_scale = (*v == "true"); + } +} + +std::vector parse_uniforms(const std::string& glsl_source) { + // Match: uniform ; optionally followed by // @ + static const std::regex line_re( + R"(^\s*uniform\s+(\w+)\s+(\w+)\s*;(?:\s*//\s*@(\w+)(.*))?\s*$)" + ); + + std::vector result; + std::istringstream stream(glsl_source); + std::string line; + + while (std::getline(stream, line)) { + std::smatch m; + if (!std::regex_match(line, m, line_re)) continue; + + std::string type_str = m[1].str(); + std::string name_str = m[2].str(); + bool has_annotation = m[3].matched; + std::string widget_str = has_annotation ? m[3].str() : ""; + std::string props_str = has_annotation ? m[4].str() : ""; + + // Skip reserved and unsupported + if (k_reserved.count(name_str)) continue; + if (type_str == "sampler2D") continue; + + UniformDescriptor d; + d.name = name_str; + + // Map GLSL type + if (type_str == "float") d.glsl_type = GLSLType::Float; + else if (type_str == "int") d.glsl_type = GLSLType::Int; + else if (type_str == "bool") d.glsl_type = GLSLType::Bool; + else if (type_str == "vec2") d.glsl_type = GLSLType::Vec2; + else if (type_str == "vec3") d.glsl_type = GLSLType::Vec3; + else if (type_str == "vec4") d.glsl_type = GLSLType::Vec4; + else continue; // unknown type — skip + + // Set defaults for type first + apply_defaults_for_type(d); + + if (has_annotation) { + // Override widget kind + if (widget_str == "slider") { + d.widget = (d.glsl_type == GLSLType::Vec2) ? WidgetKind::XY : WidgetKind::Slider; + } + else if (widget_str == "color") d.widget = WidgetKind::Color; + else if (widget_str == "toggle") d.widget = WidgetKind::Toggle; + else if (widget_str == "xy") d.widget = WidgetKind::XY; + + auto props = parse_props(props_str); + apply_props(d, props); + } + + result.push_back(d); + } + + return result; +} + +} // namespace fn::gfx + +// ────────────────────────────────────────────── +// Inline tests — compile with -DUNIFORM_PARSER_TEST +// ────────────────────────────────────────────── +#ifdef UNIFORM_PARSER_TEST +#include +#include + +using namespace fn::gfx; + +int main() { + // Test 1: sin anotación float → Slider 0..1 + { + auto v = parse_uniforms("uniform float u_x;"); + assert(v.size() == 1); + assert(v[0].name == "u_x"); + assert(v[0].glsl_type == GLSLType::Float); + assert(v[0].widget == WidgetKind::Slider); + assert(v[0].min[0] == 0.0f); + assert(v[0].max[0] == 1.0f); + assert(v[0].defaults[0] == 0.0f); + printf("PASS: sin anotacion float -> Slider 0..1\n"); + } + + // Test 2: @slider min=0 max=5 default=1 sobre float + { + auto v = parse_uniforms("uniform float u_speed; // @slider min=0 max=5 default=1"); + assert(v.size() == 1); + assert(v[0].widget == WidgetKind::Slider); + assert(v[0].min[0] == 0.0f); + assert(v[0].max[0] == 5.0f); + assert(v[0].defaults[0] == 1.0f); + printf("PASS: @slider min=0 max=5 default=1 sobre float\n"); + } + + // Test 3: @color default=0.1,0.2,0.5 sobre vec3 + { + auto v = parse_uniforms("uniform vec3 u_color; // @color default=0.1,0.2,0.5"); + assert(v.size() == 1); + assert(v[0].glsl_type == GLSLType::Vec3); + assert(v[0].widget == WidgetKind::Color); + assert(v[0].defaults[0] == 0.1f); + assert(v[0].defaults[1] == 0.2f); + assert(v[0].defaults[2] == 0.5f); + printf("PASS: @color default=0.1,0.2,0.5 sobre vec3\n"); + } + + // Test 4: @xy min=-1 max=1 default=0,0 sobre vec2 + { + auto v = parse_uniforms("uniform vec2 u_pos; // @xy min=-1 max=1 default=0,0"); + assert(v.size() == 1); + assert(v[0].glsl_type == GLSLType::Vec2); + assert(v[0].widget == WidgetKind::XY); + assert(v[0].min[0] == -1.0f); + assert(v[0].max[0] == 1.0f); + assert(v[0].defaults[0] == 0.0f); + assert(v[0].defaults[1] == 0.0f); + printf("PASS: @xy min=-1 max=1 default=0,0 sobre vec2\n"); + } + + // Test 5: uniform vec2 u_resolution ignorado (reservado) + { + auto v = parse_uniforms("uniform vec2 u_resolution;"); + assert(v.empty()); + printf("PASS: uniform vec2 u_resolution ignorado\n"); + } + + // Test 6: @toggle default=true sobre bool + { + auto v = parse_uniforms("uniform bool u_debug; // @toggle default=true"); + assert(v.size() == 1); + assert(v[0].glsl_type == GLSLType::Bool); + assert(v[0].widget == WidgetKind::Toggle); + assert(v[0].default_bool == true); + assert(v[0].defaults[0] == 1.0f); + printf("PASS: @toggle default=true sobre bool\n"); + } + + printf("---\nAll 6 tests passed.\n"); + return 0; +} +#endif diff --git a/cpp/functions/gfx/uniform_parser.h b/cpp/functions/gfx/uniform_parser.h new file mode 100644 index 00000000..4211c78d --- /dev/null +++ b/cpp/functions/gfx/uniform_parser.h @@ -0,0 +1,28 @@ +#pragma once +#include +#include + +namespace fn::gfx { + +enum class GLSLType { Float, Int, Bool, Vec2, Vec3, Vec4 }; +enum class WidgetKind { Slider, Color, Toggle, XY }; + +struct UniformDescriptor { + std::string name; + GLSLType glsl_type = GLSLType::Float; + WidgetKind widget = WidgetKind::Slider; + float min[4] = {0, 0, 0, 0}; + float max[4] = {1, 1, 1, 1}; + float defaults[4] = {0, 0, 0, 0}; + float step = 0.0f; + bool log_scale = false; + bool default_bool = false; +}; + +// Parsea líneas `uniform ; // @ key=value ...` +// Ignora u_resolution, u_time, u_mouse (reservados). +// Ignora sampler2D silenciosamente. +// Sin anotación: defaults por tipo. +std::vector parse_uniforms(const std::string& glsl_source); + +} // namespace fn::gfx diff --git a/cpp/functions/gfx/uniform_parser.md b/cpp/functions/gfx/uniform_parser.md new file mode 100644 index 00000000..9e386d17 --- /dev/null +++ b/cpp/functions/gfx/uniform_parser.md @@ -0,0 +1,82 @@ +--- +name: uniform_parser +kind: function +lang: cpp +domain: gfx +version: "1.0.0" +purity: pure +signature: "std::vector parse_uniforms(const std::string& glsl_source)" +description: "Parsea un fragment shader GLSL y extrae descriptores de uniforms anotados con @slider/@color/@toggle/@xy. Ignora los uniforms reservados (u_resolution, u_time, u_mouse) y sampler2D. Sin anotación aplica defaults por tipo." +tags: [opengl, shader, uniforms, parser, imgui, gfx] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [regex, string, vector, sstream, unordered_map, unordered_set] +tested: true +tests: + - "sin anotacion float -> Slider 0..1" + - "@slider min=0 max=5 default=1 sobre float" + - "@color default=0.1,0.2,0.5 sobre vec3" + - "@xy min=-1 max=1 default=0,0 sobre vec2" + - "uniform vec2 u_resolution ignorado" + - "@toggle default=true sobre bool" +test_file_path: "cpp/functions/gfx/uniform_parser.cpp" +file_path: "cpp/functions/gfx/uniform_parser.cpp" +params: + - name: glsl_source + desc: "Código fuente GLSL completo del fragment shader. Se escanea línea a línea buscando declaraciones uniform con anotaciones opcionales." +output: "Vector de UniformDescriptor ordenado según aparición en el shader. Cada descriptor tiene nombre, tipo GLSL, widget ImGui, rangos min/max, valor por defecto y flags (log_scale, step, default_bool)." +--- + +# uniform_parser + +Parsea declaraciones `uniform ; // @ key=value ...` en GLSL. + +## Sintaxis de anotación + +```glsl +uniform float u_speed; // @slider min=0.1 max=5 default=1 +uniform vec3 u_color; // @color default=0.5,0.2,0.8 +uniform bool u_debug; // @toggle default=true +uniform vec2 u_offset; // @xy min=-1 max=1 default=0,0 +``` + +### Widgets disponibles + +| Widget | Tipos soportados | Props relevantes | +|----------|-------------------|---------------------------------------------| +| `slider` | float, int, vec2 | min, max, default, step, log=true | +| `color` | vec3, vec4 | default (lista comma-separated) | +| `toggle` | bool | default=true/false | +| `xy` | vec2 | min, max, default (dos componentes) | + +### Defaults por tipo sin anotación + +| Tipo | Widget | min | max | default | +|-------|--------|------|------|-------------| +| float | Slider | 0 | 1 | 0 | +| int | Slider | 0 | 10 | 0, step=1 | +| bool | Toggle | — | — | false | +| vec2 | XY | 0,0 | 1,1 | 0.5,0.5 | +| vec3 | Color | — | — | 1,1,1 | +| vec4 | Color | — | — | 1,1,1,1 | + +## Notas + +- `@slider` sobre `vec2` equivale a `@xy`. +- Props desconocidas o de otro widget se ignoran silenciosamente. +- No lanza excepciones; errores de parse usan defaults. +- `u_resolution`, `u_time`, `u_mouse` y `sampler2D` se ignoran. + +### Tests + +Build y ejecución del test inline: + +```bash +g++ -std=c++17 -DUNIFORM_PARSER_TEST -I cpp/functions \ + cpp/functions/gfx/uniform_parser.cpp \ + -o /tmp/uniform_parser_test \ + && /tmp/uniform_parser_test +```