🧊Three.js ausführlich erklärt
params, die Codeansicht aktualisiert sich mit.1.🎬Scene · Camera · Renderer
Jede three.js-Anwendung braucht drei Dinge: eine Scene (der Behälter für alles Sichtbare), eine Camera (von wo und wie wird geschaut) und einen Renderer (zeichnet die Szene aus Sicht der Kamera auf ein <canvas>).
Ein Mesh ist das sichtbare Objekt: Geometry (Form: Vertices, Normalen, UVs) + Material (Aussehen: wie reagiert die Oberfläche auf Licht).
- PerspectiveCamera(fov, aspect, near, far) – fov ist der vertikale Bildwinkel in Grad.
- renderer.render(scene, camera) zeichnet genau ein Bild.
- Ohne Licht sind MeshStandardMaterial-Objekte schwarz – daher das DirectionalLight.
1import * as THREE from "three";23const params = { fov: 50, z: 4, color: "#14b8a6" }; // ← die Regler ändern genau dieses Objekt45// 1. Renderer: zeichnet auf das vorhandene <canvas>6const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });7renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);89// 2. Szene: Behälter für Objekte und Lichter10const scene = new THREE.Scene();11scene.background = new THREE.Color("#0f172a");1213// 3. Kamera: Bildwinkel, Seitenverhältnis, near/far14const camera = new THREE.PerspectiveCamera(params.fov, canvas.clientWidth / canvas.clientHeight, 0.1, 100);1516// 4. Mesh = Geometry + Material17const geometry = new THREE.BoxGeometry(1, 1, 1);18const material = new THREE.MeshStandardMaterial({ color: params.color });19const cube = new THREE.Mesh(geometry, material);20cube.rotation.set(0.5, 0.7, 0);21scene.add(cube);2223// 5. Licht, sonst bleibt ein Standard-Material schwarz24const light = new THREE.DirectionalLight(0xffffff, 3);25light.position.set(2, 3, 4);26scene.add(light, new THREE.AmbientLight(0xffffff, 0.3));2728function update() {29 camera.fov = params.fov;30 camera.position.z = params.z;31 camera.updateProjectionMatrix(); // nach Änderung von fov/aspect/near/far Pflicht!32 material.color.set(params.color);33 renderer.render(scene, camera); // ein einziges Bild zeichnen34}35update();3637return { update, dispose: () => { geometry.dispose(); material.dispose(); renderer.dispose(); } };
2.🎨Materialtypen
Das Material entscheidet, welcher Shader läuft. three.js bringt eine Reihe fertiger Materialien mit – von „ignoriert Licht“ bis „physikalisch korrekt mit Klarlack“.
Je realistischer, desto teurer pro Pixel: Basic < Lambert < Phong < Standard < Physical.
- MeshBasicMaterial: unbeleuchtet, nur Farbe/Textur.
- MeshLambertMaterial: nur diffus (Helligkeit pro Pixel nach Lambert).
- MeshPhongMaterial: diffus + Glanzlicht (Blinn-Phong), shininess.
- MeshStandardMaterial: PBR mit roughness/metalness – das Material, das der GLTFLoader erzeugt.
- MeshPhysicalMaterial: Standard + clearcoat, transmission, sheen, iridescence …
1import * as THREE from "three";23const params = { type: "Standard", roughness: 0.35, metalness: 0.2, clearcoat: 0, wireframe: false }; // ← die Regler ändern genau dieses Objekt45const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });6renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);7const scene = new THREE.Scene();8scene.background = new THREE.Color("#0f172a");9const camera = new THREE.PerspectiveCamera(40, canvas.clientWidth / canvas.clientHeight, 0.1, 100);10camera.position.set(0, 0.6, 4.2);11camera.lookAt(0, 0, 0);1213const light = new THREE.DirectionalLight(0xffffff, 3);14light.position.set(3, 4, 5);15scene.add(light, new THREE.AmbientLight(0x8899bb, 0.4));1617const geometry = new THREE.TorusKnotGeometry(0.8, 0.28, 160, 24);18const mesh = new THREE.Mesh(geometry);19scene.add(mesh);2021function makeMaterial() {22 const color = 0x14b8a6;23 switch (params.type) {24 case "Basic": return new THREE.MeshBasicMaterial({ color });25 case "Lambert": return new THREE.MeshLambertMaterial({ color });26 case "Phong": return new THREE.MeshPhongMaterial({ color, shininess: 80 });27 case "Standard": return new THREE.MeshStandardMaterial({ color, roughness: params.roughness, metalness: params.metalness });28 default: return new THREE.MeshPhysicalMaterial({ color, roughness: params.roughness, metalness: params.metalness, clearcoat: params.clearcoat });29 }30}3132function update() {33 mesh.material.dispose?.(); // altes Material freigeben (Shader-Programm bleibt im Cache)34 mesh.material = makeMaterial();35 mesh.material.wireframe = params.wireframe;36}37update();3839renderer.setAnimationLoop((time) => {40 mesh.rotation.y = time / 2000;41 renderer.render(scene, camera);42 // jedes neue Materialtyp-Paar kompiliert ein eigenes Shader-Programm43 report({ Material: mesh.material.type, "Shader-Programme": renderer.info.programs.length });44});4546return { update, dispose: () => { renderer.setAnimationLoop(null); geometry.dispose(); mesh.material.dispose(); renderer.dispose(); } };
3.💡Lichter und Schatten
Lichtquellen sind Objekte in der Szene. AmbientLight hellt alles gleichmäßig auf, HemisphereLight mischt Himmel- und Bodenfarbe, DirectionalLight ist wie die Sonne (parallele Strahlen), PointLight strahlt in alle Richtungen, SpotLight als Kegel.
Schatten kosten einen zusätzlichen Render-Durchlauf pro Licht: Die Szene wird aus Sicht des Lichts in eine Shadow-Map (Tiefentextur) gezeichnet.
- Drei Schalter nötig: renderer.shadowMap.enabled, light.castShadow, mesh.castShadow/receiveShadow.
- shadow.mapSize bestimmt die Schärfe (und den Speicher).
- Schau auf die Draw Calls: Mit Schatten verdoppeln sie sich fast.
1import * as THREE from "three";23const params = { light: "Directional", intensity: 3, angle: 40, shadows: true, mapSize: "512" }; // ← die Regler ändern genau dieses Objekt45const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });6renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);7renderer.shadowMap.enabled = true; // Schalter 18const scene = new THREE.Scene();9scene.background = new THREE.Color("#0f172a");10const camera = new THREE.PerspectiveCamera(45, canvas.clientWidth / canvas.clientHeight, 0.1, 100);11camera.position.set(0, 3.2, 5.5);12camera.lookAt(0, 0, 0);1314const floor = new THREE.Mesh(new THREE.PlaneGeometry(8, 8), new THREE.MeshStandardMaterial({ color: 0xe2e8f0 }));15floor.rotation.x = -Math.PI / 2;16floor.receiveShadow = true; // Schalter 3a17const ball = new THREE.Mesh(new THREE.SphereGeometry(0.6, 48, 24), new THREE.MeshStandardMaterial({ color: 0x8b5cf6, roughness: 0.4 }));18ball.position.set(-0.7, 0.6, 0);19const box = new THREE.Mesh(new THREE.BoxGeometry(0.9, 1.4, 0.9), new THREE.MeshStandardMaterial({ color: 0x14b8a6 }));20box.position.set(0.9, 0.7, -0.3);21for (const m of [ball, box]) { m.castShadow = true; m.receiveShadow = true; } // Schalter 3b22scene.add(floor, ball, box, new THREE.AmbientLight(0xffffff, 0.15));2324let light;25function update() {26 if (light) { scene.remove(light); light.dispose(); }27 if (params.light === "Directional") light = new THREE.DirectionalLight(0xffffff, params.intensity);28 if (params.light === "Point") light = new THREE.PointLight(0xffffff, params.intensity * 4, 0, 2);29 if (params.light === "Spot") light = new THREE.SpotLight(0xffffff, params.intensity * 6, 0, Math.PI / 6, 0.3, 2);30 if (params.light === "Hemisphere") light = new THREE.HemisphereLight(0xbcd3e8, 0x3b3a35, params.intensity);31 const a = params.angle * Math.PI / 180;32 light.position.set(Math.cos(a) * 3, 3.5, Math.sin(a) * 3);33 if (light.shadow) {34 light.castShadow = params.shadows; // Schalter 2 (HemisphereLight kann keine Schatten)35 light.shadow.mapSize.set(Number(params.mapSize), Number(params.mapSize));36 }37 scene.add(light);38}39update();4041renderer.setAnimationLoop(() => {42 renderer.render(scene, camera);43 report({ "Draw Calls": renderer.info.render.calls, Licht: light.type, "wirft Schatten": light.castShadow === true });44});4546return { update, dispose: () => { renderer.setAnimationLoop(null); scene.traverse((o) => { o.geometry?.dispose(); o.material?.dispose(); }); light.dispose(); renderer.dispose(); } };
4.🌳Object3D und der Szenengraph
Alles in einer Szene ist ein Object3D: Meshes, Lichter, Kameras, Gruppen. Jedes hat position, rotation (Euler-Winkel in Radiant), scale und Kinder (add).
Die lokale Matrix beschreibt die Lage relativ zum Elternobjekt; die Weltmatrix (matrixWorld) ergibt sich als Produkt aller Eltern. Dreht sich die Erde um die Sonne, nimmt sie ihren Mond automatisch mit.
- Group ist ein leeres Object3D – ideal als Drehpunkt.
- getWorldPosition() liefert die Position in Weltkoordinaten.
- three.js aktualisiert matrixWorld vor jedem render() automatisch.
1import * as THREE from "three";23const params = { orbitSpeed: 0.6, moonSpeed: 2.5, earthScale: 1 }; // ← die Regler ändern genau dieses Objekt45const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });6renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);7const scene = new THREE.Scene();8scene.background = new THREE.Color("#0f172a");9const camera = new THREE.PerspectiveCamera(45, canvas.clientWidth / canvas.clientHeight, 0.1, 100);10camera.position.set(0, 6, 7);11camera.lookAt(0, 0, 0);12scene.add(new THREE.PointLight(0xfff2cc, 60, 0, 2), new THREE.AmbientLight(0xffffff, 0.2));1314const sphere = new THREE.SphereGeometry(1, 32, 16);15const sun = new THREE.Mesh(sphere, new THREE.MeshBasicMaterial({ color: 0xfbbf24 }));16const earthOrbit = new THREE.Group(); // Drehpunkt in der Sonne17const earth = new THREE.Mesh(sphere, new THREE.MeshStandardMaterial({ color: 0x0ea5e9 }));18earth.position.x = 3.2; // lokal: 3,2 von der Sonne entfernt19earth.scale.setScalar(0.45);20const moonOrbit = new THREE.Group();21earth.add(moonOrbit); // Mond-Drehpunkt hängt an der Erde22const moon = new THREE.Mesh(sphere, new THREE.MeshStandardMaterial({ color: 0xcbd5e1 }));23moon.position.x = 2; // lokal – in Erd-Einheiten!24moon.scale.setScalar(0.3);25moonOrbit.add(moon);26earthOrbit.add(earth);27scene.add(sun, earthOrbit);2829const timer = new THREE.Timer();30const p = new THREE.Vector3();31renderer.setAnimationLoop((time) => {32 timer.update(time);33 const dt = timer.getDelta();34 earthOrbit.rotation.y += params.orbitSpeed * dt;35 moonOrbit.rotation.y += params.moonSpeed * dt;36 earth.scale.setScalar(0.45 * params.earthScale); // skaliert auch die Mondbahn mit!37 renderer.render(scene, camera);38 moon.getWorldPosition(p);39 report({ "moon.position (lokal)": moon.position.toArray().map((v) => v.toFixed(2)).join("; "), "Mond in Weltkoordinaten": p.toArray().map((v) => v.toFixed(2)).join("; ") });40});4142return { dispose: () => { renderer.setAnimationLoop(null); timer.dispose(); sphere.dispose(); scene.traverse((o) => o.material?.dispose()); renderer.dispose(); } };
5.🔁Render-Loop mit Timer und delta
Für Bewegung wird die Szene immer wieder neu gezeichnet – synchron zur Bildwiederholrate des Monitors. renderer.setAnimationLoop(cb) ist der three.js-Weg für requestAnimationFrame (und nötig für WebXR).
Bildraten schwanken (60, 120, 144 Hz …). Wer pro Bild einen festen Winkel dreht, dreht auf schnellen Monitoren schneller. Richtig ist: Geschwindigkeit × delta (Sekunden seit dem letzten Bild).
- THREE.Timer ersetzt seit r183 die veraltete THREE.Clock.
- timer.connect(document) verhindert riesige delta-Werte nach einem Tabwechsel.
- Links: pro Bild +0,02 rad (bildratenabhängig). Rechts: 1,2 rad/s × delta.
1import * as THREE from "three";23const params = { speed: 1.2, fpsLimit: 60 }; // ← die Regler ändern genau dieses Objekt45const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });6renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);7const scene = new THREE.Scene();8scene.background = new THREE.Color("#0f172a");9const camera = new THREE.PerspectiveCamera(40, canvas.clientWidth / canvas.clientHeight, 0.1, 100);10camera.position.z = 6;11scene.add(new THREE.HemisphereLight(0xffffff, 0x334155, 3));12const geo = new THREE.BoxGeometry(1.3, 1.3, 1.3);13const perFrame = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ color: 0xf43f5e }));14const perSecond = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ color: 0x14b8a6 }));15perFrame.position.x = -1.4;16perSecond.position.x = 1.4;17scene.add(perFrame, perSecond);1819const timer = new THREE.Timer();20timer.connect(document); // Page Visibility API21let last = 0;22renderer.setAnimationLoop((time) => {23 if (time - last < 1000 / params.fpsLimit) return; // Bildrate künstlich drosseln24 last = time;25 timer.update(time);26 const delta = timer.getDelta(); // Sekunden seit dem letzten Bild27 perFrame.rotation.y += 0.02; // ❌ hängt von der Bildrate ab28 perSecond.rotation.y += params.speed * delta; // ✅ rad/s × s = rad29 renderer.render(scene, camera);30 report({ "delta (s)": delta.toFixed(4), "Bilder/s": (1 / Math.max(delta, 1e-6)).toFixed(0), "elapsed (s)": timer.getElapsed().toFixed(1) });31});3233return { dispose: () => { renderer.setAnimationLoop(null); timer.dispose(); geo.dispose(); perFrame.material.dispose(); perSecond.material.dispose(); renderer.dispose(); } };
6.📥Modelle laden: GLTFLoader
Modelle baut man nicht im Code, man lädt sie. Der GLTFLoader liest .gltf/.glb, erzeugt daraus Meshes, Materialien, Kameras, Animationen und liefert gltf.scene – eine Group, die man in die eigene Szene hängt.
Für große Dateien gibt es Kompression: DRACOLoader (Geometrie, KHR_draco_mesh_compression) und KTX2Loader (GPU-komprimierte Texturen, KHR_texture_basisu). Beide werden dem GLTFLoader nur „eingesteckt“; das Apartment braucht keins von beiden.
- loadAsync(url, onProgress) liefert ein Promise – ideal mit await.
- gltf.scene.traverse() besucht jeden Knoten – zum Zählen, Schatten einschalten, Materialien anpassen.
- Box3().setFromObject() misst das Modell, um die Kamera passend zu setzen.
1import * as THREE from "three";2import { OrbitControls } from "three/addons/controls/OrbitControls.js";3import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";45const params = { wireframe: false }; // ← die Regler ändern genau dieses Objekt67const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });8renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);9const scene = new THREE.Scene();10scene.background = new THREE.Color("#0f172a");11scene.add(new THREE.HemisphereLight(0xbcd3e8, 0x3b3a35, 2.5));12const camera = new THREE.PerspectiveCamera(35, canvas.clientWidth / canvas.clientHeight, 0.1, 2000);13const controls = new OrbitControls(camera, canvas);1415const loader = new GLTFLoader();16// Nur falls das Modell Draco-komprimiert wäre:17// const draco = new DRACOLoader(); draco.setDecoderPath("/draco/"); loader.setDRACOLoader(draco);18const gltf = await loader.loadAsync("/modelle/apartment.glb", (e) => {19 report({ geladen: Math.round(e.loaded / 1024) + " KiB" });20});21const model = gltf.scene;22scene.add(model);2324let meshes = 0, triangles = 0;25const materials = new Set();26model.traverse((o) => {27 if (!o.isMesh) return;28 meshes++;29 triangles += o.geometry.index.count / 3;30 materials.add(o.material);31});3233// Kamera an die Größe des Gebäudes (ohne Gelände) anpassen34const box = new THREE.Box3();35for (const name of ["EG_Waende", "OG_Waende"]) box.expandByObject(model.getObjectByName(name));36const center = box.getCenter(new THREE.Vector3());37const size = box.getSize(new THREE.Vector3()).length();38camera.position.copy(center).add(new THREE.Vector3(0.6, 0.7, 0.8).multiplyScalar(size));39controls.target.copy(center);4041function update() {42 for (const m of materials) m.wireframe = params.wireframe;43}44update();45report({ Meshes: meshes, Dreiecke: triangles, Materialien: materials.size, Animationen: gltf.animations.length });4647renderer.setAnimationLoop(() => { controls.update(); renderer.render(scene, camera); });4849return { update, dispose: () => {50 renderer.setAnimationLoop(null);51 controls.dispose();52 model.traverse((o) => { o.geometry?.dispose(); o.material?.dispose(); });53 renderer.dispose();54} };
7.🕹️OrbitControls
Kamera-Steuerungen sind Add-ons, die Maus- und Touch-Ereignisse in Kamerabewegungen übersetzen. OrbitControls kreist um einen Zielpunkt (controls.target): ziehen = drehen, Mausrad = zoomen, rechte Taste = verschieben.
Mit Damping gleitet die Kamera weich aus – dann muss controls.update() in jedem Bild aufgerufen werden.
- maxPolarAngle verhindert den Blick unter den Boden.
- minDistance/maxDistance begrenzen den Zoom.
- Andere Steuerungen: TrackballControls, FlyControls, PointerLockControls, MapControls.
1import * as THREE from "three";2import { OrbitControls } from "three/addons/controls/OrbitControls.js";34const params = { damping: true, autoRotate: false, maxPolar: 89, minDistance: 3 }; // ← die Regler ändern genau dieses Objekt56const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });7renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);8const scene = new THREE.Scene();9scene.background = new THREE.Color("#0f172a");10const camera = new THREE.PerspectiveCamera(45, canvas.clientWidth / canvas.clientHeight, 0.1, 100);11camera.position.set(4, 3, 6);12scene.add(new THREE.GridHelper(10, 10, 0x14b8a6, 0x334155));13scene.add(new THREE.HemisphereLight(0xffffff, 0x334155, 3));14const house = new THREE.Mesh(new THREE.BoxGeometry(2, 1.5, 1.5), new THREE.MeshStandardMaterial({ color: 0xf1f5f9 }));15house.position.y = 0.75;16const roof = new THREE.Mesh(new THREE.ConeGeometry(1.6, 1, 4), new THREE.MeshStandardMaterial({ color: 0xf43f5e }));17roof.position.y = 2;18roof.rotation.y = Math.PI / 4;19scene.add(house, roof);2021const controls = new OrbitControls(camera, canvas);22controls.target.set(0, 1, 0);2324function update() {25 controls.enableDamping = params.damping;26 controls.autoRotate = params.autoRotate;27 controls.maxPolarAngle = params.maxPolar * Math.PI / 180;28 controls.minDistance = params.minDistance;29}30update();3132renderer.setAnimationLoop(() => {33 controls.update(); // Pflicht bei Damping und autoRotate34 renderer.render(scene, camera);35 report({ Abstand: camera.position.distanceTo(controls.target).toFixed(2), "Polarwinkel": (controls.getPolarAngle() * 180 / Math.PI).toFixed(1) + "°" });36});3738return { update, dispose: () => { renderer.setAnimationLoop(null); controls.dispose(); scene.traverse((o) => { o.geometry?.dispose(); o.material?.dispose(); }); renderer.dispose(); } };
8.🎯Raycaster: Objekte anklicken
Welches Objekt liegt unter dem Mauszeiger? Der Raycaster schießt einen Strahl von der Kamera durch den Pixel in die Szene und liefert alle getroffenen Objekte, sortiert nach Entfernung.
Dafür wird die Mausposition in NDC umgerechnet (−1 … +1) – dieselben Koordinaten wie in der Rendering-Pipeline, nur rückwärts.
- setFromCamera(ndc, camera) baut den Strahl.
- intersectObjects(liste) testet zuerst Bounding-Spheres, dann Dreiecke.
- Ergebnis: object, point (Weltkoordinaten), distance, face, faceIndex.
1import * as THREE from "three";23const params = { hover: true }; // ← die Regler ändern genau dieses Objekt45const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });6renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);7const scene = new THREE.Scene();8scene.background = new THREE.Color("#0f172a");9const camera = new THREE.PerspectiveCamera(45, canvas.clientWidth / canvas.clientHeight, 0.1, 100);10camera.position.set(0, 4, 7);11camera.lookAt(0, 0, 0);12scene.add(new THREE.HemisphereLight(0xffffff, 0x334155, 3));1314const geo = new THREE.BoxGeometry(0.8, 0.8, 0.8);15const boxes = [];16for (let i = 0; i < 16; i++) {17 const box = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ color: 0x475569 }));18 box.position.set((i % 4) * 1.3 - 1.95, 0, Math.floor(i / 4) * 1.3 - 1.95);19 box.name = "Kiste " + (i + 1);20 boxes.push(box);21 scene.add(box);22}2324const raycaster = new THREE.Raycaster();25const ndc = new THREE.Vector2();26let selected = null;27function pick(event, click) {28 if (!click && !params.hover) return;29 const r = canvas.getBoundingClientRect();30 ndc.x = ((event.clientX - r.left) / r.width) * 2 - 1; // Pixel → NDC31 ndc.y = -((event.clientY - r.top) / r.height) * 2 + 1;32 raycaster.setFromCamera(ndc, camera);33 const hit = raycaster.intersectObjects(boxes)[0];34 if (selected) selected.material.color.set(0x475569);35 selected = hit ? hit.object : null;36 if (selected) selected.material.color.set(0x14b8a6);37 report({ NDC: ndc.x.toFixed(2) + "; " + ndc.y.toFixed(2), Treffer: hit ? hit.object.name : "–", Abstand: hit ? hit.distance.toFixed(2) : "–" });38}39const onMove = (e) => pick(e, false);40const onClick = (e) => pick(e, true);41canvas.addEventListener("pointermove", onMove);42canvas.addEventListener("click", onClick);4344renderer.setAnimationLoop(() => renderer.render(scene, camera));4546return { dispose: () => {47 renderer.setAnimationLoop(null);48 canvas.removeEventListener("pointermove", onMove);49 canvas.removeEventListener("click", onClick);50 geo.dispose();51 boxes.forEach((b) => b.material.dispose());52 renderer.dispose();53} };
9.📐Resize und devicePixelRatio
Ein Canvas hat zwei Größen: die CSS-Größe (wie groß er auf der Seite erscheint) und die Zeichenpuffer-Größe (wie viele Pixel gerendert werden). Auf Retina-Displays ist devicePixelRatio 2 oder 3 – ohne Anpassung wirkt alles unscharf.
Bei jeder Größenänderung müssen Renderer-Größe und camera.aspect nachgezogen werden – sonst wird das Bild verzerrt.
- renderer.setPixelRatio(Math.min(devicePixelRatio, 2)) – mehr als 2 kostet viel und bringt wenig.
- ResizeObserver reagiert auch auf Layoutänderungen, nicht nur auf das Fenster.
- Stell den Regler auf 0,25: Pixel werden sichtbar, die GPU hat 1/16 der Arbeit.
1import * as THREE from "three";23const params = { pixelRatio: 1, fixAspect: true }; // ← die Regler ändern genau dieses Objekt45const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });6const scene = new THREE.Scene();7scene.background = new THREE.Color("#0f172a");8const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100); // aspect 1 – wird gleich korrigiert9camera.position.z = 4;10const knot = new THREE.Mesh(new THREE.TorusKnotGeometry(0.9, 0.3, 200, 32), new THREE.MeshNormalMaterial());11scene.add(knot);1213function resize() {14 const w = canvas.clientWidth, h = canvas.clientHeight; // CSS-Pixel15 renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2) * params.pixelRatio);16 renderer.setSize(w, h, false); // false: CSS-Größe nicht anfassen17 camera.aspect = params.fixAspect ? w / h : 1;18 camera.updateProjectionMatrix();19 report({ "CSS-Größe": w + " × " + h, Zeichenpuffer: canvas.width + " × " + canvas.height, devicePixelRatio: window.devicePixelRatio });20}21const observer = new ResizeObserver(resize);22observer.observe(canvas);23resize();2425renderer.setAnimationLoop((time) => {26 knot.rotation.set(time / 3000, time / 2000, 0);27 renderer.render(scene, camera);28});2930return { update: resize, dispose: () => { renderer.setAnimationLoop(null); observer.disconnect(); knot.geometry.dispose(); knot.material.dispose(); renderer.dispose(); } };
10.🧹Speicher freigeben: dispose()
JavaScript räumt Objekte selbst auf – GPU-Ressourcen aber nicht. Geometrien (Vertex-Puffer), Materialien (Shader-Programme) und Texturen belegen Grafikspeicher, bis man dispose() aufruft. scene.remove(mesh) allein genügt nicht.
renderer.info.memory zählt, wie viele Geometrien und Texturen gerade auf der GPU liegen – ein einfacher Leck-Detektor.
- geometry.dispose(), material.dispose(), texture.dispose(), renderer.dispose().
- Bei React/Next.js: im Cleanup von useEffect aufräumen (sonst Lecks bei jedem Seitenwechsel).
- Probier: dispose aus, dann mehrfach „neu erzeugen“ → der Zähler wächst.
1import * as THREE from "three";23const params = { count: 100, callDispose: true, generation: 0 }; // ← die Regler ändern genau dieses Objekt45const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });6renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);7const scene = new THREE.Scene();8scene.background = new THREE.Color("#0f172a");9const camera = new THREE.PerspectiveCamera(50, canvas.clientWidth / canvas.clientHeight, 0.1, 100);10camera.position.set(0, 0, 9);11scene.add(new THREE.HemisphereLight(0xffffff, 0x334155, 3));12const group = new THREE.Group();13scene.add(group);1415function update() {16 for (const m of [...group.children]) {17 group.remove(m); // nur aus der Szene entfernt …18 if (params.callDispose) {19 m.geometry.dispose(); // … erst das gibt den GPU-Puffer frei20 m.material.dispose();21 }22 }23 for (let i = 0; i < params.count; i++) {24 // absichtlich je Kugel eine eigene Geometrie (sonst würde three.js sie teilen)25 const m = new THREE.Mesh(new THREE.SphereGeometry(0.18, 16, 8), new THREE.MeshStandardMaterial({ color: new THREE.Color().setHSL(i / params.count, 0.6, 0.55) }));26 m.position.set(Math.random() * 8 - 4, Math.random() * 5 - 2.5, Math.random() * 4 - 2);27 group.add(m);28 }29}30update();3132renderer.setAnimationLoop((time) => {33 group.rotation.y = time / 4000;34 renderer.render(scene, camera);35 report({ Durchgang: params.generation, "Geometrien auf der GPU": renderer.info.memory.geometries, "Objekte in der Szene": group.children.length, "Draw Calls": renderer.info.render.calls });36});3738return { update, dispose: () => {39 renderer.setAnimationLoop(null);40 group.traverse((o) => { o.geometry?.dispose(); o.material?.dispose(); });41 renderer.dispose();42} };
11.🚀Performance: InstancedMesh und Draw Calls
Tausend gleiche Objekte als tausend Meshes bedeuten tausend Draw Calls – die CPU wird zum Flaschenhals, lange bevor die GPU ausgelastet ist.
InstancedMesh zeichnet dieselbe Geometrie mit demselben Material beliebig oft in einem Draw Call; jede Instanz bekommt nur ihre eigene Matrix (und optional Farbe).
- setMatrixAt(i, matrix) + instanceMatrix.needsUpdate = true.
- Weitere Hebel: BatchedMesh, mergeGeometries, LOD, Frustum-Culling.
- Achte auf die FPS bei 5000 Objekten – einzeln vs. instanziert.
1import * as THREE from "three";23const params = { count: 2000, instanced: true }; // ← die Regler ändern genau dieses Objekt45const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });6renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);7const scene = new THREE.Scene();8scene.background = new THREE.Color("#0f172a");9const camera = new THREE.PerspectiveCamera(50, canvas.clientWidth / canvas.clientHeight, 0.1, 200);10camera.position.set(0, 0, 38);11scene.add(new THREE.HemisphereLight(0xffffff, 0x334155, 3));12const geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);13const material = new THREE.MeshStandardMaterial({ color: 0xffffff });14const root = new THREE.Group();15scene.add(root);16const matrix = new THREE.Matrix4(), color = new THREE.Color();1718function position(i) { // Punkte auf einer Kugelschale (Fibonacci)19 const y = 1 - (i / (params.count - 1)) * 2, r = Math.sqrt(1 - y * y), phi = i * 2.39996;20 return new THREE.Vector3(Math.cos(phi) * r, y, Math.sin(phi) * r).multiplyScalar(14);21}2223function update() {24 for (const o of root.children) { // Vorgänger freigeben (siehe Lektion dispose)25 if (o.isInstancedMesh) o.dispose();26 else o.material.dispose();27 }28 root.clear();29 if (params.instanced) {30 const mesh = new THREE.InstancedMesh(geometry, material, params.count);31 for (let i = 0; i < params.count; i++) {32 matrix.setPosition(position(i));33 mesh.setMatrixAt(i, matrix); // nur eine Matrix pro Instanz34 mesh.setColorAt(i, color.setHSL(i / params.count, 0.7, 0.55));35 }36 mesh.instanceMatrix.needsUpdate = true;37 root.add(mesh);38 } else {39 for (let i = 0; i < params.count; i++) {40 const m = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color: color.setHSL(i / params.count, 0.7, 0.55) }));41 m.position.copy(position(i));42 root.add(m); // jedes Mesh = eigener Draw Call43 }44 }45}46update();4748const timer = new THREE.Timer();49let frames = 0, acc = 0, fps = 0;50renderer.setAnimationLoop((time) => {51 timer.update(time);52 const dt = timer.getDelta();53 root.rotation.y += dt * 0.3;54 renderer.render(scene, camera);55 frames++; acc += dt;56 if (acc > 0.5) { fps = frames / acc; frames = 0; acc = 0; }57 report({ "Draw Calls": renderer.info.render.calls, Dreiecke: renderer.info.render.triangles, FPS: fps.toFixed(0) });58});5960return { update, dispose: () => {61 renderer.setAnimationLoop(null);62 timer.dispose();63 root.traverse((o) => { if (o.material && o.material !== material) o.material.dispose(); });64 geometry.dispose(); material.dispose(); renderer.dispose();65} };
🏖️ So ist der Apartment-Viewer aufgebaut
Der Viewer auf der Seite Ferienwohnung ist nichts anderes als die Summe der Lektionen oben – angelehnt an den Viewer der Original-3D-Ansicht. Quelltext: src/components/ApartmentViewer.tsx, geladen pernext/dynamic mit ssr: false.
| Baustein | Verwendung im Viewer | Lektion |
|---|---|---|
| WebGLRenderer | antialias, setPixelRatio(min(dpr, 2)), ACESFilmicToneMapping, shadowMap (PCF) | 🎬 Scene · Camera · Renderer |
| Scene + PerspectiveCamera | Hintergrund, fov aus der Blender-Brennweite, near 0,1 / far 2000 | 🎬 Scene · Camera · Renderer |
| RoomEnvironment + PMREMGenerator | vorgefilterte Umgebung für PBR-Reflexionen (scene.environment) | 🎨 Materialtypen |
| HemisphereLight + DirectionalLight | Himmel/Boden + Sonne aus Nordosten, Shadow-Map 2048² | 💡 Lichter und Schatten |
| GLTFLoader | lädt /modelle/apartment.glb, Fortschritt aus onProgress | 📥 Modelle laden: GLTFLoader |
| traverse() | Schatten an, Materialien einseitig, Meshes und Originalmaterialien merken | 📥 Modelle laden: GLTFLoader |
| Gruppenknoten | 9 Knoten aus apartment.json: ein/aus, Explosion = position der OG-Knoten | 🌳 Object3D und der Szenengraph |
| OrbitControls | Damping, maxPolarAngle 0,495·π, Zoom 2 … 220 m | 🕹️ OrbitControls |
| Timer + requestAnimationFrame | Kamerafahrten mit delta, Easing über 1,1 s | 🔁 Render-Loop mit Timer und delta |
| Raycaster | Klick → Mesh, BoxHelper, Infos zum Bauteil | 🎯 Raycaster: Objekte anklicken |
| ResizeObserver | Renderer-Größe und camera.aspect nachführen | 📐 Resize und devicePixelRatio |
| useEffect-Cleanup | dispose() für Geometrien, Materialien, Umgebung, Renderer | 🧹 Speicher freigeben: dispose() |
| renderer.info | Draw Calls, Dreiecke, Geometrien, Programme live | 🚀 Performance: InstancedMesh und Draw Calls |
Seite /ferienwohnung (Server) ApartmentViewer (nur Browser)
+-----------------------------+ +----------------------------------------------+
| Kennzahlen aus apartment.glb| | WebGLRenderer -> Scene -+- HemisphereLight |
| (beim Build geparst) | dynamic | +- DirectionalLight |
| ApartmentViewerLoader ------┼--------->| PerspectiveCamera +- gltf.scene |
| ssr: false | import | ^ OrbitControls | +- EG_Boeden … |
+-----------------------------+ | | Timer (Kamerafahrt) | +- Aussenanlagen |
| Raycaster -> BoxHelper +- Helfer (Boxen, |
| renderer.info -> React-State Normalen) |
+----------------------------------------------+🏖️ Zum Apartment-Viewer