Fotografierte Materialien fuer die fotorealistische Ansicht
Deploy App / deploy (push) Successful in 29s
Zwoelf CC0-Materialsaetze von ambientCG, neu komprimiert auf 11 MB (public/materialien, Quellenliste in QUELLEN.md). Sie werden erst geladen, wenn auf "Fotorealistisch" geschaltet wird. - Travertin (Travertine009, 2K): 80x80-Platten und Stellplatten werden aus Fotoausschnitten zusammengesetzt - jede Platte ein eigener Ausschnitt, dazu Fugen, Fasen und Fugenschatten. Lose Platten, Stufen, Stelen und Mauern bekommen das Foto direkt. - Keramik 60x60 aus Concrete017, ebenfalls als Plattenraster. - Tegula, Verbundstein, Dekorkies, Schotterwiese, Spielsand, Beete, Rasen, Wiese, Holzschalung, Beton und Sockel direkt als Fotomaterial. - Rauheit fuer Gras, Erde und Stein angehoben: poliert fotografierter Stein und Gras spiegelten draussen sonst den Himmel grau. - Boden der Umgebungskarte als warmes Rueckstrahllicht - Decken und Unterseiten lagen vorher fast schwarz im Schatten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@@ -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`) |
|
||||
|
||||
@@ -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`).
|
||||
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 351 KiB |
|
After Width: | Height: | Size: 470 KiB |
|
After Width: | Height: | Size: 570 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 262 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 391 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 362 KiB |
|
After Width: | Height: | Size: 270 KiB |
|
After Width: | Height: | Size: 608 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 413 KiB |
|
After Width: | Height: | Size: 375 KiB |
|
After Width: | Height: | Size: 537 KiB |
|
After Width: | Height: | Size: 358 KiB |
|
After Width: | Height: | Size: 326 KiB |
|
After Width: | Height: | Size: 370 KiB |
|
After Width: | Height: | Size: 558 KiB |
|
After Width: | Height: | Size: 348 KiB |
|
After Width: | Height: | Size: 382 KiB |
|
After Width: | Height: | Size: 436 KiB |
|
After Width: | Height: | Size: 601 KiB |
|
After Width: | Height: | Size: 279 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 607 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 76 KiB |
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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/<satz>/{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<Satz, number> = {
|
||||
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<Satz, Karte[]> = {
|
||||
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<string, HTMLImageElement>();
|
||||
let bereit = false;
|
||||
let laden: Promise<void> | 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<void> {
|
||||
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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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<string, Belag>();
|
||||
|
||||
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<string, Direkt> = {
|
||||
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<string, Platten> = {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||