Fehler beim Backen von Voxels behoben

This commit is contained in:
2026-08-08 18:16:32 +02:00
parent 96483fada9
commit 936efb7bcc
11 changed files with 177 additions and 21 deletions

View File

@@ -3471,12 +3471,47 @@ public class EditorApp extends Application {
Label name = new Label(param.getName()); Label name = new Label(param.getName());
name.setStyle("-fx-text-fill: #111111;"); name.setStyle("-fx-text-fill: #111111;");
Slider slider = new Slider(param.getMin(), param.getMax(), param.getValue()); if (tool instanceof de.blight.editor.tool.VoxelTool vtR
slider.setShowTickMarks(true); && param == vtR.brushRadius) {
slider.setShowTickLabels(true); // Ganzzahliger Slider: snapToTicks mit majorTickUnit=1 → nur ganze Zahlen
slider.setMajorTickUnit((param.getMax() - param.getMin()) / 2); int initR = Math.max(1, (int) Math.round(param.getValue()));
slider.valueProperty().addListener((obs, oldV, newV) -> param.setValue(newV.doubleValue())); Slider slider = new Slider(1, 30, initR);
panel.getChildren().add(new VBox(3, name, withField(slider, "%.3f"))); slider.setShowTickMarks(true);
slider.setShowTickLabels(true);
slider.setMajorTickUnit(5);
slider.setMinorTickCount(4);
slider.setSnapToTicks(true);
slider.setBlockIncrement(1);
slider.valueProperty().addListener((obs, oldV, newV) ->
param.setValue(Math.round(newV.doubleValue())));
panel.getChildren().add(new VBox(3, name, withField(slider, "%.0f")));
} else if (tool instanceof de.blight.editor.tool.VoxelTool vtP
&& param == vtP.plateauTarget) {
// x.5-gestufter Slider: min=0.5, Schritt=1 → 0.5, 1.5, 2.5, …
double initSnap = Math.round(param.getValue() - 0.5) + 0.5;
initSnap = Math.max(0.5, Math.min(49.5, initSnap));
Slider slider = new Slider(0.5, 49.5, initSnap);
slider.setShowTickMarks(true);
slider.setShowTickLabels(true);
slider.setMajorTickUnit(5);
slider.setMinorTickCount(4);
slider.setSnapToTicks(true);
slider.setBlockIncrement(1.0);
slider.valueProperty().addListener((obs, oldV, newV) -> {
double sv = Math.round(newV.doubleValue() - 0.5) + 0.5;
param.setValue(sv);
});
panel.getChildren().add(new VBox(3, name, withField(slider, "%.1f")));
} else {
Slider slider = new Slider(param.getMin(), param.getMax(), param.getValue());
slider.setShowTickMarks(true);
slider.setShowTickLabels(true);
slider.setMajorTickUnit((param.getMax() - param.getMin()) / 2);
slider.valueProperty().addListener((obs, oldV, newV) -> param.setValue(newV.doubleValue()));
panel.getChildren().add(new VBox(3, name, withField(slider, "%.3f")));
}
} }
// Textur-Slot-Konfigurator (nur beim TextureTool) // Textur-Slot-Konfigurator (nur beim TextureTool)
@@ -3635,8 +3670,36 @@ public class EditorApp extends Application {
selPoller.setCycleCount(javafx.animation.Animation.INDEFINITE); selPoller.setCycleCount(javafx.animation.Animation.INDEFINITE);
selPoller.play(); selPoller.play();
// ── Unterirdische Voxel bereinigen ──────────────────────────────────
Label cleanupStatus = new Label("");
cleanupStatus.setWrapText(true);
cleanupStatus.setStyle("-fx-text-fill: #555; -fx-font-size: 11;");
Button cleanupBtn = new Button("Unterirdische Voxel bereinigen");
cleanupBtn.setMaxWidth(Double.MAX_VALUE);
cleanupBtn.setOnAction(e -> {
input.cleanupUndergroundStatus = null;
input.cleanupUndergroundRequested = true;
cleanupBtn.setDisable(true);
cleanupStatus.setText("Läuft...");
});
javafx.animation.Timeline cleanupPoller = new javafx.animation.Timeline(
new javafx.animation.KeyFrame(javafx.util.Duration.millis(200), ev -> {
String msg = input.cleanupUndergroundStatus;
if (msg != null) {
input.cleanupUndergroundStatus = null;
cleanupStatus.setText(msg);
cleanupBtn.setDisable(false);
}
})
);
cleanupPoller.setCycleCount(javafx.animation.Animation.INDEFINITE);
cleanupPoller.play();
panel.getChildren().addAll(new Separator(), bakeBtn, panel.getChildren().addAll(new Separator(), bakeBtn,
bakeBar, bakeBarLabel, bakeStatus, bakeBar, bakeBarLabel, bakeStatus,
new Separator(), cleanupBtn, cleanupStatus,
new Separator(), selInfoLabel, selHintLabel, deleteBtn, revertBtn); new Separator(), selInfoLabel, selHintLabel, deleteBtn, revertBtn);
} }
} }

View File

@@ -801,6 +801,10 @@ public class SharedInput {
public volatile boolean deleteMarkedBakedRequested = false; public volatile boolean deleteMarkedBakedRequested = false;
/** JFX → JME: selektierte Chunks aus Pre-Bake-Backup wiederherstellen. */ /** JFX → JME: selektierte Chunks aus Pre-Bake-Backup wiederherstellen. */
public volatile boolean revertMarkedBakedRequested = false; public volatile boolean revertMarkedBakedRequested = false;
/** JFX → JME: unterirdische Voxel-Spalten (Oberfläche < Basis-Terrain) löschen. */
public volatile boolean cleanupUndergroundRequested = false;
/** JME → JFX: Ergebnis der Unterirdisch-Bereinigung (null = läuft noch / nicht gestartet). */
public volatile String cleanupUndergroundStatus = null;
/** Terrain-Slot (0-7) für flache Voxel-Flächen, -1 = kein Slot. */ /** Terrain-Slot (0-7) für flache Voxel-Flächen, -1 = kein Slot. */
public volatile int voxelFlatSlot = -1; public volatile int voxelFlatSlot = -1;

View File

@@ -755,9 +755,12 @@ public class VoxelEditorState extends BaseAppState {
// gebakte Meshes nach Distanz priorisiert immer der korrekte Oberflächenpunkt. // gebakte Meshes nach Distanz priorisiert immer der korrekte Oberflächenpunkt.
if (!isHorizontal && isColumn && modeIdx == de.blight.editor.tool.VoxelTool.MODE_PLATEAU && lower) { if (!isHorizontal && isColumn && modeIdx == de.blight.editor.tool.VoxelTool.MODE_PLATEAU && lower) {
if (Float.isFinite(wy)) { if (Float.isFinite(wy)) {
input.voxelTool.plateauTarget.setValue(wy); // Auf x.5-Raster einrasten: MC-Oberfläche liegt ~0.5m über dem Zelldach,
// daher ist 1.5m, 2.5m, 3.5m usw. der korrekte Zielwert.
double snapped = Math.round(wy - 0.5) + 0.5;
input.voxelTool.plateauTarget.setValue(snapped);
input.voxelTool.plateauTargetChanged = true; input.voxelTool.plateauTargetChanged = true;
input.heightTool.plateauHeight.setValue(wy); input.heightTool.plateauHeight.setValue(snapped);
input.heightTool.plateauHeightChanged = true; input.heightTool.plateauHeightChanged = true;
} }
return; return;
@@ -1330,12 +1333,27 @@ public class VoxelEditorState extends BaseAppState {
} }
// Terrain-Höhe als Fallback wenn noch keine Voxel in dieser Spalte // Terrain-Höhe als Fallback wenn noch keine Voxel in dieser Spalte
float th = currentTopLY < 0 ? terrainH(wx, wz) : Float.NaN; float th = currentTopLY < 0 ? terrainH(wx, wz) : Float.NaN;
float currentTopWY = currentTopLY >= 0
? VoxelChunk.toWorldY(cy, currentTopLY)
: th;
float diff = targetH - currentTopWY; // MC-Oberfläche liegt ~0.5m über der obersten soliden Zelle.
if (Math.abs(diff) < 0.5f) continue; // Ohne Offset pendelt das Tool: raise→3m, diff=-0.5→lower→2m, diff=+0.5→raise→...
// Für leere Spalten (Terrain-Fallback) reicht terrainH direkt; dort
// genügt 0.1m Schwelle, damit z.B. Terrain 2m → Ziel 2.5m zuverlässig greift.
float effectiveCurrentH;
float threshold;
if (currentTopLY >= 0) {
effectiveCurrentH = VoxelChunk.toWorldY(cy, currentTopLY) + 0.5f;
threshold = 0.5f;
} else {
effectiveCurrentH = th;
threshold = 0.1f;
}
float diff = targetH - effectiveCurrentH;
if (Math.abs(diff) < threshold) continue;
// Ziel-Zelle: floor(targetH) = oberste SOLIDE Zelle bei Ziel-Höhe.
// Raise/Lower nie über diese Zelle hinaus → verhindert Überschuss-Artefakte.
int targetLY = Math.max(0, Math.min(VoxelChunk.SIZE - 1,
(int) Math.floor(targetH - cy * (float) VoxelChunk.CELLS)));
if (diff > 0) { if (diff > 0) {
// Erhöhen // Erhöhen
@@ -1353,13 +1371,13 @@ public class VoxelEditorState extends BaseAppState {
continue; continue;
} }
} }
int newTop = Math.min(VoxelChunk.SIZE - 1, startLY + step); int newTop = Math.min(startLY + step, targetLY); // nie über Ziel-Zelle
for (int ly = startLY; ly <= newTop; ly++) { for (int ly = startLY; ly <= newTop; ly++) {
chunk.setDensity(lx, ly, lz, (byte) 127); chunk.setDensity(lx, ly, lz, (byte) 127);
} }
} else { } else {
if (currentTopLY < 0) continue; if (currentTopLY < 0) continue;
int newTop = Math.max(0, currentTopLY - step); int newTop = Math.max(currentTopLY - step, targetLY); // nie unter Ziel-Zelle
for (int ly = newTop + 1; ly <= currentTopLY; ly++) { for (int ly = newTop + 1; ly <= currentTopLY; ly++) {
chunk.setDensity(lx, ly, lz, Byte.MIN_VALUE); chunk.setDensity(lx, ly, lz, Byte.MIN_VALUE);
} }
@@ -2093,10 +2111,81 @@ public class VoxelEditorState extends BaseAppState {
// Fix v6 (aktuell): Steigungs-Fortsetzung komplett gestrichen. Reiner Distanz-Blend // Fix v6 (aktuell): Steigungs-Fortsetzung komplett gestrichen. Reiner Distanz-Blend
// von der (ring-gemittelten) Kanten-Höhe zur tatsächlichen Basisterrain-Höhe. Folgt // von der (ring-gemittelten) Kanten-Höhe zur tatsächlichen Basisterrain-Höhe. Folgt
// nicht mehr exakt dem Rampen-Winkel, ist aber garantiert monoton/glatt. // nicht mehr exakt dem Rampen-Winkel, ist aber garantiert monoton/glatt.
int[][] DIRS8 = {{1,0},{-1,0},{0,1},{0,-1},{1,1},{1,-1},{-1,1},{-1,-1}}; // 4:1-Abwärtsrampe bei Stufenabbrüchen (inter-terrace + Basisterrain-Übergang).
//
// Schritt 2 lässt Spalten mit hUp=null flach (smoothH=h0), auch wenn direkt
// daneben eine niedrigere Terasse oder das Basisterrain-Tal liegt. Das erzeugt
// vertikale Klippen an allen Plateau-Kanten.
//
// Fix: Für jede Spalte den nächsten Nachbarn mit Höhe < h0 suchen
// (Voxel-Spalte aus hfMap ODER Basisterrain außerhalb). Wenn gefunden bei
// Chebyshev-Distanz d mit mittlerer Höhe avgLow:
// rampH = avgLow + d / 4.0
// Spalte wird auf min(smoothH, rampH) abgesenkt nie angehoben.
// Entspricht 4 Schritte vor / 1 Schritt runter, deckt beide Bedingungen ab:
// „Tal darunter" (avgLow << h0) und „<1m über Basisterrain" (kleines dH).
// Ringsuche je Spalte unabhängig von anderen Schritt-2b-Ergebnissen →
// keine Kettenfortpflanzung, keine Divergenz.
{
final float RAMP_RATIO = 4.0f;
final int MAX_RAMP_R = 20;
// Perimeter-Extension deaktiviert saubere Kante an der Voxel-Grenze. // Terrain-Cache: vermeidet wiederholte terrainH()-Aufrufe bei Überlappungen
// (Berechnung auskommentiert, Code bleibt für spätere Nutzung erhalten.) Map<Long, Float> terrainCache2b = new HashMap<>();
for (Map.Entry<Long, Float> entry : hfMap.entrySet()) {
long hk0 = entry.getKey();
float h0raw = entry.getValue(); // Original-Voxelhöhe
float h0sm = smoothMap.getOrDefault(hk0, h0raw); // Schritt-2-Ausgabe
int colX2b = (int)(hk0 / 60001L) - 30000;
int colZ2b = (int)(hk0 % 60001L) - 30000;
// Ringsuche nach nächstem niedrigeren Nachbarn (Voxel oder Terrain)
float sumLowH = 0f;
int lowCount = 0;
int lowDist = -1;
outer2b:
for (int r = 1; r <= MAX_RAMP_R; r++) {
for (int dz2b = -r; dz2b <= r; dz2b++) {
for (int dx2b = -r; dx2b <= r; dx2b++) {
if (Math.max(Math.abs(dx2b), Math.abs(dz2b)) != r) continue;
long hkN = (long)(colX2b + dx2b + 30000) * 60001L
+ (colZ2b + dz2b + 30000);
Float hN = hfMap.get(hkN);
float neighborH;
if (hN != null) {
neighborH = hN; // Voxel-Spalte: Originalhöhe
} else {
Float cached = terrainCache2b.get(hkN);
if (cached == null) {
cached = terrainH((float)(colX2b + dx2b),
(float)(colZ2b + dz2b));
terrainCache2b.put(hkN, cached);
}
neighborH = cached; // Basisterrain
}
if (neighborH < h0raw) {
sumLowH += neighborH;
lowCount++;
}
}
}
if (lowCount > 0) {
lowDist = r;
break outer2b;
}
}
if (lowDist < 0) continue; // kein niedrigerer Nachbar im Suchradius
float avgLowH = sumLowH / lowCount;
float rampH = avgLowH + lowDist / RAMP_RATIO;
if (rampH < h0sm) {
smoothMap.put(hk0, rampH);
}
}
}
// ── Schritt 3: Dichte anpassen ──────────────────────────────────── // ── Schritt 3: Dichte anpassen ────────────────────────────────────
// ALLE Spalten aus hfMap bekommen den Dichte-Gradienten geschrieben, auch // ALLE Spalten aus hfMap bekommen den Dichte-Gradienten geschrieben, auch

View File

@@ -25,7 +25,7 @@ public class HeightTool extends EditorTool {
} }
); );
public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 50.0, 1.0, 500.0); public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 5.0, 1.0, 500.0);
public final ToolParameter brushStrength = new ToolParameter("Pinselstärke", 2.0, 0.1, 50.0); public final ToolParameter brushStrength = new ToolParameter("Pinselstärke", 2.0, 0.1, 50.0);
public final ToolParameter plateauHeight = new ToolParameter("Plateau-Höhe", 0.0, -500.0, 500.0); public final ToolParameter plateauHeight = new ToolParameter("Plateau-Höhe", 0.0, -500.0, 500.0);
public volatile boolean plateauHeightChanged = false; public volatile boolean plateauHeightChanged = false;

View File

@@ -47,9 +47,9 @@ public class VoxelTool extends EditorTool {
public volatile boolean modeChanged = false; public volatile boolean modeChanged = false;
public volatile boolean horizontal = false; public volatile boolean horizontal = false;
public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 5.0, 0.5, 30.0); public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 5.0, 1.0, 30.0);
public final ToolParameter brushStrength = new ToolParameter("Stärke", 20.0, 1.0, 80.0); public final ToolParameter brushStrength = new ToolParameter("Stärke", 20.0, 1.0, 80.0);
public final ToolParameter plateauTarget = new ToolParameter("Plateau-Ziel", 0.0, -200.0, 500.0); public final ToolParameter plateauTarget = new ToolParameter("Plateau-Ziel", 1.5, -99.5, 499.5);
public volatile boolean plateauTargetChanged = false; public volatile boolean plateauTargetChanged = false;
@Override public String getName() { return "Voxel"; } @Override public String getName() { return "Voxel"; }