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:
@@ -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<VoxelChunk> 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<Long> 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<Integer> 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<Integer> q2 = new java.util.ArrayDeque<>();
|
||||
q2.add(start); vis2[start] = true;
|
||||
if (!mask[start] || vis[start]) continue;
|
||||
java.util.ArrayDeque<Integer> q = new java.util.ArrayDeque<>();
|
||||
q.add(start); vis[start] = true;
|
||||
java.util.List<Integer> 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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user