Weiter an der Karte gearbeitet - hier Höhlen und Tunnel

This commit is contained in:
2026-09-06 11:41:04 +02:00
parent ca4d34b806
commit faa0a8b254
18 changed files with 1192 additions and 63 deletions

View File

@@ -708,6 +708,56 @@ public final class WorldMapRenderer {
return s;
}
/**
* Liest alle VoxelChunks von Disk und baut daraus das voxelSurface-Array
* (oberste solide Voxel-Y pro VOXEL_SURFACE_RES-Zelle; NaN = kein Voxel).
* Aktualisiert dabei mapData.upperTop auf dem UPPER_VERTS-Raster als Nebeneffekt.
*/
public static float[] bakeVoxelSurface(MapData mapData) {
int VS = VOXEL_SURFACE_RES;
float[] voxelSurface = new float[VS * VS];
java.util.Arrays.fill(voxelSurface, Float.NaN);
List<VoxelChunk> chunks;
try {
chunks = VoxelChunkIO.loadAll();
} catch (Exception e) {
log.warn("[WorldMap] VoxelChunks nicht ladbar: {}", e.getMessage());
return voxelSurface;
}
if (chunks.isEmpty()) { return voxelSurface; }
int UV = MapData.UPPER_VERTS;
for (VoxelChunk chunk : chunks) {
if (chunk.isEmpty()) { continue; }
for (int lz = 0; lz < VoxelChunk.SIZE; lz++) {
float worldZ = VoxelChunk.toWorldZ(chunk.cz, lz);
int vsPz = Math.round((worldZ + WORLD_HALF) / WORLD_SIZE * (VS - 1));
if (vsPz < 0 || vsPz >= VS) { continue; }
int uz = Math.round((worldZ + WORLD_HALF) / WORLD_SIZE * (UV - 1));
for (int lx = 0; lx < VoxelChunk.SIZE; lx++) {
float worldX = VoxelChunk.toWorldX(chunk.cx, lx);
int vsPx = Math.round((worldX + WORLD_HALF) / WORLD_SIZE * (VS - 1));
if (vsPx < 0 || vsPx >= VS) { continue; }
int ux = Math.round((worldX + WORLD_HALF) / WORLD_SIZE * (UV - 1));
for (int ly = VoxelChunk.SIZE - 1; ly >= 0; ly--) {
if (chunk.getDensity(lx, ly, lz) > 0) {
float topY = VoxelChunk.toWorldY(chunk.cy, ly);
int vsIdx = vsPz * VS + vsPx;
if (Float.isNaN(voxelSurface[vsIdx]) || topY > voxelSurface[vsIdx]) {
voxelSurface[vsIdx] = topY;
}
if (mapData != null && uz >= 0 && uz < UV && ux >= 0 && ux < UV) {
int uvIdx = uz * UV + ux;
if (topY > mapData.upperTop[uvIdx]) { mapData.upperTop[uvIdx] = topY; }
}
break;
}
}
}
}
}
return voxelSurface;
}
/**
* Berechnet eine Hillshading-Karte (float pro Pixel, 0.55–1.35) aus reinen Terrain-Höhen.
* Voxels werden NICHT eingerechnet, um Artefakte an Voxel-Kanten zu vermeiden.
@@ -1181,6 +1231,184 @@ public final class WorldMapRenderer {
*
* @return Anzahl neu geschriebener Chunks (0 = alle aus Cache geladen)
*/
/**
* Berechnet die visuelle Footprint-Maske für die Höhlenkarte.
*
* Zeigt alle Pixel wo Voxel-Material deutlich über dem Terrain liegt
* (Kliff-Wände + Tunnel-Decke) ODER wo ein offener Tunnel-Eingang ist
* (Boden solid, kein Dach, Void startet nahe am Boden).
*
* Kriterium A: voxelSurface > Terrain + 1.5m → Kliff-Wände + Decken
* Kriterium B∧C: hasSolid ∧ Void∧ voidStartWY ≤ 8 → offene Eingänge
* Ergebnis: A ∨ (B∧C)
*/
public static boolean[] buildCaveFootprintMask(MapData mapData, int size) {
return buildCaveMasks(mapData, size, false);
}
/**
* Berechnet die Zugänglichkeits-Maske für Tunnel-Eingangs-Erkennung.
*
* Markiert nur zugängliche Bereiche (Hohlräume), NICHT solide Kliff-Wände.
* So findet Two-Farthest-Points die echten Tunnel-Eingänge, nicht den Kliff-Rand.
*
* Kriterium B∧(C∨A): hasSolid ∧ Void ∧ (voidStartWY≤8 ∨ voxelSurface>Terrain+1.5)
* → Eingangs-Korridore (niedrige voidStart) + Tunnel-Inneres (hohe voxelSurface),
* aber NICHT solide Wände (kein Void) und NICHT Terrain-Peaks (voxelSurface≈Terrain).
*/
public static boolean[] buildCaveTunnelMask(MapData mapData, int size) {
return buildCaveMasks(mapData, size, true);
}
/**
* 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.
*/
private static boolean[] buildCaveMasks(MapData mapData, int size, boolean tunnelOnly) {
final int MIN_VOID_VOXELS = 3; // Mindest-Hohlraumhöhe für echten Sandwich
boolean[] seeds = new boolean[size * size]; // Sandwich-Treffer
boolean[] solidPresent = new boolean[size * size]; // irgendwo d>0 in der Spalte
List<VoxelChunk> allChunks;
try {
allChunks = VoxelChunkIO.loadAll();
} catch (Exception e) {
log.warn("[CaveMap] VoxelChunks nicht ladbar: {}", e.getMessage());
return seeds;
}
if (allChunks.isEmpty()) return seeds;
for (VoxelChunk chunk : allChunks) {
if (chunk.cy != 0) 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));
if (pz < 0 || pz >= size) continue;
for (int lx = 0; lx < VoxelChunk.SIZE; lx++) {
float worldX = VoxelChunk.toWorldX(chunk.cx, lx);
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;
}
}
}
}
}
}
// 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);
}
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);
}
}
// 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)
final int MIN_FOOTPRINT_PX = 40;
boolean[] vis2 = 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;
java.util.List<Integer> comp = new java.util.ArrayList<>();
while (!q2.isEmpty()) {
int cur = q2.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 (comp.size() >= MIN_FOOTPRINT_PX) {
for (int idx : comp) cleaned[idx] = true;
}
}
return cleaned;
}
/**
* Rendert eine schematische Höhlen/Tunnel-Karte aus den VoxelChunks.
* Tunnelbereiche werden als Erdfarbe dargestellt, alles andere fast schwarz.
*/
public static BufferedImage buildCaveMap(MapData mapData, int targetSize) {
boolean[] mask = buildCaveFootprintMask(mapData, targetSize);
int bgPacked = 0x1c1c1c; // fast schwarz (Gestein/Außen)
int floorPacked = 0xb68a50; // Dirt-Farbe für Tunnelboden
BufferedImage img = new BufferedImage(targetSize, targetSize, BufferedImage.TYPE_INT_RGB);
int[] pixels = ((java.awt.image.DataBufferInt) img.getRaster().getDataBuffer()).getData();
java.util.Arrays.fill(pixels, bgPacked);
for (int i = 0; i < mask.length; i++) {
if (mask[i]) pixels[i] = floorPacked;
}
return img;
}
public static int buildAndSaveHiresChunks(MapData m, int[] slotColors, int[][] tilePixels,
float[] voxelSurface, List<PlacedWater> waters,
List<PlacedWaterfall> waterfalls,