Aurora
The northern lights over a mountain ridge, mirrored in a still lake. Each curtain is a line folded by noise on a high layer of air, the same line on every layer above it, so a ray marched up through 48 layers finds a wall of light with a sharp lower edge; stars sit behind it on a grid of directions, and a gamepad's stick turns the view.
· Atmosphere · Fragment shader
Teaches Thin noise curtains marched through, stars
Builds on 01 · Flowing Color Field (hash21, value noise, fractal noise), 03 · Floating 3D Scene (rotation, a voice with overtones), 05 · Cracked Clay (integer hash, sound noise), 11 · Endless Terrain
Watch
The idea
An aurora is light hanging in the air: tall, thin curtains whose lower edge is sharp and whose top fades away. There is nothing solid to hit, so there is no surface to find, as in episode 3. Instead, each curtain is drawn the way it is made. Seen from the ground, it is a line on a high layer of air, folded toward and away from us by noise, and the same line on every layer above it.
So the ray from the eye is marched up through layers: at each height it asks where it meets that layer and how near that point is to a curtain's line, and adds a little light if it is near. A ray that passes along a curtain meets its line on many layers and gathers a lot of light; one that crosses it meets the line on only a few. That is what makes a curtain look like a wall of light, bright where you look along it and thin where you look through it. Behind it all are stars on a grid of directions, and in front a ridge and a lake that gives the sky back.
The view turns with the pointer, and in the browser a gamepad's stick turns it too (see New in Ray Kiln below). It is built in five steps, and each step is a shader you can run on its own.
Stage 1 · The night sky
Each pixel is a direction: float3(q, 1.1), normalised, tipped up by 0.18 radians and turned by the pointer's offset from the middle of the picture (2.4 radians across the picture, 0.9 up and down). In a video the pointer is in the middle, so the view looks a little above the horizon. The sky is a blend from a faint, slightly lighter colour at the horizon to near-black overhead.
starField cuts the sky into cells by direction: the azimuth and elevation of the ray, times 90, so a cell is 1/90 of a radian. A hash of the cell decides whether it holds a star (7% of them when stars is 1); two more hashes put the star somewhere in the middle of its cell, so its glow never crosses into the next one. The star is a soft dot whose width is pixel, the angle one pixel covers, so it stays about a pixel wide at any size of picture. Each star has its own brightness (most faint, a few bright, from the fourth power of a hash), its own tint between warm and cool white, and twinkles at its own rate; near the horizon they fade, as stars do through more air.
stage-1-night-sky.metal
// Stage 1 · The night sky
// Each pixel is a direction to look in: a ray from the eye, tipped a little
// up, and turned by the pointer. The sky is lighter low down. The stars are
// on a grid of directions, a cell every 1/90 radian; a few cells hold a star
// at a random place, a soft dot about a pixel wide that twinkles.
// @param stars 0.6 0.0 1.0 How many stars, and how bright
float hash21(float2 p) {
p = fract(p * float2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
float2 rot(float2 p, float a) {
float c = cos(a), s = sin(a);
return float2(c * p.x - s * p.y, s * p.x + c * p.y);
}
// The star in this direction's cell, if it has one; `pixel` is a pixel's
// angle. Fainter low down, through more air.
float3 starField(float3 rd, float time, float amount, float pixel) {
float az = atan2(rd.x, rd.z), el = asin(clamp(rd.y, -1.0, 1.0));
float2 g = float2(az, el) * 90.0;
float2 cell = floor(g);
float h = hash21(cell);
if (h < 1.0 - 0.07 * amount) return float3(0.0);
float2 at = 0.2 + 0.6 * float2(hash21(cell + 3.1), hash21(cell + 7.7));
float2 d = (fract(g) - at) * float2(cos(el), 1.0) / 90.0;
float spot = exp(-dot(d, d) / (pixel * pixel * 0.8));
float rate = 3.0 + 5.0 * hash21(cell + 1.3);
float twinkle = 0.65 + 0.35 * sin(time * rate + h * 40.0);
float bright = amount * (0.3 + 1.6 * pow(hash21(cell + 5.5), 4.0));
float3 tint = mix(float3(1.0, 0.82, 0.62), float3(0.8, 0.88, 1.0),
hash21(cell + 9.2));
return tint * spot * bright * twinkle * smoothstep(0.0, 0.25, rd.y);
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
float2 q = (uv - 0.5) * float2(aspect, 1.0);
float pixel = 1.0 / (1.1 * u.resolution.y);
// The view, turned and tipped by the pointer's offset from the middle.
float2 steer = u.mouse / u.resolution - 0.5;
float3 rd = normalize(float3(q, 1.1));
rd.yz = rot(rd.yz, -(0.18 + steer.y * 0.9));
rd.xz = rot(rd.xz, steer.x * 2.4);
float3 col = mix(float3(0.035, 0.03, 0.05), float3(0.008, 0.007, 0.014),
smoothstep(0.0, 0.5, rd.y));
col += starField(rd, u.time, p.stars, pixel);
// Below the horizon, for now, only the dark.
if (rd.y < 0.0) col = float3(0.01);
return float4(1.0 - exp(-col * 1.3), 1.0);
}Stage 2 · A curtain is a line on a layer
The first curtain lives on a single layer of air, 1 up. A ray going up meets it at rd.xz * (1 / rd.y): the higher the ray points, the nearer the meeting point. On that layer the curtain is a line running across the view where p.y + fold is 3, and fold is three octaves of value noise along the line, each drifting with the clock at its own speed. The light is exp(-across² / 0.06²): bright on the line and gone a little way off it. From below, the folded line looks like a thin ribbon across the sky, bending and drifting, thinner toward the horizon where the layer is far away.
stage-2-one-layer.metal23 new lines since stage 1
// Stage 2 · A curtain is a line on a layer
// High up, the aurora's light hangs in a layer of air. Here is one layer, 1
// up: a ray meets it where its height reaches 1, at its direction times 1 over
// its rise. On that layer the curtain is a line running across the view,
// folded toward and away from us by three octaves of noise drifting along it.
//
// Near the line the light is bright and falls away fast across it. Seen from
// below, the folded line is a thin ribbon in the sky, nearer and wider
// overhead, thinner toward the horizon.
// @param speed 1.0 0.0 3.0 How fast the curtain folds and drifts
// @param stars 0.6 0.0 1.0 How many stars, and how bright
float hash21(float2 p) {
p = fract(p * float2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
// Value noise: random values on a grid, blended smoothly between the grid
// points.
float vnoise(float2 p) {
float2 i = floor(p), f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash21(i), b = hash21(i + float2(1, 0));
float c = hash21(i + float2(0, 1)), d = hash21(i + float2(1, 1));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
float2 rot(float2 p, float a) {
float c = cos(a), s = sin(a);
return float2(c * p.x - s * p.y, s * p.x + c * p.y);
}
float3 starField(float3 rd, float time, float amount, float pixel) {
float az = atan2(rd.x, rd.z), el = asin(clamp(rd.y, -1.0, 1.0));
float2 g = float2(az, el) * 90.0;
float2 cell = floor(g);
float h = hash21(cell);
if (h < 1.0 - 0.07 * amount) return float3(0.0);
float2 at = 0.2 + 0.6 * float2(hash21(cell + 3.1), hash21(cell + 7.7));
float2 d = (fract(g) - at) * float2(cos(el), 1.0) / 90.0;
float spot = exp(-dot(d, d) / (pixel * pixel * 0.8));
float rate = 3.0 + 5.0 * hash21(cell + 1.3);
float twinkle = 0.65 + 0.35 * sin(time * rate + h * 40.0);
float bright = amount * (0.3 + 1.6 * pow(hash21(cell + 5.5), 4.0));
float3 tint = mix(float3(1.0, 0.82, 0.62), float3(0.8, 0.88, 1.0),
hash21(cell + 9.2));
return tint * spot * bright * twinkle * smoothstep(0.0, 0.25, rd.y);
}
// The curtain's light at a point xz on a layer: one line, folded by noise.
float curtain(float2 xz, float time) {
float2 p = xz * 0.25;
float fold = 3.2 * vnoise(float2(p.x * 0.3 + time * 0.05, 1.0))
+ 1.2 * vnoise(float2(p.x * 0.9 - time * 0.09, 5.0))
+ 0.5 * vnoise(float2(p.x * 2.3 + time * 0.2, 9.0));
float across = p.y + fold - 3.0;
return exp(-across * across / (0.06 * 0.06));
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
float2 q = (uv - 0.5) * float2(aspect, 1.0);
float time = u.time * p.speed;
float pixel = 1.0 / (1.1 * u.resolution.y);
float2 steer = u.mouse / u.resolution - 0.5;
float3 rd = normalize(float3(q, 1.1));
rd.yz = rot(rd.yz, -(0.18 + steer.y * 0.9));
rd.xz = rot(rd.xz, steer.x * 2.4);
float3 col = mix(float3(0.035, 0.03, 0.05), float3(0.008, 0.007, 0.014),
smoothstep(0.0, 0.5, rd.y));
col += starField(rd, u.time, p.stars, pixel);
if (rd.y > 0.0) {
// Where the ray meets the layer 1 up.
float2 xz = rd.xz * (1.0 / rd.y);
col += float3(1.0, 0.75, 0.3) * curtain(xz, time)
* smoothstep(0.02, 0.2, rd.y);
} else {
col = float3(0.01);
}
return float4(1.0 - exp(-col * 1.3), 1.0);
}1 line from stage 1 that are gone
if (rd.y < 0.0) col = float3(0.01);Stage 3 · Marched up through the layers
Now the same line is on every layer from 1 up to the curtain's top (0.8 + 2.4 × height above that), and aurora marches the ray up through 48 of them. Each layer adds the curtain's light where the ray meets it, coloured by kiln at how far up it is: white-hot at the lower edge, gold, then ember at the top. The light fades with height (exp(-3f), f from 0 at the bottom to 1 at the top) and with distance, and rises over the first two layers, so the lower edge is sharp but not jagged.
Forty-eight layers would show as 48 steps, so each pixel starts its layers a random share of one step higher, a new share every frame (jitter, a hash of the pixel and the clock): the steps blur into one smooth wall. A far curtain is drawn a little wider, since only a few layers cross it there.
stage-3-marched.metal31 new lines since stage 2
// Stage 3 · Marched up through the layers
// The same line on every layer from 1 up to the curtain's top stacks into a
// wall. The ray is marched up through 48 layers, each adding the light it
// meets there, coloured by how far up it is: white-hot at the lower edge, gold,
// then ember, with the kiln's palette, fading with height and with distance.
//
// Every pixel starts its layers a random share of a step higher, a new share
// each frame, so the 48 steps blur together instead of showing as bands. The
// light rises over the first two layers: a sharp lower edge, but not a jagged
// one.
// @param speed 1.0 0.0 3.0 How fast the curtain folds and drifts
// @param height 0.5 0.0 1.0 How far up the curtain reaches before it fades
// @param stars 0.6 0.0 1.0 How many stars, and how bright
float hash21(float2 p) {
p = fract(p * float2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
// Value noise: random values on a grid, blended smoothly between the grid
// points.
float vnoise(float2 p) {
float2 i = floor(p), f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash21(i), b = hash21(i + float2(1, 0));
float c = hash21(i + float2(0, 1)), d = hash21(i + float2(1, 1));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
float2 rot(float2 p, float a) {
float c = cos(a), s = sin(a);
return float2(c * p.x - s * p.y, s * p.x + c * p.y);
}
float3 starField(float3 rd, float time, float amount, float pixel) {
float az = atan2(rd.x, rd.z), el = asin(clamp(rd.y, -1.0, 1.0));
float2 g = float2(az, el) * 90.0;
float2 cell = floor(g);
float h = hash21(cell);
if (h < 1.0 - 0.07 * amount) return float3(0.0);
float2 at = 0.2 + 0.6 * float2(hash21(cell + 3.1), hash21(cell + 7.7));
float2 d = (fract(g) - at) * float2(cos(el), 1.0) / 90.0;
float spot = exp(-dot(d, d) / (pixel * pixel * 0.8));
float rate = 3.0 + 5.0 * hash21(cell + 1.3);
float twinkle = 0.65 + 0.35 * sin(time * rate + h * 40.0);
float bright = amount * (0.3 + 1.6 * pow(hash21(cell + 5.5), 4.0));
float3 tint = mix(float3(1.0, 0.82, 0.62), float3(0.8, 0.88, 1.0),
hash21(cell + 9.2));
return tint * spot * bright * twinkle * smoothstep(0.0, 0.25, rd.y);
}
// The Ray Kiln palette: near-black, ember, orange, gold, white-hot.
float3 kiln(float t) {
float3 c0 = float3(0.020, 0.012, 0.020);
float3 c1 = float3(0.280, 0.040, 0.030);
float3 c2 = float3(0.880, 0.260, 0.050);
float3 c3 = float3(1.000, 0.680, 0.200);
float3 c4 = float3(1.000, 0.970, 0.840);
t = clamp(t, 0.0, 1.0);
float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
c = mix(c, c2, smoothstep(0.22, 0.50, t));
c = mix(c, c3, smoothstep(0.50, 0.76, t));
return mix(c, c4, smoothstep(0.76, 1.00, t));
}
float curtain(float2 xz, float time) {
float2 p = xz * 0.25;
float fold = 3.2 * vnoise(float2(p.x * 0.3 + time * 0.05, 1.0))
+ 1.2 * vnoise(float2(p.x * 0.9 - time * 0.09, 5.0))
+ 0.5 * vnoise(float2(p.x * 2.3 + time * 0.2, 9.0));
float across = p.y + fold - 3.0;
// Wider far away, so the few layers that cross it there still find it.
float width = 0.06 * (1.0 + 0.04 * length(xz));
return exp(-across * across / (width * width));
}
// The light along a ray going up, through 48 layers from 1 to the top.
float3 aurora(float3 rd, float time, constant Params& p, float jitter) {
if (rd.y <= 0.0) return float3(0.0);
const int layers = 48;
float top = 0.8 + 2.4 * p.height;
float3 col = float3(0.0);
for (int i = 0; i < layers; i++) {
float f = (float(i) + jitter) / float(layers);
float2 xz = rd.xz * ((1.0 + f * top) / rd.y);
// Rising over the first two layers, fading with height and distance.
float rise = smoothstep(0.0, 0.04, f);
float fade = exp(-f * 3.0) * exp(-length(xz) * 0.06);
col += kiln(0.95 - 0.75 * f) * curtain(xz, time) * rise * fade;
}
return col * (30.0 / float(layers)) * smoothstep(0.02, 0.2, rd.y);
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
float2 q = (uv - 0.5) * float2(aspect, 1.0);
float time = u.time * p.speed;
float pixel = 1.0 / (1.1 * u.resolution.y);
float2 steer = u.mouse / u.resolution - 0.5;
float3 rd = normalize(float3(q, 1.1));
rd.yz = rot(rd.yz, -(0.18 + steer.y * 0.9));
rd.xz = rot(rd.xz, steer.x * 2.4);
// A different share of a layer for each pixel and frame.
float jitter = hash21(uv * u.resolution + fract(u.time * 7.0) * 61.0);
float3 col = mix(float3(0.035, 0.03, 0.05), float3(0.008, 0.007, 0.014),
smoothstep(0.0, 0.5, rd.y));
col += starField(rd, u.time, p.stars, pixel);
col += aurora(rd, time, p, jitter);
if (rd.y < 0.0) col = float3(0.01);
return float4(1.0 - exp(-col * 1.3), 1.0);
}8 lines from stage 2 that are gone
return exp(-across * across / (0.06 * 0.06));
⋯
if (rd.y > 0.0) {
⋯
float2 xz = rd.xz * (1.0 / rd.y);
col += float3(1.0, 0.75, 0.3) * curtain(xz, time)
* smoothstep(0.02, 0.2, rd.y);
} else {
col = float3(0.01);
}Stage 4 · Six curtains, rays and surges
curtain now repeats the line every 1.6 across the layer, floor(z) numbering each band, and keeps the six nearest: curtains one behind another, the far ones lower in the sky. Each is lit along only part of its length, where along, a hash of its number plus slow noise along it, is above a threshold set by lit: more of it the higher storm, and more in a surge, a slow swell from 0 to 1 that comes and goes with its own noise over time (offset for each curtain, so they don't all swell at once).
The rays are two layers of noise along the curtain, fine streaks running with the clock. They depend only on where along the curtain a point is, not on its height, so they are the same on every layer and stand upright, as an aurora's rays do. The lower edge rises and falls a little along each curtain (edge).
stage-4-curtains.metal23 new lines since stage 3
// Stage 4 · Six curtains, rays and surges
// The line repeats every 1.6 across the layer: six curtains, one behind
// another. Each is lit along part of its length, from a hash of its number and
// slow noise along it: more of it the higher storm, and more in a surge, a
// slow swell that comes and goes with its own noise over time.
//
// Rays are fine streaks of noise along each curtain, the same on every layer,
// so they stand upright, and they run along it with the clock. The lower edge
// rises and falls a little along the curtain, and a far curtain is drawn wider
// so the few layers that cross it still find it.
// @param speed 1.0 0.0 3.0 How fast the curtains fold, drift and surge
// @param storm 0.6 0.0 1.0 How active the aurora is: how many curtains are lit, and how brightly
// @param height 0.5 0.0 1.0 How far up the curtains reach before they fade
// @param stars 0.6 0.0 1.0 How many stars, and how bright
float hash21(float2 p) {
p = fract(p * float2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
// Value noise: random values on a grid, blended smoothly between the grid
// points.
float vnoise(float2 p) {
float2 i = floor(p), f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash21(i), b = hash21(i + float2(1, 0));
float c = hash21(i + float2(0, 1)), d = hash21(i + float2(1, 1));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
float2 rot(float2 p, float a) {
float c = cos(a), s = sin(a);
return float2(c * p.x - s * p.y, s * p.x + c * p.y);
}
float3 starField(float3 rd, float time, float amount, float pixel) {
float az = atan2(rd.x, rd.z), el = asin(clamp(rd.y, -1.0, 1.0));
float2 g = float2(az, el) * 90.0;
float2 cell = floor(g);
float h = hash21(cell);
if (h < 1.0 - 0.07 * amount) return float3(0.0);
float2 at = 0.2 + 0.6 * float2(hash21(cell + 3.1), hash21(cell + 7.7));
float2 d = (fract(g) - at) * float2(cos(el), 1.0) / 90.0;
float spot = exp(-dot(d, d) / (pixel * pixel * 0.8));
float rate = 3.0 + 5.0 * hash21(cell + 1.3);
float twinkle = 0.65 + 0.35 * sin(time * rate + h * 40.0);
float bright = amount * (0.3 + 1.6 * pow(hash21(cell + 5.5), 4.0));
float3 tint = mix(float3(1.0, 0.82, 0.62), float3(0.8, 0.88, 1.0),
hash21(cell + 9.2));
return tint * spot * bright * twinkle * smoothstep(0.0, 0.25, rd.y);
}
// The Ray Kiln palette: near-black, ember, orange, gold, white-hot.
float3 kiln(float t) {
float3 c0 = float3(0.020, 0.012, 0.020);
float3 c1 = float3(0.280, 0.040, 0.030);
float3 c2 = float3(0.880, 0.260, 0.050);
float3 c3 = float3(1.000, 0.680, 0.200);
float3 c4 = float3(1.000, 0.970, 0.840);
t = clamp(t, 0.0, 1.0);
float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
c = mix(c, c2, smoothstep(0.22, 0.50, t));
c = mix(c, c3, smoothstep(0.50, 0.76, t));
return mix(c, c4, smoothstep(0.76, 1.00, t));
}
// The aurora's surges: a slow swell from 0 to 1, its own noise over time.
float surge(float time) {
return smoothstep(0.2, 0.8, vnoise(float2(time * 0.09, 3.7)));
}
// The curtains' light at xz on a layer, and their lower edge there.
float curtain(float2 xz, float time, float storm, thread float& edge) {
float2 p = xz * 0.25;
float fold = 3.2 * vnoise(float2(p.x * 0.3 + time * 0.05, 1.0))
+ 1.2 * vnoise(float2(p.x * 0.9 - time * 0.09, 5.0))
+ 0.5 * vnoise(float2(p.x * 2.3 + time * 0.2, 9.0));
float z = (p.y + fold) / 1.6;
float k = floor(z);
float across = (fract(z) - 0.5) * 1.6;
if (k < 0.0 || k > 5.0) { edge = 1.0; return 0.0; }
float lit = storm * (0.55 + 0.6 * surge(time + k * 3.0));
float along = 0.4 * hash21(float2(k, 4.2))
+ 0.6 * vnoise(float2(p.x * 0.3 + k * 13.0, time * 0.04));
float on = smoothstep(1.0 - lit, 1.2 - lit, along);
float width = 0.04 + 0.04 * hash21(float2(k, 8.9));
width *= 1.0 + 0.04 * length(xz);
float sheet = exp(-across * across / (width * width));
float rays = 0.45 + 0.9 * vnoise(float2(p.x * 18.0 + time * 0.7, k * 7.0));
rays *= 0.6 + 0.6 * vnoise(float2(p.x * 3.0 - time * 0.3, k * 3.0));
edge = 0.12 * vnoise(float2(p.x * 1.7 + time * 0.1, k));
return sheet * on * rays;
}
float3 aurora(float3 rd, float time, constant Params& p, float jitter) {
if (rd.y <= 0.0) return float3(0.0);
const int layers = 48;
float top = 0.8 + 2.4 * p.height;
float3 col = float3(0.0);
for (int i = 0; i < layers; i++) {
float f = (float(i) + jitter) / float(layers);
float2 xz = rd.xz * ((1.0 + f * top) / rd.y);
float edge;
float c = curtain(xz, time, p.storm, edge);
float rise = smoothstep(edge, edge + 0.04, f);
float fade = exp(-f * 3.0) * exp(-length(xz) * 0.06);
col += kiln(0.95 - 0.75 * f) * c * rise * fade;
}
return col * (30.0 / float(layers)) * smoothstep(0.02, 0.2, rd.y);
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
float2 q = (uv - 0.5) * float2(aspect, 1.0);
float time = u.time * p.speed;
float pixel = 1.0 / (1.1 * u.resolution.y);
float2 steer = u.mouse / u.resolution - 0.5;
float3 rd = normalize(float3(q, 1.1));
rd.yz = rot(rd.yz, -(0.18 + steer.y * 0.9));
rd.xz = rot(rd.xz, steer.x * 2.4);
// A different share of a layer for each pixel and frame.
float jitter = hash21(uv * u.resolution + fract(u.time * 7.0) * 61.0);
float3 col = mix(float3(0.035, 0.03, 0.05), float3(0.008, 0.007, 0.014),
smoothstep(0.0, 0.5, rd.y));
col += starField(rd, u.time, p.stars, pixel);
col += aurora(rd, time, p, jitter);
if (rd.y < 0.0) col = float3(0.01);
return float4(1.0 - exp(-col * 1.3), 1.0);
}6 lines from stage 3 that are gone
float curtain(float2 xz, float time) {
⋯
float across = p.y + fold - 3.0;
⋯
float width = 0.06 * (1.0 + 0.04 * length(xz));
return exp(-across * across / (width * width));
⋯
float rise = smoothstep(0.0, 0.04, f);
⋯
col += kiln(0.95 - 0.75 * f) * curtain(xz, time) * rise * fade;Stage 5 · Aurora
The last step puts the curtains over a landscape and lets them change colour.
- The ridge.
ridgegives the mountains' height above the horizon, as an angle, for each direction round: fractal noise, higher where a slow wave lifts it. Below that angle the sky is replaced by near-black rock, with a faint flecking of the aurora's colour where snow would catch it. - The lake. A ray pointing down sees the sky mirrored: the same
skyViewwith the ray tipped up as much as it pointed down. Value noise on the water, stretched across and drifting with the clock, shifts the mirrored ray a little, so the reflection ripples, and it is darker and a little cooler, since water gives back only part of the light. - Colour.
curtainColourblends two palettes bygreen. At 0 it is the kiln's, white-hot at the lower edge through gold to ember. At 1 it is oxygen's, the colours of a real aurora: a thin pink fringe at the edge, green through the body of the curtain and a deep red high up. - Light, not paint. The curtains and stars are added to the sky, and
1 − exp(−col × 1.3)bends bright light toward white rather than clipping it.
the kiln palette and shade(), from the finished shader
// The Ray Kiln palette: near-black, ember, orange, gold, white-hot.
float3 kiln(float t) {
float3 c0 = float3(0.020, 0.012, 0.020);
float3 c1 = float3(0.280, 0.040, 0.030);
float3 c2 = float3(0.880, 0.260, 0.050);
float3 c3 = float3(1.000, 0.680, 0.200);
float3 c4 = float3(1.000, 0.970, 0.840);
t = clamp(t, 0.0, 1.0);
float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
c = mix(c, c2, smoothstep(0.22, 0.50, t));
c = mix(c, c3, smoothstep(0.50, 0.76, t));
return mix(c, c4, smoothstep(0.76, 1.00, t));
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
float2 q = (uv - 0.5) * float2(aspect, 1.0);
float time = u.time * p.speed;
float pixel = 1.0 / (1.1 * u.resolution.y);
// The view: looking a little up, turned and tipped by the pointer's offset
// from the middle of the picture (in an export it is the middle).
float2 steer = u.mouse / u.resolution - 0.5;
float3 rd = normalize(float3(q, 1.1));
rd.yz = rot(rd.yz, -(0.18 + steer.y * 0.9));
rd.xz = rot(rd.xz, steer.x * 2.4);
// A different share of a layer for each pixel and frame (aurora above).
float jitter = hash21(uv * u.resolution + fract(u.time * 7.0) * 61.0);
float3 col;
if (rd.y >= 0.0) {
col = skyView(rd, time, p, jitter, pixel);
} else {
// The lake: the sky mirrored, the ray tipped up as much as it pointed
// down, rippled by noise on the water that drifts with the clock, and
// darker, since water gives back only part of the light.
float2 water = rd.xz / -rd.y;
float2 at = water * float2(1.0, 4.0) + float2(0.0, time * 0.4);
float ripple = vnoise(at) - 0.5;
float3 up = normalize(float3(rd.x + ripple * 0.006,
-rd.y + ripple * 0.004, rd.z));
col = skyView(up, time, p, jitter, pixel) * float3(0.5, 0.55, 0.6);
}
// Bright light goes to white rather than clipping its colour.
col = 1.0 - exp(-col * 1.3);
return float4(col, 1.0);
}episode-24-aurora.metal, the whole file119 new lines since stage 4
// Ray Kiln · Episode 24 · Aurora
//
// @episode 24 Aurora
// @length 60
// @variation 30
// @variations 1,2,3,5,6,8,9,11
// @still 14.0
// @teaches Thin noise curtains marched through, stars
// @category Fragment
// @tags volume rendering, fractal noise, stars, aurora, reflection, procedural sound
// @final Aurora | The curtains over a ridge and a still lake: they fold and drift, brighten in surges, and the lake gives them back.
//
// The northern lights over a mountain ridge, mirrored in a lake. An aurora is light hanging in the air: tall, thin
// curtains whose lower edge is sharp and whose top fades away. Seen from the ground, each curtain is a line on a high
// layer of the sky, folded by noise, and the same line on every layer above it. So the ray from the eye is marched up
// through the layers, and at each one it asks how near it is to a curtain's line: on the same line at every height,
// the light stacks into a wall. Stars sit behind it on a grid of directions, the ridge is fractal noise against the
// sky, and the lake is the same sky seen upside down, rippled. The pointer turns the view: in the playground, move it,
// steer with the arrow keys, or with a gamepad's stick.
// Each @param line below becomes a slider in the host app and a row in the page's parameters table:
// // @param name default min max description
// @param speed 1.0 0.0 3.0 How fast the curtains fold, drift and surge
// @param storm 0.6 0.0 1.0 How active the aurora is: how many curtains are lit, and how brightly
// @param height 0.5 0.0 1.0 How far up the curtains reach before they fade
// @param green 0.0 0.0 1.0 The curtains' colours, from the kiln's gold and ember to oxygen's green and red
// @param stars 0.6 0.0 1.0 How many stars, and how bright
// @param volume 0.8 0.0 1.0 Loudness of the sound
// ---- Noise ------------------------------------------------------------------
float hash21(float2 p) {
p = fract(p * float2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
// Value noise: random values on a grid, blended smoothly between the grid
// points.
float vnoise(float2 p) {
float2 i = floor(p), f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash21(i), b = hash21(i + float2(1, 0));
float c = hash21(i + float2(0, 1)), d = hash21(i + float2(1, 1));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
// Fractal noise: several octaves of value noise, each finer and fainter than
// the last.
float fbm(float2 p) {
float v = 0.0, a = 0.5;
for (int i = 0; i < 5; i++) {
v += a * vnoise(p);
p = p * 2.03 + float2(17.1, 9.2);
a *= 0.5;
}
return v;
}
float2 rot(float2 p, float a) {
float c = cos(a), s = sin(a);
return float2(c * p.x - s * p.y, s * p.x + c * p.y);
}
// The Ray Kiln palette: near-black, ember, orange, gold, white-hot.
float3 kiln(float t) {
float3 c0 = float3(0.020, 0.012, 0.020);
float3 c1 = float3(0.280, 0.040, 0.030);
float3 c2 = float3(0.880, 0.260, 0.050);
float3 c3 = float3(1.000, 0.680, 0.200);
float3 c4 = float3(1.000, 0.970, 0.840);
t = clamp(t, 0.0, 1.0);
float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
c = mix(c, c2, smoothstep(0.22, 0.50, t));
c = mix(c, c3, smoothstep(0.50, 0.76, t));
return mix(c, c4, smoothstep(0.76, 1.00, t));
}
// ---- The curtains -----------------------------------------------------------
// The aurora's surges: a slow swell from 0 to 1, its own noise over time, that
// lights more curtains and brightens them. The sound swells with it.
float surge(float time) {
return smoothstep(0.2, 0.8, vnoise(float2(time * 0.09, 3.7)));
}
// How much curtain there is at a point xz on one layer of the sky. The
// curtains are lines running across the view, one behind another, 6.4 apart
// on layers whose lowest is 1 up; each is folded toward and away from us by
// noise that drifts along it, and the six nearest are drawn. Near its line a
// curtain is bright, and thin: its light falls away fast across it. Its rays
// are fine streaks along the line, the same on every layer, so they stand
// upright. `edge` gives its lower edge here, as a share of the way up.
float curtain(float2 xz, float time, float storm, thread float& edge) {
float2 p = xz * 0.25;
float fold = 3.2 * vnoise(float2(p.x * 0.3 + time * 0.05, 1.0))
+ 1.2 * vnoise(float2(p.x * 0.9 - time * 0.09, 5.0))
+ 0.5 * vnoise(float2(p.x * 2.3 + time * 0.2, 9.0));
float z = (p.y + fold) / 1.6;
float k = floor(z);
float across = (fract(z) - 0.5) * 1.6;
// Only the six curtains from 0 to 5 bands away; the rest is empty sky.
if (k < 0.0 || k > 5.0) { edge = 1.0; return 0.0; }
// Each curtain lit along part of its length, by a hash of its number and
// slow noise along it: more of it the higher storm, and more in a surge.
float lit = storm * (0.55 + 0.6 * surge(time + k * 3.0));
float along = 0.4 * hash21(float2(k, 4.2))
+ 0.6 * vnoise(float2(p.x * 0.3 + k * 13.0, time * 0.04));
float on = smoothstep(1.0 - lit, 1.2 - lit, along);
// A far curtain is drawn a little wider, so the few layers that meet it
// still find it.
float width = 0.04 + 0.04 * hash21(float2(k, 8.9));
width *= 1.0 + 0.04 * length(xz);
float sheet = exp(-across * across / (width * width));
float rays = 0.45 + 0.9 * vnoise(float2(p.x * 18.0 + time * 0.7, k * 7.0));
rays *= 0.6 + 0.6 * vnoise(float2(p.x * 3.0 - time * 0.3, k * 3.0));
edge = 0.12 * vnoise(float2(p.x * 1.7 + time * 0.1, k));
return sheet * on * rays;
}
// The colour a share f of the way up: the kiln's, white-hot to ember, or
// oxygen's, pink at the edge, green, then deep red, blended by `green`.
float3 curtainColour(float f, float green) {
float3 kilnCol = kiln(0.95 - 0.75 * f);
float3 oxygen = mix(float3(0.15, 1.0, 0.42), float3(0.85, 0.08, 0.22),
smoothstep(0.25, 0.8, f));
oxygen = mix(float3(0.95, 0.35, 0.6), oxygen, smoothstep(0.0, 0.06, f));
return mix(kilnCol, oxygen, green);
}
// The aurora along a ray going up: marched through 48 layers of sky from
// height 1 to the curtains' top, each layer adding the light of the curtains
// it meets. `jitter` moves this pixel's layers up by a share of one step, so
// the steps blur rather than show as bands.
float3 aurora(float3 rd, float time, constant Params& p, float jitter) {
if (rd.y <= 0.0) return float3(0.0);
const int layers = 48;
float top = 0.8 + 2.4 * p.height;
float3 col = float3(0.0);
for (int i = 0; i < layers; i++) {
float f = (float(i) + jitter) / float(layers);
float h = 1.0 + f * top;
float2 xz = rd.xz * (h / rd.y);
float edge;
float c = curtain(xz, time, p.storm, edge);
// Sharp at the lower edge, fading with height, fainter far away.
float rise = smoothstep(edge, edge + 0.04, f);
float fade = exp(-f * 3.0) * exp(-length(xz) * 0.06);
col += curtainColour(f, p.green) * c * rise * fade;
}
return col * (30.0 / float(layers)) * smoothstep(0.02, 0.2, rd.y);
}
// ---- The sky, the stars, the ridge ------------------------------------------
// The stars, as in the first step: a few cells of 1/90 radian hold a soft,
// twinkling dot about a pixel wide (`pixel` is a pixel's angle).
float3 starField(float3 rd, float time, float amount, float pixel) {
float az = atan2(rd.x, rd.z), el = asin(clamp(rd.y, -1.0, 1.0));
float2 g = float2(az, el) * 90.0;
float2 cell = floor(g);
float h = hash21(cell);
if (h < 1.0 - 0.07 * amount) return float3(0.0);
float2 at = 0.2 + 0.6 * float2(hash21(cell + 3.1), hash21(cell + 7.7));
float2 d = (fract(g) - at) * float2(cos(el), 1.0) / 90.0;
float spot = exp(-dot(d, d) / (pixel * pixel * 0.8));
float rate = 3.0 + 5.0 * hash21(cell + 1.3);
float twinkle = 0.65 + 0.35 * sin(time * rate + h * 40.0);
float bright = amount * (0.3 + 1.6 * pow(hash21(cell + 5.5), 4.0));
float3 tint = mix(float3(1.0, 0.82, 0.62), float3(0.8, 0.88, 1.0),
hash21(cell + 9.2));
return tint * spot * bright * twinkle * smoothstep(0.0, 0.25, rd.y);
}
// The ridge's height above the horizon, as an angle, for each direction round:
// fractal noise, higher where a slow wave lifts it.
float ridge(float az) {
float lift = smoothstep(-0.4, 1.4, sin(az * 1.3 + 0.5) + 1.0);
return 0.02 + 0.12 * fbm(float2(az * 2.2, 1.7)) * lift;
}
// Everything a ray sees above the horizon: the night sky, a little lighter
// low down, the stars, the curtains, and the ridge in front of them, a black
// shape with a little of the aurora's light on its snow.
float3 skyView(float3 rd, float time, constant Params& p, float jitter,
float pixel) {
float3 col = mix(float3(0.035, 0.03, 0.05), float3(0.008, 0.007, 0.014),
smoothstep(0.0, 0.5, rd.y));
col += starField(rd, time, p.stars, pixel);
col += aurora(rd, time, p, jitter);
float az = atan2(rd.x, rd.z);
float r = ridge(az);
if (rd.y < r) {
float snow = fbm(float2(az * 40.0, rd.y * 60.0));
float3 glint = curtainColour(0.1, p.green) * 0.03 * p.storm * snow;
float3 rock = float3(0.012, 0.011, 0.016) + glint;
col = mix(col, rock, smoothstep(r, r - pixel * 1.5, rd.y));
}
return col;
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
float2 q = (uv - 0.5) * float2(aspect, 1.0);
float time = u.time * p.speed;
float pixel = 1.0 / (1.1 * u.resolution.y);
// The view: looking a little up, turned and tipped by the pointer's offset
// from the middle of the picture (in an export it is the middle).
float2 steer = u.mouse / u.resolution - 0.5;
float3 rd = normalize(float3(q, 1.1));
rd.yz = rot(rd.yz, -(0.18 + steer.y * 0.9));
rd.xz = rot(rd.xz, steer.x * 2.4);
// A different share of a layer for each pixel and frame (aurora above).
float jitter = hash21(uv * u.resolution + fract(u.time * 7.0) * 61.0);
float3 col;
if (rd.y >= 0.0) {
col = skyView(rd, time, p, jitter, pixel);
} else {
// The lake: the sky mirrored, the ray tipped up as much as it pointed
// down, rippled by noise on the water that drifts with the clock, and
// darker, since water gives back only part of the light.
float2 water = rd.xz / -rd.y;
float2 at = water * float2(1.0, 4.0) + float2(0.0, time * 0.4);
float ripple = vnoise(at) - 0.5;
float3 up = normalize(float3(rd.x + ripple * 0.006,
-rd.y + ripple * 0.004, rd.z));
col = skyView(up, time, p, jitter, pixel) * float3(0.5, 0.55, 0.6);
}
// Bright light goes to white rather than clipping its colour.
col = 1.0 - exp(-col * 1.3);
return float4(col, 1.0);
}
// ---- Sound ------------------------------------------------------------------
// A low drone in A, a fifth and an octave, swelling and brightening with the
// aurora's surges; over it three glassy high tones whose loudness in each ear
// runs like the curtains' rays, so the shimmer moves between the ears; and in
// a surge, faint crackles.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// Noise for sound: random values joined `rate` times a second.
float noise1(float t, float rate, uint seed) {
float x = t * rate;
float i = floor(x), f = fract(x);
f = f * f * (3.0 - 2.0 * f);
uint k = uint(i) + seed * 7919u;
return mix(hashU(k), hashU(k + 1u), f) * 2.0 - 1.0;
}
// A soft tone with some of its overtones: `bright` from 0 (a pure sine) to 1
// (a reedy, fuller sound).
float voice(float ph, float bright) {
const float tau = 6.2831853;
float over = 0.45 * sin(2.0 * tau * ph) + 0.25 * sin(3.0 * tau * ph)
+ 0.12 * sin(4.0 * tau * ph);
return sin(tau * ph) + bright * over;
}
float2 sound(float t, constant Params& p) {
const float tau = 6.2831853;
float time = t * p.speed;
float swell = p.storm * (0.35 + 0.65 * surge(time));
// The drone: A1, E2 and A2, each a few cents apart in the two ears.
float bright = 0.15 + 0.6 * swell;
float2 a1 = float2(voice(55.0 * t, bright), voice(55.08 * t, bright));
float2 e2 = float2(voice(82.45 * t, bright), voice(82.35 * t, bright));
float2 a2 = float2(voice(110.1 * t, bright), voice(109.95 * t, bright));
float2 drone = a1 * 0.5 + e2 * 0.3 + a2 * 0.2;
drone *= 0.2 + 0.15 * swell;
// The shimmer: A4, E5 and A5, each as loud in each ear as noise that runs
// at the speed of the picture's rays, one sample to the left, one to the
// right.
float2 shimmer = float2(0.0);
for (int i = 0; i < 3; i++) {
float side = float(i) - 1.0;
float run = side * 0.7 + time * 0.7;
float raysL = vnoise(float2(run - 3.0, 14.0 + float(i)));
float raysR = vnoise(float2(run + 3.0, 21.0 + float(i)));
float note = i == 0 ? 440.0 : (i == 1 ? 659.3 : 880.0);
float tone = sin(tau * note * t) / float(i + 2);
shimmer += tone * float2(raysL, raysR) * swell;
}
shimmer *= 0.085;
// The crackles: in each 1/30 of a second, a chance that grows with the
// swell of a short burst of noise, dying away in about 10 ms, panned at
// random.
float window = t * 30.0;
uint w = uint(window);
float age = fract(window) / 30.0;
float hit = step(hashU(w * 13u + 7u), 0.08 * swell * swell);
float pan = hashU(w * 29u + 3u);
float noise = hashU(uint(t * 44100.0)) * 2.0 - 1.0;
float crack = hit * exp(-age * 100.0) * noise;
float2 crackle = crack * float2(1.0 - pan, pan) * 0.25;
// A wind under it all, very low.
float2 wind = float2(noise1(t, 160.0, 3u), noise1(t, 170.0, 5u)) * 0.04;
float2 s = drone + shimmer + crackle + wind;
return s * smoothstep(0.0, 0.3, t) * p.volume;
}24 lines from stage 4 that are gone
float3 starField(float3 rd, float time, float amount, float pixel) {
float az = atan2(rd.x, rd.z), el = asin(clamp(rd.y, -1.0, 1.0));
float2 g = float2(az, el) * 90.0;
float2 cell = floor(g);
float h = hash21(cell);
if (h < 1.0 - 0.07 * amount) return float3(0.0);
float2 at = 0.2 + 0.6 * float2(hash21(cell + 3.1), hash21(cell + 7.7));
float2 d = (fract(g) - at) * float2(cos(el), 1.0) / 90.0;
float spot = exp(-dot(d, d) / (pixel * pixel * 0.8));
float rate = 3.0 + 5.0 * hash21(cell + 1.3);
float twinkle = 0.65 + 0.35 * sin(time * rate + h * 40.0);
float bright = amount * (0.3 + 1.6 * pow(hash21(cell + 5.5), 4.0));
float3 tint = mix(float3(1.0, 0.82, 0.62), float3(0.8, 0.88, 1.0),
hash21(cell + 9.2));
return tint * spot * bright * twinkle * smoothstep(0.0, 0.25, rd.y);
}
⋯
float2 xz = rd.xz * ((1.0 + f * top) / rd.y);
⋯
col += kiln(0.95 - 0.75 * f) * c * rise * fade;
⋯
float3 col = mix(float3(0.035, 0.03, 0.05), float3(0.008, 0.007, 0.014),
smoothstep(0.0, 0.5, rd.y));
col += starField(rd, u.time, p.stars, pixel);
col += aurora(rd, time, p, jitter);
if (rd.y < 0.0) col = float3(0.01);
return float4(1.0 - exp(-col * 1.3), 1.0);The sound
The sound swells with the same surges as the picture: swell is storm times a mix of a floor and surge of the same clock. Under everything is a drone in A, on A1, E2 and A2, each note about a tenth of a hertz apart in the two ears so it slowly beats, louder and brighter (more overtones from voice) as the aurora swells. Over it are three glassy high tones, A4, E5 and A5, each as loud in each ear as noise that runs at the speed of the picture's rays, one sample of it for the left and one for the right, so the shimmer wanders between the ears. In a strong surge, faint crackles come: in each thirtieth of a second there is a chance, growing with the square of the swell, of a burst of noise that dies away in about 10 ms, panned at random. A very low wind of slow noise sits under it all. It fades in over the first 0.3 s. At the defaults the peak is about -8.4 dBFS and the average (RMS) about -20.2 dBFS.
New in Ray Kiln
Gamepad steering. In the playground and in Run it live on episode pages, a gamepad now steers the pointer a shader reads (u.mouse), as the arrow keys have since episode 11. Its left stick glides the pointer across the picture, as fast as the stick is pushed: half the picture a second at the rim, and nothing within a small dead zone in the middle, so a stick that rests a little off centre doesn't drift. Its D-pad acts as the arrow keys. A page has only one picture that steers, so the gamepad needs no click on it first; browsers only show a gamepad to a page after one of its buttons has been pressed, so press any button once. While the gamepad moves the pointer, the ring shows where it is.
Aurora is made for it: the pointer turns the view, so open it in the playground, connect a gamepad, press a button, and look around the sky with the left stick, left and right along the ridge, up into the curtains overhead, or down into the lake.
Try this
- Set
greento 1 for the colours of a real aurora, green with a red top and a pink fringe; anything between blends the two. - Set
stormto 1: every curtain is lit along most of its length, and the sound swells and crackles. At 0 the sky is only stars. - Set
heightto 1 for curtains that tower over the ridge, or 0 for a low band of light. - Set
speedto 0 and the sky holds still: the curtains, the surges, the ripples and the stars' twinkling all run on the same clock. - In
aurora, changeconst int layers = 48;to8: the jitter can no longer hide the steps, and the curtains turn to grain. - In
curtain, change the5.0ofk > 5.0to1.0: only the two nearest curtains are left, high in the sky. - In
shade, change-(0.18 + steer.y * 0.9)to-(0.9 + steer.y * 0.9)to look almost straight up, where the curtains overhead fold around you.
Parameters
Pick a parameter and drag the slider. Each frame is the same moment of the shader, with only that one setting changed.
| Parameter | Default | Range | What it does |
|---|---|---|---|
speed | 1.0 | 0.0 to 3.0 | How fast the curtains fold, drift and surge |
storm | 0.6 | 0.0 to 1.0 | How active the aurora is: how many curtains are lit, and how brightly |
height | 0.5 | 0.0 to 1.0 | How far up the curtains reach before they fade |
green | 0.0 | 0.0 to 1.0 | The curtains' colours, from the kiln's gold and ember to oxygen's green and red |
stars | 0.6 | 0.0 to 1.0 | How many stars, and how bright |
volume | 0.8 | 0.0 to 1.0 | Loudness of the sound |
More from this shader
The same shader 8 more ways: each from a different moment, with different settings, one after another. Each chapter below says which settings moved most, then lists them all.
Wallpapers
A still from the shader, rendered at full size for a desktop or a phone screen. Free to use as your wallpaper.
Clips to post
Fifteen seconds of the shader with its sound, sized for posting: square for feeds, vertical for stories and short videos. Free to post, ideally with a link to this page.
Made from this episode
The full source
This is the whole episode: the picture, the parameters and the sound.
episode-24-aurora.metal
// Ray Kiln · Episode 24 · Aurora
//
// @episode 24 Aurora
// @length 60
// @variation 30
// @variations 1,2,3,5,6,8,9,11
// @still 14.0
// @teaches Thin noise curtains marched through, stars
// @category Fragment
// @tags volume rendering, fractal noise, stars, aurora, reflection, procedural sound
// @final Aurora | The curtains over a ridge and a still lake: they fold and drift, brighten in surges, and the lake gives them back.
//
// The northern lights over a mountain ridge, mirrored in a lake. An aurora is light hanging in the air: tall, thin
// curtains whose lower edge is sharp and whose top fades away. Seen from the ground, each curtain is a line on a high
// layer of the sky, folded by noise, and the same line on every layer above it. So the ray from the eye is marched up
// through the layers, and at each one it asks how near it is to a curtain's line: on the same line at every height,
// the light stacks into a wall. Stars sit behind it on a grid of directions, the ridge is fractal noise against the
// sky, and the lake is the same sky seen upside down, rippled. The pointer turns the view: in the playground, move it,
// steer with the arrow keys, or with a gamepad's stick.
// Each @param line below becomes a slider in the host app and a row in the page's parameters table:
// // @param name default min max description
// @param speed 1.0 0.0 3.0 How fast the curtains fold, drift and surge
// @param storm 0.6 0.0 1.0 How active the aurora is: how many curtains are lit, and how brightly
// @param height 0.5 0.0 1.0 How far up the curtains reach before they fade
// @param green 0.0 0.0 1.0 The curtains' colours, from the kiln's gold and ember to oxygen's green and red
// @param stars 0.6 0.0 1.0 How many stars, and how bright
// @param volume 0.8 0.0 1.0 Loudness of the sound
// ---- Noise ------------------------------------------------------------------
float hash21(float2 p) {
p = fract(p * float2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
// Value noise: random values on a grid, blended smoothly between the grid
// points.
float vnoise(float2 p) {
float2 i = floor(p), f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash21(i), b = hash21(i + float2(1, 0));
float c = hash21(i + float2(0, 1)), d = hash21(i + float2(1, 1));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
// Fractal noise: several octaves of value noise, each finer and fainter than
// the last.
float fbm(float2 p) {
float v = 0.0, a = 0.5;
for (int i = 0; i < 5; i++) {
v += a * vnoise(p);
p = p * 2.03 + float2(17.1, 9.2);
a *= 0.5;
}
return v;
}
float2 rot(float2 p, float a) {
float c = cos(a), s = sin(a);
return float2(c * p.x - s * p.y, s * p.x + c * p.y);
}
// The Ray Kiln palette: near-black, ember, orange, gold, white-hot.
float3 kiln(float t) {
float3 c0 = float3(0.020, 0.012, 0.020);
float3 c1 = float3(0.280, 0.040, 0.030);
float3 c2 = float3(0.880, 0.260, 0.050);
float3 c3 = float3(1.000, 0.680, 0.200);
float3 c4 = float3(1.000, 0.970, 0.840);
t = clamp(t, 0.0, 1.0);
float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
c = mix(c, c2, smoothstep(0.22, 0.50, t));
c = mix(c, c3, smoothstep(0.50, 0.76, t));
return mix(c, c4, smoothstep(0.76, 1.00, t));
}
// ---- The curtains -----------------------------------------------------------
// The aurora's surges: a slow swell from 0 to 1, its own noise over time, that
// lights more curtains and brightens them. The sound swells with it.
float surge(float time) {
return smoothstep(0.2, 0.8, vnoise(float2(time * 0.09, 3.7)));
}
// How much curtain there is at a point xz on one layer of the sky. The
// curtains are lines running across the view, one behind another, 6.4 apart
// on layers whose lowest is 1 up; each is folded toward and away from us by
// noise that drifts along it, and the six nearest are drawn. Near its line a
// curtain is bright, and thin: its light falls away fast across it. Its rays
// are fine streaks along the line, the same on every layer, so they stand
// upright. `edge` gives its lower edge here, as a share of the way up.
float curtain(float2 xz, float time, float storm, thread float& edge) {
float2 p = xz * 0.25;
float fold = 3.2 * vnoise(float2(p.x * 0.3 + time * 0.05, 1.0))
+ 1.2 * vnoise(float2(p.x * 0.9 - time * 0.09, 5.0))
+ 0.5 * vnoise(float2(p.x * 2.3 + time * 0.2, 9.0));
float z = (p.y + fold) / 1.6;
float k = floor(z);
float across = (fract(z) - 0.5) * 1.6;
// Only the six curtains from 0 to 5 bands away; the rest is empty sky.
if (k < 0.0 || k > 5.0) { edge = 1.0; return 0.0; }
// Each curtain lit along part of its length, by a hash of its number and
// slow noise along it: more of it the higher storm, and more in a surge.
float lit = storm * (0.55 + 0.6 * surge(time + k * 3.0));
float along = 0.4 * hash21(float2(k, 4.2))
+ 0.6 * vnoise(float2(p.x * 0.3 + k * 13.0, time * 0.04));
float on = smoothstep(1.0 - lit, 1.2 - lit, along);
// A far curtain is drawn a little wider, so the few layers that meet it
// still find it.
float width = 0.04 + 0.04 * hash21(float2(k, 8.9));
width *= 1.0 + 0.04 * length(xz);
float sheet = exp(-across * across / (width * width));
float rays = 0.45 + 0.9 * vnoise(float2(p.x * 18.0 + time * 0.7, k * 7.0));
rays *= 0.6 + 0.6 * vnoise(float2(p.x * 3.0 - time * 0.3, k * 3.0));
edge = 0.12 * vnoise(float2(p.x * 1.7 + time * 0.1, k));
return sheet * on * rays;
}
// The colour a share f of the way up: the kiln's, white-hot to ember, or
// oxygen's, pink at the edge, green, then deep red, blended by `green`.
float3 curtainColour(float f, float green) {
float3 kilnCol = kiln(0.95 - 0.75 * f);
float3 oxygen = mix(float3(0.15, 1.0, 0.42), float3(0.85, 0.08, 0.22),
smoothstep(0.25, 0.8, f));
oxygen = mix(float3(0.95, 0.35, 0.6), oxygen, smoothstep(0.0, 0.06, f));
return mix(kilnCol, oxygen, green);
}
// The aurora along a ray going up: marched through 48 layers of sky from
// height 1 to the curtains' top, each layer adding the light of the curtains
// it meets. `jitter` moves this pixel's layers up by a share of one step, so
// the steps blur rather than show as bands.
float3 aurora(float3 rd, float time, constant Params& p, float jitter) {
if (rd.y <= 0.0) return float3(0.0);
const int layers = 48;
float top = 0.8 + 2.4 * p.height;
float3 col = float3(0.0);
for (int i = 0; i < layers; i++) {
float f = (float(i) + jitter) / float(layers);
float h = 1.0 + f * top;
float2 xz = rd.xz * (h / rd.y);
float edge;
float c = curtain(xz, time, p.storm, edge);
// Sharp at the lower edge, fading with height, fainter far away.
float rise = smoothstep(edge, edge + 0.04, f);
float fade = exp(-f * 3.0) * exp(-length(xz) * 0.06);
col += curtainColour(f, p.green) * c * rise * fade;
}
return col * (30.0 / float(layers)) * smoothstep(0.02, 0.2, rd.y);
}
// ---- The sky, the stars, the ridge ------------------------------------------
// The stars, as in the first step: a few cells of 1/90 radian hold a soft,
// twinkling dot about a pixel wide (`pixel` is a pixel's angle).
float3 starField(float3 rd, float time, float amount, float pixel) {
float az = atan2(rd.x, rd.z), el = asin(clamp(rd.y, -1.0, 1.0));
float2 g = float2(az, el) * 90.0;
float2 cell = floor(g);
float h = hash21(cell);
if (h < 1.0 - 0.07 * amount) return float3(0.0);
float2 at = 0.2 + 0.6 * float2(hash21(cell + 3.1), hash21(cell + 7.7));
float2 d = (fract(g) - at) * float2(cos(el), 1.0) / 90.0;
float spot = exp(-dot(d, d) / (pixel * pixel * 0.8));
float rate = 3.0 + 5.0 * hash21(cell + 1.3);
float twinkle = 0.65 + 0.35 * sin(time * rate + h * 40.0);
float bright = amount * (0.3 + 1.6 * pow(hash21(cell + 5.5), 4.0));
float3 tint = mix(float3(1.0, 0.82, 0.62), float3(0.8, 0.88, 1.0),
hash21(cell + 9.2));
return tint * spot * bright * twinkle * smoothstep(0.0, 0.25, rd.y);
}
// The ridge's height above the horizon, as an angle, for each direction round:
// fractal noise, higher where a slow wave lifts it.
float ridge(float az) {
float lift = smoothstep(-0.4, 1.4, sin(az * 1.3 + 0.5) + 1.0);
return 0.02 + 0.12 * fbm(float2(az * 2.2, 1.7)) * lift;
}
// Everything a ray sees above the horizon: the night sky, a little lighter
// low down, the stars, the curtains, and the ridge in front of them, a black
// shape with a little of the aurora's light on its snow.
float3 skyView(float3 rd, float time, constant Params& p, float jitter,
float pixel) {
float3 col = mix(float3(0.035, 0.03, 0.05), float3(0.008, 0.007, 0.014),
smoothstep(0.0, 0.5, rd.y));
col += starField(rd, time, p.stars, pixel);
col += aurora(rd, time, p, jitter);
float az = atan2(rd.x, rd.z);
float r = ridge(az);
if (rd.y < r) {
float snow = fbm(float2(az * 40.0, rd.y * 60.0));
float3 glint = curtainColour(0.1, p.green) * 0.03 * p.storm * snow;
float3 rock = float3(0.012, 0.011, 0.016) + glint;
col = mix(col, rock, smoothstep(r, r - pixel * 1.5, rd.y));
}
return col;
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
float2 q = (uv - 0.5) * float2(aspect, 1.0);
float time = u.time * p.speed;
float pixel = 1.0 / (1.1 * u.resolution.y);
// The view: looking a little up, turned and tipped by the pointer's offset
// from the middle of the picture (in an export it is the middle).
float2 steer = u.mouse / u.resolution - 0.5;
float3 rd = normalize(float3(q, 1.1));
rd.yz = rot(rd.yz, -(0.18 + steer.y * 0.9));
rd.xz = rot(rd.xz, steer.x * 2.4);
// A different share of a layer for each pixel and frame (aurora above).
float jitter = hash21(uv * u.resolution + fract(u.time * 7.0) * 61.0);
float3 col;
if (rd.y >= 0.0) {
col = skyView(rd, time, p, jitter, pixel);
} else {
// The lake: the sky mirrored, the ray tipped up as much as it pointed
// down, rippled by noise on the water that drifts with the clock, and
// darker, since water gives back only part of the light.
float2 water = rd.xz / -rd.y;
float2 at = water * float2(1.0, 4.0) + float2(0.0, time * 0.4);
float ripple = vnoise(at) - 0.5;
float3 up = normalize(float3(rd.x + ripple * 0.006,
-rd.y + ripple * 0.004, rd.z));
col = skyView(up, time, p, jitter, pixel) * float3(0.5, 0.55, 0.6);
}
// Bright light goes to white rather than clipping its colour.
col = 1.0 - exp(-col * 1.3);
return float4(col, 1.0);
}
// ---- Sound ------------------------------------------------------------------
// A low drone in A, a fifth and an octave, swelling and brightening with the
// aurora's surges; over it three glassy high tones whose loudness in each ear
// runs like the curtains' rays, so the shimmer moves between the ears; and in
// a surge, faint crackles.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// Noise for sound: random values joined `rate` times a second.
float noise1(float t, float rate, uint seed) {
float x = t * rate;
float i = floor(x), f = fract(x);
f = f * f * (3.0 - 2.0 * f);
uint k = uint(i) + seed * 7919u;
return mix(hashU(k), hashU(k + 1u), f) * 2.0 - 1.0;
}
// A soft tone with some of its overtones: `bright` from 0 (a pure sine) to 1
// (a reedy, fuller sound).
float voice(float ph, float bright) {
const float tau = 6.2831853;
float over = 0.45 * sin(2.0 * tau * ph) + 0.25 * sin(3.0 * tau * ph)
+ 0.12 * sin(4.0 * tau * ph);
return sin(tau * ph) + bright * over;
}
float2 sound(float t, constant Params& p) {
const float tau = 6.2831853;
float time = t * p.speed;
float swell = p.storm * (0.35 + 0.65 * surge(time));
// The drone: A1, E2 and A2, each a few cents apart in the two ears.
float bright = 0.15 + 0.6 * swell;
float2 a1 = float2(voice(55.0 * t, bright), voice(55.08 * t, bright));
float2 e2 = float2(voice(82.45 * t, bright), voice(82.35 * t, bright));
float2 a2 = float2(voice(110.1 * t, bright), voice(109.95 * t, bright));
float2 drone = a1 * 0.5 + e2 * 0.3 + a2 * 0.2;
drone *= 0.2 + 0.15 * swell;
// The shimmer: A4, E5 and A5, each as loud in each ear as noise that runs
// at the speed of the picture's rays, one sample to the left, one to the
// right.
float2 shimmer = float2(0.0);
for (int i = 0; i < 3; i++) {
float side = float(i) - 1.0;
float run = side * 0.7 + time * 0.7;
float raysL = vnoise(float2(run - 3.0, 14.0 + float(i)));
float raysR = vnoise(float2(run + 3.0, 21.0 + float(i)));
float note = i == 0 ? 440.0 : (i == 1 ? 659.3 : 880.0);
float tone = sin(tau * note * t) / float(i + 2);
shimmer += tone * float2(raysL, raysR) * swell;
}
shimmer *= 0.085;
// The crackles: in each 1/30 of a second, a chance that grows with the
// swell of a short burst of noise, dying away in about 10 ms, panned at
// random.
float window = t * 30.0;
uint w = uint(window);
float age = fract(window) / 30.0;
float hit = step(hashU(w * 13u + 7u), 0.08 * swell * swell);
float pan = hashU(w * 29u + 3u);
float noise = hashU(uint(t * 44100.0)) * 2.0 - 1.0;
float crack = hit * exp(-age * 100.0) * noise;
float2 crackle = crack * float2(1.0 - pan, pan) * 0.25;
// A wind under it all, very low.
float2 wind = float2(noise1(t, 160.0, 3u), noise1(t, 170.0, 5u)) * 0.04;
float2 s = drone + shimmer + crackle + wind;
return s * smoothstep(0.0, 0.3, t) * p.volume;
}Running it in your own project
Every Ray Kiln shader is the same shape. The standalone download above is the shader with the small wrapper around it that the Ray Kiln host adds, so it compiles with the ordinary Metal compiler and runs in your own app. It defines three entry points: rk_vertex (a full-screen triangle), rk_fragment (calls the shader's shade) and, for shaders with sound, the compute kernel rk_sound.
To draw it, pass the uniforms at fragment buffer 0 and the parameter values at buffer 1, in the order they are declared:
Drawing it, in Swift
struct Uniforms {
var resolution: SIMD2<Float>
var time: Float
var timeDelta: Float
var mouse: SIMD2<Float>
var frame: UInt32
var pad: UInt32 = 0
}
let library = try device.makeLibrary(source: standaloneSource, options: nil)
let descriptor = MTLRenderPipelineDescriptor()
descriptor.vertexFunction = library.makeFunction(name: "rk_vertex")
descriptor.fragmentFunction = library.makeFunction(name: "rk_fragment")
descriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
let pipeline = try device.makeRenderPipelineState(descriptor: descriptor)
// Each frame, inside a render pass:
var uniforms = Uniforms(resolution: size, time: time, timeDelta: dt, mouse: mouse, frame: frame)
var params: [Float] = [1.0, 0.6, 0.5, 0.0, 0.6, 0.8] // the @param defaults, in the order they are declared
encoder.setRenderPipelineState(pipeline)
encoder.setFragmentBytes(&uniforms, length: MemoryLayout<Uniforms>.stride, index: 0)
encoder.setFragmentBytes(¶ms, length: params.count * MemoryLayout<Float>.stride, index: 1)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)Time is in seconds, mouse is in pixels from the bottom left, and uv in the shader runs 0 to 1 with (0, 0) at the bottom left. Write the output as sRGB: the shader's numbers go to the screen as they are.
More in Atmosphere
Press ? for keyboard shortcuts.
