From 5226e07622cee5b30d853f6cb752f83ad8bcdde9 Mon Sep 17 00:00:00 2001 From: Mario Date: Sun, 6 Sep 2026 21:54:16 +0200 Subject: [PATCH] =?UTF-8?q?H=C3=B6hlenkarte:=20Sandwich-basierter=20Chunk-?= =?UTF-8?q?Filter=20verhindert=20False=20Positives?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nur Chunks mit echtem Sandwich-Tunnel (solid → void ≥3m → solid) werden für die voxelSurface-Berechnung herangezogen. Isolierte Voxel-Strukturen (cx=16/17) werden so automatisch ausgeschlossen. Co-Authored-By: Claude Sonnet 4.6 --- .../blight/common/map/WorldMapRenderer.java | 142 ++++++++---------- 1 file changed, 62 insertions(+), 80 deletions(-) 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 3e4acb7..badc434 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 @@ -1261,27 +1261,64 @@ public final class WorldMapRenderer { } /** - * Sandwich-Erkennung als Seed: Boden + Hohlraum (≥ MIN_VOID_VOXELS) + Decke. - * Von diesen Seeds wächst die Maske per BFS durch alle Spalten mit irgendwelchem - * Voxel-Solid (Kliffwände, Tunnelboden). Abschließend 1px Dilation. + * Höhlenerkennung: voxelSurface (höchstes solides Voxel) muss mindestens CLIFF_THRESHOLD + * über dem Basis-Terrain liegen. Schließt Terrain-Übergänge (delta 1-4m) aus, + * erfasst echte Kliffwände/Tunnel (delta ≥ 5m). Nur cy=0-Chunks. */ private static boolean[] buildCaveMasks(MapData mapData, int size, boolean tunnelOnly) { - final int MIN_VOID_VOXELS = 3; // Mindest-Hohlraumhöhe für echten Sandwich + final float CLIFF_THRESHOLD = 5f; // voxelSurface muss ≥5m über terrain liegen + final int MIN_VOID_VOXELS = 3; // Sandwich: Mindest-Hohlraumhöhe - boolean[] seeds = new boolean[size * size]; // Sandwich-Treffer - boolean[] solidPresent = new boolean[size * size]; // irgendwo d>0 in der Spalte + boolean[] mask = new boolean[size * size]; + float[] vs = new float[size * size]; + java.util.Arrays.fill(vs, Float.NaN); List allChunks; try { allChunks = VoxelChunkIO.loadAll(); } catch (Exception e) { log.warn("[CaveMap] VoxelChunks nicht ladbar: {}", e.getMessage()); - return seeds; + return mask; } - if (allChunks.isEmpty()) return seeds; + if (allChunks.isEmpty()) return mask; + float[] terrainH = buildTerrainSamples(mapData, size, null); + + // Schritt 1: Finde Chunks, die einen echten Sandwich-Tunnel enthalten + java.util.Set tunnelChunkKeys = new java.util.HashSet<>(); for (VoxelChunk chunk : allChunks) { if (chunk.cy != 0) continue; + outer: + for (int lz = 0; lz < VoxelChunk.SIZE; lz++) { + for (int lx = 0; lx < VoxelChunk.SIZE; lx++) { + int phase = 0, voidCount = 0; + for (int ly = 0; ly < VoxelChunk.SIZE; ly++) { + byte d = chunk.getDensity(lx, ly, lz); + if (phase == 0) { + if (d > 0) phase = 1; + } else if (phase == 1) { + if (d < 0) { phase = 2; voidCount = 1; } + } else { + if (d < 0) { voidCount++; } + else if (d > 0) { + if (voidCount >= MIN_VOID_VOXELS) { + tunnelChunkKeys.add((long) chunk.cx << 32 | (chunk.cz & 0xFFFFFFFFL)); + break outer; + } + phase = 1; voidCount = 0; + } + } + } + } + } + } + if (tunnelChunkKeys.isEmpty()) return mask; + + // Schritt 2: voxelSurface nur aus Tunnel-Chunks berechnen + for (VoxelChunk chunk : allChunks) { + if (chunk.cy != 0) continue; + long key = (long) chunk.cx << 32 | (chunk.cz & 0xFFFFFFFFL); + if (!tunnelChunkKeys.contains(key)) continue; for (int lz = 0; lz < VoxelChunk.SIZE; lz++) { float worldZ = VoxelChunk.toWorldZ(chunk.cz, lz); int pz = Math.round((worldZ + WORLD_HALF) / WORLD_SIZE * (size - 1)); @@ -1291,104 +1328,49 @@ public final class WorldMapRenderer { int px = Math.round((worldX + WORLD_HALF) / WORLD_SIZE * (size - 1)); if (px < 0 || px >= size) continue; int idx = pz * size + px; - - // Sandwich-Scan + solid-present in einem Durchlauf - int phase = 0, voidCount = 0; - for (int ly = 0; ly < VoxelChunk.SIZE; ly++) { - byte d = chunk.getDensity(lx, ly, lz); - if (d > 0) solidPresent[idx] = true; - if (seeds[idx]) continue; // bereits gefunden, trotzdem solidPresent weiter scannen - if (phase == 0) { - if (d > 0) phase = 1; - } else if (phase == 1) { - if (d < 0) { phase = 2; voidCount = 1; } - } else { - if (d < 0) { - voidCount++; - } else if (d > 0) { - if (voidCount >= MIN_VOID_VOXELS) { seeds[idx] = true; } - phase = 1; voidCount = 0; - } + for (int ly = VoxelChunk.SIZE - 1; ly >= 0; ly--) { + if (chunk.getDensity(lx, ly, lz) > 0) { + float wY = VoxelChunk.toWorldY(chunk.cy, ly); + if (Float.isNaN(vs[idx]) || wY > vs[idx]) vs[idx] = wY; + break; } } } } } - // Bounding-Box der Seeds + Margin → BFS bleibt lokal, springt nicht zu fremden Voxelgebieten - int bbMinX = size, bbMaxX = -1, bbMinZ = size, bbMaxZ = -1; - for (int i = 0; i < size * size; i++) { - if (!seeds[i]) continue; - int spx = i % size, spz = i / size; - if (spx < bbMinX) bbMinX = spx; if (spx > bbMaxX) bbMaxX = spx; - if (spz < bbMinZ) bbMinZ = spz; if (spz > bbMaxZ) bbMaxZ = spz; - } - if (bbMaxX < 0) return seeds; // keine Seeds → leere Maske - final int MARGIN = 20; // 20px * 8m/px = 160m Puffer rund um den Tunnel - bbMinX = Math.max(0, bbMinX - MARGIN); bbMaxX = Math.min(size - 1, bbMaxX + MARGIN); - bbMinZ = Math.max(0, bbMinZ - MARGIN); bbMaxZ = Math.min(size - 1, bbMaxZ + MARGIN); - - // BFS von Sandwich-Seeds durch Spalten mit irgendwelchem Voxel-Solid (begrenzt auf Bounding-Box) - boolean[] mask = seeds.clone(); - boolean[] visited = seeds.clone(); - java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); - for (int i = 0; i < size * size; i++) { - if (seeds[i]) queue.add(i); - } + // voxelSurface ≥ terrain + CLIFF_THRESHOLD → Höhle/Kliff final int[] dx4 = {1, -1, 0, 0}; final int[] dz4 = {0, 0, 1, -1}; - while (!queue.isEmpty()) { - int cur = queue.poll(); - int cpx = cur % size, cpz = cur / size; - for (int d = 0; d < 4; d++) { - int nx = cpx + dx4[d], nz = cpz + dz4[d]; - if (nx < bbMinX || nx > bbMaxX || nz < bbMinZ || nz > bbMaxZ) continue; - int nIdx = nz * size + nx; - if (visited[nIdx] || !solidPresent[nIdx]) continue; - visited[nIdx] = true; - mask[nIdx] = true; - queue.add(nIdx); + for (int i = 0; i < size * size; i++) { + if (!Float.isNaN(vs[i]) && vs[i] >= terrainH[i] + CLIFF_THRESHOLD) { + mask[i] = true; } } - // 1px Dilation: Randbereich direkt an den Kliffwänden mit einschließen - boolean[] dilated = new boolean[size * size]; - for (int pz = 0; pz < size; pz++) { - for (int px = 0; px < size; px++) { - if (!mask[pz * size + px]) continue; - for (int d = 0; d < 4; d++) { - int nx = px + dx4[d], nz = pz + dz4[d]; - if (nx < 0 || nx >= size || nz < 0 || nz >= size) continue; - dilated[nz * size + nx] = true; - } - dilated[pz * size + px] = true; - } - } - - // Kleine isolierte Reste entfernen (≥40px behalten) + // Komponenten < MIN_FOOTPRINT_PX entfernen final int MIN_FOOTPRINT_PX = 40; - boolean[] vis2 = new boolean[size * size]; + boolean[] vis = new boolean[size * size]; boolean[] cleaned = new boolean[size * size]; for (int start = 0; start < size * size; start++) { - if (!dilated[start] || vis2[start]) continue; - java.util.ArrayDeque q2 = new java.util.ArrayDeque<>(); - q2.add(start); vis2[start] = true; + if (!mask[start] || vis[start]) continue; + java.util.ArrayDeque q = new java.util.ArrayDeque<>(); + q.add(start); vis[start] = true; java.util.List comp = new java.util.ArrayList<>(); - while (!q2.isEmpty()) { - int cur = q2.poll(); comp.add(cur); + while (!q.isEmpty()) { + int cur = q.poll(); comp.add(cur); int cpx = cur % size, cpz = cur / size; for (int d = 0; d < 4; d++) { int nx = cpx + dx4[d], nz = cpz + dz4[d]; if (nx < 0 || nx >= size || nz < 0 || nz >= size) continue; int nIdx = nz * size + nx; - if (dilated[nIdx] && !vis2[nIdx]) { vis2[nIdx] = true; q2.add(nIdx); } + if (mask[nIdx] && !vis[nIdx]) { vis[nIdx] = true; q.add(nIdx); } } } if (comp.size() >= MIN_FOOTPRINT_PX) { for (int idx : comp) cleaned[idx] = true; } } - return cleaned; }