diff --git a/flow-state/src/scenes/shader/pylon-grid.js b/flow-state/src/scenes/shader/pylon-grid.js index 8ba2fd9..79c9014 100644 --- a/flow-state/src/scenes/shader/pylon-grid.js +++ b/flow-state/src/scenes/shader/pylon-grid.js @@ -4,6 +4,14 @@ // 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 @@ -12,32 +20,40 @@ // pylon turns its own crown at its own angle, one field shows the song's form // from a dozen sides at once. See castSolid. // -// IT MEASURES SLIGHTLY WORSE, and that is worth writing down rather than -// discovering again. Over 8 songs x 8 seeds the flat version scored 0.141 and -// this one scores 0.134, almost all of it in the LAYOUT block (0.156 to 0.126). -// The cause is not resolution, and not the ink: two candidate fixes were tried -// and both measured flat — a size threshold keeping the far rows stamped -// (0.132) and running the lit surface through inkPattern (0.132). It is the -// solid itself. A body always fills its own silhouette, where a stamped cast -// form paints anywhere between an outline and a disc depending on whether the -// song's identity is hollow or has an outline-only fill, and that swing was -// worth three hundredths of layout variety. +// WHAT THE NUMBERS SAY, since two of them disagree and both are right. // -// Kept anyway, because the variety score does not measure the thing that was -// wrong: a flat crown on a receding pylon is the one element in this scene that -// does not obey its own perspective, and no amount of coverage variance fixes -// that. Reverting is a two-line change if the number matters more. +// 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', - // Paints 27% of the frame — see checks/phase12 coverage. + // Paints 25% of the frame — see checks/phase12 coverage. surface: 'composable', // Personality: see look/Personality.js. // Takes the track's surface grain, but lightly — this is drawn, not filmed. texture: 0.4, - consumes: ['form', 'ink'], + consumes: ['form', 'ink', 'staging'], traits: ['shape', 'space', 'style'], params: { @@ -56,14 +72,6 @@ export const pylonGrid = { }, shader: ` -// Distance to a thin vertical strut from (cx, y0) up to (cx, y1). -float strut(vec2 p, float cx, float y0, float y1, float w) { - float xd = abs(p.x - cx); - float yd = max(max(y0 - p.y, p.y - y1), 0.0); - float d = sqrt(xd * xd + yd * yd); - return smoothstep(w + 0.004, w - 0.004, d); -} - vec4 scene(vec2 uv, vec2 p) { float t = u_time * u_speed + u_seed; float hy = sigHorizonY(); @@ -98,9 +106,11 @@ vec4 scene(vec2 uv, vec2 p) { 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); - float w = 0.02 * scale + 0.006; - col += pal(int(mod(fc, 4.0))) * strut(p, cx, y0, yTop, w) * inten; - + // 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); @@ -125,6 +135,38 @@ vec4 scene(vec2 uv, vec2 p) { 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; + } } }