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>
This commit is contained in:
2026-09-03 17:18:43 +02:00
parent ddec931ee8
commit 81c9512244
2 changed files with 141 additions and 105 deletions

View File

@@ -1080,24 +1080,15 @@ public final class WorldMapRenderer {
float[] voxelSurface, List<PlacedWater> waters,
List<PlacedWaterfall> waterfalls) {
final int SIZE = COLOR_MAP_SIZE;
final int COARSE = 512;
final int SS = MapData.SPLAT_SIZE;
float[] heights = buildTerrainSamples(m, SIZE, voxelSurface);
float[] waterH = buildWaterHeightMap(waters, SIZE);
float cellWC = WORLD_SIZE / (COARSE - 1);
// Coarse-Typ-Map bei 512×512 (4m/px) – filtert dünne Texturen heraus
byte[] coarseTypes = new byte[COARSE * COARSE];
for (int mz = 0; mz < COARSE; mz++) {
float wz = -WORLD_HALF + mz * cellWC;
for (int mx = 0; mx < COARSE; mx++) {
float wx = -WORLD_HALF + mx * cellWC;
coarseTypes[mz * COARSE + mx] = (byte) getTerrainTile(m, SS, wx, wz, slotColors);
}
}
// Coarse-Typ-Map bei 256×256 (8m/px) – filtert Texturen <8m heraus
int[] coarseColors = buildCoarseColorMap(m, SS, slotColors, 256);
// Hochskalieren (NN) auf SIZE×SIZE und Flachfarbe zuweisen
// 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++) {
@@ -1107,15 +1098,44 @@ public final class WorldMapRenderer {
int[] wc = TILE_FALLBACK_RGB[TILE_WATER];
result[idx] = (wc[0] << 16) | (wc[1] << 8) | wc[2];
} else {
int cx = Math.min(COARSE - 1, (int)((float) px / (SIZE - 1) * (COARSE - 1)));
int cz = Math.min(COARSE - 1, (int)((float) py / (SIZE - 1) * (COARSE - 1)));
result[idx] = schematicColor(coarseTypes[cz * COARSE + cx] & 0xFF);
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),
@@ -1127,9 +1147,7 @@ public final class WorldMapRenderer {
float cellW = WORLD_SIZE / (SIZE - 1);
int dPx = Math.max(1, (int)(1.5f / cellW));
boolean[] cliffMask = new boolean[SIZE * SIZE];
float[] gradX = new float[SIZE * SIZE];
float[] gradZ = new float[SIZE * SIZE];
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;
@@ -1140,49 +1158,19 @@ 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))));
cliffMask[idx] = slope > 30.0;
gradX[idx] = (hR - hL) / (2 * dist);
gradZ[idx] = (hD - hU) / (2 * dist);
floatMask[idx] = slope > 30.0 ? 1f : 0f;
}
}
float[] floatMask = new float[SIZE * SIZE];
for (int i = 0; i < SIZE * SIZE; i++) { floatMask[i] = cliffMask[i] ? 1f : 0f; }
float[] softMask = boxBlurFloat(floatMask, SIZE, 3);
int[] result = new int[SIZE * SIZE];
// Füllung: weiches Grau, max 33% alpha
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;
}
}
// Kantenerkennung: obere Kante (Kamm) dunkler, untere Kante (Fuß) heller
for (int py = 1; py < SIZE - 1; py++) {
for (int px = 1; px < SIZE - 1; px++) {
int idx = py * SIZE + px;
if (!cliffMask[idx]) { continue; }
float gx = gradX[idx], gz = gradZ[idx];
float glen = (float) Math.sqrt(gx * gx + gz * gz);
if (glen < 1e-6f) { continue; }
float upX = -gx / glen, upZ = -gz / glen; // bergauf
float dnX = gx / glen, dnZ = gz / glen; // bergab
// Obere Kante: bergauf-Nachbar ist kein Cliff
int uhx = px + Math.round(upX * 2), uhz = py + Math.round(upZ * 2);
if (uhx >= 0 && uhx < SIZE && uhz >= 0 && uhz < SIZE
&& !cliffMask[uhz * SIZE + uhx]) {
result[idx] = (0xCC << 24) | 0x504848;
continue;
}
// Untere Kante: bergab-Nachbar ist kein Cliff
int dhx = px + Math.round(dnX), dhz = py + Math.round(dnZ);
if (dhx >= 0 && dhx < SIZE && dhz >= 0 && dhz < SIZE
&& !cliffMask[dhz * SIZE + dhx]) {
result[idx] = (0x88 << 24) | 0x686060;
}
}
}
return result;
}
@@ -1219,17 +1207,8 @@ public final class WorldMapRenderer {
float[] waterH = buildWaterHeightMap(waters, SIZE);
int written = 0;
// Coarse-Typ-Map (4m/px) – identische Filterung wie buildTerrainColorMap
final int COARSE = 512;
float cellWC = WORLD_SIZE / (COARSE - 1);
byte[] coarseTypes = new byte[COARSE * COARSE];
for (int mz = 0; mz < COARSE; mz++) {
float wz = -WORLD_HALF + mz * cellWC;
for (int mx = 0; mx < COARSE; mx++) {
float wx = -WORLD_HALF + mx * cellWC;
coarseTypes[mz * COARSE + mx] = (byte) getTerrainTile(m, SS, wx, wz, slotColors);
}
}
// 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++) {
@@ -1249,9 +1228,7 @@ public final class WorldMapRenderer {
tg = (WATER_COLOR >> 8) & 0xFF;
tb = WATER_COLOR & 0xFF;
} else {
int ctX = Math.min(COARSE - 1, (int)((float) gx / (SIZE - 1) * (COARSE - 1)));
int ctZ = Math.min(COARSE - 1, (int)((float) gy / (SIZE - 1) * (COARSE - 1)));
int col = schematicColor(coarseTypes[ctZ * COARSE + ctX] & 0xFF);
int col = sampleCoarseBilinear(coarseColors, 256, gx, gy, SIZE);
tr = (col >> 16) & 0xFF;
tg = (col >> 8) & 0xFF;
tb = col & 0xFF;