Compare commits
10 Commits
faa0a8b254
...
52801b5ddf
| Author | SHA1 | Date | |
|---|---|---|---|
| 52801b5ddf | |||
| 9fa1643264 | |||
| 7f5be2b65d | |||
| f29580cb3b | |||
| dc7c326441 | |||
| 8430eb41d6 | |||
| 174579cd0b | |||
| 6e7e94371e | |||
| a0b1e09d7d | |||
| 5226e07622 |
BIN
blight-assets/src/main/resources/Textures/hud/map_border.png
Normal file
BIN
blight-assets/src/main/resources/Textures/hud/map_border.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
BIN
blight-assets/src/main/resources/Textures/hud/minimap_cave.png
Normal file
BIN
blight-assets/src/main/resources/Textures/hud/minimap_cave.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 594 KiB After Width: | Height: | Size: 595 KiB |
File diff suppressed because it is too large
Load Diff
@@ -83,8 +83,10 @@ public class EditorApp extends Application {
|
||||
new java.util.concurrent.ConcurrentLinkedQueue<>();
|
||||
private VBox assetPanel;
|
||||
private Tab weltkartTab;
|
||||
private Tab caveMapTab;
|
||||
private MapObjectsView mapObjectsView;
|
||||
private de.blight.editor.ui.WorldMapView worldMapView;
|
||||
private de.blight.editor.ui.CaveMapView caveMapView;
|
||||
private de.blight.editor.ui.ThumbnailManagerView thumbnailManagerView;
|
||||
private StackPane worldViewport;
|
||||
private javafx.scene.canvas.Canvas compassCanvas;
|
||||
@@ -5700,17 +5702,73 @@ public class EditorApp extends Application {
|
||||
weltkartTab = new Tab("Weltkarte", worldMapView);
|
||||
weltkartTab.setClosable(false);
|
||||
weltkartTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> {
|
||||
if (isSelected && !worldMapView.isLoaded()) worldMapView.loadAndRender();
|
||||
else if (isSelected) worldMapView.ensureFitToView();
|
||||
if (!isSelected) return;
|
||||
if (!worldMapView.isLoaded()) {
|
||||
worldMapView.loadAndRender();
|
||||
} else if (caveMapView != null && caveMapView.isLoaded() && caveMapView.isViewInitialized()) {
|
||||
final double s = caveMapView.getScale(), px = caveMapView.getPanX(), py = caveMapView.getPanY();
|
||||
javafx.application.Platform.runLater(() -> worldMapView.setView(s, px, py));
|
||||
} else {
|
||||
worldMapView.ensureFitToView();
|
||||
}
|
||||
});
|
||||
|
||||
de.blight.editor.ui.CaveMapView caveMapView = new de.blight.editor.ui.CaveMapView(() -> primaryStage);
|
||||
Tab caveMapTab = new Tab("Höhlenkarte", caveMapView);
|
||||
caveMapView = new de.blight.editor.ui.CaveMapView(() -> primaryStage);
|
||||
caveMapTab = new Tab("Höhlenkarte", caveMapView);
|
||||
caveMapTab.setClosable(false);
|
||||
caveMapTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> {
|
||||
if (isSelected && !caveMapView.isLoaded()) caveMapView.loadAndRender();
|
||||
else if (isSelected) caveMapView.ensureFitToView();
|
||||
if (!isSelected) return;
|
||||
if (!caveMapView.isLoaded()) {
|
||||
caveMapView.loadAndRender();
|
||||
} else if (worldMapView.isMapReady() && worldMapView.isViewInitialized()) {
|
||||
final double s = worldMapView.getScale(), px = worldMapView.getPanX(), py = worldMapView.getPanY();
|
||||
javafx.application.Platform.runLater(() -> caveMapView.setView(s, px, py));
|
||||
} else {
|
||||
caveMapView.ensureFitToView();
|
||||
}
|
||||
});
|
||||
caveMapView.setFullscreenCallbacks(
|
||||
() -> {
|
||||
final javafx.scene.control.Label placeholder = new javafx.scene.control.Label();
|
||||
caveMapTab.setContent(placeholder);
|
||||
assetTabBtn.setVisible(false);
|
||||
javafx.application.Platform.runLater(() -> {
|
||||
caveMapView.setMaxWidth(Double.MAX_VALUE);
|
||||
javafx.scene.layout.StackPane.setAlignment(caveMapView, null);
|
||||
caveMapView.setTranslateX(0);
|
||||
try {
|
||||
centerStack.getChildren().add(caveMapView);
|
||||
} catch (Exception ex) {
|
||||
log.error("[FS-cave-enter] add failed", ex);
|
||||
return;
|
||||
}
|
||||
javafx.application.Platform.runLater(() -> {
|
||||
double w = caveMapView.getWidth();
|
||||
caveMapView.fitCanvas();
|
||||
caveMapView.setTranslateX(-w);
|
||||
javafx.animation.TranslateTransition tt = new javafx.animation.TranslateTransition(
|
||||
javafx.util.Duration.millis(300), caveMapView);
|
||||
tt.setToX(0);
|
||||
tt.setInterpolator(javafx.animation.Interpolator.EASE_OUT);
|
||||
tt.play();
|
||||
});
|
||||
});
|
||||
},
|
||||
() -> {
|
||||
double w = caveMapView.getWidth();
|
||||
javafx.animation.TranslateTransition tt = new javafx.animation.TranslateTransition(
|
||||
javafx.util.Duration.millis(220), caveMapView);
|
||||
tt.setToX(-w);
|
||||
tt.setInterpolator(javafx.animation.Interpolator.EASE_IN);
|
||||
tt.setOnFinished(ev -> {
|
||||
centerStack.getChildren().remove(caveMapView);
|
||||
caveMapView.setTranslateX(0);
|
||||
caveMapTab.setContent(caveMapView);
|
||||
assetTabBtn.setVisible(true);
|
||||
});
|
||||
tt.play();
|
||||
});
|
||||
caveMapView.loadAndRender(); // Vorladen beim Start, nicht erst beim Tab-Öffnen
|
||||
worldMapView.setFullscreenCallbacks(
|
||||
() -> {
|
||||
// Placeholder ersetzt worldMapView im Tab → sauberes Detach
|
||||
|
||||
@@ -2,10 +2,11 @@ package de.blight.editor.ui;
|
||||
|
||||
import de.blight.common.*;
|
||||
import de.blight.common.map.WorldMapRenderer;
|
||||
import de.blight.common.map.WorldMapRenderModel;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.canvas.GraphicsContext;
|
||||
import javafx.scene.control.*;
|
||||
@@ -15,9 +16,13 @@ import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.ArcType;
|
||||
import javafx.scene.shape.StrokeLineCap;
|
||||
import javafx.scene.shape.StrokeLineJoin;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -28,40 +33,98 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* 2D-Kartenansicht des Höhlen/Tunnel-Innenbereichs.
|
||||
* Hintergrund: dunkles Grau. Erdfarbe für Lochbereiche.
|
||||
* Overlays: Tunneleingänge, Wege, Gebäude, Areas.
|
||||
*/
|
||||
public class CaveMapView extends VBox {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(CaveMapView.class);
|
||||
private static final int RENDER_SIZE = 2048;
|
||||
|
||||
private final Canvas canvas = new Canvas();
|
||||
private final Label statusLbl = new Label("Höhlenkarte noch nicht geladen.");
|
||||
private final ProgressBar progress = new ProgressBar();
|
||||
private final AtomicBoolean loading = new AtomicBoolean(false);
|
||||
|
||||
private WritableImage mapFxImage = null;
|
||||
private List<float[][]> cachedPathChains = new ArrayList<>();
|
||||
private List<float[]> tunnelEntrances = new ArrayList<>();
|
||||
private List<PlacedArea> cachedAreas = new ArrayList<>();
|
||||
private List<float[]> buildingFootprints = new ArrayList<>();
|
||||
|
||||
private static final int ZOOM_LEVELS = 5;
|
||||
private static final double ZOOM_MAX_SCALE = 4.0;
|
||||
|
||||
private final Canvas canvas = new Canvas();
|
||||
private final Label statusLbl = new Label("Höhlenkarte noch nicht geladen.");
|
||||
private final ProgressBar progress = new ProgressBar();
|
||||
private final StackPane canvasPane = new StackPane();
|
||||
private final AtomicBoolean loading = new AtomicBoolean(false);
|
||||
|
||||
private final Supplier<Stage> stageSupplier;
|
||||
|
||||
// Layer-Checkboxen
|
||||
private final CheckBox cbEntrances = layerCheck("Eingänge", true);
|
||||
private final CheckBox cbBuildings = layerCheck("Gebäude", true);
|
||||
private final CheckBox cbPaths = layerCheck("Wege", true);
|
||||
private final CheckBox cbAreas = layerCheck("Areas", true);
|
||||
|
||||
private final ToggleButton labelBtn = new ToggleButton("Label");
|
||||
private final Button fullscreenBtn = new Button(">>");
|
||||
private final Button backBtn = new Button("<<");
|
||||
private boolean isFullscreen = false;
|
||||
private double savedScale, savedPanX, savedPanY;
|
||||
private Runnable enterFullscreenCallback = null;
|
||||
private Runnable exitFullscreenCallback = null;
|
||||
|
||||
private WritableImage mapFxImage = null;
|
||||
private List<float[][]> cachedPathChains = new ArrayList<>();
|
||||
private List<float[]> tunnelEntrances = new ArrayList<>();
|
||||
private List<float[][]> tunnelContourPaths = new ArrayList<>();
|
||||
private List<PlacedArea> cachedAreas = new ArrayList<>();
|
||||
private List<float[]> buildingFootprints = new ArrayList<>();
|
||||
private MapData cachedMapData = null; // für Export
|
||||
|
||||
private double panX = 0, panY = 0, scale = 1.0;
|
||||
private double[] zoomLevels = null;
|
||||
private int zoomLevelIdx = 0;
|
||||
private boolean autoLoaded = false;
|
||||
|
||||
public CaveMapView(Supplier<Stage> stageSupplier) {
|
||||
ToolBar bar = new ToolBar(makeBtn("Aktualisieren", e -> loadAndRender()));
|
||||
this.stageSupplier = stageSupplier;
|
||||
|
||||
progress.setMaxWidth(Double.MAX_VALUE);
|
||||
cbEntrances.selectedProperty().addListener((obs, o, n) -> redraw());
|
||||
cbBuildings.selectedProperty().addListener((obs, o, n) -> redraw());
|
||||
cbPaths.selectedProperty().addListener((obs, o, n) -> redraw());
|
||||
cbAreas.selectedProperty().addListener((obs, o, n) -> redraw());
|
||||
|
||||
MenuButton layerMenu = new MenuButton("Layer ▾");
|
||||
for (CheckBox cb : new CheckBox[]{cbEntrances, cbBuildings, cbPaths, cbAreas}) {
|
||||
CustomMenuItem item = new CustomMenuItem(cb, false);
|
||||
item.setHideOnClick(false);
|
||||
layerMenu.getItems().add(item);
|
||||
}
|
||||
|
||||
Button refreshBtn = new Button("↻");
|
||||
refreshBtn.setTooltip(new Tooltip("Aktualisieren"));
|
||||
refreshBtn.setOnAction(e -> loadAndRender());
|
||||
|
||||
Button exportBtn = new Button("⇩");
|
||||
exportBtn.setTooltip(new Tooltip("Als PNG exportieren…"));
|
||||
exportBtn.setOnAction(e -> exportPng());
|
||||
|
||||
labelBtn.setStyle("-fx-font-size: 11;");
|
||||
labelBtn.selectedProperty().addListener((obs, o, n) ->
|
||||
canvas.setCursor(n ? Cursor.CROSSHAIR : Cursor.DEFAULT));
|
||||
|
||||
fullscreenBtn.setTooltip(new Tooltip("Maximieren"));
|
||||
backBtn.setTooltip(new Tooltip("Minimieren"));
|
||||
fullscreenBtn.setOnAction(e -> enterFullscreen());
|
||||
backBtn.setOnAction(e -> exitFullscreen());
|
||||
backBtn.setVisible(false);
|
||||
|
||||
ToolBar bar = new ToolBar(
|
||||
layerMenu,
|
||||
new Separator(),
|
||||
new Label("Bearbeiten:"),
|
||||
labelBtn,
|
||||
new Separator(),
|
||||
refreshBtn,
|
||||
exportBtn,
|
||||
new Separator(),
|
||||
fullscreenBtn,
|
||||
backBtn
|
||||
);
|
||||
|
||||
progress.setPrefWidth(160);
|
||||
progress.setVisible(false);
|
||||
|
||||
StackPane canvasPane = new StackPane(canvas);
|
||||
canvasPane.getChildren().add(canvas);
|
||||
canvasPane.setStyle("-fx-background-color: #141414;");
|
||||
VBox.setVgrow(canvasPane, Priority.ALWAYS);
|
||||
canvas.widthProperty().bind(canvasPane.widthProperty());
|
||||
@@ -69,15 +132,49 @@ public class CaveMapView extends VBox {
|
||||
canvas.widthProperty().addListener(obs -> redraw());
|
||||
canvas.heightProperty().addListener(obs -> redraw());
|
||||
|
||||
// Status-Leiste identisch wie WorldMapView: unter dem Canvas
|
||||
HBox statusBar = new HBox(8, statusLbl, progress);
|
||||
statusBar.setAlignment(javafx.geometry.Pos.CENTER_LEFT);
|
||||
statusBar.setPadding(new Insets(4, 8, 4, 8));
|
||||
statusBar.setStyle("-fx-background-color: #2a2a3a;");
|
||||
|
||||
setupMouse();
|
||||
|
||||
getChildren().addAll(bar, progress, statusLbl, canvasPane);
|
||||
VBox.setVgrow(canvasPane, Priority.ALWAYS);
|
||||
getChildren().addAll(bar, canvasPane, statusBar);
|
||||
setStyle("-fx-background-color: #1a1a1a;");
|
||||
}
|
||||
|
||||
private static Button makeBtn(String label, javafx.event.EventHandler<javafx.event.ActionEvent> h) {
|
||||
Button b = new Button(label); b.setOnAction(h); return b;
|
||||
private static CheckBox layerCheck(String label, boolean selected) {
|
||||
CheckBox cb = new CheckBox(label);
|
||||
cb.setSelected(selected);
|
||||
return cb;
|
||||
}
|
||||
|
||||
public void setFullscreenCallbacks(Runnable enter, Runnable exit) {
|
||||
enterFullscreenCallback = enter;
|
||||
exitFullscreenCallback = exit;
|
||||
}
|
||||
|
||||
// ── Zoom-Zustand für Sync mit WorldMapView ────────────────────────────────
|
||||
|
||||
public double getScale() { return scale; }
|
||||
public double getPanX() { return panX; }
|
||||
public double getPanY() { return panY; }
|
||||
|
||||
public void setView(double newScale, double newPanX, double newPanY) {
|
||||
scale = newScale;
|
||||
panX = newPanX;
|
||||
panY = newPanY;
|
||||
if (zoomLevels != null) {
|
||||
int best = 0;
|
||||
double minDiff = Math.abs(zoomLevels[0] - scale);
|
||||
for (int i = 1; i < zoomLevels.length; i++) {
|
||||
double diff = Math.abs(zoomLevels[i] - scale);
|
||||
if (diff < minDiff) { minDiff = diff; best = i; }
|
||||
}
|
||||
zoomLevelIdx = best;
|
||||
}
|
||||
redraw();
|
||||
}
|
||||
|
||||
private void setupMouse() {
|
||||
@@ -100,12 +197,87 @@ public class CaveMapView extends VBox {
|
||||
panY = ds[3] + (e.getY()-ds[1]);
|
||||
redraw();
|
||||
});
|
||||
canvas.setOnMouseReleased(e -> {
|
||||
boolean wasDrag = Math.abs(e.getX() - ds[0]) >= 5 || Math.abs(e.getY() - ds[1]) >= 5;
|
||||
if (!wasDrag && labelBtn.isSelected()) {
|
||||
handleLabelPlacement(e.getX(), e.getY());
|
||||
}
|
||||
});
|
||||
canvas.setOnMouseMoved(e -> {
|
||||
if (mapFxImage == null) return;
|
||||
double wx = (e.getX() - panX) / scale / RENDER_SIZE * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF;
|
||||
double wz = (e.getY() - panY) / scale / RENDER_SIZE * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF;
|
||||
statusLbl.setText(String.format("X: %.1f Z: %.1f", wx, wz));
|
||||
});
|
||||
}
|
||||
|
||||
private void handleLabelPlacement(double canvasX, double canvasY) {
|
||||
if (cachedAreas.isEmpty()) return;
|
||||
float wx = toWorldX(canvasX);
|
||||
float wz = toWorldZ(canvasY);
|
||||
PlacedArea best = null;
|
||||
int bestIdx = -1;
|
||||
double bestSize = Double.MAX_VALUE;
|
||||
for (int i = 0; i < cachedAreas.size(); i++) {
|
||||
PlacedArea a = cachedAreas.get(i);
|
||||
if (WorldMapView.polyContainsStatic(a.pointsX(), a.pointsZ(), wx, wz)) {
|
||||
double sz = WorldMapView.boundingBoxAreaStatic(a.pointsX(), a.pointsZ());
|
||||
if (sz < bestSize) { bestSize = sz; best = a; bestIdx = i; }
|
||||
}
|
||||
}
|
||||
if (best != null) {
|
||||
cachedAreas.set(bestIdx, new PlacedArea(best.pointsX(), best.pointsZ(),
|
||||
best.areaId(), best.triggers(), wx, wz));
|
||||
try { AreaIO.save(cachedAreas); } catch (IOException ex) { statusLbl.setText("Fehler: " + ex.getMessage()); return; }
|
||||
redraw();
|
||||
statusLbl.setText(String.format("Label %s → (%.0f, %.0f)", best.areaId(), wx, wz));
|
||||
}
|
||||
}
|
||||
|
||||
private float toWorldX(double canvasX) {
|
||||
return (float)((canvasX - panX) / scale / (RENDER_SIZE - 1) * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF);
|
||||
}
|
||||
private float toWorldZ(double canvasZ) {
|
||||
return (float)((canvasZ - panY) / scale / (RENDER_SIZE - 1) * WorldMapRenderer.WORLD_SIZE - WorldMapRenderer.WORLD_HALF);
|
||||
}
|
||||
|
||||
private void exportPng() {
|
||||
FileChooser fc = new FileChooser();
|
||||
fc.setTitle("Höhlenkarte als PNG exportieren");
|
||||
fc.getExtensionFilters().add(new FileChooser.ExtensionFilter("PNG-Bild", "*.png"));
|
||||
fc.setInitialFileName("hoehlenkarte.png");
|
||||
File file = fc.showSaveDialog(stageSupplier.get());
|
||||
if (file == null) return;
|
||||
if (cachedMapData == null) { statusLbl.setText("Karte noch nicht geladen."); return; }
|
||||
statusLbl.setText("Exportiere 4096×4096 PNG…");
|
||||
progress.setVisible(true);
|
||||
MapData md = cachedMapData;
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
BufferedImage bi = WorldMapRenderer.buildCaveMap(md, 4096);
|
||||
ImageIO.write(bi, "PNG", file);
|
||||
Platform.runLater(() -> { progress.setVisible(false); statusLbl.setText("Exportiert: " + file.getName()); });
|
||||
} catch (IOException ex) {
|
||||
Platform.runLater(() -> { progress.setVisible(false); statusLbl.setText("Export-Fehler: " + ex.getMessage()); });
|
||||
}
|
||||
}, "CaveMapPngExport");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
public boolean isLoaded() { return mapFxImage != null; }
|
||||
/** True wenn Zoom-Levels korrekt initialisiert (Canvas hatte Größe > 0 beim Laden). */
|
||||
public boolean isViewInitialized() { return zoomLevels != null; }
|
||||
|
||||
public void ensureFitToView() { if (mapFxImage != null) fitToView(); }
|
||||
|
||||
public void fitCanvas() {
|
||||
double cw = canvasPane.getWidth(), ch = canvasPane.getHeight();
|
||||
if (cw <= 0 || ch <= 0 || mapFxImage == null) return;
|
||||
fitToViewWithSize(cw, ch);
|
||||
redraw();
|
||||
}
|
||||
|
||||
public void loadAndRender() {
|
||||
if (!loading.compareAndSet(false, true)) return;
|
||||
progress.setVisible(true);
|
||||
@@ -114,22 +286,64 @@ public class CaveMapView extends VBox {
|
||||
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
MapData mapData = MapIO.load();
|
||||
List<PlacedArea> areas = AreaIO.load();
|
||||
MapData mapData = MapIO.load();
|
||||
List<PlacedArea> areas = AreaIO.load();
|
||||
List<PlacedModel> models = PlacedModelIO.load();
|
||||
|
||||
BufferedImage bi = WorldMapRenderer.buildCaveMap(mapData, RENDER_SIZE);
|
||||
List<float[][]> paths = WorldMapView.loadPathChains();
|
||||
List<float[]> entrances = WorldMapView.computeTunnelEntrances(mapData);
|
||||
List<float[]> footprints = WorldMapView.computeBuildingFootprintsSimple(models);
|
||||
BufferedImage bi = WorldMapRenderer.buildCaveMap(mapData, RENDER_SIZE);
|
||||
// Für ingame-Nutzung speichern
|
||||
try {
|
||||
java.nio.file.Path cacheDir = java.nio.file.Paths.get(System.getProperty("user.home"), ".blight", "map_chunks");
|
||||
java.nio.file.Files.createDirectories(cacheDir);
|
||||
ImageIO.write(bi, "PNG", cacheDir.resolve("minimap_cave.png").toFile());
|
||||
} catch (Exception ex) {
|
||||
log.warn("[CaveMap] Konnte minimap_cave.png nicht speichern: {}", ex.getMessage());
|
||||
}
|
||||
List<float[][]> allPaths = WorldMapView.loadPathChains();
|
||||
List<float[]> entrances = WorldMapView.computeTunnelEntrances(mapData);
|
||||
List<float[]> allFootprints = WorldMapView.computeBuildingFootprintsSimple(models);
|
||||
|
||||
Platform.runLater(() -> statusLbl.setText("Berechne Cave-Maske…"));
|
||||
boolean[] caveMask = WorldMapRenderer.buildCaveFootprintMask(
|
||||
mapData, WorldMapRenderer.VOXEL_SURFACE_RES);
|
||||
|
||||
// Pfade: Cave-Maske + Y-basierte Trennung
|
||||
Platform.runLater(() -> statusLbl.setText("Berechne VoxelSurface…"));
|
||||
float[] voxelSurface = WorldMapRenderer.bakeVoxelSurface(mapData);
|
||||
List<float[][]>[] splitChains = WorldMapRenderer.splitPathChains(allPaths, voxelSurface, caveMask);
|
||||
List<float[][]> newContourPaths = WorldMapView.buildTunnelContourPaths(caveMask);
|
||||
List<float[][]> cavePaths = splitChains[1];
|
||||
|
||||
// Gebäude + Areas: Cave-Maske (keine Y-Info)
|
||||
List<float[]> caveFootprints = WorldMapRenderer.filterBuildingsByCave(allFootprints, caveMask, true);
|
||||
List<PlacedArea> caveAreas = WorldMapRenderer.filterAreasByCave(new ArrayList<>(areas), caveMask, true);
|
||||
|
||||
Platform.runLater(() -> {
|
||||
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
||||
cachedPathChains = paths;
|
||||
cachedPathChains = cavePaths;
|
||||
tunnelEntrances = entrances;
|
||||
cachedAreas = new ArrayList<>(areas);
|
||||
buildingFootprints = footprints;
|
||||
fitToView();
|
||||
tunnelContourPaths = newContourPaths;
|
||||
cachedAreas = caveAreas;
|
||||
buildingFootprints = caveFootprints;
|
||||
cachedMapData = mapData;
|
||||
if (zoomLevels == null) {
|
||||
fitToView(); // Erstmalig: Zoom initialisieren
|
||||
} else {
|
||||
double cw = canvas.getWidth(), ch = canvas.getHeight();
|
||||
if (cw > 0 && ch > 0 && mapFxImage != null) {
|
||||
double fit = Math.min(cw / mapFxImage.getWidth(), ch / mapFxImage.getHeight());
|
||||
double ratio = Math.pow(ZOOM_MAX_SCALE / fit, 1.0 / (ZOOM_LEVELS - 1));
|
||||
zoomLevels[0] = fit;
|
||||
for (int i = 1; i < ZOOM_LEVELS; i++) zoomLevels[i] = zoomLevels[i-1] * ratio;
|
||||
int best = 0;
|
||||
double minDiff = Math.abs(zoomLevels[0] - scale);
|
||||
for (int i = 1; i < ZOOM_LEVELS; i++) {
|
||||
double d = Math.abs(zoomLevels[i] - scale);
|
||||
if (d < minDiff) { minDiff = d; best = i; }
|
||||
}
|
||||
zoomLevelIdx = best;
|
||||
}
|
||||
}
|
||||
redraw();
|
||||
progress.setVisible(false);
|
||||
statusLbl.setText("Höhlenkarte bereit.");
|
||||
@@ -151,6 +365,10 @@ public class CaveMapView extends VBox {
|
||||
private void fitToView() {
|
||||
double cw = canvas.getWidth(), ch = canvas.getHeight();
|
||||
if (cw <= 0 || ch <= 0 || mapFxImage == null) return;
|
||||
fitToViewWithSize(cw, ch);
|
||||
}
|
||||
|
||||
private void fitToViewWithSize(double cw, double ch) {
|
||||
double fit = Math.min(cw / mapFxImage.getWidth(), ch / mapFxImage.getHeight());
|
||||
zoomLevels = new double[ZOOM_LEVELS];
|
||||
zoomLevels[0] = fit;
|
||||
@@ -162,26 +380,64 @@ public class CaveMapView extends VBox {
|
||||
panY = (ch - mapFxImage.getHeight() * scale) / 2.0;
|
||||
}
|
||||
|
||||
private void enterFullscreen() {
|
||||
isFullscreen = true;
|
||||
savedScale = scale;
|
||||
savedPanX = panX;
|
||||
savedPanY = panY;
|
||||
fullscreenBtn.setVisible(false);
|
||||
backBtn.setVisible(true);
|
||||
if (enterFullscreenCallback != null) enterFullscreenCallback.run();
|
||||
}
|
||||
|
||||
private void exitFullscreen() {
|
||||
isFullscreen = false;
|
||||
scale = savedScale;
|
||||
panX = savedPanX;
|
||||
panY = savedPanY;
|
||||
backBtn.setVisible(false);
|
||||
fullscreenBtn.setVisible(true);
|
||||
if (exitFullscreenCallback != null) exitFullscreenCallback.run();
|
||||
}
|
||||
|
||||
private void redraw() {
|
||||
if (!autoLoaded && mapFxImage == null && !loading.get()) {
|
||||
autoLoaded = true;
|
||||
loadAndRender();
|
||||
// Nachgelagerte Initialisierung: Canvas hatte beim Laden evtl. Größe 0
|
||||
if (mapFxImage != null && zoomLevels == null) {
|
||||
fitToView();
|
||||
}
|
||||
GraphicsContext gc = canvas.getGraphicsContext2D();
|
||||
double w = canvas.getWidth(), h = canvas.getHeight();
|
||||
gc.setFill(Color.rgb(14, 14, 14));
|
||||
gc.setFill(Color.rgb(28, 28, 28)); // passt zu buildCaveMap-Hintergrund 0x1c1c1c
|
||||
gc.fillRect(0, 0, w, h);
|
||||
if (mapFxImage == null) {
|
||||
gc.setFill(Color.GRAY);
|
||||
gc.fillText("Höhlenkarte wird geladen…", 20, 40);
|
||||
return;
|
||||
}
|
||||
gc.setImageSmoothing(true);
|
||||
gc.drawImage(mapFxImage, panX, panY, mapFxImage.getWidth() * scale, mapFxImage.getHeight() * scale);
|
||||
drawTunnelEntrancesOverlay(gc);
|
||||
drawBuildingOverlay(gc);
|
||||
drawPathsOverlay(gc);
|
||||
drawAreasOverlay(gc);
|
||||
// Vektor-Füllung: scharf bei jedem Zoom-Level
|
||||
if (!tunnelContourPaths.isEmpty()) {
|
||||
gc.save();
|
||||
gc.setFill(Color.rgb(182, 138, 80)); // Dirt-Farbe 0xb68a50
|
||||
for (float[][] path : tunnelContourPaths) {
|
||||
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.fill();
|
||||
}
|
||||
gc.restore();
|
||||
} else {
|
||||
// Pixel-Fallback, falls Konturpfade noch nicht berechnet
|
||||
gc.setImageSmoothing(true);
|
||||
gc.drawImage(mapFxImage, panX, panY, mapFxImage.getWidth() * scale, mapFxImage.getHeight() * scale);
|
||||
}
|
||||
if (cbEntrances.isSelected()) {
|
||||
drawTunnelContourOverlay(gc);
|
||||
drawTunnelEntrancesOverlay(gc);
|
||||
}
|
||||
if (cbBuildings.isSelected()) drawBuildingOverlay(gc);
|
||||
if (cbPaths.isSelected()) drawPathsOverlay(gc);
|
||||
if (cbAreas.isSelected()) drawAreasOverlay(gc);
|
||||
}
|
||||
|
||||
private double toCanvasX(float wx) {
|
||||
@@ -191,10 +447,58 @@ public class CaveMapView extends VBox {
|
||||
return panY + (wz + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (RENDER_SIZE-1) * scale;
|
||||
}
|
||||
|
||||
private double overlayLineWidth() {
|
||||
return switch (zoomLevelIdx) {
|
||||
case 3 -> 2.5;
|
||||
case 4 -> 3.5;
|
||||
default -> 1.2;
|
||||
};
|
||||
}
|
||||
|
||||
private double roadLineWidth() {
|
||||
return switch (zoomLevelIdx) {
|
||||
case 3 -> 4.5;
|
||||
case 4 -> 6.0;
|
||||
default -> 3.0;
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
if (tunnelEntrances.isEmpty()) return;
|
||||
gc.save();
|
||||
gc.setFill(Color.BLACK);
|
||||
gc.setStroke(Color.BLACK);
|
||||
gc.setLineWidth(1.0);
|
||||
for (float[] te : tunnelEntrances) {
|
||||
double r = Math.max(4.0, Math.min(
|
||||
te[4] / WorldMapRenderer.WORLD_SIZE * (RENDER_SIZE-1) * scale, 20.0));
|
||||
@@ -211,15 +515,19 @@ public class CaveMapView extends VBox {
|
||||
private void drawBuildingOverlay(GraphicsContext gc) {
|
||||
if (buildingFootprints.isEmpty()) return;
|
||||
gc.save();
|
||||
gc.setFill(Color.rgb(170, 140, 90, 0.55));
|
||||
gc.setStroke(Color.rgb(80, 60, 30, 0.8));
|
||||
gc.setLineWidth(1.0);
|
||||
gc.setFill(Color.rgb(220, 100, 80, 0.50));
|
||||
gc.setStroke(Color.rgb(160, 30, 30, 0.90));
|
||||
gc.setLineWidth(overlayLineWidth());
|
||||
gc.setLineDashes();
|
||||
for (float[] fp : buildingFootprints) {
|
||||
double cx = toCanvasX(fp[0]);
|
||||
double cz = toCanvasZ(fp[1]);
|
||||
double hw = fp[2] / WorldMapRenderer.WORLD_SIZE * (RENDER_SIZE-1) * scale;
|
||||
double hd = fp[3] / WorldMapRenderer.WORLD_SIZE * (RENDER_SIZE-1) * scale;
|
||||
if (hw < 0.5 || hd < 0.5) continue;
|
||||
gc.save();
|
||||
gc.translate(toCanvasX(fp[0]), toCanvasZ(fp[1]));
|
||||
gc.rotate(Math.toDegrees(fp[4]));
|
||||
gc.translate(cx, cz);
|
||||
gc.rotate(Math.toDegrees(-fp[4]));
|
||||
gc.fillRect(-hw, -hd, hw*2, hd*2);
|
||||
gc.strokeRect(-hw, -hd, hw*2, hd*2);
|
||||
gc.restore();
|
||||
@@ -234,10 +542,10 @@ public class CaveMapView extends VBox {
|
||||
gc.setLineJoin(StrokeLineJoin.ROUND);
|
||||
gc.setLineDashes();
|
||||
gc.setStroke(Color.rgb(20, 12, 5, 0.82));
|
||||
gc.setLineWidth(5.0);
|
||||
gc.setLineWidth(roadLineWidth() + 2.0);
|
||||
for (float[][] ch : cachedPathChains) drawChain(gc, ch);
|
||||
gc.setStroke(Color.rgb(148, 105, 55, 0.92));
|
||||
gc.setLineWidth(3.0);
|
||||
gc.setLineWidth(roadLineWidth());
|
||||
for (float[][] ch : cachedPathChains) drawChain(gc, ch);
|
||||
gc.restore();
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.ArcType;
|
||||
import javafx.scene.shape.StrokeLineCap;
|
||||
import javafx.scene.shape.StrokeLineJoin;
|
||||
import javafx.scene.text.Font;
|
||||
@@ -128,8 +127,6 @@ public class WorldMapView extends VBox {
|
||||
|
||||
private List<float[]> buildingFootprints = new ArrayList<>();
|
||||
private List<float[][]> cachedPathChains = new ArrayList<>();
|
||||
/** Tunneleingang-Daten: [worldX, worldZ, dirX, dirZ, radiusWorld] */
|
||||
private List<float[]> tunnelEntrances = new ArrayList<>();
|
||||
private static final java.util.concurrent.ConcurrentHashMap<String, float[]> MODEL_BOUNDS_CACHE
|
||||
= new java.util.concurrent.ConcurrentHashMap<>();
|
||||
private static final float[] DEFAULT_EXTENTS = {5f, 5f};
|
||||
@@ -269,6 +266,9 @@ public class WorldMapView extends VBox {
|
||||
canvas.setOnMouseDragged(e -> {
|
||||
panX = dragStartPanX + (e.getX() - dragStartX);
|
||||
panY = dragStartPanY + (e.getY() - dragStartY);
|
||||
double iw = RENDER_SIZE * scale, ih = iw;
|
||||
panX = Math.max(50 - iw, Math.min(canvas.getWidth() - 50, panX));
|
||||
panY = Math.max(50 - ih, Math.min(canvas.getHeight() - 50, panY));
|
||||
redraw();
|
||||
});
|
||||
canvas.setOnMouseReleased(e -> {
|
||||
@@ -293,6 +293,9 @@ public class WorldMapView extends VBox {
|
||||
scale = zoomLevels[zoomLevelIdx];
|
||||
panX = e.getX() - (e.getX() - panX) * scale / oldScale;
|
||||
panY = e.getY() - (e.getY() - panY) * scale / oldScale;
|
||||
double iw = RENDER_SIZE * scale, ih = iw;
|
||||
panX = Math.max(50 - iw, Math.min(canvas.getWidth() - 50, panX));
|
||||
panY = Math.max(50 - ih, Math.min(canvas.getHeight() - 50, panY));
|
||||
redraw();
|
||||
});
|
||||
|
||||
@@ -331,6 +334,31 @@ public class WorldMapView extends VBox {
|
||||
public boolean isMapReady() { return mapFxImage != null; }
|
||||
public String getLoadingStatus() { return statusLbl.getText(); }
|
||||
|
||||
public double getScale() { return scale; }
|
||||
public double getPanX() { return panX; }
|
||||
public double getPanY() { return panY; }
|
||||
|
||||
/** Setzt Zoom und Position direkt (für Tab-Wechsel-Sync). */
|
||||
public void setView(double newScale, double newPanX, double newPanY) {
|
||||
scale = newScale;
|
||||
panX = newPanX;
|
||||
panY = newPanY;
|
||||
if (zoomLevels == null && canvas.getWidth() > 0 && mapFxImage != null) {
|
||||
double fitScale = Math.min(canvas.getWidth() / RENDER_SIZE, canvas.getHeight() / RENDER_SIZE);
|
||||
computeZoomLevels(fitScale);
|
||||
}
|
||||
if (zoomLevels != null) {
|
||||
int best = 0;
|
||||
double minDiff = Math.abs(zoomLevels[0] - scale);
|
||||
for (int i = 1; i < zoomLevels.length; i++) {
|
||||
double diff = Math.abs(zoomLevels[i] - scale);
|
||||
if (diff < minDiff) { minDiff = diff; best = i; }
|
||||
}
|
||||
zoomLevelIdx = best;
|
||||
}
|
||||
redraw();
|
||||
}
|
||||
|
||||
/** Stellt sicher, dass die Karte beim ersten Anzeigen korrekt skaliert/positioniert ist. */
|
||||
public void ensureFitToView() {
|
||||
if (mapFxImage != null && zoomLevels == null) {
|
||||
@@ -338,6 +366,9 @@ public class WorldMapView extends VBox {
|
||||
}
|
||||
}
|
||||
|
||||
/** True wenn Zoom-Levels korrekt initialisiert (Canvas hatte Größe > 0 beim Laden). */
|
||||
public boolean isViewInitialized() { return zoomLevels != null; }
|
||||
|
||||
/** Lädt alle Weltdaten neu und baut das Render-Modell (teuer: I/O + Berechnungen). */
|
||||
public void loadAndRender() {
|
||||
if (loading.getAndSet(true)) return;
|
||||
@@ -371,6 +402,12 @@ public class WorldMapView extends VBox {
|
||||
BufferedImage bi = WorldMapRenderer.render(model, RENDER_SIZE, buildBackgroundOptions());
|
||||
log.debug("[WorldMap] Hintergrund-PNG gerendert ({}×{} px, {} ms)",
|
||||
RENDER_SIZE, RENDER_SIZE, System.currentTimeMillis() - t1);
|
||||
// Für ingame-Nutzung: Hintergrund-PNG speichern
|
||||
try {
|
||||
javax.imageio.ImageIO.write(bi, "PNG", CHUNK_CACHE_DIR.resolve("minimap_world.png").toFile());
|
||||
} catch (Exception ex) {
|
||||
log.warn("[WorldMap] Konnte minimap_world.png nicht speichern: {}", ex.getMessage());
|
||||
}
|
||||
|
||||
float[] fDensity = computeForestDensity(model);
|
||||
int[] bakedFstPx = computeForestPixels(model, fDensity, 3.0f);
|
||||
@@ -378,9 +415,19 @@ public class WorldMapView extends VBox {
|
||||
List<float[][]> newCliffPaths = buildCliffPaths(model.mapData(), model.voxelSurface());
|
||||
List<float[][]> newForestPaths = buildForestOutlinePaths(fDensity, 512, 3.0f);
|
||||
|
||||
List<float[]> newBuildingFootprints = computeBuildingFootprints(model, findAssetRoot());
|
||||
List<float[][]> newPathChains = loadPathChains();
|
||||
List<float[]> newTunnelEntrances = computeTunnelEntrances(model.mapData());
|
||||
List<float[]> allBuildingFootprints = computeBuildingFootprints(model, findAssetRoot());
|
||||
List<float[][]> allPathChains = loadPathChains();
|
||||
|
||||
Platform.runLater(() -> statusLbl.setText("Berechne Cave-Maske…"));
|
||||
boolean[] caveMask = WorldMapRenderer.buildCaveFootprintMask(
|
||||
model.mapData(), WorldMapRenderer.VOXEL_SURFACE_RES);
|
||||
// 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 = WorldMapRenderer.splitPathChains(allPathChains, vsForPaths, caveMask);
|
||||
List<float[][]> newPathChains = splitChains[0]; // nur Oberfläche
|
||||
List<float[]> newBuildingFootprints = WorldMapRenderer.filterBuildingsByCave(allBuildingFootprints, caveMask, false);
|
||||
List<PlacedArea> surfaceAreas = WorldMapRenderer.filterAreasByCave(new ArrayList<>(model.areas()), caveMask, false);
|
||||
|
||||
// Hires-Chunks im Hintergrund generieren (nach dem UI-Update, nicht-blockierend)
|
||||
final WorldMapRenderModel chunkModel = model;
|
||||
@@ -393,7 +440,7 @@ public class WorldMapView extends VBox {
|
||||
|
||||
Platform.runLater(() -> {
|
||||
currentModel = model;
|
||||
cachedAreas = new ArrayList<>(model.areas());
|
||||
cachedAreas = surfaceAreas;
|
||||
cachedLocs = new ArrayList<>(model.locations());
|
||||
mapBuffered = bi;
|
||||
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
||||
@@ -403,8 +450,22 @@ public class WorldMapView extends VBox {
|
||||
forestPaths = newForestPaths;
|
||||
buildingFootprints = newBuildingFootprints;
|
||||
cachedPathChains = newPathChains;
|
||||
tunnelEntrances = newTunnelEntrances;
|
||||
fitToView();
|
||||
if (zoomLevels == null) {
|
||||
fitToView(); // Erstmalig: Zoom initialisieren und auf Karte fitten
|
||||
} else {
|
||||
// Erneuter Load (Refresh): Zoom-Levels neu berechnen falls Canvas geändert,
|
||||
// aber aktuelle Scale und Pan-Position beibehalten.
|
||||
double fitScale = canvas.getWidth() > 0
|
||||
? Math.min(canvas.getWidth() / RENDER_SIZE, canvas.getHeight() / RENDER_SIZE) : 1.0;
|
||||
computeZoomLevels(fitScale);
|
||||
int best = 0;
|
||||
double minDiff = Math.abs(zoomLevels[0] - scale);
|
||||
for (int i = 1; i < zoomLevels.length; i++) {
|
||||
double d = Math.abs(zoomLevels[i] - scale);
|
||||
if (d < minDiff) { minDiff = d; best = i; }
|
||||
}
|
||||
zoomLevelIdx = best;
|
||||
}
|
||||
redraw();
|
||||
progress.setVisible(false);
|
||||
statusLbl.setText("Bereit – " + model.areas().size() + " Areas, " + model.waters().size() + " Wasser");
|
||||
@@ -480,8 +541,8 @@ public class WorldMapView extends VBox {
|
||||
}
|
||||
// Layer 2: Klippen (Grau-Füllung + Schraffierung + Vektor-Kontur)
|
||||
if (cbCliffs.isSelected()) drawCliffOverlay(gc);
|
||||
// Layer 2.5: Tunneleingänge (schwarze Halbkreise)
|
||||
if (cbTunnels.isSelected()) drawTunnelEntrancesOverlay(gc);
|
||||
// Layer 2.5: Tunnel-Fläche (gefülltes Polygon + Kontur)
|
||||
if (cbTunnels.isSelected()) drawTunnelOverlay(gc);
|
||||
// Layer 3: Wald (Füllung + Vektor-Kontur) – unter Wasser
|
||||
if (cbForest.isSelected()) drawForestOverlay(gc);
|
||||
// Layer 3.5: Gebäude (Füllung + Vektor-Kontur) – unter Wasser
|
||||
@@ -809,14 +870,14 @@ public class WorldMapView extends VBox {
|
||||
|
||||
List<float[]> chain = new ArrayList<>();
|
||||
de.blight.common.path.PathNode startNode = nodeMap.get(jUuid);
|
||||
chain.add(new float[]{startNode.getPosition().x, startNode.getPosition().z});
|
||||
chain.add(new float[]{startNode.getPosition().x, startNode.getPosition().z, startNode.getPosition().y});
|
||||
|
||||
String prev = jUuid, curr = nb;
|
||||
while (curr != null) {
|
||||
visitedEdges.add(pathEdgeKey(prev, curr));
|
||||
de.blight.common.path.PathNode currNode = nodeMap.get(curr);
|
||||
if (currNode == null) break;
|
||||
chain.add(new float[]{currNode.getPosition().x, currNode.getPosition().z});
|
||||
chain.add(new float[]{currNode.getPosition().x, currNode.getPosition().z, currNode.getPosition().y});
|
||||
if (junctions.contains(curr)) break;
|
||||
String next = null;
|
||||
for (String n2 : adj.getOrDefault(curr, java.util.Collections.emptyList())) {
|
||||
@@ -1013,35 +1074,20 @@ public class WorldMapView extends VBox {
|
||||
result.add(new float[]{wx, wz, dirX, dirZ, radius});
|
||||
}
|
||||
|
||||
private void drawTunnelEntrancesOverlay(GraphicsContext gc) {
|
||||
if (tunnelEntrances.isEmpty()) return;
|
||||
gc.save();
|
||||
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));
|
||||
private void drawTunnelOverlay(GraphicsContext gc) { /* Kein Tunnel-Overlay auf Weltkarte */ }
|
||||
|
||||
// Rotate so the D-curve points toward the solid (into the cliff)
|
||||
double angleDeg = Math.toDegrees(Math.atan2(dirZ, dirX));
|
||||
|
||||
gc.save();
|
||||
gc.translate(cx, cz);
|
||||
gc.rotate(angleDeg);
|
||||
// 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();
|
||||
static List<float[][]> buildTunnelContourPaths(boolean[] caveMask) {
|
||||
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<>();
|
||||
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 List<float[]> computeBuildingFootprints(WorldMapRenderModel model, Path assetRoot) {
|
||||
private static List<float[]> computeBuildingFootprints(WorldMapRenderModel model, Path assetRoot) {
|
||||
List<float[]> result = new ArrayList<>();
|
||||
for (PlacedModel pm : model.models()) {
|
||||
if (!isBuilding(pm)) continue;
|
||||
@@ -1062,6 +1108,11 @@ public class WorldMapView extends VBox {
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Cave-Mask: Trennung Oberfläche / Untergrund ───────────────────────────
|
||||
|
||||
static final int CAVE_MASK_SIZE = 1024;
|
||||
|
||||
/** Lädt die Cave-Tunnel-Maske (true = Untergrund/Tunnel). */
|
||||
private static WritableImage toWritableImage(int[] pixels, int size) {
|
||||
WritableImage img = new WritableImage(size, size);
|
||||
img.getPixelWriter().setPixels(0, 0, size, size, PixelFormat.getIntArgbInstance(), pixels, 0, size);
|
||||
@@ -1225,23 +1276,17 @@ public class WorldMapView extends VBox {
|
||||
return i;
|
||||
}
|
||||
|
||||
/** Berechnet voxelSurface (höchstes Solid-Voxel pro Spalte) aus Live- oder Disk-Chunks. */
|
||||
private float[] bakeVoxelHeights(MapData mapData) {
|
||||
int VS = WorldMapRenderer.VOXEL_SURFACE_RES;
|
||||
// Bevorzuge Live-Chunks aus dem Editor-Speicher, falle auf Disk zurück
|
||||
List<VoxelChunk> chunks = voxelChunkSupplier != null
|
||||
? voxelChunkSupplier.get()
|
||||
: VoxelChunkIO.loadAll();
|
||||
boolean allEmpty = chunks.stream().allMatch(VoxelChunk::isEmpty);
|
||||
if (allEmpty) { chunks = VoxelChunkIO.loadAll(); }
|
||||
if (chunks.isEmpty()) {
|
||||
float[] empty = new float[VS * VS];
|
||||
java.util.Arrays.fill(empty, Float.NaN);
|
||||
return empty;
|
||||
}
|
||||
// Kern-Logik ist dieselbe wie WorldMapRenderer.bakeVoxelSurface, aber mit
|
||||
// Live-Chunks statt VoxelChunkIO.loadAll().
|
||||
float[] voxelSurface = new float[VS * VS];
|
||||
java.util.Arrays.fill(voxelSurface, Float.NaN);
|
||||
if (chunks.isEmpty()) { return voxelSurface; }
|
||||
float WH = WorldMapRenderer.WORLD_HALF;
|
||||
float WS = WorldMapRenderer.WORLD_SIZE;
|
||||
int UV = MapData.UPPER_VERTS;
|
||||
@@ -1645,6 +1690,9 @@ public class WorldMapView extends VBox {
|
||||
}
|
||||
}
|
||||
|
||||
static boolean polyContainsStatic(float[] xs, float[] zs, float px, float pz) { return polyContains(xs, zs, px, pz); }
|
||||
static double boundingBoxAreaStatic(float[] xs, float[] zs) { return boundingBoxArea(xs, zs); }
|
||||
|
||||
private static boolean polyContains(float[] xs, float[] zs, float px, float pz) {
|
||||
boolean inside = false;
|
||||
int n = xs.length;
|
||||
|
||||
@@ -114,12 +114,12 @@ public class CompassHudState extends BaseAppState {
|
||||
roseH = roseTex.getImage().getHeight();
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
// Kompass zentriert über der Minimap (links unten)
|
||||
float cx = MinimapState.MARGIN + MinimapState.MINIMAP_SIZE / 2f;
|
||||
float cy = MinimapState.MARGIN + MinimapState.MINIMAP_SIZE + GAP + roseH / 2f;
|
||||
// Kompass-Mittelpunkt: 220px vom linken und unteren Bildschirmrand
|
||||
float cx = 240f;
|
||||
float cy = 240f;
|
||||
|
||||
compassNode = new Node("compass");
|
||||
compassNode.setLocalTranslation(cx, cy, 0f);
|
||||
compassNode.setLocalTranslation(cx, cy, 60f); // über Minimap (max z=51)
|
||||
|
||||
roseNode = new Node("rose");
|
||||
compassNode.attachChild(roseNode);
|
||||
|
||||
@@ -54,7 +54,6 @@ import de.blight.game.scene.WorldScene;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.nio.file.Files;
|
||||
@@ -73,17 +72,33 @@ public class MinimapState extends BaseAppState {
|
||||
|
||||
private static final float WORLD_HALF = WorldMapRenderer.WORLD_HALF;
|
||||
private static final float WORLD_SIZE = WorldMapRenderer.WORLD_SIZE;
|
||||
public static final float MINIMAP_SIZE = 200f;
|
||||
public static final float MINIMAP_SIZE = 170f; // Karte: 170px Durchmesser im Rahmen
|
||||
public static final float MINIMAP_RADIUS = MINIMAP_SIZE / 2f; // = 85px
|
||||
public static final float MARGIN = 16f;
|
||||
public static final float BORDER_DISPLAY = 256f; // Rahmen in natürlicher Größe
|
||||
// Versatz vom Rahmen-Ursprung zur linken/unteren Kante des Kreises
|
||||
private static final float MINIMAP_OFFSET = (BORDER_DISPLAY - MINIMAP_SIZE) / 2f; // = 43px
|
||||
private static final float VIEW_RADIUS_DEF = 100f; // Minimap: 100 m Sichtradius (fest)
|
||||
private static final float FM_VIEW_DEF = 200f; // Vollbild: Öffnungs-Radius 200 m
|
||||
private static final float FM_VIEW_MIN = 50f; // Vollbild: engster Ausschnitt
|
||||
private static final float FM_VIEW_MAX = WORLD_HALF; // Vollbild: ganze Welt
|
||||
// 5 diskrete Zoom-Stufen (identisch zum Editor: 1×, 2×, 4×, 8×, 16× fit)
|
||||
private static final float[] FM_ZOOM_RADII = { WORLD_HALF, 512f, 200f, 100f, 50f };
|
||||
private static final int FM_ZOOM_DEFAULT = 2; // Index 2 = 200 m
|
||||
private static final int TEXTURE_SIZE = 2048;
|
||||
private static final int VEC_TEX_SIZE = 1024; // Vektor-Overlay-Textur (neu gerastert bei Zoom/Pan)
|
||||
private static final float DOT_SIZE = 8f;
|
||||
private static final float MARK_DIST_SQ = 4f * 4f;
|
||||
private static final float AUTOSAVE_SEC = 60f;
|
||||
private static final int MINIMAP_SEGS = 64;
|
||||
|
||||
// Vorberechnete cos/sin für die Kreissegmente (einmal beim Laden)
|
||||
private static final float[] SEG_COS = new float[MINIMAP_SEGS];
|
||||
private static final float[] SEG_SIN = new float[MINIMAP_SEGS];
|
||||
static {
|
||||
for (int i = 0; i < MINIMAP_SEGS; i++) {
|
||||
double a = 2 * Math.PI * i / MINIMAP_SEGS;
|
||||
SEG_COS[i] = (float) Math.cos(a);
|
||||
SEG_SIN[i] = (float) Math.sin(a);
|
||||
}
|
||||
}
|
||||
|
||||
private static final String ACT_MAP_TOGGLE = "_MinimapToggle_";
|
||||
private static final String ACT_MAP_ESC = "_MinimapEsc_";
|
||||
@@ -97,13 +112,22 @@ public class MinimapState extends BaseAppState {
|
||||
private Camera camera;
|
||||
private boolean textureReady = false;
|
||||
|
||||
// Frisch gerenderte Karten-Images (vom Render-Thread, ausgewertet in onTextureReady im GL-Thread)
|
||||
private volatile BufferedImage pendingWorldBi;
|
||||
private volatile BufferedImage pendingCaveBi;
|
||||
// Direkt aus BufferedImage erzeugte JME3-Texturen (kein AssetManager-Cache, immer aktuell)
|
||||
private Texture2D worldMapTex;
|
||||
private Texture2D caveMapTex;
|
||||
// Vorberechnete Overlay-Daten (Pfade, Dichte-Maps) – einmalig beim Laden gecacht
|
||||
private volatile de.blight.common.map.WorldMapRenderer.MapOverlayData overlayData;
|
||||
private volatile de.blight.common.map.WorldMapRenderModel renderModel;
|
||||
|
||||
// ── Erkundungs-Nebel ──────────────────────────────────────────────────────
|
||||
|
||||
private ExploreTracker exploreTracker;
|
||||
private Material fogMat; // Minimap-Nebel
|
||||
private Material fogFullMat; // Vollbild-Nebel (eigene Overlay-Bounds)
|
||||
private float fogTime = 0f;
|
||||
private float posUpdateTimer = 0f;
|
||||
|
||||
// ── Blur-Filter (aktiv wenn Vollbild-Karte offen) ─────────────────────────
|
||||
|
||||
@@ -119,10 +143,10 @@ public class MinimapState extends BaseAppState {
|
||||
private Geometry vectorGeo;
|
||||
private volatile boolean vecRenderPending = false;
|
||||
private volatile boolean vecLayerDirty = false;
|
||||
// UV-Position des letzten abgeschlossenen Vektor-Renders (für Live-Verschiebung ohne Ghost)
|
||||
private float vecRenderU = 0.5f;
|
||||
private float vecRenderV = 0.5f;
|
||||
private float vecRenderHU = 0.5f;
|
||||
// UV-Mittelpunkt und Halbbreite des zuletzt abgeschlossenen Renders (für Live-Pan-Tracking)
|
||||
private float vecRenderCenterU = 0.5f;
|
||||
private float vecRenderCenterV = 0.5f;
|
||||
private float vecRenderHU = 0f;
|
||||
private float lastMarkX = Float.NaN;
|
||||
private float lastMarkZ = Float.NaN;
|
||||
private float autoSaveTimer = 0f;
|
||||
@@ -154,13 +178,14 @@ public class MinimapState extends BaseAppState {
|
||||
private Geometry caveGeoMini = null; // Höhlenkarte über Minimap
|
||||
private Geometry caveGeoFull = null; // Höhlenkarte über Vollkarte
|
||||
private float caveBlend = 0f; // 0 = Außen, 1 = Höhle
|
||||
/** upperHole-Maske der geladenen Karte für Laufzeit-Erkennung; null = nicht verfügbar. */
|
||||
private byte[] upperHoleMask = null;
|
||||
/** Cave-Fußabdruck-Maske für Laufzeit-Erkennung (via WorldMapRenderer.isCavePoint). */
|
||||
private boolean[] upperHoleMask = null;
|
||||
|
||||
private float fmMapSize;
|
||||
private float fmMapOriginX;
|
||||
private float fmMapOriginY;
|
||||
private float fmViewRadius = FM_VIEW_DEF;
|
||||
private int fmZoomLevelIdx = FM_ZOOM_DEFAULT;
|
||||
private float fmViewRadius = FM_ZOOM_RADII[FM_ZOOM_DEFAULT];
|
||||
private float fmCenterU = 0.5f;
|
||||
private float fmCenterV = 0.5f;
|
||||
private boolean fmDragging = false;
|
||||
@@ -231,12 +256,8 @@ public class MinimapState extends BaseAppState {
|
||||
if (fogMat != null) { fogMat.setFloat("Time", fogTime); }
|
||||
if (fogFullMat != null) { fogFullMat.setFloat("Time", fogTime); }
|
||||
|
||||
posUpdateTimer += tpf;
|
||||
boolean doPosUpdate = posUpdateTimer >= 1f;
|
||||
if (doPosUpdate) posUpdateTimer = 0f;
|
||||
|
||||
if (!fullMapOpen) {
|
||||
if (doPosUpdate) updateMinimapUV(px, pz);
|
||||
updateMinimapUV(px, pz);
|
||||
} else {
|
||||
if (fmDragging) {
|
||||
Vector2f cursor = app.getInputManager().getCursorPosition();
|
||||
@@ -248,11 +269,11 @@ public class MinimapState extends BaseAppState {
|
||||
float uvPerPixel = (2f * fmViewRadius / WORLD_SIZE) / fmMapSize;
|
||||
fmCenterU -= dx * uvPerPixel;
|
||||
fmCenterV -= dy * uvPerPixel;
|
||||
// Nur UV des Hintergrunds verschieben – kein Re-Render während des Ziehens
|
||||
updateFullMapUV();
|
||||
scheduleVectorRedraw();
|
||||
}
|
||||
}
|
||||
if (doPosUpdate) updateFullMapPlayerDot(px, pz);
|
||||
updateFullMapPlayerDot(px, pz);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,17 +301,17 @@ public class MinimapState extends BaseAppState {
|
||||
|
||||
private void tickCaveBlend(float px, float py, float pz, float tpf) {
|
||||
if (upperHoleMask == null || caveGeoMini == null) return;
|
||||
int C = MapData.UPPER_CELLS;
|
||||
int cx = Math.max(0, Math.min(C-1, (int)((px + WORLD_HALF) / WORLD_SIZE * C)));
|
||||
int cz = Math.max(0, Math.min(C-1, (int)((pz + WORLD_HALF) / WORLD_SIZE * C)));
|
||||
boolean inHole = upperHoleMask[cz * C + cx] != 0;
|
||||
boolean inHole = WorldMapRenderer.isCavePoint(upperHoleMask, px, pz);
|
||||
float target = inHole ? 1f : 0f;
|
||||
if (caveBlend != target) {
|
||||
float speed = 1.5f; // blend completes in ~0.67s
|
||||
float speed = 5f; // ~1 Sekunde Übergang (exponentiell, 99% nach 1s bei 60fps)
|
||||
caveBlend += (target - caveBlend) * Math.min(tpf * speed, 1f);
|
||||
if (Math.abs(caveBlend - target) < 0.005f) caveBlend = target;
|
||||
ColorRGBA col = new ColorRGBA(1f, 1f, 1f, caveBlend);
|
||||
caveGeoMini.getMaterial().setColor("Color", col);
|
||||
if (caveGeoFull != null) {
|
||||
caveGeoFull.getMaterial().setColor("Color", col);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,14 +340,15 @@ public class MinimapState extends BaseAppState {
|
||||
sinA = FastMath.sin(yaw);
|
||||
}
|
||||
|
||||
// minimapMesh wird von minimapGeo UND fogMiniGeo geteilt
|
||||
// Vertex-Reihenfolge: BL(-1,-1), BR(+1,-1), TR(+1,+1), TL(-1,+1)
|
||||
float[] nx = { -1f, 1f, 1f, -1f };
|
||||
float[] ny = { -1f, -1f, 1f, 1f };
|
||||
// Kreis-Mesh: Vertex 0 = Zentrum, Vertices 1..MINIMAP_SEGS = Rand
|
||||
FloatBuffer uv = (FloatBuffer) minimapMesh.getBuffer(VertexBuffer.Type.TexCoord).getData();
|
||||
for (int i = 0; i < 4; i++) {
|
||||
uv.put(i * 2, cu + (nx[i] * cosA + ny[i] * sinA) * hu);
|
||||
uv.put(i * 2 + 1, cv + (-nx[i] * sinA + ny[i] * cosA) * hu);
|
||||
uv.put(0, cu);
|
||||
uv.put(1, cv);
|
||||
for (int i = 0; i < MINIMAP_SEGS; i++) {
|
||||
float nx = SEG_COS[i];
|
||||
float ny = SEG_SIN[i];
|
||||
uv.put((i + 1) * 2, cu + (nx * cosA + ny * sinA) * hu);
|
||||
uv.put((i + 1) * 2 + 1, cv + (-nx * sinA + ny * cosA) * hu);
|
||||
}
|
||||
minimapMesh.getBuffer(VertexBuffer.Type.TexCoord).setUpdateNeeded();
|
||||
}
|
||||
@@ -344,14 +366,11 @@ public class MinimapState extends BaseAppState {
|
||||
fmMapSize = Math.min(sw, sh) * 0.85f;
|
||||
fmMapOriginX = (sw - fmMapSize) / 2f;
|
||||
fmMapOriginY = (sh - fmMapSize) / 2f;
|
||||
fmViewRadius = FM_VIEW_DEF;
|
||||
fmViewRadius = FM_ZOOM_RADII[fmZoomLevelIdx];
|
||||
float px = playerNode.getWorldTranslation().x;
|
||||
float pz = playerNode.getWorldTranslation().z;
|
||||
fmCenterU = (px + WORLD_HALF) / WORLD_SIZE;
|
||||
fmCenterV = 1f - (pz + WORLD_HALF) / WORLD_SIZE;
|
||||
vecRenderU = fmCenterU;
|
||||
vecRenderV = fmCenterV;
|
||||
vecRenderHU = FM_VIEW_DEF / WORLD_SIZE;
|
||||
|
||||
// Blur auf dem geteilten FPP – genau wie das ESC-Menü
|
||||
WorldScene ws = app.getStateManager().getState(WorldScene.class);
|
||||
@@ -399,6 +418,19 @@ public class MinimapState extends BaseAppState {
|
||||
vectorGeo.setQueueBucket(RenderQueue.Bucket.Gui);
|
||||
fullMapNode.attachChild(vectorGeo);
|
||||
|
||||
// Höhlenkarte-Overlay (teilt UV mit fullMapMesh, da gleicher Weltausschnitt)
|
||||
if (caveMapTex != null) {
|
||||
caveGeoFull = new Geometry("wm_cave", fullMapMesh);
|
||||
Material caveFMat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
caveFMat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
caveFMat.setTexture("ColorMap", caveMapTex);
|
||||
caveFMat.setColor("Color", new ColorRGBA(1f, 1f, 1f, caveBlend));
|
||||
caveGeoFull.setMaterial(caveFMat);
|
||||
caveGeoFull.setQueueBucket(RenderQueue.Bucket.Gui);
|
||||
caveGeoFull.setLocalTranslation(0f, 0f, 0.4f); // über vectorGeo (z=20.3) und fullMapGeo (z=20)
|
||||
fullMapNode.attachChild(caveGeoFull);
|
||||
}
|
||||
|
||||
app.getGuiNode().attachChild(fullMapNode);
|
||||
app.getInputManager().setCursorVisible(true);
|
||||
|
||||
@@ -466,23 +498,7 @@ public class MinimapState extends BaseAppState {
|
||||
uv.put(4, fmCenterU + hu); uv.put(5, fmCenterV + hu);
|
||||
uv.put(6, fmCenterU - hu); uv.put(7, fmCenterV + hu);
|
||||
fullMapMesh.getBuffer(VertexBuffer.Type.TexCoord).setUpdateNeeded();
|
||||
updateVectorMeshUV();
|
||||
}
|
||||
|
||||
private void updateVectorMeshUV() {
|
||||
if (vectorMesh == null || vecRenderHU <= 0f) { return; }
|
||||
float hu = fmViewRadius / WORLD_SIZE;
|
||||
float rhu = vecRenderHU;
|
||||
float uMin = (fmCenterU - hu - (vecRenderU - rhu)) / (2f * rhu);
|
||||
float uMax = (fmCenterU + hu - (vecRenderU - rhu)) / (2f * rhu);
|
||||
float vMin = (fmCenterV - hu - (vecRenderV - rhu)) / (2f * rhu);
|
||||
float vMax = (fmCenterV + hu - (vecRenderV - rhu)) / (2f * rhu);
|
||||
FloatBuffer uv = (FloatBuffer) vectorMesh.getBuffer(VertexBuffer.Type.TexCoord).getData();
|
||||
uv.put(0, uMin); uv.put(1, vMin);
|
||||
uv.put(2, uMax); uv.put(3, vMin);
|
||||
uv.put(4, uMax); uv.put(5, vMax);
|
||||
uv.put(6, uMin); uv.put(7, vMax);
|
||||
vectorMesh.getBuffer(VertexBuffer.Type.TexCoord).setUpdateNeeded();
|
||||
updateVectorMeshUV(); // vectorGeo trackt Pan/Zoom live
|
||||
}
|
||||
|
||||
// ── Input ─────────────────────────────────────────────────────────────────
|
||||
@@ -527,29 +543,12 @@ public class MinimapState extends BaseAppState {
|
||||
|
||||
private final AnalogListener analogListener = (name, value, tpf) -> {
|
||||
if (!isEnabled() || !fullMapOpen) { return; }
|
||||
float factor = ANA_FM_ZOOM_IN.equals(name) ? 1f / 1.15f : 1.15f;
|
||||
float oldHu = fmViewRadius / WORLD_SIZE;
|
||||
fmViewRadius = Math.max(FM_VIEW_MIN, Math.min(FM_VIEW_MAX, fmViewRadius * factor));
|
||||
float newHu = fmViewRadius / WORLD_SIZE;
|
||||
|
||||
// Cursor-Anker: UV unter dem Mauszeiger bleibt an gleicher Bildschirmposition
|
||||
Vector2f c = app.getInputManager().getCursorPosition();
|
||||
float rx = (c.x - fmMapOriginX) / fmMapSize;
|
||||
float ry = (c.y - fmMapOriginY) / fmMapSize;
|
||||
if (rx >= 0f && rx <= 1f && ry >= 0f && ry <= 1f) {
|
||||
fmCenterU += (rx - 0.5f) * 2f * (oldHu - newHu);
|
||||
fmCenterV += (ry - 0.5f) * 2f * (oldHu - newHu);
|
||||
} else {
|
||||
// Zoom auf Spielerposition wenn Cursor außerhalb der Karte
|
||||
float wpx = playerNode.getWorldTranslation().x;
|
||||
float wpz = playerNode.getWorldTranslation().z;
|
||||
float pu = (wpx + WORLD_HALF) / WORLD_SIZE;
|
||||
float pv = 1f - (wpz + WORLD_HALF) / WORLD_SIZE;
|
||||
float ratio = newHu / oldHu;
|
||||
fmCenterU = pu + (fmCenterU - pu) * ratio;
|
||||
fmCenterV = pv + (fmCenterV - pv) * ratio;
|
||||
}
|
||||
|
||||
// Diskrete Zoom-Stufen wie im Editor (Mausrad springt eine Stufe pro Klick)
|
||||
int newIdx = fmZoomLevelIdx + (ANA_FM_ZOOM_IN.equals(name) ? 1 : -1);
|
||||
newIdx = Math.max(0, Math.min(FM_ZOOM_RADII.length - 1, newIdx));
|
||||
if (newIdx == fmZoomLevelIdx) { return; }
|
||||
fmZoomLevelIdx = newIdx;
|
||||
fmViewRadius = FM_ZOOM_RADII[fmZoomLevelIdx];
|
||||
updateFullMapUV();
|
||||
scheduleVectorRedraw();
|
||||
float px = playerNode.getWorldTranslation().x;
|
||||
@@ -562,12 +561,9 @@ public class MinimapState extends BaseAppState {
|
||||
private void startRenderThread() {
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
Path root = AnimationLibrary.findAssetRoot();
|
||||
Path dir = root.resolve("Textures").resolve("hud");
|
||||
Files.createDirectories(dir);
|
||||
Path png = dir.resolve("minimap_world.png");
|
||||
Path cavePng = java.nio.file.Paths.get(System.getProperty("user.home"), ".blight", "map_chunks", "minimap_cave.png");
|
||||
|
||||
log.debug("[Minimap] Lade Weltdaten für Render-Modell…");
|
||||
log.debug("[Minimap] Lade Weltdaten…");
|
||||
long t0 = System.currentTimeMillis();
|
||||
MapData mapData = MapIO.load();
|
||||
List<PlacedArea> areas = AreaIO.load();
|
||||
@@ -575,30 +571,44 @@ public class MinimapState extends BaseAppState {
|
||||
List<Location> locs = LocationIO.load();
|
||||
List<PlacedWater> waters = WaterBodyIO.load();
|
||||
List<PlacedModel> models = PlacedModelIO.load();
|
||||
log.debug("[Minimap] Weltdaten geladen ({} ms)", System.currentTimeMillis() - t0);
|
||||
|
||||
// Render-Modell aufbauen (wird für Weltkarte + Vektor-Layer verwendet)
|
||||
List<PlacedWaterfall> waterfalls = WaterfallIO.load();
|
||||
int[] slotColors = computeSlotColors(mapData, root);
|
||||
int[][] tilePixels = WorldMapRenderer.loadMapTiles(root);
|
||||
int[] slotColors = computeSlotColors(mapData, AnimationLibrary.findAssetRoot());
|
||||
int[][] tilePixels = WorldMapRenderer.loadMapTiles(AnimationLibrary.findAssetRoot());
|
||||
float[] voxelSurface = WorldMapRenderer.bakeVoxelSurface(mapData);
|
||||
renderInput = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors);
|
||||
de.blight.common.map.WorldMapRenderModel renderModel = new de.blight.common.map.WorldMapRenderModel(
|
||||
de.blight.common.map.WorldMapRenderModel rm = new de.blight.common.map.WorldMapRenderModel(
|
||||
mapData, areas, zones, locs, waters, models, waterfalls,
|
||||
slotColors, null, voxelSurface, null, null, null, tilePixels, null);
|
||||
log.debug("[Minimap] Render-Modell bereit – {} Areas, {} Orte, {} Wasser ({} ms)",
|
||||
areas.size(), locs.size(), waters.size(), System.currentTimeMillis() - t0);
|
||||
|
||||
log.info("[Minimap] Rendere Weltkarte {}×{}…", TEXTURE_SIZE, TEXTURE_SIZE);
|
||||
renderInput = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors);
|
||||
|
||||
// Overlay-Daten einmalig vorberechnen (Pfade, Dichte-Maps – für alle Zoom-Level)
|
||||
log.info("[Minimap] Berechne Overlay-Daten…");
|
||||
long t1 = System.currentTimeMillis();
|
||||
BufferedImage bi = WorldMapRenderer.render(renderModel, TEXTURE_SIZE, RenderOptions.all());
|
||||
ImageIO.write(bi, "PNG", png.toFile());
|
||||
log.debug("[Minimap] Weltkarte gerendert und gespeichert ({} ms): {}",
|
||||
System.currentTimeMillis() - t1, png);
|
||||
overlayData = WorldMapRenderer.computeOverlayData(rm);
|
||||
renderModel = rm;
|
||||
log.info("[Minimap] Overlay-Daten fertig ({} ms)", System.currentTimeMillis() - t1);
|
||||
|
||||
BufferedImage caveBi = WorldMapRenderer.buildCaveMap(mapData, TEXTURE_SIZE);
|
||||
ImageIO.write(caveBi, "PNG", dir.resolve("minimap_cave.png").toFile());
|
||||
// Verwende Voxel-basierten Tunnel-Maske für Laufzeit-Erkennung (UPPER_CELLS Auflösung)
|
||||
boolean[] tunnelMask256 = WorldMapRenderer.buildCaveTunnelMask(mapData, MapData.UPPER_CELLS);
|
||||
upperHoleMask = new byte[tunnelMask256.length];
|
||||
for (int i = 0; i < tunnelMask256.length; i++) upperHoleMask[i] = tunnelMask256[i] ? (byte)1 : 0;
|
||||
// Weltkarte: Echtzeit-Rendering mit allen Layern (identisch zum Editor)
|
||||
log.info("[Minimap] Rendere Weltkarte mit allen Layern {}×{}…", TEXTURE_SIZE, TEXTURE_SIZE);
|
||||
long t2 = System.currentTimeMillis();
|
||||
pendingWorldBi = WorldMapRenderer.renderAllLayers(rm, overlayData, TEXTURE_SIZE);
|
||||
log.info("[Minimap] Weltkarte fertig ({} ms)", System.currentTimeMillis() - t2);
|
||||
|
||||
// Höhlenkarte
|
||||
if (Files.exists(cavePng)) {
|
||||
log.info("[Minimap] Lade Höhlenkarte aus Editor-Cache: {}", cavePng);
|
||||
pendingCaveBi = javax.imageio.ImageIO.read(cavePng.toFile());
|
||||
} else {
|
||||
log.info("[Minimap] Kein Editor-Cache – rendere Höhlenkarte {}×{}…", TEXTURE_SIZE, TEXTURE_SIZE);
|
||||
pendingCaveBi = WorldMapRenderer.buildCaveMap(mapData, TEXTURE_SIZE);
|
||||
}
|
||||
|
||||
// Cave-Fußabdruck-Maske für Laufzeit-Erkennung (gleiche Logik wie beim Pfad-Splitting)
|
||||
upperHoleMask = WorldMapRenderer.buildCaveFootprintMask(
|
||||
mapData, WorldMapRenderer.VOXEL_SURFACE_RES);
|
||||
|
||||
app.enqueue(() -> { onTextureReady(); return null; });
|
||||
} catch (Exception e) {
|
||||
@@ -622,52 +632,60 @@ public class MinimapState extends BaseAppState {
|
||||
}
|
||||
|
||||
private void onTextureReady() {
|
||||
// Texturen direkt aus dem gerenderten BufferedImage erzeugen — kein AssetManager,
|
||||
// kein Classpath-Cache, immer der aktuelle Render-Stand.
|
||||
worldMapTex = biToTexture(pendingWorldBi);
|
||||
caveMapTex = biToTexture(pendingCaveBi);
|
||||
pendingWorldBi = null;
|
||||
pendingCaveBi = null;
|
||||
|
||||
exploreTracker = new ExploreTracker();
|
||||
exploreTracker.load();
|
||||
|
||||
float x = MARGIN;
|
||||
float y = MARGIN;
|
||||
// Node an der Rahmen-Ecke (MARGIN, MARGIN) – Rahmen füllt 256×256 ab dort
|
||||
float x = MARGIN;
|
||||
float y = MARGIN;
|
||||
|
||||
fogMat = buildFogMat(x, y, MINIMAP_SIZE, MINIMAP_SIZE);
|
||||
// Fog-Material: Ursprung und Größe des Kreisbereichs in Bildschirm-Koordinaten
|
||||
fogMat = buildFogMat(x + MINIMAP_OFFSET, y + MINIMAP_OFFSET, MINIMAP_SIZE, MINIMAP_SIZE);
|
||||
|
||||
minimapMesh = buildMinimapMesh(MINIMAP_SIZE, MINIMAP_SIZE);
|
||||
// Kreisförmiges Mesh: Radius 85px, Mittelpunkt bei (85,85) in lokalem Raum
|
||||
minimapMesh = buildCircleMesh(MINIMAP_RADIUS, MINIMAP_SEGS);
|
||||
|
||||
// Welt-Textur-Geo
|
||||
// Alle Map-Layer teilen dasselbe Mesh; Kreis wird um MINIMAP_OFFSET verschoben
|
||||
minimapGeo = new Geometry("minimap_geo", minimapMesh);
|
||||
minimapGeo.setMaterial(buildTexMat());
|
||||
minimapGeo.setQueueBucket(RenderQueue.Bucket.Gui);
|
||||
minimapGeo.setLocalTranslation(MINIMAP_OFFSET, MINIMAP_OFFSET, 0f);
|
||||
|
||||
// Fog-Geo teilt dasselbe Mesh – UV-Updates gelten für beide
|
||||
fogMiniGeo = new Geometry("minimap_fog", minimapMesh);
|
||||
fogMiniGeo.setMaterial(fogMat);
|
||||
fogMiniGeo.setQueueBucket(RenderQueue.Bucket.Gui);
|
||||
fogMiniGeo.setLocalTranslation(0f, 0f, 1f); // eine Schicht vor der Karte
|
||||
|
||||
// Rand-Schatten
|
||||
Geometry border = makeSolidQuad("minimap_border",
|
||||
MINIMAP_SIZE + 4f, MINIMAP_SIZE + 4f,
|
||||
new ColorRGBA(0f, 0f, 0f, 0.6f));
|
||||
border.setLocalTranslation(-2f, -2f, -1f);
|
||||
|
||||
// Spieler-Punkt fest in der Mitte
|
||||
playerDotMini = makeSolidQuad("minimap_dot", DOT_SIZE, DOT_SIZE,
|
||||
new ColorRGBA(1f, 0.85f, 0f, 1f));
|
||||
playerDotMini.setLocalTranslation(
|
||||
MINIMAP_SIZE / 2f - DOT_SIZE / 2f,
|
||||
MINIMAP_SIZE / 2f - DOT_SIZE / 2f, 2f);
|
||||
fogMiniGeo.setLocalTranslation(MINIMAP_OFFSET, MINIMAP_OFFSET, 1f);
|
||||
|
||||
caveGeoMini = new Geometry("minimap_cave", minimapMesh);
|
||||
caveGeoMini.setMaterial(buildCaveTexMat());
|
||||
caveGeoMini.setQueueBucket(RenderQueue.Bucket.Gui);
|
||||
caveGeoMini.setLocalTranslation(0f, 0f, 0.5f); // between world map and fog
|
||||
caveGeoMini.setLocalTranslation(MINIMAP_OFFSET, MINIMAP_OFFSET, 0.5f);
|
||||
|
||||
// Spieler-Punkt fest in der Mitte des Kreises
|
||||
playerDotMini = makeSolidQuad("minimap_dot", DOT_SIZE, DOT_SIZE,
|
||||
new ColorRGBA(1f, 0.85f, 0f, 1f));
|
||||
playerDotMini.setLocalTranslation(
|
||||
MINIMAP_OFFSET + MINIMAP_RADIUS - DOT_SIZE / 2f,
|
||||
MINIMAP_OFFSET + MINIMAP_RADIUS - DOT_SIZE / 2f, 2f);
|
||||
|
||||
// PNG-Rahmen: 256×256 px, liegt über allen anderen Layern
|
||||
Geometry borderGeo = makeBorderGeo(BORDER_DISPLAY);
|
||||
borderGeo.setLocalTranslation(0f, 0f, 3f);
|
||||
|
||||
minimapNode = new Node("minimap");
|
||||
minimapNode.setLocalTranslation(x, y, 48f);
|
||||
minimapNode.attachChild(border);
|
||||
minimapNode.attachChild(minimapGeo);
|
||||
minimapNode.attachChild(caveGeoMini);
|
||||
minimapNode.attachChild(fogMiniGeo);
|
||||
minimapNode.attachChild(playerDotMini);
|
||||
minimapNode.attachChild(borderGeo);
|
||||
|
||||
textureReady = true;
|
||||
if (isEnabled()) {
|
||||
@@ -679,12 +697,8 @@ public class MinimapState extends BaseAppState {
|
||||
private Material buildCaveTexMat() {
|
||||
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
try {
|
||||
Texture tex = app.getAssetManager().loadTexture("Textures/hud/minimap_cave.png");
|
||||
tex.setWrap(Texture.WrapMode.EdgeClamp);
|
||||
mat.setTexture("ColorMap", tex);
|
||||
} catch (Exception e) {
|
||||
log.warn("[Minimap] Höhlenkarten-Textur nicht ladbar");
|
||||
if (caveMapTex != null) {
|
||||
mat.setTexture("ColorMap", caveMapTex);
|
||||
}
|
||||
mat.setColor("Color", new ColorRGBA(1f, 1f, 1f, 0f)); // zunächst unsichtbar
|
||||
return mat;
|
||||
@@ -693,31 +707,31 @@ public class MinimapState extends BaseAppState {
|
||||
// ── Vektor-Layer ──────────────────────────────────────────────────────────
|
||||
|
||||
private void scheduleVectorRedraw() {
|
||||
if (renderInput == null || !fullMapOpen) { return; }
|
||||
if (overlayData == null || !fullMapOpen) { return; }
|
||||
if (vecRenderPending) { vecLayerDirty = true; return; }
|
||||
vecRenderPending = true;
|
||||
vecLayerDirty = false;
|
||||
|
||||
float wx = fmCenterU * WORLD_SIZE - WORLD_HALF;
|
||||
float wz = (1f - fmCenterV) * WORLD_SIZE - WORLD_HALF;
|
||||
// Render-Zielposition jetzt einfrieren; vectorGeo bleibt sichtbar und trackt Pan via UV
|
||||
float capturedCU = fmCenterU;
|
||||
float capturedCV = fmCenterV;
|
||||
float capturedHU = fmViewRadius / WORLD_SIZE;
|
||||
float wx = capturedCU * WORLD_SIZE - WORLD_HALF;
|
||||
float wz = (1f - capturedCV) * WORLD_SIZE - WORLD_HALF;
|
||||
float vr = fmViewRadius;
|
||||
final float capU = fmCenterU;
|
||||
final float capV = fmCenterV;
|
||||
final float capHU = vr / WORLD_SIZE;
|
||||
|
||||
Thread t = new Thread(() -> {
|
||||
log.debug("[Minimap] Vektor-Layer neu rendern – Zentrum ({}/{}), Radius {}",
|
||||
log.debug("[Minimap] Region rendern – Zentrum ({}/{}), Radius {}",
|
||||
Math.round(wx), Math.round(wz), Math.round(vr));
|
||||
long t0 = System.currentTimeMillis();
|
||||
RenderOptions opts = new RenderOptions(false, false, true, true, true, true, true);
|
||||
BufferedImage bi = WorldMapRenderer.renderRegion(renderInput, VEC_TEX_SIZE, opts, wx, wz, vr);
|
||||
log.debug("[Minimap] Vektor-Layer fertig ({} ms)", System.currentTimeMillis() - t0);
|
||||
BufferedImage bi = WorldMapRenderer.renderFullRegion(overlayData, VEC_TEX_SIZE, wx, wz, vr);
|
||||
log.debug("[Minimap] Region fertig ({} ms)", System.currentTimeMillis() - t0);
|
||||
app.enqueue(() -> {
|
||||
updateVectorTexture(bi);
|
||||
vecRenderU = capU;
|
||||
vecRenderV = capV;
|
||||
vecRenderHU = capHU;
|
||||
updateVectorMeshUV();
|
||||
vecRenderCenterU = capturedCU;
|
||||
vecRenderCenterV = capturedCV;
|
||||
vecRenderHU = capturedHU;
|
||||
updateVectorMeshUV(); // UV auf frischen Render ausrichten
|
||||
vecRenderPending = false;
|
||||
if (vecLayerDirty) { scheduleVectorRedraw(); }
|
||||
return null;
|
||||
@@ -727,6 +741,23 @@ public class MinimapState extends BaseAppState {
|
||||
t.start();
|
||||
}
|
||||
|
||||
/** Passt die UVs des vectorMesh an, damit das alte Render bei Pan/Zoom korrekt sitzt. */
|
||||
private void updateVectorMeshUV() {
|
||||
if (vectorMesh == null || vecRenderHU <= 0f) { return; }
|
||||
float hu = fmViewRadius / WORLD_SIZE;
|
||||
float rhu = vecRenderHU;
|
||||
float uMin = (fmCenterU - hu - vecRenderCenterU + rhu) / (2f * rhu);
|
||||
float uMax = (fmCenterU + hu - vecRenderCenterU + rhu) / (2f * rhu);
|
||||
float vMin = (fmCenterV - hu - vecRenderCenterV + rhu) / (2f * rhu);
|
||||
float vMax = (fmCenterV + hu - vecRenderCenterV + rhu) / (2f * rhu);
|
||||
FloatBuffer uv = (FloatBuffer) vectorMesh.getBuffer(VertexBuffer.Type.TexCoord).getData();
|
||||
uv.put(0, uMin); uv.put(1, vMin);
|
||||
uv.put(2, uMax); uv.put(3, vMin);
|
||||
uv.put(4, uMax); uv.put(5, vMax);
|
||||
uv.put(6, uMin); uv.put(7, vMax);
|
||||
vectorMesh.getBuffer(VertexBuffer.Type.TexCoord).setUpdateNeeded();
|
||||
}
|
||||
|
||||
private void updateVectorTexture(BufferedImage bi) {
|
||||
if (vectorBuf == null || vectorImage == null) { return; }
|
||||
int sz = VEC_TEX_SIZE;
|
||||
@@ -749,12 +780,9 @@ public class MinimapState extends BaseAppState {
|
||||
private Material buildTexMat() {
|
||||
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Off);
|
||||
try {
|
||||
Texture tex = app.getAssetManager().loadTexture("Textures/hud/minimap_world.png");
|
||||
tex.setWrap(Texture.WrapMode.EdgeClamp);
|
||||
mat.setTexture("ColorMap", tex);
|
||||
} catch (Exception e) {
|
||||
log.warn("[Minimap] Weltkarten-Textur nicht ladbar");
|
||||
if (worldMapTex != null) {
|
||||
mat.setTexture("ColorMap", worldMapTex);
|
||||
} else {
|
||||
mat.setColor("Color", new ColorRGBA(0.05f, 0.1f, 0.2f, 1f));
|
||||
}
|
||||
return mat;
|
||||
@@ -779,12 +807,8 @@ public class MinimapState extends BaseAppState {
|
||||
|
||||
private Material buildOverlayTexMat(float originX, float originY, float size) {
|
||||
Material mat = new Material(app.getAssetManager(), "MatDefs/MapOverlay.j3md");
|
||||
try {
|
||||
Texture tex = app.getAssetManager().loadTexture("Textures/hud/minimap_world.png");
|
||||
tex.setWrap(Texture.WrapMode.EdgeClamp);
|
||||
mat.setTexture("ColorMap", tex);
|
||||
} catch (Exception e) {
|
||||
log.warn("[Minimap] Weltkarten-Textur nicht ladbar");
|
||||
if (worldMapTex != null) {
|
||||
mat.setTexture("ColorMap", worldMapTex);
|
||||
}
|
||||
mat.setVector2("OverlayOrigin", new Vector2f(originX, originY));
|
||||
mat.setVector2("OverlaySize", new Vector2f(size, size));
|
||||
@@ -793,19 +817,85 @@ public class MinimapState extends BaseAppState {
|
||||
|
||||
// ── Mesh-Helfer ───────────────────────────────────────────────────────────
|
||||
|
||||
private static Mesh buildMinimapMesh(float w, float h) {
|
||||
/**
|
||||
* Konvertiert ein BufferedImage direkt in eine JME3-Textur (Y-flip für OpenGL-Konvention).
|
||||
* Umgeht den AssetManager-Cache vollständig — immer aktueller Render-Stand.
|
||||
*/
|
||||
private static Texture2D biToTexture(BufferedImage bi) {
|
||||
int w = bi.getWidth(), h = bi.getHeight();
|
||||
int[] pixels = bi.getRGB(0, 0, w, h, null, 0, w); // Bulk-Read aller Pixel auf einmal
|
||||
java.nio.ByteBuffer buf = BufferUtils.createByteBuffer(w * h * 4);
|
||||
for (int row = 0; row < h; row++) {
|
||||
int bufRow = h - 1 - row; // Y-flip: OpenGL V=0 ist unten, Bild-Row 0 ist oben
|
||||
for (int col = 0; col < w; col++) {
|
||||
int argb = pixels[row * w + col];
|
||||
int off = (bufRow * w + col) * 4;
|
||||
buf.put(off, (byte) ((argb >> 16) & 0xFF));
|
||||
buf.put(off + 1, (byte) ((argb >> 8) & 0xFF));
|
||||
buf.put(off + 2, (byte) ( argb & 0xFF));
|
||||
buf.put(off + 3, (byte) ((argb >> 24) & 0xFF));
|
||||
}
|
||||
}
|
||||
Image jmeImg = new Image(Image.Format.RGBA8, w, h, buf);
|
||||
Texture2D tex = new Texture2D(jmeImg);
|
||||
tex.setWrap(Texture.WrapMode.EdgeClamp);
|
||||
tex.setMagFilter(Texture.MagFilter.Bilinear);
|
||||
tex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
|
||||
return tex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kreis-Mesh als Triangle-Fan: Vertex 0 = Mittelpunkt, Vertices 1..segs = Rand.
|
||||
* Mittelpunkt liegt bei (radius, radius) in lokalem Koordinatenraum.
|
||||
*/
|
||||
private static Mesh buildCircleMesh(float radius, int segs) {
|
||||
float cx = radius, cy = radius;
|
||||
float[] pos = new float[(segs + 1) * 3];
|
||||
float[] uvs = new float[(segs + 1) * 2];
|
||||
// Mittelpunkt
|
||||
pos[0] = cx; pos[1] = cy; pos[2] = 0f;
|
||||
uvs[0] = 0.5f; uvs[1] = 0.5f;
|
||||
// Randpunkte
|
||||
for (int i = 0; i < segs; i++) {
|
||||
double a = 2 * Math.PI * i / segs;
|
||||
float ca = (float) Math.cos(a), sa = (float) Math.sin(a);
|
||||
pos[(i + 1) * 3] = cx + radius * ca;
|
||||
pos[(i + 1) * 3 + 1] = cy + radius * sa;
|
||||
pos[(i + 1) * 3 + 2] = 0f;
|
||||
uvs[(i + 1) * 2] = 0.5f + 0.5f * ca;
|
||||
uvs[(i + 1) * 2 + 1] = 0.5f + 0.5f * sa;
|
||||
}
|
||||
// Triangle-Fan-Indizes
|
||||
short[] idx = new short[segs * 3];
|
||||
for (int i = 0; i < segs; i++) {
|
||||
idx[i * 3] = 0;
|
||||
idx[i * 3 + 1] = (short) (i + 1);
|
||||
idx[i * 3 + 2] = (short) (i + 1 < segs ? i + 2 : 1);
|
||||
}
|
||||
Mesh m = new Mesh();
|
||||
m.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(
|
||||
0f, 0f, 0f, w, 0f, 0f, w, h, 0f, 0f, h, 0f));
|
||||
m.setBuffer(VertexBuffer.Type.TexCoord, 2, BufferUtils.createFloatBuffer(
|
||||
0f, 0f, 1f, 0f, 1f, 1f, 0f, 1f));
|
||||
m.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createShortBuffer(
|
||||
(short) 0, (short) 1, (short) 2, (short) 0, (short) 2, (short) 3));
|
||||
m.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(pos));
|
||||
m.setBuffer(VertexBuffer.Type.TexCoord, 2, BufferUtils.createFloatBuffer(uvs));
|
||||
m.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createShortBuffer(idx));
|
||||
m.setMode(Mesh.Mode.Triangles);
|
||||
m.updateBound();
|
||||
return m;
|
||||
}
|
||||
|
||||
/** Lädt map_border.png und erzeugt ein quadratisches Geo mit Alpha-Blending. */
|
||||
private Geometry makeBorderGeo(float size) {
|
||||
Texture2D tex = (Texture2D) app.getAssetManager().loadTexture("Textures/hud/map_border.png");
|
||||
tex.setWrap(Texture.WrapMode.EdgeClamp);
|
||||
tex.setMagFilter(Texture.MagFilter.Bilinear);
|
||||
tex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
|
||||
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setTexture("ColorMap", tex);
|
||||
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
Geometry g = new Geometry("minimap_border_img", new Quad(size, size));
|
||||
g.setMaterial(mat);
|
||||
g.setQueueBucket(RenderQueue.Bucket.Gui);
|
||||
return g;
|
||||
}
|
||||
|
||||
private static Mesh buildQuadMesh(float x, float y, float w, float h, float z) {
|
||||
Mesh m = new Mesh();
|
||||
m.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(
|
||||
|
||||
Reference in New Issue
Block a user