diff --git a/flow-state/HOWTO-variety.md b/flow-state/HOWTO-variety.md index 24e2238..1ba96d3 100644 --- a/flow-state/HOWTO-variety.md +++ b/flow-state/HOWTO-variety.md @@ -28,12 +28,15 @@ checks.html?scene=X the per-scene gate, including `consumes:` lines ``` The gallery score is the mean structural distance between a scene's own six -frames, on the same descriptor the variety harness uses. **Minimum bar: 0.04** +frames, on the same descriptor the variety harness uses. **Minimum bar: 0.1** (`MIN_VARIETY` in `checks/gallery.js`), drawn as a red line. Below it a scene is the same picture wherever it appears, and because it is cast into many songs that sameness leaks between them — the viewer recognises the shot rather than the song. +Most of the library does not clear it. That is deliberate: a bar set where the +work already is measures nothing. + Some reference points, measured: ``` @@ -247,7 +250,7 @@ Two more traps, both of which have bitten: 2. No glow, bloom, grain, haze, chroma or trail parameters. 3. Element size goes through `stageScale()`. 4. Placement goes through `stageNode()` unless the composition IS the scene. -5. Gallery score above 0.04, checked before and after. +5. Gallery score above 0.1, checked before and after. 6. The rest of the per-scene battery still passes — especially `distinct`. The more scenes share a cast, the easier it is to become one of your neighbours. @@ -261,5 +264,8 @@ Two more traps, both of which have bitten: - **Fixed-geometry scenes** have no known route to variety. - **The `layout` blindness** for full-frame fields would need a new descriptor block measuring cell statistics rather than where structure sits. -- **The 0.04 bar** is a starting point, set where the flat cluster sat — and set - against scores that were depressed by the motion bug. It deserves re-deriving. +- **The bar is a target, not a description.** Most of the library does not clear + 0.1 — that is the point of it. It has moved from 0.04 to 0.05 to 0.1 + as the descriptor gained a block and lost a bug, both of which raised every + score. Expect to move it again whenever the descriptor changes, and re-read it + off a fresh gallery rather than carrying the old number forward. diff --git a/flow-state/src/checks/gallery.js b/flow-state/src/checks/gallery.js index 9d6e08e..2ea2844 100644 --- a/flow-state/src/checks/gallery.js +++ b/flow-state/src/checks/gallery.js @@ -38,10 +38,19 @@ const THUMB = { width: 256, height: 144 }; * watches two videos recognises the shot rather than the song. It is a property * of the scene, not of the generator, so it is the scene that has to be lifted. * - * 0.04 is a starting bar rather than a derived one. Set where the flat cluster - * measurably sits, to be raised as scenes are brought up to it. + * 0.1, and deliberately a target rather than a description of where the library + * currently sits. Most of it does not clear this — of the scenes measured after + * the region block went in, only Droste Feedback at 0.324 is comfortably over, + * with Plasma Bloom at 0.082 and Voronoi Shatter at 0.062 still short. A bar set + * where the work already is measures nothing. + * + * It has moved twice for instrument reasons rather than taste — the motion block + * was reading zero for the whole library, and adding the region block made the + * total a mean over six rather than five — and both raised every score. Expect + * to re-read it off a fresh gallery whenever the descriptor changes, rather than + * carrying the old number forward. */ -export const MIN_VARIETY = 0.04; +export const MIN_VARIETY = 0.1; /** * Six contexts, one per song: everything a scene is handed when it is cast. diff --git a/flow-state/src/scenes/registry.js b/flow-state/src/scenes/registry.js index 36c1fd0..23a11b8 100644 --- a/flow-state/src/scenes/registry.js +++ b/flow-state/src/scenes/registry.js @@ -69,6 +69,7 @@ import { isometricBlocks } from './shader/isometric-blocks.js'; import { halftoneMisprint } from './shader/halftone-misprint.js'; import { drosteFeedback } from './shader/droste-feedback.js'; import { analogWow } from './shader/analog-wow.js'; +import { mountainFlight } from './shader/mountain-flight.js'; /** * The scene library. Families exist so the arc driver can choose by section @@ -163,6 +164,7 @@ const MODULES = [ constellation, soloist, swarm, + mountainFlight, ]; const errors = []; diff --git a/flow-state/src/scenes/shader/mountain-flight.js b/flow-state/src/scenes/shader/mountain-flight.js new file mode 100644 index 0000000..d4f0d51 --- /dev/null +++ b/flow-state/src/scenes/shader/mountain-flight.js @@ -0,0 +1,142 @@ +// Structural family: a flight through mountains. A real perspective camera +// travelling forward over a raymarched heightfield — peaks rise, pass to either +// side and occlude what is behind them, with a snowline near the top and the +// ridge silhouettes drawn in the song's hand. +// +// Ridge Terrain is the neighbour, and it is the flat version of this: parallax +// bands of 1D noise that slide sideways and can never hide each other, because +// there is no depth for them to hide in. Here the image is a 3D field sampled +// along the ray, so the motion is INTO the frame rather than across it, valleys +// open and close as the camera passes through them, and a near peak eats a far +// one. The massifs themselves are the song's protagonist, extruded: a hollow +// cast makes calderas, a notched one makes star-shaped ridges. + +export const mountainFlight = { + name: 'Mountain Flight', + family: 'structural', + kind: 'fragment', + // Personality: see look/Personality.js. Identity: see look/Identity.js. + consumes: ['cast', 'ink', 'staging'], + traits: ['camera', 'space', 'style'], + + params: { + relief: { type: 'float', range: [0.15, 1.4], default: 0.55, uniform: 'u_relief', bias: 'energy' }, + rugged: { type: 'float', range: [0.15, 1.1], default: 0.45, uniform: 'u_rug', bias: 'density' }, + ridges: { type: 'int', range: [2, 5], default: 4, uniform: 'u_oct', bias: 'density' }, + massif: { type: 'float', range: [0.1, 0.9], default: 0.5, uniform: 'u_massif' }, + span: { type: 'float', range: [4, 16], default: 8.0, uniform: 'u_span', bias: 'density' }, + altitude: { type: 'float', range: [0.15, 2.2], default: 0.7, uniform: 'u_alt', slowAxis: true }, + snowline: { type: 'float', range: [0.15, 1.1], default: 0.55, uniform: 'u_snow', bias: 'energy' }, + speed: { type: 'float', range: [0.1, 2.5], default: 0.8, uniform: 'u_speed', bias: 'motion', rate: true }, + palette: { type: 'palette', count: 6 }, + }, + + reactive: { + relief: { feature: 'bandLow', amount: 0.22, response: 'smooth' }, + snowline: { feature: 'bandHigh', amount: 0.2, response: 'smooth' }, + altitude: { feature: 'bandSub', amount: 0.15, response: 'smooth' }, + }, + + shader: ` +// The massif standing in the world cell around w: the song's protagonist, used +// as a PLAN rather than a picture — its footprint is extruded into a mountain. +// Kept strictly inside its own cell so one sample of one cell is the whole +// answer, which is what makes it affordable inside the march. +float massifH(vec2 w) { + vec2 cell = floor(w / u_span); + vec2 local = w - (cell + 0.5) * u_span; + + // Which node of the song's lattice this cell got, and how big it stands. + vec3 node = stageNode(floor(hash12(cell) * 7.0), 8.0); + vec2 at = clamp(node.xy, -1.0, 1.0) * u_span * 0.18; + float s = u_span * 0.2 * clamp(u_massif * node.z, 0.08, 1.1); + + float d = castMain((local - at) / s) * s; + // Flanks fall away from the footprint's edge; the summit is its middle. + float m = sat(-d / max(s * 0.9, 1e-3)); + return pow(m, 0.7) * u_relief * 1.7; +} + +float terrainH(vec2 w) { + // The song's element size is the size of the whole landscape's vocabulary: + // a few enormous massifs, or a crowd of small ones. + vec2 q = w * u_rug / max(stageScale(), 0.25); + + float sum = 0.0, norm = 0.0, amp = 1.0; + for (int i = 0; i < 5; i++) { + if (i >= u_oct) break; + // Ridged, not billowy: mountains have crests. + float n = 1.0 - abs(vnoise(q) * 2.0 - 1.0); + sum += n * n * amp; + norm += amp; + q = rot(0.63) * q * 2.03; + amp *= 0.45; + } + return (sum / max(norm, 1e-3)) * u_relief + massifH(w); +} + +vec4 scene(vec2 uv, vec2 p) { + float t = u_time * u_speed + u_seed; + p = sigCamera(p); + + // The camera rides a fixed height over whatever it is flying across, so a + // tall massif lifts the flight path instead of swallowing it. + vec3 ro = vec3(u_seed * 3.7, 0.0, t * 4.0); + ro.y = terrainH(ro.xz) + u_alt * (1.0 + stageScale() * 0.3); + + // The track's horizon decides how far down the camera is looking. + vec3 rd = normalize(vec3(p.x, p.y - sigHorizonY() * 0.5, 1.6)); + + float dist = 0.4; + float hit = 0.0; + float gap = 0.0; + float grazed = 1e3; // closest the ray came, per unit distance + + // A march ends on its hit or its far plane, not on a param. + // lint: fixed-cost + for (int i = 0; i < 72; i++) { + vec3 at = ro + rd * dist; + gap = at.y - terrainH(at.xz); + grazed = min(grazed, gap / max(dist, 1.0)); + if (gap < 0.002 * dist) { hit = 1.0; break; } + // Cone stepping: safe near the surface, cheap out at the far end. + dist += max(gap * 0.5, 0.03 + dist * 0.02); + if (dist > 46.0) break; + } + + float fog = sat(dist / 46.0); + vec3 col; + + if (hit > 0.5) { + vec3 at = ro + rd * dist; + float e = 0.02 + dist * 0.006; + vec3 nrm = normalize(vec3( + terrainH(at.xz - vec2(e, 0.0)) - terrainH(at.xz + vec2(e, 0.0)), + 2.0 * e, + terrainH(at.xz - vec2(0.0, e)) - terrainH(at.xz + vec2(0.0, e)))); + + vec3 sun = normalize(vec3(0.55, 0.42, -0.5)); + float diff = sat(dot(nrm, sun)); + + float elev = sat(at.y / max(u_relief * 2.2, 0.2)); + // Snow lies high, and only where the slope will hold it. + float snow = smoothstep(u_snow, u_snow + 0.16, elev * (0.45 + 0.75 * nrm.y)); + + vec3 rock = mix(pal(2), pal(3), sat(elev * 1.3)); + col = mix(rock, pal(5), snow); + col *= 0.22 + 0.9 * diff; + col += pal(1) * 0.12 * sat(nrm.y); // sky bounce into the flats + } else { + col = mix(pal(1) * 0.45, pal(0) * 0.22, sat(rd.y * 2.2 + 0.15)); + fog = 0.85; + // The ridgeline the sky is cut against, drawn at the song's weight. + col += pal(4) * inkStroke(grazed * 0.6) * 0.5; + } + + col = sigAir(col, p, fog); + return vec4(inkValue(col), 1.0); +} +`, +}; + +export default mountainFlight;