Refactor Voronoi graph

master
Treer 2018-05-07 00:18:01 +10:00
parent c03d39eed6
commit 3b86bf8753
4 changed files with 155 additions and 123 deletions

View File

@ -0,0 +1,32 @@
package amidst.gui.voronoi;
/** Node information for a Voronoi diagram */
public class GraphNode {
public int argb;
float x;
float y;
@Override
public boolean equals(Object obj) {
if (obj == null) return false;
if (!GraphNode.class.isAssignableFrom(obj.getClass())) return false;
final GraphNode other = (GraphNode)obj;
return x == other.x && y == other.y && argb == other.argb;
}
@Override
public int hashCode() {
int hash = 8;
hash = 37 * hash + (int)argb;
hash = 37 * hash + (int)Float.floatToIntBits(x);
hash = 37 * hash + (int)Float.floatToIntBits(y);
return hash;
}
public GraphNode(float x, float y, int argb) {
this.argb = argb;
this.x = x;
this.y = y;
}
}

View File

@ -1,5 +1,85 @@
package amidst.gui.voronoi;
public class VoronoiGraph {
import java.awt.image.BufferedImage;
import java.util.List;
/**
* 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;
/** 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) {
graph = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
bufferArray = new int[(200000 / width) * width];
}
}
public BufferedImage render(List<GraphNode> 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;
float yScale = (y_max - y_min) / (float)height;
int bufferArrayY = 0;
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
GraphNode[] nodeArray = nodes.toArray(new GraphNode[nodes.size()]);
int nodeCount = nodeArray.length;
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);
bufferArrayY = y;
bufferArrayIndex = 0;
}
for (int x = 0; x < width; x++) {
//float heat = (x * xScale) + axis_min;
float valueAtX = (x * xScale) + x_min;
closestDist = Float.MAX_VALUE;
closestNode = null;
for (short i = 0; i < nodeCount; i++) {
GraphNode node = nodeArray[i];
float xDist = valueAtX - node.x;
float yDist = valueAtY - node.y;
float dist = (xDist * xDist) + (yDist * yDist);
if (dist < closestDist) {
closestDist = dist;
closestNode = node;
}
}
bufferArray[bufferArrayIndex++] = (closestNode == null) ? 0x00000000 : closestNode.argb;
}
}
if (bufferArrayIndex > 0) {
// flush the rest of bufferArray into the BufferedImage.
graph.setRGB(0, bufferArrayY, width, bufferArrayIndex / width, bufferArray, 0, width);
}
return graph;
}
public VoronoiGraph(int width, int height) {
this.width = width;
this.height = height;
}
}

View File

@ -12,9 +12,7 @@ import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.swing.JPanel;
@ -22,22 +20,21 @@ import javax.swing.JPanel;
import amidst.documentation.AmidstThread;
import amidst.documentation.CalledByAny;
import amidst.documentation.CalledOnlyBy;
import amidst.fragment.Fragment;
import amidst.gameengineabstraction.world.biome.IBiome;
import amidst.logging.AmidstLogger;
import amidst.minetest.world.mapgen.MinetestBiome;
import amidst.minetest.world.mapgen.MinetestBiomeProfileImpl;
import amidst.mojangapi.world.Dimension;
import amidst.mojangapi.world.coordinates.CoordinatesInWorld;
/**
* 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_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 boolean GRAPHICS_DEBUG = false;
public static final boolean GRAPHICS_DEBUG = false;
private static final float AXIS_WIDTH = 0.5f;
private static final int TICKMARK_WIDTH_SMALL = 3;
@ -47,7 +44,7 @@ public class VoronoiPanel extends JPanel {
private static final int NODE_LABEL_SPACE = 0;
private static final int NODE_LABEL_FONTSIZE = 3;
private static final long serialVersionUID = 1L;
private static final long serialVersionUID = 1L;
private static Stroke stroke_capButt = new BasicStroke(AXIS_WIDTH, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND);
private static Stroke stroke_capSquare = new BasicStroke(AXIS_WIDTH, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND);
@ -57,17 +54,18 @@ public class VoronoiPanel extends JPanel {
public int axis_max = 140;
public int graph_resolution = 1000;
private MinetestBiome[] nodes;
//private MinetestBiome[] nodes;
ArrayList<MinetestBiome> biomes = new ArrayList<MinetestBiome>();
private List<GraphNode> graphNodes = null;
private int renderFlags;
private BufferedImage graph;
private int[] rgbArray;
private VoronoiGraph graph = null;
@Override
@CalledOnlyBy(AmidstThread.EDT)
public void paintComponent(Graphics g) {
super.paintComponent(g);
createBufferedImage(graph_resolution);
if (graph == null) graph = new VoronoiGraph(graph_resolution, graph_resolution);
this.setBorder(null);
Rectangle panelBounds = getBounds();
@ -104,10 +102,13 @@ public class VoronoiPanel extends JPanel {
g2d.setColor(Color.WHITE);
g2d.fillRect(axis_min, axis_min, axis_max - axis_min, axis_max - axis_min);
//drawVoronoi(g2d);
drawVoronoiGraph(rgbArray, graph_resolution, graph_resolution);
graph.setRGB(0, 0, graph_resolution, graph_resolution, rgbArray, 0, graph_resolution);
g2d.drawImage(graph, axis_min, axis_min, desiredAxisLength, desiredAxisLength, Color.WHITE, null);
// Draw the filled voronoi graph
if (graphNodes != null) {
g2d.drawImage(
graph.render(graphNodes, axis_min, axis_max, axis_min, axis_max),
axis_min, axis_min, desiredAxisLength, desiredAxisLength, Color.WHITE, null
);
}
// draw nodes
drawNodesOrNodeLabels(g2d);
@ -125,8 +126,7 @@ public class VoronoiPanel extends JPanel {
boolean showNodes = (renderFlags & FLAG_SHOWNODES) > 0;
boolean showLabels = (renderFlags & FLAG_SHOWLABELS) > 0;
for (short i = 0; i < nodes.length; i++) {
MinetestBiome biome = nodes[i];
for (MinetestBiome biome: biomes) {
int x = Math.round(biome.heat_point);
int y = Math.round(biome.humidity_point);
@ -240,7 +240,7 @@ public class VoronoiPanel extends JPanel {
/** returns the perceived brightness of col, between 0 (dark) and 255 (bight) */
@CalledByAny
public double perceivedBrightness(Color col)
public static double perceivedBrightness(Color col)
{
return Math.sqrt(
.299 * Math.pow(col.getRed(), 2) +
@ -248,109 +248,30 @@ public class VoronoiPanel extends JPanel {
.114 * Math.pow(col.getBlue(), 2)
);
}
/** Create the graph BufferedImage if it doesn't already exist at the correct size */
private void createBufferedImage(int size) {
if (graph == null || graph.getWidth() != size) {
graph = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
rgbArray = new int[size * size];
}
}
@CalledByAny
private void drawVoronoiGraph(int[] rgbArray, int width, int height) {
if (nodes == null) return;
MinetestBiome biome_closest;
float dist_min;
int nodeCount = nodes.length;
float xScale = (axis_max - axis_min) / (float)width;
float yScale = (axis_max - axis_min) / (float)height;
int index = 0;
for (int y = 0; y < height; y++) {
float humidity = (y * yScale) + axis_min;
// TODO: break up rgbArray?
for (int x = 0; x < width; x++) {
float heat = (x * xScale) + axis_min;
dist_min = Float.MAX_VALUE;
biome_closest = null;
for (short i = 0; i < nodeCount; i++) {
MinetestBiome b = nodes[i];
float d_heat = heat - b.heat_point;
float d_humidity = humidity - b.humidity_point;
float dist = (d_heat * d_heat) + (d_humidity * d_humidity);
if (dist < dist_min) {
dist_min = dist;
biome_closest = b;
}
}
rgbArray[index++] = (biome_closest == null) ? 0x00000000 : biome_closest.getDefaultColor().getRGB();
}
}
}
private void drawVoronoi(Graphics2D g2d) {
if (nodes == null) return;
MinetestBiome biome_closest;
float dist_min;
int nodeCount = nodes.length;
for (int y = axis_min; y <= axis_max; y++) {
for (int x = axis_min; x <= axis_max; x++) {
float heat = x;
float humidity = y;
dist_min = Float.MAX_VALUE;
biome_closest = null;
for (short i = 0; i < nodeCount; i++) {
MinetestBiome b = nodes[i];
float d_heat = heat - b.heat_point;
float d_humidity = humidity - b.humidity_point;
float dist = (d_heat * d_heat) + (d_humidity * d_humidity);
if (dist < dist_min) {
dist_min = dist;
biome_closest = b;
}
}
if (biome_closest != null) {
g2d.setColor(biome_closest.getDefaultColor().getColor());
g2d.drawRect(x, y, 1, 1);
}
}
}
}
public int getRenderFlags() { return renderFlags; }
@CalledOnlyBy(AmidstThread.EDT)
public void Update(MinetestBiomeProfileImpl biomeProfile, int height, int flags) {
ArrayList<MinetestBiome> biomes = new ArrayList<MinetestBiome>();
ArrayList<MinetestBiome> newBiomes = new ArrayList<MinetestBiome>();
if (biomeProfile != null) for (IBiome biome : biomeProfile.allBiomes()) {
MinetestBiome mtBiome = (MinetestBiome)biome;
if (height <= (mtBiome.y_max + mtBiome.vertical_blend) && height >= mtBiome.y_min) {
biomes.add(mtBiome);
newBiomes.add(mtBiome);
}
}
ArrayList<MinetestBiome> currentNodes = nodes == null ? new ArrayList<MinetestBiome>() : new ArrayList<MinetestBiome>(Arrays.asList(nodes));
if (flags != this.renderFlags || !currentNodes.equals(biomes)) {
//ArrayList<MinetestBiome> currentBiomes = this.biomes == null ? new ArrayList<MinetestBiome>() : new ArrayList<MinetestBiome>(Arrays.asList(nodes));
if (flags != this.renderFlags || !newBiomes.equals(biomes)) {
this.renderFlags = flags;
this.nodes = biomes.toArray(new MinetestBiome[biomes.size()]);
invalidate();
biomes = newBiomes;
graphNodes = new ArrayList<GraphNode>();
for(MinetestBiome biome: newBiomes) {
graphNodes.add(new GraphNode(biome.heat_point, biome.humidity_point, biome.getDefaultColor().getRGB()));
}
repaint();
}
}
public int getRenderFlags() { return renderFlags; }
}

View File

@ -1,8 +1,6 @@
package amidst.gui.voronoi;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
@ -26,8 +24,9 @@ import amidst.settings.biomeprofile.BiomeProfileUpdateListener;
public class VoronoiWindow implements BiomeProfileUpdateListener, ChangeListener {
private static final int ALTITUDESLIDER_DEFAULT_LOW = -40;
private static final int ALTITUDESLIDER_DEFAULT_HIGH = 200;
private static final int ALTITUDESLIDER_DEFAULT_LOW = -40;
private static final int ALTITUDESLIDER_DEFAULT_HIGH = 200;
private static final int ALTITUDESLIDER_STARTING_VALUE = 10; // higher than beaches and oceans, so we start showing "normal" biomes.
private static VoronoiWindow voronoiWindow = null;
private BiomeProfileSelection biomeProfileSelection;
@ -41,7 +40,6 @@ public class VoronoiWindow implements BiomeProfileUpdateListener, ChangeListener
private JCheckBox option_showNodes;
private JSpinner altitudeOffset;
/** the profile being used by Amidstest **/
private MinetestBiomeProfileImpl selectedProfile = null;
@CalledOnlyBy(AmidstThread.EDT)
@ -69,7 +67,7 @@ public class VoronoiWindow implements BiomeProfileUpdateListener, ChangeListener
JLabel heightLabel = new JLabel("Altitude");
result.add(heightLabel, "center, wrap");
altitudeSlider = new JSlider(JSlider.VERTICAL, ALTITUDESLIDER_DEFAULT_LOW, ALTITUDESLIDER_DEFAULT_HIGH, 1);
altitudeSlider = new JSlider(JSlider.VERTICAL, ALTITUDESLIDER_DEFAULT_LOW, ALTITUDESLIDER_DEFAULT_HIGH, ALTITUDESLIDER_STARTING_VALUE);
altitudeSlider.addChangeListener(this);
altitudeSlider.setMajorTickSpacing(10);
altitudeSlider.setMinorTickSpacing(5);
@ -173,12 +171,13 @@ public class VoronoiWindow implements BiomeProfileUpdateListener, ChangeListener
@CalledOnlyBy(AmidstThread.EDT)
public void show(BiomeProfileSelection biomeProfileSelection) {
BiomeProfile newProfile = null;
if (this.biomeProfileSelection != null) {
this.biomeProfileSelection.removeUpdateListener(this);
}
this.biomeProfileSelection = biomeProfileSelection;
BiomeProfile newProfile = null;
if (biomeProfileSelection != null) {
this.biomeProfileSelection.addUpdateListener(this);
newProfile = this.biomeProfileSelection.getCurrentBiomeProfile();