feat(bloom): glow separable two-pass amb composite preserve-core i paleta neon
This commit is contained in:
@@ -147,12 +147,14 @@ set(ALL_SHADER_HEADERS
|
|||||||
"${HEADERS_DIR}/line_frag_spv.h"
|
"${HEADERS_DIR}/line_frag_spv.h"
|
||||||
"${HEADERS_DIR}/postfx_vert_spv.h"
|
"${HEADERS_DIR}/postfx_vert_spv.h"
|
||||||
"${HEADERS_DIR}/postfx_frag_spv.h"
|
"${HEADERS_DIR}/postfx_frag_spv.h"
|
||||||
|
"${HEADERS_DIR}/bloom_frag_spv.h"
|
||||||
)
|
)
|
||||||
set(ALL_SHADER_SOURCES
|
set(ALL_SHADER_SOURCES
|
||||||
"${SHADERS_DIR}/line.vert.glsl"
|
"${SHADERS_DIR}/line.vert.glsl"
|
||||||
"${SHADERS_DIR}/line.frag.glsl"
|
"${SHADERS_DIR}/line.frag.glsl"
|
||||||
"${SHADERS_DIR}/postfx.vert.glsl"
|
"${SHADERS_DIR}/postfx.vert.glsl"
|
||||||
"${SHADERS_DIR}/postfx.frag.glsl"
|
"${SHADERS_DIR}/postfx.frag.glsl"
|
||||||
|
"${SHADERS_DIR}/bloom.frag.glsl"
|
||||||
)
|
)
|
||||||
find_program(GLSLC_EXE NAMES glslc HINTS ${Vulkan_GLSLC_EXECUTABLE})
|
find_program(GLSLC_EXE NAMES glslc HINTS ${Vulkan_GLSLC_EXECUTABLE})
|
||||||
if(GLSLC_EXE)
|
if(GLSLC_EXE)
|
||||||
|
|||||||
+15
-9
@@ -11,24 +11,30 @@
|
|||||||
# - Background es muy sutil; pasa los componentes G a 0.15-0.20 para
|
# - Background es muy sutil; pasa los componentes G a 0.15-0.20 para
|
||||||
# un fondo verde-tenue más marcado.
|
# un fondo verde-tenue más marcado.
|
||||||
|
|
||||||
# Bloom / glow: desenfoque gaussiano de las regiones brillantes.
|
# Bloom / glow: separable gaussian blur de dues passes (H + V).
|
||||||
|
# Equivalent matemàtic d'un kernel 15×15 dens (225 mostres) però només cosTa
|
||||||
|
# 30 mostres per píxel. Sense moiré: sigma_px controla l'amplada del halo.
|
||||||
bloom:
|
bloom:
|
||||||
enabled: true
|
enabled: true
|
||||||
intensity: 0.6 # 0..2 — cuanto del bloom se suma a la imagen
|
intensity: 1.8 # 0..2 — cuanto del bloom se suma a la imagen
|
||||||
threshold: 0.30 # 0..1 — luminancia mínima que aporta al bloom
|
threshold: 0.20 # 0..1 — luminància mínima que aporta al bloom
|
||||||
radius_px: 2.0 # radio del kernel en píxeles lógicos (1..8 razonable)
|
sigma_px: 5.0 # sigma de la gaussiana en texels (~1.5..6 raonable;
|
||||||
|
# halo ≈ 3·sigma a cada banda. 3.5 → halo de ~10 px)
|
||||||
|
|
||||||
# Flicker: modulación global de brillo (efecto fósforo CRT).
|
# Flicker: modulación global de brillo (efecto fósforo CRT).
|
||||||
# Sustituye a la antigua oscilación CPU del ColorOscillator.
|
# Sustituye a la antigua oscilación CPU del ColorOscillator.
|
||||||
|
# Solo afecta a `(lines + bloom)` en el shader; NO toca el fondo, así que
|
||||||
|
# los píxeles negros siguen siendo negros (no pulsan).
|
||||||
flicker:
|
flicker:
|
||||||
enabled: true
|
enabled: true
|
||||||
amplitude: 0.10 # 0..1 — profundidad del flicker
|
amplitude: 0.18 # 0..1 — profundidad del flicker
|
||||||
frequency_hz: 6.0 # Hz — velocidad de la pulsación
|
frequency_hz: 6.0 # Hz — velocidad de la pulsación
|
||||||
|
|
||||||
# Background pulse: color de fondo oscilante (suma aditiva).
|
# Background pulse: color de fondo oscilante (suma aditiva).
|
||||||
# RGB en [0..255]; el shader normaliza a [0..1].
|
# Desactivado: fondo negro puro. Se mantienen los valores por si queremos
|
||||||
|
# reactivar más adelante un tinte verdoso muy tenue al estilo CRT.
|
||||||
background:
|
background:
|
||||||
enabled: true
|
enabled: false
|
||||||
color_min: [0, 5, 0] # negro casi puro
|
color_min: [0, 0, 0] # negro puro
|
||||||
color_max: [0, 15, 0] # verde muy tenue
|
color_max: [0, 0, 0] # negro puro
|
||||||
pulse_frequency_hz: 6.0 # Hz — sincronizado con flicker por defecto
|
pulse_frequency_hz: 6.0 # Hz — sincronizado con flicker por defecto
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
// Fragment shader del bloom: una passada 1D de blur gaussià separable, amb
|
||||||
|
// high-pass opcional. Es crida dues vegades per frame:
|
||||||
|
//
|
||||||
|
// Pass H: extract=1.0, direction=(1,0). Llegeix l'escena offscreen i
|
||||||
|
// emet a bloom_texture_a aplicant high-pass + gaussiana horitzontal.
|
||||||
|
// Pass V: extract=0.0, direction=(0,1). Llegeix bloom_texture_a i emet
|
||||||
|
// a bloom_texture_b amb la gaussiana vertical (sense high-pass).
|
||||||
|
//
|
||||||
|
// Resultat: equivalent matemàtic d'una convolució 2D de 15×15 mostres denses,
|
||||||
|
// però només costa 2×15 = 30 mostres per píxel. Sense moiré (samples a
|
||||||
|
// distància 1 texel, així que la gaussiana és contínua a l'escala del píxel).
|
||||||
|
//
|
||||||
|
// El paràmetre `sigma` (en texels) controla l'amplada del halo. Per a sigma=4,
|
||||||
|
// el halo cobreix ~12 texels al voltant de cada línia. Pujar sigma engreixa
|
||||||
|
// el halo; cal mantenir-lo ≤ ~5-6 perquè el rang de mostreig (±7 taps) cobreixi
|
||||||
|
// el 99% del gaussià.
|
||||||
|
//
|
||||||
|
// Recursos:
|
||||||
|
// set=2, binding=0 → sampler2D (input)
|
||||||
|
// set=3, binding=0 → uniform buffer (paràmetres)
|
||||||
|
|
||||||
|
layout(set = 2, binding = 0) uniform sampler2D src;
|
||||||
|
|
||||||
|
layout(set = 3, binding = 0) uniform BloomUBO {
|
||||||
|
vec2 texel_size; // 1.0 / texture_size
|
||||||
|
vec2 direction; // (1,0) per pass H, (0,1) per pass V
|
||||||
|
float threshold; // luminància mínima per al high-pass
|
||||||
|
float extract; // 1.0 = aplica high-pass (pass H), 0.0 = blur pur (pass V)
|
||||||
|
float sigma; // sigma de la gaussiana en texels
|
||||||
|
float _pad;
|
||||||
|
} ubo;
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 v_uv;
|
||||||
|
layout(location = 0) out vec4 frag;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec3 sum = vec3(0.0);
|
||||||
|
float total_weight = 0.0;
|
||||||
|
|
||||||
|
// 15 taps: -7..+7, espaiats 1 texel. Cobreix ±7 texels = ±~2σ per σ=3.5.
|
||||||
|
// Per σ més grans, el cua es retalla una mica però el peso del tap 7 ja és
|
||||||
|
// molt baix; visualment no es nota.
|
||||||
|
const int RADIUS = 7;
|
||||||
|
const float TWO_SIGMA_SQ_FACTOR = 2.0; // multiplicador per a 2σ² al denominador
|
||||||
|
|
||||||
|
for (int i = -RADIUS; i <= RADIUS; ++i) {
|
||||||
|
vec2 offset = ubo.direction * float(i) * ubo.texel_size;
|
||||||
|
vec3 c = texture(src, v_uv + offset).rgb;
|
||||||
|
|
||||||
|
// High-pass només a la primera passada: a la segona, c ja és el
|
||||||
|
// resultat de la H i no l'hem de tornar a filtrar.
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
frag = vec4(sum, 1.0);
|
||||||
|
}
|
||||||
+27
-44
@@ -1,37 +1,37 @@
|
|||||||
#version 450
|
#version 450
|
||||||
|
|
||||||
// Fragment shader del pase de postprocesado.
|
// Fragment shader del pase final de composite.
|
||||||
// Lee la textura offscreen (escena vectorial sobre fondo negro) y produce
|
// Llegeix dos samplers: l'escena vectorial i el bloom ja pre-calculat (resultat
|
||||||
// el fragmento final aplicando:
|
// del separable blur de dues passes a bloom.frag.glsl). Aplica:
|
||||||
// 1. Bloom kernel 5×5 con high-pass (solo los brillos por encima de
|
// 1. Mescla del bloom amb la intensitat configurada.
|
||||||
// threshold contribuyen).
|
// 2. Flicker: multiplicador global de brillo modulat per temps.
|
||||||
// 2. Flicker: multiplicador global de brillo modulado por tiempo
|
// 3. Background pulse: color de fons additiu que oscil·la entre min/max.
|
||||||
// (sustituye al oscilador CPU del legacy).
|
//
|
||||||
// 3. Background pulse: color de fondo que oscila entre min y max y se
|
// L'arquitectura anterior tenia el bloom inline (kernel 7×7 single-pass), que
|
||||||
// suma a la imagen (las líneas brillan por encima).
|
// produïa moiré per radis grans. Ara el bloom és pre-computed via separable
|
||||||
|
// gaussian (equivalent a kernel 15×15 dens) i aquí només cal samplejar-lo.
|
||||||
//
|
//
|
||||||
// Resource sets (SDL_gpu):
|
// Resource sets (SDL_gpu):
|
||||||
// set=2, binding=0 → sampler2D (escena offscreen)
|
// set=2, binding=0 → sampler2D (escena offscreen)
|
||||||
// set=3, binding=0 → uniform buffer (parámetros del postpro)
|
// set=2, binding=1 → sampler2D (bloom pre-calculat)
|
||||||
|
// set=3, binding=0 → uniform buffer (paràmetres del postpro)
|
||||||
|
|
||||||
layout(set = 2, binding = 0) uniform sampler2D scene;
|
layout(set = 2, binding = 0) uniform sampler2D scene;
|
||||||
|
layout(set = 2, binding = 1) uniform sampler2D bloom_tex;
|
||||||
|
|
||||||
layout(set = 3, binding = 0) uniform PostFxUBO {
|
layout(set = 3, binding = 0) uniform PostFxUBO {
|
||||||
float time;
|
float time;
|
||||||
float bloom_intensity;
|
float bloom_intensity;
|
||||||
float bloom_threshold;
|
|
||||||
float bloom_radius_px;
|
|
||||||
|
|
||||||
float flicker_amplitude;
|
float flicker_amplitude;
|
||||||
float flicker_frequency_hz;
|
float flicker_frequency_hz;
|
||||||
|
|
||||||
float background_pulse_freq_hz;
|
float background_pulse_freq_hz;
|
||||||
float _pad_a;
|
float _pad_a;
|
||||||
|
float _pad_b;
|
||||||
|
float _pad_c;
|
||||||
|
|
||||||
vec4 background_min; // RGB en [0..1], A=1
|
vec4 background_min; // RGB en [0..1], A=1
|
||||||
vec4 background_max; // 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;
|
} ubo;
|
||||||
|
|
||||||
layout(location = 0) in vec2 v_uv;
|
layout(location = 0) in vec2 v_uv;
|
||||||
@@ -40,43 +40,26 @@ layout(location = 0) out vec4 frag;
|
|||||||
const float TAU = 6.28318530718;
|
const float TAU = 6.28318530718;
|
||||||
|
|
||||||
void main() {
|
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 src = texture(scene, v_uv).rgb;
|
||||||
vec3 bloom = vec3(0.0);
|
vec3 bloom = texture(bloom_tex, v_uv).rgb * ubo.bloom_intensity;
|
||||||
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 ===
|
// === FLICKER ===
|
||||||
// Multiplicador global de brillo. Oscila entre (1.0 - amplitude) y 1.0.
|
// Multiplicador global de brillo. Oscil·la entre (1.0 - amplitude) i 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 pulse = (sin(ubo.time * ubo.flicker_frequency_hz * TAU) * 0.5) + 0.5;
|
||||||
float flicker = 1.0 - (ubo.flicker_amplitude * (1.0 - pulse));
|
float flicker = 1.0 - (ubo.flicker_amplitude * (1.0 - pulse));
|
||||||
|
|
||||||
// === BACKGROUND 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;
|
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);
|
vec3 background = mix(ubo.background_min.rgb, ubo.background_max.rgb, bg_pulse);
|
||||||
|
|
||||||
// === COMPOSICIÓN ===
|
// === COMPOSICIÓ (preserve-core) ===
|
||||||
// El offscreen viene con clear=black, por lo que solo las líneas y el
|
// Bloom additiu però atenuat per (1 - luma_src): no contribueix als píxels
|
||||||
// bloom aportan luz. Sumamos el fondo y luego multiplicamos por flicker
|
// on la línia ja és brillant (manté el color del core sense rentar-lo cap a
|
||||||
// para que el pulso afecte a todo (líneas + bloom + bg).
|
// blanc) i contribueix al màxim als píxels foscos del voltant (halo intens).
|
||||||
vec3 lines_and_glow = (src + bloom) * flicker;
|
// El flicker només multiplica (línies + bloom); el fons va a banda perquè
|
||||||
|
// els píxels foscos no han de pulsar.
|
||||||
|
float src_luma = max(src.r, max(src.g, src.b));
|
||||||
|
vec3 bloom_contribution = bloom * (1.0 - src_luma);
|
||||||
|
vec3 lines_and_glow = (src + bloom_contribution) * flicker;
|
||||||
frag = vec4(background + lines_and_glow, 1.0);
|
frag = vec4(background + lines_and_glow, 1.0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,102 +9,99 @@
|
|||||||
|
|
||||||
namespace Config::PostFx {
|
namespace Config::PostFx {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Helper: lee `key` en `node` solo si existe; deja `dst` intacto en caso
|
// Helper: lee `key` en `node` solo si existe; deja `dst` intacto en caso
|
||||||
// contrario. Así, un YAML parcial sigue funcionando con los defaults del
|
// contrario. Así, un YAML parcial sigue funcionando con los defaults del
|
||||||
// struct para los campos que falten.
|
// struct para los campos que falten.
|
||||||
template <typename T>
|
template <typename T>
|
||||||
void readField(const fkyaml::node& node, const char* key, T& dst) {
|
void readField(const fkyaml::node& node, const char* key, T& dst) {
|
||||||
if (node.contains(key)) {
|
if (node.contains(key)) {
|
||||||
dst = node[key].get_value<T>();
|
dst = node[key].get_value<T>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lee un array RGB [r, g, b] (0..255) y lo normaliza a [0..1] sobre tres
|
// 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 floats. Si la clave no existe o no es secuencia de 3, deja los
|
||||||
// destinos como están.
|
// destinos como están.
|
||||||
void readRgb255(const fkyaml::node& node, const char* key,
|
void readRgb255(const fkyaml::node& node, const char* key, float& dst_r, float& dst_g, float& dst_b) {
|
||||||
float& dst_r, float& dst_g, float& dst_b) {
|
if (!node.contains(key)) {
|
||||||
if (!node.contains(key)) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
const auto& arr = node[key];
|
||||||
const auto& arr = node[key];
|
if (!arr.is_sequence() || arr.size() < 3) {
|
||||||
if (!arr.is_sequence() || arr.size() < 3) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
try {
|
||||||
try {
|
const auto R = arr[0].get_value<int>();
|
||||||
const auto R = arr[0].get_value<int>();
|
const auto G = arr[1].get_value<int>();
|
||||||
const auto G = arr[1].get_value<int>();
|
const auto B = arr[2].get_value<int>();
|
||||||
const auto B = arr[2].get_value<int>();
|
dst_r = static_cast<float>(R) / 255.0F;
|
||||||
dst_r = static_cast<float>(R) / 255.0F;
|
dst_g = static_cast<float>(G) / 255.0F;
|
||||||
dst_g = static_cast<float>(G) / 255.0F;
|
dst_b = static_cast<float>(B) / 255.0F;
|
||||||
dst_b = static_cast<float>(B) / 255.0F;
|
} catch (...) { // @INTENTIONAL
|
||||||
} catch (...) { // @INTENTIONAL
|
// Mantiene los defaults si algún elemento del RGB no es entero parseable
|
||||||
// Mantiene los defaults si algún elemento del RGB no es entero parseable
|
// (el YAML viene de archivo, así que es razonable degradar a los defaults
|
||||||
// (el YAML viene de archivo, así que es razonable degradar a los defaults
|
// en vez de propagar la excepción y abortar el load del postpro entero).
|
||||||
// en vez de propagar la excepción y abortar el load del postpro entero).
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
auto load(const std::string& path) -> Rendering::GPU::PostFxParams {
|
auto load(const std::string& path) -> Rendering::GPU::PostFxParams {
|
||||||
Rendering::GPU::PostFxParams params{}; // valores por defecto del struct
|
Rendering::GPU::PostFxParams params{}; // valores por defecto del struct
|
||||||
|
|
||||||
auto bytes = Resource::Helper::loadFile(path);
|
auto bytes = Resource::Helper::loadFile(path);
|
||||||
if (bytes.empty()) {
|
if (bytes.empty()) {
|
||||||
std::cerr << "[PostFxConfig] No se pudo cargar " << path
|
std::cerr << "[PostFxConfig] No se pudo cargar " << path
|
||||||
<< " — usando defaults built-in\n";
|
<< " — usando defaults built-in\n";
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const auto* begin = reinterpret_cast<const char*>(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);
|
||||||
|
// sigma_px és el paràmetre canònic des del separable blur; acceptem
|
||||||
|
// també `radius_px` com a alias per a configs antigues (s'interpreta
|
||||||
|
// com sigma directament — els valors útils estan al mateix rang ~2-5).
|
||||||
|
readField(node, "sigma_px", params.bloom_sigma_px);
|
||||||
|
readField(node, "radius_px", params.bloom_sigma_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;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const auto* begin = reinterpret_cast<const char*>(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
|
} // namespace Config::PostFx
|
||||||
|
|||||||
@@ -10,11 +10,15 @@
|
|||||||
// usa el color global del oscilador (g_current_line_color).
|
// usa el color global del oscilador (g_current_line_color).
|
||||||
namespace Defaults::Palette {
|
namespace Defaults::Palette {
|
||||||
|
|
||||||
|
// Paleta neon: pujada lleugera dels canals secundaris per millorar la
|
||||||
|
// brillantor perceptual sota el bloom (sense alterar la identitat de color).
|
||||||
|
// El canal dominant es manté a 255 a cada color per maximitzar la saturació
|
||||||
|
// visible quan el halo s'expandeix.
|
||||||
constexpr SDL_Color SHIP = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanco neutro
|
constexpr SDL_Color SHIP = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanco neutro
|
||||||
constexpr SDL_Color BULLET = {.r = 120, .g = 255, .b = 140, .a = 255}; // Verde laser
|
constexpr SDL_Color BULLET = {.r = 155, .g = 255, .b = 175, .a = 255}; // Verde laser
|
||||||
constexpr SDL_Color PENTAGON = {.r = 120, .g = 170, .b = 255, .a = 255}; // Azul "esquivador"
|
constexpr SDL_Color PENTAGON = {.r = 155, .g = 195, .b = 255, .a = 255}; // Azul "esquivador"
|
||||||
constexpr SDL_Color QUADRAT = {.r = 255, .g = 110, .b = 110, .a = 255}; // Rojo "tank"
|
constexpr SDL_Color QUADRAT = {.r = 255, .g = 140, .b = 140, .a = 255}; // Rojo "tank"
|
||||||
constexpr SDL_Color MOLINILLO = {.r = 255, .g = 130, .b = 255, .a = 255}; // Magenta agresivo
|
constexpr SDL_Color MOLINILLO = {.r = 255, .g = 160, .b = 255, .a = 255}; // Magenta agresivo
|
||||||
constexpr SDL_Color WOUNDED = {.r = 255, .g = 215, .b = 0, .a = 255}; // Dorado: enemigo herido
|
constexpr SDL_Color WOUNDED = {.r = 255, .g = 220, .b = 60, .a = 255}; // Dorado: enemigo herido
|
||||||
|
|
||||||
} // namespace Defaults::Palette
|
} // namespace Defaults::Palette
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -28,14 +28,22 @@ namespace Rendering::GPU {
|
|||||||
device_.destroy();
|
device_.destroy();
|
||||||
return false;
|
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).
|
// Pipeline de postpro: escribe sobre swapchain (formato del swapchain).
|
||||||
if (!postfx_pipeline_.init(device_, device_.swapchainFormat())) {
|
if (!postfx_pipeline_.init(device_, device_.swapchainFormat())) {
|
||||||
|
bloom_pipeline_.destroy();
|
||||||
line_pipeline_.destroy();
|
line_pipeline_.destroy();
|
||||||
device_.destroy();
|
device_.destroy();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!createOffscreen()) {
|
if (!createOffscreen()) {
|
||||||
postfx_pipeline_.destroy();
|
postfx_pipeline_.destroy();
|
||||||
|
bloom_pipeline_.destroy();
|
||||||
line_pipeline_.destroy();
|
line_pipeline_.destroy();
|
||||||
device_.destroy();
|
device_.destroy();
|
||||||
return false;
|
return false;
|
||||||
@@ -82,6 +90,18 @@ namespace Rendering::GPU {
|
|||||||
<< SDL_GetError() << '\n';
|
<< SDL_GetError() << '\n';
|
||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +116,14 @@ namespace Rendering::GPU {
|
|||||||
SDL_ReleaseGPUTexture(dev, offscreen_texture_);
|
SDL_ReleaseGPUTexture(dev, offscreen_texture_);
|
||||||
offscreen_texture_ = nullptr;
|
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) {
|
if (linear_sampler_ != nullptr) {
|
||||||
SDL_ReleaseGPUSampler(dev, linear_sampler_);
|
SDL_ReleaseGPUSampler(dev, linear_sampler_);
|
||||||
linear_sampler_ = nullptr;
|
linear_sampler_ = nullptr;
|
||||||
@@ -105,6 +133,7 @@ namespace Rendering::GPU {
|
|||||||
void GpuFrameRenderer::destroy() {
|
void GpuFrameRenderer::destroy() {
|
||||||
destroyOffscreen();
|
destroyOffscreen();
|
||||||
postfx_pipeline_.destroy();
|
postfx_pipeline_.destroy();
|
||||||
|
bloom_pipeline_.destroy();
|
||||||
line_pipeline_.destroy();
|
line_pipeline_.destroy();
|
||||||
device_.destroy();
|
device_.destroy();
|
||||||
vertices_.clear();
|
vertices_.clear();
|
||||||
@@ -388,6 +417,113 @@ namespace Rendering::GPU {
|
|||||||
SDL_ReleaseGPUTransferBuffer(dev, tbo);
|
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() {
|
void GpuFrameRenderer::compositePass() {
|
||||||
// Cierra el render pass actual (sobre offscreen).
|
// Cierra el render pass actual (sobre offscreen).
|
||||||
if (render_pass_ != nullptr) {
|
if (render_pass_ != nullptr) {
|
||||||
@@ -413,11 +549,14 @@ namespace Rendering::GPU {
|
|||||||
|
|
||||||
SDL_BindGPUGraphicsPipeline(render_pass_, postfx_pipeline_.get());
|
SDL_BindGPUGraphicsPipeline(render_pass_, postfx_pipeline_.get());
|
||||||
|
|
||||||
// Bind del sampler (escena offscreen) en slot 0 del fragment shader.
|
// Bind de dos samplers: slot 0 = escena offscreen, slot 1 = bloom V.
|
||||||
SDL_GPUTextureSamplerBinding sampler_binding{};
|
// El bloom V conté ja el resultat de les dues passes separables.
|
||||||
sampler_binding.texture = offscreen_texture_;
|
SDL_GPUTextureSamplerBinding sampler_bindings[2]{};
|
||||||
sampler_binding.sampler = linear_sampler_;
|
sampler_bindings[0].texture = offscreen_texture_;
|
||||||
SDL_BindGPUFragmentSamplers(render_pass_, 0, &sampler_binding, 1);
|
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
|
// Uniforms del postpro. Si una sección está desactivada, anulamos sus
|
||||||
// contribuciones (intensidad / amplitud / max=min) en lugar de tener
|
// contribuciones (intensidad / amplitud / max=min) en lugar de tener
|
||||||
@@ -441,12 +580,12 @@ namespace Rendering::GPU {
|
|||||||
PostFxUniforms ubo{};
|
PostFxUniforms ubo{};
|
||||||
ubo.time = TIME_SECONDS;
|
ubo.time = TIME_SECONDS;
|
||||||
ubo.bloom_intensity = BLOOM_INTENSITY;
|
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_amplitude = FLICKER_AMPLITUDE;
|
||||||
ubo.flicker_frequency_hz = postfx_params_.flicker_frequency_hz;
|
ubo.flicker_frequency_hz = postfx_params_.flicker_frequency_hz;
|
||||||
ubo.background_pulse_freq_hz = postfx_params_.background_pulse_freq_hz;
|
ubo.background_pulse_freq_hz = postfx_params_.background_pulse_freq_hz;
|
||||||
ubo.pad_a = 0.0F;
|
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_r = BG_MIN_R;
|
||||||
ubo.background_min_g = BG_MIN_G;
|
ubo.background_min_g = BG_MIN_G;
|
||||||
ubo.background_min_b = BG_MIN_B;
|
ubo.background_min_b = BG_MIN_B;
|
||||||
@@ -455,11 +594,6 @@ namespace Rendering::GPU {
|
|||||||
ubo.background_max_g = BG_MAX_G;
|
ubo.background_max_g = BG_MAX_G;
|
||||||
ubo.background_max_b = BG_MAX_B;
|
ubo.background_max_b = BG_MAX_B;
|
||||||
ubo.background_max_a = 1.0F;
|
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));
|
SDL_PushGPUFragmentUniformData(cmd_buffer_, 0, &ubo, sizeof(ubo));
|
||||||
|
|
||||||
@@ -472,6 +606,7 @@ namespace Rendering::GPU {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
flushBatch();
|
flushBatch();
|
||||||
|
bloomPass();
|
||||||
compositePass();
|
compositePass();
|
||||||
if (render_pass_ != nullptr) {
|
if (render_pass_ != nullptr) {
|
||||||
SDL_EndGPURenderPass(render_pass_);
|
SDL_EndGPURenderPass(render_pass_);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/rendering/gpu/gpu_bloom_pipeline.hpp"
|
||||||
#include "core/rendering/gpu/gpu_device.hpp"
|
#include "core/rendering/gpu/gpu_device.hpp"
|
||||||
#include "core/rendering/gpu/gpu_line_pipeline.hpp"
|
#include "core/rendering/gpu/gpu_line_pipeline.hpp"
|
||||||
#include "core/rendering/gpu/gpu_postfx_pipeline.hpp"
|
#include "core/rendering/gpu/gpu_postfx_pipeline.hpp"
|
||||||
@@ -33,7 +34,7 @@ namespace Rendering::GPU {
|
|||||||
bool bloom_enabled{true};
|
bool bloom_enabled{true};
|
||||||
float bloom_intensity{0.6F};
|
float bloom_intensity{0.6F};
|
||||||
float bloom_threshold{0.4F};
|
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};
|
bool flicker_enabled{true};
|
||||||
float flicker_amplitude{0.10F};
|
float flicker_amplitude{0.10F};
|
||||||
@@ -124,6 +125,7 @@ namespace Rendering::GPU {
|
|||||||
private:
|
private:
|
||||||
GpuDevice device_;
|
GpuDevice device_;
|
||||||
GpuLinePipeline line_pipeline_;
|
GpuLinePipeline line_pipeline_;
|
||||||
|
GpuBloomPipeline bloom_pipeline_;
|
||||||
GpuPostFxPipeline postfx_pipeline_;
|
GpuPostFxPipeline postfx_pipeline_;
|
||||||
|
|
||||||
// Tamaño lógico del juego: espacio de coordenadas de las primitivas
|
// 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_GPUTextureFormat offscreen_format_{SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM};
|
||||||
SDL_GPUSampler* linear_sampler_{nullptr};
|
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.
|
// Batch del frame en curso.
|
||||||
std::vector<LineVertex> vertices_;
|
std::vector<LineVertex> vertices_;
|
||||||
std::vector<uint16_t> indices_;
|
std::vector<uint16_t> indices_;
|
||||||
@@ -168,6 +177,7 @@ namespace Rendering::GPU {
|
|||||||
[[nodiscard]] auto createOffscreen() -> bool;
|
[[nodiscard]] auto createOffscreen() -> bool;
|
||||||
void destroyOffscreen();
|
void destroyOffscreen();
|
||||||
void flushBatch();
|
void flushBatch();
|
||||||
|
void bloomPass(); // pre-composite: H + V passes sobre les bloom textures
|
||||||
void compositePass();
|
void compositePass();
|
||||||
void applyFinalViewport();
|
void applyFinalViewport();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ namespace Rendering::GPU {
|
|||||||
Msl::POSTFX_FRAG_MSL,
|
Msl::POSTFX_FRAG_MSL,
|
||||||
"postfx_fs",
|
"postfx_fs",
|
||||||
SDL_GPU_SHADERSTAGE_FRAGMENT,
|
SDL_GPU_SHADERSTAGE_FRAGMENT,
|
||||||
/*num_samplers=*/1,
|
/*num_samplers=*/2,
|
||||||
/*num_uniform_buffers=*/1);
|
/*num_uniform_buffers=*/1);
|
||||||
#else
|
#else
|
||||||
SDL_GPUShader* vert = createShaderSPIRV(owner_,
|
SDL_GPUShader* vert = createShaderSPIRV(owner_,
|
||||||
@@ -57,7 +57,7 @@ namespace Rendering::GPU {
|
|||||||
POSTFX_FRAG_SPV_SIZE,
|
POSTFX_FRAG_SPV_SIZE,
|
||||||
"main",
|
"main",
|
||||||
SDL_GPU_SHADERSTAGE_FRAGMENT,
|
SDL_GPU_SHADERSTAGE_FRAGMENT,
|
||||||
/*num_samplers=*/1,
|
/*num_samplers=*/2,
|
||||||
/*num_uniform_buffers=*/1);
|
/*num_uniform_buffers=*/1);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -16,39 +16,37 @@
|
|||||||
|
|
||||||
namespace Rendering::GPU {
|
namespace Rendering::GPU {
|
||||||
|
|
||||||
class GpuDevice;
|
class GpuDevice;
|
||||||
|
|
||||||
// Uniform buffer del postpro. Debe coincidir EXACTAMENTE con
|
// Uniform buffer del composite final. Ha de coincidir EXACTAMENT amb
|
||||||
// shaders/postfx.frag.glsl (layout std140 con vec4 alineadas a 16 bytes).
|
// shaders/postfx.frag.glsl (layout std140 amb vec4 alineades a 16 bytes).
|
||||||
struct PostFxUniforms {
|
// El bloom es calcula en passades separades (veure GpuBloomPipeline) i aquí
|
||||||
float time; // Tiempo acumulado en segundos
|
// només passem la intensitat per a la composició; el threshold/sigma viuen
|
||||||
float bloom_intensity; // Mezcla bloom (0..2)
|
// al UBO del bloom.
|
||||||
float bloom_threshold; // Luminancia mínima high-pass (0..1)
|
struct PostFxUniforms {
|
||||||
float bloom_radius_px; // Radio del kernel en píxeles lógicos
|
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 background_pulse_freq_hz; // Hz
|
||||||
float flicker_frequency_hz; // Hz
|
float pad_a;
|
||||||
float background_pulse_freq_hz; // Hz
|
float pad_b;
|
||||||
float pad_a;
|
float pad_c;
|
||||||
|
|
||||||
float background_min_r; // Color min RGB en [0..1], A=1
|
float background_min_r; // Color min RGB en [0..1], A=1
|
||||||
float background_min_g;
|
float background_min_g;
|
||||||
float background_min_b;
|
float background_min_b;
|
||||||
float background_min_a;
|
float background_min_a;
|
||||||
|
|
||||||
float background_max_r;
|
float background_max_r;
|
||||||
float background_max_g;
|
float background_max_g;
|
||||||
float background_max_b;
|
float background_max_b;
|
||||||
float background_max_a;
|
float background_max_a;
|
||||||
|
};
|
||||||
|
|
||||||
float texel_size_x; // 1.0 / texture_width
|
class GpuPostFxPipeline {
|
||||||
float texel_size_y;
|
public:
|
||||||
float pad_b;
|
|
||||||
float pad_c;
|
|
||||||
};
|
|
||||||
|
|
||||||
class GpuPostFxPipeline {
|
|
||||||
public:
|
|
||||||
GpuPostFxPipeline() = default;
|
GpuPostFxPipeline() = default;
|
||||||
~GpuPostFxPipeline();
|
~GpuPostFxPipeline();
|
||||||
|
|
||||||
@@ -59,14 +57,14 @@ class GpuPostFxPipeline {
|
|||||||
|
|
||||||
// target_format: formato del color target del pase final (swapchain).
|
// target_format: formato del color target del pase final (swapchain).
|
||||||
[[nodiscard]] auto init(const GpuDevice& device,
|
[[nodiscard]] auto init(const GpuDevice& device,
|
||||||
SDL_GPUTextureFormat target_format) -> bool;
|
SDL_GPUTextureFormat target_format) -> bool;
|
||||||
void destroy();
|
void destroy();
|
||||||
|
|
||||||
[[nodiscard]] auto get() const -> SDL_GPUGraphicsPipeline* { return pipeline_; }
|
[[nodiscard]] auto get() const -> SDL_GPUGraphicsPipeline* { return pipeline_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SDL_GPUDevice* owner_{nullptr};
|
SDL_GPUDevice* owner_{nullptr};
|
||||||
SDL_GPUGraphicsPipeline* pipeline_{nullptr};
|
SDL_GPUGraphicsPipeline* pipeline_{nullptr};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Rendering::GPU
|
} // 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__
|
||||||
@@ -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
|
// © 2026 JailDesigner
|
||||||
//
|
//
|
||||||
// IMPORTANT: mantenir sincronitzat a mà amb shaders/postfx.frag.glsl. SDL3 GPU
|
// 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
|
// canvi al struct PostFxUniforms (gpu_postfx_pipeline.hpp), al GLSL o al MSL
|
||||||
// cal replicar-lo a totes tres al mateix commit.
|
// cal replicar-lo a totes tres al mateix commit.
|
||||||
//
|
//
|
||||||
// Composició final: bloom 5×5 amb high-pass, flicker sinusoidal global,
|
// Composite final: llegeix escena + bloom pre-calculat (per bloom.frag.glsl en
|
||||||
// background pulse sumat. Recursos:
|
// separable two-pass) i aplica flicker + background pulse. Recursos:
|
||||||
// - texture2d<float> scene [[texture(0)]] + sampler [[sampler(0)]]
|
// - 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)
|
// - 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
|
#pragma once
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
|
|
||||||
namespace Rendering::GPU::Msl {
|
namespace Rendering::GPU::Msl {
|
||||||
|
|
||||||
inline constexpr const char* POSTFX_FRAG_MSL = R"(
|
inline constexpr const char* POSTFX_FRAG_MSL = R"(
|
||||||
#include <metal_stdlib>
|
#include <metal_stdlib>
|
||||||
using namespace metal;
|
using namespace metal;
|
||||||
|
|
||||||
@@ -30,46 +29,28 @@ struct PostVOut {
|
|||||||
struct PostFxUBO {
|
struct PostFxUBO {
|
||||||
float time;
|
float time;
|
||||||
float bloom_intensity;
|
float bloom_intensity;
|
||||||
float bloom_threshold;
|
|
||||||
float bloom_radius_px;
|
|
||||||
|
|
||||||
float flicker_amplitude;
|
float flicker_amplitude;
|
||||||
float flicker_frequency_hz;
|
float flicker_frequency_hz;
|
||||||
|
|
||||||
float background_pulse_freq_hz;
|
float background_pulse_freq_hz;
|
||||||
float pad_a;
|
float pad_a;
|
||||||
|
float pad_b;
|
||||||
|
float pad_c;
|
||||||
|
|
||||||
float4 background_min;
|
float4 background_min;
|
||||||
float4 background_max;
|
float4 background_max;
|
||||||
|
|
||||||
float2 texel_size;
|
|
||||||
float2 pad_b;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
constant float TAU = 6.28318530718;
|
constant float TAU = 6.28318530718;
|
||||||
|
|
||||||
fragment float4 postfx_fs(PostVOut in [[stage_in]],
|
fragment float4 postfx_fs(PostVOut in [[stage_in]],
|
||||||
texture2d<float> scene [[texture(0)]],
|
texture2d<float> scene [[texture(0)]],
|
||||||
sampler samp [[sampler(0)]],
|
sampler samp_s [[sampler(0)]],
|
||||||
|
texture2d<float> bloom_tex [[texture(1)]],
|
||||||
|
sampler samp_b [[sampler(1)]],
|
||||||
constant PostFxUBO& ubo [[buffer(0)]]) {
|
constant PostFxUBO& ubo [[buffer(0)]]) {
|
||||||
// === BLOOM ===
|
float3 src = scene.sample(samp_s, in.uv).rgb;
|
||||||
float3 src = scene.sample(samp, in.uv).rgb;
|
float3 bloom = bloom_tex.sample(samp_b, in.uv).rgb * ubo.bloom_intensity;
|
||||||
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;
|
|
||||||
|
|
||||||
// === FLICKER ===
|
// === FLICKER ===
|
||||||
float pulse = (sin(ubo.time * ubo.flicker_frequency_hz * TAU) * 0.5) + 0.5;
|
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;
|
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);
|
float3 background = mix(ubo.background_min.rgb, ubo.background_max.rgb, bg_pulse);
|
||||||
|
|
||||||
// === COMPOSICIÓ ===
|
// === COMPOSICIÓ (preserve-core) ===
|
||||||
float3 lines_and_glow = (src + bloom) * flicker;
|
// 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);
|
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
@@ -19,6 +19,7 @@ set(SHADER_SOURCES
|
|||||||
"line.frag.glsl"
|
"line.frag.glsl"
|
||||||
"postfx.vert.glsl"
|
"postfx.vert.glsl"
|
||||||
"postfx.frag.glsl"
|
"postfx.frag.glsl"
|
||||||
|
"bloom.frag.glsl"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Nom de la variable C++ per a cada shader (mateix ordre).
|
# Nom de la variable C++ per a cada shader (mateix ordre).
|
||||||
@@ -28,6 +29,7 @@ set(SHADER_VARS
|
|||||||
"LINE_FRAG_SPV"
|
"LINE_FRAG_SPV"
|
||||||
"POSTFX_VERT_SPV"
|
"POSTFX_VERT_SPV"
|
||||||
"POSTFX_FRAG_SPV"
|
"POSTFX_FRAG_SPV"
|
||||||
|
"BLOOM_FRAG_SPV"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Flags extra per a cada shader (necessaris perquè .vert.glsl/.frag.glsl no s'infereixen)
|
# Flags extra per a cada shader (necessaris perquè .vert.glsl/.frag.glsl no s'infereixen)
|
||||||
@@ -36,6 +38,7 @@ set(SHADER_FLAGS
|
|||||||
"-fshader-stage=frag"
|
"-fshader-stage=frag"
|
||||||
"-fshader-stage=vert"
|
"-fshader-stage=vert"
|
||||||
"-fshader-stage=frag"
|
"-fshader-stage=frag"
|
||||||
|
"-fshader-stage=frag"
|
||||||
)
|
)
|
||||||
|
|
||||||
list(LENGTH SHADER_SOURCES NUM_SHADERS)
|
list(LENGTH SHADER_SOURCES NUM_SHADERS)
|
||||||
|
|||||||
Reference in New Issue
Block a user