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;
|
||||
|
||||
|
||||
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