Curl-Noise Flow
Ninety thousand sparks carried on a current made from noise, not down its slopes but along its contour lines, so they never pile up; the current drifts through time, whirls slowly around the middle, and glows where it runs fast, while three voices of a D minor chord swell with the water at three places on the picture.
· Particles & Simulation · Compute shader
Teaches Compute particles following a flow field, with trails
Builds on 01 · Flowing Color Field, 05 · Cracked Clay (integer hash, sound noise), 10 · Particle Swarm
Watch
The idea
Episode 10's swarm was held together by springs: every spark had a place to chase. This one has no places. Each spark is dropped into a flow field, a direction and a speed at every point of the picture, and at every step it simply goes where the current carries it.
The current is made from noise, and the obvious way to do that goes wrong. Noise is a landscape of hills and hollows; let the sparks roll downhill on it and within a few seconds they have all piled into the valleys. The fix is one small turn. Take the slope at each point, (dx, dy), and turn it a quarter, to (dy, -dx). Now each spark moves along the hill instead of down it, keeping to its contour line, the way a path keeps to one height around a hill. That turned slope is the curl of the noise, and a flow made this way can't gather anywhere or run dry: what flows into any small patch flows out of it again. The sparks stay evenly spread however long they ride it.
It is built in four steps, and each one is a shader you can run on its own.
Stage 1 · Downhill on noise
The landscape is potential: gradient noise (gnoise) at one size, standing still. Value noise, from episode 1, gives each corner of a grid a random height and blends between them; its slope is flat at every corner, and a flow built from its slope would cross the grid square on everywhere and show it. Gradient noise gives each corner a random slope instead: cornerSlope turns the corner's hash into a direction on a sphere (a height and an angle around it) and dots it with the way from the corner to the point, and gnoise blends the eight corners' parts with a smooth curve. The noise is zero at every corner and its grid hardly shows.
slope measures the landscape's slope by sampling it a small step either side in x and in y, and update moves each spark against it, downhill, by speed at every step. Episode 10's fade keeps most of each step's image, so every spark draws its path. Within a couple of seconds the paths all run into the bottoms of the valleys and the hollows, and the rest of the picture empties. So that there is always something to watch, about every three seconds, at a moment of its own, each spark is picked up and dropped somewhere new (scatter), and its path runs down into a valley again. The 150,000 sparks land on the picture or a way past its edges, so a spark that rolls in from outside can still be seen.
stage-1-downhill.metal
// Stage 1 · Downhill on noise
// Noise makes a landscape of hills and hollows. The obvious way to make a flow from it is to let every spark roll
// downhill. It moves for a moment, then the sparks pile up in the valleys and the bottoms of the hollows, and the
// rest of the picture empties out.
//
// @particles 150000
// @param speed 1.0 0.0 3.0 How fast the sparks roll
// @param scale 1.6 0.6 4.0 How small the hills are
// @param trails 0.9 0.6 0.97 How much of the last image is left at each step
// A spark is just where it is.
struct Particle {
float2 position;
};
// Episode 5's integer hash: a whole number in, a random number from 0 to 1 out.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// Episode 8's corner hash, keeping all its bits: a whole number for each whole-numbered point in 3D.
uint hashCorner(float3 p) {
uint3 v = uint3(int3(floor(p))) * 1664525u + 1013904223u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
v ^= v >> 16u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
return v.x;
}
// A corner's slope: a random direction in 3D from its hash (a height, and an angle
// around), dotted with the way from the corner to the point.
float cornerSlope(uint h, float3 d) {
float z = float(h & 0xffffu) / 32767.5 - 1.0;
float a = float(h >> 16u) * (6.2831853 / 65536.0);
float s = sqrt(max(1.0 - z * z, 0.0));
return 1.5 * dot(float3(s * cos(a), s * sin(a), z), d);
}
// One corner's part of the noise at the point: its slope, from the corner to there.
float cornerPart(float3 i, float3 f, float3 c) {
return cornerSlope(hashCorner(i + c), f - c);
}
// Gradient noise: a random slope at each corner instead of a random height, so the
// hills don't line up with the grid. Roughly -1 to 1.
float gnoise(float3 p) {
float3 i = floor(p), f = p - i;
float3 w = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
float n000 = cornerPart(i, f, float3(0, 0, 0));
float n100 = cornerPart(i, f, float3(1, 0, 0));
float n010 = cornerPart(i, f, float3(0, 1, 0));
float n110 = cornerPart(i, f, float3(1, 1, 0));
float n001 = cornerPart(i, f, float3(0, 0, 1));
float n101 = cornerPart(i, f, float3(1, 0, 1));
float n011 = cornerPart(i, f, float3(0, 1, 1));
float n111 = cornerPart(i, f, float3(1, 1, 1));
float lo = mix(mix(n000, n100, w.x), mix(n010, n110, w.x), w.y);
float hi = mix(mix(n001, n101, w.x), mix(n011, n111, w.x), w.y);
return mix(lo, hi, w.z);
}
// The landscape: one octave of gradient noise, standing still.
float potential(float2 x, constant Params& p) {
return gnoise(float3(x * p.scale, 0.0)) / p.scale;
}
// The slope of the landscape, measured with four samples a small step apart: which way is uphill, and how steep.
float2 slope(float2 x, constant Params& p) {
float2 a = float2(0.002, 0.0), b = float2(0.0, 0.002);
float dx = potential(x + a, p) - potential(x - a, p);
float dy = potential(x + b, p) - potential(x - b, p);
return float2(dx, dy) / 0.004;
}
// Somewhere on the picture or a way past its edges: a spark off the picture isn't
// drawn, but it can come back on.
float2 scatter(uint k) {
float2 r = float2(hashU(k), hashU(k + 1u)) * 2.0 - 1.0;
return r * float2(16.0 / 9.0 + 0.4, 1.4);
}
// Each spark starts anywhere.
Particle spawn(uint id, constant Params& p) {
Particle q;
q.position = scatter(id * 4u);
return q;
}
// One step: the spark rolls against the slope, downhill, and stops where the
// ground is flat, with every spark that rolled the same way.
void update(thread Particle& q, uint id,
constant Uniforms& u, constant Params& p) {
// Every three seconds or so, a spark is dropped somewhere new.
if (hashU(id * 7u + u.frame * 7919u) < u.timeDelta / 3.0) {
q.position = scatter(id * 5u + u.frame * 7919u);
}
q.position -= slope(q.position, p) * p.speed * 0.3 * u.timeDelta;
}
float3 place(Particle q, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
return float3(q.position / float2(aspect, 1.0) * 0.5 + 0.5, 0.003);
}
float4 ink(Particle q, float2 at, constant Uniforms& u, constant Params& p) {
float edge = smoothstep(1.0, 0.0, length(at));
float bright = edge * 0.3 * (1.0 - p.trails) / 0.1;
return float4(float3(1.0, 0.59, 0.20) * bright, 1.0);
}
// Episode 10's trails: each step keeps most of the last one's image, so every spark draws its path.
float4 fade(float2 uv, Trails previous,
constant Uniforms& u, constant Params& p) {
return previous.at(uv) * p.trails;
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p,
Trails trails) {
float3 c = trails.at(uv).rgb;
return float4(c / (1.0 + c), 1.0);
}Stage 2 · Turned a quarter: the curl
One line changes: curl returns (dy, -dx) instead of (dx, dy), and update adds it instead of taking it away. Each spark now goes sideways to the slope, along its contour line, faster where the hill is steep. Nothing piles up: the picture stays full, and every spark goes round and round its own closed loop, because the landscape stands still, until it is dropped somewhere new and joins another.
stage-2-curl.metal3 new lines since stage 1
// Stage 2 · Turned a quarter: the curl
// The same landscape, but each spark goes sideways to the slope instead of down it: along the contour lines, the
// way a path keeps to one height around a hill. Nothing can gather anywhere, so the sparks stream round and round.
//
// @particles 150000
// @param speed 1.0 0.0 3.0 How fast the sparks roll
// @param scale 1.6 0.6 4.0 How small the hills are
// @param trails 0.9 0.6 0.97 How much of the last image is left at each step
// A spark is just where it is.
struct Particle {
float2 position;
};
// Episode 5's integer hash: a whole number in, a random number from 0 to 1 out.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// Episode 8's corner hash, keeping all its bits: a whole number for each whole-numbered point in 3D.
uint hashCorner(float3 p) {
uint3 v = uint3(int3(floor(p))) * 1664525u + 1013904223u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
v ^= v >> 16u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
return v.x;
}
// A corner's slope: a random direction in 3D from its hash (a height, and an angle
// around), dotted with the way from the corner to the point.
float cornerSlope(uint h, float3 d) {
float z = float(h & 0xffffu) / 32767.5 - 1.0;
float a = float(h >> 16u) * (6.2831853 / 65536.0);
float s = sqrt(max(1.0 - z * z, 0.0));
return 1.5 * dot(float3(s * cos(a), s * sin(a), z), d);
}
// One corner's part of the noise at the point: its slope, from the corner to there.
float cornerPart(float3 i, float3 f, float3 c) {
return cornerSlope(hashCorner(i + c), f - c);
}
// Gradient noise: a random slope at each corner instead of a random height, so the
// hills don't line up with the grid. Roughly -1 to 1.
float gnoise(float3 p) {
float3 i = floor(p), f = p - i;
float3 w = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
float n000 = cornerPart(i, f, float3(0, 0, 0));
float n100 = cornerPart(i, f, float3(1, 0, 0));
float n010 = cornerPart(i, f, float3(0, 1, 0));
float n110 = cornerPart(i, f, float3(1, 1, 0));
float n001 = cornerPart(i, f, float3(0, 0, 1));
float n101 = cornerPart(i, f, float3(1, 0, 1));
float n011 = cornerPart(i, f, float3(0, 1, 1));
float n111 = cornerPart(i, f, float3(1, 1, 1));
float lo = mix(mix(n000, n100, w.x), mix(n010, n110, w.x), w.y);
float hi = mix(mix(n001, n101, w.x), mix(n011, n111, w.x), w.y);
return mix(lo, hi, w.z);
}
// The landscape: one octave of gradient noise, standing still.
float potential(float2 x, constant Params& p) {
return gnoise(float3(x * p.scale, 0.0)) / p.scale;
}
// The curl: the slope (dx, dy) turned a quarter, to (dy, -dx). It is as long as the slope is steep, but it points
// along the hill, never up or down it. What flows into any small patch flows out of it again.
float2 curl(float2 x, constant Params& p) {
float2 a = float2(0.002, 0.0), b = float2(0.0, 0.002);
float dx = potential(x + a, p) - potential(x - a, p);
float dy = potential(x + b, p) - potential(x - b, p);
return float2(dy, -dx) / 0.004;
}
// Somewhere on the picture or a way past its edges: a spark off the picture isn't
// drawn, but it can come back on.
float2 scatter(uint k) {
float2 r = float2(hashU(k), hashU(k + 1u)) * 2.0 - 1.0;
return r * float2(16.0 / 9.0 + 0.4, 1.4);
}
// Each spark starts anywhere.
Particle spawn(uint id, constant Params& p) {
Particle q;
q.position = scatter(id * 4u);
return q;
}
// One step: the spark goes along its contour line. Since the landscape stands still, each spark goes round its own
// loop until it is dropped somewhere new, faster where the hill is steep.
void update(thread Particle& q, uint id,
constant Uniforms& u, constant Params& p) {
// Every three seconds or so, a spark is dropped somewhere new.
if (hashU(id * 7u + u.frame * 7919u) < u.timeDelta / 3.0) {
q.position = scatter(id * 5u + u.frame * 7919u);
}
q.position += curl(q.position, p) * p.speed * 0.3 * u.timeDelta;
}
float3 place(Particle q, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
return float3(q.position / float2(aspect, 1.0) * 0.5 + 0.5, 0.003);
}
float4 ink(Particle q, float2 at, constant Uniforms& u, constant Params& p) {
float edge = smoothstep(1.0, 0.0, length(at));
float bright = edge * 0.3 * (1.0 - p.trails) / 0.1;
return float4(float3(1.0, 0.59, 0.20) * bright, 1.0);
}
// Episode 10's trails: each step keeps most of the last one's image, so every spark draws its path.
float4 fade(float2 uv, Trails previous,
constant Uniforms& u, constant Params& p) {
return previous.at(uv) * p.trails;
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p,
Trails trails) {
float3 c = trails.at(uv).rgb;
return float4(c / (1.0 + c), 1.0);
}3 lines from stage 1 that are gone
float2 slope(float2 x, constant Params& p) {
⋯
return float2(dx, dy) / 0.004;
⋯
q.position -= slope(q.position, p) * p.speed * 0.3 * u.timeDelta;Stage 3 · A current that changes
Three things turn the loops into a current.
- The landscape moves.
gnoiseis 3D, and its third coordinate is the clock timesevolve, so the hills slowly change shape and the contour lines open, close and drift. A spark that was going round a hill finds the hill gone and travels on. - A second layer. A second octave of gradient noise, turned, twice as fine and about half as strong, adds small eddies to the big ones. The whole landscape is divided by
scale, so smaller eddies don't also make a faster current. - Lives. Each spark keeps a
life, from 1 down to 0, and aseedthat makes some lives longer than others. When a spark burns out or drifts off the picture, it is born again anywhere. Its brightness fades in over the first tenth of its life and out over the last fifth, so no spark pops in or out.
The step is taken more carefully too: update measures the current at the spark, goes half a step, measures it again there, and takes the whole step with that (the midpoint method), so a spark keeps to its stream through a tight bend instead of being thrown off it.
stage-3-current.metal28 new lines since stage 2
// Stage 3 · A current that changes
// The landscape now drifts through time, with a second, finer layer of noise on top, so the loops open up and the
// sparks travel. Each spark lives a few seconds and is born again somewhere else, fading in and out.
//
// @particles 90000
// @param speed 1.0 0.0 3.0 How fast the current carries the sparks
// @param scale 1.6 0.6 4.0 How small the eddies are
// @param evolve 0.12 0.0 0.5 How fast the current itself changes
// @param life 6.0 2.0 12.0 How many seconds a spark lives
// @param trails 0.96 0.8 0.985 How much of the last image is left at each step
// A spark now has a life, from 1 down to 0, and a random number of its own.
struct Particle {
float2 position;
float life;
float seed;
};
// Episode 5's integer hash: a whole number in, a random number from 0 to 1 out.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// Episode 8's corner hash, keeping all its bits: a whole number for each whole-numbered point in 3D.
uint hashCorner(float3 p) {
uint3 v = uint3(int3(floor(p))) * 1664525u + 1013904223u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
v ^= v >> 16u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
return v.x;
}
// A corner's slope: a random direction in 3D from its hash (a height, and an angle
// around), dotted with the way from the corner to the point.
float cornerSlope(uint h, float3 d) {
float z = float(h & 0xffffu) / 32767.5 - 1.0;
float a = float(h >> 16u) * (6.2831853 / 65536.0);
float s = sqrt(max(1.0 - z * z, 0.0));
return 1.5 * dot(float3(s * cos(a), s * sin(a), z), d);
}
// One corner's part of the noise at the point: its slope, from the corner to there.
float cornerPart(float3 i, float3 f, float3 c) {
return cornerSlope(hashCorner(i + c), f - c);
}
// Gradient noise: a random slope at each corner instead of a random height, so the
// hills don't line up with the grid. Roughly -1 to 1.
float gnoise(float3 p) {
float3 i = floor(p), f = p - i;
float3 w = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
float n000 = cornerPart(i, f, float3(0, 0, 0));
float n100 = cornerPart(i, f, float3(1, 0, 0));
float n010 = cornerPart(i, f, float3(0, 1, 0));
float n110 = cornerPart(i, f, float3(1, 1, 0));
float n001 = cornerPart(i, f, float3(0, 0, 1));
float n101 = cornerPart(i, f, float3(1, 0, 1));
float n011 = cornerPart(i, f, float3(0, 1, 1));
float n111 = cornerPart(i, f, float3(1, 1, 1));
float lo = mix(mix(n000, n100, w.x), mix(n010, n110, w.x), w.y);
float hi = mix(mix(n001, n101, w.x), mix(n011, n111, w.x), w.y);
return mix(lo, hi, w.z);
}
// Two octaves of gradient noise, the second turned and finer. The third coordinate
// is time, so the hills slowly change shape.
float potential(float2 x, float t, constant Params& p) {
float2 q = x * p.scale;
float2 r = float2(0.8 * q.x - 0.6 * q.y, 0.6 * q.x + 0.8 * q.y) * 2.1 + 7.3;
float z = t * p.evolve;
float n = gnoise(float3(q, z)) + 0.45 * gnoise(float3(r, z * 1.4 + 3.1));
return n / p.scale;
}
// The curl, as before, at a moment in time.
float2 curl(float2 x, float t, constant Params& p) {
float2 a = float2(0.002, 0.0), b = float2(0.0, 0.002);
float dx = potential(x + a, t, p) - potential(x - a, t, p);
float dy = potential(x + b, t, p) - potential(x - b, t, p);
return float2(dy, -dx) / 0.004;
}
// Anywhere on the picture, or a little past its edges.
float2 anywhere(uint k, float aspect) {
float2 r = float2(hashU(k), hashU(k + 1u)) * 2.0 - 1.0;
return r * float2(aspect + 0.05, 1.05);
}
// Each spark starts partway through its life, so they don't all end together.
Particle spawn(uint id, constant Params& p) {
Particle q;
q.position = anywhere(id * 4u, 16.0 / 9.0);
q.life = hashU(id * 4u + 2u);
q.seed = hashU(id * 4u + 3u);
return q;
}
// One step along the current, measured halfway through the step (the midpoint method), so a spark keeps to its
// stream through tight bends. Then it burns down; when it burns out, or drifts off the picture, it is born again.
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 v = curl(q.position, u.time, p) * p.speed * 0.3;
float2 mid = q.position + v * dt * 0.5;
q.position += curl(mid, u.time, p) * p.speed * 0.3 * dt;
q.life -= dt / (p.life * (0.5 + q.seed));
bool gone = abs(q.position.x) > aspect + 0.1 || abs(q.position.y) > 1.1;
if (q.life <= 0.0 || gone) {
q.position = anywhere(id * 3u + u.frame * 7919u, aspect);
q.life = 1.0;
}
}
float3 place(Particle q, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
return float3(q.position / float2(aspect, 1.0) * 0.5 + 0.5, 0.003);
}
// A spark fades in when it is born and out as it ends, so none pops in or out.
float4 ink(Particle q, float2 at, constant Uniforms& u, constant Params& p) {
float alive = smoothstep(0.0, 0.2, q.life) * smoothstep(1.0, 0.9, q.life);
float edge = smoothstep(1.0, 0.0, length(at));
float bright = edge * alive * 0.25 * (1.0 - p.trails) / 0.04;
return float4(float3(1.0, 0.59, 0.20) * bright, 1.0);
}
float4 fade(float2 uv, Trails previous,
constant Uniforms& u, constant Params& p) {
return previous.at(uv) * p.trails;
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p,
Trails trails) {
float3 c = trails.at(uv).rgb;
return float4(c / (1.0 + c), 1.0);
}12 lines from stage 2 that are gone
float potential(float2 x, constant Params& p) {
return gnoise(float3(x * p.scale, 0.0)) / p.scale;
⋯
float2 curl(float2 x, constant Params& p) {
⋯
float dx = potential(x + a, p) - potential(x - a, p);
float dy = potential(x + b, p) - potential(x - b, p);
⋯
float2 scatter(uint k) {
⋯
return r * float2(16.0 / 9.0 + 0.4, 1.4);
⋯
q.position = scatter(id * 4u);
⋯
if (hashU(id * 7u + u.frame * 7919u) < u.timeDelta / 3.0) {
q.position = scatter(id * 5u + u.frame * 7919u);
⋯
q.position += curl(q.position, p) * p.speed * 0.3 * u.timeDelta;
⋯
float bright = edge * 0.3 * (1.0 - p.trails) / 0.1;Stage 4 · Curl-noise flow
The last stage gives the current a shape and the picture its colour.
- A whirl.
potentialadds a smooth hill in the middle,exp(-|x|² / 0.6), whose contour lines are circles, so its curl is a slow whirl around the middle, under the eddies.swirlsets how strong it is and which way it turns. - Light from speed. Each spark remembers how fast it went at its last step.
inkwrites only how much light a dot gives: more for a fast spark, so the quick streams glow and the slow pools smoulder.shaderolls the light that piled up at each pixel off with1 - exp(-light × 1.4)and runs it through episode 1's kiln palette, so a stream's faint edges are ember and its thick middle is orange and gold.heatslides along the palette. - No stitches. A fast spark jumps a few pixels at each step, and its dots would line up like stitches. Each spark also keeps where it was a step ago, and
placedraws its dot at a random point along the step it just took, so its trail fills in.
Each spark is two float2 and three float, which the compiler pads to 32 bytes: about 2.9 MB for all 90,000.
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, from the light the sparks left.
float4 shade(float2 uv, constant Uniforms& u, constant Params& p,
Trails trails) {
// How much light piled up here, rolled off so it never clips, then through the
// kiln palette: a stream's faint edges are ember, its thick middle gold.
float light = trails.at(uv).r;
float level = 1.0 - exp(-light * 1.4);
float2 d = uv - 0.5;
float3 ground = float3(0.028, 0.017, 0.019) * (1.0 - dot(d, d) * 1.6);
float t = level * (0.7 + 0.8 * p.heat) + (p.heat - 0.5) * 0.2;
float3 c = kiln(t) * smoothstep(0.0, 0.08, level);
return float4(ground + c, 1.0);
}episode-14-curl-noise-flow.metal, the whole file78 new lines since stage 3
// Ray Kiln · Episode 14 · Curl-Noise Flow
//
// @episode 14 Curl-Noise Flow
// @length 60
// @variation 30
// @variations 1,3,4,5,6,8,9,11
// @still 20.0
// @teaches Compute particles following a flow field, with trails
// @category Compute
// @tags compute shaders, particles, curl noise, gradient noise, flow fields, trails, procedural sound
// @final Curl-noise flow | A slow whirl around the middle, eddies of curl noise on top, and every spark coloured by how fast it goes.
//
// Ninety thousand sparks drift on a current that never stops changing. The current is made from noise, but
// not in the obvious way: the sparks don't run downhill on the noise, they run along its contour lines, sideways to
// the slope. That one turn (the curl) makes a flow that never piles sparks up or leaves holes, so the picture stays
// evenly full of streams that swirl, part and join.
// 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 90000
// @param speed 1.0 0.0 3.0 How fast the current carries the sparks
// @param scale 1.6 0.6 4.0 How small the eddies are: a few wide swirls or many tight ones
// @param evolve 0.12 0.0 0.5 How fast the current itself changes
// @param swirl 0.5 -1.5 1.5 A slow whirl around the middle, one way or the other
// @param life 6.0 2.0 12.0 How many seconds a spark lives before it is born again somewhere else
// @param trails 0.96 0.8 0.985 How much of the last image is left at each step: short sparks or long streaks
// @param heat 0.5 0.0 1.0 Slides the palette from deep ember to white-hot
// @param volume 0.8 0.0 1.0 Loudness of the sound
constant float TAU = 6.2831853;
// What one spark is: where it is (in a space one unit tall from the middle, as wide as the picture) and where it was
// a step ago, how much of its life is left (1 to 0), a random number of its own, and how fast it went at its last
// step, for its brightness.
struct Particle {
float2 position;
// Where it was a step ago.
float2 last;
float life;
float seed;
// How fast it went at its last step.
float speed;
};
// Episode 5's integer hash: a whole number in, a random number from 0 to 1 out.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// Episode 8's corner hash, keeping all its bits: a whole number for each whole-numbered point in 3D.
uint hashCorner(float3 p) {
uint3 v = uint3(int3(floor(p))) * 1664525u + 1013904223u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
v ^= v >> 16u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
return v.x;
}
// A corner's slope: a random direction in 3D from its hash (a height, and an angle
// around), dotted with the way from the corner to the point.
float cornerSlope(uint h, float3 d) {
float z = float(h & 0xffffu) / 32767.5 - 1.0;
float a = float(h >> 16u) * (6.2831853 / 65536.0);
float s = sqrt(max(1.0 - z * z, 0.0));
return 1.5 * dot(float3(s * cos(a), s * sin(a), z), d);
}
// One corner's part of the noise at the point: its slope, from the corner to there.
float cornerPart(float3 i, float3 f, float3 c) {
return cornerSlope(hashCorner(i + c), f - c);
}
// Gradient noise: where value noise gives each corner a height, this gives each corner a slope, so the noise is 0
// at every corner and its slope there is random. Value noise is flat at its corners, and its contour lines would all
// cross the grid square on; gradient noise hides its grid far better. Roughly -1 to 1. z is time, so the field can change.
float gnoise(float3 p) {
float3 i = floor(p), f = p - i;
float3 w = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
float n000 = cornerPart(i, f, float3(0, 0, 0));
float n100 = cornerPart(i, f, float3(1, 0, 0));
float n010 = cornerPart(i, f, float3(0, 1, 0));
float n110 = cornerPart(i, f, float3(1, 1, 0));
float n001 = cornerPart(i, f, float3(0, 0, 1));
float n101 = cornerPart(i, f, float3(1, 0, 1));
float n011 = cornerPart(i, f, float3(0, 1, 1));
float n111 = cornerPart(i, f, float3(1, 1, 1));
float lo = mix(mix(n000, n100, w.x), mix(n010, n110, w.x), w.y);
float hi = mix(mix(n001, n101, w.x), mix(n011, n111, w.x), w.y);
return mix(lo, hi, w.z);
}
// The landscape the flow comes from: two octaves of gradient noise, the second
// turned and finer, drifting through time. Divided by scale, so smaller eddies
// don't also make a faster current.
float potential(float2 x, float t, constant Params& p) {
float2 q = x * p.scale;
float2 r = float2(0.8 * q.x - 0.6 * q.y, 0.6 * q.x + 0.8 * q.y) * 2.1 + 7.3;
float z = t * p.evolve;
float n = gnoise(float3(q, z)) + 0.45 * gnoise(float3(r, z * 1.4 + 3.1));
// Plus a smooth hill in the middle. Its contour lines are circles, so its curl
// is a slow whirl around the middle, under the eddies; swirl sets which way.
return n / p.scale + p.swirl * 1.2 * exp(-dot(x, x) / 0.6);
}
// The curl: the slope of the landscape, turned a quarter. Measured with four samples a small step apart. The water
// of a flow made this way never gathers anywhere or runs dry: what flows into any small patch flows out of it.
float2 curl(float2 x, float t, constant Params& p) {
float2 a = float2(0.002, 0.0), b = float2(0.0, 0.002);
float dx = potential(x + a, t, p) - potential(x - a, t, p);
float dy = potential(x + b, t, p) - potential(x - b, t, p);
return float2(dy, -dx) / 0.004;
}
// Anywhere on the picture, a little past its edges, so sparks drift in rather than appear at the border.
float2 anywhere(uint k, float aspect) {
float2 r = float2(hashU(k), hashU(k + 1u)) * 2.0 - 1.0;
return r * float2(aspect + 0.05, 1.05);
}
// Each spark starts somewhere on the picture, partway through its life, so they don't all end together.
Particle spawn(uint id, constant Params& p) {
Particle q;
q.position = anywhere(id * 4u, 16.0 / 9.0);
q.last = q.position;
q.life = hashU(id * 4u + 2u);
q.seed = hashU(id * 4u + 3u);
q.speed = 0.0;
return q;
}
// One step: the spark goes where the current at its place carries it. No velocity is kept: a spark has no weight,
// it is wherever the water takes it. Two half steps (the midpoint method) keep it on its stream through tight bends.
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;
// Where it was, before it moves, so place can draw it anywhere along the step.
q.last = q.position;
float2 v = curl(q.position, u.time, p) * p.speed * 0.3;
float2 mid = q.position + v * dt * 0.5;
v = curl(mid, u.time, p) * p.speed * 0.3;
q.position += v * dt;
// How fast it went, for its brightness.
q.speed = length(v);
// Burning down; a spark that burns out, or drifts off the picture, is born again somewhere else.
q.life -= dt / (p.life * (0.5 + q.seed));
bool gone = abs(q.position.x) > aspect + 0.1 || abs(q.position.y) > 1.1;
if (q.life <= 0.0 || gone) {
uint k = id * 3u + u.frame * 7919u;
q.position = anywhere(k, aspect);
q.last = q.position;
q.life = 1.0;
q.speed = 0.0;
}
}
// Where to draw a spark (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;
// A fast spark jumps a few pixels a step, and its dots would line up like
// stitches. Drawn at a random point along the step it took, its trail fills in.
float along = hashU(uint(q.seed * 16777216.0) + u.frame * 7919u);
float2 spot = mix(q.last, q.position, along);
return float3(spot / float2(aspect, 1.0) * 0.5 + 0.5, 0.003);
}
// 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;
}
// A spark's light. It fades in when it is born and out as it ends, so no spark
// pops, and longer trails dim each dot by as much. A narrower picture holds the
// same sparks closer together, so each dot is dimmer by as much as it is narrower.
float4 ink(Particle q, float2 at, constant Uniforms& u, constant Params& p) {
// Brighter the faster the current carries it: the fast streams glow and the
// slow pools smoulder. Only light is kept here; shade gives it its colour.
float fast = q.speed / (0.3 * max(p.speed, 0.05));
float glow = 0.15 + smoothstep(0.1, 1.6, fast);
float alive = smoothstep(0.0, 0.2, q.life) * smoothstep(1.0, 0.9, q.life);
float edge = smoothstep(1.0, 0.0, length(at));
float keep = (1.0 - p.trails) / 0.04;
float narrow = min(u.resolution.x / u.resolution.y / (16.0 / 9.0), 1.0);
return float4(float3(edge * alive * glow * 0.16 * keep * narrow), 1.0);
}
// What is left of the last step's image: most of it, so each spark draws its stream behind it.
float4 fade(float2 uv, Trails previous,
constant Uniforms& u, constant Params& p) {
return previous.at(uv) * p.trails;
}
// The picture, from the light the sparks left.
float4 shade(float2 uv, constant Uniforms& u, constant Params& p,
Trails trails) {
// How much light piled up here, rolled off so it never clips, then through the
// kiln palette: a stream's faint edges are ember, its thick middle gold.
float light = trails.at(uv).r;
float level = 1.0 - exp(-light * 1.4);
float2 d = uv - 0.5;
float3 ground = float3(0.028, 0.017, 0.019) * (1.0 - dot(d, d) * 1.6);
float t = level * (0.7 + 0.8 * p.heat) + (p.heat - 0.5) * 0.2;
float3 c = kiln(t) * smoothstep(0.0, 0.08, level);
return float4(ground + 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 listens to the current itself, which is a function of place and time alone: three fixed points on the
// picture, each with a voice of its own (D, A and F, a D minor chord) that swells as the water there runs faster; and
// wind, a rumble of smooth noise that follows how fast the current runs on average.
float2 sound(float t, constant Params& p) {
float2 spots[3] = {
float2(-0.9, 0.35), float2(0.15, -0.45), float2(0.95, 0.3)
};
float notes[3] = { 146.83, 220.0, 349.23 };
float2 v = float2(0.0);
float total = 0.0;
for (int i = 0; i < 3; i++) {
float run = length(curl(spots[i], t, p)) * 0.3 * p.speed;
total += run;
float swell = 0.2 + 0.8 * smoothstep(0.02, 0.5, run);
float x = notes[i] * t;
float tone = sin(TAU * x) + 0.3 * sin(2.0 * TAU * x + 0.7)
+ 0.12 * sin(3.0 * TAU * x + 1.3);
float sway = 0.8 + 0.2 * sin(TAU * 0.23 * t + float(i) * 2.0);
tone *= 0.09 * swell * sway;
float pan = 0.5 + 0.4 * spots[i].x / 1.2;
v += float2(1.0 - pan, pan) * tone;
}
float wind = (noise1(t, 260.0, 3u) - 0.5)
+ 0.5 * (noise1(t, 900.0, 9u) - 0.5);
float gust = 0.3 + 0.7 * smoothstep(0.0, 1.2, total / 3.0);
gust *= 0.6 + 0.4 * noise1(t, 0.7, 17u);
float lean = noise1(t, 0.3, 23u);
v += float2(1.0 - lean * 0.6, 0.4 + lean * 0.6) * wind * 0.22 * gust;
float fadeIn = smoothstep(0.0, 2.0, t);
return tanh(v * 3.6) / 1.5 * fadeIn * p.volume;
}8 lines from stage 3 that are gone
return n / p.scale;
⋯
q.position += curl(mid, u.time, p) * p.speed * 0.3 * dt;
⋯
q.position = anywhere(id * 3u + u.frame * 7919u, aspect);
⋯
return float3(q.position / float2(aspect, 1.0) * 0.5 + 0.5, 0.003);
⋯
float bright = edge * alive * 0.25 * (1.0 - p.trails) / 0.04;
return float4(float3(1.0, 0.59, 0.20) * bright, 1.0);
⋯
float3 c = trails.at(uv).rgb;
return float4(c / (1.0 + c), 1.0);The sound
A shader's sound has no memory, so it can't hear the sparks. But the current is a function of place and time alone, so the sound can read it too.
- Three voices. At three fixed places on the picture (left, low in the middle and right),
soundmeasures how fast the current runs with the samecurlthe sparks ride. Each place has a voice: D3, A3 and F4, a D minor chord, each a sine with two quieter harmonics. A voice swells as the water at its place runs faster and sinks to a fifth of its loudness when it is still, and it is panned to where its place is. - Wind. Smooth noise, joined 260 and 900 times a second (
noise1), makes a low rumble that follows how fast the current runs at the three places on average, and leans slowly from one ear to the other.
speed, scale, evolve and swirl change the sound as they change the current. Everything goes through tanh to round off the loud moments and fades in over the first two seconds. The levels at the defaults are a peak of about -9.6 dBFS and an average (RMS) of about -21 dBFS.
New in Ray Kiln
Slow motion in the playground: the speed beside Sound plays the clock at ¼, ½, 1 or 2 times (or press [ and ]), and the clock says the rate when it isn't 1×. A particle episode's steps follow the clock, so the sparks slow down with it. A feedback shader, slowed down, draws over its last frame only once a sixtieth of a second of shader time has gone by, so its trails fade as they do at 1×. With the sound on, the sound slows too and its pitch drops (two octaves at ¼×), as a tape's would, and the picture keeps to it. Exports, recordings and stills stay at 1×. It is free, for any shader.
Try it on this episode at ¼×: each spark's trail still fades over the same number of steps, so the streams keep their length, while every spark crawls along them and you can follow a single one around a bend. The chord drops two octaves into a low hum.
Try this
- Set
swirlto 1.5 for a slow galaxy, or to -1.5 to turn it the other way. - Set
evolveto 0 and the current stands still: every spark goes round its own loop, as in stage 2, until its life ends. - Set
scaleto 4 for many small eddies, or to 0.6 for a few wide rivers. - Set
trailsto 0.985 for long silk, or to 0.8 for short sparks. - In
curl, swapfloat2(dy, -dx)forfloat2(-dx, -dy)and watch the flow drain into the valleys, as in stage 1.
Parameters
Pick a parameter and drag the slider. Each frame is the same moment of the shader, with only that one setting changed.
| Parameter | Default | Range | What it does |
|---|---|---|---|
speed | 1.0 | 0.0 to 3.0 | How fast the current carries the sparks |
scale | 1.6 | 0.6 to 4.0 | How small the eddies are: a few wide swirls or many tight ones |
evolve | 0.12 | 0.0 to 0.5 | How fast the current itself changes |
swirl | 0.5 | -1.5 to 1.5 | A slow whirl around the middle, one way or the other |
life | 6.0 | 2.0 to 12.0 | How many seconds a spark lives before it is born again somewhere else |
trails | 0.96 | 0.8 to 0.985 | How much of the last image is left at each step: short sparks or long streaks |
heat | 0.5 | 0.0 to 1.0 | Slides the palette from deep ember to white-hot |
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-14-curl-noise-flow.metal
// Ray Kiln · Episode 14 · Curl-Noise Flow
//
// @episode 14 Curl-Noise Flow
// @length 60
// @variation 30
// @variations 1,3,4,5,6,8,9,11
// @still 20.0
// @teaches Compute particles following a flow field, with trails
// @category Compute
// @tags compute shaders, particles, curl noise, gradient noise, flow fields, trails, procedural sound
// @final Curl-noise flow | A slow whirl around the middle, eddies of curl noise on top, and every spark coloured by how fast it goes.
//
// Ninety thousand sparks drift on a current that never stops changing. The current is made from noise, but
// not in the obvious way: the sparks don't run downhill on the noise, they run along its contour lines, sideways to
// the slope. That one turn (the curl) makes a flow that never piles sparks up or leaves holes, so the picture stays
// evenly full of streams that swirl, part and join.
// 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 90000
// @param speed 1.0 0.0 3.0 How fast the current carries the sparks
// @param scale 1.6 0.6 4.0 How small the eddies are: a few wide swirls or many tight ones
// @param evolve 0.12 0.0 0.5 How fast the current itself changes
// @param swirl 0.5 -1.5 1.5 A slow whirl around the middle, one way or the other
// @param life 6.0 2.0 12.0 How many seconds a spark lives before it is born again somewhere else
// @param trails 0.96 0.8 0.985 How much of the last image is left at each step: short sparks or long streaks
// @param heat 0.5 0.0 1.0 Slides the palette from deep ember to white-hot
// @param volume 0.8 0.0 1.0 Loudness of the sound
constant float TAU = 6.2831853;
// What one spark is: where it is (in a space one unit tall from the middle, as wide as the picture) and where it was
// a step ago, how much of its life is left (1 to 0), a random number of its own, and how fast it went at its last
// step, for its brightness.
struct Particle {
float2 position;
// Where it was a step ago.
float2 last;
float life;
float seed;
// How fast it went at its last step.
float speed;
};
// Episode 5's integer hash: a whole number in, a random number from 0 to 1 out.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// Episode 8's corner hash, keeping all its bits: a whole number for each whole-numbered point in 3D.
uint hashCorner(float3 p) {
uint3 v = uint3(int3(floor(p))) * 1664525u + 1013904223u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
v ^= v >> 16u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
return v.x;
}
// A corner's slope: a random direction in 3D from its hash (a height, and an angle
// around), dotted with the way from the corner to the point.
float cornerSlope(uint h, float3 d) {
float z = float(h & 0xffffu) / 32767.5 - 1.0;
float a = float(h >> 16u) * (6.2831853 / 65536.0);
float s = sqrt(max(1.0 - z * z, 0.0));
return 1.5 * dot(float3(s * cos(a), s * sin(a), z), d);
}
// One corner's part of the noise at the point: its slope, from the corner to there.
float cornerPart(float3 i, float3 f, float3 c) {
return cornerSlope(hashCorner(i + c), f - c);
}
// Gradient noise: where value noise gives each corner a height, this gives each corner a slope, so the noise is 0
// at every corner and its slope there is random. Value noise is flat at its corners, and its contour lines would all
// cross the grid square on; gradient noise hides its grid far better. Roughly -1 to 1. z is time, so the field can change.
float gnoise(float3 p) {
float3 i = floor(p), f = p - i;
float3 w = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
float n000 = cornerPart(i, f, float3(0, 0, 0));
float n100 = cornerPart(i, f, float3(1, 0, 0));
float n010 = cornerPart(i, f, float3(0, 1, 0));
float n110 = cornerPart(i, f, float3(1, 1, 0));
float n001 = cornerPart(i, f, float3(0, 0, 1));
float n101 = cornerPart(i, f, float3(1, 0, 1));
float n011 = cornerPart(i, f, float3(0, 1, 1));
float n111 = cornerPart(i, f, float3(1, 1, 1));
float lo = mix(mix(n000, n100, w.x), mix(n010, n110, w.x), w.y);
float hi = mix(mix(n001, n101, w.x), mix(n011, n111, w.x), w.y);
return mix(lo, hi, w.z);
}
// The landscape the flow comes from: two octaves of gradient noise, the second
// turned and finer, drifting through time. Divided by scale, so smaller eddies
// don't also make a faster current.
float potential(float2 x, float t, constant Params& p) {
float2 q = x * p.scale;
float2 r = float2(0.8 * q.x - 0.6 * q.y, 0.6 * q.x + 0.8 * q.y) * 2.1 + 7.3;
float z = t * p.evolve;
float n = gnoise(float3(q, z)) + 0.45 * gnoise(float3(r, z * 1.4 + 3.1));
// Plus a smooth hill in the middle. Its contour lines are circles, so its curl
// is a slow whirl around the middle, under the eddies; swirl sets which way.
return n / p.scale + p.swirl * 1.2 * exp(-dot(x, x) / 0.6);
}
// The curl: the slope of the landscape, turned a quarter. Measured with four samples a small step apart. The water
// of a flow made this way never gathers anywhere or runs dry: what flows into any small patch flows out of it.
float2 curl(float2 x, float t, constant Params& p) {
float2 a = float2(0.002, 0.0), b = float2(0.0, 0.002);
float dx = potential(x + a, t, p) - potential(x - a, t, p);
float dy = potential(x + b, t, p) - potential(x - b, t, p);
return float2(dy, -dx) / 0.004;
}
// Anywhere on the picture, a little past its edges, so sparks drift in rather than appear at the border.
float2 anywhere(uint k, float aspect) {
float2 r = float2(hashU(k), hashU(k + 1u)) * 2.0 - 1.0;
return r * float2(aspect + 0.05, 1.05);
}
// Each spark starts somewhere on the picture, partway through its life, so they don't all end together.
Particle spawn(uint id, constant Params& p) {
Particle q;
q.position = anywhere(id * 4u, 16.0 / 9.0);
q.last = q.position;
q.life = hashU(id * 4u + 2u);
q.seed = hashU(id * 4u + 3u);
q.speed = 0.0;
return q;
}
// One step: the spark goes where the current at its place carries it. No velocity is kept: a spark has no weight,
// it is wherever the water takes it. Two half steps (the midpoint method) keep it on its stream through tight bends.
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;
// Where it was, before it moves, so place can draw it anywhere along the step.
q.last = q.position;
float2 v = curl(q.position, u.time, p) * p.speed * 0.3;
float2 mid = q.position + v * dt * 0.5;
v = curl(mid, u.time, p) * p.speed * 0.3;
q.position += v * dt;
// How fast it went, for its brightness.
q.speed = length(v);
// Burning down; a spark that burns out, or drifts off the picture, is born again somewhere else.
q.life -= dt / (p.life * (0.5 + q.seed));
bool gone = abs(q.position.x) > aspect + 0.1 || abs(q.position.y) > 1.1;
if (q.life <= 0.0 || gone) {
uint k = id * 3u + u.frame * 7919u;
q.position = anywhere(k, aspect);
q.last = q.position;
q.life = 1.0;
q.speed = 0.0;
}
}
// Where to draw a spark (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;
// A fast spark jumps a few pixels a step, and its dots would line up like
// stitches. Drawn at a random point along the step it took, its trail fills in.
float along = hashU(uint(q.seed * 16777216.0) + u.frame * 7919u);
float2 spot = mix(q.last, q.position, along);
return float3(spot / float2(aspect, 1.0) * 0.5 + 0.5, 0.003);
}
// 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;
}
// A spark's light. It fades in when it is born and out as it ends, so no spark
// pops, and longer trails dim each dot by as much. A narrower picture holds the
// same sparks closer together, so each dot is dimmer by as much as it is narrower.
float4 ink(Particle q, float2 at, constant Uniforms& u, constant Params& p) {
// Brighter the faster the current carries it: the fast streams glow and the
// slow pools smoulder. Only light is kept here; shade gives it its colour.
float fast = q.speed / (0.3 * max(p.speed, 0.05));
float glow = 0.15 + smoothstep(0.1, 1.6, fast);
float alive = smoothstep(0.0, 0.2, q.life) * smoothstep(1.0, 0.9, q.life);
float edge = smoothstep(1.0, 0.0, length(at));
float keep = (1.0 - p.trails) / 0.04;
float narrow = min(u.resolution.x / u.resolution.y / (16.0 / 9.0), 1.0);
return float4(float3(edge * alive * glow * 0.16 * keep * narrow), 1.0);
}
// What is left of the last step's image: most of it, so each spark draws its stream behind it.
float4 fade(float2 uv, Trails previous,
constant Uniforms& u, constant Params& p) {
return previous.at(uv) * p.trails;
}
// The picture, from the light the sparks left.
float4 shade(float2 uv, constant Uniforms& u, constant Params& p,
Trails trails) {
// How much light piled up here, rolled off so it never clips, then through the
// kiln palette: a stream's faint edges are ember, its thick middle gold.
float light = trails.at(uv).r;
float level = 1.0 - exp(-light * 1.4);
float2 d = uv - 0.5;
float3 ground = float3(0.028, 0.017, 0.019) * (1.0 - dot(d, d) * 1.6);
float t = level * (0.7 + 0.8 * p.heat) + (p.heat - 0.5) * 0.2;
float3 c = kiln(t) * smoothstep(0.0, 0.08, level);
return float4(ground + 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 listens to the current itself, which is a function of place and time alone: three fixed points on the
// picture, each with a voice of its own (D, A and F, a D minor chord) that swells as the water there runs faster; and
// wind, a rumble of smooth noise that follows how fast the current runs on average.
float2 sound(float t, constant Params& p) {
float2 spots[3] = {
float2(-0.9, 0.35), float2(0.15, -0.45), float2(0.95, 0.3)
};
float notes[3] = { 146.83, 220.0, 349.23 };
float2 v = float2(0.0);
float total = 0.0;
for (int i = 0; i < 3; i++) {
float run = length(curl(spots[i], t, p)) * 0.3 * p.speed;
total += run;
float swell = 0.2 + 0.8 * smoothstep(0.02, 0.5, run);
float x = notes[i] * t;
float tone = sin(TAU * x) + 0.3 * sin(2.0 * TAU * x + 0.7)
+ 0.12 * sin(3.0 * TAU * x + 1.3);
float sway = 0.8 + 0.2 * sin(TAU * 0.23 * t + float(i) * 2.0);
tone *= 0.09 * swell * sway;
float pan = 0.5 + 0.4 * spots[i].x / 1.2;
v += float2(1.0 - pan, pan) * tone;
}
float wind = (noise1(t, 260.0, 3u) - 0.5)
+ 0.5 * (noise1(t, 900.0, 9u) - 0.5);
float gust = 0.3 + 0.7 * smoothstep(0.0, 1.2, total / 3.0);
gust *= 0.6 + 0.4 * noise1(t, 0.7, 17u);
float lean = noise1(t, 0.3, 23u);
v += float2(1.0 - lean * 0.6, 0.4 + lean * 0.6) * wind * 0.22 * gust;
float fadeIn = smoothstep(0.0, 2.0, t);
return tanh(v * 3.6) / 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.6, 0.12, 0.5, 6.0, 0.96, 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.
