diff --git a/blight-common/src/main/java/de/blight/common/PlacedWater.java b/blight-common/src/main/java/de/blight/common/PlacedWater.java index 7d7e034..3f71a9b 100644 --- a/blight-common/src/main/java/de/blight/common/PlacedWater.java +++ b/blight-common/src/main/java/de/blight/common/PlacedWater.java @@ -1,6 +1,86 @@ package de.blight.common; +import java.util.Arrays; + /** - * Platzierte Wasserfläche, definiert durch ein Benutzer-Polygon, eine Höhe und eine Fließrichtung. + * Platzierte Wasserfläche: Polygon, Höhe, Fließrichtung und Rendering-Parameter. + * pointsY enthält pro Ecke eine individuelle Y-Höhe; sind alle gleich waterHeight + * ist die Fläche flach (WaterPolygonFilter), sonst abschüssig (FlowingWater-Shader). */ -public record PlacedWater(float[] pointsX, float[] pointsZ, float waterHeight, float flowDegrees) {} +public record PlacedWater( + float[] pointsX, + float[] pointsY, // pro Ecke; Länge == pointsX.length + float[] pointsZ, + float waterHeight, + float flowDegrees, + // Rendering-Parameter + float speed, + float waveScale, + float waveAmplitude, + float transparency, + float waterColorR, float waterColorG, float waterColorB, + float deepColorR, float deepColorG, float deepColorB +) { + public static final float DEF_SPEED = 0.5f; + public static final float DEF_WAVE_SCALE = 0.008f; + public static final float DEF_WAVE_AMPLITUDE = 0.1f; + public static final float DEF_TRANSPARENCY = 0.15f; + public static final float DEF_WATER_R = 0.05f, DEF_WATER_G = 0.25f, DEF_WATER_B = 0.55f; + public static final float DEF_DEEP_R = 0.02f, DEF_DEEP_G = 0.12f, DEF_DEEP_B = 0.30f; + + /** True wenn die Y-Spanne aller Eckpunkte unter 0.5 m liegt (flaches Wasser). */ + public boolean isFlat() { + float min = pointsY[0], max = pointsY[0]; + for (float y : pointsY) { + if (y < min) min = y; + if (y > max) max = y; + } + return (max - min) < 0.5f; + } + + /** Erstellt eine PlacedWater mit Standard-Rendering-Parametern und flacher Höhe. */ + public static PlacedWater of(float[] xs, float[] zs, float h, float flow) { + float[] ys = new float[xs.length]; + Arrays.fill(ys, h); + return new PlacedWater(xs, ys, zs, h, flow, + DEF_SPEED, DEF_WAVE_SCALE, DEF_WAVE_AMPLITUDE, DEF_TRANSPARENCY, + DEF_WATER_R, DEF_WATER_G, DEF_WATER_B, + DEF_DEEP_R, DEF_DEEP_G, DEF_DEEP_B); + } + + /** Gibt eine Kopie mit neuen Rendering-Parametern zurück. */ + public PlacedWater withParams(float speed, float waveScale, float waveAmplitude, + float transparency, + float wr, float wg, float wb, + float dr, float dg, float db) { + return new PlacedWater(pointsX, pointsY, pointsZ, waterHeight, flowDegrees, + speed, waveScale, waveAmplitude, transparency, + wr, wg, wb, dr, dg, db); + } + + /** Gibt eine Kopie mit neuer globaler Höhe zurück; setzt alle pointsY auf newHeight (flacht ab). */ + public PlacedWater withHeight(float newHeight) { + float[] ys = new float[pointsX.length]; + Arrays.fill(ys, newHeight); + return new PlacedWater(pointsX, ys, pointsZ, newHeight, flowDegrees, + speed, waveScale, waveAmplitude, transparency, + waterColorR, waterColorG, waterColorB, + deepColorR, deepColorG, deepColorB); + } + + /** Gibt eine Kopie mit neuer Fließrichtung zurück. */ + public PlacedWater withFlow(float newDegrees) { + return new PlacedWater(pointsX, pointsY, pointsZ, waterHeight, newDegrees, + speed, waveScale, waveAmplitude, transparency, + waterColorR, waterColorG, waterColorB, + deepColorR, deepColorG, deepColorB); + } + + /** Gibt eine Kopie mit neuen Eckpunkten (inkl. per-Vertex-Y) zurück. */ + public PlacedWater withPoints(float[] xs, float[] ys, float[] zs) { + return new PlacedWater(xs, ys, zs, waterHeight, flowDegrees, + speed, waveScale, waveAmplitude, transparency, + waterColorR, waterColorG, waterColorB, + deepColorR, deepColorG, deepColorB); + } +} diff --git a/blight-common/src/main/java/de/blight/common/PlacedWaterfall.java b/blight-common/src/main/java/de/blight/common/PlacedWaterfall.java new file mode 100644 index 0000000..156dbaa --- /dev/null +++ b/blight-common/src/main/java/de/blight/common/PlacedWaterfall.java @@ -0,0 +1,20 @@ +package de.blight.common; + +/** + * Wasserfall-Quad: definiert durch 4 explizite 3D-Eckpunkte. + * Reihenfolge: A=oben-links, B=oben-rechts, C=unten-rechts, D=unten-links. + * UV scrollt von V=0 (oben) nach V=1 (unten), Wasser fließt bergab. + */ +public record PlacedWaterfall( + float ax, float ay, float az, + float bx, float by, float bz, + float cx, float cy, float cz, + float dx, float dy, float dz, + float speed, + float transparency, + float colorR, float colorG, float colorB +) { + public static final float DEF_SPEED = 1.5f; + public static final float DEF_TRANSPARENCY = 0.75f; + public static final float DEF_R = 0.35f, DEF_G = 0.55f, DEF_B = 0.75f; +} diff --git a/blight-common/src/main/java/de/blight/common/WaterBodyIO.java b/blight-common/src/main/java/de/blight/common/WaterBodyIO.java index 62d3430..ac9e902 100644 --- a/blight-common/src/main/java/de/blight/common/WaterBodyIO.java +++ b/blight-common/src/main/java/de/blight/common/WaterBodyIO.java @@ -5,9 +5,14 @@ import java.nio.file.*; import java.util.*; /** - * Liest und schreibt platzierte Wasserflächen ({@code blight_water.blw}) neben der Kartendatei. + * Liest und schreibt platzierte Wasserflächen ({@code blight_water.blw}). * - * Format (je Zeile): x1,z1;x2,z2;x3,z3;... TAB waterHeight [TAB flowDegrees] + * Format (tab-getrennt): + * polygon_points waterHeight flowDegrees speed waveScale waveAmplitude transparency + * waterR waterG waterB deepR deepG deepB + * + * polygon_points: Semikolon-getrennte Tripel "x,y,z" (neu) oder Paare "x,z" (alt, Y = waterHeight). + * Alle Felder ab flowDegrees sind optional (rückwärtskompatibel). */ public final class WaterBodyIO { @@ -21,18 +26,28 @@ public final class WaterBodyIO { Path p = getPath(); Files.createDirectories(p.getParent()); try (BufferedWriter w = Files.newBufferedWriter(p)) { - w.write("# polygon_points\twaterHeight\tflowDegrees"); + w.write("# polygon_points\twaterHeight\tflowDegrees\tspeed\twaveScale\twaveAmplitude" + + "\ttransparency\twaterR\twaterG\twaterB\tdeepR\tdeepG\tdeepB"); w.newLine(); for (PlacedWater b : bodies) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < b.pointsX().length; i++) { if (i > 0) sb.append(';'); - sb.append(String.format(Locale.ROOT, "%.5f,%.5f", b.pointsX()[i], b.pointsZ()[i])); + sb.append(String.format(Locale.ROOT, "%.5f,%.5f,%.5f", + b.pointsX()[i], b.pointsY()[i], b.pointsZ()[i])); } - sb.append('\t'); - sb.append(String.format(Locale.ROOT, "%.5f", b.waterHeight())); - sb.append('\t'); - sb.append(String.format(Locale.ROOT, "%.1f", b.flowDegrees())); + sb.append('\t').append(String.format(Locale.ROOT, "%.5f", b.waterHeight())); + sb.append('\t').append(String.format(Locale.ROOT, "%.1f", b.flowDegrees())); + sb.append('\t').append(String.format(Locale.ROOT, "%.4f", b.speed())); + sb.append('\t').append(String.format(Locale.ROOT, "%.6f", b.waveScale())); + sb.append('\t').append(String.format(Locale.ROOT, "%.5f", b.waveAmplitude())); + sb.append('\t').append(String.format(Locale.ROOT, "%.5f", b.transparency())); + sb.append('\t').append(String.format(Locale.ROOT, "%.5f", b.waterColorR())); + sb.append('\t').append(String.format(Locale.ROOT, "%.5f", b.waterColorG())); + sb.append('\t').append(String.format(Locale.ROOT, "%.5f", b.waterColorB())); + sb.append('\t').append(String.format(Locale.ROOT, "%.5f", b.deepColorR())); + sb.append('\t').append(String.format(Locale.ROOT, "%.5f", b.deepColorG())); + sb.append('\t').append(String.format(Locale.ROOT, "%.5f", b.deepColorB())); w.write(sb.toString()); w.newLine(); } @@ -46,20 +61,42 @@ public final class WaterBodyIO { for (String line : Files.readAllLines(p)) { line = line.strip(); if (line.isEmpty() || line.startsWith("#")) continue; - String[] parts = line.split("\t", -1); - if (parts.length < 2) continue; + String[] cols = line.split("\t", -1); + if (cols.length < 2) continue; try { - float wh = Float.parseFloat(parts[1].strip()); - float fd = parts.length >= 3 ? Float.parseFloat(parts[2].strip()) : 0f; - String[] pts = parts[0].split(";", -1); + float wh = Float.parseFloat(cols[1].strip()); + float flow = cols.length >= 3 ? Float.parseFloat(cols[2].strip()) : 0f; + float spd = cols.length >= 4 ? Float.parseFloat(cols[3].strip()) : PlacedWater.DEF_SPEED; + float ws = cols.length >= 5 ? Float.parseFloat(cols[4].strip()) : PlacedWater.DEF_WAVE_SCALE; + float wa = cols.length >= 6 ? Float.parseFloat(cols[5].strip()) : PlacedWater.DEF_WAVE_AMPLITUDE; + float tr = cols.length >= 7 ? Float.parseFloat(cols[6].strip()) : PlacedWater.DEF_TRANSPARENCY; + float wr = cols.length >= 8 ? Float.parseFloat(cols[7].strip()) : PlacedWater.DEF_WATER_R; + float wg = cols.length >= 9 ? Float.parseFloat(cols[8].strip()) : PlacedWater.DEF_WATER_G; + float wb2 = cols.length >= 10? Float.parseFloat(cols[9].strip()) : PlacedWater.DEF_WATER_B; + float dr = cols.length >= 11? Float.parseFloat(cols[10].strip()): PlacedWater.DEF_DEEP_R; + float dg = cols.length >= 12? Float.parseFloat(cols[11].strip()): PlacedWater.DEF_DEEP_G; + float db = cols.length >= 13? Float.parseFloat(cols[12].strip()): PlacedWater.DEF_DEEP_B; + + String[] pts = cols[0].split(";", -1); float[] xs = new float[pts.length]; + float[] ys = new float[pts.length]; float[] zs = new float[pts.length]; for (int i = 0; i < pts.length; i++) { String[] c = pts[i].split(",", -1); xs[i] = Float.parseFloat(c[0].strip()); - zs[i] = Float.parseFloat(c[1].strip()); + if (c.length >= 3) { + // neues Format: x,y,z + ys[i] = Float.parseFloat(c[1].strip()); + zs[i] = Float.parseFloat(c[2].strip()); + } else { + // altes Format: x,z → Y = waterHeight + ys[i] = wh; + zs[i] = Float.parseFloat(c[1].strip()); + } + } + if (xs.length >= 3) { + list.add(new PlacedWater(xs, ys, zs, wh, flow, spd, ws, wa, tr, wr, wg, wb2, dr, dg, db)); } - if (xs.length >= 3) list.add(new PlacedWater(xs, zs, wh, fd)); } catch (NumberFormatException | ArrayIndexOutOfBoundsException ignored) {} } return list; diff --git a/blight-common/src/main/java/de/blight/common/WaterfallIO.java b/blight-common/src/main/java/de/blight/common/WaterfallIO.java new file mode 100644 index 0000000..3067327 --- /dev/null +++ b/blight-common/src/main/java/de/blight/common/WaterfallIO.java @@ -0,0 +1,74 @@ +package de.blight.common; + +import java.io.*; +import java.nio.file.*; +import java.util.*; + +/** + * Liest und schreibt Wasserfall-Quads ({@code blight_waterfall.blwf}). + * + * Format (tab-getrennt): + * ax,ay,az bx,by,bz cx,cy,cz dx,dy,dz speed transparency r g b + */ +public final class WaterfallIO { + + private WaterfallIO() {} + + public static Path getPath() { + return MapIO.getMapPath().resolveSibling("blight_waterfall.blwf"); + } + + public static void save(List list) throws IOException { + Path p = getPath(); + Files.createDirectories(p.getParent()); + try (BufferedWriter w = Files.newBufferedWriter(p)) { + w.write("# ax,ay,az\tbx,by,bz\tcx,cy,cz\tdx,dy,dz\tspeed\ttransparency\tr\tg\tb"); + w.newLine(); + for (PlacedWaterfall wf : list) { + w.write(String.format(Locale.ROOT, + "%.5f,%.5f,%.5f\t%.5f,%.5f,%.5f\t%.5f,%.5f,%.5f\t%.5f,%.5f,%.5f\t%.4f\t%.5f\t%.5f\t%.5f\t%.5f", + wf.ax(), wf.ay(), wf.az(), + wf.bx(), wf.by(), wf.bz(), + wf.cx(), wf.cy(), wf.cz(), + wf.dx(), wf.dy(), wf.dz(), + wf.speed(), wf.transparency(), + wf.colorR(), wf.colorG(), wf.colorB())); + w.newLine(); + } + } + } + + public static List load() throws IOException { + Path p = getPath(); + if (!Files.exists(p)) return List.of(); + List result = new ArrayList<>(); + for (String line : Files.readAllLines(p)) { + line = line.strip(); + if (line.isEmpty() || line.startsWith("#")) continue; + try { + String[] cols = line.split("\t", -1); + float[] a = parseVec(cols[0]); + float[] b = parseVec(cols[1]); + float[] c = parseVec(cols[2]); + float[] d = parseVec(cols[3]); + float speed = cols.length > 4 ? Float.parseFloat(cols[4].strip()) : PlacedWaterfall.DEF_SPEED; + float transp = cols.length > 5 ? Float.parseFloat(cols[5].strip()) : PlacedWaterfall.DEF_TRANSPARENCY; + float r = cols.length > 6 ? Float.parseFloat(cols[6].strip()) : PlacedWaterfall.DEF_R; + float g = cols.length > 7 ? Float.parseFloat(cols[7].strip()) : PlacedWaterfall.DEF_G; + float bv = cols.length > 8 ? Float.parseFloat(cols[8].strip()) : PlacedWaterfall.DEF_B; + result.add(new PlacedWaterfall( + a[0], a[1], a[2], b[0], b[1], b[2], + c[0], c[1], c[2], d[0], d[1], d[2], + speed, transp, r, g, bv)); + } catch (Exception ignored) {} + } + return result; + } + + private static float[] parseVec(String s) { + String[] p = s.strip().split(",", -1); + return new float[]{ Float.parseFloat(p[0].strip()), + Float.parseFloat(p[1].strip()), + Float.parseFloat(p[2].strip()) }; + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index af23aed..8657f16 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -814,6 +814,22 @@ public class EditorApp extends Application { } } + if (input.auswahlWaterSelected) { + input.auswahlWaterSelected = false; + if (wasserMergedBtn != null) wasserMergedBtn.setSelected(true); + input.activeLayer = SharedInput.LAYER_WATER; + root.setRight(buildWasserPanel()); + updateWaterPanel(input.selectedWaterInfo); + } + + if (input.auswahlWaterfallSelected) { + input.auswahlWaterfallSelected = false; + if (wasserMergedBtn != null) wasserMergedBtn.setSelected(true); + input.activeLayer = SharedInput.LAYER_WATERFALL; + root.setRight(buildWasserPanel()); + updateWaterfallPanel(input.selectedWaterfallInfo); + } + if (input.itemSelectionChanged) { input.itemSelectionChanged = false; updateAuswahlItemPanel(input.selectedItemInfo); @@ -2877,12 +2893,13 @@ public class EditorApp extends Application { inner.getChildren().addAll( new Separator(), - styledHint("L-Klick → Polygon-Punkt setzen"), - styledHint("R-Klick → letzten Punkt entfernen"), + styledHint("L-Klick (leer) → erste Ecke setzen"), + styledHint("L-Klick (2.) → Quad abschliessen"), + styledHint("Handle ziehen → Ecke / Kante verschieben"), + styledHint("R-Klick → Abbrechen / Auswahl aufheben"), styledHint("Leertaste → Höhe vom Terrain übernehmen"), - styledHint("Erster Punkt bestimmt die Höhe"), - styledHint("ESC → Polygon abbrechen"), - styledHint("Entf → Fläche löschen")); + styledHint("ESC → Abbrechen / Auswahl aufheben"), + styledHint("Entf → Quad löschen")); ScrollPane scroll = new ScrollPane(inner); scroll.setFitToWidth(true); @@ -2900,27 +2917,20 @@ public class EditorApp extends Application { VBox inner = new VBox(10); inner.setPadding(new Insets(10)); - Label widthTitle = sectionTitle("Breite (nächster Punkt)"); - Slider widthSlider = new Slider(8.0, 40.0, input.waterfallNewWidth); - widthSlider.setBlockIncrement(0.5); - widthSlider.setShowTickLabels(true); - widthSlider.setMajorTickUnit(5); - widthSlider.valueProperty().addListener((o, ov, nv) -> input.waterfallNewWidth = nv.floatValue()); - Button undoBtn = new Button("Letzten Punkt entfernen"); undoBtn.setMaxWidth(Double.MAX_VALUE); undoBtn.setOnAction(e -> input.undoWaterfallPointRequested = true); inner.getChildren().addAll( - sectionTitle("Wasserfall"), - new Separator(), - widthTitle, withField(widthSlider, "%.1f"), + sectionTitle("Wasserfall (4-Klick Quad)"), new Separator(), undoBtn, new Separator(), - styledHint("L-Klick → Punkt setzen"), - styledHint("R-Klick → Wasserfall abschließen"), - styledHint("Backspace → letzten Punkt löschen")); + styledHint("L-Klick → Punkt setzen (1→2→3→4)"), + styledHint("Punkte auf Klippen, Voxel & Seenkanten möglich"), + styledHint("R-Klick / Backspace → letzten Punkt rückgängig"), + styledHint("ESC → Platzierung abbrechen"), + styledHint("Wasserfall anklicken → Ecken verschiebbar")); ScrollPane scroll = new ScrollPane(inner); scroll.setFitToWidth(true); @@ -2959,7 +2969,19 @@ public class EditorApp extends Application { Button deleteBtn = new Button("Wasserfall löschen"); deleteBtn.setMaxWidth(Double.MAX_VALUE); deleteBtn.setStyle("-fx-background-color: #c0392b; -fx-text-fill: white;"); - deleteBtn.setOnAction(e -> input.deleteWaterfallRequested = true); + deleteBtn.setOnAction(e -> { + javafx.scene.control.Alert confirm = new javafx.scene.control.Alert( + javafx.scene.control.Alert.AlertType.CONFIRMATION, + "Wasserfall unwiderruflich löschen?", + javafx.scene.control.ButtonType.YES, + javafx.scene.control.ButtonType.NO); + confirm.setTitle("Wasserfall löschen"); + confirm.setHeaderText(null); + confirm.initOwner(primaryStage); + confirm.showAndWait().ifPresent(btn -> { + if (btn == javafx.scene.control.ButtonType.YES) input.deleteWaterfallRequested = true; + }); + }); inner.getChildren().addAll( sectionTitle("Wasserfall selektiert"), @@ -2999,54 +3021,132 @@ public class EditorApp extends Application { return; } - // Format: "idx|waterHeight|pointCount|flowDegrees" + // Format: idx|h|n|flow|speed|waveScale|waveAmp|transp|wr|wg|wb|dr|dg|db String[] p = info.split("\\|", -1); if (p.length < 3) return; try { float waterHeight = Float.parseFloat(p[1]); int pointCount = Integer.parseInt(p[2]); - float flowDeg = p.length >= 4 ? Float.parseFloat(p[3]) : 0f; + float flowDeg = p.length > 3 ? Float.parseFloat(p[3]) : 0f; + float speed = p.length > 4 ? Float.parseFloat(p[4]) : 0.5f; + float waveScale = p.length > 5 ? Float.parseFloat(p[5]) : 0.008f; + float waveAmp = p.length > 6 ? Float.parseFloat(p[6]) : 0.1f; + float transp = p.length > 7 ? Float.parseFloat(p[7]) : 0.15f; + float wr = p.length > 8 ? Float.parseFloat(p[8]) : 0.05f; + float wg = p.length > 9 ? Float.parseFloat(p[9]) : 0.25f; + float wb = p.length > 10 ? Float.parseFloat(p[10]) : 0.55f; + float dr = p.length > 11 ? Float.parseFloat(p[11]) : 0.02f; + float dg = p.length > 12 ? Float.parseFloat(p[12]) : 0.12f; + float db = p.length > 13 ? Float.parseFloat(p[13]) : 0.30f; - Label infoLabel = new Label(String.format("Punkte: %d | Höhe: %.2f m", pointCount, waterHeight)); + // Live-Array für alle Rendering-Parameter; wird bei jeder Änderung neu gesetzt + float[] params = {speed, waveScale, waveAmp, transp, wr, wg, wb, dr, dg, db}; + + // ── Info ────────────────────────────────────────────────────────── + Label infoLabel = new Label(String.format("Ecken: %d | Höhe: %.2f m", pointCount, waterHeight)); infoLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;"); - Label heightLabel = new Label("Höhe (m)"); - heightLabel.setStyle("-fx-font-size: 11;"); - + // ── Höhe ────────────────────────────────────────────────────────── Slider heightSlider = new Slider(-50, 500, waterHeight); heightSlider.setBlockIncrement(0.1); heightSlider.setShowTickLabels(false); - heightSlider.valueProperty().addListener((o, ov, nv) -> input.pendingWaterHeight.set(nv.floatValue())); - - Label flowLabel = new Label("Fließrichtung"); - flowLabel.setStyle("-fx-font-size: 11; -fx-font-weight: bold;"); - Label flowDegLabel = new Label(String.format("%.0f°", flowDeg)); - flowDegLabel.setStyle("-fx-font-size: 11;"); + heightSlider.valueProperty().addListener((o, ov, nv) -> + input.pendingWaterHeight.set(nv.floatValue())); + // ── Fließrichtung ───────────────────────────────────────────────── Spinner flowSpinner = new Spinner<>(0, 359, Math.round(flowDeg)); flowSpinner.setEditable(true); flowSpinner.setMaxWidth(Double.MAX_VALUE); - flowSpinner.valueProperty().addListener((o, ov, nv) -> { - flowDegLabel.setText(nv + "°"); - input.pendingWaterFlowDegrees.set(nv.floatValue()); + flowSpinner.valueProperty().addListener((o, ov, nv) -> + input.pendingWaterFlowDegrees.set(nv.floatValue())); + + // ── Geschwindigkeit ─────────────────────────────────────────────── + Slider speedSlider = new Slider(0.05, 5.0, speed); + speedSlider.setBlockIncrement(0.05); + speedSlider.valueProperty().addListener((o, ov, nv) -> { + params[0] = nv.floatValue(); + input.pendingWaterParams.set(params.clone()); }); - Button delBtn = new Button("Löschen"); + // ── Wellengröße ─────────────────────────────────────────────────── + Slider waveScaleSlider = new Slider(0.001, 0.05, waveScale); + waveScaleSlider.setBlockIncrement(0.001); + waveScaleSlider.valueProperty().addListener((o, ov, nv) -> { + params[1] = nv.floatValue(); + input.pendingWaterParams.set(params.clone()); + }); + + // ── Wellenamplitude ─────────────────────────────────────────────── + Slider waveAmpSlider = new Slider(0.0, 2.0, waveAmp); + waveAmpSlider.setBlockIncrement(0.05); + waveAmpSlider.valueProperty().addListener((o, ov, nv) -> { + params[2] = nv.floatValue(); + input.pendingWaterParams.set(params.clone()); + }); + + // ── Transparenz ─────────────────────────────────────────────────── + Slider transpSlider = new Slider(0.0, 1.0, transp); + transpSlider.setBlockIncrement(0.01); + transpSlider.valueProperty().addListener((o, ov, nv) -> { + params[3] = nv.floatValue(); + input.pendingWaterParams.set(params.clone()); + }); + + // ── Wasserfarbe ─────────────────────────────────────────────────── + javafx.scene.control.ColorPicker waterColorPicker = + new javafx.scene.control.ColorPicker(javafx.scene.paint.Color.color( + Math.min(wr, 1.0), Math.min(wg, 1.0), Math.min(wb, 1.0))); + waterColorPicker.setMaxWidth(Double.MAX_VALUE); + waterColorPicker.valueProperty().addListener((o, ov, nv) -> { + params[4] = (float) nv.getRed(); + params[5] = (float) nv.getGreen(); + params[6] = (float) nv.getBlue(); + input.pendingWaterParams.set(params.clone()); + }); + + // ── Tiefwasserfarbe ─────────────────────────────────────────────── + javafx.scene.control.ColorPicker deepColorPicker = + new javafx.scene.control.ColorPicker(javafx.scene.paint.Color.color( + Math.min(dr, 1.0), Math.min(dg, 1.0), Math.min(db, 1.0))); + deepColorPicker.setMaxWidth(Double.MAX_VALUE); + deepColorPicker.valueProperty().addListener((o, ov, nv) -> { + params[7] = (float) nv.getRed(); + params[8] = (float) nv.getGreen(); + params[9] = (float) nv.getBlue(); + input.pendingWaterParams.set(params.clone()); + }); + + // ── Löschen ─────────────────────────────────────────────────────── + Button delBtn = new Button("Quad löschen"); delBtn.setMaxWidth(Double.MAX_VALUE); delBtn.setStyle("-fx-text-fill: #c0392b;"); delBtn.setOnAction(e -> input.deleteWaterRequested = true); waterDynamicContent.getChildren().addAll( - infoLabel, new Separator(), - heightLabel, withField(heightSlider, "%.2f"), + infoLabel, new Separator(), - flowLabel, flowSpinner, + lbl("Höhe (m)"), withField(heightSlider, "%.2f"), + new Separator(), + lbl("Fließrichtung"), flowSpinner, + lbl("Geschwindigkeit"),withField(speedSlider, "%.2f"), + lbl("Wellengröße"), withField(waveScaleSlider, "%.4f"), + lbl("Wellenamplitude"),withField(waveAmpSlider, "%.2f"), + lbl("Transparenz"), withField(transpSlider, "%.2f"), + new Separator(), + lbl("Wasserfarbe"), waterColorPicker, + lbl("Tiefwasserfarbe"),deepColorPicker, new Separator(), delBtn); } catch (NumberFormatException ignored) {} } + private static Label lbl(String text) { + Label l = new Label(text); + l.setStyle("-fx-font-size: 11;"); + return l; + } + // ── Emitter-Panel ───────────────────────────────────────────────────────── private VBox buildEmitterPanel() { @@ -5354,6 +5454,12 @@ public class EditorApp extends Application { input.reloadPlacedOther = true; } case "waterfall" -> { + java.util.List list = new java.util.ArrayList<>(de.blight.common.WaterfallIO.load()); + if (index >= 0 && index < list.size()) list.remove(index); + de.blight.common.WaterfallIO.save(list); + input.reloadPlacedOther = true; + } + case "river" -> { java.util.List> list = new java.util.ArrayList<>(de.blight.common.RiverIO.load()); if (index >= 0 && index < list.size()) list.remove(index); de.blight.common.RiverIO.save(list); @@ -8748,7 +8854,8 @@ public class EditorApp extends Application { || input.activeLayer == SharedInput.LAYER_ZONEN || input.activeLayer == SharedInput.LAYER_SOUND_AREAS || input.activeLayer == SharedInput.LAYER_AREAS - || input.activeLayer == SharedInput.LAYER_LOCATION_ZONES; + || input.activeLayer == SharedInput.LAYER_LOCATION_ZONES + || input.activeLayer == SharedInput.LAYER_WATER; if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL) { // Einzel-Klick ohne Edit-Timer (verhindert Dauer-Spam) input.playToolClickQueue.offer( @@ -8764,7 +8871,8 @@ public class EditorApp extends Application { boolean singleClickLayer = input.activeLayer == SharedInput.LAYER_ZONEN || input.activeLayer == SharedInput.LAYER_SOUND_AREAS || input.activeLayer == SharedInput.LAYER_AREAS - || input.activeLayer == SharedInput.LAYER_LOCATION_ZONES; + || input.activeLayer == SharedInput.LAYER_LOCATION_ZONES + || input.activeLayer == SharedInput.LAYER_WATER; editPressX = e.getX(); editPressY = e.getY(); editPressAction = -1; submitEdit(editPressX, editPressY, editPressAction); if (!singleClickLayer) startEditTimer(); @@ -8789,6 +8897,18 @@ public class EditorApp extends Application { return; } + if (input.activeLayer == SharedInput.LAYER_WATER && e.isPrimaryButtonDown()) { + input.waterDragQueue.offer(new SharedInput.WaterDrag((float) e.getX(), (float) e.getY())); + input.mouseScreenX = (float) e.getX(); + input.mouseScreenY = (float) e.getY(); + return; + } + + if (input.activeLayer == SharedInput.LAYER_WATERFALL && e.isPrimaryButtonDown()) { + input.waterfallDragQueue.offer(new SharedInput.WaterfallDrag((float) e.getX(), (float) e.getY())); + return; + } + if (input.activeLayer == SharedInput.LAYER_ROUTINE_EDITOR && e.isSecondaryButtonDown()) { input.routineYawDragQueue.offer( new SharedInput.RoutineYawDrag((float) e.getX(), (float) e.getY(), false)); @@ -8815,6 +8935,12 @@ public class EditorApp extends Application { viewport.setOnMouseReleased(e -> { objDragging = false; if (!isObjectMode()) stopEditTimer(); + if (input.activeLayer == SharedInput.LAYER_WATER) { + input.waterDragEnd = true; + } + if (input.activeLayer == SharedInput.LAYER_WATERFALL) { + input.waterfallDragEnd = true; + } if (input.activeLayer == SharedInput.LAYER_ROUTINE_EDITOR) { input.routineYawDragQueue.offer( new SharedInput.RoutineYawDrag((float) e.getX(), (float) e.getY(), true)); @@ -9404,6 +9530,8 @@ public class EditorApp extends Application { case CONTROL -> input.ctrlHeld = pressed; case ALT -> input.altHeld = pressed; case ENTER -> { + if (pressed && input.activeLayer == SharedInput.LAYER_WATERFALL) + input.finalizeWaterfallRequested = true; if (pressed && input.activeLayer == SharedInput.LAYER_VOXEL_CLIFF) { input.generateCliffVoxelsRequested = true; if (voxelCliffGenerateBtn != null) voxelCliffGenerateBtn.setDisable(true); @@ -9416,6 +9544,7 @@ public class EditorApp extends Application { || input.activeLayer == SharedInput.LAYER_LOCATION_ZONES || input.activeLayer == SharedInput.LAYER_ZONEN || input.activeLayer == SharedInput.LAYER_WATER + || input.activeLayer == SharedInput.LAYER_WATERFALL || input.activeLayer == SharedInput.LAYER_VOXEL_CLIFF)) input.cancelZoneDrawing = true; } @@ -9439,6 +9568,23 @@ public class EditorApp extends Application { input.deleteVoxelCliffZoneRequested = true; else if (input.activeLayer == SharedInput.LAYER_WATER) input.deleteWaterRequested = true; + else if (input.activeLayer == SharedInput.LAYER_WATERFALL + && input.selectedWaterfallInfo != null) { + Platform.runLater(() -> { + javafx.scene.control.Alert confirm = new javafx.scene.control.Alert( + javafx.scene.control.Alert.AlertType.CONFIRMATION, + "Wasserfall unwiderruflich löschen?", + javafx.scene.control.ButtonType.YES, + javafx.scene.control.ButtonType.NO); + confirm.setTitle("Wasserfall löschen"); + confirm.setHeaderText(null); + confirm.initOwner(primaryStage); + confirm.showAndWait().ifPresent(btn -> { + if (btn == javafx.scene.control.ButtonType.YES) + input.deleteWaterfallRequested = true; + }); + }); + } } } case BACK_SPACE -> { diff --git a/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java b/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java index 952dd24..a78bd81 100644 --- a/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java @@ -24,6 +24,7 @@ import de.blight.editor.state.ModelEditorState; import de.blight.editor.state.EmitterState; import de.blight.editor.state.LocationZoneState; import de.blight.editor.state.RiverEditorState; +import de.blight.editor.state.WaterfallEditorState; import de.blight.editor.state.PlayToolState; import de.blight.editor.state.SoundAreaState; import de.blight.editor.state.WaterBodyState; @@ -206,6 +207,7 @@ public class JmeEditorApp extends SimpleApplication { stateManager.attach(new AreaState(input)); stateManager.attach(new LocationZoneState(input)); stateManager.attach(new RiverEditorState(input)); + stateManager.attach(new WaterfallEditorState(input)); stateManager.attach(new PlayToolState(input)); stateManager.attach(new RoutineMapState(input)); stateManager.attach(new PathNetworkEditorState(input)); diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.java b/blight-editor/src/main/java/de/blight/editor/SharedInput.java index 1adba0c..cf61b7d 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -472,14 +472,20 @@ public class SharedInput { /** activeLayer==12 → Temporären Spawnpunkt setzen und Spiel starten */ public static final int LAYER_PLAY_TOOL = 12; - /** activeLayer==13 → Wasserfälle platzieren */ + /** activeLayer==13 → Wasserfälle platzieren (4-Klick-Quad) */ public static final int LAYER_WATERFALL = 13; + /** activeLayer==40 → Flüsse/Bäche (RiverEditorState) */ + public static final int LAYER_RIVER = 40; // ── Wasserfall-Werkzeug ──────────────────────────────────────────────────── public record WaterfallClick(float screenX, float screenY, boolean rightButton) {} + public record WaterfallDrag(float screenX, float screenY) {} public final ConcurrentLinkedQueue waterfallClickQueue = new ConcurrentLinkedQueue<>(); - public volatile float waterfallNewWidth = 8.0f; - public volatile boolean undoWaterfallPointRequested = false; + public final ConcurrentLinkedQueue waterfallDragQueue = new ConcurrentLinkedQueue<>(); + public volatile boolean waterfallDragEnd = false; + public volatile boolean undoWaterfallPointRequested = false; + public volatile boolean finalizeWaterfallRequested = false; + public volatile float waterfallNewWidth = 8.0f; /** JME → JavaFX: Info des selektierten Wasserfalls. Format: "idx|numPoints|totalLengthM" oder null. */ public volatile String selectedWaterfallInfo = null; @@ -487,11 +493,18 @@ public class SharedInput { /** JavaFX → JME: Selektierten Wasserfall löschen. */ public volatile boolean deleteWaterfallRequested = false; - // ── Wasser-Werkzeug (Polygon) ───────────────────────────────────────────── - /** Klick im Viewport im Wasser-Modus: Punkt setzen oder selektieren. */ + // ── Wasser-Werkzeug (Quad) ──────────────────────────────────────────────── + /** Klick im Viewport im Wasser-Modus: Ecke setzen, Quad abschließen oder selektieren. */ public record WaterClick(float screenX, float screenY, boolean rightButton) {} public final ConcurrentLinkedQueue waterClickQueue = new ConcurrentLinkedQueue<>(); + /** Maus-Drag während Handle-Bearbeitung: absolute Bildschirmposition. */ + public record WaterDrag(float screenX, float screenY) {} + public final ConcurrentLinkedQueue waterDragQueue = new ConcurrentLinkedQueue<>(); + + /** Maus-Release im Wasser-Modus: beendet einen laufenden Handle-Drag. */ + public volatile boolean waterDragEnd = false; + /** * JME → JavaFX: Info der selektierten Wasserfläche. * Format: "idx|waterHeight|pointCount" oder null. @@ -512,6 +525,13 @@ public class SharedInput { /** JavaFX → JME: neue Fließrichtung (Grad 0–359) für selektierte Wasserfläche. null = kein Auftrag. */ public final AtomicReference pendingWaterFlowDegrees = new AtomicReference<>(); + /** + * JavaFX → JME: neue Rendering-Parameter für selektierte Wasserfläche. + * float[10]: [speed, waveScale, waveAmplitude, transparency, + * waterR, waterG, waterB, deepR, deepG, deepB] + */ + public final AtomicReference pendingWaterParams = new AtomicReference<>(); + /** JavaFX → JME: Selektierte Wasserfläche löschen. */ public volatile boolean deleteWaterRequested = false; @@ -1146,15 +1166,23 @@ public class SharedInput { /** JavaFX → JME3: Selektierten Item-Spawn löschen. */ public volatile boolean deleteSelectedItemRequested = false; /** JME3-intern: Auswahl-Klick für ItemPlacementState (kein Objekt/keine Zone getroffen). */ - public final ConcurrentLinkedQueue auswahlClickQueue = new ConcurrentLinkedQueue<>(); + public final ConcurrentLinkedQueue auswahlClickQueue = new ConcurrentLinkedQueue<>(); /** JME3-intern: Auswahl-Klick für LightState (kein Objekt/keine Zone getroffen). */ - public final ConcurrentLinkedQueue auswahlLightClickQueue = new ConcurrentLinkedQueue<>(); + public final ConcurrentLinkedQueue auswahlLightClickQueue = new ConcurrentLinkedQueue<>(); /** JME3-intern: Auswahl-Klick für EmitterState (kein Objekt/keine Zone getroffen). */ - public final ConcurrentLinkedQueue auswahlEmitterClickQueue = new ConcurrentLinkedQueue<>(); + public final ConcurrentLinkedQueue auswahlEmitterClickQueue = new ConcurrentLinkedQueue<>(); + /** JME3-intern: Auswahl-Klick für WaterBodyState. */ + public final ConcurrentLinkedQueue auswahlWaterClickQueue = new ConcurrentLinkedQueue<>(); + /** JME3-intern: Auswahl-Klick für WaterfallEditorState. */ + public final ConcurrentLinkedQueue auswahlWaterfallClickQueue = new ConcurrentLinkedQueue<>(); /** JME3 → JavaFX: Licht im Auswahl-Modus selektiert → Atmosphäre/Licht-Werkzeug aktivieren. */ - public volatile boolean auswahlLightSelected = false; + public volatile boolean auswahlLightSelected = false; /** JME3 → JavaFX: Emitter im Auswahl-Modus selektiert → Atmosphäre/Emitter-Werkzeug aktivieren. */ - public volatile boolean auswahlEmitterSelected = false; + public volatile boolean auswahlEmitterSelected = false; + /** JME3 → JavaFX: Wasserfläche im Auswahl-Modus selektiert → Wasser-Werkzeug aktivieren. */ + public volatile boolean auswahlWaterSelected = false; + /** JME3 → JavaFX: Wasserfall im Auswahl-Modus selektiert → Wasserfall-Werkzeug aktivieren. */ + public volatile boolean auswahlWaterfallSelected = false; // ── Voxel-Textur-Malen ──────────────────────────────────────────────────── /** Parallel-Queue zu textureEditQueue – wird von submitEdit(layer=4) mitbefüllt, diff --git a/blight-editor/src/main/java/de/blight/editor/state/RiverEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/RiverEditorState.java index d6422d2..9585167 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/RiverEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/RiverEditorState.java @@ -89,13 +89,27 @@ public class RiverEditorState extends BaseAppState { @Override public void update(float tpf) { - if (input.activeLayer != SharedInput.LAYER_WATERFALL) return; + if (input.activeLayer != SharedInput.LAYER_RIVER) return; if (input.undoWaterfallPointRequested) { input.undoWaterfallPointRequested = false; undoLastPoint(); } + if (input.finalizeWaterfallRequested) { + input.finalizeWaterfallRequested = false; + if (activeRiver >= 0) finalizeActiveRiver(); + } + + if (input.cancelZoneDrawing) { + input.cancelZoneDrawing = false; + if (activeRiver >= 0) { + cancelActiveRiver(); + } else if (selectedRiver >= 0) { + selectRiver(-1); + } + } + if (input.deleteWaterfallRequested) { input.deleteWaterfallRequested = false; if (selectedRiver >= 0) { @@ -114,7 +128,7 @@ public class RiverEditorState extends BaseAppState { private void handleClick(SharedInput.WaterfallClick click) { if (click.rightButton()) { - finalizeActiveRiver(); + undoLastPoint(); return; } @@ -137,6 +151,7 @@ public class RiverEditorState extends BaseAppState { int nearby = findNearestRiver(hit, 8f); if (nearby >= 0) { selectRiver(nearby); + activeRiver = nearby; // bestehenden Wasserfall bearbeiten (Punkte anhängen) return; } selectRiver(-1); @@ -163,6 +178,13 @@ public class RiverEditorState extends BaseAppState { rebuildActiveRibbon(); } + private void cancelActiveRiver() { + if (activeRiver < 0 || activeRiver >= rivers.size()) return; + removeRiver(activeRiver); + activeRiver = -1; + selectRiver(-1); + } + private void finalizeActiveRiver() { if (activeRiver >= 0 && activeRiver < rivers.size()) { if (rivers.get(activeRiver).size() < 2) { diff --git a/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java b/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java index ce14aff..542f213 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java @@ -729,6 +729,8 @@ public class SceneObjectState extends BaseAppState { input.auswahlClickQueue.offer(click); input.auswahlLightClickQueue.offer(click); input.auswahlEmitterClickQueue.offer(click); + input.auswahlWaterClickQueue.offer(click); + input.auswahlWaterfallClickQueue.offer(click); return; } if (input.activeLayer != SharedInput.LAYER_OBJECTS) { deselectAll(); return; } diff --git a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java index 2478827..1cdf03f 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java @@ -390,6 +390,11 @@ public class TerrainEditorState extends BaseAppState { riverEditorState.setTerrain(terrain); } + WaterfallEditorState waterfallEditorState = app.getStateManager().getState(WaterfallEditorState.class); + if (waterfallEditorState != null) { + waterfallEditorState.setTerrain(terrain); + } + input.loadingStatus = "Baue Szene..."; VoxelEditorState ves = app.getStateManager().getState(VoxelEditorState.class); if (ves != null) ves.setTerrainNode(terrain); @@ -1811,10 +1816,8 @@ public class TerrainEditorState extends BaseAppState { int mode = input.heightTool.mode.getSelectedIndex(); - CollisionResults hits = new CollisionResults(); - terrain.collideWith(ray, hits); - if (hits.size() == 0) continue; - Vector3f contact = hits.getClosestCollision().getContactPoint(); + Vector3f contact = raycastSurface(ray); + if (contact == null) continue; // Plateau-RMB: Höhe von der sichtbaren Oberfläche samplen (Basis + Voxel) if (mode == HeightTool.MODE_PLATEAU && edit.action() < 0) { @@ -1871,6 +1874,15 @@ public class TerrainEditorState extends BaseAppState { } } } + // 4. Gebackene Voxel-Meshes (SculptedMeshEditorState) direkt raycasten + SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class); + if (smes != null) { + Vector3f sp = smes.raycastGeometry(ray); + if (sp != null) { + float vh = sp.y; + if (Float.isFinite(vh)) h = Float.isFinite(h) ? Math.max(h, vh) : vh; + } + } if (Float.isFinite(h)) { input.heightTool.plateauHeight.setValue(h); input.heightTool.plateauHeightChanged = true; diff --git a/blight-editor/src/main/java/de/blight/editor/state/WaterBodyState.java b/blight-editor/src/main/java/de/blight/editor/state/WaterBodyState.java index fd1717f..ee0b3cc 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/WaterBodyState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/WaterBodyState.java @@ -24,25 +24,43 @@ import java.nio.IntBuffer; import java.util.*; /** - * Platziert und visualisiert Wasserflächen als frei definiertes Polygon. + * Platziert und visualisiert Wasserflächen als N-Eck-Polygone. * * Bedienung: - * L-Klick → Polygon-Punkt setzen (erster Punkt definiert Standard-Höhe) - * R-Klick → letzten Punkt entfernen (beim Zeichnen) / Auswahl aufheben - * Leertaste → Terrain-Höhe an Cursor-Position als Wasserhöhe übernehmen - * ESC → Zeichnen abbrechen - * Nahe am ersten Punkt klicken (≥3 Punkte) → Polygon schließen + * L-Klick (leer) → erste Ecke setzen (startet Quad-Zeichnung) + * L-Klick (2.) → Quad abschließen + * Stange ziehen → Ecke oder Kante verschieben + * L-Klick auf Kante (grün) → Kante aufteilen, neuen Punkt einfügen + * Leertaste (Ecke hover) → Terrain-Y dieser Ecke übernehmen (neigt Fläche) + * Leertaste (kein Hover) → globale Terrain-Höhe übernehmen (flacht ab) + * R-Klick / ESC → Zeichnung abbrechen / Auswahl aufheben + * Entf → Selektiertes Polygon löschen */ public class WaterBodyState extends BaseAppState { - private static final float SNAP_DIST = 8f; - private static final float LINE_OFFSET = 0.1f; + private static final float LINE_OFFSET = 0.1f; + private static final float POLE_HEIGHT = 5.0f; + private static final float CAP_SIZE = 1.2f; + private static final float HANDLE_PICK_PX = 24f; + private static final float EDGE_SPLIT_THRESHOLD = 1.5f; - private static final ColorRGBA COLOR_WATER = new ColorRGBA(0.05f, 0.25f, 0.70f, 0.50f); - private static final ColorRGBA COLOR_SELECTED = new ColorRGBA(0.20f, 0.60f, 1.00f, 0.70f); - private static final ColorRGBA COLOR_INPROG = new ColorRGBA(0.3f, 0.7f, 1.0f, 1f); - private static final ColorRGBA COLOR_OUTLINE = new ColorRGBA(0.1f, 0.5f, 0.9f, 1f); - private static final ColorRGBA COLOR_ARROW = new ColorRGBA(1f, 0.85f, 0f, 1f); + private static final ColorRGBA COLOR_WATER = new ColorRGBA(0.05f, 0.25f, 0.70f, 0.50f); + private static final ColorRGBA COLOR_SELECTED = new ColorRGBA(0.20f, 0.60f, 1.00f, 0.70f); + private static final ColorRGBA COLOR_OUTLINE = new ColorRGBA(0.1f, 0.5f, 0.9f, 1f); + private static final ColorRGBA COLOR_SELECTED_OUT = new ColorRGBA(0.8f, 0.9f, 1.0f, 1f); + private static final ColorRGBA COLOR_PREVIEW = new ColorRGBA(0.3f, 0.7f, 1.0f, 1f); + private static final ColorRGBA COLOR_PREVIEW_FILL = new ColorRGBA(0.3f, 0.7f, 1.0f, 0.30f); + private static final ColorRGBA COLOR_ARROW = new ColorRGBA(1f, 0.85f, 0f, 1f); + private static final ColorRGBA COLOR_POLE_CORNER = new ColorRGBA(0f, 0.9f, 1.0f, 1f); + private static final ColorRGBA COLOR_POLE_EDGE = new ColorRGBA(0f, 0.55f, 0.75f, 1f); + private static final ColorRGBA COLOR_POLE_HOVER = new ColorRGBA(1f, 0.9f, 0f, 1f); + private static final ColorRGBA COLOR_EDGE_SPLIT = new ColorRGBA(0.1f, 1.0f, 0.4f, 1f); + + private static final ColorRGBA PILLAR_SUB = new ColorRGBA(0.2f, 0.85f, 1.0f, 1f); + private static final ColorRGBA PILLAR_DRY = new ColorRGBA(1.0f, 0.50f, 0.1f, 1f); + + private enum Mode { IDLE, DRAWING, EDITING } + private Mode mode = Mode.IDLE; private final SharedInput input; private SimpleApplication app; @@ -51,26 +69,45 @@ public class WaterBodyState extends BaseAppState { private Node rootNode; private TerrainQuad terrain; - private final List bodies = new ArrayList<>(); - private final List fillGeos = new ArrayList<>(); - private final List outlineGeos = new ArrayList<>(); - private final List flowArrowGeos = new ArrayList<>(); - private int selectedIdx = -1; + // Platzierte Körper + private final List bodies = new ArrayList<>(); + private final List fillGeos = new ArrayList<>(); + private final List outlineGeos = new ArrayList<>(); + private final List arrowGeos = new ArrayList<>(); + private int selectedIdx = -1; - // in-progress polygon - private boolean placing = false; - private final List currX = new ArrayList<>(); - private final List currZ = new ArrayList<>(); - private float currentWaterHeight = 0f; - private Geometry inProgGeo = null; - private Geometry lastMarker = null; - private Geometry pillarGeo = null; - private Geometry cursorPillar = null; + // Zeichnungs-Zustand (DRAWING) + private float[] drawCorner1 = null; + private float drawHeight = 0f; + private Geometry drawFillGeo = null; + private Geometry drawOutlineGeo = null; + private float lastDrawMx = Float.NaN, lastDrawMz = Float.NaN; + + private float currentWaterHeight = 0f; + + // Handle-System (EDITING) – dynamisch für N-Eck-Polygone + // Index 0..N-1: Ecken, Index N..2N-1: Kantenmittelpunkte + private final List handleGeos = new ArrayList<>(); + private final List handleMats = new ArrayList<>(); + private final List handleCapY = new ArrayList<>(); + private int hoveredHandle = -1; + private int dragHandle = -1; + private boolean isDragging = false; + private float[] liveXs = new float[0]; + private float[] liveYs = new float[0]; + private float[] liveZs = new float[0]; + + // Kanten-Split Hover-Zustand + private int splitHoverEdge = -1; + private float splitHoverX, splitHoverZ; + private Geometry splitHoverGeo = null; + + // Cursor-Pfeiler (während DRAWING) + private Geometry cursorPillar = null; private List pendingLoad = null; public WaterBodyState(SharedInput input) { this.input = input; } - public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; } @Override @@ -94,42 +131,58 @@ public class WaterBodyState extends BaseAppState { @Override public void update(float tpf) { + if (input.activeLayer == SharedInput.LAYER_AUSWAHL) { + SharedInput.ObjectClick ac; + while ((ac = input.auswahlWaterClickQueue.poll()) != null) handleAuswahlClick(ac); + return; + } if (input.activeLayer != SharedInput.LAYER_WATER) { - if (placing) cancelPoly(); + if (mode == Mode.DRAWING) cancelDraw(); + removeCursorPillar(); return; } - // Spacebar: sample terrain height at cursor if (input.waterSampleHeightRequested) { input.waterSampleHeightRequested = false; - float h = sampleTerrainAtCursor(); - if (Float.isFinite(h)) { - currentWaterHeight = h; - input.waterCurrentHeight = h; - input.waterHeightChanged = true; - if (placing) updateInProgressGeo(); - else if (selectedIdx >= 0) applyHeightChange(selectedIdx, h); + // Eck-Handle hover → nur diesen Vertex-Y auf Terrain-Y snappen + if (mode == Mode.EDITING && selectedIdx >= 0 + && hoveredHandle >= 0 && hoveredHandle < liveXs.length) { + float terrY = getHeightAt(liveXs[hoveredHandle], liveZs[hoveredHandle]); + if (Float.isFinite(terrY)) { + liveYs[hoveredHandle] = terrY; + PlacedWater b = bodies.get(selectedIdx); + PlacedWater updated = b.withPoints(liveXs.clone(), liveYs.clone(), liveZs.clone()); + bodies.set(selectedIdx, updated); + rebuildQuadVisual(selectedIdx, liveXs, liveYs, liveZs); + rebuildHandlePositions(liveXs, liveYs, liveZs); + publishSelection(selectedIdx); + } + } else { + // Kein Handle hover → globale Höhe snappen (flacht Fläche ab) + float h = sampleTerrainAtCursor(); + if (Float.isFinite(h)) { + currentWaterHeight = h; + drawHeight = h; + input.waterCurrentHeight = h; + input.waterHeightChanged = true; + if (mode == Mode.EDITING && selectedIdx >= 0) applyHeightChange(selectedIdx, h); + } } } - // Live cursor pillar while placing - if (placing) { - updateCursorPillar(); - } else { - removeCursorPillar(); - } - - // Height change for selected body Float newH = input.pendingWaterHeight.getAndSet(null); if (newH != null && selectedIdx >= 0) applyHeightChange(selectedIdx, newH); - // Flow direction change for selected body Float newFlow = input.pendingWaterFlowDegrees.getAndSet(null); if (newFlow != null && selectedIdx >= 0) applyFlowChange(selectedIdx, newFlow); + float[] newParams = input.pendingWaterParams.getAndSet(null); + if (newParams != null && selectedIdx >= 0) applyParamsChange(selectedIdx, newParams); + if (input.cancelZoneDrawing) { input.cancelZoneDrawing = false; - if (placing) cancelPoly(); + if (mode == Mode.DRAWING) cancelDraw(); + else if (mode == Mode.EDITING) deselect(); } if (input.deleteWaterRequested) { @@ -139,192 +192,762 @@ public class WaterBodyState extends BaseAppState { SharedInput.WaterClick click; while ((click = input.waterClickQueue.poll()) != null) handleClick(click); + + SharedInput.WaterDrag drag; + while ((drag = input.waterDragQueue.poll()) != null) { + if (isDragging) processDragMove(drag.screenX(), drag.screenY()); + } + + if (input.waterDragEnd) { + input.waterDragEnd = false; + if (isDragging) finalizeDrag(); + } + + if (mode == Mode.DRAWING && drawCorner1 != null && input.mouseScreenX >= 0) { + float jmeX = input.mouseScreenX * (float) input.viewportScaleX; + float jmeY = cam.getHeight() - input.mouseScreenY * (float) input.viewportScaleY; + Vector3f pt = raycastAll(buildRay(jmeX, jmeY)); + if (pt != null) updateDrawPreview(pt.x, pt.z); + } + + if (mode == Mode.EDITING && selectedIdx >= 0 && !isDragging && input.mouseScreenX >= 0) { + updateHandleHover(); + } + + if (mode == Mode.DRAWING) updateCursorPillar(); + else removeCursorPillar(); } - // ── Click ───────────────────────────────────────────────────────────────── + // ── Klick-Verarbeitung ──────────────────────────────────────────────────── private void handleClick(SharedInput.WaterClick click) { float jmeX = click.screenX() * (float) input.viewportScaleX; float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY; - Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f); - Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f); - Ray ray = new Ray(near, far.subtract(near).normalizeLocal()); - if (click.rightButton()) { - if (placing) { - if (!currX.isEmpty()) { - currX.remove(currX.size() - 1); - currZ.remove(currZ.size() - 1); - updateInProgressGeo(); - if (currX.isEmpty()) cancelPoly(); - } - } else { - deselect(); - } + if (mode == Mode.DRAWING) cancelDraw(); + else deselect(); return; } + // Handle-Treffer prüfen + if (mode == Mode.EDITING && selectedIdx >= 0) { + PlacedWater b = bodies.get(selectedIdx); + int h = findHandleAt(jmeX, jmeY, b.pointsX(), b.pointsZ()); + if (h >= 0) { + startDrag(h, b.pointsX(), b.pointsY(), b.pointsZ()); + return; + } + } + + Ray ray = buildRay(jmeX, jmeY); Vector3f pt = raycastAll(ray); if (pt == null) return; - float hitX = pt.x, hitZ = pt.z; - if (placing) { - if (currX.size() >= 3) { - float dx = hitX - currX.get(0), dz = hitZ - currZ.get(0); - if (dx * dx + dz * dz < SNAP_DIST * SNAP_DIST * 0.25f) { - closePoly(); - return; - } + // Zweiter Klick im DRAWING-Modus: Quad abschließen + if (mode == Mode.DRAWING && drawCorner1 != null) { + finalizeQuad(pt.x, pt.z); + return; + } + + // Kante des selektierten Körpers aufteilen? + if (mode == Mode.EDITING && selectedIdx >= 0) { + PlacedWater b = bodies.get(selectedIdx); + int edgeIdx = findNearestEdge(pt.x, pt.z, b.pointsX(), b.pointsZ()); + if (edgeIdx >= 0) { + float[] proj = projectOnEdge(pt.x, pt.z, b.pointsX(), b.pointsZ(), edgeIdx); + splitEdge(selectedIdx, edgeIdx, proj[0], proj[1]); + return; } - currX.add(hitX); - currZ.add(hitZ); - updateInProgressGeo(); - } else { - for (int i = 0; i < bodies.size(); i++) { - PlacedWater b = bodies.get(i); - if (pointInPolygon(hitX, hitZ, b.pointsX(), b.pointsZ())) { - selectBody(i); - return; - } + } + + // Bestehenden Körper selektieren + for (int i = 0; i < bodies.size(); i++) { + if (pointInPolygon(pt.x, pt.z, bodies.get(i).pointsX(), bodies.get(i).pointsZ())) { + selectBody(i); + return; + } + } + + // Leere Fläche: neue Quad-Zeichnung starten + if (mode == Mode.EDITING) deselect(); + mode = Mode.DRAWING; + drawCorner1 = new float[]{pt.x, pt.z}; + drawHeight = pt.y; + currentWaterHeight = pt.y; + input.waterCurrentHeight = pt.y; + input.waterHeightChanged = true; + updateDrawPreview(pt.x, pt.z); + } + + private void handleAuswahlClick(SharedInput.ObjectClick click) { + if (click.rightButton()) return; + float jmeX = click.screenX() * (float) input.viewportScaleX; + float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY; + Vector3f pt = raycastAll(buildRay(jmeX, jmeY)); + if (pt == null) return; + for (int i = 0; i < bodies.size(); i++) { + PlacedWater b = bodies.get(i); + if (pointInPolygon(pt.x, pt.z, b.pointsX(), b.pointsZ())) { + selectBody(i); + input.auswahlWaterSelected = true; + return; } - deselect(); - placing = true; - currX.clear(); - currZ.clear(); - currentWaterHeight = pt.y; - input.waterCurrentHeight = pt.y; - input.waterHeightChanged = true; - currX.add(hitX); - currZ.add(hitZ); - updateInProgressGeo(); } } - private void closePoly() { - if (currX.size() < 3) { cancelPoly(); return; } - float[] xs = toArray(currX); - float[] zs = toArray(currZ); - PlacedWater body = new PlacedWater(xs, zs, currentWaterHeight, 0f); + // ── Quad-Zeichnung ──────────────────────────────────────────────────────── + + private void updateDrawPreview(float mx, float mz) { + if (drawCorner1 == null) return; + if (mx == lastDrawMx && mz == lastDrawMz) return; + lastDrawMx = mx; lastDrawMz = mz; + + float x1 = drawCorner1[0], z1 = drawCorner1[1]; + float[] xs = {x1, mx, mx, x1}; + float[] zs = {z1, z1, mz, mz}; + float[] ys = flatYs(4, drawHeight); + + if (drawFillGeo == null) { + drawFillGeo = buildFillGeo(xs, ys, zs, COLOR_PREVIEW_FILL); + rootNode.attachChild(drawFillGeo); + } else { + rebuildFillMesh(drawFillGeo.getMesh(), xs, ys, zs); + } + + if (drawOutlineGeo == null) { + drawOutlineGeo = buildLoopGeo("water_preview_outline", xs, ys, zs, COLOR_PREVIEW); + rootNode.attachChild(drawOutlineGeo); + } else { + rebuildLoopMesh(drawOutlineGeo.getMesh(), xs, ys, zs); + } + } + + private void finalizeQuad(float mx, float mz) { + if (drawCorner1 == null) return; + float x1 = drawCorner1[0], z1 = drawCorner1[1]; + if (Math.abs(mx - x1) < 0.5f || Math.abs(mz - z1) < 0.5f) { + cancelDraw(); + return; + } + float[] xs = {x1, mx, mx, x1}; + float[] zs = {z1, z1, mz, mz}; + // Per-Eckpunkt-Y vom Terrain abtasten, damit Wasserfälle auf Klippen + // automatisch die richtigen Höhenwerte bekommen. + float[] ys = new float[4]; + for (int i = 0; i < 4; i++) { + float h = getHeightAt(xs[i], zs[i]); + ys[i] = Float.isFinite(h) ? h : drawHeight; + } + PlacedWater body = new PlacedWater(xs, ys, zs, drawHeight, 0f, + PlacedWater.DEF_SPEED, PlacedWater.DEF_WAVE_SCALE, PlacedWater.DEF_WAVE_AMPLITUDE, + PlacedWater.DEF_TRANSPARENCY, + PlacedWater.DEF_WATER_R, PlacedWater.DEF_WATER_G, PlacedWater.DEF_WATER_B, + PlacedWater.DEF_DEEP_R, PlacedWater.DEF_DEEP_G, PlacedWater.DEF_DEEP_B); + cancelDraw(); addBody(body); selectBody(bodies.size() - 1); - cancelPoly(); } - private void cancelPoly() { - placing = false; - currX.clear(); - currZ.clear(); - if (inProgGeo != null) { rootNode.detachChild(inProgGeo); inProgGeo = null; } - if (lastMarker != null) { rootNode.detachChild(lastMarker); lastMarker = null; } - if (pillarGeo != null) { rootNode.detachChild(pillarGeo); pillarGeo = null; } - if (cursorPillar != null) { rootNode.detachChild(cursorPillar); cursorPillar = null; } + private void cancelDraw() { + mode = Mode.IDLE; + drawCorner1 = null; + lastDrawMx = Float.NaN; lastDrawMz = Float.NaN; + if (drawFillGeo != null) { rootNode.detachChild(drawFillGeo); drawFillGeo = null; } + if (drawOutlineGeo != null) { rootNode.detachChild(drawOutlineGeo); drawOutlineGeo = null; } } - // ── In-progress visual ──────────────────────────────────────────────────── + // ── Kanten-Split ───────────────────────────────────────────────────────── - private void updateInProgressGeo() { - if (inProgGeo != null) rootNode.detachChild(inProgGeo); - inProgGeo = null; - int n = currX.size(); - if (n > 0) { - inProgGeo = buildLineGeo("water_inprog", currX, currZ, - currentWaterHeight + LINE_OFFSET, COLOR_INPROG, Mesh.Mode.LineStrip); - rootNode.attachChild(inProgGeo); + private static int findNearestEdge(float px, float pz, float[] xs, float[] zs) { + int n = xs.length; + float bestDist = EDGE_SPLIT_THRESHOLD; + int bestEdge = -1; + for (int i = 0; i < n; i++) { + int j = (i + 1) % n; + float ax = xs[i], az = zs[i], bx = xs[j], bz = zs[j]; + float dx = bx - ax, dz = bz - az; + float lenSq = dx * dx + dz * dz; + if (lenSq < 0.001f) continue; + float t = Math.max(0f, Math.min(1f, ((px - ax) * dx + (pz - az) * dz) / lenSq)); + float closestX = ax + t * dx, closestZ = az + t * dz; + float dist = (float) Math.sqrt((px - closestX) * (px - closestX) + (pz - closestZ) * (pz - closestZ)); + if (dist < bestDist) { bestDist = dist; bestEdge = i; } } - updateLastMarker(); - updatePillarGeo(); + return bestEdge; } - private void updateLastMarker() { - if (lastMarker != null) { rootNode.detachChild(lastMarker); lastMarker = null; } - if (currX.isEmpty()) return; - float x = currX.get(currX.size() - 1); - float z = currZ.get(currZ.size() - 1); - float y = currentWaterHeight + LINE_OFFSET + 0.1f; - float s = 1.5f; + private static float[] projectOnEdge(float px, float pz, float[] xs, float[] zs, int edgeIdx) { + int j = (edgeIdx + 1) % xs.length; + float ax = xs[edgeIdx], az = zs[edgeIdx], bx = xs[j], bz = zs[j]; + float dx = bx - ax, dz = bz - az; + float lenSq = dx * dx + dz * dz; + float t = (lenSq < 0.001f) ? 0.5f + : Math.max(0.05f, Math.min(0.95f, ((px - ax) * dx + (pz - az) * dz) / lenSq)); + return new float[]{ ax + t * dx, az + t * dz }; + } - FloatBuffer buf = BufferUtils.createFloatBuffer(4 * 3); - buf.put(x-s).put(y).put(z-s); buf.put(x+s).put(y).put(z+s); - buf.put(x-s).put(y).put(z+s); buf.put(x+s).put(y).put(z-s); + private void splitEdge(int bodyIdx, int edgeIdx, float splitX, float splitZ) { + PlacedWater b = bodies.get(bodyIdx); + float[] oldXs = b.pointsX(), oldYs = b.pointsY(), oldZs = b.pointsZ(); + int n = oldXs.length; + float[] newXs = new float[n + 1]; + float[] newYs = new float[n + 1]; + float[] newZs = new float[n + 1]; + for (int i = 0; i <= edgeIdx; i++) { + newXs[i] = oldXs[i]; newYs[i] = oldYs[i]; newZs[i] = oldZs[i]; + } + newXs[edgeIdx + 1] = splitX; + newYs[edgeIdx + 1] = (oldYs[edgeIdx] + oldYs[(edgeIdx + 1) % n]) * 0.5f; + newZs[edgeIdx + 1] = splitZ; + for (int i = edgeIdx + 1; i < n; i++) { + newXs[i + 1] = oldXs[i]; newYs[i + 1] = oldYs[i]; newZs[i + 1] = oldZs[i]; + } + + PlacedWater updated = b.withPoints(newXs, newYs, newZs); + bodies.set(bodyIdx, updated); + rebuildQuadVisual(bodyIdx, newXs, newYs, newZs); + rebuildArrow(bodyIdx, updated); + + liveXs = newXs.clone(); + liveYs = newYs.clone(); + liveZs = newZs.clone(); + removeHandles(); + buildHandles(newXs, newYs, newZs); + publishSelection(bodyIdx); + } + + // ── Handle-System (Stangen) ─────────────────────────────────────────────── + + private int findHandleAt(float jmeX, float jmeY, float[] xs, float[] zs) { + int n = xs.length; + float bestSq = HANDLE_PICK_PX * HANDLE_PICK_PX; + int bestIdx = -1; + + for (int i = 0; i < handleGeos.size(); i++) { + if (i >= handleCapY.size()) continue; + float capY = handleCapY.get(i); + float hx, hz; + if (i < n) { + hx = xs[i]; hz = zs[i]; + } else { + int ei = i - n; + int j = (ei + 1) % n; + hx = (xs[ei] + xs[j]) * 0.5f; + hz = (zs[ei] + zs[j]) * 0.5f; + } + Vector3f sc = cam.getScreenCoordinates(new Vector3f(hx, capY, hz)); + if (sc.z <= 0 || sc.z >= 1) continue; + float dx = sc.x - jmeX, dy = sc.y - jmeY; + float d = dx * dx + dy * dy; + if (d < bestSq) { bestSq = d; bestIdx = i; } + } + return bestIdx; + } + + private void startDrag(int handleIdx, float[] xs, float[] ys, float[] zs) { + isDragging = true; + dragHandle = handleIdx; + liveXs = xs.clone(); + liveYs = ys.clone(); + liveZs = zs.clone(); + } + + private void processDragMove(float screenX, float screenY) { + if (!isDragging || selectedIdx < 0) return; + float jmeX = screenX * (float) input.viewportScaleX; + float jmeY = cam.getHeight() - screenY * (float) input.viewportScaleY; + Vector3f pt = raycastAll(buildRay(jmeX, jmeY)); + if (pt == null) return; + + int n = liveXs.length; + if (dragHandle < n) { + liveXs[dragHandle] = pt.x; + liveZs[dragHandle] = pt.z; + } else { + int edgeIdx = dragHandle - n; + int ca = edgeIdx, cb = (edgeIdx + 1) % n; + float midX = (liveXs[ca] + liveXs[cb]) * 0.5f; + float midZ = (liveZs[ca] + liveZs[cb]) * 0.5f; + float dX = pt.x - midX, dZ = pt.z - midZ; + liveXs[ca] += dX; liveZs[ca] += dZ; + liveXs[cb] += dX; liveZs[cb] += dZ; + } + + rebuildQuadVisual(selectedIdx, liveXs, liveYs, liveZs); + rebuildHandlePositions(liveXs, liveYs, liveZs); + } + + private void finalizeDrag() { + isDragging = false; + dragHandle = -1; + if (selectedIdx < 0) return; + PlacedWater b = bodies.get(selectedIdx); + + // Alte Positionen merken bevor sie überschrieben werden + float[] oldXs = b.pointsX(), oldYs = b.pointsY(), oldZs = b.pointsZ(); + + PlacedWater updated = b.withPoints(liveXs.clone(), liveYs.clone(), liveZs.clone()); + bodies.set(selectedIdx, updated); + rebuildArrow(selectedIdx, updated); + publishSelection(selectedIdx); + + // Wasserfall-Ecken die an verschobenen See-Vertices hängen mitziehen + syncWaterfallCorners(oldXs, oldYs, oldZs, liveXs, liveYs, liveZs); + } + + private static final float WATERFALL_SNAP_SYNC = 0.15f; + + private void syncWaterfallCorners(float[] oldXs, float[] oldYs, float[] oldZs, + float[] newXs, float[] newYs, float[] newZs) { + WaterfallEditorState wfes = getStateManager().getState(WaterfallEditorState.class); + if (wfes == null) return; + boolean any = false; + for (int vi = 0; vi < oldXs.length; vi++) { + float ox = oldXs[vi], oy = oldYs[vi], oz = oldZs[vi]; + float nx = newXs[vi], ny = newYs[vi], nz = newZs[vi]; + if (Math.abs(nx - ox) < 1e-4f && Math.abs(ny - oy) < 1e-4f && Math.abs(nz - oz) < 1e-4f) continue; + any |= wfes.syncCornersFromVertex(ox, oy, oz, nx, ny, nz, WATERFALL_SNAP_SYNC); + } + if (any) wfes.save(); + } + + private void buildHandles(float[] xs, float[] ys, float[] zs) { + removeHandles(); + int n = xs.length; + for (int i = 0; i < n; i++) { + float terrH = getHeightAt(xs[i], zs[i]); + float capY = computeCapY(ys[i], terrH); + handleCapY.add(capY); + Geometry geo = buildPoleGeo("wh_c" + i, xs[i], zs[i], ys[i], terrH, COLOR_POLE_CORNER); + handleGeos.add(geo); + handleMats.add(geo.getMaterial()); + rootNode.attachChild(geo); + } + for (int i = 0; i < n; i++) { + int j = (i + 1) % n; + float emx = (xs[i] + xs[j]) * 0.5f; + float emy = (ys[i] + ys[j]) * 0.5f; + float emz = (zs[i] + zs[j]) * 0.5f; + float terrH = getHeightAt(emx, emz); + float capY = computeCapY(emy, terrH) - 1f; + handleCapY.add(capY); + Geometry geo = buildPoleGeo("wh_e" + i, emx, emz, emy, terrH - 1f, COLOR_POLE_EDGE); + handleGeos.add(geo); + handleMats.add(geo.getMaterial()); + rootNode.attachChild(geo); + } + } + + private void rebuildHandlePositions(float[] xs, float[] ys, float[] zs) { + int n = xs.length; + for (int i = 0; i < n && i < handleGeos.size(); i++) { + Geometry geo = handleGeos.get(i); + if (geo == null) continue; + float terrH = getHeightAt(xs[i], zs[i]); + handleCapY.set(i, computeCapY(ys[i], terrH)); + rebuildPoleMesh(geo.getMesh(), xs[i], zs[i], ys[i], terrH); + } + for (int i = 0; i < n; i++) { + int hi = n + i; + if (hi >= handleGeos.size()) continue; + Geometry geo = handleGeos.get(hi); + if (geo == null) continue; + int j = (i + 1) % n; + float emx = (xs[i] + xs[j]) * 0.5f; + float emy = (ys[i] + ys[j]) * 0.5f; + float emz = (zs[i] + zs[j]) * 0.5f; + float terrH = getHeightAt(emx, emz) - 1f; + handleCapY.set(hi, computeCapY(emy, terrH)); + rebuildPoleMesh(geo.getMesh(), emx, emz, emy, terrH); + } + } + + private void removeHandles() { + for (Geometry g : handleGeos) if (g != null) rootNode.detachChild(g); + handleGeos.clear(); + handleMats.clear(); + handleCapY.clear(); + hideSplitIndicator(); + hoveredHandle = -1; + splitHoverEdge = -1; + } + + private void updateHandleHover() { + if (selectedIdx < 0) return; + PlacedWater b = bodies.get(selectedIdx); + float[] xs = b.pointsX(), ys = b.pointsY(), zs = b.pointsZ(); + int n = xs.length; + + float jmeX = input.mouseScreenX * (float) input.viewportScaleX; + float jmeY = cam.getHeight() - input.mouseScreenY * (float) input.viewportScaleY; + int h = findHandleAt(jmeX, jmeY, xs, zs); + + if (h != hoveredHandle) { + if (hoveredHandle >= 0 && hoveredHandle < handleMats.size()) { + ColorRGBA c = hoveredHandle < n ? COLOR_POLE_CORNER : COLOR_POLE_EDGE; + handleMats.get(hoveredHandle).setColor("Color", c); + } + if (h >= 0 && h < handleMats.size()) { + handleMats.get(h).setColor("Color", COLOR_POLE_HOVER); + } + hoveredHandle = h; + } + + // Kanten-Split Hover-Indikator + if (h < 0) { + Vector3f cursorPt = raycastAll(buildRay(jmeX, jmeY)); + if (cursorPt != null) { + int nearEdge = findNearestEdge(cursorPt.x, cursorPt.z, xs, zs); + if (nearEdge >= 0) { + float[] proj = projectOnEdge(cursorPt.x, cursorPt.z, xs, zs, nearEdge); + int j = (nearEdge + 1) % n; + float splitSurfaceY = (ys[nearEdge] + ys[j]) * 0.5f; + showSplitIndicator(proj[0], splitSurfaceY, proj[1]); + splitHoverEdge = nearEdge; + splitHoverX = proj[0]; + splitHoverZ = proj[1]; + } else { + hideSplitIndicator(); + splitHoverEdge = -1; + } + } else { + hideSplitIndicator(); + splitHoverEdge = -1; + } + } else { + hideSplitIndicator(); + splitHoverEdge = -1; + } + } + + // ── Split-Indikator ─────────────────────────────────────────────────────── + + private void showSplitIndicator(float x, float surfaceY, float z) { + float y = surfaceY + LINE_OFFSET + 0.1f; + float s = 0.9f; + FloatBuffer pos = BufferUtils.createFloatBuffer(4 * 3); + pos.put(x - s).put(y).put(z) + .put(x + s).put(y).put(z) + .put(x).put(y).put(z - s) + .put(x).put(y).put(z + s); + pos.rewind(); + if (splitHoverGeo == null) { + Mesh mesh = new Mesh(); + mesh.setMode(Mesh.Mode.Lines); + mesh.setBuffer(VertexBuffer.Type.Position, 3, pos); + mesh.updateBound(); + Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", COLOR_EDGE_SPLIT); + mat.getAdditionalRenderState().setLineWidth(2.5f); + splitHoverGeo = new Geometry("water_split_indicator", mesh); + splitHoverGeo.setMaterial(mat); + rootNode.attachChild(splitHoverGeo); + } else { + Mesh mesh = splitHoverGeo.getMesh(); + mesh.clearBuffer(VertexBuffer.Type.Position); + mesh.setBuffer(VertexBuffer.Type.Position, 3, pos); + mesh.updateBound(); + } + } + + private void hideSplitIndicator() { + if (splitHoverGeo != null) { + rootNode.detachChild(splitHoverGeo); + splitHoverGeo = null; + } + } + + // ── Selektion ───────────────────────────────────────────────────────────── + + private void selectBody(int idx) { + deselect(); + selectedIdx = idx; + mode = Mode.EDITING; + fillGeos.get(idx).getMaterial().setColor("Color", COLOR_SELECTED); + outlineGeos.get(idx).getMaterial().setColor("Color", COLOR_SELECTED_OUT); + PlacedWater b = bodies.get(idx); + liveXs = b.pointsX().clone(); + liveYs = b.pointsY().clone(); + liveZs = b.pointsZ().clone(); + buildHandles(b.pointsX(), b.pointsY(), b.pointsZ()); + publishSelection(idx); + } + + private void deselect() { + if (selectedIdx >= 0 && selectedIdx < fillGeos.size()) { + fillGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_WATER); + outlineGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_OUTLINE); + } + removeHandles(); + isDragging = false; + dragHandle = -1; + selectedIdx = -1; + mode = Mode.IDLE; + input.selectedWaterInfo = null; + input.waterSelectionChanged = true; + } + + private void publishSelection(int idx) { + PlacedWater b = bodies.get(idx); + input.selectedWaterInfo = String.format(java.util.Locale.ROOT, + "%d|%.3f|%d|%.1f|%.4f|%.6f|%.5f|%.5f|%.5f|%.5f|%.5f|%.5f|%.5f|%.5f", + idx, b.waterHeight(), b.pointsX().length, b.flowDegrees(), + b.speed(), b.waveScale(), b.waveAmplitude(), b.transparency(), + b.waterColorR(), b.waterColorG(), b.waterColorB(), + b.deepColorR(), b.deepColorG(), b.deepColorB()); + input.waterSelectionChanged = true; + } + + // ── Hinzufügen / Entfernen ──────────────────────────────────────────────── + + private void addBody(PlacedWater body) { + int idx = bodies.size(); + bodies.add(body); + Geometry fill = buildFillGeo(body.pointsX(), body.pointsY(), body.pointsZ(), COLOR_WATER); + Geometry outline = buildLoopGeo("water_outline_" + idx, + body.pointsX(), body.pointsY(), body.pointsZ(), COLOR_OUTLINE); + Geometry arrow = buildFlowArrowGeo(body, idx); + rootNode.attachChild(fill); + rootNode.attachChild(outline); + rootNode.attachChild(arrow); + fillGeos.add(fill); + outlineGeos.add(outline); + arrowGeos.add(arrow); + } + + private void removeBody(int idx) { + removeHandles(); + rootNode.detachChild(fillGeos.get(idx)); + rootNode.detachChild(outlineGeos.get(idx)); + rootNode.detachChild(arrowGeos.get(idx)); + bodies.remove(idx); + fillGeos.remove(idx); + outlineGeos.remove(idx); + arrowGeos.remove(idx); + selectedIdx = -1; + mode = Mode.IDLE; + input.selectedWaterInfo = null; + input.waterSelectionChanged = true; + } + + private void clearAll() { + removeHandles(); + cancelDraw(); + removeCursorPillar(); + if (rootNode != null) { + for (Geometry g : fillGeos) rootNode.detachChild(g); + for (Geometry g : outlineGeos) rootNode.detachChild(g); + for (Geometry g : arrowGeos) rootNode.detachChild(g); + } + bodies.clear(); fillGeos.clear(); outlineGeos.clear(); arrowGeos.clear(); + selectedIdx = -1; + mode = Mode.IDLE; + } + + private void rebuildQuadVisual(int idx, float[] xs, float[] ys, float[] zs) { + rebuildFillMesh(fillGeos.get(idx).getMesh(), xs, ys, zs); + rebuildLoopMesh(outlineGeos.get(idx).getMesh(), xs, ys, zs); + } + + private void rebuildArrow(int idx, PlacedWater body) { + rootNode.detachChild(arrowGeos.get(idx)); + Geometry arrow = buildFlowArrowGeo(body, idx); + rootNode.attachChild(arrow); + arrowGeos.set(idx, arrow); + } + + private void applyHeightChange(int idx, float newH) { + PlacedWater updated = bodies.get(idx).withHeight(newH); // setzt alle pointsY auf newH + bodies.set(idx, updated); + rebuildFillMesh(fillGeos.get(idx).getMesh(), updated.pointsX(), updated.pointsY(), updated.pointsZ()); + rebuildLoopMesh(outlineGeos.get(idx).getMesh(), updated.pointsX(), updated.pointsY(), updated.pointsZ()); + rebuildArrow(idx, updated); + if (selectedIdx == idx) { + liveYs = updated.pointsY().clone(); + rebuildHandlePositions(updated.pointsX(), liveYs, updated.pointsZ()); + } + publishSelection(idx); + } + + private void applyFlowChange(int idx, float newDegrees) { + PlacedWater updated = bodies.get(idx).withFlow(newDegrees); + bodies.set(idx, updated); + rebuildArrow(idx, updated); + publishSelection(idx); + } + + private void applyParamsChange(int idx, float[] p) { + PlacedWater updated = bodies.get(idx).withParams(p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8], p[9]); + bodies.set(idx, updated); + publishSelection(idx); + } + + // ── Geometrie-Builder ───────────────────────────────────────────────────── + + private Geometry buildFillGeo(float[] xs, float[] ys, float[] zs, ColorRGBA color) { + Mesh mesh = new Mesh(); + rebuildFillMesh(mesh, xs, ys, zs); + Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", color.clone()); + mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + mat.getAdditionalRenderState().setDepthWrite(false); + mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); + Geometry geo = new Geometry("water_fill", mesh); + geo.setMaterial(mat); + geo.setQueueBucket(RenderQueue.Bucket.Transparent); + return geo; + } + + private static void rebuildFillMesh(Mesh mesh, float[] xs, float[] ys, float[] zs) { + int n = xs.length; + FloatBuffer pos = BufferUtils.createFloatBuffer(n * 3); + IntBuffer idx = BufferUtils.createIntBuffer((n - 2) * 3); + for (int i = 0; i < n; i++) pos.put(xs[i]).put(ys[i] + LINE_OFFSET * 0.5f).put(zs[i]); + for (int i = 1; i <= n - 2; i++) idx.put(0).put(i).put(i + 1); + pos.rewind(); idx.rewind(); + mesh.clearBuffer(VertexBuffer.Type.Position); + mesh.clearBuffer(VertexBuffer.Type.Index); + mesh.setBuffer(VertexBuffer.Type.Position, 3, pos); + mesh.setBuffer(VertexBuffer.Type.Index, 3, idx); + mesh.updateBound(); + } + + private Geometry buildLoopGeo(String name, float[] xs, float[] ys, float[] zs, ColorRGBA color) { + Mesh mesh = new Mesh(); + rebuildLoopMesh(mesh, xs, ys, zs); + Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", color.clone()); + mat.getAdditionalRenderState().setLineWidth(2f); + Geometry geo = new Geometry(name, mesh); + geo.setMaterial(mat); + return geo; + } + + private static void rebuildLoopMesh(Mesh mesh, float[] xs, float[] ys, float[] zs) { + int n = xs.length; + FloatBuffer pos = BufferUtils.createFloatBuffer(n * 3); + for (int i = 0; i < n; i++) pos.put(xs[i]).put(ys[i] + LINE_OFFSET).put(zs[i]); + pos.rewind(); + mesh.setMode(Mesh.Mode.LineLoop); + mesh.clearBuffer(VertexBuffer.Type.Position); + mesh.setBuffer(VertexBuffer.Type.Position, 3, pos); + mesh.updateBound(); + } + + // ── Stangen-Geometrie ───────────────────────────────────────────────────── + + private static float computeCapY(float waterH, float terrH) { + return Math.max(waterH, terrH) + POLE_HEIGHT; + } + + private Geometry buildPoleGeo(String name, float x, float z, float waterH, float terrH, ColorRGBA color) { + Mesh mesh = new Mesh(); + rebuildPoleMesh(mesh, x, z, waterH, terrH); + Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", color.clone()); + mat.getAdditionalRenderState().setLineWidth(2.5f); + Geometry geo = new Geometry(name, mesh); + geo.setMaterial(mat); + return geo; + } + + private static void rebuildPoleMesh(Mesh mesh, float x, float z, float waterH, float terrH) { + float yBot = Math.min(waterH, terrH) - 0.5f; + float yTop = Math.max(waterH, terrH) + POLE_HEIGHT; + + FloatBuffer buf = BufferUtils.createFloatBuffer(6 * 3); + buf.put(x).put(yBot).put(z); + buf.put(x).put(yTop).put(z); + buf.put(x - CAP_SIZE).put(yTop).put(z); + buf.put(x + CAP_SIZE).put(yTop).put(z); + buf.put(x).put(yTop).put(z - CAP_SIZE); + buf.put(x).put(yTop).put(z + CAP_SIZE); buf.flip(); - Mesh mesh = new Mesh(); mesh.setMode(Mesh.Mode.Lines); + mesh.clearBuffer(VertexBuffer.Type.Position); mesh.setBuffer(VertexBuffer.Type.Position, 3, buf); mesh.updateBound(); - - Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); - mat.setColor("Color", new ColorRGBA(1f, 0.15f, 0.15f, 1f)); - mat.getAdditionalRenderState().setLineWidth(3f); - lastMarker = new Geometry("water_lastpoint", mesh); - lastMarker.setMaterial(mat); - rootNode.attachChild(lastMarker); } - // ── Height-delta pillars ────────────────────────────────────────────────── + // ── Fließ-Pfeile ───────────────────────────────────────────────────────── - private static final ColorRGBA PILLAR_SUB = new ColorRGBA(0.2f, 0.85f, 1.0f, 1f); - private static final ColorRGBA PILLAR_DRY = new ColorRGBA(1.0f, 0.50f, 0.1f, 1f); + private Geometry buildFlowArrowGeo(PlacedWater body, int idx) { + float[] xs = body.pointsX(), zs = body.pointsZ(), ys = body.pointsY(); + int n = xs.length; - private void updatePillarGeo() { - if (pillarGeo != null) { rootNode.detachChild(pillarGeo); pillarGeo = null; } - int n = currX.size(); - if (n == 0) return; - - FloatBuffer pos = BufferUtils.createFloatBuffer(n * 2 * 3); - FloatBuffer col = BufferUtils.createFloatBuffer(n * 2 * 4); - - for (int i = 0; i < n; i++) { - float x = currX.get(i), z = currZ.get(i); - float ty = getHeightAt(x, z); - float wy = currentWaterHeight; - ColorRGBA c = (ty < wy) ? PILLAR_SUB : PILLAR_DRY; - - pos.put(x).put(ty).put(z); - pos.put(x).put(wy + LINE_OFFSET).put(z); - col.put(c.r).put(c.g).put(c.b).put(c.a); - col.put(c.r).put(c.g).put(c.b).put(c.a); + float minX = xs[0], maxX = xs[0], minZ = zs[0], maxZ = zs[0]; + for (int i = 1; i < n; i++) { + if (xs[i] < minX) minX = xs[i]; if (xs[i] > maxX) maxX = xs[i]; + if (zs[i] < minZ) minZ = zs[i]; if (zs[i] > maxZ) maxZ = zs[i]; } - pos.rewind(); col.rewind(); + float avgY = 0f; + for (float y : ys) avgY += y; + avgY /= n; + + float diag = (float) Math.sqrt((double)(maxX-minX)*(maxX-minX) + (double)(maxZ-minZ)*(maxZ-minZ)); + float spacing = Math.max(4f, Math.min(diag / 8f, 30f)); + float arrowLen = spacing * 0.5f; + float hlen = arrowLen * 0.35f; + float y = avgY + 0.3f; + + double rad = Math.toRadians(body.flowDegrees()); + float dx = (float) Math.sin(rad), dz = (float) Math.cos(rad); + float b1x = (float) Math.sin(Math.toRadians(body.flowDegrees() + 150)) * hlen; + float b1z = (float) Math.cos(Math.toRadians(body.flowDegrees() + 150)) * hlen; + float b2x = (float) Math.sin(Math.toRadians(body.flowDegrees() - 150)) * hlen; + float b2z = (float) Math.cos(Math.toRadians(body.flowDegrees() - 150)) * hlen; + + List pts = new ArrayList<>(); + for (float gx = minX + spacing * 0.5f; gx < maxX; gx += spacing) + for (float gz = minZ + spacing * 0.5f; gz < maxZ; gz += spacing) + if (pointInPolygon(gx, gz, xs, zs)) pts.add(new float[]{gx, gz}); + if (pts.isEmpty()) { + float cx = 0, cz = 0; + for (int i = 0; i < n; i++) { cx += xs[i]; cz += zs[i]; } + pts.add(new float[]{cx / n, cz / n}); + } + + FloatBuffer pos = BufferUtils.createFloatBuffer(pts.size() * 6 * 3); + for (float[] pt : pts) { + float px = pt[0] - dx * arrowLen * 0.5f, pz = pt[1] - dz * arrowLen * 0.5f; + float tipX = px + dx * arrowLen, tipZ = pz + dz * arrowLen; + pos.put(px).put(y).put(pz); pos.put(tipX).put(y).put(tipZ); + pos.put(tipX).put(y).put(tipZ); pos.put(tipX + b1x).put(y).put(tipZ + b1z); + pos.put(tipX).put(y).put(tipZ); pos.put(tipX + b2x).put(y).put(tipZ + b2z); + } + pos.flip(); Mesh mesh = new Mesh(); mesh.setMode(Mesh.Mode.Lines); mesh.setBuffer(VertexBuffer.Type.Position, 3, pos); - mesh.setBuffer(VertexBuffer.Type.Color, 4, col); mesh.updateBound(); - Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); - mat.setBoolean("VertexColor", true); - mat.getAdditionalRenderState().setLineWidth(3f); - pillarGeo = new Geometry("water_pillars", mesh); - pillarGeo.setMaterial(mat); - rootNode.attachChild(pillarGeo); + mat.setColor("Color", COLOR_ARROW); + mat.getAdditionalRenderState().setLineWidth(2f); + Geometry geo = new Geometry("water_flow_" + idx, mesh); + geo.setMaterial(mat); + return geo; } + // ── Cursor-Pfeiler ──────────────────────────────────────────────────────── + private void updateCursorPillar() { if (input.mouseScreenX < 0) { removeCursorPillar(); return; } - float jmeX = input.mouseScreenX * (float) input.viewportScaleX; float jmeY = cam.getHeight() - input.mouseScreenY * (float) input.viewportScaleY; - Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f); - Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f); - Ray ray = new Ray(near, far.subtract(near).normalizeLocal()); - Vector3f pt = raycastAll(ray); + Vector3f pt = raycastAll(buildRay(jmeX, jmeY)); if (pt == null) { removeCursorPillar(); return; } - float ty = pt.y, wy = currentWaterHeight; - float delta = wy - ty; - ColorRGBA c = (delta > 0) ? PILLAR_SUB : PILLAR_DRY; - float lo = Math.min(ty, wy); - float hi = Math.max(ty, wy) + LINE_OFFSET; + float ty = pt.y, wy = currentWaterHeight; + ColorRGBA c = (wy > ty) ? PILLAR_SUB : PILLAR_DRY; + float lo = Math.min(ty, wy), hi = Math.max(ty, wy) + LINE_OFFSET; FloatBuffer pos = BufferUtils.createFloatBuffer(2 * 3); FloatBuffer col = BufferUtils.createFloatBuffer(2 * 4); - pos.put(pt.x).put(lo).put(pt.z); - pos.put(pt.x).put(hi).put(pt.z); - col.put(c.r).put(c.g).put(c.b).put(0.5f); - col.put(c.r).put(c.g).put(c.b).put(0.5f); + pos.put(pt.x).put(lo).put(pt.z); pos.put(pt.x).put(hi).put(pt.z); + col.put(c.r).put(c.g).put(c.b).put(0.5f); col.put(c.r).put(c.g).put(c.b).put(0.5f); pos.rewind(); col.rewind(); if (cursorPillar == null) { @@ -346,254 +969,23 @@ public class WaterBodyState extends BaseAppState { mesh.setBuffer(VertexBuffer.Type.Position, 3, pos); mesh.setBuffer(VertexBuffer.Type.Color, 4, col); mesh.updateBound(); - cursorPillar.getMaterial().setBoolean("VertexColor", true); } - - input.waterCurrentHeight = currentWaterHeight; } private void removeCursorPillar() { if (cursorPillar != null) { rootNode.detachChild(cursorPillar); cursorPillar = null; } } - // ── Flow arrow ──────────────────────────────────────────────────────────── - - private Geometry buildFlowArrowGeo(PlacedWater body, int idx) { - float[] xs = body.pointsX(), zs = body.pointsZ(); - int n = xs.length; - - float minX = xs[0], maxX = xs[0], minZ = zs[0], maxZ = zs[0]; - for (int i = 1; i < n; i++) { - if (xs[i] < minX) minX = xs[i]; if (xs[i] > maxX) maxX = xs[i]; - if (zs[i] < minZ) minZ = zs[i]; if (zs[i] > maxZ) maxZ = zs[i]; - } - float diag = (float) Math.sqrt((double)(maxX-minX)*(maxX-minX) + (double)(maxZ-minZ)*(maxZ-minZ)); - float spacing = Math.max(4f, Math.min(diag / 8f, 30f)); - float arrowLen = spacing * 0.5f; - float hlen = arrowLen * 0.35f; - float y = body.waterHeight() + 0.3f; - - double rad = Math.toRadians(body.flowDegrees()); - float dx = (float) Math.sin(rad); - float dz = (float) Math.cos(rad); - // head barb offsets (pre-computed, same for every arrow) - float b1x = (float) Math.sin(Math.toRadians(body.flowDegrees() + 150)) * hlen; - float b1z = (float) Math.cos(Math.toRadians(body.flowDegrees() + 150)) * hlen; - float b2x = (float) Math.sin(Math.toRadians(body.flowDegrees() - 150)) * hlen; - float b2z = (float) Math.cos(Math.toRadians(body.flowDegrees() - 150)) * hlen; - - // collect grid points inside polygon (centered in each cell) - List pts = new ArrayList<>(); - for (float gx = minX + spacing * 0.5f; gx < maxX; gx += spacing) - for (float gz = minZ + spacing * 0.5f; gz < maxZ; gz += spacing) - if (pointInPolygon(gx, gz, xs, zs)) - pts.add(new float[]{gx, gz}); - - // fallback: polygon centroid - if (pts.isEmpty()) { - float cx = 0, cz = 0; - for (int i = 0; i < n; i++) { cx += xs[i]; cz += zs[i]; } - pts.add(new float[]{cx / n, cz / n}); - } - - // 3 lines × 2 verts × 3 floats per arrow - FloatBuffer pos = BufferUtils.createFloatBuffer(pts.size() * 6 * 3); - for (float[] pt : pts) { - float px = pt[0] - dx * arrowLen * 0.5f; // center arrow on grid point - float pz = pt[1] - dz * arrowLen * 0.5f; - float tipX = px + dx * arrowLen, tipZ = pz + dz * arrowLen; - pos.put(px).put(y).put(pz); pos.put(tipX).put(y).put(tipZ); - pos.put(tipX).put(y).put(tipZ); pos.put(tipX + b1x).put(y).put(tipZ + b1z); - pos.put(tipX).put(y).put(tipZ); pos.put(tipX + b2x).put(y).put(tipZ + b2z); - } - pos.flip(); - - Mesh mesh = new Mesh(); - mesh.setMode(Mesh.Mode.Lines); - mesh.setBuffer(VertexBuffer.Type.Position, 3, pos); - mesh.updateBound(); - - Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); - mat.setColor("Color", COLOR_ARROW); - mat.getAdditionalRenderState().setLineWidth(2f); - - Geometry geo = new Geometry("water_flow_" + idx, mesh); - geo.setMaterial(mat); - return geo; - } - - // ── Height sampling ─────────────────────────────────────────────────────── + // ── Terrain-Höhe ───────────────────────────────────────────────────────── private float sampleTerrainAtCursor() { float jmeX = input.mouseScreenX * (float) input.viewportScaleX; float jmeY = cam.getHeight() - input.mouseScreenY * (float) input.viewportScaleY; - Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f); - Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f); - Ray ray = new Ray(near, far.subtract(near).normalizeLocal()); - Vector3f pt = raycastAll(ray); + Vector3f pt = raycastAll(buildRay(jmeX, jmeY)); return pt != null ? pt.y : Float.NaN; } - // ── Selection ───────────────────────────────────────────────────────────── - - private void selectBody(int idx) { - deselect(); - selectedIdx = idx; - fillGeos.get(idx).getMaterial().setColor("Color", COLOR_SELECTED); - outlineGeos.get(idx).getMaterial().setColor("Color", new ColorRGBA(0.8f, 0.9f, 1f, 1f)); - publishSelection(idx); - } - - private void deselect() { - if (selectedIdx >= 0 && selectedIdx < fillGeos.size()) { - fillGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_WATER); - outlineGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_OUTLINE); - } - selectedIdx = -1; - input.selectedWaterInfo = null; - input.waterSelectionChanged = true; - } - - private void publishSelection(int idx) { - PlacedWater b = bodies.get(idx); - input.selectedWaterInfo = String.format(java.util.Locale.ROOT, - "%d|%.3f|%d|%.1f", idx, b.waterHeight(), b.pointsX().length, b.flowDegrees()); - input.waterSelectionChanged = true; - } - - // ── Add / Remove ────────────────────────────────────────────────────────── - - private void addBody(PlacedWater body) { - int idx = bodies.size(); - Geometry fill = buildFillGeo(body); - Geometry outline = buildLineGeo("water_outline_" + idx, - toList(body.pointsX()), toList(body.pointsZ()), - body.waterHeight() + LINE_OFFSET, COLOR_OUTLINE, Mesh.Mode.LineLoop); - Geometry arrow = buildFlowArrowGeo(body, idx); - rootNode.attachChild(fill); - rootNode.attachChild(outline); - rootNode.attachChild(arrow); - bodies.add(body); - fillGeos.add(fill); - outlineGeos.add(outline); - flowArrowGeos.add(arrow); - } - - private void removeBody(int idx) { - rootNode.detachChild(fillGeos.get(idx)); - rootNode.detachChild(outlineGeos.get(idx)); - rootNode.detachChild(flowArrowGeos.get(idx)); - bodies.remove(idx); - fillGeos.remove(idx); - outlineGeos.remove(idx); - flowArrowGeos.remove(idx); - selectedIdx = -1; - input.selectedWaterInfo = null; - input.waterSelectionChanged = true; - } - - private void clearAll() { - for (Geometry g : fillGeos) if (rootNode != null) rootNode.detachChild(g); - for (Geometry g : outlineGeos) if (rootNode != null) rootNode.detachChild(g); - for (Geometry g : flowArrowGeos) if (rootNode != null) rootNode.detachChild(g); - bodies.clear(); - fillGeos.clear(); - outlineGeos.clear(); - flowArrowGeos.clear(); - cancelPoly(); - selectedIdx = -1; - } - - private void applyHeightChange(int idx, float newHeight) { - PlacedWater b = bodies.get(idx); - PlacedWater updated = new PlacedWater(b.pointsX(), b.pointsZ(), newHeight, b.flowDegrees()); - rootNode.detachChild(fillGeos.get(idx)); - rootNode.detachChild(outlineGeos.get(idx)); - rootNode.detachChild(flowArrowGeos.get(idx)); - Geometry fill = buildFillGeo(updated); - Geometry outline = buildLineGeo("water_outline_" + idx, - toList(updated.pointsX()), toList(updated.pointsZ()), - newHeight + LINE_OFFSET, COLOR_OUTLINE, Mesh.Mode.LineLoop); - Geometry arrow = buildFlowArrowGeo(updated, idx); - boolean sel = (selectedIdx == idx); - if (sel) { - fill.getMaterial().setColor("Color", COLOR_SELECTED); - outline.getMaterial().setColor("Color", new ColorRGBA(0.8f, 0.9f, 1f, 1f)); - } - rootNode.attachChild(fill); - rootNode.attachChild(outline); - rootNode.attachChild(arrow); - bodies.set(idx, updated); - fillGeos.set(idx, fill); - outlineGeos.set(idx, outline); - flowArrowGeos.set(idx, arrow); - publishSelection(idx); - } - - private void applyFlowChange(int idx, float newDegrees) { - PlacedWater b = bodies.get(idx); - PlacedWater updated = new PlacedWater(b.pointsX(), b.pointsZ(), b.waterHeight(), newDegrees); - bodies.set(idx, updated); - rootNode.detachChild(flowArrowGeos.get(idx)); - Geometry arrow = buildFlowArrowGeo(updated, idx); - rootNode.attachChild(arrow); - flowArrowGeos.set(idx, arrow); - publishSelection(idx); - } - - // ── Geometry builders ───────────────────────────────────────────────────── - - private Geometry buildFillGeo(PlacedWater body) { - float[] xs = body.pointsX(); - float[] zs = body.pointsZ(); - int n = xs.length; - float h = body.waterHeight() + LINE_OFFSET * 0.5f; - - int triCount = n - 2; - FloatBuffer pos = BufferUtils.createFloatBuffer(n * 3); - IntBuffer idx = BufferUtils.createIntBuffer(triCount * 3); - for (int i = 0; i < n; i++) pos.put(xs[i]).put(h).put(zs[i]); - for (int i = 1; i <= triCount; i++) idx.put(0).put(i).put(i + 1); - pos.rewind(); idx.rewind(); - - Mesh mesh = new Mesh(); - mesh.setBuffer(VertexBuffer.Type.Position, 3, pos); - mesh.setBuffer(VertexBuffer.Type.Index, 3, idx); - mesh.updateBound(); - - Geometry geo = new Geometry("water_fill", mesh); - Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); - mat.setColor("Color", COLOR_WATER); - mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); - mat.getAdditionalRenderState().setDepthWrite(false); - mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); - geo.setMaterial(mat); - geo.setQueueBucket(RenderQueue.Bucket.Transparent); - return geo; - } - - private Geometry buildLineGeo(String name, List xs, List zs, - float height, ColorRGBA color, Mesh.Mode mode) { - int n = xs.size(); - FloatBuffer pos = BufferUtils.createFloatBuffer(n * 3); - for (int i = 0; i < n; i++) pos.put(xs.get(i)).put(height).put(zs.get(i)); - pos.flip(); - - Mesh mesh = new Mesh(); - mesh.setMode(mode); - mesh.setBuffer(VertexBuffer.Type.Position, 3, pos); - mesh.updateBound(); - - Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); - mat.setColor("Color", color); - mat.getAdditionalRenderState().setLineWidth(2f); - - Geometry geo = new Geometry(name, mesh); - geo.setMaterial(mat); - return geo; - } - - // ── Save / Load ─────────────────────────────────────────────────────────── + // ── Speichern / Laden ───────────────────────────────────────────────────── public List getPlacedBodies() { return new ArrayList<>(bodies); } @@ -603,7 +995,19 @@ public class WaterBodyState extends BaseAppState { for (PlacedWater b : loaded) addBody(b); } - // ── Point-in-polygon ────────────────────────────────────────────────────── + // ── Hilfsmethoden ───────────────────────────────────────────────────────── + + private static float[] flatYs(int n, float h) { + float[] ys = new float[n]; + Arrays.fill(ys, h); + return ys; + } + + private Ray buildRay(float jmeX, float jmeY) { + Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f); + Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f); + return new Ray(near, far.subtract(near).normalizeLocal()); + } private static boolean pointInPolygon(float px, float pz, float[] xs, float[] zs) { int n = xs.length; @@ -616,20 +1020,6 @@ public class WaterBodyState extends BaseAppState { return inside; } - // ── Helpers ─────────────────────────────────────────────────────────────── - - private static float[] toArray(List list) { - float[] a = new float[list.size()]; - for (int i = 0; i < list.size(); i++) a[i] = list.get(i); - return a; - } - - private static List toList(float[] arr) { - List l = new ArrayList<>(arr.length); - for (float f : arr) l.add(f); - return l; - } - private Vector3f raycastAll(Ray ray) { Vector3f best = null; float bestDistSq = Float.MAX_VALUE; @@ -663,14 +1053,14 @@ public class WaterBodyState extends BaseAppState { private float getHeightAt(float wx, float wz) { Ray ray = new Ray(new Vector3f(wx, 9999f, wz), new Vector3f(0f, -1f, 0f)); float best = 0f; - float bestDistFromTop = Float.MAX_VALUE; + float bestDist = Float.MAX_VALUE; if (terrain != null) { CollisionResults res = new CollisionResults(); terrain.collideWith(ray, res); if (res.size() > 0) { float y = res.getClosestCollision().getContactPoint().y; float d = 9999f - y; - if (d < bestDistFromTop) { best = y; bestDistFromTop = d; } + if (d < bestDist) { best = y; bestDist = d; } } } VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class); @@ -678,7 +1068,7 @@ public class WaterBodyState extends BaseAppState { Vector3f vp = ves.raycastVoxelGeometry(ray); if (vp != null) { float d = 9999f - vp.y; - if (d < bestDistFromTop) { best = vp.y; bestDistFromTop = d; } + if (d < bestDist) { best = vp.y; bestDist = d; } } } SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class); @@ -686,7 +1076,7 @@ public class WaterBodyState extends BaseAppState { Vector3f sp = smes.raycastGeometry(ray); if (sp != null) { float d = 9999f - sp.y; - if (d < bestDistFromTop) { best = sp.y; } + if (d < bestDist) { best = sp.y; } } } return best; diff --git a/blight-editor/src/main/java/de/blight/editor/state/WaterfallEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/WaterfallEditorState.java new file mode 100644 index 0000000..883042a --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/state/WaterfallEditorState.java @@ -0,0 +1,739 @@ +package de.blight.editor.state; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.BaseAppState; +import com.jme3.collision.CollisionResults; +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.Spatial; +import com.jme3.scene.VertexBuffer; +import com.jme3.scene.shape.Sphere; +import com.jme3.terrain.geomipmap.TerrainQuad; +import com.jme3.util.BufferUtils; +import de.blight.common.PlacedWater; +import de.blight.common.PlacedWaterfall; +import de.blight.common.WaterfallIO; +import de.blight.editor.SharedInput; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * Editor-State für das 4-Klick-Wasserfall-Werkzeug. + * + * Workflow: + * 1–4 Linksklicks setzen Ecken (A=oben-links, B=oben-rechts, C=unten-rechts, D=unten-links). + * Raycast trifft Basis-Terrain, Voxel-Geometrie, SculptedMesh und Seenkanten. + * Selektierter Wasserfall: Ecken per Drag verschiebbar. + * Rechtsklick / Backspace: letzten Punkt rückgängig oder Selektion aufheben. + * Escape: Platzierung abbrechen oder Selektion aufheben. + * Delete: selektierten Wasserfall löschen. + */ +public class WaterfallEditorState extends BaseAppState { + + private static final Logger log = LoggerFactory.getLogger(WaterfallEditorState.class); + + private enum Mode { IDLE, PLACING, EDITING } + + private static final ColorRGBA COL_PLACING = new ColorRGBA(0.2f, 0.6f, 1.0f, 1f); + private static final ColorRGBA COL_SELECTED = new ColorRGBA(1.0f, 0.8f, 0.1f, 1f); + private static final ColorRGBA COL_NORMAL = new ColorRGBA(0.2f, 0.4f, 0.8f, 1f); + private static final float HANDLE_RADIUS = 0.25f; + private static final float HANDLE_PICK_PX = 20f; + private static final float WATER_SNAP_PX = 30f; // screen-Pixel Snap-Radius für Seenkanten + + private final SharedInput input; + + private SimpleApplication app; + private Camera cam; + private Node rootNode; + private TerrainQuad terrain; + + private Mode mode = Mode.IDLE; + + private final List waterfalls = new ArrayList<>(); + private final List wfNodes = new ArrayList<>(); + + // Platzierungs-State + private final List placingCorners = new ArrayList<>(); + private Node placingNode; + + // Editier-State + private int selectedIdx = -1; + private Vector3f[] liveCorners = null; // 4 Ecken A,B,C,D live während Drag + private List handleGeos = new ArrayList<>(); + private Geometry selOutline = null; // live Umriss des selektierten Wf + private boolean isDragging = false; + private int dragHandle = -1; + + public WaterfallEditorState(SharedInput input) { + this.input = input; + } + + public void setTerrain(TerrainQuad t) { this.terrain = t; } + + @Override + protected void initialize(Application app) { + this.app = (SimpleApplication) app; + this.cam = app.getCamera(); + this.rootNode = this.app.getRootNode(); + try { + for (PlacedWaterfall wf : WaterfallIO.load()) { + waterfalls.add(wf); + wfNodes.add(buildWfNode(wf, false)); + } + } catch (Exception e) { + log.error("Wasserfälle nicht ladbar", e); + } + } + + @Override + protected void cleanup(Application app) { + clearPlacing(); + clearEditHandles(); + for (Node n : wfNodes) n.removeFromParent(); + wfNodes.clear(); + } + + @Override protected void onEnable() { setAllCull(Spatial.CullHint.Inherit); } + @Override protected void onDisable() { setAllCull(Spatial.CullHint.Always); } + + @Override + public void update(float tpf) { + if (input.activeLayer == SharedInput.LAYER_AUSWAHL) { + SharedInput.ObjectClick ac; + while ((ac = input.auswahlWaterfallClickQueue.poll()) != null) handleAuswahlClick(ac); + return; + } + if (input.activeLayer != SharedInput.LAYER_WATERFALL) { + if (mode == Mode.PLACING) clearPlacing(); + return; + } + + if (input.cancelZoneDrawing) { + input.cancelZoneDrawing = false; + if (mode == Mode.PLACING) clearPlacing(); + else deselect(); + return; + } + + if (input.undoWaterfallPointRequested) { + input.undoWaterfallPointRequested = false; + if (mode == Mode.PLACING && !placingCorners.isEmpty()) { + placingCorners.remove(placingCorners.size() - 1); + if (placingCorners.isEmpty()) clearPlacing(); + else updatePlacingVisual(); + } + } + + if (input.deleteWaterfallRequested) { + input.deleteWaterfallRequested = false; + if (selectedIdx >= 0) { removeWf(selectedIdx); deselect(); } + } + + SharedInput.WaterfallClick click; + while ((click = input.waterfallClickQueue.poll()) != null) handleClick(click); + + SharedInput.WaterfallDrag drag; + while ((drag = input.waterfallDragQueue.poll()) != null) { + if (isDragging) processDragMove(drag.screenX(), drag.screenY()); + } + + if (input.waterfallDragEnd) { + input.waterfallDragEnd = false; + if (isDragging) finalizeDrag(); + } + } + + // ── Klick ───────────────────────────────────────────────────────────────── + + private void handleClick(SharedInput.WaterfallClick click) { + float jmeX = (float) (click.screenX() * input.viewportScaleX); + float jmeY = cam.getHeight() - (float) (click.screenY() * input.viewportScaleY); + + if (click.rightButton()) { + if (mode == Mode.PLACING && !placingCorners.isEmpty()) { + placingCorners.remove(placingCorners.size() - 1); + if (placingCorners.isEmpty()) clearPlacing(); + else updatePlacingVisual(); + } else { + deselect(); + } + return; + } + + // Selektierter Wasserfall: Handle-Treffer → Drag starten + if (mode == Mode.EDITING && selectedIdx >= 0) { + int h = findHandleAt(jmeX, jmeY); + if (h >= 0) { startDrag(h); return; } + } + + // Raycast + See-Snapping + Ray ray = buildRay(jmeX, jmeY); + Vector3f pt = raycastAll(ray); + Vector3f snapped = snapToWaterVertex(jmeX, jmeY); + if (snapped != null) pt = snapped; + if (pt == null) return; + + if (mode == Mode.PLACING) { + placingCorners.add(pt.clone()); + updatePlacingVisual(); + if (placingCorners.size() == 4) finalizeWaterfall(); + return; + } + + // Vorhandenen Wasserfall selektieren + int nearby = findNearestWf(pt, 3f); + if (nearby >= 0) { selectWf(nearby); return; } + + // Leere Fläche: Platzierung starten + deselect(); + mode = Mode.PLACING; + placingCorners.add(pt.clone()); + updatePlacingVisual(); + } + + // ── Finalisieren / Entfernen ────────────────────────────────────────────── + + private void finalizeWaterfall() { + Vector3f a = placingCorners.get(0), b = placingCorners.get(1); + Vector3f c = placingCorners.get(2), d = placingCorners.get(3); + PlacedWaterfall wf = new PlacedWaterfall( + a.x, a.y, a.z, b.x, b.y, b.z, c.x, c.y, c.z, d.x, d.y, d.z, + PlacedWaterfall.DEF_SPEED, PlacedWaterfall.DEF_TRANSPARENCY, + PlacedWaterfall.DEF_R, PlacedWaterfall.DEF_G, PlacedWaterfall.DEF_B); + clearPlacing(); + waterfalls.add(wf); + wfNodes.add(buildWfNode(wf, false)); + save(); + selectWf(waterfalls.size() - 1); + } + + private void removeWf(int idx) { + if (idx < 0 || idx >= waterfalls.size()) return; + wfNodes.remove(idx).removeFromParent(); + waterfalls.remove(idx); + save(); + } + + // ── Selektion ───────────────────────────────────────────────────────────── + + private void selectWf(int idx) { + // Vorherige Selektion aufheben + if (selectedIdx >= 0 && selectedIdx < wfNodes.size()) { + wfNodes.get(selectedIdx).setCullHint(Spatial.CullHint.Inherit); + } + clearEditHandles(); + + selectedIdx = idx; + mode = (idx >= 0) ? Mode.EDITING : Mode.IDLE; + + if (idx >= 0 && idx < waterfalls.size()) { + // wfNode ausblenden — Handles + selOutline übernehmen die Darstellung + wfNodes.get(idx).setCullHint(Spatial.CullHint.Always); + PlacedWaterfall wf = waterfalls.get(idx); + liveCorners = new Vector3f[]{ + new Vector3f(wf.ax(), wf.ay(), wf.az()), + new Vector3f(wf.bx(), wf.by(), wf.bz()), + new Vector3f(wf.cx(), wf.cy(), wf.cz()), + new Vector3f(wf.dx(), wf.dy(), wf.dz()) + }; + buildHandles(); + buildSelOutline(); + } + publishSelection(idx); + } + + private void deselect() { + if (selectedIdx >= 0 && selectedIdx < wfNodes.size()) { + wfNodes.get(selectedIdx).setCullHint(Spatial.CullHint.Inherit); + } + selectedIdx = -1; + mode = Mode.IDLE; + liveCorners = null; + isDragging = false; + dragHandle = -1; + clearEditHandles(); + publishSelection(-1); + } + + private void publishSelection(int idx) { + input.selectedWaterfallInfo = (idx >= 0 && idx < waterfalls.size()) + ? (idx + "|4|" + String.format(java.util.Locale.ROOT, "%.1f", quadWidth(waterfalls.get(idx)))) + : null; + input.waterfallSelectionChanged = true; + } + + // ── Drag ────────────────────────────────────────────────────────────────── + + private void startDrag(int h) { + isDragging = true; + dragHandle = h; + } + + private void processDragMove(float screenX, float screenY) { + if (!isDragging || selectedIdx < 0 || liveCorners == null) return; + float jmeX = screenX * (float) input.viewportScaleX; + float jmeY = cam.getHeight() - screenY * (float) input.viewportScaleY; + Vector3f pt = raycastAll(buildRay(jmeX, jmeY)); + Vector3f snapped = snapToWaterVertex(jmeX, jmeY); + if (snapped != null) pt = snapped; + if (pt == null) return; + liveCorners[dragHandle].set(pt); + rebuildHandlePositions(); + rebuildSelOutline(); + } + + private void finalizeDrag() { + isDragging = false; + dragHandle = -1; + if (selectedIdx < 0 || liveCorners == null) return; + Vector3f a = liveCorners[0], b = liveCorners[1], c = liveCorners[2], d = liveCorners[3]; + PlacedWaterfall old = waterfalls.get(selectedIdx); + PlacedWaterfall updated = new PlacedWaterfall( + a.x, a.y, a.z, b.x, b.y, b.z, c.x, c.y, c.z, d.x, d.y, d.z, + old.speed(), old.transparency(), old.colorR(), old.colorG(), old.colorB()); + waterfalls.set(selectedIdx, updated); + // wfNode (verborgen) auf neue Positionen updaten, damit er beim Abwählen stimmt + Node old2 = wfNodes.get(selectedIdx); + old2.removeFromParent(); + Node fresh = buildWfNode(updated, false); + fresh.setCullHint(Spatial.CullHint.Always); // bleibt verborgen, solange selektiert + wfNodes.set(selectedIdx, fresh); + save(); + publishSelection(selectedIdx); + } + + // ── Handles ─────────────────────────────────────────────────────────────── + + private void buildHandles() { + if (liveCorners == null) return; + for (int i = 0; i < 4; i++) { + Geometry g = buildSphere("wf_h" + i, liveCorners[i], COL_SELECTED); + handleGeos.add(g); + rootNode.attachChild(g); + } + } + + private void rebuildHandlePositions() { + for (int i = 0; i < 4 && i < handleGeos.size(); i++) { + handleGeos.get(i).setLocalTranslation(liveCorners[i]); + } + } + + private void clearEditHandles() { + for (Geometry g : handleGeos) g.removeFromParent(); + handleGeos.clear(); + if (selOutline != null) { selOutline.removeFromParent(); selOutline = null; } + liveCorners = null; + } + + private int findHandleAt(float jmeX, float jmeY) { + if (liveCorners == null) return -1; + float bestSq = HANDLE_PICK_PX * HANDLE_PICK_PX; + int best = -1; + for (int i = 0; i < 4; i++) { + Vector3f sc = cam.getScreenCoordinates(liveCorners[i]); + if (sc.z <= 0 || sc.z >= 1) continue; + float dx = sc.x - jmeX, dy = sc.y - jmeY; + float d = dx * dx + dy * dy; + if (d < bestSq) { bestSq = d; best = i; } + } + return best; + } + + private void buildSelOutline() { + if (selOutline != null) { selOutline.removeFromParent(); selOutline = null; } + if (liveCorners == null) return; + selOutline = buildLineLoop(toFlatArray(liveCorners), COL_SELECTED); + rootNode.attachChild(selOutline); + } + + private void rebuildSelOutline() { + if (selOutline == null || liveCorners == null) return; + float[] pts = toFlatArray(liveCorners); + selOutline.getMesh().setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(pts)); + selOutline.getMesh().updateBound(); + } + + // ── See-Kanten-Snapping ──────────────────────────────────────────────────── + + /** + * Prüft ob ein Seekanten-Vertex näher als WATER_SNAP_PX Pixel am Cursor liegt. + * Gibt den 3D-Vertex zurück oder null. + */ + private Vector3f snapToWaterVertex(float jmeX, float jmeY) { + WaterBodyState wbs = getStateManager().getState(WaterBodyState.class); + if (wbs == null) return null; + List bodies = wbs.getPlacedBodies(); + if (bodies.isEmpty()) return null; + + float bestSq = WATER_SNAP_PX * WATER_SNAP_PX; + Vector3f best = null; + for (PlacedWater body : bodies) { + float[] xs = body.pointsX(), ys = body.pointsY(), zs = body.pointsZ(); + for (int i = 0; i < xs.length; i++) { + Vector3f sc = cam.getScreenCoordinates(new Vector3f(xs[i], ys[i], zs[i])); + if (sc.z <= 0 || sc.z >= 1) continue; + float dx = sc.x - jmeX, dy = sc.y - jmeY; + float d = dx * dx + dy * dy; + if (d < bestSq) { bestSq = d; best = new Vector3f(xs[i], ys[i], zs[i]); } + } + } + return best; + } + + // ── Raycast ─────────────────────────────────────────────────────────────── + + private Ray buildRay(float jmeX, float jmeY) { + Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f); + Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f); + return new Ray(near, far.subtract(near).normalizeLocal()); + } + + private Vector3f raycastAll(Ray ray) { + Vector3f best = null; + float bestDistSq = Float.MAX_VALUE; + + if (terrain != null) { + CollisionResults res = new CollisionResults(); + terrain.collideWith(ray, res); + if (res.size() > 0) { + Vector3f p = res.getClosestCollision().getContactPoint(); + float d = ray.origin.distanceSquared(p); + if (d < bestDistSq) { best = p; bestDistSq = d; } + } + } + + VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class); + if (ves != null) { + Vector3f vp = ves.raycastVoxelGeometry(ray); + if (vp != null) { + float d = ray.origin.distanceSquared(vp); + if (d < bestDistSq) { best = vp; bestDistSq = d; } + } + } + + SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class); + if (smes != null) { + Vector3f sp = smes.raycastGeometry(ray); + if (sp != null) { + float d = ray.origin.distanceSquared(sp); + if (d < bestDistSq) { best = sp; } + } + } + + return best; + } + + // ── Visuals ─────────────────────────────────────────────────────────────── + + private int findNearestWf(Vector3f pos, float maxDist) { + int best = -1; + float bestD = maxDist * maxDist; + for (int i = 0; i < waterfalls.size(); i++) { + PlacedWaterfall wf = waterfalls.get(i); + float[] pts = { wf.ax(), wf.ay(), wf.az(), wf.bx(), wf.by(), wf.bz(), + wf.cx(), wf.cy(), wf.cz(), wf.dx(), wf.dy(), wf.dz() }; + for (int k = 0; k < 4; k++) { + float dx = pos.x - pts[k*3], dy = pos.y - pts[k*3+1], dz = pos.z - pts[k*3+2]; + float d2 = dx*dx + dy*dy + dz*dz; + if (d2 < bestD) { bestD = d2; best = i; } + } + } + return best; + } + + private Node buildWfNode(PlacedWaterfall wf, boolean selected) { + Node n = new Node("wf_node"); + ColorRGBA col = selected ? COL_SELECTED : COL_NORMAL; + float[] pts = { wf.ax(), wf.ay(), wf.az(), wf.bx(), wf.by(), wf.bz(), + wf.cx(), wf.cy(), wf.cz(), wf.dx(), wf.dy(), wf.dz() }; + n.attachChild(buildFillQuad(wf)); + n.attachChild(buildLineLoop(pts, col)); + rootNode.attachChild(n); + return n; + } + + // Muss mit WaterfallState (game) synchron gehalten werden + private static final int PREVIEW_ROWS = 24; + private static final int PREVIEW_COLS = 6; + private static final float PREVIEW_BULGE_MAX = 1.2f; + private static final float PREVIEW_V_RANGE = 0.30f; + + private Geometry buildFillQuad(PlacedWaterfall wf) { + Mesh mesh = buildBulgeMesh(wf); + Geometry g = new Geometry("wf_fill", mesh); + Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", new ColorRGBA(1f, 1f, 1f, 0.25f)); + mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); + mat.getAdditionalRenderState().setDepthWrite(false); + g.setMaterial(mat); + g.setQueueBucket(RenderQueue.Bucket.Transparent); + return g; + } + + private static Mesh buildBulgeMesh(PlacedWaterfall wf) { + Vector3f a = new Vector3f(wf.ax(), wf.ay(), wf.az()); + Vector3f b = new Vector3f(wf.bx(), wf.by(), wf.bz()); + Vector3f c = new Vector3f(wf.cx(), wf.cy(), wf.cz()); + Vector3f d = new Vector3f(wf.dx(), wf.dy(), wf.dz()); + + Vector3f n1 = d.subtract(a).cross(b.subtract(a)).normalizeLocal(); + Vector3f n2 = b.subtract(c).cross(d.subtract(c)).normalizeLocal(); + Vector3f avgNorm = n1.add(n2).normalizeLocal(); + if (avgNorm.lengthSquared() < 1e-4f) avgNorm.set(0f, 0f, 1f); + Vector3f outward = new Vector3f(avgNorm.x, 0f, avgNorm.z); + if (outward.lengthSquared() < 1e-4f) outward.set(avgNorm); + else outward.normalizeLocal(); + + int vRows = PREVIEW_ROWS + 1, vCols = PREVIEW_COLS + 1; + FloatBuffer pos = BufferUtils.createFloatBuffer(vRows * vCols * 3); + IntBuffer idx = BufferUtils.createIntBuffer(PREVIEW_ROWS * PREVIEW_COLS * 6); + + for (int row = 0; row < vRows; row++) { + float v = (float) row / PREVIEW_ROWS; + float bulge = (v < PREVIEW_V_RANGE) + ? PREVIEW_BULGE_MAX * (float) Math.sin(Math.PI / 2f * v / PREVIEW_V_RANGE) + : PREVIEW_BULGE_MAX; + for (int col = 0; col < vCols; col++) { + float u = (float) col / PREVIEW_COLS; + Vector3f p = a.mult(1f - u).add(b.mult(u)) + .mult(1f - v) + .add(d.mult(1f - u).add(c.mult(u)).mult(v)); + p.addLocal(outward.mult(bulge)); + pos.put(p.x).put(p.y).put(p.z); + } + } + + for (int row = 0; row < PREVIEW_ROWS; row++) { + for (int col = 0; col < PREVIEW_COLS; col++) { + int i0 = row * vCols + col, i1 = i0 + 1, i2 = i0 + vCols, i3 = i2 + 1; + idx.put(i0).put(i2).put(i1); + idx.put(i1).put(i2).put(i3); + } + } + + pos.rewind(); idx.rewind(); + Mesh mesh = new Mesh(); + mesh.setBuffer(VertexBuffer.Type.Position, 3, pos); + mesh.setBuffer(VertexBuffer.Type.Index, 3, idx); + mesh.updateBound(); + mesh.updateCounts(); + return mesh; + } + + private void updatePlacingVisual() { + if (placingNode == null) { + placingNode = new Node("wf_placing"); + rootNode.attachChild(placingNode); + } + placingNode.detachAllChildren(); + for (Vector3f p : placingCorners) { + placingNode.attachChild(buildSphere("wf_ph", p, COL_PLACING)); + } + if (placingCorners.size() >= 2) { + float[] pts = new float[placingCorners.size() * 3]; + for (int i = 0; i < placingCorners.size(); i++) { + pts[i*3] = placingCorners.get(i).x; + pts[i*3+1] = placingCorners.get(i).y; + pts[i*3+2] = placingCorners.get(i).z; + } + placingNode.attachChild(buildLineStrip(pts, COL_PLACING)); + } + } + + private void clearPlacing() { + placingCorners.clear(); + if (placingNode != null) { placingNode.removeFromParent(); placingNode = null; } + if (mode == Mode.PLACING) mode = Mode.IDLE; + } + + private void setAllCull(Spatial.CullHint hint) { + for (int i = 0; i < wfNodes.size(); i++) { + // Selektierter bleibt Always wenn hint=Always, sonst Inherit + if (hint == Spatial.CullHint.Always || i != selectedIdx) { + wfNodes.get(i).setCullHint(hint); + } + } + if (placingNode != null) placingNode.setCullHint(hint); + for (Geometry g : handleGeos) g.setCullHint(hint); + if (selOutline != null) selOutline.setCullHint(hint); + } + + // ── Geometry-Helfer ─────────────────────────────────────────────────────── + + private Geometry buildSphere(String name, Vector3f pos, ColorRGBA col) { + Sphere s = new Sphere(8, 8, HANDLE_RADIUS); + Geometry g = new Geometry(name, s); + Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", col); + g.setMaterial(mat); + g.setLocalTranslation(pos); + g.setQueueBucket(RenderQueue.Bucket.Transparent); + return g; + } + + private Geometry buildLineLoop(float[] pts, ColorRGBA col) { + int n = pts.length / 3; + IntBuffer idx = BufferUtils.createIntBuffer(n * 2); + for (int i = 0; i < n; i++) { idx.put(i).put((i + 1) % n); } + idx.rewind(); + Mesh mesh = new Mesh(); + mesh.setMode(Mesh.Mode.Lines); + mesh.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(pts)); + mesh.setBuffer(VertexBuffer.Type.Index, 2, idx); + mesh.updateBound(); mesh.updateCounts(); + Geometry g = new Geometry("wf_loop", mesh); + Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", col); + mat.getAdditionalRenderState().setLineWidth(2f); + g.setMaterial(mat); + return g; + } + + private Geometry buildLineStrip(float[] pts, ColorRGBA col) { + int n = pts.length / 3; + IntBuffer idx = BufferUtils.createIntBuffer((n - 1) * 2); + for (int i = 0; i < n - 1; i++) { idx.put(i).put(i + 1); } + idx.rewind(); + Mesh mesh = new Mesh(); + mesh.setMode(Mesh.Mode.Lines); + mesh.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(pts)); + mesh.setBuffer(VertexBuffer.Type.Index, 2, idx); + mesh.updateBound(); mesh.updateCounts(); + Geometry g = new Geometry("wf_strip", mesh); + Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", col); + mat.getAdditionalRenderState().setLineWidth(2f); + g.setMaterial(mat); + return g; + } + + private static float[] toFlatArray(Vector3f[] corners) { + float[] pts = new float[corners.length * 3]; + for (int i = 0; i < corners.length; i++) { + pts[i*3] = corners[i].x; pts[i*3+1] = corners[i].y; pts[i*3+2] = corners[i].z; + } + return pts; + } + + // ── Auswahl-Modus ───────────────────────────────────────────────────────── + + private void handleAuswahlClick(SharedInput.ObjectClick click) { + if (click.rightButton()) return; + float jmeX = click.screenX() * (float) input.viewportScaleX; + float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY; + Ray ray = buildRay(jmeX, jmeY); + for (int i = 0; i < waterfalls.size(); i++) { + if (rayHitsWaterfall(ray, waterfalls.get(i))) { + selectWf(i); + input.auswahlWaterfallSelected = true; + return; + } + } + } + + private boolean rayHitsWaterfall(Ray ray, PlacedWaterfall wf) { + Vector3f a = new Vector3f(wf.ax(), wf.ay(), wf.az()); + Vector3f b = new Vector3f(wf.bx(), wf.by(), wf.bz()); + Vector3f c = new Vector3f(wf.cx(), wf.cy(), wf.cz()); + Vector3f d = new Vector3f(wf.dx(), wf.dy(), wf.dz()); + return rayHitsTriangle(ray, a, d, b) || rayHitsTriangle(ray, b, d, c); + } + + /** Möller–Trumbore ray–triangle intersection, beide Seiten (kein Backface-Culling). */ + private static boolean rayHitsTriangle(Ray ray, Vector3f v0, Vector3f v1, Vector3f v2) { + Vector3f e1 = v1.subtract(v0); + Vector3f e2 = v2.subtract(v0); + Vector3f h = ray.direction.cross(e2); + float det = e1.dot(h); + if (Math.abs(det) < 1e-6f) return false; + float inv = 1f / det; + Vector3f s = ray.origin.subtract(v0); + float u = inv * s.dot(h); + if (u < 0f || u > 1f) return false; + Vector3f q = s.cross(e1); + float v = inv * ray.direction.dot(q); + if (v < 0f || u + v > 1f) return false; + float t = inv * e2.dot(q); + return t > 1e-4f; + } + + // ── Externaler Sync (von WaterBodyState) ────────────────────────────────── + + /** + * Verschiebt alle Wasserfall-Ecken die sich näher als {@code threshold} Meter + * an (ox,oy,oz) befinden auf die neue Position (nx,ny,nz). + * Gibt true zurück wenn mindestens eine Ecke geändert wurde (zum Batching des save()-Aufrufs). + * Wird von WaterBodyState.finalizeDrag() aufgerufen. + */ + public boolean syncCornersFromVertex(float ox, float oy, float oz, + float nx, float ny, float nz, float threshold) { + float t2 = threshold * threshold; + boolean anyChanged = false; + for (int i = 0; i < waterfalls.size(); i++) { + PlacedWaterfall wf = waterfalls.get(i); + float[] cx = { wf.ax(), wf.bx(), wf.cx(), wf.dx() }; + float[] cy = { wf.ay(), wf.by(), wf.cy(), wf.dy() }; + float[] cz = { wf.az(), wf.bz(), wf.cz(), wf.dz() }; + boolean wfChanged = false; + for (int c = 0; c < 4; c++) { + float ddx = cx[c] - ox, ddy = cy[c] - oy, ddz = cz[c] - oz; + if (ddx*ddx + ddy*ddy + ddz*ddz < t2) { + cx[c] = nx; cy[c] = ny; cz[c] = nz; + wfChanged = true; + } + } + if (wfChanged) { + PlacedWaterfall upd = new PlacedWaterfall( + cx[0], cy[0], cz[0], cx[1], cy[1], cz[1], + cx[2], cy[2], cz[2], cx[3], cy[3], cz[3], + wf.speed(), wf.transparency(), wf.colorR(), wf.colorG(), wf.colorB()); + waterfalls.set(i, upd); + boolean isSelected = (i == selectedIdx); + wfNodes.remove(i).removeFromParent(); + Node fresh = buildWfNode(upd, false); + wfNodes.add(i, fresh); + if (isSelected) { + fresh.setCullHint(Spatial.CullHint.Always); + if (liveCorners != null) { + liveCorners[0].set(upd.ax(), upd.ay(), upd.az()); + liveCorners[1].set(upd.bx(), upd.by(), upd.bz()); + liveCorners[2].set(upd.cx(), upd.cy(), upd.cz()); + liveCorners[3].set(upd.dx(), upd.dy(), upd.dz()); + rebuildHandlePositions(); + rebuildSelOutline(); + } + } + anyChanged = true; + } + } + return anyChanged; + } + + public void save() { + try { + WaterfallIO.save(waterfalls); + } catch (Exception e) { + log.error("Wasserfall-Speichern fehlgeschlagen", e); + } + } + + private static float quadWidth(PlacedWaterfall wf) { + float dx = wf.bx() - wf.ax(), dy = wf.by() - wf.ay(), dz = wf.bz() - wf.az(); + return (float) Math.sqrt(dx*dx + dy*dy + dz*dz); + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/ui/MapObjectsView.java b/blight-editor/src/main/java/de/blight/editor/ui/MapObjectsView.java index 42c82c6..452ccb0 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/MapObjectsView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/MapObjectsView.java @@ -106,6 +106,7 @@ public class MapObjectsView extends VBox { loadLights(); loadEmitters(); loadWaterBodies(); + loadWaterfalls(); loadRivers(); loadSoundAreas(); loadAreas(); @@ -203,17 +204,35 @@ public class MapObjectsView extends VBox { } catch (Exception ignored) {} } + private void loadWaterfalls() { + try { + List list = WaterfallIO.load(); + TreeItem group = group("Wasserfälle", list.size()); + for (int idx = 0; idx < list.size(); idx++) { + PlacedWaterfall wf = list.get(idx); + float cx = (wf.ax() + wf.bx() + wf.cx() + wf.dx()) / 4f; + float cy = (wf.ay() + wf.by() + wf.cy() + wf.dy()) / 4f; + float cz = (wf.az() + wf.bz() + wf.cz() + wf.dz()) / 4f; + TreeItem item = leaf("Wasserfall #" + (idx + 1) + " " + pos(cx, cy, cz)); + entryMap.put(item, new Entry(cx, cy + 5f, cz, "waterfall", wf, idx)); + group.getChildren().add(item); + } + treeRoot.getChildren().add(group); + } catch (Exception ignored) {} + } + private void loadRivers() { try { List> list = RiverIO.load(); - TreeItem group = group("Wasserfälle", list.size()); + if (list.isEmpty()) return; + TreeItem group = group("Flüsse (veraltet)", list.size()); for (int idx = 0; idx < list.size(); idx++) { List river = list.get(idx); float cx = 0f, cy = 0f, cz = 0f; for (RiverPoint pt : river) { cx += pt.x(); cy += pt.y(); cz += pt.z(); } if (!river.isEmpty()) { cx /= river.size(); cy /= river.size(); cz /= river.size(); } - TreeItem item = leaf("Wasserfall #" + (idx + 1) + " " + pos(cx, cy, cz)); - entryMap.put(item, new Entry(cx, cy + 5f, cz, "waterfall", river, idx)); + TreeItem item = leaf("Fluss #" + (idx + 1) + " " + pos(cx, cy, cz)); + entryMap.put(item, new Entry(cx, cy + 5f, cz, "river", river, idx)); group.getChildren().add(item); } treeRoot.getChildren().add(group); diff --git a/blight-game/src/main/java/de/blight/game/config/GraphicsSettings.java b/blight-game/src/main/java/de/blight/game/config/GraphicsSettings.java index afece6f..fe8a111 100644 --- a/blight-game/src/main/java/de/blight/game/config/GraphicsSettings.java +++ b/blight-game/src/main/java/de/blight/game/config/GraphicsSettings.java @@ -44,7 +44,7 @@ public class GraphicsSettings { // mapSize splits lambda zExtend zFade backface ssaoR ssaoI LOW ( 1024, 2, 0.70f, 40f, 5f, false, 0f, 0f ), MEDIUM ( 2048, 3, 0.75f, 60f, 8f, true, 1.0f, 1.5f), - HIGH ( 4096, 4, 0.80f, 80f, 10f, true, 1.5f, 2.5f); + HIGH ( 4096, 4, 0.70f, 50f, 8f, true, 1.5f, 2.5f); public final int shadowMapSize; public final int splits; diff --git a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java index a92486a..1cbb70f 100644 --- a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java +++ b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java @@ -258,6 +258,7 @@ public class WorldScene extends BaseAppState { BlightGame.status("Lade Welt-Objekte..."); app.getStateManager().attach(new RiverState()); + app.getStateManager().attach(new de.blight.game.state.WaterfallState()); WorldObjectsState worldObjects = new WorldObjectsState(); worldObjects.setLodFactor((float) graphicsSettings.viewDistance.lod0Range); app.getStateManager().attach(worldObjects); diff --git a/blight-game/src/main/java/de/blight/game/state/WaterBodyState.java b/blight-game/src/main/java/de/blight/game/state/WaterBodyState.java index 95ac5ee..63e2f9f 100644 --- a/blight-game/src/main/java/de/blight/game/state/WaterBodyState.java +++ b/blight-game/src/main/java/de/blight/game/state/WaterBodyState.java @@ -3,30 +3,53 @@ package de.blight.game.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.material.Material; +import com.jme3.material.RenderState; import com.jme3.math.ColorRGBA; import com.jme3.math.Vector2f; import com.jme3.math.Vector3f; import com.jme3.post.FilterPostProcessor; +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.texture.Texture; +import com.jme3.util.BufferUtils; import de.blight.common.PlacedWater; import de.blight.common.WaterBodyIO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; import java.util.ArrayList; import java.util.List; /** - * Rendert Polygon-Wasserflächen per WaterPolygonFilter (ein Filter pro Fläche). - * Identisches Aussehen wie WaterFilter, aber auf das eingezeichnete Polygon beschränkt. + * Rendert Polygon-Wasserflächen. + * + * Flache Flächen (isFlat) → WaterPolygonFilter. + * Abschüssige Flächen → FlowingWater-Shader auf Polygon-Mesh. + * Die Kanten werden tesselliert, Y wird linear zwischen den Editor-Eckpunkten interpoliert. */ public class WaterBodyState extends BaseAppState { private static final Logger log = LoggerFactory.getLogger(WaterBodyState.class); - private static final Vector3f SUN_DIR = new Vector3f(-0.5f, -1f, -0.5f).normalizeLocal(); + private static final Vector3f SUN_DIR = new Vector3f(-0.5f, -1f, -0.5f).normalizeLocal(); + private static final float UV_SCALE = 0.12f; + private static final float TESS_STEP = 0.8f; // Tessellierungs-Abstand in Welteinheiten + private static final float LIFT = 0.03f; // Abstand über gespeicherter Oberfläche private final FilterPostProcessor fpp; - private final List filters = new ArrayList<>(); + private final List filters = new ArrayList<>(); + private final List inclinedGeos = new ArrayList<>(); + private final List animatedMaterials = new ArrayList<>(); + private float time = 0f; + + private Node rootNode; public WaterBodyState(FilterPostProcessor fpp) { this.fpp = fpp; @@ -34,7 +57,8 @@ public class WaterBodyState extends BaseAppState { @Override protected void initialize(Application app) { - SimpleApplication sa = (SimpleApplication) app; + rootNode = ((SimpleApplication) app).getRootNode(); + AssetManager assets = app.getAssetManager(); List bodies; try { @@ -45,37 +69,221 @@ public class WaterBodyState extends BaseAppState { } if (bodies.isEmpty()) return; + int flat = 0, inclined = 0; for (PlacedWater body : bodies) { - float[] xs = body.pointsX(); - float[] zs = body.pointsZ(); - if (xs.length < 3) continue; + if (body.pointsX().length < 3) continue; try { - WaterPolygonFilter f = new WaterPolygonFilter( - sa.getRootNode(), SUN_DIR, body.waterHeight(), xs, zs); - f.setWaterColor(new ColorRGBA(0.05f, 0.25f, 0.55f, 1f)); - f.setDeepWaterColor(new ColorRGBA(0.02f, 0.12f, 0.30f, 1f)); - f.setWaterTransparency(0.15f); - f.setMaxAmplitude(0.1f); // reduced to keep visual height close to waterHeight - f.setWaveScale(0.008f); - f.setSpeed(0.5f); - float rad = (float) Math.toRadians(body.flowDegrees()); - f.setWindDirection(new Vector2f((float) Math.sin(rad), (float) Math.cos(rad))); - fpp.addFilter(f); - filters.add(f); - log.info("Wasserfläche geladen: {} Punkte, h={}", xs.length, body.waterHeight()); + if (body.isFlat()) { + buildFlatWater(body, assets); + flat++; + } else { + buildInclinedWater(body, assets); + inclined++; + } } catch (Exception e) { log.error("Fehler beim Laden einer Wasserfläche", e); } } - log.info("{}/{} Wasserfläche(n) geladen.", filters.size(), bodies.size()); + log.info("{} flache + {} abschüssige Wasserfläche(n) geladen.", flat, inclined); + } + + @Override + public void update(float tpf) { + if (!animatedMaterials.isEmpty()) { + time += tpf; + for (Material m : animatedMaterials) { + m.setFloat("Time", time); + } + } } @Override protected void cleanup(Application app) { for (WaterPolygonFilter f : filters) fpp.removeFilter(f); filters.clear(); + for (Geometry g : inclinedGeos) rootNode.detachChild(g); + inclinedGeos.clear(); + animatedMaterials.clear(); } @Override protected void onEnable() {} @Override protected void onDisable() {} + + // ── Flaches Wasser (WaterPolygonFilter) ─────────────────────────────────── + + private void buildFlatWater(PlacedWater body, AssetManager assets) { + float[] xs = body.pointsX(), zs = body.pointsZ(); + WaterPolygonFilter f = new WaterPolygonFilter( + rootNode, SUN_DIR, body.waterHeight(), xs, zs); + f.setWaterColor(new ColorRGBA(body.waterColorR(), body.waterColorG(), body.waterColorB(), 1f)); + f.setDeepWaterColor(new ColorRGBA(body.deepColorR(), body.deepColorG(), body.deepColorB(), 1f)); + f.setWaterTransparency(body.transparency()); + f.setMaxAmplitude(body.waveAmplitude()); + f.setWaveScale(body.waveScale()); + f.setSpeed(body.speed()); + float rad = (float) Math.toRadians(body.flowDegrees()); + f.setWindDirection(new Vector2f(-(float) Math.sin(rad), -(float) Math.cos(rad))); + fpp.addFilter(f); + filters.add(f); + } + + // ── Abschüssiges Wasser (FlowingWater-Shader + Polygon-Mesh) ───────────── + + private void buildInclinedWater(PlacedWater body, AssetManager assets) { + Mesh mesh = buildPointBasedMesh(body); + if (mesh == null) return; + + Material mat = buildInclinedMaterial(body, assets); + Geometry geo = new Geometry("inclined_water", mesh); + geo.setMaterial(mat); + geo.setQueueBucket(RenderQueue.Bucket.Transparent); + rootNode.attachChild(geo); + inclinedGeos.add(geo); + if (mat.getParam("Time") != null) { + animatedMaterials.add(mat); + } + } + + /** + * Baut das Wasserfall-Mesh aus den gespeicherten Eckpunkt-Koordinaten. + * Y wird zwischen Nachbar-Ecken linear interpoliert – kein Terrain-Raycast, + * da vertikale Klippen-Faces von senkrechten Strahlen nicht zuverlässig + * getroffen werden. Der Editor platziert die Punkte bereits korrekt auf der + * Terrain-Oberfläche. + */ + private Mesh buildPointBasedMesh(PlacedWater body) { + float[] xs = body.pointsX(), ys = body.pointsY(), zs = body.pointsZ(); + int n = xs.length; + if (n < 3) return null; + + // ── Rand tessellieren — Y linear zwischen Editor-Eckpunkten ────────── + List rim = new ArrayList<>(); + for (int i = 0; i < n; i++) { + int j = (i + 1) % n; + float ax = xs[i], ay = ys[i] + LIFT, az = zs[i]; + float bx = xs[j], by = ys[j] + LIFT, bz = zs[j]; + float dx = bx - ax, dy = by - ay, dz = bz - az; + float horizLen = (float) Math.sqrt(dx * dx + dz * dz); + int K = Math.max(1, (int) Math.ceil(horizLen / TESS_STEP)); + for (int k = 0; k < K; k++) { + float t = (float) k / K; + rim.add(new float[]{ ax + t * dx, ay + t * dy, az + t * dz }); + } + } + + int P = rim.size(); + if (P < 3) return null; + + // ── Zentrum: Durchschnitt der Eckpunkte (nicht der tessellierten Rand-Punkte) ── + float cx = 0, cy = 0, cz = 0; + for (int i = 0; i < n; i++) { cx += xs[i]; cy += ys[i] + LIFT; cz += zs[i]; } + cx /= n; cy /= n; cz /= n; + + // ── Y-Range für UV (V=0 oben, V=1 unten → Shader scrollt bergab) ──── + float minY = cy, maxY = cy; + for (float[] r : rim) { + if (r[1] < minY) minY = r[1]; + if (r[1] > maxY) maxY = r[1]; + } + float yRange = maxY - minY; + if (yRange < 0.01f) yRange = 1f; + + // ── Durchschnittliche Flächennormale ────────────────────────────────── + Vector3f avgNorm = new Vector3f(); + for (int k = 0; k < P; k++) { + float[] a = rim.get(k), b = rim.get((k + 1) % P); + Vector3f e1 = new Vector3f(a[0] - cx, a[1] - cy, a[2] - cz); + Vector3f e2 = new Vector3f(b[0] - cx, b[1] - cy, b[2] - cz); + avgNorm.addLocal(e1.cross(e2)); + } + if (avgNorm.lengthSquared() < 1e-6f) avgNorm.set(0f, 1f, 0f); + else avgNorm.normalizeLocal(); + if (avgNorm.y < 0) avgNorm.negateLocal(); + + // ── Buffer befüllen ─────────────────────────────────────────────────── + int vertCount = P + 1; + FloatBuffer pos = BufferUtils.createFloatBuffer(vertCount * 3); + FloatBuffer norm = BufferUtils.createFloatBuffer(vertCount * 3); + FloatBuffer uv = BufferUtils.createFloatBuffer(vertCount * 2); + IntBuffer idx = BufferUtils.createIntBuffer(P * 3); + + pos.put(cx).put(cy).put(cz); + norm.put(avgNorm.x).put(avgNorm.y).put(avgNorm.z); + uv.put(cx * UV_SCALE).put((maxY - cy) / yRange); + + for (float[] r : rim) { + pos.put(r[0]).put(r[1]).put(r[2]); + norm.put(avgNorm.x).put(avgNorm.y).put(avgNorm.z); + uv.put(r[0] * UV_SCALE).put((maxY - r[1]) / yRange); + } + + for (int k = 0; k < P; k++) { + idx.put(0).put(k + 1).put((k + 1) % P + 1); + } + + pos.rewind(); norm.rewind(); uv.rewind(); idx.rewind(); + + Mesh mesh = new Mesh(); + mesh.setBuffer(VertexBuffer.Type.Position, 3, pos); + mesh.setBuffer(VertexBuffer.Type.Normal, 3, norm); + mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, uv); + mesh.setBuffer(VertexBuffer.Type.Index, 3, idx); + mesh.updateBound(); + mesh.updateCounts(); + log.debug("Wasserfall-Mesh: {} Rand-Punkte, Y {}-{}", P, + String.format("%.1f", minY), String.format("%.1f", maxY)); + return mesh; + } + + // ── Material ────────────────────────────────────────────────────────────── + + private Material buildInclinedMaterial(PlacedWater body, AssetManager assets) { + Material mat; + try { + mat = new Material(assets, "MatDefs/FlowingWater.j3md"); + + Texture nm = loadTextureOr(assets, + "Textures/internal/water/waterfall_normal.png", + "Common/MatDefs/Water/Textures/water_normalmap.png"); + if (nm != null) { + nm.setWrap(Texture.WrapMode.Repeat); + mat.setTexture("NormalMap", nm); + } + + Texture diff = loadTextureOr(assets, + "Textures/internal/water/waterfall_diffuse.png", null); + if (diff != null) { + diff.setWrap(Texture.WrapMode.Repeat); + mat.setTexture("DiffuseMap", diff); + } + + ColorRGBA tint = new ColorRGBA( + body.waterColorR(), body.waterColorG(), body.waterColorB(), body.transparency()); + mat.setColor("Tint", tint); + mat.setFloat("UVScale", 1.0f); + mat.setFloat("NormalUVScale", 0.5f); + mat.setFloat("FlowSpeed", body.speed() * 1.5f); + mat.setFloat("FoamAmount", 0.25f); + mat.setFloat("Time", 0f); + } catch (Exception e) { + log.warn("FlowingWater-Material nicht ladbar, Fallback", e); + mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", new ColorRGBA( + body.waterColorR(), body.waterColorG(), body.waterColorB(), body.transparency())); + } + + mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); + mat.getAdditionalRenderState().setDepthWrite(false); + return mat; + } + + private static Texture loadTextureOr(AssetManager assets, String primary, String fallback) { + try { return assets.loadTexture(primary); } catch (Exception ignored) {} + if (fallback == null) return null; + try { return assets.loadTexture(fallback); } catch (Exception e) { + log.warn("Textur nicht ladbar: {} und {}", primary, fallback); + return null; + } + } } diff --git a/blight-game/src/main/java/de/blight/game/state/WaterfallState.java b/blight-game/src/main/java/de/blight/game/state/WaterfallState.java new file mode 100644 index 0000000..69859ae --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/state/WaterfallState.java @@ -0,0 +1,231 @@ +package de.blight.game.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.material.Material; +import com.jme3.material.RenderState; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector3f; +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.texture.Texture; +import com.jme3.util.BufferUtils; +import de.blight.common.PlacedWaterfall; +import de.blight.common.WaterfallIO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * Rendert Wasserfall-Quads (4-Eckpunkt-Definitionen aus blight_waterfall.blwf). + * UV V=0 oben, V=1 unten — FlowingWater-Shader scrollt bergab. + */ +public class WaterfallState extends BaseAppState { + + private static final Logger log = LoggerFactory.getLogger(WaterfallState.class); + + private static final int ROWS = 24; // Tessellierungs-Zeilen vertikal + private static final int COLS = 6; // Tessellierungs-Spalten horizontal + private static final float BULGE_MAX = 1.2f; // Maximale horizontale Vorwölbung am oberen Rand (m) + private static final float V_RANGE = 0.30f; // Anteil der Wasserfallhöhe für den Bogen (0..1) + + private Node rootNode; + private final List geos = new ArrayList<>(); + private final List materials = new ArrayList<>(); + private float time = 0f; + + @Override + protected void initialize(Application app) { + rootNode = ((SimpleApplication) app).getRootNode(); + AssetManager assets = app.getAssetManager(); + + List list; + try { + list = WaterfallIO.load(); + } catch (Exception e) { + log.error("Wasserfälle nicht ladbar", e); + return; + } + if (list.isEmpty()) return; + + for (PlacedWaterfall wf : list) { + try { + buildWaterfall(wf, assets); + } catch (Exception e) { + log.error("Fehler beim Aufbauen eines Wasserfalls", e); + } + } + log.info("{} Wasserfall-Quad(s) geladen.", list.size()); + } + + @Override + public void update(float tpf) { + if (!materials.isEmpty()) { + time += tpf; + for (Material m : materials) { + m.setFloat("Time", time); + } + } + } + + @Override + protected void cleanup(Application app) { + for (Geometry g : geos) g.removeFromParent(); + geos.clear(); + materials.clear(); + } + + @Override protected void onEnable() {} + @Override protected void onDisable() {} + + private void buildWaterfall(PlacedWaterfall wf, AssetManager assets) { + Mesh mesh = buildQuadMesh(wf); + Material mat = buildMaterial(wf, assets); + + Geometry geo = new Geometry("waterfall_quad", mesh); + geo.setMaterial(mat); + geo.setQueueBucket(RenderQueue.Bucket.Transparent); + rootNode.attachChild(geo); + geos.add(geo); + materials.add(mat); + } + + /** + * Tesselliertes Quad-Mesh aus 4 Eckpunkten. + * Bilineare Interpolation zwischen A,B,C,D. + * A=oben-links, B=oben-rechts, C=unten-rechts, D=unten-links. + * Die oberen V_RANGE Anteile erhalten eine Viertel-Sinus-Vorwölbung (sofortige Wölbung an der + * Oberkante, glatter Übergang bei V_RANGE), danach fällt das Mesh senkrecht mit BULGE_MAX-Offset. + */ + private static Mesh buildQuadMesh(PlacedWaterfall wf) { + Vector3f a = new Vector3f(wf.ax(), wf.ay(), wf.az()); + Vector3f b = new Vector3f(wf.bx(), wf.by(), wf.bz()); + Vector3f c = new Vector3f(wf.cx(), wf.cy(), wf.cz()); + Vector3f d = new Vector3f(wf.dx(), wf.dy(), wf.dz()); + + int vRows = ROWS + 1, vCols = COLS + 1; + int vertCount = vRows * vCols; + int triCount = ROWS * COLS * 2; + + // Referenz-Normale (nach außen zeigend) aus Winding A top-left, B top-right, D bottom-left + Vector3f n1 = d.subtract(a).cross(b.subtract(a)).normalizeLocal(); + Vector3f n2 = b.subtract(c).cross(d.subtract(c)).normalizeLocal(); + Vector3f avgNorm = n1.add(n2).normalizeLocal(); + if (avgNorm.lengthSquared() < 1e-4f) avgNorm.set(0f, 0f, 1f); + + // Horizontale Outward-Richtung für die Wölbung (Y=0, normiert) + Vector3f outward = new Vector3f(avgNorm.x, 0f, avgNorm.z); + if (outward.lengthSquared() < 1e-4f) outward.set(avgNorm); // Fallback für horizontale Fläche + else outward.normalizeLocal(); + + // Pass 1: alle Vertex-Positionen mit Bulge berechnen und zwischenspeichern + Vector3f[][] positions = new Vector3f[vRows][vCols]; + for (int row = 0; row < vRows; row++) { + float v = (float) row / ROWS; + // Viertel-Sinus: sofortige Wölbung an der Oberkante (Steigung max bei v=0), + // Steigung=0 bei v=V_RANGE → glatter Übergang zum konstanten senkrechten Bereich. + float bulgeVal = (v < V_RANGE) + ? BULGE_MAX * (float) Math.sin(Math.PI / 2f * v / V_RANGE) + : BULGE_MAX; + for (int col = 0; col < vCols; col++) { + float u = (float) col / COLS; + Vector3f top = a.mult(1f - u).add(b.mult(u)); + Vector3f bot = d.mult(1f - u).add(c.mult(u)); + Vector3f p = top.mult(1f - v).add(bot.mult(v)); + p.addLocal(outward.mult(bulgeVal)); + positions[row][col] = p; + } + } + + // Pass 2: Normalen per Finite-Differenzen, Vorzeichen gegen avgNorm gesichert + FloatBuffer pos = BufferUtils.createFloatBuffer(vertCount * 3); + FloatBuffer norm = BufferUtils.createFloatBuffer(vertCount * 3); + FloatBuffer uv = BufferUtils.createFloatBuffer(vertCount * 2); + + for (int row = 0; row < vRows; row++) { + for (int col = 0; col < vCols; col++) { + Vector3f p = positions[row][col]; + pos.put(p.x).put(p.y).put(p.z); + + Vector3f left = positions[row][Math.max(col - 1, 0)]; + Vector3f right = positions[row][Math.min(col + 1, vCols - 1)]; + Vector3f up = positions[Math.max(row - 1, 0)][col]; + Vector3f down = positions[Math.min(row + 1, vRows - 1)][col]; + + Vector3f dU = right.subtract(left); + Vector3f dV = down.subtract(up); + Vector3f n = dU.cross(dV).normalizeLocal(); + if (n.lengthSquared() < 1e-6f) n.set(avgNorm); + if (n.dot(avgNorm) < 0f) n.negateLocal(); + + norm.put(n.x).put(n.y).put(n.z); + uv.put((float) col / COLS).put((float) row / ROWS); + } + } + + IntBuffer idx = BufferUtils.createIntBuffer(triCount * 3); + for (int row = 0; row < ROWS; row++) { + for (int col = 0; col < COLS; col++) { + int i0 = row * vCols + col; + int i1 = i0 + 1; + int i2 = i0 + vCols; + int i3 = i2 + 1; + idx.put(i0).put(i2).put(i1); + idx.put(i1).put(i2).put(i3); + } + } + + pos.rewind(); norm.rewind(); uv.rewind(); idx.rewind(); + + Mesh mesh = new Mesh(); + mesh.setBuffer(VertexBuffer.Type.Position, 3, pos); + mesh.setBuffer(VertexBuffer.Type.Normal, 3, norm); + mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, uv); + mesh.setBuffer(VertexBuffer.Type.Index, 3, idx); + mesh.updateBound(); + mesh.updateCounts(); + return mesh; + } + + private static Material buildMaterial(PlacedWaterfall wf, AssetManager assets) { + Material mat; + try { + mat = new Material(assets, "MatDefs/FlowingWater.j3md"); + + Texture nm = loadTex(assets, "Textures/internal/water/waterfall_normal.png", + "Common/MatDefs/Water/Textures/water_normalmap.png"); + if (nm != null) { nm.setWrap(Texture.WrapMode.Repeat); mat.setTexture("NormalMap", nm); } + + Texture diff = loadTex(assets, "Textures/internal/water/waterfall_diffuse.png", null); + if (diff != null) { diff.setWrap(Texture.WrapMode.Repeat); mat.setTexture("DiffuseMap", diff); } + + mat.setColor("Tint", new ColorRGBA(wf.colorR(), wf.colorG(), wf.colorB(), wf.transparency())); + mat.setFloat("UVScale", 2.0f); + mat.setFloat("FlowSpeed", wf.speed()); + mat.setFloat("Time", 0f); + } catch (Exception e) { + log.warn("FlowingWater.j3md nicht ladbar, Fallback Unshaded", e); + mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", new ColorRGBA(wf.colorR(), wf.colorG(), wf.colorB(), wf.transparency())); + } + mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); + mat.getAdditionalRenderState().setDepthWrite(false); + return mat; + } + + private static Texture loadTex(AssetManager assets, String primary, String fallback) { + try { return assets.loadTexture(primary); } catch (Exception ignored) {} + if (fallback == null) return null; + try { return assets.loadTexture(fallback); } catch (Exception ignored) { return null; } + } +} diff --git a/blight-map/src/main/map/benches/9ac9943d-0e12-4d5c-8323-0e2b92eebdec.bench b/blight-map/src/main/map/benches/9ac9943d-0e12-4d5c-8323-0e2b92eebdec.bench index 4a304e4..c54e975 100644 --- a/blight-map/src/main/map/benches/9ac9943d-0e12-4d5c-8323-0e2b92eebdec.bench +++ b/blight-map/src/main/map/benches/9ac9943d-0e12-4d5c-8323-0e2b92eebdec.bench @@ -2,7 +2,7 @@ "id": "9ac9943d-0e12-4d5c-8323-0e2b92eebdec", "benchType": "Simple", "sitzX": 236.63928, - "sitzY": 6.9953, + "sitzY": -5.81074, "sitzZ": -888.1745, "sitzRotY": 4.7967663, "sitzSet": true diff --git a/blight-map/src/main/map/blight_grass.blg b/blight-map/src/main/map/blight_grass.blg index 534956b..8597292 100644 Binary files a/blight-map/src/main/map/blight_grass.blg and b/blight-map/src/main/map/blight_grass.blg differ diff --git a/blight-map/src/main/map/blight_grass_vertex.blgv b/blight-map/src/main/map/blight_grass_vertex.blgv index c3748c1..a9590d2 100644 Binary files a/blight-map/src/main/map/blight_grass_vertex.blgv and b/blight-map/src/main/map/blight_grass_vertex.blgv differ diff --git a/blight-map/src/main/map/blight_map.blm b/blight-map/src/main/map/blight_map.blm index f97fd36..7b3bd5d 100644 Binary files a/blight-map/src/main/map/blight_map.blm and b/blight-map/src/main/map/blight_map.blm differ diff --git a/blight-map/src/main/map/blight_objects.blo b/blight-map/src/main/map/blight_objects.blo index 5142271..1936d9a 100644 --- a/blight-map/src/main/map/blight_objects.blo +++ b/blight-map/src/main/map/blight_objects.blo @@ -3,9 +3,9 @@ Models/northcoast/wrack1.j3o 286.06750 -3.50554 -947.28595 -1.76657 1.00000 -0.0 Models/trees/palm/palm_20260816_213338.j3o 277.36694 2.74803 -956.39905 0.43292 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 Models/trees/palm/palm_20260816_213338.j3o 280.27863 2.66144 -928.60248 -1.51992 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000 Models/trees/palm/palm_20260816_213341.j3o 249.12691 6.49606 -895.80035 -0.96942 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/trees/palm/palm_20260816_213341.j3o 243.51527 6.49606 -894.84039 -2.48591 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/trees/palm/palm_20260816_213338.j3o 233.49467 4.51015 -892.32526 2.66339 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 +Models/trees/palm/palm_20260816_213341.j3o 243.51527 1.40601 -894.84039 -2.48591 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000 +Models/trees/palm/palm_20260816_213338.j3o 233.49467 1.43232 -892.32526 2.66339 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 Models/trees/palm/palm_20260816_213341.j3o 247.05431 6.44288 -888.47949 1.23606 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/trees/palm/palm_20260816_213341.j3o 239.43253 6.49157 -886.23218 -2.49448 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000 +Models/trees/palm/palm_20260816_213341.j3o 239.43253 0.94988 -886.23218 -2.49448 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000 Models/trees/palm/palm_20260816_213341.j3o 270.73062 3.06689 -913.67090 -1.45828 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000 -Models/imported/bank1.j3o 236.63928 6.49530 -888.17450 -3.22597 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BENCH 9ac9943d-0e12-4d5c-8323-0e2b92eebdec +Models/imported/bank1.j3o 236.63928 -6.31074 -888.17450 -3.22597 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BENCH 9ac9943d-0e12-4d5c-8323-0e2b92eebdec diff --git a/blight-map/src/main/map/blight_water.blw b/blight-map/src/main/map/blight_water.blw index 9d61ca9..710d40c 100644 --- a/blight-map/src/main/map/blight_water.blw +++ b/blight-map/src/main/map/blight_water.blw @@ -1 +1,2 @@ -# polygon_points waterHeight flowDegrees +# polygon_points waterHeight flowDegrees speed waveScale waveAmplitude transparency waterR waterG waterB deepR deepG deepB +155.13664,11.00000,-898.11578;120.48705,11.00000,-897.61938;107.05534,11.00000,-890.41797;96.71703,11.00000,-875.01575;98.34164,11.00000,-854.85681;182.64165,11.00000,-829.23749;195.95512,11.00000,-848.04095 11.00000 180.0 0.5000 0.008000 0.10000 0.15000 0.05000 0.25000 0.55000 0.02000 0.12000 0.30000 diff --git a/blight-map/src/main/map/blight_waterfall.blwf b/blight-map/src/main/map/blight_waterfall.blwf new file mode 100644 index 0000000..69396b9 --- /dev/null +++ b/blight-map/src/main/map/blight_waterfall.blwf @@ -0,0 +1,2 @@ +# ax,ay,az bx,by,bz cx,cy,cz dx,dy,dz speed transparency r g b +139.68401,11.11647,-897.94312 133.19110,11.13542,-897.81921 132.98654,-2.73925,-898.99731 138.49196,-1.53031,-899.29150 1.5000 0.75000 0.35000 0.55000 0.75000 diff --git a/blight-map/src/main/map/chunks/chunk_08_00.blc b/blight-map/src/main/map/chunks/chunk_08_00.blc index 07c5c78..407df62 100644 Binary files a/blight-map/src/main/map/chunks/chunk_08_00.blc and b/blight-map/src/main/map/chunks/chunk_08_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_08_01.blc b/blight-map/src/main/map/chunks/chunk_08_01.blc index 3ce8267..0e01b78 100644 Binary files a/blight-map/src/main/map/chunks/chunk_08_01.blc and b/blight-map/src/main/map/chunks/chunk_08_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_09_00.blc b/blight-map/src/main/map/chunks/chunk_09_00.blc index 5e7199f..e00274d 100644 Binary files a/blight-map/src/main/map/chunks/chunk_09_00.blc and b/blight-map/src/main/map/chunks/chunk_09_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_09_01.blc b/blight-map/src/main/map/chunks/chunk_09_01.blc index 95ff621..92156d0 100644 Binary files a/blight-map/src/main/map/chunks/chunk_09_01.blc and b/blight-map/src/main/map/chunks/chunk_09_01.blc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_08.blvc.prebake b/blight-map/src/main/map/chunks/voxel_16_0_08.blvc.prebake new file mode 100644 index 0000000..13a8ba1 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_08.blvc.prebake differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod0.j3o new file mode 100644 index 0000000..b58dfa3 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod1.j3o new file mode 100644 index 0000000..8b89a9c Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod2.j3o new file mode 100644 index 0000000..c48e0cf Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_08.blvc b/blight-map/src/main/map/chunks/voxel_16_0_09.blvc similarity index 92% rename from blight-map/src/main/map/chunks/voxel_17_0_08.blvc rename to blight-map/src/main/map/chunks/voxel_16_0_09.blvc index bc33783..6490689 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_0_08.blvc and b/blight-map/src/main/map/chunks/voxel_16_0_09.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_09.blvc b/blight-map/src/main/map/chunks/voxel_16_m1_08.blvc.prebake similarity index 79% rename from blight-map/src/main/map/chunks/voxel_17_m1_09.blvc rename to blight-map/src/main/map/chunks/voxel_16_m1_08.blvc.prebake index 1540be5..9d66284 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_m1_09.blvc and b/blight-map/src/main/map/chunks/voxel_16_m1_08.blvc.prebake differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod0.j3o new file mode 100644 index 0000000..eafde82 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod1.j3o new file mode 100644 index 0000000..fdbcec2 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod2.j3o new file mode 100644 index 0000000..6ff23dc Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_09.blvc b/blight-map/src/main/map/chunks/voxel_16_m1_09.blvc.prebake similarity index 85% rename from blight-map/src/main/map/chunks/voxel_17_0_09.blvc rename to blight-map/src/main/map/chunks/voxel_16_m1_09.blvc.prebake index 5d268c8..0064476 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_0_09.blvc and b/blight-map/src/main/map/chunks/voxel_16_m1_09.blvc.prebake differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_09_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_16_m1_09_baked_lod0.j3o new file mode 100644 index 0000000..c85c312 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_m1_09_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_09_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_16_m1_09_baked_lod1.j3o new file mode 100644 index 0000000..1b2551a Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_m1_09_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_09_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_16_m1_09_baked_lod2.j3o new file mode 100644 index 0000000..6de632b Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_m1_09_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_08.blvc.prebake b/blight-map/src/main/map/chunks/voxel_17_0_08.blvc.prebake index 38f49f7..3d267f6 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_0_08.blvc.prebake and b/blight-map/src/main/map/chunks/voxel_17_0_08.blvc.prebake differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_08_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_17_0_08_baked_lod0.j3o index 2dc8ea1..e3a387e 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_0_08_baked_lod0.j3o and b/blight-map/src/main/map/chunks/voxel_17_0_08_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_08_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_17_0_08_baked_lod1.j3o index a983d38..ebf455c 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_0_08_baked_lod1.j3o and b/blight-map/src/main/map/chunks/voxel_17_0_08_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_09.blvc.prebake b/blight-map/src/main/map/chunks/voxel_17_0_09.blvc.prebake index 0bed76f..2315f43 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_0_09.blvc.prebake and b/blight-map/src/main/map/chunks/voxel_17_0_09.blvc.prebake differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_09_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_17_0_09_baked_lod0.j3o index 441d935..4dcba4e 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_0_09_baked_lod0.j3o and b/blight-map/src/main/map/chunks/voxel_17_0_09_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_09_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_17_0_09_baked_lod1.j3o index 734e62f..d2b601f 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_0_09_baked_lod1.j3o and b/blight-map/src/main/map/chunks/voxel_17_0_09_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_09_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_17_0_09_baked_lod2.j3o new file mode 100644 index 0000000..6581f61 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_09_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_08.blvc b/blight-map/src/main/map/chunks/voxel_17_m1_08.blvc.prebake similarity index 82% rename from blight-map/src/main/map/chunks/voxel_17_m1_08.blvc rename to blight-map/src/main/map/chunks/voxel_17_m1_08.blvc.prebake index 7180d8a..97de0ff 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_m1_08.blvc and b/blight-map/src/main/map/chunks/voxel_17_m1_08.blvc.prebake differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_08_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_17_m1_08_baked_lod0.j3o new file mode 100644 index 0000000..0024b41 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_08_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_08_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_17_m1_08_baked_lod1.j3o new file mode 100644 index 0000000..2111f6f Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_08_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_08_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_17_m1_08_baked_lod2.j3o new file mode 100644 index 0000000..4d58362 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_08_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_09.blvc.prebake b/blight-map/src/main/map/chunks/voxel_17_m1_09.blvc.prebake index 01c554d..5128908 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_m1_09.blvc.prebake and b/blight-map/src/main/map/chunks/voxel_17_m1_09.blvc.prebake differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_09_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_17_m1_09_baked_lod0.j3o index ee5ed3f..2df4c43 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_m1_09_baked_lod0.j3o and b/blight-map/src/main/map/chunks/voxel_17_m1_09_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_09_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_17_m1_09_baked_lod1.j3o index bacb5dc..1f332e5 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_m1_09_baked_lod1.j3o and b/blight-map/src/main/map/chunks/voxel_17_m1_09_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_09_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_17_m1_09_baked_lod2.j3o new file mode 100644 index 0000000..b402817 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_09_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_18_0_08_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_18_0_08_baked_lod0.j3o index 3172c83..c6aac14 100644 Binary files a/blight-map/src/main/map/chunks/voxel_18_0_08_baked_lod0.j3o and b/blight-map/src/main/map/chunks/voxel_18_0_08_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_18_0_09_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_18_0_09_baked_lod0.j3o index 8238e65..0ad69d2 100644 Binary files a/blight-map/src/main/map/chunks/voxel_18_0_09_baked_lod0.j3o and b/blight-map/src/main/map/chunks/voxel_18_0_09_baked_lod0.j3o differ