Weltkarte Wald: Outline-Fill-Mismatch fix + Kleinstpolygon-Filter

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 18:28:17 +02:00
parent 28cc89dcb1
commit b79ccaa1e0

View File

@@ -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<float[][]> newCliffPaths = buildCliffPaths(model.mapData(), model.voxelSurface());
List<float[][]> newForestPaths = buildForestOutlinePaths(fDensity, 512, 2.0f);
List<float[][]> 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<float[][]> 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<float[]> 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<float[][]> 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).