mirror of
https://gitlab.com/gaelysam/mapgen_rivers.git
synced 2025-01-01 14:00:36 +01:00
Initial commit: working example using a basis of Simplex noise and implementing river flowing, lakes, and erosion
This commit is contained in:
commit
0bf351b2f6
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
__pycache__/
|
||||||
|
dem
|
||||||
|
lakes
|
||||||
|
links
|
||||||
|
rivers
|
||||||
|
size
|
||||||
|
unused/
|
77
erosion.py
Normal file
77
erosion.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import numpy as np
|
||||||
|
import scipy.ndimage as im
|
||||||
|
import rivermapper as rm
|
||||||
|
|
||||||
|
def advection(dem, dirs, rivers, time, K=1, m=0.5, sea_level=0):
|
||||||
|
dirs = dirs.copy()
|
||||||
|
dirs[0,:] = 0
|
||||||
|
dirs[-1,:] = 0
|
||||||
|
dirs[:,0] = 0
|
||||||
|
dirs[:,-1] = 0
|
||||||
|
|
||||||
|
adv_time = 1 / (K*rivers**m)
|
||||||
|
dem = np.maximum(dem, sea_level)
|
||||||
|
dem_new = np.zeros(dem.shape)
|
||||||
|
|
||||||
|
for y in range(dirs.shape[0]):
|
||||||
|
for x in range(dirs.shape[1]):
|
||||||
|
x0, y0 = x, y
|
||||||
|
x1, y1 = x, y
|
||||||
|
remaining = time
|
||||||
|
while True:
|
||||||
|
flow_dir = dirs[y0,x0]
|
||||||
|
if flow_dir == 0:
|
||||||
|
remaining = 0
|
||||||
|
break
|
||||||
|
elif flow_dir == 1:
|
||||||
|
y1 += 1
|
||||||
|
elif flow_dir == 2:
|
||||||
|
x1 += 1
|
||||||
|
elif flow_dir == 3:
|
||||||
|
y1 -= 1
|
||||||
|
elif flow_dir == 4:
|
||||||
|
x1 -= 1
|
||||||
|
|
||||||
|
if remaining <= adv_time[y0,x0]:
|
||||||
|
break
|
||||||
|
remaining -= adv_time[y0,x0]
|
||||||
|
x0, y0 = x1, y1
|
||||||
|
|
||||||
|
c = remaining / adv_time[y0,x0]
|
||||||
|
dem_new[y,x] = c*dem[y1,x1] + (1-c)*dem[y0,x0]
|
||||||
|
|
||||||
|
return np.minimum(dem, dem_new)
|
||||||
|
|
||||||
|
def diffusion(dem, time, d=1):
|
||||||
|
radius = d * time**.5
|
||||||
|
return im.gaussian_filter(dem, radius, mode='reflect')
|
||||||
|
|
||||||
|
class EvolutionModel:
|
||||||
|
def __init__(self, dem, K=1, m=0.5, d=1, sea_level=0, flow=False):
|
||||||
|
self.dem = dem
|
||||||
|
#self.bedrock = dem
|
||||||
|
self.K = K
|
||||||
|
self.m = m
|
||||||
|
self.d = d
|
||||||
|
self.sea_level = sea_level
|
||||||
|
#self.ref_isostasy = im.gaussian_filter(dem, radius, mode='reflect')
|
||||||
|
if flow:
|
||||||
|
self.calculate_flow()
|
||||||
|
else:
|
||||||
|
self.lakes = dem
|
||||||
|
self.dirs = np.zeros(dem.shape, dtype='u1')
|
||||||
|
self.rivers = np.zeros(dem.shape, dtype='u4')
|
||||||
|
self.flow_uptodate = False
|
||||||
|
|
||||||
|
def calculate_flow(self):
|
||||||
|
self.dirs, self.lakes, self.rivers = rm.flow(self.dem)
|
||||||
|
self.flow_uptodate = True
|
||||||
|
|
||||||
|
def advection(self, time):
|
||||||
|
dem = advection(self.lakes, self.dirs, self.rivers, time, K=self.K, m=self.m, sea_level=self.sea_level)
|
||||||
|
self.dem = np.minimum(dem, self.dem)
|
||||||
|
self.flow_uptodate = False
|
||||||
|
|
||||||
|
def diffusion(self, time):
|
||||||
|
self.dem = diffusion(self.dem, time, d=self.d)
|
||||||
|
self.flow_uptodate = False
|
178
init.lua
Normal file
178
init.lua
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
local modpath = minetest.get_modpath(minetest.get_current_modname()) .. '/'
|
||||||
|
local worldpath = minetest.get_worldpath() .. '/'
|
||||||
|
local load_map = dofile(modpath .. 'load.lua')
|
||||||
|
|
||||||
|
local function copy_if_needed(filename)
|
||||||
|
local wfilename = worldpath..filename
|
||||||
|
local wfile = io.open(wfilename, 'r')
|
||||||
|
if wfile then
|
||||||
|
wfile:close()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local mfilename = modpath..filename
|
||||||
|
local mfile = io.open(mfilename, 'r')
|
||||||
|
local wfile = io.open(wfilename, 'w')
|
||||||
|
wfile:write(mfile:read("*all"))
|
||||||
|
mfile:close()
|
||||||
|
wfile:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
copy_if_needed('size')
|
||||||
|
local sfile = io.open(worldpath..'size')
|
||||||
|
local X = tonumber(sfile:read('*l'))
|
||||||
|
local Z = tonumber(sfile:read('*l'))
|
||||||
|
|
||||||
|
copy_if_needed('dem')
|
||||||
|
local dem = load_map(worldpath..'dem', 2, true)
|
||||||
|
copy_if_needed('lakes')
|
||||||
|
local lakes = load_map(worldpath..'lakes', 2, true)
|
||||||
|
copy_if_needed('links')
|
||||||
|
local links = load_map(worldpath..'links', 1, false)
|
||||||
|
copy_if_needed('rivers')
|
||||||
|
local rivers = load_map(worldpath..'rivers', 4, false)
|
||||||
|
|
||||||
|
local function index(x, z)
|
||||||
|
return z*X+x+1
|
||||||
|
end
|
||||||
|
|
||||||
|
local function interp(v00, v01, v10, v11, xf, zf)
|
||||||
|
v0 = v01*xf + v00*(1-xf)
|
||||||
|
v1 = v11*xf + v10*(1-xf)
|
||||||
|
return v1*zf + v0*(1-zf)
|
||||||
|
end
|
||||||
|
|
||||||
|
local data = {}
|
||||||
|
|
||||||
|
local blocksize = 6
|
||||||
|
local sea_level = 1
|
||||||
|
local min_catchment = 25
|
||||||
|
|
||||||
|
local storage = minetest.get_mod_storage()
|
||||||
|
if storage:contains("blocksize") then
|
||||||
|
blocksize = storage:get_int("blocksize")
|
||||||
|
else
|
||||||
|
storage:set_int("blocksize", blocksize)
|
||||||
|
end
|
||||||
|
if storage:contains("sea_level") then
|
||||||
|
sea_level = storage:get_int("sea_level")
|
||||||
|
else
|
||||||
|
storage:set_int("sea_level", sea_level)
|
||||||
|
end
|
||||||
|
if storage:contains("min_catchment") then
|
||||||
|
min_catchment = storage:get_float("min_catchment")
|
||||||
|
else
|
||||||
|
storage:set_float("min_catchment", min_catchment)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function generate(minp, maxp, seed)
|
||||||
|
local c_stone = minetest.get_content_id("default:stone")
|
||||||
|
local c_dirt = minetest.get_content_id("default:dirt")
|
||||||
|
local c_lawn = minetest.get_content_id("default:dirt_with_grass")
|
||||||
|
local c_sand = minetest.get_content_id("default:sand")
|
||||||
|
local c_water = minetest.get_content_id("default:water_source")
|
||||||
|
local c_rwater = minetest.get_content_id("default:river_water_source")
|
||||||
|
|
||||||
|
local vm, emin, emax = minetest.get_mapgen_object("voxelmanip")
|
||||||
|
vm:get_data(data)
|
||||||
|
|
||||||
|
local a = VoxelArea:new({MinEdge = emin, MaxEdge = emax})
|
||||||
|
local ystride = a.ystride -- Tip : the ystride of a VoxelArea is the number to add to the array index to get the index of the position above. It's faster because it avoids to completely recalculate the index.
|
||||||
|
|
||||||
|
for x = minp.x, maxp.x do
|
||||||
|
for z = minp.z, maxp.z do
|
||||||
|
local xb = x/blocksize
|
||||||
|
local zb = z/blocksize
|
||||||
|
|
||||||
|
if xb >= 0 and xb < X-1 and zb >= 0 and zb < Z-1 then
|
||||||
|
local x0 = math.floor(xb)
|
||||||
|
local x1 = x0+1
|
||||||
|
local z0 = math.floor(zb)
|
||||||
|
local z1 = z0+1
|
||||||
|
|
||||||
|
local xf = xb - x0
|
||||||
|
local zf = zb - z0
|
||||||
|
|
||||||
|
local i00 = index(x0,z0)
|
||||||
|
local i01 = index(x1,z0)
|
||||||
|
local i10 = index(x0,z1)
|
||||||
|
local i11 = index(x1,z1)
|
||||||
|
|
||||||
|
local terrain_height = math.floor(interp(
|
||||||
|
dem[i00],
|
||||||
|
dem[i01],
|
||||||
|
dem[i10],
|
||||||
|
dem[i11],
|
||||||
|
xf, zf
|
||||||
|
))
|
||||||
|
|
||||||
|
local lake_height = math.floor(math.min(
|
||||||
|
lakes[i00],
|
||||||
|
lakes[i01],
|
||||||
|
lakes[i10],
|
||||||
|
lakes[i11]
|
||||||
|
))
|
||||||
|
|
||||||
|
local is_lake = lake_height > terrain_height
|
||||||
|
|
||||||
|
local is_river = false
|
||||||
|
if xf == 0 then
|
||||||
|
if links[i00] == 1 and rivers[i00] >= min_catchment then
|
||||||
|
is_river = true
|
||||||
|
elseif links[i10] == 3 and rivers[i10] >= min_catchment then
|
||||||
|
is_river = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if zf == 0 then
|
||||||
|
if links[i00] == 2 and rivers[i00] >= min_catchment then
|
||||||
|
is_river = true
|
||||||
|
elseif links[i01] == 4 and rivers[i01] >= min_catchment then
|
||||||
|
is_river = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local ivm = a:index(x, minp.y-1, z)
|
||||||
|
|
||||||
|
if terrain_height >= minp.y then
|
||||||
|
for y=minp.y, math.min(maxp.y, terrain_height) do
|
||||||
|
if y == terrain_height then
|
||||||
|
if is_lake or y <= sea_level then
|
||||||
|
data[ivm] = c_sand
|
||||||
|
elseif is_river then
|
||||||
|
data[ivm] = c_rwater
|
||||||
|
else
|
||||||
|
data[ivm] = c_lawn
|
||||||
|
end
|
||||||
|
else
|
||||||
|
data[ivm] = c_stone
|
||||||
|
end
|
||||||
|
ivm = ivm + ystride
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if lake_height > sea_level then
|
||||||
|
if is_lake and lake_height > minp.y then
|
||||||
|
for y=math.max(minp.y, terrain_height+1), math.min(maxp.y, lake_height) do
|
||||||
|
data[ivm] = c_rwater
|
||||||
|
ivm = ivm + ystride
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
for y=math.max(minp.y, terrain_height+1), math.min(maxp.y, sea_level) do
|
||||||
|
data[ivm] = c_water
|
||||||
|
ivm = ivm + ystride
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
vm:set_data(data)
|
||||||
|
minetest.generate_ores(vm, minp, maxp)
|
||||||
|
vm:set_lighting({day = 0, night = 0})
|
||||||
|
vm:calc_lighting()
|
||||||
|
vm:update_liquids()
|
||||||
|
vm:write_to_map()
|
||||||
|
end
|
||||||
|
|
||||||
|
minetest.register_on_generated(generate)
|
28
load.lua
Normal file
28
load.lua
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
local function load_map(filename, bytes, signed)
|
||||||
|
local file = io.open(filename, 'r')
|
||||||
|
local data = file:read('*all')
|
||||||
|
|
||||||
|
local map = {}
|
||||||
|
|
||||||
|
local size = math.floor(#data/bytes)
|
||||||
|
|
||||||
|
for i=1, size do
|
||||||
|
local i0, i1 = (i-1)*bytes+1, i*bytes
|
||||||
|
local elements = {data:byte(i0, i1)}
|
||||||
|
local n = elements[1]
|
||||||
|
if signed and n >= 128 then
|
||||||
|
n = n - 256
|
||||||
|
end
|
||||||
|
|
||||||
|
for j=2, bytes do
|
||||||
|
n = n*256 + elements[j]
|
||||||
|
end
|
||||||
|
|
||||||
|
map[i] = n
|
||||||
|
end
|
||||||
|
file:close()
|
||||||
|
|
||||||
|
return map
|
||||||
|
end
|
||||||
|
|
||||||
|
return load_map
|
98
rivermapper.py
Normal file
98
rivermapper.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import numpy as np
|
||||||
|
import heapq
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Conventions:
|
||||||
|
# 1 = South (+Y)
|
||||||
|
# 2 = East (+X)
|
||||||
|
# 3 = North (-Y)
|
||||||
|
# 4 = West (-X)
|
||||||
|
|
||||||
|
sys.setrecursionlimit(65536)
|
||||||
|
|
||||||
|
neighbours_dirs = np.array([
|
||||||
|
[0,1,0],
|
||||||
|
[2,0,4],
|
||||||
|
[0,3,0],
|
||||||
|
], dtype='u1')
|
||||||
|
|
||||||
|
neighbours_pattern = neighbours_dirs > 0
|
||||||
|
|
||||||
|
def flow_dirs_lakes(dem):
|
||||||
|
(Y, X) = dem.shape
|
||||||
|
|
||||||
|
dem_margin = np.zeros((Y+2, X+2))
|
||||||
|
dem_margin[1:-1,1:-1] = dem
|
||||||
|
|
||||||
|
# Initialize: list map borders
|
||||||
|
borders = []
|
||||||
|
|
||||||
|
for x in range(1,X+1):
|
||||||
|
dem_north = dem_margin[1,x]
|
||||||
|
borders.append((dem_north, dem_north, 1, x))
|
||||||
|
dem_south = dem_margin[Y,x]
|
||||||
|
borders.append((dem_south, dem_south, Y, x))
|
||||||
|
|
||||||
|
for y in range(2,Y):
|
||||||
|
dem_west = dem_margin[y,1]
|
||||||
|
borders.append((dem_west, dem_west, y, 1))
|
||||||
|
dem_east = dem_margin[y,X]
|
||||||
|
borders.append((dem_east, dem_east, y, X))
|
||||||
|
|
||||||
|
heapq.heapify(borders)
|
||||||
|
|
||||||
|
dirs = np.zeros((Y+2, X+2), dtype='u1')
|
||||||
|
dirs[-2:,:] = 1
|
||||||
|
dirs[:,-2:] = 2
|
||||||
|
dirs[ :2,:] = 3
|
||||||
|
dirs[:, :2] = 4
|
||||||
|
|
||||||
|
lakes = np.zeros((Y, X))
|
||||||
|
|
||||||
|
def add_point(y, x, altmax):
|
||||||
|
alt = dem_margin[y, x]
|
||||||
|
heapq.heappush(borders, (alt, altmax, y, x))
|
||||||
|
|
||||||
|
while len(borders) > 0:
|
||||||
|
(alt, altmax, y, x) = heapq.heappop(borders)
|
||||||
|
neighbours = dirs[y-1:y+2, x-1:x+2]
|
||||||
|
empty_neighbours = (neighbours == 0) * neighbours_pattern
|
||||||
|
neighbours += empty_neighbours * neighbours_dirs
|
||||||
|
|
||||||
|
lake = max(alt, altmax)
|
||||||
|
lakes[y-1,x-1] = lake
|
||||||
|
|
||||||
|
coords = np.transpose(empty_neighbours.nonzero())
|
||||||
|
for (dy,dx) in coords-1:
|
||||||
|
add_point(y+dy, x+dx, lake)
|
||||||
|
|
||||||
|
return dirs[1:-1,1:-1], lakes
|
||||||
|
|
||||||
|
def accumulate(dirs, dem=None):
|
||||||
|
(Y, X) = dirs.shape
|
||||||
|
dirs_margin = np.zeros((Y+2,X+2))-1
|
||||||
|
dirs_margin[1:-1,1:-1] = dirs
|
||||||
|
quantity = np.zeros((Y, X), dtype='u4')
|
||||||
|
|
||||||
|
def calculate_quantity(y, x):
|
||||||
|
if quantity[y,x] > 0:
|
||||||
|
return quantity[y,x]
|
||||||
|
q = 1
|
||||||
|
neighbours = dirs_margin[y:y+3, x:x+3]
|
||||||
|
donors = neighbours == neighbours_dirs
|
||||||
|
|
||||||
|
coords = np.transpose(donors.nonzero())
|
||||||
|
for (dy,dx) in coords-1:
|
||||||
|
q += calculate_quantity(y+dy, x+dx)
|
||||||
|
quantity[y, x] = q
|
||||||
|
return q
|
||||||
|
|
||||||
|
for x in range(X):
|
||||||
|
for y in range(Y):
|
||||||
|
calculate_quantity(y, x)
|
||||||
|
|
||||||
|
return quantity
|
||||||
|
|
||||||
|
def flow(dem):
|
||||||
|
dirs, lakes = flow_dirs_lakes(dem)
|
||||||
|
return dirs, lakes, accumulate(dirs, dem)
|
8
save.py
Normal file
8
save.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import numpy as np
|
||||||
|
|
||||||
|
def save(data, fname, dtype=None):
|
||||||
|
if dtype is not None:
|
||||||
|
data = data.astype(dtype)
|
||||||
|
|
||||||
|
with open(fname, 'wb') as f:
|
||||||
|
f.write(data.tobytes())
|
101
terrain_rivers.py
Executable file
101
terrain_rivers.py
Executable file
@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import noise
|
||||||
|
from save import save
|
||||||
|
from erosion import EvolutionModel
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Always place in this script's parent directory
|
||||||
|
os.chdir(os.path.dirname(sys.argv[0]))
|
||||||
|
argc = len(sys.argv)
|
||||||
|
|
||||||
|
if argc > 1:
|
||||||
|
mapsize = int(sys.argv[1])
|
||||||
|
else:
|
||||||
|
mapsize = 400
|
||||||
|
|
||||||
|
scale = mapsize / 2
|
||||||
|
n = np.zeros((mapsize, mapsize))
|
||||||
|
|
||||||
|
#micronoise_depth = 0.05
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"octaves" : 8,
|
||||||
|
"persistence" : 0.5,
|
||||||
|
"lacunarity" : 2.,
|
||||||
|
}
|
||||||
|
|
||||||
|
xbase = np.random.randint(65536)
|
||||||
|
ybase = np.random.randint(65536)
|
||||||
|
|
||||||
|
for x in range(mapsize):
|
||||||
|
for y in range(mapsize):
|
||||||
|
n[x,y] = noise.snoise2(x/scale + xbase, y/scale + ybase, **params)
|
||||||
|
|
||||||
|
#micronoise = np.random.rand(mapsize, mapsize)
|
||||||
|
#nn = np.exp(n*2) + micronoise*micronoise_depth
|
||||||
|
nn = n*mapsize/5 + mapsize/20
|
||||||
|
|
||||||
|
print('Initializing model')
|
||||||
|
model = EvolutionModel(nn, K=1, m=0.35, d=1, sea_level=0)
|
||||||
|
|
||||||
|
print('Flow calculation 1')
|
||||||
|
model.calculate_flow()
|
||||||
|
|
||||||
|
print('Advection 1')
|
||||||
|
model.advection(2)
|
||||||
|
|
||||||
|
print('Flow calculation 2')
|
||||||
|
model.calculate_flow()
|
||||||
|
|
||||||
|
print('Diffusion')
|
||||||
|
model.diffusion(4)
|
||||||
|
|
||||||
|
print('Advection 2')
|
||||||
|
model.advection(2)
|
||||||
|
|
||||||
|
print('Flow calculation 3')
|
||||||
|
model.calculate_flow()
|
||||||
|
|
||||||
|
print('Done')
|
||||||
|
|
||||||
|
save(model.dem, 'dem', dtype='>i2')
|
||||||
|
save(model.lakes, 'lakes', dtype='>i2')
|
||||||
|
save(model.dirs, 'links', dtype='u1')
|
||||||
|
save(model.rivers, 'rivers', dtype='>u4')
|
||||||
|
|
||||||
|
with open('size', 'w') as sfile:
|
||||||
|
sfile.write('{:d}\n{:d}'.format(mapsize, mapsize))
|
||||||
|
|
||||||
|
try:
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
plt.subplot(2,2,1)
|
||||||
|
plt.pcolormesh(nn, cmap='viridis')
|
||||||
|
plt.gca().set_aspect('equal', 'box')
|
||||||
|
#plt.colorbar(orientation='horizontal')
|
||||||
|
plt.title('Raw elevation')
|
||||||
|
|
||||||
|
plt.subplot(2,2,2)
|
||||||
|
plt.pcolormesh(model.lakes, cmap='viridis')
|
||||||
|
plt.gca().set_aspect('equal', 'box')
|
||||||
|
#plt.colorbar(orientation='horizontal')
|
||||||
|
plt.title('Lake surface elevation')
|
||||||
|
|
||||||
|
plt.subplot(2,2,3)
|
||||||
|
plt.pcolormesh(model.dem, cmap='viridis')
|
||||||
|
plt.gca().set_aspect('equal', 'box')
|
||||||
|
#plt.colorbar(orientation='horizontal')
|
||||||
|
plt.title('Elevation after advection')
|
||||||
|
|
||||||
|
plt.subplot(2,2,4)
|
||||||
|
plt.pcolormesh(model.rivers, vmin=0, vmax=mapsize**2/25, cmap='Blues')
|
||||||
|
plt.gca().set_aspect('equal', 'box')
|
||||||
|
#plt.colorbar(orientation='horizontal')
|
||||||
|
plt.title('Rivers discharge')
|
||||||
|
|
||||||
|
plt.show()
|
||||||
|
except:
|
||||||
|
pass
|
Loading…
Reference in New Issue
Block a user