Outdoor Survival GIS Layers
As part of my recent bout of OD&D-related tinkering, I used GIS to make a pseudo-realistic heightmap & nice-ish map layout based on the Outdoor Survival gameboard.1
Downloads
- A hex grid GIS layer with terrain types, castles, and towns can be downloaded here.
- The eroded heightmap can be downloaded here.
- The rivers can be downloaded here.
- The map above (styled using QGIS) can be downloaded in high-res here.
- I also turned the map into a no-frills hexcrawl workbook that can be downloaded here and printed at-cost on Lulu.
Methods
I started with a 5-mile hex grid in QGIS & manually assigned biomes using EdOWar's map as a reference (see here for making hex grids in QGIS). I also labeled castles, towns, and rivers as hex attributes.
Looking at the original map, it makes the most sense to assume the whole area drains toward the river outlet in the north, with the two swamps behaving like lake basins (presumably very old lakes that have mostly filled with sediment). So, I created an initial heightmap (100m x 100m resolution) from the following layers:
- A 'northward tilt' layer to ensure everything drains toward the north, spanning about 300 meters of elevation.
- A 'river drainage layer', where rivers & swamps are assigned a height of 0 and every other cell gets a height value derived from the scaled logarithm of the Euclidean distance from the nearest river/swamp cell.
- A 'mountain layer', created by multiplying a ridged multi-fractal noise layer by the square root interior distance of the mountain hexes (i.e. distance to the nearest non-mountain pixel. This was scaled to span around 2,500 meters.
- A pure random noise layer (range around 30 meters) to prevent rivers from going in straight lines and a 'soft' Perlin noise layer to mix up the flat areas just a bit (range: 250 meters).
Next, I did some fake erosion using Wilbur -- mainly incise flow & precipiton erosion. To get the river paths, I loaded it into R, made a flow accumulation raster using the terra
package, took its logarithm to get a rough 'relative river width' value, and then used r.thin
and r.to.vect
to turn it into a line layer.
The result still looks unrealistic from a distance, but zoomed I think it's pretty darn good. The rivers are all behaving as intended, and are hopefully just distinct enough to ward off copyright infringement.
Code
(Messy, this was a very iterative process -- but this should give the gist of it).
library(tidyverse)
library(terra)
library(tidyterra)
library(flowdem)
devtools::load_all()
library(worldbuildeR) # personal library, will hopefully share eventually
os.shp <- vect('../Personal/Grid1.gpkg')
base_rast <- rast(ext=ext(os.shp), resolution=100, crs=crs(os.shp), vals=0)
north_tilt <- base_rast %>% as.data.frame(xy = T) %>%
mutate(z = 3000 - (y/2500)) %>%
select(x, y, z) %>%
rast(type='xyz', crs = crs(os.shp), ext = ext(os.shp))
plot(north_tilt)
river_hexes <- os.shp %>% filter(River == T|Terrain=='Wetland') %>%
rasterize(base_rast)
plot(river_hexes)
dist_river <- river_hexes %>%
gridDist(target = NA, scale=1000) %>% log()
dist_river <- ifel((river_hexes)|dist_river<0, 0, dist_river) * 150
plot(dist_river)
dist_river_interior <- ifel([is.na](http://is.na/)(river_hexes), 1, 0) %>%
gridDist(target = 1, scale = 1000) %>% log()
dist_river_interior <- ifel([is.na](http://is.na/)(river_hexes)|dist_river_interior<0, 0, -55 * dist_river_interior)
plot(dist_river_interior)
mountain_hexes <- os.shp %>% filter(Terrain=='Mountain') %>%
rasterize(base_rast)
mountain_interior_dist <- ifel([is.na](http://is.na/)(mountain_hexes), 1, 0) %>%
gridDist(target = 1, scale = 1000) %>% sqrt() %>%
focal(w=11, fun='max')
mountain_interior_dist <- ifel([is.na](http://is.na/)(mountain_interior_dist)|mountain_interior_dist<0.25, 0.25, mountain_interior_dist) %>%
focal(w=13, fun='mean', na.rm=T) %>%
focal(w=13, fun='mean', na.rm=T)
plot(mountain_interior_dist)
mountain_noise <- generate_fractal_noise(base_rast, fractal = 'rigid-multi', frequency=0.013, lacunarity = 1.75, octaves=7, gain=2) %>% rescale_heightmap(max = 750, min = -100)
mountain_noise <- mountain_noise * (mountain_interior_dist)
render_terrain(mountain_noise)
soft_noise <- generate_perlin_noise(base_rast, frequency = 0.0015) %>% rescale_heightmap(max = 250, min = 0)
plot(soft_noise)
render_terrain(soft_noise)
base_topo <- ((north_tilt*4 - 1000) + dist_river + dist_river_interior + mountain_noise + soft_noise) %>%
flowdem::fill() %>%
add_noise(amount=15) %>%
flowdem::fill()
plot(base_topo)
render_terrain(base_topo)
rainfall_map <- data.frame("Terrain" = c("Open", "Desert", "Forest", "Mountain", "Wetland"),
"Rainfall" = c(1, 0.1, 1.5, 2, 3))
rainfall.r <- os.shp %>% left_join(rainfall_map) %>%
rasterize(base_rast, "Rainfall", background = 1)
eroded <- base_topo %>%
incise_flow(rainfall=rainfall.r)%>%
flowdem::fill() %>%
incise_flow(rainfall=rainfall.r) %>%
add_noise(amount=15) %>%
flowdem::fill() %>%
incise_flow(rainfall=rainfall.r) %>%
incise_flow(rainfall=rainfall.r)
render_terrain(eroded)
eroded <- rainfall_erode(eroded, precipitons = 1000, rainfall = rainfall.r)
writeRaster(eroded, '../Personal/outdoor_dem.tif', overwrite=T)
eroded <- rast('../Personal/outdoor_dem.tif')
flowacc <- log(flowAccumulation(terra::terrain(eroded %>% fill(), v='flowdir')))
writeRaster(flowacc, '../Personal/outdoor_flowacc.tif', overwrite=T)
For those who might not be aware, in OD&D Gygax & Arneson recommend using the Outdoor Survival board as an overworld map for hexcrawling. Lakes are to be interpreted as castles, and buildings as villages.↩