Shader

Stained Glass

A pointed window in a stone wall with the sun behind it. Its panes are Voronoi cells held in lead, each a colour of glass that lets through only its own share of the light, less the thicker it is. Traced back towards the sun, each point of the floor finds the pane its light came through, so the window lies on the floor in coloured light and slides as the sun moves; dust in the air shows the same light as beams. Clouds dim it all, and the organ under it, while panes catching the sun ring like glass.

· Light & Materials · Fragment shader

Teaches Voronoi panes, lead lines, light coloured by the glass it passes

Builds on Flowing Color Field (hash21, value noise, fractal noise), Kaleidoscope (a note of the scale), Cracked Clay (hash22, integer hash, Voronoi cells, Voronoi cell point)

Watch

The idea

A stained-glass window is a picture made of light that has passed through something. White sunlight goes in; what comes out of a red pane is only the red part of it, because the glass soaks up the rest. So the window has two lives: the glowing panes you see in the wall, and the same colours thrown across the floor wherever the light lands.

The panes are Voronoi cells, from Cracked Clay: one point in each square of a grid, and every point of the window belongs to the nearest one. Where two cells meet is where the lead runs. To find the light on the floor, the shader doesn't follow light forward from the sun. It starts at a point of the floor and walks back up towards the sun until it reaches the wall: if that point is glass, the sun came through that pane. The same trick, done at points in the air, gives the beams. It is built in four steps.

Stage 1 · Panes

outline is the window's shape: how far a point of the wall is outside it, below 0 inside. Up to the spring line it is a rectangle 0.44 wide; above it two arcs of radius 0.286 meet in a point, each with its centre just past the middle on the far side, which is what makes the arch pointed. pane cuts the window with voronoi, scaled so that cells panes fit across it. voronoi gives the distance to the nearest edge between two cells, and that, or the distance to the window's edge if it is nearer, says where the lead is: within half of lead of an edge, with a pixel and a half of blur. Each cell's random number picks its colour in glassColour: one of six jewel colours (ruby, cobalt, emerald, amber, violet, gold), or, as jewel goes to 0, a colour from the kiln palette.

stage-1-panes.metal
// Stage 1 · Panes
// A window is a shape cut into panes. Voronoi cells (from Cracked Clay)
// cut the plane: every point belongs to the nearest of a scatter of points,
// one in each square of a grid. Inside a pointed arch each cell is a pane
// of its own colour, and lead runs where two cells meet.

// @param cells 7 3 14 How many panes across the window
// @param lead 0.008 0.003 0.02 How wide the lead between the panes is
// @param jewel 1 0 1 The glass from the kiln's ambers (0) to jewel colours (1)
// @param volume 0.8 0 1 Loudness of the sound

constant float TAU = 6.2831853;
// The window, in wall units with the floor at 0: half its width, its sill,
// where the arch springs and the radius of each side's arc.
constant float HALF = 0.22;
constant float SILL = 0.08;
constant float SPRING = 0.34;
constant float RADIUS = 0.286;
// How high the eye is above the floor.
constant float EYE = 0.18;

// ---- Noise ----------------------------------------------------------------

float hash21(float2 p) {
    p = fract(p * float2(123.34, 456.21));
    p += dot(p, p + 45.32);
    return fract(p.x * p.y);
}

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 kiln palette from Flowing Color Field.
float3 kiln(float t) {
    float3 c0 = float3(0.020, 0.012, 0.020);
    float3 c1 = float3(0.280, 0.040, 0.030);
    float3 c2 = float3(0.880, 0.260, 0.050);
    float3 c3 = float3(1.000, 0.680, 0.200);
    float3 c4 = float3(1.000, 0.970, 0.840);
    t = clamp(t, 0.0, 1.0);
    float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
    c = mix(c, c2, smoothstep(0.22, 0.50, t));
    c = mix(c, c3, smoothstep(0.50, 0.76, t));
    return mix(c, c4, smoothstep(0.76, 1.00, t));
}

// ---- Voronoi (Cracked Clay) -----------------------------------------------
// One point in each square of a grid; every pixel belongs to the nearest.

float2 cellPoint(float2 c, float t) {
    float2 h = hash22(c);
    return 0.5 + 0.4 * sin(t * 0.3 + TAU * h);
}

// The distance to the nearest edge between two cells, the distance to the
// nearest point, and a random number for the cell.
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 (i != 0 || j != 0) {
                md = min(md, dot(0.5 * (mr + r), normalize(r - mr)));
            }
        }
    }
    return float3(md, near, hash21(n + mg));
}

// ---- The window -----------------------------------------------------------

// How far a point of the wall is outside the window: below 0 inside. Up to
// the spring it is a rectangle; above, two arcs meet in a point, each with
// its centre on the far side of the middle.
float outline(float2 w) {
    float side = abs(w.x) - HALF;
    float2 c = float2(abs(w.x) + RADIUS - HALF, w.y - SPRING);
    float arch = length(c) - RADIUS;
    return max(w.y > SPRING ? arch : side, SILL - w.y);
}

// A pane's colour from its random number: one of six jewel colours, or a
// colour of the kiln, blended by jewel.
float3 glassColour(float id, float jewel) {
    const float3 jewels[6] = {
        float3(0.80, 0.07, 0.10), float3(0.10, 0.22, 0.85),
        float3(0.08, 0.60, 0.28), float3(0.98, 0.60, 0.10),
        float3(0.50, 0.14, 0.66), float3(1.00, 0.84, 0.36)};
    float3 j = jewels[min(int(id * 6.0), 5)];
    return mix(kiln(0.45 + 0.5 * id), j, jewel);
}

struct Pane { float3 colour; float lead; float id; };

// The glass at a point of the wall: its pane's colour, and how much of it
// lead covers (1 is all). soft is how blurred the lead's edges are.
Pane pane(float2 w, constant Params& p, float soft) {
    float d = outline(w);
    float scale = p.cells / (2.0 * HALF);
    float3 v = voronoi(w * scale, 0.0);
    // How far to the nearest lead, in wall units: between panes, and
    // around the window.
    float edge = min(v.x / scale, -d);
    Pane g;
    g.lead = 1.0 - smoothstep(p.lead * 0.5, p.lead * 0.5 + soft, edge);
    g.colour = glassColour(v.z, p.jewel);
    g.id = v.z;
    return g;
}

// Integer hash, so noise stays exact for minutes.
float hashU(uint n) {
    n = (n << 13u) ^ n;
    n = n * (n * n * 15731u + 789221u) + 1376312589u;
    return float(n & 0x7fffffffu) / float(0x7fffffff);
}

float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
    float2 s = (uv - 0.5) * u.resolution / u.resolution.y;
    float px = 1.0 / u.resolution.y;
    // The wall, and the window in it, seen straight on.
    float2 w = float2(s.x, s.y + EYE);
    Pane g = pane(w, p, px * 1.5);
    float3 col = g.colour * (1.0 - g.lead);
    // Dark stone around it.
    float d = outline(w);
    col += float3(0.05, 0.04, 0.035) * smoothstep(0.0, px, d);
    return float4(col, 1.0);
}

// ---- Sound ----------------------------------------------------------------
// The panes ring like glass: a moment at a random place in each short slot
// of time, as the window has a point in each square.

float scaleNote(int i) {
    const float notes[16] = {
        293.66, 349.23, 392.00, 440.00, 523.25, 587.33, 698.46, 783.99,
        880.00, 1046.50, 1174.66, 1396.91, 1567.98, 1760.00, 2093.00,
        2349.32};
    return notes[clamp(i, 0, 15)];
}

// A glass bell: partials that aren't whole multiples, the higher ones
// dying sooner.
float bell(float f, float since) {
    float a = 1.0 - exp(-since * 400.0);
    float s = sin(TAU * f * since) * exp(-since * 2.5);
    s += 0.5 * sin(TAU * f * 2.76 * since) * exp(-since * 4.0);
    s += 0.25 * sin(TAU * f * 5.40 * since) * exp(-since * 7.0);
    return a * s;
}

float2 sound(float t, constant Params& p) {
    float2 sum = 0.0;
    // The bells: one moment in each 0.45 s slot, rung or not.
    float slot = 0.45;
    float k = floor(t / slot);
    for (int i = 0; i < 6; i++) {
        float kk = k - float(i);
        if (kk < 0.0) break;
        uint n = uint(kk) * 4u;
        float at = (kk + hashU(n)) * slot;
        float since = t - at;
        if (since < 0.0) continue;
        if (hashU(n + 1u) > 0.5) continue;
        float f = scaleNote(5 + int(hashU(n + 2u) * 9.0));
        float pan = hashU(n + 3u);
        float b = bell(f, since) * 0.45;
        sum += float2(b * (1.0 - 0.7 * pan), b * (0.3 + 0.7 * pan));
    }
    return tanh(sum * 1.1) * 0.7 * smoothstep(0.0, 0.5, t) * p.volume;
}

Stage 2 · Light through glass

Now the sun is behind it. through says how much of each colour gets past a pane: the glass's colour raised to the power of its thickness. That is Beer–Lambert's law: each unit of thickness lets through the same share again, so a pane twice as thick passes the square of what one does, deeper and darker, and thin glass is pale. The thickness wanders inside each pane with fractal noise, as poured glass does, and thickness scales it. A border of small panes, three times finer, runs 0.035 inside the frame in the kiln's golds, and iron saddle bars cross the window every 0.16 up from the sill. The light behind the glass is brightest where the sun stands behind it: glare is how closely the view looks towards the sun, raised to the 6th power. Around the window, stones lays blocks of stone, each its own shade, with mortar between, lit a little more close to the window. The colour is tone mapped (1 - exp(-1.2 c)) so the bright glass rolls off rather than clipping.

stage-2-light-through-glass.metal68 new lines since stage 1
// Stage 2 · Light through glass
// Put the sun behind it. Glass passes only its share of each colour, again
// for every unit of thickness (Beer-Lambert): thick glass is deep, thin
// glass pale. Golden border panes and iron bars; the stone around catches a
// little of the light.

// @param cells 7 3 14 How many panes across the window
// @param lead 0.008 0.003 0.02 How wide the lead between the panes is
// @param jewel 1 0 1 The glass from the kiln's ambers (0) to jewel colours (1)
// @param thickness 1.2 0.4 3 How thick the glass is: thicker glass is deeper
// @param sun 48 30 62 How high the sun stands, in degrees
// @param volume 0.8 0 1 Loudness of the sound

constant float TAU = 6.2831853;
// The window, in wall units with the floor at 0: half its width, its sill,
// where the arch springs and the radius of each side's arc.
constant float HALF = 0.22;
constant float SILL = 0.08;
constant float SPRING = 0.34;
constant float RADIUS = 0.286;
// How high the eye is, and how far back from the wall it stands.
constant float EYE = 0.18;
constant float BACK = 1.0;

// ---- Noise ----------------------------------------------------------------

float hash21(float2 p) {
    p = fract(p * float2(123.34, 456.21));
    p += dot(p, p + 45.32);
    return fract(p.x * p.y);
}

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;
}

// The kiln palette from Flowing Color Field.
float3 kiln(float t) {
    float3 c0 = float3(0.020, 0.012, 0.020);
    float3 c1 = float3(0.280, 0.040, 0.030);
    float3 c2 = float3(0.880, 0.260, 0.050);
    float3 c3 = float3(1.000, 0.680, 0.200);
    float3 c4 = float3(1.000, 0.970, 0.840);
    t = clamp(t, 0.0, 1.0);
    float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
    c = mix(c, c2, smoothstep(0.22, 0.50, t));
    c = mix(c, c3, smoothstep(0.50, 0.76, t));
    return mix(c, c4, smoothstep(0.76, 1.00, t));
}

// ---- Voronoi (Cracked Clay) -----------------------------------------------
// One point in each square of a grid; every pixel belongs to the nearest.

float2 cellPoint(float2 c, float t) {
    float2 h = hash22(c);
    return 0.5 + 0.4 * sin(t * 0.3 + TAU * h);
}

// The distance to the nearest edge between two cells, the distance to the
// nearest point, and a random number for the cell.
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 (i != 0 || j != 0) {
                md = min(md, dot(0.5 * (mr + r), normalize(r - mr)));
            }
        }
    }
    return float3(md, near, hash21(n + mg));
}

// ---- The window -----------------------------------------------------------

// How far a point of the wall is outside the window: below 0 inside. Up to
// the spring it is a rectangle; above, two arcs meet in a point, each with
// its centre on the far side of the middle.
float outline(float2 w) {
    float side = abs(w.x) - HALF;
    float2 c = float2(abs(w.x) + RADIUS - HALF, w.y - SPRING);
    float arch = length(c) - RADIUS;
    return max(w.y > SPRING ? arch : side, SILL - w.y);
}

// A pane's colour from its random number: one of six jewel colours, or a
// colour of the kiln, blended by jewel.
float3 glassColour(float id, float jewel) {
    const float3 jewels[6] = {
        float3(0.80, 0.07, 0.10), float3(0.10, 0.22, 0.85),
        float3(0.08, 0.60, 0.28), float3(0.98, 0.60, 0.10),
        float3(0.50, 0.14, 0.66), float3(1.00, 0.84, 0.36)};
    float3 j = jewels[min(int(id * 6.0), 5)];
    return mix(kiln(0.45 + 0.5 * id), j, jewel);
}

struct Pane { float3 colour; float lead; float id; };

// The glass at a point of the wall: its pane's colour, and how much of it
// lead covers (1 is all). soft is how blurred the lead's edges are.
Pane pane(float2 w, constant Params& p, float soft) {
    float d = outline(w);
    // Small golden panes line the frame.
    float border = 0.035;
    bool rim = d > -border;
    float scale = p.cells / (2.0 * HALF) * (rim ? 3.0 : 1.0);
    float3 v = voronoi(w * scale + (rim ? 40.0 : 0.0), 0.0);
    // The nearest lead: between panes, at the border, round the window.
    float edge = min(v.x / scale, abs(d + border));
    edge = min(edge, -d);
    // Iron saddle bars, as thick as the lead.
    float bar = abs(fract((w.y - SILL) / 0.16 + 0.5) - 0.5) * 0.16;
    edge = min(edge, bar);
    Pane g;
    g.lead = 1.0 - smoothstep(p.lead * 0.5, p.lead * 0.5 + soft, edge);
    g.colour = rim ? kiln(0.62 + 0.3 * v.z) : glassColour(v.z, p.jewel);
    g.id = v.z;
    return g;
}

// Beer-Lambert: each unit of thickness passes the glass's share of each
// colour again, and the thickness wanders.
float3 through(Pane g, float2 w, constant Params& p) {
    float wave = fbm(w * 30.0 + g.id * 40.0);
    float thick = p.thickness * (0.6 + 0.8 * wave);
    return pow(max(g.colour, 0.02), thick) * (1.0 - g.lead);
}

// ---- The sun and the sky --------------------------------------------------

// The sun's light falls and comes towards us: for each unit it falls, x is
// how far it moves sideways and y how far towards us. It swings side to side.
float2 sunSlope(float t, constant Params& p) {
    float side = 0.45 * sin(t * 0.12);
    return float2(side, 1.0 / tan(p.sun * TAU / 360.0));
}

// Integer hash, so noise stays exact for minutes.
float hashU(uint n) {
    n = (n << 13u) ^ n;
    n = n * (n * n * 15731u + 789221u) + 1376312589u;
    return float(n & 0x7fffffffu) / float(0x7fffffff);
}

// ---- The room -------------------------------------------------------------

// Stone blocks: a random shade for each, darker in the mortar between them,
// every other row offset by half a block.
float stones(float2 x, float2 size) {
    float row = floor(x.y / size.y);
    x.x += fract(row * 0.5) * size.x;
    float2 c = floor(x / size), f = x - c * size;
    float2 m = min(f, size - f);
    float mortar = smoothstep(0.0, 0.006, min(m.x, m.y));
    return (0.6 + 0.4 * hash21(c)) * (0.35 + 0.65 * mortar);
}

float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
    float2 s = (uv - 0.5) * u.resolution / u.resolution.y;
    float px = 1.0 / u.resolution.y;
    float t = u.time;
    float2 slope = sunSlope(t, p);
    float3 sun = float3(1.0, 0.93, 0.80) * 3.0;
    // The wall, and the window in it, seen straight on.
    float2 w = float2(s.x, s.y + EYE);
    Pane g = pane(w, p, px * 1.5);
    // The glass glows brightest where the sun stands behind it.
    float3 view = normalize(float3(s, BACK));
    float3 toSun = normalize(float3(-slope.x, 1.0, slope.y));
    float glare = pow(max(dot(view, toSun), 0.0), 6.0);
    float3 col = sun * (0.5 + 1.5 * glare) * through(g, w, p);
    // The stone, lit a little around the window by its own light.
    float d = outline(w);
    float stone = stones(w, float2(0.16, 0.08));
    float halo = exp(-max(d, 0.0) * 8.0);
    float3 wall = float3(0.16, 0.12, 0.10) * stone * (0.35 + 1.2 * halo);
    col += wall * smoothstep(0.0, px, d);
    // Tone mapped, and darker towards the corners.
    col = 1.0 - exp(-col * 1.2);
    col *= 1.0 - 0.35 * smoothstep(0.4, 1.0, length(s));
    return float4(col, 1.0);
}

// ---- Sound ----------------------------------------------------------------
// An organ holds a chord of D under the window. Over it, panes ring like
// glass: a moment at a random place in each short slot of time, as the
// window has a point in each square.

float scaleNote(int i) {
    const float notes[16] = {
        293.66, 349.23, 392.00, 440.00, 523.25, 587.33, 698.46, 783.99,
        880.00, 1046.50, 1174.66, 1396.91, 1567.98, 1760.00, 2093.00,
        2349.32};
    return notes[clamp(i, 0, 15)];
}

// A glass bell: partials that aren't whole multiples, the higher ones
// dying sooner.
float bell(float f, float since) {
    float a = 1.0 - exp(-since * 400.0);
    float s = sin(TAU * f * since) * exp(-since * 2.5);
    s += 0.5 * sin(TAU * f * 2.76 * since) * exp(-since * 4.0);
    s += 0.25 * sin(TAU * f * 5.40 * since) * exp(-since * 7.0);
    return a * s;
}

// An organ pipe: a tone and two overtones.
float organPipe(float f, float t) {
    float s = sin(TAU * fract(f * t));
    s += 0.4 * sin(TAU * fract(2.0 * f * t));
    return s + 0.2 * sin(TAU * fract(3.0 * f * t));
}

float2 sound(float t, constant Params& p) {
    // D and A in three octaves, open, so the bells fit over them;
    // each a little sharper in the right ear.
    const float chord[5] = {73.42, 110.0, 146.83, 220.0, 293.66};
    float2 sum = 0.0;
    for (int i = 0; i < 5; i++) {
        float f = chord[i];
        sum += 0.095 * float2(organPipe(f, t), organPipe(f * 1.003, t));
    }
    sum *= 0.6;

    // The bells: one moment in each 0.45 s slot, rung or not.
    float slot = 0.45;
    float k = floor(t / slot);
    for (int i = 0; i < 6; i++) {
        float kk = k - float(i);
        if (kk < 0.0) break;
        uint n = uint(kk) * 4u;
        float at = (kk + hashU(n)) * slot;
        float since = t - at;
        if (since < 0.0) continue;
        if (hashU(n + 1u) > 0.5) continue;
        float f = scaleNote(5 + int(hashU(n + 2u) * 9.0));
        float pan = hashU(n + 3u);
        float b = bell(f, since) * 0.28;
        sum += float2(b * (1.0 - 0.7 * pan), b * (0.3 + 0.7 * pan));
    }
    return tanh(sum * 1.1) * 0.7 * smoothstep(0.0, 0.5, t) * p.volume;
}
7 lines from stage 1 that are gone
    float scale = p.cells / (2.0 * HALF);
    float3 v = voronoi(w * scale, 0.0);
⋯
    float edge = min(v.x / scale, -d);
⋯
    g.colour = glassColour(v.z, p.jewel);
⋯
    float3 col = g.colour * (1.0 - g.lead);
⋯
    col += float3(0.05, 0.04, 0.035) * smoothstep(0.0, px, d);
⋯
        float b = bell(f, since) * 0.45;

Stage 3 · The pool on the floor

Below the wall's foot is a floor, seen from 0.18 above it and 1 back from the wall: a row of the screen k times further below the eye than the wall's foot is a point of the floor 1 - 1/k of the way to the eye. sunSlope says which way the light falls: for each unit it drops it comes 1 / tan(sun) towards the eye, and moves sideways by up to 0.45, swinging slowly. pool starts at a point of the floor and goes back up that slope: it rises f.y / slope.y (its distance out from the wall over slope.y) by the time it reaches the wall, and that point of the wall is looked up with pane and through. So the floor shows the window again, stretched out and upside down (its top lands nearest the eye), and slides across the floor as the sun swings; a higher sun pulls it in towards the wall. Further out, the lead's edges are softer, as a real shadow's are. speed sets how fast the sun moves.

stage-3-the-pool-on-the-floor.metal18 new lines since stage 2
// Stage 3 · The pool on the floor
// Now a floor in front of the wall. Follow a point of it back up towards
// the sun: if the line reaches the wall through glass, that pane's colour
// lands there. The window is laid out again across the floor, stretched
// and upside down, and it slides as the sun crosses the sky.

// @param speed 1 0 3 How fast the sun moves and the clouds pass
// @param cells 7 3 14 How many panes across the window
// @param lead 0.008 0.003 0.02 How wide the lead between the panes is
// @param jewel 1 0 1 The glass from the kiln's ambers (0) to jewel colours (1)
// @param thickness 1.2 0.4 3 How thick the glass is: thicker glass is deeper
// @param sun 48 30 62 How high the sun stands, in degrees
// @param volume 0.8 0 1 Loudness of the sound

constant float TAU = 6.2831853;
// The window, in wall units with the floor at 0: half its width, its sill,
// where the arch springs and the radius of each side's arc.
constant float HALF = 0.22;
constant float SILL = 0.08;
constant float SPRING = 0.34;
constant float RADIUS = 0.286;
// How high the eye is, and how far back from the wall it stands.
constant float EYE = 0.18;
constant float BACK = 1.0;

// ---- Noise ----------------------------------------------------------------

float hash21(float2 p) {
    p = fract(p * float2(123.34, 456.21));
    p += dot(p, p + 45.32);
    return fract(p.x * p.y);
}

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;
}

// The kiln palette from Flowing Color Field.
float3 kiln(float t) {
    float3 c0 = float3(0.020, 0.012, 0.020);
    float3 c1 = float3(0.280, 0.040, 0.030);
    float3 c2 = float3(0.880, 0.260, 0.050);
    float3 c3 = float3(1.000, 0.680, 0.200);
    float3 c4 = float3(1.000, 0.970, 0.840);
    t = clamp(t, 0.0, 1.0);
    float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
    c = mix(c, c2, smoothstep(0.22, 0.50, t));
    c = mix(c, c3, smoothstep(0.50, 0.76, t));
    return mix(c, c4, smoothstep(0.76, 1.00, t));
}

// ---- Voronoi (Cracked Clay) -----------------------------------------------
// One point in each square of a grid; every pixel belongs to the nearest.

float2 cellPoint(float2 c, float t) {
    float2 h = hash22(c);
    return 0.5 + 0.4 * sin(t * 0.3 + TAU * h);
}

// The distance to the nearest edge between two cells, the distance to the
// nearest point, and a random number for the cell.
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 (i != 0 || j != 0) {
                md = min(md, dot(0.5 * (mr + r), normalize(r - mr)));
            }
        }
    }
    return float3(md, near, hash21(n + mg));
}

// ---- The window -----------------------------------------------------------

// How far a point of the wall is outside the window: below 0 inside. Up to
// the spring it is a rectangle; above, two arcs meet in a point, each with
// its centre on the far side of the middle.
float outline(float2 w) {
    float side = abs(w.x) - HALF;
    float2 c = float2(abs(w.x) + RADIUS - HALF, w.y - SPRING);
    float arch = length(c) - RADIUS;
    return max(w.y > SPRING ? arch : side, SILL - w.y);
}

// A pane's colour from its random number: one of six jewel colours, or a
// colour of the kiln, blended by jewel.
float3 glassColour(float id, float jewel) {
    const float3 jewels[6] = {
        float3(0.80, 0.07, 0.10), float3(0.10, 0.22, 0.85),
        float3(0.08, 0.60, 0.28), float3(0.98, 0.60, 0.10),
        float3(0.50, 0.14, 0.66), float3(1.00, 0.84, 0.36)};
    float3 j = jewels[min(int(id * 6.0), 5)];
    return mix(kiln(0.45 + 0.5 * id), j, jewel);
}

struct Pane { float3 colour; float lead; float id; };

// The glass at a point of the wall: its pane's colour, and how much of it
// lead covers (1 is all). soft is how blurred the lead's edges are.
Pane pane(float2 w, constant Params& p, float soft) {
    float d = outline(w);
    // Small golden panes line the frame.
    float border = 0.035;
    bool rim = d > -border;
    float scale = p.cells / (2.0 * HALF) * (rim ? 3.0 : 1.0);
    float3 v = voronoi(w * scale + (rim ? 40.0 : 0.0), 0.0);
    // The nearest lead: between panes, at the border, round the window.
    float edge = min(v.x / scale, abs(d + border));
    edge = min(edge, -d);
    // Iron saddle bars, as thick as the lead.
    float bar = abs(fract((w.y - SILL) / 0.16 + 0.5) - 0.5) * 0.16;
    edge = min(edge, bar);
    Pane g;
    g.lead = 1.0 - smoothstep(p.lead * 0.5, p.lead * 0.5 + soft, edge);
    g.colour = rim ? kiln(0.62 + 0.3 * v.z) : glassColour(v.z, p.jewel);
    g.id = v.z;
    return g;
}

// Beer-Lambert: each unit of thickness passes the glass's share of each
// colour again, and the thickness wanders.
float3 through(Pane g, float2 w, constant Params& p) {
    float wave = fbm(w * 30.0 + g.id * 40.0);
    float thick = p.thickness * (0.6 + 0.8 * wave);
    return pow(max(g.colour, 0.02), thick) * (1.0 - g.lead);
}

// ---- The sun and the sky --------------------------------------------------

// The sun's light falls and comes towards us: for each unit it falls, x is
// how far it moves sideways and y how far towards us. It swings side to side.
float2 sunSlope(float t, constant Params& p) {
    float side = 0.45 * sin(t * 0.12);
    return float2(side, 1.0 / tan(p.sun * TAU / 360.0));
}

// Integer hash, so noise stays exact for minutes.
float hashU(uint n) {
    n = (n << 13u) ^ n;
    n = n * (n * n * 15731u + 789221u) + 1376312589u;
    return float(n & 0x7fffffffu) / float(0x7fffffff);
}

// ---- The room -------------------------------------------------------------

// Stone blocks: a random shade for each, darker in the mortar between them,
// every other row offset by half a block.
float stones(float2 x, float2 size) {
    float row = floor(x.y / size.y);
    x.x += fract(row * 0.5) * size.x;
    float2 c = floor(x / size), f = x - c * size;
    float2 m = min(f, size - f);
    float mortar = smoothstep(0.0, 0.006, min(m.x, m.y));
    return (0.6 + 0.4 * hash21(c)) * (0.35 + 0.65 * mortar);
}

// The sun on the floor at f (across, and out from the wall): trace back up
// towards the sun to the wall. If that point is glass, the light comes
// through it; lead and stone keep it out.
float3 pool(float2 f, float2 slope, constant Params& p) {
    float up = f.y / slope.y;
    float2 w = float2(f.x - slope.x * up, up);
    Pane g = pane(w, p, 0.004 + 0.01 * up);
    return through(g, w, p);
}

float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
    float2 s = (uv - 0.5) * u.resolution / u.resolution.y;
    float px = 1.0 / u.resolution.y;
    float t = u.time * p.speed;
    float2 slope = sunSlope(t, p);
    float3 sun = float3(1.0, 0.93, 0.80) * 3.0;
    float3 col;
    if (s.y > -EYE) {
        // The wall, and the window in it, seen straight on.
        float2 w = float2(s.x, s.y + EYE);
        Pane g = pane(w, p, px * 1.5);
        // The glass glows brightest where the sun stands behind it.
        float3 view = normalize(float3(s, BACK));
        float3 toSun = normalize(float3(-slope.x, 1.0, slope.y));
        float glare = pow(max(dot(view, toSun), 0.0), 6.0);
        col = sun * (0.5 + 1.5 * glare) * through(g, w, p);
        // The stone, lit a little around the window by its own light.
        float d = outline(w);
        float stone = stones(w, float2(0.16, 0.08));
        float halo = exp(-max(d, 0.0) * 8.0);
        float3 wall = float3(0.16, 0.12, 0.10) * stone * (0.35 + 1.2 * halo);
        col += wall * smoothstep(0.0, px, d);
    } else {
        // The floor: a row k times as far below the eye as the wall's foot
        // is a point k times nearer.
        float k = -s.y / EYE;
        float2 f = float2(s.x / k, BACK * (1.0 - 1.0 / k));
        float stone = stones(f, float2(0.3));
        float tilt = sin(p.sun * TAU / 360.0);
        col = float3(0.16, 0.12, 0.10) * stone * 0.35;
        col += 0.45 * stone * sun * pool(f, slope, p) * tilt;
    }
    // Tone mapped, and darker towards the corners.
    col = 1.0 - exp(-col * 1.2);
    col *= 1.0 - 0.35 * smoothstep(0.4, 1.0, length(s));
    return float4(col, 1.0);
}

// ---- Sound ----------------------------------------------------------------
// An organ holds a chord of D under the window. Over it, panes ring like
// glass: a moment at a random place in each short slot of time, as the
// window has a point in each square.

float scaleNote(int i) {
    const float notes[16] = {
        293.66, 349.23, 392.00, 440.00, 523.25, 587.33, 698.46, 783.99,
        880.00, 1046.50, 1174.66, 1396.91, 1567.98, 1760.00, 2093.00,
        2349.32};
    return notes[clamp(i, 0, 15)];
}

// A glass bell: partials that aren't whole multiples, the higher ones
// dying sooner.
float bell(float f, float since) {
    float a = 1.0 - exp(-since * 400.0);
    float s = sin(TAU * f * since) * exp(-since * 2.5);
    s += 0.5 * sin(TAU * f * 2.76 * since) * exp(-since * 4.0);
    s += 0.25 * sin(TAU * f * 5.40 * since) * exp(-since * 7.0);
    return a * s;
}

// An organ pipe: a tone and two overtones.
float organPipe(float f, float t) {
    float s = sin(TAU * fract(f * t));
    s += 0.4 * sin(TAU * fract(2.0 * f * t));
    return s + 0.2 * sin(TAU * fract(3.0 * f * t));
}

float2 sound(float t, constant Params& p) {
    // D and A in three octaves, open, so the bells fit over them;
    // each a little sharper in the right ear.
    const float chord[5] = {73.42, 110.0, 146.83, 220.0, 293.66};
    float2 sum = 0.0;
    for (int i = 0; i < 5; i++) {
        float f = chord[i];
        sum += 0.095 * float2(organPipe(f, t), organPipe(f * 1.003, t));
    }
    sum *= 0.6;

    // The bells: one moment in each 0.45 s slot, rung or not.
    float slot = 0.45;
    float k = floor(t / slot);
    for (int i = 0; i < 6; i++) {
        float kk = k - float(i);
        if (kk < 0.0) break;
        uint n = uint(kk) * 4u;
        float at = (kk + hashU(n)) * slot;
        float since = t - at;
        if (since < 0.0) continue;
        if (hashU(n + 1u) > 0.5) continue;
        float f = scaleNote(5 + int(hashU(n + 2u) * 9.0));
        float pan = hashU(n + 3u);
        float b = bell(f, since) * 0.28;
        sum += float2(b * (1.0 - 0.7 * pan), b * (0.3 + 0.7 * pan));
    }
    return tanh(sum * 1.1) * 0.7 * smoothstep(0.0, 0.5, t) * p.volume;
}
2 lines from stage 2 that are gone
    float t = u.time;
⋯
    float3 col = sun * (0.5 + 1.5 * glare) * through(g, w, p);

Stage 4 · Stained Glass

The beams. beams walks 20 steps along the ray from the eye, through the air, up to the wall or the floor. At each step it goes back towards the sun to the wall, as pool does, and if that lands in the window it adds the pane's light, scaled by dust: fractal noise that drifts slowly. To keep it quick, it only asks which pane (cellId, the first half of voronoi) and leaves out the lead. Each pixel starts its steps at its own random offset, which turns banding into a fine, still grain. dust sets how strong they are. Last, sunlight is clouds passing the sun: slow noise in time, from 0.3 to 1, that dims the glass, the pool and the beams together.

the kiln palette and shade(), from the finished shader
// The kiln palette from Flowing Color Field.
float3 kiln(float t) {
    float3 c0 = float3(0.020, 0.012, 0.020);
    float3 c1 = float3(0.280, 0.040, 0.030);
    float3 c2 = float3(0.880, 0.260, 0.050);
    float3 c3 = float3(1.000, 0.680, 0.200);
    float3 c4 = float3(1.000, 0.970, 0.840);
    t = clamp(t, 0.0, 1.0);
    float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
    c = mix(c, c2, smoothstep(0.22, 0.50, t));
    c = mix(c, c3, smoothstep(0.50, 0.76, t));
    return mix(c, c4, smoothstep(0.76, 1.00, t));
}

float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
    float2 s = (uv - 0.5) * u.resolution / u.resolution.y;
    float px = 1.0 / u.resolution.y;
    float t = u.time * p.speed;
    float2 slope = sunSlope(t, p);
    float light = sunlight(t);
    float3 sun = float3(1.0, 0.93, 0.80) * 3.0 * light;
    float3 col;
    float far;
    if (s.y > -EYE) {
        // The wall, and the window in it, seen straight on.
        float2 w = float2(s.x, s.y + EYE);
        Pane g = pane(w, p, px * 1.5);
        // The glass glows brightest where the sun stands behind it.
        float3 view = normalize(float3(s, BACK));
        float3 toSun = normalize(float3(-slope.x, 1.0, slope.y));
        float glare = pow(max(dot(view, toSun), 0.0), 6.0);
        col = sun * (0.5 + 1.5 * glare) * through(g, w, p);
        // The stone, lit a little around the window by its own light.
        float d = outline(w);
        float stone = stones(w, float2(0.16, 0.08));
        float halo = exp(-max(d, 0.0) * 8.0) * light;
        float3 wall = float3(0.16, 0.12, 0.10) * stone * (0.35 + 1.2 * halo);
        col += wall * smoothstep(0.0, px, d);
        far = BACK;
    } else {
        // The floor: a row k times as far below the eye as the wall's foot
        // is a point k times nearer.
        float k = -s.y / EYE;
        float2 f = float2(s.x / k, BACK * (1.0 - 1.0 / k));
        float stone = stones(f, float2(0.3));
        float tilt = sin(p.sun * TAU / 360.0);
        col = float3(0.16, 0.12, 0.10) * stone * 0.35;
        col += 0.45 * stone * sun * pool(f, slope, p) * tilt;
        far = BACK / k;
    }
    col += sun * beams(s, far, slope, t, p);
    // Tone mapped, and darker towards the corners.
    col = 1.0 - exp(-col * 1.2);
    col *= 1.0 - 0.35 * smoothstep(0.4, 1.0, length(s));
    return float4(col, 1.0);
}
stained-glass.metal, the whole file50 new lines since stage 3
// Ray Kiln · Stained Glass
//
// @episode Stained Glass
// @length 60
// @variation 30
// @variations 1,3,4,5,7,9,10,12
// @still 36.0
// @teaches Voronoi panes, lead lines, light coloured by the glass it passes
// @category Fragment
// @tags voronoi, stained glass, transmission, light shafts, procedural sound
// @final Stained Glass | Dust in the air shows the beams, and clouds passing the sun dim the window, the pool and the organ together.
//
// An arched window in a stone wall, the sun behind it. The panes are Voronoi
// cells, each its own colour of glass, held in lead. Light that crosses glass
// keeps only the colour the glass lets through, and less of it the thicker
// the glass is. Follow a point of the floor back up towards the sun: if the
// line meets glass, that pane's colour lands there, so the window is laid out
// again across the floor, stretched, and slides as the sun moves. Dust in the
// air catches the same light as beams. Clouds dim it all, and the organ with
// it; panes catching the sun ring like glass.
// Each @param line below becomes a slider in the host app and a row in the page's parameters table:
//   // @param name default min max description

// @param speed 1 0 3 How fast the sun moves and the clouds pass
// @param cells 7 3 14 How many panes across the window
// @param lead 0.008 0.003 0.02 How wide the lead between the panes is
// @param jewel 1 0 1 The glass from the kiln's ambers (0) to jewel colours (1)
// @param thickness 1.2 0.4 3 How thick the glass is: thicker glass is deeper
// @param sun 48 30 62 How high the sun stands, in degrees
// @param dust 1 0 2 How much dust in the air shows the beams
// @param volume 0.8 0 1 Loudness of the sound

constant float TAU = 6.2831853;
// The window, in wall units with the floor at 0: half its width, its sill,
// where the arch springs and the radius of each side's arc.
constant float HALF = 0.22;
constant float SILL = 0.08;
constant float SPRING = 0.34;
constant float RADIUS = 0.286;
// How high the eye is, and how far back from the wall it stands.
constant float EYE = 0.18;
constant float BACK = 1.0;

// ---- Noise ----------------------------------------------------------------

float hash21(float2 p) {
    p = fract(p * float2(123.34, 456.21));
    p += dot(p, p + 45.32);
    return fract(p.x * p.y);
}

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;
}

// The kiln palette from Flowing Color Field.
float3 kiln(float t) {
    float3 c0 = float3(0.020, 0.012, 0.020);
    float3 c1 = float3(0.280, 0.040, 0.030);
    float3 c2 = float3(0.880, 0.260, 0.050);
    float3 c3 = float3(1.000, 0.680, 0.200);
    float3 c4 = float3(1.000, 0.970, 0.840);
    t = clamp(t, 0.0, 1.0);
    float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
    c = mix(c, c2, smoothstep(0.22, 0.50, t));
    c = mix(c, c3, smoothstep(0.50, 0.76, t));
    return mix(c, c4, smoothstep(0.76, 1.00, t));
}

// ---- Voronoi (Cracked Clay) -----------------------------------------------
// One point in each square of a grid; every pixel belongs to the nearest.

float2 cellPoint(float2 c, float t) {
    float2 h = hash22(c);
    return 0.5 + 0.4 * sin(t * 0.3 + TAU * h);
}

// The distance to the nearest edge between two cells, the distance to the
// nearest point, and a random number for the cell.
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 (i != 0 || j != 0) {
                md = min(md, dot(0.5 * (mr + r), normalize(r - mr)));
            }
        }
    }
    return float3(md, near, hash21(n + mg));
}

// Only which cell: the first half of voronoi, for the beams.
float cellId(float2 x) {
    float2 n = floor(x), f = fract(x);
    float2 mg = 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, 0.0) - f;
            float d = dot(r, r);
            if (d < md) { md = d; mg = g; }
        }
    }
    return hash21(n + mg);
}

// ---- The window -----------------------------------------------------------

// How far a point of the wall is outside the window: below 0 inside. Up to
// the spring it is a rectangle; above, two arcs meet in a point, each with
// its centre on the far side of the middle.
float outline(float2 w) {
    float side = abs(w.x) - HALF;
    float2 c = float2(abs(w.x) + RADIUS - HALF, w.y - SPRING);
    float arch = length(c) - RADIUS;
    return max(w.y > SPRING ? arch : side, SILL - w.y);
}

// A pane's colour from its random number: one of six jewel colours, or a
// colour of the kiln, blended by jewel.
float3 glassColour(float id, float jewel) {
    const float3 jewels[6] = {
        float3(0.80, 0.07, 0.10), float3(0.10, 0.22, 0.85),
        float3(0.08, 0.60, 0.28), float3(0.98, 0.60, 0.10),
        float3(0.50, 0.14, 0.66), float3(1.00, 0.84, 0.36)};
    float3 j = jewels[min(int(id * 6.0), 5)];
    return mix(kiln(0.45 + 0.5 * id), j, jewel);
}

struct Pane { float3 colour; float lead; float id; };

// The glass at a point of the wall: its pane's colour, and how much of it
// lead covers (1 is all). soft is how blurred the lead's edges are.
Pane pane(float2 w, constant Params& p, float soft) {
    float d = outline(w);
    // Small golden panes line the frame.
    float border = 0.035;
    bool rim = d > -border;
    float scale = p.cells / (2.0 * HALF) * (rim ? 3.0 : 1.0);
    float3 v = voronoi(w * scale + (rim ? 40.0 : 0.0), 0.0);
    // The nearest lead: between panes, at the border, round the window.
    float edge = min(v.x / scale, abs(d + border));
    edge = min(edge, -d);
    // Iron saddle bars, as thick as the lead.
    float bar = abs(fract((w.y - SILL) / 0.16 + 0.5) - 0.5) * 0.16;
    edge = min(edge, bar);
    Pane g;
    g.lead = 1.0 - smoothstep(p.lead * 0.5, p.lead * 0.5 + soft, edge);
    g.colour = rim ? kiln(0.62 + 0.3 * v.z) : glassColour(v.z, p.jewel);
    g.id = v.z;
    return g;
}

// Beer-Lambert: each unit of thickness passes the glass's share of each
// colour again, and the thickness wanders.
float3 through(Pane g, float2 w, constant Params& p) {
    float wave = fbm(w * 30.0 + g.id * 40.0);
    float thick = p.thickness * (0.6 + 0.8 * wave);
    return pow(max(g.colour, 0.02), thick) * (1.0 - g.lead);
}

// ---- The sun and the sky --------------------------------------------------

// The sun's light falls and comes towards us: for each unit it falls, x is
// how far it moves sideways and y how far towards us. It swings side to side.
float2 sunSlope(float t, constant Params& p) {
    float side = 0.45 * sin(t * 0.12);
    return float2(side, 1.0 / tan(p.sun * TAU / 360.0));
}

// Integer hash, so slow noise stays exact for minutes.
float hashU(uint n) {
    n = (n << 13u) ^ n;
    n = n * (n * n * 15731u + 789221u) + 1376312589u;
    return float(n & 0x7fffffffu) / float(0x7fffffff);
}

// How much sun gets past the clouds, 0.3 to 1: slow noise in time, the same
// for the picture and the organ.
float sunlight(float t) {
    float x = t * 0.15;
    float i = floor(x), f = fract(x);
    f = f * f * (3.0 - 2.0 * f);
    float n = mix(hashU(uint(i) + 7u), hashU(uint(i) + 8u), f);
    return 0.3 + 0.7 * smoothstep(0.15, 0.7, n);
}

// ---- The room -------------------------------------------------------------

// Stone blocks: a random shade for each, darker in the mortar between them,
// every other row offset by half a block.
float stones(float2 x, float2 size) {
    float row = floor(x.y / size.y);
    x.x += fract(row * 0.5) * size.x;
    float2 c = floor(x / size), f = x - c * size;
    float2 m = min(f, size - f);
    float mortar = smoothstep(0.0, 0.006, min(m.x, m.y));
    return (0.6 + 0.4 * hash21(c)) * (0.35 + 0.65 * mortar);
}

// The sun on the floor at f (across, and out from the wall): trace back up
// towards the sun to the wall. If that point is glass, the light comes
// through it; lead and stone keep it out.
float3 pool(float2 f, float2 slope, constant Params& p) {
    float up = f.y / slope.y;
    float2 w = float2(f.x - slope.x * up, up);
    Pane g = pane(w, p, 0.004 + 0.01 * up);
    return through(g, w, p);
}

// The beams: walk from the eye through the dusty air, up to far. Each step,
// trace back towards the sun to the wall: through glass, that pane's light
// lights the dust there.
float3 beams(float2 s, float far, float2 slope, float t,
             constant Params& p) {
    float3 sum = 0.0;
    float scale = p.cells / (2.0 * HALF);
    float jitter = hash21(s * 917.0);
    for (int i = 0; i < 20; i++) {
        float c = mix(0.1, far, (float(i) + jitter) / 20.0);
        float3 at = float3(s * c / BACK, c - BACK);
        at.y += EYE;
        float2 w = float2(at.x + slope.x * at.z / slope.y,
                          at.y - at.z / slope.y);
        float inside = 1.0 - smoothstep(-0.01, 0.0, outline(w));
        if (inside <= 0.0) continue;
        float3 glass = glassColour(cellId(w * scale), p.jewel);
        float dust = fbm(at.xy * 7.0 + float2(t * 0.04, -t * 0.02));
        sum += pow(glass, p.thickness) * inside * (0.3 + dust);
    }
    return sum * (far - 0.1) / 20.0 * 0.7 * p.dust;
}

float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
    float2 s = (uv - 0.5) * u.resolution / u.resolution.y;
    float px = 1.0 / u.resolution.y;
    float t = u.time * p.speed;
    float2 slope = sunSlope(t, p);
    float light = sunlight(t);
    float3 sun = float3(1.0, 0.93, 0.80) * 3.0 * light;
    float3 col;
    float far;
    if (s.y > -EYE) {
        // The wall, and the window in it, seen straight on.
        float2 w = float2(s.x, s.y + EYE);
        Pane g = pane(w, p, px * 1.5);
        // The glass glows brightest where the sun stands behind it.
        float3 view = normalize(float3(s, BACK));
        float3 toSun = normalize(float3(-slope.x, 1.0, slope.y));
        float glare = pow(max(dot(view, toSun), 0.0), 6.0);
        col = sun * (0.5 + 1.5 * glare) * through(g, w, p);
        // The stone, lit a little around the window by its own light.
        float d = outline(w);
        float stone = stones(w, float2(0.16, 0.08));
        float halo = exp(-max(d, 0.0) * 8.0) * light;
        float3 wall = float3(0.16, 0.12, 0.10) * stone * (0.35 + 1.2 * halo);
        col += wall * smoothstep(0.0, px, d);
        far = BACK;
    } else {
        // The floor: a row k times as far below the eye as the wall's foot
        // is a point k times nearer.
        float k = -s.y / EYE;
        float2 f = float2(s.x / k, BACK * (1.0 - 1.0 / k));
        float stone = stones(f, float2(0.3));
        float tilt = sin(p.sun * TAU / 360.0);
        col = float3(0.16, 0.12, 0.10) * stone * 0.35;
        col += 0.45 * stone * sun * pool(f, slope, p) * tilt;
        far = BACK / k;
    }
    col += sun * beams(s, far, slope, t, p);
    // Tone mapped, and darker towards the corners.
    col = 1.0 - exp(-col * 1.2);
    col *= 1.0 - 0.35 * smoothstep(0.4, 1.0, length(s));
    return float4(col, 1.0);
}

// ---- Sound ----------------------------------------------------------------
// An organ holds a chord of D, louder when the sun is out. Over it, panes
// ring like glass: a moment at a random place in each short slot of time,
// as the window has a point in each square, and more of them in the sun.

float scaleNote(int i) {
    const float notes[16] = {
        293.66, 349.23, 392.00, 440.00, 523.25, 587.33, 698.46, 783.99,
        880.00, 1046.50, 1174.66, 1396.91, 1567.98, 1760.00, 2093.00,
        2349.32};
    return notes[clamp(i, 0, 15)];
}

// A glass bell: partials that aren't whole multiples, the higher ones
// dying sooner.
float bell(float f, float since) {
    float a = 1.0 - exp(-since * 400.0);
    float s = sin(TAU * f * since) * exp(-since * 2.5);
    s += 0.5 * sin(TAU * f * 2.76 * since) * exp(-since * 4.0);
    s += 0.25 * sin(TAU * f * 5.40 * since) * exp(-since * 7.0);
    return a * s;
}

// An organ pipe: a tone and two overtones.
float organPipe(float f, float t) {
    float s = sin(TAU * fract(f * t));
    s += 0.4 * sin(TAU * fract(2.0 * f * t));
    return s + 0.2 * sin(TAU * fract(3.0 * f * t));
}

float2 sound(float t, constant Params& p) {
    float light = sunlight(t * p.speed);
    // D and A in three octaves, open, so the bells fit over them;
    // each a little sharper in the right ear.
    const float chord[5] = {73.42, 110.0, 146.83, 220.0, 293.66};
    float2 sum = 0.0;
    for (int i = 0; i < 5; i++) {
        float f = chord[i];
        sum += 0.095 * float2(organPipe(f, t), organPipe(f * 1.003, t));
    }
    sum *= 0.35 + 0.65 * light;

    // The bells: one moment in each 0.45 s slot, rung or not.
    float slot = 0.45;
    float k = floor(t / slot);
    for (int i = 0; i < 6; i++) {
        float kk = k - float(i);
        if (kk < 0.0) break;
        uint n = uint(kk) * 4u;
        float at = (kk + hashU(n)) * slot;
        float since = t - at;
        if (since < 0.0) continue;
        if (hashU(n + 1u) > 0.15 + 0.5 * sunlight(at * p.speed)) continue;
        float f = scaleNote(5 + int(hashU(n + 2u) * 9.0));
        float pan = hashU(n + 3u);
        float b = bell(f, since) * 0.28;
        sum += float2(b * (1.0 - 0.7 * pan), b * (0.3 + 0.7 * pan));
    }
    return tanh(sum * 1.1) * 0.7 * smoothstep(0.0, 0.5, t) * p.volume;
}
4 lines from stage 3 that are gone
    float3 sun = float3(1.0, 0.93, 0.80) * 3.0;
⋯
        float halo = exp(-max(d, 0.0) * 8.0);
⋯
    sum *= 0.6;
⋯
        if (hashU(n + 1u) > 0.5) continue;

The sound

An organ holds D and A in three octaves (73, 110, 147, 220 and 294 Hz), each pipe a tone and two overtones, a little sharper in the right ear than the left so it beats slowly. It is open fifths with no third on purpose, so the bells always fit over it. Its loudness follows sunlight, the same clouds the picture uses: it swells when the sun comes out.

Over it, panes ring like glass. Time is cut into slots of 0.45 s with one moment at a random place in each, as the window has one point in each square, and a slot rings more often in the sun: its chance is 0.15 + 0.5 × sunlight. A ring is a glass bell: a note from the D minor pentatonic table that Kaleidoscope uses (scaleNote, D5 to A6), with partials at 2.76 and 5.4 times it that die away faster than the note, placed at a random point between the ears. tanh rounds off the loudest moments, and the sound fades in over half a second.

At the defaults the peak is about −7.1 dBFS and the average (RMS) about −20.3 dBFS over the minute; with volume at its highest and speed at 3, −5.8 dBFS and −18.2 dBFS.

New in Ray Kiln

Colour vision in the playground. A menu under the picture, beside Loupe, shows the live picture as someone with protanopia (no red cones), deuteranopia (no green cones) or tritanopia (no blue cones) sees it, or in grey. After each frame is drawn, it is drawn back over itself through a 3 × 3 colour matrix, in linear light, using the matrices of Machado, Oliveira and Fernandes (2009) at full strength, which most simulators use; grey is the colour's luminance. It works on any shader, particles and grids too, and only on screen: exports, stills, the loupe and the flash check keep the shader's own colours.

Open Stained Glass in the playground and pick Deuteranopia: the ruby and emerald panes turn into nearly the same olive, and violet and cobalt into the same blue, while cobalt against amber stays as clear as ever. Protanopia is close, but it darkens red, so ruby becomes a dark olive told from emerald only by its brightness. Tritanopia keeps red and green apart and turns cobalt and emerald into one teal instead. Grey shows which panes differ only by hue: violet and cobalt become the same grey. Then slide jewel towards 0: the kiln's colours still read in every look, because they run from dark to bright. A window, or any shader, meant to be read by everyone shouldn't rest on hue alone.

Try this

  • Set sun to 30: the light comes in low and the pool stretches out past the bottom of the picture. At 62 it lies close under the wall.
  • Set cells to 3 and lead to 0.02 for a few big panes in heavy lead, or cells to 14 for a mosaic.
  • Set thickness to 3: every pane goes deep and dark, and the pool with it. At 0.4 the glass is pale and the light nearly white.
  • Turn dust up to 2 for a dusty church, or down to 0 for clean air with no beams.
  • Turn on Colour vision in the playground and slide jewel: find the setting where the window still reads in every look.

Parameters

ParameterDefaultRangeWhat it does
speed10 to 3How fast the sun moves and the clouds pass
cells73 to 14How many panes across the window
lead0.0080.003 to 0.02How wide the lead between the panes is
jewel10 to 1The glass from the kiln's ambers (0) to jewel colours (1)
thickness1.20.4 to 3How thick the glass is: thicker glass is deeper
sun4830 to 62How high the sun stands, in degrees
dust10 to 2How much dust in the air shows the beams
volume0.80 to 1Loudness 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.

The full source

This is the whole episode: the picture, the parameters and the sound.

stained-glass.metal
// Ray Kiln · Stained Glass
//
// @episode Stained Glass
// @length 60
// @variation 30
// @variations 1,3,4,5,7,9,10,12
// @still 36.0
// @teaches Voronoi panes, lead lines, light coloured by the glass it passes
// @category Fragment
// @tags voronoi, stained glass, transmission, light shafts, procedural sound
// @final Stained Glass | Dust in the air shows the beams, and clouds passing the sun dim the window, the pool and the organ together.
//
// An arched window in a stone wall, the sun behind it. The panes are Voronoi
// cells, each its own colour of glass, held in lead. Light that crosses glass
// keeps only the colour the glass lets through, and less of it the thicker
// the glass is. Follow a point of the floor back up towards the sun: if the
// line meets glass, that pane's colour lands there, so the window is laid out
// again across the floor, stretched, and slides as the sun moves. Dust in the
// air catches the same light as beams. Clouds dim it all, and the organ with
// it; panes catching the sun ring like glass.
// Each @param line below becomes a slider in the host app and a row in the page's parameters table:
//   // @param name default min max description

// @param speed 1 0 3 How fast the sun moves and the clouds pass
// @param cells 7 3 14 How many panes across the window
// @param lead 0.008 0.003 0.02 How wide the lead between the panes is
// @param jewel 1 0 1 The glass from the kiln's ambers (0) to jewel colours (1)
// @param thickness 1.2 0.4 3 How thick the glass is: thicker glass is deeper
// @param sun 48 30 62 How high the sun stands, in degrees
// @param dust 1 0 2 How much dust in the air shows the beams
// @param volume 0.8 0 1 Loudness of the sound

constant float TAU = 6.2831853;
// The window, in wall units with the floor at 0: half its width, its sill,
// where the arch springs and the radius of each side's arc.
constant float HALF = 0.22;
constant float SILL = 0.08;
constant float SPRING = 0.34;
constant float RADIUS = 0.286;
// How high the eye is, and how far back from the wall it stands.
constant float EYE = 0.18;
constant float BACK = 1.0;

// ---- Noise ----------------------------------------------------------------

float hash21(float2 p) {
    p = fract(p * float2(123.34, 456.21));
    p += dot(p, p + 45.32);
    return fract(p.x * p.y);
}

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;
}

// The kiln palette from Flowing Color Field.
float3 kiln(float t) {
    float3 c0 = float3(0.020, 0.012, 0.020);
    float3 c1 = float3(0.280, 0.040, 0.030);
    float3 c2 = float3(0.880, 0.260, 0.050);
    float3 c3 = float3(1.000, 0.680, 0.200);
    float3 c4 = float3(1.000, 0.970, 0.840);
    t = clamp(t, 0.0, 1.0);
    float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
    c = mix(c, c2, smoothstep(0.22, 0.50, t));
    c = mix(c, c3, smoothstep(0.50, 0.76, t));
    return mix(c, c4, smoothstep(0.76, 1.00, t));
}

// ---- Voronoi (Cracked Clay) -----------------------------------------------
// One point in each square of a grid; every pixel belongs to the nearest.

float2 cellPoint(float2 c, float t) {
    float2 h = hash22(c);
    return 0.5 + 0.4 * sin(t * 0.3 + TAU * h);
}

// The distance to the nearest edge between two cells, the distance to the
// nearest point, and a random number for the cell.
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 (i != 0 || j != 0) {
                md = min(md, dot(0.5 * (mr + r), normalize(r - mr)));
            }
        }
    }
    return float3(md, near, hash21(n + mg));
}

// Only which cell: the first half of voronoi, for the beams.
float cellId(float2 x) {
    float2 n = floor(x), f = fract(x);
    float2 mg = 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, 0.0) - f;
            float d = dot(r, r);
            if (d < md) { md = d; mg = g; }
        }
    }
    return hash21(n + mg);
}

// ---- The window -----------------------------------------------------------

// How far a point of the wall is outside the window: below 0 inside. Up to
// the spring it is a rectangle; above, two arcs meet in a point, each with
// its centre on the far side of the middle.
float outline(float2 w) {
    float side = abs(w.x) - HALF;
    float2 c = float2(abs(w.x) + RADIUS - HALF, w.y - SPRING);
    float arch = length(c) - RADIUS;
    return max(w.y > SPRING ? arch : side, SILL - w.y);
}

// A pane's colour from its random number: one of six jewel colours, or a
// colour of the kiln, blended by jewel.
float3 glassColour(float id, float jewel) {
    const float3 jewels[6] = {
        float3(0.80, 0.07, 0.10), float3(0.10, 0.22, 0.85),
        float3(0.08, 0.60, 0.28), float3(0.98, 0.60, 0.10),
        float3(0.50, 0.14, 0.66), float3(1.00, 0.84, 0.36)};
    float3 j = jewels[min(int(id * 6.0), 5)];
    return mix(kiln(0.45 + 0.5 * id), j, jewel);
}

struct Pane { float3 colour; float lead; float id; };

// The glass at a point of the wall: its pane's colour, and how much of it
// lead covers (1 is all). soft is how blurred the lead's edges are.
Pane pane(float2 w, constant Params& p, float soft) {
    float d = outline(w);
    // Small golden panes line the frame.
    float border = 0.035;
    bool rim = d > -border;
    float scale = p.cells / (2.0 * HALF) * (rim ? 3.0 : 1.0);
    float3 v = voronoi(w * scale + (rim ? 40.0 : 0.0), 0.0);
    // The nearest lead: between panes, at the border, round the window.
    float edge = min(v.x / scale, abs(d + border));
    edge = min(edge, -d);
    // Iron saddle bars, as thick as the lead.
    float bar = abs(fract((w.y - SILL) / 0.16 + 0.5) - 0.5) * 0.16;
    edge = min(edge, bar);
    Pane g;
    g.lead = 1.0 - smoothstep(p.lead * 0.5, p.lead * 0.5 + soft, edge);
    g.colour = rim ? kiln(0.62 + 0.3 * v.z) : glassColour(v.z, p.jewel);
    g.id = v.z;
    return g;
}

// Beer-Lambert: each unit of thickness passes the glass's share of each
// colour again, and the thickness wanders.
float3 through(Pane g, float2 w, constant Params& p) {
    float wave = fbm(w * 30.0 + g.id * 40.0);
    float thick = p.thickness * (0.6 + 0.8 * wave);
    return pow(max(g.colour, 0.02), thick) * (1.0 - g.lead);
}

// ---- The sun and the sky --------------------------------------------------

// The sun's light falls and comes towards us: for each unit it falls, x is
// how far it moves sideways and y how far towards us. It swings side to side.
float2 sunSlope(float t, constant Params& p) {
    float side = 0.45 * sin(t * 0.12);
    return float2(side, 1.0 / tan(p.sun * TAU / 360.0));
}

// Integer hash, so slow noise stays exact for minutes.
float hashU(uint n) {
    n = (n << 13u) ^ n;
    n = n * (n * n * 15731u + 789221u) + 1376312589u;
    return float(n & 0x7fffffffu) / float(0x7fffffff);
}

// How much sun gets past the clouds, 0.3 to 1: slow noise in time, the same
// for the picture and the organ.
float sunlight(float t) {
    float x = t * 0.15;
    float i = floor(x), f = fract(x);
    f = f * f * (3.0 - 2.0 * f);
    float n = mix(hashU(uint(i) + 7u), hashU(uint(i) + 8u), f);
    return 0.3 + 0.7 * smoothstep(0.15, 0.7, n);
}

// ---- The room -------------------------------------------------------------

// Stone blocks: a random shade for each, darker in the mortar between them,
// every other row offset by half a block.
float stones(float2 x, float2 size) {
    float row = floor(x.y / size.y);
    x.x += fract(row * 0.5) * size.x;
    float2 c = floor(x / size), f = x - c * size;
    float2 m = min(f, size - f);
    float mortar = smoothstep(0.0, 0.006, min(m.x, m.y));
    return (0.6 + 0.4 * hash21(c)) * (0.35 + 0.65 * mortar);
}

// The sun on the floor at f (across, and out from the wall): trace back up
// towards the sun to the wall. If that point is glass, the light comes
// through it; lead and stone keep it out.
float3 pool(float2 f, float2 slope, constant Params& p) {
    float up = f.y / slope.y;
    float2 w = float2(f.x - slope.x * up, up);
    Pane g = pane(w, p, 0.004 + 0.01 * up);
    return through(g, w, p);
}

// The beams: walk from the eye through the dusty air, up to far. Each step,
// trace back towards the sun to the wall: through glass, that pane's light
// lights the dust there.
float3 beams(float2 s, float far, float2 slope, float t,
             constant Params& p) {
    float3 sum = 0.0;
    float scale = p.cells / (2.0 * HALF);
    float jitter = hash21(s * 917.0);
    for (int i = 0; i < 20; i++) {
        float c = mix(0.1, far, (float(i) + jitter) / 20.0);
        float3 at = float3(s * c / BACK, c - BACK);
        at.y += EYE;
        float2 w = float2(at.x + slope.x * at.z / slope.y,
                          at.y - at.z / slope.y);
        float inside = 1.0 - smoothstep(-0.01, 0.0, outline(w));
        if (inside <= 0.0) continue;
        float3 glass = glassColour(cellId(w * scale), p.jewel);
        float dust = fbm(at.xy * 7.0 + float2(t * 0.04, -t * 0.02));
        sum += pow(glass, p.thickness) * inside * (0.3 + dust);
    }
    return sum * (far - 0.1) / 20.0 * 0.7 * p.dust;
}

float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
    float2 s = (uv - 0.5) * u.resolution / u.resolution.y;
    float px = 1.0 / u.resolution.y;
    float t = u.time * p.speed;
    float2 slope = sunSlope(t, p);
    float light = sunlight(t);
    float3 sun = float3(1.0, 0.93, 0.80) * 3.0 * light;
    float3 col;
    float far;
    if (s.y > -EYE) {
        // The wall, and the window in it, seen straight on.
        float2 w = float2(s.x, s.y + EYE);
        Pane g = pane(w, p, px * 1.5);
        // The glass glows brightest where the sun stands behind it.
        float3 view = normalize(float3(s, BACK));
        float3 toSun = normalize(float3(-slope.x, 1.0, slope.y));
        float glare = pow(max(dot(view, toSun), 0.0), 6.0);
        col = sun * (0.5 + 1.5 * glare) * through(g, w, p);
        // The stone, lit a little around the window by its own light.
        float d = outline(w);
        float stone = stones(w, float2(0.16, 0.08));
        float halo = exp(-max(d, 0.0) * 8.0) * light;
        float3 wall = float3(0.16, 0.12, 0.10) * stone * (0.35 + 1.2 * halo);
        col += wall * smoothstep(0.0, px, d);
        far = BACK;
    } else {
        // The floor: a row k times as far below the eye as the wall's foot
        // is a point k times nearer.
        float k = -s.y / EYE;
        float2 f = float2(s.x / k, BACK * (1.0 - 1.0 / k));
        float stone = stones(f, float2(0.3));
        float tilt = sin(p.sun * TAU / 360.0);
        col = float3(0.16, 0.12, 0.10) * stone * 0.35;
        col += 0.45 * stone * sun * pool(f, slope, p) * tilt;
        far = BACK / k;
    }
    col += sun * beams(s, far, slope, t, p);
    // Tone mapped, and darker towards the corners.
    col = 1.0 - exp(-col * 1.2);
    col *= 1.0 - 0.35 * smoothstep(0.4, 1.0, length(s));
    return float4(col, 1.0);
}

// ---- Sound ----------------------------------------------------------------
// An organ holds a chord of D, louder when the sun is out. Over it, panes
// ring like glass: a moment at a random place in each short slot of time,
// as the window has a point in each square, and more of them in the sun.

float scaleNote(int i) {
    const float notes[16] = {
        293.66, 349.23, 392.00, 440.00, 523.25, 587.33, 698.46, 783.99,
        880.00, 1046.50, 1174.66, 1396.91, 1567.98, 1760.00, 2093.00,
        2349.32};
    return notes[clamp(i, 0, 15)];
}

// A glass bell: partials that aren't whole multiples, the higher ones
// dying sooner.
float bell(float f, float since) {
    float a = 1.0 - exp(-since * 400.0);
    float s = sin(TAU * f * since) * exp(-since * 2.5);
    s += 0.5 * sin(TAU * f * 2.76 * since) * exp(-since * 4.0);
    s += 0.25 * sin(TAU * f * 5.40 * since) * exp(-since * 7.0);
    return a * s;
}

// An organ pipe: a tone and two overtones.
float organPipe(float f, float t) {
    float s = sin(TAU * fract(f * t));
    s += 0.4 * sin(TAU * fract(2.0 * f * t));
    return s + 0.2 * sin(TAU * fract(3.0 * f * t));
}

float2 sound(float t, constant Params& p) {
    float light = sunlight(t * p.speed);
    // D and A in three octaves, open, so the bells fit over them;
    // each a little sharper in the right ear.
    const float chord[5] = {73.42, 110.0, 146.83, 220.0, 293.66};
    float2 sum = 0.0;
    for (int i = 0; i < 5; i++) {
        float f = chord[i];
        sum += 0.095 * float2(organPipe(f, t), organPipe(f * 1.003, t));
    }
    sum *= 0.35 + 0.65 * light;

    // The bells: one moment in each 0.45 s slot, rung or not.
    float slot = 0.45;
    float k = floor(t / slot);
    for (int i = 0; i < 6; i++) {
        float kk = k - float(i);
        if (kk < 0.0) break;
        uint n = uint(kk) * 4u;
        float at = (kk + hashU(n)) * slot;
        float since = t - at;
        if (since < 0.0) continue;
        if (hashU(n + 1u) > 0.15 + 0.5 * sunlight(at * p.speed)) continue;
        float f = scaleNote(5 + int(hashU(n + 2u) * 9.0));
        float pan = hashU(n + 3u);
        float b = bell(f, since) * 0.28;
        sum += float2(b * (1.0 - 0.7 * pan), b * (0.3 + 0.7 * pan));
    }
    return tanh(sum * 1.1) * 0.7 * smoothstep(0.0, 0.5, t) * p.volume;
}

Running it in your own project

Every Ray Kiln shader is the same shape. The standalone download above is the shader with the small wrapper around it that the Ray Kiln host adds, so it compiles with the ordinary Metal compiler and runs in your own app. It defines three entry points: rk_vertex (a full-screen triangle), rk_fragment (calls the shader's shade) and, for shaders with sound, the compute kernel rk_sound.

To draw it, pass the uniforms at fragment buffer 0 and the parameter values at buffer 1, in the order they are declared:

Drawing it, in Swift
struct Uniforms {
    var resolution: SIMD2<Float>
    var time: Float
    var timeDelta: Float
    var mouse: SIMD2<Float>
    var frame: UInt32
    var pad: UInt32 = 0
}

let library = try device.makeLibrary(source: standaloneSource, options: nil)
let descriptor = MTLRenderPipelineDescriptor()
descriptor.vertexFunction = library.makeFunction(name: "rk_vertex")
descriptor.fragmentFunction = library.makeFunction(name: "rk_fragment")
descriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
let pipeline = try device.makeRenderPipelineState(descriptor: descriptor)

// Each frame, inside a render pass:
var uniforms = Uniforms(resolution: size, time: time, timeDelta: dt, mouse: mouse, frame: frame)
var params: [Float] = [1, 7, 0.008, 1, 1.2, 48, 1, 0.8]     // the @param defaults, in the order they are declared
encoder.setRenderPipelineState(pipeline)
encoder.setFragmentBytes(&uniforms, length: MemoryLayout<Uniforms>.stride, index: 0)
encoder.setFragmentBytes(&params, 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 Light & Materials