Erste lauffähige Fassung: 3D-Raum, Echtzeit-Multiplayer (WebSocket), Python-IDE via Pyodide im Browser, eigener Arbeitsplatz mit Live-Code-Monitor. Modularer, self-hostbarer Aufbau (Three.js + Node/Express + ws), ohne CDN- Abhängigkeit zur Laufzeit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
129 lines
5.6 KiB
JavaScript
129 lines
5.6 KiB
JavaScript
// 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) };
|
|
}
|