diff --git a/data/shaders/crtpi_fragment.glsl b/data/shaders/crtpi_fragment.glsl index a1919d3..a6f2991 100644 --- a/data/shaders/crtpi_fragment.glsl +++ b/data/shaders/crtpi_fragment.glsl @@ -3,32 +3,19 @@ // Configuración #define SCANLINES #define MULTISAMPLE -#define GAMMA -//#define FAKE_GAMMA -//#define CURVATURE -//#define SHARPER -#define MASK_TYPE 2 -#define CURVATURE_X 0.05 -#define CURVATURE_Y 0.1 -#define MASK_BRIGHTNESS 0.80 #define SCANLINE_WEIGHT 6.0 #define SCANLINE_GAP_BRIGHTNESS 0.12 #define BLOOM_FACTOR 3.5 -#define INPUT_GAMMA 2.4 -#define OUTPUT_GAMMA 2.2 // Inputs desde vertex shader in vec2 vTexCoord; in float vFilterWidth; -#if defined(CURVATURE) -in vec2 vScreenScale; -#endif // Output out vec4 FragColor; -// Uniforms +// Uniforms existentes uniform sampler2D Texture; uniform vec2 TextureSize; uniform float uVignette; // 0 = sin viñeta, 1 = máxima @@ -36,78 +23,63 @@ uniform float uScanlines; // 0 = desactivadas, 1 = plenas uniform float uChroma; // 0 = sin aberración, 1 = máxima uniform float uOutputHeight; // altura del viewport en pixels de pantalla -#if defined(CURVATURE) -vec2 Distort(vec2 coord) -{ - vec2 CURVATURE_DISTORTION = vec2(CURVATURE_X, CURVATURE_Y); - vec2 barrelScale = 1.0 - (0.23 * CURVATURE_DISTORTION); - coord *= vScreenScale; - coord -= vec2(0.5); - float rsq = coord.x * coord.x + coord.y * coord.y; - coord += coord * (CURVATURE_DISTORTION * rsq); - coord *= barrelScale; - if (abs(coord.x) >= 0.5 || abs(coord.y) >= 0.5) - coord = vec2(-1.0); - else - { - coord += vec2(0.5); - coord /= vScreenScale; - } - return coord; +// Nuevos uniforms +uniform float uMask; // 0 = sin máscara de fósforo, 1 = máxima +uniform float uGamma; // 0 = sin corrección gamma, 1 = gamma 2.4/2.2 plena +uniform float uCurvature; // 0 = plana, 1 = curvatura barrel máxima +uniform float uBleeding; // 0 = sin sangrado NTSC, 1 = máximo + +// Conversores YCbCr para efecto NTSC +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); } -#endif float CalcScanLineWeight(float dist) { return max(1.0 - dist * dist * SCANLINE_WEIGHT, SCANLINE_GAP_BRIGHTNESS); } -float CalcScanLine(float dy) -{ - float scanLineWeight = CalcScanLineWeight(dy); -#if defined(MULTISAMPLE) - scanLineWeight += CalcScanLineWeight(dy - vFilterWidth); - scanLineWeight += CalcScanLineWeight(dy + vFilterWidth); - scanLineWeight *= 0.3333333; -#endif - return scanLineWeight; -} - void main() { -#if defined(CURVATURE) - vec2 texcoord = Distort(vTexCoord); - if (texcoord.x < 0.0) { - FragColor = vec4(0.0); - return; - } -#else vec2 texcoord = vTexCoord; -#endif + + // Curvatura barrel CRT (distorsión en UV antes de muestrear) + if (uCurvature > 0.0) { + vec2 c = texcoord - 0.5; + float rsq = dot(c, c); + vec2 dist = vec2(0.05, 0.1) * uCurvature; + vec2 barrelScale = 1.0 - 0.23 * dist; + c += c * (dist * rsq); + c *= barrelScale; + if (abs(c.x) >= 0.5 || abs(c.y) >= 0.5) { + FragColor = vec4(0.0); + return; + } + texcoord = c + 0.5; + } vec2 texcoordInPixels = texcoord * TextureSize; -#if defined(SHARPER) - vec2 tempCoord = floor(texcoordInPixels) + 0.5; - vec2 coord = tempCoord / TextureSize; - vec2 deltas = texcoordInPixels - tempCoord; - float scanLineWeight = CalcScanLine(deltas.y); - vec2 signs = sign(deltas); - deltas.x *= 2.0; - deltas = deltas * deltas; - deltas.y = deltas.y * deltas.y; - deltas.x *= 0.5; - deltas.y *= 8.0; - deltas /= TextureSize; - deltas *= signs; - vec2 tc = coord + deltas; -#else float tempY = floor(texcoordInPixels.y) + 0.5; float yCoord = tempY / TextureSize.y; // Scanline en espacio de pantalla (subpíxel) float scaleY = uOutputHeight / TextureSize.y; - float screenY = vTexCoord.y * uOutputHeight; + float screenY = texcoord.y * uOutputHeight; float posInRow = mod(screenY, scaleY); float scanLineDY = posInRow / scaleY - 0.5; float localFilterWidth = 1.0 / scaleY; @@ -116,7 +88,7 @@ void main() scanLineWeight += CalcScanLineWeight(scanLineDY + localFilterWidth); scanLineWeight *= 0.3333333; - // Phosphor blur en espacio textura (sin cambios) + // Phosphor blur en espacio textura float dy = texcoordInPixels.y - tempY; float signY = sign(dy); dy = dy * dy; @@ -125,59 +97,64 @@ void main() dy /= TextureSize.y; dy *= signY; vec2 tc = vec2(texcoord.x, yCoord + dy); -#endif - float ca = uChroma * 0.005; + // Muestra base en centro + vec3 base = texture(Texture, tc).rgb; + + // Sangrado NTSC — difuminado horizontal de crominancia vec3 colour; - colour.r = texture(Texture, tc + vec2(ca, 0.0)).r; - colour.g = texture(Texture, tc).g; - colour.b = texture(Texture, tc - vec2(ca, 0.0)).b; + if (uBleeding > 0.0) { + float tw = TextureSize.x; + vec3 ycc = rgb_to_ycc(base); + vec3 ycc_l2 = rgb_to_ycc(texture(Texture, tc - vec2(2.0/tw, 0.0)).rgb); + vec3 ycc_l1 = rgb_to_ycc(texture(Texture, tc - vec2(1.0/tw, 0.0)).rgb); + vec3 ycc_r1 = rgb_to_ycc(texture(Texture, tc + vec2(1.0/tw, 0.0)).rgb); + vec3 ycc_r2 = rgb_to_ycc(texture(Texture, tc + vec2(2.0/tw, 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), uBleeding); + } else { + colour = base; + } -#if defined(SCANLINES) -#if defined(GAMMA) -#if defined(FAKE_GAMMA) - colour = colour * colour; -#else - colour = pow(colour, vec3(INPUT_GAMMA)); -#endif -#endif + // Aberración cromática + float ca = uChroma * 0.005; + colour.r = texture(Texture, tc + vec2(ca, 0.0)).r; + colour.b = texture(Texture, tc - vec2(ca, 0.0)).b; + + // Corrección gamma (linealizar antes de scanlines, codificar después) + if (uGamma > 0.0) { + vec3 lin = pow(colour, vec3(2.4)); + colour = mix(colour, lin, uGamma); + } + + // Scanlines scanLineWeight *= BLOOM_FACTOR; colour *= mix(1.0, scanLineWeight, uScanlines); -#if defined(GAMMA) -#if defined(FAKE_GAMMA) - colour = sqrt(colour); -#else - colour = pow(colour, vec3(1.0 / OUTPUT_GAMMA)); -#endif -#endif -#endif + if (uGamma > 0.0) { + vec3 enc = pow(colour, vec3(1.0 / 2.2)); + colour = mix(colour, enc, uGamma); + } + // Viñeta if (uVignette > 0.0) { vec2 uv = texcoord - vec2(0.5); float vig = 1.0 - dot(uv, uv) * uVignette * 4.0; colour *= clamp(vig, 0.0, 1.0); } -#if MASK_TYPE == 0 + // Máscara de fósforo RGB + if (uMask > 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, uMask); + } + FragColor = vec4(colour, 1.0); -#elif MASK_TYPE == 1 - float whichMask = fract(gl_FragCoord.x * 0.5); - vec3 mask; - if (whichMask < 0.5) - mask = vec3(MASK_BRIGHTNESS, 1.0, MASK_BRIGHTNESS); - else - mask = vec3(1.0, MASK_BRIGHTNESS, 1.0); - FragColor = vec4(colour * mask, 1.0); -#elif MASK_TYPE == 2 - float whichMask = fract(gl_FragCoord.x * 0.3333333); - vec3 mask = vec3(MASK_BRIGHTNESS, MASK_BRIGHTNESS, MASK_BRIGHTNESS); - if (whichMask < 0.3333333) - mask.x = 1.0; - else if (whichMask < 0.6666666) - mask.y = 1.0; - else - mask.z = 1.0; - FragColor = vec4(colour * mask, 1.0); -#endif } diff --git a/data/shaders/crtpi_fragment_es.glsl b/data/shaders/crtpi_fragment_es.glsl index 1eb8f55..7b2e31d 100644 --- a/data/shaders/crtpi_fragment_es.glsl +++ b/data/shaders/crtpi_fragment_es.glsl @@ -6,32 +6,19 @@ precision highp float; // Configuración #define SCANLINES #define MULTISAMPLE -#define GAMMA -//#define FAKE_GAMMA -//#define CURVATURE -//#define SHARPER -#define MASK_TYPE 2 -#define CURVATURE_X 0.05 -#define CURVATURE_Y 0.1 -#define MASK_BRIGHTNESS 0.80 #define SCANLINE_WEIGHT 6.0 #define SCANLINE_GAP_BRIGHTNESS 0.12 #define BLOOM_FACTOR 3.5 -#define INPUT_GAMMA 2.4 -#define OUTPUT_GAMMA 2.2 // Inputs desde vertex shader in vec2 vTexCoord; in float vFilterWidth; -#if defined(CURVATURE) -in vec2 vScreenScale; -#endif // Output out vec4 FragColor; -// Uniforms +// Uniforms existentes uniform sampler2D Texture; uniform vec2 TextureSize; uniform float uVignette; // 0 = sin viñeta, 1 = máxima @@ -39,78 +26,63 @@ uniform float uScanlines; // 0 = desactivadas, 1 = plenas uniform float uChroma; // 0 = sin aberración, 1 = máxima uniform float uOutputHeight; // altura del viewport en pixels de pantalla -#if defined(CURVATURE) -vec2 Distort(vec2 coord) -{ - vec2 CURVATURE_DISTORTION = vec2(CURVATURE_X, CURVATURE_Y); - vec2 barrelScale = vec2(1.0) - (0.23 * CURVATURE_DISTORTION); - coord *= vScreenScale; - coord -= vec2(0.5); - float rsq = coord.x * coord.x + coord.y * coord.y; - coord += coord * (CURVATURE_DISTORTION * rsq); - coord *= barrelScale; - if (abs(coord.x) >= 0.5 || abs(coord.y) >= 0.5) - coord = vec2(-1.0); - else - { - coord += vec2(0.5); - coord /= vScreenScale; - } - return coord; +// Nuevos uniforms +uniform float uMask; // 0 = sin máscara de fósforo, 1 = máxima +uniform float uGamma; // 0 = sin corrección gamma, 1 = gamma 2.4/2.2 plena +uniform float uCurvature; // 0 = plana, 1 = curvatura barrel máxima +uniform float uBleeding; // 0 = sin sangrado NTSC, 1 = máximo + +// Conversores YCbCr para efecto NTSC +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); } -#endif float CalcScanLineWeight(float dist) { return max(1.0 - dist * dist * SCANLINE_WEIGHT, SCANLINE_GAP_BRIGHTNESS); } -float CalcScanLine(float dy) -{ - float scanLineWeight = CalcScanLineWeight(dy); -#if defined(MULTISAMPLE) - scanLineWeight += CalcScanLineWeight(dy - vFilterWidth); - scanLineWeight += CalcScanLineWeight(dy + vFilterWidth); - scanLineWeight *= 0.3333333; -#endif - return scanLineWeight; -} - void main() { -#if defined(CURVATURE) - vec2 texcoord = Distort(vTexCoord); - if (texcoord.x < 0.0) { - FragColor = vec4(0.0); - return; - } -#else vec2 texcoord = vTexCoord; -#endif + + // Curvatura barrel CRT (distorsión en UV antes de muestrear) + if (uCurvature > 0.0) { + vec2 c = texcoord - vec2(0.5); + float rsq = dot(c, c); + vec2 dist = vec2(0.05, 0.1) * uCurvature; + 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) { + FragColor = vec4(0.0); + return; + } + texcoord = c + vec2(0.5); + } vec2 texcoordInPixels = texcoord * TextureSize; -#if defined(SHARPER) - vec2 tempCoord = floor(texcoordInPixels) + vec2(0.5); - vec2 coord = tempCoord / TextureSize; - vec2 deltas = texcoordInPixels - tempCoord; - float scanLineWeight = CalcScanLine(deltas.y); - vec2 signs = sign(deltas); - deltas.x *= 2.0; - deltas = deltas * deltas; - deltas.y = deltas.y * deltas.y; - deltas.x *= 0.5; - deltas.y *= 8.0; - deltas /= TextureSize; - deltas *= signs; - vec2 tc = coord + deltas; -#else float tempY = floor(texcoordInPixels.y) + 0.5; float yCoord = tempY / TextureSize.y; // Scanline en espacio de pantalla (subpíxel) float scaleY = uOutputHeight / TextureSize.y; - float screenY = vTexCoord.y * uOutputHeight; + float screenY = texcoord.y * uOutputHeight; float posInRow = mod(screenY, scaleY); float scanLineDY = posInRow / scaleY - 0.5; float localFilterWidth = 1.0 / scaleY; @@ -119,7 +91,7 @@ void main() scanLineWeight += CalcScanLineWeight(scanLineDY + localFilterWidth); scanLineWeight *= 0.3333333; - // Phosphor blur en espacio textura (sin cambios) + // Phosphor blur en espacio textura float dy = texcoordInPixels.y - tempY; float signY = sign(dy); dy = dy * dy; @@ -128,59 +100,64 @@ void main() dy /= TextureSize.y; dy *= signY; vec2 tc = vec2(texcoord.x, yCoord + dy); -#endif - float ca = uChroma * 0.005; + // Muestra base en centro + vec3 base = texture(Texture, tc).rgb; + + // Sangrado NTSC — difuminado horizontal de crominancia vec3 colour; - colour.r = texture(Texture, tc + vec2(ca, 0.0)).r; - colour.g = texture(Texture, tc).g; - colour.b = texture(Texture, tc - vec2(ca, 0.0)).b; + if (uBleeding > 0.0) { + float tw = TextureSize.x; + vec3 ycc = rgb_to_ycc(base); + vec3 ycc_l2 = rgb_to_ycc(texture(Texture, tc - vec2(2.0/tw, 0.0)).rgb); + vec3 ycc_l1 = rgb_to_ycc(texture(Texture, tc - vec2(1.0/tw, 0.0)).rgb); + vec3 ycc_r1 = rgb_to_ycc(texture(Texture, tc + vec2(1.0/tw, 0.0)).rgb); + vec3 ycc_r2 = rgb_to_ycc(texture(Texture, tc + vec2(2.0/tw, 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), uBleeding); + } else { + colour = base; + } -#if defined(SCANLINES) -#if defined(GAMMA) -#if defined(FAKE_GAMMA) - colour = colour * colour; -#else - colour = pow(colour, vec3(INPUT_GAMMA)); -#endif -#endif + // Aberración cromática + float ca = uChroma * 0.005; + colour.r = texture(Texture, tc + vec2(ca, 0.0)).r; + colour.b = texture(Texture, tc - vec2(ca, 0.0)).b; + + // Corrección gamma (linealizar antes de scanlines, codificar después) + if (uGamma > 0.0) { + vec3 lin = pow(colour, vec3(2.4)); + colour = mix(colour, lin, uGamma); + } + + // Scanlines scanLineWeight *= BLOOM_FACTOR; colour *= mix(1.0, scanLineWeight, uScanlines); -#if defined(GAMMA) -#if defined(FAKE_GAMMA) - colour = sqrt(colour); -#else - colour = pow(colour, vec3(1.0 / OUTPUT_GAMMA)); -#endif -#endif -#endif + if (uGamma > 0.0) { + vec3 enc = pow(colour, vec3(1.0 / 2.2)); + colour = mix(colour, enc, uGamma); + } + // Viñeta if (uVignette > 0.0) { vec2 uv = texcoord - vec2(0.5); float vig = 1.0 - dot(uv, uv) * uVignette * 4.0; colour *= clamp(vig, 0.0, 1.0); } -#if MASK_TYPE == 0 + // Máscara de fósforo RGB + if (uMask > 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, uMask); + } + FragColor = vec4(colour, 1.0); -#elif MASK_TYPE == 1 - float whichMask = fract(gl_FragCoord.x * 0.5); - vec3 mask; - if (whichMask < 0.5) - mask = vec3(MASK_BRIGHTNESS, 1.0, MASK_BRIGHTNESS); - else - mask = vec3(1.0, MASK_BRIGHTNESS, 1.0); - FragColor = vec4(colour * mask, 1.0); -#elif MASK_TYPE == 2 - float whichMask = fract(gl_FragCoord.x * 0.3333333); - vec3 mask = vec3(MASK_BRIGHTNESS, MASK_BRIGHTNESS, MASK_BRIGHTNESS); - if (whichMask < 0.3333333) - mask.x = 1.0; - else if (whichMask < 0.6666666) - mask.y = 1.0; - else - mask.z = 1.0; - FragColor = vec4(colour * mask, 1.0); -#endif } diff --git a/data/shaders/postfx.frag b/data/shaders/postfx.frag new file mode 100644 index 0000000..11974e5 --- /dev/null +++ b/data/shaders/postfx.frag @@ -0,0 +1,126 @@ +#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 +// (8 floats, 32 bytes, std140/scalar layout). + +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_strength; + float scanline_strength; + float screen_height; + float mask_strength; + float gamma_strength; + float curvature; + float bleeding; +} u; + +// 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 + vec3 colour; + if (u.bleeding > 0.0) { + float tw = float(textureSize(scene, 0).x); + vec3 ycc = rgb_to_ycc(base); + vec3 ycc_l2 = rgb_to_ycc(texture(scene, uv - vec2(2.0/tw, 0.0)).rgb); + vec3 ycc_l1 = rgb_to_ycc(texture(scene, uv - vec2(1.0/tw, 0.0)).rgb); + vec3 ycc_r1 = rgb_to_ycc(texture(scene, uv + vec2(1.0/tw, 0.0)).rgb); + vec3 ycc_r2 = rgb_to_ycc(texture(scene, uv + vec2(2.0/tw, 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 + float ca = u.chroma_strength * 0.005; + colour.r = texture(scene, uv + vec2(ca, 0.0)).r; + colour.b = texture(scene, uv - vec2(ca, 0.0)).b; + + // 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 + 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); + + 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 + 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); + } + + out_color = vec4(colour, 1.0); +} diff --git a/data/shaders/postfx.vert b/data/shaders/postfx.vert new file mode 100644 index 0000000..eab75cf --- /dev/null +++ b/data/shaders/postfx.vert @@ -0,0 +1,24 @@ +#version 450 + +// Vulkan GLSL vertex shader — postfx full-screen triangle +// Used for SDL3 GPU API (SPIR-V path, Win/Linux). +// Compile: glslc postfx.vert -o postfx.vert.spv +// xxd -i postfx.vert.spv > ../../source/core/rendering/sdl3gpu/postfx_vert_spv.h + +layout(location = 0) out vec2 v_uv; + +void main() { + // Full-screen triangle (no vertex buffer needed) + const vec2 positions[3] = vec2[3]( + vec2(-1.0, -1.0), + vec2( 3.0, -1.0), + vec2(-1.0, 3.0) + ); + const vec2 uvs[3] = vec2[3]( + vec2(0.0, 1.0), + vec2(2.0, 1.0), + vec2(0.0,-1.0) + ); + gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); + v_uv = uvs[gl_VertexIndex]; +} diff --git a/source/core/rendering/opengl/opengl_shader.cpp b/source/core/rendering/opengl/opengl_shader.cpp index a7ec9ec..d01a19c 100644 --- a/source/core/rendering/opengl/opengl_shader.cpp +++ b/source/core/rendering/opengl/opengl_shader.cpp @@ -342,9 +342,17 @@ auto OpenGLShader::init(SDL_Window* window, scanlines_location_ = glGetUniformLocation(program_id_, "uScanlines"); chroma_location_ = glGetUniformLocation(program_id_, "uChroma"); output_height_location_ = glGetUniformLocation(program_id_, "uOutputHeight"); - if (vignette_location_ != -1) { glUniform1f(vignette_location_, postfx_vignette_); } - if (scanlines_location_ != -1) { glUniform1f(scanlines_location_, postfx_scanlines_); } - if (chroma_location_ != -1) { glUniform1f(chroma_location_, postfx_chroma_); } + mask_location_ = glGetUniformLocation(program_id_, "uMask"); + gamma_location_ = glGetUniformLocation(program_id_, "uGamma"); + curvature_location_ = glGetUniformLocation(program_id_, "uCurvature"); + bleeding_location_ = glGetUniformLocation(program_id_, "uBleeding"); + if (vignette_location_ != -1) { glUniform1f(vignette_location_, postfx_vignette_); } + if (scanlines_location_ != -1) { glUniform1f(scanlines_location_, postfx_scanlines_); } + if (chroma_location_ != -1) { glUniform1f(chroma_location_, postfx_chroma_); } + if (mask_location_ != -1) { glUniform1f(mask_location_, postfx_mask_); } + if (gamma_location_ != -1) { glUniform1f(gamma_location_, postfx_gamma_); } + if (curvature_location_ != -1) { glUniform1f(curvature_location_, postfx_curvature_); } + if (bleeding_location_ != -1) { glUniform1f(bleeding_location_, postfx_bleeding_); } glUseProgram(0); is_initialized_ = true; @@ -400,9 +408,13 @@ void OpenGLShader::render() { checkGLError("glUseProgram"); // Pasar uniforms PostFX - if (vignette_location_ != -1) { glUniform1f(vignette_location_, postfx_vignette_); } - if (scanlines_location_ != -1) { glUniform1f(scanlines_location_, postfx_scanlines_); } - if (chroma_location_ != -1) { glUniform1f(chroma_location_, postfx_chroma_); } + if (vignette_location_ != -1) { glUniform1f(vignette_location_, postfx_vignette_); } + if (scanlines_location_ != -1) { glUniform1f(scanlines_location_, postfx_scanlines_); } + if (chroma_location_ != -1) { glUniform1f(chroma_location_, postfx_chroma_); } + if (mask_location_ != -1) { glUniform1f(mask_location_, postfx_mask_); } + if (gamma_location_ != -1) { glUniform1f(gamma_location_, postfx_gamma_); } + if (curvature_location_ != -1) { glUniform1f(curvature_location_, postfx_curvature_); } + if (bleeding_location_ != -1) { glUniform1f(bleeding_location_, postfx_bleeding_); } // Configurar viewport (obtener tamaño lógico de SDL) int logical_w; @@ -469,10 +481,14 @@ void OpenGLShader::render() { glViewport(old_viewport[0], old_viewport[1], old_viewport[2], old_viewport[3]); } -void OpenGLShader::setPostFXParams(float vignette, float scanlines, float chroma) { - postfx_vignette_ = vignette; - postfx_scanlines_ = scanlines; - postfx_chroma_ = chroma; +void OpenGLShader::setPostFXParams(const PostFXParams& p) { + postfx_vignette_ = p.vignette; + postfx_scanlines_ = p.scanlines; + postfx_chroma_ = p.chroma; + postfx_mask_ = p.mask; + postfx_gamma_ = p.gamma; + postfx_curvature_ = p.curvature; + postfx_bleeding_ = p.bleeding; } void OpenGLShader::setTextureSize(float width, float height) { diff --git a/source/core/rendering/opengl/opengl_shader.hpp b/source/core/rendering/opengl/opengl_shader.hpp index 1d14d32..bfac91e 100644 --- a/source/core/rendering/opengl/opengl_shader.hpp +++ b/source/core/rendering/opengl/opengl_shader.hpp @@ -30,7 +30,7 @@ class OpenGLShader : public ShaderBackend { void render() override; void setTextureSize(float width, float height) override; - void setPostFXParams(float vignette, float scanlines, float chroma) override; + void setPostFXParams(const PostFXParams& p) override; void cleanup() final; [[nodiscard]] auto isHardwareAccelerated() const -> bool override { return is_initialized_; } @@ -60,11 +60,19 @@ class OpenGLShader : public ShaderBackend { GLint scanlines_location_ = -1; GLint chroma_location_ = -1; GLint output_height_location_ = -1; + GLint mask_location_ = -1; + GLint gamma_location_ = -1; + GLint curvature_location_ = -1; + GLint bleeding_location_ = -1; // Valores cacheados de PostFX float postfx_vignette_ = 0.6F; float postfx_scanlines_ = 0.7F; float postfx_chroma_ = 0.15F; + float postfx_mask_ = 0.0F; + float postfx_gamma_ = 0.0F; + float postfx_curvature_ = 0.0F; + float postfx_bleeding_ = 0.0F; // Tamaños int window_width_ = 0; diff --git a/source/core/rendering/screen.cpp b/source/core/rendering/screen.cpp index cc845bf..e1c882b 100644 --- a/source/core/rendering/screen.cpp +++ b/source/core/rendering/screen.cpp @@ -482,7 +482,9 @@ void Screen::loadShaders() { void Screen::applyCurrentPostFXPreset() { if (shader_backend_ && !Options::postfx_presets.empty()) { const auto& p = Options::postfx_presets[static_cast(Options::current_postfx_preset)]; - shader_backend_->setPostFXParams(p.vignette, p.scanlines, p.chroma); + Rendering::PostFXParams params{p.vignette, p.scanlines, p.chroma, + p.mask, p.gamma, p.curvature, p.bleeding}; + shader_backend_->setPostFXParams(params); } } diff --git a/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp b/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp index 0973683..03fce55 100644 --- a/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp +++ b/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp @@ -48,32 +48,110 @@ struct PostFXUniforms { float chroma_strength; float scanline_strength; float screen_height; + float mask_strength; + float gamma_strength; + float curvature; + float bleeding; }; +// YCbCr helpers for NTSC bleeding +static float3 rgb_to_ycc(float3 rgb) { + return float3( + 0.299f*rgb.r + 0.587f*rgb.g + 0.114f*rgb.b, + -0.169f*rgb.r - 0.331f*rgb.g + 0.500f*rgb.b + 0.5f, + 0.500f*rgb.r - 0.419f*rgb.g - 0.081f*rgb.b + 0.5f + ); +} +static float3 ycc_to_rgb(float3 ycc) { + float y = ycc.x; + float cb = ycc.y - 0.5f; + float cr = ycc.z - 0.5f; + return clamp(float3( + y + 1.402f*cr, + y - 0.344f*cb - 0.714f*cr, + y + 1.772f*cb + ), 0.0f, 1.0f); +} + fragment float4 postfx_fs(PostVOut in [[stage_in]], texture2d scene [[texture(0)]], sampler samp [[sampler(0)]], constant PostFXUniforms& u [[buffer(0)]]) { - float ca = u.chroma_strength * 0.005; - float4 color; - color.r = scene.sample(samp, in.uv + float2( ca, 0.0)).r; - color.g = scene.sample(samp, in.uv).g; - color.b = scene.sample(samp, in.uv - float2( ca, 0.0)).b; - color.a = scene.sample(samp, in.uv).a; + float2 uv = in.uv; + // Curvatura barrel CRT + if (u.curvature > 0.0f) { + float2 c = uv - 0.5f; + float rsq = dot(c, c); + float2 dist = float2(0.05f, 0.1f) * u.curvature; + float2 barrelScale = 1.0f - 0.23f * dist; + c += c * (dist * rsq); + c *= barrelScale; + if (abs(c.x) >= 0.5f || abs(c.y) >= 0.5f) { + return float4(0.0f, 0.0f, 0.0f, 1.0f); + } + uv = c + 0.5f; + } + + // Muestra base + float3 base = scene.sample(samp, uv).rgb; + + // Sangrado NTSC — difuminado horizontal de crominancia + float3 colour; + if (u.bleeding > 0.0f) { + float tw = float(scene.get_width()); + float3 ycc = rgb_to_ycc(base); + float3 ycc_l2 = rgb_to_ycc(scene.sample(samp, uv - float2(2.0f/tw, 0.0f)).rgb); + float3 ycc_l1 = rgb_to_ycc(scene.sample(samp, uv - float2(1.0f/tw, 0.0f)).rgb); + float3 ycc_r1 = rgb_to_ycc(scene.sample(samp, uv + float2(1.0f/tw, 0.0f)).rgb); + float3 ycc_r2 = rgb_to_ycc(scene.sample(samp, uv + float2(2.0f/tw, 0.0f)).rgb); + ycc.yz = (ycc_l2.yz + ycc_l1.yz*2.0f + ycc.yz*2.0f + ycc_r1.yz*2.0f + ycc_r2.yz) / 8.0f; + colour = mix(base, ycc_to_rgb(ycc), u.bleeding); + } else { + colour = base; + } + + // Aberración cromática + float ca = u.chroma_strength * 0.005f; + colour.r = scene.sample(samp, uv + float2(ca, 0.0f)).r; + colour.b = scene.sample(samp, uv - float2(ca, 0.0f)).b; + + // Corrección gamma (linealizar antes de scanlines, codificar después) + if (u.gamma_strength > 0.0f) { + float3 lin = pow(colour, float3(2.4f)); + colour = mix(colour, lin, u.gamma_strength); + } + + // Scanlines float texHeight = float(scene.get_height()); float scaleY = u.screen_height / texHeight; - float screenY = in.uv.y * u.screen_height; + float screenY = uv.y * u.screen_height; float posInRow = fmod(screenY, scaleY); - float scanLineDY = posInRow / scaleY - 0.5; - float scan = max(1.0 - scanLineDY * scanLineDY * 6.0, 0.12) * 3.5; - color.rgb *= mix(1.0, scan, u.scanline_strength); + 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); - float2 d = in.uv - float2(0.5, 0.5); - float vignette = 1.0 - dot(d, d) * u.vignette_strength; - color.rgb *= clamp(vignette, 0.0, 1.0); + if (u.gamma_strength > 0.0f) { + float3 enc = pow(colour, float3(1.0f/2.2f)); + colour = mix(colour, enc, u.gamma_strength); + } - return color; + // Viñeta + float2 d = uv - 0.5f; + float vignette = 1.0f - dot(d, d) * u.vignette_strength; + colour *= clamp(vignette, 0.0f, 1.0f); + + // Máscara de fósforo RGB + if (u.mask_strength > 0.0f) { + float whichMask = fract(in.pos.x * 0.3333333f); + float3 mask = float3(0.80f); + if (whichMask < 0.3333333f) mask.x = 1.0f; + else if (whichMask < 0.6666667f) mask.y = 1.0f; + else mask.z = 1.0f; + colour = mix(colour, colour * mask, u.mask_strength); + } + + return float4(colour, 1.0f); } )"; // NOLINTEND(readability-identifier-naming) @@ -423,10 +501,14 @@ auto SDL3GPUShader::createShaderSPIRV(SDL_GPUDevice* device, return shader; } -void SDL3GPUShader::setPostFXParams(float vignette, float scanlines, float chroma) { - uniforms_.vignette_strength = vignette; - uniforms_.scanline_strength = scanlines; - uniforms_.chroma_strength = chroma; +void SDL3GPUShader::setPostFXParams(const PostFXParams& p) { + uniforms_.vignette_strength = p.vignette; + uniforms_.scanline_strength = p.scanlines; + uniforms_.chroma_strength = p.chroma; + uniforms_.mask_strength = p.mask; + uniforms_.gamma_strength = p.gamma; + uniforms_.curvature = p.curvature; + uniforms_.bleeding = p.bleeding; } } // namespace Rendering diff --git a/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp b/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp index 43521af..95ce86d 100644 --- a/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp +++ b/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp @@ -7,11 +7,16 @@ // 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. 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 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 }; namespace Rendering { @@ -42,7 +47,7 @@ class SDL3GPUShader : public ShaderBackend { void uploadPixels(const Uint32* pixels, int width, int height) override; // Actualiza los parámetros de intensidad de los efectos PostFX - void setPostFXParams(float vignette, float scanlines, float chroma) override; + void setPostFXParams(const PostFXParams& p) override; private: static auto createShaderMSL(SDL_GPUDevice* device, diff --git a/source/core/rendering/shader_backend.hpp b/source/core/rendering/shader_backend.hpp index 559ace0..291bf67 100644 --- a/source/core/rendering/shader_backend.hpp +++ b/source/core/rendering/shader_backend.hpp @@ -6,6 +6,20 @@ namespace Rendering { +/** + * @brief Parámetros de intensidad de los efectos PostFX + * Definido a nivel de namespace para facilitar el uso desde subclases y screen.cpp + */ +struct PostFXParams { + float vignette = 0.0F; // Intensidad de la viñeta + float scanlines = 0.0F; // Intensidad de las scanlines + float chroma = 0.0F; // Aberración cromática + float mask = 0.0F; // Máscara de fósforo RGB + float gamma = 0.0F; // Corrección gamma (blend 0=off, 1=full) + float curvature = 0.0F; // Curvatura barrel CRT + float bleeding = 0.0F; // Sangrado de color NTSC +}; + /** * @brief Interfaz abstracta para backends de renderizado con shaders * @@ -54,11 +68,9 @@ class ShaderBackend { /** * @brief Establece los parámetros de intensidad de los efectos PostFX - * @param vignette Intensidad de la viñeta (0.0 = ninguna, 1.0 = máxima) - * @param scanlines Intensidad de las scanlines (0.0 = desactivadas, 1.0 = máximas) - * @param chroma Intensidad de la aberración cromática (0.0 = ninguna, 1.0 = máxima) + * @param p Struct con todos los parámetros PostFX */ - virtual void setPostFXParams(float /*vignette*/, float /*scanlines*/, float /*chroma*/) {} + virtual void setPostFXParams(const PostFXParams& /*p*/) {} /** * @brief Verifica si el backend está usando aceleración por hardware diff --git a/source/game/options.cpp b/source/game/options.cpp index bb5c68d..d986909 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -690,6 +690,18 @@ auto loadPostFXFromFile() -> bool { if (p.contains("chroma")) { try { preset.chroma = p["chroma"].get_value(); } catch (...) {} } + if (p.contains("mask")) { + try { preset.mask = p["mask"].get_value(); } catch (...) {} + } + if (p.contains("gamma")) { + try { preset.gamma = p["gamma"].get_value(); } catch (...) {} + } + if (p.contains("curvature")) { + try { preset.curvature = p["curvature"].get_value(); } catch (...) {} + } + if (p.contains("bleeding")) { + try { preset.bleeding = p["bleeding"].get_value(); } catch (...) {} + } postfx_presets.push_back(preset); } } @@ -727,20 +739,52 @@ auto savePostFXToFile() -> bool { file << "# vignette: screen darkening at the edges\n"; file << "# scanlines: horizontal scanline effect\n"; file << "# chroma: chromatic aberration (RGB color fringing)\n"; + file << "# mask: phosphor dot mask (RGB subpixel pattern)\n"; + file << "# gamma: gamma correction input 2.4 / output 2.2\n"; + file << "# curvature: CRT barrel distortion\n"; + file << "# bleeding: NTSC horizontal colour bleeding\n"; file << "\n"; file << "presets:\n"; file << " - name: \"CRT\"\n"; file << " vignette: 0.6\n"; file << " scanlines: 0.7\n"; file << " chroma: 0.15\n"; + file << " mask: 0.6\n"; + file << " gamma: 0.8\n"; + file << " curvature: 0.0\n"; + file << " bleeding: 0.0\n"; + file << " - name: \"NTSC\"\n"; + file << " vignette: 0.4\n"; + file << " scanlines: 0.5\n"; + file << " chroma: 0.2\n"; + file << " mask: 0.4\n"; + file << " gamma: 0.5\n"; + file << " curvature: 0.0\n"; + file << " bleeding: 0.6\n"; + file << " - name: \"CURVED\"\n"; + file << " vignette: 0.5\n"; + file << " scanlines: 0.6\n"; + file << " chroma: 0.1\n"; + file << " mask: 0.5\n"; + file << " gamma: 0.7\n"; + file << " curvature: 0.8\n"; + file << " bleeding: 0.0\n"; file << " - name: \"SCANLINES\"\n"; file << " vignette: 0.0\n"; file << " scanlines: 0.8\n"; file << " chroma: 0.0\n"; + file << " mask: 0.0\n"; + file << " gamma: 0.0\n"; + file << " curvature: 0.0\n"; + file << " bleeding: 0.0\n"; file << " - name: \"SUBTLE\"\n"; file << " vignette: 0.3\n"; file << " scanlines: 0.4\n"; file << " chroma: 0.05\n"; + file << " mask: 0.0\n"; + file << " gamma: 0.3\n"; + file << " curvature: 0.0\n"; + file << " bleeding: 0.0\n"; file.close(); @@ -750,9 +794,11 @@ auto savePostFXToFile() -> bool { // Cargar los presets recién creados postfx_presets.clear(); - postfx_presets.push_back({"CRT", 0.6F, 0.7F, 0.15F}); - postfx_presets.push_back({"SCANLINES", 0.0F, 0.8F, 0.0F}); - postfx_presets.push_back({"SUBTLE", 0.3F, 0.4F, 0.05F}); + postfx_presets.push_back({"CRT", 0.6F, 0.7F, 0.15F, 0.6F, 0.8F, 0.0F, 0.0F}); + postfx_presets.push_back({"NTSC", 0.4F, 0.5F, 0.2F, 0.4F, 0.5F, 0.0F, 0.6F}); + postfx_presets.push_back({"CURVED", 0.5F, 0.6F, 0.1F, 0.5F, 0.7F, 0.8F, 0.0F}); + postfx_presets.push_back({"SCANLINES",0.0F, 0.8F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F}); + postfx_presets.push_back({"SUBTLE", 0.3F, 0.4F, 0.05F, 0.0F, 0.3F, 0.0F, 0.0F}); current_postfx_preset = 0; return true; diff --git a/source/game/options.hpp b/source/game/options.hpp index e27652c..df606ae 100644 --- a/source/game/options.hpp +++ b/source/game/options.hpp @@ -120,6 +120,10 @@ struct PostFXPreset { float vignette{0.6F}; // Intensidad de la viñeta (0.0 = ninguna, 1.0 = máxima) float scanlines{0.7F}; // Intensidad de las scanlines (0.0 = desactivadas, 1.0 = máximas) float chroma{0.15F}; // Intensidad de la aberración cromática (0.0 = ninguna, 1.0 = máxima) + float mask{0.0F}; // Intensidad de la máscara de fósforo RGB (0.0 = desactivada, 1.0 = máxima) + float gamma{0.0F}; // Corrección gamma input 2.4 / output 2.2 (0.0 = off, 1.0 = plena) + float curvature{0.0F}; // Distorsión barrel CRT (0.0 = plana, 1.0 = máxima curvatura) + float bleeding{0.0F}; // Sangrado de color NTSC horizontal Y/C (0.0 = off, 1.0 = máximo) }; // --- Variables globales --- diff --git a/tools/shaders/compile_spirv.sh b/tools/shaders/compile_spirv.sh new file mode 100755 index 0000000..ed78249 --- /dev/null +++ b/tools/shaders/compile_spirv.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Compile Vulkan GLSL shaders to SPIR-V and update the C++ headers used by SDL3GPUShader. +# Required: glslc (from Vulkan SDK or: brew install glslang / apt install glslang-tools) +# +# Run from the project root: tools/shaders/compile_spirv.sh + +set -e + +SHADERS_DIR="data/shaders" +HEADERS_DIR="source/core/rendering/sdl3gpu" + +if ! command -v glslc &> /dev/null; then + echo "ERROR: glslc not found. Install Vulkan SDK or run:" + echo " macOS: brew install glslang" + echo " Linux: sudo apt install glslang-tools" + exit 1 +fi + +echo "Compiling SPIR-V shaders..." + +glslc "${SHADERS_DIR}/postfx.vert" -o /tmp/postfx.vert.spv +glslc "${SHADERS_DIR}/postfx.frag" -o /tmp/postfx.frag.spv + +echo "Generating C++ headers..." + +xxd -i /tmp/postfx.vert.spv | \ + sed 's/unsigned char __tmp_postfx_vert_spv\[\]/static const uint8_t kpostfx_vert_spv[]/' | \ + sed 's/unsigned int __tmp_postfx_vert_spv_len/static const size_t kpostfx_vert_spv_size/' | \ + sed 's/= [0-9]*/\0/' \ + > "${HEADERS_DIR}/postfx_vert_spv.h" + +xxd -i /tmp/postfx.frag.spv | \ + sed 's/unsigned char __tmp_postfx_frag_spv\[\]/static const uint8_t kpostfx_frag_spv[]/' | \ + sed 's/unsigned int __tmp_postfx_frag_spv_len/static const size_t kpostfx_frag_spv_size/' | \ + sed 's/= [0-9]*/\0/' \ + > "${HEADERS_DIR}/postfx_frag_spv.h" + +# Prepend required includes to the headers +for f in "${HEADERS_DIR}/postfx_vert_spv.h" "${HEADERS_DIR}/postfx_frag_spv.h"; do + echo -e "#pragma once\n#include \n#include \n$(cat "$f")" > "$f" +done + +echo "Done. Headers updated in ${HEADERS_DIR}/" +echo " postfx_vert_spv.h" +echo " postfx_frag_spv.h" +echo "Rebuild the project to use the new shaders."