Höhlenkarte: Sandwich-basierter Chunk-Filter verhindert False Positives

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 <noreply@anthropic.com>
This commit is contained in:
2026-09-06 21:54:16 +02:00
parent faa0a8b254
commit 5226e07622

View File

@@ -1261,27 +1261,64 @@ public final class WorldMapRenderer {
} }
/** /**
* Sandwich-Erkennung als Seed: Boden + Hohlraum (≥ MIN_VOID_VOXELS) + Decke. * Höhlenerkennung: voxelSurface (höchstes solides Voxel) muss mindestens CLIFF_THRESHOLD
* Von diesen Seeds wächst die Maske per BFS durch alle Spalten mit irgendwelchem * über dem Basis-Terrain liegen. Schließt Terrain-Übergänge (delta 1-4m) aus,
* Voxel-Solid (Kliffwände, Tunnelboden). Abschließend 1px Dilation. * erfasst echte Kliffwände/Tunnel (delta ≥ 5m). Nur cy=0-Chunks.
*/ */
private static boolean[] buildCaveMasks(MapData mapData, int size, boolean tunnelOnly) { 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[] mask = new boolean[size * size];
boolean[] solidPresent = new boolean[size * size]; // irgendwo d>0 in der Spalte float[] vs = new float[size * size];
java.util.Arrays.fill(vs, Float.NaN);
List<VoxelChunk> allChunks; List<VoxelChunk> allChunks;
try { try {
allChunks = VoxelChunkIO.loadAll(); allChunks = VoxelChunkIO.loadAll();
} catch (Exception e) { } catch (Exception e) {
log.warn("[CaveMap] VoxelChunks nicht ladbar: {}", e.getMessage()); 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<Long> tunnelChunkKeys = new java.util.HashSet<>();
for (VoxelChunk chunk : allChunks) { for (VoxelChunk chunk : allChunks) {
if (chunk.cy != 0) continue; 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++) { for (int lz = 0; lz < VoxelChunk.SIZE; lz++) {
float worldZ = VoxelChunk.toWorldZ(chunk.cz, lz); float worldZ = VoxelChunk.toWorldZ(chunk.cz, lz);
int pz = Math.round((worldZ + WORLD_HALF) / WORLD_SIZE * (size - 1)); 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)); int px = Math.round((worldX + WORLD_HALF) / WORLD_SIZE * (size - 1));
if (px < 0 || px >= size) continue; if (px < 0 || px >= size) continue;
int idx = pz * size + px; int idx = pz * size + px;
for (int ly = VoxelChunk.SIZE - 1; ly >= 0; ly--) {
// Sandwich-Scan + solid-present in einem Durchlauf if (chunk.getDensity(lx, ly, lz) > 0) {
int phase = 0, voidCount = 0; float wY = VoxelChunk.toWorldY(chunk.cy, ly);
for (int ly = 0; ly < VoxelChunk.SIZE; ly++) { if (Float.isNaN(vs[idx]) || wY > vs[idx]) vs[idx] = wY;
byte d = chunk.getDensity(lx, ly, lz); break;
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;
}
} }
} }
} }
} }
} }
// Bounding-Box der Seeds + Margin → BFS bleibt lokal, springt nicht zu fremden Voxelgebieten // voxelSurface ≥ terrain + CLIFF_THRESHOLD → Höhle/Kliff
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<Integer> queue = new java.util.ArrayDeque<>();
for (int i = 0; i < size * size; i++) {
if (seeds[i]) queue.add(i);
}
final int[] dx4 = {1, -1, 0, 0}; final int[] dx4 = {1, -1, 0, 0};
final int[] dz4 = {0, 0, 1, -1}; final int[] dz4 = {0, 0, 1, -1};
while (!queue.isEmpty()) { for (int i = 0; i < size * size; i++) {
int cur = queue.poll(); if (!Float.isNaN(vs[i]) && vs[i] >= terrainH[i] + CLIFF_THRESHOLD) {
int cpx = cur % size, cpz = cur / size; mask[i] = true;
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);
} }
} }
// 1px Dilation: Randbereich direkt an den Kliffwänden mit einschließen // Komponenten < MIN_FOOTPRINT_PX entfernen
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)
final int MIN_FOOTPRINT_PX = 40; final int MIN_FOOTPRINT_PX = 40;
boolean[] vis2 = new boolean[size * size]; boolean[] vis = new boolean[size * size];
boolean[] cleaned = new boolean[size * size]; boolean[] cleaned = new boolean[size * size];
for (int start = 0; start < size * size; start++) { for (int start = 0; start < size * size; start++) {
if (!dilated[start] || vis2[start]) continue; if (!mask[start] || vis[start]) continue;
java.util.ArrayDeque<Integer> q2 = new java.util.ArrayDeque<>(); java.util.ArrayDeque<Integer> q = new java.util.ArrayDeque<>();
q2.add(start); vis2[start] = true; q.add(start); vis[start] = true;
java.util.List<Integer> comp = new java.util.ArrayList<>(); java.util.List<Integer> comp = new java.util.ArrayList<>();
while (!q2.isEmpty()) { while (!q.isEmpty()) {
int cur = q2.poll(); comp.add(cur); int cur = q.poll(); comp.add(cur);
int cpx = cur % size, cpz = cur / size; int cpx = cur % size, cpz = cur / size;
for (int d = 0; d < 4; d++) { for (int d = 0; d < 4; d++) {
int nx = cpx + dx4[d], nz = cpz + dz4[d]; int nx = cpx + dx4[d], nz = cpz + dz4[d];
if (nx < 0 || nx >= size || nz < 0 || nz >= size) continue; if (nx < 0 || nx >= size || nz < 0 || nz >= size) continue;
int nIdx = nz * size + nx; 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) { if (comp.size() >= MIN_FOOTPRINT_PX) {
for (int idx : comp) cleaned[idx] = true; for (int idx : comp) cleaned[idx] = true;
} }
} }
return cleaned; return cleaned;
} }