mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-20 07:24:16 +00:00
Fit the height colours to the terrain on screen
The height view spread its colours over the whole world height, so a mountain range and the plains next to it got nearly the same colour. The legend now measures the visible heights (1st to 99th percentile, rounded to 8 blocks) and fits the gradient to that range, with sea level kept as the water/land split. The map also re-finds its own world after a reload instead of whichever Iris world is active, now that the active world prefers the open studio. Model used: Claude Opus 5.5
This commit is contained in:
@@ -25,7 +25,6 @@ import com.volmit.iris.core.gui.components.MapStyle;
|
||||
import com.volmit.iris.core.gui.components.RenderType;
|
||||
import com.volmit.iris.engine.framework.Engine;
|
||||
import com.volmit.iris.engine.object.IrisBiome;
|
||||
import com.volmit.iris.engine.object.IrisDimension;
|
||||
import com.volmit.iris.util.format.Form;
|
||||
|
||||
import javax.swing.JMenuItem;
|
||||
@@ -77,6 +76,7 @@ final class MapCanvas extends JPanel {
|
||||
private static final double KEYBOARD_ZOOM_FACTOR = 1.189207115002721D;
|
||||
private static final int LEGEND_REFRESH_MILLIS = 200;
|
||||
|
||||
private final UUID worldId;
|
||||
private final UUID openerId;
|
||||
private final Listener listener;
|
||||
private final VisionRenderController controller;
|
||||
@@ -108,8 +108,6 @@ final class MapCanvas extends JPanel {
|
||||
private boolean draggingDivider;
|
||||
private double centerX;
|
||||
private double centerZ;
|
||||
private double heightMaximum;
|
||||
private double fluidHeight;
|
||||
private long contentRevision = 1L;
|
||||
private long previousPaintStartedNanos;
|
||||
private double blocksPerPixel = DEFAULT_BLOCKS_PER_PIXEL;
|
||||
@@ -123,15 +121,15 @@ final class MapCanvas extends JPanel {
|
||||
private boolean active = true;
|
||||
private boolean closed;
|
||||
|
||||
MapCanvas(Engine engine, UUID openerId, Listener listener) {
|
||||
MapCanvas(Engine engine, UUID worldId, UUID openerId, Listener listener) {
|
||||
this.engine = Objects.requireNonNull(engine, "engine");
|
||||
this.worldId = Objects.requireNonNull(worldId, "worldId");
|
||||
this.openerId = openerId;
|
||||
this.listener = Objects.requireNonNull(listener, "listener");
|
||||
this.renderer = new IrisRenderer(engine);
|
||||
this.overlay = new VisionOverlay(engine, openerId);
|
||||
this.controller = new VisionRenderController(this::onTilesChanged);
|
||||
this.hotloadHook = () -> EventQueue.invokeLater(this::refreshContent);
|
||||
captureHeightRange();
|
||||
|
||||
setBackground(StudioTheme.EDITOR_BACKGROUND);
|
||||
setFocusable(true);
|
||||
@@ -214,6 +212,9 @@ final class MapCanvas extends JPanel {
|
||||
return;
|
||||
}
|
||||
currentType = view.renderType();
|
||||
if (currentType != RenderType.HEIGHT) {
|
||||
style = style.withHeightRange(null);
|
||||
}
|
||||
requestRender();
|
||||
}
|
||||
|
||||
@@ -241,7 +242,6 @@ final class MapCanvas extends JPanel {
|
||||
renderer = new IrisRenderer(engine);
|
||||
overlay = new VisionOverlay(engine, openerId);
|
||||
contentRevision++;
|
||||
captureHeightRange();
|
||||
requestRender();
|
||||
notifyUser(captured && comparison.mode() == HotloadComparison.Mode.OFF
|
||||
? "Refreshing · press C to compare with before"
|
||||
@@ -796,6 +796,7 @@ final class MapCanvas extends JPanel {
|
||||
if (currentType != RenderType.HEIGHT) {
|
||||
return;
|
||||
}
|
||||
MapStyle.HeightRange range = currentHeightRange();
|
||||
canvas.setFont(StudioTheme.smallFont());
|
||||
int padding = StudioTheme.scale(CARD_PADDING);
|
||||
int gradientHeight = StudioTheme.scale(14);
|
||||
@@ -808,23 +809,24 @@ final class MapCanvas extends JPanel {
|
||||
int gradientX = x + padding;
|
||||
int gradientY = y + padding;
|
||||
int gradientWidth = width - padding * 2;
|
||||
double span = Math.max(1D, range.highest() - range.lowest());
|
||||
for (int pixel = 0; pixel < gradientWidth; pixel++) {
|
||||
double sampleHeight = heightMaximum * pixel / Math.max(1D, gradientWidth - 1D);
|
||||
canvas.setColor(new Color(IrisRenderer.heightColor(sampleHeight, heightMaximum, fluidHeight)));
|
||||
double sampleHeight = range.lowest() + span * pixel / Math.max(1D, gradientWidth - 1D);
|
||||
canvas.setColor(new Color(IrisRenderer.heightColor(sampleHeight, range)));
|
||||
canvas.drawLine(gradientX + pixel, gradientY, gradientX + pixel, gradientY + gradientHeight);
|
||||
}
|
||||
canvas.setColor(StudioTheme.SECONDARY_TEXT);
|
||||
int labelBaseline = gradientY + gradientHeight + labelGap + canvas.getFontMetrics().getAscent();
|
||||
String minimum = "0";
|
||||
String fluid = Integer.toString((int) Math.round(fluidHeight));
|
||||
String maximum = Integer.toString((int) Math.round(heightMaximum));
|
||||
String minimum = Integer.toString((int) Math.round(range.lowest()));
|
||||
String sea = "sea " + Math.round(range.seaLevel());
|
||||
String maximum = Integer.toString((int) Math.round(range.highest()));
|
||||
int minimumRight = gradientX + canvas.getFontMetrics().stringWidth(minimum);
|
||||
int maximumX = gradientX + gradientWidth - canvas.getFontMetrics().stringWidth(maximum);
|
||||
int fluidWidth = canvas.getFontMetrics().stringWidth(fluid);
|
||||
int fluidX = gradientX + (int) Math.round(gradientWidth * fluidHeight / Math.max(1D, heightMaximum)) - fluidWidth / 2;
|
||||
int seaWidth = canvas.getFontMetrics().stringWidth(sea);
|
||||
int seaX = gradientX + (int) Math.round(gradientWidth * (range.seaLevel() - range.lowest()) / span) - seaWidth / 2;
|
||||
canvas.drawString(minimum, gradientX, labelBaseline);
|
||||
if (fluidX > minimumRight + labelGap && fluidX + fluidWidth < maximumX - labelGap) {
|
||||
canvas.drawString(fluid, fluidX, labelBaseline);
|
||||
if (seaX > minimumRight + labelGap && seaX + seaWidth < maximumX - labelGap) {
|
||||
canvas.drawString(sea, seaX, labelBaseline);
|
||||
}
|
||||
canvas.drawString(maximum, maximumX, labelBaseline);
|
||||
}
|
||||
@@ -970,9 +972,25 @@ final class MapCanvas extends JPanel {
|
||||
}
|
||||
IrisRenderer legendRenderer = renderer;
|
||||
RenderType type = currentType;
|
||||
List<MapCensus.Share> shares = MapCensus.count(renderFrame, screenToWorldX(0D), screenToWorldZ(0D),
|
||||
screenToWorldX(getWidth()), screenToWorldZ(getHeight()), label -> legendRenderer.legendColor(label, type));
|
||||
MapCensus.ViewArea area = new MapCensus.ViewArea(screenToWorldX(0D), screenToWorldZ(0D),
|
||||
screenToWorldX(getWidth()), screenToWorldZ(getHeight()));
|
||||
List<MapCensus.Share> shares = MapCensus.count(renderFrame, area, label -> legendRenderer.legendColor(label, type));
|
||||
listener.onLegendChanged(legendTitle(), shares, style.highlight());
|
||||
if (type == RenderType.HEIGHT) {
|
||||
fitHeightColorsToView(area);
|
||||
}
|
||||
}
|
||||
|
||||
// The height colours stretch over the heights on screen, so plains and mountains don't share one shade of green.
|
||||
private void fitHeightColorsToView(MapCensus.ViewArea area) {
|
||||
MapStyle.HeightRange measured = MapCensus.heightRange(renderFrame, area, renderer.defaultHeightRange().seaLevel());
|
||||
if (measured != null && !measured.equals(style.heightRange())) {
|
||||
applyStyle(style.withHeightRange(measured));
|
||||
}
|
||||
}
|
||||
|
||||
private MapStyle.HeightRange currentHeightRange() {
|
||||
return style.heightRange() == null ? renderer.defaultHeightRange() : style.heightRange();
|
||||
}
|
||||
|
||||
private void requestRender() {
|
||||
@@ -1027,7 +1045,7 @@ final class MapCanvas extends JPanel {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
Engine reacquired = GuiHost.findActiveEngine();
|
||||
Engine reacquired = GuiHost.findEngine(worldId);
|
||||
if (reacquired == null || reacquired.isClosed() || reacquired.getComplex() == null) {
|
||||
return false;
|
||||
}
|
||||
@@ -1035,7 +1053,6 @@ final class MapCanvas extends JPanel {
|
||||
renderer = new IrisRenderer(reacquired);
|
||||
overlay = new VisionOverlay(reacquired, openerId);
|
||||
contentRevision++;
|
||||
captureHeightRange();
|
||||
return true;
|
||||
} catch (Throwable error) {
|
||||
Iris.debug("Vision engine reacquisition failed: " + error.getClass().getSimpleName() + ": " + error.getMessage());
|
||||
@@ -1043,14 +1060,6 @@ final class MapCanvas extends JPanel {
|
||||
}
|
||||
}
|
||||
|
||||
private void captureHeightRange() {
|
||||
heightMaximum = Math.max(1D, engine.getHeight());
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
fluidHeight = dimension == null
|
||||
? 0D
|
||||
: Math.max(0D, Math.min(heightMaximum, dimension.getFluidHeight()));
|
||||
}
|
||||
|
||||
private void refreshMarkers() {
|
||||
if (closed || overlay == null) {
|
||||
return;
|
||||
|
||||
@@ -19,9 +19,11 @@
|
||||
package com.volmit.iris.core.gui;
|
||||
|
||||
import com.volmit.iris.core.gui.components.MapLabels;
|
||||
import com.volmit.iris.core.gui.components.MapStyle;
|
||||
import com.volmit.iris.core.gui.components.MapTile;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -29,45 +31,84 @@ import java.util.Map;
|
||||
import java.util.function.ToIntFunction;
|
||||
|
||||
/**
|
||||
* Counts how much of the visible map each biome or region covers, from the tiles already drawn.
|
||||
* Measures the visible part of the map from the tiles already drawn: how much each biome or region covers, and which
|
||||
* heights it spans.
|
||||
*/
|
||||
final class MapCensus {
|
||||
private static final long MAXIMUM_COUNTED_PIXELS = 250_000L;
|
||||
private static final int MINIMUM_HEIGHT_SAMPLES = 64;
|
||||
private static final double LOW_HEIGHT_PERCENTILE = 0.01D;
|
||||
private static final double HIGH_HEIGHT_PERCENTILE = 0.99D;
|
||||
private static final double HEIGHT_RANGE_STEP_BLOCKS = 8D;
|
||||
private static final double MINIMUM_WATER_DEPTH_BLOCKS = 8D;
|
||||
private static final double MINIMUM_LAND_HEIGHT_BLOCKS = 16D;
|
||||
|
||||
private MapCensus() {
|
||||
}
|
||||
|
||||
static List<Share> count(VisionRenderController.Frame frame, double minimumX, double minimumZ, double maximumX,
|
||||
double maximumZ, ToIntFunction<MapLabels.Label> colorOf) {
|
||||
static List<Share> count(VisionRenderController.Frame frame, ViewArea area, ToIntFunction<MapLabels.Label> colorOf) {
|
||||
Map<Short, long[]> counts = new HashMap<>();
|
||||
MapLabels[] labels = new MapLabels[1];
|
||||
long counted = visitVisiblePixels(frame, area, (tile, pixelX, pixelZ) -> {
|
||||
labels[0] = tile.labels();
|
||||
counts.computeIfAbsent(tile.subjectAt(pixelX, pixelZ), id -> new long[1])[0]++;
|
||||
});
|
||||
return shares(counts, labels[0], counted, colorOf);
|
||||
}
|
||||
|
||||
/**
|
||||
* The heights on screen, ignoring the highest and lowest few percent so one spike doesn't wash out every colour.
|
||||
* Rounded to whole steps, so panning around doesn't recolour the map for every small change.
|
||||
*
|
||||
* @return the range, or null until enough of the map is drawn to tell
|
||||
*/
|
||||
static MapStyle.HeightRange heightRange(VisionRenderController.Frame frame, ViewArea area, double seaLevel) {
|
||||
DoubleList heights = new DoubleList();
|
||||
visitVisiblePixels(frame, area, (tile, pixelX, pixelZ) -> {
|
||||
if (tile.hasHeights()) {
|
||||
heights.add(tile.heightAt(pixelX, pixelZ));
|
||||
}
|
||||
});
|
||||
if (heights.size() < MINIMUM_HEIGHT_SAMPLES) {
|
||||
return null;
|
||||
}
|
||||
double[] sorted = heights.sorted();
|
||||
double low = sorted[(int) ((sorted.length - 1) * LOW_HEIGHT_PERCENTILE)];
|
||||
double high = sorted[(int) ((sorted.length - 1) * HIGH_HEIGHT_PERCENTILE)];
|
||||
double lowest = Math.floor(Math.min(low, seaLevel - MINIMUM_WATER_DEPTH_BLOCKS) / HEIGHT_RANGE_STEP_BLOCKS)
|
||||
* HEIGHT_RANGE_STEP_BLOCKS;
|
||||
double highest = Math.ceil(Math.max(high, seaLevel + MINIMUM_LAND_HEIGHT_BLOCKS) / HEIGHT_RANGE_STEP_BLOCKS)
|
||||
* HEIGHT_RANGE_STEP_BLOCKS;
|
||||
return new MapStyle.HeightRange(lowest, seaLevel, highest);
|
||||
}
|
||||
|
||||
private static long visitVisiblePixels(VisionRenderController.Frame frame, ViewArea area, PixelVisitor visitor) {
|
||||
if (frame == null) {
|
||||
return List.of();
|
||||
return 0L;
|
||||
}
|
||||
double blocksPerPixel = frame.renderBlocksPerPixel();
|
||||
int stride = countingStride(maximumX - minimumX, maximumZ - minimumZ, blocksPerPixel);
|
||||
int stride = countingStride(area.maximumX() - area.minimumX(), area.maximumZ() - area.minimumZ(), blocksPerPixel);
|
||||
double tileSpan = VisionRenderController.TILE_PIXELS * blocksPerPixel;
|
||||
Map<Short, long[]> counts = new HashMap<>();
|
||||
MapLabels labels = null;
|
||||
long counted = 0L;
|
||||
long visited = 0L;
|
||||
for (VisionRenderController.VisibleTile visible : frame.tiles()) {
|
||||
MapTile tile = visible.tile();
|
||||
if (tile == null) {
|
||||
continue;
|
||||
}
|
||||
labels = tile.labels();
|
||||
double originX = visible.tileX() * tileSpan;
|
||||
double originZ = visible.tileZ() * tileSpan;
|
||||
int firstX = firstPixel(minimumX - originX, blocksPerPixel, stride, visible.tileX());
|
||||
int firstZ = firstPixel(minimumZ - originZ, blocksPerPixel, stride, visible.tileZ());
|
||||
int endX = endPixel(maximumX - originX, blocksPerPixel, tile.size());
|
||||
int endZ = endPixel(maximumZ - originZ, blocksPerPixel, tile.size());
|
||||
int firstX = firstPixel(area.minimumX() - originX, blocksPerPixel, stride, visible.tileX());
|
||||
int firstZ = firstPixel(area.minimumZ() - originZ, blocksPerPixel, stride, visible.tileZ());
|
||||
int endX = endPixel(area.maximumX() - originX, blocksPerPixel, tile.size());
|
||||
int endZ = endPixel(area.maximumZ() - originZ, blocksPerPixel, tile.size());
|
||||
for (int pixelZ = firstZ; pixelZ < endZ; pixelZ += stride) {
|
||||
for (int pixelX = firstX; pixelX < endX; pixelX += stride) {
|
||||
counts.computeIfAbsent(tile.subjectAt(pixelX, pixelZ), id -> new long[1])[0]++;
|
||||
counted++;
|
||||
visitor.visit(tile, pixelX, pixelZ);
|
||||
visited++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return shares(counts, labels, counted, colorOf);
|
||||
return visited;
|
||||
}
|
||||
|
||||
private static List<Share> shares(Map<Short, long[]> counts, MapLabels labels, long counted,
|
||||
@@ -106,4 +147,37 @@ final class MapCensus {
|
||||
|
||||
record Share(MapLabels.Label label, int color, double fraction) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The world area the screen shows, in blocks.
|
||||
*/
|
||||
record ViewArea(double minimumX, double minimumZ, double maximumX, double maximumZ) {
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface PixelVisitor {
|
||||
void visit(MapTile tile, int pixelX, int pixelZ);
|
||||
}
|
||||
|
||||
private static final class DoubleList {
|
||||
private double[] values = new double[1024];
|
||||
private int size;
|
||||
|
||||
void add(double value) {
|
||||
if (size == values.length) {
|
||||
values = Arrays.copyOf(values, size * 2);
|
||||
}
|
||||
values[size++] = value;
|
||||
}
|
||||
|
||||
int size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
double[] sorted() {
|
||||
double[] sorted = Arrays.copyOf(values, size);
|
||||
Arrays.sort(sorted);
|
||||
return sorted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ public final class MapWindow implements MapCanvas.Listener {
|
||||
private MapWindow(PaletteExplorerStart start) {
|
||||
this.start = start;
|
||||
this.frame = GuiHost.createFrame("Iris Map");
|
||||
this.canvas = new MapCanvas(start.engine(), start.playerId(), this);
|
||||
this.canvas = new MapCanvas(start.engine(), start.worldId(), start.playerId(), this);
|
||||
this.viewList = new MapViewList(this::showView);
|
||||
this.viewListScroll = StudioToolbar.scroll(viewList);
|
||||
this.legendPanel = new MapLegendPanel(share -> canvas.toggleHighlight(share.label()));
|
||||
|
||||
@@ -51,6 +51,8 @@ public final class IrisRenderer {
|
||||
private static final int UNKNOWN_SUBJECT = rgb(90, 92, 98);
|
||||
private static final int LARGEST_SAMPLE_BLOCK_PIXELS = 16;
|
||||
private static final double LARGEST_SAMPLE_BLOCK_BLOCKS = 64D;
|
||||
private static final double DEFAULT_DEPTH_BELOW_SEA_BLOCKS = 48D;
|
||||
private static final double DEFAULT_HEIGHT_ABOVE_SEA_BLOCKS = 160D;
|
||||
|
||||
private final Engine engine;
|
||||
private final MapLabels labels = new MapLabels();
|
||||
@@ -97,9 +99,9 @@ public final class IrisRenderer {
|
||||
subjects[index] = subjectOf(identities[index]);
|
||||
regions[index] = regionOf(identities[index]);
|
||||
}
|
||||
int[] colors = tileColors(size, type, subjects, heights);
|
||||
int[] colors = type == RenderType.HEIGHT ? null : tileColors(size, type, subjects);
|
||||
MapLabels.Kind subjectKind = type == RenderType.REGION ? MapLabels.Kind.REGION : MapLabels.Kind.BIOME;
|
||||
return new MapTile(size, blocksPerPixel, labels, subjectKind, colors, subjects, regions, heights);
|
||||
return new MapTile(size, blocksPerPixel, labels, subjectKind, colors, defaultHeightRange(), subjects, regions, heights);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,16 +112,24 @@ public final class IrisRenderer {
|
||||
return labelColor(label.id(), colorType);
|
||||
}
|
||||
|
||||
public static int heightColor(double height, double maximumHeight, double fluidHeight) {
|
||||
double boundedMaximum = Math.max(1D, maximumHeight);
|
||||
double boundedHeight = clamp(height, 0D, boundedMaximum);
|
||||
double boundedFluid = clamp(fluidHeight, 0D, boundedMaximum);
|
||||
if (boundedHeight <= boundedFluid && boundedFluid > 0D) {
|
||||
return blend(DEEP_WATER, SHALLOW_WATER, boundedHeight / boundedFluid);
|
||||
/**
|
||||
* Until the map has measured the heights on screen, land colours cover the band most terrain sits in.
|
||||
*/
|
||||
public MapStyle.HeightRange defaultHeightRange() {
|
||||
double seaLevel = fluidHeight();
|
||||
return new MapStyle.HeightRange(Math.max(0D, seaLevel - DEFAULT_DEPTH_BELOW_SEA_BLOCKS), seaLevel,
|
||||
Math.min(engine.getHeight(), seaLevel + DEFAULT_HEIGHT_ABOVE_SEA_BLOCKS));
|
||||
}
|
||||
|
||||
public static int heightColor(double height, MapStyle.HeightRange range) {
|
||||
double seaLevel = range.seaLevel();
|
||||
if (height <= seaLevel) {
|
||||
double waterDepthRange = Math.max(1D, seaLevel - range.lowest());
|
||||
return blend(DEEP_WATER, SHALLOW_WATER, (height - range.lowest()) / waterDepthRange);
|
||||
}
|
||||
|
||||
double landRange = Math.max(1D, boundedMaximum - boundedFluid);
|
||||
double land = clamp((boundedHeight - boundedFluid) / landRange, 0D, 1D);
|
||||
double landRange = Math.max(1D, range.highest() - seaLevel);
|
||||
double land = clamp((height - seaLevel) / landRange, 0D, 1D);
|
||||
if (land < 0.45D) {
|
||||
return blend(LOWLAND, HIGHLAND, land / 0.45D);
|
||||
}
|
||||
@@ -168,19 +178,13 @@ public final class IrisRenderer {
|
||||
};
|
||||
}
|
||||
|
||||
private int[] tileColors(int size, RenderType type, short[] subjects, double[] heights) {
|
||||
private int[] tileColors(int size, RenderType type, short[] subjects) {
|
||||
int[] colors = new int[size * size];
|
||||
double maximumHeight = engine.getHeight();
|
||||
double fluidHeight = fluidHeight();
|
||||
short previousSubject = MapLabels.NONE;
|
||||
int previousColor = UNKNOWN_SUBJECT;
|
||||
for (int pixelZ = 0; pixelZ < size; pixelZ++) {
|
||||
for (int pixelX = 0; pixelX < size; pixelX++) {
|
||||
int gridIndex = (pixelZ + 1) * (size + 2) + pixelX + 1;
|
||||
if (type == RenderType.HEIGHT) {
|
||||
colors[pixelZ * size + pixelX] = heightColor(heights[gridIndex], maximumHeight, fluidHeight);
|
||||
continue;
|
||||
}
|
||||
short subject = subjects[gridIndex];
|
||||
if (subject != previousSubject) {
|
||||
previousSubject = subject;
|
||||
|
||||
@@ -22,22 +22,33 @@ package com.volmit.iris.core.gui.components;
|
||||
* How map tiles are drawn on top of their plain colours: terrain shading, region borders and one highlighted biome or region.
|
||||
*
|
||||
* @param highlight the biome or region to keep bright while everything else dims, or null to show all
|
||||
* @param heightRange the heights the height view spreads its colours over, or null outside the height view
|
||||
*/
|
||||
public record MapStyle(boolean shading, boolean regionBorders, Highlight highlight) {
|
||||
public static final MapStyle PLAIN = new MapStyle(false, false, null);
|
||||
public record MapStyle(boolean shading, boolean regionBorders, Highlight highlight, HeightRange heightRange) {
|
||||
public static final MapStyle PLAIN = new MapStyle(false, false, null, null);
|
||||
|
||||
public MapStyle withShading(boolean nextShading) {
|
||||
return new MapStyle(nextShading, regionBorders, highlight);
|
||||
return new MapStyle(nextShading, regionBorders, highlight, heightRange);
|
||||
}
|
||||
|
||||
public MapStyle withRegionBorders(boolean nextRegionBorders) {
|
||||
return new MapStyle(shading, nextRegionBorders, highlight);
|
||||
return new MapStyle(shading, nextRegionBorders, highlight, heightRange);
|
||||
}
|
||||
|
||||
public MapStyle withHighlight(Highlight nextHighlight) {
|
||||
return new MapStyle(shading, regionBorders, nextHighlight);
|
||||
return new MapStyle(shading, regionBorders, nextHighlight, heightRange);
|
||||
}
|
||||
|
||||
public MapStyle withHeightRange(HeightRange nextHeightRange) {
|
||||
return new MapStyle(shading, regionBorders, highlight, nextHeightRange);
|
||||
}
|
||||
|
||||
public record Highlight(MapLabels.Kind kind, String key, String name) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Water colours run from the lowest height up to sea level, land colours from sea level up to the highest.
|
||||
*/
|
||||
public record HeightRange(double lowest, double seaLevel, double highest) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,19 +51,25 @@ public final class MapTile {
|
||||
private final MapLabels labels;
|
||||
private final MapLabels.Kind subjectKind;
|
||||
private final int[] colors;
|
||||
private final MapStyle.HeightRange defaultHeightRange;
|
||||
private final short[] subjects;
|
||||
private final short[] regions;
|
||||
private final double[] heights;
|
||||
private MapStyle styledFor;
|
||||
private BufferedImage styledImage;
|
||||
|
||||
MapTile(int size, double blocksPerPixel, MapLabels labels, MapLabels.Kind subjectKind, int[] colors, short[] subjects,
|
||||
short[] regions, double[] heights) {
|
||||
/**
|
||||
* @param colors the colour of every pixel, or null to colour the pixels by their height when they are drawn
|
||||
* @param defaultHeightRange the height colours to use until the map has measured the heights on screen
|
||||
*/
|
||||
MapTile(int size, double blocksPerPixel, MapLabels labels, MapLabels.Kind subjectKind, int[] colors,
|
||||
MapStyle.HeightRange defaultHeightRange, short[] subjects, short[] regions, double[] heights) {
|
||||
this.size = size;
|
||||
this.blocksPerPixel = blocksPerPixel;
|
||||
this.labels = labels;
|
||||
this.subjectKind = subjectKind;
|
||||
this.colors = colors;
|
||||
this.defaultHeightRange = defaultHeightRange;
|
||||
this.subjects = subjects;
|
||||
this.regions = regions;
|
||||
this.heights = heights;
|
||||
@@ -96,11 +102,16 @@ public final class MapTile {
|
||||
return regions[gridIndex(pixelX, pixelZ)];
|
||||
}
|
||||
|
||||
public double heightAt(int pixelX, int pixelZ) {
|
||||
return heights[gridIndex(pixelX, pixelZ)];
|
||||
}
|
||||
|
||||
public long bytes() {
|
||||
long styledBytes = (long) size * size * Integer.BYTES;
|
||||
long gridCells = (long) (size + 2) * (size + 2);
|
||||
long heightBytes = heights == null ? 0L : gridCells * Double.BYTES;
|
||||
return (long) colors.length * Integer.BYTES + gridCells * Short.BYTES * 2L + heightBytes + styledBytes;
|
||||
long colorBytes = colors == null ? 0L : (long) colors.length * Integer.BYTES;
|
||||
return colorBytes + gridCells * Short.BYTES * 2L + heightBytes + styledBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,9 +132,12 @@ public final class MapTile {
|
||||
short highlighted = highlightGrid == null
|
||||
? MapLabels.NONE
|
||||
: labels.idOf(style.highlight().kind(), style.highlight().key());
|
||||
MapStyle.HeightRange heightRange = style.heightRange() == null ? defaultHeightRange : style.heightRange();
|
||||
for (int pixelZ = 0; pixelZ < size; pixelZ++) {
|
||||
for (int pixelX = 0; pixelX < size; pixelX++) {
|
||||
int color = colors[pixelZ * size + pixelX];
|
||||
int color = colors == null
|
||||
? IrisRenderer.heightColor(heightAt(pixelX, pixelZ), heightRange)
|
||||
: colors[pixelZ * size + pixelX];
|
||||
if (shading) {
|
||||
color = shade(color, pixelX, pixelZ);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user