diff --git a/src/main/java/amidst/gui/main/Actions.java b/src/main/java/amidst/gui/main/Actions.java index c24a9a30..938a320b 100644 --- a/src/main/java/amidst/gui/main/Actions.java +++ b/src/main/java/amidst/gui/main/Actions.java @@ -25,8 +25,6 @@ import amidst.gui.crash.CrashWindow; import amidst.gui.main.menu.MovePlayerPopupMenu; import amidst.gui.main.viewer.ViewerFacade; import amidst.gui.seedsearcher.SeedSearcherWindow; -import amidst.gui.text.TextWindow; -import amidst.gui.voronoi.VoronoiWindow; import amidst.logging.AmidstLogger; import amidst.minetest.world.mapgen.DefaultBiomes; import amidst.mojangapi.world.WorldSeed; @@ -37,7 +35,6 @@ import amidst.mojangapi.world.player.Player; import amidst.mojangapi.world.player.PlayerCoordinates; import amidst.settings.biomeprofile.BiomeAuthority; import amidst.settings.biomeprofile.BiomeProfile; -import amidst.settings.biomeprofile.BiomeProfileSelection; import amidst.util.FileExtensionChecker; @NotThreadSafe @@ -325,7 +322,7 @@ public class Actions { generatorOptions = viewerFacade.getGeneratorOptions(); } if (generatorOptions != null && generatorOptions.length() > 0) { - TextWindow.showMonospace("World mapgen options", generatorOptions); + dialogs.displayMonospaceText("World mapgen options", generatorOptions); } else { dialogs.displayInfo("World MapGen options", "There are currently no mapgen options in use"); } @@ -338,7 +335,11 @@ public class Actions { @CalledOnlyBy(AmidstThread.EDT) public void displayBiomeProfileVoronoi() { if (gameEngineDetails.getType() != GameEngineType.MINECRAFT) { - VoronoiWindow.showDiagram(biomeAuthority.getBiomeProfileSelection()); + // Passing a null IClimateHistogram because the default one is currently the + // only one implemented anyway. + // It might be worth passing this value once Amidstest is reading game profiles, + // as then it will at least know if the current world DOESN'T use the default climate settings. + dialogs.displayVoronoiDiagram(biomeAuthority.getBiomeProfileSelection(), null); } else { dialogs.displayInfo("Biome profile Voronoi diagram", "Minecraft biomes are not determined by heat and humidity."); } diff --git a/src/main/java/amidst/gui/main/MainWindowDialogs.java b/src/main/java/amidst/gui/main/MainWindowDialogs.java index 83334c8f..3839a21c 100644 --- a/src/main/java/amidst/gui/main/MainWindowDialogs.java +++ b/src/main/java/amidst/gui/main/MainWindowDialogs.java @@ -13,12 +13,16 @@ import amidst.documentation.CalledOnlyBy; import amidst.documentation.NotThreadSafe; import amidst.gameengineabstraction.GameEngineType; import amidst.gameengineabstraction.world.WorldTypes; +import amidst.gui.text.TextWindow; +import amidst.gui.voronoi.VoronoiWindow; import amidst.logging.AmidstMessageBox; +import amidst.minetest.world.mapgen.IHistogram2D; import amidst.mojangapi.RunningLauncherProfile; import amidst.mojangapi.world.WorldSeed; import amidst.mojangapi.world.WorldType; import amidst.mojangapi.world.export.WorldExporterConfiguration; import amidst.mojangapi.world.player.WorldPlayerType; +import amidst.settings.biomeprofile.BiomeProfileSelection; @NotThreadSafe public class MainWindowDialogs { @@ -99,6 +103,11 @@ public class MainWindowDialogs { AmidstMessageBox.displayInfo(frame, title, message); } + @CalledOnlyBy(AmidstThread.EDT) + public void displayMonospaceText(String title, String content) { + TextWindow.showMonospace(frame, title, content); + } + @CalledOnlyBy(AmidstThread.EDT) public void displayError(String message) { AmidstMessageBox.displayError(frame, "Error", message); @@ -108,6 +117,11 @@ public class MainWindowDialogs { public void displayError(Exception e) { AmidstMessageBox.displayError(frame, "Error", e); } + + @CalledOnlyBy(AmidstThread.EDT) + public void displayVoronoiDiagram(BiomeProfileSelection biome_profile_selection, IHistogram2D climate_histogram) { + VoronoiWindow.showDiagram(frame, biome_profile_selection, climate_histogram); + } @CalledOnlyBy(AmidstThread.EDT) public boolean askToConfirmSaveGameManipulation() { diff --git a/src/main/java/amidst/gui/text/TextWindow.java b/src/main/java/amidst/gui/text/TextWindow.java index 77607bb6..59e2cad2 100644 --- a/src/main/java/amidst/gui/text/TextWindow.java +++ b/src/main/java/amidst/gui/text/TextWindow.java @@ -1,6 +1,7 @@ package amidst.gui.text; import java.awt.Color; +import java.awt.Component; import java.awt.Font; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; @@ -21,8 +22,8 @@ import net.miginfocom.swing.MigLayout; public class TextWindow { private static final TextWindow monospaceWindow = new TextWindow(Font.MONOSPACED); - public static void showMonospace(String title, String content) { - SwingUtilities.invokeLater(() -> monospaceWindow.show(title, content)); + public static void showMonospace(Component parent, String title, String content) { + SwingUtilities.invokeLater(() -> monospaceWindow.show(parent, title, content)); } private final JFrame frame; @@ -66,9 +67,10 @@ public class TextWindow { } @CalledOnlyBy(AmidstThread.EDT) - public void show(String title, String content) { + public void show(Component parent, String title, String content) { this.contentTextArea.setText(content); this.frame.setTitle(title); + this.frame.setLocationRelativeTo(parent); this.frame.setVisible(true); } } diff --git a/src/main/java/amidst/gui/voronoi/FrequencyGraph.java b/src/main/java/amidst/gui/voronoi/FrequencyGraph.java new file mode 100644 index 00000000..c6b4f133 --- /dev/null +++ b/src/main/java/amidst/gui/voronoi/FrequencyGraph.java @@ -0,0 +1,134 @@ +package amidst.gui.voronoi; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; + +import javax.vecmath.Point2d; + +import amidst.minetest.world.mapgen.IHistogram2D; + +public class FrequencyGraph { + + private int width; + private int height; + private BufferedImage graph; + + private IHistogram2D last_histogram2d; + int last_x_min, last_x_max, last_y_min, last_y_max; + + /** + * Returns an image of the frequency distribution graph given by histogram2d + * @param histogram2d + * @param x_min - start of the x axis for the graph + * @param x_max - end of the x axis for the graph + * @param y_min - start of the y axis for the graph + * @param y_max - end of the y axis for the graph + * @param step - the increment per pixel, i.e. a step of 0.25 means 4 pixels to increment along an axis by 1 + * @return an image of the frequency data + */ + public BufferedImage render(IHistogram2D histogram2d, int x_min, int x_max, int y_min, int y_max) { + + int xRange = 1 + x_max - x_min; // inclusive range + int yRange = 1 + y_max - y_min; // inclusive range + float xScale = xRange / (float)width; + float yScale = yRange / (float)height; + + if (graph == null || graph.getWidth() != width || graph.getHeight() != height) { + graph = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + } else { + if (histogram2d.equals(last_histogram2d) && + x_min == last_x_min && x_max == last_x_max && + y_min == last_y_min && y_max == last_y_max) { + // It's the same as last time + return graph; + } + } + last_histogram2d = histogram2d; + last_x_min = x_min; + last_x_max = x_max; + last_y_min = y_min; + last_y_max = y_max; + + + // No math here, these minDist values for band thicknesses were set by trial/eyeballing + double minDistRed = 0.0000017; + double minDistBlue = 0.000001; + double minDistBase = 0.00000001; + + // the 0.01 percentile is the lowest the ClimateHistogram lookup tables store values for. + double bottomFrequency = histogram2d.frequencyAtPercentile(0.01); + + Point2d mean = histogram2d.getSampleMean(); + int xMean = (int)Math.round((mean.x - x_min) / xScale); + int yMean = (int)Math.round((mean.y - y_min) / yScale); + + + for (int y = 0; y < height; y++) { + float histogramY = (y * yScale) + y_min; + + for (int x = 0; x < width; x++) { + float histogramX = (x * xScale) + x_min; + + // set transparent + graph.setRGB(x, y, 0x00000000); + + double distance; + double frequency = histogram2d.frequencyOfOccurance(histogramX, histogramY); + + if (frequency == 0) { + // No values were ever seen in this location, color it gray to indicate + // the climate doesn't reach these values. + graph.setRGB(x, y, 0xFF707070); + + } else if (frequency < (bottomFrequency + minDistBase)) { + // It is exceeding rare for the climate to ever reach these values. + // Draw a red quartile boundary line here. + if (frequency > (bottomFrequency - minDistBase)) { + + // trying to make a dotted circle while avoiding sin() or cos() ;) + int absX = (int)Math.abs(x - xMean); + int absY = (int)Math.abs(y - yMean); + int travel = Math.abs((absX < absY) ? absX - (absY / 2) : absY - (absX / 2)); + + if(((travel / 8) & 1) > 0) { + distance = Math.abs(frequency - bottomFrequency); + graph.setRGB(x, y, 0x00FF3030 | (255 - (int)Math.round((255 * distance) / minDistBase)) << 24); + } + } + } else { + + for(int percentile = 10; percentile <= 90; percentile += 5) { + + if ((percentile % 25) == 0) { + distance = Math.abs(frequency - histogram2d.frequencyAtPercentile(percentile)); + if (distance <= minDistRed) { + // Draw a quartile boundary line here. + graph.setRGB(x, y, 0x00FF3030 | (255 - (int)Math.round((255 * distance) / minDistRed)) << 24); + } + } else if ((percentile % 10) == 0) { + distance = Math.abs(frequency - histogram2d.frequencyAtPercentile(percentile)); + if (distance <= minDistBlue) { + // Draw a 10-percentile boundary line here. + graph.setRGB(x, y, 0x003050FF | (255 - (int)Math.round((255 * distance) / minDistBlue)) << 24); + } + } + } + } + } + } + + // Draw the mean, as the frequency data will be almost flat there, and random noise + // may prevent that looking point-like. + Graphics2D g2d = graph.createGraphics(); + g2d.setColor(Color.RED); + g2d.fillOval(xMean - 4, yMean - 4, 9, 9); + + return graph; + } + + public FrequencyGraph(int width, int height) { + this.width = width; + this.height = height; + } +} diff --git a/src/main/java/amidst/gui/voronoi/GraphNode.java b/src/main/java/amidst/gui/voronoi/GraphNode.java index 972a475a..39166fcc 100644 --- a/src/main/java/amidst/gui/voronoi/GraphNode.java +++ b/src/main/java/amidst/gui/voronoi/GraphNode.java @@ -7,6 +7,8 @@ public class GraphNode { float x; float y; + double occurrenceFrequency; + @Override public boolean equals(Object obj) { if (obj == null) return false; diff --git a/src/main/java/amidst/gui/voronoi/VoronoiGraph.java b/src/main/java/amidst/gui/voronoi/VoronoiGraph.java index 4bb5abd8..8d11c6e3 100644 --- a/src/main/java/amidst/gui/voronoi/VoronoiGraph.java +++ b/src/main/java/amidst/gui/voronoi/VoronoiGraph.java @@ -3,16 +3,20 @@ package amidst.gui.voronoi; import java.awt.image.BufferedImage; import java.util.List; +import amidst.minetest.world.mapgen.IHistogram2D; + /** - * Draws a Voronoi diagram, currently using the brute-force method + * Draws a Voronoi diagram, currently using the brute-force method. + * */ public class VoronoiGraph { - + private BufferedImage graph; private int[] bufferArray; private int width; private int height; - + private IHistogram2D climateHistogram; + /** Create the graph BufferedImage if it doesn't already exist at the correct size */ private void createBufferedImage() { if (graph == null || graph.getWidth() != width || graph.getHeight() != height) { @@ -21,10 +25,20 @@ public class VoronoiGraph { } } + /** + * Returns an image of the Vonoroi graph, and if a climateHistogram has been provided then + * it calculates and sets the occurrenceFrequency field in each of the GraphNodes. + * @param nodes + * @param x_min - start of the x axis for the graph + * @param x_max - end of the x axis for the graph + * @param y_min - start of the y axis for the graph + * @param y_max - end of the y axis for the graph + * @return an image of the Vonoroi graph + */ public BufferedImage render(List nodes, int x_min, int x_max, int y_min, int y_max) { - + createBufferedImage(); - + GraphNode closestNode; float closestDist; float xScale = (x_max - x_min) / (float)width; @@ -34,21 +48,34 @@ public class VoronoiGraph { int bufferArrayLines = bufferArray.length / width; // number of rasterlines we can write to bufferArray before it needs to be written to the BufferedImage int bufferArrayIndex = 0; - // Convert to array for hopefully faster indexed access + int occurrenceHistogramY = Integer.MIN_VALUE; + double[] occurrenceHistogram = new double[1 + x_max - x_min]; + + // Convert the List to an array for hopefully faster indexed access GraphNode[] nodeArray = nodes.toArray(new GraphNode[nodes.size()]); int nodeCount = nodeArray.length; - + + // Init/zero the occurrenceFrequency data field in each node + for (short i = 0; i < nodeCount; i++) nodeArray[i].occurrenceFrequency = 0; + for (int y = 0; y < height; y++) { float valueAtY = (y * yScale) + y_min; if (y - bufferArrayY >= bufferArrayLines) { // flush bufferArray into the BufferedImage. // (I'm hoping Java has this optimized, if not, there's no need for rgbArray) - graph.setRGB(0, bufferArrayY, width, bufferArrayLines, bufferArray, 0, width); + graph.setRGB(0, bufferArrayY, width, bufferArrayLines, bufferArray, 0, width); bufferArrayY = y; bufferArrayIndex = 0; } + // Populate occuranceHistogram for this value of y + if (Math.round(y * yScale) != occurrenceHistogramY && climateHistogram != null) { + int yIndex = Math.round(y * yScale); + for (int i = x_max - x_min; i >= 0; i--) occurrenceHistogram[i] = climateHistogram.frequencyOfOccurance(i + x_min, yIndex + y_min); + occurrenceHistogramY = yIndex; + } + for (int x = 0; x < width; x++) { //float heat = (x * xScale) + axis_min; float valueAtX = (x * xScale) + x_min; @@ -67,19 +94,31 @@ public class VoronoiGraph { closestNode = node; } } - bufferArray[bufferArrayIndex++] = (closestNode == null) ? 0x00000000 : closestNode.argb; + if (closestNode != null) { + bufferArray[bufferArrayIndex++] = closestNode.argb; + closestNode.occurrenceFrequency += occurrenceHistogram[Math.round(x * xScale)]; + } else { + bufferArray[bufferArrayIndex++] = 0x00000000; + } } } if (bufferArrayIndex > 0) { // flush the rest of bufferArray into the BufferedImage. - graph.setRGB(0, bufferArrayY, width, bufferArrayIndex / width, bufferArray, 0, width); + graph.setRGB(0, bufferArrayY, width, bufferArrayIndex / width, bufferArray, 0, width); } - + + // Correct occurrenceFrequency for the oversampling caused by difference between + // size of BufferedImage vs histogram resolution + for (short i = 0; i < nodeCount; i++) { + nodeArray[i].occurrenceFrequency = nodeArray[i].occurrenceFrequency * xScale * yScale; + } + return graph; } - - public VoronoiGraph(int width, int height) { + + public VoronoiGraph(int width, int height, IHistogram2D climate_histogram) { this.width = width; this.height = height; + this.climateHistogram = climate_histogram; } } diff --git a/src/main/java/amidst/gui/voronoi/VoronoiPanel.java b/src/main/java/amidst/gui/voronoi/VoronoiPanel.java index 58f56827..7024c17a 100644 --- a/src/main/java/amidst/gui/voronoi/VoronoiPanel.java +++ b/src/main/java/amidst/gui/voronoi/VoronoiPanel.java @@ -1,7 +1,9 @@ package amidst.gui.voronoi; +import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Color; +import java.awt.Composite; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; @@ -16,32 +18,35 @@ import java.util.ArrayList; import java.util.List; import javax.swing.JPanel; +import javax.swing.SwingUtilities; import amidst.documentation.AmidstThread; import amidst.documentation.CalledByAny; import amidst.documentation.CalledOnlyBy; import amidst.gameengineabstraction.world.biome.IBiome; +import amidst.minetest.world.mapgen.ClimateHistogram; +import amidst.minetest.world.mapgen.IHistogram2D; import amidst.minetest.world.mapgen.MinetestBiome; import amidst.minetest.world.mapgen.MinetestBiomeProfileImpl; /** - * Panel which displays a Voronoi graph of Minetest biomes + * Panel which displays a Voronoi graph of Minetest biomes */ public class VoronoiPanel extends JPanel { public static final int FLAG_SHOWLABELS = 0x01; public static final int FLAG_SHOWAXIS = 0x02; public static final int FLAG_SHOWNODES = 0x04; - public static final int FLAG_SHOWDISTRIBUTION = 0x08; + public static final int FLAG_SHOWCOVERAGE = 0x08; public static final boolean GRAPHICS_DEBUG = false; private static final float AXIS_WIDTH = 0.5f; - private static final int TICKMARK_WIDTH_SMALL = 3; - private static final int TICKMARK_WIDTH_LARGE = 6; + private static final int TICKMARK_WIDTH_SMALL = 2; + private static final int TICKMARK_WIDTH_LARGE = 4; private static final int TICKMARK_LABEL_SPACE = 1; private static final int NODE_RADIUS = 0; - private static final int NODE_LABEL_SPACE = 0; + private static final int NODE_LABEL_SPACE = 2; private static final int NODE_LABEL_FONTSIZE = 3; private static final long serialVersionUID = 1L; @@ -54,18 +59,23 @@ public class VoronoiPanel extends JPanel { public int axis_max = 140; public int graph_resolution = 1000; - //private MinetestBiome[] nodes; + private IHistogram2D climateHistogram = null; ArrayList biomes = new ArrayList(); - private List graphNodes = null; + private List graphNodes = null; + private VoronoiGraph voronoiGraph = null; + private FrequencyGraph frequencyGraph = null; + private float frequencyGraphOpacity = 0; private int renderFlags; - private VoronoiGraph graph = null; @Override @CalledOnlyBy(AmidstThread.EDT) public void paintComponent(Graphics g) { super.paintComponent(g); - if (graph == null) graph = new VoronoiGraph(graph_resolution, graph_resolution); + boolean frequencyGraphWasNull = frequencyGraph == null; + if (climateHistogram == null) climateHistogram = new ClimateHistogram(); + if (voronoiGraph == null) voronoiGraph = new VoronoiGraph(graph_resolution, graph_resolution, climateHistogram); + if (frequencyGraph == null) frequencyGraph = new FrequencyGraph(graph_resolution, graph_resolution); this.setBorder(null); Rectangle panelBounds = getBounds(); @@ -102,10 +112,10 @@ public class VoronoiPanel extends JPanel { g2d.setColor(Color.WHITE); g2d.fillRect(axis_min, axis_min, axis_max - axis_min, axis_max - axis_min); - // Draw the filled voronoi graph + // Draw the filled voronoi graph if (graphNodes != null) { g2d.drawImage( - graph.render(graphNodes, axis_min, axis_max, axis_min, axis_max), + voronoiGraph.render(graphNodes, axis_min, axis_max, axis_min, axis_max), axis_min, axis_min, desiredAxisLength, desiredAxisLength, Color.WHITE, null ); } @@ -113,18 +123,49 @@ public class VoronoiPanel extends JPanel { // draw nodes drawNodesOrNodeLabels(g2d); + // overlay the frequency graph + if (frequencyGraphOpacity > 0) { + int rule = AlphaComposite.SRC_OVER; + Composite alphaComposite = AlphaComposite.getInstance(rule , frequencyGraphOpacity); + Composite originalComposite = g2d.getComposite(); + g2d.setComposite(alphaComposite); + try { + if (graphNodes != null) { + g2d.drawImage( + frequencyGraph.render(climateHistogram, axis_min, axis_max, axis_min, axis_max), + axis_min, axis_min, desiredAxisLength, desiredAxisLength, null, null + ); + } + } finally { + g2d.setComposite(originalComposite); + } + } + // draw axis if ((renderFlags & FLAG_SHOWAXIS) > 0) drawAxes(g2d); } + + + if (frequencyGraphWasNull && frequencyGraphOpacity <= 0) { + // In order to prevent a delay when the user enables the frequencyGraph, + // Make it pre-render after the UI has finished updating. + SwingUtilities.invokeLater(() -> { + frequencyGraph.render(climateHistogram, axis_min, axis_max, axis_min, axis_max); + }); + } } @CalledOnlyBy(AmidstThread.EDT) private void drawNodesOrNodeLabels(Graphics2D g2d) { AffineTransform currentTransform = g2d.getTransform(); - FontMetrics metrics = g2d.getFontMetrics(g2d.getFont()); + Font font_original = g2d.getFont(); + + FontMetrics fontMetrics = g2d.getFontMetrics(font_original); boolean showNodes = (renderFlags & FLAG_SHOWNODES) > 0; boolean showLabels = (renderFlags & FLAG_SHOWLABELS) > 0; + boolean showCoverage = (renderFlags & FLAG_SHOWCOVERAGE) > 0; + int biomeIndex = 0; for (MinetestBiome biome: biomes) { int x = Math.round(biome.heat_point); @@ -140,18 +181,36 @@ public class VoronoiPanel extends JPanel { g2d.fillOval(x - NODE_RADIUS, y - NODE_RADIUS, 1 + NODE_RADIUS * 2, 1 + NODE_RADIUS * 2); } - if (showLabels) { - Point2D fontPos = new Point(x, y - (showNodes ? NODE_LABEL_SPACE : 0)); - Point2D fontPosTransformed = currentTransform.transform(fontPos, null); + Point2D fontPos = new Point(x, y - (showNodes ? NODE_LABEL_SPACE : 0)); + Point2D fontPosTransformed = currentTransform.transform(fontPos, null); + float fontY = (float)fontPosTransformed.getY() + (fontMetrics.getAscent() / 4f); - g2d.setTransform(noTransform); - g2d.drawString( - biome.getName(), - (float)fontPosTransformed.getX() - (metrics.stringWidth(biome.getName()) / 2), - (float)fontPosTransformed.getY() + (metrics.getAscent() / (showNodes ? 1 : -4)) - ); + g2d.setTransform(noTransform); + try { + if (showLabels) { + g2d.drawString( + biome.getName(), + (float)fontPosTransformed.getX() - (fontMetrics.stringWidth(biome.getName()) / 2), + fontY + ); + fontY += fontMetrics.getAscent(); + } + + if (showCoverage) { + String value = String.format(" %.1f", graphNodes.get(biomeIndex).occurrenceFrequency * 100); + if (value.charAt(value.length() - 1) == '0') value = value.substring(0, value.length() - 2); + value += "%"; + + g2d.drawString( + value, + (float)fontPosTransformed.getX() - (fontMetrics.stringWidth(value) / 2), + fontY + ); + } + } finally { g2d.setTransform(currentTransform); - } + } + biomeIndex++; } } @@ -191,8 +250,9 @@ public class VoronoiPanel extends JPanel { ); g2d.setTransform(currentTransform); } else { - g2d.drawLine(0, i, -TICKMARK_WIDTH_SMALL, i); - g2d.drawLine(i, 0, i, -TICKMARK_WIDTH_SMALL); + int tickmarkWidth = (i % 50 == 0) ? (int)Math.ceil((TICKMARK_WIDTH_SMALL + TICKMARK_WIDTH_LARGE) / 2f) : TICKMARK_WIDTH_SMALL; + g2d.drawLine(0, i, -tickmarkWidth, i); + g2d.drawLine(i, 0, i, -tickmarkWidth); } } } @@ -248,29 +308,48 @@ public class VoronoiPanel extends JPanel { .114 * Math.pow(col.getBlue(), 2) ); } - - public int getRenderFlags() { return renderFlags; } + + public int getRenderFlags() { return renderFlags; } + + public String getDistributionData() { + StringBuilder result = new StringBuilder(); + + int biomeIndex = 0; + for (MinetestBiome biome: biomes) { + result.append(biome.getName()); + result.append(", "); + result.append(String.format(" %.1f%%\r\n", graphNodes.get(biomeIndex).occurrenceFrequency * 100)); + biomeIndex++; + } + return result.toString(); + } + + /** + * If the world doesn't use Minetest's default Heat&Humidity algorithm, then pass + * a histogram of it here. + */ + public void setClimateHistogram(IHistogram2D climate_histogram) { this.climateHistogram = climate_histogram; } @CalledOnlyBy(AmidstThread.EDT) - public void Update(MinetestBiomeProfileImpl biomeProfile, int height, int flags) { + public void Update(MinetestBiomeProfileImpl biomeProfile, int altitude, float frequency_graph_opacity, int flags) { ArrayList newBiomes = new ArrayList(); if (biomeProfile != null) for (IBiome biome : biomeProfile.allBiomes()) { MinetestBiome mtBiome = (MinetestBiome)biome; - if (height <= (mtBiome.y_max + mtBiome.vertical_blend) && height >= mtBiome.y_min) { + if (altitude <= (mtBiome.y_max + mtBiome.vertical_blend) && altitude >= mtBiome.y_min) { newBiomes.add(mtBiome); } } - //ArrayList currentBiomes = this.biomes == null ? new ArrayList() : new ArrayList(Arrays.asList(nodes)); - if (flags != this.renderFlags || !newBiomes.equals(biomes)) { + if (flags != this.renderFlags || !newBiomes.equals(biomes) || frequencyGraphOpacity != frequency_graph_opacity) { this.renderFlags = flags; biomes = newBiomes; + frequencyGraphOpacity = frequency_graph_opacity; graphNodes = new ArrayList(); for(MinetestBiome biome: newBiomes) { graphNodes.add(new GraphNode(biome.heat_point, biome.humidity_point, biome.getDefaultColor().getRGB())); } - + repaint(); } } diff --git a/src/main/java/amidst/gui/voronoi/VoronoiWindow.java b/src/main/java/amidst/gui/voronoi/VoronoiWindow.java index c6f8964e..48ec709e 100644 --- a/src/main/java/amidst/gui/voronoi/VoronoiWindow.java +++ b/src/main/java/amidst/gui/voronoi/VoronoiWindow.java @@ -1,22 +1,39 @@ package amidst.gui.voronoi; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; import java.awt.EventQueue; +import java.awt.Image; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; +import java.util.Hashtable; +import javax.swing.ImageIcon; +import javax.swing.JButton; import javax.swing.JCheckBox; +import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JSlider; import javax.swing.JSpinner; import javax.swing.SpinnerNumberModel; import javax.swing.SwingUtilities; +import javax.swing.border.Border; +import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; +import javax.swing.border.LineBorder; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import net.miginfocom.swing.MigLayout; +import amidst.ResourceLoader; import amidst.documentation.AmidstThread; import amidst.documentation.CalledOnlyBy; +import amidst.gui.text.TextWindow; +import amidst.minetest.world.mapgen.IHistogram2D; import amidst.minetest.world.mapgen.MinetestBiomeProfileImpl; import amidst.settings.biomeprofile.BiomeProfile; import amidst.settings.biomeprofile.BiomeProfileSelection; @@ -33,27 +50,25 @@ public class VoronoiWindow implements BiomeProfileUpdateListener, ChangeListener private final JFrame windowFrame; private VoronoiPanel voronoiPanel; - private JSlider altitudeSlider; - private JLabel graphHeading; + private JSlider altitudeSlider; + private JSlider freqGraphSlider; + private JLabel graphHeading; + private JSpinner altitudeOffset; private JCheckBox option_showAxis; private JCheckBox option_showLabels; private JCheckBox option_showNodes; - private JSpinner altitudeOffset; + private JCheckBox option_showCoverage; private MinetestBiomeProfileImpl selectedProfile = null; - @CalledOnlyBy(AmidstThread.EDT) - private VoronoiWindow() { - this.windowFrame = createWindowFrame(800, 764); - setOptionFlagsInDialog(VoronoiPanel.FLAG_SHOWLABELS | VoronoiPanel.FLAG_SHOWAXIS | VoronoiPanel.FLAG_SHOWNODES); - } @CalledOnlyBy(AmidstThread.EDT) - private JFrame createWindowFrame(int width, int height) { + private JFrame createWindowFrame(Component parent, int width, int height) { JFrame result = new JFrame(); - result.getContentPane().setLayout(new MigLayout()); + result.getContentPane().setLayout(new MigLayout(/*"debug"/**/)); result.setSize(width, height); + result.setLocationRelativeTo(parent); result.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { @@ -61,47 +76,142 @@ public class VoronoiWindow implements BiomeProfileUpdateListener, ChangeListener } }); - this.voronoiPanel = new VoronoiPanel(); - result.add(voronoiPanel, "grow, pushx, spany 2");// the next row (which we span) will be "push", so don't do it here - the rest of this row needs to be thin + this.option_showAxis = createControl_VoronoiOption(VoronoiPanel.FLAG_SHOWAXIS); + this.option_showNodes = createControl_VoronoiOption(VoronoiPanel.FLAG_SHOWNODES); + this.option_showLabels = createControl_VoronoiOption(VoronoiPanel.FLAG_SHOWLABELS); + this.option_showCoverage = createControl_VoronoiOption(VoronoiPanel.FLAG_SHOWCOVERAGE); - JLabel heightLabel = new JLabel("Altitude"); - result.add(heightLabel, "center, wrap"); - - altitudeSlider = new JSlider(JSlider.VERTICAL, ALTITUDESLIDER_DEFAULT_LOW, ALTITUDESLIDER_DEFAULT_HIGH, ALTITUDESLIDER_STARTING_VALUE); - altitudeSlider.addChangeListener(this); - altitudeSlider.setMajorTickSpacing(10); - altitudeSlider.setMinorTickSpacing(5); - altitudeSlider.setPaintTicks(true); - altitudeSlider.setPaintLabels(true); - result.add(altitudeSlider, "grow, pushy, wrap"); - - graphHeading = new JLabel(); - result.add(graphHeading, "center"); - - JLabel offsetLabel = new JLabel("Altitude offset:"); - result.add(offsetLabel, "left, wrap"); - - option_showAxis = new JCheckBox("Show axes"); - option_showNodes = new JCheckBox("Show nodes"); - option_showLabels = new JCheckBox("Show labels"); - option_showAxis.addChangeListener(this); - option_showNodes.addChangeListener(this); - option_showLabels.addChangeListener(this); - result.add(option_showAxis, "center, split 3"); - result.add(option_showNodes, "center"); - result.add(option_showLabels, "center"); - - altitudeOffset = new JSpinner(new SpinnerNumberModel(0, Short.MIN_VALUE - ALTITUDESLIDER_DEFAULT_LOW, Short.MAX_VALUE - ALTITUDESLIDER_DEFAULT_HIGH, 100)); - altitudeOffset.addChangeListener(this); - result.add(altitudeOffset); + // Place the controls in the window + JLabel altOffsetLablel; + result.add(this.voronoiPanel = new VoronoiPanel(), "grow, pushx, spany 2"); // the next row (which we span) will be "push", so don't do it here - the rest of this row needs to be thin + result.add(new JLabel("Altitude"), "center, wrap"); + result.add(this.altitudeSlider = createControl_AltitudeSlider(), "grow, pushy, wrap"); + result.add(this.graphHeading = new JLabel(), "center"); + result.add(altOffsetLablel = new JLabel("Altitude offset:"), "left, wrap"); + result.add(createControl_InfoButton(), "split 6"); + result.add(this.freqGraphSlider = createControl_FreqGraphSlider(), "gapx push push"); + result.add(this.option_showAxis, "gapx push push"); + result.add(this.option_showLabels, "gapx push push"); + result.add(this.option_showNodes, "gapx push push"); + result.add(this.option_showCoverage, "gapx push push"); + result.add(altitudeOffset = createControl_AltitudeOffset(altOffsetLablel)); return result; } + /** Creates a slider which controls the altitude displayed in the Voronoi diagram */ + private JSlider createControl_AltitudeSlider() { + JSlider result = new JSlider( + JSlider.VERTICAL, + ALTITUDESLIDER_DEFAULT_LOW, + ALTITUDESLIDER_DEFAULT_HIGH, + ALTITUDESLIDER_STARTING_VALUE + ); + result.addChangeListener(this); + result.setMajorTickSpacing(10); + result.setMinorTickSpacing(5); + result.setPaintTicks(true); + result.setPaintLabels(true); + return result; + } + + /** Creates a button which shows the data in text form */ + private JButton createControl_InfoButton() { + JButton result = new JButton(); + result.setToolTipText("Show data, legend, and notes"); + Image icon = ResourceLoader.getImage("/amidst/gui/main/dataicon.png"); + result.setIcon(new ImageIcon(icon)); + //infoButton.setBackground(Color.WHITE); + //infoButton.setOpaque(false); + Border line = new LineBorder(Color.BLACK); + Border margin = new EmptyBorder(2, 2, 2, 2); + Border compound = new CompoundBorder(line, margin); + result.setBorder(compound); + result.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent actionEvent) { + InfoButtonClicked(); + } + }); + return result; + } + + /** creates a slider for controlling the visibility of the frequency distribution graph */ + private JSlider createControl_FreqGraphSlider() { + JSlider result = new JSlider(JSlider.HORIZONTAL, 0, 100, 0); + result.setToolTipText("Hides or shows a graph of how temperature and humidity are distributed in the world.
Click the \"Show data\" button on the left for more details."); + result.addChangeListener(this); + result.setMajorTickSpacing(100); + result.setMinorTickSpacing(25); + result.setPaintTicks(true); + Hashtable labels = new Hashtable(); + JLabel sliderLabel = new JLabel("Show distribution"); + labels.put(0, new JLabel("")); // otherwise it allocates space at the end for "Show distribution" + labels.put(50, sliderLabel); + labels.put(100, new JLabel("")); // otherwise it allocates space at the end for "Show distribution" + result.setLabelTable(labels); + result.setPaintLabels(true); + result.setPreferredSize( + new Dimension( + sliderLabel.getPreferredSize().width, + result.getPreferredSize().height + ) + ); + return result; + } + + /** Creates a CheckBox for specifying one of the VoronoiPanel option flags */ + private JCheckBox createControl_VoronoiOption(int voronoi_panel_flag) { + + String caption, tooltip; + switch (voronoi_panel_flag) { + case VoronoiPanel.FLAG_SHOWAXIS: + caption = "Show axes"; + tooltip = "Set whether temperature and humidity axis are displayed"; + break; + case VoronoiPanel.FLAG_SHOWNODES: + caption = "Show nodes"; + tooltip = "Set whether temperature and humidity positions of each biome are displayed"; + break; + case VoronoiPanel.FLAG_SHOWLABELS: + caption = "Show labels"; + tooltip = "Set whether the biomes and axis are labelled"; + break; + default: + caption = "Show %world covered"; + tooltip = "Set whether to show percentage of the world covered by each biome at this altitude"; + break; + } + JCheckBox result = new JCheckBox(caption); + result.setToolTipText(tooltip); + result.addChangeListener(this); + return result; + } + + /** + * Creates a numeric spinner so the range provided by the AltitudeSlider can be adjusted. + * @param label - the label to try to match the width of the JSpinner to + */ + private JSpinner createControl_AltitudeOffset(JLabel label) { + JSpinner result = new JSpinner(new SpinnerNumberModel(0, Short.MIN_VALUE - ALTITUDESLIDER_DEFAULT_LOW, Short.MAX_VALUE - ALTITUDESLIDER_DEFAULT_HIGH, 100)); + result.setToolTipText("Allows the range of the altitude slider to be adjusted.
Allowing for example, to view the biomes at Floatlands altitudes"); + result.addChangeListener(this); + + // Drop the spinner width a little + JComponent field = ((JSpinner.DefaultEditor) result.getEditor()); + Dimension prefSize = new Dimension( + (int)(label.getPreferredSize().width * 0.8f), // the 0.8 is because the scroll buttons will widen it. + field.getPreferredSize().height + ); + field.setPreferredSize(prefSize); + return result; + } + + @Override public void onBiomeProfileUpdate(BiomeProfile newBiomeProfile) { UpdateSelectedBiomeProfile(newBiomeProfile); } + @Override public void stateChanged(ChangeEvent e) { if (e.getSource() == altitudeOffset) { @@ -110,6 +220,48 @@ public class VoronoiWindow implements BiomeProfileUpdateListener, ChangeListener updateVoronoiDiagram(); } + private void InfoButtonClicked() { + StringBuilder data = new StringBuilder(); + data.append("Biome profile: "); + data.append(selectedProfile.getName()); + data.append("\r\n\r\n"); + data.append("At altitude " + getAltitudeFromDialog() + ", the world is composed of the following biomes:\r\n\r\n"); + data.append(voronoiPanel.getDistributionData()); + data.append("\r\n"); + data.append("These values are not the area the biomes cover in the Voronoi diagram,\r\n" + + "they are the area of the Minetest world that will be covered by the biome\r\n" + + "at the given altitude.\r\n\r\n" + + "The surface alitudes most commonly occuring in the world will be determined\r\n" + + "by the choice of mapgen, so these value (above) should only be considered\r\n" + + "\"world-wide\" absolute values when biomes don't change with altitude, or the\r\n" + + "mapgen is flat.\r\n"); + + data.append("\r\n\r\nDistribution legend:\r\n\r\n"); + + data.append("Dragging the \"Show distribution\" slider causes colored rings to appear on the\r\n" + + "diagram. These indicate the frequently at which different temperature and\r\n" + + "humidity combinations occur in the world:\r\n" + + "\r\n" + + " * Square grey outline — the technical limit of temperature and humidity.\r\n" + + " The world does not contain temperature or humidity values in this\r\n" + + " range.\r\n" + + " * Red dotted ring — the practical limit of temperature and humidity. The\r\n" + + " temperature and humidity in the world falls within this ring 99.99% of\r\n" + + " the time.\r\n" + + " * Red solid rings - these indicate the four quartiles. 25% of the world\r\n" + + " has a temperature and humidity falling outside the outermost quartile\r\n" + + " ring. The next ring has 50% of the world inside it, and 50% outside.\r\n" + + " The innermost red ring contains 25% of the world inside it, and 25%\r\n" + + " between it and the middle ring, etc.\r\n" + + " * Blue solid rings - these are spaced at 10 percentile intervals. Note\r\n" + + " that the 50 percentile ring is drawn red, but can also be considered\r\n" + + " one of the blue rings.\r\n" + + " * Red dot - the center of the distribution. This is the most common\r\n" + + " temperature and humidity value\r\n."); + + TextWindow.showMonospace(windowFrame, "Voronoi diagram data", data.toString()); + } + private void UpdateSelectedBiomeProfile(BiomeProfile newProfile) { MinetestBiomeProfileImpl minetestProfile = (newProfile instanceof MinetestBiomeProfileImpl) ? (MinetestBiomeProfileImpl)newProfile : null; @@ -118,14 +270,15 @@ public class VoronoiWindow implements BiomeProfileUpdateListener, ChangeListener selectedProfile = minetestProfile; if (changed) updateVoronoiDiagram(); - this.windowFrame.setTitle(selectedProfile == null ? "Biome profile Voronoi graph" : "Voronoi graph for " + selectedProfile.getName()); + this.windowFrame.setTitle(selectedProfile == null ? "Biome profile Voronoi diagram" : "Voronoi diagram for " + selectedProfile.getName()); } private void updateVoronoiDiagram() { EventQueue.invokeLater( () -> { int altitude = getAltitudeFromDialog(); - voronoiPanel.Update(selectedProfile, altitude, getOptionFlagsFromDialog()); + float freqGraphOpacity = freqGraphSlider.getValue() / 100f; + voronoiPanel.Update(selectedProfile, altitude, freqGraphOpacity, getOptionFlagsFromDialog()); graphHeading.setText("Biomes at altitude " + altitude); } ); @@ -146,37 +299,31 @@ public class VoronoiWindow implements BiomeProfileUpdateListener, ChangeListener @CalledOnlyBy(AmidstThread.EDT) private void setOptionFlagsInDialog(int optionFlags) { - option_showAxis.setSelected((optionFlags & VoronoiPanel.FLAG_SHOWAXIS) > 0); - option_showNodes.setSelected((optionFlags & VoronoiPanel.FLAG_SHOWNODES) > 0); - option_showLabels.setSelected((optionFlags & VoronoiPanel.FLAG_SHOWLABELS) > 0); + option_showAxis.setSelected((optionFlags & VoronoiPanel.FLAG_SHOWAXIS) > 0); + option_showNodes.setSelected((optionFlags & VoronoiPanel.FLAG_SHOWNODES) > 0); + option_showLabels.setSelected((optionFlags & VoronoiPanel.FLAG_SHOWLABELS) > 0); + option_showCoverage.setSelected((optionFlags & VoronoiPanel.FLAG_SHOWCOVERAGE) > 0); } @CalledOnlyBy(AmidstThread.EDT) private int getOptionFlagsFromDialog() { int result = 0; - if (option_showAxis.isSelected()) result |= VoronoiPanel.FLAG_SHOWAXIS; - if (option_showNodes.isSelected()) result |= VoronoiPanel.FLAG_SHOWNODES; - if (option_showLabels.isSelected()) result |= VoronoiPanel.FLAG_SHOWLABELS; + if (option_showAxis.isSelected()) result |= VoronoiPanel.FLAG_SHOWAXIS; + if (option_showNodes.isSelected()) result |= VoronoiPanel.FLAG_SHOWNODES; + if (option_showLabels.isSelected()) result |= VoronoiPanel.FLAG_SHOWLABELS; + if (option_showCoverage.isSelected()) result |= VoronoiPanel.FLAG_SHOWCOVERAGE; return result; } - public static void showDiagram(BiomeProfileSelection biomeProfileSelection) { - - if (voronoiWindow == null) { - voronoiWindow = new VoronoiWindow(); - } - SwingUtilities.invokeLater(() -> voronoiWindow.show(biomeProfileSelection)); - } - @CalledOnlyBy(AmidstThread.EDT) - public void show(BiomeProfileSelection biomeProfileSelection) { + private void show(BiomeProfileSelection biomeProfileSelection) { if (this.biomeProfileSelection != null) { this.biomeProfileSelection.removeUpdateListener(this); } this.biomeProfileSelection = biomeProfileSelection; - + BiomeProfile newProfile = null; if (biomeProfileSelection != null) { this.biomeProfileSelection.addUpdateListener(this); @@ -186,4 +333,31 @@ public class VoronoiWindow implements BiomeProfileUpdateListener, ChangeListener UpdateSelectedBiomeProfile(newProfile); this.windowFrame.setVisible(true); } + + /** Creates and displays the Voronoi diagram window */ + public static void showDiagram(Component parent, BiomeProfileSelection biome_profile_selection, IHistogram2D climate_histogram) { + + if (voronoiWindow == null) { + voronoiWindow = new VoronoiWindow(parent); + } + SwingUtilities.invokeLater(() -> { + voronoiWindow.voronoiPanel.setClimateHistogram(climate_histogram); + voronoiWindow.show(biome_profile_selection); + }); + } + + /** + * Private constructor as this is a singleton. Invoke showDiagram() instead. + * @see showDiagram + */ + @CalledOnlyBy(AmidstThread.EDT) + private VoronoiWindow(Component parent) { + this.windowFrame = createWindowFrame(parent, 770, 760); + setOptionFlagsInDialog( + VoronoiPanel.FLAG_SHOWLABELS | + VoronoiPanel.FLAG_SHOWAXIS | + VoronoiPanel.FLAG_SHOWNODES | + VoronoiPanel.FLAG_SHOWCOVERAGE + ); + } } diff --git a/src/main/java/amidst/minetest/world/mapgen/ClimateHistogram.java b/src/main/java/amidst/minetest/world/mapgen/ClimateHistogram.java new file mode 100644 index 00000000..89eccad9 --- /dev/null +++ b/src/main/java/amidst/minetest/world/mapgen/ClimateHistogram.java @@ -0,0 +1,326 @@ +package amidst.minetest.world.mapgen; + +import javax.vecmath.Point2d; + +import java.util.Arrays; +import java.util.LongSummaryStatistics; + +import amidst.logging.AmidstLogger; + +/** + * This class uses pre-generated data, so can only provide histogram + * information for climate noise settings similar to Minetest's default settings. + */ +public class ClimateHistogram implements IHistogram2D { + + private static final float DEFAULT_SCALE = 50; + private static final float DEFAULT_OFFSET = 50; + private static final float DEFAULT_BLEND_SCALE = 50; + private static final float DEFAULT_BLEND_OFFSET = 50; + + float scaleAdj = 1; + float offsetAdj = 0; + + double dataSampleCount = 4293525600d * 6; // data is sum of 6 full-world samples + int dataSampleOffset = 40; // the first value in sampledHistogram_Heat is for heat of -40 + // Bins: 191, range: -40 to 150 (inclusive) + int[] sampledHistogram_Heat = new int[] {0, 0, 23, 896, 5273, 18130, 35423, 56932, 85873, 144492, 227887, 330382, 441791, 575273, 785758, 1104880, 1514226, 2008173, 2580628, 3270470, 4070347, 5010230, 6233724, 7665743, 9231443, 11032009, 13054672, 15159146, 17585779, 20431953, 23539132, 26777647, 30049708, 33634225, 37557763, 41702212, 46135583, 50960488, 56066254, 61483250, 67471538, 74036663, 81075916, 88392031, 95671758, 103028198, 110423079, 118133581, 126230683, 134525547, 143089365, 151816479, 160572896, 169173433, 177480719, 185603411, 193673745, 201819880, 210125434, 218400248, 226576667, 234675120, 242660227, 250393165, 257641763, 264310278, 271052198, 277926795, 284991256, 291581211, 297501193, 303087896, 308619131, 313777446, 318058063, 322212943, 326758548, 331475102, 335722459, 338714730, 340704226, 342259956, 344228530, 346336570, 348439520, 350388503, 352166050, 353596154, 355021883, 355344601, 354627956, 353920683, 353523974, 353330547, 353024545, 352813804, 352102001, 350967293, 349073000, 346178826, 343181131, 340158957, 336577363, 332492808, 328136486, 323579081, 319083180, 314922134, 310380729, 305265626, 299550379, 293823815, 287790026, 282008407, 276140320, 269559012, 262131740, 254605986, 246951793, 239251561, 230994017, 222093705, 213071865, 204219912, 195608147, 187251811, 178964522, 170779498, 162561496, 154205050, 145761473, 137219761, 128986832, 121138771, 113558434, 106120928, 98743038, 91314126, 83938278, 77103922, 70855499, 64921287, 59244041, 53844544, 48822704, 43909928, 39348668, 35160583, 31266663, 27623387, 24258537, 21091241, 18212811, 15617700, 13237251, 11059423, 9228541, 7789154, 6607381, 5466830, 4340725, 3364753, 2610074, 2004609, 1486276, 1069920, 750199, 518992, 357847, 267761, 204042, 135284, 70635, 31506, 16563, 14437, 12363, 6219, 1714, 153, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + int[] sampledHistogram_Humidity = new int[] {0, 0, 31, 498, 2506, 4845, 8185, 21468, 59847, 122732, 193470, 286771, 438554, 610779, 784774, 1018080, 1351444, 1762206, 2271350, 2871968, 3617679, 4542319, 5722714, 7087672, 8612730, 10322878, 12252077, 14556399, 17083672, 19781372, 22738287, 25903300, 29414149, 33280369, 37456084, 41913668, 46847975, 52063820, 57365491, 62720169, 68287651, 74188447, 80622299, 87458209, 94297778, 101540429, 109407209, 117764134, 126033776, 134106841, 142075245, 150160062, 158481411, 167046905, 175806629, 184285718, 192691920, 201123443, 209651797, 218157100, 226149374, 234230435, 242366234, 249849182, 257017437, 264178652, 271211835, 277958877, 284189278, 290172767, 296085575, 301995199, 307977042, 313261332, 317868477, 322201082, 326388271, 330472979, 334532622, 338385357, 341846286, 344688035, 346974698, 349233925, 351053588, 352480803, 353676423, 354853005, 355571248, 355946018, 356187678, 355726165, 354284344, 352881704, 351909613, 350696269, 348847862, 346809607, 345187333, 343247921, 340859439, 338049103, 335025305, 331513640, 327718695, 323952706, 320112005, 316183516, 311617470, 306291881, 300848532, 295309226, 289332030, 282903833, 276250749, 269695133, 262942145, 255893757, 248411772, 240532829, 232446716, 224124693, 215607785, 206986511, 198415028, 190161781, 182277837, 174044450, 165348412, 156451475, 147683121, 139285547, 131231683, 123268099, 115122762, 107128906, 99350517, 91804617, 84384451, 77313619, 70743606, 64517248, 58541831, 52903662, 47555609, 42575213, 38003184, 33839429, 30012843, 26495082, 23211478, 20191059, 17507769, 14994437, 12727017, 10648031, 8847912, 7389304, 6184783, 5132563, 4108008, 3175730, 2394676, 1814862, 1395580, 1096984, 856680, 630982, 413928, 233980, 123637, 63560, 37317, 19663, 8361, 1825, 124, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + + // See the code in the constructor if you wish to recalculate frequencyAtPercentileTable + /** A pre-calculation table for percentiles 1 to 100, in 1 percentile increments */ + double[] frequencyAtPercentileTable = new double[] {4.288849623506293E-6, 7.849271137128854E-6, 1.0935481111330147E-5, 1.4125178764733622E-5, 1.710500033323479E-5, 1.9993831214913553E-5, 2.2662552870656077E-5, 2.534060066104869E-5, 2.802187248207058E-5, 3.080643823350056E-5, 3.33861856758062E-5, 3.6068277994214394E-5, 3.862936637606668E-5, 4.095044456557262E-5, 4.3636595467822336E-5, 4.607358640977918E-5, 4.810443669853268E-5, 5.0837165362693565E-5, 5.296404267788116E-5, 5.5325852990715865E-5, 5.7679007766327825E-5, 5.983630193800486E-5, 6.235772026254966E-5, 6.430623785962591E-5, 6.669710263491633E-5, 6.855912078302344E-5, 7.113548845931662E-5, 7.295088808887063E-5, 7.542181961574365E-5, 7.713810513240832E-5, 7.941207236464512E-5, 8.162108120760272E-5, 8.386222016118836E-5, 8.569039655815985E-5, 8.746946795713887E-5, 8.971102202243205E-5, 9.113804797595727E-5, 9.331997156194227E-5, 9.538586351908653E-5, 9.734525496179133E-5, 9.898022955022616E-5, 1.0102734313517474E-4, 1.0297431176541194E-4, 1.0459706284256709E-4, 1.0668836696809265E-4, 1.0855615168551473E-4, 1.1007621103277895E-4, 1.1237809589729694E-4, 1.1365508528516895E-4, 1.1581159959767767E-4, 1.1773346682435759E-4, 1.1928685427735908E-4, 1.2109096765312022E-4, 1.2266224460033028E-4, 1.2460344924158565E-4, 1.2639392703246802E-4, 1.2792145643612953E-4, 1.2983742624993134E-4, 1.3126086274433714E-4, 1.3318981239435993E-4, 1.3466113206470656E-4, 1.3628693214058266E-4, 1.3771262506116956E-4, 1.3927642904768147E-4, 1.4088517346529805E-4, 1.424651674401106E-4, 1.4415914818743247E-4, 1.4584085961605385E-4, 1.4707407523891278E-4, 1.48740812877974E-4, 1.4998644757789717E-4, 1.5182775373866035E-4, 1.5312561398223614E-4, 1.5476944096761824E-4, 1.564096559253218E-4, 1.5747584559976162E-4, 1.592317541086737E-4, 1.6055180720478152E-4, 1.6209298336552515E-4, 1.6335932626023207E-4, 1.6501788334796102E-4, 1.6627703968019814E-4, 1.6756546371099305E-4, 1.6888984543785755E-4, 1.7039589568960874E-4, 1.717707149296396E-4, 1.7299179342349547E-4, 1.744319494242184E-4, 1.7608213347237582E-4, 1.7744060109174724E-4, 1.7877565932783E-4, 1.8007180142527902E-4, 1.8136873249310507E-4, 1.8273003575625964E-4, 1.8385915609410723E-4, 1.8507326100357174E-4, 1.8627491892689703E-4, 1.8740872113945042E-4, 1.8879158216656317E-4, 1.9033705137062054E-4}; + /** A pre-calculation table for percentiles 0 to 0.99 in 0.01 percentile increments */ + double[] frequencyAtPerdimileTable = new double[] {0.0, 7.906411381395254E-8, 1.4281584607842585E-7, 2.0367198773824541E-7, 2.579481623074614E-7, 3.103739993944631E-7, 3.663211869054443E-7, 4.169483003809781E-7, 4.635227228946929E-7, 5.288508873488246E-7, 5.686991889665327E-7, 6.204826202407989E-7, 6.812762234709958E-7, 7.263347560698961E-7, 7.657426809358803E-7, 8.193734165334142E-7, 8.656717598377384E-7, 9.267166497130238E-7, 9.687699186503285E-7, 1.0100613846028922E-6, 1.040421908192894E-6, 1.0981246626722591E-6, 1.1626975068704957E-6, 1.2072828789096895E-6, 1.245628837090483E-6, 1.2925330631771625E-6, 1.3136720582621303E-6, 1.3539419032545604E-6, 1.4261585444809766E-6, 1.4613406586174258E-6, 1.5325176657436087E-6, 1.5737994074503633E-6, 1.6102602927831794E-6, 1.6548696775342068E-6, 1.6814410653583067E-6, 1.7109022657620896E-6, 1.739729245372117E-6, 1.818909640506737E-6, 1.860652162367776E-6, 1.918256413753668E-6, 1.950305933579097E-6, 2.0142046144063015E-6, 2.0414075345970236E-6, 2.078387302258539E-6, 2.1271279829810187E-6, 2.149899817465148E-6, 2.1755325291439493E-6, 2.204795837375724E-6, 2.2850671080176522E-6, 2.32281076679128E-6, 2.373852151766557E-6, 2.420014414654376E-6, 2.4519443333869577E-6, 2.4886672811746097E-6, 2.5482357788527957E-6, 2.5826040900445513E-6, 2.62377928182969E-6, 2.6443912878365593E-6, 2.6742844225227714E-6, 2.6967536393142994E-6, 2.729072454192572E-6, 2.7697985570250263E-6, 2.8357136937141744E-6, 2.886882959408015E-6, 2.9324497121704535E-6, 2.973268481768577E-6, 3.010129601199772E-6, 3.0440263924820617E-6, 3.097221433350364E-6, 3.153942913939244E-6, 3.1693172695523055E-6, 3.208017787514919E-6, 3.2466586058243742E-6, 3.2873911331250026E-6, 3.300640985971264E-6, 3.3135005090680394E-6, 3.360507790578272E-6, 3.4192720091466502E-6, 3.4414606133149838E-6, 3.480699054050278E-6, 3.5502316470557827E-6, 3.608426792734956E-6, 3.6396339028551223E-6, 3.6863827940008945E-6, 3.7040534454740007E-6, 3.7410242395545514E-6, 3.7825944022123806E-6, 3.847435398836006E-6, 3.873875563025558E-6, 3.9031983332453956E-6, 3.931204415840417E-6, 3.9598830516054155E-6, 3.975995856789808E-6, 3.998365240336164E-6, 4.039172655337482E-6, 4.0632862055249795E-6, 4.0990893986543585E-6, 4.183983009230883E-6, 4.216663091722728E-6, 4.245830038292284E-6}; + + // See the code in the constructor if you wish to recalculate processedHistogram_Heat + long[] processedHistogram_Heat = new long[] {0, 153, 1768, 7613, 20266, 39237, 68532, 129569, 253672, 466068, 749036, 1118894, 1652120, 2336026, 3177411, 4289864, 5747526, 7589850, 9856728, 12682921, 16136759, 20151942, 24748602, 29931873, 35920626, 43062341, 51271017, 60327682, 70390031, 81495625, 93747434, 106799416, 120743363, 135914606, 152365699, 170101021, 189361871, 209772514, 231217617, 253641954, 277358294, 302642651, 330020944, 358968983, 388063091, 417818461, 448511484, 480304585, 512482974, 545137696, 578609204, 612633066, 646964215, 681044286, 714529707, 747302721, 780388840, 814149746, 848456881, 882775746, 916166774, 948689945, 980390026, 1010742090, 1039733085, 1067743075, 1094655102, 1120797912, 1146302590, 1170887019, 1193985679, 1216640602, 1238594372, 1258144428, 1275121725, 1291945812, 1309002000, 1325954529, 1341857749, 1355308147, 1366591082, 1376374738, 1385463561, 1393347395, 1400442971, 1406379379, 1410776631, 1414661410, 1418401449, 1420937467, 1421631268, 1420937467, 1418401449, 1414661410, 1410776631, 1406379379, 1400442971, 1393347395, 1385463561, 1376374738, 1366591082, 1355308147, 1341857749, 1325954529, 1309002000, 1291945812, 1275121725, 1258144428, 1238594372, 1216640602, 1193985679, 1170887019, 1146302590, 1120797912, 1094655102, 1067743075, 1039733085, 1010742090, 980390026, 948689945, 916166774, 882775746, 848456881, 814149746, 780388840, 747302721, 714529707, 681044286, 646964215, 612633066, 578609204, 545137696, 512482974, 480304585, 448511484, 417818461, 388063091, 358968983, 330020944, 302642651, 277358294, 253641954, 231217617, 209772514, 189361871, 170101021, 152365699, 135914606, 120743363, 106799416, 93747434, 81495625, 70390031, 60327682, 51271017, 43062341, 35920626, 29931873, 24748602, 20151942, 16136759, 12682921, 9856728, 7589850, 5747526, 4289864, 3177411, 2336026, 1652120, 1118894, 749036, 466068, 253672, 129569, 68532, 39237, 20266, 7613, 1768, 153, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + long[] processedHistogram_Humidity = processedHistogram_Heat; + double dataProcessedCount = dataSampleCount * 4; // we doubled the sample data by mirroring it horizontally, and doubled that by mirroring vertically + + Point2d sampleMean = null; + + + public double FrequencyOfTemperature(float temperature) { + int index = Math.round((temperature / scaleAdj) + offsetAdj) + dataSampleOffset; + + if (index >= 0 && index < processedHistogram_Heat.length) { + //return sampledHistogram_Heat[index] / dataSampleCount; + return processedHistogram_Heat[index] / dataProcessedCount; + } else { + // Our sampling range covers all non-zero values, so the answer to return is zero, + // however NaN should help highlight if range bugs are occurring elsewhere in the app. + return Double.NaN; + } + } + + public double FrequencyOfHumidity(float humidity) { + int index = Math.round((humidity / scaleAdj) + offsetAdj) + dataSampleOffset; + + if (index >= 0 && index < processedHistogram_Heat.length) { + //return sampledHistogram_Humidity[index] / dataSampleCount; + return processedHistogram_Heat[index] / dataProcessedCount; + } else { + // Our sampling range covers all non-zero values, so the answer to return is zero, + // however NaN should help highlight if range bugs are occurring elsewhere in the app. + return Double.NaN; + } + } + + /** + * Returns a value between 0 and 1 which represents how frequently a + * block should have a climate of (temperature, humidity). + * (uses linear interpolation) + */ + @Override + public double frequencyOfOccurance(float temperature, float humidity) { + + // Linearly-interpolate instead of rounding temperature and humidity + float temperature_floor = (float)Math.floor(temperature); + float humidity_floor = (float)Math.floor(humidity); + + double freq_floor = FrequencyOfTemperature(temperature_floor); + double freq_ceil = FrequencyOfTemperature((float)Math.ceil(temperature)); + double temperature_interp = freq_floor + (freq_ceil - freq_floor) * (temperature - temperature_floor); + + freq_floor = FrequencyOfHumidity(humidity_floor); + freq_ceil = FrequencyOfHumidity((float)Math.ceil(humidity)); + double humidity_interp = freq_floor + (freq_ceil - freq_floor) * (humidity - humidity_floor); + + return temperature_interp * humidity_interp; + } + + /** + * Returns the "FrequencyOfOccurance" value at which 'percentile' amount of + * samples will fall beneath. + * So if percentile was 10, then a value between 0 and 1 would be returned such + * that 10% of results from FrequencyOfOccurance() would fall below it. + */ + @Override + public double frequencyAtPercentile(double percentile) { + // use the lookup-table + if (percentile >= 0.995) { + return frequencyAtPercentileTable[(int)Math.round(Math.max(1, Math.min(100, percentile))) - 1]; + } else { + return frequencyAtPerdimileTable[(int)Math.round(Math.max(0, Math.min(100, percentile * 100)))]; + } + } + + @Override + public Point2d getSampleMean() { + // We know the true mean of the noise, so use that. + if (sampleMean == null) sampleMean = new Point2d(DEFAULT_OFFSET + offsetAdj, DEFAULT_OFFSET + offsetAdj); + return sampleMean; + } + + /** + * The data from the 3 octaves of Perlin noise plus 2 octaves blending is + * *not* of a Normal Distribution, and any math that assumes it is will fail. + * This function calculates what the the mean the standard deviation + * would be if it was a normal distribution. + * (Not used, but left in for reference) + * + * Spoiler: it's ~26 (perhaps exactly 26) + */ + private double getStandardDeviation(int[] histogram) { + + // Work out the Mean (we actually know it should be 50) + double total = 0; + long sampleCount = 0; + for (int i = histogram.length - 1; i >= 0; i--) { + long sample = i - dataSampleOffset; + total += (sample * histogram[i]); + sampleCount += histogram[i]; + } + double mean = total / (double)sampleCount; + + // Then for each number: subtract the Mean and square the result + double squaredDifferenceTotal = 0; + for (int i = 0; i < histogram.length; i++) { + long sample = i - dataSampleOffset; + double squaredDifference = (sample - mean) * (sample - mean); + squaredDifferenceTotal += squaredDifference * histogram[i]; + } + + // Then work out the mean of those squared differences. + double meanOfSquaredDifference = squaredDifferenceTotal / ((double)sampleCount - 1); // -1 for Bessel's correction + + // Take the square root of that and we are done! + return Math.sqrt(meanOfSquaredDifference); + } + + + /** + * Returns the percentage of sample locations/bins that occur at or below the given frequency_of_occurance + * @see PercentileAtFrequency_Processed + */ + private double PercentileAtFrequency_Sampled(double frequency_of_occurance) { + + double probabilityUnderFrequency = 0; + for (int y = sampledHistogram_Humidity.length - 1; y >= 0; y--) { + for (int x = sampledHistogram_Heat.length - 1; x >= 0; x--) { + double probability = (sampledHistogram_Heat[x] / dataSampleCount) * (sampledHistogram_Humidity[y] / dataSampleCount); + if (probability <= frequency_of_occurance) { + probabilityUnderFrequency += probability; + } + } + } + return probabilityUnderFrequency * 100; + } + + /** + * Returns the percentage of sample locations/bins that occur at or below the given frequency_of_occurance, + * however it uses the processed data, rather than the real samples. + * @see PercentileAtFrequency_Sampled + */ + private double PercentileAtFrequency_Processed(double frequency_of_occurance) { + + double probabilityUnderFrequency = 0; + for (int y = processedHistogram_Humidity.length - 1; y >= 0; y--) { + for (int x = processedHistogram_Heat.length - 1; x >= 0; x--) { + double probability = (processedHistogram_Heat[x] / dataProcessedCount) * (processedHistogram_Humidity[y] / dataProcessedCount); + if (probability <= frequency_of_occurance) { + probabilityUnderFrequency += probability; + } + } + } + return probabilityUnderFrequency * 100; + } + + /** + * Performs a logrithmic search for a frequency_of_occurance which will split the population of + * samples along the given percentile line. + * It uses the processed data, rather than the real samples. + * It's unlikely to find an exact match - such a value might not exist, but it should get close. + * @param percentile - should be between 0 and 100 + * @param upper_freq_bound - note this is an exclusive upper bound, the search alg will never try it, though it should get close enough to not matter. + * @param lower_freq_bound - note this is an exclusive lower bound, the search alg will never try it, though it should get close enough to not matter. + * @param search_depth - recurse depth, and how many calls to FrequencyAtPercentile() will be made. + */ + private double SearchForFrequencyAtPercentile(double percentile, double upper_freq_bound, double lower_freq_bound, int search_depth) { + + double midPointFrequency = lower_freq_bound + ((upper_freq_bound - lower_freq_bound) / 2.0d); + double midpointPercentile = PercentileAtFrequency_Processed(midPointFrequency); + if (search_depth == 0) { + AmidstLogger.warn("Could not find exact FrequencyAtPercentile for " + percentile + ", returning " + midPointFrequency); + return midPointFrequency; + } + if (midpointPercentile < percentile) { + return SearchForFrequencyAtPercentile(percentile, upper_freq_bound, midPointFrequency, search_depth - 1); + } else if (midpointPercentile > percentile) { + return SearchForFrequencyAtPercentile(percentile, midPointFrequency, lower_freq_bound, search_depth - 1); + } else { + return midPointFrequency; + } + } + + /** + * Processes the sample counts from sampledHistogram_Heat & sampledHistogram_Humidity and + * uses what we know about the climate noise algorithm (the center and symmetry) to create + * data likely to be closer to the true distrubtion. + * + * This matters if you want quartile lines to look smooth. + * + * The processed data is written to processedHistogram_Heat/processedHistogram_Humidity + */ + private void processSamples() { + + int center = Math.round(DEFAULT_OFFSET + offsetAdj); + int distFromCenter = 0; + int maxDistFromCenter = Math.max(center - dataSampleOffset, Math.max(sampledHistogram_Heat.length - center - 1, sampledHistogram_Humidity.length - center - 1)); + while (distFromCenter <= maxDistFromCenter) { + int indexLeft = (center + dataSampleOffset) - distFromCenter; + int indexRight = (center + dataSampleOffset) + distFromCenter; + + if (indexRight > 0 && indexRight < processedHistogram_Heat.length) { + processedHistogram_Heat[indexRight] = (indexRight < sampledHistogram_Heat.length) ? sampledHistogram_Heat[indexRight] : 0; + if (indexRight < sampledHistogram_Humidity.length) processedHistogram_Heat[indexRight] += sampledHistogram_Humidity[indexRight]; + + if (indexLeft > 0) { + processedHistogram_Heat[indexRight] += sampledHistogram_Heat[indexLeft]; + processedHistogram_Heat[indexRight] += sampledHistogram_Humidity[indexLeft]; + + // Mirror the processed value to the left side of processedHistogram_Heat + processedHistogram_Heat[indexLeft] = processedHistogram_Heat[indexRight]; + } + } + distFromCenter++; + } + // each value in processedHistogram_Heat is now the left+right of both heat+humidy added together + dataProcessedCount = dataSampleCount * 4; + + AmidstLogger.info("processedHistogram_Heat: " + Arrays.toString(processedHistogram_Heat)); + } + + /** + * Caches results from SearchForFrequencyAtPercentile() into frequencyAtPercentileTable and + * frequencyAtPerdimileTable. + * @see SearchForFrequencyAtPercentile + */ + private void calculatePercentileTables(int search_depth) { + LongSummaryStatistics heatStats = Arrays.stream(processedHistogram_Heat).summaryStatistics(); + LongSummaryStatistics humidityStats = Arrays.stream(processedHistogram_Humidity).summaryStatistics(); + + frequencyAtPerdimileTable[0] = (heatStats.getMin() / (double)dataProcessedCount) * (humidityStats.getMin() / (double)dataProcessedCount); + frequencyAtPercentileTable[99] = (heatStats.getMax() / (double)dataProcessedCount) * (humidityStats.getMax() / (double)dataProcessedCount); + double lowerBound_percentile = frequencyAtPerdimileTable[0]; + double lowerBound_perdimile = frequencyAtPerdimileTable[0]; + for(int i = 1; i < 100; i++) { + frequencyAtPercentileTable[i - 1] = SearchForFrequencyAtPercentile(i, 1.00, lowerBound_percentile, search_depth); + frequencyAtPerdimileTable[i] = SearchForFrequencyAtPercentile(i / 100d, frequencyAtPercentileTable[0], lowerBound_perdimile, search_depth); + lowerBound_percentile = frequencyAtPercentileTable[i]; + lowerBound_perdimile = frequencyAtPerdimileTable[i]; + } + + AmidstLogger.info("frequencyAtPercentileTable: " + Arrays.toString(frequencyAtPercentileTable)); + AmidstLogger.info("frequencyAtPerdimileTable: " + Arrays.toString(frequencyAtPerdimileTable)); + } + + /** + * Constructor for Minetest's default climate + */ + public ClimateHistogram() { + + if (dataProcessedCount == 0) { + // the samples are already processed, and the percentile tables already + // calculated, but I leave this code here in case someone wants to update + // sampledHistogram_Heat and sampledHistogram_Humidity with their own sampled + // data and recalculate. + processSamples(); + calculatePercentileTables(40); + } + } + + /** + * Constructor for climates which have the same frequency distribution as Minetest's default climate, + * but may have been scaled or translated. + */ + public ClimateHistogram(NoiseParams heat, NoiseParams heat_blend, NoiseParams humidity, NoiseParams humidity_blend) { + + // Ensure these noise settings are close enough to Minetest's default settings + // that we can use the pre-generated data. + // + // Defaults: + // np_heat = new NoiseParams(50, 50, new Vector3f(1000, 1000, 1000), 5349, (short)3, 0.5f, 2.0f); + // np_humidity = new NoiseParams(50, 50, new Vector3f(1000, 1000, 1000), 842, (short)3, 0.5f, 2.0f); + // np_heat_blend = new NoiseParams( 0, 1.5f, new Vector3f( 8, 8, 8), 13, (short)2, 1.0f, 2.0f); + // np_humidity_blend = new NoiseParams( 0, 1.5f, new Vector3f( 8, 8, 8), 90003, (short)2, 1.0f, 2.0f); + + // TODO: Check octaves, persist, and lacunarity are all the same, and that scale and offset + // is the same or can be adjusted for, and that spread is appropriate. + scaleAdj = heat.scale / DEFAULT_SCALE; + offsetAdj = DEFAULT_OFFSET - heat.offset; + float scaleAdj_blend = heat_blend.scale / DEFAULT_BLEND_SCALE; + float offsetAdj_blend = DEFAULT_BLEND_OFFSET - heat_blend.offset; + + if (heat.scale != humidity.scale || heat.offset != humidity.offset || + heat_blend.scale != humidity_blend.scale || heat_blend.offset != humidity_blend.offset || + scaleAdj != scaleAdj_blend || offsetAdj != offsetAdj_blend || + heat.octaves != 3 || humidity.octaves != 3 || heat_blend.octaves != 2 || humidity_blend.octaves != 2 || + heat.persist != 0.5f || humidity.persist != 0.5f || heat_blend.persist != 1.0f || humidity_blend.persist != 1.0f || + heat.lacunarity != 2.0f || humidity.lacunarity != 2.0f || heat_blend.lacunarity != 2.0f || humidity_blend.lacunarity != 2.0f) { + + AmidstLogger.error("Non-standant climate noise in use, current ClimateHistogram instance will give wrong data."); + } + } +} diff --git a/src/main/java/amidst/minetest/world/mapgen/IHistogram2D.java b/src/main/java/amidst/minetest/world/mapgen/IHistogram2D.java new file mode 100644 index 00000000..6d171b80 --- /dev/null +++ b/src/main/java/amidst/minetest/world/mapgen/IHistogram2D.java @@ -0,0 +1,29 @@ +package amidst.minetest.world.mapgen; + +import javax.vecmath.Point2d; + +public interface IHistogram2D { + /** + * Returns a value between 0 and 1 which represents how frequently the + * samples fall into the bucket at (x, y). + * It's up to the implementation to decide what size its buckets are, and + * whether x and y get rounded to the nearest bucket, or bucket values are + * interpolated. + */ + double frequencyOfOccurance(float x, float y); + + /** + * Returns the "FrequencyOfOccurance" value at which 'percentile' amount of + * samples will fall beneath. + * So if percentile was 10, then a value between 0 and 1 would be returned such + * that 10% of results from FrequencyOfOccurance() would fall below it. + */ + double frequencyAtPercentile(double percentile); + + /** + * get's the mean value of the distribution. + * If the histogram were a normal distribution, this would return the location of + * the peak. If the histogram had two peaks, it would return a location between them. + */ + Point2d getSampleMean(); +} diff --git a/src/main/java/amidst/minetest/world/mapgen/NoiseParams.java b/src/main/java/amidst/minetest/world/mapgen/NoiseParams.java index 5dd09598..d98a1380 100644 --- a/src/main/java/amidst/minetest/world/mapgen/NoiseParams.java +++ b/src/main/java/amidst/minetest/world/mapgen/NoiseParams.java @@ -4,13 +4,79 @@ import javax.vecmath.Vector3f; public class NoiseParams { + /** + * 'offset' is the centre value of the noise value, often 0. + * (https://forum.minetest.net/viewtopic.php?p=227726#p227726) + */ public float offset = 0.0f; + + /** + * 'scale' is the approximate variation of the noise value either side of the centre value, often 1. + * Depending on the number of octaves and the persistence the variation is usually 1 to 2 times + * the scale value. The exact variation can be calculated from octaves and persistence. + * (To be exact, scale is the variation of octave 1, additional octaves add extra variation, see below). + * (https://forum.minetest.net/viewtopic.php?p=227726#p227726) + */ float scale = 1.0f; + + /** + * 'spread' is the size in nodes of the largest scale variation. If the noise is terrain height + * this would be the approximate size of the largest islands or seas, there will be no structure + * on a larger scale than this. + * There are 3 values for x, y, z so you can stretch or squash the shape of your structures, + * normally you would set all 3 values to be the same, even for 2D noise where the y value + * is not used). + * (https://forum.minetest.net/viewtopic.php?p=227726#p227726) + */ public Vector3f spread = new Vector3f(250, 250, 250); + + /** + * 'seed' is the magic seed number that determines the entire noise pattern. + * Just type in any random number, different for each use of noise. + * This is actually a 'seed difference', the noise actually uses 'seed' + 'world seed', to make any noise pattern world-dependant and repeatable. + * 'seed' then makes sure each use of noise in a world is using a different pattern. + * (https://forum.minetest.net/viewtopic.php?p=227726#p227726) + */ int seed = 12345; + + /** + * 'octaves' is the number of levels of detail in the noise variation. + * The largest scale variation is 'spread', that is octave 1. Each additional octave adds + * variation on a scale one half the size, so here you will have variation on scales + * of 2048, 1024, 512 nodes. + * (https://forum.minetest.net/viewtopic.php?p=227726#p227726) + */ short octaves = 3; + + /** + * 'persist' is persistence. This is how large the variation of each additional octave is relative to + * the previous octave. + * Octave 1 always outputs a variation from -1 to 1, the 'amplitude' is 1. + * With 3 octaves persist 0.5, a much used standard noise: + * Octave 2 outputs a variation from -0.5 to 0.5 (amplitude 0.5). + * Octave 3 will output -0.25 to 0.25 (amplitude 0.5 x 0.5). + * The 3 octaves are added to result in a noise variation of -1.75 to 1.75. + * 'persist' is therefore the roughness of the noise pattern, 0.33 is fairly smooth, 0.67 is rough + * and spiky, 0.5 is a medium value. + * (https://forum.minetest.net/viewtopic.php?p=227726#p227726) + */ public float persist = 0.6f; + + /** + * lacunarity is the ratio of the scales of variation of adjacent octaves. + * Standard lacunarity is 2.0, where each additional octave creates variation on a scale 1/2 that + * of the previous octave (which is why they're called octaves, it's analogous to musical octaves + * which is a doubling of frequency). + * + * Lacunarity 3.0 means that if the 'spread' (the scale of variation of octave 1) is 900 nodes, + * then the 2nd octave creates variation on a scale of 300 nodes, the 3rd octave 100 nodes etc. + * It's a way to get a wider range of detail with the same number of octaves. it also has a + * different character. + * (https://forum.minetest.net/viewtopic.php?p=228165#p228165) + */ float lacunarity = 2.0f; + + int flags = Noise.FLAG_DEFAULTS; // Static methods like compareUnsigned, divideUnsigned etc have been added to the Integer class to support the arithmetic operations for unsigned integers public String toString(String name) { @@ -40,6 +106,17 @@ public class NoiseParams { this(offset_, scale_, spread_, seed_, octaves_, persist_, lacunarity_, Noise.FLAG_DEFAULTS); } + /** + * + * @param offset_ + * @param scale_ + * @param spread_ + * @param seed_ + * @param octaves_ + * @param persist_ see field comment at top of this file for explanation. + * @param lacunarity_ see field comment at top of this file for explanation. + * @param flags_ + */ public NoiseParams(float offset_, float scale_, Vector3f spread_, int seed_, short octaves_, float persist_, float lacunarity_, int flags_) diff --git a/src/main/java/amidst/minetest/world/oracle/MinetestBiomeDataOracle.java b/src/main/java/amidst/minetest/world/oracle/MinetestBiomeDataOracle.java index 6ddd821f..8ca2b9c1 100644 --- a/src/main/java/amidst/minetest/world/oracle/MinetestBiomeDataOracle.java +++ b/src/main/java/amidst/minetest/world/oracle/MinetestBiomeDataOracle.java @@ -6,6 +6,8 @@ import amidst.fragment.IBiomeDataOracle; import amidst.gameengineabstraction.CoordinateSystem; import amidst.gameengineabstraction.world.biome.IBiome; import amidst.logging.AmidstLogger; +import amidst.minetest.world.mapgen.ClimateHistogram; +import amidst.minetest.world.mapgen.IHistogram2D; import amidst.minetest.world.mapgen.MapgenParams; import amidst.minetest.world.mapgen.MinetestBiome; import amidst.minetest.world.mapgen.MinetestBiomeProfileImpl; @@ -17,6 +19,7 @@ import amidst.settings.biomeprofile.BiomeProfileUpdateListener; public abstract class MinetestBiomeDataOracle implements IBiomeDataOracle, BiomeProfileUpdateListener { protected final int seed; protected MapgenParams params; + protected ClimateHistogram climateHistogram; /** * Updated by onBiomeProfileUpdate event, can be null. */ @@ -132,6 +135,17 @@ public abstract class MinetestBiomeDataOracle implements IBiomeDataOracle, Biome return (biome_closest != null) ? biome_closest : MinetestBiome.NONE; } + public IHistogram2D getClimateHistogram() { + if (climateHistogram == null) { + climateHistogram = new ClimateHistogram( + params.np_heat, + params.np_heat_blend, + params.np_humidity, + params.np_humidity_blend + ); + } + return climateHistogram; + } @Override public void onBiomeProfileUpdate(BiomeProfile newBiomeProfile) {