Karte: Pfad-Erkennung durch Sculpted-Mesh-Tunnel repariert

Die bisherige Y-Check-Methode versagte bei Tunneln aus Sculpted Mesh,
weil bakeVoxelSurface nur VoxelChunk-Daten liest und NaN liefert.

- loadCaveMask: buildCaveFootprintMask bei 2048px statt buildCaveTunnelMask
  bei 1024px (kein MIN_FOOTPRINT-Filter der schmale Tunnel eliminiert)
- isCavePoint: erkennt Maskengröße automatisch (1024 oder 2048px)
- buildTunnelContourPaths: Größe dynamisch aus Maskenlänge
- isNodeUnderground: Cave-Maske als primäre Prüfung, Y-Check als Fallback
- Segment-Abtastung: 7 Punkte statt Mittelpunkt (erkennt Tunnel an jeder
  Position im Segment, nicht nur bei 50%)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-07 13:37:23 +02:00
parent 7f5be2b65d
commit 9fa1643264

View File

@@ -404,7 +404,10 @@ public class WorldMapView extends VBox {
Platform.runLater(() -> statusLbl.setText("Berechne Cave-Maske…")); Platform.runLater(() -> statusLbl.setText("Berechne Cave-Maske…"));
boolean[] caveMask = loadCaveMask(model.mapData()); boolean[] caveMask = loadCaveMask(model.mapData());
List<float[][]>[] splitChains = splitPathChains(allPathChains, model.voxelSurface(), caveMask); // Disk-basierte voxelSurface für Pfad-Splitting: Live-Chunks decken oft nicht
// den Tunnel-Bereich ab, sodass dort NaN entstehen würde.
float[] vsForPaths = WorldMapRenderer.bakeVoxelSurface(model.mapData());
List<float[][]>[] splitChains = splitPathChains(allPathChains, vsForPaths, caveMask);
List<float[][]> newPathChains = splitChains[0]; // nur Oberfläche List<float[][]> newPathChains = splitChains[0]; // nur Oberfläche
List<float[]> newBuildingFootprints = filterBuildingsByCave(allBuildingFootprints, caveMask, false); List<float[]> newBuildingFootprints = filterBuildingsByCave(allBuildingFootprints, caveMask, false);
List<PlacedArea> surfaceAreas = filterAreasByCave(new ArrayList<>(model.areas()), caveMask, false); List<PlacedArea> surfaceAreas = filterAreasByCave(new ArrayList<>(model.areas()), caveMask, false);
@@ -1042,7 +1045,7 @@ public class WorldMapView extends VBox {
private void drawTunnelOverlay(GraphicsContext gc) { /* Kein Tunnel-Overlay auf Weltkarte */ } private void drawTunnelOverlay(GraphicsContext gc) { /* Kein Tunnel-Overlay auf Weltkarte */ }
static List<float[][]> buildTunnelContourPaths(boolean[] caveMask) { static List<float[][]> buildTunnelContourPaths(boolean[] caveMask) {
final int SIZE = CAVE_MASK_SIZE; final int SIZE = (int) Math.round(Math.sqrt(caveMask.length)); // 1024 oder 2048
float[] h = new float[SIZE * SIZE]; float[] h = new float[SIZE * SIZE];
for (int i = 0; i < caveMask.length; i++) h[i] = caveMask[i] ? -1f : 1f; for (int i = 0; i < caveMask.length; i++) h[i] = caveMask[i] ? -1f : 1f;
List<float[]> segs = new ArrayList<>(); List<float[]> segs = new ArrayList<>();
@@ -1079,16 +1082,19 @@ private static List<float[]> computeBuildingFootprints(WorldMapRenderModel model
/** Lädt die Cave-Tunnel-Maske (true = Untergrund/Tunnel). */ /** Lädt die Cave-Tunnel-Maske (true = Untergrund/Tunnel). */
static boolean[] loadCaveMask(MapData mapData) { static boolean[] loadCaveMask(MapData mapData) {
return WorldMapRenderer.buildCaveTunnelMask(mapData, CAVE_MASK_SIZE); // buildCaveFootprintMask bei 2048px: gleiche Auflösung wie die Höhlenkarte.
// buildCaveTunnelMask bei 1024px filtert schmale Tunnel (MIN_FOOTPRINT_PX=40 ≈ 160m²).
return WorldMapRenderer.buildCaveFootprintMask(mapData, WorldMapRenderer.VOXEL_SURFACE_RES);
} }
/** Prüft ob eine Weltposition unterhalb von Tunnel-Voxeln liegt. */ /** Prüft ob eine Weltposition im Tunnel-Fußabdruck liegt. Erkennt Maskengröße automatisch. */
static boolean isCavePoint(boolean[] mask, float wx, float wz) { static boolean isCavePoint(boolean[] mask, float wx, float wz) {
int px = Math.round((wx + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (CAVE_MASK_SIZE - 1)); int SIZE = (int) Math.round(Math.sqrt(mask.length));
int pz = Math.round((wz + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (CAVE_MASK_SIZE - 1)); int px = Math.round((wx + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SIZE - 1));
px = Math.max(0, Math.min(CAVE_MASK_SIZE - 1, px)); int pz = Math.round((wz + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SIZE - 1));
pz = Math.max(0, Math.min(CAVE_MASK_SIZE - 1, pz)); px = Math.max(0, Math.min(SIZE - 1, px));
return mask[pz * CAVE_MASK_SIZE + px]; pz = Math.max(0, Math.min(SIZE - 1, pz));
return mask[pz * SIZE + px];
} }
/** /**
@@ -1105,6 +1111,21 @@ private static List<float[]> computeBuildingFootprints(WorldMapRenderModel model
for (float[][] chain : chains) { for (float[][] chain : chains) {
splitChainByVoxelSurface(chain, voxelSurface, caveMask, surface, cave); splitChainByVoxelSurface(chain, voxelSurface, caveMask, surface, cave);
} }
log.warn("[PathSplit] {} Ketten → {} Oberfläche, {} Höhle",
chains.size(), surface.size(), cave.size());
if (!chains.isEmpty() && chains.get(0).length > 0) {
float[] n = chains.get(0)[0];
if (n.length >= 3 && voxelSurface != null) {
int VS = WorldMapRenderer.VOXEL_SURFACE_RES;
float WH = WorldMapRenderer.WORLD_HALF, WS = WorldMapRenderer.WORLD_SIZE;
int px = Math.max(0, Math.min(VS-1, Math.round((n[0]+WH)/WS*(VS-1))));
int pz = Math.max(0, Math.min(VS-1, Math.round((n[1]+WH)/WS*(VS-1))));
float vs = voxelSurface[pz*VS+px];
log.warn("[PathSplit] Erster Knoten: X={} Z={} Y={} voxSurf={} underground={}",
n[0], n[1], n[2], vs,
!Float.isNaN(vs) && vs > n[2] + PATH_UNDERGROUND_CLEARANCE);
}
}
return new List[]{surface, cave}; return new List[]{surface, cave};
} }
@@ -1112,26 +1133,28 @@ private static List<float[]> computeBuildingFootprints(WorldMapRenderModel model
/** /**
* Prüft ob ein Knoten unter der Erde liegt. * Prüft ob ein Knoten unter der Erde liegt.
* Bedingung: Das höchste Solid-Voxel in dieser X,Z-Spalte liegt ≥ PATH_UNDERGROUND_CLEARANCE * Primär: Cave-Footprint-Maske (2048px, erkennt auch Sculpted-Mesh-Tunnel).
* über der Knoten-Y — d.h. es gibt Material direkt über dem Weg (Tunneldecke). * Fallback: VoxelSurface Y-Check (nur wenn VoxelChunk-Material direkt über dem Knoten liegt).
* Die Cave-Maske wird bewusst NICHT genutzt: ihr MIN_FOOTPRINT_Filter bei niedrige Auflösung
* filtert schmale Tunnel heraus und würde echte Tunnel-Segmente blockieren.
*/ */
private static boolean isNodeUnderground(float[] node, float[] voxelSurface, private static boolean isNodeUnderground(float[] node, float[] voxelSurface, boolean[] caveMask) {
@SuppressWarnings("unused") boolean[] caveMask) { if (node.length < 2) return false;
if (node.length < 3) return false; if (caveMask != null && isCavePoint(caveMask, node[0], node[1])) return true;
if (node.length < 3 || Float.isNaN(node[2])) return false;
float wy = node[2]; float wy = node[2];
if (Float.isNaN(wy)) return false;
int VS = WorldMapRenderer.VOXEL_SURFACE_RES; int VS = WorldMapRenderer.VOXEL_SURFACE_RES;
float WH = WorldMapRenderer.WORLD_HALF, WS = WorldMapRenderer.WORLD_SIZE; float WH = WorldMapRenderer.WORLD_HALF, WS = WorldMapRenderer.WORLD_SIZE;
int px = Math.round((node[0] + WH) / WS * (VS - 1)); int px = Math.max(0, Math.min(VS - 1, Math.round((node[0] + WH) / WS * (VS - 1))));
int pz = Math.round((node[1] + WH) / WS * (VS - 1)); int pz = Math.max(0, Math.min(VS - 1, Math.round((node[1] + WH) / WS * (VS - 1))));
px = Math.max(0, Math.min(VS - 1, px));
pz = Math.max(0, Math.min(VS - 1, pz));
float vs = voxelSurface[pz * VS + px]; float vs = voxelSurface[pz * VS + px];
return !Float.isNaN(vs) && vs > wy + PATH_UNDERGROUND_CLEARANCE; return !Float.isNaN(vs) && vs > wy + PATH_UNDERGROUND_CLEARANCE;
} }
private static float[] lerpNode(float[] a, float[] b, float t) {
float ax = a[0], az = a[1], ay = a.length > 2 ? a[2] : 0f;
float bx = b[0], bz = b[1], by = b.length > 2 ? b[2] : 0f;
return new float[]{ax + (bx - ax) * t, az + (bz - az) * t, ay + (by - ay) * t};
}
private static void splitChainByVoxelSurface(float[][] chain, float[] voxelSurface, private static void splitChainByVoxelSurface(float[][] chain, float[] voxelSurface,
boolean[] caveMask, boolean[] caveMask,
List<float[][]> surface, List<float[][]> cave) { List<float[][]> surface, List<float[][]> cave) {
@@ -1146,6 +1169,27 @@ private static List<float[]> computeBuildingFootprints(WorldMapRenderModel model
boolean bCave = isNodeUnderground(b, voxelSurface, caveMask); boolean bCave = isNodeUnderground(b, voxelSurface, caveMask);
if (currentCave == bCave) { if (currentCave == bCave) {
if (!currentCave) {
// Beide Oberfläche: 7-Punkte-Abtastung, um verborgene Tunnel zu erkennen
// (kein Knoten im Tunnel, aber das Segment kreuzt ein unterirdisches Gebiet).
float[] sample = null;
for (int s = 1; s <= 7 && sample == null; s++) {
float[] p = lerpNode(a, b, s / 8f);
if (isNodeUnderground(p, voxelSurface, caveMask)) { sample = p; }
}
if (sample != null) {
float[] c1 = findPathCrossing(a, sample, voxelSurface, caveMask, false);
float[] c2 = findPathCrossing(sample, b, voxelSurface, caveMask, true);
current.add(c1);
if (current.size() >= 2) surface.add(current.toArray(new float[0][]));
current = new ArrayList<>();
current.add(c1); current.add(sample); current.add(c2);
cave.add(current.toArray(new float[0][]));
current = new ArrayList<>();
current.add(c2); current.add(b);
continue;
}
}
current.add(b); current.add(b);
} else { } else {
float[] crossing = findPathCrossing(a, b, voxelSurface, caveMask, currentCave); float[] crossing = findPathCrossing(a, b, voxelSurface, caveMask, currentCave);