2 Commits

Author SHA1 Message Date
b1f4437a91 Added settings for margin, and documented in settingtypes.txt 2022-01-03 16:39:02 +01:00
a00c1cbd39 Added margin with a settable width near grid border
Elevation gets closer to -50 when approaching the border
2022-01-03 16:39:02 +01:00
17 changed files with 839 additions and 867 deletions

View File

@ -1,18 +0,0 @@
CHANGELOG
=========
## `v1.0.2` (2022-01-10)
- Use builtin logging system and appropriate loglevels
- Skip empty chunks, when generating high above ground (~20% speedup)
- Minor optimizations (turning global variables to local...)
## `v1.0.1` (2021-09-14)
- Automatically switch to `singlenode` mapgen at init time
## `v1.0` (2021-08-01)
- Rewritten pregen code (terrainlib) in pure Lua
- Optimized grid loading
- Load grid nodes on request by default
- Changed river width settings
- Added map size in settings
- Added logs

View File

@ -1,5 +1,5 @@
# Map Generator with Rivers # Map Generator with Rivers
`mapgen_rivers v1.0.2` 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).
@ -13,11 +13,10 @@ It used to be composed of a Python script doing pre-generation, and a Lua mod re
License: GNU LGPLv3.0 License: GNU LGPLv3.0
Code: Gaël de Sailly 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. 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
No required dependency, but [`biomegen`](https://gitlab.com/gaelysam/biomegen) recommended (provides biome system). Mod dependencies: `default` required, and [`biomegen`](https://github.com/Gael-de-Sailly/biomegen) optional (provides biome system).
# Installation # Installation
This mod should be placed in the `mods/` directory of Minetest like any other mod. This mod should be placed in the `mods/` directory of Minetest like any other mod.

View File

@ -1,12 +1,12 @@
local modpath = minetest.get_modpath(minetest.get_current_modname()) local modpath = mapgen_rivers.modpath
local make_polygons = dofile(modpath .. '/polygons.lua') local make_polygons = dofile(modpath .. 'polygons.lua')
local transform_quadri = dofile(modpath .. '/geometry.lua') local transform_quadri = dofile(modpath .. 'geometry.lua')
local sea_level = tonumber(minetest.get_mapgen_setting("water_level")) local sea_level = mapgen_rivers.settings.sea_level
local riverbed_slope = tonumber(mapgen_rivers.settings:get("riverbed_slope")) * tonumber(mapgen_rivers.settings:get("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 -- Localize for performance
local floor, min, max = math.floor, math.min, math.max local floor, min, max = math.floor, math.min, math.max
@ -128,8 +128,8 @@ local function heightmaps(minp, maxp)
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

290
init.lua
View File

@ -1,43 +1,275 @@
mapgen_rivers = {} mapgen_rivers = {}
local modpath = minetest.get_modpath(minetest.get_current_modname()) .. '/'
mapgen_rivers.modpath = modpath
mapgen_rivers.world_data_path = minetest.get_worldpath() .. '/river_data/'
if minetest.get_mapgen_setting("mg_name") ~= "singlenode" then if minetest.get_mapgen_setting("mg_name") ~= "singlenode" then
minetest.set_mapgen_setting("mg_name", "singlenode", true) minetest.set_mapgen_setting("mg_name", "singlenode", true)
minetest.log("warning", "[mapgen_rivers] Mapgen set to singlenode") minetest.log("warning", "[mapgen_rivers] Mapgen set to singlenode")
end end
local modpath = minetest.get_modpath(minetest.get_current_modname()) dofile(modpath .. 'settings.lua')
dofile(modpath .. '/settings.lua')
local sfile = io.open(minetest.get_worldpath() .. '/river_data/size') local sea_level = mapgen_rivers.settings.sea_level
if sfile then local elevation_chill = mapgen_rivers.settings.elevation_chill
sfile:close() local use_distort = mapgen_rivers.settings.distort
else local use_biomes = mapgen_rivers.settings.biomes
dofile(modpath .. '/pregenerate.lua') local use_biomegen_mod = use_biomes and minetest.global_exists('biomegen')
collectgarbage() use_biomes = use_biomes and not use_biomegen_mod
if use_biomegen_mod then
biomegen.set_elevation_chill(elevation_chill)
end
dofile(modpath .. 'noises.lua')
local heightmaps = dofile(modpath .. 'heightmap.lua')
-- Linear interpolation
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)
end end
mapgen_rivers.use_mapgen_thread = minetest.settings:get_bool("mapgen_rivers_use_mapgen_thread") -- Localize for performance
mapgen_rivers.thread = 'main' local floor, min = math.floor, math.min
if mapgen_rivers.use_mapgen_thread then
if minetest.register_mapgen_dofile then local data = {}
minetest.register_mapgen_dofile(modpath .. '/mapgen.lua')
local noise_x_obj, noise_z_obj, noise_distort_obj, noise_heat_obj, noise_heat_blend_obj
local noise_x_map = {}
local noise_z_map = {}
local noise_distort_map = {}
local noise_heat_map = {}
local noise_heat_blend_map = {}
local mapsize
local init = false
local sumtime = 0
local sumtime2 = 0
local ngen = 0
local function generate(minp, maxp, seed)
minetest.log("info", ("[mapgen_rivers] Generating from %s to %s"):format(minetest.pos_to_string(minp), minetest.pos_to_string(maxp)))
local chulens = {
x = maxp.x-minp.x+1,
y = maxp.y-minp.y+1,
z = maxp.z-minp.z+1,
}
if not init then
mapsize = {
x = chulens.x,
y = chulens.y+1,
z = chulens.z,
}
if use_distort then
noise_x_obj = minetest.get_perlin_map(mapgen_rivers.noise_params.distort_x, mapsize)
noise_z_obj = minetest.get_perlin_map(mapgen_rivers.noise_params.distort_z, mapsize)
noise_distort_obj = minetest.get_perlin_map(mapgen_rivers.noise_params.distort_amplitude, chulens)
end
if use_biomes then
noise_heat_obj = minetest.get_perlin_map(mapgen_rivers.noise_params.heat, chulens)
noise_heat_blend_obj = minetest.get_perlin_map(mapgen_rivers.noise_params.heat_blend, chulens)
end
init = true
end
local t0 = os.clock()
local minp2d = {x=minp.x, y=minp.z}
if use_distort then
noise_x_obj:get_3d_map_flat(minp, noise_x_map)
noise_z_obj:get_3d_map_flat(minp, noise_z_map)
noise_distort_obj:get_2d_map_flat(minp2d, noise_distort_map)
end
if use_biomes then
noise_heat_obj:get_2d_map_flat(minp2d, noise_heat_map)
noise_heat_blend_obj:get_2d_map_flat(minp2d, noise_heat_blend_map)
end
local terrain_map, lake_map, incr, i_origin
if use_distort then
local xmin, xmax, zmin, zmax = minp.x, maxp.x, minp.z, maxp.z
local i = 0
local i2d = 0
for z=minp.z, maxp.z do
for y=minp.y, maxp.y+1 do
for x=minp.x, maxp.x do
i = i+1
i2d = i2d+1
local distort = noise_distort_map[i2d]
local xv = noise_x_map[i]*distort + x
if xv < xmin then xmin = xv end
if xv > xmax then xmax = xv end
noise_x_map[i] = xv
local zv = noise_z_map[i]*distort + z
if zv < zmin then zmin = zv end
if zv > zmax then zmax = zv end
noise_z_map[i] = zv
end
i2d = i2d-chulens.x
end
end
local pminp = {x=floor(xmin), z=floor(zmin)}
local pmaxp = {x=floor(xmax)+1, z=floor(zmax)+1}
incr = pmaxp.x-pminp.x+1
i_origin = 1 - pminp.z*incr - pminp.x
terrain_map, lake_map = heightmaps(pminp, pmaxp)
else else
minetest.log("warning", "[mapgen_rivers] Mapgen thread not available on this Minetest version.") terrain_map, lake_map = heightmaps(minp, maxp)
mapgen_rivers.use_mapgen_thread = false
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_dirt = minetest.get_content_id("default:dirt")
local c_lawn = minetest.get_content_id("default:dirt_with_grass")
local c_dirtsnow = minetest.get_content_id("default:dirt_with_snow")
local c_snow = minetest.get_content_id("default:snowblock")
local c_sand = minetest.get_content_id("default:sand")
local c_water = minetest.get_content_id("default:water_source")
local c_rwater = minetest.get_content_id("default:river_water_source")
local c_ice = minetest.get_content_id("default:ice")
local vm, emin, emax = minetest.get_mapgen_object("voxelmanip")
vm:get_data(data)
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 nid = mapsize.x*(mapsize.y-1) + 1
local incrY = -mapsize.x
local incrX = 1 - mapsize.y*incrY
local incrZ = mapsize.x*mapsize.y - mapsize.x*incrX - mapsize.x*mapsize.y*incrY
local i2d = 1
for z = minp.z, maxp.z do
for x = minp.x, maxp.x do
local ivm = a:index(x, maxp.y+1, z)
local ground_above = false
local temperature
if use_biomes then
temperature = noise_heat_map[i2d]+noise_heat_blend_map[i2d]
end
local terrain, lake
if not use_distort then
terrain = terrain_map[i2d]
lake = lake_map[i2d]
end
for y = maxp.y+1, minp.y, -1 do
if use_distort then
local xn = noise_x_map[nid]
local zn = noise_z_map[nid]
local x0 = floor(xn)
local z0 = floor(zn)
local i0 = i_origin + z0*incr + x0
local i1 = i0+1
local i2 = i1+incr
local i3 = i2-1
terrain = interp(terrain_map[i0], terrain_map[i1], terrain_map[i2], terrain_map[i3], xn-x0, zn-z0)
lake = min(lake_map[i0], lake_map[i1], lake_map[i2], lake_map[i3])
end
if y <= maxp.y then
local is_lake = lake > terrain
if y <= terrain then
if not use_biomes or y <= terrain-1 or ground_above then
data[ivm] = c_stone
elseif is_lake or y < sea_level then
data[ivm] = c_sand
else
local temperature_y = temperature - y*elevation_chill
if temperature_y >= 15 then
data[ivm] = c_lawn
elseif temperature_y >= 0 then
data[ivm] = c_dirtsnow
else
data[ivm] = c_snow
end
end
elseif y <= lake and lake > sea_level then
if not use_biomes or temperature - y*elevation_chill >= 0 then
data[ivm] = c_rwater
else
data[ivm] = c_ice
end
elseif y <= sea_level then
data[ivm] = c_water
end
end
ground_above = y <= terrain
ivm = ivm - ystride
if use_distort then
nid = nid + incrY
end
end
if use_distort then
nid = nid + incrX
end
i2d = i2d + 1
end
if use_distort then
nid = nid + incrZ
end
end
if use_biomegen_mod then
biomegen.generate_all(data, a, vm, minp, maxp, seed)
else
vm:set_data(data)
minetest.generate_ores(vm, minp, maxp)
end
vm:set_lighting({day = 0, night = 0})
vm:calc_lighting()
vm:update_liquids()
vm:write_to_map()
local t = os.clock()-t0
ngen = ngen + 1
sumtime = sumtime + t
sumtime2 = sumtime2 + t*t
minetest.log("verbose", ("[mapgen_rivers] Done in %5.3f s"):format(t))
end end
if not mapgen_rivers.use_mapgen_thread then minetest.register_on_generated(generate)
dofile(modpath .. '/mapgen.lua') minetest.register_on_shutdown(function()
end local avg = sumtime / ngen
local std = math.sqrt(sumtime2/ngen - avg*avg)
-- Setup a metatable to load grid on request if not present 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))
local mt = { end)
__index = function(_, field)
if field == 'grid' then
dofile(modpath .. '/load_grid.lua')
return mapgen_rivers.grid
end
end,
}
setmetatable(mapgen_rivers, mt)

98
load.lua Normal file
View File

@ -0,0 +1,98 @@
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)
local file = io.open(worldpath .. filename, 'rb')
local data = file:read('*all')
if #data < bytes*size then
data = minetest.decompress(data)
end
local map = {}
for i=1, size do
local i0 = (i-1)*bytes+1
local elements = {data:byte(i0, i1)}
local n = sbyte(data, i0)
if signed and n >= 128 then
n = n - 256
end
for j=1, bytes-1 do
n = n*256 + sbyte(data, i0+j)
end
map[i] = n
end
file:close()
if converter then
for i=1, size do
map[i] = converter(map[i])
end
end
return map
end
local loader_mt = {
__index = function(loader, i)
local file = loader.file
local bytes = loader.bytes
file:seek('set', (i-1)*bytes)
local strnum = file:read(bytes)
local n = sbyte(strnum, 1)
if loader.signed and n >= 128 then
n = n - 256
end
for j=2, bytes do
n = n*256 + sbyte(strnum, j)
end
if loader.conv then
n = loader.conv(n)
end
loader[i] = n
return n
end,
}
function mapgen_rivers.interactive_loader(filename, bytes, signed, size, converter)
local file = io.open(worldpath .. filename, 'rb')
if file then
minetest.register_on_shutdown(function()
file:close()
end)
converter = converter or false
return setmetatable({file=file, bytes=bytes, signed=signed, size=size, conv=converter}, loader_mt)
end
end
function mapgen_rivers.write_map(filename, data, bytes)
local size = #data
local file = io.open(worldpath .. filename, 'wb')
local bytelist = {}
for j=1, bytes do
bytelist[j] = 0
end
for i=1, size do
local n = floor(data[i])
data[i] = n
for j=bytes, 2, -1 do
bytelist[j] = n % 256
n = floor(n / 256)
end
bytelist[1] = n % 256
file:write(schar(unpk(bytelist)))
end
file:close()
end

View File

@ -1,107 +0,0 @@
local datapath = minetest.get_worldpath() .. "/river_data/"
local floor = math.floor
local sbyte = string.byte
local unpk = unpack
local load_map
local use_interactive_loader
if minetest.settings:has("mapgen_rivers_use_interactive_loader") then
use_interactive_loader = minetest.settings:get_bool("mapgen_rivers_use_interactive_loader")
else
use_interactive_loader = not minetest.settings:get_bool("mapgen_rivers_load_all")
end
if use_interactive_loader then
local loader_mt = {
__index = function(loader, i)
local file = loader.file
local bytes = loader.bytes
file:seek('set', (i-1)*bytes)
local strnum = file:read(bytes)
local n = sbyte(strnum, 1)
if loader.signed and n >= 128 then
n = n - 256
end
for j=2, bytes do
n = n*256 + sbyte(strnum, j)
end
if loader.conv then
n = loader.conv(n)
end
loader[i] = n
return n
end,
}
load_map = function(filename, bytes, signed, size, converter)
local file = io.open(datapath .. filename, 'rb')
if file then
minetest.register_on_shutdown(function()
file:close()
end)
converter = converter or false
return setmetatable({file=file, bytes=bytes, signed=signed, size=size, conv=converter}, loader_mt)
end
end
else
load_map = function(filename, bytes, signed, size, converter)
local file = io.open(datapath .. filename, 'rb')
local data = file:read('*all')
if #data < bytes*size then
data = minetest.decompress(data)
end
local map = {}
for i=1, size do
local i0 = (i-1)*bytes+1
local i1 = i*bytes
local elements = {data:byte(i0, i1)}
local n = sbyte(data, i0)
if signed and n >= 128 then
n = n - 256
end
for j=1, bytes-1 do
n = n*256 + sbyte(data, i0+j)
end
map[i] = n
end
file:close()
if converter then
for i=1, size do
map[i] = converter(map[i])
end
end
return map
end
end
local sfile = io.open(datapath..'size', 'r')
assert(sfile)
local X, Z = tonumber(sfile:read('*l')), tonumber(sfile:read('*l'))
sfile:close()
local function offset_converter(o)
return (o + 0.5) * (1/256)
end
mapgen_rivers.grid = {
size = {x=X, y=Z},
dem = load_map('dem', 2, true, X*Z),
lakes = load_map('lakes', 2, true, X*Z),
dirs = load_map('dirs', 1, false, X*Z),
rivers = load_map('rivers', 4, false, X*Z),
offset_x = load_map('offset_x', 1, true, X*Z, offset_converter),
offset_y = load_map('offset_y', 1, true, X*Z, offset_converter);
}

View File

@ -1,307 +0,0 @@
-- Recreate mod table if we are in a separate environment
if not minetest.global_exists("mapgen_rivers") then
mapgen_rivers = {use_mapgen_thread=true, thread='mapgen'}
mapgen_rivers.settings = Settings(minetest.get_worldpath() .. '/mapgen_rivers.conf')
end
if not mapgen_rivers.grid then
dofile(minetest.get_modpath(minetest.get_current_modname()) .. '/load_grid.lua')
end
local settings = mapgen_rivers.settings
local sea_level = tonumber(minetest.get_mapgen_setting("water_level"))
local elevation_chill = tonumber(settings:get('elevation_chill'))
local use_distort = settings:get_bool('distort')
local use_biomes = settings:get_bool('biomes')
local use_biomegen_mod = use_biomes and minetest.global_exists('biomegen')
use_biomes = use_biomes and minetest.get_modpath("default") and not use_biomegen_mod
local noiseparams = {
distort_x = settings:get_np_group('np_distort_x'),
distort_z = settings:get_np_group('np_distort_z'),
distort_amplitude = settings:get_np_group('np_distort_amplitude'),
}
if use_biomes then
noiseparams.heat = minetest.get_mapgen_setting_noiseparams("mg_biome_np_heat")
noiseparams.heat.offset = noiseparams.heat.offset + sea_level / elevation_chill
noiseparams.heat_blend = minetest.get_mapgen_setting_noiseparams("mg_biome_np_heat_blend")
end
if use_biomegen_mod then
biomegen.set_elevation_chill(elevation_chill)
end
local heightmaps = dofile(minetest.get_modpath(minetest.get_current_modname()) .. '/heightmap.lua')
-- Linear interpolation
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)
end
-- Localize for performance
local floor, min = math.floor, math.min
local data = {}
local noise_x_obj, noise_z_obj, noise_distort_obj, noise_heat_obj, noise_heat_blend_obj
local noise_x_map = {}
local noise_z_map = {}
local noise_distort_map = {}
local noise_heat_map = {}
local noise_heat_blend_map = {}
local mapsize
local init = false
local sumtime = 0
local sumtime2 = 0
local ngen = 0
local function init_mapgen(chulens)
mapsize = {
x = chulens.x,
y = chulens.y+1,
z = chulens.z,
}
if use_distort then
noise_x_obj = minetest.get_perlin_map(noiseparams.distort_x, mapsize)
noise_z_obj = minetest.get_perlin_map(noiseparams.distort_z, mapsize)
noise_distort_obj = minetest.get_perlin_map(noiseparams.distort_amplitude, chulens)
end
if use_biomes then
noise_heat_obj = minetest.get_perlin_map(noiseparams.heat, chulens)
noise_heat_blend_obj = minetest.get_perlin_map(noiseparams.heat_blend, chulens)
end
end
local function generate(vm, minp, maxp, seed)
minetest.log("info", ("[mapgen_rivers] Generating from %s to %s"):format(minetest.pos_to_string(minp), minetest.pos_to_string(maxp)))
local chulens = {
x = maxp.x-minp.x+1,
y = maxp.y-minp.y+1,
z = maxp.z-minp.z+1,
}
if not init then
init_mapgen(chulens)
init = true
end
local t0 = os.clock()
local minp2d = {x=minp.x, y=minp.z}
if use_distort then
noise_x_obj:get_3d_map_flat(minp, noise_x_map)
noise_z_obj:get_3d_map_flat(minp, noise_z_map)
noise_distort_obj:get_2d_map_flat(minp2d, noise_distort_map)
end
if use_biomes then
noise_heat_obj:get_2d_map_flat(minp2d, noise_heat_map)
noise_heat_blend_obj:get_2d_map_flat(minp2d, noise_heat_blend_map)
end
local terrain_map, lake_map, incr, i_origin
if use_distort then
local xmin, xmax, zmin, zmax = minp.x, maxp.x, minp.z, maxp.z
local i = 0
local i2d = 0
for z=minp.z, maxp.z do
for y=minp.y, maxp.y+1 do
for x=minp.x, maxp.x do
i = i+1
i2d = i2d+1
local distort = noise_distort_map[i2d]
local xv = noise_x_map[i]*distort + x
if xv < xmin then xmin = xv end
if xv > xmax then xmax = xv end
noise_x_map[i] = xv
local zv = noise_z_map[i]*distort + z
if zv < zmin then zmin = zv end
if zv > zmax then zmax = zv end
noise_z_map[i] = zv
end
i2d = i2d-chulens.x
end
end
local pminp = {x=floor(xmin), z=floor(zmin)}
local pmaxp = {x=floor(xmax)+1, z=floor(zmax)+1}
incr = pmaxp.x-pminp.x+1
i_origin = 1 - pminp.z*incr - pminp.x
terrain_map, lake_map = heightmaps(pminp, pmaxp)
else
terrain_map, lake_map = heightmaps(minp, maxp)
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("mapgen_stone")
local c_water = minetest.get_content_id("mapgen_water_source")
local c_rwater = minetest.get_content_id("mapgen_river_water_source")
local c_dirt, c_lawn, c_dirtsnow, c_snow, c_sand, c_ice
if use_biomes then
c_dirt = minetest.get_content_id("default:dirt")
c_lawn = minetest.get_content_id("default:dirt_with_grass")
c_dirtsnow = minetest.get_content_id("default:dirt_with_snow")
c_snow = minetest.get_content_id("default:snowblock")
c_sand = minetest.get_content_id("default:sand")
c_ice = minetest.get_content_id("default:ice")
end
local emin, emax = vm:get_emerged_area()
vm:get_data(data)
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 nid = mapsize.x*(mapsize.y-1) + 1
local incrY = -mapsize.x
local incrX = 1 - mapsize.y*incrY
local incrZ = mapsize.x*mapsize.y - mapsize.x*incrX - mapsize.x*mapsize.y*incrY
local i2d = 1
for z = minp.z, maxp.z do
for x = minp.x, maxp.x do
local ivm = a:index(x, maxp.y+1, z)
local ground_above = false
local temperature
if use_biomes then
temperature = noise_heat_map[i2d]+noise_heat_blend_map[i2d]
end
local terrain, lake
if not use_distort then
terrain = terrain_map[i2d]
lake = lake_map[i2d]
end
for y = maxp.y+1, minp.y, -1 do
if use_distort then
local xn = noise_x_map[nid]
local zn = noise_z_map[nid]
local x0 = floor(xn)
local z0 = floor(zn)
local i0 = i_origin + z0*incr + x0
local i1 = i0+1
local i2 = i1+incr
local i3 = i2-1
terrain = interp(terrain_map[i0], terrain_map[i1], terrain_map[i2], terrain_map[i3], xn-x0, zn-z0)
lake = min(lake_map[i0], lake_map[i1], lake_map[i2], lake_map[i3])
end
if y <= maxp.y then
local is_lake = lake > terrain
if y <= terrain then
if not use_biomes or y <= terrain-1 or ground_above then
data[ivm] = c_stone
elseif is_lake or y < sea_level then
data[ivm] = c_sand
else
local temperature_y = temperature - y*elevation_chill
if temperature_y >= 15 then
data[ivm] = c_lawn
elseif temperature_y >= 0 then
data[ivm] = c_dirtsnow
else
data[ivm] = c_snow
end
end
elseif y <= lake and lake > sea_level then
if not use_biomes or temperature - y*elevation_chill >= 0 then
data[ivm] = c_rwater
else
data[ivm] = c_ice
end
elseif y <= sea_level then
data[ivm] = c_water
end
end
ground_above = y <= terrain
ivm = ivm - ystride
if use_distort then
nid = nid + incrY
end
end
if use_distort then
nid = nid + incrX
end
i2d = i2d + 1
end
if use_distort then
nid = nid + incrZ
end
end
if use_biomegen_mod then
biomegen.generate_all(data, a, vm, minp, maxp, seed)
else
vm:set_data(data)
minetest.generate_ores(vm, minp, maxp)
end
vm:set_lighting({day = 0, night = 0})
vm:calc_lighting()
vm:update_liquids()
if mapgen_rivers.thread == "main" then
vm:write_to_map()
end
local t = os.clock()-t0
ngen = ngen + 1
sumtime = sumtime + t
sumtime2 = sumtime2 + t*t
minetest.log("verbose", ("[mapgen_rivers] Done in %5.3f s"):format(t))
end
if mapgen_rivers.thread == "main" then
minetest.register_on_generated(function(minp, maxp, seed)
local vm = minetest.get_mapgen_object("voxelmanip")
generate(vm, minp, maxp, seed)
end)
elseif mapgen_rivers.thread == "mapgen" then
minetest.register_on_generated(generate)
end
minetest.register_on_shutdown(function()
if ngen == 0 then
return
end
local avg = sumtime / ngen
local std = math.sqrt(sumtime2/ngen - avg*avg)
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)

View File

@ -1,3 +1,4 @@
name = mapgen_rivers name = mapgen_rivers
title = Map generator with realistic rivers title = Map generator with realistic rivers
optional_depends = biomegen, default depends = default
optional_depends = biomegen

80
noises.lua Normal file
View File

@ -0,0 +1,80 @@
local def_setting = mapgen_rivers.define_setting
mapgen_rivers.noise_params = {
base = def_setting('np_base', 'noise', {
offset = 0,
scale = 300,
seed = 2469,
octaves = 8,
spread = {x=2048, y=2048, z=2048},
persist = 0.6,
lacunarity = 2,
flags = "eased",
}),
distort_x = def_setting('np_distort_x', 'noise', {
offset = 0,
scale = 1,
seed = -4574,
spread = {x=64, y=32, z=64},
octaves = 3,
persistence = 0.75,
lacunarity = 2,
}),
distort_z = def_setting('np_distort_z', 'noise', {
offset = 0,
scale = 1,
seed = -7940,
spread = {x=64, y=32, z=64},
octaves = 3,
persistence = 0.75,
lacunarity = 2,
}),
distort_amplitude = def_setting('np_distort_amplitude', 'noise', {
offset = 0,
scale = 10,
seed = 676,
spread = {x=1024, y=1024, z=1024},
octaves = 5,
persistence = 0.5,
lacunarity = 2,
flags = "absvalue",
}),
heat = minetest.get_mapgen_setting_noiseparams('mg_biome_np_heat'),
heat_blend = minetest.get_mapgen_setting_noiseparams('mg_biome_np_heat_blend'),
}
-- Convert to number because Minetest API is not able to do it cleanly...
for name, np in pairs(mapgen_rivers.noise_params) do
for field, value in pairs(np) do
if field ~= 'flags' and type(value) == 'string' then
np[field] = tonumber(value) or value
elseif field == 'spread' then
for dir, v in pairs(value) do
value[dir] = tonumber(v) or v
end
end
end
end
local heat = mapgen_rivers.noise_params.heat
local base = mapgen_rivers.noise_params.base
local settings = mapgen_rivers.settings
heat.offset = heat.offset + settings.sea_level * settings.elevation_chill
base.spread.x = base.spread.x / settings.blocksize
base.spread.y = base.spread.y / settings.blocksize
base.spread.z = base.spread.z / settings.blocksize
for name, np in pairs(mapgen_rivers.noise_params) do
local lac = np.lacunarity or 2
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
if np.octaves > omax then
minetest.log("warning", "[mapgen_rivers] Noise " .. name .. ": 'octaves' reduced to " .. omax)
np.octaves = omax
end
end
end

View File

@ -1,17 +1,91 @@
local X = mapgen_rivers.grid.size.x local modpath = mapgen_rivers.modpath
local Z = mapgen_rivers.grid.size.y local mod_data_path = modpath .. 'river_data/'
if not io.open(mod_data_path .. 'size', 'r') then
mod_data_path = modpath .. 'demo_data/'
end
local world_data_path = mapgen_rivers.world_data_path
minetest.mkdir(world_data_path)
dofile(modpath .. 'load.lua')
mapgen_rivers.grid = {}
local X = mapgen_rivers.settings.grid_x_size
local Z = mapgen_rivers.settings.grid_z_size
local function offset_converter(o)
return (o + 0.5) * (1/256)
end
local load_all = mapgen_rivers.settings.load_all
-- Try to read file 'size'
local sfile = io.open(world_data_path..'size', 'r')
local first_mapgen = true
if sfile then
X, Z = tonumber(sfile:read('*l')), tonumber(sfile:read('*l'))
sfile:close()
first_mapgen = false
end
if first_mapgen then
-- Generate a map!!
local pregenerate = dofile(mapgen_rivers.modpath .. '/pregenerate.lua')
minetest.register_on_mods_loaded(function()
minetest.log("action", '[mapgen_rivers] Generating grid, this may take a while...')
pregenerate(load_all)
if load_all then
local offset_x = mapgen_rivers.grid.offset_x
local offset_y = mapgen_rivers.grid.offset_y
for i=1, X*Z do
offset_x[i] = offset_converter(offset_x[i])
offset_y[i] = offset_converter(offset_y[i])
end
end
end)
end
-- if data not already loaded
if not (first_mapgen and load_all) then
local load_map
if load_all then
load_map = mapgen_rivers.load_map
else
load_map = mapgen_rivers.interactive_loader
end
minetest.register_on_mods_loaded(function()
if load_all then
minetest.log("action", '[mapgen_rivers] Loading full grid')
else
minetest.log("action", '[mapgen_rivers] Loading grid as interactive loaders')
end
local grid = mapgen_rivers.grid
grid.dem = load_map('dem', 2, true, X*Z)
grid.lakes = load_map('lakes', 2, true, X*Z)
grid.dirs = load_map('dirs', 1, false, X*Z)
grid.rivers = load_map('rivers', 4, false, X*Z)
grid.offset_x = load_map('offset_x', 1, true, X*Z, offset_converter)
grid.offset_y = load_map('offset_y', 1, true, X*Z, offset_converter)
end)
end
mapgen_rivers.grid.size = {x=X, y=Z}
local function index(x, z) local function index(x, z)
return z*X+x+1 return z*X+x+1
end end
local settings = mapgen_rivers.settings local blocksize = mapgen_rivers.settings.blocksize
local min_catchment = mapgen_rivers.settings.min_catchment
local blocksize = tonumber(settings:get('blocksize')) local max_catchment = mapgen_rivers.settings.max_catchment
local min_catchment = tonumber(settings:get('min_catchment'))
local map_offset = {x=0, z=0} local map_offset = {x=0, z=0}
if settings:get_bool('center') then if mapgen_rivers.settings.center then
map_offset.x = blocksize*X/2 map_offset.x = blocksize*X/2
map_offset.z = blocksize*Z/2 map_offset.z = blocksize*Z/2
end end
@ -19,8 +93,8 @@ end
-- Localize for performance -- Localize for performance
local floor, ceil, min, max, abs = math.floor, math.ceil, math.min, math.max, math.abs local floor, ceil, min, max, abs = math.floor, math.ceil, math.min, math.max, math.abs
min_catchment = min_catchment / (blocksize*blocksize) local min_catchment = mapgen_rivers.settings.min_catchment / (blocksize*blocksize)
local wpower = settings:get('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 = abs(flow) flow = abs(flow)
@ -32,14 +106,14 @@ local function river_width(flow)
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
local elevation_chill = settings:get_bool('elevation_chill') local elevation_chill = mapgen_rivers.settings.elevation_chill
local function get_temperature(x, y, z) local function get_temperature(x, y, z)
local pos = {x=x, y=z} local pos = {x=x, y=z}
return noise_heat:get2d(pos) - y*elevation_chill return noise_heat:get2d(pos) - y*elevation_chill
end end
local glaciers = settings:get_bool('glaciers') local glaciers = mapgen_rivers.settings.glaciers
local glacier_factor = tonumber(settings:get('glacier_factor')) local glacier_factor = mapgen_rivers.settings.glacier_factor
local init = false local init = false

View File

@ -1,51 +1,74 @@
local modpath = minetest.get_modpath(minetest.get_current_modname()) local EvolutionModel = dofile(mapgen_rivers.modpath .. '/terrainlib_lua/erosion.lua')
local twist = dofile(mapgen_rivers.modpath .. '/terrainlib_lua/twist.lua')
local EvolutionModel = dofile(modpath .. '/terrainlib_lua/erosion.lua') local blocksize = mapgen_rivers.settings.blocksize
local twist = dofile(modpath .. '/terrainlib_lua/twist.lua') local tectonic_speed = mapgen_rivers.settings.tectonic_speed
local blocksize = tonumber(mapgen_rivers.settings:get("blocksize")) local np_base = table.copy(mapgen_rivers.noise_params.base)
local tectonic_speed = tonumber(mapgen_rivers.settings:get("tectonic_speed"))
local np_base = mapgen_rivers.settings:get_np_group("np_base") local evol_params = mapgen_rivers.settings.evol_params
np_base.spread.x = np_base.spread.x / blocksize
np_base.spread.y = np_base.spread.y / blocksize
np_base.spread.z = np_base.spread.z / blocksize
local evol_params = { local time = mapgen_rivers.settings.evol_time
K = tonumber(mapgen_rivers.settings:get("river_erosion_coef")), local time_step = mapgen_rivers.settings.evol_time_step
m = tonumber(mapgen_rivers.settings:get("river_erosion_power")),
d = tonumber(mapgen_rivers.settings:get("difusive_erosion")),
}
local time = tonumber(mapgen_rivers.settings:get("evol_time"))
local time_step = tonumber(mapgen_rivers.settings:get("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
-- Setup the model local use_margin = mapgen_rivers.settings.margin
local size = { local margin_width = mapgen_rivers.settings.margin_width / blocksize
x = tonumber(mapgen_rivers.settings:get("grid_x_size")), local margin_elev = mapgen_rivers.settings.margin_elev
y = tonumber(mapgen_rivers.settings:get("grid_z_size")),
}
local seed = tonumber(minetest.get_mapgen_setting("seed")) local function margin(dem, width, elev)
np_base.seed = (np_base.seed or 0) + seed 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 nobj_base = PerlinNoiseMap(np_base, {x=size.x, y=1, z=size.y}) local function pregenerate(keep_loaded)
local grid = mapgen_rivers.grid
local size = grid.size
local dem = nobj_base:get_3d_map_flat({x=0, y=0, z=0}) local seed = tonumber(minetest.get_mapgen_setting("seed"))
dem.X = size.x np_base.seed = (np_base.seed or 0) + seed
dem.Y = size.y
local model = EvolutionModel(evol_params) local nobj_base = PerlinNoiseMap(np_base, {x=size.x, y=1, z=size.y})
model.dem = dem
local ref_dem = model:define_isostasy(dem)
local tectonic_step = tectonic_speed * time_step local dem = nobj_base:get_3d_map_flat({x=0, y=0, z=0})
collectgarbage() dem.X = size.x
dem.Y = size.y
-- Run the model if use_margin then
for i=1, niter do margin(dem, margin_width, margin_elev)
end
local model = EvolutionModel(evol_params)
model.dem = dem
local ref_dem = model:define_isostasy(dem)
local tectonic_step = tectonic_speed * time_step
collectgarbage()
for i=1, niter do
minetest.log("info", "[mapgen_rivers] Iteration " .. i .. " of " .. niter) minetest.log("info", "[mapgen_rivers] Iteration " .. i .. " of " .. niter)
model:diffuse(time_step) model:diffuse(time_step)
@ -54,78 +77,44 @@ for i=1, niter do
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
collectgarbage() collectgarbage()
end end
model:flow() model:flow()
local mfloor = math.floor local mfloor = math.floor
local mmin, mmax = math.min, math.max local mmin, mmax = math.min, math.max
local offset_x, offset_y = twist(model.dirs, model.rivers, 5) local offset_x, offset_y = twist(model.dirs, model.rivers, 5)
for i=1, size.x*size.y do for i=1, size.x*size.y do
offset_x[i] = mmin(mmax(offset_x[i]*256, -128), 127) offset_x[i] = mmin(mmax(offset_x[i]*256, -128), 127)
offset_y[i] = mmin(mmax(offset_y[i]*256, -128), 127) offset_y[i] = mmin(mmax(offset_y[i]*256, -128), 127)
end
-- Write the results in the world directory
local datapath = minetest.get_worldpath() .. "/river_data/"
minetest.mkdir(datapath)
local function write_map(filename, data, bytes)
local size = #data
local file = io.open(datapath .. filename, 'wb')
local bytelist = {}
for j=1, bytes do
bytelist[j] = 0
end end
local unpk = unpack mapgen_rivers.write_map('dem', model.dem, 2)
local schar = string.char mapgen_rivers.write_map('lakes', model.lakes, 2)
local floor = math.floor mapgen_rivers.write_map('dirs', model.dirs, 1)
for i=1, size do mapgen_rivers.write_map('rivers', model.rivers, 4)
local n = floor(data[i]) mapgen_rivers.write_map('offset_x', offset_x, 1)
data[i] = n mapgen_rivers.write_map('offset_y', offset_y, 1)
for j=bytes, 2, -1 do local sfile = io.open(mapgen_rivers.world_data_path .. 'size', "w")
bytelist[j] = n % 256 sfile:write(size.x..'\n'..size.y)
n = floor(n / 256) sfile:close()
if keep_loaded then
grid.dem = model.dem
grid.lakes = model.lakes
grid.dirs = model.dirs
grid.rivers = model.rivers
grid.offset_x = offset_x
grid.offset_y = offset_y
end end
bytelist[1] = n % 256 collectgarbage()
file:write(schar(unpk(bytelist)))
end
file:close()
end end
write_map('dem', model.dem, 2) return pregenerate
write_map('lakes', model.lakes, 2)
write_map('dirs', model.dirs, 1)
write_map('rivers', model.rivers, 4)
write_map('offset_x', offset_x, 1)
write_map('offset_y', offset_y, 1)
local sfile = io.open(datapath .. 'size', "w")
sfile:write(size.x..'\n'..size.y)
sfile:close()
local use_interactive_loader
if minetest.settings:has("mapgen_rivers_use_interactive_loader") then
use_interactive_loader = minetest.settings:get_bool("mapgen_rivers_use_interactive_loader")
else
use_interactive_loader = not minetest.settings:get_bool("mapgen_rivers_load_all")
end
if not use_interactive_loader then
mapgen_rivers.grid = {
size = size,
dem = model.dem,
lakes = model.lakes,
dirs = model.dirs,
rivers = model.rivers,
offset_x = offset_x,
offset_y = offset_y,
}
end

View File

@ -1,11 +1,10 @@
local mtsettings = minetest.settings local mtsettings = minetest.settings
local settings = Settings(minetest.get_worldpath() .. '/mapgen_rivers.conf') local mgrsettings = Settings(minetest.get_worldpath() .. '/mapgen_rivers.conf')
mapgen_rivers.version = "1.0.2" mapgen_rivers.version = "1.0.1"
mapgen_rivers.settings = settings
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 = settings:get("version") or "0.0" local previous_version_mgr = mgrsettings:get("version") or "0.0"
if mapgen_rivers.version ~= previous_version_mt or mapgen_rivers.version ~= previous_version_mgr then if mapgen_rivers.version ~= previous_version_mt or mapgen_rivers.version ~= previous_version_mgr then
local compat_mt, compat_mgr = dofile(minetest.get_modpath(minetest.get_current_modname()) .. "/compatibility.lua") local compat_mt, compat_mgr = dofile(minetest.get_modpath(minetest.get_current_modname()) .. "/compatibility.lua")
@ -13,103 +12,87 @@ if mapgen_rivers.version ~= previous_version_mt or mapgen_rivers.version ~= prev
compat_mt(mtsettings) compat_mt(mtsettings)
end end
if mapgen_rivers.version ~= previous_version_mgr then if mapgen_rivers.version ~= previous_version_mgr then
compat_mgr(settings) compat_mgr(mgrsettings)
end end
end end
mtsettings:set("mapgen_rivers_version", mapgen_rivers.version) mtsettings:set("mapgen_rivers_version", mapgen_rivers.version)
settings:set("version", mapgen_rivers.version) mgrsettings:set("version", mapgen_rivers.version)
local modified = false function mapgen_rivers.define_setting(name, dtype, default)
local function verify_setting(name, dtype, default)
if settings:has(name) then
return
end
modified = true
local v = default
local mtname = 'mapgen_rivers_' .. name
local mthas = mtsettings:has(mtname)
if dtype == "number" or dtype == "string" then if dtype == "number" or dtype == "string" then
if mthas then local v = mgrsettings:get(name)
v = mtsettings:get(mtname) if v == nil then
if dtype == "number" and tonumber(v) == nil then v = mtsettings:get('mapgen_rivers_' .. name)
if v == nil then
v = default v = default
end end
mgrsettings:set(name, v)
end
if dtype == "number" then
return tonumber(v)
else
return v
end end
settings:set(name, v)
elseif dtype == "bool" then elseif dtype == "bool" then
if mthas then local v = mgrsettings:get_bool(name)
v = mtsettings:get(mtname) if v == nil then
v = mtsettings:get_bool('mapgen_rivers_' .. name)
if v == nil then
v = default
end end
settings:set_bool(name, v) mgrsettings:set_bool(name, v)
end
return v
elseif dtype == "noise" then elseif dtype == "noise" then
if mthas then local v = mgrsettings:get_np_group(name)
v = mtsettings:get_np_group(mtname) if v == nil then
v = mtsettings:get_np_group('mapgen_rivers_' .. name)
if v == nil then
v = default
end end
settings:set_np_group(name, v) mgrsettings:set_np_group(name, v)
end
return v
end end
end end
verify_setting('center', 'bool', true) local def_setting = mapgen_rivers.define_setting
verify_setting('blocksize', 'number', 15)
verify_setting('min_catchment', 'number', 3600)
verify_setting('river_widening_power', 'number', 0.5)
verify_setting('riverbed_slope', 'number', 0.4)
verify_setting('distort', 'bool', true)
verify_setting('biomes', 'bool', true)
verify_setting('glaciers', 'bool', false)
verify_setting('glacier_factor', 'number', 8)
verify_setting('elevation_chill', 'number', 0.25)
verify_setting('grid_x_size', 'number', 1000)
verify_setting('grid_z_size', 'number', 1000)
verify_setting('river_erosion_coef', 'number', 0.5)
verify_setting('river_erosion_power', 'number', 0.4)
verify_setting('diffusive_erosion', 'number', 0.5)
verify_setting('compensation_radius', 'number', 50)
verify_setting('tectonic_speed', 'number', 70)
verify_setting('evol_time', 'number', 10)
verify_setting('evol_time_step', 'number', 1)
verify_setting('np_base', 'noise', { mapgen_rivers.settings = {
offset = 0, center = def_setting('center', 'bool', true),
scale = 300, blocksize = def_setting('blocksize', 'number', 15),
seed = 2469, sea_level = tonumber(minetest.get_mapgen_setting('water_level')),
octaves = 8, min_catchment = def_setting('min_catchment', 'number', 3600),
spread = {x=2048, y=2048, z=2048}, river_widening_power = def_setting('river_widening_power', 'number', 0.5),
persist = 0.6, riverbed_slope = def_setting('riverbed_slope', 'number', 0.4),
lacunarity = 2, distort = def_setting('distort', 'bool', true),
flags = "eased", biomes = def_setting('biomes', 'bool', true),
}) glaciers = def_setting('glaciers', 'bool', false),
verify_setting('np_distort_x', 'noise', { glacier_factor = def_setting('glacier_factor', 'number', 8),
offset = 0, elevation_chill = def_setting('elevation_chill', 'number', 0.25),
scale = 1,
seed = -4574,
spread = {x=64, y=32, z=64},
octaves = 3,
persistence = 0.75,
lacunarity = 2,
})
verify_setting('np_distort_z', 'noise', {
offset = 0,
scale = 1,
seed = -7940,
spread = {x=64, y=32, z=64},
octaves = 3,
persistence = 0.75,
lacunarity = 2,
})
verify_setting('np_distort_amplitude', 'noise', {
offset = 0,
scale = 10,
seed = 676,
spread = {x=1024, y=1024, z=1024},
octaves = 5,
persistence = 0.5,
lacunarity = 2,
flags = "absvalue",
})
if modified then grid_x_size = def_setting('grid_x_size', 'number', 1000),
settings:write() 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 = {
K = def_setting('river_erosion_coef', 'number', 0.5),
m = def_setting('river_erosion_power', 'number', 0.4),
d = def_setting('diffusive_erosion', 'number', 0.5),
compensation_radius = def_setting('compensation_radius', 'number', 50),
},
tectonic_speed = def_setting('tectonic_speed', 'number', 70),
evol_time = def_setting('evol_time', 'number', 10),
evol_time_step = def_setting('evol_time_step', 'number', 1),
load_all = mtsettings:get_bool('mapgen_rivers_load_all')
}
local function write_settings()
mgrsettings:write()
end end
minetest.register_on_mods_loaded(write_settings)
minetest.register_on_shutdown(write_settings)

View File

@ -17,6 +17,16 @@ 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

View File

@ -1,13 +1,7 @@
-- erosion.lua -- erosion.lua
-- This is the main file of terrainlib_lua. It registers the EvolutionModel object and some of the
local function erode(model, time) local function erode(model, time)
-- Apply river erosion on the model --local tinsert = table.insert
-- Erosion model is based on the simplified version of the stream-power law Ey = K×A^m×S
-- where Ey is the vertical erosion speed, A catchment area of the river, S slope along the river, m and K local constants.
-- It is equivalent to considering a horizontal erosion wave travelling at Ex = K×A^m, and this latter approach allows much greather time steps so it is used here.
-- For each point, instead of moving upstream and see what point the erosion wave would reach, we move downstream and see from which point the erosion wave would reach the given point, then we can set the elevation.
local mmin, mmax = math.min, math.max local mmin, mmax = math.min, math.max
local dem = model.dem local dem = model.dem
local dirs = model.dirs local dirs = model.dirs
@ -28,9 +22,9 @@ local function erode(model, time)
if scalars then if scalars then
for i=1, X*Y do for i=1, X*Y do
local etime = 1 / (K*rivers[i]^m) -- Inverse of erosion speed (Ex); time needed for the erosion wave to move through the river section. local etime = 1 / (K*rivers[i]^m)
erosion_time[i] = etime erosion_time[i] = etime
lakes[i] = mmax(lakes[i], dem[i], sea_level) -- Use lake/sea surface if higher than ground level, because rivers can not erode below. lakes[i] = mmax(lakes[i], dem[i], sea_level)
end end
else else
for i=1, X*Y do for i=1, X*Y do
@ -45,12 +39,10 @@ local function erode(model, time)
local remaining = time local remaining = time
local new_elev local new_elev
while true do while true do
-- Explore downstream until we find the point 'iw' from which the erosion wave will reach 'i'
local inext = iw local inext = iw
local d = dirs[iw] local d = dirs[iw]
-- Follow the river downstream (move 'iw') if d == 0 then
if d == 0 then -- If no flow direction, we reach the border of the map: set elevation to the latest node's elev and abort.
new_elev = lakes[iw] new_elev = lakes[iw]
break break
elseif d == 1 then elseif d == 1 then
@ -64,13 +56,13 @@ local function erode(model, time)
end end
local etime = erosion_time[iw] local etime = erosion_time[iw]
if remaining <= etime then -- We have found the node from which the erosion wave will take 'time' to arrive to 'i'. if remaining <= etime then
local c = remaining / etime local c = remaining / etime
new_elev = (1-c) * lakes[iw] + c * lakes[inext] -- Interpolate linearly between the two nodes new_elev = (1-c) * lakes[iw] + c * lakes[inext]
break break
end end
remaining = remaining - etime -- If we still don't reach the target time, decrement time and move to next point. remaining = remaining - etime
iw = inext iw = inext
end end
@ -79,13 +71,10 @@ local function erode(model, time)
end end
local function diffuse(model, time) local function diffuse(model, time)
-- Apply diffusion using finite differences methods
-- Adapted for small radiuses
local mmax = math.max local mmax = math.max
local dem = model.dem local dem = model.dem
local X, Y = dem.X, dem.Y local X, Y = dem.X, dem.Y
local d = model.params.d local d = model.params.d
-- 'd' is equal to 4 times the diffusion coefficient
local dmax = d local dmax = d
if type(d) == "table" then if type(d) == "table" then
dmax = -math.huge dmax = -math.huge
@ -95,8 +84,6 @@ local function diffuse(model, time)
end end
local diff = dmax * time local diff = dmax * time
-- diff should never exceed 1 per iteration.
-- If needed, we will divide the process in enough iterations so that 'ddiff' is below 1.
local niter = math.floor(diff) + 1 local niter = math.floor(diff) + 1
local ddiff = diff / niter local ddiff = diff / niter
@ -109,7 +96,6 @@ local function diffuse(model, time)
for x=1, X do for x=1, X do
local iW = (x==1) and 0 or -1 local iW = (x==1) and 0 or -1
local iE = (x==X) and 0 or 1 local iE = (x==X) and 0 or 1
-- Laplacian Δdem × 1/4
temp[i] = (dem[i+iN]+dem[i+iE]+dem[i+iS]+dem[i+iW])*0.25 - dem[i] temp[i] = (dem[i+iN]+dem[i+iE]+dem[i+iS]+dem[i+iW])*0.25 - dem[i]
i = i + 1 i = i + 1
end end
@ -119,11 +105,16 @@ local function diffuse(model, time)
dem[i] = dem[i] + temp[i]*ddiff dem[i] = dem[i] + temp[i]*ddiff
end end
end end
-- TODO Test this
end end
local modpath = "" local modpath = ""
if minetest then if minetest then
modpath = minetest.get_modpath(minetest.get_current_modname()) .. "/terrainlib_lua/" if minetest.global_exists('mapgen_rivers') then
modpath = mapgen_rivers.modpath .. "terrainlib_lua/"
else
modpath = minetest.get_modpath(minetest.get_current_modname()) .. "terrainlib_lua/"
end
end end
local rivermapper = dofile(modpath .. "rivermapper.lua") local rivermapper = dofile(modpath .. "rivermapper.lua")
@ -135,7 +126,6 @@ local function flow(model)
end end
local function uplift(model, time) local function uplift(model, time)
-- Raises the terrain according to uplift rate (model.params.uplift)
local dem = model.dem local dem = model.dem
local X, Y = dem.X, dem.Y local X, Y = dem.X, dem.Y
local uplift_rate = model.params.uplift local uplift_rate = model.params.uplift
@ -152,7 +142,6 @@ local function uplift(model, time)
end end
local function noise(model, time) local function noise(model, time)
-- Adds noise to the terrain according to noise depth (model.params.noise)
local random = math.random local random = math.random
local dem = model.dem local dem = model.dem
local noise_depth = model.params.noise * 2 * time local noise_depth = model.params.noise * 2 * time
@ -162,12 +151,6 @@ local function noise(model, time)
end end
end end
-- Isostasy
-- This is the geological phenomenon that makes the lithosphere "float" over the underlying layers.
-- One of the key implications is that when a very large mass is removed from the ground, the lithosphere reacts by moving upward. This compensation only occurs at large scale (as the lithosphere is not flexible enough for small scale adjustments) so the implementation is using a very large-window Gaussian blur of the elevation array.
-- This implementation is quite simplistic, it does not do a mass balance of the lithosphere as this would introduce too many parameters. Instead, it defines a reference equilibrium elevation, and the ground will react toward this elevation (at the scale of the gaussian window).
-- A change in reference isostasy during the run can also be used to simulate tectonic forcing, like making a new mountain range appear.
local function define_isostasy(model, ref, link) local function define_isostasy(model, ref, link)
ref = ref or model.dem ref = ref or model.dem
if link then if link then
@ -185,20 +168,18 @@ local function define_isostasy(model, ref, link)
return ref2 return ref2
end end
-- Apply isostasy
local function isostasy(model) local function isostasy(model)
local dem = model.dem local dem = model.dem
local X, Y = dem.X, dem.Y local X, Y = dem.X, dem.Y
local temp = {X=X, Y=Y} local temp = {X=X, Y=Y}
local ref = model.isostasy_ref local ref = model.isostasy_ref
for i=1, X*Y do for i=1, X*Y do
temp[i] = ref[i] - dem[i] -- Compute the difference between the ground level and the target level temp[i] = ref[i] - dem[i]
end end
-- Blur the difference map using Gaussian blur
gaussian.gaussian_blur_approx(temp, model.params.compensation_radius, 4) gaussian.gaussian_blur_approx(temp, model.params.compensation_radius, 4)
for i=1, X*Y do for i=1, X*Y do
dem[i] = dem[i] + temp[i] -- Apply the difference dem[i] = dem[i] + temp[i]
end end
end end

View File

@ -71,6 +71,20 @@ local function box_blur_2d(map1, size, map2)
return map1 return map1
end end
--[[local function gaussian_blur(map, std, tail)
local exp = math.exp
local kernel_mid = math.ceil(std*tail) + 1
local kernel_size = kernel_mid * 2 - 1
local kernel = {}
local cst1 = 1/(std*(2*math.pi)^0.5)
local cst2 = -1/(2*std^2)
for i=1, kernel_size do
kernel[i] = cst1 * exp((i-kernel_mid)^2 * cst2)
end
]]
local function gaussian_blur_approx(map, sigma, n, map2) local function gaussian_blur_approx(map, sigma, n, map2)
map2 = map2 or {} map2 = map2 or {}
local sizes = get_box_size(sigma, n) local sizes = get_box_size(sigma, n)

View File

@ -9,26 +9,24 @@
-- --
-- Big thanks to them for releasing this paper under a free license ! :) -- 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 Borůvka algorithm. -- 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. -- Only flow_local and accumulate_flow are custom algorithms.
local function flow_local_semirandom(plist) local function flow_local_semirandom(plist)
-- Determines how water should flow at 1 node scale.
-- The straightforward approach would be "Water will flow to the lowest of the 4 neighbours", but here water flows to one of the lower neighbours, chosen randomly, but probability depends on height difference.
-- This makes rivers better follow the curvature of the topography at large scale, and be less biased by pure N/E/S/W directions.
-- 'plist': array of downward height differences (0 if upward)
local sum = 0 local sum = 0
for i=1, #plist do for i=1, #plist do
sum = sum + plist[i] -- Sum of probabilities sum = sum + plist[i]
end end
--for _, p in ipairs(plist) do
--sum = sum + p
--end
if sum == 0 then if sum == 0 then
return 0 return 0
end end
local r = math.random() * sum local r = math.random() * sum
for i=1, #plist do for i=1, #plist do
local p = plist[i] local p = plist[i]
--for i, p in ipairs(plist) do
if r < p then if r < p then
return i return i
end end
@ -37,14 +35,11 @@ local function flow_local_semirandom(plist)
return 0 return 0
end end
-- Maybe implement more flow methods in the future?
local flow_methods = { local flow_methods = {
semirandom = flow_local_semirandom, semirandom = flow_local_semirandom,
} }
-- Applies all steps of the flow routing, to calculate flow direction for every node, and lake surface elevation. local function flow_routing(dem, dirs, lakes, method)
-- It's quite a hard piece of code, but we will go step by step and explain what's going on, so stay with me and... let's goooooooo!
local function flow_routing(dem, dirs, lakes, method) -- 'dirs' and 'lakes' are optional tables to reuse for memory optimization, they may contain any data.
method = method or 'semirandom' method = method or 'semirandom'
local flow_local = flow_methods[method] or flow_local_semirandom local flow_local = flow_methods[method] or flow_local_semirandom
@ -52,6 +47,7 @@ local function flow_routing(dem, dirs, lakes, method) -- 'dirs' and 'lakes' are
lakes = lakes or {} lakes = lakes or {}
-- Localize for performance -- Localize for performance
--local tinsert = table.insert
local tremove = table.remove local tremove = table.remove
local mmax = math.max local mmax = math.max
@ -66,15 +62,11 @@ local function flow_routing(dem, dirs, lakes, method) -- 'dirs' and 'lakes' are
dirs2[i] = 0 dirs2[i] = 0
end end
----------------------------------------
-- STEP 1: Find local flow directions --
----------------------------------------
-- Use the local flow function and fill the flow direction tables
local singular = {} local singular = {}
for y=1, Y do for y=1, Y do
for x=1, X do for x=1, X do
local zi = dem[i] local zi = dem[i]
local plist = { -- Get the height difference of the 4 neighbours (and 0 if uphill) local plist = {
y<Y and mmax(zi-dem[i+X], 0) or 0, -- Southward y<Y and mmax(zi-dem[i+X], 0) or 0, -- Southward
x<X and mmax(zi-dem[i+1], 0) or 0, -- Eastward x<X and mmax(zi-dem[i+1], 0) or 0, -- Eastward
y>1 and mmax(zi-dem[i-X], 0) or 0, -- Northward y>1 and mmax(zi-dem[i-X], 0) or 0, -- Northward
@ -82,10 +74,8 @@ local function flow_routing(dem, dirs, lakes, method) -- 'dirs' and 'lakes' are
} }
local d = flow_local(plist) local d = flow_local(plist)
-- 'dirs': Direction toward which water flow
-- 'dirs2': Directions from which water comes
dirs[i] = d dirs[i] = d
if d == 0 then -- If water can't flow from this node, add it to the list of singular nodes that will be resolved later if d == 0 then
singular[#singular+1] = i singular[#singular+1] = i
elseif d == 1 then elseif d == 1 then
dirs2[i+X] = dirs2[i+X] + 1 dirs2[i+X] = dirs2[i+X] + 1
@ -100,39 +90,30 @@ local function flow_routing(dem, dirs, lakes, method) -- 'dirs' and 'lakes' are
end end
end end
-------------------------------------- -- Compute basins and links
-- STEP 2: Compute basins and links --
--------------------------------------
-- Now water can flow until it reaches a singular node (which is in most cases the bottom of a depression)
-- We will calculate the drainage basin of every singular node (all the nodes from which the water will flow in this singular node, directly or indirectly), make an adjacency list of basins, and find the lowest pass between each pair of adjacent basins (they are potential lake outlets)
local nbasins = #singular local nbasins = #singular
local basin_id = {} local basin_id = {}
local links = {} local links = {}
local basin_links local basin_links
-- Function to analyse a link between two nodes
local function add_link(i1, i2, b1, isY) local function add_link(i1, i2, b1, isY)
-- i1, i2: coordinates of two nodes
-- b1: basin that contains i1
-- isY: whether the link is in Y direction
local b2 local b2
-- Note that basin number #0 represents the outside of the map; or if the coordinate is inside the map, means that the basin number is uninitialized. if i2 == 0 then
if i2 == 0 then -- If outside the map
b2 = 0 b2 = 0
else else
b2 = basin_id[i2] b2 = basin_id[i2]
if b2 == 0 then -- If basin of i2 is not already computed, skip if b2 == 0 then
return return
end end
end end
if b2 ~= b1 then -- If these two nodes don't belong to the same basin, we have found a link between two adjacent basins if b2 ~= b1 then
local elev = i2 == 0 and dem[i1] or mmax(dem[i1], dem[i2]) -- Elevation of the highest of the two sides of the link (or only i1 if b2 is map outside) local elev = i2 == 0 and dem[i1] or mmax(dem[i1], dem[i2])
local l2 = basin_links[b2] local l2 = basin_links[b2]
if not l2 then if not l2 then
l2 = {} l2 = {}
basin_links[b2] = l2 basin_links[b2] = l2
end end
if not l2.elev or l2.elev > elev then -- If this link is lower than the lowest registered link between these two basins, register it as the new lowest pass if not l2.elev or l2.elev > elev then
l2.elev = elev l2.elev = elev
l2.i = mmax(i1,i2) l2.i = mmax(i1,i2)
l2.is_y = isY l2.is_y = isY
@ -145,48 +126,51 @@ local function flow_routing(dem, dirs, lakes, method) -- 'dirs' and 'lakes' are
for i=1, X*Y do for i=1, X*Y do
basin_id[i] = 0 basin_id[i] = 0
end end
--for ib, s in ipairs(singular) do
for ib=1, nbasins do for ib=1, nbasins do
-- Here we will recursively search upstream from the singular node to determine its drainage basin --local s = singular[ib]
local queue = {singular[ib]} -- Start with the singular node, then this queue will be filled with water donors neighbours local queue = {singular[ib]}
basin_links = {} basin_links = {}
links[#links+1] = basin_links links[#links+1] = basin_links
--tinsert(links, basin_links)
while #queue > 0 do while #queue > 0 do
local i = tremove(queue) local i = tremove(queue)
basin_id[i] = ib basin_id[i] = ib
local d = dirs2[i] -- Get the directions water is coming from local d = dirs2[i]
-- Iterate through the 4 directions if d >= 8 then -- River coming from East
if d >= 8 then -- River coming from the East
d = d - 8 d = d - 8
queue[#queue+1] = i+1 queue[#queue+1] = i+1
-- If no river is coming from the East, we might be at the limit of two basins, thus we need to test adjacency. --tinsert(queue, i+X)
elseif i%X > 0 then elseif i%X > 0 then
add_link(i, i+1, ib, false) add_link(i, i+1, ib, false)
else -- If the eastern neighbour is outside the map else
add_link(i, 0, ib, false) add_link(i, 0, ib, false)
end end
if d >= 4 then -- River coming from the South if d >= 4 then -- River coming from South
d = d - 4 d = d - 4
queue[#queue+1] = i+X queue[#queue+1] = i+X
--tinsert(queue, i+1)
elseif i <= X*(Y-1) then elseif i <= X*(Y-1) then
add_link(i, i+X, ib, true) add_link(i, i+X, ib, true)
else else
add_link(i, 0, ib, true) add_link(i, 0, ib, true)
end end
if d >= 2 then -- River coming from the West if d >= 2 then -- River coming from West
d = d - 2 d = d - 2
queue[#queue+1] = i-1 queue[#queue+1] = i-1
--tinsert(queue, i-X)
elseif i%X ~= 1 then elseif i%X ~= 1 then
add_link(i, i-1, ib, false) add_link(i, i-1, ib, false)
else else
add_link(i, 0, ib, false) add_link(i, 0, ib, false)
end end
if d >= 1 then -- River coming from the North if d >= 1 then -- River coming from North
queue[#queue+1] = i-X queue[#queue+1] = i-X
--tinsert(queue, i-1)
elseif i > X then elseif i > X then
add_link(i, i-X, ib, true) add_link(i, i-X, ib, true)
else else
@ -202,7 +186,7 @@ local function flow_routing(dem, dirs, lakes, method) -- 'dirs' and 'lakes' are
nlinks[i] = 0 nlinks[i] = 0
end end
-- Iterate through pairs of adjacent basins, and make the links reciprocal --for ib1, blinks in ipairs(links) do
for ib1=1, #links do for ib1=1, #links do
for ib2, link in pairs(links[ib1]) do for ib2, link in pairs(links[ib1]) do
if ib2 < ib1 then if ib2 < ib1 then
@ -213,15 +197,6 @@ local function flow_routing(dem, dirs, lakes, method) -- 'dirs' and 'lakes' are
end end
end end
-----------------------------------------------------
-- STEP 3: Compute minimal spanning tree of basins --
-----------------------------------------------------
-- We've got an adjacency list of basins with the elevation of their links.
-- We will build a minimal spanning tree of the basins (where costs are the elevation of the links). As demonstrated by Cordonnier et al., this finds the outlets of the basins, where water would naturally flow. This does not tell in which direction water is flowing, however.
-- We will use a version of Borůvka's algorithm, with Mareš' optimizations to approach linear complexity (see paper).
-- The concept of Borůvka's algorithm is to take elements and merge them with their lowest neighbour, until all elements are merged.
-- Mareš' optimizations mainly consist in skipping elements that have over 8 links, until extra links are removed when other elements are merged.
-- Note that for this step we are only working on basins, not grid nodes.
local lowlevel = {} local lowlevel = {}
for i, n in pairs(nlinks) do for i, n in pairs(nlinks) do
if n <= 8 then if n <= 8 then
@ -231,8 +206,6 @@ local function flow_routing(dem, dirs, lakes, method) -- 'dirs' and 'lakes' are
local basin_graph = {} local basin_graph = {}
for n=1, nbasins do for n=1, nbasins do
-- Iterate in lowlevel but its contents may change during the loop
-- 'next' called with only one argument always returns an element if table is not empty
local b1, lnk1 = next(lowlevel) local b1, lnk1 = next(lowlevel)
lowlevel[b1] = nil lowlevel[b1] = nil
@ -240,7 +213,6 @@ local function flow_routing(dem, dirs, lakes, method) -- 'dirs' and 'lakes' are
local lowest = math.huge local lowest = math.huge
local lnk1 = links[b1] local lnk1 = links[b1]
local i = 0 local i = 0
-- Look for lowest link
for bn, bdata in pairs(lnk1) do for bn, bdata in pairs(lnk1) do
i = i + 1 i = i + 1
if bdata.elev < lowest then if bdata.elev < lowest then
@ -249,7 +221,7 @@ local function flow_routing(dem, dirs, lakes, method) -- 'dirs' and 'lakes' are
end end
end end
-- Add link to the graph, in both directions -- Add link to the graph
local bound = lnk1[b2] local bound = lnk1[b2]
local bb1, bb2 = bound[1], bound[2] local bb1, bb2 = bound[1], bound[2]
if not basin_graph[bb1] then if not basin_graph[bb1] then
@ -267,7 +239,6 @@ local function flow_routing(dem, dirs, lakes, method) -- 'dirs' and 'lakes' are
lnk1[b2] = nil lnk1[b2] = nil
lnk2[b1] = nil lnk2[b1] = nil
nlinks[b2] = nlinks[b2] - 1 nlinks[b2] = nlinks[b2] - 1
-- When the number of links is changing, we need to check whether the basin can be added to / removed from 'lowlevel'
if nlinks[b2] == 8 then if nlinks[b2] == 8 then
lowlevel[b2] = lnk2 lowlevel[b2] = lnk2
end end
@ -276,32 +247,25 @@ local function flow_routing(dem, dirs, lakes, method) -- 'dirs' and 'lakes' are
local lnkn = links[bn] local lnkn = links[bn]
lnkn[b1] = nil lnkn[b1] = nil
if lnkn[b2] then -- If bassin bn is also linked to b2 if lnkn[b2] then
nlinks[bn] = nlinks[bn] - 1 -- Then bassin bn is losing a link because it keeps only one link toward b1/b2 after the merge nlinks[bn] = nlinks[bn] - 1
if nlinks[bn] == 8 then if nlinks[bn] == 8 then
lowlevel[bn] = lnkn lowlevel[bn] = lnkn
end end
else -- If bn was linked to b1 but not to b2 else
nlinks[b2] = nlinks[b2] + 1 -- Then b2 is gaining a link to bn because of the merge nlinks[b2] = nlinks[b2] + 1
if nlinks[b2] == 9 then if nlinks[b2] == 9 then
lowlevel[b2] = nil lowlevel[b2] = nil
end end
end end
if not lnkn[b2] or lnkn[b2].elev > bdata.elev then -- If the link b1-bn will become the new lowest link between b2 and bn, redirect the link to b2 if not lnkn[b2] or lnkn[b2].elev > bdata.elev then
lnkn[b2] = bdata lnkn[b2] = bdata
lnk2[bn] = bdata lnk2[bn] = bdata
end end
end end
end end
--------------------------------------------------------------
-- STEP 4: Orient basin graph, and grid nodes inside basins --
--------------------------------------------------------------
-- We will finally solve those freaking singular nodes.
-- To orient the basin graph, we will consider that the ultimate basin water should flow into is the map outside (basin #0). We will start from it and recursively walk upstream to the neighbouring basins, using only links that are in the minimal spanning tree. This gives the flow direction of the links, and thus, the outlet of every basin.
-- This will also give lake elevation, which is the highest link encountered between map outside and the given basin on the spanning tree.
-- And within each basin, we need to modify flow directions to connect the singular node to the outlet.
local queue = {[0] = -math.huge} local queue = {[0] = -math.huge}
local basin_lake = {} local basin_lake = {}
for n=1, nbasins do for n=1, nbasins do
@ -309,17 +273,15 @@ local function flow_routing(dem, dirs, lakes, method) -- 'dirs' and 'lakes' are
end end
local reverse = {3, 4, 1, 2, [0]=0} local reverse = {3, 4, 1, 2, [0]=0}
for n=1, nbasins do for n=1, nbasins do
local b1, elev1 = next(queue) -- Pop from queue local b1, elev1 = next(queue)
queue[b1] = nil queue[b1] = nil
basin_lake[b1] = elev1 basin_lake[b1] = elev1
-- Iterate through b1's neighbours (according to the spanning tree)
for b2, bound in pairs(basin_graph[b1]) do for b2, bound in pairs(basin_graph[b1]) do
-- Make b2 flow into b1 -- Make b2 flow into b1
local i = bound.i -- Get the coordinate of the link (which is the basin's outlet) local i = bound.i
local dir = bound.is_y and 3 or 4 -- And get the direction (S/E/N/W) local dir = bound.is_y and 3 or 4
if basin_id[i] ~= b2 then if basin_id[i] ~= b2 then
dir = dir - 2 dir = dir - 2
-- Coordinate 'i' refers to the side of the link with the highest X/Y position. In case it is in the wrong basin, take the other side by decrementing by one row/column.
if bound.is_y then if bound.is_y then
i = i - X i = i - X
else else
@ -329,12 +291,8 @@ local function flow_routing(dem, dirs, lakes, method) -- 'dirs' and 'lakes' are
dir = 0 dir = 0
end end
-- Use the flow directions computed in STEP 2 to find the route from the outlet position to the singular node, and reverse this route to make the singular node flow into the outlet
-- This can make the river flow uphill, which may seem unnatural, but it can only happen below a lake (because outlet elevation defines lake surface elevation)
repeat repeat
-- Assign i's direction to 'dir', and get i's former direction
dir, dirs[i] = dirs[i], dir dir, dirs[i] = dirs[i], dir
-- Move i by following its former flow direction (downstream)
if dir == 1 then if dir == 1 then
i = i + X i = i + X
elseif dir == 2 then elseif dir == 2 then
@ -344,36 +302,26 @@ local function flow_routing(dem, dirs, lakes, method) -- 'dirs' and 'lakes' are
elseif dir == 4 then elseif dir == 4 then
i = i - 1 i = i - 1
end end
-- Reverse the flow direction for the next node, which will flow into i
dir = reverse[dir] dir = reverse[dir]
until dir == 0 -- Stop when reaching the singular node until dir == 0
-- Add b2 into the queue
-- Add basin b2 into the queue, and keep the highest link elevation, that will define the elevation of the lake in b2
queue[b2] = mmax(elev1, bound.elev) queue[b2] = mmax(elev1, bound.elev)
-- Remove b1 from b2's neighbours to avoid coming back to b1
basin_graph[b2][b1] = nil basin_graph[b2][b1] = nil
end end
basin_graph[b1] = nil basin_graph[b1] = nil
end end
-- Every node will be assigned the lake elevation of the basin it belongs to.
-- If lake elevation is lower than ground elevation, it simply means that there is no lake here.
for i=1, X*Y do for i=1, X*Y do
lakes[i] = basin_lake[basin_id[i]] lakes[i] = basin_lake[basin_id[i]]
end end
-- That's it!
return dirs, lakes return dirs, lakes
end end
local function accumulate(dirs, waterq) local function accumulate(dirs, waterq)
-- Calculates the river flow by determining the surface of the catchment area for every node
-- This means: how many nodes will give their water to that given node, directly or indirectly?
-- This is obtained by following rivers downstream and summing up the flow of every tributary, starting with a value of 1 at the sources.
-- This will give non-zero values for every node but only large values will be considered to be rivers.
waterq = waterq or {} waterq = waterq or {}
local X, Y = dirs.X, dirs.Y local X, Y = dirs.X, dirs.Y
--local tinsert = table.insert
local ndonors = {} local ndonors = {}
local waterq = {X=X, Y=Y} local waterq = {X=X, Y=Y}
@ -382,7 +330,7 @@ local function accumulate(dirs, waterq)
waterq[i] = 1 waterq[i] = 1
end end
-- Calculate the number of direct donors --for i1, dir in ipairs(dirs) do
for i1=1, X*Y do for i1=1, X*Y do
local i2 local i2
local dir = dirs[i1] local dir = dirs[i1]
@ -401,12 +349,10 @@ local function accumulate(dirs, waterq)
end end
for i1=1, X*Y do for i1=1, X*Y do
-- Find sources (nodes that have no donor)
if ndonors[i1] == 0 then if ndonors[i1] == 0 then
local i2 = i1 local i2 = i1
local dir = dirs[i2] local dir = dirs[i2]
local w = waterq[i2] local w = waterq[i2]
-- Follow the water flow downstream: move 'i2' to the next node according to its flow direction
while dir > 0 do while dir > 0 do
if dir == 1 then if dir == 1 then
i2 = i2 + X i2 = i2 + X
@ -417,12 +363,9 @@ local function accumulate(dirs, waterq)
elseif dir == 4 then elseif dir == 4 then
i2 = i2 - 1 i2 = i2 - 1
end end
-- Increment the water quantity of i2
w = w + waterq[i2] w = w + waterq[i2]
waterq[i2] = w waterq[i2] = w
-- Stop on an unresolved confluence (node with >1 donors) and decrease the number of remaining donors
-- When the ndonors of a confluence has decreased to 1, it means that its water quantity has already been incremented by its tributaries, so it can be resolved like a standard river section. However, do not decrease ndonors to zero to avoid considering it as a source.
if ndonors[i2] > 1 then if ndonors[i2] > 1 then
ndonors[i2] = ndonors[i2] - 1 ndonors[i2] = ndonors[i2] - 1
break break

View File

@ -1,4 +1,4 @@
-- twist.lua -- bounds.lua
local function get_bounds(dirs, rivers) local function get_bounds(dirs, rivers)
local X, Y = dirs.X, dirs.Y local X, Y = dirs.X, dirs.Y