music-video-gen/flow-state/src/scenes/shader/pitch-shatter.js
2026-08-05 20:55:00 +02:00

71 lines
3.3 KiB
JavaScript

// Glitch family: vertical transposition. The frame is cut into vertical bands,
// each of which shifts up or down by a small amount. That alone is a tear; what
// makes it churn is that the offset is re-rolled on the quantised grid and the
// previous frame's transposed image is smeared underneath — so slices translate
// to a new fixed pose each step instead of crawling, and the pitch reads as an
// edit rather than a broken renderer.
export const pitchShatter = {
name: 'Pitch Shatter',
family: 'glitch',
kind: 'fragment',
// Personality: see look/Personality.js.
traits: ['camera', 'style'],
params: {
slices: { type: 'float', range: [4, 40], default: 18, uniform: 'u_slices', bias: 'density' },
amp: { type: 'float', range: [0, 0.35], default: 0.1, uniform: 'u_amp', bias: 'energy' },
quantize: { type: 'float', range: [1, 8], default: 4, uniform: 'u_quantize' },
bleed: { type: 'float', range: [0, 0.9], default: 0.4, uniform: 'u_bleed' },
glow: { type: 'float', range: [0, 1], default: 0.3, uniform: 'u_glow', bias: 'energy' },
speed: { type: 'float', range: [0.05, 0.8], default: 0.25, uniform: 'u_speed', bias: 'motion', rate: true },
palette: { type: 'palette', count: 5 },
},
reactive: {
amp: { feature: 'beat', amount: 0.4, response: 'spike' },
bleed: { feature: 'flux', amount: 0.25, response: 'smooth' },
glow: { feature: 'bandHigh', amount: 0.2 },
},
shader: `
vec4 scene(vec2 uv, vec2 p) {
float t = u_time * u_speed + u_seed;
p = sigCamera(p);
// One shared grid for every discontinuity below: slices re-roll on the
// quantised step rather than moving continuously frame-to-frame.
float q = max(u_quantize, 1.0);
float step_ = floor(u_barPhase * q) + floor(t * 6.0) * q;
// A vertical slice is a column of pixels; each gets one fixed offset per
// step, decided by a hash of (slice, step) so it jumps cleanly on the grid.
float slice = floor(uv.x * u_slices);
float sliceRand = hash12(vec2(slice, step_ * 31.0));
float pitch = (sliceRand - 0.5) * 2.0 * u_amp;
// The base image is always sampled from the un-pitched field so the scene
// has real content behind the displacement, even on its first frame.
vec2 baseUv = vec2(uv.x, fract(uv.y - pitch));
float field = fbm(vec2(baseUv.x * 2.5, baseUv.y * 4.0) + t * 0.4, 4);
float ramp = fract(field * 2.0 + baseUv.y * 2.0 - t * 0.4);
vec3 col = palRamp(ramp * 0.7 + slice * 0.02);
col *= 0.4 + 0.6 * smoothstep(0.1, 0.9, field);
// The smear: pull the previous frame across the pitch so slices leave a
// transient ghost as they land, and the corruption feels sludgy.
vec3 ghost = prev(vec2(uv.x, fract(uv.y + pitch * 0.55)));
col = mix(col, ghost, u_bleed * (0.4 + sliceRand));
// A thin scan beam rooting the hard edge of each step, kept local and beat
// -pinned so the frame never swings in whole-frame luminance.
float beamY = fract(u_beatPhase) * 2.0 - 1.0;
float beam = exp(-abs(p.y - beamY * 0.6) * 6.0);
col += pal(3) * beam * u_slices * 0.001 * (0.5 + u_beat);
col += pal(int(mod(slice, 5.0))) * (1.0 - sliceRand) * u_glow * 0.3 * sliceRand;
col += sigGrain(uv);
return vec4(col, 1.0);
}
`,
};