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>
This commit is contained in:
2026-09-13 11:44:09 +02:00
co-authored by Claude Opus 5
parent 62f4d72355
commit 134fb22937
47 changed files with 281 additions and 9 deletions
+174 -5
View File
@@ -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,
};
}