Murmuration
Four thousand starlings over the kiln at dusk, and not one of them knows the shape of the flock. Each bird looks only at the birds near it and follows three rules, keep a little apart, fly the way they fly, stay among them, and the flock's folding, swirling cloud comes out of that. A falcon circles above, and every fifteen seconds it stoops into the flock, which tears open around it and closes again.
· Particles & Simulation · Compute shader
Teaches Boids: separation, alignment, cohesion
Builds on 05 · Cracked Clay (integer hash, sound noise), 12 · Glass Orb (the kiln's dark)
Watch
The idea
A murmuration of starlings looks as if it is steered from somewhere: the whole cloud turns at once, thickens, thins, folds over itself. Nothing steers it. In 1987 Craig Reynolds showed that a flock comes out of three rules, followed by every bird about the birds it can see:
- Separation: don't fly into the bird beside you.
- Alignment: fly the way your neighbours are flying.
- Cohesion: stay among them, steering toward their middle.
He called his birds boids. Each rule needs what the other birds are doing, so every bird has to read the others. In Ray Kiln that is new: update can now take a fifth argument, the swarm, every particle as it was after the last step. swarm.count says how many there are and swarm.at(j) gives particle j. The host keeps the particles in two buffers, reading the last step from one while this step writes the other, so every bird sees the others as they were, whatever order the GPU runs them in, and the frame at a given time is always the same picture.
Every bird looks at all 4,095 others at every step: about 16 million looks, sixty times a second. The GPU does it, because every bird does its looking at the same time.
It is built in five steps, and each step is a shader you can run on its own.
Stage 1 · Birds flying alone
A bird is four floats and one more: where it is and how fast it flies, in a space two units tall from the middle of the picture and as wide as the picture, plus a random number of its own (seed) that sets when its wings beat. spawn puts each of the 4,096 birds somewhere in a loose cloud, flying whichever way a hash of its number says.
update runs for every bird at every step. For now a bird looks only at itself. It is pulled back toward the roost, a point that drifts slowly about the sky (sines of the time), but only once it strays: the pull is zero within about a third of the way to the edge and grows to full strength further out, so the birds stay in the sky without being dragged to one spot. A small random push, hashed from its number and the step, keeps any two from flying quite the same line. Then its speed is held between 0.35 and 0.75 units a second, times speed: a starling never hovers. Without a rule about each other, the birds cross through each other like strangers.
Each bird is drawn as a dot turned to face the way it flies: a short body and two wings swept back, beating (flap is a sine of the time and its seed). A bird is a shadow, not a light: ink says how much of the sky it blocks, the dots add up into an image, and shade darkens the sky by exp(-2.2 × shadow), so where many birds overlap the flock is darkest. fade keeps 45% of the last step, a short blur behind each bird. The sky is the kiln palette from the glow on the horizon up into a deep violet, and in front of it the low hills and the kiln, its door flickering.
stage-1-flying.metal
// Stage 1 · Birds flying alone
// Four thousand starlings, each one a few numbers in a buffer on the GPU: where it is and how fast it flies. At
// every step, sixty a second, each turns a little toward the roost when it strays and flies on. No bird sees any
// other yet, so they cross through each other like strangers, and there is no flock.
//
// @particles 4096
// @param speed 1.0 0.5 1.5 How fast the birds fly
constant float TAU = 6.2831853;
// What one bird is: where it is and how fast it flies (in a space two units tall, as wide as the picture), and a
// random number of its own.
struct Particle {
float2 position;
float2 velocity;
float seed;
};
// Episode 5's integer hash.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// The roost: a point drifting slowly about the sky. Birds that stray far from it are drawn back.
float2 roost(float t, float aspect) {
return float2(aspect * (0.35 * sin(0.11 * t + 0.4) + 0.12 * sin(0.27 * t)),
0.25 + 0.18 * sin(0.17 * t + 1.3));
}
// Each bird starts somewhere in a loose cloud near the roost, flying any way.
Particle spawn(uint id, constant Params& p) {
Particle q;
float a = TAU * hashU(id * 4u);
float r = 0.45 * sqrt(hashU(id * 4u + 1u));
q.position = float2(0.0, 0.3) + float2(cos(a) * 1.6, sin(a)) * r;
float heading = TAU * hashU(id * 4u + 2u);
q.velocity = float2(cos(heading), sin(heading)) * 0.5;
q.seed = hashU(id * 4u + 3u);
return q;
}
// One step, for every bird at once. For now a bird looks only at itself.
void update(thread Particle& q, uint id,
constant Uniforms& u, constant Params& p) {
float dt = u.timeDelta;
float aspect = u.resolution.x / u.resolution.y;
float2 push = float2(0.0);
// Back toward the roost when straying far, so the flock stays in the sky.
float2 home = roost(u.time, aspect) - q.position;
float far = length(home / float2(aspect * 0.8, 1.0));
push += home * smoothstep(0.35, 0.9, far) * 2.0 * p.speed * p.speed;
// A little wobble of its own, so no two birds fly the same line.
uint k = id * 3u + u.frame * 7919u;
push += (float2(hashU(k), hashU(k + 1u)) - 0.5) * 0.6;
q.velocity += push * dt;
// A starling never hovers and never goes past its top speed.
float s = length(q.velocity);
q.velocity *= clamp(s, 0.35, 0.75) * p.speed / max(s, 1e-4);
q.position += q.velocity * dt;
}
// Where to draw a bird (the picture's uv) and how big.
float3 place(Particle q, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
float2 uv = q.position / float2(aspect, 1.0) * 0.5 + 0.5;
return float3(uv, 0.009);
}
// The distance from point p to the segment from a to b.
float segment(float2 p, float2 a, float2 b) {
float2 pa = p - a, ba = b - a;
return length(pa - ba * clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0));
}
// A bird's shadow against the sky: how much of the light it blocks. Turned to face the way it flies, a starling is a
// short body with two wings swept back that beat.
float4 ink(Particle q, float2 at, constant Uniforms& u, constant Params& p) {
float2 f = normalize(q.velocity + 1e-5);
float2 b = float2(dot(at, f), dot(at, float2(-f.y, f.x)));
float flap = sin(u.time * 22.0 + q.seed * TAU);
float2 tip = float2(-0.25 + 0.2 * flap, 0.85);
float d = segment(float2(b.x, abs(b.y)), float2(0.15, 0.0), tip);
d = min(d, segment(b, float2(-0.6, 0.0), float2(0.6, 0.0)));
float body = smoothstep(0.22, 0.08, d);
return float4(float3(body * 0.7), 1.0);
}
// What is left of the last step's shadows: a little, so a bird leaves a short blur behind it as it flies.
float4 fade(float2 uv, Trails previous,
constant Uniforms& u, constant Params& p) {
return previous.at(uv) * 0.45;
}
// The kiln palette from episode 1.
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.78, t));
c = mix(c, c4, smoothstep(0.78, 1.00, t));
return c;
}
// The sky at dusk: the kiln's ramp from the glow on the horizon up into the dark.
float3 sky(float2 uv) {
float glow = exp(-max(uv.y - 0.12, 0.0) * 2.3);
float sun = exp(-length((uv - float2(0.62, 0.1)) * float2(1.0, 2.2)) * 5.0);
float3 c = kiln(0.1 + 0.43 * glow + 0.175 * sun);
// Above the glow, the night coming in: a deep violet.
return c + float3(0.05, 0.035, 0.09) * (1.0 - glow) * 0.75;
}
// The ground: low hills, and the kiln on them with its chimney, its door glowing.
float3 ground(float2 uv, float3 c, constant Uniforms& u, float aspect) {
float x = uv.x * aspect;
float hill = 0.1 + 0.025 * sin(x * 3.1 + 1.0) + 0.015 * sin(x * 7.3);
float2 k = float2(x - 0.3 * aspect, uv.y - hill + 0.01);
float dome = length(k * float2(1.0, 1.25)) - 0.075;
float stack = max(abs(k.x - 0.03) - 0.012, abs(k.y - 0.1) - 0.08);
float solid = min(min(uv.y - hill, dome), stack);
float3 dark = float3(0.035, 0.027, 0.031);
float door = length((k - float2(0.0, 0.02)) * float2(1.6, 1.0)) - 0.025;
float flicker = 0.8 + 0.2 * sin(u.time * 9.0) * sin(u.time * 5.3);
dark += kiln(0.85) * smoothstep(0.004, -0.002, door) * flicker;
dark += kiln(0.6) * exp(-max(door, 0.0) * 60.0) * 0.25 * flicker;
return mix(c, dark, smoothstep(0.002, -0.001, solid));
}
// The picture: the sky, darkened wherever the birds' shadows pile up, then the ground in front.
float4 shade(float2 uv, constant Uniforms& u, constant Params& p,
Trails trails) {
float aspect = u.resolution.x / u.resolution.y;
float shadow = trails.at(uv).r;
float3 c = sky(uv);
c = mix(float3(0.035, 0.027, 0.031), c, exp(-shadow * 2.2));
c = ground(uv, c, u, aspect);
float2 q = uv - 0.5;
c *= 1.0 - dot(q, q) * 0.5;
return float4(c, 1.0);
}Stage 2 · Separation
Now update takes the swarm: void update(thread Particle& q, uint id, constant Uniforms& u, constant Params& p, Swarm swarm). A loop over j from 0 to swarm.count reads each other bird with swarm.at(j) (skipping j == id, itself). Any bird closer than 0.035 units pushes it away along the line between them, d / r × (1 - r / 0.035): hardest when they almost touch, nothing at the edge. separation scales it.
On its own the rule spreads the birds out evenly, every bird keeping its own little patch of sky.
stage-2-separation.metal10 new lines since stage 1
// Stage 2 · Separation
// Now update takes a fifth argument, the swarm: every bird as it was after the last step. A bird looks at all of
// them, and any that is too close pushes it away, harder the closer it is. The first rule keeps birds from
// colliding; on its own it only spreads them out evenly, like people on a crowded square.
//
// @particles 4096
// @param speed 1.0 0.5 1.5 How fast the birds fly
// @param separation 1.0 0.0 2.0 How hard each bird keeps clear of the ones right beside it
constant float TAU = 6.2831853;
// What one bird is: where it is and how fast it flies (in a space two units tall, as wide as the picture), and a
// random number of its own.
struct Particle {
float2 position;
float2 velocity;
float seed;
};
// Episode 5's integer hash.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// The roost: a point drifting slowly about the sky. Birds that stray far from it are drawn back.
float2 roost(float t, float aspect) {
return float2(aspect * (0.35 * sin(0.11 * t + 0.4) + 0.12 * sin(0.27 * t)),
0.25 + 0.18 * sin(0.17 * t + 1.3));
}
// Each bird starts somewhere in a loose cloud near the roost, flying any way.
Particle spawn(uint id, constant Params& p) {
Particle q;
float a = TAU * hashU(id * 4u);
float r = 0.45 * sqrt(hashU(id * 4u + 1u));
q.position = float2(0.0, 0.3) + float2(cos(a) * 1.6, sin(a)) * r;
float heading = TAU * hashU(id * 4u + 2u);
q.velocity = float2(cos(heading), sin(heading)) * 0.5;
q.seed = hashU(id * 4u + 3u);
return q;
}
// One step, for every bird at once. The swarm is read from one buffer while this step writes another, so every bird
// sees the others as they were, whatever order the GPU runs them in.
void update(thread Particle& q, uint id,
constant Uniforms& u, constant Params& p, Swarm swarm) {
float dt = u.timeDelta;
float aspect = u.resolution.x / u.resolution.y;
float close = 0.035;
float2 apart = float2(0.0);
for (uint j = 0; j < swarm.count; j++) {
if (j == id) continue;
float2 d = q.position - swarm.at(j).position;
float r = length(d);
// Separation: away from a bird too close, harder the closer it is.
if (r < close) apart += d / max(r, 1e-4) * (1.0 - r / close);
}
float2 push = float2(0.0);
push += apart * 2.2 * p.separation;
// Back toward the roost when straying far, so the flock stays in the sky.
float2 home = roost(u.time, aspect) - q.position;
float far = length(home / float2(aspect * 0.8, 1.0));
push += home * smoothstep(0.35, 0.9, far) * 2.0 * p.speed * p.speed;
// A little wobble of its own, so no two birds fly the same line.
uint k = id * 3u + u.frame * 7919u;
push += (float2(hashU(k), hashU(k + 1u)) - 0.5) * 0.6;
q.velocity += push * dt;
// A starling never hovers and never goes past its top speed.
float s = length(q.velocity);
q.velocity *= clamp(s, 0.35, 0.75) * p.speed / max(s, 1e-4);
q.position += q.velocity * dt;
}
// Where to draw a bird (the picture's uv) and how big.
float3 place(Particle q, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
float2 uv = q.position / float2(aspect, 1.0) * 0.5 + 0.5;
return float3(uv, 0.009);
}
// The distance from point p to the segment from a to b.
float segment(float2 p, float2 a, float2 b) {
float2 pa = p - a, ba = b - a;
return length(pa - ba * clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0));
}
// A bird's shadow against the sky: how much of the light it blocks. Turned to face the way it flies, a starling is a
// short body with two wings swept back that beat.
float4 ink(Particle q, float2 at, constant Uniforms& u, constant Params& p) {
float2 f = normalize(q.velocity + 1e-5);
float2 b = float2(dot(at, f), dot(at, float2(-f.y, f.x)));
float flap = sin(u.time * 22.0 + q.seed * TAU);
float2 tip = float2(-0.25 + 0.2 * flap, 0.85);
float d = segment(float2(b.x, abs(b.y)), float2(0.15, 0.0), tip);
d = min(d, segment(b, float2(-0.6, 0.0), float2(0.6, 0.0)));
float body = smoothstep(0.22, 0.08, d);
return float4(float3(body * 0.7), 1.0);
}
// What is left of the last step's shadows: a little, so a bird leaves a short blur behind it as it flies.
float4 fade(float2 uv, Trails previous,
constant Uniforms& u, constant Params& p) {
return previous.at(uv) * 0.45;
}
// The kiln palette from episode 1.
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.78, t));
c = mix(c, c4, smoothstep(0.78, 1.00, t));
return c;
}
// The sky at dusk: the kiln's ramp from the glow on the horizon up into the dark.
float3 sky(float2 uv) {
float glow = exp(-max(uv.y - 0.12, 0.0) * 2.3);
float sun = exp(-length((uv - float2(0.62, 0.1)) * float2(1.0, 2.2)) * 5.0);
float3 c = kiln(0.1 + 0.43 * glow + 0.175 * sun);
// Above the glow, the night coming in: a deep violet.
return c + float3(0.05, 0.035, 0.09) * (1.0 - glow) * 0.75;
}
// The ground: low hills, and the kiln on them with its chimney, its door glowing.
float3 ground(float2 uv, float3 c, constant Uniforms& u, float aspect) {
float x = uv.x * aspect;
float hill = 0.1 + 0.025 * sin(x * 3.1 + 1.0) + 0.015 * sin(x * 7.3);
float2 k = float2(x - 0.3 * aspect, uv.y - hill + 0.01);
float dome = length(k * float2(1.0, 1.25)) - 0.075;
float stack = max(abs(k.x - 0.03) - 0.012, abs(k.y - 0.1) - 0.08);
float solid = min(min(uv.y - hill, dome), stack);
float3 dark = float3(0.035, 0.027, 0.031);
float door = length((k - float2(0.0, 0.02)) * float2(1.6, 1.0)) - 0.025;
float flicker = 0.8 + 0.2 * sin(u.time * 9.0) * sin(u.time * 5.3);
dark += kiln(0.85) * smoothstep(0.004, -0.002, door) * flicker;
dark += kiln(0.6) * exp(-max(door, 0.0) * 60.0) * 0.25 * flicker;
return mix(c, dark, smoothstep(0.002, -0.001, solid));
}
// The picture: the sky, darkened wherever the birds' shadows pile up, then the ground in front.
float4 shade(float2 uv, constant Uniforms& u, constant Params& p,
Trails trails) {
float aspect = u.resolution.x / u.resolution.y;
float shadow = trails.at(uv).r;
float3 c = sky(uv);
c = mix(float3(0.035, 0.027, 0.031), c, exp(-shadow * 2.2));
c = ground(uv, c, u, aspect);
float2 q = uv - 0.5;
c *= 1.0 - dot(q, q) * 0.5;
return float4(c, 1.0);
}1 line from stage 1 that are gone
constant Uniforms& u, constant Params& p) {Stage 3 · Alignment
A bird's neighbours are the birds within sight of it (a fraction of the picture's height). The loop adds up their velocities, and the bird turns toward their average: (heading / seen - velocity) × 1.6 × alignment is added to its push. Each bird copies its neighbours, they copy theirs, and a turn passes through the crowd like a rumour. Streams form that flow together, but nothing pulls a stream back once it drifts from the others.
stage-3-alignment.metal11 new lines since stage 2
// Stage 3 · Alignment
// The second rule: a bird turns toward the average velocity of the birds it can see, those within its sight. Each
// copies its neighbours, they copy theirs, and a turn spreads through the crowd like a rumour. Streams form that flow
// together, but nothing yet pulls a stream back to the others.
//
// @particles 4096
// @param speed 1.0 0.5 1.5 How fast the birds fly
// @param separation 1.0 0.0 2.0 How hard each bird keeps clear of the ones right beside it
// @param alignment 1.0 0.0 2.0 How strongly each bird turns to fly the way its neighbours fly
// @param sight 0.08 0.04 0.16 How far a bird sees its neighbours, as a fraction of the picture's height
constant float TAU = 6.2831853;
// What one bird is: where it is and how fast it flies (in a space two units tall, as wide as the picture), and a
// random number of its own.
struct Particle {
float2 position;
float2 velocity;
float seed;
};
// Episode 5's integer hash.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// The roost: a point drifting slowly about the sky. Birds that stray far from it are drawn back.
float2 roost(float t, float aspect) {
return float2(aspect * (0.35 * sin(0.11 * t + 0.4) + 0.12 * sin(0.27 * t)),
0.25 + 0.18 * sin(0.17 * t + 1.3));
}
// Each bird starts somewhere in a loose cloud near the roost, flying any way.
Particle spawn(uint id, constant Params& p) {
Particle q;
float a = TAU * hashU(id * 4u);
float r = 0.45 * sqrt(hashU(id * 4u + 1u));
q.position = float2(0.0, 0.3) + float2(cos(a) * 1.6, sin(a)) * r;
float heading = TAU * hashU(id * 4u + 2u);
q.velocity = float2(cos(heading), sin(heading)) * 0.5;
q.seed = hashU(id * 4u + 3u);
return q;
}
// One step, for every bird at once. The birds within its sight are its neighbours: it adds up their velocities.
void update(thread Particle& q, uint id,
constant Uniforms& u, constant Params& p, Swarm swarm) {
float dt = u.timeDelta;
float aspect = u.resolution.x / u.resolution.y;
float sight = p.sight * 2.0;
float close = 0.035;
float2 apart = float2(0.0), heading = float2(0.0);
float seen = 0.0;
for (uint j = 0; j < swarm.count; j++) {
if (j == id) continue;
Particle o = swarm.at(j);
float2 d = q.position - o.position;
float r = length(d);
if (r >= sight) continue;
seen += 1.0;
heading += o.velocity;
// Separation: away from a bird too close, harder the closer it is.
if (r < close) apart += d / max(r, 1e-4) * (1.0 - r / close);
}
float2 push = float2(0.0);
push += apart * 2.2 * p.separation;
if (seen > 0.0) {
// Alignment: turn toward the neighbours' average velocity.
push += (heading / seen - q.velocity) * 1.6 * p.alignment;
}
// Back toward the roost when straying far, so the flock stays in the sky.
float2 home = roost(u.time, aspect) - q.position;
float far = length(home / float2(aspect * 0.8, 1.0));
push += home * smoothstep(0.35, 0.9, far) * 2.0 * p.speed * p.speed;
// A little wobble of its own, so no two birds fly the same line.
uint k = id * 3u + u.frame * 7919u;
push += (float2(hashU(k), hashU(k + 1u)) - 0.5) * 0.6;
q.velocity += push * dt;
// A starling never hovers and never goes past its top speed.
float s = length(q.velocity);
q.velocity *= clamp(s, 0.35, 0.75) * p.speed / max(s, 1e-4);
q.position += q.velocity * dt;
}
// Where to draw a bird (the picture's uv) and how big.
float3 place(Particle q, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
float2 uv = q.position / float2(aspect, 1.0) * 0.5 + 0.5;
return float3(uv, 0.009);
}
// The distance from point p to the segment from a to b.
float segment(float2 p, float2 a, float2 b) {
float2 pa = p - a, ba = b - a;
return length(pa - ba * clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0));
}
// A bird's shadow against the sky: how much of the light it blocks. Turned to face the way it flies, a starling is a
// short body with two wings swept back that beat.
float4 ink(Particle q, float2 at, constant Uniforms& u, constant Params& p) {
float2 f = normalize(q.velocity + 1e-5);
float2 b = float2(dot(at, f), dot(at, float2(-f.y, f.x)));
float flap = sin(u.time * 22.0 + q.seed * TAU);
float2 tip = float2(-0.25 + 0.2 * flap, 0.85);
float d = segment(float2(b.x, abs(b.y)), float2(0.15, 0.0), tip);
d = min(d, segment(b, float2(-0.6, 0.0), float2(0.6, 0.0)));
float body = smoothstep(0.22, 0.08, d);
return float4(float3(body * 0.7), 1.0);
}
// What is left of the last step's shadows: a little, so a bird leaves a short blur behind it as it flies.
float4 fade(float2 uv, Trails previous,
constant Uniforms& u, constant Params& p) {
return previous.at(uv) * 0.45;
}
// The kiln palette from episode 1.
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.78, t));
c = mix(c, c4, smoothstep(0.78, 1.00, t));
return c;
}
// The sky at dusk: the kiln's ramp from the glow on the horizon up into the dark.
float3 sky(float2 uv) {
float glow = exp(-max(uv.y - 0.12, 0.0) * 2.3);
float sun = exp(-length((uv - float2(0.62, 0.1)) * float2(1.0, 2.2)) * 5.0);
float3 c = kiln(0.1 + 0.43 * glow + 0.175 * sun);
// Above the glow, the night coming in: a deep violet.
return c + float3(0.05, 0.035, 0.09) * (1.0 - glow) * 0.75;
}
// The ground: low hills, and the kiln on them with its chimney, its door glowing.
float3 ground(float2 uv, float3 c, constant Uniforms& u, float aspect) {
float x = uv.x * aspect;
float hill = 0.1 + 0.025 * sin(x * 3.1 + 1.0) + 0.015 * sin(x * 7.3);
float2 k = float2(x - 0.3 * aspect, uv.y - hill + 0.01);
float dome = length(k * float2(1.0, 1.25)) - 0.075;
float stack = max(abs(k.x - 0.03) - 0.012, abs(k.y - 0.1) - 0.08);
float solid = min(min(uv.y - hill, dome), stack);
float3 dark = float3(0.035, 0.027, 0.031);
float door = length((k - float2(0.0, 0.02)) * float2(1.6, 1.0)) - 0.025;
float flicker = 0.8 + 0.2 * sin(u.time * 9.0) * sin(u.time * 5.3);
dark += kiln(0.85) * smoothstep(0.004, -0.002, door) * flicker;
dark += kiln(0.6) * exp(-max(door, 0.0) * 60.0) * 0.25 * flicker;
return mix(c, dark, smoothstep(0.002, -0.001, solid));
}
// The picture: the sky, darkened wherever the birds' shadows pile up, then the ground in front.
float4 shade(float2 uv, constant Uniforms& u, constant Params& p,
Trails trails) {
float aspect = u.resolution.x / u.resolution.y;
float shadow = trails.at(uv).r;
float3 c = sky(uv);
c = mix(float3(0.035, 0.027, 0.031), c, exp(-shadow * 2.2));
c = ground(uv, c, u, aspect);
float2 q = uv - 0.5;
c *= 1.0 - dot(q, q) * 0.5;
return float4(c, 1.0);
}2 lines from stage 2 that are gone
float2 apart = float2(0.0);
⋯
float2 d = q.position - swarm.at(j).position;Stage 4 · Cohesion
The loop also adds up the neighbours' positions, and the bird steers toward their middle: (middle / seen - position) × 3 × cohesion. Streams that drift apart are pulled back together. With all three rules the flock appears: dense where birds crowd in, thin at its edges, folding and turning as one, while no bird knows its shape. Set any one of the three to 0 and watch what the flock loses.
stage-4-cohesion.metal3 new lines since stage 3
// Stage 4 · Cohesion
// The third rule: a bird steers toward the middle of the birds it can see. Streams that drift apart are pulled back
// together, and with all three rules the flock appears: thick where birds crowd in, thin at its edges, folding and
// turning as one, with no bird knowing its shape.
//
// @particles 4096
// @param speed 1.0 0.5 1.5 How fast the birds fly
// @param separation 1.0 0.0 2.0 How hard each bird keeps clear of the ones right beside it
// @param alignment 1.0 0.0 2.0 How strongly each bird turns to fly the way its neighbours fly
// @param cohesion 1.0 0.0 2.0 How strongly each bird steers toward the middle of its neighbours
// @param sight 0.08 0.04 0.16 How far a bird sees its neighbours, as a fraction of the picture's height
constant float TAU = 6.2831853;
// What one bird is: where it is and how fast it flies (in a space two units tall, as wide as the picture), and a
// random number of its own.
struct Particle {
float2 position;
float2 velocity;
float seed;
};
// Episode 5's integer hash.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// The roost: a point drifting slowly about the sky. Birds that stray far from it are drawn back.
float2 roost(float t, float aspect) {
return float2(aspect * (0.35 * sin(0.11 * t + 0.4) + 0.12 * sin(0.27 * t)),
0.25 + 0.18 * sin(0.17 * t + 1.3));
}
// Each bird starts somewhere in a loose cloud near the roost, flying any way.
Particle spawn(uint id, constant Params& p) {
Particle q;
float a = TAU * hashU(id * 4u);
float r = 0.45 * sqrt(hashU(id * 4u + 1u));
q.position = float2(0.0, 0.3) + float2(cos(a) * 1.6, sin(a)) * r;
float heading = TAU * hashU(id * 4u + 2u);
q.velocity = float2(cos(heading), sin(heading)) * 0.5;
q.seed = hashU(id * 4u + 3u);
return q;
}
// One step, for every bird at once. A bird looks at every other one as it was after the last step: those it can see
// are its neighbours, and the three rules each give it a push.
void update(thread Particle& q, uint id,
constant Uniforms& u, constant Params& p, Swarm swarm) {
float dt = u.timeDelta;
float aspect = u.resolution.x / u.resolution.y;
float sight = p.sight * 2.0;
float close = 0.035;
float2 apart = float2(0.0), heading = float2(0.0), middle = float2(0.0);
float seen = 0.0;
for (uint j = 0; j < swarm.count; j++) {
if (j == id) continue;
Particle o = swarm.at(j);
float2 d = q.position - o.position;
float r = length(d);
if (r >= sight) continue;
seen += 1.0;
heading += o.velocity;
middle += o.position;
// Separation: away from a bird too close, harder the closer it is.
if (r < close) apart += d / max(r, 1e-4) * (1.0 - r / close);
}
float2 push = float2(0.0);
push += apart * 2.2 * p.separation;
if (seen > 0.0) {
// Alignment: turn toward the neighbours' average velocity.
push += (heading / seen - q.velocity) * 1.6 * p.alignment;
// Cohesion: steer toward the neighbours' middle.
push += (middle / seen - q.position) * 3.0 * p.cohesion;
}
// Back toward the roost when straying far, so the flock stays in the sky.
float2 home = roost(u.time, aspect) - q.position;
float far = length(home / float2(aspect * 0.8, 1.0));
push += home * smoothstep(0.35, 0.9, far) * 2.0 * p.speed * p.speed;
// A little wobble of its own, so no two birds fly the same line.
uint k = id * 3u + u.frame * 7919u;
push += (float2(hashU(k), hashU(k + 1u)) - 0.5) * 0.6;
q.velocity += push * dt;
// A starling never hovers and never goes past its top speed.
float s = length(q.velocity);
q.velocity *= clamp(s, 0.35, 0.75) * p.speed / max(s, 1e-4);
q.position += q.velocity * dt;
}
// Where to draw a bird (the picture's uv) and how big.
float3 place(Particle q, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
float2 uv = q.position / float2(aspect, 1.0) * 0.5 + 0.5;
return float3(uv, 0.009);
}
// The distance from point p to the segment from a to b.
float segment(float2 p, float2 a, float2 b) {
float2 pa = p - a, ba = b - a;
return length(pa - ba * clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0));
}
// A bird's shadow against the sky: how much of the light it blocks. Turned to face the way it flies, a starling is a
// short body with two wings swept back that beat.
float4 ink(Particle q, float2 at, constant Uniforms& u, constant Params& p) {
float2 f = normalize(q.velocity + 1e-5);
float2 b = float2(dot(at, f), dot(at, float2(-f.y, f.x)));
float flap = sin(u.time * 22.0 + q.seed * TAU);
float2 tip = float2(-0.25 + 0.2 * flap, 0.85);
float d = segment(float2(b.x, abs(b.y)), float2(0.15, 0.0), tip);
d = min(d, segment(b, float2(-0.6, 0.0), float2(0.6, 0.0)));
float body = smoothstep(0.22, 0.08, d);
return float4(float3(body * 0.7), 1.0);
}
// What is left of the last step's shadows: a little, so a bird leaves a short blur behind it as it flies.
float4 fade(float2 uv, Trails previous,
constant Uniforms& u, constant Params& p) {
return previous.at(uv) * 0.45;
}
// The kiln palette from episode 1.
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.78, t));
c = mix(c, c4, smoothstep(0.78, 1.00, t));
return c;
}
// The sky at dusk: the kiln's ramp from the glow on the horizon up into the dark.
float3 sky(float2 uv) {
float glow = exp(-max(uv.y - 0.12, 0.0) * 2.3);
float sun = exp(-length((uv - float2(0.62, 0.1)) * float2(1.0, 2.2)) * 5.0);
float3 c = kiln(0.1 + 0.43 * glow + 0.175 * sun);
// Above the glow, the night coming in: a deep violet.
return c + float3(0.05, 0.035, 0.09) * (1.0 - glow) * 0.75;
}
// The ground: low hills, and the kiln on them with its chimney, its door glowing.
float3 ground(float2 uv, float3 c, constant Uniforms& u, float aspect) {
float x = uv.x * aspect;
float hill = 0.1 + 0.025 * sin(x * 3.1 + 1.0) + 0.015 * sin(x * 7.3);
float2 k = float2(x - 0.3 * aspect, uv.y - hill + 0.01);
float dome = length(k * float2(1.0, 1.25)) - 0.075;
float stack = max(abs(k.x - 0.03) - 0.012, abs(k.y - 0.1) - 0.08);
float solid = min(min(uv.y - hill, dome), stack);
float3 dark = float3(0.035, 0.027, 0.031);
float door = length((k - float2(0.0, 0.02)) * float2(1.6, 1.0)) - 0.025;
float flicker = 0.8 + 0.2 * sin(u.time * 9.0) * sin(u.time * 5.3);
dark += kiln(0.85) * smoothstep(0.004, -0.002, door) * flicker;
dark += kiln(0.6) * exp(-max(door, 0.0) * 60.0) * 0.25 * flicker;
return mix(c, dark, smoothstep(0.002, -0.001, solid));
}
// The picture: the sky, darkened wherever the birds' shadows pile up, then the ground in front.
float4 shade(float2 uv, constant Uniforms& u, constant Params& p,
Trails trails) {
float aspect = u.resolution.x / u.resolution.y;
float shadow = trails.at(uv).r;
float3 c = sky(uv);
c = mix(float3(0.035, 0.027, 0.031), c, exp(-shadow * 2.2));
c = ground(uv, c, u, aspect);
float2 q = uv - 0.5;
c *= 1.0 - dot(q, q) * 0.5;
return float4(c, 1.0);
}1 line from stage 3 that are gone
float2 apart = float2(0.0), heading = float2(0.0);Stage 5 · Murmuration
The flock's danger is the falcon, and the falcon is particle 0. It has a falcon field set to 1 by spawn, and update flies it with its own rules (flyFalcon): most of the time it steers toward a wide circle high above the flock. Every 15 seconds, for 4 of them, it stoops: it reads the whole swarm, takes the middle of all the starlings as its target, and dives at up to 1.8 units a second. The stoops start at 11, 26, 41 and 56 seconds, whatever the sliders say. The starlings now loop from j = 1, so the falcon is never one of their neighbours.
Every starling also reads the falcon, swarm.at(0), and turns away from it hard when it is within about a sixth of the picture's height, (1 - smoothstep(0.05, 0.32, distance))² times fear. The birds nearest the falcon swerve, their neighbours align with them a moment later, and the flock tears open around the falcon in a wave and closes again behind it. The falcon is drawn larger, its wings swept back and held still.
dusk moves the sky from the last of the sunset to deep night, the glow lower and dimmer.
the kiln palette and shade(), from the finished shader
// The kiln palette from episode 1.
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.78, t));
c = mix(c, c4, smoothstep(0.78, 1.00, t));
return c;
}
// The picture: the sky, darkened wherever the birds' shadows pile up, so the thick of the flock reads as a dark
// cloud and its edges as a haze of single birds; then the ground in front.
float4 shade(float2 uv, constant Uniforms& u, constant Params& p,
Trails trails) {
float aspect = u.resolution.x / u.resolution.y;
float shadow = trails.at(uv).r;
float3 c = sky(uv, p);
c = mix(float3(0.035, 0.027, 0.031), c, exp(-shadow * 2.2));
c = ground(uv, c, u, aspect);
float2 q = uv - 0.5;
c *= 1.0 - dot(q, q) * 0.5;
return float4(c, 1.0);
}episode-19-murmuration.metal, the whole file80 new lines since stage 4
// Ray Kiln · Episode 19 · Murmuration
//
// @episode 19 Murmuration
// @length 60
// @variation 30
// @variations 2,3,5,8,9,10,11,12
// @sweeps separation, alignment, dusk
// @still 26.0
// @teaches Boids: separation, alignment, cohesion
// @category Compute
// @tags boids, flocking, particles, compute shaders, swarm, steering, procedural sound
// @final Murmuration | Starlings over the kiln at dusk, every bird steering by its neighbours alone, the flock folding away each time the falcon stoops.
//
// Starlings over the kiln at dusk. No bird knows the shape of the flock: each one looks only at the birds near it and
// follows three rules, keep a little apart from them (separation), fly the way they fly (alignment) and stay among
// them (cohesion). That is Craig Reynolds' boids, from 1987, and the flock's swirling shape comes out of it. Every bird
// reads every other one, so update takes the swarm: all the birds as they were after the last step. A falcon is
// one of the particles too, number 0. It circles, and every fifteen seconds it stoops into the flock, which folds
// away from it in a wave.
// 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
//
// @particles 4096
// @param speed 1.0 0.5 1.5 How fast the birds fly
// @param separation 1.0 0.0 2.0 How hard each bird keeps clear of the ones right beside it
// @param alignment 1.0 0.0 2.0 How strongly each bird turns to fly the way its neighbours fly
// @param cohesion 1.0 0.0 2.0 How strongly each bird steers toward the middle of its neighbours
// @param sight 0.08 0.04 0.16 How far a bird sees its neighbours, as a fraction of the picture's height
// @param fear 1.0 0.0 2.0 How hard the flock turns away from the falcon
// @param dusk 0.5 0.0 1.0 From the last of the sunset to deep night
// @param volume 0.8 0.0 1.0 Loudness of the sound
constant float TAU = 6.2831853;
// The falcon stoops once every STOOP seconds, for DIVE of them.
constant float STOOP = 15.0;
constant float DIVE = 4.0;
// What one bird is: where it is and how fast it flies (in a space two units tall, as wide as the picture), whether
// it is the falcon (1) or a starling (0), and a random number of its own.
struct Particle {
float2 position;
float2 velocity;
float falcon;
float seed;
};
// Episode 5's integer hash.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// The roost: a point drifting slowly about the sky. Birds that stray far from it are drawn back.
float2 roost(float t, float aspect) {
return float2(aspect * (0.35 * sin(0.11 * t + 0.4) + 0.12 * sin(0.27 * t)),
0.25 + 0.18 * sin(0.17 * t + 1.3));
}
// How far into a stoop the falcon is at time t: 0 while it circles, rising to 1 and back down across the dive.
float stoopAt(float t) {
float x = fmod(t, STOOP) - (STOOP - DIVE);
return x > 0.0 ? sin(3.1415927 * x / DIVE) : 0.0;
}
// The falcon's circle, high above the flock, when it isn't diving.
float2 circling(float t, float aspect) {
return float2(aspect * 0.55 * cos(0.31 * t), 0.78 + 0.1 * sin(0.62 * t));
}
// Each bird starts somewhere in a loose cloud near the roost, flying any way. Number 0 is the falcon.
Particle spawn(uint id, constant Params& p) {
Particle q;
float a = TAU * hashU(id * 4u);
float r = 0.45 * sqrt(hashU(id * 4u + 1u));
q.position = float2(0.0, 0.3) + float2(cos(a) * 1.6, sin(a)) * r;
float heading = TAU * hashU(id * 4u + 2u);
q.velocity = float2(cos(heading), sin(heading)) * 0.5;
q.falcon = id == 0 ? 1.0 : 0.0;
q.seed = hashU(id * 4u + 3u);
if (id == 0) q.position = float2(-1.5, 0.8);
return q;
}
// The falcon's step: steer toward its circle, or while it stoops, toward the middle of the flock, fast.
void flyFalcon(thread Particle& q, float t, float aspect, Swarm swarm) {
float dive = stoopAt(t);
float2 goal = circling(t, aspect);
if (dive > 0.0) {
float2 middle = float2(0.0);
for (uint j = 1; j < swarm.count; j++) middle += swarm.at(j).position;
goal = middle / float(swarm.count - 1);
}
float2 want = normalize(goal - q.position + 1e-5) * (0.7 + 1.1 * dive);
q.velocity += (want - q.velocity) * 2.5 / 60.0;
}
// One step, for every bird at once. A starling looks at every other bird as it was after the last step: those it can
// see are its neighbours, and the three rules each give it a push.
void update(thread Particle& q, uint id,
constant Uniforms& u, constant Params& p, Swarm swarm) {
float dt = u.timeDelta;
float aspect = u.resolution.x / u.resolution.y;
if (q.falcon > 0.5) {
flyFalcon(q, u.time, aspect, swarm);
q.position += q.velocity * dt;
return;
}
float sight = p.sight * 2.0;
float close = 0.035;
float2 apart = float2(0.0), heading = float2(0.0), middle = float2(0.0);
float seen = 0.0;
for (uint j = 1; j < swarm.count; j++) {
if (j == id) continue;
Particle o = swarm.at(j);
float2 d = q.position - o.position;
float r = length(d);
if (r >= sight) continue;
seen += 1.0;
heading += o.velocity;
middle += o.position;
// Separation: away from a bird too close, harder the closer it is.
if (r < close) apart += d / max(r, 1e-4) * (1.0 - r / close);
}
float2 push = float2(0.0);
push += apart * 2.2 * p.separation;
if (seen > 0.0) {
// Alignment: turn toward the neighbours' average velocity.
push += (heading / seen - q.velocity) * 1.6 * p.alignment;
// Cohesion: steer toward the neighbours' middle.
push += (middle / seen - q.position) * 3.0 * p.cohesion;
}
// Back toward the roost when straying far, so the flock stays in the sky.
float2 home = roost(u.time, aspect) - q.position;
float far = length(home / float2(aspect * 0.8, 1.0));
push += home * smoothstep(0.35, 0.9, far) * 2.0 * p.speed * p.speed;
// Away from the falcon, hard, when it is near: the flock folds open where it passes.
Particle hawk = swarm.at(0);
float2 away = q.position - hawk.position;
float danger = 1.0 - smoothstep(0.05, 0.32, length(away));
push += normalize(away + 1e-5) * danger * danger * 6.0 * p.fear;
// A little wobble of its own, so no two birds fly the same line.
uint k = id * 3u + u.frame * 7919u;
push += (float2(hashU(k), hashU(k + 1u)) - 0.5) * 0.6;
q.velocity += push * dt;
// A starling never hovers and never goes past its top speed.
float s = length(q.velocity);
q.velocity *= clamp(s, 0.35, 0.75) * p.speed / max(s, 1e-4);
q.position += q.velocity * dt;
}
// Where to draw a bird (the picture's uv) and how big: the falcon is larger.
float3 place(Particle q, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
float2 uv = q.position / float2(aspect, 1.0) * 0.5 + 0.5;
return float3(uv, q.falcon > 0.5 ? 0.034 : 0.009);
}
// The distance from point p to the segment from a to b.
float segment(float2 p, float2 a, float2 b) {
float2 pa = p - a, ba = b - a;
return length(pa - ba * clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0));
}
// A bird's shadow against the sky: how much of the light it blocks. Turned to face the way it flies, a starling is a
// short body with two wings swept back that beat; the falcon's wings are longer and held still in its stoop.
float4 ink(Particle q, float2 at, constant Uniforms& u, constant Params& p) {
float2 f = normalize(q.velocity + 1e-5);
float2 b = float2(dot(at, f), dot(at, float2(-f.y, f.x)));
float flap = sin(u.time * 22.0 + q.seed * TAU);
float sweep = q.falcon > 0.5 ? -0.55 : -0.25 + 0.2 * flap;
float2 tip = float2(sweep, 0.85);
float d = segment(float2(b.x, abs(b.y)), float2(0.15, 0.0), tip);
d = min(d, segment(b, float2(-0.6, 0.0), float2(0.6, 0.0)));
float body = smoothstep(0.22, 0.08, d);
return float4(float3(body * 0.7), 1.0);
}
// What is left of the last step's shadows: a little, so a bird leaves a short blur behind it as it flies.
float4 fade(float2 uv, Trails previous,
constant Uniforms& u, constant Params& p) {
return previous.at(uv) * 0.45;
}
// The kiln palette from episode 1.
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.78, t));
c = mix(c, c4, smoothstep(0.78, 1.00, t));
return c;
}
// The sky at dusk: the kiln's ramp from the glow on the horizon up into the dark, lower and dimmer as dusk deepens.
float3 sky(float2 uv, constant Params& p) {
float glow = exp(-max(uv.y - 0.12, 0.0) * (1.4 + 1.8 * p.dusk));
float sun = exp(-length((uv - float2(0.62, 0.1)) * float2(1.0, 2.2)) * 5.0);
float high = 0.58 - 0.3 * p.dusk;
float3 c = kiln(0.1 + high * glow + 0.25 * sun * (1.0 - 0.6 * p.dusk));
// Above the glow, the night coming in: a deep violet.
return c + float3(0.05, 0.035, 0.09) * (1.0 - glow) * (1.0 - 0.5 * p.dusk);
}
// The ground: low hills, and the kiln on them with its chimney, its door glowing.
float3 ground(float2 uv, float3 c, constant Uniforms& u, float aspect) {
float x = uv.x * aspect;
float hill = 0.1 + 0.025 * sin(x * 3.1 + 1.0) + 0.015 * sin(x * 7.3);
float2 k = float2(x - 0.3 * aspect, uv.y - hill + 0.01);
float dome = length(k * float2(1.0, 1.25)) - 0.075;
float stack = max(abs(k.x - 0.03) - 0.012, abs(k.y - 0.1) - 0.08);
float solid = min(min(uv.y - hill, dome), stack);
float3 dark = float3(0.035, 0.027, 0.031);
float door = length((k - float2(0.0, 0.02)) * float2(1.6, 1.0)) - 0.025;
float flicker = 0.8 + 0.2 * sin(u.time * 9.0) * sin(u.time * 5.3);
dark += kiln(0.85) * smoothstep(0.004, -0.002, door) * flicker;
dark += kiln(0.6) * exp(-max(door, 0.0) * 60.0) * 0.25 * flicker;
return mix(c, dark, smoothstep(0.002, -0.001, solid));
}
// The picture: the sky, darkened wherever the birds' shadows pile up, so the thick of the flock reads as a dark
// cloud and its edges as a haze of single birds; then the ground in front.
float4 shade(float2 uv, constant Uniforms& u, constant Params& p,
Trails trails) {
float aspect = u.resolution.x / u.resolution.y;
float shadow = trails.at(uv).r;
float3 c = sky(uv, p);
c = mix(float3(0.035, 0.027, 0.031), c, exp(-shadow * 2.2));
c = ground(uv, c, u, aspect);
float2 q = uv - 0.5;
c *= 1.0 - dot(q, q) * 0.5;
return float4(c, 1.0);
}
// A smooth random value from 0 to 1 that changes `rate` times a second: two hashed values eased into each other.
float noise1(float t, float rate, uint salt) {
float x = t * rate;
float i = floor(x);
float f = x - i;
f = f * f * (3.0 - 2.0 * f);
uint n = uint(int(i) + 1000000);
return mix(hashU(n * 31u + salt), hashU((n + 1u) * 31u + salt), f);
}
// The sound follows the falcon's clock, the one thing here that is a function of time alone: a rush of wings (noise
// smoothed at a few rates, so it hisses rather than crackles) that swells as the flock folds away from each stoop and
// drifts from side to side once a stoop; starlings' chirps, a few a second and many in the alarm of a stoop; and the
// kiln's low hum under it all.
float2 sound(float t, constant Params& p) {
float dive = stoopAt(t);
// The wings: louder in a stoop, and a little with how hard the birds turn to keep together.
float wings = 0.25 + 0.75 * dive * p.fear + 0.1 * p.alignment;
float rush = (noise1(t, 1800.0, 3u) - 0.5)
+ 0.6 * (noise1(t, 4100.0, 7u) - 0.5)
+ 0.4 * (noise1(t, 650.0, 11u) - 0.5);
rush *= wings * (0.7 + 0.3 * noise1(t, 2.5, 13u)) * 0.5 * p.speed;
float side = 0.5 + 0.35 * sin(TAU * t / STOOP);
float2 air = float2(1.0 - side, side) * rush;
// Chirps: in slots of 1/14 s, a slot sometimes holds a chirp, a short whistle gliding down; more in a stoop. Each
// starts and ends inside its slot, so none is cut off.
float rate = 14.0;
float slot = floor(t * rate);
uint s = uint(int(slot) + 1000000);
float chirp = 0.0;
float pan = 0.5;
if (hashU(s * 5u) < 0.25 + 0.6 * dive) {
float age = t - (slot + 0.3 * hashU(s * 5u + 1u)) / rate;
float pitch = 2600.0 + 1800.0 * hashU(s * 5u + 2u);
float len = 0.03 + 0.018 * hashU(s * 5u + 3u);
float rise = smoothstep(0.0, 0.004, age);
float env = age > 0.0 ? rise * smoothstep(len, len * 0.4, age) : 0.0;
// The glide: the pitch falls to a third of where it started, so the phase is the integral of a falling frequency.
chirp = sin(TAU * pitch * (age - age * age / (3.0 * len))) * env * 0.16;
pan = hashU(s * 5u + 4u);
}
float2 birds = float2(1.0 - pan, pan) * chirp;
// The hum: A1 and E2, slowly breathing.
float hum = sin(TAU * 55.0 * t) + 0.5 * sin(TAU * 82.41 * t + 1.0);
hum *= 0.06 * (0.8 + 0.2 * sin(0.35 * t));
float2 v = air * 3.0 + birds * 1.7 + float2(hum) * 1.7;
// tanh rounds off the loudest moments instead of letting them clip.
float fadeIn = smoothstep(0.0, 2.0, t);
return tanh(v * 1.3) / 1.5 * fadeIn * p.volume;
}8 lines from stage 4 that are gone
for (uint j = 0; j < swarm.count; j++) {
⋯
return float3(uv, 0.009);
⋯
float2 tip = float2(-0.25 + 0.2 * flap, 0.85);
⋯
float3 sky(float2 uv) {
float glow = exp(-max(uv.y - 0.12, 0.0) * 2.3);
⋯
float3 c = kiln(0.1 + 0.43 * glow + 0.175 * sun);
⋯
return c + float3(0.05, 0.035, 0.09) * (1.0 - glow) * 0.75;
⋯
float3 c = sky(uv);The sound
The sound is a function of time alone, so it can't hear the birds (they are state on the GPU). It follows the falcon's clock instead, which is the same 15 seconds as the picture's.
- Wings. A rush of air: noise smoothed at 650, 1800 and 4100 changes a second, so it hisses rather than crackles. It is quiet while the falcon circles and swells through each stoop, with
fear, as the flock folds away. It drifts from side to side once every 15 seconds. - Chirps. Starlings' calls, short whistles between 2.6 and 4.4 kHz that glide down to a third of their pitch, each in a slot of 1/14 s and panned to a random side. A quarter of the slots hold one while the falcon circles, and most of them in the alarm of a stoop. Each starts and ends inside its slot, so none is cut off.
- The kiln's hum. A1 and E2, 55 and 82.4 Hz, slowly breathing.
Everything goes through tanh and fades in over the first two seconds. At the defaults the peak is about -6.2 dBFS and the average (RMS) about -21 dBFS.
New in Ray Kiln
Particles that see each other, and the particle inspector. A particle's update can now take Swarm swarm as a fifth argument: every particle as it was after the last step (swarm.count, swarm.at(j)), in the browser and in raykiln's renders now, and in the Mac app with its next release. Without it, a particle only knew about itself. The host reads the last step from one buffer while the step writes the other, so the result doesn't depend on the order the GPU runs them in. In the playground, hover over Swarm in the code to read what it holds.
And in the playground, a particle episode now has Inspect (or the I key). Turn it on and click near a dot: the particle whose dot is nearest is found on the GPU, its fields are read back from the particle buffer, and they are shown under the picture by name, with a ring on its dot, read again every frame as it moves. Pause, and you can read every number. It is free.
In this episode, click a starling during a stoop and watch its velocity swing round as the falcon passes. Click the large bird and you are reading particle 0, the falcon, with falcon at 1.000.
Try this
- Set
separationto 0: nothing keeps the birds apart, and the flock collapses into a few dark points, every bird on the same spot. - Set
alignmentto 0: the birds still crowd together but no longer fly together, a buzzing swarm instead of a flock. - Set
sightto 0.04, then to 0.16: short-sighted birds make a loose, ragged flock, long-sighted ones tighter, smoother clouds. - Set
fearto 2 and pause in a stoop: a hole the shape of the falcon's danger, opened in the flock. - In the loop in
update, changej < swarm.counttoj < 200: each bird sees only the first 200, so nothing keeps the rest apart and the flock pulls into one dark knot. - In
update, changeq.velocity *= clamp(s, 0.35, 0.75) * p.speed / max(s, 1e-4);toq.velocity *= clamp(s, 0.0, 0.75) * p.speed / max(s, 1e-4);: birds may now slow down, and the flock slows into a loose cloud that drifts rather than sweeps.
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.5 to 1.5 | How fast the birds fly |
separation | 1.0 | 0.0 to 2.0 | How hard each bird keeps clear of the ones right beside it |
alignment | 1.0 | 0.0 to 2.0 | How strongly each bird turns to fly the way its neighbours fly |
cohesion | 1.0 | 0.0 to 2.0 | How strongly each bird steers toward the middle of its neighbours |
sight | 0.08 | 0.04 to 0.16 | How far a bird sees its neighbours, as a fraction of the picture's height |
fear | 1.0 | 0.0 to 2.0 | How hard the flock turns away from the falcon |
dusk | 0.5 | 0.0 to 1.0 | From the last of the sunset to deep night |
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-19-murmuration.metal
// Ray Kiln · Episode 19 · Murmuration
//
// @episode 19 Murmuration
// @length 60
// @variation 30
// @variations 2,3,5,8,9,10,11,12
// @sweeps separation, alignment, dusk
// @still 26.0
// @teaches Boids: separation, alignment, cohesion
// @category Compute
// @tags boids, flocking, particles, compute shaders, swarm, steering, procedural sound
// @final Murmuration | Starlings over the kiln at dusk, every bird steering by its neighbours alone, the flock folding away each time the falcon stoops.
//
// Starlings over the kiln at dusk. No bird knows the shape of the flock: each one looks only at the birds near it and
// follows three rules, keep a little apart from them (separation), fly the way they fly (alignment) and stay among
// them (cohesion). That is Craig Reynolds' boids, from 1987, and the flock's swirling shape comes out of it. Every bird
// reads every other one, so update takes the swarm: all the birds as they were after the last step. A falcon is
// one of the particles too, number 0. It circles, and every fifteen seconds it stoops into the flock, which folds
// away from it in a wave.
// 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
//
// @particles 4096
// @param speed 1.0 0.5 1.5 How fast the birds fly
// @param separation 1.0 0.0 2.0 How hard each bird keeps clear of the ones right beside it
// @param alignment 1.0 0.0 2.0 How strongly each bird turns to fly the way its neighbours fly
// @param cohesion 1.0 0.0 2.0 How strongly each bird steers toward the middle of its neighbours
// @param sight 0.08 0.04 0.16 How far a bird sees its neighbours, as a fraction of the picture's height
// @param fear 1.0 0.0 2.0 How hard the flock turns away from the falcon
// @param dusk 0.5 0.0 1.0 From the last of the sunset to deep night
// @param volume 0.8 0.0 1.0 Loudness of the sound
constant float TAU = 6.2831853;
// The falcon stoops once every STOOP seconds, for DIVE of them.
constant float STOOP = 15.0;
constant float DIVE = 4.0;
// What one bird is: where it is and how fast it flies (in a space two units tall, as wide as the picture), whether
// it is the falcon (1) or a starling (0), and a random number of its own.
struct Particle {
float2 position;
float2 velocity;
float falcon;
float seed;
};
// Episode 5's integer hash.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// The roost: a point drifting slowly about the sky. Birds that stray far from it are drawn back.
float2 roost(float t, float aspect) {
return float2(aspect * (0.35 * sin(0.11 * t + 0.4) + 0.12 * sin(0.27 * t)),
0.25 + 0.18 * sin(0.17 * t + 1.3));
}
// How far into a stoop the falcon is at time t: 0 while it circles, rising to 1 and back down across the dive.
float stoopAt(float t) {
float x = fmod(t, STOOP) - (STOOP - DIVE);
return x > 0.0 ? sin(3.1415927 * x / DIVE) : 0.0;
}
// The falcon's circle, high above the flock, when it isn't diving.
float2 circling(float t, float aspect) {
return float2(aspect * 0.55 * cos(0.31 * t), 0.78 + 0.1 * sin(0.62 * t));
}
// Each bird starts somewhere in a loose cloud near the roost, flying any way. Number 0 is the falcon.
Particle spawn(uint id, constant Params& p) {
Particle q;
float a = TAU * hashU(id * 4u);
float r = 0.45 * sqrt(hashU(id * 4u + 1u));
q.position = float2(0.0, 0.3) + float2(cos(a) * 1.6, sin(a)) * r;
float heading = TAU * hashU(id * 4u + 2u);
q.velocity = float2(cos(heading), sin(heading)) * 0.5;
q.falcon = id == 0 ? 1.0 : 0.0;
q.seed = hashU(id * 4u + 3u);
if (id == 0) q.position = float2(-1.5, 0.8);
return q;
}
// The falcon's step: steer toward its circle, or while it stoops, toward the middle of the flock, fast.
void flyFalcon(thread Particle& q, float t, float aspect, Swarm swarm) {
float dive = stoopAt(t);
float2 goal = circling(t, aspect);
if (dive > 0.0) {
float2 middle = float2(0.0);
for (uint j = 1; j < swarm.count; j++) middle += swarm.at(j).position;
goal = middle / float(swarm.count - 1);
}
float2 want = normalize(goal - q.position + 1e-5) * (0.7 + 1.1 * dive);
q.velocity += (want - q.velocity) * 2.5 / 60.0;
}
// One step, for every bird at once. A starling looks at every other bird as it was after the last step: those it can
// see are its neighbours, and the three rules each give it a push.
void update(thread Particle& q, uint id,
constant Uniforms& u, constant Params& p, Swarm swarm) {
float dt = u.timeDelta;
float aspect = u.resolution.x / u.resolution.y;
if (q.falcon > 0.5) {
flyFalcon(q, u.time, aspect, swarm);
q.position += q.velocity * dt;
return;
}
float sight = p.sight * 2.0;
float close = 0.035;
float2 apart = float2(0.0), heading = float2(0.0), middle = float2(0.0);
float seen = 0.0;
for (uint j = 1; j < swarm.count; j++) {
if (j == id) continue;
Particle o = swarm.at(j);
float2 d = q.position - o.position;
float r = length(d);
if (r >= sight) continue;
seen += 1.0;
heading += o.velocity;
middle += o.position;
// Separation: away from a bird too close, harder the closer it is.
if (r < close) apart += d / max(r, 1e-4) * (1.0 - r / close);
}
float2 push = float2(0.0);
push += apart * 2.2 * p.separation;
if (seen > 0.0) {
// Alignment: turn toward the neighbours' average velocity.
push += (heading / seen - q.velocity) * 1.6 * p.alignment;
// Cohesion: steer toward the neighbours' middle.
push += (middle / seen - q.position) * 3.0 * p.cohesion;
}
// Back toward the roost when straying far, so the flock stays in the sky.
float2 home = roost(u.time, aspect) - q.position;
float far = length(home / float2(aspect * 0.8, 1.0));
push += home * smoothstep(0.35, 0.9, far) * 2.0 * p.speed * p.speed;
// Away from the falcon, hard, when it is near: the flock folds open where it passes.
Particle hawk = swarm.at(0);
float2 away = q.position - hawk.position;
float danger = 1.0 - smoothstep(0.05, 0.32, length(away));
push += normalize(away + 1e-5) * danger * danger * 6.0 * p.fear;
// A little wobble of its own, so no two birds fly the same line.
uint k = id * 3u + u.frame * 7919u;
push += (float2(hashU(k), hashU(k + 1u)) - 0.5) * 0.6;
q.velocity += push * dt;
// A starling never hovers and never goes past its top speed.
float s = length(q.velocity);
q.velocity *= clamp(s, 0.35, 0.75) * p.speed / max(s, 1e-4);
q.position += q.velocity * dt;
}
// Where to draw a bird (the picture's uv) and how big: the falcon is larger.
float3 place(Particle q, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
float2 uv = q.position / float2(aspect, 1.0) * 0.5 + 0.5;
return float3(uv, q.falcon > 0.5 ? 0.034 : 0.009);
}
// The distance from point p to the segment from a to b.
float segment(float2 p, float2 a, float2 b) {
float2 pa = p - a, ba = b - a;
return length(pa - ba * clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0));
}
// A bird's shadow against the sky: how much of the light it blocks. Turned to face the way it flies, a starling is a
// short body with two wings swept back that beat; the falcon's wings are longer and held still in its stoop.
float4 ink(Particle q, float2 at, constant Uniforms& u, constant Params& p) {
float2 f = normalize(q.velocity + 1e-5);
float2 b = float2(dot(at, f), dot(at, float2(-f.y, f.x)));
float flap = sin(u.time * 22.0 + q.seed * TAU);
float sweep = q.falcon > 0.5 ? -0.55 : -0.25 + 0.2 * flap;
float2 tip = float2(sweep, 0.85);
float d = segment(float2(b.x, abs(b.y)), float2(0.15, 0.0), tip);
d = min(d, segment(b, float2(-0.6, 0.0), float2(0.6, 0.0)));
float body = smoothstep(0.22, 0.08, d);
return float4(float3(body * 0.7), 1.0);
}
// What is left of the last step's shadows: a little, so a bird leaves a short blur behind it as it flies.
float4 fade(float2 uv, Trails previous,
constant Uniforms& u, constant Params& p) {
return previous.at(uv) * 0.45;
}
// The kiln palette from episode 1.
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.78, t));
c = mix(c, c4, smoothstep(0.78, 1.00, t));
return c;
}
// The sky at dusk: the kiln's ramp from the glow on the horizon up into the dark, lower and dimmer as dusk deepens.
float3 sky(float2 uv, constant Params& p) {
float glow = exp(-max(uv.y - 0.12, 0.0) * (1.4 + 1.8 * p.dusk));
float sun = exp(-length((uv - float2(0.62, 0.1)) * float2(1.0, 2.2)) * 5.0);
float high = 0.58 - 0.3 * p.dusk;
float3 c = kiln(0.1 + high * glow + 0.25 * sun * (1.0 - 0.6 * p.dusk));
// Above the glow, the night coming in: a deep violet.
return c + float3(0.05, 0.035, 0.09) * (1.0 - glow) * (1.0 - 0.5 * p.dusk);
}
// The ground: low hills, and the kiln on them with its chimney, its door glowing.
float3 ground(float2 uv, float3 c, constant Uniforms& u, float aspect) {
float x = uv.x * aspect;
float hill = 0.1 + 0.025 * sin(x * 3.1 + 1.0) + 0.015 * sin(x * 7.3);
float2 k = float2(x - 0.3 * aspect, uv.y - hill + 0.01);
float dome = length(k * float2(1.0, 1.25)) - 0.075;
float stack = max(abs(k.x - 0.03) - 0.012, abs(k.y - 0.1) - 0.08);
float solid = min(min(uv.y - hill, dome), stack);
float3 dark = float3(0.035, 0.027, 0.031);
float door = length((k - float2(0.0, 0.02)) * float2(1.6, 1.0)) - 0.025;
float flicker = 0.8 + 0.2 * sin(u.time * 9.0) * sin(u.time * 5.3);
dark += kiln(0.85) * smoothstep(0.004, -0.002, door) * flicker;
dark += kiln(0.6) * exp(-max(door, 0.0) * 60.0) * 0.25 * flicker;
return mix(c, dark, smoothstep(0.002, -0.001, solid));
}
// The picture: the sky, darkened wherever the birds' shadows pile up, so the thick of the flock reads as a dark
// cloud and its edges as a haze of single birds; then the ground in front.
float4 shade(float2 uv, constant Uniforms& u, constant Params& p,
Trails trails) {
float aspect = u.resolution.x / u.resolution.y;
float shadow = trails.at(uv).r;
float3 c = sky(uv, p);
c = mix(float3(0.035, 0.027, 0.031), c, exp(-shadow * 2.2));
c = ground(uv, c, u, aspect);
float2 q = uv - 0.5;
c *= 1.0 - dot(q, q) * 0.5;
return float4(c, 1.0);
}
// A smooth random value from 0 to 1 that changes `rate` times a second: two hashed values eased into each other.
float noise1(float t, float rate, uint salt) {
float x = t * rate;
float i = floor(x);
float f = x - i;
f = f * f * (3.0 - 2.0 * f);
uint n = uint(int(i) + 1000000);
return mix(hashU(n * 31u + salt), hashU((n + 1u) * 31u + salt), f);
}
// The sound follows the falcon's clock, the one thing here that is a function of time alone: a rush of wings (noise
// smoothed at a few rates, so it hisses rather than crackles) that swells as the flock folds away from each stoop and
// drifts from side to side once a stoop; starlings' chirps, a few a second and many in the alarm of a stoop; and the
// kiln's low hum under it all.
float2 sound(float t, constant Params& p) {
float dive = stoopAt(t);
// The wings: louder in a stoop, and a little with how hard the birds turn to keep together.
float wings = 0.25 + 0.75 * dive * p.fear + 0.1 * p.alignment;
float rush = (noise1(t, 1800.0, 3u) - 0.5)
+ 0.6 * (noise1(t, 4100.0, 7u) - 0.5)
+ 0.4 * (noise1(t, 650.0, 11u) - 0.5);
rush *= wings * (0.7 + 0.3 * noise1(t, 2.5, 13u)) * 0.5 * p.speed;
float side = 0.5 + 0.35 * sin(TAU * t / STOOP);
float2 air = float2(1.0 - side, side) * rush;
// Chirps: in slots of 1/14 s, a slot sometimes holds a chirp, a short whistle gliding down; more in a stoop. Each
// starts and ends inside its slot, so none is cut off.
float rate = 14.0;
float slot = floor(t * rate);
uint s = uint(int(slot) + 1000000);
float chirp = 0.0;
float pan = 0.5;
if (hashU(s * 5u) < 0.25 + 0.6 * dive) {
float age = t - (slot + 0.3 * hashU(s * 5u + 1u)) / rate;
float pitch = 2600.0 + 1800.0 * hashU(s * 5u + 2u);
float len = 0.03 + 0.018 * hashU(s * 5u + 3u);
float rise = smoothstep(0.0, 0.004, age);
float env = age > 0.0 ? rise * smoothstep(len, len * 0.4, age) : 0.0;
// The glide: the pitch falls to a third of where it started, so the phase is the integral of a falling frequency.
chirp = sin(TAU * pitch * (age - age * age / (3.0 * len))) * env * 0.16;
pan = hashU(s * 5u + 4u);
}
float2 birds = float2(1.0 - pan, pan) * chirp;
// The hum: A1 and E2, slowly breathing.
float hum = sin(TAU * 55.0 * t) + 0.5 * sin(TAU * 82.41 * t + 1.0);
hum *= 0.06 * (0.8 + 0.2 * sin(0.35 * t));
float2 v = air * 3.0 + birds * 1.7 + float2(hum) * 1.7;
// tanh rounds off the loudest moments instead of letting them clip.
float fadeIn = smoothstep(0.0, 2.0, t);
return tanh(v * 1.3) / 1.5 * fadeIn * 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, 1.0, 1.0, 1.0, 0.08, 1.0, 0.5, 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 Particles & Simulation
Press ? for keyboard shortcuts.

