Spinning Planet
A planet turning in the dark under a low sun. A vertex shader wraps a flat grid round a ball, longitude across and latitude down, and lifts its continents out of fractal noise read in three dimensions, so the grid's seam closes without a crack. Sea, land, ice and cloud are coloured per pixel, cities light the night side, and the air glows blue at the rim and ember at dusk.
Teaches A grid wrapped into a sphere, continents from noise, an atmosphere rim
Builds on Cracked Clay (integer hash, sound noise), Kiln Fire (3D fractal noise), Rippling Water (the camera's axes, perspective projection), Flag in the Wind
Watch
The idea
Rippling Water and Flag in the Wind bent a flat grid into water and cloth. A planet bends it all the way round. The host still lays down a square of squares from 0 to 1; corner reads the grid's across as longitude, once round the ball, and its down as latitude, from the south pole to the north. Two things happen at the edges. The left and right edges of the grid land on the same meridian, so the ball closes along a seam from pole to pole. And the top and bottom rows each shrink to a single point, the poles.
The seam is where a planet usually goes wrong. Colour or height read from the grid's place jumps there, from the value at 1 back to the value at 0. This planet never reads the grid's place for anything but its position: the land comes from fractal noise read at the point on the ball, in three dimensions, and the grid's two edges are the same points, so they read the same noise and the seam is invisible.
It is built in five steps, and each step is a shader you can run on its own.
Stage 1 · A grid wrapped into a ball
onBall turns a grid point into a direction from the centre: longitude grid.x × 2π, latitude (grid.y − 0.5) × π, and the usual cos lat cos lon, sin lat, cos lat sin lon. turn spins that direction about the planet's axis (once every 30 seconds at speed 1) and then leans the axis by 0.41 radians, about Earth's tilt. project is Flag in the Wind's camera, here 3.4 from the centre and looking at it, zoomed out in a picture taller than it is wide so the planet still fits across it.
The grid is only 24 squares a side, and shade paints them as a checkerboard in two kiln colours, lit by a sun on the left as much as the ball faces it. The column of squares on each side of the seam is gold, so you can watch the seam come round the front about 22.5 seconds in, and see the squares narrow to slivers at the poles.
stage-1-ball.metal
// Stage 1 · A grid wrapped into a ball
// The host lays down 24 by 24 squares. corner makes across longitude and
// down latitude, so the grid's left and right edges meet on one meridian:
// the seam, where the grid closes. shade paints the squares to show it.
//
// @grid 24
// @param speed 1.0 0.0 3.0 How fast the planet turns: once in 30 s at 1
constant float PI = 3.1415927;
constant float TAU = 6.2831853;
// Seconds for one turn at speed 1, and the lean of the planet's axis.
constant float PERIOD = 30.0;
constant float TILT = 0.41;
// How far the eye is from the planet's centre, and how much the camera
// magnifies.
constant float DISTANCE = 3.4;
constant float ZOOM = 2.4;
// What each corner carries to the pixels: where it lands on screen, where it
// is in the world, and its place on the grid.
struct Vertex {
float4 position [[position]];
float3 world;
float2 grid;
};
// Flowing Color Field's palette: near-black, ember, orange, gold, white-hot.
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));
}
// A point on the grid as a direction from the centre: across is longitude,
// once round; down is latitude, from the south pole to the north. At the
// poles a whole row of corners lands on one point.
float3 onBall(float2 grid) {
float lon = grid.x * TAU;
float lat = (grid.y - 0.5) * PI;
float c = cos(lat);
return float3(c * cos(lon), sin(lat), c * sin(lon));
}
// The planet turned to now: spun about its axis, then the axis leaned.
float3 turn(float3 d, float t) {
float s = t * TAU / PERIOD;
float cs = cos(s), sn = sin(s);
float3 r = float3(cs * d.x - sn * d.z, d.y, sn * d.x + cs * d.z);
float ct = cos(TILT), st = sin(TILT);
return float3(ct * r.x - st * r.y, st * r.x + ct * r.y, r.z);
}
// The camera's three directions, looking from the eye at the centre.
float3x3 cameraAxes(float3 eye) {
float3 f = normalize(-eye);
float3 r = normalize(cross(float3(0, 1, 0), f));
return float3x3(r, cross(f, r), f);
}
// A world point on screen, as Flag in the Wind's; a tall picture zooms out.
float4 project(float3 world, float3 eye, float aspect) {
float zoom = ZOOM * min(aspect, 1.0);
float3x3 axes = cameraAxes(eye);
float3 v = world - eye;
float3 view = float3(dot(v, axes[0]), dot(v, axes[1]), dot(v, axes[2]));
float near = 0.1, far = 30.0;
float depth = (view.z - near) / (far - near) * view.z;
return float4(view.x * zoom / aspect, view.y * zoom, depth, view.z);
}
// Once for every corner: its point on the ball, turned, put on screen.
Vertex corner(float2 grid, constant Uniforms& u, constant Params& p) {
float3 eye = float3(0.0, 0.0, -DISTANCE);
Vertex o;
o.world = turn(onBall(grid), u.time * p.speed);
o.position = project(o.world, eye, u.resolution.x / u.resolution.y);
o.grid = grid;
return o;
}
// The sun: left, a little above and in front; night is on the right.
constant float3 SUN = float3(-0.88, 0.3, -0.3);
// Once for every pixel: the grid's squares as a checkerboard, gold beside
// the seam, lit by the sun.
float4 shade(Vertex v, constant Uniforms& u, constant Params& p) {
float2 cell = floor(v.grid * 24.0);
float check = fmod(cell.x + cell.y, 2.0);
float3 col = mix(kiln(0.35), kiln(0.55), check);
if (cell.x < 1.0 || cell.x > 22.0) col = kiln(0.75);
float light = max(dot(normalize(v.world), normalize(SUN)), 0.0);
return float4(col * (0.08 + 0.92 * light), 1.0);
}
// Behind the ball, the dark.
float4 background(float2 uv, constant Uniforms& u, constant Params& p) {
return float4(0.004, 0.003, 0.006, 1.0);
}Stage 2 · Continents from noise
landHeight is fractal noise in three dimensions: seven layers of value noise (valueNoise3, a random value at every whole-numbered point of space, blended smoothly across each cube), each about twice as fine and half as strong as the one before and shifted so their grids don't line up. It is read at the direction on the ball times 1.7, so a few large continents fit round the planet and the finer layers make their coasts ragged.
corner now hands its unturned direction (planet) to the pixels, and shade reads landHeight again there, per pixel. A coast is where the noise crosses sea, blended over ±0.004 so it stays sharp at any grid size. Below the sea level the water deepens from teal to near-black as the noise falls further below, and above it the land goes from green to brown as it rises. The grid is 64 squares a side now, though the colours no longer depend on it.
stage-2-continents.metal50 new lines since stage 1
// Stage 2 · Continents from noise
// Land and sea come from fractal noise, read at the point on the ball in
// three dimensions, not at the grid's place. The grid's two edges meet at
// the same points, so they read the same noise and the seam disappears.
// The colour is worked out per pixel, so coasts stay sharp on a coarse grid.
//
// @grid 64
// @param speed 1.0 0.0 3.0 How fast the planet turns: once in 30 s at 1
// @param sea 0.52 0.3 0.75 Sea level: low drains the oceans, high leaves islands
constant float PI = 3.1415927;
constant float TAU = 6.2831853;
// Seconds for one turn at speed 1, and the lean of the planet's axis.
constant float PERIOD = 30.0;
constant float TILT = 0.41;
// How far the eye is from the planet's centre, and how much the camera
// magnifies.
constant float DISTANCE = 3.4;
constant float ZOOM = 2.4;
// What each corner carries to the pixels: where it lands on screen, where it
// is in the world, and its point on the planet before it is turned (for the
// colours, which turn with it).
struct Vertex {
float4 position [[position]];
float3 world;
float3 planet;
};
// Cracked Clay's hash: a whole number in, a random 0 to 1 out.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// A random 0 to 1 for each whole-numbered point in space.
float hash3(float3 c) {
uint x = uint(int(c.x) + 4096);
uint y = uint(int(c.y) + 4096);
uint z = uint(int(c.z) + 4096);
return hashU(x * 73856093u ^ y * 19349663u ^ z * 83492791u);
}
// Value noise in three dimensions: a random value at every whole-numbered
// point, blended smoothly across the cube between eight of them.
float valueNoise3(float3 x) {
float3 i = floor(x);
float3 f = x - i;
f = f * f * (3.0 - 2.0 * f);
float a = mix(hash3(i), hash3(i + float3(1, 0, 0)), f.x);
float b = mix(hash3(i + float3(0, 1, 0)), hash3(i + float3(1, 1, 0)), f.x);
float c = mix(hash3(i + float3(0, 0, 1)), hash3(i + float3(1, 0, 1)), f.x);
float d = mix(hash3(i + float3(0, 1, 1)), hash3(i + float3(1, 1, 1)), f.x);
return mix(mix(a, b, f.y), mix(c, d, f.y), f.z);
}
// Fractal noise: `octaves` layers of value noise, each twice as fine and half
// as strong as the one before, shifted so their grids don't line up. 0 to 1.
float fbm3(float3 x, int octaves) {
float sum = 0.0, amp = 0.5, total = 0.0;
for (int k = 0; k < octaves; k++) {
sum += amp * valueNoise3(x);
total += amp;
x = x * 2.03 + float3(1.7, 9.2, 4.1);
amp *= 0.5;
}
return sum / total;
}
// The ground's height at a point on the planet (a direction from its centre):
// big continents from coarse noise, made rough by the finer layers.
float landHeight(float3 d) {
return fbm3(d * 1.7 + float3(3.0, 1.0, 7.0), 7);
}
// Flowing Color Field's palette: near-black, ember, orange, gold, white-hot.
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));
}
// A point on the grid as a direction from the centre: across is longitude,
// once round; down is latitude, from the south pole to the north. At the
// poles a whole row of corners lands on one point.
float3 onBall(float2 grid) {
float lon = grid.x * TAU;
float lat = (grid.y - 0.5) * PI;
float c = cos(lat);
return float3(c * cos(lon), sin(lat), c * sin(lon));
}
// The planet turned to now: spun about its axis, then the axis leaned.
float3 turn(float3 d, float t) {
float s = t * TAU / PERIOD;
float cs = cos(s), sn = sin(s);
float3 r = float3(cs * d.x - sn * d.z, d.y, sn * d.x + cs * d.z);
float ct = cos(TILT), st = sin(TILT);
return float3(ct * r.x - st * r.y, st * r.x + ct * r.y, r.z);
}
// The camera's three directions, looking from the eye at the centre.
float3x3 cameraAxes(float3 eye) {
float3 f = normalize(-eye);
float3 r = normalize(cross(float3(0, 1, 0), f));
return float3x3(r, cross(f, r), f);
}
// A world point on screen, as Flag in the Wind's; a tall picture zooms out.
float4 project(float3 world, float3 eye, float aspect) {
float zoom = ZOOM * min(aspect, 1.0);
float3x3 axes = cameraAxes(eye);
float3 v = world - eye;
float3 view = float3(dot(v, axes[0]), dot(v, axes[1]), dot(v, axes[2]));
float near = 0.1, far = 30.0;
float depth = (view.z - near) / (far - near) * view.z;
return float4(view.x * zoom / aspect, view.y * zoom, depth, view.z);
}
// Once for every corner: its point on a ball of radius 1, turned, and
// placed on screen by a camera in front of it.
Vertex corner(float2 grid, constant Uniforms& u, constant Params& p) {
float3 eye = float3(0.0, 0.0, -DISTANCE);
Vertex o;
float3 d = onBall(grid);
o.world = turn(d, u.time * p.speed);
o.position = project(o.world, eye, u.resolution.x / u.resolution.y);
o.planet = d;
return o;
}
// The sun: left, a little above and in front; night is on the right.
constant float3 SUN = float3(-0.88, 0.3, -0.3);
// Once for every pixel: the noise read again at this point, sea below the
// sea level (deeper further below) and land above it (green low, brown
// higher), lit by the sun as much as the ball faces it there.
float4 shade(Vertex v, constant Uniforms& u, constant Params& p) {
float3 d = normalize(v.planet);
float h = landHeight(d);
float land = smoothstep(p.sea - 0.004, p.sea + 0.004, h);
float above = max(h - p.sea, 0.0) / max(1.0 - p.sea, 0.05);
float below = max(p.sea - h, 0.0) / max(p.sea, 0.05);
float3 shallow = float3(0.03, 0.16, 0.22);
float3 deep = float3(0.005, 0.025, 0.07);
float3 sea = mix(shallow, deep, smoothstep(0.0, 0.25, below));
float3 low = float3(0.16, 0.22, 0.07), high = float3(0.40, 0.29, 0.14);
float3 soil = mix(low, high, smoothstep(0.02, 0.18, above));
float3 albedo = mix(sea, soil, land);
float light = max(dot(normalize(v.world), normalize(SUN)), 0.0);
return float4(albedo * kiln(0.9) * 1.25 * light + albedo * 0.02, 1.0);
}
// Behind the ball, the dark.
float4 background(float2 uv, constant Uniforms& u, constant Params& p) {
return float4(0.004, 0.003, 0.006, 1.0);
}8 lines from stage 1 that are gone
float2 grid;
⋯
o.world = turn(onBall(grid), u.time * p.speed);
⋯
o.grid = grid;
⋯
float2 cell = floor(v.grid * 24.0);
float check = fmod(cell.x + cell.y, 2.0);
float3 col = mix(kiln(0.35), kiln(0.55), check);
if (cell.x < 1.0 || cell.x > 22.0) col = kiln(0.75);
⋯
return float4(col * (0.08 + 0.92 * light), 1.0);Stage 3 · Mountains
groundAt lifts the land off the ball: the radius is 1 plus relief × 2 × (height − sea) where the noise is above the sea, and exactly 1 where it isn't, so the oceans stay a smooth ball and only the land stands up. The highest land turns grey rock.
The light needs the ground's normal. As in Flag in the Wind, corner asks where its neighbours are and crosses the two directions to them. But the grid's own directions are no use here: at the poles, a step across the grid doesn't move at all. So the two directions are made from the point itself: e1 is at right angles to the point and to a helper axis (the world's up, or its right near the poles), and e2 is at right angles to both. corner steps 0.003 each way along each, lifts those four points with groundAt, and crosses the differences. The grid is 256 squares a side, 131,072 triangles, enough that mountains show on the outline.
stage-3-mountains.metal34 new lines since stage 2
// Stage 3 · Mountains
// corner lifts the land off the ball; the sea stays smooth. For the light,
// it finds the normal from neighbours along the ball, in directions made
// from the point itself, which still work at the poles.
//
// @grid 256
// @param speed 1.0 0.0 3.0 How fast the planet turns: once in 30 s at 1
// @param sea 0.52 0.3 0.75 Sea level: low drains the oceans, high leaves islands
// @param relief 0.05 0.0 0.15 How tall the mountains stand off the ball
constant float PI = 3.1415927;
constant float TAU = 6.2831853;
// Seconds for one turn at speed 1, and the lean of the planet's axis.
constant float PERIOD = 30.0;
constant float TILT = 0.41;
// How far the eye is from the planet's centre, and how much the camera
// magnifies.
constant float DISTANCE = 3.4;
constant float ZOOM = 2.4;
// How far apart, on the ball, a corner's neighbours are asked for.
constant float NEAR = 0.003;
// What each corner carries to the pixels: where it lands on screen, where it
// is in the world, which way the ground faces, and its point on the planet
// before it is turned (for the colours, which turn with it).
struct Vertex {
float4 position [[position]];
float3 world;
float3 normal;
float3 planet;
};
// Cracked Clay's hash: a whole number in, a random 0 to 1 out.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// Flowing Color Field's palette: near-black, ember, orange, gold, white-hot.
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));
}
// A random 0 to 1 for each whole-numbered point in space.
float hash3(float3 c) {
uint x = uint(int(c.x) + 4096);
uint y = uint(int(c.y) + 4096);
uint z = uint(int(c.z) + 4096);
return hashU(x * 73856093u ^ y * 19349663u ^ z * 83492791u);
}
// Value noise in three dimensions: a random value at every whole-numbered
// point, blended smoothly across the cube between eight of them.
float valueNoise3(float3 x) {
float3 i = floor(x);
float3 f = x - i;
f = f * f * (3.0 - 2.0 * f);
float a = mix(hash3(i), hash3(i + float3(1, 0, 0)), f.x);
float b = mix(hash3(i + float3(0, 1, 0)), hash3(i + float3(1, 1, 0)), f.x);
float c = mix(hash3(i + float3(0, 0, 1)), hash3(i + float3(1, 0, 1)), f.x);
float d = mix(hash3(i + float3(0, 1, 1)), hash3(i + float3(1, 1, 1)), f.x);
return mix(mix(a, b, f.y), mix(c, d, f.y), f.z);
}
// Fractal noise: `octaves` layers of value noise, each twice as fine and half
// as strong as the one before, shifted so their grids don't line up. 0 to 1.
float fbm3(float3 x, int octaves) {
float sum = 0.0, amp = 0.5, total = 0.0;
for (int k = 0; k < octaves; k++) {
sum += amp * valueNoise3(x);
total += amp;
x = x * 2.03 + float3(1.7, 9.2, 4.1);
amp *= 0.5;
}
return sum / total;
}
// The ground's height at a point on the planet (a direction from its centre):
// big continents from coarse noise, made rough by the finer layers.
float landHeight(float3 d) {
return fbm3(d * 1.7 + float3(3.0, 1.0, 7.0), 7);
}
// A point on the grid as a direction from the centre: across is longitude,
// once round; down is latitude, from the south pole to the north.
float3 onBall(float2 grid) {
float lon = grid.x * TAU;
float lat = (grid.y - 0.5) * PI;
float c = cos(lat);
return float3(c * cos(lon), sin(lat), c * sin(lon));
}
// The planet turned to now: spun about its axis, then the axis leaned.
float3 turn(float3 d, float t) {
float s = t * TAU / PERIOD;
float cs = cos(s), sn = sin(s);
float3 r = float3(cs * d.x - sn * d.z, d.y, sn * d.x + cs * d.z);
float ct = cos(TILT), st = sin(TILT);
return float3(ct * r.x - st * r.y, st * r.x + ct * r.y, r.z);
}
// The camera's three directions, looking from the eye at the centre.
float3x3 cameraAxes(float3 eye) {
float3 f = normalize(-eye);
float3 r = normalize(cross(float3(0, 1, 0), f));
return float3x3(r, cross(f, r), f);
}
// A world point on screen, as Flag in the Wind's; a tall picture zooms out.
float4 project(float3 world, float3 eye, float aspect) {
float zoom = ZOOM * min(aspect, 1.0);
float3x3 axes = cameraAxes(eye);
float3 v = world - eye;
float3 view = float3(dot(v, axes[0]), dot(v, axes[1]), dot(v, axes[2]));
float near = 0.1, far = 30.0;
float depth = (view.z - near) / (far - near) * view.z;
return float4(view.x * zoom / aspect, view.y * zoom, depth, view.z);
}
// The ground at a point on the planet: the ball, lifted by the land's height
// above the sea, so the sea stays a smooth ball and the mountains stand out.
float3 groundAt(float3 d, constant Params& p) {
float h = landHeight(d);
return d * (1.0 + p.relief * max(h - p.sea, 0.0) * 2.0);
}
// Once for every corner: its point on the ball, the ground there, and the
// ground's normal from two neighbours each way along the ball's surface, in
// directions made from the point itself, so there is no trouble at the poles
// where the grid's squares shrink to nothing.
Vertex corner(float2 grid, constant Uniforms& u, constant Params& p) {
float t = u.time * p.speed;
float3 d = onBall(grid);
float3 helper = abs(d.y) < 0.9 ? float3(0, 1, 0) : float3(1, 0, 0);
float3 e1 = normalize(cross(helper, d));
float3 e2 = cross(d, e1);
float3 a = groundAt(normalize(d + e1 * NEAR), p)
- groundAt(normalize(d - e1 * NEAR), p);
float3 b = groundAt(normalize(d + e2 * NEAR), p)
- groundAt(normalize(d - e2 * NEAR), p);
float3 n = normalize(cross(a, b));
if (dot(n, d) < 0.0) n = -n;
float3 eye = float3(0.0, 0.0, -DISTANCE);
Vertex o;
o.world = turn(groundAt(d, p), t);
o.position = project(o.world, eye, u.resolution.x / u.resolution.y);
o.normal = turn(n, t);
o.planet = d;
return o;
}
// The sun: left, a little above and in front; night is on the right.
constant float3 SUN = float3(-0.88, 0.3, -0.3);
// Once for every pixel of the planet: sea and land as in stage 2, the
// highest land rocky, lit by the ground's own normal, so the sun picks out
// the slopes that face it and leaves the far sides of the mountains dark.
float4 shade(Vertex v, constant Uniforms& u, constant Params& p) {
float3 d = normalize(v.planet);
float3 n = normalize(v.normal);
float h = landHeight(d);
float land = smoothstep(p.sea - 0.004, p.sea + 0.004, h);
float above = max(h - p.sea, 0.0) / max(1.0 - p.sea, 0.05);
float below = max(p.sea - h, 0.0) / max(p.sea, 0.05);
float3 shallow = float3(0.03, 0.16, 0.22);
float3 deep = float3(0.005, 0.025, 0.07);
float3 sea = mix(shallow, deep, smoothstep(0.0, 0.25, below));
float3 low = float3(0.16, 0.22, 0.07), high = float3(0.40, 0.29, 0.14);
float3 soil = mix(low, high, smoothstep(0.02, 0.18, above));
soil = mix(soil, float3(0.30, 0.26, 0.24), smoothstep(0.2, 0.35, above));
float3 albedo = mix(sea, soil, land);
float light = max(dot(n, normalize(SUN)), 0.0);
return float4(albedo * kiln(0.9) * 1.25 * light + albedo * 0.02, 1.0);
}
// Behind the planet, the dark.
float4 background(float2 uv, constant Uniforms& u, constant Params& p) {
return float4(0.004, 0.003, 0.006, 1.0);
}15 lines from stage 2 that are gone
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));
}
⋯
float3 d = onBall(grid);
o.world = turn(d, u.time * p.speed);
⋯
float light = max(dot(normalize(v.world), normalize(SUN)), 0.0);Stage 4 · The air
A planet's air is a thin shell, but along the rim we look through a lot of it at a slant. shade adds its glow by (1 − n·v)³, which is nothing where the ground faces us and climbs steeply toward the edge. Beyond the edge, background finds how close the ray through each pixel passes the planet's centre, and where that is just over 1 (the ball's radius) the air glows, fading by exp(−(r − 1) / 0.035).
Both use the same colour of air: blue-white where the sun is up, and ember at dusk, a narrow band where the sun is on the horizon (a Gaussian in sunUp, the sun's height over that point). Light on the ground now ends softly at the terminator rather than at a hard line, so the night side falls dark. Behind it all are stars: the sky's directions cut into cells 1/70 of a radian across, about one cell in six holding a star at a random place in it, white-blue or warm, most faint and a few bright.
stage-4-air.metal46 new lines since stage 3
// Stage 4 · The air
// A planet's air is thin, but along the rim we look through a lot of it.
// shade adds its glow where the ground turns away from us, and background
// adds a halo just outside the edge, from how close the ray through each
// pixel passes the centre. Both are blue-white by day and ember at dusk,
// and the day ends softly at the terminator. Stars fill the dark behind.
//
// @grid 256
// @param speed 1.0 0.0 3.0 How fast the planet turns: once in 30 s at 1
// @param sea 0.52 0.3 0.75 Sea level: low drains the oceans, high leaves islands
// @param relief 0.05 0.0 0.15 How tall the mountains stand off the ball
constant float PI = 3.1415927;
constant float TAU = 6.2831853;
// Seconds for one turn at speed 1, and the lean of the planet's axis.
constant float PERIOD = 30.0;
constant float TILT = 0.41;
// How far the eye is from the planet's centre, and how much the camera
// magnifies.
constant float DISTANCE = 3.4;
constant float ZOOM = 2.4;
// How far apart, on the ball, a corner's neighbours are asked for.
constant float NEAR = 0.003;
// What each corner carries to the pixels: where it lands on screen, where it
// is in the world, which way the ground faces, and its point on the planet
// before it is turned (for the colours, which turn with it).
struct Vertex {
float4 position [[position]];
float3 world;
float3 normal;
float3 planet;
};
// Cracked Clay's hash: a whole number in, a random 0 to 1 out.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// Flowing Color Field's palette: near-black, ember, orange, gold, white-hot.
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));
}
// A random 0 to 1 for each whole-numbered point in space.
float hash3(float3 c) {
uint x = uint(int(c.x) + 4096);
uint y = uint(int(c.y) + 4096);
uint z = uint(int(c.z) + 4096);
return hashU(x * 73856093u ^ y * 19349663u ^ z * 83492791u);
}
// Value noise in three dimensions: a random value at every whole-numbered
// point, blended smoothly across the cube between eight of them.
float valueNoise3(float3 x) {
float3 i = floor(x);
float3 f = x - i;
f = f * f * (3.0 - 2.0 * f);
float a = mix(hash3(i), hash3(i + float3(1, 0, 0)), f.x);
float b = mix(hash3(i + float3(0, 1, 0)), hash3(i + float3(1, 1, 0)), f.x);
float c = mix(hash3(i + float3(0, 0, 1)), hash3(i + float3(1, 0, 1)), f.x);
float d = mix(hash3(i + float3(0, 1, 1)), hash3(i + float3(1, 1, 1)), f.x);
return mix(mix(a, b, f.y), mix(c, d, f.y), f.z);
}
// Fractal noise: `octaves` layers of value noise, each twice as fine and half
// as strong as the one before, shifted so their grids don't line up. 0 to 1.
float fbm3(float3 x, int octaves) {
float sum = 0.0, amp = 0.5, total = 0.0;
for (int k = 0; k < octaves; k++) {
sum += amp * valueNoise3(x);
total += amp;
x = x * 2.03 + float3(1.7, 9.2, 4.1);
amp *= 0.5;
}
return sum / total;
}
// The ground's height at a point on the planet (a direction from its centre):
// big continents from coarse noise, made rough by the finer layers.
float landHeight(float3 d) {
return fbm3(d * 1.7 + float3(3.0, 1.0, 7.0), 7);
}
// A point on the grid as a direction from the centre: across is longitude,
// once round; down is latitude, from the south pole to the north.
float3 onBall(float2 grid) {
float lon = grid.x * TAU;
float lat = (grid.y - 0.5) * PI;
float c = cos(lat);
return float3(c * cos(lon), sin(lat), c * sin(lon));
}
// The planet turned to now: spun about its axis, then the axis leaned.
float3 turn(float3 d, float t) {
float s = t * TAU / PERIOD;
float cs = cos(s), sn = sin(s);
float3 r = float3(cs * d.x - sn * d.z, d.y, sn * d.x + cs * d.z);
float ct = cos(TILT), st = sin(TILT);
return float3(ct * r.x - st * r.y, st * r.x + ct * r.y, r.z);
}
// The camera's three directions, looking from the eye at the centre.
float3x3 cameraAxes(float3 eye) {
float3 f = normalize(-eye);
float3 r = normalize(cross(float3(0, 1, 0), f));
return float3x3(r, cross(f, r), f);
}
// A world point on screen, as Flag in the Wind's; a tall picture zooms out.
float4 project(float3 world, float3 eye, float aspect) {
float zoom = ZOOM * min(aspect, 1.0);
float3x3 axes = cameraAxes(eye);
float3 v = world - eye;
float3 view = float3(dot(v, axes[0]), dot(v, axes[1]), dot(v, axes[2]));
float near = 0.1, far = 30.0;
float depth = (view.z - near) / (far - near) * view.z;
return float4(view.x * zoom / aspect, view.y * zoom, depth, view.z);
}
// The ground at a point on the planet: the ball, lifted by the land's height
// above the sea, so the sea stays a smooth ball and the mountains stand out.
float3 groundAt(float3 d, constant Params& p) {
float h = landHeight(d);
return d * (1.0 + p.relief * max(h - p.sea, 0.0) * 2.0);
}
// Once for every corner: its point on the ball, the ground there, and the
// ground's normal from two neighbours each way along the ball's surface, in
// directions made from the point itself, so there is no trouble at the poles
// where the grid's squares shrink to nothing.
Vertex corner(float2 grid, constant Uniforms& u, constant Params& p) {
float t = u.time * p.speed;
float3 d = onBall(grid);
float3 helper = abs(d.y) < 0.9 ? float3(0, 1, 0) : float3(1, 0, 0);
float3 e1 = normalize(cross(helper, d));
float3 e2 = cross(d, e1);
float3 a = groundAt(normalize(d + e1 * NEAR), p)
- groundAt(normalize(d - e1 * NEAR), p);
float3 b = groundAt(normalize(d + e2 * NEAR), p)
- groundAt(normalize(d - e2 * NEAR), p);
float3 n = normalize(cross(a, b));
if (dot(n, d) < 0.0) n = -n;
float3 eye = float3(0.0, 0.0, -DISTANCE);
Vertex o;
o.world = turn(groundAt(d, p), t);
o.position = project(o.world, eye, u.resolution.x / u.resolution.y);
o.normal = turn(n, t);
o.planet = d;
return o;
}
// The sun: left, a little above and in front; night is on the right.
constant float3 SUN = float3(-0.88, 0.3, -0.3);
// The colour of the air: blue-white by day, ember where the sun is low.
float3 airColour(float day) {
return mix(kiln(0.55), float3(0.45, 0.62, 1.0), smoothstep(0.0, 0.5, day));
}
// Once for every pixel of the planet: sea and land as before, lit by the
// mountains' normals, and only by day: the sun's light ends softly at the
// terminator, where the sun is on the horizon. The air glows along the rim,
// blue-white by day and ember at dusk.
float4 shade(Vertex v, constant Uniforms& u, constant Params& p) {
float3 d = normalize(v.planet);
float3 n = normalize(v.normal);
float3 eye = float3(0.0, 0.0, -DISTANCE);
float3 toEye = normalize(eye - v.world);
float3 sun = normalize(SUN);
float h = landHeight(d);
float land = smoothstep(p.sea - 0.004, p.sea + 0.004, h);
float above = max(h - p.sea, 0.0) / max(1.0 - p.sea, 0.05);
float below = max(p.sea - h, 0.0) / max(p.sea, 0.05);
float3 shallow = float3(0.03, 0.16, 0.22);
float3 deep = float3(0.005, 0.025, 0.07);
float3 sea = mix(shallow, deep, smoothstep(0.0, 0.25, below));
float3 low = float3(0.16, 0.22, 0.07), high = float3(0.40, 0.29, 0.14);
float3 soil = mix(low, high, smoothstep(0.02, 0.18, above));
soil = mix(soil, float3(0.30, 0.26, 0.24), smoothstep(0.2, 0.35, above));
float3 albedo = mix(sea, soil, land);
float3 ballN = normalize(v.world);
float sunUp = dot(ballN, sun);
float day = smoothstep(-0.12, 0.25, sunUp);
float3 col = albedo * kiln(0.9) * 1.25 * max(dot(n, sun), 0.0) * day;
col += albedo * 0.02;
// The air: more of it is seen along the rim, lit by day, ember at dusk.
float rim = pow(1.0 - max(dot(ballN, toEye), 0.0), 3.0);
float dusk = exp(-sunUp * sunUp / 0.03);
col += airColour(sunUp) * rim * (day * 0.9 + dusk * 0.6);
col += kiln(0.6) * dusk * 0.03;
return float4(col, 1.0);
}
// Behind the planet: stars, and the air around its edge. The ray through the
// pixel passes the centre at some closest distance; just outside 1 (the
// ball's radius) the air glows, fading over a few hundredths, brighter on
// the side the sun is on and ember where it turns to night.
float4 background(float2 uv, constant Uniforms& u, constant Params& p) {
float3 eye = float3(0.0, 0.0, -DISTANCE);
float aspect = u.resolution.x / u.resolution.y;
float3x3 axes = cameraAxes(eye);
float2 s = (uv * 2.0 - 1.0) / (ZOOM * min(aspect, 1.0));
float3 dir = normalize(axes[2] + axes[0] * s.x * aspect + axes[1] * s.y);
// Stars: a grid of cells over the sky's directions, a star in some.
float2 sph = float2(atan2(dir.z, dir.x), asin(clamp(dir.y, -1.0, 1.0)));
sph *= 70.0;
float2 cell = floor(sph);
uint k = uint(int(cell.x) + 1000) * 7919u;
k += uint(int(cell.y) + 1000) * 104729u;
float2 at = float2(hashU(k), hashU(k + 1u)) * 0.8 + 0.1;
float star = step(0.82, hashU(k + 2u));
star *= smoothstep(0.09, 0.0, length(fract(sph) - at));
float bright = pow(hashU(k + 3u), 3.0);
float3 tint = mix(float3(0.7, 0.8, 1.0), kiln(0.85), hashU(k + 4u));
float3 col = float3(0.004, 0.003, 0.006);
col += tint * star * (0.25 + 1.2 * bright);
// The air outside the edge.
float along = dot(-eye, dir);
float3 closest = eye + dir * along;
float r = length(closest);
if (along > 0.0) {
float3 sun = normalize(SUN);
float sunUp = dot(closest / r, sun);
float lit = smoothstep(-0.35, 0.3, sunUp);
float dusk = exp(-sunUp * sunUp / 0.02);
float glow = exp(-max(r - 1.0, 0.0) / 0.035) * step(1.0, r);
float3 air = airColour(sunUp) * lit + kiln(0.5) * dusk * 0.3;
col = col * (1.0 - glow * lit) + air * glow * 0.9;
}
return float4(col, 1.0);
}3 lines from stage 3 that are gone
float light = max(dot(n, normalize(SUN)), 0.0);
return float4(albedo * kiln(0.9) * 1.25 * light + albedo * 0.02, 1.0);
⋯
return float4(0.004, 0.003, 0.006, 1.0);Stage 5 · Spinning Planet
The last step finishes the planet.
- Ice and snow. Ice covers each pole for about 26° round it: the test is on
abs(d.y), the point's height above the equator on a ball of radius 1, and ice starts where that passes about 0.9, its edge wavering with the noise. Snow caps the highest mountains, and comes further down the nearer they are to the poles. - Glint. The sea reflects the sun: a tight highlight (
pow(…, 60)) where the sun's reflection off the ball meets the eye. - Cloud. A second fractal noise, five layers read at the direction times 3.0 against the land's 1.7, so about 1.8 times finer, turned slowly against the ground (0.012 radians a second at
speed1), so the weather drifts across the land.cloudsraises how much of it counts as cloud. It is lit by the sun as the ground is, and goes dark past the terminator. - Cities. On low land in the night, specks of light from fine noise (240 times across), only where a coarser noise says people live, under no cloud, in kiln gold.
- The pointer moves the eye round the planet, up to 0.7 radians left and right and 0.45 up and down; with no pointer it looks straight on.
the kiln palette and shade(), from the finished shader
// Flowing Color Field's palette: near-black, ember, orange, gold, white-hot.
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));
}
// Once for every pixel of the planet. The colour comes from the noise read
// again here, per pixel, so coasts stay sharp however coarse the grid: deep
// and shallow sea, lowland, highland, rock and snow, and ice toward the poles.
// The sun lights it, the sea shines where it reflects the sun, cloud drifts
// over it, cities light the night side, and the air glows at the rim.
float4 shade(Vertex v, constant Uniforms& u, constant Params& p) {
float t = u.time * p.speed;
float3 d = normalize(v.planet);
float3 n = normalize(v.normal);
float3 eye = eyeAt(u);
float3 toEye = normalize(eye - v.world);
float3 sun = normalize(SUN);
float h = landHeight(d);
float land = smoothstep(p.sea - 0.004, p.sea + 0.004, h);
float above = max(h - p.sea, 0.0) / max(1.0 - p.sea, 0.05);
float below = max(p.sea - h, 0.0) / max(p.sea, 0.05);
float3 shallow = float3(0.03, 0.16, 0.22);
float3 deep = float3(0.005, 0.025, 0.07);
float3 sea = mix(shallow, deep, smoothstep(0.0, 0.25, below));
float3 low = float3(0.16, 0.22, 0.07), high = float3(0.40, 0.29, 0.14);
float3 soil = mix(low, high, smoothstep(0.02, 0.18, above));
soil = mix(soil, float3(0.30, 0.26, 0.24), smoothstep(0.2, 0.35, above));
float lat = abs(d.y);
float snow = smoothstep(0.36, 0.42, above)
+ smoothstep(0.80, 0.86, lat + 0.1 * above);
soil = mix(soil, float3(0.92, 0.94, 0.96), clamp(snow, 0.0, 1.0));
float3 albedo = mix(sea, soil, land);
float ice = smoothstep(0.88, 0.92, lat + 0.05 * (h - 0.5));
albedo = mix(albedo, float3(0.85, 0.9, 0.95), ice);
float3 ballN = normalize(v.world);
float sunUp = dot(ballN, sun);
float day = smoothstep(-0.12, 0.25, sunUp);
float3 sunLight = kiln(0.9) * 1.25;
float3 col = albedo * sunLight * max(dot(n, sun), 0.0) * day;
col += albedo * 0.02;
float glint = pow(max(dot(reflect(-sun, ballN), toEye), 0.0), 60.0);
col += kiln(0.88) * glint * (1.0 - land) * (1.0 - ice) * day * 0.9;
// Cloud: a fractal noise, turned slowly against the ground so it drifts.
float drift = t * 0.012;
float cs = cos(drift), sn = sin(drift);
float3 cd = float3(cs * d.x - sn * d.z, d.y, sn * d.x + cs * d.z);
float c = fbm3(cd * 3.0 + float3(t * 0.01, 0.0, 0.0), 5);
float top = 1.0 - 0.45 * p.clouds;
float cover = smoothstep(top - 0.25, top, c) * step(0.001, p.clouds);
float sunOnCloud = clamp(sunUp * 1.2 + 0.1, 0.0, 1.0);
float3 cloudCol = float3(0.95, 0.94, 0.92) * sunLight * sunOnCloud;
col = mix(col, cloudCol + 0.01, cover * 0.9);
// Cities: specks of light on the low land, only where it is night.
float speck = smoothstep(0.7, 0.95, valueNoise3(d * 240.0));
speck *= smoothstep(0.5, 0.8, valueNoise3(d * 11.0));
float towns = speck * land * (1.0 - smoothstep(0.15, 0.3, above));
towns *= (1.0 - ice) * (1.0 - day) * (1.0 - cover);
col += kiln(0.78) * towns * p.lights * 2.8;
// The air: more of it is seen along the rim, lit by day, ember at dusk.
float rim = pow(1.0 - max(dot(ballN, toEye), 0.0), 3.0);
float dusk = exp(-sunUp * sunUp / 0.03);
col += airColour(sunUp) * rim * (day * 0.9 + dusk * 0.6);
col += kiln(0.6) * dusk * 0.03;
return float4(col, 1.0);
}spinning-planet.metal, the whole file89 new lines since stage 4
// Ray Kiln · Spinning Planet
//
// @episode Spinning Planet
// @length 60
// @variation 30
// @variations 1,2,3,5,6,8,9,12
// @still 14.0
// @teaches A grid wrapped into a sphere, continents from noise, an atmosphere rim
// @category Vertex
// @tags vertex shaders, grids, spheres, value noise, fractal noise, displacement, atmosphere, stars, procedural sound
// @final Spinning planet | Continents and clouds turn under a low sun, cities light the night side, and the air glows at the rim.
//
// A planet turning in the dark. The host lays down a grid of squares; `corner` wraps it round a ball, the grid's
// across becoming longitude and its down latitude, so the grid's left and right edges meet in a seam from pole to
// pole. The land is noise, but noise read at the point on the ball, in three dimensions, not at the grid's place, so
// the two sides of the seam read the same noise and close without a crack. Where the noise is above the sea, the
// ground is lifted into mountains; each corner's normal comes from its neighbours on the ball. `shade` colours sea,
// land, ice and cloud, lights them by a low sun, and lights cities on the night side; `background` draws the stars
// and the glow of the air around the edge.
// 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
//
// @grid 256
// @param speed 1.0 0.0 3.0 How fast the planet turns: once in 30 s at 1
// @param sea 0.52 0.3 0.75 Sea level: low drains the oceans, high leaves islands
// @param relief 0.05 0.0 0.15 How tall the mountains stand off the ball
// @param clouds 0.5 0.0 1.0 How much of the sky is cloud
// @param lights 0.7 0.0 1.0 How bright the cities are on the night side
// @param volume 0.8 0.0 1.0 Loudness of the sound
constant float PI = 3.1415927;
constant float TAU = 6.2831853;
// Seconds for one turn at speed 1, and the lean of the planet's axis.
constant float PERIOD = 30.0;
constant float TILT = 0.41;
// How far the eye is from the planet's centre, and how much the camera
// magnifies.
constant float DISTANCE = 3.4;
constant float ZOOM = 2.4;
// How far apart, on the ball, a corner's neighbours are asked for.
constant float NEAR = 0.003;
// What each corner carries to the pixels: where it lands on screen, where it
// is in the world, which way the ground faces, and its point on the planet
// before it is turned (for the colours, which turn with it).
struct Vertex {
float4 position [[position]];
float3 world;
float3 normal;
float3 planet;
};
// Cracked Clay's hash: a whole number in, a random 0 to 1 out.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// Flowing Color Field's palette: near-black, ember, orange, gold, white-hot.
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));
}
// Rippling Water's noise from joined random values, `rate` times a second.
float noise1(float t, float rate, uint salt) {
float x = t * rate;
float i = floor(x);
float f = x - i;
f = f * f * (3.0 - 2.0 * f);
uint n = uint(int(i) + 1000000);
return mix(hashU(n * 31u + salt), hashU((n + 1u) * 31u + salt), f);
}
// A random 0 to 1 for each whole-numbered point in space.
float hash3(float3 c) {
uint x = uint(int(c.x) + 4096);
uint y = uint(int(c.y) + 4096);
uint z = uint(int(c.z) + 4096);
return hashU(x * 73856093u ^ y * 19349663u ^ z * 83492791u);
}
// Value noise in three dimensions: a random value at every whole-numbered
// point, blended smoothly across the cube between eight of them.
float valueNoise3(float3 x) {
float3 i = floor(x);
float3 f = x - i;
f = f * f * (3.0 - 2.0 * f);
float a = mix(hash3(i), hash3(i + float3(1, 0, 0)), f.x);
float b = mix(hash3(i + float3(0, 1, 0)), hash3(i + float3(1, 1, 0)), f.x);
float c = mix(hash3(i + float3(0, 0, 1)), hash3(i + float3(1, 0, 1)), f.x);
float d = mix(hash3(i + float3(0, 1, 1)), hash3(i + float3(1, 1, 1)), f.x);
return mix(mix(a, b, f.y), mix(c, d, f.y), f.z);
}
// Fractal noise: `octaves` layers of value noise, each twice as fine and half
// as strong as the one before, shifted so their grids don't line up. 0 to 1.
float fbm3(float3 x, int octaves) {
float sum = 0.0, amp = 0.5, total = 0.0;
for (int k = 0; k < octaves; k++) {
sum += amp * valueNoise3(x);
total += amp;
x = x * 2.03 + float3(1.7, 9.2, 4.1);
amp *= 0.5;
}
return sum / total;
}
// The ground's height at a point on the planet (a direction from its centre):
// big continents from coarse noise, made rough by the finer layers.
float landHeight(float3 d) {
return fbm3(d * 1.7 + float3(3.0, 1.0, 7.0), 7);
}
// A point on the grid as a direction from the centre: across is longitude,
// once round; down is latitude, from the south pole to the north.
float3 onBall(float2 grid) {
float lon = grid.x * TAU;
float lat = (grid.y - 0.5) * PI;
float c = cos(lat);
return float3(c * cos(lon), sin(lat), c * sin(lon));
}
// The planet turned to now: spun about its axis, then the axis leaned.
float3 turn(float3 d, float t) {
float s = t * TAU / PERIOD;
float cs = cos(s), sn = sin(s);
float3 r = float3(cs * d.x - sn * d.z, d.y, sn * d.x + cs * d.z);
float ct = cos(TILT), st = sin(TILT);
return float3(ct * r.x - st * r.y, st * r.x + ct * r.y, r.z);
}
// Where the eye is: in front of the planet, leaned by the pointer (the
// middle of the picture looks straight on).
float3 eyeAt(constant Uniforms& u) {
float2 m = u.mouse / max(u.resolution, float2(1.0)) - 0.5;
float yaw = -m.x * 1.4, pitch = -m.y * 0.9;
float c = cos(pitch);
return DISTANCE * float3(sin(yaw) * c, sin(pitch), -cos(yaw) * c);
}
// The camera's three directions, looking from the eye at the centre.
float3x3 cameraAxes(float3 eye) {
float3 f = normalize(-eye);
float3 r = normalize(cross(float3(0, 1, 0), f));
return float3x3(r, cross(f, r), f);
}
// A world point on screen, as Flag in the Wind's; a tall picture zooms out.
float4 project(float3 world, float3 eye, float aspect) {
float zoom = ZOOM * min(aspect, 1.0);
float3x3 axes = cameraAxes(eye);
float3 v = world - eye;
float3 view = float3(dot(v, axes[0]), dot(v, axes[1]), dot(v, axes[2]));
float near = 0.1, far = 30.0;
float depth = (view.z - near) / (far - near) * view.z;
return float4(view.x * zoom / aspect, view.y * zoom, depth, view.z);
}
// The ground at a point on the planet: the ball, lifted by the land's height
// above the sea, so the sea stays a smooth ball and the mountains stand out.
float3 groundAt(float3 d, constant Params& p) {
float h = landHeight(d);
return d * (1.0 + p.relief * max(h - p.sea, 0.0) * 2.0);
}
// Once for every corner: its point on the ball, the ground there, and the
// ground's normal from two neighbours each way along the ball's surface, in
// directions made from the point itself, so there is no trouble at the poles
// where the grid's squares shrink to nothing.
Vertex corner(float2 grid, constant Uniforms& u, constant Params& p) {
float t = u.time * p.speed;
float3 d = onBall(grid);
float3 helper = abs(d.y) < 0.9 ? float3(0, 1, 0) : float3(1, 0, 0);
float3 e1 = normalize(cross(helper, d));
float3 e2 = cross(d, e1);
float3 a = groundAt(normalize(d + e1 * NEAR), p)
- groundAt(normalize(d - e1 * NEAR), p);
float3 b = groundAt(normalize(d + e2 * NEAR), p)
- groundAt(normalize(d - e2 * NEAR), p);
float3 n = normalize(cross(a, b));
if (dot(n, d) < 0.0) n = -n;
float3 eye = eyeAt(u);
Vertex o;
o.world = turn(groundAt(d, p), t);
o.position = project(o.world, eye, u.resolution.x / u.resolution.y);
o.normal = turn(n, t);
o.planet = d;
return o;
}
// The sun: left, a little above and in front; night is on the right.
constant float3 SUN = float3(-0.88, 0.3, -0.3);
// The colour of the air: blue-white by day, ember where the sun is low.
float3 airColour(float day) {
return mix(kiln(0.55), float3(0.45, 0.62, 1.0), smoothstep(0.0, 0.5, day));
}
// Once for every pixel of the planet. The colour comes from the noise read
// again here, per pixel, so coasts stay sharp however coarse the grid: deep
// and shallow sea, lowland, highland, rock and snow, and ice toward the poles.
// The sun lights it, the sea shines where it reflects the sun, cloud drifts
// over it, cities light the night side, and the air glows at the rim.
float4 shade(Vertex v, constant Uniforms& u, constant Params& p) {
float t = u.time * p.speed;
float3 d = normalize(v.planet);
float3 n = normalize(v.normal);
float3 eye = eyeAt(u);
float3 toEye = normalize(eye - v.world);
float3 sun = normalize(SUN);
float h = landHeight(d);
float land = smoothstep(p.sea - 0.004, p.sea + 0.004, h);
float above = max(h - p.sea, 0.0) / max(1.0 - p.sea, 0.05);
float below = max(p.sea - h, 0.0) / max(p.sea, 0.05);
float3 shallow = float3(0.03, 0.16, 0.22);
float3 deep = float3(0.005, 0.025, 0.07);
float3 sea = mix(shallow, deep, smoothstep(0.0, 0.25, below));
float3 low = float3(0.16, 0.22, 0.07), high = float3(0.40, 0.29, 0.14);
float3 soil = mix(low, high, smoothstep(0.02, 0.18, above));
soil = mix(soil, float3(0.30, 0.26, 0.24), smoothstep(0.2, 0.35, above));
float lat = abs(d.y);
float snow = smoothstep(0.36, 0.42, above)
+ smoothstep(0.80, 0.86, lat + 0.1 * above);
soil = mix(soil, float3(0.92, 0.94, 0.96), clamp(snow, 0.0, 1.0));
float3 albedo = mix(sea, soil, land);
float ice = smoothstep(0.88, 0.92, lat + 0.05 * (h - 0.5));
albedo = mix(albedo, float3(0.85, 0.9, 0.95), ice);
float3 ballN = normalize(v.world);
float sunUp = dot(ballN, sun);
float day = smoothstep(-0.12, 0.25, sunUp);
float3 sunLight = kiln(0.9) * 1.25;
float3 col = albedo * sunLight * max(dot(n, sun), 0.0) * day;
col += albedo * 0.02;
float glint = pow(max(dot(reflect(-sun, ballN), toEye), 0.0), 60.0);
col += kiln(0.88) * glint * (1.0 - land) * (1.0 - ice) * day * 0.9;
// Cloud: a fractal noise, turned slowly against the ground so it drifts.
float drift = t * 0.012;
float cs = cos(drift), sn = sin(drift);
float3 cd = float3(cs * d.x - sn * d.z, d.y, sn * d.x + cs * d.z);
float c = fbm3(cd * 3.0 + float3(t * 0.01, 0.0, 0.0), 5);
float top = 1.0 - 0.45 * p.clouds;
float cover = smoothstep(top - 0.25, top, c) * step(0.001, p.clouds);
float sunOnCloud = clamp(sunUp * 1.2 + 0.1, 0.0, 1.0);
float3 cloudCol = float3(0.95, 0.94, 0.92) * sunLight * sunOnCloud;
col = mix(col, cloudCol + 0.01, cover * 0.9);
// Cities: specks of light on the low land, only where it is night.
float speck = smoothstep(0.7, 0.95, valueNoise3(d * 240.0));
speck *= smoothstep(0.5, 0.8, valueNoise3(d * 11.0));
float towns = speck * land * (1.0 - smoothstep(0.15, 0.3, above));
towns *= (1.0 - ice) * (1.0 - day) * (1.0 - cover);
col += kiln(0.78) * towns * p.lights * 2.8;
// The air: more of it is seen along the rim, lit by day, ember at dusk.
float rim = pow(1.0 - max(dot(ballN, toEye), 0.0), 3.0);
float dusk = exp(-sunUp * sunUp / 0.03);
col += airColour(sunUp) * rim * (day * 0.9 + dusk * 0.6);
col += kiln(0.6) * dusk * 0.03;
return float4(col, 1.0);
}
// Behind the planet: stars, and the air around its edge. The ray through the
// pixel passes the centre at some closest distance; just outside 1 (the
// ball's radius) the air glows, fading over a few hundredths, brighter on
// the side the sun is on and ember where it turns to night.
float4 background(float2 uv, constant Uniforms& u, constant Params& p) {
float3 eye = eyeAt(u);
float aspect = u.resolution.x / u.resolution.y;
float3x3 axes = cameraAxes(eye);
float2 s = (uv * 2.0 - 1.0) / (ZOOM * min(aspect, 1.0));
float3 dir = normalize(axes[2] + axes[0] * s.x * aspect + axes[1] * s.y);
// Stars: a grid of cells over the sky's directions, a star in some.
float2 sph = float2(atan2(dir.z, dir.x), asin(clamp(dir.y, -1.0, 1.0)));
sph *= 70.0;
float2 cell = floor(sph);
uint k = uint(int(cell.x) + 1000) * 7919u;
k += uint(int(cell.y) + 1000) * 104729u;
float2 at = float2(hashU(k), hashU(k + 1u)) * 0.8 + 0.1;
float star = step(0.82, hashU(k + 2u));
star *= smoothstep(0.09, 0.0, length(fract(sph) - at));
float bright = pow(hashU(k + 3u), 3.0);
float3 tint = mix(float3(0.7, 0.8, 1.0), kiln(0.85), hashU(k + 4u));
float3 col = float3(0.004, 0.003, 0.006);
col += tint * star * (0.25 + 1.2 * bright);
// The air outside the edge.
float along = dot(-eye, dir);
float3 closest = eye + dir * along;
float r = length(closest);
if (along > 0.0) {
float3 sun = normalize(SUN);
float sunUp = dot(closest / r, sun);
float lit = smoothstep(-0.35, 0.3, sunUp);
float dusk = exp(-sunUp * sunUp / 0.02);
float glow = exp(-max(r - 1.0, 0.0) / 0.035) * step(1.0, r);
float3 air = airColour(sunUp) * lit + kiln(0.5) * dusk * 0.3;
col = col * (1.0 - glow * lit) + air * glow * 0.9;
}
return float4(col, 1.0);
}
// ---- Sound -----------------------------------------------------------------------------------------------------
// The sound is part of the shader: a function of time, run on the GPU 48,000 times a second and put in the video.
// How much land faces the camera at time t: the planet's front meridian, read
// at three latitudes, before it is turned (the lean doesn't move the front).
float landInFront(float t, constant Params& p) {
float s = t * TAU / PERIOD;
float lon = -PI * 0.5 - s;
float sum = 0.0;
for (int i = -1; i <= 1; i++) {
float lat = float(i) * 0.45;
float c = cos(lat);
float3 d = float3(c * cos(lon), sin(lat), c * sin(lon));
sum += smoothstep(p.sea - 0.03, p.sea + 0.03, landHeight(d));
}
return sum / 3.0;
}
// A drone in D that the planet colours as it turns: over sea it is a soft
// fifth with a slow wash of surf; as land comes round to the front, brighter
// partials join it. The seam where the grid closes rings a bell each time it
// passes the front of the planet.
float2 sound(float t, constant Params& p) {
float tt = t * p.speed;
float land = landInFront(tt, p);
float wob = 0.003 * sin(TAU * 0.07 * t);
float2 v = float2(0.0);
v += float2(1.0, 0.9) * sin(TAU * 73.42 * t);
v += float2(0.9, 1.0) * sin(TAU * 110.0 * (1.0 + wob) * t) * 0.7;
v += float2(1.0, 1.0) * sin(TAU * 146.83 * (1.0 - wob) * t) * 0.4;
float bright = 0.15 + 0.85 * land;
float vib = 0.6 * sin(TAU * 0.5 * t);
v += float2(0.8, 1.0) * sin(TAU * 220.0 * t + vib) * 0.3 * bright;
float b2 = bright * bright;
v += float2(1.0, 0.8) * sin(TAU * 293.66 * t) * 0.22 * b2;
v += float2(0.9, 0.9) * sin(TAU * 440.0 * (1.0 + wob) * t) * 0.10 * b2;
v *= 0.135;
// Surf over the sea: dark noise, swelling every eight seconds or so.
float surf = noise1(t, 600.0, 3u) - 0.5;
surf += 0.7 * (noise1(t, 170.0, 9u) - 0.5);
float swell = 0.5 + 0.5 * sin(TAU * t / 8.0 + 1.3 * noise1(t, 0.2, 5u));
float lean = noise1(t, 0.15, 13u);
surf *= 0.11 * (1.0 - land) * (0.3 + 0.7 * swell);
v += float2(0.6 + 0.4 * lean, 1.0 - 0.4 * lean) * surf;
// The bell: the seam faces us when the turn is three quarters round.
if (p.speed > 0.01) {
float turns = tt / PERIOD - 0.75;
float age = (turns - floor(turns)) * PERIOD / p.speed;
if (tt / PERIOD >= 0.75) {
float ring = sin(TAU * 587.33 * age);
ring += 0.45 * sin(TAU * 1480.0 * age);
ring += 0.25 * sin(TAU * 2349.3 * age);
ring *= exp(-age / 1.4) * (1.0 - exp(-age / 0.004));
v += float2(0.75, 0.55) * ring * 0.16;
}
}
float fadeIn = smoothstep(0.0, 2.0, t);
return tanh(v * 1.6) / 1.6 * fadeIn * p.volume;
}4 lines from stage 4 that are gone
float3 eye = float3(0.0, 0.0, -DISTANCE);
⋯
float3 eye = float3(0.0, 0.0, -DISTANCE);
⋯
float3 col = albedo * kiln(0.9) * 1.25 * max(dot(n, sun), 0.0) * day;
⋯
float3 eye = float3(0.0, 0.0, -DISTANCE);The sound
The sound is a drone in D that the planet colours as it turns. Under it are D2 (73.42 Hz), A2 (110 Hz) and D3 (146.83 Hz), the A and the upper D drifting a few cents apart and back every 14 seconds or so. Over them are A3, D4 and A4, and how much of them is heard depends on the land: landInFront reads landHeight on the meridian that faces the camera at that moment, at the equator and 0.45 radians either side, and the more of those three points is land, the brighter the drone. Where the front of the planet is sea, the drone is soft and a wash of surf (smooth noise joined 600 and 170 times a second) rises and falls every eight seconds or so, drifting between the ears. Change sea and the drone changes with it.
Once a turn, when the seam faces the camera (22.5 and 52.5 seconds at speed 1), a bell rings left of centre: D5 (587.33 Hz) with two partials above it, out of tune with each other as a real bell's are, dying over about a second and a half. It all goes through tanh and fades in over the first two seconds. At the defaults the peak is about -9.9 dBFS and the average (RMS) about -20.7 dBFS.
New in Ray Kiln
Seams in the wireframe. The wireframe now draws the grid's border, the edges where grid.x or grid.y is 0 or 1, in gold at full strength, and the rest in cream as before. The border edges are the seams: on a flat sheet they are its outline, but on a grid wrapped into a sphere or a tube they are where its two ends meet, so a gap or an overlap there shows as a gold line that doesn't close. Unlike the other edges, a seam never fades out when it is only a few pixels long on screen, since it is one line however fine the grid. It is in the playground's Wireframe button and the Mac app's Shader > Show Wireframe (with its next release), and raykiln render … --wireframe draws it in a still.
Open Spinning Planet in the playground, turn on Wireframe and drag Grid down to 16. The gold meridian is the seam, leaning with the axis; it comes round the front at 22.5 seconds, the moment the bell rings. The top and bottom rows shrink to the poles, so their gold edges are too short to see. Then make the gap the next section describes, and watch the gold line split in two.
Try this
- With Wireframe on, in
onBallchangegrid.x * TAUtogrid.x * TAU * 0.9: the ball no longer closes. A slice of it goes missing between two gold meridians, and through the gap you see the inside of the far side. - Set
seato 0.3: the oceans drain to lakes. Set it to 0.75: a few islands and a wide sea that catches the sun. - Set
reliefto 0.15: the mountains stand well off the ball, and the outline goes rough. - In
landHeight, change the7to2: only the two coarsest layers of noise are left, and the continents turn to smooth blobs. - Set
TILT(at the top) to0.0: the axis stands upright and the poles hide at the top and bottom edge. - Set
cloudsto 0 andlightsto 1: the whole night side shows its cities.
Parameters
Pick a parameter and drag the slider. Each frame is the same moment of the shader, with only that one setting changed.
| Parameter | Default | Range | What it does |
|---|---|---|---|
speed | 1.0 | 0.0 to 3.0 | How fast the planet turns: once in 30 s at 1 |
sea | 0.52 | 0.3 to 0.75 | Sea level: low drains the oceans, high leaves islands |
relief | 0.05 | 0.0 to 0.15 | How tall the mountains stand off the ball |
clouds | 0.5 | 0.0 to 1.0 | How much of the sky is cloud |
lights | 0.7 | 0.0 to 1.0 | How bright the cities are on the night side |
volume | 0.8 | 0.0 to 1.0 | Loudness of the sound |
More from this shader
The same shader 8 more ways: each from a different moment, with different settings, one after another. Each chapter below says which settings moved most, then lists them all.
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.
Made from this episode
The full source
This is the whole episode: the picture, the parameters and the sound.
spinning-planet.metal
// Ray Kiln · Spinning Planet
//
// @episode Spinning Planet
// @length 60
// @variation 30
// @variations 1,2,3,5,6,8,9,12
// @still 14.0
// @teaches A grid wrapped into a sphere, continents from noise, an atmosphere rim
// @category Vertex
// @tags vertex shaders, grids, spheres, value noise, fractal noise, displacement, atmosphere, stars, procedural sound
// @final Spinning planet | Continents and clouds turn under a low sun, cities light the night side, and the air glows at the rim.
//
// A planet turning in the dark. The host lays down a grid of squares; `corner` wraps it round a ball, the grid's
// across becoming longitude and its down latitude, so the grid's left and right edges meet in a seam from pole to
// pole. The land is noise, but noise read at the point on the ball, in three dimensions, not at the grid's place, so
// the two sides of the seam read the same noise and close without a crack. Where the noise is above the sea, the
// ground is lifted into mountains; each corner's normal comes from its neighbours on the ball. `shade` colours sea,
// land, ice and cloud, lights them by a low sun, and lights cities on the night side; `background` draws the stars
// and the glow of the air around the edge.
// 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
//
// @grid 256
// @param speed 1.0 0.0 3.0 How fast the planet turns: once in 30 s at 1
// @param sea 0.52 0.3 0.75 Sea level: low drains the oceans, high leaves islands
// @param relief 0.05 0.0 0.15 How tall the mountains stand off the ball
// @param clouds 0.5 0.0 1.0 How much of the sky is cloud
// @param lights 0.7 0.0 1.0 How bright the cities are on the night side
// @param volume 0.8 0.0 1.0 Loudness of the sound
constant float PI = 3.1415927;
constant float TAU = 6.2831853;
// Seconds for one turn at speed 1, and the lean of the planet's axis.
constant float PERIOD = 30.0;
constant float TILT = 0.41;
// How far the eye is from the planet's centre, and how much the camera
// magnifies.
constant float DISTANCE = 3.4;
constant float ZOOM = 2.4;
// How far apart, on the ball, a corner's neighbours are asked for.
constant float NEAR = 0.003;
// What each corner carries to the pixels: where it lands on screen, where it
// is in the world, which way the ground faces, and its point on the planet
// before it is turned (for the colours, which turn with it).
struct Vertex {
float4 position [[position]];
float3 world;
float3 normal;
float3 planet;
};
// Cracked Clay's hash: a whole number in, a random 0 to 1 out.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// Flowing Color Field's palette: near-black, ember, orange, gold, white-hot.
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));
}
// Rippling Water's noise from joined random values, `rate` times a second.
float noise1(float t, float rate, uint salt) {
float x = t * rate;
float i = floor(x);
float f = x - i;
f = f * f * (3.0 - 2.0 * f);
uint n = uint(int(i) + 1000000);
return mix(hashU(n * 31u + salt), hashU((n + 1u) * 31u + salt), f);
}
// A random 0 to 1 for each whole-numbered point in space.
float hash3(float3 c) {
uint x = uint(int(c.x) + 4096);
uint y = uint(int(c.y) + 4096);
uint z = uint(int(c.z) + 4096);
return hashU(x * 73856093u ^ y * 19349663u ^ z * 83492791u);
}
// Value noise in three dimensions: a random value at every whole-numbered
// point, blended smoothly across the cube between eight of them.
float valueNoise3(float3 x) {
float3 i = floor(x);
float3 f = x - i;
f = f * f * (3.0 - 2.0 * f);
float a = mix(hash3(i), hash3(i + float3(1, 0, 0)), f.x);
float b = mix(hash3(i + float3(0, 1, 0)), hash3(i + float3(1, 1, 0)), f.x);
float c = mix(hash3(i + float3(0, 0, 1)), hash3(i + float3(1, 0, 1)), f.x);
float d = mix(hash3(i + float3(0, 1, 1)), hash3(i + float3(1, 1, 1)), f.x);
return mix(mix(a, b, f.y), mix(c, d, f.y), f.z);
}
// Fractal noise: `octaves` layers of value noise, each twice as fine and half
// as strong as the one before, shifted so their grids don't line up. 0 to 1.
float fbm3(float3 x, int octaves) {
float sum = 0.0, amp = 0.5, total = 0.0;
for (int k = 0; k < octaves; k++) {
sum += amp * valueNoise3(x);
total += amp;
x = x * 2.03 + float3(1.7, 9.2, 4.1);
amp *= 0.5;
}
return sum / total;
}
// The ground's height at a point on the planet (a direction from its centre):
// big continents from coarse noise, made rough by the finer layers.
float landHeight(float3 d) {
return fbm3(d * 1.7 + float3(3.0, 1.0, 7.0), 7);
}
// A point on the grid as a direction from the centre: across is longitude,
// once round; down is latitude, from the south pole to the north.
float3 onBall(float2 grid) {
float lon = grid.x * TAU;
float lat = (grid.y - 0.5) * PI;
float c = cos(lat);
return float3(c * cos(lon), sin(lat), c * sin(lon));
}
// The planet turned to now: spun about its axis, then the axis leaned.
float3 turn(float3 d, float t) {
float s = t * TAU / PERIOD;
float cs = cos(s), sn = sin(s);
float3 r = float3(cs * d.x - sn * d.z, d.y, sn * d.x + cs * d.z);
float ct = cos(TILT), st = sin(TILT);
return float3(ct * r.x - st * r.y, st * r.x + ct * r.y, r.z);
}
// Where the eye is: in front of the planet, leaned by the pointer (the
// middle of the picture looks straight on).
float3 eyeAt(constant Uniforms& u) {
float2 m = u.mouse / max(u.resolution, float2(1.0)) - 0.5;
float yaw = -m.x * 1.4, pitch = -m.y * 0.9;
float c = cos(pitch);
return DISTANCE * float3(sin(yaw) * c, sin(pitch), -cos(yaw) * c);
}
// The camera's three directions, looking from the eye at the centre.
float3x3 cameraAxes(float3 eye) {
float3 f = normalize(-eye);
float3 r = normalize(cross(float3(0, 1, 0), f));
return float3x3(r, cross(f, r), f);
}
// A world point on screen, as Flag in the Wind's; a tall picture zooms out.
float4 project(float3 world, float3 eye, float aspect) {
float zoom = ZOOM * min(aspect, 1.0);
float3x3 axes = cameraAxes(eye);
float3 v = world - eye;
float3 view = float3(dot(v, axes[0]), dot(v, axes[1]), dot(v, axes[2]));
float near = 0.1, far = 30.0;
float depth = (view.z - near) / (far - near) * view.z;
return float4(view.x * zoom / aspect, view.y * zoom, depth, view.z);
}
// The ground at a point on the planet: the ball, lifted by the land's height
// above the sea, so the sea stays a smooth ball and the mountains stand out.
float3 groundAt(float3 d, constant Params& p) {
float h = landHeight(d);
return d * (1.0 + p.relief * max(h - p.sea, 0.0) * 2.0);
}
// Once for every corner: its point on the ball, the ground there, and the
// ground's normal from two neighbours each way along the ball's surface, in
// directions made from the point itself, so there is no trouble at the poles
// where the grid's squares shrink to nothing.
Vertex corner(float2 grid, constant Uniforms& u, constant Params& p) {
float t = u.time * p.speed;
float3 d = onBall(grid);
float3 helper = abs(d.y) < 0.9 ? float3(0, 1, 0) : float3(1, 0, 0);
float3 e1 = normalize(cross(helper, d));
float3 e2 = cross(d, e1);
float3 a = groundAt(normalize(d + e1 * NEAR), p)
- groundAt(normalize(d - e1 * NEAR), p);
float3 b = groundAt(normalize(d + e2 * NEAR), p)
- groundAt(normalize(d - e2 * NEAR), p);
float3 n = normalize(cross(a, b));
if (dot(n, d) < 0.0) n = -n;
float3 eye = eyeAt(u);
Vertex o;
o.world = turn(groundAt(d, p), t);
o.position = project(o.world, eye, u.resolution.x / u.resolution.y);
o.normal = turn(n, t);
o.planet = d;
return o;
}
// The sun: left, a little above and in front; night is on the right.
constant float3 SUN = float3(-0.88, 0.3, -0.3);
// The colour of the air: blue-white by day, ember where the sun is low.
float3 airColour(float day) {
return mix(kiln(0.55), float3(0.45, 0.62, 1.0), smoothstep(0.0, 0.5, day));
}
// Once for every pixel of the planet. The colour comes from the noise read
// again here, per pixel, so coasts stay sharp however coarse the grid: deep
// and shallow sea, lowland, highland, rock and snow, and ice toward the poles.
// The sun lights it, the sea shines where it reflects the sun, cloud drifts
// over it, cities light the night side, and the air glows at the rim.
float4 shade(Vertex v, constant Uniforms& u, constant Params& p) {
float t = u.time * p.speed;
float3 d = normalize(v.planet);
float3 n = normalize(v.normal);
float3 eye = eyeAt(u);
float3 toEye = normalize(eye - v.world);
float3 sun = normalize(SUN);
float h = landHeight(d);
float land = smoothstep(p.sea - 0.004, p.sea + 0.004, h);
float above = max(h - p.sea, 0.0) / max(1.0 - p.sea, 0.05);
float below = max(p.sea - h, 0.0) / max(p.sea, 0.05);
float3 shallow = float3(0.03, 0.16, 0.22);
float3 deep = float3(0.005, 0.025, 0.07);
float3 sea = mix(shallow, deep, smoothstep(0.0, 0.25, below));
float3 low = float3(0.16, 0.22, 0.07), high = float3(0.40, 0.29, 0.14);
float3 soil = mix(low, high, smoothstep(0.02, 0.18, above));
soil = mix(soil, float3(0.30, 0.26, 0.24), smoothstep(0.2, 0.35, above));
float lat = abs(d.y);
float snow = smoothstep(0.36, 0.42, above)
+ smoothstep(0.80, 0.86, lat + 0.1 * above);
soil = mix(soil, float3(0.92, 0.94, 0.96), clamp(snow, 0.0, 1.0));
float3 albedo = mix(sea, soil, land);
float ice = smoothstep(0.88, 0.92, lat + 0.05 * (h - 0.5));
albedo = mix(albedo, float3(0.85, 0.9, 0.95), ice);
float3 ballN = normalize(v.world);
float sunUp = dot(ballN, sun);
float day = smoothstep(-0.12, 0.25, sunUp);
float3 sunLight = kiln(0.9) * 1.25;
float3 col = albedo * sunLight * max(dot(n, sun), 0.0) * day;
col += albedo * 0.02;
float glint = pow(max(dot(reflect(-sun, ballN), toEye), 0.0), 60.0);
col += kiln(0.88) * glint * (1.0 - land) * (1.0 - ice) * day * 0.9;
// Cloud: a fractal noise, turned slowly against the ground so it drifts.
float drift = t * 0.012;
float cs = cos(drift), sn = sin(drift);
float3 cd = float3(cs * d.x - sn * d.z, d.y, sn * d.x + cs * d.z);
float c = fbm3(cd * 3.0 + float3(t * 0.01, 0.0, 0.0), 5);
float top = 1.0 - 0.45 * p.clouds;
float cover = smoothstep(top - 0.25, top, c) * step(0.001, p.clouds);
float sunOnCloud = clamp(sunUp * 1.2 + 0.1, 0.0, 1.0);
float3 cloudCol = float3(0.95, 0.94, 0.92) * sunLight * sunOnCloud;
col = mix(col, cloudCol + 0.01, cover * 0.9);
// Cities: specks of light on the low land, only where it is night.
float speck = smoothstep(0.7, 0.95, valueNoise3(d * 240.0));
speck *= smoothstep(0.5, 0.8, valueNoise3(d * 11.0));
float towns = speck * land * (1.0 - smoothstep(0.15, 0.3, above));
towns *= (1.0 - ice) * (1.0 - day) * (1.0 - cover);
col += kiln(0.78) * towns * p.lights * 2.8;
// The air: more of it is seen along the rim, lit by day, ember at dusk.
float rim = pow(1.0 - max(dot(ballN, toEye), 0.0), 3.0);
float dusk = exp(-sunUp * sunUp / 0.03);
col += airColour(sunUp) * rim * (day * 0.9 + dusk * 0.6);
col += kiln(0.6) * dusk * 0.03;
return float4(col, 1.0);
}
// Behind the planet: stars, and the air around its edge. The ray through the
// pixel passes the centre at some closest distance; just outside 1 (the
// ball's radius) the air glows, fading over a few hundredths, brighter on
// the side the sun is on and ember where it turns to night.
float4 background(float2 uv, constant Uniforms& u, constant Params& p) {
float3 eye = eyeAt(u);
float aspect = u.resolution.x / u.resolution.y;
float3x3 axes = cameraAxes(eye);
float2 s = (uv * 2.0 - 1.0) / (ZOOM * min(aspect, 1.0));
float3 dir = normalize(axes[2] + axes[0] * s.x * aspect + axes[1] * s.y);
// Stars: a grid of cells over the sky's directions, a star in some.
float2 sph = float2(atan2(dir.z, dir.x), asin(clamp(dir.y, -1.0, 1.0)));
sph *= 70.0;
float2 cell = floor(sph);
uint k = uint(int(cell.x) + 1000) * 7919u;
k += uint(int(cell.y) + 1000) * 104729u;
float2 at = float2(hashU(k), hashU(k + 1u)) * 0.8 + 0.1;
float star = step(0.82, hashU(k + 2u));
star *= smoothstep(0.09, 0.0, length(fract(sph) - at));
float bright = pow(hashU(k + 3u), 3.0);
float3 tint = mix(float3(0.7, 0.8, 1.0), kiln(0.85), hashU(k + 4u));
float3 col = float3(0.004, 0.003, 0.006);
col += tint * star * (0.25 + 1.2 * bright);
// The air outside the edge.
float along = dot(-eye, dir);
float3 closest = eye + dir * along;
float r = length(closest);
if (along > 0.0) {
float3 sun = normalize(SUN);
float sunUp = dot(closest / r, sun);
float lit = smoothstep(-0.35, 0.3, sunUp);
float dusk = exp(-sunUp * sunUp / 0.02);
float glow = exp(-max(r - 1.0, 0.0) / 0.035) * step(1.0, r);
float3 air = airColour(sunUp) * lit + kiln(0.5) * dusk * 0.3;
col = col * (1.0 - glow * lit) + air * glow * 0.9;
}
return float4(col, 1.0);
}
// ---- Sound -----------------------------------------------------------------------------------------------------
// The sound is part of the shader: a function of time, run on the GPU 48,000 times a second and put in the video.
// How much land faces the camera at time t: the planet's front meridian, read
// at three latitudes, before it is turned (the lean doesn't move the front).
float landInFront(float t, constant Params& p) {
float s = t * TAU / PERIOD;
float lon = -PI * 0.5 - s;
float sum = 0.0;
for (int i = -1; i <= 1; i++) {
float lat = float(i) * 0.45;
float c = cos(lat);
float3 d = float3(c * cos(lon), sin(lat), c * sin(lon));
sum += smoothstep(p.sea - 0.03, p.sea + 0.03, landHeight(d));
}
return sum / 3.0;
}
// A drone in D that the planet colours as it turns: over sea it is a soft
// fifth with a slow wash of surf; as land comes round to the front, brighter
// partials join it. The seam where the grid closes rings a bell each time it
// passes the front of the planet.
float2 sound(float t, constant Params& p) {
float tt = t * p.speed;
float land = landInFront(tt, p);
float wob = 0.003 * sin(TAU * 0.07 * t);
float2 v = float2(0.0);
v += float2(1.0, 0.9) * sin(TAU * 73.42 * t);
v += float2(0.9, 1.0) * sin(TAU * 110.0 * (1.0 + wob) * t) * 0.7;
v += float2(1.0, 1.0) * sin(TAU * 146.83 * (1.0 - wob) * t) * 0.4;
float bright = 0.15 + 0.85 * land;
float vib = 0.6 * sin(TAU * 0.5 * t);
v += float2(0.8, 1.0) * sin(TAU * 220.0 * t + vib) * 0.3 * bright;
float b2 = bright * bright;
v += float2(1.0, 0.8) * sin(TAU * 293.66 * t) * 0.22 * b2;
v += float2(0.9, 0.9) * sin(TAU * 440.0 * (1.0 + wob) * t) * 0.10 * b2;
v *= 0.135;
// Surf over the sea: dark noise, swelling every eight seconds or so.
float surf = noise1(t, 600.0, 3u) - 0.5;
surf += 0.7 * (noise1(t, 170.0, 9u) - 0.5);
float swell = 0.5 + 0.5 * sin(TAU * t / 8.0 + 1.3 * noise1(t, 0.2, 5u));
float lean = noise1(t, 0.15, 13u);
surf *= 0.11 * (1.0 - land) * (0.3 + 0.7 * swell);
v += float2(0.6 + 0.4 * lean, 1.0 - 0.4 * lean) * surf;
// The bell: the seam faces us when the turn is three quarters round.
if (p.speed > 0.01) {
float turns = tt / PERIOD - 0.75;
float age = (turns - floor(turns)) * PERIOD / p.speed;
if (tt / PERIOD >= 0.75) {
float ring = sin(TAU * 587.33 * age);
ring += 0.45 * sin(TAU * 1480.0 * age);
ring += 0.25 * sin(TAU * 2349.3 * age);
ring *= exp(-age / 1.4) * (1.0 - exp(-age / 0.004));
v += float2(0.75, 0.55) * ring * 0.16;
}
}
float fadeIn = smoothstep(0.0, 2.0, t);
return tanh(v * 1.6) / 1.6 * 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] = [1.0, 0.52, 0.05, 0.5, 0.7, 0.8] // the @param defaults, in the order they are declared
encoder.setRenderPipelineState(pipeline)
encoder.setFragmentBytes(&uniforms, length: MemoryLayout<Uniforms>.stride, index: 0)
encoder.setFragmentBytes(¶ms, length: params.count * MemoryLayout<Float>.stride, index: 1)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)Time is in seconds, mouse is in pixels from the bottom left, and uv in the shader runs 0 to 1 with (0, 0) at the bottom left. Write the output as sRGB: the shader's numbers go to the screen as they are.
Built on later
- Black Hole · reuses 3D hash, 3D value noise
More in Geometry
Press ? for keyboard shortcuts.


