diff --git a/core/src/main/java/com/volmit/iris/core/commands/CommandStudio.java b/core/src/main/java/com/volmit/iris/core/commands/CommandStudio.java
index 55d17e50d..6c9f5b7ff 100644
--- a/core/src/main/java/com/volmit/iris/core/commands/CommandStudio.java
+++ b/core/src/main/java/com/volmit/iris/core/commands/CommandStudio.java
@@ -252,6 +252,16 @@ public class CommandStudio implements DecreeExecutor {
NoiseExplorerGUI.launch();
}
+ @Decree(description = "Preview biome palette layers (External GUI)", aliases = "pal")
+ public void palette(
+ @Param(description = "The biome to open on", contextual = true)
+ IrisBiome biome
+ ) {
+ if (noGUI()) return;
+ sender().sendMessage(C.GREEN + "Opening Palette Explorer!");
+ NoiseExplorerGUI.launchPalettes(biome == null ? null : biome.getLoadKey());
+ }
+
@Decree(description = "Charges all spawners in the area", aliases = "zzt", origin = DecreeOrigin.PLAYER)
public void charge() {
if (!IrisToolbelt.isIrisWorld(world())) {
diff --git a/core/src/main/java/com/volmit/iris/core/gui/BlockPreviewColors.java b/core/src/main/java/com/volmit/iris/core/gui/BlockPreviewColors.java
new file mode 100644
index 000000000..292cfff3f
--- /dev/null
+++ b/core/src/main/java/com/volmit/iris/core/gui/BlockPreviewColors.java
@@ -0,0 +1,101 @@
+/*
+ * 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 .
+ */
+
+package com.volmit.iris.core.gui;
+
+import org.bukkit.block.data.BlockData;
+
+import java.awt.Color;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+final class BlockPreviewColors {
+ private static final Method MAP_COLOR_METHOD = findMapColorMethod();
+ private static final double[] MAP_SHADES = {1D, 0.86D, 0.71D, 0.53D};
+ private static final int NO_MAP_COLOR = 0;
+
+ private BlockPreviewColors() {
+ }
+
+ static Map colorsByBlock(List blocks) {
+ Map colors = new HashMap<>();
+ List usedColors = new ArrayList<>();
+ for (BlockData block : blocks) {
+ String key = block.getAsString(true);
+ if (colors.containsKey(key)) {
+ continue;
+ }
+ int baseColor = baseColor(block);
+ int color = firstUnusedShade(baseColor, usedColors);
+ usedColors.add(color);
+ colors.put(key, color);
+ }
+ return colors;
+ }
+
+ // Blocks that share a map colour, like sand and sandstone, get darker shades the way Minecraft maps shade them.
+ private static int firstUnusedShade(int baseColor, List usedColors) {
+ for (double shade : MAP_SHADES) {
+ int shaded = shade(baseColor, shade);
+ if (!usedColors.contains(shaded)) {
+ return shaded;
+ }
+ }
+ return baseColor;
+ }
+
+ private static int baseColor(BlockData block) {
+ int mapColor = mapColor(block);
+ return mapColor == NO_MAP_COLOR ? colorFromName(block.getMaterial().getKey().getKey()) : mapColor;
+ }
+
+ private static int mapColor(BlockData block) {
+ if (MAP_COLOR_METHOD == null) {
+ return NO_MAP_COLOR;
+ }
+ try {
+ return ((org.bukkit.Color) MAP_COLOR_METHOD.invoke(block)).asRGB();
+ } catch (ReflectiveOperationException | RuntimeException error) {
+ return NO_MAP_COLOR;
+ }
+ }
+
+ private static int colorFromName(String name) {
+ float hue = (name.hashCode() & 0xFFFF) / (float) 0xFFFF;
+ return Color.HSBtoRGB(hue, 0.45F, 0.75F) & 0xFFFFFF;
+ }
+
+ static int shade(int color, double shade) {
+ int red = (int) Math.min(255, ((color >>> 16) & 0xFF) * shade);
+ int green = (int) Math.min(255, ((color >>> 8) & 0xFF) * shade);
+ int blue = (int) Math.min(255, (color & 0xFF) * shade);
+ return (red << 16) | (green << 8) | blue;
+ }
+
+ // Iris compiles against the 1.20.1 API, which predates BlockData#getMapColor.
+ private static Method findMapColorMethod() {
+ try {
+ return BlockData.class.getMethod("getMapColor");
+ } catch (NoSuchMethodException error) {
+ return null;
+ }
+ }
+}
diff --git a/core/src/main/java/com/volmit/iris/core/gui/NoiseColorScale.java b/core/src/main/java/com/volmit/iris/core/gui/NoiseColorScale.java
new file mode 100644
index 000000000..a10fcb369
--- /dev/null
+++ b/core/src/main/java/com/volmit/iris/core/gui/NoiseColorScale.java
@@ -0,0 +1,35 @@
+/*
+ * 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 .
+ */
+
+package com.volmit.iris.core.gui;
+
+import java.awt.Color;
+
+interface NoiseColorScale {
+ int colorFinite(double value);
+
+ int colorInvalid();
+
+ Color displayColorNormalized(double normalized);
+
+ String label();
+
+ double minimum();
+
+ double maximum();
+}
diff --git a/core/src/main/java/com/volmit/iris/core/gui/NoiseExplorerGUI.java b/core/src/main/java/com/volmit/iris/core/gui/NoiseExplorerGUI.java
index 50873eb76..e8c40e3e9 100644
--- a/core/src/main/java/com/volmit/iris/core/gui/NoiseExplorerGUI.java
+++ b/core/src/main/java/com/volmit/iris/core/gui/NoiseExplorerGUI.java
@@ -22,6 +22,8 @@ import com.volmit.iris.Iris;
import com.volmit.iris.core.IrisSettings;
import com.volmit.iris.core.loader.IrisData;
import com.volmit.iris.engine.framework.Engine;
+import com.volmit.iris.engine.object.IrisBiome;
+import com.volmit.iris.engine.object.IrisBiomePaletteLayer;
import com.volmit.iris.engine.object.IrisGenerator;
import com.volmit.iris.engine.object.NoiseStyle;
import com.volmit.iris.util.function.NoiseProvider;
@@ -82,26 +84,27 @@ import java.util.function.LongFunction;
public final class NoiseExplorerGUI extends JPanel {
private static final long serialVersionUID = 2094606939770332040L;
private static final Color BACKGROUND = new Color(12, 15, 22);
- private static final Color SIDEBAR_BACKGROUND = new Color(17, 20, 29);
- private static final Color PANEL_BACKGROUND = new Color(23, 27, 38);
- private static final Color FIELD_BACKGROUND = new Color(29, 34, 47);
+ static final Color SIDEBAR_BACKGROUND = new Color(17, 20, 29);
+ static final Color PANEL_BACKGROUND = new Color(23, 27, 38);
+ static final Color FIELD_BACKGROUND = new Color(29, 34, 47);
private static final Color SELECTED_BACKGROUND = new Color(42, 55, 78);
- private static final Color PRIMARY_TEXT = new Color(230, 234, 242);
- private static final Color SECONDARY_TEXT = new Color(151, 160, 178);
- private static final Color ACCENT = new Color(99, 161, 255);
- private static final Color BORDER = new Color(48, 55, 72);
- private static final Color ERROR = new Color(255, 116, 116);
+ static final Color PRIMARY_TEXT = new Color(230, 234, 242);
+ static final Color SECONDARY_TEXT = new Color(151, 160, 178);
+ static final Color ACCENT = new Color(99, 161, 255);
+ static final Color BORDER = new Color(48, 55, 72);
+ static final Color ERROR = new Color(255, 116, 116);
private static final Color GRID = new Color(255, 255, 255, 15);
private static final Color CROSSHAIR = new Color(255, 255, 255, 125);
private static final Color LEGEND_BACKGROUND = new Color(10, 12, 18, 215);
private static final Font MONOSPACED_FONT = new Font(Font.MONOSPACED, Font.PLAIN, 12);
- private static final Font HEADER_FONT = new Font(Font.SANS_SERIF, Font.BOLD, 11);
- private static final Font ITEM_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
- private static final Font LEGEND_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 10);
+ static final Font HEADER_FONT = new Font(Font.SANS_SERIF, Font.BOLD, 11);
+ static final Font ITEM_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
+ static final Font LEGEND_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 10);
private static final int SIDEBAR_WIDTH = 270;
private static final int STATUS_HEIGHT = 50;
private static final int REFINEMENT_DELAY_MILLIS = 180;
private static final long DEFAULT_SEED = 12345L;
+ private static final int SUMMARIZED_BLOCK_COUNT = 3;
private static final long COARSE_SAMPLE_BUDGET = 4_096L;
private static final long MAX_REFINED_SAMPLE_BUDGET = 48_000L;
private static final double REFINEMENT_TARGET_MILLIS = 250D;
@@ -109,6 +112,7 @@ public final class NoiseExplorerGUI extends JPanel {
private static final NoiseCategory[] CATEGORY_ORDER = {
NoiseCategory.CUSTOM,
NoiseCategory.PACK_GENERATORS,
+ NoiseCategory.PACK_PALETTES,
NoiseCategory.SIMPLEX,
NoiseCategory.PERLIN,
NoiseCategory.CELLULAR,
@@ -126,6 +130,8 @@ public final class NoiseExplorerGUI extends JPanel {
NoiseCategory.OTHER
};
+ private final ExplorerMode mode;
+ private final String initialPackSourceId;
private volatile Engine activeEngine;
private final LongFunction customFactory;
private final String customName;
@@ -146,12 +152,15 @@ public final class NoiseExplorerGUI extends JPanel {
private final JLabel progressLabel = new JLabel("Opening noise explorer...");
private final JLabel catalogLabel = new JLabel("Pack catalog waiting");
private final JComboBox palettePicker = new JComboBox<>(NoisePalette.values());
+ private final PaletteInspectorPanel paletteInspector = new PaletteInspectorPanel(new PaletteInspectorListener());
private List packSources = List.of();
private NoiseViewport viewport = new NoiseViewport(0D, 0D, 1D);
private NoisePalette palette;
private SourceItem selectedSource;
private NoiseProvider sampler;
+ private PaletteLayerPreview palettePreview;
+ private int paletteDepth;
private NoiseRenderCoordinator.Result renderResult;
private BufferedImage renderedImage;
private Point pointer;
@@ -162,10 +171,14 @@ public final class NoiseExplorerGUI extends JPanel {
private boolean sourceLoading;
private boolean rendering;
private boolean disposed;
+ private boolean awaitingInitialPackSource;
private String errorMessage;
- private NoiseExplorerGUI(Engine engine, String customGeneratorKey, IrisGenerator fallbackGenerator,
- String customName, long initialSeed) {
+ private NoiseExplorerGUI(ExplorerMode mode, Engine engine, String customGeneratorKey, IrisGenerator fallbackGenerator,
+ String customName, long initialSeed, String initialPackSourceId) {
+ this.mode = mode;
+ this.initialPackSourceId = initialPackSourceId;
+ this.awaitingInitialPackSource = mode == ExplorerMode.PALETTES;
this.activeEngine = engine;
this.customFactory = fallbackGenerator == null
? null
@@ -193,21 +206,33 @@ public final class NoiseExplorerGUI extends JPanel {
public static void launch() {
Engine engine = GuiHost.findActiveEngine();
EventQueue.invokeLater(() -> {
- NoiseExplorerGUI explorer = new NoiseExplorerGUI(engine, null, null, null, DEFAULT_SEED);
+ NoiseExplorerGUI explorer = new NoiseExplorerGUI(ExplorerMode.NOISE, engine, null, null, null, DEFAULT_SEED, null);
buildFrame("Noise Explorer", explorer);
});
}
+ public static void launchPalettes(String biomeKey) {
+ Engine engine = GuiHost.findActiveEngine();
+ String initialSourceId = biomeKey == null ? null : paletteSourceId(biomeKey, 0);
+ String title = biomeKey == null ? "Palette Explorer" : "Palette Explorer: " + biomeKey;
+ EventQueue.invokeLater(() -> {
+ NoiseExplorerGUI explorer = new NoiseExplorerGUI(ExplorerMode.PALETTES, engine, null, null, null, DEFAULT_SEED, initialSourceId);
+ buildFrame(title, explorer);
+ });
+ }
+
public static void launchGeneratorKey(String generatorKey, IrisGenerator fallbackGenerator, long initialSeed) {
Engine engine = GuiHost.findActiveEngine();
String displayName = generatorKey == null || generatorKey.isBlank() ? "Custom Generator" : generatorKey;
EventQueue.invokeLater(() -> {
NoiseExplorerGUI explorer = new NoiseExplorerGUI(
+ ExplorerMode.NOISE,
engine,
generatorKey,
fallbackGenerator,
displayName,
- initialSeed
+ initialSeed,
+ null
);
buildFrame("Noise Explorer: " + displayName, explorer);
});
@@ -219,6 +244,7 @@ public final class NoiseExplorerGUI extends JPanel {
frame.setLayout(new BorderLayout());
frame.add(explorer.buildSidebar(), BorderLayout.WEST);
frame.add(explorer, BorderLayout.CENTER);
+ frame.add(explorer.paletteInspector, BorderLayout.EAST);
frame.setSize(1440, 820);
frame.setMinimumSize(new Dimension(760, 500));
frame.setLocationRelativeTo(null);
@@ -339,21 +365,11 @@ public final class NoiseExplorerGUI extends JPanel {
catalogLabel.setFont(ITEM_FONT);
controls.add(catalogLabel);
- JLabel seedTitle = new JLabel("Seed");
- seedTitle.setForeground(SECONDARY_TEXT);
- seedTitle.setFont(HEADER_FONT);
- controls.add(seedTitle);
- JPanel seedRow = new JPanel(new BorderLayout(7, 0));
- seedRow.setOpaque(false);
- configureTextField(seedField);
- JButton applySeed = createButton("Apply");
- applySeed.addActionListener(event -> applySeed());
- seedField.addActionListener(event -> applySeed());
- seedRow.add(seedField, BorderLayout.CENTER);
- seedRow.add(applySeed, BorderLayout.EAST);
- controls.add(seedRow);
+ if (mode == ExplorerMode.NOISE) {
+ addSeedControls(controls);
+ }
- JLabel paletteTitle = new JLabel("Palette");
+ JLabel paletteTitle = new JLabel(mode == ExplorerMode.PALETTES ? "Noise view colours" : "Palette");
paletteTitle.setForeground(SECONDARY_TEXT);
paletteTitle.setFont(HEADER_FONT);
controls.add(paletteTitle);
@@ -375,6 +391,22 @@ public final class NoiseExplorerGUI extends JPanel {
return controls;
}
+ private void addSeedControls(JPanel controls) {
+ JLabel seedTitle = new JLabel("Seed");
+ seedTitle.setForeground(SECONDARY_TEXT);
+ seedTitle.setFont(HEADER_FONT);
+ controls.add(seedTitle);
+ JPanel seedRow = new JPanel(new BorderLayout(7, 0));
+ seedRow.setOpaque(false);
+ configureTextField(seedField);
+ JButton applySeed = createButton("Apply");
+ applySeed.addActionListener(event -> applySeed());
+ seedField.addActionListener(event -> applySeed());
+ seedRow.add(seedField, BorderLayout.CENTER);
+ seedRow.add(applySeed, BorderLayout.EAST);
+ controls.add(seedRow);
+ }
+
private static JButton createButton(String text) {
JButton button = new JButton(text);
button.setBackground(FIELD_BACKGROUND);
@@ -423,6 +455,9 @@ public final class NoiseExplorerGUI extends JPanel {
}
private List buildFixedSources() {
+ if (mode == ExplorerMode.PALETTES) {
+ return List.of();
+ }
List sources = new ArrayList<>();
if (customFactory != null && customName != null) {
sources.add(new SourceItem(
@@ -431,7 +466,7 @@ public final class NoiseExplorerGUI extends JPanel {
customName,
NoiseCategory.CUSTOM,
true,
- customFactory::apply
+ (sourceSeed, depth) -> LoadedSource.of(customFactory.apply(sourceSeed))
));
}
for (NoiseStyle style : NoiseStyle.values()) {
@@ -441,9 +476,9 @@ public final class NoiseExplorerGUI extends JPanel {
style.name(),
categorize(style),
true,
- sourceSeed -> {
+ (sourceSeed, depth) -> {
CNG noise = style.create(new RNG(sourceSeed));
- return (x, z) -> noise.noise(x, z);
+ return LoadedSource.of((x, z) -> noise.noise(x, z));
}
));
}
@@ -489,11 +524,17 @@ public final class NoiseExplorerGUI extends JPanel {
return (x, z) -> selectedGenerator.getHeight(x, z, mixedSeed);
}
- private static List loadPackSources(Engine engine) {
+ private List loadPackSources(Engine engine) {
if (engine == null) {
return List.of();
}
- IrisData data = engine.getData();
+ return switch (mode) {
+ case NOISE -> loadPackGeneratorSources(engine.getData());
+ case PALETTES -> loadPackPaletteSources(engine);
+ };
+ }
+
+ private static List loadPackGeneratorSources(IrisData data) {
String[] keys = data.getGeneratorLoader().getPossibleKeys();
if (keys == null || keys.length == 0) {
return List.of();
@@ -507,19 +548,67 @@ public final class NoiseExplorerGUI extends JPanel {
key,
NoiseCategory.PACK_GENERATORS,
true,
- sourceSeed -> {
+ (sourceSeed, depth) -> {
IrisGenerator generator = data.getGeneratorLoader().load(key);
if (generator == null) {
throw new IllegalStateException("Pack generator not found: " + key);
}
long mixedSeed = new RNG(sourceSeed).nextParallelRNG(3245).lmax();
- return (x, z) -> generator.getHeight(x, z, mixedSeed);
+ return LoadedSource.of((x, z) -> generator.getHeight(x, z, mixedSeed));
}
));
}
return List.copyOf(sources);
}
+ // Palette layers always use the world's terrain seed: they share their cached noise with the running world.
+ private static List loadPackPaletteSources(Engine engine) {
+ IrisData data = engine.getData();
+ String[] keys = data.getBiomeLoader().getPossibleKeys();
+ if (keys == null || keys.length == 0) {
+ return List.of();
+ }
+ Arrays.sort(keys);
+ List sources = new ArrayList<>();
+ for (String key : keys) {
+ IrisBiome biome = data.getBiomeLoader().load(key);
+ if (biome == null) {
+ continue;
+ }
+ for (int layer = 0; layer < biome.getLayers().size(); layer++) {
+ int selectedLayer = layer;
+ sources.add(new SourceItem(
+ paletteSourceId(key, layer),
+ "Layer " + layer + " · " + summarizeBlocks(biome.getLayers().get(layer)),
+ key,
+ NoiseCategory.PACK_PALETTES,
+ false,
+ (sourceSeed, depth) -> {
+ PaletteLayerPreview preview = PaletteLayerPreview.load(engine, key, selectedLayer, depth);
+ return new LoadedSource(preview::sample, preview);
+ }
+ ));
+ }
+ }
+ return List.copyOf(sources);
+ }
+
+ private static String paletteSourceId(String biomeKey, int layer) {
+ return "palette:" + biomeKey + "#" + layer;
+ }
+
+ private static String summarizeBlocks(IrisBiomePaletteLayer layer) {
+ List names = layer.getPalette().stream()
+ .map(block -> block.getBlock().replace("minecraft:", ""))
+ .distinct()
+ .toList();
+ if (names.size() <= SUMMARIZED_BLOCK_COUNT) {
+ return String.join(", ", names);
+ }
+ return String.join(", ", names.subList(0, SUMMARIZED_BLOCK_COUNT))
+ + " +" + (names.size() - SUMMARIZED_BLOCK_COUNT) + " more";
+ }
+
private void applyCatalog(long revision, List sources, Throwable error) {
if (disposed || revision != catalogLoadRevision.get()) {
return;
@@ -529,17 +618,14 @@ public final class NoiseExplorerGUI extends JPanel {
if (error != null) {
catalogLabel.setForeground(ERROR);
catalogLabel.setText("Pack catalog unavailable");
- } else if (sources.isEmpty()) {
- catalogLabel.setForeground(SECONDARY_TEXT);
- catalogLabel.setText("No active pack generators");
} else {
catalogLabel.setForeground(SECONDARY_TEXT);
- catalogLabel.setText(sources.size() + " pack generators");
+ catalogLabel.setText(catalogSummary(sources));
}
boolean reloadSelectedPack = false;
if (error == null
&& selectedSource != null
- && selectedSource.category() == NoiseCategory.PACK_GENERATORS) {
+ && selectedSource.category().isFromPack()) {
SourceItem replacement = findSourceById(selectedSource.id(), sources);
if (replacement == null) {
failRemovedPackSource(selectedSource);
@@ -552,6 +638,29 @@ public final class NoiseExplorerGUI extends JPanel {
if (reloadSelectedPack) {
loadSelectedSource();
}
+ if (error == null && awaitingInitialPackSource && !sources.isEmpty()) {
+ awaitingInitialPackSource = false;
+ selectInitialPackSource(sources);
+ }
+ }
+
+ private void selectInitialPackSource(List sources) {
+ SourceItem requested = initialPackSourceId == null ? null : findSourceById(initialPackSourceId, sources);
+ SourceItem initial = requested == null ? sources.get(0) : requested;
+ if (!selectSourceInList(initial.id())) {
+ selectSource(initial);
+ }
+ }
+
+ private String catalogSummary(List sources) {
+ if (mode == ExplorerMode.NOISE) {
+ return sources.isEmpty() ? "No active pack generators" : sources.size() + " pack generators";
+ }
+ if (resolveEngine() == null) {
+ return "Open a studio world to see its palettes";
+ }
+ long biomes = sources.stream().map(SourceItem::rawName).distinct().count();
+ return sources.size() + " palette layers in " + biomes + " biomes";
}
private void failRemovedPackSource(SourceItem removedSource) {
@@ -560,15 +669,15 @@ public final class NoiseExplorerGUI extends JPanel {
viewRevision++;
renderCoordinator.cancel(viewRevision);
selectedSource = null;
- sampler = null;
+ clearSampler();
renderedImage = null;
renderResult = null;
sourceLoading = false;
rendering = false;
- errorMessage = "Pack generator removed during hotload: " + removedSource.rawName();
+ errorMessage = "Pack source removed during hotload: " + removedSource.text();
selectedLabel.setText("No source selected");
progressLabel.setForeground(ERROR);
- progressLabel.setText("Selected pack generator was removed");
+ progressLabel.setText("Selected pack source was removed");
sourceList.clearSelection();
repaint();
}
@@ -586,6 +695,10 @@ public final class NoiseExplorerGUI extends JPanel {
if (matching.isEmpty()) {
continue;
}
+ if (category == NoiseCategory.PACK_PALETTES) {
+ addGroupedByBiome(matching);
+ continue;
+ }
sourceModel.addElement(new ListItem(categoryLabel(category), null));
for (SourceItem source : matching) {
sourceModel.addElement(new ListItem(source.text(), source));
@@ -596,6 +709,17 @@ public final class NoiseExplorerGUI extends JPanel {
}
}
+ private void addGroupedByBiome(List paletteSources) {
+ String currentBiome = null;
+ for (SourceItem source : paletteSources) {
+ if (!source.rawName().equals(currentBiome)) {
+ currentBiome = source.rawName();
+ sourceModel.addElement(new ListItem(currentBiome, null));
+ }
+ sourceModel.addElement(new ListItem(source.text(), source));
+ }
+ }
+
private List matchingSources(NoiseCategory category, String filter) {
List matching = new ArrayList<>();
addMatchingSources(matching, fixedSources, category, filter);
@@ -633,7 +757,8 @@ public final class NoiseExplorerGUI extends JPanel {
return;
}
selectedSource = source;
- selectedLabel.setText(source.text());
+ paletteDepth = 0;
+ selectedLabel.setText(source.category() == NoiseCategory.PACK_PALETTES ? source.rawName() : source.text());
loadSelectedSource();
}
@@ -644,6 +769,7 @@ public final class NoiseExplorerGUI extends JPanel {
}
long revision = sourceLoadRevision.incrementAndGet();
long requestedSeed = seed;
+ int requestedDepth = paletteDepth;
refinementTimer.stop();
viewRevision++;
renderCoordinator.cancel(viewRevision);
@@ -658,11 +784,11 @@ public final class NoiseExplorerGUI extends JPanel {
repaint();
submitLatest(sourceExecutor, () -> {
try {
- NoiseProvider loadedSampler = source.factory().create(requestedSeed);
- if (loadedSampler == null) {
+ LoadedSource loaded = source.factory().create(requestedSeed, requestedDepth);
+ if (loaded == null || loaded.sampler() == null) {
throw new IllegalStateException("Noise source returned no sampler: " + source.rawName());
}
- EventQueue.invokeLater(() -> applyLoadedSource(revision, source, loadedSampler));
+ EventQueue.invokeLater(() -> applyLoadedSource(revision, source, loaded));
} catch (Throwable error) {
Iris.reportError(error);
EventQueue.invokeLater(() -> applySourceError(revision, source, error));
@@ -670,11 +796,13 @@ public final class NoiseExplorerGUI extends JPanel {
});
}
- private void applyLoadedSource(long revision, SourceItem source, NoiseProvider loadedSampler) {
+ private void applyLoadedSource(long revision, SourceItem source, LoadedSource loaded) {
if (disposed || revision != sourceLoadRevision.get() || selectedSource != source) {
return;
}
- sampler = loadedSampler;
+ sampler = loaded.sampler();
+ palettePreview = loaded.palettePreview();
+ showPaletteInspector();
sourceLoading = false;
errorMessage = null;
renderedImage = null;
@@ -688,7 +816,7 @@ public final class NoiseExplorerGUI extends JPanel {
if (disposed || revision != sourceLoadRevision.get() || selectedSource != source) {
return;
}
- sampler = null;
+ clearSampler();
sourceLoading = false;
rendering = false;
errorMessage = readableError(error);
@@ -697,6 +825,28 @@ public final class NoiseExplorerGUI extends JPanel {
repaint();
}
+ private void clearSampler() {
+ sampler = null;
+ palettePreview = null;
+ showPaletteInspector();
+ }
+
+ private void showPaletteInspector() {
+ if (palettePreview == null) {
+ paletteInspector.hideInspector();
+ } else {
+ paletteInspector.showPreview(palettePreview);
+ }
+ if (paletteInspector.getParent() != null) {
+ paletteInspector.getParent().revalidate();
+ }
+ }
+
+ private NoiseColorScale activeColorScale() {
+ PaletteLayerPreview preview = palettePreview;
+ return preview == null ? palette : new PaletteLayerScale(paletteInspector.selectedView(), preview, palette);
+ }
+
private void applySeed() {
String rawSeed = seedField.getText().trim();
try {
@@ -767,7 +917,7 @@ public final class NoiseExplorerGUI extends JPanel {
viewRevision,
sampler,
viewport,
- palette,
+ activeColorScale(),
getWidth(),
renderHeight(),
sampleStep
@@ -935,6 +1085,7 @@ public final class NoiseExplorerGUI extends JPanel {
}
private void drawLegend(Graphics2D canvas, int width) {
+ NoiseColorScale colorScale = activeColorScale();
int legendWidth = 190;
int legendHeight = 40;
int legendX = Math.max(10, width - legendWidth - 12);
@@ -946,16 +1097,16 @@ public final class NoiseExplorerGUI extends JPanel {
int gradientWidth = legendWidth - 18;
for (int x = 0; x < gradientWidth; x++) {
double normalized = x / (double) Math.max(1, gradientWidth - 1);
- canvas.setColor(palette.displayColorNormalized(normalized));
+ canvas.setColor(colorScale.displayColorNormalized(normalized));
canvas.drawLine(gradientX + x, gradientY, gradientX + x, gradientY + 8);
}
canvas.setFont(LEGEND_FONT);
canvas.setColor(PRIMARY_TEXT);
- canvas.drawString(formatNumber(palette.minimum()), gradientX, legendY + 31);
- String maximum = formatNumber(palette.maximum());
+ canvas.drawString(formatNumber(colorScale.minimum()), gradientX, legendY + 31);
+ String maximum = formatNumber(colorScale.maximum());
int maximumWidth = canvas.getFontMetrics().stringWidth(maximum);
canvas.drawString(maximum, gradientX + gradientWidth - maximumWidth, legendY + 31);
- String label = palette.label();
+ String label = colorScale.label();
int labelWidth = canvas.getFontMetrics().stringWidth(label);
canvas.drawString(label, legendX + ((legendWidth - labelWidth) / 2), legendY + 31);
}
@@ -1010,12 +1161,24 @@ public final class NoiseExplorerGUI extends JPanel {
if (renderResult == null) {
return coordinates + (catalogLoading ? " · loading catalog" : "");
}
+ PaletteLayerPreview preview = palettePreview;
+ if (preview != null) {
+ return coordinates + " · " + describePaletteAt(preview, worldX, worldZ);
+ }
NoiseRenderCoordinator.Result result = renderResult;
long clipped = result.underflow() + result.overflow();
return coordinates + " · range " + formatNumber(result.minimum()) + ".." + formatNumber(result.maximum())
+ " · clipped " + clipped + " invalid " + result.invalid();
}
+ private static String describePaletteAt(PaletteLayerPreview preview, double worldX, double worldZ) {
+ double noise = preview.sample(worldX, worldZ);
+ if (Double.isNaN(noise)) {
+ return "layer doesn't reach this depth here";
+ }
+ return preview.entryForNoise(noise).block() + " · noise " + formatNumber(noise);
+ }
+
private int renderHeight() {
return Math.max(1, getHeight() - STATUS_HEIGHT);
}
@@ -1103,6 +1266,7 @@ public final class NoiseExplorerGUI extends JPanel {
return switch (category) {
case CUSTOM -> "Custom";
case PACK_GENERATORS -> "Pack Generators";
+ case PACK_PALETTES -> "Pack Palettes";
case SIMPLEX -> "Simplex";
case PERLIN -> "Perlin";
case CELLULAR -> "Cellular";
@@ -1131,7 +1295,13 @@ public final class NoiseExplorerGUI extends JPanel {
@FunctionalInterface
private interface SourceFactory {
- NoiseProvider create(long seed);
+ LoadedSource create(long seed, int paletteDepth);
+ }
+
+ private record LoadedSource(NoiseProvider sampler, PaletteLayerPreview palettePreview) {
+ private static LoadedSource of(NoiseProvider sampler) {
+ return new LoadedSource(sampler, null);
+ }
}
private record SourceItem(String id, String text, String rawName, NoiseCategory category, boolean usesSeed,
@@ -1141,6 +1311,22 @@ public final class NoiseExplorerGUI extends JPanel {
private record ListItem(String text, SourceItem source) {
}
+ private final class PaletteInspectorListener implements PaletteInspectorPanel.Listener {
+ @Override
+ public void onViewChanged(PaletteLayerScale.View view) {
+ requestPreview();
+ }
+
+ @Override
+ public void onDepthChanged(int depth) {
+ if (depth == paletteDepth) {
+ return;
+ }
+ paletteDepth = depth;
+ loadSelectedSource();
+ }
+ }
+
private final class RenderListener implements NoiseRenderCoordinator.Listener {
@Override
public void onRenderStarted(NoiseRenderCoordinator.Request request) {
@@ -1217,9 +1403,15 @@ public final class NoiseExplorerGUI extends JPanel {
}
}
+ private enum ExplorerMode {
+ NOISE,
+ PALETTES
+ }
+
private enum NoiseCategory {
CUSTOM,
PACK_GENERATORS,
+ PACK_PALETTES,
SIMPLEX,
PERLIN,
CELLULAR,
@@ -1234,6 +1426,10 @@ public final class NoiseExplorerGUI extends JPanel {
NOWHERE,
SIERPINSKI,
UTILITY,
- OTHER
+ OTHER;
+
+ private boolean isFromPack() {
+ return this == PACK_GENERATORS || this == PACK_PALETTES;
+ }
}
}
diff --git a/core/src/main/java/com/volmit/iris/core/gui/NoisePalette.java b/core/src/main/java/com/volmit/iris/core/gui/NoisePalette.java
index 1783b1b68..f976b4d5d 100644
--- a/core/src/main/java/com/volmit/iris/core/gui/NoisePalette.java
+++ b/core/src/main/java/com/volmit/iris/core/gui/NoisePalette.java
@@ -20,7 +20,7 @@ package com.volmit.iris.core.gui;
import java.awt.Color;
-enum NoisePalette {
+enum NoisePalette implements NoiseColorScale {
TERRAIN("Terrain", 0D, 1D, new int[]{0x071A2F, 0x155E75, 0x2A9D8F, 0xE9C46A, 0xF4F1DE}),
SIGNED("Signed", -1D, 1D, new int[]{0x173B66, 0x4F86C6, 0xE7EDF3, 0xE89555, 0x9D3B35}),
GRAYSCALE("Grayscale", 0D, 1D, new int[]{0x050505, 0xFFFFFF});
@@ -48,7 +48,8 @@ enum NoisePalette {
return colorFinite(value);
}
- int colorFinite(double value) {
+ @Override
+ public int colorFinite(double value) {
double normalized = (value - minimum) / (maximum - minimum);
return colorNormalized(normalized);
}
@@ -58,20 +59,29 @@ enum NoisePalette {
return lookup[(int) Math.round(clipped * (lookup.length - 1))];
}
- Color displayColorNormalized(double normalized) {
+ @Override
+ public int colorInvalid() {
+ return INVALID_COLOR;
+ }
+
+ @Override
+ public Color displayColorNormalized(double normalized) {
double clipped = Math.max(0D, Math.min(1D, normalized));
return displayColors[(int) Math.round(clipped * (displayColors.length - 1))];
}
- String label() {
+ @Override
+ public String label() {
return label;
}
- double minimum() {
+ @Override
+ public double minimum() {
return minimum;
}
- double maximum() {
+ @Override
+ public double maximum() {
return maximum;
}
diff --git a/core/src/main/java/com/volmit/iris/core/gui/NoiseRenderCoordinator.java b/core/src/main/java/com/volmit/iris/core/gui/NoiseRenderCoordinator.java
index 115d2b502..7f6148539 100644
--- a/core/src/main/java/com/volmit/iris/core/gui/NoiseRenderCoordinator.java
+++ b/core/src/main/java/com/volmit/iris/core/gui/NoiseRenderCoordinator.java
@@ -222,9 +222,9 @@ final class NoiseRenderCoordinator implements AutoCloseable {
double sampleOffset = request.sampleStep() * 0.5D;
double startWorldX = request.viewport().worldX(sampleOffset, request.width());
double worldStep = request.viewport().blocksPerPixel() * request.sampleStep();
- NoisePalette palette = request.palette();
- double paletteMinimum = palette.minimum();
- double paletteMaximum = palette.maximum();
+ NoiseColorScale colorScale = request.colorScale();
+ double paletteMinimum = colorScale.minimum();
+ double paletteMaximum = colorScale.maximum();
for (int y = fromY; y < toY; y++) {
double screenY = (y * (double) request.sampleStep()) + sampleOffset;
double worldZ = request.viewport().worldZ(screenY, request.height());
@@ -238,10 +238,10 @@ final class NoiseRenderCoordinator implements AutoCloseable {
double value = request.sampler().noise(worldX, worldZ);
boolean center = centerRow && x == outputWidth / 2;
if (Double.isFinite(value)) {
- pixels[pixelIndex++] = palette.colorFinite(value);
+ pixels[pixelIndex++] = colorScale.colorFinite(value);
stats.acceptFinite(value, center, paletteMinimum, paletteMaximum);
} else {
- pixels[pixelIndex++] = NoisePalette.INVALID_COLOR;
+ pixels[pixelIndex++] = colorScale.colorInvalid();
stats.acceptInvalid(value, center);
}
worldX += worldStep;
@@ -315,12 +315,12 @@ final class NoiseRenderCoordinator implements AutoCloseable {
void onRenderFailed(Request request, Throwable error);
}
- record Request(long revision, NoiseProvider sampler, NoiseViewport viewport, NoisePalette palette,
+ record Request(long revision, NoiseProvider sampler, NoiseViewport viewport, NoiseColorScale colorScale,
int width, int height, int sampleStep) {
Request {
Objects.requireNonNull(sampler, "sampler");
Objects.requireNonNull(viewport, "viewport");
- Objects.requireNonNull(palette, "palette");
+ Objects.requireNonNull(colorScale, "colorScale");
if (revision < 0L || width < 1 || height < 1 || sampleStep < 1) {
throw new IllegalArgumentException("Invalid noise render request");
}
diff --git a/core/src/main/java/com/volmit/iris/core/gui/PaletteInspectorPanel.java b/core/src/main/java/com/volmit/iris/core/gui/PaletteInspectorPanel.java
new file mode 100644
index 000000000..d63b81dff
--- /dev/null
+++ b/core/src/main/java/com/volmit/iris/core/gui/PaletteInspectorPanel.java
@@ -0,0 +1,399 @@
+/*
+ * 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 .
+ */
+
+package com.volmit.iris.core.gui;
+
+import javax.swing.BorderFactory;
+import javax.swing.JComboBox;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSlider;
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.FontMetrics;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.GridLayout;
+import java.awt.RenderingHints;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import static com.volmit.iris.core.gui.NoiseExplorerGUI.ACCENT;
+import static com.volmit.iris.core.gui.NoiseExplorerGUI.BORDER;
+import static com.volmit.iris.core.gui.NoiseExplorerGUI.FIELD_BACKGROUND;
+import static com.volmit.iris.core.gui.NoiseExplorerGUI.HEADER_FONT;
+import static com.volmit.iris.core.gui.NoiseExplorerGUI.ITEM_FONT;
+import static com.volmit.iris.core.gui.NoiseExplorerGUI.LEGEND_FONT;
+import static com.volmit.iris.core.gui.NoiseExplorerGUI.PANEL_BACKGROUND;
+import static com.volmit.iris.core.gui.NoiseExplorerGUI.PRIMARY_TEXT;
+import static com.volmit.iris.core.gui.NoiseExplorerGUI.SECONDARY_TEXT;
+import static com.volmit.iris.core.gui.NoiseExplorerGUI.SIDEBAR_BACKGROUND;
+
+final class PaletteInspectorPanel extends JPanel {
+ private static final long serialVersionUID = 4473102938518712001L;
+ private static final int PANEL_WIDTH = 310;
+ private static final Color WARNING = new Color(255, 196, 92);
+ private static final Color HISTOGRAM_BAR = new Color(151, 160, 178, 170);
+ private static final Color BAND_DIVIDER = new Color(255, 255, 255, 70);
+
+ private final Listener listener;
+ private final JLabel titleLabel = new JLabel();
+ private final JLabel detailLabel = new JLabel();
+ private final JComboBox viewPicker = new JComboBox<>(PaletteLayerScale.View.values());
+ private final JLabel depthLabel = new JLabel();
+ private final JSlider depthSlider = new JSlider(0, 0, 0);
+ private final StatisticsView statisticsView = new StatisticsView();
+ private boolean updatingControls;
+
+ PaletteInspectorPanel(Listener listener) {
+ super(new BorderLayout());
+ this.listener = listener;
+ setPreferredSize(new Dimension(PANEL_WIDTH, 0));
+ setBackground(SIDEBAR_BACKGROUND);
+ setBorder(BorderFactory.createMatteBorder(0, 1, 0, 0, BORDER));
+ add(buildControls(), BorderLayout.NORTH);
+ JScrollPane statisticsScroll = new JScrollPane(statisticsView);
+ statisticsScroll.setBorder(BorderFactory.createEmptyBorder());
+ statisticsScroll.getVerticalScrollBar().setUnitIncrement(18);
+ add(statisticsScroll, BorderLayout.CENTER);
+ setVisible(false);
+ }
+
+ void showPreview(PaletteLayerPreview preview) {
+ updatingControls = true;
+ try {
+ titleLabel.setText(preview.biomeKey() + " · layer " + preview.layer());
+ detailLabel.setText(thicknessText(preview) + " · zoom " + formatZoom(preview.zoom())
+ + " (×" + formatZoom(preview.zoom() * preview.zoom()) + " effective)");
+ int deepestDepth = Math.max(0, preview.maxThickness() - 1);
+ depthSlider.setMaximum(deepestDepth);
+ depthSlider.setValue(preview.depth());
+ depthSlider.setEnabled(deepestDepth > 0);
+ depthLabel.setText(depthText(preview.depth(), deepestDepth));
+ } finally {
+ updatingControls = false;
+ }
+ statisticsView.show(preview);
+ setVisible(true);
+ }
+
+ void hideInspector() {
+ setVisible(false);
+ }
+
+ PaletteLayerScale.View selectedView() {
+ return (PaletteLayerScale.View) viewPicker.getSelectedItem();
+ }
+
+ private JPanel buildControls() {
+ JPanel controls = new JPanel(new GridLayout(0, 1, 0, 6));
+ controls.setBackground(PANEL_BACKGROUND);
+ controls.setBorder(BorderFactory.createCompoundBorder(
+ BorderFactory.createMatteBorder(0, 0, 1, 0, BORDER),
+ BorderFactory.createEmptyBorder(12, 14, 12, 14)
+ ));
+ titleLabel.setForeground(PRIMARY_TEXT);
+ titleLabel.setFont(ITEM_FONT.deriveFont(Font.BOLD, 14F));
+ detailLabel.setForeground(SECONDARY_TEXT);
+ detailLabel.setFont(ITEM_FONT);
+ controls.add(titleLabel);
+ controls.add(detailLabel);
+
+ controls.add(sectionLabel("View"));
+ viewPicker.setBackground(FIELD_BACKGROUND);
+ viewPicker.setForeground(PRIMARY_TEXT);
+ viewPicker.addActionListener(event -> listener.onViewChanged(selectedView()));
+ controls.add(viewPicker);
+
+ depthLabel.setForeground(SECONDARY_TEXT);
+ depthLabel.setFont(HEADER_FONT);
+ controls.add(depthLabel);
+ depthSlider.setOpaque(false);
+ depthSlider.addChangeListener(event -> {
+ if (!updatingControls && !depthSlider.getValueIsAdjusting()) {
+ listener.onDepthChanged(depthSlider.getValue());
+ }
+ });
+ depthSlider.addChangeListener(event -> depthLabel.setText(depthText(depthSlider.getValue(), depthSlider.getMaximum())));
+ controls.add(depthSlider);
+ return controls;
+ }
+
+ private static JLabel sectionLabel(String text) {
+ JLabel label = new JLabel(text);
+ label.setForeground(SECONDARY_TEXT);
+ label.setFont(HEADER_FONT);
+ return label;
+ }
+
+ private static String thicknessText(PaletteLayerPreview preview) {
+ if (preview.minThickness() == preview.maxThickness()) {
+ return preview.maxThickness() + (preview.maxThickness() == 1 ? " block thick" : " blocks thick");
+ }
+ return preview.minThickness() + "–" + preview.maxThickness() + " blocks thick";
+ }
+
+ private static String depthText(int depth, int deepestDepth) {
+ if (deepestDepth == 0) {
+ return "Depth: top block only";
+ }
+ return "Depth " + depth + (depth == 0 ? " (top)" : "") + " of " + deepestDepth;
+ }
+
+ private static String formatZoom(double zoom) {
+ return zoom == Math.rint(zoom) ? Long.toString((long) zoom) : String.format(Locale.ROOT, "%.2f", zoom);
+ }
+
+ private static String percent(double share) {
+ if (share > 0D && share < 0.001D) {
+ return "<0.1%";
+ }
+ return String.format(Locale.ROOT, "%.1f%%", share * 100D);
+ }
+
+ interface Listener {
+ void onViewChanged(PaletteLayerScale.View view);
+
+ void onDepthChanged(int depth);
+ }
+
+ private static final class StatisticsView extends JComponent {
+ private static final long serialVersionUID = -2318840125766150871L;
+ private static final int MARGIN = 14;
+ private static final int HISTOGRAM_HEIGHT = 84;
+ private static final int BAND_HEIGHT = 12;
+ private static final int ROW_HEIGHT = 20;
+ private static final int SWATCH_SIZE = 10;
+ private static final int WEIGHT_COLUMN_WIDTH = 48;
+ private static final int ACTUAL_COLUMN_WIDTH = 70;
+ private static final int NOTE_LINE_HEIGHT = 15;
+
+ private PaletteLayerPreview preview;
+
+ private StatisticsView() {
+ setOpaque(true);
+ setBackground(SIDEBAR_BACKGROUND);
+ }
+
+ private void show(PaletteLayerPreview nextPreview) {
+ preview = nextPreview;
+ revalidate();
+ repaint();
+ }
+
+ @Override
+ public Dimension getPreferredSize() {
+ if (preview == null) {
+ return new Dimension(PANEL_WIDTH - 20, 0);
+ }
+ int width = Math.max(getWidth(), PANEL_WIDTH - 20) - MARGIN * 2;
+ FontMetrics noteMetrics = getFontMetrics(LEGEND_FONT);
+ int noteLines = 0;
+ for (String note : notes(preview)) {
+ noteLines += wrap(note, noteMetrics, width).size() + 1;
+ }
+ int height = MARGIN + 18 + HISTOGRAM_HEIGHT + BAND_HEIGHT + 30
+ + 22 + ROW_HEIGHT * (preview.entries().size() + 1)
+ + 16 + noteLines * NOTE_LINE_HEIGHT + MARGIN;
+ return new Dimension(PANEL_WIDTH - 20, 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);
+ canvas.setColor(SIDEBAR_BACKGROUND);
+ canvas.fillRect(0, 0, getWidth(), getHeight());
+ if (preview == null) {
+ return;
+ }
+ int width = getWidth() - MARGIN * 2;
+ int y = drawValueSpread(canvas, MARGIN, MARGIN, width);
+ y = drawBlockTable(canvas, MARGIN, y + 22, width);
+ drawNotes(canvas, MARGIN, y + 16, width);
+ } finally {
+ canvas.dispose();
+ }
+ }
+
+ private int drawValueSpread(Graphics2D canvas, int x, int y, int width) {
+ drawSectionTitle(canvas, "NOISE VALUES THIS LAYER PRODUCES", x, y + 10);
+ int histogramTop = y + 18;
+ long[] histogram = preview.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(HISTOGRAM_HEIGHT * (histogram[bin] / (double) peak));
+ canvas.fillRect(barLeft, histogramTop + HISTOGRAM_HEIGHT - barHeight, Math.max(1, barRight - barLeft - 1), barHeight);
+ }
+
+ int bandTop = histogramTop + HISTOGRAM_HEIGHT + 2;
+ for (PaletteLayerPreview.Entry entry : preview.entries()) {
+ if (Double.isNaN(entry.lowestNoise())) {
+ continue;
+ }
+ int bandLeft = x + (int) Math.round(entry.lowestNoise() * width);
+ int bandRight = x + (int) Math.round(entry.highestNoise() * width);
+ canvas.setColor(new Color(entry.color()));
+ canvas.fillRect(bandLeft, bandTop, Math.max(1, bandRight - bandLeft), BAND_HEIGHT);
+ if (entry.highestNoise() < 1D) {
+ canvas.setColor(BAND_DIVIDER);
+ canvas.drawLine(bandRight, histogramTop, bandRight, bandTop + BAND_HEIGHT - 1);
+ }
+ }
+
+ canvas.setFont(LEGEND_FONT);
+ canvas.setColor(SECONDARY_TEXT);
+ FontMetrics metrics = canvas.getFontMetrics();
+ int labelY = bandTop + BAND_HEIGHT + 13;
+ 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);
+ return labelY;
+ }
+
+ private int drawBlockTable(Graphics2D canvas, int x, int y, int width) {
+ int actualRight = x + width;
+ int weightRight = actualRight - ACTUAL_COLUMN_WIDTH;
+ drawSectionTitle(canvas, "BLOCKS", x, y);
+ drawRightAligned(canvas, "weight", weightRight, y);
+ drawRightAligned(canvas, "actual", actualRight, y);
+
+ int rowY = y + ROW_HEIGHT;
+ for (PaletteLayerPreview.Entry entry : preview.entries()) {
+ canvas.setColor(new Color(entry.color()));
+ canvas.fillRoundRect(x, rowY - SWATCH_SIZE, SWATCH_SIZE, SWATCH_SIZE, 3, 3);
+ canvas.setFont(ITEM_FONT);
+ canvas.setColor(entry.found() ? PRIMARY_TEXT : NoiseExplorerGUI.ERROR);
+ String name = entry.found() ? shortBlockName(entry.block()) : shortBlockName(entry.block()) + " (not found)";
+ int nameLeft = x + SWATCH_SIZE + 7;
+ canvas.drawString(fit(canvas, name, weightRight - WEIGHT_COLUMN_WIDTH - nameLeft), nameLeft, rowY);
+ canvas.setColor(SECONDARY_TEXT);
+ drawRightAligned(canvas, percent(entry.weightShare()), weightRight, rowY);
+ canvas.setColor(actualColor(entry));
+ drawRightAligned(canvas, actualText(entry), actualRight, rowY);
+ rowY += ROW_HEIGHT;
+ }
+ return rowY - ROW_HEIGHT;
+ }
+
+ private void drawNotes(Graphics2D canvas, int x, int y, int width) {
+ canvas.setFont(LEGEND_FONT);
+ canvas.setColor(SECONDARY_TEXT);
+ FontMetrics metrics = canvas.getFontMetrics();
+ int lineY = y;
+ for (String note : notes(preview)) {
+ for (String line : wrap(note, metrics, width)) {
+ canvas.drawString(line, x, lineY);
+ lineY += NOTE_LINE_HEIGHT;
+ }
+ lineY += 4;
+ }
+ }
+
+ private static List notes(PaletteLayerPreview preview) {
+ List notes = new ArrayList<>();
+ notes.add("▼ gets under half its weight, ▲ gets over double.");
+ notes.add(String.format(Locale.ROOT, "Measured on a %d × %d grid spread over %,.0f blocks around 0, 0.",
+ PaletteLayerPreview.STATISTICS_GRID_SIZE, PaletteLayerPreview.STATISTICS_GRID_SIZE,
+ PaletteLayerPreview.statisticsAreaBlocks()));
+ if (preview.absentShare() > 0D) {
+ notes.add("The layer doesn't reach this depth on " + percent(preview.absentShare())
+ + " of the ground. That ground is dark on the map and left out of the shares.");
+ }
+ if (preview.outOfRangeShare() > 0D) {
+ notes.add(percent(preview.outOfRangeShare()) + " of the noise values fall outside 0 to 1."
+ + " Those spots get the first block.");
+ }
+ if (preview.hasSlopeCondition()) {
+ notes.add("This layer has a slope condition, which the preview ignores.");
+ }
+ return notes;
+ }
+
+ private static String actualText(PaletteLayerPreview.Entry entry) {
+ if (entry.isFarBelowWeight()) {
+ return "▼ " + percent(entry.actualShare());
+ }
+ if (entry.isFarAboveWeight()) {
+ return "▲ " + percent(entry.actualShare());
+ }
+ return percent(entry.actualShare());
+ }
+
+ private static Color actualColor(PaletteLayerPreview.Entry entry) {
+ return entry.isFarBelowWeight() || entry.isFarAboveWeight() ? WARNING : PRIMARY_TEXT;
+ }
+
+ private static void drawSectionTitle(Graphics2D canvas, String title, int x, int y) {
+ canvas.setFont(HEADER_FONT);
+ canvas.setColor(ACCENT);
+ canvas.drawString(title, x, y);
+ }
+
+ private static void drawRightAligned(Graphics2D canvas, String text, int right, int y) {
+ canvas.drawString(text, right - canvas.getFontMetrics().stringWidth(text), y);
+ }
+
+ private static String shortBlockName(String block) {
+ return block.startsWith("minecraft:") ? block.substring("minecraft:".length()) : block;
+ }
+
+ 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 + "…";
+ }
+
+ private static List wrap(String text, FontMetrics metrics, int maxWidth) {
+ List lines = new ArrayList<>();
+ StringBuilder line = new StringBuilder();
+ for (String word : text.split(" ")) {
+ String candidate = line.isEmpty() ? word : line + " " + word;
+ if (metrics.stringWidth(candidate) > maxWidth && !line.isEmpty()) {
+ lines.add(line.toString());
+ line = new StringBuilder(word);
+ } else {
+ line = new StringBuilder(candidate);
+ }
+ }
+ if (!line.isEmpty()) {
+ lines.add(line.toString());
+ }
+ return lines;
+ }
+ }
+}
diff --git a/core/src/main/java/com/volmit/iris/core/gui/PaletteLayerPreview.java b/core/src/main/java/com/volmit/iris/core/gui/PaletteLayerPreview.java
new file mode 100644
index 000000000..9a4932d3b
--- /dev/null
+++ b/core/src/main/java/com/volmit/iris/core/gui/PaletteLayerPreview.java
@@ -0,0 +1,267 @@
+/*
+ * 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 .
+ */
+
+package com.volmit.iris.core.gui;
+
+import com.volmit.iris.core.loader.IrisData;
+import com.volmit.iris.engine.framework.Engine;
+import com.volmit.iris.engine.object.IrisBiome;
+import com.volmit.iris.engine.object.IrisBiomePaletteLayer;
+import com.volmit.iris.engine.object.IrisBlockData;
+import com.volmit.iris.util.collection.KList;
+import com.volmit.iris.util.math.RNG;
+import org.bukkit.block.data.BlockData;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+final class PaletteLayerPreview {
+ static final int HISTOGRAM_BINS = 50;
+ static final int STATISTICS_GRID_SIZE = 64;
+ static final double STATISTICS_SPACING_BLOCKS = 37D;
+ private static final int MISSING_BLOCK_COLOR = 0x5A5F6B;
+ private static final double FAR_FROM_WEIGHT_FACTOR = 2D;
+
+ private final String biomeKey;
+ private final IrisBiome biome;
+ private final IrisBiomePaletteLayer paletteLayer;
+ private final int layer;
+ private final int depth;
+ private final RNG random;
+ private final IrisData data;
+ private final int[] entryBySlot;
+ private final int[] colorByEntry;
+ private final List entries;
+ private final long[] histogram;
+ private final double absentShare;
+ private final double outOfRangeShare;
+
+ private PaletteLayerPreview(String biomeKey, IrisBiome biome, int layer, int depth, RNG random, IrisData data) {
+ this.biomeKey = biomeKey;
+ this.biome = biome;
+ this.paletteLayer = biome.getLayers().get(layer);
+ this.layer = layer;
+ this.depth = depth;
+ this.random = random;
+ this.data = data;
+ this.entryBySlot = paletteLayer.getPaletteEntryPerSlot(data).stream().mapToInt(Integer::intValue).toArray();
+ if (entryBySlot.length == 0) {
+ throw new IllegalStateException("No palette block in " + biomeKey + " layer " + layer + " could be found");
+ }
+ this.colorByEntry = resolveEntryColors();
+
+ Statistics statistics = measure();
+ this.histogram = statistics.histogram();
+ this.absentShare = statistics.absentShare();
+ this.outOfRangeShare = statistics.outOfRangeShare();
+ this.entries = describeEntries(statistics.sharesByEntry());
+ }
+
+ static PaletteLayerPreview load(Engine engine, String biomeKey, int layer, int depth) {
+ if (engine == null || engine.isClosed()) {
+ throw new IllegalStateException("Open a studio world to preview pack palettes");
+ }
+ IrisData data = engine.getData();
+ IrisBiome biome = data.getBiomeLoader().load(biomeKey);
+ if (biome == null || layer >= biome.getLayers().size()) {
+ throw new IllegalStateException("Palette layer removed during hotload: " + biomeKey + " layer " + layer);
+ }
+ RNG terrainRandom = new RNG(engine.getSeedManager().getTerrain());
+ return new PaletteLayerPreview(biomeKey, biome, layer, depth, terrainRandom, data);
+ }
+
+ double sample(double x, double z) {
+ if (biome.getLayerThickness(layer, x, z, random, data) <= depth) {
+ return Double.NaN;
+ }
+ return biome.getLayerNoise(layer, x, z, depth, random, data);
+ }
+
+ int colorForNoise(double noise) {
+ return colorByEntry[entryIndexForNoise(noise)];
+ }
+
+ Entry entryForNoise(double noise) {
+ return entries.get(entryIndexForNoise(noise));
+ }
+
+ String biomeKey() {
+ return biomeKey;
+ }
+
+ int layer() {
+ return layer;
+ }
+
+ int depth() {
+ return depth;
+ }
+
+ int minThickness() {
+ return paletteLayer.getMinHeight();
+ }
+
+ int maxThickness() {
+ return paletteLayer.getMaxHeight();
+ }
+
+ double zoom() {
+ return paletteLayer.getZoom();
+ }
+
+ boolean hasSlopeCondition() {
+ return !paletteLayer.getSlopeCondition().isDefault();
+ }
+
+ List entries() {
+ return entries;
+ }
+
+ long[] histogram() {
+ return histogram.clone();
+ }
+
+ double absentShare() {
+ return absentShare;
+ }
+
+ double outOfRangeShare() {
+ return outOfRangeShare;
+ }
+
+ static double statisticsAreaBlocks() {
+ return STATISTICS_GRID_SIZE * STATISTICS_SPACING_BLOCKS;
+ }
+
+ private int entryIndexForNoise(double noise) {
+ return entryBySlot[IrisBiomePaletteLayer.slotForNoise(noise, entryBySlot.length)];
+ }
+
+ private int[] resolveEntryColors() {
+ KList palette = paletteLayer.getPalette();
+ List blocks = new ArrayList<>();
+ for (IrisBlockData entry : palette) {
+ BlockData block = entry.getBlockData(data);
+ if (block != null) {
+ blocks.add(block);
+ }
+ }
+ Map colorsByBlock = BlockPreviewColors.colorsByBlock(blocks);
+ int[] colors = new int[palette.size()];
+ for (int entry = 0; entry < palette.size(); entry++) {
+ BlockData block = palette.get(entry).getBlockData(data);
+ colors[entry] = block == null ? MISSING_BLOCK_COLOR : colorsByBlock.get(block.getAsString(true));
+ }
+ return colors;
+ }
+
+ private Statistics measure() {
+ long[] values = new long[HISTOGRAM_BINS];
+ long[] samplesByEntry = new long[paletteLayer.getPalette().size()];
+ long present = 0;
+ long absent = 0;
+ long outOfRange = 0;
+ double origin = -statisticsAreaBlocks() / 2D;
+ for (int gridX = 0; gridX < STATISTICS_GRID_SIZE; gridX++) {
+ for (int gridZ = 0; gridZ < STATISTICS_GRID_SIZE; gridZ++) {
+ double noise = sample(origin + gridX * STATISTICS_SPACING_BLOCKS, origin + gridZ * STATISTICS_SPACING_BLOCKS);
+ if (Double.isNaN(noise)) {
+ absent++;
+ continue;
+ }
+ present++;
+ if (noise < 0D || noise > 1D) {
+ outOfRange++;
+ }
+ values[Math.clamp((int) (noise * HISTOGRAM_BINS), 0, HISTOGRAM_BINS - 1)]++;
+ samplesByEntry[entryIndexForNoise(noise)]++;
+ }
+ }
+ double[] sharesByEntry = new double[samplesByEntry.length];
+ for (int entry = 0; entry < samplesByEntry.length; entry++) {
+ sharesByEntry[entry] = present == 0 ? 0D : samplesByEntry[entry] / (double) present;
+ }
+ long total = present + absent;
+ return new Statistics(values, sharesByEntry, absent / (double) total, present == 0 ? 0D : outOfRange / (double) present);
+ }
+
+ private List describeEntries(double[] sharesByEntry) {
+ KList palette = paletteLayer.getPalette();
+ int slotCount = entryBySlot.length;
+ List described = new ArrayList<>(palette.size());
+ for (int entry = 0; entry < palette.size(); entry++) {
+ IrisBlockData block = palette.get(entry);
+ int firstSlot = firstSlotOf(entry);
+ int lastSlot = lastSlotOf(entry);
+ boolean placed = firstSlot >= 0;
+ described.add(new Entry(
+ block.getBlock(),
+ block.getWeight(),
+ block.getBlockData(data) != null,
+ colorByEntry[entry],
+ placed ? (lastSlot - firstSlot + 1) / (double) slotCount : 0D,
+ sharesByEntry[entry],
+ placed ? slotLowerBound(firstSlot, slotCount) : Double.NaN,
+ placed ? slotUpperBound(lastSlot, slotCount) : Double.NaN
+ ));
+ }
+ return List.copyOf(described);
+ }
+
+ private int firstSlotOf(int entry) {
+ for (int slot = 0; slot < entryBySlot.length; slot++) {
+ if (entryBySlot[slot] == entry) {
+ return slot;
+ }
+ }
+ return -1;
+ }
+
+ private int lastSlotOf(int entry) {
+ for (int slot = entryBySlot.length - 1; slot >= 0; slot--) {
+ if (entryBySlot[slot] == entry) {
+ return slot;
+ }
+ }
+ return -1;
+ }
+
+ // Slots are picked by rounding, so the first and last slot only own half a step of the value range.
+ private static double slotLowerBound(int slot, int slotCount) {
+ return slot == 0 ? 0D : (slot - 0.5D) / (slotCount - 1);
+ }
+
+ private static double slotUpperBound(int slot, int slotCount) {
+ return slot == slotCount - 1 ? 1D : (slot + 0.5D) / (slotCount - 1);
+ }
+
+ record Entry(String block, int weight, boolean found, int color, double weightShare, double actualShare,
+ double lowestNoise, double highestNoise) {
+ boolean isFarBelowWeight() {
+ return found && actualShare < weightShare / FAR_FROM_WEIGHT_FACTOR;
+ }
+
+ boolean isFarAboveWeight() {
+ return found && actualShare > weightShare * FAR_FROM_WEIGHT_FACTOR;
+ }
+ }
+
+ private record Statistics(long[] histogram, double[] sharesByEntry, double absentShare, double outOfRangeShare) {
+ }
+}
diff --git a/core/src/main/java/com/volmit/iris/core/gui/PaletteLayerScale.java b/core/src/main/java/com/volmit/iris/core/gui/PaletteLayerScale.java
new file mode 100644
index 000000000..53aec82a2
--- /dev/null
+++ b/core/src/main/java/com/volmit/iris/core/gui/PaletteLayerScale.java
@@ -0,0 +1,91 @@
+/*
+ * 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 .
+ */
+
+package com.volmit.iris.core.gui;
+
+import java.awt.Color;
+
+final class PaletteLayerScale implements NoiseColorScale {
+ static final int ABSENT_COLOR = 0x161A24;
+ private static final double OVERLAY_DARKEST_SHADE = 0.3D;
+
+ private final View view;
+ private final PaletteLayerPreview preview;
+ private final NoisePalette noisePalette;
+
+ PaletteLayerScale(View view, PaletteLayerPreview preview, NoisePalette noisePalette) {
+ this.view = view;
+ this.preview = preview;
+ this.noisePalette = noisePalette;
+ }
+
+ @Override
+ public int colorFinite(double value) {
+ return switch (view) {
+ case BLOCKS -> preview.colorForNoise(value);
+ case NOISE -> noisePalette.colorFinite(value);
+ case BLOCKS_SHADED_BY_NOISE -> BlockPreviewColors.shade(preview.colorForNoise(value), overlayShade(value));
+ };
+ }
+
+ @Override
+ public int colorInvalid() {
+ return ABSENT_COLOR;
+ }
+
+ @Override
+ public Color displayColorNormalized(double normalized) {
+ return new Color(colorFinite(minimum() + normalized * (maximum() - minimum())));
+ }
+
+ @Override
+ public String label() {
+ return view == View.NOISE ? noisePalette.label() : view.toString();
+ }
+
+ @Override
+ public double minimum() {
+ return view == View.NOISE ? noisePalette.minimum() : 0D;
+ }
+
+ @Override
+ public double maximum() {
+ return view == View.NOISE ? noisePalette.maximum() : 1D;
+ }
+
+ private static double overlayShade(double value) {
+ return OVERLAY_DARKEST_SHADE + (1D - OVERLAY_DARKEST_SHADE) * Math.clamp(value, 0D, 1D);
+ }
+
+ enum View {
+ BLOCKS("Blocks"),
+ NOISE("Noise"),
+ BLOCKS_SHADED_BY_NOISE("Blocks + noise");
+
+ private final String label;
+
+ View(String label) {
+ this.label = label;
+ }
+
+ @Override
+ public String toString() {
+ return label;
+ }
+ }
+}
diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisBiome.java b/core/src/main/java/com/volmit/iris/engine/object/IrisBiome.java
index 42caee79e..9ac7ba1bd 100644
--- a/core/src/main/java/com/volmit/iris/engine/object/IrisBiome.java
+++ b/core/src/main/java/com/volmit/iris/engine/object/IrisBiome.java
@@ -314,8 +314,7 @@ public class IrisBiome extends IrisRegistrant implements IRare {
}
for (int i = 0; i < layers.size(); i++) {
- CNG hgen = getLayerHeightGenerators(random, rdata).get(i);
- double d = hgen.fit(layers.get(i).getMinHeight(), layers.get(i).getMaxHeight(), wx / layers.get(i).getZoom(), wz / layers.get(i).getZoom());
+ double d = getLayerThickness(i, wx, wz, random, rdata);
IrisSlopeClip sc = getLayers().get(i).getSlopeCondition();
@@ -335,7 +334,7 @@ public class IrisBiome extends IrisRegistrant implements IRare {
}
try {
- data.add(getLayers().get(i).get(random.nextParallelRNG(i + j), (wx + j) / layers.get(i).getZoom(), j, (wz - j) / layers.get(i).getZoom(), rdata));
+ data.add(getLayerBlock(i, wx, wz, j, random, rdata));
} catch (Throwable e) {
Iris.reportError(e);
e.printStackTrace();
@@ -360,6 +359,25 @@ public class IrisBiome extends IrisRegistrant implements IRare {
return data;
}
+ public int getLayerThickness(int layer, double wx, double wz, RNG random, IrisData rdata) {
+ IrisBiomePaletteLayer paletteLayer = layers.get(layer);
+ CNG thicknessGenerator = getLayerHeightGenerators(random, rdata).get(layer);
+ return thicknessGenerator.fit(paletteLayer.getMinHeight(), paletteLayer.getMaxHeight(), wx / paletteLayer.getZoom(), wz / paletteLayer.getZoom());
+ }
+
+ public BlockData getLayerBlock(int layer, double wx, double wz, int depth, RNG random, IrisData rdata) {
+ return layers.get(layer).getAtDepth(layerRandom(layer, random), wx, wz, depth, rdata);
+ }
+
+ public double getLayerNoise(int layer, double wx, double wz, int depth, RNG random, IrisData rdata) {
+ return layers.get(layer).getNoiseAtDepth(layerRandom(layer, random), wx, wz, depth, rdata);
+ }
+
+ // Only the first call's random seeds the layer's cached generator, and generation always makes that call at depth 0.
+ private static RNG layerRandom(int layer, RNG random) {
+ return random.nextParallelRNG(layer);
+ }
+
public KList generateCeilingLayers(IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex) {
KList data = new KList<>();
@@ -415,8 +433,7 @@ public class IrisBiome extends IrisRegistrant implements IRare {
}
for (int i = 0; i < layers.size(); i++) {
- CNG hgen = getLayerHeightGenerators(random, rdata).get(i);
- double d = hgen.fit(layers.get(i).getMinHeight(), layers.get(i).getMaxHeight(), wx / layers.get(i).getZoom(), wz / layers.get(i).getZoom());
+ double d = getLayerThickness(i, wx, wz, random, rdata);
IrisSlopeClip sc = getLayers().get(i).getSlopeCondition();
@@ -432,7 +449,7 @@ public class IrisBiome extends IrisRegistrant implements IRare {
for (int j = 0; j < d; j++) {
try {
- data.add(getLayers().get(i).get(random.nextParallelRNG(i + j), (wx + j) / layers.get(i).getZoom(), j, (wz - j) / layers.get(i).getZoom(), rdata));
+ data.add(getLayerBlock(i, wx, wz, j, random, rdata));
} catch (Throwable e) {
Iris.reportError(e);
e.printStackTrace();
diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisBiomePaletteLayer.java b/core/src/main/java/com/volmit/iris/engine/object/IrisBiomePaletteLayer.java
index c2fa1b59f..334e63cfd 100644
--- a/core/src/main/java/com/volmit/iris/engine/object/IrisBiomePaletteLayer.java
+++ b/core/src/main/java/com/volmit/iris/engine/object/IrisBiomePaletteLayer.java
@@ -22,6 +22,7 @@ import com.volmit.iris.core.loader.IrisData;
import com.volmit.iris.engine.data.cache.AtomicCache;
import com.volmit.iris.engine.object.annotations.*;
import com.volmit.iris.util.collection.KList;
+import com.volmit.iris.util.interpolation.IrisInterpolation;
import com.volmit.iris.util.math.RNG;
import com.volmit.iris.util.noise.CNG;
import lombok.AllArgsConstructor;
@@ -69,15 +70,37 @@ public class IrisBiomePaletteLayer {
}
public BlockData get(RNG rng, double x, double y, double z, IrisData data) {
- if (getBlockData(data).isEmpty()) {
+ KList blocks = getBlockData(data);
+ if (blocks.isEmpty()) {
return null;
}
- if (getBlockData(data).size() == 1) {
- return getBlockData(data).get(0);
+ if (blocks.size() == 1) {
+ return blocks.get(0);
}
- return getLayerGenerator(rng, data).fit(getBlockData(data), x / zoom, y / zoom, z / zoom);
+ return blocks.get(slotForNoise(getLayerGenerator(rng, data).noise(x / zoom, y / zoom, z / zoom), blocks.size()));
+ }
+
+ public BlockData getAtDepth(RNG rng, double wx, double wz, int depth, IrisData data) {
+ return get(rng, depthShiftedX(wx, depth), depth, depthShiftedZ(wz, depth), data);
+ }
+
+ public double getNoiseAtDepth(RNG rng, double wx, double wz, int depth, IrisData data) {
+ return getLayerGenerator(rng, data).noise(depthShiftedX(wx, depth) / zoom, depth / zoom, depthShiftedZ(wz, depth) / zoom);
+ }
+
+ public static int slotForNoise(double noise, int slotCount) {
+ int slot = (int) Math.round(IrisInterpolation.lerp(0, slotCount - 1, noise));
+ return slot >= 0 && slot < slotCount ? slot : 0;
+ }
+
+ private double depthShiftedX(double wx, int depth) {
+ return (wx + depth) / zoom;
+ }
+
+ private double depthShiftedZ(double wz, int depth) {
+ return (wz - depth) / zoom;
}
public CNG getLayerGenerator(RNG rng, IrisData data) {
@@ -98,19 +121,30 @@ public class IrisBiomePaletteLayer {
return blockData.aquire(() ->
{
KList blockData = new KList<>();
- for (IrisBlockData ix : palette) {
- BlockData bx = ix.getBlockData(data);
- if (bx != null) {
- for (int i = 0; i < ix.getWeight(); i++) {
- blockData.add(bx);
- }
- }
+ for (int entry : getPaletteEntryPerSlot(data)) {
+ blockData.add(palette.get(entry).getBlockData(data));
}
return blockData;
});
}
+ public KList getPaletteEntryPerSlot(IrisData data) {
+ KList entries = new KList<>();
+ for (int entry = 0; entry < palette.size(); entry++) {
+ IrisBlockData block = palette.get(entry);
+ if (block.getBlockData(data) == null) {
+ continue;
+ }
+
+ for (int i = 0; i < block.getWeight(); i++) {
+ entries.add(entry);
+ }
+ }
+
+ return entries;
+ }
+
public IrisBiomePaletteLayer zero() {
palette.clear();
return this;