Etappe D: Solitaergehoelze, Kronenschattierung, Nachbargelaende Sued
Deploy App / deploy (push) Successful in 27s
Deploy App / deploy (push) Successful in 27s
- blob(): die Auslenkung war ein Zufallswert je Vertex. IcosahedronGeometry ist unindiziert, jeder Eckpunkt kommt mehrfach vor - die Flaeche riss in Splitter auf. Jetzt eine stetige Funktion der Richtung, dazu wahlweise die Normalen der ungestoerten Kugel: die Krone wird weich schattiert, die Silhouette bleibt unregelmaessig. - Baumkronen mit eigener Laubtextur (laubBaum) statt flacher Farbe. - Parrotia als mehrstaemmiges Solitaergehoelz: die drei Staemme laufen aus einem Punkt schraeg auseinander, die Kronenblasen sitzen darueber. - Gelaendezonen: Grenze Garten/Sued liegt jetzt auf den Winkelelementen bei z 4.96, Grenze Garten/Ost auf den Winkelelementen bei x 3.74. Das Nachbargrundstueck Sued liegt hoeher als der Garten - dafuer stehen dort Winkelelemente und die bestehende Betonmauer. Koten setzen den Trend der Grenzkoten fort. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -652,6 +652,8 @@ function laub(key: string, base: string, seed: number, leaf: number): Belag {
|
|||||||
|
|
||||||
/** Eibe: dunkel, feinnadelig. */
|
/** Eibe: dunkel, feinnadelig. */
|
||||||
export const laubEibe = () => laub('laubEibe', 'rgb(64,92,52)', 201, 3.4);
|
export const laubEibe = () => laub('laubEibe', 'rgb(64,92,52)', 201, 3.4);
|
||||||
|
/** Baumkrone: groesseres, helleres Blatt mit staerkerer Streuung. */
|
||||||
|
export const laubBaum = () => laub('laubBaum', 'rgb(105,132,68)', 223, 7.0);
|
||||||
/** Liguster: mittelgruen, groesseres Blatt. */
|
/** Liguster: mittelgruen, groesseres Blatt. */
|
||||||
export const laubLiguster = () => laub('laubLiguster', 'rgb(85,118,55)', 211, 5.2);
|
export const laubLiguster = () => laub('laubLiguster', 'rgb(85,118,55)', 211, 5.2);
|
||||||
|
|
||||||
|
|||||||
+54
-19
@@ -29,18 +29,41 @@ function rng(seed: number) {
|
|||||||
* wichtig: eine ungleichmaessige Instanz-Skalierung verzerrt die Normalen und
|
* wichtig: eine ungleichmaessige Instanz-Skalierung verzerrt die Normalen und
|
||||||
* erzeugt harte, glaenzende Kanten.
|
* erzeugt harte, glaenzende Kanten.
|
||||||
*/
|
*/
|
||||||
function blob(radius: number, detail: number, roughness: number, seed: number, stretchY = 1): BufferGeometry {
|
function blob(
|
||||||
|
radius: number, detail: number, roughness: number, seed: number, stretchY = 1,
|
||||||
|
smooth = false, uvScale = 1,
|
||||||
|
): BufferGeometry {
|
||||||
const g = new IcosahedronGeometry(radius, detail);
|
const g = new IcosahedronGeometry(radius, detail);
|
||||||
const pos = g.getAttribute('position') as BufferAttribute;
|
const pos = g.getAttribute('position') as BufferAttribute;
|
||||||
const r = rng(seed);
|
const r = rng(seed);
|
||||||
|
// Die Auslenkung muss eine Funktion der Richtung sein, nicht des Vertex:
|
||||||
|
// IcosahedronGeometry ist unindiziert, jeder Eckpunkt kommt mehrfach vor.
|
||||||
|
// Ein Zufallswert je Vertex reisst die Flaeche in Splitter auf.
|
||||||
|
const ph = [r() * 6.28, r() * 6.28, r() * 6.28, r() * 6.28];
|
||||||
|
const wob = (x: number, y: number, z: number) =>
|
||||||
|
Math.sin(x * 2.7 + ph[0]) * Math.sin(y * 3.1 + ph[1]) * Math.sin(z * 2.3 + ph[2]) * 0.62
|
||||||
|
+ Math.sin(x * 5.3 + y * 4.1 + ph[3]) * 0.38;
|
||||||
const v = new Vector3();
|
const v = new Vector3();
|
||||||
|
const normals = smooth ? new Float32Array(pos.count * 3) : null;
|
||||||
for (let i = 0; i < pos.count; i++) {
|
for (let i = 0; i < pos.count; i++) {
|
||||||
v.fromBufferAttribute(pos, i);
|
v.fromBufferAttribute(pos, i);
|
||||||
const f = 1 + (r() - 0.5) * 2 * roughness;
|
const n = v.clone().normalize();
|
||||||
|
if (normals) {
|
||||||
|
// Normale der ungestoerten Kugel: die Krone wird dadurch weich
|
||||||
|
// schattiert, waehrend die Silhouette unregelmaessig bleibt. Mit den
|
||||||
|
// Normalen der verbeulten Flaeche zerfaellt sie in sichtbare Facetten.
|
||||||
|
normals[i * 3] = n.x; normals[i * 3 + 1] = n.y; normals[i * 3 + 2] = n.z;
|
||||||
|
}
|
||||||
|
const f = 1 + wob(n.x, n.y, n.z) * roughness;
|
||||||
v.multiplyScalar(f);
|
v.multiplyScalar(f);
|
||||||
pos.setXYZ(i, v.x, v.y * stretchY, v.z);
|
pos.setXYZ(i, v.x, v.y * stretchY, v.z);
|
||||||
}
|
}
|
||||||
g.computeVertexNormals();
|
if (normals) g.setAttribute('normal', new BufferAttribute(normals, 3));
|
||||||
|
else g.computeVertexNormals();
|
||||||
|
if (uvScale !== 1) {
|
||||||
|
const uv = g.getAttribute('uv') as BufferAttribute;
|
||||||
|
for (let i = 0; i < uv.count; i++) uv.setXY(i, uv.getX(i) * uvScale, uv.getY(i) * uvScale);
|
||||||
|
}
|
||||||
return g;
|
return g;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,39 +91,51 @@ function buildTree(t: Tree, y: number): Group {
|
|||||||
const crownR = Math.max(0.9, t.crown);
|
const crownR = Math.max(0.9, t.crown);
|
||||||
const trunkH = t.height * 0.42;
|
const trunkH = t.height * 0.42;
|
||||||
|
|
||||||
|
// Parrotia persica steht im Plan als mehrstaemmiges Solitaergehoelz: die
|
||||||
|
// Staemme laufen aus einem Punkt am Boden schraeg auseinander.
|
||||||
const stems = t.species === 'parrotia' ? 3 : 1;
|
const stems = t.species === 'parrotia' ? 3 : 1;
|
||||||
|
const tips: [number, number][] = [];
|
||||||
for (let s = 0; s < stems; s++) {
|
for (let s = 0; s < stems; s++) {
|
||||||
const lean = stems > 1 ? 0.13 : 0;
|
const a = (s / stems) * Math.PI * 2 + r() * 0.8;
|
||||||
const ox = (r() - 0.5) * 2 * lean * trunkH;
|
const spread = stems > 1 ? crownR * (0.32 + r() * 0.22) : 0;
|
||||||
const oz = (r() - 0.5) * 2 * lean * trunkH;
|
const ox = Math.cos(a) * spread, oz = Math.sin(a) * spread;
|
||||||
const geo = new CylinderGeometry(0.05 + r() * 0.015, 0.075 + r() * 0.02, trunkH, 9, 1);
|
const len = Math.hypot(trunkH, Math.hypot(ox, oz));
|
||||||
|
const geo = new CylinderGeometry(0.035 + r() * 0.015, 0.07 + r() * 0.025, len, 10, 1);
|
||||||
const m = new Mesh(geo, RINDE);
|
const m = new Mesh(geo, RINDE);
|
||||||
m.position.set(t.x + ox / 2, y + trunkH / 2, t.z + oz / 2);
|
m.position.set(t.x + ox / 2, y + trunkH / 2, t.z + oz / 2);
|
||||||
m.rotation.z = ox / trunkH;
|
m.rotation.z = -Math.atan2(ox, trunkH);
|
||||||
m.rotation.x = -oz / trunkH;
|
m.rotation.x = Math.atan2(oz, trunkH);
|
||||||
m.castShadow = true;
|
m.castShadow = true;
|
||||||
g.add(m);
|
g.add(m);
|
||||||
|
tips.push([ox, oz]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Krone aus mehreren, leicht unterschiedlich getoenten Blasen
|
// Krone aus mehreren, leicht unterschiedlich getoenten Blasen
|
||||||
|
const laub = tex.laubBaum();
|
||||||
const blobs = 5 + Math.round(r() * 2);
|
const blobs = 5 + Math.round(r() * 2);
|
||||||
for (let i = 0; i < blobs; i++) {
|
for (let i = 0; i < blobs; i++) {
|
||||||
const rr = crownR * (0.6 + r() * 0.35);
|
const rr = crownR * (0.6 + r() * 0.35);
|
||||||
const geo = blob(rr, 2, 0.07, i * 17 + 5);
|
const geo = blob(rr, 3, 0.18, i * 17 + 5, 0.82 + r() * 0.2, true, Math.max(2, rr * 4));
|
||||||
const shade = 1 - r() * 0.16;
|
const shade = 1 - r() * 0.16;
|
||||||
const mat = foliage(
|
const mat = new MeshStandardMaterial({
|
||||||
new Color(LAUB[t.species]).multiplyScalar(shade).getHex(),
|
map: laub.map,
|
||||||
0.94,
|
normalMap: laub.normalMap ?? null,
|
||||||
);
|
normalScale: laub.normalScale,
|
||||||
|
color: new Color(LAUB[t.species]).multiplyScalar(shade * 1.35),
|
||||||
|
roughness: 0.95,
|
||||||
|
metalness: 0,
|
||||||
|
});
|
||||||
const m = new Mesh(geo, mat);
|
const m = new Mesh(geo, mat);
|
||||||
const a = (i / blobs) * Math.PI * 2 + r();
|
const a = (i / blobs) * Math.PI * 2 + r();
|
||||||
const rad = crownR * 0.3 * (i === 0 ? 0 : 1);
|
// Die Blasen sitzen ueber den Stammspitzen und greifen weiter aus, damit
|
||||||
|
// die Krone nicht als eine glatte Kugel liest.
|
||||||
|
const tip = tips[i % tips.length];
|
||||||
|
const rad = crownR * (i === 0 ? 0.12 : 0.42 + r() * 0.28);
|
||||||
m.position.set(
|
m.position.set(
|
||||||
t.x + Math.cos(a) * rad,
|
t.x + tip[0] * 0.8 + Math.cos(a) * rad,
|
||||||
y + trunkH + crownR * (0.4 + r() * 0.45),
|
y + trunkH + crownR * (0.15 + r() * 0.7),
|
||||||
t.z + Math.sin(a) * rad,
|
t.z + tip[1] * 0.8 + Math.sin(a) * rad,
|
||||||
);
|
);
|
||||||
m.scale.y = 0.86;
|
|
||||||
m.castShadow = true;
|
m.castShadow = true;
|
||||||
m.receiveShadow = true;
|
m.receiveShadow = true;
|
||||||
g.add(m);
|
g.add(m);
|
||||||
|
|||||||
+16
-7
@@ -393,7 +393,7 @@ export const NEIGHBOUR_BUILDINGS = [
|
|||||||
export const TERRAIN_ZONES = [
|
export const TERRAIN_ZONES = [
|
||||||
{
|
{
|
||||||
id: 'garten', label: 'Garten Kellenberger',
|
id: 'garten', label: 'Garten Kellenberger',
|
||||||
polygon: [[-20.90, -6.60], [3.00, -6.60], [3.00, 5.95], [-20.90, 5.95]],
|
polygon: [[-20.90, -6.60], [3.74, -6.60], [3.74, 4.96], [-20.90, 4.96]],
|
||||||
fallback: -0.05,
|
fallback: -0.05,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -407,13 +407,16 @@ export const TERRAIN_ZONES = [
|
|||||||
fallback: -1.70,
|
fallback: -1.70,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
// Die Grenze der Zone liegt auf den Winkelelementen bei z 4.96: dort
|
||||||
|
// faengt der Nachbar sein hoeher liegendes Terrain, dahinter steht die
|
||||||
|
// bestehende Betonmauer.
|
||||||
id: 'sued', label: 'Nachbargrundstück Süd',
|
id: 'sued', label: 'Nachbargrundstück Süd',
|
||||||
polygon: [[-22.00, 5.95], [6.00, 5.95], [6.00, 12.00], [-22.00, 12.00]],
|
polygon: [[-22.00, 4.96], [6.00, 4.96], [6.00, 12.00], [-22.00, 12.00]],
|
||||||
fallback: -0.20,
|
fallback: 0.45,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ost', label: 'Wiese Ost',
|
id: 'ost', label: 'Wiese Ost',
|
||||||
polygon: [[3.00, -6.60], [9.00, -6.60], [9.00, 5.95], [3.00, 5.95]],
|
polygon: [[3.74, -6.60], [9.00, -6.60], [9.00, 4.96], [3.74, 4.96]],
|
||||||
fallback: 0.55,
|
fallback: 0.55,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -430,9 +433,15 @@ export const EXTRA_SPOTS = [
|
|||||||
{ x: -20.0, z: -12.5, y: -1.75, zone: 'zollet' },
|
{ x: -20.0, z: -12.5, y: -1.75, zone: 'zollet' },
|
||||||
{ x: -8.0, z: -12.5, y: -1.70, zone: 'zollet' },
|
{ x: -8.0, z: -12.5, y: -1.70, zone: 'zollet' },
|
||||||
{ x: 2.0, z: -11.5, y: -1.65, zone: 'zollet' },
|
{ x: 2.0, z: -11.5, y: -1.65, zone: 'zollet' },
|
||||||
{ x: -18.0, z: 9.0, y: -0.40, zone: 'sued' },
|
// Das Nachbargrundstueck im Sueden liegt hoeher als der Garten - genau
|
||||||
{ x: -6.0, z: 9.0, y: 0.10, zone: 'sued' },
|
// deshalb stehen dort Winkelelemente und die bestehende Betonmauer. Die
|
||||||
{ x: 4.0, z: 9.0, y: 0.60, zone: 'sued' },
|
// Werte setzen den Trend der Grenzkoten (-0.04 / 0.26 / 0.63) fort.
|
||||||
|
{ x: -18.0, z: 5.6, y: 0.10, zone: 'sued' },
|
||||||
|
{ x: -6.0, z: 5.6, y: 0.45, zone: 'sued' },
|
||||||
|
{ x: 4.0, z: 5.6, y: 0.85, zone: 'sued' },
|
||||||
|
{ x: -18.0, z: 9.0, y: 0.25, zone: 'sued' },
|
||||||
|
{ x: -6.0, z: 9.0, y: 0.60, zone: 'sued' },
|
||||||
|
{ x: 4.0, z: 9.0, y: 1.00, zone: 'sued' },
|
||||||
{ x: 7.5, z: -5.0, y: 0.10, zone: 'ost' },
|
{ x: 7.5, z: -5.0, y: 0.10, zone: 'ost' },
|
||||||
{ x: 7.5, z: 0.0, y: 0.45, zone: 'ost' },
|
{ x: 7.5, z: 0.0, y: 0.45, zone: 'ost' },
|
||||||
{ x: 7.5, z: 5.0, y: 0.80, zone: 'ost' },
|
{ x: 7.5, z: 5.0, y: 0.80, zone: 'ost' },
|
||||||
|
|||||||
Reference in New Issue
Block a user