Weltkarte: Kachel-Overlay, Wald-Overlay, ModelCategory-System, Selektions-Fix
- Terrain-Textur-Overlay (Splatmap-Blend, Klippen, Wald) als Dirty-Flag-Cache - Wald wird nach Wasser gerendert, 50% Deckkraft, Schwelle 3+ Bäume - Speichern vor Weltkarten-Refresh (save-then-load via pendingAfterSave) - ModelCategory-System: Baum/Palme/Busch/Gebäude; Ordner-Kategorie im UI versteckt - Selektion nur mit Auswahl-Werkzeug (kein versehentliches Selektieren im Platzier-Modus) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,7 +33,8 @@ public record ModelMeta(
|
|||||||
float interactableOffsetY,
|
float interactableOffsetY,
|
||||||
float interactableOffsetZ,
|
float interactableOffsetZ,
|
||||||
float interactableRotY,
|
float interactableRotY,
|
||||||
String footstepSurface
|
String footstepSurface,
|
||||||
|
de.blight.common.ModelCategory modelCategory
|
||||||
) {
|
) {
|
||||||
/** Lichtquelle relativ zum Modell-Ursprung. */
|
/** Lichtquelle relativ zum Modell-Ursprung. */
|
||||||
public record AttachedLight(
|
public record AttachedLight(
|
||||||
@@ -53,6 +54,7 @@ public record ModelMeta(
|
|||||||
"", "", 30f, 80f, 120f,
|
"", "", 30f, 80f, 120f,
|
||||||
List.of(), List.of(),
|
List.of(), List.of(),
|
||||||
de.blight.common.model.InteractableType.NONE,
|
de.blight.common.model.InteractableType.NONE,
|
||||||
0f, 0.5f, 0f, 0f, "");
|
0f, 0.5f, 0f, 0f, "",
|
||||||
|
de.blight.common.ModelCategory.NONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ public final class ModelMetaIO {
|
|||||||
p.setProperty("interactableOffsetZ", String.valueOf(m.interactableOffsetZ()));
|
p.setProperty("interactableOffsetZ", String.valueOf(m.interactableOffsetZ()));
|
||||||
p.setProperty("interactableRotY", String.valueOf(m.interactableRotY()));
|
p.setProperty("interactableRotY", String.valueOf(m.interactableRotY()));
|
||||||
p.setProperty("footstepSurface", m.footstepSurface() != null ? m.footstepSurface() : "");
|
p.setProperty("footstepSurface", m.footstepSurface() != null ? m.footstepSurface() : "");
|
||||||
|
p.setProperty("modelCategory", m.modelCategory() != null ? m.modelCategory().name() : "NONE");
|
||||||
|
|
||||||
// Anhänge: Lichter
|
// Anhänge: Lichter
|
||||||
List<ModelMeta.AttachedLight> lights = m.attachedLights();
|
List<ModelMeta.AttachedLight> lights = m.attachedLights();
|
||||||
@@ -140,7 +141,8 @@ public final class ModelMetaIO {
|
|||||||
parseFloat(p, "interactableOffsetY", 0.5f),
|
parseFloat(p, "interactableOffsetY", 0.5f),
|
||||||
parseFloat(p, "interactableOffsetZ", 0f),
|
parseFloat(p, "interactableOffsetZ", 0f),
|
||||||
parseFloat(p, "interactableRotY", 0f),
|
parseFloat(p, "interactableRotY", 0f),
|
||||||
p.getProperty("footstepSurface", "")
|
p.getProperty("footstepSurface", ""),
|
||||||
|
de.blight.common.ModelCategory.fromString(p.getProperty("modelCategory", "NONE"))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,5 +22,7 @@ public record PlacedModel(
|
|||||||
/** "CRAFTING_TABLE" / "BED" / "" für kein Interactable. */
|
/** "CRAFTING_TABLE" / "BED" / "" für kein Interactable. */
|
||||||
String interactableType,
|
String interactableType,
|
||||||
/** ID des verknüpften Interactables (CraftingTableType-Name oder Bett-UUID); "" wenn nicht gesetzt. */
|
/** 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
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ public final class PlacedModelIO {
|
|||||||
Path p = getPath();
|
Path p = getPath();
|
||||||
Files.createDirectories(p.getParent());
|
Files.createDirectories(p.getParent());
|
||||||
try (BufferedWriter w = Files.newBufferedWriter(p)) {
|
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();
|
w.newLine();
|
||||||
for (PlacedModel m : models) {
|
for (PlacedModel m : models) {
|
||||||
w.write(String.format(Locale.ROOT,
|
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.modelPath(),
|
||||||
m.x(), m.y(), m.z(),
|
m.x(), m.y(), m.z(),
|
||||||
m.rotY(), m.scale(),
|
m.rotY(), m.scale(),
|
||||||
@@ -41,7 +41,8 @@ public final class PlacedModelIO {
|
|||||||
m.castShadow(), m.receiveShadow(),
|
m.castShadow(), m.receiveShadow(),
|
||||||
nvl(m.lod1Path()), nvl(m.lod2Path()),
|
nvl(m.lod1Path()), nvl(m.lod2Path()),
|
||||||
m.lod1Distance(), m.lod2Distance(), m.cullDistance(),
|
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;
|
float cullDistance = f.length > 20 ? parseFloat(f[20], 120f) : 120f;
|
||||||
String interactableType = f.length > 21 ? f[21] : "";
|
String interactableType = f.length > 21 ? f[21] : "";
|
||||||
String interactableId = f.length > 22 ? f[22] : "";
|
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,
|
list.add(new PlacedModel(modelPath, x, y, z,
|
||||||
rotY, rotX, rotZ, scale, solid,
|
rotY, rotX, rotZ, scale, solid,
|
||||||
texPath, nmPath, matPath, meshFile, animClip,
|
texPath, nmPath, matPath, meshFile, animClip,
|
||||||
castShadow, receiveShadow,
|
castShadow, receiveShadow,
|
||||||
lod1Path, lod2Path, lod1Distance, lod2Distance, cullDistance,
|
lod1Path, lod2Path, lod1Distance, lod2Distance, cullDistance,
|
||||||
interactableType, interactableId));
|
interactableType, interactableId, modelCategory));
|
||||||
} catch (NumberFormatException ignored) {}
|
} catch (NumberFormatException ignored) {}
|
||||||
}
|
}
|
||||||
return list;
|
return list;
|
||||||
|
|||||||
@@ -25,5 +25,6 @@ public record WorldMapRenderModel(
|
|||||||
float[] voxelSurface, // oberster solider Voxel-Y pro UPPER_VERTS-Zelle; NaN = keine Daten
|
float[] voxelSurface, // oberster solider Voxel-Y pro UPPER_VERTS-Zelle; NaN = keine Daten
|
||||||
float[] terrainSamples, // Höhenwerte bei SEA_MASK_SIZE (für Wellen-Prüfung)
|
float[] terrainSamples, // Höhenwerte bei SEA_MASK_SIZE (für Wellen-Prüfung)
|
||||||
List<float[][]> coastPaths, // geglättete Marching-Squares Küstenpfade (Weltkoord.)
|
List<float[][]> coastPaths, // geglättete Marching-Squares Küstenpfade (Weltkoord.)
|
||||||
List<List<PlacedModel>> treeClusters
|
List<List<PlacedModel>> treeClusters,
|
||||||
|
int[][] mapTilePixels // 6 Kacheln (TILE_WATER..TILE_ROCK), je 16×16 ARGB; null = Fallback
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package de.blight.common.map;
|
|||||||
import de.blight.common.*;
|
import de.blight.common.*;
|
||||||
import de.blight.common.model.Location;
|
import de.blight.common.model.Location;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.geom.Path2D;
|
import java.awt.geom.Path2D;
|
||||||
@@ -21,6 +23,8 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public final class WorldMapRenderer {
|
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_HALF = 1024f;
|
||||||
public static final float WORLD_SIZE = 2048f;
|
public static final float WORLD_SIZE = 2048f;
|
||||||
/** Auflösung des voxelSurface-Bake-Arrays (1 Zelle pro Welteinheit). */
|
/** 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_G = { 148, 82, 115, 184, 80, 90, 60, 100 };
|
||||||
private static final int[] DEF_SLOT_B = { 46, 64, 77, 128, 40, 60, 50, 70 };
|
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() {}
|
private WorldMapRenderer() {}
|
||||||
|
|
||||||
/** Hauptmethode: rendert das Hintergrund-PNG aus einem vorberechneten Modell. */
|
/** Hauptmethode: rendert das Hintergrund-PNG aus einem vorberechneten Modell. */
|
||||||
public static BufferedImage render(WorldMapRenderModel model, int targetSize, RenderOptions opts) {
|
public static BufferedImage render(WorldMapRenderModel model, int targetSize, RenderOptions opts) {
|
||||||
MapData m = model.mapData();
|
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 ────────────────────────────
|
// 1. Hoehenfeld
|
||||||
int UV = MapData.UPPER_VERTS;
|
float[] heights = buildTerrainSamples(m, targetSize, model.voxelSurface());
|
||||||
float[] heights = new float[targetSize * targetSize];
|
|
||||||
// Meerserkennung basiert nur auf terrainHeight (ohne upperTop/voxelSurface):
|
|
||||||
// bakeVoxelHeights trägt Voxel-Höhen in upperTop ein, was sonst
|
|
||||||
// Voxel auf dem Meeresboden fälschlich als Landfläche klassifiziert.
|
|
||||||
boolean[] seaPixels = new boolean[targetSize * targetSize];
|
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;
|
// 2. Wasser-Maske: Ozean + PlacedWater vorab berechnen
|
||||||
for (int py = 0; py < targetSize; py++) {
|
boolean[] isWater = java.util.Arrays.copyOf(seaPixels, seaPixels.length);
|
||||||
for (int px = 0; px < targetSize; px++) {
|
if (opts.showWater()) {
|
||||||
int hx = Math.min((int)((float) px / (targetSize - 1) * (TV - 1)), TV - 1);
|
for (PlacedWater w : model.waters()) {
|
||||||
int hz = Math.min((int)((float) py / (targetSize - 1) * (TV - 1)), TV - 1);
|
int[] wxPx = worldToPixels(w.pointsX(), targetSize);
|
||||||
float ux_f = (float) px / (targetSize - 1) * (UV - 1);
|
int[] wzPx = worldToPixels(w.pointsZ(), targetSize);
|
||||||
float uz_f = (float) py / (targetSize - 1) * (UV - 1);
|
Polygon waterPoly = new Polygon(wxPx, wzPx, wxPx.length);
|
||||||
float baseH = m.terrainHeight[hz * TV + hx];
|
Rectangle bb = waterPoly.getBounds();
|
||||||
float h = baseH;
|
float wh = w.waterHeight();
|
||||||
float upper = bilerpPos(m.upperTop, ux_f, uz_f, UV);
|
for (int py = Math.max(0, bb.y); py <= Math.min(targetSize-1, bb.y+bb.height); py++) {
|
||||||
if (upper > 0f && upper > h) { h = upper; }
|
for (int px = Math.max(0, bb.x); px <= Math.min(targetSize-1, bb.x+bb.width); px++) {
|
||||||
// Voxel-Oberfläche: direkte 1:1-Abfrage des hochauflösenden Bake-Arrays
|
if (waterPoly.contains(px, py) && heights[py * targetSize + px] < wh)
|
||||||
// seaH = max(terrain, voxelSurface): Voxel über Y=0 → Land, darunter → Meer
|
isWater[py * targetSize + px] = true;
|
||||||
float seaH = baseH;
|
|
||||||
if (model.voxelSurface() != null) {
|
|
||||||
int vsPx = Math.min((int)((float) px / (targetSize - 1) * (VS - 1)), VS - 1);
|
|
||||||
int vsPz = Math.min((int)((float) py / (targetSize - 1) * (VS - 1)), VS - 1);
|
|
||||||
float vs = model.voxelSurface()[vsPz * VS + vsPx];
|
|
||||||
if (!Float.isNaN(vs)) {
|
|
||||||
h = vs;
|
|
||||||
if (vs > seaH) { seaH = vs; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
heights[py * targetSize + px] = h;
|
|
||||||
seaPixels[py * targetSize + px] = (seaH < 0f);
|
|
||||||
if (h < minH) minH = h;
|
|
||||||
if (h > maxH) maxH = h;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float heightRange = Math.max(0.01f, maxH - minH);
|
// 3. Tile-Rendering: tileIdx + Rohfarbe pro Pixel speichern
|
||||||
|
float cellW = WORLD_SIZE / (targetSize - 1);
|
||||||
// ── 2. Pixel-Farben berechnen ─────────────────────────────────────────
|
int SS = MapData.SPLAT_SIZE;
|
||||||
BufferedImage img = new BufferedImage(targetSize, targetSize, BufferedImage.TYPE_INT_RGB);
|
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++) {
|
for (int py = 0; py < targetSize; py++) {
|
||||||
|
float wz = -WORLD_HALF + py * cellW;
|
||||||
for (int px = 0; px < targetSize; px++) {
|
for (int px = 0; px < targetSize; px++) {
|
||||||
float h = heights[py * targetSize + px];
|
float wx = -WORLD_HALF + px * cellW;
|
||||||
float hn = (h - minH) / heightRange;
|
int idx = py * targetSize + px;
|
||||||
|
|
||||||
int r, g, b;
|
int tileIdx;
|
||||||
|
int tr, tg, tb;
|
||||||
if (opts.showSplatColors()) {
|
if (isWater[idx]) {
|
||||||
int sx = Math.min((int)((float) px / (targetSize - 1) * (SS - 1)), SS - 1);
|
tileIdx = TILE_WATER;
|
||||||
int sz = Math.min((int)((float)(targetSize - 1 - py) / (targetSize - 1) * (SS - 1)), SS - 1);
|
tr = waterR; tg = waterG; tb = waterB;
|
||||||
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);
|
|
||||||
} else {
|
} else {
|
||||||
int lum = clamp(30 + (int)(hn * 200));
|
int dPx = Math.max(1, (int)(0.25f / cellW));
|
||||||
r = g = b = lum;
|
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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
rawRGB[idx] = (tr << 16) | (tg << 8) | tb;
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kuwahara-Filter: lässt das Terrain wie gemalt wirken
|
// 4. Kuestenlinie: Sub-Pixel-Sampling fuer pixelgenaue Wasser-Land-Grenze.
|
||||||
if (opts.showSplatColors()) { applyKuwahara(img, 3); }
|
// 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();
|
Graphics2D gfx = img.createGraphics();
|
||||||
gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
float lineW = Math.max(1.5f, targetSize / 400f);
|
float lineW = Math.max(1.5f, targetSize / 400f);
|
||||||
|
|
||||||
// Wasser
|
// Wasser
|
||||||
if (opts.showWater()) {
|
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 ──────────────────────────────────────────────────────
|
// ── Wellenmarken ──────────────────────────────────────────────────────
|
||||||
if (opts.showWaterWaves()) {
|
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
|
// Wasserfälle – klassisches Kartensymbol: Kammlinie + Kaskaden-Linien
|
||||||
@@ -471,7 +531,7 @@ public final class WorldMapRenderer {
|
|||||||
return render(new WorldMapRenderModel(
|
return render(new WorldMapRenderModel(
|
||||||
input.mapData(), input.areas(), input.zones(), input.locations(),
|
input.mapData(), input.areas(), input.zones(), input.locations(),
|
||||||
input.waters(), input.models(), null,
|
input.waters(), input.models(), null,
|
||||||
input.slotColorsRGB(), null, null, null, null, null
|
input.slotColorsRGB(), null, null, null, null, null, null
|
||||||
), targetSize, opts);
|
), 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()) {
|
if (opts.showAreas()) {
|
||||||
@@ -885,13 +906,264 @@ public final class WorldMapRenderer {
|
|||||||
* @param baseFallbacks Fallback-Pfade für leere Base-Slots (kann null sein)
|
* @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
|
* @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<PlacedWater> 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<PlacedWater> 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 = {
|
int[] result = {
|
||||||
71,148, 46, 115, 82, 64, 140,115, 77, 204,184,128,
|
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.terrainTextures, baseFallbacks, assetRoot, result, 0);
|
||||||
sampleTexColors(mapData.upperTextures, null, assetRoot, result, 4);
|
sampleTexColors(mapData.upperTextures, null, assetRoot, result, 4);
|
||||||
|
sampleTexColors(mapData.thirdTextures, null, assetRoot, result, 8);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -963,6 +1235,11 @@ public final class WorldMapRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isTree(PlacedModel m) {
|
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/");
|
return m.modelPath().replace('\\', '/').toLowerCase().contains("/trees/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ public class EditorApp extends Application {
|
|||||||
private ImageView viewport;
|
private ImageView viewport;
|
||||||
private Label statusLabel;
|
private Label statusLabel;
|
||||||
private Label camCoordsLabel;
|
private Label camCoordsLabel;
|
||||||
private boolean launchGameAfterSave = false;
|
private boolean launchGameAfterSave = false;
|
||||||
|
private Runnable pendingAfterSave = null;
|
||||||
private Button gamePlayBtn; // Spielen-Button im Seitenpanel
|
private Button gamePlayBtn; // Spielen-Button im Seitenpanel
|
||||||
private CheckBox debugConsoleCB; // Debug-Konsole bei Spielstart anzeigen
|
private CheckBox debugConsoleCB; // Debug-Konsole bei Spielstart anzeigen
|
||||||
private VBox toolPanel;
|
private VBox toolPanel;
|
||||||
@@ -345,6 +346,7 @@ public class EditorApp extends Application {
|
|||||||
private final java.util.List<de.blight.common.ModelMeta.AttachedEmitter> modelEditorEmitters = new java.util.ArrayList<>();
|
private final java.util.List<de.blight.common.ModelMeta.AttachedEmitter> modelEditorEmitters = new java.util.ArrayList<>();
|
||||||
private javafx.scene.layout.VBox modelEditorLightBox = null;
|
private javafx.scene.layout.VBox modelEditorLightBox = null;
|
||||||
private javafx.scene.layout.VBox modelEditorEmitterBox = null;
|
private javafx.scene.layout.VBox modelEditorEmitterBox = null;
|
||||||
|
private ComboBox<de.blight.common.ModelCategory> modelEditorCategoryCB = null;
|
||||||
private ComboBox<de.blight.common.model.InteractableType> modelEditorInteractableCB = null;
|
private ComboBox<de.blight.common.model.InteractableType> modelEditorInteractableCB = null;
|
||||||
private Spinner<Double> modelEditorInteractableXSpin = null;
|
private Spinner<Double> modelEditorInteractableXSpin = null;
|
||||||
private Spinner<Double> modelEditorInteractableYSpin = null;
|
private Spinner<Double> modelEditorInteractableYSpin = null;
|
||||||
@@ -587,6 +589,11 @@ public class EditorApp extends Application {
|
|||||||
launchGameAfterSave = false;
|
launchGameAfterSave = false;
|
||||||
startGameProcess();
|
startGameProcess();
|
||||||
}
|
}
|
||||||
|
if (pendingAfterSave != null) {
|
||||||
|
Runnable r = pendingAfterSave;
|
||||||
|
pendingAfterSave = null;
|
||||||
|
r.run();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String treeMsg = input.treeGenStatusMsg;
|
String treeMsg = input.treeGenStatusMsg;
|
||||||
@@ -5659,6 +5666,11 @@ public class EditorApp extends Application {
|
|||||||
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.setRefreshAction(() -> {
|
||||||
|
input.saveRequested = true;
|
||||||
|
pendingAfterSave = worldMapView::loadAndRender;
|
||||||
|
setStatus("Speichere vor Weltkarten-Refresh…");
|
||||||
|
});
|
||||||
worldMapView.setFullscreenCallbacks(
|
worldMapView.setFullscreenCallbacks(
|
||||||
() -> {
|
() -> {
|
||||||
// Placeholder ersetzt worldMapView im Tab → sauberes Detach
|
// Placeholder ersetzt worldMapView im Tab → sauberes Detach
|
||||||
@@ -7906,6 +7918,18 @@ public class EditorApp extends Application {
|
|||||||
// Initiale Gizmos pushen
|
// Initiale Gizmos pushen
|
||||||
pushAttachmentsToJme();
|
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 ────────────────────────────────────────────────────
|
// ── Interaktivität ────────────────────────────────────────────────────
|
||||||
Label interactTitle = new Label("Interaktivität:");
|
Label interactTitle = new Label("Interaktivität:");
|
||||||
interactTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;");
|
interactTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;");
|
||||||
@@ -8119,7 +8143,10 @@ public class EditorApp extends Application {
|
|||||||
input.modelInteractableRotY,
|
input.modelInteractableRotY,
|
||||||
modelEditorFootstepSurfaceCB != null
|
modelEditorFootstepSurfaceCB != null
|
||||||
? modelEditorFootstepSurfaceCB.getValue()
|
? modelEditorFootstepSurfaceCB.getValue()
|
||||||
: ""));
|
: "",
|
||||||
|
modelEditorCategoryCB != null
|
||||||
|
? modelEditorCategoryCB.getValue()
|
||||||
|
: de.blight.common.ModelCategory.NONE));
|
||||||
|
|
||||||
placeBtn.setOnAction(e -> {
|
placeBtn.setOnAction(e -> {
|
||||||
input.modelEditorCloseRequest = true;
|
input.modelEditorCloseRequest = true;
|
||||||
@@ -8140,7 +8167,7 @@ public class EditorApp extends Application {
|
|||||||
title,
|
title,
|
||||||
new Separator(),
|
new Separator(),
|
||||||
nameLabel, nameTF,
|
nameLabel, nameTF,
|
||||||
catLabel, catCB,
|
|
||||||
tagsLabel, tagsTF,
|
tagsLabel, tagsTF,
|
||||||
new Separator(),
|
new Separator(),
|
||||||
scaleTitle, modelEditorUniformCB, scaleGrid,
|
scaleTitle, modelEditorUniformCB, scaleGrid,
|
||||||
@@ -8160,6 +8187,8 @@ public class EditorApp extends Application {
|
|||||||
lightSectionLbl, modelEditorLightBox, addLightBtn,
|
lightSectionLbl, modelEditorLightBox, addLightBtn,
|
||||||
emitterSectionLbl, modelEditorEmitterBox, addEmitterBtn,
|
emitterSectionLbl, modelEditorEmitterBox, addEmitterBtn,
|
||||||
new Separator(),
|
new Separator(),
|
||||||
|
catTitle, modelEditorCategoryCB,
|
||||||
|
new Separator(),
|
||||||
interactTitle, modelEditorInteractableCB, restPointBox,
|
interactTitle, modelEditorInteractableCB, restPointBox,
|
||||||
new Separator(),
|
new Separator(),
|
||||||
footstepTitle, modelEditorFootstepSurfaceCB,
|
footstepTitle, modelEditorFootstepSurfaceCB,
|
||||||
@@ -8656,7 +8685,8 @@ public class EditorApp extends Application {
|
|||||||
de.blight.common.model.InteractableType interactableType,
|
de.blight.common.model.InteractableType interactableType,
|
||||||
float interactableOffsetX, float interactableOffsetY,
|
float interactableOffsetX, float interactableOffsetY,
|
||||||
float interactableOffsetZ, float interactableRotY,
|
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)
|
// 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(
|
de.blight.common.ModelMeta meta = new de.blight.common.ModelMeta(
|
||||||
name, category, tags, 1f, 1f, 1f, uniform,
|
name, category, tags, 1f, 1f, 1f, uniform,
|
||||||
@@ -8665,7 +8695,8 @@ public class EditorApp extends Application {
|
|||||||
lights, emitters,
|
lights, emitters,
|
||||||
interactableType != null ? interactableType : de.blight.common.model.InteractableType.NONE,
|
interactableType != null ? interactableType : de.blight.common.model.InteractableType.NONE,
|
||||||
interactableOffsetX, interactableOffsetY, interactableOffsetZ, interactableRotY,
|
interactableOffsetX, interactableOffsetY, interactableOffsetZ, interactableRotY,
|
||||||
footstepSurface != null ? footstepSurface : "");
|
footstepSurface != null ? footstepSurface : "",
|
||||||
|
modelCategory != null ? modelCategory : de.blight.common.ModelCategory.NONE);
|
||||||
|
|
||||||
if (absolutePath == null || !absolutePath.toFile().exists()) {
|
if (absolutePath == null || !absolutePath.toFile().exists()) {
|
||||||
setStatus("Fehler: Modell-Datei nicht gefunden – Meta nicht gespeichert");
|
setStatus("Fehler: Modell-Datei nicht gefunden – Meta nicht gespeichert");
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public class SceneObject extends PlacedObject {
|
|||||||
public float cullDistance = 120f;
|
public float cullDistance = 120f;
|
||||||
public String interactableType = "";
|
public String interactableType = "";
|
||||||
public String interactableId = "";
|
public String interactableId = "";
|
||||||
|
public de.blight.common.ModelCategory modelCategory = de.blight.common.ModelCategory.NONE;
|
||||||
|
|
||||||
public SceneObject(String modelPath, float worldX, float worldZ, float groundY,
|
public SceneObject(String modelPath, float worldX, float worldZ, float groundY,
|
||||||
boolean solid) {
|
boolean solid) {
|
||||||
|
|||||||
@@ -723,6 +723,12 @@ public class EzTreeState extends BaseAppState {
|
|||||||
BinaryExporter.getInstance().save(lodRoot, outPath.toFile());
|
BinaryExporter.getInstance().save(lodRoot, outPath.toFile());
|
||||||
if (thumb != null) ThumbnailRenderer.saveSidecar(thumb, outPath, ASSET_ROOT);
|
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(
|
de.blight.common.ModelMeta meta = new de.blight.common.ModelMeta(
|
||||||
fileName, "trees", "",
|
fileName, "trees", "",
|
||||||
1f, 1f, 1f, true,
|
1f, 1f, 1f, true,
|
||||||
@@ -733,7 +739,7 @@ public class EzTreeState extends BaseAppState {
|
|||||||
"", "", 30f, 80f, 120f,
|
"", "", 30f, 80f, 120f,
|
||||||
java.util.List.of(), java.util.List.of(),
|
java.util.List.of(), java.util.List.of(),
|
||||||
de.blight.common.model.InteractableType.NONE,
|
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);
|
de.blight.common.ModelMetaIO.save(meta, outPath);
|
||||||
|
|
||||||
log.info("[EZ-Tree] Gespeichert: {}", outPath.toAbsolutePath());
|
log.info("[EZ-Tree] Gespeichert: {}", outPath.toAbsolutePath());
|
||||||
|
|||||||
@@ -428,7 +428,7 @@ public class ModelImportState extends BaseAppState {
|
|||||||
def.interactableType(),
|
def.interactableType(),
|
||||||
def.interactableOffsetX(), def.interactableOffsetY(),
|
def.interactableOffsetX(), def.interactableOffsetY(),
|
||||||
def.interactableOffsetZ(), def.interactableRotY(),
|
def.interactableOffsetZ(), def.interactableRotY(),
|
||||||
def.footstepSurface());
|
def.footstepSurface(), def.modelCategory());
|
||||||
ModelMetaIO.save(meta, outFile.toPath());
|
ModelMetaIO.save(meta, outFile.toPath());
|
||||||
log.info("[ModelImport] Meta erstellt: solid={}", solid);
|
log.info("[ModelImport] Meta erstellt: solid={}", solid);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ public class SceneObjectState extends BaseAppState {
|
|||||||
so.castShadow, so.receiveShadow,
|
so.castShadow, so.receiveShadow,
|
||||||
so.lod1Path, so.lod2Path,
|
so.lod1Path, so.lod2Path,
|
||||||
so.lod1Distance, so.lod2Distance, so.cullDistance,
|
so.lod1Distance, so.lod2Distance, so.cullDistance,
|
||||||
so.interactableType, so.interactableId));
|
so.interactableType, so.interactableId, so.modelCategory));
|
||||||
}
|
}
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
@@ -228,6 +228,7 @@ public class SceneObjectState extends BaseAppState {
|
|||||||
so.cullDistance = pm.cullDistance();
|
so.cullDistance = pm.cullDistance();
|
||||||
so.interactableType = pm.interactableType() != null ? pm.interactableType() : "";
|
so.interactableType = pm.interactableType() != null ? pm.interactableType() : "";
|
||||||
so.interactableId = pm.interactableId() != null ? pm.interactableId() : "";
|
so.interactableId = pm.interactableId() != null ? pm.interactableId() : "";
|
||||||
|
so.modelCategory = pm.modelCategory() != null ? pm.modelCategory() : de.blight.common.ModelCategory.NONE;
|
||||||
objects.add(so);
|
objects.add(so);
|
||||||
animClips.add(pm.animClip() != null ? pm.animClip() : "");
|
animClips.add(pm.animClip() != null ? pm.animClip() : "");
|
||||||
|
|
||||||
@@ -685,8 +686,9 @@ public class SceneObjectState extends BaseAppState {
|
|||||||
activeRing = -1;
|
activeRing = -1;
|
||||||
|
|
||||||
// 2. Objekt-Treffer? (Highlight-Geometrien ignorieren)
|
// 2. Objekt-Treffer? (Highlight-Geometrien ignorieren)
|
||||||
|
// Im Platzier-Modus keine Selektion — Klick soll immer platzieren
|
||||||
CollisionResults objHits = new CollisionResults();
|
CollisionResults objHits = new CollisionResults();
|
||||||
objectRoot.collideWith(ray, objHits);
|
if (input.pendingModelPath == null) objectRoot.collideWith(ray, objHits);
|
||||||
if (objHits.size() > 0) {
|
if (objHits.size() > 0) {
|
||||||
CollisionResult closest = null;
|
CollisionResult closest = null;
|
||||||
for (int ci = 0; ci < objHits.size(); ci++) {
|
for (int ci = 0; ci < objHits.size(); ci++) {
|
||||||
@@ -826,9 +828,10 @@ public class SceneObjectState extends BaseAppState {
|
|||||||
if (meta != null) {
|
if (meta != null) {
|
||||||
so.lod1Path = meta.lod1Path() != null ? meta.lod1Path() : "";
|
so.lod1Path = meta.lod1Path() != null ? meta.lod1Path() : "";
|
||||||
so.lod2Path = meta.lod2Path() != null ? meta.lod2Path() : "";
|
so.lod2Path = meta.lod2Path() != null ? meta.lod2Path() : "";
|
||||||
so.lod1Distance = meta.lod1Distance();
|
so.lod1Distance = meta.lod1Distance();
|
||||||
so.lod2Distance = meta.lod2Distance();
|
so.lod2Distance = meta.lod2Distance();
|
||||||
so.cullDistance = meta.cullDistance();
|
so.cullDistance = meta.cullDistance();
|
||||||
|
so.modelCategory = meta.modelCategory() != null ? meta.modelCategory() : de.blight.common.ModelCategory.NONE;
|
||||||
if (meta.interactableType() != null
|
if (meta.interactableType() != null
|
||||||
&& meta.interactableType() != de.blight.common.model.InteractableType.NONE) {
|
&& meta.interactableType() != de.blight.common.model.InteractableType.NONE) {
|
||||||
so.interactableType = meta.interactableType().name();
|
so.interactableType = meta.interactableType().name();
|
||||||
|
|||||||
@@ -484,6 +484,15 @@ public class TreeGeneratorState extends BaseAppState {
|
|||||||
} catch (Exception te) { log.warn("[Blight-Baum] Thumbnail-Fehler: {}", te.getMessage()); }
|
} catch (Exception te) { log.warn("[Blight-Baum] Thumbnail-Fehler: {}", te.getMessage()); }
|
||||||
BinaryExporter.getInstance().save(treeNode, out);
|
BinaryExporter.getInstance().save(treeNode, out);
|
||||||
if (thumb != null) ThumbnailRenderer.saveSidecar(thumb, out.toPath(), ASSET_ROOT);
|
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());
|
log.info("[Blight-Baum] Gespeichert: {}", out.getAbsolutePath());
|
||||||
input.treeGenStatusMsg = "Gespeichert: Models/trees/" + treeType + "/" + sizeClass + "/" + fileName + ".j3o";
|
input.treeGenStatusMsg = "Gespeichert: Models/trees/" + treeType + "/" + sizeClass + "/" + fileName + ".j3o";
|
||||||
input.refreshAssets = true;
|
input.refreshAssets = true;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import javafx.scene.Cursor;
|
|||||||
import javafx.scene.canvas.Canvas;
|
import javafx.scene.canvas.Canvas;
|
||||||
import javafx.scene.canvas.GraphicsContext;
|
import javafx.scene.canvas.GraphicsContext;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.image.PixelFormat;
|
||||||
import javafx.scene.image.WritableImage;
|
import javafx.scene.image.WritableImage;
|
||||||
import javafx.scene.input.MouseButton;
|
import javafx.scene.input.MouseButton;
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
@@ -84,6 +85,7 @@ public class WorldMapView extends VBox {
|
|||||||
// Layer-Checkboxen (im MenuButton gebündelt)
|
// Layer-Checkboxen (im MenuButton gebündelt)
|
||||||
private final CheckBox cbTerrain = layerCheck("Gelände");
|
private final CheckBox cbTerrain = layerCheck("Gelände");
|
||||||
private final CheckBox cbWater = layerCheck("Wasser");
|
private final CheckBox cbWater = layerCheck("Wasser");
|
||||||
|
private final CheckBox cbCoastline = layerCheck("Küstenlinie");
|
||||||
private final CheckBox cbWaterfalls = layerCheck("Wasserfälle");
|
private final CheckBox cbWaterfalls = layerCheck("Wasserfälle");
|
||||||
private final CheckBox cbAreas = layerCheck("Areas");
|
private final CheckBox cbAreas = layerCheck("Areas");
|
||||||
private final CheckBox cbZones = layerCheck("Zonen");
|
private final CheckBox cbZones = layerCheck("Zonen");
|
||||||
@@ -101,6 +103,35 @@ public class WorldMapView extends VBox {
|
|||||||
private WritableImage mapFxImage;
|
private WritableImage mapFxImage;
|
||||||
private BufferedImage mapBuffered;
|
private BufferedImage mapBuffered;
|
||||||
|
|
||||||
|
// Canvas-level Tile-Overlay: zoom-unabhängige Kacheldarstellung
|
||||||
|
private static final int CAT_SIZE = 2048;
|
||||||
|
private byte[] categoryMap = null;
|
||||||
|
private float[] heightMap = null; // terrain+voxel, CAT_SIZE×CAT_SIZE (für Klippen)
|
||||||
|
private float[] heightMapTerrain = null; // terrain-only, CAT_SIZE×CAT_SIZE (für Wasser-Füllung)
|
||||||
|
private WritableImage tileOverlayImg = null;
|
||||||
|
private int[] tileOverlayPixels = null;
|
||||||
|
private int tileOverlayW = 0;
|
||||||
|
private int tileOverlayH = 0;
|
||||||
|
private WritableImage forestOverlayImg = null;
|
||||||
|
private int[] forestOverlayPixels = null;
|
||||||
|
private int forestOverlayW = 0;
|
||||||
|
private int forestOverlayH = 0;
|
||||||
|
// Gecachte Vorab-Berechnungen — nur bei Modell-Wechsel neu berechnen
|
||||||
|
private WorldMapRenderModel tileOverlayModel = null;
|
||||||
|
private float[] tileOverlayForestD = null;
|
||||||
|
private int[] tileOverlaySlots = null;
|
||||||
|
// Dirty-Flag: Overlay-Pixel neu rendern wenn Pan/Zoom oder Modell geändert
|
||||||
|
private double tileOverlayCachedScale = Double.NaN;
|
||||||
|
private double tileOverlayCachedPanX = Double.NaN;
|
||||||
|
private double tileOverlayCachedPanY = Double.NaN;
|
||||||
|
// Wasser-Fill-Overlay (Layer über Terrain-Textur, unter Küstenlinie)
|
||||||
|
private WritableImage waterFillImg = null;
|
||||||
|
private int[] waterFillPixels = null;
|
||||||
|
private int waterFillW = 0;
|
||||||
|
private int waterFillH = 0;
|
||||||
|
// PlacedWater-Wasserhoehe pro Zelle (0 = kein PlacedWater); fuer glatte Uebergaenge
|
||||||
|
private float[] waterHeightMap = null;
|
||||||
|
|
||||||
private List<PlacedArea> cachedAreas = new ArrayList<>();
|
private List<PlacedArea> cachedAreas = new ArrayList<>();
|
||||||
private List<Location> cachedLocs = new ArrayList<>();
|
private List<Location> cachedLocs = new ArrayList<>();
|
||||||
private WorldMapRenderModel currentModel = null;
|
private WorldMapRenderModel currentModel = null;
|
||||||
@@ -117,6 +148,7 @@ public class WorldMapView extends VBox {
|
|||||||
private Consumer<float[]> teleportCallback = null; // [worldX, worldY, worldZ]
|
private Consumer<float[]> teleportCallback = null; // [worldX, worldY, worldZ]
|
||||||
private Runnable enterFullscreenCallback = null;
|
private Runnable enterFullscreenCallback = null;
|
||||||
private Runnable exitFullscreenCallback = null;
|
private Runnable exitFullscreenCallback = null;
|
||||||
|
private Runnable refreshAction = null;
|
||||||
private final AtomicBoolean loading = new AtomicBoolean(false);
|
private final AtomicBoolean loading = new AtomicBoolean(false);
|
||||||
|
|
||||||
/** Verknüpft den Live-Voxel-Chunk-Zustand des Editors mit der Kartenansicht. */
|
/** Verknüpft den Live-Voxel-Chunk-Zustand des Editors mit der Kartenansicht. */
|
||||||
@@ -129,6 +161,8 @@ public class WorldMapView extends VBox {
|
|||||||
public void setTeleportCallback(Consumer<float[]> cb) { teleportCallback = cb; }
|
public void setTeleportCallback(Consumer<float[]> cb) { teleportCallback = cb; }
|
||||||
|
|
||||||
/** Callbacks für Vollbild-Modus ein/aus (typisch: Eltern-Layout anpassen). */
|
/** 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) {
|
public void setFullscreenCallbacks(Runnable enter, Runnable exit) {
|
||||||
enterFullscreenCallback = enter;
|
enterFullscreenCallback = enter;
|
||||||
exitFullscreenCallback = exit;
|
exitFullscreenCallback = exit;
|
||||||
@@ -174,9 +208,10 @@ public class WorldMapView extends VBox {
|
|||||||
cbModels.selectedProperty().addListener((obs, o, n) -> redraw());
|
cbModels.selectedProperty().addListener((obs, o, n) -> redraw());
|
||||||
cbAreas.selectedProperty().addListener((obs, o, n) -> redraw());
|
cbAreas.selectedProperty().addListener((obs, o, n) -> redraw());
|
||||||
cbLocations.selectedProperty().addListener((obs, o, n) -> redraw());
|
cbLocations.selectedProperty().addListener((obs, o, n) -> redraw());
|
||||||
|
cbCoastline.selectedProperty().addListener((obs, o, n) -> redraw());
|
||||||
|
|
||||||
MenuButton layerMenu = new MenuButton("Layer ▾");
|
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);
|
CustomMenuItem item = new CustomMenuItem(cb, false);
|
||||||
item.setHideOnClick(false);
|
item.setHideOnClick(false);
|
||||||
layerMenu.getItems().add(item);
|
layerMenu.getItems().add(item);
|
||||||
@@ -185,7 +220,7 @@ public class WorldMapView extends VBox {
|
|||||||
// ── Buttons ───────────────────────────────────────────────────────────
|
// ── Buttons ───────────────────────────────────────────────────────────
|
||||||
Button refreshBtn = new Button("↻");
|
Button refreshBtn = new Button("↻");
|
||||||
refreshBtn.setTooltip(new javafx.scene.control.Tooltip("Modell aktualisieren"));
|
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("⇩");
|
Button exportBtn = new Button("⇩");
|
||||||
exportBtn.setTooltip(new javafx.scene.control.Tooltip("Als PNG exportieren…"));
|
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…"));
|
Platform.runLater(() -> statusLbl.setText("Rendere Karte…"));
|
||||||
long t1 = System.currentTimeMillis();
|
long t1 = System.currentTimeMillis();
|
||||||
BufferedImage bi = WorldMapRenderer.render(model, RENDER_SIZE, buildBackgroundOptions());
|
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);
|
RENDER_SIZE, RENDER_SIZE, System.currentTimeMillis() - t1);
|
||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
@@ -329,6 +371,10 @@ public class WorldMapView extends VBox {
|
|||||||
cachedLocs = new ArrayList<>(model.locations());
|
cachedLocs = new ArrayList<>(model.locations());
|
||||||
mapBuffered = bi;
|
mapBuffered = bi;
|
||||||
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
||||||
|
heightMap = hMap;
|
||||||
|
heightMapTerrain = hMapT;
|
||||||
|
categoryMap = catMap;
|
||||||
|
waterHeightMap = whMap;
|
||||||
fitToView();
|
fitToView();
|
||||||
redraw();
|
redraw();
|
||||||
progress.setVisible(false);
|
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);
|
log.debug("[WorldMap] PNG-Rerender aus gecachtem Modell gestartet ({}×{})", RENDER_SIZE, RENDER_SIZE);
|
||||||
long t0 = System.currentTimeMillis();
|
long t0 = System.currentTimeMillis();
|
||||||
BufferedImage bi = WorldMapRenderer.render(model, RENDER_SIZE, buildBackgroundOptions());
|
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);
|
log.debug("[WorldMap] PNG-Rerender fertig ({} ms)", System.currentTimeMillis() - t0);
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
mapBuffered = bi;
|
mapBuffered = bi;
|
||||||
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
||||||
|
heightMap = hMap;
|
||||||
|
heightMapTerrain = hMapT;
|
||||||
|
categoryMap = catMap;
|
||||||
|
waterHeightMap = whMap;
|
||||||
redraw();
|
redraw();
|
||||||
progress.setVisible(false);
|
progress.setVisible(false);
|
||||||
statusLbl.setText("Bereit");
|
statusLbl.setText("Bereit");
|
||||||
@@ -397,15 +454,246 @@ public class WorldMapView extends VBox {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gc.setImageSmoothing(false);
|
||||||
gc.drawImage(mapFxImage, panX, panY, mapFxImage.getWidth() * scale, mapFxImage.getHeight() * scale);
|
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);
|
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 (cbAreas.isSelected()) drawAreasOverlay(gc);
|
||||||
if (cbLocations.isSelected()) drawLocationsOverlay(gc);
|
if (cbLocations.isSelected()) drawLocationsOverlay(gc);
|
||||||
if (cbModels.isSelected()) drawTreeOverlay(gc);
|
|
||||||
if (!isFullscreen) drawCameraIndicator(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 ────────────────────────────────────────────────
|
// ── Canvas-Vektor-Overlays ────────────────────────────────────────────────
|
||||||
|
|
||||||
private void drawAreasOverlay(GraphicsContext gc) {
|
private void drawAreasOverlay(GraphicsContext gc) {
|
||||||
@@ -480,135 +768,96 @@ public class WorldMapView extends VBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void drawWaterOverlay(GraphicsContext gc) {
|
private void drawWaterOverlay(GraphicsContext gc) {
|
||||||
if (currentModel == null) return;
|
if (currentModel == null || heightMap == null || waterHeightMap == null) return;
|
||||||
boolean[] seaMask = currentModel.seaMask();
|
|
||||||
float[] terrainSamples = currentModel.terrainSamples();
|
int cW = (int) canvas.getWidth(), cH = (int) canvas.getHeight();
|
||||||
|
if (cW <= 0 || cH <= 0) return;
|
||||||
|
|
||||||
|
// ── Wasser-Füllung: blauer Pixel-Layer über dem Terrain-Overlay ──────────
|
||||||
|
if (waterFillPixels == null || waterFillPixels.length != cW * cH) {
|
||||||
|
waterFillPixels = new int[cW * cH];
|
||||||
|
}
|
||||||
|
if (waterFillImg == null || waterFillW != cW || waterFillH != cH) {
|
||||||
|
waterFillImg = new WritableImage(cW, cH);
|
||||||
|
waterFillW = cW;
|
||||||
|
waterFillH = cH;
|
||||||
|
}
|
||||||
|
|
||||||
|
final float WH = WorldMapRenderer.WORLD_HALF;
|
||||||
|
final float WS = WorldMapRenderer.WORLD_SIZE;
|
||||||
|
final float scaleF = (float) scale;
|
||||||
|
final float panXf = (float) panX;
|
||||||
|
final float panYf = (float) panY;
|
||||||
|
final float imgW = (float)(RENDER_SIZE - 1);
|
||||||
|
final int CS = CAT_SIZE;
|
||||||
|
final float[] hm = heightMap; // terrain+voxel
|
||||||
|
final float[] whm = waterHeightMap; // PlacedWater-Höhe pro Zelle (0 = kein PlacedWater)
|
||||||
|
final float CS1 = CS - 1f;
|
||||||
|
final int WR = 0xAD, WG = 0xD8, WB = 0xE6; // WATER_COLOR = #ADD8E6
|
||||||
|
|
||||||
|
for (int cy = 0; cy < cH; cy++) {
|
||||||
|
float wz = (cy - panYf) / scaleF / imgW * WS - WH;
|
||||||
|
for (int cx = 0; cx < cW; cx++) {
|
||||||
|
float wx = (cx - panXf) / scaleF / imgW * WS - WH;
|
||||||
|
|
||||||
|
if (wx < -WH || wx >= WH || wz < -WH || wz >= WH) {
|
||||||
|
waterFillPixels[cy * cW + cx] = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
float fmx = (wx + WH) / WS * CS1;
|
||||||
|
float fmz = (wz + WH) / WS * CS1;
|
||||||
|
int cmx = Math.max(0, Math.min(CS-1, (int) fmx));
|
||||||
|
int cmz = Math.max(0, Math.min(CS-1, (int) fmz));
|
||||||
|
int cmx1 = Math.min(CS-1, cmx+1), cmz1 = Math.min(CS-1, cmz+1);
|
||||||
|
float tx = fmx - cmx, tz = fmz - cmz;
|
||||||
|
|
||||||
|
// Bilinear heightMap (terrain+voxel)
|
||||||
|
float hPx = hm[cmz *CS+cmx ] * (1-tx)*(1-tz)
|
||||||
|
+ hm[cmz *CS+cmx1] * tx *(1-tz)
|
||||||
|
+ hm[cmz1*CS+cmx ] * (1-tx)*tz
|
||||||
|
+ hm[cmz1*CS+cmx1] * tx *tz;
|
||||||
|
|
||||||
|
// Bilinear waterHeightMap → PlacedWater-Schwelle (0 = nur Ozean)
|
||||||
|
float wH = whm[cmz *CS+cmx ] * (1-tx)*(1-tz)
|
||||||
|
+ whm[cmz *CS+cmx1] * tx *(1-tz)
|
||||||
|
+ whm[cmz1*CS+cmx ] * (1-tx)*tz
|
||||||
|
+ whm[cmz1*CS+cmx1] * tx *tz;
|
||||||
|
|
||||||
|
// delta < 0 → unter Wasseroberfläche; Übergangsband ±0.5 WU
|
||||||
|
float delta = hPx - wH;
|
||||||
|
int alpha;
|
||||||
|
if (delta <= -0.5f) {
|
||||||
|
alpha = 255;
|
||||||
|
} else if (delta < 0.5f) {
|
||||||
|
alpha = Math.max(0, Math.min(255, (int)((0.5f - delta) * 255f)));
|
||||||
|
} else {
|
||||||
|
alpha = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
waterFillPixels[cy * cW + cx] = (alpha << 24) | (WR << 16) | (WG << 8) | WB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
waterFillImg.getPixelWriter().setPixels(
|
||||||
|
0, 0, cW, cH, PixelFormat.getIntArgbInstance(), waterFillPixels, 0, cW);
|
||||||
|
gc.drawImage(waterFillImg, 0, 0);
|
||||||
|
|
||||||
|
// ── Küstenlinie – geglättete Pfade über der Wasser-Füllung ─────────────
|
||||||
List<float[][]> seaCoastPaths = currentModel.coastPaths();
|
List<float[][]> seaCoastPaths = currentModel.coastPaths();
|
||||||
|
if (cbCoastline.isSelected() && seaCoastPaths != null && !seaCoastPaths.isEmpty()) {
|
||||||
// ── Seewellen (Terrain < 0) ──────────────────────────────────────────────
|
double canvasW = canvas.getWidth(), canvasH = canvas.getHeight();
|
||||||
if (seaMask != null && cbWater.isSelected()) {
|
|
||||||
// Wellengröße in Weltkoordinaten (skaliert mit Zoom, damit Bildschirmgröße = WAVE_W)
|
|
||||||
double pxPerWorld = (RENDER_SIZE - 1) * scale / WorldMapRenderer.WORLD_SIZE;
|
|
||||||
double wWave = WAVE_W / pxPerWorld;
|
|
||||||
double wSpX = wWave * 1.8, wSpY = wWave * 1.05, wAmp = WAVE_W * 0.2;
|
|
||||||
|
|
||||||
// Sichtbarer Weltbereich
|
|
||||||
double cW = canvas.getWidth(), cH = canvas.getHeight();
|
|
||||||
double wxMin = toWorldX(0), wxMax = toWorldX(cW);
|
|
||||||
double wzMin = toWorldZ(0), wzMax = toWorldZ(cH);
|
|
||||||
wxMin = Math.max(-WorldMapRenderer.WORLD_HALF, wxMin);
|
|
||||||
wzMin = Math.max(-WorldMapRenderer.WORLD_HALF, wzMin);
|
|
||||||
wxMax = Math.min(WorldMapRenderer.WORLD_HALF, wxMax);
|
|
||||||
wzMax = Math.min(WorldMapRenderer.WORLD_HALF, wzMax);
|
|
||||||
|
|
||||||
gc.setStroke(Color.BLACK);
|
|
||||||
gc.setLineWidth(0.8);
|
|
||||||
gc.setLineDashes(null);
|
|
||||||
|
|
||||||
// Gitter-Startzeile: auf Weltgitter einrasten → Wellen bleiben beim Panning stabil
|
|
||||||
long rowBase = (long)Math.floor((wzMin - wSpY * 0.5) / wSpY);
|
|
||||||
for (long rowIdx = rowBase; rowIdx * wSpY + wSpY * 0.5 < wzMax; rowIdx++) {
|
|
||||||
double wz = rowIdx * wSpY + wSpY * 0.5;
|
|
||||||
double rowOff = (rowIdx & 1L) == 1L ? wSpX * 0.5 : 0;
|
|
||||||
long colBase = (long)Math.floor((wxMin - rowOff) / wSpX);
|
|
||||||
for (long colIdx = colBase; colIdx * wSpX + rowOff < wxMax; colIdx++) {
|
|
||||||
double wx = colIdx * wSpX + rowOff;
|
|
||||||
double jx = Math.abs(Math.sin(rowIdx * 73.1 + colIdx * 157.3)) * wSpX * 0.25;
|
|
||||||
double jy = Math.sin(rowIdx * 211.7 + colIdx * 89.5) * wSpY * 0.2;
|
|
||||||
double owx = wx + jx, owz = wz + jy;
|
|
||||||
// Seekartenmaske prüfen (Mitte und Ende der Welle)
|
|
||||||
int mxS = (int)((owx + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE - 1));
|
|
||||||
int mzS = (int)((owz + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE - 1));
|
|
||||||
int mxE = (int)((owx + wWave + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE - 1));
|
|
||||||
if (mxS < 0 || mxS >= SEA_MASK_SIZE || mzS < 0 || mzS >= SEA_MASK_SIZE) continue;
|
|
||||||
if (!seaMask[mzS * SEA_MASK_SIZE + mxS]) continue;
|
|
||||||
if (mxE < 0 || mxE >= SEA_MASK_SIZE || !seaMask[mzS * SEA_MASK_SIZE + mxE]) continue;
|
|
||||||
// Canvas-Koordinaten; Breite = WAVE_W px, Amplitude = wAmp px
|
|
||||||
double ox = toCanvasX((float)owx), oy = toCanvasZ((float)owz);
|
|
||||||
double ex = ox + WAVE_W;
|
|
||||||
gc.beginPath();
|
|
||||||
gc.moveTo(ox, oy);
|
|
||||||
gc.bezierCurveTo(ox + WAVE_W*0.3, oy - wAmp, ox + WAVE_W*0.7, oy + wAmp, ex, oy);
|
|
||||||
gc.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<PlacedWater> waters = currentModel.waters();
|
|
||||||
if (waters.isEmpty()) return;
|
|
||||||
|
|
||||||
final double spX = WAVE_W * 1.8, spY = WAVE_W * 1.05, wmA = WAVE_W * 0.2;
|
|
||||||
|
|
||||||
for (PlacedWater w : waters) {
|
|
||||||
float[] wx = w.pointsX(), wz = w.pointsZ();
|
|
||||||
int n = wx.length;
|
|
||||||
if (n < 3) continue;
|
|
||||||
|
|
||||||
float wh = w.waterHeight();
|
|
||||||
|
|
||||||
double[] cx = new double[n], cz = new double[n];
|
|
||||||
for (int i = 0; i < n; i++) { cx[i] = toCanvasX(wx[i]); cz[i] = toCanvasZ(wz[i]); }
|
|
||||||
|
|
||||||
double minX = cx[0], maxX = cx[0], minZ = cz[0], maxZ = cz[0];
|
|
||||||
for (int i = 1; i < n; i++) {
|
|
||||||
if (cx[i] < minX) minX = cx[i]; if (cx[i] > maxX) maxX = cx[i];
|
|
||||||
if (cz[i] < minZ) minZ = cz[i]; if (cz[i] > maxZ) maxZ = cz[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wellenmarken: nur wenn Fläche groß genug für mindestens eine Welle
|
|
||||||
if ((maxX - minX) > WAVE_W * 1.5 && (maxZ - minZ) > spY) {
|
|
||||||
gc.save();
|
|
||||||
gc.beginPath();
|
|
||||||
gc.moveTo(cx[0], cz[0]);
|
|
||||||
for (int i = 1; i < n; i++) { gc.lineTo(cx[i], cz[i]); }
|
|
||||||
gc.closePath();
|
|
||||||
gc.clip();
|
|
||||||
|
|
||||||
gc.setStroke(Color.BLACK);
|
|
||||||
gc.setLineWidth(0.8);
|
|
||||||
gc.setLineDashes(null);
|
|
||||||
|
|
||||||
int row = 0;
|
|
||||||
for (double wy = minZ + spY * 0.5; wy < maxZ; wy += spY, row++) {
|
|
||||||
double rowOff = (row & 1) == 1 ? spX * 0.5 : 0;
|
|
||||||
int col = 0;
|
|
||||||
for (double wxx = minX + rowOff; wxx + WAVE_W <= maxX; wxx += spX, col++) {
|
|
||||||
double jx = Math.abs(Math.sin(row * 73.1 + col * 157.3)) * spX * 0.25;
|
|
||||||
double jy = Math.sin(row * 211.7 + col * 89.5) * spY * 0.2;
|
|
||||||
double ox = wxx + jx, oy = wy + jy;
|
|
||||||
if (ox < minX || ox + WAVE_W > maxX || oy < minZ || oy > maxZ) { continue; }
|
|
||||||
// Höhenprüfung: Terrain muss unter waterHeight liegen
|
|
||||||
if (terrainSamples != null) {
|
|
||||||
float owx = toWorldX(ox), owz = toWorldZ(oy);
|
|
||||||
int mxS = Math.max(0, Math.min(SEA_MASK_SIZE-1, (int)((owx + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE-1))));
|
|
||||||
int mzS = Math.max(0, Math.min(SEA_MASK_SIZE-1, (int)((owz + WorldMapRenderer.WORLD_HALF) / WorldMapRenderer.WORLD_SIZE * (SEA_MASK_SIZE-1))));
|
|
||||||
if (terrainSamples[mzS * SEA_MASK_SIZE + mxS] >= wh) { continue; }
|
|
||||||
}
|
|
||||||
gc.beginPath();
|
|
||||||
gc.moveTo(ox, oy);
|
|
||||||
gc.bezierCurveTo(ox + WAVE_W*0.3, oy - wmA, ox + WAVE_W*0.7, oy + wmA, ox + WAVE_W, oy);
|
|
||||||
gc.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gc.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Seeküstenlinie – verbundene Pfade, runde Joins, weicher Halo ────────────
|
|
||||||
if (seaCoastPaths != null && !seaCoastPaths.isEmpty()) {
|
|
||||||
double cW = canvas.getWidth(), cH = canvas.getHeight();
|
|
||||||
gc.setLineDashes(null);
|
gc.setLineDashes(null);
|
||||||
gc.setLineCap(StrokeLineCap.ROUND);
|
gc.setLineCap(StrokeLineCap.ROUND);
|
||||||
gc.setLineJoin(StrokeLineJoin.ROUND);
|
gc.setLineJoin(StrokeLineJoin.ROUND);
|
||||||
|
|
||||||
gc.setStroke(Color.color(0, 0, 0, 0.22));
|
gc.setStroke(Color.color(0, 0, 0, 0.22));
|
||||||
gc.setLineWidth(7.0);
|
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.setStroke(Color.BLACK);
|
||||||
gc.setLineWidth(3.0);
|
gc.setLineWidth(3.0);
|
||||||
for (float[][] path : seaCoastPaths) { drawCoastPath(gc, path, cW, cH, 4); }
|
for (float[][] path : seaCoastPaths) { drawCoastPath(gc, path, canvasW, canvasH, 4); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -811,35 +1060,17 @@ public class WorldMapView extends VBox {
|
|||||||
List<Location> locs, List<PlacedWater> waters,
|
List<Location> locs, List<PlacedWater> waters,
|
||||||
List<PlacedModel> models, List<PlacedWaterfall> waterfalls,
|
List<PlacedModel> models, List<PlacedWaterfall> waterfalls,
|
||||||
float[] voxelSurface) {
|
float[] voxelSurface) {
|
||||||
int[] slotColors = computeSlotColors(mapData);
|
int[] slotColors = computeSlotColors(mapData);
|
||||||
boolean[] mask = WorldMapRenderer.buildSeaMask(mapData, SEA_MASK_SIZE, voxelSurface);
|
int[][] mapTiles = WorldMapRenderer.loadMapTiles(findAssetRoot());
|
||||||
float[] tSamp = buildTerrainSamples(mapData, SEA_MASK_SIZE);
|
boolean[] mask = WorldMapRenderer.buildSeaMask(mapData, SEA_MASK_SIZE, voxelSurface);
|
||||||
|
float[] tSamp = WorldMapRenderer.buildTerrainSamples(mapData, SEA_MASK_SIZE, voxelSurface);
|
||||||
List<float[][]> paths = buildSeaCoastPaths(buildCoastSegsMS(mapData, waters, SEA_MASK_SIZE, voxelSurface));
|
List<float[][]> paths = buildSeaCoastPaths(buildCoastSegsMS(mapData, waters, SEA_MASK_SIZE, voxelSurface));
|
||||||
List<PlacedModel> treeList = models.stream()
|
List<PlacedModel> treeList = models.stream()
|
||||||
.filter(WorldMapRenderer::isTree).collect(Collectors.toList());
|
.filter(WorldMapRenderer::isTree).collect(Collectors.toList());
|
||||||
List<List<PlacedModel>> tClusters = clusterTreeModels(treeList, 40f);
|
List<List<PlacedModel>> tClusters = clusterTreeModels(treeList, 40f);
|
||||||
return new WorldMapRenderModel(
|
return new WorldMapRenderModel(
|
||||||
mapData, areas, zones, locs, waters, models, waterfalls, slotColors,
|
mapData, areas, zones, locs, waters, models, waterfalls, slotColors,
|
||||||
mask, voxelSurface, tSamp, paths, tClusters);
|
mask, voxelSurface, tSamp, paths, tClusters, mapTiles);
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Erzeugt interpolierte Marching-Squares-Segmente für Meer + Wasserflächen.
|
// Erzeugt interpolierte Marching-Squares-Segmente für Meer + Wasserflächen.
|
||||||
@@ -1147,7 +1378,7 @@ public class WorldMapView extends VBox {
|
|||||||
cbTerrain.isSelected(),
|
cbTerrain.isSelected(),
|
||||||
cbTerrain.isSelected(),
|
cbTerrain.isSelected(),
|
||||||
cbWater.isSelected(),
|
cbWater.isSelected(),
|
||||||
true, // Wellen im Export-PNG gebacken
|
false,
|
||||||
cbAreas.isSelected(),
|
cbAreas.isSelected(),
|
||||||
cbZones.isSelected(),
|
cbZones.isSelected(),
|
||||||
cbLocations.isSelected(),
|
cbLocations.isSelected(),
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ public class MinimapState extends BaseAppState {
|
|||||||
private Material fogMat; // Minimap-Nebel
|
private Material fogMat; // Minimap-Nebel
|
||||||
private Material fogFullMat; // Vollbild-Nebel (eigene Overlay-Bounds)
|
private Material fogFullMat; // Vollbild-Nebel (eigene Overlay-Bounds)
|
||||||
private float fogTime = 0f;
|
private float fogTime = 0f;
|
||||||
|
private float posUpdateTimer = 0f;
|
||||||
|
|
||||||
// ── Blur-Filter (aktiv wenn Vollbild-Karte offen) ─────────────────────────
|
// ── Blur-Filter (aktiv wenn Vollbild-Karte offen) ─────────────────────────
|
||||||
|
|
||||||
@@ -217,8 +218,12 @@ public class MinimapState extends BaseAppState {
|
|||||||
if (fogMat != null) { fogMat.setFloat("Time", fogTime); }
|
if (fogMat != null) { fogMat.setFloat("Time", fogTime); }
|
||||||
if (fogFullMat != null) { fogFullMat.setFloat("Time", fogTime); }
|
if (fogFullMat != null) { fogFullMat.setFloat("Time", fogTime); }
|
||||||
|
|
||||||
|
posUpdateTimer += tpf;
|
||||||
|
boolean doPosUpdate = posUpdateTimer >= 1f;
|
||||||
|
if (doPosUpdate) posUpdateTimer = 0f;
|
||||||
|
|
||||||
if (!fullMapOpen) {
|
if (!fullMapOpen) {
|
||||||
updateMinimapUV(px, pz);
|
if (doPosUpdate) updateMinimapUV(px, pz);
|
||||||
} else {
|
} else {
|
||||||
if (fmDragging) {
|
if (fmDragging) {
|
||||||
Vector2f cursor = app.getInputManager().getCursorPosition();
|
Vector2f cursor = app.getInputManager().getCursorPosition();
|
||||||
@@ -234,7 +239,7 @@ public class MinimapState extends BaseAppState {
|
|||||||
scheduleVectorRedraw();
|
scheduleVectorRedraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateFullMapPlayerDot(px, pz);
|
if (doPosUpdate) updateFullMapPlayerDot(px, pz);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user