Asked for, built, and honest about the result: the focal point costs this scene
a quarter of its variety and I could not make it pay for itself.
uniform scatter, no focus 0.0490
focus, trading scatter for it 0.0374
focus, added on top of it 0.0365
The first version spent the scatter budget near the focus and calmed the rest,
which put most of the field back on the rigid lattice and took the orientation
variety with it, 0.164 down to 0.105. That diagnosis looked right, so the second
version made the focus additive — every cell keeps its full scatter and the ones
near the point get more. It changed nothing: 0.0365, orientation still 0.104.
So the diagnosis was wrong and I do not have the real one. The remaining suspect
is the height modulation, which lifts blocks toward the focus and may be
flattening the skyline's variety between songs by dominating it — but that is a
guess, and two guesses have already been wrong here.
The feature is committed rather than reverted because the metric does not
measure what was asked for. A scatter point is a compositional idea, and the
gallery scores how much a scene changes BETWEEN songs, which is a different
question — Apollonian Gasket makes the same point from the other direction. This
may well look better and score worse. It needs eyes on it before the number
decides.
`gather` is a parameter and seed-driven like any other, so the amount varies per
song as asked; the focal points themselves come from the identity.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
165 lines
8.7 KiB
JavaScript
165 lines
8.7 KiB
JavaScript
// Geometric family: a field of extruded blocks seen isometrically, rising and
|
|
// falling on the low end. A solid, lit surface with a light direction — three
|
|
// faces per block, and the whole grid reads as a landscape rather than a chart.
|
|
//
|
|
// Floating Geometry is bodies adrift in space with nothing under them. This is
|
|
// the opposite proposition: everything is on one grid, everything touches the
|
|
// ground, and the only freedom any block has is its height. The plan of each
|
|
// block is the track's signature form, so a hexagonal video gets a honeycomb of
|
|
// columns rather than a chessboard of them.
|
|
|
|
export const isometricBlocks = {
|
|
name: 'Isometric Blocks',
|
|
family: 'geometric',
|
|
kind: 'fragment',
|
|
texture: 0.6,
|
|
consumes: ['cast', 'ink'],
|
|
traits: ['shape', 'camera', 'style'],
|
|
|
|
params: {
|
|
grid: { type: 'float', range: [2, 10], default: 4.5, uniform: 'u_grid', bias: 'density', slowAxis: true },
|
|
height: { type: 'float', range: [0.1, 0.7], default: 0.35, uniform: 'u_height', bias: 'energy' },
|
|
// Displacement: how far each block wanders from its slot on the grid.
|
|
//
|
|
// Every block sat exactly on the lattice, so the plan of the field was
|
|
// the same plan in every song and only the heights moved — which is
|
|
// what the gallery was reporting as a layout distance of 0.005, near
|
|
// enough to nothing. Displacing them per cell puts the SONG in the
|
|
// arrangement rather than only in the elevation.
|
|
scatter: { type: 'float', range: [0, 0.85], default: 0.3, uniform: 'u_scatter', bias: 'density' },
|
|
// Extra disturbance AT the song's focal points, on top of the scatter
|
|
// everywhere — not instead of it.
|
|
//
|
|
// It was a trade at first, spending the scatter budget near the focus
|
|
// and calming the rest, and that measured worse than no focus at all:
|
|
// 0.049 down to 0.037. Most of the field went back onto the rigid
|
|
// lattice and took the orientation variety with it, 0.164 to 0.105. The
|
|
// focal point has to be something the field gains, not something the
|
|
// rest of it pays for.
|
|
gather: { type: 'float', range: [0, 1.6], default: 0.8, uniform: 'u_gather' },
|
|
plan: { type: 'float', range: [0.2, 0.5], default: 0.4, uniform: 'u_plan' },
|
|
tilt: { type: 'float', range: [0.35, 0.75], default: 0.55, uniform: 'u_isoTilt' },
|
|
light: { type: 'float', range: [0, 1.4], default: 0.65, uniform: 'u_light' },
|
|
wave: { type: 'float', range: [0, 1], default: 0.5, uniform: 'u_wave' },
|
|
roll: { type: 'float', range: [0.01, 0.08], default: 0.035, uniform: 'u_roll', bias: 'motion', rate: true },
|
|
palette: { type: 'palette', count: 5 },
|
|
},
|
|
|
|
// The low end deliberately does NOT drive the block heights. Measured at
|
|
// aggressive settings that read as 4 flashes a second: every tower in the
|
|
// frame grows and shrinks together, which is a whole-frame luminance cycle
|
|
// on the kick and exactly what the flash ceiling exists to stop. The beat
|
|
// lands on a quarter of the tops instead — same reading, local swing.
|
|
reactive: {
|
|
light: { feature: 'loudness', amount: 0.25, response: 'smooth' },
|
|
},
|
|
|
|
shader: `
|
|
// How tall the block at this cell stands, in screen units. A travelling wave
|
|
// plus a fixed per-cell character, so the field has a rhythm running through it
|
|
// and a skyline that is still recognisable between beats.
|
|
float columnHeight(vec2 cell, float t) {
|
|
float own = hash12(cell + u_seed);
|
|
float wave = sin(dot(cell, vec2(0.7, 0.5)) - t * 1.1) * 0.5 + 0.5;
|
|
// Squared, so most of the field lies flat and the tall ones stand out as
|
|
// individual towers. A linear height makes every column tall enough to
|
|
// occlude its neighbours, and the field turns into vertical stripes.
|
|
return u_height * pow(mix(own, wave, u_wave), 2.4);
|
|
}
|
|
|
|
vec4 scene(vec2 uv, vec2 p) {
|
|
float t = u_time * u_roll + u_seed;
|
|
p = sigCamera(p);
|
|
|
|
float tilt = max(u_isoTilt, 0.1);
|
|
float cw = 1.0 / u_grid; // one cell across, on screen
|
|
|
|
vec3 col = mix(pal(0) * 0.06, pal(1) * 0.14, sat(p.y * 0.4 + 0.5));
|
|
|
|
// A block standing on row r covers screen rows ABOVE r's ground line, so a
|
|
// pixel can only belong to a row at or behind it. Walking a fixed number of
|
|
// rows from the back forward and overwriting as we go is the painter's
|
|
// algorithm, and it is the whole of the depth sorting an isometric field
|
|
// needs — no sorting, no z buffer, just the order of the loop.
|
|
float colX = floor(p.x / cw);
|
|
float fx = (p.x / cw - colX - 0.5) * cw; // screen offset within the column
|
|
|
|
// Alternate columns are staggered half a row, which is what interlocks the
|
|
// field into a lattice instead of leaving it a set of independent stacks.
|
|
float stagger = mod(colX, 2.0) * 0.5;
|
|
float rowHere = floor(p.y / (tilt * cw) - stagger);
|
|
|
|
// Rows FURTHER AWAY stand higher up the frame, so a pixel can only belong
|
|
// to a block whose ground line is at or below it — row <= rowHere. Counting
|
|
// down from there draws the far ones first and lets the near ones paint
|
|
// over them.
|
|
// lint: fixed-cost — eight rows of depth, back to front
|
|
for (int j = 7; j >= 0; j--) {
|
|
float row = rowHere - float(7 - j);
|
|
vec2 cell = vec2(colX, row);
|
|
|
|
// Where this block would stand if nothing disturbed it, so the focus can
|
|
// be asked about the cell rather than about the pixel.
|
|
float restY = (row + stagger) * tilt * cw;
|
|
float focus = focusField(vec2((colX + 0.5) * cw - 0.5, restY));
|
|
|
|
// Each cell steps off its slot by a fixed amount of its own, and how far
|
|
// depends on how close it is to what the song is looking at. Spread
|
|
// evenly this is noise on a grid; concentrated, the lattice stands
|
|
// except where it is disturbed, and the field acquires a subject.
|
|
//
|
|
// Bounded under half a cell: past that blocks cross each other and the
|
|
// isometric read — which is the whole point of the scene — breaks down.
|
|
float amount = u_scatter * (1.0 + focus * u_gather * 2.0);
|
|
vec2 offset = (hash22(cell + u_seed + 3.7) - 0.5) * amount * cw * 0.9;
|
|
|
|
float groundY = restY + offset.y; // where this cell meets the ground
|
|
// Blocks rise toward the focus as well as scattering around it, so the
|
|
// disturbance is something you can see in the skyline rather than only
|
|
// in the plan — and rising changes WHERE the frame's energy is, which
|
|
// scattering alone did not.
|
|
float h = columnHeight(cell, t)
|
|
* (1.0 + offset.x * 2.0)
|
|
* (1.0 + focus * u_gather * u_focusPull * 1.3);
|
|
float dy = p.y - groundY; // height up the block face
|
|
|
|
// Side wall: the plan swept up from the ground line to the top face.
|
|
float inX = smoothstep(u_plan * cw, u_plan * cw * 0.85, abs(fx - offset.x));
|
|
float wall = inX * step(0.0, dy) * step(dy, h);
|
|
|
|
float lit = 0.5 + 0.5 * sat(fx / (u_plan * cw) * 0.8 + 0.5);
|
|
// The wall darkens toward the ground, which is the only shading cue
|
|
// that tells a tall block from a short one at a glance.
|
|
// Neither face takes the loudness. Anything that multiplies a face
|
|
// colour multiplies most of the frame, and a field of blocks brightening
|
|
// together on the kick measured at 5 flashes a second. The light level
|
|
// is expressed on the EDGES, which are a few percent of the image.
|
|
vec3 side = mix(pal(0) * 0.5, pal(2) * 0.8, lit) * 0.75;
|
|
side *= 0.45 + 0.55 * sat(dy / max(h, 1e-3));
|
|
col = mix(col, side, wall);
|
|
|
|
// Top face: the track's signature form, lying flat, lifted by h and
|
|
// squashed by the tilt — so every block in the field is the same
|
|
// silhouette as every other subject in the video.
|
|
vec2 q = vec2(fx, (p.y - groundY - h) / tilt);
|
|
float dTop = castMain(q / (u_plan * cw)) * (u_plan * cw);
|
|
float topMask = smoothstep(0.004, -0.004, dTop);
|
|
|
|
vec3 top = mix(pal(2), pal(3), sat(h / max(u_height, 1e-3))) * 0.8;
|
|
col = mix(col, top, topMask);
|
|
// A quarter of the blocks answer the beat, chosen by cell rather than
|
|
// by anything global, so the field has a pulse without the frame having
|
|
// a flash.
|
|
float picked = step(0.75, hash12(cell * 3.17 + 5.0));
|
|
col += pal(3) * topMask * picked * u_beat * 0.45;
|
|
col += pal(4) * inkStroke(dTop) * (0.2 + u_light * 1.2) * step(0.0, dy);
|
|
}
|
|
|
|
col *= 0.75 + 0.25 * exp(-dot(p, p) * 0.3);
|
|
return vec4(inkValue(col), 1.0);
|
|
}
|
|
`,
|
|
};
|
|
|
|
export default isometricBlocks;
|