Weltkarte: Render-Modell-Trennung, UI-Umbau und Kamera-Indikator
- WorldMapRenderModel: neues Record als Zwischenschicht zwischen I/O und Rendering - WorldMapRenderer: render() akzeptiert Modell direkt; rückwärtskompatible Wrapper-Signatur für MinimapState - WorldMapView: Layer-Auswahl als MenuButton mit Checkboxen; Vollbild-Modus (centerStack-Swap); Kamera-Indikator mit FOV-Kegel (120ms-Timer, unabhängig vom Karten-Rendering); Linksklick teleportiert JME3-Kamera unter Beibehaltung der Terrain-Höhendifferenz - EditorApp: CameraInfoSupplier, TeleportCallback und FullscreenCallbacks verdrahtet - MinimapState, Logback: Debug-Logging für Modell-Build und Render-Zyklen - Map-Daten: Zwischenstand Weltdaten Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 73 KiB |
@@ -0,0 +1,27 @@
|
|||||||
|
package de.blight.common.map;
|
||||||
|
|
||||||
|
import de.blight.common.*;
|
||||||
|
import de.blight.common.model.Location;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fertig vorberechnetes Karten-Modell: enthält alle Rohdaten + die im Editor
|
||||||
|
* vorcompilierten Overlay-Informationen (SeaMask, Küstenpfade, Baum-Cluster).
|
||||||
|
* WorldMapRenderer.render() arbeitet nur noch gegen dieses Modell.
|
||||||
|
*/
|
||||||
|
public record WorldMapRenderModel(
|
||||||
|
// Terrain-Daten + Polygon-Objekte (werden vom Renderer für das PNG genutzt)
|
||||||
|
MapData mapData,
|
||||||
|
List<PlacedArea> areas,
|
||||||
|
List<PlacedLocationZone> zones,
|
||||||
|
List<Location> locations,
|
||||||
|
List<PlacedWater> waters,
|
||||||
|
List<PlacedModel> models,
|
||||||
|
int[] slotColorsRGB,
|
||||||
|
// Vorberechnete Overlay-Daten (werden vom Canvas genutzt, nicht vom PNG-Renderer)
|
||||||
|
boolean[] seaMask, // Wasser-Pixel-Maske bei SEA_MASK_SIZE-Auflösung
|
||||||
|
float[] terrainSamples, // Höhenwerte bei SEA_MASK_SIZE (für Wellen-Prüfung)
|
||||||
|
List<float[][]> coastPaths, // geglättete Marching-Squares Küstenpfade (Weltkoord.)
|
||||||
|
List<List<PlacedModel>> treeClusters
|
||||||
|
) {}
|
||||||
@@ -62,17 +62,25 @@ public final class WorldMapRenderer {
|
|||||||
|
|
||||||
public static boolean[] buildSeaMask(MapData m, int size) {
|
public static boolean[] buildSeaMask(MapData m, int size) {
|
||||||
int TV = MapData.TERRAIN_VERTS;
|
int TV = MapData.TERRAIN_VERTS;
|
||||||
|
int UV = MapData.UPPER_VERTS;
|
||||||
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);
|
||||||
mask[py * size + px] = m.terrainHeight[hz * TV + hx] < 0f;
|
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 upper = m.upperTop[uz * UV + ux];
|
||||||
|
if (upper > 0f && upper > h) { h = upper; }
|
||||||
|
mask[py * size + px] = h < 0f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return mask;
|
return mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final int WATER_COLOR = 0xFF_ADD8E6;
|
||||||
|
|
||||||
// Default-Slot-Farben für Slots 1-8 (Base-Layer 1-4 + Upper-Layer 5-8)
|
// Default-Slot-Farben für Slots 1-8 (Base-Layer 1-4 + Upper-Layer 5-8)
|
||||||
private static final int[] DEF_SLOT_R = { 71, 115, 140, 204, 90, 130, 110, 180 };
|
private static final int[] DEF_SLOT_R = { 71, 115, 140, 204, 90, 130, 110, 180 };
|
||||||
private static final int[] DEF_SLOT_G = { 148, 82, 115, 184, 80, 90, 60, 100 };
|
private static final int[] DEF_SLOT_G = { 148, 82, 115, 184, 80, 90, 60, 100 };
|
||||||
@@ -80,15 +88,17 @@ public final class WorldMapRenderer {
|
|||||||
|
|
||||||
private WorldMapRenderer() {}
|
private WorldMapRenderer() {}
|
||||||
|
|
||||||
public static BufferedImage render(RenderInput input, int targetSize, RenderOptions opts) {
|
/** Hauptmethode: rendert das Hintergrund-PNG aus einem vorberechneten Modell. */
|
||||||
MapData m = input.mapData();
|
public static BufferedImage render(WorldMapRenderModel model, int targetSize, RenderOptions opts) {
|
||||||
|
MapData m = model.mapData();
|
||||||
int TV = MapData.TERRAIN_VERTS;
|
int TV = MapData.TERRAIN_VERTS;
|
||||||
int SS = MapData.SPLAT_SIZE;
|
int SS = MapData.SPLAT_SIZE;
|
||||||
int[] slotR = slotChannel(input, 0);
|
int[] slotR = slotChannel(model.slotColorsRGB(), 0);
|
||||||
int[] slotG = slotChannel(input, 1);
|
int[] slotG = slotChannel(model.slotColorsRGB(), 1);
|
||||||
int[] slotB = slotChannel(input, 2);
|
int[] slotB = slotChannel(model.slotColorsRGB(), 2);
|
||||||
|
|
||||||
// ── 1. Heightmap auf Zielauflösung samplen ────────────────────────────
|
// ── 1. Heightmap auf Zielauflösung samplen ────────────────────────────
|
||||||
|
int UV = MapData.UPPER_VERTS;
|
||||||
float[] heights = new float[targetSize * targetSize];
|
float[] heights = new float[targetSize * targetSize];
|
||||||
float minH = Float.MAX_VALUE, maxH = -Float.MAX_VALUE;
|
float minH = Float.MAX_VALUE, maxH = -Float.MAX_VALUE;
|
||||||
|
|
||||||
@@ -96,7 +106,11 @@ public final class WorldMapRenderer {
|
|||||||
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);
|
||||||
|
int uz = Math.min((int)((float) py / (targetSize - 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 (upper > 0f && upper > h) { h = upper; }
|
||||||
heights[py * targetSize + px] = h;
|
heights[py * targetSize + px] = h;
|
||||||
if (h < minH) minH = h;
|
if (h < minH) minH = h;
|
||||||
if (h > maxH) maxH = h;
|
if (h > maxH) maxH = h;
|
||||||
@@ -161,6 +175,9 @@ public final class WorldMapRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kuwahara-Filter: lässt das Terrain wie gemalt wirken
|
||||||
|
if (opts.showSplatColors()) { applyKuwahara(img, 3); }
|
||||||
|
|
||||||
// ── 3. Vektor-Overlays ────────────────────────────────────────────────
|
// ── 3. Vektor-Overlays ────────────────────────────────────────────────
|
||||||
Graphics2D gfx = img.createGraphics();
|
Graphics2D gfx = img.createGraphics();
|
||||||
gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
@@ -171,10 +188,10 @@ 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, 0xFFFFFFFF);
|
if (heights[py * targetSize + px] < 0f) img.setRGB(px, py, WATER_COLOR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (PlacedWater w : input.waters()) {
|
for (PlacedWater w : model.waters()) {
|
||||||
int[] xs = worldToPixels(w.pointsX(), targetSize);
|
int[] xs = worldToPixels(w.pointsX(), targetSize);
|
||||||
int[] ys = worldToPixels(w.pointsZ(), targetSize);
|
int[] ys = worldToPixels(w.pointsZ(), targetSize);
|
||||||
Polygon poly = new Polygon(xs, ys, xs.length);
|
Polygon poly = new Polygon(xs, ys, xs.length);
|
||||||
@@ -186,7 +203,7 @@ public final class WorldMapRenderer {
|
|||||||
for (int py = y0; py <= y1; py++) {
|
for (int py = y0; py <= y1; py++) {
|
||||||
for (int px = x0; px <= x1; px++) {
|
for (int px = x0; px <= x1; px++) {
|
||||||
if (poly.contains(px, py) && heights[py * targetSize + px] < wh)
|
if (poly.contains(px, py) && heights[py * targetSize + px] < wh)
|
||||||
img.setRGB(px, py, 0xFFFFFFFF);
|
img.setRGB(px, py, WATER_COLOR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,7 +236,7 @@ public final class WorldMapRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Wasserflächen: nur wenn groß genug
|
// Wasserflächen: nur wenn groß genug
|
||||||
for (PlacedWater w : input.waters()) {
|
for (PlacedWater w : model.waters()) {
|
||||||
int[] xs = worldToPixels(w.pointsX(), targetSize);
|
int[] xs = worldToPixels(w.pointsX(), targetSize);
|
||||||
int[] ys = worldToPixels(w.pointsZ(), targetSize);
|
int[] ys = worldToPixels(w.pointsZ(), targetSize);
|
||||||
Polygon poly = new Polygon(xs, ys, xs.length);
|
Polygon poly = new Polygon(xs, ys, xs.length);
|
||||||
@@ -250,7 +267,7 @@ public final class WorldMapRenderer {
|
|||||||
gfx.setColor(Color.BLACK);
|
gfx.setColor(Color.BLACK);
|
||||||
gfx.setStroke(new BasicStroke(3.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
gfx.setStroke(new BasicStroke(3.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
||||||
// Wasserflächen: Polygon-Umriss
|
// Wasserflächen: Polygon-Umriss
|
||||||
for (PlacedWater w : input.waters()) {
|
for (PlacedWater w : model.waters()) {
|
||||||
int[] xs = worldToPixels(w.pointsX(), targetSize);
|
int[] xs = worldToPixels(w.pointsX(), targetSize);
|
||||||
int[] ys = worldToPixels(w.pointsZ(), targetSize);
|
int[] ys = worldToPixels(w.pointsZ(), targetSize);
|
||||||
gfx.drawPolygon(xs, ys, xs.length);
|
gfx.drawPolygon(xs, ys, xs.length);
|
||||||
@@ -293,7 +310,7 @@ public final class WorldMapRenderer {
|
|||||||
10f, new float[]{dash, gap}, 0f);
|
10f, new float[]{dash, gap}, 0f);
|
||||||
int aFontSize = Math.max(8, targetSize / 160);
|
int aFontSize = Math.max(8, targetSize / 160);
|
||||||
gfx.setFont(new Font("SansSerif", Font.BOLD, aFontSize));
|
gfx.setFont(new Font("SansSerif", Font.BOLD, aFontSize));
|
||||||
for (PlacedArea a : input.areas()) {
|
for (PlacedArea a : model.areas()) {
|
||||||
int[] xs = worldToPixels(a.pointsX(), targetSize);
|
int[] xs = worldToPixels(a.pointsX(), targetSize);
|
||||||
int[] ys = worldToPixels(a.pointsZ(), targetSize);
|
int[] ys = worldToPixels(a.pointsZ(), targetSize);
|
||||||
gfx.setStroke(dashed);
|
gfx.setStroke(dashed);
|
||||||
@@ -314,7 +331,7 @@ public final class WorldMapRenderer {
|
|||||||
// Location-Zonen
|
// Location-Zonen
|
||||||
if (opts.showZones()) {
|
if (opts.showZones()) {
|
||||||
gfx.setStroke(new BasicStroke(lineW));
|
gfx.setStroke(new BasicStroke(lineW));
|
||||||
for (PlacedLocationZone z : input.zones()) {
|
for (PlacedLocationZone z : model.zones()) {
|
||||||
int[] xs = worldToPixels(z.pointsX(), targetSize);
|
int[] xs = worldToPixels(z.pointsX(), targetSize);
|
||||||
int[] ys = worldToPixels(z.pointsZ(), targetSize);
|
int[] ys = worldToPixels(z.pointsZ(), targetSize);
|
||||||
gfx.setColor(new Color(240, 190, 40, 70));
|
gfx.setColor(new Color(240, 190, 40, 70));
|
||||||
@@ -324,13 +341,13 @@ public final class WorldMapRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modell-Punkte
|
|
||||||
if (opts.showModels()) {
|
if (opts.showModels()) {
|
||||||
int dotR = Math.max(1, targetSize / 600);
|
int dotR = Math.max(1, targetSize / 600);
|
||||||
gfx.setColor(new Color(160, 80, 20, 200));
|
gfx.setColor(new Color(160, 80, 20, 200));
|
||||||
for (PlacedModel model : input.models()) {
|
for (PlacedModel pm : model.models()) {
|
||||||
int mx = worldToPixel(model.x(), targetSize);
|
if (isTree(pm)) { continue; }
|
||||||
int mz = worldToPixel(model.z(), targetSize);
|
int mx = worldToPixel(pm.x(), targetSize);
|
||||||
|
int mz = worldToPixel(pm.z(), targetSize);
|
||||||
gfx.fillRect(mx - dotR, mz - dotR, dotR * 2 + 1, dotR * 2 + 1);
|
gfx.fillRect(mx - dotR, mz - dotR, dotR * 2 + 1, dotR * 2 + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,7 +356,7 @@ public final class WorldMapRenderer {
|
|||||||
if (opts.showLocations()) {
|
if (opts.showLocations()) {
|
||||||
int fontSize = Math.max(8, targetSize / 140);
|
int fontSize = Math.max(8, targetSize / 140);
|
||||||
gfx.setFont(new Font("SansSerif", Font.BOLD, fontSize));
|
gfx.setFont(new Font("SansSerif", Font.BOLD, fontSize));
|
||||||
for (Location loc : input.locations()) {
|
for (Location loc : model.locations()) {
|
||||||
if (!loc.isShowOnMap()) continue;
|
if (!loc.isShowOnMap()) continue;
|
||||||
if (loc.getId() == null || loc.getId().isEmpty()) continue;
|
if (loc.getId() == null || loc.getId().isEmpty()) continue;
|
||||||
float wx = Float.isNaN(loc.getLabelX()) ? loc.getCenterX() : loc.getLabelX();
|
float wx = Float.isNaN(loc.getLabelX()) ? loc.getCenterX() : loc.getLabelX();
|
||||||
@@ -354,6 +371,15 @@ public final class WorldMapRenderer {
|
|||||||
return img;
|
return img;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Rückwärts-kompatibel: baut ein minimales Modell (ohne Overlay-Daten) und delegiert. */
|
||||||
|
public static BufferedImage render(RenderInput input, int targetSize, RenderOptions opts) {
|
||||||
|
return render(new WorldMapRenderModel(
|
||||||
|
input.mapData(), input.areas(), input.zones(), input.locations(),
|
||||||
|
input.waters(), input.models(), input.slotColorsRGB(),
|
||||||
|
null, null, null, null
|
||||||
|
), targetSize, opts);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rendert einen rechteckigen Weltausschnitt als {@link BufferedImage}.
|
* Rendert einen rechteckigen Weltausschnitt als {@link BufferedImage}.
|
||||||
* Koordinatenursprung und Skalierung passen sich dem Ausschnitt an,
|
* Koordinatenursprung und Skalierung passen sich dem Ausschnitt an,
|
||||||
@@ -383,16 +409,21 @@ public final class WorldMapRenderer {
|
|||||||
float minH = 0f, maxH = 1f;
|
float minH = 0f, maxH = 1f;
|
||||||
|
|
||||||
if (opts.showTerrain() || opts.showWater()) {
|
if (opts.showTerrain() || opts.showWater()) {
|
||||||
|
int UV2 = MapData.UPPER_VERTS;
|
||||||
heights = new float[targetSize * targetSize];
|
heights = new float[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++) {
|
||||||
float wz = wz0 + (float) py / (targetSize - 1) * rSize;
|
float wz = wz0 + (float) py / (targetSize - 1) * rSize;
|
||||||
int hz = iclamp((int) ((wz + WORLD_HALF) / WORLD_SIZE * (TV - 1)), 0, TV - 1);
|
int hz = iclamp((int) ((wz + WORLD_HALF) / WORLD_SIZE * (TV - 1)), 0, TV - 1);
|
||||||
|
int uz = iclamp((int) ((wz + WORLD_HALF) / WORLD_SIZE * (UV2 - 1)), 0, UV2 - 1);
|
||||||
for (int px = 0; px < targetSize; px++) {
|
for (int px = 0; px < targetSize; px++) {
|
||||||
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);
|
||||||
float h = m.terrainHeight[hz * TV + hx];
|
float h = m.terrainHeight[hz * TV + hx];
|
||||||
|
float upper = m.upperTop[uz * UV2 + ux];
|
||||||
|
if (upper > 0f && upper > h) { h = upper; }
|
||||||
heights[py * targetSize + px] = h;
|
heights[py * targetSize + px] = h;
|
||||||
if (h < minH) { minH = h; }
|
if (h < minH) { minH = h; }
|
||||||
if (h > maxH) { maxH = h; }
|
if (h > maxH) { maxH = h; }
|
||||||
@@ -403,9 +434,9 @@ public final class WorldMapRenderer {
|
|||||||
// ── Terrain (optional) ────────────────────────────────────────────────
|
// ── Terrain (optional) ────────────────────────────────────────────────
|
||||||
if (opts.showTerrain()) {
|
if (opts.showTerrain()) {
|
||||||
float heightRange = Math.max(0.01f, maxH - minH);
|
float heightRange = Math.max(0.01f, maxH - minH);
|
||||||
int[] sR = slotChannel(input, 0);
|
int[] sR = slotChannel(input.slotColorsRGB(), 0);
|
||||||
int[] sG = slotChannel(input, 1);
|
int[] sG = slotChannel(input.slotColorsRGB(), 1);
|
||||||
int[] sB = slotChannel(input, 2);
|
int[] sB = slotChannel(input.slotColorsRGB(), 2);
|
||||||
|
|
||||||
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++) {
|
||||||
@@ -465,6 +496,8 @@ public final class WorldMapRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts.showSplatColors()) { applyKuwahara(img, 3); }
|
||||||
|
|
||||||
// ── Vektor-Overlays ───────────────────────────────────────────────────
|
// ── Vektor-Overlays ───────────────────────────────────────────────────
|
||||||
Graphics2D gfx = img.createGraphics();
|
Graphics2D gfx = img.createGraphics();
|
||||||
gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
@@ -474,7 +507,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, 0xFFFFFFFF);
|
if (heights[py * targetSize + px] < 0f) img.setRGB(px, py, WATER_COLOR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (PlacedWater w : input.waters()) {
|
for (PlacedWater w : input.waters()) {
|
||||||
@@ -489,7 +522,7 @@ public final class WorldMapRenderer {
|
|||||||
for (int py = y0; py <= y1; py++) {
|
for (int py = y0; py <= y1; py++) {
|
||||||
for (int px = x0; px <= x1; px++) {
|
for (int px = x0; px <= x1; px++) {
|
||||||
if (poly.contains(px, py) && heights[py * targetSize + px] < wh)
|
if (poly.contains(px, py) && heights[py * targetSize + px] < wh)
|
||||||
img.setRGB(px, py, 0xFFFFFFFF);
|
img.setRGB(px, py, WATER_COLOR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -629,6 +662,7 @@ public final class WorldMapRenderer {
|
|||||||
int dotR = Math.max(1, targetSize / 600);
|
int dotR = Math.max(1, targetSize / 600);
|
||||||
gfx.setColor(new Color(160, 80, 20, 200));
|
gfx.setColor(new Color(160, 80, 20, 200));
|
||||||
for (PlacedModel model : input.models()) {
|
for (PlacedModel model : input.models()) {
|
||||||
|
if (isTree(model)) { continue; }
|
||||||
int mx = wrp1(model.x(), wx0, rSize, targetSize);
|
int mx = wrp1(model.x(), wx0, rSize, targetSize);
|
||||||
int mz = wrp1(model.z(), wz0, rSize, targetSize);
|
int mz = wrp1(model.z(), wz0, rSize, targetSize);
|
||||||
gfx.fillRect(mx - dotR, mz - dotR, dotR * 2 + 1, dotR * 2 + 1);
|
gfx.fillRect(mx - dotR, mz - dotR, dotR * 2 + 1, dotR * 2 + 1);
|
||||||
@@ -680,13 +714,12 @@ public final class WorldMapRenderer {
|
|||||||
|
|
||||||
// Gibt den R-, G- oder B-Kanal (channel=0/1/2) aller 8 Splatmap-Slots zurück.
|
// Gibt den R-, G- oder B-Kanal (channel=0/1/2) aller 8 Splatmap-Slots zurück.
|
||||||
// slotColorsRGB: 24 Werte (8 Slots × 3), 12 Werte (4 Slots, Upper-Layer = Defaults) oder null.
|
// slotColorsRGB: 24 Werte (8 Slots × 3), 12 Werte (4 Slots, Upper-Layer = Defaults) oder null.
|
||||||
private static int[] slotChannel(RenderInput input, int channel) {
|
private static int[] slotChannel(int[] slotColorsRGB, int channel) {
|
||||||
int[] rgb = input.slotColorsRGB();
|
|
||||||
int[] def = channel == 0 ? DEF_SLOT_R : (channel == 1 ? DEF_SLOT_G : DEF_SLOT_B);
|
int[] def = channel == 0 ? DEF_SLOT_R : (channel == 1 ? DEF_SLOT_G : DEF_SLOT_B);
|
||||||
if (rgb == null || rgb.length < 12) { return def; }
|
if (slotColorsRGB == null || slotColorsRGB.length < 12) { return def; }
|
||||||
int[] out = new int[8];
|
int[] out = new int[8];
|
||||||
for (int s = 0; s < 8; s++) {
|
for (int s = 0; s < 8; s++) {
|
||||||
out[s] = (rgb.length >= (s + 1) * 3) ? rgb[s * 3 + channel] : def[s];
|
out[s] = (slotColorsRGB.length >= (s + 1) * 3) ? slotColorsRGB[s * 3 + channel] : def[s];
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -802,4 +835,53 @@ public final class WorldMapRenderer {
|
|||||||
float v01 = (arr[i01] & 0xFF) / 255f, v11 = (arr[i11] & 0xFF) / 255f;
|
float v01 = (arr[i01] & 0xFF) / 255f, v11 = (arr[i11] & 0xFF) / 255f;
|
||||||
return (v00*(1-tx) + v10*tx)*(1-tz) + (v01*(1-tx) + v11*tx)*tz;
|
return (v00*(1-tx) + v10*tx)*(1-tz) + (v01*(1-tx) + v11*tx)*tz;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isTree(PlacedModel m) {
|
||||||
|
return m.modelPath().replace('\\', '/').toLowerCase().contains("/trees/");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kuwahara-Filter: Für jeden Pixel das Quadranten-Fenster mit der kleinsten
|
||||||
|
* Varianz wählen und dessen Mittelwert setzen → Ölgemälde-/gemalt-Effekt.
|
||||||
|
* Radius r=3 → Fenster 7×7, 4 Quadranten je 4×4.
|
||||||
|
*/
|
||||||
|
private static void applyKuwahara(BufferedImage img, int r) {
|
||||||
|
int w = img.getWidth(), h = img.getHeight();
|
||||||
|
int[] src = img.getRGB(0, 0, w, h, null, 0, w);
|
||||||
|
int[] dst = new int[src.length];
|
||||||
|
|
||||||
|
for (int y = 0; y < h; y++) {
|
||||||
|
for (int x = 0; x < w; x++) {
|
||||||
|
float bestVar = Float.MAX_VALUE;
|
||||||
|
int bestPacked = src[y * w + x];
|
||||||
|
|
||||||
|
// 4 Quadranten: [xOff0..xOff1] × [yOff0..yOff1]
|
||||||
|
int[][] quads = {{-r,-r,0,0},{0,-r,r,0},{-r,0,0,r},{0,0,r,r}};
|
||||||
|
for (int[] q : quads) {
|
||||||
|
int x0 = Math.max(0, x+q[0]), y0 = Math.max(0, y+q[1]);
|
||||||
|
int x1 = Math.min(w-1, x+q[2]), y1 = Math.min(h-1, y+q[3]);
|
||||||
|
float sumR=0,sumG=0,sumB=0, sum2R=0,sum2G=0,sum2B=0;
|
||||||
|
int cnt = 0;
|
||||||
|
for (int qy = y0; qy <= y1; qy++) {
|
||||||
|
for (int qx = x0; qx <= x1; qx++) {
|
||||||
|
int p = src[qy * w + qx];
|
||||||
|
float pr = (p>>16)&0xFF, pg = (p>>8)&0xFF, pb = p&0xFF;
|
||||||
|
sumR+=pr; sumG+=pg; sumB+=pb;
|
||||||
|
sum2R+=pr*pr; sum2G+=pg*pg; sum2B+=pb*pb;
|
||||||
|
cnt++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
float inv = 1f / cnt;
|
||||||
|
float mR=sumR*inv, mG=sumG*inv, mB=sumB*inv;
|
||||||
|
float var = (sum2R*inv - mR*mR) + (sum2G*inv - mG*mG) + (sum2B*inv - mB*mB);
|
||||||
|
if (var < bestVar) {
|
||||||
|
bestVar = var;
|
||||||
|
bestPacked = (clamp((int)mR)<<16) | (clamp((int)mG)<<8) | clamp((int)mB);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dst[y * w + x] = bestPacked | 0xFF000000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
img.setRGB(0, 0, w, h, dst, 0, w);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5639,11 +5639,36 @@ public class EditorApp extends Application {
|
|||||||
});
|
});
|
||||||
|
|
||||||
worldMapView = new de.blight.editor.ui.WorldMapView(() -> primaryStage);
|
worldMapView = new de.blight.editor.ui.WorldMapView(() -> primaryStage);
|
||||||
Tab weltkartTab = new Tab("Weltkarte", worldMapView);
|
worldMapView.setVoxelChunkSupplier(() -> {
|
||||||
|
de.blight.editor.state.VoxelEditorState ves = jmeApp == null ? null
|
||||||
|
: jmeApp.getStateManager().getState(de.blight.editor.state.VoxelEditorState.class);
|
||||||
|
return ves != null ? ves.getChunksSnapshot() : de.blight.common.VoxelChunkIO.loadAll();
|
||||||
|
});
|
||||||
|
worldMapView.setCameraInfoSupplier(() ->
|
||||||
|
new de.blight.editor.ui.WorldMapView.CameraInfo(
|
||||||
|
input.camX, input.camY, input.camZ, input.camYaw));
|
||||||
|
worldMapView.setTeleportCallback(pos -> {
|
||||||
|
input.pendingGotoX = pos[0];
|
||||||
|
input.pendingGotoY = pos[1];
|
||||||
|
input.pendingGotoZ = pos[2];
|
||||||
|
});
|
||||||
|
|
||||||
|
final Tab 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(
|
||||||
|
() -> {
|
||||||
|
// Erst aus Tab lösen, dann in centerStack einsetzen
|
||||||
|
weltkartTab.setContent(new javafx.scene.control.Label(""));
|
||||||
|
setCenterView(worldMapView);
|
||||||
|
},
|
||||||
|
() -> {
|
||||||
|
// Erst aus centerStack lösen (worldViewport zurück), dann in Tab setzen
|
||||||
|
setCenterView(worldViewport);
|
||||||
|
weltkartTab.setContent(worldMapView);
|
||||||
|
});
|
||||||
|
|
||||||
TabPane tabPane = new TabPane(assetsTab, karteTab, weltkartTab);
|
TabPane tabPane = new TabPane(assetsTab, karteTab, weltkartTab);
|
||||||
tabPane.setStyle("-fx-background-color: #e8e8e8;");
|
tabPane.setStyle("-fx-background-color: #e8e8e8;");
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,9 @@
|
|||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
|
<!-- Karten-Rendering: Modell-Build und PNG-Render auf DEBUG -->
|
||||||
|
<logger name="de.blight.editor.ui.WorldMapView" 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"/>
|
||||||
<!-- GltfLoader meldet bei jeder Animation "only supports linear interpolation" – bekanntes JME-Verhalten, kein Fehler -->
|
<!-- GltfLoader meldet bei jeder Animation "only supports linear interpolation" – bekanntes JME-Verhalten, kein Fehler -->
|
||||||
|
|||||||
@@ -533,6 +533,8 @@ public class MinimapState extends BaseAppState {
|
|||||||
Files.createDirectories(dir);
|
Files.createDirectories(dir);
|
||||||
Path png = dir.resolve("minimap_world.png");
|
Path png = dir.resolve("minimap_world.png");
|
||||||
|
|
||||||
|
log.debug("[Minimap] Lade Weltdaten für Render-Modell…");
|
||||||
|
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();
|
||||||
@@ -541,6 +543,8 @@ public class MinimapState extends BaseAppState {
|
|||||||
List<PlacedModel> models = PlacedModelIO.load();
|
List<PlacedModel> models = PlacedModelIO.load();
|
||||||
int[] slotColors = computeSlotColors(mapData, root);
|
int[] slotColors = computeSlotColors(mapData, root);
|
||||||
renderInput = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors);
|
renderInput = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors);
|
||||||
|
log.debug("[Minimap] Render-Modell bereit – {} Areas, {} Orte, {} Wasser ({} ms)",
|
||||||
|
areas.size(), locs.size(), waters.size(), System.currentTimeMillis() - t0);
|
||||||
|
|
||||||
boolean needsRender = !Files.exists(png);
|
boolean needsRender = !Files.exists(png);
|
||||||
if (!needsRender) {
|
if (!needsRender) {
|
||||||
@@ -553,9 +557,11 @@ public class MinimapState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
if (needsRender) {
|
if (needsRender) {
|
||||||
log.info("[Minimap] Rendere Weltkarte {}×{}…", TEXTURE_SIZE, TEXTURE_SIZE);
|
log.info("[Minimap] Rendere Weltkarte {}×{}…", TEXTURE_SIZE, TEXTURE_SIZE);
|
||||||
|
long t1 = System.currentTimeMillis();
|
||||||
BufferedImage bi = WorldMapRenderer.render(renderInput, TEXTURE_SIZE, RenderOptions.all());
|
BufferedImage bi = WorldMapRenderer.render(renderInput, TEXTURE_SIZE, RenderOptions.all());
|
||||||
ImageIO.write(bi, "PNG", png.toFile());
|
ImageIO.write(bi, "PNG", png.toFile());
|
||||||
log.info("[Minimap] Weltkarte gespeichert: {}", png);
|
log.debug("[Minimap] Weltkarte gerendert und gespeichert ({} ms): {}",
|
||||||
|
System.currentTimeMillis() - t1, png);
|
||||||
} else {
|
} else {
|
||||||
log.info("[Minimap] Gecachte Weltkarte: {}", png);
|
log.info("[Minimap] Gecachte Weltkarte: {}", png);
|
||||||
}
|
}
|
||||||
@@ -645,8 +651,12 @@ public class MinimapState extends BaseAppState {
|
|||||||
final float capHU = vr / WORLD_SIZE;
|
final float capHU = vr / WORLD_SIZE;
|
||||||
|
|
||||||
Thread t = new Thread(() -> {
|
Thread t = new Thread(() -> {
|
||||||
|
log.debug("[Minimap] Vektor-Layer neu rendern – Zentrum ({}/{}), Radius {}",
|
||||||
|
Math.round(wx), Math.round(wz), Math.round(vr));
|
||||||
|
long t0 = System.currentTimeMillis();
|
||||||
RenderOptions opts = new RenderOptions(false, false, true, true, true, true, true);
|
RenderOptions opts = new RenderOptions(false, false, true, true, true, true, true);
|
||||||
BufferedImage bi = WorldMapRenderer.renderRegion(renderInput, VEC_TEX_SIZE, opts, wx, wz, vr);
|
BufferedImage bi = WorldMapRenderer.renderRegion(renderInput, VEC_TEX_SIZE, opts, wx, wz, vr);
|
||||||
|
log.debug("[Minimap] Vektor-Layer fertig ({} ms)", System.currentTimeMillis() - t0);
|
||||||
app.enqueue(() -> {
|
app.enqueue(() -> {
|
||||||
updateVectorTexture(bi);
|
updateVectorTexture(bi);
|
||||||
vecRenderU = capU;
|
vecRenderU = capU;
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
<!-- LOD-Slot-Wechsel auf DEBUG aktivieren -->
|
<!-- LOD-Slot-Wechsel auf DEBUG aktivieren -->
|
||||||
<logger name="de.blight.game.state.ModelLodControl" level="DEBUG"/>
|
<logger name="de.blight.game.state.ModelLodControl" level="DEBUG"/>
|
||||||
|
<!-- Minimap: Modell-Build und Render-Zyklen auf DEBUG -->
|
||||||
|
<logger name="de.blight.game.state.MinimapState" 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"/>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"id": "b26f42df-7897-4840-9e7f-5f35329aa0a6",
|
||||||
|
"benchType": "Simple",
|
||||||
|
"sitzX": 0.30349,
|
||||||
|
"sitzY": 2.5,
|
||||||
|
"sitzZ": -5.00573,
|
||||||
|
"sitzRotY": 1.5707964,
|
||||||
|
"sitzSet": true
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
@@ -11,3 +11,5 @@ Models/trees/palm/palm_20260816_213341.j3o 270.73062 3.06689 -913.67090 -1.45828
|
|||||||
Models/imported/bank1.j3o 236.63928 -6.31074 -888.17450 -3.22597 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BENCH 9ac9943d-0e12-4d5c-8323-0e2b92eebdec
|
Models/imported/bank1.j3o 236.63928 -6.31074 -888.17450 -3.22597 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BENCH 9ac9943d-0e12-4d5c-8323-0e2b92eebdec
|
||||||
Models/trees/willow/willow_20260823_101850.j3o 152.40488 11.49070 -888.86847 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
Models/trees/willow/willow_20260823_101850.j3o 152.40488 11.49070 -888.86847 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||||
Models/trees/willow/willow_20260823_101856.j3o 170.68971 11.48961 -873.92236 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
Models/trees/willow/willow_20260823_101856.j3o 170.68971 11.48961 -873.92236 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||||
|
Models/imported/wolf.j3o -1.25617 2.00000 -1.86424 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||||
|
Models/imported/bank1.j3o 0.30349 2.00000 -5.00573 0.00000 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BENCH b26f42df-7897-4840-9e7f-5f35329aa0a6
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user