postfx: scanlines analítiques amb smoothstep a l'estil crtpi, exposant forma per preset YAML
This commit is contained in:
1 parent
4dc2654a66
commit
c9e6261cf1
8 files changed
+1287
-901
No files matched your search
+22
-17
@@ -6,7 +6,9 @@
|
|||||||
// xxd -i postfx.frag.spv > ../../source/core/rendering/sdl3gpu/postfx_frag_spv.h
|
// xxd -i postfx.frag.spv > ../../source/core/rendering/sdl3gpu/postfx_frag_spv.h
|
||||||
//
|
//
|
||||||
// PostFXUniforms must match exactly the C++ struct in sdl3gpu_shader.hpp
|
// 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) in vec2 v_uv;
|
||||||
layout(location = 0) out vec4 out_color;
|
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 pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
|
||||||
float time; // seconds since SDL init
|
float time; // seconds since SDL init
|
||||||
float oversample; // supersampling factor (1.0 = off, 3.0 = 3×SS)
|
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;
|
} u;
|
||||||
|
|
||||||
// YCbCr helpers for NTSC bleeding
|
// YCbCr helpers for NTSC bleeding
|
||||||
@@ -96,22 +103,20 @@ void main() {
|
|||||||
colour = mix(colour, lin, u.gamma_strength);
|
colour = mix(colour, lin, u.gamma_strength);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scanlines — proporción 2/3 brillantes + 1/3 oscuras por fila lógica.
|
// Scanlines — tècnica dels 3 subpíxels verticals per píxel lògic (aee/projecte_2026):
|
||||||
// Casos especiales: 1 subfila → sin efecto; 2 subfilas → 1+1 (50/50).
|
// franja fosca ocupant `scan_dark_ratio` al final de cada fila lògica. La transició es
|
||||||
// Constantes ajustables:
|
// suavitza amb smoothstep d'ample ≈ 1 píxel físic (estil crtpi: filtratge analític
|
||||||
const float SCAN_DARK_RATIO = 0.333; // fracción de subfilas oscuras (ps >= 3)
|
// continu), controlat per `scan_edge_soft`. A 0 és equivalent al step dur antic.
|
||||||
const float SCAN_DARK_FLOOR = 0.42; // multiplicador de brillo de subfilas oscuras
|
|
||||||
if (u.scanline_strength > 0.0) {
|
if (u.scanline_strength > 0.0) {
|
||||||
float ps = max(1.0, round(u.pixel_scale));
|
float ps = max(u.pixel_scale, 1.0);
|
||||||
float frac_in_row = fract(uv.y * u.screen_height);
|
float sub = fract(uv.y * u.screen_height); // [0,1) dins la fila lògica
|
||||||
float row_pos = floor(frac_in_row * ps);
|
float dark_center = 1.0 - u.scan_dark_ratio * 0.5; // centre de la franja fosca
|
||||||
// bright_rows: cuántas subfilas son brillantes
|
float d = abs(sub - dark_center);
|
||||||
// ps==1 → ps (todo brillante → is_dark nunca se activa)
|
d = min(d, 1.0 - d); // wrap a la fila següent
|
||||||
// ps==2 → 1 brillante + 1 oscura
|
float half_width = u.scan_dark_ratio * 0.5;
|
||||||
// ps>=3 → floor(ps * (1 - DARK_RATIO)) brillantes
|
float softness = u.scan_edge_soft * 0.5 / ps; // mig píxel físic a cada costat
|
||||||
float bright_rows = (ps < 2.0) ? ps : ((ps < 3.0) ? 1.0 : floor(ps * (1.0 - SCAN_DARK_RATIO)));
|
float band = 1.0 - smoothstep(half_width - softness, half_width + softness, d);
|
||||||
float is_dark = step(bright_rows, row_pos);
|
float scan = mix(1.0, u.scan_dark_floor, band);
|
||||||
float scan = mix(1.0, SCAN_DARK_FLOOR, is_dark);
|
|
||||||
colour *= mix(1.0, scan, u.scanline_strength);
|
colour *= mix(1.0, scan, u.scanline_strength);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,11 @@
|
|||||||
|
|
||||||
// Fragment shader del shader "postfx": vignette, chroma, scanlines, mask,
|
// Fragment shader del shader "postfx": vignette, chroma, scanlines, mask,
|
||||||
// gamma, curvature, bleeding i flicker. Els paràmetres venen via uniforms.
|
// 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 {
|
namespace Rendering::Msl {
|
||||||
|
|
||||||
inline constexpr const char* kPostfxFrag = R"(
|
inline constexpr const char* kPostfxFrag = R"(
|
||||||
@@ -28,6 +33,11 @@ struct PostFXUniforms {
|
|||||||
float time;
|
float time;
|
||||||
float oversample;
|
float oversample;
|
||||||
float flicker;
|
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) {
|
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);
|
colour = mix(colour, lin, u.gamma_strength);
|
||||||
}
|
}
|
||||||
|
|
||||||
const float SCAN_DARK_RATIO = 0.333f;
|
// Scanlines — 3 subpíxels per fila lògica (2 brillants + 1 fosca). Transició
|
||||||
const float SCAN_DARK_FLOOR = 0.42f;
|
// 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) {
|
if (u.scanline_strength > 0.0f) {
|
||||||
float ps = max(1.0f, round(u.pixel_scale));
|
float ps = max(u.pixel_scale, 1.0f);
|
||||||
float frac_in_row = fract(uv.y * u.screen_height);
|
float sub = fract(uv.y * u.screen_height);
|
||||||
float row_pos = floor(frac_in_row * ps);
|
float dark_center = 1.0f - u.scan_dark_ratio * 0.5f;
|
||||||
float bright_rows = (ps < 2.0f) ? ps : ((ps < 3.0f) ? 1.0f : floor(ps * (1.0f - SCAN_DARK_RATIO)));
|
float d = abs(sub - dark_center);
|
||||||
float is_dark = step(bright_rows, row_pos);
|
d = min(d, 1.0f - d);
|
||||||
float scan = mix(1.0f, SCAN_DARK_FLOOR, is_dark);
|
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);
|
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_);
|
const SDL_GPUViewport VP = computeViewport(scaling_mode_, sw, sh, game_width_, game_height_);
|
||||||
SDL_SetGPUViewport(pass, &VP);
|
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_BindGPUGraphicsPipeline(pass, pipeline_);
|
||||||
|
|
||||||
SDL_GPUTextureSamplerBinding binding{};
|
SDL_GPUTextureSamplerBinding binding{};
|
||||||
@@ -234,6 +240,9 @@ namespace Rendering {
|
|||||||
uniforms_.curvature = params.curvature;
|
uniforms_.curvature = params.curvature;
|
||||||
uniforms_.bleeding = params.bleeding;
|
uniforms_.bleeding = params.bleeding;
|
||||||
uniforms_.flicker = params.flicker;
|
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) {
|
void SDL3GPUShader::setVSync(bool vsync) {
|
||||||
|
|||||||
@@ -11,20 +11,28 @@
|
|||||||
#include "core/rendering/shader_backend.hpp"
|
#include "core/rendering/shader_backend.hpp"
|
||||||
|
|
||||||
// Uniforms enviats al fragment stage cada frame.
|
// 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 {
|
struct PostFXUniforms {
|
||||||
|
// vec4 #0
|
||||||
float vignette_strength;
|
float vignette_strength;
|
||||||
float chroma_strength;
|
float chroma_strength;
|
||||||
float scanline_strength;
|
float scanline_strength;
|
||||||
float screen_height;
|
float screen_height;
|
||||||
|
// vec4 #1
|
||||||
float mask_strength;
|
float mask_strength;
|
||||||
float gamma_strength;
|
float gamma_strength;
|
||||||
float curvature;
|
float curvature;
|
||||||
float bleeding;
|
float bleeding;
|
||||||
|
// vec4 #2
|
||||||
float pixel_scale;
|
float pixel_scale;
|
||||||
float time;
|
float time;
|
||||||
float oversample;
|
float oversample;
|
||||||
float flicker;
|
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 {
|
namespace Rendering {
|
||||||
@@ -96,6 +104,11 @@ namespace Rendering {
|
|||||||
.time = 0.0F,
|
.time = 0.0F,
|
||||||
.oversample = 1.0F,
|
.oversample = 1.0F,
|
||||||
.flicker = 0.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};
|
int game_width_{0};
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -25,6 +25,10 @@ namespace Rendering {
|
|||||||
float curvature{0.0F};
|
float curvature{0.0F};
|
||||||
float bleeding{0.0F};
|
float bleeding{0.0F};
|
||||||
float flicker{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).
|
// Paràmetres del shader CRT-Pi (algoritme de scanlines amb pesos gaussians).
|
||||||
|
|||||||
@@ -224,6 +224,9 @@ namespace {
|
|||||||
readField(node, "curvature", preset.curvature);
|
readField(node, "curvature", preset.curvature);
|
||||||
readField(node, "bleeding", preset.bleeding);
|
readField(node, "bleeding", preset.bleeding);
|
||||||
readField(node, "flicker", preset.flicker);
|
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) {
|
void writePostFXPreset(std::ostringstream& out, const Options::PostFXPreset& preset) {
|
||||||
@@ -236,6 +239,9 @@ namespace {
|
|||||||
out << " curvature: " << preset.curvature << "\n";
|
out << " curvature: " << preset.curvature << "\n";
|
||||||
out << " bleeding: " << preset.bleeding << "\n";
|
out << " bleeding: " << preset.bleeding << "\n";
|
||||||
out << " flicker: " << preset.flicker << "\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.
|
// Presets per defecte del shader CRT-Pi, portats de projecte_2026.
|
||||||
@@ -642,6 +648,9 @@ namespace Options {
|
|||||||
.curvature = IT->curvature,
|
.curvature = IT->curvature,
|
||||||
.bleeding = IT->bleeding,
|
.bleeding = IT->bleeding,
|
||||||
.flicker = IT->flicker,
|
.flicker = IT->flicker,
|
||||||
|
.scan_dark_ratio = IT->scan_dark_ratio,
|
||||||
|
.scan_dark_floor = IT->scan_dark_floor,
|
||||||
|
.scan_edge_soft = IT->scan_edge_soft,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ namespace Options {
|
|||||||
float curvature{0.0F};
|
float curvature{0.0F};
|
||||||
float bleeding{0.0F};
|
float bleeding{0.0F};
|
||||||
float flicker{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).
|
// Preset del shader CRT-Pi (un per entrada a crtpi.yaml).
|
||||||
|
|||||||
Reference in new issue
Block a user