Synthwave verges stay off the road; camera stays level unless the tail says otherwise
Synthwave Run: roadside passers stream on the verges, not on the road. Each passer places at roadHalf + psize*0.92 + gap where roadHalf mirrors the grid's abs(p.x*perspective) < 0.8, so even a large chorus form's edge stays clear of the centre lane reserved for the protagonist. Lateral wobble reduced to worst ~0.025 inward vs. minimum 0.07 gap so it can never carry back onto the road. Four random chorus objects rushing horizon->camera make each song's traffic its own; on-road traffic the hero weaves around is left as a separate kind later. Also carries the previous chorus passer loop, hero castSolid, and texture 0.4 / form+staging declarations still pending in the working tree from the last step. Personality camera: uniform boxes put as many tracks at the wild edge as near level, so the grid swung too often. Replace with a bell curve (Box-Muller over the seeded Rng) — mode at level, rarity in the tails. driftRate/sway/swayRate/spin/breathe now via gaussAround(centre,sigma) centred near level (spin at 0, sign from the Gaussian itself) with audio tilting the centre (fast→more drift/sway/spin, dynamic→breathe) rather than displacing the whole box. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c216e31413
commit
df609424d5
@ -46,6 +46,15 @@ export const TRAITS = ['shape', 'camera', 'space', 'style'];
|
|||||||
|
|
||||||
const clamp01 = (x) => Math.max(0, Math.min(1, x));
|
const clamp01 = (x) => Math.max(0, Math.min(1, x));
|
||||||
|
|
||||||
|
// Box-Muller standard normal from the seeded rng. Two uniform draws per
|
||||||
|
// sample; the mode is at 0 and the tails are rare, which is the whole
|
||||||
|
// point — uniform boxes put as many tracks at the wild edge as near level.
|
||||||
|
function gauss(rng) {
|
||||||
|
const u1 = Math.max(rng.next(), 1e-7);
|
||||||
|
const u2 = rng.next();
|
||||||
|
return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Traits eligible to be a track's signature, and how often.
|
* Traits eligible to be a track's signature, and how often.
|
||||||
*
|
*
|
||||||
@ -137,16 +146,29 @@ export function generatePersonality(summary, rng, countEligible = null, sections
|
|||||||
tilt: rng.range(0, Math.PI),
|
tilt: rng.range(0, Math.PI),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Camera: bell-curve over level vs. wild. Most tracks stay near
|
||||||
|
// level — a little drift, a little sway, almost no roll — and only
|
||||||
|
// the tail really swings. Uniform boxes put as many tracks at the
|
||||||
|
// wild edge as near level, which is why the grid read as wild so
|
||||||
|
// often; a Gaussian puts the mode at level and rarity in the tails.
|
||||||
|
// Audio still tilts the centre (fast → more drift/sway/spin,
|
||||||
|
// dynamic → more breathe) but the seed picks within a bell around it.
|
||||||
|
const gaussAround = (centre, sigma, lo, hi) =>
|
||||||
|
Math.max(lo, Math.min(hi, centre + gauss(rng) * sigma));
|
||||||
const camera = {
|
const camera = {
|
||||||
driftAngle: rng.range(0, Math.PI * 2),
|
driftAngle: rng.range(0, Math.PI * 2),
|
||||||
// A slow track should not be filmed from a moving car.
|
// A slow track should not be filmed from a moving car. Centre near
|
||||||
driftRate: rng.range(0.01, 0.06) * (0.6 + fast * 0.8),
|
// level (0.016 + tempo), sigma narrow so wild drift is a tail event.
|
||||||
sway: around(0.01 + fast * 0.035, 0.02, 0, 0.06),
|
driftRate: gaussAround(0.016 + fast * 0.016, 0.010, 0.004, 0.065),
|
||||||
swayRate: around(0.08 + fast * 0.1, 0.06, 0.05, 0.22),
|
sway: gaussAround(0.010 + fast * 0.018, 0.009, 0, 0.055),
|
||||||
spin: around((rng.bool() ? 1 : -1) * fast * 0.03, 0.025, -0.05, 0.05),
|
swayRate: gaussAround(0.09 + fast * 0.06, 0.028, 0.05, 0.22),
|
||||||
|
// Spin centred at zero — most tracks stay level, only the tails roll.
|
||||||
|
// Spread scales with tempo so a fast track has a wider bell, not a
|
||||||
|
// displaced one. Sign comes from the Gaussian itself.
|
||||||
|
spin: gaussAround(0, 0.012 + fast * 0.012, -0.05, 0.05),
|
||||||
// Breathing is locked to the bar, so it is the one camera move that
|
// Breathing is locked to the bar, so it is the one camera move that
|
||||||
// reads as musical rather than as drifting. A dynamic track breathes.
|
// reads as musical rather than as drifting. A dynamic track breathes.
|
||||||
breathe: around(dynamic * 0.03, 0.02, 0, 0.05),
|
breathe: gaussAround(dynamic * 0.022, 0.010, 0, 0.048),
|
||||||
};
|
};
|
||||||
|
|
||||||
const space = {
|
const space = {
|
||||||
|
|||||||
@ -8,7 +8,9 @@
|
|||||||
// track drove the same car. The thing on the road is now the song's own
|
// track drove the same car. The thing on the road is now the song's own
|
||||||
// protagonist — the same solid every other stage in the video draws, seen
|
// protagonist — the same solid every other stage in the video draws, seen
|
||||||
// here as a single hero turning slowly above the grid — so a hexagonal
|
// here as a single hero turning slowly above the grid — so a hexagonal
|
||||||
// track drives a hexagonal form. See castSolid.
|
// track drives a hexagonal form — and the verges stream with its chorus,
|
||||||
|
// random objects rushing past that make each song's traffic unmistakably
|
||||||
|
// its own. See castSolid / castChorusSolid.
|
||||||
|
|
||||||
export const synthwaveRun = {
|
export const synthwaveRun = {
|
||||||
name: 'Synthwave Run',
|
name: 'Synthwave Run',
|
||||||
@ -26,6 +28,7 @@ export const synthwaveRun = {
|
|||||||
sun: { type: 'float', range: [0, 1], default: 1.0, uniform: 'u_sun' },
|
sun: { type: 'float', range: [0, 1], default: 1.0, uniform: 'u_sun' },
|
||||||
mountains: { type: 'float', range: [0, 1], default: 1.0, uniform: 'u_mountains' },
|
mountains: { type: 'float', range: [0, 1], default: 1.0, uniform: 'u_mountains' },
|
||||||
car: { type: 'float', range: [0, 1], default: 1.0, uniform: 'u_car' },
|
car: { type: 'float', range: [0, 1], default: 1.0, uniform: 'u_car' },
|
||||||
|
passers: { type: 'int', range: [0, 6], default: 3, uniform: 'u_passers', bias: 'density' },
|
||||||
palette: { type: 'palette', count: 4 },
|
palette: { type: 'palette', count: 4 },
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -81,6 +84,72 @@ vec4 scene(vec2 uv, vec2 p) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- roadside passers — the chorus streaming past on the verges ---
|
||||||
|
// Random chorus objects outside the road, rushing from the horizon toward
|
||||||
|
// the camera. Each one is a chorus member, so a hexagonal track is
|
||||||
|
// overtaken by hexagonal debris and a round track by round — the variety
|
||||||
|
// the old car could never provide. Kept strictly off the road stripe
|
||||||
|
// (abs(p.x) < 0.8/(horizon - y + 0.05)) so the centre lane stays clear
|
||||||
|
// for the protagonist; on-road traffic that the hero weaves around would
|
||||||
|
// be a separate kind later.
|
||||||
|
if (u_passers > 0) {
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
if (i >= u_passers) break;
|
||||||
|
float fi = float(i);
|
||||||
|
float ra = hash12(vec2(fi * 7.13 + 1.1, u_seed * 0.31 + fi * 3.7));
|
||||||
|
float rb = hash12(vec2(fi * 3.19 + 4.2, u_seed * 0.73 + fi * 1.9));
|
||||||
|
float side = ra < 0.5 ? -1.0 : 1.0;
|
||||||
|
// each passer on its own phase so they are staggered, not a convoy
|
||||||
|
float phase = fract(t * (0.28 + rb * 0.10) + ra * 7.0 + fi * 1.63);
|
||||||
|
float f = pow(phase, 0.85);
|
||||||
|
// along-road position: horizon to bottom, perspective is in the x offset
|
||||||
|
float py = mix(horizon + 0.06, -0.92, f);
|
||||||
|
float psize = mix(0.05, 0.24, f) * stageScale() * (0.70 + rb * 0.65);
|
||||||
|
// road half-width at this depth — matches the grid's
|
||||||
|
// abs(p.x*perspective) < 0.8 => abs(p.x) < 0.8*(horizon - y + 0.05)
|
||||||
|
// clamp: just above the horizon that goes slightly negative
|
||||||
|
float roadHalf = max(0.015, 0.8 * (horizon - py + 0.05));
|
||||||
|
// verge offset: strictly outside the road stripe at this depth.
|
||||||
|
// Grows with psize so even a large chorus form's edge stays clear
|
||||||
|
// of the road — the centre lane is reserved for the protagonist.
|
||||||
|
// On-road traffic the hero weaves around would be a separate kind.
|
||||||
|
float gap = 0.07 + rb * 0.12;
|
||||||
|
float verge = roadHalf + psize * 0.92 + gap;
|
||||||
|
vec2 passPos = vec2(side * verge, py);
|
||||||
|
// slight lateral wobble — small enough it can never carry back onto the road
|
||||||
|
// (worst inward ~0.025, gap is >= 0.07)
|
||||||
|
passPos.x += sin(t * 0.35 + ra * 6.28) * 0.018 * (0.4 + rb * 0.6);
|
||||||
|
passPos.y += cos(t * 0.5 + rb * 6.28) * 0.012;
|
||||||
|
float bound = dot(p - passPos, p - passPos) / max(psize * psize, 1e-6);
|
||||||
|
if (bound > 1.7) continue;
|
||||||
|
vec2 local = (p - passPos) / max(psize, 1e-3);
|
||||||
|
if (dot(local, local) > 1.7) continue;
|
||||||
|
mat3 turn = castTurn(t * (0.45 + rb * 0.6) + ra * 6.28 + fi * 2.1,
|
||||||
|
sin(t * 0.4 + ra * 9.0) * 0.55 + f * 0.25);
|
||||||
|
vec3 n;
|
||||||
|
float hit = castChorusSolid(local, turn, n);
|
||||||
|
float depthShade = mix(0.38, 1.0, f);
|
||||||
|
if (hit > 0.0) {
|
||||||
|
vec3 lit = castLit(n, vec3(0.0, 0.0, 1.0));
|
||||||
|
lit *= depthShade * (0.75 + 0.25 * u_beat);
|
||||||
|
// pick palette entry per passer so the stream keeps the colour rhythm
|
||||||
|
lit *= mix(vec3(1.0), pal(int(mod(fi * 1.7 + ra * 3.0, 4.0))), 0.35);
|
||||||
|
col = mix(col, lit, 0.90);
|
||||||
|
}
|
||||||
|
float d = castChorus3(turn * vec3(local, 0.0)) * psize;
|
||||||
|
col += pal(int(mod(fi * 1.3 + rb * 2.0, 4.0))) * inkStroke(d) * 0.55 * depthShade;
|
||||||
|
// faint glow — keeps far passers readable at horizon scale
|
||||||
|
col += pal(int(mod(fi + 1.0, 4.0))) * exp(-bound * 1.1) * 0.06 * depthShade;
|
||||||
|
// soft ground ellipse under each passer, stretched with perspective
|
||||||
|
if (f > 0.15) {
|
||||||
|
vec2 shP = vec2((p.x - passPos.x) / max(psize * 1.5, 1e-3),
|
||||||
|
(p.y - passPos.y + psize * 0.42) / max(psize * 0.24, 1e-3));
|
||||||
|
float sh = exp(-dot(shP, shP)) * 0.22 * depthShade * sat((f - 0.15) / 0.35);
|
||||||
|
col = mix(col, vec3(0.0), sh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- protagonist on the road ---
|
// --- protagonist on the road ---
|
||||||
// The same solid every other stage in this video draws, now as the hero
|
// The same solid every other stage in this video draws, now as the hero
|
||||||
// the camera is chasing down the grid. One instance, turning slowly so
|
// the camera is chasing down the grid. One instance, turning slowly so
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user