Forty-nine scenes carried their own `glow`, every one of the sixty-five added its own grain, and a handful had bloom, chroma, haze, smear or trail besides — all of which the post chain already does, with an envelope and a threshold no scene can see. Two uncoordinated effect layers is the smaller half of the problem. The larger half is that an effect knob sampled per song makes a scene look varied across parameter draws while its structure never moves, so the gallery and the variety harness were both being told a scene was original when only its halo had changed. Removed in three shapes. Scene grain goes entirely — the post chain owns it. Statements that ADD a falloff term scaled by the effect are bloom by another name and are deleted. What remains is an intensity on something already drawn, so the uniform is pinned to its default and the knob deleted: the picture survives, the fake variety does not. Reactive entries driving those params went too, since an effect modulated by the audio was the most convincing fake of the lot. That broke the style trait, which is the interesting part. Surface treatment had been leaving the scenes for two commits — sigGrain to post, sigEdge to inkStroke — and removing the last of it left `style` with almost nothing to express: a style+shape signature had two eligible scenes in the whole library. The trait is not obsolete, it has moved, so inkStroke and inkMask now read u_sigLine and u_sigSoft alongside the ink's own weight and edge. A scene drawing in the song's hand honours the track's line weight by construction, and the runtime probe confirms it rather than taking it on trust. This reverses a call made two commits ago for a reason that only became true now. One check needed re-aiming rather than fixing. Two independently built shows are two WebGL contexts, and engine/hash.js says at the top that bit-exactness is a same-context guarantee; the check had been demanding it anyway and getting away with it because the scenes it happened to cast were bit-stable. A casting change put smooth-gradient scenes in frame and the last bit moved. Measured before touching the check: three levels out of 255 across 2.6% of pixels, invisible. The cross-context comparison is now a distance with a ceiling of four, and bit-exactness within one show is still demanded by the check below it. 89/89 checks, 11/11 tests, all static gates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
127 lines
5.0 KiB
JavaScript
127 lines
5.0 KiB
JavaScript
// Structural family: an overhead truss, seen from underneath.
|
|
//
|
|
// Distinct from Pylon Grid (columns standing on the ground), Gate Corridor
|
|
// (frames receding head-on) and Neon City (a skyline at eye level): this is the
|
|
// only scene in the library whose subject is ABOVE the camera. Two parallel
|
|
// chords run away toward the horizon with diagonal bracing zigzagging between
|
|
// them, and the whole thing is looked up at, so the perspective converges
|
|
// downward rather than inward.
|
|
//
|
|
// Joint plates are stamped in the track's signature form.
|
|
//
|
|
// Scaffolded by tools/new-scene.js. See HOWTO-visualizers.md.
|
|
|
|
export const girderLattice = {
|
|
name: 'Girder Lattice',
|
|
family: 'structural',
|
|
kind: 'fragment',
|
|
// Crisp line work: the track's surface grain would only fur the edges.
|
|
texture: 0,
|
|
consumes: ['cast', 'ink'],
|
|
traits: ['shape', 'camera', 'space', 'style'],
|
|
|
|
params: {
|
|
bays: { type: 'int', range: [3, 16], default: 8, uniform: 'u_bays', bias: 'density' },
|
|
beam: { type: 'float', range: [0.01, 0.12],default: 0.04, uniform: 'u_beam' },
|
|
depth: { type: 'float', range: [0.3, 2.2], default: 1.0, uniform: 'u_depth' },
|
|
brace: { type: 'float', range: [0, 1], default: 0.7, uniform: 'u_brace', bias: 'density' },
|
|
travel: { type: 'float', range: [0.0, 0.6], default: 0.12, uniform: 'u_travel', bias: 'motion', rate: true },
|
|
plate: { type: 'float', range: [0, 0.12], default: 0.05, uniform: 'u_plate' },
|
|
lamp: { type: 'float', range: [0, 1.4], default: 0.45, uniform: 'u_lamp', bias: 'energy' },
|
|
palette: { type: 'palette', count: 5 },
|
|
},
|
|
|
|
reactive: {
|
|
lamp: { feature: 'beat', amount: 0.3, response: 'spike' },
|
|
brace: { feature: 'bandLow', amount: 0.15, response: 'smooth' },
|
|
},
|
|
|
|
shader: `
|
|
// Distance to a line segment. The truss is nothing but segments.
|
|
float segment(vec2 p, vec2 a, vec2 b) {
|
|
vec2 pa = p - a, ba = b - a;
|
|
float h = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-6), 0.0, 1.0);
|
|
return length(pa - ba * h);
|
|
}
|
|
|
|
vec4 scene(vec2 uv, vec2 p) {
|
|
float t = u_time * u_travel + u_seed;
|
|
p = sigCamera(p);
|
|
|
|
// Looking up: the vanishing point sits BELOW the frame's horizon, and the
|
|
// structure spans above it. Everything converges toward that point.
|
|
float vanishY = sigHorizonY() * 0.5 - 0.35;
|
|
float above = p.y - vanishY;
|
|
|
|
// Sky between the members.
|
|
vec3 col = mix(pal(0) * 0.1, pal(1) * 0.06, sat(above * 0.6));
|
|
|
|
if (above <= 0.001) {
|
|
col = sigAir(col, p, 1.0);
|
|
return vec4(inkValue(col), 1.0);
|
|
}
|
|
|
|
float best = 1e3;
|
|
float nearest = 0.0;
|
|
float plateHit = 1e3;
|
|
|
|
// Bays march toward the camera on a wrapping saw, so the truss travels
|
|
// overhead rather than sitting still.
|
|
for (int i = 0; i < 16; i++) {
|
|
if (i >= u_bays) break;
|
|
float fi = float(i);
|
|
float phase = fract(fi / float(u_bays) + t);
|
|
|
|
// Perspective: nearer bays are further up the frame and further apart.
|
|
float y0 = vanishY + exp(phase * 2.4) * 0.12 * u_depth;
|
|
float y1 = vanishY + exp((phase + 1.0 / float(u_bays)) * 2.4) * 0.12 * u_depth;
|
|
float halfW0 = (y0 - vanishY) * 1.5;
|
|
float halfW1 = (y1 - vanishY) * 1.5;
|
|
|
|
// The two chords, running away from the camera.
|
|
best = min(best, segment(p, vec2(-halfW0, y0), vec2(-halfW1, y1)));
|
|
best = min(best, segment(p, vec2(halfW0, y0), vec2(halfW1, y1)));
|
|
|
|
// Cross member at this joint.
|
|
best = min(best, segment(p, vec2(-halfW0, y0), vec2(halfW0, y0)));
|
|
|
|
// Diagonal bracing, alternating direction bay to bay: the zigzag is
|
|
// what makes it a truss rather than a ladder.
|
|
if (u_brace > 0.05) {
|
|
float flip = mod(fi, 2.0) < 0.5 ? 1.0 : -1.0;
|
|
float d = segment(p, vec2(-halfW0 * flip, y0), vec2(halfW1 * flip, y1));
|
|
best = min(best, d + (1.0 - u_brace) * 0.05);
|
|
}
|
|
|
|
if (u_plate > 0.004) {
|
|
float pd = castMain((p - vec2(halfW0, y0)) / u_plate) * u_plate;
|
|
plateHit = min(plateHit, pd);
|
|
pd = castMain((p - vec2(-halfW0, y0)) / u_plate) * u_plate;
|
|
plateHit = min(plateHit, pd);
|
|
}
|
|
|
|
if (phase > 0.75) nearest = max(nearest, phase);
|
|
}
|
|
|
|
// Members are drawn thicker nearer the camera, which is the only depth cue
|
|
// a wireframe has.
|
|
float weight = u_beam * (0.4 + above * 0.9) * (0.5 + u_sigLine);
|
|
float steel = smoothstep(weight, weight * 0.3, best);
|
|
|
|
col = mix(col, pal(2) * (0.3 + above * 0.5), steel);
|
|
col += pal(3) * inkStroke(best - weight) * 0.4;
|
|
|
|
// Joint plates.
|
|
col = mix(col, pal(4) * 0.7, smoothstep(0.006, -0.006, plateHit));
|
|
|
|
// A lamp slung under the nearest bay, pulsing on the beat. Local.
|
|
col += pal(4) * exp(-length(p - vec2(0.0, vanishY + nearest * 1.4)) * 14.0) * u_lamp;
|
|
|
|
col = sigAir(col, p, 1.0 - smoothstep(0.0, 1.4, above));
|
|
return vec4(col, 1.0);
|
|
}
|
|
`,
|
|
};
|
|
|
|
export default girderLattice;
|