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:
@@ -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;
|
||||
|
||||
@@ -96,7 +96,8 @@ public class WorldMapView extends VBox {
|
||||
private WritableImage mapFxImage;
|
||||
private BufferedImage mapBuffered;
|
||||
|
||||
private WritableImage bakedCliffOverlay = null;
|
||||
private WritableImage bakedCliffOverlay = null;
|
||||
private List<float[][]> cliffPaths = new ArrayList<>();
|
||||
private static final int ZOOM_LEVELS = 5;
|
||||
private static final double ZOOM_MAX_SCALE = 4.0; // 1 Canvas-Pixel = 0,25 WU = 0,25m
|
||||
private static final int ZOOM_HIRES_CUTOFF = 3; // ab diesem Index Chunk-Rendering
|
||||
@@ -118,7 +119,8 @@ public class WorldMapView extends VBox {
|
||||
java.util.Collections.synchronizedSet(new java.util.HashSet<>());
|
||||
private volatile boolean chunkRedrawPending = false;
|
||||
|
||||
private WritableImage bakedForestOverlay = null;
|
||||
private WritableImage bakedForestOverlay = null;
|
||||
private List<float[][]> forestPaths = new ArrayList<>();
|
||||
|
||||
private List<PlacedArea> cachedAreas = new ArrayList<>();
|
||||
private List<Location> cachedLocs = new ArrayList<>();
|
||||
@@ -355,9 +357,11 @@ public class WorldMapView extends VBox {
|
||||
log.debug("[WorldMap] Hintergrund-PNG gerendert ({}×{} px, {} ms)",
|
||||
RENDER_SIZE, RENDER_SIZE, System.currentTimeMillis() - t1);
|
||||
|
||||
float[] fDensity = computeForestDensity(model);
|
||||
int[] bakedFstPx = computeForestPixels(model, fDensity, 2.0f);
|
||||
int[] cliffPx = WorldMapRenderer.buildCliffOverlay(model.mapData(), model.voxelSurface());
|
||||
float[] fDensity = computeForestDensity(model);
|
||||
int[] bakedFstPx = computeForestPixels(model, fDensity, 2.0f);
|
||||
int[] cliffPx = WorldMapRenderer.buildCliffOverlay(model.mapData(), model.voxelSurface());
|
||||
List<float[][]> newCliffPaths = buildCliffPaths(model.mapData(), model.voxelSurface());
|
||||
List<float[][]> newForestPaths = buildForestOutlinePaths(fDensity, 512, 2.0f);
|
||||
|
||||
// Hires-Chunks im Hintergrund generieren (nach dem UI-Update, nicht-blockierend)
|
||||
final WorldMapRenderModel chunkModel = model;
|
||||
@@ -376,6 +380,8 @@ public class WorldMapView extends VBox {
|
||||
mapFxImage = SwingFXUtils.toFXImage(bi, null);
|
||||
bakedForestOverlay = toWritableImage(bakedFstPx, RENDER_SIZE);
|
||||
bakedCliffOverlay = toWritableImage(cliffPx, WorldMapRenderer.COLOR_MAP_SIZE);
|
||||
cliffPaths = newCliffPaths;
|
||||
forestPaths = newForestPaths;
|
||||
fitToView();
|
||||
redraw();
|
||||
progress.setVisible(false);
|
||||
@@ -450,12 +456,12 @@ public class WorldMapView extends VBox {
|
||||
} else {
|
||||
gc.drawImage(mapFxImage, panX, panY, mapFxImage.getWidth() * scale, mapFxImage.getHeight() * scale);
|
||||
}
|
||||
// Layer 2: Küstenlinie (Vektor)
|
||||
if (cbWater.isSelected()) drawWaterOverlay(gc);
|
||||
// Layer 3: Wald (Füllung + Rand)
|
||||
if (cbForest.isSelected()) drawForestOverlay(gc);
|
||||
// Layer 4: Klippen (Grau-Füllung + Kantenlinien)
|
||||
// Layer 2: Klippen (Grau-Füllung + Vektor-Kontur)
|
||||
if (cbCliffs.isSelected()) drawCliffOverlay(gc);
|
||||
// Layer 3: Küstenlinie (Vektor)
|
||||
if (cbWater.isSelected()) drawWaterOverlay(gc);
|
||||
// Layer 4: Wald (Füllung + Vektor-Kontur)
|
||||
if (cbForest.isSelected()) drawForestOverlay(gc);
|
||||
// Layer 5: Areas (Vektoren)
|
||||
if (cbAreas.isSelected()) drawAreasOverlay(gc);
|
||||
if (!isFullscreen) drawCameraIndicator(gc);
|
||||
@@ -552,15 +558,37 @@ public class WorldMapView extends VBox {
|
||||
}
|
||||
|
||||
private void drawForestOverlay(GraphicsContext gc) {
|
||||
if (bakedForestOverlay == null) return;
|
||||
int cW = (int) canvas.getWidth(), cH = (int) canvas.getHeight();
|
||||
gc.drawImage(bakedForestOverlay, -panX / scale, -panY / scale, cW / scale, cH / scale, 0, 0, cW, cH);
|
||||
double cW = canvas.getWidth(), cH = canvas.getHeight();
|
||||
if (bakedForestOverlay != null) {
|
||||
gc.drawImage(bakedForestOverlay, -panX / scale, -panY / scale, cW / scale, cH / scale, 0, 0, cW, cH);
|
||||
}
|
||||
if (!forestPaths.isEmpty()) {
|
||||
gc.save();
|
||||
gc.setStroke(Color.rgb(45, 99, 22, 0.85));
|
||||
gc.setLineWidth(Math.max(1.0, 1.5 / scale));
|
||||
gc.setLineDashes();
|
||||
for (float[][] path : forestPaths) {
|
||||
drawCoastPath(gc, path, cW, cH, 8);
|
||||
}
|
||||
gc.restore();
|
||||
}
|
||||
}
|
||||
|
||||
private void drawCliffOverlay(GraphicsContext gc) {
|
||||
if (bakedCliffOverlay == null) return;
|
||||
int cW = (int) canvas.getWidth(), cH = (int) canvas.getHeight();
|
||||
gc.drawImage(bakedCliffOverlay, -panX / scale, -panY / scale, cW / scale, cH / scale, 0, 0, cW, cH);
|
||||
double cW = canvas.getWidth(), cH = canvas.getHeight();
|
||||
if (bakedCliffOverlay != null) {
|
||||
gc.drawImage(bakedCliffOverlay, -panX / scale, -panY / scale, cW / scale, cH / scale, 0, 0, cW, cH);
|
||||
}
|
||||
if (!cliffPaths.isEmpty()) {
|
||||
gc.save();
|
||||
gc.setStroke(Color.rgb(80, 72, 72, 0.80));
|
||||
gc.setLineWidth(Math.max(1.0, 1.5 / scale));
|
||||
gc.setLineDashes();
|
||||
for (float[][] path : cliffPaths) {
|
||||
drawCoastPath(gc, path, cW, cH, 8);
|
||||
}
|
||||
gc.restore();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Wald-Baking (läuft im Background-Thread) ─────────────────────────────
|
||||
@@ -590,37 +618,20 @@ public class WorldMapView extends VBox {
|
||||
private static int[] computeForestPixels(WorldMapRenderModel model, float[] density, float threshold) {
|
||||
final int SIZE = RENDER_SIZE, FD = 512;
|
||||
final float WH = WorldMapRenderer.WORLD_HALF, WS = WorldMapRenderer.WORLD_SIZE;
|
||||
final int forestFill = 0x80_4a9a28; // 50% alpha grün
|
||||
final int forestOutline = 0xCC_2d6316; // 80% alpha dunkelgrün
|
||||
final int forestFill = 0x80_4a9a28; // 50% alpha grün
|
||||
final int[] px = new int[SIZE * SIZE];
|
||||
boolean[] isFill = new boolean[SIZE * SIZE];
|
||||
for (int py = 0; py < SIZE; py++) {
|
||||
float wz = (float) py / (SIZE - 1) * WS - WH;
|
||||
for (int bx = 0; bx < SIZE; bx++) {
|
||||
float wx = (float) bx / (SIZE - 1) * WS - WH;
|
||||
float fsfx=Math.max(0f,Math.min(FD-1.001f,(wx+WH)/WS*FD));
|
||||
float fsfz=Math.max(0f,Math.min(FD-1.001f,(WH-wz)/WS*FD));
|
||||
int fsx0=(int)fsfx, fsx1=Math.min(fsx0+1,FD-1);
|
||||
int fsz0=(int)fsfz, fsz1=Math.min(fsz0+1,FD-1);
|
||||
float fstx=fsfx-fsx0, fstz=fsfz-fsz0;
|
||||
float fd=(density[fsz0*FD+fsx0]*(1-fstx)+density[fsz0*FD+fsx1]*fstx)*(1-fstz)
|
||||
+(density[fsz1*FD+fsx0]*(1-fstx)+density[fsz1*FD+fsx1]*fstx)*fstz;
|
||||
if (fd > threshold) {
|
||||
px[py * SIZE + bx] = forestFill;
|
||||
isFill[py * SIZE + bx] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Randlinie: Füll-Pixel mit mindestens einem Nicht-Füll-Nachbarn
|
||||
for (int py = 1; py < SIZE - 1; py++) {
|
||||
for (int bx = 1; bx < SIZE - 1; bx++) {
|
||||
int idx = py * SIZE + bx;
|
||||
if (!isFill[idx]) { continue; }
|
||||
boolean edge = !isFill[idx - 1] || !isFill[idx + 1]
|
||||
|| !isFill[idx - SIZE] || !isFill[idx + SIZE]
|
||||
|| !isFill[idx - SIZE - 1] || !isFill[idx - SIZE + 1]
|
||||
|| !isFill[idx + SIZE - 1] || !isFill[idx + SIZE + 1];
|
||||
if (edge) { px[idx] = forestOutline; }
|
||||
float fsfx = Math.max(0f, Math.min(FD - 1.001f, (wx + WH) / WS * FD));
|
||||
float fsfz = Math.max(0f, Math.min(FD - 1.001f, (WH - wz) / WS * FD));
|
||||
int fsx0 = (int) fsfx, fsx1 = Math.min(fsx0 + 1, FD - 1);
|
||||
int fsz0 = (int) fsfz, fsz1 = Math.min(fsz0 + 1, FD - 1);
|
||||
float fstx = fsfx - fsx0, fstz = fsfz - fsz0;
|
||||
float fd = (density[fsz0 * FD + fsx0] * (1 - fstx) + density[fsz0 * FD + fsx1] * fstx) * (1 - fstz)
|
||||
+ (density[fsz1 * FD + fsx0] * (1 - fstx) + density[fsz1 * FD + fsx1] * fstx) * fstz;
|
||||
if (fd > threshold) { px[py * SIZE + bx] = forestFill; }
|
||||
}
|
||||
}
|
||||
return px;
|
||||
@@ -994,6 +1005,54 @@ public class WorldMapView extends VBox {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Extrahiert Klippen-Konturen aus Terrain-Höhen als geglättete Weltkoordinaten-Pfade. */
|
||||
private static List<float[][]> buildCliffPaths(MapData mapData, float[] voxelSurface) {
|
||||
final int SIZE = SEA_MASK_SIZE;
|
||||
float[] heights = WorldMapRenderer.buildTerrainSamples(mapData, SIZE, voxelSurface);
|
||||
float cellW = WorldMapRenderer.WORLD_SIZE / (SIZE - 1);
|
||||
int dPx = Math.max(1, (int)(1.5f / cellW));
|
||||
|
||||
float[] mask = 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))));
|
||||
// Kliff-Bereich = "inside" (< threshold 0.5): -1; Nicht-Kliff = 1
|
||||
mask[idx] = slope > 30.0 ? -1f : 1f;
|
||||
}
|
||||
}
|
||||
|
||||
List<float[]> segs = new ArrayList<>();
|
||||
marchingSquares(mask, SIZE, 0.5f, null, null, segs);
|
||||
return buildSeaCoastPaths(segs.toArray(new float[0][]));
|
||||
}
|
||||
|
||||
/** Extrahiert Wald-Konturen aus der Tree-Density-Map als geglättete Weltkoordinaten-Pfade. */
|
||||
private static List<float[][]> buildForestOutlinePaths(float[] density, int fdSize, float threshold) {
|
||||
final int SIZE = SEA_MASK_SIZE;
|
||||
float[] mask = new float[SIZE * SIZE];
|
||||
for (int py = 0; py < SIZE; py++) {
|
||||
for (int px = 0; px < SIZE; px++) {
|
||||
float fx = (float) px / (SIZE - 1) * (fdSize - 1);
|
||||
float fz = (float) py / (SIZE - 1) * (fdSize - 1);
|
||||
int sx = Math.min(fdSize - 1, (int) fx), sz = Math.min(fdSize - 1, (int) fz);
|
||||
float d = density[sz * fdSize + sx];
|
||||
// Wald-Bereich = "inside" (< threshold 0.5): -1; Nicht-Wald = 1
|
||||
mask[py * SIZE + px] = d >= threshold ? -1f : 1f;
|
||||
}
|
||||
}
|
||||
|
||||
List<float[]> segs = new ArrayList<>();
|
||||
marchingSquares(mask, SIZE, 0.5f, null, null, segs);
|
||||
return buildSeaCoastPaths(segs.toArray(new float[0][]));
|
||||
}
|
||||
|
||||
// Entfernt kollineare Zwischenpunkte (lange H/V-Läufe → ein Segment).
|
||||
private static float[][] simplifyCoastPath(float[][] path) {
|
||||
int n = path.length;
|
||||
|
||||
Reference in New Issue
Block a user