Shader 23

Neon Sign

A neon sign, the kiln's mark over the word OPEN, on a brick wall at night. Every tube is a stroke, the distance to a line rather than to a shape's inside; its glow is an inverse square of that distance, its power is switched by the clock, so the sign warms up with a stutter and the faulty tubes flicker and buzz, and the wall is lit by the same light.

· Shapes & Symmetry · Fragment shader

Teaches 2D distance-field strokes, glow, flicker

Builds on 02 · Morphing Shapes, 05 · Cracked Clay (integer hash)

Watch

The idea

Episode 2 drew shapes from their distance: negative inside, positive outside, zero on the edge. A neon tube is a different use of the same number. It has no inside; it is a line of glass, and what matters is how far each pixel is from that line, on either side. That is a stroke: the distance to a segment, a ring or an arch, and a tube wherever it is smaller than the tube's radius. Any shape's outline is a stroke too: abs(d) of its distance turns its inside out.

Then the light. Close to the line the tube is white-hot glass, and farther out its colour falls away as a glow, an inverse square of the distance that never quite reaches zero, so every pixel gets a little light from every tube. And neon is never quite steady: each tube has a power that the clock switches on and off, so the sign warms up when it is turned on and the faulty tubes flicker.

It is built in five steps, and each step is a shader you can run on its own.

Stage 1 · A stroke is a distance to a line

sdSegment is the distance from a point to the segment from a to b: the point is projected onto the line, the projection is clamped to the two ends, and the distance is to that nearest point. Every pixel within 0.012 of it is white, blended over one pixel. On the left is a slanted line. On the right, the O: the distance to a short upright segment less 0.34 is a stadium, round at the top and bottom, and abs of that is its outline, a tube that runs all the way round.

stage-1-stroke.metal
// Stage 1 · A stroke is a distance to a line
// A shape's distance is negative inside it. A stroke has no inside: it is
// the distance to a line itself, so every pixel near the line, on either
// side, is close. The O is the distance to a short upright line, less the
// letter's half width, with abs taking the inside out: an outline.

// The distance to the segment from a to b: along it, the nearest point is
// clamped to the ends.
float sdSegment(float2 p, float2 a, float2 b) {
    float2 pa = p - a, ba = b - a;
    float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
    return length(pa - ba * h);
}

float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
    // One pixel's width, in units where the shorter side runs -1.2 to 1.2.
    float px = 2.4 / min(u.resolution.x, u.resolution.y);
    float2 q = (uv - 0.5) * u.resolution * px;
    // A slanted line on the left, and the O on the right.
    float line = sdSegment(q, float2(-1.3, -0.5), float2(-0.5, 0.5));
    float d = sdSegment(q, float2(0.6, -0.1), float2(0.6, 0.1));
    float ring = abs(d - 0.34);
    // A tube 0.012 thick: white within it, black outside, over one pixel.
    const float radius = 0.012;
    float stroke = 1.0 - smoothstep(radius, radius + px, min(line, ring));
    return float4(float3(stroke), 1.0);
}

Stage 2 · Glow

The O lights up in red neon. The glass is its colour at the edge and close to white in the middle (hot, within 0.6 of the radius). Around it, the glow is radius² / (d² + radius²): 1 on the tube, a half at one radius away, a fifth at two, and on, never zero. A wider, softer glow, exp(-d × (14 − 10 × glow)), is added on top, as much as glow asks for. Light adds up past 1, so the last line, 1 − exp(−col × 1.4), bends it back below white: a bright pixel goes toward white rather than clipping to a flat colour.

stage-2-glow.metal10 new lines since stage 1
// Stage 2 · Glow
// A lit tube is brightest in its middle and lights the air around it. The
// glow is an inverse square of the distance: it falls away fast near the
// tube and never quite ends. Light adds up past white, so the last line
// bends it back: the hot middle goes white, the edge keeps its colour.

// @param glow 0.6 0.0 1.0 How far the light spreads from the tube

float sdSegment(float2 p, float2 a, float2 b) {
    float2 pa = p - a, ba = b - a;
    float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
    return length(pa - ba * 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 d = abs(sdSegment(q, float2(0.0, -0.1), float2(0.0, 0.1)) - 0.34);
    const float radius = 0.012;
    float3 red = float3(1.0, 0.16, 0.05);
    // The glass: its colour at the edge, near white in the middle.
    float body = 1.0 - smoothstep(radius * 0.5, radius + px, d);
    float hot = 1.0 - smoothstep(0.0, radius * 0.6, d);
    float3 col = body * (red * 1.6 + hot * 1.2);
    // The glow: 1 in the tube's middle, a half one radius out, a fifth at
    // two, never zero; and a wider, softer glow on top for `glow`.
    float g = radius * radius / (d * d + radius * radius);
    float wide = exp(-d * (14.0 - 10.0 * p.glow));
    col += red * (0.6 * g + p.glow * 0.35 * wide);
    // Bright light goes to white rather than clipping its colour.
    col = 1.0 - exp(-col * 1.4);
    return float4(col, 1.0);
}
5 lines from stage 1 that are gone
    float line = sdSegment(q, float2(-1.3, -0.5), float2(-0.5, 0.5));
    float d = sdSegment(q, float2(0.6, -0.1), float2(0.6, 0.1));
    float ring = abs(d - 0.34);
⋯
    float stroke = 1.0 - smoothstep(radius, radius + px, min(line, ring));
    return float4(float3(stroke), 1.0);

Stage 3 · The sign

Seven tubes, each the nearest of a few strokes. The letters O P E N sit on one baseline: the P is an upright, two short bars and halfRing, the right half of a ring with its two ends; the E is an upright and three bars; the N two uprights and a diagonal. Over them is the kiln's mark: archLine is two upright legs and a half ring over them (the distance to a leg below the top, to the ring above it), with a hearth line under the arch, a smaller arch inside it for the heated interior, and five rays around its top. The letters are red, the arch gold, the interior ember and the rays cream (tubeColour). Each tube's glow is added to the others, so where tubes are close, the light between them is brighter.

stage-3-sign.metal67 new lines since stage 2
// Stage 3 · The sign
// Seven tubes, each the nearest of a few strokes: segments, half a ring for
// the P's bowl, and an arch, two upright lines with a half ring over them.
// The letters spell OPEN under the kiln's mark. Every tube has a colour and
// its own glow, and the glows add up where tubes are close.

// @param glow 0.6 0.0 1.0 How far the light spreads from the tubes

// The distance to the segment from a to b: along it, the nearest point is
// clamped to the ends.
float sdSegment(float2 p, float2 a, float2 b) {
    float2 pa = p - a, ba = b - a;
    float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
    return length(pa - ba * h);
}

// The distance to the right half of a ring around c: the ring's own distance on
// its side, its two ends on the other.
float halfRing(float2 p, float2 c, float r) {
    p -= c;
    if (p.x > 0.0) return abs(length(p) - r);
    return length(float2(p.x, abs(p.y) - r));
}

// The distance to an arch's outline: two legs of height h, w either side of
// `base`, and a half ring over them.
float archLine(float2 p, float2 base, float w, float h) {
    p -= base;
    p.x = abs(p.x);
    if (p.y > h) return abs(length(p - float2(0.0, h)) - w);
    return length(p - float2(w, clamp(p.y, 0.0, h)));
}

constant int tubeCount = 7;

// Tube i's distance at p. 0 to 3 are the letters O P E N, 4 the arch and its
// hearth, 5 the heated interior, 6 the rays.
float tube(int i, float2 p) {
    const float lo = -0.75, hi = -0.25, mid = -0.51;
    if (i == 0) {
        // O: a stadium's outline, the distance to a short upright line less the
        // letter's half width.
        float d = sdSegment(p, float2(-0.75, -0.58), float2(-0.75, -0.42));
        return abs(d - 0.17);
    }
    if (i == 1) {
        float x = -0.42;
        float d = sdSegment(p, float2(x, lo), float2(x, hi));
        d = min(d, sdSegment(p, float2(x, hi), float2(x + 0.19, hi)));
        d = min(d, sdSegment(p, float2(x, mid), float2(x + 0.19, mid)));
        float2 c = float2(x + 0.19, (hi + mid) * 0.5);
        return min(d, halfRing(p, c, (hi - mid) * 0.5));
    }
    if (i == 2) {
        float x = 0.08;
        float d = sdSegment(p, float2(x, lo), float2(x, hi));
        d = min(d, sdSegment(p, float2(x, hi), float2(x + 0.3, hi)));
        d = min(d, sdSegment(p, float2(x, -0.5), float2(x + 0.24, -0.5)));
        return min(d, sdSegment(p, float2(x, lo), float2(x + 0.3, lo)));
    }
    if (i == 3) {
        float x = 0.56;
        float d = sdSegment(p, float2(x, lo), float2(x, hi));
        d = min(d, sdSegment(p, float2(x, hi), float2(x + 0.32, lo)));
        return min(d, sdSegment(p, float2(x + 0.32, lo), float2(x + 0.32, hi)));
    }
    if (i == 4) {
        float d = archLine(p, float2(0.0, -0.05), 0.3, 0.32);
        return min(d, sdSegment(p, float2(-0.48, -0.05), float2(0.48, -0.05)));
    }
    if (i == 5) return archLine(p, float2(0.0, -0.05), 0.14, 0.26);
    // Five rays around the arch's top, each a short line pointing out from its
    // centre.
    float d = 1e9;
    for (int k = 0; k < 5; k++) {
        float a = (30.0 + 30.0 * float(k)) * 0.01745329;
        float2 dir = float2(cos(a), sin(a));
        float2 c = float2(0.0, 0.27);
        d = min(d, sdSegment(p, c + dir * 0.44, c + dir * 0.6));
    }
    return d;
}

// Each tube's colour: the letters a red neon, the arch gold, the interior
// ember, the rays cream.
float3 tubeColour(int i) {
    if (i < 4) return float3(1.0, 0.16, 0.05);
    if (i == 4) return float3(1.0, 0.6, 0.16);
    if (i == 5) return float3(1.0, 0.36, 0.07);
    return float3(1.0, 0.86, 0.62);
}

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 + float2(0.0, 0.08);
    const float radius = 0.012;
    float3 col = float3(0.0);
    for (int i = 0; i < tubeCount; i++) {
        float d = tube(i, q);
        float3 c = tubeColour(i);
        float body = 1.0 - smoothstep(radius * 0.5, radius + px, d);
        float hot = 1.0 - smoothstep(0.0, radius * 0.6, d);
        col += body * (c * 1.6 + hot * 1.2);
        float g = radius * radius / (d * d + radius * radius);
        float wide = exp(-d * (14.0 - 10.0 * p.glow));
        col += c * (0.6 * g + p.glow * 0.35 * wide);
    }
    col = 1.0 - exp(-col * 1.4);
    return float4(col, 1.0);
}
5 lines from stage 2 that are gone
    float2 q = (uv - 0.5) * u.resolution * px;
    float d = abs(sdSegment(q, float2(0.0, -0.1), float2(0.0, 0.1)) - 0.34);
⋯
    float3 red = float3(1.0, 0.16, 0.05);
⋯
    float3 col = body * (red * 1.6 + hot * 1.2);
⋯
    col += red * (0.6 * g + p.glow * 0.35 * wide);

Stage 4 · Flicker

tubeOn says whether a tube is lit. When the sign is switched on, each tube strikes in turn, 0.22 s after the one before, stuttering for 0.4 s on a hash of each thirtieth of a second, as a cold tube does. After that, fault time runs at 16 steps a second (speed times faster or slower). Every 32 steps, two seconds at the default speed, a hash says whether a tube is in a fault: more often the higher flicker is, and the N far more often than the rest (0.95 × flicker against 0.3 × flicker). In a fault, every step is a coin toss. A tube that is off is still there, as dull grey glass, and gives no light.

stage-4-flicker.metal21 new lines since stage 3
// Stage 4 · Flicker
// Each tube has a power, on or off, from the clock. Turned on, the sign
// warms up: one tube after another strikes with a stutter. Then a hash of
// every two seconds says whether a tube is in a fault, more often the higher
// flicker is, and in a fault each sixteenth of a second is a coin toss. A
// tube that is off is dull grey glass, and gives no light.

// @param speed 1.0 0.0 3.0 How fast the faults come and go
// @param glow 0.6 0.0 1.0 How far the light spreads from the tubes
// @param flicker 0.5 0.0 1.0 How faulty the sign is, from new to about to fail

// The distance to the segment from a to b: along it, the nearest point is
// clamped to the ends.
float sdSegment(float2 p, float2 a, float2 b) {
    float2 pa = p - a, ba = b - a;
    float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
    return length(pa - ba * h);
}

// The distance to the right half of a ring around c: the ring's own distance on
// its side, its two ends on the other.
float halfRing(float2 p, float2 c, float r) {
    p -= c;
    if (p.x > 0.0) return abs(length(p) - r);
    return length(float2(p.x, abs(p.y) - r));
}

// The distance to an arch's outline: two legs of height h, w either side of
// `base`, and a half ring over them.
float archLine(float2 p, float2 base, float w, float h) {
    p -= base;
    p.x = abs(p.x);
    if (p.y > h) return abs(length(p - float2(0.0, h)) - w);
    return length(p - float2(w, clamp(p.y, 0.0, h)));
}

constant int tubeCount = 7;

// Tube i's distance at p. 0 to 3 are the letters O P E N, 4 the arch and its
// hearth, 5 the heated interior, 6 the rays.
float tube(int i, float2 p) {
    const float lo = -0.75, hi = -0.25, mid = -0.51;
    if (i == 0) {
        // O: a stadium's outline, the distance to a short upright line less the
        // letter's half width.
        float d = sdSegment(p, float2(-0.75, -0.58), float2(-0.75, -0.42));
        return abs(d - 0.17);
    }
    if (i == 1) {
        float x = -0.42;
        float d = sdSegment(p, float2(x, lo), float2(x, hi));
        d = min(d, sdSegment(p, float2(x, hi), float2(x + 0.19, hi)));
        d = min(d, sdSegment(p, float2(x, mid), float2(x + 0.19, mid)));
        float2 c = float2(x + 0.19, (hi + mid) * 0.5);
        return min(d, halfRing(p, c, (hi - mid) * 0.5));
    }
    if (i == 2) {
        float x = 0.08;
        float d = sdSegment(p, float2(x, lo), float2(x, hi));
        d = min(d, sdSegment(p, float2(x, hi), float2(x + 0.3, hi)));
        d = min(d, sdSegment(p, float2(x, -0.5), float2(x + 0.24, -0.5)));
        return min(d, sdSegment(p, float2(x, lo), float2(x + 0.3, lo)));
    }
    if (i == 3) {
        float x = 0.56;
        float d = sdSegment(p, float2(x, lo), float2(x, hi));
        d = min(d, sdSegment(p, float2(x, hi), float2(x + 0.32, lo)));
        return min(d, sdSegment(p, float2(x + 0.32, lo), float2(x + 0.32, hi)));
    }
    if (i == 4) {
        float d = archLine(p, float2(0.0, -0.05), 0.3, 0.32);
        return min(d, sdSegment(p, float2(-0.48, -0.05), float2(0.48, -0.05)));
    }
    if (i == 5) return archLine(p, float2(0.0, -0.05), 0.14, 0.26);
    // Five rays around the arch's top, each a short line pointing out from its
    // centre.
    float d = 1e9;
    for (int k = 0; k < 5; k++) {
        float a = (30.0 + 30.0 * float(k)) * 0.01745329;
        float2 dir = float2(cos(a), sin(a));
        float2 c = float2(0.0, 0.27);
        d = min(d, sdSegment(p, c + dir * 0.44, c + dir * 0.6));
    }
    return d;
}

// Each tube's colour: the letters a red neon, the arch gold, the interior
// ember, the rays cream.
float3 tubeColour(int i) {
    if (i < 4) return float3(1.0, 0.16, 0.05);
    if (i == 4) return float3(1.0, 0.6, 0.16);
    if (i == 5) return float3(1.0, 0.36, 0.07);
    return float3(1.0, 0.86, 0.62);
}

float hashU(uint n) {
    n = (n << 13u) ^ n;
    n = n * (n * n * 15731u + 789221u) + 1376312589u;
    return float(n & 0x7fffffffu) / float(0x7fffffff);
}

// How often a tube's state can change: 16 steps a second of fault time.
constant float flickSteps = 16.0;

// Tube i on (1) or off (0) in step k of fault time, `time` seconds after the
// sign was switched on: a stutter as it strikes, then now and then a fault,
// more often the higher `flicker`, in which each step is a coin toss.
float tubeOn(int i, float k, float time, float flicker) {
    float strike = 0.3 + 0.22 * float(i);
    if (time < strike) return 0.0;
    uint stutter = uint(time * 30.0) * 7u + uint(i) * 131u;
    if (time < strike + 0.4) return step(0.45, hashU(stutter));
    uint ki = uint(max(k, 0.0));
    float burst = hashU((ki / 32u) * 17u + uint(i) * 977u + 5u);
    float chance = flicker * (i == 3 ? 0.95 : 0.3);
    if (burst < chance) return step(0.5, hashU(ki * 31u + uint(i) * 7919u));
    return 1.0;
}

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 + float2(0.0, 0.08);
    float k = floor(u.time * p.speed * flickSteps);
    const float radius = 0.012;
    float3 col = float3(0.0);
    for (int i = 0; i < tubeCount; i++) {
        float d = tube(i, q);
        float on = tubeOn(i, k, u.time, p.flicker);
        float3 c = tubeColour(i);
        float body = 1.0 - smoothstep(radius * 0.5, radius + px, d);
        float hot = 1.0 - smoothstep(0.0, radius * 0.6, d);
        // Off, the tube is still there: grey glass.
        col += body * mix(float3(0.10, 0.09, 0.09), c * 1.6 + hot * 1.2, on);
        float g = radius * radius / (d * d + radius * radius);
        float wide = exp(-d * (14.0 - 10.0 * p.glow));
        col += on * c * (0.6 * g + p.glow * 0.35 * wide);
    }
    col = 1.0 - exp(-col * 1.4);
    return float4(col, 1.0);
}
2 lines from stage 3 that are gone
        col += body * (c * 1.6 + hot * 1.2);
⋯
        col += c * (0.6 * g + p.glow * 0.35 * wide);

Stage 5 · Neon Sign

The last step hangs the sign on a wall.

  • Bricks. bricks lays rows of bricks 0.36 by 0.15, every other row moved half a brick along, each brick its own shade of fired red from a hash of its number, with a rough face and pale mortar between them.
  • Light on the wall. The wall is lit by a little street light from above (wall), brighter toward the top, and by the sign: each lit tube adds its colour times exp(−3 d), a much wider fall-off than the glow in the air, so the bricks around the sign are warm and the corners dark.
  • Shadows. The tubes stand off the wall. Each pixel asks how far it would be from the tubes if they were moved 0.03 down and 0.012 right (tube at q + float2(-0.012, 0.03)), and where that is close, the wall is darker: a shadow below and to the right of every tube, whether it is lit or not.
  • The glass over all of it, then the glow added on top, light rather than paint, and the same bend toward white.
shade(), from the finished shader
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 + float2(0.0, 0.08);
    float k = floor(u.time * p.speed * flickSteps);

    // The tubes, each as glass and as light; the light on the wall, and the
    // tubes' shadow below and to the right.
    const float radius = 0.012;
    float3 light = float3(0.0), glass = float3(0.0), onWall = float3(0.0);
    float near = 1e9, shadow = 1e9;
    for (int i = 0; i < tubeCount; i++) {
        float d = tube(i, q);
        float on = tubeOn(i, k, u.time, p.flicker);
        float3 c = tubeColour(i);
        // The glass: a hot middle near white, its colour at the edge, a dull
        // grey tube when it is off.
        float body = 1.0 - smoothstep(radius * 0.5, radius + px, d);
        float hot = 1.0 - smoothstep(0.0, radius * 0.6, d);
        glass += body * mix(float3(0.10, 0.09, 0.09), c * 1.6 + hot * 1.2, on);
        // The glow: an inverse square of the distance, so it never quite ends,
        // scaled by `glow`.
        float g = radius * radius / (d * d + radius * radius);
        float wide = exp(-d * (14.0 - 10.0 * p.glow));
        light += on * c * (0.6 * g + p.glow * 0.35 * wide);
        onWall += on * c * exp(-d * 3.0) * (0.15 + 0.35 * p.glow);
        near = min(near, d);
        shadow = min(shadow, tube(i, q + float2(-0.012, 0.03)));
    }

    // The wall, lit by a little street light from above and by the sign; darker
    // in the tubes' shadow.
    float mortar;
    float3 wall = bricks(q, mortar);
    float street = p.wall * (0.12 + 0.1 * smoothstep(-1.2, 1.2, q.y));
    float unshaded = mix(0.35, 1.0, smoothstep(0.0, 0.025, shadow - radius));
    float3 col = wall * (street + onWall) * unshaded;

    // The glass over the wall, then the glow added on top of both: light, not
    // paint.
    float cover = 1.0 - smoothstep(radius * 0.5, radius + px, near);
    col = mix(col, glass, cover) + light;

    // Bright light goes to white rather than clipping its colour.
    col = 1.0 - exp(-col * 1.4);
    return float4(col, 1.0);
}
episode-23-neon-sign.metal, the whole file64 new lines since stage 4
// Ray Kiln · Episode 23 · Neon Sign
//
// @episode 23 Neon Sign
// @length 60
// @variation 30
// @variations 1,2,3,5,6,8,9,11
// @still 12.0
// @teaches 2D distance-field strokes, glow, flicker
// @category Fragment
// @tags signed distance fields, strokes, glow, flicker, neon, procedural sound
// @final Neon sign | The sign on a brick wall at night: each tube lights the bricks around it, casts a shadow below it, and the faulty ones stutter and buzz.
//
// A neon sign, the kiln's mark over the word OPEN, hung on a brick wall at night. Every tube is a stroke: the distance
// to a line, a ring or an arch, measured to its middle rather than its inside, so a shape's outline is all there is.
// Close to that line the tube is white-hot glass; farther out its colour falls away as a glow, a little light for every
// pixel however far. Each tube has a power that the clock switches on and off: the sign warms up when it starts, and
// the faulty tubes stutter in bursts. The wall is lit by the same glow, and the shadow of each tube falls on it below.
// 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 0.0 3.0 How fast the faults come and go
// @param glow 0.6 0.0 1.0 How far the light spreads from the tubes
// @param flicker 0.5 0.0 1.0 How faulty the sign is, from new to about to fail
// @param wall 0.5 0.0 1.0 How much street light falls on the brick wall
// @param volume 0.8 0.0 1.0 Loudness of the sound

// ---- Strokes: distances to a line, not to a shape's inside ------------------

// The distance to the segment from a to b: along it, the nearest point is
// clamped to the ends.
float sdSegment(float2 p, float2 a, float2 b) {
    float2 pa = p - a, ba = b - a;
    float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
    return length(pa - ba * h);
}

// The distance to the right half of a ring around c: the ring's own distance on
// its side, its two ends on the other.
float halfRing(float2 p, float2 c, float r) {
    p -= c;
    if (p.x > 0.0) return abs(length(p) - r);
    return length(float2(p.x, abs(p.y) - r));
}

// The distance to an arch's outline: two legs of height h, w either side of
// `base`, and a half ring over them.
float archLine(float2 p, float2 base, float w, float h) {
    p -= base;
    p.x = abs(p.x);
    if (p.y > h) return abs(length(p - float2(0.0, h)) - w);
    return length(p - float2(w, clamp(p.y, 0.0, h)));
}

// ---- The sign: seven tubes --------------------------------------------------

constant int tubeCount = 7;

// Tube i's distance at p. 0 to 3 are the letters O P E N, 4 the arch and its
// hearth, 5 the heated interior, 6 the rays.
float tube(int i, float2 p) {
    const float lo = -0.75, hi = -0.25, mid = -0.51;
    if (i == 0) {
        // O: a stadium's outline, the distance to a short upright line less the
        // letter's half width.
        float d = sdSegment(p, float2(-0.75, -0.58), float2(-0.75, -0.42));
        return abs(d - 0.17);
    }
    if (i == 1) {
        float x = -0.42;
        float d = sdSegment(p, float2(x, lo), float2(x, hi));
        d = min(d, sdSegment(p, float2(x, hi), float2(x + 0.19, hi)));
        d = min(d, sdSegment(p, float2(x, mid), float2(x + 0.19, mid)));
        float2 c = float2(x + 0.19, (hi + mid) * 0.5);
        return min(d, halfRing(p, c, (hi - mid) * 0.5));
    }
    if (i == 2) {
        float x = 0.08;
        float d = sdSegment(p, float2(x, lo), float2(x, hi));
        d = min(d, sdSegment(p, float2(x, hi), float2(x + 0.3, hi)));
        d = min(d, sdSegment(p, float2(x, -0.5), float2(x + 0.24, -0.5)));
        return min(d, sdSegment(p, float2(x, lo), float2(x + 0.3, lo)));
    }
    if (i == 3) {
        float x = 0.56;
        float d = sdSegment(p, float2(x, lo), float2(x, hi));
        d = min(d, sdSegment(p, float2(x, hi), float2(x + 0.32, lo)));
        return min(d, sdSegment(p, float2(x + 0.32, lo), float2(x + 0.32, hi)));
    }
    if (i == 4) {
        float d = archLine(p, float2(0.0, -0.05), 0.3, 0.32);
        return min(d, sdSegment(p, float2(-0.48, -0.05), float2(0.48, -0.05)));
    }
    if (i == 5) return archLine(p, float2(0.0, -0.05), 0.14, 0.26);
    // Five rays around the arch's top, each a short line pointing out from its
    // centre.
    float d = 1e9;
    for (int k = 0; k < 5; k++) {
        float a = (30.0 + 30.0 * float(k)) * 0.01745329;
        float2 dir = float2(cos(a), sin(a));
        float2 c = float2(0.0, 0.27);
        d = min(d, sdSegment(p, c + dir * 0.44, c + dir * 0.6));
    }
    return d;
}

// Each tube's colour: the letters a red neon, the arch gold, the interior
// ember, the rays cream.
float3 tubeColour(int i) {
    if (i < 4) return float3(1.0, 0.16, 0.05);
    if (i == 4) return float3(1.0, 0.6, 0.16);
    if (i == 5) return float3(1.0, 0.36, 0.07);
    return float3(1.0, 0.86, 0.62);
}

// ---- Flicker: each tube's power from the clock ------------------------------

float hashU(uint n) {
    n = (n << 13u) ^ n;
    n = n * (n * n * 15731u + 789221u) + 1376312589u;
    return float(n & 0x7fffffffu) / float(0x7fffffff);
}

// How often a tube's state can change: 16 steps a second of fault time.
constant float flickSteps = 16.0;

// Tube i on (1) or off (0) in step k of fault time, `time` seconds after the
// sign was switched on: a stutter as it strikes, then now and then a fault,
// more often the higher `flicker`, in which each step is a coin toss.
float tubeOn(int i, float k, float time, float flicker) {
    float strike = 0.3 + 0.22 * float(i);
    if (time < strike) return 0.0;
    uint stutter = uint(time * 30.0) * 7u + uint(i) * 131u;
    if (time < strike + 0.4) return step(0.45, hashU(stutter));
    uint ki = uint(max(k, 0.0));
    float burst = hashU((ki / 32u) * 17u + uint(i) * 977u + 5u);
    float chance = flicker * (i == 3 ? 0.95 : 0.3);
    if (burst < chance) return step(0.5, hashU(ki * 31u + uint(i) * 7919u));
    return 1.0;
}

// ---- The wall ---------------------------------------------------------------

// Bricks of 0.36 by 0.15, every other row moved by half a brick: each brick's
// own shade from a hash of its number, a rough face, and mortar between them.
// Gives the brick's colour and how much of it is mortar.
float3 bricks(float2 p, thread float& mortar) {
    float2 size = float2(0.36, 0.15);
    float row = floor(p.y / size.y);
    float2 q = float2(p.x / size.x + 0.5 * fmod(abs(row), 2.0), p.y / size.y);
    float2 cell = floor(q), f = fract(q);
    float2 edge = min(f, 1.0 - f) * size;
    mortar = 1.0 - smoothstep(0.006, 0.012, min(edge.x, edge.y));
    uint2 n = uint2(cell + 512.0), g = uint2(p * 900.0 + 9000.0);
    float tone = hashU(n.x * 73u + n.y * 1361u);
    float grit = hashU(g.x * 3u + g.y * 7919u);
    float3 brick = mix(float3(0.42, 0.16, 0.1), float3(0.58, 0.26, 0.15), tone);
    brick *= 0.85 + 0.15 * grit;
    return mix(brick, float3(0.30, 0.27, 0.24), mortar);
}

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 + float2(0.0, 0.08);
    float k = floor(u.time * p.speed * flickSteps);

    // The tubes, each as glass and as light; the light on the wall, and the
    // tubes' shadow below and to the right.
    const float radius = 0.012;
    float3 light = float3(0.0), glass = float3(0.0), onWall = float3(0.0);
    float near = 1e9, shadow = 1e9;
    for (int i = 0; i < tubeCount; i++) {
        float d = tube(i, q);
        float on = tubeOn(i, k, u.time, p.flicker);
        float3 c = tubeColour(i);
        // The glass: a hot middle near white, its colour at the edge, a dull
        // grey tube when it is off.
        float body = 1.0 - smoothstep(radius * 0.5, radius + px, d);
        float hot = 1.0 - smoothstep(0.0, radius * 0.6, d);
        glass += body * mix(float3(0.10, 0.09, 0.09), c * 1.6 + hot * 1.2, on);
        // The glow: an inverse square of the distance, so it never quite ends,
        // scaled by `glow`.
        float g = radius * radius / (d * d + radius * radius);
        float wide = exp(-d * (14.0 - 10.0 * p.glow));
        light += on * c * (0.6 * g + p.glow * 0.35 * wide);
        onWall += on * c * exp(-d * 3.0) * (0.15 + 0.35 * p.glow);
        near = min(near, d);
        shadow = min(shadow, tube(i, q + float2(-0.012, 0.03)));
    }

    // The wall, lit by a little street light from above and by the sign; darker
    // in the tubes' shadow.
    float mortar;
    float3 wall = bricks(q, mortar);
    float street = p.wall * (0.12 + 0.1 * smoothstep(-1.2, 1.2, q.y));
    float unshaded = mix(0.35, 1.0, smoothstep(0.0, 0.025, shadow - radius));
    float3 col = wall * (street + onWall) * unshaded;

    // The glass over the wall, then the glow added on top of both: light, not
    // paint.
    float cover = 1.0 - smoothstep(radius * 0.5, radius + px, near);
    col = mix(col, glass, cover) + light;

    // Bright light goes to white rather than clipping its colour.
    col = 1.0 - exp(-col * 1.4);
    return float4(col, 1.0);
}

// ---- Sound ------------------------------------------------------------------
// The sign's transformer hums at 120 Hz with its overtones, louder the more
// tubes are lit, and a tube that switches on or off crackles, heard from where
// it is on the sign. A tube in a fault buzzes. The hum hears each tube as the
// average of four moments 1.5 ms apart, so a switch fades over 4.5 ms and never
// clicks.

// Tube i on or off at `t` seconds, as the picture has it.
float tubeAt(int i, float t, constant Params& p) {
    return tubeOn(i, floor(t * p.speed * flickSteps), t, p.flicker);
}

float2 sound(float t, constant Params& p) {
    const float tau = 6.2831853;
    float tt = t * p.speed * flickSteps;
    float k = floor(tt);
    // Seconds since this step of fault time began (a stopped clock has none).
    float age = p.speed > 0.0 ? fract(tt) / (p.speed * flickSteps) : 1.0;
    float noise = hashU(uint(t * 44100.0)) * 2.0 - 1.0;

    float lit = 0.0;
    float2 crackle = float2(0.0);
    for (int i = 0; i < tubeCount; i++) {
        float now = tubeOn(i, k, t, p.flicker);
        float before = tubeOn(i, k - 1.0, max(t - age, 0.0), p.flicker);
        for (int j = 0; j < 4; j++) {
            lit += tubeAt(i, max(t - 0.0015 * float(j), 0.0), p) * 0.25;
        }
        // A switch: a burst of noise dying away in about 15 ms, from the tube's
        // side of the sign.
        float pan = i < 4 ? 0.125 + 0.25 * float(i) : 0.5;
        float burst = abs(now - before) * exp(-age * 70.0);
        crackle += burst * float2(1.0 - pan, pan);
    }
    lit /= float(tubeCount);

    float hum = 0.0;
    for (int n = 1; n <= 6; n++) {
        hum += sin(tau * 120.0 * float(n) * t) / float(n * n);
    }
    float buzz = clamp(sin(tau * 120.0 * t) * 3.0, -1.0, 1.0);
    float faulty = 0.0;
    for (int j = 0; j < 4; j++) {
        faulty += (1.0 - tubeAt(3, max(t - 0.0015 * float(j), 0.0), p)) * 0.25;
    }

    float2 s = float2(hum * 0.16 * lit + buzz * 0.03 * lit * p.flicker);
    s += crackle * noise * 0.22;
    s += faulty * float2(0.03, 0.05) * sin(tau * 60.0 * t);
    return s * p.volume;
}
3 lines from stage 4 that are gone
    float3 col = float3(0.0);
⋯
        col += body * mix(float3(0.10, 0.09, 0.09), c * 1.6 + hot * 1.2, on);
⋯
        col += on * c * (0.6 * g + p.glow * 0.35 * wide);

The sound

The sound reads the same tubes. The transformer behind the sign hums at 120 Hz, twice the mains frequency, with five overtones at an inverse square of their number, and a little clipped buzz under it that grows with flicker. The hum is as loud as the share of tubes that are lit: it hears each tube at four moments 1.5 ms apart and averages them, so a tube switching on or off turns the hum up or down over 4.5 ms and never clicks.

Each time a tube switches, it crackles: a burst of noise that dies away in about 15 ms, heard from where that tube is on the sign, the O on the left and the N on the right, the mark in the middle. While the N is out, a low 60 Hz buzz plays, slightly louder on the right. At the defaults the peak is about -7.4 dBFS and the average (RMS) about -20.2 dBFS.

New in Ray Kiln

A/B code in the playground. Under the picture there is now an A/B code button (or press B). It splits the picture down the middle: the left half runs the code as it was before your last edit, the right half the code as it is now, at the same moment and with the same sliders. An edit is everything you type without stopping for a second and a half, so the split shows what that whole change did, not each keystroke of it; the next edit moves it on. ⇄ puts the code from before back in the editor, an edit like any other, so you can go back and forth. It works for any picture with no state between frames: a fragment shader without feedback, or a SwiftUI effect. The A/B button beside it still compares two settings of the sliders.

Neon is a good place to use it, because its glow is two terms added together, and it is hard to tell from the code alone what each one does. Open stage 2 in the playground, change float wide = exp(-d * (14.0 - 10.0 * p.glow)); to float wide = 0.0; and press A/B code. The left half keeps both terms; the right half has only the inverse square, which is bright on the tube but has nearly gone a few radii out, so the O is a thin, sharp line with no halo. The soft light around the tube in the left half was all the second term.

Try this

  • Set flicker to 1: the N is out most of the time, the other tubes fail now and then, and the crackles come in bursts. Set it to 0 for a new sign.
  • Set speed to 0: fault time stands still at its first step, so whichever tubes are out then stay out.
  • Set glow to 0 and wall to 0: thin lines of colour in the dark, and only the bricks right behind the tubes lit.
  • Press Restart: the sign warms up again, one tube after another.
  • In tube, change the O's abs(d - 0.17) to d - 0.17: the O is no longer an outline but a filled stadium of white-hot glass.
  • In tubeColour, change the letters' float3(1.0, 0.16, 0.05) to float3(0.2, 0.5, 1.0): blue neon over the warm bricks.
  • In shade, change the shadow's float2(-0.012, 0.03) to float2(0.05, 0.08): the shadows fall farther down and to the left, as if the tubes stood farther off the wall.

Parameters

ParameterDefaultRangeWhat it does
speed1.00.0 to 3.0How fast the faults come and go
glow0.60.0 to 1.0How far the light spreads from the tubes
flicker0.50.0 to 1.0How faulty the sign is, from new to about to fail
wall0.50.0 to 1.0How much street light falls on the brick wall
volume0.80.0 to 1.0Loudness 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.

episode-23-neon-sign.metal
// Ray Kiln · Episode 23 · Neon Sign
//
// @episode 23 Neon Sign
// @length 60
// @variation 30
// @variations 1,2,3,5,6,8,9,11
// @still 12.0
// @teaches 2D distance-field strokes, glow, flicker
// @category Fragment
// @tags signed distance fields, strokes, glow, flicker, neon, procedural sound
// @final Neon sign | The sign on a brick wall at night: each tube lights the bricks around it, casts a shadow below it, and the faulty ones stutter and buzz.
//
// A neon sign, the kiln's mark over the word OPEN, hung on a brick wall at night. Every tube is a stroke: the distance
// to a line, a ring or an arch, measured to its middle rather than its inside, so a shape's outline is all there is.
// Close to that line the tube is white-hot glass; farther out its colour falls away as a glow, a little light for every
// pixel however far. Each tube has a power that the clock switches on and off: the sign warms up when it starts, and
// the faulty tubes stutter in bursts. The wall is lit by the same glow, and the shadow of each tube falls on it below.
// 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 0.0 3.0 How fast the faults come and go
// @param glow 0.6 0.0 1.0 How far the light spreads from the tubes
// @param flicker 0.5 0.0 1.0 How faulty the sign is, from new to about to fail
// @param wall 0.5 0.0 1.0 How much street light falls on the brick wall
// @param volume 0.8 0.0 1.0 Loudness of the sound

// ---- Strokes: distances to a line, not to a shape's inside ------------------

// The distance to the segment from a to b: along it, the nearest point is
// clamped to the ends.
float sdSegment(float2 p, float2 a, float2 b) {
    float2 pa = p - a, ba = b - a;
    float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
    return length(pa - ba * h);
}

// The distance to the right half of a ring around c: the ring's own distance on
// its side, its two ends on the other.
float halfRing(float2 p, float2 c, float r) {
    p -= c;
    if (p.x > 0.0) return abs(length(p) - r);
    return length(float2(p.x, abs(p.y) - r));
}

// The distance to an arch's outline: two legs of height h, w either side of
// `base`, and a half ring over them.
float archLine(float2 p, float2 base, float w, float h) {
    p -= base;
    p.x = abs(p.x);
    if (p.y > h) return abs(length(p - float2(0.0, h)) - w);
    return length(p - float2(w, clamp(p.y, 0.0, h)));
}

// ---- The sign: seven tubes --------------------------------------------------

constant int tubeCount = 7;

// Tube i's distance at p. 0 to 3 are the letters O P E N, 4 the arch and its
// hearth, 5 the heated interior, 6 the rays.
float tube(int i, float2 p) {
    const float lo = -0.75, hi = -0.25, mid = -0.51;
    if (i == 0) {
        // O: a stadium's outline, the distance to a short upright line less the
        // letter's half width.
        float d = sdSegment(p, float2(-0.75, -0.58), float2(-0.75, -0.42));
        return abs(d - 0.17);
    }
    if (i == 1) {
        float x = -0.42;
        float d = sdSegment(p, float2(x, lo), float2(x, hi));
        d = min(d, sdSegment(p, float2(x, hi), float2(x + 0.19, hi)));
        d = min(d, sdSegment(p, float2(x, mid), float2(x + 0.19, mid)));
        float2 c = float2(x + 0.19, (hi + mid) * 0.5);
        return min(d, halfRing(p, c, (hi - mid) * 0.5));
    }
    if (i == 2) {
        float x = 0.08;
        float d = sdSegment(p, float2(x, lo), float2(x, hi));
        d = min(d, sdSegment(p, float2(x, hi), float2(x + 0.3, hi)));
        d = min(d, sdSegment(p, float2(x, -0.5), float2(x + 0.24, -0.5)));
        return min(d, sdSegment(p, float2(x, lo), float2(x + 0.3, lo)));
    }
    if (i == 3) {
        float x = 0.56;
        float d = sdSegment(p, float2(x, lo), float2(x, hi));
        d = min(d, sdSegment(p, float2(x, hi), float2(x + 0.32, lo)));
        return min(d, sdSegment(p, float2(x + 0.32, lo), float2(x + 0.32, hi)));
    }
    if (i == 4) {
        float d = archLine(p, float2(0.0, -0.05), 0.3, 0.32);
        return min(d, sdSegment(p, float2(-0.48, -0.05), float2(0.48, -0.05)));
    }
    if (i == 5) return archLine(p, float2(0.0, -0.05), 0.14, 0.26);
    // Five rays around the arch's top, each a short line pointing out from its
    // centre.
    float d = 1e9;
    for (int k = 0; k < 5; k++) {
        float a = (30.0 + 30.0 * float(k)) * 0.01745329;
        float2 dir = float2(cos(a), sin(a));
        float2 c = float2(0.0, 0.27);
        d = min(d, sdSegment(p, c + dir * 0.44, c + dir * 0.6));
    }
    return d;
}

// Each tube's colour: the letters a red neon, the arch gold, the interior
// ember, the rays cream.
float3 tubeColour(int i) {
    if (i < 4) return float3(1.0, 0.16, 0.05);
    if (i == 4) return float3(1.0, 0.6, 0.16);
    if (i == 5) return float3(1.0, 0.36, 0.07);
    return float3(1.0, 0.86, 0.62);
}

// ---- Flicker: each tube's power from the clock ------------------------------

float hashU(uint n) {
    n = (n << 13u) ^ n;
    n = n * (n * n * 15731u + 789221u) + 1376312589u;
    return float(n & 0x7fffffffu) / float(0x7fffffff);
}

// How often a tube's state can change: 16 steps a second of fault time.
constant float flickSteps = 16.0;

// Tube i on (1) or off (0) in step k of fault time, `time` seconds after the
// sign was switched on: a stutter as it strikes, then now and then a fault,
// more often the higher `flicker`, in which each step is a coin toss.
float tubeOn(int i, float k, float time, float flicker) {
    float strike = 0.3 + 0.22 * float(i);
    if (time < strike) return 0.0;
    uint stutter = uint(time * 30.0) * 7u + uint(i) * 131u;
    if (time < strike + 0.4) return step(0.45, hashU(stutter));
    uint ki = uint(max(k, 0.0));
    float burst = hashU((ki / 32u) * 17u + uint(i) * 977u + 5u);
    float chance = flicker * (i == 3 ? 0.95 : 0.3);
    if (burst < chance) return step(0.5, hashU(ki * 31u + uint(i) * 7919u));
    return 1.0;
}

// ---- The wall ---------------------------------------------------------------

// Bricks of 0.36 by 0.15, every other row moved by half a brick: each brick's
// own shade from a hash of its number, a rough face, and mortar between them.
// Gives the brick's colour and how much of it is mortar.
float3 bricks(float2 p, thread float& mortar) {
    float2 size = float2(0.36, 0.15);
    float row = floor(p.y / size.y);
    float2 q = float2(p.x / size.x + 0.5 * fmod(abs(row), 2.0), p.y / size.y);
    float2 cell = floor(q), f = fract(q);
    float2 edge = min(f, 1.0 - f) * size;
    mortar = 1.0 - smoothstep(0.006, 0.012, min(edge.x, edge.y));
    uint2 n = uint2(cell + 512.0), g = uint2(p * 900.0 + 9000.0);
    float tone = hashU(n.x * 73u + n.y * 1361u);
    float grit = hashU(g.x * 3u + g.y * 7919u);
    float3 brick = mix(float3(0.42, 0.16, 0.1), float3(0.58, 0.26, 0.15), tone);
    brick *= 0.85 + 0.15 * grit;
    return mix(brick, float3(0.30, 0.27, 0.24), mortar);
}

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 + float2(0.0, 0.08);
    float k = floor(u.time * p.speed * flickSteps);

    // The tubes, each as glass and as light; the light on the wall, and the
    // tubes' shadow below and to the right.
    const float radius = 0.012;
    float3 light = float3(0.0), glass = float3(0.0), onWall = float3(0.0);
    float near = 1e9, shadow = 1e9;
    for (int i = 0; i < tubeCount; i++) {
        float d = tube(i, q);
        float on = tubeOn(i, k, u.time, p.flicker);
        float3 c = tubeColour(i);
        // The glass: a hot middle near white, its colour at the edge, a dull
        // grey tube when it is off.
        float body = 1.0 - smoothstep(radius * 0.5, radius + px, d);
        float hot = 1.0 - smoothstep(0.0, radius * 0.6, d);
        glass += body * mix(float3(0.10, 0.09, 0.09), c * 1.6 + hot * 1.2, on);
        // The glow: an inverse square of the distance, so it never quite ends,
        // scaled by `glow`.
        float g = radius * radius / (d * d + radius * radius);
        float wide = exp(-d * (14.0 - 10.0 * p.glow));
        light += on * c * (0.6 * g + p.glow * 0.35 * wide);
        onWall += on * c * exp(-d * 3.0) * (0.15 + 0.35 * p.glow);
        near = min(near, d);
        shadow = min(shadow, tube(i, q + float2(-0.012, 0.03)));
    }

    // The wall, lit by a little street light from above and by the sign; darker
    // in the tubes' shadow.
    float mortar;
    float3 wall = bricks(q, mortar);
    float street = p.wall * (0.12 + 0.1 * smoothstep(-1.2, 1.2, q.y));
    float unshaded = mix(0.35, 1.0, smoothstep(0.0, 0.025, shadow - radius));
    float3 col = wall * (street + onWall) * unshaded;

    // The glass over the wall, then the glow added on top of both: light, not
    // paint.
    float cover = 1.0 - smoothstep(radius * 0.5, radius + px, near);
    col = mix(col, glass, cover) + light;

    // Bright light goes to white rather than clipping its colour.
    col = 1.0 - exp(-col * 1.4);
    return float4(col, 1.0);
}

// ---- Sound ------------------------------------------------------------------
// The sign's transformer hums at 120 Hz with its overtones, louder the more
// tubes are lit, and a tube that switches on or off crackles, heard from where
// it is on the sign. A tube in a fault buzzes. The hum hears each tube as the
// average of four moments 1.5 ms apart, so a switch fades over 4.5 ms and never
// clicks.

// Tube i on or off at `t` seconds, as the picture has it.
float tubeAt(int i, float t, constant Params& p) {
    return tubeOn(i, floor(t * p.speed * flickSteps), t, p.flicker);
}

float2 sound(float t, constant Params& p) {
    const float tau = 6.2831853;
    float tt = t * p.speed * flickSteps;
    float k = floor(tt);
    // Seconds since this step of fault time began (a stopped clock has none).
    float age = p.speed > 0.0 ? fract(tt) / (p.speed * flickSteps) : 1.0;
    float noise = hashU(uint(t * 44100.0)) * 2.0 - 1.0;

    float lit = 0.0;
    float2 crackle = float2(0.0);
    for (int i = 0; i < tubeCount; i++) {
        float now = tubeOn(i, k, t, p.flicker);
        float before = tubeOn(i, k - 1.0, max(t - age, 0.0), p.flicker);
        for (int j = 0; j < 4; j++) {
            lit += tubeAt(i, max(t - 0.0015 * float(j), 0.0), p) * 0.25;
        }
        // A switch: a burst of noise dying away in about 15 ms, from the tube's
        // side of the sign.
        float pan = i < 4 ? 0.125 + 0.25 * float(i) : 0.5;
        float burst = abs(now - before) * exp(-age * 70.0);
        crackle += burst * float2(1.0 - pan, pan);
    }
    lit /= float(tubeCount);

    float hum = 0.0;
    for (int n = 1; n <= 6; n++) {
        hum += sin(tau * 120.0 * float(n) * t) / float(n * n);
    }
    float buzz = clamp(sin(tau * 120.0 * t) * 3.0, -1.0, 1.0);
    float faulty = 0.0;
    for (int j = 0; j < 4; j++) {
        faulty += (1.0 - tubeAt(3, max(t - 0.0015 * float(j), 0.0), p)) * 0.25;
    }

    float2 s = float2(hum * 0.16 * lit + buzz * 0.03 * lit * p.flicker);
    s += crackle * noise * 0.22;
    s += faulty * float2(0.03, 0.05) * sin(tau * 60.0 * t);
    return s * p.volume;
}

Running it in your own project

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

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

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

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

// Each frame, inside a render pass:
var uniforms = Uniforms(resolution: size, time: time, timeDelta: dt, mouse: mouse, frame: frame)
var params: [Float] = [1.0, 0.6, 0.5, 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(&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 Shapes & Symmetry