Karte: Tunnel-Rendering verbessert, Pfad-Erkennung robuster

- Pfad-Split: Sandwich-Maske (per-Spalte, fragil) ersetzt durch
  Chunk-Level-Cave-Maske + Y-Prüfung; erkennt schmalere Tunnel
  zuverlässig und vermeidet extra Disk-Scan
- Weltkarte: Tunneleingang-Halbkreise ersetzt durch gefüllte
  Fläche (semi-transparent braun) + glatte Chaikin-Kontur
  (ähnlich Wasser-Rendering)
- Höhlenkarte: smooth Kontur-Overlay (warmweiss) um die
  Tunnelfläche ergänzt, zusätzlich zu den D-Eingängen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-07 10:22:48 +02:00
parent dc7c326441
commit f29580cb3b
2 changed files with 118 additions and 124 deletions

View File

@@ -63,12 +63,13 @@ public class CaveMapView extends VBox {
private Runnable enterFullscreenCallback = null; private Runnable enterFullscreenCallback = null;
private Runnable exitFullscreenCallback = null; private Runnable exitFullscreenCallback = null;
private WritableImage mapFxImage = null; private WritableImage mapFxImage = null;
private List<float[][]> cachedPathChains = new ArrayList<>(); private List<float[][]> cachedPathChains = new ArrayList<>();
private List<float[]> tunnelEntrances = new ArrayList<>(); private List<float[]> tunnelEntrances = new ArrayList<>();
private List<PlacedArea> cachedAreas = new ArrayList<>(); private List<float[][]> tunnelContourPaths = new ArrayList<>();
private List<float[]> buildingFootprints = new ArrayList<>(); private List<PlacedArea> cachedAreas = new ArrayList<>();
private MapData cachedMapData = null; // für Export private List<float[]> buildingFootprints = new ArrayList<>();
private MapData cachedMapData = null; // für Export
private double panX = 0, panY = 0, scale = 1.0; private double panX = 0, panY = 0, scale = 1.0;
private double[] zoomLevels = null; private double[] zoomLevels = null;
@@ -297,11 +298,11 @@ public class CaveMapView extends VBox {
Platform.runLater(() -> statusLbl.setText("Berechne Cave-Maske…")); Platform.runLater(() -> statusLbl.setText("Berechne Cave-Maske…"));
boolean[] caveMask = WorldMapView.loadCaveMask(mapData); boolean[] caveMask = WorldMapView.loadCaveMask(mapData);
// Pfade: Sandwich+Y-basierte Trennung (echter Tunnel: solid→void≥3→solid) // Pfade: Cave-Maske + Y-basierte Trennung
Platform.runLater(() -> statusLbl.setText("Berechne VoxelSurface…")); Platform.runLater(() -> statusLbl.setText("Berechne VoxelSurface…"));
float[] voxelSurface = WorldMapRenderer.bakeVoxelSurface(mapData); float[] voxelSurface = WorldMapRenderer.bakeVoxelSurface(mapData);
boolean[] sandwichMask = WorldMapView.bakeSandwichMask(); List<float[][]>[] splitChains = WorldMapView.splitPathChains(allPaths, voxelSurface, caveMask);
List<float[][]>[] splitChains = WorldMapView.splitPathChains(allPaths, voxelSurface, sandwichMask); List<float[][]> newContourPaths = WorldMapView.buildTunnelContourPaths(caveMask);
List<float[][]> cavePaths = splitChains[1]; List<float[][]> cavePaths = splitChains[1];
// Gebäude + Areas: Cave-Maske (keine Y-Info) // Gebäude + Areas: Cave-Maske (keine Y-Info)
@@ -312,6 +313,7 @@ public class CaveMapView extends VBox {
mapFxImage = SwingFXUtils.toFXImage(bi, null); mapFxImage = SwingFXUtils.toFXImage(bi, null);
cachedPathChains = cavePaths; cachedPathChains = cavePaths;
tunnelEntrances = entrances; tunnelEntrances = entrances;
tunnelContourPaths = newContourPaths;
cachedAreas = caveAreas; cachedAreas = caveAreas;
buildingFootprints = caveFootprints; buildingFootprints = caveFootprints;
cachedMapData = mapData; cachedMapData = mapData;
@@ -388,7 +390,10 @@ public class CaveMapView extends VBox {
} }
gc.setImageSmoothing(true); gc.setImageSmoothing(true);
gc.drawImage(mapFxImage, panX, panY, mapFxImage.getWidth() * scale, mapFxImage.getHeight() * scale); gc.drawImage(mapFxImage, panX, panY, mapFxImage.getWidth() * scale, mapFxImage.getHeight() * scale);
if (cbEntrances.isSelected()) drawTunnelEntrancesOverlay(gc); if (cbEntrances.isSelected()) {
drawTunnelContourOverlay(gc);
drawTunnelEntrancesOverlay(gc);
}
if (cbBuildings.isSelected()) drawBuildingOverlay(gc); if (cbBuildings.isSelected()) drawBuildingOverlay(gc);
if (cbPaths.isSelected()) drawPathsOverlay(gc); if (cbPaths.isSelected()) drawPathsOverlay(gc);
if (cbAreas.isSelected()) drawAreasOverlay(gc); if (cbAreas.isSelected()) drawAreasOverlay(gc);
@@ -417,6 +422,36 @@ public class CaveMapView extends VBox {
}; };
} }
private void drawTunnelContourOverlay(GraphicsContext gc) {
if (tunnelContourPaths.isEmpty()) return;
double cW = canvas.getWidth(), cH = canvas.getHeight();
gc.save();
gc.setStroke(Color.rgb(220, 200, 160, 0.80));
gc.setLineWidth(overlayLineWidth() + 0.5);
gc.setLineDashes();
gc.setLineCap(javafx.scene.shape.StrokeLineCap.ROUND);
gc.setLineJoin(javafx.scene.shape.StrokeLineJoin.ROUND);
for (float[][] path : tunnelContourPaths) drawCavePath(gc, path, cW, cH);
gc.restore();
}
private void drawCavePath(GraphicsContext gc, float[][] path, double cW, double cH) {
if (path.length < 2) return;
double bx0 = Double.MAX_VALUE, bx1 = -Double.MAX_VALUE;
double by0 = Double.MAX_VALUE, by1 = -Double.MAX_VALUE;
for (float[] pt : path) {
double px = toCanvasX(pt[0]), py = toCanvasZ(pt[1]);
if (px < bx0) bx0 = px; if (px > bx1) bx1 = px;
if (py < by0) by0 = py; if (py > by1) by1 = py;
}
if (bx1 < -8 || bx0 > cW + 8 || by1 < -8 || by0 > cH + 8) return;
gc.beginPath();
gc.moveTo(toCanvasX(path[0][0]), toCanvasZ(path[0][1]));
for (int i = 1; i < path.length; i++) gc.lineTo(toCanvasX(path[i][0]), toCanvasZ(path[i][1]));
gc.closePath();
gc.stroke();
}
private void drawTunnelEntrancesOverlay(GraphicsContext gc) { private void drawTunnelEntrancesOverlay(GraphicsContext gc) {
if (tunnelEntrances.isEmpty()) return; if (tunnelEntrances.isEmpty()) return;
gc.save(); gc.save();

View File

@@ -23,7 +23,6 @@ import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseButton; import javafx.scene.input.MouseButton;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.StrokeLineCap; import javafx.scene.shape.StrokeLineCap;
import javafx.scene.shape.StrokeLineJoin; import javafx.scene.shape.StrokeLineJoin;
import javafx.scene.text.Font; import javafx.scene.text.Font;
@@ -128,8 +127,8 @@ public class WorldMapView extends VBox {
private List<float[]> buildingFootprints = new ArrayList<>(); private List<float[]> buildingFootprints = new ArrayList<>();
private List<float[][]> cachedPathChains = new ArrayList<>(); private List<float[][]> cachedPathChains = new ArrayList<>();
/** Tunneleingang-Daten: [worldX, worldZ, dirX, dirZ, radiusWorld] */ private List<float[][]> tunnelPaths = new ArrayList<>();
private List<float[]> tunnelEntrances = new ArrayList<>(); private WritableImage bakedTunnelOverlay = null;
private static final java.util.concurrent.ConcurrentHashMap<String, float[]> MODEL_BOUNDS_CACHE private static final java.util.concurrent.ConcurrentHashMap<String, float[]> MODEL_BOUNDS_CACHE
= new java.util.concurrent.ConcurrentHashMap<>(); = new java.util.concurrent.ConcurrentHashMap<>();
private static final float[] DEFAULT_EXTENTS = {5f, 5f}; private static final float[] DEFAULT_EXTENTS = {5f, 5f};
@@ -380,9 +379,7 @@ public class WorldMapView extends VBox {
List<PlacedWater> waters = WaterBodyIO.load(); List<PlacedWater> waters = WaterBodyIO.load();
List<PlacedModel> models = PlacedModelIO.load(); List<PlacedModel> models = PlacedModelIO.load();
List<PlacedWaterfall> waterfalls = WaterfallIO.load(); List<PlacedWaterfall> waterfalls = WaterfallIO.load();
VoxelData vd = bakeVoxelData(mapData); float[] voxelSurf = bakeVoxelHeights(mapData);
float[] voxelSurf = vd.voxelSurface();
boolean[] sandwichMask = vd.sandwichMask();
Platform.runLater(() -> statusLbl.setText("Berechne Modell…")); Platform.runLater(() -> statusLbl.setText("Berechne Modell…"));
WorldMapRenderModel model = buildRenderModel( WorldMapRenderModel model = buildRenderModel(
@@ -406,15 +403,17 @@ public class WorldMapView extends VBox {
List<float[]> allBuildingFootprints = computeBuildingFootprints(model, findAssetRoot()); List<float[]> allBuildingFootprints = computeBuildingFootprints(model, findAssetRoot());
List<float[][]> allPathChains = loadPathChains(); List<float[][]> allPathChains = loadPathChains();
List<float[]> newTunnelEntrances = computeTunnelEntrances(model.mapData());
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(), sandwichMask); List<float[][]>[] splitChains = splitPathChains(allPathChains, model.voxelSurface(), 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);
List<float[][]> newTunnelPaths = buildTunnelContourPaths(caveMask);
int[] tunnelFillPx = buildTunnelFillPixels(caveMask);
// Hires-Chunks im Hintergrund generieren (nach dem UI-Update, nicht-blockierend) // Hires-Chunks im Hintergrund generieren (nach dem UI-Update, nicht-blockierend)
final WorldMapRenderModel chunkModel = model; final WorldMapRenderModel chunkModel = model;
Platform.runLater(() -> statusLbl.setText("Generiere Hires-Chunks…")); Platform.runLater(() -> statusLbl.setText("Generiere Hires-Chunks…"));
@@ -436,7 +435,8 @@ public class WorldMapView extends VBox {
forestPaths = newForestPaths; forestPaths = newForestPaths;
buildingFootprints = newBuildingFootprints; buildingFootprints = newBuildingFootprints;
cachedPathChains = newPathChains; cachedPathChains = newPathChains;
tunnelEntrances = newTunnelEntrances; tunnelPaths = newTunnelPaths;
bakedTunnelOverlay = toWritableImage(tunnelFillPx, RENDER_SIZE);
fitToView(); fitToView();
redraw(); redraw();
progress.setVisible(false); progress.setVisible(false);
@@ -513,8 +513,8 @@ public class WorldMapView extends VBox {
} }
// Layer 2: Klippen (Grau-Füllung + Schraffierung + Vektor-Kontur) // Layer 2: Klippen (Grau-Füllung + Schraffierung + Vektor-Kontur)
if (cbCliffs.isSelected()) drawCliffOverlay(gc); if (cbCliffs.isSelected()) drawCliffOverlay(gc);
// Layer 2.5: Tunneleingänge (schwarze Halbkreise) // Layer 2.5: Tunnel-Fläche (gefülltes Polygon + Kontur)
if (cbTunnels.isSelected()) drawTunnelEntrancesOverlay(gc); if (cbTunnels.isSelected()) drawTunnelOverlay(gc);
// Layer 3: Wald (Füllung + Vektor-Kontur) – unter Wasser // Layer 3: Wald (Füllung + Vektor-Kontur) – unter Wasser
if (cbForest.isSelected()) drawForestOverlay(gc); if (cbForest.isSelected()) drawForestOverlay(gc);
// Layer 3.5: Gebäude (Füllung + Vektor-Kontur) – unter Wasser // Layer 3.5: Gebäude (Füllung + Vektor-Kontur) – unter Wasser
@@ -1046,32 +1046,50 @@ public class WorldMapView extends VBox {
result.add(new float[]{wx, wz, dirX, dirZ, radius}); result.add(new float[]{wx, wz, dirX, dirZ, radius});
} }
private void drawTunnelEntrancesOverlay(GraphicsContext gc) { private void drawTunnelOverlay(GraphicsContext gc) {
if (tunnelEntrances.isEmpty()) return; double cW = canvas.getWidth(), cH = canvas.getHeight();
gc.save(); if (bakedTunnelOverlay != null) {
gc.setFill(Color.BLACK);
gc.setStroke(Color.BLACK);
gc.setLineWidth(1.0);
for (float[] te : tunnelEntrances) {
double cx = toCanvasX(te[0]);
double cz = toCanvasZ(te[1]);
double dirX = te[2]; // toward solid (in map X = canvas X)
double dirZ = te[3]; // toward solid (in map Z = canvas Y)
double radius = te[4] / WorldMapRenderer.WORLD_SIZE * (RENDER_SIZE - 1) * scale;
radius = Math.max(4.0, Math.min(radius, 20.0));
// Rotate so the D-curve points toward the solid (into the cliff)
double angleDeg = Math.toDegrees(Math.atan2(dirZ, dirX));
gc.save(); gc.save();
gc.translate(cx, cz); gc.setImageSmoothing(false);
gc.rotate(angleDeg); gc.drawImage(bakedTunnelOverlay, -panX / scale, -panY / scale, cW / scale, cH / scale, 0, 0, cW, cH);
// Semicircle: curve on the +X side (toward solid after rotation), flat face on -X
// fillArc(x, y, w, h, startAngle, extent): startAngle=−90° (screen-down), CCW 180° → goes through east (right)
gc.fillArc(-radius, -radius, radius * 2, radius * 2, -90, 180, ArcType.CHORD);
gc.restore(); gc.restore();
} }
gc.restore(); if (!tunnelPaths.isEmpty()) {
gc.save();
gc.setStroke(Color.rgb(40, 22, 5, 0.90));
gc.setLineWidth(overlayLineWidth() + 0.5);
gc.setLineDashes();
gc.setLineCap(StrokeLineCap.ROUND);
gc.setLineJoin(StrokeLineJoin.ROUND);
for (float[][] path : tunnelPaths) drawCoastPath(gc, path, cW, cH, 8);
gc.restore();
}
}
static List<float[][]> buildTunnelContourPaths(boolean[] caveMask) {
final int SIZE = CAVE_MASK_SIZE;
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<>();
marchingSquares(h, SIZE, 0f, null, null, segs);
List<float[][]> paths = assembleClosedPaths(segs.toArray(new float[0][]), 4);
final float MIN_PERI_SQ = 20f * 20f;
return paths.stream().filter(p -> pathPerimeterSq(p) >= MIN_PERI_SQ).collect(Collectors.toList());
}
private static int[] buildTunnelFillPixels(boolean[] caveMask) {
final int SIZE = CAVE_MASK_SIZE;
final int OUT = RENDER_SIZE;
final int FILL = 0x60_3D2208; // 37% alpha, very dark earthy brown
int[] px = new int[OUT * OUT];
for (int py = 0; py < OUT; py++) {
int mz = py * SIZE / OUT;
for (int bx = 0; bx < OUT; bx++) {
int mx = bx * SIZE / OUT;
if (caveMask[mz * SIZE + mx]) px[py * OUT + bx] = FILL;
}
}
return px;
} }
private static List<float[]> computeBuildingFootprints(WorldMapRenderModel model, Path assetRoot) { private static List<float[]> computeBuildingFootprints(WorldMapRenderModel model, Path assetRoot) {
@@ -1115,17 +1133,17 @@ public class WorldMapView extends VBox {
/** /**
* Teilt Pfad-Ketten an Tunnel-Grenzen auf. * Teilt Pfad-Ketten an Tunnel-Grenzen auf.
* Kombiniert Sandwich-Prüfung (solid→void≥3→solid = echter Tunnel) mit Y-Koordinate, * Nutzt die Cave-Chunk-Maske als räumlichen Filter (Chunk-Level-Erkennung)
* um False-Positives an Klippen-Füßen zu vermeiden. * kombiniert mit Y-Koordinaten-Prüfung gegen die Voxel-Oberfläche.
* Rückgabe: Index 0 = Oberflächensegmente, Index 1 = Tunnel-Segmente. * Rückgabe: Index 0 = Oberflächensegmente, Index 1 = Tunnel-Segmente.
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
static List<float[][]>[] splitPathChains(List<float[][]> chains, float[] voxelSurface, static List<float[][]>[] splitPathChains(List<float[][]> chains, float[] voxelSurface,
boolean[] sandwichMask) { boolean[] caveMask) {
List<float[][]> surface = new ArrayList<>(); List<float[][]> surface = new ArrayList<>();
List<float[][]> cave = new ArrayList<>(); List<float[][]> cave = new ArrayList<>();
for (float[][] chain : chains) { for (float[][] chain : chains) {
splitChainByVoxelSurface(chain, voxelSurface, sandwichMask, surface, cave); splitChainByVoxelSurface(chain, voxelSurface, caveMask, surface, cave);
} }
return new List[]{surface, cave}; return new List[]{surface, cave};
} }
@@ -1134,43 +1152,43 @@ public class WorldMapView extends VBox {
/** /**
* Prüft ob ein Knoten unter der Erde liegt. * Prüft ob ein Knoten unter der Erde liegt.
* Bedingung: In dieser X,Z-Spalte liegt ein Sandwich (solid→void≥3→solid) * Bedingung: Position liegt in einem Tunnel-Chunk-Bereich (Cave-Maske)
* UND das höchste Solid ist ≥ PATH_UNDERGROUND_CLEARANCE über der Knoten-Y. * UND das höchste Solid in dieser Spalte ist ≥ PATH_UNDERGROUND_CLEARANCE über der Knoten-Y.
* Das Sandwich-Kriterium verhindert False-Positives bei Wegen am Klippenfuß.
*/ */
private static boolean isNodeUnderground(float[] node, float[] voxelSurface, boolean[] sandwichMask) { private static boolean isNodeUnderground(float[] node, float[] voxelSurface, boolean[] caveMask) {
if (node.length < 3) return false; if (node.length < 3) return false;
float wy = node[2]; float wy = node[2];
if (Float.isNaN(wy)) return false; if (Float.isNaN(wy)) return false;
// Cave-Maske (Chunk-Level): nur Bereiche nahe Tunneln prüfen
if (caveMask != null && !isCavePoint(caveMask, node[0], node[1])) return false;
// Y-Prüfung: Knoten muss unter festem Voxel-Material liegen
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.round((node[0] + WH) / WS * (VS - 1));
int pz = Math.round((node[1] + WH) / WS * (VS - 1)); int pz = Math.round((node[1] + WH) / WS * (VS - 1));
px = Math.max(0, Math.min(VS - 1, px)); px = Math.max(0, Math.min(VS - 1, px));
pz = Math.max(0, Math.min(VS - 1, pz)); pz = Math.max(0, Math.min(VS - 1, pz));
// Sandwich-Prüfung: nur dort wo echter Hohlraum vorliegt (nicht bloß Klippe)
if (sandwichMask != null && !sandwichMask[pz * VS + px]) return false;
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 void splitChainByVoxelSurface(float[][] chain, float[] voxelSurface, private static void splitChainByVoxelSurface(float[][] chain, float[] voxelSurface,
boolean[] sandwichMask, boolean[] caveMask,
List<float[][]> surface, List<float[][]> cave) { List<float[][]> surface, List<float[][]> cave) {
if (chain.length < 2) return; if (chain.length < 2) return;
List<float[]> current = new ArrayList<>(); List<float[]> current = new ArrayList<>();
boolean currentCave = isNodeUnderground(chain[0], voxelSurface, sandwichMask); boolean currentCave = isNodeUnderground(chain[0], voxelSurface, caveMask);
current.add(chain[0]); current.add(chain[0]);
for (int i = 0; i < chain.length - 1; i++) { for (int i = 0; i < chain.length - 1; i++) {
float[] a = chain[i]; float[] a = chain[i];
float[] b = chain[i + 1]; float[] b = chain[i + 1];
boolean bCave = isNodeUnderground(b, voxelSurface, sandwichMask); boolean bCave = isNodeUnderground(b, voxelSurface, caveMask);
if (currentCave == bCave) { if (currentCave == bCave) {
current.add(b); current.add(b);
} else { } else {
float[] crossing = findPathCrossing(a, b, voxelSurface, sandwichMask, currentCave); float[] crossing = findPathCrossing(a, b, voxelSurface, caveMask, currentCave);
current.add(crossing); current.add(crossing);
if (current.size() >= 2) { if (current.size() >= 2) {
(currentCave ? cave : surface).add(current.toArray(new float[0][])); (currentCave ? cave : surface).add(current.toArray(new float[0][]));
@@ -1187,15 +1205,15 @@ public class WorldMapView extends VBox {
} }
/** Bisektion mit Y-Interpolation: 12 Iterationen ≈ 0.05m Genauigkeit. */ /** Bisektion mit Y-Interpolation: 12 Iterationen ≈ 0.05m Genauigkeit. */
private static float[] findPathCrossing(float[] a, float[] b, float[] vs, boolean[] sm, private static float[] findPathCrossing(float[] a, float[] b, float[] vs, boolean[] caveMask,
boolean aIsUnderground) { boolean aIsUnderground) {
float ax = a[0], az = a[1], ay = a.length > 2 ? a[2] : 0f; 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; float bx = b[0], bz = b[1], by = b.length > 2 ? b[2] : 0f;
for (int i = 0; i < 12; i++) { for (int i = 0; i < 12; i++) {
float mx = (ax + bx) * 0.5f, mz = (az + bz) * 0.5f, my = (ay + by) * 0.5f; float mx = (ax + bx) * 0.5f, mz = (az + bz) * 0.5f, my = (ay + by) * 0.5f;
float[] mid = {mx, mz, my}; float[] mid = {mx, mz, my};
if (isNodeUnderground(mid, vs, sm) == aIsUnderground) { ax = mx; az = mz; ay = my; } if (isNodeUnderground(mid, vs, caveMask) == aIsUnderground) { ax = mx; az = mz; ay = my; }
else { bx = mx; bz = mz; by = my; } else { bx = mx; bz = mz; by = my; }
} }
return new float[]{(ax + bx) * 0.5f, (az + bz) * 0.5f, (ay + by) * 0.5f}; return new float[]{(ax + bx) * 0.5f, (az + bz) * 0.5f, (ay + by) * 0.5f};
} }
@@ -1389,26 +1407,17 @@ public class WorldMapView extends VBox {
return i; return i;
} }
private record VoxelData(float[] voxelSurface, boolean[] sandwichMask) {} /** Berechnet voxelSurface (höchstes Solid-Voxel pro Spalte) aus Live- oder Disk-Chunks. */
private float[] bakeVoxelHeights(MapData mapData) {
private VoxelData bakeVoxelData(MapData mapData) {
int VS = WorldMapRenderer.VOXEL_SURFACE_RES; int VS = WorldMapRenderer.VOXEL_SURFACE_RES;
// Bevorzuge Live-Chunks aus dem Editor-Speicher, falle auf Disk zurück
List<VoxelChunk> chunks = voxelChunkSupplier != null List<VoxelChunk> chunks = voxelChunkSupplier != null
? voxelChunkSupplier.get() ? voxelChunkSupplier.get()
: VoxelChunkIO.loadAll(); : VoxelChunkIO.loadAll();
boolean allEmpty = chunks.stream().allMatch(VoxelChunk::isEmpty); boolean allEmpty = chunks.stream().allMatch(VoxelChunk::isEmpty);
if (allEmpty) { chunks = VoxelChunkIO.loadAll(); } if (allEmpty) { chunks = VoxelChunkIO.loadAll(); }
boolean[] sandwichMask = new boolean[VS * VS];
if (chunks.isEmpty()) {
float[] empty = new float[VS * VS];
java.util.Arrays.fill(empty, Float.NaN);
return new VoxelData(empty, sandwichMask);
}
// Kern-Logik ist dieselbe wie WorldMapRenderer.bakeVoxelSurface, aber mit
// Live-Chunks statt VoxelChunkIO.loadAll().
float[] voxelSurface = new float[VS * VS]; float[] voxelSurface = new float[VS * VS];
java.util.Arrays.fill(voxelSurface, Float.NaN); java.util.Arrays.fill(voxelSurface, Float.NaN);
if (chunks.isEmpty()) { return voxelSurface; }
float WH = WorldMapRenderer.WORLD_HALF; float WH = WorldMapRenderer.WORLD_HALF;
float WS = WorldMapRenderer.WORLD_SIZE; float WS = WorldMapRenderer.WORLD_SIZE;
int UV = MapData.UPPER_VERTS; int UV = MapData.UPPER_VERTS;
@@ -1441,57 +1450,7 @@ public class WorldMapView extends VBox {
} }
} }
} }
// Sandwich-Maske: pro Spalte prüfen ob solid→void(≥3)→solid vorliegt (echter Tunnel) return voxelSurface;
buildSandwichMaskInto(chunks, sandwichMask, VS, WH, WS);
return new VoxelData(voxelSurface, sandwichMask);
}
/**
* Berechnet pro Voxel-Spalte, ob eine Tunnel-Struktur (solid→void≥3→solid) vorliegt.
* Nur cy=0-Chunks werden berücksichtigt; Klippen ohne Hohlraum werden NICHT markiert.
*/
private static void buildSandwichMaskInto(List<VoxelChunk> chunks, boolean[] out, int VS,
float WH, float WS) {
for (VoxelChunk chunk : chunks) {
if (chunk.isEmpty() || chunk.cy != 0) continue;
for (int lz = 0; lz < VoxelChunk.SIZE; lz++) {
float worldZ = VoxelChunk.toWorldZ(chunk.cz, lz);
int vsPz = Math.round((worldZ + WH) / WS * (VS - 1));
if (vsPz < 0 || vsPz >= VS) continue;
for (int lx = 0; lx < VoxelChunk.SIZE; lx++) {
float worldX = VoxelChunk.toWorldX(chunk.cx, lx);
int vsPx = Math.round((worldX + WH) / WS * (VS - 1));
if (vsPx < 0 || vsPx >= VS) continue;
int idx = vsPz * VS + vsPx;
if (out[idx]) continue;
int phase = 0, voidCnt = 0;
for (int ly = 0; ly < VoxelChunk.SIZE; ly++) {
byte d = chunk.getDensity(lx, ly, lz);
if (phase == 0) {
if (d > 0) phase = 1;
} else if (phase == 1) {
if (d <= 0) { phase = 2; voidCnt = 1; }
} else {
if (d <= 0) { voidCnt++; }
else {
if (voidCnt >= 3) { out[idx] = true; break; }
phase = 1; voidCnt = 0;
}
}
}
}
}
}
}
/** Wie buildSandwichMaskInto, aber lädt Chunks von Disk (für CaveMapView). */
static boolean[] bakeSandwichMask() {
int VS = WorldMapRenderer.VOXEL_SURFACE_RES;
boolean[] sm = new boolean[VS * VS];
List<VoxelChunk> chunks;
try { chunks = VoxelChunkIO.loadAll(); } catch (Exception e) { return sm; }
buildSandwichMaskInto(chunks, sm, VS, WorldMapRenderer.WORLD_HALF, WorldMapRenderer.WORLD_SIZE);
return sm;
} }
private WorldMapRenderModel buildRenderModel( private WorldMapRenderModel buildRenderModel(