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:
2026-09-01 22:05:38 +02:00
parent 0fc8f7f3c3
commit 2fd6a9b737
14 changed files with 937 additions and 363 deletions

View File

@@ -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");

View File

@@ -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) {

View File

@@ -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());

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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;

View File

@@ -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(),