Flag in the Wind
A flag with the kiln's mark on it, flying from a pole against an evening sky. A vertex shader turns a flat grid into cloth, pinned at the pole and waving more the farther it is from it; every corner finds which way the cloth faces by asking where its neighbours are, and the low sun behind the flag shines through the folds that turn away from it.
Teaches Vertex waves, normals from neighbours, two-sided light
Builds on 05 · Cracked Clay (integer hash, sound noise), 12 · Glass Orb (the kiln's dark), 15 · Rippling Water (the camera's axes, perspective projection)
Watch
The idea
Episode 15 lifted a grid into water. A flag is the same kind of shader turned on its side: a flat grid of squares, and a corner function that moves every corner of it to where the cloth is at this moment. Nothing is simulated. A real flag's motion is mostly waves running from the pole to the free end, growing as they go, and sines that do the same read as cloth at once.
Two things are harder than in episode 15. Light needs the normal, the direction the cloth faces, at every corner, and these waves are tangled enough that working its formula out by hand would be a chore. So corner uses finite differences: it asks the same cloth function where two neighbours are, a small step along the grid each way, and crosses the two directions to them. And a flag has two sides. The normal points out of only one, so shade turns it to face the eye, and the side that faces away from the sun gets the sun through the cloth instead of none: two-sided lighting.
It is built in five steps, and each step is a shader you can run on its own.
Stage 1 · A cloth on a pole
The host lays down 16 × 16 squares, and corner maps the grid's 0 to 1 onto a flat rectangle 1.6 long and 1 tall, its left edge on the pole. Each corner carries its place on the grid along to the pixels (cloth), so shade can dye the cloth: cream, an ember band along the pole, and the kiln's mark. The mark is drawn from a distance, archDistance, a box below its centre and a circle above it; the arch's wall is where that distance is just over zero, its hot interior where it is below, from white at the floor to ember at the top, and gold rays around it come from the angle round its centre. No light yet: the cloth is its dye.
Behind it, background draws an evening sky from the height of each pixel's ray, and the pole: the ray meets a thin upright cylinder where its distance from the y axis is 0.025, a quadratic solved in one step, with a ball on top found the same way.
stage-1-cloth.metal
// Stage 1 · A cloth on a pole
// The host lays down a grid of 16 by 16 squares, two triangles each, and
// calls corner for every corner. Here each corner only moves into place on
// a flat rectangle beside a pole, and shade dyes the cloth with the mark.
//
// @grid 16
// @param sun 0.4 0.0 1.0 How high the sun is, from sunset to late afternoon
// The flag's length from the pole, its height, and where its top is.
constant float FLAG_W = 1.6;
constant float FLAG_H = 1.0;
constant float FLAG_TOP = 0.95;
// The eye, and the point it looks at.
constant float3 EYE = float3(-0.75, 0.05, -2.0);
constant float3 AIM = float3(0.72, 0.42, 0.0);
// How much the camera magnifies.
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 cloth, for the dye.
struct Vertex {
float4 position [[position]];
float3 world;
float2 cloth;
};
// Episode 1's kiln 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));
}
// The camera's three directions: right, up and forward.
float3x3 cameraAxes() {
float3 f = normalize(AIM - EYE);
float3 r = normalize(cross(float3(0, 1, 0), f));
return float3x3(r, cross(f, r), f);
}
// A point in the world as the host wants it: x and y on screen (times
// the depth), the depth from 0 to 1 for the depth test, and the depth itself.
float4 project(float3 world, float aspect) {
float3x3 axes = cameraAxes();
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: the grid's 0 to 1 becomes a flat rectangle, its
// left edge on the pole, 1.6 long and 1 tall.
Vertex corner(float2 grid, constant Uniforms& u, constant Params& p) {
float x = grid.x * FLAG_W;
float y = FLAG_TOP - FLAG_H + grid.y * FLAG_H;
Vertex o;
o.world = float3(x, y, 0.0);
o.position = project(o.world, u.resolution.x / u.resolution.y);
o.cloth = grid;
return o;
}
// The kiln's mark, as a distance: below its centre a box, above it a circle,
// so an arch. Negative inside.
float archDistance(float2 q, float r) {
if (q.y > 0.0) return length(q) - r;
return max(abs(q.x) - r, -0.22 - q.y);
}
// What the cloth is dyed: cream, an ember band along the pole, and the mark
// near the middle: a dark arch, hot inside (white at the floor, ember at the
// top), with gold rays around it.
float3 dye(float2 g) {
float3 col = float3(0.95, 0.89, 0.78);
float2 q = float2((g.x - 0.56) * FLAG_W / FLAG_H, g.y - 0.46);
float d = archDistance(q, 0.15);
float angle = atan2(q.y, q.x);
float rays = smoothstep(0.15, 0.5, cos(angle * 13.0));
rays *= smoothstep(0.06, 0.09, d) * (1.0 - smoothstep(0.17, 0.25, d));
rays *= smoothstep(-0.12, -0.02, q.y);
col = mix(col, kiln(0.62), rays);
float wall = 1.0 - smoothstep(0.045, 0.052, d);
col = mix(col, float3(0.07, 0.04, 0.04), wall);
float heat = clamp((q.y + 0.22) / 0.37, 0.0, 1.0);
float inside = 1.0 - smoothstep(-0.004, 0.003, d);
col = mix(col, kiln(0.95 - 0.55 * heat), inside);
float band = 1.0 - smoothstep(0.075, 0.08, g.x);
float edge = 1.0 - smoothstep(0.092, 0.097, g.x);
col = mix(col, kiln(0.42), band);
col = mix(col, kiln(0.66), edge - band);
return col;
}
// Once for every pixel of the cloth: its dye, as it is. No light yet.
float4 shade(Vertex v, constant Uniforms& u, constant Params& p) {
return float4(dye(v.cloth) * 0.8, 1.0);
}
// Behind the flag: an evening sky, dark overhead and gold low down, and the
// pole, found by a ray through the pixel meeting a thin upright cylinder at
// x = 0, z = 0, with a ball on top.
float4 background(float2 uv, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
float3x3 axes = cameraAxes();
float2 s = (uv * 2.0 - 1.0) / ZOOM;
float3 d = normalize(axes[2] + axes[0] * s.x * aspect + axes[1] * s.y);
float up = clamp(d.y * 1.4 + 0.25, 0.0, 1.0);
float3 col = kiln(0.72 - 0.62 * pow(up, 0.7) + 0.12 * p.sun);
// The pole: solve for where the ray is 0.025 from the y axis.
float r = 0.025;
float a = d.x * d.x + d.z * d.z;
float b = EYE.x * d.x + EYE.z * d.z;
float c = EYE.x * EYE.x + EYE.z * EYE.z - r * r;
float h = b * b - a * c;
if (h > 0.0 && (EYE + d * (-b - sqrt(h)) / a).y < 1.0) {
col = float3(0.05, 0.035, 0.035);
}
// The ball on top.
float3 ball = float3(0.0, 1.03, 0.0) - EYE;
float bb = dot(ball, d);
if (bb * bb - dot(ball, ball) + 0.045 * 0.045 > 0.0) col = kiln(0.45);
return float4(col, 1.0);
}Stage 2 · Waves
Now cloth moves each point sideways, in z. The wave is sin(k x - phase): k fits ripple waves along the flag, and phase runs on at 4 + 7 × wind a second, so the waves travel away from the pole. A second wave, 0.3 as tall, crosses it at a slant, and each takes a little of the height up the flag (g.y) into its phase, so the folds lean. Both are multiplied by how far along the flag the point is (along to the power 1.15), so the pole edge never moves and the free end flaps most. Folds take up the cloth's length, so the far end comes in a little. The grid is 64 squares a side now, enough for smooth waves; but without light the cloth still looks flat, and only its outline shows the waves.
stage-2-waves.metal15 new lines since stage 1
// Stage 2 · Waves
// Now corner moves each corner sideways, by waves that run down the flag
// from the pole. The pole edge stays put: the waves grow with the distance
// from it. Without light the cloth still looks flat: only its edges show
// the waves. The grid is finer, so the waves are smooth.
//
// @grid 64
// @param speed 1.0 0.0 3.0 How fast the wind blows the waves down the flag
// @param wind 0.65 0.0 1.0 How hard the wind blows: taller, faster waves
// @param ripple 1.4 0.5 3.0 Waves along the flag at once
// @param sun 0.4 0.0 1.0 How high the sun is, from sunset to late afternoon
constant float TAU = 6.2831853;
// The flag's length from the pole, its height, and where its top is.
constant float FLAG_W = 1.6;
constant float FLAG_H = 1.0;
constant float FLAG_TOP = 0.95;
// The eye, and the point it looks at.
constant float3 EYE = float3(-0.75, 0.05, -2.0);
constant float3 AIM = float3(0.72, 0.42, 0.0);
// How much the camera magnifies.
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 cloth, for the dye.
struct Vertex {
float4 position [[position]];
float3 world;
float2 cloth;
};
// Episode 1's kiln 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));
}
// The camera's three directions: right, up and forward.
float3x3 cameraAxes() {
float3 f = normalize(AIM - EYE);
float3 r = normalize(cross(float3(0, 1, 0), f));
return float3x3(r, cross(f, r), f);
}
// A point in the world as the host wants it: x and y on screen (times
// the depth), the depth from 0 to 1 for the depth test, and the depth itself.
float4 project(float3 world, float aspect) {
float3x3 axes = cameraAxes();
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);
}
// One point of the cloth, from its place on the grid (along, up). Waves run
// down the flag in z, a second crosses them at a slant, and both grow with
// the distance from the pole, so that edge never moves.
float3 cloth(float2 g, float t, constant Params& p) {
float along = g.x;
float x = along * FLAG_W;
float y = FLAG_TOP - FLAG_H + g.y * FLAG_H;
float phase = t * (4.0 + 7.0 * p.wind);
float k = TAU * p.ripple / FLAG_W;
float s = k * x - phase;
float amp = (0.12 + 0.22 * p.wind) * pow(along, 1.15);
float z = sin(s + 1.3 * g.y) + 0.3 * sin(1.7 * s + 1.7 - 2.4 * g.y);
z *= amp;
x -= 0.15 * amp * abs(sin(s));
return float3(x, y, z);
}
// Once for every corner: the cloth's point there.
Vertex corner(float2 grid, constant Uniforms& u, constant Params& p) {
Vertex o;
o.world = cloth(grid, u.time * p.speed, p);
o.position = project(o.world, u.resolution.x / u.resolution.y);
o.cloth = grid;
return o;
}
// The kiln's mark, as a distance: below its centre a box, above it a circle,
// so an arch. Negative inside.
float archDistance(float2 q, float r) {
if (q.y > 0.0) return length(q) - r;
return max(abs(q.x) - r, -0.22 - q.y);
}
// What the cloth is dyed: cream, an ember band along the pole, and the mark
// near the middle: a dark arch, hot inside (white at the floor, ember at the
// top), with gold rays around it.
float3 dye(float2 g) {
float3 col = float3(0.95, 0.89, 0.78);
float2 q = float2((g.x - 0.56) * FLAG_W / FLAG_H, g.y - 0.46);
float d = archDistance(q, 0.15);
float angle = atan2(q.y, q.x);
float rays = smoothstep(0.15, 0.5, cos(angle * 13.0));
rays *= smoothstep(0.06, 0.09, d) * (1.0 - smoothstep(0.17, 0.25, d));
rays *= smoothstep(-0.12, -0.02, q.y);
col = mix(col, kiln(0.62), rays);
float wall = 1.0 - smoothstep(0.045, 0.052, d);
col = mix(col, float3(0.07, 0.04, 0.04), wall);
float heat = clamp((q.y + 0.22) / 0.37, 0.0, 1.0);
float inside = 1.0 - smoothstep(-0.004, 0.003, d);
col = mix(col, kiln(0.95 - 0.55 * heat), inside);
float band = 1.0 - smoothstep(0.075, 0.08, g.x);
float edge = 1.0 - smoothstep(0.092, 0.097, g.x);
col = mix(col, kiln(0.42), band);
col = mix(col, kiln(0.66), edge - band);
return col;
}
// Once for every pixel of the cloth: its dye, as it is. No light yet.
float4 shade(Vertex v, constant Uniforms& u, constant Params& p) {
return float4(dye(v.cloth) * 0.8, 1.0);
}
// Behind the flag: an evening sky, dark overhead and gold low down, and the
// pole, found by a ray through the pixel meeting a thin upright cylinder at
// x = 0, z = 0, with a ball on top.
float4 background(float2 uv, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
float3x3 axes = cameraAxes();
float2 s = (uv * 2.0 - 1.0) / ZOOM;
float3 d = normalize(axes[2] + axes[0] * s.x * aspect + axes[1] * s.y);
float up = clamp(d.y * 1.4 + 0.25, 0.0, 1.0);
float3 col = kiln(0.72 - 0.62 * pow(up, 0.7) + 0.12 * p.sun);
// The pole: solve for where the ray is 0.025 from the y axis.
float r = 0.025;
float a = d.x * d.x + d.z * d.z;
float b = EYE.x * d.x + EYE.z * d.z;
float c = EYE.x * EYE.x + EYE.z * EYE.z - r * r;
float h = b * b - a * c;
if (h > 0.0 && (EYE + d * (-b - sqrt(h)) / a).y < 1.0) {
col = float3(0.05, 0.035, 0.035);
}
// The ball on top.
float3 ball = float3(0.0, 1.03, 0.0) - EYE;
float bb = dot(ball, d);
if (bb * bb - dot(ball, ball) + 0.045 * 0.045 > 0.0) col = kiln(0.45);
return float4(col, 1.0);
}3 lines from stage 1 that are gone
float x = grid.x * FLAG_W;
float y = FLAG_TOP - FLAG_H + grid.y * FLAG_H;
⋯
o.world = float3(x, y, 0.0);Stage 3 · Normals from neighbours
corner now calls cloth four more times, at grid ± (0.002, 0) and grid ± (0, 0.002). The two differences are the directions the cloth runs in, along and up, and their cross product is the direction it faces. The step is a fixed fraction of the cloth, not of a square, so the normal is the same however fine the grid is. shade lights the dye by the sun as much as the normal faces it, and a little by the sky everywhere.
That shows the waves at once, but it shows a problem too. The sun is low and behind the flag, so the side we see is mostly in shadow; and where a fold turns the back of the cloth towards us, it is lit as if it were the front, because the normal points out of one side only.
stage-3-normals.metal23 new lines since stage 2
// Stage 3 · Normals from neighbours
// Light needs the way the cloth faces at every corner: its normal. Waves this
// tangled have no easy formula for one, so corner asks cloth where two
// neighbours are, a small step along the grid each way, and crosses the two
// directions to them. The sun is behind the flag, so the side we see is
// mostly in shadow, and where a fold shows us the back, it is lit as if it
// were the front: the normal points out of one side only.
//
// @grid 96
// @param speed 1.0 0.0 3.0 How fast the wind blows the waves down the flag
// @param wind 0.65 0.0 1.0 How hard the wind blows: taller, faster waves
// @param ripple 1.4 0.5 3.0 Waves along the flag at once
// @param sun 0.4 0.0 1.0 How high the sun is, from sunset to late afternoon
constant float TAU = 6.2831853;
// The flag's length from the pole, its height, and where its top is.
constant float FLAG_W = 1.6;
constant float FLAG_H = 1.0;
constant float FLAG_TOP = 0.95;
// The eye, and the point it looks at.
constant float3 EYE = float3(-0.75, 0.05, -2.0);
constant float3 AIM = float3(0.72, 0.42, 0.0);
// How much the camera magnifies.
constant float ZOOM = 2.4;
// How far apart, on the grid, a corner's neighbours are asked for.
constant float NEAR = 0.002;
// What each corner carries to the pixels: where it lands on screen, where it
// is in the world, which way the cloth faces, and its place on the cloth.
struct Vertex {
float4 position [[position]];
float3 world;
float3 normal;
float2 cloth;
};
// Episode 1's kiln 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));
}
// The camera's three directions: right, up and forward.
float3x3 cameraAxes() {
float3 f = normalize(AIM - EYE);
float3 r = normalize(cross(float3(0, 1, 0), f));
return float3x3(r, cross(f, r), f);
}
// A point in the world as the host wants it: x and y on screen (times
// the depth), the depth from 0 to 1 for the depth test, and the depth itself.
float4 project(float3 world, float aspect) {
float3x3 axes = cameraAxes();
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);
}
// One point of the cloth, from its place on the grid (along, up). Waves run
// down the flag in z, a second crosses them at a slant, and both grow with
// the distance from the pole, so that edge never moves.
float3 cloth(float2 g, float t, constant Params& p) {
float along = g.x;
float x = along * FLAG_W;
float y = FLAG_TOP - FLAG_H + g.y * FLAG_H;
float phase = t * (4.0 + 7.0 * p.wind);
float k = TAU * p.ripple / FLAG_W;
float s = k * x - phase;
float amp = (0.12 + 0.22 * p.wind) * pow(along, 1.15);
float z = sin(s + 1.3 * g.y) + 0.3 * sin(1.7 * s + 1.7 - 2.4 * g.y);
z *= amp;
x -= 0.15 * amp * abs(sin(s));
return float3(x, y, z);
}
// Once for every corner: the cloth's point there, and its normal from its
// neighbours. A small step along the grid each way finds the directions the
// cloth runs in; their cross product is the direction it faces.
Vertex corner(float2 grid, constant Uniforms& u, constant Params& p) {
float t = u.time * p.speed;
float3 here = cloth(grid, t, p);
float2 dx = float2(NEAR, 0.0), dy = float2(0.0, NEAR);
float3 along = cloth(grid + dx, t, p) - cloth(grid - dx, t, p);
float3 up = cloth(grid + dy, t, p) - cloth(grid - dy, t, p);
Vertex o;
o.world = here;
o.position = project(here, u.resolution.x / u.resolution.y);
o.normal = normalize(cross(up, along));
o.cloth = grid;
return o;
}
// The kiln's mark, as a distance: below its centre a box, above it a circle,
// so an arch. Negative inside.
float archDistance(float2 q, float r) {
if (q.y > 0.0) return length(q) - r;
return max(abs(q.x) - r, -0.22 - q.y);
}
// What the cloth is dyed: cream, an ember band along the pole, and the mark
// near the middle: a dark arch, hot inside (white at the floor, ember at the
// top), with gold rays around it.
float3 dye(float2 g) {
float3 col = float3(0.95, 0.89, 0.78);
float2 q = float2((g.x - 0.56) * FLAG_W / FLAG_H, g.y - 0.46);
float d = archDistance(q, 0.15);
float angle = atan2(q.y, q.x);
float rays = smoothstep(0.15, 0.5, cos(angle * 13.0));
rays *= smoothstep(0.06, 0.09, d) * (1.0 - smoothstep(0.17, 0.25, d));
rays *= smoothstep(-0.12, -0.02, q.y);
col = mix(col, kiln(0.62), rays);
float wall = 1.0 - smoothstep(0.045, 0.052, d);
col = mix(col, float3(0.07, 0.04, 0.04), wall);
float heat = clamp((q.y + 0.22) / 0.37, 0.0, 1.0);
float inside = 1.0 - smoothstep(-0.004, 0.003, d);
col = mix(col, kiln(0.95 - 0.55 * heat), inside);
float band = 1.0 - smoothstep(0.075, 0.08, g.x);
float edge = 1.0 - smoothstep(0.092, 0.097, g.x);
col = mix(col, kiln(0.42), band);
col = mix(col, kiln(0.66), edge - band);
return col;
}
// Which way the sun is: low, to the right and a little behind the flag.
float3 sunDirection(constant Params& p) {
return normalize(float3(0.9, 0.08 + 0.45 * p.sun, 0.4));
}
// Once for every pixel of the cloth: the dye, lit by the sun as much as the
// normal faces it, and a little by the sky everywhere.
float4 shade(Vertex v, constant Uniforms& u, constant Params& p) {
float3 n = normalize(v.normal);
float3 albedo = dye(v.cloth);
float3 sunLight = kiln(0.82 + 0.1 * p.sun) * 1.3;
float lit = max(dot(n, sunDirection(p)), 0.0);
float3 col = albedo * sunLight * lit;
col += albedo * kiln(0.55) * (0.18 + 0.12 * n.y);
return float4(col, 1.0);
}
// Behind the flag: an evening sky, dark overhead and gold low down, the sun,
// and the pole, found by a ray through the pixel meeting a thin upright
// cylinder at x = 0, z = 0, with a ball on top.
float4 background(float2 uv, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
float3x3 axes = cameraAxes();
float2 s = (uv * 2.0 - 1.0) / ZOOM;
float3 d = normalize(axes[2] + axes[0] * s.x * aspect + axes[1] * s.y);
float up = clamp(d.y * 1.4 + 0.25, 0.0, 1.0);
float3 col = kiln(0.72 - 0.62 * pow(up, 0.7) + 0.12 * p.sun);
// The sun and its glow.
float g = max(dot(d, sunDirection(p)), 0.0);
float glow = 0.9 * pow(g, 900.0) + 0.25 * pow(g, 40.0) + 0.12 * pow(g, 6.0);
col += kiln(0.9) * glow;
// The pole: solve for where the ray is 0.025 from the y axis.
float r = 0.025;
float a = d.x * d.x + d.z * d.z;
float b = EYE.x * d.x + EYE.z * d.z;
float c = EYE.x * EYE.x + EYE.z * EYE.z - r * r;
float h = b * b - a * c;
if (h > 0.0 && (EYE + d * (-b - sqrt(h)) / a).y < 1.0) {
col = float3(0.05, 0.035, 0.035);
}
// The ball on top.
float3 ball = float3(0.0, 1.03, 0.0) - EYE;
float bb = dot(ball, d);
if (bb * bb - dot(ball, ball) + 0.045 * 0.045 > 0.0) col = kiln(0.45);
return float4(col, 1.0);
}3 lines from stage 2 that are gone
o.world = cloth(grid, u.time * p.speed, p);
o.position = project(o.world, u.resolution.x / u.resolution.y);
⋯
return float4(dye(v.cloth) * 0.8, 1.0);Stage 4 · Two sides
shade now turns the normal round when it points away from the eye (dot(n, toEye) < 0), so whichever side we see is lit as itself. A side facing the sun is lit as before. A side facing away gets the sun through the cloth: by how directly the sun is behind it, at 0.55 of the strength, and in the dye's colour twice over (albedo * albedo), once going in and once coming out, so the cream glows warm and the ember band glows deep red. The folds the sun is behind now glow instead of going dark.
stage-4-two-sides.metal6 new lines since stage 3
// Stage 4 · Two sides
// A cloth has two sides, and the normal points out of only one. shade turns
// it to face the eye, so whichever side is seen is lit as itself. A side
// facing away from the sun isn't dark: the cloth is thin, and the sun shines
// through it, dimmer and in the dye's colours, so the folds glow.
//
// @grid 96
// @param speed 1.0 0.0 3.0 How fast the wind blows the waves down the flag
// @param wind 0.65 0.0 1.0 How hard the wind blows: taller, faster waves
// @param ripple 1.4 0.5 3.0 Waves along the flag at once
// @param sun 0.4 0.0 1.0 How high the sun is, from sunset to late afternoon
constant float TAU = 6.2831853;
// The flag's length from the pole, its height, and where its top is.
constant float FLAG_W = 1.6;
constant float FLAG_H = 1.0;
constant float FLAG_TOP = 0.95;
// The eye, and the point it looks at.
constant float3 EYE = float3(-0.75, 0.05, -2.0);
constant float3 AIM = float3(0.72, 0.42, 0.0);
// How much the camera magnifies.
constant float ZOOM = 2.4;
// How far apart, on the grid, a corner's neighbours are asked for.
constant float NEAR = 0.002;
// What each corner carries to the pixels: where it lands on screen, where it
// is in the world, which way the cloth faces, and its place on the cloth.
struct Vertex {
float4 position [[position]];
float3 world;
float3 normal;
float2 cloth;
};
// Episode 1's kiln 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));
}
// The camera's three directions: right, up and forward.
float3x3 cameraAxes() {
float3 f = normalize(AIM - EYE);
float3 r = normalize(cross(float3(0, 1, 0), f));
return float3x3(r, cross(f, r), f);
}
// A point in the world as the host wants it: x and y on screen (times
// the depth), the depth from 0 to 1 for the depth test, and the depth itself.
float4 project(float3 world, float aspect) {
float3x3 axes = cameraAxes();
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);
}
// One point of the cloth, from its place on the grid (along, up). Waves run
// down the flag in z, a second crosses them at a slant, and both grow with
// the distance from the pole, so that edge never moves.
float3 cloth(float2 g, float t, constant Params& p) {
float along = g.x;
float x = along * FLAG_W;
float y = FLAG_TOP - FLAG_H + g.y * FLAG_H;
float phase = t * (4.0 + 7.0 * p.wind);
float k = TAU * p.ripple / FLAG_W;
float s = k * x - phase;
float amp = (0.12 + 0.22 * p.wind) * pow(along, 1.15);
float z = sin(s + 1.3 * g.y) + 0.3 * sin(1.7 * s + 1.7 - 2.4 * g.y);
z *= amp;
x -= 0.15 * amp * abs(sin(s));
return float3(x, y, z);
}
// Once for every corner: the cloth's point there, and its normal from its
// neighbours. A small step along the grid each way finds the directions the
// cloth runs in; their cross product is the direction it faces.
Vertex corner(float2 grid, constant Uniforms& u, constant Params& p) {
float t = u.time * p.speed;
float3 here = cloth(grid, t, p);
float2 dx = float2(NEAR, 0.0), dy = float2(0.0, NEAR);
float3 along = cloth(grid + dx, t, p) - cloth(grid - dx, t, p);
float3 up = cloth(grid + dy, t, p) - cloth(grid - dy, t, p);
Vertex o;
o.world = here;
o.position = project(here, u.resolution.x / u.resolution.y);
o.normal = normalize(cross(up, along));
o.cloth = grid;
return o;
}
// The kiln's mark, as a distance: below its centre a box, above it a circle,
// so an arch. Negative inside.
float archDistance(float2 q, float r) {
if (q.y > 0.0) return length(q) - r;
return max(abs(q.x) - r, -0.22 - q.y);
}
// What the cloth is dyed: cream, an ember band along the pole, and the mark
// near the middle: a dark arch, hot inside (white at the floor, ember at the
// top), with gold rays around it.
float3 dye(float2 g) {
float3 col = float3(0.95, 0.89, 0.78);
float2 q = float2((g.x - 0.56) * FLAG_W / FLAG_H, g.y - 0.46);
float d = archDistance(q, 0.15);
float angle = atan2(q.y, q.x);
float rays = smoothstep(0.15, 0.5, cos(angle * 13.0));
rays *= smoothstep(0.06, 0.09, d) * (1.0 - smoothstep(0.17, 0.25, d));
rays *= smoothstep(-0.12, -0.02, q.y);
col = mix(col, kiln(0.62), rays);
float wall = 1.0 - smoothstep(0.045, 0.052, d);
col = mix(col, float3(0.07, 0.04, 0.04), wall);
float heat = clamp((q.y + 0.22) / 0.37, 0.0, 1.0);
float inside = 1.0 - smoothstep(-0.004, 0.003, d);
col = mix(col, kiln(0.95 - 0.55 * heat), inside);
float band = 1.0 - smoothstep(0.075, 0.08, g.x);
float edge = 1.0 - smoothstep(0.092, 0.097, g.x);
col = mix(col, kiln(0.42), band);
col = mix(col, kiln(0.66), edge - band);
return col;
}
// Which way the sun is: low, to the right and a little behind the flag.
float3 sunDirection(constant Params& p) {
return normalize(float3(0.9, 0.08 + 0.45 * p.sun, 0.4));
}
// Once for every pixel of the cloth: the normal turned to face the eye. The
// sun lights a side that faces it; a side facing away gets it through the
// cloth, the dye's colour twice over (once in, once out), at half strength.
float4 shade(Vertex v, constant Uniforms& u, constant Params& p) {
float3 n = normalize(v.normal);
float3 toEye = normalize(EYE - v.world);
if (dot(n, toEye) < 0.0) n = -n;
float3 sunD = sunDirection(p);
float3 albedo = dye(v.cloth);
float3 sunLight = kiln(0.82 + 0.1 * p.sun) * 1.3;
float lit = max(dot(n, sunD), 0.0);
float through = max(-dot(n, sunD), 0.0);
float3 col = albedo * sunLight * lit;
col += albedo * albedo * sunLight * through * 0.55;
col += albedo * kiln(0.55) * (0.18 + 0.12 * n.y);
return float4(col, 1.0);
}
// Behind the flag: an evening sky, dark overhead and gold low down, the sun,
// and the pole, found by a ray through the pixel meeting a thin upright
// cylinder at x = 0, z = 0, with a ball on top.
float4 background(float2 uv, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
float3x3 axes = cameraAxes();
float2 s = (uv * 2.0 - 1.0) / ZOOM;
float3 d = normalize(axes[2] + axes[0] * s.x * aspect + axes[1] * s.y);
float up = clamp(d.y * 1.4 + 0.25, 0.0, 1.0);
float3 col = kiln(0.72 - 0.62 * pow(up, 0.7) + 0.12 * p.sun);
// The sun and its glow.
float g = max(dot(d, sunDirection(p)), 0.0);
float glow = 0.9 * pow(g, 900.0) + 0.25 * pow(g, 40.0) + 0.12 * pow(g, 6.0);
col += kiln(0.9) * glow;
// The pole: solve for where the ray is 0.025 from the y axis.
float r = 0.025;
float a = d.x * d.x + d.z * d.z;
float b = EYE.x * d.x + EYE.z * d.z;
float c = EYE.x * EYE.x + EYE.z * EYE.z - r * r;
float h = b * b - a * c;
if (h > 0.0 && (EYE + d * (-b - sqrt(h)) / a).y < 1.0) {
col = float3(0.05, 0.035, 0.035);
}
// The ball on top.
float3 ball = float3(0.0, 1.03, 0.0) - EYE;
float bb = dot(ball, d);
if (bb * bb - dot(ball, ball) + 0.045 * 0.045 > 0.0) col = kiln(0.45);
return float4(col, 1.0);
}1 line from stage 3 that are gone
float lit = max(dot(n, sunDirection(p)), 0.0);Stage 5 · Flag in the Wind
The last step adds the wind's moods and the finish.
- Gusts.
windNowiswindrising and falling bygust, from two slow noises (about one bump every 5 seconds and one every 1.5). The waves grow taller in a gust, andwindPhasepushes the waves a little ahead, smoothly, so they hurry through it without jumping. - Hanging. The calmer the wind, the more the far end drops and swings in toward the pole: by (1 − wind)², growing with the square of the distance along. At
wind0 the flag hangs in a curve. - Flutter. A fast small wave, strongest at the free end, as the edge of a flag shivers.
- The finish. A soft sheen where the sun reflects off the folds toward the eye. The mark's interior glows on its own, lit or not. The sky gets the sun and its glow, and clouds of value noise that drift with the wind.
- The grid is 128 squares a side: 32,768 triangles.
the kiln palette and shade(), from the finished shader
// Episode 1's kiln 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 cloth. A cloth has two sides: the normal is
// turned to face the eye, so whichever side is seen is lit as the side it is.
// The sun lights a side that faces it; a side that faces away gets the sun
// through the cloth instead, dimmer and warmer, in the dye's colours. The sky
// lights everything a little, and a soft sheen runs along the folds.
float4 shade(Vertex v, constant Uniforms& u, constant Params& p) {
float3 n = normalize(v.normal);
float3 toEye = normalize(EYE - v.world);
if (dot(n, toEye) < 0.0) n = -n;
float3 sunD = sunDirection(p);
float3 albedo = dye(v.cloth);
float3 sunLight = kiln(0.82 + 0.1 * p.sun) * 1.3;
float lit = max(dot(n, sunD), 0.0);
float through = max(-dot(n, sunD), 0.0);
float3 col = albedo * sunLight * lit;
col += albedo * albedo * sunLight * through * 0.55;
col += albedo * kiln(0.55) * (0.18 + 0.12 * n.y);
float sheen = pow(max(dot(reflect(-sunD, n), toEye), 0.0), 12.0);
col += sunLight * sheen * 0.15;
col += kilnGlow(v.cloth);
return float4(col, 1.0);
}episode-22-flag-in-the-wind.metal, the whole file116 new lines since stage 4
// Ray Kiln · Episode 22 · Flag in the Wind
//
// @episode 22 Flag in the Wind
// @length 60
// @variation 30
// @variations 1,2,3,4,6,9,11,12
// @still 14.0
// @teaches Vertex waves, normals from neighbours, two-sided light
// @category Vertex
// @tags vertex shaders, grids, displacement, normals, finite differences, two-sided lighting, cloth, procedural sound
// @final Flag in the wind | Waves run down the cloth from the pole, gusts swell them, and the low sun shines through the folds that turn away.
//
// A flag with the kiln's mark on it, flying from a pole against an evening sky. The host lays down a grid of squares;
// `corner` turns each corner into a point of the cloth, pinned at the pole and waving more the farther it is from it.
// Waves are made of sines running down the flag, which is all a flag's motion needs to look right. The light needs a
// normal at every corner, and a wave this tangled has no easy formula for one, so `corner` asks the cloth where its two
// neighbours are and crosses the two directions to them. A cloth also has two sides: `shade` turns the normal to face
// the eye, and the low sun behind a fold shines through it.
// 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 128
// @param speed 1.0 0.0 3.0 How fast the wind blows the waves down the flag
// @param wind 0.65 0.0 1.0 How hard the wind blows: calm lets the flag hang, strong pulls it out flat and fast
// @param gust 0.5 0.0 1.0 How much the wind rises and falls
// @param ripple 1.4 0.5 3.0 Waves along the flag at once
// @param sun 0.4 0.0 1.0 How high the sun is, from sunset to late afternoon
// @param volume 0.8 0.0 1.0 Loudness of the sound
constant float TAU = 6.2831853;
// The flag's length from the pole, its height, and where its top is.
constant float FLAG_W = 1.6;
constant float FLAG_H = 1.0;
constant float FLAG_TOP = 0.95;
// The eye, and the point it looks at.
constant float3 EYE = float3(-0.75, 0.05, -2.0);
constant float3 AIM = float3(0.72, 0.42, 0.0);
// How much the camera magnifies.
constant float ZOOM = 2.4;
// How far apart, on the grid, a corner's neighbours are asked for.
constant float NEAR = 0.002;
// What each corner carries to the pixels: where it lands on screen, where it
// is in the world, which way the cloth faces, and its place on the cloth.
struct Vertex {
float4 position [[position]];
float3 world;
float3 normal;
float2 cloth;
};
// Episode 5'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);
}
// Episode 1's kiln 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));
}
// Episode 15'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);
}
// The camera's three directions: right, up and forward.
float3x3 cameraAxes() {
float3 f = normalize(AIM - EYE);
float3 r = normalize(cross(float3(0, 1, 0), f));
return float3x3(r, cross(f, r), f);
}
// A point in the world as the host wants it: x and y on screen (times
// the depth), the depth from 0 to 1 for the depth test, and the depth itself.
float4 project(float3 world, float aspect) {
float3x3 axes = cameraAxes();
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 wind's strength now: `wind`, rising and falling by `gust` as two slow
// noises add up. The sound reads the same wind, so gusts are heard as seen.
float windNow(float t, constant Params& p) {
float g = 0.65 * noise1(t, 0.21, 5u) + 0.35 * noise1(t, 0.67, 11u);
return clamp(p.wind * (1.0 + p.gust * (g - 0.5) * 1.6), 0.0, 1.0);
}
// Where the waves have got to: their phase runs on faster when the wind is
// stronger, and a gust pushes it a little ahead, smoothly, so the waves
// hurry through a gust without ever jumping.
float windPhase(float t, constant Params& p) {
float base = 4.0 + 7.0 * p.wind;
float g = 0.65 * noise1(t, 0.21, 5u) + 0.35 * noise1(t, 0.67, 11u);
return t * base + p.gust * 2.0 * (g - 0.5);
}
// One point of the cloth, from its place on the grid (along, up): the waves
// grow in a gust, the free edge flutters, and the calmer the wind, the more
// the far end hangs down and in toward the pole.
float3 cloth(float2 g, float t, constant Params& p) {
float w = windNow(t, p);
float along = g.x;
float x = along * FLAG_W;
float y = FLAG_TOP - FLAG_H + g.y * FLAG_H;
float phase = windPhase(t, p);
float k = TAU * p.ripple / FLAG_W;
float s = k * x - phase;
float amp = (0.12 + 0.22 * w) * pow(along, 1.15);
float z = sin(s + 1.3 * g.y) + 0.3 * sin(1.7 * s + 1.7 - 2.4 * g.y);
z *= amp;
z += 0.025 * w * pow(along, 3.0) * sin(3.1 * s + 7.0 * g.y);
// Hanging: the far end drops and swings in toward the pole.
float hang = (1.0 - w) * (1.0 - w);
float sag = hang * along * along;
y -= 0.55 * sag * (0.6 + 0.4 * (1.0 - g.y));
x *= 1.0 - 0.3 * sag;
// Folds take up the cloth's length, so the far end comes in a little.
x -= 0.15 * amp * abs(sin(s));
return float3(x, y, z);
}
// Once for every corner: the cloth's point there, and its normal from its
// neighbours. A small step along the grid each way finds the directions the
// cloth runs in; their cross product is the direction it faces.
Vertex corner(float2 grid, constant Uniforms& u, constant Params& p) {
float t = u.time * p.speed;
float3 here = cloth(grid, t, p);
float2 dx = float2(NEAR, 0.0), dy = float2(0.0, NEAR);
float3 along = cloth(grid + dx, t, p) - cloth(grid - dx, t, p);
float3 up = cloth(grid + dy, t, p) - cloth(grid - dy, t, p);
Vertex o;
o.world = here;
o.position = project(here, u.resolution.x / u.resolution.y);
o.normal = normalize(cross(up, along));
o.cloth = grid;
return o;
}
// The kiln's mark, as a distance: below its centre a box, above it a circle,
// so an arch. Negative inside.
float archDistance(float2 q, float r) {
if (q.y > 0.0) return length(q) - r;
return max(abs(q.x) - r, -0.22 - q.y);
}
// What the cloth is dyed: cream, an ember band along the pole, and the mark
// near the middle: a dark arch, hot inside (white at the floor, ember at the
// top), with gold rays around it.
float3 dye(float2 g) {
float3 col = float3(0.95, 0.89, 0.78);
float2 q = float2((g.x - 0.56) * FLAG_W / FLAG_H, g.y - 0.46);
float d = archDistance(q, 0.15);
float angle = atan2(q.y, q.x);
float rays = smoothstep(0.15, 0.5, cos(angle * 13.0));
rays *= smoothstep(0.06, 0.09, d) * (1.0 - smoothstep(0.17, 0.25, d));
rays *= smoothstep(-0.12, -0.02, q.y);
col = mix(col, kiln(0.62), rays);
float wall = 1.0 - smoothstep(0.045, 0.052, d);
col = mix(col, float3(0.07, 0.04, 0.04), wall);
float heat = clamp((q.y + 0.22) / 0.37, 0.0, 1.0);
float inside = 1.0 - smoothstep(-0.004, 0.003, d);
col = mix(col, kiln(0.95 - 0.55 * heat), inside);
float band = 1.0 - smoothstep(0.075, 0.08, g.x);
float edge = 1.0 - smoothstep(0.092, 0.097, g.x);
col = mix(col, kiln(0.42), band);
col = mix(col, kiln(0.66), edge - band);
return col;
}
// The mark's hot interior glows on its own, lit or not: the kiln is burning.
float3 kilnGlow(float2 g) {
float2 q = float2((g.x - 0.56) * FLAG_W / FLAG_H, g.y - 0.46);
float d = archDistance(q, 0.15);
float heat = clamp((q.y + 0.22) / 0.37, 0.0, 1.0);
float inside = 1.0 - smoothstep(-0.004, 0.003, d);
return kiln(0.95 - 0.55 * heat) * 0.7 * inside;
}
// Which way the sun is: low, to the right and a little behind the flag.
float3 sunDirection(constant Params& p) {
return normalize(float3(0.9, 0.08 + 0.45 * p.sun, 0.4));
}
// The evening sky in a direction: dark overhead, ember to gold toward the
// horizon, the sun and its glow, and a few clouds that drift with the wind.
float3 sky(float3 d, float t, constant Params& p) {
float3 sunD = sunDirection(p);
float up = clamp(d.y * 1.4 + 0.25, 0.0, 1.0);
float3 col = kiln(0.72 - 0.62 * pow(up, 0.7) + 0.12 * p.sun);
float g = max(dot(d, sunD), 0.0);
float glow = 0.9 * pow(g, 900.0) + 0.25 * pow(g, 40.0) + 0.12 * pow(g, 6.0);
col += kiln(0.9) * glow;
float2 c = d.xz / max(d.y + 0.35, 0.05) * 0.6;
c.x += t * 0.05 * (0.3 + p.wind);
float2 i = floor(c), f = fract(c);
f = f * f * (3.0 - 2.0 * f);
uint k = uint(int(i.x) + 5000) * 73u + uint(int(i.y) + 5000) * 9781u;
float lo = mix(hashU(k), hashU(k + 73u), f.x);
float hi = mix(hashU(k + 9781u), hashU(k + 9854u), f.x);
float n = mix(lo, hi, f.y);
float cloud = smoothstep(0.55, 0.85, n) * smoothstep(-0.2, 0.25, d.y);
return mix(col, kiln(0.3 + 0.35 * g), cloud * 0.6);
}
// Once for every pixel of the cloth. A cloth has two sides: the normal is
// turned to face the eye, so whichever side is seen is lit as the side it is.
// The sun lights a side that faces it; a side that faces away gets the sun
// through the cloth instead, dimmer and warmer, in the dye's colours. The sky
// lights everything a little, and a soft sheen runs along the folds.
float4 shade(Vertex v, constant Uniforms& u, constant Params& p) {
float3 n = normalize(v.normal);
float3 toEye = normalize(EYE - v.world);
if (dot(n, toEye) < 0.0) n = -n;
float3 sunD = sunDirection(p);
float3 albedo = dye(v.cloth);
float3 sunLight = kiln(0.82 + 0.1 * p.sun) * 1.3;
float lit = max(dot(n, sunD), 0.0);
float through = max(-dot(n, sunD), 0.0);
float3 col = albedo * sunLight * lit;
col += albedo * albedo * sunLight * through * 0.55;
col += albedo * kiln(0.55) * (0.18 + 0.12 * n.y);
float sheen = pow(max(dot(reflect(-sunD, n), toEye), 0.0), 12.0);
col += sunLight * sheen * 0.15;
col += kilnGlow(v.cloth);
return float4(col, 1.0);
}
// Behind the flag: the sky, and the pole, found by a ray through the pixel
// meeting a thin upright cylinder at x = 0, z = 0, with a gold ball on top.
float4 background(float2 uv, constant Uniforms& u, constant Params& p) {
float t = u.time * p.speed;
float aspect = u.resolution.x / u.resolution.y;
float3x3 axes = cameraAxes();
float2 s = (uv * 2.0 - 1.0) / ZOOM;
float3 d = normalize(axes[2] + axes[0] * s.x * aspect + axes[1] * s.y);
float3 col = sky(d, t, p);
float3 sunD = sunDirection(p);
// The pole: solve for where the ray is 0.025 from the y axis.
float r = 0.025;
float a = d.x * d.x + d.z * d.z;
float b = EYE.x * d.x + EYE.z * d.z;
float c = EYE.x * EYE.x + EYE.z * EYE.z - r * r;
float h = b * b - a * c;
if (h > 0.0) {
float hit = (-b - sqrt(h)) / a;
float3 at = EYE + d * hit;
if (at.y < 1.0) {
float3 n = normalize(float3(at.x, 0.0, at.z));
float l = max(dot(n, sunD), 0.0);
float shine = pow(max(dot(reflect(d, n), sunD), 0.0), 30.0);
col = float3(0.05, 0.035, 0.035);
col += kiln(0.8) * (0.6 * l + 0.8 * shine);
}
}
// The ball on top.
float3 ball = float3(0.0, 1.03, 0.0) - EYE;
float bb = dot(ball, d);
float hb = bb * bb - dot(ball, ball) + 0.045 * 0.045;
if (hb > 0.0) {
float3 at = EYE + d * (bb - sqrt(hb));
float3 n = normalize(at - float3(0.0, 1.03, 0.0));
float shine = pow(max(dot(reflect(d, n), sunD), 0.0), 40.0);
col = kiln(0.45) * 0.3 + kiln(0.75) * max(dot(n, sunD), 0.0);
col += kiln(0.95) * shine;
}
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.
// A short burst of noise: the snap of cloth, `age` seconds after it.
float snap(float t, float age, uint salt) {
if (age < 0.0 || age > 0.2) return 0.0;
float hiss = noise1(t, 5200.0, salt) - 0.5;
hiss += 0.6 * (noise1(t, 1500.0, salt + 7u) - 0.5);
return hiss * (1.0 - exp(-age / 0.002)) * exp(-age / 0.035);
}
// The same wind: a rush of noise, dark when calm, with a brighter hiss added
// as it blows harder, and louder too. Every time a wave runs off the free
// edge the cloth snaps, twice for the two faces of the wave; the snaps are
// louder in a gust and sit right of the middle, where the flag's end is. Now
// and then, in a gust, the halyard's clip rings against the pole, on the left.
float2 sound(float t, constant Params& p) {
float tt = t * p.speed;
float w = windNow(tt, p);
float dark = noise1(t, 400.0, 3u) - 0.5;
dark += 0.8 * (noise1(t, 90.0, 13u) - 0.5);
float bright = noise1(t, 1300.0, 3u) - 0.5;
bright += 0.6 * (noise1(t, 4200.0, 9u) - 0.5);
float rush = dark + w * bright;
float lean = 0.1 * noise1(t, 0.3, 17u);
float2 v = float2(rush * (1.0 + lean), rush * (1.1 - lean));
v *= 0.08 + 0.4 * w * w;
// The snaps: the wave's phase at the free edge passes a half turn.
float k = TAU * p.ripple;
float edge = (windPhase(tt, p) - k) / (TAU * 0.5);
float n = floor(edge);
float rate = (4.0 + 7.0 * p.wind) / (TAU * 0.5) * max(p.speed, 0.05);
float age = (edge - n) / rate;
float loud = 0.6 + 0.4 * hashU(uint(int(n) + 100000) * 17u);
loud *= 0.25 + 0.75 * w;
float flap = snap(t, age, 41u) * loud * smoothstep(0.1, 0.4, w);
v += float2(0.45, 0.75) * flap * 3.2;
// The clip: a slot every 1.3 s rings if a coin toss and the gust say so.
float slot = floor(t / 1.3);
float at = t - slot * 1.3 - 0.4 * hashU(uint(int(slot) + 9000) * 5u);
uint sk = uint(int(slot) + 9000) * 29u;
if (at > 0.0 && hashU(sk) < 0.6 * w * w * p.gust + 0.05) {
float ring = sin(TAU * 1730.0 * at) + 0.5 * sin(TAU * 4310.0 * at);
ring += 0.3 * sin(TAU * 2890.0 * at);
ring *= exp(-at / 0.18) * (1.0 - exp(-at / 0.001));
v += float2(0.4, 0.25) * ring * 0.18;
}
float fadeIn = smoothstep(0.0, 2.0, t);
return tanh(v * 2.2) / 2.0 * fadeIn * p.volume;
}9 lines from stage 4 that are gone
float phase = t * (4.0 + 7.0 * p.wind);
⋯
float amp = (0.12 + 0.22 * p.wind) * pow(along, 1.15);
⋯
float up = clamp(d.y * 1.4 + 0.25, 0.0, 1.0);
float3 col = kiln(0.72 - 0.62 * pow(up, 0.7) + 0.12 * p.sun);
⋯
float g = max(dot(d, sunDirection(p)), 0.0);
float glow = 0.9 * pow(g, 900.0) + 0.25 * pow(g, 40.0) + 0.12 * pow(g, 6.0);
col += kiln(0.9) * glow;
⋯
if (h > 0.0 && (EYE + d * (-b - sqrt(h)) / a).y < 1.0) {
⋯
if (bb * bb - dot(ball, ball) + 0.045 * 0.045 > 0.0) col = kiln(0.45);The sound
The sound reads the same wind. Under everything is a rush of noise: a dark part (smooth random values joined 400 and 90 times a second) and a brighter hiss (1,300 and 4,200 times a second) that is mixed in as the wind blows harder, and the whole rush gets louder with the square of the wind, so a gust is heard as it is seen.
Every time the wave passes a half turn at the free end of the flag, the cloth snaps: a burst of noise that dies in a few hundredths of a second, two to a wave, louder in a gust and silent when the wind is too weak to snap anything. The snaps sit right of the middle, where the flag's end is in the picture. Now and then a clip on the halyard rings against the pole, on the left: three partials at 1,730, 2,890 and 4,310 Hz, out of tune with each other as a small piece of metal is, that die in about a fifth of a second; it rings more often the stronger the gusts. It all goes through tanh and fades in over the first two seconds. At the defaults the peak is about -8.0 dBFS and the average (RMS) about -19.9 dBFS.
New in Ray Kiln
Grid size in the playground. Under a vertex episode's picture there is now a Grid slider for its // @grid N line, from 1 square a side to 1,024, with the number of triangles beside it (N × N × 2) and, where the browser can time the GPU, what the grid takes to draw and what that is a triangle. While you drag, the grid is redrawn at once at the new size, with no compile; when you let go, the new size is written into the code's // @grid line, so a Share link, a save and an export all have it.
Open Flag in the Wind in the playground, turn on Wireframe and drag Grid down to 8. The waves turn into a few flat facets and the outline goes jagged, but the light across them stays smooth, because each corner's normal comes from the cloth around it, not from the triangles. Then drag it up past 128: the outline barely changes, while the triangle count and the GPU time climb. That is why the flag uses 128.
Try this
- Drag Grid to 4 with Wireframe on: 32 triangles still make a recognisable flag.
- Set
windto 0: the flag hangs from the pole in a curve, its waves small and slow. Then set it to 1. - Set
gustto 1 andwindto 0.3: the flag drops between gusts and lifts in them, and the halyard clip rings now and then. - Set
rippleto 3: three waves crowd along the flag at once, and the mark is folded through. - In
shade, delete the lineif (dot(n, toEye) < 0.0) n = -n;: wherever a fold shows the back of the cloth, it is lit as the front, and the glow through the folds is gone. - In
corner, changefloat NEAR = 0.002(at the top) to0.2: the neighbours are so far apart that the normals smooth the small waves away, and the folds look soft and blurry. - In
sunDirection, change0.4to-0.6: the sun moves round in front of the flag, behind the camera, and lights the side we see directly.
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 wind blows the waves down the flag |
wind | 0.65 | 0.0 to 1.0 | How hard the wind blows: calm lets the flag hang, strong pulls it out flat and fast |
gust | 0.5 | 0.0 to 1.0 | How much the wind rises and falls |
ripple | 1.4 | 0.5 to 3.0 | Waves along the flag at once |
sun | 0.4 | 0.0 to 1.0 | How high the sun is, from sunset to late afternoon |
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.
episode-22-flag-in-the-wind.metal
// Ray Kiln · Episode 22 · Flag in the Wind
//
// @episode 22 Flag in the Wind
// @length 60
// @variation 30
// @variations 1,2,3,4,6,9,11,12
// @still 14.0
// @teaches Vertex waves, normals from neighbours, two-sided light
// @category Vertex
// @tags vertex shaders, grids, displacement, normals, finite differences, two-sided lighting, cloth, procedural sound
// @final Flag in the wind | Waves run down the cloth from the pole, gusts swell them, and the low sun shines through the folds that turn away.
//
// A flag with the kiln's mark on it, flying from a pole against an evening sky. The host lays down a grid of squares;
// `corner` turns each corner into a point of the cloth, pinned at the pole and waving more the farther it is from it.
// Waves are made of sines running down the flag, which is all a flag's motion needs to look right. The light needs a
// normal at every corner, and a wave this tangled has no easy formula for one, so `corner` asks the cloth where its two
// neighbours are and crosses the two directions to them. A cloth also has two sides: `shade` turns the normal to face
// the eye, and the low sun behind a fold shines through it.
// 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 128
// @param speed 1.0 0.0 3.0 How fast the wind blows the waves down the flag
// @param wind 0.65 0.0 1.0 How hard the wind blows: calm lets the flag hang, strong pulls it out flat and fast
// @param gust 0.5 0.0 1.0 How much the wind rises and falls
// @param ripple 1.4 0.5 3.0 Waves along the flag at once
// @param sun 0.4 0.0 1.0 How high the sun is, from sunset to late afternoon
// @param volume 0.8 0.0 1.0 Loudness of the sound
constant float TAU = 6.2831853;
// The flag's length from the pole, its height, and where its top is.
constant float FLAG_W = 1.6;
constant float FLAG_H = 1.0;
constant float FLAG_TOP = 0.95;
// The eye, and the point it looks at.
constant float3 EYE = float3(-0.75, 0.05, -2.0);
constant float3 AIM = float3(0.72, 0.42, 0.0);
// How much the camera magnifies.
constant float ZOOM = 2.4;
// How far apart, on the grid, a corner's neighbours are asked for.
constant float NEAR = 0.002;
// What each corner carries to the pixels: where it lands on screen, where it
// is in the world, which way the cloth faces, and its place on the cloth.
struct Vertex {
float4 position [[position]];
float3 world;
float3 normal;
float2 cloth;
};
// Episode 5'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);
}
// Episode 1's kiln 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));
}
// Episode 15'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);
}
// The camera's three directions: right, up and forward.
float3x3 cameraAxes() {
float3 f = normalize(AIM - EYE);
float3 r = normalize(cross(float3(0, 1, 0), f));
return float3x3(r, cross(f, r), f);
}
// A point in the world as the host wants it: x and y on screen (times
// the depth), the depth from 0 to 1 for the depth test, and the depth itself.
float4 project(float3 world, float aspect) {
float3x3 axes = cameraAxes();
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 wind's strength now: `wind`, rising and falling by `gust` as two slow
// noises add up. The sound reads the same wind, so gusts are heard as seen.
float windNow(float t, constant Params& p) {
float g = 0.65 * noise1(t, 0.21, 5u) + 0.35 * noise1(t, 0.67, 11u);
return clamp(p.wind * (1.0 + p.gust * (g - 0.5) * 1.6), 0.0, 1.0);
}
// Where the waves have got to: their phase runs on faster when the wind is
// stronger, and a gust pushes it a little ahead, smoothly, so the waves
// hurry through a gust without ever jumping.
float windPhase(float t, constant Params& p) {
float base = 4.0 + 7.0 * p.wind;
float g = 0.65 * noise1(t, 0.21, 5u) + 0.35 * noise1(t, 0.67, 11u);
return t * base + p.gust * 2.0 * (g - 0.5);
}
// One point of the cloth, from its place on the grid (along, up): the waves
// grow in a gust, the free edge flutters, and the calmer the wind, the more
// the far end hangs down and in toward the pole.
float3 cloth(float2 g, float t, constant Params& p) {
float w = windNow(t, p);
float along = g.x;
float x = along * FLAG_W;
float y = FLAG_TOP - FLAG_H + g.y * FLAG_H;
float phase = windPhase(t, p);
float k = TAU * p.ripple / FLAG_W;
float s = k * x - phase;
float amp = (0.12 + 0.22 * w) * pow(along, 1.15);
float z = sin(s + 1.3 * g.y) + 0.3 * sin(1.7 * s + 1.7 - 2.4 * g.y);
z *= amp;
z += 0.025 * w * pow(along, 3.0) * sin(3.1 * s + 7.0 * g.y);
// Hanging: the far end drops and swings in toward the pole.
float hang = (1.0 - w) * (1.0 - w);
float sag = hang * along * along;
y -= 0.55 * sag * (0.6 + 0.4 * (1.0 - g.y));
x *= 1.0 - 0.3 * sag;
// Folds take up the cloth's length, so the far end comes in a little.
x -= 0.15 * amp * abs(sin(s));
return float3(x, y, z);
}
// Once for every corner: the cloth's point there, and its normal from its
// neighbours. A small step along the grid each way finds the directions the
// cloth runs in; their cross product is the direction it faces.
Vertex corner(float2 grid, constant Uniforms& u, constant Params& p) {
float t = u.time * p.speed;
float3 here = cloth(grid, t, p);
float2 dx = float2(NEAR, 0.0), dy = float2(0.0, NEAR);
float3 along = cloth(grid + dx, t, p) - cloth(grid - dx, t, p);
float3 up = cloth(grid + dy, t, p) - cloth(grid - dy, t, p);
Vertex o;
o.world = here;
o.position = project(here, u.resolution.x / u.resolution.y);
o.normal = normalize(cross(up, along));
o.cloth = grid;
return o;
}
// The kiln's mark, as a distance: below its centre a box, above it a circle,
// so an arch. Negative inside.
float archDistance(float2 q, float r) {
if (q.y > 0.0) return length(q) - r;
return max(abs(q.x) - r, -0.22 - q.y);
}
// What the cloth is dyed: cream, an ember band along the pole, and the mark
// near the middle: a dark arch, hot inside (white at the floor, ember at the
// top), with gold rays around it.
float3 dye(float2 g) {
float3 col = float3(0.95, 0.89, 0.78);
float2 q = float2((g.x - 0.56) * FLAG_W / FLAG_H, g.y - 0.46);
float d = archDistance(q, 0.15);
float angle = atan2(q.y, q.x);
float rays = smoothstep(0.15, 0.5, cos(angle * 13.0));
rays *= smoothstep(0.06, 0.09, d) * (1.0 - smoothstep(0.17, 0.25, d));
rays *= smoothstep(-0.12, -0.02, q.y);
col = mix(col, kiln(0.62), rays);
float wall = 1.0 - smoothstep(0.045, 0.052, d);
col = mix(col, float3(0.07, 0.04, 0.04), wall);
float heat = clamp((q.y + 0.22) / 0.37, 0.0, 1.0);
float inside = 1.0 - smoothstep(-0.004, 0.003, d);
col = mix(col, kiln(0.95 - 0.55 * heat), inside);
float band = 1.0 - smoothstep(0.075, 0.08, g.x);
float edge = 1.0 - smoothstep(0.092, 0.097, g.x);
col = mix(col, kiln(0.42), band);
col = mix(col, kiln(0.66), edge - band);
return col;
}
// The mark's hot interior glows on its own, lit or not: the kiln is burning.
float3 kilnGlow(float2 g) {
float2 q = float2((g.x - 0.56) * FLAG_W / FLAG_H, g.y - 0.46);
float d = archDistance(q, 0.15);
float heat = clamp((q.y + 0.22) / 0.37, 0.0, 1.0);
float inside = 1.0 - smoothstep(-0.004, 0.003, d);
return kiln(0.95 - 0.55 * heat) * 0.7 * inside;
}
// Which way the sun is: low, to the right and a little behind the flag.
float3 sunDirection(constant Params& p) {
return normalize(float3(0.9, 0.08 + 0.45 * p.sun, 0.4));
}
// The evening sky in a direction: dark overhead, ember to gold toward the
// horizon, the sun and its glow, and a few clouds that drift with the wind.
float3 sky(float3 d, float t, constant Params& p) {
float3 sunD = sunDirection(p);
float up = clamp(d.y * 1.4 + 0.25, 0.0, 1.0);
float3 col = kiln(0.72 - 0.62 * pow(up, 0.7) + 0.12 * p.sun);
float g = max(dot(d, sunD), 0.0);
float glow = 0.9 * pow(g, 900.0) + 0.25 * pow(g, 40.0) + 0.12 * pow(g, 6.0);
col += kiln(0.9) * glow;
float2 c = d.xz / max(d.y + 0.35, 0.05) * 0.6;
c.x += t * 0.05 * (0.3 + p.wind);
float2 i = floor(c), f = fract(c);
f = f * f * (3.0 - 2.0 * f);
uint k = uint(int(i.x) + 5000) * 73u + uint(int(i.y) + 5000) * 9781u;
float lo = mix(hashU(k), hashU(k + 73u), f.x);
float hi = mix(hashU(k + 9781u), hashU(k + 9854u), f.x);
float n = mix(lo, hi, f.y);
float cloud = smoothstep(0.55, 0.85, n) * smoothstep(-0.2, 0.25, d.y);
return mix(col, kiln(0.3 + 0.35 * g), cloud * 0.6);
}
// Once for every pixel of the cloth. A cloth has two sides: the normal is
// turned to face the eye, so whichever side is seen is lit as the side it is.
// The sun lights a side that faces it; a side that faces away gets the sun
// through the cloth instead, dimmer and warmer, in the dye's colours. The sky
// lights everything a little, and a soft sheen runs along the folds.
float4 shade(Vertex v, constant Uniforms& u, constant Params& p) {
float3 n = normalize(v.normal);
float3 toEye = normalize(EYE - v.world);
if (dot(n, toEye) < 0.0) n = -n;
float3 sunD = sunDirection(p);
float3 albedo = dye(v.cloth);
float3 sunLight = kiln(0.82 + 0.1 * p.sun) * 1.3;
float lit = max(dot(n, sunD), 0.0);
float through = max(-dot(n, sunD), 0.0);
float3 col = albedo * sunLight * lit;
col += albedo * albedo * sunLight * through * 0.55;
col += albedo * kiln(0.55) * (0.18 + 0.12 * n.y);
float sheen = pow(max(dot(reflect(-sunD, n), toEye), 0.0), 12.0);
col += sunLight * sheen * 0.15;
col += kilnGlow(v.cloth);
return float4(col, 1.0);
}
// Behind the flag: the sky, and the pole, found by a ray through the pixel
// meeting a thin upright cylinder at x = 0, z = 0, with a gold ball on top.
float4 background(float2 uv, constant Uniforms& u, constant Params& p) {
float t = u.time * p.speed;
float aspect = u.resolution.x / u.resolution.y;
float3x3 axes = cameraAxes();
float2 s = (uv * 2.0 - 1.0) / ZOOM;
float3 d = normalize(axes[2] + axes[0] * s.x * aspect + axes[1] * s.y);
float3 col = sky(d, t, p);
float3 sunD = sunDirection(p);
// The pole: solve for where the ray is 0.025 from the y axis.
float r = 0.025;
float a = d.x * d.x + d.z * d.z;
float b = EYE.x * d.x + EYE.z * d.z;
float c = EYE.x * EYE.x + EYE.z * EYE.z - r * r;
float h = b * b - a * c;
if (h > 0.0) {
float hit = (-b - sqrt(h)) / a;
float3 at = EYE + d * hit;
if (at.y < 1.0) {
float3 n = normalize(float3(at.x, 0.0, at.z));
float l = max(dot(n, sunD), 0.0);
float shine = pow(max(dot(reflect(d, n), sunD), 0.0), 30.0);
col = float3(0.05, 0.035, 0.035);
col += kiln(0.8) * (0.6 * l + 0.8 * shine);
}
}
// The ball on top.
float3 ball = float3(0.0, 1.03, 0.0) - EYE;
float bb = dot(ball, d);
float hb = bb * bb - dot(ball, ball) + 0.045 * 0.045;
if (hb > 0.0) {
float3 at = EYE + d * (bb - sqrt(hb));
float3 n = normalize(at - float3(0.0, 1.03, 0.0));
float shine = pow(max(dot(reflect(d, n), sunD), 0.0), 40.0);
col = kiln(0.45) * 0.3 + kiln(0.75) * max(dot(n, sunD), 0.0);
col += kiln(0.95) * shine;
}
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.
// A short burst of noise: the snap of cloth, `age` seconds after it.
float snap(float t, float age, uint salt) {
if (age < 0.0 || age > 0.2) return 0.0;
float hiss = noise1(t, 5200.0, salt) - 0.5;
hiss += 0.6 * (noise1(t, 1500.0, salt + 7u) - 0.5);
return hiss * (1.0 - exp(-age / 0.002)) * exp(-age / 0.035);
}
// The same wind: a rush of noise, dark when calm, with a brighter hiss added
// as it blows harder, and louder too. Every time a wave runs off the free
// edge the cloth snaps, twice for the two faces of the wave; the snaps are
// louder in a gust and sit right of the middle, where the flag's end is. Now
// and then, in a gust, the halyard's clip rings against the pole, on the left.
float2 sound(float t, constant Params& p) {
float tt = t * p.speed;
float w = windNow(tt, p);
float dark = noise1(t, 400.0, 3u) - 0.5;
dark += 0.8 * (noise1(t, 90.0, 13u) - 0.5);
float bright = noise1(t, 1300.0, 3u) - 0.5;
bright += 0.6 * (noise1(t, 4200.0, 9u) - 0.5);
float rush = dark + w * bright;
float lean = 0.1 * noise1(t, 0.3, 17u);
float2 v = float2(rush * (1.0 + lean), rush * (1.1 - lean));
v *= 0.08 + 0.4 * w * w;
// The snaps: the wave's phase at the free edge passes a half turn.
float k = TAU * p.ripple;
float edge = (windPhase(tt, p) - k) / (TAU * 0.5);
float n = floor(edge);
float rate = (4.0 + 7.0 * p.wind) / (TAU * 0.5) * max(p.speed, 0.05);
float age = (edge - n) / rate;
float loud = 0.6 + 0.4 * hashU(uint(int(n) + 100000) * 17u);
loud *= 0.25 + 0.75 * w;
float flap = snap(t, age, 41u) * loud * smoothstep(0.1, 0.4, w);
v += float2(0.45, 0.75) * flap * 3.2;
// The clip: a slot every 1.3 s rings if a coin toss and the gust say so.
float slot = floor(t / 1.3);
float at = t - slot * 1.3 - 0.4 * hashU(uint(int(slot) + 9000) * 5u);
uint sk = uint(int(slot) + 9000) * 29u;
if (at > 0.0 && hashU(sk) < 0.6 * w * w * p.gust + 0.05) {
float ring = sin(TAU * 1730.0 * at) + 0.5 * sin(TAU * 4310.0 * at);
ring += 0.3 * sin(TAU * 2890.0 * at);
ring *= exp(-at / 0.18) * (1.0 - exp(-at / 0.001));
v += float2(0.4, 0.25) * ring * 0.18;
}
float fadeIn = smoothstep(0.0, 2.0, t);
return tanh(v * 2.2) / 2.0 * 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.65, 0.5, 1.4, 0.4, 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.
More in Geometry
Press ? for keyboard shortcuts.
