Wasserfall System eingebaut
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<PlacedWaterfall> 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<PlacedWaterfall> load() throws IOException {
|
||||
Path p = getPath();
|
||||
if (!Files.exists(p)) return List.of();
|
||||
List<PlacedWaterfall> 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()) };
|
||||
}
|
||||
}
|
||||
@@ -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<Integer> 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<de.blight.common.PlacedWaterfall> 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<java.util.List<de.blight.common.RiverPoint>> 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 -> {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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<WaterfallClick> waterfallClickQueue = new ConcurrentLinkedQueue<>();
|
||||
public volatile float waterfallNewWidth = 8.0f;
|
||||
public volatile boolean undoWaterfallPointRequested = false;
|
||||
public final ConcurrentLinkedQueue<WaterfallDrag> 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<WaterClick> waterClickQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
/** Maus-Drag während Handle-Bearbeitung: absolute Bildschirmposition. */
|
||||
public record WaterDrag(float screenX, float screenY) {}
|
||||
public final ConcurrentLinkedQueue<WaterDrag> 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<Float> 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<float[]> 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<ObjectClick> auswahlClickQueue = new ConcurrentLinkedQueue<>();
|
||||
public final ConcurrentLinkedQueue<ObjectClick> auswahlClickQueue = new ConcurrentLinkedQueue<>();
|
||||
/** JME3-intern: Auswahl-Klick für LightState (kein Objekt/keine Zone getroffen). */
|
||||
public final ConcurrentLinkedQueue<ObjectClick> auswahlLightClickQueue = new ConcurrentLinkedQueue<>();
|
||||
public final ConcurrentLinkedQueue<ObjectClick> auswahlLightClickQueue = new ConcurrentLinkedQueue<>();
|
||||
/** JME3-intern: Auswahl-Klick für EmitterState (kein Objekt/keine Zone getroffen). */
|
||||
public final ConcurrentLinkedQueue<ObjectClick> auswahlEmitterClickQueue = new ConcurrentLinkedQueue<>();
|
||||
public final ConcurrentLinkedQueue<ObjectClick> auswahlEmitterClickQueue = new ConcurrentLinkedQueue<>();
|
||||
/** JME3-intern: Auswahl-Klick für WaterBodyState. */
|
||||
public final ConcurrentLinkedQueue<ObjectClick> auswahlWaterClickQueue = new ConcurrentLinkedQueue<>();
|
||||
/** JME3-intern: Auswahl-Klick für WaterfallEditorState. */
|
||||
public final ConcurrentLinkedQueue<ObjectClick> 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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<PlacedWaterfall> waterfalls = new ArrayList<>();
|
||||
private final List<Node> wfNodes = new ArrayList<>();
|
||||
|
||||
// Platzierungs-State
|
||||
private final List<Vector3f> 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<Geometry> 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<PlacedWater> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<PlacedWaterfall> list = WaterfallIO.load();
|
||||
TreeItem<String> 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<String> 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<RiverPoint>> list = RiverIO.load();
|
||||
TreeItem<String> group = group("Wasserfälle", list.size());
|
||||
if (list.isEmpty()) return;
|
||||
TreeItem<String> group = group("Flüsse (veraltet)", list.size());
|
||||
for (int idx = 0; idx < list.size(); idx++) {
|
||||
List<RiverPoint> 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<String> item = leaf("Wasserfall #" + (idx + 1) + " " + pos(cx, cy, cz));
|
||||
entryMap.put(item, new Entry(cx, cy + 5f, cz, "waterfall", river, idx));
|
||||
TreeItem<String> 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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<WaterPolygonFilter> filters = new ArrayList<>();
|
||||
private final List<WaterPolygonFilter> filters = new ArrayList<>();
|
||||
private final List<Geometry> inclinedGeos = new ArrayList<>();
|
||||
private final List<Material> 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<PlacedWater> 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<float[]> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Geometry> geos = new ArrayList<>();
|
||||
private final List<Material> materials = new ArrayList<>();
|
||||
private float time = 0f;
|
||||
|
||||
@Override
|
||||
protected void initialize(Application app) {
|
||||
rootNode = ((SimpleApplication) app).getRootNode();
|
||||
AssetManager assets = app.getAssetManager();
|
||||
|
||||
List<PlacedWaterfall> 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; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
2
blight-map/src/main/map/blight_waterfall.blwf
Normal file
2
blight-map/src/main/map/blight_waterfall.blwf
Normal file
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_16_0_08.blvc.prebake
Normal file
BIN
blight-map/src/main/map/chunks/voxel_16_0_08.blvc.prebake
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod0.j3o
Normal file
BIN
blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod0.j3o
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod1.j3o
Normal file
BIN
blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod1.j3o
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod2.j3o
Normal file
BIN
blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod2.j3o
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod0.j3o
Normal file
BIN
blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod0.j3o
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod1.j3o
Normal file
BIN
blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod1.j3o
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod2.j3o
Normal file
BIN
blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod2.j3o
Normal file
Binary file not shown.
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_16_m1_09_baked_lod0.j3o
Normal file
BIN
blight-map/src/main/map/chunks/voxel_16_m1_09_baked_lod0.j3o
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_16_m1_09_baked_lod1.j3o
Normal file
BIN
blight-map/src/main/map/chunks/voxel_16_m1_09_baked_lod1.j3o
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_16_m1_09_baked_lod2.j3o
Normal file
BIN
blight-map/src/main/map/chunks/voxel_16_m1_09_baked_lod2.j3o
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_17_0_09_baked_lod2.j3o
Normal file
BIN
blight-map/src/main/map/chunks/voxel_17_0_09_baked_lod2.j3o
Normal file
Binary file not shown.
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_17_m1_08_baked_lod0.j3o
Normal file
BIN
blight-map/src/main/map/chunks/voxel_17_m1_08_baked_lod0.j3o
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_17_m1_08_baked_lod1.j3o
Normal file
BIN
blight-map/src/main/map/chunks/voxel_17_m1_08_baked_lod1.j3o
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_17_m1_08_baked_lod2.j3o
Normal file
BIN
blight-map/src/main/map/chunks/voxel_17_m1_08_baked_lod2.j3o
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_17_m1_09_baked_lod2.j3o
Normal file
BIN
blight-map/src/main/map/chunks/voxel_17_m1_09_baked_lod2.j3o
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user