Arbeiten an der Karte - neuer Render Modus mit Zwischenschicht, Küstenlinien, Wasserfälle

This commit is contained in:
2026-08-29 22:27:57 +02:00
parent 36c6a9b7d8
commit 0fc8f7f3c3
19 changed files with 369 additions and 87 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -0,0 +1,28 @@
#Tue Aug 25 21:32:55 CEST 2026
attachedEmitters.count=0
attachedLights.count=0
castShadow=true
category=
cullDistance=120.0
footstepSurface=
interactableOffsetX=0.0
interactableOffsetY=0.5
interactableOffsetZ=0.0
interactableRotY=0.0
interactableType=NONE
lod1Distance=30.0
lod1Path=
lod2Distance=80.0
lod2Path=
name=3D-Modell eines schwarzen Wolfs
pivotOffsetY=0.0
placementOffsetY=0.0
randomScaleMax=1.0
randomScaleMin=1.0
receiveShadow=true
scaleX=1.0
scaleY=1.0
scaleZ=1.0
solid=true
tags=
uniformScale=true

View File

@@ -139,12 +139,17 @@ public final class VoxelChunkIO {
/** /**
* Liest alle vorhandenen VoxelChunks aus dem Chunks-Verzeichnis. * Liest alle vorhandenen VoxelChunks aus dem Chunks-Verzeichnis.
* Für Koordinaten, bei denen die .blvc nach dem Backen gelöscht wurde,
* wird automatisch die .blvc.prebake als Fallback herangezogen.
* Gibt leere Liste zurück wenn kein Chunks-Verzeichnis existiert. * Gibt leere Liste zurück wenn kein Chunks-Verzeichnis existiert.
*/ */
public static List<VoxelChunk> loadAll() { public static List<VoxelChunk> loadAll() {
List<VoxelChunk> result = new ArrayList<>(); List<VoxelChunk> result = new ArrayList<>();
Path dir = ChunkTerrainIO.chunksDir(); Path dir = ChunkTerrainIO.chunksDir();
if (!Files.isDirectory(dir)) return result; if (!Files.isDirectory(dir)) return result;
// Aktive .blvc-Dateien einlesen
Set<String> loaded = new java.util.HashSet<>();
try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir, "voxel_*.blvc")) { try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir, "voxel_*.blvc")) {
for (Path p : ds) { for (Path p : ds) {
String name = p.getFileName().toString() String name = p.getFileName().toString()
@@ -158,9 +163,30 @@ public final class VoxelChunkIO {
: Integer.parseInt(parts[1]); : Integer.parseInt(parts[1]);
int cz = Integer.parseInt(parts[2]); int cz = Integer.parseInt(parts[2]);
result.add(VoxelChunk.deserialize(Files.readAllBytes(p), cx, cy, cz)); result.add(VoxelChunk.deserialize(Files.readAllBytes(p), cx, cy, cz));
loaded.add(name);
} catch (Exception ignored) {} } catch (Exception ignored) {}
} }
} catch (IOException ignored) {} } catch (IOException ignored) {}
// Prebake-Fallback: Koordinaten die gebacken wurden haben keine .blvc mehr
try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir, "voxel_*.blvc.prebake")) {
for (Path p : ds) {
String name = p.getFileName().toString()
.replace("voxel_", "").replace(".blvc.prebake", "");
if (loaded.contains(name)) continue; // aktive .blvc hat Vorrang
String[] parts = name.split("_");
if (parts.length != 3) continue;
try {
int cx = Integer.parseInt(parts[0]);
int cy = parts[1].startsWith("m")
? -Integer.parseInt(parts[1].substring(1))
: Integer.parseInt(parts[1]);
int cz = Integer.parseInt(parts[2]);
result.add(VoxelChunk.deserialize(Files.readAllBytes(p), cx, cy, cz));
} catch (Exception ignored) {}
}
} catch (IOException ignored) {}
return result; return result;
} }
} }

View File

@@ -18,9 +18,11 @@ public record WorldMapRenderModel(
List<Location> locations, List<Location> locations,
List<PlacedWater> waters, List<PlacedWater> waters,
List<PlacedModel> models, List<PlacedModel> models,
List<PlacedWaterfall> waterfalls,
int[] slotColorsRGB, int[] slotColorsRGB,
// Vorberechnete Overlay-Daten (werden vom Canvas genutzt, nicht vom PNG-Renderer) // Vorberechnete Overlay-Daten (werden vom Canvas genutzt, nicht vom PNG-Renderer)
boolean[] seaMask, // Wasser-Pixel-Maske bei SEA_MASK_SIZE-Auflösung boolean[] seaMask, // Wasser-Pixel-Maske bei SEA_MASK_SIZE-Auflösung
float[] voxelSurface, // oberster solider Voxel-Y pro UPPER_VERTS-Zelle; NaN = keine Daten
float[] terrainSamples, // Höhenwerte bei SEA_MASK_SIZE (für Wellen-Prüfung) float[] terrainSamples, // Höhenwerte bei SEA_MASK_SIZE (für Wellen-Prüfung)
List<float[][]> coastPaths, // geglättete Marching-Squares Küstenpfade (Weltkoord.) List<float[][]> coastPaths, // geglättete Marching-Squares Küstenpfade (Weltkoord.)
List<List<PlacedModel>> treeClusters List<List<PlacedModel>> treeClusters

View File

@@ -23,6 +23,8 @@ public final class WorldMapRenderer {
public static final float WORLD_HALF = 1024f; public static final float WORLD_HALF = 1024f;
public static final float WORLD_SIZE = 2048f; public static final float WORLD_SIZE = 2048f;
/** Auflösung des voxelSurface-Bake-Arrays (1 Zelle pro Welteinheit). */
public static final int VOXEL_SURFACE_RES = 2048;
public record RenderInput( public record RenderInput(
MapData mapData, MapData mapData,
@@ -47,32 +49,46 @@ public final class WorldMapRenderer {
boolean showAreas, boolean showAreas,
boolean showZones, boolean showZones,
boolean showLocations, boolean showLocations,
boolean showModels boolean showModels,
boolean showWaterfalls
) { ) {
// Compat: alte 7-Parameter-Form showWaterWaves = showWater // Compat: 8-Parameter-Form (ohne showWaterfalls) → showWaterfalls = true
public RenderOptions(boolean showTerrain, boolean showSplatColors, boolean showWater,
boolean showWaterWaves, boolean showAreas, boolean showZones,
boolean showLocations, boolean showModels) {
this(showTerrain, showSplatColors, showWater, showWaterWaves,
showAreas, showZones, showLocations, showModels, true);
}
// Compat: alte 7-Parameter-Form → showWaterWaves = showWater, showWaterfalls = true
public RenderOptions(boolean showTerrain, boolean showSplatColors, boolean showWater, public RenderOptions(boolean showTerrain, boolean showSplatColors, boolean showWater,
boolean showAreas, boolean showZones, boolean showLocations, boolean showModels) { boolean showAreas, boolean showZones, boolean showLocations, boolean showModels) {
this(showTerrain, showSplatColors, showWater, showWater, this(showTerrain, showSplatColors, showWater, showWater,
showAreas, showZones, showLocations, showModels); showAreas, showZones, showLocations, showModels, true);
} }
public static RenderOptions all() { public static RenderOptions all() {
return new RenderOptions(true, true, true, true, true, true, true, true); return new RenderOptions(true, true, true, true, true, true, true, true, true);
} }
} }
public static boolean[] buildSeaMask(MapData m, int size) { public static boolean[] buildSeaMask(MapData m, int size) {
return buildSeaMask(m, size, null);
}
public static boolean[] buildSeaMask(MapData m, int size, float[] voxelSurface) {
int TV = MapData.TERRAIN_VERTS; int TV = MapData.TERRAIN_VERTS;
int UV = MapData.UPPER_VERTS; int VS = VOXEL_SURFACE_RES;
boolean[] mask = new boolean[size * size]; boolean[] mask = new boolean[size * size];
for (int py = 0; py < size; py++) { for (int py = 0; py < size; py++) {
for (int px = 0; px < size; px++) { for (int px = 0; px < size; px++) {
int hx = Math.min((int)((float) px / (size - 1) * (TV - 1)), TV - 1); int hx = Math.min((int)((float) px / (size - 1) * (TV - 1)), TV - 1);
int hz = Math.min((int)((float) py / (size - 1) * (TV - 1)), TV - 1); int hz = Math.min((int)((float) py / (size - 1) * (TV - 1)), TV - 1);
int ux = Math.min((int)((float) px / (size - 1) * (UV - 1)), UV - 1);
int uz = Math.min((int)((float) py / (size - 1) * (UV - 1)), UV - 1);
float h = m.terrainHeight[hz * TV + hx]; float h = m.terrainHeight[hz * TV + hx];
float upper = m.upperTop[uz * UV + ux]; if (voxelSurface != null) {
if (upper > 0f && upper > h) { h = upper; } int vsPx = Math.min((int)((float) px / (size - 1) * (VS - 1)), VS - 1);
int vsPz = Math.min((int)((float) py / (size - 1) * (VS - 1)), VS - 1);
float vs = voxelSurface[vsPz * VS + vsPx];
if (!Float.isNaN(vs) && vs > h) { h = vs; }
}
mask[py * size + px] = h < 0f; mask[py * size + px] = h < 0f;
} }
} }
@@ -100,18 +116,37 @@ public final class WorldMapRenderer {
// ── 1. Heightmap auf Zielauflösung samplen ──────────────────────────── // ── 1. Heightmap auf Zielauflösung samplen ────────────────────────────
int UV = MapData.UPPER_VERTS; int UV = MapData.UPPER_VERTS;
float[] heights = new float[targetSize * targetSize]; float[] heights = new float[targetSize * targetSize];
// Meerserkennung basiert nur auf terrainHeight (ohne upperTop/voxelSurface):
// bakeVoxelHeights trägt Voxel-Höhen in upperTop ein, was sonst
// Voxel auf dem Meeresboden fälschlich als Landfläche klassifiziert.
boolean[] seaPixels = new boolean[targetSize * targetSize];
float minH = Float.MAX_VALUE, maxH = -Float.MAX_VALUE; float minH = Float.MAX_VALUE, maxH = -Float.MAX_VALUE;
int VS = VOXEL_SURFACE_RES;
for (int py = 0; py < targetSize; py++) { for (int py = 0; py < targetSize; py++) {
for (int px = 0; px < targetSize; px++) { for (int px = 0; px < targetSize; px++) {
int hx = Math.min((int)((float) px / (targetSize - 1) * (TV - 1)), TV - 1); int hx = Math.min((int)((float) px / (targetSize - 1) * (TV - 1)), TV - 1);
int hz = Math.min((int)((float) py / (targetSize - 1) * (TV - 1)), TV - 1); int hz = Math.min((int)((float) py / (targetSize - 1) * (TV - 1)), TV - 1);
int ux = Math.min((int)((float) px / (targetSize - 1) * (UV - 1)), UV - 1); float ux_f = (float) px / (targetSize - 1) * (UV - 1);
int uz = Math.min((int)((float) py / (targetSize - 1) * (UV - 1)), UV - 1); float uz_f = (float) py / (targetSize - 1) * (UV - 1);
float h = m.terrainHeight[hz * TV + hx]; float baseH = m.terrainHeight[hz * TV + hx];
float upper = m.upperTop[uz * UV + ux]; float h = baseH;
float upper = bilerpPos(m.upperTop, ux_f, uz_f, UV);
if (upper > 0f && upper > h) { h = upper; } if (upper > 0f && upper > h) { h = upper; }
// Voxel-Oberfläche: direkte 1:1-Abfrage des hochauflösenden Bake-Arrays
// seaH = max(terrain, voxelSurface): Voxel über Y=0 → Land, darunter → Meer
float seaH = baseH;
if (model.voxelSurface() != null) {
int vsPx = Math.min((int)((float) px / (targetSize - 1) * (VS - 1)), VS - 1);
int vsPz = Math.min((int)((float) py / (targetSize - 1) * (VS - 1)), VS - 1);
float vs = model.voxelSurface()[vsPz * VS + vsPx];
if (!Float.isNaN(vs)) {
h = vs;
if (vs > seaH) { seaH = vs; }
}
}
heights[py * targetSize + px] = h; heights[py * targetSize + px] = h;
seaPixels[py * targetSize + px] = (seaH < 0f);
if (h < minH) minH = h; if (h < minH) minH = h;
if (h > maxH) maxH = h; if (h > maxH) maxH = h;
} }
@@ -188,7 +223,7 @@ public final class WorldMapRenderer {
// ── Weißfüllung ─────────────────────────────────────────────────────── // ── Weißfüllung ───────────────────────────────────────────────────────
for (int py = 0; py < targetSize; py++) { for (int py = 0; py < targetSize; py++) {
for (int px = 0; px < targetSize; px++) { for (int px = 0; px < targetSize; px++) {
if (heights[py * targetSize + px] < 0f) img.setRGB(px, py, WATER_COLOR); if (seaPixels[py * targetSize + px]) img.setRGB(px, py, WATER_COLOR);
} }
} }
for (PlacedWater w : model.waters()) { for (PlacedWater w : model.waters()) {
@@ -227,7 +262,7 @@ public final class WorldMapRenderer {
int cx = (int)(ox + wmW * 0.5f), cy = (int)oy; int cx = (int)(ox + wmW * 0.5f), cy = (int)oy;
if (cx >= 0 && cx < targetSize && cy >= 0 && cy < targetSize if (cx >= 0 && cx < targetSize && cy >= 0 && cy < targetSize
&& ox >= 0 && ox + wmW < targetSize && ox >= 0 && ox + wmW < targetSize
&& heights[cy * targetSize + cx] < 0f) { && seaPixels[cy * targetSize + cx]) {
Path2D mark = new Path2D.Float(); Path2D mark = new Path2D.Float();
mark.moveTo(ox, oy); mark.moveTo(ox, oy);
mark.curveTo(ox + wmW*0.3f, oy - wmA, ox + wmW*0.7f, oy + wmA, ox + wmW, oy); mark.curveTo(ox + wmW*0.3f, oy - wmA, ox + wmW*0.7f, oy + wmA, ox + wmW, oy);
@@ -278,9 +313,9 @@ public final class WorldMapRenderer {
boolean[] nxt = new boolean[targetSize * targetSize]; boolean[] nxt = new boolean[targetSize * targetSize];
for (int py = 1; py < targetSize - 1; py++) { for (int py = 1; py < targetSize - 1; py++) {
for (int px = 1; px < targetSize - 1; px++) { for (int px = 1; px < targetSize - 1; px++) {
if (heights[py * targetSize + px] >= 0f) continue; if (!seaPixels[py * targetSize + px]) continue;
if (heights[py * targetSize + (px-1)] >= 0f || heights[py * targetSize + (px+1)] >= 0f || if (!seaPixels[py * targetSize + (px-1)] || !seaPixels[py * targetSize + (px+1)] ||
heights[(py-1) * targetSize + px] >= 0f || heights[(py+1) * targetSize + px] >= 0f) !seaPixels[(py-1) * targetSize + px] || !seaPixels[(py+1) * targetSize + px])
cur[py * targetSize + px] = true; cur[py * targetSize + px] = true;
} }
} }
@@ -289,10 +324,10 @@ public final class WorldMapRenderer {
for (int py = 1; py < targetSize - 1; py++) { for (int py = 1; py < targetSize - 1; py++) {
for (int px = 1; px < targetSize - 1; px++) { for (int px = 1; px < targetSize - 1; px++) {
if (!cur[py * targetSize + px]) continue; if (!cur[py * targetSize + px]) continue;
if (heights[py * targetSize + (px-1)] < 0f) nxt[py * targetSize + (px-1)] = true; if (seaPixels[py * targetSize + (px-1)]) nxt[py * targetSize + (px-1)] = true;
if (heights[py * targetSize + (px+1)] < 0f) nxt[py * targetSize + (px+1)] = true; if (seaPixels[py * targetSize + (px+1)]) nxt[py * targetSize + (px+1)] = true;
if (heights[(py-1) * targetSize + px] < 0f) nxt[(py-1) * targetSize + px] = true; if (seaPixels[(py-1) * targetSize + px]) nxt[(py-1) * targetSize + px] = true;
if (heights[(py+1) * targetSize + px] < 0f) nxt[(py+1) * targetSize + px] = true; if (seaPixels[(py+1) * targetSize + px]) nxt[(py+1) * targetSize + px] = true;
} }
} }
boolean[] tmp = cur; cur = nxt; nxt = tmp; boolean[] tmp = cur; cur = nxt; nxt = tmp;
@@ -303,6 +338,66 @@ public final class WorldMapRenderer {
} }
} }
// Wasserfälle klassisches Kartensymbol: Kammlinie + Kaskaden-Linien
if (opts.showWaterfalls() && model.waterfalls() != null && !model.waterfalls().isEmpty()) {
float wfLineW = Math.max(1.5f, targetSize / 800f);
for (PlacedWaterfall wf : model.waterfalls()) {
// Kamm A→B und Basis D→C in Pixel-Koordinaten
int pax = worldToPixel(wf.ax(), targetSize), paz = worldToPixel(wf.az(), targetSize);
int pbx = worldToPixel(wf.bx(), targetSize), pbz = worldToPixel(wf.bz(), targetSize);
int pdx = worldToPixel(wf.dx(), targetSize), pdz = worldToPixel(wf.dz(), targetSize);
int pcx = worldToPixel(wf.cx(), targetSize), pcz = worldToPixel(wf.cz(), targetSize);
// Kammrichtung in Pixel-Space
float cdx = pbx - pax, cdz = pbz - paz;
float clen = (float) Math.sqrt(cdx * cdx + cdz * cdz);
if (clen < 1f) { continue; }
float ncx = cdx / clen, ncz = cdz / clen;
// Fließrichtung: Basismitten → Kammmitte im Pixel-Space
float pmcx = (pax + pbx) * 0.5f, pmcz = (paz + pbz) * 0.5f;
float pmbx = (pdx + pcx) * 0.5f, pmbz = (pdz + pcz) * 0.5f;
float fvx = pmbx - pmcx, fvz = pmbz - pmcz;
float flen = (float) Math.sqrt(fvx * fvx + fvz * fvz);
float perpX, perpZ;
if (flen > 2f) {
// echte XZ-Fallrichtung verfügbar
perpX = fvx / flen;
perpZ = fvz / flen;
} else {
// Wasserfall senkrecht → Rechtsnormale der Kammlinie als Fallback
perpX = ncz;
perpZ = -ncx;
}
// Pixelabstand pro Kaskaden-Stufe (mindestens 3 px, skaliert mit Bild)
float step = Math.max(3f, targetSize / 512f) * 1.8f;
// ① Dunkler Halo unter der Kammlinie
gfx.setStroke(new BasicStroke(wfLineW * 2.5f + 1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
gfx.setColor(new Color(10, 50, 120, 160));
gfx.drawLine(pax, paz, pbx, pbz);
// ② Helle Kammlinie (Kante, über die das Wasser fällt)
gfx.setStroke(new BasicStroke(wfLineW * 2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
gfx.setColor(new Color(220, 240, 255, 255));
gfx.drawLine(pax, paz, pbx, pbz);
// ③ Kaskaden-Linien: 2 parallele Linien in Fallrichtung, jede schwächer
int[] alpha = {190, 100};
float[] wf2 = {wfLineW * 1.3f, wfLineW * 0.8f};
for (int ci = 0; ci < 2; ci++) {
float off = step * (ci + 1);
int ox1 = Math.round(pax + perpX * off), oz1 = Math.round(paz + perpZ * off);
int ox2 = Math.round(pbx + perpX * off), oz2 = Math.round(pbz + perpZ * off);
gfx.setStroke(new BasicStroke(wf2[ci], BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
gfx.setColor(new Color(160, 210, 255, alpha[ci]));
gfx.drawLine(ox1, oz1, ox2, oz2);
}
}
}
// Area-Polygone // Area-Polygone
if (opts.showAreas()) { if (opts.showAreas()) {
float dash = lineW * 7f, gap = lineW * 4f; float dash = lineW * 7f, gap = lineW * 4f;
@@ -375,8 +470,8 @@ public final class WorldMapRenderer {
public static BufferedImage render(RenderInput input, int targetSize, RenderOptions opts) { public static BufferedImage render(RenderInput input, int targetSize, RenderOptions opts) {
return render(new WorldMapRenderModel( return render(new WorldMapRenderModel(
input.mapData(), input.areas(), input.zones(), input.locations(), input.mapData(), input.areas(), input.zones(), input.locations(),
input.waters(), input.models(), input.slotColorsRGB(), input.waters(), input.models(), null,
null, null, null, null input.slotColorsRGB(), null, null, null, null, null
), targetSize, opts); ), targetSize, opts);
} }
@@ -408,9 +503,11 @@ public final class WorldMapRenderer {
float[] heights = null; float[] heights = null;
float minH = 0f, maxH = 1f; float minH = 0f, maxH = 1f;
boolean[] seaPixels2 = null;
if (opts.showTerrain() || opts.showWater()) { if (opts.showTerrain() || opts.showWater()) {
int UV2 = MapData.UPPER_VERTS; int UV2 = MapData.UPPER_VERTS;
heights = new float[targetSize * targetSize]; heights = new float[targetSize * targetSize];
seaPixels2 = new boolean[targetSize * targetSize];
minH = Float.MAX_VALUE; minH = Float.MAX_VALUE;
maxH = -Float.MAX_VALUE; maxH = -Float.MAX_VALUE;
for (int py = 0; py < targetSize; py++) { for (int py = 0; py < targetSize; py++) {
@@ -421,10 +518,13 @@ public final class WorldMapRenderer {
float wx = wx0 + (float) px / (targetSize - 1) * rSize; float wx = wx0 + (float) px / (targetSize - 1) * rSize;
int hx = iclamp((int) ((wx + WORLD_HALF) / WORLD_SIZE * (TV - 1)), 0, TV - 1); int hx = iclamp((int) ((wx + WORLD_HALF) / WORLD_SIZE * (TV - 1)), 0, TV - 1);
int ux = iclamp((int) ((wx + WORLD_HALF) / WORLD_SIZE * (UV2 - 1)), 0, UV2 - 1); int ux = iclamp((int) ((wx + WORLD_HALF) / WORLD_SIZE * (UV2 - 1)), 0, UV2 - 1);
float h = m.terrainHeight[hz * TV + hx]; float baseH = m.terrainHeight[hz * TV + hx];
float h = baseH;
float upper = m.upperTop[uz * UV2 + ux]; float upper = m.upperTop[uz * UV2 + ux];
if (upper > 0f && upper > h) { h = upper; } if (upper > 0f && upper > h) { h = upper; }
heights[py * targetSize + px] = h; heights[py * targetSize + px] = h;
// upperTop enthält nach bakeVoxelHeights auch Voxel-Höhen → korrekt als Proxy nutzbar
seaPixels2[py * targetSize + px] = (Math.max(baseH, upper) < 0f);
if (h < minH) { minH = h; } if (h < minH) { minH = h; }
if (h > maxH) { maxH = h; } if (h > maxH) { maxH = h; }
} }
@@ -507,7 +607,7 @@ public final class WorldMapRenderer {
// ── Weißfüllung ─────────────────────────────────────────────────────── // ── Weißfüllung ───────────────────────────────────────────────────────
for (int py = 0; py < targetSize; py++) { for (int py = 0; py < targetSize; py++) {
for (int px = 0; px < targetSize; px++) { for (int px = 0; px < targetSize; px++) {
if (heights[py * targetSize + px] < 0f) img.setRGB(px, py, WATER_COLOR); if (seaPixels2 != null && seaPixels2[py * targetSize + px]) img.setRGB(px, py, WATER_COLOR);
} }
} }
for (PlacedWater w : input.waters()) { for (PlacedWater w : input.waters()) {
@@ -546,7 +646,7 @@ public final class WorldMapRenderer {
int cx = (int)(ox + wmW * 0.5f), cy = (int)oy; int cx = (int)(ox + wmW * 0.5f), cy = (int)oy;
if (cx >= 0 && cx < targetSize && cy >= 0 && cy < targetSize if (cx >= 0 && cx < targetSize && cy >= 0 && cy < targetSize
&& ox >= 0 && ox + wmW < targetSize && ox >= 0 && ox + wmW < targetSize
&& heights[cy * targetSize + cx] < 0f) { && seaPixels2 != null && seaPixels2[cy * targetSize + cx]) {
Path2D mark = new Path2D.Float(); Path2D mark = new Path2D.Float();
mark.moveTo(ox, oy); mark.moveTo(ox, oy);
mark.curveTo(ox + wmW*0.3f, oy - wmA, ox + wmW*0.7f, oy + wmA, ox + wmW, oy); mark.curveTo(ox + wmW*0.3f, oy - wmA, ox + wmW*0.7f, oy + wmA, ox + wmW, oy);
@@ -597,9 +697,9 @@ public final class WorldMapRenderer {
boolean[] nxt = new boolean[targetSize * targetSize]; boolean[] nxt = new boolean[targetSize * targetSize];
for (int py = 1; py < targetSize - 1; py++) { for (int py = 1; py < targetSize - 1; py++) {
for (int px = 1; px < targetSize - 1; px++) { for (int px = 1; px < targetSize - 1; px++) {
if (heights[py * targetSize + px] >= 0f) continue; if (seaPixels2 == null || !seaPixels2[py * targetSize + px]) continue;
if (heights[py * targetSize + (px-1)] >= 0f || heights[py * targetSize + (px+1)] >= 0f || if (!seaPixels2[py * targetSize + (px-1)] || !seaPixels2[py * targetSize + (px+1)] ||
heights[(py-1) * targetSize + px] >= 0f || heights[(py+1) * targetSize + px] >= 0f) !seaPixels2[(py-1) * targetSize + px] || !seaPixels2[(py+1) * targetSize + px])
cur[py * targetSize + px] = true; cur[py * targetSize + px] = true;
} }
} }
@@ -608,10 +708,10 @@ public final class WorldMapRenderer {
for (int py = 1; py < targetSize - 1; py++) { for (int py = 1; py < targetSize - 1; py++) {
for (int px = 1; px < targetSize - 1; px++) { for (int px = 1; px < targetSize - 1; px++) {
if (!cur[py * targetSize + px]) continue; if (!cur[py * targetSize + px]) continue;
if (heights[py * targetSize + (px-1)] < 0f) nxt[py * targetSize + (px-1)] = true; if (seaPixels2[py * targetSize + (px-1)]) nxt[py * targetSize + (px-1)] = true;
if (heights[py * targetSize + (px+1)] < 0f) nxt[py * targetSize + (px+1)] = true; if (seaPixels2[py * targetSize + (px+1)]) nxt[py * targetSize + (px+1)] = true;
if (heights[(py-1) * targetSize + px] < 0f) nxt[(py-1) * targetSize + px] = true; if (seaPixels2[(py-1) * targetSize + px]) nxt[(py-1) * targetSize + px] = true;
if (heights[(py+1) * targetSize + px] < 0f) nxt[(py+1) * targetSize + px] = true; if (seaPixels2[(py+1) * targetSize + px]) nxt[(py+1) * targetSize + px] = true;
} }
} }
boolean[] tmp = cur; cur = nxt; nxt = tmp; boolean[] tmp = cur; cur = nxt; nxt = tmp;
@@ -830,6 +930,32 @@ public final class WorldMapRenderer {
} }
} }
/** Bilinear auf float[stride²]; nur wenn alle 4 Nachbarn > 0 sonst nearest-neighbor. */
private static float bilerpPos(float[] arr, float fx, float fz, int stride) {
int x0 = (int) fx, z0 = (int) fz;
int x1 = Math.min(x0 + 1, stride - 1), z1 = Math.min(z0 + 1, stride - 1);
float v00 = arr[z0 * stride + x0], v10 = arr[z0 * stride + x1];
float v01 = arr[z1 * stride + x0], v11 = arr[z1 * stride + x1];
if (v00 <= 0f || v10 <= 0f || v01 <= 0f || v11 <= 0f) { return v00; }
float tx = fx - x0, tz = fz - z0;
return (v00*(1-tx) + v10*tx)*(1-tz) + (v01*(1-tx) + v11*tx)*tz;
}
/** Bilinear auf float[stride²]; nur wenn alle 4 Nachbarn nicht NaN und gleiches Vorzeichen sonst nearest-neighbor. */
private static float bilerpNaN(float[] arr, float fx, float fz, int stride) {
int x0 = (int) fx, z0 = (int) fz;
int x1 = Math.min(x0 + 1, stride - 1), z1 = Math.min(z0 + 1, stride - 1);
float v00 = arr[z0 * stride + x0], v10 = arr[z0 * stride + x1];
float v01 = arr[z1 * stride + x0], v11 = arr[z1 * stride + x1];
if (Float.isNaN(v00) || Float.isNaN(v10) || Float.isNaN(v01) || Float.isNaN(v11)) { return v00; }
// Kein Bilinear über Vorzeichen-Wechsel (Unterwasser↔Oberfläche) → nearest-neighbor
boolean allPos = v00 > 0 && v10 > 0 && v01 > 0 && v11 > 0;
boolean allNeg = v00 < 0 && v10 < 0 && v01 < 0 && v11 < 0;
if (!allPos && !allNeg) { return v00; }
float tx = fx - x0, tz = fz - z0;
return (v00*(1-tx) + v10*tx)*(1-tz) + (v01*(1-tx) + v11*tx)*tz;
}
private static float bilerp(byte[] arr, int i00, int i10, int i01, int i11, float tx, float tz) { private static float bilerp(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 v00 = (arr[i00] & 0xFF) / 255f, v10 = (arr[i10] & 0xFF) / 255f;
float v01 = (arr[i01] & 0xFF) / 255f, v11 = (arr[i11] & 0xFF) / 255f; float v01 = (arr[i01] & 0xFF) / 255f, v11 = (arr[i11] & 0xFF) / 255f;

View File

@@ -81,6 +81,7 @@ public class EditorApp extends Application {
private final java.util.concurrent.ConcurrentLinkedQueue<String> consoleBuffer = private final java.util.concurrent.ConcurrentLinkedQueue<String> consoleBuffer =
new java.util.concurrent.ConcurrentLinkedQueue<>(); new java.util.concurrent.ConcurrentLinkedQueue<>();
private VBox assetPanel; private VBox assetPanel;
private Tab weltkartTab;
private MapObjectsView mapObjectsView; private MapObjectsView mapObjectsView;
private de.blight.editor.ui.WorldMapView worldMapView; private de.blight.editor.ui.WorldMapView worldMapView;
private de.blight.editor.ui.ThumbnailManagerView thumbnailManagerView; private de.blight.editor.ui.ThumbnailManagerView thumbnailManagerView;
@@ -360,8 +361,8 @@ public class EditorApp extends Application {
private final java.util.Deque<File> modelImportQueue = new java.util.ArrayDeque<>(); private final java.util.Deque<File> modelImportQueue = new java.util.ArrayDeque<>();
// Asset-Overlay-Zustand // Asset-Overlay-Zustand
private boolean assetOverlayOpen = false; private boolean assetOverlayOpen = false;
private Button assetTabBtn; private Button assetTabBtn;
private StackPane centerStack; // persistenter Wrapper für alle 3D-Ansichten private StackPane centerStack; // persistenter Wrapper für alle 3D-Ansichten
// Toolbar-Buttons (müssen vom Status-Poller erreichbar sein) // Toolbar-Buttons (müssen vom Status-Poller erreichbar sein)
@@ -5653,21 +5654,59 @@ public class EditorApp extends Application {
input.pendingGotoZ = pos[2]; input.pendingGotoZ = pos[2];
}); });
final Tab weltkartTab = new Tab("Weltkarte", worldMapView); weltkartTab = new Tab("Weltkarte", worldMapView);
weltkartTab.setClosable(false); weltkartTab.setClosable(false);
weltkartTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> { weltkartTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> {
if (isSelected && !worldMapView.isLoaded()) worldMapView.loadAndRender(); if (isSelected && !worldMapView.isLoaded()) worldMapView.loadAndRender();
}); });
worldMapView.setFullscreenCallbacks( worldMapView.setFullscreenCallbacks(
() -> { () -> {
// Erst aus Tab lösen, dann in centerStack einsetzen // Placeholder ersetzt worldMapView im Tab → sauberes Detach
weltkartTab.setContent(new javafx.scene.control.Label("")); final javafx.scene.control.Label placeholder = new javafx.scene.control.Label();
setCenterView(worldMapView); weltkartTab.setContent(placeholder);
assetTabBtn.setVisible(false);
// Nach einem Pulse hat der TabPane-Skin den Swap verarbeitet
javafx.application.Platform.runLater(() -> {
log.debug("[FS-enter] parent after detach: {}", worldMapView.getParent());
worldMapView.setMaxWidth(Double.MAX_VALUE);
// KEIN explizites Alignment → null = CENTER → fillWidth=true → View füllt volle Breite
StackPane.setAlignment(worldMapView, null);
worldMapView.setTranslateX(0);
try {
centerStack.getChildren().add(worldMapView);
} catch (Exception ex) {
log.error("[FS-enter] add failed", ex);
return;
}
// Nach nächstem Layout-Pass hat centerStack die View korrekt dimensioniert
javafx.application.Platform.runLater(() -> {
double w = worldMapView.getWidth();
log.debug("[FS-enter] post-layout: w={} h={}", w, worldMapView.getHeight());
// Karte auf volle Canvas-Größe einpassen (scale/pan), dann animieren
worldMapView.fitCanvas();
worldMapView.setTranslateX(-w);
javafx.animation.TranslateTransition tt = new javafx.animation.TranslateTransition(
javafx.util.Duration.millis(300), worldMapView);
tt.setToX(0);
tt.setInterpolator(javafx.animation.Interpolator.EASE_OUT);
tt.play();
});
});
}, },
() -> { () -> {
// Erst aus centerStack lösen (worldViewport zurück), dann in Tab setzen // Zurück nach links schieben, dann in Tab zurücklegen
setCenterView(worldViewport); double w = worldMapView.getWidth();
weltkartTab.setContent(worldMapView); javafx.animation.TranslateTransition tt = new javafx.animation.TranslateTransition(
javafx.util.Duration.millis(220), worldMapView);
tt.setToX(-w);
tt.setInterpolator(javafx.animation.Interpolator.EASE_IN);
tt.setOnFinished(ev -> {
centerStack.getChildren().remove(worldMapView);
worldMapView.setTranslateX(0);
weltkartTab.setContent(worldMapView);
assetTabBtn.setVisible(true);
});
tt.play();
}); });
TabPane tabPane = new TabPane(assetsTab, karteTab, weltkartTab); TabPane tabPane = new TabPane(assetsTab, karteTab, weltkartTab);

View File

@@ -82,16 +82,17 @@ public class WorldMapView extends VBox {
private final StackPane canvasPane = new StackPane(canvas); private final StackPane canvasPane = new StackPane(canvas);
// Layer-Checkboxen (im MenuButton gebündelt) // Layer-Checkboxen (im MenuButton gebündelt)
private final CheckBox cbTerrain = layerCheck("Gelände"); private final CheckBox cbTerrain = layerCheck("Gelände");
private final CheckBox cbWater = layerCheck("Wasser"); private final CheckBox cbWater = layerCheck("Wasser");
private final CheckBox cbAreas = layerCheck("Areas"); private final CheckBox cbWaterfalls = layerCheck("Wasserfälle");
private final CheckBox cbZones = layerCheck("Zonen"); private final CheckBox cbAreas = layerCheck("Areas");
private final CheckBox cbLocations = layerCheck("Orte"); private final CheckBox cbZones = layerCheck("Zonen");
private final CheckBox cbModels = layerCheck("Modelle"); private final CheckBox cbLocations = layerCheck("Orte");
private final CheckBox cbModels = layerCheck("Modelle");
private final ToggleButton labelBtn = new ToggleButton("Label"); private final ToggleButton labelBtn = new ToggleButton("Label");
private final Button fullscreenBtn = new Button("⤢ Vollbild"); private final Button fullscreenBtn = new Button(">>");
private final Button backBtn = new Button("< Zurück"); private final Button backBtn = new Button("<<");
private boolean isFullscreen = false; private boolean isFullscreen = false;
/** Kamera-Zustand (Position + Blickrichtung) für die Karten-Überlagerung. */ /** Kamera-Zustand (Position + Blickrichtung) für die Karten-Überlagerung. */
@@ -107,6 +108,7 @@ public class WorldMapView extends VBox {
private double panX = 0, panY = 0; private double panX = 0, panY = 0;
private double scale = 1.0; private double scale = 1.0;
private double savedScale, savedPanX, savedPanY;
private double dragStartX, dragStartY, dragStartPanX, dragStartPanY; private double dragStartX, dragStartY, dragStartPanX, dragStartPanY;
private final Supplier<Stage> stageSupplier; private final Supplier<Stage> stageSupplier;
@@ -167,25 +169,31 @@ public class WorldMapView extends VBox {
// ── Layer-Auswahl als MenuButton mit Checkboxen ─────────────────────── // ── Layer-Auswahl als MenuButton mit Checkboxen ───────────────────────
cbTerrain.selectedProperty().addListener((obs, o, n) -> rerenderFromModel()); cbTerrain.selectedProperty().addListener((obs, o, n) -> rerenderFromModel());
cbWater.selectedProperty().addListener((obs, o, n) -> rerenderFromModel()); cbWater.selectedProperty().addListener((obs, o, n) -> rerenderFromModel());
cbWaterfalls.selectedProperty().addListener((obs, o, n) -> rerenderFromModel());
cbZones.selectedProperty().addListener((obs, o, n) -> rerenderFromModel()); cbZones.selectedProperty().addListener((obs, o, n) -> rerenderFromModel());
cbModels.selectedProperty().addListener((obs, o, n) -> redraw()); cbModels.selectedProperty().addListener((obs, o, n) -> redraw());
cbAreas.selectedProperty().addListener((obs, o, n) -> redraw()); cbAreas.selectedProperty().addListener((obs, o, n) -> redraw());
cbLocations.selectedProperty().addListener((obs, o, n) -> redraw()); cbLocations.selectedProperty().addListener((obs, o, n) -> redraw());
MenuButton layerMenu = new MenuButton("Layer ▾"); MenuButton layerMenu = new MenuButton("Layer ▾");
for (CheckBox cb : new CheckBox[]{cbTerrain, cbWater, cbAreas, cbZones, cbLocations, cbModels}) { for (CheckBox cb : new CheckBox[]{cbTerrain, cbWater, cbWaterfalls, cbAreas, cbZones, cbLocations, cbModels}) {
CustomMenuItem item = new CustomMenuItem(cb, false); CustomMenuItem item = new CustomMenuItem(cb, false);
item.setHideOnClick(false); item.setHideOnClick(false);
layerMenu.getItems().add(item); layerMenu.getItems().add(item);
} }
// ── Buttons ─────────────────────────────────────────────────────────── // ── Buttons ───────────────────────────────────────────────────────────
Button refreshBtn = new Button(" Modell aktualisieren"); Button refreshBtn = new Button("");
refreshBtn.setTooltip(new javafx.scene.control.Tooltip("Modell aktualisieren"));
refreshBtn.setOnAction(e -> loadAndRender()); refreshBtn.setOnAction(e -> loadAndRender());
Button exportBtn = new Button("Als PNG exportieren…"); Button exportBtn = new Button("");
exportBtn.setTooltip(new javafx.scene.control.Tooltip("Als PNG exportieren…"));
exportBtn.setOnAction(e -> exportPng()); exportBtn.setOnAction(e -> exportPng());
fullscreenBtn.setTooltip(new javafx.scene.control.Tooltip("Maximieren"));
backBtn.setTooltip(new javafx.scene.control.Tooltip("Minimieren"));
labelBtn.setStyle("-fx-font-size: 11;"); labelBtn.setStyle("-fx-font-size: 11;");
labelBtn.selectedProperty().addListener((obs, o, n) -> labelBtn.selectedProperty().addListener((obs, o, n) ->
canvas.setCursor(n ? Cursor.CROSSHAIR : Cursor.DEFAULT)); canvas.setCursor(n ? Cursor.CROSSHAIR : Cursor.DEFAULT));
@@ -293,18 +301,20 @@ public class WorldMapView extends VBox {
long t0 = System.currentTimeMillis(); long t0 = System.currentTimeMillis();
MapData mapData = MapIO.load(); MapData mapData = MapIO.load();
List<PlacedArea> areas = AreaIO.load(); List<PlacedArea> areas = AreaIO.load();
List<PlacedLocationZone> zones = LocationZoneIO.load(); List<PlacedLocationZone> zones = LocationZoneIO.load();
List<Location> locs = LocationIO.load(); List<Location> locs = LocationIO.load();
List<PlacedWater> waters = WaterBodyIO.load(); List<PlacedWater> waters = WaterBodyIO.load();
List<PlacedModel> models = PlacedModelIO.load(); List<PlacedModel> models = PlacedModelIO.load();
bakeVoxelHeights(mapData); List<PlacedWaterfall> waterfalls = WaterfallIO.load();
float[] voxelSurf = bakeVoxelHeights(mapData);
Platform.runLater(() -> statusLbl.setText("Berechne Modell…")); Platform.runLater(() -> statusLbl.setText("Berechne Modell…"));
WorldMapRenderModel model = buildRenderModel(mapData, areas, zones, locs, waters, models); WorldMapRenderModel model = buildRenderModel(
log.debug("[WorldMap] Modell fertig {} Areas, {} Orte, {} Wasser, {} Modelle ({} ms)", mapData, areas, zones, locs, waters, models, waterfalls, voxelSurf);
log.debug("[WorldMap] Modell fertig {} Areas, {} Orte, {} Wasser, {} Wasserfälle, {} Modelle ({} ms)",
model.areas().size(), model.locations().size(), model.areas().size(), model.locations().size(),
model.waters().size(), model.models().size(), model.waters().size(), model.waterfalls().size(), model.models().size(),
System.currentTimeMillis() - t0); System.currentTimeMillis() - t0);
Platform.runLater(() -> statusLbl.setText("Rendere Karte…")); Platform.runLater(() -> statusLbl.setText("Rendere Karte…"));
@@ -740,55 +750,77 @@ public class WorldMapView extends VBox {
/** /**
* Liest alle VoxelChunks (aus dem Live-Supplier oder von Disk) und schreibt die * Liest alle VoxelChunks (aus dem Live-Supplier oder von Disk) und schreibt die
* höchsten soliden Voxel-Y-Werte in mapData.upperTop. * höchsten soliden Voxel-Y-Werte in mapData.upperTop (für SeaMask/TerrainSamples)
* und gibt zusätzlich ein voxelSurface-Array zurück, das den tatsächlichen
* Oberflächen-Y pro UV-Zelle speichert — NaN wo kein Voxel, sonst der echte
* Top-Y-Wert (kann auch unter terrainHeight liegen, z. B. bei Canyons).
*/ */
private void bakeVoxelHeights(MapData mapData) { private float[] bakeVoxelHeights(MapData mapData) {
// voxelSurface bei voller Render-Auflösung (1 Zelle = 1 Welteinheit) backen,
// damit keine 8×8-Pixel-Blöcke entstehen.
int VS = WorldMapRenderer.VOXEL_SURFACE_RES;
float[] voxelSurface = new float[VS * VS];
java.util.Arrays.fill(voxelSurface, Float.NaN);
List<VoxelChunk> chunks = voxelChunkSupplier != null List<VoxelChunk> chunks = voxelChunkSupplier != null
? voxelChunkSupplier.get() ? voxelChunkSupplier.get()
: VoxelChunkIO.loadAll(); : VoxelChunkIO.loadAll();
if (chunks.isEmpty()) { return; } // Nach dem Backen sind Live-Chunks geleert → Fallback auf Disk (inkl. .blvc.prebake)
int UV = MapData.UPPER_VERTS; boolean allEmpty = chunks.stream().allMatch(VoxelChunk::isEmpty);
if (allEmpty) { chunks = VoxelChunkIO.loadAll(); }
if (chunks.isEmpty()) { return voxelSurface; }
float WH = WorldMapRenderer.WORLD_HALF; float WH = WorldMapRenderer.WORLD_HALF;
float WS = WorldMapRenderer.WORLD_SIZE; float WS = WorldMapRenderer.WORLD_SIZE;
int UV = MapData.UPPER_VERTS;
for (VoxelChunk chunk : chunks) { for (VoxelChunk chunk : chunks) {
if (chunk.isEmpty()) { continue; } if (chunk.isEmpty()) { continue; }
for (int lz = 0; lz < VoxelChunk.SIZE; lz++) { for (int lz = 0; lz < VoxelChunk.SIZE; lz++) {
float worldZ = VoxelChunk.toWorldZ(chunk.cz, lz); float worldZ = VoxelChunk.toWorldZ(chunk.cz, lz);
int vsPz = Math.round((worldZ + WH) / WS * (VS - 1));
if (vsPz < 0 || vsPz >= VS) { continue; }
int uz = Math.round((worldZ + WH) / WS * (UV - 1)); int uz = Math.round((worldZ + WH) / WS * (UV - 1));
if (uz < 0 || uz >= UV) { continue; }
for (int lx = 0; lx < VoxelChunk.SIZE; lx++) { for (int lx = 0; lx < VoxelChunk.SIZE; lx++) {
float worldX = VoxelChunk.toWorldX(chunk.cx, lx); float worldX = VoxelChunk.toWorldX(chunk.cx, lx);
int vsPx = Math.round((worldX + WH) / WS * (VS - 1));
if (vsPx < 0 || vsPx >= VS) { continue; }
int ux = Math.round((worldX + WH) / WS * (UV - 1)); int ux = Math.round((worldX + WH) / WS * (UV - 1));
if (ux < 0 || ux >= UV) { continue; }
// Oberste solide Voxel-Y in dieser Spalte suchen // Oberste solide Voxel-Y in dieser Spalte suchen
for (int ly = VoxelChunk.SIZE - 1; ly >= 0; ly--) { for (int ly = VoxelChunk.SIZE - 1; ly >= 0; ly--) {
if (chunk.getDensity(lx, ly, lz) > 0) { if (chunk.getDensity(lx, ly, lz) > 0) {
float topY = VoxelChunk.toWorldY(chunk.cy, ly); float topY = VoxelChunk.toWorldY(chunk.cy, ly);
int idx = uz * UV + ux; int vsIdx = vsPz * VS + vsPx;
if (topY > mapData.upperTop[idx]) { mapData.upperTop[idx] = topY; } if (Float.isNaN(voxelSurface[vsIdx]) || topY > voxelSurface[vsIdx]) {
voxelSurface[vsIdx] = topY;
}
// upperTop auf UV-Raster aktualisieren (für SeaMask / renderRegion)
if (uz >= 0 && uz < UV && ux >= 0 && ux < UV) {
int uvIdx = uz * UV + ux;
if (topY > mapData.upperTop[uvIdx]) { mapData.upperTop[uvIdx] = topY; }
}
break; break;
} }
} }
} }
} }
} }
return voxelSurface;
} }
private WorldMapRenderModel buildRenderModel( private WorldMapRenderModel buildRenderModel(
MapData mapData, MapData mapData,
List<PlacedArea> areas, List<PlacedLocationZone> zones, List<PlacedArea> areas, List<PlacedLocationZone> zones,
List<Location> locs, List<PlacedWater> waters, List<Location> locs, List<PlacedWater> waters,
List<PlacedModel> models) { List<PlacedModel> models, List<PlacedWaterfall> waterfalls,
float[] voxelSurface) {
int[] slotColors = computeSlotColors(mapData); int[] slotColors = computeSlotColors(mapData);
boolean[] mask = WorldMapRenderer.buildSeaMask(mapData, SEA_MASK_SIZE); boolean[] mask = WorldMapRenderer.buildSeaMask(mapData, SEA_MASK_SIZE, voxelSurface);
float[] tSamp = buildTerrainSamples(mapData, SEA_MASK_SIZE); float[] tSamp = buildTerrainSamples(mapData, SEA_MASK_SIZE);
List<float[][]> paths = buildSeaCoastPaths(buildCoastSegsMS(mapData, waters, SEA_MASK_SIZE)); List<float[][]> paths = buildSeaCoastPaths(buildCoastSegsMS(mapData, waters, SEA_MASK_SIZE, voxelSurface));
List<PlacedModel> treeList = models.stream() List<PlacedModel> treeList = models.stream()
.filter(WorldMapRenderer::isTree).collect(Collectors.toList()); .filter(WorldMapRenderer::isTree).collect(Collectors.toList());
List<List<PlacedModel>> tClusters = clusterTreeModels(treeList, 40f); List<List<PlacedModel>> tClusters = clusterTreeModels(treeList, 40f);
return new WorldMapRenderModel( return new WorldMapRenderModel(
mapData, areas, zones, locs, waters, models, slotColors, mapData, areas, zones, locs, waters, models, waterfalls, slotColors,
mask, tSamp, paths, tClusters); mask, voxelSurface, tSamp, paths, tClusters);
} }
private static float[] buildTerrainSamples(MapData mapData, int size) { private static float[] buildTerrainSamples(MapData mapData, int size) {
@@ -812,19 +844,27 @@ public class WorldMapView extends VBox {
// Erzeugt interpolierte Marching-Squares-Segmente für Meer + Wasserflächen. // Erzeugt interpolierte Marching-Squares-Segmente für Meer + Wasserflächen.
// Endpunkte liegen am echten Höhen-Nulldurchgang → keine Treppenstufen auf Diagonalen. // Endpunkte liegen am echten Höhen-Nulldurchgang → keine Treppenstufen auf Diagonalen.
private static float[][] buildCoastSegsMS(MapData mapData, List<PlacedWater> waters, int size) { private static float[][] buildCoastSegsMS(MapData mapData, List<PlacedWater> waters, int size,
float[] voxelSurface) {
int TV = MapData.TERRAIN_VERTS; int TV = MapData.TERRAIN_VERTS;
int UV = MapData.UPPER_VERTS; int VS = WorldMapRenderer.VOXEL_SURFACE_RES;
float[] h = new float[size * size]; float[] h = new float[size * size];
for (int mz = 0; mz < size; mz++) { for (int mz = 0; mz < size; mz++) {
for (int mx = 0; mx < size; mx++) { for (int mx = 0; mx < size; mx++) {
int hx = Math.min((int)(mx / (double)(size-1) * (TV-1)), TV-1); 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 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); float base = mapData.terrainHeight[hz * TV + hx];
int uz = Math.min((int)(mz / (double)(size-1) * (UV-1)), UV-1); // Voxel-Oberfläche hochauflösend abgreifen (falls vorhanden)
float base = mapData.terrainHeight[hz * TV + hx]; float vs = Float.NaN;
float upper = mapData.upperTop[uz * UV + ux]; if (voxelSurface != null) {
h[mz * size + mx] = (upper > 0f && upper > base) ? upper : base; int vsPx = Math.min((int)(mx / (double)(size-1) * (VS-1)), VS-1);
int vsPz = Math.min((int)(mz / (double)(size-1) * (VS-1)), VS-1);
vs = voxelSurface[vsPz * VS + vsPx];
}
// Kein upperTop-Fallback: bakeVoxelHeights kontaminiert upperTop auf
// UV-Auflösung (8 Welteinheiten/Zelle) → würde Küste verfälschen.
// Übereinstimmung mit seaPixels-Logik im Renderer.
h[mz * size + mx] = Float.isNaN(vs) ? base : Math.max(base, vs);
} }
} }
List<float[]> segs = new ArrayList<>(); List<float[]> segs = new ArrayList<>();
@@ -1092,11 +1132,12 @@ public class WorldMapView extends VBox {
cbTerrain.isSelected(), cbTerrain.isSelected(),
cbTerrain.isSelected(), cbTerrain.isSelected(),
cbWater.isSelected(), cbWater.isSelected(),
false, // Wellen im Editor über Canvas gezeichnet false, // Wellen im Editor über Canvas gezeichnet
false, false,
cbZones.isSelected(), cbZones.isSelected(),
false, false,
cbModels.isSelected() cbModels.isSelected(),
cbWaterfalls.isSelected()
); );
} }
@@ -1106,11 +1147,12 @@ public class WorldMapView extends VBox {
cbTerrain.isSelected(), cbTerrain.isSelected(),
cbTerrain.isSelected(), cbTerrain.isSelected(),
cbWater.isSelected(), cbWater.isSelected(),
true, // Wellen im Export-PNG gebacken true, // Wellen im Export-PNG gebacken
cbAreas.isSelected(), cbAreas.isSelected(),
cbZones.isSelected(), cbZones.isSelected(),
cbLocations.isSelected(), cbLocations.isSelected(),
cbModels.isSelected() cbModels.isSelected(),
cbWaterfalls.isSelected()
); );
} }
@@ -1122,8 +1164,22 @@ public class WorldMapView extends VBox {
// ── Vollbild ────────────────────────────────────────────────────────────── // ── Vollbild ──────────────────────────────────────────────────────────────
/** Passt Scale+Pan an die aktuelle Canvas-Größe an (zentriert, ganz sichtbar). */
public void fitCanvas() {
double cw = canvasPane.getWidth();
double ch = canvasPane.getHeight();
if (cw <= 0 || ch <= 0 || mapFxImage == null) return;
scale = Math.min(cw / RENDER_SIZE, ch / RENDER_SIZE);
panX = (cw - RENDER_SIZE * scale) / 2.0;
panY = (ch - RENDER_SIZE * scale) / 2.0;
redraw();
}
private void enterFullscreen() { private void enterFullscreen() {
isFullscreen = true; isFullscreen = true;
savedScale = scale;
savedPanX = panX;
savedPanY = panY;
fullscreenBtn.setVisible(false); fullscreenBtn.setVisible(false);
backBtn.setVisible(true); backBtn.setVisible(true);
if (enterFullscreenCallback != null) { if (enterFullscreenCallback != null) {
@@ -1133,6 +1189,9 @@ public class WorldMapView extends VBox {
private void exitFullscreen() { private void exitFullscreen() {
isFullscreen = false; isFullscreen = false;
scale = savedScale;
panX = savedPanX;
panY = savedPanY;
backBtn.setVisible(false); backBtn.setVisible(false);
fullscreenBtn.setVisible(true); fullscreenBtn.setVisible(true);
if (exitFullscreenCallback != null) { if (exitFullscreenCallback != null) {

View File

@@ -19,6 +19,8 @@
<!-- Karten-Rendering: Modell-Build und PNG-Render auf DEBUG --> <!-- Karten-Rendering: Modell-Build und PNG-Render auf DEBUG -->
<logger name="de.blight.editor.ui.WorldMapView" level="DEBUG"/> <logger name="de.blight.editor.ui.WorldMapView" level="DEBUG"/>
<!-- Weltkarte-Vollbild: Layout-Debugging -->
<logger name="de.blight.editor.EditorApp" level="DEBUG"/>
<!-- JME-interne JUL-Logs auf WARN reduzieren --> <!-- JME-interne JUL-Logs auf WARN reduzieren -->
<logger name="com.jme3" level="WARN"/> <logger name="com.jme3" level="WARN"/>

Binary file not shown.

Binary file not shown.

BIN
doc/Lore.odt Normal file

Binary file not shown.