Minimap und ingame Karte - umsetzung begonnen

This commit is contained in:
2026-08-23 22:13:32 +02:00
parent 46b504e050
commit 9238378bc8
16 changed files with 2119 additions and 8 deletions

View File

@@ -0,0 +1,609 @@
package de.blight.common.map;
import de.blight.common.*;
import de.blight.common.model.Location;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
/**
* Rendert eine 2D-Draufsicht der Spielwelt als {@link BufferedImage}.
*
* Koordinaten-Konvention:
* pixelX=0 / pixelY=0 → worldX=-1024 / worldZ=-1024
* pixelX=size-1 → worldX=+1024
* pixelY=size-1 → worldZ=+1024
*/
public final class WorldMapRenderer {
public static final float WORLD_HALF = 1024f;
public static final float WORLD_SIZE = 2048f;
public record RenderInput(
MapData mapData,
List<PlacedArea> areas,
List<PlacedLocationZone> zones,
List<Location> locations,
List<PlacedWater> waters,
List<PlacedModel> models,
int[] slotColorsRGB // 12 Werte: [R0,G0,B0, R1,G1,B1, R2,G2,B2, R3,G3,B3], null = Defaults
) {
public RenderInput(MapData mapData, List<PlacedArea> areas, List<PlacedLocationZone> zones,
List<Location> locations, List<PlacedWater> waters, List<PlacedModel> models) {
this(mapData, areas, zones, locations, waters, models, null);
}
}
public record RenderOptions(
boolean showTerrain,
boolean showSplatColors,
boolean showWater,
boolean showAreas,
boolean showZones,
boolean showLocations,
boolean showModels
) {
public static RenderOptions all() {
return new RenderOptions(true, true, true, true, true, true, true);
}
}
// Default-Slot-Farben für Slots 1-8 (Base-Layer 1-4 + Upper-Layer 5-8)
private static final int[] DEF_SLOT_R = { 71, 115, 140, 204, 90, 130, 110, 180 };
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 };
private WorldMapRenderer() {}
public static BufferedImage render(RenderInput input, int targetSize, RenderOptions opts) {
MapData m = input.mapData();
int TV = MapData.TERRAIN_VERTS;
int SS = MapData.SPLAT_SIZE;
int[] slotR = slotChannel(input, 0);
int[] slotG = slotChannel(input, 1);
int[] slotB = slotChannel(input, 2);
// ── 1. Heightmap auf Zielauflösung samplen ────────────────────────────
float[] heights = new float[targetSize * targetSize];
float minH = Float.MAX_VALUE, maxH = -Float.MAX_VALUE;
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 h = m.terrainHeight[hz * TV + hx];
heights[py * targetSize + px] = h;
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);
for (int py = 0; py < targetSize; py++) {
for (int px = 0; px < targetSize; px++) {
float h = heights[py * targetSize + px];
float hn = (h - minH) / heightRange;
int r, g, b;
if (opts.showSplatColors()) {
int sx = Math.min((int)((float) px / (targetSize - 1) * (SS - 1)), SS - 1);
int sz = Math.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);
} else {
int lum = clamp(30 + (int)(hn * 200));
r = g = b = lum;
}
// 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);
}
}
// ── 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()) {
final int WATER_RGB = (255 << 24) | (35 << 16) | (100 << 8) | 190;
// Meer: Terrain unter Meeresspiegel
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_RGB);
}
}
}
// Wasserflächen: nur dort sichtbar, wo Terrain unter Wasseroberfläche liegt
for (PlacedWater w : input.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);
int 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_RGB);
}
}
}
}
}
// Area-Polygone
if (opts.showAreas()) {
gfx.setStroke(new BasicStroke(lineW));
for (PlacedArea a : input.areas()) {
Color c = hashColor(a.areaId());
int[] xs = worldToPixels(a.pointsX(), targetSize);
int[] ys = worldToPixels(a.pointsZ(), targetSize);
gfx.setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), 55));
gfx.fillPolygon(xs, ys, xs.length);
gfx.setColor(c);
gfx.drawPolygon(xs, ys, xs.length);
}
}
// Location-Zonen
if (opts.showZones()) {
gfx.setStroke(new BasicStroke(lineW));
for (PlacedLocationZone z : input.zones()) {
int[] xs = worldToPixels(z.pointsX(), targetSize);
int[] ys = worldToPixels(z.pointsZ(), targetSize);
gfx.setColor(new Color(240, 190, 40, 70));
gfx.fillPolygon(xs, ys, xs.length);
gfx.setColor(new Color(200, 150, 20));
gfx.drawPolygon(xs, ys, xs.length);
}
}
// Modell-Punkte
if (opts.showModels()) {
int dotR = Math.max(1, targetSize / 600);
gfx.setColor(new Color(160, 80, 20, 200));
for (PlacedModel model : input.models()) {
int mx = worldToPixel(model.x(), targetSize);
int mz = worldToPixel(model.z(), targetSize);
gfx.fillRect(mx - dotR, mz - dotR, dotR * 2 + 1, dotR * 2 + 1);
}
}
// Locations (Kreise + Labels)
if (opts.showLocations()) {
gfx.setStroke(new BasicStroke(lineW));
int fontSize = Math.max(8, targetSize / 140);
gfx.setFont(new Font("SansSerif", Font.BOLD, fontSize));
for (Location loc : input.locations()) {
int lx = worldToPixel(loc.getCenterX(), targetSize);
int lz = worldToPixel(loc.getCenterZ(), targetSize);
int lr = Math.max(4, (int)(loc.getRadius() / WORLD_SIZE * targetSize));
gfx.setColor(new Color(255, 255, 200, 50));
gfx.fillOval(lx - lr, lz - lr, lr * 2, lr * 2);
gfx.setColor(new Color(255, 220, 60));
gfx.drawOval(lx - lr, lz - lr, lr * 2, lr * 2);
if (targetSize >= 512 && loc.getId() != null && !loc.getId().isEmpty()) {
String label = friendlyLocationName(loc.getId());
FontMetrics fm = gfx.getFontMetrics();
int tw = fm.stringWidth(label);
gfx.setColor(new Color(0, 0, 0, 160));
gfx.drawString(label, lx - tw / 2 + 1, lz - lr - 3);
gfx.setColor(Color.WHITE);
gfx.drawString(label, lx - tw / 2, lz - lr - 4);
}
}
}
// Spawnpunkt
int spx = worldToPixel(m.spawnX, targetSize);
int spz = worldToPixel(m.spawnZ, targetSize);
int cr = Math.max(5, targetSize / 180);
gfx.setStroke(new BasicStroke(Math.max(2f, targetSize / 300f)));
gfx.setColor(new Color(0, 0, 0, 120));
gfx.drawLine(spx - cr + 1, spz + 1, spx + cr + 1, spz + 1);
gfx.drawLine(spx + 1, spz - cr + 1, spx + 1, spz + cr + 1);
gfx.setColor(new Color(60, 230, 90));
gfx.drawLine(spx - cr, spz, spx + cr, spz);
gfx.drawLine(spx, spz - cr, spx, spz + cr);
gfx.dispose();
return img;
}
/**
* Rendert einen rechteckigen Weltausschnitt als {@link BufferedImage}.
* Koordinatenursprung und Skalierung passen sich dem Ausschnitt an,
* sodass die Ausgabe immer {@code targetSize × targetSize} Pixel scharf gezeichnet wird.
*
* Wenn {@code opts.showTerrain() == false}, ist der Hintergrund transparent (ARGB).
* Das ist nützlich für eine Vektor-Overlay-Textur über einem separaten Terrain-Layer.
*
* @param worldCenterX Weltkoordinate X des Mittelpunkts
* @param worldCenterZ Weltkoordinate Z des Mittelpunkts
* @param halfExtent Halbausdehnung in Weltmetern (sichtbarer Radius)
*/
public static BufferedImage renderRegion(RenderInput input, int targetSize, RenderOptions opts,
float worldCenterX, float worldCenterZ, float halfExtent) {
halfExtent = Math.max(1f, halfExtent);
float wx0 = worldCenterX - halfExtent;
float wz0 = worldCenterZ - halfExtent;
float rSize = halfExtent * 2f;
int imageType = opts.showTerrain() ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
BufferedImage img = new BufferedImage(targetSize, targetSize, imageType);
// ── Höhen samplen (für Terrain-Render und Wasser-Clipping) ──────────
MapData m = input.mapData();
int TV = MapData.TERRAIN_VERTS;
float[] heights = null;
float minH = 0f, maxH = 1f;
if (opts.showTerrain() || opts.showWater()) {
heights = new float[targetSize * targetSize];
minH = Float.MAX_VALUE;
maxH = -Float.MAX_VALUE;
for (int py = 0; py < targetSize; py++) {
float wz = wz0 + (float) py / (targetSize - 1) * rSize;
int hz = iclamp((int) ((wz + WORLD_HALF) / WORLD_SIZE * (TV - 1)), 0, TV - 1);
for (int px = 0; px < targetSize; px++) {
float wx = wx0 + (float) px / (targetSize - 1) * rSize;
int hx = iclamp((int) ((wx + WORLD_HALF) / WORLD_SIZE * (TV - 1)), 0, TV - 1);
float h = m.terrainHeight[hz * TV + hx];
heights[py * targetSize + px] = h;
if (h < minH) { minH = h; }
if (h > maxH) { maxH = h; }
}
}
}
// ── Terrain (optional) ────────────────────────────────────────────────
if (opts.showTerrain()) {
float heightRange = Math.max(0.01f, maxH - minH);
int[] sR = slotChannel(input, 0);
int[] sG = slotChannel(input, 1);
int[] sB = slotChannel(input, 2);
for (int py = 0; py < targetSize; py++) {
for (int px = 0; px < targetSize; px++) {
float h = heights[py * targetSize + px];
float hn = (h - minH) / heightRange;
int r, g, b;
if (opts.showSplatColors()) {
float wx = wx0 + (float) px / (targetSize - 1) * rSize;
float wz = wz0 + (float) py / (targetSize - 1) * rSize;
int SS2 = MapData.SPLAT_SIZE;
float sfx = Math.max(0f, Math.min(SS2 - 1, (wx + WORLD_HALF) / WORLD_SIZE * (SS2 - 1)));
float sfz = Math.max(0f, Math.min(SS2 - 1, (WORLD_HALF - wz) / WORLD_SIZE * (SS2 - 1)));
int sx0 = (int) sfx; int sx1 = Math.min(sx0 + 1, SS2 - 1);
int sz0 = (int) sfz; int sz1 = Math.min(sz0 + 1, SS2 - 1);
float tx = sfx - sx0, tz = sfz - sz0;
int i00 = sz0 * SS2 + sx0, i10 = sz0 * SS2 + sx1;
int i01 = sz1 * SS2 + sx0, i11 = sz1 * SS2 + sx1;
// Base layer (Slots 1-4) bilinear interpolated weights
float wg = bilerp(m.splatG, i00, i10, i01, i11, tx, tz);
float wb = bilerp(m.splatB, i00, i10, i01, i11, tx, tz);
float wa = bilerp(m.splatA, i00, i10, i01, i11, tx, tz);
float fr = sR[0], fg = sG[0], fb = sB[0];
fr = fr*(1-wg) + sR[1]*wg; fg = fg*(1-wg) + sG[1]*wg; fb = fb*(1-wg) + sB[1]*wg;
fr = fr*(1-wb) + sR[2]*wb; fg = fg*(1-wb) + sG[2]*wb; fb = fb*(1-wb) + sB[2]*wb;
fr = fr*(1-wa) + sR[3]*wa; fg = fg*(1-wa) + sG[3]*wa; fb = fb*(1-wa) + sB[3]*wa;
// Upper layer (Slots 5-8) bilinear interpolated weights
float u1 = bilerp(m.upperSplatR, i00, i10, i01, i11, tx, tz);
float u2 = bilerp(m.upperSplatG, i00, i10, i01, i11, tx, tz);
float u3 = bilerp(m.upperSplatB, i00, i10, i01, i11, tx, tz);
float u4 = bilerp(m.upperSplatA, i00, i10, i01, i11, tx, tz);
fr = fr*(1-u1) + sR[4]*u1; fg = fg*(1-u1) + sG[4]*u1; fb = fb*(1-u1) + sB[4]*u1;
fr = fr*(1-u2) + sR[5]*u2; fg = fg*(1-u2) + sG[5]*u2; fb = fb*(1-u2) + sB[5]*u2;
fr = fr*(1-u3) + sR[6]*u3; fg = fg*(1-u3) + sG[6]*u3; fb = fb*(1-u3) + sB[6]*u3;
fr = fr*(1-u4) + sR[7]*u4; fg = fg*(1-u4) + sG[7]*u4; fb = fb*(1-u4) + sB[7]*u4;
r = clamp((int) fr); g = clamp((int) fg); b = clamp((int) fb);
} else {
int lum = clamp(30 + (int) (hn * 200));
r = g = b = lum;
}
if (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 = Math.max(0.2f, Math.min(1.8f,
0.45f + (nx * (-0.577f) + ny * 0.577f + nz * (-0.577f)) * 0.85f));
r = clamp((int) (r * shade));
g = clamp((int) (g * shade));
b = clamp((int) (b * shade));
}
img.setRGB(px, py, (r << 16) | (g << 8) | b);
}
}
}
// ── Vektor-Overlays ───────────────────────────────────────────────────
Graphics2D gfx = img.createGraphics();
gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
float lineW = Math.max(1.5f, targetSize / 400f);
if (opts.showWater()) {
final int WATER_RGB = (255 << 24) | (35 << 16) | (100 << 8) | 190;
// Meer: Terrain unter Meeresspiegel
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_RGB);
}
}
}
// Wasserflächen: nur dort sichtbar, wo Terrain unter Wasseroberfläche liegt
for (PlacedWater w : input.waters()) {
int[] xs = wrp(w.pointsX(), wx0, rSize, targetSize);
int[] ys = wrp(w.pointsZ(), wz0, rSize, targetSize);
Polygon poly = new Polygon(xs, ys, xs.length);
Rectangle bb = poly.getBounds();
int x0 = Math.max(0, bb.x);
int 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_RGB);
}
}
}
}
}
if (opts.showAreas()) {
gfx.setStroke(new BasicStroke(lineW));
for (PlacedArea a : input.areas()) {
Color c = hashColor(a.areaId());
int[] xs = wrp(a.pointsX(), wx0, rSize, targetSize);
int[] ys = wrp(a.pointsZ(), wz0, rSize, targetSize);
gfx.setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), 55));
gfx.fillPolygon(xs, ys, xs.length);
gfx.setColor(c);
gfx.drawPolygon(xs, ys, xs.length);
}
}
if (opts.showZones()) {
gfx.setStroke(new BasicStroke(lineW));
for (PlacedLocationZone z : input.zones()) {
int[] xs = wrp(z.pointsX(), wx0, rSize, targetSize);
int[] ys = wrp(z.pointsZ(), wz0, rSize, targetSize);
gfx.setColor(new Color(240, 190, 40, 70));
gfx.fillPolygon(xs, ys, xs.length);
gfx.setColor(new Color(200, 150, 20));
gfx.drawPolygon(xs, ys, xs.length);
}
}
if (opts.showModels()) {
int dotR = Math.max(1, targetSize / 600);
gfx.setColor(new Color(160, 80, 20, 200));
for (PlacedModel model : input.models()) {
int mx = wrp1(model.x(), wx0, rSize, targetSize);
int mz = wrp1(model.z(), wz0, rSize, targetSize);
gfx.fillRect(mx - dotR, mz - dotR, dotR * 2 + 1, dotR * 2 + 1);
}
}
if (opts.showLocations()) {
gfx.setStroke(new BasicStroke(lineW));
int fontSize = Math.max(8, targetSize / 140);
gfx.setFont(new Font("SansSerif", Font.BOLD, fontSize));
for (Location loc : input.locations()) {
int lx = wrp1(loc.getCenterX(), wx0, rSize, targetSize);
int lz = wrp1(loc.getCenterZ(), wz0, rSize, targetSize);
int lr = Math.max(4, (int) (loc.getRadius() / rSize * targetSize));
gfx.setColor(new Color(255, 255, 200, 50));
gfx.fillOval(lx - lr, lz - lr, lr * 2, lr * 2);
gfx.setColor(new Color(255, 220, 60));
gfx.drawOval(lx - lr, lz - lr, lr * 2, lr * 2);
if (loc.getId() != null && !loc.getId().isEmpty()) {
String label = friendlyLocationName(loc.getId());
FontMetrics fm = gfx.getFontMetrics();
int tw = fm.stringWidth(label);
gfx.setColor(new Color(0, 0, 0, 160));
gfx.drawString(label, lx - tw / 2 + 1, lz - lr - 3);
gfx.setColor(Color.WHITE);
gfx.drawString(label, lx - tw / 2, lz - lr - 4);
}
}
}
int spx = wrp1(m.spawnX, wx0, rSize, targetSize);
int spz = wrp1(m.spawnZ, wz0, rSize, targetSize);
int cr = Math.max(5, targetSize / 180);
gfx.setStroke(new BasicStroke(Math.max(2f, targetSize / 300f)));
gfx.setColor(new Color(0, 0, 0, 120));
gfx.drawLine(spx - cr + 1, spz + 1, spx + cr + 1, spz + 1);
gfx.drawLine(spx + 1, spz - cr + 1, spx + 1, spz + cr + 1);
gfx.setColor(new Color(60, 230, 90));
gfx.drawLine(spx - cr, spz, spx + cr, spz);
gfx.drawLine(spx, spz - cr, spx, spz + cr);
gfx.dispose();
return img;
}
// ── Koordinaten-Hilfsmethoden ─────────────────────────────────────────────
public static int worldToPixel(float worldCoord, int targetSize) {
int px = (int)((worldCoord + WORLD_HALF) / WORLD_SIZE * (targetSize - 1));
return Math.max(0, Math.min(targetSize - 1, px));
}
public static float pixelToWorld(double pixel, double canvasSize, double panOffset, double scale) {
return (float)((pixel - panOffset) / scale / canvasSize * WORLD_SIZE - WORLD_HALF);
}
private static int[] worldToPixels(float[] coords, int targetSize) {
int[] px = new int[coords.length];
for (int i = 0; i < coords.length; i++) {
px[i] = worldToPixel(coords[i], targetSize);
}
return px;
}
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
private static int clamp(int v) {
return Math.max(0, Math.min(255, v));
}
// Gibt den R-, G- oder B-Kanal (channel=0/1/2) aller 8 Splatmap-Slots zurück.
// slotColorsRGB: 24 Werte (8 Slots × 3), 12 Werte (4 Slots, Upper-Layer = Defaults) oder null.
private static int[] slotChannel(RenderInput input, int channel) {
int[] rgb = input.slotColorsRGB();
int[] def = channel == 0 ? DEF_SLOT_R : (channel == 1 ? DEF_SLOT_G : DEF_SLOT_B);
if (rgb == null || rgb.length < 12) { return def; }
int[] out = new int[8];
for (int s = 0; s < 8; s++) {
out[s] = (rgb.length >= (s + 1) * 3) ? rgb[s * 3 + channel] : def[s];
}
return out;
}
private static Color hashColor(String key) {
int hash = key == null ? 0 : key.hashCode();
int r = 100 + ((hash & 0xFF0000) >> 16) % 120;
int g = 100 + ((hash & 0x00FF00) >> 8) % 120;
int b = 100 + ((hash & 0x0000FF)) % 120;
return new Color(r, g, b);
}
private static String friendlyLocationName(String id) {
String s = id.replace("location.", "").replace(".name", "");
return s.isEmpty() ? id : s;
}
// Konvertiert eine Weltkoordinate in einen Pixel-Index innerhalb einer Region.
private static int wrp1(float worldCoord, float regionOrigin, float regionSize, int targetSize) {
return (int) ((worldCoord - regionOrigin) / regionSize * (targetSize - 1));
}
private static int[] wrp(float[] coords, float regionOrigin, float regionSize, int targetSize) {
int[] px = new int[coords.length];
for (int i = 0; i < coords.length; i++) {
px[i] = wrp1(coords[i], regionOrigin, regionSize, targetSize);
}
return px;
}
private static int iclamp(int v, int lo, int hi) {
return Math.max(lo, Math.min(hi, v));
}
/**
* Berechnet die Durchschnittsfarben aller 8 Textur-Slots (Base 1-4 + Upper 5-8)
* durch Pixel-Sampling der Texturdateien.
*
* @param mapData Kartendaten (terrainTextures / upperTextures)
* @param assetRoot Wurzelverzeichnis der Assets (enthält Textures/…)
* @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) {
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
};
sampleTexColors(mapData.terrainTextures, baseFallbacks, assetRoot, result, 0);
sampleTexColors(mapData.upperTextures, null, assetRoot, result, 4);
return result;
}
private static void sampleTexColors(String[] textures, String[] fallbacks,
Path assetRoot, int[] result, int slotOffset) {
if (textures == null) { return; }
for (int slot = 0; slot < 4 && slot < textures.length; slot++) {
String rel = textures[slot];
if ((rel == null || rel.isEmpty()) && fallbacks != null && slot < fallbacks.length) {
rel = fallbacks[slot];
}
if (rel == null || rel.isEmpty()) { continue; }
Path texFile = assetRoot.resolve(rel);
if (!Files.exists(texFile)) { continue; }
try {
BufferedImage img = ImageIO.read(texFile.toFile());
if (img == null) { continue; }
long sumR = 0, sumG = 0, sumB = 0, count = 0;
int stride = Math.max(1, img.getWidth() / 32);
for (int y = 0; y < img.getHeight(); y += stride) {
for (int x = 0; x < img.getWidth(); x += stride) {
int rgb = img.getRGB(x, y);
sumR += (rgb >> 16) & 0xFF;
sumG += (rgb >> 8) & 0xFF;
sumB += rgb & 0xFF;
count++;
}
}
if (count > 0) {
int i = (slotOffset + slot) * 3;
result[i] = (int)(sumR / count);
result[i + 1] = (int)(sumG / count);
result[i + 2] = (int)(sumB / count);
}
} catch (Exception ignored) {}
}
}
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;
return (v00*(1-tx) + v10*tx)*(1-tz) + (v01*(1-tx) + v11*tx)*tz;
}
}