Weltkarte: tile-basiertes Rendering, Hires-Chunks, 5 Zoom-Stufen
- Durchschnittliche Kachel-Farben (terrainColorMap, 2048px) als Basis für Low-Res-Ansicht - Bilineares Splatmap-Blending (blendTileAvgColors) statt Nearest-Neighbor - Chunk-basierte Hires-Karte (8192px, 256 Chunks à 512px) auf Disk gecacht - Zoom-Stufen 0–2: Overview-Bild; Stufen 3–4: Hires-Chunks per LRU-Cache - 5 geometrisch verteilte Zoom-Stufen (Fit-All bis 0,25m/Pixel) - Klippen-Erkennung per Steigungswinkel (1,5m Sampling-Distanz, harter 30°-Schwellwert) - Leichter Box-Blur (Radius 2) über Terrain-Layer - Wasserfarbe in Hires-Chunks vereinheitlicht (WATER_COLOR) - Kein Hillshading Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 44 KiB |
@@ -26,5 +26,6 @@ public record WorldMapRenderModel(
|
||||
float[] terrainSamples, // Höhenwerte bei SEA_MASK_SIZE (für Wellen-Prüfung)
|
||||
List<float[][]> coastPaths, // geglättete Marching-Squares Küstenpfade (Weltkoord.)
|
||||
List<List<PlacedModel>> treeClusters,
|
||||
int[][] mapTilePixels // 6 Kacheln (TILE_WATER..TILE_ROCK), je 16×16 ARGB; null = Fallback
|
||||
int[][] mapTilePixels, // 6 Kacheln (TILE_WATER..TILE_ROCK), je 16×16 ARGB; null = Fallback
|
||||
int[] terrainColorMap // COLOR_MAP_SIZE×COLOR_MAP_SIZE packed RGB; null → render() berechnet intern
|
||||
) {}
|
||||
|
||||
@@ -29,6 +29,18 @@ public final class WorldMapRenderer {
|
||||
public static final float WORLD_SIZE = 2048f;
|
||||
/** Auflösung des voxelSurface-Bake-Arrays (1 Zelle pro Welteinheit). */
|
||||
public static final int VOXEL_SURFACE_RES = 2048;
|
||||
/**
|
||||
* Auflösung der vorberechneten Übersichts-Farbkarte (terrainColorMap, für Zoom 0-2).
|
||||
* 2048 → 1 Farbwert pro Meter.
|
||||
*/
|
||||
public static final int COLOR_MAP_SIZE = 2048;
|
||||
/**
|
||||
* Auflösung der hochauflösenden Chunk-Karte (für Zoom 3-4).
|
||||
* 8192 → 1 Farbwert pro 0,25m. Auf 4096 für 0,5m/px ändern.
|
||||
*/
|
||||
public static final int HIRES_MAP_SIZE = 8192;
|
||||
/** Pixelgröße eines Chunks (quadratisch). HIRES_MAP_SIZE muss ganzzahlig teilbar sein. */
|
||||
public static final int CHUNK_SIZE = 512;
|
||||
|
||||
public record RenderInput(
|
||||
MapData mapData,
|
||||
@@ -146,49 +158,37 @@ public final class WorldMapRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Tile-Rendering: tileIdx + Rohfarbe pro Pixel speichern
|
||||
float cellW = WORLD_SIZE / (targetSize - 1);
|
||||
int SS = MapData.SPLAT_SIZE;
|
||||
int[] slotCols = model.slotColorsRGB();
|
||||
int[][] tiles = model.mapTilePixels();
|
||||
int[] rawRGB = new int[targetSize * targetSize];
|
||||
// 3. Terrain-Farben aus vorberechneter Farbkarte (oder intern berechnen als Fallback)
|
||||
float cellW = WORLD_SIZE / (targetSize - 1);
|
||||
int[] slotCols = model.slotColorsRGB();
|
||||
int[] colorMap = model.terrainColorMap() != null
|
||||
? model.terrainColorMap()
|
||||
: buildTerrainColorMap(m, slotCols, model.mapTilePixels(), model.voxelSurface(), model.waters());
|
||||
int[] rawRGB = new int[targetSize * targetSize];
|
||||
int waterR = (WATER_COLOR >> 16) & 0xFF;
|
||||
int waterG = (WATER_COLOR >> 8) & 0xFF;
|
||||
int waterB = WATER_COLOR & 0xFF;
|
||||
|
||||
for (int py = 0; py < targetSize; py++) {
|
||||
float wz = -WORLD_HALF + py * cellW;
|
||||
for (int px = 0; px < targetSize; px++) {
|
||||
float wx = -WORLD_HALF + px * cellW;
|
||||
int idx = py * targetSize + px;
|
||||
|
||||
int tileIdx;
|
||||
int tr, tg, tb;
|
||||
if (isWater[idx]) {
|
||||
tileIdx = TILE_WATER;
|
||||
tr = waterR; tg = waterG; tb = waterB;
|
||||
rawRGB[idx] = (waterR << 16) | (waterG << 8) | waterB;
|
||||
} else {
|
||||
int dPx = Math.max(1, (int)(0.25f / cellW));
|
||||
float hR = heights[py * targetSize + Math.min(targetSize-1, px+dPx)];
|
||||
float hL = heights[py * targetSize + Math.max(0, px-dPx)];
|
||||
float hD = heights[Math.min(targetSize-1, py+dPx) * targetSize + px];
|
||||
float hU = heights[Math.max(0, py-dPx) * targetSize + px];
|
||||
float dist = dPx * cellW;
|
||||
double slope = Math.toDegrees(Math.atan(Math.sqrt(
|
||||
Math.pow((hR-hL) / (2*dist), 2) + Math.pow((hD-hU) / (2*dist), 2))));
|
||||
tileIdx = (slope > 30.0) ? TILE_CLIFF : getTerrainTile(m, SS, wx, wz, slotCols);
|
||||
if (tiles != null && tileIdx < tiles.length && tiles[tileIdx] != null) {
|
||||
int tu = Math.floorMod((int)Math.floor((wx + WORLD_HALF) / TILE_WU), 16);
|
||||
int tv = Math.floorMod((int)Math.floor((wz + WORLD_HALF) / TILE_WU), 16);
|
||||
int argb = tiles[tileIdx][tv * 16 + tu];
|
||||
tr = (argb >> 16) & 0xFF; tg = (argb >> 8) & 0xFF; tb = argb & 0xFF;
|
||||
} else {
|
||||
tr = TILE_FALLBACK_RGB[tileIdx][0];
|
||||
tg = TILE_FALLBACK_RGB[tileIdx][1];
|
||||
tb = TILE_FALLBACK_RGB[tileIdx][2];
|
||||
}
|
||||
// Bilinear sample aus der Farbkarte (COLOR_MAP_SIZE → targetSize)
|
||||
float fx = (float) px / (targetSize - 1) * (COLOR_MAP_SIZE - 1);
|
||||
float fz = (float) py / (targetSize - 1) * (COLOR_MAP_SIZE - 1);
|
||||
int cx0 = (int) fx, cx1 = Math.min(cx0 + 1, COLOR_MAP_SIZE - 1);
|
||||
int cz0 = (int) fz, cz1 = Math.min(cz0 + 1, COLOR_MAP_SIZE - 1);
|
||||
float tx = fx - cx0, tz = fz - cz0;
|
||||
int c00 = colorMap[cz0 * COLOR_MAP_SIZE + cx0];
|
||||
int c10 = colorMap[cz0 * COLOR_MAP_SIZE + cx1];
|
||||
int c01 = colorMap[cz1 * COLOR_MAP_SIZE + cx0];
|
||||
int c11 = colorMap[cz1 * COLOR_MAP_SIZE + cx1];
|
||||
rawRGB[idx] = (clamp((int)(((c00>>16)&0xFF)*(1-tx)*(1-tz)+((c10>>16)&0xFF)*tx*(1-tz)+((c01>>16)&0xFF)*(1-tx)*tz+((c11>>16)&0xFF)*tx*tz)) << 16)
|
||||
| (clamp((int)(((c00>>8) &0xFF)*(1-tx)*(1-tz)+((c10>>8) &0xFF)*tx*(1-tz)+((c01>>8) &0xFF)*(1-tx)*tz+((c11>>8) &0xFF)*tx*tz)) << 8)
|
||||
| clamp((int)(( c00 &0xFF)*(1-tx)*(1-tz)+( c10 &0xFF)*tx*(1-tz)+( c01 &0xFF)*(1-tx)*tz+( c11 &0xFF)*tx*tz));
|
||||
}
|
||||
rawRGB[idx] = (tr << 16) | (tg << 8) | tb;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,31 +223,10 @@ public final class WorldMapRenderer {
|
||||
}
|
||||
}
|
||||
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];
|
||||
}
|
||||
// Land-Farbe aus Farbkarte (Wasser-Pixel bekommen ihren Nachbar-Landwert)
|
||||
int mapX = Math.min((int)((float)px / (targetSize-1) * (COLOR_MAP_SIZE-1)), COLOR_MAP_SIZE-1);
|
||||
int mapZ = Math.min((int)((float)py / (targetSize-1) * (COLOR_MAP_SIZE-1)), COLOR_MAP_SIZE-1);
|
||||
int landRGB = colorMap[mapZ * COLOR_MAP_SIZE + mapX];
|
||||
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)
|
||||
@@ -267,9 +246,7 @@ public final class WorldMapRenderer {
|
||||
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;
|
||||
@@ -294,31 +271,9 @@ public final class WorldMapRenderer {
|
||||
}
|
||||
}
|
||||
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 mapX2 = Math.min((int)((float)px / (targetSize-1) * (COLOR_MAP_SIZE-1)), COLOR_MAP_SIZE-1);
|
||||
int mapZ2 = Math.min((int)((float)py / (targetSize-1) * (COLOR_MAP_SIZE-1)), COLOR_MAP_SIZE-1);
|
||||
int landRGB2 = colorMap[mapZ2 * COLOR_MAP_SIZE + mapX2];
|
||||
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)
|
||||
@@ -531,7 +486,7 @@ public final class WorldMapRenderer {
|
||||
return render(new WorldMapRenderModel(
|
||||
input.mapData(), input.areas(), input.zones(), input.locations(),
|
||||
input.waters(), input.models(), null,
|
||||
input.slotColorsRGB(), null, null, null, null, null, null
|
||||
input.slotColorsRGB(), null, null, null, null, null, null, null
|
||||
), targetSize, opts);
|
||||
}
|
||||
|
||||
@@ -1243,6 +1198,256 @@ public final class WorldMapRenderer {
|
||||
return m.modelPath().replace('\\', '/').toLowerCase().contains("/trees/");
|
||||
}
|
||||
|
||||
/** Bilineare Tile-Textur-Abfrage mit UV-Gewichten (w00/w10/w01/w11 = bilinear weights). */
|
||||
private static int tileSampleBil(int[][] tiles, int tileIdx,
|
||||
int u0, int u1, int v0, int v1, float w00, float w10, float w01, float w11) {
|
||||
if (tiles == null || tileIdx < 0 || tileIdx >= tiles.length || tiles[tileIdx] == null) {
|
||||
int[] fb = TILE_FALLBACK_RGB[Math.max(0, Math.min(TILE_FALLBACK_RGB.length-1, tileIdx))];
|
||||
return (fb[0]<<16)|(fb[1]<<8)|fb[2];
|
||||
}
|
||||
int[] t = tiles[tileIdx];
|
||||
int c00=t[v0*16+u0], c10=t[v0*16+u1], c01=t[v1*16+u0], c11=t[v1*16+u1];
|
||||
int r=clamp((int)(((c00>>16)&0xFF)*w00+((c10>>16)&0xFF)*w10+((c01>>16)&0xFF)*w01+((c11>>16)&0xFF)*w11));
|
||||
int g=clamp((int)(((c00>>8)&0xFF)*w00+((c10>>8)&0xFF)*w10+((c01>>8)&0xFF)*w01+((c11>>8)&0xFF)*w11));
|
||||
int b=clamp((int)((c00&0xFF)*w00+(c10&0xFF)*w10+(c01&0xFF)*w01+(c11&0xFF)*w11));
|
||||
return (r<<16)|(g<<8)|b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut die Terrain-Farbkarte vor: für jeden Pixel den dominanten Splatmap-Slot
|
||||
* ermitteln, Durchschnittsfarbe der Kachel lesen, Hillshading anwenden.
|
||||
* Das Ergebnis wird im WorldMapRenderModel gespeichert und von render() direkt
|
||||
* verwendet — kein Splatmap-Blend zur Renderzeit mehr.
|
||||
*/
|
||||
public static int[] buildTerrainColorMap(MapData m, int[] slotColors, int[][] tilePixels,
|
||||
float[] voxelSurface, List<PlacedWater> waters) {
|
||||
final int SIZE = COLOR_MAP_SIZE;
|
||||
final int SS = MapData.SPLAT_SIZE;
|
||||
float[] heights = buildTerrainSamples(m, SIZE, voxelSurface);
|
||||
float[] waterH = buildWaterHeightMap(waters, SIZE);
|
||||
int[] tileAvg = computeTileAvgColors(tilePixels);
|
||||
int[] slotTypes = computeSlotTileTypes(m, slotColors);
|
||||
float cellW = WORLD_SIZE / (SIZE - 1);
|
||||
int[] result = new int[SIZE * SIZE];
|
||||
for (int py = 0; py < SIZE; py++) {
|
||||
float wz = -WORLD_HALF + py * cellW;
|
||||
for (int px = 0; px < SIZE; px++) {
|
||||
float wx = -WORLD_HALF + px * cellW;
|
||||
int idx = py * SIZE + px;
|
||||
float h = heights[idx];
|
||||
int rgb;
|
||||
if (h < 0f || (waterH[idx] > 0f && waterH[idx] > h)) {
|
||||
int[] wc = TILE_FALLBACK_RGB[TILE_WATER];
|
||||
rgb = (wc[0]<<16)|(wc[1]<<8)|wc[2];
|
||||
} else {
|
||||
int dPx = Math.max(1, (int)(1.5f / cellW));
|
||||
float hR = heights[py * SIZE + Math.min(SIZE-1, px+dPx)];
|
||||
float hL = heights[py * SIZE + Math.max(0, px-dPx)];
|
||||
float hD = heights[Math.min(SIZE-1, py+dPx) * SIZE + px];
|
||||
float hU = heights[Math.max(0, py-dPx) * SIZE + 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))));
|
||||
if (slope > 30.0) {
|
||||
rgb = tileAvg[Math.min(TILE_CLIFF, tileAvg.length-1)];
|
||||
} else {
|
||||
rgb = blendTileAvgColors(m, SS, wx, wz, tileAvg, slotTypes);
|
||||
}
|
||||
}
|
||||
result[idx] = rgb;
|
||||
}
|
||||
}
|
||||
return boxBlur(result, SIZE, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert die hochauflösende Terrain-Farbkarte (HIRES_MAP_SIZE×HIRES_MAP_SIZE) in
|
||||
* CHUNK_SIZE×CHUNK_SIZE-Kacheln und speichert sie als PNG unter {@code cacheDir}.
|
||||
* Bereits vorhandene Chunks werden übersprungen, wenn {@code forceRegen} false ist.
|
||||
*
|
||||
* @return Anzahl neu geschriebener Chunks (0 = alle aus Cache geladen)
|
||||
*/
|
||||
public static int buildAndSaveHiresChunks(MapData m, int[] slotColors, int[][] tilePixels,
|
||||
float[] voxelSurface, List<PlacedWater> waters,
|
||||
java.nio.file.Path cacheDir, boolean forceRegen) {
|
||||
final int SIZE = HIRES_MAP_SIZE;
|
||||
final int CS = CHUNK_SIZE;
|
||||
final int COLS = SIZE / CS; // Chunks pro Zeile/Spalte
|
||||
final int SS = MapData.SPLAT_SIZE;
|
||||
|
||||
try { Files.createDirectories(cacheDir); } catch (Exception e) { log.error("Chunk-Dir nicht erstellbar", e); return 0; }
|
||||
|
||||
// Prüfen ob alle Chunks vorhanden sind (dann Früh-Exit)
|
||||
if (!forceRegen) {
|
||||
boolean allExist = true;
|
||||
for (int cy = 0; cy < COLS && allExist; cy++) {
|
||||
for (int cx = 0; cx < COLS && allExist; cx++) {
|
||||
if (!Files.exists(chunkPath(cacheDir, cx, cy))) allExist = false;
|
||||
}
|
||||
}
|
||||
if (allExist) { log.debug("[HiresChunks] Alle {} Chunks im Cache – übersprungen.", COLS*COLS); return 0; }
|
||||
}
|
||||
|
||||
float[] heights = buildTerrainSamples(m, SIZE, voxelSurface);
|
||||
float[] waterH = buildWaterHeightMap(waters, SIZE);
|
||||
int[] tileAvg = computeTileAvgColors(tilePixels);
|
||||
int[] slotTypes = computeSlotTileTypes(m, slotColors);
|
||||
float cellW = WORLD_SIZE / (SIZE - 1);
|
||||
int written = 0;
|
||||
|
||||
for (int cy = 0; cy < COLS; cy++) {
|
||||
for (int cx = 0; cx < COLS; cx++) {
|
||||
java.nio.file.Path p = chunkPath(cacheDir, cx, cy);
|
||||
if (!forceRegen && Files.exists(p)) continue;
|
||||
// Chunk-Pixel berechnen
|
||||
int[] chunk = new int[CS * CS];
|
||||
for (int ly = 0; ly < CS; ly++) {
|
||||
int gy = cy * CS + ly; // globaler Y-Index in der HIRES-Karte
|
||||
float wz = -WORLD_HALF + gy * cellW;
|
||||
for (int lx = 0; lx < CS; lx++) {
|
||||
int gx = cx * CS + lx;
|
||||
float wx = -WORLD_HALF + gx * cellW;
|
||||
int idx = gy * SIZE + gx;
|
||||
float h = heights[idx];
|
||||
int tr, tg, tb;
|
||||
if (h < 0f || (waterH[idx] > 0f && waterH[idx] > h)) {
|
||||
tr = (WATER_COLOR >> 16) & 0xFF;
|
||||
tg = (WATER_COLOR >> 8) & 0xFF;
|
||||
tb = WATER_COLOR & 0xFF;
|
||||
} else {
|
||||
int dPx = Math.max(1, (int)(1.5f / cellW));
|
||||
float hR = heights[gy * SIZE + Math.min(SIZE-1, gx+dPx)];
|
||||
float hL = heights[gy * SIZE + Math.max(0, gx-dPx)];
|
||||
float hD = heights[Math.min(SIZE-1, gy+dPx) * SIZE + gx];
|
||||
float hU = heights[Math.max(0, gy-dPx) * SIZE + gx];
|
||||
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))));
|
||||
int rgb = (slope > 30.0)
|
||||
? tileAvg[Math.min(TILE_CLIFF, tileAvg.length-1)]
|
||||
: blendTileAvgColors(m, SS, wx, wz, tileAvg, slotTypes);
|
||||
tr = (rgb>>16)&0xFF; tg = (rgb>>8)&0xFF; tb = rgb&0xFF;
|
||||
}
|
||||
chunk[ly * CS + lx] = (0xFF << 24) | (tr << 16) | (tg << 8) | tb;
|
||||
}
|
||||
}
|
||||
// Als PNG speichern
|
||||
BufferedImage img = new BufferedImage(CS, CS, BufferedImage.TYPE_INT_ARGB);
|
||||
img.setRGB(0, 0, CS, CS, chunk, 0, CS);
|
||||
try { javax.imageio.ImageIO.write(img, "PNG", p.toFile()); written++; }
|
||||
catch (Exception e) { log.error("Chunk {}/{} konnte nicht gespeichert werden", cx, cy, e); }
|
||||
}
|
||||
}
|
||||
log.debug("[HiresChunks] {} neue Chunks geschrieben nach {}", written, cacheDir);
|
||||
return written;
|
||||
}
|
||||
|
||||
/** Lädt einen Chunk aus dem Cache. Gibt null zurück wenn nicht vorhanden. */
|
||||
public static int[] loadChunk(int chunkX, int chunkY, java.nio.file.Path cacheDir) {
|
||||
java.nio.file.Path p = chunkPath(cacheDir, chunkX, chunkY);
|
||||
if (!Files.exists(p)) return null;
|
||||
try {
|
||||
BufferedImage img = javax.imageio.ImageIO.read(p.toFile());
|
||||
if (img == null) return null;
|
||||
return img.getRGB(0, 0, CHUNK_SIZE, CHUNK_SIZE, null, 0, CHUNK_SIZE);
|
||||
} catch (Exception e) {
|
||||
log.error("Chunk {}/{} konnte nicht geladen werden", chunkX, chunkY, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static java.nio.file.Path chunkPath(java.nio.file.Path dir, int cx, int cy) {
|
||||
return dir.resolve(String.format("chunk_%02x_%02x.png", cx, cy));
|
||||
}
|
||||
|
||||
/** Separabler Box-Blur (horizontal + vertikal), Radius in Pixeln. */
|
||||
private static int[] boxBlur(int[] src, int size, int radius) {
|
||||
int[] tmp = new int[size * size];
|
||||
int[] dst = new int[size * size];
|
||||
int diam = radius * 2 + 1;
|
||||
// Horizontaler Pass
|
||||
for (int py = 0; py < size; py++) {
|
||||
for (int px = 0; px < size; px++) {
|
||||
long r = 0, g = 0, b = 0;
|
||||
for (int dx = -radius; dx <= radius; dx++) {
|
||||
int c = src[py * size + Math.max(0, Math.min(size - 1, px + dx))];
|
||||
r += (c >> 16) & 0xFF; g += (c >> 8) & 0xFF; b += c & 0xFF;
|
||||
}
|
||||
tmp[py * size + px] = ((int)(r/diam) << 16) | ((int)(g/diam) << 8) | (int)(b/diam);
|
||||
}
|
||||
}
|
||||
// Vertikaler Pass
|
||||
for (int py = 0; py < size; py++) {
|
||||
for (int px = 0; px < size; px++) {
|
||||
long r = 0, g = 0, b = 0;
|
||||
for (int dz = -radius; dz <= radius; dz++) {
|
||||
int c = tmp[Math.max(0, Math.min(size - 1, py + dz)) * size + px];
|
||||
r += (c >> 16) & 0xFF; g += (c >> 8) & 0xFF; b += c & 0xFF;
|
||||
}
|
||||
dst[py * size + px] = ((int)(r/diam) << 16) | ((int)(g/diam) << 8) | (int)(b/diam);
|
||||
}
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bilineare Splatmap-Blendung der Tile-Durchschnittsfarben an Weltposition (wx, wz).
|
||||
* Identische Logik wie der alte progressiver Splatmap-Blend in render(), aber mit
|
||||
* vorberechneten Durchschnittsfarben statt Tile-UV-Sampling.
|
||||
*/
|
||||
private static int blendTileAvgColors(MapData m, int SS, float wx, float wz,
|
||||
int[] tileAvg, int[] slotTypes) {
|
||||
float sfx = Math.max(0f, Math.min(SS-1, (wx+WORLD_HALF)/WORLD_SIZE*(SS-1)));
|
||||
float sfz = Math.max(0f, Math.min(SS-1, (WORLD_HALF-wz)/WORLD_SIZE*(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 = bilerp(m.splatG, bi00,bi10,bi01,bi11,stx,stz);
|
||||
float wb = bilerp(m.splatB, bi00,bi10,bi01,bi11,stx,stz);
|
||||
float wa = bilerp(m.splatA, bi00,bi10,bi01,bi11,stx,stz);
|
||||
float up1 = bilerp(m.upperSplatR, bi00,bi10,bi01,bi11,stx,stz);
|
||||
float up2 = bilerp(m.upperSplatG, bi00,bi10,bi01,bi11,stx,stz);
|
||||
float up3 = bilerp(m.upperSplatB, bi00,bi10,bi01,bi11,stx,stz);
|
||||
float up4 = bilerp(m.upperSplatA, bi00,bi10,bi01,bi11,stx,stz);
|
||||
float t31 = bilerp(m.thirdSplatR, bi00,bi10,bi01,bi11,stx,stz);
|
||||
float t32 = bilerp(m.thirdSplatG, bi00,bi10,bi01,bi11,stx,stz);
|
||||
float t33 = bilerp(m.thirdSplatB, bi00,bi10,bi01,bi11,stx,stz);
|
||||
float t34 = bilerp(m.thirdSplatA, bi00,bi10,bi01,bi11,stx,stz);
|
||||
int c0 = tileAvg[slotTypes[0]];
|
||||
float fr=(c0>>16)&0xFF, fg=(c0>>8)&0xFF, fb=c0&0xFF;
|
||||
int col;
|
||||
col=tileAvg[slotTypes[1]]; fr=fr*(1-wg) +((col>>16)&0xFF)*wg; fg=fg*(1-wg) +((col>>8)&0xFF)*wg; fb=fb*(1-wg) +(col&0xFF)*wg;
|
||||
col=tileAvg[slotTypes[2]]; fr=fr*(1-wb) +((col>>16)&0xFF)*wb; fg=fg*(1-wb) +((col>>8)&0xFF)*wb; fb=fb*(1-wb) +(col&0xFF)*wb;
|
||||
col=tileAvg[slotTypes[3]]; fr=fr*(1-wa) +((col>>16)&0xFF)*wa; fg=fg*(1-wa) +((col>>8)&0xFF)*wa; fb=fb*(1-wa) +(col&0xFF)*wa;
|
||||
col=tileAvg[slotTypes[4]]; fr=fr*(1-up1)+((col>>16)&0xFF)*up1; fg=fg*(1-up1)+((col>>8)&0xFF)*up1; fb=fb*(1-up1)+(col&0xFF)*up1;
|
||||
col=tileAvg[slotTypes[5]]; fr=fr*(1-up2)+((col>>16)&0xFF)*up2; fg=fg*(1-up2)+((col>>8)&0xFF)*up2; fb=fb*(1-up2)+(col&0xFF)*up2;
|
||||
col=tileAvg[slotTypes[6]]; fr=fr*(1-up3)+((col>>16)&0xFF)*up3; fg=fg*(1-up3)+((col>>8)&0xFF)*up3; fb=fb*(1-up3)+(col&0xFF)*up3;
|
||||
col=tileAvg[slotTypes[7]]; fr=fr*(1-up4)+((col>>16)&0xFF)*up4; fg=fg*(1-up4)+((col>>8)&0xFF)*up4; fb=fb*(1-up4)+(col&0xFF)*up4;
|
||||
col=tileAvg[slotTypes[8]]; fr=fr*(1-t31)+((col>>16)&0xFF)*t31; fg=fg*(1-t31)+((col>>8)&0xFF)*t31; fb=fb*(1-t31)+(col&0xFF)*t31;
|
||||
col=tileAvg[slotTypes[9]]; fr=fr*(1-t32)+((col>>16)&0xFF)*t32; fg=fg*(1-t32)+((col>>8)&0xFF)*t32; fb=fb*(1-t32)+(col&0xFF)*t32;
|
||||
col=tileAvg[slotTypes[10]]; fr=fr*(1-t33)+((col>>16)&0xFF)*t33; fg=fg*(1-t33)+((col>>8)&0xFF)*t33; fb=fb*(1-t33)+(col&0xFF)*t33;
|
||||
col=tileAvg[slotTypes[11]]; fr=fr*(1-t34)+((col>>16)&0xFF)*t34; fg=fg*(1-t34)+((col>>8)&0xFF)*t34; fb=fb*(1-t34)+(col&0xFF)*t34;
|
||||
return (clamp((int)fr)<<16)|(clamp((int)fg)<<8)|clamp((int)fb);
|
||||
}
|
||||
|
||||
private static int[] computeTileAvgColors(int[][] tiles) {
|
||||
int n = TILE_FALLBACK_RGB.length;
|
||||
int[] avg = new int[n];
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (tiles != null && i < tiles.length && tiles[i] != null) {
|
||||
long r = 0, g = 0, b = 0;
|
||||
for (int argb : tiles[i]) { r += (argb>>16)&0xFF; g += (argb>>8)&0xFF; b += argb&0xFF; }
|
||||
int len = tiles[i].length;
|
||||
avg[i] = ((int)(r/len) << 16) | ((int)(g/len) << 8) | (int)(b/len);
|
||||
} else {
|
||||
int[] fb = TILE_FALLBACK_RGB[i];
|
||||
avg[i] = (fb[0] << 16) | (fb[1] << 8) | fb[2];
|
||||
}
|
||||
}
|
||||
return avg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kuwahara-Filter: Für jeden Pixel das Quadranten-Fenster mit der kleinsten
|
||||
* Varianz wählen und dessen Mittelwert setzen → Ölgemälde-/gemalt-Effekt.
|
||||
|
||||
@@ -108,27 +108,28 @@ public class WorldMapView extends VBox {
|
||||
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;
|
||||
private static final int ZOOM_LEVELS = 5;
|
||||
private static final double ZOOM_MAX_SCALE = 4.0; // 1 Canvas-Pixel = 0,25 WU = 0,25m
|
||||
private static final int ZOOM_HIRES_CUTOFF = 3; // ab diesem Index Chunk-Rendering
|
||||
private static final int CHUNK_CACHE_MAX = 24; // max. Chunks gleichzeitig im RAM
|
||||
private static final java.nio.file.Path CHUNK_CACHE_DIR =
|
||||
java.nio.file.Paths.get(System.getProperty("user.home"), ".blight", "map_chunks");
|
||||
|
||||
private double[] zoomLevels = null;
|
||||
private int zoomLevelIdx = 0;
|
||||
|
||||
/** LRU-Cache: chunk-Key (cy*COLS+cx) → WritableImage. */
|
||||
private final java.util.LinkedHashMap<Integer, WritableImage> chunkCache =
|
||||
new java.util.LinkedHashMap<>(CHUNK_CACHE_MAX * 2, 0.75f, true) {
|
||||
@Override protected boolean removeEldestEntry(java.util.Map.Entry<Integer, WritableImage> e) {
|
||||
return size() > CHUNK_CACHE_MAX;
|
||||
}
|
||||
};
|
||||
private final java.util.Set<Integer> chunksLoading =
|
||||
java.util.Collections.synchronizedSet(new java.util.HashSet<>());
|
||||
|
||||
private WritableImage bakedForestOverlay = null;
|
||||
// Wasser-Fill-Overlay entfernt: ocean + PlacedWater sind bereits in mapFxImage korrekt gerendert
|
||||
// PlacedWater-Wasserhoehe pro Zelle (0 = kein PlacedWater); fuer glatte Uebergaenge
|
||||
private float[] waterHeightMap = null;
|
||||
|
||||
@@ -282,10 +283,13 @@ public class WorldMapView extends VBox {
|
||||
});
|
||||
|
||||
canvas.setOnScroll(e -> {
|
||||
if (e.getDeltaY() == 0) return;
|
||||
double factor = e.getDeltaY() > 0 ? 1.15 : 1.0 / 1.15;
|
||||
if (zoomLevels == null || e.getDeltaY() == 0) return;
|
||||
int newIdx = zoomLevelIdx + (e.getDeltaY() > 0 ? 1 : -1);
|
||||
newIdx = Math.max(0, Math.min(ZOOM_LEVELS - 1, newIdx));
|
||||
if (newIdx == zoomLevelIdx) return;
|
||||
double oldScale = scale;
|
||||
scale = Math.max(0.1, Math.min(30.0, scale * factor));
|
||||
zoomLevelIdx = newIdx;
|
||||
scale = zoomLevels[zoomLevelIdx];
|
||||
panX = e.getX() - (e.getX() - panX) * scale / oldScale;
|
||||
panY = e.getY() - (e.getY() - panY) * scale / oldScale;
|
||||
redraw();
|
||||
@@ -365,6 +369,17 @@ public class WorldMapView extends VBox {
|
||||
log.debug("[WorldMap] Hintergrund-PNG + Kategorie-Karte gerendert ({}×{} px, {} ms)",
|
||||
RENDER_SIZE, RENDER_SIZE, System.currentTimeMillis() - t1);
|
||||
|
||||
float[] fDensity = computeForestDensity(model);
|
||||
int[] bakedFstPx = computeForestPixels(model, fDensity, 2.0f);
|
||||
|
||||
// Hires-Chunks im Hintergrund generieren (nach dem UI-Update, nicht-blockierend)
|
||||
final WorldMapRenderModel chunkModel = model;
|
||||
Platform.runLater(() -> statusLbl.setText("Generiere Hires-Chunks…"));
|
||||
WorldMapRenderer.buildAndSaveHiresChunks(
|
||||
chunkModel.mapData(), chunkModel.slotColorsRGB(), chunkModel.mapTilePixels(),
|
||||
chunkModel.voxelSurface(), chunkModel.waters(), CHUNK_CACHE_DIR, true);
|
||||
chunkCache.clear();
|
||||
|
||||
Platform.runLater(() -> {
|
||||
currentModel = model;
|
||||
cachedAreas = new ArrayList<>(model.areas());
|
||||
@@ -375,6 +390,7 @@ public class WorldMapView extends VBox {
|
||||
heightMapTerrain = hMapT;
|
||||
categoryMap = catMap;
|
||||
waterHeightMap = whMap;
|
||||
bakedForestOverlay = toWritableImage(bakedFstPx, RENDER_SIZE);
|
||||
fitToView();
|
||||
redraw();
|
||||
progress.setVisible(false);
|
||||
@@ -409,22 +425,13 @@ public class WorldMapView extends VBox {
|
||||
Thread t = new Thread(() -> {
|
||||
log.debug("[WorldMap] PNG-Rerender aus gecachtem Modell gestartet ({}×{})", RENDER_SIZE, RENDER_SIZE);
|
||||
long t0 = System.currentTimeMillis();
|
||||
// Nur das PNG neu rendern — heightMap/catMap/waterHeightMap kommen aus Stage 1
|
||||
// und sind bereits in den Instanz-Feldern vorhanden
|
||||
BufferedImage bi = WorldMapRenderer.render(model, RENDER_SIZE, buildBackgroundOptions());
|
||||
float[] hMap = WorldMapRenderer.buildTerrainSamples(
|
||||
model.mapData(), CAT_SIZE, model.voxelSurface());
|
||||
float[] hMapT = WorldMapRenderer.buildTerrainSamples(
|
||||
model.mapData(), CAT_SIZE, null);
|
||||
byte[] catMap = WorldMapRenderer.buildCategoryMap(
|
||||
model.mapData(), CAT_SIZE, model.voxelSurface(), model.waters(), model.slotColorsRGB());
|
||||
float[] whMap = WorldMapRenderer.buildWaterHeightMap(model.waters(), CAT_SIZE);
|
||||
log.debug("[WorldMap] PNG-Rerender fertig ({} ms)", System.currentTimeMillis() - t0);
|
||||
Platform.runLater(() -> {
|
||||
mapBuffered = bi;
|
||||
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
||||
heightMap = hMap;
|
||||
heightMapTerrain = hMapT;
|
||||
categoryMap = catMap;
|
||||
waterHeightMap = whMap;
|
||||
redraw();
|
||||
progress.setVisible(false);
|
||||
statusLbl.setText("Bereit");
|
||||
@@ -454,20 +461,71 @@ public class WorldMapView extends VBox {
|
||||
return;
|
||||
}
|
||||
|
||||
gc.setImageSmoothing(false);
|
||||
gc.drawImage(mapFxImage, panX, panY, mapFxImage.getWidth() * scale, mapFxImage.getHeight() * scale);
|
||||
gc.setImageSmoothing(true);
|
||||
// Layer 1: Terrain-Textur (überall, opak)
|
||||
drawTileOverlay(gc);
|
||||
// Layer 2: Wasser darüber (glatte Küste via seaCoastPaths)
|
||||
if (zoomLevelIdx >= ZOOM_HIRES_CUTOFF) {
|
||||
drawHiresChunks(gc);
|
||||
} else {
|
||||
gc.drawImage(mapFxImage, panX, panY, mapFxImage.getWidth() * scale, mapFxImage.getHeight() * scale);
|
||||
}
|
||||
// Layer 1: Wasser (glatte Küste via seaCoastPaths)
|
||||
if (cbWater.isSelected()) drawWaterOverlay(gc);
|
||||
// Layer 3: Wald-Overlay (nach Wasser, damit Bäume nicht im Wasser erscheinen)
|
||||
// Layer 2: Wald-Overlay (nach Wasser, damit Bäume nicht im Wasser erscheinen)
|
||||
drawForestOverlay(gc);
|
||||
if (cbAreas.isSelected()) drawAreasOverlay(gc);
|
||||
if (cbLocations.isSelected()) drawLocationsOverlay(gc);
|
||||
if (!isFullscreen) drawCameraIndicator(gc);
|
||||
}
|
||||
|
||||
private void drawHiresChunks(GraphicsContext gc) {
|
||||
final int CS = WorldMapRenderer.CHUNK_SIZE;
|
||||
final int COLS = WorldMapRenderer.HIRES_MAP_SIZE / CS;
|
||||
// Pixelgröße eines Hires-Chunks auf dem Canvas
|
||||
final double chunkCanvasW = (double) RENDER_SIZE * scale / COLS;
|
||||
final double chunkCanvasH = chunkCanvasW;
|
||||
int cW = (int) canvas.getWidth(), cH = (int) canvas.getHeight();
|
||||
|
||||
for (int cy = 0; cy < COLS; cy++) {
|
||||
double canvasTop = panY + cy * chunkCanvasH;
|
||||
if (canvasTop > cH) break;
|
||||
if (canvasTop + chunkCanvasH < 0) continue;
|
||||
for (int cx = 0; cx < COLS; cx++) {
|
||||
double canvasLeft = panX + cx * chunkCanvasW;
|
||||
if (canvasLeft > cW) break;
|
||||
if (canvasLeft + chunkCanvasW < 0) continue;
|
||||
|
||||
int key = cy * COLS + cx;
|
||||
WritableImage img = chunkCache.get(key);
|
||||
if (img != null) {
|
||||
gc.drawImage(img, canvasLeft, canvasTop, chunkCanvasW, chunkCanvasH);
|
||||
} else {
|
||||
// Platzhalter (dunkelgrau) + asynchrones Laden
|
||||
gc.setFill(javafx.scene.paint.Color.rgb(40, 40, 40));
|
||||
gc.fillRect(canvasLeft, canvasTop, chunkCanvasW, chunkCanvasH);
|
||||
if (chunksLoading.add(key)) {
|
||||
final int fcx = cx, fcy = cy, fkey = key;
|
||||
Thread lt = new Thread(() -> {
|
||||
int[] pixels = WorldMapRenderer.loadChunk(fcx, fcy, CHUNK_CACHE_DIR);
|
||||
if (pixels != null) {
|
||||
WritableImage wi = new WritableImage(CS, CS);
|
||||
wi.getPixelWriter().setPixels(0, 0, CS, CS,
|
||||
javafx.scene.image.PixelFormat.getIntArgbInstance(), pixels, 0, CS);
|
||||
Platform.runLater(() -> {
|
||||
chunkCache.put(fkey, wi);
|
||||
chunksLoading.remove(fkey);
|
||||
redraw();
|
||||
});
|
||||
} else {
|
||||
chunksLoading.remove(fkey);
|
||||
}
|
||||
}, "ChunkLoader-" + fcx + "-" + fcy);
|
||||
lt.setDaemon(true);
|
||||
lt.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Canvas Tile-Overlay (zoom-unabhängige Kacheln) ────────────────────────
|
||||
|
||||
private static int tileSample(int[][] tilePx, int cat,
|
||||
@@ -492,206 +550,67 @@ public class WorldMapView extends VBox {
|
||||
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;
|
||||
private void drawForestOverlay(GraphicsContext gc) {
|
||||
if (bakedForestOverlay == null) return;
|
||||
int cW = (int) canvas.getWidth(), cH = (int) canvas.getHeight();
|
||||
gc.drawImage(bakedForestOverlay, -panX / scale, -panY / scale, cW / scale, cH / scale, 0, 0, cW, cH);
|
||||
}
|
||||
|
||||
int cW = (int) canvas.getWidth();
|
||||
int cH = (int) canvas.getHeight();
|
||||
if (cW <= 0 || cH <= 0) return;
|
||||
// ── Wald-Baking (läuft im Background-Thread) ─────────────────────────────
|
||||
|
||||
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;
|
||||
}
|
||||
private static float[] computeForestDensity(WorldMapRenderModel model) {
|
||||
final int FD = 512;
|
||||
final float WH = WorldMapRenderer.WORLD_HALF, WS = WorldMapRenderer.WORLD_SIZE;
|
||||
final float radius = 14f;
|
||||
final int rc = Math.max(1, (int)(radius / (WS / FD)));
|
||||
final float[] fd = new float[FD * FD];
|
||||
for (de.blight.common.PlacedModel pm : model.models()) {
|
||||
if (!WorldMapRenderer.isTree(pm)) continue;
|
||||
int fx = Math.max(0, Math.min(FD-1, (int)((pm.x()+WH)/WS*FD)));
|
||||
int fz = Math.max(0, Math.min(FD-1, (int)((WH-pm.z())/WS*FD)));
|
||||
for (int dz=-rc; dz<=rc; dz++) {
|
||||
for (int dx=-rc; dx<=rc; dx++) {
|
||||
if (dx*dx+dz*dz <= rc*rc) {
|
||||
int nx=fx+dx, nz=fz+dz;
|
||||
if (nx>=0&&nx<FD&&nz>=0&&nz<FD) fd[nz*FD+nx]+=1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
tileOverlayForestD = fd0;
|
||||
tileOverlayCachedScale = Double.NaN;
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
// ── 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));
|
||||
private static int[] computeForestPixels(WorldMapRenderModel model, float[] density, float threshold) {
|
||||
final int SIZE = RENDER_SIZE, FD = 512;
|
||||
final float WH = WorldMapRenderer.WORLD_HALF, WS = WorldMapRenderer.WORLD_SIZE;
|
||||
final int[][] tilePx = model.mapTilePixels();
|
||||
final int forestArgb = (tilePx != null && WorldMapRenderer.TILE_FOREST < tilePx.length
|
||||
&& tilePx[WorldMapRenderer.TILE_FOREST] != null)
|
||||
? tileSample(tilePx, WorldMapRenderer.TILE_FOREST, 0, 0, 0, 0, 1f, 0f, 0f, 0f)
|
||||
: 0x3D7A28;
|
||||
final int forestColor = 0x80000000 | (forestArgb & 0x00FFFFFF);
|
||||
final int[] px = new int[SIZE * SIZE];
|
||||
for (int py = 0; py < SIZE; py++) {
|
||||
float wz = (float) py / (SIZE - 1) * WS - WH;
|
||||
for (int bx = 0; bx < SIZE; bx++) {
|
||||
float wx = (float) bx / (SIZE - 1) * WS - WH;
|
||||
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);
|
||||
float fd=(density[fsz0*FD+fsx0]*(1-fstx)+density[fsz0*FD+fsx1]*fstx)*(1-fstz)
|
||||
+(density[fsz1*FD+fsx0]*(1-fstx)+density[fsz1*FD+fsx1]*fstx)*fstz;
|
||||
px[py*SIZE+bx] = (fd > threshold) ? forestColor : 0x00000000;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
return px;
|
||||
}
|
||||
|
||||
private void drawForestOverlay(GraphicsContext gc) {
|
||||
if (forestOverlayImg == null) return;
|
||||
gc.drawImage(forestOverlayImg, 0, 0);
|
||||
private static WritableImage toWritableImage(int[] pixels, int size) {
|
||||
WritableImage img = new WritableImage(size, size);
|
||||
img.getPixelWriter().setPixels(0, 0, size, size, PixelFormat.getIntArgbInstance(), pixels, 0, size);
|
||||
return img;
|
||||
}
|
||||
|
||||
// ── Canvas-Vektor-Overlays ────────────────────────────────────────────────
|
||||
@@ -768,82 +687,11 @@ public class WorldMapView extends VBox {
|
||||
}
|
||||
|
||||
private void drawWaterOverlay(GraphicsContext gc) {
|
||||
if (currentModel == null || heightMap == null || waterHeightMap == null) return;
|
||||
if (currentModel == null) return;
|
||||
// Wasser-Füllung kommt aus mapFxImage (bereits korrekt gerendert).
|
||||
// Hier nur noch Küstenlinie und Wellen-Vektoren.
|
||||
|
||||
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 ─────────────
|
||||
// ── Küstenlinie ───────────────────────────────────────────────────────────
|
||||
List<float[][]> seaCoastPaths = currentModel.coastPaths();
|
||||
if (cbCoastline.isSelected() && seaCoastPaths != null && !seaCoastPaths.isEmpty()) {
|
||||
double canvasW = canvas.getWidth(), canvasH = canvas.getHeight();
|
||||
@@ -1068,9 +916,10 @@ public class WorldMapView extends VBox {
|
||||
List<PlacedModel> treeList = models.stream()
|
||||
.filter(WorldMapRenderer::isTree).collect(Collectors.toList());
|
||||
List<List<PlacedModel>> tClusters = clusterTreeModels(treeList, 40f);
|
||||
int[] colorMap = WorldMapRenderer.buildTerrainColorMap(mapData, slotColors, mapTiles, voxelSurface, waters);
|
||||
return new WorldMapRenderModel(
|
||||
mapData, areas, zones, locs, waters, models, waterfalls, slotColors,
|
||||
mask, voxelSurface, tSamp, paths, tClusters, mapTiles);
|
||||
mask, voxelSurface, tSamp, paths, tClusters, mapTiles, colorMap);
|
||||
}
|
||||
|
||||
// Erzeugt interpolierte Marching-Squares-Segmente für Meer + Wasserflächen.
|
||||
@@ -1274,9 +1123,21 @@ public class WorldMapView extends VBox {
|
||||
|
||||
private void fitToView() {
|
||||
if (canvas.getWidth() <= 0 || mapFxImage == null) return;
|
||||
scale = Math.min(canvas.getWidth() / mapFxImage.getWidth(), canvas.getHeight() / mapFxImage.getHeight());
|
||||
panX = (canvas.getWidth() - mapFxImage.getWidth() * scale) / 2;
|
||||
panY = (canvas.getHeight() - mapFxImage.getHeight() * scale) / 2;
|
||||
double fitScale = Math.min(canvas.getWidth() / RENDER_SIZE, canvas.getHeight() / RENDER_SIZE);
|
||||
computeZoomLevels(fitScale);
|
||||
zoomLevelIdx = 0;
|
||||
scale = zoomLevels[0];
|
||||
panX = (canvas.getWidth() - RENDER_SIZE * scale) / 2;
|
||||
panY = (canvas.getHeight() - RENDER_SIZE * scale) / 2;
|
||||
}
|
||||
|
||||
private void computeZoomLevels(double fitScale) {
|
||||
zoomLevels = new double[ZOOM_LEVELS];
|
||||
zoomLevels[0] = fitScale;
|
||||
double ratio = Math.pow(ZOOM_MAX_SCALE / fitScale, 1.0 / (ZOOM_LEVELS - 1));
|
||||
for (int i = 1; i < ZOOM_LEVELS; i++) {
|
||||
zoomLevels[i] = zoomLevels[i-1] * ratio;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean polyContains(float[] xs, float[] zs, float px, float pz) {
|
||||
@@ -1400,7 +1261,10 @@ public class WorldMapView extends VBox {
|
||||
double cw = canvasPane.getWidth();
|
||||
double ch = canvasPane.getHeight();
|
||||
if (cw <= 0 || ch <= 0 || mapFxImage == null) return;
|
||||
scale = Math.min(cw / RENDER_SIZE, ch / RENDER_SIZE);
|
||||
double fitScale = Math.min(cw / RENDER_SIZE, ch / RENDER_SIZE);
|
||||
computeZoomLevels(fitScale);
|
||||
zoomLevelIdx = 0;
|
||||
scale = zoomLevels[0];
|
||||
panX = (cw - RENDER_SIZE * scale) / 2.0;
|
||||
panY = (ch - RENDER_SIZE * scale) / 2.0;
|
||||
redraw();
|
||||
|
||||
@@ -40,7 +40,9 @@ import de.blight.common.PlacedLocationZone;
|
||||
import de.blight.common.PlacedModel;
|
||||
import de.blight.common.PlacedModelIO;
|
||||
import de.blight.common.PlacedWater;
|
||||
import de.blight.common.PlacedWaterfall;
|
||||
import de.blight.common.WaterBodyIO;
|
||||
import de.blight.common.WaterfallIO;
|
||||
import de.blight.common.map.WorldMapRenderer;
|
||||
import de.blight.common.map.WorldMapRenderer.RenderInput;
|
||||
import de.blight.common.map.WorldMapRenderer.RenderOptions;
|
||||
@@ -546,30 +548,22 @@ public class MinimapState extends BaseAppState {
|
||||
List<Location> locs = LocationIO.load();
|
||||
List<PlacedWater> waters = WaterBodyIO.load();
|
||||
List<PlacedModel> models = PlacedModelIO.load();
|
||||
List<PlacedWaterfall> waterfalls = WaterfallIO.load();
|
||||
int[] slotColors = computeSlotColors(mapData, root);
|
||||
int[][] tilePixels = WorldMapRenderer.loadMapTiles(root);
|
||||
renderInput = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors);
|
||||
de.blight.common.map.WorldMapRenderModel renderModel = new de.blight.common.map.WorldMapRenderModel(
|
||||
mapData, areas, zones, locs, waters, models, waterfalls,
|
||||
slotColors, null, null, null, null, null, tilePixels, null);
|
||||
log.debug("[Minimap] Render-Modell bereit – {} Areas, {} Orte, {} Wasser ({} ms)",
|
||||
areas.size(), locs.size(), waters.size(), System.currentTimeMillis() - t0);
|
||||
|
||||
boolean needsRender = !Files.exists(png);
|
||||
if (!needsRender) {
|
||||
// Cache ungültig wenn Karte neuer als das PNG
|
||||
try {
|
||||
long pngMod = Files.getLastModifiedTime(png).toMillis();
|
||||
long mapMod = Files.getLastModifiedTime(MapIO.getMapPath()).toMillis();
|
||||
if (mapMod > pngMod) { needsRender = true; }
|
||||
} catch (Exception ignored) { needsRender = true; }
|
||||
}
|
||||
if (needsRender) {
|
||||
log.info("[Minimap] Rendere Weltkarte {}×{}…", TEXTURE_SIZE, TEXTURE_SIZE);
|
||||
long t1 = System.currentTimeMillis();
|
||||
BufferedImage bi = WorldMapRenderer.render(renderInput, TEXTURE_SIZE, RenderOptions.all());
|
||||
ImageIO.write(bi, "PNG", png.toFile());
|
||||
log.debug("[Minimap] Weltkarte gerendert und gespeichert ({} ms): {}",
|
||||
System.currentTimeMillis() - t1, png);
|
||||
} else {
|
||||
log.info("[Minimap] Gecachte Weltkarte: {}", png);
|
||||
}
|
||||
log.info("[Minimap] Rendere Weltkarte {}×{}…", TEXTURE_SIZE, TEXTURE_SIZE);
|
||||
long t1 = System.currentTimeMillis();
|
||||
BufferedImage bi = WorldMapRenderer.render(renderModel, TEXTURE_SIZE, RenderOptions.all());
|
||||
ImageIO.write(bi, "PNG", png.toFile());
|
||||
log.debug("[Minimap] Weltkarte gerendert und gespeichert ({} ms): {}",
|
||||
System.currentTimeMillis() - t1, png);
|
||||
|
||||
app.enqueue(() -> { onTextureReady(); return null; });
|
||||
} catch (Exception e) {
|
||||
|
||||
Reference in New Issue
Block a user