diff --git a/README.md b/README.md index cc48ebf..f0c769c 100644 --- a/README.md +++ b/README.md @@ -221,7 +221,7 @@ Was die fotorealistische Variante anders macht: | Bereich | Umsetzung | | --- | --- | -| Materialien | Texturen doppelt so fein (1024 px), dazu Rauheits- und Verdeckungskarten aus dem Höhenbild (`textures.ts`) | +| Materialien | **Fotografierte Materialien von ambientCG (CC0)** für Travertin, Pflaster, Kies, Sand, Erde, Rasen, Holzschalung, Beton und Keramik – Liste in `public/materialien/QUELLEN.md`, rund 11 MB, erst beim Umschalten geladen (`fotomaterial.ts`). Plattenbeläge werden aus Fotoausschnitten zusammengesetzt, jede Platte mit eigenem Ausschnitt und echten Fugen. Was kein Foto hat, wird doppelt so fein gezeichnet | | Laub | Blattkarten statt Laubkörpern: Blätter, Eibennadeln, Ligusterblätter als Textur mit Alpha-Test, als InstancedMesh. Die Lichtnormalen zeigen vom Kronenmittelpunkt weg – so wird eine Krone als Volumen beleuchtet (`laub.ts`) | | Bäume | Rekursiv gewachsene, gekrümmte Äste; Blattwolken an den Zweigspitzen | | Rasen | Grasnarbe aus Büscheln einzelner Halme, die im Wind wiegen (`gras.ts`) | diff --git a/public/materialien/QUELLEN.md b/public/materialien/QUELLEN.md new file mode 100644 index 0000000..5ef340b --- /dev/null +++ b/public/materialien/QUELLEN.md @@ -0,0 +1,24 @@ +# Fotomaterialien + +Alle Materialien stammen von **ambientCG** (https://ambientcg.com) und stehen +unter der Lizenz **CC0 1.0** (gemeinfrei, keine Namensnennung nötig). Sie +wurden als 1K- bzw. 2K-JPG heruntergeladen und für das Web neu komprimiert +(Farbe 86 %, Normalen 90 %, Rauheit/Verdeckung 82 %). + +| Ordner | ambientCG-Satz | Auflösung | Verwendung | +| --- | --- | --- | --- | +| travertin | Travertine009 | 2K | Travertinplatten 80×80, Stellplatten, lose Platten, Stufen, Stelen, Mauern | +| tegula | PavingStones111 | 1K | Tegula-Pflastersteine grau | +| verbundstein | PavingStones099 | 1K | Verbundsteine | +| kies | Gravel023 | 1K | Dekorkies, Spritzschutz | +| schotter | Gravel036L | 1K | Schotterwiese | +| sand | Ground091 | 1K | Spielsand | +| erde | Ground048 | 1K | Pflanzbeete | +| rasen | Grass004 | 1K | Rasen, Wiese | +| holz | Planks001 | 1K | Holzschalung der Fassade, Gartenschrank | +| beton | Concrete034 | 1K | Winkelelemente, Betonmauer, Sockel | +| keramik | Concrete017 | 1K | Keramikplatten 60×60 (gedeckter Sitzplatz) | + +Plattenbeläge werden im Browser aus Ausschnitten dieser Bilder zusammengesetzt – +jede Platte bekommt einen eigenen Ausschnitt, dazu Fugen und Fasen +(`fotoPlatten` in `src/scene/textures.ts`). diff --git a/public/materialien/beton/color.jpg b/public/materialien/beton/color.jpg new file mode 100644 index 0000000..4cc2b56 Binary files /dev/null and b/public/materialien/beton/color.jpg differ diff --git a/public/materialien/beton/normal.jpg b/public/materialien/beton/normal.jpg new file mode 100644 index 0000000..9902685 Binary files /dev/null and b/public/materialien/beton/normal.jpg differ diff --git a/public/materialien/beton/rough.jpg b/public/materialien/beton/rough.jpg new file mode 100644 index 0000000..b24084f Binary files /dev/null and b/public/materialien/beton/rough.jpg differ diff --git a/public/materialien/erde/ao.jpg b/public/materialien/erde/ao.jpg new file mode 100644 index 0000000..2e0c7d3 Binary files /dev/null and b/public/materialien/erde/ao.jpg differ diff --git a/public/materialien/erde/color.jpg b/public/materialien/erde/color.jpg new file mode 100644 index 0000000..496fcb2 Binary files /dev/null and b/public/materialien/erde/color.jpg differ diff --git a/public/materialien/erde/normal.jpg b/public/materialien/erde/normal.jpg new file mode 100644 index 0000000..a75d699 Binary files /dev/null and b/public/materialien/erde/normal.jpg differ diff --git a/public/materialien/erde/rough.jpg b/public/materialien/erde/rough.jpg new file mode 100644 index 0000000..d90fc23 Binary files /dev/null and b/public/materialien/erde/rough.jpg differ diff --git a/public/materialien/holz/color.jpg b/public/materialien/holz/color.jpg new file mode 100644 index 0000000..0126485 Binary files /dev/null and b/public/materialien/holz/color.jpg differ diff --git a/public/materialien/holz/normal.jpg b/public/materialien/holz/normal.jpg new file mode 100644 index 0000000..d49b53f Binary files /dev/null and b/public/materialien/holz/normal.jpg differ diff --git a/public/materialien/holz/rough.jpg b/public/materialien/holz/rough.jpg new file mode 100644 index 0000000..a3e0a07 Binary files /dev/null and b/public/materialien/holz/rough.jpg differ diff --git a/public/materialien/keramik/color.jpg b/public/materialien/keramik/color.jpg new file mode 100644 index 0000000..3c15f4c Binary files /dev/null and b/public/materialien/keramik/color.jpg differ diff --git a/public/materialien/keramik/normal.jpg b/public/materialien/keramik/normal.jpg new file mode 100644 index 0000000..e488278 Binary files /dev/null and b/public/materialien/keramik/normal.jpg differ diff --git a/public/materialien/keramik/rough.jpg b/public/materialien/keramik/rough.jpg new file mode 100644 index 0000000..73da512 Binary files /dev/null and b/public/materialien/keramik/rough.jpg differ diff --git a/public/materialien/kies/ao.jpg b/public/materialien/kies/ao.jpg new file mode 100644 index 0000000..2672af5 Binary files /dev/null and b/public/materialien/kies/ao.jpg differ diff --git a/public/materialien/kies/color.jpg b/public/materialien/kies/color.jpg new file mode 100644 index 0000000..bbf06c7 Binary files /dev/null and b/public/materialien/kies/color.jpg differ diff --git a/public/materialien/kies/normal.jpg b/public/materialien/kies/normal.jpg new file mode 100644 index 0000000..126814f Binary files /dev/null and b/public/materialien/kies/normal.jpg differ diff --git a/public/materialien/kies/rough.jpg b/public/materialien/kies/rough.jpg new file mode 100644 index 0000000..0772158 Binary files /dev/null and b/public/materialien/kies/rough.jpg differ diff --git a/public/materialien/rasen/ao.jpg b/public/materialien/rasen/ao.jpg new file mode 100644 index 0000000..99b7438 Binary files /dev/null and b/public/materialien/rasen/ao.jpg differ diff --git a/public/materialien/rasen/color.jpg b/public/materialien/rasen/color.jpg new file mode 100644 index 0000000..2b20c0e Binary files /dev/null and b/public/materialien/rasen/color.jpg differ diff --git a/public/materialien/rasen/normal.jpg b/public/materialien/rasen/normal.jpg new file mode 100644 index 0000000..449215b Binary files /dev/null and b/public/materialien/rasen/normal.jpg differ diff --git a/public/materialien/rasen/rough.jpg b/public/materialien/rasen/rough.jpg new file mode 100644 index 0000000..259b1c3 Binary files /dev/null and b/public/materialien/rasen/rough.jpg differ diff --git a/public/materialien/sand/ao.jpg b/public/materialien/sand/ao.jpg new file mode 100644 index 0000000..eb65684 Binary files /dev/null and b/public/materialien/sand/ao.jpg differ diff --git a/public/materialien/sand/color.jpg b/public/materialien/sand/color.jpg new file mode 100644 index 0000000..e890d95 Binary files /dev/null and b/public/materialien/sand/color.jpg differ diff --git a/public/materialien/sand/normal.jpg b/public/materialien/sand/normal.jpg new file mode 100644 index 0000000..dee31e2 Binary files /dev/null and b/public/materialien/sand/normal.jpg differ diff --git a/public/materialien/sand/rough.jpg b/public/materialien/sand/rough.jpg new file mode 100644 index 0000000..20abe38 Binary files /dev/null and b/public/materialien/sand/rough.jpg differ diff --git a/public/materialien/schotter/ao.jpg b/public/materialien/schotter/ao.jpg new file mode 100644 index 0000000..9f7b589 Binary files /dev/null and b/public/materialien/schotter/ao.jpg differ diff --git a/public/materialien/schotter/color.jpg b/public/materialien/schotter/color.jpg new file mode 100644 index 0000000..82ef404 Binary files /dev/null and b/public/materialien/schotter/color.jpg differ diff --git a/public/materialien/schotter/normal.jpg b/public/materialien/schotter/normal.jpg new file mode 100644 index 0000000..b89965b Binary files /dev/null and b/public/materialien/schotter/normal.jpg differ diff --git a/public/materialien/schotter/rough.jpg b/public/materialien/schotter/rough.jpg new file mode 100644 index 0000000..79bcf1b Binary files /dev/null and b/public/materialien/schotter/rough.jpg differ diff --git a/public/materialien/tegula/ao.jpg b/public/materialien/tegula/ao.jpg new file mode 100644 index 0000000..065ffe2 Binary files /dev/null and b/public/materialien/tegula/ao.jpg differ diff --git a/public/materialien/tegula/color.jpg b/public/materialien/tegula/color.jpg new file mode 100644 index 0000000..8f2e3f2 Binary files /dev/null and b/public/materialien/tegula/color.jpg differ diff --git a/public/materialien/tegula/normal.jpg b/public/materialien/tegula/normal.jpg new file mode 100644 index 0000000..9ff1fbc Binary files /dev/null and b/public/materialien/tegula/normal.jpg differ diff --git a/public/materialien/tegula/rough.jpg b/public/materialien/tegula/rough.jpg new file mode 100644 index 0000000..3df31a8 Binary files /dev/null and b/public/materialien/tegula/rough.jpg differ diff --git a/public/materialien/travertin/ao.jpg b/public/materialien/travertin/ao.jpg new file mode 100644 index 0000000..b74ffbd Binary files /dev/null and b/public/materialien/travertin/ao.jpg differ diff --git a/public/materialien/travertin/color.jpg b/public/materialien/travertin/color.jpg new file mode 100644 index 0000000..6b78b1d Binary files /dev/null and b/public/materialien/travertin/color.jpg differ diff --git a/public/materialien/travertin/normal.jpg b/public/materialien/travertin/normal.jpg new file mode 100644 index 0000000..28d6686 Binary files /dev/null and b/public/materialien/travertin/normal.jpg differ diff --git a/public/materialien/travertin/rough.jpg b/public/materialien/travertin/rough.jpg new file mode 100644 index 0000000..e3dc5a3 Binary files /dev/null and b/public/materialien/travertin/rough.jpg differ diff --git a/public/materialien/verbundstein/color.jpg b/public/materialien/verbundstein/color.jpg new file mode 100644 index 0000000..c4fe91d Binary files /dev/null and b/public/materialien/verbundstein/color.jpg differ diff --git a/public/materialien/verbundstein/normal.jpg b/public/materialien/verbundstein/normal.jpg new file mode 100644 index 0000000..ea3adfa Binary files /dev/null and b/public/materialien/verbundstein/normal.jpg differ diff --git a/public/materialien/verbundstein/rough.jpg b/public/materialien/verbundstein/rough.jpg new file mode 100644 index 0000000..c1aa37c Binary files /dev/null and b/public/materialien/verbundstein/rough.jpg differ diff --git a/src/main.ts b/src/main.ts index b7a025c..0d9c24f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -24,6 +24,7 @@ import { setupHud } from './ui/hud'; import { setQuality, type Quality } from './scene/quality'; import { buildPost, type Post } from './scene/post'; import { wind } from './scene/gras'; +import { ladeFotomaterialien } from './scene/fotomaterial'; const CENTER: [number, number] = [-9.5, -1.0]; const BOUNDS = { x0: -24.0, z0: -12.5, x1: 7.5, z1: 8.5 }; @@ -75,6 +76,12 @@ async function main() { const group = new Group(); group.name = foto ? 'Fotorealistisch' : 'Stilisiert'; + if (foto) { + // Fotografierte Materialien (ambientCG, CC0) - erst jetzt geladen, damit + // die stilisierte Ansicht leicht bleibt. + await melde(10, 'Fotomaterialien werden geladen …'); + await ladeFotomaterialien((anteil) => { void melde(10 + anteil * 10, 'Fotomaterialien werden geladen …'); }); + } await melde(22, foto ? 'Fotorealistisches Gelände …' : 'Gelände wird modelliert …'); const terrainGroup = buildTerrain(site.terrain); group.add(terrainGroup); diff --git a/src/scene/environment.ts b/src/scene/environment.ts index d485151..8cbb464 100644 --- a/src/scene/environment.ts +++ b/src/scene/environment.ts @@ -253,7 +253,10 @@ export function buildEnvironment(scene: Scene, renderer: WebGLRenderer, opts: { sky.visible = daemmerung > 0.001; skyU.sunPosition.value.copy(s.direction); envU.sunPosition.value.copy(s.direction); - (envBoden.material as ShaderMaterial).uniforms.uTone.value.setRGB(0.10, 0.12, 0.08).multiplyScalar(0.2 + dusk); + // Der Boden in der Umgebungskarte ist das zurueckgeworfene Licht von Rasen + // und hellem Travertin. Zu dunkel gewaehlt, liegen Decken, Vordaecher und + // Unterseiten im Fotomodus fast schwarz und blaustichig im Schatten. + (envBoden.material as ShaderMaterial).uniforms.uTone.value.setRGB(0.30, 0.28, 0.21).multiplyScalar(0.15 + 0.85 * dusk); envTarget?.dispose(); envTarget = pmrem.fromScene(envScene, 0.02, 0.1, 200); scene.environment = envTarget.texture; diff --git a/src/scene/fotomaterial.ts b/src/scene/fotomaterial.ts new file mode 100644 index 0000000..172dc08 --- /dev/null +++ b/src/scene/fotomaterial.ts @@ -0,0 +1,69 @@ +/** + * Fotografierte Materialien fuer die fotorealistische Darstellung. + * + * Quelle: ambientCG (https://ambientcg.com), Lizenz CC0 - frei nutzbar, auch + * ohne Namensnennung. Die Saetze liegen neu komprimiert unter + * public/materialien//{color,normal,rough,ao}.jpg; welche Karte woher + * stammt, steht in public/materialien/QUELLEN.md. + * + * Die Bilder werden erst geladen, wenn jemand auf "Fotorealistisch" schaltet - + * die stilisierte Ansicht bleibt so leicht wie bisher. + */ +export type Satz = + | 'travertin' | 'tegula' | 'verbundstein' | 'kies' | 'schotter' | 'sand' + | 'erde' | 'rasen' | 'holz' | 'beton' | 'keramik'; +export type Karte = 'color' | 'normal' | 'rough' | 'ao'; + +/** Kantenlaenge, die ein Bild in der Natur abdeckt (Meter). */ +export const FOTO_M: Record = { + travertin: 1.2, tegula: 1.0, verbundstein: 2.0, kies: 1.5, schotter: 1.5, sand: 1.5, + erde: 1.4, rasen: 1.4, holz: 0.9, beton: 1.1, keramik: 1.5, +}; + +/** Welche Karten es je Satz gibt - nicht jeder Satz hat eine Verdeckungskarte. */ +const KARTEN: Record = { + travertin: ['color', 'normal', 'rough', 'ao'], + tegula: ['color', 'normal', 'rough', 'ao'], + verbundstein: ['color', 'normal', 'rough'], + kies: ['color', 'normal', 'rough', 'ao'], + schotter: ['color', 'normal', 'rough', 'ao'], + sand: ['color', 'normal', 'rough', 'ao'], + erde: ['color', 'normal', 'rough', 'ao'], + rasen: ['color', 'normal', 'rough', 'ao'], + holz: ['color', 'normal', 'rough'], + beton: ['color', 'normal', 'rough'], + keramik: ['color', 'normal', 'rough'], +}; + +const bilder = new Map(); +let bereit = false; +let laden: Promise | null = null; + +export function fotoBereit(): boolean { return bereit; } + +export function bild(satz: Satz, karte: Karte): HTMLImageElement | undefined { + return bilder.get(`${satz}/${karte}`); +} + +/** Laedt alle Saetze; mehrfacher Aufruf laedt nur einmal. */ +export function ladeFotomaterialien(fortschritt?: (anteil: number) => void): Promise { + if (laden) return laden; + const auftraege: [string, string][] = []; + for (const [satz, karten] of Object.entries(KARTEN)) { + for (const k of karten) auftraege.push([`${satz}/${k}`, `./materialien/${satz}/${k}.jpg`]); + } + let fertig = 0; + laden = Promise.all(auftraege.map(async ([key, url]) => { + const img = new Image(); + img.src = url; + try { + await img.decode(); + bilder.set(key, img); + } catch { + // Ein fehlendes Bild faellt auf die gezeichnete Textur zurueck. + console.warn('Material fehlt:', url); + } + fortschritt?.(++fertig / auftraege.length); + })).then(() => { bereit = true; }); + return laden; +} diff --git a/src/scene/terrain.ts b/src/scene/terrain.ts index 211c313..da395f0 100644 --- a/src/scene/terrain.ts +++ b/src/scene/terrain.ts @@ -29,7 +29,7 @@ function wieseFoto(): MeshStandardMaterial { const belag = rohboden(); const mat = new MeshStandardMaterial({ map: belag.map, roughness: 1, metalness: 0, roughnessMap: belag.roughnessMap ?? null, - color: 0xc4c9ae, side: DoubleSide, + color: 0xe6e8dc, side: DoubleSide, }); mat.onBeforeCompile = (sh) => { sh.vertexShader = sh.vertexShader @@ -45,7 +45,7 @@ function wieseFoto(): MeshStandardMaterial { diffuseColor *= mix(kA, kB, smoothstep(0.25, 0.75, mischung)); #endif float gross = vnoise(vWelt.xz * 0.035) * 0.6 + vnoise(vWelt.xz * 0.16) * 0.4; - diffuseColor.rgb *= mix(0.80, 1.10, gross); + diffuseColor.rgb *= mix(0.70, 1.12, gross); // trockenere, gelblichere Stellen float trocken = smoothstep(0.62, 0.85, vnoise(vWelt.xz * 0.021 + 7.3)); diffuseColor.rgb = mix(diffuseColor.rgb, diffuseColor.rgb * vec3(1.12, 1.05, 0.78), trocken * 0.7); diff --git a/src/scene/textures.ts b/src/scene/textures.ts index b345591..10852db 100644 --- a/src/scene/textures.ts +++ b/src/scene/textures.ts @@ -8,9 +8,10 @@ */ import { CanvasTexture, RepeatWrapping, SRGBColorSpace, LinearMipmapLinearFilter, - LinearSRGBColorSpace, Vector2, + LinearSRGBColorSpace, Texture, Vector2, } from 'three'; import { isFoto } from './quality'; +import { bild, fotoBereit, FOTO_M, type Satz } from './fotomaterial'; /** * Die Zeichenfunktionen rechnen mit einer logischen Kantenlaenge von SIZE. @@ -115,12 +116,12 @@ function slabs( } export interface Belag { - map: CanvasTexture; + map: Texture; /** Nur fotorealistisch: Rauheit und Fugenschatten aus dem Hoehenbild. */ - roughnessMap?: CanvasTexture; - aoMap?: CanvasTexture; + roughnessMap?: Texture; + aoMap?: Texture; /** Aus dem Fugenbild abgeleitete Normalmap - gibt den Belaegen Tiefe. */ - normalMap?: CanvasTexture; + normalMap?: Texture; normalScale?: Vector2; tile: number; roughness: number; @@ -130,6 +131,8 @@ export interface Belag { const cache = new Map(); function make(key: string, tile: number, roughness: number, draw: (ctx: CanvasRenderingContext2D) => void): Belag { + const foto = fotoVariante(key); + if (foto) return foto; const k = keyFor(key); const hit = cache.get(k); if (hit) return hit; @@ -219,6 +222,8 @@ function makeRelief( draw: (ctx: CanvasRenderingContext2D) => void, drawHeight: (ctx: CanvasRenderingContext2D) => void, ): Belag { + const foto = fotoVariante(key); + if (foto) return foto; const k = keyFor(key); const hit = cache.get(k); if (hit) return hit; @@ -752,3 +757,167 @@ export const travertinStele = () => makeRelief('travertinStele', 0.5, 0.62, 1.7, } noiseTile(ctx, 95, 120, 0.06); }); + +// --------------------------------------------------------------------------- +// Fotografierte Materialien (nur fotorealistische Darstellung) +// --------------------------------------------------------------------------- + +interface Direkt { + satz: Satz; + /** Rauheit anheben (0..1): polierter Stein wirkt draussen sonst lackiert. */ + rauh?: number; + /** Helligkeit anheben (0..1) oder absenken (negativ). */ + hell?: number; +} + +interface Platten extends Direkt { + tile: number; slabW: number; slabH: number; fuge: number; fugenfarbe: string; + /** Helligkeitsschwankung zwischen den einzelnen Platten */ + streuung: number; + fase: number; relief: number; + /** Leinwandgroesse in Pixeln */ + px: number; +} + +/** + * Welcher gezeichnete Belag im Fotomodus durch welches Foto ersetzt wird. + * Plattenbelaege werden aus Fotoausschnitten zusammengesetzt - jede Platte + * bekommt einen eigenen Ausschnitt des Steins, dazu echte Fugen und Fasen. + */ +const FOTO_DIREKT: Record = { + travertinSlab: { satz: 'travertin', rauh: 0.38 }, + travertinMauer: { satz: 'travertin', rauh: 0.45, hell: -0.04 }, + travertinStele: { satz: 'travertin', rauh: 0.4 }, + tegula: { satz: 'tegula', hell: 0.08 }, + verbundstein: { satz: 'verbundstein' }, + dekorkies: { satz: 'kies' }, + spritzschutz: { satz: 'kies', hell: -0.1 }, + spielsand: { satz: 'sand' }, + beet: { satz: 'erde', rauh: 0.3 }, + // Gras glaenzt nicht - ohne angehobene Rauheit spiegelt es den Himmel grau. + rasen: { satz: 'rasen', rauh: 0.6 }, + schotterwiese: { satz: 'schotter', rauh: 0.3 }, + rohboden: { satz: 'rasen', rauh: 0.6 }, + beton: { satz: 'beton' }, + sockel: { satz: 'beton', hell: -0.05 }, + fassadeHolz: { satz: 'holz', hell: 0.04 }, +}; +const FOTO_PLATTEN: Record = { + travertin80: { + satz: 'travertin', tile: 3.2, slabW: 0.8, slabH: 0.8, fuge: 0.008, fugenfarbe: 'rgb(176,168,150)', + streuung: 0.06, fase: 0.012, relief: 3.2, px: 2048, rauh: 0.38, + }, + travertinstell: { + satz: 'travertin', tile: 2.4, slabW: 0.4, slabH: 0.4, fuge: 0.01, fugenfarbe: 'rgb(170,162,146)', + streuung: 0.06, fase: 0.01, relief: 3.0, px: 1024, rauh: 0.38, + }, + keramik: { + satz: 'keramik', tile: 2.4, slabW: 0.6, slabH: 0.6, fuge: 0.006, fugenfarbe: 'rgb(84,84,82)', + streuung: 0.04, fase: 0.006, relief: 2.6, px: 1024, hell: 0.22, rauh: 0.1, + }, +}; + +function fotoVariante(key: string): Belag | null { + if (!isFoto() || !fotoBereit()) return null; + const k = key + ':foto'; + const hit = cache.get(k); + if (hit) return hit; + let belag: Belag | null = null; + if (FOTO_PLATTEN[key]) belag = fotoPlatten(key, FOTO_PLATTEN[key]); + else if (FOTO_DIREKT[key]) belag = fotoDirekt(FOTO_DIREKT[key]); + if (belag) cache.set(k, belag); + return belag; +} + +function bildTextur(src: HTMLImageElement | HTMLCanvasElement, farbe: boolean): Texture { + const tx = src instanceof HTMLCanvasElement ? new CanvasTexture(src) : new Texture(src); + tx.wrapS = tx.wrapT = RepeatWrapping; + tx.colorSpace = farbe ? SRGBColorSpace : LinearSRGBColorSpace; + tx.anisotropy = 8; + tx.minFilter = LinearMipmapLinearFilter; + tx.generateMipmaps = true; + tx.needsUpdate = true; + return tx; +} + +/** Bild auf eine eigene Leinwand kopieren und dabei auf- oder abhellen. */ +function nachbearbeitet(img: HTMLImageElement, hell: number): HTMLCanvasElement { + const c = document.createElement('canvas'); + c.width = img.width; c.height = img.height; + const ctx = c.getContext('2d')!; + ctx.drawImage(img, 0, 0); + if (hell) { + ctx.fillStyle = hell > 0 ? `rgba(255,255,255,${hell})` : `rgba(0,0,0,${-hell})`; + ctx.fillRect(0, 0, c.width, c.height); + } + return c; +} + +function fotoDirekt(d: Direkt): Belag { + const color = bild(d.satz, 'color')!; + const normal = bild(d.satz, 'normal'); + const rough = bild(d.satz, 'rough'); + const ao = bild(d.satz, 'ao'); + return { + map: bildTextur(d.hell ? nachbearbeitet(color, d.hell) : color, true), + normalMap: normal ? bildTextur(normal, false) : undefined, + normalScale: new Vector2(1, 1), + roughnessMap: rough ? bildTextur(d.rauh ? nachbearbeitet(rough, d.rauh) : rough, false) : undefined, + aoMap: ao ? bildTextur(ao, false) : undefined, + tile: FOTO_M[d.satz], + roughness: 1, + }; +} + +function fotoPlatten(key: string, d: Platten): Belag { + const color = bild(d.satz, 'color')!; + const rough = bild(d.satz, 'rough'); + PHYS = d.px; + const [c, ctx] = canvas(); + const [rc, rctx] = canvas(); + const [hc, hctx] = canvas(); + const pxm = SIZE / d.tile; + ctx.fillStyle = d.fugenfarbe; + ctx.fillRect(0, 0, SIZE, SIZE); + rctx.fillStyle = 'rgb(250,250,250)'; + rctx.fillRect(0, 0, SIZE, SIZE); + let seed = 7; + for (let i = 0; i < key.length; i++) seed = (seed * 31 + key.charCodeAt(i)) >>> 0; + const rnd = mulberry32(seed); + const cols = Math.round(d.tile / d.slabW), rows = Math.round(d.tile / d.slabH); + const j = d.fuge * pxm; + const physM = FOTO_M[d.satz]; + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + const x = col * d.slabW * pxm + j / 2, y = row * d.slabH * pxm + j / 2; + const w = d.slabW * pxm - j, h = d.slabH * pxm - j; + // eigener Ausschnitt des Steins je Platte + const sw = (d.slabW / physM) * color.width, sh = (d.slabH / physM) * color.height; + const sx = rnd() * Math.max(0, color.width - sw), sy = rnd() * Math.max(0, color.height - sh); + ctx.drawImage(color, sx, sy, sw, sh, x, y, w, h); + const v = (rnd() - 0.5) * 2 * d.streuung + (d.hell ?? 0); + if (v) { + ctx.fillStyle = v > 0 ? `rgba(255,250,240,${v})` : `rgba(40,34,28,${-v})`; + ctx.fillRect(x, y, w, h); + } + if (rough) { + const k = rough.width / color.width; + rctx.drawImage(rough, sx * k, sy * k, sw * k, sh * k, x, y, w, h); + if (d.rauh) { + rctx.fillStyle = `rgba(255,255,255,${d.rauh})`; + rctx.fillRect(x, y, w, h); + } + } + } + } + slabHeight(hctx, d.tile, d.slabW, d.slabH, d.fuge, d.fase); + return { + map: bildTextur(c, true), + normalMap: normalFromHeight(hc, d.relief * (PHYS / SIZE)), + normalScale: new Vector2(1, 1), + roughnessMap: bildTextur(rc, false), + aoMap: derivedMap(hc, (hh) => 0.35 + Math.min(1, hh * 1.25) * 0.65), + tile: d.tile, + roughness: 1, + }; +}