4 Commits

7 changed files with 214 additions and 22 deletions

2
.gitignore vendored
View File

@ -4,4 +4,6 @@ lakes
links links
rivers rivers
size size
offset_x
offset_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,
}

114
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
@ -31,19 +32,37 @@ local links = load_map(worldpath..'links', 1, false)
copy_if_needed('rivers') copy_if_needed('rivers')
local rivers = load_map(worldpath..'rivers', 4, false) local rivers = load_map(worldpath..'rivers', 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 get_point_location(x, z)
local i = index(x, z)
return x+offset_x[i], z+offset_z[i]
end
local function interp(v00, v01, v10, v11, xf, zf) local function interp(v00, v01, v10, v11, xf, zf)
v0 = v01*xf + v00*(1-xf) local v0 = v01*xf + v00*(1-xf)
v1 = v11*xf + v10*(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
@ -77,25 +96,77 @@ 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 polygon_number = {}
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)
local n = 1
local n_filled = 0
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 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
polygon_number[i] = n
i = i + 1
n_filled = n_filled + 1
end
end
end
polygons[n] = {x=poly_x, z=poly_z, i={iA, iB, iC, iD}}
n = n + 1
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 npoly = polygon_number[i]
local zb = z/blocksize if npoly then
local poly = polygons[npoly]
if xb >= 0 and xb < X-1 and zb >= 0 and zb < Z-1 then local xf, zf = geometry.transform_quadri(poly.x, poly.z, x/blocksize, z/blocksize)
local x0 = math.floor(xb) if xf < 0 or xf > 1 or zf < 0 or zf > 1 then
local x1 = x0+1 print(xf, zf, x, z)
local z0 = math.floor(zb) end
local z1 = z0+1 local i00, i01, i11, i10 = unpack(poly.i)
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( local terrain_height = math.floor(interp(
dem[i00], dem[i00],
@ -115,7 +186,7 @@ local function generate(minp, maxp, seed)
local is_lake = lake_height > terrain_height local is_lake = lake_height > terrain_height
local is_river = false local is_river = false
if xf == 0 then if xf < 1/6 then
if links[i00] == 1 and rivers[i00] >= min_catchment then if links[i00] == 1 and rivers[i00] >= min_catchment then
is_river = true is_river = true
elseif links[i10] == 3 and rivers[i10] >= min_catchment then elseif links[i10] == 3 and rivers[i10] >= min_catchment then
@ -123,7 +194,7 @@ local function generate(minp, maxp, seed)
end end
end end
if zf == 0 then if zf < 1/6 then
if links[i00] == 2 and rivers[i00] >= min_catchment then if links[i00] == 2 and rivers[i00] >= min_catchment then
is_river = true is_river = true
elseif links[i01] == 4 and rivers[i01] >= min_catchment then elseif links[i01] == 4 and rivers[i01] >= min_catchment then
@ -164,6 +235,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
@ -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(model.dirs, 'links', dtype='u1')
save(model.rivers, 'rivers', dtype='>u4') save(model.rivers, 'rivers', dtype='>u4')
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))