diff --git a/data/shaders/postfx.frag b/data/shaders/postfx.frag index 11974e58..d88e21ec 100644 --- a/data/shaders/postfx.frag +++ b/data/shaders/postfx.frag @@ -22,6 +22,10 @@ layout(set = 3, binding = 0) uniform PostFXUniforms { float gamma_strength; float curvature; float bleeding; + float pixel_scale; // physical pixels per logical pixel (vh / tex_height_) + float time; // seconds since SDL init (for future animated effects) + float pad0; // padding — 48 bytes total (3 × 16) + float pad1; } u; // YCbCr helpers for NTSC bleeding @@ -90,14 +94,19 @@ void main() { colour = mix(colour, lin, u.gamma_strength); } - // Scanlines - float texHeight = float(textureSize(scene, 0).y); - float scaleY = u.screen_height / texHeight; - float screenY = uv.y * u.screen_height; - float posInRow = mod(screenY, scaleY); - float scanLineDY = posInRow / scaleY - 0.5; - float scan = max(1.0 - scanLineDY * scanLineDY * 6.0, 0.12) * 3.5; - colour *= mix(1.0, scan, u.scanline_strength); + // Scanlines — 1 pixel físico oscuro por fila lógica. + // Usa uv.y (independiente del offset de letterbox) con pixel_scale para + // calcular la posición dentro de la fila en coordenadas físicas. + // 3x: 1 dark + 2 bright. 4x: 1 dark + 3 bright. + // bright=3.5×, dark floor=0.42 (mantiene aspecto CRT original). + 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); + float is_dark = step(ps - 1.0, row_pos); + float scan = mix(3.5, 0.42, is_dark); + colour *= mix(1.0, scan, u.scanline_strength); + } if (u.gamma_strength > 0.0) { vec3 enc = pow(colour, vec3(1.0 / 2.2)); @@ -109,7 +118,8 @@ void main() { float vignette = 1.0 - dot(d, d) * u.vignette_strength; colour *= clamp(vignette, 0.0, 1.0); - // Máscara de fósforo RGB + // 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); diff --git a/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp b/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp index 3f6e89cf..0556c177 100644 --- a/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp +++ b/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp @@ -54,6 +54,10 @@ struct PostFXUniforms { float gamma_strength; float curvature; float bleeding; + float pixel_scale; + float time; + float pad0; + float pad1; }; // YCbCr helpers for NTSC bleeding @@ -124,14 +128,19 @@ fragment float4 postfx_fs(PostVOut in [[stage_in]], colour = mix(colour, lin, u.gamma_strength); } - // Scanlines - float texHeight = float(scene.get_height()); - float scaleY = u.screen_height / texHeight; - float screenY = uv.y * u.screen_height; - float posInRow = fmod(screenY, scaleY); - float scanLineDY = posInRow / scaleY - 0.5f; - float scan = max(1.0f - scanLineDY * scanLineDY * 6.0f, 0.12f) * 3.5f; - colour *= mix(1.0f, scan, u.scanline_strength); + // Scanlines — 1 pixel físico oscuro por fila lógica. + // Usa uv.y (independiente del offset de letterbox) con pixel_scale para + // calcular la posición dentro de la fila en coordenadas físicas. + // 3x: 1 dark + 2 bright. 4x: 1 dark + 3 bright. + // bright=3.5×, dark floor=0.42 (mantiene aspecto CRT 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 is_dark = step(ps - 1.0f, row_pos); + float scan = mix(3.5f, 0.42f, is_dark); + colour *= mix(1.0f, scan, u.scanline_strength); + } if (u.gamma_strength > 0.0f) { float3 enc = pow(colour, float3(1.0f/2.2f)); @@ -143,7 +152,8 @@ fragment float4 postfx_fs(PostVOut in [[stage_in]], float vignette = 1.0f - dot(d, d) * u.vignette_strength; colour *= clamp(vignette, 0.0f, 1.0f); - // Máscara de fósforo RGB + // 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.0f) { float whichMask = fract(in.pos.x * 0.3333333f); float3 mask = float3(0.80f); @@ -427,6 +437,12 @@ namespace Rendering { SDL_GPUViewport vp = {vx, vy, vw, vh, 0.0F, 1.0F}; SDL_SetGPUViewport(pass, &vp); + // Uniforms dinámicos: pixel_scale y time + uniforms_.pixel_scale = (tex_height_ > 0) + ? (vh / static_cast(tex_height_)) + : 1.0F; + uniforms_.time = static_cast(SDL_GetTicks()) / 1000.0F; + SDL_GPUTextureSamplerBinding binding = {}; binding.texture = scene_texture_; binding.sampler = sampler_; diff --git a/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp b/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp index a9eb21fd..1a900b33 100644 --- a/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp +++ b/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp @@ -7,16 +7,20 @@ // PostFX uniforms pushed to fragment stage each frame. // Must match the MSL struct and GLSL uniform block layout. -// 8 floats = 32 bytes — meets Metal/Vulkan 16-byte alignment requirement. +// 12 floats = 48 bytes — meets Metal/Vulkan 16-byte alignment requirement. struct PostFXUniforms { float vignette_strength; // 0 = none, ~0.8 = subtle float chroma_strength; // 0 = off, ~0.2 = subtle chromatic aberration float scanline_strength; // 0 = off, 1 = full - float screen_height; // logical height in pixels (for resolution-independent scanlines) + float screen_height; // logical height in pixels (used by bleeding effect) float mask_strength; // 0 = off, 1 = full phosphor dot mask float gamma_strength; // 0 = off, 1 = full gamma 2.4/2.2 correction float curvature; // 0 = flat, 1 = max barrel distortion float bleeding; // 0 = off, 1 = max NTSC chrominance bleeding + float pixel_scale; // physical pixels per logical pixel (vh / tex_height_) + float time; // seconds since SDL init (SDL_GetTicks() / 1000.0f) + float pad0; // padding — keep struct at 48 bytes (3 × 16) + float pad1; }; namespace Rendering { @@ -81,7 +85,7 @@ namespace Rendering { SDL_GPUTransferBuffer* upload_buffer_ = nullptr; SDL_GPUSampler* sampler_ = nullptr; - PostFXUniforms uniforms_{.vignette_strength = 0.6F, .chroma_strength = 0.15F, .scanline_strength = 0.7F, .screen_height = 192.0F}; + PostFXUniforms uniforms_{.vignette_strength = 0.6F, .chroma_strength = 0.15F, .scanline_strength = 0.7F, .screen_height = 192.0F, .pixel_scale = 1.0F}; int tex_width_ = 0; int tex_height_ = 0;