From b79ccaa1e0eced5894c9197ddaffbf9351f4e8d9 Mon Sep 17 00:00:00 2001 From: Mario Date: Thu, 3 Sep 2026 18:28:17 +0200 Subject: [PATCH] Weltkarte Wald: Outline-Fill-Mismatch fix + Kleinstpolygon-Filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - buildForestOutlinePaths: bilineare Interpolation statt NN → Outline liegt exakt auf der gleichen Schwelle wie die Füllung in computeForestPixels - Perimeter-Filter: Pfade mit Umfang < 60m² werden verworfen (Artefakte aus 1-2 isolierten Bäumen) - Threshold 2.0 → 3.0: 2 Bäume ergeben max. Density=2, liegen damit unter der Schwelle → kein Wald-Ring mehr bei Kleingruppen Co-Authored-By: Claude Sonnet 4.6 --- .../de/blight/editor/ui/WorldMapView.java | 43 +++++++++++++++---- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/blight-editor/src/main/java/de/blight/editor/ui/WorldMapView.java b/blight-editor/src/main/java/de/blight/editor/ui/WorldMapView.java index 7da363d..f2b90e7 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/WorldMapView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/WorldMapView.java @@ -358,10 +358,10 @@ public class WorldMapView extends VBox { RENDER_SIZE, RENDER_SIZE, System.currentTimeMillis() - t1); float[] fDensity = computeForestDensity(model); - int[] bakedFstPx = computeForestPixels(model, fDensity, 2.0f); + int[] bakedFstPx = computeForestPixels(model, fDensity, 3.0f); int[] cliffPx = WorldMapRenderer.buildCliffOverlay(model.mapData(), model.voxelSurface()); List newCliffPaths = buildCliffPaths(model.mapData(), model.voxelSurface()); - List newForestPaths = buildForestOutlinePaths(fDensity, 512, 2.0f); + List newForestPaths = buildForestOutlinePaths(fDensity, 512, 3.0f); // Hires-Chunks im Hintergrund generieren (nach dem UI-Update, nicht-blockierend) final WorldMapRenderModel chunkModel = model; @@ -1119,16 +1119,41 @@ public class WorldMapView extends VBox { private static List buildForestOutlinePaths(float[] density, int fdSize, float threshold) { // Density-Map: mz=0 = Norden (WH-pm.z()=0 → pm.z()=+WH), mz=fdSize-1 = Süden. // Marching Squares: mz=0 → msW=-WH (Süden). → Z muss gespiegelt werden. - float[] mask = new float[fdSize * fdSize]; - for (int mz = 0; mz < fdSize; mz++) { - int dz = fdSize - 1 - mz; // Süden im MS ↔ Süden in der Density-Map - for (int mx = 0; mx < fdSize; mx++) { - mask[mz * fdSize + mx] = density[dz * fdSize + mx] >= threshold ? -1f : 1f; + // Bilinear-Interpolation wie in computeForestPixels → Outline liegt exakt auf Fill-Grenze. + final int SIZE = SEA_MASK_SIZE; + float[] mask = new float[SIZE * SIZE]; + for (int mz = 0; mz < SIZE; mz++) { + // Z-Flip: mz=0 (Süd im MS) → Süd in Density (dz=fdSize-1) + float fz = (float)(SIZE - 1 - mz) / (SIZE - 1) * (fdSize - 1); + int dz0 = (int) fz, dz1 = Math.min(dz0 + 1, fdSize - 1); + float tz = fz - dz0; + for (int mx = 0; mx < SIZE; mx++) { + float fx = (float) mx / (SIZE - 1) * (fdSize - 1); + int dx0 = (int) fx, dx1 = Math.min(dx0 + 1, fdSize - 1); + float tx = fx - dx0; + float d = (density[dz0 * fdSize + dx0] * (1 - tx) + density[dz0 * fdSize + dx1] * tx) * (1 - tz) + + (density[dz1 * fdSize + dx0] * (1 - tx) + density[dz1 * fdSize + dx1] * tx) * tz; + mask[mz * SIZE + mx] = d >= threshold ? -1f : 1f; } } List segs = new ArrayList<>(); - marchingSquares(mask, fdSize, 0.5f, null, null, segs); - return buildSeaCoastPaths(segs.toArray(new float[0][])); + marchingSquares(mask, SIZE, 0.5f, null, null, segs); + List paths = buildSeaCoastPaths(segs.toArray(new float[0][])); + + // Kleinstpolygone filtern: weniger als ~100m Umfang = Artefakt (z.B. 2 isolierte Bäume) + final float MIN_PERIMETER_SQ = 60f * 60f; + return paths.stream().filter(p -> pathPerimeterSq(p) >= MIN_PERIMETER_SQ) + .collect(java.util.stream.Collectors.toList()); + } + + private static float pathPerimeterSq(float[][] path) { + float len = 0f; + for (int i = 0; i < path.length; i++) { + float[] a = path[i], b = path[(i + 1) % path.length]; + float dx = b[0] - a[0], dz = b[1] - a[1]; + len += (float) Math.sqrt(dx * dx + dz * dz); + } + return len * len; } // Entfernt kollineare Zwischenpunkte (lange H/V-Läufe → ein Segment).