3D-Politur: ACES-Tonemapping, PMREM-Environment, Standard-Materialien

Viewport3D nutzt jetzt ACESFilmicToneMapping + sRGB-Ausgabe (weiche
Lichter statt hartem Clipping) und eine neutrale Raum-Umgebung
(PMREMGenerator + RoomEnvironment) als IBL-Quelle für plausible
Reflexionen. Wände/Decken/Öffnungsrahmen/Glas/Türblatt/Treppen und das
Auswahl-Highlight laufen von MeshLambertMaterial auf MeshStandardMaterial
um (Rauigkeit/Metallizität je Bauteilart, moderate envMapIntensity);
direktes Licht entsprechend zurückgenommen, da die Umgebung nun mit-
trägt. Hintergrundfarbe, Hidden-Line- und Clay-Modus unverändert.
This commit is contained in:
2026-07-02 21:04:22 +02:00
parent 45f9d0c381
commit 0189eed5ac
+67 -18
View File
@@ -7,6 +7,7 @@ import type { MutableRefObject } from "react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import * as THREE from "three"; import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js"; import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { RoomEnvironment } from "three/addons/environments/RoomEnvironment.js";
import type { import type {
Ceiling, Ceiling,
ContextObject, ContextObject,
@@ -382,8 +383,25 @@ export function Viewport3D({
return; return;
} }
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// Filmisches Tonemapping + sRGB-Ausgabe für einen fotorealistischeren,
// nicht ausgefressenen Kontrast (ACES komprimiert Lichter sanft, statt sie
// hart zu clippen). Belichtung leicht über 1, damit die PBR-Materialien
// (s. u.) nicht zu dunkel wirken.
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.1;
renderer.outputColorSpace = THREE.SRGBColorSpace;
mount.appendChild(renderer.domElement); mount.appendChild(renderer.domElement);
// Neutrale Raum-Umgebung (PMREMGenerator + RoomEnvironment) als IBL-Quelle:
// gibt MeshStandardMaterial weiche, plausible Reflexionen/Ambient-Licht,
// ohne eine externe HDR-Datei laden zu müssen. Nur `scene.environment`
// gesetzt (NICHT `scene.background`) — der helle Papier-Hintergrund bleibt
// wie bisher erhalten.
const pmremGenerator = new THREE.PMREMGenerator(renderer);
const envRenderTarget = pmremGenerator.fromScene(new RoomEnvironment(), 0.04);
scene.environment = envRenderTarget.texture;
pmremGenerator.dispose();
// OrbitControls startet an der Perspektivkamera; bei Preset-Wechsel wird // OrbitControls startet an der Perspektivkamera; bei Preset-Wechsel wird
// `.object` auf die aktive Kamera umgehängt (siehe applyView3d). Aktive // `.object` auf die aktive Kamera umgehängt (siehe applyView3d). Aktive
// Kamera initial = Perspektive (wird vom Initial-Preset ggf. überschrieben). // Kamera initial = Perspektive (wird vom Initial-Preset ggf. überschrieben).
@@ -412,9 +430,11 @@ export function Viewport3D({
// Szenen-Neuaufbau konsistent, da die Controls hier frisch entstehen. // Szenen-Neuaufbau konsistent, da die Controls hier frisch entstehen.
controls.enableRotate = view3dRef.current !== "top"; controls.enableRotate = view3dRef.current !== "top";
// Licht // Licht — mit der Raum-Umgebung (s. o.) als weichem IBL-Ambient reichen
scene.add(new THREE.AmbientLight(0xffffff, 0.6)); // gedämpftes Umgebungslicht + eine gerichtete „Sonne" für Modellierung
const sun = new THREE.DirectionalLight(0xffffff, 1.1); // (Schlagschatten sind hier bewusst NICHT aktiv, wie zuvor).
scene.add(new THREE.AmbientLight(0xffffff, 0.35));
const sun = new THREE.DirectionalLight(0xffffff, 0.9);
sun.position.set(6, 12, 4); sun.position.set(6, 12, 4);
scene.add(sun); scene.add(sun);
@@ -428,8 +448,8 @@ export function Viewport3D({
// gestapelt; je Geschoss nur Wände mit sichtbarer Kategorie (categoryDisplay). // gestapelt; je Geschoss nur Wände mit sichtbarer Kategorie (categoryDisplay).
// Normale und gedimmte Materialien getrennt cachen. // Normale und gedimmte Materialien getrennt cachen.
const mats: MatCaches = { const mats: MatCaches = {
normal: new Map<string, THREE.MeshLambertMaterial>(), normal: new Map<string, THREE.MeshStandardMaterial>(),
greyed: new Map<string, THREE.MeshLambertMaterial>(), greyed: new Map<string, THREE.MeshStandardMaterial>(),
lines: new Map<string, THREE.LineBasicMaterial>(), lines: new Map<string, THREE.LineBasicMaterial>(),
}; };
// Geteilte Materialien für den „Verdeckte Kanten"-Modus: flache weiße Faces // Geteilte Materialien für den „Verdeckte Kanten"-Modus: flache weiße Faces
@@ -446,6 +466,7 @@ export function Viewport3D({
color: 0xe8e8e8, color: 0xe8e8e8,
roughness: 0.85, roughness: 0.85,
metalness: 0, metalness: 0,
envMapIntensity: 0.6,
}); });
// Laufzeit für PBR-Texturen (Render-Modus „textured"). Lädt Karten lazy + // Laufzeit für PBR-Texturen (Render-Modus „textured"). Lädt Karten lazy +
// gecacht; im Cleanup werden Texturen/Materialien freigegeben. // gecacht; im Cleanup werden Texturen/Materialien freigegeben.
@@ -481,6 +502,7 @@ export function Viewport3D({
side: THREE.DoubleSide, side: THREE.DoubleSide,
wireframe: CONTEXT_WIREFRAME, wireframe: CONTEXT_WIREFRAME,
flatShading: true, flatShading: true,
envMapIntensity: 0.5,
}), }),
mesh: new THREE.MeshStandardMaterial({ mesh: new THREE.MeshStandardMaterial({
color: 0x9fa6ae, // neutrales Hellgrau für importierte Meshes color: 0x9fa6ae, // neutrales Hellgrau für importierte Meshes
@@ -488,6 +510,7 @@ export function Viewport3D({
metalness: 0, metalness: 0,
side: THREE.DoubleSide, side: THREE.DoubleSide,
wireframe: CONTEXT_WIREFRAME, wireframe: CONTEXT_WIREFRAME,
envMapIntensity: 0.5,
}), }),
contour: new THREE.LineBasicMaterial({ color: 0x6b7a5a }), contour: new THREE.LineBasicMaterial({ color: 0x6b7a5a }),
}; };
@@ -874,10 +897,12 @@ export function Viewport3D({
// Auswahl-Hervorhebung: die Meshes der gewählten Wand mit einem geteilten // Auswahl-Hervorhebung: die Meshes der gewählten Wand mit einem geteilten
// Highlight-Material überziehen; Originale merken und beim Wechsel/Cleanup // Highlight-Material überziehen; Originale merken und beim Wechsel/Cleanup
// wiederherstellen. Material wird im Cleanup verworfen. // wiederherstellen. Material wird im Cleanup verworfen.
const highlightMat = new THREE.MeshLambertMaterial({ const highlightMat = new THREE.MeshStandardMaterial({
color: 0x5fa896, color: 0x5fa896,
emissive: 0x1a655a, emissive: 0x1a655a,
emissiveIntensity: 0.6, emissiveIntensity: 0.6,
roughness: 0.6,
metalness: 0,
transparent: true, transparent: true,
opacity: 0.92, opacity: 0.92,
}); });
@@ -1367,6 +1392,7 @@ export function Viewport3D({
activeCameraRef.current = null; activeCameraRef.current = null;
boundsRef.current = null; boundsRef.current = null;
renderer.dispose(); renderer.dispose();
envRenderTarget.dispose();
for (const m of mats.normal.values()) m.dispose(); for (const m of mats.normal.values()) m.dispose();
for (const m of mats.greyed.values()) m.dispose(); for (const m of mats.greyed.values()) m.dispose();
for (const m of mats.lines.values()) m.dispose(); for (const m of mats.lines.values()) m.dispose();
@@ -1481,8 +1507,8 @@ export function Viewport3D({
/** Getrennte Material-Caches: normal und gedimmt (je Component-ID). */ /** Getrennte Material-Caches: normal und gedimmt (je Component-ID). */
interface MatCaches { interface MatCaches {
normal: Map<string, THREE.MeshLambertMaterial>; normal: Map<string, THREE.MeshStandardMaterial>;
greyed: Map<string, THREE.MeshLambertMaterial>; greyed: Map<string, THREE.MeshStandardMaterial>;
/** Linien-Materialien der 2D-Zeichengeometrie (Schlüssel: `farbe|greyed`). */ /** Linien-Materialien der 2D-Zeichengeometrie (Schlüssel: `farbe|greyed`). */
lines: Map<string, THREE.LineBasicMaterial>; lines: Map<string, THREE.LineBasicMaterial>;
} }
@@ -2102,8 +2128,11 @@ function addOpeningMeshes(
}; };
// Rahmen-Material (dunkelgrau) + Glas-Material (bläulich, transparent). // Rahmen-Material (dunkelgrau) + Glas-Material (bläulich, transparent).
const frameMat = new THREE.MeshLambertMaterial({ const frameMat = new THREE.MeshStandardMaterial({
color: greyed ? 0x9aa0a6 : 0x55606c, color: greyed ? 0x9aa0a6 : 0x55606c,
roughness: 0.55,
metalness: 0.15,
envMapIntensity: 0.7,
transparent: greyed, transparent: greyed,
opacity, opacity,
}); });
@@ -2117,8 +2146,11 @@ function addOpeningMeshes(
if (o.kind === "window") { if (o.kind === "window") {
// Glasscheibe in der Wandmitte, zwischen den Rahmenprofilen. // Glasscheibe in der Wandmitte, zwischen den Rahmenprofilen.
const glassMat = new THREE.MeshLambertMaterial({ const glassMat = new THREE.MeshStandardMaterial({
color: 0x9fc4e8, color: 0x9fc4e8,
roughness: 0.08,
metalness: 0,
envMapIntensity: 1,
transparent: true, transparent: true,
opacity: greyed ? 0.18 : 0.35, opacity: greyed ? 0.18 : 0.35,
depthWrite: false, depthWrite: false,
@@ -2140,8 +2172,11 @@ function addOpeningMeshes(
// Boden (zBottom) bis zTop; sein Grundriss-Verlauf folgt der doorSymbol-Achse. // Boden (zBottom) bis zTop; sein Grundriss-Verlauf folgt der doorSymbol-Achse.
const sym = doorSymbol(wall, o); const sym = doorSymbol(wall, o);
if (!sym) return; if (!sym) return;
const leafMat = new THREE.MeshLambertMaterial({ const leafMat = new THREE.MeshStandardMaterial({
color: greyed ? 0xb0b4b8 : 0x8a6f57, color: greyed ? 0xb0b4b8 : 0x8a6f57,
roughness: 0.75,
metalness: 0,
envMapIntensity: 0.6,
transparent: greyed, transparent: greyed,
opacity, opacity,
}); });
@@ -2169,16 +2204,18 @@ function addOpeningMeshes(
} }
/** /**
* MeshLambertMaterial pro Component-ID gecached (3D-Farbe = component.color). * MeshStandardMaterial pro Component-ID gecached (3D-Farbe = component.color).
* Gedimmte Variante: Bauteilfarbe Richtung Grau gemischt + transparent, damit * Rauigkeit hoch (mattes Putz-/Wandbild), keine Metallizität — passend für
* gedimmte Geschosse/Kategorien als „andere" sichtbar, aber zurückgenommen sind. * Wände/Decken. Gedimmte Variante: Bauteilfarbe Richtung Grau gemischt +
* transparent, damit gedimmte Geschosse/Kategorien als „andere" sichtbar,
* aber zurückgenommen sind.
*/ */
function layerMaterial( function layerMaterial(
project: Project, project: Project,
componentId: string, componentId: string,
greyed: boolean, greyed: boolean,
opts: BuildOpts, opts: BuildOpts,
): THREE.MeshLambertMaterial { ): THREE.MeshStandardMaterial {
const { mats } = opts; const { mats } = opts;
// Drahtgitter: Component-Material mit wireframe=true. Wird mitgecacht; da der // Drahtgitter: Component-Material mit wireframe=true. Wird mitgecacht; da der
// Render-Modus den gesamten Szenen-Neuaufbau auslöst, ist der Cache pro Modus // Render-Modus den gesamten Szenen-Neuaufbau auslöst, ist der Cache pro Modus
@@ -2191,15 +2228,24 @@ function layerMaterial(
if (greyed) { if (greyed) {
// Zu mittlerem Grau hin mischen (entsättigen) und transparent machen. // Zu mittlerem Grau hin mischen (entsättigen) und transparent machen.
color.lerp(new THREE.Color(0x9aa0a6), 0.7); color.lerp(new THREE.Color(0x9aa0a6), 0.7);
mat = new THREE.MeshLambertMaterial({ mat = new THREE.MeshStandardMaterial({
color, color,
wireframe, wireframe,
roughness: 0.9,
metalness: 0,
envMapIntensity: 0.6,
transparent: true, transparent: true,
opacity: 0.35, opacity: 0.35,
depthWrite: false, depthWrite: false,
}); });
} else { } else {
mat = new THREE.MeshLambertMaterial({ color, wireframe }); mat = new THREE.MeshStandardMaterial({
color,
wireframe,
roughness: 0.9,
metalness: 0,
envMapIntensity: 0.6,
});
} }
cache.set(componentId, mat); cache.set(componentId, mat);
} }
@@ -2373,8 +2419,11 @@ function addStairMeshes(
? opts.hiddenFace ? opts.hiddenFace
: opts.renderMode === "white" : opts.renderMode === "white"
? opts.whiteFace ? opts.whiteFace
: new THREE.MeshLambertMaterial({ : new THREE.MeshStandardMaterial({
color: greyed ? 0xb8bcc2 : 0x9aa0a6, color: greyed ? 0xb8bcc2 : 0x9aa0a6,
roughness: 0.85,
metalness: 0,
envMapIntensity: 0.6,
transparent: greyed, transparent: greyed,
opacity: greyed ? 0.35 : 1, opacity: greyed ? 0.35 : 1,
}); });