Einmal den Fortschritt mit dem Wasser sichern
This commit is contained in:
@@ -2,11 +2,13 @@ MaterialDef Flowing Water {
|
||||
MaterialParameters {
|
||||
Texture2D NormalMap
|
||||
Texture2D FoamMap
|
||||
Texture2D DiffuseMap
|
||||
Color Tint
|
||||
Float UVScale : 6.0
|
||||
Float Time : 0.0
|
||||
Float FlowSpeed : 1.0
|
||||
Float FoamAmount : 0.0
|
||||
Float UVScale : 6.0
|
||||
Float NormalUVScale : 0.5
|
||||
Float Time : 0.0
|
||||
Float FlowSpeed : 1.0
|
||||
Float FoamAmount : 0.0
|
||||
}
|
||||
Technique {
|
||||
VertexShader GLSL150: Shaders/FlowingWater.vert
|
||||
@@ -15,8 +17,9 @@ MaterialDef Flowing Water {
|
||||
WorldViewProjectionMatrix
|
||||
}
|
||||
Defines {
|
||||
HAS_NORMALMAP : NormalMap
|
||||
HAS_FOAMMAP : FoamMap
|
||||
HAS_NORMALMAP : NormalMap
|
||||
HAS_FOAMMAP : FoamMap
|
||||
HAS_DIFFUSEMAP : DiffuseMap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.
@@ -4,6 +4,9 @@ uniform sampler2D m_NormalMap;
|
||||
#ifdef HAS_FOAMMAP
|
||||
uniform sampler2D m_FoamMap;
|
||||
#endif
|
||||
#ifdef HAS_DIFFUSEMAP
|
||||
uniform sampler2D m_DiffuseMap;
|
||||
#endif
|
||||
|
||||
uniform vec4 m_Tint;
|
||||
uniform float m_FoamAmount;
|
||||
@@ -27,15 +30,18 @@ void main() {
|
||||
float specular = 0.0;
|
||||
float ripple = 0.0;
|
||||
#ifdef HAS_NORMALMAP
|
||||
vec3 n1 = texture(m_NormalMap, vTex1).rgb * 2.0 - 1.0;
|
||||
vec3 n2 = texture(m_NormalMap, vTex2).rgb * 2.0 - 1.0;
|
||||
vec3 raw1 = texture(m_NormalMap, vTex1).rgb * 2.0 - 1.0;
|
||||
vec3 raw2 = texture(m_NormalMap, vTex2).rgb * 2.0 - 1.0;
|
||||
// Tangent-Space → World-Space für horizontales Mesh (T=X, B=Z, N=Y):
|
||||
// raw.xyz = (tangent-X, tangent-Y, depth) → world = (raw.x, raw.z, raw.y)
|
||||
vec3 n1 = normalize(vec3(raw1.x, raw1.z, raw1.y));
|
||||
vec3 n2 = normalize(vec3(raw2.x, raw2.z, raw2.y));
|
||||
vec3 n = normalize(n1 + n2);
|
||||
|
||||
// Feste Sonnenrichtung – gut für Bachlauf-Optik
|
||||
vec3 sunDir = normalize(vec3(0.5, 1.0, 0.3));
|
||||
specular = pow(max(0.0, dot(n, sunDir)), 22.0);
|
||||
// Wellenhelligkeit variiert den Basiston leicht
|
||||
ripple = dot(n, vec3(0.0, 1.0, 0.0)) * 0.10;
|
||||
specular = pow(max(0.0, dot(n, sunDir)), 8.0);
|
||||
// Ripple: Bereiche mit starker Oberflächenneigung (Wellenkämme) heller
|
||||
ripple = (abs(n.x) + abs(n.z)) * 0.30;
|
||||
#endif
|
||||
|
||||
// ── Schaum ────────────────────────────────────────────────────────────────
|
||||
@@ -54,9 +60,26 @@ void main() {
|
||||
#endif
|
||||
|
||||
// ── Zusammenführen ────────────────────────────────────────────────────────
|
||||
vec3 waterColor = baseColor + ripple + vec3(0.35, 0.45, 0.55) * specular;
|
||||
// specular-Anteil erhöht (0.55→0.75) damit Glanzlichter deutlich sichtbar sind
|
||||
vec3 waterColor = baseColor + ripple + vec3(0.55, 0.65, 0.75) * specular;
|
||||
vec3 finalColor = mix(waterColor, vec3(1.0), foamMask);
|
||||
float finalAlpha = max(baseAlpha, foamMask * 0.9);
|
||||
|
||||
// ── Diffuse-Map: Fluss = Farbmodulation in Fließrichtung, Wasserfall = Gischt ──
|
||||
#ifdef HAS_DIFFUSEMAP
|
||||
float d1 = texture(m_DiffuseMap, vTex1 * 0.55).r;
|
||||
float d2 = texture(m_DiffuseMap, vTex2 * 0.40).r;
|
||||
float diffuseBlend = max(d1, d2 * 0.6);
|
||||
|
||||
// Fluss (FoamAmount=0): Textur moduliert die Wasserfarbe (dunkle/helle Strömungsmuster)
|
||||
float riverMod = (diffuseBlend - 0.5) * 0.4 * (1.0 - m_FoamAmount);
|
||||
finalColor = clamp(finalColor + riverMod, 0.0, 1.0);
|
||||
|
||||
// Wasserfall (FoamAmount>0): weiße Gischt-Überlagerung
|
||||
float foamStr = diffuseBlend * m_FoamAmount * 0.85;
|
||||
finalColor = mix(finalColor, vec3(1.0), foamStr);
|
||||
finalAlpha = max(finalAlpha, foamStr * 0.9);
|
||||
#endif
|
||||
|
||||
outFragColor = vec4(clamp(finalColor, 0.0, 1.0), finalAlpha);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
uniform mat4 g_WorldViewProjectionMatrix;
|
||||
uniform float m_Time;
|
||||
uniform float m_UVScale;
|
||||
uniform float m_NormalUVScale;
|
||||
uniform float m_FlowSpeed;
|
||||
|
||||
in vec3 inPosition;
|
||||
@@ -12,10 +13,11 @@ out vec2 vTex2; // scrollende Normal-Map-UV (diagonal, gegenläufig)
|
||||
|
||||
void main() {
|
||||
vUV = inTexCoord;
|
||||
float t = m_Time * m_FlowSpeed;
|
||||
vTex1 = vec2(inTexCoord.x * m_UVScale,
|
||||
inTexCoord.y * m_UVScale - t);
|
||||
vTex2 = vec2(inTexCoord.x * m_UVScale * 0.7 - t * 0.25,
|
||||
inTexCoord.y * m_UVScale * 0.7 + t * 0.55);
|
||||
float ns = m_UVScale * m_NormalUVScale; // Normal-Map UV-Dichte (0.5 = 2x größeres Muster)
|
||||
float t = m_Time * m_FlowSpeed * m_NormalUVScale; // Scroll-Geschwindigkeit skaliert mit
|
||||
vTex1 = vec2(inTexCoord.x * ns,
|
||||
inTexCoord.y * ns - t);
|
||||
vTex2 = vec2(inTexCoord.x * ns * 0.7 - t * 0.25,
|
||||
inTexCoord.y * ns * 0.7 + t * 0.55);
|
||||
gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ package de.blight.common;
|
||||
/**
|
||||
* Serialisierbarer Zustand einer Blight-Weltkarte.
|
||||
*
|
||||
* Basis-Terrain : 4097 × 4097 Vertices (= 4096 × 4096 Zellen),
|
||||
* 8 Welteinheiten pro Zelle → Welt −2048 .. +2048.
|
||||
* Basis-Terrain : 16385 × 16385 Vertices (= 16384 × 16384 Zellen, 0,25 m/Vertex),
|
||||
* Welt −2048 .. +2048 m.
|
||||
* Obere Schicht : 513 × 513 Vertices (= 512 × 512 Zellen), gleiche Weltausdehnung.
|
||||
* Splatmap : 513 × 513 Pixel (1:1 zu beiden Terrain-Grids).
|
||||
* Splatmap : 2049 × 2049 Pixel (≈ 2 m/Pixel).
|
||||
* Kanäle R/G/B/A = Gewicht für Tex1-Helligkeit / Tex2 / Tex3 / Tex4.
|
||||
*/
|
||||
public final class MapData {
|
||||
@@ -14,7 +14,7 @@ public final class MapData {
|
||||
// ── Terrain-Konstanten ────────────────────────────────────────────────────
|
||||
|
||||
/** Vertices pro Achse des Basis-Terrains (muss 2^n + 1 sein). */
|
||||
public static final int TERRAIN_VERTS = 4097;
|
||||
public static final int TERRAIN_VERTS = 16385;
|
||||
|
||||
// ── Upper-Layer-Konstanten ────────────────────────────────────────────────
|
||||
|
||||
@@ -26,8 +26,8 @@ public final class MapData {
|
||||
|
||||
// ── Splatmap-Konstanten ────────────────────────────────────────────────────
|
||||
|
||||
/** Pixel pro Achse der Splatmap (entspricht UPPER_VERTS = Spiel-Terrain-Auflösung). */
|
||||
public static final int SPLAT_SIZE = 513;
|
||||
/** Pixel pro Achse der Splatmap (≈ 2 m/Pixel bei 4096 m Weltkante). */
|
||||
public static final int SPLAT_SIZE = 2049;
|
||||
|
||||
/** Anzahl konfigurierbarer Textur-Slots pro Terrain-Layer. */
|
||||
public static final int TEXTURE_SLOTS = 4;
|
||||
|
||||
@@ -22,6 +22,8 @@ import java.util.zip.*;
|
||||
* 7 – wie 6 + Gras-Texturpfad (UTF-String) + Gras-Standardhöhe (float)
|
||||
* 8 – wie 7 + Gras-Höhen-Map (SPLAT_SIZE² Bytes, 0=ungesetzt 1-255=0.1-10m)
|
||||
* 9 – wie 8 + Gras-Textur-Map (SPLAT_SIZE² Bytes) + Slots (N×UTF)
|
||||
* 10 – wie 9, aber TERRAIN_VERTS=16385 (0,25 m/Vertex) + SPLAT_SIZE=2049 (2 m/Pixel)
|
||||
* Altes Format (v≤9) wird beim Laden bilinear hochskaliert.
|
||||
*/
|
||||
public final class MapIO {
|
||||
|
||||
@@ -53,7 +55,11 @@ public final class MapIO {
|
||||
}
|
||||
|
||||
private static final int MAGIC = 0x424C4947; // "BLIG"
|
||||
private static final int VERSION = 9;
|
||||
private static final int VERSION = 10;
|
||||
|
||||
// Größen älterer Saves (v≤9) – für Migrations-Upsampling
|
||||
private static final int OLD_TERRAIN_VERTS = 4097;
|
||||
private static final int OLD_SPLAT_SIZE = 513;
|
||||
|
||||
private MapIO() {}
|
||||
|
||||
@@ -134,36 +140,76 @@ public final class MapIO {
|
||||
if (version < 1 || version > VERSION)
|
||||
throw new IOException("Unbekannte Map-Version: " + version);
|
||||
|
||||
readFloats(in, data.terrainHeight);
|
||||
readFloats(in, data.upperTop);
|
||||
readFloats(in, data.upperBottom);
|
||||
if (version >= 10) {
|
||||
readFloats(in, data.terrainHeight);
|
||||
readFloats(in, data.upperTop);
|
||||
readFloats(in, data.upperBottom);
|
||||
} else {
|
||||
// v≤9: alte Größen lesen und bilinear hochskalieren
|
||||
float[] oldH = new float[OLD_TERRAIN_VERTS * OLD_TERRAIN_VERTS];
|
||||
float[] oldUT = new float[MapData.UPPER_VERTS * MapData.UPPER_VERTS];
|
||||
float[] oldUB = new float[MapData.UPPER_VERTS * MapData.UPPER_VERTS];
|
||||
readFloats(in, oldH);
|
||||
upsampleFloats(oldH, OLD_TERRAIN_VERTS, data.terrainHeight, MapData.TERRAIN_VERTS);
|
||||
readFloats(in, oldUT); System.arraycopy(oldUT, 0, data.upperTop, 0, oldUT.length);
|
||||
readFloats(in, oldUB); System.arraycopy(oldUB, 0, data.upperBottom, 0, oldUB.length);
|
||||
}
|
||||
|
||||
if (version <= 5) {
|
||||
// v5 had upperHole[UPPER_CELLS²]; read and discard
|
||||
in.skip((long) MapData.UPPER_CELLS * MapData.UPPER_CELLS);
|
||||
}
|
||||
|
||||
if (version >= 2) {
|
||||
in.readFully(data.splatR);
|
||||
in.readFully(data.splatG);
|
||||
in.readFully(data.splatB);
|
||||
if (version >= 10) {
|
||||
in.readFully(data.splatR); in.readFully(data.splatG); in.readFully(data.splatB);
|
||||
} else {
|
||||
byte[] oR = new byte[OLD_SPLAT_SIZE*OLD_SPLAT_SIZE];
|
||||
byte[] oG = new byte[OLD_SPLAT_SIZE*OLD_SPLAT_SIZE];
|
||||
byte[] oB = new byte[OLD_SPLAT_SIZE*OLD_SPLAT_SIZE];
|
||||
in.readFully(oR); in.readFully(oG); in.readFully(oB);
|
||||
upsampleBytes(oR, OLD_SPLAT_SIZE, data.splatR, MapData.SPLAT_SIZE);
|
||||
upsampleBytes(oG, OLD_SPLAT_SIZE, data.splatG, MapData.SPLAT_SIZE);
|
||||
upsampleBytes(oB, OLD_SPLAT_SIZE, data.splatB, MapData.SPLAT_SIZE);
|
||||
}
|
||||
}
|
||||
if (version >= 3) {
|
||||
in.readFully(data.grassDensity);
|
||||
if (version >= 10) {
|
||||
in.readFully(data.grassDensity);
|
||||
} else {
|
||||
byte[] old = new byte[OLD_SPLAT_SIZE*OLD_SPLAT_SIZE];
|
||||
in.readFully(old);
|
||||
upsampleBytes(old, OLD_SPLAT_SIZE, data.grassDensity, MapData.SPLAT_SIZE);
|
||||
}
|
||||
}
|
||||
if (version >= 4) {
|
||||
data.spawnX = in.readFloat();
|
||||
data.spawnZ = in.readFloat();
|
||||
}
|
||||
if (version >= 5) {
|
||||
in.readFully(data.splatA);
|
||||
readStrings(in, data.terrainTextures);
|
||||
in.readFully(data.upperSplatR);
|
||||
in.readFully(data.upperSplatG);
|
||||
in.readFully(data.upperSplatB);
|
||||
in.readFully(data.upperSplatA);
|
||||
readStrings(in, data.upperTextures);
|
||||
if (version >= 10) {
|
||||
in.readFully(data.splatA);
|
||||
readStrings(in, data.terrainTextures);
|
||||
in.readFully(data.upperSplatR); in.readFully(data.upperSplatG);
|
||||
in.readFully(data.upperSplatB); in.readFully(data.upperSplatA);
|
||||
readStrings(in, data.upperTextures);
|
||||
} else {
|
||||
byte[] oA = new byte[OLD_SPLAT_SIZE*OLD_SPLAT_SIZE];
|
||||
byte[] oUR = new byte[OLD_SPLAT_SIZE*OLD_SPLAT_SIZE];
|
||||
byte[] oUG = new byte[OLD_SPLAT_SIZE*OLD_SPLAT_SIZE];
|
||||
byte[] oUB = new byte[OLD_SPLAT_SIZE*OLD_SPLAT_SIZE];
|
||||
byte[] oUA = new byte[OLD_SPLAT_SIZE*OLD_SPLAT_SIZE];
|
||||
in.readFully(oA);
|
||||
readStrings(in, data.terrainTextures);
|
||||
in.readFully(oUR); in.readFully(oUG); in.readFully(oUB); in.readFully(oUA);
|
||||
readStrings(in, data.upperTextures);
|
||||
upsampleBytes(oA, OLD_SPLAT_SIZE, data.splatA, MapData.SPLAT_SIZE);
|
||||
upsampleBytes(oUR, OLD_SPLAT_SIZE, data.upperSplatR, MapData.SPLAT_SIZE);
|
||||
upsampleBytes(oUG, OLD_SPLAT_SIZE, data.upperSplatG, MapData.SPLAT_SIZE);
|
||||
upsampleBytes(oUB, OLD_SPLAT_SIZE, data.upperSplatB, MapData.SPLAT_SIZE);
|
||||
upsampleBytes(oUA, OLD_SPLAT_SIZE, data.upperSplatA, MapData.SPLAT_SIZE);
|
||||
}
|
||||
} else {
|
||||
// Ältere Maps: upperSplatR auf 255 initialisieren (Tex1 immer sichtbar)
|
||||
java.util.Arrays.fill(data.upperSplatR, (byte) 255);
|
||||
}
|
||||
if (version >= 7) {
|
||||
@@ -171,13 +217,25 @@ public final class MapIO {
|
||||
data.grassDefaultHeight = in.readFloat();
|
||||
}
|
||||
if (version >= 8) {
|
||||
in.readFully(data.grassHeightMap);
|
||||
if (version >= 10) {
|
||||
in.readFully(data.grassHeightMap);
|
||||
} else {
|
||||
byte[] old = new byte[OLD_SPLAT_SIZE*OLD_SPLAT_SIZE];
|
||||
in.readFully(old);
|
||||
upsampleBytes(old, OLD_SPLAT_SIZE, data.grassHeightMap, MapData.SPLAT_SIZE);
|
||||
}
|
||||
}
|
||||
if (version >= 9) {
|
||||
int n = in.readInt();
|
||||
data.grassTextureSlots = new String[n];
|
||||
for (int i = 0; i < n; i++) data.grassTextureSlots[i] = in.readUTF();
|
||||
in.readFully(data.grassTextureMap);
|
||||
if (version >= 10) {
|
||||
in.readFully(data.grassTextureMap);
|
||||
} else {
|
||||
byte[] old = new byte[OLD_SPLAT_SIZE*OLD_SPLAT_SIZE];
|
||||
in.readFully(old);
|
||||
upsampleBytes(old, OLD_SPLAT_SIZE, data.grassTextureMap, MapData.SPLAT_SIZE);
|
||||
}
|
||||
}
|
||||
}
|
||||
return data;
|
||||
@@ -207,4 +265,39 @@ public final class MapIO {
|
||||
int len = in.readInt();
|
||||
for (int i = 0; i < len && i < arr.length; i++) arr[i] = in.readUTF();
|
||||
}
|
||||
|
||||
private static void upsampleFloats(float[] src, int srcSize, float[] dst, int dstSize) {
|
||||
float scale = (float)(srcSize - 1) / (dstSize - 1);
|
||||
for (int dz = 0; dz < dstSize; dz++) {
|
||||
float sz = dz * scale;
|
||||
int sz0 = Math.min((int)sz, srcSize - 2), sz1 = sz0 + 1;
|
||||
float fz = sz - sz0;
|
||||
for (int dx = 0; dx < dstSize; dx++) {
|
||||
float sx = dx * scale;
|
||||
int sx0 = Math.min((int)sx, srcSize - 2), sx1 = sx0 + 1;
|
||||
float fx = sx - sx0;
|
||||
dst[dz*dstSize+dx] =
|
||||
(src[sz0*srcSize+sx0]*(1-fx) + src[sz0*srcSize+sx1]*fx)*(1-fz)
|
||||
+ (src[sz1*srcSize+sx0]*(1-fx) + src[sz1*srcSize+sx1]*fx)*fz;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void upsampleBytes(byte[] src, int srcSize, byte[] dst, int dstSize) {
|
||||
float scale = (float)(srcSize - 1) / (dstSize - 1);
|
||||
for (int dz = 0; dz < dstSize; dz++) {
|
||||
float sz = dz * scale;
|
||||
int sz0 = Math.min((int)sz, srcSize - 2), sz1 = sz0 + 1;
|
||||
float fz = sz - sz0;
|
||||
for (int dx = 0; dx < dstSize; dx++) {
|
||||
float sx = dx * scale;
|
||||
int sx0 = Math.min((int)sx, srcSize - 2), sx1 = sx0 + 1;
|
||||
float fx = sx - sx0;
|
||||
float v =
|
||||
((src[sz0*srcSize+sx0]&0xFF)*(1-fx) + (src[sz0*srcSize+sx1]&0xFF)*fx)*(1-fz)
|
||||
+ ((src[sz1*srcSize+sx0]&0xFF)*(1-fx) + (src[sz1*srcSize+sx1]&0xFF)*fx)*fz;
|
||||
dst[dz*dstSize+dx] = (byte)Math.round(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -569,7 +569,11 @@ public class EditorApp extends Application {
|
||||
Menu viewMenu = new Menu("Ansicht");
|
||||
MenuItem resetCam = new MenuItem("Kamera zurücksetzen");
|
||||
resetCam.setOnAction(e -> input.addMouseDelta(0, 0));
|
||||
viewMenu.getItems().add(resetCam);
|
||||
MenuItem viewTexture = new MenuItem("Textur");
|
||||
MenuItem viewWireframe = new MenuItem("Drahtgitter");
|
||||
viewTexture.setOnAction(e -> input.wireframeRequest = 2);
|
||||
viewWireframe.setOnAction(e -> input.wireframeRequest = 1);
|
||||
viewMenu.getItems().addAll(resetCam, new SeparatorMenuItem(), viewTexture, viewWireframe);
|
||||
|
||||
menuBar.getMenus().addAll(fileMenu, toolsMenu, viewMenu);
|
||||
|
||||
@@ -1781,11 +1785,11 @@ public class EditorApp extends Application {
|
||||
VBox inner = new VBox(10);
|
||||
inner.setPadding(new Insets(10));
|
||||
|
||||
// Width slider
|
||||
Label widthTitle = sectionTitle("Flussbreite");
|
||||
// Width slider — setzt die Breite des NÄCHSTEN platzierten Punktes
|
||||
Label widthTitle = sectionTitle("Breite (nächster Punkt)");
|
||||
Label widthVal = new Label(String.format("%.1f m", input.riverNewWidth));
|
||||
widthVal.setStyle("-fx-font-size: 11;");
|
||||
Slider widthSlider = new Slider(1.0, 20.0, input.riverNewWidth);
|
||||
widthVal.setStyle("-fx-font-size: 11; -fx-font-weight: bold;");
|
||||
Slider widthSlider = new Slider(8.0, 40.0, input.riverNewWidth);
|
||||
widthSlider.setBlockIncrement(0.5);
|
||||
widthSlider.setShowTickLabels(true);
|
||||
widthSlider.setMajorTickUnit(5);
|
||||
@@ -1822,6 +1826,7 @@ public class EditorApp extends Application {
|
||||
undoBtn,
|
||||
new Separator(),
|
||||
styledHint("L-Klick → Punkt setzen"),
|
||||
styledHint("Breite ändern → nächsten Punkt setzen"),
|
||||
styledHint("R-Klick → Fluss abschließen"),
|
||||
styledHint("Backspace → letzten Punkt löschen"));
|
||||
|
||||
|
||||
@@ -121,6 +121,10 @@ public class SharedInput {
|
||||
public volatile boolean saveRequested = false;
|
||||
public volatile String saveStatusMsg = null;
|
||||
|
||||
// ── Wireframe-Modus ───────────────────────────────────────────────────────
|
||||
// 0 = keine Änderung, 1 = Drahtgitter aktivieren, 2 = Textur-Modus aktivieren
|
||||
public volatile int wireframeRequest = 0;
|
||||
|
||||
// ── Vorschau-Viewport (gemeinsam für Baum-Generator & EZ-Tree) ───────────
|
||||
public volatile float treePreviewRotY = 0f; // Yaw-Winkel in Grad
|
||||
public volatile float treePreviewRotX = 30f; // Elevation in Grad [5, 80]
|
||||
@@ -349,7 +353,7 @@ public class SharedInput {
|
||||
// ── Fluss-Werkzeug ─────────────────────────────────────────────────────────
|
||||
public record RiverClick(float screenX, float screenY, boolean rightButton) {}
|
||||
public final ConcurrentLinkedQueue<RiverClick> riverClickQueue = new ConcurrentLinkedQueue<>();
|
||||
public volatile float riverNewWidth = 4.0f; // Breite des nächsten Punktes
|
||||
public volatile float riverNewWidth = 8.0f; // Breite des nächsten Punktes (Minimum 8m)
|
||||
public volatile float riverNewSpeed = 0.4f; // UV-Geschwindigkeit (0.4=Fluss, 3.0=Wasserfall)
|
||||
public volatile boolean undoRiverPointRequested = false;
|
||||
public volatile String riverHint = null;
|
||||
|
||||
@@ -105,6 +105,10 @@ public class RiverEditorState extends BaseAppState {
|
||||
|
||||
public void setTerrainEditor(TerrainEditorState te) {
|
||||
this.terrainEditor = te;
|
||||
// Beim ersten Setzen sofort alle geladenen Flüsse ins Terrain graben
|
||||
if (te != null && !rivers.isEmpty()) {
|
||||
te.reapplyAllRivers(getPlacedRivers());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Update ────────────────────────────────────────────────────────────────
|
||||
@@ -189,17 +193,11 @@ public class RiverEditorState extends BaseAppState {
|
||||
}
|
||||
|
||||
List<RiverPoint> current = rivers.get(activeRiver);
|
||||
if (!current.isEmpty() && terrainEditor != null) {
|
||||
if (!current.isEmpty()) {
|
||||
RiverPoint prev = current.get(current.size() - 1);
|
||||
// Gefälle sicherstellen: neuer Punkt darf nicht höher als vorheriger sein
|
||||
float newY = Math.min(pt.y(), prev.y() - 0.05f);
|
||||
pt = new RiverPoint(pt.x(), newY, pt.z(), pt.width(), pt.uvSpeed());
|
||||
// Flussbett graben
|
||||
terrainEditor.carveRiverbedSegment(
|
||||
prev.x(), prev.y(), prev.z(),
|
||||
pt.x(), pt.y(), pt.z(),
|
||||
pt.width() * 0.5f
|
||||
);
|
||||
}
|
||||
|
||||
rivers.get(activeRiver).add(pt);
|
||||
@@ -214,13 +212,18 @@ public class RiverEditorState extends BaseAppState {
|
||||
}
|
||||
|
||||
private void finalizeActiveRiver() {
|
||||
// Fluss mit weniger als 2 Punkten verwerfen
|
||||
if (activeRiver >= 0 && activeRiver < rivers.size()) {
|
||||
if (rivers.get(activeRiver).size() < 2) {
|
||||
removeRiver(activeRiver);
|
||||
removeRiver(activeRiver); // löst intern reapplyAllRivers aus
|
||||
activeRiver = -1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
activeRiver = -1;
|
||||
// Terrain erst jetzt graben — vollständiger Spline, non-destructive
|
||||
if (terrainEditor != null) {
|
||||
terrainEditor.reapplyAllRivers(getPlacedRivers());
|
||||
}
|
||||
}
|
||||
|
||||
private void undoLastPoint() {
|
||||
@@ -332,6 +335,10 @@ public class RiverEditorState extends BaseAppState {
|
||||
else if (activeRiver == idx) activeRiver = -1;
|
||||
if (selectedRiver > idx) selectedRiver--;
|
||||
else if (selectedRiver == idx) selectedRiver = -1;
|
||||
// Terrain revertieren und verbleibende Flüsse neu graben
|
||||
if (terrainEditor != null) {
|
||||
terrainEditor.reapplyAllRivers(getPlacedRivers());
|
||||
}
|
||||
}
|
||||
|
||||
private void selectRiver(int idx) {
|
||||
@@ -393,16 +400,21 @@ public class RiverEditorState extends BaseAppState {
|
||||
}
|
||||
|
||||
private Geometry buildPointGeo(RiverPoint pt) {
|
||||
Sphere sphere = new Sphere(8, 8, 0.4f);
|
||||
// Radius = halbe Flussbreite, damit der Marker die Breite sichtbar macht
|
||||
float r = Math.max(0.4f, pt.width() * 0.5f);
|
||||
Sphere sphere = new Sphere(10, 10, r);
|
||||
Geometry geo = new Geometry("riverPoint", sphere);
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
if (pt.isWaterfall()) {
|
||||
mat.setColor("Color", new ColorRGBA(1.0f, 0.45f, 0.0f, 1f));
|
||||
mat.setColor("Color", new ColorRGBA(1.0f, 0.45f, 0.0f, 0.7f));
|
||||
} else {
|
||||
mat.setColor("Color", new ColorRGBA(0.1f, 0.4f, 1.0f, 1f));
|
||||
mat.setColor("Color", new ColorRGBA(0.1f, 0.4f, 1.0f, 0.7f));
|
||||
}
|
||||
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
geo.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
geo.setShadowMode(RenderQueue.ShadowMode.Off);
|
||||
geo.setMaterial(mat);
|
||||
geo.setLocalTranslation(pt.x(), pt.y() + 0.4f, pt.z());
|
||||
geo.setLocalTranslation(pt.x(), pt.y() + r, pt.z());
|
||||
return geo;
|
||||
}
|
||||
|
||||
@@ -454,7 +466,7 @@ public class RiverEditorState extends BaseAppState {
|
||||
if (right.lengthSquared() < 1e-6f) right.set(1f, 0f, 0f);
|
||||
|
||||
float halfW = pt.width() * 0.5f;
|
||||
float px = pt.x(), py = pt.y() + 0.05f, pz = pt.z();
|
||||
float px = pt.x(), py = pt.y() - 0.45f, pz = pt.z(); // 0,5m Absenkung + 0,05m Offset
|
||||
|
||||
pos.put(px - right.x * halfW).put(py).put(pz - right.z * halfW);
|
||||
norm.put(0f).put(1f).put(0f);
|
||||
@@ -487,6 +499,7 @@ public class RiverEditorState extends BaseAppState {
|
||||
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
|
||||
geo.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
geo.setShadowMode(RenderQueue.ShadowMode.Off);
|
||||
geo.setMaterial(mat);
|
||||
return geo;
|
||||
}
|
||||
|
||||
@@ -50,22 +50,30 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
import de.blight.common.RiverPoint;
|
||||
import de.blight.common.RiverSpline;
|
||||
|
||||
public class TerrainEditorState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TerrainEditorState.class);
|
||||
|
||||
// ── Terrain-Konstanten ────────────────────────────────────────────────────
|
||||
private static final int TERRAIN_SIZE = 4096;
|
||||
private static final int TOTAL_SIZE = TERRAIN_SIZE + 1; // 4097
|
||||
private static final int PATCH_SIZE = 65;
|
||||
private static final int TERRAIN_SIZE = 4096;
|
||||
private static final int TOTAL_SIZE = TERRAIN_SIZE + 1; // 4097
|
||||
private static final float VERTEX_SPACING = (float) TERRAIN_SIZE / (TOTAL_SIZE - 1); // 1.0f
|
||||
private static final int PATCH_SIZE = 65;
|
||||
|
||||
// ── Splatmap-Konstanten ────────────────────────────────────────────────────
|
||||
private static final int SPLAT_SIZE = MapData.SPLAT_SIZE; // 513
|
||||
private static final int SPLAT_SIZE = MapData.SPLAT_SIZE; // 2049
|
||||
private static final float WORLD_HALF = 2048f;
|
||||
private static final float SPLAT_WE_PER_PX = 4096f / (SPLAT_SIZE - 1); // 8 WE/px
|
||||
private static final float SPLAT_WE_PER_PX = (float) TERRAIN_SIZE / (SPLAT_SIZE - 1); // 2 WE/px
|
||||
|
||||
// ── Kamera ────────────────────────────────────────────────────────────────
|
||||
private static final float CAM_SPEED = 300f;
|
||||
@@ -80,7 +88,10 @@ public class TerrainEditorState extends BaseAppState {
|
||||
private final SharedInput input;
|
||||
|
||||
private TerrainQuad terrain;
|
||||
private float[] cachedHeightMap; // Einmal geladen, danach manuell synchron gehalten
|
||||
private float[] cachedHeightMap; // Einmal geladen, danach manuell synchron gehalten
|
||||
private float[] originalHeightMap; // Unveränderter Stand vor allen Fluss-Modifikationen
|
||||
private byte[] originalSplatR, originalSplatG, originalSplatB, originalSplatA;
|
||||
private final HashSet<Integer> modifiedVertices = new HashSet<>(); // Alle durch Flüsse veränderten Vertices
|
||||
private Geometry brushIndicator;
|
||||
private Geometry livePlayerMarker;
|
||||
private PlacedObjectState placedObjectState;
|
||||
@@ -93,6 +104,7 @@ public class TerrainEditorState extends BaseAppState {
|
||||
private RiverEditorState riverEditorState;
|
||||
private MapData loadedMapData;
|
||||
private Node axesGizmo;
|
||||
private boolean wireframeMode = false;
|
||||
|
||||
// ── Splatmap (Slots 1-4) ─────────────────────────────────────────────────
|
||||
private byte[] splatR, splatG, splatB, splatA;
|
||||
@@ -193,7 +205,8 @@ public class TerrainEditorState extends BaseAppState {
|
||||
private void buildScene() {
|
||||
input.loadingStatus = "Lade Terrain...";
|
||||
terrain = buildTerrain();
|
||||
cachedHeightMap = terrain.getHeightMap(); // einmalige 67MB-Allokation; danach nie wieder
|
||||
cachedHeightMap = terrain.getHeightMap(); // einmalige 67MB-Allokation; danach nie wieder
|
||||
originalHeightMap = cachedHeightMap.clone(); // Snapshot vor allen Fluss-Modifikationen
|
||||
rootNode.attachChild(terrain);
|
||||
|
||||
input.loadingStatus = "Lade platzierte Objekte...";
|
||||
@@ -301,7 +314,14 @@ public class TerrainEditorState extends BaseAppState {
|
||||
private TerrainQuad buildTerrain() {
|
||||
float[] heights;
|
||||
if (loadedMapData != null) {
|
||||
heights = loadedMapData.terrainHeight.clone();
|
||||
float[] src = loadedMapData.terrainHeight;
|
||||
heights = new float[TOTAL_SIZE * TOTAL_SIZE];
|
||||
int srcVerts = MapData.TERRAIN_VERTS; // 16385
|
||||
if (srcVerts == TOTAL_SIZE) {
|
||||
System.arraycopy(src, 0, heights, 0, heights.length);
|
||||
} else {
|
||||
downsampleHeights(src, srcVerts, heights, TOTAL_SIZE);
|
||||
}
|
||||
mergeUpperHeights(heights, loadedMapData);
|
||||
} else {
|
||||
heights = new float[TOTAL_SIZE * TOTAL_SIZE];
|
||||
@@ -309,6 +329,7 @@ public class TerrainEditorState extends BaseAppState {
|
||||
}
|
||||
|
||||
TerrainQuad tq = new TerrainQuad("terrain", PATCH_SIZE, TOTAL_SIZE, heights);
|
||||
tq.setLocalScale(VERTEX_SPACING, 1f, VERTEX_SPACING);
|
||||
// Kein scaleTerrainUVs – Terrain.j3md nutzt TexNScale direkt
|
||||
|
||||
TerrainLodControl lod = new TerrainLodControl(tq, cam);
|
||||
@@ -320,10 +341,11 @@ public class TerrainEditorState extends BaseAppState {
|
||||
}
|
||||
|
||||
private void mergeUpperHeights(float[] heights, MapData map) {
|
||||
float scale = (float)(MapData.UPPER_VERTS - 1) / (TOTAL_SIZE - 1);
|
||||
for (int tz = 0; tz < TOTAL_SIZE; tz++) {
|
||||
for (int tx = 0; tx < TOTAL_SIZE; tx++) {
|
||||
float gi = (float) tx / 8f;
|
||||
float gj = (float) tz / 8f;
|
||||
float gi = tx * scale;
|
||||
float gj = tz * scale;
|
||||
float upperH = bilinearSample(map.upperTop, MapData.UPPER_VERTS, gi, gj);
|
||||
int idx = tz * TOTAL_SIZE + tx;
|
||||
if (upperH > 0f && upperH > heights[idx]) heights[idx] = upperH;
|
||||
@@ -379,6 +401,29 @@ public class TerrainEditorState extends BaseAppState {
|
||||
upperSplatA = new byte[SPLAT_SIZE * SPLAT_SIZE];
|
||||
}
|
||||
|
||||
// Snapshot vor allen Fluss-Modifikationen
|
||||
// Fluss-Farbe aus dem A-Kanal rückrechnen: A-Kanal ist ausschließlich für Flüsse.
|
||||
// Alte Saves können dort nicht-null-Werte haben. Diese jetzt aus dem Original entfernen,
|
||||
// damit reapplyAllRivers immer von einer sauberen Basis aus malt.
|
||||
originalSplatR = splatR.clone();
|
||||
originalSplatG = splatG.clone();
|
||||
originalSplatB = splatB.clone();
|
||||
originalSplatA = splatA.clone();
|
||||
for (int i = 0; i < originalSplatA.length; i++) {
|
||||
int a = originalSplatA[i] & 0xFF;
|
||||
if (a > 0) {
|
||||
float ps = a / 255f;
|
||||
float denom = 1f - ps;
|
||||
// R-Kanal zurückrechnen (war gedimmt: R_painted = R_orig * (1-ps))
|
||||
originalSplatR[i] = denom < 0.01f ? (byte) 255
|
||||
: (byte) Math.min(255, Math.round((originalSplatR[i] & 0xFF) / denom));
|
||||
// G/B wurden von alten Fluss-Saves ggf. auf Nicht-Null gesetzt → bereinigen
|
||||
originalSplatG[i] = 0;
|
||||
originalSplatB[i] = 0;
|
||||
originalSplatA[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
splatBuf = BufferUtils.createByteBuffer(SPLAT_SIZE * SPLAT_SIZE * 4);
|
||||
for (int i = 0; i < SPLAT_SIZE * SPLAT_SIZE; i++) {
|
||||
splatBuf.put(splatR[i]).put(splatG[i]).put(splatB[i]).put(splatA[i]);
|
||||
@@ -455,6 +500,7 @@ public class TerrainEditorState extends BaseAppState {
|
||||
}
|
||||
|
||||
mat.setTexture("AlphaMap", splatTex);
|
||||
if (wireframeMode) mat.getAdditionalRenderState().setWireframe(true);
|
||||
|
||||
// Zweite Splatmap → Slots 5-8 (AlphaMap_1), nur wenn Texturen konfiguriert
|
||||
boolean hasUpperTex = false;
|
||||
@@ -639,6 +685,15 @@ public class TerrainEditorState extends BaseAppState {
|
||||
updateAxesGizmo();
|
||||
updateLivePlayerMarker();
|
||||
|
||||
// Wireframe-Modus setzen
|
||||
int wfReq = input.wireframeRequest;
|
||||
if (wfReq != 0) {
|
||||
input.wireframeRequest = 0;
|
||||
wireframeMode = (wfReq == 1);
|
||||
if (terrain != null)
|
||||
terrain.getMaterial().getAdditionalRenderState().setWireframe(wireframeMode);
|
||||
}
|
||||
|
||||
// Terrain-Material neu aufbauen wenn Texturen (Slots 1-8) oder Normal-Maps geändert
|
||||
if (input.terrainTexturesChanged || input.terrainNormalMapsChanged
|
||||
|| input.upperTexturesChanged || input.upperNormalMapsChanged) {
|
||||
@@ -701,6 +756,319 @@ public class TerrainEditorState extends BaseAppState {
|
||||
|
||||
// ── Flussbett graben ──────────────────────────────────────────────────────
|
||||
|
||||
// Wasser liegt 50 cm unter den Kontrollpunkten
|
||||
private static final float WATER_SINK = 0.5f;
|
||||
// Tiefste Stelle des Flussbetts: 1 m unter Wasseroberfläche
|
||||
private static final float BED_DEPTH = 1.0f;
|
||||
// Bettbreite = Flussbreite × (1 + 2×0.25) = 1.5× — je 25 % der Breite als Böschung
|
||||
private static final float BED_EXTRA = 0.25f;
|
||||
// Mindest-Halbbreite 4 m (= 8 m gesamt)
|
||||
private static final float MIN_HALF_WIDTH = 4.0f;
|
||||
|
||||
/**
|
||||
* Setzt das Terrain auf den Original-Zustand zurück und gräbt ALLE übergebenen
|
||||
* Flüsse in einem einzigen terrain.adjustHeight()-Aufruf neu.
|
||||
* Aufzurufen wenn ein Fluss fertiggestellt oder gelöscht wird.
|
||||
*/
|
||||
public void reapplyAllRivers(List<List<RiverPoint>> allRivers) {
|
||||
if (terrain == null || cachedHeightMap == null) return;
|
||||
|
||||
// 1. Alle zuvor durch Flüsse veränderten Vertices auf Original zurücksetzen
|
||||
if (!modifiedVertices.isEmpty()) {
|
||||
List<Vector2f> resetLocs = new ArrayList<>(modifiedVertices.size());
|
||||
List<Float> resetDeltas = new ArrayList<>(modifiedVertices.size());
|
||||
for (int vidx : modifiedVertices) {
|
||||
float orig = originalHeightMap[vidx];
|
||||
float curH = cachedHeightMap[vidx];
|
||||
if (curH != orig) {
|
||||
int vx = vidx % TOTAL_SIZE, vz = vidx / TOTAL_SIZE;
|
||||
resetLocs.add(new Vector2f(vx * VERTEX_SPACING - TERRAIN_SIZE * 0.5f,
|
||||
vz * VERTEX_SPACING - TERRAIN_SIZE * 0.5f));
|
||||
resetDeltas.add(orig - curH);
|
||||
}
|
||||
cachedHeightMap[vidx] = orig;
|
||||
}
|
||||
modifiedVertices.clear();
|
||||
if (!resetLocs.isEmpty()) {
|
||||
terrain.adjustHeight(resetLocs, resetDeltas);
|
||||
terrain.updateModelBound();
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Splatmap auf Original zurücksetzen
|
||||
if (splatR != null) {
|
||||
System.arraycopy(originalSplatR, 0, splatR, 0, splatR.length);
|
||||
System.arraycopy(originalSplatG, 0, splatG, 0, splatG.length);
|
||||
System.arraycopy(originalSplatB, 0, splatB, 0, splatB.length);
|
||||
System.arraycopy(originalSplatA, 0, splatA, 0, splatA.length);
|
||||
for (int i = 0; i < splatR.length; i++) {
|
||||
int bi = i * 4;
|
||||
splatBuf.put(bi, splatR[i]);
|
||||
splatBuf.put(bi + 1, splatG[i]);
|
||||
splatBuf.put(bi + 2, splatB[i]);
|
||||
splatBuf.put(bi + 3, splatA[i]);
|
||||
}
|
||||
splatBuf.rewind();
|
||||
splatImage.setUpdateNeeded();
|
||||
}
|
||||
|
||||
if (allRivers == null || allRivers.isEmpty()) return;
|
||||
|
||||
// 3. Alle Flüsse in einem Batch berechnen und anwenden
|
||||
HashMap<Integer, Float> channelTargets = new HashMap<>();
|
||||
HashMap<Integer, Float> bankTargets = new HashMap<>();
|
||||
List<List<RiverPoint>> allSplined = new ArrayList<>(allRivers.size());
|
||||
|
||||
for (List<RiverPoint> river : allRivers) {
|
||||
if (river == null || river.size() < 2) continue;
|
||||
List<RiverPoint> splined = RiverSpline.subdivide(river);
|
||||
allSplined.add(splined);
|
||||
collectRiverTargets(splined, channelTargets, bankTargets);
|
||||
}
|
||||
|
||||
applyTargets(channelTargets);
|
||||
applyBankTargets(bankTargets, channelTargets.keySet());
|
||||
|
||||
// 4. Splatmap für alle Flüsse neu malen
|
||||
if (splatR != null) {
|
||||
for (List<RiverPoint> splined : allSplined) paintRiverSplat(splined);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet Tiefenziele für alle Vertices entlang eines gesplineten Flusses.
|
||||
*
|
||||
* Profil (gemessen von Wasseroberfläche = waterY − WATER_SINK):
|
||||
* dist ≤ halfWidth → flacher Kanalboden, BED_DEPTH tief
|
||||
* halfWidth < dist ≤ bedHW → linearer Anstieg von BED_DEPTH → 0
|
||||
* dist > bedHW → kein Eingriff
|
||||
*/
|
||||
private void collectRiverTargets(List<RiverPoint> splined,
|
||||
HashMap<Integer, Float> channelTargets,
|
||||
HashMap<Integer, Float> bankTargets) {
|
||||
for (int si = 1; si < splined.size(); si++) {
|
||||
RiverPoint pa = splined.get(si - 1);
|
||||
RiverPoint pb = splined.get(si);
|
||||
|
||||
float ax = pa.x(), ay = pa.y(), az = pa.z();
|
||||
float bx = pb.x(), by = pb.y(), bz = pb.z();
|
||||
float halfWidth = Math.max(MIN_HALF_WIDTH, (pa.width() + pb.width()) * 0.25f);
|
||||
float bedHW = halfWidth * (1f + 2f * BED_EXTRA); // 1.5 × halfWidth
|
||||
float paintHW = bedHW + SPLAT_WE_PER_PX; // Texturrand (+2 m)
|
||||
|
||||
float segDx = bx - ax, segDz = bz - az;
|
||||
float segLen2 = segDx*segDx + segDz*segDz;
|
||||
if (segLen2 < 0.001f) continue;
|
||||
|
||||
float scanHW = paintHW + 1f;
|
||||
int vxMin = Math.max(0, (int)((Math.min(ax, bx) - scanHW + TERRAIN_SIZE * 0.5f) / VERTEX_SPACING));
|
||||
int vxMax = Math.min(TOTAL_SIZE - 1,(int)((Math.max(ax, bx) + scanHW + 1 + TERRAIN_SIZE * 0.5f) / VERTEX_SPACING));
|
||||
int vzMin = Math.max(0, (int)((Math.min(az, bz) - scanHW + TERRAIN_SIZE * 0.5f) / VERTEX_SPACING));
|
||||
int vzMax = Math.min(TOTAL_SIZE - 1,(int)((Math.max(az, bz) + scanHW + 1 + TERRAIN_SIZE * 0.5f) / VERTEX_SPACING));
|
||||
|
||||
for (int vz = vzMin; vz <= vzMax; vz++) {
|
||||
for (int vx = vxMin; vx <= vxMax; vx++) {
|
||||
float worldX = vx * VERTEX_SPACING - TERRAIN_SIZE * 0.5f;
|
||||
float worldZ = vz * VERTEX_SPACING - TERRAIN_SIZE * 0.5f;
|
||||
|
||||
float t = FastMath.clamp(
|
||||
((worldX - ax)*segDx + (worldZ - az)*segDz) / segLen2, 0f, 1f);
|
||||
float projX = ax + t*segDx, projZ = az + t*segDz;
|
||||
float dist = FastMath.sqrt(
|
||||
(worldX - projX)*(worldX - projX) + (worldZ - projZ)*(worldZ - projZ));
|
||||
if (dist > paintHW) continue;
|
||||
|
||||
float waterY = ay + t * (by - ay);
|
||||
int vidx = vz * TOTAL_SIZE + vx;
|
||||
|
||||
if (dist <= bedHW) {
|
||||
float depth;
|
||||
if (dist <= halfWidth) {
|
||||
depth = BED_DEPTH;
|
||||
} else {
|
||||
depth = BED_DEPTH * (1f - (dist - halfWidth) / (bedHW - halfWidth));
|
||||
}
|
||||
float target = waterY - WATER_SINK - depth;
|
||||
if (originalHeightMap != null && originalHeightMap[vidx] - target > 20f) continue;
|
||||
channelTargets.merge(vidx, target, Math::min);
|
||||
} else {
|
||||
// Uferzone: Höhe exakt auf Wasseroberfläche setzen
|
||||
bankTargets.merge(vidx, waterY - WATER_SINK, Math::min);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Wendet vorberechnete Kanalziele in einem adjustHeight()-Aufruf an. */
|
||||
private void applyTargets(HashMap<Integer, Float> channelTargets) {
|
||||
List<Vector2f> locs = new ArrayList<>(channelTargets.size());
|
||||
List<Float> deltas = new ArrayList<>(channelTargets.size());
|
||||
|
||||
for (Map.Entry<Integer, Float> e : channelTargets.entrySet()) {
|
||||
int vidx = e.getKey();
|
||||
float target = e.getValue();
|
||||
float curH = cachedHeightMap[vidx];
|
||||
if (curH > target) {
|
||||
int vx = vidx % TOTAL_SIZE, vz = vidx / TOTAL_SIZE;
|
||||
locs.add(new Vector2f(vx * VERTEX_SPACING - TERRAIN_SIZE * 0.5f,
|
||||
vz * VERTEX_SPACING - TERRAIN_SIZE * 0.5f));
|
||||
deltas.add(target - curH);
|
||||
cachedHeightMap[vidx] = target;
|
||||
modifiedVertices.add(vidx);
|
||||
}
|
||||
}
|
||||
|
||||
if (!locs.isEmpty()) {
|
||||
terrain.adjustHeight(locs, deltas);
|
||||
terrain.updateModelBound();
|
||||
}
|
||||
}
|
||||
|
||||
/** Setzt Ufer-Vertices exakt auf die Wasseroberfläche (hebt und senkt). Channel-Vertices haben Vorrang. */
|
||||
private void applyBankTargets(HashMap<Integer, Float> bankTargets, Set<Integer> channelVerts) {
|
||||
List<Vector2f> locs = new ArrayList<>(bankTargets.size());
|
||||
List<Float> deltas = new ArrayList<>(bankTargets.size());
|
||||
|
||||
for (Map.Entry<Integer, Float> e : bankTargets.entrySet()) {
|
||||
int vidx = e.getKey();
|
||||
if (channelVerts.contains(vidx)) continue;
|
||||
float target = e.getValue();
|
||||
float curH = cachedHeightMap[vidx];
|
||||
float delta = target - curH;
|
||||
if (Math.abs(delta) < 0.01f) continue;
|
||||
int vx = vidx % TOTAL_SIZE, vz = vidx / TOTAL_SIZE;
|
||||
locs.add(new Vector2f(vx * VERTEX_SPACING - TERRAIN_SIZE * 0.5f,
|
||||
vz * VERTEX_SPACING - TERRAIN_SIZE * 0.5f));
|
||||
deltas.add(delta);
|
||||
cachedHeightMap[vidx] = target;
|
||||
modifiedVertices.add(vidx);
|
||||
}
|
||||
|
||||
if (!locs.isEmpty()) {
|
||||
terrain.adjustHeight(locs, deltas);
|
||||
terrain.updateModelBound();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Einzelnen Fluss graben (ohne Reset). Nur für isolierte Fälle —
|
||||
* für Editor-Finalisierung/Löschen immer reapplyAllRivers() verwenden.
|
||||
*/
|
||||
public void carveRiver(List<RiverPoint> controlPts) {
|
||||
if (terrain == null || cachedHeightMap == null || controlPts.size() < 2) return;
|
||||
List<RiverPoint> splined = RiverSpline.subdivide(controlPts);
|
||||
HashMap<Integer, Float> channelTargets = new HashMap<>();
|
||||
HashMap<Integer, Float> bankTargets = new HashMap<>();
|
||||
collectRiverTargets(splined, channelTargets, bankTargets);
|
||||
applyTargets(channelTargets);
|
||||
applyBankTargets(bankTargets, channelTargets.keySet());
|
||||
if (splatR != null) paintRiverSplat(splined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Malt Textur 4 (Kanal A) entlang des gesplineten Flusses.
|
||||
*
|
||||
* Für jeden Splatmap-Pixel im AABB des Flusses wird der minimale Abstand
|
||||
* zur gesamten Pfadlinie berechnet — kein per-Segment-Clipping, daher
|
||||
* folgt die Bemalung exakt dem Kurvenverlauf.
|
||||
*
|
||||
* Stärke maximal 70 % (0,3 × Gras bleibt immer), damit kein dunkler
|
||||
* Schatten-Effekt entsteht wenn die Textur dunkler als Gras ist.
|
||||
*/
|
||||
private void paintRiverSplat(List<RiverPoint> splined) {
|
||||
if (splined.size() < 2) return;
|
||||
|
||||
// Globales AABB über alle Punkte inkl. Clearing-Zone (+16 m über Bettkante)
|
||||
// Die Clearing-Zone entfernt G/B-Reste älterer Algorithmen auch außerhalb der Bemalung.
|
||||
final float CLEAR_EXTRA = 16f;
|
||||
float globalMinX = Float.MAX_VALUE, globalMaxX = -Float.MAX_VALUE;
|
||||
float globalMinZ = Float.MAX_VALUE, globalMaxZ = -Float.MAX_VALUE;
|
||||
for (RiverPoint pt : splined) {
|
||||
float hw = Math.max(MIN_HALF_WIDTH, pt.width() * 0.5f);
|
||||
float pad = hw * (1f + 2f * BED_EXTRA) + CLEAR_EXTRA + SPLAT_WE_PER_PX;
|
||||
if (pt.x() - pad < globalMinX) globalMinX = pt.x() - pad;
|
||||
if (pt.x() + pad > globalMaxX) globalMaxX = pt.x() + pad;
|
||||
if (pt.z() - pad < globalMinZ) globalMinZ = pt.z() - pad;
|
||||
if (pt.z() + pad > globalMaxZ) globalMaxZ = pt.z() + pad;
|
||||
}
|
||||
|
||||
int pxMin = Math.max(0, (int)((globalMinX + WORLD_HALF) / SPLAT_WE_PER_PX) - 1);
|
||||
int pxMax = Math.min(SPLAT_SIZE-1, (int)((globalMaxX + WORLD_HALF) / SPLAT_WE_PER_PX) + 1);
|
||||
int pzMin = Math.max(0, (SPLAT_SIZE-1) - (int)((globalMaxZ + WORLD_HALF) / SPLAT_WE_PER_PX) - 1);
|
||||
int pzMax = Math.min(SPLAT_SIZE-1, (SPLAT_SIZE-1) - (int)((globalMinZ + WORLD_HALF) / SPLAT_WE_PER_PX) + 1);
|
||||
|
||||
boolean changed = false;
|
||||
for (int pz = pzMin; pz <= pzMax; pz++) {
|
||||
// Pixel-Position = Vertex-Raster (kein +0,5 Offset)
|
||||
float worldZ = (SPLAT_SIZE - 1 - pz) * SPLAT_WE_PER_PX - WORLD_HALF;
|
||||
for (int px = pxMin; px <= pxMax; px++) {
|
||||
float worldX = px * SPLAT_WE_PER_PX - WORLD_HALF;
|
||||
|
||||
// Minimalen Abstand zur gesamten Pfadlinie bestimmen
|
||||
float minDist = Float.MAX_VALUE;
|
||||
float nearHalfW = MIN_HALF_WIDTH;
|
||||
float nearBedHW = MIN_HALF_WIDTH * (1f + 2f * BED_EXTRA);
|
||||
|
||||
for (int si = 1; si < splined.size(); si++) {
|
||||
RiverPoint pa = splined.get(si - 1);
|
||||
RiverPoint pb = splined.get(si);
|
||||
float ax = pa.x(), az = pa.z();
|
||||
float segDx = pb.x() - ax, segDz = pb.z() - az;
|
||||
float segLen2 = segDx*segDx + segDz*segDz;
|
||||
if (segLen2 < 0.001f) continue;
|
||||
|
||||
float t = FastMath.clamp(((worldX-ax)*segDx + (worldZ-az)*segDz) / segLen2, 0f, 1f);
|
||||
float projX = ax + t*segDx, projZ = az + t*segDz;
|
||||
float d = FastMath.sqrt((worldX-projX)*(worldX-projX) + (worldZ-projZ)*(worldZ-projZ));
|
||||
if (d < minDist) {
|
||||
minDist = d;
|
||||
float hw = Math.max(MIN_HALF_WIDTH, (pa.width() + pb.width()) * 0.25f);
|
||||
nearHalfW = hw;
|
||||
nearBedHW = hw * (1f + 2f * BED_EXTRA);
|
||||
}
|
||||
}
|
||||
|
||||
float paintHW = nearBedHW + SPLAT_WE_PER_PX;
|
||||
float clearHW = nearBedHW + CLEAR_EXTRA;
|
||||
if (minDist > clearHW) continue;
|
||||
|
||||
int sidx = pz * SPLAT_SIZE + px;
|
||||
int bi = sidx * 4;
|
||||
|
||||
// Immer G/B in der gesamten Clearing-Zone nullen (entfernt Reste alter Algorithmen)
|
||||
splatG[sidx] = 0;
|
||||
splatB[sidx] = 0;
|
||||
splatBuf.put(bi + 1, (byte) 0);
|
||||
splatBuf.put(bi + 2, (byte) 0);
|
||||
changed = true;
|
||||
|
||||
if (minDist > paintHW) continue; // nur räumen, nicht bemalen
|
||||
|
||||
float strength;
|
||||
if (minDist <= nearHalfW) {
|
||||
strength = 1.0f;
|
||||
} else if (minDist <= nearBedHW) {
|
||||
strength = 1.0f - (minDist - nearHalfW) / (nearBedHW - nearHalfW);
|
||||
} else {
|
||||
strength = (paintHW - minDist) / SPLAT_WE_PER_PX * 0.15f;
|
||||
}
|
||||
if (strength <= 0f) continue;
|
||||
|
||||
// Maximal 70 % River-Textur → immer 30 % Gras → kein Schatten-Effekt
|
||||
float ps = Math.min(strength, 0.7f);
|
||||
splatR[sidx] = (byte) Math.round((originalSplatR[sidx] & 0xFF) * (1f - ps));
|
||||
splatA[sidx] = (byte) Math.round(ps * 255);
|
||||
splatBuf.put(bi, splatR[sidx]);
|
||||
splatBuf.put(bi + 3, splatA[sidx]);
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
splatBuf.rewind();
|
||||
splatImage.setUpdateNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gräbt ein Flussbett zwischen zwei Wasseroberflächenpunkten A und B.
|
||||
* Alle Terrain-Vertices innerhalb von halfWidth werden auf die linear
|
||||
@@ -717,22 +1085,26 @@ public class TerrainEditorState extends BaseAppState {
|
||||
if (segLen2 < 0.001f) return;
|
||||
|
||||
// Tiefe proportional zur Breite: 0,5m bei 4m Breite, 1,0m bei 10m Breite
|
||||
float width = halfWidth * 2f;
|
||||
float maxDepth = Math.max(0.5f, Math.min(1.0f, 0.5f + (width - 4f) / 12f));
|
||||
// + 0,5m Wasserabsenkung (Wasseroberfläche sitzt 0,5m unter Kontrollpunkten)
|
||||
float width = halfWidth * 2f;
|
||||
float maxDepth = Math.max(0.5f, Math.min(1.0f, 0.5f + (width - 4f) / 12f));
|
||||
float WATER_SINK = 0.5f;
|
||||
float BANK_WIDTH = 1.5f; // Breite der Böschungszone außerhalb des Kanals
|
||||
float BANK_HEIGHT = 0.5f; // Höhe der Böschung über Wasseroberfläche
|
||||
|
||||
// ── Terrain-Vertices graben ──────────────────────────────────────────
|
||||
int vxMin = Math.max(0, (int)((Math.min(ax, bx) - halfWidth - 1) + TERRAIN_SIZE * 0.5f));
|
||||
int vxMax = Math.min(TOTAL_SIZE - 1, (int)((Math.max(ax, bx) + halfWidth + 2) + TERRAIN_SIZE * 0.5f));
|
||||
int vzMin = Math.max(0, (int)((Math.min(az, bz) - halfWidth - 1) + TERRAIN_SIZE * 0.5f));
|
||||
int vzMax = Math.min(TOTAL_SIZE - 1, (int)((Math.max(az, bz) + halfWidth + 2) + TERRAIN_SIZE * 0.5f));
|
||||
// Scan-Bereich inklusive Böschungszone erweitern
|
||||
int vxMin = Math.max(0, (int)((Math.min(ax, bx) - halfWidth - BANK_WIDTH - 1 + TERRAIN_SIZE * 0.5f) / VERTEX_SPACING));
|
||||
int vxMax = Math.min(TOTAL_SIZE - 1,(int)((Math.max(ax, bx) + halfWidth + BANK_WIDTH + 2 + TERRAIN_SIZE * 0.5f) / VERTEX_SPACING));
|
||||
int vzMin = Math.max(0, (int)((Math.min(az, bz) - halfWidth - BANK_WIDTH - 1 + TERRAIN_SIZE * 0.5f) / VERTEX_SPACING));
|
||||
int vzMax = Math.min(TOTAL_SIZE - 1,(int)((Math.max(az, bz) + halfWidth + BANK_WIDTH + 2 + TERRAIN_SIZE * 0.5f) / VERTEX_SPACING));
|
||||
|
||||
List<Vector2f> locs = new ArrayList<>();
|
||||
List<Float> deltas = new ArrayList<>();
|
||||
|
||||
for (int vz = vzMin; vz <= vzMax; vz++) {
|
||||
for (int vx = vxMin; vx <= vxMax; vx++) {
|
||||
float worldX = vx - TERRAIN_SIZE * 0.5f;
|
||||
float worldZ = vz - TERRAIN_SIZE * 0.5f;
|
||||
float worldX = vx * VERTEX_SPACING - TERRAIN_SIZE * 0.5f;
|
||||
float worldZ = vz * VERTEX_SPACING - TERRAIN_SIZE * 0.5f;
|
||||
|
||||
float t = ((worldX - ax) * segDx + (worldZ - az) * segDz) / segLen2;
|
||||
t = FastMath.clamp(t, 0f, 1f);
|
||||
@@ -740,21 +1112,31 @@ public class TerrainEditorState extends BaseAppState {
|
||||
float projX = ax + t * segDx, projZ = az + t * segDz;
|
||||
float dist = FastMath.sqrt((worldX - projX) * (worldX - projX)
|
||||
+ (worldZ - projZ) * (worldZ - projZ));
|
||||
if (dist > halfWidth) continue;
|
||||
|
||||
float waterY = ay + t * (by - ay);
|
||||
// U-Form: 60% des Kanals als flacher Boden, danach linearer Anstieg
|
||||
float norm = dist / halfWidth;
|
||||
float uShape = 1.0f - FastMath.clamp((norm - 0.6f) / 0.4f, 0f, 1f);
|
||||
float depth = maxDepth * uShape;
|
||||
float target = waterY - depth;
|
||||
int idx = vz * TOTAL_SIZE + vx;
|
||||
float curH = cachedHeightMap[idx];
|
||||
|
||||
int idx = vz * TOTAL_SIZE + vx;
|
||||
float curH = cachedHeightMap[idx];
|
||||
if (curH > target) {
|
||||
deltas.add(target - curH);
|
||||
locs.add(new Vector2f(worldX, worldZ));
|
||||
cachedHeightMap[idx] = target;
|
||||
if (dist <= halfWidth) {
|
||||
// ── Kanal graben (Wasseroberfläche + U-förmiges Bett) ──────
|
||||
float norm = dist / halfWidth;
|
||||
float uShape = 1.0f - FastMath.clamp((norm - 0.6f) / 0.4f, 0f, 1f);
|
||||
float target = waterY - WATER_SINK - maxDepth * uShape;
|
||||
if (curH > target) {
|
||||
deltas.add(target - curH);
|
||||
locs.add(new Vector2f(worldX, worldZ));
|
||||
cachedHeightMap[idx] = target;
|
||||
}
|
||||
} else if (dist <= halfWidth + BANK_WIDTH) {
|
||||
// ── Böschung aufschütten ──────────────────────────────────
|
||||
// Quadratischer Abfall: volle Höhe am Kanalrand, 0 am Außenrand
|
||||
float bankT = 1.0f - (dist - halfWidth) / BANK_WIDTH;
|
||||
float target = waterY + BANK_HEIGHT * bankT * bankT;
|
||||
if (curH < target) {
|
||||
deltas.add(target - curH);
|
||||
locs.add(new Vector2f(worldX, worldZ));
|
||||
cachedHeightMap[idx] = target;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -819,12 +1201,25 @@ public class TerrainEditorState extends BaseAppState {
|
||||
try {
|
||||
MapData data = new MapData();
|
||||
|
||||
if (cachedHeightMap != null) {
|
||||
System.arraycopy(cachedHeightMap, 0, data.terrainHeight, 0,
|
||||
Math.min(cachedHeightMap.length, data.terrainHeight.length));
|
||||
// Post-River-Terrain speichern: Das Spiel erhält korrekt eingegrabene Flussbetten.
|
||||
// originalHeightMap bleibt in-session als Rücksetz-Basis erhalten.
|
||||
float[] saveHeight = (cachedHeightMap != null) ? cachedHeightMap : originalHeightMap;
|
||||
if (saveHeight != null) {
|
||||
if (saveHeight.length == data.terrainHeight.length) {
|
||||
System.arraycopy(saveHeight, 0, data.terrainHeight, 0, saveHeight.length);
|
||||
} else {
|
||||
upsampleHeights(saveHeight, TOTAL_SIZE, data.terrainHeight, MapData.TERRAIN_VERTS);
|
||||
}
|
||||
}
|
||||
// Uferkante mit 0,25 m Präzision exakt auf Wasseroberfläche setzen
|
||||
if (riverEditorState != null) {
|
||||
applyHighResBankLeveling(data.terrainHeight, riverEditorState.getPlacedRivers());
|
||||
}
|
||||
|
||||
if (splatR != null) {
|
||||
// Bemalte Splatmap speichern (mit Fluss-Textur) → Spiel zeigt Fluss-Textur korrekt.
|
||||
// Beim nächsten Laden strippt initSplatmap() die Fluss-Farbe für originalSplatX,
|
||||
// sodass reapplyAllRivers ohne doppeltes Malen neu aufträgt.
|
||||
System.arraycopy(splatR, 0, data.splatR, 0, data.splatR.length);
|
||||
System.arraycopy(splatG, 0, data.splatG, 0, data.splatG.length);
|
||||
System.arraycopy(splatB, 0, data.splatB, 0, data.splatB.length);
|
||||
@@ -941,8 +1336,8 @@ public class TerrainEditorState extends BaseAppState {
|
||||
if (cachedHeightMap == null) return;
|
||||
for (int i = 0; i < locs.size(); i++) {
|
||||
Vector2f loc = locs.get(i);
|
||||
int vx = Math.round(loc.x + TERRAIN_SIZE * 0.5f);
|
||||
int vz = Math.round(loc.y + TERRAIN_SIZE * 0.5f);
|
||||
int vx = Math.round((loc.x + TERRAIN_SIZE * 0.5f) / VERTEX_SPACING);
|
||||
int vz = Math.round((loc.y + TERRAIN_SIZE * 0.5f) / VERTEX_SPACING);
|
||||
if (vx >= 0 && vx < TOTAL_SIZE && vz >= 0 && vz < TOTAL_SIZE)
|
||||
cachedHeightMap[vz * TOTAL_SIZE + vx] += deltas.get(i);
|
||||
}
|
||||
@@ -1000,9 +1395,9 @@ public class TerrainEditorState extends BaseAppState {
|
||||
|
||||
private void modifyHeight(Vector3f worldContact, float delta, int mode) {
|
||||
float radius = (float) input.heightTool.brushRadius.getValue();
|
||||
int r = (int) Math.ceil(radius);
|
||||
int cx = Math.round(worldContact.x + TERRAIN_SIZE * 0.5f);
|
||||
int cz = Math.round(worldContact.z + TERRAIN_SIZE * 0.5f);
|
||||
int r = (int) Math.ceil(radius / VERTEX_SPACING);
|
||||
int cx = Math.round((worldContact.x + TERRAIN_SIZE * 0.5f) / VERTEX_SPACING);
|
||||
int cz = Math.round((worldContact.z + TERRAIN_SIZE * 0.5f) / VERTEX_SPACING);
|
||||
|
||||
List<Vector2f> locs = new ArrayList<>();
|
||||
List<Float> deltas = new ArrayList<>();
|
||||
@@ -1012,7 +1407,7 @@ public class TerrainEditorState extends BaseAppState {
|
||||
int vx = cx + dx, vz = cz + dz;
|
||||
if (vx < 0 || vx >= TOTAL_SIZE || vz < 0 || vz >= TOTAL_SIZE) continue;
|
||||
|
||||
float dist = FastMath.sqrt(dx * dx + dz * dz);
|
||||
float dist = FastMath.sqrt(dx * dx + dz * dz) * VERTEX_SPACING;
|
||||
if (dist >= radius) continue;
|
||||
|
||||
float t = dist / radius;
|
||||
@@ -1035,7 +1430,8 @@ public class TerrainEditorState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
locs.add(new Vector2f(vx - TERRAIN_SIZE * 0.5f, vz - TERRAIN_SIZE * 0.5f));
|
||||
locs.add(new Vector2f(vx * VERTEX_SPACING - TERRAIN_SIZE * 0.5f,
|
||||
vz * VERTEX_SPACING - TERRAIN_SIZE * 0.5f));
|
||||
deltas.add(delta * falloff);
|
||||
}
|
||||
}
|
||||
@@ -1051,9 +1447,9 @@ public class TerrainEditorState extends BaseAppState {
|
||||
private void smoothHeight(Vector3f worldContact) {
|
||||
float radius = (float) input.heightTool.brushRadius.getValue();
|
||||
float strength = (float) input.heightTool.brushStrength.getValue();
|
||||
int r = (int) Math.ceil(radius);
|
||||
int cx = Math.round(worldContact.x + TERRAIN_SIZE * 0.5f);
|
||||
int cz = Math.round(worldContact.z + TERRAIN_SIZE * 0.5f);
|
||||
int r = (int) Math.ceil(radius / VERTEX_SPACING);
|
||||
int cx = Math.round((worldContact.x + TERRAIN_SIZE * 0.5f) / VERTEX_SPACING);
|
||||
int cz = Math.round((worldContact.z + TERRAIN_SIZE * 0.5f) / VERTEX_SPACING);
|
||||
|
||||
if (cachedHeightMap == null) return;
|
||||
float[] hmap = cachedHeightMap;
|
||||
@@ -1066,12 +1462,13 @@ public class TerrainEditorState extends BaseAppState {
|
||||
for (int dx = -r; dx <= r; dx++) {
|
||||
int vx = cx + dx, vz = cz + dz;
|
||||
if (vx < 0 || vx >= TOTAL_SIZE || vz < 0 || vz >= TOTAL_SIZE) continue;
|
||||
float dist = FastMath.sqrt(dx * dx + dz * dz);
|
||||
float dist = FastMath.sqrt(dx * dx + dz * dz) * VERTEX_SPACING;
|
||||
if (dist >= radius) continue;
|
||||
int idx = vz * TOTAL_SIZE + vx;
|
||||
float h = hmap[idx];
|
||||
if (!Float.isFinite(h)) continue;
|
||||
locs.add(new Vector2f(vx - TERRAIN_SIZE * 0.5f, vz - TERRAIN_SIZE * 0.5f));
|
||||
locs.add(new Vector2f(vx * VERTEX_SPACING - TERRAIN_SIZE * 0.5f,
|
||||
vz * VERTEX_SPACING - TERRAIN_SIZE * 0.5f));
|
||||
heights.add(h);
|
||||
dists.add(dist);
|
||||
}
|
||||
@@ -1100,8 +1497,8 @@ public class TerrainEditorState extends BaseAppState {
|
||||
/** Liest die Terrain-Höhe am nächstgelegenen Vertex zum Kontaktpunkt. */
|
||||
private float sampleTerrainHeight(Vector3f worldContact) {
|
||||
if (cachedHeightMap == null) return Float.NaN;
|
||||
int cx = Math.round(worldContact.x + TERRAIN_SIZE * 0.5f);
|
||||
int cz = Math.round(worldContact.z + TERRAIN_SIZE * 0.5f);
|
||||
int cx = Math.round((worldContact.x + TERRAIN_SIZE * 0.5f) / VERTEX_SPACING);
|
||||
int cz = Math.round((worldContact.z + TERRAIN_SIZE * 0.5f) / VERTEX_SPACING);
|
||||
if (cx < 0 || cx >= TOTAL_SIZE || cz < 0 || cz >= TOTAL_SIZE) return Float.NaN;
|
||||
return cachedHeightMap[cz * TOTAL_SIZE + cx];
|
||||
}
|
||||
@@ -1111,9 +1508,9 @@ public class TerrainEditorState extends BaseAppState {
|
||||
float radius = (float) input.heightTool.brushRadius.getValue();
|
||||
float strength = (float) input.heightTool.brushStrength.getValue();
|
||||
float target = (float) input.heightTool.plateauHeight.getValue();
|
||||
int r = (int) Math.ceil(radius);
|
||||
int cx = Math.round(worldContact.x + TERRAIN_SIZE * 0.5f);
|
||||
int cz = Math.round(worldContact.z + TERRAIN_SIZE * 0.5f);
|
||||
int r = (int) Math.ceil(radius / VERTEX_SPACING);
|
||||
int cx = Math.round((worldContact.x + TERRAIN_SIZE * 0.5f) / VERTEX_SPACING);
|
||||
int cz = Math.round((worldContact.z + TERRAIN_SIZE * 0.5f) / VERTEX_SPACING);
|
||||
|
||||
if (cachedHeightMap == null) return;
|
||||
|
||||
@@ -1124,7 +1521,7 @@ public class TerrainEditorState extends BaseAppState {
|
||||
for (int dx = -r; dx <= r; dx++) {
|
||||
int vx = cx + dx, vz = cz + dz;
|
||||
if (vx < 0 || vx >= TOTAL_SIZE || vz < 0 || vz >= TOTAL_SIZE) continue;
|
||||
float dist = FastMath.sqrt(dx * dx + dz * dz);
|
||||
float dist = FastMath.sqrt(dx * dx + dz * dz) * VERTEX_SPACING;
|
||||
if (dist >= radius) continue;
|
||||
|
||||
float curH = cachedHeightMap[vz * TOTAL_SIZE + vx];
|
||||
@@ -1137,7 +1534,8 @@ public class TerrainEditorState extends BaseAppState {
|
||||
: 1f - FastMath.pow((t - edge) / (1f - edge), 2f);
|
||||
|
||||
float blend = FastMath.clamp(falloff * (strength / 50f), 0f, 1f);
|
||||
locs.add(new Vector2f(vx - TERRAIN_SIZE * 0.5f, vz - TERRAIN_SIZE * 0.5f));
|
||||
locs.add(new Vector2f(vx * VERTEX_SPACING - TERRAIN_SIZE * 0.5f,
|
||||
vz * VERTEX_SPACING - TERRAIN_SIZE * 0.5f));
|
||||
deltas.add((target - curH) * blend);
|
||||
}
|
||||
}
|
||||
@@ -1402,4 +1800,103 @@ public class TerrainEditorState extends BaseAppState {
|
||||
livePlayerMarker.setLocalTranslation(x, input.livePlayerY + 0.9f, input.livePlayerZ);
|
||||
}
|
||||
}
|
||||
|
||||
// Nearest-Neighbor-Downsampling (src 16385 → dst 4097)
|
||||
private static void downsampleHeights(float[] src, int srcSize, float[] dst, int dstSize) {
|
||||
float step = (float)(srcSize - 1) / (dstSize - 1);
|
||||
for (int dz = 0; dz < dstSize; dz++) {
|
||||
int sz = Math.round(dz * step);
|
||||
for (int dx = 0; dx < dstSize; dx++) {
|
||||
dst[dz * dstSize + dx] = src[sz * srcSize + Math.round(dx * step)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bilinear-Upsampling (src 4097 → dst 16385)
|
||||
private static void upsampleHeights(float[] src, int srcSize, float[] dst, int dstSize) {
|
||||
float scale = (float)(srcSize - 1) / (dstSize - 1);
|
||||
for (int dz = 0; dz < dstSize; dz++) {
|
||||
float sz = dz * scale;
|
||||
int sz0 = Math.min((int)sz, srcSize - 2), sz1 = sz0 + 1;
|
||||
float fz = sz - sz0;
|
||||
for (int dx = 0; dx < dstSize; dx++) {
|
||||
float sx = dx * scale;
|
||||
int sx0 = Math.min((int)sx, srcSize - 2), sx1 = sx0 + 1;
|
||||
float fx = sx - sx0;
|
||||
dst[dz * dstSize + dx] =
|
||||
(src[sz0 * srcSize + sx0] * (1 - fx) + src[sz0 * srcSize + sx1] * fx) * (1 - fz)
|
||||
+ (src[sz1 * srcSize + sx0] * (1 - fx) + src[sz1 * srcSize + sx1] * fx) * fz;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt Uferkanten-Vertices (bedHW < dist ≤ paintHW) in der 16385²-Heightmap
|
||||
* exakt auf die Wasseroberfläche (0,25 m Präzision).
|
||||
* Wird nach dem Hochskalieren in performSave() aufgerufen.
|
||||
*/
|
||||
private static void applyHighResBankLeveling(float[] heights, List<List<RiverPoint>> rivers) {
|
||||
if (heights == null || rivers == null || rivers.isEmpty()) return;
|
||||
final int HR_VERTS = MapData.TERRAIN_VERTS; // 16385
|
||||
final float HR_SPACING = 4096f / (HR_VERTS - 1); // 0.25 m/Vertex
|
||||
final float HR_HALF = 2048f;
|
||||
|
||||
for (List<RiverPoint> controlPts : rivers) {
|
||||
if (controlPts == null || controlPts.size() < 2) continue;
|
||||
List<RiverPoint> splined = RiverSpline.subdivide(controlPts);
|
||||
if (splined.size() < 2) continue;
|
||||
|
||||
// AABB (inkl. paintHW-Rand)
|
||||
float minX = Float.MAX_VALUE, maxX = -Float.MAX_VALUE;
|
||||
float minZ = Float.MAX_VALUE, maxZ = -Float.MAX_VALUE;
|
||||
for (RiverPoint pt : splined) {
|
||||
float hw = Math.max(MIN_HALF_WIDTH, pt.width() * 0.5f);
|
||||
float pad = hw * (1f + 2f * BED_EXTRA) + SPLAT_WE_PER_PX + 1f;
|
||||
if (pt.x() - pad < minX) minX = pt.x() - pad;
|
||||
if (pt.x() + pad > maxX) maxX = pt.x() + pad;
|
||||
if (pt.z() - pad < minZ) minZ = pt.z() - pad;
|
||||
if (pt.z() + pad > maxZ) maxZ = pt.z() + pad;
|
||||
}
|
||||
int vxMin = Math.max(0, (int)((minX + HR_HALF) / HR_SPACING));
|
||||
int vxMax = Math.min(HR_VERTS-1, (int)((maxX + HR_HALF) / HR_SPACING) + 1);
|
||||
int vzMin = Math.max(0, (int)((minZ + HR_HALF) / HR_SPACING));
|
||||
int vzMax = Math.min(HR_VERTS-1, (int)((maxZ + HR_HALF) / HR_SPACING) + 1);
|
||||
|
||||
for (int vz = vzMin; vz <= vzMax; vz++) {
|
||||
for (int vx = vxMin; vx <= vxMax; vx++) {
|
||||
float worldX = vx * HR_SPACING - HR_HALF;
|
||||
float worldZ = vz * HR_SPACING - HR_HALF;
|
||||
|
||||
float minDist = Float.MAX_VALUE;
|
||||
float bestWaterY = 0f;
|
||||
float bestBedHW = 0f;
|
||||
float bestPaintHW = 0f;
|
||||
|
||||
for (int si = 1; si < splined.size(); si++) {
|
||||
RiverPoint pa = splined.get(si - 1);
|
||||
RiverPoint pb = splined.get(si);
|
||||
float ax = pa.x(), ay = pa.y(), az = pa.z();
|
||||
float bx = pb.x(), by = pb.y(), bz = pb.z();
|
||||
float segDx = bx - ax, segDz = bz - az;
|
||||
float segLen2 = segDx*segDx + segDz*segDz;
|
||||
if (segLen2 < 0.001f) continue;
|
||||
float t = FastMath.clamp(((worldX-ax)*segDx + (worldZ-az)*segDz) / segLen2, 0f, 1f);
|
||||
float projX = ax + t*segDx, projZ = az + t*segDz;
|
||||
float d = FastMath.sqrt((worldX-projX)*(worldX-projX) + (worldZ-projZ)*(worldZ-projZ));
|
||||
if (d < minDist) {
|
||||
minDist = d;
|
||||
float hw = Math.max(MIN_HALF_WIDTH, (pa.width() + pb.width()) * 0.25f);
|
||||
bestBedHW = hw * (1f + 2f * BED_EXTRA);
|
||||
bestPaintHW = bestBedHW + SPLAT_WE_PER_PX;
|
||||
bestWaterY = ay + t * (by - ay);
|
||||
}
|
||||
}
|
||||
|
||||
if (minDist > bestBedHW && minDist <= bestPaintHW) {
|
||||
heights[vz * HR_VERTS + vx] = bestWaterY - WATER_SINK;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,14 +324,14 @@ public class WorldScene extends BaseAppState {
|
||||
|
||||
/**
|
||||
* Erstellt ein Terrain aus der gespeicherten {@link MapData}.
|
||||
* Die 4097×4097 Editor-Daten werden auf 513×513 heruntergesampelt
|
||||
* (jeder 8. Vertex), mit Scale (8, 1, 8) auf die gleiche Weltgröße
|
||||
* Die 16385×16385 Editor-Daten werden auf 513×513 heruntergesampelt
|
||||
* (jeder 32. Vertex), mit Scale (8, 1, 8) auf die gleiche Weltgröße
|
||||
* 4096 × 4096 WE gebracht.
|
||||
*/
|
||||
private TerrainQuad buildTerrainFromMap(MapData map) {
|
||||
final int GAME_VERTS = 513; // 512 Zellen à 8 WE = 4096 WE
|
||||
final int STEP = 8; // 4096 / 512 = 8 Vertices überspringen
|
||||
final int SRC_VERTS = MapData.TERRAIN_VERTS; // 4097
|
||||
final int STEP = (MapData.TERRAIN_VERTS - 1) / (GAME_VERTS - 1); // 32
|
||||
final int SRC_VERTS = MapData.TERRAIN_VERTS; // 16385
|
||||
|
||||
float[] heights = new float[GAME_VERTS * GAME_VERTS];
|
||||
for (int gz = 0; gz < GAME_VERTS; gz++) {
|
||||
|
||||
@@ -40,7 +40,8 @@ import java.util.Random;
|
||||
public class RiverState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(RiverState.class);
|
||||
private static final float UV_SCALE = 6.0f;
|
||||
private static final float UV_SCALE = 6.0f;
|
||||
private static final float WATER_SINK = 0.5f; // Wasser liegt 0,5m unter den Kontrollpunkten
|
||||
|
||||
private Node riverNode;
|
||||
private AssetManager assets;
|
||||
@@ -118,7 +119,7 @@ public class RiverState extends BaseAppState {
|
||||
List<RiverPoint> run = RiverSpline.subdivide(pts.subList(i, j + 1));
|
||||
if (run.size() >= 2) {
|
||||
buildRibbonSection(run, wf);
|
||||
if (wf) buildWaterfallParticles(run.get(run.size() - 1));
|
||||
if (wf) buildWaterfallParticles(run.get(run.size() - 1), computeFlowDir(run));
|
||||
}
|
||||
i = j;
|
||||
}
|
||||
@@ -175,7 +176,7 @@ public class RiverState extends BaseAppState {
|
||||
if (right.lengthSquared() < 1e-6f) right.set(1f, 0f, 0f);
|
||||
|
||||
float halfW = pt.width() * 0.5f;
|
||||
float px = pt.x(), py = pt.y(), pz = pt.z();
|
||||
float px = pt.x(), py = pt.y() - WATER_SINK, pz = pt.z();
|
||||
float vCoord = arcLen[i] / UV_SCALE;
|
||||
|
||||
// Linker Rand (U=0), rechter Rand (U=1)
|
||||
@@ -208,28 +209,44 @@ public class RiverState extends BaseAppState {
|
||||
|
||||
private Material buildMaterial(boolean isWaterfall) {
|
||||
ColorRGBA tint = isWaterfall
|
||||
? new ColorRGBA(0.65f, 0.82f, 0.95f, 0.80f)
|
||||
: new ColorRGBA(0.10f, 0.30f, 0.62f, 0.85f);
|
||||
? new ColorRGBA(0.65f, 0.82f, 0.95f, 0.75f)
|
||||
: new ColorRGBA(0.10f, 0.30f, 0.62f, 0.68f);
|
||||
|
||||
Material mat;
|
||||
try {
|
||||
mat = new Material(assets, "MatDefs/FlowingWater.j3md");
|
||||
try {
|
||||
Texture nm = assets.loadTexture(
|
||||
"Common/MatDefs/Water/Textures/water_normalmap.png");
|
||||
|
||||
// Normal-Map: erst eigene Texturen versuchen, dann JME-Fallback
|
||||
Texture nm = loadTextureOr(
|
||||
isWaterfall ? "Textures/water/waterfall_normal.png"
|
||||
: "Textures/water/river_normal.jpg",
|
||||
"Common/MatDefs/Water/Textures/water_normalmap.png");
|
||||
if (nm != null) {
|
||||
nm.setWrap(Texture.WrapMode.Repeat);
|
||||
mat.setTexture("NormalMap", nm);
|
||||
} catch (Exception e) {
|
||||
log.warn("Normal-Map nicht ladbar, wird ohne Wellenstruktur gerendert");
|
||||
} else {
|
||||
log.warn("Keine Normal-Map geladen – Mesh ohne Wellenstruktur");
|
||||
}
|
||||
|
||||
if (foamTexture != null) {
|
||||
mat.setTexture("FoamMap", foamTexture);
|
||||
}
|
||||
mat.setColor("Tint", tint);
|
||||
mat.setFloat("UVScale", UV_SCALE);
|
||||
mat.setFloat("FlowSpeed", isWaterfall ? RiverPoint.WATERFALL_SPEED
|
||||
: RiverPoint.RIVER_SPEED);
|
||||
mat.setFloat("FoamAmount", isWaterfall ? 1.0f : 0.0f);
|
||||
|
||||
// Diffuse-Map: river.jpg für Fluss (Farbmodulation), waterfall_diffuse für Gischt
|
||||
String diffPath = isWaterfall ? "Textures/water/waterfall_diffuse.png"
|
||||
: "Textures/water/river.jpg";
|
||||
Texture diff = loadTextureOr(diffPath, null);
|
||||
if (diff != null) {
|
||||
diff.setWrap(Texture.WrapMode.Repeat);
|
||||
mat.setTexture("DiffuseMap", diff);
|
||||
}
|
||||
|
||||
mat.setColor("Tint", tint);
|
||||
mat.setFloat("UVScale", UV_SCALE);
|
||||
mat.setFloat("NormalUVScale", 0.5f); // 2x größeres Wellenmuster
|
||||
mat.setFloat("FlowSpeed", isWaterfall ? RiverPoint.WATERFALL_SPEED
|
||||
: RiverPoint.RIVER_SPEED);
|
||||
mat.setFloat("FoamAmount", isWaterfall ? 1.0f : 0.0f);
|
||||
} catch (Exception e) {
|
||||
log.warn("FlowingWater-Material nicht ladbar, Fallback auf Unshaded", e);
|
||||
mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
@@ -242,6 +259,20 @@ public class RiverState extends BaseAppState {
|
||||
return mat;
|
||||
}
|
||||
|
||||
/** Versucht primaryPath zu laden; schlägt das fehl, wird fallbackPath versucht (null = kein Fallback). */
|
||||
private Texture loadTextureOr(String primaryPath, String fallbackPath) {
|
||||
try {
|
||||
return assets.loadTexture(primaryPath);
|
||||
} catch (Exception ignored) {}
|
||||
if (fallbackPath == null) return null;
|
||||
try {
|
||||
return assets.loadTexture(fallbackPath);
|
||||
} catch (Exception e) {
|
||||
log.warn("Textur nicht ladbar: {} und {}", primaryPath, fallbackPath);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Worley-Noise Schaum-Textur ────────────────────────────────────────────
|
||||
|
||||
private Texture2D generateFoamTexture() {
|
||||
@@ -286,29 +317,58 @@ public class RiverState extends BaseAppState {
|
||||
|
||||
// ── Partikel-Emitter ──────────────────────────────────────────────────────
|
||||
|
||||
private void buildWaterfallParticles(RiverPoint base) {
|
||||
/**
|
||||
* Gischt-Partikel am Fuß des Wasserfalls.
|
||||
* Die Spray-Richtung ist das Spiegelbild der horizontalen Fließrichtung
|
||||
* (Wasser prallt auf und spritzt zurück + hoch).
|
||||
*/
|
||||
private void buildWaterfallParticles(RiverPoint base, Vector3f flowDir) {
|
||||
ParticleEmitter emitter = new ParticleEmitter(
|
||||
"waterfall_particles", ParticleMesh.Type.Triangle, 30);
|
||||
"waterfall_particles", ParticleMesh.Type.Triangle, 60);
|
||||
|
||||
Material pMat = new Material(assets, "Common/MatDefs/Misc/Particle.j3md");
|
||||
try {
|
||||
pMat.setTexture("Texture", assets.loadTexture("Effects/Smoke/Smoke.png"));
|
||||
} catch (Exception e) {
|
||||
log.warn("Partikel-Textur nicht ladbar", e);
|
||||
}
|
||||
Texture pTex = loadTextureOr("Textures/Water/spray.png", "Effects/Smoke/Smoke.png");
|
||||
if (pTex != null) pMat.setTexture("Texture", pTex);
|
||||
pMat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.AlphaAdditive);
|
||||
|
||||
emitter.setMaterial(pMat);
|
||||
emitter.setImagesX(1);
|
||||
emitter.setImagesY(1);
|
||||
emitter.setStartColor(new ColorRGBA(1f, 1f, 1f, 0.5f));
|
||||
emitter.setEndColor(new ColorRGBA(1f, 1f, 1f, 0f));
|
||||
emitter.setStartSize(1.2f);
|
||||
emitter.setEndSize(2.5f);
|
||||
emitter.setGravity(0f, -0.5f, 0f);
|
||||
emitter.setLowLife(0.8f);
|
||||
emitter.setHighLife(1.2f);
|
||||
emitter.setInitialVelocity(new Vector3f(0f, 3f, 0f));
|
||||
emitter.setVelocityVariation(0.6f);
|
||||
emitter.setParticlesPerSec(15);
|
||||
|
||||
// Hellweiße, leicht bläuliche Gischt – startet fast opak, verblasst komplett
|
||||
emitter.setStartColor(new ColorRGBA(0.90f, 0.95f, 1.00f, 0.55f));
|
||||
emitter.setEndColor (new ColorRGBA(1.00f, 1.00f, 1.00f, 0.00f));
|
||||
|
||||
// Partikel starten klein, werden als Nebelwolke größer
|
||||
emitter.setStartSize(0.4f);
|
||||
emitter.setEndSize (2.8f);
|
||||
|
||||
// Leichte Schwerkraft damit Gischt wieder fällt
|
||||
emitter.setGravity(0f, -1.2f, 0f);
|
||||
emitter.setLowLife (0.7f);
|
||||
emitter.setHighLife(1.6f);
|
||||
|
||||
// Spray-Richtung: nach oben + horizontal entgegen der Fließrichtung (Aufprall-Bounce)
|
||||
// flowDir zeigt bergab/in Fließrichtung; horizontale Umkehrung = Rückspritzer
|
||||
Vector3f sprayVel = new Vector3f(
|
||||
-flowDir.x * 1.8f,
|
||||
4.5f,
|
||||
-flowDir.z * 1.8f);
|
||||
emitter.setInitialVelocity(sprayVel);
|
||||
emitter.setVelocityVariation(0.75f); // hohe Variation → aufgefächerte Gischt-Wolke
|
||||
|
||||
emitter.setParticlesPerSec(35);
|
||||
emitter.setLocalTranslation(base.x(), base.y(), base.z());
|
||||
riverNode.attachChild(emitter);
|
||||
}
|
||||
|
||||
/** Fließrichtung am Ende eines Abschnitts aus den letzten Stützpunkten. */
|
||||
private Vector3f computeFlowDir(List<RiverPoint> pts) {
|
||||
int n = pts.size();
|
||||
RiverPoint a = pts.get(Math.max(0, n - 3));
|
||||
RiverPoint b = pts.get(n - 1);
|
||||
Vector3f dir = new Vector3f(b.x() - a.x(), b.y() - a.y(), b.z() - a.z());
|
||||
if (dir.lengthSquared() < 1e-6f) return new Vector3f(0f, -1f, 0f);
|
||||
return dir.normalizeLocal();
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1,3 +1,3 @@
|
||||
# blight_rivers.blr – Flussdaten
|
||||
# Format: x,y,z,width,uvSpeed | x,y,z,width,uvSpeed | ...
|
||||
3.71271,1.45984,-299.11252,8.98643,0.40000|11.98857,1.17747,-290.66177,8.98643,0.40000|31.99506,1.01111,-282.99625,8.98643,0.40000|48.89030,0.96111,-289.70255,8.98643,0.40000|63.18369,0.91111,-300.59106,8.98643,0.40000|72.72055,0.86111,-308.32092,8.98643,0.40000|81.47565,0.81111,-310.34119,8.98643,0.40000|88.85998,0.54894,-310.70313,8.98643,0.40000|95.29650,0.01967,-310.18607,8.98643,0.40000
|
||||
83.70156,11.20734,-377.98270,8.00000,0.40000|53.36767,11.15734,-386.29489,8.00000,0.40000|19.23992,11.10734,-376.33185,8.00000,0.40000|-22.25724,11.05734,-322.99374,8.00000,0.40000
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
# polygon soundPath volume crossfade
|
||||
67.278,-201.343;67.278,-201.343;57.509,-236.901;57.509,-236.901 1.0000 false
|
||||
-4.282,-291.410;-4.282,-291.410;-4.282,-291.410 1.0000 false
|
||||
|
||||
Reference in New Issue
Block a user