import { BufferGeometry, Float32BufferAttribute, ShapeUtils, Vector2 } from "three"; const GROUND_THICKNESS = 1.0; const SURFACE_OFFSET = 0.02; // Sand, Park und Stadtfläche liegen knapp über dem Boden const ROAD_OFFSET = 0.03; // Straßen wiederum knapp über den Flächen const BRIDGE_THICKNESS = 0.6; export const SURFACE_KINDS = ["sand", "park", "urban"]; /** Pastellton je Gebäude, reproduzierbar aus seiner Lage. */ function buildingTint(poly) { const [x, z] = poly[0][0]; const h = Math.abs(Math.sin(x * 12.9898 + z * 78.233) * 43758.5453) % 1; return [0.82 + 0.18 * h, 0.80 + 0.20 * ((h * 3) % 1), 0.78 + 0.22 * ((h * 7) % 1)]; } // Die extrahierten Ringe schließen sich selbst: der letzte Punkt wiederholt den ersten // (geprüft über alle 144 Chunks — ausnahmslos). ShapeUtils.triangulateShape entfernt diesen // Schlusspunkt intern über Vector2#equals und zählt seine Indizes danach neu durch. Bauen wir // die Positionsliste dagegen aus dem rohen, geschlossenen Ring auf, verschiebt sich jeder Index // ab dem ersten Loch um eins gegenüber dieser internen Nummerierung, und Dreiecke zeigen auf // falsche Punkte. Deshalb hier einmal vorab entfernen, bevor irgendetwas anderes mit dem Ring // passiert — Ringe ohne Schlusspunkt (wie in den Tests) bleiben dabei unverändert. // // Ruling 47: Länge zuerst prüfen, dann destrukturieren. Ein Ring mit 0 oder 1 Punkten hat // keinen von "erstem" verschiedenen "letzten" Punkt, den man entdoppeln könnte — vorher // stand die Längenprüfung erst im return, sodass ring[0] bei einem leeren Ring bereits // undefined war und die Destrukturierung warf, statt wie der Rest dieses Moduls zu einem // harmlosen leeren Ergebnis zu degradieren. const openRing = (ring) => { if (ring.length <= 1) return ring; const [x0, z0] = ring[0]; const [xn, zn] = ring[ring.length - 1]; return x0 === xn && z0 === zn ? ring.slice(0, -1) : ring; }; const signedArea = (ring) => ring.reduce((sum, [x0, z0], i) => { const [x1, z1] = ring[(i + 1) % ring.length]; return sum + x0 * z1 - x1 * z0; }, 0) / 2; /** Ringe im Uhrzeigersinn, damit Wände nach außen zeigen. */ const clockwise = (ring) => (signedArea(ring) > 0 ? [...ring].reverse() : ring); function toGeometry(pos, idx, uv, color) { const geo = new BufferGeometry(); geo.setAttribute("position", new Float32BufferAttribute(pos, 3)); geo.setAttribute("uv", new Float32BufferAttribute(uv, 2)); if (color) geo.setAttribute("color", new Float32BufferAttribute(color, 3)); geo.setIndex(idx); geo.computeVertexNormals(); return geo; } export function flatPolygons(polygons, { y = 0, uvScale = 8 } = {}) { const pos = [], idx = [], uv = []; for (const raw of polygons) { // raw[0] fehlt, wenn die Fläche ganz ohne Ringe übergeben wird (raw = []) — dann // wie einen leeren Außenring behandeln, statt in openRing() auf undefined zuzugreifen. const outer = clockwise(openRing(raw[0] ?? [])); // clockwise() gibt denselben Array zurueck, wenn der Ring schon passt — deshalb // vor dem Umdrehen kopieren, sonst wird die Eingabe des Aufrufers veraendert. const holes = raw.slice(1).map((hole) => [...clockwise(openRing(hole))].reverse()); const base = pos.length / 3; for (const ring of [outer, ...holes]) { for (const [x, z] of ring) { pos.push(x, y, z); uv.push(x / uvScale, z / uvScale); } } const toPoints = (ring) => ring.map(([x, z]) => new Vector2(x, z)); for (const tri of ShapeUtils.triangulateShape(toPoints(outer), holes.map(toPoints))) { idx.push(base + tri[0], base + tri[2], base + tri[1]); // nach oben gedreht } } return pos.length ? toGeometry(pos, idx, uv) : null; } export function extrudePolygons(polygons, { base = 0, top = 10, uvScale = 4, tint = null } = {}) { const pos = [], idx = [], uv = [], color = []; for (const raw of polygons) { // Jede Fläche ist eine Liste von Ringen: Außenring zuerst, danach die Löcher. // triangulateShape erwartet die Löcher gegenläufig zum Außenring. // raw[0] fehlt, wenn die Fläche ganz ohne Ringe übergeben wird (raw = []) — dann // wie einen leeren Außenring behandeln, statt in openRing() auf undefined zuzugreifen. const outer = clockwise(openRing(raw[0] ?? [])); // clockwise() gibt denselben Array zurueck, wenn der Ring schon passt — deshalb // vor dem Umdrehen kopieren, sonst wird die Eingabe des Aufrufers veraendert. const holes = raw.slice(1).map((hole) => [...clockwise(openRing(hole))].reverse()); for (const ring of [outer, ...holes]) { const n = ring.length; for (let i = 0; i < n; i++) { const [x0, z0] = ring[i]; const [x1, z1] = ring[(i + 1) % n]; const len = Math.hypot(x1 - x0, z1 - z0); const b = pos.length / 3; pos.push(x0, base, z0, x1, base, z1, x1, top, z1, x0, top, z0); uv.push(0, 0, len / uvScale, 0, len / uvScale, (top - base) / uvScale, 0, (top - base) / uvScale); if (tint) for (let k = 0; k < 4; k++) color.push(...tint); idx.push(b, b + 1, b + 2, b, b + 2, b + 3); } } // Deckfläche: erst alle Punkte in derselben Reihenfolge, die triangulateShape indiziert. const b = pos.length / 3; for (const ring of [outer, ...holes]) { for (const [x, z] of ring) { pos.push(x, top, z); uv.push(x / uvScale, z / uvScale); if (tint) color.push(...tint); } } const toPoints = (ring) => ring.map(([x, z]) => new Vector2(x, z)); for (const tri of ShapeUtils.triangulateShape(toPoints(outer), holes.map(toPoints))) { idx.push(b + tri[0], b + tri[2], b + tri[1]); } } return pos.length ? toGeometry(pos, idx, uv, tint ? color : null) : 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: 0, uvScale: 8 }), buildings: buildBuildings(chunk.buildings), }; for (const kind of SURFACE_KINDS) { const rings = (chunk.surfaces ?? []).filter((s) => s.kind === kind).map((s) => s.poly); out[kind] = flatPolygons(rings, { y: SURFACE_OFFSET, uvScale: 8 }); } return out; } function buildBuildings(buildings) { const parts = buildings.map((b) => extrudePolygons([b.poly], { base: 0, top: b.height, uvScale: 4, tint: buildingTint(b.poly) })); return parts.filter(Boolean).reduce((acc, geo) => (acc ? mergeInto(acc, geo) : geo), null); } function mergeInto(a, b) { const offset = a.getAttribute("position").count; const pos = [...a.getAttribute("position").array, ...b.getAttribute("position").array]; const uv = [...a.getAttribute("uv").array, ...b.getAttribute("uv").array]; const ca = a.getAttribute("color"), cb = b.getAttribute("color"); const color = ca && cb ? [...ca.array, ...cb.array] : null; const idx = [...a.getIndex().array, ...[...b.getIndex().array].map((i) => i + offset)]; return toGeometry(pos, idx, uv, color); }