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>
170 lines
4.5 KiB
GLSL
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);
|
|
}
|