@@ -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 ) / 1 2f) ) ;
// + 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
// ── Terrai n-V ert ices 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 ) ) ;
// Sca n-B ere ich 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 - T ERRAIN_SIZE * 0 . 5f , vz - TERRAIN_SIZE * 0 . 5f ) ) ;
locs . add ( new Vector2f ( vx * V ERTEX_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 - T ERRAIN_SIZE * 0 . 5f , vz - TERRAIN_SIZE * 0 . 5f ) ) ;
locs . add ( new Vector2f ( vx * V ERTEX_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 - T ERRAIN_SIZE * 0 . 5f , vz - TERRAIN_SIZE * 0 . 5f ) ) ;
locs . add ( new Vector2f ( vx * V ERTEX_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 ;
}
}
}
}
}
}