├── images ├── hexbigger.png ├── hexsteps.gif ├── hexsteps.png └── hexstepstri.png ├── LICENSE ├── HexFractalRegionGeneratorDemo.java ├── README.md └── HexFractalRegionGenerator.java /images/hexbigger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KdotJPG/Hex-Fractal-Region-Generator/HEAD/images/hexbigger.png -------------------------------------------------------------------------------- /images/hexsteps.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KdotJPG/Hex-Fractal-Region-Generator/HEAD/images/hexsteps.gif -------------------------------------------------------------------------------- /images/hexsteps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KdotJPG/Hex-Fractal-Region-Generator/HEAD/images/hexsteps.png -------------------------------------------------------------------------------- /images/hexstepstri.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KdotJPG/Hex-Fractal-Region-Generator/HEAD/images/hexstepstri.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 KdotJPG 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /HexFractalRegionGeneratorDemo.java: -------------------------------------------------------------------------------- 1 | import java.awt.image.BufferedImage; 2 | import javax.imageio.ImageIO; 3 | import java.io.*; 4 | import javax.swing.*; 5 | import java.awt.Color; 6 | 7 | public class HexFractalRegionGeneratorDemo 8 | { 9 | private static final int WIDTH = 1024; 10 | private static final int HEIGHT = 1024; 11 | 12 | private static final int SEED = 8; 13 | private static final int VARIETY = 4; 14 | private static final int SIZE = 9; 15 | private static final int STEPS = 9; 16 | 17 | private static final Color[] COLORS = new Color[] { 18 | Color.GREEN, Color.YELLOW, Color.DARK_GRAY, Color.GRAY, Color.ORANGE, Color.MAGENTA, Color.PINK, Color.RED, Color.BLUE 19 | }; 20 | 21 | public static void main(String[] args) 22 | throws IOException { 23 | 24 | // Initialize 25 | HexFractalRegionGenerator hexLayer = new HexFractalRegionGenerator(SEED, VARIETY, SIZE, STEPS); 26 | 27 | // Image 28 | BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB); 29 | for (int y = 0; y < HEIGHT; y++) 30 | { 31 | for (int x = 0; x < WIDTH; x++) 32 | { 33 | int value = hexLayer.smoothSample(x * SIZE * 1.0 / WIDTH, y * SIZE * 1.0 / HEIGHT); 34 | int rgb = COLORS[value].getRGB(); 35 | image.setRGB(x, y, rgb); 36 | } 37 | } 38 | 39 | // Save it or show it 40 | if (args.length > 0 && args[0] != null) { 41 | ImageIO.write(image, "png", new File(args[0])); 42 | System.out.println("Saved image as " + args[0]); 43 | } else { 44 | JFrame frame = new JFrame(); 45 | JLabel imageLabel = new JLabel(); 46 | imageLabel.setIcon(new ImageIcon(image)); 47 | frame.add(imageLabel); 48 | frame.pack(); 49 | frame.setResizable(false); 50 | frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); 51 | frame.setVisible(true); 52 | } 53 | 54 | } 55 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hex Fractal Region Generator 2 | The [Grown Biomes](http://mc-server.xoft.cz/docs/Generator.html#biome.grown) method, but implemented using a hex grid instead. 3 | 4 | # Images 5 | 6 | ![Steps](images/hexsteps.gif?raw=true) 7 | 8 | ![Bigger Area](images/hexbigger.png?raw=true) 9 | 10 | # How it works 11 | 12 | 1. Create a square array of side length (size * 2^steps)+1. This represents a diagonally-compressed square (rhombus) that maps to the hexagonal lattice. 13 | 2. Populate the initial values, spaced out by stride=(2^steps). 14 | * Care could be taken in this step to ensure that certain region types don't border each other. But in this implementation, they are just assigned randomly. 15 | 3. Between each pair of defined values, set a new value that is randomly chosen between the two. These correspond to the midpoints of the triangular edges. 16 | 4. Repeat #3 until every cell is assigned a value. 17 | 5. Sample the grid using the skew transform from 2D simplex noise. 18 | * The first implemented sampler just finds the closest hexagon. 19 | * The second implemented sampler (shown in demo) considers identically-valued cells together, to straighten the edges. 20 | 21 | ![Steps](images/hexsteps.png?raw=true) 22 | Step order is: Red, Orange, Yellow, Green. Rhombic section is highlighted, with the surrounding area grayed out. 23 | 24 | # Extensions 25 | 26 | ## Infinite Grid 27 | It would be straightforward to extend this to an infinite grid. Divide the grid into compressed-square / rhombic patches, that overlap on just the padding (the +1 in step 1). Maintain a cache of patches. Have the sampler identify the current patch, then load or generate it. Discard old / infrequently used patches from the cache when appropriate, as part of this step. Replace java.util.random with a hash function that always produces the same value for a given coordinate. 28 | 29 | ## Higher Dimensions 30 | It could be generalized to higher dimensions, using either the A or A* lattice as a generalization of the triangular lattice. See: Simplex or OpenSimplex noise. 31 | 32 | ## Centers not edges 33 | This approach iteratively populates the midpoints of the edges of triangles. An alternative approach could populate the centers of the triangles instead, chosen by the three corners. This is a more complex case, and would require more padding to make all necessary data available. It may also be more difficult to generalize to higher dimensions. But could be worth exploring the different properties it has, if any. The below image illustrates these steps with the same coloring as the image shown above. 34 | 35 | ![Midpoint Steps](images/hexstepstri.png?raw=true) 36 | 37 | # Is it better? 38 | 39 | Is this better than the square grid approach? Hard to say. I do tend to notice hexagonal bias less readily than square bias. Hexagonal grids also have a number of nice properties, such as having more angles of symmetry, and being both the optimal packing and covering of 2D space. I like the result better than the square approach, but I think it could be improved further. Perhaps the "Centers" approach would provide even better results, or perhaps some bias control measures could be employed. 40 | 41 | ###### Source for hex grid image used as a base for some illustrations: https://svgsilh.com/image/156568.html 42 | -------------------------------------------------------------------------------- /HexFractalRegionGenerator.java: -------------------------------------------------------------------------------- 1 | import java.util.Random; 2 | 3 | public class HexFractalRegionGenerator { 4 | 5 | private int[][] cells; 6 | double coordinateScale; 7 | 8 | public HexFractalRegionGenerator(long seed, int variety, int size, int steps) { 9 | Random random = new Random(seed); 10 | 11 | // (2^N)x(2^N) plus one for padding, for diagonally-compressed-square array 12 | // which models hexagonal grid. 13 | int stride = 1 << steps; // Math.pow(2, steps); 14 | int unpaddedArraySize = stride * size; 15 | int arraySize = unpaddedArraySize + 1; 16 | cells = new int[arraySize][arraySize]; 17 | 18 | // Adjust for detail level and "compression" of square, so (1, 1) maps to (stride, stride) 19 | this.coordinateScale = stride * 0.5773502691896257; // sqrt(1/3) 20 | 21 | // Assign initial values 22 | for (int y = 0; y < arraySize; y += stride) { 23 | for (int x = 0; x < arraySize; x += stride) { 24 | cells[x][y] = random.nextInt(variety); 25 | } 26 | } 27 | 28 | // Fractal steps 29 | for (int i = 0; i < steps; i++) { 30 | 31 | // When indexing neighbors on the next level of detail, we use this. 32 | // We will also use it as the actual stride for the next step. 33 | int halfStride = stride / 2; 34 | 35 | // For each cell on the current level of detail... 36 | for (int y = 0; y < arraySize; y += stride) { 37 | for (int x = 0; x < arraySize; x += stride) { 38 | // It has six hexagonal neighbors on the next layer down. It suffices and also avoids repetition, 39 | // to consider only the "positive" three, (h, 0), (h, h), (0, h), omitting those out of range. 40 | // h = halfStride, s = stride 41 | 42 | // (h, 0) picks between current and current+(s, 0) 43 | if (x < unpaddedArraySize) 44 | cells[y][x + halfStride] = random.nextBoolean() ? cells[y][x + stride] : cells[y][x]; 45 | 46 | // (0, h) picks between current and current+(0, s) 47 | if (y < unpaddedArraySize) 48 | cells[y + halfStride][x] = random.nextBoolean() ? cells[y + stride][x] : cells[y][x]; 49 | 50 | // (h, h) picks between current and current+(s, s) 51 | if (x < unpaddedArraySize && y < unpaddedArraySize) 52 | cells[y + halfStride][x + halfStride] = random.nextBoolean() ? cells[y + stride][x + stride] : cells[y][x]; 53 | } 54 | } 55 | 56 | stride = halfStride; 57 | } 58 | } 59 | 60 | public int simpleSample(double x, double y) { 61 | 62 | // Rescale input coordinates [0,1] to match stride. 63 | x *= coordinateScale; 64 | y *= coordinateScale; 65 | 66 | // Skew like simplex noise 67 | double s = 0.366025403784439 * (x + y); 68 | double xs = x + s, ys = y + s; 69 | 70 | // Get base and internal offsets, for local diagonally-compressed square. 71 | int xsb = (int)xs; if (xs < xsb) xsb -= 1; 72 | int ysb = (int)ys; if (ys < ysb) ysb -= 1; 73 | double xsi = xs - xsb, ysi = ys - ysb; 74 | 75 | // Find closest of the four points. 76 | double p = 2 * xsi - ysi; 77 | double q = 2 * ysi - xsi; 78 | double r = xsi + ysi; 79 | if (r > 1) { 80 | p -= 1; q -= 1; r -= 2; 81 | if (p < -1) { 82 | return cells[ysb + 1][xsb]; 83 | } else if (q < -1) { 84 | return cells[ysb][xsb + 1]; 85 | } 86 | return cells[ysb + 1][xsb + 1]; 87 | } else { 88 | if (p > 1) { 89 | return cells[ysb][xsb + 1]; 90 | } else if (q > 1) { 91 | return cells[ysb + 1][xsb]; 92 | } 93 | return cells[ysb][xsb]; 94 | } 95 | } 96 | 97 | public int smoothSample(double x, double y) { 98 | 99 | // Rescale input coordinates [0,1] to match stride. 100 | x *= coordinateScale; 101 | y *= coordinateScale; 102 | 103 | // Skew like simplex noise 104 | double s = 0.366025403784439 * (x + y); 105 | double xs = x + s, ys = y + s; 106 | 107 | // Get base and internal offsets, for local diagonally-compressed square. 108 | int xsb = (int)xs; if (xs < xsb) xsb -= 1; 109 | int ysb = (int)ys; if (ys < ysb) ysb -= 1; 110 | double xsi = xs - xsb, ysi = ys - ysb; 111 | 112 | // Prepare arrays to combine weights of like values, and separate those of different ones. 113 | int[] values = new int[] { -1, -1, -1 }; 114 | double[] scores = new double[3]; 115 | 116 | // Consider (0, 0) and (1, 1) 117 | handleSmoothSampleWeight(values, scores, cells[ysb][xsb], xsi, ysi); 118 | handleSmoothSampleWeight(values, scores, cells[ysb + 1][xsb + 1], xsi - 1, ysi - 1); 119 | 120 | // Consider one of (1, 0) and (0, 1) 121 | if (ysi > xsi) 122 | handleSmoothSampleWeight(values, scores, cells[ysb + 1][xsb], xsi, ysi - 1); 123 | else 124 | handleSmoothSampleWeight(values, scores, cells[ysb][xsb + 1], xsi - 1, ysi); 125 | 126 | // Find the winning value for this input coordinate. 127 | int value = -1; 128 | double score = -1; 129 | for (int i = 0; i < 3; i++) { 130 | if (scores[i] > score) { 131 | value = values[i]; 132 | score = scores[i]; 133 | } 134 | } 135 | 136 | return value; 137 | } 138 | 139 | private void handleSmoothSampleWeight(int[] values, double[] scores, int value, double xsi, double ysi) { 140 | 141 | // Hexagonally-symmetric bump function 142 | double zsi = xsi - ysi; 143 | double weight = (1 - xsi * xsi) * (1 - ysi * ysi) * (1 - zsi * zsi); 144 | 145 | // Combine weights of identical cells, otherwise create new entry. 146 | for (int i = 0; i < 3; i++) { 147 | if (values[i] == -1 || values[i] == value) { 148 | values[i] = value; 149 | scores[i] += weight; 150 | break; 151 | } 152 | } 153 | } 154 | } 155 | --------------------------------------------------------------------------------