Shader 16

Pottery Wheel

A fluted pot turning on a potter's wheel in a kiln. The pot is one curve spun round an axis, a solid of revolution, so a foot, a belly, a neck and a lip are a few lines of a profile. Flutes and throwing lines are carved into it by letting the radius depend on the angle too, and a twist winds the flutes into a spiral. Its glaze mirrors the kiln by Fresnel's share, catches the light in a highlight, and pools darker in the grooves; the foot stays bare clay.

· Light & Materials · Fragment shader

Teaches Solids of revolution, twisting, a glaze material

Builds on 03 · Floating 3D Scene (rotation), 05 · Cracked Clay (integer hash, sound noise), 12 · Glass Orb (Fresnel, the light's direction, the kiln's dark, the tiled floor, the room)

Watch

The idea

Most things a potter makes are the same shape at every angle: a pot is one curve, spun. That curve is a profile, the radius at each height, and the shape it makes is a solid of revolution. For a raymarcher this is a gift: the distance from a point to the pot is, near enough, its distance from the axis less the radius at its height, so a foot, a belly, a neck and a lip are a few lines of arithmetic rather than a model.

Once the radius is a function of the height, it can be a function of the angle round the axis too, and the surface can be carved: flutes where a cosine of the angle peaks, throwing lines where a sine of the height does. Shift the angle by the height and the flutes twist into a spiral. Then the clay is lit as glaze: a material that mirrors the room, catches the light, and pools in its own grooves.

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

Stage 1 · A profile, revolved

profile(y, belly, neck) is the pot's radius at height y, from its foot at 0 to its lip at HEIGHT: a base width, a belly that swells by a sine over the height (pow(h, 0.85) pushes its widest point below the middle), a neck that narrows by a bell curve near the top, and a lip that flares out again by a smoothstep in the last tenth.

revolve turns the curve into a solid. A point's distance from the axis is length(p.xz), and the distance to the surface is that less the radius at the point's height. Where the profile slopes, the true distance is shorter than this by the slope, so the estimate is divided by sqrt(1 + slope²) (the slope measured from the radius a hundredth above and below) and the march steps 70% of the way, to be safe. pot cuts the solid to its height, which gives a flat foot and a flat lip. The pot stands on episode 12's tiled floor, lit like clay: clayShade is the kiln palette at how squarely the surface faces the light.

stage-1-revolved.metal
// Stage 1 · A profile, revolved
// A pot is one curve spun round an axis: its radius at each height. `profile` is that curve, with a foot, a
// belly, a neck and a lip, and `revolve` turns it into a solid by measuring each point's distance from the axis
// against the radius at its height. The pot is raymarched as in episode 3 and lit like clay, on the floor of a kiln.

// @param belly 0.6 0.0 1.0 How far the pot swells at its widest
// @param neck 0.5 0.0 1.0 How much the pot narrows below the lip
// @param heat 0.5 0.0 1.0 Slides the clay from deep ember to white-hot

constant float TAU = 6.2831853;
constant float FLOOR = 0.0;
constant float HEIGHT = 1.5;

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

// ---- The pot ---------------------------------------------------------------------------------------------------

// The profile: the pot's radius at height y, from its foot at 0 to its lip at
// HEIGHT. A foot, a belly that swells in the lower half, a neck that narrows
// under the lip, and the lip flaring out again.
float profile(float y, float belly, float neck) {
    float h = clamp(y / HEIGHT, 0.0, 1.0);
    float r = 0.34 + 0.46 * belly * sin(3.14159 * pow(h, 0.85));
    r -= 0.22 * neck * exp(-pow((h - 0.78) / 0.1, 2.0));
    r += 0.10 * smoothstep(0.88, 1.0, h);
    return r;
}

// The solid of revolution: a point's distance from the axis, less the radius
// at its height. The surface leans where the profile slopes, so the distance
// is scaled down by that slope to stay safe to march.
float revolve(float3 p, float4 shape) {
    float r = length(p.xz);
    float d = r - profile(p.y, shape.x, shape.y);
    float dy = profile(p.y + 0.01, shape.x, shape.y)
             - profile(p.y - 0.01, shape.x, shape.y);
    return d / sqrt(1.0 + dy * dy * 2500.0);
}

// The pot: the revolved profile, cut to its height, with a flat foot and lip.
float pot(float3 p, float4 shape) {
    float d = revolve(p, shape);
    float slab = abs(p.y - HEIGHT * 0.5) - HEIGHT * 0.5;
    return max(d, slab);
}

// The scene is the pot alone for now.
float scene(float3 p, float t, float4 shape) {
    return pot(p, shape);
}

// The direction the surface faces: how the distance changes a small step
// along each axis.
float3 potNormal(float3 p, float t, float4 shape) {
    const float2 e = float2(0.002, 0.0);
    return normalize(float3(
        scene(p + e.xyy, t, shape) - scene(p - e.xyy, t, shape),
        scene(p + e.yxy, t, shape) - scene(p - e.yxy, t, shape),
        scene(p + e.yyx, t, shape) - scene(p - e.yyx, t, shape)));
}

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

// Where the light comes from: high, in front and to the left of the camera,
// so it goes round with it and the pot is always lit from the viewer's side.
float3 lightDir(float t) {
    float a = t * 0.18 + 0.9;
    return normalize(float3(sin(a), 1.1, cos(a)));
}

// The dark of the kiln, warmer toward the horizon, its hot mouth glowing
// where the light comes from.
float sky(float3 rd, float3 l) {
    float up = abs(rd.y);
    float mouth = pow(max(dot(rd, l), 0.0), 6.0);
    return 0.08 + 0.14 * exp(-up * 6.0) + 0.55 * mouth;
}

// The floor: tiles of fired clay with dark grout, the pot's shadow over
// them, fading into the kiln's haze far off.
float floorLevel(float3 p, float3 l) {
    float2 g = p.xz * 1.6;
    float2 f = abs(fract(g) - 0.5);
    float grout = smoothstep(0.47, 0.44, max(f.x, f.y));
    float2 id = floor(g);
    float tone = 0.28 + 0.08 * fract(sin(dot(id, float2(12.9, 78.2))) * 437.5);
    float3 toPot = float3(0.0, 0.5, 0.0) - p;
    float along = dot(toPot, l);
    float miss = length(toPot - l * along);
    float shadow = along > 0.0 ? smoothstep(0.6, 1.2, miss) : 1.0;
    return tone * grout * (0.35 + 0.65 * shadow);
}

// What a ray sees of the room: the floor if it heads down, otherwise the
// kiln's dark.
float env(float3 ro, float3 rd, float t) {
    float3 l = lightDir(t);
    float back = sky(rd, l);
    if (rd.y < -0.001) {
        float d = (FLOOR - ro.y) / rd.y;
        if (d > 0.0) {
            float3 p = ro + rd * d;
            return mix(back, floorLevel(p, l), exp(-d * 0.12));
        }
    }
    return back;
}

// Clay: as bright as the surface faces the light, a little of the kiln's
// glow everywhere.
float3 clayShade(float3 n, float t, float heat) {
    float diffuse = max(dot(n, lightDir(t)), 0.0);
    return kiln(0.18 + 0.55 * diffuse + (heat - 0.5) * 0.5);
}

float3 cameraPos(float t) {
    float a = t * 0.18;
    return float3(3.3 * sin(a), 1.25 + 0.3 * sin(t * 0.13), 3.3 * cos(a));
}

float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
    float t = 14.0;
    float side = min(u.resolution.x, u.resolution.y);
    float2 q = (uv - 0.5) * u.resolution / side * 2.0;
    float4 shape = float4(p.belly, p.neck, 0.0, 0.0);
    float3 ro = cameraPos(t);
    float3 fw = normalize(float3(0.0, 0.62, 0.0) - ro);
    float3 rt = normalize(cross(fw, float3(0.0, 1.0, 0.0)));
    float3 up = cross(rt, fw);
    float3 rd = normalize(q.x * rt + q.y * up + 2.3 * fw);

    // March to the pot: each step goes 70% of the way, since the leaning
    // surface's distance is only an estimate.
    float dist = 0.0;
    bool hit = false;
    for (int i = 0; i < 160; i++) {
        float d = scene(ro + rd * dist, t, shape);
        if (d < 0.001) { hit = true; break; }
        dist += d * 0.7;
        if (dist > 14.0) break;
    }

    float3 col;
    if (!hit) {
        col = kiln(env(ro, rd, t) + (p.heat - 0.5) * 0.4);
    } else {
        float3 pos = ro + rd * dist;
        float3 n = potNormal(pos, t, shape);
        col = clayShade(n, t, p.heat);
    }
    return float4(clamp(col, 0.0, 1.0), 1.0);
}

Stage 2 · Carved, and turning

Now radius(y, a, shape) takes the angle round the axis, atan2(p.z, p.x), as well as the height, and adds carve to the profile. The flutes are twelve grooves: cos(12 × a) peaks twelve times round the pot, and ridge³ makes each peak a narrow groove with a flat ridge between, cut 0.1 × flutes deep. A band keeps them off the foot and the lip. The throwing lines are a fine sine over the height, 0.004 × sin(44 × y), the marks a potter's fingers leave as the clay rises.

The wheel turns in scene: before the pot is measured, the point is spun back round the axis by the time (rot(p.xz, -t)), so the carving goes round with the wheel. Turning the point the other way is how anything in a distance field is turned.

stage-2-carved.metal20 new lines since stage 1
// Stage 2 · Carved, and turning
// Now the radius depends on the angle round the axis too, so the surface can be carved: twelve flutes running up
// the pot, where the cosine of twelve times the angle peaks, and fine throwing lines running round it. The wheel
// turns: every point is spun back by the time before the pot is measured, so the carving goes round with it.

// @param speed 0.5 0.0 2.0 How fast the wheel turns and the camera circles
// @param belly 0.6 0.0 1.0 How far the pot swells at its widest
// @param neck 0.5 0.0 1.0 How much the pot narrows below the lip
// @param flutes 0.5 0.0 1.0 How deep the flutes are carved
// @param heat 0.5 0.0 1.0 Slides the clay from deep ember to white-hot

constant float TAU = 6.2831853;
constant float FLOOR = 0.0;
constant float HEIGHT = 1.5;

// Episode 3's rotation.
float2 rot(float2 p, float a) {
    float c = cos(a), s = sin(a);
    return float2(c * p.x - s * p.y, s * p.x + c * p.y);
}

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

// ---- The pot ---------------------------------------------------------------------------------------------------

// The profile: the pot's radius at height y, from its foot at 0 to its lip at
// HEIGHT. A foot, a belly that swells in the lower half, a neck that narrows
// under the lip, and the lip flaring out again.
float profile(float y, float belly, float neck) {
    float h = clamp(y / HEIGHT, 0.0, 1.0);
    float r = 0.34 + 0.46 * belly * sin(3.14159 * pow(h, 0.85));
    r -= 0.22 * neck * exp(-pow((h - 0.78) / 0.1, 2.0));
    r += 0.10 * smoothstep(0.88, 1.0, h);
    return r;
}

// The carving: twelve flutes up the pot, each a groove where the cosine of
// twelve times the angle peaks, kept off the foot and the lip, and fine
// throwing lines round it, as a small change to the radius.
float carve(float y, float a, float flutes, float twist) {
    float h = clamp(y / HEIGHT, 0.0, 1.0);
    float ridge = 0.5 + 0.5 * cos(12.0 * a);
    float band = smoothstep(0.08, 0.3, h) * smoothstep(1.0, 0.8, h);
    float lines = 0.004 * sin(y * 44.0) * smoothstep(0.0, 0.1, h);
    return -0.1 * flutes * ridge * ridge * ridge * band + lines;
}

// The radius of the pot at a height and an angle: the profile, carved.
float radius(float y, float a, float4 shape) {
    return profile(y, shape.x, shape.y) + carve(y, a, shape.z, shape.w);
}

// The solid of revolution, with the angle round the axis now part of the
// radius, so the surface can differ from one side to another.
float revolve(float3 p, float4 shape) {
    float r = length(p.xz);
    float a = atan2(p.z, p.x);
    float d = r - radius(p.y, a, shape);
    float dy = radius(p.y + 0.01, a, shape) - radius(p.y - 0.01, a, shape);
    return d / sqrt(1.0 + dy * dy * 2500.0);
}

// The pot: the revolved profile, cut to its height, with a flat foot and lip.
float pot(float3 p, float4 shape) {
    float d = revolve(p, shape);
    float slab = abs(p.y - HEIGHT * 0.5) - HEIGHT * 0.5;
    return max(d, slab);
}

// The scene turns with the wheel: the point is spun back before the pot is
// measured, so the carving goes round with it.
float scene(float3 p, float t, float4 shape) {
    p.xz = rot(p.xz, -t);
    return pot(p, shape);
}

// The direction the surface faces: how the distance changes a small step
// along each axis.
float3 potNormal(float3 p, float t, float4 shape) {
    const float2 e = float2(0.002, 0.0);
    return normalize(float3(
        scene(p + e.xyy, t, shape) - scene(p - e.xyy, t, shape),
        scene(p + e.yxy, t, shape) - scene(p - e.yxy, t, shape),
        scene(p + e.yyx, t, shape) - scene(p - e.yyx, t, shape)));
}

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

// Where the light comes from: high, in front and to the left of the camera,
// so it goes round with it and the pot is always lit from the viewer's side.
float3 lightDir(float t) {
    float a = t * 0.18 + 0.9;
    return normalize(float3(sin(a), 1.1, cos(a)));
}

// The dark of the kiln, warmer toward the horizon, its hot mouth glowing
// where the light comes from.
float sky(float3 rd, float3 l) {
    float up = abs(rd.y);
    float mouth = pow(max(dot(rd, l), 0.0), 6.0);
    return 0.08 + 0.14 * exp(-up * 6.0) + 0.55 * mouth;
}

// The floor: tiles of fired clay with dark grout, the pot's shadow over
// them, fading into the kiln's haze far off.
float floorLevel(float3 p, float3 l) {
    float2 g = p.xz * 1.6;
    float2 f = abs(fract(g) - 0.5);
    float grout = smoothstep(0.47, 0.44, max(f.x, f.y));
    float2 id = floor(g);
    float tone = 0.28 + 0.08 * fract(sin(dot(id, float2(12.9, 78.2))) * 437.5);
    float3 toPot = float3(0.0, 0.5, 0.0) - p;
    float along = dot(toPot, l);
    float miss = length(toPot - l * along);
    float shadow = along > 0.0 ? smoothstep(0.6, 1.2, miss) : 1.0;
    return tone * grout * (0.35 + 0.65 * shadow);
}

// What a ray sees of the room: the floor if it heads down, otherwise the
// kiln's dark.
float env(float3 ro, float3 rd, float t) {
    float3 l = lightDir(t);
    float back = sky(rd, l);
    if (rd.y < -0.001) {
        float d = (FLOOR - ro.y) / rd.y;
        if (d > 0.0) {
            float3 p = ro + rd * d;
            return mix(back, floorLevel(p, l), exp(-d * 0.12));
        }
    }
    return back;
}

// Clay: as bright as the surface faces the light, a little of the kiln's
// glow everywhere.
float3 clayShade(float3 n, float t, float heat) {
    float diffuse = max(dot(n, lightDir(t)), 0.0);
    return kiln(0.18 + 0.55 * diffuse + (heat - 0.5) * 0.5);
}

float3 cameraPos(float t) {
    float a = t * 0.18;
    return float3(3.3 * sin(a), 1.25 + 0.3 * sin(t * 0.13), 3.3 * cos(a));
}

float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
    float t = u.time * p.speed;
    float side = min(u.resolution.x, u.resolution.y);
    float2 q = (uv - 0.5) * u.resolution / side * 2.0;
    float4 shape = float4(p.belly, p.neck, p.flutes, 0.0);
    float3 ro = cameraPos(t);
    float3 fw = normalize(float3(0.0, 0.62, 0.0) - ro);
    float3 rt = normalize(cross(fw, float3(0.0, 1.0, 0.0)));
    float3 up = cross(rt, fw);
    float3 rd = normalize(q.x * rt + q.y * up + 2.3 * fw);

    // March to the pot: the carved surface is not an exact distance, so each
    // step goes 70% of the way.
    float dist = 0.0;
    bool hit = false;
    for (int i = 0; i < 160; i++) {
        float d = scene(ro + rd * dist, t, shape);
        if (d < 0.001) { hit = true; break; }
        dist += d * 0.7;
        if (dist > 14.0) break;
    }

    float3 col;
    if (!hit) {
        col = kiln(env(ro, rd, t) + (p.heat - 0.5) * 0.4);
    } else {
        float3 pos = ro + rd * dist;
        float3 n = potNormal(pos, t, shape);
        col = clayShade(n, t, p.heat);
    }
    return float4(clamp(col, 0.0, 1.0), 1.0);
}
5 lines from stage 1 that are gone
    float d = r - profile(p.y, shape.x, shape.y);
    float dy = profile(p.y + 0.01, shape.x, shape.y)
             - profile(p.y - 0.01, shape.x, shape.y);
⋯
    float t = 14.0;
⋯
    float4 shape = float4(p.belly, p.neck, 0.0, 0.0);

Stage 3 · The twist

One line in carve winds the flutes: the angle a flute sits at is shifted by the height, spiral = a + twist × h × π, so a flute that starts at the foot has gone twist half-turns round the pot by the lip. That is all a twist is, in a distance field: the shape is measured at a point turned by an angle that grows along its length. The slider winds the spiral either way, and at 0 the flutes run straight.

stage-3-twist.metal3 new lines since stage 2
// Stage 3 · The twist
// A twist winds the flutes into a spiral: the angle each flute sits at is shifted by the height, so a flute that
// starts at the foot ends somewhere else at the lip. One line in `carve` does it, and the slider can wind the
// spiral either way.

// @param speed 0.5 0.0 2.0 How fast the wheel turns and the camera circles
// @param belly 0.6 0.0 1.0 How far the pot swells at its widest
// @param neck 0.5 0.0 1.0 How much the pot narrows below the lip
// @param flutes 0.5 0.0 1.0 How deep the flutes are carved
// @param twist 0.6 -2.0 2.0 How far the flutes wind round the pot from foot to lip
// @param heat 0.5 0.0 1.0 Slides the clay from deep ember to white-hot

constant float TAU = 6.2831853;
constant float FLOOR = 0.0;
constant float HEIGHT = 1.5;

// Episode 3's rotation.
float2 rot(float2 p, float a) {
    float c = cos(a), s = sin(a);
    return float2(c * p.x - s * p.y, s * p.x + c * p.y);
}

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

// ---- The pot ---------------------------------------------------------------------------------------------------

// The profile: the pot's radius at height y, from its foot at 0 to its lip at
// HEIGHT. A foot, a belly that swells in the lower half, a neck that narrows
// under the lip, and the lip flaring out again.
float profile(float y, float belly, float neck) {
    float h = clamp(y / HEIGHT, 0.0, 1.0);
    float r = 0.34 + 0.46 * belly * sin(3.14159 * pow(h, 0.85));
    r -= 0.22 * neck * exp(-pow((h - 0.78) / 0.1, 2.0));
    r += 0.10 * smoothstep(0.88, 1.0, h);
    return r;
}

// The carving: flutes that run up the pot and throwing lines that run round
// it, as a small change to the radius. The angle each flute sits at is
// shifted by the height, so a twist winds them into a spiral.
float carve(float y, float a, float flutes, float twist) {
    float h = clamp(y / HEIGHT, 0.0, 1.0);
    float spiral = a + twist * h * TAU * 0.5;
    float ridge = 0.5 + 0.5 * cos(12.0 * spiral);
    float band = smoothstep(0.08, 0.3, h) * smoothstep(1.0, 0.8, h);
    float lines = 0.004 * sin(y * 44.0) * smoothstep(0.0, 0.1, h);
    return -0.1 * flutes * ridge * ridge * ridge * band + lines;
}

// The radius of the pot at a height and an angle: the profile, carved.
float radius(float y, float a, float4 shape) {
    return profile(y, shape.x, shape.y) + carve(y, a, shape.z, shape.w);
}

// The solid of revolution, with the angle round the axis now part of the
// radius, so the surface can differ from one side to another.
float revolve(float3 p, float4 shape) {
    float r = length(p.xz);
    float a = atan2(p.z, p.x);
    float d = r - radius(p.y, a, shape);
    float dy = radius(p.y + 0.01, a, shape) - radius(p.y - 0.01, a, shape);
    return d / sqrt(1.0 + dy * dy * 2500.0);
}

// The pot: the revolved profile, cut to its height, with a flat foot and lip.
float pot(float3 p, float4 shape) {
    float d = revolve(p, shape);
    float slab = abs(p.y - HEIGHT * 0.5) - HEIGHT * 0.5;
    return max(d, slab);
}

// The scene turns with the wheel: the point is spun back before the pot is
// measured, so the carving goes round with it.
float scene(float3 p, float t, float4 shape) {
    p.xz = rot(p.xz, -t);
    return pot(p, shape);
}

// The direction the surface faces: how the distance changes a small step
// along each axis.
float3 potNormal(float3 p, float t, float4 shape) {
    const float2 e = float2(0.002, 0.0);
    return normalize(float3(
        scene(p + e.xyy, t, shape) - scene(p - e.xyy, t, shape),
        scene(p + e.yxy, t, shape) - scene(p - e.yxy, t, shape),
        scene(p + e.yyx, t, shape) - scene(p - e.yyx, t, shape)));
}

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

// Where the light comes from: high, in front and to the left of the camera,
// so it goes round with it and the pot is always lit from the viewer's side.
float3 lightDir(float t) {
    float a = t * 0.18 + 0.9;
    return normalize(float3(sin(a), 1.1, cos(a)));
}

// The dark of the kiln, warmer toward the horizon, its hot mouth glowing
// where the light comes from.
float sky(float3 rd, float3 l) {
    float up = abs(rd.y);
    float mouth = pow(max(dot(rd, l), 0.0), 6.0);
    return 0.08 + 0.14 * exp(-up * 6.0) + 0.55 * mouth;
}

// The floor: tiles of fired clay with dark grout, the pot's shadow over
// them, fading into the kiln's haze far off.
float floorLevel(float3 p, float3 l) {
    float2 g = p.xz * 1.6;
    float2 f = abs(fract(g) - 0.5);
    float grout = smoothstep(0.47, 0.44, max(f.x, f.y));
    float2 id = floor(g);
    float tone = 0.28 + 0.08 * fract(sin(dot(id, float2(12.9, 78.2))) * 437.5);
    float3 toPot = float3(0.0, 0.5, 0.0) - p;
    float along = dot(toPot, l);
    float miss = length(toPot - l * along);
    float shadow = along > 0.0 ? smoothstep(0.6, 1.2, miss) : 1.0;
    return tone * grout * (0.35 + 0.65 * shadow);
}

// What a ray sees of the room: the floor if it heads down, otherwise the
// kiln's dark.
float env(float3 ro, float3 rd, float t) {
    float3 l = lightDir(t);
    float back = sky(rd, l);
    if (rd.y < -0.001) {
        float d = (FLOOR - ro.y) / rd.y;
        if (d > 0.0) {
            float3 p = ro + rd * d;
            return mix(back, floorLevel(p, l), exp(-d * 0.12));
        }
    }
    return back;
}

// Clay: as bright as the surface faces the light, a little of the kiln's
// glow everywhere.
float3 clayShade(float3 n, float t, float heat) {
    float diffuse = max(dot(n, lightDir(t)), 0.0);
    return kiln(0.18 + 0.55 * diffuse + (heat - 0.5) * 0.5);
}

float3 cameraPos(float t) {
    float a = t * 0.18;
    return float3(3.3 * sin(a), 1.25 + 0.3 * sin(t * 0.13), 3.3 * cos(a));
}

float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
    float t = u.time * p.speed;
    float side = min(u.resolution.x, u.resolution.y);
    float2 q = (uv - 0.5) * u.resolution / side * 2.0;
    float4 shape = float4(p.belly, p.neck, p.flutes, p.twist);
    float3 ro = cameraPos(t);
    float3 fw = normalize(float3(0.0, 0.62, 0.0) - ro);
    float3 rt = normalize(cross(fw, float3(0.0, 1.0, 0.0)));
    float3 up = cross(rt, fw);
    float3 rd = normalize(q.x * rt + q.y * up + 2.3 * fw);

    // March to the pot: the carved surface is not an exact distance, so each
    // step goes 70% of the way.
    float dist = 0.0;
    bool hit = false;
    for (int i = 0; i < 160; i++) {
        float d = scene(ro + rd * dist, t, shape);
        if (d < 0.001) { hit = true; break; }
        dist += d * 0.7;
        if (dist > 14.0) break;
    }

    float3 col;
    if (!hit) {
        col = kiln(env(ro, rd, t) + (p.heat - 0.5) * 0.4);
    } else {
        float3 pos = ro + rd * dist;
        float3 n = potNormal(pos, t, shape);
        col = clayShade(n, t, p.heat);
    }
    return float4(clamp(col, 0.0, 1.0), 1.0);
}
2 lines from stage 2 that are gone
    float ridge = 0.5 + 0.5 * cos(12.0 * a);
⋯
    float4 shape = float4(p.belly, p.neck, p.flutes, 0.0);

Stage 4 · The glaze

glaze is the material. Underneath it is still clay, lit by how squarely it faces the light. On top, the kiln is mirrored: reflect(rd, n) bounces the ray off the surface and env says what it sees, and episode 12's fresnel decides how much of that is mirrored at this angle, little head on and nearly all at a grazing angle. gloss sets both the index of refraction the Fresnel is worked out from and the highlight, pow(dot(rr, l), 120) for a glassy glaze and a broad, faint one for a matte glaze.

The glaze pools. grooveShade takes four short steps out along the normal and asks how much nearer than each step the surface is: in a groove the surface closes in and the number falls. The colour there is pushed down the kiln palette and warmed, so the grooves are deep and dark while the ridges run light, as a real glaze does when it is thicker where it can't run off. Below a wavy line at the foot, where the potter's fingers held the pot, the clay is bare: matte, brown and lit only by the light.

stage-4-glaze.metal35 new lines since stage 3
// Stage 4 · The glaze
// The clay becomes glaze: a material. It mirrors the kiln, as much as Schlick's Fresnel allows at each angle,
// catches the light in a highlight sharpened by `gloss`, and pools in the grooves, where a few short steps out
// along the normal find the surface closed in, so the colour there is darker and deeper. Below the glaze line the
// foot stays bare clay.

// @param speed 0.5 0.0 2.0 How fast the wheel turns and the camera circles
// @param belly 0.6 0.0 1.0 How far the pot swells at its widest
// @param neck 0.5 0.0 1.0 How much the pot narrows below the lip
// @param flutes 0.5 0.0 1.0 How deep the flutes are carved
// @param twist 0.6 -2.0 2.0 How far the flutes wind round the pot from foot to lip
// @param gloss 0.7 0.0 1.0 How glassy the glaze is: matte to mirror
// @param heat 0.5 0.0 1.0 Slides the glaze from deep ember to white-hot

constant float TAU = 6.2831853;
constant float FLOOR = 0.0;
constant float HEIGHT = 1.5;

// Episode 3's rotation.
float2 rot(float2 p, float a) {
    float c = cos(a), s = sin(a);
    return float2(c * p.x - s * p.y, s * p.x + c * p.y);
}

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

// Episode 12's Fresnel: Schlick's approximation.
float fresnel(float cosi, float ior) {
    float f0 = (ior - 1.0) / (ior + 1.0);
    f0 *= f0;
    return f0 + (1.0 - f0) * pow(1.0 - clamp(cosi, 0.0, 1.0), 5.0);
}

// ---- The pot ---------------------------------------------------------------------------------------------------

// The profile: the pot's radius at height y, from its foot at 0 to its lip at
// HEIGHT. A foot, a belly that swells in the lower half, a neck that narrows
// under the lip, and the lip flaring out again.
float profile(float y, float belly, float neck) {
    float h = clamp(y / HEIGHT, 0.0, 1.0);
    float r = 0.34 + 0.46 * belly * sin(3.14159 * pow(h, 0.85));
    r -= 0.22 * neck * exp(-pow((h - 0.78) / 0.1, 2.0));
    r += 0.10 * smoothstep(0.88, 1.0, h);
    return r;
}

// The carving: flutes that run up the pot and throwing lines that run round
// it, as a small change to the radius. The angle each flute sits at is
// shifted by the height, so a twist winds them into a spiral.
float carve(float y, float a, float flutes, float twist) {
    float h = clamp(y / HEIGHT, 0.0, 1.0);
    float spiral = a + twist * h * TAU * 0.5;
    float ridge = 0.5 + 0.5 * cos(12.0 * spiral);
    float band = smoothstep(0.08, 0.3, h) * smoothstep(1.0, 0.8, h);
    float lines = 0.004 * sin(y * 44.0) * smoothstep(0.0, 0.1, h);
    return -0.1 * flutes * ridge * ridge * ridge * band + lines;
}

// The radius of the pot at a height and an angle: the profile, carved.
float radius(float y, float a, float4 shape) {
    return profile(y, shape.x, shape.y) + carve(y, a, shape.z, shape.w);
}

// The solid of revolution, with the angle round the axis now part of the
// radius, so the surface can differ from one side to another.
float revolve(float3 p, float4 shape) {
    float r = length(p.xz);
    float a = atan2(p.z, p.x);
    float d = r - radius(p.y, a, shape);
    float dy = radius(p.y + 0.01, a, shape) - radius(p.y - 0.01, a, shape);
    return d / sqrt(1.0 + dy * dy * 2500.0);
}

// The pot: the revolved profile, cut to its height, with a flat foot and lip.
float pot(float3 p, float4 shape) {
    float d = revolve(p, shape);
    float slab = abs(p.y - HEIGHT * 0.5) - HEIGHT * 0.5;
    return max(d, slab);
}

// The scene turns with the wheel: the point is spun back before the pot is
// measured, so the carving goes round with it.
float scene(float3 p, float t, float4 shape) {
    p.xz = rot(p.xz, -t);
    return pot(p, shape);
}

// The direction the surface faces: how the distance changes a small step
// along each axis.
float3 potNormal(float3 p, float t, float4 shape) {
    const float2 e = float2(0.002, 0.0);
    return normalize(float3(
        scene(p + e.xyy, t, shape) - scene(p - e.xyy, t, shape),
        scene(p + e.yxy, t, shape) - scene(p - e.yxy, t, shape),
        scene(p + e.yyx, t, shape) - scene(p - e.yyx, t, shape)));
}

// How closed in a point is: a few short steps out along the normal, and
// how much nearer than each step the scene is. The grooves come out dark.
float grooveShade(float3 p, float3 n, float t, float4 shape) {
    float occ = 0.0;
    for (int i = 1; i <= 4; i++) {
        float h = 0.03 * float(i);
        occ += (h - scene(p + n * h, t, shape)) / h;
    }
    return clamp(1.0 - occ * 0.25, 0.0, 1.0);
}

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

// Where the light comes from: high, in front and to the left of the camera,
// so it goes round with it and the pot is always lit from the viewer's side.
float3 lightDir(float t) {
    float a = t * 0.18 + 0.9;
    return normalize(float3(sin(a), 1.1, cos(a)));
}

// The dark of the kiln, warmer toward the horizon, its hot mouth glowing
// where the light comes from.
float sky(float3 rd, float3 l) {
    float up = abs(rd.y);
    float mouth = pow(max(dot(rd, l), 0.0), 6.0);
    return 0.08 + 0.14 * exp(-up * 6.0) + 0.55 * mouth;
}

// The floor: tiles of fired clay with dark grout, the pot's shadow over
// them, fading into the kiln's haze far off.
float floorLevel(float3 p, float3 l) {
    float2 g = p.xz * 1.6;
    float2 f = abs(fract(g) - 0.5);
    float grout = smoothstep(0.47, 0.44, max(f.x, f.y));
    float2 id = floor(g);
    float tone = 0.28 + 0.08 * fract(sin(dot(id, float2(12.9, 78.2))) * 437.5);
    float3 toPot = float3(0.0, 0.5, 0.0) - p;
    float along = dot(toPot, l);
    float miss = length(toPot - l * along);
    float shadow = along > 0.0 ? smoothstep(0.6, 1.2, miss) : 1.0;
    return tone * grout * (0.35 + 0.65 * shadow);
}

// What a ray sees of the room: the floor if it heads down, otherwise the
// kiln's dark.
float env(float3 ro, float3 rd, float t) {
    float3 l = lightDir(t);
    float back = sky(rd, l);
    if (rd.y < -0.001) {
        float d = (FLOOR - ro.y) / rd.y;
        if (d > 0.0) {
            float3 p = ro + rd * d;
            return mix(back, floorLevel(p, l), exp(-d * 0.12));
        }
    }
    return back;
}

// ---- The glaze ---------------------------------------------------------------------------------------------------

// The glaze: a lit surface that mirrors the kiln. Bare clay at the foot;
// above it the kiln palette, dark where the glaze pools in the grooves and
// lighter over a ridge. Fresnel sets how much of the kiln is mirrored, and
// the highlight is the light itself.
float3 glaze(float3 p, float3 n, float3 rd, float occ, float t,
             float gloss, float heat) {
    float3 l = lightDir(t);
    float diffuse = max(dot(n, l), 0.0);
    float3 rr = reflect(rd, n);
    float mirror = env(p + n * 0.01, rr, t);
    float f = fresnel(dot(-rd, n), 1.0 + 1.2 * gloss);
    float spec = pow(max(dot(rr, l), 0.0), mix(6.0, 120.0, gloss))
               * mix(0.08, 2.2, gloss);
    float pool = 1.0 - occ;
    float level = (0.18 + 0.55 * diffuse) * occ + (heat - 0.5) * 0.5;
    float3 colour = kiln(level - 0.3 * pool);
    colour = mix(colour, colour * float3(0.9, 0.7, 0.6), pool);
    float3 seen = mix(colour, kiln(mirror + 0.25), f * gloss)
                + spec * kiln(0.95);
    float edge = 0.14 + 0.02 * sin(atan2(p.z, p.x) * 5.0);
    float bare = smoothstep(edge + 0.02, edge - 0.02, p.y);
    float3 clay = float3(0.42, 0.28, 0.2) * (0.25 + 0.75 * diffuse) * occ;
    return mix(seen, clay, bare);
}

float3 cameraPos(float t) {
    float a = t * 0.18;
    return float3(3.3 * sin(a), 1.25 + 0.3 * sin(t * 0.13), 3.3 * cos(a));
}

float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
    float t = u.time * p.speed;
    float side = min(u.resolution.x, u.resolution.y);
    float2 q = (uv - 0.5) * u.resolution / side * 2.0;
    float4 shape = float4(p.belly, p.neck, p.flutes, p.twist);
    float3 ro = cameraPos(t);
    float3 fw = normalize(float3(0.0, 0.62, 0.0) - ro);
    float3 rt = normalize(cross(fw, float3(0.0, 1.0, 0.0)));
    float3 up = cross(rt, fw);
    float3 rd = normalize(q.x * rt + q.y * up + 2.3 * fw);

    // March to the pot: the carved surface is not an exact distance, so each
    // step goes 70% of the way.
    float dist = 0.0;
    bool hit = false;
    for (int i = 0; i < 160; i++) {
        float d = scene(ro + rd * dist, t, shape);
        if (d < 0.001) { hit = true; break; }
        dist += d * 0.7;
        if (dist > 14.0) break;
    }

    float3 col;
    if (!hit) {
        col = kiln(env(ro, rd, t) + (p.heat - 0.5) * 0.4);
    } else {
        float3 pos = ro + rd * dist;
        float3 n = potNormal(pos, t, shape);
        float occ = grooveShade(pos, n, t, shape);
        // The glaze line is drawn on the pot, so the point is spun back to it.
        float3 local = float3(rot(pos.xz, -t), pos.y).xzy;
        col = glaze(local, n, rd, occ, t, p.gloss, p.heat);
    }
    return float4(clamp(col, 0.0, 1.0), 1.0);
}
4 lines from stage 3 that are gone
float3 clayShade(float3 n, float t, float heat) {
    float diffuse = max(dot(n, lightDir(t)), 0.0);
    return kiln(0.18 + 0.55 * diffuse + (heat - 0.5) * 0.5);
⋯
        col = clayShade(n, t, p.heat);

Stage 5 · Pottery wheel

The last step hollows the pot and puts it on the wheel.

  • Hollow. pot cuts a cavity out of the solid: the same revolved profile, 0.08 smaller, from a thick base at y = 0.2 up through the lip. The lip becomes a ring, and the dark inside shows when the camera looks down into it.
  • The wheel. wheel is a thick disc, a solid of revolution with the simplest profile of all, and a stem down to the floor, which drops to y = -1. scene now returns which it hit as well as how far, so the wheel head is shaded as iron, dark with concentric rings and a dull highlight, turning with the pot.
  • The camera circles once every 70 seconds at speed 0.5, rising and falling a little, and the light goes round with it, high and to the left, so the pot is always lit from the viewer's side. Far parts of the scene fade into the kiln's dark.
the kiln palette and shade(), from the finished shader
// The kiln palette from episode 1.
float3 kiln(float t) {
    float3 c0 = float3(0.020, 0.012, 0.020);
    float3 c1 = float3(0.280, 0.040, 0.030);
    float3 c2 = float3(0.880, 0.260, 0.050);
    float3 c3 = float3(1.000, 0.680, 0.200);
    float3 c4 = float3(1.000, 0.970, 0.840);
    t = clamp(t, 0.0, 1.0);
    float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
    c = mix(c, c2, smoothstep(0.22, 0.50, t));
    c = mix(c, c3, smoothstep(0.50, 0.76, t));
    return mix(c, c4, smoothstep(0.76, 1.00, t));
}

float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
    float t = u.time * p.speed;
    float side = min(u.resolution.x, u.resolution.y);
    float2 q = (uv - 0.5) * u.resolution / side * 2.0;
    float4 shape = float4(p.belly, p.neck, p.flutes, p.twist);
    float3 ro = cameraPos(t);
    float3 fw = normalize(float3(0.0, 0.62, 0.0) - ro);
    float3 rt = normalize(cross(fw, float3(0.0, 1.0, 0.0)));
    float3 up = cross(rt, fw);
    float3 rd = normalize(q.x * rt + q.y * up + 2.3 * fw);

    // March to the scene: the carved surface is not an exact distance, so
    // each step goes 70% of the way.
    float dist = 0.0;
    float which = 0.0;
    for (int i = 0; i < 160; i++) {
        float2 s = scene(ro + rd * dist, t, shape);
        if (s.x < 0.001) { which = s.y; break; }
        dist += s.x * 0.7;
        if (dist > 14.0) break;
    }

    float3 col;
    if (which == 0.0) {
        col = kiln(env(ro, rd, t) + (p.heat - 0.5) * 0.4);
    } else {
        float3 pos = ro + rd * dist;
        float3 n = potNormal(pos, t, shape);
        float occ = grooveShade(pos, n, t, shape);
        if (which == 1.0) {
            float3 local = float3(rot(pos.xz, -t), pos.y).xzy;
            col = glaze(local, n, rd, occ, t, p.gloss, p.heat);
        } else {
            col = ironShade(pos, n, rd, t) * occ;
        }
        col = mix(col, kiln(0.08), smoothstep(6.0, 14.0, dist));
    }
    return float4(clamp(col, 0.0, 1.0), 1.0);
}
episode-16-pottery-wheel.metal, the whole file76 new lines since stage 4
// Ray Kiln · Episode 16 · Pottery Wheel
//
// @episode 16 Pottery Wheel
// @length 60
// @variation 30
// @variations 1,2,4,5,6,7,9,10
// @still 14.0
// @teaches Solids of revolution, twisting, a glaze material
// @category Fragment
// @tags raymarching, signed distance fields, solids of revolution, twisting, glaze, fresnel, procedural sound
// @final Pottery wheel | A fluted pot turning on the wheel, its glaze mirroring the kiln and pooling dark in the grooves, bare clay at its foot.
//
// A pot on a potter's wheel, drawn by raymarching. A pot is a solid of revolution: one curve, the radius at each
// height, spun round an axis. Flutes and throwing lines carve its surface, and a twist winds the flutes into a
// spiral as the wheel turns. The glaze is a material: it mirrors the kiln by Schlick's Fresnel, catches the light in
// a sharp highlight, and pools darker where the grooves keep it from running off.
// Each @param line below becomes a slider in the host app and a row in the page's parameters table:
//   // @param name default min max description

// @param speed 0.5 0.0 2.0 How fast the wheel turns and the camera circles
// @param belly 0.6 0.0 1.0 How far the pot swells at its widest
// @param neck 0.5 0.0 1.0 How much the pot narrows below the lip
// @param flutes 0.5 0.0 1.0 How deep the flutes are carved
// @param twist 0.6 -2.0 2.0 How far the flutes wind round the pot from foot to lip
// @param gloss 0.7 0.0 1.0 How glassy the glaze is: matte to mirror
// @param heat 0.5 0.0 1.0 Slides the glaze from deep ember to white-hot
// @param volume 0.8 0.0 1.0 Loudness of the sound

constant float TAU = 6.2831853;
constant float FLOOR = -1.0;
constant float HEIGHT = 1.5;

// Episode 3's rotation.
float2 rot(float2 p, float a) {
    float c = cos(a), s = sin(a);
    return float2(c * p.x - s * p.y, s * p.x + c * p.y);
}

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

// Episode 12's Fresnel: Schlick's approximation.
float fresnel(float cosi, float ior) {
    float f0 = (ior - 1.0) / (ior + 1.0);
    f0 *= f0;
    return f0 + (1.0 - f0) * pow(1.0 - clamp(cosi, 0.0, 1.0), 5.0);
}

// ---- The pot ---------------------------------------------------------------------------------------------------

// The profile: the pot's radius at height y, from its foot at 0 to its lip at
// HEIGHT. A foot, a belly that swells in the lower half, a neck that narrows
// under the lip, and the lip flaring out again.
float profile(float y, float belly, float neck) {
    float h = clamp(y / HEIGHT, 0.0, 1.0);
    float r = 0.34 + 0.46 * belly * sin(3.14159 * pow(h, 0.85));
    r -= 0.22 * neck * exp(-pow((h - 0.78) / 0.1, 2.0));
    r += 0.10 * smoothstep(0.88, 1.0, h);
    return r;
}

// The carving: flutes that run up the pot and throwing lines that run round
// it, as a small change to the radius. The angle each flute sits at is
// shifted by the height, so a twist winds them into a spiral.
float carve(float y, float a, float flutes, float twist) {
    float h = clamp(y / HEIGHT, 0.0, 1.0);
    float spiral = a + twist * h * TAU * 0.5;
    float ridge = 0.5 + 0.5 * cos(12.0 * spiral);
    float band = smoothstep(0.08, 0.3, h) * smoothstep(1.0, 0.8, h);
    float lines = 0.004 * sin(y * 44.0) * smoothstep(0.0, 0.1, h);
    return -0.1 * flutes * ridge * ridge * ridge * band + lines;
}

// The radius of the pot at a height and an angle: the profile, carved.
float radius(float y, float a, float4 shape) {
    return profile(y, shape.x, shape.y) + carve(y, a, shape.z, shape.w);
}

// The solid of revolution: a point's distance from the axis, less the radius
// at its height and angle. The surface leans where the profile slopes, so
// the distance is scaled down by that slope to stay safe to march.
float revolve(float3 p, float4 shape) {
    float r = length(p.xz);
    float a = atan2(p.z, p.x);
    float d = r - radius(p.y, a, shape);
    float dy = radius(p.y + 0.01, a, shape) - radius(p.y - 0.01, a, shape);
    return d / sqrt(1.0 + dy * dy * 2500.0);
}

// The pot: the revolved profile, cut to its height, hollowed from the inside
// down to a thick base so the lip is a ring and the dark inside shows.
float pot(float3 p, float4 shape) {
    float d = revolve(p, shape);
    float slab = abs(p.y - HEIGHT * 0.5) - HEIGHT * 0.5;
    float body = max(d, slab);
    float cavity = max(d + 0.08, 0.2 - p.y);
    return max(body, -cavity);
}

// The wheel: a thick disc the pot sits on, a stem down to the floor.
float wheel(float3 p) {
    float2 q = float2(length(p.xz), p.y);
    float head = max(q.x - 1.05, abs(q.y + 0.08) - 0.08);
    float stem = max(q.x - 0.18, abs(q.y + 0.58) - 0.42);
    return min(head, stem);
}

// The scene: the pot first, then the wheel, with the nearer one winning.
// The .w of the answer says which: 1 for the pot, 2 for the wheel.
float2 scene(float3 p, float t, float4 shape) {
    p.xz = rot(p.xz, -t);
    float dp = pot(p, shape);
    float dw = wheel(p);
    return dp < dw ? float2(dp, 1.0) : float2(dw, 2.0);
}

// The direction the surface faces: how the distance changes a small step
// along each axis.
float3 potNormal(float3 p, float t, float4 shape) {
    const float2 e = float2(0.002, 0.0);
    return normalize(float3(
        scene(p + e.xyy, t, shape).x - scene(p - e.xyy, t, shape).x,
        scene(p + e.yxy, t, shape).x - scene(p - e.yxy, t, shape).x,
        scene(p + e.yyx, t, shape).x - scene(p - e.yyx, t, shape).x));
}

// How closed in a point is: a few short steps out along the normal, and
// how much nearer than each step the scene is. The grooves come out dark.
float grooveShade(float3 p, float3 n, float t, float4 shape) {
    float occ = 0.0;
    for (int i = 1; i <= 4; i++) {
        float h = 0.03 * float(i);
        occ += (h - scene(p + n * h, t, shape).x) / h;
    }
    return clamp(1.0 - occ * 0.25, 0.0, 1.0);
}

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

// Where the light comes from: high, in front and to the left of the camera,
// so it goes round with it and the pot is always lit from the viewer's side.
float3 lightDir(float t) {
    float a = t * 0.18 + 0.9;
    return normalize(float3(sin(a), 1.1, cos(a)));
}

// The dark of the kiln, warmer toward the horizon, its hot mouth glowing
// where the light comes from.
float sky(float3 rd, float3 l) {
    float up = abs(rd.y);
    float mouth = pow(max(dot(rd, l), 0.0), 6.0);
    return 0.08 + 0.14 * exp(-up * 6.0) + 0.55 * mouth;
}

// The floor: tiles of fired clay with dark grout, the wheel's shadow over
// them, fading into the kiln's haze far off.
float floorLevel(float3 p, float3 l) {
    float2 g = p.xz * 1.6;
    float2 f = abs(fract(g) - 0.5);
    float grout = smoothstep(0.47, 0.44, max(f.x, f.y));
    float2 id = floor(g);
    float tone = 0.28 + 0.08 * fract(sin(dot(id, float2(12.9, 78.2))) * 437.5);
    float3 toWheel = float3(0.0, -0.08, 0.0) - p;
    float along = dot(toWheel, l);
    float miss = length(toWheel - l * along);
    float shadow = along > 0.0 ? smoothstep(0.9, 1.6, miss) : 1.0;
    return tone * grout * (0.35 + 0.65 * shadow);
}

// What a ray sees of the room: the floor if it heads down, otherwise the
// kiln's dark.
float env(float3 ro, float3 rd, float t) {
    float3 l = lightDir(t);
    float back = sky(rd, l);
    if (rd.y < -0.001) {
        float d = (FLOOR - ro.y) / rd.y;
        if (d > 0.0) {
            float3 p = ro + rd * d;
            return mix(back, floorLevel(p, l), exp(-d * 0.12));
        }
    }
    return back;
}

// ---- The glaze ---------------------------------------------------------------------------------------------------

// The glaze: a lit surface that mirrors the kiln. Bare clay at the foot;
// above it the kiln palette, dark where the glaze pools in the grooves and
// lighter over a ridge. Fresnel sets how much of the kiln is mirrored, and
// the highlight is the light itself.
float3 glaze(float3 p, float3 n, float3 rd, float occ, float t,
             float gloss, float heat) {
    float3 l = lightDir(t);
    float diffuse = max(dot(n, l), 0.0);
    float3 rr = reflect(rd, n);
    float mirror = env(p + n * 0.01, rr, t);
    float f = fresnel(dot(-rd, n), 1.0 + 1.2 * gloss);
    float spec = pow(max(dot(rr, l), 0.0), mix(6.0, 120.0, gloss))
               * mix(0.08, 2.2, gloss);
    float pool = 1.0 - occ;
    float level = (0.18 + 0.55 * diffuse) * occ + (heat - 0.5) * 0.5;
    float3 colour = kiln(level - 0.3 * pool);
    colour = mix(colour, colour * float3(0.9, 0.7, 0.6), pool);
    float3 seen = mix(colour, kiln(mirror + 0.25), f * gloss)
                + spec * kiln(0.95);
    float edge = 0.14 + 0.02 * sin(atan2(p.z, p.x) * 5.0);
    float bare = smoothstep(edge + 0.02, edge - 0.02, p.y);
    float3 clay = float3(0.42, 0.28, 0.2) * (0.25 + 0.75 * diffuse) * occ;
    return mix(seen, clay, bare);
}

// The wheel head: dark iron, its rim catching the light, grooves turning.
float3 ironShade(float3 p, float3 n, float3 rd, float t) {
    float3 l = lightDir(t);
    float diffuse = max(dot(n, l), 0.0);
    float3 rr = reflect(rd, n);
    float spec = pow(max(dot(rr, l), 0.0), 30.0) * 0.5;
    float r = length(p.xz);
    float rings = 0.85 + 0.15 * sin(r * 60.0);
    float3 base = float3(0.16, 0.14, 0.14) * rings;
    return base * (0.3 + 0.7 * diffuse) + spec * float3(0.9, 0.8, 0.7);
}

float3 cameraPos(float t) {
    float a = t * 0.18;
    return float3(3.3 * sin(a), 1.25 + 0.3 * sin(t * 0.13), 3.3 * cos(a));
}

float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
    float t = u.time * p.speed;
    float side = min(u.resolution.x, u.resolution.y);
    float2 q = (uv - 0.5) * u.resolution / side * 2.0;
    float4 shape = float4(p.belly, p.neck, p.flutes, p.twist);
    float3 ro = cameraPos(t);
    float3 fw = normalize(float3(0.0, 0.62, 0.0) - ro);
    float3 rt = normalize(cross(fw, float3(0.0, 1.0, 0.0)));
    float3 up = cross(rt, fw);
    float3 rd = normalize(q.x * rt + q.y * up + 2.3 * fw);

    // March to the scene: the carved surface is not an exact distance, so
    // each step goes 70% of the way.
    float dist = 0.0;
    float which = 0.0;
    for (int i = 0; i < 160; i++) {
        float2 s = scene(ro + rd * dist, t, shape);
        if (s.x < 0.001) { which = s.y; break; }
        dist += s.x * 0.7;
        if (dist > 14.0) break;
    }

    float3 col;
    if (which == 0.0) {
        col = kiln(env(ro, rd, t) + (p.heat - 0.5) * 0.4);
    } else {
        float3 pos = ro + rd * dist;
        float3 n = potNormal(pos, t, shape);
        float occ = grooveShade(pos, n, t, shape);
        if (which == 1.0) {
            float3 local = float3(rot(pos.xz, -t), pos.y).xzy;
            col = glaze(local, n, rd, occ, t, p.gloss, p.heat);
        } else {
            col = ironShade(pos, n, rd, t) * occ;
        }
        col = mix(col, kiln(0.08), smoothstep(6.0, 14.0, dist));
    }
    return float4(clamp(col, 0.0, 1.0), 1.0);
}

// ---- Sound -----------------------------------------------------------------------------------------------------
// The wheel at work. A low hum from the wheel's motor, its pitch following the speed; the hiss of wet hands on
// turning clay, which swells once a turn where the fingers meet the thickest part; a knock as each flute passes the
// potter's thumb, twelve to a turn, soft when the flutes are shallow; and under it all the slow breath of the kiln.

// Episode 5's integer hash.
float hashU(uint n) {
    n = (n << 13u) ^ n;
    n = n * (n * n * 15731u + 789221u) + 1376312589u;
    return float(n & 0x7fffffffu) / float(0x7fffffff);
}

// Episode 5's sound noise: random values joined `rate` times a second.
float noise1(float t, float rate, uint seed) {
    float x = t * rate;
    float i = floor(x), f = fract(x);
    f = f * f * (3.0 - 2.0 * f);
    uint k = uint(i) + seed * 7919u;
    return mix(hashU(k), hashU(k + 1u), f) * 2.0 - 1.0;
}

float2 sound(float t, constant Params& p) {
    // One turn of the wheel takes TAU / speed seconds: the picture's spin.
    float turns = t * p.speed / TAU;
    float phase = fract(turns);

    // The motor: a hum whose pitch climbs with the speed, with its octave.
    float f = 55.0 * (0.8 + 0.6 * p.speed);
    float hum = sin(TAU * f * t) + 0.4 * sin(TAU * f * 2.0 * t + 0.3);
    hum *= 0.12 * smoothstep(0.0, 0.3, p.speed);

    // The hands on the clay: hiss that swells where the belly passes.
    float hiss = noise1(t, 6000.0, 1u) * 0.5 + noise1(t, 1800.0, 2u) * 0.5;
    float swell = 0.4 + 0.6 * pow(0.5 + 0.5 * cos(TAU * phase), 2.0);
    hiss *= 0.07 * swell * (0.3 + 0.7 * p.belly)
          * smoothstep(0.0, 0.2, p.speed);

    // The flutes under the thumb: twelve soft knocks a turn, each a short
    // damped thump; deeper flutes knock louder.
    float slot = floor(turns * 12.0);
    float since = (turns * 12.0 - slot) * TAU / max(p.speed * 12.0, 0.05);
    float knock = sin(TAU * 140.0 * since) * exp(-since * 28.0)
                * (0.6 + 0.4 * hashU(uint(slot)));
    knock *= 0.35 * p.flutes * smoothstep(0.0, 0.2, p.speed);

    // The kiln's breath: a slow swell of low noise.
    float breath = noise1(t, 90.0, 3u) * 0.06 * (0.5 + 0.5 * sin(t * 0.4));

    float pan = 0.5 + 0.3 * sin(TAU * phase);
    float2 mixdown = float2(hum + breath)
                   + float2(1.0 - pan, pan) * (hiss * 2.0 + knock);
    float fadeIn = smoothstep(0.0, 1.5, t);
    return tanh(mixdown * 1.6) * 0.8 * fadeIn * p.volume;
}
17 lines from stage 4 that are gone
constant float FLOOR = 0.0;
⋯
    return max(d, slab);
⋯
float scene(float3 p, float t, float4 shape) {
⋯
    return pot(p, shape);
⋯
        scene(p + e.xyy, t, shape) - scene(p - e.xyy, t, shape),
        scene(p + e.yxy, t, shape) - scene(p - e.yxy, t, shape),
        scene(p + e.yyx, t, shape) - scene(p - e.yyx, t, shape)));
⋯
        occ += (h - scene(p + n * h, t, shape)) / h;
⋯
    float3 toPot = float3(0.0, 0.5, 0.0) - p;
    float along = dot(toPot, l);
    float miss = length(toPot - l * along);
    float shadow = along > 0.0 ? smoothstep(0.6, 1.2, miss) : 1.0;
⋯
    bool hit = false;
⋯
        float d = scene(ro + rd * dist, t, shape);
        if (d < 0.001) { hit = true; break; }
        dist += d * 0.7;
⋯
    if (!hit) {

The sound

The sound is worked out from the time alone and reads the same sliders as the picture: everything is timed from the wheel's turn, TAU / speed seconds, which is the picture's spin.

  • The motor. A low hum, 55 Hz at a slow wheel, whose pitch climbs with speed, with its octave on top.
  • The hands. Two layers of noise, one bright and one dull, for wet hands on turning clay, swelling once a turn as the belly comes round, and louder for a wider pot. Both pan with the turn.
  • The flutes. Twelve knocks a turn, one as each flute passes under the thumb: a short damped thump at 140 Hz, a little louder or softer each by a hash of its number, and as loud as the flutes are deep. With flutes at 0 they are silent, and at speed 0 the wheel is still and so is everything but the kiln.
  • The kiln's breath. A slow swell of low noise under all of it.

Everything goes through tanh and fades in over the first second and a half. At the defaults the peak is about -11 dBFS and the average (RMS) about -21 dBFS.

New in Ray Kiln

Help in the code. In the playground, rest the pointer on a word in the editor and a note above it says what it means. A Metal builtin (smoothstep, reflect, atan2, M_PI_F, float3…) shows its signature and a sentence on what it does and what it is for in a shader; a field read off u (u.time, u.resolution) says what the host puts there; one of the functions the episodes share (kiln, fresnel, rot…) shows what it does, with a link to the functions list; and a term from the glossary, in the code or in a comment, shows its definition with a link to the rest, including a term of several words, such as signed distance field. It is free, for any code in the editor, and it never changes the code.

This episode leans on it. atan2, reflect, pow and smoothstep each do one thing in carve and glaze, and the note under the pointer says which; the comments name Fresnel, raymarching, twisting and solids of revolution, and each opens its glossary entry from the editor.

Try this

  • Set flutes to 0: a plain thrown pot, with only the throwing lines left to catch the light.
  • Set twist to -2 and flutes to 1: the deepest grooves, wound the other way a full turn.
  • Set gloss to 0: the glaze goes matte and mirrors nothing, and the pot looks like unfired clay with a wash on it.
  • In carve, change cos(12.0 * spiral) to cos(5.0 * spiral) for five broad flutes, or to cos(40.0 * spiral) for a fine reeded pot.
  • In profile, change pow(h, 0.85) to pow(h, 1.5): the belly moves up and the pot becomes a vase.
  • In pot, drop the cavity line's 0.2 - p.y to -p.y: the base goes too, and the pot becomes a sleeve you can see the wheel through.

Parameters

ParameterDefaultRangeWhat it does
speed0.50.0 to 2.0How fast the wheel turns and the camera circles
belly0.60.0 to 1.0How far the pot swells at its widest
neck0.50.0 to 1.0How much the pot narrows below the lip
flutes0.50.0 to 1.0How deep the flutes are carved
twist0.6-2.0 to 2.0How far the flutes wind round the pot from foot to lip
gloss0.70.0 to 1.0How glassy the glaze is: matte to mirror
heat0.50.0 to 1.0Slides the glaze from deep ember to white-hot
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-16-pottery-wheel.metal
// Ray Kiln · Episode 16 · Pottery Wheel
//
// @episode 16 Pottery Wheel
// @length 60
// @variation 30
// @variations 1,2,4,5,6,7,9,10
// @still 14.0
// @teaches Solids of revolution, twisting, a glaze material
// @category Fragment
// @tags raymarching, signed distance fields, solids of revolution, twisting, glaze, fresnel, procedural sound
// @final Pottery wheel | A fluted pot turning on the wheel, its glaze mirroring the kiln and pooling dark in the grooves, bare clay at its foot.
//
// A pot on a potter's wheel, drawn by raymarching. A pot is a solid of revolution: one curve, the radius at each
// height, spun round an axis. Flutes and throwing lines carve its surface, and a twist winds the flutes into a
// spiral as the wheel turns. The glaze is a material: it mirrors the kiln by Schlick's Fresnel, catches the light in
// a sharp highlight, and pools darker where the grooves keep it from running off.
// Each @param line below becomes a slider in the host app and a row in the page's parameters table:
//   // @param name default min max description

// @param speed 0.5 0.0 2.0 How fast the wheel turns and the camera circles
// @param belly 0.6 0.0 1.0 How far the pot swells at its widest
// @param neck 0.5 0.0 1.0 How much the pot narrows below the lip
// @param flutes 0.5 0.0 1.0 How deep the flutes are carved
// @param twist 0.6 -2.0 2.0 How far the flutes wind round the pot from foot to lip
// @param gloss 0.7 0.0 1.0 How glassy the glaze is: matte to mirror
// @param heat 0.5 0.0 1.0 Slides the glaze from deep ember to white-hot
// @param volume 0.8 0.0 1.0 Loudness of the sound

constant float TAU = 6.2831853;
constant float FLOOR = -1.0;
constant float HEIGHT = 1.5;

// Episode 3's rotation.
float2 rot(float2 p, float a) {
    float c = cos(a), s = sin(a);
    return float2(c * p.x - s * p.y, s * p.x + c * p.y);
}

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

// Episode 12's Fresnel: Schlick's approximation.
float fresnel(float cosi, float ior) {
    float f0 = (ior - 1.0) / (ior + 1.0);
    f0 *= f0;
    return f0 + (1.0 - f0) * pow(1.0 - clamp(cosi, 0.0, 1.0), 5.0);
}

// ---- The pot ---------------------------------------------------------------------------------------------------

// The profile: the pot's radius at height y, from its foot at 0 to its lip at
// HEIGHT. A foot, a belly that swells in the lower half, a neck that narrows
// under the lip, and the lip flaring out again.
float profile(float y, float belly, float neck) {
    float h = clamp(y / HEIGHT, 0.0, 1.0);
    float r = 0.34 + 0.46 * belly * sin(3.14159 * pow(h, 0.85));
    r -= 0.22 * neck * exp(-pow((h - 0.78) / 0.1, 2.0));
    r += 0.10 * smoothstep(0.88, 1.0, h);
    return r;
}

// The carving: flutes that run up the pot and throwing lines that run round
// it, as a small change to the radius. The angle each flute sits at is
// shifted by the height, so a twist winds them into a spiral.
float carve(float y, float a, float flutes, float twist) {
    float h = clamp(y / HEIGHT, 0.0, 1.0);
    float spiral = a + twist * h * TAU * 0.5;
    float ridge = 0.5 + 0.5 * cos(12.0 * spiral);
    float band = smoothstep(0.08, 0.3, h) * smoothstep(1.0, 0.8, h);
    float lines = 0.004 * sin(y * 44.0) * smoothstep(0.0, 0.1, h);
    return -0.1 * flutes * ridge * ridge * ridge * band + lines;
}

// The radius of the pot at a height and an angle: the profile, carved.
float radius(float y, float a, float4 shape) {
    return profile(y, shape.x, shape.y) + carve(y, a, shape.z, shape.w);
}

// The solid of revolution: a point's distance from the axis, less the radius
// at its height and angle. The surface leans where the profile slopes, so
// the distance is scaled down by that slope to stay safe to march.
float revolve(float3 p, float4 shape) {
    float r = length(p.xz);
    float a = atan2(p.z, p.x);
    float d = r - radius(p.y, a, shape);
    float dy = radius(p.y + 0.01, a, shape) - radius(p.y - 0.01, a, shape);
    return d / sqrt(1.0 + dy * dy * 2500.0);
}

// The pot: the revolved profile, cut to its height, hollowed from the inside
// down to a thick base so the lip is a ring and the dark inside shows.
float pot(float3 p, float4 shape) {
    float d = revolve(p, shape);
    float slab = abs(p.y - HEIGHT * 0.5) - HEIGHT * 0.5;
    float body = max(d, slab);
    float cavity = max(d + 0.08, 0.2 - p.y);
    return max(body, -cavity);
}

// The wheel: a thick disc the pot sits on, a stem down to the floor.
float wheel(float3 p) {
    float2 q = float2(length(p.xz), p.y);
    float head = max(q.x - 1.05, abs(q.y + 0.08) - 0.08);
    float stem = max(q.x - 0.18, abs(q.y + 0.58) - 0.42);
    return min(head, stem);
}

// The scene: the pot first, then the wheel, with the nearer one winning.
// The .w of the answer says which: 1 for the pot, 2 for the wheel.
float2 scene(float3 p, float t, float4 shape) {
    p.xz = rot(p.xz, -t);
    float dp = pot(p, shape);
    float dw = wheel(p);
    return dp < dw ? float2(dp, 1.0) : float2(dw, 2.0);
}

// The direction the surface faces: how the distance changes a small step
// along each axis.
float3 potNormal(float3 p, float t, float4 shape) {
    const float2 e = float2(0.002, 0.0);
    return normalize(float3(
        scene(p + e.xyy, t, shape).x - scene(p - e.xyy, t, shape).x,
        scene(p + e.yxy, t, shape).x - scene(p - e.yxy, t, shape).x,
        scene(p + e.yyx, t, shape).x - scene(p - e.yyx, t, shape).x));
}

// How closed in a point is: a few short steps out along the normal, and
// how much nearer than each step the scene is. The grooves come out dark.
float grooveShade(float3 p, float3 n, float t, float4 shape) {
    float occ = 0.0;
    for (int i = 1; i <= 4; i++) {
        float h = 0.03 * float(i);
        occ += (h - scene(p + n * h, t, shape).x) / h;
    }
    return clamp(1.0 - occ * 0.25, 0.0, 1.0);
}

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

// Where the light comes from: high, in front and to the left of the camera,
// so it goes round with it and the pot is always lit from the viewer's side.
float3 lightDir(float t) {
    float a = t * 0.18 + 0.9;
    return normalize(float3(sin(a), 1.1, cos(a)));
}

// The dark of the kiln, warmer toward the horizon, its hot mouth glowing
// where the light comes from.
float sky(float3 rd, float3 l) {
    float up = abs(rd.y);
    float mouth = pow(max(dot(rd, l), 0.0), 6.0);
    return 0.08 + 0.14 * exp(-up * 6.0) + 0.55 * mouth;
}

// The floor: tiles of fired clay with dark grout, the wheel's shadow over
// them, fading into the kiln's haze far off.
float floorLevel(float3 p, float3 l) {
    float2 g = p.xz * 1.6;
    float2 f = abs(fract(g) - 0.5);
    float grout = smoothstep(0.47, 0.44, max(f.x, f.y));
    float2 id = floor(g);
    float tone = 0.28 + 0.08 * fract(sin(dot(id, float2(12.9, 78.2))) * 437.5);
    float3 toWheel = float3(0.0, -0.08, 0.0) - p;
    float along = dot(toWheel, l);
    float miss = length(toWheel - l * along);
    float shadow = along > 0.0 ? smoothstep(0.9, 1.6, miss) : 1.0;
    return tone * grout * (0.35 + 0.65 * shadow);
}

// What a ray sees of the room: the floor if it heads down, otherwise the
// kiln's dark.
float env(float3 ro, float3 rd, float t) {
    float3 l = lightDir(t);
    float back = sky(rd, l);
    if (rd.y < -0.001) {
        float d = (FLOOR - ro.y) / rd.y;
        if (d > 0.0) {
            float3 p = ro + rd * d;
            return mix(back, floorLevel(p, l), exp(-d * 0.12));
        }
    }
    return back;
}

// ---- The glaze ---------------------------------------------------------------------------------------------------

// The glaze: a lit surface that mirrors the kiln. Bare clay at the foot;
// above it the kiln palette, dark where the glaze pools in the grooves and
// lighter over a ridge. Fresnel sets how much of the kiln is mirrored, and
// the highlight is the light itself.
float3 glaze(float3 p, float3 n, float3 rd, float occ, float t,
             float gloss, float heat) {
    float3 l = lightDir(t);
    float diffuse = max(dot(n, l), 0.0);
    float3 rr = reflect(rd, n);
    float mirror = env(p + n * 0.01, rr, t);
    float f = fresnel(dot(-rd, n), 1.0 + 1.2 * gloss);
    float spec = pow(max(dot(rr, l), 0.0), mix(6.0, 120.0, gloss))
               * mix(0.08, 2.2, gloss);
    float pool = 1.0 - occ;
    float level = (0.18 + 0.55 * diffuse) * occ + (heat - 0.5) * 0.5;
    float3 colour = kiln(level - 0.3 * pool);
    colour = mix(colour, colour * float3(0.9, 0.7, 0.6), pool);
    float3 seen = mix(colour, kiln(mirror + 0.25), f * gloss)
                + spec * kiln(0.95);
    float edge = 0.14 + 0.02 * sin(atan2(p.z, p.x) * 5.0);
    float bare = smoothstep(edge + 0.02, edge - 0.02, p.y);
    float3 clay = float3(0.42, 0.28, 0.2) * (0.25 + 0.75 * diffuse) * occ;
    return mix(seen, clay, bare);
}

// The wheel head: dark iron, its rim catching the light, grooves turning.
float3 ironShade(float3 p, float3 n, float3 rd, float t) {
    float3 l = lightDir(t);
    float diffuse = max(dot(n, l), 0.0);
    float3 rr = reflect(rd, n);
    float spec = pow(max(dot(rr, l), 0.0), 30.0) * 0.5;
    float r = length(p.xz);
    float rings = 0.85 + 0.15 * sin(r * 60.0);
    float3 base = float3(0.16, 0.14, 0.14) * rings;
    return base * (0.3 + 0.7 * diffuse) + spec * float3(0.9, 0.8, 0.7);
}

float3 cameraPos(float t) {
    float a = t * 0.18;
    return float3(3.3 * sin(a), 1.25 + 0.3 * sin(t * 0.13), 3.3 * cos(a));
}

float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
    float t = u.time * p.speed;
    float side = min(u.resolution.x, u.resolution.y);
    float2 q = (uv - 0.5) * u.resolution / side * 2.0;
    float4 shape = float4(p.belly, p.neck, p.flutes, p.twist);
    float3 ro = cameraPos(t);
    float3 fw = normalize(float3(0.0, 0.62, 0.0) - ro);
    float3 rt = normalize(cross(fw, float3(0.0, 1.0, 0.0)));
    float3 up = cross(rt, fw);
    float3 rd = normalize(q.x * rt + q.y * up + 2.3 * fw);

    // March to the scene: the carved surface is not an exact distance, so
    // each step goes 70% of the way.
    float dist = 0.0;
    float which = 0.0;
    for (int i = 0; i < 160; i++) {
        float2 s = scene(ro + rd * dist, t, shape);
        if (s.x < 0.001) { which = s.y; break; }
        dist += s.x * 0.7;
        if (dist > 14.0) break;
    }

    float3 col;
    if (which == 0.0) {
        col = kiln(env(ro, rd, t) + (p.heat - 0.5) * 0.4);
    } else {
        float3 pos = ro + rd * dist;
        float3 n = potNormal(pos, t, shape);
        float occ = grooveShade(pos, n, t, shape);
        if (which == 1.0) {
            float3 local = float3(rot(pos.xz, -t), pos.y).xzy;
            col = glaze(local, n, rd, occ, t, p.gloss, p.heat);
        } else {
            col = ironShade(pos, n, rd, t) * occ;
        }
        col = mix(col, kiln(0.08), smoothstep(6.0, 14.0, dist));
    }
    return float4(clamp(col, 0.0, 1.0), 1.0);
}

// ---- Sound -----------------------------------------------------------------------------------------------------
// The wheel at work. A low hum from the wheel's motor, its pitch following the speed; the hiss of wet hands on
// turning clay, which swells once a turn where the fingers meet the thickest part; a knock as each flute passes the
// potter's thumb, twelve to a turn, soft when the flutes are shallow; and under it all the slow breath of the kiln.

// Episode 5's integer hash.
float hashU(uint n) {
    n = (n << 13u) ^ n;
    n = n * (n * n * 15731u + 789221u) + 1376312589u;
    return float(n & 0x7fffffffu) / float(0x7fffffff);
}

// Episode 5's sound noise: random values joined `rate` times a second.
float noise1(float t, float rate, uint seed) {
    float x = t * rate;
    float i = floor(x), f = fract(x);
    f = f * f * (3.0 - 2.0 * f);
    uint k = uint(i) + seed * 7919u;
    return mix(hashU(k), hashU(k + 1u), f) * 2.0 - 1.0;
}

float2 sound(float t, constant Params& p) {
    // One turn of the wheel takes TAU / speed seconds: the picture's spin.
    float turns = t * p.speed / TAU;
    float phase = fract(turns);

    // The motor: a hum whose pitch climbs with the speed, with its octave.
    float f = 55.0 * (0.8 + 0.6 * p.speed);
    float hum = sin(TAU * f * t) + 0.4 * sin(TAU * f * 2.0 * t + 0.3);
    hum *= 0.12 * smoothstep(0.0, 0.3, p.speed);

    // The hands on the clay: hiss that swells where the belly passes.
    float hiss = noise1(t, 6000.0, 1u) * 0.5 + noise1(t, 1800.0, 2u) * 0.5;
    float swell = 0.4 + 0.6 * pow(0.5 + 0.5 * cos(TAU * phase), 2.0);
    hiss *= 0.07 * swell * (0.3 + 0.7 * p.belly)
          * smoothstep(0.0, 0.2, p.speed);

    // The flutes under the thumb: twelve soft knocks a turn, each a short
    // damped thump; deeper flutes knock louder.
    float slot = floor(turns * 12.0);
    float since = (turns * 12.0 - slot) * TAU / max(p.speed * 12.0, 0.05);
    float knock = sin(TAU * 140.0 * since) * exp(-since * 28.0)
                * (0.6 + 0.4 * hashU(uint(slot)));
    knock *= 0.35 * p.flutes * smoothstep(0.0, 0.2, p.speed);

    // The kiln's breath: a slow swell of low noise.
    float breath = noise1(t, 90.0, 3u) * 0.06 * (0.5 + 0.5 * sin(t * 0.4));

    float pan = 0.5 + 0.3 * sin(TAU * phase);
    float2 mixdown = float2(hum + breath)
                   + float2(1.0 - pan, pan) * (hiss * 2.0 + knock);
    float fadeIn = smoothstep(0.0, 1.5, t);
    return tanh(mixdown * 1.6) * 0.8 * fadeIn * p.volume;
}

Running it in your own project

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

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

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

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

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