Arbeiten an der Karte - neuer Render Modus mit Zwischenschicht, Küstenlinien, Wasserfälle
This commit is contained in:
@@ -139,12 +139,17 @@ public final class VoxelChunkIO {
|
||||
|
||||
/**
|
||||
* Liest alle vorhandenen VoxelChunks aus dem Chunks-Verzeichnis.
|
||||
* Für Koordinaten, bei denen die .blvc nach dem Backen gelöscht wurde,
|
||||
* wird automatisch die .blvc.prebake als Fallback herangezogen.
|
||||
* Gibt leere Liste zurück wenn kein Chunks-Verzeichnis existiert.
|
||||
*/
|
||||
public static List<VoxelChunk> loadAll() {
|
||||
List<VoxelChunk> result = new ArrayList<>();
|
||||
Path dir = ChunkTerrainIO.chunksDir();
|
||||
if (!Files.isDirectory(dir)) return result;
|
||||
|
||||
// Aktive .blvc-Dateien einlesen
|
||||
Set<String> loaded = new java.util.HashSet<>();
|
||||
try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir, "voxel_*.blvc")) {
|
||||
for (Path p : ds) {
|
||||
String name = p.getFileName().toString()
|
||||
@@ -158,9 +163,30 @@ public final class VoxelChunkIO {
|
||||
: Integer.parseInt(parts[1]);
|
||||
int cz = Integer.parseInt(parts[2]);
|
||||
result.add(VoxelChunk.deserialize(Files.readAllBytes(p), cx, cy, cz));
|
||||
loaded.add(name);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
} catch (IOException ignored) {}
|
||||
|
||||
// Prebake-Fallback: Koordinaten die gebacken wurden haben keine .blvc mehr
|
||||
try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir, "voxel_*.blvc.prebake")) {
|
||||
for (Path p : ds) {
|
||||
String name = p.getFileName().toString()
|
||||
.replace("voxel_", "").replace(".blvc.prebake", "");
|
||||
if (loaded.contains(name)) continue; // aktive .blvc hat Vorrang
|
||||
String[] parts = name.split("_");
|
||||
if (parts.length != 3) continue;
|
||||
try {
|
||||
int cx = Integer.parseInt(parts[0]);
|
||||
int cy = parts[1].startsWith("m")
|
||||
? -Integer.parseInt(parts[1].substring(1))
|
||||
: Integer.parseInt(parts[1]);
|
||||
int cz = Integer.parseInt(parts[2]);
|
||||
result.add(VoxelChunk.deserialize(Files.readAllBytes(p), cx, cy, cz));
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
} catch (IOException ignored) {}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,11 @@ public record WorldMapRenderModel(
|
||||
List<Location> locations,
|
||||
List<PlacedWater> waters,
|
||||
List<PlacedModel> models,
|
||||
List<PlacedWaterfall> waterfalls,
|
||||
int[] slotColorsRGB,
|
||||
// Vorberechnete Overlay-Daten (werden vom Canvas genutzt, nicht vom PNG-Renderer)
|
||||
boolean[] seaMask, // Wasser-Pixel-Maske bei SEA_MASK_SIZE-Auflösung
|
||||
float[] voxelSurface, // oberster solider Voxel-Y pro UPPER_VERTS-Zelle; NaN = keine Daten
|
||||
float[] terrainSamples, // Höhenwerte bei SEA_MASK_SIZE (für Wellen-Prüfung)
|
||||
List<float[][]> coastPaths, // geglättete Marching-Squares Küstenpfade (Weltkoord.)
|
||||
List<List<PlacedModel>> treeClusters
|
||||
|
||||
@@ -23,6 +23,8 @@ public final class WorldMapRenderer {
|
||||
|
||||
public static final float WORLD_HALF = 1024f;
|
||||
public static final float WORLD_SIZE = 2048f;
|
||||
/** Auflösung des voxelSurface-Bake-Arrays (1 Zelle pro Welteinheit). */
|
||||
public static final int VOXEL_SURFACE_RES = 2048;
|
||||
|
||||
public record RenderInput(
|
||||
MapData mapData,
|
||||
@@ -47,32 +49,46 @@ public final class WorldMapRenderer {
|
||||
boolean showAreas,
|
||||
boolean showZones,
|
||||
boolean showLocations,
|
||||
boolean showModels
|
||||
boolean showModels,
|
||||
boolean showWaterfalls
|
||||
) {
|
||||
// Compat: alte 7-Parameter-Form → showWaterWaves = showWater
|
||||
// Compat: 8-Parameter-Form (ohne showWaterfalls) → showWaterfalls = true
|
||||
public RenderOptions(boolean showTerrain, boolean showSplatColors, boolean showWater,
|
||||
boolean showWaterWaves, boolean showAreas, boolean showZones,
|
||||
boolean showLocations, boolean showModels) {
|
||||
this(showTerrain, showSplatColors, showWater, showWaterWaves,
|
||||
showAreas, showZones, showLocations, showModels, true);
|
||||
}
|
||||
// Compat: alte 7-Parameter-Form → showWaterWaves = showWater, showWaterfalls = true
|
||||
public RenderOptions(boolean showTerrain, boolean showSplatColors, boolean showWater,
|
||||
boolean showAreas, boolean showZones, boolean showLocations, boolean showModels) {
|
||||
this(showTerrain, showSplatColors, showWater, showWater,
|
||||
showAreas, showZones, showLocations, showModels);
|
||||
showAreas, showZones, showLocations, showModels, true);
|
||||
}
|
||||
public static RenderOptions all() {
|
||||
return new RenderOptions(true, true, true, true, true, true, true, true);
|
||||
return new RenderOptions(true, true, true, true, true, true, true, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean[] buildSeaMask(MapData m, int size) {
|
||||
return buildSeaMask(m, size, null);
|
||||
}
|
||||
|
||||
public static boolean[] buildSeaMask(MapData m, int size, float[] voxelSurface) {
|
||||
int TV = MapData.TERRAIN_VERTS;
|
||||
int UV = MapData.UPPER_VERTS;
|
||||
int VS = VOXEL_SURFACE_RES;
|
||||
boolean[] mask = new boolean[size * size];
|
||||
for (int py = 0; py < size; py++) {
|
||||
for (int px = 0; px < size; px++) {
|
||||
int hx = Math.min((int)((float) px / (size - 1) * (TV - 1)), TV - 1);
|
||||
int hz = Math.min((int)((float) py / (size - 1) * (TV - 1)), TV - 1);
|
||||
int ux = Math.min((int)((float) px / (size - 1) * (UV - 1)), UV - 1);
|
||||
int uz = Math.min((int)((float) py / (size - 1) * (UV - 1)), UV - 1);
|
||||
float h = m.terrainHeight[hz * TV + hx];
|
||||
float upper = m.upperTop[uz * UV + ux];
|
||||
if (upper > 0f && upper > h) { h = upper; }
|
||||
if (voxelSurface != null) {
|
||||
int vsPx = Math.min((int)((float) px / (size - 1) * (VS - 1)), VS - 1);
|
||||
int vsPz = Math.min((int)((float) py / (size - 1) * (VS - 1)), VS - 1);
|
||||
float vs = voxelSurface[vsPz * VS + vsPx];
|
||||
if (!Float.isNaN(vs) && vs > h) { h = vs; }
|
||||
}
|
||||
mask[py * size + px] = h < 0f;
|
||||
}
|
||||
}
|
||||
@@ -100,18 +116,37 @@ public final class WorldMapRenderer {
|
||||
// ── 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.
|
||||
boolean[] seaPixels = new boolean[targetSize * targetSize];
|
||||
float minH = Float.MAX_VALUE, maxH = -Float.MAX_VALUE;
|
||||
|
||||
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);
|
||||
int ux = Math.min((int)((float) px / (targetSize - 1) * (UV - 1)), UV - 1);
|
||||
int uz = Math.min((int)((float) py / (targetSize - 1) * (UV - 1)), UV - 1);
|
||||
float h = m.terrainHeight[hz * TV + hx];
|
||||
float upper = m.upperTop[uz * UV + ux];
|
||||
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;
|
||||
float 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; }
|
||||
}
|
||||
}
|
||||
heights[py * targetSize + px] = h;
|
||||
seaPixels[py * targetSize + px] = (seaH < 0f);
|
||||
if (h < minH) minH = h;
|
||||
if (h > maxH) maxH = h;
|
||||
}
|
||||
@@ -188,7 +223,7 @@ public final class WorldMapRenderer {
|
||||
// ── Weißfüllung ───────────────────────────────────────────────────────
|
||||
for (int py = 0; py < targetSize; py++) {
|
||||
for (int px = 0; px < targetSize; px++) {
|
||||
if (heights[py * targetSize + px] < 0f) img.setRGB(px, py, WATER_COLOR);
|
||||
if (seaPixels[py * targetSize + px]) img.setRGB(px, py, WATER_COLOR);
|
||||
}
|
||||
}
|
||||
for (PlacedWater w : model.waters()) {
|
||||
@@ -227,7 +262,7 @@ public final class WorldMapRenderer {
|
||||
int cx = (int)(ox + wmW * 0.5f), cy = (int)oy;
|
||||
if (cx >= 0 && cx < targetSize && cy >= 0 && cy < targetSize
|
||||
&& ox >= 0 && ox + wmW < targetSize
|
||||
&& heights[cy * targetSize + cx] < 0f) {
|
||||
&& seaPixels[cy * targetSize + cx]) {
|
||||
Path2D mark = new Path2D.Float();
|
||||
mark.moveTo(ox, oy);
|
||||
mark.curveTo(ox + wmW*0.3f, oy - wmA, ox + wmW*0.7f, oy + wmA, ox + wmW, oy);
|
||||
@@ -278,9 +313,9 @@ public final class WorldMapRenderer {
|
||||
boolean[] nxt = new boolean[targetSize * targetSize];
|
||||
for (int py = 1; py < targetSize - 1; py++) {
|
||||
for (int px = 1; px < targetSize - 1; px++) {
|
||||
if (heights[py * targetSize + px] >= 0f) continue;
|
||||
if (heights[py * targetSize + (px-1)] >= 0f || heights[py * targetSize + (px+1)] >= 0f ||
|
||||
heights[(py-1) * targetSize + px] >= 0f || heights[(py+1) * targetSize + px] >= 0f)
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -289,10 +324,10 @@ public final class WorldMapRenderer {
|
||||
for (int py = 1; py < targetSize - 1; py++) {
|
||||
for (int px = 1; px < targetSize - 1; px++) {
|
||||
if (!cur[py * targetSize + px]) continue;
|
||||
if (heights[py * targetSize + (px-1)] < 0f) nxt[py * targetSize + (px-1)] = true;
|
||||
if (heights[py * targetSize + (px+1)] < 0f) nxt[py * targetSize + (px+1)] = true;
|
||||
if (heights[(py-1) * targetSize + px] < 0f) nxt[(py-1) * targetSize + px] = true;
|
||||
if (heights[(py+1) * targetSize + px] < 0f) nxt[(py+1) * targetSize + px] = true;
|
||||
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;
|
||||
@@ -303,6 +338,66 @@ public final class WorldMapRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
// Wasserfälle – klassisches Kartensymbol: Kammlinie + Kaskaden-Linien
|
||||
if (opts.showWaterfalls() && model.waterfalls() != null && !model.waterfalls().isEmpty()) {
|
||||
float wfLineW = Math.max(1.5f, targetSize / 800f);
|
||||
|
||||
for (PlacedWaterfall wf : model.waterfalls()) {
|
||||
// Kamm A→B und Basis D→C in Pixel-Koordinaten
|
||||
int pax = worldToPixel(wf.ax(), targetSize), paz = worldToPixel(wf.az(), targetSize);
|
||||
int pbx = worldToPixel(wf.bx(), targetSize), pbz = worldToPixel(wf.bz(), targetSize);
|
||||
int pdx = worldToPixel(wf.dx(), targetSize), pdz = worldToPixel(wf.dz(), targetSize);
|
||||
int pcx = worldToPixel(wf.cx(), targetSize), pcz = worldToPixel(wf.cz(), targetSize);
|
||||
|
||||
// Kammrichtung in Pixel-Space
|
||||
float cdx = pbx - pax, cdz = pbz - paz;
|
||||
float clen = (float) Math.sqrt(cdx * cdx + cdz * cdz);
|
||||
if (clen < 1f) { continue; }
|
||||
float ncx = cdx / clen, ncz = cdz / clen;
|
||||
|
||||
// Fließrichtung: Basismitten → Kammmitte im Pixel-Space
|
||||
float pmcx = (pax + pbx) * 0.5f, pmcz = (paz + pbz) * 0.5f;
|
||||
float pmbx = (pdx + pcx) * 0.5f, pmbz = (pdz + pcz) * 0.5f;
|
||||
float fvx = pmbx - pmcx, fvz = pmbz - pmcz;
|
||||
float flen = (float) Math.sqrt(fvx * fvx + fvz * fvz);
|
||||
float perpX, perpZ;
|
||||
if (flen > 2f) {
|
||||
// echte XZ-Fallrichtung verfügbar
|
||||
perpX = fvx / flen;
|
||||
perpZ = fvz / flen;
|
||||
} else {
|
||||
// Wasserfall senkrecht → Rechtsnormale der Kammlinie als Fallback
|
||||
perpX = ncz;
|
||||
perpZ = -ncx;
|
||||
}
|
||||
|
||||
// Pixelabstand pro Kaskaden-Stufe (mindestens 3 px, skaliert mit Bild)
|
||||
float step = Math.max(3f, targetSize / 512f) * 1.8f;
|
||||
|
||||
// ① Dunkler Halo unter der Kammlinie
|
||||
gfx.setStroke(new BasicStroke(wfLineW * 2.5f + 1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
||||
gfx.setColor(new Color(10, 50, 120, 160));
|
||||
gfx.drawLine(pax, paz, pbx, pbz);
|
||||
|
||||
// ② Helle Kammlinie (Kante, über die das Wasser fällt)
|
||||
gfx.setStroke(new BasicStroke(wfLineW * 2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
||||
gfx.setColor(new Color(220, 240, 255, 255));
|
||||
gfx.drawLine(pax, paz, pbx, pbz);
|
||||
|
||||
// ③ Kaskaden-Linien: 2 parallele Linien in Fallrichtung, jede schwächer
|
||||
int[] alpha = {190, 100};
|
||||
float[] wf2 = {wfLineW * 1.3f, wfLineW * 0.8f};
|
||||
for (int ci = 0; ci < 2; ci++) {
|
||||
float off = step * (ci + 1);
|
||||
int ox1 = Math.round(pax + perpX * off), oz1 = Math.round(paz + perpZ * off);
|
||||
int ox2 = Math.round(pbx + perpX * off), oz2 = Math.round(pbz + perpZ * off);
|
||||
gfx.setStroke(new BasicStroke(wf2[ci], BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
||||
gfx.setColor(new Color(160, 210, 255, alpha[ci]));
|
||||
gfx.drawLine(ox1, oz1, ox2, oz2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Area-Polygone
|
||||
if (opts.showAreas()) {
|
||||
float dash = lineW * 7f, gap = lineW * 4f;
|
||||
@@ -375,8 +470,8 @@ public final class WorldMapRenderer {
|
||||
public static BufferedImage render(RenderInput input, int targetSize, RenderOptions opts) {
|
||||
return render(new WorldMapRenderModel(
|
||||
input.mapData(), input.areas(), input.zones(), input.locations(),
|
||||
input.waters(), input.models(), input.slotColorsRGB(),
|
||||
null, null, null, null
|
||||
input.waters(), input.models(), null,
|
||||
input.slotColorsRGB(), null, null, null, null, null
|
||||
), targetSize, opts);
|
||||
}
|
||||
|
||||
@@ -408,9 +503,11 @@ public final class WorldMapRenderer {
|
||||
float[] heights = null;
|
||||
float minH = 0f, maxH = 1f;
|
||||
|
||||
boolean[] seaPixels2 = null;
|
||||
if (opts.showTerrain() || opts.showWater()) {
|
||||
int UV2 = MapData.UPPER_VERTS;
|
||||
heights = new float[targetSize * targetSize];
|
||||
seaPixels2 = new boolean[targetSize * targetSize];
|
||||
minH = Float.MAX_VALUE;
|
||||
maxH = -Float.MAX_VALUE;
|
||||
for (int py = 0; py < targetSize; py++) {
|
||||
@@ -421,10 +518,13 @@ public final class WorldMapRenderer {
|
||||
float wx = wx0 + (float) px / (targetSize - 1) * rSize;
|
||||
int hx = iclamp((int) ((wx + WORLD_HALF) / WORLD_SIZE * (TV - 1)), 0, TV - 1);
|
||||
int ux = iclamp((int) ((wx + WORLD_HALF) / WORLD_SIZE * (UV2 - 1)), 0, UV2 - 1);
|
||||
float h = m.terrainHeight[hz * TV + hx];
|
||||
float baseH = m.terrainHeight[hz * TV + hx];
|
||||
float h = baseH;
|
||||
float upper = m.upperTop[uz * UV2 + ux];
|
||||
if (upper > 0f && upper > h) { h = upper; }
|
||||
heights[py * targetSize + px] = h;
|
||||
// upperTop enthält nach bakeVoxelHeights auch Voxel-Höhen → korrekt als Proxy nutzbar
|
||||
seaPixels2[py * targetSize + px] = (Math.max(baseH, upper) < 0f);
|
||||
if (h < minH) { minH = h; }
|
||||
if (h > maxH) { maxH = h; }
|
||||
}
|
||||
@@ -507,7 +607,7 @@ public final class WorldMapRenderer {
|
||||
// ── Weißfüllung ───────────────────────────────────────────────────────
|
||||
for (int py = 0; py < targetSize; py++) {
|
||||
for (int px = 0; px < targetSize; px++) {
|
||||
if (heights[py * targetSize + px] < 0f) img.setRGB(px, py, WATER_COLOR);
|
||||
if (seaPixels2 != null && seaPixels2[py * targetSize + px]) img.setRGB(px, py, WATER_COLOR);
|
||||
}
|
||||
}
|
||||
for (PlacedWater w : input.waters()) {
|
||||
@@ -546,7 +646,7 @@ public final class WorldMapRenderer {
|
||||
int cx = (int)(ox + wmW * 0.5f), cy = (int)oy;
|
||||
if (cx >= 0 && cx < targetSize && cy >= 0 && cy < targetSize
|
||||
&& ox >= 0 && ox + wmW < targetSize
|
||||
&& heights[cy * targetSize + cx] < 0f) {
|
||||
&& seaPixels2 != null && seaPixels2[cy * targetSize + cx]) {
|
||||
Path2D mark = new Path2D.Float();
|
||||
mark.moveTo(ox, oy);
|
||||
mark.curveTo(ox + wmW*0.3f, oy - wmA, ox + wmW*0.7f, oy + wmA, ox + wmW, oy);
|
||||
@@ -597,9 +697,9 @@ public final class WorldMapRenderer {
|
||||
boolean[] nxt = new boolean[targetSize * targetSize];
|
||||
for (int py = 1; py < targetSize - 1; py++) {
|
||||
for (int px = 1; px < targetSize - 1; px++) {
|
||||
if (heights[py * targetSize + px] >= 0f) continue;
|
||||
if (heights[py * targetSize + (px-1)] >= 0f || heights[py * targetSize + (px+1)] >= 0f ||
|
||||
heights[(py-1) * targetSize + px] >= 0f || heights[(py+1) * targetSize + px] >= 0f)
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -608,10 +708,10 @@ public final class WorldMapRenderer {
|
||||
for (int py = 1; py < targetSize - 1; py++) {
|
||||
for (int px = 1; px < targetSize - 1; px++) {
|
||||
if (!cur[py * targetSize + px]) continue;
|
||||
if (heights[py * targetSize + (px-1)] < 0f) nxt[py * targetSize + (px-1)] = true;
|
||||
if (heights[py * targetSize + (px+1)] < 0f) nxt[py * targetSize + (px+1)] = true;
|
||||
if (heights[(py-1) * targetSize + px] < 0f) nxt[(py-1) * targetSize + px] = true;
|
||||
if (heights[(py+1) * targetSize + px] < 0f) nxt[(py+1) * targetSize + px] = true;
|
||||
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;
|
||||
@@ -830,6 +930,32 @@ public final class WorldMapRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
/** Bilinear auf float[stride²]; nur wenn alle 4 Nachbarn > 0 – sonst nearest-neighbor. */
|
||||
private static float bilerpPos(float[] arr, float fx, float fz, int stride) {
|
||||
int x0 = (int) fx, z0 = (int) fz;
|
||||
int x1 = Math.min(x0 + 1, stride - 1), z1 = Math.min(z0 + 1, stride - 1);
|
||||
float v00 = arr[z0 * stride + x0], v10 = arr[z0 * stride + x1];
|
||||
float v01 = arr[z1 * stride + x0], v11 = arr[z1 * stride + x1];
|
||||
if (v00 <= 0f || v10 <= 0f || v01 <= 0f || v11 <= 0f) { return v00; }
|
||||
float tx = fx - x0, tz = fz - z0;
|
||||
return (v00*(1-tx) + v10*tx)*(1-tz) + (v01*(1-tx) + v11*tx)*tz;
|
||||
}
|
||||
|
||||
/** Bilinear auf float[stride²]; nur wenn alle 4 Nachbarn nicht NaN und gleiches Vorzeichen – sonst nearest-neighbor. */
|
||||
private static float bilerpNaN(float[] arr, float fx, float fz, int stride) {
|
||||
int x0 = (int) fx, z0 = (int) fz;
|
||||
int x1 = Math.min(x0 + 1, stride - 1), z1 = Math.min(z0 + 1, stride - 1);
|
||||
float v00 = arr[z0 * stride + x0], v10 = arr[z0 * stride + x1];
|
||||
float v01 = arr[z1 * stride + x0], v11 = arr[z1 * stride + x1];
|
||||
if (Float.isNaN(v00) || Float.isNaN(v10) || Float.isNaN(v01) || Float.isNaN(v11)) { return v00; }
|
||||
// Kein Bilinear über Vorzeichen-Wechsel (Unterwasser↔Oberfläche) → nearest-neighbor
|
||||
boolean allPos = v00 > 0 && v10 > 0 && v01 > 0 && v11 > 0;
|
||||
boolean allNeg = v00 < 0 && v10 < 0 && v01 < 0 && v11 < 0;
|
||||
if (!allPos && !allNeg) { return v00; }
|
||||
float tx = fx - x0, tz = fz - z0;
|
||||
return (v00*(1-tx) + v10*tx)*(1-tz) + (v01*(1-tx) + v11*tx)*tz;
|
||||
}
|
||||
|
||||
private static float bilerp(byte[] arr, int i00, int i10, int i01, int i11, float tx, float tz) {
|
||||
float v00 = (arr[i00] & 0xFF) / 255f, v10 = (arr[i10] & 0xFF) / 255f;
|
||||
float v01 = (arr[i01] & 0xFF) / 255f, v11 = (arr[i11] & 0xFF) / 255f;
|
||||
|
||||
Reference in New Issue
Block a user