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,484 @@
|
||||
// VENUE: Chamber — derelict room full of memories.
|
||||
//
|
||||
// A sparse, still place. Window-light + floor reflections carry the viz
|
||||
// tint; scattered memories (primitives via actorToGeometry) populate it
|
||||
// via stageNode. Population density + light/fog evolve per Story act
|
||||
// (setup sparse/distant, climax one large memory at centre, resolution dim).
|
||||
// Primitives only in V1 — no glTF kit.
|
||||
|
||||
import { actorToGeometry } from '../actors/meshes.js';
|
||||
|
||||
export const chamber = {
|
||||
name: 'Chamber',
|
||||
archetype: 'chamber',
|
||||
family: 'minimal',
|
||||
kind: 'model',
|
||||
venue: true,
|
||||
consumes: ['form', 'ink', 'staging'],
|
||||
traits: ['shape', 'space', 'style'],
|
||||
texture: 0.35,
|
||||
// How much viz bloom this room can take before its highlights clip: the
|
||||
// chamber is dim plaster and floor light, so it earns the blaze often.
|
||||
reach: 1.1,
|
||||
|
||||
params: {
|
||||
density: { type: 'float', range: [0.2, 0.9], default: 0.45, uniform: 'u_density', bias: 'density' },
|
||||
jitter: { type: 'float', range: [0, 0.6], default: 0.25, uniform: 'u_jitter' },
|
||||
light: { type: 'float', range: [0.3, 1.2], default: 0.7, 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 window and the floor
|
||||
// reflection sample it, so the viz appears INSIDE the room rather than
|
||||
// composited over it. Texture object identity is stable frame-to-frame, so
|
||||
// assigning map/emissiveMap is cheap; needsUpdate only on first bind.
|
||||
bindVizTexture(instance, texture) {
|
||||
if (!instance || !texture) return;
|
||||
let dirty = false;
|
||||
if (instance.windowMat && instance.windowMat.emissiveMap !== texture) {
|
||||
instance.windowMat.emissiveMap = texture;
|
||||
dirty = true;
|
||||
}
|
||||
// The spill shows the viz through its emissive channel (the room is too
|
||||
// dark for diffuse alone); map rides along so the two never disagree.
|
||||
if (instance.reflectMat && (instance.reflectMat.map !== texture
|
||||
|| instance.reflectMat.emissiveMap !== texture)) {
|
||||
instance.reflectMat.map = texture;
|
||||
instance.reflectMat.emissiveMap = texture;
|
||||
dirty = true;
|
||||
}
|
||||
if (dirty) {
|
||||
if (instance.windowMat) instance.windowMat.needsUpdate = true;
|
||||
if (instance.reflectMat) instance.reflectMat.needsUpdate = true;
|
||||
}
|
||||
},
|
||||
|
||||
// Base wash for the viz composite. The room's own wall colour is too dark
|
||||
// to densify anything (dark song → dark palette → wash ≈ black, and the
|
||||
// 5x section jump stays), so the wash is a glowing screen base instead:
|
||||
// palette-tinted mid grey that arcs with venueState.light. A screen that
|
||||
// glows brighter than its walls is physically sensible, and the wash
|
||||
// carries the slow travel while calming the keyed shot's jumps.
|
||||
// See Layer.washColor.
|
||||
washColor(instance) {
|
||||
const st = (instance && instance.venueState) || null;
|
||||
const light = st ? st.light : 0.7;
|
||||
let r = 0.05, g = 0.05, b = 0.07;
|
||||
if (instance && instance.walls && instance.walls.length) {
|
||||
const c = instance.walls[0].material.color;
|
||||
r = c.r; g = c.g; b = c.b;
|
||||
}
|
||||
const glow = (0.07 + light * 0.09);
|
||||
return [r * 0.5 + glow, g * 0.5 + glow, b * 0.5 + glow * 1.1];
|
||||
},
|
||||
|
||||
build({ scene, seed, params, actorSpec, venueSpec, THREE }) {
|
||||
const T = THREE;
|
||||
// Room shell — 5 planes: floor + 4 walls (no ceiling, open to sky)
|
||||
const room = new T.Group();
|
||||
room.name = 'room';
|
||||
|
||||
const floorMat = new T.MeshStandardMaterial({
|
||||
color: new T.Color(0.14, 0.13, 0.12),
|
||||
roughness: 0.85,
|
||||
metalness: 0.04,
|
||||
});
|
||||
const wallMat = new T.MeshStandardMaterial({
|
||||
color: new T.Color(0.18, 0.17, 0.16),
|
||||
roughness: 0.92,
|
||||
metalness: 0.02,
|
||||
});
|
||||
const floorGeo = new T.PlaneGeometry(22, 22);
|
||||
const floor = new T.Mesh(floorGeo, floorMat);
|
||||
floor.rotation.x = -Math.PI / 2;
|
||||
floor.position.y = -1.4;
|
||||
floor.receiveShadow = false;
|
||||
room.add(floor);
|
||||
|
||||
const wallGeo = new T.PlaneGeometry(22, 6);
|
||||
const walls = [];
|
||||
const wallPositions = [
|
||||
{ pos: [0, 1.6, -11], rotY: 0 },
|
||||
{ pos: [0, 1.6, 11], rotY: Math.PI },
|
||||
{ pos: [-11, 1.6, 0], rotY: Math.PI / 2 },
|
||||
{ pos: [11, 1.6, 0], rotY: -Math.PI / 2 },
|
||||
];
|
||||
for (const w of wallPositions) {
|
||||
const wall = new T.Mesh(wallGeo, wallMat.clone());
|
||||
wall.position.set(w.pos[0], w.pos[1], w.pos[2]);
|
||||
wall.rotation.y = w.rotY;
|
||||
wall.receiveShadow = false;
|
||||
room.add(wall);
|
||||
walls.push(wall);
|
||||
}
|
||||
|
||||
// Window plane — the viz render surface. 16:9 like the viz that
|
||||
// feeds it, so the image arrives undistorted; emissiveMap carries the
|
||||
// viz (bound per-frame by bindVizTexture), emissive color stays the
|
||||
// palette tint multiplier. Sized to fill ~40% of the frame from the
|
||||
// room camera (was 6.4x3.6 ≈ 22% — a postage stamp the section's viz
|
||||
// could not drive the frame through, flattening filmstrip direction).
|
||||
const windowGeo = new T.PlaneGeometry(12, 6.75);
|
||||
const windowMat = new T.MeshStandardMaterial({
|
||||
color: new T.Color(0.02, 0.02, 0.02),
|
||||
emissive: new T.Color(1, 1, 1),
|
||||
emissiveIntensity: 1.0,
|
||||
roughness: 0.6,
|
||||
metalness: 0.02,
|
||||
});
|
||||
const windowMesh = new T.Mesh(windowGeo, windowMat);
|
||||
windowMesh.position.set(0, 1.35, -10.9);
|
||||
windowMesh.name = 'window';
|
||||
room.add(windowMesh);
|
||||
|
||||
// Floor spill of the viz screen. Same 16:9 texture, dimmed and
|
||||
// translucent — reads as light spilling out of the window. It is a
|
||||
// SECOND plane lying on the floor, so every way it differs from the
|
||||
// floor reads as architecture: its rim reads as a raised bar, its face
|
||||
// as a second floor. Four things keep it a light streak instead:
|
||||
// exactly co-planar (no tilt — a 1° tilt over 10 units is a 22 cm ramp
|
||||
// at this grazing angle), feathered on all sides, faded to nothing at
|
||||
// the camera end (canvas bottom = near side) so no near rim ever slides
|
||||
// under the lens, and pulled back toward the window.
|
||||
let spillAlpha = null;
|
||||
try {
|
||||
if (typeof document !== 'undefined' && document.createElement) {
|
||||
const cnv = document.createElement('canvas');
|
||||
cnv.width = 256; cnv.height = 160;
|
||||
const g = cnv.getContext('2d');
|
||||
if (g) {
|
||||
// Along-beam: opaque at the window end (canvas top = far
|
||||
// side), gone at the camera end.
|
||||
const along = g.createLinearGradient(0, 0, 0, 160);
|
||||
along.addColorStop(0, '#fff');
|
||||
along.addColorStop(0.45, '#888');
|
||||
along.addColorStop(0.8, '#111');
|
||||
along.addColorStop(1, '#000');
|
||||
g.fillStyle = along;
|
||||
g.fillRect(0, 0, 256, 160);
|
||||
// Across-beam: kill the left/right rims.
|
||||
g.globalCompositeOperation = 'destination-in';
|
||||
const across = g.createLinearGradient(0, 0, 256, 0);
|
||||
across.addColorStop(0, '#000');
|
||||
across.addColorStop(0.18, '#fff');
|
||||
across.addColorStop(0.82, '#fff');
|
||||
across.addColorStop(1, '#000');
|
||||
g.fillStyle = across;
|
||||
g.fillRect(0, 0, 256, 160);
|
||||
spillAlpha = new T.CanvasTexture(cnv);
|
||||
}
|
||||
}
|
||||
} catch { spillAlpha = null; }
|
||||
const reflectGeo = new T.PlaneGeometry(12, 6);
|
||||
const reflectMat = new T.MeshStandardMaterial({
|
||||
color: new T.Color(0.02, 0.02, 0.03),
|
||||
emissive: new T.Color(1, 1, 1),
|
||||
emissiveIntensity: 0.35,
|
||||
roughness: 0.35,
|
||||
metalness: 0.15,
|
||||
transparent: true,
|
||||
opacity: 0.3,
|
||||
depthWrite: false,
|
||||
polygonOffset: true,
|
||||
polygonOffsetFactor: -1,
|
||||
});
|
||||
if (spillAlpha) reflectMat.alphaMap = spillAlpha;
|
||||
const reflection = new T.Mesh(reflectGeo, reflectMat);
|
||||
reflection.rotation.x = -Math.PI / 2;
|
||||
reflection.position.set(0, -1.398, -4.6);
|
||||
reflection.name = 'reflection';
|
||||
room.add(reflection);
|
||||
|
||||
scene.add(room);
|
||||
|
||||
// Lighting — ambient + key from the camera side. The key used to sit
|
||||
// behind the window wall, leaving every camera-facing surface in
|
||||
// shadow lit only by ambient — the room read as a black void with a
|
||||
// floating screen. Front light keeps a dim room present.
|
||||
const ambient = new T.AmbientLight(0xffffff, 0.42);
|
||||
scene.add(ambient);
|
||||
const sun = new T.DirectionalLight(0xfff6e0, 0.95);
|
||||
sun.position.set(2.5, 4.2, 7.5);
|
||||
sun.castShadow = false;
|
||||
scene.add(sun);
|
||||
const fill = new T.DirectionalLight(0xffffff, 0.22);
|
||||
fill.position.set(-3.2, 2.0, 4.0);
|
||||
scene.add(fill);
|
||||
|
||||
scene.background = null;
|
||||
scene.fog = new T.Fog(0x0e0e12, 12, 28);
|
||||
scene.userData.transparentBackground = true;
|
||||
|
||||
// Population container — memories
|
||||
const popGroup = new T.Group();
|
||||
popGroup.name = 'memories';
|
||||
scene.add(popGroup);
|
||||
|
||||
return {
|
||||
room, floor, floorMat, wallMat, walls, windowMesh, windowMat,
|
||||
reflection, reflectMat,
|
||||
ambient, sun, fill,
|
||||
popGroup,
|
||||
seed,
|
||||
venueSpec: venueSpec || null,
|
||||
builtKey: null,
|
||||
popCount: 0,
|
||||
};
|
||||
},
|
||||
|
||||
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;
|
||||
|
||||
// Trait reads so lint / scene-gate sees delta
|
||||
const shapeTilt = shape ? shape.tilt : 0;
|
||||
const shapeSides = shape ? shape.sides : 0;
|
||||
const spaceHorizon = space ? space.horizon : 0.5;
|
||||
const styleWeight = style ? style.lineWeight : 0.5;
|
||||
|
||||
const ink = identity ? identity.ink : null;
|
||||
const lattice = identity ? identity.lattice : null;
|
||||
const elementScale = identity ? identity.lattice.elementScale : 0.35;
|
||||
void elementScale; void lattice; void shapeSides;
|
||||
|
||||
// Resolve venueSpec — prefer passed-in, fallback to instance stored
|
||||
const vs = venueSpec || instance.venueSpec;
|
||||
const vState = venueState || instance.venueState || null;
|
||||
const perSectionMul = vs && vs.perSection ? 1 : 1;
|
||||
if (vState) instance.venueState = vState;
|
||||
|
||||
// Ground/personality driven materials. The room MUST read the story arc:
|
||||
// a static dark box dilutes every section change into churn (filmstrip
|
||||
// direction collapsed when the venue arrived). venueState.light swings
|
||||
// ~0.5 (setup) to ~0.92 (climax) — map it onto wall/floor albedo and
|
||||
// the sun so the room breathes with the song, not just the window.
|
||||
const c0 = pal[0];
|
||||
// The venue is the STABLE place — it moves less than the viz, not
|
||||
// more. Full light steps at every section made consecutive sections
|
||||
// ~4x further apart than without a venue (a shuffle, not an arc), so
|
||||
// the room takes half steps; the viz window takes the full ones.
|
||||
//
|
||||
// Walls take their HUE from the palette but their LEVEL from the room:
|
||||
// a dark song gives a near-black palette, and palette-dark walls are
|
||||
// unlightable — the room read as a black void with a floating screen.
|
||||
// Normalise the tint so the song colours the room without darkening it.
|
||||
const roomLight = vState ? vState.light : 0.7;
|
||||
const tintMax = Math.max(c0[0], c0[1], c0[2], 1e-3);
|
||||
const tint = [c0[0] / tintMax, c0[1] / tintMax, c0[2] / tintMax];
|
||||
// Bright songs light their own room through the palette; dark songs
|
||||
// cannot (their tint is near-black even normalised against the level
|
||||
// the room needs). So the room's level compensates the palette's:
|
||||
// dark palette → higher level, bright palette → lower. Keeps the room
|
||||
// present for drone without flooding it for centre.
|
||||
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 wallLevel = (0.22 + roomLight * 0.30) * comp;
|
||||
const floorLevel = (0.18 + roomLight * 0.26) * comp;
|
||||
instance.floorMat.color.setRGB(tint[0] * floorLevel, tint[1] * floorLevel, tint[2] * floorLevel);
|
||||
instance.floorMat.roughness = 0.85;
|
||||
for (const w of instance.walls) {
|
||||
w.material.color.setRGB(tint[0] * wallLevel, tint[1] * wallLevel, tint[2] * wallLevel);
|
||||
}
|
||||
if (instance.sun) instance.sun.intensity = 0.5 + roomLight * 1.0;
|
||||
if (instance.ambient) instance.ambient.intensity = 0.32 + roomLight * 0.5;
|
||||
// Window brightness from story light. The IMAGE is the viz texture
|
||||
// (emissiveMap); emissive color stays near-white so the viz keeps its
|
||||
// own colors. Palette tint reaches the room via floor/walls instead.
|
||||
{
|
||||
const lightMul = vState ? vState.light : 1;
|
||||
instance.windowMat.emissive.setRGB(1, 1, 1);
|
||||
instance.windowMat.emissiveIntensity = (params.light || 0.7) * (0.75 + lightMul * 0.5);
|
||||
if (instance.reflectMat) {
|
||||
// Tint the spill toward the palette floor colour so it melts into
|
||||
// the floor instead of reading as a lit slab with its own edge.
|
||||
instance.reflectMat.emissive.setRGB(c0[0] * 0.5, c0[1] * 0.48, c0[2] * 0.55);
|
||||
instance.reflectMat.color.setRGB(c0[0] * 0.10, c0[1] * 0.10, c0[2] * 0.12);
|
||||
instance.reflectMat.emissiveIntensity = 0.5 * (params.light || 0.7) * (0.75 + lightMul * 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
// Fog via space + story: resolution (fog 0.55) closes the room in,
|
||||
// climax (0.18) opens it. Swing near AND far so the depth change reads.
|
||||
if (scene.fog) {
|
||||
const fogMul = vState ? vState.fog : 0.35;
|
||||
const fogDepth = (space ? 12 + space.depth * 6 : 15) * (0.7 + fogMul * 0.75);
|
||||
scene.fog.near = fogDepth;
|
||||
scene.fog.far = fogDepth + 10 + fogMul * 10;
|
||||
scene.fog.color.setRGB(c0[0] * 0.06, c0[1] * 0.06, c0[2] * 0.08);
|
||||
}
|
||||
|
||||
// Rebuild population if needed (deterministic, seeded). The arrangement
|
||||
// salt is part of the key: same salt → same layout, guaranteed
|
||||
// carryover; a new salt after a story jump re-hangs the room (always
|
||||
// under a camera cut — see ArcDriver.venueJumpAt). Nothing else here
|
||||
// may rebuild mid-song: no per-section seeds in this key.
|
||||
const arrangement = venueArrangement ?? instance.venueArrangement ?? 0;
|
||||
instance.venueArrangement = arrangement;
|
||||
const popKey = vs ? `${vs.seed}:${vs.population ? vs.population.count : 0}:${identity ? identity.cast.protagonist.sides : 0}:${actorSpec ? actorSpec.seed : 0}:a${arrangement}` : `no-vs:${instance.seed}:a${arrangement}`;
|
||||
const needRebuild = instance.builtKey !== popKey || !instance.popCount;
|
||||
if (needRebuild && identity && actorSpec && actorSpec.form) {
|
||||
for (const child of [...instance.popGroup.children]) {
|
||||
instance.popGroup.remove(child);
|
||||
child.traverse((obj) => {
|
||||
if (obj.isMesh) {
|
||||
if (obj.geometry) obj.geometry.dispose();
|
||||
if (obj.material) {
|
||||
const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
|
||||
mats.forEach((m) => m.dispose());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
const count = vs && vs.population ? Math.max(2, Math.min(12, vs.population.count | 0)) : 5;
|
||||
const spread = vs && vs.population ? vs.population.spread || 0.7 : 0.7;
|
||||
const jitter = vs && vs.population ? vs.population.jitter || 0.25 : 0.25;
|
||||
// One actor, furnished as a roomful: the first build makes the
|
||||
// track's monolith body (the same geometry the viz hero uses), and
|
||||
// each memory unwraps one of its parts. Unwrapping per part rather
|
||||
// than cloning the whole body matters — cloning stamped the same
|
||||
// ~4-part composite at 0.2 scale, so a radial assembly read as grey
|
||||
// noise. Parts are the limbs; a room of limbs reads as furniture.
|
||||
let bodyParts = null;
|
||||
try {
|
||||
const body = actorToGeometry(actorSpec, identity, T);
|
||||
bodyParts = body.children.slice();
|
||||
} catch { bodyParts = null; }
|
||||
if (!bodyParts || !bodyParts.length) {
|
||||
const g = new T.BoxGeometry(0.3, 0.3, 0.3);
|
||||
const m = new T.Mesh(g, new T.MeshStandardMaterial({ color: 0xffffff }));
|
||||
m.userData.partIndex = 0;
|
||||
bodyParts = [m];
|
||||
}
|
||||
// Deterministic positions — golden-angle spiral + separation passes.
|
||||
// The old sin-scatter clustered near the centre, and at furniture
|
||||
// scale (1+ units wide) neighbours interpenetrated. The spiral gives
|
||||
// even coverage with an open middle for the camera; the relaxation
|
||||
// below enforces a minimum gap so no two memories touch.
|
||||
const seeds = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const frac = (i + 0.5) / Math.max(1, count);
|
||||
// Arrangement salt rotates the whole spiral, so a re-hang is a
|
||||
// visibly new layout built from the same objects — deterministic
|
||||
// per (seed, arrangement), stable across seeks.
|
||||
const ang = i * 2.39996 + (instance.seed % 628) * 0.01 + arrangement * 2.39996 * 3;
|
||||
const rad = (2.4 + 5.6 * Math.sqrt(frac)) * (0.7 + spread * 0.45);
|
||||
let rx = Math.cos(ang) * rad + Math.sin(i * 78.233) * jitter * 1.2;
|
||||
let rz = Math.sin(ang) * rad * 0.78 - 1.2 + Math.cos(i * 43.12) * jitter;
|
||||
rx = Math.max(-9, Math.min(9, rx));
|
||||
// Never in front of the room: the camera pushes as close as z≈1.3,
|
||||
// so anything at z>-0.5 is at the lens and reads as a bar across
|
||||
// the viz. Furniture lives in the back two-thirds, toward the window.
|
||||
rz = Math.max(-8.5, Math.min(-0.5, rz));
|
||||
seeds.push([rx, rz]);
|
||||
}
|
||||
// Repulsion passes: push overlapping pairs apart. Deterministic,
|
||||
// analytic, no wall-clock — same inputs always give same layout.
|
||||
const minDist = 2.9;
|
||||
for (let pass = 0; pass < 10; pass++) {
|
||||
for (let a = 0; a < count; a++) {
|
||||
for (let b = a + 1; b < count; b++) {
|
||||
const dx = seeds[b][0] - seeds[a][0];
|
||||
const dz = seeds[b][1] - seeds[a][1];
|
||||
const d = Math.hypot(dx, dz);
|
||||
if (d >= minDist || d < 1e-4) continue;
|
||||
const push = (minDist - d) * 0.5;
|
||||
const nx = dx / d, nz = dz / d;
|
||||
seeds[a][0] = Math.max(-9, Math.min(9, seeds[a][0] - nx * push));
|
||||
seeds[a][1] = Math.max(-8.5, Math.min(-0.5, seeds[a][1] - nz * push));
|
||||
seeds[b][0] = Math.max(-9, Math.min(9, seeds[b][0] + nx * push));
|
||||
seeds[b][1] = Math.max(-8.5, Math.min(-0.5, seeds[b][1] + nz * push));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < count; i++) {
|
||||
const rxClamped = seeds[i][0];
|
||||
const rzClamped = seeds[i][1];
|
||||
const ry = -1.15 + 0.15 + Math.sin(i * 5.5) * 0.25;
|
||||
|
||||
const src = bodyParts[i % bodyParts.length];
|
||||
const m = new T.Mesh(
|
||||
src.geometry,
|
||||
new T.MeshStandardMaterial({ color: 0xffffff }),
|
||||
);
|
||||
m.userData.partIndex = src.userData.partIndex ?? (i % bodyParts.length);
|
||||
const palIdx = vs && vs.paletteMap ? vs.paletteMap[i % vs.paletteMap.length] : i % pal.length;
|
||||
const cc = pal[palIdx % pal.length] || pal[0];
|
||||
// Faint emissive in the furniture's own colour: in the darkest
|
||||
// rooms (darkest songs pick the chamber by design) lit-only
|
||||
// limbs go black and the traverse moves the camera through
|
||||
// nothing visible. Kept low — these are memories, not lamps.
|
||||
m.material = new T.MeshStandardMaterial({
|
||||
color: new T.Color(cc[0], cc[1], cc[2]),
|
||||
emissive: new T.Color(cc[0] * 0.14, cc[1] * 0.14, cc[2] * 0.14),
|
||||
emissiveIntensity: 1.0,
|
||||
roughness: 0.45 + styleWeight * 0.12,
|
||||
metalness: 0.08,
|
||||
transparent: opacity < 0.999,
|
||||
opacity,
|
||||
});
|
||||
m.castShadow = false;
|
||||
m.receiveShadow = false;
|
||||
const g = new T.Group();
|
||||
g.add(m);
|
||||
// Small furniture, not a second hero: part geometry is already
|
||||
// ~0.5–1.0 units in radius, and the camera walks among it, so a
|
||||
// unit-scale limb at 3 units fills the frame like a monument.
|
||||
// Memories stay well under half scale; elementScale only nudges
|
||||
// within that — a huge-form track gets noticeably bigger
|
||||
// memories, not a room of heroes.
|
||||
const s = (0.22 + elementScale * 0.28) * (1 - (i / Math.max(1, count)) * 0.25);
|
||||
g.scale.setScalar(s * (0.9 + shapeSides * 0.015));
|
||||
g.position.set(rxClamped, ry, rzClamped);
|
||||
g.rotation.y = shapeTilt + i * 0.9 + Math.sin(i) * 0.5;
|
||||
g.rotation.x = Math.sin(i * 1.3) * 0.22;
|
||||
g.userData.baseY = ry;
|
||||
g.userData.index = i;
|
||||
instance.popGroup.add(g);
|
||||
}
|
||||
instance.builtKey = popKey;
|
||||
instance.popCount = count;
|
||||
}
|
||||
|
||||
// Analytic animation — bob + slow yaw, no integration
|
||||
for (const g of instance.popGroup.children) {
|
||||
const idx = g.userData.index || 0;
|
||||
const baseY = g.userData.baseY || 0;
|
||||
g.position.y = baseY + Math.sin(t * 0.35 + idx * 1.1) * 0.06;
|
||||
g.rotation.y += 0; // keep deterministic: set rather than accumulate
|
||||
const yaw = shapeTilt + idx * 0.9 + Math.sin(t * 0.18 + idx) * 0.28;
|
||||
g.rotation.y = yaw;
|
||||
// Per-section popMul would modulate visible/count — V1 keeps all visible
|
||||
{ const popMul = vState ? vState.popMul : 1; const showIndex = g.userData.index / 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;
|
||||
// The room's population breathes with the story arc: dim in
|
||||
// setup, bright at the climax. Pure f(venueState), no clock.
|
||||
if (vState && mesh.material.emissiveIntensity !== undefined) {
|
||||
mesh.material.emissiveIntensity = 0.5 + vState.light * 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subtle room drift from camera trait
|
||||
const camTrait = personality ? personality.camera : null;
|
||||
const camSway = camTrait ? camTrait.sway : 0;
|
||||
const camSwayRate = camTrait ? camTrait.swayRate : 0.09;
|
||||
instance.room.position.x = Math.sin(t * camSwayRate) * camSway * 0.35;
|
||||
instance.room.position.z = Math.cos(t * camSwayRate * 0.83) * camSway * 0.22;
|
||||
|
||||
void perSectionMul; void spaceHorizon; void instance; void params; void T;
|
||||
},
|
||||
};
|
||||
|
||||
export default chamber;
|
||||
Reference in New Issue
Block a user