Weltkarte: Kachel-Overlay, Wald-Overlay, ModelCategory-System, Selektions-Fix
- Terrain-Textur-Overlay (Splatmap-Blend, Klippen, Wald) als Dirty-Flag-Cache - Wald wird nach Wasser gerendert, 50% Deckkraft, Schwelle 3+ Bäume - Speichern vor Weltkarten-Refresh (save-then-load via pendingAfterSave) - ModelCategory-System: Baum/Palme/Busch/Gebäude; Ordner-Kategorie im UI versteckt - Selektion nur mit Auswahl-Werkzeug (kein versehentliches Selektieren im Platzier-Modus) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -71,7 +71,8 @@ public class EditorApp extends Application {
|
||||
private ImageView viewport;
|
||||
private Label statusLabel;
|
||||
private Label camCoordsLabel;
|
||||
private boolean launchGameAfterSave = false;
|
||||
private boolean launchGameAfterSave = false;
|
||||
private Runnable pendingAfterSave = null;
|
||||
private Button gamePlayBtn; // Spielen-Button im Seitenpanel
|
||||
private CheckBox debugConsoleCB; // Debug-Konsole bei Spielstart anzeigen
|
||||
private VBox toolPanel;
|
||||
@@ -345,6 +346,7 @@ public class EditorApp extends Application {
|
||||
private final java.util.List<de.blight.common.ModelMeta.AttachedEmitter> modelEditorEmitters = new java.util.ArrayList<>();
|
||||
private javafx.scene.layout.VBox modelEditorLightBox = null;
|
||||
private javafx.scene.layout.VBox modelEditorEmitterBox = null;
|
||||
private ComboBox<de.blight.common.ModelCategory> modelEditorCategoryCB = null;
|
||||
private ComboBox<de.blight.common.model.InteractableType> modelEditorInteractableCB = null;
|
||||
private Spinner<Double> modelEditorInteractableXSpin = null;
|
||||
private Spinner<Double> modelEditorInteractableYSpin = null;
|
||||
@@ -587,6 +589,11 @@ public class EditorApp extends Application {
|
||||
launchGameAfterSave = false;
|
||||
startGameProcess();
|
||||
}
|
||||
if (pendingAfterSave != null) {
|
||||
Runnable r = pendingAfterSave;
|
||||
pendingAfterSave = null;
|
||||
r.run();
|
||||
}
|
||||
}
|
||||
|
||||
String treeMsg = input.treeGenStatusMsg;
|
||||
@@ -5659,6 +5666,11 @@ public class EditorApp extends Application {
|
||||
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…");
|
||||
});
|
||||
worldMapView.setFullscreenCallbacks(
|
||||
() -> {
|
||||
// Placeholder ersetzt worldMapView im Tab → sauberes Detach
|
||||
@@ -7906,6 +7918,18 @@ public class EditorApp extends Application {
|
||||
// Initiale Gizmos pushen
|
||||
pushAttachmentsToJme();
|
||||
|
||||
// ── Modell-Kategorie ──────────────────────────────────────────────────
|
||||
Label catTitle = new Label("Kategorie:");
|
||||
catTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;");
|
||||
modelEditorCategoryCB = new ComboBox<>();
|
||||
modelEditorCategoryCB.getItems().addAll(de.blight.common.ModelCategory.values());
|
||||
modelEditorCategoryCB.setValue(meta.modelCategory() != null ? meta.modelCategory() : de.blight.common.ModelCategory.NONE);
|
||||
modelEditorCategoryCB.setMaxWidth(Double.MAX_VALUE);
|
||||
modelEditorCategoryCB.setConverter(new javafx.util.StringConverter<>() {
|
||||
@Override public String toString(de.blight.common.ModelCategory c) { return c == null ? "" : c.getLabel(); }
|
||||
@Override public de.blight.common.ModelCategory fromString(String s) { return de.blight.common.ModelCategory.fromString(s); }
|
||||
});
|
||||
|
||||
// ── Interaktivität ────────────────────────────────────────────────────
|
||||
Label interactTitle = new Label("Interaktivität:");
|
||||
interactTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;");
|
||||
@@ -8119,7 +8143,10 @@ public class EditorApp extends Application {
|
||||
input.modelInteractableRotY,
|
||||
modelEditorFootstepSurfaceCB != null
|
||||
? modelEditorFootstepSurfaceCB.getValue()
|
||||
: ""));
|
||||
: "",
|
||||
modelEditorCategoryCB != null
|
||||
? modelEditorCategoryCB.getValue()
|
||||
: de.blight.common.ModelCategory.NONE));
|
||||
|
||||
placeBtn.setOnAction(e -> {
|
||||
input.modelEditorCloseRequest = true;
|
||||
@@ -8140,7 +8167,7 @@ public class EditorApp extends Application {
|
||||
title,
|
||||
new Separator(),
|
||||
nameLabel, nameTF,
|
||||
catLabel, catCB,
|
||||
|
||||
tagsLabel, tagsTF,
|
||||
new Separator(),
|
||||
scaleTitle, modelEditorUniformCB, scaleGrid,
|
||||
@@ -8160,6 +8187,8 @@ public class EditorApp extends Application {
|
||||
lightSectionLbl, modelEditorLightBox, addLightBtn,
|
||||
emitterSectionLbl, modelEditorEmitterBox, addEmitterBtn,
|
||||
new Separator(),
|
||||
catTitle, modelEditorCategoryCB,
|
||||
new Separator(),
|
||||
interactTitle, modelEditorInteractableCB, restPointBox,
|
||||
new Separator(),
|
||||
footstepTitle, modelEditorFootstepSurfaceCB,
|
||||
@@ -8656,7 +8685,8 @@ public class EditorApp extends Application {
|
||||
de.blight.common.model.InteractableType interactableType,
|
||||
float interactableOffsetX, float interactableOffsetY,
|
||||
float interactableOffsetZ, float interactableRotY,
|
||||
String footstepSurface) {
|
||||
String footstepSurface,
|
||||
de.blight.common.ModelCategory modelCategory) {
|
||||
// Scale wird in j3o eingebrannt → Meta bekommt immer 1.0 (kein doppelter Scale beim Laden)
|
||||
de.blight.common.ModelMeta meta = new de.blight.common.ModelMeta(
|
||||
name, category, tags, 1f, 1f, 1f, uniform,
|
||||
@@ -8665,7 +8695,8 @@ public class EditorApp extends Application {
|
||||
lights, emitters,
|
||||
interactableType != null ? interactableType : de.blight.common.model.InteractableType.NONE,
|
||||
interactableOffsetX, interactableOffsetY, interactableOffsetZ, interactableRotY,
|
||||
footstepSurface != null ? footstepSurface : "");
|
||||
footstepSurface != null ? footstepSurface : "",
|
||||
modelCategory != null ? modelCategory : de.blight.common.ModelCategory.NONE);
|
||||
|
||||
if (absolutePath == null || !absolutePath.toFile().exists()) {
|
||||
setStatus("Fehler: Modell-Datei nicht gefunden – Meta nicht gespeichert");
|
||||
|
||||
@@ -26,6 +26,7 @@ public class SceneObject extends PlacedObject {
|
||||
public float cullDistance = 120f;
|
||||
public String interactableType = "";
|
||||
public String interactableId = "";
|
||||
public de.blight.common.ModelCategory modelCategory = de.blight.common.ModelCategory.NONE;
|
||||
|
||||
public SceneObject(String modelPath, float worldX, float worldZ, float groundY,
|
||||
boolean solid) {
|
||||
|
||||
@@ -723,6 +723,12 @@ public class EzTreeState extends BaseAppState {
|
||||
BinaryExporter.getInstance().save(lodRoot, outPath.toFile());
|
||||
if (thumb != null) ThumbnailRenderer.saveSidecar(thumb, outPath, ASSET_ROOT);
|
||||
|
||||
String sp = subPath.toLowerCase();
|
||||
de.blight.common.ModelCategory autoCategory =
|
||||
sp.contains("palm") ? de.blight.common.ModelCategory.PALM
|
||||
: sp.contains("bush") ? de.blight.common.ModelCategory.BUSH
|
||||
: sp.contains("trellis") ? de.blight.common.ModelCategory.BUSH
|
||||
: de.blight.common.ModelCategory.TREE;
|
||||
de.blight.common.ModelMeta meta = new de.blight.common.ModelMeta(
|
||||
fileName, "trees", "",
|
||||
1f, 1f, 1f, true,
|
||||
@@ -733,7 +739,7 @@ public class EzTreeState extends BaseAppState {
|
||||
"", "", 30f, 80f, 120f,
|
||||
java.util.List.of(), java.util.List.of(),
|
||||
de.blight.common.model.InteractableType.NONE,
|
||||
0f, 0.5f, 0f, 0f, "bark");
|
||||
0f, 0.5f, 0f, 0f, "bark", autoCategory);
|
||||
de.blight.common.ModelMetaIO.save(meta, outPath);
|
||||
|
||||
log.info("[EZ-Tree] Gespeichert: {}", outPath.toAbsolutePath());
|
||||
|
||||
@@ -428,7 +428,7 @@ public class ModelImportState extends BaseAppState {
|
||||
def.interactableType(),
|
||||
def.interactableOffsetX(), def.interactableOffsetY(),
|
||||
def.interactableOffsetZ(), def.interactableRotY(),
|
||||
def.footstepSurface());
|
||||
def.footstepSurface(), def.modelCategory());
|
||||
ModelMetaIO.save(meta, outFile.toPath());
|
||||
log.info("[ModelImport] Meta erstellt: solid={}", solid);
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -193,7 +193,7 @@ public class SceneObjectState extends BaseAppState {
|
||||
so.castShadow, so.receiveShadow,
|
||||
so.lod1Path, so.lod2Path,
|
||||
so.lod1Distance, so.lod2Distance, so.cullDistance,
|
||||
so.interactableType, so.interactableId));
|
||||
so.interactableType, so.interactableId, so.modelCategory));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
@@ -228,6 +228,7 @@ public class SceneObjectState extends BaseAppState {
|
||||
so.cullDistance = pm.cullDistance();
|
||||
so.interactableType = pm.interactableType() != null ? pm.interactableType() : "";
|
||||
so.interactableId = pm.interactableId() != null ? pm.interactableId() : "";
|
||||
so.modelCategory = pm.modelCategory() != null ? pm.modelCategory() : de.blight.common.ModelCategory.NONE;
|
||||
objects.add(so);
|
||||
animClips.add(pm.animClip() != null ? pm.animClip() : "");
|
||||
|
||||
@@ -685,8 +686,9 @@ public class SceneObjectState extends BaseAppState {
|
||||
activeRing = -1;
|
||||
|
||||
// 2. Objekt-Treffer? (Highlight-Geometrien ignorieren)
|
||||
// Im Platzier-Modus keine Selektion — Klick soll immer platzieren
|
||||
CollisionResults objHits = new CollisionResults();
|
||||
objectRoot.collideWith(ray, objHits);
|
||||
if (input.pendingModelPath == null) objectRoot.collideWith(ray, objHits);
|
||||
if (objHits.size() > 0) {
|
||||
CollisionResult closest = null;
|
||||
for (int ci = 0; ci < objHits.size(); ci++) {
|
||||
@@ -826,9 +828,10 @@ public class SceneObjectState extends BaseAppState {
|
||||
if (meta != null) {
|
||||
so.lod1Path = meta.lod1Path() != null ? meta.lod1Path() : "";
|
||||
so.lod2Path = meta.lod2Path() != null ? meta.lod2Path() : "";
|
||||
so.lod1Distance = meta.lod1Distance();
|
||||
so.lod2Distance = meta.lod2Distance();
|
||||
so.cullDistance = meta.cullDistance();
|
||||
so.lod1Distance = meta.lod1Distance();
|
||||
so.lod2Distance = meta.lod2Distance();
|
||||
so.cullDistance = meta.cullDistance();
|
||||
so.modelCategory = meta.modelCategory() != null ? meta.modelCategory() : de.blight.common.ModelCategory.NONE;
|
||||
if (meta.interactableType() != null
|
||||
&& meta.interactableType() != de.blight.common.model.InteractableType.NONE) {
|
||||
so.interactableType = meta.interactableType().name();
|
||||
|
||||
@@ -484,6 +484,15 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
} catch (Exception te) { log.warn("[Blight-Baum] Thumbnail-Fehler: {}", te.getMessage()); }
|
||||
BinaryExporter.getInstance().save(treeNode, out);
|
||||
if (thumb != null) ThumbnailRenderer.saveSidecar(thumb, out.toPath(), ASSET_ROOT);
|
||||
de.blight.common.ModelCategory autoCategory =
|
||||
"bush".equals(treeType) ? de.blight.common.ModelCategory.BUSH : de.blight.common.ModelCategory.TREE;
|
||||
de.blight.common.ModelMeta autoMeta = new de.blight.common.ModelMeta(
|
||||
fileName, "trees", "", 1f, 1f, 1f, true, 0f, 0f,
|
||||
true, true, true, 1f, 1f, "", "", 30f, 80f, 120f,
|
||||
java.util.List.of(), java.util.List.of(),
|
||||
de.blight.common.model.InteractableType.NONE,
|
||||
0f, 0.5f, 0f, 0f, "bark", autoCategory);
|
||||
de.blight.common.ModelMetaIO.save(autoMeta, out.toPath());
|
||||
log.info("[Blight-Baum] Gespeichert: {}", out.getAbsolutePath());
|
||||
input.treeGenStatusMsg = "Gespeichert: Models/trees/" + treeType + "/" + sizeClass + "/" + fileName + ".j3o";
|
||||
input.refreshAssets = true;
|
||||
|
||||
@@ -18,6 +18,7 @@ import javafx.scene.Cursor;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.canvas.GraphicsContext;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.PixelFormat;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.*;
|
||||
@@ -84,6 +85,7 @@ public class WorldMapView extends VBox {
|
||||
// Layer-Checkboxen (im MenuButton gebündelt)
|
||||
private final CheckBox cbTerrain = layerCheck("Gelände");
|
||||
private final CheckBox cbWater = layerCheck("Wasser");
|
||||
private final CheckBox cbCoastline = layerCheck("Küstenlinie");
|
||||
private final CheckBox cbWaterfalls = layerCheck("Wasserfälle");
|
||||
private final CheckBox cbAreas = layerCheck("Areas");
|
||||
private final CheckBox cbZones = layerCheck("Zonen");
|
||||
@@ -101,6 +103,35 @@ public class WorldMapView extends VBox {
|
||||
private WritableImage mapFxImage;
|
||||
private BufferedImage mapBuffered;
|
||||
|
||||
// Canvas-level Tile-Overlay: zoom-unabhängige Kacheldarstellung
|
||||
private static final int CAT_SIZE = 2048;
|
||||
private byte[] categoryMap = null;
|
||||
private float[] heightMap = null; // terrain+voxel, CAT_SIZE×CAT_SIZE (für Klippen)
|
||||
private float[] heightMapTerrain = null; // terrain-only, CAT_SIZE×CAT_SIZE (für Wasser-Füllung)
|
||||
private WritableImage tileOverlayImg = null;
|
||||
private int[] tileOverlayPixels = null;
|
||||
private int tileOverlayW = 0;
|
||||
private int tileOverlayH = 0;
|
||||
private WritableImage forestOverlayImg = null;
|
||||
private int[] forestOverlayPixels = null;
|
||||
private int forestOverlayW = 0;
|
||||
private int forestOverlayH = 0;
|
||||
// Gecachte Vorab-Berechnungen — nur bei Modell-Wechsel neu berechnen
|
||||
private WorldMapRenderModel tileOverlayModel = null;
|
||||
private float[] tileOverlayForestD = null;
|
||||
private int[] tileOverlaySlots = null;
|
||||
// Dirty-Flag: Overlay-Pixel neu rendern wenn Pan/Zoom oder Modell geändert
|
||||
private double tileOverlayCachedScale = Double.NaN;
|
||||
private double tileOverlayCachedPanX = Double.NaN;
|
||||
private double tileOverlayCachedPanY = Double.NaN;
|
||||
// Wasser-Fill-Overlay (Layer über Terrain-Textur, unter Küstenlinie)
|
||||
private WritableImage waterFillImg = null;
|
||||
private int[] waterFillPixels = null;
|
||||
private int waterFillW = 0;
|
||||
private int waterFillH = 0;
|
||||
// PlacedWater-Wasserhoehe pro Zelle (0 = kein PlacedWater); fuer glatte Uebergaenge
|
||||
private float[] waterHeightMap = null;
|
||||
|
||||
private List<PlacedArea> cachedAreas = new ArrayList<>();
|
||||
private List<Location> cachedLocs = new ArrayList<>();
|
||||
private WorldMapRenderModel currentModel = null;
|
||||
@@ -117,6 +148,7 @@ public class WorldMapView extends VBox {
|
||||
private Consumer<float[]> teleportCallback = null; // [worldX, worldY, worldZ]
|
||||
private Runnable enterFullscreenCallback = null;
|
||||
private Runnable exitFullscreenCallback = null;
|
||||
private Runnable refreshAction = null;
|
||||
private final AtomicBoolean loading = new AtomicBoolean(false);
|
||||
|
||||
/** Verknüpft den Live-Voxel-Chunk-Zustand des Editors mit der Kartenansicht. */
|
||||
@@ -129,6 +161,8 @@ public class WorldMapView extends VBox {
|
||||
public void setTeleportCallback(Consumer<float[]> cb) { teleportCallback = cb; }
|
||||
|
||||
/** Callbacks für Vollbild-Modus ein/aus (typisch: Eltern-Layout anpassen). */
|
||||
public void setRefreshAction(Runnable r) { this.refreshAction = r; }
|
||||
|
||||
public void setFullscreenCallbacks(Runnable enter, Runnable exit) {
|
||||
enterFullscreenCallback = enter;
|
||||
exitFullscreenCallback = exit;
|
||||
@@ -174,9 +208,10 @@ public class WorldMapView extends VBox {
|
||||
cbModels.selectedProperty().addListener((obs, o, n) -> redraw());
|
||||
cbAreas.selectedProperty().addListener((obs, o, n) -> redraw());
|
||||
cbLocations.selectedProperty().addListener((obs, o, n) -> redraw());
|
||||
cbCoastline.selectedProperty().addListener((obs, o, n) -> redraw());
|
||||
|
||||
MenuButton layerMenu = new MenuButton("Layer ▾");
|
||||
for (CheckBox cb : new CheckBox[]{cbTerrain, cbWater, cbWaterfalls, cbAreas, cbZones, cbLocations, cbModels}) {
|
||||
for (CheckBox cb : new CheckBox[]{cbTerrain, cbWater, cbCoastline, cbWaterfalls, cbAreas, cbZones, cbLocations, cbModels}) {
|
||||
CustomMenuItem item = new CustomMenuItem(cb, false);
|
||||
item.setHideOnClick(false);
|
||||
layerMenu.getItems().add(item);
|
||||
@@ -185,7 +220,7 @@ public class WorldMapView extends VBox {
|
||||
// ── Buttons ───────────────────────────────────────────────────────────
|
||||
Button refreshBtn = new Button("↻");
|
||||
refreshBtn.setTooltip(new javafx.scene.control.Tooltip("Modell aktualisieren"));
|
||||
refreshBtn.setOnAction(e -> loadAndRender());
|
||||
refreshBtn.setOnAction(e -> { if (refreshAction != null) refreshAction.run(); else loadAndRender(); });
|
||||
|
||||
Button exportBtn = new Button("⇩");
|
||||
exportBtn.setTooltip(new javafx.scene.control.Tooltip("Als PNG exportieren…"));
|
||||
@@ -320,7 +355,14 @@ public class WorldMapView extends VBox {
|
||||
Platform.runLater(() -> statusLbl.setText("Rendere Karte…"));
|
||||
long t1 = System.currentTimeMillis();
|
||||
BufferedImage bi = WorldMapRenderer.render(model, RENDER_SIZE, buildBackgroundOptions());
|
||||
log.debug("[WorldMap] Hintergrund-PNG gerendert ({}×{} px, {} ms)",
|
||||
float[] hMap = WorldMapRenderer.buildTerrainSamples(
|
||||
model.mapData(), CAT_SIZE, model.voxelSurface());
|
||||
float[] hMapT = WorldMapRenderer.buildTerrainSamples(
|
||||
model.mapData(), CAT_SIZE, null);
|
||||
byte[] catMap = WorldMapRenderer.buildCategoryMap(
|
||||
model.mapData(), CAT_SIZE, model.voxelSurface(), model.waters(), model.slotColorsRGB());
|
||||
float[] whMap = WorldMapRenderer.buildWaterHeightMap(model.waters(), CAT_SIZE);
|
||||
log.debug("[WorldMap] Hintergrund-PNG + Kategorie-Karte gerendert ({}×{} px, {} ms)",
|
||||
RENDER_SIZE, RENDER_SIZE, System.currentTimeMillis() - t1);
|
||||
|
||||
Platform.runLater(() -> {
|
||||
@@ -329,6 +371,10 @@ public class WorldMapView extends VBox {
|
||||
cachedLocs = new ArrayList<>(model.locations());
|
||||
mapBuffered = bi;
|
||||
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
||||
heightMap = hMap;
|
||||
heightMapTerrain = hMapT;
|
||||
categoryMap = catMap;
|
||||
waterHeightMap = whMap;
|
||||
fitToView();
|
||||
redraw();
|
||||
progress.setVisible(false);
|
||||
@@ -364,10 +410,21 @@ public class WorldMapView extends VBox {
|
||||
log.debug("[WorldMap] PNG-Rerender aus gecachtem Modell gestartet ({}×{})", RENDER_SIZE, RENDER_SIZE);
|
||||
long t0 = System.currentTimeMillis();
|
||||
BufferedImage bi = WorldMapRenderer.render(model, RENDER_SIZE, buildBackgroundOptions());
|
||||
float[] hMap = WorldMapRenderer.buildTerrainSamples(
|
||||
model.mapData(), CAT_SIZE, model.voxelSurface());
|
||||
float[] hMapT = WorldMapRenderer.buildTerrainSamples(
|
||||
model.mapData(), CAT_SIZE, null);
|
||||
byte[] catMap = WorldMapRenderer.buildCategoryMap(
|
||||
model.mapData(), CAT_SIZE, model.voxelSurface(), model.waters(), model.slotColorsRGB());
|
||||
float[] whMap = WorldMapRenderer.buildWaterHeightMap(model.waters(), CAT_SIZE);
|
||||
log.debug("[WorldMap] PNG-Rerender fertig ({} ms)", System.currentTimeMillis() - t0);
|
||||
Platform.runLater(() -> {
|
||||
mapBuffered = bi;
|
||||
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
||||
heightMap = hMap;
|
||||
heightMapTerrain = hMapT;
|
||||
categoryMap = catMap;
|
||||
waterHeightMap = whMap;
|
||||
redraw();
|
||||
progress.setVisible(false);
|
||||
statusLbl.setText("Bereit");
|
||||
@@ -397,15 +454,246 @@ public class WorldMapView extends VBox {
|
||||
return;
|
||||
}
|
||||
|
||||
gc.setImageSmoothing(false);
|
||||
gc.drawImage(mapFxImage, panX, panY, mapFxImage.getWidth() * scale, mapFxImage.getHeight() * scale);
|
||||
|
||||
gc.setImageSmoothing(true);
|
||||
// Layer 1: Terrain-Textur (überall, opak)
|
||||
drawTileOverlay(gc);
|
||||
// Layer 2: Wasser darüber (glatte Küste via seaCoastPaths)
|
||||
if (cbWater.isSelected()) drawWaterOverlay(gc);
|
||||
// Layer 3: Wald-Overlay (nach Wasser, damit Bäume nicht im Wasser erscheinen)
|
||||
drawForestOverlay(gc);
|
||||
if (cbAreas.isSelected()) drawAreasOverlay(gc);
|
||||
if (cbLocations.isSelected()) drawLocationsOverlay(gc);
|
||||
if (cbModels.isSelected()) drawTreeOverlay(gc);
|
||||
if (!isFullscreen) drawCameraIndicator(gc);
|
||||
}
|
||||
|
||||
// ── Canvas Tile-Overlay (zoom-unabhängige Kacheln) ────────────────────────
|
||||
|
||||
private static int tileSample(int[][] tilePx, int cat,
|
||||
int u0, int u1, int v0, int v1,
|
||||
float w00, float w10, float w01, float w11) {
|
||||
if (cat >= 0 && cat < tilePx.length && tilePx[cat] != null) {
|
||||
int[] tp = tilePx[cat];
|
||||
int p00=tp[v0*16+u0], p10=tp[v0*16+u1], p01=tp[v1*16+u0], p11=tp[v1*16+u1];
|
||||
int r=(int)((p00>>16&0xFF)*w00+(p10>>16&0xFF)*w10+(p01>>16&0xFF)*w01+(p11>>16&0xFF)*w11);
|
||||
int g=(int)((p00>> 8&0xFF)*w00+(p10>> 8&0xFF)*w10+(p01>> 8&0xFF)*w01+(p11>> 8&0xFF)*w11);
|
||||
int b=(int)((p00 &0xFF)*w00+(p10 &0xFF)*w10+(p01 &0xFF)*w01+(p11 &0xFF)*w11);
|
||||
return (r<<16)|(g<<8)|b;
|
||||
}
|
||||
int c = Math.max(0, Math.min(WorldMapRenderer.TILE_FALLBACK_RGB.length-1, cat));
|
||||
int[] fb = WorldMapRenderer.TILE_FALLBACK_RGB[c];
|
||||
return (fb[0]<<16)|(fb[1]<<8)|fb[2];
|
||||
}
|
||||
|
||||
private static float spBilerp(byte[] arr, int i00, int i10, int i01, int i11, float tx, float tz) {
|
||||
float v00=(arr[i00]&0xFF)/255f, v10=(arr[i10]&0xFF)/255f;
|
||||
float v01=(arr[i01]&0xFF)/255f, v11=(arr[i11]&0xFF)/255f;
|
||||
return (v00*(1-tx)+v10*tx)*(1-tz)+(v01*(1-tx)+v11*tx)*tz;
|
||||
}
|
||||
|
||||
private void drawTileOverlay(GraphicsContext gc) {
|
||||
if (heightMap == null || currentModel == null) return;
|
||||
de.blight.common.MapData md = currentModel.mapData();
|
||||
if (md == null) return;
|
||||
int[][] tilePx = currentModel.mapTilePixels();
|
||||
if (tilePx == null) return;
|
||||
|
||||
int cW = (int) canvas.getWidth();
|
||||
int cH = (int) canvas.getHeight();
|
||||
if (cW <= 0 || cH <= 0) return;
|
||||
|
||||
final float WH = WorldMapRenderer.WORLD_HALF;
|
||||
final float WS = WorldMapRenderer.WORLD_SIZE;
|
||||
final float TW = WorldMapRenderer.TILE_WU;
|
||||
final float scaleF = (float) scale;
|
||||
final float panXf = (float) panX;
|
||||
final float panYf = (float) panY;
|
||||
final float imgW = (float)(RENDER_SIZE - 1);
|
||||
final int CS = CAT_SIZE;
|
||||
final float[] hm = heightMap;
|
||||
final float CS1 = CS - 1f;
|
||||
final float CLIFF_LO = 0.4663f, CLIFF_RNG = 0.8391f - 0.4663f;
|
||||
|
||||
// ── Modell-abhängige Vorab-Berechnungen cachen ────────────────────
|
||||
if (tileOverlayModel != currentModel) {
|
||||
tileOverlayModel = currentModel;
|
||||
tileOverlaySlots = WorldMapRenderer.computeSlotTileTypes(md, currentModel.slotColorsRGB());
|
||||
final int FD0 = 512;
|
||||
final float forestRadius0 = 14f;
|
||||
final int forestRCells0 = Math.max(1, (int)(forestRadius0 / (WS / FD0)));
|
||||
final float[] fd0 = new float[FD0 * FD0];
|
||||
for (de.blight.common.PlacedModel pm : currentModel.models()) {
|
||||
if (!WorldMapRenderer.isTree(pm)) continue;
|
||||
int fx = Math.max(0, Math.min(FD0-1, (int)((pm.x() + WH) / WS * FD0)));
|
||||
int fz = Math.max(0, Math.min(FD0-1, (int)((WH - pm.z()) / WS * FD0)));
|
||||
for (int dz = -forestRCells0; dz <= forestRCells0; dz++) {
|
||||
for (int dx = -forestRCells0; dx <= forestRCells0; dx++) {
|
||||
if (dx*dx + dz*dz <= forestRCells0*forestRCells0) {
|
||||
int nx = fx+dx, nz = fz+dz;
|
||||
if (nx >= 0 && nx < FD0 && nz >= 0 && nz < FD0) fd0[nz*FD0+nx] += 1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tileOverlayForestD = fd0;
|
||||
tileOverlayCachedScale = Double.NaN;
|
||||
}
|
||||
|
||||
// ── Pixel-Buffer nur neu berechnen wenn Pan/Zoom oder Canvas-Größe geändert ──
|
||||
boolean needsRedraw = tileOverlayPixels == null || tileOverlayPixels.length != cW * cH
|
||||
|| tileOverlayW != cW || tileOverlayH != cH
|
||||
|| tileOverlayCachedScale != scale
|
||||
|| tileOverlayCachedPanX != panX
|
||||
|| tileOverlayCachedPanY != panY;
|
||||
|
||||
if (!needsRedraw) {
|
||||
gc.drawImage(tileOverlayImg, 0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (tileOverlayPixels == null || tileOverlayPixels.length != cW * cH) {
|
||||
tileOverlayPixels = new int[cW * cH];
|
||||
}
|
||||
if (forestOverlayPixels == null || forestOverlayPixels.length != cW * cH) {
|
||||
forestOverlayPixels = new int[cW * cH];
|
||||
}
|
||||
if (tileOverlayImg == null || tileOverlayW != cW || tileOverlayH != cH) {
|
||||
tileOverlayImg = new WritableImage(cW, cH);
|
||||
tileOverlayW = cW;
|
||||
tileOverlayH = cH;
|
||||
}
|
||||
if (forestOverlayImg == null || forestOverlayW != cW || forestOverlayH != cH) {
|
||||
forestOverlayImg = new WritableImage(cW, cH);
|
||||
forestOverlayW = cW;
|
||||
forestOverlayH = cH;
|
||||
}
|
||||
tileOverlayCachedScale = scale;
|
||||
tileOverlayCachedPanX = panX;
|
||||
tileOverlayCachedPanY = panY;
|
||||
|
||||
final int[] slotTiles = tileOverlaySlots;
|
||||
final float[] forestDensity = tileOverlayForestD;
|
||||
final int FD = 512;
|
||||
|
||||
final byte[] spG = md.splatG, spB = md.splatB, spA = md.splatA;
|
||||
final byte[] upR = md.upperSplatR, upG = md.upperSplatG, upB = md.upperSplatB, upA = md.upperSplatA;
|
||||
final byte[] t3R = md.thirdSplatR, t3G = md.thirdSplatG, t3B = md.thirdSplatB, t3A = md.thirdSplatA;
|
||||
final int SS = de.blight.common.MapData.SPLAT_SIZE;
|
||||
|
||||
int[] tileArgbs = new int[8];
|
||||
|
||||
for (int cy = 0; cy < cH; cy++) {
|
||||
float wz = (cy - panYf) / scaleF / imgW * WS - WH;
|
||||
for (int cx = 0; cx < cW; cx++) {
|
||||
float wx = (cx - panXf) / scaleF / imgW * WS - WH;
|
||||
|
||||
if (wx < -WH || wx >= WH || wz < -WH || wz >= WH) {
|
||||
tileOverlayPixels[cy * cW + cx] = 0x00000000;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ── Steigung aus heightMap → Cliff-Blend
|
||||
float fmx = (wx + WH) / WS * CS1;
|
||||
float fmz = (wz + WH) / WS * CS1;
|
||||
int cmx = Math.max(0, Math.min(CS-1, (int)fmx));
|
||||
int cmz = Math.max(0, Math.min(CS-1, (int)fmz));
|
||||
int cmxR = Math.min(CS-1, cmx+2), cmxL = Math.max(0, cmx-2);
|
||||
int cmzD = Math.min(CS-1, cmz+2), cmzU = Math.max(0, cmz-2);
|
||||
float hC = hm[cmz*CS+cmx];
|
||||
float hR = hm[cmz*CS+cmxR]; if (hR < 0f) hR = hC;
|
||||
float hL = hm[cmz*CS+cmxL]; if (hL < 0f) hL = hC;
|
||||
float hD = hm[cmzD*CS+cmx]; if (hD < 0f) hD = hC;
|
||||
float hU = hm[cmzU*CS+cmx]; if (hU < 0f) hU = hC;
|
||||
float dx = (hR - hL) / (cmxR - cmxL), dz = (hD - hU) / (cmzD - cmzU);
|
||||
float gradMag = (float)Math.sqrt((double)(dx*dx + dz*dz));
|
||||
float t = Math.max(0f, Math.min(1f, (gradMag - CLIFF_LO) / CLIFF_RNG));
|
||||
t = t * t * (3f - 2f * t);
|
||||
|
||||
// ── Tile-UV bilinear
|
||||
float rawU = (wx + WH) / TW, rawV = (wz + WH) / TW;
|
||||
float uf16 = (rawU - (float)Math.floor(rawU)) * 16f;
|
||||
float vf16 = (rawV - (float)Math.floor(rawV)) * 16f;
|
||||
int u0 = Math.floorMod((int)uf16,16), u1 = Math.floorMod(u0+1,16);
|
||||
int v0 = Math.floorMod((int)vf16,16), v1 = Math.floorMod(v0+1,16);
|
||||
float ux = uf16-(float)Math.floor(uf16), vxf = vf16-(float)Math.floor(vf16);
|
||||
float w00=(1-ux)*(1-vxf), w10=ux*(1-vxf), w01=(1-ux)*vxf, w11=ux*vxf;
|
||||
|
||||
// ── Splatmap bilinear (Y-flip)
|
||||
float sfx = Math.max(0f, Math.min(SS-1, (wx+WH)/WS*(SS-1)));
|
||||
float sfz = Math.max(0f, Math.min(SS-1, (WH-wz)/WS*(SS-1)));
|
||||
int sx0=(int)sfx, sx1=Math.min(sx0+1,SS-1);
|
||||
int sz0=(int)sfz, sz1=Math.min(sz0+1,SS-1);
|
||||
float stx=sfx-sx0, stz=sfz-sz0;
|
||||
int bi00=sz0*SS+sx0, bi10=sz0*SS+sx1, bi01=sz1*SS+sx0, bi11=sz1*SS+sx1;
|
||||
float wg = spBilerp(spG,bi00,bi10,bi01,bi11,stx,stz);
|
||||
float wb = spBilerp(spB,bi00,bi10,bi01,bi11,stx,stz);
|
||||
float wa = spBilerp(spA,bi00,bi10,bi01,bi11,stx,stz);
|
||||
float up1 = spBilerp(upR,bi00,bi10,bi01,bi11,stx,stz);
|
||||
float up2 = spBilerp(upG,bi00,bi10,bi01,bi11,stx,stz);
|
||||
float up3 = spBilerp(upB,bi00,bi10,bi01,bi11,stx,stz);
|
||||
float up4 = spBilerp(upA,bi00,bi10,bi01,bi11,stx,stz);
|
||||
float t31 = spBilerp(t3R,bi00,bi10,bi01,bi11,stx,stz);
|
||||
float t32 = spBilerp(t3G,bi00,bi10,bi01,bi11,stx,stz);
|
||||
float t33 = spBilerp(t3B,bi00,bi10,bi01,bi11,stx,stz);
|
||||
float t34 = spBilerp(t3A,bi00,bi10,bi01,bi11,stx,stz);
|
||||
|
||||
// ── Alle Tile-Farben sampeln
|
||||
for (int ti = 0; ti < 8; ti++) tileArgbs[ti] = tileSample(tilePx,ti,u0,u1,v0,v1,w00,w10,w01,w11);
|
||||
|
||||
// ── Splatmap-Blend
|
||||
int col = tileArgbs[slotTiles[0]];
|
||||
int cr=(col>>16)&0xFF, cgv=(col>>8)&0xFF, cbv=col&0xFF;
|
||||
col=tileArgbs[slotTiles[1]]; cr=(int)(cr*(1-wg) +((col>>16)&0xFF)*wg ); cgv=(int)(cgv*(1-wg) +((col>>8)&0xFF)*wg ); cbv=(int)(cbv*(1-wg) +(col&0xFF)*wg );
|
||||
col=tileArgbs[slotTiles[2]]; cr=(int)(cr*(1-wb) +((col>>16)&0xFF)*wb ); cgv=(int)(cgv*(1-wb) +((col>>8)&0xFF)*wb ); cbv=(int)(cbv*(1-wb) +(col&0xFF)*wb );
|
||||
col=tileArgbs[slotTiles[3]]; cr=(int)(cr*(1-wa) +((col>>16)&0xFF)*wa ); cgv=(int)(cgv*(1-wa) +((col>>8)&0xFF)*wa ); cbv=(int)(cbv*(1-wa) +(col&0xFF)*wa );
|
||||
col=tileArgbs[slotTiles[4]]; cr=(int)(cr*(1-up1)+((col>>16)&0xFF)*up1); cgv=(int)(cgv*(1-up1)+((col>>8)&0xFF)*up1); cbv=(int)(cbv*(1-up1)+(col&0xFF)*up1);
|
||||
col=tileArgbs[slotTiles[5]]; cr=(int)(cr*(1-up2)+((col>>16)&0xFF)*up2); cgv=(int)(cgv*(1-up2)+((col>>8)&0xFF)*up2); cbv=(int)(cbv*(1-up2)+(col&0xFF)*up2);
|
||||
col=tileArgbs[slotTiles[6]]; cr=(int)(cr*(1-up3)+((col>>16)&0xFF)*up3); cgv=(int)(cgv*(1-up3)+((col>>8)&0xFF)*up3); cbv=(int)(cbv*(1-up3)+(col&0xFF)*up3);
|
||||
col=tileArgbs[slotTiles[7]]; cr=(int)(cr*(1-up4)+((col>>16)&0xFF)*up4); cgv=(int)(cgv*(1-up4)+((col>>8)&0xFF)*up4); cbv=(int)(cbv*(1-up4)+(col&0xFF)*up4);
|
||||
col=tileArgbs[slotTiles[8]]; cr=(int)(cr*(1-t31)+((col>>16)&0xFF)*t31); cgv=(int)(cgv*(1-t31)+((col>>8)&0xFF)*t31); cbv=(int)(cbv*(1-t31)+(col&0xFF)*t31);
|
||||
col=tileArgbs[slotTiles[9]]; cr=(int)(cr*(1-t32)+((col>>16)&0xFF)*t32); cgv=(int)(cgv*(1-t32)+((col>>8)&0xFF)*t32); cbv=(int)(cbv*(1-t32)+(col&0xFF)*t32);
|
||||
col=tileArgbs[slotTiles[10]];cr=(int)(cr*(1-t33)+((col>>16)&0xFF)*t33); cgv=(int)(cgv*(1-t33)+((col>>8)&0xFF)*t33); cbv=(int)(cbv*(1-t33)+(col&0xFF)*t33);
|
||||
col=tileArgbs[slotTiles[11]];cr=(int)(cr*(1-t34)+((col>>16)&0xFF)*t34); cgv=(int)(cgv*(1-t34)+((col>>8)&0xFF)*t34); cbv=(int)(cbv*(1-t34)+(col&0xFF)*t34);
|
||||
|
||||
// ── Wald-Dichte (bilinear) → separater Buffer, nach Wasser gezeichnet
|
||||
float fsfx = Math.max(0f, Math.min(FD-1.001f, (wx + WH) / WS * FD));
|
||||
float fsfz = Math.max(0f, Math.min(FD-1.001f, (WH - wz) / WS * FD));
|
||||
int fsx0=(int)fsfx, fsx1=Math.min(fsx0+1,FD-1);
|
||||
int fsz0=(int)fsfz, fsz1=Math.min(fsz0+1,FD-1);
|
||||
float fstx=fsfx-fsx0, fstz=fsfz-fsz0;
|
||||
float fd = (forestDensity[fsz0*FD+fsx0]*(1-fstx)+forestDensity[fsz0*FD+fsx1]*fstx)*(1-fstz)
|
||||
+ (forestDensity[fsz1*FD+fsx0]*(1-fstx)+forestDensity[fsz1*FD+fsx1]*fstx)*fstz;
|
||||
// mind. 3 Bäume im Überlappungsbereich (fd > 2.0 bedeutet >2 Bäume)
|
||||
forestOverlayPixels[cy * cW + cx] = (fd > 2.0f)
|
||||
? (0x80000000 | (tileArgbs[WorldMapRenderer.TILE_FOREST] & 0x00FFFFFF))
|
||||
: 0x00000000;
|
||||
|
||||
int argb = (cr<<16)|(cgv<<8)|cbv;
|
||||
|
||||
// ── Cliff-Overlay
|
||||
if (t > 0f) {
|
||||
int cArgb = tileArgbs[WorldMapRenderer.TILE_CLIFF];
|
||||
int ccr=(cArgb>>16)&0xFF, ccg=(cArgb>>8)&0xFF, ccb=cArgb&0xFF;
|
||||
argb = ((int)(cr+(ccr-cr)*t)<<16)|((int)(cgv+(ccg-cgv)*t)<<8)|(int)(cbv+(ccb-cbv)*t);
|
||||
}
|
||||
|
||||
tileOverlayPixels[cy * cW + cx] = 0xFF000000 | (argb & 0x00FFFFFF);
|
||||
}
|
||||
}
|
||||
|
||||
tileOverlayImg.getPixelWriter().setPixels(
|
||||
0, 0, cW, cH, PixelFormat.getIntArgbInstance(), tileOverlayPixels, 0, cW);
|
||||
forestOverlayImg.getPixelWriter().setPixels(
|
||||
0, 0, cW, cH, PixelFormat.getIntArgbInstance(), forestOverlayPixels, 0, cW);
|
||||
gc.drawImage(tileOverlayImg, 0, 0);
|
||||
}
|
||||
|
||||
private void drawForestOverlay(GraphicsContext gc) {
|
||||
if (forestOverlayImg == null) return;
|
||||
gc.drawImage(forestOverlayImg, 0, 0);
|
||||
}
|
||||
|
||||
// ── Canvas-Vektor-Overlays ────────────────────────────────────────────────
|
||||
|
||||
private void drawAreasOverlay(GraphicsContext gc) {
|
||||
@@ -480,135 +768,96 @@ public class WorldMapView extends VBox {
|
||||
}
|
||||
|
||||
private void drawWaterOverlay(GraphicsContext gc) {
|
||||
if (currentModel == null) return;
|
||||
boolean[] seaMask = currentModel.seaMask();
|
||||
float[] terrainSamples = currentModel.terrainSamples();
|
||||
if (currentModel == null || heightMap == null || waterHeightMap == null) return;
|
||||
|
||||
int cW = (int) canvas.getWidth(), cH = (int) canvas.getHeight();
|
||||
if (cW <= 0 || cH <= 0) return;
|
||||
|
||||
// ── Wasser-Füllung: blauer Pixel-Layer über dem Terrain-Overlay ──────────
|
||||
if (waterFillPixels == null || waterFillPixels.length != cW * cH) {
|
||||
waterFillPixels = new int[cW * cH];
|
||||
}
|
||||
if (waterFillImg == null || waterFillW != cW || waterFillH != cH) {
|
||||
waterFillImg = new WritableImage(cW, cH);
|
||||
waterFillW = cW;
|
||||
waterFillH = cH;
|
||||
}
|
||||
|
||||
final float WH = WorldMapRenderer.WORLD_HALF;
|
||||
final float WS = WorldMapRenderer.WORLD_SIZE;
|
||||
final float scaleF = (float) scale;
|
||||
final float panXf = (float) panX;
|
||||
final float panYf = (float) panY;
|
||||
final float imgW = (float)(RENDER_SIZE - 1);
|
||||
final int CS = CAT_SIZE;
|
||||
final float[] hm = heightMap; // terrain+voxel
|
||||
final float[] whm = waterHeightMap; // PlacedWater-Höhe pro Zelle (0 = kein PlacedWater)
|
||||
final float CS1 = CS - 1f;
|
||||
final int WR = 0xAD, WG = 0xD8, WB = 0xE6; // WATER_COLOR = #ADD8E6
|
||||
|
||||
for (int cy = 0; cy < cH; cy++) {
|
||||
float wz = (cy - panYf) / scaleF / imgW * WS - WH;
|
||||
for (int cx = 0; cx < cW; cx++) {
|
||||
float wx = (cx - panXf) / scaleF / imgW * WS - WH;
|
||||
|
||||
if (wx < -WH || wx >= WH || wz < -WH || wz >= WH) {
|
||||
waterFillPixels[cy * cW + cx] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
float fmx = (wx + WH) / WS * CS1;
|
||||
float fmz = (wz + WH) / WS * CS1;
|
||||
int cmx = Math.max(0, Math.min(CS-1, (int) fmx));
|
||||
int cmz = Math.max(0, Math.min(CS-1, (int) fmz));
|
||||
int cmx1 = Math.min(CS-1, cmx+1), cmz1 = Math.min(CS-1, cmz+1);
|
||||
float tx = fmx - cmx, tz = fmz - cmz;
|
||||
|
||||
// Bilinear heightMap (terrain+voxel)
|
||||
float hPx = hm[cmz *CS+cmx ] * (1-tx)*(1-tz)
|
||||
+ hm[cmz *CS+cmx1] * tx *(1-tz)
|
||||
+ hm[cmz1*CS+cmx ] * (1-tx)*tz
|
||||
+ hm[cmz1*CS+cmx1] * tx *tz;
|
||||
|
||||
// Bilinear waterHeightMap → PlacedWater-Schwelle (0 = nur Ozean)
|
||||
float wH = whm[cmz *CS+cmx ] * (1-tx)*(1-tz)
|
||||
+ whm[cmz *CS+cmx1] * tx *(1-tz)
|
||||
+ whm[cmz1*CS+cmx ] * (1-tx)*tz
|
||||
+ whm[cmz1*CS+cmx1] * tx *tz;
|
||||
|
||||
// delta < 0 → unter Wasseroberfläche; Übergangsband ±0.5 WU
|
||||
float delta = hPx - wH;
|
||||
int alpha;
|
||||
if (delta <= -0.5f) {
|
||||
alpha = 255;
|
||||
} else if (delta < 0.5f) {
|
||||
alpha = Math.max(0, Math.min(255, (int)((0.5f - delta) * 255f)));
|
||||
} else {
|
||||
alpha = 0;
|
||||
}
|
||||
|
||||
waterFillPixels[cy * cW + cx] = (alpha << 24) | (WR << 16) | (WG << 8) | WB;
|
||||
}
|
||||
}
|
||||
|
||||
waterFillImg.getPixelWriter().setPixels(
|
||||
0, 0, cW, cH, PixelFormat.getIntArgbInstance(), waterFillPixels, 0, cW);
|
||||
gc.drawImage(waterFillImg, 0, 0);
|
||||
|
||||
// ── Küstenlinie – geglättete Pfade über der Wasser-Füllung ─────────────
|
||||
List<float[][]> seaCoastPaths = currentModel.coastPaths();
|
||||
|
||||
// ── Seewellen (Terrain < 0) ──────────────────────────────────────────────
|
||||
if (seaMask != null && cbWater.isSelected()) {
|
||||
// Wellengröße in Weltkoordinaten (skaliert mit Zoom, damit Bildschirmgröße = WAVE_W)
|
||||
double pxPerWorld = (RENDER_SIZE - 1) * scale / WorldMapRenderer.WORLD_SIZE;
|
||||
double wWave = WAVE_W / pxPerWorld;
|
||||
double wSpX = wWave * 1.8, wSpY = wWave * 1.05, wAmp = WAVE_W * 0.2;
|
||||
|
||||
// Sichtbarer Weltbereich
|
||||
double cW = canvas.getWidth(), cH = canvas.getHeight();
|
||||
double wxMin = toWorldX(0), wxMax = toWorldX(cW);
|
||||
double wzMin = toWorldZ(0), wzMax = toWorldZ(cH);
|
||||
wxMin = Math.max(-WorldMapRenderer.WORLD_HALF, wxMin);
|
||||
wzMin = Math.max(-WorldMapRenderer.WORLD_HALF, wzMin);
|
||||
wxMax = Math.min(WorldMapRenderer.WORLD_HALF, wxMax);
|
||||
wzMax = Math.min(WorldMapRenderer.WORLD_HALF, wzMax);
|
||||
|
||||
gc.setStroke(Color.BLACK);
|
||||
gc.setLineWidth(0.8);
|
||||
gc.setLineDashes(null);
|
||||
|
||||
// Gitter-Startzeile: auf Weltgitter einrasten → Wellen bleiben beim Panning stabil
|
||||
long rowBase = (long)Math.floor((wzMin - wSpY * 0.5) / wSpY);
|
||||
for (long rowIdx = rowBase; rowIdx * wSpY + wSpY * 0.5 < wzMax; rowIdx++) {
|
||||
double wz = rowIdx * wSpY + wSpY * 0.5;
|
||||
double rowOff = (rowIdx & 1L) == 1L ? wSpX * 0.5 : 0;
|
||||
long colBase = (long)Math.floor((wxMin - rowOff) / wSpX);
|
||||
for (long colIdx = colBase; colIdx * wSpX + rowOff < wxMax; colIdx++) {
|
||||
double wx = colIdx * wSpX + rowOff;
|
||||
double jx = Math.abs(Math.sin(rowIdx * 73.1 + colIdx * 157.3)) * wSpX * 0.25;
|
||||
double jy = Math.sin(rowIdx * 211.7 + colIdx * 89.5) * wSpY * 0.2;
|
||||
double owx = wx + jx, owz = wz + jy;
|
||||
// Seekartenmaske prüfen (Mitte und Ende der Welle)
|
||||
int mxS = (int)((owx + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE - 1));
|
||||
int mzS = (int)((owz + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE - 1));
|
||||
int mxE = (int)((owx + wWave + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE - 1));
|
||||
if (mxS < 0 || mxS >= SEA_MASK_SIZE || mzS < 0 || mzS >= SEA_MASK_SIZE) continue;
|
||||
if (!seaMask[mzS * SEA_MASK_SIZE + mxS]) continue;
|
||||
if (mxE < 0 || mxE >= SEA_MASK_SIZE || !seaMask[mzS * SEA_MASK_SIZE + mxE]) continue;
|
||||
// Canvas-Koordinaten; Breite = WAVE_W px, Amplitude = wAmp px
|
||||
double ox = toCanvasX((float)owx), oy = toCanvasZ((float)owz);
|
||||
double ex = ox + WAVE_W;
|
||||
gc.beginPath();
|
||||
gc.moveTo(ox, oy);
|
||||
gc.bezierCurveTo(ox + WAVE_W*0.3, oy - wAmp, ox + WAVE_W*0.7, oy + wAmp, ex, oy);
|
||||
gc.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<PlacedWater> waters = currentModel.waters();
|
||||
if (waters.isEmpty()) return;
|
||||
|
||||
final double spX = WAVE_W * 1.8, spY = WAVE_W * 1.05, wmA = WAVE_W * 0.2;
|
||||
|
||||
for (PlacedWater w : waters) {
|
||||
float[] wx = w.pointsX(), wz = w.pointsZ();
|
||||
int n = wx.length;
|
||||
if (n < 3) continue;
|
||||
|
||||
float wh = w.waterHeight();
|
||||
|
||||
double[] cx = new double[n], cz = new double[n];
|
||||
for (int i = 0; i < n; i++) { cx[i] = toCanvasX(wx[i]); cz[i] = toCanvasZ(wz[i]); }
|
||||
|
||||
double minX = cx[0], maxX = cx[0], minZ = cz[0], maxZ = cz[0];
|
||||
for (int i = 1; i < n; i++) {
|
||||
if (cx[i] < minX) minX = cx[i]; if (cx[i] > maxX) maxX = cx[i];
|
||||
if (cz[i] < minZ) minZ = cz[i]; if (cz[i] > maxZ) maxZ = cz[i];
|
||||
}
|
||||
|
||||
// Wellenmarken: nur wenn Fläche groß genug für mindestens eine Welle
|
||||
if ((maxX - minX) > WAVE_W * 1.5 && (maxZ - minZ) > spY) {
|
||||
gc.save();
|
||||
gc.beginPath();
|
||||
gc.moveTo(cx[0], cz[0]);
|
||||
for (int i = 1; i < n; i++) { gc.lineTo(cx[i], cz[i]); }
|
||||
gc.closePath();
|
||||
gc.clip();
|
||||
|
||||
gc.setStroke(Color.BLACK);
|
||||
gc.setLineWidth(0.8);
|
||||
gc.setLineDashes(null);
|
||||
|
||||
int row = 0;
|
||||
for (double wy = minZ + spY * 0.5; wy < maxZ; wy += spY, row++) {
|
||||
double rowOff = (row & 1) == 1 ? spX * 0.5 : 0;
|
||||
int col = 0;
|
||||
for (double wxx = minX + rowOff; wxx + WAVE_W <= maxX; wxx += spX, col++) {
|
||||
double jx = Math.abs(Math.sin(row * 73.1 + col * 157.3)) * spX * 0.25;
|
||||
double jy = Math.sin(row * 211.7 + col * 89.5) * spY * 0.2;
|
||||
double ox = wxx + jx, oy = wy + jy;
|
||||
if (ox < minX || ox + WAVE_W > maxX || oy < minZ || oy > maxZ) { continue; }
|
||||
// Höhenprüfung: Terrain muss unter waterHeight liegen
|
||||
if (terrainSamples != null) {
|
||||
float owx = toWorldX(ox), owz = toWorldZ(oy);
|
||||
int mxS = Math.max(0, Math.min(SEA_MASK_SIZE-1, (int)((owx + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE-1))));
|
||||
int mzS = Math.max(0, Math.min(SEA_MASK_SIZE-1, (int)((owz + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE-1))));
|
||||
if (terrainSamples[mzS * SEA_MASK_SIZE + mxS] >= wh) { continue; }
|
||||
}
|
||||
gc.beginPath();
|
||||
gc.moveTo(ox, oy);
|
||||
gc.bezierCurveTo(ox + WAVE_W*0.3, oy - wmA, ox + WAVE_W*0.7, oy + wmA, ox + WAVE_W, oy);
|
||||
gc.stroke();
|
||||
}
|
||||
}
|
||||
gc.restore();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ── Seeküstenlinie – verbundene Pfade, runde Joins, weicher Halo ────────────
|
||||
if (seaCoastPaths != null && !seaCoastPaths.isEmpty()) {
|
||||
double cW = canvas.getWidth(), cH = canvas.getHeight();
|
||||
if (cbCoastline.isSelected() && seaCoastPaths != null && !seaCoastPaths.isEmpty()) {
|
||||
double canvasW = canvas.getWidth(), canvasH = canvas.getHeight();
|
||||
gc.setLineDashes(null);
|
||||
gc.setLineCap(StrokeLineCap.ROUND);
|
||||
gc.setLineJoin(StrokeLineJoin.ROUND);
|
||||
|
||||
gc.setStroke(Color.color(0, 0, 0, 0.22));
|
||||
gc.setLineWidth(7.0);
|
||||
for (float[][] path : seaCoastPaths) { drawCoastPath(gc, path, cW, cH, 8); }
|
||||
for (float[][] path : seaCoastPaths) { drawCoastPath(gc, path, canvasW, canvasH, 8); }
|
||||
|
||||
gc.setStroke(Color.BLACK);
|
||||
gc.setLineWidth(3.0);
|
||||
for (float[][] path : seaCoastPaths) { drawCoastPath(gc, path, cW, cH, 4); }
|
||||
for (float[][] path : seaCoastPaths) { drawCoastPath(gc, path, canvasW, canvasH, 4); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -811,35 +1060,17 @@ public class WorldMapView extends VBox {
|
||||
List<Location> locs, List<PlacedWater> waters,
|
||||
List<PlacedModel> models, List<PlacedWaterfall> waterfalls,
|
||||
float[] voxelSurface) {
|
||||
int[] slotColors = computeSlotColors(mapData);
|
||||
boolean[] mask = WorldMapRenderer.buildSeaMask(mapData, SEA_MASK_SIZE, voxelSurface);
|
||||
float[] tSamp = buildTerrainSamples(mapData, SEA_MASK_SIZE);
|
||||
int[] slotColors = computeSlotColors(mapData);
|
||||
int[][] mapTiles = WorldMapRenderer.loadMapTiles(findAssetRoot());
|
||||
boolean[] mask = WorldMapRenderer.buildSeaMask(mapData, SEA_MASK_SIZE, voxelSurface);
|
||||
float[] tSamp = WorldMapRenderer.buildTerrainSamples(mapData, SEA_MASK_SIZE, voxelSurface);
|
||||
List<float[][]> paths = buildSeaCoastPaths(buildCoastSegsMS(mapData, waters, SEA_MASK_SIZE, voxelSurface));
|
||||
List<PlacedModel> treeList = models.stream()
|
||||
.filter(WorldMapRenderer::isTree).collect(Collectors.toList());
|
||||
List<List<PlacedModel>> tClusters = clusterTreeModels(treeList, 40f);
|
||||
return new WorldMapRenderModel(
|
||||
mapData, areas, zones, locs, waters, models, waterfalls, slotColors,
|
||||
mask, voxelSurface, tSamp, paths, tClusters);
|
||||
}
|
||||
|
||||
private static float[] buildTerrainSamples(MapData mapData, int size) {
|
||||
int TV = MapData.TERRAIN_VERTS;
|
||||
int UV = MapData.UPPER_VERTS;
|
||||
float[] s = new float[size * size];
|
||||
for (int mz = 0; mz < size; mz++) {
|
||||
for (int mx = 0; mx < size; mx++) {
|
||||
int hx = Math.min((int)(mx / (double)(size-1) * (TV-1)), TV-1);
|
||||
int hz = Math.min((int)(mz / (double)(size-1) * (TV-1)), TV-1);
|
||||
int ux = Math.min((int)(mx / (double)(size-1) * (UV-1)), UV-1);
|
||||
int uz = Math.min((int)(mz / (double)(size-1) * (UV-1)), UV-1);
|
||||
float h = mapData.terrainHeight[hz * TV + hx];
|
||||
float upper = mapData.upperTop[uz * UV + ux];
|
||||
if (upper > 0f && upper > h) { h = upper; }
|
||||
s[mz * size + mx] = h;
|
||||
}
|
||||
}
|
||||
return s;
|
||||
mask, voxelSurface, tSamp, paths, tClusters, mapTiles);
|
||||
}
|
||||
|
||||
// Erzeugt interpolierte Marching-Squares-Segmente für Meer + Wasserflächen.
|
||||
@@ -1147,7 +1378,7 @@ public class WorldMapView extends VBox {
|
||||
cbTerrain.isSelected(),
|
||||
cbTerrain.isSelected(),
|
||||
cbWater.isSelected(),
|
||||
true, // Wellen im Export-PNG gebacken
|
||||
false,
|
||||
cbAreas.isSelected(),
|
||||
cbZones.isSelected(),
|
||||
cbLocations.isSelected(),
|
||||
|
||||
Reference in New Issue
Block a user