diff --git a/flow-state/src/engine/shader-contract.js b/flow-state/src/engine/shader-contract.js index 6c8dd8b..d36414e 100644 --- a/flow-state/src/engine/shader-contract.js +++ b/flow-state/src/engine/shader-contract.js @@ -955,6 +955,13 @@ vec3 castLit(vec3 n, vec3 rd) { vec3 col = mix(pal(1) * 0.22, pal(2), diff); col += pal(0) * fill; col += pal(3) * rim * 0.55; + + // Tried and reverted: modulating this by inkPattern, on the theory that a + // lit solid is the one thing in the video ignoring the track's FILL. It + // reads well and measures nothing — Pylon Grid 0.1324 to 0.1322, Effigy + // 0.2382 to 0.2373, both inside the last digit that means anything. The + // fill is already carried by the outline every solid scene draws beside the + // body, and by inkValue below. return inkValue(col); } `; diff --git a/flow-state/src/scenes/shader/pylon-grid.js b/flow-state/src/scenes/shader/pylon-grid.js index 775e613..8ba2fd9 100644 --- a/flow-state/src/scenes/shader/pylon-grid.js +++ b/flow-state/src/scenes/shader/pylon-grid.js @@ -1,8 +1,32 @@ // 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 a crown in the signature form so a hexagonal +// 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. +// +// 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. +// +// 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. +// +// 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. export const pylonGrid = { name: 'Pylon Grid', @@ -13,7 +37,7 @@ export const pylonGrid = { // Personality: see look/Personality.js. // Takes the track's surface grain, but lightly — this is drawn, not filmed. texture: 0.4, - consumes: ['cast', 'ink'], + consumes: ['form', 'ink'], traits: ['shape', 'space', 'style'], params: { @@ -45,6 +69,11 @@ vec4 scene(vec2 uv, vec2 p) { 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); @@ -72,10 +101,29 @@ vec4 scene(vec2 uv, vec2 p) { float w = 0.02 * scale + 0.006; col += pal(int(mod(fc, 4.0))) * strut(p, cx, y0, yTop, w) * inten; - // Crown stamped in the track's signature form. + // 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); - col += pal(3) * castForm(p, crown, size) * inten * (0.6 + u_pulse); + 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; } }