postfx: scanlines analítiques amb smoothstep a l'estil crtpi, exposant forma per preset YAML

This commit is contained in:
JailDesigner committed 2026-04-20 20:57:14 +02:00
1 parent 4dc2654a66
commit c9e6261cf1
8 files changed
+1287 -901

No files matched your search

+22 -17
View File
@@ -6,7 +6,9 @@
// xxd -i postfx.frag.spv > ../../source/core/rendering/sdl3gpu/postfx_frag_spv.h
//
// PostFXUniforms must match exactly the C++ struct in sdl3gpu_shader.hpp
// (8 floats, 32 bytes, std140/scalar layout).
// (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;
@@ -25,7 +27,12 @@ layout(set = 3, binding = 0) uniform PostFXUniforms {
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
float time; // seconds since SDL init
float oversample; // supersampling factor (1.0 = off, 3.0 = 3×SS)
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz — 48 bytes total (3 × 16)
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz
// 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;
// YCbCr helpers for NTSC bleeding
@@ -96,22 +103,20 @@ void main() {
colour = mix(colour, lin, u.gamma_strength);
}
// Scanlines — proporción 2/3 brillantes + 1/3 oscuras por fila lógica.
// Casos especiales: 1 subfila → sin efecto; 2 subfilas → 1+1 (50/50).
// Constantes ajustables:
const float SCAN_DARK_RATIO = 0.333; // fracción de subfilas oscuras (ps >= 3)
const float SCAN_DARK_FLOOR = 0.42; // multiplicador de brillo de subfilas oscuras
// 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(1.0, round(u.pixel_scale));
float frac_in_row = fract(uv.y * u.screen_height);
float row_pos = floor(frac_in_row * ps);
// bright_rows: cuántas subfilas son brillantes
// ps==1 → ps (todo brillante → is_dark nunca se activa)
// ps==2 → 1 brillante + 1 oscura
// ps>=3 → floor(ps * (1 - DARK_RATIO)) brillantes
float bright_rows = (ps < 2.0) ? ps : ((ps < 3.0) ? 1.0 : floor(ps * (1.0 - SCAN_DARK_RATIO)));
float is_dark = step(bright_rows, row_pos);
float scan = mix(1.0, SCAN_DARK_FLOOR, is_dark);
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);
}
@@ -4,6 +4,11 @@
// Fragment shader del shader "postfx": vignette, chroma, scanlines, mask,
// gamma, curvature, bleeding i flicker. Els paràmetres venen via uniforms.
//
// IMPORTANT: mantenir sincronitzat a mà amb data/shaders/postfx.frag. SDL3 GPU
// compila aquest string MSL en runtime; no hi ha generador automàtic. Qualsevol
// canvi a la struct d'uniforms o a la lògica del GLSL cal replicar-lo ací al
// mateix commit. Mida total = 64 bytes (4 × vec4).
namespace Rendering::Msl {
inline constexpr const char* kPostfxFrag = R"(
@@ -28,6 +33,11 @@ struct PostFXUniforms {
float time;
float oversample;
float flicker;
// vec4 #3 — paràmetres de scanlines (exposats per preset YAML)
float scan_dark_ratio;
float scan_dark_floor;
float scan_edge_soft;
float pad3;
};
static float3 rgb_to_ycc(float3 rgb) {
@@ -93,15 +103,19 @@ fragment float4 postfx_fs(PostVOut in [[stage_in]],
colour = mix(colour, lin, u.gamma_strength);
}
const float SCAN_DARK_RATIO = 0.333f;
const float SCAN_DARK_FLOOR = 0.42f;
// Scanlines — 3 subpíxels per fila lògica (2 brillants + 1 fosca). Transició
// suavitzada amb smoothstep d'ample ≈ 1 píxel físic (estil crtpi: filtratge
// analític continu). scan_edge_soft = 0 recupera el step dur de l'original.
if (u.scanline_strength > 0.0f) {
float ps = max(1.0f, round(u.pixel_scale));
float frac_in_row = fract(uv.y * u.screen_height);
float row_pos = floor(frac_in_row * ps);
float bright_rows = (ps < 2.0f) ? ps : ((ps < 3.0f) ? 1.0f : floor(ps * (1.0f - SCAN_DARK_RATIO)));
float is_dark = step(bright_rows, row_pos);
float scan = mix(1.0f, SCAN_DARK_FLOOR, is_dark);
float ps = max(u.pixel_scale, 1.0f);
float sub = fract(uv.y * u.screen_height);
float dark_center = 1.0f - u.scan_dark_ratio * 0.5f;
float d = abs(sub - dark_center);
d = min(d, 1.0f - d);
float half_width = u.scan_dark_ratio * 0.5f;
float softness = u.scan_edge_soft * 0.5f / ps;
float band = 1.0f - smoothstep(half_width - softness, half_width + softness, d);
float scan = mix(1.0f, u.scan_dark_floor, band);
colour *= mix(1.0f, scan, u.scanline_strength);
}
@@ -204,6 +204,12 @@ namespace Rendering {
const SDL_GPUViewport VP = computeViewport(scaling_mode_, sw, sh, game_width_, game_height_);
SDL_SetGPUViewport(pass, &VP);
// pixel_scale = píxels físics per fila lògica al viewport. El shader l'usa
// per ajustar l'ample del smoothstep de scanlines a 1 píxel físic (estil crtpi).
uniforms_.pixel_scale = (game_height_ > 0)
? VP.h / static_cast<float>(game_height_)
: 1.0F;
SDL_BindGPUGraphicsPipeline(pass, pipeline_);
SDL_GPUTextureSamplerBinding binding{};
@@ -234,6 +240,9 @@ namespace Rendering {
uniforms_.curvature = params.curvature;
uniforms_.bleeding = params.bleeding;
uniforms_.flicker = params.flicker;
uniforms_.scan_dark_ratio = params.scan_dark_ratio;
uniforms_.scan_dark_floor = params.scan_dark_floor;
uniforms_.scan_edge_soft = params.scan_edge_soft;
}
void SDL3GPUShader::setVSync(bool vsync) {
@@ -11,20 +11,28 @@
#include "core/rendering/shader_backend.hpp"
// Uniforms enviats al fragment stage cada frame.
// Han de coincidir amb l'estructura MSL/GLSL (48 bytes, alineats a 16).
// Han de coincidir amb l'estructura MSL/GLSL (64 bytes = 4 × vec4, alineats a 16).
struct PostFXUniforms {
// vec4 #0
float vignette_strength;
float chroma_strength;
float scanline_strength;
float screen_height;
// vec4 #1
float mask_strength;
float gamma_strength;
float curvature;
float bleeding;
// vec4 #2
float pixel_scale;
float time;
float oversample;
float flicker;
// vec4 #3 — paràmetres de scanlines exposats al preset YAML
float scan_dark_ratio;
float scan_dark_floor;
float scan_edge_soft;
float pad3;
};
namespace Rendering {
@@ -96,6 +104,11 @@ namespace Rendering {
.time = 0.0F,
.oversample = 1.0F,
.flicker = 0.0F,
// Defaults canònics: 2/3 brillant + 1/3 fosca, brillantor 42%, suavitzat complet (1 px físic).
.scan_dark_ratio = 0.333F,
.scan_dark_floor = 0.42F,
.scan_edge_soft = 1.0F,
.pad3 = 0.0F,
};
int game_width_{0};
File diff suppressed because it is too large. Load diff
+4
View File
@@ -25,6 +25,10 @@ namespace Rendering {
float curvature{0.0F};
float bleeding{0.0F};
float flicker{0.0F};
// Forma de les scanlines — 3 subpíxels per fila lògica per defecte.
float scan_dark_ratio{0.333F}; // fracció obscura (1/3)
float scan_dark_floor{0.42F}; // brillantor subfila fosca
float scan_edge_soft{1.0F}; // 0 = step dur; 1 = suavitzat 1 px físic
};
// Paràmetres del shader CRT-Pi (algoritme de scanlines amb pesos gaussians).
+9
View File
@@ -224,6 +224,9 @@ namespace {
readField(node, "curvature", preset.curvature);
readField(node, "bleeding", preset.bleeding);
readField(node, "flicker", preset.flicker);
readField(node, "scan_dark_ratio", preset.scan_dark_ratio);
readField(node, "scan_dark_floor", preset.scan_dark_floor);
readField(node, "scan_edge_soft", preset.scan_edge_soft);
}
void writePostFXPreset(std::ostringstream& out, const Options::PostFXPreset& preset) {
@@ -236,6 +239,9 @@ namespace {
out << " curvature: " << preset.curvature << "\n";
out << " bleeding: " << preset.bleeding << "\n";
out << " flicker: " << preset.flicker << "\n";
out << " scan_dark_ratio: " << preset.scan_dark_ratio << "\n";
out << " scan_dark_floor: " << preset.scan_dark_floor << "\n";
out << " scan_edge_soft: " << preset.scan_edge_soft << "\n";
}
// Presets per defecte del shader CRT-Pi, portats de projecte_2026.
@@ -642,6 +648,9 @@ namespace Options {
.curvature = IT->curvature,
.bleeding = IT->bleeding,
.flicker = IT->flicker,
.scan_dark_ratio = IT->scan_dark_ratio,
.scan_dark_floor = IT->scan_dark_floor,
.scan_edge_soft = IT->scan_edge_soft,
};
}
+4
View File
@@ -53,6 +53,10 @@ namespace Options {
float curvature{0.0F};
float bleeding{0.0F};
float flicker{0.0F};
// Forma de les scanlines (3 subpíxels per fila lògica per defecte).
float scan_dark_ratio{0.333F};
float scan_dark_floor{0.42F};
float scan_edge_soft{1.0F};
};
// Preset del shader CRT-Pi (un per entrada a crtpi.yaml).