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:
@@ -404,7 +404,10 @@ public class WorldMapView extends VBox {
|
||||
|
||||
Platform.runLater(() -> statusLbl.setText("Berechne Cave-Maske…"));
|
||||
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[]> newBuildingFootprints = filterBuildingsByCave(allBuildingFootprints, 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 */ }
|
||||
|
||||
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];
|
||||
for (int i = 0; i < caveMask.length; i++) h[i] = caveMask[i] ? -1f : 1f;
|
||||
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). */
|
||||
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) {
|
||||
int px = Math.round((wx + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (CAVE_MASK_SIZE - 1));
|
||||
int pz = Math.round((wz + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (CAVE_MASK_SIZE - 1));
|
||||
px = Math.max(0, Math.min(CAVE_MASK_SIZE - 1, px));
|
||||
pz = Math.max(0, Math.min(CAVE_MASK_SIZE - 1, pz));
|
||||
return mask[pz * CAVE_MASK_SIZE + px];
|
||||
int SIZE = (int) Math.round(Math.sqrt(mask.length));
|
||||
int px = Math.round((wx + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SIZE - 1));
|
||||
int pz = Math.round((wz + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SIZE - 1));
|
||||
px = Math.max(0, Math.min(SIZE - 1, 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) {
|
||||
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};
|
||||
}
|
||||
|
||||
@@ -1112,26 +1133,28 @@ private static List<float[]> computeBuildingFootprints(WorldMapRenderModel model
|
||||
|
||||
/**
|
||||
* Prüft ob ein Knoten unter der Erde liegt.
|
||||
* Bedingung: Das höchste Solid-Voxel in dieser X,Z-Spalte liegt ≥ PATH_UNDERGROUND_CLEARANCE
|
||||
* über der Knoten-Y — d.h. es gibt Material direkt über dem Weg (Tunneldecke).
|
||||
* 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.
|
||||
* Primär: Cave-Footprint-Maske (2048px, erkennt auch Sculpted-Mesh-Tunnel).
|
||||
* Fallback: VoxelSurface Y-Check (nur wenn VoxelChunk-Material direkt über dem Knoten liegt).
|
||||
*/
|
||||
private static boolean isNodeUnderground(float[] node, float[] voxelSurface,
|
||||
@SuppressWarnings("unused") boolean[] caveMask) {
|
||||
if (node.length < 3) return false;
|
||||
private static boolean isNodeUnderground(float[] node, float[] voxelSurface, boolean[] caveMask) {
|
||||
if (node.length < 2) 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];
|
||||
if (Float.isNaN(wy)) return false;
|
||||
int VS = WorldMapRenderer.VOXEL_SURFACE_RES;
|
||||
float WH = WorldMapRenderer.WORLD_HALF, WS = WorldMapRenderer.WORLD_SIZE;
|
||||
int px = Math.round((node[0] + WH) / WS * (VS - 1));
|
||||
int pz = 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));
|
||||
int px = Math.max(0, Math.min(VS - 1, Math.round((node[0] + WH) / WS * (VS - 1))));
|
||||
int pz = Math.max(0, Math.min(VS - 1, Math.round((node[1] + WH) / WS * (VS - 1))));
|
||||
float vs = voxelSurface[pz * VS + px];
|
||||
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,
|
||||
boolean[] caveMask,
|
||||
List<float[][]> surface, List<float[][]> cave) {
|
||||
@@ -1146,6 +1169,27 @@ private static List<float[]> computeBuildingFootprints(WorldMapRenderModel model
|
||||
boolean bCave = isNodeUnderground(b, voxelSurface, caveMask);
|
||||
|
||||
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);
|
||||
} else {
|
||||
float[] crossing = findPathCrossing(a, b, voxelSurface, caveMask, currentCave);
|
||||
|
||||
Reference in New Issue
Block a user