Raise the variety bar to 0.1

Deliberately a target rather than a description of where the library sits. Of
the scenes measured since the region block went in, only Droste Feedback at
0.324 is comfortably over; Plasma Bloom is 0.082, Voronoi Shatter and Apollonian
Gasket 0.062, Moiré Grid 0.047. Most of the library fails this, which is the
point — a bar set where the work already is measures nothing.

Third value in two days, and both previous moves were instrument corrections
rather than changes of mind: the motion block was reading zero for the entire
library, and adding the region block made the total a mean over six blocks
instead of five. Both raised every score, so 0.04 had quietly drifted down
relative to the thing it was judging. Documented as something to re-read off a
fresh gallery whenever the descriptor changes, rather than a number to carry
forward.

The marker now sits at the middle of each meter and the grades land where they
should: 0.047, 0.062 and 0.082 all red, 0.140 amber, 0.324 green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dejvino 2026-08-18 18:11:33 +02:00
parent b879abe230
commit c099501ead
4 changed files with 166 additions and 7 deletions

View File

@ -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 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 (`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 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 that sameness leaks between them — the viewer recognises the shot rather than
the song. 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: 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. 2. No glow, bloom, grain, haze, chroma or trail parameters.
3. Element size goes through `stageScale()`. 3. Element size goes through `stageScale()`.
4. Placement goes through `stageNode()` unless the composition IS the scene. 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 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. 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. - **Fixed-geometry scenes** have no known route to variety.
- **The `layout` blindness** for full-frame fields would need a new descriptor - **The `layout` blindness** for full-frame fields would need a new descriptor
block measuring cell statistics rather than where structure sits. 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 - **The bar is a target, not a description.** Most of the library does not clear
against scores that were depressed by the motion bug. It deserves re-deriving. 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.

View File

@ -38,10 +38,19 @@ const THUMB = { width: 256, height: 144 };
* watches two videos recognises the shot rather than the song. It is a property * 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. * 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 * 0.1, and deliberately a target rather than a description of where the library
* measurably sits, to be raised as scenes are brought up to it. * 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. * Six contexts, one per song: everything a scene is handed when it is cast.

View File

@ -69,6 +69,7 @@ import { isometricBlocks } from './shader/isometric-blocks.js';
import { halftoneMisprint } from './shader/halftone-misprint.js'; import { halftoneMisprint } from './shader/halftone-misprint.js';
import { drosteFeedback } from './shader/droste-feedback.js'; import { drosteFeedback } from './shader/droste-feedback.js';
import { analogWow } from './shader/analog-wow.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 * The scene library. Families exist so the arc driver can choose by section
@ -163,6 +164,7 @@ const MODULES = [
constellation, constellation,
soloist, soloist,
swarm, swarm,
mountainFlight,
]; ];
const errors = []; const errors = [];

View File

@ -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;