Venues: persistent room, motivated camera, ride; drop Pylon Grid
- Venue as one continuous place: persistent per-song layer, interpolated venueState, story jumps (one re-hang max + recap) with camera cuts - Room camera shot plan (venueShots.js): per-section size/target, monotonic look sweep, traverse glide body, room-side alternation - New Ride venue: generated vehicle + actor passengers, radio RTT screen, chase/stationary modes, ride archetype across generator/camera/staging - Flora cost trim (5x(12+6+6) -> 4x(8+4+4) SDF evals, 204ms -> 140ms worst) - Delete Pylon Grid shader (2.5s/frame SwiftShader); drop its metadata row - tools/venue-check.js headless gate harness + venue-gallery explorer
This commit is contained in:
@@ -0,0 +1,436 @@
|
||||
// VENUE: Ride — a night drive. A randomly generated vehicle (the track's
|
||||
// monolith body as chassis, signature-form panels) carries actor passengers;
|
||||
// the viz plays on the car radio screen; the road scrolls beneath in chase
|
||||
// mode or the world sits still in stationary mode. Camera orbits, pushes in
|
||||
// and pulls out per section.
|
||||
//
|
||||
// One venue = one place (this car, this road). Primitives + actorToGeometry
|
||||
// only — no glTF kit. Per-section evolution via venueState
|
||||
// {light,fog,popMul,reachMul} from VenueSpec.perSection, interpolated by the
|
||||
// ArcDriver (never stepped).
|
||||
|
||||
import { actorToGeometry } from '../actors/meshes.js';
|
||||
|
||||
export const ride = {
|
||||
name: 'Ride',
|
||||
archetype: 'ride',
|
||||
family: 'structural',
|
||||
kind: 'model',
|
||||
venue: true,
|
||||
consumes: ['form', 'ink', 'staging'],
|
||||
traits: ['shape', 'space', 'camera', 'style'],
|
||||
texture: 0.4,
|
||||
// The radio is a small bright screen in a dark cabin — it earns the blaze
|
||||
// about as often as the stage wall.
|
||||
reach: 0.6,
|
||||
|
||||
params: {
|
||||
density: { type: 'float', range: [0.2, 0.9], default: 0.5, uniform: 'u_density', bias: 'density' },
|
||||
speed: { type: 'float', range: [0, 1.5], default: 0.7, uniform: 'u_speed', bias: 'motion' },
|
||||
light: { type: 'float', range: [0.4, 1.3], default: 0.85, uniform: 'u_light', bias: 'energy' },
|
||||
palette: { type: 'palette', count: 4 },
|
||||
},
|
||||
|
||||
reactive: {},
|
||||
|
||||
// Venue RTT: the compositor calls this with the finished viz texture after
|
||||
// compositing the viz stack into vizTarget. The radio screen samples it,
|
||||
// so the viz plays ON the dash rather than composited over the venue.
|
||||
bindVizTexture(instance, texture) {
|
||||
if (!instance || !texture) return;
|
||||
if (instance.radioMat && instance.radioMat.emissiveMap !== texture) {
|
||||
instance.radioMat.emissiveMap = texture;
|
||||
instance.radioMat.needsUpdate = true;
|
||||
}
|
||||
},
|
||||
|
||||
// Radio-glow wash for the viz composite — arcs with venueState.light.
|
||||
// See Layer.washColor.
|
||||
washColor(instance) {
|
||||
const st = (instance && instance.venueState) || null;
|
||||
const light = st ? st.light : 0.7;
|
||||
const glow = 0.08 + light * 0.1;
|
||||
return [glow, glow, glow * 1.15];
|
||||
},
|
||||
|
||||
build({ scene, seed, params, actorSpec, venueSpec, THREE }) {
|
||||
const T = THREE;
|
||||
const rng = mulberry(seed >>> 0 || 1);
|
||||
|
||||
// Road: long dark plane the car sits over. In chase mode its texture
|
||||
// offset scrolls (see update); in stationary it holds still.
|
||||
const roadMat = new T.MeshStandardMaterial({
|
||||
color: new T.Color(0.09, 0.09, 0.11),
|
||||
roughness: 0.9,
|
||||
metalness: 0.05,
|
||||
});
|
||||
const road = new T.Mesh(new T.PlaneGeometry(30, 120), roadMat);
|
||||
road.rotation.x = -Math.PI / 2;
|
||||
road.position.set(0, -1.15, -30);
|
||||
scene.add(road);
|
||||
|
||||
// Lane dashes: small emissive quads scrolling with the road, so speed
|
||||
// reads even when the asphalt is near-black.
|
||||
const dashMat = new T.MeshStandardMaterial({
|
||||
color: new T.Color(0.05, 0.05, 0.05),
|
||||
emissive: new T.Color(0.9, 0.85, 0.6),
|
||||
emissiveIntensity: 0.7,
|
||||
roughness: 0.6,
|
||||
});
|
||||
const dashGeo = new T.PlaneGeometry(0.18, 2.2);
|
||||
const dashes = new T.Group();
|
||||
dashes.name = 'dashes';
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const d = new T.Mesh(dashGeo, dashMat);
|
||||
d.rotation.x = -Math.PI / 2;
|
||||
d.position.set(i % 2 === 0 ? -1.6 : 1.6, -1.13, -70 + i * 6);
|
||||
dashes.add(d);
|
||||
}
|
||||
scene.add(dashes);
|
||||
|
||||
// Lane edge glow: two long emissive strips so the road reads as a
|
||||
// road even when the asphalt is near-black.
|
||||
const edgeMat = new T.MeshStandardMaterial({
|
||||
color: new T.Color(0.05, 0.05, 0.06),
|
||||
emissive: new T.Color(0.5, 0.55, 0.8),
|
||||
emissiveIntensity: 0.5,
|
||||
roughness: 0.7,
|
||||
});
|
||||
const edgeGeo = new T.PlaneGeometry(0.12, 120);
|
||||
const edges = [];
|
||||
for (const ex of [-3.2, 3.2]) {
|
||||
const edge = new T.Mesh(edgeGeo, edgeMat);
|
||||
edge.rotation.x = -Math.PI / 2;
|
||||
edge.position.set(ex, -1.12, -30);
|
||||
scene.add(edge);
|
||||
edges.push(edge);
|
||||
}
|
||||
|
||||
// The car: a group at the origin facing -z (driving away from camera
|
||||
// start). Body panels come from the track's actor; chassis/cabin/wheels
|
||||
// are seeded primitives so every song's vehicle differs.
|
||||
const car = new T.Group();
|
||||
car.name = 'car';
|
||||
scene.add(car);
|
||||
|
||||
// Cabin glow + headlights so the car reads at night.
|
||||
const ambient = new T.AmbientLight(0xffffff, 0.5);
|
||||
scene.add(ambient);
|
||||
const key = new T.DirectionalLight(0xcfe0ff, 1.0);
|
||||
key.position.set(3.5, 5.0, 6.0);
|
||||
key.castShadow = false;
|
||||
scene.add(key);
|
||||
const tail = new T.PointLight(0xff2a1a, 2.5, 10);
|
||||
tail.position.set(0, 0.2, 2.6);
|
||||
scene.add(tail);
|
||||
|
||||
scene.background = null;
|
||||
scene.fog = new T.Fog(0x07070c, 14, 46);
|
||||
scene.userData.transparentBackground = true;
|
||||
|
||||
// Passenger container — actor riders seated in the cabin.
|
||||
const crew = new T.Group();
|
||||
crew.name = 'crew';
|
||||
car.add(crew);
|
||||
|
||||
return {
|
||||
car, road, roadMat, dashes, dashMat, edges, edgeMat, crew,
|
||||
ambient, key, tail,
|
||||
radioMat: null, radioMesh: null,
|
||||
bodyMeshes: [],
|
||||
seed,
|
||||
venueSpec: venueSpec || null,
|
||||
builtKey: null,
|
||||
popCount: 0,
|
||||
// Chase (road scrolls, world rushes) vs stationary (parked, camera
|
||||
// does all the moving). Drawn once per video from the venue seed.
|
||||
chase: (rng() < 0.62),
|
||||
bodyHue: rng(),
|
||||
};
|
||||
},
|
||||
|
||||
update({ instance, scene, camera, timeline, features, params, palette, personality, framing, opacity, actorSpec, venueSpec, venueState, venueArrangement = null, THREE }) {
|
||||
const T = THREE;
|
||||
const t = timeline.time;
|
||||
const pal = palette && palette.length ? palette : [[0.9, 0.9, 0.92], [0.7, 0.6, 0.8], [0.4, 0.6, 0.9], [0.9, 0.5, 0.4]];
|
||||
const identity = personality ? personality.identity : null;
|
||||
const shape = personality ? personality.shape : null;
|
||||
const space = personality ? personality.space : null;
|
||||
const style = personality ? personality.style : null;
|
||||
|
||||
const shapeTilt = shape ? shape.tilt : 0;
|
||||
const styleWeight = style ? style.lineWeight : 0.5;
|
||||
const spaceHorizon = space ? space.horizon : 0.5;
|
||||
void shapeTilt;
|
||||
|
||||
const vs = venueSpec || instance.venueSpec;
|
||||
const vState = venueState || instance.venueState || null;
|
||||
if (vState) instance.venueState = vState;
|
||||
const arrangement = venueArrangement ?? instance.venueArrangement ?? 0;
|
||||
instance.venueArrangement = arrangement;
|
||||
|
||||
const c0 = pal[0];
|
||||
const fieldLight = vState ? vState.light : 0.7;
|
||||
// Asphalt takes hue from the palette, level from the section — a night
|
||||
// road you can still see.
|
||||
const tMax = Math.max(c0[0], c0[1], c0[2], 1e-3);
|
||||
const palLum = 0.2126 * c0[0] + 0.7152 * c0[1] + 0.0722 * c0[2];
|
||||
const comp = 0.55 + Math.max(0, 0.45 - palLum) * 1.1;
|
||||
const roadLevel = (0.16 + fieldLight * 0.24) * comp;
|
||||
instance.roadMat.color.setRGB(
|
||||
(c0[0] / tMax) * roadLevel, (c0[1] / tMax) * roadLevel, (c0[2] / tMax) * roadLevel);
|
||||
if (instance.key) instance.key.intensity = 0.7 + fieldLight * 1.1;
|
||||
if (instance.ambient) instance.ambient.intensity = 0.4 + fieldLight * 0.5;
|
||||
if (instance.tail) instance.tail.intensity = 1.2 + fieldLight * 2;
|
||||
if (scene.fog) {
|
||||
const fogMul = vState ? vState.fog : 0.35;
|
||||
const fogDepth = (space ? 10 + space.depth * 8 : 15) * (0.9 + fogMul * 0.45);
|
||||
scene.fog.near = fogDepth;
|
||||
scene.fog.far = fogDepth + 30;
|
||||
scene.fog.color.setRGB(c0[0] * 0.05, c0[1] * 0.05, c0[2] * 0.07);
|
||||
}
|
||||
|
||||
// --- vehicle body (built once; arrangement re-hangs it) ---
|
||||
const popKey = vs
|
||||
? `${vs.seed}:ride:${identity ? identity.cast.protagonist.sides : 0}:${actorSpec ? actorSpec.seed : 0}:a${arrangement}`
|
||||
: `no-vs:${instance.seed}:a${arrangement}`;
|
||||
if ((instance.builtKey !== popKey || !instance.popCount) && identity && actorSpec && actorSpec.form) {
|
||||
// Clear previous build.
|
||||
for (const m of instance.bodyMeshes) {
|
||||
instance.car.remove(m);
|
||||
m.traverse((o) => {
|
||||
if (o.isMesh) {
|
||||
if (o.geometry) o.geometry.dispose();
|
||||
if (o.material) {
|
||||
const mats = Array.isArray(o.material) ? o.material : [o.material];
|
||||
mats.forEach((mm) => mm.dispose());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
instance.bodyMeshes = [];
|
||||
while (instance.crew.children.length) instance.crew.remove(instance.crew.children[0]);
|
||||
|
||||
const rng = mulberry(((vs ? vs.seed : instance.seed) ^ Math.imul(arrangement + 1, 0x9e3779b9)) >>> 0);
|
||||
const cc = (i) => pal[(vs && vs.paletteMap ? vs.paletteMap[i % vs.paletteMap.length] : i) % pal.length] || pal[0];
|
||||
|
||||
// Chassis: the track's monolith body, squashed wide, low and long.
|
||||
// Same character as the viz hero, so the car reads as this track's
|
||||
// vehicle rather than a stock model.
|
||||
try {
|
||||
const built = actorToGeometry(actorSpec, identity, T);
|
||||
const parts = built.children.slice();
|
||||
const body = new T.Group();
|
||||
body.name = 'chassis';
|
||||
for (let i = 0; i < Math.min(3, parts.length); i++) {
|
||||
const src = parts[(i + arrangement) % parts.length];
|
||||
const col = cc(i);
|
||||
const m = new T.Mesh(src.geometry, new T.MeshStandardMaterial({
|
||||
color: new T.Color(col[0] * 0.9, col[1] * 0.9, col[2] * 0.9),
|
||||
emissive: new T.Color(col[0] * 0.15, col[1] * 0.15, col[2] * 0.15),
|
||||
roughness: 0.3 + styleWeight * 0.15,
|
||||
metalness: 0.3,
|
||||
transparent: opacity < 0.999, opacity,
|
||||
}));
|
||||
m.userData.partIndex = src.userData.partIndex ?? i;
|
||||
m.scale.set(1.5, 0.55, 2.4);
|
||||
m.position.set(0, -0.15 + i * 0.12, -0.4 + i * 0.35);
|
||||
m.rotation.y = (rng() - 0.5) * 0.6;
|
||||
body.add(m);
|
||||
}
|
||||
instance.car.add(body);
|
||||
instance.bodyMeshes.push(body);
|
||||
} catch { /* body optional; cabin + wheels still read as a car */ }
|
||||
|
||||
// Cabin: low dark-glass tub so passenger heads clear the roofline
|
||||
// from rear angles — a tall box hid the whole crew.
|
||||
const glassCol = cc(3);
|
||||
const cabin = new T.Mesh(
|
||||
new T.BoxGeometry(1.7, 0.4, 2.1),
|
||||
new T.MeshStandardMaterial({
|
||||
color: new T.Color(0.04, 0.045, 0.06),
|
||||
emissive: new T.Color(glassCol[0] * 0.12, glassCol[1] * 0.12, glassCol[2] * 0.14),
|
||||
roughness: 0.15, metalness: 0.7,
|
||||
transparent: opacity < 0.999, opacity,
|
||||
}));
|
||||
cabin.position.set(0, 0.5, -0.2);
|
||||
instance.car.add(cabin);
|
||||
instance.bodyMeshes.push(cabin);
|
||||
|
||||
// Wheels: four dark torus + hub, spin with speed (see below).
|
||||
const wheelGeo = new T.TorusGeometry(0.34, 0.13, 8, 18);
|
||||
const hubGeo = new T.CylinderGeometry(0.14, 0.14, 0.1, 10);
|
||||
hubGeo.rotateZ(Math.PI / 2);
|
||||
instance.wheels = [];
|
||||
for (const [wx, wz] of [[-0.95, 1.05], [0.95, 1.05], [-0.95, -1.35], [0.95, -1.35]]) {
|
||||
const wcol = cc(1);
|
||||
const wheel = new T.Group();
|
||||
const tire = new T.Mesh(wheelGeo, new T.MeshStandardMaterial({
|
||||
color: new T.Color(0.03, 0.03, 0.035), roughness: 0.9,
|
||||
}));
|
||||
const hub = new T.Mesh(hubGeo, new T.MeshStandardMaterial({
|
||||
color: new T.Color(wcol[0], wcol[1], wcol[2]),
|
||||
emissive: new T.Color(wcol[0] * 0.25, wcol[1] * 0.25, wcol[2] * 0.25),
|
||||
roughness: 0.4, metalness: 0.6,
|
||||
}));
|
||||
wheel.add(tire); wheel.add(hub);
|
||||
wheel.position.set(wx, -0.72, wz);
|
||||
instance.car.add(wheel);
|
||||
instance.bodyMeshes.push(wheel);
|
||||
instance.wheels.push(wheel);
|
||||
}
|
||||
|
||||
// Radio: the viz screen. Rises above the dash on a small stalk so
|
||||
// it reads over the bodywork from chase angles — emissiveMap bound
|
||||
// per-frame by bindVizTexture, emissive stays near-white so the
|
||||
// viz keeps its own colours.
|
||||
const radioMat = new T.MeshStandardMaterial({
|
||||
color: new T.Color(0.02, 0.02, 0.02),
|
||||
emissive: new T.Color(1, 1, 1),
|
||||
emissiveIntensity: 1.2,
|
||||
roughness: 0.5, metalness: 0.2,
|
||||
side: T.DoubleSide,
|
||||
});
|
||||
const stalk = new T.Mesh(
|
||||
new T.CylinderGeometry(0.05, 0.07, 0.5, 8),
|
||||
new T.MeshStandardMaterial({ color: new T.Color(0.05, 0.05, 0.06), roughness: 0.6 }));
|
||||
stalk.position.set(0, 0.75, 0.9);
|
||||
instance.car.add(stalk);
|
||||
instance.bodyMeshes.push(stalk);
|
||||
const radio = new T.Mesh(new T.PlaneGeometry(1.3, 0.73), radioMat);
|
||||
radio.position.set(0, 1.15, 0.9);
|
||||
radio.rotation.x = -0.12;
|
||||
radio.rotation.y = Math.PI;
|
||||
radio.name = 'radio';
|
||||
instance.car.add(radio);
|
||||
instance.radioMat = radioMat;
|
||||
instance.radioMesh = radio;
|
||||
instance.bodyMeshes.push(radio);
|
||||
|
||||
// Headlight cones: two small translucent wedges so chase mode
|
||||
// reads. Kept short and faint — long cones filled the frame.
|
||||
const beamMat = new T.MeshBasicMaterial({
|
||||
color: new T.Color(0.75, 0.8, 0.9), transparent: true, opacity: 0.05,
|
||||
depthWrite: false, side: T.DoubleSide,
|
||||
});
|
||||
for (const bx of [-0.55, 0.55]) {
|
||||
const beamGeo = new T.ConeGeometry(0.45, 3.5, 12, 1, true);
|
||||
beamGeo.translate(0, -1.75, 0);
|
||||
beamGeo.rotateX(-Math.PI / 2 - 0.06);
|
||||
const beam = new T.Mesh(beamGeo, beamMat);
|
||||
beam.position.set(bx, -0.1, -2.2);
|
||||
instance.car.add(beam);
|
||||
instance.bodyMeshes.push(beam);
|
||||
}
|
||||
|
||||
// Passengers: actor riders — one part of the track's body each,
|
||||
// dealt round like chamber memories, seated in a row in the cabin.
|
||||
// Same limbs as the viz hero, so the crew reads as its passengers.
|
||||
let bodyParts = null;
|
||||
try {
|
||||
const body = actorToGeometry(actorSpec, identity, T);
|
||||
bodyParts = body.children.slice();
|
||||
} catch { bodyParts = null; }
|
||||
const count = vs && vs.population ? Math.max(2, Math.min(4, vs.population.count | 0)) : 2;
|
||||
for (let i = 0; i < count; i++) {
|
||||
let m;
|
||||
if (bodyParts && bodyParts.length) {
|
||||
const src = bodyParts[(i + arrangement) % bodyParts.length];
|
||||
const col = cc(i + 1);
|
||||
m = new T.Mesh(src.geometry, new T.MeshStandardMaterial({
|
||||
color: new T.Color(col[0], col[1], col[2]),
|
||||
emissive: new T.Color(col[0] * 0.3, col[1] * 0.3, col[2] * 0.3),
|
||||
emissiveIntensity: 1.0,
|
||||
roughness: 0.5, metalness: 0.1,
|
||||
transparent: opacity < 0.999, opacity,
|
||||
}));
|
||||
m.userData.partIndex = src.userData.partIndex ?? 0;
|
||||
} else {
|
||||
const col = cc(i + 1);
|
||||
m = new T.Mesh(
|
||||
new T.SphereGeometry(0.22, 12, 10),
|
||||
new T.MeshStandardMaterial({
|
||||
color: new T.Color(col[0], col[1], col[2]),
|
||||
emissive: new T.Color(col[0] * 0.3, col[1] * 0.3, col[2] * 0.3),
|
||||
roughness: 0.5,
|
||||
}));
|
||||
}
|
||||
const g = new T.Group();
|
||||
g.add(m);
|
||||
// Seat row: spread across the cabin width, heads above the glass.
|
||||
const fx = (i - (count - 1) / 2) * 0.62 + (rng() - 0.5) * 0.1;
|
||||
g.scale.setScalar(0.5);
|
||||
g.position.set(fx, 0.78, -0.25 + (rng() - 0.5) * 0.3);
|
||||
g.rotation.y = Math.PI + (rng() - 0.5) * 0.5;
|
||||
g.userData.baseY = g.position.y;
|
||||
g.userData.index = i;
|
||||
g.userData.phase = rng() * Math.PI * 2;
|
||||
instance.crew.add(g);
|
||||
}
|
||||
|
||||
instance.builtKey = popKey;
|
||||
instance.popCount = count;
|
||||
}
|
||||
|
||||
// --- motion: analytic, no integration ---
|
||||
const speed = (params.speed || 0.7) * (instance.chase ? 1 : 0);
|
||||
const rate = 1 + (features.bandLow || 0) * 0.6;
|
||||
// Road scroll in chase mode: dashes stream past; parked otherwise.
|
||||
if (instance.chase && speed > 0.01) {
|
||||
const dz = speed * 9 * rate;
|
||||
for (const d of instance.dashes.children) {
|
||||
let z = d.position.z + dz * 0.016;
|
||||
if (z > 4) z -= 72;
|
||||
d.position.z = z;
|
||||
}
|
||||
}
|
||||
// Wheels spin with speed; bob with the low band.
|
||||
const bob = Math.sin(t * 2.1) * 0.02 * (0.4 + (features.bandLow || 0));
|
||||
instance.car.position.y = bob * (instance.chase ? 1 : 0.3);
|
||||
instance.car.rotation.z = Math.sin(t * 0.9) * 0.008 * (instance.chase ? 1 : 0.3);
|
||||
if (instance.wheels) {
|
||||
for (const w of instance.wheels) w.rotation.x = t * speed * 6 * rate;
|
||||
}
|
||||
// Passengers bob gently, out of phase — analytic f(t), seek-stable.
|
||||
// popMul gates how many are visible (setup: driver + 1; climax: full car).
|
||||
const popMul = vState ? vState.popMul : 1;
|
||||
for (const g of instance.crew.children) {
|
||||
const idx = g.userData.index || 0;
|
||||
g.position.y = (g.userData.baseY || 0) + Math.sin(t * 0.5 + (g.userData.phase || 0)) * 0.03;
|
||||
g.rotation.y = Math.PI + Math.sin(t * 0.18 + idx * 1.3) * 0.22;
|
||||
const showIndex = idx / Math.max(1, instance.popCount);
|
||||
g.visible = opacity > 0.01 && (showIndex < (0.35 + popMul * 0.75));
|
||||
for (const mesh of g.children) {
|
||||
if (mesh.isMesh) {
|
||||
mesh.material.opacity = opacity;
|
||||
mesh.material.transparent = opacity < 0.999;
|
||||
if (vState && mesh.material.emissiveIntensity !== undefined) {
|
||||
mesh.material.emissiveIntensity = 0.5 + vState.light * 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Radio glow follows the section light; the IMAGE is the viz texture.
|
||||
if (instance.radioMat) {
|
||||
const l = vState ? vState.light : 1;
|
||||
instance.radioMat.emissive.setRGB(1, 1, 1);
|
||||
instance.radioMat.emissiveIntensity = (params.light || 0.85) * (0.7 + l * 0.7);
|
||||
}
|
||||
// Ground follows horizon like the stage field.
|
||||
instance.road.position.y = -1.15 + (spaceHorizon - 0.5) * 0.6;
|
||||
|
||||
void camera; void framing; void params;
|
||||
},
|
||||
};
|
||||
|
||||
function mulberry(seed) {
|
||||
let a = seed >>> 0;
|
||||
return function () {
|
||||
a |= 0; a = (a + 0x6D2B79F5) | 0;
|
||||
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
export default ride;
|
||||
Reference in New Issue
Block a user