Cracked Clay
A floor of drying clay split into pieces by Voronoi cells, the fire glowing through the cracks, the pieces lit by a lamp that circles them, and a crackle in the sound built the same way the cells are.
· Noise & Texture · Fragment shader
Teaches Voronoi noise, cell edges, bump lighting
Watch
The idea
Episode 1's noise gave every square of a grid a random number and blended between them. Voronoi noise gives every square a random point instead, and asks a different question of each pixel: which point is nearest? Everything closer to one point than to any other is that point's cell, and the cells fit together like dried mud, cracked glaze or the scales of a lizard.
Once each pixel knows how far it is from the edge of its cell, the edges become cracks. Treat that distance as a height, and the pieces bulge up between them. Work out which way that surface faces, light it, and flat noise becomes clay you could touch.
It is built in five steps, and each step is a shader you can run on its own.
Stage 1 · Nearest point
The screen is cut into squares, about 1.2 × scale of them from top to bottom. cellPoint puts one point in each square: hash22 turns the square's number into a random place, and a slow sin with a random start swings it back and forth across most of the square. Each pixel then looks at the points in its own square and the eight around it, since the nearest point is always one of those, and keeps the closest.
Every cell is coloured with its own random shade of clay, darker the further the pixel is from the cell's point, which is drawn as a hot dot. The straight lines where two shades meet are the edges of the cells.
stage-1-cells.metal
// Stage 1 · Nearest point
// Scatter one point in every square of a grid. Each pixel belongs to whichever point is nearest: that is its cell.
// @param speed 0.5 0.0 2.0 How fast the points sway
// @param scale 4.0 1.5 10.0 How many cells fit on the screen: higher makes smaller pieces
constant float TAU = 6.2831853;
// A random number for a square, and a random point inside it.
float hash21(float2 p) {
p = fract(p * float2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
float2 hash22(float2 p) {
float3 q = fract(float3(p.xyx) * float3(0.1031, 0.1030, 0.0973));
q += dot(q, q.yzx + 33.33);
return fract((q.xx + q.yz) * q.zy);
}
// The point in square c: a random place, swinging back and forth in time.
float2 cellPoint(float2 c, float t) {
float2 h = hash22(c);
return 0.5 + 0.4 * sin(t * 0.3 + TAU * h);
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float px = 2.4 / min(u.resolution.x, u.resolution.y);
float2 q = (uv - 0.5) * u.resolution * px;
float t = u.time * p.speed;
float2 x = q * p.scale * 0.5;
// Which square this pixel is in, and where in it. The nearest point is in this square or one of its eight neighbours.
float2 n = floor(x), f = fract(x);
float near = 8.0;
float id = 0.0;
for (int j = -1; j <= 1; j++) {
for (int i = -1; i <= 1; i++) {
float2 g = float2(i, j);
float2 r = g + cellPoint(n + g, t) - f;
float d = length(r);
if (d < near) { near = d; id = hash21(n + g); }
}
}
// Each cell gets its own shade of clay, darker away from its point, and the point itself is a hot dot.
float3 col = mix(float3(0.34, 0.15, 0.09), float3(0.70, 0.40, 0.22), id);
col *= 1.0 - 0.7 * near;
col = mix(col, float3(1.0, 0.68, 0.2), 1.0 - smoothstep(0.03, 0.05, near));
return float4(col, 1.0);
}Stage 2 · Edges become cracks
The edge between two cells is the line halfway between their points. The distance to the nearest point does not tell you how far away that line is, so voronoi makes a second pass. Knowing the nearest point, it looks at the points around it, and for each one measures how far the pixel is from the halfway line between the two, along the line that joins them: dot(0.5 * (mr + r), normalize(r - mr)). The smallest of these is the distance to the edge.
The faint bands inside each cell show that distance growing towards the middle. Where it is smaller than crack, the pixel is in a crack, and the fire shows through.
stage-2-edges.metal
// Stage 2 · Edges become cracks
// The edge between two cells lies halfway between their points. Measure how far each pixel is from it, and draw cracks.
// @param speed 0.5 0.0 2.0 How fast the points sway
// @param scale 4.0 1.5 10.0 How many cells fit on the screen: higher makes smaller pieces
// @param crack 0.06 0.01 0.2 How wide the cracks open
constant float TAU = 6.2831853;
float hash21(float2 p) {
p = fract(p * float2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
float2 hash22(float2 p) {
float3 q = fract(float3(p.xyx) * float3(0.1031, 0.1030, 0.0973));
q += dot(q, q.yzx + 33.33);
return fract((q.xx + q.yz) * q.zy);
}
float2 cellPoint(float2 c, float t) {
float2 h = hash22(c);
return 0.5 + 0.4 * sin(t * 0.3 + TAU * h);
}
// Two passes. The first finds the nearest point, as before. The second looks at the points around it and measures the
// distance to each halfway line, along the line that joins the two points. The smallest is the distance to the edge.
float3 voronoi(float2 x, float t) {
float2 n = floor(x), f = fract(x);
float2 mg = 0.0, mr = 0.0;
float md = 8.0;
for (int j = -1; j <= 1; j++) {
for (int i = -1; i <= 1; i++) {
float2 g = float2(i, j);
float2 r = g + cellPoint(n + g, t) - f;
float d = dot(r, r);
if (d < md) { md = d; mr = r; mg = g; }
}
}
float near = sqrt(md);
md = 8.0;
for (int j = -2; j <= 2; j++) {
for (int i = -2; i <= 2; i++) {
float2 g = mg + float2(i, j);
float2 r = g + cellPoint(n + g, t) - f;
if (dot(mr - r, mr - r) > 0.00001) {
md = min(md, dot(0.5 * (mr + r), normalize(r - mr)));
}
}
}
return float3(md, near, hash21(n + mg));
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float px = 2.4 / min(u.resolution.x, u.resolution.y);
float2 q = (uv - 0.5) * u.resolution * px;
float t = u.time * p.speed;
float2 x = q * p.scale * 0.5;
float3 v = voronoi(x, t);
float3 col = mix(float3(0.34, 0.15, 0.09), float3(0.56, 0.30, 0.17), v.z);
// Faint bands show the distance to the edge growing towards the middle of each cell.
col *= 0.85 + 0.15 * cos(TAU * v.x * 8.0);
// Where that distance is smaller than the crack width, the pixel is in a crack, and the fire shows through.
float inCrack = 1.0 - smoothstep(p.crack * 0.25, p.crack * 1.1, v.x);
col = mix(col, float3(1.0, 0.68, 0.2), inCrack);
return float4(col, 1.0);
}Stage 3 · A height for every pixel
Now the cells become a surface. clayHeight gives each piece a low dome over its point, and multiplies it by a smoothstep of the edge distance, so the piece rounds off and sinks to nothing at the crack. Before any of that, the point is pushed about by two fbm noises from episode 1, so the cracks wander instead of running in dead straight lines.
A second, smaller Voronoi (2.6 times as many cells) carves fine cracks into the pieces, but only where a slow noise lets them through, so some pieces are whole and others are crazed. A faint noise adds the grain of the clay. Here the height is shown as brightness, and the clay drifts slowly to the left so new pieces keep arriving.
stage-3-height.metal
// Stage 3 · A height for every pixel
// Turn the cells into a surface: each piece bulges, the cracks sink, noise bends the edges, finer cracks split the pieces.
// @param speed 0.5 0.0 2.0 How fast the points sway and the clay drifts
// @param scale 4.0 1.5 10.0 How many cells fit on the screen: higher makes smaller pieces
// @param crack 0.06 0.01 0.2 How wide the cracks open
constant float TAU = 6.2831853;
float hash21(float2 p) {
p = fract(p * float2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
float2 hash22(float2 p) {
float3 q = fract(float3(p.xyx) * float3(0.1031, 0.1030, 0.0973));
q += dot(q, q.yzx + 33.33);
return fract((q.xx + q.yz) * q.zy);
}
float vnoise(float2 p) {
float2 i = floor(p), f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash21(i), b = hash21(i + float2(1, 0));
float c = hash21(i + float2(0, 1)), d = hash21(i + float2(1, 1));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
float fbm(float2 p) {
float v = 0.0, a = 0.5;
for (int i = 0; i < 4; i++) {
v += a * vnoise(p);
p = p * 2.03 + float2(17.1, 9.2);
a *= 0.5;
}
return v;
}
// ---- Voronoi ---------------------------------------------------------------------------------------------------
// Cut the plane into squares and put one point somewhere inside each. Every pixel belongs to the nearest point: that
// is its cell. The nearest point is always in this square or one of its eight neighbours.
// The point in square c: a random place, swinging back and forth in time.
float2 cellPoint(float2 c, float t) {
float2 h = hash22(c);
return 0.5 + 0.4 * sin(t * 0.3 + TAU * h);
}
// Returns the distance to the nearest edge between two cells, the distance to the nearest point, and a random number
// for the cell. The edge distance needs a second pass: the edge is halfway between the nearest point and another one,
// so measure along the line joining them.
float3 voronoi(float2 x, float t) {
float2 n = floor(x), f = fract(x);
float2 mg = 0.0, mr = 0.0;
float md = 8.0;
for (int j = -1; j <= 1; j++) {
for (int i = -1; i <= 1; i++) {
float2 g = float2(i, j);
float2 r = g + cellPoint(n + g, t) - f;
float d = dot(r, r);
if (d < md) { md = d; mr = r; mg = g; }
}
}
float near = sqrt(md);
md = 8.0;
for (int j = -2; j <= 2; j++) {
for (int i = -2; i <= 2; i++) {
float2 g = mg + float2(i, j);
float2 r = g + cellPoint(n + g, t) - f;
if (dot(mr - r, mr - r) > 0.00001) {
md = min(md, dot(0.5 * (mr + r), normalize(r - mr)));
}
}
}
return float3(md, near, hash21(n + mg));
}
// The height of the clay: a rounded piece for each cell, sinking to nothing at its cracks.
float clayHeight(float2 x, float t, float w,
thread float& edge, thread float& id) {
// Bend the space a little first, so the cracks wander.
float2 warp = float2(fbm(x * 0.9), fbm(x * 0.9 + float2(5.2, 1.3))) - 0.5;
float3 v = voronoi(x + warp * 0.5, t);
edge = v.x;
id = v.z;
float h = 0.3 * (1.0 - 0.6 * v.y * v.y);
h *= smoothstep(w * 0.5, w * 1.6 + 0.12, v.x);
// Finer cracks, only where a slow noise lets them through.
float3 fine = voronoi(x * 2.6 + warp + float2(31.7, 12.4), t);
float mask = smoothstep(0.5, 0.65, fbm(x * 0.6 + float2(3.0, 8.0)));
h -= 0.1 * mask * (1.0 - smoothstep(0.0, max(w * 1.2, 0.04), fine.x));
float2 turned = float2(x.x * 0.8 - x.y * 0.6, x.x * 0.6 + x.y * 0.8);
h += 0.006 * fbm(turned * 9.0 + id * 50.0);
return h;
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float px = 2.4 / min(u.resolution.x, u.resolution.y);
float2 q = (uv - 0.5) * u.resolution * px;
float t = u.time * p.speed;
// The clay drifts left.
float2 x = q * p.scale * 0.5 + float2(t * 0.15, 0.0);
float edge, id;
float h = clayHeight(x, t, p.crack, edge, id);
// High is pale, the bottom of a crack is black.
float3 col = float3(1.0, 0.95, 0.87) * smoothstep(-0.05, 0.32, h);
return float4(col, 1.0);
}Stage 4 · Bump lighting
To light a surface you need to know which way it faces: its normal. The height is worked out three times, here, a small step to the right and a small step up. The differences are the slope in each direction, and the normal leans against the slope: normalize(float3(-slope * bump, 1.0)). bump makes the same heights look flatter or deeper, without changing the shapes at all.
The lamp circles the clay, low enough to rake across it, and the diffuse light is how squarely the surface faces it: dot(n, l), as in episode 3. That one number is what makes the rims of the pieces catch the light on one side and fall into shadow on the other.
stage-4-bump.metal
// Stage 4 · Bump lighting
// The slope of the height gives the way the surface faces. Light it from a lamp that circles, and the clay stands out.
// @param speed 0.5 0.0 2.0 How fast the points sway and the clay drifts
// @param scale 4.0 1.5 10.0 How many cells fit on the screen: higher makes smaller pieces
// @param crack 0.06 0.01 0.2 How wide the cracks open
// @param bump 1.0 0.0 3.0 How strongly the pieces bulge and catch the light
// @param light 0.15 0.0 1.0 Where the lamp stands, once around the clay
constant float TAU = 6.2831853;
float hash21(float2 p) {
p = fract(p * float2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
float2 hash22(float2 p) {
float3 q = fract(float3(p.xyx) * float3(0.1031, 0.1030, 0.0973));
q += dot(q, q.yzx + 33.33);
return fract((q.xx + q.yz) * q.zy);
}
float vnoise(float2 p) {
float2 i = floor(p), f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash21(i), b = hash21(i + float2(1, 0));
float c = hash21(i + float2(0, 1)), d = hash21(i + float2(1, 1));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
float fbm(float2 p) {
float v = 0.0, a = 0.5;
for (int i = 0; i < 4; i++) {
v += a * vnoise(p);
p = p * 2.03 + float2(17.1, 9.2);
a *= 0.5;
}
return v;
}
// ---- Voronoi ---------------------------------------------------------------------------------------------------
// Cut the plane into squares and put one point somewhere inside each. Every pixel belongs to the nearest point: that
// is its cell. The nearest point is always in this square or one of its eight neighbours.
// The point in square c: a random place, swinging back and forth in time.
float2 cellPoint(float2 c, float t) {
float2 h = hash22(c);
return 0.5 + 0.4 * sin(t * 0.3 + TAU * h);
}
// Returns the distance to the nearest edge between two cells, the distance to the nearest point, and a random number
// for the cell. The edge distance needs a second pass: the edge is halfway between the nearest point and another one,
// so measure along the line joining them.
float3 voronoi(float2 x, float t) {
float2 n = floor(x), f = fract(x);
float2 mg = 0.0, mr = 0.0;
float md = 8.0;
for (int j = -1; j <= 1; j++) {
for (int i = -1; i <= 1; i++) {
float2 g = float2(i, j);
float2 r = g + cellPoint(n + g, t) - f;
float d = dot(r, r);
if (d < md) { md = d; mr = r; mg = g; }
}
}
float near = sqrt(md);
md = 8.0;
for (int j = -2; j <= 2; j++) {
for (int i = -2; i <= 2; i++) {
float2 g = mg + float2(i, j);
float2 r = g + cellPoint(n + g, t) - f;
if (dot(mr - r, mr - r) > 0.00001) {
md = min(md, dot(0.5 * (mr + r), normalize(r - mr)));
}
}
}
return float3(md, near, hash21(n + mg));
}
// The height of the clay: a rounded piece for each cell, sinking to nothing at its cracks.
float clayHeight(float2 x, float t, float w,
thread float& edge, thread float& id) {
// Bend the space a little first, so the cracks wander.
float2 warp = float2(fbm(x * 0.9), fbm(x * 0.9 + float2(5.2, 1.3))) - 0.5;
float3 v = voronoi(x + warp * 0.5, t);
edge = v.x;
id = v.z;
float h = 0.3 * (1.0 - 0.6 * v.y * v.y);
h *= smoothstep(w * 0.5, w * 1.6 + 0.12, v.x);
// Finer cracks, only where a slow noise lets them through.
float3 fine = voronoi(x * 2.6 + warp + float2(31.7, 12.4), t);
float mask = smoothstep(0.5, 0.65, fbm(x * 0.6 + float2(3.0, 8.0)));
h -= 0.1 * mask * (1.0 - smoothstep(0.0, max(w * 1.2, 0.04), fine.x));
float2 turned = float2(x.x * 0.8 - x.y * 0.6, x.x * 0.6 + x.y * 0.8);
h += 0.006 * fbm(turned * 9.0 + id * 50.0);
return h;
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float px = 2.4 / min(u.resolution.x, u.resolution.y);
float2 q = (uv - 0.5) * u.resolution * px;
float t = u.time * p.speed;
float2 x = q * p.scale * 0.5 + float2(t * 0.15, 0.0);
float w = p.crack;
// The height here, a step to the right and a step up. The differences are the slope, and
// the normal leans against it: bump makes the same heights look flatter or deeper.
float edge, id, e2, i2;
float e = 0.01;
float h = clayHeight(x, t, w, edge, id);
float hx = clayHeight(x + float2(e, 0.0), t, w, e2, i2);
float hy = clayHeight(x + float2(0.0, e), t, w, e2, i2);
float2 slope = float2(hx - h, hy - h) / e;
float3 n = normalize(float3(-slope * p.bump, 1.0));
// A lamp circles the clay. Diffuse light is how squarely the surface faces it.
float la = TAU * p.light + t * 0.2;
float3 l = normalize(float3(cos(la), sin(la), 0.8));
float diffuse = max(dot(n, l), 0.0);
float3 col = float3(0.62, 0.48, 0.40) * (0.12 + 0.95 * diffuse);
return float4(col, 1.0);
}Stage 5 · Fired clay
The last step colours it. Each piece is a slightly different terracotta, chosen by its cell's random number, and darker where it sinks towards a crack, with a soft sheen where the surface turns the lamp's light up towards you. In the cracks, episode 1's flowing noise picks a colour from the kiln palette, so the fire moves under the clay, and its light spills out onto the rims of the cracks. heat slides the fire along the palette, from deep red to white-hot.
The cracks also breathe: their width swings slowly between half of crack and all of it, as if the clay were shrinking and swelling in the heat.
the kiln palette and shade(), from the finished shader
float3 kiln(float t) {
float3 c0 = float3(0.020, 0.012, 0.020);
float3 c1 = float3(0.280, 0.040, 0.030);
float3 c2 = float3(0.880, 0.260, 0.050);
float3 c3 = float3(1.000, 0.680, 0.200);
float3 c4 = float3(1.000, 0.970, 0.840);
t = clamp(t, 0.0, 1.0);
float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
c = mix(c, c2, smoothstep(0.22, 0.50, t));
c = mix(c, c3, smoothstep(0.50, 0.76, t));
return mix(c, c4, smoothstep(0.76, 1.00, t));
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float px = 2.4 / min(u.resolution.x, u.resolution.y);
float2 q = (uv - 0.5) * u.resolution * px;
float t = u.time * p.speed;
// The clay drifts left.
float2 x = q * p.scale * 0.5 + float2(t * 0.15, 0.0);
float w = crackWidth(t, p);
// Bump lighting, as in stage 4: the slope of the height gives the normal.
float edge, id, e2, i2;
float e = 0.01;
float h = clayHeight(x, t, w, edge, id);
float hx = clayHeight(x + float2(e, 0.0), t, w, e2, i2);
float hy = clayHeight(x + float2(0.0, e), t, w, e2, i2);
float2 slope = float2(hx - h, hy - h) / e;
float3 n = normalize(float3(-slope * p.bump, 1.0));
// A lamp circles the clay. Diffuse light is how squarely the surface faces it.
float la = TAU * p.light + t * 0.2;
float3 l = normalize(float3(cos(la), sin(la), 0.8));
float diffuse = max(dot(n, l), 0.0);
// A soft sheen where the surface turns the lamp's light up towards the viewer.
float3 halfway = normalize(l + float3(0.0, 0.0, 1.0));
float sheen = pow(max(dot(n, halfway), 0.0), 24.0);
// Each piece is its own terracotta, darker where it sinks.
float3 clay = mix(float3(0.34, 0.15, 0.09), float3(0.56, 0.30, 0.17), id);
clay *= 0.55 + 1.5 * h;
float3 col = clay * (0.12 + 0.95 * diffuse);
col += float3(1.0, 0.9, 0.75) * sheen * 0.12;
// The fire flows under the cracks.
float fire = fbm(x * 1.2 + float2(-t * 0.5, t * 0.2));
float3 ember = kiln(0.5 + 0.45 * fire + (p.heat - 0.5) * 0.6);
float inCrack = 1.0 - smoothstep(w * 0.25, w * 1.1, edge);
col = mix(col, ember * (0.6 + 0.6 * fire), inCrack);
// Its light spills onto their rims.
float spill = exp(-max(edge - w * 0.5, 0.0) / (w * 1.5 + 0.02));
col += kiln(0.45 + p.heat * 0.3) * 0.45 * spill * (1.0 - inCrack);
// Darker towards the corners.
col *= 1.0 - 0.4 * smoothstep(0.6, 1.9, length(q));
return float4(col, 1.0);
}The sound
The sound is Voronoi in one dimension. Time is cut into short slots, and each slot gets one moment at a random place inside it, the way the picture puts one point at a random place in each square. Each moment is a crack: a short burst of noise and a small, dull ring between about 420 and 1020 Hz, placed at random between the ears. There are 0.8 × scale slots a second, so about three at the default scale, and more cells in the picture means more cracks in the sound. Not every slot cracks: more of them do while the cracks in the picture are wide open, and the noise of a wider crack lasts longer.
The noise is made by joining random values a set number of times a second, with an integer hash so it stays exact for minutes on end: the fewer values a second, the darker it sounds. Under the cracks, the kiln's roar is two layers of that noise, darker or brighter with heat and swelling as the cracks widen, over a low hum on A1 and E2.
Try this
- Set
scaleto 10 for crazed glaze, or 1.5 to get close to a few big pieces. - Set
bumpto 0: the cracks are still there, but the clay goes flat. Then try 3. - Set
crackto 0.2 and listen to each crack last longer. - In stage 2, colour by the distance to the nearest point instead of the distance to the edge, and see why the second pass is needed.
- In
clayHeight, remove thewarpand the cracks run straight from corner to corner of each cell.
Parameters
| Parameter | Default | Range | What it does |
|---|---|---|---|
speed | 0.5 | 0.0 to 2.0 | How fast the clay drifts, the fire flows and the light circles |
scale | 4.0 | 1.5 to 10.0 | How many cells fit on the screen: higher makes smaller pieces |
crack | 0.06 | 0.01 to 0.2 | How wide the cracks open |
bump | 1.0 | 0.0 to 3.0 | How strongly the pieces bulge and catch the light |
light | 0.15 | 0.0 to 1.0 | Where the lamp stands, once around the clay |
heat | 0.5 | 0.0 to 1.0 | Slides the fire 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.
The full source
This is the whole episode: the picture, the parameters and the sound.
episode-05-cracked-clay.metal
// Ray Kiln · Episode 5 · Cracked Clay
//
// @episode 5 Cracked Clay
// @length 60
// @variation 30
// @variations 1,3,5,6,7,9,10,11
// @still 20.0
// @teaches Voronoi noise, cell edges, bump lighting
// @category Fragment
// @tags voronoi, cellular noise, bump mapping, lighting, procedural sound
// @final Fired clay | Colour the clay, let the fire glow through the cracks and onto their rims, and let them breathe.
//
// A floor of drying clay, split into cells by cracks with the fire glowing underneath, lit by a lamp that slowly
// circles it. The cells are Voronoi noise: every pixel belongs to the nearest of a scatter of points.
// Each @param line below becomes a slider in the host app and a row in the page's parameters table:
// // @param name default min max description
// @param speed 0.5 0.0 2.0 How fast the clay drifts, the fire flows and the light circles
// @param scale 4.0 1.5 10.0 How many cells fit on the screen: higher makes smaller pieces
// @param crack 0.06 0.01 0.2 How wide the cracks open
// @param bump 1.0 0.0 3.0 How strongly the pieces bulge and catch the light
// @param light 0.15 0.0 1.0 Where the lamp stands, once around the clay
// @param heat 0.5 0.0 1.0 Slides the fire from deep ember to white-hot
// @param volume 0.8 0.0 1.0 Loudness of the sound
constant float TAU = 6.2831853;
// ---- Noise (episode 1) -----------------------------------------------------------------------------------------
float hash21(float2 p) {
p = fract(p * float2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
float2 hash22(float2 p) {
float3 q = fract(float3(p.xyx) * float3(0.1031, 0.1030, 0.0973));
q += dot(q, q.yzx + 33.33);
return fract((q.xx + q.yz) * q.zy);
}
float vnoise(float2 p) {
float2 i = floor(p), f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash21(i), b = hash21(i + float2(1, 0));
float c = hash21(i + float2(0, 1)), d = hash21(i + float2(1, 1));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
float fbm(float2 p) {
float v = 0.0, a = 0.5;
for (int i = 0; i < 4; i++) {
v += a * vnoise(p);
p = p * 2.03 + float2(17.1, 9.2);
a *= 0.5;
}
return v;
}
// ---- Voronoi ---------------------------------------------------------------------------------------------------
// Cut the plane into squares and put one point somewhere inside each. Every pixel belongs to the nearest point: that
// is its cell. The nearest point is always in this square or one of its eight neighbours.
// The point in square c: a random place, swinging back and forth in time.
float2 cellPoint(float2 c, float t) {
float2 h = hash22(c);
return 0.5 + 0.4 * sin(t * 0.3 + TAU * h);
}
// Returns the distance to the nearest edge between two cells, the distance to the nearest point, and a random number
// for the cell. The edge distance needs a second pass: the edge is halfway between the nearest point and another one,
// so measure along the line joining them.
float3 voronoi(float2 x, float t) {
float2 n = floor(x), f = fract(x);
float2 mg = 0.0, mr = 0.0;
float md = 8.0;
for (int j = -1; j <= 1; j++) {
for (int i = -1; i <= 1; i++) {
float2 g = float2(i, j);
float2 r = g + cellPoint(n + g, t) - f;
float d = dot(r, r);
if (d < md) { md = d; mr = r; mg = g; }
}
}
float near = sqrt(md);
md = 8.0;
for (int j = -2; j <= 2; j++) {
for (int i = -2; i <= 2; i++) {
float2 g = mg + float2(i, j);
float2 r = g + cellPoint(n + g, t) - f;
if (dot(mr - r, mr - r) > 0.00001) {
md = min(md, dot(0.5 * (mr + r), normalize(r - mr)));
}
}
}
return float3(md, near, hash21(n + mg));
}
// ---- The clay --------------------------------------------------------------------------------------------------
// The width of the cracks: they slowly open and close, as if the clay breathes in the heat.
float crackWidth(float t, constant Params& p) {
return p.crack * (0.75 + 0.25 * sin(t * 0.4));
}
// The height of the clay: a rounded piece for each cell, sinking to nothing at its cracks.
float clayHeight(float2 x, float t, float w,
thread float& edge, thread float& id) {
// Bend the space a little first, so the cracks wander.
float2 warp = float2(fbm(x * 0.9), fbm(x * 0.9 + float2(5.2, 1.3))) - 0.5;
float3 v = voronoi(x + warp * 0.5, t);
edge = v.x;
id = v.z;
float h = 0.3 * (1.0 - 0.6 * v.y * v.y);
h *= smoothstep(w * 0.5, w * 1.6 + 0.12, v.x);
// Finer cracks, only where a slow noise lets them through.
float3 fine = voronoi(x * 2.6 + warp + float2(31.7, 12.4), t);
float mask = smoothstep(0.5, 0.65, fbm(x * 0.6 + float2(3.0, 8.0)));
h -= 0.1 * mask * (1.0 - smoothstep(0.0, max(w * 1.2, 0.04), fine.x));
float2 turned = float2(x.x * 0.8 - x.y * 0.6, x.x * 0.6 + x.y * 0.8);
h += 0.006 * fbm(turned * 9.0 + id * 50.0);
return h;
}
float3 kiln(float t) {
float3 c0 = float3(0.020, 0.012, 0.020);
float3 c1 = float3(0.280, 0.040, 0.030);
float3 c2 = float3(0.880, 0.260, 0.050);
float3 c3 = float3(1.000, 0.680, 0.200);
float3 c4 = float3(1.000, 0.970, 0.840);
t = clamp(t, 0.0, 1.0);
float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
c = mix(c, c2, smoothstep(0.22, 0.50, t));
c = mix(c, c3, smoothstep(0.50, 0.76, t));
return mix(c, c4, smoothstep(0.76, 1.00, t));
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float px = 2.4 / min(u.resolution.x, u.resolution.y);
float2 q = (uv - 0.5) * u.resolution * px;
float t = u.time * p.speed;
// The clay drifts left.
float2 x = q * p.scale * 0.5 + float2(t * 0.15, 0.0);
float w = crackWidth(t, p);
// Bump lighting, as in stage 4: the slope of the height gives the normal.
float edge, id, e2, i2;
float e = 0.01;
float h = clayHeight(x, t, w, edge, id);
float hx = clayHeight(x + float2(e, 0.0), t, w, e2, i2);
float hy = clayHeight(x + float2(0.0, e), t, w, e2, i2);
float2 slope = float2(hx - h, hy - h) / e;
float3 n = normalize(float3(-slope * p.bump, 1.0));
// A lamp circles the clay. Diffuse light is how squarely the surface faces it.
float la = TAU * p.light + t * 0.2;
float3 l = normalize(float3(cos(la), sin(la), 0.8));
float diffuse = max(dot(n, l), 0.0);
// A soft sheen where the surface turns the lamp's light up towards the viewer.
float3 halfway = normalize(l + float3(0.0, 0.0, 1.0));
float sheen = pow(max(dot(n, halfway), 0.0), 24.0);
// Each piece is its own terracotta, darker where it sinks.
float3 clay = mix(float3(0.34, 0.15, 0.09), float3(0.56, 0.30, 0.17), id);
clay *= 0.55 + 1.5 * h;
float3 col = clay * (0.12 + 0.95 * diffuse);
col += float3(1.0, 0.9, 0.75) * sheen * 0.12;
// The fire flows under the cracks.
float fire = fbm(x * 1.2 + float2(-t * 0.5, t * 0.2));
float3 ember = kiln(0.5 + 0.45 * fire + (p.heat - 0.5) * 0.6);
float inCrack = 1.0 - smoothstep(w * 0.25, w * 1.1, edge);
col = mix(col, ember * (0.6 + 0.6 * fire), inCrack);
// Its light spills onto their rims.
float spill = exp(-max(edge - w * 0.5, 0.0) / (w * 1.5 + 0.02));
col += kiln(0.45 + p.heat * 0.3) * 0.45 * spill * (1.0 - inCrack);
// Darker towards the corners.
col *= 1.0 - 0.4 * smoothstep(0.6, 1.9, length(q));
return float4(col, 1.0);
}
// ---- Sound -----------------------------------------------------------------------------------------------------
// Voronoi in one dimension: cut time into short slots and put one moment at a random place in each, the way the
// picture puts one point in each square. Each moment is a crack: a burst of noise and a small dull ring, somewhere
// between the ears. Under them, the roar of the kiln, breathing with the cracks.
// Integer hash, so noise stays exact for minutes of sound.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// Noise made by joining random values `rate` times a second: a lower rate sounds darker.
float noise1(float t, float rate, uint seed) {
float x = t * rate;
float i = floor(x), f = fract(x);
f = f * f * (3.0 - 2.0 * f);
uint k = uint(i) + seed * 7919u;
return mix(hashU(k), hashU(k + 1u), f) * 2.0 - 1.0;
}
float2 sound(float t, constant Params& p) {
float ts = t * p.speed;
// How open the cracks are now, from 0.5 to 1.
float w = crackWidth(ts, p) / max(p.crack, 0.0001);
// More cells, more cracks: about three a second at the default scale.
float rate = 0.8 * p.scale;
float slot = t * rate;
float2 snaps = 0.0;
for (int back = 0; back < 3; back++) {
float s = floor(slot) - float(back);
if (s < 0.0) { continue; }
uint k = uint(s);
// Not every slot cracks: more of them do while the cracks are wide open.
if (hashU(k * 3u + 1u) > 0.35 + 0.5 * w) { continue; }
float at = (s + 0.1 + 0.8 * hashU(k * 3u)) / rate;
float since = t - at;
if (since < 0.0) { continue; }
float loud = 0.4 + 0.6 * hashU(k * 3u + 2u);
// The noise of a wider crack lasts longer.
float decay = 70.0 - 250.0 * p.crack;
float attack = 1.0 - exp(-since * 3000.0);
float burst = noise1(t, 9000.0, k) * attack * exp(-since * decay);
float f = 420.0 + 600.0 * hashU(k * 5u + 4u);
float ring = sin(TAU * f * since) * attack * exp(-since * 30.0);
float c = (burst * 0.8 + ring * 0.5) * loud;
float pan = hashU(k * 7u + 3u);
snaps += c * float2(1.0 - 0.7 * pan, 0.3 + 0.7 * pan);
}
// The roar: dark noise, brighter with heat, swelling as the cracks widen.
float roarRate = mix(90.0, 400.0, p.heat);
float hi = roarRate * 2.1;
float2 roar = float2(noise1(t, roarRate, 11u) + 0.5 * noise1(t, hi, 12u),
noise1(t, roarRate, 13u) + 0.5 * noise1(t, hi, 14u));
roar *= 0.16 * (0.6 + 0.6 * (w - 0.5));
// A low hum on A1 and E2 under it all.
float hum = sin(TAU * fract(55.0 * t)) + 0.5 * sin(TAU * fract(82.41 * t));
float2 bed = roar + float2(hum, hum) * 0.06;
float fadeIn = smoothstep(0.0, 2.0, t);
return tanh((snaps * 0.45 + bed) * 1.4) * 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] = [0.5, 4.0, 0.06, 1.0, 0.15, 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.
