#version 300 es precision highp float; // highp: fullscreen UV addressing (mediump fp16 is not enough on mobile) in vec2 vTexCoord; out vec4 fragColor; uniform mediump sampler2D u_tex; uniform int u_tonemapEnabled; uniform float u_exposure; uniform int u_tonemapMode; // 0 = Reinhard, 1 = ACES (Narkowicz) uniform int u_gradeEnabled; uniform vec3 u_lift; // additive uniform vec3 u_gamma; // exponent (identity = 1) uniform vec3 u_gain; // multiplicative uniform int u_chromaticEnabled; uniform float u_chromaticStrength; uniform int u_vignetteEnabled; uniform float u_vignetteRadius; uniform float u_vignetteSoftness; uniform float u_vignetteIntensity; uniform float u_saturation; // 1 = untouched; < 1 desaturates (low-hp dread) uniform mediump sampler2D u_distortTex; // half-res RG UV-offset field (ripple quads, additive) uniform int u_distortEnabled; uniform float u_glitchStrength; // 0 = off — horizontal slice tearing + fine jitter uniform float u_glitchTime; float glitchHash(float x, float y) { return fract(sin((x * 12.9898) + (y * 78.233)) * 43758.5453); } void main() { // distortion displaces where the scene is fetched — every later op works on the displaced sample vec2 uv = vTexCoord; if (u_distortEnabled != 0) uv += texture(u_distortTex, vTexCoord).rg; // glitch tear: rows grouped into slices, a strength-scaled fraction of them ripped sideways. // the clock is quantized so tears JUMP between positions instead of sliding — reads as signal damage if (u_glitchStrength > 0.001) { // wrap keeps the sin-hash arguments small — mobile fp32 sin degrades on large inputs. // 240 ticks = 10 s repeat, imperceptible under random tearing float tq = mod(floor(u_glitchTime * 24.0), 240.0); float band = floor(uv.y * 28.0); float pick = glitchHash(band, tq); if (pick > (1.0 - (0.35 * u_glitchStrength))) { float shift = glitchHash(band + 61.0, tq * 1.37); uv.x += (shift - 0.5) * (0.09 * u_glitchStrength); } // fine scanline shiver under the big tears float fine = glitchHash(floor(uv.y * 220.0), tq * 3.1); uv.x += (fine - 0.5) * (0.008 * u_glitchStrength); } // chromatic aberration picks the sample position first; all later ops are per-pixel, so ordering is equivalent vec3 c; if (u_chromaticEnabled != 0) { vec2 ofs = (uv - vec2(0.5)) * u_chromaticStrength; c.r = texture(u_tex, uv + ofs).r; c.g = texture(u_tex, uv ).g; c.b = texture(u_tex, uv - ofs).b; } else c = texture(u_tex, uv).rgb; if (u_tonemapEnabled != 0) { c *= u_exposure; if (u_tonemapMode == 0) { c = c / (c + vec3(1.0)); } else { // ACES (Narkowicz 2015 approximation) const float a = 2.51; const float b = 0.03; const float c2 = 2.43; const float d = 0.59; const float e = 0.14; c = clamp((c * (a * c + b)) / (c * (c2 * c + d) + e), 0.0, 1.0); } } if (u_gradeEnabled != 0) { // ASC-CDL style: out = pow((in * gain + lift), 1/gamma) c = c * u_gain + u_lift; c = max(c, vec3(0.0)); c = pow(c, vec3(1.0) / max(u_gamma, vec3(0.001))); } if (u_vignetteEnabled != 0) { float dist = length(vTexCoord - vec2(0.5)); // GLSL smoothstep is undefined when edge0 >= edge1 — keep edges ascending (and apart) and invert instead float v = 1.0 - smoothstep(u_vignetteRadius - max(u_vignetteSoftness, 0.0001), u_vignetteRadius, dist); c *= mix(1.0, v, u_vignetteIntensity); } // desaturate toward luma — the color drains as the player nears death if (u_saturation < 0.999) { float luma = dot(c, vec3(0.2126, 0.7152, 0.0722)); c = mix(vec3(luma), c, u_saturation); } // everything above ran in linear (the sRGB input texture decodes on sample); // the backbuffer is NOT sRGB, so encode manually as the last step c = pow(max(c, vec3(0.0)), vec3(1.0 / 2.2)); fragColor = vec4(c, 1.0); }