Phase 0: determinism spine
Engine core: Timeline (fixed dt, audio-mastered in realtime), seeded Rng, Renderer, Layer/ShaderLayer/SceneLayer, Compositor with blend modes, feedback and post chain. Shader scenes are compiled against a fixed uniform contract and define only `vec4 scene(vec2 uv, vec2 p)`, so adding a scene costs a shader plus a params block. Deep Nebula ported from party-stage as the first one. Gate passes, 7/7 in checks.html: - 300 frames rendered twice are bit-identical - a fresh Engine reproduces the same frames - simulated dropped frames change nothing (proves dt is fixed) - seek matches sequential playback - 320x180 vs 1280x720 agree within 0.010 (limit 0.06) - seeded rng reproducible, forked streams independent - compositor reset clears feedback history Static gates: no wall-clock or unseeded randomness in deterministic directories; scene schemas and shader sources agree in both directions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import { validateModule } from '../params/schema.js';
|
||||
|
||||
import { nebula } from './shader/nebula.js';
|
||||
|
||||
/**
|
||||
* The scene library. Families exist so the arc driver can choose by section
|
||||
* character rather than at random — a breakdown never lands on a strobing glitch
|
||||
* scene, and an intro never opens at full density.
|
||||
*/
|
||||
export const FAMILIES = {
|
||||
flow: { label: 'Flow', energy: [0.0, 0.7] },
|
||||
organic: { label: 'Organic', energy: [0.0, 0.8] },
|
||||
minimal: { label: 'Minimal', energy: [0.0, 0.45] },
|
||||
structural: { label: 'Structural', energy: [0.3, 0.9] },
|
||||
geometric: { label: 'Geometric', energy: [0.4, 1.0] },
|
||||
glitch: { label: 'Glitch', energy: [0.6, 1.0] },
|
||||
};
|
||||
|
||||
const MODULES = [
|
||||
nebula,
|
||||
];
|
||||
|
||||
const errors = [];
|
||||
for (const m of MODULES) {
|
||||
const e = validateModule(m);
|
||||
if (e.length) errors.push(...e);
|
||||
if (m.family && !FAMILIES[m.family]) errors.push(`${m.name}: unknown family '${m.family}'`);
|
||||
}
|
||||
if (errors.length) {
|
||||
// Fail loudly at import: a malformed scene must never reach the compositor,
|
||||
// where the symptom would be a black frame with no explanation.
|
||||
console.error('[registry] invalid scene modules:\n' + errors.join('\n'));
|
||||
}
|
||||
|
||||
export const scenes = MODULES;
|
||||
export const sceneErrors = errors;
|
||||
|
||||
export function sceneByName(name) {
|
||||
return MODULES.find((m) => m.name === name) || null;
|
||||
}
|
||||
|
||||
export function scenesInFamily(family) {
|
||||
return MODULES.filter((m) => m.family === family);
|
||||
}
|
||||
|
||||
export function familyNames() {
|
||||
return Object.keys(FAMILIES);
|
||||
}
|
||||
|
||||
export default scenes;
|
||||
@@ -0,0 +1,59 @@
|
||||
// Ported from party-stage's "Deep Nebula". Changes on port:
|
||||
// - LED-grid mask removed (it existed to sell a screen inside a 3D room)
|
||||
// - hardcoded vec3 colours replaced with palette lookups
|
||||
// - magic numbers lifted into declared params
|
||||
// - beat reactivity moved from a single u_beat multiply to the reactive block
|
||||
|
||||
export const nebula = {
|
||||
name: 'Deep Nebula',
|
||||
family: 'organic',
|
||||
kind: 'fragment',
|
||||
|
||||
params: {
|
||||
scale: { type: 'float', range: [4, 24], default: 12, uniform: 'u_scale', bias: 'density' },
|
||||
swirl: { type: 'float', range: [0, 3], default: 1.0, uniform: 'u_swirl', bias: 'motion' },
|
||||
rings: { type: 'float', range: [0, 1], default: 0.5, uniform: 'u_rings' },
|
||||
glow: { type: 'float', range: [0, 1.5], default: 0.5, uniform: 'u_glow', bias: 'energy' },
|
||||
speed: { type: 'float', range: [0.1, 1.5],default: 0.5, uniform: 'u_speed', bias: 'motion' },
|
||||
depth: { type: 'float', range: [0, 1], default: 0.4, uniform: 'u_depth' },
|
||||
palette: { type: 'palette', count: 4 },
|
||||
},
|
||||
|
||||
reactive: {
|
||||
glow: { feature: 'beat', amount: 0.35, response: 'spike' },
|
||||
swirl: { feature: 'bandLow', amount: 0.20 },
|
||||
rings: { feature: 'flux', amount: 0.25, response: 'smooth' },
|
||||
},
|
||||
|
||||
shader: `
|
||||
vec4 scene(vec2 uv, vec2 p) {
|
||||
float r = length(p);
|
||||
float a = atan(p.y, p.x);
|
||||
float t = u_time * u_speed + u_seed;
|
||||
|
||||
// Layered drift, the "nebula" body.
|
||||
float n = fbm(p * u_scale * 0.25 + vec2(t * 0.2, -t * 0.15), 5);
|
||||
float n2 = fbm(p * u_scale * 0.6 - vec2(t * 0.1, t * 0.25) + n, 4);
|
||||
float body = sin(r * u_scale - t * 2.0 + n * 6.0 * u_swirl) * 0.5 + 0.5;
|
||||
body = mix(body, n2, u_depth);
|
||||
|
||||
vec3 col = mix(pal(0), pal(1), sat(body));
|
||||
col = mix(col, pal(2), sat(n2 * n2) * u_depth);
|
||||
|
||||
// Concentric pulse, tied to onset energy rather than a fixed rate.
|
||||
float ring = smoothstep(0.4, 0.5, abs(fract(r * 2.0 - t * 3.0) - 0.5));
|
||||
col += pal(1) * ring * u_rings * 0.6;
|
||||
|
||||
// Core glow.
|
||||
float edge = 1.0 - smoothstep(0.1, 0.9, r);
|
||||
col += pal(3) * edge * u_glow * 0.5;
|
||||
|
||||
// Vignette the far field so the frame has a subject.
|
||||
col *= 0.5 + 0.5 * (1.0 - smoothstep(0.6, 1.6, r));
|
||||
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default nebula;
|
||||
Reference in New Issue
Block a user