mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-22 16:17:44 +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:
1 parent
ec0d2d816a
commit
19bbca0e4b
6 files changed
+182
-70
No files matched your search
@@ -25,7 +25,6 @@ import com.volmit.iris.core.gui.components.MapStyle;
|
|||||||
import com.volmit.iris.core.gui.components.RenderType;
|
import com.volmit.iris.core.gui.components.RenderType;
|
||||||
import com.volmit.iris.engine.framework.Engine;
|
import com.volmit.iris.engine.framework.Engine;
|
||||||
import com.volmit.iris.engine.object.IrisBiome;
|
import com.volmit.iris.engine.object.IrisBiome;
|
||||||
import com.volmit.iris.engine.object.IrisDimension;
|
|
||||||
import com.volmit.iris.util.format.Form;
|
import com.volmit.iris.util.format.Form;
|
||||||
|
|
||||||
import javax.swing.JMenuItem;
|
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 double KEYBOARD_ZOOM_FACTOR = 1.189207115002721D;
|
||||||
private static final int LEGEND_REFRESH_MILLIS = 200;
|
private static final int LEGEND_REFRESH_MILLIS = 200;
|
||||||
|
|
||||||
|
private final UUID worldId;
|
||||||
private final UUID openerId;
|
private final UUID openerId;
|
||||||
private final Listener listener;
|
private final Listener listener;
|
||||||
private final VisionRenderController controller;
|
private final VisionRenderController controller;
|
||||||
@@ -108,8 +108,6 @@ final class MapCanvas extends JPanel {
|
|||||||
private boolean draggingDivider;
|
private boolean draggingDivider;
|
||||||
private double centerX;
|
private double centerX;
|
||||||
private double centerZ;
|
private double centerZ;
|
||||||
private double heightMaximum;
|
|
||||||
private double fluidHeight;
|
|
||||||
private long contentRevision = 1L;
|
private long contentRevision = 1L;
|
||||||
private long previousPaintStartedNanos;
|
private long previousPaintStartedNanos;
|
||||||
private double blocksPerPixel = DEFAULT_BLOCKS_PER_PIXEL;
|
private double blocksPerPixel = DEFAULT_BLOCKS_PER_PIXEL;
|
||||||
@@ -123,15 +121,15 @@ final class MapCanvas extends JPanel {
|
|||||||
private boolean active = true;
|
private boolean active = true;
|
||||||
private boolean closed;
|
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.engine = Objects.requireNonNull(engine, "engine");
|
||||||
|
this.worldId = Objects.requireNonNull(worldId, "worldId");
|
||||||
this.openerId = openerId;
|
this.openerId = openerId;
|
||||||
this.listener = Objects.requireNonNull(listener, "listener");
|
this.listener = Objects.requireNonNull(listener, "listener");
|
||||||
this.renderer = new IrisRenderer(engine);
|
this.renderer = new IrisRenderer(engine);
|
||||||
this.overlay = new VisionOverlay(engine, openerId);
|
this.overlay = new VisionOverlay(engine, openerId);
|
||||||
this.controller = new VisionRenderController(this::onTilesChanged);
|
this.controller = new VisionRenderController(this::onTilesChanged);
|
||||||
this.hotloadHook = () -> EventQueue.invokeLater(this::refreshContent);
|
this.hotloadHook = () -> EventQueue.invokeLater(this::refreshContent);
|
||||||
captureHeightRange();
|
|
||||||
|
|
||||||
setBackground(StudioTheme.EDITOR_BACKGROUND);
|
setBackground(StudioTheme.EDITOR_BACKGROUND);
|
||||||
setFocusable(true);
|
setFocusable(true);
|
||||||
@@ -214,6 +212,9 @@ final class MapCanvas extends JPanel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
currentType = view.renderType();
|
currentType = view.renderType();
|
||||||
|
if (currentType != RenderType.HEIGHT) {
|
||||||
|
style = style.withHeightRange(null);
|
||||||
|
}
|
||||||
requestRender();
|
requestRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,7 +242,6 @@ final class MapCanvas extends JPanel {
|
|||||||
renderer = new IrisRenderer(engine);
|
renderer = new IrisRenderer(engine);
|
||||||
overlay = new VisionOverlay(engine, openerId);
|
overlay = new VisionOverlay(engine, openerId);
|
||||||
contentRevision++;
|
contentRevision++;
|
||||||
captureHeightRange();
|
|
||||||
requestRender();
|
requestRender();
|
||||||
notifyUser(captured && comparison.mode() == HotloadComparison.Mode.OFF
|
notifyUser(captured && comparison.mode() == HotloadComparison.Mode.OFF
|
||||||
? "Refreshing · press C to compare with before"
|
? "Refreshing · press C to compare with before"
|
||||||
@@ -796,6 +796,7 @@ final class MapCanvas extends JPanel {
|
|||||||
if (currentType != RenderType.HEIGHT) {
|
if (currentType != RenderType.HEIGHT) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
MapStyle.HeightRange range = currentHeightRange();
|
||||||
canvas.setFont(StudioTheme.smallFont());
|
canvas.setFont(StudioTheme.smallFont());
|
||||||
int padding = StudioTheme.scale(CARD_PADDING);
|
int padding = StudioTheme.scale(CARD_PADDING);
|
||||||
int gradientHeight = StudioTheme.scale(14);
|
int gradientHeight = StudioTheme.scale(14);
|
||||||
@@ -808,23 +809,24 @@ final class MapCanvas extends JPanel {
|
|||||||
int gradientX = x + padding;
|
int gradientX = x + padding;
|
||||||
int gradientY = y + padding;
|
int gradientY = y + padding;
|
||||||
int gradientWidth = width - padding * 2;
|
int gradientWidth = width - padding * 2;
|
||||||
|
double span = Math.max(1D, range.highest() - range.lowest());
|
||||||
for (int pixel = 0; pixel < gradientWidth; pixel++) {
|
for (int pixel = 0; pixel < gradientWidth; pixel++) {
|
||||||
double sampleHeight = heightMaximum * pixel / Math.max(1D, gradientWidth - 1D);
|
double sampleHeight = range.lowest() + span * pixel / Math.max(1D, gradientWidth - 1D);
|
||||||
canvas.setColor(new Color(IrisRenderer.heightColor(sampleHeight, heightMaximum, fluidHeight)));
|
canvas.setColor(new Color(IrisRenderer.heightColor(sampleHeight, range)));
|
||||||
canvas.drawLine(gradientX + pixel, gradientY, gradientX + pixel, gradientY + gradientHeight);
|
canvas.drawLine(gradientX + pixel, gradientY, gradientX + pixel, gradientY + gradientHeight);
|
||||||
}
|
}
|
||||||
canvas.setColor(StudioTheme.SECONDARY_TEXT);
|
canvas.setColor(StudioTheme.SECONDARY_TEXT);
|
||||||
int labelBaseline = gradientY + gradientHeight + labelGap + canvas.getFontMetrics().getAscent();
|
int labelBaseline = gradientY + gradientHeight + labelGap + canvas.getFontMetrics().getAscent();
|
||||||
String minimum = "0";
|
String minimum = Integer.toString((int) Math.round(range.lowest()));
|
||||||
String fluid = Integer.toString((int) Math.round(fluidHeight));
|
String sea = "sea " + Math.round(range.seaLevel());
|
||||||
String maximum = Integer.toString((int) Math.round(heightMaximum));
|
String maximum = Integer.toString((int) Math.round(range.highest()));
|
||||||
int minimumRight = gradientX + canvas.getFontMetrics().stringWidth(minimum);
|
int minimumRight = gradientX + canvas.getFontMetrics().stringWidth(minimum);
|
||||||
int maximumX = gradientX + gradientWidth - canvas.getFontMetrics().stringWidth(maximum);
|
int maximumX = gradientX + gradientWidth - canvas.getFontMetrics().stringWidth(maximum);
|
||||||
int fluidWidth = canvas.getFontMetrics().stringWidth(fluid);
|
int seaWidth = canvas.getFontMetrics().stringWidth(sea);
|
||||||
int fluidX = gradientX + (int) Math.round(gradientWidth * fluidHeight / Math.max(1D, heightMaximum)) - fluidWidth / 2;
|
int seaX = gradientX + (int) Math.round(gradientWidth * (range.seaLevel() - range.lowest()) / span) - seaWidth / 2;
|
||||||
canvas.drawString(minimum, gradientX, labelBaseline);
|
canvas.drawString(minimum, gradientX, labelBaseline);
|
||||||
if (fluidX > minimumRight + labelGap && fluidX + fluidWidth < maximumX - labelGap) {
|
if (seaX > minimumRight + labelGap && seaX + seaWidth < maximumX - labelGap) {
|
||||||
canvas.drawString(fluid, fluidX, labelBaseline);
|
canvas.drawString(sea, seaX, labelBaseline);
|
||||||
}
|
}
|
||||||
canvas.drawString(maximum, maximumX, labelBaseline);
|
canvas.drawString(maximum, maximumX, labelBaseline);
|
||||||
}
|
}
|
||||||
@@ -970,9 +972,25 @@ final class MapCanvas extends JPanel {
|
|||||||
}
|
}
|
||||||
IrisRenderer legendRenderer = renderer;
|
IrisRenderer legendRenderer = renderer;
|
||||||
RenderType type = currentType;
|
RenderType type = currentType;
|
||||||
List<MapCensus.Share> shares = MapCensus.count(renderFrame, screenToWorldX(0D), screenToWorldZ(0D),
|
MapCensus.ViewArea area = new MapCensus.ViewArea(screenToWorldX(0D), screenToWorldZ(0D),
|
||||||
screenToWorldX(getWidth()), screenToWorldZ(getHeight()), label -> legendRenderer.legendColor(label, type));
|
screenToWorldX(getWidth()), screenToWorldZ(getHeight()));
|
||||||
|
List<MapCensus.Share> shares = MapCensus.count(renderFrame, area, label -> legendRenderer.legendColor(label, type));
|
||||||
listener.onLegendChanged(legendTitle(), shares, style.highlight());
|
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() {
|
private void requestRender() {
|
||||||
@@ -1027,7 +1045,7 @@ final class MapCanvas extends JPanel {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Engine reacquired = GuiHost.findActiveEngine();
|
Engine reacquired = GuiHost.findEngine(worldId);
|
||||||
if (reacquired == null || reacquired.isClosed() || reacquired.getComplex() == null) {
|
if (reacquired == null || reacquired.isClosed() || reacquired.getComplex() == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1035,7 +1053,6 @@ final class MapCanvas extends JPanel {
|
|||||||
renderer = new IrisRenderer(reacquired);
|
renderer = new IrisRenderer(reacquired);
|
||||||
overlay = new VisionOverlay(reacquired, openerId);
|
overlay = new VisionOverlay(reacquired, openerId);
|
||||||
contentRevision++;
|
contentRevision++;
|
||||||
captureHeightRange();
|
|
||||||
return true;
|
return true;
|
||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
Iris.debug("Vision engine reacquisition failed: " + error.getClass().getSimpleName() + ": " + error.getMessage());
|
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() {
|
private void refreshMarkers() {
|
||||||
if (closed || overlay == null) {
|
if (closed || overlay == null) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -19,9 +19,11 @@
|
|||||||
package com.volmit.iris.core.gui;
|
package com.volmit.iris.core.gui;
|
||||||
|
|
||||||
import com.volmit.iris.core.gui.components.MapLabels;
|
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 com.volmit.iris.core.gui.components.MapTile;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -29,45 +31,84 @@ import java.util.Map;
|
|||||||
import java.util.function.ToIntFunction;
|
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 {
|
final class MapCensus {
|
||||||
private static final long MAXIMUM_COUNTED_PIXELS = 250_000L;
|
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() {
|
private MapCensus() {
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<Share> count(VisionRenderController.Frame frame, double minimumX, double minimumZ, double maximumX,
|
static List<Share> count(VisionRenderController.Frame frame, ViewArea area, ToIntFunction<MapLabels.Label> colorOf) {
|
||||||
double maximumZ, 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) {
|
if (frame == null) {
|
||||||
return List.of();
|
return 0L;
|
||||||
}
|
}
|
||||||
double blocksPerPixel = frame.renderBlocksPerPixel();
|
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;
|
double tileSpan = VisionRenderController.TILE_PIXELS * blocksPerPixel;
|
||||||
Map<Short, long[]> counts = new HashMap<>();
|
long visited = 0L;
|
||||||
MapLabels labels = null;
|
|
||||||
long counted = 0L;
|
|
||||||
for (VisionRenderController.VisibleTile visible : frame.tiles()) {
|
for (VisionRenderController.VisibleTile visible : frame.tiles()) {
|
||||||
MapTile tile = visible.tile();
|
MapTile tile = visible.tile();
|
||||||
if (tile == null) {
|
if (tile == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
labels = tile.labels();
|
|
||||||
double originX = visible.tileX() * tileSpan;
|
double originX = visible.tileX() * tileSpan;
|
||||||
double originZ = visible.tileZ() * tileSpan;
|
double originZ = visible.tileZ() * tileSpan;
|
||||||
int firstX = firstPixel(minimumX - originX, blocksPerPixel, stride, visible.tileX());
|
int firstX = firstPixel(area.minimumX() - originX, blocksPerPixel, stride, visible.tileX());
|
||||||
int firstZ = firstPixel(minimumZ - originZ, blocksPerPixel, stride, visible.tileZ());
|
int firstZ = firstPixel(area.minimumZ() - originZ, blocksPerPixel, stride, visible.tileZ());
|
||||||
int endX = endPixel(maximumX - originX, blocksPerPixel, tile.size());
|
int endX = endPixel(area.maximumX() - originX, blocksPerPixel, tile.size());
|
||||||
int endZ = endPixel(maximumZ - originZ, blocksPerPixel, tile.size());
|
int endZ = endPixel(area.maximumZ() - originZ, blocksPerPixel, tile.size());
|
||||||
for (int pixelZ = firstZ; pixelZ < endZ; pixelZ += stride) {
|
for (int pixelZ = firstZ; pixelZ < endZ; pixelZ += stride) {
|
||||||
for (int pixelX = firstX; pixelX < endX; pixelX += stride) {
|
for (int pixelX = firstX; pixelX < endX; pixelX += stride) {
|
||||||
counts.computeIfAbsent(tile.subjectAt(pixelX, pixelZ), id -> new long[1])[0]++;
|
visitor.visit(tile, pixelX, pixelZ);
|
||||||
counted++;
|
visited++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return shares(counts, labels, counted, colorOf);
|
return visited;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Share> shares(Map<Short, long[]> counts, MapLabels labels, long counted,
|
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) {
|
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) {
|
private MapWindow(PaletteExplorerStart start) {
|
||||||
this.start = start;
|
this.start = start;
|
||||||
this.frame = GuiHost.createFrame("Iris Map");
|
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.viewList = new MapViewList(this::showView);
|
||||||
this.viewListScroll = StudioToolbar.scroll(viewList);
|
this.viewListScroll = StudioToolbar.scroll(viewList);
|
||||||
this.legendPanel = new MapLegendPanel(share -> canvas.toggleHighlight(share.label()));
|
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 UNKNOWN_SUBJECT = rgb(90, 92, 98);
|
||||||
private static final int LARGEST_SAMPLE_BLOCK_PIXELS = 16;
|
private static final int LARGEST_SAMPLE_BLOCK_PIXELS = 16;
|
||||||
private static final double LARGEST_SAMPLE_BLOCK_BLOCKS = 64D;
|
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 Engine engine;
|
||||||
private final MapLabels labels = new MapLabels();
|
private final MapLabels labels = new MapLabels();
|
||||||
@@ -97,9 +99,9 @@ public final class IrisRenderer {
|
|||||||
subjects[index] = subjectOf(identities[index]);
|
subjects[index] = subjectOf(identities[index]);
|
||||||
regions[index] = regionOf(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;
|
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);
|
return labelColor(label.id(), colorType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int heightColor(double height, double maximumHeight, double fluidHeight) {
|
/**
|
||||||
double boundedMaximum = Math.max(1D, maximumHeight);
|
* Until the map has measured the heights on screen, land colours cover the band most terrain sits in.
|
||||||
double boundedHeight = clamp(height, 0D, boundedMaximum);
|
*/
|
||||||
double boundedFluid = clamp(fluidHeight, 0D, boundedMaximum);
|
public MapStyle.HeightRange defaultHeightRange() {
|
||||||
if (boundedHeight <= boundedFluid && boundedFluid > 0D) {
|
double seaLevel = fluidHeight();
|
||||||
return blend(DEEP_WATER, SHALLOW_WATER, boundedHeight / boundedFluid);
|
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 landRange = Math.max(1D, range.highest() - seaLevel);
|
||||||
double land = clamp((boundedHeight - boundedFluid) / landRange, 0D, 1D);
|
double land = clamp((height - seaLevel) / landRange, 0D, 1D);
|
||||||
if (land < 0.45D) {
|
if (land < 0.45D) {
|
||||||
return blend(LOWLAND, HIGHLAND, 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];
|
int[] colors = new int[size * size];
|
||||||
double maximumHeight = engine.getHeight();
|
|
||||||
double fluidHeight = fluidHeight();
|
|
||||||
short previousSubject = MapLabels.NONE;
|
short previousSubject = MapLabels.NONE;
|
||||||
int previousColor = UNKNOWN_SUBJECT;
|
int previousColor = UNKNOWN_SUBJECT;
|
||||||
for (int pixelZ = 0; pixelZ < size; pixelZ++) {
|
for (int pixelZ = 0; pixelZ < size; pixelZ++) {
|
||||||
for (int pixelX = 0; pixelX < size; pixelX++) {
|
for (int pixelX = 0; pixelX < size; pixelX++) {
|
||||||
int gridIndex = (pixelZ + 1) * (size + 2) + pixelX + 1;
|
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];
|
short subject = subjects[gridIndex];
|
||||||
if (subject != previousSubject) {
|
if (subject != previousSubject) {
|
||||||
previousSubject = subject;
|
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.
|
* 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 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 record MapStyle(boolean shading, boolean regionBorders, Highlight highlight, HeightRange heightRange) {
|
||||||
public static final MapStyle PLAIN = new MapStyle(false, false, null);
|
public static final MapStyle PLAIN = new MapStyle(false, false, null, null);
|
||||||
|
|
||||||
public MapStyle withShading(boolean nextShading) {
|
public MapStyle withShading(boolean nextShading) {
|
||||||
return new MapStyle(nextShading, regionBorders, highlight);
|
return new MapStyle(nextShading, regionBorders, highlight, heightRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MapStyle withRegionBorders(boolean nextRegionBorders) {
|
public MapStyle withRegionBorders(boolean nextRegionBorders) {
|
||||||
return new MapStyle(shading, nextRegionBorders, highlight);
|
return new MapStyle(shading, nextRegionBorders, highlight, heightRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MapStyle withHighlight(Highlight nextHighlight) {
|
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) {
|
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 labels;
|
||||||
private final MapLabels.Kind subjectKind;
|
private final MapLabels.Kind subjectKind;
|
||||||
private final int[] colors;
|
private final int[] colors;
|
||||||
|
private final MapStyle.HeightRange defaultHeightRange;
|
||||||
private final short[] subjects;
|
private final short[] subjects;
|
||||||
private final short[] regions;
|
private final short[] regions;
|
||||||
private final double[] heights;
|
private final double[] heights;
|
||||||
private MapStyle styledFor;
|
private MapStyle styledFor;
|
||||||
private BufferedImage styledImage;
|
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.size = size;
|
||||||
this.blocksPerPixel = blocksPerPixel;
|
this.blocksPerPixel = blocksPerPixel;
|
||||||
this.labels = labels;
|
this.labels = labels;
|
||||||
this.subjectKind = subjectKind;
|
this.subjectKind = subjectKind;
|
||||||
this.colors = colors;
|
this.colors = colors;
|
||||||
|
this.defaultHeightRange = defaultHeightRange;
|
||||||
this.subjects = subjects;
|
this.subjects = subjects;
|
||||||
this.regions = regions;
|
this.regions = regions;
|
||||||
this.heights = heights;
|
this.heights = heights;
|
||||||
@@ -96,11 +102,16 @@ public final class MapTile {
|
|||||||
return regions[gridIndex(pixelX, pixelZ)];
|
return regions[gridIndex(pixelX, pixelZ)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public double heightAt(int pixelX, int pixelZ) {
|
||||||
|
return heights[gridIndex(pixelX, pixelZ)];
|
||||||
|
}
|
||||||
|
|
||||||
public long bytes() {
|
public long bytes() {
|
||||||
long styledBytes = (long) size * size * Integer.BYTES;
|
long styledBytes = (long) size * size * Integer.BYTES;
|
||||||
long gridCells = (long) (size + 2) * (size + 2);
|
long gridCells = (long) (size + 2) * (size + 2);
|
||||||
long heightBytes = heights == null ? 0L : gridCells * Double.BYTES;
|
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
|
short highlighted = highlightGrid == null
|
||||||
? MapLabels.NONE
|
? MapLabels.NONE
|
||||||
: labels.idOf(style.highlight().kind(), style.highlight().key());
|
: 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 pixelZ = 0; pixelZ < size; pixelZ++) {
|
||||||
for (int pixelX = 0; pixelX < size; pixelX++) {
|
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) {
|
if (shading) {
|
||||||
color = shade(color, pixelX, pixelZ);
|
color = shade(color, pixelX, pixelZ);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user