Shader 11

Endless Terrain

Mountains that never end, flown over at dusk toward a low sun. The land is a single function, a height for every point on the ground made of fractal noise; each pixel walks a ray over it until the ray drops below, the sun lights and shadows what it finds, the sky fills the rest, and distance fades it all into a glowing haze. You can steer it, with the pointer or the arrow keys.

· 3D Scenes · Fragment shader

Teaches Heightfield raymarching, sky, sun, distance fog

Builds on 01 · Flowing Color Field (hash21, value noise), 03 · Floating 3D Scene (rotation), 05 · Cracked Clay (integer hash, sound noise), 06 · Molten Metaballs (soft shadows), 09 · Ember Tunnel (camera travel)

Watch

The idea

Episode 3 drew a scene from distance functions: shapes that say how far away they are, so a ray can jump safely toward them. A landscape is easier to describe another way. Give it a point on the ground, (x, z), and it gives back one number: how high the ground is there. That is a heightfield, and here it is fractal noise, episode 1's octaves of value noise added up.

The land is never stored. Nothing about it exists until a ray looks at a place, and then only that place is worked out, so the camera can fly forever and there is always more. Each pixel's ray walks forward, checking how far it is above the ground under it, until it goes below; that is where it meets the land. Then the same three ideas that make any outdoor picture: the sun lights the slopes that face it and casts shadows, the sky fills every ray that misses the land, and distance fog fades the far mountains into the sky's colour so they stand back in layers.

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

Stage 1 · A height for every point

terrain(x, octaves, height) is the whole landscape. It adds up octaves of value noise, each twice as fine and a bit less than half as strong as the last (a *= 0.45), and turns each one by about 37 degrees against the one before (rot(p, 0.64)), so the grid the noise is built on never lines up and shows. The total is squared: low values stay low and flat, high ones climb fast, so the land has wide valleys and steep peaks. height scales it.

marchLand finds where a ray meets it. At each step it measures gap, how far the ray's point is above the ground straight below it, and moves on by 40% of that. The gap isn't the true distance to the land (a slope can be closer to the side than below), which is why the step is only a share of it. Far from the camera the steps are allowed to grow (0.01 * d), since a small mistake there is less than a pixel. Once the point is below the ground, the last step is halved back and forth six times to find the edge closely. Here the ground is coloured by its height alone, so the valleys are dark and the peaks bright.

stage-1-heightfield.metal
// Stage 1 · A height for every point
// The land is one function: give it a point on the ground, it gives back a height. A ray
// from each pixel walks forward until it drops below that height. Here, higher is brighter.

// @param height 1.0 0.3 1.6 How tall the mountains are
// @param heat 0.5 0.0 1.0 Slides the palette from deep ember to white-hot

// Episode 1's hash and value noise.
float hash21(float2 p) {
    p = fract(p * float2(123.34, 456.21));
    p += dot(p, p + 45.32);
    return fract(p.x * p.y);
}

float vnoise(float2 p) {
    float2 i = floor(p), f = fract(p);
    f = f * f * (3.0 - 2.0 * f);
    float a = hash21(i), b = hash21(i + float2(1, 0));
    float c = hash21(i + float2(0, 1)), d = hash21(i + float2(1, 1));
    return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}

// 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 land is a height for every point (x, z) on the ground: episode 1's fractal
// noise, each octave turned a little so no grid shows. Squaring it keeps the valleys
// wide and makes the peaks climb.
float terrain(float2 x, int octaves, float height) {
    float2 p = x * 0.06;
    float v = 0.0, a = 0.5;
    for (int i = 0; i < octaves; i++) {
        v += a * vnoise(p);
        p = rot(p, 0.64) * 2.0 + float2(3.1, 7.7);
        a *= 0.45;
    }
    return v * v * 18.0 * height - 1.5;
}

// Walk the ray forward by a share of its height above the ground, with longer steps
// far away. Once below the ground, halve the last step back and forth to find the edge.
float marchLand(float3 ro, float3 rd, float height) {
    float d = 0.1;
    float prev = 0.1;
    for (int i = 0; i < 160; i++) {
        float3 p = ro + rd * d;
        float gap = p.y - terrain(p.xz, 5, height);
        if (gap < 0.002 * d) {
            float lo = prev, hi = d;
            for (int k = 0; k < 6; k++) {
                float mid = 0.5 * (lo + hi);
                float3 q = ro + rd * mid;
                bool under = q.y < terrain(q.xz, 5, height);
                if (under) hi = mid; else lo = mid;
            }
            return hi;
        }
        prev = d;
        d += max(gap * 0.4 / max(height, 1.0), 0.01 * d);
        if (d > 140.0 || p.y > 30.0) break;
    }
    return -1.0;
}

// 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) {
    // A camera four units up, looking along z and a little down.
    float side = min(u.resolution.x, u.resolution.y);
    float2 q = (uv - 0.5) * u.resolution / side * 2.0;
    float3 ro = float3(0.0, 4.0, 0.0);
    float3 fw = normalize(float3(0.0, -0.25, 1.0));
    float3 rt = normalize(cross(float3(0.0, 1.0, 0.0), fw));
    float3 up = cross(fw, rt);
    float3 rd = normalize(q.x * rt + q.y * up + 1.7 * fw);

    float d = marchLand(ro, rd, p.height);
    float level = 0.15;
    if (d > 0.0) {
        // Colour the ground by its height alone: valleys dark, peaks bright.
        float3 pos = ro + rd * d;
        level = 0.18 + 0.05 * pos.y;
    }
    return float4(kiln(level + (p.heat - 0.5) * 0.5), 1.0);
}

Stage 2 · Sunlight

A heightfield's normal comes from its slope: landNormal measures the height a small step along x and a small step along z, and the differences tilt the straight-up normal toward the downhill side. It uses eight octaves where the march used five, so the light finds small bumps the march doesn't need. The step grows with distance, so far slopes stay smooth instead of shimmering.

The sun is a direction, low ahead and to the right, raised by sun. A face is lit by how squarely it faces the sun, dot(n, l), times softShadow: a second walk, from the point toward the sun, that keeps the smallest 10 * gap / d it sees. A ray that passes close above a ridge comes out partly dark, so shadow edges are soft, wider the further the ridge is. Light from the sky above, more on flat ground than on cliffs (0.5 + 0.5 * n.y), keeps the shadows from going black.

stage-2-sunlight.metal27 new lines since stage 1
// Stage 2 · Sunlight
// Light the land by the sun: the slope gives each point a normal, a face turned to the sun
// is bright, and a ray walked toward the sun finds the shadows of the ridges in the way.

// @param height 1.0 0.3 1.6 How tall the mountains are
// @param sun 0.35 0.0 1.0 How high the sun stands, from the horizon to the afternoon
// @param heat 0.5 0.0 1.0 Slides the palette from deep ember to white-hot

// Episode 1's hash and value noise.
float hash21(float2 p) {
    p = fract(p * float2(123.34, 456.21));
    p += dot(p, p + 45.32);
    return fract(p.x * p.y);
}

float vnoise(float2 p) {
    float2 i = floor(p), f = fract(p);
    f = f * f * (3.0 - 2.0 * f);
    float a = hash21(i), b = hash21(i + float2(1, 0));
    float c = hash21(i + float2(0, 1)), d = hash21(i + float2(1, 1));
    return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}

// 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 land is a height for every point (x, z) on the ground: episode 1's fractal
// noise, each octave turned a little so no grid shows. Squaring it keeps the valleys
// wide and makes the peaks climb.
float terrain(float2 x, int octaves, float height) {
    float2 p = x * 0.06;
    float v = 0.0, a = 0.5;
    for (int i = 0; i < octaves; i++) {
        v += a * vnoise(p);
        p = rot(p, 0.64) * 2.0 + float2(3.1, 7.7);
        a *= 0.45;
    }
    return v * v * 18.0 * height - 1.5;
}

// Walk the ray forward by a share of its height above the ground, with longer steps
// far away. Once below the ground, halve the last step back and forth to find the edge.
float marchLand(float3 ro, float3 rd, float height) {
    float d = 0.1;
    float prev = 0.1;
    for (int i = 0; i < 160; i++) {
        float3 p = ro + rd * d;
        float gap = p.y - terrain(p.xz, 5, height);
        if (gap < 0.002 * d) {
            float lo = prev, hi = d;
            for (int k = 0; k < 6; k++) {
                float mid = 0.5 * (lo + hi);
                float3 q = ro + rd * mid;
                bool under = q.y < terrain(q.xz, 5, height);
                if (under) hi = mid; else lo = mid;
            }
            return hi;
        }
        prev = d;
        d += max(gap * 0.4 / max(height, 1.0), 0.01 * d);
        if (d > 140.0 || p.y > 30.0) break;
    }
    return -1.0;
}

// The normal from the slope: how the height changes a small step along x and along z.
float3 landNormal(float2 x, float e, float height) {
    float h = terrain(x, 8, height);
    float hx = terrain(x + float2(e, 0.0), 8, height);
    float hz = terrain(x + float2(0.0, e), 8, height);
    return normalize(float3(h - hx, e, h - hz));
}

// A soft shadow: walk toward the sun; the closer the ray passes to the land, the darker.
float softShadow(float3 p, float3 l, float height) {
    float lit = 1.0;
    float d = 0.3;
    for (int i = 0; i < 32; i++) {
        float3 q = p + l * d;
        float gap = q.y - terrain(q.xz, 4, height);
        lit = min(lit, 10.0 * gap / d);
        if (lit < 0.0 || q.y > 25.0) break;
        d += clamp(gap, 0.3, 4.0);
    }
    return smoothstep(0.0, 1.0, lit);
}

// The sun: low ahead and to the right; `sun` raises it.
float3 sunDir(float sun) {
    return normalize(float3(0.6, 0.04 + 0.6 * sun, 1.0));
}

// 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) {
    // A camera four units up, looking along z and a little down.
    float side = min(u.resolution.x, u.resolution.y);
    float2 q = (uv - 0.5) * u.resolution / side * 2.0;
    float3 ro = float3(0.0, 4.0, 0.0);
    float3 fw = normalize(float3(0.0, -0.25, 1.0));
    float3 rt = normalize(cross(float3(0.0, 1.0, 0.0), fw));
    float3 up = cross(fw, rt);
    float3 rd = normalize(q.x * rt + q.y * up + 1.7 * fw);

    float3 l = sunDir(p.sun);
    float d = marchLand(ro, rd, p.height);
    float level = 0.15;
    if (d > 0.0) {
        float3 pos = ro + rd * d;
        float3 n = landNormal(pos.xz, 0.002 + 0.0015 * d, p.height);
        float shadow = softShadow(pos + n * 0.05, l, p.height);
        float sunLight = max(dot(n, l), 0.0) * shadow;
        // Some light from the sky above too, more on flat ground, so shadows aren't black.
        float skyLight = 0.5 + 0.5 * n.y;
        level = 0.75 * sunLight + 0.2 * skyLight;
    }
    return float4(kiln(level + (p.heat - 0.5) * 0.5), 1.0);
}
1 line from stage 1 that are gone
        level = 0.18 + 0.05 * pos.y;

Stage 3 · Sky, sun and haze

skyLevel is the light of a ray that misses the land: dim overhead, brighter toward the horizon (exp(-rd.y * 5)), brighter again near the sun (pow(toward, 8) and pow(toward, 64)), and the sun's disc where the ray points within about a degree of it.

The fog is episode 9's: the light from the land is mixed toward the haze's colour by exp(-distance * density), so the further a mountain is, the more of it is haze. Here the haze is brighter toward the sun, as real haze is when the light comes through it. Ridge behind ridge, each one paler, is what makes the land look deep. fog sets the density.

stage-3-sky.metal14 new lines since stage 2
// Stage 3 · Sky, sun and haze
// Where a ray meets no land, it sees the sky, bright at the horizon and round the sun. Far
// land fades into a haze, so the mountains go back in layers.

// @param height 1.0 0.3 1.6 How tall the mountains are
// @param sun 0.35 0.0 1.0 How high the sun stands, from the horizon to the afternoon
// @param fog 0.5 0.0 1.0 How thick the haze of distance is
// @param heat 0.5 0.0 1.0 Slides the palette from deep ember to white-hot

// Episode 1's hash and value noise.
float hash21(float2 p) {
    p = fract(p * float2(123.34, 456.21));
    p += dot(p, p + 45.32);
    return fract(p.x * p.y);
}

float vnoise(float2 p) {
    float2 i = floor(p), f = fract(p);
    f = f * f * (3.0 - 2.0 * f);
    float a = hash21(i), b = hash21(i + float2(1, 0));
    float c = hash21(i + float2(0, 1)), d = hash21(i + float2(1, 1));
    return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}

// 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 land is a height for every point (x, z) on the ground: episode 1's fractal
// noise, each octave turned a little so no grid shows. Squaring it keeps the valleys
// wide and makes the peaks climb.
float terrain(float2 x, int octaves, float height) {
    float2 p = x * 0.06;
    float v = 0.0, a = 0.5;
    for (int i = 0; i < octaves; i++) {
        v += a * vnoise(p);
        p = rot(p, 0.64) * 2.0 + float2(3.1, 7.7);
        a *= 0.45;
    }
    return v * v * 18.0 * height - 1.5;
}

// Walk the ray forward by a share of its height above the ground, with longer steps
// far away. Once below the ground, halve the last step back and forth to find the edge.
float marchLand(float3 ro, float3 rd, float height) {
    float d = 0.1;
    float prev = 0.1;
    for (int i = 0; i < 160; i++) {
        float3 p = ro + rd * d;
        float gap = p.y - terrain(p.xz, 5, height);
        if (gap < 0.002 * d) {
            float lo = prev, hi = d;
            for (int k = 0; k < 6; k++) {
                float mid = 0.5 * (lo + hi);
                float3 q = ro + rd * mid;
                bool under = q.y < terrain(q.xz, 5, height);
                if (under) hi = mid; else lo = mid;
            }
            return hi;
        }
        prev = d;
        d += max(gap * 0.4 / max(height, 1.0), 0.01 * d);
        if (d > 140.0 || p.y > 30.0) break;
    }
    return -1.0;
}

// The normal from the slope: how the height changes a small step along x and along z.
float3 landNormal(float2 x, float e, float height) {
    float h = terrain(x, 8, height);
    float hx = terrain(x + float2(e, 0.0), 8, height);
    float hz = terrain(x + float2(0.0, e), 8, height);
    return normalize(float3(h - hx, e, h - hz));
}

// A soft shadow: walk toward the sun; the closer the ray passes to the land, the darker.
float softShadow(float3 p, float3 l, float height) {
    float lit = 1.0;
    float d = 0.3;
    for (int i = 0; i < 32; i++) {
        float3 q = p + l * d;
        float gap = q.y - terrain(q.xz, 4, height);
        lit = min(lit, 10.0 * gap / d);
        if (lit < 0.0 || q.y > 25.0) break;
        d += clamp(gap, 0.3, 4.0);
    }
    return smoothstep(0.0, 1.0, lit);
}

// The sun: low ahead and to the right; `sun` raises it.
float3 sunDir(float sun) {
    return normalize(float3(0.6, 0.04 + 0.6 * sun, 1.0));
}

// 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 sky: dark overhead, bright at the horizon, brightest round the sun, and its disc.
float skyLevel(float3 rd, float3 l) {
    float toward = max(dot(rd, l), 0.0);
    float horizon = exp(-max(rd.y, 0.0) * 5.0);
    float level = 0.12 + 0.3 * horizon + 0.35 * pow(toward, 8.0) * horizon;
    level += 0.25 * pow(toward, 64.0);
    level += smoothstep(0.9996, 0.9998, toward);
    return level;
}

float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
    // A camera four units up, looking along z and a little down.
    float side = min(u.resolution.x, u.resolution.y);
    float2 q = (uv - 0.5) * u.resolution / side * 2.0;
    float3 ro = float3(0.0, 4.0, 0.0);
    float3 fw = normalize(float3(0.0, -0.25, 1.0));
    float3 rt = normalize(cross(float3(0.0, 1.0, 0.0), fw));
    float3 up = cross(fw, rt);
    float3 rd = normalize(q.x * rt + q.y * up + 1.7 * fw);

    float3 l = sunDir(p.sun);
    float sky = skyLevel(rd, l);
    float d = marchLand(ro, rd, p.height);
    float level = sky;
    if (d > 0.0) {
        float3 pos = ro + rd * d;
        float3 n = landNormal(pos.xz, 0.002 + 0.0015 * d, p.height);
        float shadow = softShadow(pos + n * 0.05, l, p.height);
        float sunLight = max(dot(n, l), 0.0) * shadow;
        float skyLight = 0.5 + 0.5 * n.y;
        level = 0.75 * sunLight + 0.2 * skyLight;
        // Distance fog: the further the land, the more the haze replaces it, by
        // exp(-distance). Toward the sun the haze is lit and glows.
        float density = 0.006 + 0.03 * p.fog;
        float through = exp(-d * density);
        float haze = 0.25 + 0.25 * pow(max(dot(rd, l), 0.0), 6.0);
        level = mix(haze, level, through);
    }
    return float4(kiln(level + (p.heat - 0.5) * 0.5), 1.0);
}
1 line from stage 2 that are gone
    float level = 0.15;

Stage 4 · Flying

Now the camera flies: forward along z, eight units a second at speed 1, weaving across on two slow sine waves (flightX). It must not fly into a mountain, and a shader has no memory to steer with, so flightY looks at the ground a little ahead and to each side of the path, takes the highest point, and stays 2.6 units above it; the camera rises before a ridge reaches it. It looks along the path six units ahead and a little down, and banks into its turns by how far the path swings in the next eight units.

stage-4-flying.metal24 new lines since stage 3
// Stage 4 · Flying
// Fly: the camera moves along z forever, over land that is never stored, only worked out
// where a ray looks. It weaves across, keeps clear of the ground and banks into its turns.

// @param speed 0.5 0.0 2.0 How fast the camera flies over the land
// @param height 1.0 0.3 1.6 How tall the mountains are
// @param sun 0.35 0.0 1.0 How high the sun stands, from the horizon to the afternoon
// @param fog 0.5 0.0 1.0 How thick the haze of distance is
// @param heat 0.5 0.0 1.0 Slides the palette from deep ember to white-hot

// Episode 1's hash and value noise.
float hash21(float2 p) {
    p = fract(p * float2(123.34, 456.21));
    p += dot(p, p + 45.32);
    return fract(p.x * p.y);
}

float vnoise(float2 p) {
    float2 i = floor(p), f = fract(p);
    f = f * f * (3.0 - 2.0 * f);
    float a = hash21(i), b = hash21(i + float2(1, 0));
    float c = hash21(i + float2(0, 1)), d = hash21(i + float2(1, 1));
    return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}

// 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 land is a height for every point (x, z) on the ground: episode 1's fractal
// noise, each octave turned a little so no grid shows. Squaring it keeps the valleys
// wide and makes the peaks climb.
float terrain(float2 x, int octaves, float height) {
    float2 p = x * 0.06;
    float v = 0.0, a = 0.5;
    for (int i = 0; i < octaves; i++) {
        v += a * vnoise(p);
        p = rot(p, 0.64) * 2.0 + float2(3.1, 7.7);
        a *= 0.45;
    }
    return v * v * 18.0 * height - 1.5;
}

// Walk the ray forward by a share of its height above the ground, with longer steps
// far away. Once below the ground, halve the last step back and forth to find the edge.
float marchLand(float3 ro, float3 rd, float height) {
    float d = 0.1;
    float prev = 0.1;
    for (int i = 0; i < 160; i++) {
        float3 p = ro + rd * d;
        float gap = p.y - terrain(p.xz, 5, height);
        if (gap < 0.002 * d) {
            float lo = prev, hi = d;
            for (int k = 0; k < 6; k++) {
                float mid = 0.5 * (lo + hi);
                float3 q = ro + rd * mid;
                bool under = q.y < terrain(q.xz, 5, height);
                if (under) hi = mid; else lo = mid;
            }
            return hi;
        }
        prev = d;
        d += max(gap * 0.4 / max(height, 1.0), 0.01 * d);
        if (d > 140.0 || p.y > 30.0) break;
    }
    return -1.0;
}

// The normal from the slope: how the height changes a small step along x and along z.
float3 landNormal(float2 x, float e, float height) {
    float h = terrain(x, 8, height);
    float hx = terrain(x + float2(e, 0.0), 8, height);
    float hz = terrain(x + float2(0.0, e), 8, height);
    return normalize(float3(h - hx, e, h - hz));
}

// A soft shadow: walk toward the sun; the closer the ray passes to the land, the darker.
float softShadow(float3 p, float3 l, float height) {
    float lit = 1.0;
    float d = 0.3;
    for (int i = 0; i < 32; i++) {
        float3 q = p + l * d;
        float gap = q.y - terrain(q.xz, 4, height);
        lit = min(lit, 10.0 * gap / d);
        if (lit < 0.0 || q.y > 25.0) break;
        d += clamp(gap, 0.3, 4.0);
    }
    return smoothstep(0.0, 1.0, lit);
}

// The sun: low ahead and to the right; `sun` raises it.
float3 sunDir(float sun) {
    return normalize(float3(0.6, 0.04 + 0.6 * sun, 1.0));
}

// The camera flies along z, wandering across on two slow waves.
float flightX(float z) {
    return 9.0 * sin(z * 0.021) + 4.0 * sin(z * 0.047 + 1.3);
}

// It holds its height above the highest ground a little ahead and to each side, so it
// lifts before a ridge instead of flying into it.
float flightY(float z, float height) {
    float top = -10.0;
    for (int i = 0; i < 4; i++) {
        float zz = z + float(i) * 3.0;
        float x = flightX(zz);
        top = max(top, terrain(float2(x, zz), 5, height));
        top = max(top, terrain(float2(x - 2.5, zz), 5, height) - 1.0);
        top = max(top, terrain(float2(x + 2.5, zz), 5, height) - 1.0);
    }
    return max(top, 0.0) + 2.6;
}

float travel(float t) {
    return t * 8.0;
}

// 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 sky: dark overhead, bright at the horizon, brightest round the sun, and its disc.
float skyLevel(float3 rd, float3 l) {
    float toward = max(dot(rd, l), 0.0);
    float horizon = exp(-max(rd.y, 0.0) * 5.0);
    float level = 0.12 + 0.3 * horizon + 0.35 * pow(toward, 8.0) * horizon;
    level += 0.25 * pow(toward, 64.0);
    level += smoothstep(0.9996, 0.9998, toward);
    return level;
}

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;

    // Ride the flight path and look along it, a little ahead and down.
    float z = travel(t);
    float3 ro = float3(flightX(z), flightY(z, p.height), z);
    float ahead = z + 6.0;
    float3 fw = normalize(float3(flightX(ahead), ro.y - 1.2, ahead) - ro);
    // Bank into the turns, as a plane does.
    float bank = (flightX(z + 8.0) - flightX(z)) * 0.04;
    float3 rt = normalize(cross(float3(sin(bank), cos(bank), 0.0), fw));
    float3 up = cross(fw, rt);
    float3 rd = normalize(q.x * rt + q.y * up + 1.7 * fw);

    float3 l = sunDir(p.sun);
    float sky = skyLevel(rd, l);
    float d = marchLand(ro, rd, p.height);
    float level = sky;
    if (d > 0.0) {
        float3 pos = ro + rd * d;
        float3 n = landNormal(pos.xz, 0.002 + 0.0015 * d, p.height);
        float shadow = softShadow(pos + n * 0.05, l, p.height);
        float sunLight = max(dot(n, l), 0.0) * shadow;
        float skyLight = 0.5 + 0.5 * n.y;
        level = 0.75 * sunLight + 0.2 * skyLight;
        // Distance fog: the further the land, the more the haze replaces it, by
        // exp(-distance). Toward the sun the haze is lit and glows.
        float density = 0.006 + 0.03 * p.fog;
        float through = exp(-d * density);
        float haze = 0.25 + 0.25 * pow(max(dot(rd, l), 0.0), 6.0);
        level = mix(haze, level, through);
    }
    return float4(kiln(level + (p.heat - 0.5) * 0.5), 1.0);
}
3 lines from stage 3 that are gone
    float3 ro = float3(0.0, 4.0, 0.0);
    float3 fw = normalize(float3(0.0, -0.25, 1.0));
    float3 rt = normalize(cross(float3(0.0, 1.0, 0.0), fw));

Stage 5 · Endless terrain

The last step makes the land sharper and gives you the controls.

  • Ridges. Each of the first four octaves is folded into a ridge, 1 - |2n - 1|, squared: the noise's middle values become crests and its high and low ones become valleys. rough mixes between plain noise and ridges; the finer octaves are ridged only a little, or every pebble would be a crest.
  • Rock. Flat tops, where dust would settle, are a little lighter than steep faces, with a touch of noise so no face is one flat colour. Light bounced from the sunlit land lights the faces turned away from the sun.
  • Steering. The camera reads the pointer, u.mouse, as an offset from the middle of the picture. Across turns the view up to about 63 degrees each way, and banks with it; above the middle the camera climbs up to twelve units and tips down to look at the land; below it the view lifts toward the sky. With the pointer in the middle the flight is the one in the videos, since every export puts the pointer there.
the kiln palette and shade(), from the finished shader
float3 kiln(float t) {
    float3 c0 = float3(0.020, 0.012, 0.020);
    float3 c1 = float3(0.280, 0.040, 0.030);
    float3 c2 = float3(0.880, 0.260, 0.050);
    float3 c3 = float3(1.000, 0.680, 0.200);
    float3 c4 = float3(1.000, 0.970, 0.840);
    t = clamp(t, 0.0, 1.0);
    float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
    c = mix(c, c2, smoothstep(0.22, 0.50, t));
    c = mix(c, c3, smoothstep(0.50, 0.76, t));
    return mix(c, c4, smoothstep(0.76, 1.00, t));
}

float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
    float t = u.time * p.speed;
    float side = min(u.resolution.x, u.resolution.y);
    float2 q = (uv - 0.5) * u.resolution / side * 2.0;

    // Steering: the pointer's offset from the middle, zero there, as in every video. Across
    // turns the view; up climbs and looks down at the land; down looks up at the sky.
    float2 steer = u.mouse / u.resolution - 0.5;

    float z = travel(t);
    float3 ro = float3(flightX(z), flightY(z, p.height, p.rough), z);
    float climb = max(steer.y, 0.0);
    ro.y += climb * 24.0;
    // Look along the flight path a little ahead, turned by the pointer.
    float ahead = z + 6.0;
    float tip = 1.2 + climb * 5.0 + min(steer.y, 0.0) * 6.0;
    float3 target = float3(flightX(ahead), ro.y - tip, ahead);
    float3 fw = normalize(target - ro);
    float yaw = steer.x * 2.2;
    fw.xz = rot(fw.xz, -yaw);
    // Bank into the turns of the path, as a plane does.
    float bank = (flightX(z + 8.0) - flightX(z)) * 0.04 - steer.x * 0.25;
    float3 rt = normalize(cross(float3(sin(bank), cos(bank), 0.0), fw));
    float3 up = cross(fw, rt);
    float3 rd = normalize(q.x * rt + q.y * up + 1.7 * fw);

    float3 l = sunDir(p.sun);
    float sky = skyLevel(rd, l);
    float d = marchLand(ro, rd, p.height, p.rough);

    float level = sky;
    if (d > 0.0) {
        float3 pos = ro + rd * d;
        // The normal's step grows with distance, so far slopes stay smooth instead of shimmering.
        float3 n = landNormal(pos.xz, 0.002 + 0.0015 * d, p.height, p.rough);
        float shadow = softShadow(pos + n * 0.05, l, p.height, p.rough);
        float sunLight = max(dot(n, l), 0.0) * shadow;
        // Light from the sky above, more on flat ground; light bounced back from the sunlit land facing away.
        float skyLight = 0.5 + 0.5 * n.y;
        float bounce = max(dot(n, normalize(float3(-l.x, 0.0, -l.z))), 0.0);
        // Rock is darker than the flat tops where dust settles; steep faces are bare.
        float flat = smoothstep(0.6, 0.9, n.y);
        float rock = 0.7 + 0.3 * flat + 0.2 * (vnoise(pos.xz * 0.8) - 0.5);
        level = rock * (0.75 * sunLight + 0.2 * skyLight + 0.1 * bounce);

        // Distance fog: the further the land, the more of the sky's own light along that ray replaces it, by
        // exp(-distance). Toward the sun the haze is lit and glows.
        float density = 0.006 + 0.03 * p.fog;
        float through = exp(-d * density);
        float haze = 0.25 + 0.25 * pow(max(dot(rd, l), 0.0), 6.0);
        level = mix(haze, level, through);
    }

    return float4(kiln(level + (p.heat - 0.5) * 0.5), 1.0);
}
episode-11-endless-terrain.metal, the whole file109 new lines since stage 4
// Ray Kiln · Episode 11 · Endless Terrain
//
// @episode 11 Endless Terrain
// @length 60
// @variation 30
// @variations 1,2,3,4,5,7,10,12
// @still 24.0
// @teaches Heightfield raymarching, sky, sun, distance fog
// @category Fragment
// @tags raymarching, heightfield, terrain, fractal noise, value noise, fog, sky, soft shadows, procedural sound
// @final Endless terrain | Fly low over the ridges toward the setting sun, and steer with the pointer: across to look aside, up to climb.
//
// Mountains that go on forever, flown over at dusk. The land is one function: a height for every point on the
// ground, made of fractal noise. Each pixel sends a ray out and walks it forward until it drops below the ground;
// the sun lights what it finds, the sky fills the rest, and the haze of distance joins the two.
// 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 camera flies over the land
// @param height 1.0 0.3 1.6 How tall the mountains are
// @param rough 0.5 0.0 0.8 How rough the land is: soft hills, or sharp ridges
// @param sun 0.35 0.0 1.0 How high the sun stands, from the horizon to the afternoon
// @param fog 0.5 0.0 1.0 How thick the haze of distance is
// @param heat 0.5 0.0 1.0 Slides the palette from deep ember to white-hot
// @param volume 0.8 0.0 1.0 Loudness of the sound

constant float TAU = 6.2831853;
constant float FAR = 140.0;

// Episode 1's hash and value noise.
float hash21(float2 p) {
    p = fract(p * float2(123.34, 456.21));
    p += dot(p, p + 45.32);
    return fract(p.x * p.y);
}

float vnoise(float2 p) {
    float2 i = floor(p), f = fract(p);
    f = f * f * (3.0 - 2.0 * f);
    float a = hash21(i), b = hash21(i + float2(1, 0));
    float c = hash21(i + float2(0, 1)), d = hash21(i + float2(1, 1));
    return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}

// 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 land --------------------------------------------------------------------------------------------------
// A heightfield: one height for every point (x, z) on the ground. It is episode 1's fractal noise, with two changes.
// Each octave is folded into a ridge, 1 - |2n - 1|, as far as `rough` asks, so peaks come to a sharp crest. And
// each octave is turned a little against the one before, so no grid lines show. Fewer octaves give the broad shape,
// for marching; more give the detail, for lighting.

float terrain(float2 x, int octaves, float height, float rough) {
    float2 p = x * 0.06;
    float v = 0.0, a = 0.5;
    for (int i = 0; i < octaves; i++) {
        float n = vnoise(p);
        float ridge = 1.0 - abs(2.0 * n - 1.0);
        // The fine octaves are only a little ridged, or every small bump would be a crest.
        v += a * mix(n, ridge * ridge, i < 4 ? rough : rough * 0.3);
        p = rot(p, 0.64) * 2.0 + float2(3.1, 7.7);
        a *= 0.45;
    }
    // Low ground stays flat and wide, high ground climbs fast: the valleys and the peaks.
    return v * v * 18.0 * height - 1.5;
}

// The normal from the land's slope: how much the height changes a small step along x and along z.
float3 landNormal(float2 x, float e, float height, float rough) {
    float h = terrain(x, 8, height, rough);
    float hx = terrain(x + float2(e, 0.0), 8, height, rough);
    float hz = terrain(x + float2(0.0, e), 8, height, rough);
    return normalize(float3(h - hx, e, h - hz));
}

// Marching the heightfield: step forward by a share of the height above the ground, longer
// steps far away, then halve the last step back and forth to find the edge.
float marchLand(float3 ro, float3 rd, float height, float rough) {
    float d = 0.1;
    float prev = 0.1;
    for (int i = 0; i < 160; i++) {
        float3 p = ro + rd * d;
        float gap = p.y - terrain(p.xz, 5, height, rough);
        if (gap < 0.002 * d) {
            float lo = prev, hi = d;
            for (int k = 0; k < 6; k++) {
                float mid = 0.5 * (lo + hi);
                float3 q = ro + rd * mid;
                bool under = q.y < terrain(q.xz, 5, height, rough);
                if (under) hi = mid; else lo = mid;
            }
            return hi;
        }
        prev = d;
        d += max(gap * 0.4 / max(height, 1.0), 0.01 * d);
        if (d > FAR || p.y > 30.0) break;
    }
    return -1.0;
}

// A soft shadow: walk toward the sun; the closer the ray passes to the land, the darker.
float softShadow(float3 p, float3 l, float height, float rough) {
    float lit = 1.0;
    float d = 0.3;
    for (int i = 0; i < 32; i++) {
        float3 q = p + l * d;
        float gap = q.y - terrain(q.xz, 4, height, rough);
        lit = min(lit, 10.0 * gap / d);
        if (lit < 0.0 || q.y > 25.0) break;
        d += clamp(gap, 0.3, 4.0);
    }
    return smoothstep(0.0, 1.0, lit);
}

// ---- The flight ------------------------------------------------------------------------------------------------
// The camera flies along z, wandering across on two slow waves. It holds its height above the highest ground a little
// ahead and to each side, so it lifts before a ridge instead of flying into it.

float flightX(float z) {
    return 9.0 * sin(z * 0.021) + 4.0 * sin(z * 0.047 + 1.3);
}

float flightY(float z, float height, float rough) {
    float top = -10.0;
    for (int i = 0; i < 4; i++) {
        float zz = z + float(i) * 3.0;
        float x = flightX(zz);
        top = max(top, terrain(float2(x, zz), 5, height, rough));
        top = max(top, terrain(float2(x - 2.5, zz), 5, height, rough) - 1.0);
        top = max(top, terrain(float2(x + 2.5, zz), 5, height, rough) - 1.0);
    }
    return max(top, 0.0) + 2.6;
}

float travel(float t) {
    return t * 8.0;
}

// The sun: low ahead and to the right; `sun` raises it.
float3 sunDir(float sun) {
    return normalize(float3(0.6, 0.04 + 0.6 * sun, 1.0));
}

// ---- 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 sky's brightness along a ray: dark overhead, bright at the horizon, brightest round the sun, with its disc.
float skyLevel(float3 rd, float3 l) {
    float toward = max(dot(rd, l), 0.0);
    float horizon = exp(-max(rd.y, 0.0) * 5.0);
    float level = 0.12 + 0.3 * horizon + 0.35 * pow(toward, 8.0) * horizon;
    level += 0.25 * pow(toward, 64.0);
    level += smoothstep(0.9996, 0.9998, toward);
    return level;
}

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;

    // Steering: the pointer's offset from the middle, zero there, as in every video. Across
    // turns the view; up climbs and looks down at the land; down looks up at the sky.
    float2 steer = u.mouse / u.resolution - 0.5;

    float z = travel(t);
    float3 ro = float3(flightX(z), flightY(z, p.height, p.rough), z);
    float climb = max(steer.y, 0.0);
    ro.y += climb * 24.0;
    // Look along the flight path a little ahead, turned by the pointer.
    float ahead = z + 6.0;
    float tip = 1.2 + climb * 5.0 + min(steer.y, 0.0) * 6.0;
    float3 target = float3(flightX(ahead), ro.y - tip, ahead);
    float3 fw = normalize(target - ro);
    float yaw = steer.x * 2.2;
    fw.xz = rot(fw.xz, -yaw);
    // Bank into the turns of the path, as a plane does.
    float bank = (flightX(z + 8.0) - flightX(z)) * 0.04 - steer.x * 0.25;
    float3 rt = normalize(cross(float3(sin(bank), cos(bank), 0.0), fw));
    float3 up = cross(fw, rt);
    float3 rd = normalize(q.x * rt + q.y * up + 1.7 * fw);

    float3 l = sunDir(p.sun);
    float sky = skyLevel(rd, l);
    float d = marchLand(ro, rd, p.height, p.rough);

    float level = sky;
    if (d > 0.0) {
        float3 pos = ro + rd * d;
        // The normal's step grows with distance, so far slopes stay smooth instead of shimmering.
        float3 n = landNormal(pos.xz, 0.002 + 0.0015 * d, p.height, p.rough);
        float shadow = softShadow(pos + n * 0.05, l, p.height, p.rough);
        float sunLight = max(dot(n, l), 0.0) * shadow;
        // Light from the sky above, more on flat ground; light bounced back from the sunlit land facing away.
        float skyLight = 0.5 + 0.5 * n.y;
        float bounce = max(dot(n, normalize(float3(-l.x, 0.0, -l.z))), 0.0);
        // Rock is darker than the flat tops where dust settles; steep faces are bare.
        float flat = smoothstep(0.6, 0.9, n.y);
        float rock = 0.7 + 0.3 * flat + 0.2 * (vnoise(pos.xz * 0.8) - 0.5);
        level = rock * (0.75 * sunLight + 0.2 * skyLight + 0.1 * bounce);

        // Distance fog: the further the land, the more of the sky's own light along that ray replaces it, by
        // exp(-distance). Toward the sun the haze is lit and glows.
        float density = 0.006 + 0.03 * p.fog;
        float through = exp(-d * density);
        float haze = 0.25 + 0.25 * pow(max(dot(rd, l), 0.0), 6.0);
        level = mix(haze, level, through);
    }

    return float4(kiln(level + (p.heat - 0.5) * 0.5), 1.0);
}

// ---- Sound -----------------------------------------------------------------------------------------------------
// Wind over the land: a rush of air that swells as the ground rises close under the camera and thins as it falls
// away. Under it a low chord holds, and it opens up, brighter, the higher the sun stands. Each time the camera
// crosses a crest, a soft bell rings, its note from how high that crest is.

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

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

// How close the ground is under the flight at z: 1 skimming it, falling toward 0 as it drops away.
float closeness(float z, float height, float rough) {
    float ground = terrain(float2(flightX(z), z), 4, height, rough);
    float clearance = flightY(z, height, rough) - ground;
    return exp(-max(clearance - 2.6, 0.0) * 0.35);
}

float2 sound(float t, constant Params& p) {
    float ts = t * p.speed;
    float z = travel(ts);

    // The wind: two bands of noise in each ear, a dark one always there and a brighter one that comes with the
    // ground. Faster flight, more wind.
    float near = closeness(z, p.height, p.rough);
    float gust = 0.6 + 0.4 * noise1(t, 0.4, 9u);
    float2 low = float2(noise1(t, 180.0, 1u), noise1(t, 180.0, 2u));
    float2 high = float2(noise1(t, 900.0, 3u), noise1(t, 900.0, 4u));
    float2 wind = (low * 0.6 + high * near * 0.8) * gust;
    wind *= min(0.3 + p.speed, 1.2);

    // The chord: A2, E3 and C4. Its upper partials grow with the sun.
    float bright = 0.15 + 0.6 * p.sun;
    float2 chord = float2(0.0);
    for (int i = 0; i < 3; i++) {
        float f = i == 0 ? 110.0 : i == 1 ? 164.81 : 261.63;
        // Each ear a few cents apart, so the chord beats slowly.
        float l = sin(TAU * f * t) + bright * 0.5 * sin(TAU * 2.0 * f * t)
                + bright * 0.25 * sin(TAU * 3.0 * f * t);
        float g = f * 1.003;
        float r = sin(TAU * g * t + 1.0)
                + bright * 0.5 * sin(TAU * 2.0 * g * t + 0.4);
        float swell = 0.75 + 0.25 * sin(t * 0.3 + float(i) * 2.1);
        chord += float2(l, r) * swell;
    }
    chord *= 0.05;

    // The bells: every 6 units of flight is a stretch of land; its bell rings as the camera passes its middle, on a
    // note of A minor pentatonic picked by the height of the ground there.
    float2 bells = float2(0.0);
    float slot = floor(z / 6.0);
    for (int k = -2; k <= 0; k++) {
        float j = slot + float(k);
        float at = (j + 0.5) * 6.0;
        float since = (z - at) / (8.0 * max(p.speed, 0.05));
        if (since < 0.0) continue;
        float g = terrain(float2(flightX(at), at), 3, p.height, p.rough);
        float rank = clamp(floor(g / (2.0 * p.height) + 2.0), 0.0, 6.0);
        float semis = rank < 1.0 ? 0.0 : rank < 2.0 ? 3.0 : rank < 3.0 ? 5.0
                    : rank < 4.0 ? 7.0 : rank < 5.0 ? 10.0
                    : rank < 6.0 ? 12.0 : 15.0;
        float f = 440.0 * exp2(semis / 12.0);
        uint s = uint(int(j) + 100000);
        float pick = hashU(s);
        // Only some stretches ring, the higher ground more often: fixed for each stretch.
        float rings = step(0.55 - 0.06 * rank, pick);
        float env = since * exp(-since * 2.2) * 5.0 * rings;
        env *= 1.0 - exp(-since * 400.0);
        // Gone smoothly before the loop lets go of it.
        env *= 1.0 - smoothstep(9.0, 15.0, z - at);
        float tone = sin(TAU * f * since)
                   + 0.3 * sin(TAU * 2.76 * f * since) * exp(-since * 4.0);
        float pan = hashU(s + 7u);
        bells += float2(1.0 - pan, pan) * tone * env;
    }
    bells *= 0.12 * smoothstep(0.0, 0.1, p.speed);

    float fadeIn = smoothstep(0.0, 2.0, t);
    return tanh((wind * 0.35 + chord + bells) * 1.6) * 0.9 * fadeIn * p.volume;
}
28 lines from stage 4 that are gone
float terrain(float2 x, int octaves, float height) {
⋯
        v += a * vnoise(p);
⋯
float marchLand(float3 ro, float3 rd, float height) {
⋯
        float gap = p.y - terrain(p.xz, 5, height);
⋯
                bool under = q.y < terrain(q.xz, 5, height);
⋯
        if (d > 140.0 || p.y > 30.0) break;
⋯
float3 landNormal(float2 x, float e, float height) {
    float h = terrain(x, 8, height);
    float hx = terrain(x + float2(e, 0.0), 8, height);
    float hz = terrain(x + float2(0.0, e), 8, height);
    return normalize(float3(h - hx, e, h - hz));
}
⋯
float softShadow(float3 p, float3 l, float height) {
⋯
        float gap = q.y - terrain(q.xz, 4, height);
⋯
float3 sunDir(float sun) {
    return normalize(float3(0.6, 0.04 + 0.6 * sun, 1.0));
}
⋯
float flightY(float z, float height) {
⋯
        top = max(top, terrain(float2(x, zz), 5, height));
        top = max(top, terrain(float2(x - 2.5, zz), 5, height) - 1.0);
        top = max(top, terrain(float2(x + 2.5, zz), 5, height) - 1.0);
⋯
    float3 ro = float3(flightX(z), flightY(z, p.height), z);
⋯
    float3 fw = normalize(float3(flightX(ahead), ro.y - 1.2, ahead) - ro);
⋯
    float bank = (flightX(z + 8.0) - flightX(z)) * 0.04;
⋯
    float d = marchLand(ro, rd, p.height);
⋯
        float3 n = landNormal(pos.xz, 0.002 + 0.0015 * d, p.height);
        float shadow = softShadow(pos + n * 0.05, l, p.height);
⋯
        level = 0.75 * sunLight + 0.2 * skyLight;

The sound

The sound is worked out from the time alone, so it follows the flight path rather than the picture, and it measures the same land.

  • The wind. Two bands of noise in each ear: a dark one that is always there and a brighter one that swells when the ground rises close under the camera (closeness measures the gap between the flight height and the ground below), with a slow gust over both. Faster flight blows harder.
  • The chord. A2, E3 and C4, each ear a few cents apart so they beat slowly, each note swelling on its own. The higher the sun, the more upper harmonics: the chord opens and brightens as the sun climbs.
  • The bells. The flight is cut into stretches six units long. As the camera passes the middle of a stretch, some of them ring a soft bell, more of them over high ground; the note is picked from A minor pentatonic by how high the ground is there, so climbing over a range rings higher.

Everything goes through tanh and fades in over the first two seconds. At the defaults the peak is about -8 dBFS and the average (RMS) about -21 dBFS.

New in Ray Kiln

Steering with the keys. In the playground and under Run it live on this page, the picture now takes the arrow keys and W A S D as well as the mouse. Click the picture (or Tab to it) and hold a key: the pointer the shader reads, u.mouse, glides across the picture at half its width a second and stays where you let go, and a small ring shows where it is while it moves. Diagonals are no faster than one key. The keys only steer while the picture has the focus, so they never reach the code editor or scroll the page. On a phone, drag a finger across the picture. It is free, and it works for any shader that reads u.mouse.

This episode is the first to read the pointer, and it is made for it: hold → to bank and look along the ridges to the right, hold ↑ to climb above the range and look down on it, and let go to keep that view as the land goes on under you.

Try this

  • Set rough to 0 for soft rolling hills, or to 0.8 for a range of needles.
  • Set sun to 0 and the sun sits on the horizon, the land almost all shadow; at 1 it is afternoon and the sun is out of the picture, above.
  • Set fog to 1 and only the nearest ridges come out of the haze.
  • In flightY, change + 2.6 to + 8.0 and fly high above the range instead of skimming it.
  • In sunDir, change 0.6 to -0.6 and the sun moves to the left: the faces that were lit go into shadow, and the shadows fall the other way.
  • Steer up into the sky with ↓ and watch the haze glow brighter toward the sun.

Parameters

ParameterDefaultRangeWhat it does
speed0.50.0 to 2.0How fast the camera flies over the land
height1.00.3 to 1.6How tall the mountains are
rough0.50.0 to 0.8How rough the land is: soft hills, or sharp ridges
sun0.350.0 to 1.0How high the sun stands, from the horizon to the afternoon
fog0.50.0 to 1.0How thick the haze of distance is
heat0.50.0 to 1.0Slides the palette 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-11-endless-terrain.metal
// Ray Kiln · Episode 11 · Endless Terrain
//
// @episode 11 Endless Terrain
// @length 60
// @variation 30
// @variations 1,2,3,4,5,7,10,12
// @still 24.0
// @teaches Heightfield raymarching, sky, sun, distance fog
// @category Fragment
// @tags raymarching, heightfield, terrain, fractal noise, value noise, fog, sky, soft shadows, procedural sound
// @final Endless terrain | Fly low over the ridges toward the setting sun, and steer with the pointer: across to look aside, up to climb.
//
// Mountains that go on forever, flown over at dusk. The land is one function: a height for every point on the
// ground, made of fractal noise. Each pixel sends a ray out and walks it forward until it drops below the ground;
// the sun lights what it finds, the sky fills the rest, and the haze of distance joins the two.
// 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 camera flies over the land
// @param height 1.0 0.3 1.6 How tall the mountains are
// @param rough 0.5 0.0 0.8 How rough the land is: soft hills, or sharp ridges
// @param sun 0.35 0.0 1.0 How high the sun stands, from the horizon to the afternoon
// @param fog 0.5 0.0 1.0 How thick the haze of distance is
// @param heat 0.5 0.0 1.0 Slides the palette from deep ember to white-hot
// @param volume 0.8 0.0 1.0 Loudness of the sound

constant float TAU = 6.2831853;
constant float FAR = 140.0;

// Episode 1's hash and value noise.
float hash21(float2 p) {
    p = fract(p * float2(123.34, 456.21));
    p += dot(p, p + 45.32);
    return fract(p.x * p.y);
}

float vnoise(float2 p) {
    float2 i = floor(p), f = fract(p);
    f = f * f * (3.0 - 2.0 * f);
    float a = hash21(i), b = hash21(i + float2(1, 0));
    float c = hash21(i + float2(0, 1)), d = hash21(i + float2(1, 1));
    return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}

// 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 land --------------------------------------------------------------------------------------------------
// A heightfield: one height for every point (x, z) on the ground. It is episode 1's fractal noise, with two changes.
// Each octave is folded into a ridge, 1 - |2n - 1|, as far as `rough` asks, so peaks come to a sharp crest. And
// each octave is turned a little against the one before, so no grid lines show. Fewer octaves give the broad shape,
// for marching; more give the detail, for lighting.

float terrain(float2 x, int octaves, float height, float rough) {
    float2 p = x * 0.06;
    float v = 0.0, a = 0.5;
    for (int i = 0; i < octaves; i++) {
        float n = vnoise(p);
        float ridge = 1.0 - abs(2.0 * n - 1.0);
        // The fine octaves are only a little ridged, or every small bump would be a crest.
        v += a * mix(n, ridge * ridge, i < 4 ? rough : rough * 0.3);
        p = rot(p, 0.64) * 2.0 + float2(3.1, 7.7);
        a *= 0.45;
    }
    // Low ground stays flat and wide, high ground climbs fast: the valleys and the peaks.
    return v * v * 18.0 * height - 1.5;
}

// The normal from the land's slope: how much the height changes a small step along x and along z.
float3 landNormal(float2 x, float e, float height, float rough) {
    float h = terrain(x, 8, height, rough);
    float hx = terrain(x + float2(e, 0.0), 8, height, rough);
    float hz = terrain(x + float2(0.0, e), 8, height, rough);
    return normalize(float3(h - hx, e, h - hz));
}

// Marching the heightfield: step forward by a share of the height above the ground, longer
// steps far away, then halve the last step back and forth to find the edge.
float marchLand(float3 ro, float3 rd, float height, float rough) {
    float d = 0.1;
    float prev = 0.1;
    for (int i = 0; i < 160; i++) {
        float3 p = ro + rd * d;
        float gap = p.y - terrain(p.xz, 5, height, rough);
        if (gap < 0.002 * d) {
            float lo = prev, hi = d;
            for (int k = 0; k < 6; k++) {
                float mid = 0.5 * (lo + hi);
                float3 q = ro + rd * mid;
                bool under = q.y < terrain(q.xz, 5, height, rough);
                if (under) hi = mid; else lo = mid;
            }
            return hi;
        }
        prev = d;
        d += max(gap * 0.4 / max(height, 1.0), 0.01 * d);
        if (d > FAR || p.y > 30.0) break;
    }
    return -1.0;
}

// A soft shadow: walk toward the sun; the closer the ray passes to the land, the darker.
float softShadow(float3 p, float3 l, float height, float rough) {
    float lit = 1.0;
    float d = 0.3;
    for (int i = 0; i < 32; i++) {
        float3 q = p + l * d;
        float gap = q.y - terrain(q.xz, 4, height, rough);
        lit = min(lit, 10.0 * gap / d);
        if (lit < 0.0 || q.y > 25.0) break;
        d += clamp(gap, 0.3, 4.0);
    }
    return smoothstep(0.0, 1.0, lit);
}

// ---- The flight ------------------------------------------------------------------------------------------------
// The camera flies along z, wandering across on two slow waves. It holds its height above the highest ground a little
// ahead and to each side, so it lifts before a ridge instead of flying into it.

float flightX(float z) {
    return 9.0 * sin(z * 0.021) + 4.0 * sin(z * 0.047 + 1.3);
}

float flightY(float z, float height, float rough) {
    float top = -10.0;
    for (int i = 0; i < 4; i++) {
        float zz = z + float(i) * 3.0;
        float x = flightX(zz);
        top = max(top, terrain(float2(x, zz), 5, height, rough));
        top = max(top, terrain(float2(x - 2.5, zz), 5, height, rough) - 1.0);
        top = max(top, terrain(float2(x + 2.5, zz), 5, height, rough) - 1.0);
    }
    return max(top, 0.0) + 2.6;
}

float travel(float t) {
    return t * 8.0;
}

// The sun: low ahead and to the right; `sun` raises it.
float3 sunDir(float sun) {
    return normalize(float3(0.6, 0.04 + 0.6 * sun, 1.0));
}

// ---- 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 sky's brightness along a ray: dark overhead, bright at the horizon, brightest round the sun, with its disc.
float skyLevel(float3 rd, float3 l) {
    float toward = max(dot(rd, l), 0.0);
    float horizon = exp(-max(rd.y, 0.0) * 5.0);
    float level = 0.12 + 0.3 * horizon + 0.35 * pow(toward, 8.0) * horizon;
    level += 0.25 * pow(toward, 64.0);
    level += smoothstep(0.9996, 0.9998, toward);
    return level;
}

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;

    // Steering: the pointer's offset from the middle, zero there, as in every video. Across
    // turns the view; up climbs and looks down at the land; down looks up at the sky.
    float2 steer = u.mouse / u.resolution - 0.5;

    float z = travel(t);
    float3 ro = float3(flightX(z), flightY(z, p.height, p.rough), z);
    float climb = max(steer.y, 0.0);
    ro.y += climb * 24.0;
    // Look along the flight path a little ahead, turned by the pointer.
    float ahead = z + 6.0;
    float tip = 1.2 + climb * 5.0 + min(steer.y, 0.0) * 6.0;
    float3 target = float3(flightX(ahead), ro.y - tip, ahead);
    float3 fw = normalize(target - ro);
    float yaw = steer.x * 2.2;
    fw.xz = rot(fw.xz, -yaw);
    // Bank into the turns of the path, as a plane does.
    float bank = (flightX(z + 8.0) - flightX(z)) * 0.04 - steer.x * 0.25;
    float3 rt = normalize(cross(float3(sin(bank), cos(bank), 0.0), fw));
    float3 up = cross(fw, rt);
    float3 rd = normalize(q.x * rt + q.y * up + 1.7 * fw);

    float3 l = sunDir(p.sun);
    float sky = skyLevel(rd, l);
    float d = marchLand(ro, rd, p.height, p.rough);

    float level = sky;
    if (d > 0.0) {
        float3 pos = ro + rd * d;
        // The normal's step grows with distance, so far slopes stay smooth instead of shimmering.
        float3 n = landNormal(pos.xz, 0.002 + 0.0015 * d, p.height, p.rough);
        float shadow = softShadow(pos + n * 0.05, l, p.height, p.rough);
        float sunLight = max(dot(n, l), 0.0) * shadow;
        // Light from the sky above, more on flat ground; light bounced back from the sunlit land facing away.
        float skyLight = 0.5 + 0.5 * n.y;
        float bounce = max(dot(n, normalize(float3(-l.x, 0.0, -l.z))), 0.0);
        // Rock is darker than the flat tops where dust settles; steep faces are bare.
        float flat = smoothstep(0.6, 0.9, n.y);
        float rock = 0.7 + 0.3 * flat + 0.2 * (vnoise(pos.xz * 0.8) - 0.5);
        level = rock * (0.75 * sunLight + 0.2 * skyLight + 0.1 * bounce);

        // Distance fog: the further the land, the more of the sky's own light along that ray replaces it, by
        // exp(-distance). Toward the sun the haze is lit and glows.
        float density = 0.006 + 0.03 * p.fog;
        float through = exp(-d * density);
        float haze = 0.25 + 0.25 * pow(max(dot(rd, l), 0.0), 6.0);
        level = mix(haze, level, through);
    }

    return float4(kiln(level + (p.heat - 0.5) * 0.5), 1.0);
}

// ---- Sound -----------------------------------------------------------------------------------------------------
// Wind over the land: a rush of air that swells as the ground rises close under the camera and thins as it falls
// away. Under it a low chord holds, and it opens up, brighter, the higher the sun stands. Each time the camera
// crosses a crest, a soft bell rings, its note from how high that crest is.

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

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

// How close the ground is under the flight at z: 1 skimming it, falling toward 0 as it drops away.
float closeness(float z, float height, float rough) {
    float ground = terrain(float2(flightX(z), z), 4, height, rough);
    float clearance = flightY(z, height, rough) - ground;
    return exp(-max(clearance - 2.6, 0.0) * 0.35);
}

float2 sound(float t, constant Params& p) {
    float ts = t * p.speed;
    float z = travel(ts);

    // The wind: two bands of noise in each ear, a dark one always there and a brighter one that comes with the
    // ground. Faster flight, more wind.
    float near = closeness(z, p.height, p.rough);
    float gust = 0.6 + 0.4 * noise1(t, 0.4, 9u);
    float2 low = float2(noise1(t, 180.0, 1u), noise1(t, 180.0, 2u));
    float2 high = float2(noise1(t, 900.0, 3u), noise1(t, 900.0, 4u));
    float2 wind = (low * 0.6 + high * near * 0.8) * gust;
    wind *= min(0.3 + p.speed, 1.2);

    // The chord: A2, E3 and C4. Its upper partials grow with the sun.
    float bright = 0.15 + 0.6 * p.sun;
    float2 chord = float2(0.0);
    for (int i = 0; i < 3; i++) {
        float f = i == 0 ? 110.0 : i == 1 ? 164.81 : 261.63;
        // Each ear a few cents apart, so the chord beats slowly.
        float l = sin(TAU * f * t) + bright * 0.5 * sin(TAU * 2.0 * f * t)
                + bright * 0.25 * sin(TAU * 3.0 * f * t);
        float g = f * 1.003;
        float r = sin(TAU * g * t + 1.0)
                + bright * 0.5 * sin(TAU * 2.0 * g * t + 0.4);
        float swell = 0.75 + 0.25 * sin(t * 0.3 + float(i) * 2.1);
        chord += float2(l, r) * swell;
    }
    chord *= 0.05;

    // The bells: every 6 units of flight is a stretch of land; its bell rings as the camera passes its middle, on a
    // note of A minor pentatonic picked by the height of the ground there.
    float2 bells = float2(0.0);
    float slot = floor(z / 6.0);
    for (int k = -2; k <= 0; k++) {
        float j = slot + float(k);
        float at = (j + 0.5) * 6.0;
        float since = (z - at) / (8.0 * max(p.speed, 0.05));
        if (since < 0.0) continue;
        float g = terrain(float2(flightX(at), at), 3, p.height, p.rough);
        float rank = clamp(floor(g / (2.0 * p.height) + 2.0), 0.0, 6.0);
        float semis = rank < 1.0 ? 0.0 : rank < 2.0 ? 3.0 : rank < 3.0 ? 5.0
                    : rank < 4.0 ? 7.0 : rank < 5.0 ? 10.0
                    : rank < 6.0 ? 12.0 : 15.0;
        float f = 440.0 * exp2(semis / 12.0);
        uint s = uint(int(j) + 100000);
        float pick = hashU(s);
        // Only some stretches ring, the higher ground more often: fixed for each stretch.
        float rings = step(0.55 - 0.06 * rank, pick);
        float env = since * exp(-since * 2.2) * 5.0 * rings;
        env *= 1.0 - exp(-since * 400.0);
        // Gone smoothly before the loop lets go of it.
        env *= 1.0 - smoothstep(9.0, 15.0, z - at);
        float tone = sin(TAU * f * since)
                   + 0.3 * sin(TAU * 2.76 * f * since) * exp(-since * 4.0);
        float pan = hashU(s + 7u);
        bells += float2(1.0 - pan, pan) * tone * env;
    }
    bells *= 0.12 * smoothstep(0.0, 0.1, p.speed);

    float fadeIn = smoothstep(0.0, 2.0, t);
    return tanh((wind * 0.35 + chord + bells) * 1.6) * 0.9 * 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, 1.0, 0.5, 0.35, 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 3D Scenes