diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index b87334a..17091d6 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -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"); @@ -5673,6 +5688,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 +8217,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 +8259,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; diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.java b/blight-editor/src/main/java/de/blight/editor/SharedInput.java index 2065b9b..51c0de5 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -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). diff --git a/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java index 1506bf4..6d34b9f 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java @@ -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 debugLabelsTerrain = new java.util.ArrayList<>(); + private java.util.List debugLabelsVoxel = new java.util.ArrayList<>(); + private java.util.List 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; @@ -1112,6 +1151,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 +1255,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; } } } @@ -1490,6 +1560,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 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 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 toProcess) { saveAll(); List nonEmpty = new java.util.ArrayList<>(); @@ -1595,18 +1894,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 hfMap = new HashMap<>(); + Map 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 +1934,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 +1998,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 smoothMap = new HashMap<>(hfMap.size()); - for (Map.Entry entry : hfMap.entrySet()) { - long hk0 = entry.getKey(); - int h0 = entry.getValue(); + for (Map.Entry 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 +2014,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; @@ -1740,9 +2048,13 @@ public class VoxelEditorState extends BaseAppState { // abrupten Sprungs zwischen "geramped" und "unverändert flach". float smoothH; if (hUp == null) { - smoothH = h0; // Plateau: kein höherer Nachbar -> bewusst flach (Originalregel) + smoothH = h0; // Plateau + } else if (hDown == null) { + // Kein niedrigerer Voxel-Nachbar: obere Rampe zum Rand hin auslaufen lassen. + float dDown = RADIUS - 1; + smoothH = (h0 * dUp + hUp * dDown) / (dUp + dDown); } 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); @@ -1856,14 +2168,15 @@ public class VoxelEditorState extends BaseAppState { 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 e : extensionMap.entrySet()) { + log.info("Perimeter-Übergang: {} Erweiterungs-Spalten berechnet (aktuell deaktiviert – saubere Kante).", extCount); + // Erweiterungs-Spalten DEAKTIVIERT: liefert saubere Kante an der Voxel-Grenze + // statt der problematischen flachen Rampe. Zum Reaktivieren diesen Block einkommentieren: + /* for (Map.Entry e : extensionMap.entrySet()) { long hk = e.getKey(); if (hfMap.containsKey(hk)) continue; - hfMap.put(hk, Math.round(e.getValue())); + hfMap.put(hk, e.getValue()); smoothMap.put(hk, e.getValue()); - } + } */ // ── Schritt 3: Dichte anpassen ──────────────────────────────────── // ALLE Spalten aus hfMap bekommen den Dichte-Gradienten geschrieben, auch @@ -1889,7 +2202,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; diff --git a/blight-map/src/main/map/blight_map.blm b/blight-map/src/main/map/blight_map.blm index 7150a2d..7afd5df 100644 Binary files a/blight-map/src/main/map/blight_map.blm and b/blight-map/src/main/map/blight_map.blm differ diff --git a/blight-map/src/main/map/chunks/chunk_15_06.blc b/blight-map/src/main/map/chunks/chunk_15_06.blc index b71f657..82130e1 100644 Binary files a/blight-map/src/main/map/chunks/chunk_15_06.blc and b/blight-map/src/main/map/chunks/chunk_15_06.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_15_07.blc b/blight-map/src/main/map/chunks/chunk_15_07.blc index 322ddf3..a051d51 100644 Binary files a/blight-map/src/main/map/chunks/chunk_15_07.blc and b/blight-map/src/main/map/chunks/chunk_15_07.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_16_06.blc b/blight-map/src/main/map/chunks/chunk_16_06.blc index 2c6a397..b653ce9 100644 Binary files a/blight-map/src/main/map/chunks/chunk_16_06.blc and b/blight-map/src/main/map/chunks/chunk_16_06.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_16_07.blc b/blight-map/src/main/map/chunks/chunk_16_07.blc index 414c236..f9f93cf 100644 Binary files a/blight-map/src/main/map/chunks/chunk_16_07.blc and b/blight-map/src/main/map/chunks/chunk_16_07.blc differ diff --git a/blight-map/src/main/map/chunks/voxel_14_0_06.blvc.prebake b/blight-map/src/main/map/chunks/voxel_14_0_06.blvc.prebake index 0cf630e..de33262 100644 Binary files a/blight-map/src/main/map/chunks/voxel_14_0_06.blvc.prebake and b/blight-map/src/main/map/chunks/voxel_14_0_06.blvc.prebake differ diff --git a/blight-map/src/main/map/chunks/voxel_14_0_06_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_14_0_06_baked_lod0.j3o index 240cd19..4134da5 100644 Binary files a/blight-map/src/main/map/chunks/voxel_14_0_06_baked_lod0.j3o and b/blight-map/src/main/map/chunks/voxel_14_0_06_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_14_0_06_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_14_0_06_baked_lod1.j3o index 45bb4c5..b5fdbfa 100644 Binary files a/blight-map/src/main/map/chunks/voxel_14_0_06_baked_lod1.j3o and b/blight-map/src/main/map/chunks/voxel_14_0_06_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_14_0_06_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_14_0_06_baked_lod2.j3o index 460f2e6..2b86686 100644 Binary files a/blight-map/src/main/map/chunks/voxel_14_0_06_baked_lod2.j3o and b/blight-map/src/main/map/chunks/voxel_14_0_06_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_14_0_07.blvc.prebake b/blight-map/src/main/map/chunks/voxel_14_0_07.blvc.prebake index d7a8518..453da18 100644 Binary files a/blight-map/src/main/map/chunks/voxel_14_0_07.blvc.prebake and b/blight-map/src/main/map/chunks/voxel_14_0_07.blvc.prebake differ diff --git a/blight-map/src/main/map/chunks/voxel_14_0_07_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_14_0_07_baked_lod0.j3o index 444c545..a3cc7c1 100644 Binary files a/blight-map/src/main/map/chunks/voxel_14_0_07_baked_lod0.j3o and b/blight-map/src/main/map/chunks/voxel_14_0_07_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_14_0_07_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_14_0_07_baked_lod1.j3o index e321d27..5509da8 100644 Binary files a/blight-map/src/main/map/chunks/voxel_14_0_07_baked_lod1.j3o and b/blight-map/src/main/map/chunks/voxel_14_0_07_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_14_0_07_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_14_0_07_baked_lod2.j3o index 71bab81..b6379ed 100644 Binary files a/blight-map/src/main/map/chunks/voxel_14_0_07_baked_lod2.j3o and b/blight-map/src/main/map/chunks/voxel_14_0_07_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_06.blvc.prebake b/blight-map/src/main/map/chunks/voxel_15_0_06.blvc.prebake index 43fce7f..94f4d3d 100644 Binary files a/blight-map/src/main/map/chunks/voxel_15_0_06.blvc.prebake and b/blight-map/src/main/map/chunks/voxel_15_0_06.blvc.prebake differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_06_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_15_0_06_baked_lod0.j3o index 2ddc896..3183432 100644 Binary files a/blight-map/src/main/map/chunks/voxel_15_0_06_baked_lod0.j3o and b/blight-map/src/main/map/chunks/voxel_15_0_06_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_06_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_15_0_06_baked_lod1.j3o index df69e0f..06aa49b 100644 Binary files a/blight-map/src/main/map/chunks/voxel_15_0_06_baked_lod1.j3o and b/blight-map/src/main/map/chunks/voxel_15_0_06_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_06_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_15_0_06_baked_lod2.j3o index 26f091f..12745d9 100644 Binary files a/blight-map/src/main/map/chunks/voxel_15_0_06_baked_lod2.j3o and b/blight-map/src/main/map/chunks/voxel_15_0_06_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_07.blvc.prebake b/blight-map/src/main/map/chunks/voxel_15_0_07.blvc.prebake index 2e8a049..dcad2f5 100644 Binary files a/blight-map/src/main/map/chunks/voxel_15_0_07.blvc.prebake and b/blight-map/src/main/map/chunks/voxel_15_0_07.blvc.prebake differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_07_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_15_0_07_baked_lod0.j3o index 707273e..750320d 100644 Binary files a/blight-map/src/main/map/chunks/voxel_15_0_07_baked_lod0.j3o and b/blight-map/src/main/map/chunks/voxel_15_0_07_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_07_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_15_0_07_baked_lod1.j3o index 6ee4902..517bacb 100644 Binary files a/blight-map/src/main/map/chunks/voxel_15_0_07_baked_lod1.j3o and b/blight-map/src/main/map/chunks/voxel_15_0_07_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_07_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_15_0_07_baked_lod2.j3o index 69a2b37..333cb46 100644 Binary files a/blight-map/src/main/map/chunks/voxel_15_0_07_baked_lod2.j3o and b/blight-map/src/main/map/chunks/voxel_15_0_07_baked_lod2.j3o differ