feat(bloom): glow separable two-pass amb composite preserve-core i paleta neon

This commit is contained in:
2026-05-21 18:39:16 +02:00
parent 8b4683b77b
commit ae946b578e
17 changed files with 3683 additions and 2159 deletions
@@ -0,0 +1,129 @@
// gpu_bloom_pipeline.cpp - Implementació del pipeline de bloom separable.
#include "core/rendering/gpu/gpu_bloom_pipeline.hpp"
#include <SDL3/SDL.h>
#include <SDL3/SDL_gpu.h>
#include <iostream>
#include "core/rendering/gpu/gpu_device.hpp"
#include "core/rendering/gpu/shader_factory.hpp"
#ifdef __APPLE__
#include "core/rendering/gpu/msl/bloom_frag.msl.h"
#include "core/rendering/gpu/msl/postfx_vert.msl.h"
#else
#include "core/rendering/gpu/spv/bloom_frag_spv.h"
#include "core/rendering/gpu/spv/postfx_vert_spv.h"
#endif
namespace Rendering::GPU {
GpuBloomPipeline::~GpuBloomPipeline() { destroy(); }
auto GpuBloomPipeline::init(const GpuDevice& device,
SDL_GPUTextureFormat target_format) -> bool {
owner_ = device.get();
if (owner_ == nullptr) {
return false;
}
// Reutilitzem el vertex shader del postfx (fullscreen triangle, sense UBO).
// El fragment shader és nou: 1 sampler (input) + 1 UBO (paràmetres del blur).
#ifdef __APPLE__
SDL_GPUShader* vert = createShaderMSL(owner_,
Msl::POSTFX_VERT_MSL,
"postfx_vs",
SDL_GPU_SHADERSTAGE_VERTEX,
/*num_samplers=*/0,
/*num_uniform_buffers=*/0);
SDL_GPUShader* frag = createShaderMSL(owner_,
Msl::BLOOM_FRAG_MSL,
"bloom_fs",
SDL_GPU_SHADERSTAGE_FRAGMENT,
/*num_samplers=*/1,
/*num_uniform_buffers=*/1);
#else
SDL_GPUShader* vert = createShaderSPIRV(owner_,
POSTFX_VERT_SPV,
POSTFX_VERT_SPV_SIZE,
"main",
SDL_GPU_SHADERSTAGE_VERTEX,
/*num_samplers=*/0,
/*num_uniform_buffers=*/0);
SDL_GPUShader* frag = createShaderSPIRV(owner_,
BLOOM_FRAG_SPV,
BLOOM_FRAG_SPV_SIZE,
"main",
SDL_GPU_SHADERSTAGE_FRAGMENT,
/*num_samplers=*/1,
/*num_uniform_buffers=*/1);
#endif
if ((vert == nullptr) || (frag == nullptr)) {
if (vert != nullptr) {
SDL_ReleaseGPUShader(owner_, vert);
}
if (frag != nullptr) {
SDL_ReleaseGPUShader(owner_, frag);
}
std::cerr << "[GpuBloomPipeline] Error carregant shaders bloom: " << SDL_GetError() << '\n';
return false;
}
// Sense vertex input: els tres vèrtexs del fullscreen triangle es generen al 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 = textura de bloom (output). Sense blending: cada passada
// reescriu completament el contingut del target.
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 << "[GpuBloomPipeline] SDL_CreateGPUGraphicsPipeline: "
<< SDL_GetError() << '\n';
return false;
}
return true;
}
void GpuBloomPipeline::destroy() {
if ((pipeline_ != nullptr) && (owner_ != nullptr)) {
SDL_ReleaseGPUGraphicsPipeline(owner_, pipeline_);
}
pipeline_ = nullptr;
owner_ = nullptr;
}
} // namespace Rendering::GPU
@@ -0,0 +1,59 @@
// gpu_bloom_pipeline.hpp - Pipeline gráfico per al bloom separable de dues passes.
// © 2026 JailDesigner
//
// El bloom es calcula en dues passes 1D (horizontal i vertical) abans del pase
// final de composite. La mateixa instància de pipeline serveix per a tots dos
// passes; només canvien els uniformes (direction + extract).
//
// Recursos del shader (SDL_gpu set bindings):
// fragment set=2, binding=0 → sampler2D (input)
// fragment set=3, binding=0 → uniform buffer (paràmetres del blur)
//
// Per al vertex shader es reutilitza postfx.vert.glsl (fullscreen triangle).
#pragma once
#include <SDL3/SDL_gpu.h>
namespace Rendering::GPU {
class GpuDevice;
// Uniform buffer del bloom. Ha de coincidir EXACTAMENT amb shaders/bloom.frag.glsl
// (std140 — vec2 alineats a 8 bytes; padding a 16).
struct BloomUniforms {
float texel_size_x; // 1.0 / texture_width
float texel_size_y; // 1.0 / texture_height
float direction_x; // 1.0 per pass H, 0.0 per pass V
float direction_y; // 0.0 per pass H, 1.0 per pass V
float threshold; // luminància mínima per al high-pass (només si extract>0)
float extract; // 1.0 = pass H amb high-pass, 0.0 = pass V (blur pur)
float sigma; // amplada de la gaussiana en texels
float pad_a; // alineament a 16 bytes
};
class GpuBloomPipeline {
public:
GpuBloomPipeline() = default;
~GpuBloomPipeline();
GpuBloomPipeline(const GpuBloomPipeline&) = delete;
auto operator=(const GpuBloomPipeline&) -> GpuBloomPipeline& = delete;
GpuBloomPipeline(GpuBloomPipeline&&) = delete;
auto operator=(GpuBloomPipeline&&) -> GpuBloomPipeline& = delete;
// target_format: format del color target on s'escriu el resultat (bloom
// texture, idealment el mateix format que l'offscreen per portabilitat).
[[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
+147 -12
View File
@@ -28,14 +28,22 @@ namespace Rendering::GPU {
device_.destroy();
return false;
}
// Pipeline de bloom: escriu sobre les bloom textures (mateix format).
if (!bloom_pipeline_.init(device_, offscreen_format_)) {
line_pipeline_.destroy();
device_.destroy();
return false;
}
// Pipeline de postpro: escribe sobre swapchain (formato del swapchain).
if (!postfx_pipeline_.init(device_, device_.swapchainFormat())) {
bloom_pipeline_.destroy();
line_pipeline_.destroy();
device_.destroy();
return false;
}
if (!createOffscreen()) {
postfx_pipeline_.destroy();
bloom_pipeline_.destroy();
line_pipeline_.destroy();
device_.destroy();
return false;
@@ -82,6 +90,18 @@ namespace Rendering::GPU {
<< SDL_GetError() << '\n';
return false;
}
// Bloom textures: mateixa mida i format que l'offscreen. Es fan servir
// ping-pong (A = sortida de la passada H, B = sortida de la V i lectura
// del composite). COLOR_TARGET + SAMPLER, igual que l'offscreen.
tex_info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER;
bloom_texture_a_ = SDL_CreateGPUTexture(dev, &tex_info);
bloom_texture_b_ = SDL_CreateGPUTexture(dev, &tex_info);
if ((bloom_texture_a_ == nullptr) || (bloom_texture_b_ == nullptr)) {
std::cerr << "[GpuFrameRenderer] SDL_CreateGPUTexture (bloom): "
<< SDL_GetError() << '\n';
return false;
}
return true;
}
@@ -96,6 +116,14 @@ namespace Rendering::GPU {
SDL_ReleaseGPUTexture(dev, offscreen_texture_);
offscreen_texture_ = nullptr;
}
if (bloom_texture_a_ != nullptr) {
SDL_ReleaseGPUTexture(dev, bloom_texture_a_);
bloom_texture_a_ = nullptr;
}
if (bloom_texture_b_ != nullptr) {
SDL_ReleaseGPUTexture(dev, bloom_texture_b_);
bloom_texture_b_ = nullptr;
}
if (linear_sampler_ != nullptr) {
SDL_ReleaseGPUSampler(dev, linear_sampler_);
linear_sampler_ = nullptr;
@@ -105,6 +133,7 @@ namespace Rendering::GPU {
void GpuFrameRenderer::destroy() {
destroyOffscreen();
postfx_pipeline_.destroy();
bloom_pipeline_.destroy();
line_pipeline_.destroy();
device_.destroy();
vertices_.clear();
@@ -388,6 +417,113 @@ namespace Rendering::GPU {
SDL_ReleaseGPUTransferBuffer(dev, tbo);
}
void GpuFrameRenderer::bloomPass() {
// Tanca el render pass actual (sobre l'offscreen) abans de canviar de
// target. Cada passada de bloom obre el seu propi render pass.
if (render_pass_ != nullptr) {
SDL_EndGPURenderPass(render_pass_);
render_pass_ = nullptr;
}
// Si el bloom està desactivat, fem clear a negre sobre bloom_b perquè
// el composite el samplegi com a "sense bloom" sense haver de tenir un
// path alternatiu al shader.
const bool BLOOM_ON = postfx_params_.bloom_enabled;
const float TEXEL_X = 1.0F / render_w_;
const float TEXEL_Y = 1.0F / render_h_;
const float SIGMA = postfx_params_.bloom_sigma_px;
const float THRESHOLD = postfx_params_.bloom_threshold;
if (!BLOOM_ON) {
// Clear bloom_b a negre i prou.
SDL_GPUColorTargetInfo clear_target{};
clear_target.texture = bloom_texture_b_;
clear_target.clear_color = SDL_FColor{.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
clear_target.load_op = SDL_GPU_LOADOP_CLEAR;
clear_target.store_op = SDL_GPU_STOREOP_STORE;
clear_target.cycle = false;
SDL_GPURenderPass* clear_pass = SDL_BeginGPURenderPass(cmd_buffer_, &clear_target, 1, nullptr);
if (clear_pass != nullptr) {
SDL_EndGPURenderPass(clear_pass);
}
return;
}
// === PASS H: high-pass + gaussiana horitzontal ===
// Llegim de l'offscreen i escrivim a bloom_texture_a_.
{
SDL_GPUColorTargetInfo target{};
target.texture = bloom_texture_a_;
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;
SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd_buffer_, &target, 1, nullptr);
if (pass == nullptr) {
std::cerr << "[GpuFrameRenderer] BeginRenderPass (bloom H): "
<< SDL_GetError() << '\n';
return;
}
SDL_BindGPUGraphicsPipeline(pass, bloom_pipeline_.get());
SDL_GPUTextureSamplerBinding binding{};
binding.texture = offscreen_texture_;
binding.sampler = linear_sampler_;
SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1);
BloomUniforms ubo{};
ubo.texel_size_x = TEXEL_X;
ubo.texel_size_y = TEXEL_Y;
ubo.direction_x = 1.0F;
ubo.direction_y = 0.0F;
ubo.threshold = THRESHOLD;
ubo.extract = 1.0F; // primera passada → aplica high-pass
ubo.sigma = SIGMA;
ubo.pad_a = 0.0F;
SDL_PushGPUFragmentUniformData(cmd_buffer_, 0, &ubo, sizeof(ubo));
SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
SDL_EndGPURenderPass(pass);
}
// === PASS V: gaussiana vertical (sense high-pass) ===
// Llegim de bloom_texture_a_ i escrivim a bloom_texture_b_.
{
SDL_GPUColorTargetInfo target{};
target.texture = bloom_texture_b_;
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;
SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd_buffer_, &target, 1, nullptr);
if (pass == nullptr) {
std::cerr << "[GpuFrameRenderer] BeginRenderPass (bloom V): "
<< SDL_GetError() << '\n';
return;
}
SDL_BindGPUGraphicsPipeline(pass, bloom_pipeline_.get());
SDL_GPUTextureSamplerBinding binding{};
binding.texture = bloom_texture_a_;
binding.sampler = linear_sampler_;
SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1);
BloomUniforms ubo{};
ubo.texel_size_x = TEXEL_X;
ubo.texel_size_y = TEXEL_Y;
ubo.direction_x = 0.0F;
ubo.direction_y = 1.0F;
ubo.threshold = 0.0F;
ubo.extract = 0.0F; // segona passada → blur pur
ubo.sigma = SIGMA;
ubo.pad_a = 0.0F;
SDL_PushGPUFragmentUniformData(cmd_buffer_, 0, &ubo, sizeof(ubo));
SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
SDL_EndGPURenderPass(pass);
}
}
void GpuFrameRenderer::compositePass() {
// Cierra el render pass actual (sobre offscreen).
if (render_pass_ != nullptr) {
@@ -413,11 +549,14 @@ namespace Rendering::GPU {
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);
// Bind de dos samplers: slot 0 = escena offscreen, slot 1 = bloom V.
// El bloom V conté ja el resultat de les dues passes separables.
SDL_GPUTextureSamplerBinding sampler_bindings[2]{};
sampler_bindings[0].texture = offscreen_texture_;
sampler_bindings[0].sampler = linear_sampler_;
sampler_bindings[1].texture = bloom_texture_b_;
sampler_bindings[1].sampler = linear_sampler_;
SDL_BindGPUFragmentSamplers(render_pass_, 0, sampler_bindings, 2);
// Uniforms del postpro. Si una sección está desactivada, anulamos sus
// contribuciones (intensidad / amplitud / max=min) en lugar de tener
@@ -441,12 +580,12 @@ namespace Rendering::GPU {
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.pad_b = 0.0F;
ubo.pad_c = 0.0F;
ubo.background_min_r = BG_MIN_R;
ubo.background_min_g = BG_MIN_G;
ubo.background_min_b = BG_MIN_B;
@@ -455,11 +594,6 @@ namespace Rendering::GPU {
ubo.background_max_g = BG_MAX_G;
ubo.background_max_b = BG_MAX_B;
ubo.background_max_a = 1.0F;
// El sampling del bloom muestrea el offscreen → texel size del tamaño físico.
ubo.texel_size_x = 1.0F / render_w_;
ubo.texel_size_y = 1.0F / render_h_;
ubo.pad_b = 0.0F;
ubo.pad_c = 0.0F;
SDL_PushGPUFragmentUniformData(cmd_buffer_, 0, &ubo, sizeof(ubo));
@@ -472,6 +606,7 @@ namespace Rendering::GPU {
return;
}
flushBatch();
bloomPass();
compositePass();
if (render_pass_ != nullptr) {
SDL_EndGPURenderPass(render_pass_);
@@ -21,6 +21,7 @@
#include <cstdint>
#include <vector>
#include "core/rendering/gpu/gpu_bloom_pipeline.hpp"
#include "core/rendering/gpu/gpu_device.hpp"
#include "core/rendering/gpu/gpu_line_pipeline.hpp"
#include "core/rendering/gpu/gpu_postfx_pipeline.hpp"
@@ -33,7 +34,7 @@ namespace Rendering::GPU {
bool bloom_enabled{true};
float bloom_intensity{0.6F};
float bloom_threshold{0.4F};
float bloom_radius_px{2.0F};
float bloom_sigma_px{3.5F}; // sigma de la gaussiana en texels (separable blur)
bool flicker_enabled{true};
float flicker_amplitude{0.10F};
@@ -124,6 +125,7 @@ namespace Rendering::GPU {
private:
GpuDevice device_;
GpuLinePipeline line_pipeline_;
GpuBloomPipeline bloom_pipeline_;
GpuPostFxPipeline postfx_pipeline_;
// Tamaño lógico del juego: espacio de coordenadas de las primitivas
@@ -149,6 +151,13 @@ namespace Rendering::GPU {
SDL_GPUTextureFormat offscreen_format_{SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM};
SDL_GPUSampler* linear_sampler_{nullptr};
// Bloom: dues textures intermèdies per al separable blur.
// _a rep el resultat de la passada H (high-pass + horitzontal); _b rep
// la V i és el bloom final que llegeix el composite. Mateixa mida que
// l'offscreen (full-res, no downsample en aquesta versió).
SDL_GPUTexture* bloom_texture_a_{nullptr};
SDL_GPUTexture* bloom_texture_b_{nullptr};
// Batch del frame en curso.
std::vector<LineVertex> vertices_;
std::vector<uint16_t> indices_;
@@ -168,6 +177,7 @@ namespace Rendering::GPU {
[[nodiscard]] auto createOffscreen() -> bool;
void destroyOffscreen();
void flushBatch();
void bloomPass(); // pre-composite: H + V passes sobre les bloom textures
void compositePass();
void applyFinalViewport();
};
@@ -42,7 +42,7 @@ namespace Rendering::GPU {
Msl::POSTFX_FRAG_MSL,
"postfx_fs",
SDL_GPU_SHADERSTAGE_FRAGMENT,
/*num_samplers=*/1,
/*num_samplers=*/2,
/*num_uniform_buffers=*/1);
#else
SDL_GPUShader* vert = createShaderSPIRV(owner_,
@@ -57,7 +57,7 @@ namespace Rendering::GPU {
POSTFX_FRAG_SPV_SIZE,
"main",
SDL_GPU_SHADERSTAGE_FRAGMENT,
/*num_samplers=*/1,
/*num_samplers=*/2,
/*num_uniform_buffers=*/1);
#endif
@@ -16,39 +16,37 @@
namespace Rendering::GPU {
class GpuDevice;
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
// Uniform buffer del composite final. Ha de coincidir EXACTAMENT amb
// shaders/postfx.frag.glsl (layout std140 amb vec4 alineades a 16 bytes).
// El bloom es calcula en passades separades (veure GpuBloomPipeline) i aquí
// només passem la intensitat per a la composició; el threshold/sigma viuen
// al UBO del bloom.
struct PostFxUniforms {
float time; // Temps acumulat en segons
float bloom_intensity; // Mescla bloom (0..2)
float flicker_amplitude; // Profunditat del flicker (0..1)
float flicker_frequency_hz; // Hz
float flicker_amplitude; // Profundidad del flicker (0..1)
float flicker_frequency_hz; // Hz
float background_pulse_freq_hz; // Hz
float pad_a;
float background_pulse_freq_hz; // Hz
float pad_a;
float pad_b;
float pad_c;
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_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 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:
class GpuPostFxPipeline {
public:
GpuPostFxPipeline() = default;
~GpuPostFxPipeline();
@@ -59,14 +57,14 @@ class GpuPostFxPipeline {
// target_format: formato del color target del pase final (swapchain).
[[nodiscard]] auto init(const GpuDevice& device,
SDL_GPUTextureFormat target_format) -> bool;
SDL_GPUTextureFormat target_format) -> bool;
void destroy();
[[nodiscard]] auto get() const -> SDL_GPUGraphicsPipeline* { return pipeline_; }
private:
private:
SDL_GPUDevice* owner_{nullptr};
SDL_GPUGraphicsPipeline* pipeline_{nullptr};
};
};
} // namespace Rendering::GPU
@@ -0,0 +1,72 @@
// bloom_frag.msl.h - Metal Shading Language del fragment shader del bloom
// © 2026 JailDesigner
//
// IMPORTANT: mantenir sincronitzat a mà amb shaders/bloom.frag.glsl. SDL3 GPU
// compila aquest string MSL en runtime; qualsevol canvi al GLSL o al struct
// BloomUniforms (gpu_bloom_pipeline.hpp) cal replicar-lo aquí al mateix commit.
//
// Pass 1D del bloom separable: high-pass + gaussiana en una direcció.
// Recursos:
// - texture2d<float> src [[texture(0)]] + sampler [[sampler(0)]]
// - constant BloomUBO& ubo [[buffer(0)]]
#pragma once
#ifdef __APPLE__
namespace Rendering::GPU::Msl {
inline constexpr const char* BLOOM_FRAG_MSL = R"(
#include <metal_stdlib>
using namespace metal;
struct PostVOut {
float4 pos [[position]];
float2 uv;
};
struct BloomUBO {
float2 texel_size;
float2 direction;
float threshold;
float extract;
float sigma;
float pad_a;
};
fragment float4 bloom_fs(PostVOut in [[stage_in]],
texture2d<float> src [[texture(0)]],
sampler samp [[sampler(0)]],
constant BloomUBO& ubo [[buffer(0)]]) {
float3 sum = float3(0.0);
float total_weight = 0.0;
constexpr int RADIUS = 7;
constexpr float TWO_SIGMA_SQ_FACTOR = 2.0;
for (int i = -RADIUS; i <= RADIUS; ++i) {
float2 offset = ubo.direction * float(i) * ubo.texel_size;
float3 c = src.sample(samp, in.uv + offset).rgb;
if (ubo.extract > 0.5) {
float luma = max(c.r, max(c.g, c.b));
float high_pass = max(0.0, luma - ubo.threshold);
c *= high_pass;
}
float fi = float(i);
float w = exp(-(fi * fi) / (TWO_SIGMA_SQ_FACTOR * ubo.sigma * ubo.sigma));
sum += c * w;
total_weight += w;
}
if (total_weight > 0.0) {
sum /= total_weight;
}
return float4(sum, 1.0);
}
)";
} // namespace Rendering::GPU::Msl
#endif // __APPLE__
+20 -35
View File
@@ -1,4 +1,4 @@
// postfx_frag.msl.h - Metal Shading Language del fragment shader del postpro
// postfx_frag.msl.h - Metal Shading Language del fragment shader del composite
// © 2026 JailDesigner
//
// IMPORTANT: mantenir sincronitzat a mà amb shaders/postfx.frag.glsl. SDL3 GPU
@@ -6,19 +6,18 @@
// canvi al struct PostFxUniforms (gpu_postfx_pipeline.hpp), al GLSL o al MSL
// cal replicar-lo a totes tres al mateix commit.
//
// Composició final: bloom 5×5 amb high-pass, flicker sinusoidal global,
// background pulse sumat. Recursos:
// Composite final: llegeix escena + bloom pre-calculat (per bloom.frag.glsl en
// separable two-pass) i aplica flicker + background pulse. Recursos:
// - texture2d<float> scene [[texture(0)]] + sampler [[sampler(0)]]
// - texture2d<float> bloom_tex [[texture(1)]] + sampler [[sampler(1)]]
// - constant PostFxUBO& ubo [[buffer(0)]] (slot 0 SDL → buffer(0) MSL)
//
// L'struct PostFxUBO té layout idèntic a PostFxUniforms (5×vec4 = 80 bytes).
#pragma once
#ifdef __APPLE__
namespace Rendering::GPU::Msl {
inline constexpr const char* POSTFX_FRAG_MSL = R"(
inline constexpr const char* POSTFX_FRAG_MSL = R"(
#include <metal_stdlib>
using namespace metal;
@@ -30,46 +29,28 @@ struct PostVOut {
struct 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;
float pad_b;
float pad_c;
float4 background_min;
float4 background_max;
float2 texel_size;
float2 pad_b;
};
constant float TAU = 6.28318530718;
fragment float4 postfx_fs(PostVOut in [[stage_in]],
texture2d<float> scene [[texture(0)]],
sampler samp [[sampler(0)]],
texture2d<float> scene [[texture(0)]],
sampler samp_s [[sampler(0)]],
texture2d<float> bloom_tex [[texture(1)]],
sampler samp_b [[sampler(1)]],
constant PostFxUBO& ubo [[buffer(0)]]) {
// === BLOOM ===
float3 src = scene.sample(samp, in.uv).rgb;
float3 bloom = float3(0.0);
float total_weight = 0.0;
for (int dy = -2; dy <= 2; ++dy) {
for (int dx = -2; dx <= 2; ++dx) {
float2 offset = float2(float(dx), float(dy)) * ubo.texel_size * ubo.bloom_radius_px;
float3 c = scene.sample(samp, in.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;
float3 src = scene.sample(samp_s, in.uv).rgb;
float3 bloom = bloom_tex.sample(samp_b, in.uv).rgb * ubo.bloom_intensity;
// === FLICKER ===
float pulse = (sin(ubo.time * ubo.flicker_frequency_hz * TAU) * 0.5) + 0.5;
@@ -79,8 +60,12 @@ fragment float4 postfx_fs(PostVOut in [[stage_in]],
float bg_pulse = (sin(ubo.time * ubo.background_pulse_freq_hz * TAU) * 0.5) + 0.5;
float3 background = mix(ubo.background_min.rgb, ubo.background_max.rgb, bg_pulse);
// === COMPOSICIÓ ===
float3 lines_and_glow = (src + bloom) * flicker;
// === COMPOSICIÓ (preserve-core) ===
// Bloom additiu atenuat per (1 - luma_src) — manté el color del core
// i posa el halo intens només als píxels foscos del voltant.
float src_luma = max(src.r, max(src.g, src.b));
float3 bloom_contribution = bloom * (1.0 - src_luma);
float3 lines_and_glow = (src + bloom_contribution) * flicker;
return float4(background + lines_and_glow, 1.0);
}
)";
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff