Skip to content

Instantly share code, notes, and snippets.

@MagnusThor
Created July 16, 2025 15:36
Show Gist options
  • Select an option

  • Save MagnusThor/fd485bad97cddf3a52f91500103f2558 to your computer and use it in GitHub Desktop.

Select an option

Save MagnusThor/fd485bad97cddf3a52f91500103f2558 to your computer and use it in GitHub Desktop.
This shader raymarches a stylized landscape with fractal terrain, dynamic lake shading, procedural clouds, and post-processing effects like god rays, lens flares, and sun glow. It uses screen-space techniques to simulate lighting, reflections, and atmosphere, creating a rich, animated scene with depth and color variation.
struct Uniforms {
resolution: vec3<f32>,
time: f32
};
struct VertexOutput {
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>
};
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(0) @binding(1) var linearSampler: sampler;
fn fractalNoise(P: vec2<f32>, U: vec3<f32>) -> f32 {
var p_var = P;
var total = 0.0;
var amp = 1.0;
var deriv = vec2<f32>(0.0);
let rot = mat2x2(
cos(1.0 + 11.0 * U.z), -sin(1.0 + 11.0 * U.z),
sin(1.0 + 11.0 * U.z), cos(1.0 + 11.0 * U.z)
);
loop {
if amp < 0.01 {
break;
}
let o = cos(p_var.xyyx + 19.0 * U.xzxz);
let q = vec3<f32>(o.y, o.x, o.w);
deriv += q.xy;
total -= amp * (1.0 + q.z) / (1.0 + 3.0 * dot(deriv, deriv));
p_var = rot * p_var * 2.0;
amp *= 0.55;
}
return total;
}
fn radialBlurFast(fragCoord: vec2<f32>, sunPos: vec2<f32>, sceneColor: vec3<f32>) -> vec3<f32> {
let samples = 20;
let decay = 0.95;
let exposure = 0.12;
let density = 1.85;
let weight = 1.3;
var illuminationDecay = 1.0;
var result = vec3<f32>(0.0);
let deltaTex = (sunPos - fragCoord) / f32(samples) * density;
var coord = fragCoord;
for (var i = 0; i < samples; i = i + 1) {
coord = coord + deltaTex;
let sunStrength = smoothstep(150.0, 50.0, length(coord - sunPos));
let sampleBrightness = sunStrength * length(sceneColor);
let scalar_product = sampleBrightness * illuminationDecay * weight;
result = fma(sceneColor, vec3<f32>(scalar_product), result);
illuminationDecay *= decay;
}
return result * exposure;
}
fn sunDot(uv: vec2<f32>, center: vec2<f32>, radius: f32, color: vec3<f32>) -> vec3<f32> {
let d = distance(uv, center);
let glow = exp(-pow(d / radius, 2.5));
return color * glow;
}
fn doLensGlint(uv: vec2<f32>, c: vec2<f32>, r: f32, w: f32) -> f32 {
let l: f32 = length(uv - c);
return smoothstep(0., w * r, l) * (1. - smoothstep(w * r, r, l));
}
fn lensFlare(fragCoord: vec2<f32>, sunPos: vec2<f32>, res: vec2<f32>, time: f32) -> vec3<f32> {
let dir = sunPos - fragCoord;
let dist = length(dir);
let normDir = dir / dist; // Normalized direction from fragCoord to sunPos
var flare = vec3<f32>(0.0);
// Main central glow/halo (unchanged - good base)
let haloRadius = 200.0;
let halo = pow(max(0.0, 1.0 - length(fragCoord - sunPos) / haloRadius), 4.0);
flare += vec3<f32>(1.0, 0.75, 0.5) * halo * 0.7;
// --- Tiny Ghosting Elements (dots) ---
let numGhosts = 5;
for (var i = 0; i < numGhosts; i = i + 1) {
let ghostOffsetFactor = 0.03 + f32(i) * 0.015;
let dynamicOffsetAmount = sin(time * (0.2 + f32(i) * 0.02)) * 3.0 + 5.0;
let ghostPos = sunPos + normDir * dynamicOffsetAmount * ghostOffsetFactor;
let ghostDist = distance(fragCoord, ghostPos);
let ghostRadius = 1.0 + sin(time * 0.08 + f32(i) * 0.3) * 0.5;
let ghostIntensity = exp(-pow(ghostDist / ghostRadius, 3.0));
let baseColor = vec3<f32>(1.0, 0.9 - 0.15 * f32(i), 0.7 - 0.1 * f32(i));
flare += baseColor * ghostIntensity * (0.02 + sin(time * 0.07 + f32(i)) * 0.002);
}
let numGlints = 4; // Number of small glint rings
for (var i = 0; i < numGlints; i = i + 1) {
// Glint position: subtle offset along the sun-to-fragment direction, with slow animation
let glintOffsetFactor = 0.04 + f32(i) * 0.02;
let glintDynamicOffset = cos(time * (0.15 + f32(i) * 0.02)) * 5.0;
let glintPos = sunPos + normDir * (glintOffsetFactor * 100.0 + glintDynamicOffset);
// Glint radius: tiny, with subtle breathing
let glintRadius = 8.0 + sin(time * (0.1 + f32(i) * 0.01)) * 3.0;
// --- MODIFIED: Glint width (w parameter in doLensGlint) ---
// Increased 'w' to make the rings thicker and more visible.
// This creates a more substantial "circle" rather than a thin line.
let glintThicknessFactor = 0.5 + f32(i) * 0.05; // Base 0.5, increasing to 0.65 for last glint
let glintIntensity = doLensGlint(fragCoord, glintPos, glintRadius, glintThicknessFactor);
// Chromatic aberration effect: separate colors slightly for each glint
var glintColor: vec3<f32>;
if (i == 0) { glintColor = vec3<f32>(1.0, 0.2, 0.2); } // Reddish
else if (i == 1) { glintColor = vec3<f32>(0.2, 1.0, 0.2); } // Greenish
else if (i == 2) { glintColor = vec3<f32>(0.2, 0.2, 1.0); } // Blueish
else { glintColor = vec3<f32>(1.0, 1.0, 0.5); } // Yellowish for the last one
// --- MODIFIED: Overall intensity of the glints ---
// Increased from 0.03 to 0.1 for better visibility, adjust as needed.
flare += glintColor * glintIntensity * 0.1;
}
// Streaks (unchanged - they add a different, distinct effect)
let streakAngle = atan2(normDir.y, normDir.x);
let streaks = abs(sin(6.0 * streakAngle)) * pow(1.0 - dist / length(res), 4.0);
flare += vec3<f32>(1.0, 0.85, 0.65) * streaks * 0.15;
return flare;
}
fn ACESFilm(x: vec3<f32>) -> vec3<f32> {
let a = 2.51;
let b = 0.03;
let c = 2.43;
let d = 0.59;
let e = 0.14;
return clamp((x * (a * x + b)) / (x * (c * x + d) + e), vec3<f32>(0.0), vec3<f32>(1.0));
}
fn noise(p: vec2<f32>) -> f32 {
let i = floor(p);
let f = fract(p);
let a = hash(i);
let b = hash(i + vec2(1.0, 0.0));
let c = hash(i + vec2(0.0, 1.0));
let d = hash(i + vec2(1.0, 1.0));
let u = f * f * (3.0 - 2.0 * f); // smoothstep
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
fn hash2(p: vec2<f32>) -> vec2<f32> {
let a = dot(p, vec2<f32>(127.1, 311.7));
let b = dot(p, vec2<f32>(269.5, 183.3));
return fract(sin(vec2<f32>(a, b)) * 43758.5453);
}
fn hash(p: vec2<f32>) -> f32 {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
fn fbm(p: vec2<f32>) -> f32 {
var value = 0.0;
var amplitude = 0.5;
var frequency = 1.0;
let gain = 0.5;
let lacunarity = 2.0;
for (var i = 0; i < 5; i++) {
value += amplitude * noise(p * frequency);
frequency *= lacunarity;
amplitude *= gain;
}
return value;
}
fn hash3(p: vec3<f32>) -> f32 {
return fract(sin(dot(p, vec3<f32>(127.1, 311.7, 74.7))) * 43758.5453123);
}
fn fresnelSchlick(cosTheta: f32, F0: vec3<f32>) -> vec3<f32> {
let one_minus_cosTheta_pow5 = pow(1.0 - cosTheta, 5.0);
let term_B_vec3 = vec3<f32>(one_minus_cosTheta_pow5);
return fma(vec3<f32>(1.0) - F0, term_B_vec3, F0);
}
fn mainImage(invocation_id: vec2<f32>) -> vec4<f32> {
let R = uniforms.resolution.xy;
let fragCoord = invocation_id.xy;
let time = uniforms.time;
let C = fragCoord.xy;
var O = vec3<f32>(0.0);
var d = 9.0;
var z = 0.0;
let maxSteps = 64;
let epsilon = 1e-3;
let U = vec3<f32>(1.0, 3.0, 0.0);
var orbitTrap = 1e20;
for (var i = 0; i < maxSteps && d > epsilon; i++) {
let speed = 2.5;
let t = time * speed;
let dir = normalize(vec3<f32>(C - 0.5 * R, R.y));
let p = fma(dir, vec3<f32>(z), vec3<f32>(0.0, 0.0, t));
let P = 0.23 * p.xz;
let s = p.y + 4.0;
d = abs(s) + 0.6;
d += fractalNoise(P, U);
orbitTrap = min(orbitTrap, d);
let lake_uv_offset_x = sin(P.x * 5.0 + time);
let lake_uv_offset_y = cos(P.y * 5.0 - time);
let lake_uv = fma(vec2<f32>(lake_uv_offset_x, lake_uv_offset_y), vec2<f32>(0.05), P);
var lake_color = vec3<f32>(
fma(0.15, sin(lake_uv.x * 4.0 + time * 0.5), 0.1),
fma(0.2, cos(lake_uv.y * 6.0 - time * 0.3), 0.3),
fma(0.15, sin(length(lake_uv) * 8.0 + time), 0.5)
);
let shallow = clamp(1.0 - smoothstep(-0.5, 0.0, s), 0.0, 1.0);
lake_color = fma(vec3<f32>(-0.15, -0.1, -0.1), vec3<f32>(shallow), lake_color);
let distFade = exp(-0.02 * z);
let rockA = vec3<f32>(0.3, 0.2, 0.18);
let rockB = vec3<f32>(0.4, 0.25, 0.15);
let rockMix = mix(rockA, rockB, fractalNoise(P * 1.5, U));
let mountain_color_scalar_term = distFade * (1.0 + 2.0 * d);
let mountain_color = rockMix * vec3<f32>(mountain_color_scalar_term);
let grassAmount = smoothstep(0.1, 0.5, s) * smoothstep(80.0, 15.0, z);
let grassColor_base = vec3<f32>(0.15, 0.4, 0.1);
let grassColor_noise_scalar = 0.05 * fractalNoise(P * 10.0, U);
let grassColor_total_scalar = grassAmount * 2.0;
let grassColor = fma(vec3<f32>(grassColor_noise_scalar), vec3<f32>(grassColor_total_scalar),
grassColor_base * vec3<f32>(grassColor_total_scalar));
let mountainWithGrass = mix(mountain_color, grassColor, grassAmount);
let inSky = s > 0.0;
let highUp = z > 100.0;
let scatter = mix(vec3<f32>(1.1, 0.15, 0.25), vec3<f32>(0.3, 0.5, 0.7), C.y / R.y);
let shaded = mix(lake_color, mix(mountainWithGrass, scatter, f32(highUp)), f32(inSky));
O += shaded;
z += d;
}
let skyBlend = C.y / R.y;
let baseSkyColor = mix(vec3<f32>(0.02, 0.03, 0.08), vec3<f32>(0.15, 0.2, 0.3), skyBlend);
var color = mix(baseSkyColor, tanh(O / 100.0), 0.36);
let sunWorldDir = normalize(vec3<f32>(-0.1, 0.8, 0.1));
let depthParallax = 0.04 * skyBlend;
let cloudDriftSpeed1 = 0.05; // Faster layer, drifts right
let cloudDriftSpeed2 = 0.07; // Slower layer, drifts left
let cloudDriftSpeed3 = 0.010; // Medium layer, drifts slightly up-right
let cloud_uv_offset1 = vec2<f32>(time * cloudDriftSpeed1, 0.0);
let cloud_uv_offset2 = vec2<f32>(-time * cloudDriftSpeed2, time * 0.005);
let cloud_uv_offset3 = vec2<f32>(time * cloudDriftSpeed3, time * cloudDriftSpeed3 * 0.5);
let cloud_uv = C * 0.008 + cloud_uv_offset1;
let cloud_uv2 = C * 0.004 + vec2<f32>(10.0, 10.0) + cloud_uv_offset2;
let cloud_uv3 = C * 0.012 + vec2<f32>(20.0, -5.0) + cloud_uv_offset3;
let uv1 = cloud_uv + vec2<f32>(
fbm(cloud_uv * 2.0 + depthParallax),
-fbm(cloud_uv * 1.5 - depthParallax)
);
let uv2 = cloud_uv2 + vec2<f32>(
fbm(cloud_uv2 * 1.5 + depthParallax * 0.8),
-fbm(cloud_uv2 * 1.0 - depthParallax * 0.8)
);
// New third layer UV distortion
let uv3 = cloud_uv3 + vec2<f32>(
fbm(cloud_uv3 * 2.5 + depthParallax * 1.2),
-fbm(cloud_uv3 * 1.8 - depthParallax * 1.2)
);
let verticalFade = smoothstep(0.25, 0.65, C.y / R.y);
let cloud_mask1 = verticalFade *
smoothstep(0.3, 0.7, pow(fbm(uv1 * 1.1), 1.5)) *
smoothstep(0.3, 1.0, skyBlend) *
smoothstep(0.4, 0.75, skyBlend);
let cloud_mask2 = verticalFade *
smoothstep(0.25, 0.6, pow(fbm(uv2 * 1.0), 2.0)) *
smoothstep(0.3, 1.0, skyBlend);
// New third cloud mask
let cloud_mask3 = verticalFade *
smoothstep(0.2, 0.5, pow(fbm(uv3 * 1.3), 1.8)) *
smoothstep(0.25, 0.9, skyBlend); // A bit lower/more visible
let sunLight = clamp(dot(vec3<f32>(0.0, 1.5, 0.0), sunWorldDir), 0.0, 1.0);
let cloudColor = mix(vec3<f32>(1.05, 1.05, 1.0), vec3<f32>(1.3, 1.25, 1.0), 0.4 * sunLight) *
mix(0.5, 1.0, sunLight);
color = mix(color, cloudColor, 0.3 * cloud_mask1);
color = mix(color, cloudColor * 0.6, 0.2 * cloud_mask2);
// Blend in third cloud layer
color = mix(color, cloudColor * 0.8, 0.15 * cloud_mask3);
let reflectedSunWorldDir = normalize(vec3<f32>(sunWorldDir.x, -sunWorldDir.y, sunWorldDir.z));
let reflected_sun_screen_pos = R * 0.5 + reflectedSunWorldDir.xy * R * 0.45;
let sunDist = distance(C, reflected_sun_screen_pos);
let godrayMask = smoothstep(R.x * 0.3, R.x * 0.1, sunDist);
let godrayColor = vec3<f32>(1.0, 0.85, 0.7) * godrayMask * 4.0;
let viewDir = normalize(vec3<f32>(C - 0.5 * R, R.y));
let cosTheta = dot(viewDir, vec3<f32>(0.0, 1.0, 0.0));
let F0 = vec3<f32>(0.08);
let fresnel = fresnelSchlick(clamp(1.0 - cosTheta, 0.0, 1.0), F0).r;
let horizon_y = R.y * 0.45;
let fade_range = R.y * 0.05;
let reflection_visibility = smoothstep(horizon_y - fade_range, horizon_y + fade_range, C.y);
let reflFade = smoothstep(R.y * 0.35, R.y * 0.45, C.y) * fresnel * 2.0;
color += godrayColor * reflFade * reflection_visibility;
let sun_screen_pos = R * 0.5 + sunWorldDir.xy * R * 0.45;
let godray_pull_factor = 0.3;
let screen_center = R * 0.5;
let godray_origin_offset = (screen_center - sun_screen_pos) * godray_pull_factor;
let angled_godray_pos = sun_screen_pos + godray_origin_offset;
let sunGlow = sunDot(C, sun_screen_pos, 60.0, vec3<f32>(4.5, 3.5, 2.8));
let flare = lensFlare(C, sun_screen_pos, R,uniforms.time*0.1) * 1.5;
let godrays = radialBlurFast(C, angled_godray_pos, color) * 1.5;
color += godrays + flare + sunGlow;
let lightDir = normalize(sunWorldDir);
let terrainShade = clamp(dot(vec3<f32>(0.0, 1.0, 0.0), lightDir), 0.0, 1.0);
color *= mix(0.6, 1.0, terrainShade);
let fog = exp(-0.0002 * length(C - sun_screen_pos));
color = mix(baseSkyColor * 1.2, color, fog);
let vignette = smoothstep(1.5, 0.0, length((C - 0.5 * R) / R));
color *= vignette;
return vec4<f32>(ACESFilm(color + sunGlow), 1.0);
}
@fragment
fn main_fragment(in: VertexOutput) -> @location(0) vec4<f32> {
return mainImage(in.pos.xy);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment