western-shooter/src/rendering/shaders/post-process.frag
Developer b070bab2e3 Initial commit: Western Shooter - Complete implementation
Retro western vertical shooter inspired by Gun.Smoke, built with
TypeScript and WebGL2. Features 3-direction shooting, vertical
scrolling, economy/shop loop, boss fights, and CRT shader effects.

Phases implemented:
- Phase 1: Engine skeleton (WebGL2 renderer, fixed timestep loop, input)
- Phase 2: Shooting identity (3-dir shooting, bullet pools, collision)
- Phase 3: Enemies & patterns (JSON waves, 4 enemy types, parallax bg)
- Phase 4: Economy loop (pickups, shop, upgrades, HUD)
- Phase 5: Boss system (3 bosses, wanted posters, multi-phase attacks)
- Phase 6: Shader layer (CRT effects, bloom, scanlines, screen shake)
- Phase 7: Performance (VAO batching, O(1) allocation, stress testing)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:28:35 -06:00

170 lines
4.5 KiB
GLSL

precision mediump float;
uniform sampler2D u_texture;
uniform vec2 u_resolution;
uniform float u_time;
uniform float u_screenShake;
uniform float u_damageFlash;
uniform float u_brightness;
varying vec2 v_texCoord;
// CRT curvature amount
const float CURVATURE = 0.03;
// Scanline intensity
const float SCANLINE_INTENSITY = 0.15;
const float SCANLINE_COUNT = 180.0;
// Vignette
const float VIGNETTE_INTENSITY = 0.3;
// Chromatic aberration
const float CHROMA_AMOUNT = 0.002;
// Bloom threshold and intensity
const float BLOOM_THRESHOLD = 0.7;
const float BLOOM_INTENSITY = 0.15;
// Apply barrel distortion for CRT curvature
vec2 curveUV(vec2 uv) {
uv = uv * 2.0 - 1.0;
vec2 offset = abs(uv.yx) / vec2(6.0, 4.0);
uv = uv + uv * offset * offset * CURVATURE;
uv = uv * 0.5 + 0.5;
return uv;
}
// Scanlines
float scanline(vec2 uv) {
float line = sin(uv.y * SCANLINE_COUNT * 3.14159);
return 1.0 - SCANLINE_INTENSITY * (0.5 + 0.5 * line);
}
// Vignette effect
float vignette(vec2 uv) {
uv = uv * 2.0 - 1.0;
float dist = length(uv * vec2(0.8, 1.0));
return 1.0 - smoothstep(0.7, 1.4, dist) * VIGNETTE_INTENSITY;
}
// Simple bloom by sampling bright areas
vec3 bloom(sampler2D tex, vec2 uv, vec2 resolution) {
vec3 sum = vec3(0.0);
float pixelSize = 1.0 / resolution.x;
// Sample in a cross pattern
for (float i = -2.0; i <= 2.0; i += 1.0) {
for (float j = -2.0; j <= 2.0; j += 1.0) {
vec2 offset = vec2(i, j) * pixelSize * 2.0;
vec3 sample = texture2D(tex, uv + offset).rgb;
// Only add bright pixels
float brightness = max(max(sample.r, sample.g), sample.b);
if (brightness > BLOOM_THRESHOLD) {
sum += sample * (brightness - BLOOM_THRESHOLD);
}
}
}
return sum * BLOOM_INTENSITY / 25.0;
}
// Chromatic aberration
vec3 chromaticAberration(sampler2D tex, vec2 uv) {
vec2 dir = uv - 0.5;
float dist = length(dir);
vec2 offset = dir * dist * CHROMA_AMOUNT;
float r = texture2D(tex, uv + offset).r;
float g = texture2D(tex, uv).g;
float b = texture2D(tex, uv - offset).b;
return vec3(r, g, b);
}
// Dithering pattern
float dither(vec2 position) {
int x = int(mod(position.x, 4.0));
int y = int(mod(position.y, 4.0));
int index = x + y * 4;
// Bayer 4x4 dither matrix
float threshold;
if (index == 0) threshold = 0.0;
else if (index == 1) threshold = 8.0;
else if (index == 2) threshold = 2.0;
else if (index == 3) threshold = 10.0;
else if (index == 4) threshold = 12.0;
else if (index == 5) threshold = 4.0;
else if (index == 6) threshold = 14.0;
else if (index == 7) threshold = 6.0;
else if (index == 8) threshold = 3.0;
else if (index == 9) threshold = 11.0;
else if (index == 10) threshold = 1.0;
else if (index == 11) threshold = 9.0;
else if (index == 12) threshold = 15.0;
else if (index == 13) threshold = 7.0;
else if (index == 14) threshold = 13.0;
else threshold = 5.0;
return threshold / 16.0;
}
// Quantize color to limited palette
vec3 quantize(vec3 color, float levels) {
return floor(color * levels + 0.5) / levels;
}
void main() {
// Apply screen shake
vec2 uv = v_texCoord;
if (u_screenShake > 0.0) {
float shake = u_screenShake * 0.01;
uv.x += sin(u_time * 50.0) * shake;
uv.y += cos(u_time * 43.0) * shake;
}
// Apply CRT curvature
vec2 curvedUV = curveUV(uv);
// Check if we're outside the curved screen
if (curvedUV.x < 0.0 || curvedUV.x > 1.0 || curvedUV.y < 0.0 || curvedUV.y > 1.0) {
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
return;
}
// Sample with chromatic aberration
vec3 color = chromaticAberration(u_texture, curvedUV);
// Add bloom
color += bloom(u_texture, curvedUV, u_resolution);
// Apply scanlines
color *= scanline(curvedUV);
// Apply vignette
color *= vignette(curvedUV);
// Subtle dithering for retro feel
vec2 pixelPos = curvedUV * u_resolution;
float ditherValue = dither(pixelPos);
color += (ditherValue - 0.5) * 0.03;
// Quantize to limited color palette (subtle)
color = mix(color, quantize(color, 32.0), 0.2);
// Apply brightness adjustment
color *= u_brightness;
// Apply damage flash (red tint)
if (u_damageFlash > 0.0) {
color = mix(color, vec3(1.0, 0.2, 0.1), u_damageFlash * 0.4);
}
// Ensure we don't exceed valid range
color = clamp(color, 0.0, 1.0);
gl_FragColor = vec4(color, 1.0);
}