// Ray Kiln · Week 2 · Morphing Shapes // // @episode 2 Morphing Shapes // @length 60 // @short 30 // @still 19.2 // @teaches 2D signed distance fields and smooth blending // @category Fragment // @tags signed distance fields, smooth minimum, morphing, procedural sound // @final Heat and glow | Colour by distance: a hot core inside, a glow outside, and rings that ripple away from the edge. // // A circle, a box and a triangle described only by their distance functions, morphing into each other while three // small circles orbit and melt into them, run through the kiln palette. // Each @param line below becomes a slider in the host app and a row in the page's parameters table: // // @param name default min max description // @param speed 0.5 0.0 2.0 How fast the shapes morph and orbit // @param size 0.55 0.2 0.9 Size of the shape // @param blend 0.35 0.0 1.0 How softly the shapes melt into each other // @param rings 6.0 0.0 16.0 Contour lines per unit of distance // @param heat 0.5 0.0 1.0 Slides the palette from deep ember to white-hot // @param volume 0.8 0.0 1.0 Loudness of the sound float sdCircle(float2 p, float r) { return length(p) - r; } float sdBox(float2 p, float2 b) { float2 d = abs(p) - b; return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0); } float sdTriangle(float2 p, float r) { const float k = 1.7320508; p.x = abs(p.x) - r; p.y = p.y + r / k; if (p.x + k * p.y > 0.0) p = float2(p.x - k * p.y, -k * p.x - p.y) / 2.0; p.x -= clamp(p.x, -2.0 * r, 0.0); return -length(p) * sign(p.y); } float morphShape(float2 p, float r, float t) { float s = fract(t / 3.0) * 3.0; float f = smoothstep(0.5, 1.0, fract(s)); float a = sdCircle(p, r); float b = sdBox(p, float2(r * 0.8)) - r * 0.1; float c = sdTriangle(p, r * 1.2) - r * 0.1; if (s < 1.0) return mix(a, b, f); if (s < 2.0) return mix(b, c, f); return mix(c, a, f); } float smin(float a, float b, float k) { float h = max(k - abs(a - b), 0.0) / k; return min(a, b) - h * h * k * 0.25; } float2 orbit(int i, float t) { float a = t * (0.7 + 0.23 * float(i)) + 2.094 * float(i); float2 o = float2(cos(a), sin(a) * 0.8); return o * (0.8 + 0.12 * sin(t * 0.5 + float(i))); } float scene(float2 p, float r, float t, float k) { float d = morphShape(p, r, t); for (int i = 0; i < 3; i++) { d = smin(d, sdCircle(p - orbit(i, t), r * 0.32), k); } return d; } // The kiln palette from week 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 px = 2.4 / min(u.resolution.x, u.resolution.y); float2 q = (uv - 0.5) * u.resolution * px; float t = u.time * p.speed; float k = max(p.blend * 0.6, 0.0001); float d = scene(q, p.size, t, k); // Inside: hotter towards the middle. float core = 0.62 + 0.38 * clamp(-d * 2.5, 0.0, 1.0); // Outside: a glow, and rings rippling outward. float glow = exp(-max(d, 0.0) * 4.0); float ring = 0.5 + 0.5 * cos(6.2831853 * (p.rings * d - t)); float outside = 0.06 + 0.36 * glow + 0.16 * ring * exp(-max(d, 0.0) * 1.5); // Join the two over one pixel, then slide along the palette by heat. float heat = mix(core, outside, smoothstep(-px, px, d)); float3 col = kiln(heat + (p.heat - 0.5) * 0.5); return float4(col, 1.0); } // ---- Sound ----------------------------------------------------------------------------------------------------- // The sound follows the same clock as the picture. A low chord changes the shape of its wave with the shape on screen: // a sine for the circle, a square for the box, a triangle for the triangle. Each orbiting circle sings a high note // that swells as it melts into the middle shape, and a soft bell rings each time a morph begins. // One wave, part sine, part square, part triangle, at phase `ph` in cycles. The square and triangle are built from // their first four odd harmonics, so they stay soft. float morphWave(float ph, float3 w) { const float tau = 6.2831853; float sq = 0.0, tri = 0.0; for (int n = 1; n <= 7; n += 2) { float s = sin(tau * float(n) * ph); sq += s / float(n); tri += s * ((n / 2) % 2 == 0 ? 1.0 : -1.0) / float(n * n); } return w.x * sin(tau * ph) + w.y * sq * 0.75 + w.z * tri * 0.81; } float2 sound(float t, constant Params& p) { const float tau = 6.2831853; float ts = t * p.speed; // How much of each shape is on screen, from the same clock as morphShape. float s = fract(ts / 3.0) * 3.0; float f = smoothstep(0.5, 1.0, fract(s)); float3 w = s < 1.0 ? float3(1.0 - f, f, 0.0) : (s < 2.0 ? float3(0.0, 1.0 - f, f) : float3(f, 0.0, 1.0 - f)); // A chord on A (A2 E3 A3), a few cents apart in each ear, with the wave shape of the shape on screen. const float freq[3] = {110.0, 164.81, 220.0}; const float amp[3] = {1.0, 0.6, 0.45}; float2 chord = float2(0.0); for (int i = 0; i < 3; i++) { chord.x += amp[i] * morphWave(freq[i] * 1.001 * t, w); chord.y += amp[i] * morphWave(freq[i] * 0.999 * t + 0.25, w); } chord *= (0.8 + 0.2 * sin(tau * t / 9.0)) * 0.09; // The orbiting circles: a note each (A4, C#5, E5), louder the closer the circle is to melting into the middle, // and heard from where it is on the screen. const float note[3] = {440.0, 554.37, 659.25}; float2 high = float2(0.0); for (int i = 0; i < 3; i++) { float2 o = orbit(i, ts); float gap = morphShape(o, p.size, ts) - p.size * 0.32; float near = exp(-max(gap, 0.0) * (4.0 / (0.15 + p.blend))); float tone = sin(tau * note[i] * t) + 0.25 * sin(tau * note[i] * 2.0 * t); float pan = clamp(0.5 + 0.4 * o.x, 0.0, 1.0); high += float2(1.0 - pan, pan) * tone * near * 0.04; } // A soft bell as each morph begins, fading out before the next one. // Before the morph begins `since` is clamped to 0, where the envelope is silent (and exp cannot overflow). float since = max(fract(s) - 0.5, 0.0); float bellEnv = exp(-since * 7.0) * (1.0 - exp(-since * 300.0)) * (1.0 - smoothstep(0.35, 0.5, since)); float bell = (sin(tau * 880.0 * t) + 0.4 * sin(tau * 1320.0 * t) + 0.2 * sin(tau * 2217.0 * t)) * bellEnv * 0.05; float fadeIn = smoothstep(0.0, 2.0, t); return tanh((chord + high + float2(bell)) * 2.0) * fadeIn * p.volume; }