5e6a469d46
- Substitueix postfx.frag per la versió analítica amb smoothstep
- PostFXUniforms 12→16 floats (64B, 4×vec4): afegeix chroma_min/max,
scan_dark_ratio, scan_dark_floor, scan_edge_soft
- PostFXParams i PostFXPreset adopten els nous camps amb defaults d'AEE
- MSL extret a source/core/rendering/sdl3gpu/msl/{postfx_vert,postfx_frag,
crtpi_frag}.msl.h (estil Rendering::Msl::kXxx)
- SPIR-V regenerat (postfx_frag_spv.h: 13648 bytes)
- options.cpp llegeix 'chroma' antic com compat (assigna a min i max);
escriu els 6 presets per defecte (CRT/NTSC/CURVED/SCANLINES/SUBTLE/CRT LIVE)
amb els valors d'aee_arcade
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
172 lines
6.9 KiB
GLSL
172 lines
6.9 KiB
GLSL
#version 450
|
||
|
||
// Vulkan GLSL fragment shader — PostFX effects
|
||
// Used for SDL3 GPU API (SPIR-V path, Win/Linux).
|
||
// Compile: glslc postfx.frag -o postfx.frag.spv
|
||
// xxd -i postfx.frag.spv > ../../source/core/rendering/sdl3gpu/postfx_frag_spv.h
|
||
//
|
||
// PostFXUniforms must match exactly the C++ struct in sdl3gpu_shader.hpp
|
||
// (16 floats = 4 × vec4 = 64 bytes, std140/scalar layout).
|
||
// IMPORTANT: Qualsevol canvi ací cal replicar-lo a mà a
|
||
// source/core/rendering/sdl3gpu/msl/postfx_frag.msl.h (no hi ha generador).
|
||
|
||
layout(location = 0) in vec2 v_uv;
|
||
layout(location = 0) out vec4 out_color;
|
||
|
||
layout(set = 2, binding = 0) uniform sampler2D scene;
|
||
|
||
layout(set = 3, binding = 0) uniform PostFXUniforms {
|
||
float vignette_strength;
|
||
float chroma_min; // intensitat mínima de l'aberració cromàtica
|
||
float scanline_strength;
|
||
float screen_height;
|
||
float mask_strength;
|
||
float gamma_strength;
|
||
float curvature;
|
||
float bleeding;
|
||
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
|
||
float time; // seconds since SDL init
|
||
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz
|
||
float chroma_max; // intensitat màxima; si == chroma_min → chroma estàtic
|
||
// vec4 #3 — paràmetres de scanlines (exposats per preset YAML)
|
||
float scan_dark_ratio; // fracció de subfila fosca per fila lògica (1/3 ≈ 0.333)
|
||
float scan_dark_floor; // multiplicador de brillantor de la subfila fosca
|
||
float scan_edge_soft; // 0 = step dur; 1 = suavitzat d'1 píxel físic (estil crtpi)
|
||
float pad3; // padding per tancar a 64 bytes (4 × vec4)
|
||
} u;
|
||
|
||
// Mostreig bilinear horitzontal d'un canal RGB. Evita el "tic-tac" del sampler
|
||
// NEAREST quan l'offset de chroma és subpíxel: sense interpolar, l'offset
|
||
// arrodonia entre 1 i 2 píxels i el drift temporal feia un parpelleig discret.
|
||
float sampleBilinearX(vec2 uv_target, int channel) {
|
||
vec2 tex_size = vec2(textureSize(scene, 0));
|
||
float px = uv_target.x * tex_size.x - 0.5;
|
||
float p_floor = floor(px);
|
||
float f = px - p_floor;
|
||
vec4 c0 = texture(scene, vec2((p_floor + 0.5) / tex_size.x, uv_target.y));
|
||
vec4 c1 = texture(scene, vec2((p_floor + 1.5) / tex_size.x, uv_target.y));
|
||
return mix(c0[channel], c1[channel], f);
|
||
}
|
||
|
||
// YCbCr helpers for NTSC bleeding
|
||
vec3 rgb_to_ycc(vec3 rgb) {
|
||
return vec3(
|
||
0.299*rgb.r + 0.587*rgb.g + 0.114*rgb.b,
|
||
-0.169*rgb.r - 0.331*rgb.g + 0.500*rgb.b + 0.5,
|
||
0.500*rgb.r - 0.419*rgb.g - 0.081*rgb.b + 0.5
|
||
);
|
||
}
|
||
vec3 ycc_to_rgb(vec3 ycc) {
|
||
float y = ycc.x;
|
||
float cb = ycc.y - 0.5;
|
||
float cr = ycc.z - 0.5;
|
||
return clamp(vec3(
|
||
y + 1.402*cr,
|
||
y - 0.344*cb - 0.714*cr,
|
||
y + 1.772*cb
|
||
), 0.0, 1.0);
|
||
}
|
||
|
||
void main() {
|
||
vec2 uv = v_uv;
|
||
|
||
// Curvatura barrel CRT
|
||
if (u.curvature > 0.0) {
|
||
vec2 c = uv - 0.5;
|
||
float rsq = dot(c, c);
|
||
vec2 dist = vec2(0.05, 0.1) * u.curvature;
|
||
vec2 barrelScale = vec2(1.0) - 0.23 * dist;
|
||
c += c * (dist * rsq);
|
||
c *= barrelScale;
|
||
if (abs(c.x) >= 0.5 || abs(c.y) >= 0.5) {
|
||
out_color = vec4(0.0, 0.0, 0.0, 1.0);
|
||
return;
|
||
}
|
||
uv = c + 0.5;
|
||
}
|
||
|
||
// Muestra base
|
||
vec3 base = texture(scene, uv).rgb;
|
||
|
||
// Sangrado NTSC — difuminado horizontal de crominancia.
|
||
// step = 1 pixel lógico de juego en UV.
|
||
vec3 colour;
|
||
if (u.bleeding > 0.0) {
|
||
float tw = float(textureSize(scene, 0).x);
|
||
float step = 1.0 / tw; // 1 pixel lógico en UV
|
||
vec3 ycc = rgb_to_ycc(base);
|
||
vec3 ycc_l2 = rgb_to_ycc(texture(scene, uv - vec2(2.0*step, 0.0)).rgb);
|
||
vec3 ycc_l1 = rgb_to_ycc(texture(scene, uv - vec2(1.0*step, 0.0)).rgb);
|
||
vec3 ycc_r1 = rgb_to_ycc(texture(scene, uv + vec2(1.0*step, 0.0)).rgb);
|
||
vec3 ycc_r2 = rgb_to_ycc(texture(scene, uv + vec2(2.0*step, 0.0)).rgb);
|
||
ycc.yz = (ycc_l2.yz + ycc_l1.yz*2.0 + ycc.yz*2.0 + ycc_r1.yz*2.0 + ycc_r2.yz) / 8.0;
|
||
colour = mix(base, ycc_to_rgb(ycc), u.bleeding);
|
||
} else {
|
||
colour = base;
|
||
}
|
||
|
||
// Aberración cromática — intensitat varia entre chroma_min i chroma_max amb
|
||
// una sinusoidal (si min == max, queda estàtica). Mostreig bilinear horitzontal
|
||
// per evitar el "tic-tac" del NEAREST sampler quan l'offset és subpíxel.
|
||
if (u.chroma_min > 0.0 || u.chroma_max > 0.0) {
|
||
float ca = mix(u.chroma_min, u.chroma_max, 0.5 + 0.5 * sin(u.time * 7.3)) * 0.005;
|
||
colour.r = sampleBilinearX(uv + vec2(ca, 0.0), 0);
|
||
colour.b = sampleBilinearX(uv - vec2(ca, 0.0), 2);
|
||
}
|
||
|
||
// Corrección gamma (linealizar antes de scanlines, codificar después)
|
||
if (u.gamma_strength > 0.0) {
|
||
vec3 lin = pow(colour, vec3(2.4));
|
||
colour = mix(colour, lin, u.gamma_strength);
|
||
}
|
||
|
||
// Scanlines — tècnica dels 3 subpíxels verticals per píxel lògic (aee/projecte_2026):
|
||
// franja fosca ocupant `scan_dark_ratio` al final de cada fila lògica. La transició es
|
||
// suavitza amb smoothstep d'ample ≈ 1 píxel físic (estil crtpi: filtratge analític
|
||
// continu), controlat per `scan_edge_soft`. A 0 és equivalent al step dur antic.
|
||
if (u.scanline_strength > 0.0) {
|
||
float ps = max(u.pixel_scale, 1.0);
|
||
float sub = fract(uv.y * u.screen_height); // [0,1) dins la fila lògica
|
||
float dark_center = 1.0 - u.scan_dark_ratio * 0.5; // centre de la franja fosca
|
||
float d = abs(sub - dark_center);
|
||
d = min(d, 1.0 - d); // wrap a la fila següent
|
||
float half_width = u.scan_dark_ratio * 0.5;
|
||
float softness = u.scan_edge_soft * 0.5 / ps; // mig píxel físic a cada costat
|
||
float band = 1.0 - smoothstep(half_width - softness, half_width + softness, d);
|
||
float scan = mix(1.0, u.scan_dark_floor, band);
|
||
colour *= mix(1.0, scan, u.scanline_strength);
|
||
}
|
||
|
||
if (u.gamma_strength > 0.0) {
|
||
vec3 enc = pow(colour, vec3(1.0 / 2.2));
|
||
colour = mix(colour, enc, u.gamma_strength);
|
||
}
|
||
|
||
// Viñeta
|
||
vec2 d = uv - 0.5;
|
||
float vignette = 1.0 - dot(d, d) * u.vignette_strength;
|
||
colour *= clamp(vignette, 0.0, 1.0);
|
||
|
||
// Máscara de fósforo RGB — después de scanlines (orden original):
|
||
// filas brillantes saturadas → máscara invisible, filas oscuras → RGB visible.
|
||
if (u.mask_strength > 0.0) {
|
||
float whichMask = fract(gl_FragCoord.x * 0.3333333);
|
||
vec3 mask = vec3(0.80);
|
||
if (whichMask < 0.3333333)
|
||
mask.x = 1.0;
|
||
else if (whichMask < 0.6666666)
|
||
mask.y = 1.0;
|
||
else
|
||
mask.z = 1.0;
|
||
colour = mix(colour, colour * mask, u.mask_strength);
|
||
}
|
||
|
||
// Parpadeo de fósforo CRT (~50 Hz)
|
||
if (u.flicker > 0.0) {
|
||
float flicker_wave = sin(u.time * 100.0) * 0.5 + 0.5;
|
||
colour *= 1.0 - u.flicker * 0.04 * flicker_wave;
|
||
}
|
||
|
||
out_color = vec4(colour, 1.0);
|
||
}
|