Voxel Umstellung vollendet

This commit is contained in:
2026-10-05 08:58:59 +02:00
parent 4ed61998dd
commit 7069c5f632
90 changed files with 67 additions and 84 deletions

View File

@@ -74,73 +74,10 @@ public final class VoxelChunkIO {
} }
} }
// ── Pre-Bake-Backups ──────────────────────────────────────────────────────
/** Backup-Pfad der .blvc-Datei (wird vor dem Bake angelegt). */
public static Path getPreBakePath(int cx, int cy, int cz) {
return getPath(cx, cy, cz).resolveSibling(
getPath(cx, cy, cz).getFileName() + ".prebake");
}
public static boolean preBakeExists(int cx, int cy, int cz) {
return Files.exists(getPreBakePath(cx, cy, cz));
}
/** Kopiert die aktuelle .blvc-Datei als Pre-Bake-Backup. */
public static void savePreBake(int cx, int cy, int cz) throws IOException {
Path src = getPath(cx, cy, cz);
if (!Files.exists(src)) return;
Files.copy(src, getPreBakePath(cx, cy, cz), StandardCopyOption.REPLACE_EXISTING);
}
public static void deletePreBake(int cx, int cy, int cz) throws IOException {
Files.deleteIfExists(getPreBakePath(cx, cy, cz));
}
/** Stellt die .blvc-Datei aus dem Backup wieder her. */
public static void restorePreBake(int cx, int cy, int cz) throws IOException {
Path backup = getPreBakePath(cx, cy, cz);
if (!Files.exists(backup)) return;
Files.copy(backup, getPath(cx, cy, cz), StandardCopyOption.REPLACE_EXISTING);
}
/** true wenn mindestens ein Pre-Bake-Backup im Chunks-Verzeichnis existiert. */
public static boolean hasAnyPreBake() {
Path dir = ChunkTerrainIO.chunksDir();
if (!Files.isDirectory(dir)) return false;
try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir, "voxel_*.blvc.prebake")) {
return ds.iterator().hasNext();
} catch (IOException e) { return false; }
}
/** Alle Chunk-Koordinaten für die ein Pre-Bake-Backup existiert. */
public static List<int[]> listPreBakeCoords() {
List<int[]> result = new java.util.ArrayList<>();
Path dir = ChunkTerrainIO.chunksDir();
if (!Files.isDirectory(dir)) return result;
try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir, "voxel_*.blvc.prebake")) {
for (Path p : ds) {
String name = p.getFileName().toString()
.replace("voxel_", "").replace(".blvc.prebake", "");
String[] parts = name.split("_");
if (parts.length != 3) continue;
try {
int cx = Integer.parseInt(parts[0]);
int cy = parts[1].startsWith("m")
? -Integer.parseInt(parts[1].substring(1))
: Integer.parseInt(parts[1]);
int cz = Integer.parseInt(parts[2]);
result.add(new int[]{cx, cy, cz});
} catch (NumberFormatException ignored) {}
}
} catch (IOException ignored) {}
return result;
}
/** /**
* Liest alle vorhandenen VoxelChunks aus dem Chunks-Verzeichnis. * Liest alle vorhandenen VoxelChunks aus dem Chunks-Verzeichnis.
* Für Koordinaten, bei denen die .blvc nach dem Backen gelöscht wurde, * Vorhandene .blvc.prebake-Dateien (Altbestand) werden einmalig als .blvc migriert
* wird automatisch die .blvc.prebake als Fallback herangezogen. * und danach gelöscht.
* Gibt leere Liste zurück wenn kein Chunks-Verzeichnis existiert. * Gibt leere Liste zurück wenn kein Chunks-Verzeichnis existiert.
*/ */
public static List<VoxelChunk> loadAll() { public static List<VoxelChunk> loadAll() {
@@ -148,7 +85,6 @@ public final class VoxelChunkIO {
Path dir = ChunkTerrainIO.chunksDir(); Path dir = ChunkTerrainIO.chunksDir();
if (!Files.isDirectory(dir)) return result; if (!Files.isDirectory(dir)) return result;
// Aktive .blvc-Dateien einlesen
Set<String> loaded = new java.util.HashSet<>(); Set<String> loaded = new java.util.HashSet<>();
try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir, "voxel_*.blvc")) { try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir, "voxel_*.blvc")) {
for (Path p : ds) { for (Path p : ds) {
@@ -168,12 +104,17 @@ public final class VoxelChunkIO {
} }
} catch (IOException ignored) {} } catch (IOException ignored) {}
// Prebake-Fallback: Koordinaten die gebacken wurden haben keine .blvc mehr // Einmalige Migration: .blvc.prebake → .blvc für Altbestand ohne aktive .blvc.
// Erst kopieren, dann löschen – umgekehrte Reihenfolge würde Daten vernichten.
List<Path> prebakeToDelete = new java.util.ArrayList<>();
try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir, "voxel_*.blvc.prebake")) { try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir, "voxel_*.blvc.prebake")) {
for (Path p : ds) { for (Path p : ds) {
String name = p.getFileName().toString() String name = p.getFileName().toString()
.replace("voxel_", "").replace(".blvc.prebake", ""); .replace("voxel_", "").replace(".blvc.prebake", "");
if (loaded.contains(name)) continue; // aktive .blvc hat Vorrang if (loaded.contains(name)) {
prebakeToDelete.add(p); // .blvc existiert bereits → nur löschen
continue;
}
String[] parts = name.split("_"); String[] parts = name.split("_");
if (parts.length != 3) continue; if (parts.length != 3) continue;
try { try {
@@ -182,10 +123,15 @@ public final class VoxelChunkIO {
? -Integer.parseInt(parts[1].substring(1)) ? -Integer.parseInt(parts[1].substring(1))
: Integer.parseInt(parts[1]); : Integer.parseInt(parts[1]);
int cz = Integer.parseInt(parts[2]); int cz = Integer.parseInt(parts[2]);
result.add(VoxelChunk.deserialize(Files.readAllBytes(p), cx, cy, cz)); Path blvc = getPath(cx, cy, cz);
Files.copy(p, blvc, StandardCopyOption.REPLACE_EXISTING); // erst kopieren
result.add(VoxelChunk.deserialize(Files.readAllBytes(blvc), cx, cy, cz));
loaded.add(name);
prebakeToDelete.add(p); // dann löschen
} catch (Exception ignored) {} } catch (Exception ignored) {}
} }
} catch (IOException ignored) {} } catch (IOException ignored) {}
for (Path p : prebakeToDelete) { try { Files.deleteIfExists(p); } catch (IOException ignored) {} }
return result; return result;
} }

View File

@@ -399,7 +399,7 @@ public class VoxelEditorState extends BaseAppState {
// Verwaiste gebackene .j3o ohne zugehörige .blvc löschen // Verwaiste gebackene .j3o ohne zugehörige .blvc löschen
for (int[] cxyz : SculptedMeshIO.findAllBakedChunks()) { for (int[] cxyz : SculptedMeshIO.findAllBakedChunks()) {
int cx = cxyz[0], cy = cxyz[1], cz = cxyz[2]; int cx = cxyz[0], cy = cxyz[1], cz = cxyz[2];
if (!VoxelChunkIO.exists(cx, cy, cz) && !VoxelChunkIO.preBakeExists(cx, cy, cz)) { if (!VoxelChunkIO.exists(cx, cy, cz)) {
for (int lod = 0; lod <= 3; lod++) { for (int lod = 0; lod <= 3; lod++) {
try { Files.deleteIfExists(VoxelChunkIO.getBakedPath(cx, cy, cz, lod)); } catch (Exception ignored) {} try { Files.deleteIfExists(VoxelChunkIO.getBakedPath(cx, cy, cz, lod)); } catch (Exception ignored) {}
} }
@@ -422,7 +422,9 @@ public class VoxelEditorState extends BaseAppState {
if (input.markAllChunksDirtyRequested) { if (input.markAllChunksDirtyRequested) {
input.markAllChunksDirtyRequested = false; input.markAllChunksDirtyRequested = false;
for (long key : chunks.keySet()) sessionDirtyChunks.add(key); for (long key : chunks.keySet()) sessionDirtyChunks.add(key);
// Chunks die noch nicht geladen sind von Disk nachladen, damit der Bake sie kennt // Sofort aktualisieren damit der Bake-on-Exit-Guard den neuen Wert sieht
input.sessionDirtyVoxelCount = sessionDirtyChunks.size();
// Chunks die noch nicht geladen sind von Disk nachladen, damit der Bake-on-Exit sie kennt
executor.submit(() -> { executor.submit(() -> {
List<VoxelChunk> allDisk = VoxelChunkIO.loadAll(); List<VoxelChunk> allDisk = VoxelChunkIO.loadAll();
app.enqueue(() -> { app.enqueue(() -> {
@@ -431,7 +433,22 @@ public class VoxelEditorState extends BaseAppState {
if (!chunks.containsKey(key)) chunks.put(key, c); if (!chunks.containsKey(key)) chunks.put(key, c);
sessionDirtyChunks.add(key); sessionDirtyChunks.add(key);
} }
// Leere Chunks: .blvc löschen (kein Mesh zu backen, kein Speicherbedarf)
List<Long> emptyKeys = new ArrayList<>();
for (long key : sessionDirtyChunks) {
VoxelChunk c = chunks.get(key);
if (c != null && c.isEmpty()) emptyKeys.add(key);
}
for (long key : emptyKeys) {
VoxelChunk c = chunks.remove(key);
sessionDirtyChunks.remove(key);
VoxelChunkNode node = nodes.remove(key);
if (node != null) node.removeFromParent();
try { VoxelChunkIO.delete(c.cx, c.cy, c.cz); } catch (Exception ignored) {}
}
input.sessionDirtyVoxelCount = sessionDirtyChunks.size(); input.sessionDirtyVoxelCount = sessionDirtyChunks.size();
log.info("Alle-dirty: {} Chunks geladen, {} leer gelöscht, {} dirty für Bake-on-Exit",
allDisk.size(), emptyKeys.size(), sessionDirtyChunks.size());
}); });
}); });
} }
@@ -1970,20 +1987,27 @@ public class VoxelEditorState extends BaseAppState {
} }
} }
private void bakeAll(List<VoxelChunk> toProcess) { /** Gibt die Anzahl erfolgreich gebackener Chunks zurück (0 wenn nichts zu tun). */
private int bakeAll(List<VoxelChunk> toProcess) {
saveAll(); saveAll();
log.info("bakeAll: {} Chunks übergeben", toProcess.size());
List<VoxelChunk> nonEmpty = new java.util.ArrayList<>(); List<VoxelChunk> nonEmpty = new java.util.ArrayList<>();
for (VoxelChunk c : toProcess) { if (!c.isEmpty()) nonEmpty.add(c); } for (VoxelChunk c : toProcess) { if (!c.isEmpty()) nonEmpty.add(c); }
log.info("bakeAll: nach isEmpty-Filter: {} verbleiben", nonEmpty.size());
// Chunks ohne .blvc-Datei überspringen (noch nie gespeichert). // Chunks ohne .blvc-Datei überspringen (noch nie gespeichert).
nonEmpty.removeIf(c -> !VoxelChunkIO.exists(c.cx, c.cy, c.cz)); nonEmpty.removeIf(c -> !VoxelChunkIO.exists(c.cx, c.cy, c.cz));
log.info("bakeAll: nach exists-Filter: {} verbleiben", nonEmpty.size());
// Chunks ohne nennenswerte Geometrie überspringen (nur Brush-Randberührungen, // Chunks ohne nennenswerte Geometrie überspringen (nur Brush-Randberührungen,
// solidYSpan < 2 → gleiche Schwelle wie der Game-Loader). // solidYSpan < 2 → gleiche Schwelle wie der Game-Loader).
nonEmpty.removeIf(c -> c.solidYSpan() < 2); nonEmpty.removeIf(c -> c.solidYSpan() < 2);
log.info("bakeAll: nach solidYSpan-Filter: {} verbleiben (→ diese werden gebacken)", nonEmpty.size());
if (nonEmpty.isEmpty()) { if (nonEmpty.isEmpty()) {
input.bakeStatusMsg = "Nichts zu backen (keine nicht-leeren Chunks)."; input.bakeStatusMsg = "Nichts zu backen (keine nicht-leeren Chunks).";
return; log.warn("bakeAll: kein Chunk überlebt alle Filter – übergebene Chunks: {}",
toProcess.stream().map(c -> c.cx+","+c.cy+","+c.cz).toList());
return 0;
} }
// bakeTotal sofort setzen, damit die Fortschrittsanzeige schon während des Blurs läuft // bakeTotal sofort setzen, damit die Fortschrittsanzeige schon während des Blurs läuft
@@ -2236,8 +2260,12 @@ public class VoxelEditorState extends BaseAppState {
// gebackene Terrain systematisch zu hoch machen. // gebackene Terrain systematisch zu hoch machen.
smoothH = h0; smoothH = h0;
} else { } else {
float dDown = dDownRaw - 1; // dDown = dDownRaw (kein -1): auch unmittelbar angrenzende Stufen
smoothH = (dDown <= 0f) ? h0 : (h0 * dUp + hUp * dDown) / (dUp + dDown); // (dDownRaw=1) bekommen eine interpolierte Höhe statt h0 zurück.
// Die frühere "Trailing-Edge-Korrektur" (dDownRaw-1) ließ genau die
// Kanten-Spalten unberührt, die am meisten abfallen müssten.
float dDown = (float) dDownRaw;
smoothH = (h0 * dUp + hUp * dDown) / (dUp + dDown);
} }
smoothMap.put(hk0, smoothH); smoothMap.put(hk0, smoothH);
} }
@@ -2291,7 +2319,9 @@ public class VoxelEditorState extends BaseAppState {
final float RAMP_RATIO = 4.0f; final float RAMP_RATIO = 4.0f;
final int MAX_RAMP_R = 20; final int MAX_RAMP_R = 20;
final int MAX_RAMP_R_TERRAIN = 4; final int MAX_RAMP_R_TERRAIN = 4;
final float MAX_TERRAIN_DIFF = 1.0f; // Erhöht von 1.0 auf 3.0: unterste Voxel-Schicht liegt oft 2–3 m über dem
// Basisterrain – mit 1.0 wurde sie komplett übersprungen → senkrechte Klippe.
final float MAX_TERRAIN_DIFF = 3.0f;
final float MAX_VOXEL_DIFF = 1.05f; final float MAX_VOXEL_DIFF = 1.05f;
Map<Long, Float> terrainCache2b = new HashMap<>(); Map<Long, Float> terrainCache2b = new HashMap<>();
@@ -2308,6 +2338,12 @@ public class VoxelEditorState extends BaseAppState {
int colX2b = (int)(hk0 / 60001L) - 30000; int colX2b = (int)(hk0 / 60001L) - 30000;
int colZ2b = (int)(hk0 % 60001L) - 30000; int colZ2b = (int)(hk0 % 60001L) - 30000;
// Schritt 2 hat diese Spalte bereits auf eine Rampe gesetzt → Pass A
// nicht überschreiben, sonst entsteht ein Konflikt (Schritt-2-Steigung
// vs. terrain-basierter Rampenwert). Pass A nur für Spalten die Schritt 2
// unberührt gelassen hat (smoothH ≈ h0raw = Plateau oder unterste Schicht).
if (Math.abs(h0sm - h0raw) > 0.1f) continue;
float minRampH = h0sm; float minRampH = h0sm;
float minRaiseH = Float.POSITIVE_INFINITY; float minRaiseH = Float.POSITIVE_INFINITY;
float passA_maxThAbove = Float.NEGATIVE_INFINITY; float passA_maxThAbove = Float.NEGATIVE_INFINITY;
@@ -2392,7 +2428,9 @@ public class VoxelEditorState extends BaseAppState {
for (Map.Entry<Long, Float> entry : hfMap.entrySet()) { for (Map.Entry<Long, Float> entry : hfMap.entrySet()) {
long hk0 = entry.getKey(); long hk0 = entry.getKey();
float h0raw = entry.getValue(); float h0raw = entry.getValue();
if (h0raw > 4.25f) continue; // Kein Höhen-Cutoff hier – Pass B muss für alle Voxel-Säulen laufen.
// Der frühere "h0raw > 4.25f"-Guard hat Pass B für praktisch alle
// Terrain-Voxel (Welt-Y >> 4) komplett deaktiviert.
float h0sm = smoothMap.getOrDefault(hk0, h0raw); float h0sm = smoothMap.getOrDefault(hk0, h0raw);
int colX2b = (int)(hk0 / 60001L) - 30000; int colX2b = (int)(hk0 / 60001L) - 30000;
int colZ2b = (int)(hk0 % 60001L) - 30000; int colZ2b = (int)(hk0 % 60001L) - 30000;
@@ -2717,14 +2755,11 @@ public class VoxelEditorState extends BaseAppState {
} }
} }
// Pre-Bake-Backup anlegen (ermöglicht Revert nach dem Bake). // .blvc sicherstellen bevor der Bake läuft (falls Auto-Save noch nicht lief)
// Wichtig: zuerst .blvc auf Disk speichern, falls Auto-Save noch nicht gelaufen ist,
// damit savePreBake() etwas zum Kopieren hat.
for (VoxelChunk chunk : nonEmpty) { for (VoxelChunk chunk : nonEmpty) {
try { try {
VoxelChunkIO.save(chunk); VoxelChunkIO.save(chunk);
VoxelChunkIO.savePreBake(chunk.cx, chunk.cy, chunk.cz); } catch (Exception e) { log.warn("Speichern vor Bake fehlgeschlagen ({},{},{}): {}",
} catch (Exception e) { log.warn("Pre-Bake-Backup fehlgeschlagen ({},{},{}): {}",
chunk.cx, chunk.cy, chunk.cz, e.getMessage()); } chunk.cx, chunk.cy, chunk.cz, e.getMessage()); }
} }
@@ -2760,6 +2795,7 @@ public class VoxelEditorState extends BaseAppState {
input.bakeStatusMsg = msg; input.bakeStatusMsg = msg;
input.sculptRescanNeeded = true; input.sculptRescanNeeded = true;
log.info("Voxel-Bake abgeschlossen – {}.", msg); log.info("Voxel-Bake abgeschlossen – {}.", msg);
return successfullyBaked.size();
} }
/** Bäckt einen einzelnen Chunk. Gibt true zurück wenn erfolgreich, false bei Fehler. */ /** Bäckt einen einzelnen Chunk. Gibt true zurück wenn erfolgreich, false bei Fehler. */

View File

@@ -1266,8 +1266,9 @@ public class TerrainPanelController {
"wird alles neu gebacken – kann je nach Groesse der Karte sehr lange dauern."); "wird alles neu gebacken – kann je nach Groesse der Karte sehr lange dauern.");
rebakeWarning.setStyle("-fx-text-fill: #b05000; -fx-font-size: 10;"); rebakeWarning.setStyle("-fx-text-fill: #b05000; -fx-font-size: 10;");
rebakeWarning.setWrapText(true); rebakeWarning.setWrapText(true);
// visibleProperty binden – aber NICHT managedProperty: der Label belegt immer
// seinen Platz, damit sich das Layout nicht verschiebt und der OK-Button sichtbar bleibt.
rebakeWarning.visibleProperty().bind(cbRebakeAll.selectedProperty()); rebakeWarning.visibleProperty().bind(cbRebakeAll.selectedProperty());
rebakeWarning.managedProperty().bind(cbRebakeAll.selectedProperty());
javafx.scene.layout.VBox content = new javafx.scene.layout.VBox(8, javafx.scene.layout.VBox content = new javafx.scene.layout.VBox(8,
cbUnderground, cbOrphaned, cbUnderground, cbOrphaned,
@@ -1298,7 +1299,7 @@ public class TerrainPanelController {
if (doRebakeAll) { if (doRebakeAll) {
input.markAllChunksDirtyRequested = true; input.markAllChunksDirtyRequested = true;
statusLbl.setText("Alle Chunks als dirty markiert."); statusLbl.setText("Alle Chunks als dirty markiert – Bake beim Tab-Verlassen.");
} }
if (doUnderground || doOrphaned) { if (doUnderground || doOrphaned) {
input.cleanupUndergroundVoxels = doUnderground; input.cleanupUndergroundVoxels = doUnderground;