mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-22 16:17:44 +00:00
Show the noise that picks regions and biomes
Noise view, rarity bands, hover picks.
This commit is contained in:
1 parent
97dfb63741
commit
0b5b75e936
14 files changed
+1022
-109
No files matched your search
@@ -23,6 +23,7 @@ import com.volmit.iris.core.gui.components.IrisRenderer;
|
||||
import com.volmit.iris.core.gui.components.MapLabels;
|
||||
import com.volmit.iris.core.gui.components.MapLayer;
|
||||
import com.volmit.iris.core.gui.components.MapStyle;
|
||||
import com.volmit.iris.core.gui.components.PickDisplay;
|
||||
import com.volmit.iris.core.gui.components.RenderType;
|
||||
import com.volmit.iris.engine.framework.Engine;
|
||||
import com.volmit.iris.engine.object.IrisBiome;
|
||||
@@ -97,6 +98,7 @@ final class MapCanvas extends JPanel {
|
||||
private MapStyle style = MapStyle.PLAIN;
|
||||
private RenderType currentType = RenderType.BIOME;
|
||||
private String previewRegionKey;
|
||||
private PickDisplay pickDisplay = PickDisplay.PICKS;
|
||||
private String viewLabel = MapView.BIOMES.label();
|
||||
private List<GuiMarker> players = List.of();
|
||||
private List<GuiMarker> entities = List.of();
|
||||
@@ -219,19 +221,36 @@ final class MapCanvas extends JPanel {
|
||||
return;
|
||||
}
|
||||
previewRegionKey = regionKey;
|
||||
if (currentType == RenderType.REGION || currentType == RenderType.REGION_PREVIEW) {
|
||||
if (currentType.isRegionView()) {
|
||||
currentType = regionKey == null ? RenderType.REGION : RenderType.REGION_PREVIEW;
|
||||
requestRender();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the region view's picks, the noise that picks them, or both.
|
||||
*/
|
||||
void showPicksAs(PickDisplay display) {
|
||||
if (display == pickDisplay) {
|
||||
return;
|
||||
}
|
||||
pickDisplay = display;
|
||||
if (currentType.isRegionView()) {
|
||||
requestRender();
|
||||
}
|
||||
}
|
||||
|
||||
MapCensus.ViewArea viewArea() {
|
||||
return new MapCensus.ViewArea(screenToWorldX(0D), screenToWorldZ(0D), screenToWorldX(getWidth()),
|
||||
screenToWorldZ(getHeight()));
|
||||
}
|
||||
|
||||
private MapLayer currentLayer() {
|
||||
return currentType == RenderType.REGION_PREVIEW ? MapLayer.regionPreview(previewRegionKey) : MapLayer.of(currentType);
|
||||
return switch (currentType) {
|
||||
case REGION -> MapLayer.regions(pickDisplay);
|
||||
case REGION_PREVIEW -> MapLayer.regionPreview(previewRegionKey, pickDisplay);
|
||||
default -> MapLayer.of(currentType);
|
||||
};
|
||||
}
|
||||
|
||||
String legendTitle() {
|
||||
@@ -1079,7 +1098,7 @@ final class MapCanvas extends JPanel {
|
||||
double worldZ = screenToWorldZ(point.y);
|
||||
controller.submitProbe(
|
||||
frame,
|
||||
() -> MapHoverProbe.sample(probeEngine, worldX, worldZ, previewedRegion()),
|
||||
() -> MapHoverProbe.sample(probeEngine, worldX, worldZ, previewedRegion(), currentType.isRegionView()),
|
||||
details -> {
|
||||
QuickHover quick = quickHover;
|
||||
if (quick != null && quick.worldX() == details.worldX() && quick.worldZ() == details.worldZ()) {
|
||||
|
||||
@@ -27,6 +27,7 @@ import com.volmit.iris.engine.object.IrisBiome;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* The text of the card that follows the cursor: what the drawn map shows there at once, and the rest once probed.
|
||||
@@ -95,6 +96,32 @@ final class MapHoverCard {
|
||||
if (details.placement() != null) {
|
||||
lines.add(placementText(details.placement()));
|
||||
}
|
||||
if (details.pickNoise() != null) {
|
||||
addPickNoise(lines, details.pickNoise(), surface);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addPickNoise(List<String> lines, MapHoverProbe.PickNoise noise, IrisBiome surface) {
|
||||
if (noise.regionNoise() != null) {
|
||||
lines.add("Region noise " + noiseText(noise.regionNoise()));
|
||||
}
|
||||
if (noise.biomeType() == null) {
|
||||
return;
|
||||
}
|
||||
String biomeNoise = capitalize(noise.biomeType().name().toLowerCase(Locale.ROOT)) + " biome noise "
|
||||
+ noiseText(noise.biomeNoise());
|
||||
IrisBiome picked = noise.noisePick();
|
||||
if (picked == null) {
|
||||
lines.add(biomeNoise);
|
||||
} else if (surface == null || picked.getLoadKey().equals(surface.getLoadKey())) {
|
||||
lines.add(biomeNoise + " picks " + picked.getName());
|
||||
} else {
|
||||
lines.add(biomeNoise + " picks " + picked.getName() + ", then " + surface.getName() + " takes over");
|
||||
}
|
||||
}
|
||||
|
||||
private static String noiseText(double noise) {
|
||||
return String.format(Locale.ROOT, "%.3f", noise);
|
||||
}
|
||||
|
||||
// How hybrid terrain chose the base biome; a child biome or a coast can still replace it afterwards.
|
||||
|
||||
@@ -41,8 +41,9 @@ final class MapHoverProbe {
|
||||
|
||||
/**
|
||||
* @param previewRegionKey the region the region preview treats every spot as part of, or null for the real world
|
||||
* @param explainPicks also look up the noise values that picked the region and biome, for the region view
|
||||
*/
|
||||
static Details sample(Engine engine, double worldX, double worldZ, String previewRegionKey) {
|
||||
static Details sample(Engine engine, double worldX, double worldZ, String previewRegionKey, boolean explainPicks) {
|
||||
IrisComplex complex = engine.getComplex();
|
||||
IrisRegion previewRegion = previewRegionKey == null ? null : engine.getData().getRegionLoader().load(previewRegionKey);
|
||||
IrisRegion region = previewRegion != null ? previewRegion : complex.getRegionStream().get(worldX, worldZ);
|
||||
@@ -66,10 +67,22 @@ final class MapHoverProbe {
|
||||
height,
|
||||
dimension == null ? 0D : dimension.getFluidHeight(),
|
||||
Math.toDegrees(Math.atan(slope / SLOPE_STREAM_RANGE_BLOCKS)),
|
||||
complex.explainHybridChoice(region, worldX, worldZ)
|
||||
complex.explainHybridChoice(region, worldX, worldZ),
|
||||
explainPicks ? pickNoise(complex, region, previewRegion == null, worldX, worldZ) : null
|
||||
);
|
||||
}
|
||||
|
||||
// HYBRID terrain picks biomes by height, which the placement line already tells, so only the region noise is left.
|
||||
private static PickNoise pickNoise(IrisComplex complex, IrisRegion region, boolean regionPicked, double x, double z) {
|
||||
Double regionNoise = regionPicked ? complex.getRegionStyleStream().get(x, z) : null;
|
||||
if (complex.isHybridTerrain() || region == null) {
|
||||
return new PickNoise(regionNoise, null, Double.NaN, null);
|
||||
}
|
||||
InferredType biomeType = complex.surfacePickAsIfIn(region, x, z).type();
|
||||
return new PickNoise(regionNoise, biomeType, complex.biomePickNoise(region, biomeType, x, z),
|
||||
complex.biomePickedByNoise(region, biomeType, x, z));
|
||||
}
|
||||
|
||||
// A shore biome sits on land the continent stream calls land or sea, so the biome's own type says it best.
|
||||
private static String terrainKindName(IrisBiome surfaceBiome, InferredType continentKind) {
|
||||
InferredType kind = surfaceBiome != null && surfaceBiome.getInferredType() != null
|
||||
@@ -93,7 +106,16 @@ final class MapHoverProbe {
|
||||
double height,
|
||||
double fluidHeight,
|
||||
double slopeDegrees,
|
||||
HybridBiomeSelector.Choice placement
|
||||
HybridBiomeSelector.Choice placement,
|
||||
PickNoise pickNoise
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param regionNoise the region noise value, or null when the region preview chose the region
|
||||
* @param biomeType the biome list the biome noise picked from, or null when no noise picked the biome
|
||||
* @param noisePick the biome the noise picked, which a child biome can still replace
|
||||
*/
|
||||
record PickNoise(Double regionNoise, InferredType biomeType, double biomeNoise, IrisBiome noisePick) {
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ public final class MapWindow implements MapCanvas.Listener {
|
||||
this.viewList = new MapViewList(this::showView);
|
||||
this.viewListScroll = StudioToolbar.scroll(viewList);
|
||||
this.legendPanel = new MapLegendPanel(share -> canvas.toggleHighlight(share.label()));
|
||||
this.regionExplorer = new RegionExplorer(canvas::engine, canvas::previewRegion);
|
||||
this.regionExplorer = new RegionExplorer(canvas::engine, canvas::previewRegion, canvas::showPicksAs);
|
||||
canvas.centerOn(start.centerX(), start.centerZ());
|
||||
buildFrame();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.gui;
|
||||
|
||||
import javax.swing.JComponent;
|
||||
import java.awt.Color;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.FontMetrics;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* One noise that picks regions or biomes: the spread of its values in view above the bands that pick each option,
|
||||
* and a table of how much of the noise each option is given and how much it gets in view.
|
||||
*/
|
||||
final class NoisePickChart extends JComponent {
|
||||
private static final long serialVersionUID = 1873365510472853316L;
|
||||
private static final int PREFERRED_WIDTH = 300;
|
||||
private static final int MARGIN = 16;
|
||||
private static final int HISTOGRAM_HEIGHT = 64;
|
||||
private static final int BAND_HEIGHT = 12;
|
||||
private static final int TITLE_GAP = 6;
|
||||
private static final int SECTION_GAP = 12;
|
||||
private static final int BOTTOM_GAP = 14;
|
||||
private static final Color HISTOGRAM_BAR = new Color(0x9D, 0xA0, 0xA8, 150);
|
||||
private static final Color BAND_DIVIDER = new Color(255, 255, 255, 70);
|
||||
private static final String WIDEST_SHARE = "▲ 100.0%";
|
||||
|
||||
private final NoisePickSpread spread;
|
||||
private final String optionNoun;
|
||||
|
||||
/**
|
||||
* @param optionNoun what the noise picks, as a table heading: "Regions" or "Biomes"
|
||||
*/
|
||||
NoisePickChart(NoisePickSpread spread, String optionNoun) {
|
||||
this.spread = spread;
|
||||
this.optionNoun = optionNoun;
|
||||
setAlignmentX(LEFT_ALIGNMENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getPreferredSize() {
|
||||
int height = StudioTheme.scale(SECTION_GAP) + spreadHeight() + StudioTheme.scale(SECTION_GAP) + tableHeight()
|
||||
+ StudioTheme.scale(BOTTOM_GAP);
|
||||
return new Dimension(StudioTheme.scale(PREFERRED_WIDTH), height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getMaximumSize() {
|
||||
return new Dimension(Integer.MAX_VALUE, getPreferredSize().height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintComponent(Graphics graphics) {
|
||||
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||
try {
|
||||
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
int margin = StudioTheme.scale(MARGIN);
|
||||
int width = getWidth() - margin * 2;
|
||||
int top = StudioTheme.scale(SECTION_GAP);
|
||||
drawSpread(canvas, margin, top, width);
|
||||
drawTable(canvas, margin, top + spreadHeight() + StudioTheme.scale(SECTION_GAP), width);
|
||||
} finally {
|
||||
canvas.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private int spreadHeight() {
|
||||
return sectionMetrics().getHeight() + StudioTheme.scale(TITLE_GAP) + StudioTheme.scale(HISTOGRAM_HEIGHT)
|
||||
+ StudioTheme.scale(2) + StudioTheme.scale(BAND_HEIGHT) + StudioTheme.scale(3) + smallMetrics().getHeight();
|
||||
}
|
||||
|
||||
private int tableHeight() {
|
||||
return sectionMetrics().getHeight() + StudioTheme.scale(TITLE_GAP) + rowHeight() * spread.rows().size();
|
||||
}
|
||||
|
||||
private void drawSpread(Graphics2D canvas, int x, int top, int width) {
|
||||
drawSectionTitle(canvas, spread.title(), x, top + sectionMetrics().getAscent());
|
||||
int histogramTop = top + sectionMetrics().getHeight() + StudioTheme.scale(TITLE_GAP);
|
||||
int histogramHeight = StudioTheme.scale(HISTOGRAM_HEIGHT);
|
||||
drawHistogram(canvas, x, histogramTop, width, histogramHeight);
|
||||
int bandTop = histogramTop + histogramHeight + StudioTheme.scale(2);
|
||||
drawBands(canvas, x, width, histogramTop, bandTop);
|
||||
|
||||
canvas.setFont(StudioTheme.smallFont());
|
||||
canvas.setColor(StudioTheme.SECONDARY_TEXT);
|
||||
FontMetrics metrics = canvas.getFontMetrics();
|
||||
int labelY = bandTop + StudioTheme.scale(BAND_HEIGHT) + StudioTheme.scale(3) + metrics.getAscent();
|
||||
canvas.drawString("0", x, labelY);
|
||||
canvas.drawString("0.5", x + (width - metrics.stringWidth("0.5")) / 2, labelY);
|
||||
canvas.drawString("1", x + width - metrics.stringWidth("1"), labelY);
|
||||
}
|
||||
|
||||
private void drawHistogram(Graphics2D canvas, int x, int top, int width, int height) {
|
||||
long[] histogram = spread.histogram();
|
||||
long peak = 1L;
|
||||
for (long count : histogram) {
|
||||
peak = Math.max(peak, count);
|
||||
}
|
||||
canvas.setColor(HISTOGRAM_BAR);
|
||||
for (int bin = 0; bin < histogram.length; bin++) {
|
||||
int barLeft = x + (bin * width) / histogram.length;
|
||||
int barRight = x + ((bin + 1) * width) / histogram.length;
|
||||
int barHeight = (int) Math.round(height * (histogram[bin] / (double) peak));
|
||||
canvas.fillRect(barLeft, top + height - barHeight, Math.max(1, barRight - barLeft - 1), barHeight);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawBands(Graphics2D canvas, int x, int width, int histogramTop, int bandTop) {
|
||||
int bandHeight = StudioTheme.scale(BAND_HEIGHT);
|
||||
for (NoisePickSpread.Band band : spread.bands()) {
|
||||
int bandLeft = x + (int) Math.round(band.fromNoise() * width);
|
||||
int bandRight = x + (int) Math.round(band.toNoise() * width);
|
||||
canvas.setColor(new Color(band.option().color()));
|
||||
canvas.fillRect(bandLeft, bandTop, Math.max(1, bandRight - bandLeft), bandHeight);
|
||||
if (band.toNoise() < 1D) {
|
||||
canvas.setColor(BAND_DIVIDER);
|
||||
canvas.drawLine(bandRight, histogramTop, bandRight, bandTop + bandHeight - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void drawTable(Graphics2D canvas, int x, int top, int width) {
|
||||
int columnGap = StudioTheme.scale(10);
|
||||
int actualRight = x + width;
|
||||
int weightRight = actualRight - textMetrics().stringWidth(WIDEST_SHARE) - columnGap;
|
||||
int nameRight = weightRight - textMetrics().stringWidth(WIDEST_SHARE) - columnGap;
|
||||
int titleBaseline = top + sectionMetrics().getAscent();
|
||||
drawSectionTitle(canvas, optionNoun, x, titleBaseline);
|
||||
canvas.setFont(StudioTheme.sectionFont());
|
||||
drawRightAligned(canvas, "WEIGHT", weightRight, titleBaseline);
|
||||
drawRightAligned(canvas, "IN VIEW", actualRight, titleBaseline);
|
||||
|
||||
FontMetrics text = textMetrics();
|
||||
int rowHeight = rowHeight();
|
||||
int swatchSize = text.getAscent() - text.getDescent();
|
||||
int rowTop = top + sectionMetrics().getHeight() + StudioTheme.scale(TITLE_GAP);
|
||||
List<NoisePickSpread.Row> rows = spread.rows();
|
||||
for (NoisePickSpread.Row row : rows) {
|
||||
int baseline = rowTop + (rowHeight + text.getAscent() - text.getDescent()) / 2;
|
||||
int arc = StudioTheme.scale(3);
|
||||
canvas.setColor(new Color(row.option().color()));
|
||||
canvas.fillRoundRect(x, rowTop + (rowHeight - swatchSize) / 2, swatchSize, swatchSize, arc, arc);
|
||||
canvas.setFont(StudioTheme.font());
|
||||
canvas.setColor(StudioTheme.TEXT);
|
||||
int nameLeft = x + swatchSize + StudioTheme.scale(7);
|
||||
canvas.drawString(fit(canvas, distinctName(row, rows), nameRight - nameLeft), nameLeft, baseline);
|
||||
canvas.setColor(StudioTheme.SECONDARY_TEXT);
|
||||
drawRightAligned(canvas, percent(row.weightShare()), weightRight, baseline);
|
||||
boolean farFromWeight = row.isFarBelowWeight() || row.isFarAboveWeight();
|
||||
canvas.setColor(farFromWeight ? StudioTheme.WARNING : StudioTheme.TEXT);
|
||||
drawRightAligned(canvas, actualText(row), actualRight, baseline);
|
||||
rowTop += rowHeight;
|
||||
}
|
||||
}
|
||||
|
||||
private int rowHeight() {
|
||||
return textMetrics().getHeight() + StudioTheme.scale(4);
|
||||
}
|
||||
|
||||
private FontMetrics textMetrics() {
|
||||
return getFontMetrics(StudioTheme.font());
|
||||
}
|
||||
|
||||
private FontMetrics smallMetrics() {
|
||||
return getFontMetrics(StudioTheme.smallFont());
|
||||
}
|
||||
|
||||
private FontMetrics sectionMetrics() {
|
||||
return getFontMetrics(StudioTheme.sectionFont());
|
||||
}
|
||||
|
||||
private static void drawSectionTitle(Graphics2D canvas, String title, int x, int y) {
|
||||
canvas.setFont(StudioTheme.sectionFont());
|
||||
canvas.setColor(StudioTheme.MUTED_TEXT);
|
||||
canvas.drawString(title.toUpperCase(Locale.ROOT), x, y);
|
||||
}
|
||||
|
||||
private static void drawRightAligned(Graphics2D canvas, String text, int right, int y) {
|
||||
canvas.drawString(text, right - canvas.getFontMetrics().stringWidth(text), y);
|
||||
}
|
||||
|
||||
// Two biomes in different folders can share a name, so their keys tell them apart.
|
||||
private static String distinctName(NoisePickSpread.Row row, List<NoisePickSpread.Row> rows) {
|
||||
String name = row.option().name();
|
||||
boolean shared = rows.stream().anyMatch(other -> other != row && other.option().name().equals(name));
|
||||
return shared ? name + " (" + row.option().key() + ")" : name;
|
||||
}
|
||||
|
||||
private static String actualText(NoisePickSpread.Row row) {
|
||||
if (row.isFarBelowWeight()) {
|
||||
return "▼ " + percent(row.actualShare());
|
||||
}
|
||||
if (row.isFarAboveWeight()) {
|
||||
return "▲ " + percent(row.actualShare());
|
||||
}
|
||||
return percent(row.actualShare());
|
||||
}
|
||||
|
||||
static String percent(double share) {
|
||||
if (share > 0D && share < 0.001D) {
|
||||
return "<0.1%";
|
||||
}
|
||||
return String.format(Locale.ROOT, "%.1f%%", share * 100D);
|
||||
}
|
||||
|
||||
private static String fit(Graphics2D canvas, String text, int maxWidth) {
|
||||
FontMetrics metrics = canvas.getFontMetrics();
|
||||
if (metrics.stringWidth(text) <= maxWidth) {
|
||||
return text;
|
||||
}
|
||||
String shortened = text;
|
||||
while (shortened.length() > 1 && metrics.stringWidth(shortened + "…") > maxWidth) {
|
||||
shortened = shortened.substring(0, shortened.length() - 1);
|
||||
}
|
||||
return shortened + "…";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.gui;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* How a noise picks one of several regions or biomes, measured against the noise values in view: rarity splits the
|
||||
* noise from 0 to 1 into one band per option, and each spot gets the option whose band its noise value falls in.
|
||||
*/
|
||||
final class NoisePickSpread {
|
||||
static final int HISTOGRAM_BINS = 50;
|
||||
|
||||
private final String title;
|
||||
private final List<Band> bands;
|
||||
private final long[] histogram = new long[HISTOGRAM_BINS];
|
||||
private final long[] samplesByBand;
|
||||
private long sampleCount;
|
||||
private long belowZeroCount;
|
||||
private long aboveOneCount;
|
||||
|
||||
private NoisePickSpread(String title, List<Band> bands) {
|
||||
this.title = title;
|
||||
this.bands = List.copyOf(bands);
|
||||
this.samplesByBand = new long[bands.size()];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param legacyRarity whether the dimension splits the noise the legacy way
|
||||
*/
|
||||
static NoisePickSpread of(String title, List<Option> options, boolean legacyRarity) {
|
||||
return new NoisePickSpread(title, legacyRarity ? legacyBands(options) : bands(options));
|
||||
}
|
||||
|
||||
void add(double noise) {
|
||||
if (bands.isEmpty() || !Double.isFinite(noise)) {
|
||||
return;
|
||||
}
|
||||
sampleCount++;
|
||||
if (noise < 0D) {
|
||||
belowZeroCount++;
|
||||
} else if (noise > 1D) {
|
||||
aboveOneCount++;
|
||||
}
|
||||
histogram[Math.clamp((int) (noise * HISTOGRAM_BINS), 0, HISTOGRAM_BINS - 1)]++;
|
||||
samplesByBand[bandIndexOf(noise)]++;
|
||||
}
|
||||
|
||||
String title() {
|
||||
return title;
|
||||
}
|
||||
|
||||
List<Band> bands() {
|
||||
return bands;
|
||||
}
|
||||
|
||||
long[] histogram() {
|
||||
return histogram;
|
||||
}
|
||||
|
||||
long sampleCount() {
|
||||
return sampleCount;
|
||||
}
|
||||
|
||||
double belowZeroShare() {
|
||||
return share(belowZeroCount);
|
||||
}
|
||||
|
||||
double aboveOneShare() {
|
||||
return share(aboveOneCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Every option once, with its bands' widths and samples added up, in the order the noise meets them.
|
||||
*/
|
||||
List<Row> rows() {
|
||||
Map<String, Row> rows = new LinkedHashMap<>();
|
||||
for (int index = 0; index < bands.size(); index++) {
|
||||
Band band = bands.get(index);
|
||||
Row row = new Row(band.option(), band.width(), share(samplesByBand[index]));
|
||||
rows.merge(band.option().key(), row, Row::plus);
|
||||
}
|
||||
return new ArrayList<>(rows.values());
|
||||
}
|
||||
|
||||
private double share(long count) {
|
||||
return sampleCount == 0 ? 0D : count / (double) sampleCount;
|
||||
}
|
||||
|
||||
// The engine gives values below 0 the first option and values above 1 the last, the same as these bands.
|
||||
private int bandIndexOf(double noise) {
|
||||
for (int index = 0; index < bands.size(); index++) {
|
||||
if (noise <= bands.get(index).toNoise()) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return bands.size() - 1;
|
||||
}
|
||||
|
||||
// Mirrors IRare.pick: each option gets a share of the noise in proportion to one over its rarity.
|
||||
private static List<Band> bands(List<Option> options) {
|
||||
double total = 0D;
|
||||
for (Option option : options) {
|
||||
total += 1D / option.rarity();
|
||||
}
|
||||
List<Band> bands = new ArrayList<>();
|
||||
double from = 0D;
|
||||
for (Option option : options) {
|
||||
double to = options.size() == 1 ? 1D : from + (1D / option.rarity()) / total;
|
||||
bands.add(new Band(option, from, to));
|
||||
from = to;
|
||||
}
|
||||
return bands;
|
||||
}
|
||||
|
||||
// Mirrors IRare.pickLegacy: each option gets a share in proportion to the total rarity minus its own.
|
||||
private static List<Band> legacyBands(List<Option> options) {
|
||||
if (options.size() <= 1) {
|
||||
return bands(options);
|
||||
}
|
||||
double totalRarity = 0D;
|
||||
for (Option option : options) {
|
||||
totalRarity += option.rarity();
|
||||
}
|
||||
double whole = totalRarity * (options.size() - 1);
|
||||
List<Band> bands = new ArrayList<>();
|
||||
double from = 0D;
|
||||
double buffer = 0D;
|
||||
for (Option option : options) {
|
||||
buffer += totalRarity - option.rarity();
|
||||
double to = buffer / whole;
|
||||
bands.add(new Band(option, from, to));
|
||||
from = to;
|
||||
}
|
||||
return bands;
|
||||
}
|
||||
|
||||
/**
|
||||
* A region or biome the noise can pick.
|
||||
*/
|
||||
record Option(String key, String name, int color, int rarity) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The noise values from {@code fromNoise} to {@code toNoise} that pick the option.
|
||||
*/
|
||||
record Band(Option option, double fromNoise, double toNoise) {
|
||||
double width() {
|
||||
return toNoise - fromNoise;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param weightShare how much of the noise from 0 to 1 picks the option
|
||||
* @param actualShare how much of the noise in view picks the option
|
||||
*/
|
||||
record Row(Option option, double weightShare, double actualShare) {
|
||||
private static final double FAR_FROM_WEIGHT_FACTOR = 2D;
|
||||
|
||||
Row plus(Row other) {
|
||||
return new Row(option, weightShare + other.weightShare, actualShare + other.actualShare);
|
||||
}
|
||||
|
||||
boolean isFarBelowWeight() {
|
||||
return actualShare * FAR_FROM_WEIGHT_FACTOR < weightShare;
|
||||
}
|
||||
|
||||
boolean isFarAboveWeight() {
|
||||
return actualShare > weightShare * FAR_FROM_WEIGHT_FACTOR;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,10 @@
|
||||
package com.volmit.iris.core.gui;
|
||||
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.gui.NoisePickSpread.Option;
|
||||
import com.volmit.iris.core.gui.RegionHeightCoverage.BiomeRange;
|
||||
import com.volmit.iris.core.gui.RegionNavigator.RegionRow;
|
||||
import com.volmit.iris.core.gui.components.PickDisplay;
|
||||
import com.volmit.iris.core.gui.components.RenderType;
|
||||
import com.volmit.iris.engine.IrisComplex;
|
||||
import com.volmit.iris.engine.framework.Engine;
|
||||
@@ -30,7 +32,11 @@ import com.volmit.iris.engine.object.IrisRegion;
|
||||
|
||||
import java.awt.EventQueue;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
@@ -38,19 +44,21 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* The region view's explorer: choosing a region fills the map with its biomes and measures which of them covers which
|
||||
* ground height in view. Measuring runs off the Swing thread and only the newest request counts.
|
||||
* The region view's explorer: shows how the region noise picks the regions, and for a chosen region fills the map with
|
||||
* its biomes and shows how they are picked. Measuring what is in view runs off the Swing thread and only the newest
|
||||
* request counts.
|
||||
*/
|
||||
final class RegionExplorer {
|
||||
private static final int GROUND_SAMPLES_PER_SIDE = 96;
|
||||
private static final int SAMPLES_PER_SIDE = 96;
|
||||
private static final List<InferredType> PLACED_TYPES = List.of(InferredType.LAND, InferredType.SHORE, InferredType.SEA);
|
||||
|
||||
private final Supplier<Engine> engineSource;
|
||||
private final Consumer<String> onPreviewChanged;
|
||||
private final RegionNavigator navigator = new RegionNavigator(this::choose);
|
||||
private final RegionInspectorPanel inspector = new RegionInspectorPanel();
|
||||
private final RegionInspectorPanel inspector;
|
||||
private final ThreadPoolExecutor measurer = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
|
||||
new ArrayBlockingQueue<>(1), RegionExplorer::daemonThread, new ThreadPoolExecutor.DiscardOldestPolicy());
|
||||
private final AtomicLong measurementGeneration = new AtomicLong();
|
||||
@@ -60,10 +68,12 @@ final class RegionExplorer {
|
||||
|
||||
/**
|
||||
* @param onPreviewChanged receives the region to fill the map with, or null to show every region again
|
||||
* @param onDisplayChosen receives how the map should draw the picks
|
||||
*/
|
||||
RegionExplorer(Supplier<Engine> engineSource, Consumer<String> onPreviewChanged) {
|
||||
RegionExplorer(Supplier<Engine> engineSource, Consumer<String> onPreviewChanged, Consumer<PickDisplay> onDisplayChosen) {
|
||||
this.engineSource = engineSource;
|
||||
this.onPreviewChanged = onPreviewChanged;
|
||||
this.inspector = new RegionInspectorPanel(onDisplayChosen);
|
||||
}
|
||||
|
||||
RegionNavigator navigator() {
|
||||
@@ -81,18 +91,17 @@ final class RegionExplorer {
|
||||
}
|
||||
List<RegionRow> rows = new ArrayList<>();
|
||||
for (IrisRegion region : engine.getData().getRegionLoader().loadAll(engine.getDimension().getRegions())) {
|
||||
rows.add(new RegionRow(region.getLoadKey(), region.getName(),
|
||||
region.getColor(engine.getComplex(), RenderType.REGION).getRGB() & 0xFFFFFF));
|
||||
rows.add(new RegionRow(region.getLoadKey(), region.getName(), regionColor(engine, region)));
|
||||
}
|
||||
navigator.showRegions(rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Measures the chosen region against the ground in this area, unless that was already done.
|
||||
* Measures the regions, or the chosen region's biomes, against this area, unless that was already done.
|
||||
*/
|
||||
void measure(MapCensus.ViewArea area) {
|
||||
latestArea = area;
|
||||
if (chosenRegionKey == null || area == null || area.equals(measuredArea)) {
|
||||
if (area == null || area.equals(measuredArea)) {
|
||||
return;
|
||||
}
|
||||
measuredArea = area;
|
||||
@@ -126,11 +135,7 @@ final class RegionExplorer {
|
||||
measuredArea = null;
|
||||
measurementGeneration.incrementAndGet();
|
||||
onPreviewChanged.accept(regionKey);
|
||||
if (regionKey == null) {
|
||||
inspector.showNothingChosen();
|
||||
return;
|
||||
}
|
||||
inspector.showMeasuring(regionName(regionKey));
|
||||
inspector.showMeasuring(regionKey == null ? "Regions" : regionName(regionKey));
|
||||
measure(latestArea);
|
||||
}
|
||||
|
||||
@@ -144,34 +149,144 @@ final class RegionExplorer {
|
||||
if (!Objects.equals(measurement.regionKey(), chosenRegionKey)) {
|
||||
return;
|
||||
}
|
||||
if (measurement.coverage() == null) {
|
||||
inspector.showBiomeTerrain(measurement.regionName());
|
||||
} else {
|
||||
inspector.showCoverage(measurement.regionName(), measurement.coverage());
|
||||
switch (measurement) {
|
||||
case RegionPicks picks -> inspector.showRegionPicks(picks.spread(), picks.notes());
|
||||
case BiomePicks picks -> inspector.showBiomePicks(picks.regionName(), picks.spreads(), picks.notes());
|
||||
case HeightPicks picks -> inspector.showCoverage(picks.regionName(), picks.coverage(), picks.notes());
|
||||
}
|
||||
}
|
||||
|
||||
private Measurement measureSafely(String regionKey, MapCensus.ViewArea area) {
|
||||
try {
|
||||
return measureRegion(regionKey, area);
|
||||
return measurePicks(regionKey, area);
|
||||
} catch (Throwable error) {
|
||||
Iris.reportError(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Measurement measureRegion(String regionKey, MapCensus.ViewArea area) {
|
||||
private Measurement measurePicks(String regionKey, MapCensus.ViewArea area) {
|
||||
Engine engine = engineSource.get();
|
||||
IrisComplex complex = engine == null ? null : engine.getComplex();
|
||||
IrisRegion region = complex == null ? null : engine.getData().getRegionLoader().load(regionKey);
|
||||
if (complex == null) {
|
||||
return null;
|
||||
}
|
||||
if (regionKey == null) {
|
||||
return measureRegionPicks(engine, complex, area);
|
||||
}
|
||||
IrisRegion region = engine.getData().getRegionLoader().load(regionKey);
|
||||
if (region == null) {
|
||||
return null;
|
||||
}
|
||||
if (!complex.isHybridTerrain()) {
|
||||
return new Measurement(regionKey, region.getName(), null);
|
||||
return complex.isHybridTerrain()
|
||||
? measureHeightPicks(engine, complex, region, area)
|
||||
: measureBiomePicks(engine, complex, region, area);
|
||||
}
|
||||
|
||||
private static RegionPicks measureRegionPicks(Engine engine, IrisComplex complex, MapCensus.ViewArea area) {
|
||||
List<Option> options = complex.noisePickedRegions().stream()
|
||||
.map(region -> new Option(region.getLoadKey(), region.getName(), regionColor(engine, region), region.getRarity()))
|
||||
.toList();
|
||||
NoisePickSpread spread = NoisePickSpread.of("Region noise in view", options, complex.isLegacyRarity());
|
||||
forEachSample(area, (x, z) -> spread.add(complex.getRegionStyleStream().get(x, z)));
|
||||
List<String> notes = new ArrayList<>();
|
||||
addSpreadNotes(notes, spread, "region");
|
||||
addRarityNotes(notes, complex, List.of(spread));
|
||||
return new RegionPicks(spread, notes);
|
||||
}
|
||||
|
||||
private static BiomePicks measureBiomePicks(Engine engine, IrisComplex complex, IrisRegion region, MapCensus.ViewArea area) {
|
||||
Map<InferredType, NoisePickSpread> spreads = new LinkedHashMap<>();
|
||||
for (InferredType type : PLACED_TYPES) {
|
||||
List<Option> options = complex.noisePickedBiomes(region, type).stream()
|
||||
.map(biome -> biomeOption(engine, biome))
|
||||
.toList();
|
||||
if (!options.isEmpty()) {
|
||||
spreads.put(type, NoisePickSpread.of(typeName(type) + " biome noise in view", options, complex.isLegacyRarity()));
|
||||
}
|
||||
}
|
||||
RegionHeightCoverage coverage = RegionHeightCoverage.measure(biomeRanges(engine, region), groundHeights(complex, area));
|
||||
return new Measurement(regionKey, region.getName(), coverage);
|
||||
forEachSample(area, (x, z) -> {
|
||||
IrisComplex.BiomePick pick = complex.surfacePickAsIfIn(region, x, z);
|
||||
NoisePickSpread spread = spreads.get(pick.type());
|
||||
if (spread != null) {
|
||||
spread.add(complex.biomePickNoise(region, pick.type(), x, z));
|
||||
}
|
||||
});
|
||||
List<String> notes = new ArrayList<>();
|
||||
spreads.forEach((type, spread) -> addSpreadNotes(notes, spread, typeName(type).toLowerCase(Locale.ROOT) + " biome"));
|
||||
addChildBiomeNote(notes, complex, region);
|
||||
addRarityNotes(notes, complex, spreads.values());
|
||||
return new BiomePicks(region.getLoadKey(), region.getName(), List.copyOf(spreads.values()), notes);
|
||||
}
|
||||
|
||||
private static HeightPicks measureHeightPicks(Engine engine, IrisComplex complex, IrisRegion region, MapCensus.ViewArea area) {
|
||||
RegionHeightCoverage coverage = RegionHeightCoverage.measure(biomeRanges(engine, region), pickingHeights(complex, area));
|
||||
List<String> notes = new ArrayList<>(coverage.notes());
|
||||
notes.add("Noise shows the height biomes are picked at: the overlay noise plus the dimension's biome placement "
|
||||
+ "jitter. Where ranges share a height, each biome's competition noise, divided by its rarity, decides.");
|
||||
return new HeightPicks(region.getLoadKey(), region.getName(), coverage, notes);
|
||||
}
|
||||
|
||||
private static void addSpreadNotes(List<String> notes, NoisePickSpread spread, String noun) {
|
||||
List<NoisePickSpread.Row> rows = spread.rows();
|
||||
if (spread.sampleCount() == 0) {
|
||||
notes.add("No " + noun + " is picked in view.");
|
||||
return;
|
||||
}
|
||||
if (rows.size() == 1) {
|
||||
notes.add(rows.getFirst().option().name() + " is the only " + noun + ", so the " + noun + " noise has nothing to pick.");
|
||||
return;
|
||||
}
|
||||
if (spread.belowZeroShare() > 0D) {
|
||||
notes.add(NoisePickChart.percent(spread.belowZeroShare()) + " of the " + noun + " noise in view is below 0, so "
|
||||
+ rows.getFirst().option().name() + " gets it.");
|
||||
}
|
||||
if (spread.aboveOneShare() > 0D) {
|
||||
notes.add(NoisePickChart.percent(spread.aboveOneShare()) + " of the " + noun + " noise in view is above 1, so "
|
||||
+ rows.getLast().option().name() + " gets it.");
|
||||
}
|
||||
List<String> starved = rows.stream().filter(NoisePickSpread.Row::isFarBelowWeight).map(row -> row.option().name()).toList();
|
||||
if (!starved.isEmpty()) {
|
||||
notes.add(String.join(", ", starved) + (starved.size() == 1 ? " gets" : " get")
|
||||
+ " under half " + (starved.size() == 1 ? "its" : "their") + " weight in view (▼): the "
|
||||
+ noun + " noise rarely reaches " + (starved.size() == 1 ? "its band." : "their bands."));
|
||||
}
|
||||
}
|
||||
|
||||
private static void addChildBiomeNote(List<String> notes, IrisComplex complex, IrisRegion region) {
|
||||
String parents = PLACED_TYPES.stream()
|
||||
.flatMap(type -> complex.noisePickedBiomes(region, type).stream())
|
||||
.filter(biome -> !biome.getChildren().isEmpty())
|
||||
.map(IrisBiome::getName)
|
||||
.distinct()
|
||||
.collect(Collectors.joining(", "));
|
||||
if (!parents.isEmpty()) {
|
||||
notes.add(parents + " then hand some spots to child biomes by a noise of their own.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void addRarityNotes(List<String> notes, IrisComplex complex, Collection<NoisePickSpread> spreads) {
|
||||
if (spreads.stream().allMatch(spread -> spread.bands().size() <= 1)) {
|
||||
return;
|
||||
}
|
||||
notes.add(complex.isLegacyRarity()
|
||||
? "Legacy rarity: each weight is the total rarity minus the option's own."
|
||||
: "Each weight is one over the rarity, so rarity 2 gets half the noise of rarity 1.");
|
||||
notes.add("Measured on a " + SAMPLES_PER_SIDE + " × " + SAMPLES_PER_SIDE + " grid over the map in view.");
|
||||
}
|
||||
|
||||
private static Option biomeOption(Engine engine, IrisBiome biome) {
|
||||
return new Option(biome.getLoadKey(), biome.getName(), biome.getColor(engine, RenderType.BIOME).getRGB() & 0xFFFFFF,
|
||||
biome.getRarity());
|
||||
}
|
||||
|
||||
private static int regionColor(Engine engine, IrisRegion region) {
|
||||
return region.getColor(engine.getComplex(), RenderType.REGION).getRGB() & 0xFFFFFF;
|
||||
}
|
||||
|
||||
private static String typeName(InferredType type) {
|
||||
String name = type.name().toLowerCase(Locale.ROOT);
|
||||
return Character.toUpperCase(name.charAt(0)) + name.substring(1);
|
||||
}
|
||||
|
||||
private static List<BiomeRange> biomeRanges(Engine engine, IrisRegion region) {
|
||||
@@ -189,19 +304,22 @@ final class RegionExplorer {
|
||||
return ranges;
|
||||
}
|
||||
|
||||
// In hybrid terrain the overlay noise is the ground's height above the fluid, before any biome adds its own terrain.
|
||||
private static int[] groundHeights(IrisComplex complex, MapCensus.ViewArea area) {
|
||||
int[] heights = new int[GROUND_SAMPLES_PER_SIDE * GROUND_SAMPLES_PER_SIDE];
|
||||
double stepX = (area.maximumX() - area.minimumX()) / GROUND_SAMPLES_PER_SIDE;
|
||||
double stepZ = (area.maximumZ() - area.minimumZ()) / GROUND_SAMPLES_PER_SIDE;
|
||||
for (int row = 0; row < GROUND_SAMPLES_PER_SIDE; row++) {
|
||||
for (int column = 0; column < GROUND_SAMPLES_PER_SIDE; column++) {
|
||||
double x = area.minimumX() + (column + 0.5D) * stepX;
|
||||
double z = area.minimumZ() + (row + 0.5D) * stepZ;
|
||||
heights[row * GROUND_SAMPLES_PER_SIDE + column] = (int) Math.round(complex.getOverlayStream().get(x, z));
|
||||
// HYBRID terrain picks biomes by the overlay noise plus the placement jitter, in blocks above the fluid.
|
||||
private static int[] pickingHeights(IrisComplex complex, MapCensus.ViewArea area) {
|
||||
int[] heights = new int[SAMPLES_PER_SIDE * SAMPLES_PER_SIDE];
|
||||
int[] next = {0};
|
||||
forEachSample(area, (x, z) -> heights[next[0]++] = (int) Math.round(complex.getPlacementHeightStream().get(x, z)));
|
||||
return heights;
|
||||
}
|
||||
|
||||
private static void forEachSample(MapCensus.ViewArea area, SampleVisitor visitor) {
|
||||
double stepX = (area.maximumX() - area.minimumX()) / SAMPLES_PER_SIDE;
|
||||
double stepZ = (area.maximumZ() - area.minimumZ()) / SAMPLES_PER_SIDE;
|
||||
for (int row = 0; row < SAMPLES_PER_SIDE; row++) {
|
||||
for (int column = 0; column < SAMPLES_PER_SIDE; column++) {
|
||||
visitor.visit(area.minimumX() + (column + 0.5D) * stepX, area.minimumZ() + (row + 0.5D) * stepZ);
|
||||
}
|
||||
}
|
||||
return heights;
|
||||
}
|
||||
|
||||
private static Thread daemonThread(Runnable task) {
|
||||
@@ -210,9 +328,30 @@ final class RegionExplorer {
|
||||
return thread;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface SampleVisitor {
|
||||
void visit(double x, double z);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param coverage how the region's biomes cover the ground in view, or null when the dimension uses biome terrain
|
||||
* What was measured for the region view; the region key is null when every region was measured.
|
||||
*/
|
||||
private record Measurement(String regionKey, String regionName, RegionHeightCoverage coverage) {
|
||||
private sealed interface Measurement permits RegionPicks, BiomePicks, HeightPicks {
|
||||
String regionKey();
|
||||
}
|
||||
|
||||
private record RegionPicks(NoisePickSpread spread, List<String> notes) implements Measurement {
|
||||
@Override
|
||||
public String regionKey() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private record BiomePicks(String regionKey, String regionName, List<NoisePickSpread> spreads, List<String> notes)
|
||||
implements Measurement {
|
||||
}
|
||||
|
||||
private record HeightPicks(String regionKey, String regionName, RegionHeightCoverage coverage, List<String> notes)
|
||||
implements Measurement {
|
||||
}
|
||||
}
|
||||
@@ -208,7 +208,7 @@ final class RegionHeightChart extends JComponent {
|
||||
canvas.drawString(label, x - metrics.stringWidth(label) / 2, axisTop + StudioTheme.scale(4) + metrics.getAscent());
|
||||
}
|
||||
canvas.setColor(StudioTheme.MUTED_TEXT);
|
||||
canvas.drawString("Ground in view", StudioTheme.scale(PADDING), axisTop - StudioTheme.scale(4));
|
||||
canvas.drawString("Heights in view", StudioTheme.scale(PADDING), axisTop - StudioTheme.scale(4));
|
||||
}
|
||||
|
||||
private int tickStep() {
|
||||
|
||||
@@ -18,88 +18,136 @@
|
||||
|
||||
package com.volmit.iris.core.gui;
|
||||
|
||||
import com.volmit.iris.core.gui.components.PickDisplay;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.BoxLayout;
|
||||
import javax.swing.ButtonGroup;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JTextArea;
|
||||
import javax.swing.JToggleButton;
|
||||
import javax.swing.Scrollable;
|
||||
import javax.swing.SwingConstants;
|
||||
import javax.swing.text.DefaultCaret;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.CardLayout;
|
||||
import java.awt.Component;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.GridLayout;
|
||||
import java.awt.Rectangle;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* The region view's inspector: for the chosen region, which of its biomes covers which ground height, and what that
|
||||
* means in words.
|
||||
* The region view's inspector: how the noise picks the regions, or for a chosen region how its biomes are picked,
|
||||
* measured against what is in view and put in words.
|
||||
*/
|
||||
final class RegionInspectorPanel extends JPanel {
|
||||
private static final long serialVersionUID = -2874105263188240317L;
|
||||
private static final String MESSAGE_CARD = "message";
|
||||
private static final String COVERAGE_CARD = "coverage";
|
||||
private static final String DETAILS_CARD = "details";
|
||||
private static final int PANEL_WIDTH = 330;
|
||||
private static final String PARAGRAPH_BREAK = "\n\n";
|
||||
|
||||
private final JLabel title = new JLabel();
|
||||
private final JLabel detail = new JLabel();
|
||||
private final Map<PickDisplay, JToggleButton> displayButtons = new EnumMap<>(PickDisplay.class);
|
||||
private final CardLayout cards = new CardLayout();
|
||||
private final JPanel cardPanel = new JPanel(cards);
|
||||
private final JTextArea message = wrappingText();
|
||||
private final RegionHeightChart chart = new RegionHeightChart();
|
||||
private final JTextArea notes = wrappingText();
|
||||
private final JPanel details = new ViewportWidthPanel();
|
||||
private final RegionHeightChart heightChart = new RegionHeightChart();
|
||||
|
||||
RegionInspectorPanel() {
|
||||
/**
|
||||
* @param onDisplayChosen receives how the map should draw the picks
|
||||
*/
|
||||
RegionInspectorPanel(Consumer<PickDisplay> onDisplayChosen) {
|
||||
super(new BorderLayout());
|
||||
setPreferredSize(StudioTheme.scaledSize(PANEL_WIDTH, 0));
|
||||
setBackground(StudioTheme.PANEL_BACKGROUND);
|
||||
add(buildHeader(), BorderLayout.NORTH);
|
||||
add(buildHeader(onDisplayChosen), BorderLayout.NORTH);
|
||||
cardPanel.setOpaque(false);
|
||||
cardPanel.add(buildMessageCard(), MESSAGE_CARD);
|
||||
cardPanel.add(StudioToolbar.scroll(buildCoverageCard()), COVERAGE_CARD);
|
||||
details.setLayout(new BoxLayout(details, BoxLayout.Y_AXIS));
|
||||
details.setBackground(StudioTheme.PANEL_BACKGROUND);
|
||||
heightChart.setAlignmentX(LEFT_ALIGNMENT);
|
||||
cardPanel.add(StudioToolbar.scroll(details), DETAILS_CARD);
|
||||
add(cardPanel, BorderLayout.CENTER);
|
||||
showNothingChosen();
|
||||
showMeasuring("Regions");
|
||||
}
|
||||
|
||||
void showNothingChosen() {
|
||||
showHeader("Regions", "Every region in its own colour");
|
||||
showMessage("Pick a region on the left to fill the map with its biomes and see which biome covers which height.");
|
||||
void showMeasuring(String name) {
|
||||
showHeader(name, "Measuring what is in view…");
|
||||
showMessage("");
|
||||
}
|
||||
|
||||
void showMeasuring(String regionName) {
|
||||
showHeader(regionName, "Measuring the ground in view…");
|
||||
void showRegionPicks(NoisePickSpread spread, List<String> notes) {
|
||||
showHeader("Regions", "Region noise, split by rarity");
|
||||
nameFirstDisplay("Regions");
|
||||
showDetails(List.of(new NoisePickChart(spread, "Regions")), notes);
|
||||
}
|
||||
|
||||
void showBiomeTerrain(String regionName) {
|
||||
showHeader(regionName, "Biome terrain");
|
||||
showMessage("This dimension uses BIOME terrain, where biomes are placed by noise and ignore height. "
|
||||
+ "Set the dimension's terrainMode to HYBRID to choose biomes by the height of the ground.");
|
||||
void showBiomePicks(String regionName, List<NoisePickSpread> spreads, List<String> notes) {
|
||||
showHeader(regionName, "Biome terrain · biome noise, split by rarity");
|
||||
nameFirstDisplay("Biomes");
|
||||
showDetails(spreads.stream().map(spread -> (JComponent) new NoisePickChart(spread, "Biomes")).toList(), notes);
|
||||
}
|
||||
|
||||
void showCoverage(String regionName, RegionHeightCoverage coverage) {
|
||||
showHeader(regionName, "Hybrid terrain · biomes by ground height");
|
||||
chart.show(coverage);
|
||||
notes.setText(notesText(coverage.notes()));
|
||||
cards.show(cardPanel, COVERAGE_CARD);
|
||||
void showCoverage(String regionName, RegionHeightCoverage coverage, List<String> notes) {
|
||||
showHeader(regionName, "Hybrid terrain · biomes by height");
|
||||
nameFirstDisplay("Biomes");
|
||||
heightChart.show(coverage);
|
||||
showDetails(List.of(heightChart), notes);
|
||||
}
|
||||
|
||||
private JPanel buildHeader() {
|
||||
JPanel header = new JPanel(new GridLayout(0, 1, 0, StudioTheme.scale(2)));
|
||||
private JPanel buildHeader(Consumer<PickDisplay> onDisplayChosen) {
|
||||
JPanel header = new JPanel(new GridLayout(0, 1, 0, StudioTheme.scale(6)));
|
||||
header.setOpaque(false);
|
||||
header.setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createMatteBorder(0, 0, 1, 0, StudioTheme.SEPARATOR),
|
||||
StudioTheme.emptyBorder(10, 16, 9, 16)));
|
||||
StudioTheme.emptyBorder(10, 16, 12, 16)));
|
||||
title.setFont(StudioTheme.titleFont());
|
||||
detail.setFont(StudioTheme.smallFont());
|
||||
detail.setForeground(StudioTheme.MUTED_TEXT);
|
||||
header.add(title);
|
||||
header.add(detail);
|
||||
header.add(sectionLabel("Show"));
|
||||
header.add(buildDisplayButtons(onDisplayChosen));
|
||||
return header;
|
||||
}
|
||||
|
||||
private JPanel buildDisplayButtons(Consumer<PickDisplay> onDisplayChosen) {
|
||||
JPanel row = new JPanel(new GridLayout(1, 0, StudioTheme.scale(4), 0));
|
||||
row.setOpaque(false);
|
||||
ButtonGroup group = new ButtonGroup();
|
||||
for (PickDisplay display : PickDisplay.values()) {
|
||||
JToggleButton button = new JToggleButton(displayName(display));
|
||||
button.setFocusable(false);
|
||||
button.setSelected(display == PickDisplay.PICKS);
|
||||
button.addActionListener(event -> onDisplayChosen.accept(display));
|
||||
group.add(button);
|
||||
row.add(button);
|
||||
displayButtons.put(display, button);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
private static String displayName(PickDisplay display) {
|
||||
return switch (display) {
|
||||
case PICKS -> "Regions";
|
||||
case NOISE -> "Noise";
|
||||
case BOTH -> "Both";
|
||||
};
|
||||
}
|
||||
|
||||
private void nameFirstDisplay(String name) {
|
||||
displayButtons.get(PickDisplay.PICKS).setText(name);
|
||||
}
|
||||
|
||||
private JPanel buildMessageCard() {
|
||||
JPanel card = new JPanel(new BorderLayout());
|
||||
card.setOpaque(false);
|
||||
@@ -108,20 +156,8 @@ final class RegionInspectorPanel extends JPanel {
|
||||
return card;
|
||||
}
|
||||
|
||||
private JPanel buildCoverageCard() {
|
||||
JPanel card = new ViewportWidthPanel();
|
||||
card.setLayout(new BoxLayout(card, BoxLayout.Y_AXIS));
|
||||
card.setBackground(StudioTheme.PANEL_BACKGROUND);
|
||||
chart.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
notes.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
notes.setBorder(StudioTheme.emptyBorder(0, 16, 12, 16));
|
||||
card.add(chart);
|
||||
card.add(notes);
|
||||
return card;
|
||||
}
|
||||
|
||||
private void showHeader(String regionName, String detailText) {
|
||||
title.setText(regionName);
|
||||
private void showHeader(String name, String detailText) {
|
||||
title.setText(name);
|
||||
detail.setText(detailText);
|
||||
}
|
||||
|
||||
@@ -130,11 +166,27 @@ final class RegionInspectorPanel extends JPanel {
|
||||
cards.show(cardPanel, MESSAGE_CARD);
|
||||
}
|
||||
|
||||
private static String notesText(List<String> lines) {
|
||||
if (lines.isEmpty()) {
|
||||
return "Every ground height in view has a biome, and no two ranged biomes share one.";
|
||||
private void showDetails(List<JComponent> charts, List<String> notes) {
|
||||
details.removeAll();
|
||||
charts.forEach(details::add);
|
||||
if (!notes.isEmpty()) {
|
||||
JTextArea notesText = wrappingText();
|
||||
notesText.setAlignmentX(LEFT_ALIGNMENT);
|
||||
notesText.setBorder(StudioTheme.emptyBorder(4, 16, 12, 16));
|
||||
notesText.setText(String.join(PARAGRAPH_BREAK, notes));
|
||||
details.add(notesText);
|
||||
}
|
||||
return String.join(PARAGRAPH_BREAK, lines);
|
||||
details.revalidate();
|
||||
details.repaint();
|
||||
details.scrollRectToVisible(new Rectangle());
|
||||
cards.show(cardPanel, DETAILS_CARD);
|
||||
}
|
||||
|
||||
private static JLabel sectionLabel(String text) {
|
||||
JLabel label = new JLabel(text.toUpperCase(Locale.ROOT));
|
||||
label.setForeground(StudioTheme.MUTED_TEXT);
|
||||
label.setFont(StudioTheme.sectionFont());
|
||||
return label;
|
||||
}
|
||||
|
||||
private static JTextArea wrappingText() {
|
||||
@@ -142,6 +194,7 @@ final class RegionInspectorPanel extends JPanel {
|
||||
text.setEditable(false);
|
||||
text.setFocusable(false);
|
||||
text.setOpaque(false);
|
||||
((DefaultCaret) text.getCaret()).setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
|
||||
text.setLineWrap(true);
|
||||
text.setWrapStyleWord(true);
|
||||
text.setFont(StudioTheme.font());
|
||||
@@ -150,7 +203,7 @@ final class RegionInspectorPanel extends JPanel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Follows the scroll pane's width, so the chart stretches to it and the notes wrap inside it.
|
||||
* Follows the scroll pane's width, so the charts stretch to it and the notes wrap inside it.
|
||||
*/
|
||||
private static final class ViewportWidthPanel extends JPanel implements Scrollable {
|
||||
private static final long serialVersionUID = 6086542174870301352L;
|
||||
|
||||
@@ -20,6 +20,7 @@ package com.volmit.iris.core.gui.components;
|
||||
|
||||
import com.volmit.iris.engine.IrisComplex;
|
||||
import com.volmit.iris.engine.framework.Engine;
|
||||
import com.volmit.iris.engine.object.InferredType;
|
||||
import com.volmit.iris.engine.object.IrisBiome;
|
||||
import com.volmit.iris.engine.object.IrisBiomeGeneratorLink;
|
||||
import com.volmit.iris.engine.object.IrisDimension;
|
||||
@@ -28,8 +29,10 @@ import com.volmit.iris.util.stream.ProceduralStream;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumMap;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -53,6 +56,9 @@ public final class IrisRenderer {
|
||||
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 static final int NOISE_LOW = rgb(5, 5, 5);
|
||||
private static final int NOISE_HIGH = rgb(255, 255, 255);
|
||||
private static final double DARKEST_PICK_SHADE = 0.3D;
|
||||
|
||||
private final Engine engine;
|
||||
private final MapLabels labels = new MapLabels();
|
||||
@@ -101,6 +107,9 @@ public final class IrisRenderer {
|
||||
regions[index] = regionOf(identities[index]);
|
||||
}
|
||||
int[] colors = type == RenderType.HEIGHT ? null : tileColors(size, type, subjects);
|
||||
if (layer.display() != PickDisplay.PICKS) {
|
||||
drawPickNoise(colors, grid, size, layer);
|
||||
}
|
||||
MapLabels.Kind subjectKind = type == RenderType.REGION ? MapLabels.Kind.REGION : MapLabels.Kind.BIOME;
|
||||
return new MapTile(size, blocksPerPixel, labels, subjectKind, colors, defaultHeightRange(), subjects, regions, heights);
|
||||
}
|
||||
@@ -183,6 +192,51 @@ public final class IrisRenderer {
|
||||
return (double x, double z) -> identity(lookup.idOf(complex.surfaceBiomeAsIfIn(region, x, z)), regionId);
|
||||
}
|
||||
|
||||
private void drawPickNoise(int[] colors, TileGrid grid, int size, MapLayer layer) {
|
||||
PickNoise noise = pickNoise(grid, layer);
|
||||
for (int pixelZ = 0; pixelZ < size; pixelZ++) {
|
||||
checkCancelled(grid.cancelled());
|
||||
double worldZ = grid.worldZ(pixelZ + 1);
|
||||
for (int pixelX = 0; pixelX < size; pixelX++) {
|
||||
int index = pixelZ * size + pixelX;
|
||||
double value = noise.valueAt((pixelZ + 1) * grid.size() + pixelX + 1, grid.worldX(pixelX + 1), worldZ);
|
||||
colors[index] = layer.display() == PickDisplay.NOISE
|
||||
? noise.color(value)
|
||||
: shade(colors[index], DARKEST_PICK_SHADE + (1D - DARKEST_PICK_SHADE) * noise.brightness(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Regions and BIOME terrain biomes are picked by splitting a noise by rarity; HYBRID terrain biomes by a height.
|
||||
private PickNoise pickNoise(TileGrid grid, MapLayer layer) {
|
||||
IrisComplex complex = engine.getComplex();
|
||||
if (layer.type() == RenderType.REGION) {
|
||||
ProceduralStream<Double> regionNoise = complex.getRegionStyleStream();
|
||||
return new UnitNoise((gridIndex, x, z) -> regionNoise.get(x, z));
|
||||
}
|
||||
IrisRegion region = engine.getData().getRegionLoader().load(layer.previewRegionKey());
|
||||
if (region == null) {
|
||||
return new UnitNoise((gridIndex, x, z) -> 0D);
|
||||
}
|
||||
if (complex.isHybridTerrain()) {
|
||||
ProceduralStream<Double> placementHeight = complex.getPlacementHeightStream();
|
||||
return new PlacementHeightNoise((gridIndex, x, z) -> placementHeight.get(x, z), fluidHeight(), defaultHeightRange());
|
||||
}
|
||||
return new UnitNoise(biomePickNoise(complex, region, grid));
|
||||
}
|
||||
|
||||
// Finding which biome list picked a spot is slow, so it is sampled in squares like the biomes; the noise is cheap.
|
||||
private static NoiseField biomePickNoise(IrisComplex complex, IrisRegion region, TileGrid grid) {
|
||||
IdentitySampler typeSampler = new IdentitySampler(grid, (x, z) -> complex.surfacePickAsIfIn(region, x, z).type().ordinal());
|
||||
typeSampler.renderAll(sampleBlockPixels(grid.step()));
|
||||
int[] typeByGridIndex = typeSampler.identities;
|
||||
InferredType[] types = InferredType.values();
|
||||
Map<InferredType, ProceduralStream<Double>> noiseByType = new EnumMap<>(InferredType.class);
|
||||
return (gridIndex, x, z) -> noiseByType
|
||||
.computeIfAbsent(types[typeByGridIndex[gridIndex]], type -> complex.biomePickNoiseStream(region, type))
|
||||
.get(x, z);
|
||||
}
|
||||
|
||||
// Hybrid terrain decides land, coast and sea by height after the biomes are chosen, so the final biome tells.
|
||||
private static ProceduralStream<IrisBiome> subjectBiomeStream(IrisComplex complex, RenderType type) {
|
||||
return switch (type) {
|
||||
@@ -276,6 +330,13 @@ public final class IrisRenderer {
|
||||
return new Color(red, green, blue).getRGB() & 0xFFFFFF;
|
||||
}
|
||||
|
||||
private static int shade(int color, double brightness) {
|
||||
int red = (int) Math.round(((color >> 16) & 0xFF) * brightness);
|
||||
int green = (int) Math.round(((color >> 8) & 0xFF) * brightness);
|
||||
int blue = (int) Math.round((color & 0xFF) * brightness);
|
||||
return (red << 16) | (green << 8) | blue;
|
||||
}
|
||||
|
||||
private static int blend(int first, int second, double progress) {
|
||||
double bounded = clamp(progress, 0D, 1D);
|
||||
int red = (int) Math.round(((first >> 16) & 0xFF) * (1D - bounded) + ((second >> 16) & 0xFF) * bounded);
|
||||
@@ -302,6 +363,62 @@ public final class IrisRenderer {
|
||||
private record LabelColorKey(RenderType type, short id) {
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface NoiseField {
|
||||
double at(int gridIndex, double x, double z);
|
||||
}
|
||||
|
||||
/**
|
||||
* A noise that picks regions or biomes, drawn on its own or as how bright the picks are.
|
||||
*/
|
||||
private interface PickNoise {
|
||||
double valueAt(int gridIndex, double x, double z);
|
||||
|
||||
int color(double value);
|
||||
|
||||
double brightness(double value);
|
||||
}
|
||||
|
||||
/**
|
||||
* A noise whose values run from 0 to 1, split into one band per region or biome by their rarities.
|
||||
*/
|
||||
private record UnitNoise(NoiseField field) implements PickNoise {
|
||||
@Override
|
||||
public double valueAt(int gridIndex, double x, double z) {
|
||||
return field.at(gridIndex, x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int color(double value) {
|
||||
return blend(NOISE_LOW, NOISE_HIGH, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double brightness(double value) {
|
||||
return clamp(value, 0D, 1D);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The height above the fluid that HYBRID terrain picks biomes by, coloured like the height view.
|
||||
*/
|
||||
private record PlacementHeightNoise(NoiseField field, double fluidHeight, MapStyle.HeightRange range) implements PickNoise {
|
||||
@Override
|
||||
public double valueAt(int gridIndex, double x, double z) {
|
||||
return field.at(gridIndex, x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int color(double value) {
|
||||
return heightColor(fluidHeight + value, range);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double brightness(double value) {
|
||||
return clamp((fluidHeight + value - range.lowest()) / Math.max(1D, range.highest() - range.lowest()), 0D, 1D);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The square of world positions one tile samples, including the one pixel ring borrowed from its neighbours.
|
||||
*/
|
||||
|
||||
@@ -21,23 +21,33 @@ package com.volmit.iris.core.gui.components;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* What a map tile shows: one of the render types, and for a region preview which region every spot is treated as.
|
||||
* What a map tile shows: one of the render types, for a region preview which region every spot is treated as, and in
|
||||
* the region view whether the noise that picks is drawn too.
|
||||
*
|
||||
* @param previewRegionKey the region a {@link RenderType#REGION_PREVIEW} tile shows everywhere, null for every other type
|
||||
* @param display how the picking noise is drawn; only the region view draws anything but the picks
|
||||
*/
|
||||
public record MapLayer(RenderType type, String previewRegionKey) {
|
||||
public record MapLayer(RenderType type, String previewRegionKey, PickDisplay display) {
|
||||
public MapLayer {
|
||||
Objects.requireNonNull(type, "type");
|
||||
Objects.requireNonNull(display, "display");
|
||||
if ((type == RenderType.REGION_PREVIEW) != (previewRegionKey != null)) {
|
||||
throw new IllegalArgumentException("Only a region preview names a region");
|
||||
}
|
||||
if (display != PickDisplay.PICKS && !type.isRegionView()) {
|
||||
throw new IllegalArgumentException("Only the region view draws its picking noise");
|
||||
}
|
||||
}
|
||||
|
||||
public static MapLayer of(RenderType type) {
|
||||
return new MapLayer(type, null);
|
||||
return new MapLayer(type, null, PickDisplay.PICKS);
|
||||
}
|
||||
|
||||
public static MapLayer regionPreview(String regionKey) {
|
||||
return new MapLayer(RenderType.REGION_PREVIEW, Objects.requireNonNull(regionKey, "regionKey"));
|
||||
public static MapLayer regions(PickDisplay display) {
|
||||
return new MapLayer(RenderType.REGION, null, display);
|
||||
}
|
||||
|
||||
public static MapLayer regionPreview(String regionKey, PickDisplay display) {
|
||||
return new MapLayer(RenderType.REGION_PREVIEW, Objects.requireNonNull(regionKey, "regionKey"), display);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.gui.components;
|
||||
|
||||
/**
|
||||
* How the region view draws the noise that picks its regions or biomes: the picks, the noise itself, or the picks
|
||||
* darkened where the noise is low.
|
||||
*/
|
||||
public enum PickDisplay {
|
||||
PICKS,
|
||||
NOISE,
|
||||
BOTH
|
||||
}
|
||||
@@ -19,5 +19,9 @@
|
||||
package com.volmit.iris.core.gui.components;
|
||||
|
||||
public enum RenderType {
|
||||
BIOME, BIOME_LAND, BIOME_SEA, REGION, REGION_PREVIEW, CAVE_LAND, HEIGHT, CONTINENT
|
||||
BIOME, BIOME_LAND, BIOME_SEA, REGION, REGION_PREVIEW, CAVE_LAND, HEIGHT, CONTINENT;
|
||||
|
||||
public boolean isRegionView() {
|
||||
return this == REGION || this == REGION_PREVIEW;
|
||||
}
|
||||
}
|
||||
@@ -270,12 +270,61 @@ public class IrisComplex implements DataProvider {
|
||||
* The surface biome a spot would get if it belonged to the given region, for previewing one region across the map.
|
||||
*/
|
||||
public IrisBiome surfaceBiomeAsIfIn(IrisRegion region, double x, double z) {
|
||||
return surfacePickAsIfIn(region, x, z).biome();
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link #surfaceBiomeAsIfIn}, also telling which of the region's biome lists the biome was picked from.
|
||||
*/
|
||||
public BiomePick surfacePickAsIfIn(IrisRegion region, double x, double z) {
|
||||
if (focusBiome != null) {
|
||||
return focusBiome;
|
||||
return new BiomePick(focusBiome, focusBiome.getInferredType());
|
||||
}
|
||||
|
||||
IrisBiome base = implode(biomeInRegion(region, bridgeStream.get(x, z), x, z), x, z);
|
||||
return fixBiomeType(heightStream.get(x, z), base, region, x, z, (type, xx, zz) -> biomeInRegion(region, type, xx, zz));
|
||||
InferredType baseType = bridgeStream.get(x, z);
|
||||
IrisBiome base = implode(biomeInRegion(region, baseType, x, z), x, z);
|
||||
InferredType correctedType = correctedType(heightStream.get(x, z), base, region, x, z);
|
||||
if (correctedType == null) {
|
||||
return new BiomePick(base, baseType);
|
||||
}
|
||||
|
||||
return new BiomePick(biomeInRegion(region, correctedType, x, z), correctedType);
|
||||
}
|
||||
|
||||
/**
|
||||
* The noise value that picks one of the region's biomes of this type in BIOME terrain, before rarity splits it.
|
||||
*/
|
||||
public double biomePickNoise(IrisRegion region, InferredType type, double x, double z) {
|
||||
return biomeNoiseStream(region, type).get(x, z);
|
||||
}
|
||||
|
||||
/**
|
||||
* The biome the noise picks from the region's biomes of this type in BIOME terrain, before child biomes or the
|
||||
* coast replace it.
|
||||
*/
|
||||
public IrisBiome biomePickedByNoise(IrisRegion region, InferredType type, double x, double z) {
|
||||
return noiseBiomeStream(region, type).get(x, z);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link #biomePickNoise} as a stream, for sampling it many times.
|
||||
*/
|
||||
public ProceduralStream<Double> biomePickNoiseStream(IrisRegion region, InferredType type) {
|
||||
return biomeNoiseStream(region, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* The region's biomes of this type in the order their rarities split the picking noise.
|
||||
*/
|
||||
public KList<IrisBiome> noisePickedBiomes(IrisRegion region, InferredType type) {
|
||||
return data.getBiomeLoader().loadAll(biomeKeys(region, type), biome -> biome.setInferredType(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* The dimension's regions in the order their rarities split the region noise.
|
||||
*/
|
||||
public KList<IrisRegion> noisePickedRegions() {
|
||||
return data.getRegionLoader().loadAll(dimension.getRegions());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -302,8 +351,12 @@ public class IrisComplex implements DataProvider {
|
||||
return hybridBiomeSelector.choose(region.getBiomes(this, type), placementHeightStream.get(x, z), x / zoom, z / zoom);
|
||||
}
|
||||
|
||||
// The zooms stay chained one by one as they always were, so BIOME terrain keeps placing exactly the same biomes.
|
||||
private ProceduralStream<IrisBiome> noiseBiomeStream(IrisRegion region, InferredType type) {
|
||||
return biomeNoiseStream(region, type).selectRarity(noisePickedBiomes(region, type), legacyRarity);
|
||||
}
|
||||
|
||||
// The zooms stay chained one by one as they always were, so BIOME terrain keeps placing exactly the same biomes.
|
||||
private ProceduralStream<Double> biomeNoiseStream(IrisRegion region, InferredType type) {
|
||||
ProceduralStream<Double> noise = dimension.getBiomeStyle(type).create(rng.nextParallelRNG(type.ordinal()), getData()).stream()
|
||||
.zoom(dimension.getBiomeZoom());
|
||||
if (type == InferredType.LAND) {
|
||||
@@ -312,8 +365,7 @@ public class IrisComplex implements DataProvider {
|
||||
noise = noise.zoom(dimension.getSeaZoom());
|
||||
}
|
||||
|
||||
return noise.zoom(region.getBiomeZoom(type))
|
||||
.selectRarity(data.getBiomeLoader().loadAll(biomeKeys(region, type), biome -> biome.setInferredType(type)), legacyRarity);
|
||||
return noise.zoom(region.getBiomeZoom(type));
|
||||
}
|
||||
|
||||
private static KList<String> biomeKeys(IrisRegion region, InferredType type) {
|
||||
@@ -376,25 +428,31 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
|
||||
private IrisBiome fixBiomeType(double height, IrisBiome biome, IrisRegion region, double x, double z, BiomeOfType biomeOfType) {
|
||||
InferredType correctedType = correctedType(height, biome, region, x, z);
|
||||
return correctedType == null ? biome : biomeOfType.at(correctedType, x, z);
|
||||
}
|
||||
|
||||
// The type the terrain height asks for when the biome doesn't fit it, or null when it does.
|
||||
private InferredType correctedType(double height, IrisBiome biome, IrisRegion region, double x, double z) {
|
||||
double sh = region.getShoreHeight(x, z);
|
||||
|
||||
if (height >= fluidHeight - 1 && height <= fluidHeight + sh && !biome.isShore()) {
|
||||
return biomeOfType.at(InferredType.SHORE, x, z);
|
||||
return InferredType.SHORE;
|
||||
}
|
||||
|
||||
if (height > fluidHeight + sh && !biome.isLand()) {
|
||||
return biomeOfType.at(InferredType.LAND, x, z);
|
||||
return InferredType.LAND;
|
||||
}
|
||||
|
||||
if (height < fluidHeight && !biome.isAquatic()) {
|
||||
return biomeOfType.at(InferredType.SEA, x, z);
|
||||
return InferredType.SEA;
|
||||
}
|
||||
|
||||
if (height == fluidHeight && !biome.isShore()) {
|
||||
return biomeOfType.at(InferredType.SHORE, x, z);
|
||||
return InferredType.SHORE;
|
||||
}
|
||||
|
||||
return biome;
|
||||
return null;
|
||||
}
|
||||
|
||||
private double interpolateGenerators(Engine engine, IrisInterpolator interpolator, Set<IrisGenerator> generators, double x, double z, long seed) {
|
||||
@@ -506,6 +564,12 @@ public class IrisComplex implements DataProvider {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A biome and the type of biome list it was picked from.
|
||||
*/
|
||||
public record BiomePick(IrisBiome biome, InferredType type) {
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface BiomeOfType {
|
||||
IrisBiome at(InferredType type, double x, double z);
|
||||
|
||||
Reference in new issue
Block a user