Saubere Kante beim Voxel backen, Höhenindikator hinzugefügt
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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;
|
||||
@@ -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<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 +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<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 +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<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 +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<Long, Float> 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<Long, Float> 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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user