Compare commits
3 Commits
8feaf24490
...
936efb7bcc
| Author | SHA1 | Date | |
|---|---|---|---|
| 936efb7bcc | |||
| 96483fada9 | |||
| 3b7f3b7e86 |
@@ -14,6 +14,7 @@ import de.blight.eztree.Billboard;
|
||||
import de.blight.eztree.TreeOptions;
|
||||
import de.blight.eztree.TreePresets;
|
||||
import de.blight.eztree.TreeType;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -799,10 +800,15 @@ public class EditorApp extends Application {
|
||||
}
|
||||
|
||||
// Kamera-Koordinaten aktualisieren
|
||||
camCoordsLabel.setText(String.format(
|
||||
String camText = String.format(
|
||||
"X:%.1f Y:%.1f Z:%.1f Yaw:%.0f° Pitch:%.0f°",
|
||||
input.camX, input.camY, input.camZ,
|
||||
input.camYaw, input.camPitch));
|
||||
input.camYaw, input.camPitch);
|
||||
float moh = input.mouseOverlayHeight;
|
||||
if (input.debugHeightOverlayEnabled && !Float.isNaN(moh)) {
|
||||
camText += String.format(java.util.Locale.US, " H:%.2fm", moh);
|
||||
}
|
||||
camCoordsLabel.setText(camText);
|
||||
|
||||
// Konsolen-Antwort anzeigen
|
||||
String consoleMsg = input.consoleOutput;
|
||||
@@ -1318,9 +1324,12 @@ public class EditorApp extends Application {
|
||||
importTexSetItem.setOnAction(e -> openTextureSetImport(primaryStage));
|
||||
importAudioItem.setOnAction(e -> handleAudioImport(primaryStage));
|
||||
importAnimItem.setOnAction(e -> handleAnimationImport(primaryStage));
|
||||
MenuItem screenshotItem = new MenuItem("Screenshot");
|
||||
screenshotItem.setOnAction(e -> takeEditorScreenshot());
|
||||
fileMenu.getItems().addAll(newItem, saveItem, new SeparatorMenuItem(),
|
||||
importModelLodItem, importTexItem, importTexZipItem, importTexSetItem,
|
||||
importAudioItem, importAnimItem);
|
||||
importAudioItem, importAnimItem,
|
||||
new SeparatorMenuItem(), screenshotItem);
|
||||
|
||||
Menu toolsMenu = new Menu("Werkzeuge");
|
||||
MenuItem vegetationsItem = new MenuItem("Vegetations Generator");
|
||||
@@ -1359,9 +1368,9 @@ public class EditorApp extends Application {
|
||||
Menu viewMenu = new Menu("Ansicht");
|
||||
MenuItem resetCam = new MenuItem("Kamera zurücksetzen");
|
||||
resetCam.setOnAction(e -> input.addMouseDelta(0, 0));
|
||||
MenuItem viewTexture = new MenuItem("Textur (Ctrl+G)");
|
||||
MenuItem viewWireframe = new MenuItem("Drahtgitter (Ctrl+G)");
|
||||
viewTopologyItem = new CheckMenuItem("Topologie-Overlay (Ctrl+T)");
|
||||
MenuItem viewTexture = new MenuItem("Textur (Alt+G)");
|
||||
MenuItem viewWireframe = new MenuItem("Drahtgitter (Alt+G)");
|
||||
viewTopologyItem = new CheckMenuItem("Topologie-Overlay (Alt+T)");
|
||||
viewTexture.setOnAction(e -> {
|
||||
wireframeActive = false;
|
||||
input.wireframeRequest = 2;
|
||||
@@ -1382,8 +1391,14 @@ public class EditorApp extends Application {
|
||||
camOrbitItem.setOnAction(e -> input.camMode = SharedInput.CAM_ORBIT);
|
||||
camFreeItem.setOnAction(e -> input.camMode = SharedInput.CAM_FREEFLY);
|
||||
|
||||
CheckMenuItem debugHeightItem = new CheckMenuItem("Höhen-Overlay (Alt+H)");
|
||||
debugHeightItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Alt+H"));
|
||||
debugHeightItem.setOnAction(e ->
|
||||
input.debugHeightOverlayEnabled = debugHeightItem.isSelected());
|
||||
|
||||
viewMenu.getItems().addAll(resetCam, new SeparatorMenuItem(), viewTexture, viewWireframe,
|
||||
new SeparatorMenuItem(), viewTopologyItem,
|
||||
new SeparatorMenuItem(), debugHeightItem,
|
||||
new SeparatorMenuItem(), camOrbitItem, camFreeItem);
|
||||
|
||||
Menu zeitMenu = new Menu("Zeit");
|
||||
@@ -3456,12 +3471,47 @@ public class EditorApp extends Application {
|
||||
Label name = new Label(param.getName());
|
||||
name.setStyle("-fx-text-fill: #111111;");
|
||||
|
||||
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")));
|
||||
if (tool instanceof de.blight.editor.tool.VoxelTool vtR
|
||||
&& param == vtR.brushRadius) {
|
||||
// Ganzzahliger Slider: snapToTicks mit majorTickUnit=1 → nur ganze Zahlen
|
||||
int initR = Math.max(1, (int) Math.round(param.getValue()));
|
||||
Slider slider = new Slider(1, 30, initR);
|
||||
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)
|
||||
@@ -3620,8 +3670,36 @@ public class EditorApp extends Application {
|
||||
selPoller.setCycleCount(javafx.animation.Animation.INDEFINITE);
|
||||
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,
|
||||
bakeBar, bakeBarLabel, bakeStatus,
|
||||
new Separator(), cleanupBtn, cleanupStatus,
|
||||
new Separator(), selInfoLabel, selHintLabel, deleteBtn, revertBtn);
|
||||
}
|
||||
}
|
||||
@@ -5673,6 +5751,21 @@ public class EditorApp extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
private void takeEditorScreenshot() {
|
||||
WritableImage image = primaryStage.getScene().snapshot(null);
|
||||
String timestamp = java.time.LocalDateTime.now()
|
||||
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"));
|
||||
Path dir = BlightHome.resolve("screenshots");
|
||||
try {
|
||||
Files.createDirectories(dir);
|
||||
File out = dir.resolve("editor_" + timestamp + ".png").toFile();
|
||||
ImageIO.write(SwingFXUtils.fromFXImage(image, null), "PNG", out);
|
||||
setStatus("Screenshot gespeichert: " + out.getPath());
|
||||
} catch (IOException ex) {
|
||||
setStatus("Screenshot fehlgeschlagen: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleTextureImport(javafx.stage.Window owner) {
|
||||
FileChooser fc = new FileChooser();
|
||||
fc.setTitle("Texturen importieren (nicht-PNG wird automatisch konvertiert)");
|
||||
@@ -8187,6 +8280,7 @@ public class EditorApp extends Application {
|
||||
case E -> input.down = pressed;
|
||||
case SHIFT -> input.shiftHeld = pressed;
|
||||
case CONTROL -> input.ctrlHeld = pressed;
|
||||
case ALT -> input.altHeld = pressed;
|
||||
case ENTER -> {
|
||||
if (pressed && input.activeLayer == SharedInput.LAYER_VOXEL_CLIFF) {
|
||||
input.generateCliffVoxelsRequested = true;
|
||||
@@ -8228,13 +8322,13 @@ public class EditorApp extends Application {
|
||||
input.waterSampleHeightRequested = true;
|
||||
}
|
||||
case G -> {
|
||||
if (pressed && input.ctrlHeld) {
|
||||
if (pressed && input.altHeld) {
|
||||
wireframeActive = !wireframeActive;
|
||||
input.wireframeRequest = wireframeActive ? 1 : 2;
|
||||
}
|
||||
}
|
||||
case T -> {
|
||||
if (pressed && input.ctrlHeld && viewTopologyItem != null) {
|
||||
if (pressed && input.altHeld && viewTopologyItem != null) {
|
||||
boolean sel = !viewTopologyItem.isSelected();
|
||||
viewTopologyItem.setSelected(sel);
|
||||
input.topologyRequest = sel ? 1 : 2;
|
||||
|
||||
@@ -60,9 +60,10 @@ public class SharedInput {
|
||||
public final java.util.concurrent.atomic.AtomicInteger scrollAccum =
|
||||
new java.util.concurrent.atomic.AtomicInteger();
|
||||
|
||||
// ── Shift / Ctrl ─────────────────────────────────────────────────────────
|
||||
// ── Shift / Ctrl / Alt ───────────────────────────────────────────────────
|
||||
public volatile boolean shiftHeld;
|
||||
public volatile boolean ctrlHeld;
|
||||
public volatile boolean altHeld;
|
||||
|
||||
// ── Debug-Toggle: Strg+F8 schaltet Raw-Texture-Modus (kein Lighting) ─────
|
||||
public volatile boolean debugNoLightToggle;
|
||||
@@ -185,6 +186,8 @@ public class SharedInput {
|
||||
// ── Mausposition im Viewport (JavaFX-Pixel, -1 = außerhalb) ─────────────
|
||||
public volatile float mouseScreenX = -1f;
|
||||
public volatile float mouseScreenY = -1f;
|
||||
/** JME → JavaFX: Höhe am Mauszeiger wenn Height-Overlay aktiv (NaN = unbekannt). */
|
||||
public volatile float mouseOverlayHeight = Float.NaN;
|
||||
|
||||
// ── Speichern ─────────────────────────────────────────────────────────────
|
||||
public volatile boolean saveRequested = false;
|
||||
@@ -783,6 +786,9 @@ public class SharedInput {
|
||||
/** JME → JFX: Status-Meldung nach Abschluss des Backens oder Löschens. */
|
||||
public volatile String bakeStatusMsg = null;
|
||||
|
||||
/** JFX → JME: Höhen-Overlay ein/aus (Terrain / Voxel / Baked im 100m-Radius). */
|
||||
public volatile boolean debugHeightOverlayEnabled = false;
|
||||
|
||||
/**
|
||||
* Per BFS selektierte gebackene Chunks (chunkKey-kodiert).
|
||||
* Thread-sicher; JME schreibt, JFX liest (nur size() für Anzeige).
|
||||
@@ -795,6 +801,10 @@ public class SharedInput {
|
||||
public volatile boolean deleteMarkedBakedRequested = false;
|
||||
/** JFX → JME: selektierte Chunks aus Pre-Bake-Backup wiederherstellen. */
|
||||
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. */
|
||||
public volatile int voxelFlatSlot = -1;
|
||||
|
||||
@@ -131,6 +131,23 @@ public class VoxelEditorState extends BaseAppState {
|
||||
|
||||
private Geometry brushIndicator;
|
||||
|
||||
// ── Höhen-Debug-Overlay ───────────────────────────────────────────────────
|
||||
|
||||
private Node debugPtsNode;
|
||||
private com.jme3.font.BitmapFont debugFont;
|
||||
private com.jme3.font.BitmapText debugHudText;
|
||||
private java.util.List<com.jme3.font.BitmapText> debugLabelsTerrain = new java.util.ArrayList<>();
|
||||
private java.util.List<com.jme3.font.BitmapText> debugLabelsVoxel = new java.util.ArrayList<>();
|
||||
private java.util.List<com.jme3.font.BitmapText> debugLabelsBaked = new java.util.ArrayList<>();
|
||||
private float[] debugLabelXZ; // [i*2]=x, [i*2+1]=z
|
||||
private float[] debugLabelTH; // Terrain-Höhe
|
||||
private float[] debugLabelVH; // Voxel-Höhe (NaN = kein Voxel)
|
||||
private float[] debugLabelBH; // Baked-Höhe (NaN = kein Treffer)
|
||||
private boolean debugOverlayActive = false;
|
||||
private float debugCenterX = Float.NaN;
|
||||
private float debugCenterZ = Float.NaN;
|
||||
private int debugLastStep = -1;
|
||||
|
||||
// ── Basis-Terrain-Referenzebene (y = -10) ────────────────────────────────
|
||||
|
||||
/** Flache Referenzebene bei Welt-Y = -10; nur im LAYER_VOXEL sichtbar. */
|
||||
@@ -231,9 +248,12 @@ public class VoxelEditorState extends BaseAppState {
|
||||
protected void cleanup(Application app) {
|
||||
executor.shutdownNow();
|
||||
voxelRoot.removeFromParent();
|
||||
if (brushIndicator != null) brushIndicator.removeFromParent();
|
||||
if (basePlaneNode != null) basePlaneNode.removeFromParent();
|
||||
if (wireframeActive) applyWireframe(false);
|
||||
if (brushIndicator != null) brushIndicator.removeFromParent();
|
||||
if (basePlaneNode != null) basePlaneNode.removeFromParent();
|
||||
if (debugPtsNode != null) debugPtsNode.removeFromParent();
|
||||
if (debugHudText != null) debugHudText.removeFromParent();
|
||||
clearDebugLabels();
|
||||
if (wireframeActive) applyWireframe(false);
|
||||
nodes.clear();
|
||||
chunks.clear();
|
||||
}
|
||||
@@ -263,6 +283,25 @@ public class VoxelEditorState extends BaseAppState {
|
||||
// Brush-Indikator immer aktualisieren (zeigen/verstecken je nach Layer)
|
||||
updateBrushIndicator();
|
||||
|
||||
// Höhen-Overlay ein/ausschalten und aktualisieren
|
||||
if (input.debugHeightOverlayEnabled != debugOverlayActive) {
|
||||
debugOverlayActive = input.debugHeightOverlayEnabled;
|
||||
applyDebugOverlay(debugOverlayActive);
|
||||
}
|
||||
if (debugOverlayActive) {
|
||||
float cx = cam.getLocation().x, cz = cam.getLocation().z;
|
||||
int step = debugLabelStep(cam.getLocation().y);
|
||||
float halfStep = step / 2f;
|
||||
if (Float.isNaN(debugCenterX)
|
||||
|| Math.abs(cx - debugCenterX) > halfStep
|
||||
|| Math.abs(cz - debugCenterZ) > halfStep
|
||||
|| step != debugLastStep) {
|
||||
debugCenterX = cx; debugCenterZ = cz; debugLastStep = step;
|
||||
rebuildDebugGeometry(cx, cz);
|
||||
}
|
||||
refreshDebugHud();
|
||||
}
|
||||
|
||||
// Bake angefordert?
|
||||
if (input.bakeVoxelsRequested) {
|
||||
input.bakeVoxelsRequested = false;
|
||||
@@ -716,9 +755,12 @@ public class VoxelEditorState extends BaseAppState {
|
||||
// gebakte Meshes nach Distanz priorisiert – immer der korrekte Oberflächenpunkt.
|
||||
if (!isHorizontal && isColumn && modeIdx == de.blight.editor.tool.VoxelTool.MODE_PLATEAU && lower) {
|
||||
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.heightTool.plateauHeight.setValue(wy);
|
||||
input.heightTool.plateauHeight.setValue(snapped);
|
||||
input.heightTool.plateauHeightChanged = true;
|
||||
}
|
||||
return;
|
||||
@@ -1112,6 +1154,21 @@ public class VoxelEditorState extends BaseAppState {
|
||||
return 0f;
|
||||
}
|
||||
|
||||
/** Höchste bekannte Oberfläche an (worldX, worldZ): max(Terrain, Voxel, Baked). */
|
||||
public float heightAt(float worldX, float worldZ) {
|
||||
float h = terrainH(worldX, worldZ);
|
||||
float vh = columnTopWorldY(worldX, worldZ);
|
||||
if (vh > h + 0.1f) h = vh;
|
||||
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
|
||||
if (smes != null) {
|
||||
com.jme3.math.Ray r = new com.jme3.math.Ray(
|
||||
new Vector3f(worldX, 500f, worldZ), new Vector3f(0f, -1f, 0f));
|
||||
Vector3f[] hit = smes.raycastGeometryWithNormal(r);
|
||||
if (hit != null && hit[0].y > h) h = hit[0].y;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
private boolean hasTerrainMesh() {
|
||||
return terrainEditorState != null || terrainQuad != null;
|
||||
}
|
||||
@@ -1201,18 +1258,34 @@ public class VoxelEditorState extends BaseAppState {
|
||||
return results.getClosestCollision().getContactPoint();
|
||||
}
|
||||
|
||||
/** Welt-Y des höchsten Solid-Voxels an (worldX, worldZ), oder Terrain-Höhe wenn keine Voxel. */
|
||||
/**
|
||||
* Interpolierte Welt-Y der Voxel-Oberfläche an (worldX, worldZ).
|
||||
* Verwendet dieselbe Lineare Interpolation wie Marching Cubes (t = -d0 / (d1 - d0)),
|
||||
* sodass der zurückgegebene Wert mit dem echten Mesh übereinstimmt.
|
||||
* Gibt Terrain-Höhe zurück wenn keine Voxel vorhanden.
|
||||
*/
|
||||
public float columnTopWorldY(float worldX, float worldZ) {
|
||||
int cx = VoxelChunk.worldXToCx(worldX);
|
||||
int cz = VoxelChunk.worldZToCz(worldZ);
|
||||
int lx = Math.max(0, Math.min(VoxelChunk.SIZE - 1, (int) VoxelChunk.worldXToLocal(worldX, cx)));
|
||||
int cx = VoxelChunk.worldXToCx(worldX);
|
||||
int cz = VoxelChunk.worldZToCz(worldZ);
|
||||
int lx = Math.max(0, Math.min(VoxelChunk.SIZE - 1, (int) VoxelChunk.worldXToLocal(worldX, cx)));
|
||||
int lzl = Math.max(0, Math.min(VoxelChunk.SIZE - 1, (int) VoxelChunk.worldZToLocal(worldZ, cz)));
|
||||
for (int cy = 10; cy >= -2; cy--) {
|
||||
VoxelChunk chunk = chunks.get(chunkKey(cx, cy, cz));
|
||||
if (chunk == null || chunk.isEmpty()) continue;
|
||||
for (int ly = VoxelChunk.SIZE - 1; ly >= 0; ly--) {
|
||||
if (chunk.getDensity(lx, ly, lzl) > 0) {
|
||||
return VoxelChunk.toWorldY(cy, ly);
|
||||
float d0 = chunk.getDensity(lx, ly, lzl);
|
||||
if (d0 > 0) {
|
||||
// Density der Zelle darüber (ggf. nächster Chunk)
|
||||
float d1;
|
||||
if (ly + 1 < VoxelChunk.SIZE) {
|
||||
d1 = chunk.getDensity(lx, ly + 1, lzl);
|
||||
} else {
|
||||
VoxelChunk above = chunks.get(chunkKey(cx, cy + 1, cz));
|
||||
d1 = (above != null) ? above.getDensity(lx, 0, lzl) : -1f;
|
||||
}
|
||||
float t = (Math.abs(d1 - d0) < 0.001f) ? 0.5f
|
||||
: Math.max(0f, Math.min(1f, -d0 / (d1 - d0)));
|
||||
return VoxelChunk.toWorldY(cy, ly) + t;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1260,12 +1333,27 @@ public class VoxelEditorState extends BaseAppState {
|
||||
}
|
||||
// Terrain-Höhe als Fallback wenn noch keine Voxel in dieser Spalte
|
||||
float th = currentTopLY < 0 ? terrainH(wx, wz) : Float.NaN;
|
||||
float currentTopWY = currentTopLY >= 0
|
||||
? VoxelChunk.toWorldY(cy, currentTopLY)
|
||||
: th;
|
||||
|
||||
float diff = targetH - currentTopWY;
|
||||
if (Math.abs(diff) < 0.5f) continue;
|
||||
// MC-Oberfläche liegt ~0.5m über der obersten soliden Zelle.
|
||||
// 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) {
|
||||
// Erhöhen
|
||||
@@ -1283,13 +1371,13 @@ public class VoxelEditorState extends BaseAppState {
|
||||
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++) {
|
||||
chunk.setDensity(lx, ly, lz, (byte) 127);
|
||||
}
|
||||
} else {
|
||||
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++) {
|
||||
chunk.setDensity(lx, ly, lz, Byte.MIN_VALUE);
|
||||
}
|
||||
@@ -1490,6 +1578,235 @@ public class VoxelEditorState extends BaseAppState {
|
||||
* Bäckt alle übergebenen Chunks: speichert .blvc, glättet die Dichte mit
|
||||
* Nachbar-Lookup, erzeugt LOD0/1/2-Meshes und exportiert sie als .j3o.
|
||||
*/
|
||||
// ── Höhen-Debug-Overlay ───────────────────────────────────────────────────
|
||||
|
||||
private void applyDebugOverlay(boolean on) {
|
||||
if (on) {
|
||||
if (debugFont == null) {
|
||||
debugFont = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
|
||||
}
|
||||
if (debugPtsNode == null) {
|
||||
debugPtsNode = new Node("debugHeightPts");
|
||||
debugPtsNode.setShadowMode(RenderQueue.ShadowMode.Off);
|
||||
}
|
||||
if (debugHudText == null) {
|
||||
debugHudText = new com.jme3.font.BitmapText(debugFont, false);
|
||||
debugHudText.setSize(debugFont.getCharSet().getRenderedSize() * 1.4f);
|
||||
debugHudText.setColor(com.jme3.math.ColorRGBA.Yellow);
|
||||
}
|
||||
app.getRootNode().attachChild(debugPtsNode);
|
||||
app.getGuiNode().attachChild(debugHudText);
|
||||
debugCenterX = Float.NaN; // erzwingt sofortigen Rebuild
|
||||
} else {
|
||||
if (debugPtsNode != null) debugPtsNode.removeFromParent();
|
||||
if (debugHudText != null) debugHudText.removeFromParent();
|
||||
clearDebugLabels();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut drei farbige Punkt-Wolken im 100m-Radius um (cx, cz):
|
||||
* Grau = Basis-Terrain
|
||||
* Cyan = Raw-Voxel-Oberfläche
|
||||
* Orange = Gebackene Mesh-Oberfläche (4m-Raster, Downward-Raycast)
|
||||
* Jeder Punkt sitzt an seiner echten Welt-Y-Position, sodass
|
||||
* man von der Seite drei überlagerte Flächen sieht.
|
||||
*/
|
||||
private void rebuildDebugGeometry(float cx, float cz) {
|
||||
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
|
||||
clearDebugLabels();
|
||||
|
||||
int ls = debugLastStep;
|
||||
int lr = debugLabelRadius(ls);
|
||||
java.util.List<float[]> lPts = new java.util.ArrayList<>();
|
||||
for (int dz = -lr; dz <= lr; dz += ls) {
|
||||
for (int dx = -lr; dx <= lr; dx += ls) {
|
||||
if (dx * dx + dz * dz > lr * lr) continue;
|
||||
lPts.add(new float[]{cx + dx, cz + dz});
|
||||
}
|
||||
}
|
||||
int ln = lPts.size();
|
||||
debugLabelXZ = new float[ln * 2];
|
||||
debugLabelTH = new float[ln];
|
||||
debugLabelVH = new float[ln]; // NaN wenn kein Voxel
|
||||
debugLabelBH = new float[ln];
|
||||
|
||||
com.jme3.math.ColorRGBA colTerrain = com.jme3.math.ColorRGBA.White;
|
||||
com.jme3.math.ColorRGBA colVoxel = new com.jme3.math.ColorRGBA(0.4f, 0.8f, 1f, 1f);
|
||||
com.jme3.math.ColorRGBA colBaked = new com.jme3.math.ColorRGBA(1f, 0.5f, 0.5f, 1f);
|
||||
|
||||
for (int i = 0; i < ln; i++) {
|
||||
float lx = lPts.get(i)[0], lz = lPts.get(i)[1];
|
||||
debugLabelXZ[i * 2] = lx;
|
||||
debugLabelXZ[i * 2 + 1] = lz;
|
||||
float th = terrainH(lx, lz);
|
||||
float vh = columnTopWorldY(lx, lz);
|
||||
debugLabelTH[i] = th;
|
||||
debugLabelVH[i] = (vh > th + 0.1f) ? vh : Float.NaN; // NaN wenn kein Voxel
|
||||
if (smes != null) {
|
||||
com.jme3.math.Ray r = new com.jme3.math.Ray(
|
||||
new Vector3f(lx, 500f, lz), new Vector3f(0f, -1f, 0f));
|
||||
Vector3f[] hit = smes.raycastGeometryWithNormal(r);
|
||||
debugLabelBH[i] = (hit != null) ? hit[0].y : Float.NaN;
|
||||
} else {
|
||||
debugLabelBH[i] = Float.NaN;
|
||||
}
|
||||
debugLabelsTerrain.add(makeLabelBt(colTerrain));
|
||||
debugLabelsVoxel .add(makeLabelBt(colVoxel));
|
||||
debugLabelsBaked .add(makeLabelBt(colBaked));
|
||||
}
|
||||
}
|
||||
|
||||
private int debugLabelStep(float camY) {
|
||||
if (camY < 25f) return 1;
|
||||
else if (camY < 70f) return 4;
|
||||
else return 8;
|
||||
}
|
||||
|
||||
private int debugLabelRadius(int step) {
|
||||
if (step <= 1) return 30;
|
||||
else if (step <= 4) return 60;
|
||||
else return 120;
|
||||
}
|
||||
|
||||
private com.jme3.font.BitmapText makeLabelBt(com.jme3.math.ColorRGBA color) {
|
||||
com.jme3.font.BitmapText bt = new com.jme3.font.BitmapText(debugFont, false);
|
||||
bt.setSize(11f);
|
||||
bt.setColor(color);
|
||||
bt.setText("");
|
||||
bt.setCullHint(com.jme3.scene.Spatial.CullHint.Always);
|
||||
app.getGuiNode().attachChild(bt);
|
||||
return bt;
|
||||
}
|
||||
|
||||
private void clearDebugLabels() {
|
||||
for (com.jme3.font.BitmapText bt : debugLabelsTerrain) bt.removeFromParent();
|
||||
for (com.jme3.font.BitmapText bt : debugLabelsVoxel) bt.removeFromParent();
|
||||
for (com.jme3.font.BitmapText bt : debugLabelsBaked) bt.removeFromParent();
|
||||
debugLabelsTerrain.clear();
|
||||
debugLabelsVoxel .clear();
|
||||
debugLabelsBaked .clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt eine Geometrie aus kurzen vertikalen Tick-Linien (0.5m hoch) an jedem
|
||||
* Probenpunkt. Mode.Lines macht die Ticks deutlich sichtbarer als einzelne Pixel.
|
||||
* posList: [x0,y0,z0, x1,y1,z1, ...] – Fußpunkte der Ticks.
|
||||
*/
|
||||
private Geometry makeTickCloud(String name, java.util.List<Float> posList,
|
||||
com.jme3.math.ColorRGBA color) {
|
||||
int n = posList.size() / 3;
|
||||
// 2 Vertices pro Tick (Basis + Spitze), je 3 Positions-Floats
|
||||
FloatBuffer posBuf = BufferUtils.createFloatBuffer(n * 6);
|
||||
FloatBuffer colBuf = BufferUtils.createFloatBuffer(n * 8);
|
||||
for (int i = 0; i < n; i++) {
|
||||
float x = posList.get(i * 3);
|
||||
float y = posList.get(i * 3 + 1);
|
||||
float z = posList.get(i * 3 + 2);
|
||||
posBuf.put(x).put(y).put(z); // Basis
|
||||
posBuf.put(x).put(y + 0.5f).put(z); // Spitze
|
||||
colBuf.put(color.r).put(color.g).put(color.b).put(color.a);
|
||||
colBuf.put(color.r).put(color.g).put(color.b).put(color.a);
|
||||
}
|
||||
posBuf.rewind(); colBuf.rewind();
|
||||
|
||||
Mesh mesh = new Mesh();
|
||||
mesh.setMode(Mesh.Mode.Lines);
|
||||
mesh.setBuffer(VertexBuffer.Type.Position, 3, posBuf);
|
||||
mesh.setBuffer(VertexBuffer.Type.Color, 4, colBuf);
|
||||
mesh.updateBound();
|
||||
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setBoolean("VertexColor", true);
|
||||
mat.getAdditionalRenderState().setDepthTest(false);
|
||||
|
||||
Geometry geo = new Geometry(name, mesh);
|
||||
geo.setMaterial(mat);
|
||||
geo.setShadowMode(RenderQueue.ShadowMode.Off);
|
||||
return geo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert den HUD-Text mit den Höhen direkt unterhalb der Kamera.
|
||||
* Position: oben links im Viewport.
|
||||
*/
|
||||
private void refreshDebugHud() {
|
||||
float wx = cam.getLocation().x;
|
||||
float wz = cam.getLocation().z;
|
||||
float th = terrainH(wx, wz);
|
||||
float vh = columnTopWorldY(wx, wz);
|
||||
|
||||
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
|
||||
String bakedLine = "";
|
||||
if (smes != null) {
|
||||
com.jme3.math.Ray ray = new com.jme3.math.Ray(
|
||||
new Vector3f(wx, 500f, wz), new Vector3f(0f, -1f, 0f));
|
||||
Vector3f[] hit = smes.raycastGeometryWithNormal(ray);
|
||||
if (hit != null) {
|
||||
bakedLine = String.format(java.util.Locale.US,
|
||||
"\n Baked (orange): %.2fm", hit[0].y);
|
||||
}
|
||||
}
|
||||
|
||||
debugHudText.setText(String.format(java.util.Locale.US,
|
||||
"Höhen bei X=%.1f Z=%.1f\n" +
|
||||
" Terrain (grau): %.2fm\n" +
|
||||
" Voxel roh (cyan): %.2fm%s",
|
||||
wx, wz, th, vh, bakedLine));
|
||||
debugHudText.setLocalTranslation(8, cam.getHeight() - 8, 0);
|
||||
|
||||
// ── Projizierte Zahlenlabels ─────────────────────────────────────────
|
||||
if (debugLabelXZ != null) {
|
||||
int ln = debugLabelsTerrain.size();
|
||||
for (int i = 0; i < ln; i++) {
|
||||
float lx = debugLabelXZ[i * 2];
|
||||
float lz = debugLabelXZ[i * 2 + 1];
|
||||
projectLabel(debugLabelsTerrain.get(i), lx, debugLabelTH[i], lz,
|
||||
String.format(java.util.Locale.US, "%.1f", debugLabelTH[i]));
|
||||
float lvh = debugLabelVH[i];
|
||||
if (!Float.isNaN(lvh)) {
|
||||
projectLabel(debugLabelsVoxel.get(i), lx, lvh, lz,
|
||||
String.format(java.util.Locale.US, "%.1f", lvh));
|
||||
} else {
|
||||
debugLabelsVoxel.get(i).setCullHint(com.jme3.scene.Spatial.CullHint.Always);
|
||||
}
|
||||
float lbh = debugLabelBH[i];
|
||||
if (!Float.isNaN(lbh)) {
|
||||
projectLabel(debugLabelsBaked.get(i), lx, lbh, lz,
|
||||
String.format(java.util.Locale.US, "%.1f", lbh));
|
||||
} else {
|
||||
debugLabelsBaked.get(i).setCullHint(com.jme3.scene.Spatial.CullHint.Always);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mauszeiger-Höhe → SharedInput (echter Raycast gegen alle Geometrien) ──
|
||||
float msx = input.mouseScreenX;
|
||||
if (msx >= 0f) {
|
||||
float msy = cam.getHeight() - input.mouseScreenY;
|
||||
Hit mouseHit = raycastHit(msx, msy);
|
||||
input.mouseOverlayHeight = (mouseHit != null && mouseHit.pos != null)
|
||||
? mouseHit.pos.y : Float.NaN;
|
||||
} else {
|
||||
input.mouseOverlayHeight = Float.NaN;
|
||||
}
|
||||
}
|
||||
|
||||
private void projectLabel(com.jme3.font.BitmapText bt,
|
||||
float wx, float wy, float wz, String text) {
|
||||
Vector3f screen = cam.getScreenCoordinates(new Vector3f(wx, wy + 0.3f, wz));
|
||||
boolean onScreen = screen.z > 0f && screen.z < 1f
|
||||
&& screen.x > 0 && screen.x < cam.getWidth()
|
||||
&& screen.y > 10 && screen.y < cam.getHeight();
|
||||
if (onScreen) {
|
||||
bt.setText(text);
|
||||
bt.setLocalTranslation(screen.x, screen.y, 0);
|
||||
bt.setCullHint(com.jme3.scene.Spatial.CullHint.Never);
|
||||
} else {
|
||||
bt.setCullHint(com.jme3.scene.Spatial.CullHint.Always);
|
||||
}
|
||||
}
|
||||
|
||||
private void bakeAll(List<VoxelChunk> toProcess) {
|
||||
saveAll();
|
||||
List<VoxelChunk> nonEmpty = new java.util.ArrayList<>();
|
||||
@@ -1595,18 +1912,22 @@ public class VoxelEditorState extends BaseAppState {
|
||||
// ── Schritt 1: Höhenfeld ──────────────────────────────────────────
|
||||
// Key-Schema: (cx*C + bx + 30000) * 60001 + (cz*C + bz + 30000)
|
||||
// Überlapp-Voxel (bx = CELLS) werden ausgelassen.
|
||||
Map<Long, Integer> hfMap = new HashMap<>();
|
||||
Map<Long, Float> hfMap = new HashMap<>();
|
||||
for (VoxelChunk c : nonEmpty) {
|
||||
long k = chunkKey(c.cx, c.cy, c.cz);
|
||||
float[] buf = curBufs.get(k);
|
||||
for (int bz = 0; bz < C; bz++) {
|
||||
for (int bx = 0; bx < C; bx++) {
|
||||
for (int by = blurN - 1; by >= 0; by--) {
|
||||
if (buf[c.idx(bx, by, bz)] > 0) {
|
||||
int wiy = c.cy * C + by;
|
||||
float d0 = buf[c.idx(bx, by, bz)];
|
||||
if (d0 > 0) {
|
||||
float d1 = (by + 1 < blurN) ? buf[c.idx(bx, by + 1, bz)] : -1f;
|
||||
float t = (Math.abs(d1 - d0) < 0.001f) ? 0.5f
|
||||
: Math.max(0f, Math.min(1f, -d0 / (d1 - d0)));
|
||||
float wfy = c.cy * C + by + t;
|
||||
long hk = (long)(c.cx * C + bx + 30000) * 60001L
|
||||
+ (c.cz * C + bz + 30000);
|
||||
hfMap.merge(hk, wiy, Math::max);
|
||||
hfMap.merge(hk, wfy, Math::max);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1631,11 +1952,16 @@ public class VoxelEditorState extends BaseAppState {
|
||||
for (int bz = 0; bz < C; bz++) {
|
||||
for (int bx = 0; bx < C; bx++) {
|
||||
for (int by = VoxelChunk.SIZE - 1; by >= 0; by--) {
|
||||
if (nc.getDensity(bx, by, bz) > 0) {
|
||||
int wiy = ncy * C + by;
|
||||
float d0 = nc.getDensity(bx, by, bz);
|
||||
if (d0 > 0) {
|
||||
float d1 = (by + 1 < VoxelChunk.SIZE)
|
||||
? nc.getDensity(bx, by + 1, bz) : -1f;
|
||||
float t = (Math.abs(d1 - d0) < 0.001f) ? 0.5f
|
||||
: Math.max(0f, Math.min(1f, -d0 / (d1 - d0)));
|
||||
float wfy = ncy * C + by + t;
|
||||
long hk = (long)(ncx * C + bx + 30000) * 60001L
|
||||
+ (ncz * C + bz + 30000);
|
||||
hfMap.merge(hk, wiy, Math::max);
|
||||
hfMap.merge(hk, wfy, Math::max);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1690,9 +2016,9 @@ public class VoxelEditorState extends BaseAppState {
|
||||
// kleinsten Höhe > h0 bzw. größten Höhe < h0 wählen. Betraf 17 von 10002 Spalten
|
||||
// in den Test-Chunks, max. Abweichung 1,0 (diag_realwall*.jsh im scratchpad).
|
||||
Map<Long, Float> smoothMap = new HashMap<>(hfMap.size());
|
||||
for (Map.Entry<Long, Integer> entry : hfMap.entrySet()) {
|
||||
long hk0 = entry.getKey();
|
||||
int h0 = entry.getValue();
|
||||
for (Map.Entry<Long, Float> entry : hfMap.entrySet()) {
|
||||
long hk0 = entry.getKey();
|
||||
float h0 = entry.getValue();
|
||||
int colZ = (int)(hk0 % 60001L) - 30000;
|
||||
int colX = (int)(hk0 / 60001L) - 30000;
|
||||
|
||||
@@ -1706,15 +2032,15 @@ public class VoxelEditorState extends BaseAppState {
|
||||
// mit der KLEINSTEN Höhe > h0 (bzw. GRÖSSTEN Höhe < h0) wählen, also den
|
||||
// nächstgelegenen tatsächlichen Stufen-Nachbarn, nicht irgendeinen weiter
|
||||
// entfernten. Validiert gegen reale Chunk-Daten (diag_realwall*.jsh).
|
||||
Integer hUp = null; int dUp = -1;
|
||||
Integer hDown = null; int dDownRaw = -1;
|
||||
Float hUp = null; int dUp = -1;
|
||||
Float hDown = null; int dDownRaw = -1;
|
||||
for (int r = 1; r <= RADIUS && (hUp == null || hDown == null); r++) {
|
||||
Integer bestUpThisRing = null, bestDownThisRing = null;
|
||||
Float bestUpThisRing = null, bestDownThisRing = null;
|
||||
for (int dz = -r; dz <= r; dz++) {
|
||||
for (int dx = -r; dx <= r; dx++) {
|
||||
if (Math.max(Math.abs(dx), Math.abs(dz)) != r) continue;
|
||||
long hkN = (long)(colX + dx + 30000) * 60001L + (colZ + dz + 30000);
|
||||
Integer hN = hfMap.get(hkN);
|
||||
Float hN = hfMap.get(hkN);
|
||||
if (hN == null) continue;
|
||||
if (hN > h0 && (bestUpThisRing == null || hN < bestUpThisRing)) bestUpThisRing = hN;
|
||||
if (hN < h0 && (bestDownThisRing == null || hN > bestDownThisRing)) bestDownThisRing = hN;
|
||||
@@ -1739,10 +2065,15 @@ public class VoxelEditorState extends BaseAppState {
|
||||
// Rand hin auslaufende Rampe über die vollen RADIUS Meter, statt eines
|
||||
// abrupten Sprungs zwischen "geramped" und "unverändert flach".
|
||||
float smoothH;
|
||||
if (hUp == null) {
|
||||
smoothH = h0; // Plateau: kein höherer Nachbar -> bewusst flach (Originalregel)
|
||||
if (hUp == null || hDown == null) {
|
||||
// Plateau (kein höherer Nachbar) ODER unterste/äußerste Ebene
|
||||
// (kein niedrigerer Voxel-Nachbar): Höhe unverändert lassen.
|
||||
// Die Formel darf hier NICHT angewendet werden – sie würde die
|
||||
// unterste Terrasse künstlich in Richtung hUp anheben und das
|
||||
// gebackene Terrain systematisch zu hoch machen.
|
||||
smoothH = h0;
|
||||
} else {
|
||||
float dDown = (hDown != null) ? (dDownRaw - 1) : (RADIUS - 1);
|
||||
float dDown = dDownRaw - 1;
|
||||
smoothH = (dDown <= 0f) ? h0 : (h0 * dUp + hUp * dDown) / (dUp + dDown);
|
||||
}
|
||||
smoothMap.put(hk0, smoothH);
|
||||
@@ -1780,89 +2111,80 @@ public class VoxelEditorState extends BaseAppState {
|
||||
// Fix v6 (aktuell): Steigungs-Fortsetzung komplett gestrichen. Reiner Distanz-Blend
|
||||
// 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.
|
||||
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;
|
||||
|
||||
// Kandidaten sammeln: alle leeren Zellen innerhalb RADIUS einer Rand-Spalte.
|
||||
java.util.Set<Long> candidates = new java.util.HashSet<>();
|
||||
for (long hk0 : hfMap.keySet()) {
|
||||
int colZ = (int)(hk0 % 60001L) - 30000;
|
||||
int colX = (int)(hk0 / 60001L) - 30000;
|
||||
for (int dz = -RADIUS; dz <= RADIUS; dz++) {
|
||||
for (int dx = -RADIUS; dx <= RADIUS; dx++) {
|
||||
if (Math.max(Math.abs(dx), Math.abs(dz)) > RADIUS) continue;
|
||||
long hkC = (long)(colX + dx + 30000) * 60001L + (colZ + dz + 30000);
|
||||
if (!hfMap.containsKey(hkC)) candidates.add(hkC);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Terrain-Cache: vermeidet wiederholte terrainH()-Aufrufe bei Überlappungen
|
||||
Map<Long, Float> terrainCache2b = new HashMap<>();
|
||||
|
||||
Map<Long, Float> extensionMap = new HashMap<>();
|
||||
int extCount = 0, extLogged = 0;
|
||||
for (long hkC : candidates) {
|
||||
int colZ = (int)(hkC % 60001L) - 30000;
|
||||
int colX = (int)(hkC / 60001L) - 30000;
|
||||
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;
|
||||
|
||||
// Nächsten Ring mit Struktur-Zellen isotrop suchen (gleiches Vorgehen wie
|
||||
// Schritt 2), dann ALLE Struktur-Zellen in diesem Ring mitteln – nicht nur die
|
||||
// erste gefundene. Grund (Log-Befund): benachbarte Zielzellen fanden oft
|
||||
// unterschiedliche einzelne "nächste" Zellen mit stark abweichender Höhe
|
||||
// (edgeH schwankte 3,9 bis 5,33 nur 4 Zellen auseinander) → wildes Springen.
|
||||
// Mittelung über den ganzen Ring glättet das.
|
||||
int nearestDist = -1;
|
||||
java.util.List<long[]> ringHits = new java.util.ArrayList<>(); // {colX,colZ}
|
||||
outer:
|
||||
for (int r = 1; r <= RADIUS; r++) {
|
||||
for (int dz = -r; dz <= r; dz++) {
|
||||
for (int dx = -r; dx <= r; dx++) {
|
||||
if (Math.max(Math.abs(dx), Math.abs(dz)) != r) continue;
|
||||
long hkN = (long)(colX + dx + 30000) * 60001L + (colZ + dz + 30000);
|
||||
if (hfMap.containsKey(hkN)) ringHits.add(new long[]{colX + dx, colZ + dz});
|
||||
// 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 (!ringHits.isEmpty()) { nearestDist = r; break outer; }
|
||||
}
|
||||
if (nearestDist < 0) continue; // außerhalb der Reichweite, bleibt Klippe
|
||||
|
||||
float sumEdgeH = 0f; int hitCnt = 0;
|
||||
for (long[] hit : ringHits) {
|
||||
int hColX = (int) hit[0], hColZ = (int) hit[1];
|
||||
long hKey = (long)(hColX + 30000) * 60001L + (hColZ + 30000);
|
||||
Float hH = smoothMap.get(hKey);
|
||||
if (hH == null) continue;
|
||||
sumEdgeH += hH; hitCnt++;
|
||||
}
|
||||
if (hitCnt == 0) continue;
|
||||
float edgeH = sumEdgeH / hitCnt;
|
||||
if (lowDist < 0) continue; // kein niedrigerer Nachbar im Suchradius
|
||||
|
||||
// KEINE Steigungs-Extrapolation mehr (v4/v5 hatten das versucht – eine an nur
|
||||
// wenigen Pixeln gemessene Steigung ist zu verrauscht, verstärkt sich über die
|
||||
// Distanz und erzeugte ein welliges/oszillierendes Band über eigentlich flachen
|
||||
// Flächen, siehe Screenshot 22-43-57). Reiner, robuster Distanz-Blend von der
|
||||
// (gemittelten) Kanten-Höhe zur tatsächlichen Basisterrain-Höhe: nah am Rand
|
||||
// dominiert die Kante, weiter draußen die Terrainhöhe. Folgt nicht mehr exakt
|
||||
// dem Winkel der letzten Rampe, ist aber garantiert monoton und ohne Rauschen.
|
||||
float wx = colX - 2048f, wz = colZ - 2048f;
|
||||
float th = terrainH(wx, wz);
|
||||
float finalH = edgeH;
|
||||
if (Float.isFinite(th)) {
|
||||
float t = nearestDist / (float) RADIUS; // 0 am Rand, 1 bei RADIUS
|
||||
finalH = edgeH * (1f - t) + th * t;
|
||||
float avgLowH = sumLowH / lowCount;
|
||||
float rampH = avgLowH + lowDist / RAMP_RATIO;
|
||||
if (rampH < h0sm) {
|
||||
smoothMap.put(hk0, rampH);
|
||||
}
|
||||
}
|
||||
extensionMap.put(hkC, finalH);
|
||||
extCount++;
|
||||
if (extLogged < 20) {
|
||||
log.info("Perimeter-Rampe: colX={} colZ={} dist={} edgeH={} th={} finalH={}",
|
||||
colX, colZ, nearestDist, edgeH, th, finalH);
|
||||
extLogged++;
|
||||
}
|
||||
}
|
||||
log.info("Perimeter-Übergang: {} Erweiterungs-Spalten jenseits des Strukturrands erzeugt.", extCount);
|
||||
// Erweiterungs-Spalten in hfMap/smoothMap einspeisen, damit Schritt 3 sie mitschreibt.
|
||||
for (Map.Entry<Long, Float> e : extensionMap.entrySet()) {
|
||||
long hk = e.getKey();
|
||||
if (hfMap.containsKey(hk)) continue;
|
||||
hfMap.put(hk, Math.round(e.getValue()));
|
||||
smoothMap.put(hk, e.getValue());
|
||||
}
|
||||
|
||||
// ── Schritt 3: Dichte anpassen ────────────────────────────────────
|
||||
@@ -1889,7 +2211,7 @@ public class VoxelEditorState extends BaseAppState {
|
||||
for (int bx = 0; bx < C; bx++) {
|
||||
int colX = c.cx * C + bx;
|
||||
long hk0 = (long)(colX + 30000) * 60001L + (colZ + 30000);
|
||||
Integer h0 = hfMap.get(hk0);
|
||||
Float h0 = hfMap.get(hk0);
|
||||
Float smoothH = smoothMap.get(hk0);
|
||||
if (h0 == null || smoothH == null) continue;
|
||||
|
||||
|
||||
@@ -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 plateauHeight = new ToolParameter("Plateau-Höhe", 0.0, -500.0, 500.0);
|
||||
public volatile boolean plateauHeightChanged = false;
|
||||
|
||||
@@ -47,9 +47,9 @@ public class VoxelTool extends EditorTool {
|
||||
public volatile boolean modeChanged = 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 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;
|
||||
|
||||
@Override public String getName() { return "Voxel"; }
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user