mirror of
https://gitlab.com/gaelysam/mapgen_rivers.git
synced 2025-01-01 05:50:34 +01:00
Comment and clarify
This commit is contained in:
parent
49bc397718
commit
6af6795d90
@ -1,17 +1,17 @@
|
|||||||
mapgen_rivers
|
mapgen_rivers
|
||||||
=============
|
=============
|
||||||
|
|
||||||
Procedural map generator for Minetest 5.x. Still experimental and basic.
|
Procedural map generator for Minetest 5.x. Focused on river networks, and features valley erosion and lakes.
|
||||||
|
|
||||||
Contains two distinct programs: Python scripts for pre-processing, and Lua scripts to generate the map on Minetest.
|
Contains two distinct programs: Python scripts for pre-processing, and Lua scripts to generate the map on Minetest.
|
||||||
|
|
||||||
![Screenshot](https://user-images.githubusercontent.com/6905002/79073532-7a567f00-7ce7-11ea-9791-8fb453f5175d.png)
|
![Screenshot](https://user-images.githubusercontent.com/6905002/79541028-687b3000-8089-11ea-9209-c23c15d75383.png)
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
This mod should be placed in the `/mods` directory like any other Minetest mod.
|
This mod should be placed in the `/mods` directory like any other Minetest mod.
|
||||||
|
|
||||||
The Python part relies on external libraries that you need to install:
|
The Python part relies on external libraries that you need to install:
|
||||||
- `numpy`, a widely used library for numerical calculations
|
- `numpy` and `scipy`, widely used libraries for numerical calculations
|
||||||
- `noise`, doing Perlin/Simplex noises
|
- `noise`, doing Perlin/Simplex noises
|
||||||
- optionally, `matplotlib` (for map preview)
|
- optionally, `matplotlib` (for map preview)
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ Run the script `terrain_rivers.py` via command line. You can optionally append t
|
|||||||
```
|
```
|
||||||
./terrain_rivers.py 1000
|
./terrain_rivers.py 1000
|
||||||
```
|
```
|
||||||
For a default 400x400 map, it should take between 1 and 2 minutes. It will generate 5 files directly in the mod folder, containing the map data (1.4 MB for the default size).
|
For a default 401x401 map, it should take between 1 and 2 minutes. It will generate 5 files directly in the mod folder, containing the map data.
|
||||||
|
|
||||||
## Map generation
|
## Map generation
|
||||||
Just create a Minetest world with `singlenode` mapgen, enable this mod and start the world. The data files are immediately copied in the world folder so you can re-generate them afterwards, it won't affect the old worlds.
|
Just create a Minetest world with `singlenode` mapgen, enable this mod and start the world. The data files are immediately copied in the world folder so you can re-generate them afterwards, it won't affect the old worlds.
|
||||||
|
16
bounds.py
16
bounds.py
@ -1,7 +1,10 @@
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
|
|
||||||
def make_bounds(dirs, rivers):
|
def make_bounds(dirs, rivers):
|
||||||
|
"""
|
||||||
|
Give an array of all horizontal and vertical bounds
|
||||||
|
"""
|
||||||
|
|
||||||
(Y, X) = dirs.shape
|
(Y, X) = dirs.shape
|
||||||
bounds_h = np.zeros((Y, X-1), dtype='i4')
|
bounds_h = np.zeros((Y, X-1), dtype='i4')
|
||||||
bounds_v = np.zeros((Y-1, X), dtype='i4')
|
bounds_v = np.zeros((Y-1, X), dtype='i4')
|
||||||
@ -14,6 +17,10 @@ def make_bounds(dirs, rivers):
|
|||||||
return bounds_h, bounds_v
|
return bounds_h, bounds_v
|
||||||
|
|
||||||
def get_fixed(dirs):
|
def get_fixed(dirs):
|
||||||
|
"""
|
||||||
|
Give the list of points that should not be twisted
|
||||||
|
"""
|
||||||
|
|
||||||
borders = np.zeros(dirs.shape, dtype='?')
|
borders = np.zeros(dirs.shape, dtype='?')
|
||||||
borders[-1,:] |= dirs[-1,:]==1
|
borders[-1,:] |= dirs[-1,:]==1
|
||||||
borders[:,-1] |= dirs[:,-1]==2
|
borders[:,-1] |= dirs[:,-1]==2
|
||||||
@ -28,6 +35,11 @@ def get_fixed(dirs):
|
|||||||
return borders | ~donors
|
return borders | ~donors
|
||||||
|
|
||||||
def twist(bounds_x, bounds_y, fixed, d=0.1, n=5):
|
def twist(bounds_x, bounds_y, fixed, d=0.1, n=5):
|
||||||
|
"""
|
||||||
|
Twist the grid (define an offset for every node). Model river bounds as if they were elastics.
|
||||||
|
Smoothes preferentially big rivers.
|
||||||
|
"""
|
||||||
|
|
||||||
moveable = ~fixed
|
moveable = ~fixed
|
||||||
|
|
||||||
(Y, X) = fixed.shape
|
(Y, X) = fixed.shape
|
||||||
@ -55,7 +67,7 @@ def twist(bounds_x, bounds_y, fixed, d=0.1, n=5):
|
|||||||
|
|
||||||
length = np.hypot(force_x, force_y)
|
length = np.hypot(force_x, force_y)
|
||||||
length[length==0] = 1
|
length[length==0] = 1
|
||||||
coeff = d / length * moveable
|
coeff = d / length * moveable # Normalize, take into account the direction only
|
||||||
offset_x += force_x * coeff
|
offset_x += force_x * coeff
|
||||||
offset_y += force_y * coeff
|
offset_y += force_y * coeff
|
||||||
|
|
||||||
|
27
erosion.py
27
erosion.py
@ -3,22 +3,33 @@ import scipy.ndimage as im
|
|||||||
import rivermapper as rm
|
import rivermapper as rm
|
||||||
|
|
||||||
def advection(dem, dirs, rivers, time, K=1, m=0.5, sea_level=0):
|
def advection(dem, dirs, rivers, time, K=1, m=0.5, sea_level=0):
|
||||||
|
"""
|
||||||
|
Simulate erosion by rivers.
|
||||||
|
This models erosion as an upstream advection of elevations ("erosion waves").
|
||||||
|
Advection speed depends on water flux and parameters:
|
||||||
|
|
||||||
|
v = K * flux^m
|
||||||
|
"""
|
||||||
|
|
||||||
dirs = dirs.copy()
|
dirs = dirs.copy()
|
||||||
dirs[0,:] = 0
|
dirs[0,:] = 0
|
||||||
dirs[-1,:] = 0
|
dirs[-1,:] = 0
|
||||||
dirs[:,0] = 0
|
dirs[:,0] = 0
|
||||||
dirs[:,-1] = 0
|
dirs[:,-1] = 0
|
||||||
|
|
||||||
adv_time = 1 / (K*rivers**m)
|
adv_time = 1 / (K*rivers**m) # For every pixel, calculate the time an "erosion wave" will need to cross it.
|
||||||
dem = np.maximum(dem, sea_level)
|
dem = np.maximum(dem, sea_level)
|
||||||
dem_new = np.zeros(dem.shape)
|
dem_new = np.zeros(dem.shape)
|
||||||
|
|
||||||
for y in range(dirs.shape[0]):
|
for y in range(dirs.shape[0]):
|
||||||
for x in range(dirs.shape[1]):
|
for x in range(dirs.shape[1]):
|
||||||
|
# Elevations propagate upstream, so for every pixel we seek the downstream pixel whose erosion wave just reached the current pixel.
|
||||||
|
# This means summing the advection times downstream until we reach the erosion time.
|
||||||
x0, y0 = x, y
|
x0, y0 = x, y
|
||||||
x1, y1 = x, y
|
x1, y1 = x, y
|
||||||
remaining = time
|
remaining = time
|
||||||
while True:
|
while True:
|
||||||
|
# Move one pixel downstream
|
||||||
flow_dir = dirs[y0,x0]
|
flow_dir = dirs[y0,x0]
|
||||||
if flow_dir == 0:
|
if flow_dir == 0:
|
||||||
remaining = 0
|
remaining = 0
|
||||||
@ -32,19 +43,19 @@ def advection(dem, dirs, rivers, time, K=1, m=0.5, sea_level=0):
|
|||||||
elif flow_dir == 4:
|
elif flow_dir == 4:
|
||||||
x1 -= 1
|
x1 -= 1
|
||||||
|
|
||||||
if remaining <= adv_time[y0,x0]:
|
if remaining <= adv_time[y0,x0]: # Time is over, we found it.
|
||||||
break
|
break
|
||||||
remaining -= adv_time[y0,x0]
|
remaining -= adv_time[y0,x0]
|
||||||
x0, y0 = x1, y1
|
x0, y0 = x1, y1
|
||||||
|
|
||||||
c = remaining / adv_time[y0,x0]
|
c = remaining / adv_time[y0,x0]
|
||||||
dem_new[y,x] = c*dem[y1,x1] + (1-c)*dem[y0,x0]
|
dem_new[y,x] = c*dem[y1,x1] + (1-c)*dem[y0,x0] # If between 2 pixels, perform linear interpolation.
|
||||||
|
|
||||||
return np.minimum(dem, dem_new)
|
return np.minimum(dem, dem_new)
|
||||||
|
|
||||||
def diffusion(dem, time, d=1):
|
def diffusion(dem, time, d=1):
|
||||||
radius = d * time**.5
|
radius = d * time**.5
|
||||||
return im.gaussian_filter(dem, radius, mode='reflect')
|
return im.gaussian_filter(dem, radius, mode='reflect') # Diffusive erosion is a simple Gaussian blur
|
||||||
|
|
||||||
class EvolutionModel:
|
class EvolutionModel:
|
||||||
def __init__(self, dem, K=1, m=0.5, d=1, sea_level=0, flow=False, flex_radius=100):
|
def __init__(self, dem, K=1, m=0.5, d=1, sea_level=0, flow=False, flex_radius=100):
|
||||||
@ -78,9 +89,9 @@ class EvolutionModel:
|
|||||||
self.flow_uptodate = False
|
self.flow_uptodate = False
|
||||||
|
|
||||||
def define_isostasy(self):
|
def define_isostasy(self):
|
||||||
self.ref_isostasy = im.gaussian_filter(self.dem, self.flex_radius, mode='reflect')
|
self.ref_isostasy = im.gaussian_filter(self.dem, self.flex_radius, mode='reflect') # Define a blurred version of the DEM that will be considered as the reference isostatic elevation.
|
||||||
|
|
||||||
def adjust_isostasy(self, rate=1):
|
def adjust_isostasy(self, rate=1):
|
||||||
isostasy = im.gaussian_filter(self.dem, self.flex_radius, mode='reflect')
|
isostasy = im.gaussian_filter(self.dem, self.flex_radius, mode='reflect') # Calculate blurred DEM
|
||||||
correction = (self.ref_isostasy - isostasy) * rate
|
correction = (self.ref_isostasy - isostasy) * rate # Compare it with the reference isostasy
|
||||||
self.dem = self.dem + correction
|
self.dem = self.dem + correction # Adjust
|
||||||
|
10
geometry.lua
10
geometry.lua
@ -1,27 +1,33 @@
|
|||||||
local function distance_to_segment(x1, y1, x2, y2, x, y)
|
local function distance_to_segment(x1, y1, x2, y2, x, y)
|
||||||
-- get the distance between point (x,y) and segment (x1,y1)-(x2,y2)
|
-- get the distance between point (x,y) and segment (x1,y1)-(x2,y2)
|
||||||
local a = (x1-x2)^2 + (y1-y2)^2
|
local a = (x1-x2)^2 + (y1-y2)^2 -- square of distance
|
||||||
local b = (x1-x)^2 + (y1-y)^2
|
local b = (x1-x)^2 + (y1-y)^2
|
||||||
local c = (x2-x)^2 + (y2-y)^2
|
local c = (x2-x)^2 + (y2-y)^2
|
||||||
if a + b < c then
|
if a + b < c then
|
||||||
|
-- The closest point of the segment is the extremity 1
|
||||||
return math.sqrt(b)
|
return math.sqrt(b)
|
||||||
elseif a + c < b then
|
elseif a + c < b then
|
||||||
|
-- The closest point of the segment is the extremity 2
|
||||||
return math.sqrt(c)
|
return math.sqrt(c)
|
||||||
else
|
else
|
||||||
|
-- The closest point is on the segment
|
||||||
return math.abs(x1 * (y2-y) + x2 * (y-y1) + x * (y1-y2)) / math.sqrt(a)
|
return math.abs(x1 * (y2-y) + x2 * (y-y1) + x * (y1-y2)) / math.sqrt(a)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function transform_quadri(X, Y, x, y)
|
local function transform_quadri(X, Y, x, y)
|
||||||
-- X, Y 4-vectors giving the coordinates of the 4 nodes
|
-- To index points in an irregular quadrilateral, giving x and y between 0 (one edge) and 1 (opposite edge)
|
||||||
|
-- X, Y 4-vectors giving the coordinates of the 4 vertices
|
||||||
-- x, y position to index.
|
-- x, y position to index.
|
||||||
local x1, x2, x3, x4 = unpack(X)
|
local x1, x2, x3, x4 = unpack(X)
|
||||||
local y1, y2, y3, y4 = unpack(Y)
|
local y1, y2, y3, y4 = unpack(Y)
|
||||||
|
|
||||||
|
-- Compare distance to 2 opposite edges, they give the X coordinate
|
||||||
local d23 = distance_to_segment(x2,y2,x3,y3,x,y)
|
local d23 = distance_to_segment(x2,y2,x3,y3,x,y)
|
||||||
local d41 = distance_to_segment(x4,y4,x1,y1,x,y)
|
local d41 = distance_to_segment(x4,y4,x1,y1,x,y)
|
||||||
local xc = d41 / (d23+d41)
|
local xc = d41 / (d23+d41)
|
||||||
|
|
||||||
|
-- Same for the 2 other edges, they give the Y coordinate
|
||||||
local d12 = distance_to_segment(x1,y1,x2,y2,x,y)
|
local d12 = distance_to_segment(x1,y1,x2,y2,x,y)
|
||||||
local d34 = distance_to_segment(x3,y3,x4,y4,x,y)
|
local d34 = distance_to_segment(x3,y3,x4,y4,x,y)
|
||||||
local yc = d12 / (d12+d34)
|
local yc = d12 / (d12+d34)
|
||||||
|
6
init.lua
6
init.lua
@ -12,6 +12,7 @@ local make_polygons = dofile(modpath .. 'polygons.lua')
|
|||||||
|
|
||||||
local transform_quadri = dofile(modpath .. 'geometry.lua')
|
local transform_quadri = dofile(modpath .. 'geometry.lua')
|
||||||
|
|
||||||
|
-- Linear interpolation
|
||||||
local function interp(v00, v01, v11, v10, xf, zf)
|
local function interp(v00, v01, v11, v10, xf, zf)
|
||||||
local v0 = v01*xf + v00*(1-xf)
|
local v0 = v01*xf + v00*(1-xf)
|
||||||
local v1 = v11*xf + v10*(1-xf)
|
local v1 = v11*xf + v10*(1-xf)
|
||||||
@ -44,6 +45,7 @@ local function generate(minp, maxp, seed)
|
|||||||
local xf, zf = transform_quadri(poly.x, poly.z, x/blocksize, z/blocksize)
|
local xf, zf = transform_quadri(poly.x, poly.z, x/blocksize, z/blocksize)
|
||||||
local i00, i01, i11, i10 = unpack(poly.i)
|
local i00, i01, i11, i10 = unpack(poly.i)
|
||||||
|
|
||||||
|
-- Test the 4 edges to see whether we are in a river or not
|
||||||
local is_river = false
|
local is_river = false
|
||||||
local depth_factor = 0
|
local depth_factor = 0
|
||||||
local r_west, r_north, r_east, r_south = unpack(poly.rivers)
|
local r_west, r_north, r_east, r_south = unpack(poly.rivers)
|
||||||
@ -66,7 +68,7 @@ local function generate(minp, maxp, seed)
|
|||||||
zf = 0
|
zf = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
if not is_river then
|
if not is_river then -- Test corners also
|
||||||
local c_NW, c_NE, c_SE, c_SW = unpack(poly.river_corners)
|
local c_NW, c_NE, c_SE, c_SW = unpack(poly.river_corners)
|
||||||
if xf+zf < c_NW then
|
if xf+zf < c_NW then
|
||||||
is_river = true
|
is_river = true
|
||||||
@ -87,7 +89,7 @@ local function generate(minp, maxp, seed)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if not is_river then
|
if not is_river then -- Redefine indicesto have 0/1 on the riverbanks (avoids ugly edge cuts, at least for small rivers)
|
||||||
xf = (xf-r_west) / (r_east-r_west)
|
xf = (xf-r_west) / (r_east-r_west)
|
||||||
zf = (zf-r_north) / (r_south-r_north)
|
zf = (zf-r_north) / (r_south-r_north)
|
||||||
end
|
end
|
||||||
|
22
polygons.lua
22
polygons.lua
@ -44,6 +44,7 @@ for k, v in ipairs(offset_z) do
|
|||||||
offset_z[k] = (v+0.5)/256
|
offset_z[k] = (v+0.5)/256
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- To index a flat array representing a 2D map
|
||||||
local function index(x, z)
|
local function index(x, z)
|
||||||
return z*X+x+1
|
return z*X+x+1
|
||||||
end
|
end
|
||||||
@ -66,26 +67,33 @@ local function river_width(flow)
|
|||||||
return math.min(wfactor * flow ^ wpower, 1)
|
return math.min(wfactor * flow ^ wpower, 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- On map generation, determine into which polygon every point (in 2D) will fall.
|
||||||
|
-- Also store polygon-specific data
|
||||||
local function make_polygons(minp, maxp)
|
local function make_polygons(minp, maxp)
|
||||||
local chulens = maxp.z - minp.z + 1
|
local chulens = maxp.z - minp.z + 1
|
||||||
|
|
||||||
local polygons = {}
|
local polygons = {}
|
||||||
|
-- Determine the minimum and maximum coordinates of the polygons that could be on the chunk, knowing that they have an average size of 'blocksize' and a maximal offset of 0.5 blocksize.
|
||||||
local xpmin, xpmax = math.max(math.floor(minp.x/blocksize - 0.5), 0), math.min(math.ceil(maxp.x/blocksize), X-2)
|
local xpmin, xpmax = math.max(math.floor(minp.x/blocksize - 0.5), 0), math.min(math.ceil(maxp.x/blocksize), X-2)
|
||||||
local zpmin, zpmax = math.max(math.floor(minp.z/blocksize - 0.5), 0), math.min(math.ceil(maxp.z/blocksize), Z-2)
|
local zpmin, zpmax = math.max(math.floor(minp.z/blocksize - 0.5), 0), math.min(math.ceil(maxp.z/blocksize), Z-2)
|
||||||
|
|
||||||
|
-- Iterate over the polygons
|
||||||
for xp = xpmin, xpmax do
|
for xp = xpmin, xpmax do
|
||||||
for zp=zpmin, zpmax do
|
for zp=zpmin, zpmax do
|
||||||
local iA = index(xp, zp)
|
local iA = index(xp, zp)
|
||||||
local iB = index(xp+1, zp)
|
local iB = index(xp+1, zp)
|
||||||
local iC = index(xp+1, zp+1)
|
local iC = index(xp+1, zp+1)
|
||||||
local iD = index(xp, zp+1)
|
local iD = index(xp, zp+1)
|
||||||
|
-- Extract the vertices of the polygon
|
||||||
local poly_x = {offset_x[iA]+xp, offset_x[iB]+xp+1, offset_x[iC]+xp+1, offset_x[iD]+xp}
|
local poly_x = {offset_x[iA]+xp, offset_x[iB]+xp+1, offset_x[iC]+xp+1, offset_x[iD]+xp}
|
||||||
local poly_z = {offset_z[iA]+zp, offset_z[iB]+zp, offset_z[iC]+zp+1, offset_z[iD]+zp+1}
|
local poly_z = {offset_z[iA]+zp, offset_z[iB]+zp, offset_z[iC]+zp+1, offset_z[iD]+zp+1}
|
||||||
local polygon = {x=poly_x, z=poly_z, i={iA, iB, iC, iD}}
|
local polygon = {x=poly_x, z=poly_z, i={iA, iB, iC, iD}}
|
||||||
|
|
||||||
local bounds = {}
|
local bounds = {} -- Will be a list of the intercepts of polygon edges for every X position (scanline algorithm)
|
||||||
|
-- Calculate the min and max X positions
|
||||||
local xmin = math.max(math.floor(blocksize*math.min(unpack(poly_x)))+1, minp.x)
|
local xmin = math.max(math.floor(blocksize*math.min(unpack(poly_x)))+1, minp.x)
|
||||||
local xmax = math.min(math.floor(blocksize*math.max(unpack(poly_x))), maxp.x)
|
local xmax = math.min(math.floor(blocksize*math.max(unpack(poly_x))), maxp.x)
|
||||||
|
-- And initialize the arrays
|
||||||
for x=xmin, xmax do
|
for x=xmin, xmax do
|
||||||
bounds[x] = {}
|
bounds[x] = {}
|
||||||
end
|
end
|
||||||
@ -93,27 +101,33 @@ local function make_polygons(minp, maxp)
|
|||||||
local i1 = 4
|
local i1 = 4
|
||||||
for i2=1, 4 do -- Loop on 4 edges
|
for i2=1, 4 do -- Loop on 4 edges
|
||||||
local x1, x2 = poly_x[i1], poly_x[i2]
|
local x1, x2 = poly_x[i1], poly_x[i2]
|
||||||
|
-- Calculate the integer X positions over which this edge spans
|
||||||
local lxmin = math.floor(blocksize*math.min(x1, x2))+1
|
local lxmin = math.floor(blocksize*math.min(x1, x2))+1
|
||||||
local lxmax = math.floor(blocksize*math.max(x1, x2))
|
local lxmax = math.floor(blocksize*math.max(x1, x2))
|
||||||
if lxmin <= lxmax then
|
if lxmin <= lxmax then -- If there is at least one position in it
|
||||||
local z1, z2 = poly_z[i1], poly_z[i2]
|
local z1, z2 = poly_z[i1], poly_z[i2]
|
||||||
|
-- Calculate coefficient of the equation defining the edge: Z=aX+b
|
||||||
local a = (z1-z2) / (x1-x2)
|
local a = (z1-z2) / (x1-x2)
|
||||||
local b = blocksize*(z1 - a*x1)
|
local b = blocksize*(z1 - a*x1)
|
||||||
for x=math.max(lxmin, minp.x), math.min(lxmax, maxp.x) do
|
for x=math.max(lxmin, minp.x), math.min(lxmax, maxp.x) do
|
||||||
|
-- For every X position involved, add the intercepted Z position in the table
|
||||||
table.insert(bounds[x], a*x+b)
|
table.insert(bounds[x], a*x+b)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
i1 = i2
|
i1 = i2
|
||||||
end
|
end
|
||||||
for x=xmin, xmax do
|
for x=xmin, xmax do
|
||||||
|
-- Now sort the bounds list
|
||||||
local xlist = bounds[x]
|
local xlist = bounds[x]
|
||||||
table.sort(xlist)
|
table.sort(xlist)
|
||||||
local c = math.floor(#xlist/2)
|
local c = math.floor(#xlist/2)
|
||||||
for l=1, c do
|
for l=1, c do
|
||||||
|
-- Take pairs of Z coordinates: all positions between them belong to the polygon.
|
||||||
local zmin = math.max(math.floor(xlist[l*2-1])+1, minp.z)
|
local zmin = math.max(math.floor(xlist[l*2-1])+1, minp.z)
|
||||||
local zmax = math.min(math.floor(xlist[l*2]), maxp.z)
|
local zmax = math.min(math.floor(xlist[l*2]), maxp.z)
|
||||||
local i = (x-minp.x) * chulens + (zmin-minp.z) + 1
|
local i = (x-minp.x) * chulens + (zmin-minp.z) + 1
|
||||||
for z=zmin, zmax do
|
for z=zmin, zmax do
|
||||||
|
-- Fill the map at these places
|
||||||
polygons[i] = polygon
|
polygons[i] = polygon
|
||||||
i = i + 1
|
i = i + 1
|
||||||
end
|
end
|
||||||
@ -123,10 +137,13 @@ local function make_polygons(minp, maxp)
|
|||||||
polygon.dem = {dem[iA], dem[iB], dem[iC], dem[iD]}
|
polygon.dem = {dem[iA], dem[iB], dem[iC], dem[iD]}
|
||||||
polygon.lake = math.min(lakes[iA], lakes[iB], lakes[iC], lakes[iD])
|
polygon.lake = math.min(lakes[iA], lakes[iB], lakes[iC], lakes[iD])
|
||||||
|
|
||||||
|
-- Now, rivers.
|
||||||
|
-- Start by finding the river width (if any) for the polygon's 4 edges.
|
||||||
local river_west = river_width(bounds_z[iA])
|
local river_west = river_width(bounds_z[iA])
|
||||||
local river_north = river_width(bounds_x[iA-zp])
|
local river_north = river_width(bounds_x[iA-zp])
|
||||||
local river_east = 1-river_width(bounds_z[iB])
|
local river_east = 1-river_width(bounds_z[iB])
|
||||||
local river_south = 1-river_width(bounds_x[iD-zp-1])
|
local river_south = 1-river_width(bounds_x[iD-zp-1])
|
||||||
|
-- Only if opposite rivers overlap (should be rare)
|
||||||
if river_west > river_east then
|
if river_west > river_east then
|
||||||
local mean = (river_west + river_east) / 2
|
local mean = (river_west + river_east) / 2
|
||||||
river_west = mean
|
river_west = mean
|
||||||
@ -139,6 +156,7 @@ local function make_polygons(minp, maxp)
|
|||||||
end
|
end
|
||||||
polygon.rivers = {river_west, river_north, river_east, river_south}
|
polygon.rivers = {river_west, river_north, river_east, river_south}
|
||||||
|
|
||||||
|
-- Look for river corners
|
||||||
local around = {0,0,0,0,0,0,0,0}
|
local around = {0,0,0,0,0,0,0,0}
|
||||||
if zp > 0 then
|
if zp > 0 then
|
||||||
around[1] = river_width(bounds_z[iA-X])
|
around[1] = river_width(bounds_z[iA-X])
|
||||||
|
@ -19,9 +19,14 @@ neighbours_dirs = np.array([
|
|||||||
neighbours_pattern = neighbours_dirs > 0
|
neighbours_pattern = neighbours_dirs > 0
|
||||||
|
|
||||||
def flow_dirs_lakes(dem, random=0):
|
def flow_dirs_lakes(dem, random=0):
|
||||||
|
"""
|
||||||
|
Calculates flow direction in D4 (4 choices) for every pixel of the DEM
|
||||||
|
Also returns an array of lake elevation
|
||||||
|
"""
|
||||||
|
|
||||||
(Y, X) = dem.shape
|
(Y, X) = dem.shape
|
||||||
|
|
||||||
dem_margin = np.zeros((Y+2, X+2))
|
dem_margin = np.zeros((Y+2, X+2)) # We need a margin of one pixel at every edge, to prevent crashes when scanning the neighbour pixels on the borders
|
||||||
dem_margin[1:-1,1:-1] = dem
|
dem_margin[1:-1,1:-1] = dem
|
||||||
if random > 0:
|
if random > 0:
|
||||||
dem_margin += np.random.random(dem_margin.shape) * random
|
dem_margin += np.random.random(dem_margin.shape) * random
|
||||||
@ -41,10 +46,11 @@ def flow_dirs_lakes(dem, random=0):
|
|||||||
dem_east = dem_margin[y,X]
|
dem_east = dem_margin[y,X]
|
||||||
borders.append((dem_east, dem_east, y, X))
|
borders.append((dem_east, dem_east, y, X))
|
||||||
|
|
||||||
|
# Make a binary heap
|
||||||
heapq.heapify(borders)
|
heapq.heapify(borders)
|
||||||
|
|
||||||
dirs = np.zeros((Y+2, X+2), dtype='u1')
|
dirs = np.zeros((Y+2, X+2), dtype='u1')
|
||||||
dirs[-2:,:] = 1
|
dirs[-2:,:] = 1 # Border pixels flow outside the map
|
||||||
dirs[:,-2:] = 2
|
dirs[:,-2:] = 2
|
||||||
dirs[ :2,:] = 3
|
dirs[ :2,:] = 3
|
||||||
dirs[:, :2] = 4
|
dirs[:, :2] = 4
|
||||||
@ -56,21 +62,26 @@ def flow_dirs_lakes(dem, random=0):
|
|||||||
heapq.heappush(borders, (alt, altmax, y, x))
|
heapq.heappush(borders, (alt, altmax, y, x))
|
||||||
|
|
||||||
while len(borders) > 0:
|
while len(borders) > 0:
|
||||||
(alt, altmax, y, x) = heapq.heappop(borders)
|
(alt, altmax, y, x) = heapq.heappop(borders) # Take the lowest pixel in the queue
|
||||||
neighbours = dirs[y-1:y+2, x-1:x+2]
|
neighbours = dirs[y-1:y+2, x-1:x+2]
|
||||||
empty_neighbours = (neighbours == 0) * neighbours_pattern
|
empty_neighbours = (neighbours == 0) * neighbours_pattern # Find the neighbours whose flow direction is not yet defined
|
||||||
neighbours += empty_neighbours * neighbours_dirs
|
neighbours += empty_neighbours * neighbours_dirs # They flow into the pixel being studied
|
||||||
|
|
||||||
lake = max(alt, altmax)
|
lake = max(alt, altmax) # Set lake elevation to the maximal height of the downstream section.
|
||||||
lakes[y-1,x-1] = lake
|
lakes[y-1,x-1] = lake
|
||||||
|
|
||||||
coords = np.transpose(empty_neighbours.nonzero())
|
coords = np.transpose(empty_neighbours.nonzero())
|
||||||
for (dy,dx) in coords-1:
|
for (dy,dx) in coords-1: # Add these neighbours into the queue
|
||||||
add_point(y+dy, x+dx, lake)
|
add_point(y+dy, x+dx, lake)
|
||||||
|
|
||||||
return dirs[1:-1,1:-1], lakes
|
return dirs[1:-1,1:-1], lakes
|
||||||
|
|
||||||
def accumulate(dirs, dem=None):
|
def accumulate(dirs, dem=None):
|
||||||
|
"""
|
||||||
|
Calculates the quantity of water that accumulates at every pixel,
|
||||||
|
following flow directions.
|
||||||
|
"""
|
||||||
|
|
||||||
(Y, X) = dirs.shape
|
(Y, X) = dirs.shape
|
||||||
dirs_margin = np.zeros((Y+2,X+2))-1
|
dirs_margin = np.zeros((Y+2,X+2))-1
|
||||||
dirs_margin[1:-1,1:-1] = dirs
|
dirs_margin[1:-1,1:-1] = dirs
|
||||||
@ -79,13 +90,13 @@ def accumulate(dirs, dem=None):
|
|||||||
def calculate_quantity(y, x):
|
def calculate_quantity(y, x):
|
||||||
if quantity[y,x] > 0:
|
if quantity[y,x] > 0:
|
||||||
return quantity[y,x]
|
return quantity[y,x]
|
||||||
q = 1
|
q = 1 # Consider that every pixel contains a water quantity of 1 by default.
|
||||||
neighbours = dirs_margin[y:y+3, x:x+3]
|
neighbours = dirs_margin[y:y+3, x:x+3]
|
||||||
donors = neighbours == neighbours_dirs
|
donors = neighbours == neighbours_dirs # Identify neighbours that give their water to the pixel being studied
|
||||||
|
|
||||||
coords = np.transpose(donors.nonzero())
|
coords = np.transpose(donors.nonzero())
|
||||||
for (dy,dx) in coords-1:
|
for (dy,dx) in coords-1:
|
||||||
q += calculate_quantity(y+dy, x+dx)
|
q += calculate_quantity(y+dy, x+dx) # Add water quantity of the donors pixels (this triggers calculation for these pixels, recursively)
|
||||||
quantity[y, x] = q
|
quantity[y, x] = q
|
||||||
return q
|
return q
|
||||||
|
|
||||||
@ -96,5 +107,9 @@ def accumulate(dirs, dem=None):
|
|||||||
return quantity
|
return quantity
|
||||||
|
|
||||||
def flow(dem):
|
def flow(dem):
|
||||||
|
"""
|
||||||
|
Calculates flow directions and water quantity
|
||||||
|
"""
|
||||||
|
|
||||||
dirs, lakes = flow_dirs_lakes(dem)
|
dirs, lakes = flow_dirs_lakes(dem)
|
||||||
return dirs, lakes, accumulate(dirs, dem)
|
return dirs, lakes, accumulate(dirs, dem)
|
||||||
|
@ -20,28 +20,29 @@ else:
|
|||||||
scale = (mapsize-1) / 2
|
scale = (mapsize-1) / 2
|
||||||
n = np.zeros((mapsize, mapsize))
|
n = np.zeros((mapsize, mapsize))
|
||||||
|
|
||||||
#micronoise_depth = 0.05
|
# Set noise parameters
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
"octaves" : int(np.ceil(np.log2(mapsize-1)))+1,
|
"octaves" : int(np.ceil(np.log2(mapsize-1)))+1,
|
||||||
"persistence" : 0.5,
|
"persistence" : 0.5,
|
||||||
"lacunarity" : 2.,
|
"lacunarity" : 2.,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Determine noise offset randomly
|
||||||
xbase = np.random.randint(65536)
|
xbase = np.random.randint(65536)
|
||||||
ybase = np.random.randint(65536)
|
ybase = np.random.randint(65536)
|
||||||
|
|
||||||
|
# Generate the noise
|
||||||
for x in range(mapsize):
|
for x in range(mapsize):
|
||||||
for y in range(mapsize):
|
for y in range(mapsize):
|
||||||
n[x,y] = noise.snoise2(x/scale + xbase, y/scale + ybase, **params)
|
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
|
nn = n*mapsize/5 + mapsize/20
|
||||||
|
|
||||||
|
# Initialize landscape evolution model
|
||||||
print('Initializing model')
|
print('Initializing model')
|
||||||
model = EvolutionModel(nn, K=1, m=0.35, d=1, sea_level=0)
|
model = EvolutionModel(nn, K=1, m=0.35, d=1, sea_level=0)
|
||||||
|
|
||||||
|
# Run the model's processes: the order in which the processes are run is arbitrary and could be changed.
|
||||||
print('Flow calculation 1')
|
print('Flow calculation 1')
|
||||||
model.calculate_flow()
|
model.calculate_flow()
|
||||||
|
|
||||||
@ -68,12 +69,15 @@ model.calculate_flow()
|
|||||||
|
|
||||||
print('Done')
|
print('Done')
|
||||||
|
|
||||||
|
# Twist the grid
|
||||||
bx, by = bounds.make_bounds(model.dirs, model.rivers)
|
bx, by = bounds.make_bounds(model.dirs, model.rivers)
|
||||||
ox, oy = bounds.twist(bx, by, bounds.get_fixed(model.dirs))
|
ox, oy = bounds.twist(bx, by, bounds.get_fixed(model.dirs))
|
||||||
|
|
||||||
|
# Convert offset in 8-bits
|
||||||
offset_x = np.clip(np.floor(ox * 256), -128, 127)
|
offset_x = np.clip(np.floor(ox * 256), -128, 127)
|
||||||
offset_y = np.clip(np.floor(oy * 256), -128, 127)
|
offset_y = np.clip(np.floor(oy * 256), -128, 127)
|
||||||
|
|
||||||
|
# Save the files
|
||||||
save(model.dem, 'dem', dtype='>i2')
|
save(model.dem, 'dem', dtype='>i2')
|
||||||
save(model.lakes, 'lakes', dtype='>i2')
|
save(model.lakes, 'lakes', dtype='>i2')
|
||||||
save(np.abs(bx), 'bounds_x', dtype='>i4')
|
save(np.abs(bx), 'bounds_x', dtype='>i4')
|
||||||
@ -86,6 +90,7 @@ save(model.rivers, 'rivers', dtype='>u4')
|
|||||||
with open('size', 'w') as sfile:
|
with open('size', 'w') as sfile:
|
||||||
sfile.write('{:d}\n{:d}'.format(mapsize, mapsize))
|
sfile.write('{:d}\n{:d}'.format(mapsize, mapsize))
|
||||||
|
|
||||||
|
# Display the map if matplotlib is found
|
||||||
try:
|
try:
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
@ -111,7 +116,7 @@ try:
|
|||||||
plt.pcolormesh(model.rivers, vmin=0, vmax=mapsize**2/25, cmap='Blues')
|
plt.pcolormesh(model.rivers, vmin=0, vmax=mapsize**2/25, cmap='Blues')
|
||||||
plt.gca().set_aspect('equal', 'box')
|
plt.gca().set_aspect('equal', 'box')
|
||||||
#plt.colorbar(orientation='horizontal')
|
#plt.colorbar(orientation='horizontal')
|
||||||
plt.title('Rivers discharge')
|
plt.title('Rivers flux')
|
||||||
|
|
||||||
plt.show()
|
plt.show()
|
||||||
except:
|
except:
|
||||||
|
@ -34,6 +34,6 @@ plt.subplot(1,3,3)
|
|||||||
plt.pcolormesh(np.log(rivers), vmin=0, vmax=np.log(n/25), cmap='Blues')
|
plt.pcolormesh(np.log(rivers), vmin=0, vmax=np.log(n/25), cmap='Blues')
|
||||||
plt.gca().set_aspect('equal', 'box')
|
plt.gca().set_aspect('equal', 'box')
|
||||||
#plt.colorbar(orientation='horizontal')
|
#plt.colorbar(orientation='horizontal')
|
||||||
plt.title('Rivers discharge')
|
plt.title('Rivers flux')
|
||||||
|
|
||||||
plt.show()
|
plt.show()
|
||||||
|
Loading…
Reference in New Issue
Block a user