From 2fd6a9b7373bdeefea40b4a8ef392316897502cf Mon Sep 17 00:00:00 2001 From: Mario Date: Tue, 1 Sep 2026 22:05:38 +0200 Subject: [PATCH] Weltkarte: Kachel-Overlay, Wald-Overlay, ModelCategory-System, Selektions-Fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Terrain-Textur-Overlay (Splatmap-Blend, Klippen, Wald) als Dirty-Flag-Cache - Wald wird nach Wasser gerendert, 50% Deckkraft, Schwelle 3+ Bäume - Speichern vor Weltkarten-Refresh (save-then-load via pendingAfterSave) - ModelCategory-System: Baum/Palme/Busch/Gebäude; Ordner-Kategorie im UI versteckt - Selektion nur mit Auswahl-Werkzeug (kein versehentliches Selektieren im Platzier-Modus) Co-Authored-By: Claude Sonnet 4.6 --- .../main/java/de/blight/common/ModelMeta.java | 6 +- .../java/de/blight/common/ModelMetaIO.java | 4 +- .../java/de/blight/common/PlacedModel.java | 4 +- .../java/de/blight/common/PlacedModelIO.java | 12 +- .../common/map/WorldMapRenderModel.java | 3 +- .../blight/common/map/WorldMapRenderer.java | 661 +++++++++++++----- .../main/java/de/blight/editor/EditorApp.java | 41 +- .../de/blight/editor/object/SceneObject.java | 1 + .../de/blight/editor/state/EzTreeState.java | 8 +- .../blight/editor/state/ModelImportState.java | 2 +- .../blight/editor/state/SceneObjectState.java | 13 +- .../editor/state/TreeGeneratorState.java | 9 + .../de/blight/editor/ui/WorldMapView.java | 527 ++++++++++---- .../de/blight/game/state/MinimapState.java | 9 +- 14 files changed, 937 insertions(+), 363 deletions(-) diff --git a/blight-common/src/main/java/de/blight/common/ModelMeta.java b/blight-common/src/main/java/de/blight/common/ModelMeta.java index 5870f18..64ed684 100644 --- a/blight-common/src/main/java/de/blight/common/ModelMeta.java +++ b/blight-common/src/main/java/de/blight/common/ModelMeta.java @@ -33,7 +33,8 @@ public record ModelMeta( float interactableOffsetY, float interactableOffsetZ, float interactableRotY, - String footstepSurface + String footstepSurface, + de.blight.common.ModelCategory modelCategory ) { /** Lichtquelle relativ zum Modell-Ursprung. */ public record AttachedLight( @@ -53,6 +54,7 @@ public record ModelMeta( "", "", 30f, 80f, 120f, List.of(), List.of(), de.blight.common.model.InteractableType.NONE, - 0f, 0.5f, 0f, 0f, ""); + 0f, 0.5f, 0f, 0f, "", + de.blight.common.ModelCategory.NONE); } } diff --git a/blight-common/src/main/java/de/blight/common/ModelMetaIO.java b/blight-common/src/main/java/de/blight/common/ModelMetaIO.java index dd0426b..652d35c 100644 --- a/blight-common/src/main/java/de/blight/common/ModelMetaIO.java +++ b/blight-common/src/main/java/de/blight/common/ModelMetaIO.java @@ -40,6 +40,7 @@ public final class ModelMetaIO { p.setProperty("interactableOffsetZ", String.valueOf(m.interactableOffsetZ())); p.setProperty("interactableRotY", String.valueOf(m.interactableRotY())); p.setProperty("footstepSurface", m.footstepSurface() != null ? m.footstepSurface() : ""); + p.setProperty("modelCategory", m.modelCategory() != null ? m.modelCategory().name() : "NONE"); // Anhänge: Lichter List lights = m.attachedLights(); @@ -140,7 +141,8 @@ public final class ModelMetaIO { parseFloat(p, "interactableOffsetY", 0.5f), parseFloat(p, "interactableOffsetZ", 0f), parseFloat(p, "interactableRotY", 0f), - p.getProperty("footstepSurface", "") + p.getProperty("footstepSurface", ""), + de.blight.common.ModelCategory.fromString(p.getProperty("modelCategory", "NONE")) ); } diff --git a/blight-common/src/main/java/de/blight/common/PlacedModel.java b/blight-common/src/main/java/de/blight/common/PlacedModel.java index 2d03f01..a10e611 100644 --- a/blight-common/src/main/java/de/blight/common/PlacedModel.java +++ b/blight-common/src/main/java/de/blight/common/PlacedModel.java @@ -22,5 +22,7 @@ public record PlacedModel( /** "CRAFTING_TABLE" / "BED" / "" für kein Interactable. */ String interactableType, /** ID des verknüpften Interactables (CraftingTableType-Name oder Bett-UUID); "" wenn nicht gesetzt. */ - String interactableId + String interactableId, + /** Kategorie für Karten-Rendering (TREE, BUILDING, BUSH, NONE). */ + de.blight.common.ModelCategory modelCategory ) {} diff --git a/blight-common/src/main/java/de/blight/common/PlacedModelIO.java b/blight-common/src/main/java/de/blight/common/PlacedModelIO.java index 3767bca..36ed957 100644 --- a/blight-common/src/main/java/de/blight/common/PlacedModelIO.java +++ b/blight-common/src/main/java/de/blight/common/PlacedModelIO.java @@ -26,11 +26,11 @@ public final class PlacedModelIO { Path p = getPath(); Files.createDirectories(p.getParent()); try (BufferedWriter w = Files.newBufferedWriter(p)) { - w.write("# modelPath\tx\ty\tz\trotY\tscale\trotX\trotZ\tsolid\ttexPath\tnmPath\tmatPath\tmeshFile\tanimClip\tcastShadow\treceiveShadow\tlod1Path\tlod2Path\tlod1Distance\tlod2Distance\tcullDistance\tinteractableType\tinteractableId"); + w.write("# modelPath\tx\ty\tz\trotY\tscale\trotX\trotZ\tsolid\ttexPath\tnmPath\tmatPath\tmeshFile\tanimClip\tcastShadow\treceiveShadow\tlod1Path\tlod2Path\tlod1Distance\tlod2Distance\tcullDistance\tinteractableType\tinteractableId\tmodelCategory"); w.newLine(); for (PlacedModel m : models) { w.write(String.format(Locale.ROOT, - "%s\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%b\t%s\t%s\t%s\t%s\t%s\t%b\t%b\t%s\t%s\t%.5f\t%.5f\t%.5f\t%s\t%s%n", + "%s\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%b\t%s\t%s\t%s\t%s\t%s\t%b\t%b\t%s\t%s\t%.5f\t%.5f\t%.5f\t%s\t%s\t%s%n", m.modelPath(), m.x(), m.y(), m.z(), m.rotY(), m.scale(), @@ -41,7 +41,8 @@ public final class PlacedModelIO { m.castShadow(), m.receiveShadow(), nvl(m.lod1Path()), nvl(m.lod2Path()), m.lod1Distance(), m.lod2Distance(), m.cullDistance(), - nvl(m.interactableType()), nvl(m.interactableId()))); + nvl(m.interactableType()), nvl(m.interactableId()), + m.modelCategory() != null ? m.modelCategory().name() : "NONE")); } } } @@ -79,12 +80,15 @@ public final class PlacedModelIO { float cullDistance = f.length > 20 ? parseFloat(f[20], 120f) : 120f; String interactableType = f.length > 21 ? f[21] : ""; String interactableId = f.length > 22 ? f[22] : ""; + de.blight.common.ModelCategory modelCategory = + f.length > 23 ? de.blight.common.ModelCategory.fromString(f[23]) + : de.blight.common.ModelCategory.NONE; list.add(new PlacedModel(modelPath, x, y, z, rotY, rotX, rotZ, scale, solid, texPath, nmPath, matPath, meshFile, animClip, castShadow, receiveShadow, lod1Path, lod2Path, lod1Distance, lod2Distance, cullDistance, - interactableType, interactableId)); + interactableType, interactableId, modelCategory)); } catch (NumberFormatException ignored) {} } return list; diff --git a/blight-common/src/main/java/de/blight/common/map/WorldMapRenderModel.java b/blight-common/src/main/java/de/blight/common/map/WorldMapRenderModel.java index b611479..2e86df4 100644 --- a/blight-common/src/main/java/de/blight/common/map/WorldMapRenderModel.java +++ b/blight-common/src/main/java/de/blight/common/map/WorldMapRenderModel.java @@ -25,5 +25,6 @@ public record WorldMapRenderModel( 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) List coastPaths, // geglättete Marching-Squares Küstenpfade (Weltkoord.) - List> treeClusters + List> treeClusters, + int[][] mapTilePixels // 6 Kacheln (TILE_WATER..TILE_ROCK), je 16×16 ARGB; null = Fallback ) {} diff --git a/blight-common/src/main/java/de/blight/common/map/WorldMapRenderer.java b/blight-common/src/main/java/de/blight/common/map/WorldMapRenderer.java index f7d5a83..ac54bf1 100644 --- a/blight-common/src/main/java/de/blight/common/map/WorldMapRenderer.java +++ b/blight-common/src/main/java/de/blight/common/map/WorldMapRenderer.java @@ -3,6 +3,8 @@ package de.blight.common.map; import de.blight.common.*; import de.blight.common.model.Location; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.imageio.ImageIO; import java.awt.*; import java.awt.geom.Path2D; @@ -21,6 +23,8 @@ import java.util.List; */ public final class WorldMapRenderer { + private static final Logger log = LoggerFactory.getLogger(WorldMapRenderer.class); + public static final float WORLD_HALF = 1024f; public static final float WORLD_SIZE = 2048f; /** Auflösung des voxelSurface-Bake-Arrays (1 Zelle pro Welteinheit). */ @@ -102,146 +106,241 @@ public final class WorldMapRenderer { private static final int[] DEF_SLOT_G = { 148, 82, 115, 184, 80, 90, 60, 100 }; private static final int[] DEF_SLOT_B = { 46, 64, 77, 128, 40, 60, 50, 70 }; + // Karten-Kachel-Indizes + public static final int TILE_WATER = 0, TILE_CLIFF = 1, TILE_GRASS = 2, + TILE_SAND = 3, TILE_DIRT = 4, TILE_ROCK = 5, + TILE_PAVEMENT = 6, TILE_FOREST = 7; + private static final String[] TILE_FILE_NAMES = { + "map_water", "map_cliff", "map_grass", "map_sand", "map_dirt", "map_rock", "map_pavement", "map_forest" }; + public static final int[][] TILE_FALLBACK_RGB = { + {55, 120, 195}, {115, 108, 100}, {78, 138, 52}, + {198, 182, 110}, {118, 82, 46}, {122, 116, 108}, {148, 135, 112}, {34, 85, 34} }; + public static final float TILE_WU = 1f; // Kachelgroesse in Welteinheiten (1m → 1:1 bei scale=16) + private WorldMapRenderer() {} /** Hauptmethode: rendert das Hintergrund-PNG aus einem vorberechneten Modell. */ public static BufferedImage render(WorldMapRenderModel model, int targetSize, RenderOptions opts) { MapData m = model.mapData(); - int TV = MapData.TERRAIN_VERTS; - int SS = MapData.SPLAT_SIZE; - int[] slotR = slotChannel(model.slotColorsRGB(), 0); - int[] slotG = slotChannel(model.slotColorsRGB(), 1); - int[] slotB = slotChannel(model.slotColorsRGB(), 2); - // ── 1. Heightmap auf Zielauflösung samplen ──────────────────────────── - int UV = MapData.UPPER_VERTS; - 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. + // 1. Hoehenfeld + float[] heights = buildTerrainSamples(m, targetSize, model.voxelSurface()); boolean[] seaPixels = new boolean[targetSize * targetSize]; - float minH = Float.MAX_VALUE, maxH = -Float.MAX_VALUE; + for (int i = 0; i < heights.length; i++) { seaPixels[i] = heights[i] < 0f; } - int VS = VOXEL_SURFACE_RES; - for (int py = 0; py < targetSize; py++) { - for (int px = 0; px < targetSize; px++) { - 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); - float ux_f = (float) px / (targetSize - 1) * (UV - 1); - float uz_f = (float) py / (targetSize - 1) * (UV - 1); - float baseH = m.terrainHeight[hz * TV + hx]; - float h = baseH; - float upper = bilerpPos(m.upperTop, ux_f, uz_f, UV); - 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; } + // 2. Wasser-Maske: Ozean + PlacedWater vorab berechnen + boolean[] isWater = java.util.Arrays.copyOf(seaPixels, seaPixels.length); + if (opts.showWater()) { + for (PlacedWater w : model.waters()) { + int[] wxPx = worldToPixels(w.pointsX(), targetSize); + int[] wzPx = worldToPixels(w.pointsZ(), targetSize); + Polygon waterPoly = new Polygon(wxPx, wzPx, wxPx.length); + Rectangle bb = waterPoly.getBounds(); + float wh = w.waterHeight(); + for (int py = Math.max(0, bb.y); py <= Math.min(targetSize-1, bb.y+bb.height); py++) { + for (int px = Math.max(0, bb.x); px <= Math.min(targetSize-1, bb.x+bb.width); px++) { + if (waterPoly.contains(px, py) && heights[py * targetSize + px] < wh) + isWater[py * targetSize + px] = true; } } - heights[py * targetSize + px] = h; - seaPixels[py * targetSize + px] = (seaH < 0f); - if (h < minH) minH = h; - if (h > maxH) maxH = h; } } - float heightRange = Math.max(0.01f, maxH - minH); - - // ── 2. Pixel-Farben berechnen ───────────────────────────────────────── - BufferedImage img = new BufferedImage(targetSize, targetSize, BufferedImage.TYPE_INT_RGB); + // 3. Tile-Rendering: tileIdx + Rohfarbe pro Pixel speichern + float cellW = WORLD_SIZE / (targetSize - 1); + int SS = MapData.SPLAT_SIZE; + int[] slotCols = model.slotColorsRGB(); + int[][] tiles = model.mapTilePixels(); + int[] rawRGB = new int[targetSize * targetSize]; + int waterR = (WATER_COLOR >> 16) & 0xFF; + int waterG = (WATER_COLOR >> 8) & 0xFF; + int waterB = WATER_COLOR & 0xFF; for (int py = 0; py < targetSize; py++) { + float wz = -WORLD_HALF + py * cellW; for (int px = 0; px < targetSize; px++) { - float h = heights[py * targetSize + px]; - float hn = (h - minH) / heightRange; + float wx = -WORLD_HALF + px * cellW; + int idx = py * targetSize + px; - int r, g, b; - - if (opts.showSplatColors()) { - int sx = Math.min((int)((float) px / (targetSize - 1) * (SS - 1)), SS - 1); - int sz = Math.min((int)((float)(targetSize - 1 - py) / (targetSize - 1) * (SS - 1)), SS - 1); - int si = sz * SS + sx; - // Base layer (Slots 1-4) - float wg = (m.splatG[si] & 0xFF) / 255f; - float wb = (m.splatB[si] & 0xFF) / 255f; - float wa = (m.splatA[si] & 0xFF) / 255f; - float fr = slotR[0], fg = slotG[0], fb = slotB[0]; - fr = fr*(1-wg) + slotR[1]*wg; fg = fg*(1-wg) + slotG[1]*wg; fb = fb*(1-wg) + slotB[1]*wg; - fr = fr*(1-wb) + slotR[2]*wb; fg = fg*(1-wb) + slotG[2]*wb; fb = fb*(1-wb) + slotB[2]*wb; - fr = fr*(1-wa) + slotR[3]*wa; fg = fg*(1-wa) + slotG[3]*wa; fb = fb*(1-wa) + slotB[3]*wa; - // Upper layer (Slots 5-8) - float u1 = (m.upperSplatR[si] & 0xFF) / 255f; - float u2 = (m.upperSplatG[si] & 0xFF) / 255f; - float u3 = (m.upperSplatB[si] & 0xFF) / 255f; - float u4 = (m.upperSplatA[si] & 0xFF) / 255f; - fr = fr*(1-u1) + slotR[4]*u1; fg = fg*(1-u1) + slotG[4]*u1; fb = fb*(1-u1) + slotB[4]*u1; - fr = fr*(1-u2) + slotR[5]*u2; fg = fg*(1-u2) + slotG[5]*u2; fb = fb*(1-u2) + slotB[5]*u2; - fr = fr*(1-u3) + slotR[6]*u3; fg = fg*(1-u3) + slotG[6]*u3; fb = fb*(1-u3) + slotB[6]*u3; - fr = fr*(1-u4) + slotR[7]*u4; fg = fg*(1-u4) + slotG[7]*u4; fb = fb*(1-u4) + slotB[7]*u4; - r = clamp((int) fr); g = clamp((int) fg); b = clamp((int) fb); + int tileIdx; + int tr, tg, tb; + if (isWater[idx]) { + tileIdx = TILE_WATER; + tr = waterR; tg = waterG; tb = waterB; } else { - int lum = clamp(30 + (int)(hn * 200)); - r = g = b = lum; + int dPx = Math.max(1, (int)(0.25f / cellW)); + float hR = heights[py * targetSize + Math.min(targetSize-1, px+dPx)]; + float hL = heights[py * targetSize + Math.max(0, px-dPx)]; + float hD = heights[Math.min(targetSize-1, py+dPx) * targetSize + px]; + float hU = heights[Math.max(0, py-dPx) * targetSize + px]; + float dist = dPx * cellW; + double slope = Math.toDegrees(Math.atan(Math.sqrt( + Math.pow((hR-hL) / (2*dist), 2) + Math.pow((hD-hU) / (2*dist), 2)))); + tileIdx = (slope > 30.0) ? TILE_CLIFF : getTerrainTile(m, SS, wx, wz, slotCols); + if (tiles != null && tileIdx < tiles.length && tiles[tileIdx] != null) { + int tu = Math.floorMod((int)Math.floor((wx + WORLD_HALF) / TILE_WU), 16); + int tv = Math.floorMod((int)Math.floor((wz + WORLD_HALF) / TILE_WU), 16); + int argb = tiles[tileIdx][tv * 16 + tu]; + tr = (argb >> 16) & 0xFF; tg = (argb >> 8) & 0xFF; tb = argb & 0xFF; + } else { + tr = TILE_FALLBACK_RGB[tileIdx][0]; + tg = TILE_FALLBACK_RGB[tileIdx][1]; + tb = TILE_FALLBACK_RGB[tileIdx][2]; + } } - - // Hillshading (NW-Licht) - if (opts.showTerrain() && px > 0 && px < targetSize - 1 && py > 0 && py < targetSize - 1) { - float dx = heights[py * targetSize + (px + 1)] - heights[py * targetSize + (px - 1)]; - float dz = heights[(py + 1) * targetSize + px] - heights[(py - 1) * targetSize + px]; - float nx = -dx * 0.25f, ny = 1f, nz = -dz * 0.25f; - float len = (float) Math.sqrt(nx * nx + ny * ny + nz * nz); - nx /= len; ny /= len; nz /= len; - float shade = nx * (-0.577f) + ny * 0.577f + nz * (-0.577f); - shade = 0.45f + shade * 0.85f; - shade = Math.max(0.2f, Math.min(1.8f, shade)); - r = clamp((int)(r * shade)); - g = clamp((int)(g * shade)); - b = clamp((int)(b * shade)); - } - - img.setRGB(px, py, (r << 16) | (g << 8) | b); + rawRGB[idx] = (tr << 16) | (tg << 8) | tb; } } - // Kuwahara-Filter: lässt das Terrain wie gemalt wirken - if (opts.showSplatColors()) { applyKuwahara(img, 3); } + // 4. Kuestenlinie: Sub-Pixel-Sampling fuer pixelgenaue Wasser-Land-Grenze. + // Greift nur auf Grenz-Pixel zur Ozean-Kuestenlinie. Interior-Pixel bleiben scharf. + int SUB = 8; + float subInv = 1f / (SUB * SUB); + for (int py = 0; py < targetSize; py++) { + float wz = -WORLD_HALF + py * cellW; + for (int px = 0; px < targetSize; px++) { + float wx = -WORLD_HALF + px * cellW; + int idx = py * targetSize + px; + boolean isS = seaPixels[idx]; + boolean boundary = false; + for (int dy = -1; dy <= 1 && !boundary; dy++) { + for (int dx = -1; dx <= 1 && !boundary; dx++) { + if (dx == 0 && dy == 0) continue; + int ny = py + dy, nx = px + dx; + if (ny < 0 || ny >= targetSize || nx < 0 || nx >= targetSize) continue; + if (seaPixels[ny * targetSize + nx] != isS) boundary = true; + } + } + if (!boundary) continue; + // 8x8 Sub-Samples: Wasser-Anteil dieses Pixels bestimmen + int wCount = 0; + for (int sy = 0; sy < SUB; sy++) { + for (int sx = 0; sx < SUB; sx++) { + float spx = px + (sx + 0.5f) / SUB - 0.5f; + float spy = py + (sy + 0.5f) / SUB - 0.5f; + if (sampleTerrainH(m, targetSize, spx, spy, model.voxelSurface()) < 0f) + wCount++; + } + } + float wFrac = wCount * subInv; + // Land-Farbe: fuer Wasser-Pixel on-the-fly berechnen, sonst aus rawRGB + int landRGB; + if (isS) { + int dPx = Math.max(1, (int)(0.25f / cellW)); + float hR2 = heights[py * targetSize + Math.min(targetSize-1, px+dPx)]; + float hL2 = heights[py * targetSize + Math.max(0, px-dPx)]; + float hD2 = heights[Math.min(targetSize-1, py+dPx) * targetSize + px]; + float hU2 = heights[Math.max(0, py-dPx) * targetSize + px]; + float d2 = dPx * cellW; + double sl2 = Math.toDegrees(Math.atan(Math.sqrt( + Math.pow((hR2-hL2)/(2*d2), 2) + Math.pow((hD2-hU2)/(2*d2), 2)))); + int ti2 = (sl2 > 30.0) ? TILE_CLIFF : getTerrainTile(m, SS, wx, wz, slotCols); + int tr2, tg2, tb2; + if (tiles != null && ti2 < tiles.length && tiles[ti2] != null) { + int tu=Math.floorMod((int)Math.floor((wx+WORLD_HALF)/TILE_WU),16); + int tv=Math.floorMod((int)Math.floor((wz+WORLD_HALF)/TILE_WU),16); + int argb = tiles[ti2][tv * 16 + tu]; + tr2=(argb>>16)&0xFF; tg2=(argb>>8)&0xFF; tb2=argb&0xFF; + } else { + tr2=TILE_FALLBACK_RGB[ti2][0]; tg2=TILE_FALLBACK_RGB[ti2][1]; tb2=TILE_FALLBACK_RGB[ti2][2]; + } + landRGB = (tr2<<16)|(tg2<<8)|tb2; + } else { + landRGB = rawRGB[idx]; + } + int lr=(landRGB>>16)&0xFF, lg=(landRGB>>8)&0xFF, lb=landRGB&0xFF; + rawRGB[idx] = (clamp((int)(waterR*wFrac + lr*(1-wFrac))) << 16) + | (clamp((int)(waterG*wFrac + lg*(1-wFrac))) << 8) + | clamp((int)(waterB*wFrac + lb*(1-wFrac))); + } + } - // ── 3. Vektor-Overlays ──────────────────────────────────────────────── + // 4b. PlacedWater-Kuestenlinie: Sub-Pixel-Sampling fuer definierte Wasserflaechen + if (opts.showWater()) { + for (PlacedWater w : model.waters()) { + int[] wxPx2 = worldToPixels(w.pointsX(), targetSize); + int[] wzPx2 = worldToPixels(w.pointsZ(), targetSize); + Polygon wp = new Polygon(wxPx2, wzPx2, wxPx2.length); + Rectangle bb2 = wp.getBounds(); + float wh2 = w.waterHeight(); + int x0 = Math.max(0, bb2.x - 1), y0 = Math.max(0, bb2.y - 1); + int x1 = Math.min(targetSize-1, bb2.x + bb2.width + 1); + int y1 = Math.min(targetSize-1, bb2.y + bb2.height + 1); + for (int py = y0; py <= y1; py++) { + float wz = -WORLD_HALF + py * cellW; + for (int px = x0; px <= x1; px++) { + float wx = -WORLD_HALF + px * cellW; + int idx = py * targetSize + px; + boolean inPoly = wp.contains(px, py) && heights[idx] < wh2; + boolean boundary2 = false; + for (int dy = -1; dy <= 1 && !boundary2; dy++) { + for (int dx = -1; dx <= 1 && !boundary2; dx++) { + if (dx == 0 && dy == 0) continue; + int ny = py+dy, nx = px+dx; + if (ny<0||ny>=targetSize||nx<0||nx>=targetSize) continue; + boolean nIn = wp.contains(nx, ny) && heights[ny*targetSize+nx] < wh2; + if (nIn != inPoly) boundary2 = true; + } + } + if (!boundary2) continue; + int wCount2 = 0; + for (int sy = 0; sy < SUB; sy++) { + for (int sx = 0; sx < SUB; sx++) { + float spx = px + (sx + 0.5f) / SUB - 0.5f; + float spy = py + (sy + 0.5f) / SUB - 0.5f; + if (wp.contains(spx, spy) + && sampleTerrainH(m, targetSize, spx, spy, model.voxelSurface()) < wh2) + wCount2++; + } + } + float wFrac2 = wCount2 * subInv; + int landRGB2; + if (inPoly) { + // Wasser-Pixel: Land-Farbe on-the-fly + int dPx = Math.max(1, (int)(0.25f / cellW)); + float hR2 = heights[py * targetSize + Math.min(targetSize-1, px+dPx)]; + float hL2 = heights[py * targetSize + Math.max(0, px-dPx)]; + float hD2 = heights[Math.min(targetSize-1, py+dPx) * targetSize + px]; + float hU2 = heights[Math.max(0, py-dPx) * targetSize + px]; + float d2 = dPx * cellW; + double sl2 = Math.toDegrees(Math.atan(Math.sqrt( + Math.pow((hR2-hL2)/(2*d2),2) + Math.pow((hD2-hU2)/(2*d2),2)))); + int ti2 = (sl2 > 30.0) ? TILE_CLIFF : getTerrainTile(m, SS, wx, wz, slotCols); + int tr2, tg2, tb2; + if (tiles != null && ti2 < tiles.length && tiles[ti2] != null) { + int tu2=Math.floorMod((int)Math.floor((wx+WORLD_HALF)/TILE_WU),16); + int tv2=Math.floorMod((int)Math.floor((wz+WORLD_HALF)/TILE_WU),16); + int argb = tiles[ti2][tv2 * 16 + tu2]; + tr2=(argb>>16)&0xFF; tg2=(argb>>8)&0xFF; tb2=argb&0xFF; + } else { + tr2=TILE_FALLBACK_RGB[ti2][0]; tg2=TILE_FALLBACK_RGB[ti2][1]; tb2=TILE_FALLBACK_RGB[ti2][2]; + } + landRGB2 = (tr2<<16)|(tg2<<8)|tb2; + } else { + landRGB2 = rawRGB[idx]; + } + int lr2=(landRGB2>>16)&0xFF, lg2=(landRGB2>>8)&0xFF, lb2=landRGB2&0xFF; + rawRGB[idx] = (clamp((int)(waterR*wFrac2 + lr2*(1-wFrac2))) << 16) + | (clamp((int)(waterG*wFrac2 + lg2*(1-wFrac2))) << 8) + | clamp((int)(waterB*wFrac2 + lb2*(1-wFrac2))); + } + } + } + } + + // 5. Ergebnis in Bild schreiben + BufferedImage img = new BufferedImage(targetSize, targetSize, BufferedImage.TYPE_INT_RGB); + for (int i = 0; i < rawRGB.length; i++) { + img.setRGB(i % targetSize, i / targetSize, rawRGB[i]); + } + + // ── 3. Vektor-Overlays ──────────────────────────────────────────────── Graphics2D gfx = img.createGraphics(); gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); float lineW = Math.max(1.5f, targetSize / 400f); // Wasser if (opts.showWater()) { - // ── Weißfüllung ─────────────────────────────────────────────────────── - for (int py = 0; py < targetSize; py++) { - for (int px = 0; px < targetSize; px++) { - if (seaPixels[py * targetSize + px]) img.setRGB(px, py, WATER_COLOR); - } - } - for (PlacedWater w : model.waters()) { - int[] xs = worldToPixels(w.pointsX(), targetSize); - int[] ys = worldToPixels(w.pointsZ(), targetSize); - Polygon poly = new Polygon(xs, ys, xs.length); - Rectangle bb = poly.getBounds(); - int x0 = Math.max(0, bb.x), y0 = Math.max(0, bb.y); - int x1 = Math.min(targetSize - 1, bb.x + bb.width); - int y1 = Math.min(targetSize - 1, bb.y + bb.height); - float wh = w.waterHeight(); - for (int py = y0; py <= y1; py++) { - for (int px = x0; px <= x1; px++) { - if (poly.contains(px, py) && heights[py * targetSize + px] < wh) - img.setRGB(px, py, WATER_COLOR); - } - } - } // ── Wellenmarken ────────────────────────────────────────────────────── if (opts.showWaterWaves()) { @@ -297,45 +396,6 @@ public final class WorldMapRenderer { } } - // ── Uferlinie ───────────────────────────────────────────────────────── - if (opts.showWaterWaves()) { - gfx.setColor(Color.BLACK); - gfx.setStroke(new BasicStroke(3.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); - // Wasserflächen: Polygon-Umriss - for (PlacedWater w : model.waters()) { - int[] xs = worldToPixels(w.pointsX(), targetSize); - int[] ys = worldToPixels(w.pointsZ(), targetSize); - gfx.drawPolygon(xs, ys, xs.length); - } - // Meer: Pixel-Küstenlinie (morphologisch aufgeweitet) - int bw = 3; - boolean[] cur = new boolean[targetSize * targetSize]; - boolean[] nxt = new boolean[targetSize * targetSize]; - for (int py = 1; py < targetSize - 1; py++) { - for (int px = 1; px < targetSize - 1; px++) { - if (!seaPixels[py * targetSize + px]) continue; - if (!seaPixels[py * targetSize + (px-1)] || !seaPixels[py * targetSize + (px+1)] || - !seaPixels[(py-1) * targetSize + px] || !seaPixels[(py+1) * targetSize + px]) - cur[py * targetSize + px] = true; - } - } - for (int pass = 1; pass < bw; pass++) { - System.arraycopy(cur, 0, nxt, 0, cur.length); - for (int py = 1; py < targetSize - 1; py++) { - for (int px = 1; px < targetSize - 1; px++) { - if (!cur[py * targetSize + px]) continue; - if (seaPixels[py * targetSize + (px-1)]) nxt[py * targetSize + (px-1)] = true; - if (seaPixels[py * targetSize + (px+1)]) nxt[py * targetSize + (px+1)] = true; - if (seaPixels[(py-1) * targetSize + px]) 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; - } - for (int i = 0, n = cur.length; i < n; i++) { - if (cur[i]) img.setRGB(i % targetSize, i / targetSize, 0xFF000000); - } - } } // Wasserfälle – klassisches Kartensymbol: Kammlinie + Kaskaden-Linien @@ -471,7 +531,7 @@ public final class WorldMapRenderer { return render(new WorldMapRenderModel( input.mapData(), input.areas(), input.zones(), input.locations(), input.waters(), input.models(), null, - input.slotColorsRGB(), null, null, null, null, null + input.slotColorsRGB(), null, null, null, null, null, null ), targetSize, opts); } @@ -681,45 +741,6 @@ public final class WorldMapRenderer { } } - // ── Uferlinie ───────────────────────────────────────────────────────── - if (opts.showWaterWaves()) { - gfx.setColor(Color.BLACK); - gfx.setStroke(new BasicStroke(3.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); - // Wasserflächen: Polygon-Umriss - for (PlacedWater w : input.waters()) { - int[] xs = wrp(w.pointsX(), wx0, rSize, targetSize); - int[] ys = wrp(w.pointsZ(), wz0, rSize, targetSize); - gfx.drawPolygon(xs, ys, xs.length); - } - // Meer: Pixel-Küstenlinie (morphologisch aufgeweitet) - int bw = 3; - boolean[] cur = new boolean[targetSize * targetSize]; - boolean[] nxt = new boolean[targetSize * targetSize]; - for (int py = 1; py < targetSize - 1; py++) { - for (int px = 1; px < targetSize - 1; px++) { - if (seaPixels2 == null || !seaPixels2[py * targetSize + px]) continue; - if (!seaPixels2[py * targetSize + (px-1)] || !seaPixels2[py * targetSize + (px+1)] || - !seaPixels2[(py-1) * targetSize + px] || !seaPixels2[(py+1) * targetSize + px]) - cur[py * targetSize + px] = true; - } - } - for (int pass = 1; pass < bw; pass++) { - System.arraycopy(cur, 0, nxt, 0, cur.length); - for (int py = 1; py < targetSize - 1; py++) { - for (int px = 1; px < targetSize - 1; px++) { - if (!cur[py * targetSize + px]) continue; - if (seaPixels2[py * targetSize + (px-1)]) nxt[py * targetSize + (px-1)] = true; - if (seaPixels2[py * targetSize + (px+1)]) nxt[py * targetSize + (px+1)] = true; - if (seaPixels2[(py-1) * targetSize + px]) 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; - } - for (int i = 0, n = cur.length; i < n; i++) { - if (cur[i]) img.setRGB(i % targetSize, i / targetSize, 0xFF000000); - } - } } if (opts.showAreas()) { @@ -885,13 +906,264 @@ public final class WorldMapRenderer { * @param baseFallbacks Fallback-Pfade für leere Base-Slots (kann null sein) * @return int[24] mit [R0,G0,B0, R1,G1,B1, ..., R7,G7,B7] für Slots 1-8 */ - public static int[] computeSlotColors(MapData mapData, Path assetRoot, String[] baseFallbacks) { + /** + * Bilineare Höhen-Samples aus terrainHeight (8193×8193, 0.25m/Vertex) + max(voxelSurface). + * Zentrale Methode für alle Karten-Berechnungen die Geländehöhen benötigen. + */ + public static float[] buildTerrainSamples(MapData m, int size, float[] voxelSurface) { + int TV = MapData.TERRAIN_VERTS; + int VS = VOXEL_SURFACE_RES; + float[] s = new float[size * size]; + for (int mz = 0; mz < size; mz++) { + for (int mx = 0; mx < size; mx++) { + float fx = (float)mx / (size-1) * (TV-1); + float fz = (float)mz / (size-1) * (TV-1); + int x0 = Math.max(0, Math.min(TV-2, (int)fx)); + int z0 = Math.max(0, Math.min(TV-2, (int)fz)); + float tx = fx - x0, tz = fz - z0; + float h = m.terrainHeight[z0*TV+x0] * (1-tx)*(1-tz) + + m.terrainHeight[z0*TV+x0+1] * tx *(1-tz) + + m.terrainHeight[(z0+1)*TV+x0] * (1-tx)*tz + + m.terrainHeight[(z0+1)*TV+x0+1] * tx *tz; + if (voxelSurface != null) { + int vx = Math.min((int)((float)mx/(size-1)*(VS-1)), VS-1); + int vz = Math.min((int)((float)mz/(size-1)*(VS-1)), VS-1); + for (int dz = -1; dz <= 1; dz++) { + for (int dx = -1; dx <= 1; dx++) { + float vs = voxelSurface[Math.max(0,Math.min(VS-1,vz+dz))*VS + +Math.max(0,Math.min(VS-1,vx+dx))]; + if (!Float.isNaN(vs) && vs > h) { h = vs; } + } + } + } + s[mz*size+mx] = h; + } + } + return s; + } + + /** + * Berechnet eine Hillshading-Karte (float pro Pixel, 0.55–1.35) aus reinen Terrain-Höhen. + * Voxels werden NICHT eingerechnet, um Artefakte an Voxel-Kanten zu vermeiden. + */ + public static float[] buildShadeMap(MapData m, int size) { + float[] th = buildTerrainSamples(m, size, null); + float[] shd = new float[size * size]; + for (int mz = 0; mz < size; mz++) { + for (int mx = 0; mx < size; mx++) { + int px1 = Math.min(size-1, mx+1), px0 = Math.max(0, mx-1); + int pz1 = Math.min(size-1, mz+1), pz0 = Math.max(0, mz-1); + float dx = th[mz*size + px1] - th[mz*size + px0]; + float dz = th[pz1*size + mx] - th[pz0*size + mx]; + shd[mz*size + mx] = Math.max(0.55f, Math.min(1.35f, 1f - (dx * 0.3f + dz * 0.15f))); + } + } + return shd; + } + + /** + * Berechnet eine Textur-Kategorie-Karte (byte pro Pixel) in der angegebenen Auflösung. + * Werte: TILE_GRASS=2, TILE_SAND=3, TILE_DIRT=4, TILE_ROCK=5. + * TILE_WATER kommt hier nicht vor — Wasser-Erkennung erfolgt über buildWaterHeightMap. + * Wird vom Canvas-Overlay genutzt, um bei jedem Zoom-Level die richtige Tile-Textur zu samplen. + */ + public static byte[] buildCategoryMap(MapData m, int size, float[] voxelSurface, + List waters, int[] slotColors) { + float cellW = WORLD_SIZE / (size - 1); + int SS = MapData.SPLAT_SIZE; + byte[] cats = new byte[size * size]; + for (int mz = 0; mz < size; mz++) { + float wz = -WORLD_HALF + mz * cellW; + for (int mx = 0; mx < size; mx++) { + float wx = -WORLD_HALF + mx * cellW; + cats[mz * size + mx] = (byte) getTerrainTile(m, SS, wx, wz, slotColors); + } + } + return cats; + } + + /** + * Baut eine Wasserhoehen-Karte: pro Zelle die maximale PlacedWater-Wasserhoehe, + * 0 fuer Zellen ausserhalb jeder Wasserflaeche. + * In drawWaterOverlay: Pixel ist Wasser wenn heightMap[i] < waterHeightMap[i]. + */ + public static float[] buildWaterHeightMap(List waters, int size) { + float[] map = new float[size * size]; + if (waters == null) return map; + for (PlacedWater w : waters) { + int[] wxPx = worldToPixels(w.pointsX(), size); + int[] wzPx = worldToPixels(w.pointsZ(), size); + Polygon poly = new Polygon(wxPx, wzPx, wxPx.length); + Rectangle bb = poly.getBounds(); + float wh = w.waterHeight(); + for (int py = Math.max(0, bb.y); py <= Math.min(size-1, bb.y+bb.height); py++) { + for (int px = Math.max(0, bb.x); px <= Math.min(size-1, bb.x+bb.width); px++) { + if (poly.contains(px, py)) { + map[py * size + px] = Math.max(map[py * size + px], wh); + } + } + } + } + return map; + } + + /** Lädt die 6 Karten-Kacheln aus Textures/map/ im Asset-Verzeichnis. */ + public static int[][] loadMapTiles(Path assetRoot) { + int[][] result = new int[TILE_FILE_NAMES.length][]; + for (int i = 0; i < TILE_FILE_NAMES.length; i++) { + Path p = assetRoot.resolve("Textures/map/" + TILE_FILE_NAMES[i] + ".png"); + int[] pixels = null; + if (Files.exists(p)) { + try { + BufferedImage img = ImageIO.read(p.toFile()); + if (img != null) { + pixels = new int[256]; + for (int py = 0; py < 16; py++) { + for (int px = 0; px < 16; px++) { + pixels[py * 16 + px] = img.getRGB(px % img.getWidth(), py % img.getHeight()); + } + } + } + } catch (Exception ignored) {} + } + if (pixels == null) { + int[] fb = TILE_FALLBACK_RGB[i]; + pixels = new int[256]; + int packed = 0xFF000000 | (fb[0] << 16) | (fb[1] << 8) | fb[2]; + java.util.Arrays.fill(pixels, packed); + } + result[i] = pixels; + } + return result; + } + + /** Ermittelt den Tile-Index via sequentieller Splatmap-Blendung (identisch zu showSplatColors). + * slotColors: 24-Werte-Array (8 Slots × RGB) aus computeSlotColors(); null → Defaults. */ + private static int getTerrainTile(MapData m, int SS, float wx, float wz, int[] slotColors) { + int sx = Math.max(0, Math.min(SS-1, (int)((wx + WORLD_HALF) / WORLD_SIZE * (SS-1)))); + int sz = Math.max(0, Math.min(SS-1, SS-1 - (int)((wz + WORLD_HALF) / WORLD_SIZE * (SS-1)))); + int si = sz * SS + sx; + // Base-Layer sequentiell blenden (Slot 0 → 1 → 2 → 3) + float wg = (m.splatG[si] & 0xFF) / 255f; + float wb = (m.splatB[si] & 0xFF) / 255f; + float wa = (m.splatA[si] & 0xFF) / 255f; + float fr = sc(slotColors, 0), fg = sc(slotColors, 1), fb = sc(slotColors, 2); + fr=fr*(1-wg)+sc(slotColors, 3)*wg; fg=fg*(1-wg)+sc(slotColors, 4)*wg; fb=fb*(1-wg)+sc(slotColors, 5)*wg; + fr=fr*(1-wb)+sc(slotColors, 6)*wb; fg=fg*(1-wb)+sc(slotColors, 7)*wb; fb=fb*(1-wb)+sc(slotColors, 8)*wb; + fr=fr*(1-wa)+sc(slotColors, 9)*wa; fg=fg*(1-wa)+sc(slotColors,10)*wa; fb=fb*(1-wa)+sc(slotColors,11)*wa; + // Upper-Layer + float u1=(m.upperSplatR[si]&0xFF)/255f, u2=(m.upperSplatG[si]&0xFF)/255f; + float u3=(m.upperSplatB[si]&0xFF)/255f, u4=(m.upperSplatA[si]&0xFF)/255f; + fr=fr*(1-u1)+sc(slotColors,12)*u1; fg=fg*(1-u1)+sc(slotColors,13)*u1; fb=fb*(1-u1)+sc(slotColors,14)*u1; + fr=fr*(1-u2)+sc(slotColors,15)*u2; fg=fg*(1-u2)+sc(slotColors,16)*u2; fb=fb*(1-u2)+sc(slotColors,17)*u2; + fr=fr*(1-u3)+sc(slotColors,18)*u3; fg=fg*(1-u3)+sc(slotColors,19)*u3; fb=fb*(1-u3)+sc(slotColors,20)*u3; + fr=fr*(1-u4)+sc(slotColors,21)*u4; fg=fg*(1-u4)+sc(slotColors,22)*u4; fb=fb*(1-u4)+sc(slotColors,23)*u4; + // Third-Layer + float t1=(m.thirdSplatR[si]&0xFF)/255f, t2=(m.thirdSplatG[si]&0xFF)/255f; + float t3=(m.thirdSplatB[si]&0xFF)/255f, t4=(m.thirdSplatA[si]&0xFF)/255f; + fr=fr*(1-t1)+sc(slotColors,24)*t1; fg=fg*(1-t1)+sc(slotColors,25)*t1; fb=fb*(1-t1)+sc(slotColors,26)*t1; + fr=fr*(1-t2)+sc(slotColors,27)*t2; fg=fg*(1-t2)+sc(slotColors,28)*t2; fb=fb*(1-t2)+sc(slotColors,29)*t2; + fr=fr*(1-t3)+sc(slotColors,30)*t3; fg=fg*(1-t3)+sc(slotColors,31)*t3; fb=fb*(1-t3)+sc(slotColors,32)*t3; + fr=fr*(1-t4)+sc(slotColors,33)*t4; fg=fg*(1-t4)+sc(slotColors,34)*t4; fb=fb*(1-t4)+sc(slotColors,35)*t4; + return slotToTile(Math.round(fr), Math.round(fg), Math.round(fb)); + } + + private static float sc(int[] cols, int i) { + if (cols != null && i < cols.length) return cols[i]; + int slot = i / 3, ch = i % 3, si = Math.min(slot, DEF_SLOT_R.length - 1); + return ch == 0 ? DEF_SLOT_R[si] : ch == 1 ? DEF_SLOT_G[si] : DEF_SLOT_B[si]; + } + + /** + * Tile-Typen für alle 12 Slots (base 0-3, upper 4-7, third 8-11) aus Textur-Pfaden (primär) und Farb-Fallback. + */ + public static int[] computeSlotTileTypes(MapData mapData, int[] slotColors) { + int[] types = new int[12]; + for (int s = 0; s < 12; s++) { + String path = null; + if (s < 4 && mapData.terrainTextures != null && s < mapData.terrainTextures.length) + path = mapData.terrainTextures[s]; + else if (s < 8 && mapData.upperTextures != null && (s-4) < mapData.upperTextures.length) + path = mapData.upperTextures[s - 4]; + else if (s < 12 && mapData.thirdTextures != null && (s-8) < mapData.thirdTextures.length) + path = mapData.thirdTextures[s - 8]; + int byPath = path != null && !path.isEmpty() ? tileTypeByPath(path) : -1; + types[s] = byPath >= 0 ? byPath : classifySlot(slotColors, s); + } + if (log.isDebugEnabled()) { + StringBuilder dbg = new StringBuilder("SlotTileTypes: "); + for (int s = 0; s < 12; s++) { + String path = s < 4 && mapData.terrainTextures != null ? mapData.terrainTextures[s] + : s < 8 && mapData.upperTextures != null ? mapData.upperTextures[s - 4] + : s < 12 && mapData.thirdTextures != null ? mapData.thirdTextures[s - 8] : null; + dbg.append("s").append(s).append("='").append(path != null ? path : "").append("'→").append(types[s]).append(" "); + } + log.debug("{}", dbg); + } + return types; + } + + /** Klassifiziert Textur-Pfad über Schlüsselwörter im Pfad. Gibt -1 zurück wenn unbekannt. */ + private static int tileTypeByPath(String path) { + String p = path.toLowerCase().replace('\\', '/'); + if (p.contains("dirt")) return TILE_DIRT; + if (p.contains("leaves")) return TILE_DIRT; + if (p.contains("gras")) return TILE_GRASS; + if (p.contains("vegetation")) return TILE_GRASS; + if (p.contains("foliage")) return TILE_GRASS; + if (p.contains("gravel")) return TILE_ROCK; + if (p.contains("rock")) return TILE_ROCK; + if (p.contains("stone")) return TILE_ROCK; + if (p.contains("pavement")) return TILE_PAVEMENT; + if (p.contains("sand")) return TILE_SAND; + return -1; + } + + /** Gibt den Tile-Typ für Slot s per Farb-Schwellwert zurück (Fallback). */ + public static int classifySlot(int[] slotColors, int s) { + return slotToTile(Math.round(sc(slotColors, s * 3)), + Math.round(sc(slotColors, s * 3 + 1)), + Math.round(sc(slotColors, s * 3 + 2))); + } + + private static int slotToTile(int r, int g, int b) { + int max = Math.max(Math.max(r, g), b); + int rng = max - Math.min(Math.min(r, g), b); + if (rng < 15) return TILE_ROCK; + if (r > g + 20 && b < 115) return TILE_DIRT; + if (b > 90 && rng < 80) return TILE_SAND; + return TILE_GRASS; + } + + private static float sampleTerrainH(MapData m, int targetSize, float fpx, float fpy, float[] voxelSurface) { + int TV = MapData.TERRAIN_VERTS, VS = VOXEL_SURFACE_RES; + float fx = Math.max(0, Math.min(TV - 1.001f, fpx / (targetSize - 1) * (TV - 1))); + float fz = Math.max(0, Math.min(TV - 1.001f, fpy / (targetSize - 1) * (TV - 1))); + int x0 = (int)fx, z0 = (int)fz; + int x1 = Math.min(x0 + 1, TV - 1), z1 = Math.min(z0 + 1, TV - 1); + float tx = fx - x0, tz = fz - z0; + float h = m.terrainHeight[z0*TV+x0]*(1-tx)*(1-tz) + + m.terrainHeight[z0*TV+x1]*tx*(1-tz) + + m.terrainHeight[z1*TV+x0]*(1-tx)*tz + + m.terrainHeight[z1*TV+x1]*tx*tz; + if (voxelSurface != null) { + int vx = Math.min(Math.max(0, (int)(fpx/(targetSize-1)*(VS-1))), VS-1); + int vz = Math.min(Math.max(0, (int)(fpy/(targetSize-1)*(VS-1))), VS-1); + float vs = voxelSurface[vz * VS + vx]; + if (!Float.isNaN(vs) && vs > h) h = vs; + } + return h; + } + + public static int[] computeSlotColors(MapData mapData, Path assetRoot, String[] baseFallbacks) { int[] result = { 71,148, 46, 115, 82, 64, 140,115, 77, 204,184,128, - 90, 80, 40, 130, 90, 60, 110, 60, 50, 180,100, 70 + 90, 80, 40, 130, 90, 60, 110, 60, 50, 180,100, 70, + 118, 82, 46, 118, 82, 46, 118, 82, 46, 118, 82, 46 }; sampleTexColors(mapData.terrainTextures, baseFallbacks, assetRoot, result, 0); sampleTexColors(mapData.upperTextures, null, assetRoot, result, 4); + sampleTexColors(mapData.thirdTextures, null, assetRoot, result, 8); return result; } @@ -963,6 +1235,11 @@ public final class WorldMapRenderer { } public static boolean isTree(PlacedModel m) { + de.blight.common.ModelCategory cat = m.modelCategory(); + if (cat == de.blight.common.ModelCategory.TREE || cat == de.blight.common.ModelCategory.PALM) + return true; + if (cat != null && cat != de.blight.common.ModelCategory.NONE) + return false; return m.modelPath().replace('\\', '/').toLowerCase().contains("/trees/"); } diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index b5062ac..b06ed8d 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -71,7 +71,8 @@ public class EditorApp extends Application { private ImageView viewport; private Label statusLabel; private Label camCoordsLabel; - private boolean launchGameAfterSave = false; + private boolean launchGameAfterSave = false; + private Runnable pendingAfterSave = null; private Button gamePlayBtn; // Spielen-Button im Seitenpanel private CheckBox debugConsoleCB; // Debug-Konsole bei Spielstart anzeigen private VBox toolPanel; @@ -345,6 +346,7 @@ public class EditorApp extends Application { private final java.util.List modelEditorEmitters = new java.util.ArrayList<>(); private javafx.scene.layout.VBox modelEditorLightBox = null; private javafx.scene.layout.VBox modelEditorEmitterBox = null; + private ComboBox modelEditorCategoryCB = null; private ComboBox modelEditorInteractableCB = null; private Spinner modelEditorInteractableXSpin = null; private Spinner modelEditorInteractableYSpin = null; @@ -587,6 +589,11 @@ public class EditorApp extends Application { launchGameAfterSave = false; startGameProcess(); } + if (pendingAfterSave != null) { + Runnable r = pendingAfterSave; + pendingAfterSave = null; + r.run(); + } } String treeMsg = input.treeGenStatusMsg; @@ -5659,6 +5666,11 @@ public class EditorApp extends Application { weltkartTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> { if (isSelected && !worldMapView.isLoaded()) worldMapView.loadAndRender(); }); + worldMapView.setRefreshAction(() -> { + input.saveRequested = true; + pendingAfterSave = worldMapView::loadAndRender; + setStatus("Speichere vor Weltkarten-Refresh…"); + }); worldMapView.setFullscreenCallbacks( () -> { // Placeholder ersetzt worldMapView im Tab → sauberes Detach @@ -7906,6 +7918,18 @@ public class EditorApp extends Application { // Initiale Gizmos pushen pushAttachmentsToJme(); + // ── Modell-Kategorie ────────────────────────────────────────────────── + Label catTitle = new Label("Kategorie:"); + catTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); + modelEditorCategoryCB = new ComboBox<>(); + modelEditorCategoryCB.getItems().addAll(de.blight.common.ModelCategory.values()); + modelEditorCategoryCB.setValue(meta.modelCategory() != null ? meta.modelCategory() : de.blight.common.ModelCategory.NONE); + modelEditorCategoryCB.setMaxWidth(Double.MAX_VALUE); + modelEditorCategoryCB.setConverter(new javafx.util.StringConverter<>() { + @Override public String toString(de.blight.common.ModelCategory c) { return c == null ? "" : c.getLabel(); } + @Override public de.blight.common.ModelCategory fromString(String s) { return de.blight.common.ModelCategory.fromString(s); } + }); + // ── Interaktivität ──────────────────────────────────────────────────── Label interactTitle = new Label("Interaktivität:"); interactTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); @@ -8119,7 +8143,10 @@ public class EditorApp extends Application { input.modelInteractableRotY, modelEditorFootstepSurfaceCB != null ? modelEditorFootstepSurfaceCB.getValue() - : "")); + : "", + modelEditorCategoryCB != null + ? modelEditorCategoryCB.getValue() + : de.blight.common.ModelCategory.NONE)); placeBtn.setOnAction(e -> { input.modelEditorCloseRequest = true; @@ -8140,7 +8167,7 @@ public class EditorApp extends Application { title, new Separator(), nameLabel, nameTF, - catLabel, catCB, + tagsLabel, tagsTF, new Separator(), scaleTitle, modelEditorUniformCB, scaleGrid, @@ -8160,6 +8187,8 @@ public class EditorApp extends Application { lightSectionLbl, modelEditorLightBox, addLightBtn, emitterSectionLbl, modelEditorEmitterBox, addEmitterBtn, new Separator(), + catTitle, modelEditorCategoryCB, + new Separator(), interactTitle, modelEditorInteractableCB, restPointBox, new Separator(), footstepTitle, modelEditorFootstepSurfaceCB, @@ -8656,7 +8685,8 @@ public class EditorApp extends Application { de.blight.common.model.InteractableType interactableType, float interactableOffsetX, float interactableOffsetY, float interactableOffsetZ, float interactableRotY, - String footstepSurface) { + String footstepSurface, + de.blight.common.ModelCategory modelCategory) { // Scale wird in j3o eingebrannt → Meta bekommt immer 1.0 (kein doppelter Scale beim Laden) de.blight.common.ModelMeta meta = new de.blight.common.ModelMeta( name, category, tags, 1f, 1f, 1f, uniform, @@ -8665,7 +8695,8 @@ public class EditorApp extends Application { lights, emitters, interactableType != null ? interactableType : de.blight.common.model.InteractableType.NONE, interactableOffsetX, interactableOffsetY, interactableOffsetZ, interactableRotY, - footstepSurface != null ? footstepSurface : ""); + footstepSurface != null ? footstepSurface : "", + modelCategory != null ? modelCategory : de.blight.common.ModelCategory.NONE); if (absolutePath == null || !absolutePath.toFile().exists()) { setStatus("Fehler: Modell-Datei nicht gefunden – Meta nicht gespeichert"); diff --git a/blight-editor/src/main/java/de/blight/editor/object/SceneObject.java b/blight-editor/src/main/java/de/blight/editor/object/SceneObject.java index e9fad45..1f68c3e 100644 --- a/blight-editor/src/main/java/de/blight/editor/object/SceneObject.java +++ b/blight-editor/src/main/java/de/blight/editor/object/SceneObject.java @@ -26,6 +26,7 @@ public class SceneObject extends PlacedObject { public float cullDistance = 120f; public String interactableType = ""; public String interactableId = ""; + public de.blight.common.ModelCategory modelCategory = de.blight.common.ModelCategory.NONE; public SceneObject(String modelPath, float worldX, float worldZ, float groundY, boolean solid) { diff --git a/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java b/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java index 4d9a1b6..0f6b614 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java @@ -723,6 +723,12 @@ public class EzTreeState extends BaseAppState { BinaryExporter.getInstance().save(lodRoot, outPath.toFile()); if (thumb != null) ThumbnailRenderer.saveSidecar(thumb, outPath, ASSET_ROOT); + String sp = subPath.toLowerCase(); + de.blight.common.ModelCategory autoCategory = + sp.contains("palm") ? de.blight.common.ModelCategory.PALM + : sp.contains("bush") ? de.blight.common.ModelCategory.BUSH + : sp.contains("trellis") ? de.blight.common.ModelCategory.BUSH + : de.blight.common.ModelCategory.TREE; de.blight.common.ModelMeta meta = new de.blight.common.ModelMeta( fileName, "trees", "", 1f, 1f, 1f, true, @@ -733,7 +739,7 @@ public class EzTreeState extends BaseAppState { "", "", 30f, 80f, 120f, java.util.List.of(), java.util.List.of(), de.blight.common.model.InteractableType.NONE, - 0f, 0.5f, 0f, 0f, "bark"); + 0f, 0.5f, 0f, 0f, "bark", autoCategory); de.blight.common.ModelMetaIO.save(meta, outPath); log.info("[EZ-Tree] Gespeichert: {}", outPath.toAbsolutePath()); diff --git a/blight-editor/src/main/java/de/blight/editor/state/ModelImportState.java b/blight-editor/src/main/java/de/blight/editor/state/ModelImportState.java index 65a9001..e2047e8 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/ModelImportState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/ModelImportState.java @@ -428,7 +428,7 @@ public class ModelImportState extends BaseAppState { def.interactableType(), def.interactableOffsetX(), def.interactableOffsetY(), def.interactableOffsetZ(), def.interactableRotY(), - def.footstepSurface()); + def.footstepSurface(), def.modelCategory()); ModelMetaIO.save(meta, outFile.toPath()); log.info("[ModelImport] Meta erstellt: solid={}", solid); } catch (IOException e) { diff --git a/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java b/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java index 542f213..98efa3b 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java @@ -193,7 +193,7 @@ public class SceneObjectState extends BaseAppState { so.castShadow, so.receiveShadow, so.lod1Path, so.lod2Path, so.lod1Distance, so.lod2Distance, so.cullDistance, - so.interactableType, so.interactableId)); + so.interactableType, so.interactableId, so.modelCategory)); } return list; } @@ -228,6 +228,7 @@ public class SceneObjectState extends BaseAppState { so.cullDistance = pm.cullDistance(); so.interactableType = pm.interactableType() != null ? pm.interactableType() : ""; so.interactableId = pm.interactableId() != null ? pm.interactableId() : ""; + so.modelCategory = pm.modelCategory() != null ? pm.modelCategory() : de.blight.common.ModelCategory.NONE; objects.add(so); animClips.add(pm.animClip() != null ? pm.animClip() : ""); @@ -685,8 +686,9 @@ public class SceneObjectState extends BaseAppState { activeRing = -1; // 2. Objekt-Treffer? (Highlight-Geometrien ignorieren) + // Im Platzier-Modus keine Selektion — Klick soll immer platzieren CollisionResults objHits = new CollisionResults(); - objectRoot.collideWith(ray, objHits); + if (input.pendingModelPath == null) objectRoot.collideWith(ray, objHits); if (objHits.size() > 0) { CollisionResult closest = null; for (int ci = 0; ci < objHits.size(); ci++) { @@ -826,9 +828,10 @@ public class SceneObjectState extends BaseAppState { if (meta != null) { so.lod1Path = meta.lod1Path() != null ? meta.lod1Path() : ""; so.lod2Path = meta.lod2Path() != null ? meta.lod2Path() : ""; - so.lod1Distance = meta.lod1Distance(); - so.lod2Distance = meta.lod2Distance(); - so.cullDistance = meta.cullDistance(); + so.lod1Distance = meta.lod1Distance(); + so.lod2Distance = meta.lod2Distance(); + so.cullDistance = meta.cullDistance(); + so.modelCategory = meta.modelCategory() != null ? meta.modelCategory() : de.blight.common.ModelCategory.NONE; if (meta.interactableType() != null && meta.interactableType() != de.blight.common.model.InteractableType.NONE) { so.interactableType = meta.interactableType().name(); diff --git a/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java index fd47458..ca19238 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java @@ -484,6 +484,15 @@ public class TreeGeneratorState extends BaseAppState { } catch (Exception te) { log.warn("[Blight-Baum] Thumbnail-Fehler: {}", te.getMessage()); } BinaryExporter.getInstance().save(treeNode, out); if (thumb != null) ThumbnailRenderer.saveSidecar(thumb, out.toPath(), ASSET_ROOT); + de.blight.common.ModelCategory autoCategory = + "bush".equals(treeType) ? de.blight.common.ModelCategory.BUSH : de.blight.common.ModelCategory.TREE; + de.blight.common.ModelMeta autoMeta = new de.blight.common.ModelMeta( + fileName, "trees", "", 1f, 1f, 1f, true, 0f, 0f, + true, true, true, 1f, 1f, "", "", 30f, 80f, 120f, + java.util.List.of(), java.util.List.of(), + de.blight.common.model.InteractableType.NONE, + 0f, 0.5f, 0f, 0f, "bark", autoCategory); + de.blight.common.ModelMetaIO.save(autoMeta, out.toPath()); log.info("[Blight-Baum] Gespeichert: {}", out.getAbsolutePath()); input.treeGenStatusMsg = "Gespeichert: Models/trees/" + treeType + "/" + sizeClass + "/" + fileName + ".j3o"; input.refreshAssets = true; diff --git a/blight-editor/src/main/java/de/blight/editor/ui/WorldMapView.java b/blight-editor/src/main/java/de/blight/editor/ui/WorldMapView.java index 5f4ca55..bf6bc1e 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/WorldMapView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/WorldMapView.java @@ -18,6 +18,7 @@ import javafx.scene.Cursor; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.control.*; +import javafx.scene.image.PixelFormat; import javafx.scene.image.WritableImage; import javafx.scene.input.MouseButton; import javafx.scene.layout.*; @@ -84,6 +85,7 @@ public class WorldMapView extends VBox { // Layer-Checkboxen (im MenuButton gebündelt) private final CheckBox cbTerrain = layerCheck("Gelände"); private final CheckBox cbWater = layerCheck("Wasser"); + private final CheckBox cbCoastline = layerCheck("Küstenlinie"); private final CheckBox cbWaterfalls = layerCheck("Wasserfälle"); private final CheckBox cbAreas = layerCheck("Areas"); private final CheckBox cbZones = layerCheck("Zonen"); @@ -101,6 +103,35 @@ public class WorldMapView extends VBox { private WritableImage mapFxImage; private BufferedImage mapBuffered; + // Canvas-level Tile-Overlay: zoom-unabhängige Kacheldarstellung + private static final int CAT_SIZE = 2048; + private byte[] categoryMap = null; + private float[] heightMap = null; // terrain+voxel, CAT_SIZE×CAT_SIZE (für Klippen) + private float[] heightMapTerrain = null; // terrain-only, CAT_SIZE×CAT_SIZE (für Wasser-Füllung) + private WritableImage tileOverlayImg = null; + private int[] tileOverlayPixels = null; + private int tileOverlayW = 0; + private int tileOverlayH = 0; + private WritableImage forestOverlayImg = null; + private int[] forestOverlayPixels = null; + private int forestOverlayW = 0; + private int forestOverlayH = 0; + // Gecachte Vorab-Berechnungen — nur bei Modell-Wechsel neu berechnen + private WorldMapRenderModel tileOverlayModel = null; + private float[] tileOverlayForestD = null; + private int[] tileOverlaySlots = null; + // Dirty-Flag: Overlay-Pixel neu rendern wenn Pan/Zoom oder Modell geändert + private double tileOverlayCachedScale = Double.NaN; + private double tileOverlayCachedPanX = Double.NaN; + private double tileOverlayCachedPanY = Double.NaN; + // Wasser-Fill-Overlay (Layer über Terrain-Textur, unter Küstenlinie) + private WritableImage waterFillImg = null; + private int[] waterFillPixels = null; + private int waterFillW = 0; + private int waterFillH = 0; + // PlacedWater-Wasserhoehe pro Zelle (0 = kein PlacedWater); fuer glatte Uebergaenge + private float[] waterHeightMap = null; + private List cachedAreas = new ArrayList<>(); private List cachedLocs = new ArrayList<>(); private WorldMapRenderModel currentModel = null; @@ -117,6 +148,7 @@ public class WorldMapView extends VBox { private Consumer teleportCallback = null; // [worldX, worldY, worldZ] private Runnable enterFullscreenCallback = null; private Runnable exitFullscreenCallback = null; + private Runnable refreshAction = null; private final AtomicBoolean loading = new AtomicBoolean(false); /** Verknüpft den Live-Voxel-Chunk-Zustand des Editors mit der Kartenansicht. */ @@ -129,6 +161,8 @@ public class WorldMapView extends VBox { public void setTeleportCallback(Consumer cb) { teleportCallback = cb; } /** Callbacks für Vollbild-Modus ein/aus (typisch: Eltern-Layout anpassen). */ + public void setRefreshAction(Runnable r) { this.refreshAction = r; } + public void setFullscreenCallbacks(Runnable enter, Runnable exit) { enterFullscreenCallback = enter; exitFullscreenCallback = exit; @@ -174,9 +208,10 @@ public class WorldMapView extends VBox { cbModels.selectedProperty().addListener((obs, o, n) -> redraw()); cbAreas.selectedProperty().addListener((obs, o, n) -> redraw()); cbLocations.selectedProperty().addListener((obs, o, n) -> redraw()); + cbCoastline.selectedProperty().addListener((obs, o, n) -> redraw()); MenuButton layerMenu = new MenuButton("Layer ▾"); - for (CheckBox cb : new CheckBox[]{cbTerrain, cbWater, cbWaterfalls, cbAreas, cbZones, cbLocations, cbModels}) { + for (CheckBox cb : new CheckBox[]{cbTerrain, cbWater, cbCoastline, cbWaterfalls, cbAreas, cbZones, cbLocations, cbModels}) { CustomMenuItem item = new CustomMenuItem(cb, false); item.setHideOnClick(false); layerMenu.getItems().add(item); @@ -185,7 +220,7 @@ public class WorldMapView extends VBox { // ── Buttons ─────────────────────────────────────────────────────────── Button refreshBtn = new Button("↻"); refreshBtn.setTooltip(new javafx.scene.control.Tooltip("Modell aktualisieren")); - refreshBtn.setOnAction(e -> loadAndRender()); + refreshBtn.setOnAction(e -> { if (refreshAction != null) refreshAction.run(); else loadAndRender(); }); Button exportBtn = new Button("⇩"); exportBtn.setTooltip(new javafx.scene.control.Tooltip("Als PNG exportieren…")); @@ -320,7 +355,14 @@ public class WorldMapView extends VBox { Platform.runLater(() -> statusLbl.setText("Rendere Karte…")); long t1 = System.currentTimeMillis(); BufferedImage bi = WorldMapRenderer.render(model, RENDER_SIZE, buildBackgroundOptions()); - log.debug("[WorldMap] Hintergrund-PNG gerendert ({}×{} px, {} ms)", + float[] hMap = WorldMapRenderer.buildTerrainSamples( + model.mapData(), CAT_SIZE, model.voxelSurface()); + float[] hMapT = WorldMapRenderer.buildTerrainSamples( + model.mapData(), CAT_SIZE, null); + byte[] catMap = WorldMapRenderer.buildCategoryMap( + model.mapData(), CAT_SIZE, model.voxelSurface(), model.waters(), model.slotColorsRGB()); + float[] whMap = WorldMapRenderer.buildWaterHeightMap(model.waters(), CAT_SIZE); + log.debug("[WorldMap] Hintergrund-PNG + Kategorie-Karte gerendert ({}×{} px, {} ms)", RENDER_SIZE, RENDER_SIZE, System.currentTimeMillis() - t1); Platform.runLater(() -> { @@ -329,6 +371,10 @@ public class WorldMapView extends VBox { cachedLocs = new ArrayList<>(model.locations()); mapBuffered = bi; mapFxImage = SwingFXUtils.toFXImage(bi, null); + heightMap = hMap; + heightMapTerrain = hMapT; + categoryMap = catMap; + waterHeightMap = whMap; fitToView(); redraw(); progress.setVisible(false); @@ -364,10 +410,21 @@ public class WorldMapView extends VBox { log.debug("[WorldMap] PNG-Rerender aus gecachtem Modell gestartet ({}×{})", RENDER_SIZE, RENDER_SIZE); long t0 = System.currentTimeMillis(); BufferedImage bi = WorldMapRenderer.render(model, RENDER_SIZE, buildBackgroundOptions()); + float[] hMap = WorldMapRenderer.buildTerrainSamples( + model.mapData(), CAT_SIZE, model.voxelSurface()); + float[] hMapT = WorldMapRenderer.buildTerrainSamples( + model.mapData(), CAT_SIZE, null); + byte[] catMap = WorldMapRenderer.buildCategoryMap( + model.mapData(), CAT_SIZE, model.voxelSurface(), model.waters(), model.slotColorsRGB()); + float[] whMap = WorldMapRenderer.buildWaterHeightMap(model.waters(), CAT_SIZE); log.debug("[WorldMap] PNG-Rerender fertig ({} ms)", System.currentTimeMillis() - t0); Platform.runLater(() -> { mapBuffered = bi; mapFxImage = SwingFXUtils.toFXImage(bi, null); + heightMap = hMap; + heightMapTerrain = hMapT; + categoryMap = catMap; + waterHeightMap = whMap; redraw(); progress.setVisible(false); statusLbl.setText("Bereit"); @@ -397,15 +454,246 @@ public class WorldMapView extends VBox { return; } + gc.setImageSmoothing(false); gc.drawImage(mapFxImage, panX, panY, mapFxImage.getWidth() * scale, mapFxImage.getHeight() * scale); - + gc.setImageSmoothing(true); + // Layer 1: Terrain-Textur (überall, opak) + drawTileOverlay(gc); + // Layer 2: Wasser darüber (glatte Küste via seaCoastPaths) if (cbWater.isSelected()) drawWaterOverlay(gc); + // Layer 3: Wald-Overlay (nach Wasser, damit Bäume nicht im Wasser erscheinen) + drawForestOverlay(gc); if (cbAreas.isSelected()) drawAreasOverlay(gc); if (cbLocations.isSelected()) drawLocationsOverlay(gc); - if (cbModels.isSelected()) drawTreeOverlay(gc); if (!isFullscreen) drawCameraIndicator(gc); } + // ── Canvas Tile-Overlay (zoom-unabhängige Kacheln) ──────────────────────── + + private static int tileSample(int[][] tilePx, int cat, + int u0, int u1, int v0, int v1, + float w00, float w10, float w01, float w11) { + if (cat >= 0 && cat < tilePx.length && tilePx[cat] != null) { + int[] tp = tilePx[cat]; + int p00=tp[v0*16+u0], p10=tp[v0*16+u1], p01=tp[v1*16+u0], p11=tp[v1*16+u1]; + int r=(int)((p00>>16&0xFF)*w00+(p10>>16&0xFF)*w10+(p01>>16&0xFF)*w01+(p11>>16&0xFF)*w11); + int g=(int)((p00>> 8&0xFF)*w00+(p10>> 8&0xFF)*w10+(p01>> 8&0xFF)*w01+(p11>> 8&0xFF)*w11); + int b=(int)((p00 &0xFF)*w00+(p10 &0xFF)*w10+(p01 &0xFF)*w01+(p11 &0xFF)*w11); + return (r<<16)|(g<<8)|b; + } + int c = Math.max(0, Math.min(WorldMapRenderer.TILE_FALLBACK_RGB.length-1, cat)); + int[] fb = WorldMapRenderer.TILE_FALLBACK_RGB[c]; + return (fb[0]<<16)|(fb[1]<<8)|fb[2]; + } + + private static float spBilerp(byte[] arr, int i00, int i10, int i01, int i11, float tx, float tz) { + float v00=(arr[i00]&0xFF)/255f, v10=(arr[i10]&0xFF)/255f; + float v01=(arr[i01]&0xFF)/255f, v11=(arr[i11]&0xFF)/255f; + return (v00*(1-tx)+v10*tx)*(1-tz)+(v01*(1-tx)+v11*tx)*tz; + } + + private void drawTileOverlay(GraphicsContext gc) { + if (heightMap == null || currentModel == null) return; + de.blight.common.MapData md = currentModel.mapData(); + if (md == null) return; + int[][] tilePx = currentModel.mapTilePixels(); + if (tilePx == null) return; + + int cW = (int) canvas.getWidth(); + int cH = (int) canvas.getHeight(); + if (cW <= 0 || cH <= 0) return; + + final float WH = WorldMapRenderer.WORLD_HALF; + final float WS = WorldMapRenderer.WORLD_SIZE; + final float TW = WorldMapRenderer.TILE_WU; + final float scaleF = (float) scale; + final float panXf = (float) panX; + final float panYf = (float) panY; + final float imgW = (float)(RENDER_SIZE - 1); + final int CS = CAT_SIZE; + final float[] hm = heightMap; + final float CS1 = CS - 1f; + final float CLIFF_LO = 0.4663f, CLIFF_RNG = 0.8391f - 0.4663f; + + // ── Modell-abhängige Vorab-Berechnungen cachen ──────────────────── + if (tileOverlayModel != currentModel) { + tileOverlayModel = currentModel; + tileOverlaySlots = WorldMapRenderer.computeSlotTileTypes(md, currentModel.slotColorsRGB()); + final int FD0 = 512; + final float forestRadius0 = 14f; + final int forestRCells0 = Math.max(1, (int)(forestRadius0 / (WS / FD0))); + final float[] fd0 = new float[FD0 * FD0]; + for (de.blight.common.PlacedModel pm : currentModel.models()) { + if (!WorldMapRenderer.isTree(pm)) continue; + int fx = Math.max(0, Math.min(FD0-1, (int)((pm.x() + WH) / WS * FD0))); + int fz = Math.max(0, Math.min(FD0-1, (int)((WH - pm.z()) / WS * FD0))); + for (int dz = -forestRCells0; dz <= forestRCells0; dz++) { + for (int dx = -forestRCells0; dx <= forestRCells0; dx++) { + if (dx*dx + dz*dz <= forestRCells0*forestRCells0) { + int nx = fx+dx, nz = fz+dz; + if (nx >= 0 && nx < FD0 && nz >= 0 && nz < FD0) fd0[nz*FD0+nx] += 1f; + } + } + } + } + tileOverlayForestD = fd0; + tileOverlayCachedScale = Double.NaN; + } + + // ── Pixel-Buffer nur neu berechnen wenn Pan/Zoom oder Canvas-Größe geändert ── + boolean needsRedraw = tileOverlayPixels == null || tileOverlayPixels.length != cW * cH + || tileOverlayW != cW || tileOverlayH != cH + || tileOverlayCachedScale != scale + || tileOverlayCachedPanX != panX + || tileOverlayCachedPanY != panY; + + if (!needsRedraw) { + gc.drawImage(tileOverlayImg, 0, 0); + return; + } + + if (tileOverlayPixels == null || tileOverlayPixels.length != cW * cH) { + tileOverlayPixels = new int[cW * cH]; + } + if (forestOverlayPixels == null || forestOverlayPixels.length != cW * cH) { + forestOverlayPixels = new int[cW * cH]; + } + if (tileOverlayImg == null || tileOverlayW != cW || tileOverlayH != cH) { + tileOverlayImg = new WritableImage(cW, cH); + tileOverlayW = cW; + tileOverlayH = cH; + } + if (forestOverlayImg == null || forestOverlayW != cW || forestOverlayH != cH) { + forestOverlayImg = new WritableImage(cW, cH); + forestOverlayW = cW; + forestOverlayH = cH; + } + tileOverlayCachedScale = scale; + tileOverlayCachedPanX = panX; + tileOverlayCachedPanY = panY; + + final int[] slotTiles = tileOverlaySlots; + final float[] forestDensity = tileOverlayForestD; + final int FD = 512; + + final byte[] spG = md.splatG, spB = md.splatB, spA = md.splatA; + final byte[] upR = md.upperSplatR, upG = md.upperSplatG, upB = md.upperSplatB, upA = md.upperSplatA; + final byte[] t3R = md.thirdSplatR, t3G = md.thirdSplatG, t3B = md.thirdSplatB, t3A = md.thirdSplatA; + final int SS = de.blight.common.MapData.SPLAT_SIZE; + + int[] tileArgbs = new int[8]; + + for (int cy = 0; cy < cH; cy++) { + float wz = (cy - panYf) / scaleF / imgW * WS - WH; + for (int cx = 0; cx < cW; cx++) { + float wx = (cx - panXf) / scaleF / imgW * WS - WH; + + if (wx < -WH || wx >= WH || wz < -WH || wz >= WH) { + tileOverlayPixels[cy * cW + cx] = 0x00000000; + continue; + } + + // ── Steigung aus heightMap → Cliff-Blend + float fmx = (wx + WH) / WS * CS1; + float fmz = (wz + WH) / WS * CS1; + int cmx = Math.max(0, Math.min(CS-1, (int)fmx)); + int cmz = Math.max(0, Math.min(CS-1, (int)fmz)); + int cmxR = Math.min(CS-1, cmx+2), cmxL = Math.max(0, cmx-2); + int cmzD = Math.min(CS-1, cmz+2), cmzU = Math.max(0, cmz-2); + float hC = hm[cmz*CS+cmx]; + float hR = hm[cmz*CS+cmxR]; if (hR < 0f) hR = hC; + float hL = hm[cmz*CS+cmxL]; if (hL < 0f) hL = hC; + float hD = hm[cmzD*CS+cmx]; if (hD < 0f) hD = hC; + float hU = hm[cmzU*CS+cmx]; if (hU < 0f) hU = hC; + float dx = (hR - hL) / (cmxR - cmxL), dz = (hD - hU) / (cmzD - cmzU); + float gradMag = (float)Math.sqrt((double)(dx*dx + dz*dz)); + float t = Math.max(0f, Math.min(1f, (gradMag - CLIFF_LO) / CLIFF_RNG)); + t = t * t * (3f - 2f * t); + + // ── Tile-UV bilinear + float rawU = (wx + WH) / TW, rawV = (wz + WH) / TW; + float uf16 = (rawU - (float)Math.floor(rawU)) * 16f; + float vf16 = (rawV - (float)Math.floor(rawV)) * 16f; + int u0 = Math.floorMod((int)uf16,16), u1 = Math.floorMod(u0+1,16); + int v0 = Math.floorMod((int)vf16,16), v1 = Math.floorMod(v0+1,16); + float ux = uf16-(float)Math.floor(uf16), vxf = vf16-(float)Math.floor(vf16); + float w00=(1-ux)*(1-vxf), w10=ux*(1-vxf), w01=(1-ux)*vxf, w11=ux*vxf; + + // ── Splatmap bilinear (Y-flip) + float sfx = Math.max(0f, Math.min(SS-1, (wx+WH)/WS*(SS-1))); + float sfz = Math.max(0f, Math.min(SS-1, (WH-wz)/WS*(SS-1))); + int sx0=(int)sfx, sx1=Math.min(sx0+1,SS-1); + int sz0=(int)sfz, sz1=Math.min(sz0+1,SS-1); + float stx=sfx-sx0, stz=sfz-sz0; + int bi00=sz0*SS+sx0, bi10=sz0*SS+sx1, bi01=sz1*SS+sx0, bi11=sz1*SS+sx1; + float wg = spBilerp(spG,bi00,bi10,bi01,bi11,stx,stz); + float wb = spBilerp(spB,bi00,bi10,bi01,bi11,stx,stz); + float wa = spBilerp(spA,bi00,bi10,bi01,bi11,stx,stz); + float up1 = spBilerp(upR,bi00,bi10,bi01,bi11,stx,stz); + float up2 = spBilerp(upG,bi00,bi10,bi01,bi11,stx,stz); + float up3 = spBilerp(upB,bi00,bi10,bi01,bi11,stx,stz); + float up4 = spBilerp(upA,bi00,bi10,bi01,bi11,stx,stz); + float t31 = spBilerp(t3R,bi00,bi10,bi01,bi11,stx,stz); + float t32 = spBilerp(t3G,bi00,bi10,bi01,bi11,stx,stz); + float t33 = spBilerp(t3B,bi00,bi10,bi01,bi11,stx,stz); + float t34 = spBilerp(t3A,bi00,bi10,bi01,bi11,stx,stz); + + // ── Alle Tile-Farben sampeln + for (int ti = 0; ti < 8; ti++) tileArgbs[ti] = tileSample(tilePx,ti,u0,u1,v0,v1,w00,w10,w01,w11); + + // ── Splatmap-Blend + int col = tileArgbs[slotTiles[0]]; + int cr=(col>>16)&0xFF, cgv=(col>>8)&0xFF, cbv=col&0xFF; + col=tileArgbs[slotTiles[1]]; cr=(int)(cr*(1-wg) +((col>>16)&0xFF)*wg ); cgv=(int)(cgv*(1-wg) +((col>>8)&0xFF)*wg ); cbv=(int)(cbv*(1-wg) +(col&0xFF)*wg ); + col=tileArgbs[slotTiles[2]]; cr=(int)(cr*(1-wb) +((col>>16)&0xFF)*wb ); cgv=(int)(cgv*(1-wb) +((col>>8)&0xFF)*wb ); cbv=(int)(cbv*(1-wb) +(col&0xFF)*wb ); + col=tileArgbs[slotTiles[3]]; cr=(int)(cr*(1-wa) +((col>>16)&0xFF)*wa ); cgv=(int)(cgv*(1-wa) +((col>>8)&0xFF)*wa ); cbv=(int)(cbv*(1-wa) +(col&0xFF)*wa ); + col=tileArgbs[slotTiles[4]]; cr=(int)(cr*(1-up1)+((col>>16)&0xFF)*up1); cgv=(int)(cgv*(1-up1)+((col>>8)&0xFF)*up1); cbv=(int)(cbv*(1-up1)+(col&0xFF)*up1); + col=tileArgbs[slotTiles[5]]; cr=(int)(cr*(1-up2)+((col>>16)&0xFF)*up2); cgv=(int)(cgv*(1-up2)+((col>>8)&0xFF)*up2); cbv=(int)(cbv*(1-up2)+(col&0xFF)*up2); + col=tileArgbs[slotTiles[6]]; cr=(int)(cr*(1-up3)+((col>>16)&0xFF)*up3); cgv=(int)(cgv*(1-up3)+((col>>8)&0xFF)*up3); cbv=(int)(cbv*(1-up3)+(col&0xFF)*up3); + col=tileArgbs[slotTiles[7]]; cr=(int)(cr*(1-up4)+((col>>16)&0xFF)*up4); cgv=(int)(cgv*(1-up4)+((col>>8)&0xFF)*up4); cbv=(int)(cbv*(1-up4)+(col&0xFF)*up4); + col=tileArgbs[slotTiles[8]]; cr=(int)(cr*(1-t31)+((col>>16)&0xFF)*t31); cgv=(int)(cgv*(1-t31)+((col>>8)&0xFF)*t31); cbv=(int)(cbv*(1-t31)+(col&0xFF)*t31); + col=tileArgbs[slotTiles[9]]; cr=(int)(cr*(1-t32)+((col>>16)&0xFF)*t32); cgv=(int)(cgv*(1-t32)+((col>>8)&0xFF)*t32); cbv=(int)(cbv*(1-t32)+(col&0xFF)*t32); + col=tileArgbs[slotTiles[10]];cr=(int)(cr*(1-t33)+((col>>16)&0xFF)*t33); cgv=(int)(cgv*(1-t33)+((col>>8)&0xFF)*t33); cbv=(int)(cbv*(1-t33)+(col&0xFF)*t33); + col=tileArgbs[slotTiles[11]];cr=(int)(cr*(1-t34)+((col>>16)&0xFF)*t34); cgv=(int)(cgv*(1-t34)+((col>>8)&0xFF)*t34); cbv=(int)(cbv*(1-t34)+(col&0xFF)*t34); + + // ── Wald-Dichte (bilinear) → separater Buffer, nach Wasser gezeichnet + float fsfx = Math.max(0f, Math.min(FD-1.001f, (wx + WH) / WS * FD)); + float fsfz = Math.max(0f, Math.min(FD-1.001f, (WH - wz) / WS * FD)); + int fsx0=(int)fsfx, fsx1=Math.min(fsx0+1,FD-1); + int fsz0=(int)fsfz, fsz1=Math.min(fsz0+1,FD-1); + float fstx=fsfx-fsx0, fstz=fsfz-fsz0; + float fd = (forestDensity[fsz0*FD+fsx0]*(1-fstx)+forestDensity[fsz0*FD+fsx1]*fstx)*(1-fstz) + + (forestDensity[fsz1*FD+fsx0]*(1-fstx)+forestDensity[fsz1*FD+fsx1]*fstx)*fstz; + // mind. 3 Bäume im Überlappungsbereich (fd > 2.0 bedeutet >2 Bäume) + forestOverlayPixels[cy * cW + cx] = (fd > 2.0f) + ? (0x80000000 | (tileArgbs[WorldMapRenderer.TILE_FOREST] & 0x00FFFFFF)) + : 0x00000000; + + int argb = (cr<<16)|(cgv<<8)|cbv; + + // ── Cliff-Overlay + if (t > 0f) { + int cArgb = tileArgbs[WorldMapRenderer.TILE_CLIFF]; + int ccr=(cArgb>>16)&0xFF, ccg=(cArgb>>8)&0xFF, ccb=cArgb&0xFF; + argb = ((int)(cr+(ccr-cr)*t)<<16)|((int)(cgv+(ccg-cgv)*t)<<8)|(int)(cbv+(ccb-cbv)*t); + } + + tileOverlayPixels[cy * cW + cx] = 0xFF000000 | (argb & 0x00FFFFFF); + } + } + + tileOverlayImg.getPixelWriter().setPixels( + 0, 0, cW, cH, PixelFormat.getIntArgbInstance(), tileOverlayPixels, 0, cW); + forestOverlayImg.getPixelWriter().setPixels( + 0, 0, cW, cH, PixelFormat.getIntArgbInstance(), forestOverlayPixels, 0, cW); + gc.drawImage(tileOverlayImg, 0, 0); + } + + private void drawForestOverlay(GraphicsContext gc) { + if (forestOverlayImg == null) return; + gc.drawImage(forestOverlayImg, 0, 0); + } + // ── Canvas-Vektor-Overlays ──────────────────────────────────────────────── private void drawAreasOverlay(GraphicsContext gc) { @@ -480,135 +768,96 @@ public class WorldMapView extends VBox { } private void drawWaterOverlay(GraphicsContext gc) { - if (currentModel == null) return; - boolean[] seaMask = currentModel.seaMask(); - float[] terrainSamples = currentModel.terrainSamples(); + if (currentModel == null || heightMap == null || waterHeightMap == null) return; + + int cW = (int) canvas.getWidth(), cH = (int) canvas.getHeight(); + if (cW <= 0 || cH <= 0) return; + + // ── Wasser-Füllung: blauer Pixel-Layer über dem Terrain-Overlay ────────── + if (waterFillPixels == null || waterFillPixels.length != cW * cH) { + waterFillPixels = new int[cW * cH]; + } + if (waterFillImg == null || waterFillW != cW || waterFillH != cH) { + waterFillImg = new WritableImage(cW, cH); + waterFillW = cW; + waterFillH = cH; + } + + final float WH = WorldMapRenderer.WORLD_HALF; + final float WS = WorldMapRenderer.WORLD_SIZE; + final float scaleF = (float) scale; + final float panXf = (float) panX; + final float panYf = (float) panY; + final float imgW = (float)(RENDER_SIZE - 1); + final int CS = CAT_SIZE; + final float[] hm = heightMap; // terrain+voxel + final float[] whm = waterHeightMap; // PlacedWater-Höhe pro Zelle (0 = kein PlacedWater) + final float CS1 = CS - 1f; + final int WR = 0xAD, WG = 0xD8, WB = 0xE6; // WATER_COLOR = #ADD8E6 + + for (int cy = 0; cy < cH; cy++) { + float wz = (cy - panYf) / scaleF / imgW * WS - WH; + for (int cx = 0; cx < cW; cx++) { + float wx = (cx - panXf) / scaleF / imgW * WS - WH; + + if (wx < -WH || wx >= WH || wz < -WH || wz >= WH) { + waterFillPixels[cy * cW + cx] = 0; + continue; + } + + float fmx = (wx + WH) / WS * CS1; + float fmz = (wz + WH) / WS * CS1; + int cmx = Math.max(0, Math.min(CS-1, (int) fmx)); + int cmz = Math.max(0, Math.min(CS-1, (int) fmz)); + int cmx1 = Math.min(CS-1, cmx+1), cmz1 = Math.min(CS-1, cmz+1); + float tx = fmx - cmx, tz = fmz - cmz; + + // Bilinear heightMap (terrain+voxel) + float hPx = hm[cmz *CS+cmx ] * (1-tx)*(1-tz) + + hm[cmz *CS+cmx1] * tx *(1-tz) + + hm[cmz1*CS+cmx ] * (1-tx)*tz + + hm[cmz1*CS+cmx1] * tx *tz; + + // Bilinear waterHeightMap → PlacedWater-Schwelle (0 = nur Ozean) + float wH = whm[cmz *CS+cmx ] * (1-tx)*(1-tz) + + whm[cmz *CS+cmx1] * tx *(1-tz) + + whm[cmz1*CS+cmx ] * (1-tx)*tz + + whm[cmz1*CS+cmx1] * tx *tz; + + // delta < 0 → unter Wasseroberfläche; Übergangsband ±0.5 WU + float delta = hPx - wH; + int alpha; + if (delta <= -0.5f) { + alpha = 255; + } else if (delta < 0.5f) { + alpha = Math.max(0, Math.min(255, (int)((0.5f - delta) * 255f))); + } else { + alpha = 0; + } + + waterFillPixels[cy * cW + cx] = (alpha << 24) | (WR << 16) | (WG << 8) | WB; + } + } + + waterFillImg.getPixelWriter().setPixels( + 0, 0, cW, cH, PixelFormat.getIntArgbInstance(), waterFillPixels, 0, cW); + gc.drawImage(waterFillImg, 0, 0); + + // ── Küstenlinie – geglättete Pfade über der Wasser-Füllung ───────────── List seaCoastPaths = currentModel.coastPaths(); - - // ── Seewellen (Terrain < 0) ────────────────────────────────────────────── - if (seaMask != null && cbWater.isSelected()) { - // Wellengröße in Weltkoordinaten (skaliert mit Zoom, damit Bildschirmgröße = WAVE_W) - double pxPerWorld = (RENDER_SIZE - 1) * scale / WorldMapRenderer.WORLD_SIZE; - double wWave = WAVE_W / pxPerWorld; - double wSpX = wWave * 1.8, wSpY = wWave * 1.05, wAmp = WAVE_W * 0.2; - - // Sichtbarer Weltbereich - double cW = canvas.getWidth(), cH = canvas.getHeight(); - double wxMin = toWorldX(0), wxMax = toWorldX(cW); - double wzMin = toWorldZ(0), wzMax = toWorldZ(cH); - wxMin = Math.max(-WorldMapRenderer.WORLD_HALF, wxMin); - wzMin = Math.max(-WorldMapRenderer.WORLD_HALF, wzMin); - wxMax = Math.min(WorldMapRenderer.WORLD_HALF, wxMax); - wzMax = Math.min(WorldMapRenderer.WORLD_HALF, wzMax); - - gc.setStroke(Color.BLACK); - gc.setLineWidth(0.8); - gc.setLineDashes(null); - - // Gitter-Startzeile: auf Weltgitter einrasten → Wellen bleiben beim Panning stabil - long rowBase = (long)Math.floor((wzMin - wSpY * 0.5) / wSpY); - for (long rowIdx = rowBase; rowIdx * wSpY + wSpY * 0.5 < wzMax; rowIdx++) { - double wz = rowIdx * wSpY + wSpY * 0.5; - double rowOff = (rowIdx & 1L) == 1L ? wSpX * 0.5 : 0; - long colBase = (long)Math.floor((wxMin - rowOff) / wSpX); - for (long colIdx = colBase; colIdx * wSpX + rowOff < wxMax; colIdx++) { - double wx = colIdx * wSpX + rowOff; - double jx = Math.abs(Math.sin(rowIdx * 73.1 + colIdx * 157.3)) * wSpX * 0.25; - double jy = Math.sin(rowIdx * 211.7 + colIdx * 89.5) * wSpY * 0.2; - double owx = wx + jx, owz = wz + jy; - // Seekartenmaske prüfen (Mitte und Ende der Welle) - int mxS = (int)((owx + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE - 1)); - int mzS = (int)((owz + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE - 1)); - int mxE = (int)((owx + wWave + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE - 1)); - if (mxS < 0 || mxS >= SEA_MASK_SIZE || mzS < 0 || mzS >= SEA_MASK_SIZE) continue; - if (!seaMask[mzS * SEA_MASK_SIZE + mxS]) continue; - if (mxE < 0 || mxE >= SEA_MASK_SIZE || !seaMask[mzS * SEA_MASK_SIZE + mxE]) continue; - // Canvas-Koordinaten; Breite = WAVE_W px, Amplitude = wAmp px - double ox = toCanvasX((float)owx), oy = toCanvasZ((float)owz); - double ex = ox + WAVE_W; - gc.beginPath(); - gc.moveTo(ox, oy); - gc.bezierCurveTo(ox + WAVE_W*0.3, oy - wAmp, ox + WAVE_W*0.7, oy + wAmp, ex, oy); - gc.stroke(); - } - } - } - - List waters = currentModel.waters(); - if (waters.isEmpty()) return; - - final double spX = WAVE_W * 1.8, spY = WAVE_W * 1.05, wmA = WAVE_W * 0.2; - - for (PlacedWater w : waters) { - float[] wx = w.pointsX(), wz = w.pointsZ(); - int n = wx.length; - if (n < 3) continue; - - float wh = w.waterHeight(); - - double[] cx = new double[n], cz = new double[n]; - for (int i = 0; i < n; i++) { cx[i] = toCanvasX(wx[i]); cz[i] = toCanvasZ(wz[i]); } - - double minX = cx[0], maxX = cx[0], minZ = cz[0], maxZ = cz[0]; - for (int i = 1; i < n; i++) { - if (cx[i] < minX) minX = cx[i]; if (cx[i] > maxX) maxX = cx[i]; - if (cz[i] < minZ) minZ = cz[i]; if (cz[i] > maxZ) maxZ = cz[i]; - } - - // Wellenmarken: nur wenn Fläche groß genug für mindestens eine Welle - if ((maxX - minX) > WAVE_W * 1.5 && (maxZ - minZ) > spY) { - gc.save(); - gc.beginPath(); - gc.moveTo(cx[0], cz[0]); - for (int i = 1; i < n; i++) { gc.lineTo(cx[i], cz[i]); } - gc.closePath(); - gc.clip(); - - gc.setStroke(Color.BLACK); - gc.setLineWidth(0.8); - gc.setLineDashes(null); - - int row = 0; - for (double wy = minZ + spY * 0.5; wy < maxZ; wy += spY, row++) { - double rowOff = (row & 1) == 1 ? spX * 0.5 : 0; - int col = 0; - for (double wxx = minX + rowOff; wxx + WAVE_W <= maxX; wxx += spX, col++) { - double jx = Math.abs(Math.sin(row * 73.1 + col * 157.3)) * spX * 0.25; - double jy = Math.sin(row * 211.7 + col * 89.5) * spY * 0.2; - double ox = wxx + jx, oy = wy + jy; - if (ox < minX || ox + WAVE_W > maxX || oy < minZ || oy > maxZ) { continue; } - // Höhenprüfung: Terrain muss unter waterHeight liegen - if (terrainSamples != null) { - float owx = toWorldX(ox), owz = toWorldZ(oy); - int mxS = Math.max(0, Math.min(SEA_MASK_SIZE-1, (int)((owx + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE-1)))); - int mzS = Math.max(0, Math.min(SEA_MASK_SIZE-1, (int)((owz + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE-1)))); - if (terrainSamples[mzS * SEA_MASK_SIZE + mxS] >= wh) { continue; } - } - gc.beginPath(); - gc.moveTo(ox, oy); - gc.bezierCurveTo(ox + WAVE_W*0.3, oy - wmA, ox + WAVE_W*0.7, oy + wmA, ox + WAVE_W, oy); - gc.stroke(); - } - } - gc.restore(); - } - - } - - // ── Seeküstenlinie – verbundene Pfade, runde Joins, weicher Halo ──────────── - if (seaCoastPaths != null && !seaCoastPaths.isEmpty()) { - double cW = canvas.getWidth(), cH = canvas.getHeight(); + if (cbCoastline.isSelected() && seaCoastPaths != null && !seaCoastPaths.isEmpty()) { + double canvasW = canvas.getWidth(), canvasH = canvas.getHeight(); gc.setLineDashes(null); gc.setLineCap(StrokeLineCap.ROUND); gc.setLineJoin(StrokeLineJoin.ROUND); gc.setStroke(Color.color(0, 0, 0, 0.22)); gc.setLineWidth(7.0); - for (float[][] path : seaCoastPaths) { drawCoastPath(gc, path, cW, cH, 8); } + for (float[][] path : seaCoastPaths) { drawCoastPath(gc, path, canvasW, canvasH, 8); } gc.setStroke(Color.BLACK); gc.setLineWidth(3.0); - for (float[][] path : seaCoastPaths) { drawCoastPath(gc, path, cW, cH, 4); } + for (float[][] path : seaCoastPaths) { drawCoastPath(gc, path, canvasW, canvasH, 4); } } } @@ -811,35 +1060,17 @@ public class WorldMapView extends VBox { List locs, List waters, List models, List waterfalls, float[] voxelSurface) { - int[] slotColors = computeSlotColors(mapData); - boolean[] mask = WorldMapRenderer.buildSeaMask(mapData, SEA_MASK_SIZE, voxelSurface); - float[] tSamp = buildTerrainSamples(mapData, SEA_MASK_SIZE); + int[] slotColors = computeSlotColors(mapData); + int[][] mapTiles = WorldMapRenderer.loadMapTiles(findAssetRoot()); + boolean[] mask = WorldMapRenderer.buildSeaMask(mapData, SEA_MASK_SIZE, voxelSurface); + float[] tSamp = WorldMapRenderer.buildTerrainSamples(mapData, SEA_MASK_SIZE, voxelSurface); List paths = buildSeaCoastPaths(buildCoastSegsMS(mapData, waters, SEA_MASK_SIZE, voxelSurface)); List treeList = models.stream() .filter(WorldMapRenderer::isTree).collect(Collectors.toList()); List> tClusters = clusterTreeModels(treeList, 40f); return new WorldMapRenderModel( mapData, areas, zones, locs, waters, models, waterfalls, slotColors, - mask, voxelSurface, tSamp, paths, tClusters); - } - - private static float[] buildTerrainSamples(MapData mapData, int size) { - int TV = MapData.TERRAIN_VERTS; - int UV = MapData.UPPER_VERTS; - float[] s = new float[size * size]; - for (int mz = 0; mz < size; mz++) { - for (int mx = 0; mx < size; mx++) { - int hx = Math.min((int)(mx / (double)(size-1) * (TV-1)), TV-1); - int hz = Math.min((int)(mz / (double)(size-1) * (TV-1)), TV-1); - int ux = Math.min((int)(mx / (double)(size-1) * (UV-1)), UV-1); - int uz = Math.min((int)(mz / (double)(size-1) * (UV-1)), UV-1); - float h = mapData.terrainHeight[hz * TV + hx]; - float upper = mapData.upperTop[uz * UV + ux]; - if (upper > 0f && upper > h) { h = upper; } - s[mz * size + mx] = h; - } - } - return s; + mask, voxelSurface, tSamp, paths, tClusters, mapTiles); } // Erzeugt interpolierte Marching-Squares-Segmente für Meer + Wasserflächen. @@ -1147,7 +1378,7 @@ public class WorldMapView extends VBox { cbTerrain.isSelected(), cbTerrain.isSelected(), cbWater.isSelected(), - true, // Wellen im Export-PNG gebacken + false, cbAreas.isSelected(), cbZones.isSelected(), cbLocations.isSelected(), diff --git a/blight-game/src/main/java/de/blight/game/state/MinimapState.java b/blight-game/src/main/java/de/blight/game/state/MinimapState.java index 19cf440..866605b 100644 --- a/blight-game/src/main/java/de/blight/game/state/MinimapState.java +++ b/blight-game/src/main/java/de/blight/game/state/MinimapState.java @@ -101,6 +101,7 @@ public class MinimapState extends BaseAppState { private Material fogMat; // Minimap-Nebel private Material fogFullMat; // Vollbild-Nebel (eigene Overlay-Bounds) private float fogTime = 0f; + private float posUpdateTimer = 0f; // ── Blur-Filter (aktiv wenn Vollbild-Karte offen) ───────────────────────── @@ -217,8 +218,12 @@ public class MinimapState extends BaseAppState { if (fogMat != null) { fogMat.setFloat("Time", fogTime); } if (fogFullMat != null) { fogFullMat.setFloat("Time", fogTime); } + posUpdateTimer += tpf; + boolean doPosUpdate = posUpdateTimer >= 1f; + if (doPosUpdate) posUpdateTimer = 0f; + if (!fullMapOpen) { - updateMinimapUV(px, pz); + if (doPosUpdate) updateMinimapUV(px, pz); } else { if (fmDragging) { Vector2f cursor = app.getInputManager().getCursorPosition(); @@ -234,7 +239,7 @@ public class MinimapState extends BaseAppState { scheduleVectorRedraw(); } } - updateFullMapPlayerDot(px, pz); + if (doPosUpdate) updateFullMapPlayerDot(px, pz); } }