feat(world): Fahrbahnmulde, Bordsteinwand und Markierungen bauen
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3d39a9bd66
commit
ca4f7161af
2 files changed
+255
-16
No files matched your search
+143
-4
@@ -14,6 +14,30 @@ const BRIDGE_THICKNESS = 0.6;
|
||||
const BRIDGE_OFFSET = SURFACE_OFFSET;
|
||||
export const SURFACE_KINDS = ["sand", "park", "urban", "field", "court", "pool", "parking"];
|
||||
|
||||
// Die Fahrbahn liegt in einer Mulde, statt dass der Gehweg auf einem Sockel läge (Entwurf
|
||||
// Teil 2, "Die Mulde"): Ein erhöhter Gehweg hätte überall dort eine Kante ins Nichts, wo kein
|
||||
// Haus anschließt — an jedem Park, jeder Baulücke, jedem Strandzugang. So bleibt die Welt auf
|
||||
// y = 0, und der Bordstein ist die Wand zwischen Gehweg und Fahrbahn.
|
||||
export const CARRIAGEWAY_DEPTH = 0.15;
|
||||
export const KERB_TOP = 0.20; // sichtbare Breite der Bordstein-Oberkante
|
||||
// Die Oberkante liegt 1 cm über den Belägen statt auf y = 0: Der Gehweg beginnt genau an der
|
||||
// Fahrbahnkante und liegt auf SURFACE_OFFSET — eine Oberkante auf y = 0 deckte er vollständig zu.
|
||||
export const KERB_HEIGHT = SURFACE_OFFSET + 0.01;
|
||||
// Die Wand steht 1 cm in der Fahrbahn: Auf der Linie selbst fiele sie genau auf die Lochwand der
|
||||
// Bodenplatte (extrudePolygons baut auch für Löcher Wände), und zwei deckungsgleiche Flächen
|
||||
// flimmern im Tiefenpuffer.
|
||||
export const KERB_INSET = 0.01;
|
||||
const KERB_PROBE = 0.3; // Abstand der Proben links und rechts einer Bordsteinlinie
|
||||
export const MARKING_OFFSET = 0.01; // Markierung über der Fahrbahn
|
||||
export const MARKING_COLORS = {
|
||||
gelb_gestrichelt: [0.94, 0.75, 0.13],
|
||||
gelb_doppelt: [0.94, 0.75, 0.13],
|
||||
weiss_gestrichelt: [0.94, 0.94, 0.94],
|
||||
weiss_rand: [0.94, 0.94, 0.94],
|
||||
haltelinie: [0.94, 0.94, 0.94],
|
||||
zebra: [0.94, 0.94, 0.94],
|
||||
};
|
||||
|
||||
/** Pastellton je Gebäude, reproduzierbar aus seiner Lage.
|
||||
*
|
||||
* Der Ton hängt am ersten Punkt des Außenrings, nicht an der Listenposition. Welcher Punkt das
|
||||
@@ -129,13 +153,128 @@ export function extrudePolygons(polygons, { base = 0, top = 10, uvScale = 4, tin
|
||||
return pos.length ? toGeometry(pos, idx, uv, tint ? color : null) : null;
|
||||
}
|
||||
|
||||
/** Liegt (x, z) in einer der Flächen? Gerade-ungerade über alle Ringe, ein Loch zählt also als
|
||||
* außen. Die Flächen eines Chunks überlappen sich nicht, sie sind Stücke einer Vereinigung. */
|
||||
function insideAny(x, z, polys) {
|
||||
let inside = false;
|
||||
for (const poly of polys) {
|
||||
for (const ring of poly) {
|
||||
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
||||
const [xi, zi] = ring[i];
|
||||
const [xj, zj] = ring[j];
|
||||
if ((zi > z) !== (zj > z) && x < ((xj - xi) * (z - zi)) / (zj - zi) + xi) inside = !inside;
|
||||
}
|
||||
}
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
/** Auf welcher Seite einer Bordsteinlinie liegt die Fahrbahn: +1 in Richtung der Normale
|
||||
* (-dz, dx), -1 gegenüber, 0, wenn es sich nicht entscheiden lässt.
|
||||
*
|
||||
* Die Laufrichtung der Linie sagt darüber nichts: emit.py legt jede Linie auf eine feste Richtung
|
||||
* fest, damit die Ausgabe nicht vom Rechenweg abhängt. Ein Bordsteinstück ist aber ein
|
||||
* zusammenhängendes Stück Fahrbahnrand, die Fahrbahn liegt also auf seiner ganzen Länge auf
|
||||
* derselben Seite. Geprüft wird Abschnitt für Abschnitt, bis genau eine der beiden Proben in der
|
||||
* Fahrbahn liegt — an der Chunkgrenze kann eine Probe jenseits des Chunks landen, wo es keine
|
||||
* Fahrbahnfläche dieses Chunks gibt. */
|
||||
function carriagewaySide(line, carriageway) {
|
||||
for (let i = 0; i + 1 < line.length; i++) {
|
||||
const [x0, z0] = line[i];
|
||||
const [x1, z1] = line[i + 1];
|
||||
const len = Math.hypot(x1 - x0, z1 - z0);
|
||||
if (len < 1e-6) continue;
|
||||
const mx = (x0 + x1) / 2, mz = (z0 + z1) / 2;
|
||||
const px = (-(z1 - z0) / len) * KERB_PROBE, pz = ((x1 - x0) / len) * KERB_PROBE;
|
||||
const links = insideAny(mx + px, mz + pz, carriageway);
|
||||
const rechts = insideAny(mx - px, mz - pz, carriageway);
|
||||
if (links !== rechts) return links ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Bordstein: senkrechte Wand aus der Mulde bis knapp über die Beläge, dazu eine schmale
|
||||
* Oberkante auf der Gehwegseite.
|
||||
*
|
||||
* Die Oberkante ist kein Schmuck: Ohne sie endet der Bordstein von oben gesehen als
|
||||
* Messerschneide zwischen Gehwegbelag und Wand, und aus der Ferne verschwindet er ganz. Auf
|
||||
* welche Seite sie gehört, bestimmt carriagewaySide() an den Fahrbahnflächen desselben Chunks —
|
||||
* auf der Fahrbahnseite hinge sie als Sims über der Mulde. Lässt sich die Seite nicht bestimmen,
|
||||
* entsteht nur die Wand, auf der Linie selbst. Die Wand bekommt in materials.js DoubleSide. */
|
||||
export function kerbGeometry(lines, carriageway = [],
|
||||
{ depth = CARRIAGEWAY_DEPTH, height = KERB_HEIGHT, top = KERB_TOP,
|
||||
inset = KERB_INSET } = {}) {
|
||||
const pos = [], idx = [], uv = [];
|
||||
for (const line of lines) {
|
||||
const side = carriagewaySide(line, carriageway);
|
||||
for (let i = 0; i + 1 < line.length; i++) {
|
||||
const [x0, z0] = line[i];
|
||||
const [x1, z1] = line[i + 1];
|
||||
const dx = x1 - x0, dz = z1 - z0;
|
||||
const len = Math.hypot(dx, dz);
|
||||
if (len < 1e-6) continue;
|
||||
// Einheitsvektor zur Fahrbahn hin; (0, 0), wenn die Seite unbekannt ist.
|
||||
const cx = (-dz / len) * side, cz = (dx / len) * side;
|
||||
const ax = x0 + cx * inset, az = z0 + cz * inset;
|
||||
const bx = x1 + cx * inset, bz = z1 + cz * inset;
|
||||
const w = pos.length / 3;
|
||||
pos.push(ax, -depth, az, bx, -depth, bz, bx, height, bz, ax, height, az);
|
||||
uv.push(0, 0, len, 0, len, depth + height, 0, depth + height);
|
||||
idx.push(w, w + 1, w + 2, w, w + 2, w + 3);
|
||||
if (side === 0) continue;
|
||||
const t = pos.length / 3;
|
||||
pos.push(ax, height, az, bx, height, bz,
|
||||
x1 - cx * top, height, z1 - cz * top, x0 - cx * top, height, z0 - cz * top);
|
||||
uv.push(0, 0, len, 0, len, top + inset, 0, top + inset);
|
||||
// Nach oben zeigen: Der Umlaufsinn des Vierecks hängt an der Seite.
|
||||
if (side > 0) idx.push(t, t + 1, t + 2, t, t + 2, t + 3);
|
||||
else idx.push(t, t + 2, t + 1, t, t + 3, t + 2);
|
||||
}
|
||||
}
|
||||
return pos.length ? toGeometry(pos, idx, uv) : null;
|
||||
}
|
||||
|
||||
/** Markierungen als flache Vierecke in einem Netz, Farbe je Art als Vertexfarbe.
|
||||
*
|
||||
* Über einer Brücke liegt die Fahrbahn im Deck verborgen: Sie liegt überall auf
|
||||
* -CARRIAGEWAY_DEPTH, das Deck reicht bis BRIDGE_OFFSET. Eine Markierung, deren Eckpunkte alle in
|
||||
* einer Brückenfläche desselben Chunks liegen, kommt deshalb auf das Deck. Liegt sie nur
|
||||
* teilweise darauf, bleibt sie in der Mulde und verschwindet auf der Brücke im Deck — auf
|
||||
* Deckhöhe schwebte ihr übriger Teil 17 cm über der Fahrbahn. */
|
||||
export function markingGeometry(markings, bridges = []) {
|
||||
const pos = [], idx = [], uv = [], color = [];
|
||||
for (const mark of markings) {
|
||||
const tint = MARKING_COLORS[mark.art] ?? MARKING_COLORS.weiss_rand;
|
||||
const ring = clockwise(openRing(mark.poly[0] ?? []));
|
||||
if (ring.length < 3) continue;
|
||||
const onDeck = bridges.length > 0 && ring.every(([x, z]) => insideAny(x, z, bridges));
|
||||
const y = (onDeck ? BRIDGE_OFFSET : -CARRIAGEWAY_DEPTH) + MARKING_OFFSET;
|
||||
const base = pos.length / 3;
|
||||
for (const [x, z] of ring) {
|
||||
pos.push(x, y, z);
|
||||
uv.push(x, z);
|
||||
color.push(...tint);
|
||||
}
|
||||
const points = ring.map(([x, z]) => new Vector2(x, z));
|
||||
for (const tri of ShapeUtils.triangulateShape(points, [])) {
|
||||
idx.push(base + tri[0], base + tri[2], base + tri[1]);
|
||||
}
|
||||
}
|
||||
return pos.length ? toGeometry(pos, idx, uv, color) : null;
|
||||
}
|
||||
|
||||
export function buildChunkGeometry(chunk) {
|
||||
const roads = chunk.roads.filter((r) => !r.bridge).map((r) => r.poly);
|
||||
const bridges = chunk.roads.filter((r) => r.bridge).map((r) => r.poly);
|
||||
const out = {
|
||||
ground: extrudePolygons(chunk.land, { base: -GROUND_THICKNESS, top: 0, uvScale: 8 }),
|
||||
roads: flatPolygons(roads, { y: ROAD_OFFSET, uvScale: 8 }),
|
||||
bridges: extrudePolygons(bridges, { base: -BRIDGE_THICKNESS, top: BRIDGE_OFFSET, uvScale: 8 }),
|
||||
// Fahrbahn und Bordstein sind echte Geometrie in der Mulde, keine Auflagen.
|
||||
carriageway: flatPolygons((chunk.carriageway ?? []).map((c) => c.poly),
|
||||
{ y: -CARRIAGEWAY_DEPTH, uvScale: 8 }),
|
||||
curb: kerbGeometry(chunk.curb ?? [], (chunk.carriageway ?? []).map((c) => c.poly)),
|
||||
sidewalk: flatPolygons((chunk.sidewalk ?? []).map((s) => s.poly), { y: SURFACE_OFFSET, uvScale: 4 }),
|
||||
markings: markingGeometry(chunk.markings ?? [], (chunk.bridges ?? []).map((b) => b.poly)),
|
||||
lots: flatPolygons((chunk.lots ?? []).map((l) => l.poly), { y: SURFACE_OFFSET, uvScale: 8 }),
|
||||
bridges: extrudePolygons((chunk.bridges ?? []).map((b) => b.poly),
|
||||
{ base: -BRIDGE_THICKNESS, top: BRIDGE_OFFSET, uvScale: 8 }),
|
||||
buildings: buildBuildings(chunk.buildings),
|
||||
// Parkwege (docs/superpowers/specs/2026-09-15-flamingo-park-design.md): flach wie Straßen, Textur in
|
||||
// echter Größe — Poly Haven misst concrete_pavement mit 1,8 × 1,8 m.
|
||||
|
||||
Reference in new issue
Block a user