Smoke Stack
The kiln's chimney at night, and the smoke rising out of it, lit silver by the moon behind it and orange by the fire below. The smoke is a density at every point, so each ray walks through it in steps, and each step marches again toward the moon and the fire to find how much light reaches it: thick smoke shadows itself, and its edges glow where the moon shines through them. Each time the kiln is stoked a thick column rises up the chimney, and the wind swings the plume over and back across the minute.
· Atmosphere · Fragment shader
Teaches Volumetric clouds: light marching, self-shadowing, scattering
Builds on 05 · Cracked Clay (integer hash, sound noise), 08 · Kiln Fire (3D hash, 3D value noise, 3D fractal noise)
Watch
The idea
A surface can be found: a ray steps until it reaches it, and the light is worked out there, once. Smoke has no surface. It is a density, a number at every point in space saying how much smoke is there, and light is changed by every bit of it the ray passes through.
So the ray walks through the smoke in even steps and adds up what each step does. Two things happen at every step:
- Some light is taken out. Smoke absorbs light, or sends it off somewhere else, so whatever lies behind it is dimmed: by
exp(-density × length)over a step, Beer and Lambert's law. The share still getting through after all the steps so far is the transmittance. - Some light is sent toward the eye. How much depends on how much light reaches that bit of smoke, and that takes a second, shorter march toward the light, adding up the smoke on the way: thick smoke shadows itself. It also depends on the angle, since smoke sends most of the light it scatters on in the direction it was already going.
It is built in five steps, and each step is a shader you can run on its own.
Stage 1 · A plume of smoke
smokeAt says how much smoke there is at a point. The smoke rises from the chimney, whose top is at height 0, as a column bent sideways by the wind: the column's middle at height h is wind × (0.32 h² + 0.2 h) across, so it leans further the higher it goes, and its radius is 0.3 + 0.4 h, so it spreads. How far a point is inside the column, from 1 on its middle to 0 at its edge, is added to fractal noise (episode 8's fbm3, four octaves) sampled at a point that drifts up at the speed the smoke rises (and along with the wind), so the edges fray in shapes that rise with it. That is clamped to 0...1 and thinned with height by exp(-0.28 h), and thickness scales it.
shade puts a camera on the ground near the chimney, looking up past it. The chimney is a cylinder of radius 0.3, found exactly (hitStack), and black for now; where the ray meets it the smoke march stops. The march runs through a box round the plume in 48 even steps, starting at a random place in the first step for each pixel, so the steps show as a fine grain rather than bands. The random place comes from a hash of the pixel alone, so the grain stays still from one frame to the next. Each step multiplies the transmittance by exp(-d × dt). Here the smoke only takes light away: what it took becomes plain grey, so the plume is a grey shadow over the sky.
stage-1-plume.metal
// Stage 1 · A plume of smoke
// Smoke has no surface: it is a density at every point. The ray walks through it in even steps, and each step lets
// through exp(-density × length) of the light from behind: Beer and Lambert's law. Here the smoke only takes light
// away, so it is a grey shadow over the sky.
// @param speed 1.0 0.0 2.0 How fast the smoke rises
// @param wind 0.5 0.0 1.0 How hard the wind bends the plume
// @param thickness 0.6 0.1 1.0 How dense the smoke is
constant float PI = 3.1415927;
// How fast smoke rises, in units a second, and the chimney's radius.
constant float RISE = 0.45;
constant float STACK = 0.3;
// Episode 8's 3D hash, value noise and fractal noise.
float hash31(float3 p) {
uint3 v = uint3(int3(floor(p))) * 1664525u + 1013904223u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
v ^= v >> 16u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
return float(v.x) * (1.0 / 4294967295.0);
}
float vnoise3(float3 p) {
float3 i = floor(p), f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash31(i), b = hash31(i + float3(1, 0, 0));
float c = hash31(i + float3(0, 1, 0)), d = hash31(i + float3(1, 1, 0));
float e = hash31(i + float3(0, 0, 1)), g = hash31(i + float3(1, 0, 1));
float h = hash31(i + float3(0, 1, 1)), k = hash31(i + float3(1, 1, 1));
float lo = mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
float hi = mix(mix(e, g, f.x), mix(h, k, f.x), f.y);
return mix(lo, hi, f.z);
}
// Octaves of vnoise3, as episode 8's fbm3, but with the number of octaves
// given: the light march gets by with fewer.
float fbm3(float3 p, int octaves) {
float v = 0.0, a = 0.5;
for (int i = 0; i < octaves; i++) {
v += a * vnoise3(p);
p = p * 2.02 + float3(17.1, 9.2, 4.7);
a *= 0.5;
}
return v;
}
// Smoke per unit of length at a point, 0 in clear air: a column bent by
// the wind, spreading as it rises, its edges frayed by rising noise.
float smokeAt(float3 pos, float t, constant Params& p, int octaves) {
float h = pos.y;
if (h < 0.0) return 0.0;
float w = p.wind;
float3 axis = float3(w * h * h * 0.32 + w * h * 0.2, h, 0.0);
float r = STACK + 0.4 * h;
float3 q = pos - float3(w * t * 0.5, t * RISE, 0.0);
float n = fbm3(q * 1.7, octaves);
float edge = 1.0 - length((pos - axis).xz) / r;
float d = clamp(edge * 1.3 + (n - 0.5) * 2.2 - 0.1, 0.0, 1.0);
d *= smoothstep(0.0, 0.15, h) * exp(-h * 0.28);
return d * 10.0 * p.thickness;
}
// The night sky: dark blue overhead, warm haze low down.
float3 nightSky(float3 rd) {
return mix(float3(0.10, 0.045, 0.040), float3(0.012, 0.016, 0.040),
smoothstep(-0.05, 0.45, rd.y));
}
// Where a ray meets the outside of the chimney, a cylinder round the y axis
// up to y = 0, or -1.
float hitStack(float3 ro, float3 rd) {
float a = dot(rd.xz, rd.xz);
float b = dot(ro.xz, rd.xz);
float c = dot(ro.xz, ro.xz) - STACK * STACK;
float disc = b * b - a * c;
if (disc < 0.0) return -1.0;
float d = (-b - sqrt(disc)) / a;
return (d > 0.0 && ro.y + rd.y * d < 0.0) ? d : -1.0;
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float t = u.time * p.speed + 4.0;
// A camera on the ground near the chimney, looking up past it.
float side = min(u.resolution.x, u.resolution.y);
float2 s = (uv - 0.5) * u.resolution / side * 2.0;
float3 ro = float3(-0.7, 0.05, -3.4);
float3 target = float3(0.5, 1.25, 0.0);
float3 fw = normalize(target - ro);
float3 rt = normalize(cross(float3(0.0, 1.0, 0.0), fw));
float3 up = cross(fw, rt);
float3 rd = normalize(s.x * rt + s.y * up + 1.9 * fw);
// The sky, or the chimney (black for now), where the march stops.
float3 col = nightSky(rd);
float far = 14.0;
float hit = hitStack(ro, rd);
if (hit > 0.0) {
col = float3(0.0);
far = hit;
}
// March through the smoke's box in even steps, from a random place in
// the first step, fixed for each pixel.
float3 lo = float3(-2.2, 0.0, -2.2), hi = float3(4.2, 5.5, 2.2);
float3 inv = 1.0 / rd;
float3 t0 = (lo - ro) * inv, t1 = (hi - ro) * inv;
float3 near = min(t0, t1), away = max(t0, t1);
float enter = max(max(near.x, near.y), max(near.z, 0.0));
float leave = min(min(away.x, away.y), min(away.z, far));
if (leave > enter) {
const int STEPS = 48;
float dt = (leave - enter) / float(STEPS);
float dist = enter + dt * hash31(float3(uv * u.resolution, 5.1));
float transmit = 1.0;
for (int i = 0; i < STEPS; i++) {
if (dist > leave || transmit < 0.02) break;
float d = smokeAt(ro + rd * dist, t, p, 4);
// What this step lets through of the light from behind it.
transmit *= exp(-d * dt);
dist += dt;
}
// What the smoke took away, it shows as plain grey.
col = col * transmit + float3(0.32, 0.33, 0.36) * (1.0 - transmit);
}
return float4(col, 1.0);
}Stage 2 · Light marching
Now the moon lights the smoke, from behind it and above as the camera sees it, and it is in the sky with a glow round it.
At each step with smoke in it, lightThrough marches from that point toward the moon: four steps over 1.6 units, adding up smokeAt (with two octaves of noise, since a shadow is soft anyway), and the light that gets through is exp(-sum × step). That is the step's moonlight. Here it is sent off every way alike, a share of 1 / 4π toward each direction, and of what the step takes out of the ray, the share it sends back is that light: light += transmit × lit × 0.55 × (1 - stepT), then transmit *= stepT. The 0.55 says smoke is dark: of the light it takes out, it scatters about half and swallows the rest. At the end, col × transmit + light is the sky behind dimmed by the smoke, plus all the light the smoke sent toward the eye, and 1 - exp(-1.6 col) keeps it from clipping.
The smoke is dark on the side away from the moon: the light reaching it has come through the rest of the plume first. That is self-shadowing.
stage-2-light-march.metal29 new lines since stage 1
// Stage 2 · Light marching
// Now the moon lights the smoke. How much moonlight reaches a bit of smoke takes a second, shorter march toward the
// moon, adding up the smoke on the way: thick smoke shadows itself. Each step sends back the light it catches, and
// what lies behind it is dimmed.
// @param speed 1.0 0.0 2.0 How fast the smoke rises
// @param wind 0.5 0.0 1.0 How hard the wind bends the plume
// @param thickness 0.6 0.1 1.0 How dense the smoke is
constant float PI = 3.1415927;
// How fast smoke rises, in units a second, and the chimney's radius.
constant float RISE = 0.45;
constant float STACK = 0.3;
// The moon: behind the smoke as the camera sees it, up and a little left.
constant float3 MOON = float3(-0.06, 0.55, 0.833);
constant float3 MOONLIGHT = float3(0.72, 0.80, 1.0);
// Episode 8's 3D hash, value noise and fractal noise.
float hash31(float3 p) {
uint3 v = uint3(int3(floor(p))) * 1664525u + 1013904223u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
v ^= v >> 16u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
return float(v.x) * (1.0 / 4294967295.0);
}
float vnoise3(float3 p) {
float3 i = floor(p), f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash31(i), b = hash31(i + float3(1, 0, 0));
float c = hash31(i + float3(0, 1, 0)), d = hash31(i + float3(1, 1, 0));
float e = hash31(i + float3(0, 0, 1)), g = hash31(i + float3(1, 0, 1));
float h = hash31(i + float3(0, 1, 1)), k = hash31(i + float3(1, 1, 1));
float lo = mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
float hi = mix(mix(e, g, f.x), mix(h, k, f.x), f.y);
return mix(lo, hi, f.z);
}
// Octaves of vnoise3, as episode 8's fbm3, but with the number of octaves
// given: the light march gets by with fewer.
float fbm3(float3 p, int octaves) {
float v = 0.0, a = 0.5;
for (int i = 0; i < octaves; i++) {
v += a * vnoise3(p);
p = p * 2.02 + float3(17.1, 9.2, 4.7);
a *= 0.5;
}
return v;
}
// Smoke per unit of length at a point, 0 in clear air: a column bent by
// the wind, spreading as it rises, its edges frayed by rising noise.
float smokeAt(float3 pos, float t, constant Params& p, int octaves) {
float h = pos.y;
if (h < 0.0) return 0.0;
float w = p.wind;
float3 axis = float3(w * h * h * 0.32 + w * h * 0.2, h, 0.0);
float r = STACK + 0.4 * h;
float3 q = pos - float3(w * t * 0.5, t * RISE, 0.0);
float n = fbm3(q * 1.7, octaves);
float edge = 1.0 - length((pos - axis).xz) / r;
float d = clamp(edge * 1.3 + (n - 0.5) * 2.2 - 0.1, 0.0, 1.0);
d *= smoothstep(0.0, 0.15, h) * exp(-h * 0.28);
return d * 10.0 * p.thickness;
}
// The light from a direction that reaches a point: a short march toward
// it adding up the smoke, then exp(-sum), Beer and Lambert's law.
float lightThrough(float3 pos, float3 dir, float t, constant Params& p,
float len, int steps) {
float ds = len / float(steps);
float sum = 0.0;
for (int i = 0; i < steps; i++) {
sum += smokeAt(pos + dir * (ds * (float(i) + 0.5)), t, p, 2);
}
return exp(-sum * ds);
}
// The night sky: dark blue overhead, warm haze low down, and the moon with
// a glow round it.
float3 nightSky(float3 rd) {
float3 col = mix(float3(0.10, 0.045, 0.040), float3(0.012, 0.016, 0.040),
smoothstep(-0.05, 0.45, rd.y));
float m = dot(rd, MOON);
col += MOONLIGHT * smoothstep(0.9993, 0.9995, m) * 1.4;
m = max(m, 0.0);
col += MOONLIGHT * (pow(m, 300.0) * 0.25 + pow(m, 12.0) * 0.05);
return col;
}
// Where a ray meets the outside of the chimney, a cylinder round the y axis
// up to y = 0, or -1.
float hitStack(float3 ro, float3 rd) {
float a = dot(rd.xz, rd.xz);
float b = dot(ro.xz, rd.xz);
float c = dot(ro.xz, ro.xz) - STACK * STACK;
float disc = b * b - a * c;
if (disc < 0.0) return -1.0;
float d = (-b - sqrt(disc)) / a;
return (d > 0.0 && ro.y + rd.y * d < 0.0) ? d : -1.0;
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float t = u.time * p.speed + 4.0;
// A camera on the ground near the chimney, looking up past it.
float side = min(u.resolution.x, u.resolution.y);
float2 s = (uv - 0.5) * u.resolution / side * 2.0;
float3 ro = float3(-0.7, 0.05, -3.4);
float3 target = float3(0.5, 1.25, 0.0);
float3 fw = normalize(target - ro);
float3 rt = normalize(cross(float3(0.0, 1.0, 0.0), fw));
float3 up = cross(fw, rt);
float3 rd = normalize(s.x * rt + s.y * up + 1.9 * fw);
float3 col = nightSky(rd);
float far = 14.0;
float hit = hitStack(ro, rd);
if (hit > 0.0) {
col = float3(0.0);
far = hit;
}
// March through the smoke's box in even steps, from a random place in
// the first step, fixed for each pixel.
float3 lo = float3(-2.2, 0.0, -2.2), hi = float3(4.2, 5.5, 2.2);
float3 inv = 1.0 / rd;
float3 t0 = (lo - ro) * inv, t1 = (hi - ro) * inv;
float3 near = min(t0, t1), away = max(t0, t1);
float enter = max(max(near.x, near.y), max(near.z, 0.0));
float leave = min(min(away.x, away.y), min(away.z, far));
if (leave > enter) {
const int STEPS = 48;
float dt = (leave - enter) / float(STEPS);
float dist = enter + dt * hash31(float3(uv * u.resolution, 5.1));
float transmit = 1.0;
float3 light = float3(0.0);
for (int i = 0; i < STEPS; i++) {
if (dist > leave || transmit < 0.02) break;
float3 pos = ro + rd * dist;
float d = smokeAt(pos, t, p, 4);
if (d > 0.002) {
// The moonlight that gets through, sent every way alike.
float3 lit = MOONLIGHT * 5.0 / (4.0 * PI)
* lightThrough(pos, MOON, t, p, 1.6, 4);
// Light sent to the eye: what the step takes out, times the
// light arriving. Then it dims what lies behind.
float stepT = exp(-d * dt);
light += transmit * lit * 0.55 * (1.0 - stepT);
transmit *= stepT;
}
dist += dt;
}
col = col * transmit + light;
}
col = 1.0 - exp(-col * 1.6);
return float4(col, 1.0);
}4 lines from stage 1 that are gone
return mix(float3(0.10, 0.045, 0.040), float3(0.012, 0.016, 0.040),
⋯
float d = smokeAt(ro + rd * dist, t, p, 4);
⋯
transmit *= exp(-d * dt);
⋯
col = col * transmit + float3(0.32, 0.33, 0.36) * (1.0 - transmit);Stage 3 · Scattering forward
Smoke doesn't send light every way alike. phase is Henyey and Greenstein's phase function: for light scattered at an angle whose cosine is c from the way it was going, the share is (1 - g²) / (4π (1 + g² - 2gc)^1.5). At g = 0 that is 1 / 4π every way, as in stage 2; toward 1, more and more goes straight on. forward is g.
The angle between the ray and the moonlight is the same at every step, so the phase is worked out once per pixel. With the moon behind the smoke, the light it sends on toward the camera is bright where the smoke lies between the camera and the moon, and brightest where the smoke is thin enough for the moonlight to reach it: the plume's edges glow, its silver lining. Turn forward to 0 and the smoke goes dull; turn it to 0.9 and the smoke nearest the moon blazes.
The sky also lights the smoke softly from every side, a dim blue added at each step, a little more for thin smoke.
stage-3-scattering.metal8 new lines since stage 2
// Stage 3 · Scattering forward
// Smoke doesn't send light every way alike: most goes on the way it was going. Henyey and Greenstein's phase function
// says how much goes each way, so the smoke glows where the moon is behind it, its silver lining. Forward sets how
// strongly. The sky lights it softly from every side.
// @param speed 1.0 0.0 2.0 How fast the smoke rises
// @param wind 0.5 0.0 1.0 How hard the wind bends the plume
// @param thickness 0.6 0.1 1.0 How dense the smoke is
// @param forward 0.6 0.0 0.9 How strongly the smoke sends light on the way it was going (the silver lining)
constant float PI = 3.1415927;
// How fast smoke rises, in units a second, and the chimney's radius.
constant float RISE = 0.45;
constant float STACK = 0.3;
// The moon: behind the smoke as the camera sees it, up and a little left.
constant float3 MOON = float3(-0.06, 0.55, 0.833);
constant float3 MOONLIGHT = float3(0.72, 0.80, 1.0);
// Episode 8's 3D hash, value noise and fractal noise.
float hash31(float3 p) {
uint3 v = uint3(int3(floor(p))) * 1664525u + 1013904223u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
v ^= v >> 16u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
return float(v.x) * (1.0 / 4294967295.0);
}
float vnoise3(float3 p) {
float3 i = floor(p), f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash31(i), b = hash31(i + float3(1, 0, 0));
float c = hash31(i + float3(0, 1, 0)), d = hash31(i + float3(1, 1, 0));
float e = hash31(i + float3(0, 0, 1)), g = hash31(i + float3(1, 0, 1));
float h = hash31(i + float3(0, 1, 1)), k = hash31(i + float3(1, 1, 1));
float lo = mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
float hi = mix(mix(e, g, f.x), mix(h, k, f.x), f.y);
return mix(lo, hi, f.z);
}
// Octaves of vnoise3, as episode 8's fbm3, but with the number of octaves
// given: the light march gets by with fewer.
float fbm3(float3 p, int octaves) {
float v = 0.0, a = 0.5;
for (int i = 0; i < octaves; i++) {
v += a * vnoise3(p);
p = p * 2.02 + float3(17.1, 9.2, 4.7);
a *= 0.5;
}
return v;
}
// Smoke per unit of length at a point, 0 in clear air: a column bent by
// the wind, spreading as it rises, its edges frayed by rising noise.
float smokeAt(float3 pos, float t, constant Params& p, int octaves) {
float h = pos.y;
if (h < 0.0) return 0.0;
float w = p.wind;
float3 axis = float3(w * h * h * 0.32 + w * h * 0.2, h, 0.0);
float r = STACK + 0.4 * h;
float3 q = pos - float3(w * t * 0.5, t * RISE, 0.0);
float n = fbm3(q * 1.7, octaves);
float edge = 1.0 - length((pos - axis).xz) / r;
float d = clamp(edge * 1.3 + (n - 0.5) * 2.2 - 0.1, 0.0, 1.0);
d *= smoothstep(0.0, 0.15, h) * exp(-h * 0.28);
return d * 10.0 * p.thickness;
}
// Henyey-Greenstein's phase function: the share of scattered light sent at
// cosine c from its way. g = 0: every way alike; toward 1, straight on.
float phase(float c, float g) {
float k = 1.0 + g * g - 2.0 * g * c;
return (1.0 - g * g) / (4.0 * PI * k * sqrt(k));
}
// The light from a direction that reaches a point: a short march toward
// it adding up the smoke, then exp(-sum), Beer and Lambert's law.
float lightThrough(float3 pos, float3 dir, float t, constant Params& p,
float len, int steps) {
float ds = len / float(steps);
float sum = 0.0;
for (int i = 0; i < steps; i++) {
sum += smokeAt(pos + dir * (ds * (float(i) + 0.5)), t, p, 2);
}
return exp(-sum * ds);
}
// The night sky: dark blue overhead, warm haze low down, and the moon with
// a glow round it.
float3 nightSky(float3 rd) {
float3 col = mix(float3(0.10, 0.045, 0.040), float3(0.012, 0.016, 0.040),
smoothstep(-0.05, 0.45, rd.y));
float m = dot(rd, MOON);
col += MOONLIGHT * smoothstep(0.9993, 0.9995, m) * 1.4;
m = max(m, 0.0);
col += MOONLIGHT * (pow(m, 300.0) * 0.25 + pow(m, 12.0) * 0.05);
return col;
}
// Where a ray meets the outside of the chimney, a cylinder round the y axis
// up to y = 0, or -1.
float hitStack(float3 ro, float3 rd) {
float a = dot(rd.xz, rd.xz);
float b = dot(ro.xz, rd.xz);
float c = dot(ro.xz, ro.xz) - STACK * STACK;
float disc = b * b - a * c;
if (disc < 0.0) return -1.0;
float d = (-b - sqrt(disc)) / a;
return (d > 0.0 && ro.y + rd.y * d < 0.0) ? d : -1.0;
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float t = u.time * p.speed + 4.0;
// A camera on the ground near the chimney, looking up past it.
float side = min(u.resolution.x, u.resolution.y);
float2 s = (uv - 0.5) * u.resolution / side * 2.0;
float3 ro = float3(-0.7, 0.05, -3.4);
float3 target = float3(0.5, 1.25, 0.0);
float3 fw = normalize(target - ro);
float3 rt = normalize(cross(float3(0.0, 1.0, 0.0), fw));
float3 up = cross(fw, rt);
float3 rd = normalize(s.x * rt + s.y * up + 1.9 * fw);
float3 col = nightSky(rd);
float far = 14.0;
float hit = hitStack(ro, rd);
if (hit > 0.0) {
col = float3(0.0);
far = hit;
}
// March through the smoke's box in even steps, from a random place in
// the first step, fixed for each pixel.
float3 lo = float3(-2.2, 0.0, -2.2), hi = float3(4.2, 5.5, 2.2);
float3 inv = 1.0 / rd;
float3 t0 = (lo - ro) * inv, t1 = (hi - ro) * inv;
float3 near = min(t0, t1), away = max(t0, t1);
float enter = max(max(near.x, near.y), max(near.z, 0.0));
float leave = min(min(away.x, away.y), min(away.z, far));
if (leave > enter) {
const int STEPS = 48;
float dt = (leave - enter) / float(STEPS);
float dist = enter + dt * hash31(float3(uv * u.resolution, 5.1));
float transmit = 1.0;
float3 light = float3(0.0);
// The same angle all along the ray: its phase is worked out once.
float moonPhase = phase(dot(rd, MOON), p.forward);
for (int i = 0; i < STEPS; i++) {
if (dist > leave || transmit < 0.02) break;
float3 pos = ro + rd * dist;
float d = smokeAt(pos, t, p, 4);
if (d > 0.002) {
float3 lit = MOONLIGHT * 5.0 * moonPhase
* lightThrough(pos, MOON, t, p, 1.6, 4);
// The sky's light from every side, more for thin smoke.
lit += float3(0.03, 0.04, 0.08)
* (1.0 - 0.5 * smoothstep(0.0, 1.5, d));
float stepT = exp(-d * dt);
light += transmit * lit * 0.55 * (1.0 - stepT);
transmit *= stepT;
}
dist += dt;
}
col = col * transmit + light;
}
col = 1.0 - exp(-col * 1.6);
return float4(col, 1.0);
}1 line from stage 2 that are gone
float3 lit = MOONLIGHT * 5.0 / (4.0 * PI)Stage 4 · The fire below
The fire in the chimney's mouth is a second light, close and orange, just below the top of the chimney.
lightAt now gathers everything that reaches a bit of smoke. The moon as before. The fire: the smoke between the point and the fire, by a light march of two steps (over at most 1.2 units), times a fall-off with distance, 1 / (1 + 1.6 d²), times its own phase. The fire is near, and lights the smoke from below and from many angles at once, so its phase uses half of forward. The sky's soft light. Then smoke fresh out of the kiln is still hot enough to glow a little itself: a glow of the kiln palette that fades as exp(-5 h) above the chimney.
The chimney gets bricks: a grid wrapped round the cylinder with every other row shifted by half a brick, a shade for each brick from a hash, dark mortar between, lit by the moon on the side that faces it and by the fire spilling over the rim near the top. glow sets how bright the fire burns, and it flickers a little, 0.8 + 0.2 sin(7t) sin(2.3t).
stage-4-fire-below.metal48 new lines since stage 3
// Stage 4 · The fire below
// The fire in the chimney is a second light, close and orange. Its light takes a march of its own through the smoke,
// fades with distance, and lights the smoke from underneath. Fresh out of the kiln the smoke glows a little itself,
// and the fire spills over the chimney's bricks.
// @param speed 1.0 0.0 2.0 How fast the smoke rises
// @param wind 0.5 0.0 1.0 How hard the wind bends the plume
// @param thickness 0.6 0.1 1.0 How dense the smoke is
// @param forward 0.6 0.0 0.9 How strongly the smoke sends light on the way it was going (the silver lining)
// @param glow 0.6 0.0 1.0 How bright the fire under the smoke burns
constant float PI = 3.1415927;
// How fast smoke rises, in units a second, and the chimney's radius.
constant float RISE = 0.45;
constant float STACK = 0.3;
// The kiln palette from episode 1.
float3 kiln(float t) {
float3 c0 = float3(0.020, 0.012, 0.020);
float3 c1 = float3(0.280, 0.040, 0.030);
float3 c2 = float3(0.880, 0.260, 0.050);
float3 c3 = float3(1.000, 0.680, 0.200);
float3 c4 = float3(1.000, 0.970, 0.840);
t = clamp(t, 0.0, 1.0);
float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
c = mix(c, c2, smoothstep(0.22, 0.50, t));
c = mix(c, c3, smoothstep(0.50, 0.76, t));
return mix(c, c4, smoothstep(0.76, 1.00, t));
}
// Episode 8's 3D hash, value noise and fractal noise.
float hash31(float3 p) {
uint3 v = uint3(int3(floor(p))) * 1664525u + 1013904223u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
v ^= v >> 16u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
return float(v.x) * (1.0 / 4294967295.0);
}
float vnoise3(float3 p) {
float3 i = floor(p), f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash31(i), b = hash31(i + float3(1, 0, 0));
float c = hash31(i + float3(0, 1, 0)), d = hash31(i + float3(1, 1, 0));
float e = hash31(i + float3(0, 0, 1)), g = hash31(i + float3(1, 0, 1));
float h = hash31(i + float3(0, 1, 1)), k = hash31(i + float3(1, 1, 1));
float lo = mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
float hi = mix(mix(e, g, f.x), mix(h, k, f.x), f.y);
return mix(lo, hi, f.z);
}
// Octaves of vnoise3, as episode 8's fbm3, but with the number of octaves
// given: the light march gets by with fewer.
float fbm3(float3 p, int octaves) {
float v = 0.0, a = 0.5;
for (int i = 0; i < octaves; i++) {
v += a * vnoise3(p);
p = p * 2.02 + float3(17.1, 9.2, 4.7);
a *= 0.5;
}
return v;
}
// Smoke per unit of length at a point, 0 in clear air: a column bent by
// the wind, spreading as it rises, its edges frayed by rising noise.
float smokeAt(float3 pos, float t, constant Params& p, int octaves) {
float h = pos.y;
if (h < 0.0) return 0.0;
float w = p.wind;
float3 axis = float3(w * h * h * 0.32 + w * h * 0.2, h, 0.0);
float r = STACK + 0.4 * h;
float3 q = pos - float3(w * t * 0.5, t * RISE, 0.0);
float n = fbm3(q * 1.7, octaves);
float edge = 1.0 - length((pos - axis).xz) / r;
float d = clamp(edge * 1.3 + (n - 0.5) * 2.2 - 0.1, 0.0, 1.0);
d *= smoothstep(0.0, 0.15, h) * exp(-h * 0.28);
return d * 10.0 * p.thickness;
}
// The moon: behind the smoke as the camera sees it, up and a little left.
constant float3 MOON = float3(-0.06, 0.55, 0.833);
constant float3 MOONLIGHT = float3(0.72, 0.80, 1.0);
// The fire in the chimney's mouth, glowing up into the smoke.
constant float3 FIRE = float3(0.0, -0.05, 0.0);
// Henyey-Greenstein's phase function: the share of scattered light sent at
// cosine c from its way. g = 0: every way alike; toward 1, straight on.
float phase(float c, float g) {
float k = 1.0 + g * g - 2.0 * g * c;
return (1.0 - g * g) / (4.0 * PI * k * sqrt(k));
}
// The light from a direction that reaches a point: a short march toward
// it adding up the smoke, then exp(-sum), Beer and Lambert's law.
float lightThrough(float3 pos, float3 dir, float t, constant Params& p,
float len, int steps) {
float ds = len / float(steps);
float sum = 0.0;
for (int i = 0; i < steps; i++) {
sum += smokeAt(pos + dir * (ds * (float(i) + 0.5)), t, p, 2);
}
return exp(-sum * ds);
}
// The night sky: dark blue overhead, warm haze low down, and the moon with
// a glow round it.
float3 nightSky(float3 rd) {
float3 col = mix(float3(0.10, 0.045, 0.040), float3(0.012, 0.016, 0.040),
smoothstep(-0.05, 0.45, rd.y));
float m = dot(rd, MOON);
col += MOONLIGHT * smoothstep(0.9993, 0.9995, m) * 1.4;
m = max(m, 0.0);
col += MOONLIGHT * (pow(m, 300.0) * 0.25 + pow(m, 12.0) * 0.05);
return col;
}
// Where a ray meets the outside of the chimney, a cylinder round the y axis
// up to y = 0, or -1.
float hitStack(float3 ro, float3 rd) {
float a = dot(rd.xz, rd.xz);
float b = dot(ro.xz, rd.xz);
float c = dot(ro.xz, ro.xz) - STACK * STACK;
float disc = b * b - a * c;
if (disc < 0.0) return -1.0;
float d = (-b - sqrt(disc)) / a;
return (d > 0.0 && ro.y + rd.y * d < 0.0) ? d : -1.0;
}
// The chimney's bricks at a point on it, lit by the moon on one side and,
// near the top, by the fire spilling over the rim.
float3 brick(float3 pos, float glow) {
float3 n = normalize(float3(pos.x, 0.0, pos.z));
float2 b = float2(atan2(pos.x, pos.z) * STACK / 0.11, pos.y / 0.06);
b.x += 0.5 * fmod(floor(b.y), 2.0);
float2 f = fract(b);
float mortar = smoothstep(0.0, 0.08, min(f.x, 1.0 - f.x) * 2.2)
* smoothstep(0.0, 0.1, min(f.y, 1.0 - f.y));
float3 col = mix(float3(0.10, 0.05, 0.04), float3(0.24, 0.10, 0.07),
hash31(float3(floor(b), 2.0)));
col *= 0.45 + 0.55 * mortar;
float moon = max(dot(n, MOON), 0.0) * 0.6 + 0.08;
float3 lit = col * MOONLIGHT * moon;
lit += kiln(0.6) * glow * exp(pos.y * 9.0) * 0.8 * col * 6.0;
return lit;
}
// The light a bit of smoke sends toward the eye, per unit it takes out.
float3 lightAt(float3 pos, float3 rd, float d, float moonPhase, float glow,
float t, constant Params& p) {
// The moon, through the smoke between, by the phase function.
float3 lit = MOONLIGHT * 5.0 * moonPhase
* lightThrough(pos, MOON, t, p, 1.6, 4);
// The fire, from below: through the smoke, fading with distance.
float3 toFire = FIRE - pos;
float fd = length(toFire);
toFire /= fd;
float fireT = lightThrough(pos, toFire, t, p, min(fd, 1.2), 2);
float firePhase = phase(dot(rd, -toFire), p.forward * 0.5) * 4.0 * PI;
lit += kiln(0.62) * 4.0 * glow * fireT * firePhase / (1.0 + fd * fd * 1.6);
// The sky's soft light.
lit += float3(0.03, 0.04, 0.08) * (1.0 - 0.5 * smoothstep(0.0, 1.5, d));
// Smoke scatters about half of what it takes out. Fresh smoke glows.
float heat = exp(-pos.y * 5.0) * glow;
return lit * 0.55 + kiln(0.5 + 0.3 * heat) * heat * 1.2;
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float t = u.time * p.speed + 4.0;
// A camera on the ground near the chimney, looking up past it.
float side = min(u.resolution.x, u.resolution.y);
float2 s = (uv - 0.5) * u.resolution / side * 2.0;
float3 ro = float3(-0.7, 0.05, -3.4);
float3 target = float3(0.5, 1.25, 0.0);
float3 fw = normalize(target - ro);
float3 rt = normalize(cross(float3(0.0, 1.0, 0.0), fw));
float3 up = cross(fw, rt);
float3 rd = normalize(s.x * rt + s.y * up + 1.9 * fw);
// The chimney now has bricks, and the fire flickers.
float3 col = nightSky(rd);
float far = 14.0;
float hit = hitStack(ro, rd);
float glow = p.glow * (0.8 + 0.2 * sin(t * 7.0) * sin(t * 2.3));
if (hit > 0.0) {
col = brick(ro + rd * hit, glow);
far = hit;
}
// March through the smoke's box in even steps, from a random place in
// the first step, fixed for each pixel.
float3 lo = float3(-2.2, 0.0, -2.2), hi = float3(4.2, 5.5, 2.2);
float3 inv = 1.0 / rd;
float3 t0 = (lo - ro) * inv, t1 = (hi - ro) * inv;
float3 near = min(t0, t1), away = max(t0, t1);
float enter = max(max(near.x, near.y), max(near.z, 0.0));
float leave = min(min(away.x, away.y), min(away.z, far));
if (leave > enter) {
const int STEPS = 48;
float dt = (leave - enter) / float(STEPS);
float dist = enter + dt * hash31(float3(uv * u.resolution, 5.1));
float transmit = 1.0;
float3 light = float3(0.0);
float moonPhase = phase(dot(rd, MOON), p.forward);
for (int i = 0; i < STEPS; i++) {
if (dist > leave || transmit < 0.02) break;
float3 pos = ro + rd * dist;
float d = smokeAt(pos, t, p, 4);
if (d > 0.002) {
// The moon, the fire and the sky, in lightAt.
float stepT = exp(-d * dt);
float3 lit = lightAt(pos, rd, d, moonPhase, glow, t, p);
light += transmit * lit * (1.0 - stepT);
transmit *= stepT;
}
dist += dt;
}
col = col * transmit + light;
}
col = 1.0 - exp(-col * 1.6);
return float4(col, 1.0);
}8 lines from stage 3 that are gone
constant float3 MOON = float3(-0.06, 0.55, 0.833);
constant float3 MOONLIGHT = float3(0.72, 0.80, 1.0);
⋯
col = float3(0.0);
⋯
float3 lit = MOONLIGHT * 5.0 * moonPhase
* lightThrough(pos, MOON, t, p, 1.6, 4);
⋯
lit += float3(0.03, 0.04, 0.08)
* (1.0 - 0.5 * smoothstep(0.0, 1.5, d));
⋯
light += transmit * lit * 0.55 * (1.0 - stepT);Stage 5 · Smoke stack
The last step makes the kiln breathe.
- Stoking.
emission(t)says how hard the kiln is breathing out at timet: a little all the time, and every 20 seconds a stoking, a strong burst that swells over a second and dies away slowly. Smoke at heighthleft the chimneyh / 0.45seconds ago, since it rises at 0.45 units a second, sosmokeAtasksemissionabout then, not now. A stoking at the bottom of the chimney becomes a thick column climbing up the picture. Atspeed1 the stokings come at 4, 24 and 44 seconds. - Puffs. On top of that the kiln puffs, every two or three seconds, a sine whose own speed wavers;
puffsgoes from a steady stream to a smoke that leaves in separate breaths. - Wind. The wind swings,
0.55 + 0.45 sin(0.11 t - 0.6)ofwind, once in about 57 seconds, and the smoke keeps the wind of when it left, so the plume leans over and straightens again, bent in a slow curve. - Stars. A few stars, picked by a hash of a grid over the sky's directions, twinkling.
the kiln palette and shade(), from the finished shader
// The kiln palette from episode 1.
float3 kiln(float t) {
float3 c0 = float3(0.020, 0.012, 0.020);
float3 c1 = float3(0.280, 0.040, 0.030);
float3 c2 = float3(0.880, 0.260, 0.050);
float3 c3 = float3(1.000, 0.680, 0.200);
float3 c4 = float3(1.000, 0.970, 0.840);
t = clamp(t, 0.0, 1.0);
float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
c = mix(c, c2, smoothstep(0.22, 0.50, t));
c = mix(c, c3, smoothstep(0.50, 0.76, t));
return mix(c, c4, smoothstep(0.76, 1.00, t));
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float t = u.time * p.speed + 4.0;
// A camera on the ground near the chimney, looking up past it.
float side = min(u.resolution.x, u.resolution.y);
float2 s = (uv - 0.5) * u.resolution / side * 2.0;
float3 ro = float3(-0.7, 0.05, -3.4);
float3 target = float3(0.5, 1.25, 0.0);
float3 fw = normalize(target - ro);
float3 rt = normalize(cross(float3(0.0, 1.0, 0.0), fw));
float3 up = cross(fw, rt);
float3 rd = normalize(s.x * rt + s.y * up + 1.9 * fw);
// Behind everything, the sky; in front of it, maybe the chimney, where
// the smoke march stops.
float3 col = nightSky(rd, t);
float far = 14.0;
float hit = hitStack(ro, rd);
float glow = p.glow * (0.8 + 0.2 * sin(t * 7.0) * sin(t * 2.3));
if (hit > 0.0) {
col = brick(ro + rd * hit, glow);
far = hit;
}
// March through the smoke's box in even steps, from a random place in
// the first step, fixed for each pixel.
float3 lo = float3(-2.2, 0.0, -2.2), hi = float3(4.2, 5.5, 2.2);
float3 inv = 1.0 / rd;
float3 t0 = (lo - ro) * inv, t1 = (hi - ro) * inv;
float3 near = min(t0, t1), away = max(t0, t1);
float enter = max(max(near.x, near.y), max(near.z, 0.0));
float leave = min(min(away.x, away.y), min(away.z, far));
if (leave > enter) {
const int STEPS = 48;
float dt = (leave - enter) / float(STEPS);
float dist = enter + dt * hash31(float3(uv * u.resolution, 5.1));
float transmit = 1.0;
float3 light = float3(0.0);
float moonPhase = phase(dot(rd, MOON), p.forward);
for (int i = 0; i < STEPS; i++) {
if (dist > leave || transmit < 0.02) break;
float3 pos = ro + rd * dist;
float d = smokeAt(pos, t, p, 4);
if (d > 0.002) {
// Light sent to the eye: what the step takes out, times the
// light arriving. Then it dims what lies behind.
float stepT = exp(-d * dt);
float3 lit = lightAt(pos, rd, d, moonPhase, glow, t, p);
light += transmit * lit * (1.0 - stepT);
transmit *= stepT;
}
dist += dt;
}
col = col * transmit + light;
}
col = 1.0 - exp(-col * 1.6);
return float4(col, 1.0);
}episode-18-smoke-stack.metal, the whole file60 new lines since stage 4
// Ray Kiln · Episode 18 · Smoke Stack
//
// @episode 18 Smoke Stack
// @length 60
// @variation 30
// @variations 1,2,4,5,7,8,9,12
// @still 27.0
// @teaches Volumetric clouds: light marching, self-shadowing, scattering
// @category Fragment
// @tags volumetric rendering, light marching, self-shadowing, phase function, transmittance, procedural sound
// @final Smoke stack | The kiln's chimney at night, its smoke lit silver by the moon behind it and orange by the fire below, puffing harder each time the kiln is stoked.
//
// The kiln's chimney at night, and the smoke rising out of it. Smoke has no surface to find: it is a density at
// every point, so the ray walks through it in even steps and adds up what each bit does to the light. Some light is
// taken out on the way (absorbed or sent elsewhere), so what lies behind thick smoke fades. Some is sent toward the eye,
// and how much depends on how much light reaches that bit of smoke, which takes a second, shorter march toward the
// light: the smoke shadows itself. And it depends on the angle: smoke sends most light on in the direction it was going,
// so the edges glow where the moon is behind them.
// Each @param line below becomes a slider in the host app and a row in the page's parameters table:
// // @param name default min max description
// @param speed 1.0 0.0 2.0 How fast the smoke rises and the kiln is stoked
// @param wind 0.5 0.0 1.0 How hard the wind bends the plume
// @param thickness 0.6 0.1 1.0 How dense the smoke is
// @param puffs 0.5 0.0 1.0 From a steady stream to the kiln breathing out in puffs
// @param forward 0.6 0.0 0.9 How strongly the smoke sends light on the way it was going (the silver lining)
// @param glow 0.6 0.0 1.0 How bright the fire under the smoke burns
// @param volume 0.8 0.0 1.0 Loudness of the sound
constant float TAU = 6.2831853;
constant float PI = 3.1415927;
// How fast smoke rises, in units a second, and the chimney's radius.
constant float RISE = 0.45;
constant float STACK = 0.3;
// The kiln palette from episode 1.
float3 kiln(float t) {
float3 c0 = float3(0.020, 0.012, 0.020);
float3 c1 = float3(0.280, 0.040, 0.030);
float3 c2 = float3(0.880, 0.260, 0.050);
float3 c3 = float3(1.000, 0.680, 0.200);
float3 c4 = float3(1.000, 0.970, 0.840);
t = clamp(t, 0.0, 1.0);
float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
c = mix(c, c2, smoothstep(0.22, 0.50, t));
c = mix(c, c3, smoothstep(0.50, 0.76, t));
return mix(c, c4, smoothstep(0.76, 1.00, t));
}
// Episode 8's 3D hash, value noise and fractal noise.
float hash31(float3 p) {
uint3 v = uint3(int3(floor(p))) * 1664525u + 1013904223u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
v ^= v >> 16u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
return float(v.x) * (1.0 / 4294967295.0);
}
float vnoise3(float3 p) {
float3 i = floor(p), f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash31(i), b = hash31(i + float3(1, 0, 0));
float c = hash31(i + float3(0, 1, 0)), d = hash31(i + float3(1, 1, 0));
float e = hash31(i + float3(0, 0, 1)), g = hash31(i + float3(1, 0, 1));
float h = hash31(i + float3(0, 1, 1)), k = hash31(i + float3(1, 1, 1));
float lo = mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
float hi = mix(mix(e, g, f.x), mix(h, k, f.x), f.y);
return mix(lo, hi, f.z);
}
// Octaves of vnoise3, as episode 8's fbm3, but with the number of octaves
// given: the light march gets by with fewer.
float fbm3(float3 p, int octaves) {
float v = 0.0, a = 0.5;
for (int i = 0; i < octaves; i++) {
v += a * vnoise3(p);
p = p * 2.02 + float3(17.1, 9.2, 4.7);
a *= 0.5;
}
return v;
}
// ---- The smoke ---------------------------------------------------------------------------------------------------
// How hard the kiln breathes out at time t: a little all the time, a
// stoking every 20 seconds that sends up a thick column for a while,
// and puffs, every two or three seconds, on top.
float emission(float t, constant Params& p) {
float since = fract(t / 20.0 + 0.6) * 20.0;
float stoke = smoothstep(0.0, 1.0, since) * exp(-since * 0.22);
float puff = 0.5 + 0.5 * sin(t * 2.4 + 1.3 * sin(t * 0.9));
puff = mix(1.0, puff * puff * 1.6, p.puffs);
return (0.35 + 1.1 * stoke) * puff;
}
// The wind at time t: it swings slowly, so the plume leans further over and
// back across the minute.
float windAt(float t, constant Params& p) {
return p.wind * (0.55 + 0.45 * sin(t * 0.11 - 0.6));
}
// Smoke per unit of length at a point: 0 for clear air. The smoke at
// height h left the chimney h / RISE seconds ago, so it carries the kiln's
// breath from then; the wind carries it sideways the higher it goes, and it
// spreads and thins as it rises. Noise drifting up with it frays the edges.
float smokeAt(float3 pos, float t, constant Params& p, int octaves) {
float h = pos.y;
if (h < 0.0) return 0.0;
float left = t - h / RISE;
float w = windAt(left, p);
float3 axis = float3(w * h * h * 0.32 + w * h * 0.2, h, 0.0);
float r = STACK + 0.4 * h;
float3 q = pos - float3(w * t * 0.5, t * RISE, 0.0);
float n = fbm3(q * 1.7, octaves);
float edge = 1.0 - length((pos - axis).xz) / r;
float d = edge * 1.3 + (n - 0.5) * 2.2 - 0.1;
d = clamp(d, 0.0, 1.0) * emission(left, p);
d *= smoothstep(0.0, 0.15, h) * exp(-h * 0.28);
return d * 10.0 * p.thickness;
}
// ---- Light -------------------------------------------------------------------------------------------------------
// The moon: behind the smoke as the camera sees it, up and a little left.
constant float3 MOON = float3(-0.06, 0.55, 0.833);
constant float3 MOONLIGHT = float3(0.72, 0.80, 1.0);
// The fire in the chimney's mouth, glowing up into the smoke.
constant float3 FIRE = float3(0.0, -0.05, 0.0);
// Henyey-Greenstein's phase function: the share of scattered light sent at
// cosine c from its way. g = 0: every way alike; toward 1, straight on.
float phase(float c, float g) {
float k = 1.0 + g * g - 2.0 * g * c;
return (1.0 - g * g) / (4.0 * PI * k * sqrt(k));
}
// The light from a direction that reaches a point: a short march toward
// it adding up the smoke, then exp(-sum), Beer and Lambert's law.
float lightThrough(float3 pos, float3 dir, float t, constant Params& p,
float len, int steps) {
float ds = len / float(steps);
float sum = 0.0;
for (int i = 0; i < steps; i++) {
sum += smokeAt(pos + dir * (ds * (float(i) + 0.5)), t, p, 2);
}
return exp(-sum * ds);
}
// The night sky: dark blue overhead, warm haze low down from the kiln yard's
// lights, stars, and the moon with a glow round it.
float3 nightSky(float3 rd, float t) {
float up = clamp(rd.y, 0.0, 1.0);
float3 col = mix(float3(0.10, 0.045, 0.040), float3(0.012, 0.016, 0.040),
smoothstep(-0.05, 0.45, rd.y));
float2 a = float2(atan2(rd.x, rd.z), rd.y) * 90.0;
float star = hash31(float3(floor(a), 1.0));
float2 at = fract(a) - 0.5;
col += step(0.985, star) * up * smoothstep(0.12, 0.0, length(at)) * 0.7
* (0.75 + 0.25 * sin(t * 3.0 + star * 60.0));
float m = dot(rd, MOON);
col += MOONLIGHT * smoothstep(0.9993, 0.9995, m) * 1.4;
m = max(m, 0.0);
col += MOONLIGHT * (pow(m, 300.0) * 0.25 + pow(m, 12.0) * 0.05);
return col;
}
// ---- The chimney -------------------------------------------------------------------------------------------------
// Where a ray meets the outside of the chimney, a cylinder round the y axis
// up to y = 0, or -1.
float hitStack(float3 ro, float3 rd) {
float a = dot(rd.xz, rd.xz);
float b = dot(ro.xz, rd.xz);
float c = dot(ro.xz, ro.xz) - STACK * STACK;
float disc = b * b - a * c;
if (disc < 0.0) return -1.0;
float d = (-b - sqrt(disc)) / a;
return (d > 0.0 && ro.y + rd.y * d < 0.0) ? d : -1.0;
}
// The chimney's bricks at a point on it, lit by the moon on one side and,
// near the top, by the fire spilling over the rim.
float3 brick(float3 pos, float glow) {
float3 n = normalize(float3(pos.x, 0.0, pos.z));
float2 b = float2(atan2(pos.x, pos.z) * STACK / 0.11, pos.y / 0.06);
b.x += 0.5 * fmod(floor(b.y), 2.0);
float2 f = fract(b);
float mortar = smoothstep(0.0, 0.08, min(f.x, 1.0 - f.x) * 2.2)
* smoothstep(0.0, 0.1, min(f.y, 1.0 - f.y));
float3 col = mix(float3(0.10, 0.05, 0.04), float3(0.24, 0.10, 0.07),
hash31(float3(floor(b), 2.0)));
col *= 0.45 + 0.55 * mortar;
float moon = max(dot(n, MOON), 0.0) * 0.6 + 0.08;
float3 lit = col * MOONLIGHT * moon;
lit += kiln(0.6) * glow * exp(pos.y * 9.0) * 0.8 * col * 6.0;
return lit;
}
// The light a bit of smoke sends toward the eye, per unit it takes out.
float3 lightAt(float3 pos, float3 rd, float d, float moonPhase, float glow,
float t, constant Params& p) {
// The moon, through the smoke between, by the phase function.
float3 lit = MOONLIGHT * 5.0 * moonPhase
* lightThrough(pos, MOON, t, p, 1.6, 4);
// The fire, from below: through the smoke, fading with distance.
float3 toFire = FIRE - pos;
float fd = length(toFire);
toFire /= fd;
float fireT = lightThrough(pos, toFire, t, p, min(fd, 1.2), 2);
float firePhase = phase(dot(rd, -toFire), p.forward * 0.5) * 4.0 * PI;
lit += kiln(0.62) * 4.0 * glow * fireT * firePhase / (1.0 + fd * fd * 1.6);
// The sky's soft light.
lit += float3(0.03, 0.04, 0.08) * (1.0 - 0.5 * smoothstep(0.0, 1.5, d));
// Smoke scatters about half of what it takes out. Fresh smoke glows.
float heat = exp(-pos.y * 5.0) * glow;
return lit * 0.55 + kiln(0.5 + 0.3 * heat) * heat * 1.2;
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float t = u.time * p.speed + 4.0;
// A camera on the ground near the chimney, looking up past it.
float side = min(u.resolution.x, u.resolution.y);
float2 s = (uv - 0.5) * u.resolution / side * 2.0;
float3 ro = float3(-0.7, 0.05, -3.4);
float3 target = float3(0.5, 1.25, 0.0);
float3 fw = normalize(target - ro);
float3 rt = normalize(cross(float3(0.0, 1.0, 0.0), fw));
float3 up = cross(fw, rt);
float3 rd = normalize(s.x * rt + s.y * up + 1.9 * fw);
// Behind everything, the sky; in front of it, maybe the chimney, where
// the smoke march stops.
float3 col = nightSky(rd, t);
float far = 14.0;
float hit = hitStack(ro, rd);
float glow = p.glow * (0.8 + 0.2 * sin(t * 7.0) * sin(t * 2.3));
if (hit > 0.0) {
col = brick(ro + rd * hit, glow);
far = hit;
}
// March through the smoke's box in even steps, from a random place in
// the first step, fixed for each pixel.
float3 lo = float3(-2.2, 0.0, -2.2), hi = float3(4.2, 5.5, 2.2);
float3 inv = 1.0 / rd;
float3 t0 = (lo - ro) * inv, t1 = (hi - ro) * inv;
float3 near = min(t0, t1), away = max(t0, t1);
float enter = max(max(near.x, near.y), max(near.z, 0.0));
float leave = min(min(away.x, away.y), min(away.z, far));
if (leave > enter) {
const int STEPS = 48;
float dt = (leave - enter) / float(STEPS);
float dist = enter + dt * hash31(float3(uv * u.resolution, 5.1));
float transmit = 1.0;
float3 light = float3(0.0);
float moonPhase = phase(dot(rd, MOON), p.forward);
for (int i = 0; i < STEPS; i++) {
if (dist > leave || transmit < 0.02) break;
float3 pos = ro + rd * dist;
float d = smokeAt(pos, t, p, 4);
if (d > 0.002) {
// Light sent to the eye: what the step takes out, times the
// light arriving. Then it dims what lies behind.
float stepT = exp(-d * dt);
float3 lit = lightAt(pos, rd, d, moonPhase, glow, t, p);
light += transmit * lit * (1.0 - stepT);
transmit *= stepT;
}
dist += dt;
}
col = col * transmit + light;
}
col = 1.0 - exp(-col * 1.6);
return float4(col, 1.0);
}
// ---- Sound -------------------------------------------------------------------------------------------------------
// Episode 5's sound hash: a random number from a whole number.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// Episode 5's sound noise: random values joined `rate` times a second.
float noise1(float t, float rate, uint seed) {
float x = t * rate;
float i = floor(x), f = fract(x);
f = f * f * (3.0 - 2.0 * f);
uint k = uint(i) + seed * 7919u;
return mix(hashU(k), hashU(k + 1u), f) * 2.0 - 1.0;
}
float2 sound(float t, constant Params& p) {
float tt = t * p.speed + 4.0;
// The kiln breathing out, as the picture's smoke leaves the chimney.
float breath = emission(tt, p);
float since = fract(tt / 20.0 + 0.6) * 20.0;
// The draft: air rushing up the stack, a dark rumble of slow noise
// and a hollow note, both louder with each breath.
float rumble = noise1(t, 90.0, 1u) * 0.6 + noise1(t, 230.0, 2u) * 0.4;
float hum = sin(TAU * 49.0 * t + 0.6 * noise1(t, 3.0, 3u))
+ 0.4 * sin(TAU * 98.0 * t);
float draft = rumble * 0.34 * (0.4 + 0.5 * breath)
+ hum * 0.1 * (0.5 + 0.5 * p.glow);
// The smoke going: a soft hiss, whooshing with the puffs, panned with
// the wind that carries it.
float hiss = noise1(t, 2600.0, 4u) * 0.6 + noise1(t, 900.0, 5u) * 0.4;
hiss *= 0.07 * breath * p.thickness;
float pan = 0.5 + 0.35 * windAt(tt, p);
// The fire: crackles, short bright bursts at random moments, many just
// after a stoking, a few the rest of the time.
float slot = floor(t * 14.0);
float age = fract(t * 14.0) / 14.0;
float chance = 0.08 + 0.6 * exp(-since * 0.4);
float crack = hashU(uint(slot) * 3u + 11u) < chance * p.glow ? 1.0 : 0.0;
float burst = noise1(t, 7000.0, 6u) * exp(-age * 260.0)
* smoothstep(0.0, 0.0006, age) * crack;
float side2 = hashU(uint(slot) * 3u + 12u);
float2 mixdown = float2(draft)
+ hiss * float2(1.0 - pan, pan) * 1.6
+ burst * 0.45 * float2(1.0 - side2, side2);
float fadeIn = smoothstep(0.0, 1.5, t);
return tanh(mixdown * 1.5) * 0.8 * fadeIn * p.volume;
}4 lines from stage 4 that are gone
float w = p.wind;
⋯
float d = clamp(edge * 1.3 + (n - 0.5) * 2.2 - 0.1, 0.0, 1.0);
⋯
float3 nightSky(float3 rd) {
⋯
float3 col = nightSky(rd);The sound
The sound is worked out from the time alone, and reads the same emission and wind as the picture.
- The draft. Air rushing up the chimney: a dark rumble of slow noise, louder with every breath the kiln takes, and a hollow note at 49 Hz with its octave, wavering a little and stronger as the fire burns brighter.
- The smoke. A soft hiss, loudest with each puff and with
thickness, panned by the wind that carries the smoke. - The fire. Crackles, short bright bursts of fine noise at random moments: many just after each stoking, a few the rest of the time, and none with
glowat 0. Each one starts over half a millisecond, so none clicks.
Everything goes through tanh and fades in over the first second and a half. At the defaults the peak is about -7.4 dBFS and the average (RMS) about -21 dBFS.
New in Ray Kiln
The film strip. In the playground, press Film strip under the picture and eight stills appear in a row below the clock, one from the middle of each eighth of the shader's length (its // @length, or a minute). Click one and the clock goes to that moment; the one whose eighth the clock is in is marked as it plays. The stills are drawn as an export draws those moments, with the sliders as they are, and drawn again a moment after you change the code or a slider. A particle episode runs every step up to each still, and a feedback episode every frame, so they look as the video would. It works for every shader, and it is free.
This episode changes over its minute, so the strip is a map of it. In the finished shader the still at 3.75 s is just before a stoking and the one at 26.25 s just after the next, the thick column standing over the chimney; across the row the plume leans over with the wind and straightens again. Move wind or puffs and watch all eight change together.
Try this
- Set
forwardto 0, then to 0.9: the same smoke, dull and then blazing round the moon. That is the phase function alone. - Set
windto 0: the plume rises straight up, in front of the moon, and is lit all round its edge. - Set
thicknessto 1 andpuffsto 1: dense separate puffs, each dark in its middle where it shadows itself. - Set
glowto 1 andspeedto 0.3: the fire lights the underside of every puff as it slowly climbs. - In
lightThrough, changereturn exp(-sum * ds);toreturn 1.0;: the light march is gone, nothing shadows anything, and the smoke goes flat. - In
shade, changeconst int STEPS = 48;toconst int STEPS = 12;: the march takes big steps, and the smoke's shapes coarsen into slabs and grain.
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 2.0 | How fast the smoke rises and the kiln is stoked |
wind | 0.5 | 0.0 to 1.0 | How hard the wind bends the plume |
thickness | 0.6 | 0.1 to 1.0 | How dense the smoke is |
puffs | 0.5 | 0.0 to 1.0 | From a steady stream to the kiln breathing out in puffs |
forward | 0.6 | 0.0 to 0.9 | How strongly the smoke sends light on the way it was going (the silver lining) |
glow | 0.6 | 0.0 to 1.0 | How bright the fire under the smoke burns |
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-18-smoke-stack.metal
// Ray Kiln · Episode 18 · Smoke Stack
//
// @episode 18 Smoke Stack
// @length 60
// @variation 30
// @variations 1,2,4,5,7,8,9,12
// @still 27.0
// @teaches Volumetric clouds: light marching, self-shadowing, scattering
// @category Fragment
// @tags volumetric rendering, light marching, self-shadowing, phase function, transmittance, procedural sound
// @final Smoke stack | The kiln's chimney at night, its smoke lit silver by the moon behind it and orange by the fire below, puffing harder each time the kiln is stoked.
//
// The kiln's chimney at night, and the smoke rising out of it. Smoke has no surface to find: it is a density at
// every point, so the ray walks through it in even steps and adds up what each bit does to the light. Some light is
// taken out on the way (absorbed or sent elsewhere), so what lies behind thick smoke fades. Some is sent toward the eye,
// and how much depends on how much light reaches that bit of smoke, which takes a second, shorter march toward the
// light: the smoke shadows itself. And it depends on the angle: smoke sends most light on in the direction it was going,
// so the edges glow where the moon is behind them.
// Each @param line below becomes a slider in the host app and a row in the page's parameters table:
// // @param name default min max description
// @param speed 1.0 0.0 2.0 How fast the smoke rises and the kiln is stoked
// @param wind 0.5 0.0 1.0 How hard the wind bends the plume
// @param thickness 0.6 0.1 1.0 How dense the smoke is
// @param puffs 0.5 0.0 1.0 From a steady stream to the kiln breathing out in puffs
// @param forward 0.6 0.0 0.9 How strongly the smoke sends light on the way it was going (the silver lining)
// @param glow 0.6 0.0 1.0 How bright the fire under the smoke burns
// @param volume 0.8 0.0 1.0 Loudness of the sound
constant float TAU = 6.2831853;
constant float PI = 3.1415927;
// How fast smoke rises, in units a second, and the chimney's radius.
constant float RISE = 0.45;
constant float STACK = 0.3;
// The kiln palette from episode 1.
float3 kiln(float t) {
float3 c0 = float3(0.020, 0.012, 0.020);
float3 c1 = float3(0.280, 0.040, 0.030);
float3 c2 = float3(0.880, 0.260, 0.050);
float3 c3 = float3(1.000, 0.680, 0.200);
float3 c4 = float3(1.000, 0.970, 0.840);
t = clamp(t, 0.0, 1.0);
float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
c = mix(c, c2, smoothstep(0.22, 0.50, t));
c = mix(c, c3, smoothstep(0.50, 0.76, t));
return mix(c, c4, smoothstep(0.76, 1.00, t));
}
// Episode 8's 3D hash, value noise and fractal noise.
float hash31(float3 p) {
uint3 v = uint3(int3(floor(p))) * 1664525u + 1013904223u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
v ^= v >> 16u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
return float(v.x) * (1.0 / 4294967295.0);
}
float vnoise3(float3 p) {
float3 i = floor(p), f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash31(i), b = hash31(i + float3(1, 0, 0));
float c = hash31(i + float3(0, 1, 0)), d = hash31(i + float3(1, 1, 0));
float e = hash31(i + float3(0, 0, 1)), g = hash31(i + float3(1, 0, 1));
float h = hash31(i + float3(0, 1, 1)), k = hash31(i + float3(1, 1, 1));
float lo = mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
float hi = mix(mix(e, g, f.x), mix(h, k, f.x), f.y);
return mix(lo, hi, f.z);
}
// Octaves of vnoise3, as episode 8's fbm3, but with the number of octaves
// given: the light march gets by with fewer.
float fbm3(float3 p, int octaves) {
float v = 0.0, a = 0.5;
for (int i = 0; i < octaves; i++) {
v += a * vnoise3(p);
p = p * 2.02 + float3(17.1, 9.2, 4.7);
a *= 0.5;
}
return v;
}
// ---- The smoke ---------------------------------------------------------------------------------------------------
// How hard the kiln breathes out at time t: a little all the time, a
// stoking every 20 seconds that sends up a thick column for a while,
// and puffs, every two or three seconds, on top.
float emission(float t, constant Params& p) {
float since = fract(t / 20.0 + 0.6) * 20.0;
float stoke = smoothstep(0.0, 1.0, since) * exp(-since * 0.22);
float puff = 0.5 + 0.5 * sin(t * 2.4 + 1.3 * sin(t * 0.9));
puff = mix(1.0, puff * puff * 1.6, p.puffs);
return (0.35 + 1.1 * stoke) * puff;
}
// The wind at time t: it swings slowly, so the plume leans further over and
// back across the minute.
float windAt(float t, constant Params& p) {
return p.wind * (0.55 + 0.45 * sin(t * 0.11 - 0.6));
}
// Smoke per unit of length at a point: 0 for clear air. The smoke at
// height h left the chimney h / RISE seconds ago, so it carries the kiln's
// breath from then; the wind carries it sideways the higher it goes, and it
// spreads and thins as it rises. Noise drifting up with it frays the edges.
float smokeAt(float3 pos, float t, constant Params& p, int octaves) {
float h = pos.y;
if (h < 0.0) return 0.0;
float left = t - h / RISE;
float w = windAt(left, p);
float3 axis = float3(w * h * h * 0.32 + w * h * 0.2, h, 0.0);
float r = STACK + 0.4 * h;
float3 q = pos - float3(w * t * 0.5, t * RISE, 0.0);
float n = fbm3(q * 1.7, octaves);
float edge = 1.0 - length((pos - axis).xz) / r;
float d = edge * 1.3 + (n - 0.5) * 2.2 - 0.1;
d = clamp(d, 0.0, 1.0) * emission(left, p);
d *= smoothstep(0.0, 0.15, h) * exp(-h * 0.28);
return d * 10.0 * p.thickness;
}
// ---- Light -------------------------------------------------------------------------------------------------------
// The moon: behind the smoke as the camera sees it, up and a little left.
constant float3 MOON = float3(-0.06, 0.55, 0.833);
constant float3 MOONLIGHT = float3(0.72, 0.80, 1.0);
// The fire in the chimney's mouth, glowing up into the smoke.
constant float3 FIRE = float3(0.0, -0.05, 0.0);
// Henyey-Greenstein's phase function: the share of scattered light sent at
// cosine c from its way. g = 0: every way alike; toward 1, straight on.
float phase(float c, float g) {
float k = 1.0 + g * g - 2.0 * g * c;
return (1.0 - g * g) / (4.0 * PI * k * sqrt(k));
}
// The light from a direction that reaches a point: a short march toward
// it adding up the smoke, then exp(-sum), Beer and Lambert's law.
float lightThrough(float3 pos, float3 dir, float t, constant Params& p,
float len, int steps) {
float ds = len / float(steps);
float sum = 0.0;
for (int i = 0; i < steps; i++) {
sum += smokeAt(pos + dir * (ds * (float(i) + 0.5)), t, p, 2);
}
return exp(-sum * ds);
}
// The night sky: dark blue overhead, warm haze low down from the kiln yard's
// lights, stars, and the moon with a glow round it.
float3 nightSky(float3 rd, float t) {
float up = clamp(rd.y, 0.0, 1.0);
float3 col = mix(float3(0.10, 0.045, 0.040), float3(0.012, 0.016, 0.040),
smoothstep(-0.05, 0.45, rd.y));
float2 a = float2(atan2(rd.x, rd.z), rd.y) * 90.0;
float star = hash31(float3(floor(a), 1.0));
float2 at = fract(a) - 0.5;
col += step(0.985, star) * up * smoothstep(0.12, 0.0, length(at)) * 0.7
* (0.75 + 0.25 * sin(t * 3.0 + star * 60.0));
float m = dot(rd, MOON);
col += MOONLIGHT * smoothstep(0.9993, 0.9995, m) * 1.4;
m = max(m, 0.0);
col += MOONLIGHT * (pow(m, 300.0) * 0.25 + pow(m, 12.0) * 0.05);
return col;
}
// ---- The chimney -------------------------------------------------------------------------------------------------
// Where a ray meets the outside of the chimney, a cylinder round the y axis
// up to y = 0, or -1.
float hitStack(float3 ro, float3 rd) {
float a = dot(rd.xz, rd.xz);
float b = dot(ro.xz, rd.xz);
float c = dot(ro.xz, ro.xz) - STACK * STACK;
float disc = b * b - a * c;
if (disc < 0.0) return -1.0;
float d = (-b - sqrt(disc)) / a;
return (d > 0.0 && ro.y + rd.y * d < 0.0) ? d : -1.0;
}
// The chimney's bricks at a point on it, lit by the moon on one side and,
// near the top, by the fire spilling over the rim.
float3 brick(float3 pos, float glow) {
float3 n = normalize(float3(pos.x, 0.0, pos.z));
float2 b = float2(atan2(pos.x, pos.z) * STACK / 0.11, pos.y / 0.06);
b.x += 0.5 * fmod(floor(b.y), 2.0);
float2 f = fract(b);
float mortar = smoothstep(0.0, 0.08, min(f.x, 1.0 - f.x) * 2.2)
* smoothstep(0.0, 0.1, min(f.y, 1.0 - f.y));
float3 col = mix(float3(0.10, 0.05, 0.04), float3(0.24, 0.10, 0.07),
hash31(float3(floor(b), 2.0)));
col *= 0.45 + 0.55 * mortar;
float moon = max(dot(n, MOON), 0.0) * 0.6 + 0.08;
float3 lit = col * MOONLIGHT * moon;
lit += kiln(0.6) * glow * exp(pos.y * 9.0) * 0.8 * col * 6.0;
return lit;
}
// The light a bit of smoke sends toward the eye, per unit it takes out.
float3 lightAt(float3 pos, float3 rd, float d, float moonPhase, float glow,
float t, constant Params& p) {
// The moon, through the smoke between, by the phase function.
float3 lit = MOONLIGHT * 5.0 * moonPhase
* lightThrough(pos, MOON, t, p, 1.6, 4);
// The fire, from below: through the smoke, fading with distance.
float3 toFire = FIRE - pos;
float fd = length(toFire);
toFire /= fd;
float fireT = lightThrough(pos, toFire, t, p, min(fd, 1.2), 2);
float firePhase = phase(dot(rd, -toFire), p.forward * 0.5) * 4.0 * PI;
lit += kiln(0.62) * 4.0 * glow * fireT * firePhase / (1.0 + fd * fd * 1.6);
// The sky's soft light.
lit += float3(0.03, 0.04, 0.08) * (1.0 - 0.5 * smoothstep(0.0, 1.5, d));
// Smoke scatters about half of what it takes out. Fresh smoke glows.
float heat = exp(-pos.y * 5.0) * glow;
return lit * 0.55 + kiln(0.5 + 0.3 * heat) * heat * 1.2;
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float t = u.time * p.speed + 4.0;
// A camera on the ground near the chimney, looking up past it.
float side = min(u.resolution.x, u.resolution.y);
float2 s = (uv - 0.5) * u.resolution / side * 2.0;
float3 ro = float3(-0.7, 0.05, -3.4);
float3 target = float3(0.5, 1.25, 0.0);
float3 fw = normalize(target - ro);
float3 rt = normalize(cross(float3(0.0, 1.0, 0.0), fw));
float3 up = cross(fw, rt);
float3 rd = normalize(s.x * rt + s.y * up + 1.9 * fw);
// Behind everything, the sky; in front of it, maybe the chimney, where
// the smoke march stops.
float3 col = nightSky(rd, t);
float far = 14.0;
float hit = hitStack(ro, rd);
float glow = p.glow * (0.8 + 0.2 * sin(t * 7.0) * sin(t * 2.3));
if (hit > 0.0) {
col = brick(ro + rd * hit, glow);
far = hit;
}
// March through the smoke's box in even steps, from a random place in
// the first step, fixed for each pixel.
float3 lo = float3(-2.2, 0.0, -2.2), hi = float3(4.2, 5.5, 2.2);
float3 inv = 1.0 / rd;
float3 t0 = (lo - ro) * inv, t1 = (hi - ro) * inv;
float3 near = min(t0, t1), away = max(t0, t1);
float enter = max(max(near.x, near.y), max(near.z, 0.0));
float leave = min(min(away.x, away.y), min(away.z, far));
if (leave > enter) {
const int STEPS = 48;
float dt = (leave - enter) / float(STEPS);
float dist = enter + dt * hash31(float3(uv * u.resolution, 5.1));
float transmit = 1.0;
float3 light = float3(0.0);
float moonPhase = phase(dot(rd, MOON), p.forward);
for (int i = 0; i < STEPS; i++) {
if (dist > leave || transmit < 0.02) break;
float3 pos = ro + rd * dist;
float d = smokeAt(pos, t, p, 4);
if (d > 0.002) {
// Light sent to the eye: what the step takes out, times the
// light arriving. Then it dims what lies behind.
float stepT = exp(-d * dt);
float3 lit = lightAt(pos, rd, d, moonPhase, glow, t, p);
light += transmit * lit * (1.0 - stepT);
transmit *= stepT;
}
dist += dt;
}
col = col * transmit + light;
}
col = 1.0 - exp(-col * 1.6);
return float4(col, 1.0);
}
// ---- Sound -------------------------------------------------------------------------------------------------------
// Episode 5's sound hash: a random number from a whole number.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// Episode 5's sound noise: random values joined `rate` times a second.
float noise1(float t, float rate, uint seed) {
float x = t * rate;
float i = floor(x), f = fract(x);
f = f * f * (3.0 - 2.0 * f);
uint k = uint(i) + seed * 7919u;
return mix(hashU(k), hashU(k + 1u), f) * 2.0 - 1.0;
}
float2 sound(float t, constant Params& p) {
float tt = t * p.speed + 4.0;
// The kiln breathing out, as the picture's smoke leaves the chimney.
float breath = emission(tt, p);
float since = fract(tt / 20.0 + 0.6) * 20.0;
// The draft: air rushing up the stack, a dark rumble of slow noise
// and a hollow note, both louder with each breath.
float rumble = noise1(t, 90.0, 1u) * 0.6 + noise1(t, 230.0, 2u) * 0.4;
float hum = sin(TAU * 49.0 * t + 0.6 * noise1(t, 3.0, 3u))
+ 0.4 * sin(TAU * 98.0 * t);
float draft = rumble * 0.34 * (0.4 + 0.5 * breath)
+ hum * 0.1 * (0.5 + 0.5 * p.glow);
// The smoke going: a soft hiss, whooshing with the puffs, panned with
// the wind that carries it.
float hiss = noise1(t, 2600.0, 4u) * 0.6 + noise1(t, 900.0, 5u) * 0.4;
hiss *= 0.07 * breath * p.thickness;
float pan = 0.5 + 0.35 * windAt(tt, p);
// The fire: crackles, short bright bursts at random moments, many just
// after a stoking, a few the rest of the time.
float slot = floor(t * 14.0);
float age = fract(t * 14.0) / 14.0;
float chance = 0.08 + 0.6 * exp(-since * 0.4);
float crack = hashU(uint(slot) * 3u + 11u) < chance * p.glow ? 1.0 : 0.0;
float burst = noise1(t, 7000.0, 6u) * exp(-age * 260.0)
* smoothstep(0.0, 0.0006, age) * crack;
float side2 = hashU(uint(slot) * 3u + 12u);
float2 mixdown = float2(draft)
+ hiss * float2(1.0 - pan, pan) * 1.6
+ burst * 0.45 * float2(1.0 - side2, side2);
float fadeIn = smoothstep(0.0, 1.5, t);
return tanh(mixdown * 1.5) * 0.8 * fadeIn * p.volume;
}Running it in your own project
Every Ray Kiln shader is the same shape. The standalone download above is the shader with the small wrapper around it that the Ray Kiln host adds, so it compiles with the ordinary Metal compiler and runs in your own app. It defines three entry points: rk_vertex (a full-screen triangle), rk_fragment (calls the shader's shade) and, for shaders with sound, the compute kernel rk_sound.
To draw it, pass the uniforms at fragment buffer 0 and the parameter values at buffer 1, in the order they are declared:
Drawing it, in Swift
struct Uniforms {
var resolution: SIMD2<Float>
var time: Float
var timeDelta: Float
var mouse: SIMD2<Float>
var frame: UInt32
var pad: UInt32 = 0
}
let library = try device.makeLibrary(source: standaloneSource, options: nil)
let descriptor = MTLRenderPipelineDescriptor()
descriptor.vertexFunction = library.makeFunction(name: "rk_vertex")
descriptor.fragmentFunction = library.makeFunction(name: "rk_fragment")
descriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
let pipeline = try device.makeRenderPipelineState(descriptor: descriptor)
// Each frame, inside a render pass:
var uniforms = Uniforms(resolution: size, time: time, timeDelta: dt, mouse: mouse, frame: frame)
var params: [Float] = [1.0, 0.5, 0.6, 0.5, 0.6, 0.6, 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.
Press ? for keyboard shortcuts.