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), "
|
"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(), "
|
"already_stocked boolean DEFAULT false, updated_at timestamptz NOT NULL DEFAULT now(), "
|
||||||
"PRIMARY KEY (store_id, release_id))",
|
"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();
|
geom.setIndex([0, 1, 2, 2, 1, 3]); geom.computeVertexNormals();
|
||||||
return new THREE.Mesh(geom, m);
|
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.
|
// a decal's mesh (image plane or canvas-text plane) — shared by wall, crate-logo + rack decals.
|
||||||
function makeDecalMesh(dec) {
|
function makeDecalMesh(dec) {
|
||||||
const w = num(dec.width, 0.2), h = num(dec.height, 0.2), op = num(dec.opacity, 1);
|
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'));
|
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);
|
ceil.rotation.x = Math.PI / 2; ceil.position.y = H; roomGroup.add(ceil);
|
||||||
const wallMat = material(sp.wall_color, '#3a3a42');
|
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]];
|
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) => {
|
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 w = (i < 2) ? W : D;
|
||||||
const m = new THREE.Mesh(new THREE.PlaneGeometry(w, H), wallMat.clone());
|
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);
|
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)
|
// 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)));
|
roomGroup.add(new THREE.AmbientLight(0xffffff, num(sp.ambient_light_intensity, 0.6)));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user