Files
jaildoctors-dilemma/data/shaders/postfx.frag
T
JailDesigner 5e6a469d46 postfx analític: nou shader + estructures chroma_min/max + scan_*
- 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>
2026-05-17 20:32:34 +02:00

172 lines
6.9 KiB
GLSL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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);
}