// scene.js — Aufbau der statischen 3D-Welt (Raum, Licht, Arbeitsplätze). import * as THREE from 'three'; /** * Erstellt Szene, Kamera und Player-Rig samt Raum-Geometrie. * @returns {{scene: THREE.Scene, camera: THREE.PerspectiveCamera, player: THREE.Group, monMat: THREE.Material}} */ export function createWorld() { const scene = new THREE.Scene(); scene.background = new THREE.Color(0x0b1020); scene.fog = new THREE.Fog(0x0b1020, 22, 65); const camera = new THREE.PerspectiveCamera(72, innerWidth / innerHeight, 0.1, 200); // Player-Rig: dieses bewegen wir (VR-tauglich). Kamera sitzt drin auf Augenhöhe. const player = new THREE.Group(); player.position.set(0, 0, 8); camera.position.set(0, 1.7, 0); player.add(camera); scene.add(player); addLights(scene); addRoom(scene); const monMat = addWorkstations(scene); return { scene, camera, player, monMat }; } function addLights(scene) { scene.add(new THREE.HemisphereLight(0x9fb4ff, 0x20263f, 1.0)); const key = new THREE.DirectionalLight(0xffffff, 1.1); key.position.set(8, 14, 6); key.castShadow = true; key.shadow.mapSize.set(1024, 1024); key.shadow.camera.near = 1; key.shadow.camera.far = 60; key.shadow.camera.left = -25; key.shadow.camera.right = 25; key.shadow.camera.top = 25; key.shadow.camera.bottom = -25; scene.add(key); } function addRoom(scene) { const floor = new THREE.Mesh( new THREE.PlaneGeometry(60, 60), new THREE.MeshStandardMaterial({ color: 0x141a30, roughness: 0.95 }) ); floor.rotation.x = -Math.PI / 2; floor.receiveShadow = true; scene.add(floor); const grid = new THREE.GridHelper(60, 60, 0x3a4470, 0x232a47); grid.position.y = 0.01; scene.add(grid); const wallMat = new THREE.MeshStandardMaterial({ color: 0x1a2140, roughness: 1, side: THREE.DoubleSide }); const wall = (w, h, x, y, z, ry) => { const m = new THREE.Mesh(new THREE.PlaneGeometry(w, h), wallMat); m.position.set(x, y, z); m.rotation.y = ry; scene.add(m); }; wall(60, 8, 0, 4, -30, 0); wall(60, 8, 0, 4, 30, Math.PI); wall(60, 8, -30, 4, 0, Math.PI / 2); wall(60, 8, 30, 4, 0, -Math.PI / 2); } /** Arbeitsplätze (Tisch + Monitor) als InstancedMesh = wenige Draw-Calls. */ function addWorkstations(scene) { const COLS = 6, ROWS = 4, GAP = 6, N = COLS * ROWS; const desks = new THREE.InstancedMesh( new THREE.BoxGeometry(2.2, 0.15, 1.2), new THREE.MeshStandardMaterial({ color: 0x2a3358, roughness: 0.8 }), N); const monMat = new THREE.MeshStandardMaterial({ color: 0x0e1428, emissive: 0x2b6cff, emissiveIntensity: 0.6, roughness: 0.4 }); const monitors = new THREE.InstancedMesh(new THREE.BoxGeometry(1.4, 0.85, 0.06), monMat, N); desks.castShadow = desks.receiveShadow = monitors.castShadow = true; const m = new THREE.Object3D(); const startX = -((COLS - 1) * GAP) / 2, startZ = -((ROWS - 1) * GAP) / 2; let i = 0; for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) { const x = startX + c * GAP, z = startZ + r * GAP; m.position.set(x, 0.9, z); m.rotation.set(0, 0, 0); m.updateMatrix(); desks.setMatrixAt(i, m.matrix); m.position.set(x, 1.5, z - 0.5); m.updateMatrix(); monitors.setMatrixAt(i, m.matrix); i++; } scene.add(desks, monitors); return monMat; } /** * Baut einen konkreten Arbeitsplatz (Tisch, Monitor mit Rahmen/Ständer, Stuhl) und * bettet die übergebene Bildschirmfläche als echten Monitor ein. * Lokales Koordinatensystem: Bildschirm zeigt nach +z, der Sitzplatz liegt bei +z davor. * @param {THREE.Mesh} screenMesh - die CodeScreen-Fläche (wird als Kind eingehängt) * @param {number} screenWidth - Breite der Fläche in Metern (16:10-Verhältnis) * @returns {{ group: THREE.Group, seat: THREE.Vector3 }} seat = Sitzposition (lokal) */ export function createWorkstation(screenMesh, screenWidth) { const g = new THREE.Group(); const wood = new THREE.MeshStandardMaterial({ color: 0x2a3358, roughness: 0.85 }); const dark = new THREE.MeshStandardMaterial({ color: 0x0a0e1a, roughness: 0.5 }); const metal = new THREE.MeshStandardMaterial({ color: 0x1a2140, roughness: 0.6, metalness: 0.3 }); const chairMat = new THREE.MeshStandardMaterial({ color: 0x394260, roughness: 0.7 }); // Tischplatte + Beine const desk = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.08, 0.8), wood); desk.position.set(0, 0.74, 0.55); desk.castShadow = desk.receiveShadow = true; g.add(desk); const legGeo = new THREE.BoxGeometry(0.07, 0.74, 0.07); for (const [x, z] of [[-0.82, 0.25], [0.82, 0.25], [-0.82, 0.85], [0.82, 0.85]]) { const leg = new THREE.Mesh(legGeo, metal); leg.position.set(x, 0.37, z); g.add(leg); } // Monitorfuß + Hals const base = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.03, 0.22), dark); base.position.set(0, 0.80, 0.35); g.add(base); const neck = new THREE.Mesh(new THREE.BoxGeometry(0.07, 0.42, 0.07), dark); neck.position.set(0, 1.0, 0.35); g.add(neck); // Rahmen (Bezel) hinter dem Bildschirm const sh = screenWidth * 640 / 1024; const bezel = new THREE.Mesh(new THREE.BoxGeometry(screenWidth + 0.1, sh + 0.1, 0.06), dark); bezel.position.set(0, 1.35, 0.30); bezel.castShadow = true; g.add(bezel); // Bildschirm: knapp vor dem Bezel, zeigt nach +z (zum Sitzplatz) screenMesh.position.set(0, 1.35, 0.335); g.add(screenMesh); // Einfacher Stuhl am Sitzplatz const seatPad = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.08, 0.5), chairMat); seatPad.position.set(0, 0.5, 1.5); seatPad.castShadow = true; g.add(seatPad); const backRest = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.6, 0.08), chairMat); backRest.position.set(0, 0.8, 1.75); backRest.castShadow = true; g.add(backRest); return { group: g, seat: new THREE.Vector3(0, 0, 1.5) }; }