feat(3d): procedural cyclorama / infinity cove (new — not in WowPlatter)
The hip-hop room's north section is a real cyclorama (white curved infinity wall). Added a data-driven cove: virtual_space gains cyclorama (comma list of north/south/east/west) + cyclorama_radius + cyclorama_color. buildCyclorama() sweeps a profile per wall — [flat wall down to R] → [quarter-circle fillet radius R] → [white floor lead-in] — so the floor curves up into the wall with no hard seam (the seamless studio look). Cyclorama walls skip their flat wall (the cove replaces it). Set on hiphop-room (space 3): cyclorama=north, R=0.9. v1 = single-wall cove (back curve); 3-sided + rounded vertical corners + partial-wall extent for the L-shape = next iteration, all tunable via the columns. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
205967d506
commit
5551963ef4
@ -119,6 +119,10 @@ _STARTUP_DDL = [
|
||||
"median_au numeric(10,2), discogs_seller_count int, discogs_lowest numeric(10,2), "
|
||||
"already_stocked boolean DEFAULT false, updated_at timestamptz NOT NULL DEFAULT now(), "
|
||||
"PRIMARY KEY (store_id, release_id))",
|
||||
# 3D store: cyclorama / infinity-cove walls (comma list of north/south/east/west) + fillet radius
|
||||
"ALTER TABLE virtual_space ADD COLUMN IF NOT EXISTS cyclorama text",
|
||||
"ALTER TABLE virtual_space ADD COLUMN IF NOT EXISTS cyclorama_radius numeric",
|
||||
"ALTER TABLE virtual_space ADD COLUMN IF NOT EXISTS cyclorama_color text",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -123,6 +123,36 @@ function slopedSide(x, ch, cd, hf, th, m) {
|
||||
geom.setIndex([0, 1, 2, 2, 1, 3]); geom.computeVertexNormals();
|
||||
return new THREE.Mesh(geom, m);
|
||||
}
|
||||
// Cyclorama / infinity cove — the floor curves up into the named wall(s) via a fillet of radius R,
|
||||
// with a white floor lead-in, so there's no hard floor↔wall seam (the seamless studio look). Each
|
||||
// wall gets one swept profile: [wall down to R] → [quarter-circle fillet] → [flat floor lead-in].
|
||||
function buildCyclorama(walls, R, lead, W, D, H, mat) {
|
||||
const prof = [];
|
||||
for (let i = 0; i <= 6; i++) prof.push([0, H - (H - R) * i / 6]); // wall: u=0, v H→R
|
||||
for (let i = 1; i <= 12; i++) { const t = Math.PI + (Math.PI / 2) * i / 12; prof.push([R + R * Math.cos(t), R + R * Math.sin(t)]); } // fillet R→0
|
||||
for (let i = 1; i <= 3; i++) prof.push([R + lead * i / 3, 0]); // floor lead-in
|
||||
prof.forEach(p => { p[1] += 0.004; }); // lift a hair off the floor (no z-fight)
|
||||
const P = prof.length, grp = new THREE.Group();
|
||||
const maps = {
|
||||
north: { len: W, f: (s, u, v) => [s, v, -D / 2 + u] },
|
||||
south: { len: W, f: (s, u, v) => [s, v, D / 2 - u] },
|
||||
west: { len: D, f: (s, u, v) => [-W / 2 + u, v, s] },
|
||||
east: { len: D, f: (s, u, v) => [W / 2 - u, v, s] },
|
||||
};
|
||||
walls.forEach(wall => {
|
||||
const mp = maps[wall]; if (!mp) return;
|
||||
const pos = [];
|
||||
[-mp.len / 2, mp.len / 2].forEach(s => prof.forEach(([u, v]) => { const p = mp.f(s, u, v); pos.push(p[0], p[1], p[2]); }));
|
||||
const idx = [];
|
||||
for (let p = 0; p < P - 1; p++) idx.push(p, P + p, p + 1, p + 1, P + p, P + p + 1);
|
||||
const geo = new THREE.BufferGeometry();
|
||||
geo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(pos), 3));
|
||||
geo.setIndex(idx); geo.computeVertexNormals();
|
||||
grp.add(new THREE.Mesh(geo, mat));
|
||||
});
|
||||
return grp;
|
||||
}
|
||||
|
||||
// a decal's mesh (image plane or canvas-text plane) — shared by wall, crate-logo + rack decals.
|
||||
function makeDecalMesh(dec) {
|
||||
const w = num(dec.width, 0.2), h = num(dec.height, 0.2), op = num(dec.opacity, 1);
|
||||
@ -212,12 +242,21 @@ function buildScene(data) {
|
||||
const ceil = new THREE.Mesh(new THREE.PlaneGeometry(W, D), material(sp.ceiling_color, '#1a1a1f'));
|
||||
ceil.rotation.x = Math.PI / 2; ceil.position.y = H; roomGroup.add(ceil);
|
||||
const wallMat = material(sp.wall_color, '#3a3a42');
|
||||
const cyc = String(sp.cyclorama || '').toLowerCase().split(',').map(s => s.trim()).filter(Boolean);
|
||||
const wallNames = ['north', 'south', 'west', 'east'];
|
||||
const walls = [[0, -D / 2, 0], [0, D / 2, Math.PI], [-W / 2, 0, Math.PI / 2], [W / 2, 0, -Math.PI / 2]];
|
||||
walls.forEach(([x, z, ry], i) => {
|
||||
if (cyc.includes(wallNames[i])) return; // a cyclorama wall is replaced by the curved cove
|
||||
const w = (i < 2) ? W : D;
|
||||
const m = new THREE.Mesh(new THREE.PlaneGeometry(w, H), wallMat.clone());
|
||||
m.position.set(x, H / 2, z); m.rotation.y = ry; roomGroup.add(m);
|
||||
});
|
||||
if (cyc.length) {
|
||||
const R = num(sp.cyclorama_radius, 0.9);
|
||||
const cmat = material(sp.cyclorama_color || '#ffffff', '#ffffff');
|
||||
cmat.side = THREE.DoubleSide; cmat.roughness = 0.92; cmat.metalness = 0;
|
||||
roomGroup.add(buildCyclorama(cyc, R, Math.max(R + 0.3, 1.2), W, D, H, cmat));
|
||||
}
|
||||
|
||||
// lights (data-driven + a soft ambient so it's never pitch black)
|
||||
roomGroup.add(new THREE.AmbientLight(0xffffff, num(sp.ambient_light_intensity, 0.6)));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user