Weiter daran gearbeitet, die welt aus dem editor ins game zu bringen

This commit is contained in:
2026-05-23 09:07:44 +02:00
parent f9a77cc321
commit 728f506e97
36 changed files with 3728 additions and 77 deletions

View File

@@ -44,12 +44,17 @@ public class EditorApp extends Application {
static final int VP_WIDTH = 1024;
static final int VP_HEIGHT = 640;
private static final Path ASSET_ROOT = Paths.get("editor-assets");
private static final Path ASSET_ROOT = ProjectRoot.resolve("editor-assets");
private final SharedInput input = new SharedInput();
private WritableImage jfxImage;
private ImageView viewport;
private Label statusLabel;
private Label camCoordsLabel;
private HBox consoleBar;
private TextField consoleField;
private boolean consoleOpen = false;
private boolean launchGameAfterSave = false;
private VBox toolPanel;
private BorderPane root;
private VBox assetPanel;
@@ -129,7 +134,7 @@ public class EditorApp extends Application {
root.setLeft(assetPanel);
root.setCenter(worldViewport);
root.setRight(toolPanel);
root.setBottom(buildStatusBar());
root.setBottom(buildBottomBox());
Scene scene = new Scene(root, 1280, 760);
scene.setOnKeyPressed(e -> handleKeyPress(e.getCode(), true));
@@ -140,14 +145,21 @@ public class EditorApp extends Application {
stage.setScene(scene);
stage.setMinWidth(900);
stage.setMinHeight(600);
stage.setOnCloseRequest(e -> Platform.exit());
stage.setOnCloseRequest(e -> { saveCameraPrefs(); Platform.exit(); });
stage.setMaximized(true);
stage.show();
javafx.animation.Timeline statusPoller = new javafx.animation.Timeline(
new javafx.animation.KeyFrame(javafx.util.Duration.millis(200), ev -> {
String saveMsg = input.saveStatusMsg;
if (saveMsg != null) { input.saveStatusMsg = null; setStatus(saveMsg); }
if (saveMsg != null) {
input.saveStatusMsg = null;
setStatus(saveMsg);
if (launchGameAfterSave) {
launchGameAfterSave = false;
startGameProcess();
}
}
String treeMsg = input.treeGenStatusMsg;
if (treeMsg != null) { input.treeGenStatusMsg = null; setStatus(treeMsg); }
@@ -178,10 +190,31 @@ public class EditorApp extends Application {
input.objectSelectionChanged = false;
updateObjectPanel(input.selectedObjectInfo);
}
// Kamera-Koordinaten aktualisieren
camCoordsLabel.setText(String.format(
"X:%.1f Y:%.1f Z:%.1f Yaw:%.0f° Pitch:%.0f°",
input.camX, input.camY, input.camZ,
input.camYaw, input.camPitch));
// Konsolen-Antwort anzeigen
String consoleMsg = input.consoleOutput;
if (consoleMsg != null) { input.consoleOutput = null; setStatus(consoleMsg); }
})
);
statusPoller.setCycleCount(javafx.animation.Timeline.INDEFINITE);
statusPoller.play();
javafx.animation.Timeline autoSave = new javafx.animation.Timeline(
new javafx.animation.KeyFrame(javafx.util.Duration.seconds(60), ev -> {
if (!input.saveRequested) {
input.saveRequested = true;
setStatus("Auto-Speicherung…");
}
})
);
autoSave.setCycleCount(javafx.animation.Timeline.INDEFINITE);
autoSave.play();
}
// ── Modus-Wechsel ────────────────────────────────────────────────────────
@@ -246,7 +279,7 @@ public class EditorApp extends Application {
ToolBar toolBar = new ToolBar();
ToggleButton baseBtn = new ToggleButton("▲▼ Basis-Terrain");
ToggleButton upperBtn = new ToggleButton("Obere Schicht");
ToggleButton upperBtn = new ToggleButton("Gebirge");
ToggleButton holesBtn = new ToggleButton("⬤ Höhlen/Löcher");
ToggleButton grassBtn = new ToggleButton("🌿 Gras");
ToggleButton textureBtn = new ToggleButton("🎨 Textur");
@@ -305,17 +338,28 @@ public class EditorApp extends Application {
root.setRight(buildObjectEditPanel());
});
CheckBox visibleCB = new CheckBox("Obere Schicht sichtbar");
CheckBox visibleCB = new CheckBox("Gebirge sichtbar");
visibleCB.setSelected(true);
visibleCB.setOnAction(e -> input.upperLayerVisible = visibleCB.isSelected());
Label hint = new Label("WASD/QE: Kamera | Mitte-Drag / L+R-Drag: Drehen | L-Klick: hoch | R-Klick: tief");
hint.setStyle("-fx-text-fill: #555;");
Region toolbarSpacer = new Region();
HBox.setHgrow(toolbarSpacer, Priority.ALWAYS);
Button playBtn = new Button("▶ Spielen");
playBtn.setStyle(
"-fx-background-color: #2d8a3e; -fx-text-fill: white; " +
"-fx-font-weight: bold; -fx-padding: 4 12 4 12;");
playBtn.setOnAction(e -> launchGame());
toolBar.getItems().addAll(baseBtn, upperBtn, holesBtn, grassBtn, textureBtn,
new Separator(Orientation.VERTICAL), objPlaceBtn, objEditBtn,
new Separator(Orientation.VERTICAL), visibleCB,
new Separator(Orientation.VERTICAL), hint);
new Separator(Orientation.VERTICAL), hint,
toolbarSpacer,
new Separator(Orientation.VERTICAL), playBtn);
worldToolBar = toolBar;
return new VBox(menuBar, toolBar);
@@ -1819,13 +1863,118 @@ public class EditorApp extends Application {
// ── Statusleiste ─────────────────────────────────────────────────────────
private HBox buildStatusBar() {
private VBox buildBottomBox() {
// Status-Leiste
statusLabel = new Label("Bereit | Werkzeug: Höhe | WASD/QE: Bewegen | Mitte-Drag / L+R-Drag: Drehen");
statusLabel.setPadding(new Insets(3, 8, 3, 8));
statusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #333;");
HBox bar = new HBox(statusLabel);
bar.setStyle("-fx-background-color: #e8e8e8; -fx-border-color: #bbb; -fx-border-width: 1 0 0 0;");
return bar;
camCoordsLabel = new Label("X:0.0 Y:0.0 Z:0.0 Yaw:0° Pitch:0°");
camCoordsLabel.setPadding(new Insets(3, 8, 3, 8));
camCoordsLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555; -fx-font-family: monospace;");
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
HBox statusBar = new HBox(statusLabel, spacer, camCoordsLabel);
statusBar.setStyle("-fx-background-color: #e8e8e8; -fx-border-color: #bbb; -fx-border-width: 1 0 0 0;");
// Konsolen-Panel (anfangs ausgeblendet)
Label prompt = new Label(">");
prompt.setStyle("-fx-text-fill: #7ec8e3; -fx-font-family: monospace; -fx-font-size: 12; -fx-padding: 0 4 0 0;");
consoleField = new TextField();
consoleField.setStyle(
"-fx-background-color: transparent; -fx-text-fill: #f0f0f0; " +
"-fx-font-family: monospace; -fx-font-size: 12; -fx-border-width: 0;");
consoleField.setPromptText("Befehl eingeben… (Enter = ausführen, Esc = schließen)");
HBox.setHgrow(consoleField, Priority.ALWAYS);
consoleField.setOnAction(e -> {
String cmd = consoleField.getText().trim();
if (!cmd.isEmpty()) input.pendingCommand = cmd;
toggleConsole();
});
consoleField.setOnKeyPressed(e -> {
if (e.getCode() == KeyCode.ESCAPE
|| e.getCode() == KeyCode.DEAD_CIRCUMFLEX
|| e.getCode() == KeyCode.CIRCUMFLEX) {
toggleConsole();
e.consume();
}
});
consoleBar = new HBox(4, prompt, consoleField);
consoleBar.setStyle(
"-fx-background-color: #1e1e1e; -fx-padding: 3 8 3 8; " +
"-fx-border-color: #555; -fx-border-width: 1 0 0 0;");
consoleBar.setVisible(false);
consoleBar.setManaged(false);
return new VBox(statusBar, consoleBar);
}
private void toggleConsole() {
boolean show = !consoleOpen;
consoleOpen = show;
consoleBar.setVisible(show);
consoleBar.setManaged(show);
if (show) {
// Bewegungstasten loslassen, damit keine Dauerbewegung entsteht
input.forward = input.backward = input.left = input.right = input.up = input.down = false;
consoleField.clear();
consoleField.requestFocus();
}
}
private void launchGame() {
if (launchGameAfterSave) return; // bereits ausstehend
launchGameAfterSave = true;
input.saveRequested = true;
setStatus("Karte wird gespeichert, Spiel startet…");
}
private void startGameProcess() {
new Thread(() -> {
try {
String javaExe = Paths.get(System.getProperty("java.home"), "bin", "java").toString();
String classpath = System.getProperty("java.class.path");
String libPath = System.getProperty("java.library.path", "");
String projRoot = ProjectRoot.PATH.toString();
new ProcessBuilder(
javaExe,
"--add-opens", "java.base/java.lang=ALL-UNNAMED",
"--add-opens", "java.desktop/sun.awt=ALL-UNNAMED",
"-Djava.library.path=" + libPath,
"-Dblight.project.root=" + projRoot,
"-cp", classpath,
"de.blight.game.BlightApp")
.directory(ProjectRoot.PATH.toFile())
.inheritIO()
.start();
Platform.runLater(() -> setStatus("Spiel gestartet"));
} catch (IOException ex) {
Platform.runLater(() -> setStatus("Spielstart fehlgeschlagen: " + ex.getMessage()));
}
}, "game-launcher").start();
}
private void saveCameraPrefs() {
java.util.Properties p = new java.util.Properties();
p.setProperty("cam.x", String.valueOf(input.camX));
p.setProperty("cam.y", String.valueOf(input.camY));
p.setProperty("cam.z", String.valueOf(input.camZ));
p.setProperty("cam.yaw", String.valueOf(input.camYaw));
p.setProperty("cam.pitch", String.valueOf(input.camPitch));
try {
Files.createDirectories(ProjectRoot.resolve("config"));
try (java.io.Writer w = Files.newBufferedWriter(ProjectRoot.resolve("config", "editor.prefs"))) {
p.store(w, "Blight Editor Kamera-Einstellungen");
}
} catch (IOException e) {
System.err.println("[Editor] Kamera-Prefs konnten nicht gespeichert werden: " + e.getMessage());
}
}
private void setStatus(String msg) {
@@ -1835,6 +1984,13 @@ public class EditorApp extends Application {
// ── Tastatur-Handling ────────────────────────────────────────────────────
private void handleKeyPress(KeyCode code, boolean pressed) {
// ^ öffnet/schließt die Konsole (DEAD_CIRCUMFLEX = deutsche Tastatur)
if (code == KeyCode.DEAD_CIRCUMFLEX || code == KeyCode.CIRCUMFLEX) {
if (pressed) toggleConsole();
return;
}
// Während die Konsole offen ist, keine Editor-Tasten weiterleiten
if (consoleOpen) return;
switch (code) {
case W -> input.forward = pressed;
case S -> input.backward = pressed;

View File

@@ -6,6 +6,10 @@ package de.blight.editor;
*/
public class EditorLauncher {
public static void main(String[] args) {
// ProjectRoot muss als erstes initialisiert werden, damit alle
// relativen Pfade korrekt aufgelöst werden (auch bei IDE-Start mit
// workingDir = blight-editor/ statt Projekt-Root).
ProjectRoot.PATH.toString(); // Trigger static init
EditorApp.main(args);
}
}

View File

@@ -57,7 +57,7 @@ public class JmeEditorApp extends SimpleApplication {
// aus diesem Verzeichnis geladen werden können (relativ zum Arbeitsverzeichnis).
try {
assetManager.registerLocator(
java.nio.file.Paths.get("editor-assets").toAbsolutePath().toString(),
ProjectRoot.resolve("editor-assets").toAbsolutePath().toString(),
FileLocator.class);
} catch (Exception ignored) {}
@@ -79,5 +79,64 @@ public class JmeEditorApp extends SimpleApplication {
}
@Override
public void simpleUpdate(float tpf) {}
public void simpleUpdate(float tpf) {
com.jme3.math.Vector3f loc = cam.getLocation();
com.jme3.math.Vector3f dir = cam.getDirection();
input.camX = loc.x;
input.camY = loc.y;
input.camZ = loc.z;
input.camYaw = (float) Math.toDegrees(Math.atan2(-dir.x, -dir.z));
input.camPitch = (float) Math.toDegrees(
Math.asin(Math.max(-1f, Math.min(1f, dir.y))));
String cmd = input.pendingCommand;
if (cmd != null) {
input.pendingCommand = null;
processCommand(cmd);
}
}
private void processCommand(String raw) {
String[] parts = raw.trim().split("\\s+");
switch (parts[0].toLowerCase()) {
case "goto" -> {
try {
if (parts.length >= 4) {
// goto x y z — direkte Koordinaten
float x = Float.parseFloat(parts[1]);
float y = Float.parseFloat(parts[2]);
float z = Float.parseFloat(parts[3]);
cam.setLocation(new com.jme3.math.Vector3f(x, y, z));
input.consoleOutput = "Goto → " + x + " / " + y + " / " + z;
} else if (parts.length >= 3) {
// goto x z — Bodenabstand beibehalten
float x = Float.parseFloat(parts[1]);
float z = Float.parseFloat(parts[2]);
TerrainEditorState tes =
stateManager.getState(TerrainEditorState.class);
float srcGround = tes != null
? tes.getTerrainHeightAt(cam.getLocation().x,
cam.getLocation().z)
: 0f;
float heightAboveGround = cam.getLocation().y - srcGround;
float dstGround = tes != null
? tes.getTerrainHeightAt(x, z)
: 0f;
float y = dstGround + heightAboveGround;
cam.setLocation(new com.jme3.math.Vector3f(x, y, z));
input.consoleOutput = "Goto → X=" + x + " Z=" + z
+ " (Y=" + String.format("%.1f", y) + ")";
} else {
input.consoleOutput = "Syntax: goto <x> <z> oder goto <x> <y> <z>";
}
} catch (NumberFormatException e) {
input.consoleOutput = "Fehler: Koordinaten müssen Zahlen sein";
}
}
case "help" -> input.consoleOutput =
"Befehle: goto <x> <z> | goto <x> <y> <z> | help";
default ->
input.consoleOutput = "Unbekannter Befehl: " + parts[0];
}
}
}

View File

@@ -0,0 +1,41 @@
package de.blight.editor;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Ermittelt einmalig den Projekt-Root (das Verzeichnis, das gradlew enthält)
* und stellt ihn als kanonischen Basispfad bereit.
*
* Muss so früh wie möglich aufgerufen werden (EditorLauncher.main),
* damit alle relativen Pfade korrekt aufgelöst werden — auch wenn der Editor
* aus einer IDE mit workingDir=blight-editor/ gestartet wird.
*/
public final class ProjectRoot {
public static final Path PATH = findRoot();
private ProjectRoot() {}
public static Path resolve(String first, String... more) {
return PATH.resolve(Paths.get(first, more));
}
private static Path findRoot() {
// blight-editor/ UND blight-game/ als Kinder → eindeutig der echte Root
File dir = Paths.get(".").toAbsolutePath().normalize().toFile();
while (dir != null) {
if (new File(dir, "blight-editor").isDirectory()
&& new File(dir, "blight-game").isDirectory()) {
Path root = dir.toPath();
System.setProperty("blight.project.root", root.toString());
return root;
}
dir = dir.getParentFile();
}
Path fallback = Paths.get(".").toAbsolutePath().normalize();
System.setProperty("blight.project.root", fallback.toString());
return fallback;
}
}

View File

@@ -24,7 +24,7 @@ public class SharedInput {
public final TextureTool textureTool = new TextureTool();
public volatile EditorTool activeTool = heightTool;
// ── Aktive Ebene: 0=Basis-Terrain, 1=Obere Schicht, 2=Höhlen, 3=Gras, 4=Textur ──
// ── Aktive Ebene: 0=Basis-Terrain, 1=Gebirge, 2=Höhlen, 3=Gras, 4=Textur ──
public volatile int activeLayer = 0;
public volatile boolean upperLayerVisible = true;
@@ -152,6 +152,19 @@ public class SharedInput {
public final ConcurrentLinkedQueue<MeshCreateRequest> meshCreateQueue =
new ConcurrentLinkedQueue<>();
// ── Kamera-Info (JME3-Thread schreibt, JavaFX-Thread liest) ─────────────
public volatile float camX = 0f, camY = 0f, camZ = 0f;
/** Yaw in Grad: 0° = Süden (Z), 90° = Westen (X), ±180° = Norden (+Z). */
public volatile float camYaw = 0f;
/** Pitch in Grad: positiv = Blick nach oben, negativ = nach unten. */
public volatile float camPitch = 0f;
// ── Konsole (JavaFX → JME3 und zurück) ──────────────────────────────────
/** Befehl, der beim nächsten JME3-Update ausgeführt werden soll. */
public volatile String pendingCommand = null;
/** Antworttext, den JME3 nach der Befehlsausführung setzt. */
public volatile String consoleOutput = null;
// ── Modell-Konvertierung ──────────────────────────────────────────────────
/**
* Konvertiert ein natives Modell (OBJ/GLTF/…) zu .j3o.

View File

@@ -38,6 +38,8 @@ import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* JME3-AppState für den EZ-Tree-Generator.
@@ -50,7 +52,7 @@ import java.nio.file.Paths;
public class EzTreeState extends BaseAppState {
private static final int IMPOSTOR_SIZE = 512;
private static final Path ASSET_ROOT = Paths.get("editor-assets");
private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("editor-assets");
private final SharedInput input;
private SimpleApplication app;
@@ -155,8 +157,10 @@ public class EzTreeState extends BaseAppState {
app.getRenderer().readFrameBuffer(captureFB, pixels);
cleanupCapture();
saveImpostor(pixels, "ez_impostor_" + pendingRequest.exportName());
exportTree(pendingTreeNode, pendingRequest.exportName());
String exportName = pendingRequest.exportName() + "_"
+ DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now());
saveImpostor(pixels, "ez_impostor_" + exportName);
exportTree(pendingTreeNode, exportName);
pendingRequest = null;
pendingTreeNode = null;

View File

@@ -24,10 +24,12 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class PalmGeneratorState extends BaseAppState {
private static final Path ASSET_ROOT = Paths.get("editor-assets");
private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("editor-assets");
private final SharedInput input;
private SimpleApplication app;
@@ -188,7 +190,9 @@ public class PalmGeneratorState extends BaseAppState {
try {
Path modelDir = ASSET_ROOT.resolve("models");
Files.createDirectories(modelDir);
File out = modelDir.resolve("Palm_" + name + ".j3o").toFile();
String stampedName = name + "_"
+ DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now());
File out = modelDir.resolve("Palm_" + stampedName + ".j3o").toFile();
BinaryExporter.getInstance().save(palmNode, out);
input.treeGenStatusMsg = "Palme exportiert: " + out.getName();
input.refreshAssets = true;

View File

@@ -39,7 +39,7 @@ import java.util.List;
*/
public class SceneObjectState extends BaseAppState {
private static final Path ASSET_ROOT = Paths.get("editor-assets");
private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("editor-assets");
// ── Gizmo-Farben ─────────────────────────────────────────────────────────
private static final ColorRGBA COL_X = new ColorRGBA(0.9f, 0.1f, 0.1f, 1f);

View File

@@ -31,12 +31,18 @@ import de.blight.editor.SharedInput;
import de.blight.editor.tool.HeightTool;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
public class TerrainEditorState extends BaseAppState {
@@ -75,9 +81,13 @@ public class TerrainEditorState extends BaseAppState {
private Texture2D splatTex;
// ── Kameraposition ────────────────────────────────────────────────────────
private static final Path EDITOR_PREFS = de.blight.editor.ProjectRoot.resolve("config", "editor.prefs");
private static final float DEFAULT_CAM_Y = 50f;
private static final float DEFAULT_PITCH = (float) (-Math.PI / 4); // -45°
private float camYaw = 0f;
private float camPitch = -1.0f;
private final Vector3f camPos = new Vector3f(0f, 800f, (float)(800.0 / Math.tan(1.0)));
private float camPitch = DEFAULT_PITCH;
private final Vector3f camPos = new Vector3f(0f, DEFAULT_CAM_Y, 0f);
public TerrainEditorState(SharedInput input) {
this.input = input;
@@ -101,6 +111,28 @@ public class TerrainEditorState extends BaseAppState {
System.err.println("[TerrainEditor] Karte nicht ladbar: " + e.getMessage());
}
}
loadCameraPrefs();
}
private void loadCameraPrefs() {
if (!Files.exists(EDITOR_PREFS)) return;
Properties p = new Properties();
try (Reader r = Files.newBufferedReader(EDITOR_PREFS)) {
p.load(r);
camPos.set(
parsePref(p, "cam.x", 0f),
parsePref(p, "cam.y", DEFAULT_CAM_Y),
parsePref(p, "cam.z", 0f));
camYaw = (float) Math.toRadians(parsePref(p, "cam.yaw", 0f));
camPitch = (float) Math.toRadians(parsePref(p, "cam.pitch", (float) Math.toDegrees(DEFAULT_PITCH)));
} catch (IOException e) {
System.err.println("[TerrainEditor] Kamera-Prefs nicht ladbar: " + e.getMessage());
}
}
private static float parsePref(Properties p, String key, float def) {
try { return Float.parseFloat(p.getProperty(key, String.valueOf(def))); }
catch (NumberFormatException e) { return def; }
}
@Override
@@ -354,6 +386,13 @@ public class TerrainEditorState extends BaseAppState {
}
}
/** Gibt die Terrain-Höhe (Welt-Y) an der angegebenen Welt-XZ-Position zurück. */
public float getTerrainHeightAt(float worldX, float worldZ) {
if (terrain == null) return 0f;
Float h = terrain.getHeight(new Vector2f(worldX, worldZ));
return h != null ? h : 0f;
}
// ── Speichern ─────────────────────────────────────────────────────────────
private void performSave() {

View File

@@ -7,6 +7,8 @@ import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import javax.imageio.ImageIO;
@@ -64,7 +66,7 @@ public class TreeGeneratorState extends BaseAppState {
private static final int IMPOSTOR_SIZE = 512;
private static final int PREVIEW_SIZE = 1024;
private static final Path ASSET_ROOT = Paths.get("editor-assets");
private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("editor-assets");
private final SharedInput input;
@@ -286,7 +288,12 @@ public class TreeGeneratorState extends BaseAppState {
app.getRenderer().readFrameBuffer(captureFB, pixels);
cleanupCapture();
Texture2D impostorTex = saveImpostor(pixels, "impostor_" + pendingRequest.exportName());
String baseName = pendingRequest.exportName();
String exportName = pendingRequest.exportAfter()
? baseName + "_" + DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now())
: baseName;
Texture2D impostorTex = saveImpostor(pixels, "impostor_" + exportName);
// HD-Mesh im Dialog-Preview anzeigen (keine LOD-Umschaltung, kein Welt-Platzierung)
Node previewTree = makeTreeNode(pendingHdResult,
@@ -301,9 +308,9 @@ public class TreeGeneratorState extends BaseAppState {
if (pendingRequest.exportAfter()) {
Node treeNode = assembleLodNode(impostorTex);
exportTree(treeNode, pendingRequest.exportName());
exportTree(treeNode, exportName);
} else {
input.treeGenStatusMsg = "Vorschau: '" + pendingRequest.exportName() + "'";
input.treeGenStatusMsg = "Vorschau: '" + baseName + "'";
}
pendingRequest = null;

View File

@@ -23,8 +23,9 @@ public class UpperLayerData {
/** Whether a cell is an open hole (no geometry emitted) [CELLS*CELLS]. */
public final boolean[] hole;
/** Initiale Höhe der Gras-Oberfläche (muss mit TerrainEditorState übereinstimmen). */
public static final float INITIAL_TERRAIN_Y = 1f;
/** Initiale Höhe der Gebirgsschicht-Oberseite. Tiefer als das flache Terrain (Y=1),
* damit das Gebirge standardmäßig verdeckt ist und gezielt hochgezogen werden muss. */
public static final float INITIAL_TERRAIN_Y = -10f;
/** Dicke der Gesteinsschicht in Welteinheiten. */
public static final float LAYER_THICKNESS = 30f;

View File

@@ -17,8 +17,9 @@ import de.blight.editor.SharedInput;
import de.blight.editor.tool.HoleTool;
import de.blight.editor.tool.UpperHeightTool;
import java.util.HashSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* AppState that owns the upper (mountain) layer: 1024 chunk geometries
@@ -256,20 +257,27 @@ public class UpperLayerState extends BaseAppState {
/**
* Verschiebt top- und bottomHeight um dieselben Deltas wie das Basis-Terrain.
* Jeder obere Vertex wird dabei nur einmal angepasst, auch wenn mehrere
* Terrain-Vertices auf denselben oberen Vertex fallen.
* Da das Terrain-Grid (4097×4097) feiner ist als das Gebirge-Grid (513×513),
* fallen mehrere Terrain-Vertices auf denselben Gebirge-Vertex. Wir nehmen
* jeweils den Delta mit dem größten Absolutbetrag, damit der Gebirge-Vertex
* der stärksten Terrain-Verschiebung folgt und das Terrain nicht durchsticht.
*/
public void adjustHeightsWithTerrain(List<Vector2f> worldXZ, List<Float> deltas) {
HashSet<Integer> seen = new HashSet<>();
HashMap<Integer, Float> maxDelta = new HashMap<>();
for (int i = 0; i < worldXZ.size(); i++) {
Vector2f p = worldXZ.get(i);
int uvx = UpperLayerData.worldToVertexX(p.x);
int uvz = UpperLayerData.worldToVertexZ(p.y);
int uvx = UpperLayerData.worldToVertexX(worldXZ.get(i).x);
int uvz = UpperLayerData.worldToVertexZ(worldXZ.get(i).y);
int key = uvx + uvz * UpperLayerData.VERTS;
if (!seen.add(key)) continue;
float d = deltas.get(i);
maxDelta.merge(key, d, (a, b) -> Math.abs(b) > Math.abs(a) ? b : a);
}
for (Map.Entry<Integer, Float> e : maxDelta.entrySet()) {
int key = e.getKey();
float d = e.getValue();
data.topHeight[key] += d;
data.bottomHeight[key] += d;
int uvx = key % UpperLayerData.VERTS;
int uvz = key / UpperLayerData.VERTS;
markVertexDirty(uvx, uvz);
}
}

View File

@@ -23,7 +23,7 @@ public class UpperHeightTool extends EditorTool {
public final ToolParameter brushStrength = new ToolParameter("Pinselstärke", 2.0, 0.1, 50.0);
@Override
public String getName() { return "Obere Schicht Höhe"; }
public String getName() { return "Gebirge Höhe"; }
@Override
public List<ChoiceToolParameter> getChoiceParameters() {