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.
* Für Koordinaten, bei denen die .blvc nach dem Backen gelöscht wurde,
* wird automatisch die .blvc.prebake als Fallback herangezogen.
* Vorhandene .blvc.prebake-Dateien (Altbestand) werden einmalig als .blvc migriert
* und danach gelöscht.
* Gibt leere Liste zurück wenn kein Chunks-Verzeichnis existiert.
*/
public static List<VoxelChunk> loadAll() {
@@ -148,7 +85,6 @@ public final class VoxelChunkIO {
Path dir = ChunkTerrainIO.chunksDir();
if (!Files.isDirectory(dir)) return result;
// Aktive .blvc-Dateien einlesen
Set<String> loaded = new java.util.HashSet<>();
try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir, "voxel_*.blvc")) {
for (Path p : ds) {
@@ -168,12 +104,17 @@ public final class VoxelChunkIO {
}
} 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")) {
for (Path p : ds) {
String name = p.getFileName().toString()
.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("_");
if (parts.length != 3) continue;
try {
@@ -182,10 +123,15 @@ public final class VoxelChunkIO {
? -Integer.parseInt(parts[1].substring(1))
: Integer.parseInt(parts[1]);
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 (IOException ignored) {}
for (Path p : prebakeToDelete) { try { Files.deleteIfExists(p); } catch (IOException ignored) {} }
return result;
}

View File

@@ -399,7 +399,7 @@ public class VoxelEditorState extends BaseAppState {
// Verwaiste gebackene .j3o ohne zugehörige .blvc löschen
for (int[] cxyz : SculptedMeshIO.findAllBakedChunks()) {
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++) {
try { Files.deleteIfExists(VoxelChunkIO.getBakedPath(cx, cy, cz, lod)); } catch (Exception ignored) {}
}
@@ -422,7 +422,9 @@ public class VoxelEditorState extends BaseAppState {
if (input.markAllChunksDirtyRequested) {
input.markAllChunksDirtyRequested = false;
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(() -> {
List<VoxelChunk> allDisk = VoxelChunkIO.loadAll();
app.enqueue(() -> {
@@ -431,7 +433,22 @@ public class VoxelEditorState extends BaseAppState {
if (!chunks.containsKey(key)) chunks.put(key, c);
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();
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();
log.info("bakeAll: {} Chunks übergeben", toProcess.size());
List<VoxelChunk> nonEmpty = new java.util.ArrayList<>();
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).
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,
// solidYSpan < 2 → gleiche Schwelle wie der Game-Loader).
nonEmpty.removeIf(c -> c.solidYSpan() < 2);
log.info("bakeAll: nach solidYSpan-Filter: {} verbleiben (→ diese werden gebacken)", nonEmpty.size());
if (nonEmpty.isEmpty()) {
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
@@ -2236,8 +2260,12 @@ public class VoxelEditorState extends BaseAppState {
// gebackene Terrain systematisch zu hoch machen.
smoothH = h0;
} else {
float dDown = dDownRaw - 1;
smoothH = (dDown <= 0f) ? h0 : (h0 * dUp + hUp * dDown) / (dUp + dDown);
// dDown = dDownRaw (kein -1): auch unmittelbar angrenzende Stufen
// (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);
}
@@ -2291,7 +2319,9 @@ public class VoxelEditorState extends BaseAppState {
final float RAMP_RATIO = 4.0f;
final int MAX_RAMP_R = 20;
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;
Map<Long, Float> terrainCache2b = new HashMap<>();
@@ -2308,6 +2338,12 @@ public class VoxelEditorState extends BaseAppState {
int colX2b = (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 minRaiseH = Float.POSITIVE_INFINITY;
float passA_maxThAbove = Float.NEGATIVE_INFINITY;
@@ -2392,7 +2428,9 @@ public class VoxelEditorState extends BaseAppState {
for (Map.Entry<Long, Float> entry : hfMap.entrySet()) {
long hk0 = entry.getKey();
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);
int colX2b = (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).
// Wichtig: zuerst .blvc auf Disk speichern, falls Auto-Save noch nicht gelaufen ist,
// damit savePreBake() etwas zum Kopieren hat.
// .blvc sicherstellen bevor der Bake läuft (falls Auto-Save noch nicht lief)
for (VoxelChunk chunk : nonEmpty) {
try {
VoxelChunkIO.save(chunk);
VoxelChunkIO.savePreBake(chunk.cx, chunk.cy, chunk.cz);
} catch (Exception e) { log.warn("Pre-Bake-Backup fehlgeschlagen ({},{},{}): {}",
} catch (Exception e) { log.warn("Speichern vor Bake fehlgeschlagen ({},{},{}): {}",
chunk.cx, chunk.cy, chunk.cz, e.getMessage()); }
}
@@ -2760,6 +2795,7 @@ public class VoxelEditorState extends BaseAppState {
input.bakeStatusMsg = msg;
input.sculptRescanNeeded = true;
log.info("Voxel-Bake abgeschlossen – {}.", msg);
return successfullyBaked.size();
}
/** 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.");
rebakeWarning.setStyle("-fx-text-fill: #b05000; -fx-font-size: 10;");
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.managedProperty().bind(cbRebakeAll.selectedProperty());
javafx.scene.layout.VBox content = new javafx.scene.layout.VBox(8,
cbUnderground, cbOrphaned,
@@ -1298,7 +1299,7 @@ public class TerrainPanelController {
if (doRebakeAll) {
input.markAllChunksDirtyRequested = true;
statusLbl.setText("Alle Chunks als dirty markiert.");
statusLbl.setText("Alle Chunks als dirty markiert – Bake beim Tab-Verlassen.");
}
if (doUnderground || doOrphaned) {
input.cleanupUndergroundVoxels = doUnderground;