Oben in der Mitte schaltet ein Knopf zwischen Stilisiert und Fotorealistisch. Beide Varianten entstehen aus derselben Geometrie; die zweite wird beim ersten Umschalten aufgebaut und bleibt danach bestehen. Fotorealistisch: - Texturen doppelt so fein, mit Rauheits- und Verdeckungskarten aus dem Hoehenbild. Das Motiv wird skaliert auf die groessere Leinwand gezeichnet, damit Scheite, Kiesel und Poren gleich gross bleiben. - Laub aus Blattkarten (Baumlaub, Eibennadeln, Liguster, Straeucher, Kraeuter) mit Volumen-Normalen und Alpha-Test-Schatten. - Baeume rekursiv gewachsen, mit gekruemmten Aesten und Blattwolken. - Rasen und Schotterwiese aus Grasbueschelen, die im Wind wiegen. - Physikalischer Himmel mit Umgebungslicht, Sonne zu Himmel etwa 5:1, Verlaufshimmel in der Daemmerung, 4096er Schatten. - GTAO, leichtes Bloom, MSAA. - Wiese ohne sichtbares Kachelmuster. Nebenbei behoben: - Normalmaps hatten die y-Steigung verkehrt: Poren und Fugen standen als Buckel hervor. Betrifft auch die stilisierte Ansicht. - Umlandkranz schliesst jetzt luecklos an das Raster an; vorher sah man von oben als helle Linie den Himmel durch den Spalt. - Glas im Fotomodus fast spiegelnd, Holzfassade mit Relief. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+74
-10
@@ -7,6 +7,52 @@ import {
|
||||
} from 'three';
|
||||
import type { Terrain } from '../site';
|
||||
import { rohboden } from './textures';
|
||||
import { isFoto } from './quality';
|
||||
|
||||
/** Wertrauschen im Shader, fuer grossflaechige Farbwechsel der Wiese. */
|
||||
const NOISE_GLSL = /* glsl */ `
|
||||
float hash21(vec2 p) { p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); }
|
||||
float vnoise(vec2 p) {
|
||||
vec2 i = floor(p), f = fract(p);
|
||||
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||
return mix(mix(hash21(i), hash21(i + vec2(1, 0)), u.x), mix(hash21(i + vec2(0, 1)), hash21(i + vec2(1, 1)), u.x), u.y);
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Wiese fuer die fotorealistische Darstellung. Die Kachel wird in zwei
|
||||
* Massstaeben gemischt und grossflaechig in Farbe und Helligkeit variiert -
|
||||
* sonst erkennt man das Wiederholungsmuster sofort, sobald man von oben
|
||||
* auf das Umland schaut.
|
||||
*/
|
||||
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,
|
||||
});
|
||||
mat.onBeforeCompile = (sh) => {
|
||||
sh.vertexShader = sh.vertexShader
|
||||
.replace('#include <common>', '#include <common>\nvarying vec3 vWelt;')
|
||||
.replace('#include <begin_vertex>', '#include <begin_vertex>\nvWelt = (modelMatrix * vec4(transformed, 1.0)).xyz;');
|
||||
sh.fragmentShader = sh.fragmentShader
|
||||
.replace('#include <common>', '#include <common>\nvarying vec3 vWelt;' + NOISE_GLSL)
|
||||
.replace('#include <map_fragment>', `
|
||||
#ifdef USE_MAP
|
||||
vec4 kA = texture2D(map, vMapUv);
|
||||
vec4 kB = texture2D(map, vMapUv * 0.29 + vec2(0.37, 0.11));
|
||||
float mischung = vnoise(vWelt.xz * 0.09);
|
||||
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);
|
||||
// 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);
|
||||
`);
|
||||
};
|
||||
return mat;
|
||||
}
|
||||
|
||||
export function buildTerrain(terrain: Terrain): Group {
|
||||
const group = new Group();
|
||||
@@ -42,30 +88,35 @@ export function buildTerrain(terrain: Terrain): Group {
|
||||
geo.setIndex(idx);
|
||||
geo.computeVertexNormals();
|
||||
|
||||
const ground = new Mesh(geo, new MeshStandardMaterial({
|
||||
const foto = isFoto();
|
||||
const wiese = foto ? wieseFoto() : null;
|
||||
const ground = new Mesh(geo, wiese ?? new MeshStandardMaterial({
|
||||
map: belag.map, roughness: belag.roughness, metalness: 0,
|
||||
}));
|
||||
ground.name = 'Terrain';
|
||||
ground.receiveShadow = true;
|
||||
group.add(ground);
|
||||
|
||||
|
||||
// --- Umland ---------------------------------------------------------------
|
||||
// Das Grundstueck liegt an einem Hang ueber dem Tal. Das Umland wird als
|
||||
// Kranz direkt an die Randpunkte des Rasters gehaengt - so entsteht keine
|
||||
// sichtbare Kante - und faellt gegen aussen ab, vor allem nach Suedwesten.
|
||||
const wiese = rohboden();
|
||||
const umland = rohboden();
|
||||
const cx = x0 + ((nx - 1) * cell) / 2;
|
||||
const cz = z0 + ((nz - 1) * cell) / 2;
|
||||
|
||||
/**
|
||||
* Randpunkte des Rasters im Umlauf, bewusst grob abgetastet: bei jedem
|
||||
* Rasterpunkt entstuenden extrem schlanke Dreiecke, deren Normalen
|
||||
* unbrauchbar werden und sich als dunkle Streifen abzeichnen.
|
||||
* Randpunkte des Rasters im Umlauf - jeder einzelne. Nur so schliesst der
|
||||
* Kranz luecklos an; mit groeber Abtastung blieb zwischen Rand und Kranz ein
|
||||
* Spalt, durch den man von oben als helle Linie den Himmel sah. Die dabei
|
||||
* entstehenden schlanken Dreiecke bekommen weiter unten ruhige Normalen,
|
||||
* sonst zeichnen sie sich als dunkle Streifen ab.
|
||||
*/
|
||||
const STEP = 10;
|
||||
const STEP = 1;
|
||||
const border: { x: number; z: number; y: number }[] = [];
|
||||
const at = (i: number, j: number) => ({
|
||||
x: x0 + i * cell, z: z0 + j * cell, y: h[j * nx + i] - 0.05,
|
||||
x: x0 + i * cell, z: z0 + j * cell, y: h[j * nx + i],
|
||||
});
|
||||
for (let i = 0; i < nx - 1; i += STEP) border.push(at(i, 0));
|
||||
for (let j = 0; j < nz - 1; j += STEP) border.push(at(nx - 1, j));
|
||||
@@ -91,7 +142,7 @@ export function buildTerrain(terrain: Terrain): Group {
|
||||
// Nach Suedwesten faellt das Gelaende deutlich staerker ab (Blick ins Tal).
|
||||
const sw = (1 - Math.cos(Math.atan2(dz, dx) - 2.4)) / 2;
|
||||
ringPos.push(x, p.y - t * (6 + 26 * sw * sw), z);
|
||||
ringUv.push(x / wiese.tile, -z / wiese.tile);
|
||||
ringUv.push(x / umland.tile, -z / umland.tile);
|
||||
}
|
||||
}
|
||||
for (let k = 0; k < RINGS; k++) {
|
||||
@@ -106,8 +157,21 @@ export function buildTerrain(terrain: Terrain): Group {
|
||||
far.setAttribute('uv', new BufferAttribute(new Float32Array(ringUv), 2));
|
||||
far.setIndex(ringIdx);
|
||||
far.computeVertexNormals();
|
||||
const surroundings = new Mesh(far, new MeshStandardMaterial({
|
||||
map: wiese.map, roughness: 1, metalness: 0, color: 0xa9b096, side: DoubleSide,
|
||||
// Ruhige Normalen: das Umland ist ein weicher Hang, die Beleuchtung soll
|
||||
// nicht die Dreiecksstruktur nachzeichnen.
|
||||
const fn = far.getAttribute('normal') as BufferAttribute;
|
||||
// Der Kranz ist so gewickelt, dass man von oben seine Rueckseite sieht
|
||||
// (DoubleSide dreht die Normale dort um) - also zur selben Seite hin glaetten.
|
||||
let sum = 0;
|
||||
for (let i = 0; i < fn.count; i++) sum += fn.getY(i);
|
||||
const oben = sum >= 0 ? 1 : -1;
|
||||
for (let i = 0; i < fn.count; i++) {
|
||||
const nx2 = fn.getX(i) * 0.35, ny2 = fn.getY(i) * 0.35 + 0.65 * oben, nz2 = fn.getZ(i) * 0.35;
|
||||
const l = Math.hypot(nx2, ny2, nz2) || 1;
|
||||
fn.setXYZ(i, nx2 / l, ny2 / l, nz2 / l);
|
||||
}
|
||||
const surroundings = new Mesh(far, wiese ?? new MeshStandardMaterial({
|
||||
map: umland.map, roughness: 1, metalness: 0, color: 0xa9b096, side: DoubleSide,
|
||||
}));
|
||||
surroundings.name = 'Umland';
|
||||
group.add(surroundings);
|
||||
|
||||
Reference in New Issue
Block a user