Compare commits
13 Commits
43829a42f3
...
ca4d34b806
| Author | SHA1 | Date | |
|---|---|---|---|
| ca4d34b806 | |||
| 3feff60e24 | |||
| cf11a50a21 | |||
| f0a3c462b6 | |||
| b79ccaa1e0 | |||
| 28cc89dcb1 | |||
| 8d6da6b013 | |||
| 972d5d6e0b | |||
| 81c9512244 | |||
| ddec931ee8 | |||
| e8896c7a4d | |||
| 0bf7725051 | |||
| abf56f81b6 |
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(), model.waterfalls());
|
||||
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)
|
||||
@@ -334,195 +289,6 @@ public final class WorldMapRenderer {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -531,7 +297,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);
|
||||
}
|
||||
|
||||
@@ -1007,6 +773,41 @@ public final class WorldMapRenderer {
|
||||
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. */
|
||||
public static int[][] loadMapTiles(Path assetRoot) {
|
||||
int[][] result = new int[TILE_FILE_NAMES.length][];
|
||||
@@ -1243,6 +1044,341 @@ 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;
|
||||
}
|
||||
|
||||
/** 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
|
||||
* Varianz wählen und dessen Mittelwert setzen → Ölgemälde-/gemalt-Effekt.
|
||||
|
||||
@@ -563,8 +563,15 @@ public class EditorApp extends Application {
|
||||
javafx.animation.Timeline[] pollerRef = new javafx.animation.Timeline[1];
|
||||
pollerRef[0] = new javafx.animation.Timeline(
|
||||
new javafx.animation.KeyFrame(javafx.util.Duration.millis(100), ev -> {
|
||||
splashStatus.setText(input.loadingStatus);
|
||||
if (input.jmeReady) {
|
||||
boolean jmeOk = 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();
|
||||
stage.show();
|
||||
restoreEditorPrefs();
|
||||
@@ -5646,30 +5653,34 @@ public class EditorApp extends Application {
|
||||
if (isSelected) mapObjectsView.refresh();
|
||||
});
|
||||
|
||||
worldMapView = new de.blight.editor.ui.WorldMapView(() -> primaryStage);
|
||||
worldMapView.setVoxelChunkSupplier(() -> {
|
||||
de.blight.editor.state.VoxelEditorState ves = jmeApp == null ? null
|
||||
: jmeApp.getStateManager().getState(de.blight.editor.state.VoxelEditorState.class);
|
||||
return ves != null ? ves.getChunksSnapshot() : de.blight.common.VoxelChunkIO.loadAll();
|
||||
});
|
||||
worldMapView.setCameraInfoSupplier(() ->
|
||||
new de.blight.editor.ui.WorldMapView.CameraInfo(
|
||||
input.camX, input.camY, input.camZ, input.camYaw));
|
||||
worldMapView.setTeleportCallback(pos -> {
|
||||
input.pendingGotoX = pos[0];
|
||||
input.pendingGotoY = pos[1];
|
||||
input.pendingGotoZ = pos[2];
|
||||
});
|
||||
if (worldMapView == null) {
|
||||
worldMapView = new de.blight.editor.ui.WorldMapView(() -> primaryStage);
|
||||
worldMapView.setVoxelChunkSupplier(() -> {
|
||||
de.blight.editor.state.VoxelEditorState ves = jmeApp == null ? null
|
||||
: jmeApp.getStateManager().getState(de.blight.editor.state.VoxelEditorState.class);
|
||||
return ves != null ? ves.getChunksSnapshot() : de.blight.common.VoxelChunkIO.loadAll();
|
||||
});
|
||||
worldMapView.setCameraInfoSupplier(() ->
|
||||
new de.blight.editor.ui.WorldMapView.CameraInfo(
|
||||
input.camX, input.camY, input.camZ, input.camYaw));
|
||||
worldMapView.setTeleportCallback(pos -> {
|
||||
input.pendingGotoX = pos[0];
|
||||
input.pendingGotoY = pos[1];
|
||||
input.pendingGotoZ = pos[2];
|
||||
});
|
||||
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.setClosable(false);
|
||||
weltkartTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> {
|
||||
if (isSelected && !worldMapView.isLoaded()) worldMapView.loadAndRender();
|
||||
});
|
||||
worldMapView.setRefreshAction(() -> {
|
||||
input.saveRequested = true;
|
||||
pendingAfterSave = worldMapView::loadAndRender;
|
||||
setStatus("Speichere vor Weltkarten-Refresh…");
|
||||
else if (isSelected) worldMapView.ensureFitToView();
|
||||
});
|
||||
worldMapView.setFullscreenCallbacks(
|
||||
() -> {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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