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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user