precision highp float;
uniform vec2 resolution;
uniform float time;
// --- Hash & Noise Functions ---
float hash(vec2 p) {
p = fract(p * vec2(123.34, 456.21));
p = dot(p, p 45.32);
return fract(p.x * p.y);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
return mix(mix(hash(i), hash(i vec2(1.0, 0.0)), f.x),
mix(hash(i vec2(0.0, 1.0)), hash(i vec2(1.0, 1.0)), f.x), f.y);
}
float fbm(vec2 p) {
float v = 0.0;
float a = 0.5;
mat2 m = mat2(1.6, 1.2, -1.2, 1.6);
for(int i = 0; i < 5; i ) {
v = a * noise(p);
p = m * p;
a *= 0.5;
}
return v;
}
// --- SDFs ---
float sdOctagon(vec2 p, float r) {
p = abs(p);
float d = max(p.x, p.y) - r;
d = max(d, dot(p, vec2(0.70710678)) - r * 1.12);
return d;
}
float sdCone(vec3 p, vec2 dim) {
vec2 q = vec2(length(p.xz), p.y);
return max(dot(q, vec2(dim.y, -dim.x)), -q.y - dim.x);
}
// --- Ocean (Gerstner-inspired waves) ---
float waveHeight(vec2 p) {
float t = time * 1.5;
float h = 0.0;
h = sin(p.x * 0.15 t) * 3.0;
h = sin(p.y * 0.25 - t * 1.3) * 2.0;
h = sin((p.x p.y) * 0.3 t * 1.7) * 1.5;
h = fbm(p * 0.4 vec2(t * 0.2, -t * 0.3)) * 5.0;
return h;
}
// --- Scene Map ---
vec2 map(vec3 p) {
float oceanBase = -1.5;
float ocean = p.y - (oceanBase waveHeight(p.xz));
float city = 1000.0;
float cellSize = 18.0;
vec2 id = floor(p.xz / cellSize);
vec2 q = mod(p.xz, cellSize) - cellSize * 0.5;
float rnd = hash(id);
float rnd2 = hash(id 1.0);
if (rnd > 0.15) {
float w = 2.5 rnd * 1.5;
float h = 35.0 rnd2 * 70.0;
float baseY = -25.0;
// Tapered Main Body
float taperY = clamp((p.y - baseY) / max(0.1, h - baseY), 0.0, 1.0);
float taper = mix(1.3, 0.5, taperY);
float dOct = sdOctagon(q / taper, w) * taper;
// Gothic Vertical Ribs
float angle = atan(q.x, q.y);
float ribs = cos(angle * 8.0) * 0.15;
dOct = ribs * smoothstep(h, h - 15.0, p.y) * smoothstep(baseY, baseY 10.0, p.y);
float body = max(dOct, max(p.y - h, baseY - p.y));
city = min(city, body);
// Central Spire
if (p.y > h - 2.0) {
float spireH = 20.0 rnd2 * 15.0;
float spire = sdCone(vec3(q.x, p.y - h, q.y), vec2(w * 0.8, spireH));
city = min(city, spire);
}
// Corner Spires (Buttresses)
vec2 cornerOffset = vec2(w 1.5);
for(int i = 0; i < 4; i ) {
vec2 c = cornerOffset;
if(i == 1) c.x = -c.x;
if(i == 2) c.y = -c.y;
if(i == 3) c = -c;
vec2 cq = q - c;
float c_h = h * 0.75 rnd2 * 10.0;
float c_taperY = clamp((p.y - baseY) / max(0.1, c_h - baseY), 0.0, 1.0);
float c_taper = mix(1.2, 0.4, c_taperY);
float c_dOct = sdOctagon(cq / c_taper, 0.8) * c_taper;
float cBody = max(c_dOct, max(p.y - c_h, baseY - p.y));
city = min(city, cBody);
// Small spires on corners
if (p.y > c_h - 1.0) {
float c_spireH = 10.0 rnd2 * 5.0;
float c_spire = sdCone(vec3(cq.x, p.y - c_h, cq.y), vec2(0.7, c_spireH));
city = min(city, c_spire);
}
// Flying Buttresses connecting to main tower
if (p.y > baseY && p.y < c_h) {
vec2 dir = normalize(-c);
vec2 perp = vec2(-dir.y, dir.x);
float dist = dot(q - c, dir);
float rad = abs(dot(q - c, perp));
float arch = sin(clamp(dist / length(cornerOffset), 0.0, 3.14) * 0.5) * 2.0;
float buttress = max(rad - 0.4, max(p.y - (c_h * 0.6 arch), dist - length(cornerOffset)));
city = min(city, buttress);
}
}
}
if (city < ocean) return vec2(city, 1.0);
return vec2(ocean, 0.0);
}
vec3 calcNormal(vec3 p) {
vec2 e = vec2(1.0, -1.0) * 0.02;
return normalize(
e.xyy * map(p e.xyy).x
e.yyx * map(p e.yyx).x
e.yxy * map(p e.yxy).x
e.xxx * map(p
e.xxx).x
);
}
// --- Lightning System ---
float getLightningFlash() {
float flashTime = floor(time * 1.5);
float flashRnd = hash(vec2(flashTime, 1.0));
if (flashRnd > 0.92) {
float flashFrac = fract(time * 1.5);
return pow(1.0 - flashFrac, 6.0) step(0.8, flashRnd) * step(flashFrac, 0.2);
}
return 0.0;
}
float lightningBolt(vec3 ro, vec3 rd, float seed) {
vec3 p = vec3(hash(vec2(seed, 1.0)) * 40.0 - 20.0, 30.0, 50.0);
vec3 dir = normalize(vec3(hash(vec2(seed, 2.0)) * 2.0 - 1.0, -1.0, -0.2));
float dist = 1000.0;
for(int i = 0; i < 8; i ) {
vec3 toP = p - ro;
float t = dot(toP, rd);
vec3 closest = ro rd * t;
dist = min(dist, length(closest - p));
// Move bolt point down
p = dir * 5.0;
// Jitter direction
dir.x = (hash(vec2(seed, float(i))) - 0.5) * 0.8;
dir = normalize(dir);
}
return 0.005 / (dist * dist 0.01);
}
// --- ACES Tonemapping ---
vec3 ACESFilm(vec3 x) {
float a = 2.51;
float b = 0.03;
float c = 2.43;
float d = 0.59;
float e = 0.14;
return clamp((x * (a * x b)) / (x * (c * x d) e), 0.0, 1.0);
}
void main() {
vec2 uv = (gl_FragCoord.xy - resolution.xy * 0.5) / resolution.y;
// Cinematic High Altitude Camera (Sweeping over the city)
// Starts at Z=40.0 so we are immediately over the city, not empty ocean
vec3 ro = vec3(sin(time * 0.08) * 25.0, 55.0 sin(time * 0.2) * 8.0, time * 5.0 40.0);
vec3 ta = vec3(sin(time * 0.08 0.5) * 12.0, 10.0 sin(time * 0.4) * 3.0, ro.z 50.0);
vec3 ww = normalize(ta - ro);
vec3 uu = normalize(cross(ww, vec3(0.0, 1.0, 0.0)));
vec3 vv = normalize(cross(uu, ww));
// Chromatic Aberration
float ca = 0.003;
vec3 rd = normalize(uv.x * uu uv.y * vv 1.4 * ww);
vec3 rdR = normalize((uv.x - ca) * uu uv.y * vv 1.4 * ww);
vec3 rdB = normalize((uv.x ca) * uu uv.y * vv 1.4 * ww);
// Raymarch
float t = 0.0, tR = 0.0, tB = 0.0;
float matID = -1.0;
for(int i = 0; i < 120; i ) {
vec3 p = ro rd * t;
vec2 res = map(p);
if(res.x < 0.01) { matID = res.y; break; }
t = res.x * 0.8; // slow down near surfaces for precision
if(t > 250.0) break;
}
// Accurate Chromatic Aberration Depth
if(matID > -1.0) {
tR = t; tB = t;
for(int i = 0; i < 6; i ) {
float dR = map(ro rdR * tR).x;
if(dR < 0.01) break;
tR = dR * 0.5;
}
for(int i = 0; i < 6; i ) {
float dB = map(ro rdB * tB).x;
if(dB < 0.01) break;
tB = dB * 0.5;
}
}
float lightning = getLightningFlash();
vec3 moonColor = vec3(0.3, 0.4, 0.6);
vec3 lightningColor = vec3(0.9, 0.95, 1.0);
vec3 col = vec3(0.0);
if(matID >= 0.0) {
vec3 p = ro rd * t;
vec3 pR = ro rdR * tR;
vec3 pB = ro rdB * tB;
vec3 n = calcNormal(p);
vec3 lightDir = normalize(vec3(0.5, 0.6, 0.3));
if(matID < 0.5) {
// Ocean
float spec = pow(max(0.0, dot(reflect(-lightDir, n), -rd)), 80.0);
vec3 baseCol = mix(vec3(0.01, 0.02, 0.03), vec3(0.05, 0.1, 0.12), n.y);
float foam = smoothstep(2.0, 5.0, waveHeight(p.xz));
baseCol = mix(baseCol, vec3(0.6, 0.7, 0.75), foam);
col = baseCol * (n.y * 0.5 0.5);
col = spec * moonColor * 1.2;
col = spec * lightningColor * lightning * 10.0;
// Foam color separation
col.r = smoothstep(2.0, 5.0, waveHeight(pR.xz)) * 0.2;
col.b = smoothstep(2.0, 5.0, waveHeight(pB.xz)) * 0.2;
} else {
// Gothic City
float diff = max(0.0, dot(n, lightDir));
// Slightly brightened ambient so towers aren't invisible between lightning strikes
vec3 baseCol = vec3(0.07, 0.08, 0.09) * (diff * 0.6 0.4);
float spec = pow(max(0.0, dot(reflect(-lightDir, n), -rd)), 24.0);
baseCol = spec * moonColor * 0.3;
baseCol = spec * lightningColor * lightning * 5.0;
// Glowing Windows (Simplified and robust logic)
vec2 id = floor(p.xz / 18.0);
vec2 lq = mod(p.xz, 18.0) - 9.0;
float wr = 2.5 hash(id) * 1.5;
// Window strips
float winX = abs(mod(lq.x, 1.5) - 0.75);
float winShape = step(winX, 0.2);
float winY = abs(fract(p.y * 0.25) - 0.5);
winShape *= step(winY, 0.35);
float winId = hash(id floor(p.y * 0.25));
float winMask = winShape * step(0.6, winId);
vec3 winCol = vec3(1.0, 0.6, 0.2) * winMask * 2.0; // Brighter glow
winCol *= 0.7 0.3 * sin(time * 8.0 winId * 100.0);
baseCol = winCol;
col = baseCol;
// Window CA
float winShapeR = step(abs(mod(lq.x, 1.5) - 0.75), 0.2) * step(abs(fract(pR.y * 0.25) - 0.5), 0.35);
col.r = winShapeR * step(0.6, hash(id floor(pR.y * 0.25))) * 1.0;
float winShapeB = step(abs(mod(lq.x, 1.5) - 0.75), 0.2) * step(abs(fract(pB.y * 0.25) - 0.5), 0.35);
col.b = winShapeB * step(0.6, hash(id floor(pB.y * 0.25))) * 1.0;
}
// Volumetric Height Fog
vec3 fogCol = mix(vec3(0.01, 0.02, 0.03), vec3(0.05, 0.07, 0.09), clamp(rd.y * 0.5 0.5, 0.0, 1.0));
fogCol = lightningColor * lightning * 0.4;
float fogDensity = exp(-p.y * 0.05) * 0.04;
float fog = 1.0 - exp(-t * fogDensity);
col = mix(col, fogCol, clamp(fog, 0.0, 0.9));
} else {
// Sky
vec3 skyCol = mix(vec3(0.005, 0.01, 0.02), vec3(0.04, 0.05, 0.07), clamp(rd.y * 0.5 0.5, 0.0, 1.0));
// Volumetric Clouds
if(rd.y > 0.0) {
vec2 skyUV = rd.xz / rd.y;
float clouds = fbm(skyUV * 1.5 vec2(time * 0.05, 0.0));
clouds = smoothstep(0.4, 0.8, clouds) * rd.y;
skyCol = mix(skyCol, vec3(0.02, 0.03, 0.04), clouds);
skyCol = lightningColor * lightning * (1.0 - rd.y) * 0.5;
}
// Lightning Bolts
float boltSeed = floor(time * 1.5);
if(hash(vec2(boltSeed, 1.0)) > 0.92) {
skyCol = lightningColor * lightningBolt(ro, rd, boltSeed) * 50.0;
if(hash(vec2(boltSeed, 2.0)) > 0.5) {
skyCol = lightningColor * lightningBolt(ro, rd, boltSeed 1.0) * 50.0;
}
}
col = skyCol;
}
// Driving Rain (Parallax layers)
for(float i = 1.0; i <= 4.0; i ) {
vec2 rUV = uv * (5.0 i * 3.0);
rUV.y = time * (15.0 i * 5.0); // Speed
rUV.x = sin(time * 1.0 i) * 2.0 - ro.z * 0.1; // Wind Camera Z movement
float d = abs(rUV.x - floor(rUV.x 0.5));
float rain = smoothstep(0.06, 0.0, d) * smoothstep(0.5, 0.4, fract(rUV.y));
col = rain * vec3(0.5, 0.6, 0.7) * (0.2 / i);
}
// Color Grading & Vignette
col = ACESFilm(col * 1.1);
col = pow(col, vec3(0.9, 0.95, 1.0)); // Slight color shift
float vignette = smoothstep(1.4, 0.3, length(uv));
col *= vignette;
// Film Grain
col = (hash(gl_FragCoord.xy time) - 0.5) * 0.03;
gl_FragColor = vec4(col, 1.0);
}