From a7aecbadd1d07071a9463851df49d559f91f5ce3 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 20 May 2026 08:52:03 +0200 Subject: [PATCH] Fase 8c: postpro (bloom + flicker + background) en SDL_gpu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renderiza la escena de líneas a una textura offscreen y aplica un pase final de postpro que compone la imagen al swapchain. El shader del postpro hace tres cosas: - Bloom: kernel gaussiano 5×5 con high-pass por luminancia. Configurable vía intensity, threshold y radius_px. - Flicker: multiplicador global de brillo modulado por sin(time*freq). Sustituye al antiguo ColorOscillator CPU; eliminados oscillator.{hpp,cpp} y Defaults::Color. SDLManager::updateColors queda como no-op para no tocar las escenas que lo invocaban. - Background pulse: color de fondo aditivo entre color_min y color_max, pulsando en el tiempo. Parámetros expuestos en data/config/postfx.yaml y cargados con fkYAML. Si el archivo falta o falla, se usan defaults built-in. UV.y invertida en el vertex shader del postpro para compensar la convención de muestreo de SDL_gpu/Vulkan (el line shader sigue con su ndc.y flip). Co-Authored-By: Claude Opus 4.7 (1M context) --- data/config/postfx.yaml | 34 +++ shaders/postfx.frag.glsl | 82 +++++++ shaders/postfx.vert.glsl | 28 +++ source/core/config/postfx_config.cpp | 108 +++++++++ source/core/config/postfx_config.hpp | 21 ++ source/core/defaults.hpp | 26 +-- source/core/rendering/color_oscillator.cpp | 68 ------ source/core/rendering/color_oscillator.hpp | 29 --- source/core/rendering/gpu/gpu_device.cpp | 5 +- source/core/rendering/gpu/gpu_device.hpp | 4 +- .../core/rendering/gpu/gpu_frame_renderer.cpp | 218 +++++++++++++++--- .../core/rendering/gpu/gpu_frame_renderer.hpp | 85 +++++-- .../core/rendering/gpu/gpu_line_pipeline.cpp | 8 +- .../core/rendering/gpu/gpu_line_pipeline.hpp | 6 +- .../rendering/gpu/gpu_postfx_pipeline.cpp | 98 ++++++++ .../rendering/gpu/gpu_postfx_pipeline.hpp | 72 ++++++ source/core/rendering/line_renderer.cpp | 8 +- source/core/rendering/sdl_manager.cpp | 22 +- source/core/rendering/sdl_manager.hpp | 7 +- 19 files changed, 731 insertions(+), 198 deletions(-) create mode 100644 data/config/postfx.yaml create mode 100644 shaders/postfx.frag.glsl create mode 100644 shaders/postfx.vert.glsl create mode 100644 source/core/config/postfx_config.cpp create mode 100644 source/core/config/postfx_config.hpp delete mode 100644 source/core/rendering/color_oscillator.cpp delete mode 100644 source/core/rendering/color_oscillator.hpp create mode 100644 source/core/rendering/gpu/gpu_postfx_pipeline.cpp create mode 100644 source/core/rendering/gpu/gpu_postfx_pipeline.hpp diff --git a/data/config/postfx.yaml b/data/config/postfx.yaml new file mode 100644 index 0000000..812cf01 --- /dev/null +++ b/data/config/postfx.yaml @@ -0,0 +1,34 @@ +# postfx.yaml - Parámetros del shader de postprocesado +# +# Este archivo configura el pase final del renderer (bloom + flicker + +# background pulse). Se carga al iniciar el juego desde resources.pack. +# Si falta o tiene errores, se usan los valores por defecto de +# Defaults::PostFx (defaults.hpp). +# +# Tip de tuning: +# - Para más "neón vector", sube bloom.intensity y bloom.radius_px. +# - Para más "CRT viejo", sube flicker.amplitude (riesgo de mareo si >0.3). +# - Background es muy sutil; pasa los componentes G a 0.15-0.20 para +# un fondo verde-tenue más marcado. + +# Bloom / glow: desenfoque gaussiano de las regiones brillantes. +bloom: + enabled: true + intensity: 0.6 # 0..2 — cuanto del bloom se suma a la imagen + threshold: 0.30 # 0..1 — luminancia mínima que aporta al bloom + radius_px: 2.0 # radio del kernel en píxeles lógicos (1..8 razonable) + +# Flicker: modulación global de brillo (efecto fósforo CRT). +# Sustituye a la antigua oscilación CPU del ColorOscillator. +flicker: + enabled: true + amplitude: 0.10 # 0..1 — profundidad del flicker + frequency_hz: 6.0 # Hz — velocidad de la pulsación + +# Background pulse: color de fondo oscilante (suma aditiva). +# RGB en [0..255]; el shader normaliza a [0..1]. +background: + enabled: true + color_min: [0, 5, 0] # negro casi puro + color_max: [0, 15, 0] # verde muy tenue + pulse_frequency_hz: 6.0 # Hz — sincronizado con flicker por defecto diff --git a/shaders/postfx.frag.glsl b/shaders/postfx.frag.glsl new file mode 100644 index 0000000..eccd236 --- /dev/null +++ b/shaders/postfx.frag.glsl @@ -0,0 +1,82 @@ +#version 450 + +// Fragment shader del pase de postprocesado. +// Lee la textura offscreen (escena vectorial sobre fondo negro) y produce +// el fragmento final aplicando: +// 1. Bloom kernel 5×5 con high-pass (solo los brillos por encima de +// threshold contribuyen). +// 2. Flicker: multiplicador global de brillo modulado por tiempo +// (sustituye al oscilador CPU del legacy). +// 3. Background pulse: color de fondo que oscila entre min y max y se +// suma a la imagen (las líneas brillan por encima). +// +// Resource sets (SDL_gpu): +// set=2, binding=0 → sampler2D (escena offscreen) +// set=3, binding=0 → uniform buffer (parámetros del postpro) + +layout(set = 2, binding = 0) uniform sampler2D scene; + +layout(set = 3, binding = 0) uniform PostFxUBO { + float time; + float bloom_intensity; + float bloom_threshold; + float bloom_radius_px; + + float flicker_amplitude; + float flicker_frequency_hz; + float background_pulse_freq_hz; + float _pad_a; + + vec4 background_min; // RGB en [0..1], A=1 + vec4 background_max; // RGB en [0..1], A=1 + + vec2 texel_size; // 1.0 / texture_size + vec2 _pad_b; +} ubo; + +layout(location = 0) in vec2 v_uv; +layout(location = 0) out vec4 frag; + +const float TAU = 6.28318530718; + +void main() { + // === BLOOM === + // Kernel 5×5 con muestreo radial y high-pass por luminancia (max RGB). + // Pesos gaussianos: w = exp(-(dx²+dy²) / 4). + vec3 src = texture(scene, v_uv).rgb; + vec3 bloom = vec3(0.0); + float total_weight = 0.0; + for (int dy = -2; dy <= 2; ++dy) { + for (int dx = -2; dx <= 2; ++dx) { + vec2 offset = vec2(float(dx), float(dy)) * ubo.texel_size * ubo.bloom_radius_px; + vec3 c = texture(scene, v_uv + offset).rgb; + float luma = max(c.r, max(c.g, c.b)); + float high_pass = max(0.0, luma - ubo.bloom_threshold); + float w = exp(-(float(dx * dx + dy * dy)) / 4.0); + bloom += c * high_pass * w; + total_weight += w; + } + } + if (total_weight > 0.0) { + bloom /= total_weight; + } + bloom *= ubo.bloom_intensity; + + // === FLICKER === + // Multiplicador global de brillo. Oscila entre (1.0 - amplitude) y 1.0. + // amplitude=0 → sin flicker; amplitude=1 → pulsa entre apagado y máximo. + float pulse = (sin(ubo.time * ubo.flicker_frequency_hz * TAU) * 0.5) + 0.5; + float flicker = 1.0 - (ubo.flicker_amplitude * (1.0 - pulse)); + + // === BACKGROUND PULSE === + // Suma de color de fondo oscilante. min..max se interpolan con sin(t). + float bg_pulse = (sin(ubo.time * ubo.background_pulse_freq_hz * TAU) * 0.5) + 0.5; + vec3 background = mix(ubo.background_min.rgb, ubo.background_max.rgb, bg_pulse); + + // === COMPOSICIÓN === + // El offscreen viene con clear=black, por lo que solo las líneas y el + // bloom aportan luz. Sumamos el fondo y luego multiplicamos por flicker + // para que el pulso afecte a todo (líneas + bloom + bg). + vec3 lines_and_glow = (src + bloom) * flicker; + frag = vec4(background + lines_and_glow, 1.0); +} diff --git a/shaders/postfx.vert.glsl b/shaders/postfx.vert.glsl new file mode 100644 index 0000000..70ac360 --- /dev/null +++ b/shaders/postfx.vert.glsl @@ -0,0 +1,28 @@ +#version 450 + +// Vertex shader del pase de postprocesado. +// Emite un solo triángulo que cubre toda la pantalla (técnica del "fullscreen +// triangle"): tres vértices en (-1,-1), (3,-1), (-1,3) → la región visible +// [-1..1]² queda completamente cubierta y el clip recorta el resto. No hace +// falta vertex buffer; el draw es DrawPrimitives(vertex_count=3). + +layout(location = 0) out vec2 v_uv; + +void main() { + vec2 positions[3] = vec2[3]( + vec2(-1.0, -1.0), + vec2( 3.0, -1.0), + vec2(-1.0, 3.0) + ); + // UV.y invertida para compensar la diferencia entre la convención de + // clip-space del line shader (ndc.y flipeado, GL-style) y la convención + // de muestreo de SDL_gpu/Vulkan (origen de textura en top-left). Sin esta + // inversión, el offscreen se ve cabeza-abajo en el composite. + vec2 uvs[3] = vec2[3]( + vec2(0.0, 1.0), + vec2(2.0, 1.0), + vec2(0.0, -1.0) + ); + gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); + v_uv = uvs[gl_VertexIndex]; +} diff --git a/source/core/config/postfx_config.cpp b/source/core/config/postfx_config.cpp new file mode 100644 index 0000000..c83bde7 --- /dev/null +++ b/source/core/config/postfx_config.cpp @@ -0,0 +1,108 @@ +// postfx_config.cpp - Implementación del cargador de YAML del postpro. + +#include "core/config/postfx_config.hpp" + +#include + +#include "core/resources/resource_helper.hpp" +#include "external/fkyaml_node.hpp" + +namespace Config::PostFx { + +namespace { + +// Helper: lee `key` en `node` solo si existe; deja `dst` intacto en caso +// contrario. Así, un YAML parcial sigue funcionando con los defaults del +// struct para los campos que falten. +template +void readField(const fkyaml::node& node, const char* key, T& dst) { + if (node.contains(key)) { + dst = node[key].get_value(); + } +} + +// Lee un array RGB [r, g, b] (0..255) y lo normaliza a [0..1] sobre tres +// destinos floats. Si la clave no existe o no es secuencia de 3, deja los +// destinos como están. +void readRgb255(const fkyaml::node& node, const char* key, + float& dst_r, float& dst_g, float& dst_b) { + if (!node.contains(key)) { + return; + } + const auto& arr = node[key]; + if (!arr.is_sequence() || arr.size() < 3) { + return; + } + try { + const auto R = arr[0].get_value(); + const auto G = arr[1].get_value(); + const auto B = arr[2].get_value(); + dst_r = static_cast(R) / 255.0F; + dst_g = static_cast(G) / 255.0F; + dst_b = static_cast(B) / 255.0F; + } catch (...) { + // Mantiene los defaults si algún elemento no es entero. + } +} + +} // namespace + +auto load(const std::string& path) -> Rendering::GPU::PostFxParams { + Rendering::GPU::PostFxParams params{}; // valores por defecto del struct + + auto bytes = Resource::Helper::loadFile(path); + if (bytes.empty()) { + std::cerr << "[PostFxConfig] No se pudo cargar " << path + << " — usando defaults built-in\n"; + return params; + } + + try { + const auto* begin = reinterpret_cast(bytes.data()); + const auto* end = begin + bytes.size(); + auto yaml = fkyaml::node::deserialize(begin, end); + + if (yaml.contains("bloom") && yaml["bloom"].is_mapping()) { + const auto& node = yaml["bloom"]; + readField(node, "enabled", params.bloom_enabled); + readField(node, "intensity", params.bloom_intensity); + readField(node, "threshold", params.bloom_threshold); + readField(node, "radius_px", params.bloom_radius_px); + } + + if (yaml.contains("flicker") && yaml["flicker"].is_mapping()) { + const auto& node = yaml["flicker"]; + readField(node, "enabled", params.flicker_enabled); + readField(node, "amplitude", params.flicker_amplitude); + readField(node, "frequency_hz", params.flicker_frequency_hz); + } + + if (yaml.contains("background") && yaml["background"].is_mapping()) { + const auto& node = yaml["background"]; + readField(node, "enabled", params.background_enabled); + readRgb255(node, "color_min", + params.background_min_r, + params.background_min_g, + params.background_min_b); + readRgb255(node, "color_max", + params.background_max_r, + params.background_max_g, + params.background_max_b); + readField(node, "pulse_frequency_hz", params.background_pulse_freq_hz); + } + + std::cout << "[PostFxConfig] Cargado " << path + << " (bloom=" << (params.bloom_enabled ? "on" : "off") + << " intensity=" << params.bloom_intensity + << ", flicker=" << (params.flicker_enabled ? "on" : "off") + << " amp=" << params.flicker_amplitude + << ", bg=" << (params.background_enabled ? "on" : "off") + << ")\n"; + } catch (const fkyaml::exception& e) { + std::cerr << "[PostFxConfig] Error parseando " << path << ": " << e.what() + << " — usando defaults built-in\n"; + } + return params; +} + +} // namespace Config::PostFx diff --git a/source/core/config/postfx_config.hpp b/source/core/config/postfx_config.hpp new file mode 100644 index 0000000..7e1f41b --- /dev/null +++ b/source/core/config/postfx_config.hpp @@ -0,0 +1,21 @@ +// postfx_config.hpp - Carga de los parámetros del shader de postpro desde YAML. +// © 2025 Orni Attack +// +// Lee `config/postfx.yaml` (dentro de resources.pack) y devuelve un struct +// PostFxParams listo para pasar a GpuFrameRenderer::setPostFx(). Si el YAML +// no existe o falla el parser, retorna los defaults built-in. + +#pragma once + +#include + +#include "core/rendering/gpu/gpu_frame_renderer.hpp" + +namespace Config::PostFx { + +// Carga desde el resource pack. Path relativo dentro del pack (p.ej. +// "config/postfx.yaml"). Si falla, devuelve un PostFxParams construido por +// defecto (valores embebidos en el struct). +[[nodiscard]] auto load(const std::string& path) -> Rendering::GPU::PostFxParams; + +} // namespace Config::PostFx diff --git a/source/core/defaults.hpp b/source/core/defaults.hpp index 8e5644f..da5552a 100644 --- a/source/core/defaults.hpp +++ b/source/core/defaults.hpp @@ -248,29 +248,9 @@ namespace Math { constexpr float PI = std::numbers::pi_v; } // namespace Math -// Colores (oscilación para efecto CRT) -namespace Color { -// Frecuencia de oscilación -constexpr float FREQUENCY = 6.0F; // 1 Hz (1 ciclo/segundo) - -// Color de líneas (efecto fósforo verde CRT) -constexpr uint8_t LINE_MIN_R = 100; // Verde oscuro -constexpr uint8_t LINE_MIN_G = 200; -constexpr uint8_t LINE_MIN_B = 100; - -constexpr uint8_t LINE_MAX_R = 100; // Verde brillante -constexpr uint8_t LINE_MAX_G = 255; -constexpr uint8_t LINE_MAX_B = 100; - -// Color de fondo (pulso sutil verde oscuro) -constexpr uint8_t BACKGROUND_MIN_R = 0; // Negro -constexpr uint8_t BACKGROUND_MIN_G = 5; -constexpr uint8_t BACKGROUND_MIN_B = 0; - -constexpr uint8_t BACKGROUND_MAX_R = 0; // Verde muy oscuro -constexpr uint8_t BACKGROUND_MAX_G = 15; -constexpr uint8_t BACKGROUND_MAX_B = 0; -} // namespace Color +// La antigua oscilación CPU (namespace Color) se ha migrado al shader de +// postpro. Los parámetros de flicker / background pulse viven ahora en +// data/config/postfx.yaml y se aplican en shaders/postfx.frag.glsl. // Brillantor (control de intensitat per cada type de entidad) namespace Brightness { diff --git a/source/core/rendering/color_oscillator.cpp b/source/core/rendering/color_oscillator.cpp deleted file mode 100644 index f88e1f5..0000000 --- a/source/core/rendering/color_oscillator.cpp +++ /dev/null @@ -1,68 +0,0 @@ -// color_oscillator.cpp - Implementació de oscil·lació de color -// © 2025 Port a C++20 con SDL3 - -#include "core/rendering/color_oscillator.hpp" - -#include - -#include "core/defaults.hpp" - -namespace Rendering { - -ColorOscillator::ColorOscillator() - : accumulated_time_(0.0F) { - // Inicialitzar con el color mínim - current_line_color_ = {.r = Defaults::Color::LINE_MIN_R, - .g = Defaults::Color::LINE_MIN_G, - .b = Defaults::Color::LINE_MIN_B, - .a = 255}; - current_background_color_ = {.r = Defaults::Color::BACKGROUND_MIN_R, - .g = Defaults::Color::BACKGROUND_MIN_G, - .b = Defaults::Color::BACKGROUND_MIN_B, - .a = 255}; -} - -void ColorOscillator::update(float delta_time) { - accumulated_time_ += delta_time; - - float factor = - calculateOscillationFactor(accumulated_time_, Defaults::Color::FREQUENCY); - - // Interpolar colors de línies - SDL_Color line_min = {Defaults::Color::LINE_MIN_R, - Defaults::Color::LINE_MIN_G, - Defaults::Color::LINE_MIN_B, - 255}; - SDL_Color line_max = {Defaults::Color::LINE_MAX_R, - Defaults::Color::LINE_MAX_G, - Defaults::Color::LINE_MAX_B, - 255}; - current_line_color_ = interpolateColor(line_min, line_max, factor); - - // Interpolar colors de fons - SDL_Color bg_min = {Defaults::Color::BACKGROUND_MIN_R, - Defaults::Color::BACKGROUND_MIN_G, - Defaults::Color::BACKGROUND_MIN_B, - 255}; - SDL_Color bg_max = {Defaults::Color::BACKGROUND_MAX_R, - Defaults::Color::BACKGROUND_MAX_G, - Defaults::Color::BACKGROUND_MAX_B, - 255}; - current_background_color_ = interpolateColor(bg_min, bg_max, factor); -} - -float ColorOscillator::calculateOscillationFactor(float time, float frequency) { - // Oscil·lació senoïdal: sin(t * freq * 2π) - // Mapejar de [-1, 1] a [0, 1] - float radians = time * frequency * 2.0F * Defaults::Math::PI; - return (std::sin(radians) + 1.0F) / 2.0F; -} - -SDL_Color ColorOscillator::interpolateColor(SDL_Color min, SDL_Color max, float factor) { - return {static_cast(min.r + ((max.r - min.r) * factor)), - static_cast(min.g + ((max.g - min.g) * factor)), - static_cast(min.b + ((max.b - min.b) * factor)), - 255}; -} - -} // namespace Rendering diff --git a/source/core/rendering/color_oscillator.hpp b/source/core/rendering/color_oscillator.hpp deleted file mode 100644 index 3b65912..0000000 --- a/source/core/rendering/color_oscillator.hpp +++ /dev/null @@ -1,29 +0,0 @@ -// color_oscillator.hpp - Sistema de oscil·lació de color per efecte CRT -// © 2025 Port a C++20 con SDL3 - -#pragma once -#include - -namespace Rendering { - -class ColorOscillator { - public: - ColorOscillator(); - - void update(float delta_time); - - [[nodiscard]] SDL_Color getCurrentLineColor() const { return current_line_color_; } - [[nodiscard]] SDL_Color getCurrentBackgroundColor() const { - return current_background_color_; - } - - private: - float accumulated_time_; - SDL_Color current_line_color_; - SDL_Color current_background_color_; - - static float calculateOscillationFactor(float time, float frequency); - static SDL_Color interpolateColor(SDL_Color min, SDL_Color max, float factor); -}; - -} // namespace Rendering diff --git a/source/core/rendering/gpu/gpu_device.cpp b/source/core/rendering/gpu/gpu_device.cpp index 92b0952..c5a1260 100644 --- a/source/core/rendering/gpu/gpu_device.cpp +++ b/source/core/rendering/gpu/gpu_device.cpp @@ -62,7 +62,8 @@ void GpuDevice::destroy() { auto GpuDevice::loadShader(const std::string& spv_filename, SDL_GPUShaderStage stage, - uint32_t num_uniform_buffers) const -> SDL_GPUShader* { + uint32_t num_uniform_buffers, + uint32_t num_samplers) const -> SDL_GPUShader* { if (device_ == nullptr) { return nullptr; } @@ -90,7 +91,7 @@ auto GpuDevice::loadShader(const std::string& spv_filename, info.format = SDL_GPU_SHADERFORMAT_SPIRV; info.stage = stage; info.num_uniform_buffers = num_uniform_buffers; - info.num_samplers = 0; + info.num_samplers = num_samplers; info.num_storage_buffers = 0; info.num_storage_textures = 0; diff --git a/source/core/rendering/gpu/gpu_device.hpp b/source/core/rendering/gpu/gpu_device.hpp index 8099bb7..6e5b828 100644 --- a/source/core/rendering/gpu/gpu_device.hpp +++ b/source/core/rendering/gpu/gpu_device.hpp @@ -40,9 +40,11 @@ class GpuDevice { // SDL_ReleaseGPUShader). Retorna nullptr si falla. // // num_uniform_buffers: nº de uniform buffers que usa el shader (slot 0..N-1). + // num_samplers: nº de samplers (combined image+sampler) usados por el shader. [[nodiscard]] auto loadShader(const std::string& spv_filename, SDL_GPUShaderStage stage, - uint32_t num_uniform_buffers) const -> SDL_GPUShader*; + uint32_t num_uniform_buffers, + uint32_t num_samplers = 0) const -> SDL_GPUShader*; [[nodiscard]] auto get() const -> SDL_GPUDevice* { return device_; } [[nodiscard]] auto window() const -> SDL_Window* { return window_; } diff --git a/source/core/rendering/gpu/gpu_frame_renderer.cpp b/source/core/rendering/gpu/gpu_frame_renderer.cpp index d716749..dcbac0e 100644 --- a/source/core/rendering/gpu/gpu_frame_renderer.cpp +++ b/source/core/rendering/gpu/gpu_frame_renderer.cpp @@ -1,4 +1,4 @@ -// gpu_frame_renderer.cpp - Implementación del FrameRenderer +// gpu_frame_renderer.cpp - Implementación del FrameRenderer con offscreen + postpro #include "core/rendering/gpu/gpu_frame_renderer.hpp" @@ -21,21 +21,99 @@ auto GpuFrameRenderer::init(SDL_Window* window, float logical_w, float logical_h if (!device_.init(window)) { return false; } - if (!pipeline_.init(device_)) { + // Pipeline de líneas: escribe sobre el offscreen (formato fijo). + if (!line_pipeline_.init(device_, offscreen_format_)) { + device_.destroy(); + return false; + } + // Pipeline de postpro: escribe sobre swapchain (formato del swapchain). + if (!postfx_pipeline_.init(device_, device_.swapchainFormat())) { + line_pipeline_.destroy(); + device_.destroy(); + return false; + } + if (!createOffscreen()) { + postfx_pipeline_.destroy(); + line_pipeline_.destroy(); device_.destroy(); return false; } return true; } +auto GpuFrameRenderer::createOffscreen() -> bool { + SDL_GPUDevice* dev = device_.get(); + if (dev == nullptr) { + return false; + } + + // Textura offscreen del tamaño lógico del juego, COLOR_TARGET + SAMPLER. + SDL_GPUTextureCreateInfo tex_info{}; + tex_info.type = SDL_GPU_TEXTURETYPE_2D; + tex_info.format = offscreen_format_; + tex_info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER; + tex_info.width = static_cast(logical_w_); + tex_info.height = static_cast(logical_h_); + tex_info.layer_count_or_depth = 1; + tex_info.num_levels = 1; + tex_info.sample_count = SDL_GPU_SAMPLECOUNT_1; + offscreen_texture_ = SDL_CreateGPUTexture(dev, &tex_info); + if (offscreen_texture_ == nullptr) { + std::cerr << "[GpuFrameRenderer] SDL_CreateGPUTexture (offscreen): " + << SDL_GetError() << '\n'; + return false; + } + + // Sampler lineal con clamp-to-edge (evita sangrado en los bordes del bloom). + SDL_GPUSamplerCreateInfo sampler_info{}; + sampler_info.min_filter = SDL_GPU_FILTER_LINEAR; + sampler_info.mag_filter = SDL_GPU_FILTER_LINEAR; + sampler_info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_LINEAR; + sampler_info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; + sampler_info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; + sampler_info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; + linear_sampler_ = SDL_CreateGPUSampler(dev, &sampler_info); + if (linear_sampler_ == nullptr) { + std::cerr << "[GpuFrameRenderer] SDL_CreateGPUSampler: " + << SDL_GetError() << '\n'; + return false; + } + return true; +} + +void GpuFrameRenderer::destroyOffscreen() { + SDL_GPUDevice* dev = device_.get(); + if (dev == nullptr) { + offscreen_texture_ = nullptr; + linear_sampler_ = nullptr; + return; + } + if (offscreen_texture_ != nullptr) { + SDL_ReleaseGPUTexture(dev, offscreen_texture_); + offscreen_texture_ = nullptr; + } + if (linear_sampler_ != nullptr) { + SDL_ReleaseGPUSampler(dev, linear_sampler_); + linear_sampler_ = nullptr; + } +} + void GpuFrameRenderer::destroy() { - pipeline_.destroy(); + destroyOffscreen(); + postfx_pipeline_.destroy(); + line_pipeline_.destroy(); device_.destroy(); vertices_.clear(); indices_.clear(); } auto GpuFrameRenderer::beginFrame(float clear_r, float clear_g, float clear_b) -> bool { + // Los clear_* se ignoran: el fondo lo pinta el postpro. Mantenemos la + // firma para no romper el SDLManager. + (void)clear_r; + (void)clear_g; + (void)clear_b; + SDL_GPUDevice* dev = device_.get(); if (dev == nullptr) { return false; @@ -62,21 +140,23 @@ auto GpuFrameRenderer::beginFrame(float clear_r, float clear_g, float clear_b) - return false; } + // Abrir render pass sobre OFFSCREEN con clear a negro. SDL_GPUColorTargetInfo color_target{}; - color_target.texture = swapchain_texture_; - color_target.clear_color = SDL_FColor{.r = clear_r, .g = clear_g, .b = clear_b, .a = 1.0F}; + color_target.texture = offscreen_texture_; + color_target.clear_color = SDL_FColor{.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F}; color_target.load_op = SDL_GPU_LOADOP_CLEAR; color_target.store_op = SDL_GPU_STOREOP_STORE; color_target.cycle = false; render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &color_target, 1, nullptr); if (render_pass_ == nullptr) { - std::cerr << "[GpuFrameRenderer] SDL_BeginGPURenderPass: " << SDL_GetError() << '\n'; + std::cerr << "[GpuFrameRenderer] SDL_BeginGPURenderPass (offscreen): " + << SDL_GetError() << '\n'; SDL_SubmitGPUCommandBuffer(cmd_buffer_); cmd_buffer_ = nullptr; return false; } - applyViewport(); + // Sin SetGPUViewport: el offscreen se llena entero a tamaño lógico. vertices_.clear(); indices_.clear(); @@ -88,10 +168,9 @@ void GpuFrameRenderer::setViewport(float x, float y, float w, float h) { viewport_y_ = y; viewport_w_ = w; viewport_h_ = h; - // Si estamos en medio de un frame, aplicar inmediatamente. - if (render_pass_ != nullptr) { - applyViewport(); - } + // El viewport solo se aplica en el pase final (composite). Si estamos + // ya dentro del composite, lo aplicaríamos inmediatamente, pero la API + // está pensada para llamarse antes de endFrame/al cambiar de ventana. } void GpuFrameRenderer::setVSync(bool enabled) { @@ -102,19 +181,18 @@ void GpuFrameRenderer::setVSync(bool enabled) { const SDL_GPUPresentMode MODE = enabled ? SDL_GPU_PRESENTMODE_VSYNC : SDL_GPU_PRESENTMODE_IMMEDIATE; - // Composition por defecto: SDR sin HDR. if (!SDL_SetGPUSwapchainParameters(dev, device_.window(), SDL_GPU_SWAPCHAINCOMPOSITION_SDR, MODE)) { std::cerr << "[GpuFrameRenderer] SDL_SetGPUSwapchainParameters: " << SDL_GetError() << '\n'; } } -void GpuFrameRenderer::applyViewport() { +void GpuFrameRenderer::applyFinalViewport() { if (render_pass_ == nullptr) { return; } if (viewport_w_ <= 0.0F || viewport_h_ <= 0.0F) { - return; // full window por defecto, no setear nada + return; } SDL_GPUViewport vp{}; vp.x = viewport_x_; @@ -128,29 +206,24 @@ void GpuFrameRenderer::applyViewport() { void GpuFrameRenderer::pushLine(float x1, float y1, float x2, float y2, float thickness, float r, float g, float b, float a) { - // Extrusión perpendicular en CPU: por cada línea generamos 4 vértices que - // forman un quad (2 triángulos = 6 índices). const float DX = x2 - x1; const float DY = y2 - y1; const float LEN = std::sqrt((DX * DX) + (DY * DY)); if (LEN < 1e-6F) { - return; // línea degenerada, saltar + return; } - // Vector unitario perpendicular (90° CCW): (-DY, DX) / LEN. const float HALF = thickness * 0.5F; const float NX = -DY / LEN * HALF; const float NY = DX / LEN * HALF; const auto BASE_INDEX = static_cast(vertices_.size()); - // 4 vértices del quad: top-start, bottom-start, top-end, bottom-end. - vertices_.push_back({x1 + NX, y1 + NY, r, g, b, a}); // 0: top-start - vertices_.push_back({x1 - NX, y1 - NY, r, g, b, a}); // 1: bottom-start - vertices_.push_back({x2 + NX, y2 + NY, r, g, b, a}); // 2: top-end - vertices_.push_back({x2 - NX, y2 - NY, r, g, b, a}); // 3: bottom-end + vertices_.push_back({x1 + NX, y1 + NY, r, g, b, a}); + vertices_.push_back({x1 - NX, y1 - NY, r, g, b, a}); + vertices_.push_back({x2 + NX, y2 + NY, r, g, b, a}); + vertices_.push_back({x2 - NX, y2 - NY, r, g, b, a}); - // 2 triángulos: 0-1-2 y 1-3-2 (CCW) indices_.push_back(BASE_INDEX + 0); indices_.push_back(BASE_INDEX + 1); indices_.push_back(BASE_INDEX + 2); @@ -166,7 +239,6 @@ void GpuFrameRenderer::flushBatch() { SDL_GPUDevice* dev = device_.get(); - // Crear buffers transitorios para este frame. const uint32_t VBO_SIZE = static_cast(vertices_.size() * sizeof(LineVertex)); const uint32_t IBO_SIZE = static_cast(indices_.size() * sizeof(uint16_t)); @@ -180,7 +252,6 @@ void GpuFrameRenderer::flushBatch() { ibo_info.size = IBO_SIZE; SDL_GPUBuffer* ibo = SDL_CreateGPUBuffer(dev, &ibo_info); - // Transfer buffer combinado (vertex + index) para subir los datos. SDL_GPUTransferBufferCreateInfo tbo_info{}; tbo_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; tbo_info.size = VBO_SIZE + IBO_SIZE; @@ -191,8 +262,7 @@ void GpuFrameRenderer::flushBatch() { std::memcpy(mapped + VBO_SIZE, indices_.data(), IBO_SIZE); SDL_UnmapGPUTransferBuffer(dev, tbo); - // Copy pass: subir transfer buffer → device buffers. - // Importante: el copy pass debe ejecutarse FUERA del render pass. + // Copy pass FUERA del render pass. SDL_EndGPURenderPass(render_pass_); render_pass_ = nullptr; @@ -205,17 +275,16 @@ void GpuFrameRenderer::flushBatch() { SDL_UploadToGPUBuffer(copy_pass, &ibo_src, &ibo_dst, false); SDL_EndGPUCopyPass(copy_pass); - // Reabrir render pass (load_op = LOAD para preservar lo ya pintado). + // Reabrir render pass sobre OFFSCREEN (load_op=LOAD para preservar el clear). SDL_GPUColorTargetInfo color_target{}; - color_target.texture = swapchain_texture_; + color_target.texture = offscreen_texture_; color_target.load_op = SDL_GPU_LOADOP_LOAD; color_target.store_op = SDL_GPU_STOREOP_STORE; render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &color_target, 1, nullptr); - applyViewport(); - // Bind pipeline + buffers + uniforms. - SDL_BindGPUGraphicsPipeline(render_pass_, pipeline_.get()); + SDL_BindGPUGraphicsPipeline(render_pass_, line_pipeline_.get()); + // UBO de líneas usa el tamaño lógico (también del offscreen). LineUniforms ubo{.viewport_width = logical_w_, .viewport_height = logical_h_, .padding_0 = 0.0F, @@ -230,22 +299,95 @@ void GpuFrameRenderer::flushBatch() { SDL_DrawGPUIndexedPrimitives(render_pass_, static_cast(indices_.size()), - 1, // num_instances - 0, // first_index - 0, // vertex_offset - 0); // first_instance + 1, 0, 0, 0); - // Liberar buffers transitorios (SDL los retiene hasta que el cmd buffer termine). SDL_ReleaseGPUBuffer(dev, vbo); SDL_ReleaseGPUBuffer(dev, ibo); SDL_ReleaseGPUTransferBuffer(dev, tbo); } +void GpuFrameRenderer::compositePass() { + // Cierra el render pass actual (sobre offscreen). + if (render_pass_ != nullptr) { + SDL_EndGPURenderPass(render_pass_); + render_pass_ = nullptr; + } + + // Pase final: render pass sobre SWAPCHAIN con clear a negro (cubre el + // letterbox del viewport físico). + SDL_GPUColorTargetInfo target{}; + target.texture = swapchain_texture_; + target.clear_color = SDL_FColor{.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F}; + target.load_op = SDL_GPU_LOADOP_CLEAR; + target.store_op = SDL_GPU_STOREOP_STORE; + target.cycle = false; + render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &target, 1, nullptr); + if (render_pass_ == nullptr) { + std::cerr << "[GpuFrameRenderer] BeginRenderPass (composite): " + << SDL_GetError() << '\n'; + return; + } + applyFinalViewport(); + + SDL_BindGPUGraphicsPipeline(render_pass_, postfx_pipeline_.get()); + + // Bind del sampler (escena offscreen) en slot 0 del fragment shader. + SDL_GPUTextureSamplerBinding sampler_binding{}; + sampler_binding.texture = offscreen_texture_; + sampler_binding.sampler = linear_sampler_; + SDL_BindGPUFragmentSamplers(render_pass_, 0, &sampler_binding, 1); + + // Uniforms del postpro. Si una sección está desactivada, anulamos sus + // contribuciones (intensidad / amplitud / max=min) en lugar de tener + // un branch en el shader. + const float BLOOM_INTENSITY = postfx_params_.bloom_enabled + ? postfx_params_.bloom_intensity : 0.0F; + const float FLICKER_AMPLITUDE = postfx_params_.flicker_enabled + ? postfx_params_.flicker_amplitude : 0.0F; + const float BG_MIN_R = postfx_params_.background_enabled ? postfx_params_.background_min_r : 0.0F; + const float BG_MIN_G = postfx_params_.background_enabled ? postfx_params_.background_min_g : 0.0F; + const float BG_MIN_B = postfx_params_.background_enabled ? postfx_params_.background_min_b : 0.0F; + const float BG_MAX_R = postfx_params_.background_enabled ? postfx_params_.background_max_r : 0.0F; + const float BG_MAX_G = postfx_params_.background_enabled ? postfx_params_.background_max_g : 0.0F; + const float BG_MAX_B = postfx_params_.background_enabled ? postfx_params_.background_max_b : 0.0F; + + // Tiempo en segundos desde el inicio de SDL (wall-clock real, robusto a FPS variables). + const float TIME_SECONDS = static_cast(SDL_GetTicks()) / 1000.0F; + + PostFxUniforms ubo{}; + ubo.time = TIME_SECONDS; + ubo.bloom_intensity = BLOOM_INTENSITY; + ubo.bloom_threshold = postfx_params_.bloom_threshold; + ubo.bloom_radius_px = postfx_params_.bloom_radius_px; + ubo.flicker_amplitude = FLICKER_AMPLITUDE; + ubo.flicker_frequency_hz = postfx_params_.flicker_frequency_hz; + ubo.background_pulse_freq_hz = postfx_params_.background_pulse_freq_hz; + ubo.pad_a_ = 0.0F; + ubo.background_min_r = BG_MIN_R; + ubo.background_min_g = BG_MIN_G; + ubo.background_min_b = BG_MIN_B; + ubo.background_min_a = 1.0F; + ubo.background_max_r = BG_MAX_R; + ubo.background_max_g = BG_MAX_G; + ubo.background_max_b = BG_MAX_B; + ubo.background_max_a = 1.0F; + ubo.texel_size_x = 1.0F / logical_w_; + ubo.texel_size_y = 1.0F / logical_h_; + ubo.pad_b_ = 0.0F; + ubo.pad_c_ = 0.0F; + + SDL_PushGPUFragmentUniformData(cmd_buffer_, 0, &ubo, sizeof(ubo)); + + // Fullscreen triangle: 3 vértices generados en el shader, sin VBO. + SDL_DrawGPUPrimitives(render_pass_, 3, 1, 0, 0); +} + void GpuFrameRenderer::endFrame() { if (cmd_buffer_ == nullptr) { return; } flushBatch(); + compositePass(); if (render_pass_ != nullptr) { SDL_EndGPURenderPass(render_pass_); render_pass_ = nullptr; diff --git a/source/core/rendering/gpu/gpu_frame_renderer.hpp b/source/core/rendering/gpu/gpu_frame_renderer.hpp index 8505d19..3e78bdd 100644 --- a/source/core/rendering/gpu/gpu_frame_renderer.hpp +++ b/source/core/rendering/gpu/gpu_frame_renderer.hpp @@ -1,13 +1,17 @@ // gpu_frame_renderer.hpp - Renderer de alto nivel basado en SDL_GPU // © 2025 Orni Attack // -// API por frame: -// 1. beginFrame(clear_color) — acquire swapchain + begin render pass -// 2. pushLine(x1, y1, x2, y2, thickness, r, g, b, a) — encola la línea -// 3. endFrame() — flush del batch + submit + presenta +// Flujo por frame: +// 1. beginFrame(clear_color) +// → acquire swapchain + begin render pass sobre la textura OFFSCREEN +// (clear a black; la swapchain se pinta después con el postpro). +// 2. pushLine(...) — encola líneas (extrusión en CPU). +// 3. endFrame() +// → flush del batch en offscreen + pase de postpro (sample offscreen +// → swapchain con bloom/flicker/background) + submit + presenta. // -// Internamente: extruye cada línea como un quad (4 vértices, 6 índices) en CPU -// y construye un vertex buffer único por frame. Un solo draw call por frame. +// La oscilación de brillo y el fondo pulsante viven ahora en el shader de +// postpro; el CPU solo le pasa los uniformes del struct PostFxParams. #pragma once @@ -19,9 +23,32 @@ #include "core/rendering/gpu/gpu_device.hpp" #include "core/rendering/gpu/gpu_line_pipeline.hpp" +#include "core/rendering/gpu/gpu_postfx_pipeline.hpp" namespace Rendering::GPU { +// Parámetros del postpro que el caller (SDLManager) pasa cada frame. +// Equivalente al lado "humano" del PostFxUniforms (sin paddings). +struct PostFxParams { + bool bloom_enabled{true}; + float bloom_intensity{0.6F}; + float bloom_threshold{0.4F}; + float bloom_radius_px{2.0F}; + + bool flicker_enabled{true}; + float flicker_amplitude{0.10F}; + float flicker_frequency_hz{6.0F}; + + bool background_enabled{true}; + float background_min_r{0.0F}; + float background_min_g{0.02F}; + float background_min_b{0.0F}; + float background_max_r{0.0F}; + float background_max_g{0.06F}; + float background_max_b{0.0F}; + float background_pulse_freq_hz{6.0F}; +}; + class GpuFrameRenderer { public: GpuFrameRenderer() = default; @@ -32,47 +59,63 @@ class GpuFrameRenderer { GpuFrameRenderer(GpuFrameRenderer&&) = delete; auto operator=(GpuFrameRenderer&&) -> GpuFrameRenderer& = delete; - // Crea device + pipeline. logical_w/h = tamaño en píxeles lógicos del - // juego (1280×720) — se usa como transformación a NDC en el shader. + // Crea device + pipeline + offscreen + sampler. logical_w/h = tamaño + // en píxeles lógicos del juego (1280×720), usado como base del + // offscreen y de la transformación a NDC del shader de líneas. [[nodiscard]] auto init(SDL_Window* window, float logical_w, float logical_h) -> bool; void destroy(); - // beginFrame: adquiere swapchain, abre render pass, hace clear. - // Devuelve false si no hay textura disponible (ventana minimizada). + // beginFrame: adquiere swapchain, abre render pass sobre offscreen + // con clear a negro. Devuelve false si no hay textura disponible. + // Los argumentos clear_r/g/b se ignoran (compatibilidad de API: el + // fondo lo dibuja el postpro). [[nodiscard]] auto beginFrame(float clear_r, float clear_g, float clear_b) -> bool; // Encola una línea con grosor configurable (px). Color RGBA en [0..1]. void pushLine(float x1, float y1, float x2, float y2, float thickness, float r, float g, float b, float a); - // endFrame: sube el VBO, ejecuta el draw, cierra render pass y presenta. + // endFrame: flush del batch de líneas → composite postpro → submit + presenta. void endFrame(); - // Viewport en píxeles físicos de la swapchain. Si w<=0 o h<=0, se - // usa el tamaño completo de la ventana (sin letterbox). + // Viewport del PASE FINAL (postpro → swapchain) en píxeles físicos. + // Implementa el letterbox: cualquier área fuera del viewport queda + // como el clear color del pase de composite (negro). Si w<=0 o h<=0, + // se usa el tamaño completo de la ventana. void setViewport(float x, float y, float w, float h); // Activa/desactiva VSync. true = SDL_GPU_PRESENTMODE_VSYNC, false = IMMEDIATE. void setVSync(bool enabled); - // Acceso a internals (necesario para SDLManager y futuros sistemas). + // Parámetros del postpro que se aplican en endFrame. Por defecto = + // valores de Defaults (bloom moderado, flicker suave, fondo verde tenue). + void setPostFx(const PostFxParams& params) { postfx_params_ = params; } + [[nodiscard]] auto postfx() const -> const PostFxParams& { return postfx_params_; } + + // Acceso a internals. [[nodiscard]] auto device() -> GpuDevice& { return device_; } [[nodiscard]] auto isInsideFrame() const -> bool { return cmd_buffer_ != nullptr; } private: GpuDevice device_; - GpuLinePipeline pipeline_; + GpuLinePipeline line_pipeline_; + GpuPostFxPipeline postfx_pipeline_; - // Tamaño lógico del juego (para transformación NDC en el shader). + // Tamaño lógico del juego (= tamaño del offscreen). float logical_w_{1280.0F}; float logical_h_{720.0F}; - // Viewport en píxeles físicos. <0 = full window. + // Viewport del pase final en píxeles físicos. <0 = full window. float viewport_x_{0.0F}; float viewport_y_{0.0F}; float viewport_w_{-1.0F}; float viewport_h_{-1.0F}; + // Offscreen color target (formato fijo R8G8B8A8_UNORM para portabilidad). + SDL_GPUTexture* offscreen_texture_{nullptr}; + SDL_GPUTextureFormat offscreen_format_{SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM}; + SDL_GPUSampler* linear_sampler_{nullptr}; + // Batch del frame en curso. std::vector vertices_; std::vector indices_; @@ -82,9 +125,15 @@ class GpuFrameRenderer { SDL_GPUTexture* swapchain_texture_{nullptr}; SDL_GPURenderPass* render_pass_{nullptr}; + // Parámetros del postpro (configurables vía YAML). + PostFxParams postfx_params_{}; + // Helpers internos. + [[nodiscard]] auto createOffscreen() -> bool; + void destroyOffscreen(); void flushBatch(); - void applyViewport(); + void compositePass(); + void applyFinalViewport(); }; } // namespace Rendering::GPU diff --git a/source/core/rendering/gpu/gpu_line_pipeline.cpp b/source/core/rendering/gpu/gpu_line_pipeline.cpp index 4a34016..38705bf 100644 --- a/source/core/rendering/gpu/gpu_line_pipeline.cpp +++ b/source/core/rendering/gpu/gpu_line_pipeline.cpp @@ -13,7 +13,8 @@ namespace Rendering::GPU { GpuLinePipeline::~GpuLinePipeline() { destroy(); } -auto GpuLinePipeline::init(const GpuDevice& device) -> bool { +auto GpuLinePipeline::init(const GpuDevice& device, + SDL_GPUTextureFormat target_format) -> bool { owner_ = device.get(); if (owner_ == nullptr) { return false; @@ -59,9 +60,10 @@ auto GpuLinePipeline::init(const GpuDevice& device) -> bool { vertex_input.vertex_attributes = attrs; vertex_input.num_vertex_attributes = 2; - // Color target = swapchain. Blending alpha estándar. + // Color target = formato pasado por el caller (offscreen u otro). + // Blending alpha estándar. SDL_GPUColorTargetDescription color_target{}; - color_target.format = device.swapchainFormat(); + color_target.format = target_format; color_target.blend_state.enable_blend = true; color_target.blend_state.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA; color_target.blend_state.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; diff --git a/source/core/rendering/gpu/gpu_line_pipeline.hpp b/source/core/rendering/gpu/gpu_line_pipeline.hpp index a11511d..f5c6c0f 100644 --- a/source/core/rendering/gpu/gpu_line_pipeline.hpp +++ b/source/core/rendering/gpu/gpu_line_pipeline.hpp @@ -43,7 +43,11 @@ class GpuLinePipeline { GpuLinePipeline(GpuLinePipeline&&) = delete; auto operator=(GpuLinePipeline&&) -> GpuLinePipeline& = delete; - [[nodiscard]] auto init(const GpuDevice& device) -> bool; + // target_format: formato del color target sobre el que renderizamos + // (swapchain o offscreen). Por defecto coincide con el del swapchain + // del device. + [[nodiscard]] auto init(const GpuDevice& device, + SDL_GPUTextureFormat target_format) -> bool; void destroy(); [[nodiscard]] auto get() const -> SDL_GPUGraphicsPipeline* { return pipeline_; } diff --git a/source/core/rendering/gpu/gpu_postfx_pipeline.cpp b/source/core/rendering/gpu/gpu_postfx_pipeline.cpp new file mode 100644 index 0000000..63a53cd --- /dev/null +++ b/source/core/rendering/gpu/gpu_postfx_pipeline.cpp @@ -0,0 +1,98 @@ +// gpu_postfx_pipeline.cpp - Implementación del pipeline de postprocesado. + +#include "core/rendering/gpu/gpu_postfx_pipeline.hpp" + +#include +#include + +#include + +#include "core/rendering/gpu/gpu_device.hpp" + +namespace Rendering::GPU { + +GpuPostFxPipeline::~GpuPostFxPipeline() { destroy(); } + +auto GpuPostFxPipeline::init(const GpuDevice& device, + SDL_GPUTextureFormat target_format) -> bool { + owner_ = device.get(); + if (owner_ == nullptr) { + return false; + } + + // El vertex shader no usa UBO (emite tres vértices hardcodeados). + // El fragment shader usa 1 sampler (escena) y 1 UBO (parámetros postpro). + SDL_GPUShader* vert = device.loadShader("postfx.vert.spv", + SDL_GPU_SHADERSTAGE_VERTEX, + /*num_uniform_buffers=*/0, + /*num_samplers=*/0); + SDL_GPUShader* frag = device.loadShader("postfx.frag.spv", + SDL_GPU_SHADERSTAGE_FRAGMENT, + /*num_uniform_buffers=*/1, + /*num_samplers=*/1); + if ((vert == nullptr) || (frag == nullptr)) { + if (vert != nullptr) { + SDL_ReleaseGPUShader(owner_, vert); + } + if (frag != nullptr) { + SDL_ReleaseGPUShader(owner_, frag); + } + std::cerr << "[GpuPostFxPipeline] Error cargando shaders postfx\n"; + return false; + } + + // Sin vertex input: los tres vértices del triángulo se generan en el shader. + SDL_GPUVertexInputState vertex_input{}; + vertex_input.vertex_buffer_descriptions = nullptr; + vertex_input.num_vertex_buffers = 0; + vertex_input.vertex_attributes = nullptr; + vertex_input.num_vertex_attributes = 0; + + // Color target del postpro = swapchain. Sin blending: el postpro reescribe + // píxeles directamente (la mezcla con la escena ya se hizo dentro del shader). + SDL_GPUColorTargetDescription color_target{}; + color_target.format = target_format; + color_target.blend_state.enable_blend = false; + color_target.blend_state.color_write_mask = + SDL_GPU_COLORCOMPONENT_R | SDL_GPU_COLORCOMPONENT_G | + SDL_GPU_COLORCOMPONENT_B | SDL_GPU_COLORCOMPONENT_A; + + SDL_GPUGraphicsPipelineTargetInfo target_info{}; + target_info.color_target_descriptions = &color_target; + target_info.num_color_targets = 1; + target_info.has_depth_stencil_target = false; + + SDL_GPUGraphicsPipelineCreateInfo info{}; + info.vertex_shader = vert; + info.fragment_shader = frag; + info.vertex_input_state = vertex_input; + info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; + info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL; + info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE; + info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE; + info.multisample_state.sample_count = SDL_GPU_SAMPLECOUNT_1; + info.depth_stencil_state = {}; + info.target_info = target_info; + + pipeline_ = SDL_CreateGPUGraphicsPipeline(owner_, &info); + + SDL_ReleaseGPUShader(owner_, vert); + SDL_ReleaseGPUShader(owner_, frag); + + if (pipeline_ == nullptr) { + std::cerr << "[GpuPostFxPipeline] SDL_CreateGPUGraphicsPipeline: " + << SDL_GetError() << '\n'; + return false; + } + return true; +} + +void GpuPostFxPipeline::destroy() { + if ((pipeline_ != nullptr) && (owner_ != nullptr)) { + SDL_ReleaseGPUGraphicsPipeline(owner_, pipeline_); + } + pipeline_ = nullptr; + owner_ = nullptr; +} + +} // namespace Rendering::GPU diff --git a/source/core/rendering/gpu/gpu_postfx_pipeline.hpp b/source/core/rendering/gpu/gpu_postfx_pipeline.hpp new file mode 100644 index 0000000..0d897c4 --- /dev/null +++ b/source/core/rendering/gpu/gpu_postfx_pipeline.hpp @@ -0,0 +1,72 @@ +// gpu_postfx_pipeline.hpp - Pipeline de postprocesado (fullscreen triangle) +// © 2025 Orni Attack +// +// Pase final del frame: muestrea la escena renderizada en offscreen y aplica +// bloom + flicker + background pulse en el fragment shader. El vertex shader +// emite un único triángulo que cubre toda la pantalla, así que el draw no +// necesita vertex buffer (DrawPrimitives con vertex_count=3). +// +// Recursos del shader (SDL_gpu set bindings): +// fragment set=2, binding=0 → sampler2D (escena offscreen) +// fragment set=3, binding=0 → uniform buffer (parámetros del postpro) + +#pragma once + +#include + +namespace Rendering::GPU { + +class GpuDevice; + +// Uniform buffer del postpro. Debe coincidir EXACTAMENTE con +// shaders/postfx.frag.glsl (layout std140 con vec4 alineadas a 16 bytes). +struct PostFxUniforms { + float time; // Tiempo acumulado en segundos + float bloom_intensity; // Mezcla bloom (0..2) + float bloom_threshold; // Luminancia mínima high-pass (0..1) + float bloom_radius_px; // Radio del kernel en píxeles lógicos + + float flicker_amplitude; // Profundidad del flicker (0..1) + float flicker_frequency_hz; // Hz + float background_pulse_freq_hz; // Hz + float pad_a_; + + float background_min_r; // Color min RGB en [0..1], A=1 + float background_min_g; + float background_min_b; + float background_min_a; + + float background_max_r; + float background_max_g; + float background_max_b; + float background_max_a; + + float texel_size_x; // 1.0 / texture_width + float texel_size_y; + float pad_b_; + float pad_c_; +}; + +class GpuPostFxPipeline { + public: + GpuPostFxPipeline() = default; + ~GpuPostFxPipeline(); + + GpuPostFxPipeline(const GpuPostFxPipeline&) = delete; + auto operator=(const GpuPostFxPipeline&) -> GpuPostFxPipeline& = delete; + GpuPostFxPipeline(GpuPostFxPipeline&&) = delete; + auto operator=(GpuPostFxPipeline&&) -> GpuPostFxPipeline& = delete; + + // target_format: formato del color target del pase final (swapchain). + [[nodiscard]] auto init(const GpuDevice& device, + SDL_GPUTextureFormat target_format) -> bool; + void destroy(); + + [[nodiscard]] auto get() const -> SDL_GPUGraphicsPipeline* { return pipeline_; } + + private: + SDL_GPUDevice* owner_{nullptr}; + SDL_GPUGraphicsPipeline* pipeline_{nullptr}; +}; + +} // namespace Rendering::GPU diff --git a/source/core/rendering/line_renderer.cpp b/source/core/rendering/line_renderer.cpp index cd3513f..c6afdc8 100644 --- a/source/core/rendering/line_renderer.cpp +++ b/source/core/rendering/line_renderer.cpp @@ -6,8 +6,10 @@ namespace Rendering { -// Color global compartido (actualizado por ColorOscillator via SDLManager). -SDL_Color g_current_line_color = {255, 255, 255, 255}; +// Color global compartido para líneas sin paleta propia (HUD, debug, texto +// genérico). Equivale al "color máximo" de la antigua oscilación CPU: verde +// fósforo CRT. El pulso de brillo lo aplica ahora el shader de postpro. +SDL_Color g_current_line_color = {100, 255, 100, 255}; // Grosor global por defecto. Configurable via setLineThickness. // 1.5 da una línea visible y crujiente; 1.0 se ve demasiado fino en pantallas grandes. @@ -29,7 +31,7 @@ void linea(Renderer* renderer, const float FX2 = static_cast(x2); const float FY2 = static_cast(y2); - // color.alpha==0 → usar global del oscilador. alpha>0 → color directo. + // color.alpha==0 → usar color global (verde fósforo). alpha>0 → color directo. const SDL_Color SOURCE = (color.a > 0) ? color : g_current_line_color; const float R = (static_cast(SOURCE.r) * brightness) / 255.0F; const float G = (static_cast(SOURCE.g) * brightness) / 255.0F; diff --git a/source/core/rendering/sdl_manager.cpp b/source/core/rendering/sdl_manager.cpp index 03a576f..a28d693 100644 --- a/source/core/rendering/sdl_manager.cpp +++ b/source/core/rendering/sdl_manager.cpp @@ -9,6 +9,7 @@ #include #include +#include "core/config/postfx_config.hpp" #include "core/defaults.hpp" #include "core/input/mouse.hpp" #include "core/rendering/coordinate_transform.hpp" @@ -48,6 +49,12 @@ auto initWindowAndGpu(SDL_Window** out_window, } gpu_renderer.setVSync(Options::rendering.vsync != 0); + + // Cargar parámetros del postpro desde el resource pack. Si el YAML falta + // o falla, el loader devuelve los defaults built-in (bloom suave + flicker + // sutil + background verde tenue). + gpu_renderer.setPostFx(Config::PostFx::load("config/postfx.yaml")); + *out_window = window; return true; } @@ -314,15 +321,13 @@ auto SDLManager::handleWindowEvent(const SDL_Event& event) -> bool { } void SDLManager::clear(uint8_t r, uint8_t g, uint8_t b) { - // Usar el color oscilatorio de fondo (sustituye los parámetros pasados, - // que solo existían por compatibilidad con la API anterior). + // El fondo lo dibuja ahora el shader de postpro (background pulse). El + // offscreen se limpia en negro dentro de beginFrame. Los argumentos r/g/b + // se mantienen por compatibilidad de API. (void)r; (void)g; (void)b; - SDL_Color bg = color_oscillator_.getCurrentBackgroundColor(); - gpu_renderer_.beginFrame(static_cast(bg.r) / 255.0F, - static_cast(bg.g) / 255.0F, - static_cast(bg.b) / 255.0F); + gpu_renderer_.beginFrame(0.0F, 0.0F, 0.0F); } void SDLManager::present() { @@ -330,8 +335,9 @@ void SDLManager::present() { } void SDLManager::updateColors(float delta_time) { - color_oscillator_.update(delta_time); - Rendering::setLineColor(color_oscillator_.getCurrentLineColor()); + // No-op desde la migración a postpro. La oscilación de brillo y el pulso + // de fondo los aplica shaders/postfx.frag.glsl directamente sobre wall-clock. + (void)delta_time; } void SDLManager::updateFPS(float delta_time) { diff --git a/source/core/rendering/sdl_manager.hpp b/source/core/rendering/sdl_manager.hpp index 960dafa..81ce63f 100644 --- a/source/core/rendering/sdl_manager.hpp +++ b/source/core/rendering/sdl_manager.hpp @@ -13,7 +13,6 @@ #include #include -#include "core/rendering/color_oscillator.hpp" #include "core/rendering/render_context.hpp" class SDLManager { @@ -37,7 +36,9 @@ class SDLManager { void clear(uint8_t r = 0, uint8_t g = 0, uint8_t b = 0); void present(); - // [NUEVO] Actualització de colors (oscil·lació) + // No-op desde la migración a postpro (la oscilación de brillo la + // gestiona el shader, no la CPU). Se mantiene la firma para no tocar + // los escenarios que la siguen invocando. void updateColors(float delta_time); // [NUEVO] Actualitzar counter de FPS @@ -82,6 +83,4 @@ class SDLManager { void applyWindowSize(int width, int height); // Canviar mida + centrar void updateViewport(); // Configurar viewport con letterbox - // [NUEVO] Oscil·lador de colors - Rendering::ColorOscillator color_oscillator_; };