Initaler Commit

This commit is contained in:
2026-05-07 11:54:11 +02:00
commit b8a0234ad2
158 changed files with 15138 additions and 0 deletions

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

View File

@@ -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);
}
}

View File

@@ -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) {}
}

View File

@@ -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) {}
}

View File

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

View File

@@ -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();
}
}