Chamber: procedural junk furniture instead of actor-part memories
Tables, chairs, cupboards, shelves with seeded book rows, potted plants and box stacks built from primitives, dealt from a shuffled deck per (seed, arrangement) so the first slots always land distinct types. Same spiral + repulsion placement with per-item footprint radii; front clamp pulled to z<=-1.5 for furniture scale so close cameras never wear a table as a lens bar. Static (junk doesn't bob) with the existing story-driven emissive breathe; re-hang on arrangement jumps moves everything under the camera cut. popKey drops the actor dependence. Verified: 24/24 tests, scene lint clean, same-frame re-render bit-identical, rooms read as furnished across centre/drone/elegy. Chamber-forced strips: elegy direction 0.26->0.61; centre stays pre-existing-flat (0.11->0.09, dark song, same early arrangement); drone agit unchanged (viz-driven).
This commit is contained in:
@@ -1,21 +1,181 @@
|
|||||||
// VENUE: Chamber — derelict room full of memories.
|
// VENUE: Chamber — derelict room full of junk furniture.
|
||||||
//
|
//
|
||||||
// A sparse, still place. Window-light + floor reflections carry the viz
|
// A sparse, still place. Window-light + floor reflections carry the viz
|
||||||
// tint; scattered memories (primitives via actorToGeometry) populate it
|
// tint; procedurally built junk (tables, chairs, cupboards, shelves with
|
||||||
// via stageNode. Population density + light/fog evolve per Story act
|
// books, plants, box stacks) populates it. Population density + light/fog
|
||||||
// (setup sparse/distant, climax one large memory at centre, resolution dim).
|
// evolve per Story act (setup sparse, climax bright, resolution dim); the
|
||||||
// Primitives only in V1 — no glTF kit.
|
// arrangement salt re-hangs the room on story jumps. Primitives only —
|
||||||
|
// no glTF kit.
|
||||||
|
|
||||||
import { actorToGeometry } from '../actors/meshes.js';
|
|
||||||
import { buildSky, paintSky } from './sky.js';
|
import { buildSky, paintSky } from './sky.js';
|
||||||
|
|
||||||
|
// --- procedural junk furniture ------------------------------------------------
|
||||||
|
// Primitives only, palette-lit, seeded. Every builder returns { group, radius }
|
||||||
|
// with the group origin at floor level (+y up); radius is the footprint used
|
||||||
|
// by the repulsion passes. Deterministic in (rng): same draws, same junk.
|
||||||
|
|
||||||
|
function mulberry32(a) {
|
||||||
|
let s = a >>> 0;
|
||||||
|
return function () {
|
||||||
|
s |= 0; s = (s + 0x6D2B79F5) | 0;
|
||||||
|
let z = Math.imul(s ^ (s >>> 15), 1 | s);
|
||||||
|
z = (z + Math.imul(z ^ (z >>> 7), 61 | z)) ^ z;
|
||||||
|
return ((z ^ (z >>> 14)) >>> 0) / 4294967296;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function junkMat(T, cc, rough = 0.7) {
|
||||||
|
// Faint emissive in the piece's own colour: in the darkest rooms (darkest
|
||||||
|
// songs pick the chamber by design) lit-only junk goes black and the
|
||||||
|
// traverse moves the camera through nothing visible. Kept low — these are
|
||||||
|
// dusty things, not lamps.
|
||||||
|
return 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: rough,
|
||||||
|
metalness: 0.06,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function junkBox(T, parent, mat, w, h, d, x, y, z, ry = 0, rz = 0) {
|
||||||
|
const m = new T.Mesh(new T.BoxGeometry(w, h, d), mat);
|
||||||
|
m.position.set(x, y, z);
|
||||||
|
m.rotation.y = ry;
|
||||||
|
m.rotation.z = rz;
|
||||||
|
m.castShadow = false;
|
||||||
|
m.receiveShadow = false;
|
||||||
|
parent.add(m);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTable(T, rng, pal) {
|
||||||
|
const g = new T.Group();
|
||||||
|
const main = junkMat(T, pal[(rng() * pal.length) | 0], 0.75);
|
||||||
|
const dark = junkMat(T, pal[(rng() * pal.length) | 0], 0.85);
|
||||||
|
junkBox(T, g, main, 1.5, 0.09, 0.95, 0, 0.72, 0);
|
||||||
|
for (const sx of [-0.65, 0.65]) for (const sz of [-0.38, 0.38]) {
|
||||||
|
junkBox(T, g, dark, 0.08, 0.72, 0.08, sx, 0.36, sz);
|
||||||
|
}
|
||||||
|
return { group: g, radius: 0.95 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildChair(T, rng, pal) {
|
||||||
|
const g = new T.Group();
|
||||||
|
const main = junkMat(T, pal[(rng() * pal.length) | 0], 0.7);
|
||||||
|
junkBox(T, g, main, 0.52, 0.07, 0.5, 0, 0.45, 0);
|
||||||
|
junkBox(T, g, main, 0.52, 0.6, 0.07, 0, 0.78, -0.245);
|
||||||
|
for (const sx of [-0.21, 0.21]) for (const sz of [-0.2, 0.2]) {
|
||||||
|
junkBox(T, g, main, 0.06, 0.45, 0.06, sx, 0.225, sz);
|
||||||
|
}
|
||||||
|
return { group: g, radius: 0.5 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCupboard(T, rng, pal) {
|
||||||
|
const g = new T.Group();
|
||||||
|
const main = junkMat(T, pal[(rng() * pal.length) | 0], 0.8);
|
||||||
|
const trim = junkMat(T, pal[(rng() * pal.length) | 0], 0.6);
|
||||||
|
junkBox(T, g, main, 1.15, 2.0, 0.55, 0, 1.0, 0);
|
||||||
|
junkBox(T, g, trim, 1.21, 0.08, 0.61, 0, 0.04, 0);
|
||||||
|
junkBox(T, g, trim, 1.21, 0.07, 0.61, 0, 2.03, 0);
|
||||||
|
for (const sx of [-0.27, 0.27]) {
|
||||||
|
junkBox(T, g, trim, 0.5, 1.7, 0.04, sx, 1.05, 0.28);
|
||||||
|
const knob = new T.Mesh(new T.SphereGeometry(0.035, 10, 8), trim);
|
||||||
|
knob.position.set(sx * 0.25, 1.05, 0.32);
|
||||||
|
g.add(knob);
|
||||||
|
}
|
||||||
|
return { group: g, radius: 0.75 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBooks(T, g, rng, pal, y, width) {
|
||||||
|
// One shelf-board of books: upright row, seeded widths/heights/colours,
|
||||||
|
// sometimes a leaner, sometimes a gap with a flat stack.
|
||||||
|
let x = -width / 2 + 0.08;
|
||||||
|
const end = width / 2 - 0.08;
|
||||||
|
while (x < end - 0.05) {
|
||||||
|
const r = rng();
|
||||||
|
if (r < 0.18 && x < end - 0.35) {
|
||||||
|
// Flat stack of 2-3.
|
||||||
|
const n = 2 + ((rng() * 2) | 0);
|
||||||
|
let sy = y;
|
||||||
|
for (let k = 0; k < n; k++) {
|
||||||
|
const bw = 0.3 + rng() * 0.12, bh = 0.055 + rng() * 0.02;
|
||||||
|
junkBox(T, g, junkMat(T, pal[(rng() * pal.length) | 0], 0.85),
|
||||||
|
bw, bh, 0.24, x + bw / 2, sy + bh / 2, 0, (rng() - 0.5) * 0.2);
|
||||||
|
sy += bh;
|
||||||
|
}
|
||||||
|
x += 0.48;
|
||||||
|
} else {
|
||||||
|
const bw = 0.05 + rng() * 0.05, bh = 0.28 + rng() * 0.14;
|
||||||
|
const lean = (rng() < 0.12 && x < end - 0.2) ? -0.16 : 0;
|
||||||
|
junkBox(T, g, junkMat(T, pal[(rng() * pal.length) | 0], 0.85),
|
||||||
|
bw, bh, 0.24, x + bw / 2, y + bh / 2, 0, 0, lean);
|
||||||
|
x += bw + 0.012 + (lean ? 0.06 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildShelf(T, rng, pal) {
|
||||||
|
const g = new T.Group();
|
||||||
|
const main = junkMat(T, pal[(rng() * pal.length) | 0], 0.8);
|
||||||
|
junkBox(T, g, main, 0.06, 1.9, 0.42, -0.72, 0.95, 0);
|
||||||
|
junkBox(T, g, main, 0.06, 1.9, 0.42, 0.72, 0.95, 0);
|
||||||
|
junkBox(T, g, main, 1.5, 1.9, 0.03, 0, 0.95, -0.2);
|
||||||
|
const boards = [0.12, 0.72, 1.3, 1.87];
|
||||||
|
for (const y of boards) junkBox(T, g, main, 1.44, 0.06, 0.4, 0, y, 0);
|
||||||
|
for (const y of boards.slice(0, 3)) buildBooks(T, g, rng, pal, y + 0.03, 1.32);
|
||||||
|
return { group: g, radius: 0.85 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPlant(T, rng, pal) {
|
||||||
|
const g = new T.Group();
|
||||||
|
const cc = pal[(rng() * pal.length) | 0];
|
||||||
|
const pot = junkMat(T, [cc[0] * 0.6 + 0.08, cc[1] * 0.6 + 0.05, cc[2] * 0.6 + 0.04], 0.9);
|
||||||
|
// Foliage reads leafy in any palette: palette hue pulled toward green.
|
||||||
|
const leaf = junkMat(T,
|
||||||
|
[cc[0] * 0.35 + 0.1, cc[1] * 0.35 + 0.3, cc[2] * 0.35 + 0.1], 0.95);
|
||||||
|
const potMesh = new T.Mesh(new T.CylinderGeometry(0.24, 0.17, 0.42, 12), pot);
|
||||||
|
potMesh.position.y = 0.21;
|
||||||
|
g.add(potMesh);
|
||||||
|
const trunk = new T.Mesh(new T.CylinderGeometry(0.03, 0.045, 0.55, 8), pot);
|
||||||
|
trunk.position.y = 0.65;
|
||||||
|
g.add(trunk);
|
||||||
|
const blobs = 2 + ((rng() * 2) | 0);
|
||||||
|
for (let i = 0; i < blobs; i++) {
|
||||||
|
const r = 0.3 + rng() * 0.2;
|
||||||
|
const b = new T.Mesh(new T.IcosahedronGeometry(r, 0), leaf);
|
||||||
|
b.position.set((rng() - 0.5) * 0.5, 1.0 + i * 0.32 + rng() * 0.15, (rng() - 0.5) * 0.5);
|
||||||
|
b.rotation.set(rng() * 3, rng() * 3, rng() * 3);
|
||||||
|
g.add(b);
|
||||||
|
}
|
||||||
|
return { group: g, radius: 0.55 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBoxes(T, rng, pal) {
|
||||||
|
const g = new T.Group();
|
||||||
|
const n = 2 + ((rng() * 2) | 0);
|
||||||
|
let y = 0;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const s = 0.55 + rng() * 0.3 - i * 0.06;
|
||||||
|
const h = 0.4 + rng() * 0.25;
|
||||||
|
junkBox(T, g, junkMat(T, pal[(rng() * pal.length) | 0], 0.9),
|
||||||
|
s, h, s * (0.8 + rng() * 0.4),
|
||||||
|
(rng() - 0.5) * 0.16, y + h / 2, (rng() - 0.5) * 0.16,
|
||||||
|
(rng() - 0.5) * 0.6);
|
||||||
|
y += h;
|
||||||
|
}
|
||||||
|
return { group: g, radius: 0.6 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const JUNK_BUILDERS = [buildTable, buildChair, buildCupboard, buildShelf, buildPlant, buildBoxes];
|
||||||
|
|
||||||
export const chamber = {
|
export const chamber = {
|
||||||
name: 'Chamber',
|
name: 'Chamber',
|
||||||
archetype: 'chamber',
|
archetype: 'chamber',
|
||||||
family: 'minimal',
|
family: 'minimal',
|
||||||
kind: 'model',
|
kind: 'model',
|
||||||
venue: true,
|
venue: true,
|
||||||
consumes: ['form', 'ink', 'staging'],
|
consumes: ['ink', 'staging'],
|
||||||
traits: ['shape', 'space', 'style'],
|
traits: ['shape', 'space', 'style'],
|
||||||
texture: 0.35,
|
texture: 0.35,
|
||||||
// How much viz bloom this room can take before its highlights clip: the
|
// How much viz bloom this room can take before its highlights clip: the
|
||||||
@@ -218,9 +378,9 @@ export const chamber = {
|
|||||||
const sky = buildSky(T, { radius: 90 });
|
const sky = buildSky(T, { radius: 90 });
|
||||||
scene.add(sky.mesh);
|
scene.add(sky.mesh);
|
||||||
|
|
||||||
// Population container — memories
|
// Population container — junk furniture
|
||||||
const popGroup = new T.Group();
|
const popGroup = new T.Group();
|
||||||
popGroup.name = 'memories';
|
popGroup.name = 'furniture';
|
||||||
scene.add(popGroup);
|
scene.add(popGroup);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -337,9 +497,9 @@ export const chamber = {
|
|||||||
// may rebuild mid-song: no per-section seeds in this key.
|
// may rebuild mid-song: no per-section seeds in this key.
|
||||||
const arrangement = venueArrangement ?? instance.venueArrangement ?? 0;
|
const arrangement = venueArrangement ?? instance.venueArrangement ?? 0;
|
||||||
instance.venueArrangement = arrangement;
|
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 popKey = vs ? `junk:${vs.seed}:${vs.population ? vs.population.count : 0}:a${arrangement}` : `junk:no-vs:${instance.seed}:a${arrangement}`;
|
||||||
const needRebuild = instance.builtKey !== popKey || !instance.popCount;
|
const needRebuild = instance.builtKey !== popKey || !instance.popCount;
|
||||||
if (needRebuild && identity && actorSpec && actorSpec.form) {
|
if (needRebuild) {
|
||||||
for (const child of [...instance.popGroup.children]) {
|
for (const child of [...instance.popGroup.children]) {
|
||||||
instance.popGroup.remove(child);
|
instance.popGroup.remove(child);
|
||||||
child.traverse((obj) => {
|
child.traverse((obj) => {
|
||||||
@@ -355,51 +515,55 @@ export const chamber = {
|
|||||||
const count = vs && vs.population ? Math.max(2, Math.min(12, vs.population.count | 0)) : 5;
|
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 spread = vs && vs.population ? vs.population.spread || 0.7 : 0.7;
|
||||||
const jitter = vs && vs.population ? vs.population.jitter || 0.25 : 0.25;
|
const jitter = vs && vs.population ? vs.population.jitter || 0.25 : 0.25;
|
||||||
// One actor, furnished as a roomful: the first build makes the
|
// Junk dealt from a shuffled deck: the first slots always land
|
||||||
// track's monolith body (the same geometry the viz hero uses), and
|
// distinct types (table, chair, cupboard, shelf, plant, boxes),
|
||||||
// each memory unwraps one of its parts. Unwrapping per part rather
|
// then the deck cycles. Seeded by (seed, arrangement), so a
|
||||||
// than cloning the whole body matters — cloning stamped the same
|
// re-hang deals different junk in different places — still
|
||||||
// ~4-part composite at 0.2 scale, so a radial assembly read as grey
|
// deterministic per key, stable across seeks.
|
||||||
// noise. Parts are the limbs; a room of limbs reads as furniture.
|
const rng = mulberry32(((instance.seed >>> 0)
|
||||||
let bodyParts = null;
|
^ Math.imul(arrangement + 1, 0x9E3779B9)) >>> 0);
|
||||||
try {
|
const deck = JUNK_BUILDERS.map((_, i) => i);
|
||||||
const body = actorToGeometry(actorSpec, identity, T);
|
for (let i = deck.length - 1; i > 0; i--) {
|
||||||
bodyParts = body.children.slice();
|
const j = (rng() * (i + 1)) | 0;
|
||||||
} catch { bodyParts = null; }
|
const tmp = deck[i]; deck[i] = deck[j]; deck[j] = tmp;
|
||||||
if (!bodyParts || !bodyParts.length) {
|
}
|
||||||
const g = new T.BoxGeometry(0.3, 0.3, 0.3);
|
// Build pieces first — each reports its footprint radius, which
|
||||||
const m = new T.Mesh(g, new T.MeshStandardMaterial({ color: 0xffffff }));
|
// the placement passes below need before positioning.
|
||||||
m.userData.partIndex = 0;
|
const pieces = [];
|
||||||
bodyParts = [m];
|
for (let i = 0; i < count; i++) {
|
||||||
|
const built = JUNK_BUILDERS[deck[i % deck.length]](T, rng, pal);
|
||||||
|
built.group.userData.index = i;
|
||||||
|
pieces.push(built);
|
||||||
}
|
}
|
||||||
// Deterministic positions — golden-angle spiral + separation passes.
|
// Deterministic positions — golden-angle spiral + separation passes.
|
||||||
// The old sin-scatter clustered near the centre, and at furniture
|
// The spiral gives even coverage with an open middle for the
|
||||||
// scale (1+ units wide) neighbours interpenetrated. The spiral gives
|
// camera; the relaxation below enforces per-pair gaps from the
|
||||||
// even coverage with an open middle for the camera; the relaxation
|
// pieces' own footprint radii, so a cupboard never swallows a
|
||||||
// below enforces a minimum gap so no two memories touch.
|
// chair. Arrangement salt rotates the whole spiral, so a re-hang
|
||||||
|
// is a visibly new layout — deterministic per (seed,
|
||||||
|
// arrangement), stable across seeks.
|
||||||
const seeds = [];
|
const seeds = [];
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const frac = (i + 0.5) / Math.max(1, count);
|
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 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);
|
const rad = (2.6 + 5.8 * Math.sqrt(frac)) * (0.7 + spread * 0.45);
|
||||||
let rx = Math.cos(ang) * rad + Math.sin(i * 78.233) * jitter * 1.2;
|
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;
|
let rz = Math.sin(ang) * rad * 0.78 - 1.2 + Math.cos(i * 43.12) * jitter;
|
||||||
rx = Math.max(-9, Math.min(9, rx));
|
rx = Math.max(-9, Math.min(9, rx));
|
||||||
// Never in front of the room: the camera pushes as close as z≈1.3,
|
// Never in front of the room: the camera pushes as close as z≈0.4,
|
||||||
// so anything at z>-0.5 is at the lens and reads as a bar across
|
// and at furniture scale (1+ units wide) anything nearer than
|
||||||
// the viz. Furniture lives in the back two-thirds, toward the window.
|
// z≈-1.5 is at the lens and reads as a bar across the viz.
|
||||||
rz = Math.max(-8.5, Math.min(-0.5, rz));
|
// Furniture lives in the back two-thirds, toward the window.
|
||||||
|
rz = Math.max(-8.5, Math.min(-1.5, rz));
|
||||||
seeds.push([rx, rz]);
|
seeds.push([rx, rz]);
|
||||||
}
|
}
|
||||||
// Repulsion passes: push overlapping pairs apart. Deterministic,
|
// Repulsion passes: push overlapping pairs apart. Minimum gap from
|
||||||
|
// the pair's footprints — big pieces need more room. Deterministic,
|
||||||
// analytic, no wall-clock — same inputs always give same layout.
|
// analytic, no wall-clock — same inputs always give same layout.
|
||||||
const minDist = 2.9;
|
for (let pass = 0; pass < 12; pass++) {
|
||||||
for (let pass = 0; pass < 10; pass++) {
|
|
||||||
for (let a = 0; a < count; a++) {
|
for (let a = 0; a < count; a++) {
|
||||||
for (let b = a + 1; b < count; b++) {
|
for (let b = a + 1; b < count; b++) {
|
||||||
|
const minDist = (pieces[a].radius + pieces[b].radius) * 0.7 + 0.6;
|
||||||
const dx = seeds[b][0] - seeds[a][0];
|
const dx = seeds[b][0] - seeds[a][0];
|
||||||
const dz = seeds[b][1] - seeds[a][1];
|
const dz = seeds[b][1] - seeds[a][1];
|
||||||
const d = Math.hypot(dx, dz);
|
const d = Math.hypot(dx, dz);
|
||||||
@@ -407,72 +571,33 @@ export const chamber = {
|
|||||||
const push = (minDist - d) * 0.5;
|
const push = (minDist - d) * 0.5;
|
||||||
const nx = dx / d, nz = dz / d;
|
const nx = dx / d, nz = dz / d;
|
||||||
seeds[a][0] = Math.max(-9, Math.min(9, seeds[a][0] - nx * push));
|
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[a][1] = Math.max(-8.5, Math.min(-1.5, seeds[a][1] - nz * push));
|
||||||
seeds[b][0] = Math.max(-9, Math.min(9, seeds[b][0] + nx * 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));
|
seeds[b][1] = Math.max(-8.5, Math.min(-1.5, seeds[b][1] + nz * push));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const rxClamped = seeds[i][0];
|
const g = pieces[i].group;
|
||||||
const rzClamped = seeds[i][1];
|
g.position.set(seeds[i][0], -1.4, seeds[i][1]);
|
||||||
const ry = -1.15 + 0.15 + Math.sin(i * 5.5) * 0.25;
|
g.rotation.y = rng() * Math.PI * 2;
|
||||||
|
g.userData.baseY = -1.4;
|
||||||
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.popGroup.add(g);
|
||||||
}
|
}
|
||||||
instance.builtKey = popKey;
|
instance.builtKey = popKey;
|
||||||
instance.popCount = count;
|
instance.popCount = count;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Analytic animation — bob + slow yaw, no integration
|
// Furniture stands still — it is junk, not actors. The room's life is
|
||||||
|
// the camera walk, the story-driven emissive breathe below, and the
|
||||||
|
// re-hang on arrangement jumps. Positions are never integrated, only
|
||||||
|
// set at build: a seek lands on the same room as playback.
|
||||||
for (const g of instance.popGroup.children) {
|
for (const g of instance.popGroup.children) {
|
||||||
const idx = g.userData.index || 0;
|
const idx = g.userData.index || 0;
|
||||||
const baseY = g.userData.baseY || 0;
|
// Per-section popMul gates how much of the room is dressed — V1
|
||||||
g.position.y = baseY + Math.sin(t * 0.35 + idx * 1.1) * 0.06;
|
// keeps the gating the memories had.
|
||||||
g.rotation.y += 0; // keep deterministic: set rather than accumulate
|
{ const popMul = vState ? vState.popMul : 1; const showIndex = idx / Math.max(1, instance.popCount); g.visible = opacity > 0.01 && (showIndex < (0.35 + popMul * 0.75)); }
|
||||||
const yaw = shapeTilt + idx * 0.9 + Math.sin(t * 0.18 + idx) * 0.28;
|
g.traverse((mesh) => {
|
||||||
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) {
|
if (mesh.isMesh) {
|
||||||
mesh.material.opacity = opacity;
|
mesh.material.opacity = opacity;
|
||||||
mesh.material.transparent = opacity < 0.999;
|
mesh.material.transparent = opacity < 0.999;
|
||||||
@@ -482,7 +607,7 @@ export const chamber = {
|
|||||||
mesh.material.emissiveIntensity = 0.5 + vState.light * 1.0;
|
mesh.material.emissiveIntensity = 0.5 + vState.light * 1.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subtle room drift from camera trait
|
// Subtle room drift from camera trait
|
||||||
|
|||||||
Reference in New Issue
Block a user