// Ray Kiln ยท Kiln Floor Tiles // // @episode Kiln Floor Tiles // @length 60 // @variation 30 // @variations 2,3,4,6,7,8,9,12 // @sweeps cells, line, heat // @still 20.0 // @teaches Hex grids, Truchet tiles, wallpaper symmetry // @category Fragment // @tags hex grid, truchet tiles, wallpaper symmetry, periodic noise, procedural sound // @final Kiln floor | Clay tiles with bevelled edges, fire showing through the grout, the glowing channels lit by a light that wanders and wraps round the picture. // // The floor of a kiln laid with hexagonal tiles. Each tile carries three arcs of glowing glaze, and since every arc // ends at the middle of an edge, the arcs join their neighbours' into lines that wind across the whole floor: Truchet // tiles. Now and then a tile turns a sixth of the way round and the lines join up another way. The floor is a // wallpaper: it fits a whole number of tiles in the picture, and everything on it, the tiles, the heat flowing along // the channels and the light, repeats with the picture, so copies laid side by side meet with no seam. // 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 heat flows and the tiles turn // @param cells 9.0 3.0 16.0 How many tiles across the picture // @param turns 0.5 0.0 1.0 How often a tile turns a sixth of the way round // @param order 0.0 0.0 1.0 From tiles laid at random to every tile laid the same way // @param line 0.5 0.0 1.0 How wide the glowing channels are // @param heat 0.5 0.0 1.0 Slides the glow from deep ember to white-hot // @param volume 0.8 0.0 1.0 Loudness of the sound constant float TAU = 6.2831853; constant float SQ3 = 1.7320508; // How long a turn takes. constant float TURN = 0.8; // Floating 3D Scene's rotation. float2 rot(float2 p, float a) { float c = cos(a), s = sin(a); return float2(c * p.x - s * p.y, s * p.x + c * p.y); } // The kiln palette from Flowing Color Field. 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)); } // Flowing Color Field's hash: a random number from a point. float hash21(float2 p) { p = fract(p * float2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); } // Wrap x into 0 to m, below 0 too. float wrap(float x, float m) { return x - m * floor(x / m); } // ---- The hex grid ---------------------------------------------------------------------------------------------- // Whole tiles across, whole pairs of rows down: the picture holds whole // tiles both ways. float2 floorSize(float2 res, float cells) { float n = floor(cells + 0.5); float pairs = max(floor(n * res.y / (res.x * SQ3) + 0.5), 1.0); return float2(n, pairs); } // Two grids of rectangles, one shifted by half: a point belongs to the // nearer centre. xy is the point from its centre, zw the centre itself. float4 hexCell(float2 p) { float2 r = float2(1.0, SQ3), h = r * 0.5; float2 a = p - r * floor(p / r) - h; float2 b = (p - h) - r * floor((p - h) / r) - h; float2 gv = dot(a, a) < dot(b, b) ? a : b; return float4(gv, p - gv); } // A tile's number, wrapped: the tile leaving one edge is the one coming // in at the other. float2 tileKey(float2 centre, float2 size) { float col = floor(centre.x * 2.0 + 0.5); float row = floor(centre.y * 2.0 / SQ3 + 0.5); return float2(wrap(col, size.x * 2.0), wrap(row, size.y * 2.0)); } // Distance in the hexagon's own measure: 0.5 on its edges. float hexDist(float2 gv) { gv = abs(gv); return max(gv.x, dot(gv, float2(0.5, SQ3 * 0.5))); } // ---- Truchet arcs ---------------------------------------------------------------------------------------------- // Three arcs round every other corner of the tile. Each joins the middles // of the two edges beside its corner, and meets them square, so it carries // straight on into the neighbour's arc. Turned a sixth, the other three // corners take them. float truchet(float2 gv) { float R = 1.0 / SQ3; float d = 1e3; for (int k = 0; k < 3; k++) { float a = TAU / 12.0 + float(k) * TAU / 3.0; float2 corner = R * float2(cos(a), sin(a)); d = min(d, abs(length(gv - corner) - R * 0.5)); } return d; } // How far a tile has turned, in sixths: 0 or 1 by its hash, then one // more every `every` seconds, eased over TURN. float tileTurn(float2 key, float tt, float order, float every) { float base = step(hash21(key + 3.1), 0.5 * (1.0 - order)); if (every <= 0.0) return base; float offset = hash21(key + 7.7); float k = floor(tt / every - offset); float since = tt - (k + offset) * every; float e = smoothstep(0.0, TURN, since); return base + k + e; } // Seconds between a tile's turns; none for turns 0. float turnEvery(float turns) { return turns > 0.01 ? 6.0 / turns : 0.0; } // ---- Heat that repeats with the floor -------------------------------------------------------------------------- // Value noise on a lattice that wraps every `period` cells (whole numbers), // so it repeats exactly. Moving it keeps it repeating. float pnoise(float2 q, float2 period) { float2 i = floor(q), f = fract(q); f = f * f * (3.0 - 2.0 * f); float a = hash21(float2(wrap(i.x, period.x), wrap(i.y, period.y))); float b = hash21(float2(wrap(i.x + 1.0, period.x), wrap(i.y, period.y))); float c = hash21(float2(wrap(i.x, period.x), wrap(i.y + 1.0, period.y))); float d = hash21(float2(wrap(i.x + 1.0, period.x), wrap(i.y + 1.0, period.y))); return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); } // Three octaves, each lattice twice as fine and still whole: it repeats too. float pfbm(float2 uv, float2 period, float2 drift) { float v = 0.0, a = 0.5; for (int i = 0; i < 3; i++) { v += a * pnoise(uv * period + drift, period); period *= 2.0; drift *= 2.0; a *= 0.5; } return v / 0.875; } // ---- The floor ------------------------------------------------------------------------------------------------- float4 shade(float2 uv, constant Uniforms& u, constant Params& p) { float2 size = floorSize(u.resolution, p.cells); float2 sp = uv * float2(size.x, size.y * SQ3); float4 cell = hexCell(sp); float2 key = tileKey(cell.zw, size); float tt = u.time * p.speed; // Lifted a little while it turns, so the grout opens round it. float turn = tileTurn(key, tt, p.order, turnEvery(p.turns)); float lift = sin(3.14159 * fract(turn)) * step(0.001, fract(turn)); float2 gv = rot(cell.xy, turn * TAU / 6.0) * (1.0 + 0.1 * lift); float aa = 1.5 * size.x / u.resolution.x; float edge = 0.5 - hexDist(gv); float d = truchet(gv); // The heat under the floor, flowing, on a lattice of whole tiles. float2 period = float2(size.x, size.y * 2.0); float flow = pfbm(uv, period, float2(tt * 0.6, tt * 0.25)); float glowT = 0.12 + p.heat * 0.5 + (flow - 0.35) * 0.8; // The clay: a shade of its own for each tile, a bevel lit from the top // left, and grout between the tiles where the fire shows through. float shadeOf = hash21(key + 11.3); float3 clay = mix(float3(0.20, 0.11, 0.08), float3(0.36, 0.22, 0.15), shadeOf); float2 nrm = abs(gv.x) > dot(abs(gv), float2(0.5, SQ3 * 0.5)) ? float2(sign(gv.x), 0.0) : sign(gv) * float2(0.5, SQ3 * 0.5); float bevel = 1.0 - smoothstep(0.0, 0.07, edge); clay *= 1.0 + bevel * 0.9 * dot(nrm, normalize(float2(-0.6, 0.8))); // A light wandering over the floor. Its distance is measured round the // picture's edges, as on a torus, so it repeats with the floor too. float2 at = float2(0.5 + 0.3 * sin(tt * 0.21), 0.5 + 0.3 * cos(tt * 0.17)); float2 dl = uv - at; dl -= floor(dl + 0.5); dl.x *= u.resolution.x / u.resolution.y; float light = 0.45 + 0.9 * exp(-dot(dl, dl) * 5.0); float3 col = clay * light; // The channels: glaze glowing with the heat, spilling light on the clay. // Hottest down the middle of the channel. float w = mix(0.02, 0.09, p.line); float channel = 1.0 - smoothstep(w - aa, w + aa, d); float3 hot = kiln(glowT); col += hot * exp(-max(d - w, 0.0) * 22.0) * 0.3; float core = 1.0 - d / max(w, 1e-3); col = mix(col, hot * (0.8 + 0.9 * core), channel); // Grout: dark, with the fire glowing deep in it. float gap = 0.012 + 0.03 * lift; float grout = 1.0 - smoothstep(gap, gap + aa, edge); float3 deep = kiln(glowT * 0.75) * 0.5; col = mix(col, deep, grout); col = col / (1.0 + col * 0.35); return float4(col, 1.0); } // ---- Sound ----------------------------------------------------------------------------------------------------- // Cracked Clay'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); } // Cracked Clay'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) { // The floor as the 16:9 picture lays it: the same tiles, the same turns. float2 size = floorSize(float2(16.0, 9.0), p.cells); float tt = t * p.speed; float every = turnEvery(p.turns); float s = max(p.speed, 0.01); // Each tile set down after its turn clinks: a ceramic ring, three // partials not in tune with each other, its pitch from the tile's number // on a pentatonic scale, from where it lies on the floor. float2 clinks = float2(0.0); if (every > 0.0 && p.speed > 0.001) { for (float row = 0.0; row < size.y * 2.0; row += 1.0) { for (float col = wrap(row, 2.0); col < size.x * 2.0; col += 2.0) { float2 key = float2(col, row); float offset = hash21(key + 7.7); float k = floor(tt / every - offset); float age = (tt - (k + offset) * every - TURN) / s; if (k < 0.0 || age < 0.0 || age > 1.2) continue; float note = floor(hash21(key + 11.3) * 10.0); float semis = floor(note / 5.0) * 12.0 + float(int(note) % 5) * 2.4; float f = 660.0 * exp2(floor(semis + 0.5) / 12.0); float ring = sin(TAU * f * age) * exp(-age * 9.0) + 0.5 * sin(TAU * f * 2.76 * age) * exp(-age * 16.0) + 0.25 * sin(TAU * f * 5.4 * age) * exp(-age * 28.0); ring *= smoothstep(0.0, 0.002, age); float pan = col / (size.x * 2.0); clinks += ring * float2(1.0 - pan, pan); } } } clinks *= 0.26; // The kiln under the floor: a low drone, a fifth above it, swelling with // the heat, and a roar of slow noise. float swell = 0.6 + 0.4 * sin(tt * 0.6); float drone = sin(TAU * 73.4 * t) + 0.5 * sin(TAU * 110.0 * t + 0.4); drone *= 0.24 * (0.4 + 0.6 * p.heat) * swell; float roar = noise1(t, 140.0, 1u) * 0.6 + noise1(t, 420.0, 2u) * 0.4; roar *= 0.19 * (0.5 + 0.5 * p.heat); // The glaze fizzing in its channels: a little bright hiss, more for // wider and hotter channels. float fizz = noise1(t, 5200.0, 3u) * 0.03 * p.line * (0.3 + p.heat); float2 mixdown = float2(drone + roar) + float2(fizz, -fizz) + clinks; float fadeIn = smoothstep(0.0, 1.5, t); return tanh(mixdown * 1.6) * 0.66 * fadeIn * p.volume; }