Initaler Commit
This commit is contained in:
310
blight-editor/src/main/java/de/blight/editor/EditorApp.java
Normal file
310
blight-editor/src/main/java/de/blight/editor/EditorApp.java
Normal file
@@ -0,0 +1,310 @@
|
||||
package de.blight.editor;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
|
||||
public class EditorApp extends Application {
|
||||
|
||||
// ── Viewport-Auflösung (JME3 rendert intern auf diese Größe) ────────────
|
||||
static final int VP_WIDTH = 1024;
|
||||
static final int VP_HEIGHT = 640;
|
||||
|
||||
// ── Asset-Verzeichnis ────────────────────────────────────────────────────
|
||||
private static final Path ASSET_ROOT = Paths.get("editor-assets");
|
||||
|
||||
private final SharedInput input = new SharedInput();
|
||||
private WritableImage jfxImage;
|
||||
private ImageView viewport;
|
||||
private Label statusLabel;
|
||||
|
||||
// Drag-Tracking für Kamerarotation (mittlere Taste)
|
||||
private double prevDragX, prevDragY;
|
||||
|
||||
// ── Asset-Tree-Items ─────────────────────────────────────────────────────
|
||||
private TreeItem<String> modelsNode;
|
||||
private TreeItem<String> texturesNode;
|
||||
private TreeItem<String> audioNode;
|
||||
|
||||
// ── JavaFX Entry-Point ───────────────────────────────────────────────────
|
||||
|
||||
public static void main(String[] args) {
|
||||
launch(args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Stage stage) {
|
||||
// WritableImage für den JME3-Frame-Export
|
||||
jfxImage = new WritableImage(VP_WIDTH, VP_HEIGHT);
|
||||
|
||||
// JME3 starten (Hintergrund-Daemon-Thread)
|
||||
JmeEditorApp.launch(input, jfxImage, VP_WIDTH, VP_HEIGHT);
|
||||
|
||||
// UI zusammenbauen
|
||||
BorderPane root = new BorderPane();
|
||||
root.setTop(buildTop());
|
||||
root.setLeft(buildAssetPanel());
|
||||
root.setCenter(buildViewport());
|
||||
root.setBottom(buildStatusBar());
|
||||
|
||||
Scene scene = new Scene(root, 1280, 760);
|
||||
scene.setOnKeyPressed(e -> handleKeyPress(e.getCode(), true));
|
||||
scene.setOnKeyReleased(e -> handleKeyPress(e.getCode(), false));
|
||||
|
||||
stage.setTitle("Blight World Editor");
|
||||
stage.setScene(scene);
|
||||
stage.setMinWidth(900);
|
||||
stage.setMinHeight(600);
|
||||
stage.setOnCloseRequest(e -> Platform.exit());
|
||||
stage.show();
|
||||
}
|
||||
|
||||
// ── Oberer Bereich: MenuBar + ToolBar ────────────────────────────────────
|
||||
|
||||
private VBox buildTop() {
|
||||
// Menüleiste
|
||||
MenuBar menuBar = new MenuBar();
|
||||
Menu fileMenu = new Menu("Datei");
|
||||
MenuItem newItem = new MenuItem("Neue Karte");
|
||||
MenuItem saveItem = new MenuItem("Speichern");
|
||||
fileMenu.getItems().addAll(newItem, saveItem);
|
||||
Menu viewMenu = new Menu("Ansicht");
|
||||
MenuItem resetCam = new MenuItem("Kamera zurücksetzen");
|
||||
resetCam.setOnAction(e -> input.addMouseDelta(0, 0)); // noop, Kamera reset via SharedInput wäre aufwändiger
|
||||
viewMenu.getItems().add(resetCam);
|
||||
menuBar.getMenus().addAll(fileMenu, viewMenu);
|
||||
|
||||
// Werkzeugleiste
|
||||
ToolBar toolBar = new ToolBar();
|
||||
Button heightTool = new Button("▲▼ Höhe");
|
||||
heightTool.setStyle("-fx-font-weight:bold;");
|
||||
heightTool.setTooltip(new Tooltip(
|
||||
"Linksklick: Terrain anheben\nRechtsklick: Terrain absenken"));
|
||||
|
||||
Separator sep1 = new Separator(Orientation.VERTICAL);
|
||||
|
||||
Label brushLabel = new Label("Pinselstärke:");
|
||||
Slider brushSlider = new Slider(0.1, 1.0, 1.0);
|
||||
brushSlider.setPrefWidth(100);
|
||||
brushSlider.setMajorTickUnit(0.5);
|
||||
brushSlider.setShowTickMarks(true);
|
||||
|
||||
Separator sep2 = new Separator(Orientation.VERTICAL);
|
||||
|
||||
Label hint = new Label("WASD/QE: Kamera | Mitte-Drag: Drehen | L-Klick: hoch | R-Klick: tief");
|
||||
hint.setStyle("-fx-text-fill: #555;");
|
||||
|
||||
toolBar.getItems().addAll(heightTool, sep1, brushLabel, brushSlider, sep2, hint);
|
||||
|
||||
VBox top = new VBox(menuBar, toolBar);
|
||||
return top;
|
||||
}
|
||||
|
||||
// ── Linke Seite: Asset-Panel ─────────────────────────────────────────────
|
||||
|
||||
private VBox buildAssetPanel() {
|
||||
VBox panel = new VBox(6);
|
||||
panel.setPadding(new Insets(8));
|
||||
panel.setPrefWidth(210);
|
||||
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 1 0 0;");
|
||||
|
||||
Label title = new Label("Assets");
|
||||
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
|
||||
|
||||
// Baum
|
||||
TreeItem<String> root = new TreeItem<>("Projekt");
|
||||
root.setExpanded(true);
|
||||
modelsNode = new TreeItem<>("Models");
|
||||
texturesNode = new TreeItem<>("Texturen");
|
||||
audioNode = new TreeItem<>("Audio");
|
||||
root.getChildren().addAll(modelsNode, texturesNode, audioNode);
|
||||
|
||||
// Bestehende Assets laden
|
||||
loadAssetsInto(modelsNode, "models", ".j3o", ".obj", ".fbx", ".gltf", ".glb");
|
||||
loadAssetsInto(texturesNode, "textures", ".png", ".jpg", ".jpeg", ".bmp", ".tga", ".dds");
|
||||
loadAssetsInto(audioNode, "audio", ".ogg", ".wav", ".mp3");
|
||||
|
||||
TreeView<String> tree = new TreeView<>(root);
|
||||
tree.setShowRoot(false);
|
||||
VBox.setVgrow(tree, Priority.ALWAYS);
|
||||
|
||||
// Kontextmenü im Baum (Datei öffnen im Dateimanager)
|
||||
ContextMenu ctx = new ContextMenu();
|
||||
MenuItem showItem = new MenuItem("Im Dateisystem anzeigen");
|
||||
ctx.getItems().add(showItem);
|
||||
tree.setContextMenu(ctx);
|
||||
|
||||
// Import-Button
|
||||
Button importBtn = new Button("⊕ Import…");
|
||||
importBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
importBtn.setOnAction(e -> handleImport(tree.getScene().getWindow()));
|
||||
|
||||
panel.getChildren().addAll(title, tree, importBtn);
|
||||
return panel;
|
||||
}
|
||||
|
||||
private void loadAssetsInto(TreeItem<String> parent, String subDir, String... exts) {
|
||||
Path dir = ASSET_ROOT.resolve(subDir);
|
||||
if (!Files.exists(dir)) return;
|
||||
try (var stream = Files.list(dir)) {
|
||||
stream.filter(p -> {
|
||||
String name = p.getFileName().toString().toLowerCase();
|
||||
for (String ext : exts) if (name.endsWith(ext)) return true;
|
||||
return false;
|
||||
}).map(p -> new TreeItem<>(p.getFileName().toString()))
|
||||
.forEach(parent.getChildren()::add);
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
|
||||
private void handleImport(javafx.stage.Window owner) {
|
||||
FileChooser fc = new FileChooser();
|
||||
fc.setTitle("Assets importieren");
|
||||
fc.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("Alle unterstützten Dateien",
|
||||
"*.j3o","*.obj","*.fbx","*.gltf","*.glb",
|
||||
"*.png","*.jpg","*.jpeg","*.bmp","*.tga","*.dds",
|
||||
"*.ogg","*.wav","*.mp3"),
|
||||
new FileChooser.ExtensionFilter("Modelle", "*.j3o","*.obj","*.fbx","*.gltf","*.glb"),
|
||||
new FileChooser.ExtensionFilter("Texturen", "*.png","*.jpg","*.jpeg","*.bmp","*.tga","*.dds"),
|
||||
new FileChooser.ExtensionFilter("Audio", "*.ogg","*.wav","*.mp3")
|
||||
);
|
||||
var files = fc.showOpenMultipleDialog(owner);
|
||||
if (files == null) return;
|
||||
|
||||
for (File file : files) {
|
||||
String name = file.getName().toLowerCase();
|
||||
String subDir;
|
||||
TreeItem<String> parent;
|
||||
if (name.matches(".*\\.(j3o|obj|fbx|gltf|glb)")) {
|
||||
subDir = "models"; parent = modelsNode;
|
||||
} else if (name.matches(".*\\.(ogg|wav|mp3)")) {
|
||||
subDir = "audio"; parent = audioNode;
|
||||
} else {
|
||||
subDir = "textures"; parent = texturesNode;
|
||||
}
|
||||
try {
|
||||
Path dest = ASSET_ROOT.resolve(subDir);
|
||||
Files.createDirectories(dest);
|
||||
Path target = dest.resolve(file.getName());
|
||||
Files.copy(file.toPath(), target, StandardCopyOption.REPLACE_EXISTING);
|
||||
parent.getChildren().add(new TreeItem<>(file.getName()));
|
||||
parent.setExpanded(true);
|
||||
setStatus("Importiert: " + file.getName());
|
||||
} catch (IOException ex) {
|
||||
setStatus("Fehler beim Import: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Zentraler Bereich: JME3-Viewport ────────────────────────────────────
|
||||
|
||||
private StackPane buildViewport() {
|
||||
viewport = new ImageView(jfxImage);
|
||||
viewport.setPreserveRatio(false);
|
||||
viewport.setFocusTraversable(true);
|
||||
|
||||
StackPane pane = new StackPane(viewport);
|
||||
pane.setStyle("-fx-background-color: #1a1a2e;");
|
||||
|
||||
// ImageView auf Pane-Größe binden → JME3-Pixelskalierung aktualisieren
|
||||
pane.widthProperty().addListener((o, oldW, newW) -> {
|
||||
viewport.setFitWidth(newW.doubleValue());
|
||||
input.viewportScaleX = VP_WIDTH / newW.doubleValue();
|
||||
});
|
||||
pane.heightProperty().addListener((o, oldH, newH) -> {
|
||||
viewport.setFitHeight(newH.doubleValue());
|
||||
input.viewportScaleY = VP_HEIGHT / newH.doubleValue();
|
||||
});
|
||||
|
||||
// ── Maus-Events ──────────────────────────────────────────────────────
|
||||
viewport.setOnMousePressed(e -> {
|
||||
viewport.requestFocus();
|
||||
if (e.getButton() == MouseButton.MIDDLE) {
|
||||
prevDragX = e.getX();
|
||||
prevDragY = e.getY();
|
||||
}
|
||||
if (e.getButton() == MouseButton.PRIMARY) {
|
||||
submitEdit(e.getX(), e.getY(), +1);
|
||||
}
|
||||
if (e.getButton() == MouseButton.SECONDARY) {
|
||||
submitEdit(e.getX(), e.getY(), -1);
|
||||
}
|
||||
});
|
||||
|
||||
viewport.setOnMouseDragged(e -> {
|
||||
if (e.isMiddleButtonDown()) {
|
||||
double dx = e.getX() - prevDragX;
|
||||
double dy = e.getY() - prevDragY;
|
||||
input.addMouseDelta((int) dx, (int) dy);
|
||||
prevDragX = e.getX();
|
||||
prevDragY = e.getY();
|
||||
}
|
||||
if (e.isPrimaryButtonDown()) {
|
||||
submitEdit(e.getX(), e.getY(), +1);
|
||||
}
|
||||
if (e.isSecondaryButtonDown()) {
|
||||
submitEdit(e.getX(), e.getY(), -1);
|
||||
}
|
||||
});
|
||||
|
||||
viewport.setOnScroll(e -> {
|
||||
// Scrollen = Kamera vorwärts/rückwärts
|
||||
double delta = e.getDeltaY();
|
||||
input.forward = delta > 0;
|
||||
input.backward = delta < 0;
|
||||
// Nach kurzem Delay zurücksetzen (kein physischer Key-Release)
|
||||
javafx.animation.PauseTransition pause =
|
||||
new javafx.animation.PauseTransition(javafx.util.Duration.millis(150));
|
||||
pause.setOnFinished(ev -> { input.forward = false; input.backward = false; });
|
||||
pause.play();
|
||||
});
|
||||
|
||||
return pane;
|
||||
}
|
||||
|
||||
private void submitEdit(double x, double y, int action) {
|
||||
input.editQueue.offer(new SharedInput.TerrainEdit((float) x, (float) y, action));
|
||||
}
|
||||
|
||||
// ── Statusleiste ─────────────────────────────────────────────────────────
|
||||
|
||||
private HBox buildStatusBar() {
|
||||
statusLabel = new Label("Bereit | Werkzeug: Höhe | WASD/QE: Bewegen | Mitte-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;
|
||||
}
|
||||
|
||||
private void setStatus(String msg) {
|
||||
Platform.runLater(() -> statusLabel.setText(msg));
|
||||
}
|
||||
|
||||
// ── Tastatur-Handling ────────────────────────────────────────────────────
|
||||
|
||||
private void handleKeyPress(KeyCode code, boolean pressed) {
|
||||
switch (code) {
|
||||
case W -> input.forward = pressed;
|
||||
case S -> input.backward = pressed;
|
||||
case A -> input.left = pressed;
|
||||
case D -> input.right = pressed;
|
||||
case Q -> input.up = pressed;
|
||||
case E -> input.down = pressed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.blight.editor;
|
||||
|
||||
/**
|
||||
* Separater Launcher-Einstiegspunkt, damit der JavaFX-Klassenpfad korrekt
|
||||
* aufgelöst wird, bevor Application.launch() aufgerufen wird.
|
||||
*/
|
||||
public class EditorLauncher {
|
||||
public static void main(String[] args) {
|
||||
EditorApp.main(args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package de.blight.editor;
|
||||
|
||||
import com.jme3.post.SceneProcessor;
|
||||
import com.jme3.profile.AppProfiler;
|
||||
import com.jme3.renderer.RenderManager;
|
||||
import com.jme3.renderer.Renderer;
|
||||
import com.jme3.renderer.ViewPort;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.texture.FrameBuffer;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.image.PixelFormat;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import javafx.scene.image.WritableImage;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* SceneProcessor: liest nach jedem JME3-Frame den Framebuffer aus und
|
||||
* schreibt die Pixel (RGBA→ARGB, Y-gespiegelt) in ein JavaFX WritableImage.
|
||||
*/
|
||||
public class FrameTransfer implements SceneProcessor {
|
||||
|
||||
private final WritableImage image;
|
||||
private final PixelWriter pw;
|
||||
private final int width;
|
||||
private final int height;
|
||||
|
||||
private Renderer renderer;
|
||||
private ByteBuffer cpuBuf;
|
||||
private byte[] snapshot;
|
||||
private int[] argbRow; // Zeile für JavaFX-PixelWriter
|
||||
|
||||
private final AtomicBoolean jfxBusy = new AtomicBoolean(false);
|
||||
|
||||
public FrameTransfer(WritableImage image) {
|
||||
this.image = image;
|
||||
this.pw = image.getPixelWriter();
|
||||
this.width = (int) image.getWidth();
|
||||
this.height = (int) image.getHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(RenderManager rm, ViewPort vp) {
|
||||
this.renderer = rm.getRenderer();
|
||||
this.cpuBuf = ByteBuffer.allocateDirect(width * height * 4);
|
||||
this.snapshot = new byte[width * height * 4];
|
||||
this.argbRow = new int[width];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postFrame(FrameBuffer out) {
|
||||
if (!jfxBusy.compareAndSet(false, true)) return;
|
||||
|
||||
cpuBuf.clear();
|
||||
renderer.readFrameBuffer(out, cpuBuf);
|
||||
cpuBuf.rewind();
|
||||
cpuBuf.get(snapshot);
|
||||
|
||||
final byte[] pixels = snapshot.clone();
|
||||
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
// GL: Y=0 unten → JavaFX: Y=0 oben + RGBA → 0xFFRRGGBB (int ARGB)
|
||||
PixelFormat<IntBuffer> fmt = PixelFormat.getIntArgbInstance();
|
||||
for (int y = 0; y < height; y++) {
|
||||
int srcBase = (height - 1 - y) * width * 4;
|
||||
for (int x = 0; x < width; x++) {
|
||||
int r = pixels[srcBase + x * 4 ] & 0xFF;
|
||||
int g = pixels[srcBase + x * 4 + 1] & 0xFF;
|
||||
int b = pixels[srcBase + x * 4 + 2] & 0xFF;
|
||||
argbRow[x] = 0xFF000000 | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
pw.setPixels(0, y, width, 1, fmt, argbRow, 0, width);
|
||||
}
|
||||
} finally {
|
||||
jfxBusy.set(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Pflichtmethoden ──────────────────────────────────────────────────────
|
||||
|
||||
@Override public boolean isInitialized() { return renderer != null; }
|
||||
@Override public void reshape(ViewPort vp, int w, int h) {}
|
||||
@Override public void preFrame(float tpf) {}
|
||||
@Override public void postQueue(RenderQueue rq) {}
|
||||
@Override public void cleanup() {}
|
||||
@Override public void setProfiler(AppProfiler profiler) {}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package de.blight.editor;
|
||||
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.system.AppSettings;
|
||||
import com.jme3.system.JmeContext;
|
||||
import de.blight.editor.state.TerrainEditorState;
|
||||
import javafx.scene.image.WritableImage;
|
||||
|
||||
public class JmeEditorApp extends SimpleApplication {
|
||||
|
||||
private final SharedInput input;
|
||||
private final WritableImage jfxImage;
|
||||
|
||||
public JmeEditorApp(SharedInput input, WritableImage jfxImage) {
|
||||
this.input = input;
|
||||
this.jfxImage = jfxImage;
|
||||
}
|
||||
|
||||
/** Startet JME3 in einem Daemon-Thread (blockierend bis App endet). */
|
||||
public static JmeEditorApp launch(SharedInput input, WritableImage jfxImage,
|
||||
int vpWidth, int vpHeight) {
|
||||
JmeEditorApp app = new JmeEditorApp(input, jfxImage);
|
||||
|
||||
AppSettings settings = new AppSettings(true);
|
||||
settings.setTitle("Blight Editor – JME3");
|
||||
settings.setResolution(vpWidth, vpHeight);
|
||||
settings.setRenderer(AppSettings.LWJGL_OPENGL32);
|
||||
settings.setAudioRenderer(null);
|
||||
settings.setSamples(4);
|
||||
|
||||
app.setSettings(settings);
|
||||
app.setShowSettings(false);
|
||||
app.setPauseOnLostFocus(false);
|
||||
|
||||
Thread t = new Thread(() -> app.start(JmeContext.Type.OffscreenSurface), "jme3-editor");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
return app;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void simpleInitApp() {
|
||||
flyCam.setEnabled(false);
|
||||
|
||||
// Frame-Export in das JavaFX-WritableImage
|
||||
viewPort.addProcessor(new FrameTransfer(jfxImage));
|
||||
|
||||
stateManager.attach(new TerrainEditorState(input));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void simpleUpdate(float tpf) {}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package de.blight.editor;
|
||||
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/** Thread-safe Brücke: JavaFX-Events → JME3-Update-Schleife. */
|
||||
public class SharedInput {
|
||||
|
||||
// ── Kamerabewegung (WASD + QE) ──────────────────────────────────────────
|
||||
public volatile boolean forward, backward, left, right, up, down;
|
||||
|
||||
// ── Kamerarotation (Maus-Drag mit mittlerer Taste) ───────────────────────
|
||||
private final AtomicInteger mouseDxAccum = new AtomicInteger();
|
||||
private final AtomicInteger mouseDyAccum = new AtomicInteger();
|
||||
|
||||
public void addMouseDelta(int dx, int dy) {
|
||||
mouseDxAccum.addAndGet(dx);
|
||||
mouseDyAccum.addAndGet(dy);
|
||||
}
|
||||
|
||||
/** Gibt akkumulierten Maus-Delta zurück und setzt ihn zurück. */
|
||||
public int[] consumeMouseDelta() {
|
||||
return new int[]{ mouseDxAccum.getAndSet(0), mouseDyAccum.getAndSet(0) };
|
||||
}
|
||||
|
||||
// ── Terrain-Edits ────────────────────────────────────────────────────────
|
||||
public record TerrainEdit(float screenX, float screenY, int action) {}
|
||||
public final ConcurrentLinkedQueue<TerrainEdit> editQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
// ── Viewport-Skalierung (JavaFX-Pixel → JME3-Pixel) ─────────────────────
|
||||
public volatile double viewportScaleX = 1.0;
|
||||
public volatile double viewportScaleY = 1.0;
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
package de.blight.editor.state;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.collision.CollisionResults;
|
||||
import com.jme3.light.AmbientLight;
|
||||
import com.jme3.light.DirectionalLight;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.material.RenderState;
|
||||
import com.jme3.math.*;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.Geometry;
|
||||
import com.jme3.scene.Mesh;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.scene.VertexBuffer;
|
||||
import com.jme3.scene.shape.Quad;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import de.blight.editor.SharedInput;
|
||||
|
||||
import java.nio.FloatBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
|
||||
public class TerrainEditorState extends BaseAppState {
|
||||
|
||||
// ── Konstanten ──────────────────────────────────────────────────────────
|
||||
private static final int V = 17; // Vertices pro Achse (16 Zellen)
|
||||
private static final float BRUSH_RADIUS = 2.0f; // Meter
|
||||
private static final float BRUSH_DELTA = 0.25f; // Höhenänderung pro Klick
|
||||
private static final float CAM_SPEED = 12f;
|
||||
private static final float MOUSE_SENS = 0.003f;
|
||||
|
||||
// ── Zustand ─────────────────────────────────────────────────────────────
|
||||
private SimpleApplication app;
|
||||
private Camera cam;
|
||||
private AssetManager assets;
|
||||
private Node rootNode;
|
||||
private final SharedInput input;
|
||||
|
||||
private final float[] heights = new float[V * V]; // flaches Array, Index = z*V+x
|
||||
private Mesh terrainMesh;
|
||||
private Geometry terrainGeo;
|
||||
|
||||
// Kamera-Euler-Winkel
|
||||
private float camYaw = 0f;
|
||||
private float camPitch = -0.4f;
|
||||
private final Vector3f camPos = new Vector3f(0, 14, 22);
|
||||
|
||||
public TerrainEditorState(SharedInput input) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
// ── Lifecycle ────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected void initialize(Application app) {
|
||||
this.app = (SimpleApplication) app;
|
||||
this.cam = app.getCamera();
|
||||
this.assets = app.getAssetManager();
|
||||
this.rootNode = this.app.getRootNode();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnable() {
|
||||
buildScene();
|
||||
applyCameraTransform();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDisable() {
|
||||
rootNode.detachAllChildren();
|
||||
}
|
||||
|
||||
@Override protected void cleanup(Application app) {}
|
||||
|
||||
// ── Szene aufbauen ───────────────────────────────────────────────────────
|
||||
|
||||
private void buildScene() {
|
||||
// Licht
|
||||
DirectionalLight sun = new DirectionalLight();
|
||||
sun.setDirection(new Vector3f(-0.5f, -1f, -0.5f).normalizeLocal());
|
||||
sun.setColor(new ColorRGBA(1.2f, 1.1f, 0.9f, 1f));
|
||||
rootNode.addLight(sun);
|
||||
|
||||
AmbientLight ambient = new AmbientLight(new ColorRGBA(0.35f, 0.38f, 0.45f, 1f));
|
||||
rootNode.addLight(ambient);
|
||||
|
||||
// Terrain
|
||||
terrainGeo = buildTerrainGeometry();
|
||||
rootNode.attachChild(terrainGeo);
|
||||
|
||||
// Wasser bei Y = 0
|
||||
rootNode.attachChild(buildWater());
|
||||
|
||||
// Raster-Linien auf dem Terrain (als dünne Linien-Node wäre komplex, Gitter via Grid-Overlay)
|
||||
// Einfache Grid-Markierung: ein flaches transparentes Quad mit Wireframe
|
||||
rootNode.attachChild(buildGridOverlay());
|
||||
|
||||
// Himmel (einfacher Hintergrund-Farbverlauf über Viewport-BG-Farbe)
|
||||
app.getViewPort().setBackgroundColor(new ColorRGBA(0.45f, 0.60f, 0.80f, 1f));
|
||||
}
|
||||
|
||||
private Geometry buildTerrainGeometry() {
|
||||
terrainMesh = new Mesh();
|
||||
FloatBuffer posBuf = BufferUtils.createFloatBuffer(V * V * 3);
|
||||
FloatBuffer normBuf = BufferUtils.createFloatBuffer(V * V * 3);
|
||||
FloatBuffer texBuf = BufferUtils.createFloatBuffer(V * V * 2);
|
||||
IntBuffer idxBuf = BufferUtils.createIntBuffer((V - 1) * (V - 1) * 6);
|
||||
|
||||
for (int z = 0; z < V; z++) {
|
||||
for (int x = 0; x < V; x++) {
|
||||
posBuf.put(x).put(heights[z * V + x]).put(z);
|
||||
normBuf.put(0).put(1).put(0);
|
||||
texBuf.put(x / 16f).put(z / 16f);
|
||||
}
|
||||
}
|
||||
for (int z = 0; z < V - 1; z++) {
|
||||
for (int x = 0; x < V - 1; x++) {
|
||||
int bl = z * V + x, br = bl + 1;
|
||||
int tl = bl + V, tr = tl + 1;
|
||||
idxBuf.put(bl).put(tr).put(br);
|
||||
idxBuf.put(bl).put(tl).put(tr);
|
||||
}
|
||||
}
|
||||
|
||||
terrainMesh.setBuffer(VertexBuffer.Type.Position, 3, posBuf);
|
||||
terrainMesh.setBuffer(VertexBuffer.Type.Normal, 3, normBuf);
|
||||
terrainMesh.setBuffer(VertexBuffer.Type.TexCoord, 2, texBuf);
|
||||
terrainMesh.setBuffer(VertexBuffer.Type.Index, 3, idxBuf);
|
||||
terrainMesh.updateBound();
|
||||
|
||||
Geometry geo = new Geometry("terrain", terrainMesh);
|
||||
geo.setLocalTranslation(-8, 0, -8); // Terrain zentriert bei Ursprung
|
||||
|
||||
Material mat = new Material(assets, "Common/MatDefs/Light/Lighting.j3md");
|
||||
mat.setBoolean("UseMaterialColors", true);
|
||||
mat.setColor("Diffuse", new ColorRGBA(0.28f, 0.58f, 0.18f, 1f));
|
||||
mat.setColor("Ambient", new ColorRGBA(0.12f, 0.28f, 0.08f, 1f));
|
||||
mat.setColor("Specular", ColorRGBA.Black);
|
||||
mat.setFloat("Shininess", 0f);
|
||||
geo.setMaterial(mat);
|
||||
return geo;
|
||||
}
|
||||
|
||||
private Geometry buildWater() {
|
||||
Geometry water = new Geometry("water", new Quad(16, 16));
|
||||
water.rotate(-FastMath.HALF_PI, 0, 0);
|
||||
water.setLocalTranslation(-8, 0.01f, 8); // leicht über Y=0 damit kein Z-Fighting
|
||||
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", new ColorRGBA(0.05f, 0.25f, 0.70f, 0.55f));
|
||||
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
water.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
water.setMaterial(mat);
|
||||
return water;
|
||||
}
|
||||
|
||||
private Geometry buildGridOverlay() {
|
||||
// Einfaches Wireframe-Duplikat des Terrains als Gitter
|
||||
Geometry grid = new Geometry("grid", terrainMesh);
|
||||
grid.setLocalTranslation(-8, 0.02f, -8);
|
||||
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", new ColorRGBA(0f, 0f, 0f, 0.25f));
|
||||
mat.getAdditionalRenderState().setWireframe(true);
|
||||
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
grid.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
grid.setMaterial(mat);
|
||||
return grid;
|
||||
}
|
||||
|
||||
// ── Update-Schleife ──────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
updateCamera(tpf);
|
||||
processEdits();
|
||||
}
|
||||
|
||||
private void updateCamera(float tpf) {
|
||||
// Rotation aus akkumuliertem Maus-Delta
|
||||
int[] delta = input.consumeMouseDelta();
|
||||
if (delta[0] != 0 || delta[1] != 0) {
|
||||
camYaw -= delta[0] * MOUSE_SENS;
|
||||
camPitch -= delta[1] * MOUSE_SENS;
|
||||
camPitch = FastMath.clamp(camPitch,
|
||||
-FastMath.HALF_PI + 0.05f,
|
||||
FastMath.HALF_PI - 0.05f);
|
||||
}
|
||||
|
||||
applyCameraTransform();
|
||||
|
||||
// Bewegung in Kamera-Vorwärtsrichtung projiziert auf XZ-Ebene
|
||||
float speed = CAM_SPEED * tpf;
|
||||
Vector3f fwd = cam.getDirection().clone().setY(0);
|
||||
if (fwd.lengthSquared() > 0.001f) fwd.normalizeLocal();
|
||||
Vector3f lft = cam.getLeft().clone().setY(0);
|
||||
if (lft.lengthSquared() > 0.001f) lft.normalizeLocal();
|
||||
|
||||
if (input.forward) camPos.addLocal(fwd.mult(speed));
|
||||
if (input.backward) camPos.subtractLocal(fwd.mult(speed));
|
||||
if (input.left) camPos.addLocal(lft.mult(speed));
|
||||
if (input.right) camPos.subtractLocal(lft.mult(speed));
|
||||
if (input.up) camPos.y += speed;
|
||||
if (input.down) camPos.y -= speed;
|
||||
|
||||
cam.setLocation(camPos);
|
||||
}
|
||||
|
||||
private void applyCameraTransform() {
|
||||
Quaternion yawQ = new Quaternion().fromAngleAxis(camYaw, Vector3f.UNIT_Y);
|
||||
Quaternion pitchQ = new Quaternion().fromAngleAxis(camPitch, Vector3f.UNIT_X);
|
||||
cam.setRotation(yawQ.mult(pitchQ));
|
||||
cam.setLocation(camPos);
|
||||
}
|
||||
|
||||
private void processEdits() {
|
||||
SharedInput.TerrainEdit edit;
|
||||
while ((edit = input.editQueue.poll()) != null) {
|
||||
// JavaFX-Koordinaten → JME3-Screen-Koordinaten (Y spiegeln)
|
||||
float jmeX = (float)(edit.screenX() * input.viewportScaleX);
|
||||
float jmeY = cam.getHeight() - (float)(edit.screenY() * input.viewportScaleY);
|
||||
|
||||
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||
Vector3f dir = far.subtract(near).normalizeLocal();
|
||||
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrainGeo.collideWith(new com.jme3.math.Ray(near, dir), hits);
|
||||
|
||||
if (hits.size() > 0) {
|
||||
Vector3f contact = hits.getClosestCollision().getContactPoint();
|
||||
modifyHeight(contact, edit.action() * BRUSH_DELTA);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Höhen-Werkzeug ───────────────────────────────────────────────────────
|
||||
|
||||
private void modifyHeight(Vector3f worldContact, float delta) {
|
||||
// Terrain-Geometrie ist bei (-8, 0, -8), Vertices bei (0..16, h, 0..16)
|
||||
float localX = worldContact.x + 8;
|
||||
float localZ = worldContact.z + 8;
|
||||
|
||||
for (int z = 0; z < V; z++) {
|
||||
for (int x = 0; x < V; x++) {
|
||||
float dx = x - localX;
|
||||
float dz = z - localZ;
|
||||
float dist = FastMath.sqrt(dx * dx + dz * dz);
|
||||
if (dist < BRUSH_RADIUS) {
|
||||
float falloff = 1f - dist / BRUSH_RADIUS;
|
||||
heights[z * V + x] += delta * falloff;
|
||||
}
|
||||
}
|
||||
}
|
||||
updateTerrainMesh();
|
||||
}
|
||||
|
||||
private void updateTerrainMesh() {
|
||||
FloatBuffer posBuf = terrainMesh.getFloatBuffer(VertexBuffer.Type.Position);
|
||||
FloatBuffer normBuf = terrainMesh.getFloatBuffer(VertexBuffer.Type.Normal);
|
||||
|
||||
posBuf.rewind();
|
||||
for (int z = 0; z < V; z++) {
|
||||
for (int x = 0; x < V; x++) {
|
||||
posBuf.put(x).put(heights[z * V + x]).put(z);
|
||||
}
|
||||
}
|
||||
|
||||
// Normalen per finite differences
|
||||
normBuf.rewind();
|
||||
for (int z = 0; z < V; z++) {
|
||||
for (int x = 0; x < V; x++) {
|
||||
float hL = x > 0 ? heights[z * V + (x - 1)] : heights[z * V + x];
|
||||
float hR = x < V-1 ? heights[z * V + (x + 1)] : heights[z * V + x];
|
||||
float hD = z > 0 ? heights[(z - 1) * V + x] : heights[z * V + x];
|
||||
float hU = z < V-1 ? heights[(z + 1) * V + x] : heights[z * V + x];
|
||||
float nx = -(hR - hL);
|
||||
float ny = 2.0f;
|
||||
float nz = -(hU - hD);
|
||||
float len = FastMath.sqrt(nx*nx + ny*ny + nz*nz);
|
||||
normBuf.put(nx / len).put(ny / len).put(nz / len);
|
||||
}
|
||||
}
|
||||
|
||||
terrainMesh.getBuffer(VertexBuffer.Type.Position).setUpdateNeeded();
|
||||
terrainMesh.getBuffer(VertexBuffer.Type.Normal).setUpdateNeeded();
|
||||
terrainMesh.updateBound();
|
||||
terrainGeo.updateModelBound();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user