mirror of
https://gitlab.com/gaelysam/mapgen_rivers.git
synced 2025-07-04 01:10:39 +02:00
Compare commits
11 Commits
terrainlib
...
margin
Author | SHA1 | Date | |
---|---|---|---|
b1f4437a91 | |||
a00c1cbd39 | |||
417ce1bcbc | |||
c3a798933f | |||
0c98fc0881 | |||
cb71f4400a | |||
f8f467ac3f | |||
2e29474686 | |||
27670addb3 | |||
54b94e6485 | |||
09de0fd298 |
10
README.md
10
README.md
@ -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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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).
|
||||||
|
|
||||||
|
13
geometry.lua
13
geometry.lua
@ -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)
|
||||||
|
@ -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
|
||||||
|
56
init.lua
56
init.lua
@ -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)
|
||||||
|
16
load.lua
16
load.lua
@ -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()
|
||||||
|
@ -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
|
||||||
|
41
polygons.lua
41
polygons.lua
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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),
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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, 549–562, 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
|
||||||
|
Reference in New Issue
Block a user