Lightning
A storm at night over a field. Each bolt is a straight line from the cloud to the ground, split again and again by midpoint displacement until it is as rough as lightning, with branches grown the same way. A faint leader comes down, the return stroke lights the bolt and the clouds, and the thunder leaves the strike as a ring on the ground that reaches you, and your ears, seconds after the flash.
Teaches Branching bolts by midpoint displacement, glow, thunder that arrives late
Builds on Flowing Color Field (hash21, value noise, fractal noise), Cracked Clay (integer hash, sound noise), Murmuration (segment distance), Rain on Glass (thunder), Pulse Synth
Watch
The idea
Lightning looks complicated, but one rule draws it. Take a straight line from the cloud to the ground and push its middle sideways by a random amount. Now there are two lines: push the middle of each sideways, by less. Then the four, the eight, and so on. That is midpoint displacement, one of the oldest ways to make a fractal: every level adds the same kind of detail at half the size, so the line is rough at every scale and never stops being one line. Seven levels give 128 pieces, which is plenty at 1080p.
The rest is timing. A strike comes in steps: a faint leader feels its way down, the return stroke lights the channel, and the bolt often strikes again down the same path a tenth of a second later. And sound is slow. Light from the strike reaches you at once, but the thunder takes about three seconds for every kilometre, which is why you count after a flash.
It is built in five steps, and each step is a shader you can run on its own.
Stage 1 · Split the line
boltOffset is the whole rule, written so that any point can be found without building the line first. Level 0 is one tent: zero at both ends of the bolt, rising to a random height in the middle. Level 1 is two tents, one on each half; level l is 2^l tents. Each level's heights are drawn from a hash of the bolt's seed, the level and the tent, and each level is jag times as tall as the one before, starting at a fifth of the bolt's length. Adding the tents up gives exactly what pushing the midpoints one level at a time would: a point a fraction s along the line moves sideways by the sum of the tents over it. boltPoint turns that into a position.
To draw it, boltDistance projects the pixel onto the straight line to find its s, and measures the distance to the six pieces around that place with segment, the same distance-to-a-segment as in Pulse Synth. A pixel further from the straight line than all the levels together could push it, 0.2 / (1 − jag) of the length, skips the work. The bolt is solid within a pixel and a half and glows beyond. The levels slider stops the splitting early: at 0 it is the straight line, faint behind it; at 1 a single kink; at 7 lightning. A new bolt comes every three seconds.
stage-1-split.metal
// Stage 1 · Split the line
// A bolt starts as a straight line from the cloud to the ground. Push its
// middle sideways, then the middle of each half, then of each quarter, each
// push smaller than the last. Seven levels make 128 pieces: rough, and still
// one line. A new bolt every three seconds.
// @param levels 7 0 7 How many times the line is split
// @param jag 0.58 0.4 0.72 How rough the bolt is: each level of displacement keeps this much of the last
// @param volume 0.8 0 1 Loudness of the sound
// How many times the line is split, at most: 2^7 = 128 pieces.
constant int LEVELS = 7;
constant float PIECES = 128.0;
// The kiln palette from Flowing Color Field.
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));
}
// Integer hash, so noise stays exact for minutes of sound.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// Distance from point a to the segment from b to c.
float segment(float2 a, float2 b, float2 c) {
float2 ba = a - b, cb = c - b;
float h = clamp(dot(ba, cb) / dot(cb, cb), 0.0, 1.0);
return length(ba - cb * h);
}
// How far sideways the point a fraction s along a bolt is pushed: one tent
// over the whole line, two over its halves, 2^l at level l, each level jag
// times as tall as the last. Rough, and still one line.
float boltOffset(float s, uint seed, float jag, float levels) {
float off = 0.0, amp = 0.2, n = 1.0;
for (int l = 0; l < LEVELS; l++) {
if (float(l) >= levels) break;
float u = clamp(s, 0.0, 1.0) * n;
float j = min(floor(u), n - 1.0), f = u - j;
uint k = seed * 2048u + uint(l) * 256u + uint(j);
off += (hashU(k) * 2.0 - 1.0) * amp * (1.0 - abs(2.0 * f - 1.0));
amp *= jag;
n *= 2.0;
}
return off;
}
// The point a fraction s along the bolt from a to b, pushed sideways.
float2 boltPoint(float s, float2 a, float2 b, uint seed, float jag,
float levels) {
float2 d = b - a;
return a + d * s + float2(-d.y, d.x) * boltOffset(s, seed, jag, levels);
}
// Distance from p to the bolt from a to b.
// Only the few pieces about p's place along the line can be nearest.
float boltDistance(float2 p, float2 a, float2 b, uint seed, float jag,
float levels) {
float2 d = b - a;
float len2 = dot(d, d);
float s = dot(p - a, d) / len2;
float side = abs(dot(p - a, float2(-d.y, d.x))) / len2;
if (s < -0.1 || s > 1.1 || side > 0.2 / (1.0 - jag))
return 1e3;
float i0 = floor(clamp(s, 0.0, 1.0) * PIECES);
float best = 1e3;
float2 last = boltPoint((i0 - 3.0) / PIECES, a, b, seed, jag,
levels);
for (int i = -2; i <= 3; i++) {
float s1 = (i0 + float(i)) / PIECES;
float2 next = boltPoint(s1, a, b, seed, jag, levels);
if (s1 > 0.0)
best = min(best, segment(p, last, next));
last = next;
}
return best;
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
float2 q = float2(uv.x * aspect, uv.y);
float px = 1.0 / u.resolution.y;
// A new bolt every three seconds, from the top to the ground.
uint seed = uint(floor(u.time / 3.0)) + 1u;
float2 top = float2(0.5 * aspect, 1.02);
float2 foot = float2(0.5 * aspect + 0.15, 0.08);
// The straight line it started as, faint.
float line = segment(q, top, foot);
float heat = 0.04 + 0.25 * (1.0 - smoothstep(0.0, px, line));
float d = boltDistance(q, top, foot, seed, p.jag, p.levels);
heat += 1.1 * (1.0 - smoothstep(0.0, 1.5 * px, d));
heat += 0.35 * exp(-d / 0.01);
return float4(kiln(heat), 1.0);
}
// Rain, for now.
// Noise made by joining random values `rate` times a second:
// a lower rate sounds darker.
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;
}
float2 sound(float t, constant Params& p) {
// The rain: noise with its lowest part taken away.
float2 rain = float2(noise1(t, 9000.0, 1u) - noise1(t, 900.0, 2u) * 0.6,
noise1(t, 9000.0, 4u) - noise1(t, 900.0, 5u) * 0.6);
// A fade in over the first 0.3 s, so it doesn't start with a click.
return rain * 0.09 * 0.8 * smoothstep(0.0, 0.3, t) * p.volume;
}Stage 2 · Branches
A bolt forks, and each fork is a small bolt of its own. branchDistance picks, from a hash of the bolt and the branch number, a place between 10% and 75% of the way down, a turn of 20 to 52 degrees to one side or the other, and a length of 15% to 40% of the bolt's, shorter the lower it starts. It starts at that point of the jagged channel, boltPoint, and is split by the same boltDistance with a seed of its own, so it is as rough as the channel at its own size.
Glow is light spreading into the air: an exp of the distance, as wide as the glow slider says, thinner for the branches. Glows add, so where branches cross the light builds up.
stage-2-branches.metal32 new lines since stage 1
// Stage 2 · Branches
// A real bolt forks. Each branch leaves the channel at a random point along
// it, turned 20 to 50 degrees from its way down, shorter the lower it starts,
// and is split the same way, a smaller bolt of its own. Glow falls away from
// every line, so the air around it is lit.
// @param jag 0.58 0.4 0.72 How rough the bolts are: each level of displacement keeps this much of the last
// @param branches 5 0 8 Branches on the bolt
// @param glow 1 0 2 How far the light spreads from the bolt
// @param volume 0.8 0 1 Loudness of the sound
// How many times the line is split: 2^7 = 128 pieces.
constant int LEVELS = 7;
constant float PIECES = 128.0;
// The kiln palette from Flowing Color Field.
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));
}
// Integer hash, so noise stays exact for minutes of sound.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// Distance from point a to the segment from b to c.
float segment(float2 a, float2 b, float2 c) {
float2 ba = a - b, cb = c - b;
float h = clamp(dot(ba, cb) / dot(cb, cb), 0.0, 1.0);
return length(ba - cb * h);
}
// How far sideways the point a fraction s along a bolt is pushed: one tent
// over the whole line, two over its halves, 2^l at level l, each level jag
// times as tall as the last. Rough, and still one line.
float boltOffset(float s, uint seed, float jag) {
float off = 0.0, amp = 0.2, n = 1.0;
for (int l = 0; l < LEVELS; l++) {
float u = clamp(s, 0.0, 1.0) * n;
float j = min(floor(u), n - 1.0), f = u - j;
uint k = seed * 2048u + uint(l) * 256u + uint(j);
off += (hashU(k) * 2.0 - 1.0) * amp * (1.0 - abs(2.0 * f - 1.0));
amp *= jag;
n *= 2.0;
}
return off;
}
// The point a fraction s along the bolt from a to b, pushed sideways.
float2 boltPoint(float s, float2 a, float2 b, uint seed, float jag) {
float2 d = b - a;
return a + d * s + float2(-d.y, d.x) * boltOffset(s, seed, jag);
}
// Distance from p to the bolt from a to b, as far as the fraction `upto`.
// Only the few pieces about p's place along the line can be nearest.
float boltDistance(float2 p, float2 a, float2 b, uint seed, float jag,
float upto) {
float2 d = b - a;
float len2 = dot(d, d);
float s = dot(p - a, d) / len2;
float side = abs(dot(p - a, float2(-d.y, d.x))) / len2;
if (upto <= 0.0 || s < -0.1 || s > 1.1 || side > 0.2 / (1.0 - jag))
return 1e3;
float i0 = floor(clamp(s, 0.0, 1.0) * PIECES);
float best = 1e3;
float2 last = boltPoint((i0 - 3.0) / PIECES, a, b, seed, jag);
for (int i = -2; i <= 3; i++) {
float s1 = (i0 + float(i)) / PIECES;
float2 next = boltPoint(s1, a, b, seed, jag);
if (s1 > 0.0 && s1 - 1.0 / PIECES < upto)
best = min(best, segment(p, last, next));
last = next;
}
return best;
}
// Branches leave the channel at random points and angles, shorter lower
// down. Each is a bolt of its own, from where it leaves to where it ends.
float branchDistance(float2 q, float2 top, float2 foot, uint seed, int b,
float jag, float upto) {
uint bs = seed * 16u + uint(b);
float from = 0.1 + 0.65 * hashU(bs * 3u);
float turn = (0.35 + 0.55 * hashU(bs * 3u + 1u))
* (hashU(bs * 3u + 2u) < 0.5 ? -1.0 : 1.0);
float len = (0.15 + 0.25 * hashU(bs + 999u)) * (1.0 - 0.5 * from);
float2 a = boltPoint(from, top, foot, seed, jag);
float2 axis = foot - top;
float c = cos(turn), sn = sin(turn);
float2 dir = float2(axis.x * c - axis.y * sn, axis.x * sn + axis.y * c);
float reach = (upto - from) / (len * c);
return boltDistance(q, a, a + dir * len, bs + 77u, jag, reach);
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
float2 q = float2(uv.x * aspect, uv.y);
float px = 1.0 / u.resolution.y;
uint seed = uint(floor(u.time / 3.0)) + 1u;
float2 top = float2(0.5 * aspect, 1.02);
float2 foot = float2(0.5 * aspect + 0.15, 0.08);
float reach = 0.012 * p.glow;
float d = boltDistance(q, top, foot, seed, p.jag, 1.0);
float heat = 0.04 + 1.1 * (1.0 - smoothstep(0.0, 1.5 * px, d));
heat += 0.4 * exp(-d / max(reach, px));
for (int b = 0; b < 8; b++) {
if (float(b) >= p.branches) break;
float bd = branchDistance(q, top, foot, seed, b, p.jag, 1.0);
heat += 0.6 * (1.0 - smoothstep(0.0, 1.2 * px, bd));
heat += 0.15 * exp(-bd / max(reach * 0.6, px));
}
return float4(kiln(heat), 1.0);
}
// Rain, for now.
// Noise made by joining random values `rate` times a second:
// a lower rate sounds darker.
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;
}
float2 sound(float t, constant Params& p) {
// The rain: noise with its lowest part taken away.
float2 rain = float2(noise1(t, 9000.0, 1u) - noise1(t, 900.0, 2u) * 0.6,
noise1(t, 9000.0, 4u) - noise1(t, 900.0, 5u) * 0.6);
// A fade in over the first 0.3 s, so it doesn't start with a click.
return rain * 0.09 * 0.8 * smoothstep(0.0, 0.3, t) * p.volume;
}16 lines from stage 1 that are gone
float boltOffset(float s, uint seed, float jag, float levels) {
⋯
if (float(l) >= levels) break;
⋯
float2 boltPoint(float s, float2 a, float2 b, uint seed, float jag,
float levels) {
⋯
return a + d * s + float2(-d.y, d.x) * boltOffset(s, seed, jag, levels);
⋯
float levels) {
⋯
if (s < -0.1 || s > 1.1 || side > 0.2 / (1.0 - jag))
⋯
float2 last = boltPoint((i0 - 3.0) / PIECES, a, b, seed, jag,
levels);
⋯
float2 next = boltPoint(s1, a, b, seed, jag, levels);
if (s1 > 0.0)
⋯
float line = segment(q, top, foot);
float heat = 0.04 + 0.25 * (1.0 - smoothstep(0.0, px, line));
float d = boltDistance(q, top, foot, seed, p.jag, p.levels);
heat += 1.1 * (1.0 - smoothstep(0.0, 1.5 * px, d));
heat += 0.35 * exp(-d / 0.01);Stage 3 · The strike
Now the strikes come by themselves. As in Rain on Glass, the clock is cut into windows, every seconds long, and each window's strike is chosen by a hash of its number: its moment, somewhere in the first half of the window, its place across the screen and its bolt's seed. Picture and sound work it out separately and always agree. Since a strike is in the first half of its window, two are never closer than half a window apart, a second at the shortest.
Each strike has a timetable. For 0.2 s the leader comes down: only the part of the bolt above x / 0.2 is drawn, dimly, and branches appear as it reaches them. Then the return stroke: strokes is a sharp pulse that fades with exp(−14 t), and each restrike adds another 0.1 s later, at 0.8 times the last. The branches fade sooner than the channel. The clouds are lit by skyLight, which rises in 0.04 s and falls smoothly with exp(−2.2 t): once a strike, however many times the bolt flickers. That is deliberate. A picture that flashes more than three times a second can cause seizures in people with photosensitive epilepsy; this sky never does, and the flicker is kept to the bolt, a thin line, too small to count as a flash.
stage-3-strike.metal117 new lines since stage 2
// Stage 3 · The strike
// Time is cut into windows, as in Rain on Glass, with a strike at a random
// moment in the first half of each. A faint leader comes down in 0.2 s; the
// return stroke lights it white and lights the clouds, once, smoothly; then
// the bolt alone flickers as it strikes again down the same channel.
// @param every 5 2 12 Seconds between strikes, on average
// @param jag 0.58 0.4 0.72 How rough the bolts are: each level of displacement keeps this much of the last
// @param branches 5 0 8 Branches on each bolt
// @param restrikes 2 0 3 Strokes after the first down the same channel, in the bolt alone
// @param glow 1 0 2 How far the light spreads from the bolt
// @param volume 0.8 0 1 Loudness of the sound
// How many times the line is split: 2^7 = 128 pieces.
constant int LEVELS = 7;
constant float PIECES = 128.0;
// Where the ground starts.
constant float GROUND = 0.12;
// The kiln palette from Flowing Color Field.
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 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.
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;
}
// Integer hash, so noise stays exact for minutes of sound.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// Distance from point a to the segment from b to c.
float segment(float2 a, float2 b, float2 c) {
float2 ba = a - b, cb = c - b;
float h = clamp(dot(ba, cb) / dot(cb, cb), 0.0, 1.0);
return length(ba - cb * h);
}
// How far sideways the point a fraction s along a bolt is pushed: one tent
// over the whole line, two over its halves, 2^l at level l, each level jag
// times as tall as the last. Rough, and still one line.
float boltOffset(float s, uint seed, float jag) {
float off = 0.0, amp = 0.2, n = 1.0;
for (int l = 0; l < LEVELS; l++) {
float u = clamp(s, 0.0, 1.0) * n;
float j = min(floor(u), n - 1.0), f = u - j;
uint k = seed * 2048u + uint(l) * 256u + uint(j);
off += (hashU(k) * 2.0 - 1.0) * amp * (1.0 - abs(2.0 * f - 1.0));
amp *= jag;
n *= 2.0;
}
return off;
}
// The point a fraction s along the bolt from a to b, pushed sideways.
float2 boltPoint(float s, float2 a, float2 b, uint seed, float jag) {
float2 d = b - a;
return a + d * s + float2(-d.y, d.x) * boltOffset(s, seed, jag);
}
// Distance from p to the bolt from a to b, as far as the fraction `upto`.
// Only the few pieces about p's place along the line can be nearest.
float boltDistance(float2 p, float2 a, float2 b, uint seed, float jag,
float upto) {
float2 d = b - a;
float len2 = dot(d, d);
float s = dot(p - a, d) / len2;
float side = abs(dot(p - a, float2(-d.y, d.x))) / len2;
if (upto <= 0.0 || s < -0.1 || s > 1.1 || side > 0.2 / (1.0 - jag))
return 1e3;
float i0 = floor(clamp(s, 0.0, 1.0) * PIECES);
float best = 1e3;
float2 last = boltPoint((i0 - 3.0) / PIECES, a, b, seed, jag);
for (int i = -2; i <= 3; i++) {
float s1 = (i0 + float(i)) / PIECES;
float2 next = boltPoint(s1, a, b, seed, jag);
if (s1 > 0.0 && s1 - 1.0 / PIECES < upto)
best = min(best, segment(p, last, next));
last = next;
}
return best;
}
// Branches leave the channel at random points and angles, shorter lower
// down. Each is a bolt of its own, from where it leaves to where it ends.
float branchDistance(float2 q, float2 top, float2 foot, uint seed, int b,
float jag, float upto) {
uint bs = seed * 16u + uint(b);
float from = 0.1 + 0.65 * hashU(bs * 3u);
float turn = (0.35 + 0.55 * hashU(bs * 3u + 1u))
* (hashU(bs * 3u + 2u) < 0.5 ? -1.0 : 1.0);
float len = (0.15 + 0.25 * hashU(bs + 999u)) * (1.0 - 0.5 * from);
float2 a = boltPoint(from, top, foot, seed, jag);
float2 axis = foot - top;
float c = cos(turn), sn = sin(turn);
float2 dir = float2(axis.x * c - axis.y * sn, axis.x * sn + axis.y * c);
float reach = (upto - from) / (len * c);
return boltDistance(q, a, a + dir * len, bs + 77u, jag, reach);
}
// The strike in window k: when, where across the screen, and its seed.
struct Strike {
float at;
float x;
uint seed;
};
Strike strikeIn(float k, float aspect, constant Params& p) {
uint w = uint(k);
Strike s;
s.at = k * p.every + 0.5 * p.every * hashU(w * 7u + 1u);
s.x = (0.15 + 0.7 * hashU(w * 7u + 3u)) * aspect;
s.seed = w * 7u + 4u;
return s;
}
// The bolt's light `x` seconds into the strike. The leader takes 0.2 s to
// come down, faint; then the return stroke, and each restrike 0.1 s apart,
// is a sharp pulse that fades.
constant float LEADER = 0.2;
float strokes(float x, constant Params& p) {
if (x < LEADER) return 0.25 * step(0.0, x);
float light = 0.0;
for (int j = 0; j <= 3; j++) {
float y = x - LEADER - 0.1 * float(j);
if (float(j) <= p.restrikes + 0.5 && y >= 0.0)
light += pow(0.8, float(j)) * exp(-y * 14.0);
}
return light;
}
// The light thrown on the clouds and the ground: one rise and one smooth fall
// a strike, however often its bolt flickers. A flash that comes and goes more
// than three times a second can cause seizures; this one never does.
float skyLight(float x) {
float y = x - LEADER;
if (y < 0.0) return 0.0;
return smoothstep(0.0, 0.04, y) * exp(-y * 2.2);
}
// One bolt and its branches, as heat, for the strike `x` seconds old.
float boltHeat(float2 q, Strike s, float x, float px, constant Params& p) {
float2 top = float2(s.x - 0.1, 1.05);
float2 foot = float2(s.x, GROUND);
float down = clamp(x / LEADER, 0.0, 1.0);
float light = strokes(x, p);
float sky = skyLight(x);
float reach = 0.012 * p.glow;
float d = boltDistance(q, top, foot, s.seed, p.jag, down);
float heat = light * (1.0 - smoothstep(0.0, 1.5 * px, d)) * 1.3;
heat += (sky + 0.3 * step(x, LEADER) * down) * 0.5
* exp(-d / max(reach, px));
for (int b = 0; b < 8; b++) {
if (float(b) >= p.branches) break;
float bd = branchDistance(q, top, foot, s.seed, b, p.jag, down);
float fade = x < LEADER ? 0.7 : 0.45 * exp(-(x - LEADER) * 3.0);
heat += fade * (light + 0.3) * (1.0 - smoothstep(0.0, 1.2 * px, bd));
heat += fade * sky * 0.3 * exp(-bd / max(reach * 0.6, px));
}
return heat;
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float t = u.time;
float aspect = u.resolution.x / u.resolution.y;
float2 q = float2(uv.x * aspect, uv.y);
float px = 1.0 / u.resolution.y;
bool sky = q.y > GROUND;
// Clouds drifting across a dim sky, and dark ground.
float cloud = smoothstep(0.3, 0.8, fbm(q * float2(2.0, 4.0)
+ float2(t * 0.05, 0.0)));
float heat = sky ? 0.06 + 0.05 * cloud : 0.03;
float k = floor(t / p.every);
for (int i = 0; i < 2; i++) {
float kk = k - float(i);
if (kk < 0.0) break;
Strike s = strikeIn(kk, aspect, p);
float x = t - s.at;
if (x < 0.0) continue;
// The clouds light about the bolt's top, a little everywhere.
float far = length(q - float2(s.x, 1.0));
float lit = skyLight(x);
heat += sky ? lit * (0.1 + 0.6 * cloud) * (0.2 + exp(-far * 2.5))
: lit * 0.2 * exp(-abs(q.x - s.x) * 3.0);
if (x < 1.5) heat += boltHeat(q, s, x, px, p);
}
return float4(kiln(heat), 1.0);
}
// Rain, and each strike's thunder, three seconds after its flash: as if it
// struck a kilometre away, since sound covers one in about three seconds.
// Noise made by joining random values `rate` times a second:
// a lower rate sounds darker.
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;
}
// The thunder `x` seconds after it arrives, from `far` kilometres away.
float thunder(float t, float x, float far, uint seed) {
if (x <= 0.0) return 0.0;
float near = smoothstep(1.6, 0.4, far);
float crack = (noise1(t, 3500.0, seed) * 0.6
+ noise1(t, 1200.0, seed + 1u) * 0.5)
* exp(-x * 12.0) * smoothstep(0.0, 0.004, x) * near;
float env = (1.0 - exp(-x * 5.0)) * exp(-x / (0.9 + 0.6 * far))
* smoothstep(12.0, 9.0, x);
float swell = 0.55 + 0.45 * noise1(t, 2.5, seed + 2u);
float low = 1.0 - 0.15 * far;
float rumble = noise1(t, 50.0 * low, seed + 3u) * 0.6
+ noise1(t, 110.0 * low, seed + 4u) * 0.4
+ noise1(t, 260.0 * low, seed + 5u) * 0.25;
return (crack * 0.7 + rumble * env * swell) * 5.0 / (0.5 + far);
}
float2 sound(float t, constant Params& p) {
float aspect = 16.0 / 9.0;
float2 rain = float2(noise1(t, 9000.0, 1u) - noise1(t, 900.0, 2u) * 0.6,
noise1(t, 9000.0, 4u) - noise1(t, 900.0, 5u) * 0.6);
float2 sum = rain * 0.09;
float k = floor(t / p.every);
for (int i = 0; i < 8; i++) {
float kk = k - float(i);
if (kk < 0.0) break;
Strike s = strikeIn(kk, aspect, p);
float x = t - s.at - 3.0;
if (x <= 0.0 || x > 12.0) continue;
// Louder on the side it struck.
float pan = s.x / aspect;
float a = thunder(t, x, 1.0, s.seed);
sum += a * float2(1.2 - pan, 0.2 + pan);
}
// A fade in over the first 0.3 s, so the rain doesn't start with a click.
return tanh(sum * 1.2) * 0.66 * smoothstep(0.0, 0.3, t) * p.volume;
}13 lines from stage 2 that are gone
uint seed = uint(floor(u.time / 3.0)) + 1u;
float2 top = float2(0.5 * aspect, 1.02);
float2 foot = float2(0.5 * aspect + 0.15, 0.08);
float reach = 0.012 * p.glow;
⋯
float d = boltDistance(q, top, foot, seed, p.jag, 1.0);
float heat = 0.04 + 1.1 * (1.0 - smoothstep(0.0, 1.5 * px, d));
heat += 0.4 * exp(-d / max(reach, px));
for (int b = 0; b < 8; b++) {
if (float(b) >= p.branches) break;
float bd = branchDistance(q, top, foot, seed, b, p.jag, 1.0);
heat += 0.6 * (1.0 - smoothstep(0.0, 1.2 * px, bd));
heat += 0.15 * exp(-bd / max(reach * 0.6, px));
⋯
return rain * 0.09 * 0.8 * smoothstep(0.0, 0.3, t) * p.volume;Stage 4 · Near and far
Now the field has depth. The ground and the cloud base, 1.2 km up, are flat planes seen from 60 m above the ground: a point z kilometres away is drawn (height − 0.06) / z above the horizon. Read the other way, every pixel of sky tells where on the cloud base it looks, so cloudAt lays the cloud noise, fbm from Flowing Color Field, on that plane, and it shrinks and fades towards the horizon as a real ceiling of cloud does.
Each strike now lands somewhere on the ground: distance times 0.55 to 1.45 kilometres away, and across the field as far as the screen shows. The bolt runs from that point on the cloud base down to that point on the ground, so a far strike is short, low on the screen, thinner and dimmer, and a near one comes from above the top of the picture. The clouds light most around the strike's place on the cloud base, and the ground around its foot.
The thunder waits. A strike far kilometres away is heard far / 0.343 seconds after its flash, since sound covers 343 metres a second.
stage-4-near-and-far.metal56 new lines since stage 3
// Stage 4 · Near and far
// Now the field has depth: the ground and the cloud base are planes, drawn
// smaller towards the horizon, and each strike lands some distance away, so
// a far bolt is short, thin and dim. Its thunder waits for the sound to cross
// that distance: about three seconds a kilometre.
// @param every 5 2 12 Seconds between strikes, on average
// @param distance 1.2 0.4 3 How far away the storm is, in kilometres
// @param jag 0.58 0.4 0.72 How rough the bolts are: each level of displacement keeps this much of the last
// @param branches 5 0 8 Branches on each bolt
// @param restrikes 2 0 3 Strokes after the first down the same channel, in the bolt alone
// @param glow 1 0 2 How far the light spreads from the bolt
// @param volume 0.8 0 1 Loudness of the sound
// The horizon on screen, and our height in km: ground z km away is drawn
// CAMERA / z below the horizon.
constant float HORIZON = 0.24;
constant float CAMERA = 0.06;
// The cloud base's height, in km.
constant float CEILING = 1.2;
// The speed of sound, in km a second.
constant float SOUND = 0.343;
// How many times the line is split: 2^7 = 128 pieces.
constant int LEVELS = 7;
constant float PIECES = 128.0;
// The kiln palette from Flowing Color Field.
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 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.
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;
}
// Integer hash, so noise stays exact for minutes of sound.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// Distance from point a to the segment from b to c.
float segment(float2 a, float2 b, float2 c) {
float2 ba = a - b, cb = c - b;
float h = clamp(dot(ba, cb) / dot(cb, cb), 0.0, 1.0);
return length(ba - cb * h);
}
// How far sideways the point a fraction s along a bolt is pushed: one tent
// over the whole line, two over its halves, 2^l at level l, each level jag
// times as tall as the last. Rough, and still one line.
float boltOffset(float s, uint seed, float jag) {
float off = 0.0, amp = 0.2, n = 1.0;
for (int l = 0; l < LEVELS; l++) {
float u = clamp(s, 0.0, 1.0) * n;
float j = min(floor(u), n - 1.0), f = u - j;
uint k = seed * 2048u + uint(l) * 256u + uint(j);
off += (hashU(k) * 2.0 - 1.0) * amp * (1.0 - abs(2.0 * f - 1.0));
amp *= jag;
n *= 2.0;
}
return off;
}
// The point a fraction s along the bolt from a to b, pushed sideways.
float2 boltPoint(float s, float2 a, float2 b, uint seed, float jag) {
float2 d = b - a;
return a + d * s + float2(-d.y, d.x) * boltOffset(s, seed, jag);
}
// Distance from p to the bolt from a to b, as far as the fraction `upto`.
// Only the few pieces about p's place along the line can be nearest.
float boltDistance(float2 p, float2 a, float2 b, uint seed, float jag,
float upto) {
float2 d = b - a;
float len2 = dot(d, d);
float s = dot(p - a, d) / len2;
float side = abs(dot(p - a, float2(-d.y, d.x))) / len2;
if (upto <= 0.0 || s < -0.1 || s > 1.1 || side > 0.2 / (1.0 - jag))
return 1e3;
float i0 = floor(clamp(s, 0.0, 1.0) * PIECES);
float best = 1e3;
float2 last = boltPoint((i0 - 3.0) / PIECES, a, b, seed, jag);
for (int i = -2; i <= 3; i++) {
float s1 = (i0 + float(i)) / PIECES;
float2 next = boltPoint(s1, a, b, seed, jag);
if (s1 > 0.0 && s1 - 1.0 / PIECES < upto)
best = min(best, segment(p, last, next));
last = next;
}
return best;
}
// Branches leave the channel at random points and angles, shorter lower
// down. Each is a bolt of its own, from where it leaves to where it ends.
float branchDistance(float2 q, float2 top, float2 foot, uint seed, int b,
float jag, float upto) {
uint bs = seed * 16u + uint(b);
float from = 0.1 + 0.65 * hashU(bs * 3u);
float turn = (0.35 + 0.55 * hashU(bs * 3u + 1u))
* (hashU(bs * 3u + 2u) < 0.5 ? -1.0 : 1.0);
float len = (0.15 + 0.25 * hashU(bs + 999u)) * (1.0 - 0.5 * from);
float2 a = boltPoint(from, top, foot, seed, jag);
float2 axis = foot - top;
float c = cos(turn), sn = sin(turn);
float2 dir = float2(axis.x * c - axis.y * sn, axis.x * sn + axis.y * c);
float reach = (upto - from) / (len * c);
return boltDistance(q, a, a + dir * len, bs + 77u, jag, reach);
}
// As in Rain on Glass, the clock is cut into windows, here `every` seconds
// long, with a strike at a random moment in the first half of each, so two
// are never closer than half a window. Each strike's place on the ground,
// x across and z away in kilometres, and its seed come from its window.
struct Strike {
float at;
float2 ground;
uint seed;
};
Strike strikeIn(float k, float aspect, constant Params& p) {
uint w = uint(k);
Strike s;
s.at = k * p.every + 0.5 * p.every * hashU(w * 7u + 1u);
float z = p.distance * (0.55 + 0.9 * hashU(w * 7u + 2u));
s.ground = float2((hashU(w * 7u + 3u) - 0.5) * 0.75 * aspect * z, z);
s.seed = w * 7u + 4u;
return s;
}
// Where a point on the ground or the cloud base is drawn.
float2 toScreen(float2 ground, float height, float aspect) {
return float2(0.5 * aspect + ground.x / ground.y,
HORIZON + (height - CAMERA) / ground.y);
}
// The bolt's light `x` seconds into the strike. The leader takes 0.2 s to
// come down, faint; then the return stroke, and each restrike 0.1 s apart,
// is a sharp pulse that fades.
constant float LEADER = 0.2;
float strokes(float x, constant Params& p) {
if (x < LEADER) return 0.25 * step(0.0, x);
float light = 0.0;
for (int j = 0; j <= 3; j++) {
float y = x - LEADER - 0.1 * float(j);
if (float(j) <= p.restrikes + 0.5 && y >= 0.0)
light += pow(0.8, float(j)) * exp(-y * 14.0);
}
return light;
}
// The light thrown on the clouds and the ground: one rise and one smooth fall
// a strike, however often its bolt flickers. A flash that comes and goes more
// than three times a second can cause seizures; this one never does.
float skyLight(float x) {
float y = x - LEADER;
if (y < 0.0) return 0.0;
return smoothstep(0.0, 0.04, y) * exp(-y * 2.2);
}
// One bolt and its branches, as heat, for the strike `x` seconds old.
float boltHeat(float2 q, Strike s, float x, float px, float aspect,
constant Params& p) {
float2 top = toScreen(s.ground, CEILING, aspect);
top.y = min(top.y, 1.15);
float2 foot = toScreen(s.ground, 0.0, aspect);
// How far down the leader has come, and how bright the channel is.
float down = clamp(x / LEADER, 0.0, 1.0);
float light = strokes(x, p);
float sky = skyLight(x);
// Thinner and dimmer the farther it is.
float near = clamp(0.8 / s.ground.y, 0.3, 1.6);
float width = px * (0.6 + 0.9 * near);
float reach = 0.012 * p.glow * near;
float d = boltDistance(q, top, foot, s.seed, p.jag, down);
float heat = light * near * (1.0 - smoothstep(0.0, width, d)) * 1.3;
heat += (sky + 0.3 * step(x, LEADER) * down) * near * 0.5
* exp(-d / max(reach, px));
// The branches, lit as the leader reaches them; they fade sooner.
for (int b = 0; b < 8; b++) {
if (float(b) >= p.branches) break;
float bd = branchDistance(q, top, foot, s.seed, b, p.jag, down);
float fade = x < LEADER ? 0.7 : 0.45 * exp(-(x - LEADER) * 3.0);
heat += fade * (light + 0.3) * near
* (1.0 - smoothstep(0.0, width * 0.8, bd)) * 0.9;
heat += fade * sky * near * 0.3 * exp(-bd / max(reach * 0.6, px));
}
return heat;
}
// The cloud base, seen from below: a plane CEILING high, so its noise is
// drawn smaller towards the horizon. Its density, and the place on it.
float cloudAt(float2 q, float aspect, float t, thread float2& place) {
float z = (CEILING - CAMERA) / max(q.y - HORIZON, 1e-3);
place = float2((q.x - 0.5 * aspect) * z, z);
float c = fbm(place * float2(1.6, 1.2) + float2(t * 0.03, 0.0));
return smoothstep(0.25, 0.85, c) * exp(-z * 0.08);
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float t = u.time;
float aspect = u.resolution.x / u.resolution.y;
float2 q = float2(uv.x * aspect, uv.y);
float px = 1.0 / u.resolution.y;
bool sky = q.y > HORIZON;
float2 place;
float cloud = sky ? cloudAt(q, aspect, t, place) : 0.0;
// The ground's place, x across and z away.
float zg = CAMERA / max(HORIZON - q.y, 1e-3);
float2 ground = float2((q.x - 0.5 * aspect) * zg, zg);
// A dim sky, darker overhead, and haze along the horizon.
float heat = sky ? 0.07 + 0.05 * cloud - 0.03 * (q.y - HORIZON)
: 0.03 + 0.02 * fbm(ground * 3.0);
heat += 0.06 * exp(-abs(q.y - HORIZON) * 25.0);
float k = floor(t / p.every);
for (int i = 0; i < 4; i++) {
float kk = k - float(i);
if (kk < 0.0) break;
Strike s = strikeIn(kk, aspect, p);
float x = t - s.at;
if (x < 0.0) continue;
float lit = skyLight(x) * clamp(0.9 / s.ground.y, 0.25, 1.5);
if (sky) {
// The clouds glow about the strike, a little everywhere.
float far = length(place - s.ground);
heat += lit * (0.2 + cloud) * (0.25 + exp(-far * 1.2));
} else {
float far = length(ground - s.ground);
heat += lit * 0.35 * exp(-far * 1.5);
}
if (x < 1.5) heat += boltHeat(q, s, x, px, aspect, p);
}
return float4(kiln(heat), 1.0);
}
// Rain, and thunder. Each strike's thunder starts when its sound reaches
// you: its distance over the speed of sound. A near strike cracks first, then
// rumbles; a far one only rumbles, lower, longer and quieter.
// Noise made by joining random values `rate` times a second:
// a lower rate sounds darker.
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;
}
// The thunder `x` seconds after it arrives, from `far` kilometres away.
float thunder(float t, float x, float far, uint seed) {
if (x <= 0.0) return 0.0;
float near = smoothstep(1.6, 0.4, far);
float crack = (noise1(t, 3500.0, seed) * 0.6
+ noise1(t, 1200.0, seed + 1u) * 0.5)
* exp(-x * 12.0) * smoothstep(0.0, 0.004, x) * near;
float env = (1.0 - exp(-x * 5.0)) * exp(-x / (0.9 + 0.6 * far))
* smoothstep(12.0, 9.0, x);
float swell = 0.55 + 0.45 * noise1(t, 2.5, seed + 2u);
float low = 1.0 - 0.15 * far;
float rumble = noise1(t, 50.0 * low, seed + 3u) * 0.6
+ noise1(t, 110.0 * low, seed + 4u) * 0.4
+ noise1(t, 260.0 * low, seed + 5u) * 0.25;
return (crack * 0.7 + rumble * env * swell) * 5.0 / (0.5 + far);
}
float2 sound(float t, constant Params& p) {
float aspect = 16.0 / 9.0;
// The rain: noise with its lowest part taken away.
float2 rain = float2(noise1(t, 9000.0, 1u) - noise1(t, 900.0, 2u) * 0.6,
noise1(t, 9000.0, 4u) - noise1(t, 900.0, 5u) * 0.6);
float2 sum = rain * 0.09;
float k = floor(t / p.every);
for (int i = 0; i < 16; i++) {
float kk = k - float(i);
if (kk < 0.0) break;
Strike s = strikeIn(kk, aspect, p);
float far = length(s.ground);
float x = t - s.at - far / SOUND;
if (x <= 0.0 || x > 12.0) continue;
// Louder on the side it struck, each ear a little different.
float pan = 0.5 + 0.4 * s.ground.x / far;
float a = thunder(t, x, far, s.seed);
float b = thunder(t, x, far, s.seed + 50u);
sum += float2((0.7 * a + 0.3 * b) * (1.2 - pan),
(0.3 * a + 0.7 * b) * (0.2 + pan));
}
// A fade in over the first 0.3 s, so the rain doesn't start with a click.
return tanh(sum * 1.2) * 0.66 * smoothstep(0.0, 0.3, t) * p.volume;
}26 lines from stage 3 that are gone
constant float GROUND = 0.12;
⋯
float x;
⋯
s.x = (0.15 + 0.7 * hashU(w * 7u + 3u)) * aspect;
⋯
float boltHeat(float2 q, Strike s, float x, float px, constant Params& p) {
float2 top = float2(s.x - 0.1, 1.05);
float2 foot = float2(s.x, GROUND);
⋯
float reach = 0.012 * p.glow;
⋯
float heat = light * (1.0 - smoothstep(0.0, 1.5 * px, d)) * 1.3;
heat += (sky + 0.3 * step(x, LEADER) * down) * 0.5
⋯
heat += fade * (light + 0.3) * (1.0 - smoothstep(0.0, 1.2 * px, bd));
heat += fade * sky * 0.3 * exp(-bd / max(reach * 0.6, px));
⋯
bool sky = q.y > GROUND;
⋯
float cloud = smoothstep(0.3, 0.8, fbm(q * float2(2.0, 4.0)
+ float2(t * 0.05, 0.0)));
float heat = sky ? 0.06 + 0.05 * cloud : 0.03;
⋯
for (int i = 0; i < 2; i++) {
⋯
float far = length(q - float2(s.x, 1.0));
float lit = skyLight(x);
heat += sky ? lit * (0.1 + 0.6 * cloud) * (0.2 + exp(-far * 2.5))
: lit * 0.2 * exp(-abs(q.x - s.x) * 3.0);
if (x < 1.5) heat += boltHeat(q, s, x, px, p);
⋯
for (int i = 0; i < 8; i++) {
⋯
float x = t - s.at - 3.0;
⋯
float pan = s.x / aspect;
float a = thunder(t, x, 1.0, s.seed);
sum += a * float2(1.2 - pan, 0.2 + pan);Stage 5 · Lightning
The finished shader draws that wait. Each strike sends a ring out along the ground from its foot, its radius growing at the speed of sound, 0.343 × t kilometres, fading as it goes. Drawn on the ground plane, it is an ellipse that widens towards you, and the moment it passes under you is the moment the thunder starts. At the default distance that is two to six seconds after the flash; set distance to 3 and it can be fifteen.
the kiln palette and shade(), from the finished shader
// The kiln palette from Flowing Color Field.
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 t = u.time;
float aspect = u.resolution.x / u.resolution.y;
float2 q = float2(uv.x * aspect, uv.y);
float px = 1.0 / u.resolution.y;
bool sky = q.y > HORIZON;
float2 place;
float cloud = sky ? cloudAt(q, aspect, t, place) : 0.0;
// The ground's place, x across and z away.
float zg = CAMERA / max(HORIZON - q.y, 1e-3);
float2 ground = float2((q.x - 0.5 * aspect) * zg, zg);
// A dim sky, darker overhead, and haze along the horizon.
float heat = sky ? 0.07 + 0.05 * cloud - 0.03 * (q.y - HORIZON)
: 0.03 + 0.02 * fbm(ground * 3.0);
heat += 0.06 * exp(-abs(q.y - HORIZON) * 25.0);
float k = floor(t / p.every);
for (int i = 0; i < 4; i++) {
float kk = k - float(i);
if (kk < 0.0) break;
Strike s = strikeIn(kk, aspect, p);
float x = t - s.at;
if (x < 0.0) continue;
float lit = skyLight(x) * clamp(0.9 / s.ground.y, 0.25, 1.5);
if (sky) {
// The clouds glow about the strike, a little everywhere.
float far = length(place - s.ground);
heat += lit * (0.2 + cloud) * (0.25 + exp(-far * 1.2));
} else {
float far = length(ground - s.ground);
heat += lit * 0.35 * exp(-far * 1.5);
// The thunder: a ring on the ground leaving the strike at the
// speed of sound. It reaches you as you hear it.
float r = SOUND * x;
float w = px * max(zg * zg / CAMERA, zg) * 1.5;
float ring = exp(-abs(far - r) / w) * exp(-r * 0.5)
* step(r, length(s.ground) + 0.05);
heat += 0.22 * ring;
}
if (x < 1.5) heat += boltHeat(q, s, x, px, aspect, p);
}
return float4(kiln(heat), 1.0);
}lightning.metal, the whole file5 new lines since stage 4
// Ray Kiln · Lightning
//
// @episode Lightning
// @length 60
// @variation 30
// @variations 1,2,3,5,8,9,11,12
// @still 45.9
// @teaches Branching bolts by midpoint displacement, glow, thunder that arrives late
// @category Fragment
// @tags midpoint displacement, fractals, lightning, glow, procedural sound
// @sweeps jag, distance, glow
// @final Lightning | Strikes near and far under a ceiling of cloud, each one's thunder a ring on the ground that reaches you seconds after the flash.
//
// A storm at night, seen from a field. Each bolt is drawn by midpoint
// displacement: a straight line from the cloud to the ground, its middle
// pushed sideways, then the middle of each half, and so on, each time by less.
// Branches grow from points along it the same way. A strike comes down as a
// faint leader, then the return stroke lights it white and lights the clouds;
// the bolt flickers as it strikes again down the same channel. The thunder
// leaves the strike at the speed of sound, drawn as a ring on the ground, and
// you hear it when the ring reaches you: about three seconds for every
// kilometre.
// 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 every 5 2 12 Seconds between strikes, on average
// @param distance 1.2 0.4 3 How far away the storm is, in kilometres
// @param jag 0.58 0.4 0.72 How rough the bolts are: each level of displacement keeps this much of the last
// @param branches 5 0 8 Branches on each bolt
// @param restrikes 2 0 3 Strokes after the first down the same channel, in the bolt alone
// @param glow 1 0 2 How far the light spreads from the bolt
// @param volume 0.8 0 1 Loudness of the sound
// The horizon on screen, and our height in km: ground z km away is drawn
// CAMERA / z below the horizon.
constant float HORIZON = 0.24;
constant float CAMERA = 0.06;
// The cloud base's height, in km.
constant float CEILING = 1.2;
// The speed of sound, in km a second.
constant float SOUND = 0.343;
// How many times the line is split: 2^7 = 128 pieces.
constant int LEVELS = 7;
constant float PIECES = 128.0;
// The kiln palette from Flowing Color Field.
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 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.
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;
}
// Integer hash, so noise stays exact for minutes of sound.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// Distance from point a to the segment from b to c.
float segment(float2 a, float2 b, float2 c) {
float2 ba = a - b, cb = c - b;
float h = clamp(dot(ba, cb) / dot(cb, cb), 0.0, 1.0);
return length(ba - cb * h);
}
// How far sideways the point a fraction s along a bolt is pushed: one tent
// over the whole line, two over its halves, 2^l at level l, each level jag
// times as tall as the last. Rough, and still one line.
float boltOffset(float s, uint seed, float jag) {
float off = 0.0, amp = 0.2, n = 1.0;
for (int l = 0; l < LEVELS; l++) {
float u = clamp(s, 0.0, 1.0) * n;
float j = min(floor(u), n - 1.0), f = u - j;
uint k = seed * 2048u + uint(l) * 256u + uint(j);
off += (hashU(k) * 2.0 - 1.0) * amp * (1.0 - abs(2.0 * f - 1.0));
amp *= jag;
n *= 2.0;
}
return off;
}
// The point a fraction s along the bolt from a to b, pushed sideways.
float2 boltPoint(float s, float2 a, float2 b, uint seed, float jag) {
float2 d = b - a;
return a + d * s + float2(-d.y, d.x) * boltOffset(s, seed, jag);
}
// Distance from p to the bolt from a to b, as far as the fraction `upto`.
// Only the few pieces about p's place along the line can be nearest.
float boltDistance(float2 p, float2 a, float2 b, uint seed, float jag,
float upto) {
float2 d = b - a;
float len2 = dot(d, d);
float s = dot(p - a, d) / len2;
float side = abs(dot(p - a, float2(-d.y, d.x))) / len2;
if (upto <= 0.0 || s < -0.1 || s > 1.1 || side > 0.2 / (1.0 - jag))
return 1e3;
float i0 = floor(clamp(s, 0.0, 1.0) * PIECES);
float best = 1e3;
float2 last = boltPoint((i0 - 3.0) / PIECES, a, b, seed, jag);
for (int i = -2; i <= 3; i++) {
float s1 = (i0 + float(i)) / PIECES;
float2 next = boltPoint(s1, a, b, seed, jag);
if (s1 > 0.0 && s1 - 1.0 / PIECES < upto)
best = min(best, segment(p, last, next));
last = next;
}
return best;
}
// Branches leave the channel at random points and angles, shorter lower
// down. Each is a bolt of its own, from where it leaves to where it ends.
float branchDistance(float2 q, float2 top, float2 foot, uint seed, int b,
float jag, float upto) {
uint bs = seed * 16u + uint(b);
float from = 0.1 + 0.65 * hashU(bs * 3u);
float turn = (0.35 + 0.55 * hashU(bs * 3u + 1u))
* (hashU(bs * 3u + 2u) < 0.5 ? -1.0 : 1.0);
float len = (0.15 + 0.25 * hashU(bs + 999u)) * (1.0 - 0.5 * from);
float2 a = boltPoint(from, top, foot, seed, jag);
float2 axis = foot - top;
float c = cos(turn), sn = sin(turn);
float2 dir = float2(axis.x * c - axis.y * sn, axis.x * sn + axis.y * c);
float reach = (upto - from) / (len * c);
return boltDistance(q, a, a + dir * len, bs + 77u, jag, reach);
}
// As in Rain on Glass, the clock is cut into windows, here `every` seconds
// long, with a strike at a random moment in the first half of each, so two
// are never closer than half a window. Each strike's place on the ground,
// x across and z away in kilometres, and its seed come from its window.
struct Strike {
float at;
float2 ground;
uint seed;
};
Strike strikeIn(float k, float aspect, constant Params& p) {
uint w = uint(k);
Strike s;
s.at = k * p.every + 0.5 * p.every * hashU(w * 7u + 1u);
float z = p.distance * (0.55 + 0.9 * hashU(w * 7u + 2u));
s.ground = float2((hashU(w * 7u + 3u) - 0.5) * 0.75 * aspect * z, z);
s.seed = w * 7u + 4u;
return s;
}
// Where a point on the ground or the cloud base is drawn.
float2 toScreen(float2 ground, float height, float aspect) {
return float2(0.5 * aspect + ground.x / ground.y,
HORIZON + (height - CAMERA) / ground.y);
}
// The bolt's light `x` seconds into the strike. The leader takes 0.2 s to
// come down, faint; then the return stroke, and each restrike 0.1 s apart,
// is a sharp pulse that fades.
constant float LEADER = 0.2;
float strokes(float x, constant Params& p) {
if (x < LEADER) return 0.25 * step(0.0, x);
float light = 0.0;
for (int j = 0; j <= 3; j++) {
float y = x - LEADER - 0.1 * float(j);
if (float(j) <= p.restrikes + 0.5 && y >= 0.0)
light += pow(0.8, float(j)) * exp(-y * 14.0);
}
return light;
}
// The light thrown on the clouds and the ground: one rise and one smooth fall
// a strike, however often its bolt flickers. A flash that comes and goes more
// than three times a second can cause seizures; this one never does.
float skyLight(float x) {
float y = x - LEADER;
if (y < 0.0) return 0.0;
return smoothstep(0.0, 0.04, y) * exp(-y * 2.2);
}
// One bolt and its branches, as heat, for the strike `x` seconds old.
float boltHeat(float2 q, Strike s, float x, float px, float aspect,
constant Params& p) {
float2 top = toScreen(s.ground, CEILING, aspect);
top.y = min(top.y, 1.15);
float2 foot = toScreen(s.ground, 0.0, aspect);
// How far down the leader has come, and how bright the channel is.
float down = clamp(x / LEADER, 0.0, 1.0);
float light = strokes(x, p);
float sky = skyLight(x);
// Thinner and dimmer the farther it is.
float near = clamp(0.8 / s.ground.y, 0.3, 1.6);
float width = px * (0.6 + 0.9 * near);
float reach = 0.012 * p.glow * near;
float d = boltDistance(q, top, foot, s.seed, p.jag, down);
float heat = light * near * (1.0 - smoothstep(0.0, width, d)) * 1.3;
heat += (sky + 0.3 * step(x, LEADER) * down) * near * 0.5
* exp(-d / max(reach, px));
// The branches, lit as the leader reaches them; they fade sooner.
for (int b = 0; b < 8; b++) {
if (float(b) >= p.branches) break;
float bd = branchDistance(q, top, foot, s.seed, b, p.jag, down);
float fade = x < LEADER ? 0.7 : 0.45 * exp(-(x - LEADER) * 3.0);
heat += fade * (light + 0.3) * near
* (1.0 - smoothstep(0.0, width * 0.8, bd)) * 0.9;
heat += fade * sky * near * 0.3 * exp(-bd / max(reach * 0.6, px));
}
return heat;
}
// The cloud base, seen from below: a plane CEILING high, so its noise is
// drawn smaller towards the horizon. Its density, and the place on it.
float cloudAt(float2 q, float aspect, float t, thread float2& place) {
float z = (CEILING - CAMERA) / max(q.y - HORIZON, 1e-3);
place = float2((q.x - 0.5 * aspect) * z, z);
float c = fbm(place * float2(1.6, 1.2) + float2(t * 0.03, 0.0));
return smoothstep(0.25, 0.85, c) * exp(-z * 0.08);
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float t = u.time;
float aspect = u.resolution.x / u.resolution.y;
float2 q = float2(uv.x * aspect, uv.y);
float px = 1.0 / u.resolution.y;
bool sky = q.y > HORIZON;
float2 place;
float cloud = sky ? cloudAt(q, aspect, t, place) : 0.0;
// The ground's place, x across and z away.
float zg = CAMERA / max(HORIZON - q.y, 1e-3);
float2 ground = float2((q.x - 0.5 * aspect) * zg, zg);
// A dim sky, darker overhead, and haze along the horizon.
float heat = sky ? 0.07 + 0.05 * cloud - 0.03 * (q.y - HORIZON)
: 0.03 + 0.02 * fbm(ground * 3.0);
heat += 0.06 * exp(-abs(q.y - HORIZON) * 25.0);
float k = floor(t / p.every);
for (int i = 0; i < 4; i++) {
float kk = k - float(i);
if (kk < 0.0) break;
Strike s = strikeIn(kk, aspect, p);
float x = t - s.at;
if (x < 0.0) continue;
float lit = skyLight(x) * clamp(0.9 / s.ground.y, 0.25, 1.5);
if (sky) {
// The clouds glow about the strike, a little everywhere.
float far = length(place - s.ground);
heat += lit * (0.2 + cloud) * (0.25 + exp(-far * 1.2));
} else {
float far = length(ground - s.ground);
heat += lit * 0.35 * exp(-far * 1.5);
// The thunder: a ring on the ground leaving the strike at the
// speed of sound. It reaches you as you hear it.
float r = SOUND * x;
float w = px * max(zg * zg / CAMERA, zg) * 1.5;
float ring = exp(-abs(far - r) / w) * exp(-r * 0.5)
* step(r, length(s.ground) + 0.05);
heat += 0.22 * ring;
}
if (x < 1.5) heat += boltHeat(q, s, x, px, aspect, p);
}
return float4(kiln(heat), 1.0);
}
// Rain, and thunder. Each strike's thunder starts when its ring reaches you:
// its distance over the speed of sound. A near strike cracks first, then
// rumbles; a far one only rumbles, lower, longer and quieter.
// Noise made by joining random values `rate` times a second:
// a lower rate sounds darker.
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;
}
// The thunder `x` seconds after it arrives, from `far` kilometres away.
float thunder(float t, float x, float far, uint seed) {
if (x <= 0.0) return 0.0;
float near = smoothstep(1.6, 0.4, far);
float crack = (noise1(t, 3500.0, seed) * 0.6
+ noise1(t, 1200.0, seed + 1u) * 0.5)
* exp(-x * 12.0) * smoothstep(0.0, 0.004, x) * near;
float env = (1.0 - exp(-x * 5.0)) * exp(-x / (0.9 + 0.6 * far))
* smoothstep(12.0, 9.0, x);
float swell = 0.55 + 0.45 * noise1(t, 2.5, seed + 2u);
float low = 1.0 - 0.15 * far;
float rumble = noise1(t, 50.0 * low, seed + 3u) * 0.6
+ noise1(t, 110.0 * low, seed + 4u) * 0.4
+ noise1(t, 260.0 * low, seed + 5u) * 0.25;
return (crack * 0.7 + rumble * env * swell) * 5.0 / (0.5 + far);
}
float2 sound(float t, constant Params& p) {
float aspect = 16.0 / 9.0;
// The rain: noise with its lowest part taken away.
float2 rain = float2(noise1(t, 9000.0, 1u) - noise1(t, 900.0, 2u) * 0.6,
noise1(t, 9000.0, 4u) - noise1(t, 900.0, 5u) * 0.6);
float2 sum = rain * 0.09;
float k = floor(t / p.every);
for (int i = 0; i < 16; i++) {
float kk = k - float(i);
if (kk < 0.0) break;
Strike s = strikeIn(kk, aspect, p);
float far = length(s.ground);
float x = t - s.at - far / SOUND;
if (x <= 0.0 || x > 12.0) continue;
// Louder on the side it struck, each ear a little different.
float pan = 0.5 + 0.4 * s.ground.x / far;
float a = thunder(t, x, far, s.seed);
float b = thunder(t, x, far, s.seed + 50u);
sum += float2((0.7 * a + 0.3 * b) * (1.2 - pan),
(0.3 * a + 0.7 * b) * (0.2 + pan));
}
// A fade in over the first 0.3 s, so the rain doesn't start with a click.
return tanh(sum * 1.2) * 0.66 * smoothstep(0.0, 0.3, t) * p.volume;
}The sound
Rain and thunder. The rain is noise with its lowest part taken away, as in Rain on Glass, a little different in each ear. Each strike's thunder starts when its sound arrives. A strike within about a kilometre and a half cracks first: bright noise, joined 3,500 and 1,200 times a second, that dies in a tenth of a second. Then the rumble, three layers of dark noise around 50, 110 and 260 Hz, swelling in over a fifth of a second and rolling away over a few, its loudness wandering. The farther the strike, the lower the rumble, the longer it rolls and the quieter it is, and a far one never cracks. It is louder on the side the bolt struck, and each ear gets its own mix of two noises, so it is wide. tanh keeps it all under full scale, and the rain fades in over the first 0.3 s.
At the defaults the peak is about −6.0 dBFS and the average (RMS) about −22.3 dBFS; it is quiet between thunders and loud in them. With the storm close and busy (distance 0.4, every 2) the peak is −5.6 dBFS and the RMS −15.6 dBFS; far away (distance 3) −8.7 and −25.1. The 60-second video ends in the rain and the tail of a rumble, at a sample of about 0.025 (−32 dBFS).
New in Ray Kiln
The flash check. The playground now watches the picture for flashing. Every frame on screen is shrunk on the GPU to 32 × 18 cells of relative luminance and read back, and the brightness of the whole picture and of each quarter is followed over the shader's clock. A change of a tenth of full brightness or more, up and then down, is one flash (the general flash rule of the web's accessibility guidelines, WCAG 2.3.1, simplified). When the picture flashes more than three times in any one second, a warning appears under the clock, with the count and the moment, and stays until the code changes or the clock goes back. A paused clock counts nothing, so dragging a slider never sets it off. It is always on, for any shader, and free.
Lightning is the shader it was made for. Open Lightning in the playground and let it run: the bolt flickers with every restrike, but the sky lights once a strike, so the check stays quiet. Then make the clouds follow the bolt (see Try this) and the warning appears.
Try this
- Set
levelsin the first stage to 1, 2, 3 in turn: each level adds kinks half the size of the last. - Set
jagto 0.4: the bolt barely wanders, smooth as a crack in glass. At 0.72 it scribbles. - Set
distanceto 3 and count from a flash to the thunder: nine or so seconds, three a kilometre. - Set
branchesto 8 andglowto 2 for a crowded, hazy sky;glow0 leaves only the lines. - In
shade, changefloat lit = skyLight(x)tofloat lit = 3.0 * strokes(x, p), setrestrikesto 3 anddistanceto 0.4: now the whole sky flickers with the bolt, four flashes in half a second, and the playground's flash check warns you. - In
boltOffset, startampat 0.4 instead of 0.2: the coarse levels dominate, and the bolt swings wide before it gets rough.
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 |
|---|---|---|---|
every | 5 | 2 to 12 | Seconds between strikes, on average |
distance | 1.2 | 0.4 to 3 | How far away the storm is, in kilometres |
jag | 0.58 | 0.4 to 0.72 | How rough the bolts are: each level of displacement keeps this much of the last |
branches | 5 | 0 to 8 | Branches on each bolt |
restrikes | 2 | 0 to 3 | Strokes after the first down the same channel, in the bolt alone |
glow | 1 | 0 to 2 | How far the light spreads from the bolt |
volume | 0.8 | 0 to 1 | 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.
lightning.metal
// Ray Kiln · Lightning
//
// @episode Lightning
// @length 60
// @variation 30
// @variations 1,2,3,5,8,9,11,12
// @still 45.9
// @teaches Branching bolts by midpoint displacement, glow, thunder that arrives late
// @category Fragment
// @tags midpoint displacement, fractals, lightning, glow, procedural sound
// @sweeps jag, distance, glow
// @final Lightning | Strikes near and far under a ceiling of cloud, each one's thunder a ring on the ground that reaches you seconds after the flash.
//
// A storm at night, seen from a field. Each bolt is drawn by midpoint
// displacement: a straight line from the cloud to the ground, its middle
// pushed sideways, then the middle of each half, and so on, each time by less.
// Branches grow from points along it the same way. A strike comes down as a
// faint leader, then the return stroke lights it white and lights the clouds;
// the bolt flickers as it strikes again down the same channel. The thunder
// leaves the strike at the speed of sound, drawn as a ring on the ground, and
// you hear it when the ring reaches you: about three seconds for every
// kilometre.
// 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 every 5 2 12 Seconds between strikes, on average
// @param distance 1.2 0.4 3 How far away the storm is, in kilometres
// @param jag 0.58 0.4 0.72 How rough the bolts are: each level of displacement keeps this much of the last
// @param branches 5 0 8 Branches on each bolt
// @param restrikes 2 0 3 Strokes after the first down the same channel, in the bolt alone
// @param glow 1 0 2 How far the light spreads from the bolt
// @param volume 0.8 0 1 Loudness of the sound
// The horizon on screen, and our height in km: ground z km away is drawn
// CAMERA / z below the horizon.
constant float HORIZON = 0.24;
constant float CAMERA = 0.06;
// The cloud base's height, in km.
constant float CEILING = 1.2;
// The speed of sound, in km a second.
constant float SOUND = 0.343;
// How many times the line is split: 2^7 = 128 pieces.
constant int LEVELS = 7;
constant float PIECES = 128.0;
// The kiln palette from Flowing Color Field.
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 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.
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;
}
// Integer hash, so noise stays exact for minutes of sound.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// Distance from point a to the segment from b to c.
float segment(float2 a, float2 b, float2 c) {
float2 ba = a - b, cb = c - b;
float h = clamp(dot(ba, cb) / dot(cb, cb), 0.0, 1.0);
return length(ba - cb * h);
}
// How far sideways the point a fraction s along a bolt is pushed: one tent
// over the whole line, two over its halves, 2^l at level l, each level jag
// times as tall as the last. Rough, and still one line.
float boltOffset(float s, uint seed, float jag) {
float off = 0.0, amp = 0.2, n = 1.0;
for (int l = 0; l < LEVELS; l++) {
float u = clamp(s, 0.0, 1.0) * n;
float j = min(floor(u), n - 1.0), f = u - j;
uint k = seed * 2048u + uint(l) * 256u + uint(j);
off += (hashU(k) * 2.0 - 1.0) * amp * (1.0 - abs(2.0 * f - 1.0));
amp *= jag;
n *= 2.0;
}
return off;
}
// The point a fraction s along the bolt from a to b, pushed sideways.
float2 boltPoint(float s, float2 a, float2 b, uint seed, float jag) {
float2 d = b - a;
return a + d * s + float2(-d.y, d.x) * boltOffset(s, seed, jag);
}
// Distance from p to the bolt from a to b, as far as the fraction `upto`.
// Only the few pieces about p's place along the line can be nearest.
float boltDistance(float2 p, float2 a, float2 b, uint seed, float jag,
float upto) {
float2 d = b - a;
float len2 = dot(d, d);
float s = dot(p - a, d) / len2;
float side = abs(dot(p - a, float2(-d.y, d.x))) / len2;
if (upto <= 0.0 || s < -0.1 || s > 1.1 || side > 0.2 / (1.0 - jag))
return 1e3;
float i0 = floor(clamp(s, 0.0, 1.0) * PIECES);
float best = 1e3;
float2 last = boltPoint((i0 - 3.0) / PIECES, a, b, seed, jag);
for (int i = -2; i <= 3; i++) {
float s1 = (i0 + float(i)) / PIECES;
float2 next = boltPoint(s1, a, b, seed, jag);
if (s1 > 0.0 && s1 - 1.0 / PIECES < upto)
best = min(best, segment(p, last, next));
last = next;
}
return best;
}
// Branches leave the channel at random points and angles, shorter lower
// down. Each is a bolt of its own, from where it leaves to where it ends.
float branchDistance(float2 q, float2 top, float2 foot, uint seed, int b,
float jag, float upto) {
uint bs = seed * 16u + uint(b);
float from = 0.1 + 0.65 * hashU(bs * 3u);
float turn = (0.35 + 0.55 * hashU(bs * 3u + 1u))
* (hashU(bs * 3u + 2u) < 0.5 ? -1.0 : 1.0);
float len = (0.15 + 0.25 * hashU(bs + 999u)) * (1.0 - 0.5 * from);
float2 a = boltPoint(from, top, foot, seed, jag);
float2 axis = foot - top;
float c = cos(turn), sn = sin(turn);
float2 dir = float2(axis.x * c - axis.y * sn, axis.x * sn + axis.y * c);
float reach = (upto - from) / (len * c);
return boltDistance(q, a, a + dir * len, bs + 77u, jag, reach);
}
// As in Rain on Glass, the clock is cut into windows, here `every` seconds
// long, with a strike at a random moment in the first half of each, so two
// are never closer than half a window. Each strike's place on the ground,
// x across and z away in kilometres, and its seed come from its window.
struct Strike {
float at;
float2 ground;
uint seed;
};
Strike strikeIn(float k, float aspect, constant Params& p) {
uint w = uint(k);
Strike s;
s.at = k * p.every + 0.5 * p.every * hashU(w * 7u + 1u);
float z = p.distance * (0.55 + 0.9 * hashU(w * 7u + 2u));
s.ground = float2((hashU(w * 7u + 3u) - 0.5) * 0.75 * aspect * z, z);
s.seed = w * 7u + 4u;
return s;
}
// Where a point on the ground or the cloud base is drawn.
float2 toScreen(float2 ground, float height, float aspect) {
return float2(0.5 * aspect + ground.x / ground.y,
HORIZON + (height - CAMERA) / ground.y);
}
// The bolt's light `x` seconds into the strike. The leader takes 0.2 s to
// come down, faint; then the return stroke, and each restrike 0.1 s apart,
// is a sharp pulse that fades.
constant float LEADER = 0.2;
float strokes(float x, constant Params& p) {
if (x < LEADER) return 0.25 * step(0.0, x);
float light = 0.0;
for (int j = 0; j <= 3; j++) {
float y = x - LEADER - 0.1 * float(j);
if (float(j) <= p.restrikes + 0.5 && y >= 0.0)
light += pow(0.8, float(j)) * exp(-y * 14.0);
}
return light;
}
// The light thrown on the clouds and the ground: one rise and one smooth fall
// a strike, however often its bolt flickers. A flash that comes and goes more
// than three times a second can cause seizures; this one never does.
float skyLight(float x) {
float y = x - LEADER;
if (y < 0.0) return 0.0;
return smoothstep(0.0, 0.04, y) * exp(-y * 2.2);
}
// One bolt and its branches, as heat, for the strike `x` seconds old.
float boltHeat(float2 q, Strike s, float x, float px, float aspect,
constant Params& p) {
float2 top = toScreen(s.ground, CEILING, aspect);
top.y = min(top.y, 1.15);
float2 foot = toScreen(s.ground, 0.0, aspect);
// How far down the leader has come, and how bright the channel is.
float down = clamp(x / LEADER, 0.0, 1.0);
float light = strokes(x, p);
float sky = skyLight(x);
// Thinner and dimmer the farther it is.
float near = clamp(0.8 / s.ground.y, 0.3, 1.6);
float width = px * (0.6 + 0.9 * near);
float reach = 0.012 * p.glow * near;
float d = boltDistance(q, top, foot, s.seed, p.jag, down);
float heat = light * near * (1.0 - smoothstep(0.0, width, d)) * 1.3;
heat += (sky + 0.3 * step(x, LEADER) * down) * near * 0.5
* exp(-d / max(reach, px));
// The branches, lit as the leader reaches them; they fade sooner.
for (int b = 0; b < 8; b++) {
if (float(b) >= p.branches) break;
float bd = branchDistance(q, top, foot, s.seed, b, p.jag, down);
float fade = x < LEADER ? 0.7 : 0.45 * exp(-(x - LEADER) * 3.0);
heat += fade * (light + 0.3) * near
* (1.0 - smoothstep(0.0, width * 0.8, bd)) * 0.9;
heat += fade * sky * near * 0.3 * exp(-bd / max(reach * 0.6, px));
}
return heat;
}
// The cloud base, seen from below: a plane CEILING high, so its noise is
// drawn smaller towards the horizon. Its density, and the place on it.
float cloudAt(float2 q, float aspect, float t, thread float2& place) {
float z = (CEILING - CAMERA) / max(q.y - HORIZON, 1e-3);
place = float2((q.x - 0.5 * aspect) * z, z);
float c = fbm(place * float2(1.6, 1.2) + float2(t * 0.03, 0.0));
return smoothstep(0.25, 0.85, c) * exp(-z * 0.08);
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float t = u.time;
float aspect = u.resolution.x / u.resolution.y;
float2 q = float2(uv.x * aspect, uv.y);
float px = 1.0 / u.resolution.y;
bool sky = q.y > HORIZON;
float2 place;
float cloud = sky ? cloudAt(q, aspect, t, place) : 0.0;
// The ground's place, x across and z away.
float zg = CAMERA / max(HORIZON - q.y, 1e-3);
float2 ground = float2((q.x - 0.5 * aspect) * zg, zg);
// A dim sky, darker overhead, and haze along the horizon.
float heat = sky ? 0.07 + 0.05 * cloud - 0.03 * (q.y - HORIZON)
: 0.03 + 0.02 * fbm(ground * 3.0);
heat += 0.06 * exp(-abs(q.y - HORIZON) * 25.0);
float k = floor(t / p.every);
for (int i = 0; i < 4; i++) {
float kk = k - float(i);
if (kk < 0.0) break;
Strike s = strikeIn(kk, aspect, p);
float x = t - s.at;
if (x < 0.0) continue;
float lit = skyLight(x) * clamp(0.9 / s.ground.y, 0.25, 1.5);
if (sky) {
// The clouds glow about the strike, a little everywhere.
float far = length(place - s.ground);
heat += lit * (0.2 + cloud) * (0.25 + exp(-far * 1.2));
} else {
float far = length(ground - s.ground);
heat += lit * 0.35 * exp(-far * 1.5);
// The thunder: a ring on the ground leaving the strike at the
// speed of sound. It reaches you as you hear it.
float r = SOUND * x;
float w = px * max(zg * zg / CAMERA, zg) * 1.5;
float ring = exp(-abs(far - r) / w) * exp(-r * 0.5)
* step(r, length(s.ground) + 0.05);
heat += 0.22 * ring;
}
if (x < 1.5) heat += boltHeat(q, s, x, px, aspect, p);
}
return float4(kiln(heat), 1.0);
}
// Rain, and thunder. Each strike's thunder starts when its ring reaches you:
// its distance over the speed of sound. A near strike cracks first, then
// rumbles; a far one only rumbles, lower, longer and quieter.
// Noise made by joining random values `rate` times a second:
// a lower rate sounds darker.
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;
}
// The thunder `x` seconds after it arrives, from `far` kilometres away.
float thunder(float t, float x, float far, uint seed) {
if (x <= 0.0) return 0.0;
float near = smoothstep(1.6, 0.4, far);
float crack = (noise1(t, 3500.0, seed) * 0.6
+ noise1(t, 1200.0, seed + 1u) * 0.5)
* exp(-x * 12.0) * smoothstep(0.0, 0.004, x) * near;
float env = (1.0 - exp(-x * 5.0)) * exp(-x / (0.9 + 0.6 * far))
* smoothstep(12.0, 9.0, x);
float swell = 0.55 + 0.45 * noise1(t, 2.5, seed + 2u);
float low = 1.0 - 0.15 * far;
float rumble = noise1(t, 50.0 * low, seed + 3u) * 0.6
+ noise1(t, 110.0 * low, seed + 4u) * 0.4
+ noise1(t, 260.0 * low, seed + 5u) * 0.25;
return (crack * 0.7 + rumble * env * swell) * 5.0 / (0.5 + far);
}
float2 sound(float t, constant Params& p) {
float aspect = 16.0 / 9.0;
// The rain: noise with its lowest part taken away.
float2 rain = float2(noise1(t, 9000.0, 1u) - noise1(t, 900.0, 2u) * 0.6,
noise1(t, 9000.0, 4u) - noise1(t, 900.0, 5u) * 0.6);
float2 sum = rain * 0.09;
float k = floor(t / p.every);
for (int i = 0; i < 16; i++) {
float kk = k - float(i);
if (kk < 0.0) break;
Strike s = strikeIn(kk, aspect, p);
float far = length(s.ground);
float x = t - s.at - far / SOUND;
if (x <= 0.0 || x > 12.0) continue;
// Louder on the side it struck, each ear a little different.
float pan = 0.5 + 0.4 * s.ground.x / far;
float a = thunder(t, x, far, s.seed);
float b = thunder(t, x, far, s.seed + 50u);
sum += float2((0.7 * a + 0.3 * b) * (1.2 - pan),
(0.3 * a + 0.7 * b) * (0.2 + pan));
}
// A fade in over the first 0.3 s, so the rain doesn't start with a click.
return tanh(sum * 1.2) * 0.66 * 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] = [5, 1.2, 0.58, 5, 2, 1, 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 Fractals
Press ? for keyboard shortcuts.

