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…"));
|
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);
|
||||||
|
|||||||
Reference in New Issue
Block a user