Minimap und ingame Karte - umsetzung begonnen
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user