mapgen_rivers/terrainlib/rivermapper.py
Gael-de-Sailly f0dddee33c Lakes map: keep initial height (reduces file size)
Lake height is calculated for every basin, and there is a lake if lake height is higher than ground height. If it is lower, there is no lake.
In that case, it was previously raised to ground level, but since this can be done in Lua, we can write initial lakes height in the files.
This has the advantage of reducing file size, since there are bigger areas of equal values, that are more efficiently compressed.
2020-12-24 14:48:17 +01:00

248 lines
6.7 KiB
Python

import numpy as np
import numpy.random as npr
from collections import defaultdict
def flow_local(plist):
"""
Determines a flow direction based on denivellation for every neighbouring node.
Denivellation must be positive for downward and zero for flat or upward:
dz = max(zref-z, 0)
"""
psum = sum(plist)
if psum == 0:
return 0
r = npr.random() * psum
for i, p in enumerate(plist):
if r < p:
return i+1
r -= p
def flow(dem):
# Flow locally
dirs1 = np.zeros(dem.shape, dtype=int)
dirs2 = np.zeros(dem.shape, dtype=int)
(X, Y) = dem.shape
Xmax, Ymax = X-1, Y-1
singular = []
for x in range(X):
z0 = z1 = z2 = dem[x,0]
for y in range(Y):
z0 = z1
z1 = z2
if y < Ymax:
z2 = dem[x, y+1]
plist = [
max(z1-dem[x+1,y],0) if x<Xmax else 0, # 1: x -> x+1
max(z1-z2,0), # 2: y -> y+1
max(z1-dem[x-1,y],0) if x>0 else 0, # 3: x -> x-1
max(z1-z0,0), # 4: y -> y-1
]
pdir = flow_local(plist)
dirs2[x,y] = pdir
if pdir == 0:
singular.append((x,y))
elif pdir == 1:
dirs1[x+1,y] += 1
elif pdir == 2:
dirs1[x,y+1] += 2
elif pdir == 3:
dirs1[x-1,y] += 4
elif pdir == 4:
dirs1[x,y-1] += 8
# Compute basins
basin_id = np.zeros(dem.shape, dtype=int)
stack = []
for i, s in enumerate(singular):
queue = [s]
while queue:
x, y = queue.pop()
basin_id[x,y] = i
d = int(dirs1[x,y])
if d & 1:
queue.append((x-1,y))
if d & 2:
queue.append((x,y-1))
if d & 4:
queue.append((x+1,y))
if d & 8:
queue.append((x,y+1))
del dirs1
# Link basins
nsing = len(singular)
links = {}
def add_link(b0, b1, elev, bound):
b = (min(b0,b1),max(b0,b1))
if b not in links or links[b][0] > elev:
links[b] = (elev, bound)
for x in range(X):
b0 = basin_id[x,0]
add_link(-1, b0, dem[x,0], (True, x, 0))
for y in range(1,Y):
b1 = basin_id[x,y]
if b0 != b1:
add_link(b0, b1, max(dem[x,y-1],dem[x,y]), (True, x, y))
b0 = b1
add_link(-1, b1, dem[x,Ymax], (True, x, Y))
for y in range(Y):
b0 = basin_id[0,y]
add_link(-1, b0, dem[0,y], (False, 0, y))
for x in range(1,X):
b1 = basin_id[x,y]
if b0 != b1:
add_link(b0, b1, max(dem[x-1,y],dem[x,y]), (False, x, y))
b0 = b1
add_link(-1, b1, dem[Xmax,y], (False, X, y))
# Computing basin tree
graph = planar_boruvka(links)
basin_links = defaultdict(dict)
for elev, b1, b2, bound in graph:
basin_links[b1][b2] = basin_links[b2][b1] = (elev, bound)
basins = np.zeros(nsing+1)
stack = [(-1, float('-inf'))]
# Applying basin flowing
dir_reverse = (0, 3, 4, 1, 2)
while stack:
b1, elev1 = stack.pop()
basins[b1] = elev1
for b2, (elev2, bound) in basin_links[b1].items():
stack.append((b2, max(elev1, elev2)))
# Reverse flow direction in b2 (TODO)
isY, x, y = bound
backward = True # Whether water will escape the basin in +X/+Y direction
if not (x < X and y < Y and basin_id[x,y] == b2):
if isY:
y -= 1
else:
x -= 1
backward = False
d = 2*backward + isY + 1
while d > 0:
d, dirs2[x,y] = dirs2[x,y], d
if d == 1:
x += 1
elif d == 2:
y += 1
elif d == 3:
x -= 1
elif d == 4:
y -= 1
d = dir_reverse[d]
del basin_links[b2][b1]
del basin_links[b1]
# Calculating water quantity
dirs2[-1,:][dirs2[-1,:]==1] = 0
dirs2[:,-1][dirs2[:,-1]==2] = 0
dirs2[0,:][dirs2[0,:]==3] = 0
dirs2[:,0][dirs2[:,0]==4] = 0
waterq = accumulate_flow(dirs2)
return dirs2, basins[basin_id], waterq
def accumulate_flow(dirs):
ndonors = np.zeros(dirs.shape, dtype=int)
ndonors[1:,:] += dirs[:-1,:] == 1
ndonors[:,1:] += dirs[:,:-1] == 2
ndonors[:-1,:] += dirs[1:,:] == 3
ndonors[:,:-1] += dirs[:,1:] == 4
waterq = np.ones(dirs.shape, dtype=int)
(X, Y) = dirs.shape
rangeX = range(X)
rangeY = range(Y)
for x in rangeX:
for y in rangeY:
if ndonors[x,y] > 0:
continue
xw, yw = x, y
w = waterq[xw, yw]
while 1:
d = dirs[xw, yw]
if d <= 0:
break
elif d == 1:
xw += 1
elif d == 2:
yw += 1
elif d == 3:
xw -= 1
elif d == 4:
yw -= 1
w += waterq[xw, yw]
waterq[xw, yw] = w
if ndonors[xw, yw] > 1:
ndonors[xw, yw] -= 1
break
return waterq
def planar_boruvka(links):
# Compute basin tree
basin_list = defaultdict(dict)
for (b1, b2), (elev, bound) in links.items():
basin_list[b1][b2] = basin_list[b2][b1] = (elev, b1, b2, bound)
threshold = 8
lowlevel = {}
for k, v in basin_list.items():
if len(v) <= threshold:
lowlevel[k] = v
basin_graph = []
n = len(basin_list)
while n > 1:
(b1, lnk1) = lowlevel.popitem()
b2 = min(lnk1, key=lnk1.get)
lnk2 = basin_list[b2]
# Add link to the graph
basin_graph.append(lnk1[b2])
# Union : merge basin 1 into basin 2
# First, delete the direct link
del lnk1[b2]
del lnk2[b1]
# Look for basin 1's neighbours, and add them to basin 2 if they have a lower pass
for k, v in lnk1.items():
bk = basin_list[k]
if k in lnk2 and lnk2[k] < v:
del bk[b1]
else:
lnk2[k] = v
bk[b2] = bk.pop(b1)
if k not in lowlevel and len(bk) <= threshold:
lowlevel[k] = bk
if b2 in lowlevel:
if len(lnk2) > threshold:
del lowlevel[b2]
elif len(lnk2) <= threshold:
lowlevel[b2] = lnk2
del lnk1
n -= 1
return basin_graph