11 Commits

11 changed files with 173 additions and 65 deletions

View File

@ -1,14 +1,20 @@
# Map Generator with Rivers # Map Generator with Rivers
`mapgen_rivers v1.0` by Gaël de Sailly. `mapgen_rivers v1.0.1` by Gaël de Sailly.
Semi-procedural map generator for Minetest 5.x. It aims to create realistic and nice-looking landscapes for the game, focused on river networks. It is based on algorithms modelling water flow and river erosion at a broad scale, similar to some used by researchers in Earth Sciences. It is taking some inspiration from [Fastscape](https://github.com/fastscape-lem/fastscape). Semi-procedural map generator for Minetest 5.x. It aims to create realistic and nice-looking landscapes for the game, focused on river networks. It is based on algorithms modelling water flow and river erosion at a broad scale, similar to some used by researchers in Earth Sciences. It is taking some inspiration from [Fastscape](https://github.com/fastscape-lem/fastscape).
Its main particularity compared to conventional Minetest mapgens is that rivers that flow strictly downhill, and combine together to form wider rivers, until they reach the sea. Another notable feature is the possibility of large lakes above sea level. Its main particularity compared to conventional Minetest mapgens is that rivers that flow strictly downhill, and combine together to form wider rivers, until they reach the sea. Another notable feature is the possibility of large lakes above sea level.
![Screenshot](https://user-images.githubusercontent.com/6905002/98825953-6289d980-2435-11eb-9e0b-704a95663ce0.png) ![Screenshot](https://content.minetest.net/uploads/fff09f2269.png)
It used to be composed of a Python script doing pre-generation, and a Lua mod reading the pre-generation output and generating the map. The code has been rewritten in full Lua for version 1.0 (July 2021), and is now usable out-of-the-box as any other Minetest mod. It used to be composed of a Python script doing pre-generation, and a Lua mod reading the pre-generation output and generating the map. The code has been rewritten in full Lua for version 1.0 (July 2021), and is now usable out-of-the-box as any other Minetest mod.
# Author and license
License: GNU LGPLv3.0
Code: Gaël de Sailly
Flow routing algorithm concept (in `terrainlib/rivermapper.lua`): Cordonnier, G., Bovy, B., & Braun, J. (2019). A versatile, linear complexity algorithm for flow routing in topographies with depressions. Earth Surface Dynamics, 7(2), 549-562.
# Requirements # Requirements
Mod dependencies: `default` required, and [`biomegen`](https://github.com/Gael-de-Sailly/biomegen) optional (provides biome system). Mod dependencies: `default` required, and [`biomegen`](https://github.com/Gael-de-Sailly/biomegen) optional (provides biome system).

View File

@ -1,3 +1,6 @@
local sqrt, abs = math.sqrt, math.abs
local unpk = unpack
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 -- square of distance local a = (x1-x2)^2 + (y1-y2)^2 -- square of distance
@ -5,13 +8,13 @@ local function distance_to_segment(x1, y1, x2, y2, x, y)
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 -- The closest point of the segment is the extremity 1
return math.sqrt(b) return sqrt(b)
elseif a + c < b then elseif a + c < b then
-- The closest point of the segment is the extremity 2 -- The closest point of the segment is the extremity 2
return math.sqrt(c) return sqrt(c)
else else
-- The closest point is on the segment -- The closest point is on the segment
return math.abs(x1 * (y2-y) + x2 * (y-y1) + x * (y1-y2)) / math.sqrt(a) return abs(x1 * (y2-y) + x2 * (y-y1) + x * (y1-y2)) / sqrt(a)
end end
end end
@ -19,8 +22,8 @@ local function transform_quadri(X, Y, x, y)
-- To index points in an irregular quadrilateral, giving x and y between 0 (one edge) and 1 (opposite edge) -- 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 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 = unpk(X)
local y1, y2, y3, y4 = unpack(Y) local y1, y2, y3, y4 = unpk(Y)
-- Compare distance to 2 opposite edges, they give the X coordinate -- 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)

View File

@ -6,7 +6,11 @@ local transform_quadri = dofile(modpath .. 'geometry.lua')
local sea_level = mapgen_rivers.settings.sea_level local sea_level = mapgen_rivers.settings.sea_level
local riverbed_slope = mapgen_rivers.settings.riverbed_slope * mapgen_rivers.settings.blocksize local riverbed_slope = mapgen_rivers.settings.riverbed_slope * mapgen_rivers.settings.blocksize
local MAP_BOTTOM = -31000 local out_elev = mapgen_rivers.settings.margin_elev
-- Localize for performance
local floor, min, max = math.floor, math.min, math.max
local unpk = unpack
-- Linear interpolation -- Linear interpolation
local function interp(v00, v01, v11, v10, xf, zf) local function interp(v00, v01, v11, v10, xf, zf)
@ -30,11 +34,11 @@ local function heightmaps(minp, maxp)
if poly then if poly then
local xf, zf = transform_quadri(poly.x, poly.z, x, z) local xf, zf = transform_quadri(poly.x, poly.z, x, z)
local i00, i01, i11, i10 = unpack(poly.i) local i00, i01, i11, i10 = unpk(poly.i)
-- Load river width on 4 edges and corners -- Load river width on 4 edges and corners
local r_west, r_north, r_east, r_south = unpack(poly.rivers) local r_west, r_north, r_east, r_south = unpk(poly.rivers)
local c_NW, c_NE, c_SE, c_SW = unpack(poly.river_corners) local c_NW, c_NE, c_SE, c_SW = unpk(poly.river_corners)
-- Calculate the depth factor for each edge and corner. -- Calculate the depth factor for each edge and corner.
-- Depth factor: -- Depth factor:
@ -64,10 +68,10 @@ local function heightmaps(minp, maxp)
-- Transform the coordinates to have xf and zf = 0 or 1 in rivers (to avoid rivers having lateral slope and to accomodate the surrounding smoothly) -- Transform the coordinates to have xf and zf = 0 or 1 in rivers (to avoid rivers having lateral slope and to accomodate the surrounding smoothly)
if imax == 0 then if imax == 0 then
local x0 = math.max(r_west, c_NW-zf, zf-c_SW) local x0 = max(r_west, c_NW-zf, zf-c_SW)
local x1 = math.min(r_east, c_NE+zf, c_SE-zf) local x1 = min(r_east, c_NE+zf, c_SE-zf)
local z0 = math.max(r_north, c_NW-xf, xf-c_NE) local z0 = max(r_north, c_NW-xf, xf-c_NE)
local z1 = math.min(r_south, c_SW+xf, c_SE-xf) local z1 = min(r_south, c_SW+xf, c_SE-xf)
xf = (xf-x0) / (x1-x0) xf = (xf-x0) / (x1-x0)
zf = (zf-z0) / (z1-z0) zf = (zf-z0) / (z1-z0)
elseif imax == 1 then elseif imax == 1 then
@ -90,7 +94,7 @@ local function heightmaps(minp, maxp)
-- Determine elevation by interpolation -- Determine elevation by interpolation
local vdem = poly.dem local vdem = poly.dem
local terrain_height = math.floor(0.5+interp( local terrain_height = floor(0.5+interp(
vdem[1], vdem[1],
vdem[2], vdem[2],
vdem[3], vdem[3],
@ -115,17 +119,17 @@ local function heightmaps(minp, maxp)
lake_id = 1 lake_id = 1
end end
end end
local lake_height = math.max(math.floor(poly.lake[lake_id]), terrain_height) local lake_height = max(floor(poly.lake[lake_id]), terrain_height)
if imax > 0 and depth_factor_max > 0 then if imax > 0 and depth_factor_max > 0 then
terrain_height = math.min(math.max(lake_height, sea_level) - math.floor(1+depth_factor_max*riverbed_slope), terrain_height) terrain_height = min(max(lake_height, sea_level) - floor(1+depth_factor_max*riverbed_slope), terrain_height)
end end
terrain_height_map[i] = terrain_height terrain_height_map[i] = terrain_height
lake_height_map[i] = lake_height lake_height_map[i] = lake_height
else else
terrain_height_map[i] = MAP_BOTTOM terrain_height_map[i] = out_elev
lake_height_map[i] = MAP_BOTTOM lake_height_map[i] = out_elev
end end
i = i + 1 i = i + 1
end end

View File

@ -4,6 +4,11 @@ local modpath = minetest.get_modpath(minetest.get_current_modname()) .. '/'
mapgen_rivers.modpath = modpath mapgen_rivers.modpath = modpath
mapgen_rivers.world_data_path = minetest.get_worldpath() .. '/river_data/' mapgen_rivers.world_data_path = minetest.get_worldpath() .. '/river_data/'
if minetest.get_mapgen_setting("mg_name") ~= "singlenode" then
minetest.set_mapgen_setting("mg_name", "singlenode", true)
minetest.log("warning", "[mapgen_rivers] Mapgen set to singlenode")
end
dofile(modpath .. 'settings.lua') dofile(modpath .. 'settings.lua')
local sea_level = mapgen_rivers.settings.sea_level local sea_level = mapgen_rivers.settings.sea_level
@ -27,6 +32,9 @@ local function interp(v00, v01, v11, v10, xf, zf)
return v1*zf + v0*(1-zf) return v1*zf + v0*(1-zf)
end end
-- Localize for performance
local floor, min = math.floor, math.min
local data = {} local data = {}
local noise_x_obj, noise_z_obj, noise_distort_obj, noise_heat_obj, noise_heat_blend_obj local noise_x_obj, noise_z_obj, noise_distort_obj, noise_heat_obj, noise_heat_blend_obj
@ -43,7 +51,7 @@ local sumtime2 = 0
local ngen = 0 local ngen = 0
local function generate(minp, maxp, seed) local function generate(minp, maxp, seed)
print(("[mapgen_rivers] Generating from %s to %s"):format(minetest.pos_to_string(minp), minetest.pos_to_string(maxp))) minetest.log("info", ("[mapgen_rivers] Generating from %s to %s"):format(minetest.pos_to_string(minp), minetest.pos_to_string(maxp)))
local chulens = { local chulens = {
x = maxp.x-minp.x+1, x = maxp.x-minp.x+1,
@ -106,8 +114,8 @@ local function generate(minp, maxp, seed)
end end
end end
local pminp = {x=math.floor(xmin), z=math.floor(zmin)} local pminp = {x=floor(xmin), z=floor(zmin)}
local pmaxp = {x=math.floor(xmax)+1, z=math.floor(zmax)+1} local pmaxp = {x=floor(xmax)+1, z=floor(zmax)+1}
incr = pmaxp.x-pminp.x+1 incr = pmaxp.x-pminp.x+1
i_origin = 1 - pminp.z*incr - pminp.x i_origin = 1 - pminp.z*incr - pminp.x
terrain_map, lake_map = heightmaps(pminp, pmaxp) terrain_map, lake_map = heightmaps(pminp, pmaxp)
@ -115,6 +123,30 @@ local function generate(minp, maxp, seed)
terrain_map, lake_map = heightmaps(minp, maxp) terrain_map, lake_map = heightmaps(minp, maxp)
end end
-- Check that there is at least one position that reaches min y
if minp.y > sea_level then
local y0 = minp.y
local is_empty = true
for i=1, #terrain_map do
if terrain_map[i] >= y0 or lake_map[i] >= y0 then
is_empty = false
break
end
end
-- If not, skip chunk
if is_empty then
local t = os.clock() - t0
ngen = ngen + 1
sumtime = sumtime + t
sumtime2 = sumtime2 + t*t
minetest.log("verbose", "[mapgen_rivers] Skipping empty chunk (fully above ground level)")
minetest.log("verbose", ("[mapgen_rivers] Done in %5.3f s"):format(t))
return
end
end
local c_stone = minetest.get_content_id("default:stone") local c_stone = minetest.get_content_id("default:stone")
local c_dirt = minetest.get_content_id("default:dirt") local c_dirt = minetest.get_content_id("default:dirt")
local c_lawn = minetest.get_content_id("default:dirt_with_grass") local c_lawn = minetest.get_content_id("default:dirt_with_grass")
@ -140,7 +172,7 @@ local function generate(minp, maxp, seed)
for z = minp.z, maxp.z do for z = minp.z, maxp.z do
for x = minp.x, maxp.x do for x = minp.x, maxp.x do
local ivm = a:index(x, minp.y, z) local ivm = a:index(x, maxp.y+1, z)
local ground_above = false local ground_above = false
local temperature local temperature
if use_biomes then if use_biomes then
@ -156,8 +188,8 @@ local function generate(minp, maxp, seed)
if use_distort then if use_distort then
local xn = noise_x_map[nid] local xn = noise_x_map[nid]
local zn = noise_z_map[nid] local zn = noise_z_map[nid]
local x0 = math.floor(xn) local x0 = floor(xn)
local z0 = math.floor(zn) local z0 = floor(zn)
local i0 = i_origin + z0*incr + x0 local i0 = i_origin + z0*incr + x0
local i1 = i0+1 local i1 = i0+1
@ -165,13 +197,12 @@ local function generate(minp, maxp, seed)
local i3 = i2-1 local i3 = i2-1
terrain = interp(terrain_map[i0], terrain_map[i1], terrain_map[i2], terrain_map[i3], xn-x0, zn-z0) terrain = interp(terrain_map[i0], terrain_map[i1], terrain_map[i2], terrain_map[i3], xn-x0, zn-z0)
lake = math.min(lake_map[i0], lake_map[i1], lake_map[i2], lake_map[i3]) lake = min(lake_map[i0], lake_map[i1], lake_map[i2], lake_map[i3])
end end
if y <= maxp.y then if y <= maxp.y then
local is_lake = lake > terrain local is_lake = lake > terrain
local ivm = a:index(x, y, z)
if y <= terrain then if y <= terrain then
if not use_biomes or y <= terrain-1 or ground_above then if not use_biomes or y <= terrain-1 or ground_above then
data[ivm] = c_stone data[ivm] = c_stone
@ -200,7 +231,7 @@ local function generate(minp, maxp, seed)
ground_above = y <= terrain ground_above = y <= terrain
ivm = ivm + ystride ivm = ivm - ystride
if use_distort then if use_distort then
nid = nid + incrY nid = nid + incrY
end end
@ -228,18 +259,17 @@ local function generate(minp, maxp, seed)
vm:calc_lighting() vm:calc_lighting()
vm:update_liquids() vm:update_liquids()
vm:write_to_map() vm:write_to_map()
local t1 = os.clock()
local t = t1-t0 local t = os.clock()-t0
ngen = ngen + 1 ngen = ngen + 1
sumtime = sumtime + t sumtime = sumtime + t
sumtime2 = sumtime2 + t*t sumtime2 = sumtime2 + t*t
print(("[mapgen_rivers] Done in %5.3f s"):format(t)) minetest.log("verbose", ("[mapgen_rivers] Done in %5.3f s"):format(t))
end end
minetest.register_on_generated(generate) minetest.register_on_generated(generate)
minetest.register_on_shutdown(function() minetest.register_on_shutdown(function()
local avg = sumtime / ngen local avg = sumtime / ngen
local std = math.sqrt(sumtime2/ngen - avg*avg) local std = math.sqrt(sumtime2/ngen - avg*avg)
print(("[mapgen_rivers] Mapgen statistics:\n- Mapgen calls: %4d\n- Mean time: %5.3f s\n- Standard deviation: %5.3f s"):format(ngen, avg, std)) minetest.log("action", ("[mapgen_rivers] Mapgen statistics:\n- Mapgen calls: %4d\n- Mean time: %5.3f s\n- Standard deviation: %5.3f s"):format(ngen, avg, std))
end) end)

View File

@ -1,12 +1,15 @@
local worldpath = mapgen_rivers.world_data_path local worldpath = mapgen_rivers.world_data_path
local floor = math.floor
local sbyte, schar = string.byte, string.char
local unpk = unpack
function mapgen_rivers.load_map(filename, bytes, signed, size, converter) function mapgen_rivers.load_map(filename, bytes, signed, size, converter)
local file = io.open(worldpath .. filename, 'rb') local file = io.open(worldpath .. filename, 'rb')
local data = file:read('*all') local data = file:read('*all')
if #data < bytes*size then if #data < bytes*size then
data = minetest.decompress(data) data = minetest.decompress(data)
end end
local sbyte = string.byte
local map = {} local map = {}
@ -35,8 +38,6 @@ function mapgen_rivers.load_map(filename, bytes, signed, size, converter)
return map return map
end end
local sbyte = string.byte
local loader_mt = { local loader_mt = {
__index = function(loader, i) __index = function(loader, i)
local file = loader.file local file = loader.file
@ -75,9 +76,6 @@ end
function mapgen_rivers.write_map(filename, data, bytes) function mapgen_rivers.write_map(filename, data, bytes)
local size = #data local size = #data
local file = io.open(worldpath .. filename, 'wb') local file = io.open(worldpath .. filename, 'wb')
local mfloor = math.floor
local schar = string.char
local upack = unpack
local bytelist = {} local bytelist = {}
for j=1, bytes do for j=1, bytes do
@ -85,15 +83,15 @@ function mapgen_rivers.write_map(filename, data, bytes)
end end
for i=1, size do for i=1, size do
local n = mfloor(data[i]) local n = floor(data[i])
data[i] = n data[i] = n
for j=bytes, 2, -1 do for j=bytes, 2, -1 do
bytelist[j] = n % 256 bytelist[j] = n % 256
n = mfloor(n / 256) n = floor(n / 256)
end end
bytelist[1] = n % 256 bytelist[1] = n % 256
file:write(schar(upack(bytelist))) file:write(schar(unpk(bytelist)))
end end
file:close() file:close()

View File

@ -73,7 +73,7 @@ for name, np in pairs(mapgen_rivers.noise_params) do
if lac > 1 then if lac > 1 then
local omax = math.floor(math.log(math.min(np.spread.x, np.spread.y, np.spread.z)) / math.log(lac))+1 local omax = math.floor(math.log(math.min(np.spread.x, np.spread.y, np.spread.z)) / math.log(lac))+1
if np.octaves > omax then if np.octaves > omax then
print("[mapgen_rivers] Noise " .. name .. ": 'octaves' reduced to " .. omax) minetest.log("warning", "[mapgen_rivers] Noise " .. name .. ": 'octaves' reduced to " .. omax)
np.octaves = omax np.octaves = omax
end end
end end

View File

@ -33,7 +33,7 @@ if first_mapgen then
-- Generate a map!! -- Generate a map!!
local pregenerate = dofile(mapgen_rivers.modpath .. '/pregenerate.lua') local pregenerate = dofile(mapgen_rivers.modpath .. '/pregenerate.lua')
minetest.register_on_mods_loaded(function() minetest.register_on_mods_loaded(function()
print('[mapgen_rivers] Generating grid') minetest.log("action", '[mapgen_rivers] Generating grid, this may take a while...')
pregenerate(load_all) pregenerate(load_all)
if load_all then if load_all then
@ -58,9 +58,9 @@ if not (first_mapgen and load_all) then
minetest.register_on_mods_loaded(function() minetest.register_on_mods_loaded(function()
if load_all then if load_all then
print('[mapgen_rivers] Loading full grid') minetest.log("action", '[mapgen_rivers] Loading full grid')
else else
print('[mapgen_rivers] Loading grid as interactive loaders') minetest.log("action", '[mapgen_rivers] Loading grid as interactive loaders')
end end
local grid = mapgen_rivers.grid local grid = mapgen_rivers.grid
@ -90,16 +90,19 @@ if mapgen_rivers.settings.center then
map_offset.z = blocksize*Z/2 map_offset.z = blocksize*Z/2
end end
-- Localize for performance
local floor, ceil, min, max, abs = math.floor, math.ceil, math.min, math.max, math.abs
local min_catchment = mapgen_rivers.settings.min_catchment / (blocksize*blocksize) local min_catchment = mapgen_rivers.settings.min_catchment / (blocksize*blocksize)
local wpower = mapgen_rivers.settings.river_widening_power local wpower = mapgen_rivers.settings.river_widening_power
local wfactor = 1/(2*blocksize * min_catchment^wpower) local wfactor = 1/(2*blocksize * min_catchment^wpower)
local function river_width(flow) local function river_width(flow)
flow = math.abs(flow) flow = abs(flow)
if flow < min_catchment then if flow < min_catchment then
return 0 return 0
end end
return math.min(wfactor * flow ^ wpower, 1) return min(wfactor * flow ^ wpower, 1)
end end
local noise_heat -- Need a large-scale noise here so no heat blend local noise_heat -- Need a large-scale noise here so no heat blend
@ -138,8 +141,8 @@ local function make_polygons(minp, maxp)
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. -- 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+map_offset.x)/blocksize - 0.5), 0), math.min(math.ceil((maxp.x+map_offset.x)/blocksize + 0.5), X-2) local xpmin, xpmax = max(floor((minp.x+map_offset.x)/blocksize - 0.5), 0), min(ceil((maxp.x+map_offset.x)/blocksize + 0.5), X-2)
local zpmin, zpmax = math.max(math.floor((minp.z+map_offset.z)/blocksize - 0.5), 0), math.min(math.ceil((maxp.z+map_offset.z)/blocksize + 0.5), Z-2) local zpmin, zpmax = max(floor((minp.z+map_offset.z)/blocksize - 0.5), 0), min(ceil((maxp.z+map_offset.z)/blocksize + 0.5), Z-2)
-- Iterate over the polygons -- Iterate over the polygons
for xp = xpmin, xpmax do for xp = xpmin, xpmax do
@ -165,8 +168,8 @@ local function make_polygons(minp, maxp)
local bounds = {} -- Will be a list of the intercepts of polygon edges for every Z position (scanline algorithm) local bounds = {} -- Will be a list of the intercepts of polygon edges for every Z position (scanline algorithm)
-- Calculate the min and max Z positions -- Calculate the min and max Z positions
local zmin = math.max(math.floor(math.min(unpack(poly_z)))+1, minp.z) local zmin = max(floor(min(unpack(poly_z)))+1, minp.z)
local zmax = math.min(math.floor(math.max(unpack(poly_z))), maxp.z) local zmax = min(floor(max(unpack(poly_z))), maxp.z)
-- And initialize the arrays -- And initialize the arrays
for z=zmin, zmax do for z=zmin, zmax do
bounds[z] = {} bounds[z] = {}
@ -176,14 +179,14 @@ local function make_polygons(minp, maxp)
for i2=1, 4 do -- Loop on 4 edges for i2=1, 4 do -- Loop on 4 edges
local z1, z2 = poly_z[i1], poly_z[i2] local z1, z2 = poly_z[i1], poly_z[i2]
-- Calculate the integer Z positions over which this edge spans -- Calculate the integer Z positions over which this edge spans
local lzmin = math.floor(math.min(z1, z2))+1 local lzmin = floor(min(z1, z2))+1
local lzmax = math.floor(math.max(z1, z2)) local lzmax = floor(max(z1, z2))
if lzmin <= lzmax then -- If there is at least one position in it if lzmin <= lzmax then -- If there is at least one position in it
local x1, x2 = poly_x[i1], poly_x[i2] local x1, x2 = poly_x[i1], poly_x[i2]
-- Calculate coefficient of the equation defining the edge: X=aZ+b -- Calculate coefficient of the equation defining the edge: X=aZ+b
local a = (x1-x2) / (z1-z2) local a = (x1-x2) / (z1-z2)
local b = (x1 - a*z1) local b = (x1 - a*z1)
for z=math.max(lzmin, minp.z), math.min(lzmax, maxp.z) do for z=max(lzmin, minp.z), min(lzmax, maxp.z) do
-- For every Z position involved, add the intercepted X position in the table -- For every Z position involved, add the intercepted X position in the table
table.insert(bounds[z], a*z+b) table.insert(bounds[z], a*z+b)
end end
@ -194,11 +197,11 @@ local function make_polygons(minp, maxp)
-- Now sort the bounds list -- Now sort the bounds list
local zlist = bounds[z] local zlist = bounds[z]
table.sort(zlist) table.sort(zlist)
local c = math.floor(#zlist/2) local c = floor(#zlist/2)
for l=1, c do for l=1, c do
-- Take pairs of X coordinates: all positions between them belong to the polygon. -- Take pairs of X coordinates: all positions between them belong to the polygon.
local xmin = math.max(math.floor(zlist[l*2-1])+1, minp.x) local xmin = max(floor(zlist[l*2-1])+1, minp.x)
local xmax = math.min(math.floor(zlist[l*2]), maxp.x) local xmax = min(floor(zlist[l*2]), maxp.x)
local i = (z-minp.z) * chulens + (xmin-minp.x) + 1 local i = (z-minp.z) * chulens + (xmin-minp.x) + 1
for x=xmin, xmax do for x=xmin, xmax do
-- Fill the map at these places -- Fill the map at these places
@ -220,16 +223,16 @@ local function make_polygons(minp, maxp)
local riverD = river_width(rivers[iD]) local riverD = river_width(rivers[iD])
if glaciers then -- Widen the river if glaciers then -- Widen the river
if get_temperature(poly_x[1], poly_dem[1], poly_z[1]) < 0 then if get_temperature(poly_x[1], poly_dem[1], poly_z[1]) < 0 then
riverA = math.min(riverA*glacier_factor, 1) riverA = min(riverA*glacier_factor, 1)
end end
if get_temperature(poly_x[2], poly_dem[2], poly_z[2]) < 0 then if get_temperature(poly_x[2], poly_dem[2], poly_z[2]) < 0 then
riverB = math.min(riverB*glacier_factor, 1) riverB = min(riverB*glacier_factor, 1)
end end
if get_temperature(poly_x[3], poly_dem[3], poly_z[3]) < 0 then if get_temperature(poly_x[3], poly_dem[3], poly_z[3]) < 0 then
riverC = math.min(riverC*glacier_factor, 1) riverC = min(riverC*glacier_factor, 1)
end end
if get_temperature(poly_x[4], poly_dem[4], poly_z[4]) < 0 then if get_temperature(poly_x[4], poly_dem[4], poly_z[4]) < 0 then
riverD = math.min(riverD*glacier_factor, 1) riverD = min(riverD*glacier_factor, 1)
end end
end end

View File

@ -13,6 +13,38 @@ local time_step = mapgen_rivers.settings.evol_time_step
local niter = math.ceil(time/time_step) local niter = math.ceil(time/time_step)
time_step = time / niter time_step = time / niter
local use_margin = mapgen_rivers.settings.margin
local margin_width = mapgen_rivers.settings.margin_width / blocksize
local margin_elev = mapgen_rivers.settings.margin_elev
local function margin(dem, width, elev)
local X, Y = dem.X, dem.Y
for i=1, width do
local c1 = ((i-1)/width) ^ 0.5
local c2 = (1-c1) * elev
local index = (i-1)*X + 1
for x=1, X do
dem[index] = dem[index] * c1 + c2
index = index + 1
end
index = i
for y=1, Y do
dem[index] = dem[index] * c1 + c2
index = index + X
end
index = X*(Y-i) + 1
for x=1, X do
dem[index] = dem[index] * c1 + c2
index = index + 1
end
index = X-i + 1
for y=1, Y do
dem[index] = dem[index] * c1 + c2
index = index + X
end
end
end
local function pregenerate(keep_loaded) local function pregenerate(keep_loaded)
local grid = mapgen_rivers.grid local grid = mapgen_rivers.grid
local size = grid.size local size = grid.size
@ -26,6 +58,10 @@ local function pregenerate(keep_loaded)
dem.X = size.x dem.X = size.x
dem.Y = size.y dem.Y = size.y
if use_margin then
margin(dem, margin_width, margin_elev)
end
local model = EvolutionModel(evol_params) local model = EvolutionModel(evol_params)
model.dem = dem model.dem = dem
local ref_dem = model:define_isostasy(dem) local ref_dem = model:define_isostasy(dem)
@ -33,7 +69,7 @@ local function pregenerate(keep_loaded)
local tectonic_step = tectonic_speed * time_step local tectonic_step = tectonic_speed * time_step
collectgarbage() collectgarbage()
for i=1, niter do for i=1, niter do
print("[mapgen_rivers] Iteration " .. i .. " of " .. niter) minetest.log("info", "[mapgen_rivers] Iteration " .. i .. " of " .. niter)
model:diffuse(time_step) model:diffuse(time_step)
model:flow() model:flow()
@ -41,6 +77,9 @@ local function pregenerate(keep_loaded)
if i < niter then if i < niter then
if tectonic_step ~= 0 then if tectonic_step ~= 0 then
nobj_base:get_3d_map_flat({x=0, y=tectonic_step*i, z=0}, ref_dem) nobj_base:get_3d_map_flat({x=0, y=tectonic_step*i, z=0}, ref_dem)
if use_margin then
margin(ref_dem, margin_width, margin_elev)
end
end end
model:isostasy() model:isostasy()
end end

View File

@ -1,7 +1,7 @@
local mtsettings = minetest.settings local mtsettings = minetest.settings
local mgrsettings = Settings(minetest.get_worldpath() .. '/mapgen_rivers.conf') local mgrsettings = Settings(minetest.get_worldpath() .. '/mapgen_rivers.conf')
mapgen_rivers.version = "1.0" mapgen_rivers.version = "1.0.1"
local previous_version_mt = mtsettings:get("mapgen_rivers_version") or "0.0" local previous_version_mt = mtsettings:get("mapgen_rivers_version") or "0.0"
local previous_version_mgr = mgrsettings:get("version") or "0.0" local previous_version_mgr = mgrsettings:get("version") or "0.0"
@ -74,6 +74,9 @@ mapgen_rivers.settings = {
grid_x_size = def_setting('grid_x_size', 'number', 1000), grid_x_size = def_setting('grid_x_size', 'number', 1000),
grid_z_size = def_setting('grid_z_size', 'number', 1000), grid_z_size = def_setting('grid_z_size', 'number', 1000),
margin = def_setting('margin', 'bool', true),
margin_width = def_setting('margin_width', 'number', 2000),
margin_elev = def_setting('margin_elev', 'number', -200),
evol_params = { evol_params = {
K = def_setting('river_erosion_coef', 'number', 0.5), K = def_setting('river_erosion_coef', 'number', 0.5),
m = def_setting('river_erosion_power', 'number', 0.4), m = def_setting('river_erosion_power', 'number', 0.4),

View File

@ -17,13 +17,23 @@ mapgen_rivers_grid_x_size (Grid X size) int 1000 50 5000
# Actual size of the map is grid_z_size * blocksize # Actual size of the map is grid_z_size * blocksize
mapgen_rivers_grid_z_size (Grid Z size) int 1000 50 5000 mapgen_rivers_grid_z_size (Grid Z size) int 1000 50 5000
# If margin is enabled, elevation becomes closer to a fixed value when approaching
# the edges of the map.
mapgen_rivers_margin (Margin) bool true
# Width of the transition at map borders, in nodes
mapgen_rivers_margin_width (Margin width) float 2000.0 0.0 15000.0
# Elevation toward which to converge at map borders
mapgen_rivers_margin_elev (Margin elevation) float -200.0 -31000.0 31000.0
# Minimal catchment area for a river to be drawn, in square nodes # Minimal catchment area for a river to be drawn, in square nodes
# Lower value means bigger river density # Lower value means bigger river density
mapgen_rivers_min_catchment (Minimal catchment area) float 3600.0 100.0 1000000.0 mapgen_rivers_min_catchment (Minimal catchment area) float 3600.0 100.0 1000000.0
# Coefficient describing how rivers widen when merging. # Coefficient describing how rivers widen when merging.
# Riwer width is a power law W = a*D^p. D is river flow and p is this parameter. # Riwer width is a power law W = a*D^p. D is river flow and p is this parameter.
# Higher value means a river needs to receive more tributaries to grow in width. # Higher value means that a river will grow more when receiving a tributary.
# Note that a river can never exceed 2*blocksize. # Note that a river can never exceed 2*blocksize.
mapgen_rivers_river_widening_power (River widening power) float 0.5 0.0 1.0 mapgen_rivers_river_widening_power (River widening power) float 0.5 0.0 1.0

View File

@ -1,5 +1,17 @@
-- rivermapper.lua -- rivermapper.lua
-- This file provide functions to construct the river tree from an elevation model.
-- Based on a research paper:
--
-- Cordonnier, G., Bovy, B., and Braun, J.:
-- A versatile, linear complexity algorithm for flow routing in topographies with depressions,
-- Earth Surf. Dynam., 7, 549562, https://doi.org/10.5194/esurf-7-549-2019, 2019.
--
-- Big thanks to them for releasing this paper under a free license ! :)
-- The algorithm here makes use of most of the paper's concepts, including the Planar Boruvka algorithm.
-- Only flow_local and accumulate_flow are custom algorithms.
local function flow_local_semirandom(plist) local function flow_local_semirandom(plist)
local sum = 0 local sum = 0
for i=1, #plist do for i=1, #plist do