diff --git a/blight-common/src/main/java/de/blight/common/map/WorldMapRenderer.java b/blight-common/src/main/java/de/blight/common/map/WorldMapRenderer.java index 7b28cc2..10baafe 100644 --- a/blight-common/src/main/java/de/blight/common/map/WorldMapRenderer.java +++ b/blight-common/src/main/java/de/blight/common/map/WorldMapRenderer.java @@ -1223,22 +1223,22 @@ public final class WorldMapRenderer { float[] voxelSurface, List 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]; + 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[] terrainRGB = new int[SIZE * SIZE]; + float[] cliffMask = new float[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]; + terrainRGB[idx] = (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)]; @@ -1248,15 +1248,24 @@ public final class WorldMapRenderer { 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); - } + cliffMask[idx] = (slope > 30.0) ? 1f : 0f; + terrainRGB[idx] = blendTileAvgColors(m, SS, wx, wz, tileAvg, slotTypes); } - result[idx] = rgb; } } + // Cliff-Maske weichzeichnen → weiche Klippen-Kanten ohne Änderung der Schwellwert-Logik + float[] softMask = boxBlurFloat(cliffMask, SIZE, 3); + int cliffRgb = tileAvg[Math.min(TILE_CLIFF, tileAvg.length-1)]; + int cr = (cliffRgb>>16)&0xFF, cg = (cliffRgb>>8)&0xFF, cb = cliffRgb&0xFF; + int[] result = new int[SIZE * SIZE]; + for (int i = 0; i < SIZE * SIZE; i++) { + float t = softMask[i]; + int tc = terrainRGB[i]; + int r = (tc>>16)&0xFF, g = (tc>>8)&0xFF, b = tc&0xFF; + result[i] = (clamp((int)(r + (cr-r)*t)) << 16) + | (clamp((int)(g + (cg-g)*t)) << 8) + | clamp((int)(b + (cb-b)*t)); + } return boxBlur(result, SIZE, 2); } @@ -1295,38 +1304,67 @@ public final class WorldMapRenderer { float cellW = WORLD_SIZE / (SIZE - 1); int written = 0; + final int CLIFF_BLUR_R = 4; // Blur-Radius für weiche Klippen-Kanten + final int EXT = CLIFF_BLUR_R; + final int EXTW = CS + 2 * EXT; // erweitertes Fenster inkl. Rand für nahtlosen Blur + int dPxHires = Math.max(1, (int)(1.5f / cellW)); + 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 + + // Erweiterte Cliff-Maske + Terrain-Farben (mit Rand über Chunk-Grenze) + float[] cliffMaskExt = new float[EXTW * EXTW]; + boolean[] isWaterExt = new boolean[EXTW * EXTW]; + int[] terrainColExt = new int[EXTW * EXTW]; + for (int ely = 0; ely < EXTW; ely++) { + int gy = cy * CS + ely - EXT; + int gyC = Math.max(0, Math.min(SIZE-1, gy)); + float wz = -WORLD_HALF + gyC * cellW; + for (int elx = 0; elx < EXTW; elx++) { + int gx = cx * CS + elx - EXT; + int gxC = Math.max(0, Math.min(SIZE-1, gx)); + float wx = -WORLD_HALF + gxC * cellW; + int eidx = ely * EXTW + elx; + int hidx = gyC * SIZE + gxC; + float h = heights[hidx]; + if (h < 0f || (waterH[hidx] > 0f && waterH[hidx] > h)) { + isWaterExt[eidx] = true; + } else { + float hR = heights[gyC * SIZE + Math.min(SIZE-1, gxC+dPxHires)]; + float hL = heights[gyC * SIZE + Math.max(0, gxC-dPxHires)]; + float hD = heights[Math.min(SIZE-1, gyC+dPxHires) * SIZE + gxC]; + float hU = heights[Math.max(0, gyC-dPxHires) * SIZE + gxC]; + float dist = dPxHires * cellW; + double slope = Math.toDegrees(Math.atan(Math.sqrt( + Math.pow((hR-hL)/(2*dist),2) + Math.pow((hD-hU)/(2*dist),2)))); + cliffMaskExt[eidx] = (slope > 30.0) ? 1f : 0f; + terrainColExt[eidx] = blendTileAvgColors(m, SS, wx, wz, tileAvg, slotTypes); + } + } + } + float[] softMaskExt = boxBlurFloat(cliffMaskExt, EXTW, CLIFF_BLUR_R); + int cliffRgb = tileAvg[Math.min(TILE_CLIFF, tileAvg.length-1)]; + int cr = (cliffRgb>>16)&0xFF, cg = (cliffRgb>>8)&0xFF, cb = cliffRgb&0xFF; + + // Chunk-Pixel aus dem inneren Bereich der erweiterten Maske zusammensetzen 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 extIdx = (ly + EXT) * EXTW + (lx + EXT); int tr, tg, tb; - if (h < 0f || (waterH[idx] > 0f && waterH[idx] > h)) { + if (isWaterExt[extIdx]) { 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; + float t = softMaskExt[extIdx]; + int tc = terrainColExt[extIdx]; + int r2 = (tc>>16)&0xFF, g2 = (tc>>8)&0xFF, b2 = tc&0xFF; + tr = clamp((int)(r2 + (cr-r2)*t)); + tg = clamp((int)(g2 + (cg-g2)*t)); + tb = clamp((int)(b2 + (cb-b2)*t)); } chunk[ly * CS + lx] = (0xFF << 24) | (tr << 16) | (tg << 8) | tb; } @@ -1360,6 +1398,32 @@ public final class WorldMapRenderer { 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];