Ingame Karte weitergebaut
This commit is contained in:
@@ -1589,6 +1589,928 @@ public final class WorldMapRenderer {
|
||||
return avg;
|
||||
}
|
||||
|
||||
// ── Allgemeine Layer-Rendering (Editor + Ingame) ──────────────────────────
|
||||
|
||||
/** Auflösung für Marching-Squares (Küste, Klippe, Wald). */
|
||||
public static final int SEA_MASK_SIZE = 1024;
|
||||
/** Auflösung der Forst-Dichte-Map. */
|
||||
public static final int FOREST_DENSITY_SIZE = 512;
|
||||
|
||||
/** Berechnet die Baum-Dichte-Map (FOREST_DENSITY_SIZE × FOREST_DENSITY_SIZE). */
|
||||
public static float[] computeForestDensity(WorldMapRenderModel model) {
|
||||
final int FD = FOREST_DENSITY_SIZE;
|
||||
final float radius = 14f;
|
||||
final int rc = Math.max(1, (int)(radius / (WORLD_SIZE / FD)));
|
||||
final float[] fd = new float[FD * FD];
|
||||
for (PlacedModel pm : model.models()) {
|
||||
if (!isTree(pm)) continue;
|
||||
int fx = Math.max(0, Math.min(FD-1, (int)((pm.x()+WORLD_HALF)/WORLD_SIZE*FD)));
|
||||
int fz = Math.max(0, Math.min(FD-1, (int)((WORLD_HALF-pm.z())/WORLD_SIZE*FD)));
|
||||
for (int dz=-rc; dz<=rc; dz++) {
|
||||
for (int dx=-rc; dx<=rc; dx++) {
|
||||
if (dx*dx+dz*dz <= rc*rc) {
|
||||
int nx=fx+dx, nz=fz+dz;
|
||||
if (nx>=0&&nx<FD&&nz>=0&&nz<FD) fd[nz*FD+nx]+=1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
/** Erzeugt Forst-Pixel (ARGB int[], Größe size×size) aus der Dichte-Map. */
|
||||
public static int[] computeForestPixels(float[] density, float threshold, int size) {
|
||||
final int FD = FOREST_DENSITY_SIZE;
|
||||
final int forestFill = 0x80_4a9a28;
|
||||
final int[] px = new int[size * size];
|
||||
for (int py = 0; py < size; py++) {
|
||||
float wz = (float) py / (size - 1) * WORLD_SIZE - WORLD_HALF;
|
||||
for (int bx = 0; bx < size; bx++) {
|
||||
float wx = (float) bx / (size - 1) * WORLD_SIZE - WORLD_HALF;
|
||||
float fsfx = Math.max(0f, Math.min(FD - 1.001f, (wx + WORLD_HALF) / WORLD_SIZE * FD));
|
||||
float fsfz = Math.max(0f, Math.min(FD - 1.001f, (WORLD_HALF - wz) / WORLD_SIZE * FD));
|
||||
int fsx0 = (int) fsfx, fsx1 = Math.min(fsx0 + 1, FD - 1);
|
||||
int fsz0 = (int) fsfz, fsz1 = Math.min(fsz0 + 1, FD - 1);
|
||||
float fstx = fsfx - fsx0, fstz = fsfz - fsz0;
|
||||
float d = (density[fsz0 * FD + fsx0] * (1 - fstx) + density[fsz0 * FD + fsx1] * fstx) * (1 - fstz)
|
||||
+ (density[fsz1 * FD + fsx0] * (1 - fstx) + density[fsz1 * FD + fsx1] * fstx) * fstz;
|
||||
if (d > threshold) px[py * size + bx] = forestFill;
|
||||
}
|
||||
}
|
||||
return px;
|
||||
}
|
||||
|
||||
/** Liefert Küstenpfade: aus Modell wenn vorhanden, sonst per Marching Squares. */
|
||||
public static List<float[][]> buildCoastPaths(WorldMapRenderModel model) {
|
||||
if (model.coastPaths() != null) return model.coastPaths();
|
||||
float[] heights = buildTerrainSamples(model.mapData(), SEA_MASK_SIZE, model.voxelSurface());
|
||||
java.util.List<float[]> segs = new java.util.ArrayList<>();
|
||||
marchingSquares(heights, SEA_MASK_SIZE, 0f, null, null, segs);
|
||||
for (PlacedWater w : model.waters()) {
|
||||
marchingSquares(heights, SEA_MASK_SIZE, w.waterHeight(), w.pointsX(), w.pointsZ(), segs);
|
||||
}
|
||||
return assembleClosedPaths(segs.toArray(new float[0][]), 3);
|
||||
}
|
||||
|
||||
/** Extrahiert Kliff-Konturen als geglättete Weltkoordinaten-Pfade. */
|
||||
public static List<float[][]> buildCliffPaths(MapData mapData, float[] voxelSurface) {
|
||||
final int SIZE = SEA_MASK_SIZE;
|
||||
float[] heights = buildTerrainSamples(mapData, SIZE, voxelSurface);
|
||||
float cellW = WORLD_SIZE / (SIZE - 1);
|
||||
int dPx = Math.max(1, (int)(1.5f / cellW));
|
||||
float[] mask = new float[SIZE * SIZE];
|
||||
for (int py = 0; py < SIZE; py++) {
|
||||
for (int px = 0; px < SIZE; px++) {
|
||||
int idx = py * SIZE + px;
|
||||
float hR = heights[py * SIZE + Math.min(SIZE - 1, px + dPx)];
|
||||
float hL = heights[py * SIZE + Math.max(0, px - dPx)];
|
||||
float hD = heights[Math.min(SIZE - 1, py + dPx) * SIZE + px];
|
||||
float hU = heights[Math.max(0, py - dPx) * SIZE + 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))));
|
||||
mask[idx] = slope > 30.0 ? -1f : 1f;
|
||||
}
|
||||
}
|
||||
java.util.List<float[]> segs = new java.util.ArrayList<>();
|
||||
marchingSquares(mask, SIZE, 0.5f, null, null, segs);
|
||||
return fixWindingForClip(assembleClosedPaths(segs.toArray(new float[0][]), 3));
|
||||
}
|
||||
|
||||
/** Extrahiert Wald-Konturen aus der Density-Map als Weltkoordinaten-Pfade. */
|
||||
public static List<float[][]> buildForestOutlinePaths(float[] density, int fdSize, float threshold) {
|
||||
final int SIZE = SEA_MASK_SIZE;
|
||||
float[] h = new float[SIZE * SIZE];
|
||||
for (int py = 0; py < SIZE; py++) {
|
||||
float wz = (float) py / (SIZE - 1) * WORLD_SIZE - WORLD_HALF;
|
||||
float fsfz = Math.max(0f, Math.min(fdSize - 1.001f, (WORLD_HALF - wz) / WORLD_SIZE * fdSize));
|
||||
int fsz0 = (int) fsfz, fsz1 = Math.min(fsz0 + 1, fdSize - 1);
|
||||
float fstz = fsfz - fsz0;
|
||||
for (int px = 0; px < SIZE; px++) {
|
||||
float wx = (float) px / (SIZE - 1) * WORLD_SIZE - WORLD_HALF;
|
||||
float fsfx = Math.max(0f, Math.min(fdSize - 1.001f, (wx + WORLD_HALF) / WORLD_SIZE * fdSize));
|
||||
int fsx0 = (int) fsfx, fsx1 = Math.min(fsx0 + 1, fdSize - 1);
|
||||
float fstx = fsfx - fsx0;
|
||||
float d = (density[fsz0 * fdSize + fsx0] * (1 - fstx) + density[fsz0 * fdSize + fsx1] * fstx) * (1 - fstz)
|
||||
+ (density[fsz1 * fdSize + fsx0] * (1 - fstx) + density[fsz1 * fdSize + fsx1] * fstx) * fstz;
|
||||
h[py * SIZE + px] = threshold - d;
|
||||
}
|
||||
}
|
||||
java.util.List<float[]> segs = new java.util.ArrayList<>();
|
||||
marchingSquares(h, SIZE, 0f, null, null, segs);
|
||||
List<float[][]> paths = assembleClosedPaths(segs.toArray(new float[0][]), 0);
|
||||
final float MIN_PERIM_SQ = 60f * 60f;
|
||||
return paths.stream().filter(p -> pathPerimeterSq(p) >= MIN_PERIM_SQ)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
/** Baut zusammenhängende Weg-Ketten aus dem PathNetwork. */
|
||||
public static List<float[][]> loadPathChains() {
|
||||
try {
|
||||
de.blight.common.path.PathNetwork net = de.blight.common.path.PathNetworkIO.load();
|
||||
List<de.blight.common.path.PathNode> nodes = net.getNodes();
|
||||
if (nodes.isEmpty()) return java.util.Collections.emptyList();
|
||||
java.util.Map<String, de.blight.common.path.PathNode> nodeMap = new java.util.HashMap<>();
|
||||
java.util.Map<String, List<String>> adj = new java.util.HashMap<>();
|
||||
for (de.blight.common.path.PathNode n : nodes) {
|
||||
nodeMap.put(n.getUuid(), n);
|
||||
adj.put(n.getUuid(), new java.util.ArrayList<>());
|
||||
}
|
||||
for (de.blight.common.path.PathEdge e : net.getEdges()) {
|
||||
adj.computeIfAbsent(e.getNodeUuidA(), k -> new java.util.ArrayList<>()).add(e.getNodeUuidB());
|
||||
adj.computeIfAbsent(e.getNodeUuidB(), k -> new java.util.ArrayList<>()).add(e.getNodeUuidA());
|
||||
}
|
||||
java.util.Set<String> junctions = new java.util.HashSet<>();
|
||||
for (de.blight.common.path.PathNode n : nodes) {
|
||||
if (adj.get(n.getUuid()).size() != 2) junctions.add(n.getUuid());
|
||||
}
|
||||
if (junctions.isEmpty()) junctions.add(nodes.get(0).getUuid());
|
||||
java.util.Set<String> visitedEdges = new java.util.HashSet<>();
|
||||
List<float[][]> chains = new java.util.ArrayList<>();
|
||||
for (String jUuid : junctions) {
|
||||
for (String nb : adj.getOrDefault(jUuid, java.util.Collections.emptyList())) {
|
||||
String ek = pathEdgeKeyStr(jUuid, nb);
|
||||
if (visitedEdges.contains(ek)) continue;
|
||||
java.util.List<float[]> chain = new java.util.ArrayList<>();
|
||||
de.blight.common.path.PathNode startNode = nodeMap.get(jUuid);
|
||||
chain.add(new float[]{startNode.getPosition().x, startNode.getPosition().z});
|
||||
String prev = jUuid, curr = nb;
|
||||
while (curr != null) {
|
||||
visitedEdges.add(pathEdgeKeyStr(prev, curr));
|
||||
de.blight.common.path.PathNode currNode = nodeMap.get(curr);
|
||||
if (currNode == null) break;
|
||||
chain.add(new float[]{currNode.getPosition().x, currNode.getPosition().z});
|
||||
if (junctions.contains(curr)) break;
|
||||
String next = null;
|
||||
for (String n2 : adj.getOrDefault(curr, java.util.Collections.emptyList())) {
|
||||
if (!n2.equals(prev)) { next = n2; break; }
|
||||
}
|
||||
prev = curr; curr = next;
|
||||
}
|
||||
if (chain.size() >= 2) chains.add(chain.toArray(new float[0][]));
|
||||
}
|
||||
}
|
||||
return chains;
|
||||
} catch (Exception ignore) {
|
||||
return java.util.Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cave-Pfad-Splitting (gemeinsam genutzt von Editor und Ingame) ─────────
|
||||
|
||||
public static final float PATH_UNDERGROUND_CLEARANCE = 3.0f;
|
||||
|
||||
/** Prüft ob eine Weltposition im Cave-Fußabdruck liegt. Erkennt Maskengröße automatisch. */
|
||||
public static boolean isCavePoint(boolean[] mask, float wx, float wz) {
|
||||
int SIZE = (int) Math.round(Math.sqrt(mask.length));
|
||||
int px = Math.round((wx + WORLD_HALF) / WORLD_SIZE * (SIZE - 1));
|
||||
int pz = Math.round((wz + WORLD_HALF) / WORLD_SIZE * (SIZE - 1));
|
||||
px = Math.max(0, Math.min(SIZE - 1, px));
|
||||
pz = Math.max(0, Math.min(SIZE - 1, pz));
|
||||
return mask[pz * SIZE + px];
|
||||
}
|
||||
|
||||
/**
|
||||
* Teilt Pfad-Ketten an Tunnel-Grenzen auf.
|
||||
* Index 0 = Oberflächensegmente, Index 1 = Tunnel-Segmente.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static List<float[][]>[] splitPathChains(List<float[][]> chains, float[] voxelSurface,
|
||||
boolean[] caveMask) {
|
||||
List<float[][]> surface = new java.util.ArrayList<>();
|
||||
List<float[][]> cave = new java.util.ArrayList<>();
|
||||
for (float[][] chain : chains) {
|
||||
splitChainByVoxelSurface(chain, voxelSurface, caveMask, surface, cave);
|
||||
}
|
||||
return new List[]{surface, cave};
|
||||
}
|
||||
|
||||
private static boolean isNodeUnderground(float[] node, float[] voxelSurface, boolean[] caveMask) {
|
||||
if (node.length < 2) return false;
|
||||
if (caveMask != null && isCavePoint(caveMask, node[0], node[1])) return true;
|
||||
if (node.length < 3 || Float.isNaN(node[2])) return false;
|
||||
float wy = node[2];
|
||||
int px = Math.max(0, Math.min(VOXEL_SURFACE_RES - 1,
|
||||
Math.round((node[0] + WORLD_HALF) / WORLD_SIZE * (VOXEL_SURFACE_RES - 1))));
|
||||
int pz = Math.max(0, Math.min(VOXEL_SURFACE_RES - 1,
|
||||
Math.round((node[1] + WORLD_HALF) / WORLD_SIZE * (VOXEL_SURFACE_RES - 1))));
|
||||
float vs = voxelSurface[pz * VOXEL_SURFACE_RES + px];
|
||||
return !Float.isNaN(vs) && vs > wy + PATH_UNDERGROUND_CLEARANCE;
|
||||
}
|
||||
|
||||
private static float[] lerpNode(float[] a, float[] b, float t) {
|
||||
float ax = a[0], az = a[1], ay = a.length > 2 ? a[2] : 0f;
|
||||
float bx = b[0], bz = b[1], by = b.length > 2 ? b[2] : 0f;
|
||||
return new float[]{ax + (bx - ax) * t, az + (bz - az) * t, ay + (by - ay) * t};
|
||||
}
|
||||
|
||||
private static void splitChainByVoxelSurface(float[][] chain, float[] voxelSurface,
|
||||
boolean[] caveMask,
|
||||
List<float[][]> surface, List<float[][]> cave) {
|
||||
if (chain.length < 2) return;
|
||||
java.util.List<float[]> current = new java.util.ArrayList<>();
|
||||
boolean currentCave = isNodeUnderground(chain[0], voxelSurface, caveMask);
|
||||
current.add(chain[0]);
|
||||
for (int i = 0; i < chain.length - 1; i++) {
|
||||
float[] a = chain[i];
|
||||
float[] b = chain[i + 1];
|
||||
boolean bCave = isNodeUnderground(b, voxelSurface, caveMask);
|
||||
if (currentCave == bCave) {
|
||||
if (!currentCave) {
|
||||
float dx = b[0] - a[0], dz = b[1] - a[1];
|
||||
int steps = Math.max(8, (int) ((float) Math.sqrt(dx * dx + dz * dz) / 2f));
|
||||
float[] sample = null;
|
||||
for (int s = 1; s < steps && sample == null; s++) {
|
||||
float[] p = lerpNode(a, b, s / (float) steps);
|
||||
if (isNodeUnderground(p, voxelSurface, caveMask)) sample = p;
|
||||
}
|
||||
if (sample != null) {
|
||||
float[] c1 = findPathCrossing(a, sample, voxelSurface, caveMask, false);
|
||||
float[] c2 = findPathCrossing(sample, b, voxelSurface, caveMask, true);
|
||||
current.add(c1);
|
||||
if (current.size() >= 2) surface.add(current.toArray(new float[0][]));
|
||||
current = new java.util.ArrayList<>();
|
||||
current.add(c1); current.add(sample); current.add(c2);
|
||||
cave.add(current.toArray(new float[0][]));
|
||||
current = new java.util.ArrayList<>();
|
||||
current.add(c2); current.add(b);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
current.add(b);
|
||||
} else {
|
||||
float[] crossing = findPathCrossing(a, b, voxelSurface, caveMask, currentCave);
|
||||
current.add(crossing);
|
||||
if (current.size() >= 2) (currentCave ? cave : surface).add(current.toArray(new float[0][]));
|
||||
current = new java.util.ArrayList<>();
|
||||
current.add(crossing); current.add(b);
|
||||
currentCave = bCave;
|
||||
}
|
||||
}
|
||||
if (current.size() >= 2) (currentCave ? cave : surface).add(current.toArray(new float[0][]));
|
||||
}
|
||||
|
||||
private static float[] findPathCrossing(float[] a, float[] b, float[] vs, boolean[] caveMask,
|
||||
boolean aIsUnderground) {
|
||||
float ax = a[0], az = a[1], ay = a.length > 2 ? a[2] : 0f;
|
||||
float bx = b[0], bz = b[1], by = b.length > 2 ? b[2] : 0f;
|
||||
for (int i = 0; i < 12; i++) {
|
||||
float mx = (ax + bx) * 0.5f, mz = (az + bz) * 0.5f, my = (ay + by) * 0.5f;
|
||||
float[] mid = {mx, mz, my};
|
||||
if (isNodeUnderground(mid, vs, caveMask) == aIsUnderground) { ax = mx; az = mz; ay = my; }
|
||||
else { bx = mx; bz = mz; by = my; }
|
||||
}
|
||||
return new float[]{(ax + bx) * 0.5f, (az + bz) * 0.5f, (ay + by) * 0.5f};
|
||||
}
|
||||
|
||||
/** Filtert Gebäude-Footprints: wantCave=true → nur Untergrund, false → nur Oberfläche. */
|
||||
public static List<float[]> filterBuildingsByCave(List<float[]> footprints, boolean[] caveMask,
|
||||
boolean wantCave) {
|
||||
List<float[]> result = new java.util.ArrayList<>();
|
||||
for (float[] fp : footprints) {
|
||||
if (isCavePoint(caveMask, fp[0], fp[1]) == wantCave) result.add(fp);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Filtert Areas anhand des Polygon-Schwerpunkts. */
|
||||
public static List<PlacedArea> filterAreasByCave(List<PlacedArea> areas, boolean[] caveMask,
|
||||
boolean wantCave) {
|
||||
List<PlacedArea> result = new java.util.ArrayList<>();
|
||||
for (PlacedArea area : areas) {
|
||||
float[] xs = area.pointsX(), zs = area.pointsZ();
|
||||
if (xs == null || xs.length == 0) { if (!wantCave) result.add(area); continue; }
|
||||
float cx = 0, cz = 0;
|
||||
for (int i = 0; i < xs.length; i++) { cx += xs[i]; cz += zs[i]; }
|
||||
cx /= xs.length; cz /= zs.length;
|
||||
if (isCavePoint(caveMask, cx, cz) == wantCave) result.add(area);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String pathEdgeKeyStr(String a, String b) {
|
||||
return a.compareTo(b) < 0 ? a + "|" + b : b + "|" + a;
|
||||
}
|
||||
|
||||
// ── Marching-Squares (identisch zu WorldMapView, hier für game-Zugriff) ───
|
||||
|
||||
static void marchingSquares(float[] h, int size, float thr,
|
||||
float[] polyX, float[] polyZ, java.util.List<float[]> out) {
|
||||
for (int mz = 0; mz < size - 1; mz++) {
|
||||
for (int mx = 0; mx < size - 1; mx++) {
|
||||
float hTL = h[mz*size + mx], hTR = h[mz*size + (mx+1)];
|
||||
float hBL = h[(mz+1)*size + mx], hBR = h[(mz+1)*size + (mx+1)];
|
||||
float wx0 = msW(mx, size), wx1 = msW(mx+1, size);
|
||||
float wz0 = msW(mz, size), wz1 = msW(mz+1, size);
|
||||
boolean iTL, iTR, iBL, iBR;
|
||||
if (polyX != null) {
|
||||
iTL = hTL < thr && msPolyContains(polyX, polyZ, wx0, wz0);
|
||||
iTR = hTR < thr && msPolyContains(polyX, polyZ, wx1, wz0);
|
||||
iBL = hBL < thr && msPolyContains(polyX, polyZ, wx0, wz1);
|
||||
iBR = hBR < thr && msPolyContains(polyX, polyZ, wx1, wz1);
|
||||
} else {
|
||||
iTL = hTL < thr; iTR = hTR < thr;
|
||||
iBL = hBL < thr; iBR = hBR < thr;
|
||||
}
|
||||
int idx = (iTL?8:0)|(iTR?4:0)|(iBR?2:0)|(iBL?1:0);
|
||||
if (idx == 0 || idx == 15) continue;
|
||||
float xTop = msLerp(wx0, wx1, thr, hTL, hTR);
|
||||
float xBot = msLerp(wx0, wx1, thr, hBL, hBR);
|
||||
float zLft = msLerp(wz0, wz1, thr, hTL, hBL);
|
||||
float zRgt = msLerp(wz0, wz1, thr, hTR, hBR);
|
||||
switch (idx) {
|
||||
case 1: case 14: out.add(new float[]{wx0,zLft, xBot,wz1}); break;
|
||||
case 2: case 13: out.add(new float[]{xBot,wz1, wx1,zRgt}); break;
|
||||
case 4: case 11: out.add(new float[]{xTop,wz0, wx1,zRgt}); break;
|
||||
case 8: case 7: out.add(new float[]{wx0,zLft, xTop,wz0}); break;
|
||||
case 3: case 12: out.add(new float[]{wx0,zLft, wx1,zRgt}); break;
|
||||
case 6: case 9: out.add(new float[]{xTop,wz0, xBot,wz1}); break;
|
||||
case 5: {
|
||||
float hC = (hTL+hTR+hBL+hBR)*0.25f;
|
||||
if (hC >= thr) { out.add(new float[]{wx0,zLft,xTop,wz0}); out.add(new float[]{xBot,wz1,wx1,zRgt}); }
|
||||
else { out.add(new float[]{wx0,zLft,xBot,wz1}); out.add(new float[]{xTop,wz0,wx1,zRgt}); }
|
||||
break;
|
||||
}
|
||||
case 10: {
|
||||
float hC = (hTL+hTR+hBL+hBR)*0.25f;
|
||||
if (hC >= thr) { out.add(new float[]{xTop,wz0,wx1,zRgt}); out.add(new float[]{wx0,zLft,xBot,wz1}); }
|
||||
else { out.add(new float[]{wx0,zLft,xTop,wz0}); out.add(new float[]{xBot,wz1,wx1,zRgt}); }
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static float msLerp(float from, float to, float thr, float hA, float hB) {
|
||||
float d = hB - hA;
|
||||
if (Math.abs(d) < 1e-6f) return (from + to) * 0.5f;
|
||||
return from + Math.max(0f, Math.min(1f, (thr - hA) / d)) * (to - from);
|
||||
}
|
||||
|
||||
private static float msW(int idx, int size) {
|
||||
return (float)(idx / (double)(size-1) * WORLD_SIZE - WORLD_HALF);
|
||||
}
|
||||
|
||||
private static boolean msPolyContains(float[] xs, float[] zs, float px, float pz) {
|
||||
boolean inside = false;
|
||||
int n = xs.length;
|
||||
for (int i = 0, j = n - 1; i < n; j = i++) {
|
||||
if ((zs[i] > pz) != (zs[j] > pz) &&
|
||||
px < (xs[j] - xs[i]) * (pz - zs[i]) / (zs[j] - zs[i]) + xs[i]) {
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
static List<float[][]> assembleClosedPaths(float[][] segs, int chaikinIter) {
|
||||
if (segs == null || segs.length == 0) return java.util.Collections.emptyList();
|
||||
java.util.Map<Long, java.util.List<Integer>> adj = new java.util.HashMap<>(segs.length * 4);
|
||||
for (int i = 0; i < segs.length; i++) {
|
||||
adj.computeIfAbsent(coastPtKey(segs[i][0], segs[i][1]), k -> new java.util.ArrayList<>()).add(i);
|
||||
adj.computeIfAbsent(coastPtKey(segs[i][2], segs[i][3]), k -> new java.util.ArrayList<>()).add(i);
|
||||
}
|
||||
boolean[] used = new boolean[segs.length];
|
||||
List<float[][]> result = new java.util.ArrayList<>();
|
||||
for (int start = 0; start < segs.length; start++) {
|
||||
if (used[start]) continue;
|
||||
used[start] = true;
|
||||
java.util.List<float[]> fwd = new java.util.ArrayList<>();
|
||||
java.util.List<float[]> bwd = new java.util.ArrayList<>();
|
||||
fwd.add(new float[]{segs[start][2], segs[start][3]});
|
||||
bwd.add(new float[]{segs[start][0], segs[start][1]});
|
||||
coastExtend(fwd, segs, adj, used);
|
||||
coastExtend(bwd, segs, adj, used);
|
||||
float[][] raw = new float[bwd.size() + fwd.size()][];
|
||||
for (int i = 0; i < bwd.size(); i++) raw[i] = bwd.get(bwd.size() - 1 - i);
|
||||
for (int i = 0; i < fwd.size(); i++) raw[bwd.size() + i] = fwd.get(i);
|
||||
float[][] path = simplifyCoastPath(raw);
|
||||
result.add(chaikinIter > 0 ? chaikin(path, chaikinIter) : path);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void coastExtend(java.util.List<float[]> pts, float[][] segs,
|
||||
java.util.Map<Long, java.util.List<Integer>> adj, boolean[] used) {
|
||||
for (;;) {
|
||||
float[] p = pts.get(pts.size() - 1);
|
||||
java.util.List<Integer> nb = adj.get(coastPtKey(p[0], p[1]));
|
||||
int next = -1;
|
||||
if (nb != null) { for (int idx : nb) { if (!used[idx]) { next = idx; break; } } }
|
||||
if (next < 0) break;
|
||||
used[next] = true;
|
||||
float[] seg = segs[next];
|
||||
boolean atA = Math.abs(seg[0] - p[0]) < 0.1f && Math.abs(seg[1] - p[1]) < 0.1f;
|
||||
pts.add(atA ? new float[]{seg[2], seg[3]} : new float[]{seg[0], seg[1]});
|
||||
}
|
||||
}
|
||||
|
||||
private static long coastPtKey(float x, float z) {
|
||||
return ((long)Math.round(x * 4)) << 32 | (Math.round(z * 4) & 0xFFFFFFFFL);
|
||||
}
|
||||
|
||||
private static float[][] simplifyCoastPath(float[][] path) {
|
||||
int n = path.length;
|
||||
int len = (n > 1 && Math.abs(path[0][0]-path[n-1][0]) < 0.01f
|
||||
&& Math.abs(path[0][1]-path[n-1][1]) < 0.01f) ? n - 1 : n;
|
||||
if (len <= 2) return path;
|
||||
java.util.List<float[]> out = new java.util.ArrayList<>(len);
|
||||
for (int i = 0; i < len; i++) {
|
||||
float[] a = path[(i + len - 1) % len], b = path[i], c = path[(i + 1) % len];
|
||||
float cross = (b[0]-a[0])*(c[1]-a[1]) - (b[1]-a[1])*(c[0]-a[0]);
|
||||
if (Math.abs(cross) > 1e-3f) out.add(b);
|
||||
}
|
||||
return out.isEmpty() ? new float[][]{path[0]} : out.toArray(new float[0][]);
|
||||
}
|
||||
|
||||
private static float[][] chaikin(float[][] path, int iterations) {
|
||||
float[][] cur = path;
|
||||
for (int iter = 0; iter < iterations; iter++) {
|
||||
int n = cur.length;
|
||||
float[][] next = new float[n * 2][];
|
||||
for (int i = 0; i < n; i++) {
|
||||
float[] p0 = cur[i], p1 = cur[(i + 1) % n];
|
||||
next[i*2] = new float[]{ p0[0]*0.75f + p1[0]*0.25f, p0[1]*0.75f + p1[1]*0.25f };
|
||||
next[i*2+1] = new float[]{ p0[0]*0.25f + p1[0]*0.75f, p0[1]*0.25f + p1[1]*0.75f };
|
||||
}
|
||||
cur = next;
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
private static List<float[][]> fixWindingForClip(List<float[][]> paths) {
|
||||
if (paths.size() <= 1) return paths;
|
||||
List<float[][]> result = new java.util.ArrayList<>(paths.size());
|
||||
for (float[][] path : paths) {
|
||||
float[] probe = path[path.length / 2];
|
||||
int cnt = 0;
|
||||
for (float[][] other : paths) {
|
||||
if (other != path && msPointInPolygon(probe[0], probe[1], other)) cnt++;
|
||||
}
|
||||
boolean isHole = (cnt % 2) == 1;
|
||||
boolean isCCW = msSignedArea(path) > 0;
|
||||
result.add(isCCW == !isHole ? path : msReversePath(path));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static float msSignedArea(float[][] path) {
|
||||
float a = 0f;
|
||||
for (int i = 0, n = path.length; i < n; i++) {
|
||||
float[] p0 = path[i], p1 = path[(i + 1) % n];
|
||||
a += p0[0] * p1[1] - p1[0] * p0[1];
|
||||
}
|
||||
return a * 0.5f;
|
||||
}
|
||||
|
||||
private static boolean msPointInPolygon(float px, float py, float[][] poly) {
|
||||
boolean inside = false;
|
||||
for (int i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
||||
float xi = poly[i][0], yi = poly[i][1], xj = poly[j][0], yj = poly[j][1];
|
||||
if (((yi > py) != (yj > py)) && (px < (xj - xi) * (py - yi) / (yj - yi) + xi)) {
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
private static float[][] msReversePath(float[][] path) {
|
||||
float[][] r = new float[path.length][];
|
||||
for (int i = 0; i < path.length; i++) r[i] = path[path.length - 1 - i];
|
||||
return r;
|
||||
}
|
||||
|
||||
private static float pathPerimeterSq(float[][] path) {
|
||||
float len = 0f;
|
||||
for (int i = 0; i < path.length; i++) {
|
||||
float[] a = path[i], b = path[(i + 1) % path.length];
|
||||
float dx = b[0] - a[0], dz = b[1] - a[1];
|
||||
len += (float) Math.sqrt(dx * dx + dz * dz);
|
||||
}
|
||||
return len * len;
|
||||
}
|
||||
|
||||
// ── Vorberechnete Overlay-Daten (einmalig beim Laden, dann gecacht) ─────────
|
||||
|
||||
/** Vorberechnete Overlay-Daten (einmalig beim Laden, gecacht). */
|
||||
public record MapOverlayData(
|
||||
float[] forestDensity,
|
||||
int[] cliffPx, // ARGB bei COLOR_MAP_SIZE×COLOR_MAP_SIZE
|
||||
int[] colorMap, // RGB bei COLOR_MAP_SIZE×COLOR_MAP_SIZE (Terrain + Wasser mit AA)
|
||||
List<float[][]> coastPaths,
|
||||
List<float[][]> cliffPaths,
|
||||
List<float[][]> forestPaths,
|
||||
List<float[][]> pathChains
|
||||
) {}
|
||||
|
||||
/** Berechnet alle Overlay-Daten einmalig aus dem Render-Modell. */
|
||||
public static MapOverlayData computeOverlayData(WorldMapRenderModel model) {
|
||||
float[] density = computeForestDensity(model);
|
||||
|
||||
int[] cm = model.terrainColorMap() != null ? model.terrainColorMap()
|
||||
: buildTerrainColorMap(model.mapData(), model.slotColorsRGB(),
|
||||
model.mapTilePixels(), model.voxelSurface(),
|
||||
model.waters(), model.waterfalls());
|
||||
bakeWaterIntoColorMap(cm, model, COLOR_MAP_SIZE);
|
||||
|
||||
// Cave-Pfad-Splitting: nur Oberflächen-Pfade in pathChains
|
||||
List<float[][]> allPaths = loadPathChains();
|
||||
boolean[] caveMask = buildCaveFootprintMask(model.mapData(), VOXEL_SURFACE_RES);
|
||||
float[] vsForPaths = bakeVoxelSurface(model.mapData());
|
||||
List<float[][]> surfPaths = splitPathChains(allPaths, vsForPaths, caveMask)[0];
|
||||
|
||||
return new MapOverlayData(
|
||||
density,
|
||||
buildCliffOverlay(model.mapData(), model.voxelSurface()),
|
||||
cm,
|
||||
buildCoastPaths(model),
|
||||
buildCliffPaths(model.mapData(), model.voxelSurface()),
|
||||
buildForestOutlinePaths(density, FOREST_DENSITY_SIZE, 3.0f),
|
||||
surfPaths
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bäckt Wasser (Ozean + PlacedWater) mit 8×8 Sub-Pixel-Antialiasing
|
||||
* an der Küstenlinie direkt in den colorMap ein – identische Logik wie in render().
|
||||
*/
|
||||
private static void bakeWaterIntoColorMap(int[] cm, WorldMapRenderModel model, int size) {
|
||||
float[] heights = buildTerrainSamples(model.mapData(), size, model.voxelSurface());
|
||||
|
||||
// 1. Ozean + PlacedWater-Maske bei size×size
|
||||
boolean[] isWater = new boolean[size * size];
|
||||
for (int i = 0; i < heights.length; i++) isWater[i] = heights[i] < 0f;
|
||||
for (PlacedWater w : model.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) && heights[py * size + px] < wh)
|
||||
isWater[py * size + px] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int waterR = (WATER_COLOR >> 16) & 0xFF;
|
||||
int waterG = (WATER_COLOR >> 8) & 0xFF;
|
||||
int waterB = WATER_COLOR & 0xFF;
|
||||
|
||||
// 2. Vollständige Wasser-Pixel direkt setzen
|
||||
for (int i = 0; i < size * size; i++) {
|
||||
if (isWater[i]) cm[i] = (waterR << 16) | (waterG << 8) | waterB;
|
||||
}
|
||||
|
||||
// 3. Sub-Pixel-AA an der Ozean-Küste (8×8 Samples)
|
||||
int SUB = 8;
|
||||
float subInv = 1f / (SUB * SUB);
|
||||
for (int py = 0; py < size; py++) {
|
||||
for (int px = 0; px < size; px++) {
|
||||
int idx = py * size + px;
|
||||
boolean sea = isWater[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 >= size || nx < 0 || nx >= size) continue;
|
||||
if (isWater[ny * size + nx] != sea) boundary = true;
|
||||
}
|
||||
}
|
||||
if (!boundary) continue;
|
||||
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(model.mapData(), size, spx, spy, model.voxelSurface()) < 0f)
|
||||
wCount++;
|
||||
}
|
||||
}
|
||||
float wFrac = wCount * subInv;
|
||||
int landRGB = cm[idx];
|
||||
// bei Wasser-Pixel: Nachbarland-Farbe holen
|
||||
if (sea) {
|
||||
for (int dy = -1; dy <= 1 && sea; dy++) {
|
||||
for (int dx = -1; dx <= 1 && sea; dx++) {
|
||||
int ny = py+dy, nx = px+dx;
|
||||
if (ny < 0||ny>=size||nx<0||nx>=size) continue;
|
||||
if (!isWater[ny*size+nx]) { landRGB = cm[ny*size+nx]; sea = false; }
|
||||
}
|
||||
}
|
||||
}
|
||||
int lr=(landRGB>>16)&0xFF, lg=(landRGB>>8)&0xFF, lb=landRGB&0xFF;
|
||||
cm[idx] = (clamp((int)(waterR*wFrac + lr*(1-wFrac))) << 16)
|
||||
| (clamp((int)(waterG*wFrac + lg*(1-wFrac))) << 8)
|
||||
| clamp((int)(waterB*wFrac + lb*(1-wFrac)));
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Sub-Pixel-AA an PlacedWater-Ufern
|
||||
for (PlacedWater w : model.waters()) {
|
||||
int[] wxPx = worldToPixels(w.pointsX(), size);
|
||||
int[] wzPx = worldToPixels(w.pointsZ(), size);
|
||||
Polygon wp = new Polygon(wxPx, wzPx, wxPx.length);
|
||||
Rectangle bb = wp.getBounds();
|
||||
float wh = w.waterHeight();
|
||||
int x0=Math.max(0,bb.x-1), y0=Math.max(0,bb.y-1);
|
||||
int x1=Math.min(size-1,bb.x+bb.width+1), y1=Math.min(size-1,bb.y+bb.height+1);
|
||||
for (int py = y0; py <= y1; py++) {
|
||||
for (int px = x0; px <= x1; px++) {
|
||||
int idx = py * size + px;
|
||||
boolean inPoly = wp.contains(px,py) && heights[idx] < wh;
|
||||
boolean boundary2 = false;
|
||||
for (int dy=-1; dy<=1 && !boundary2; dy++) {
|
||||
for (int dx=-1; dx<=1 && !boundary2; dx++) {
|
||||
int ny=py+dy, nx=px+dx;
|
||||
if (ny<0||ny>=size||nx<0||nx>=size) continue;
|
||||
if ((wp.contains(nx,ny)&&heights[ny*size+nx]<wh) != 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(model.mapData(),size,spx,spy,model.voxelSurface()) < wh)
|
||||
wCount2++;
|
||||
}
|
||||
}
|
||||
float wFrac2 = wCount2 * subInv;
|
||||
int landRGB2 = cm[idx];
|
||||
if (inPoly) {
|
||||
for (int dy=-1; dy<=1; dy++) {
|
||||
for (int dx=-1; dx<=1; dx++) {
|
||||
int ny=py+dy, nx=px+dx;
|
||||
if (ny<0||ny>=size||nx<0||nx>=size) continue;
|
||||
if (!(wp.contains(nx,ny)&&heights[ny*size+nx]<wh)) { landRGB2=cm[ny*size+nx]; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
int lr2=(landRGB2>>16)&0xFF, lg2=(landRGB2>>8)&0xFF, lb2=landRGB2&0xFF;
|
||||
cm[idx] = (clamp((int)(waterR*wFrac2+lr2*(1-wFrac2))) << 16)
|
||||
| (clamp((int)(waterG*wFrac2+lg2*(1-wFrac2))) << 8)
|
||||
| clamp((int)(waterB*wFrac2+lb2*(1-wFrac2)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Haupt-Layer-Renderer (Terrain + alle Overlay-Layer) ───────────────────
|
||||
|
||||
/**
|
||||
* Rendert alle Layer (Terrain, Wasser, Wald-Füllung, Kliff-Füllung,
|
||||
* Küstenlinie, Kliff-Schraffierung + Kontur, Wald-Kontur, Wege)
|
||||
* identisch zum Editor-Canvas, aber via Java2D.
|
||||
*/
|
||||
/**
|
||||
* Rendert den Minimap-Hintergrund (volle Welt): Terrain + Wasser + Wald-Füllung + Kliff-Füllung
|
||||
* + alle Vektorlinien (Küste, Klippen, Wald, Wege) in Weltkarten-Auflösung.
|
||||
*/
|
||||
public static BufferedImage renderAllLayers(WorldMapRenderModel model, MapOverlayData overlay, int size) {
|
||||
RenderOptions bgOpts = new RenderOptions(true, true, true, false, false, false, false, false, false);
|
||||
BufferedImage bi = render(model, size, bgOpts);
|
||||
Graphics2D g2 = bi.createGraphics();
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
|
||||
|
||||
// Wald-Füllung (semi-transparent ARGB)
|
||||
int[] forestPx = computeForestPixels(overlay.forestDensity(), 3.0f, size);
|
||||
BufferedImage forestImg = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
|
||||
forestImg.setRGB(0, 0, size, size, forestPx, 0, size);
|
||||
g2.drawImage(forestImg, 0, 0, null);
|
||||
|
||||
// Kliff-Füllung (ARGB bei COLOR_MAP_SIZE, skaliert auf size)
|
||||
BufferedImage cliffFill = new BufferedImage(COLOR_MAP_SIZE, COLOR_MAP_SIZE, BufferedImage.TYPE_INT_ARGB);
|
||||
cliffFill.setRGB(0, 0, COLOR_MAP_SIZE, COLOR_MAP_SIZE, overlay.cliffPx(), 0, COLOR_MAP_SIZE);
|
||||
g2.drawImage(cliffFill, 0, 0, size, size, null);
|
||||
|
||||
// Vektorlinien in Weltkarten-Koordinaten (Linienbreite in Textur-Pixeln)
|
||||
float lw = Math.max(1f, size / 1024f);
|
||||
drawVectorLayers(g2, overlay, lw,
|
||||
wx -> worldToImgX((float) wx, size),
|
||||
wz -> worldToImgY((float) wz, size));
|
||||
|
||||
g2.dispose();
|
||||
return bi;
|
||||
}
|
||||
|
||||
/** Convenience-Overload: berechnet overlay intern. */
|
||||
public static BufferedImage renderAllLayers(WorldMapRenderModel model, int size) {
|
||||
return renderAllLayers(model, computeOverlayData(model), size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert den sichtbaren Weltausschnitt vollständig: Terrain, Wasser, Wald-Füllung,
|
||||
* Kliff-Füllung und alle Vektor-Linien. Linienbreiten sind konstant in Textur-Pixeln
|
||||
* → immer scharf, egal bei welchem Zoom-Level.
|
||||
*
|
||||
* @param overlay vorberechnete Overlay-Daten
|
||||
* @param size Ausgabegröße (Pixel)
|
||||
* @param cx Weltmittelpunkt X
|
||||
* @param cz Weltmittelpunkt Z
|
||||
* @param viewRadius halber Weltausschnitt (cx ± viewRadius sichtbar)
|
||||
*/
|
||||
public static BufferedImage renderFullRegion(MapOverlayData overlay,
|
||||
int size, float cx, float cz, float viewRadius) {
|
||||
// colorMap enthält bereits Terrain + Wasser mit antialiased Küstenlinie (aus computeOverlayData)
|
||||
int[] colorMap = overlay.colorMap();
|
||||
int[] cliffPx = overlay.cliffPx();
|
||||
float[] density = overlay.forestDensity();
|
||||
|
||||
// ── Terrain + Wasser: bilinear aus vorberechnetem colorMap ────────
|
||||
int[] pixels = new int[size * size];
|
||||
for (int py = 0; py < size; py++) {
|
||||
float wz = cz - viewRadius + (float) py / (size - 1) * 2f * viewRadius;
|
||||
float fz = Math.max(0f, Math.min(COLOR_MAP_SIZE - 1.001f,
|
||||
(wz + WORLD_HALF) / WORLD_SIZE * (COLOR_MAP_SIZE - 1)));
|
||||
int cz0 = (int) fz, cz1 = Math.min(cz0+1, COLOR_MAP_SIZE-1);
|
||||
float tz = fz - cz0;
|
||||
for (int px = 0; px < size; px++) {
|
||||
float wx = cx - viewRadius + (float) px / (size - 1) * 2f * viewRadius;
|
||||
float fx = Math.max(0f, Math.min(COLOR_MAP_SIZE - 1.001f,
|
||||
(wx + WORLD_HALF) / WORLD_SIZE * (COLOR_MAP_SIZE - 1)));
|
||||
int cx0 = (int) fx, cx1 = Math.min(cx0+1, COLOR_MAP_SIZE-1);
|
||||
float tx = fx - cx0;
|
||||
int c00 = colorMap[cz0*COLOR_MAP_SIZE+cx0], c10 = colorMap[cz0*COLOR_MAP_SIZE+cx1];
|
||||
int c01 = colorMap[cz1*COLOR_MAP_SIZE+cx0], c11 = colorMap[cz1*COLOR_MAP_SIZE+cx1];
|
||||
int r = clamp((int)(((c00>>16)&0xFF)*(1-tx)*(1-tz)+((c10>>16)&0xFF)*tx*(1-tz)+((c01>>16)&0xFF)*(1-tx)*tz+((c11>>16)&0xFF)*tx*tz));
|
||||
int g = clamp((int)(((c00>>8)&0xFF)*(1-tx)*(1-tz)+((c10>>8)&0xFF)*tx*(1-tz)+((c01>>8)&0xFF)*(1-tx)*tz+((c11>>8)&0xFF)*tx*tz));
|
||||
int b = clamp((int)((c00&0xFF)*(1-tx)*(1-tz)+(c10&0xFF)*tx*(1-tz)+(c01&0xFF)*(1-tx)*tz+(c11&0xFF)*tx*tz));
|
||||
pixels[py * size + px] = (r << 16) | (g << 8) | b;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Wald-Füllung + Kliff-Füllung (alpha-compositing direkt in pixels) ──
|
||||
final int FD = FOREST_DENSITY_SIZE;
|
||||
for (int py = 0; py < size; py++) {
|
||||
float wz = cz - viewRadius + (float) py / (size - 1) * 2f * viewRadius;
|
||||
float fsfz = Math.max(0f, Math.min(FD - 1.001f, (WORLD_HALF - wz) / WORLD_SIZE * FD));
|
||||
int fsz0 = (int) fsfz, fsz1 = Math.min(fsz0+1, FD-1);
|
||||
float fstz = fsfz - fsz0;
|
||||
float cfz = Math.max(0f, Math.min(COLOR_MAP_SIZE - 1.001f,
|
||||
(wz + WORLD_HALF) / WORLD_SIZE * (COLOR_MAP_SIZE - 1)));
|
||||
int cfz0 = (int) cfz;
|
||||
for (int px = 0; px < size; px++) {
|
||||
float wx = cx - viewRadius + (float) px / (size - 1) * 2f * viewRadius;
|
||||
|
||||
// Wald
|
||||
float fsfx = Math.max(0f, Math.min(FD - 1.001f, (wx + WORLD_HALF) / WORLD_SIZE * FD));
|
||||
int fsx0 = (int) fsfx, fsx1 = Math.min(fsx0+1, FD-1);
|
||||
float fstx = fsfx - fsx0;
|
||||
float d = (density[fsz0*FD+fsx0]*(1-fstx)+density[fsz0*FD+fsx1]*fstx)*(1-fstz)
|
||||
+ (density[fsz1*FD+fsx0]*(1-fstx)+density[fsz1*FD+fsx1]*fstx)*fstz;
|
||||
if (d > 3.0f) pixels[py*size+px] = alphaBlendRGB(pixels[py*size+px], 0x80_4a9a28);
|
||||
|
||||
// Kliff
|
||||
float cfx = Math.max(0f, Math.min(COLOR_MAP_SIZE - 1.001f,
|
||||
(wx + WORLD_HALF) / WORLD_SIZE * (COLOR_MAP_SIZE - 1)));
|
||||
int cliff = cliffPx[cfz0 * COLOR_MAP_SIZE + (int) cfx];
|
||||
if ((cliff >>> 24) > 10) pixels[py*size+px] = alphaBlendRGB(pixels[py*size+px], cliff);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Vektor-Linien ─────────────────────────────────────────────────
|
||||
BufferedImage bi = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB);
|
||||
bi.setRGB(0, 0, size, size, pixels, 0, size);
|
||||
Graphics2D g2 = bi.createGraphics();
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
|
||||
drawVectorLayers(g2, overlay, 2.0f,
|
||||
wx -> regionToImgX((float) wx, cx, viewRadius, size),
|
||||
wz -> regionToImgY((float) wz, cz, viewRadius, size));
|
||||
g2.dispose();
|
||||
return bi;
|
||||
}
|
||||
|
||||
private static int alphaBlendRGB(int dstRGB, int srcARGB) {
|
||||
float a = ((srcARGB >>> 24) & 0xFF) / 255f;
|
||||
int sr = (srcARGB >> 16) & 0xFF, sg = (srcARGB >> 8) & 0xFF, sb = srcARGB & 0xFF;
|
||||
int dr = (dstRGB >> 16) & 0xFF, dg = (dstRGB >> 8) & 0xFF, db = dstRGB & 0xFF;
|
||||
return (clamp((int)(dr + (sr - dr) * a)) << 16)
|
||||
| (clamp((int)(dg + (sg - dg) * a)) << 8)
|
||||
| clamp((int)(db + (sb - db) * a));
|
||||
}
|
||||
|
||||
/** Zeichnet alle Vektor-Layer über g2; xFn/zFn konvertieren Weltkoord → Bildpixel. */
|
||||
private static void drawVectorLayers(Graphics2D g2, MapOverlayData overlay, float lw,
|
||||
java.util.function.DoubleUnaryOperator xFn,
|
||||
java.util.function.DoubleUnaryOperator zFn) {
|
||||
// Küstenpfade
|
||||
g2.setColor(new Color(30, 80, 145, 200));
|
||||
g2.setStroke(new java.awt.BasicStroke(lw * 1.2f, java.awt.BasicStroke.CAP_ROUND, java.awt.BasicStroke.JOIN_ROUND));
|
||||
for (float[][] path : overlay.coastPaths()) drawMappedPath(g2, path, xFn, zFn, true);
|
||||
|
||||
// Kliff-Schraffierung + Konturlinie
|
||||
if (!overlay.cliffPaths().isEmpty()) {
|
||||
Graphics2D g2c = (Graphics2D) g2.create();
|
||||
Path2D clipArea = new Path2D.Float(Path2D.WIND_NON_ZERO);
|
||||
for (float[][] path : overlay.cliffPaths()) {
|
||||
if (path.length < 2) continue;
|
||||
clipArea.moveTo(xFn.applyAsDouble(path[0][0]), zFn.applyAsDouble(path[0][1]));
|
||||
for (int i = 1; i < path.length; i++) {
|
||||
clipArea.lineTo(xFn.applyAsDouble(path[i][0]), zFn.applyAsDouble(path[i][1]));
|
||||
}
|
||||
clipArea.closePath();
|
||||
}
|
||||
g2c.clip(clipArea);
|
||||
g2c.setColor(new Color(120, 115, 115, 80));
|
||||
g2c.setStroke(new java.awt.BasicStroke(lw));
|
||||
float hatch = lw * 4f;
|
||||
int imgSize = g2c.getClipBounds() != null ? g2c.getClipBounds().width : 2048;
|
||||
for (float d = -imgSize; d < imgSize * 2f; d += hatch) {
|
||||
g2c.drawLine((int)d, 0, (int)(d + imgSize), imgSize);
|
||||
}
|
||||
g2c.dispose();
|
||||
g2.setColor(new Color(80, 72, 72, 200));
|
||||
g2.setStroke(new java.awt.BasicStroke(lw, java.awt.BasicStroke.CAP_ROUND, java.awt.BasicStroke.JOIN_ROUND));
|
||||
for (float[][] path : overlay.cliffPaths()) drawMappedPath(g2, path, xFn, zFn, true);
|
||||
}
|
||||
|
||||
// Wald-Konturlinie
|
||||
g2.setColor(new Color(28, 78, 12, 220));
|
||||
g2.setStroke(new java.awt.BasicStroke(lw, java.awt.BasicStroke.CAP_ROUND, java.awt.BasicStroke.JOIN_ROUND));
|
||||
for (float[][] path : overlay.forestPaths()) drawMappedPath(g2, path, xFn, zFn, true);
|
||||
|
||||
// Wege: dunkle Außenlinie dann helle Innenlinie
|
||||
if (!overlay.pathChains().isEmpty()) {
|
||||
g2.setColor(new Color(20, 12, 5, 208));
|
||||
g2.setStroke(new java.awt.BasicStroke(lw * 2.5f + 1f, java.awt.BasicStroke.CAP_ROUND, java.awt.BasicStroke.JOIN_ROUND));
|
||||
for (float[][] chain : overlay.pathChains()) drawMappedChain(g2, chain, xFn, zFn);
|
||||
g2.setColor(new Color(148, 105, 55, 234));
|
||||
g2.setStroke(new java.awt.BasicStroke(lw * 2.5f, java.awt.BasicStroke.CAP_ROUND, java.awt.BasicStroke.JOIN_ROUND));
|
||||
for (float[][] chain : overlay.pathChains()) drawMappedChain(g2, chain, xFn, zFn);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Koordinaten-Helfer ────────────────────────────────────────────────────
|
||||
|
||||
private static double worldToImgX(float wx, int size) {
|
||||
return (wx + WORLD_HALF) / WORLD_SIZE * (size - 1);
|
||||
}
|
||||
|
||||
private static double worldToImgY(float wz, int size) {
|
||||
return (wz + WORLD_HALF) / WORLD_SIZE * (size - 1);
|
||||
}
|
||||
|
||||
private static double regionToImgX(float wx, float cx, float vr, int size) {
|
||||
return (wx - cx + vr) / (2f * vr) * (size - 1);
|
||||
}
|
||||
|
||||
private static double regionToImgY(float wz, float cz, float vr, int size) {
|
||||
return (wz - cz + vr) / (2f * vr) * (size - 1);
|
||||
}
|
||||
|
||||
// ── Pfad-Zeichnen mit generischen Koordinaten-Funktionen ─────────────────
|
||||
|
||||
private static void drawMappedPath(Graphics2D g2, float[][] path,
|
||||
java.util.function.DoubleUnaryOperator xFn,
|
||||
java.util.function.DoubleUnaryOperator zFn,
|
||||
boolean close) {
|
||||
if (path.length < 2) return;
|
||||
Path2D p = new Path2D.Float();
|
||||
p.moveTo(xFn.applyAsDouble(path[0][0]), zFn.applyAsDouble(path[0][1]));
|
||||
for (int i = 1; i < path.length; i++) {
|
||||
p.lineTo(xFn.applyAsDouble(path[i][0]), zFn.applyAsDouble(path[i][1]));
|
||||
}
|
||||
if (close) p.closePath();
|
||||
g2.draw(p);
|
||||
}
|
||||
|
||||
private static void drawMappedChain(Graphics2D g2, float[][] chain,
|
||||
java.util.function.DoubleUnaryOperator xFn,
|
||||
java.util.function.DoubleUnaryOperator zFn) {
|
||||
if (chain.length < 2) return;
|
||||
final float R = 8f;
|
||||
Path2D p = new Path2D.Float();
|
||||
p.moveTo(xFn.applyAsDouble(chain[0][0]), zFn.applyAsDouble(chain[0][1]));
|
||||
for (int i = 1; i < chain.length - 1; i++) {
|
||||
float px = chain[i-1][0], pz = chain[i-1][1];
|
||||
float cx = chain[i][0], cz = chain[i][1];
|
||||
float nx = chain[i+1][0], nz = chain[i+1][1];
|
||||
float d1x = cx-px, d1z = cz-pz, d2x = nx-cx, d2z = nz-cz;
|
||||
float len1 = (float)Math.sqrt(d1x*d1x + d1z*d1z);
|
||||
float len2 = (float)Math.sqrt(d2x*d2x + d2z*d2z);
|
||||
if (len1 < 0.01f || len2 < 0.01f) {
|
||||
p.lineTo(xFn.applyAsDouble(cx), zFn.applyAsDouble(cz));
|
||||
continue;
|
||||
}
|
||||
float r = Math.min(R, Math.min(len1*0.4f, len2*0.4f));
|
||||
float q1x = cx - (d1x/len1)*r, q1z = cz - (d1z/len1)*r;
|
||||
float q2x = cx + (d2x/len2)*r, q2z = cz + (d2z/len2)*r;
|
||||
p.lineTo(xFn.applyAsDouble(q1x), zFn.applyAsDouble(q1z));
|
||||
p.quadTo(xFn.applyAsDouble(cx), zFn.applyAsDouble(cz),
|
||||
xFn.applyAsDouble(q2x), zFn.applyAsDouble(q2z));
|
||||
}
|
||||
p.lineTo(xFn.applyAsDouble(chain[chain.length-1][0]), zFn.applyAsDouble(chain[chain.length-1][1]));
|
||||
g2.draw(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kuwahara-Filter: Für jeden Pixel das Quadranten-Fenster mit der kleinsten
|
||||
* Varianz wählen und dessen Mittelwert setzen → Ölgemälde-/gemalt-Effekt.
|
||||
|
||||
Reference in New Issue
Block a user