// Structural family: a field of standing pylons receding to the track's // horizon. The columns converge on a vanishing point, giving real perspective // depth, and each column carries the song's own object on top, so a hexagonal // track grows hexagonal pylons. The beat walks a pulse of light down the rows // rather than flashing the whole frame. // // Nothing here is the scene's own invention any more. The crown is the song's // protagonist and the LEG is a stack of its chorus: a column of bodies holding // the subject up, rather than the thin fixed-width strut this drew for years. // That strut was the same object in every video — a scene that has migrated its // subject and kept its structure is still telling you what IT is, in the half of // the frame that is structure, and next to a marched crown it read as scaffolding // left up around a finished building. // // The crowns are SOLID. This scene already had the one thing the 3D cast needs // and most scenes lack — a real depth axis, with near rows large and far rows // small — so a flat crown was the only part of it drawn without perspective: a // sticker facing the viewer on top of a receding structure. Marching them puts // the object in the same space as the field it stands in, and because each // pylon turns its own crown at its own angle, one field shows the song's form // from a dozen sides at once. See castSolid. // // WHAT THE NUMBERS SAY, since two of them disagree and both are right. // // Crop to the bottom third of the frame — the near rows, where the columns are // tall and the crowns are not — and measure how far apart six identities are: // 13.4/255 with the old strut, 22.2/255 with the chorus stacked. That is the // complaint answered. The strut was a fixed width and a fixed shape, so the only // thing separating one video's structure from another's was the palette. // // The whole-frame variety score barely moves for it: 0.1322 to 0.1348 over // 8 songs x 8 seeds. That is honest rather than disappointing — the crowns // already dominate a frame-level descriptor, and a measurement averaged over the // whole image cannot see a change confined to the lower third. It is the reason // the crop above exists. // // The frame score is also still under the flat version's 0.141, and the cause is // known: a body always fills its own silhouette, where a stamped cast form paints // anywhere between an outline and a disc depending on whether the identity is // hollow, and that swing was worth three hundredths of LAYOUT variety. Two // candidate fixes were tried and both measured flat — keeping the far rows // stamped (0.132), and running the lit surface through inkPattern (0.132 here, // and 0.0009 on Effigy). It is the solid itself, and the trade is deliberate: // the score does not measure a crown that ignores the perspective of the field // it stands in, or a leg that is the same object in every video. export const pylonGrid = { name: 'Pylon Grid', family: 'structural', kind: 'fragment', // Personality: see look/Personality.js. // Takes the track's surface grain, but lightly — this is drawn, not filmed. texture: 0.4, consumes: ['form', 'ink', 'staging'], traits: ['shape', 'space', 'style'], params: { columns: { type: 'float', range: [3, 16], default: 8, uniform: 'u_columns', bias: 'density' }, rows: { type: 'float', range: [2, 10], default: 6, uniform: 'u_rows', bias: 'density' }, spread: { type: 'float', range: [0.6, 1.6], default: 1.1, uniform: 'u_spread' }, height: { type: 'float', range: [0.3, 1.4], default: 0.85, uniform: 'u_height', bias: 'energy' }, pulse: { type: 'float', range: [0, 1.2], default: 0.5, uniform: 'u_pulse', bias: 'energy' }, speed: { type: 'float', range: [0.05, 0.8], default: 0.3, uniform: 'u_speed', bias: 'motion', rate: true }, palette: { type: 'palette', count: 4 }, }, reactive: { pulse: { feature: 'beat', amount: 0.4, response: 'smooth' }, rows: { feature: 'bandLow', amount: 0.25 }, }, shader: ` vec4 scene(vec2 uv, vec2 p) { float t = u_time * u_speed + u_seed; float hy = sigHorizonY(); vec3 col = pal(0) * 0.05; // Whether a crown already owns this pixel. Rows are walked near to far, so // first-wins is the correct occlusion here rather than merely the cheap one // — and it bounds a hundred and sixty instances to about one march. float painted = 0.0; for (int r = 0; r < 10; r++) { if (float(r) >= u_rows) break; float fr = float(r); // Perspective: the last row sits on the horizon, the first at the bottom // of the frame, everything between scaling by its depth fraction. float f = fr / max(u_rows - 1.0, 1.0); // 0 near .. 1 far float y0 = mix(-1.08, hy + 0.06, f); // row baseline float scale = mix(1.0, 0.12, f); // perspective shrink float depthShade = mix(1.0, 0.35, f); // far rows dim for (int c = 0; c < 16; c++) { if (float(c) >= u_columns) break; float fc = float(c); float cx = (fc / max(u_columns - 1.0, 1.0) - 0.5) * 2.0 * u_spread * scale; float h = u_height * scale; float yTop = y0 + h; // The beat pulse walks a glow down the depth axis, row by row, so the // light travels rather than strobing the whole frame. float walk = sat(1.0 - abs(fr - mod(u_beatPhase * 6.0 + t * 0.4, u_rows))); float inten = (0.35 + 0.65 * walk) * depthShade * (0.7 + 0.3 * u_beat); // The crown is resolved BEFORE the leg, because the two overlap // where the object sits on the column and the crown has to win it — // resolved the other way round, every subject is cut off at the // waist by the thing holding it up. // // The song's object, standing on the pylon. vec2 crown = vec2(cx, yTop); float size = (0.06 + 0.2 * scale) * (1.0 + u_beat * 0.4); vec2 local = (p - crown) / max(size, 1e-3); if (dot(local, local) < 1.6 && painted < 0.5) { // Each crown at its own angle, so the field is one form seen // from many sides rather than one silhouette repeated. The turn // is slow and per-pylon; the row term keeps a receding line of // them from presenting the same profile all the way back. mat3 turn = castTurn(t * 0.6 + fc * 0.9 + fr * 0.4, 0.2 + sin(t + fr) * 0.2); vec3 n; float hit = castSolid(local, turn, n); if (hit > 0.0) { painted = 1.0; col = mix(col, castLit(n, vec3(0.0, 0.0, 1.0)) * inten * (0.6 + u_pulse), 0.92); } // Its outline in the track's line weight, which is what carries // the far rows: at twelve percent scale a crown is a few pixels // and the lit faces are gone before the edge is. float d = castSDF3(turn * vec3(local, 0.0)) * size; col += pal(3) * inkStroke(d) * inten * (0.5 + u_pulse); } col += pal(int(mod(fr, 4.0))) * exp(-dot(p - crown, p - crown) * 60.0) * u_pulse * depthShade; // THE LEG: the chorus, stacked. How many members make up a column // is the song's element size deciding it — a track built of a few // huge forms stands on three of them, a track of many small ones on // a ladder of nine — and how WIDE the column is comes from the // chorus's own proportions, which is why no two songs get the same // pylon. See Identity.js: this is what the chorus solid is for. float segs = clamp(floor(2.0 + 3.6 / max(stageScale(), 0.3)), 2.0, 9.0); float member = max(h / (segs * 2.0), 1e-3); // The bounding test is the column's own box, so a pixel outside it // costs two comparisons rather than a march. With a hundred and // sixty pylons on screen that test is most of the scene's budget. if (abs(p.x - cx) < member * 1.3 && p.y > y0 - member * 0.3 && p.y < yTop + member * 0.3 && painted < 0.5) { float rung = floor((p.y - y0) / (member * 2.0)); float centreY = y0 + (rung + 0.5) * member * 2.0; vec2 legLocal = vec2(p.x - cx, p.y - centreY) / member; // Every member at its own angle, so a column is a stack of one // form seen many ways rather than one form printed nine times. mat3 legTurn = castTurn(t * 0.35 + rung * 1.1 + fc * 0.5, rung * 0.5); vec3 ln; float legHit = castChorusSolid(legLocal, legTurn, ln); if (legHit > 0.0) { painted = 1.0; col = mix(col, castLit(ln, vec3(0.0, 0.0, 1.0)) * mix(inten, 1.0, 0.25) * pal(int(mod(fc, 4.0))) * 1.6, 0.9); } col += pal(int(mod(fc, 4.0))) * inkStroke(castChorus3(legTurn * vec3(legLocal, 0.0)) * member) * inten * 0.8; } } } // Ground haze and the shared air between here and the horizon. col = sigAir(col, p, smoothstep(0.0, 1.6, abs(p.y - hy))); return vec4(inkValue(col), 1.0); } `, };