Release God's Eye View as open source

This commit is contained in:
Bilawal Sidhu committed 2026-08-24 17:02:16 -05:00
1 parent a5aaea7e16
commit 880a672b5e
433 files changed
+219893 -23

No files matched your search

+70
View File
@@ -0,0 +1,70 @@
/**
* Anime / Studio Ghibli Style — Cel-shading + Saturation boost
* Transforms the world into an animated film look
*/
export const animeShader = {
name: 'anime',
uniforms: {
saturation: { default: 1.0, min: 0, max: 2, label: 'Saturation' },
edgeThick: { default: 0.5, min: 0, max: 1, label: 'Edge Thickness' },
},
fragmentShader: /* glsl */ `
uniform sampler2D colorTexture;
uniform vec2 colorTextureDimensions;
uniform float intensity;
uniform float saturation;
uniform float edgeThick;
in vec2 v_textureCoordinates;
vec3 rgb2hsv(vec3 c) {
vec4 K = vec4(0.0, -1.0/3.0, 2.0/3.0, -1.0);
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0/3.0, 1.0/3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
void main() {
vec2 uv = v_textureCoordinates;
vec2 texel = 1.0 / colorTextureDimensions;
vec4 color = texture(colorTexture, uv);
// Cel-shade: quantize luminance into bands
float luma = dot(color.rgb, vec3(0.299, 0.587, 0.114));
float bands = mix(256.0, 4.0, intensity);
float celLuma = floor(luma * bands + 0.5) / bands;
float lumaScale = (luma > 0.001) ? celLuma / luma : 1.0;
vec3 celColor = color.rgb * lumaScale;
// Boost saturation for vibrant anime look (driven by saturation uniform)
vec3 hsv = rgb2hsv(celColor);
hsv.y = min(hsv.y * (1.0 + 0.6 * intensity * saturation), 1.0);
hsv.z = min(hsv.z * (1.0 + 0.1 * intensity), 1.0);
vec3 saturated = hsv2rgb(hsv);
// Soft edge detection for outlines
vec4 left = texture(colorTexture, uv + vec2(-texel.x, 0.0));
vec4 right = texture(colorTexture, uv + vec2( texel.x, 0.0));
vec4 up = texture(colorTexture, uv + vec2(0.0, texel.y));
vec4 down = texture(colorTexture, uv + vec2(0.0, -texel.y));
float edgeH = length(right.rgb - left.rgb);
float edgeV = length(up.rgb - down.rgb);
float edge = sqrt(edgeH * edgeH + edgeV * edgeV);
float outline = 1.0 - smoothstep(0.05, mix(0.35, 0.1, edgeThick), edge) * 0.6 * intensity;
// Warm color shift (Ghibli palette tends warm)
vec3 warmShift = saturated * vec3(1.02, 1.0, 0.95);
vec3 result = warmShift * outline;
out_FragColor = vec4(mix(color.rgb, result, intensity), color.a);
}
`,
};
+57
View File
@@ -0,0 +1,57 @@
/**
* Noir Style — Desaturation + High Contrast + Vignette
* Classic film noir / detective movie look
*/
export const noirShader = {
name: 'noir',
uniforms: {
contrastAmt: { default: 1.2, min: 0, max: 2, label: 'Contrast' },
grainAmt: { default: 0.5, min: 0, max: 1, label: 'Grain' },
vignetteAmt: { default: 0.5, min: 0, max: 1, label: 'Vignette' },
},
fragmentShader: /* glsl */ `
uniform sampler2D colorTexture;
uniform vec2 colorTextureDimensions;
uniform float intensity;
uniform float contrastAmt;
uniform float grainAmt;
uniform float vignetteAmt;
in vec2 v_textureCoordinates;
void main() {
vec2 uv = v_textureCoordinates;
vec4 color = texture(colorTexture, uv);
// Desaturate
float luma = dot(color.rgb, vec3(0.299, 0.587, 0.114));
vec3 gray = vec3(luma);
vec3 desaturated = mix(color.rgb, gray, intensity);
// High contrast with S-curve (driven by contrastAmt uniform)
float contrast = 1.0 + contrastAmt * intensity;
vec3 contrasted = (desaturated - 0.5) * contrast + 0.5;
contrasted = clamp(contrasted, 0.0, 1.0);
// Film grain (driven by grainAmt uniform)
float grain = fract(sin(dot(uv * colorTextureDimensions, vec2(12.9898, 78.233))) * 43758.5453);
grain = (grain - 0.5) * 0.08 * grainAmt * intensity;
contrasted += grain;
// Vignette (driven by vignetteAmt uniform)
vec2 vigUV = uv * (1.0 - uv);
float vig = vigUV.x * vigUV.y * 16.0;
vig = pow(vig, 0.3 + 0.4 * vignetteAmt * intensity);
// Slight sepia tint for warmth
vec3 sepia = vec3(
dot(contrasted, vec3(0.393, 0.769, 0.189)),
dot(contrasted, vec3(0.349, 0.686, 0.168)),
dot(contrasted, vec3(0.272, 0.534, 0.131))
);
vec3 tinted = mix(contrasted, sepia, 0.15 * intensity);
vec3 result = tinted * vig;
out_FragColor = vec4(mix(color.rgb, result, intensity), color.a);
}
`,
};
+178
View File
@@ -0,0 +1,178 @@
/**
* CRT Terminal — Dynamic CRT Monitor Aesthetic
* Pixelation + Bayer dithering + barrel distortion + scanlines +
* chromatic aberration + vignette + phosphor persistence/ghosting +
* horizontal jitter + flicker + glitch lines + RGB shadow mask subpixels
*
* Exposed uniforms:
* pixelation (1-10) — pixel grid size
* distortion (0-1) — CRT barrel/lens bulge strength
* instability (0-1) — jitter, flicker, glitch frequency
*/
export const retroShader = {
name: 'retro',
uniforms: {
pixelation: { default: 5.0, min: 1, max: 10, label: 'Pixelation' },
distortion: { default: 0, min: 0, max: 1, label: 'Distortion' },
instability: { default: 0.4, min: 0, max: 1, label: 'Instability' },
},
fragmentShader: /* glsl */ `
uniform sampler2D colorTexture;
uniform vec2 colorTextureDimensions;
uniform float intensity;
uniform float pixelation;
uniform float distortion;
uniform float instability;
uniform float time;
in vec2 v_textureCoordinates;
// ── Hash for noise/randomness ─────────────────────────
float hash(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
// 8x8 Bayer dithering matrix (normalized 0-1)
float bayer8(vec2 pos) {
ivec2 p = ivec2(mod(pos, 8.0));
int index = p.x + p.y * 8;
int bayer[64] = int[64](
0, 32, 8, 40, 2, 34, 10, 42,
48, 16, 56, 24, 50, 18, 58, 26,
12, 44, 4, 36, 14, 46, 6, 38,
60, 28, 52, 20, 62, 30, 54, 22,
3, 35, 11, 43, 1, 33, 9, 41,
51, 19, 59, 27, 49, 17, 57, 25,
15, 47, 7, 39, 13, 45, 5, 37,
63, 31, 55, 23, 61, 29, 53, 21
);
return float(bayer[index]) / 64.0;
}
// CRT barrel distortion
vec2 barrelDistort(vec2 uv, float strength) {
vec2 centered = uv * 2.0 - 1.0;
float r2 = dot(centered, centered);
float distort = 1.0 + r2 * strength * 0.4;
centered *= distort;
return centered * 0.5 + 0.5;
}
void main() {
vec2 uv = v_textureCoordinates;
vec2 dims = colorTextureDimensions;
vec2 texel = 1.0 / dims;
// ── Barrel distortion (CRT monitor bulge) ───────────
float dist = distortion * intensity;
vec2 distUV = barrelDistort(uv, dist);
// Black outside the distorted area
if (distUV.x < 0.0 || distUV.x > 1.0 || distUV.y < 0.0 || distUV.y > 1.0) {
out_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
return;
}
// ── Horizontal jitter (random scanline displacement) ──
float lineY = floor(distUV.y * dims.y);
float jitterSeed = hash(vec2(lineY, floor(time * 8.0)));
// Only jitter a few lines at a time (sparse)
float jitterActive = step(0.97 - instability * 0.04, jitterSeed);
float jitterAmount = (hash(vec2(lineY * 7.0, floor(time * 12.0))) - 0.5) *
0.008 * instability * jitterActive * intensity;
vec2 jitteredUV = distUV + vec2(jitterAmount, 0.0);
// ── Chromatic aberration — slight RGB channel offset ──
vec2 centered = jitteredUV - 0.5;
float caStrength = length(centered) * 0.008 * intensity;
float r = texture(colorTexture, jitteredUV + centered * caStrength).r;
float g = texture(colorTexture, jitteredUV).g;
float b = texture(colorTexture, jitteredUV - centered * caStrength).b;
vec4 color = vec4(r, g, b, 1.0);
// ── Pixelation: snap UV to grid ─────────────────────
float pixSize = mix(1.0, pixelation, intensity);
vec2 pixelUV = floor(jitteredUV * dims / pixSize) * pixSize / dims;
vec4 pixelColor = texture(colorTexture, mix(jitteredUV, pixelUV, intensity));
// Blend chromatic aberration with pixelated sample
color = mix(color, pixelColor, 0.7 * intensity);
// ── Bayer dithering before posterization ────────────
vec2 ditherCoord = jitteredUV * dims / pixSize;
float dither = bayer8(ditherCoord) - 0.5;
float ditherAmount = 0.12 * intensity;
vec3 dithered = color.rgb + dither * ditherAmount;
// ── Posterize: reduce color levels ──────────────────
float levels = mix(256.0, 10.0, intensity);
vec3 posterized = floor(dithered * levels + 0.5) / levels;
// ── Slight saturation boost ─────────────────────────
float gray = dot(posterized, vec3(0.299, 0.587, 0.114));
vec3 saturated = mix(vec3(gray), posterized, 1.0 + 0.3 * intensity);
// ── RGB shadow mask subpixel pattern ────────────────
// Each pixel shows faint R/G/B vertical stripes
float subpixelX = mod(distUV.x * dims.x, 3.0);
vec3 subpixelMask = vec3(
smoothstep(0.0, 0.8, 1.0 - abs(subpixelX - 0.5)), // R stripe
smoothstep(0.0, 0.8, 1.0 - abs(subpixelX - 1.5)), // G stripe
smoothstep(0.0, 0.8, 1.0 - abs(subpixelX - 2.5)) // B stripe
);
// Only apply at higher pixelation (visible "pixels")
float subpixelStrength = smoothstep(2.0, 6.0, pixSize) * 0.3 * intensity;
vec3 withSubpixels = mix(saturated, saturated * (subpixelMask * 0.7 + 0.3), subpixelStrength);
// ── Block edge darkening ────────────────────────────
vec2 pixelCenter = fract(distUV * dims / pixSize);
float blockEdge = smoothstep(0.0, 0.08, min(min(pixelCenter.x, 1.0 - pixelCenter.x),
min(pixelCenter.y, 1.0 - pixelCenter.y)));
float edgeFactor = mix(1.0, blockEdge * 0.15 + 0.85, intensity);
vec3 result = withSubpixels * edgeFactor;
// ── Horizontal scanlines — rolling CRT refresh ──────
float scanY = distUV.y * dims.y;
float scanline = sin(scanY * 1.0 + time * 2.5) * 0.5 + 0.5;
scanline = pow(scanline, 1.5);
float scanFade = 0.35 * intensity;
result *= mix(1.0, scanline * scanFade + (1.0 - scanFade), intensity);
// ── Phosphor persistence / ghosting ─────────────────
// Approximate by blurring in a direction (simulates previous frame lingering)
vec3 ghost = texture(colorTexture, jitteredUV - vec2(texel.x * 2.0, 0.0)).rgb;
float ghostGray = dot(ghost, vec3(0.299, 0.587, 0.114));
// Blend a dim ghost of the offset sample
result = mix(result, result + vec3(ghostGray) * 0.08, instability * intensity);
// ── Flicker (overall brightness fluctuation ~50-60Hz) ──
float flicker = sin(time * 188.5) * 0.5 + 0.5; // ~60Hz equivalent
flicker = 1.0 - flicker * 0.03 * instability * intensity; // very subtle
result *= flicker;
// ── Glitch lines (rare horizontal bright bars) ──────
float glitchSeed = hash(vec2(floor(time * 2.0), 0.0));
float glitchLine = step(0.92 - instability * 0.08, glitchSeed);
if (glitchLine > 0.0) {
float glitchY = hash(vec2(floor(time * 2.0), 1.0));
float glitchHit = smoothstep(0.0, 0.003, abs(distUV.y - glitchY));
glitchHit = 1.0 - glitchHit;
result += glitchHit * 0.3 * instability * intensity;
}
// ── Warm phosphor tint (P1 green-amber) ─────────────
vec3 warmTint = result * vec3(1.02, 1.0, 0.94);
result = mix(result, warmTint, 0.4 * intensity);
// ── Edge vignette (darker corners — CRT curvature) ──
vec2 vigUV = distUV * (1.0 - distUV);
float vig = vigUV.x * vigUV.y * 20.0;
vig = clamp(pow(vig, 0.25 + 0.15 * intensity), 0.0, 1.0);
result *= mix(1.0, vig, 0.6 * intensity);
out_FragColor = vec4(mix(texture(colorTexture, uv).rgb, result, intensity), 1.0);
}
`,
};
+93
View File
@@ -0,0 +1,93 @@
/**
* Snow Style — White overlay + Desaturation + Falling particles
* Transforms the world into a winter wonderland
*/
export const snowShader = {
name: 'snow',
uniforms: {
density: { default: 0.6, min: 0, max: 1, label: 'Density' },
wind: { default: 0.5, min: 0, max: 1, label: 'Wind' },
},
fragmentShader: /* glsl */ `
uniform sampler2D colorTexture;
uniform vec2 colorTextureDimensions;
uniform float intensity;
uniform float time;
uniform float density;
uniform float wind;
in vec2 v_textureCoordinates;
// Hash function for pseudo-random snow
float hash(vec2 p) {
p = fract(p * vec2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
// Layered snow particles
float snowLayer(vec2 uv, float layer) {
float depth = 0.5 + layer * 0.5;
float speed = 0.4 + layer * 0.3;
float size = mix(0.01, 0.025, layer);
float windForce = sin(time * 0.5 + layer * 3.14) * (0.05 + wind * 0.2);
vec2 snowUV = uv * vec2(1.0, 0.5) * (4.0 + layer * 4.0);
snowUV.y += time * speed;
snowUV.x += time * windForce;
vec2 cell = floor(snowUV);
vec2 f = fract(snowUV);
float snow = 0.0;
// Check neighboring cells for smooth particles
for (int y = -1; y <= 1; y++) {
for (int x = -1; x <= 1; x++) {
vec2 neighbor = vec2(float(x), float(y));
vec2 point = vec2(hash(cell + neighbor), hash(cell + neighbor + 100.0));
// Animate particle position slightly
point = 0.5 + 0.4 * sin(time * 0.3 + 6.2831 * point);
float d = length(f - neighbor - point);
snow += smoothstep(size, 0.0, d) * depth;
}
}
return snow;
}
void main() {
vec2 uv = v_textureCoordinates;
vec4 color = texture(colorTexture, uv);
// Cool/blue color shift
vec3 cool = color.rgb * vec3(0.85, 0.9, 1.1);
// Desaturate partially
float luma = dot(cool, vec3(0.299, 0.587, 0.114));
vec3 desaturated = mix(cool, vec3(luma), 0.4 * intensity);
// Brighten (snow coverage lightens everything)
vec3 brightened = mix(desaturated, vec3(1.0), 0.15 * intensity);
// Add white frost overlay on brighter areas
float frostMask = smoothstep(0.3, 0.8, luma);
vec3 frosted = mix(brightened, vec3(0.95, 0.97, 1.0), frostMask * 0.25 * intensity);
// Accumulate snow particle layers (density controls amount)
float snow = 0.0;
snow += snowLayer(uv, 0.0) * 0.5;
snow += snowLayer(uv, 0.25) * 0.6;
snow += snowLayer(uv, 0.5) * 0.7;
snow += snowLayer(uv, 0.75) * 0.8;
snow += snowLayer(uv, 1.0) * 1.0;
snow = clamp(snow * (0.4 + density * 1.2), 0.0, 1.0);
// Composite snow particles
vec3 result = frosted + snow * 0.8 * intensity;
// Subtle fog in lower areas
float fog = smoothstep(0.0, 0.4, 1.0 - uv.y) * 0.1 * intensity;
result = mix(result, vec3(0.85, 0.88, 0.95), fog);
out_FragColor = vec4(mix(color.rgb, result, intensity), color.a);
}
`,
};
+279
View File
@@ -0,0 +1,279 @@
/**
* Night Vision / NVG — PVS-14 Image Intensifier Emulation
* P43 phosphor green, intensifier tube bloom, circular vignette,
* scintillation noise, honeycomb pattern, auto-gain, HUD overlay.
*
* Exposed uniforms:
* gain (0-1) — intensifier gain (bloom + noise balance)
* bloom (0-1) — bloom/halo intensity around bright sources
* scanlineStr (0-1) — scanline intensity (kept for compatibility)
* pixelation (1-6) — intensifier tube resolution pixelation
*/
export const nightVisionShader = {
name: 'surveillance',
uniforms: {
gain: { default: 0.55, min: 0, max: 1, label: 'Gain' },
bloom: { default: 0.30, min: 0, max: 1, label: 'Bloom' },
scanlineStr: { default: 1.0, min: 0, max: 1, label: 'Scanlines' },
pixelation: { default: 2.5, min: 1, max: 6, label: 'Pixelation' },
},
fragmentShader: /* glsl */ `
uniform sampler2D colorTexture;
uniform vec2 colorTextureDimensions;
uniform float intensity;
uniform float time;
uniform float gain;
uniform float bloom;
uniform float scanlineStr;
uniform float pixelation;
in vec2 v_textureCoordinates;
// ── Noise functions ───────────────────────────────────
float hash(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
float valueNoise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
// ── Barrel distortion (NVG lens) ──────────────────────
vec2 barrelDistort(vec2 uv, float strength) {
vec2 c = uv * 2.0 - 1.0;
float r2 = dot(c, c);
float distort = 1.0 + r2 * strength * 0.5 + r2 * r2 * strength * 0.15;
c *= distort;
return c * 0.5 + 0.5;
}
// ── Honeycomb pattern (fiber optic plate texture) ─────
float honeycomb(vec2 uv) {
vec2 dims = colorTextureDimensions;
float scale = min(dims.x, dims.y) * 0.008;
vec2 p = uv * dims * scale;
// Hex grid
vec2 r = vec2(1.0, 1.732);
vec2 h = r * 0.5;
vec2 a = mod(p, r) - h;
vec2 b = mod(p - h, r) - h;
vec2 gv = dot(a, a) < dot(b, b) ? a : b;
float d = max(abs(gv.x), abs(gv.y * 0.577 + abs(gv.x) * 0.5));
return smoothstep(0.4, 0.45, d);
}
// ── 7-segment digit renderer ──────────────────────────
float segment(vec2 p, int seg) {
float s = 0.0;
if (seg == 0) s = step(0.2, p.x) * step(p.x, 0.8) * step(0.85, p.y) * step(p.y, 1.0);
if (seg == 1) s = step(0.7, p.x) * step(p.x, 0.9) * step(0.5, p.y) * step(p.y, 0.95);
if (seg == 2) s = step(0.7, p.x) * step(p.x, 0.9) * step(0.05, p.y) * step(p.y, 0.5);
if (seg == 3) s = step(0.2, p.x) * step(p.x, 0.8) * step(0.0, p.y) * step(p.y, 0.15);
if (seg == 4) s = step(0.1, p.x) * step(p.x, 0.3) * step(0.05, p.y) * step(p.y, 0.5);
if (seg == 5) s = step(0.1, p.x) * step(p.x, 0.3) * step(0.5, p.y) * step(p.y, 0.95);
if (seg == 6) s = step(0.2, p.x) * step(p.x, 0.8) * step(0.42, p.y) * step(p.y, 0.58);
return s;
}
float digit(vec2 p, int d) {
int masks[10] = int[10](0x7E, 0x30, 0x6D, 0x79, 0x33, 0x5B, 0x5F, 0x70, 0x7F, 0x7B);
int m = masks[d];
float s = 0.0;
for (int i = 0; i < 7; i++) {
if ((m >> (6 - i) & 1) == 1) s += segment(p, i);
}
return clamp(s, 0.0, 1.0);
}
// Render HH:MM:SS timestamp
float renderTimestamp(vec2 uv) {
vec2 tsOrigin = vec2(0.02, 0.03);
vec2 tsSize = vec2(0.22, 0.035);
vec2 p = (uv - tsOrigin) / tsSize;
if (p.x < 0.0 || p.x > 1.0 || p.y < 0.0 || p.y > 1.0) return 0.0;
int totalSec = int(mod(time, 86400.0));
int hours = totalSec / 3600;
int minutes = (totalSec % 3600) / 60;
int seconds = totalSec % 60;
float charWidth = 1.0 / 8.5;
int charIdx = int(p.x / charWidth);
float localX = mod(p.x, charWidth) / charWidth;
vec2 localP = vec2(localX, p.y);
int dv = -1;
if (charIdx == 0) dv = hours / 10;
else if (charIdx == 1) dv = hours % 10;
else if (charIdx == 2) return (step(0.3, localP.x) * step(localP.x, 0.7)) *
(step(0.2, localP.y) * step(localP.y, 0.4) + step(0.6, localP.y) * step(localP.y, 0.8));
else if (charIdx == 3) dv = minutes / 10;
else if (charIdx == 4) dv = minutes % 10;
else if (charIdx == 5) return (step(0.3, localP.x) * step(localP.x, 0.7)) *
(step(0.2, localP.y) * step(localP.y, 0.4) + step(0.6, localP.y) * step(localP.y, 0.8));
else if (charIdx == 6) dv = seconds / 10;
else if (charIdx == 7) dv = seconds % 10;
if (dv < 0 || dv > 9) return 0.0;
return digit(localP, dv);
}
// ── Crosshair (thin, subtle NVG reticle) ──────────────
float crosshair(vec2 uv) {
vec2 c = uv - 0.5;
float h = smoothstep(0.001, 0.0004, abs(c.y)) *
step(0.01, abs(c.x)) * step(abs(c.x), 0.025);
float v = smoothstep(0.001, 0.0004, abs(c.x)) *
step(0.01, abs(c.y)) * step(abs(c.y), 0.025);
return clamp(h + v, 0.0, 1.0);
}
void main() {
vec2 uv = v_textureCoordinates;
vec2 dims = colorTextureDimensions;
vec2 texel = 1.0 / dims;
// ── Barrel distortion (NVG lens distortion) ─────────
float dist = 0.5 * intensity;
vec2 distUV = barrelDistort(uv, dist);
// ── Circular vignette mask (NVG tube field of view) ──
vec2 centered = uv * 2.0 - 1.0;
float aspect = dims.x / dims.y;
centered.x *= aspect;
float radius = length(centered);
float tubeMask = pow(1.0 - smoothstep(0.6, 1.05, radius), 0.7);
// Tube brightness falloff (center brightest)
float tubeShading = 1.0 - radius * radius * 0.3;
tubeShading = max(tubeShading, 0.0);
// If outside tube, render black
if (tubeMask < 0.001) {
out_FragColor = vec4(vec3(0.0), 1.0);
return;
}
// Black outside distorted area
if (distUV.x < 0.0 || distUV.x > 1.0 || distUV.y < 0.0 || distUV.y > 1.0) {
out_FragColor = vec4(vec3(0.0), 1.0);
return;
}
// ── Intensifier tube resolution pixelation ────────────
float pixSize = mix(1.0, pixelation, intensity);
vec2 snappedUV = floor(distUV * dims / pixSize) * pixSize / dims;
distUV = mix(distUV, snappedUV, intensity);
vec4 original = texture(colorTexture, distUV);
// ── Luminance ───────────────────────────────────────
float luma = dot(original.rgb, vec3(0.299, 0.587, 0.114));
// ── Auto-gain response ──────────────────────────────
// Higher gain = more amplification, more noise, more bloom
float gainLevel = mix(0.8, 2.5, gain);
float amplified = clamp(luma * gainLevel, 0.0, 1.0);
// Slight contrast curve for gain response
amplified = pow(amplified, mix(1.2, 0.7, gain));
// ── Intensifier tube bloom (THE key NVG visual) ─────
// Bloom around bright sources — wider kernel for realistic halos
float bloomAccum = 0.0;
float bloomW = 0.0;
for (int y = -5; y <= 5; y++) {
for (int x = -5; x <= 5; x++) {
vec2 offset = vec2(float(x), float(y)) * texel * 4.0;
float sLuma = dot(texture(colorTexture, distUV + offset).rgb, vec3(0.299, 0.587, 0.114));
float bright = smoothstep(0.4, 0.9, sLuma * gainLevel);
float w = exp(-float(x * x + y * y) / 18.0);
bloomAccum += bright * w;
bloomW += w;
}
}
bloomAccum /= bloomW;
// Edge glow / corona on bright objects
float corona = bloomAccum * bloom * 1.5;
// ── P43 phosphor green (530nm) ──────────────────────
vec3 phosphor = vec3(0.16, 1.0, 0.22);
vec3 nvgColor = phosphor * (amplified + corona);
// ── Scintillation (image intensifier sparkle noise) ──
// Base tube grain (slow, coherent)
vec2 grainCoord = uv * 120.0 + vec2(time * 0.5, time * 0.3);
float tubeGrain = valueNoise(grainCoord);
tubeGrain = (tubeGrain - 0.5) * mix(0.06, 0.2, gain) * intensity;
nvgColor += phosphor * tubeGrain;
// More noise in dark areas (real gain response)
float darkNoise = (1.0 - amplified) * hash(uv * dims + vec2(time * 200.0, time * 300.0));
nvgColor += phosphor * darkNoise * 0.08 * gain * intensity;
// ── Honeycomb fiber optic plate ─────────────────────
float hc = honeycomb(distUV);
nvgColor *= 1.0 - hc * 0.04 * intensity; // very subtle
// ── Scanlines (subtle, from the display) ────────────
float scanline = sin(distUV.y * dims.y * 1.2 + time * 2.0) * 0.5 + 0.5;
scanline = pow(scanline, 2.5);
nvgColor *= 1.0 - scanline * scanlineStr * 0.15 * intensity;
// ── Tube shading (brightness falloff from center) ───
nvgColor *= tubeShading;
// ── Circular vignette (dark edges, NVG tube shape) ──
nvgColor *= tubeMask;
// ── HUD Overlay ─────────────────────────────────────
// Top-left: "NVG" / "I²" label marker
vec2 labelArea = (uv - vec2(0.03, 0.92)) / vec2(0.06, 0.03);
if (labelArea.x >= 0.0 && labelArea.x <= 1.0 && labelArea.y >= 0.0 && labelArea.y <= 1.0) {
float lbl = step(0.1, labelArea.x) * step(labelArea.x, 0.9) *
step(0.2, labelArea.y) * step(labelArea.y, 0.8);
nvgColor += phosphor * lbl * 0.3 * intensity;
}
// Gain indicator below label: "AUTO" marker
vec2 gainArea = (uv - vec2(0.03, 0.88)) / vec2(0.05, 0.025);
if (gainArea.x >= 0.0 && gainArea.x <= 1.0 && gainArea.y >= 0.0 && gainArea.y <= 1.0) {
float gLbl = step(0.1, gainArea.x) * step(gainArea.x, 0.9) *
step(0.2, gainArea.y) * step(gainArea.y, 0.8);
nvgColor += phosphor * gLbl * 0.2 * intensity;
}
// Center crosshair (thin, subtle)
float ch = crosshair(uv);
nvgColor += phosphor * ch * 0.4 * intensity;
// Bottom-left: Timestamp (7-segment)
float ts = renderTimestamp(uv);
nvgColor += phosphor * ts * 0.6 * intensity;
// REC indicator — top-right (blinking)
vec2 recPos = uv - vec2(0.95, 0.94);
float recDot = smoothstep(0.008, 0.004, length(recPos));
float blink = step(0.5, fract(time * 0.8));
// REC dot in slightly warmer green
nvgColor += vec3(0.3, 1.0, 0.2) * recDot * blink * intensity;
// ── Final composite ─────────────────────────────────
nvgColor = clamp(nvgColor, 0.0, 1.0);
// Keep NVG output fully tube-masked at full intensity to avoid color bleed at the lens edge.
vec3 finalColor = mix(original.rgb, nvgColor * tubeMask, intensity);
out_FragColor = vec4(finalColor, 1.0);
}
`,
};
+340
View File
@@ -0,0 +1,340 @@
/**
* FLIR — Forward Looking Infrared
* Military-grade thermal imaging with White-Hot/Black-Hot modes,
* coherent temporal noise, hot-spot bloom, temperature banding,
* soft IR diffraction, and full HUD overlay rendered in GLSL.
*
* Exposed uniforms:
* sensitivity (0-1) — contrast/range of temperature mapping
* bloom (0-1) — hot-spot bloom/bleed intensity
* mode (0-1) — 0 = White-Hot, 1 = Black-Hot (step at 0.5)
* pixelation (1-6) — sensor resolution pixelation grid size
* palette (0-1) — 0 = monochrome FLIR, 1 = Ironbow "Predator" color ramp
*/
export const thermalShader = {
name: 'thermal',
uniforms: {
sensitivity: { default: 0.75, min: 0, max: 1, label: 'Sensitivity' },
bloom: { default: 0.65, min: 0, max: 1, label: 'Bloom' },
mode: { default: 0.0, min: 0, max: 1, label: 'WHOT/BHOT' },
pixelation: { default: 1.5, min: 1, max: 6, label: 'Pixelation' },
palette: { default: 0.0, min: 0, max: 1, label: 'Ironbow' },
},
fragmentShader: /* glsl */ `
uniform sampler2D colorTexture;
uniform vec2 colorTextureDimensions;
uniform float intensity;
uniform float time;
uniform float sensitivity;
uniform float bloom;
uniform float mode;
uniform float pixelation;
uniform float palette;
in vec2 v_textureCoordinates;
// ── Ironbow "Predator" thermal palette ────────────────
// Maps a 0-1 temperature to the classic FLIR ironbow ramp:
// black -> deep purple -> magenta -> red -> orange -> yellow -> white.
vec3 ironbow(float t) {
t = clamp(t, 0.0, 1.0);
const vec3 c0 = vec3(0.0, 0.0, 0.0); // cold
const vec3 c1 = vec3(0.13, 0.0, 0.30); // deep purple
const vec3 c2 = vec3(0.49, 0.0, 0.45); // magenta
const vec3 c3 = vec3(0.86, 0.10, 0.18); // red
const vec3 c4 = vec3(1.0, 0.55, 0.0); // orange
const vec3 c5 = vec3(1.0, 0.91, 0.32); // yellow
const vec3 c6 = vec3(1.0, 1.0, 1.0); // hot (white)
float s = t * 6.0;
if (s < 1.0) return mix(c0, c1, s);
if (s < 2.0) return mix(c1, c2, s - 1.0);
if (s < 3.0) return mix(c2, c3, s - 2.0);
if (s < 4.0) return mix(c3, c4, s - 3.0);
if (s < 5.0) return mix(c4, c5, s - 4.0);
return mix(c5, c6, s - 5.0);
}
// ── Value noise (coherent, smooth, drifting) ──────────
float hash(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
float valueNoise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f); // smoothstep interpolation
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
// Fractal Brownian motion for layered coherent noise
float fbm(vec2 p) {
float v = 0.0;
float a = 0.5;
vec2 shift = vec2(100.0);
for (int i = 0; i < 4; i++) {
v += a * valueNoise(p);
p = p * 2.0 + shift;
a *= 0.5;
}
return v;
}
// ── 7-segment digit renderer ──────────────────────────
float segment(vec2 p, int seg) {
float s = 0.0;
if (seg == 0) s = step(0.2, p.x) * step(p.x, 0.8) * step(0.85, p.y) * step(p.y, 1.0);
if (seg == 1) s = step(0.7, p.x) * step(p.x, 0.9) * step(0.5, p.y) * step(p.y, 0.95);
if (seg == 2) s = step(0.7, p.x) * step(p.x, 0.9) * step(0.05, p.y) * step(p.y, 0.5);
if (seg == 3) s = step(0.2, p.x) * step(p.x, 0.8) * step(0.0, p.y) * step(p.y, 0.15);
if (seg == 4) s = step(0.1, p.x) * step(p.x, 0.3) * step(0.05, p.y) * step(p.y, 0.5);
if (seg == 5) s = step(0.1, p.x) * step(p.x, 0.3) * step(0.5, p.y) * step(p.y, 0.95);
if (seg == 6) s = step(0.2, p.x) * step(p.x, 0.8) * step(0.42, p.y) * step(p.y, 0.58);
return s;
}
float digit(vec2 p, int d) {
int masks[10] = int[10](0x7E, 0x30, 0x6D, 0x79, 0x33, 0x5B, 0x5F, 0x70, 0x7F, 0x7B);
int m = masks[d];
float s = 0.0;
for (int i = 0; i < 7; i++) {
if ((m >> (6 - i) & 1) == 1) s += segment(p, i);
}
return clamp(s, 0.0, 1.0);
}
// Render a character (digit, '.', or '°') at position
float renderChar(vec2 p, int ch) {
if (ch == 10) { // '.' decimal point
return smoothstep(0.15, 0.0, length(p - vec2(0.5, 0.08)));
}
if (ch == 11) { // '°' degree symbol
float ring = abs(length(p - vec2(0.5, 0.82)) - 0.08);
return smoothstep(0.04, 0.02, ring);
}
if (ch >= 0 && ch <= 9) return digit(p, ch);
return 0.0;
}
// ── Crosshair ─────────────────────────────────────────
float crosshair(vec2 uv) {
vec2 c = uv - 0.5;
float h = smoothstep(0.0015, 0.0005, abs(c.y)) *
step(0.015, abs(c.x)) * step(abs(c.x), 0.04);
float v = smoothstep(0.0015, 0.0005, abs(c.x)) *
step(0.015, abs(c.y)) * step(abs(c.y), 0.04);
return clamp(h + v, 0.0, 1.0);
}
// ── Scale bar (right edge) ────────────────────────────
float scaleBar(vec2 uv) {
// Vertical bar on right edge
float barX = step(0.94, uv.x) * step(uv.x, 0.955);
float barY = step(0.15, uv.y) * step(uv.y, 0.85);
return barX * barY;
}
void main() {
vec2 uv = v_textureCoordinates;
vec2 dims = colorTextureDimensions;
vec2 texel = 1.0 / dims;
vec2 hudUV = uv; // preserve original UV for HUD overlay
// ── Circular vignette mask (FLIR optics field of view) ──
vec2 centered = uv * 2.0 - 1.0;
float aspect = dims.x / dims.y;
centered.x *= aspect;
float radius = length(centered);
float lensMask = pow(1.0 - smoothstep(0.6, 1.05, radius), 0.7);
// Tube brightness falloff (center brightest)
float lensShading = 1.0 - radius * radius * 0.25;
lensShading = max(lensShading, 0.0);
// If outside lens, render black
if (lensMask < 0.001) {
out_FragColor = vec4(vec3(0.0), 1.0);
return;
}
// ── Sensor resolution pixelation (authentic FLIR resolution limits) ──
float pixSize = mix(1.0, pixelation, intensity);
vec2 snappedUV = floor(uv * dims / pixSize) * pixSize / dims;
uv = mix(uv, snappedUV, intensity);
// ── Soft IR blur (thermal cameras have lower resolution / diffraction) ──
vec3 blurred = vec3(0.0);
float totalWeight = 0.0;
for (int y = -2; y <= 2; y++) {
for (int x = -2; x <= 2; x++) {
float w = exp(-0.5 * float(x * x + y * y) / 2.0);
blurred += texture(colorTexture, uv + vec2(float(x), float(y)) * texel * 1.5).rgb * w;
totalWeight += w;
}
}
blurred /= totalWeight;
vec4 original = texture(colorTexture, uv);
// Mix between sharp and blurred based on intensity
vec3 src = mix(original.rgb, blurred, 0.6 * intensity);
// ── Luminance → temperature mapping ─────────────────
float luma = dot(src, vec3(0.299, 0.587, 0.114));
// Sensitivity remaps the luminance range
float sens = mix(0.25, 1.0, sensitivity);
float temp = clamp((luma - (0.5 - sens * 0.5)) / sens, 0.0, 1.0);
// ── Temperature banding (contour lines) ─────────────
float bands = 12.0;
float bandLine = abs(fract(temp * bands) - 0.5);
float contour = smoothstep(0.04, 0.06, bandLine);
temp *= mix(1.0, contour * 0.85 + 0.15, 0.3 * intensity);
// ── White-Hot / Black-Hot mode ──────────────────────
float isBlackHot = step(0.5, mode);
float thermal = mix(temp, 1.0 - temp, isBlackHot);
// Monochrome FLIR (white/black-hot) vs Ironbow "Predator" color ramp.
// Ironbow maps TRUE temperature (cold->dark, hot->white) so the colors
// read correctly regardless of the WHOT/BHOT toggle.
vec3 mono = vec3(thermal);
vec3 iron = ironbow(temp);
vec3 thermalColor = mix(mono, iron, palette);
// ── Hot-spot bloom/bleed ────────────────────────────
// Sample a wider area for bloom on bright spots
float bloomSample = 0.0;
float bloomWeight = 0.0;
for (int y = -4; y <= 4; y++) {
for (int x = -4; x <= 4; x++) {
vec2 offset = vec2(float(x), float(y)) * texel * 3.0;
float sLuma = dot(texture(colorTexture, uv + offset).rgb, vec3(0.299, 0.587, 0.114));
float sMapped = clamp((sLuma - (0.5 - sens * 0.5)) / sens, 0.0, 1.0);
float sFinal = mix(sMapped, 1.0 - sMapped, isBlackHot);
float w = exp(-0.5 * float(x * x + y * y) / 8.0);
// Only bloom the "hot" pixels (bright in WHOT, dark values in BHOT...
// but since we already inverted, just bloom high values)
float hotness = smoothstep(0.6, 1.0, sFinal);
bloomSample += hotness * w;
bloomWeight += w;
}
}
bloomSample /= bloomWeight;
thermalColor += bloomSample * bloom * 0.8;
// ── Coherent temporal noise (slowly drifting, cloud-like) ──
vec2 noiseCoord = uv * 80.0 + vec2(time * 0.3, time * 0.2);
float noise = fbm(noiseCoord);
noise = (noise - 0.5) * 0.08 * intensity;
thermalColor += noise;
// ── Subtle motion blur feel (slight blur) ───────────
// Already handled by the initial IR blur above
// ── HUD Overlay ─────────────────────────────────────
float hud = 0.0;
// Top-left: "FLIR" label + mode indicator
// Rendered as simple box presence markers (not full text rendering)
// We'll use a simplified approach: render mode text near top-left
vec2 labelArea = (hudUV - vec2(0.02, 0.92)) / vec2(0.08, 0.04);
if (labelArea.x >= 0.0 && labelArea.x <= 1.0 && labelArea.y >= 0.0 && labelArea.y <= 1.0) {
// Simple horizontal bar as "FLIR" label marker
hud += step(0.1, labelArea.x) * step(labelArea.x, 0.9) *
step(0.3, labelArea.y) * step(labelArea.y, 0.7) * 0.6;
}
// Mode indicator below label
vec2 modeArea = (hudUV - vec2(0.02, 0.88)) / vec2(0.06, 0.03);
if (modeArea.x >= 0.0 && modeArea.x <= 1.0 && modeArea.y >= 0.0 && modeArea.y <= 1.0) {
hud += step(0.1, modeArea.x) * step(modeArea.x, 0.9) *
step(0.2, modeArea.y) * step(modeArea.y, 0.8) * 0.4;
}
// Center crosshair
hud += crosshair(hudUV) * 0.7;
// Top-right: simulated temperature readout (derived from center luminance)
float centerLuma = dot(texture(colorTexture, vec2(0.5)).rgb, vec3(0.299, 0.587, 0.114));
float tempC = 20.0 + centerLuma * 30.0; // 20°C to 50°C range
int tempInt = int(tempC);
int tempDec = int(fract(tempC) * 10.0);
// Temperature digits at top-right
float tempHud = 0.0;
vec2 tempOrigin = vec2(0.88, 0.92);
vec2 charSize = vec2(0.018, 0.035);
float spacing = 0.02;
// Tens digit
vec2 d1p = (hudUV - tempOrigin) / charSize;
if (d1p.x >= 0.0 && d1p.x <= 1.0 && d1p.y >= 0.0 && d1p.y <= 1.0) {
tempHud += renderChar(d1p, tempInt / 10);
}
// Ones digit
vec2 d2p = (hudUV - (tempOrigin + vec2(spacing, 0.0))) / charSize;
if (d2p.x >= 0.0 && d2p.x <= 1.0 && d2p.y >= 0.0 && d2p.y <= 1.0) {
tempHud += renderChar(d2p, tempInt % 10);
}
// Decimal point
vec2 dpp = (hudUV - (tempOrigin + vec2(spacing * 2.0, 0.0))) / charSize;
if (dpp.x >= 0.0 && dpp.x <= 1.0 && dpp.y >= 0.0 && dpp.y <= 1.0) {
tempHud += renderChar(dpp, 10); // '.'
}
// Decimal digit
vec2 d3p = (hudUV - (tempOrigin + vec2(spacing * 2.6, 0.0))) / charSize;
if (d3p.x >= 0.0 && d3p.x <= 1.0 && d3p.y >= 0.0 && d3p.y <= 1.0) {
tempHud += renderChar(d3p, tempDec);
}
// Degree symbol
vec2 dgp = (hudUV - (tempOrigin + vec2(spacing * 3.5, 0.0))) / charSize;
if (dgp.x >= 0.0 && dgp.x <= 1.0 && dgp.y >= 0.0 && dgp.y <= 1.0) {
tempHud += renderChar(dgp, 11); // '°'
}
hud += tempHud * 0.8;
// Bottom-right: frame counter
int frame = int(mod(time * 30.0, 10000.0));
float framHud = 0.0;
vec2 fOrigin = vec2(0.88, 0.04);
for (int i = 0; i < 4; i++) {
int dv = (frame / int(pow(10.0, float(3 - i)))) % 10;
vec2 fp = (hudUV - (fOrigin + vec2(float(i) * spacing, 0.0))) / charSize;
if (fp.x >= 0.0 && fp.x <= 1.0 && fp.y >= 0.0 && fp.y <= 1.0) {
framHud += renderChar(fp, dv);
}
}
hud += framHud * 0.5;
// Scale bar (right edge gradient)
float bar = scaleBar(hudUV);
if (bar > 0.0) {
float barGrad = (hudUV.y - 0.15) / 0.7; // 0 at bottom, 1 at top
float barVal = mix(barGrad, 1.0 - barGrad, isBlackHot);
thermalColor = mix(thermalColor, vec3(barVal), bar * 0.9);
}
// Composite HUD (rendered in white, slightly transparent)
float hudBright = mix(1.0, 0.0, isBlackHot); // HUD is white in WHOT, dark in BHOT inverted
// Actually, HUD should always be visible — use contrast
thermalColor += hud * 0.6 * intensity;
// ── Lens shading + vignette ───────────────────
thermalColor *= lensShading;
thermalColor *= lensMask;
// Clamp final
thermalColor = clamp(thermalColor, 0.0, 1.0);
// Blend with original based on intensity, then fade to black at lens edges
vec3 finalColor = mix(original.rgb, thermalColor, intensity);
finalColor *= mix(1.0, lensMask, intensity);
out_FragColor = vec4(finalColor, 1.0);
}
`,
};