mirror of
https://gitlab.com/gaelysam/mapgen_rivers.git
synced 2024-12-29 12:20:41 +01:00
Added more comments in terrainlib_lua
This commit is contained in:
parent
68c19c3b94
commit
b3d79eaaf8
@ -1,7 +1,13 @@
|
|||||||
-- 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)
|
||||||
--local tinsert = table.insert
|
-- Apply river erosion on the model
|
||||||
|
-- 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
|
||||||
@ -22,9 +28,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)
|
local etime = 1 / (K*rivers[i]^m) -- Inverse of erosion speed (Ex); time needed for the erosion wave to move through the river section.
|
||||||
erosion_time[i] = etime
|
erosion_time[i] = etime
|
||||||
lakes[i] = mmax(lakes[i], dem[i], sea_level)
|
lakes[i] = mmax(lakes[i], dem[i], sea_level) -- Use lake/sea surface if higher than ground level, because rivers can not erode below.
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
for i=1, X*Y do
|
for i=1, X*Y do
|
||||||
@ -39,10 +45,12 @@ 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]
|
||||||
|
|
||||||
if d == 0 then
|
-- Follow the river downstream (move 'iw')
|
||||||
|
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
|
||||||
@ -56,13 +64,13 @@ local function erode(model, time)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local etime = erosion_time[iw]
|
local etime = erosion_time[iw]
|
||||||
if remaining <= etime then
|
if remaining <= etime then -- We have found the node from which the erosion wave will take 'time' to arrive to 'i'.
|
||||||
local c = remaining / etime
|
local c = remaining / etime
|
||||||
new_elev = (1-c) * lakes[iw] + c * lakes[inext]
|
new_elev = (1-c) * lakes[iw] + c * lakes[inext] -- Interpolate linearly between the two nodes
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
|
||||||
remaining = remaining - etime
|
remaining = remaining - etime -- If we still don't reach the target time, decrement time and move to next point.
|
||||||
iw = inext
|
iw = inext
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -71,10 +79,13 @@ 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
|
||||||
@ -84,6 +95,8 @@ 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
|
||||||
|
|
||||||
@ -96,6 +109,7 @@ 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
|
||||||
@ -105,7 +119,6 @@ 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 = ""
|
||||||
@ -126,6 +139,7 @@ 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
|
||||||
@ -142,6 +156,7 @@ 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
|
||||||
@ -151,6 +166,12 @@ 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
|
||||||
@ -168,18 +189,20 @@ 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]
|
temp[i] = ref[i] - dem[i] -- Compute the difference between the ground level and the target level
|
||||||
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]
|
dem[i] = dem[i] + temp[i] -- Apply the difference
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -71,20 +71,6 @@ 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)
|
||||||
|
@ -9,24 +9,26 @@
|
|||||||
--
|
--
|
||||||
-- 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 Boruvka algorithm.
|
-- The algorithm here makes use of most of the paper's concepts, including the Planar Borůvka 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 = sum + plist[i] -- Sum of probabilities
|
||||||
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
|
||||||
@ -35,11 +37,14 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
local function flow_routing(dem, dirs, lakes, method)
|
-- Applies all steps of the flow routing, to calculate flow direction for every node, and lake surface elevation.
|
||||||
|
-- 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
|
||||||
|
|
||||||
@ -47,7 +52,6 @@ local function flow_routing(dem, dirs, lakes, method)
|
|||||||
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
|
||||||
|
|
||||||
@ -62,11 +66,15 @@ local function flow_routing(dem, dirs, lakes, method)
|
|||||||
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 = {
|
local plist = { -- Get the height difference of the 4 neighbours (and 0 if uphill)
|
||||||
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
|
||||||
@ -74,8 +82,10 @@ local function flow_routing(dem, dirs, lakes, method)
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 d == 0 then -- If water can't flow from this node, add it to the list of singular nodes that will be resolved later
|
||||||
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
|
||||||
@ -90,30 +100,39 @@ local function flow_routing(dem, dirs, lakes, method)
|
|||||||
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
|
||||||
if i2 == 0 then
|
-- 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 outside the map
|
||||||
b2 = 0
|
b2 = 0
|
||||||
else
|
else
|
||||||
b2 = basin_id[i2]
|
b2 = basin_id[i2]
|
||||||
if b2 == 0 then
|
if b2 == 0 then -- If basin of i2 is not already computed, skip
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if b2 ~= b1 then
|
if b2 ~= b1 then -- If these two nodes don't belong to the same basin, we have found a link between two adjacent basins
|
||||||
local elev = i2 == 0 and dem[i1] or mmax(dem[i1], dem[i2])
|
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 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 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
|
||||||
l2.elev = elev
|
l2.elev = elev
|
||||||
l2.i = mmax(i1,i2)
|
l2.i = mmax(i1,i2)
|
||||||
l2.is_y = isY
|
l2.is_y = isY
|
||||||
@ -126,51 +145,48 @@ local function flow_routing(dem, dirs, lakes, method)
|
|||||||
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
|
||||||
--local s = singular[ib]
|
-- Here we will recursively search upstream from the singular node to determine its drainage basin
|
||||||
local queue = {singular[ib]}
|
local queue = {singular[ib]} -- Start with the singular node, then this queue will be filled with water donors neighbours
|
||||||
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]
|
local d = dirs2[i] -- Get the directions water is coming from
|
||||||
|
|
||||||
if d >= 8 then -- River coming from East
|
-- Iterate through the 4 directions
|
||||||
|
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
|
||||||
--tinsert(queue, i+X)
|
-- If no river is coming from the East, we might be at the limit of two basins, thus we need to test adjacency.
|
||||||
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
|
else -- If the eastern neighbour is outside the map
|
||||||
add_link(i, 0, ib, false)
|
add_link(i, 0, ib, false)
|
||||||
end
|
end
|
||||||
|
|
||||||
if d >= 4 then -- River coming from South
|
if d >= 4 then -- River coming from the 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 West
|
if d >= 2 then -- River coming from the 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 North
|
if d >= 1 then -- River coming from the 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
|
||||||
@ -186,7 +202,7 @@ local function flow_routing(dem, dirs, lakes, method)
|
|||||||
nlinks[i] = 0
|
nlinks[i] = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
--for ib1, blinks in ipairs(links) do
|
-- Iterate through pairs of adjacent basins, and make the links reciprocal
|
||||||
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
|
||||||
@ -197,6 +213,15 @@ local function flow_routing(dem, dirs, lakes, method)
|
|||||||
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
|
||||||
@ -206,6 +231,8 @@ local function flow_routing(dem, dirs, lakes, method)
|
|||||||
|
|
||||||
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
|
||||||
|
|
||||||
@ -213,6 +240,7 @@ local function flow_routing(dem, dirs, lakes, method)
|
|||||||
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
|
||||||
@ -221,7 +249,7 @@ local function flow_routing(dem, dirs, lakes, method)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Add link to the graph
|
-- Add link to the graph, in both directions
|
||||||
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
|
||||||
@ -239,6 +267,7 @@ local function flow_routing(dem, dirs, lakes, method)
|
|||||||
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
|
||||||
@ -247,25 +276,32 @@ local function flow_routing(dem, dirs, lakes, method)
|
|||||||
local lnkn = links[bn]
|
local lnkn = links[bn]
|
||||||
lnkn[b1] = nil
|
lnkn[b1] = nil
|
||||||
|
|
||||||
if lnkn[b2] then
|
if lnkn[b2] then -- If bassin bn is also linked to b2
|
||||||
nlinks[bn] = nlinks[bn] - 1
|
nlinks[bn] = nlinks[bn] - 1 -- Then bassin bn is losing a link because it keeps only one link toward b1/b2 after the merge
|
||||||
if nlinks[bn] == 8 then
|
if nlinks[bn] == 8 then
|
||||||
lowlevel[bn] = lnkn
|
lowlevel[bn] = lnkn
|
||||||
end
|
end
|
||||||
else
|
else -- If bn was linked to b1 but not to b2
|
||||||
nlinks[b2] = nlinks[b2] + 1
|
nlinks[b2] = nlinks[b2] + 1 -- Then b2 is gaining a link to bn because of the merge
|
||||||
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 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
|
||||||
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
|
||||||
@ -273,15 +309,17 @@ local function flow_routing(dem, dirs, lakes, method)
|
|||||||
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)
|
local b1, elev1 = next(queue) -- Pop from 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
|
local i = bound.i -- Get the coordinate of the link (which is the basin's outlet)
|
||||||
local dir = bound.is_y and 3 or 4
|
local dir = bound.is_y and 3 or 4 -- And get the direction (S/E/N/W)
|
||||||
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
|
||||||
@ -291,8 +329,12 @@ local function flow_routing(dem, dirs, lakes, method)
|
|||||||
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
|
||||||
@ -302,26 +344,36 @@ local function flow_routing(dem, dirs, lakes, method)
|
|||||||
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
|
until dir == 0 -- Stop when reaching the singular node
|
||||||
-- 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}
|
||||||
@ -330,7 +382,7 @@ local function accumulate(dirs, waterq)
|
|||||||
waterq[i] = 1
|
waterq[i] = 1
|
||||||
end
|
end
|
||||||
|
|
||||||
--for i1, dir in ipairs(dirs) do
|
-- Calculate the number of direct donors
|
||||||
for i1=1, X*Y do
|
for i1=1, X*Y do
|
||||||
local i2
|
local i2
|
||||||
local dir = dirs[i1]
|
local dir = dirs[i1]
|
||||||
@ -349,10 +401,12 @@ 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
|
||||||
@ -363,9 +417,12 @@ 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
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
-- bounds.lua
|
-- twist.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
|
||||||
|
Loading…
Reference in New Issue
Block a user