metalness: 0.05, ...opts }); return cache[key]; } function box(w, h, d, mat, x, y, z) { const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), mat); m.position.set(x, y, z); m.castShadow = true; m.receiveShadow = true; scene.add(m); return m; } function cyl(r, h, mat, x, y, z) { const m = new THREE.Mesh(new THREE.CylinderGeometry(r, r, h, 20), mat); m.position.set(x, y, z); m.castShadow = true; scene.add(m); return m; } // ---------- Материалы ---------- const woodTop = M('#7a4a22'); const legMat = M('#2a2a2a', { metalness: 0.4 }); const chairDark = M('#2f2f32'); const chairOrange = M('#e0662b'); const sofaVelvet = M('#4a2a22', { roughness: 0.9 }); const cushionGreen = M('#1f6b3a'); const kidsSofaMat = M('#ffb347'); const stageMat = M('#c9c2b8'); const danceMat = M('#e8c9a0'); const lampBlack = M('#111111', { roughness: 0.9 }); const lampWhite = M('#f5f5f0'); // ---------- Пол, стены, окна ---------- const floor = new THREE.Mesh(new THREE.PlaneGeometry(26, 18), M('#a0683a', { roughness: 0.8 })); floor.rotation.x = -Math.PI / 2; floor.receiveShadow = true; scene.add(floor); const winMesh = new THREE.Mesh(new THREE.BoxGeometry(26, 4, 0.2), M('#9fd3ff', { transparent: true, opacity: 0.35 })); winMesh.position.set(0, 2, -9.1); scene.add(winMesh); box(0.3, 4, 18, M('#3a2f2a'), -13.1, 2, 0); box(0.3, 4, 18, M('#3a2f2a'), 13.1, 2, 0); // ---------- Стулья и столы ---------- function chairAt(x, z, mat, rot) { const g = new THREE.Group(); const seat = new THREE.Mesh(new THREE.BoxGeometry(0.45, 0.08, 0.45), mat); seat.position.y = 0.45; const back = new THREE.Mesh(new THREE.BoxGeometry(0.45, 0.5, 0.06), mat); back.position.set(0, 0.72, -0.2); g.add(seat, back); g.position.set(x, 0, z); g.rotation.y = rot; g.traverse(o => { if (o.isMesh) o.castShadow = true; }); scene.add(g); } // стол вдоль оси X (dirX=true) или Z; n стульев с каждой стороны function table(cx, cz, len, dirX, n) { const w = dirX ? len : 0.9, d = dirX ? 0.9 : len; box(w, 0.08, d, woodTop, cx, 0.75, cz); cyl(0.12, 0.7, legMat, cx, 0.37, cz); const step = len / n; for (let i = 0; i < n; i++) { const p = -len / 2 + step * (i + 0.5); const mat = i % 2 ? chairOrange : chairDark; if (dirX) { chairAt(cx + p, cz - 0.7, mat, 0); chairAt(cx + p, cz + 0.7, mat, Math.PI); } else { chairAt(cx - 0.7, cz + p, mat, -Math.PI / 2); chairAt(cx + 0.7, cz + p, mat, Math.PI / 2); } } } // Ряд у окна: 3 стола по 10 table(-4, -6.5, 3.2, true, 5); table(0, -6.5, 3.2, true, 5); table(4, -6.5, 3.2, true, 5); // Длинный ряд: 20 мест (10 с каждой стороны) table(0, -3, 10, true, 10); // Голубая зона у фотозоны: стол на 10 table(-9.5, 0, 4.5, false, 5); // Нижние ряды: 4 стола по 12 (6 с каждой стороны) table(-4, 1.5, 4.5, true, 6); table(3.5, 1.5, 4.5, true, 6); table(-4, 4.5, 4.5, true, 6); table(3.5, 4.5, 4.5, true, 6); // ---------- Диваны детской зоны (12 детей) ---------- for (const z of [-7.5, -5, -2.5]) { box(1.0, 0.45, 2.3, kidsSofaMat, 11.2, 0.25, z); box(0.3, 0.8, 2.3, kidsSofaMat, 11.9, 0.6, z); } // Игровая / детская комната const play = new THREE.Mesh(new THREE.PlaneGeometry(3, 2.5), M('#2d6b2d')); play.rotation.x = -Math.PI / 2; play.position.set(11, 0.02, -0.2); scene.add(play); box(1.2, 0.5, 0.9, M('#e8e0d0'), 11, 0.3, 0.8); // ---------- Сцены и танцпол ---------- box(14, 0.5, 1.2, stageMat, 1, 0.25, 9); // сцена внизу box(2, 0.5, 6, stageMat, 10.5, 0.25, 5.5); // сцена справа const dance = new THREE.Mesh(new THREE.CircleGeometry(1, 48), danceMat); dance.rotation.x = -Math.PI / 2; dance.scale.set(4, 1.5, 1); dance.position.set(0, 0.02, 6.8); dance.receiveShadow = true; scene.add(dance); // ---------- Фотозона ---------- const photoZone = new THREE.Mesh(new THREE.PlaneGeometry(4, 4), M('#fff3d6')); photoZone.rotation.x = -Math.PI / 2; photoZone.position.set(-10, 0.02, -6); scene.add(photoZone); for (const [x, z] of [[-11.5, -7.5], [-8.5, -7.5], [-11.5, -4.5]]) { cyl(0.35, 0.8, lampBlack, x, 0.4, z); const leaf = new THREE.Mesh(new THREE.SphereGeometry(0.5, 16, 12), M('#2d7a3a')); leaf.position.set(x, 1.1, z); scene.add(leaf); } // Фиолетовый декор у окон box(8, 0.1, 0.3, M('#b56cd9'), 0, 0.1, -8.5); // ---------- Подвесные светильники ---------- for (const [x, z, mat] of [[0, -3, lampBlack], [-4, 1.5, lampWhite], [3.5, 4.5, lampBlack], [-9.5, 0, lampWhite], [4, -6.5, lampBlack], [-4, -6.5, lampWhite]]) { const sph = new THREE.Mesh(new THREE.SphereGeometry(0.35, 20, 16), mat); sph.position.set(x, 3.2, z); scene.add(sph); } // ---------- Подписи зон (HTML поверх 3D) ---------- const labelEls = []; function addLabel(text, x, y, z) { const el = document.createElement('div'); el.textContent = text; el.style.cssText = 'position:absolute;color:#fff;background:rgba(0,0,0,.55);padding:3px 7px;border-radius:6px;font-size:12px;transform:translate(-50%,-50%);pointer-events:none;white-space:nowrap;z-index:1;'; document.body.appendChild(el); labelEls.push({ el, p: new THREE.Vector3(x, y, z) }); } addLabel('ФОТОЗОНА', -10, 1.6, -6); addLabel('30 мест (3 стола × 10)', 0, 1.6, -9.2); addLabel('20 мест (4 стола сдвинуты)', 0, 1.6, -1.2); addLabel('стол на 10', -9.5, 1.6, -2.8); addLabel('каждый стол — 12 мест', 0, 1.6, 3); addLabel('ТАНЦПОЛ', 0, 1.6, 6.8); addLabel('СЦЕНА', 1, 1.2, 10); addLabel('ДЕТСКАЯ — 12 мест', 11, 1.8, -5); addLabel('СЦЕНА', 10.5, 1.2, 8.5); function updateLabels() { const w = window.innerWidth, h = window.innerHeight; for (const L of labelEls) { const v = L.p.clone().project(camera); L.el.style.left = ((v.x + 1) / 2 * w) + 'px'; L.el.style.top = ((-v.y + 1) / 2 * h) + 'px'; L.el.style.display = v.z > 1 ? 'none' : 'block'; } } // ---------- Ракурсы ---------- const views = { top: [[0, 30, 0.01], [0, 0, 0]], general: [[-16, 14, 18], [0, 0, 0]], stage: [[0, 3, 14], [0, 1, -4]], photo: [[-11, 2.5, -7.5], [3, 1, 2]], kids: [[9, 2.5, -1], [-3, 1, -3]], window: [[0, 2.5, -8], [0, 1, 6]], closeup: [[1, 2.5, 9], [1, 0.8, 4.5]] }; let anim = null; window.view = (k) => { const v = views[k]; anim = { from: camera.position.clone(), to: new THREE.Vector3(...v[0]), tf: controls.target.clone(), tt: new THREE.Vector3(...v[1]), t: 0 }; }; // ---------- Цикл отрисовки ---------- function tick() { requestAnimationFrame(tick); if (anim) { anim.t = Math.min(1, anim.t + 0.025); const e = anim.t < 0.5 ? 2 * anim.t * anim.t : 1 - Math.pow(-2 * anim.t + 2, 2) / 2; camera.position.lerpVectors(anim.from, anim.to, e); controls.target.lerpVectors(anim.tf, anim.tt, e); if (anim.t >= 1) anim = null; } controls.update(); renderer.render(scene, camera); updateLabels(); } window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }); tick();
Made on
Tilda