diff --git a/blight-assets/src/main/resources/MatDefs/FlowingWater.j3md b/blight-assets/src/main/resources/MatDefs/FlowingWater.j3md index 61fccea..350731d 100644 --- a/blight-assets/src/main/resources/MatDefs/FlowingWater.j3md +++ b/blight-assets/src/main/resources/MatDefs/FlowingWater.j3md @@ -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 } } } diff --git a/blight-assets/src/main/resources/Models/custom_mesh_0.j3o b/blight-assets/src/main/resources/Models/custom_mesh_0.j3o index 83c0c97..6d32f7c 100644 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_0.j3o and b/blight-assets/src/main/resources/Models/custom_mesh_0.j3o differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_1.j3o b/blight-assets/src/main/resources/Models/custom_mesh_1.j3o index a72e2d1..506be8e 100644 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_1.j3o and b/blight-assets/src/main/resources/Models/custom_mesh_1.j3o differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_2.j3o b/blight-assets/src/main/resources/Models/custom_mesh_2.j3o index 39ecd36..92244e6 100644 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_2.j3o and b/blight-assets/src/main/resources/Models/custom_mesh_2.j3o differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_3.j3o b/blight-assets/src/main/resources/Models/custom_mesh_3.j3o index 7c03e32..03a1b1a 100644 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_3.j3o and b/blight-assets/src/main/resources/Models/custom_mesh_3.j3o differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_4.j3o b/blight-assets/src/main/resources/Models/custom_mesh_4.j3o index 605e054..22e7c57 100644 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_4.j3o and b/blight-assets/src/main/resources/Models/custom_mesh_4.j3o differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_5.j3o b/blight-assets/src/main/resources/Models/custom_mesh_5.j3o index 5860cdf..eb7ec92 100644 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_5.j3o and b/blight-assets/src/main/resources/Models/custom_mesh_5.j3o differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_6.j3o b/blight-assets/src/main/resources/Models/custom_mesh_6.j3o index a82805d..ce73c21 100644 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_6.j3o and b/blight-assets/src/main/resources/Models/custom_mesh_6.j3o differ diff --git a/blight-assets/src/main/resources/Shaders/FlowingWater.frag b/blight-assets/src/main/resources/Shaders/FlowingWater.frag index e9eff5d..b476e4e 100644 --- a/blight-assets/src/main/resources/Shaders/FlowingWater.frag +++ b/blight-assets/src/main/resources/Shaders/FlowingWater.frag @@ -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); } diff --git a/blight-assets/src/main/resources/Shaders/FlowingWater.vert b/blight-assets/src/main/resources/Shaders/FlowingWater.vert index 9776adf..8a53805 100644 --- a/blight-assets/src/main/resources/Shaders/FlowingWater.vert +++ b/blight-assets/src/main/resources/Shaders/FlowingWater.vert @@ -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); } diff --git a/blight-common/src/main/java/de/blight/common/MapData.java b/blight-common/src/main/java/de/blight/common/MapData.java index 5e171b7..06aeccd 100644 --- a/blight-common/src/main/java/de/blight/common/MapData.java +++ b/blight-common/src/main/java/de/blight/common/MapData.java @@ -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; diff --git a/blight-common/src/main/java/de/blight/common/MapIO.java b/blight-common/src/main/java/de/blight/common/MapIO.java index 7307885..de5373c 100644 --- a/blight-common/src/main/java/de/blight/common/MapIO.java +++ b/blight-common/src/main/java/de/blight/common/MapIO.java @@ -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); + } + } + } } diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index 15b10c0..8513ed5 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -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")); diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.java b/blight-editor/src/main/java/de/blight/editor/SharedInput.java index d44f288..8a52707 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -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 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; diff --git a/blight-editor/src/main/java/de/blight/editor/state/RiverEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/RiverEditorState.java index 4c45b38..7f82419 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/RiverEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/RiverEditorState.java @@ -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 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; } diff --git a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java index c8efe30..5675dd1 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java @@ -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 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> allRivers) { + if (terrain == null || cachedHeightMap == null) return; + + // 1. Alle zuvor durch Flüsse veränderten Vertices auf Original zurücksetzen + if (!modifiedVertices.isEmpty()) { + List resetLocs = new ArrayList<>(modifiedVertices.size()); + List 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 channelTargets = new HashMap<>(); + HashMap bankTargets = new HashMap<>(); + List> allSplined = new ArrayList<>(allRivers.size()); + + for (List river : allRivers) { + if (river == null || river.size() < 2) continue; + List 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 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 splined, + HashMap channelTargets, + HashMap 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 channelTargets) { + List locs = new ArrayList<>(channelTargets.size()); + List deltas = new ArrayList<>(channelTargets.size()); + + for (Map.Entry 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 bankTargets, Set channelVerts) { + List locs = new ArrayList<>(bankTargets.size()); + List deltas = new ArrayList<>(bankTargets.size()); + + for (Map.Entry 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 controlPts) { + if (terrain == null || cachedHeightMap == null || controlPts.size() < 2) return; + List splined = RiverSpline.subdivide(controlPts); + HashMap channelTargets = new HashMap<>(); + HashMap 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 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 locs = new ArrayList<>(); List 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 locs = new ArrayList<>(); List 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> 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 controlPts : rivers) { + if (controlPts == null || controlPts.size() < 2) continue; + List 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; + } + } + } + } + } } diff --git a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java index c7237ff..5e425a3 100644 --- a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java +++ b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java @@ -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++) { diff --git a/blight-game/src/main/java/de/blight/game/state/RiverState.java b/blight-game/src/main/java/de/blight/game/state/RiverState.java index 90f36fb..c03cf3e 100644 --- a/blight-game/src/main/java/de/blight/game/state/RiverState.java +++ b/blight-game/src/main/java/de/blight/game/state/RiverState.java @@ -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 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 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(); + } } diff --git a/blight-map/src/main/map/blight_map.blm b/blight-map/src/main/map/blight_map.blm index c70b8a4..ace61c0 100644 Binary files a/blight-map/src/main/map/blight_map.blm and b/blight-map/src/main/map/blight_map.blm differ diff --git a/blight-map/src/main/map/blight_rivers.blr b/blight-map/src/main/map/blight_rivers.blr index cd09073..ad9fbca 100644 --- a/blight-map/src/main/map/blight_rivers.blr +++ b/blight-map/src/main/map/blight_rivers.blr @@ -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 diff --git a/blight-map/src/main/map/blight_sound_areas.bsa b/blight-map/src/main/map/blight_sound_areas.bsa index 6acdd3d..283c465 100644 --- a/blight-map/src/main/map/blight_sound_areas.bsa +++ b/blight-map/src/main/map/blight_sound_areas.bsa @@ -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