diff --git a/blight-common/src/main/java/de/blight/common/map/WorldMapRenderer.java b/blight-common/src/main/java/de/blight/common/map/WorldMapRenderer.java index 10baafe..b462be8 100644 --- a/blight-common/src/main/java/de/blight/common/map/WorldMapRenderer.java +++ b/blight-common/src/main/java/de/blight/common/map/WorldMapRenderer.java @@ -163,7 +163,7 @@ public final class WorldMapRenderer { int[] slotCols = model.slotColorsRGB(); int[] colorMap = model.terrainColorMap() != null ? model.terrainColorMap() - : buildTerrainColorMap(m, slotCols, model.mapTilePixels(), model.voxelSurface(), model.waters()); + : buildTerrainColorMap(m, slotCols, model.mapTilePixels(), model.voxelSurface(), model.waters(), model.waterfalls()); int[] rawRGB = new int[targetSize * targetSize]; int waterR = (WATER_COLOR >> 16) & 0xFF; int waterG = (WATER_COLOR >> 8) & 0xFF; @@ -962,6 +962,41 @@ public final class WorldMapRenderer { return map; } + /** Erzeugt eine Maske: true für jeden Pixel, der innerhalb eines Wasserfall-Quads liegt. + * Nutzt Graphics2D-Rasterisierung inkl. Stroke, damit auch schmale Quads erfasst werden. */ + private static boolean[] buildWaterfallMask(List waterfalls, int size) { + boolean[] mask = new boolean[size * size]; + if (waterfalls == null || waterfalls.isEmpty()) return mask; + // BYTE_BINARY: 1 bit/px → 512 KB bei 2048px, 8 MB bei 8192px + BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_BYTE_BINARY); + Graphics2D g = img.createGraphics(); + g.setColor(java.awt.Color.WHITE); + float strokeW = Math.max(4f, size / 512f); // mind. 4px Strichbreite für dünne Quads + g.setStroke(new BasicStroke(strokeW, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + for (PlacedWaterfall wf : waterfalls) { + int[] xs = { + worldToPixel(wf.ax(), size), worldToPixel(wf.bx(), size), + worldToPixel(wf.cx(), size), worldToPixel(wf.dx(), size) + }; + int[] zs = { + worldToPixel(wf.az(), size), worldToPixel(wf.bz(), size), + worldToPixel(wf.cz(), size), worldToPixel(wf.dz(), size) + }; + g.fillPolygon(xs, zs, 4); // Füllung + g.drawPolygon(xs, zs, 4); // Stroke-Backup für schmale Quads + } + g.dispose(); + java.awt.image.Raster raster = img.getData(); + int[] px = new int[1]; + for (int py = 0; py < size; py++) { + for (int x = 0; x < size; x++) { + raster.getPixel(x, py, px); + if (px[0] != 0) mask[py * size + x] = true; + } + } + return mask; + } + /** Lädt die 6 Karten-Kacheln aus Textures/map/ im Asset-Verzeichnis. */ public static int[][] loadMapTiles(Path assetRoot) { int[][] result = new int[TILE_FILE_NAMES.length][]; @@ -1220,23 +1255,25 @@ public final class WorldMapRenderer { * verwendet — kein Splatmap-Blend zur Renderzeit mehr. */ public static int[] buildTerrainColorMap(MapData m, int[] slotColors, int[][] tilePixels, - float[] voxelSurface, List waters) { + float[] voxelSurface, List waters, + List waterfalls) { final int SIZE = COLOR_MAP_SIZE; final int SS = MapData.SPLAT_SIZE; - float[] heights = buildTerrainSamples(m, SIZE, voxelSurface); - float[] waterH = buildWaterHeightMap(waters, SIZE); - int[] tileAvg = computeTileAvgColors(tilePixels); - int[] slotTypes = computeSlotTileTypes(m, slotColors); - float cellW = WORLD_SIZE / (SIZE - 1); - int[] terrainRGB = new int[SIZE * SIZE]; - float[] cliffMask = new float[SIZE * SIZE]; + float[] heights = buildTerrainSamples(m, SIZE, voxelSurface); + float[] waterH = buildWaterHeightMap(waters, SIZE); + boolean[] wfMask = buildWaterfallMask(waterfalls, SIZE); + int[] tileAvg = computeTileAvgColors(tilePixels); + int[] slotTypes = computeSlotTileTypes(m, slotColors); + float cellW = WORLD_SIZE / (SIZE - 1); + int[] terrainRGB = new int[SIZE * SIZE]; + float[] cliffMask = new float[SIZE * SIZE]; for (int py = 0; py < SIZE; py++) { float wz = -WORLD_HALF + py * cellW; for (int px = 0; px < SIZE; px++) { float wx = -WORLD_HALF + px * cellW; int idx = py * SIZE + px; float h = heights[idx]; - if (h < 0f || (waterH[idx] > 0f && waterH[idx] > h)) { + if (h < 0f || (waterH[idx] > 0f && waterH[idx] > h) || wfMask[idx]) { int[] wc = TILE_FALLBACK_RGB[TILE_WATER]; terrainRGB[idx] = (wc[0]<<16)|(wc[1]<<8)|wc[2]; } else { @@ -1278,6 +1315,7 @@ public final class WorldMapRenderer { */ public static int buildAndSaveHiresChunks(MapData m, int[] slotColors, int[][] tilePixels, float[] voxelSurface, List waters, + List waterfalls, java.nio.file.Path cacheDir, boolean forceRegen) { final int SIZE = HIRES_MAP_SIZE; final int CS = CHUNK_SIZE; @@ -1297,11 +1335,12 @@ public final class WorldMapRenderer { if (allExist) { log.debug("[HiresChunks] Alle {} Chunks im Cache – übersprungen.", COLS*COLS); return 0; } } - float[] heights = buildTerrainSamples(m, SIZE, voxelSurface); - float[] waterH = buildWaterHeightMap(waters, SIZE); - int[] tileAvg = computeTileAvgColors(tilePixels); - int[] slotTypes = computeSlotTileTypes(m, slotColors); - float cellW = WORLD_SIZE / (SIZE - 1); + float[] heights = buildTerrainSamples(m, SIZE, voxelSurface); + float[] waterH = buildWaterHeightMap(waters, SIZE); + boolean[] wfMask = buildWaterfallMask(waterfalls, SIZE); + int[] tileAvg = computeTileAvgColors(tilePixels); + int[] slotTypes = computeSlotTileTypes(m, slotColors); + float cellW = WORLD_SIZE / (SIZE - 1); int written = 0; final int CLIFF_BLUR_R = 4; // Blur-Radius für weiche Klippen-Kanten @@ -1329,7 +1368,7 @@ public final class WorldMapRenderer { int eidx = ely * EXTW + elx; int hidx = gyC * SIZE + gxC; float h = heights[hidx]; - if (h < 0f || (waterH[hidx] > 0f && waterH[hidx] > h)) { + if (h < 0f || (waterH[hidx] > 0f && waterH[hidx] > h) || wfMask[hidx]) { isWaterExt[eidx] = true; } else { float hR = heights[gyC * SIZE + Math.min(SIZE-1, gxC+dPxHires)]; diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index b06ed8d..949e334 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -563,8 +563,15 @@ public class EditorApp extends Application { javafx.animation.Timeline[] pollerRef = new javafx.animation.Timeline[1]; pollerRef[0] = new javafx.animation.Timeline( new javafx.animation.KeyFrame(javafx.util.Duration.millis(100), ev -> { - splashStatus.setText(input.loadingStatus); - if (input.jmeReady) { + boolean jmeOk = input.jmeReady; + boolean mapOk = worldMapView != null && worldMapView.isMapReady(); + if (!jmeOk) { + splashStatus.setText(input.loadingStatus); + } else { + splashStatus.setText("Karte: " + (worldMapView != null + ? worldMapView.getLoadingStatus() : "…")); + } + if (jmeOk && mapOk) { pollerRef[0].stop(); stage.show(); restoreEditorPrefs(); @@ -5646,30 +5653,34 @@ public class EditorApp extends Application { if (isSelected) mapObjectsView.refresh(); }); - worldMapView = new de.blight.editor.ui.WorldMapView(() -> primaryStage); - worldMapView.setVoxelChunkSupplier(() -> { - de.blight.editor.state.VoxelEditorState ves = jmeApp == null ? null - : jmeApp.getStateManager().getState(de.blight.editor.state.VoxelEditorState.class); - return ves != null ? ves.getChunksSnapshot() : de.blight.common.VoxelChunkIO.loadAll(); - }); - worldMapView.setCameraInfoSupplier(() -> - new de.blight.editor.ui.WorldMapView.CameraInfo( - input.camX, input.camY, input.camZ, input.camYaw)); - worldMapView.setTeleportCallback(pos -> { - input.pendingGotoX = pos[0]; - input.pendingGotoY = pos[1]; - input.pendingGotoZ = pos[2]; - }); + if (worldMapView == null) { + worldMapView = new de.blight.editor.ui.WorldMapView(() -> primaryStage); + worldMapView.setVoxelChunkSupplier(() -> { + de.blight.editor.state.VoxelEditorState ves = jmeApp == null ? null + : jmeApp.getStateManager().getState(de.blight.editor.state.VoxelEditorState.class); + return ves != null ? ves.getChunksSnapshot() : de.blight.common.VoxelChunkIO.loadAll(); + }); + worldMapView.setCameraInfoSupplier(() -> + new de.blight.editor.ui.WorldMapView.CameraInfo( + input.camX, input.camY, input.camZ, input.camYaw)); + worldMapView.setTeleportCallback(pos -> { + input.pendingGotoX = pos[0]; + input.pendingGotoY = pos[1]; + input.pendingGotoZ = pos[2]; + }); + worldMapView.setRefreshAction(() -> { + input.saveRequested = true; + pendingAfterSave = worldMapView::loadAndRender; + setStatus("Speichere vor Weltkarten-Refresh…"); + }); + worldMapView.loadAndRender(); // Vorladen beim Start, nicht erst beim Tab-Öffnen + } weltkartTab = new Tab("Weltkarte", worldMapView); weltkartTab.setClosable(false); weltkartTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> { if (isSelected && !worldMapView.isLoaded()) worldMapView.loadAndRender(); - }); - worldMapView.setRefreshAction(() -> { - input.saveRequested = true; - pendingAfterSave = worldMapView::loadAndRender; - setStatus("Speichere vor Weltkarten-Refresh…"); + else if (isSelected) worldMapView.ensureFitToView(); }); worldMapView.setFullscreenCallbacks( () -> { diff --git a/blight-editor/src/main/java/de/blight/editor/ui/WorldMapView.java b/blight-editor/src/main/java/de/blight/editor/ui/WorldMapView.java index 0348496..5f50fb4 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/WorldMapView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/WorldMapView.java @@ -127,6 +127,7 @@ public class WorldMapView extends VBox { }; private final java.util.Set chunksLoading = java.util.Collections.synchronizedSet(new java.util.HashSet<>()); + private volatile boolean chunkRedrawPending = false; private WritableImage bakedForestOverlay = null; // Wasser-Fill-Overlay entfernt: ocean + PlacedWater sind bereits in mapFxImage korrekt gerendert @@ -326,7 +327,16 @@ public class WorldMapView extends VBox { // ── Öffentliche API ─────────────────────────────────────────────────────── - public boolean isLoaded() { return mapFxImage != null || loading.get(); } + public boolean isLoaded() { return mapFxImage != null || loading.get(); } + public boolean isMapReady() { return mapFxImage != null; } + public String getLoadingStatus() { return statusLbl.getText(); } + + /** Stellt sicher, dass die Karte beim ersten Anzeigen korrekt skaliert/positioniert ist. */ + public void ensureFitToView() { + if (mapFxImage != null && zoomLevels == null) { + fitCanvas(); + } + } /** Lädt alle Weltdaten neu und baut das Render-Modell (teuer: I/O + Berechnungen). */ public void loadAndRender() { @@ -377,7 +387,8 @@ public class WorldMapView extends VBox { Platform.runLater(() -> statusLbl.setText("Generiere Hires-Chunks…")); WorldMapRenderer.buildAndSaveHiresChunks( chunkModel.mapData(), chunkModel.slotColorsRGB(), chunkModel.mapTilePixels(), - chunkModel.voxelSurface(), chunkModel.waters(), CHUNK_CACHE_DIR, true); + chunkModel.voxelSurface(), chunkModel.waters(), chunkModel.waterfalls(), + CHUNK_CACHE_DIR, true); chunkCache.clear(); Platform.runLater(() -> { @@ -498,9 +509,14 @@ public class WorldMapView extends VBox { if (img != null) { gc.drawImage(img, canvasLeft, canvasTop, chunkCanvasW, chunkCanvasH); } else { - // Platzhalter (dunkelgrau) + asynchrones Laden - gc.setFill(javafx.scene.paint.Color.rgb(40, 40, 40)); - gc.fillRect(canvasLeft, canvasTop, chunkCanvasW, chunkCanvasH); + // Fallback: lowres-Ausschnitt zeigen bis Hires-Chunk geladen ist + if (mapFxImage != null) { + double srcX = (double) cx * RENDER_SIZE / COLS; + double srcY = (double) cy * RENDER_SIZE / COLS; + double srcW = (double) RENDER_SIZE / COLS; + gc.drawImage(mapFxImage, srcX, srcY, srcW, srcW, + canvasLeft, canvasTop, chunkCanvasW, chunkCanvasH); + } if (chunksLoading.add(key)) { final int fcx = cx, fcy = cy, fkey = key; Thread lt = new Thread(() -> { @@ -512,7 +528,7 @@ public class WorldMapView extends VBox { Platform.runLater(() -> { chunkCache.put(fkey, wi); chunksLoading.remove(fkey); - redraw(); + scheduleChunkRedraw(); }); } else { chunksLoading.remove(fkey); @@ -526,6 +542,17 @@ public class WorldMapView extends VBox { } } + /** Bündelt Redraws nach Chunk-Ladevorgängen: maximal ein Redraw pro FX-Puls. */ + private void scheduleChunkRedraw() { + if (!chunkRedrawPending) { + chunkRedrawPending = true; + Platform.runLater(() -> { + chunkRedrawPending = false; + redraw(); + }); + } + } + // ── Canvas Tile-Overlay (zoom-unabhängige Kacheln) ──────────────────────── private static int tileSample(int[][] tilePx, int cat, @@ -916,7 +943,7 @@ public class WorldMapView extends VBox { List treeList = models.stream() .filter(WorldMapRenderer::isTree).collect(Collectors.toList()); List> tClusters = clusterTreeModels(treeList, 40f); - int[] colorMap = WorldMapRenderer.buildTerrainColorMap(mapData, slotColors, mapTiles, voxelSurface, waters); + int[] colorMap = WorldMapRenderer.buildTerrainColorMap(mapData, slotColors, mapTiles, voxelSurface, waters, waterfalls); return new WorldMapRenderModel( mapData, areas, zones, locs, waters, models, waterfalls, slotColors, mask, voxelSurface, tSamp, paths, tClusters, mapTiles, colorMap); @@ -924,8 +951,8 @@ public class WorldMapView extends VBox { // Erzeugt interpolierte Marching-Squares-Segmente für Meer + Wasserflächen. // Endpunkte liegen am echten Höhen-Nulldurchgang → keine Treppenstufen auf Diagonalen. - private static float[][] buildCoastSegsMS(MapData mapData, List waters, int size, - float[] voxelSurface) { + private static float[][] buildCoastSegsMS(MapData mapData, List waters, + int size, float[] voxelSurface) { int TV = MapData.TERRAIN_VERTS; int VS = WorldMapRenderer.VOXEL_SURFACE_RES; float[] h = new float[size * size];