@@ -3,6 +3,8 @@ package de.blight.common.map;
import de.blight.common.* ;
import de.blight.common.model.Location ;
import org.slf4j.Logger ;
import org.slf4j.LoggerFactory ;
import javax.imageio.ImageIO ;
import java.awt.* ;
import java.awt.geom.Path2D ;
@@ -21,6 +23,8 @@ import java.util.List;
*/
public final class WorldMapRenderer {
private static final Logger log = LoggerFactory . getLogger ( WorldMapRenderer . class ) ;
public static final float WORLD_HALF = 1024f ;
public static final float WORLD_SIZE = 2048f ;
/** Auflösung des voxelSurface-Bake-Arrays (1 Zelle pro Welteinheit). */
@@ -102,146 +106,241 @@ public final class WorldMapRenderer {
private static final int [ ] DEF_SLOT_G = { 148 , 82 , 115 , 184 , 80 , 90 , 60 , 100 } ;
private static final int [ ] DEF_SLOT_B = { 46 , 64 , 77 , 128 , 40 , 60 , 50 , 70 } ;
// Karten-Kachel-Indizes
public static final int TILE_WATER = 0 , TILE_CLIFF = 1 , TILE_GRASS = 2 ,
TILE_SAND = 3 , TILE_DIRT = 4 , TILE_ROCK = 5 ,
TILE_PAVEMENT = 6 , TILE_FOREST = 7 ;
private static final String [ ] TILE_FILE_NAMES = {
" map_water " , " map_cliff " , " map_grass " , " map_sand " , " map_dirt " , " map_rock " , " map_pavement " , " map_forest " } ;
public static final int [ ] [ ] TILE_FALLBACK_RGB = {
{ 55 , 120 , 195 } , { 115 , 108 , 100 } , { 78 , 138 , 52 } ,
{ 198 , 182 , 110 } , { 118 , 82 , 46 } , { 122 , 116 , 108 } , { 148 , 135 , 112 } , { 34 , 85 , 34 } } ;
public static final float TILE_WU = 1f ; // Kachelgroesse in Welteinheiten (1m → 1:1 bei scale=16)
private WorldMapRenderer ( ) { }
/** Hauptmethode: rendert das Hintergrund-PNG aus einem vorberechneten Modell. */
public static BufferedImage render ( WorldMapRenderModel model , int targetSize , RenderOptions opts ) {
MapData m = model . mapData ( ) ;
int TV = MapData . TERRAIN_VERTS ;
int SS = MapData . SPLAT_SIZE ;
int [ ] slotR = slotChannel ( model . slotColorsRGB ( ) , 0 ) ;
int [ ] slotG = slotChannel ( model . slotColorsRGB ( ) , 1 ) ;
int [ ] slotB = slotChannel ( model . slotColorsRGB ( ) , 2 ) ;
// ── 1. Heightmap auf Zielauflösung samplen ────────────────────────────
int UV = MapData . UPPER_VERTS ;
float [ ] heights = new float [ targetSize * targetSize ] ;
// Meerserkennung basiert nur auf terrainHeight (ohne upperTop/voxelSurface):
// bakeVoxelHeights trägt Voxel-Höhen in upperTop ein, was sonst
// Voxel auf dem Meeresboden fälschlich als Landfläche klassifiziert.
// 1. Hoehenfeld
float [ ] heights = buildTerrainSamples ( m , targetSize , model . voxelSurface ( ) ) ;
boolean [ ] seaPixels = new boolean [ targetSize * targetSize ] ;
floa t minH = Float . MAX_VALUE , maxH = - Float . MAX_VALUE ;
for ( in t i = 0 ; i < heights . length ; i + + ) { seaPixels [ i ] = heights [ i ] < 0f ; }
int VS = VOXEL_SURFACE_RES ;
for ( int py = 0 ; py < targetSize ; py + + ) {
for ( int px = 0 ; px < targetSize ; px + + ) {
int hx = Math . min ( ( int ) ( ( float ) px / ( targetSize - 1 ) * ( TV - 1 ) ) , TV - 1 ) ;
int hz = Math . min ( ( int ) ( ( float ) py / ( targetSize - 1 ) * ( TV - 1 ) ) , TV - 1 ) ;
float ux_f = ( float ) px / ( targetSize - 1 ) * ( UV - 1 ) ;
float uz_f = ( float ) py / ( targetSize - 1 ) * ( UV - 1 ) ;
float baseH = m . terrainHeight [ hz * TV + hx ] ;
float h = baseH ;
floa t upper = bilerpPos ( m . upperTop , ux_f , uz_f , UV ) ;
if ( upper > 0f & & upper > h ) { h = upper ; }
// Voxel-Oberfläche: direkte 1:1-Abfrage des hochauflösenden Bake-Arrays
// seaH = max(terrain, voxelSurface): Voxel über Y=0 → Land, darunter → Meer
float seaH = baseH ;
if ( model . voxelSurface ( ) ! = null ) {
int vsPx = Math . min ( ( int ) ( ( float ) px / ( targetSize - 1 ) * ( VS - 1 ) ) , VS - 1 ) ;
int vsPz = Math . min ( ( int ) ( ( float ) py / ( targetSize - 1 ) * ( VS - 1 ) ) , VS - 1 ) ;
float vs = model . voxelSurface ( ) [ vsPz * VS + vsPx ] ;
if ( ! Float . isNaN ( vs ) ) {
h = vs ;
if ( vs > seaH ) { seaH = vs ; }
// 2. Wasser-Maske: Ozean + PlacedWater vorab berechnen
boolean [ ] isWater = java . util . Arrays . copyOf ( seaPixels , seaPixels . length ) ;
if ( opts . showWater ( ) ) {
for ( PlacedWater w : model . waters ( ) ) {
int [ ] wxPx = worldToPixels ( w . pointsX ( ) , targetSize ) ;
int [ ] wzPx = worldToPixels ( w . pointsZ ( ) , targetSize ) ;
Polygon waterPoly = new Polygon ( wxPx , wzPx , wxPx . length ) ;
Rectangle bb = waterPoly . getBounds ( ) ;
float w h = w . waterHeight ( ) ;
for ( in t py = Math . max ( 0 , bb . y ) ; py < = Math . min ( targetSize - 1 , bb . y + bb . height ) ; py + + ) {
for ( int px = Math . max ( 0 , bb . x ) ; px < = Math . min ( targetSize - 1 , bb . x + bb . width ) ; px + + ) {
if ( waterPoly . contains ( px , py ) & & heights [ py * targetSize + px ] < wh )
isWater [ py * targetSize + px ] = true ;
}
}
heights [ py * targetSize + px ] = h ;
seaPixels [ py * targetSize + px ] = ( seaH < 0f ) ;
if ( h < minH ) minH = h ;
if ( h > maxH ) maxH = h ;
}
}
float heightRange = Math . max ( 0 . 01f , maxH - minH ) ;
// ── 2. Pixel-Farben berechnen ─────────────────────────────────────────
BufferedImage img = new BufferedImage ( targetSize , targetSize , BufferedImage . TYPE_INT_RGB ) ;
// 3. Tile-Rendering: tileIdx + Rohfarbe pro Pixel speichern
float cellW = WORLD_SIZE / ( targetSize - 1 ) ;
int SS = MapData . SPLAT_SIZE ;
int [ ] slotCols = model . slotColorsRGB ( ) ;
int [ ] [ ] tiles = model . mapTilePixels ( ) ;
int [ ] rawRGB = new int [ targetSize * targetSize ] ;
int waterR = ( WATER_COLOR > > 16 ) & 0xFF ;
int waterG = ( WATER_COLOR > > 8 ) & 0xFF ;
int waterB = WATER_COLOR & 0xFF ;
for ( int py = 0 ; py < targetSize ; py + + ) {
float wz = - WORLD_HALF + py * cellW ;
for ( int px = 0 ; px < targetSize ; px + + ) {
float h = heights [ py * targetSize + px ] ;
floa t hn = ( h - minH ) / heightRange ;
float wx = - WORLD_HALF + px * cellW ;
in t idx = py * targetSize + px ;
int r , g , b ;
if ( opts . showSplatColors ( ) ) {
int sx = Math . min ( ( int ) ( ( float ) px / ( targetSize - 1 ) * ( SS - 1 ) ) , SS - 1 ) ;
int sz = M ath . min ( ( int ) ( ( float ) ( targetSize - 1 - py ) / ( targetSize - 1 ) * ( SS - 1 ) ) , SS - 1 ) ;
int si = sz * SS + sx ;
// Base layer (Slots 1-4)
float wg = ( m . splatG [ si ] & 0xFF ) / 255f ;
float wb = ( m . splatB [ si ] & 0xFF ) / 255f ;
float wa = ( m . splatA [ si ] & 0xFF ) / 255f ;
float fr = slotR [ 0 ] , fg = slotG [ 0 ] , fb = slotB [ 0 ] ;
fr = fr * ( 1 - wg ) + slotR [ 1 ] * wg ; fg = fg * ( 1 - wg ) + slotG [ 1 ] * wg ; fb = fb * ( 1 - wg ) + slotB [ 1 ] * wg ;
fr = fr * ( 1 - wb ) + slotR [ 2 ] * wb ; fg = fg * ( 1 - wb ) + slotG [ 2 ] * wb ; fb = fb * ( 1 - wb ) + slotB [ 2 ] * wb ;
fr = fr * ( 1 - wa ) + slotR [ 3 ] * wa ; fg = fg * ( 1 - wa ) + slotG [ 3 ] * wa ; fb = fb * ( 1 - wa ) + slotB [ 3 ] * wa ;
// Upper layer (Slots 5-8)
float u1 = ( m . upperSplatR [ si ] & 0xFF ) / 255f ;
float u2 = ( m . upperSplatG [ si ] & 0xFF ) / 255f ;
float u3 = ( m . upperSplatB [ si ] & 0xFF ) / 255f ;
float u4 = ( m . upperSplatA [ si ] & 0xFF ) / 255f ;
fr = fr * ( 1 - u1 ) + slotR [ 4 ] * u1 ; fg = fg * ( 1 - u1 ) + slotG [ 4 ] * u1 ; fb = fb * ( 1 - u1 ) + slotB [ 4 ] * u1 ;
fr = fr * ( 1 - u2 ) + slotR [ 5 ] * u2 ; fg = fg * ( 1 - u2 ) + slotG [ 5 ] * u2 ; fb = fb * ( 1 - u2 ) + slotB [ 5 ] * u2 ;
fr = fr * ( 1 - u3 ) + slotR [ 6 ] * u3 ; fg = fg * ( 1 - u3 ) + slotG [ 6 ] * u3 ; fb = fb * ( 1 - u3 ) + slotB [ 6 ] * u3 ;
fr = fr * ( 1 - u4 ) + slotR [ 7 ] * u4 ; fg = fg * ( 1 - u4 ) + slotG [ 7 ] * u4 ; fb = fb * ( 1 - u4 ) + slotB [ 7 ] * u4 ;
r = clamp ( ( int ) fr ) ; g = clamp ( ( int ) fg ) ; b = clamp ( ( int ) fb ) ;
int tileIdx ;
int tr , tg , tb ;
if ( isWater [ idx ] ) {
tileIdx = TILE_WATER ;
tr = w aterR ; tg = waterG ; tb = waterB ;
} else {
int lum = clamp ( 30 + ( int ) ( hn * 200 ) ) ;
r = g = b = lum ;
int dPx = Math . max ( 1 , ( int ) ( 0 . 25f / cellW ) ) ;
float hR = heights [ py * targetSize + Math . min ( targetSize - 1 , px + dPx ) ] ;
float hL = heights [ py * targetSize + Math . max ( 0 , px - dPx ) ] ;
float hD = heights [ Math . min ( targetSize - 1 , py + dPx ) * targetSize + px ] ;
float hU = heights [ Math . max ( 0 , py - dPx ) * targetSize + px ] ;
float dist = dPx * cellW ;
double slope = Math . toDegrees ( Math . atan ( Math . sqrt (
Math . pow ( ( hR - hL ) / ( 2 * dist ) , 2 ) + Math . pow ( ( hD - hU ) / ( 2 * dist ) , 2 ) ) ) ) ;
tileIdx = ( slope > 30 . 0 ) ? TILE_CLIFF : getTerrainTile ( m , SS , wx , wz , slotCols ) ;
if ( tiles ! = null & & tileIdx < tiles . length & & tiles [ tileIdx ] ! = null ) {
int tu = Math . floorMod ( ( int ) Math . floor ( ( wx + WORLD_HALF ) / TILE_WU ) , 16 ) ;
int tv = Math . floorMod ( ( int ) Math . floor ( ( wz + WORLD_HALF ) / TILE_WU ) , 16 ) ;
int argb = tiles [ tileIdx ] [ tv * 16 + tu ] ;
tr = ( argb > > 16 ) & 0xFF ; tg = ( argb > > 8 ) & 0xFF ; tb = argb & 0xFF ;
} else {
tr = TILE_FALLBACK_RGB [ tileIdx ] [ 0 ] ;
tg = TILE_FALLBACK_RGB [ tileIdx ] [ 1 ] ;
tb = TILE_FALLBACK_RGB [ tileIdx ] [ 2 ] ;
}
}
// Hillshading (NW-Licht)
if ( opts . showTerrain ( ) & & px > 0 & & px < targetSize - 1 & & py > 0 & & py < targetSize - 1 ) {
float dx = heights [ py * targetSize + ( px + 1 ) ] - heights [ py * targetSize + ( px - 1 ) ] ;
float dz = heights [ ( py + 1 ) * targetSize + px ] - heights [ ( py - 1 ) * targetSize + px ] ;
float nx = - dx * 0 . 25f , ny = 1f , nz = - dz * 0 . 25f ;
float len = ( float ) Math . sqrt ( nx * nx + ny * ny + nz * nz ) ;
nx / = len ; ny / = len ; nz / = len ;
float shade = nx * ( - 0 . 577f ) + ny * 0 . 577f + nz * ( - 0 . 577f ) ;
shade = 0 . 45f + shade * 0 . 85f ;
shade = Math . max ( 0 . 2f , Math . min ( 1 . 8f , shade ) ) ;
r = clamp ( ( int ) ( r * shade ) ) ;
g = clamp ( ( int ) ( g * shade ) ) ;
b = clamp ( ( int ) ( b * shade ) ) ;
}
img . setRGB ( px , py , ( r < < 16 ) | ( g < < 8 ) | b ) ;
rawRGB [ idx ] = ( tr < < 16 ) | ( tg < < 8 ) | tb ;
}
}
// Kuwahara-Filter: lässt das Terrain wie gemalt wirken
if ( opts . showSplatColors ( ) ) { applyKuwahara ( img , 3 ) ; }
// 4. Kuestenlinie: Sub-Pixel-Sampling fuer pixelgenaue Wasser-Land-Grenze.
// Greift nur auf Grenz-Pixel zur Ozean-Kuestenlinie. Interior-Pixel bleiben scharf.
int SUB = 8 ;
float subInv = 1f / ( SUB * SUB ) ;
for ( int py = 0 ; py < targetSize ; py + + ) {
float wz = - WORLD_HALF + py * cellW ;
for ( int px = 0 ; px < targetSize ; px + + ) {
float wx = - WORLD_HALF + px * cellW ;
int idx = py * targetSize + px ;
boolean isS = seaPixels [ idx ] ;
boolean boundary = false ;
for ( int dy = - 1 ; dy < = 1 & & ! boundary ; dy + + ) {
for ( int dx = - 1 ; dx < = 1 & & ! boundary ; dx + + ) {
if ( dx = = 0 & & dy = = 0 ) continue ;
int ny = py + dy , nx = px + dx ;
if ( ny < 0 | | ny > = targetSize | | nx < 0 | | nx > = targetSize ) continue ;
if ( seaPixels [ ny * targetSize + nx ] ! = isS ) boundary = true ;
}
}
if ( ! boundary ) continue ;
// 8x8 Sub-Samples: Wasser-Anteil dieses Pixels bestimmen
int wCount = 0 ;
for ( int sy = 0 ; sy < SUB ; sy + + ) {
for ( int sx = 0 ; sx < SUB ; sx + + ) {
float spx = px + ( sx + 0 . 5f ) / SUB - 0 . 5f ;
float spy = py + ( sy + 0 . 5f ) / SUB - 0 . 5f ;
if ( sampleTerrainH ( m , targetSize , spx , spy , model . voxelSurface ( ) ) < 0f )
wCount + + ;
}
}
float wFrac = wCount * subInv ;
// Land-Farbe: fuer Wasser-Pixel on-the-fly berechnen, sonst aus rawRGB
int landRGB ;
if ( isS ) {
int dPx = Math . max ( 1 , ( int ) ( 0 . 25f / cellW ) ) ;
float hR2 = heights [ py * targetSize + Math . min ( targetSize - 1 , px + dPx ) ] ;
float hL2 = heights [ py * targetSize + Math . max ( 0 , px - dPx ) ] ;
float hD2 = heights [ Math . min ( targetSize - 1 , py + dPx ) * targetSize + px ] ;
float hU2 = heights [ Math . max ( 0 , py - dPx ) * targetSize + px ] ;
float d2 = dPx * cellW ;
double sl2 = Math . toDegrees ( Math . atan ( Math . sqrt (
Math . pow ( ( hR2 - hL2 ) / ( 2 * d2 ) , 2 ) + Math . pow ( ( hD2 - hU2 ) / ( 2 * d2 ) , 2 ) ) ) ) ;
int ti2 = ( sl2 > 30 . 0 ) ? TILE_CLIFF : getTerrainTile ( m , SS , wx , wz , slotCols ) ;
int tr2 , tg2 , tb2 ;
if ( tiles ! = null & & ti2 < tiles . length & & tiles [ ti2 ] ! = null ) {
int tu = Math . floorMod ( ( int ) Math . floor ( ( wx + WORLD_HALF ) / TILE_WU ) , 16 ) ;
int tv = Math . floorMod ( ( int ) Math . floor ( ( wz + WORLD_HALF ) / TILE_WU ) , 16 ) ;
int argb = tiles [ ti2 ] [ tv * 16 + tu ] ;
tr2 = ( argb > > 16 ) & 0xFF ; tg2 = ( argb > > 8 ) & 0xFF ; tb2 = argb & 0xFF ;
} else {
tr2 = TILE_FALLBACK_RGB [ ti2 ] [ 0 ] ; tg2 = TILE_FALLBACK_RGB [ ti2 ] [ 1 ] ; tb2 = TILE_FALLBACK_RGB [ ti2 ] [ 2 ] ;
}
landRGB = ( tr2 < < 16 ) | ( tg2 < < 8 ) | tb2 ;
} else {
landRGB = rawRGB [ idx ] ;
}
int lr = ( landRGB > > 16 ) & 0xFF , lg = ( landRGB > > 8 ) & 0xFF , lb = landRGB & 0xFF ;
rawRGB [ idx ] = ( clamp ( ( int ) ( waterR * wFrac + lr * ( 1 - wFrac ) ) ) < < 16 )
| ( clamp ( ( int ) ( waterG * wFrac + lg * ( 1 - wFrac ) ) ) < < 8 )
| clamp ( ( int ) ( waterB * wFrac + lb * ( 1 - wFrac ) ) ) ;
}
}
// ── 3. Vektor-Ov erlays ────────────────────────────────────────────────
// 4b. PlacedWater-Kuestenlinie: Sub-Pixel-Sampling fuer definierte Wass erf laechen
if ( opts . showWater ( ) ) {
for ( PlacedWater w : model . waters ( ) ) {
int [ ] wxPx2 = worldToPixels ( w . pointsX ( ) , targetSize ) ;
int [ ] wzPx2 = worldToPixels ( w . pointsZ ( ) , targetSize ) ;
Polygon wp = new Polygon ( wxPx2 , wzPx2 , wxPx2 . length ) ;
Rectangle bb2 = wp . getBounds ( ) ;
float wh2 = w . waterHeight ( ) ;
int x0 = Math . max ( 0 , bb2 . x - 1 ) , y0 = Math . max ( 0 , bb2 . y - 1 ) ;
int x1 = Math . min ( targetSize - 1 , bb2 . x + bb2 . width + 1 ) ;
int y1 = Math . min ( targetSize - 1 , bb2 . y + bb2 . height + 1 ) ;
for ( int py = y0 ; py < = y1 ; py + + ) {
float wz = - WORLD_HALF + py * cellW ;
for ( int px = x0 ; px < = x1 ; px + + ) {
float wx = - WORLD_HALF + px * cellW ;
int idx = py * targetSize + px ;
boolean inPoly = wp . contains ( px , py ) & & heights [ idx ] < wh2 ;
boolean boundary2 = false ;
for ( int dy = - 1 ; dy < = 1 & & ! boundary2 ; dy + + ) {
for ( int dx = - 1 ; dx < = 1 & & ! boundary2 ; dx + + ) {
if ( dx = = 0 & & dy = = 0 ) continue ;
int ny = py + dy , nx = px + dx ;
if ( ny < 0 | | ny > = targetSize | | nx < 0 | | nx > = targetSize ) continue ;
boolean nIn = wp . contains ( nx , ny ) & & heights [ ny * targetSize + nx ] < wh2 ;
if ( nIn ! = inPoly ) boundary2 = true ;
}
}
if ( ! boundary2 ) continue ;
int wCount2 = 0 ;
for ( int sy = 0 ; sy < SUB ; sy + + ) {
for ( int sx = 0 ; sx < SUB ; sx + + ) {
float spx = px + ( sx + 0 . 5f ) / SUB - 0 . 5f ;
float spy = py + ( sy + 0 . 5f ) / SUB - 0 . 5f ;
if ( wp . contains ( spx , spy )
& & sampleTerrainH ( m , targetSize , spx , spy , model . voxelSurface ( ) ) < wh2 )
wCount2 + + ;
}
}
float wFrac2 = wCount2 * subInv ;
int landRGB2 ;
if ( inPoly ) {
// Wasser-Pixel: Land-Farbe on-the-fly
int dPx = Math . max ( 1 , ( int ) ( 0 . 25f / cellW ) ) ;
float hR2 = heights [ py * targetSize + Math . min ( targetSize - 1 , px + dPx ) ] ;
float hL2 = heights [ py * targetSize + Math . max ( 0 , px - dPx ) ] ;
float hD2 = heights [ Math . min ( targetSize - 1 , py + dPx ) * targetSize + px ] ;
float hU2 = heights [ Math . max ( 0 , py - dPx ) * targetSize + px ] ;
float d2 = dPx * cellW ;
double sl2 = Math . toDegrees ( Math . atan ( Math . sqrt (
Math . pow ( ( hR2 - hL2 ) / ( 2 * d2 ) , 2 ) + Math . pow ( ( hD2 - hU2 ) / ( 2 * d2 ) , 2 ) ) ) ) ;
int ti2 = ( sl2 > 30 . 0 ) ? TILE_CLIFF : getTerrainTile ( m , SS , wx , wz , slotCols ) ;
int tr2 , tg2 , tb2 ;
if ( tiles ! = null & & ti2 < tiles . length & & tiles [ ti2 ] ! = null ) {
int tu2 = Math . floorMod ( ( int ) Math . floor ( ( wx + WORLD_HALF ) / TILE_WU ) , 16 ) ;
int tv2 = Math . floorMod ( ( int ) Math . floor ( ( wz + WORLD_HALF ) / TILE_WU ) , 16 ) ;
int argb = tiles [ ti2 ] [ tv2 * 16 + tu2 ] ;
tr2 = ( argb > > 16 ) & 0xFF ; tg2 = ( argb > > 8 ) & 0xFF ; tb2 = argb & 0xFF ;
} else {
tr2 = TILE_FALLBACK_RGB [ ti2 ] [ 0 ] ; tg2 = TILE_FALLBACK_RGB [ ti2 ] [ 1 ] ; tb2 = TILE_FALLBACK_RGB [ ti2 ] [ 2 ] ;
}
landRGB2 = ( tr2 < < 16 ) | ( tg2 < < 8 ) | tb2 ;
} else {
landRGB2 = rawRGB [ idx ] ;
}
int lr2 = ( landRGB2 > > 16 ) & 0xFF , lg2 = ( landRGB2 > > 8 ) & 0xFF , lb2 = landRGB2 & 0xFF ;
rawRGB [ idx ] = ( clamp ( ( int ) ( waterR * wFrac2 + lr2 * ( 1 - wFrac2 ) ) ) < < 16 )
| ( clamp ( ( int ) ( waterG * wFrac2 + lg2 * ( 1 - wFrac2 ) ) ) < < 8 )
| clamp ( ( int ) ( waterB * wFrac2 + lb2 * ( 1 - wFrac2 ) ) ) ;
}
}
}
}
// 5. Ergebnis in Bild schreiben
BufferedImage img = new BufferedImage ( targetSize , targetSize , BufferedImage . TYPE_INT_RGB ) ;
for ( int i = 0 ; i < rawRGB . length ; i + + ) {
img . setRGB ( i % targetSize , i / targetSize , rawRGB [ i ] ) ;
}
// ── 3. Vektor-Overlays ────────────────────────────────────────────────
Graphics2D gfx = img . createGraphics ( ) ;
gfx . setRenderingHint ( RenderingHints . KEY_ANTIALIASING , RenderingHints . VALUE_ANTIALIAS_ON ) ;
float lineW = Math . max ( 1 . 5f , targetSize / 400f ) ;
// Wasser
if ( opts . showWater ( ) ) {
// ── Weißfüllung ───────────────────────────────────────────────────────
for ( int py = 0 ; py < targetSize ; py + + ) {
for ( int px = 0 ; px < targetSize ; px + + ) {
if ( seaPixels [ py * targetSize + px ] ) img . setRGB ( px , py , WATER_COLOR ) ;
}
}
for ( PlacedWater w : model . waters ( ) ) {
int [ ] xs = worldToPixels ( w . pointsX ( ) , targetSize ) ;
int [ ] ys = worldToPixels ( w . pointsZ ( ) , targetSize ) ;
Polygon poly = new Polygon ( xs , ys , xs . length ) ;
Rectangle bb = poly . getBounds ( ) ;
int x0 = Math . max ( 0 , bb . x ) , y0 = Math . max ( 0 , bb . y ) ;
int x1 = Math . min ( targetSize - 1 , bb . x + bb . width ) ;
int y1 = Math . min ( targetSize - 1 , bb . y + bb . height ) ;
float wh = w . waterHeight ( ) ;
for ( int py = y0 ; py < = y1 ; py + + ) {
for ( int px = x0 ; px < = x1 ; px + + ) {
if ( poly . contains ( px , py ) & & heights [ py * targetSize + px ] < wh )
img . setRGB ( px , py , WATER_COLOR ) ;
}
}
}
// ── Wellenmarken ──────────────────────────────────────────────────────
if ( opts . showWaterWaves ( ) ) {
@@ -297,45 +396,6 @@ public final class WorldMapRenderer {
}
}
// ── Uferlinie ─────────────────────────────────────────────────────────
if ( opts . showWaterWaves ( ) ) {
gfx . setColor ( Color . BLACK ) ;
gfx . setStroke ( new BasicStroke ( 3 . 0f , BasicStroke . CAP_ROUND , BasicStroke . JOIN_ROUND ) ) ;
// Wasserflächen: Polygon-Umriss
for ( PlacedWater w : model . waters ( ) ) {
int [ ] xs = worldToPixels ( w . pointsX ( ) , targetSize ) ;
int [ ] ys = worldToPixels ( w . pointsZ ( ) , targetSize ) ;
gfx . drawPolygon ( xs , ys , xs . length ) ;
}
// Meer: Pixel-Küstenlinie (morphologisch aufgeweitet)
int bw = 3 ;
boolean [ ] cur = new boolean [ targetSize * targetSize ] ;
boolean [ ] nxt = new boolean [ targetSize * targetSize ] ;
for ( int py = 1 ; py < targetSize - 1 ; py + + ) {
for ( int px = 1 ; px < targetSize - 1 ; px + + ) {
if ( ! seaPixels [ py * targetSize + px ] ) continue ;
if ( ! seaPixels [ py * targetSize + ( px - 1 ) ] | | ! seaPixels [ py * targetSize + ( px + 1 ) ] | |
! seaPixels [ ( py - 1 ) * targetSize + px ] | | ! seaPixels [ ( py + 1 ) * targetSize + px ] )
cur [ py * targetSize + px ] = true ;
}
}
for ( int pass = 1 ; pass < bw ; pass + + ) {
System . arraycopy ( cur , 0 , nxt , 0 , cur . length ) ;
for ( int py = 1 ; py < targetSize - 1 ; py + + ) {
for ( int px = 1 ; px < targetSize - 1 ; px + + ) {
if ( ! cur [ py * targetSize + px ] ) continue ;
if ( seaPixels [ py * targetSize + ( px - 1 ) ] ) nxt [ py * targetSize + ( px - 1 ) ] = true ;
if ( seaPixels [ py * targetSize + ( px + 1 ) ] ) nxt [ py * targetSize + ( px + 1 ) ] = true ;
if ( seaPixels [ ( py - 1 ) * targetSize + px ] ) nxt [ ( py - 1 ) * targetSize + px ] = true ;
if ( seaPixels [ ( py + 1 ) * targetSize + px ] ) nxt [ ( py + 1 ) * targetSize + px ] = true ;
}
}
boolean [ ] tmp = cur ; cur = nxt ; nxt = tmp ;
}
for ( int i = 0 , n = cur . length ; i < n ; i + + ) {
if ( cur [ i ] ) img . setRGB ( i % targetSize , i / targetSize , 0xFF000000 ) ;
}
}
}
// Wasserfälle – klassisches Kartensymbol: Kammlinie + Kaskaden-Linien
@@ -471,7 +531,7 @@ public final class WorldMapRenderer {
return render ( new WorldMapRenderModel (
input . mapData ( ) , input . areas ( ) , input . zones ( ) , input . locations ( ) ,
input . waters ( ) , input . models ( ) , null ,
input . slotColorsRGB ( ) , null , null , null , null , null
input . slotColorsRGB ( ) , null , null , null , null , null , null
) , targetSize , opts ) ;
}
@@ -681,45 +741,6 @@ public final class WorldMapRenderer {
}
}
// ── Uferlinie ─────────────────────────────────────────────────────────
if ( opts . showWaterWaves ( ) ) {
gfx . setColor ( Color . BLACK ) ;
gfx . setStroke ( new BasicStroke ( 3 . 0f , BasicStroke . CAP_ROUND , BasicStroke . JOIN_ROUND ) ) ;
// Wasserflächen: Polygon-Umriss
for ( PlacedWater w : input . waters ( ) ) {
int [ ] xs = wrp ( w . pointsX ( ) , wx0 , rSize , targetSize ) ;
int [ ] ys = wrp ( w . pointsZ ( ) , wz0 , rSize , targetSize ) ;
gfx . drawPolygon ( xs , ys , xs . length ) ;
}
// Meer: Pixel-Küstenlinie (morphologisch aufgeweitet)
int bw = 3 ;
boolean [ ] cur = new boolean [ targetSize * targetSize ] ;
boolean [ ] nxt = new boolean [ targetSize * targetSize ] ;
for ( int py = 1 ; py < targetSize - 1 ; py + + ) {
for ( int px = 1 ; px < targetSize - 1 ; px + + ) {
if ( seaPixels2 = = null | | ! seaPixels2 [ py * targetSize + px ] ) continue ;
if ( ! seaPixels2 [ py * targetSize + ( px - 1 ) ] | | ! seaPixels2 [ py * targetSize + ( px + 1 ) ] | |
! seaPixels2 [ ( py - 1 ) * targetSize + px ] | | ! seaPixels2 [ ( py + 1 ) * targetSize + px ] )
cur [ py * targetSize + px ] = true ;
}
}
for ( int pass = 1 ; pass < bw ; pass + + ) {
System . arraycopy ( cur , 0 , nxt , 0 , cur . length ) ;
for ( int py = 1 ; py < targetSize - 1 ; py + + ) {
for ( int px = 1 ; px < targetSize - 1 ; px + + ) {
if ( ! cur [ py * targetSize + px ] ) continue ;
if ( seaPixels2 [ py * targetSize + ( px - 1 ) ] ) nxt [ py * targetSize + ( px - 1 ) ] = true ;
if ( seaPixels2 [ py * targetSize + ( px + 1 ) ] ) nxt [ py * targetSize + ( px + 1 ) ] = true ;
if ( seaPixels2 [ ( py - 1 ) * targetSize + px ] ) nxt [ ( py - 1 ) * targetSize + px ] = true ;
if ( seaPixels2 [ ( py + 1 ) * targetSize + px ] ) nxt [ ( py + 1 ) * targetSize + px ] = true ;
}
}
boolean [ ] tmp = cur ; cur = nxt ; nxt = tmp ;
}
for ( int i = 0 , n = cur . length ; i < n ; i + + ) {
if ( cur [ i ] ) img . setRGB ( i % targetSize , i / targetSize , 0xFF000000 ) ;
}
}
}
if ( opts . showAreas ( ) ) {
@@ -885,13 +906,264 @@ public final class WorldMapRenderer {
* @param baseFallbacks Fallback-Pfade für leere Base-Slots (kann null sein)
* @return int[24] mit [R0,G0,B0, R1,G1,B1, ..., R7,G7,B7] für Slots 1-8
*/
public static int [ ] computeSlotColors ( MapData mapData , Path assetRoot , String [ ] baseFallbacks ) {
/**
* Bilineare Höhen-Samples aus terrainHeight (8193× 8193, 0.25m/Vertex) + max(voxelSurface).
* Zentrale Methode für alle Karten-Berechnungen die Geländehöhen benötigen.
*/
public static float [ ] buildTerrainSamples ( MapData m , int size , float [ ] voxelSurface ) {
int TV = MapData . TERRAIN_VERTS ;
int VS = VOXEL_SURFACE_RES ;
float [ ] s = new float [ size * size ] ;
for ( int mz = 0 ; mz < size ; mz + + ) {
for ( int mx = 0 ; mx < size ; mx + + ) {
float fx = ( float ) mx / ( size - 1 ) * ( TV - 1 ) ;
float fz = ( float ) mz / ( size - 1 ) * ( TV - 1 ) ;
int x0 = Math . max ( 0 , Math . min ( TV - 2 , ( int ) fx ) ) ;
int z0 = Math . max ( 0 , Math . min ( TV - 2 , ( int ) fz ) ) ;
float tx = fx - x0 , tz = fz - z0 ;
float h = m . terrainHeight [ z0 * TV + x0 ] * ( 1 - tx ) * ( 1 - tz )
+ m . terrainHeight [ z0 * TV + x0 + 1 ] * tx * ( 1 - tz )
+ m . terrainHeight [ ( z0 + 1 ) * TV + x0 ] * ( 1 - tx ) * tz
+ m . terrainHeight [ ( z0 + 1 ) * TV + x0 + 1 ] * tx * tz ;
if ( voxelSurface ! = null ) {
int vx = Math . min ( ( int ) ( ( float ) mx / ( size - 1 ) * ( VS - 1 ) ) , VS - 1 ) ;
int vz = Math . min ( ( int ) ( ( float ) mz / ( size - 1 ) * ( VS - 1 ) ) , VS - 1 ) ;
for ( int dz = - 1 ; dz < = 1 ; dz + + ) {
for ( int dx = - 1 ; dx < = 1 ; dx + + ) {
float vs = voxelSurface [ Math . max ( 0 , Math . min ( VS - 1 , vz + dz ) ) * VS
+ Math . max ( 0 , Math . min ( VS - 1 , vx + dx ) ) ] ;
if ( ! Float . isNaN ( vs ) & & vs > h ) { h = vs ; }
}
}
}
s [ mz * size + mx ] = h ;
}
}
return s ;
}
/**
* Berechnet eine Hillshading-Karte (float pro Pixel, 0.55– 1.35) aus reinen Terrain-Höhen.
* Voxels werden NICHT eingerechnet, um Artefakte an Voxel-Kanten zu vermeiden.
*/
public static float [ ] buildShadeMap ( MapData m , int size ) {
float [ ] th = buildTerrainSamples ( m , size , null ) ;
float [ ] shd = new float [ size * size ] ;
for ( int mz = 0 ; mz < size ; mz + + ) {
for ( int mx = 0 ; mx < size ; mx + + ) {
int px1 = Math . min ( size - 1 , mx + 1 ) , px0 = Math . max ( 0 , mx - 1 ) ;
int pz1 = Math . min ( size - 1 , mz + 1 ) , pz0 = Math . max ( 0 , mz - 1 ) ;
float dx = th [ mz * size + px1 ] - th [ mz * size + px0 ] ;
float dz = th [ pz1 * size + mx ] - th [ pz0 * size + mx ] ;
shd [ mz * size + mx ] = Math . max ( 0 . 55f , Math . min ( 1 . 35f , 1f - ( dx * 0 . 3f + dz * 0 . 15f ) ) ) ;
}
}
return shd ;
}
/**
* Berechnet eine Textur-Kategorie-Karte (byte pro Pixel) in der angegebenen Auflösung.
* Werte: TILE_GRASS=2, TILE_SAND=3, TILE_DIRT=4, TILE_ROCK=5.
* TILE_WATER kommt hier nicht vor — Wasser-Erkennung erfolgt über buildWaterHeightMap.
* Wird vom Canvas-Overlay genutzt, um bei jedem Zoom-Level die richtige Tile-Textur zu samplen.
*/
public static byte [ ] buildCategoryMap ( MapData m , int size , float [ ] voxelSurface ,
List < PlacedWater > waters , int [ ] slotColors ) {
float cellW = WORLD_SIZE / ( size - 1 ) ;
int SS = MapData . SPLAT_SIZE ;
byte [ ] cats = new byte [ size * size ] ;
for ( int mz = 0 ; mz < size ; mz + + ) {
float wz = - WORLD_HALF + mz * cellW ;
for ( int mx = 0 ; mx < size ; mx + + ) {
float wx = - WORLD_HALF + mx * cellW ;
cats [ mz * size + mx ] = ( byte ) getTerrainTile ( m , SS , wx , wz , slotColors ) ;
}
}
return cats ;
}
/**
* Baut eine Wasserhoehen-Karte: pro Zelle die maximale PlacedWater-Wasserhoehe,
* 0 fuer Zellen ausserhalb jeder Wasserflaeche.
* In drawWaterOverlay: Pixel ist Wasser wenn heightMap[i] < waterHeightMap[i].
*/
public static float [ ] buildWaterHeightMap ( List < PlacedWater > waters , int size ) {
float [ ] map = new float [ size * size ] ;
if ( waters = = null ) return map ;
for ( PlacedWater w : waters ) {
int [ ] wxPx = worldToPixels ( w . pointsX ( ) , size ) ;
int [ ] wzPx = worldToPixels ( w . pointsZ ( ) , size ) ;
Polygon poly = new Polygon ( wxPx , wzPx , wxPx . length ) ;
Rectangle bb = poly . getBounds ( ) ;
float wh = w . waterHeight ( ) ;
for ( int py = Math . max ( 0 , bb . y ) ; py < = Math . min ( size - 1 , bb . y + bb . height ) ; py + + ) {
for ( int px = Math . max ( 0 , bb . x ) ; px < = Math . min ( size - 1 , bb . x + bb . width ) ; px + + ) {
if ( poly . contains ( px , py ) ) {
map [ py * size + px ] = Math . max ( map [ py * size + px ] , wh ) ;
}
}
}
}
return map ;
}
/** Lädt die 6 Karten-Kacheln aus Textures/map/ im Asset-Verzeichnis. */
public static int [ ] [ ] loadMapTiles ( Path assetRoot ) {
int [ ] [ ] result = new int [ TILE_FILE_NAMES . length ] [ ] ;
for ( int i = 0 ; i < TILE_FILE_NAMES . length ; i + + ) {
Path p = assetRoot . resolve ( " Textures/map/ " + TILE_FILE_NAMES [ i ] + " .png " ) ;
int [ ] pixels = null ;
if ( Files . exists ( p ) ) {
try {
BufferedImage img = ImageIO . read ( p . toFile ( ) ) ;
if ( img ! = null ) {
pixels = new int [ 256 ] ;
for ( int py = 0 ; py < 16 ; py + + ) {
for ( int px = 0 ; px < 16 ; px + + ) {
pixels [ py * 16 + px ] = img . getRGB ( px % img . getWidth ( ) , py % img . getHeight ( ) ) ;
}
}
}
} catch ( Exception ignored ) { }
}
if ( pixels = = null ) {
int [ ] fb = TILE_FALLBACK_RGB [ i ] ;
pixels = new int [ 256 ] ;
int packed = 0xFF000000 | ( fb [ 0 ] < < 16 ) | ( fb [ 1 ] < < 8 ) | fb [ 2 ] ;
java . util . Arrays . fill ( pixels , packed ) ;
}
result [ i ] = pixels ;
}
return result ;
}
/** Ermittelt den Tile-Index via sequentieller Splatmap-Blendung (identisch zu showSplatColors).
* slotColors: 24-Werte-Array (8 Slots × RGB) aus computeSlotColors(); null → Defaults. */
private static int getTerrainTile ( MapData m , int SS , float wx , float wz , int [ ] slotColors ) {
int sx = Math . max ( 0 , Math . min ( SS - 1 , ( int ) ( ( wx + WORLD_HALF ) / WORLD_SIZE * ( SS - 1 ) ) ) ) ;
int sz = Math . max ( 0 , Math . min ( SS - 1 , SS - 1 - ( int ) ( ( wz + WORLD_HALF ) / WORLD_SIZE * ( SS - 1 ) ) ) ) ;
int si = sz * SS + sx ;
// Base-Layer sequentiell blenden (Slot 0 → 1 → 2 → 3)
float wg = ( m . splatG [ si ] & 0xFF ) / 255f ;
float wb = ( m . splatB [ si ] & 0xFF ) / 255f ;
float wa = ( m . splatA [ si ] & 0xFF ) / 255f ;
float fr = sc ( slotColors , 0 ) , fg = sc ( slotColors , 1 ) , fb = sc ( slotColors , 2 ) ;
fr = fr * ( 1 - wg ) + sc ( slotColors , 3 ) * wg ; fg = fg * ( 1 - wg ) + sc ( slotColors , 4 ) * wg ; fb = fb * ( 1 - wg ) + sc ( slotColors , 5 ) * wg ;
fr = fr * ( 1 - wb ) + sc ( slotColors , 6 ) * wb ; fg = fg * ( 1 - wb ) + sc ( slotColors , 7 ) * wb ; fb = fb * ( 1 - wb ) + sc ( slotColors , 8 ) * wb ;
fr = fr * ( 1 - wa ) + sc ( slotColors , 9 ) * wa ; fg = fg * ( 1 - wa ) + sc ( slotColors , 10 ) * wa ; fb = fb * ( 1 - wa ) + sc ( slotColors , 11 ) * wa ;
// Upper-Layer
float u1 = ( m . upperSplatR [ si ] & 0xFF ) / 255f , u2 = ( m . upperSplatG [ si ] & 0xFF ) / 255f ;
float u3 = ( m . upperSplatB [ si ] & 0xFF ) / 255f , u4 = ( m . upperSplatA [ si ] & 0xFF ) / 255f ;
fr = fr * ( 1 - u1 ) + sc ( slotColors , 12 ) * u1 ; fg = fg * ( 1 - u1 ) + sc ( slotColors , 13 ) * u1 ; fb = fb * ( 1 - u1 ) + sc ( slotColors , 14 ) * u1 ;
fr = fr * ( 1 - u2 ) + sc ( slotColors , 15 ) * u2 ; fg = fg * ( 1 - u2 ) + sc ( slotColors , 16 ) * u2 ; fb = fb * ( 1 - u2 ) + sc ( slotColors , 17 ) * u2 ;
fr = fr * ( 1 - u3 ) + sc ( slotColors , 18 ) * u3 ; fg = fg * ( 1 - u3 ) + sc ( slotColors , 19 ) * u3 ; fb = fb * ( 1 - u3 ) + sc ( slotColors , 20 ) * u3 ;
fr = fr * ( 1 - u4 ) + sc ( slotColors , 21 ) * u4 ; fg = fg * ( 1 - u4 ) + sc ( slotColors , 22 ) * u4 ; fb = fb * ( 1 - u4 ) + sc ( slotColors , 23 ) * u4 ;
// Third-Layer
float t1 = ( m . thirdSplatR [ si ] & 0xFF ) / 255f , t2 = ( m . thirdSplatG [ si ] & 0xFF ) / 255f ;
float t3 = ( m . thirdSplatB [ si ] & 0xFF ) / 255f , t4 = ( m . thirdSplatA [ si ] & 0xFF ) / 255f ;
fr = fr * ( 1 - t1 ) + sc ( slotColors , 24 ) * t1 ; fg = fg * ( 1 - t1 ) + sc ( slotColors , 25 ) * t1 ; fb = fb * ( 1 - t1 ) + sc ( slotColors , 26 ) * t1 ;
fr = fr * ( 1 - t2 ) + sc ( slotColors , 27 ) * t2 ; fg = fg * ( 1 - t2 ) + sc ( slotColors , 28 ) * t2 ; fb = fb * ( 1 - t2 ) + sc ( slotColors , 29 ) * t2 ;
fr = fr * ( 1 - t3 ) + sc ( slotColors , 30 ) * t3 ; fg = fg * ( 1 - t3 ) + sc ( slotColors , 31 ) * t3 ; fb = fb * ( 1 - t3 ) + sc ( slotColors , 32 ) * t3 ;
fr = fr * ( 1 - t4 ) + sc ( slotColors , 33 ) * t4 ; fg = fg * ( 1 - t4 ) + sc ( slotColors , 34 ) * t4 ; fb = fb * ( 1 - t4 ) + sc ( slotColors , 35 ) * t4 ;
return slotToTile ( Math . round ( fr ) , Math . round ( fg ) , Math . round ( fb ) ) ;
}
private static float sc ( int [ ] cols , int i ) {
if ( cols ! = null & & i < cols . length ) return cols [ i ] ;
int slot = i / 3 , ch = i % 3 , si = Math . min ( slot , DEF_SLOT_R . length - 1 ) ;
return ch = = 0 ? DEF_SLOT_R [ si ] : ch = = 1 ? DEF_SLOT_G [ si ] : DEF_SLOT_B [ si ] ;
}
/**
* Tile-Typen für alle 12 Slots (base 0-3, upper 4-7, third 8-11) aus Textur-Pfaden (primär) und Farb-Fallback.
*/
public static int [ ] computeSlotTileTypes ( MapData mapData , int [ ] slotColors ) {
int [ ] types = new int [ 12 ] ;
for ( int s = 0 ; s < 12 ; s + + ) {
String path = null ;
if ( s < 4 & & mapData . terrainTextures ! = null & & s < mapData . terrainTextures . length )
path = mapData . terrainTextures [ s ] ;
else if ( s < 8 & & mapData . upperTextures ! = null & & ( s - 4 ) < mapData . upperTextures . length )
path = mapData . upperTextures [ s - 4 ] ;
else if ( s < 12 & & mapData . thirdTextures ! = null & & ( s - 8 ) < mapData . thirdTextures . length )
path = mapData . thirdTextures [ s - 8 ] ;
int byPath = path ! = null & & ! path . isEmpty ( ) ? tileTypeByPath ( path ) : - 1 ;
types [ s ] = byPath > = 0 ? byPath : classifySlot ( slotColors , s ) ;
}
if ( log . isDebugEnabled ( ) ) {
StringBuilder dbg = new StringBuilder ( " SlotTileTypes: " ) ;
for ( int s = 0 ; s < 12 ; s + + ) {
String path = s < 4 & & mapData . terrainTextures ! = null ? mapData . terrainTextures [ s ]
: s < 8 & & mapData . upperTextures ! = null ? mapData . upperTextures [ s - 4 ]
: s < 12 & & mapData . thirdTextures ! = null ? mapData . thirdTextures [ s - 8 ] : null ;
dbg . append ( " s " ) . append ( s ) . append ( " =' " ) . append ( path ! = null ? path : " " ) . append ( " '→ " ) . append ( types [ s ] ) . append ( " " ) ;
}
log . debug ( " {} " , dbg ) ;
}
return types ;
}
/** Klassifiziert Textur-Pfad über Schlüsselwörter im Pfad. Gibt -1 zurück wenn unbekannt. */
private static int tileTypeByPath ( String path ) {
String p = path . toLowerCase ( ) . replace ( '\\' , '/' ) ;
if ( p . contains ( " dirt " ) ) return TILE_DIRT ;
if ( p . contains ( " leaves " ) ) return TILE_DIRT ;
if ( p . contains ( " gras " ) ) return TILE_GRASS ;
if ( p . contains ( " vegetation " ) ) return TILE_GRASS ;
if ( p . contains ( " foliage " ) ) return TILE_GRASS ;
if ( p . contains ( " gravel " ) ) return TILE_ROCK ;
if ( p . contains ( " rock " ) ) return TILE_ROCK ;
if ( p . contains ( " stone " ) ) return TILE_ROCK ;
if ( p . contains ( " pavement " ) ) return TILE_PAVEMENT ;
if ( p . contains ( " sand " ) ) return TILE_SAND ;
return - 1 ;
}
/** Gibt den Tile-Typ für Slot s per Farb-Schwellwert zurück (Fallback). */
public static int classifySlot ( int [ ] slotColors , int s ) {
return slotToTile ( Math . round ( sc ( slotColors , s * 3 ) ) ,
Math . round ( sc ( slotColors , s * 3 + 1 ) ) ,
Math . round ( sc ( slotColors , s * 3 + 2 ) ) ) ;
}
private static int slotToTile ( int r , int g , int b ) {
int max = Math . max ( Math . max ( r , g ) , b ) ;
int rng = max - Math . min ( Math . min ( r , g ) , b ) ;
if ( rng < 15 ) return TILE_ROCK ;
if ( r > g + 20 & & b < 115 ) return TILE_DIRT ;
if ( b > 90 & & rng < 80 ) return TILE_SAND ;
return TILE_GRASS ;
}
private static float sampleTerrainH ( MapData m , int targetSize , float fpx , float fpy , float [ ] voxelSurface ) {
int TV = MapData . TERRAIN_VERTS , VS = VOXEL_SURFACE_RES ;
float fx = Math . max ( 0 , Math . min ( TV - 1 . 001f , fpx / ( targetSize - 1 ) * ( TV - 1 ) ) ) ;
float fz = Math . max ( 0 , Math . min ( TV - 1 . 001f , fpy / ( targetSize - 1 ) * ( TV - 1 ) ) ) ;
int x0 = ( int ) fx , z0 = ( int ) fz ;
int x1 = Math . min ( x0 + 1 , TV - 1 ) , z1 = Math . min ( z0 + 1 , TV - 1 ) ;
float tx = fx - x0 , tz = fz - z0 ;
float h = m . terrainHeight [ z0 * TV + x0 ] * ( 1 - tx ) * ( 1 - tz )
+ m . terrainHeight [ z0 * TV + x1 ] * tx * ( 1 - tz )
+ m . terrainHeight [ z1 * TV + x0 ] * ( 1 - tx ) * tz
+ m . terrainHeight [ z1 * TV + x1 ] * tx * tz ;
if ( voxelSurface ! = null ) {
int vx = Math . min ( Math . max ( 0 , ( int ) ( fpx / ( targetSize - 1 ) * ( VS - 1 ) ) ) , VS - 1 ) ;
int vz = Math . min ( Math . max ( 0 , ( int ) ( fpy / ( targetSize - 1 ) * ( VS - 1 ) ) ) , VS - 1 ) ;
float vs = voxelSurface [ vz * VS + vx ] ;
if ( ! Float . isNaN ( vs ) & & vs > h ) h = vs ;
}
return h ;
}
public static int [ ] computeSlotColors ( MapData mapData , Path assetRoot , String [ ] baseFallbacks ) {
int [ ] result = {
71 , 148 , 46 , 115 , 82 , 64 , 140 , 115 , 77 , 204 , 184 , 128 ,
90 , 80 , 40 , 130 , 90 , 60 , 110 , 60 , 50 , 180 , 100 , 70
90 , 80 , 40 , 130 , 90 , 60 , 110 , 60 , 50 , 180 , 100 , 70 ,
118 , 82 , 46 , 118 , 82 , 46 , 118 , 82 , 46 , 118 , 82 , 46
} ;
sampleTexColors ( mapData . terrainTextures , baseFallbacks , assetRoot , result , 0 ) ;
sampleTexColors ( mapData . upperTextures , null , assetRoot , result , 4 ) ;
sampleTexColors ( mapData . thirdTextures , null , assetRoot , result , 8 ) ;
return result ;
}
@@ -963,6 +1235,11 @@ public final class WorldMapRenderer {
}
public static boolean isTree ( PlacedModel m ) {
de . blight . common . ModelCategory cat = m . modelCategory ( ) ;
if ( cat = = de . blight . common . ModelCategory . TREE | | cat = = de . blight . common . ModelCategory . PALM )
return true ;
if ( cat ! = null & & cat ! = de . blight . common . ModelCategory . NONE )
return false ;
return m . modelPath ( ) . replace ( '\\' , '/' ) . toLowerCase ( ) . contains ( " /trees/ " ) ;
}