11 Commits

Author SHA1 Message Date
1adb4fbece Added an offset of 0.5 on terrain elevation
This prevents rounding errors and improves interpolation on nearly flat areas
2020-04-13 12:27:24 +02:00
13d3e70b66 Implemented variable river width.
Also changed the river data exported by terrain_rivers.py. They will not be compatible with what's generated by older versions.
2020-04-13 12:15:10 +02:00
4b63ed371e Add more information in the polygon table 2020-04-13 10:31:38 +02:00
eba90803fe Removed useless debug print 2020-04-13 10:01:23 +02:00
34de4269ee Add directly a reference to the polygon table in the polygon list, instead of adding an index 2020-04-13 09:54:04 +02:00
4e8288afbe Added screenshot in README 2020-04-13 09:27:41 +02:00
56cebecb13 More robust and faster code for grid twisting on the Lua side.
At chunkgen init, build a list of the polygons instead of calculating them for every node.
2020-04-13 09:27:41 +02:00
b7c6f71635 Implemented grid twisting. Still many possible bugs, potentially clumsy implementation, but it seems to work. 2020-04-13 09:27:41 +02:00
6314117642 Added bounds.py: twists the grid as if the rivers were elastic bounds. Unused for now. 2020-04-13 09:27:41 +02:00
ed34dec4fa Adjust number of octaves in function of map size 2020-04-12 17:26:37 +02:00
538bfb6d6d Added script to view map, using matplotlib 2020-04-12 16:44:29 +02:00
8 changed files with 309 additions and 56 deletions

6
.gitignore vendored
View File

@ -1,7 +1,9 @@
__pycache__/ __pycache__/
dem dem
lakes lakes
links
rivers
size size
offset_x
offset_y
bounds_x
bounds_y
unused/ unused/

View File

@ -5,6 +5,8 @@ Procedural map generator for Minetest 5.x. Still experimental and basic.
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)
# 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.

62
bounds.py Normal file
View File

@ -0,0 +1,62 @@
import numpy as np
import matplotlib.pyplot as plt
def make_bounds(dirs, rivers):
(Y, X) = dirs.shape
bounds_h = np.zeros((Y, X-1), dtype='i4')
bounds_v = np.zeros((Y-1, X), dtype='i4')
bounds_v += (rivers * (dirs==1))[:-1,:]
bounds_h += (rivers * (dirs==2))[:,:-1]
bounds_v -= (rivers * (dirs==3))[1:,:]
bounds_h -= (rivers * (dirs==4))[:,1:]
return bounds_h, bounds_v
def get_fixed(dirs):
borders = np.zeros(dirs.shape, dtype='?')
borders[-1,:] |= dirs[-1,:]==1
borders[:,-1] |= dirs[:,-1]==2
borders[0,:] |= dirs[0,:]==3
borders[:,0] |= dirs[:,0]==4
donors = np.zeros(dirs.shape, dtype='?')
donors[1:,:] |= dirs[:-1,:]==1
donors[:,1:] |= dirs[:,:-1]==2
donors[:-1,:] |= dirs[1:,:]==3
donors[:,:-1] |= dirs[:,1:]==4
return borders | ~donors
def twist(bounds_x, bounds_y, fixed, d=0.1, n=5):
moveable = ~fixed
(Y, X) = fixed.shape
offset_x = np.zeros((Y, X))
offset_y = np.zeros((Y, X))
for i in range(n):
force_long = np.abs(bounds_x) * (1+np.diff(offset_x, axis=1))
force_trans = np.abs(bounds_y) * np.diff(offset_x, axis=0)
force_x = np.zeros((Y, X))
force_x[:,:-1] = force_long
force_x[:,1:] -= force_long
force_x[:-1,:]+= force_trans
force_x[1:,:] -= force_trans
force_long = np.abs(bounds_y) * (1+np.diff(offset_y, axis=0))
force_trans = np.abs(bounds_x) * np.diff(offset_y, axis=1)
force_y = np.zeros((Y, X))
force_y[:-1,:] = force_long
force_y[1:,:] -= force_long
force_y[:,:-1]+= force_trans
force_y[:,1:] -= force_trans
length = np.hypot(force_x, force_y)
length[length==0] = 1
coeff = d / length * moveable
offset_x += force_x * coeff
offset_y += force_y * coeff
return offset_x, offset_y

45
geometry.lua Normal file
View File

@ -0,0 +1,45 @@
local function distance_to_segment(x1, y1, x2, y2, x, y)
-- get the distance between point (x,y) and segment (x1,y1)-(x2,y2)
local a = (x1-x2)^2 + (y1-y2)^2
local b = (x1-x)^2 + (y1-y)^2
local c = (x2-x)^2 + (y2-y)^2
if a + b < c then
return math.sqrt(b)
elseif a + c < b then
return math.sqrt(c)
else
return math.abs(x1 * (y2-y) + x2 * (y-y1) + x * (y1-y2)) / math.sqrt(a)
end
end
local function transform_quadri(X, Y, x, y)
-- X, Y 4-vectors giving the coordinates of the 4 nodes
-- x, y position to index.
local x1, x2, x3, x4 = unpack(X)
local y1, y2, y3, y4 = unpack(Y)
local d23 = distance_to_segment(x2,y2,x3,y3,x,y)
local d41 = distance_to_segment(x4,y4,x1,y1,x,y)
local xc = d41 / (d23+d41)
local d12 = distance_to_segment(x1,y1,x2,y2,x,y)
local d34 = distance_to_segment(x3,y3,x4,y4,x,y)
local yc = d12 / (d12+d34)
return xc, yc
end
local function area(X, Y) -- Signed area of polygon, in function of direction of rotation. Clockwise = positive.
local n = #X
local sum = X[1]*Y[n] - X[n]*Y[1]
for i=2, n do
sum = sum + X[i]*Y[i-1] - X[i-1]*Y[i]
end
return sum/2
end
return {
distance_to_segment = distance_to_segment,
transform_quadri = transform_quadri,
area = area,
}

203
init.lua
View File

@ -1,6 +1,7 @@
local modpath = minetest.get_modpath(minetest.get_current_modname()) .. '/' local modpath = minetest.get_modpath(minetest.get_current_modname()) .. '/'
local worldpath = minetest.get_worldpath() .. '/' local worldpath = minetest.get_worldpath() .. '/'
local load_map = dofile(modpath .. 'load.lua') local load_map = dofile(modpath .. 'load.lua')
local geometry = dofile(modpath .. 'geometry.lua')
local function copy_if_needed(filename) local function copy_if_needed(filename)
local wfilename = worldpath..filename local wfilename = worldpath..filename
@ -26,26 +27,45 @@ copy_if_needed('dem')
local dem = load_map(worldpath..'dem', 2, true) local dem = load_map(worldpath..'dem', 2, true)
copy_if_needed('lakes') copy_if_needed('lakes')
local lakes = load_map(worldpath..'lakes', 2, true) local lakes = load_map(worldpath..'lakes', 2, true)
copy_if_needed('links') copy_if_needed('bounds_x')
local links = load_map(worldpath..'links', 1, false) local bounds_x = load_map(worldpath..'bounds_x', 4, false)
copy_if_needed('rivers') copy_if_needed('bounds_y')
local rivers = load_map(worldpath..'rivers', 4, false) local bounds_z = load_map(worldpath..'bounds_y', 4, false)
copy_if_needed('offset_x')
local offset_x = load_map(worldpath..'offset_x', 1, true)
for k, v in ipairs(offset_x) do
offset_x[k] = (v+0.5)/256
end
copy_if_needed('offset_y')
local offset_z = load_map(worldpath..'offset_y', 1, true)
for k, v in ipairs(offset_z) do
offset_z[k] = (v+0.5)/256
end
local function index(x, z) local function index(x, z)
return z*X+x+1 return z*X+x+1
end end
local function interp(v00, v01, v10, v11, xf, zf) local function get_point_location(x, z)
v0 = v01*xf + v00*(1-xf) local i = index(x, z)
v1 = v11*xf + v10*(1-xf) return x+offset_x[i], z+offset_z[i]
end
local function interp(v00, v01, v11, v10, xf, zf)
local v0 = v01*xf + v00*(1-xf)
local v1 = v11*xf + v10*(1-xf)
return v1*zf + v0*(1-zf) return v1*zf + v0*(1-zf)
end end
local data = {} local data = {}
local blocksize = 6 local blocksize = 12
local sea_level = 1 local sea_level = 1
local min_catchment = 25 local min_catchment = 25
local max_catchment = 40000
local storage = minetest.get_mod_storage() local storage = minetest.get_mod_storage()
if storage:contains("blocksize") then if storage:contains("blocksize") then
@ -63,6 +83,25 @@ if storage:contains("min_catchment") then
else else
storage:set_float("min_catchment", min_catchment) storage:set_float("min_catchment", min_catchment)
end end
if storage:contains("max_catchment") then
max_catchment = storage:get_float("max_catchment")
else
storage:set_float("max_catchment", max_catchment)
end
-- Width coefficients: coefficients solving
-- wfactor * min_catchment ^ wpower = 1/(2*blocksize)
-- wfactor * max_catchment ^ wpower = 1
local wpower = math.log(2*blocksize)/math.log(max_catchment/min_catchment)
local wfactor = 1 / max_catchment ^ wpower
local function river_width(flow)
flow = math.abs(flow)
if flow < min_catchment then
return 0
end
return math.min(wfactor * flow ^ wpower, 1)
end
local function generate(minp, maxp, seed) local function generate(minp, maxp, seed)
local c_stone = minetest.get_content_id("default:stone") local c_stone = minetest.get_content_id("default:stone")
@ -77,59 +116,122 @@ local function generate(minp, maxp, seed)
local a = VoxelArea:new({MinEdge = emin, MaxEdge = emax}) 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. 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.
local chulens = maxp.z - minp.z + 1
local polygons = {}
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)
for xp = xpmin, xpmax do
for zp=zpmin, zpmax do
local iA = index(xp, zp)
local iB = index(xp+1, zp)
local iC = index(xp+1, zp+1)
local iD = index(xp, zp+1)
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 polygon = {x=poly_x, z=poly_z, i={iA, iB, iC, iD}}
local bounds = {}
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)
for x=xmin, xmax do
bounds[x] = {}
end
local i1 = 4
for i2=1, 4 do -- Loop on 4 edges
local x1, x2 = poly_x[i1], poly_x[i2]
local lxmin = math.floor(blocksize*math.min(x1, x2))+1
local lxmax = math.floor(blocksize*math.max(x1, x2))
if lxmin <= lxmax then
local z1, z2 = poly_z[i1], poly_z[i2]
local a = (z1-z2) / (x1-x2)
local b = blocksize*(z1 - a*x1)
for x=math.max(lxmin, minp.x), math.min(lxmax, maxp.x) do
table.insert(bounds[x], a*x+b)
end
end
i1 = i2
end
for x=xmin, xmax do
local xlist = bounds[x]
table.sort(xlist)
local c = math.floor(#xlist/2)
for l=1, c do
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 i = (x-minp.x) * chulens + (zmin-minp.z) + 1
for z=zmin, zmax do
polygons[i] = polygon
i = i + 1
end
end
end
polygon.dem = {dem[iA], dem[iB], dem[iC], dem[iD]}
polygon.lake = math.min(lakes[iA], lakes[iB], lakes[iC], lakes[iD])
local river_west = river_width(bounds_z[iA])
local river_north = river_width(bounds_x[iA-zp])
local river_east = 1-river_width(bounds_z[iB])
local river_south = 1-river_width(bounds_x[iD-zp-1])
if river_west > river_east then
local mean = (river_west + river_east) / 2
river_west = mean
river_east = mean
end
if river_north > river_south then
local mean = (river_north + river_south) / 2
river_north = mean
river_south = mean
end
polygon.rivers = {river_west, river_north, river_east, river_south}
end
end
local i = 1
for x = minp.x, maxp.x do for x = minp.x, maxp.x do
for z = minp.z, maxp.z do for z = minp.z, maxp.z do
local xb = x/blocksize local poly = polygons[i]
local zb = z/blocksize if poly then
local xf, zf = geometry.transform_quadri(poly.x, poly.z, x/blocksize, z/blocksize)
local i00, i01, i11, i10 = unpack(poly.i)
if xb >= 0 and xb < X-1 and zb >= 0 and zb < Z-1 then local is_river = false
local x0 = math.floor(xb) local r_west, r_north, r_east, r_south = unpack(poly.rivers)
local x1 = x0+1 if xf >= r_east then
local z0 = math.floor(zb) is_river = true
local z1 = z0+1 xf = 1
elseif xf <= r_west then
is_river = true
xf = 0
end
if zf >= r_south then
is_river = true
zf = 1
elseif zf <= r_north then
is_river = true
zf = 0
end
local xf = xb - x0 if not is_river then
local zf = zb - z0 xf = (xf-r_west) / (r_east-r_west)
zf = (zf-r_north) / (r_south-r_north)
end
local i00 = index(x0,z0) local vdem = poly.dem
local i01 = index(x1,z0) local terrain_height = math.floor(0.5+interp(
local i10 = index(x0,z1) vdem[1],
local i11 = index(x1,z1) vdem[2],
vdem[3],
local terrain_height = math.floor(interp( vdem[4],
dem[i00],
dem[i01],
dem[i10],
dem[i11],
xf, zf xf, zf
)) ))
local lake_height = math.floor(math.min( local lake_height = math.floor(poly.lake)
lakes[i00],
lakes[i01],
lakes[i10],
lakes[i11]
))
local is_lake = lake_height > terrain_height 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) local ivm = a:index(x, minp.y-1, z)
@ -164,6 +266,7 @@ local function generate(minp, maxp, seed)
end end
end end
end end
i = i + 1
end end
end end

View File

@ -18,7 +18,7 @@ neighbours_dirs = np.array([
neighbours_pattern = neighbours_dirs > 0 neighbours_pattern = neighbours_dirs > 0
def flow_dirs_lakes(dem, random=0.0625): def flow_dirs_lakes(dem, random=0):
(Y, X) = dem.shape (Y, X) = dem.shape
dem_margin = np.zeros((Y+2, X+2)) dem_margin = np.zeros((Y+2, X+2))

View File

@ -4,6 +4,7 @@ import numpy as np
import noise import noise
from save import save from save import save
from erosion import EvolutionModel from erosion import EvolutionModel
import bounds
import os import os
import sys import sys
@ -22,7 +23,7 @@ n = np.zeros((mapsize, mapsize))
#micronoise_depth = 0.05 #micronoise_depth = 0.05
params = { params = {
"octaves" : 8, "octaves" : int(np.log2(mapsize)),
"persistence" : 0.5, "persistence" : 0.5,
"lacunarity" : 2., "lacunarity" : 2.,
} }
@ -67,10 +68,18 @@ model.calculate_flow()
print('Done') print('Done')
bx, by = bounds.make_bounds(model.dirs, model.rivers)
ox, oy = bounds.twist(bx, by, bounds.get_fixed(model.dirs))
offset_x = np.clip(np.floor(ox * 256), -128, 127)
offset_y = np.clip(np.floor(oy * 256), -128, 127)
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(model.dirs, 'links', dtype='u1') save(np.abs(bx), 'bounds_x', dtype='>i4')
save(model.rivers, 'rivers', dtype='>u4') save(np.abs(by), 'bounds_y', dtype='>i4')
save(offset_x, 'offset_x', dtype='i1')
save(offset_y, 'offset_y', dtype='i1')
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))

30
view_map.py Executable file
View File

@ -0,0 +1,30 @@
#!/usr/bin/env python3
import numpy as np
import matplotlib.pyplot as plt
shape = np.loadtxt('size', dtype='u4')
n = shape[0] * shape[1]
dem = np.fromfile('dem', dtype='>i2').reshape(shape)
lakes = np.fromfile('lakes', dtype='>i2').reshape(shape)
rivers = np.fromfile('rivers', dtype='>u4').reshape(shape)
plt.subplot(1,3,1)
plt.pcolormesh(dem, cmap='viridis')
plt.gca().set_aspect('equal', 'box')
#plt.colorbar(orientation='horizontal')
plt.title('Raw elevation')
plt.subplot(1,3,2)
plt.pcolormesh(lakes, cmap='viridis')
plt.gca().set_aspect('equal', 'box')
#plt.colorbar(orientation='horizontal')
plt.title('Lake surface elevation')
plt.subplot(1,3,3)
plt.pcolormesh(np.log(rivers), vmin=0, vmax=np.log(n/25), cmap='Blues')
plt.gca().set_aspect('equal', 'box')
#plt.colorbar(orientation='horizontal')
plt.title('Rivers discharge')
plt.show()