Haus - Oeffnungen aus den Fassadenplaenen von EHLE abgegriffen. Zum Garten steht im Erdgeschoss eine einzige, mittige Fensterfront (x -8.82 bis -4.67), daneben das Lamellenband vor dem gedeckten Sitzplatz; im Obergeschoss zwei Fenster mit zwei schmalen Lamellen dazwischen. - Der gedeckte Sitzplatz ist nur noch gegen Westen offen, so wie es die Suedwestfassade als "Aussensitzplatz" zeigt. Gegen den Garten steht dort eine geschlossene Wand - bisher schaute man durch die Fassade hinein. - Hauszugang an der Nordostfassade statt an der Nordwestfassade: Tuerfluegel und feststehende Verglasung, zusammen 3.15 m, darueber das Vordach. Das Vordach kennt jetzt seine Fassadenseite. - Die Fassadennamen im Datenmodell waren vertauscht; jetzt mit Tabelle im README festgehalten. Garten - Sichtschutz: die gruenen Punkte im Kiesbett sind Ziergraeser, keine Polster. Neue Halmgeometrie - schmales, sich verjuengendes Band, das nach oben ueberhaengt, rund 1.10 m. Die Bodenleuchten sind jetzt Strahler und leuchten Stelen und Graeser von unten an. - Baenkli: die Fuesse standen quer zur Sitzplatte, weil der Versatz in Welt-Z statt entlang der gedrehten Achse gerechnet wurde. - Obere Ebene bis an das Kraeuterbeet eben auf -0.02. Zwischen Grillfeld und Kraeuterbeet stieg der Rasen um gut 30 cm an. - Unter dem Spielhaus steht der Betonklotz aus dem Plan (graues Rechteck 1.01 x 2.70 m); die offene Plattform daneben bleibt auf Stuetzen. - Aus dem Gartenhaus wird ein Gartenschrank: 1.80 x 0.50 m, 2.00 m hoch, mit dem Ruecken in der Ecke und den Doppeltueren zum Garten. - Neben dem Gelaender der Osttreppe lag Rasen; dort liegt jetzt derselbe Belag wie auf dem Weg, bis an den Treppenkopf. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+25
-8
@@ -118,7 +118,10 @@ export function buildBuilding(b: Building): Group {
|
||||
const list: Rect[] = b.openings
|
||||
.filter((o) => o.face === face)
|
||||
.map((o) => ({ u0: Math.min(o.a, o.b), u1: Math.max(o.a, o.b), y0: o.sill, y1: o.head }));
|
||||
if (face === 'south') list.push({ u0: b.loggia.x0, u1: b.loggia.x1, y0: b.floorLevel - 0.4, y1: b.loggia.ceiling });
|
||||
// Der gedeckte Sitzplatz ist nur gegen Westen offen - so zeigt es die
|
||||
// Suedwestfassade ("Aussensitzplatz"). Gegen den Garten steht dort eine
|
||||
// geschlossene Wand mit dem Lamellenband davor; ohne sie schaut man von
|
||||
// aussen durch die Fassade in den Sitzplatz.
|
||||
if (face === 'west') list.push({ u0: b.loggia.z0, u1: b.loggia.z1, y0: b.floorLevel - 0.4, y1: b.loggia.ceiling });
|
||||
return list;
|
||||
};
|
||||
@@ -159,6 +162,11 @@ export function buildBuilding(b: Building): Group {
|
||||
holzMat,
|
||||
);
|
||||
loggia.add(back);
|
||||
const front = new Mesh(
|
||||
boxGeometry((lg.x0 + lg.x1) / 2, lg.z1 - 0.08, lg.x1 - lg.x0, 0.16, b.floorLevel - 0.4, lg.ceiling, 0, holz.tile),
|
||||
holzMat,
|
||||
);
|
||||
loggia.add(front);
|
||||
const ceil = new PlaneGeometry(lg.x1 - lg.x0, lg.z1 - lg.z0);
|
||||
ceil.rotateX(Math.PI / 2);
|
||||
const ceilMesh = new Mesh(ceil, new MeshStandardMaterial({ color: 0xd8d4cc, roughness: 0.85 }));
|
||||
@@ -232,14 +240,23 @@ export function buildBuilding(b: Building): Group {
|
||||
group.add(roof);
|
||||
|
||||
// --- Vordach ueber dem Eingang -----------------------------------------
|
||||
// --- Vordach ueber dem Hauszugang --------------------------------------
|
||||
// Es kragt senkrecht zu seiner Fassade aus; a und b laufen in der
|
||||
// Fassadenrichtung (Site-X bei Nord/Sued, Site-Z bei Ost/West).
|
||||
const cp = b.canopy;
|
||||
const canopy = new Mesh(
|
||||
boxGeometry((cp.a + cp.b) / 2, z0 - cp.depth / 2, cp.b - cp.a, cp.depth, cp.y, cp.y + cp.thickness, 0, 2),
|
||||
new MeshStandardMaterial({ color: 0x5a5148, roughness: 0.6 }),
|
||||
);
|
||||
canopy.name = 'Vordach Eingang';
|
||||
canopy.castShadow = true;
|
||||
group.add(canopy);
|
||||
const dachMat = new MeshStandardMaterial({ color: 0x5a5148, roughness: 0.6 });
|
||||
const mitte = (cp.a + cp.b) / 2, laenge = Math.abs(cp.b - cp.a);
|
||||
let dach: Mesh;
|
||||
if (cp.face === 'east' || cp.face === 'west') {
|
||||
const xf = cp.face === 'east' ? x1 + cp.depth / 2 : x0 - cp.depth / 2;
|
||||
dach = new Mesh(boxGeometry(xf, mitte, cp.depth, laenge, cp.y, cp.y + cp.thickness, 0, 2), dachMat);
|
||||
} else {
|
||||
const zf = cp.face === 'south' ? z1 + cp.depth / 2 : z0 - cp.depth / 2;
|
||||
dach = new Mesh(boxGeometry(mitte, zf, laenge, cp.depth, cp.y, cp.y + cp.thickness, 0, 2), dachMat);
|
||||
}
|
||||
dach.name = 'Vordach Hauszugang';
|
||||
dach.castShadow = true;
|
||||
group.add(dach);
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
+75
-22
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
import {
|
||||
BoxGeometry, BufferGeometry, CylinderGeometry, Group, Mesh, MeshStandardMaterial,
|
||||
PointLight, type MeshStandardMaterialParameters,
|
||||
PointLight, SpotLight, type MeshStandardMaterialParameters,
|
||||
} from 'three';
|
||||
import type { Lamp, Linear, Prop, Stele } from '../site';
|
||||
import { boxGeometry, mergeGeometries } from './geometry';
|
||||
@@ -80,35 +80,61 @@ function schwebendeStufe(p: Prop, y: number): Group {
|
||||
// Freistehende Objekte
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Gartenschrank: schmaler Schrank mit Doppeltuer und leicht geneigtem Deckel.
|
||||
* Der Plan reserviert an dieser Stelle 1.8 x 1.8 m fuer ein Gartenhaus; gebaut
|
||||
* wird ein Schrank von 1.80 x 0.50 m, rund 2.00 m hoch, mit dem Ruecken in der
|
||||
* Ecke und den Tueren zum Garten.
|
||||
*/
|
||||
function gartenhaus(p: Prop, y: number): Group {
|
||||
const g = new Group();
|
||||
const holz = material(tex.fassadeHolz(), { roughness: 0.8 });
|
||||
const dunkel = new MeshStandardMaterial({ color: 0x3b3934, roughness: 0.62 });
|
||||
const metall = new MeshStandardMaterial({ color: 0x55565a, roughness: 0.45, metalness: 0.6 });
|
||||
const a = (p.rot * Math.PI) / 180;
|
||||
// u laeuft entlang p.w, v entlang p.d - so wie in boxGeometry.
|
||||
const ux = Math.cos(a), uz = Math.sin(a);
|
||||
const vx = -Math.sin(a), vz = Math.cos(a);
|
||||
|
||||
const body = new Mesh(boxGeometry(p.x, p.z, p.w, p.d, y, y + p.h, p.rot, 1.2), holz);
|
||||
body.castShadow = true; body.receiveShadow = true;
|
||||
g.add(body);
|
||||
|
||||
// Deckel mit leichtem Ueberstand
|
||||
const roof = new Mesh(
|
||||
boxGeometry(p.x, p.z, p.w + 0.16, p.d + 0.16, y + p.h, y + p.h + 0.09, p.rot, 2),
|
||||
boxGeometry(p.x + ux * 0.03, p.z + uz * 0.03, p.w + 0.12, p.d + 0.14, y + p.h, y + p.h + 0.06, p.rot, 2),
|
||||
material(tex.beton(), { color: 0x6a6a66, roughness: 0.9 }),
|
||||
);
|
||||
roof.castShadow = true;
|
||||
g.add(roof);
|
||||
// Tuer
|
||||
const door = new Mesh(
|
||||
new BoxGeometry(0.78, 1.95, 0.04),
|
||||
new MeshStandardMaterial({ color: 0x3b3934, roughness: 0.6 }),
|
||||
);
|
||||
door.position.set(p.x, y + 0.98, p.z - p.d / 2 - 0.02);
|
||||
g.add(door);
|
||||
|
||||
// Zwei Tuerfluegel auf der Laengsseite, leicht vor der Schrankfront
|
||||
const front = p.w / 2 + 0.015;
|
||||
const parts: BufferGeometry[] = [];
|
||||
for (const s of [-1, 1]) {
|
||||
const off = s * (p.d / 4);
|
||||
parts.push(boxGeometry(
|
||||
p.x - ux * front + vx * off, p.z - uz * front + vz * off,
|
||||
0.03, p.d / 2 - 0.03, y + 0.06, y + p.h - 0.06, p.rot, 1.2,
|
||||
));
|
||||
}
|
||||
const doors = new Mesh(mergeGeometries(parts), dunkel);
|
||||
doors.castShadow = true;
|
||||
g.add(doors);
|
||||
|
||||
// Zwei schlanke Griffe
|
||||
const griffe: BufferGeometry[] = [];
|
||||
for (const s of [-1, 1]) {
|
||||
const off = s * 0.06;
|
||||
griffe.push(boxGeometry(
|
||||
p.x - ux * (front + 0.035) + vx * off, p.z - uz * (front + 0.035) + vz * off,
|
||||
0.025, 0.025, y + p.h * 0.45, y + p.h * 0.62, p.rot, 1,
|
||||
));
|
||||
}
|
||||
g.add(new Mesh(mergeGeometries(griffe), metall));
|
||||
return g;
|
||||
}
|
||||
|
||||
/** Spielhaus durch Zimmermann: Podest, Bretterwaende, Gelaender, Pultdach. */
|
||||
/**
|
||||
* Spielhaus des Zimmermanns: ein Stelzenhaus. Der Plan zeigt ein Podest von
|
||||
* 1.81 x 2.70 m mit der Kote 1.47 - also rund 0.9 m ueber dem Gelaende - und
|
||||
* darauf an der Ostseite eine Huette von 1.01 x 2.70 m. Der westliche Teil
|
||||
* des Podests bleibt offene Plattform mit Bruestung und Leiter.
|
||||
*/
|
||||
function spielhaus(p: Prop, y: number): Group {
|
||||
const g = new Group();
|
||||
const holz = material(tex.fassadeHolz(), { color: 0xd8b98c, roughness: 0.85 });
|
||||
@@ -122,12 +148,20 @@ function spielhaus(p: Prop, y: number): Group {
|
||||
const plattW = p.w - hutW; // offene Plattform im Westen
|
||||
const plattX = p.x - hw + plattW / 2;
|
||||
|
||||
// Unter der Huette steht ein Betonklotz - im Plan das graue Rechteck von
|
||||
// 1.01 x 2.70 m unter dem Podest. Die offene Plattform daneben steht auf
|
||||
// zwei schlanken Stuetzen.
|
||||
const klotz = new Mesh(
|
||||
boxGeometry(hutX, p.z, hutW, p.d, y - 0.45, deck - 0.06, p.rot, 2),
|
||||
material(tex.beton(), { color: 0x93918c, roughness: 0.92 }),
|
||||
);
|
||||
klotz.name = 'Betonsockel Spielhaus';
|
||||
klotz.castShadow = true; klotz.receiveShadow = true;
|
||||
g.add(klotz);
|
||||
|
||||
const trag: BufferGeometry[] = [];
|
||||
// Vier Stuetzen, etwas eingerueckt
|
||||
for (const dx of [-hw + 0.14, hw - 0.14]) {
|
||||
for (const dz of [-hd + 0.14, hd - 0.14]) {
|
||||
trag.push(boxGeometry(p.x + dx, p.z + dz, 0.11, 0.11, y - 0.35, deck - 0.06, 0, 1));
|
||||
}
|
||||
for (const dz of [-hd + 0.14, hd - 0.14]) {
|
||||
trag.push(boxGeometry(p.x - hw + 0.14, p.z + dz, 0.11, 0.11, y - 0.35, deck - 0.06, 0, 1));
|
||||
}
|
||||
// Podestboden
|
||||
trag.push(boxGeometry(p.x, p.z, p.w, p.d, deck - 0.06, deck, p.rot, 1.2));
|
||||
@@ -242,9 +276,14 @@ function bank(p: Prop, y: number): Group {
|
||||
const seat = new Mesh(boxGeometry(p.x, p.z, p.w, p.d, y + p.h - 0.09, y + p.h, p.rot, belag.tile), stein);
|
||||
seat.castShadow = true; seat.receiveShadow = true;
|
||||
g.add(seat);
|
||||
// Die Fuesse sitzen unter den Enden der Sitzplatte. Der Versatz muss der
|
||||
// gedrehten v-Achse folgen - sonst stehen sie quer daneben.
|
||||
const a = (p.rot * Math.PI) / 180;
|
||||
const vx = -Math.sin(a), vz = Math.cos(a);
|
||||
for (const s of [-1, 1]) {
|
||||
const off = s * (p.d / 2 - 0.22);
|
||||
const m = new Mesh(
|
||||
boxGeometry(p.x, p.z + (s * (p.d / 2 - 0.22)), p.w - 0.08, 0.14, y, y + p.h - 0.09, p.rot, belag.tile),
|
||||
boxGeometry(p.x + vx * off, p.z + vz * off, p.w - 0.08, 0.14, y, y + p.h - 0.09, p.rot, belag.tile),
|
||||
stein,
|
||||
);
|
||||
m.castShadow = true;
|
||||
@@ -381,6 +420,7 @@ export function buildLamps(lamps: Lamp[], height: (x: number, z: number) => numb
|
||||
color: 0xffd9a0, emissive: 0xffc26e, emissiveIntensity: 0, roughness: 0.4,
|
||||
});
|
||||
const pointLights: PointLight[] = [];
|
||||
const spots: SpotLight[] = [];
|
||||
|
||||
for (const l of lamps) {
|
||||
const y = height(l.x, l.z);
|
||||
@@ -405,6 +445,18 @@ export function buildLamps(lamps: Lamp[], height: (x: number, z: number) => numb
|
||||
pl.position.set(l.x, y + (l.kind === 'poller' ? 0.6 : 0.1), l.z);
|
||||
pointLights.push(pl);
|
||||
g.add(pl);
|
||||
|
||||
if (l.kind === 'boden') {
|
||||
// Bodenleuchten sind Strahler: sie leuchten die Travertinstelen und die
|
||||
// Ziergraeser darueber von unten an. Ohne Schatten, sonst wird die
|
||||
// Abendszene mit 11 Strahlern zu teuer.
|
||||
const sp = new SpotLight(0xffd6a2, 0, 5.5, 0.55, 0.55, 1.4);
|
||||
sp.position.set(l.x, y + 0.04, l.z);
|
||||
sp.target.position.set(l.x, y + 3.0, l.z);
|
||||
g.add(sp);
|
||||
g.add(sp.target);
|
||||
spots.push(sp);
|
||||
}
|
||||
group.add(g);
|
||||
}
|
||||
|
||||
@@ -413,6 +465,7 @@ export function buildLamps(lamps: Lamp[], height: (x: number, z: number) => numb
|
||||
setOn(on: boolean) {
|
||||
glowMat.emissiveIntensity = on ? 1.6 : 0;
|
||||
for (const pl of pointLights) pl.intensity = on ? 2.2 : 0;
|
||||
for (const sp of spots) sp.intensity = on ? 5.0 : 0;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+47
-13
@@ -6,7 +6,7 @@
|
||||
* statt Blattgeometrie - das bleibt auch auf dem Handy fluessig.
|
||||
*/
|
||||
import {
|
||||
BoxGeometry, BufferAttribute, BufferGeometry, Color, CylinderGeometry, DoubleSide,
|
||||
BufferAttribute, BufferGeometry, Color, CylinderGeometry, DoubleSide,
|
||||
Group, IcosahedronGeometry, InstancedMesh, Matrix4, Mesh, MeshStandardMaterial,
|
||||
Object3D, Quaternion, Vector3,
|
||||
} from 'three';
|
||||
@@ -67,12 +67,43 @@ function blob(
|
||||
return g;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein Grashalm: schmales Band, das sich nach oben verjuengt und ueberhaengt.
|
||||
* Fusspunkt im Ursprung, Hoehe rund 1.10 m - Lage und Neigung kommen aus der
|
||||
* Instanzmatrix, damit die Normalen nicht verzerrt werden.
|
||||
*/
|
||||
function halmGeometry(): BufferGeometry {
|
||||
const SEG = 6, H = 1.10, W = 0.026;
|
||||
const pos: number[] = [];
|
||||
const uv: number[] = [];
|
||||
const pt = (i: number) => {
|
||||
const f = i / SEG;
|
||||
const y = H * f;
|
||||
// Ueberhang: der Halm biegt sich mit dem Quadrat der Hoehe nach vorn.
|
||||
const x = 0.42 * H * f * f;
|
||||
const w = W * (1 - f * 0.92);
|
||||
return { x, y, w };
|
||||
};
|
||||
for (let i = 0; i < SEG; i++) {
|
||||
const a = pt(i), b2 = pt(i + 1);
|
||||
pos.push(a.x, a.y, -a.w, a.x, a.y, a.w, b2.x, b2.y, b2.w);
|
||||
pos.push(a.x, a.y, -a.w, b2.x, b2.y, b2.w, b2.x, b2.y, -b2.w);
|
||||
const v0 = i / SEG, v1 = (i + 1) / SEG;
|
||||
uv.push(0, v0, 1, v0, 1, v1, 0, v0, 1, v1, 0, v1);
|
||||
}
|
||||
const g = new BufferGeometry();
|
||||
g.setAttribute('position', new BufferAttribute(new Float32Array(pos), 3));
|
||||
g.setAttribute('uv', new BufferAttribute(new Float32Array(uv), 2));
|
||||
g.computeVertexNormals();
|
||||
return g;
|
||||
}
|
||||
|
||||
const LAUB = {
|
||||
parrotia: 0x76944f,
|
||||
apfel: 0x7e9c52,
|
||||
eibe: 0x466b3a,
|
||||
strauch: 0x6b8f45,
|
||||
gras: 0xa3b45c,
|
||||
gras: 0x9aad5b,
|
||||
kraut: 0x8ba455,
|
||||
liguster: 0x557a37,
|
||||
};
|
||||
@@ -437,24 +468,27 @@ export function buildVegetation(
|
||||
}), 'Beerensträucher');
|
||||
if (strauchMesh) group.add(strauchMesh);
|
||||
|
||||
// --- Graeser: Buendel schmaler, leicht geneigter Halme -------------------
|
||||
// --- Ziergraeser --------------------------------------------------------
|
||||
// Im Plan stehen zwischen den Travertinstelen kleine gruene Punkte. Das
|
||||
// sind keine Polster, sondern Graeser, die an den Stelen hochwachsen und
|
||||
// abends von den Bodenleuchten angestrahlt werden. Ein Halm ist ein
|
||||
// schmales, sich verjuengendes Band, das nach oben ueberhaengt.
|
||||
const graeser = plants.filter((p) => p.type === 'gras' && p.x !== undefined);
|
||||
if (graeser.length) {
|
||||
// Ein Halm als Einheitsgeometrie mit Fusspunkt im Ursprung; Neigung und
|
||||
// Position kommen aus der Instanzmatrix, nicht aus einer Mesh-Rotation.
|
||||
const blade = new BoxGeometry(0.022, 1, 0.022);
|
||||
blade.translate(0, 0.5, 0);
|
||||
const mesh = new InstancedMesh(blade, foliage(LAUB.gras, 0.98), graeser.length * 9);
|
||||
const halm = halmGeometry();
|
||||
const mesh = new InstancedMesh(halm, foliage(LAUB.gras, 0.96, false), graeser.length * 16);
|
||||
(mesh.material as MeshStandardMaterial).side = DoubleSide;
|
||||
const dummy = new Object3D();
|
||||
let k = 0;
|
||||
for (const p of graeser) {
|
||||
const y = height(p.x!, p.z!);
|
||||
for (let i = 0; i < 9; i++) {
|
||||
for (let i = 0; i < 16; i++) {
|
||||
const a = r() * Math.PI * 2;
|
||||
const rad = (p.r ?? 0.12) * r() * 0.9;
|
||||
const rad = (p.r ?? 0.12) * r() * 0.8;
|
||||
dummy.position.set(p.x! + Math.cos(a) * rad, y, p.z! + Math.sin(a) * rad);
|
||||
dummy.rotation.set((r() - 0.5) * 0.45, r() * Math.PI, (r() - 0.5) * 0.45);
|
||||
dummy.scale.set(1, 0.42 + r() * 0.4, 1);
|
||||
dummy.rotation.set(0, a + (r() - 0.5) * 1.2, 0);
|
||||
const s = 0.85 + r() * 0.55;
|
||||
dummy.scale.set(0.75 + r() * 0.5, s, 1);
|
||||
dummy.updateMatrix();
|
||||
mesh.setMatrixAt(k++, dummy.matrix);
|
||||
}
|
||||
@@ -462,7 +496,7 @@ export function buildVegetation(
|
||||
mesh.count = k;
|
||||
mesh.instanceMatrix.needsUpdate = true;
|
||||
mesh.castShadow = true;
|
||||
mesh.name = 'Gräser';
|
||||
mesh.name = 'Ziergräser';
|
||||
group.add(mesh);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -147,7 +147,7 @@ export interface Building {
|
||||
parapet: number;
|
||||
outline: Vec2[];
|
||||
loggia: { x0: number; z0: number; x1: number; z1: number; ceiling: number };
|
||||
canopy: { a: number; b: number; depth: number; y: number; thickness: number };
|
||||
canopy: { face: 'north' | 'south' | 'east' | 'west'; a: number; b: number; depth: number; y: number; thickness: number };
|
||||
openings: Opening[];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user