diff --git a/apps/shaders_lab/shaders_lab.exe b/apps/shaders_lab/shaders_lab.exe index b94309fc..a3bc7490 100755 Binary files a/apps/shaders_lab/shaders_lab.exe and b/apps/shaders_lab/shaders_lab.exe differ diff --git a/cpp/apps/shaders_lab/CMakeLists.txt b/cpp/apps/shaders_lab/CMakeLists.txt index f4769339..af898697 100644 --- a/cpp/apps/shaders_lab/CMakeLists.txt +++ b/cpp/apps/shaders_lab/CMakeLists.txt @@ -7,6 +7,10 @@ add_imgui_app(shaders_lab ${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/gfx/dag_catalog.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/dag_compile.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/dag_uniforms.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/dag_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 934d4c0d..e2d5e0f8 100644 --- a/cpp/apps/shaders_lab/main.cpp +++ b/cpp/apps/shaders_lab/main.cpp @@ -5,6 +5,10 @@ #include "gfx/gl_shader.h" #include "gfx/uniform_parser.h" #include "gfx/uniform_panel.h" +#include "gfx/dag_catalog.h" +#include "gfx/dag_compile.h" +#include "gfx/dag_uniforms.h" +#include "gfx/dag_panel.h" #include "core/fps_overlay.h" #include "seed_shaders.h" @@ -12,6 +16,9 @@ #include #include +enum class AppMode { Code, Dag }; + +static AppMode g_mode = AppMode::Code; static fn::gfx::ShaderCanvas g_canvas; static std::string g_source = PLASMA; static std::string g_last_err; @@ -20,18 +27,35 @@ 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 std::vector g_pipeline; +static std::string g_dag_glsl; 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; + if (g_mode == AppMode::Code) { + 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; + } else { + g_last_err = r.err_msg; + g_last_err_line = r.err_line; + } } else { - g_last_err = r.err_msg; - g_last_err_line = r.err_line; + g_dag_glsl = fn::gfx::compile_dag_to_glsl(g_pipeline); + auto r = fn::gfx::compile_fragment(g_dag_glsl); + if (r.ok) { + g_descs.clear(); + g_store.values.clear(); + 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; + } } } @@ -45,6 +69,20 @@ static void load_preset(const char* src) { mark_dirty(); } +static void ensure_dag_default() { + if (g_pipeline.empty()) { + const fn::gfx::DagNodeDef* def = fn::gfx::dag_find("plasma"); + if (def) { + fn::gfx::DagStep step; + step.id = "n_default"; + step.name = def->name; + step.params = def->param_defaults; + g_pipeline.push_back(step); + mark_dirty(); + } + } +} + static void render() { if (!g_canvas.initialized) fn::gfx::canvas_init(g_canvas); @@ -59,38 +97,82 @@ static void render() { 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()) { + // --- Left panel: Code or DAG --- + const char* panel_title = (g_mode == AppMode::Code) ? "Code" : "DAG"; + if (ImGui::Begin(panel_title)) { + // Mode toggle topbar + { + bool code_active = (g_mode == AppMode::Code); + bool dag_active = (g_mode == AppMode::Dag); + if (code_active) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.5f, 0.8f, 1.0f)); + if (ImGui::Button("Code")) { + if (g_mode != AppMode::Code) { + g_mode = AppMode::Code; + mark_dirty(); + } + } + if (code_active) ImGui::PopStyleColor(); + ImGui::SameLine(); + if (dag_active) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.5f, 0.8f, 1.0f)); + if (ImGui::Button("DAG")) { + if (g_mode != AppMode::Dag) { + g_mode = AppMode::Dag; + ensure_dag_default(); + mark_dirty(); + } + } + if (dag_active) ImGui::PopStyleColor(); 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()); + } + + if (g_mode == AppMode::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); + 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()); + } + } + } else { + // DAG mode + bool topo_changed = fn::gfx::dag_panel(g_pipeline); + if (topo_changed) { + mark_dirty(); + } + + 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()); + } } } } @@ -98,16 +180,35 @@ static void render() { // --- Canvas panel --- if (ImGui::Begin("Canvas")) { - fn::gfx::canvas_render(g_canvas, static_cast(ImGui::GetTime()), - [](unsigned int program) { - fn::gfx::uniforms_apply(g_store, g_descs, program); - }); + if (g_mode == AppMode::Code) { + fn::gfx::canvas_render(g_canvas, static_cast(ImGui::GetTime()), + [](unsigned int program) { + fn::gfx::uniforms_apply(g_store, g_descs, program); + }); + } else { + fn::gfx::canvas_render(g_canvas, static_cast(ImGui::GetTime()), + [](unsigned int program) { + fn::gfx::dag_uniforms_apply(g_pipeline, program); + }); + } } ImGui::End(); // --- Controls panel --- if (ImGui::Begin("Controls")) { - fn::gfx::uniforms_panel(g_store, g_descs); + if (g_mode == AppMode::Code) { + fn::gfx::uniforms_panel(g_store, g_descs); + } else { + // Show generated GLSL read-only + if (ImGui::CollapsingHeader("Generated GLSL")) { + ImVec2 avail = ImGui::GetContentRegionAvail(); + ImGui::InputTextMultiline("##dag_glsl", + const_cast(g_dag_glsl.c_str()), + g_dag_glsl.size() + 1, + ImVec2(avail.x, std::min(avail.y, 400.0f)), + ImGuiInputTextFlags_ReadOnly); + } + } ImGui::Spacing(); fps_overlay(); } diff --git a/cpp/functions/gfx/dag_catalog.cpp b/cpp/functions/gfx/dag_catalog.cpp new file mode 100644 index 00000000..65685d2d --- /dev/null +++ b/cpp/functions/gfx/dag_catalog.cpp @@ -0,0 +1,233 @@ +#include "gfx/dag_catalog.h" +#include + +namespace fn::gfx { + +static const std::vector& build_catalog() { + static std::vector catalog = []() { + std::vector v; + + // ── Gen: solid ──────────────────────────────────────────────── + { + DagNodeDef n; + n.name = "solid"; + n.label = "solid"; + n.desc = "color constante"; + n.kind = DagKind::Gen; + n.param_names = {"r", "g", "b", ""}; + n.param_defaults = {0.35f, 0.25f, 0.55f, 0.0f}; + n.controls = { + { DagControl::Kind::Color, "color", {0, 1, 2}, 0.0f, 1.0f, 0.0f }, + }; + n.body_glsl = [](int idx) -> std::string { + std::string i = std::to_string(idx); + return " vec4 p = u_params[" + i + "];\n" + " return vec4(p.x, p.y, p.z, 1.0);"; + }; + v.push_back(std::move(n)); + } + + // ── Gen: gradient ───────────────────────────────────────────── + { + DagNodeDef n; + n.name = "gradient"; + n.label = "gradient"; + n.desc = "gradiente direccional"; + n.kind = DagKind::Gen; + n.param_names = {"angle", "hue", "", ""}; + n.param_defaults = {0.8f, 0.5f, 0.0f, 0.0f}; + n.controls = { + { DagControl::Kind::Slider, "angulo", {0, -1, -1}, 0.0f, 6.2832f, 0.01f }, + { DagControl::Kind::Slider, "tono", {1, -1, -1}, 0.0f, 1.0f, 0.01f }, + }; + n.body_glsl = [](int idx) -> std::string { + std::string i = std::to_string(idx); + return " vec4 p = u_params[" + i + "];\n" + " vec2 dir = vec2(cos(p.x), sin(p.x));\n" + " float t = dot(uv - 0.5, dir) + 0.5;\n" + " vec3 col = 0.5 + 0.5 * cos(6.28318 * (p.y + vec3(0.0, 0.33, 0.67) + t));\n" + " return vec4(col, 1.0);"; + }; + v.push_back(std::move(n)); + } + + // ── Gen: plasma ─────────────────────────────────────────────── + { + DagNodeDef n; + n.name = "plasma"; + n.label = "plasma"; + n.desc = "onda trigonometrica"; + n.kind = DagKind::Gen; + n.param_names = {"speed", "scale", "", ""}; + n.param_defaults = {1.0f, 2.0f, 0.0f, 0.0f}; + n.controls = { + { DagControl::Kind::Slider, "velocidad", {0, -1, -1}, 0.0f, 3.0f, 0.01f }, + { DagControl::Kind::Slider, "escala", {1, -1, -1}, 0.5f, 10.0f, 0.1f }, + }; + n.body_glsl = [](int idx) -> std::string { + std::string i = std::to_string(idx); + return " vec4 p = u_params[" + i + "];\n" + " vec3 col = 0.5 + 0.5 * cos(u_time * p.x + uv.xyx * p.y + vec3(0.0, 2.0, 4.0));\n" + " return vec4(col, 1.0);"; + }; + v.push_back(std::move(n)); + } + + // ── Gen: circle ─────────────────────────────────────────────── + { + DagNodeDef n; + n.name = "circle"; + n.label = "circle"; + n.desc = "sdf de circulo"; + n.kind = DagKind::Gen; + n.param_names = {"cx", "cy", "radius", "soft"}; + n.param_defaults = {0.0f, 0.0f, 0.35f, 0.01f}; + n.controls = { + { DagControl::Kind::XY, "centro", {0, 1, -1}, -0.8f, 0.8f, 0.01f }, + { DagControl::Kind::Slider, "radio", {2, -1, -1}, 0.0f, 1.0f, 0.01f }, + { DagControl::Kind::Slider, "suavidad", {3, -1, -1}, 0.001f, 0.1f, 0.001f }, + }; + n.body_glsl = [](int idx) -> std::string { + std::string i = std::to_string(idx); + return " vec4 p = u_params[" + i + "];\n" + " float aspect = u_resolution.x / u_resolution.y;\n" + " vec2 pos = vec2((uv.x - 0.5) * aspect - p.x, uv.y - 0.5 - p.y);\n" + " float d = length(pos) - p.z;\n" + " float fill = smoothstep(p.w, -p.w, d);\n" + " return mix(c, vec4(1.0), fill);"; + }; + v.push_back(std::move(n)); + } + + // ── Op: invert ──────────────────────────────────────────────── + { + DagNodeDef n; + n.name = "invert"; + n.label = "invert"; + n.desc = "1 - rgb"; + n.kind = DagKind::Op; + n.param_names = {"", "", "", ""}; + n.param_defaults = {0.0f, 0.0f, 0.0f, 0.0f}; + n.controls = {}; + n.body_glsl = [](int /*idx*/) -> std::string { + return " return vec4(1.0 - c.rgb, c.a);"; + }; + v.push_back(std::move(n)); + } + + // ── Op: gamma ───────────────────────────────────────────────── + { + DagNodeDef n; + n.name = "gamma"; + n.label = "gamma"; + n.desc = "pow(rgb, gamma)"; + n.kind = DagKind::Op; + n.param_names = {"gamma", "", "", ""}; + n.param_defaults = {1.0f, 0.0f, 0.0f, 0.0f}; + n.controls = { + { DagControl::Kind::Slider, "gamma", {0, -1, -1}, 0.1f, 4.0f, 0.01f }, + }; + n.body_glsl = [](int idx) -> std::string { + std::string i = std::to_string(idx); + return " vec4 p = u_params[" + i + "];\n" + " return vec4(pow(c.rgb, vec3(1.0 / max(p.x, 0.001))), c.a);"; + }; + v.push_back(std::move(n)); + } + + // ── Op: hueShift ────────────────────────────────────────────── + { + DagNodeDef n; + n.name = "hueShift"; + n.label = "hue shift"; + n.desc = "rotar matiz"; + n.kind = DagKind::Op; + n.param_names = {"h", "", "", ""}; + n.param_defaults = {0.0f, 0.0f, 0.0f, 0.0f}; + n.controls = { + { DagControl::Kind::Slider, "h", {0, -1, -1}, 0.0f, 1.0f, 0.01f }, + }; + n.body_glsl = [](int idx) -> std::string { + std::string i = std::to_string(idx); + return " vec4 p = u_params[" + i + "];\n" + " float a = 6.28318 * p.x;\n" + " float ca = cos(a), sa = sin(a);\n" + " mat3 hueMat = mat3(\n" + " vec3(0.299 + 0.701 * ca + 0.168 * sa, 0.587 - 0.587 * ca + 0.330 * sa, 0.114 - 0.114 * ca - 0.497 * sa),\n" + " vec3(0.299 - 0.299 * ca - 0.328 * sa, 0.587 + 0.413 * ca + 0.035 * sa, 0.114 - 0.114 * ca + 0.292 * sa),\n" + " vec3(0.299 - 0.300 * ca + 1.250 * sa, 0.587 - 0.588 * ca - 1.050 * sa, 0.114 + 0.886 * ca - 0.203 * sa)\n" + " );\n" + " return vec4(clamp(hueMat * c.rgb, 0.0, 1.0), c.a);"; + }; + v.push_back(std::move(n)); + } + + // ── Blend: mix ──────────────────────────────────────────────── + { + DagNodeDef n; + n.name = "blend_mix"; + n.label = "mix"; + n.desc = "interpolacion mix(a, b, t)"; + n.kind = DagKind::Blend; + n.param_names = {"t", "", "", ""}; + n.param_defaults = {0.5f, 0.0f, 0.0f, 0.0f}; + n.controls = { + { DagControl::Kind::Slider, "t", {0, -1, -1}, 0.0f, 1.0f, 0.01f }, + }; + n.body_glsl = [](int idx) -> std::string { + std::string i = std::to_string(idx); + return " vec4 p = u_params[" + i + "];\n" + " return mix(a, b, p.x);"; + }; + v.push_back(std::move(n)); + } + + // ── Blend: multiply ─────────────────────────────────────────── + { + DagNodeDef n; + n.name = "blend_multiply"; + n.label = "multiply"; + n.desc = "a * b"; + n.kind = DagKind::Blend; + n.param_names = {"", "", "", ""}; + n.param_defaults = {0.0f, 0.0f, 0.0f, 0.0f}; + n.controls = {}; + n.body_glsl = [](int /*idx*/) -> std::string { + return " return vec4(a.rgb * b.rgb, a.a);"; + }; + v.push_back(std::move(n)); + } + + // ── Blend: screen ───────────────────────────────────────────── + { + DagNodeDef n; + n.name = "blend_screen"; + n.label = "screen"; + n.desc = "1 - (1-a)(1-b)"; + n.kind = DagKind::Blend; + n.param_names = {"", "", "", ""}; + n.param_defaults = {0.0f, 0.0f, 0.0f, 0.0f}; + n.controls = {}; + n.body_glsl = [](int /*idx*/) -> std::string { + return " return vec4(1.0 - (1.0 - a.rgb) * (1.0 - b.rgb), a.a);"; + }; + v.push_back(std::move(n)); + } + + return v; + }(); + return catalog; +} + +const std::vector& dag_catalog() { + return build_catalog(); +} + +const DagNodeDef* dag_find(const std::string& name) { + for (const auto& n : dag_catalog()) { + if (n.name == name) return &n; + } + return nullptr; +} + +} // namespace fn::gfx diff --git a/cpp/functions/gfx/dag_catalog.h b/cpp/functions/gfx/dag_catalog.h new file mode 100644 index 00000000..0ebbae0e --- /dev/null +++ b/cpp/functions/gfx/dag_catalog.h @@ -0,0 +1,11 @@ +#pragma once +#include "gfx/dag_types.h" +#include + +namespace fn::gfx { + +const std::vector& dag_catalog(); + +const DagNodeDef* dag_find(const std::string& name); + +} // namespace fn::gfx diff --git a/cpp/functions/gfx/dag_catalog.md b/cpp/functions/gfx/dag_catalog.md new file mode 100644 index 00000000..41a4d58f --- /dev/null +++ b/cpp/functions/gfx/dag_catalog.md @@ -0,0 +1,46 @@ +--- +name: dag_catalog +kind: function +lang: cpp +domain: gfx +version: "1.0.0" +purity: pure +signature: "const std::vector& dag_catalog(); const DagNodeDef* dag_find(const std::string& name)" +description: "Catalogo global de nodos DAG para el pipeline de shaders. dag_catalog() devuelve referencia estable a los 10 nodos hardcoded (4 gen, 3 op, 3 blend) portados desde shader-dag-blends.jsx. dag_find() busca por nombre." +tags: [dag, shader, catalog, nodes, gfx, pipeline] +uses_functions: [] +uses_types: + - dag_types_cpp_gfx +returns: [] +returns_optional: false +error_type: "" +imports: [dag_types, string, vector] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/gfx/dag_catalog.cpp" +params: [] +output: "dag_catalog(): referencia const estable al vector de DagNodeDef (instancia estatica, no se invalida). dag_find(name): puntero al nodo con ese nombre o nullptr si no existe." +--- + +## Nodos incluidos + +### Generadores (4) +- **solid**: color constante RGB +- **gradient**: gradiente direccional con tono +- **plasma**: onda trigonometrica animada +- **circle**: SDF de circulo con suavizado + +### Operadores (3) +- **invert**: 1 - rgb +- **gamma**: correccion gamma pow(rgb, 1/g) +- **hueShift**: rotacion de matiz via matriz YIQ + +### Blends (3) +- **blend_mix**: interpolacion mix(a, b, t) +- **blend_multiply**: a.rgb * b.rgb +- **blend_screen**: 1 - (1-a)(1-b) + +## Notas + +Los cuerpos GLSL omiten las declaraciones de u_time, u_resolution, u_params — las proporciona el preamble de gl_shader::compile_fragment o compile_dag_to_glsl. El indice idx que recibe body_glsl es la posicion en el pipeline (para indexar u_params[idx]). diff --git a/cpp/functions/gfx/dag_compile.cpp b/cpp/functions/gfx/dag_compile.cpp new file mode 100644 index 00000000..8f29d329 --- /dev/null +++ b/cpp/functions/gfx/dag_compile.cpp @@ -0,0 +1,84 @@ +#include "gfx/dag_compile.h" +#include "gfx/dag_catalog.h" +#include +#include + +namespace fn::gfx { + +static constexpr int MAX_NODES = 16; + +std::string compile_dag_to_glsl(const std::vector& pipeline) { + const int n = static_cast(std::min(pipeline.size(), static_cast(MAX_NODES))); + std::ostringstream out; + + out << "uniform vec4 u_params[16];\n\n"; + + if (n == 0) { + out << "void main() {\n"; + out << " vec2 uv = gl_FragCoord.xy / u_resolution;\n"; + out << " (void)uv;\n"; + out << " fragColor = vec4(0.04, 0.04, 0.06, 1.0);\n"; + out << "}\n"; + return out.str(); + } + + for (int i = 0; i < n; ++i) { + const DagStep& step = pipeline[static_cast(i)]; + const DagNodeDef* def = dag_find(step.name); + if (!def) continue; + + if (def->kind == DagKind::Blend) { + out << "vec4 node_" << i << "(vec4 a, vec4 b, vec2 uv) {\n"; + } else { + out << "vec4 node_" << i << "(vec4 c, vec2 uv) {\n"; + } + out << def->body_glsl(i) << "\n"; + out << "}\n\n"; + } + + out << "void main() {\n"; + out << " vec2 uv = gl_FragCoord.xy / u_resolution;\n"; + out << " vec4 c = vec4(0.04, 0.04, 0.06, 1.0);\n"; + + for (int i = 0; i < n; ++i) { + const DagStep& step = pipeline[static_cast(i)]; + const DagNodeDef* def = dag_find(step.name); + if (!def) { + out << " vec4 out_" << i << " = (i > 0 ? out_" << (i-1) << " : c);\n"; + continue; + } + + std::string prev = (i == 0) ? "vec4(0.0, 0.0, 0.0, 1.0)" : "out_" + std::to_string(i - 1); + + if (def->kind == DagKind::Blend) { + int src_idx = -1; + if (!step.source_id.empty()) { + for (int j = 0; j < i; ++j) { + if (pipeline[static_cast(j)].id == step.source_id) { + src_idx = j; + break; + } + } + } + if (src_idx < 0 || src_idx >= i) { + src_idx = std::max(0, i - 2); + } + std::string src; + if (i == 0) { + src = prev; + } else { + src = "out_" + std::to_string(std::min(src_idx, i - 1)); + } + out << " vec4 out_" << i << " = node_" << i << "(" << prev << ", " << src << ", uv);\n"; + } else { + out << " vec4 out_" << i << " = node_" << i << "(" << prev << ", uv);\n"; + } + } + + out << " fragColor = out_" << (n - 1) << ";\n"; + out << "}\n"; + + return out.str(); +} + +} // namespace fn::gfx diff --git a/cpp/functions/gfx/dag_compile.h b/cpp/functions/gfx/dag_compile.h new file mode 100644 index 00000000..1b14ed29 --- /dev/null +++ b/cpp/functions/gfx/dag_compile.h @@ -0,0 +1,14 @@ +#pragma once +#include +#include +#include "gfx/dag_types.h" + +namespace fn::gfx { + +// Compila un pipeline DAG a GLSL 330 core completo (listo para gl_shader::compile_fragment). +// El preamble de gl_shader ya declara #version, fragColor, u_time, u_resolution, u_mouse. +// Este compilador emite uniform vec4 u_params[16], las funciones node_ y void main(). +// Si el pipeline esta vacio, emite un fragment que pinta gris oscuro. +std::string compile_dag_to_glsl(const std::vector& pipeline); + +} // namespace fn::gfx diff --git a/cpp/functions/gfx/dag_compile.md b/cpp/functions/gfx/dag_compile.md new file mode 100644 index 00000000..7b22135a --- /dev/null +++ b/cpp/functions/gfx/dag_compile.md @@ -0,0 +1,51 @@ +--- +name: dag_compile +kind: function +lang: cpp +domain: gfx +version: "1.0.0" +purity: pure +signature: "std::string compile_dag_to_glsl(const std::vector& pipeline)" +description: "Compila un pipeline DAG a GLSL 330 core listo para pasarle a gl_shader::compile_fragment. Emite uniform vec4 u_params[16], una funcion node_ por paso y void main() que encadena los outputs. Blends usan source_id para fan-in estable ante reorders." +tags: [dag, shader, glsl, compiler, gfx, pipeline] +uses_functions: + - dag_catalog_cpp_gfx +uses_types: + - dag_types_cpp_gfx +returns: [] +returns_optional: false +error_type: "" +imports: [dag_compile, dag_catalog, dag_types, string, vector, sstream, algorithm] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/gfx/dag_compile.cpp" +params: + - name: pipeline + desc: "Vector de DagStep. Cada paso tiene un nombre de nodo del catalogo, params array y source_id para blends." +output: "String GLSL que se pega tras el preamble de gl_shader (que ya declara #version 330 core, fragColor, u_time, u_resolution, u_mouse). Incluye uniform vec4 u_params[16], funciones node_ y void main()." +--- + +## Estructura del GLSL emitido + +```glsl +uniform vec4 u_params[16]; + +vec4 node_0(vec4 c, vec2 uv) { ... } +vec4 node_1(vec4 a, vec4 b, vec2 uv) { ... } // blend + +void main() { + vec2 uv = gl_FragCoord.xy / u_resolution; + vec4 c = vec4(0.04, 0.04, 0.06, 1.0); + vec4 out_0 = node_0(vec4(0.0, 0.0, 0.0, 1.0), uv); + vec4 out_1 = node_1(out_0, out_0, uv); + fragColor = out_1; +} +``` + +## Notas + +- El preamble de gl_shader::compile_fragment ya declara los 3 uniforms basicos. compile_dag_to_glsl NO los redeclara. +- Si el pipeline esta vacio, emite void main() que pinta gris oscuro (0.04, 0.04, 0.06). +- MAX_NODES = 16. Pipelines mas largos se truncan silenciosamente. +- source_id fallback: si el id no se encuentra o apunta a un indice >= idx, usa max(0, idx-2). diff --git a/cpp/functions/gfx/dag_panel.cpp b/cpp/functions/gfx/dag_panel.cpp new file mode 100644 index 00000000..af42e49d --- /dev/null +++ b/cpp/functions/gfx/dag_panel.cpp @@ -0,0 +1,228 @@ +#include "gfx/dag_panel.h" +#include "gfx/dag_catalog.h" +#include "imgui.h" +#include +#include + +namespace fn::gfx { + +static constexpr int MAX_NODES = 16; + +static uint64_t s_next_id = 1; + +static std::string make_id() { + return "n" + std::to_string(s_next_id++); +} + +static ImVec4 kind_color(DagKind kind) { + switch (kind) { + case DagKind::Gen: return ImVec4(0.25f, 0.55f, 0.90f, 1.0f); + case DagKind::Op: return ImVec4(0.65f, 0.40f, 0.90f, 1.0f); + case DagKind::Blend: return ImVec4(0.90f, 0.65f, 0.15f, 1.0f); + } + return ImVec4(1, 1, 1, 1); +} + +static const char* kind_label(DagKind kind) { + switch (kind) { + case DagKind::Gen: return "Gen"; + case DagKind::Op: return "Op"; + case DagKind::Blend: return "Blend"; + } + return "?"; +} + +bool dag_panel(std::vector& pipeline) { + bool changed = false; + + // Toolbar + int sz = static_cast(pipeline.size()); + if (ImGui::Button("+ Add Node")) { + ImGui::OpenPopup("dag_add_popup"); + } + ImGui::SameLine(); + ImGui::Text("%d/%d nodes", sz, MAX_NODES); + ImGui::SameLine(); + if (ImGui::Button("Clear") && !pipeline.empty()) { + ImGui::OpenPopup("dag_clear_confirm"); + } + + if (ImGui::BeginPopupModal("dag_clear_confirm", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("Vaciar el pipeline?"); + if (ImGui::Button("Si")) { + pipeline.clear(); + changed = true; + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("No")) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + if (ImGui::BeginPopup("dag_add_popup")) { + const char* kind_names[] = { "Gen", "Op", "Blend" }; + DagKind kinds[] = { DagKind::Gen, DagKind::Op, DagKind::Blend }; + for (int k = 0; k < 3; ++k) { + if (ImGui::BeginMenu(kind_names[k])) { + for (const auto& def : dag_catalog()) { + if (def.kind != kinds[k]) continue; + if (ImGui::MenuItem(def.label.c_str())) { + if (static_cast(pipeline.size()) < MAX_NODES) { + DagStep step; + step.id = make_id(); + step.name = def.name; + step.params = def.param_defaults; + if (def.kind == DagKind::Blend && !pipeline.empty()) { + int src = std::max(0, static_cast(pipeline.size()) - 2); + step.source_id = pipeline[static_cast(src)].id; + } + pipeline.push_back(step); + changed = true; + } + } + } + ImGui::EndMenu(); + } + } + ImGui::EndPopup(); + } + + ImGui::Separator(); + + // Pipeline list + int delete_idx = -1; + int move_up_idx = -1; + int move_down_idx = -1; + + ImGui::BeginChild("dag_pipeline_scroll", ImVec2(0, 0), false); + + for (int i = 0; i < static_cast(pipeline.size()); ++i) { + DagStep& step = pipeline[static_cast(i)]; + const DagNodeDef* def = dag_find(step.name); + if (!def) continue; + + ImVec4 col = kind_color(def->kind); + ImGui::PushID(i); + + ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(col.x*0.4f, col.y*0.4f, col.z*0.4f, 0.8f)); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(col.x*0.5f, col.y*0.5f, col.z*0.5f, 0.9f)); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(col.x*0.6f, col.y*0.6f, col.z*0.6f, 1.0f)); + + std::string header = std::string("#") + std::to_string(i) + " " + def->label + " [" + kind_label(def->kind) + "]"; + bool open = ImGui::CollapsingHeader(header.c_str()); + + ImGui::PopStyleColor(3); + + if (open) { + ImGui::Indent(8.0f); + + // Source selector for blends + if (def->kind == DagKind::Blend) { + std::vector prev_labels; + std::vector prev_indices; + for (int j = 0; j < i; ++j) { + const DagNodeDef* pdef = dag_find(pipeline[static_cast(j)].name); + std::string lbl = "out_" + std::to_string(j); + if (pdef) lbl += " (" + pdef->label + ")"; + prev_labels.push_back(lbl); + prev_indices.push_back(j); + } + + int current_src = std::max(0, i - 2); + if (!step.source_id.empty()) { + for (int j = 0; j < i; ++j) { + if (pipeline[static_cast(j)].id == step.source_id) { + current_src = j; + break; + } + } + } + + if (!prev_labels.empty()) { + std::vector items; + for (const auto& l : prev_labels) items.push_back(l.c_str()); + int sel = current_src; + if (ImGui::Combo("Source", &sel, items.data(), static_cast(items.size()))) { + if (sel >= 0 && sel < i) { + step.source_id = pipeline[static_cast(sel)].id; + changed = true; + } + } + } else { + ImGui::TextDisabled("(requiere al menos un nodo antes)"); + } + } + + // Controls + for (const auto& ctrl : def->controls) { + std::string uid_label = ctrl.label + "##" + step.id + std::to_string(&ctrl - def->controls.data()); + if (ctrl.kind == DagControl::Kind::Slider) { + int pidx = ctrl.param_idx[0]; + if (pidx >= 0 && pidx < 4) { + ImGui::SliderFloat(uid_label.c_str(), &step.params[static_cast(pidx)], ctrl.min, ctrl.max); + } + } else if (ctrl.kind == DagControl::Kind::XY) { + int px = ctrl.param_idx[0]; + int py = ctrl.param_idx[1]; + if (px >= 0 && px < 4 && py >= 0 && py < 4 && py == px + 1) { + ImGui::SliderFloat2(uid_label.c_str(), &step.params[static_cast(px)], ctrl.min, ctrl.max); + } + } else if (ctrl.kind == DagControl::Kind::Color) { + int pr = ctrl.param_idx[0]; + if (pr >= 0 && pr + 2 < 4) { + ImGui::ColorEdit3(uid_label.c_str(), &step.params[static_cast(pr)]); + } + } + } + + // Action buttons + ImGui::Spacing(); + ImGui::BeginDisabled(i == 0); + if (ImGui::SmallButton("Move Up")) { + move_up_idx = i; + } + ImGui::EndDisabled(); + ImGui::SameLine(); + ImGui::BeginDisabled(i == static_cast(pipeline.size()) - 1); + if (ImGui::SmallButton("Move Down")) { + move_down_idx = i; + } + ImGui::EndDisabled(); + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.15f, 0.15f, 0.8f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.2f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); + if (ImGui::SmallButton("Delete")) { + delete_idx = i; + } + ImGui::PopStyleColor(3); + + ImGui::Unindent(8.0f); + } + + ImGui::PopID(); + ImGui::Spacing(); + } + + ImGui::EndChild(); + + // Apply deferred mutations + if (delete_idx >= 0) { + pipeline.erase(pipeline.begin() + delete_idx); + changed = true; + } + if (move_up_idx > 0) { + std::swap(pipeline[static_cast(move_up_idx)], pipeline[static_cast(move_up_idx - 1)]); + changed = true; + } + if (move_down_idx >= 0 && move_down_idx < static_cast(pipeline.size()) - 1) { + std::swap(pipeline[static_cast(move_down_idx)], pipeline[static_cast(move_down_idx + 1)]); + changed = true; + } + + return changed; +} + +} // namespace fn::gfx diff --git a/cpp/functions/gfx/dag_panel.h b/cpp/functions/gfx/dag_panel.h new file mode 100644 index 00000000..a6582200 --- /dev/null +++ b/cpp/functions/gfx/dag_panel.h @@ -0,0 +1,13 @@ +#pragma once +#include +#include "gfx/dag_types.h" + +namespace fn::gfx { + +// Renderiza el panel del pipeline dentro del ImGui::Begin actual. +// Modifica pipeline in-place. Devuelve true si la topologia cambio +// (anadir/borrar/reordenar/cambiar source_id). +// Cambios de param (sliders/color/xy) no cuentan como topology change. +bool dag_panel(std::vector& pipeline); + +} // namespace fn::gfx diff --git a/cpp/functions/gfx/dag_panel.md b/cpp/functions/gfx/dag_panel.md new file mode 100644 index 00000000..e7648306 --- /dev/null +++ b/cpp/functions/gfx/dag_panel.md @@ -0,0 +1,52 @@ +--- +name: dag_panel +kind: component +lang: cpp +domain: gfx +version: "1.0.0" +purity: impure +signature: "bool dag_panel(std::vector& pipeline)" +description: "Panel ImGui para editar un pipeline DAG de shaders. Toolbar con Add Node (popup por kind) y Clear. Lista scrollable de pasos con CollapsingHeader coloreado por kind, selector de source para blends, widgets de control (Slider/XY/Color) y botones Move Up/Down/Delete. Devuelve true si la topologia cambio." +tags: [dag, shader, imgui, pipeline, editor, gfx, component] +uses_functions: + - dag_catalog_cpp_gfx +uses_types: + - dag_types_cpp_gfx +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [dag_panel, dag_catalog, dag_types, imgui, algorithm, string, vector] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/gfx/dag_panel.cpp" +framework: imgui +params: + - name: pipeline + desc: "Vector de DagStep modificado in-place. Add/Delete/Move cambian la topologia. Los sliders solo cambian params." +output: "true si la topologia cambio (add/delete/move/source_id change). false si solo cambiaron valores de params (sliders). El caller usa este flag para decidir si recompilar el shader." +--- + +## Estructura del panel + +``` +[+ Add Node] [N/16 nodes] [Clear] +───────────────────────────────────── +▼ #0 plasma [Gen] + velocidad [slider] + escala [slider] + [Move Up(disabled)] [Move Down] [Delete] + +▶ #1 blend_mix [Blend] + ... +``` + +## Colores por kind + +- Gen: azul (0.25, 0.55, 0.90) +- Op: violeta (0.65, 0.40, 0.90) +- Blend: ambar (0.90, 0.65, 0.15) + +## Notas + +Los IDs de paso se generan con contador estatico `s_next_id`. Unicos dentro de la sesion. Al anadir un Blend, asigna automaticamente el source_id al paso de hace dos (o el primero si el pipeline tiene menos de 2 pasos). Las mutaciones (delete/move) se aplican al final del loop para no invalidar iteradores. diff --git a/cpp/functions/gfx/dag_types.h b/cpp/functions/gfx/dag_types.h new file mode 100644 index 00000000..1851b126 --- /dev/null +++ b/cpp/functions/gfx/dag_types.h @@ -0,0 +1,39 @@ +#pragma once +#include +#include +#include +#include + +namespace fn::gfx { + +enum class DagKind { Gen, Op, Blend }; + +struct DagControl { + enum class Kind { Slider, XY, Color }; + Kind kind; + std::string label; + std::array param_idx{-1, -1, -1}; + float min = 0.0f; + float max = 1.0f; + float step = 0.0f; +}; + +struct DagNodeDef { + std::string name; + std::string label; + std::string desc; + DagKind kind = DagKind::Gen; + std::array param_names{"", "", "", ""}; + std::array param_defaults{0, 0, 0, 0}; + std::vector controls; + std::function body_glsl; +}; + +struct DagStep { + std::string id; + std::string name; + std::array params{0, 0, 0, 0}; + std::string source_id; +}; + +} // namespace fn::gfx diff --git a/cpp/functions/gfx/dag_uniforms.cpp b/cpp/functions/gfx/dag_uniforms.cpp new file mode 100644 index 00000000..a3326ced --- /dev/null +++ b/cpp/functions/gfx/dag_uniforms.cpp @@ -0,0 +1,27 @@ +#include "gfx/dag_uniforms.h" +#include "gfx/gl_loader.h" +#include +#include + +namespace fn::gfx { + +static constexpr int MAX_NODES = 16; + +void dag_uniforms_apply(const std::vector& pipeline, unsigned int program) { + float data[MAX_NODES * 4]; + std::memset(data, 0, sizeof(data)); + + const int n = static_cast(std::min(pipeline.size(), static_cast(MAX_NODES))); + for (int i = 0; i < n; ++i) { + const auto& step = pipeline[static_cast(i)]; + data[i * 4 + 0] = step.params[0]; + data[i * 4 + 1] = step.params[1]; + data[i * 4 + 2] = step.params[2]; + data[i * 4 + 3] = step.params[3]; + } + + GLint loc = glGetUniformLocation(program, "u_params"); + if (loc >= 0) glUniform4fv(loc, MAX_NODES, data); +} + +} // namespace fn::gfx diff --git a/cpp/functions/gfx/dag_uniforms.h b/cpp/functions/gfx/dag_uniforms.h new file mode 100644 index 00000000..6d5b6aa7 --- /dev/null +++ b/cpp/functions/gfx/dag_uniforms.h @@ -0,0 +1,11 @@ +#pragma once +#include +#include "gfx/dag_types.h" + +namespace fn::gfx { + +// Sube los params del pipeline al shader via glUniform4fv(u_params, 16, data). +// El programa GL debe estar activo (glUseProgram'd) por el caller. +void dag_uniforms_apply(const std::vector& pipeline, unsigned int program); + +} // namespace fn::gfx diff --git a/cpp/functions/gfx/dag_uniforms.md b/cpp/functions/gfx/dag_uniforms.md new file mode 100644 index 00000000..71a77064 --- /dev/null +++ b/cpp/functions/gfx/dag_uniforms.md @@ -0,0 +1,33 @@ +--- +name: dag_uniforms +kind: function +lang: cpp +domain: gfx +version: "1.0.0" +purity: impure +signature: "void dag_uniforms_apply(const std::vector& pipeline, unsigned int program)" +description: "Sube los params del pipeline al shader activo via glUniform4fv. Construye un float[64] con los params de cada DagStep (hasta 16 pasos) y llama glUniform4fv(u_params, 16, data)." +tags: [dag, shader, uniforms, opengl, gfx, pipeline] +uses_functions: + - gl_loader_cpp_gfx +uses_types: + - dag_types_cpp_gfx +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [dag_uniforms, gl_loader, dag_types, vector, algorithm, cstring] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/gfx/dag_uniforms.cpp" +params: + - name: pipeline + desc: "Vector de DagStep. params[0..3] de cada step se copian en data[i*4..i*4+3]. Steps mas alla de MAX_NODES=16 se ignoran." + - name: program + desc: "ID del programa GL activo. Debe haber sido activado con glUseProgram antes de llamar." +output: "Efecto lateral: actualiza el uniform u_params[16] en el programa GL activo para el frame actual." +--- + +## Notas + +El array data[64] se inicializa a 0 antes de copiar, por lo que steps no usados quedan en cero. El caller es responsable de activar el programa antes de llamar. diff --git a/cpp/functions/gfx/gl_loader.cpp b/cpp/functions/gfx/gl_loader.cpp index 6c73f818..f62e1f66 100644 --- a/cpp/functions/gfx/gl_loader.cpp +++ b/cpp/functions/gfx/gl_loader.cpp @@ -30,6 +30,7 @@ PFNGLUNIFORM1IPROC fn_glUniform1i = nullptr; PFNGLUNIFORM2FPROC fn_glUniform2f = nullptr; PFNGLUNIFORM3FPROC fn_glUniform3f = nullptr; PFNGLUNIFORM4FPROC fn_glUniform4f = nullptr; +PFNGLUNIFORM4FVPROC fn_glUniform4fv = nullptr; PFNGLUSEPROGRAMPROC fn_glUseProgram = nullptr; namespace fn::gfx { @@ -67,6 +68,7 @@ bool gl_loader_init() { LOAD(glUniform2f); LOAD(glUniform3f); LOAD(glUniform4f); + LOAD(glUniform4fv); LOAD(glUseProgram); #undef LOAD diff --git a/cpp/functions/gfx/gl_loader.h b/cpp/functions/gfx/gl_loader.h index db36d5ac..fd63f2f0 100644 --- a/cpp/functions/gfx/gl_loader.h +++ b/cpp/functions/gfx/gl_loader.h @@ -36,6 +36,7 @@ extern PFNGLUNIFORM2FPROC fn_glUniform2f; extern PFNGLUNIFORM3FPROC fn_glUniform3f; extern PFNGLUNIFORM4FPROC fn_glUniform4f; + extern PFNGLUNIFORM4FVPROC fn_glUniform4fv; extern PFNGLUSEPROGRAMPROC fn_glUseProgram; #define glAttachShader fn_glAttachShader @@ -66,6 +67,7 @@ #define glUniform2f fn_glUniform2f #define glUniform3f fn_glUniform3f #define glUniform4f fn_glUniform4f + #define glUniform4fv fn_glUniform4fv #define glUseProgram fn_glUseProgram #else #define GL_GLEXT_PROTOTYPES diff --git a/types/gfx/dag_types.md b/types/gfx/dag_types.md new file mode 100644 index 00000000..79ae7329 --- /dev/null +++ b/types/gfx/dag_types.md @@ -0,0 +1,45 @@ +--- +name: dag_types +lang: cpp +domain: gfx +version: "1.0.0" +algebraic: product +definition: | + enum class DagKind { Gen, Op, Blend }; + + struct DagControl { + enum class Kind { Slider, XY, Color }; + Kind kind; + std::string label; + std::array param_idx{-1, -1, -1}; + float min = 0.0f; + float max = 1.0f; + float step = 0.0f; + }; + + struct DagNodeDef { + std::string name; + std::string label; + std::string desc; + DagKind kind = DagKind::Gen; + std::array param_names{"", "", "", ""}; + std::array param_defaults{0, 0, 0, 0}; + std::vector controls; + std::function body_glsl; + }; + + struct DagStep { + std::string id; + std::string name; + std::array params{0, 0, 0, 0}; + std::string source_id; + }; +description: "Tipos del DAG de shaders: DagKind (gen/op/blend), DagControl (descriptor de widget UI), DagNodeDef (entrada del catalogo de nodos con body GLSL), DagStep (paso del pipeline con params y source_id estable ante reorders)." +tags: [dag, shader, gfx, pipeline, nodes, types] +uses_types: [] +file_path: "cpp/functions/gfx/dag_types.h" +--- + +## Notas + +Header-only. DagNodeDef::body_glsl es un callable que recibe el indice del nodo en el pipeline y devuelve el cuerpo GLSL de la funcion (sin llaves). DagStep::source_id es el id estable del paso fuente para blends; sobrevive a reordenamientos del pipeline porque referencia por id, no por indice.