Compare commits

..

13 Commits

Author SHA1 Message Date
ca4d34b806 Weltkarte: Gras-Grün abdämpfen (125,180,72 → 158,178,112)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-03 18:48:13 +02:00
3feff60e24 Weltkarte Wald: Outline-Versatz durch Koordinaten-Mismatch behoben
Root cause: Fill nutzt fsfz = (WH-wz)/WS * FD (Skala: /FD=512),
MS-Grid nutzt msW → / (fdSize-1)=511. Ergibt ~2m Versatz in Kartenmitte.

Fix: h-Feld bei SEA_MASK_SIZE mit exakt der Fill-Formel aufbauen
(* FD statt * (FD-1)), dann MS mit thr=0 auf diesem h-Feld.
Die Isokontur liegt damit pixelgenau auf der Fill-Grenze.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-03 18:46:23 +02:00
cf11a50a21 Weltkarte Wald: Outline auf echter Density-Isokontur statt Binärmaske
Vorher: Density → binäre Maske (-1/+1) → MS bei 0.5 → Boundary 75% vom
Zellrand entfernt (falscher Offset, unabhängig von echten Density-Werten).

Jetzt: h = threshold - density → MS direkt auf Density-Werten bei thr=0
→ msLerp interpoliert genau an der echten threshold-Isokontur, identisch
zur Fill-Grenze in computeForestPixels.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-03 18:33:56 +02:00
f0a3c462b6 Weltkarte Wald: Outline-Fill-Mismatch durch Chaikin-Schrumpfen behoben
Chaikin corner-cutting bewegt Punkte nach innen → Outline lag kleiner als
die Fill-Fläche. Die Wald-Density-Boundary ist bereits glatt (Summe
kreisförmiger Kernels), braucht kein Smoothing.

- assembleClosedPaths(segs, chaikinIter) extrahiert aus buildSeaCoastPaths:
  chaikinIter=0 → Pfade ohne Glättung, liegen exakt auf der Fill-Grenze
- buildForestOutlinePaths nutzt assembleClosedPaths(..., 0)
- buildSeaCoastPaths delegiert weiterhin mit chaikinIter=3 (Küste braucht
  Glättung wegen der stepped Terrain-Height-Boundary)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-03 18:31:30 +02:00
b79ccaa1e0 Weltkarte Wald: Outline-Fill-Mismatch fix + Kleinstpolygon-Filter
- buildForestOutlinePaths: bilineare Interpolation statt NN → Outline liegt
  exakt auf der gleichen Schwelle wie die Füllung in computeForestPixels
- Perimeter-Filter: Pfade mit Umfang < 60m² werden verworfen (Artefakte
  aus 1-2 isolierten Bäumen)
- Threshold 2.0 → 3.0: 2 Bäume ergeben max. Density=2, liegen damit unter
  der Schwelle → kein Wald-Ring mehr bei Kleingruppen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-03 18:28:17 +02:00
28cc89dcb1 Weltkarte: Wald-Linien Z-Flip fix + korrekte Layer-Reihenfolge
- buildForestOutlinePaths: Density-Map hat Z invertiert (Nord=mz=0) gegenüber
  Marching-Squares-Koordinaten (Süd=mz=0) → gespiegelte Konturen landeten
  off-screen, kein sichtbarer Wald-Rand. Fix: dz = fdSize-1-mz beim Samplen.
- Render-Reihenfolge korrigiert: Klippen → Wald → Wasser → Areas
  (Wald war über Wasser, sollte darunter liegen)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-03 18:20:43 +02:00
8d6da6b013 Weltkarte Klippen: Schraffierung nur im Kliff-Band via Winding-Korrektur
- fixWindingForClip(): äußere Pfade CCW, innere Pfade (Löcher/Berg-Inneres) CW
  → gc.clip() füllt mit Non-Zero-Rule nur das echte Kliff-Band, nicht den
  gesamten eingeschlossenen Bereich inkl. Berggipfel
- Schraffierungs-Liniendicke = overlayLineWidth() (identisch zu Konturlinie)
- Schraffierungs-Abstand = 4× Liniendicke → konstante ~25% Flächendeckung
  über alle Zoom-Stufen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-03 18:06:30 +02:00
972d5d6e0b Weltkarte Klippen: Schraffierung, Zoom-abhängige Linienstärke, Wald-Stil
- Kliff-Bereiche: diagonale Schraffierung (45°, ~5.5px Abstand) im gleichen
  Grauton via gc.clip() auf die Marching-Squares-Pfade
- Klippen- und Wald-Konturlinien: 1.2px bei Zoom 0-2, 2.5px bei Zoom 3,
  3.5px bei Zoom 4 (deutlich dicker zum Reinzoomen)
- Wald-Umrandung: selber Stil und Dicke wie Klippen, aber dunkelgrün (28/78/12)
- Render-Reihenfolge bleibt: Klippen → Wasser → Wald

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-03 17:47:19 +02:00
81c9512244 Weltkarte: Vektor-Konturen für Wald/Klippen, 8m Textur-Filter, Render-Reihenfolge
- Cliff- und Wald-Randlinien via Marching Squares + Chaikin als Canvas-Vektoren
  statt Pixel-Kanten im Raster → immer scharf wie Küstenlinie und Areas
- COARSE von 512px (4m/px) auf 256px (8m/px) erhöht + Bilinear-Interpolation
  der Flachfarben → dünne Texturen (<8m) verschwinden, keine Block-Artefakte
  bei Hires-Zoom
- Render-Reihenfolge: Klippen → Wasser → Wald (laut User-Anforderung)
- buildCliffOverlay: nur noch weiche Grau-Füllung, keine pixel-basierten Kanten
- computeForestPixels: nur noch Füllung, Randlinie kommt als Vektor-Overlay

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-03 17:18:43 +02:00
ddec931ee8 Weltkarte: schematische 5-Layer-Optik
Layer 1: Flachfarben aus Splatmap (Coarse 512px filtert dünne Texturen <4m heraus, kein Hillshading)
Layer 2: Wasser-Füllung (PNG) + Küstenlinie (Canvas-Vektor), eine Checkbox
Layer 3: Wald – 50% alpha grün + dunkelgrüne Randlinie (vorgebacken)
Layer 4: Klippen – graue Füllung + Kammlinie + Fußlinie (vorgebacken, buildCliffOverlay)
Layer 5: Areas – gestrichelte Vektorpolygone (Canvas)

Entfernt: Wellenmarken, Wasserfall-Symbole, Zonen, Modell-Punkte, Orte-Labels, Baum-Symbole

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-03 16:32:18 +02:00
e8896c7a4d Weltkarte: Anpassungen vor schematischem Umbau
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-03 16:22:13 +02:00
0bf7725051 Anpassung der KArte 2026-09-03 10:44:59 +02:00
abf56f81b6 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>
2026-09-03 10:39:23 +02:00
6 changed files with 890 additions and 793 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -26,5 +26,6 @@ public record WorldMapRenderModel(
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 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
) {} ) {}

View File

@@ -29,6 +29,18 @@ public final class WorldMapRenderer {
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). */
public static final int VOXEL_SURFACE_RES = 2048; 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( public record RenderInput(
MapData mapData, MapData mapData,
@@ -146,49 +158,37 @@ public final class WorldMapRenderer {
} }
} }
// 3. Tile-Rendering: tileIdx + Rohfarbe pro Pixel speichern // 3. Terrain-Farben aus vorberechneter Farbkarte (oder intern berechnen als Fallback)
float cellW = WORLD_SIZE / (targetSize - 1); float cellW = WORLD_SIZE / (targetSize - 1);
int SS = MapData.SPLAT_SIZE; int[] slotCols = model.slotColorsRGB();
int[] slotCols = model.slotColorsRGB(); int[] colorMap = model.terrainColorMap() != null
int[][] tiles = model.mapTilePixels(); ? model.terrainColorMap()
int[] rawRGB = new int[targetSize * targetSize]; : buildTerrainColorMap(m, slotCols, model.mapTilePixels(), model.voxelSurface(), model.waters(), model.waterfalls());
int[] rawRGB = new int[targetSize * targetSize];
int waterR = (WATER_COLOR >> 16) & 0xFF; int waterR = (WATER_COLOR >> 16) & 0xFF;
int waterG = (WATER_COLOR >> 8) & 0xFF; int waterG = (WATER_COLOR >> 8) & 0xFF;
int waterB = WATER_COLOR & 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 wx = -WORLD_HALF + px * cellW;
int idx = py * targetSize + px; int idx = py * targetSize + px;
int tileIdx;
int tr, tg, tb;
if (isWater[idx]) { if (isWater[idx]) {
tileIdx = TILE_WATER; rawRGB[idx] = (waterR << 16) | (waterG << 8) | waterB;
tr = waterR; tg = waterG; tb = waterB;
} else { } else {
int dPx = Math.max(1, (int)(0.25f / cellW)); // Bilinear sample aus der Farbkarte (COLOR_MAP_SIZE → targetSize)
float hR = heights[py * targetSize + Math.min(targetSize-1, px+dPx)]; float fx = (float) px / (targetSize - 1) * (COLOR_MAP_SIZE - 1);
float hL = heights[py * targetSize + Math.max(0, px-dPx)]; float fz = (float) py / (targetSize - 1) * (COLOR_MAP_SIZE - 1);
float hD = heights[Math.min(targetSize-1, py+dPx) * targetSize + px]; int cx0 = (int) fx, cx1 = Math.min(cx0 + 1, COLOR_MAP_SIZE - 1);
float hU = heights[Math.max(0, py-dPx) * targetSize + px]; int cz0 = (int) fz, cz1 = Math.min(cz0 + 1, COLOR_MAP_SIZE - 1);
float dist = dPx * cellW; float tx = fx - cx0, tz = fz - cz0;
double slope = Math.toDegrees(Math.atan(Math.sqrt( int c00 = colorMap[cz0 * COLOR_MAP_SIZE + cx0];
Math.pow((hR-hL) / (2*dist), 2) + Math.pow((hD-hU) / (2*dist), 2)))); int c10 = colorMap[cz0 * COLOR_MAP_SIZE + cx1];
tileIdx = (slope > 30.0) ? TILE_CLIFF : getTerrainTile(m, SS, wx, wz, slotCols); int c01 = colorMap[cz1 * COLOR_MAP_SIZE + cx0];
if (tiles != null && tileIdx < tiles.length && tiles[tileIdx] != null) { int c11 = colorMap[cz1 * COLOR_MAP_SIZE + cx1];
int tu = Math.floorMod((int)Math.floor((wx + WORLD_HALF) / TILE_WU), 16); 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)
int tv = Math.floorMod((int)Math.floor((wz + WORLD_HALF) / TILE_WU), 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)
int argb = tiles[tileIdx][tv * 16 + tu]; | clamp((int)(( c00 &0xFF)*(1-tx)*(1-tz)+( c10 &0xFF)*tx*(1-tz)+( c01 &0xFF)*(1-tx)*tz+( c11 &0xFF)*tx*tz));
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;
} }
} }
@@ -223,31 +223,10 @@ public final class WorldMapRenderer {
} }
} }
float wFrac = wCount * subInv; float wFrac = wCount * subInv;
// Land-Farbe: fuer Wasser-Pixel on-the-fly berechnen, sonst aus rawRGB // Land-Farbe aus Farbkarte (Wasser-Pixel bekommen ihren Nachbar-Landwert)
int landRGB; int mapX = Math.min((int)((float)px / (targetSize-1) * (COLOR_MAP_SIZE-1)), COLOR_MAP_SIZE-1);
if (isS) { int mapZ = Math.min((int)((float)py / (targetSize-1) * (COLOR_MAP_SIZE-1)), COLOR_MAP_SIZE-1);
int dPx = Math.max(1, (int)(0.25f / cellW)); int landRGB = colorMap[mapZ * COLOR_MAP_SIZE + mapX];
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; int lr=(landRGB>>16)&0xFF, lg=(landRGB>>8)&0xFF, lb=landRGB&0xFF;
rawRGB[idx] = (clamp((int)(waterR*wFrac + lr*(1-wFrac))) << 16) rawRGB[idx] = (clamp((int)(waterR*wFrac + lr*(1-wFrac))) << 16)
| (clamp((int)(waterG*wFrac + lg*(1-wFrac))) << 8) | (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 x1 = Math.min(targetSize-1, bb2.x + bb2.width + 1);
int y1 = Math.min(targetSize-1, bb2.y + bb2.height + 1); int y1 = Math.min(targetSize-1, bb2.y + bb2.height + 1);
for (int py = y0; py <= y1; py++) { for (int py = y0; py <= y1; py++) {
float wz = -WORLD_HALF + py * cellW;
for (int px = x0; px <= x1; px++) { for (int px = x0; px <= x1; px++) {
float wx = -WORLD_HALF + px * cellW;
int idx = py * targetSize + px; int idx = py * targetSize + px;
boolean inPoly = wp.contains(px, py) && heights[idx] < wh2; boolean inPoly = wp.contains(px, py) && heights[idx] < wh2;
boolean boundary2 = false; boolean boundary2 = false;
@@ -294,31 +271,9 @@ public final class WorldMapRenderer {
} }
} }
float wFrac2 = wCount2 * subInv; float wFrac2 = wCount2 * subInv;
int landRGB2; int mapX2 = Math.min((int)((float)px / (targetSize-1) * (COLOR_MAP_SIZE-1)), COLOR_MAP_SIZE-1);
if (inPoly) { int mapZ2 = Math.min((int)((float)py / (targetSize-1) * (COLOR_MAP_SIZE-1)), COLOR_MAP_SIZE-1);
// Wasser-Pixel: Land-Farbe on-the-fly int landRGB2 = colorMap[mapZ2 * COLOR_MAP_SIZE + mapX2];
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; int lr2=(landRGB2>>16)&0xFF, lg2=(landRGB2>>8)&0xFF, lb2=landRGB2&0xFF;
rawRGB[idx] = (clamp((int)(waterR*wFrac2 + lr2*(1-wFrac2))) << 16) rawRGB[idx] = (clamp((int)(waterR*wFrac2 + lr2*(1-wFrac2))) << 16)
| (clamp((int)(waterG*wFrac2 + lg2*(1-wFrac2))) << 8) | (clamp((int)(waterG*wFrac2 + lg2*(1-wFrac2))) << 8)
@@ -334,195 +289,6 @@ public final class WorldMapRenderer {
img.setRGB(i % targetSize, i / targetSize, rawRGB[i]); img.setRGB(i % targetSize, i / targetSize, rawRGB[i]);
} }
// ── 3. Vektor-Overlays ────────────────────────────────────────────────
Graphics2D gfx = img.createGraphics();
gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
float lineW = Math.max(1.5f, targetSize / 400f);
// Wasser
if (opts.showWater()) {
// ── Wellenmarken ──────────────────────────────────────────────────────
if (opts.showWaterWaves()) {
float wmW = Math.max(20f, targetSize / 70f);
float wmA = wmW * 0.2f, spX = wmW * 1.8f, spY = wmW * 1.05f;
gfx.setColor(Color.BLACK);
gfx.setStroke(new BasicStroke(Math.max(0.7f, lineW * 0.4f),
BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
// Meer: versetzt + gejittert
int seaRow = 0;
for (float wy = spY * 0.5f; wy < targetSize; wy += spY, seaRow++) {
float rowOff = (seaRow & 1) == 1 ? spX * 0.5f : 0f;
int seaCol = 0;
for (float wx = rowOff; wx + wmW <= targetSize; wx += spX, seaCol++) {
float jx = (float)(Math.abs(Math.sin(seaRow * 73.1 + seaCol * 157.3)) * spX * 0.25);
float jy = (float)(Math.sin(seaRow * 211.7 + seaCol * 89.5) * spY * 0.2);
float ox = wx + jx, oy = wy + jy;
int cx = (int)(ox + wmW * 0.5f), cy = (int)oy;
if (cx >= 0 && cx < targetSize && cy >= 0 && cy < targetSize
&& ox >= 0 && ox + wmW < targetSize
&& seaPixels[cy * targetSize + cx]) {
Path2D mark = new Path2D.Float();
mark.moveTo(ox, oy);
mark.curveTo(ox + wmW*0.3f, oy - wmA, ox + wmW*0.7f, oy + wmA, ox + wmW, oy);
gfx.draw(mark);
}
}
}
// Wasserflächen: nur wenn groß genug
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();
if (bb.width <= wmW * 1.5f || bb.height <= spY) continue;
gfx.setClip(poly);
int wRow = 0;
for (float wy = bb.y + spY * 0.5f; wy < bb.y + bb.height; wy += spY, wRow++) {
float rowOff = (wRow & 1) == 1 ? spX * 0.5f : 0f;
int wCol = 0;
for (float wx = bb.x + rowOff; wx + wmW <= bb.x + bb.width; wx += spX, wCol++) {
float jx = (float)(Math.abs(Math.sin(wRow * 73.1 + wCol * 157.3)) * spX * 0.25);
float jy = (float)(Math.sin(wRow * 211.7 + wCol * 89.5) * spY * 0.2);
float ox = wx + jx, oy = wy + jy;
if (ox < bb.x || ox + wmW > bb.x + bb.width) continue;
Path2D mark = new Path2D.Float();
mark.moveTo(ox, oy);
mark.curveTo(ox + wmW*0.3f, oy - wmA, ox + wmW*0.7f, oy + wmA, ox + wmW, oy);
gfx.draw(mark);
}
}
gfx.setClip(null);
}
}
}
// Wasserfälle – klassisches Kartensymbol: Kammlinie + Kaskaden-Linien
if (opts.showWaterfalls() && model.waterfalls() != null && !model.waterfalls().isEmpty()) {
float wfLineW = Math.max(1.5f, targetSize / 800f);
for (PlacedWaterfall wf : model.waterfalls()) {
// Kamm A→B und Basis D→C in Pixel-Koordinaten
int pax = worldToPixel(wf.ax(), targetSize), paz = worldToPixel(wf.az(), targetSize);
int pbx = worldToPixel(wf.bx(), targetSize), pbz = worldToPixel(wf.bz(), targetSize);
int pdx = worldToPixel(wf.dx(), targetSize), pdz = worldToPixel(wf.dz(), targetSize);
int pcx = worldToPixel(wf.cx(), targetSize), pcz = worldToPixel(wf.cz(), targetSize);
// Kammrichtung in Pixel-Space
float cdx = pbx - pax, cdz = pbz - paz;
float clen = (float) Math.sqrt(cdx * cdx + cdz * cdz);
if (clen < 1f) { continue; }
float ncx = cdx / clen, ncz = cdz / clen;
// Fließrichtung: Basismitten → Kammmitte im Pixel-Space
float pmcx = (pax + pbx) * 0.5f, pmcz = (paz + pbz) * 0.5f;
float pmbx = (pdx + pcx) * 0.5f, pmbz = (pdz + pcz) * 0.5f;
float fvx = pmbx - pmcx, fvz = pmbz - pmcz;
float flen = (float) Math.sqrt(fvx * fvx + fvz * fvz);
float perpX, perpZ;
if (flen > 2f) {
// echte XZ-Fallrichtung verfügbar
perpX = fvx / flen;
perpZ = fvz / flen;
} else {
// Wasserfall senkrecht → Rechtsnormale der Kammlinie als Fallback
perpX = ncz;
perpZ = -ncx;
}
// Pixelabstand pro Kaskaden-Stufe (mindestens 3 px, skaliert mit Bild)
float step = Math.max(3f, targetSize / 512f) * 1.8f;
// ① Dunkler Halo unter der Kammlinie
gfx.setStroke(new BasicStroke(wfLineW * 2.5f + 1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
gfx.setColor(new Color(10, 50, 120, 160));
gfx.drawLine(pax, paz, pbx, pbz);
// ② Helle Kammlinie (Kante, über die das Wasser fällt)
gfx.setStroke(new BasicStroke(wfLineW * 2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
gfx.setColor(new Color(220, 240, 255, 255));
gfx.drawLine(pax, paz, pbx, pbz);
// ③ Kaskaden-Linien: 2 parallele Linien in Fallrichtung, jede schwächer
int[] alpha = {190, 100};
float[] wf2 = {wfLineW * 1.3f, wfLineW * 0.8f};
for (int ci = 0; ci < 2; ci++) {
float off = step * (ci + 1);
int ox1 = Math.round(pax + perpX * off), oz1 = Math.round(paz + perpZ * off);
int ox2 = Math.round(pbx + perpX * off), oz2 = Math.round(pbz + perpZ * off);
gfx.setStroke(new BasicStroke(wf2[ci], BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
gfx.setColor(new Color(160, 210, 255, alpha[ci]));
gfx.drawLine(ox1, oz1, ox2, oz2);
}
}
}
// Area-Polygone
if (opts.showAreas()) {
float dash = lineW * 7f, gap = lineW * 4f;
BasicStroke dashed = new BasicStroke(lineW * 1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND,
10f, new float[]{dash, gap}, 0f);
int aFontSize = Math.max(8, targetSize / 160);
gfx.setFont(new Font("SansSerif", Font.BOLD, aFontSize));
for (PlacedArea a : model.areas()) {
int[] xs = worldToPixels(a.pointsX(), targetSize);
int[] ys = worldToPixels(a.pointsZ(), targetSize);
gfx.setStroke(dashed);
gfx.setColor(Color.BLACK);
gfx.draw(toPath(xs, ys));
String lbl = friendlyAreaName(a.areaId());
if (lbl != null) {
float[] cen = centroid(a.pointsX(), a.pointsZ());
float lx = Float.isNaN(a.labelX()) ? cen[0] : a.labelX();
float lz = Float.isNaN(a.labelZ()) ? cen[1] : a.labelZ();
int plx = worldToPixel(lx, targetSize);
int plz = worldToPixel(lz, targetSize);
drawLabel(gfx, lbl, plx, plz);
}
}
}
// Location-Zonen
if (opts.showZones()) {
gfx.setStroke(new BasicStroke(lineW));
for (PlacedLocationZone z : model.zones()) {
int[] xs = worldToPixels(z.pointsX(), targetSize);
int[] ys = worldToPixels(z.pointsZ(), targetSize);
gfx.setColor(new Color(240, 190, 40, 70));
gfx.fillPolygon(xs, ys, xs.length);
gfx.setColor(new Color(200, 150, 20));
gfx.drawPolygon(xs, ys, xs.length);
}
}
if (opts.showModels()) {
int dotR = Math.max(1, targetSize / 600);
gfx.setColor(new Color(160, 80, 20, 200));
for (PlacedModel pm : model.models()) {
if (isTree(pm)) { continue; }
int mx = worldToPixel(pm.x(), targetSize);
int mz = worldToPixel(pm.z(), targetSize);
gfx.fillRect(mx - dotR, mz - dotR, dotR * 2 + 1, dotR * 2 + 1);
}
}
// Locations (nur Label)
if (opts.showLocations()) {
int fontSize = Math.max(8, targetSize / 140);
gfx.setFont(new Font("SansSerif", Font.BOLD, fontSize));
for (Location loc : model.locations()) {
if (!loc.isShowOnMap()) continue;
if (loc.getId() == null || loc.getId().isEmpty()) continue;
float wx = Float.isNaN(loc.getLabelX()) ? loc.getCenterX() : loc.getLabelX();
float wz = Float.isNaN(loc.getLabelZ()) ? loc.getCenterZ() : loc.getLabelZ();
int lx = worldToPixel(wx, targetSize);
int lz = worldToPixel(wz, targetSize);
drawLabel(gfx, friendlyLocationName(loc.getId()), lx, lz);
}
}
gfx.dispose();
return img; return img;
} }
@@ -531,7 +297,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, null input.slotColorsRGB(), null, null, null, null, null, null, null
), targetSize, opts); ), targetSize, opts);
} }
@@ -1007,6 +773,41 @@ public final class WorldMapRenderer {
return map; return map;
} }
/** Erzeugt eine Maske: true für jeden Pixel, der innerhalb eines Wasserfall-Quads liegt.
* Nutzt Graphics2D-Rasterisierung inkl. Stroke, damit auch schmale Quads erfasst werden. */
private static boolean[] buildWaterfallMask(List<PlacedWaterfall> waterfalls, int size) {
boolean[] mask = new boolean[size * size];
if (waterfalls == null || waterfalls.isEmpty()) return mask;
// BYTE_BINARY: 1 bit/px → 512 KB bei 2048px, 8 MB bei 8192px
BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_BYTE_BINARY);
Graphics2D g = img.createGraphics();
g.setColor(java.awt.Color.WHITE);
float strokeW = Math.max(4f, size / 512f); // mind. 4px Strichbreite für dünne Quads
g.setStroke(new BasicStroke(strokeW, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
for (PlacedWaterfall wf : waterfalls) {
int[] xs = {
worldToPixel(wf.ax(), size), worldToPixel(wf.bx(), size),
worldToPixel(wf.cx(), size), worldToPixel(wf.dx(), size)
};
int[] zs = {
worldToPixel(wf.az(), size), worldToPixel(wf.bz(), size),
worldToPixel(wf.cz(), size), worldToPixel(wf.dz(), size)
};
g.fillPolygon(xs, zs, 4); // Füllung
g.drawPolygon(xs, zs, 4); // Stroke-Backup für schmale Quads
}
g.dispose();
java.awt.image.Raster raster = img.getData();
int[] px = new int[1];
for (int py = 0; py < size; py++) {
for (int x = 0; x < size; x++) {
raster.getPixel(x, py, px);
if (px[0] != 0) mask[py * size + x] = true;
}
}
return mask;
}
/** Lädt die 6 Karten-Kacheln aus Textures/map/ im Asset-Verzeichnis. */ /** Lädt die 6 Karten-Kacheln aus Textures/map/ im Asset-Verzeichnis. */
public static int[][] loadMapTiles(Path assetRoot) { public static int[][] loadMapTiles(Path assetRoot) {
int[][] result = new int[TILE_FILE_NAMES.length][]; int[][] result = new int[TILE_FILE_NAMES.length][];
@@ -1243,6 +1044,341 @@ public final class WorldMapRenderer {
return m.modelPath().replace('\\', '/').toLowerCase().contains("/trees/"); 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;
}
/** Schematische Flachfarbe pro Tile-Typ (kein Hillshading, keine Klippen-Blend). */
private static int schematicColor(int tileType) {
return switch (tileType) {
case TILE_GRASS -> (158 << 16) | (178 << 8) | 112;
case TILE_DIRT -> (182 << 16) | (138 << 8) | 80;
case TILE_SAND -> (212 << 16) | (198 << 8) | 128;
case TILE_ROCK -> (162 << 16) | (155 << 8) | 148;
case TILE_PAVEMENT -> (155 << 16) | (145 << 8) | 128;
default -> (158 << 16) | (178 << 8) | 112;
};
}
/**
* Baut die schematische Terrain-Farbkarte: Tile-Typ bei 512×512 (4m/px) bestimmen,
* dann per Nearest-Neighbor auf COLOR_MAP_SIZE hochskalieren und Flachfarbe zuweisen.
* Dünne Texturen (<4m) werden durch das Coarse-Sampling automatisch gefiltert.
*/
public static int[] buildTerrainColorMap(MapData m, int[] slotColors, int[][] tilePixels,
float[] voxelSurface, List<PlacedWater> waters,
List<PlacedWaterfall> waterfalls) {
final int SIZE = COLOR_MAP_SIZE;
final int SS = MapData.SPLAT_SIZE;
float[] heights = buildTerrainSamples(m, SIZE, voxelSurface);
float[] waterH = buildWaterHeightMap(waters, SIZE);
// Coarse-Typ-Map bei 256×256 (8m/px) – filtert Texturen <8m heraus
int[] coarseColors = buildCoarseColorMap(m, SS, slotColors, 256);
// Bilinear-Interpolation der Flachfarben auf SIZE×SIZE → keine Block-Artefakte
int[] result = new int[SIZE * SIZE];
for (int py = 0; py < SIZE; py++) {
for (int px = 0; px < SIZE; px++) {
int idx = py * SIZE + px;
float h = heights[idx];
if (h < 0f || (waterH[idx] > 0f && waterH[idx] > h)) {
int[] wc = TILE_FALLBACK_RGB[TILE_WATER];
result[idx] = (wc[0] << 16) | (wc[1] << 8) | wc[2];
} else {
result[idx] = sampleCoarseBilinear(coarseColors, 256, px, py, SIZE);
}
}
}
return result;
}
/** Baut eine Coarse-Farbkarte bei gegebener Auflösung: 1 Pixel = WORLD_SIZE/coarseSize m. */
private static int[] buildCoarseColorMap(MapData m, int SS, int[] slotColors, int coarseSize) {
float cellW = WORLD_SIZE / (coarseSize - 1);
int[] colors = new int[coarseSize * coarseSize];
for (int mz = 0; mz < coarseSize; mz++) {
float wz = -WORLD_HALF + mz * cellW;
for (int mx = 0; mx < coarseSize; mx++) {
float wx = -WORLD_HALF + mx * cellW;
colors[mz * coarseSize + mx] = schematicColor(getTerrainTile(m, SS, wx, wz, slotColors));
}
}
return colors;
}
/** Bilinear-Sample einer Coarse-Farbkarte (packed RGB) auf einen Ziel-Pixel. */
private static int sampleCoarseBilinear(int[] coarseColors, int coarseSize, int px, int py, int targetSize) {
float fx = (float) px / (targetSize - 1) * (coarseSize - 1);
float fz = (float) py / (targetSize - 1) * (coarseSize - 1);
int cx0 = (int) fx, cx1 = Math.min(cx0 + 1, coarseSize - 1);
int cz0 = (int) fz, cz1 = Math.min(cz0 + 1, coarseSize - 1);
float tx = fx - cx0, tz = fz - cz0;
int c00 = coarseColors[cz0 * coarseSize + cx0];
int c10 = coarseColors[cz0 * coarseSize + cx1];
int c01 = coarseColors[cz1 * coarseSize + cx0];
int c11 = coarseColors[cz1 * coarseSize + cx1];
int r = 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));
int g = 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));
int b = clamp((int)((c00&0xFF)*(1-tx)*(1-tz)+(c10&0xFF)*tx*(1-tz)+(c01&0xFF)*(1-tx)*tz+(c11&0xFF)*tx*tz));
return (r << 16) | (g << 8) | b;
}
/**
* Berechnet das schematische Cliff-Overlay (ARGB, COLOR_MAP_SIZE×COLOR_MAP_SIZE):
* graue Füllung wo Steigung > 30°, dunklere Linie an der oberen Kante (Kamm),
* mittlere Linie an der unteren Kante (Fuß).
*/
public static int[] buildCliffOverlay(MapData m, float[] voxelSurface) {
final int SIZE = COLOR_MAP_SIZE;
float[] heights = buildTerrainSamples(m, SIZE, voxelSurface);
float cellW = WORLD_SIZE / (SIZE - 1);
int dPx = Math.max(1, (int)(1.5f / cellW));
float[] floatMask = new float[SIZE * SIZE];
for (int py = 0; py < SIZE; py++) {
for (int px = 0; px < SIZE; px++) {
int idx = py * SIZE + px;
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))));
floatMask[idx] = slope > 30.0 ? 1f : 0f;
}
}
float[] softMask = boxBlurFloat(floatMask, SIZE, 3);
int[] result = new int[SIZE * SIZE];
for (int i = 0; i < SIZE * SIZE; i++) {
if (softMask[i] > 0.05f) {
int alpha = clamp((int)(softMask[i] * 85));
result[i] = (alpha << 24) | 0x999090;
}
}
return result;
}
/**
* 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,
List<PlacedWaterfall> waterfalls,
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 written = 0;
// Coarse-Farbkarte bei 256×256 (8m/px) – identische Filterung wie buildTerrainColorMap
int[] coarseColors = buildCoarseColorMap(m, SS, slotColors, 256);
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;
int[] chunk = new int[CS * CS];
for (int ly = 0; ly < CS; ly++) {
int gy = cy * CS + ly;
for (int lx = 0; lx < CS; lx++) {
int gx = cx * CS + lx;
int hidx = gy * SIZE + gx;
int tr, tg, tb;
float h = heights[hidx];
if (h < 0f || (waterH[hidx] > 0f && waterH[hidx] > h)) {
tr = (WATER_COLOR >> 16) & 0xFF;
tg = (WATER_COLOR >> 8) & 0xFF;
tb = WATER_COLOR & 0xFF;
} else {
int col = sampleCoarseBilinear(coarseColors, 256, gx, gy, SIZE);
tr = (col >> 16) & 0xFF;
tg = (col >> 8) & 0xFF;
tb = col & 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 auf float-Array (horizontal + vertikal), Radius in Pixeln. */
private static float[] boxBlurFloat(float[] src, int size, int radius) {
float[] tmp = new float[size * size];
float[] dst = new float[size * size];
float inv = 1f / (radius * 2 + 1);
for (int py = 0; py < size; py++) {
for (int px = 0; px < size; px++) {
float sum = 0;
for (int dx = -radius; dx <= radius; dx++) {
sum += src[py * size + Math.max(0, Math.min(size-1, px+dx))];
}
tmp[py * size + px] = sum * inv;
}
}
for (int py = 0; py < size; py++) {
for (int px = 0; px < size; px++) {
float sum = 0;
for (int dz = -radius; dz <= radius; dz++) {
sum += tmp[Math.max(0, Math.min(size-1, py+dz)) * size + px];
}
dst[py * size + px] = sum * inv;
}
}
return dst;
}
/** 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 * Kuwahara-Filter: Für jeden Pixel das Quadranten-Fenster mit der kleinsten
* Varianz wählen und dessen Mittelwert setzen → Ölgemälde-/gemalt-Effekt. * Varianz wählen und dessen Mittelwert setzen → Ölgemälde-/gemalt-Effekt.

View File

@@ -563,8 +563,15 @@ public class EditorApp extends Application {
javafx.animation.Timeline[] pollerRef = new javafx.animation.Timeline[1]; javafx.animation.Timeline[] pollerRef = new javafx.animation.Timeline[1];
pollerRef[0] = new javafx.animation.Timeline( pollerRef[0] = new javafx.animation.Timeline(
new javafx.animation.KeyFrame(javafx.util.Duration.millis(100), ev -> { new javafx.animation.KeyFrame(javafx.util.Duration.millis(100), ev -> {
splashStatus.setText(input.loadingStatus); boolean jmeOk = input.jmeReady;
if (input.jmeReady) { boolean mapOk = worldMapView != null && worldMapView.isMapReady();
if (!jmeOk) {
splashStatus.setText(input.loadingStatus);
} else {
splashStatus.setText("Karte: " + (worldMapView != null
? worldMapView.getLoadingStatus() : "…"));
}
if (jmeOk && mapOk) {
pollerRef[0].stop(); pollerRef[0].stop();
stage.show(); stage.show();
restoreEditorPrefs(); restoreEditorPrefs();
@@ -5646,30 +5653,34 @@ public class EditorApp extends Application {
if (isSelected) mapObjectsView.refresh(); if (isSelected) mapObjectsView.refresh();
}); });
worldMapView = new de.blight.editor.ui.WorldMapView(() -> primaryStage); if (worldMapView == null) {
worldMapView.setVoxelChunkSupplier(() -> { worldMapView = new de.blight.editor.ui.WorldMapView(() -> primaryStage);
de.blight.editor.state.VoxelEditorState ves = jmeApp == null ? null worldMapView.setVoxelChunkSupplier(() -> {
: jmeApp.getStateManager().getState(de.blight.editor.state.VoxelEditorState.class); de.blight.editor.state.VoxelEditorState ves = jmeApp == null ? null
return ves != null ? ves.getChunksSnapshot() : de.blight.common.VoxelChunkIO.loadAll(); : jmeApp.getStateManager().getState(de.blight.editor.state.VoxelEditorState.class);
}); return ves != null ? ves.getChunksSnapshot() : de.blight.common.VoxelChunkIO.loadAll();
worldMapView.setCameraInfoSupplier(() -> });
new de.blight.editor.ui.WorldMapView.CameraInfo( worldMapView.setCameraInfoSupplier(() ->
input.camX, input.camY, input.camZ, input.camYaw)); new de.blight.editor.ui.WorldMapView.CameraInfo(
worldMapView.setTeleportCallback(pos -> { input.camX, input.camY, input.camZ, input.camYaw));
input.pendingGotoX = pos[0]; worldMapView.setTeleportCallback(pos -> {
input.pendingGotoY = pos[1]; input.pendingGotoX = pos[0];
input.pendingGotoZ = pos[2]; input.pendingGotoY = pos[1];
}); input.pendingGotoZ = pos[2];
});
worldMapView.setRefreshAction(() -> {
input.saveRequested = true;
pendingAfterSave = worldMapView::loadAndRender;
setStatus("Speichere vor Weltkarten-Refresh…");
});
worldMapView.loadAndRender(); // Vorladen beim Start, nicht erst beim Tab-Öffnen
}
weltkartTab = new Tab("Weltkarte", worldMapView); weltkartTab = new Tab("Weltkarte", worldMapView);
weltkartTab.setClosable(false); weltkartTab.setClosable(false);
weltkartTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> { weltkartTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> {
if (isSelected && !worldMapView.isLoaded()) worldMapView.loadAndRender(); if (isSelected && !worldMapView.isLoaded()) worldMapView.loadAndRender();
}); else if (isSelected) worldMapView.ensureFitToView();
worldMapView.setRefreshAction(() -> {
input.saveRequested = true;
pendingAfterSave = worldMapView::loadAndRender;
setStatus("Speichere vor Weltkarten-Refresh…");
}); });
worldMapView.setFullscreenCallbacks( worldMapView.setFullscreenCallbacks(
() -> { () -> {

View File

@@ -40,7 +40,9 @@ import de.blight.common.PlacedLocationZone;
import de.blight.common.PlacedModel; import de.blight.common.PlacedModel;
import de.blight.common.PlacedModelIO; import de.blight.common.PlacedModelIO;
import de.blight.common.PlacedWater; import de.blight.common.PlacedWater;
import de.blight.common.PlacedWaterfall;
import de.blight.common.WaterBodyIO; import de.blight.common.WaterBodyIO;
import de.blight.common.WaterfallIO;
import de.blight.common.map.WorldMapRenderer; import de.blight.common.map.WorldMapRenderer;
import de.blight.common.map.WorldMapRenderer.RenderInput; import de.blight.common.map.WorldMapRenderer.RenderInput;
import de.blight.common.map.WorldMapRenderer.RenderOptions; import de.blight.common.map.WorldMapRenderer.RenderOptions;
@@ -546,30 +548,22 @@ public class MinimapState extends BaseAppState {
List<Location> locs = LocationIO.load(); List<Location> locs = LocationIO.load();
List<PlacedWater> waters = WaterBodyIO.load(); List<PlacedWater> waters = WaterBodyIO.load();
List<PlacedModel> models = PlacedModelIO.load(); List<PlacedModel> models = PlacedModelIO.load();
List<PlacedWaterfall> waterfalls = WaterfallIO.load();
int[] slotColors = computeSlotColors(mapData, root); int[] slotColors = computeSlotColors(mapData, root);
int[][] tilePixels = WorldMapRenderer.loadMapTiles(root);
renderInput = new RenderInput(mapData, areas, zones, locs, waters, models, slotColors); 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)", log.debug("[Minimap] Render-Modell bereit – {} Areas, {} Orte, {} Wasser ({} ms)",
areas.size(), locs.size(), waters.size(), System.currentTimeMillis() - t0); areas.size(), locs.size(), waters.size(), System.currentTimeMillis() - t0);
boolean needsRender = !Files.exists(png); log.info("[Minimap] Rendere Weltkarte {}×{}…", TEXTURE_SIZE, TEXTURE_SIZE);
if (!needsRender) { long t1 = System.currentTimeMillis();
// Cache ungültig wenn Karte neuer als das PNG BufferedImage bi = WorldMapRenderer.render(renderModel, TEXTURE_SIZE, RenderOptions.all());
try { ImageIO.write(bi, "PNG", png.toFile());
long pngMod = Files.getLastModifiedTime(png).toMillis(); log.debug("[Minimap] Weltkarte gerendert und gespeichert ({} ms): {}",
long mapMod = Files.getLastModifiedTime(MapIO.getMapPath()).toMillis(); System.currentTimeMillis() - t1, png);
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);
}
app.enqueue(() -> { onTextureReady(); return null; }); app.enqueue(() -> { onTextureReady(); return null; });
} catch (Exception e) { } catch (Exception e) {