Arbeiten an der Karte - neuer Render Modus mit Zwischenschicht, Küstenlinien, Wasserfälle
This commit is contained in:
BIN
assets/imported/models/3D-Modell eines schwarzen Wolfs.glb
Normal file
BIN
assets/imported/models/3D-Modell eines schwarzen Wolfs.glb
Normal file
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
@@ -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
|
||||||
BIN
blight-assets/src/main/resources/Models/imported/wolf.j3o
Normal file
BIN
blight-assets/src/main/resources/Models/imported/wolf.j3o
Normal file
Binary file not shown.
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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"/>
|
||||||
|
|||||||
BIN
blight-map/src/main/map/chunks/voxel_17_0_08.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_17_0_08.blvc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_17_m1_08.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_17_m1_08.blvc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
doc/Lore.odt
Normal file
BIN
doc/Lore.odt
Normal file
Binary file not shown.
Reference in New Issue
Block a user