(Automated) Die-drop Hexmaps with Minecraft Biomes
A couple of weeks ago, Zak H. of Bommyknocker Press posted this really cool guide to using die drops to generate diverse Minecraft-esque biomes.
Being as a GIS nerd, I kept noticing that the basic steps are all fundamental geoprocessing operations, so (with Zak's blessing) I automated it in R (terra
), hoping to host it on itch.io as a shiny app. Unfortunately, itch seems to freak out at at least one of the terra
dependencies and troubleshooting web apps is above my pay grade. So for a functional but slightly disappointing alternative, I wired it up in the QGIS model builder.
If you want to try it, download it here and grab the biome .csv file here, save them to a QGIS project directory, and load the csv into your QGIS layer list and the model in the QGIS Model Designer window (accessed through the Processing menu in the top ribbon). Then hit the green arrow to run it, and plug in whatever parameters you want.
Implementation
Here's the model in its tangled glory:
The light green boxes represent inputs, which should be self-explanatory.
- The row count, column count, and hex size are used to make a hex grid. (Might be a little wonky because QGIS requires defining an extent for the grid to cover. You may need to overshoot and delete a row or column.)
- The "number of dice to drop" parameter generates random points for temperature and elevation with a minimum separation equal to the size of one hex. The "randomize" steps assign a random integer 1-5 to each.
- Inverse distance weighting interpolation is used to generate a smooth surface from the points, and each pixel is then rounded to the nearest integer to get contour regions, which are then converted to polygons.
- The Intersection operation is used to merge the two contour maps into one, and then an attribute is calculated containing the elevation value, the temperature value, and a random number from 1-100.
- Then,
biomes100.csv
is joined to the compositeelev.temp.d100
variable to assign a single minecraft biome to each region. (I adapted this from Zak's google sheet, assigning each biome within a temperature-elevation zone a range of d100 values). - Finally, a Spatial Join is used to assign each hex the biome of the contour region it most overlaps with. Both the contour map and the hex grid are outputted.
To visualize the output, right-click the hex layer and select Properties, navigate to the Symbology tab, select Categorized symbology, select the Biome
field, and press Classify to assign each biome a random color. You can then double-click the swatches to set them manually if desired.
Notes
I really like how this method creates organic-looking blobby biomes and works off of Minecraft's evocative biome types. Also, despite all the effort I sunk into automating it, I think it's awesome that this method can be done by hand.
I think it would be cool to take a larger hexmap with already-assigned biomes and adapt this method to carve up the larger biome patches into smaller sub-biome blobs.
One suggested improvement: having temperature range from 1 to 5 results in maps that include both frozen tundra and steaming jungles -- which may work for some very fantastical worlds but strains credibility in others. I find I get better results when I manually edit the Ranomize_elev
step to draw from 1-4 or 2-5.
The R Version
The basic functions of the terra
implementation were as follows, recorded here on the off chance it's of use to anyone.
The die drop generates random points:
drop_locations <- spatSample(shape, size = die_count, method = "random")
Instead of intentionally dispersing points (as advised in the original post), we can implement Lloyd relaxation:
lloyd_relax <- function(points, extent){
return(centroids(voronoi(points, bnd = extent)))
}
Then we can use inverse distance weighting interpolation to turn the points into a smooth surface, and then generate contours by rounding the surface to the nearest integer:
drop_surface <- interpIDW(r, drop_locations, field = "die_result", radius = hex_size_m*max(rc, cc), power=2.5)
We can then run that twice, use an intersect operation to overlay the maps, and then use a table join to assign biomes:
elev <- die_drop_countours(variable = 'elev', die = input$elevrange[1]:input$elevrange[2], shape=hexes, die_count = input$diecount, rc = input$rowcount, cc = input$colcount)
temp <- die_drop_countours(variable = 'temp', die = input$temprange[1]:input$temprange[2], shape=hexes, die_count = input$diecount, rc = input$rowcount, cc = input$colcount)
elev.temp.intersect <- terra::intersect(elev, temp) %>%
mutate(elev.temp = paste(elev, temp, sep = ';'),
zone_id = row_number()) %>%
left_join(biomes) %>%
group_by(zone_id) %>%
slice_sample(n = 1)
And finally, we can sample the layer at hex centers to make a final hex map:
sampled_centroids <- terra::intersect(hex_centroids, elev.temp.intersect)
print(colnames(sampled_centroids))
hexes <- hexes %>%
left_join(as.data.frame(sampled_centroids), by = c('id' = 'id'))