From 20ccf321ce3fcb69f3bbb459f6fe6e47f45a8bd9 Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Tue, 18 Aug 2020 02:11:37 +0100 Subject: [PATCH 1/8] Start setting things out to implement erosion First up, we're going to attempt to implement a snowballs-based algorithm. Reference: https://jobtalle.com/simulating_hydraulic_erosion.html --- worldeditadditions/init.lua | 2 +- .../lib/conv/{convolution.lua => conv.lua} | 0 worldeditadditions/lib/erode/erode.lua | 3 +++ worldeditadditions/lib/erode/snowballs.lua | 9 +++++++++ 4 files changed, 13 insertions(+), 1 deletion(-) rename worldeditadditions/lib/conv/{convolution.lua => conv.lua} (100%) create mode 100644 worldeditadditions/lib/erode/erode.lua create mode 100644 worldeditadditions/lib/erode/snowballs.lua diff --git a/worldeditadditions/init.lua b/worldeditadditions/init.lua index 7758048..8be8c1a 100644 --- a/worldeditadditions/init.lua +++ b/worldeditadditions/init.lua @@ -23,7 +23,7 @@ dofile(worldeditadditions.modpath.."/lib/walls.lua") dofile(worldeditadditions.modpath.."/lib/replacemix.lua") dofile(worldeditadditions.modpath.."/lib/maze2d.lua") dofile(worldeditadditions.modpath.."/lib/maze3d.lua") -dofile(worldeditadditions.modpath.."/lib/conv/convolution.lua") +dofile(worldeditadditions.modpath.."/lib/conv/conv.lua") dofile(worldeditadditions.modpath.."/lib/count.lua") diff --git a/worldeditadditions/lib/conv/convolution.lua b/worldeditadditions/lib/conv/conv.lua similarity index 100% rename from worldeditadditions/lib/conv/convolution.lua rename to worldeditadditions/lib/conv/conv.lua diff --git a/worldeditadditions/lib/erode/erode.lua b/worldeditadditions/lib/erode/erode.lua new file mode 100644 index 0000000..d30118d --- /dev/null +++ b/worldeditadditions/lib/erode/erode.lua @@ -0,0 +1,3 @@ +worldeditadditions.erode = {} + +dofile(worldeditadditions.modpath.."/lib/erode/snowballs.lua") diff --git a/worldeditadditions/lib/erode/snowballs.lua b/worldeditadditions/lib/erode/snowballs.lua new file mode 100644 index 0000000..e834f10 --- /dev/null +++ b/worldeditadditions/lib/erode/snowballs.lua @@ -0,0 +1,9 @@ +--[[ +2D erosion algorithm based on snowballs +Note that this *mutates* the given heightmap. +@source https://jobtalle.com/simulating_hydraulic_erosion.html + +]]-- +function worldeditadditions.erode.snowballs(heightmap, heightmap_size, erosion_params) + +end From cdba38d37dae68dfc85ef873cdf9a79305674894 Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Thu, 20 Aug 2020 01:53:26 +0100 Subject: [PATCH 2/8] Start implementing snowball erosion algorithm. There's still a long way to go - we're only just getting warmed up! --- worldeditadditions/init.lua | 1 + worldeditadditions/lib/erode/snowballs.lua | 67 +++++++++++++++++++- worldeditadditions/utils.lua | 26 +++++++- worldeditadditions/utils/nodes.lua | 38 ------------ worldeditadditions/utils/tables.lua | 10 +++ worldeditadditions/utils/terrain.lua | 72 ++++++++++++++++++++++ 6 files changed, 173 insertions(+), 41 deletions(-) create mode 100644 worldeditadditions/utils/terrain.lua diff --git a/worldeditadditions/init.lua b/worldeditadditions/init.lua index 8be8c1a..571d68d 100644 --- a/worldeditadditions/init.lua +++ b/worldeditadditions/init.lua @@ -11,6 +11,7 @@ dofile(worldeditadditions.modpath.."/utils/strings.lua") dofile(worldeditadditions.modpath.."/utils/numbers.lua") dofile(worldeditadditions.modpath.."/utils/nodes.lua") dofile(worldeditadditions.modpath.."/utils/tables.lua") +dofile(worldeditadditions.modpath.."/utils/terrain.lua") dofile(worldeditadditions.modpath.."/utils/raycast_adv.lua") -- For the farwand dofile(worldeditadditions.modpath.."/utils.lua") diff --git a/worldeditadditions/lib/erode/snowballs.lua b/worldeditadditions/lib/erode/snowballs.lua index e834f10..f9fc82e 100644 --- a/worldeditadditions/lib/erode/snowballs.lua +++ b/worldeditadditions/lib/erode/snowballs.lua @@ -4,6 +4,71 @@ Note that this *mutates* the given heightmap. @source https://jobtalle.com/simulating_hydraulic_erosion.html ]]-- -function worldeditadditions.erode.snowballs(heightmap, heightmap_size, erosion_params) +function worldeditadditions.erode.snowballs(heightmap, heightmap_size, params) + -- Apply the default settings + worldeditadditions.table_apply({ + rate_deposit = 0.03, + rate_erosion = 0.04, + friction = 0.07, + speed = 0.15, + radius = 0.8, + snowball_max_steps = 80, + scale_iterations = 0.04, + drops_per_cell = 0.4, + snowball_count = 50000 + }, params) + local normals = worldeditadditions.calculate_normals(heightmap, heightmap_size) + + for i = 1, params.snowball_count do + snowball( + heightmap, normals, heightmap_size, + { x = math.random() } + ) + end +end + +local function snowball(heightmap, normalmap, heightmap_size, startpos, params) + local offset = { -- Random jitter - apparently helps to avoid snowballs from entrenching too much + x = (math.random() * 2 - 1) * params.radius, + z = (math.random() * 2 - 1) * params.radius + } + local sediment = 0 + local pos = { x = startpos.x, z = startpos.z } + local pos_prev = { x = pos.x, z = pos.z } + local velocity = { x = 0, z = 0 } + local heightmap_length = #heightmap + + for i = 1, params.snowball_max_steps do + local hi = math.floor(pos.z+offset.z+0.5)*heightmap_size[1] + math.floor(pos.x+offset.x+0.5) + if hi > heightmap_length then break end + + -- Stop if we go out of bounds + if offset.x < 0 or offset.z < 0 + or offset.x >= heightmap[1] or offset.z >= heightmap[0] then + break + end + + local step_deposit = sediment * params.rate_deposit * normalmap[hi].z + local step_erode = params.rate_erosion * (1 - normalmap[hi].z) * math.min(1, i*params.scale_iterations) + + -- Erode / Deposit, but only if we are on a different node than we were in the last step + if math.floor(pos_prev.x) ~= math.floor(pos.x) + and math.floor(pos_prev.z) ~= math.floor(pos.z) then + heightmap[hi] = heightmap[hi] + (deposit - erosion) + end + + velocity.x = params.friction * velocity.x + normalmap[hi].x * params.speed + velocity.z = params.friction * velocity.z + normalmap[hi].y * params.speed + pos_prev.x = pos.x + pos_prev.z = pos.z + pos.x = pos.x + velocity.x + pos.z = pos.z + velocity.z + end + + -- Round everything to the nearest int, since you can't really have + -- something like .141592671 of a node + for i,v in ipairs(heightmap) do + heightmap[i] = math.floor(heightmap[i] + 0.5) + end end diff --git a/worldeditadditions/utils.lua b/worldeditadditions/utils.lua index 375c3b1..3ab8504 100644 --- a/worldeditadditions/utils.lua +++ b/worldeditadditions/utils.lua @@ -4,12 +4,34 @@ function worldeditadditions.vector.tostring(v) return "(" .. v.x ..", " .. v.y ..", " .. v.z ..")" end +-- Calculates the length squared of the given vector. +-- @param v Vector The vector to operate on +-- @return number The length of the given vector squared function worldeditadditions.vector.lengthsquared(v) return v.x*v.x + v.y*v.y + v.z*v.z end +--- Normalises the given vector such that its length is 1. +-- Also known as calculating the unit vector. +-- This method does *not* mutate. +-- @param v Vector The vector to calculate from. +-- @return Vector A new normalised vector. +function worldeditadditions.vector.normalise(v) + local length = math.sqrt(worldeditadditions.lengthsquared(v)) + return { + x = x / length, + y = y / length, + z = z / length + } +end + +--- Rounds the values in a vector down. +-- Warning: This MUTATES the given vector! +-- @param v Vector The vector to operate on function worldeditadditions.vector.floor(v) v.x = math.floor(v.x) - v.y = math.floor(v.y) - v.z = math.floor(v.z) + -- Some vectors are 2d, but on the x / z axes + if v.y then v.y = math.floor(v.y) end + -- Some vectors are 2d + if v.z then v.z = math.floor(v.z) end end diff --git a/worldeditadditions/utils/nodes.lua b/worldeditadditions/utils/nodes.lua index e39b7ae..fe7cf32 100644 --- a/worldeditadditions/utils/nodes.lua +++ b/worldeditadditions/utils/nodes.lua @@ -78,41 +78,3 @@ function worldeditadditions.is_liquidlike(id) -- If it's not none, then it has to be a liquid as the only other values are source and flowing return true end - ---- Given a manip object and associates, generates a 2D x/z heightmap. --- Note that pos1 and pos2 should have already been pushed through --- worldedit.sort_pos(pos1, pos2) before passing them to this function. --- @param pos1 Vector Position 1 of the region to operate on --- @param pos2 Vector Position 2 of the region to operate on --- @param manip VoxelManip The VoxelManip object. --- @param area area The associated area object. --- @param data table The associated data object. --- @return table The ZERO-indexed heightmap data (as 1 single flat array). -function worldeditadditions.make_heightmap(pos1, pos2, manip, area, data) - -- z y x (in reverse for little-endian machines) is the preferred loop order, but that isn't really possible here - - local heightmap = {} - local hi = 0 - local changes = { updated = 0, skipped_columns = 0 } - for z = pos1.z, pos2.z, 1 do - for x = pos1.x, pos2.x, 1 do - local found_node = false - -- Scan each column top to bottom - for y = pos2.y+1, pos1.y, -1 do - local i = area:index(x, y, z) - if not worldeditadditions.is_airlike(data[i]) then - -- It's the first non-airlike node in this column - -- Start heightmap values from 1 (i.e. there's at least 1 node in the column) - heightmap[hi] = (y - pos1.y) + 1 - found_node = true - break - end - end - - if not found_node then heightmap[hi] = -1 end - hi = hi + 1 - end - end - - return heightmap -end diff --git a/worldeditadditions/utils/tables.lua b/worldeditadditions/utils/tables.lua index c41b47d..2c868b3 100644 --- a/worldeditadditions/utils/tables.lua +++ b/worldeditadditions/utils/tables.lua @@ -15,3 +15,13 @@ function worldeditadditions.shallowcopy(orig) end return copy end + +--- SHALLOWLY applies the values in source to overwrite the equivalent keys in target. +-- Warning: This function mutates target! +-- @param source table The source to take values from +-- @param target table The target to write values to +function worldeditadditions.table_apply(source, target) + for key, value in pairs(source) do + target[key] = value + end +end diff --git a/worldeditadditions/utils/terrain.lua b/worldeditadditions/utils/terrain.lua new file mode 100644 index 0000000..2148f39 --- /dev/null +++ b/worldeditadditions/utils/terrain.lua @@ -0,0 +1,72 @@ + +--- Given a manip object and associates, generates a 2D x/z heightmap. +-- Note that pos1 and pos2 should have already been pushed through +-- worldedit.sort_pos(pos1, pos2) before passing them to this function. +-- @param pos1 Vector Position 1 of the region to operate on +-- @param pos2 Vector Position 2 of the region to operate on +-- @param manip VoxelManip The VoxelManip object. +-- @param area area The associated area object. +-- @param data table The associated data object. +-- @return table The ZERO-indexed heightmap data (as 1 single flat array). +function worldeditadditions.make_heightmap(pos1, pos2, manip, area, data) + -- z y x (in reverse for little-endian machines) is the preferred loop order, but that isn't really possible here + + local heightmap = {} + local hi = 0 + local changes = { updated = 0, skipped_columns = 0 } + for z = pos1.z, pos2.z, 1 do + for x = pos1.x, pos2.x, 1 do + local found_node = false + -- Scan each column top to bottom + for y = pos2.y+1, pos1.y, -1 do + local i = area:index(x, y, z) + if not worldeditadditions.is_airlike(data[i]) then + -- It's the first non-airlike node in this column + -- Start heightmap values from 1 (i.e. there's at least 1 node in the column) + heightmap[hi] = (y - pos1.y) + 1 + found_node = true + break + end + end + + if not found_node then heightmap[hi] = -1 end + hi = hi + 1 + end + end + + return heightmap +end + +--- Calculates a normal map for the given heightmap. +-- Caution: This method (like worldeditadditions.make_heightmap) works on +-- X AND Z, and NOT x and y. This means that the resulting 3d normal vectors +-- will have the z and y values swapped. +-- @param heightmap table A ZERO indexed flat heightmap. See worldeditadditions.make_heightmap(). +-- @param heightmap_size int[] The size of the heightmap in the form [ z, x ] +-- @return Vector[] The calculated normal map, in the same form as the input heightmap. Each element of the array is a 3D Vector (i.e. { x, y, z }) representing a normal. +function worldeditadditions.calculate_normals(heightmap, heightmap_size) + local result = {} + for z = heightmap_size[0], 0, -1 do + for x = heightmap_size[1], 0, -1 do + -- Algorithm ref https://stackoverflow.com/a/13983431/1460422 + -- Also ref Vector.mjs, which I implemented myself (available upon request) + local hi = z*heightmap_size[1] + x + -- Default to this pixel's height + local up = heightmap[hi] + local down = heightmap[hi] + local left = heightmap[hi] + local right = heightmap[hi] + if z - 1 > 0 then up = heightmap[(z-1)*heightmap_size[1] + x] end + if z + 1 < heightmap_size[1] then down = heightmap[(z+1)*heightmap_size[1] + x] end + if x - 1 > 0 then left = heightmap[z*heightmap_size[1] + (x-1)] end + if x + 1 < heightmap_size[0] then right = heightmap[z*heightmap_size[1] + (x+1)] end + + result[hi] = worldeditadditions.vector.normalize({ + x = left - right, + y = 2, -- Z & Y are flipped + z = down - up + }) + end + end + return result +end From acb288b984699f6d404a49675bc14e415be7419d Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Fri, 21 Aug 2020 13:27:40 +0100 Subject: [PATCH 3/8] Fix a ton of bugs but there are lots more to go..... --- worldeditadditions/init.lua | 3 +- worldeditadditions/lib/conv/conv.lua | 42 ++----------- worldeditadditions/lib/erode/erode.lua | 36 +++++++++++ worldeditadditions/lib/erode/snowballs.lua | 59 ++++++++++--------- worldeditadditions/utils/strings.lua | 27 +++++++++ worldeditadditions/utils/terrain.lua | 58 ++++++++++++++++-- .../{utils.lua => utils/vector.lua} | 10 ++-- .../commands/erode.lua | 43 ++++++++++++++ worldeditadditions_commands/init.lua | 1 + 9 files changed, 202 insertions(+), 77 deletions(-) rename worldeditadditions/{utils.lua => utils/vector.lua} (84%) create mode 100644 worldeditadditions_commands/commands/erode.lua diff --git a/worldeditadditions/init.lua b/worldeditadditions/init.lua index 571d68d..a7c10f8 100644 --- a/worldeditadditions/init.lua +++ b/worldeditadditions/init.lua @@ -7,6 +7,7 @@ worldeditadditions = {} worldeditadditions.modpath = minetest.get_modpath("worldeditadditions") +dofile(worldeditadditions.modpath.."/utils/vector.lua") dofile(worldeditadditions.modpath.."/utils/strings.lua") dofile(worldeditadditions.modpath.."/utils/numbers.lua") dofile(worldeditadditions.modpath.."/utils/nodes.lua") @@ -14,7 +15,6 @@ dofile(worldeditadditions.modpath.."/utils/tables.lua") dofile(worldeditadditions.modpath.."/utils/terrain.lua") dofile(worldeditadditions.modpath.."/utils/raycast_adv.lua") -- For the farwand -dofile(worldeditadditions.modpath.."/utils.lua") dofile(worldeditadditions.modpath.."/lib/floodfill.lua") dofile(worldeditadditions.modpath.."/lib/overlay.lua") dofile(worldeditadditions.modpath.."/lib/layers.lua") @@ -25,6 +25,7 @@ dofile(worldeditadditions.modpath.."/lib/replacemix.lua") dofile(worldeditadditions.modpath.."/lib/maze2d.lua") dofile(worldeditadditions.modpath.."/lib/maze3d.lua") dofile(worldeditadditions.modpath.."/lib/conv/conv.lua") +dofile(worldeditadditions.modpath.."/lib/erode/erode.lua") dofile(worldeditadditions.modpath.."/lib/count.lua") diff --git a/worldeditadditions/lib/conv/conv.lua b/worldeditadditions/lib/conv/conv.lua index eff91ab..93b0dbc 100644 --- a/worldeditadditions/lib/conv/conv.lua +++ b/worldeditadditions/lib/conv/conv.lua @@ -75,45 +75,11 @@ function worldeditadditions.convolve(pos1, pos2, kernel, kernel_size) -- worldeditadditions.print_2d(heightmap, (pos2.z - pos1.z) + 1) -- print("transformed") -- worldeditadditions.print_2d(heightmap_conv, (pos2.z - pos1.z) + 1) - -- It seems to be convolving as intended, but something's probably getting lost in translation below - for z = heightmap_size[0], 0, -1 do - for x = heightmap_size[1], 0, -1 do - local hi = z*heightmap_size[1] + x - - - local height_old = heightmap[hi] - local height_new = heightmap_conv[hi] - -- print("[conv/save] hi", hi, "height_old", heightmap[hi], "height_new", heightmap_conv[hi], "z", z, "x", x, "pos1.y", pos1.y) - - -- Lua doesn't have a continue statement :-/ - if height_old == height_new then - -- noop - elseif height_new < height_old then - stats.removed = stats.removed + (height_old - height_new) - local y = height_new - while y < height_old do - local ci = area:index(pos1.x + x, pos1.y + y, pos1.z + z) - -- print("[conv/save] remove at y", y, "→", pos1.y + y, "current:", minetest.get_name_from_content_id(data[ci])) - data[ci] = node_id_air - y = y + 1 - end - else -- height_new > height_old - -- We subtract one because the heightmap starts at 1 (i.e. 1 = 1 node in the column), but the selected region is inclusive - local node_id = data[area:index(pos1.x + x, pos1.y + (height_old - 1), pos1.z + z)] - -- print("[conv/save] filling with ", node_id, "→", minetest.get_name_from_content_id(node_id)) - - stats.added = stats.added + (height_new - height_old) - local y = height_old - while y < height_new do - local ci = area:index(pos1.x + x, pos1.y + y, pos1.z + z) - -- print("[conv/save] add at y", y, "→", pos1.y + y, "current:", minetest.get_name_from_content_id(data[ci])) - data[ci] = node_id - y = y + 1 - end - end - end - end + worldeditadditions.apply_heightmap_changes( + pos1, pos2, area, data, + heightmap, heightmap_conv, heightmap_size + ) worldedit.manip_helpers.finish(manip, data) diff --git a/worldeditadditions/lib/erode/erode.lua b/worldeditadditions/lib/erode/erode.lua index d30118d..4e87ffe 100644 --- a/worldeditadditions/lib/erode/erode.lua +++ b/worldeditadditions/lib/erode/erode.lua @@ -1,3 +1,39 @@ worldeditadditions.erode = {} dofile(worldeditadditions.modpath.."/lib/erode/snowballs.lua") + + +function worldeditadditions.erode.run(pos1, pos2, algorithm, params) + pos1, pos2 = worldedit.sort_pos(pos1, pos2) + + local manip, area = worldedit.manip_helpers.init(pos1, pos2) + local data = manip:get_data() + + local heightmap_size = {} + heightmap_size[0] = (pos2.z - pos1.z) + 1 + heightmap_size[1] = (pos2.x - pos1.x) + 1 + + local heightmap = worldeditadditions.make_heightmap(pos1, pos2, manip, area, data) + local heightmap_eroded = worldeditadditions.shallowcopy(heightmap) + + print("[erode.run] algorithm: "..algorithm..", params:"); + print(worldeditadditions.map_stringify(params)) + worldeditadditions.print_2d(heightmap, heightmap_size[1]) + + if algorithm == "snowballs" then + local success, msg = worldeditadditions.erode.snowballs(heightmap_eroded, heightmap_size, params) + if not success then return success, msg end + else + return false, "Error: Unknown algorithm '"..algorithm.."'. Currently implemented algorithms: snowballs (2d; hydraulic-like). Ideas for algorithms to implement are welcome!" + end + + local success, msg = worldeditadditions.apply_heightmap_changes( + pos1, pos2, area, data, + heightmap, heightmap_eroded, heightmap_size + ) + if not success then return success, msg end + + worldedit.manip_helpers.finish(manip, data) + + return true, stats +end diff --git a/worldeditadditions/lib/erode/snowballs.lua b/worldeditadditions/lib/erode/snowballs.lua index f9fc82e..0cf2b7c 100644 --- a/worldeditadditions/lib/erode/snowballs.lua +++ b/worldeditadditions/lib/erode/snowballs.lua @@ -1,32 +1,3 @@ ---[[ -2D erosion algorithm based on snowballs -Note that this *mutates* the given heightmap. -@source https://jobtalle.com/simulating_hydraulic_erosion.html - -]]-- -function worldeditadditions.erode.snowballs(heightmap, heightmap_size, params) - -- Apply the default settings - worldeditadditions.table_apply({ - rate_deposit = 0.03, - rate_erosion = 0.04, - friction = 0.07, - speed = 0.15, - radius = 0.8, - snowball_max_steps = 80, - scale_iterations = 0.04, - drops_per_cell = 0.4, - snowball_count = 50000 - }, params) - - local normals = worldeditadditions.calculate_normals(heightmap, heightmap_size) - - for i = 1, params.snowball_count do - snowball( - heightmap, normals, heightmap_size, - { x = math.random() } - ) - end -end local function snowball(heightmap, normalmap, heightmap_size, startpos, params) local offset = { -- Random jitter - apparently helps to avoid snowballs from entrenching too much @@ -72,3 +43,33 @@ local function snowball(heightmap, normalmap, heightmap_size, startpos, params) heightmap[i] = math.floor(heightmap[i] + 0.5) end end + +--[[ +2D erosion algorithm based on snowballs +Note that this *mutates* the given heightmap. +@source https://jobtalle.com/simulating_hydraulic_erosion.html + +]]-- +function worldeditadditions.erode.snowballs(heightmap, heightmap_size, params) + -- Apply the default settings + worldeditadditions.table_apply({ + rate_deposit = 0.03, + rate_erosion = 0.04, + friction = 0.07, + speed = 0.15, + radius = 0.8, + snowball_max_steps = 80, + scale_iterations = 0.04, + drops_per_cell = 0.4, + snowball_count = 50000 + }, params) + + local normals = worldeditadditions.calculate_normals(heightmap, heightmap_size) + + for i = 1, params.snowball_count do + snowball( + heightmap, normals, heightmap_size, + { x = math.random() } + ) + end +end diff --git a/worldeditadditions/utils/strings.lua b/worldeditadditions/utils/strings.lua index 52a0b2b..f3e75fa 100644 --- a/worldeditadditions/utils/strings.lua +++ b/worldeditadditions/utils/strings.lua @@ -86,6 +86,7 @@ end -- @param tbl number[] The ZERO-indexed list of numbers -- @param width number The width of 2D array. function worldeditadditions.print_2d(tbl, width) + print("==== count: "..#tbl..", width:"..width.." ====") local display_width = 1 for _i,value in pairs(tbl) do display_width = math.max(display_width, #tostring(value)) @@ -209,6 +210,32 @@ function worldeditadditions.parse_weighted_nodes(parts, as_list) return true, result end +function worldeditadditions.parse_map(params_text) + local result = {} + local parts = worldeditadditions.split(params_text, "%s+", false) + + local last_key = nil + for i, part in ipairs(parts) do + if i % 2 == 1 then + -- Try converting to a number to see if it works + local part_converted = tonumber(part) + if as_number == nil then part_converted = part end + result[last_key] = part + else + last_key = part + end + end + return true, result +end + +function worldeditadditions.map_stringify(map) + local result = {} + for key, value in pairs(map) do + table.insert(key.."\t"..value) + end + return table.concat(result, "\n") +end + --- Converts a float milliseconds into a human-readable string. -- Ported from PHP human_time from Pepperminty Wiki: https://github.com/sbrl/Pepperminty-Wiki/blob/fa81f0d/core/05-functions.php#L82-L104 -- @param ms float The number of milliseconds to convert. diff --git a/worldeditadditions/utils/terrain.lua b/worldeditadditions/utils/terrain.lua index 2148f39..c941608 100644 --- a/worldeditadditions/utils/terrain.lua +++ b/worldeditadditions/utils/terrain.lua @@ -45,9 +45,10 @@ end -- @param heightmap_size int[] The size of the heightmap in the form [ z, x ] -- @return Vector[] The calculated normal map, in the same form as the input heightmap. Each element of the array is a 3D Vector (i.e. { x, y, z }) representing a normal. function worldeditadditions.calculate_normals(heightmap, heightmap_size) + print("heightmap_size: "..heightmap_size[1].."x"..heightmap_size[0]) local result = {} - for z = heightmap_size[0], 0, -1 do - for x = heightmap_size[1], 0, -1 do + for z = heightmap_size[0]-1, 0, -1 do + for x = heightmap_size[1]-1, 0, -1 do -- Algorithm ref https://stackoverflow.com/a/13983431/1460422 -- Also ref Vector.mjs, which I implemented myself (available upon request) local hi = z*heightmap_size[1] + x @@ -57,9 +58,14 @@ function worldeditadditions.calculate_normals(heightmap, heightmap_size) local left = heightmap[hi] local right = heightmap[hi] if z - 1 > 0 then up = heightmap[(z-1)*heightmap_size[1] + x] end - if z + 1 < heightmap_size[1] then down = heightmap[(z+1)*heightmap_size[1] + x] end + if z + 1 < heightmap_size[0]-1 then down = heightmap[(z+1)*heightmap_size[1] + x] end if x - 1 > 0 then left = heightmap[z*heightmap_size[1] + (x-1)] end - if x + 1 < heightmap_size[0] then right = heightmap[z*heightmap_size[1] + (x+1)] end + if x + 1 < heightmap_size[1]-1 then right = heightmap[z*heightmap_size[1] + (x+1)] end + + print("[normals] UP | index", (z-1)*heightmap_size[1] + x, "z", z, "z-1", z - 1, "up", up, "limit", 0) + print("[normals] DOWN | index", (z+1)*heightmap_size[1] + x, "z", z, "z+1", z + 1, "down", down, "limit", heightmap_size[1]-1) + print("[normals] LEFT | index", z*heightmap_size[1] + (x-1), "x", x, "x-1", x - 1, "left", left, "limit", 0) + print("[normals] RIGHT | index", z*heightmap_size[1] + (x+1), "x", x, "x+1", x + 1, "right", right, "limit", heightmap_size[1]-1) result[hi] = worldeditadditions.vector.normalize({ x = left - right, @@ -70,3 +76,47 @@ function worldeditadditions.calculate_normals(heightmap, heightmap_size) end return result end + +function worldeditadditions.apply_heightmap_changes(pos1, pos2, area, data, heightmap_old, heightmap_new, heightmap_size) + local stats = { added = 0, removed = 0 } + local node_id_air = minetest.get_content_id("air") + + for z = heightmap_size[0], 0, -1 do + for x = heightmap_size[1], 0, -1 do + local hi = z*heightmap_size[1] + x + + local height_old = heightmap[hi] + local height_new = heightmap_new[hi] + -- print("[conv/save] hi", hi, "height_old", heightmap[hi], "height_new", heightmap_new[hi], "z", z, "x", x, "pos1.y", pos1.y) + + -- Lua doesn't have a continue statement :-/ + if height_old == height_new then + -- noop + elseif height_new < height_old then + stats.removed = stats.removed + (height_old - height_new) + local y = height_new + while y < height_old do + local ci = area:index(pos1.x + x, pos1.y + y, pos1.z + z) + -- print("[conv/save] remove at y", y, "→", pos1.y + y, "current:", minetest.get_name_from_content_id(data[ci])) + data[ci] = node_id_air + y = y + 1 + end + else -- height_new > height_old + -- We subtract one because the heightmap starts at 1 (i.e. 1 = 1 node in the column), but the selected region is inclusive + local node_id = data[area:index(pos1.x + x, pos1.y + (height_old - 1), pos1.z + z)] + -- print("[conv/save] filling with ", node_id, "→", minetest.get_name_from_content_id(node_id)) + + stats.added = stats.added + (height_new - height_old) + local y = height_old + while y < height_new do + local ci = area:index(pos1.x + x, pos1.y + y, pos1.z + z) + -- print("[conv/save] add at y", y, "→", pos1.y + y, "current:", minetest.get_name_from_content_id(data[ci])) + data[ci] = node_id + y = y + 1 + end + end + end + end + + return true, stats +end diff --git a/worldeditadditions/utils.lua b/worldeditadditions/utils/vector.lua similarity index 84% rename from worldeditadditions/utils.lua rename to worldeditadditions/utils/vector.lua index 3ab8504..73e86f1 100644 --- a/worldeditadditions/utils.lua +++ b/worldeditadditions/utils/vector.lua @@ -16,12 +16,12 @@ end -- This method does *not* mutate. -- @param v Vector The vector to calculate from. -- @return Vector A new normalised vector. -function worldeditadditions.vector.normalise(v) - local length = math.sqrt(worldeditadditions.lengthsquared(v)) +function worldeditadditions.vector.normalize(v) + local length = math.sqrt(worldeditadditions.vector.lengthsquared(v)) return { - x = x / length, - y = y / length, - z = z / length + x = v.x / length, + y = v.y / length, + z = v.z / length } end diff --git a/worldeditadditions_commands/commands/erode.lua b/worldeditadditions_commands/commands/erode.lua new file mode 100644 index 0000000..81b98e1 --- /dev/null +++ b/worldeditadditions_commands/commands/erode.lua @@ -0,0 +1,43 @@ +-- ██████ ██ ██ ███████ ██████ ██ █████ ██ ██ +-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +-- ██ ██ ██ ██ █████ ██████ ██ ███████ ████ +-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +-- ██████ ████ ███████ ██ ██ ███████ ██ ██ ██ +worldedit.register_command("erode", { + params = "[ [ []] [ []] ...]", + description = "Runs the specified erosion algorithm over the given defined region. This may occur in 2d or 3d. Currently implemented algorithms: snowballs (default;2d hydraulic-like). Also optionally takes an arbitrary set of key - value pairs representing parameters to pass to the algorithm. See the full documentation for details.", + privs = { worldedit = true }, + require_pos = 2, + parse = function(params_text) + if not params_text or params_text == "" then + return true, "snowballs", {} + end + + if params_text:find("%s") == nil then + return true, params_text, {} + end + + local algorithm, params = params_text:match("([^%s]+)%s(.+)") + if algorithm == nil then + return false, "Failed to split params_text into 2 parts (this is probably a bug)" + end + + local success, map = worldeditadditions.parse_map(params) + if not success then return success, map end + return true, algorithm, map + end, + nodes_needed = function(name) + return worldedit.volume(worldedit.pos1[name], worldedit.pos2[name]) + end, + func = function(name, algorithm, params) + local start_time = worldeditadditions.get_ms_time() + local success, stats = worldeditadditions.erode.run( + worldedit.pos1[name], worldedit.pos2[name], + algorithm, params + ) + local time_taken = worldeditadditions.get_ms_time() - start_time + + minetest.log("action", name .. " used //erode "..algorithm.." at " .. worldeditadditions.vector.tostring(worldedit.pos1[name]) .. ", replacing " .. changes.replaced .. " nodes and skipping " .. changes.skipped_columns .. " columns in " .. time_taken .. "s") + return true, changes.replaced .. " nodes replaced and " .. changes.skipped_columns .. " columns skipped in " .. worldeditadditions.human_time(time_taken) + end +}) diff --git a/worldeditadditions_commands/init.lua b/worldeditadditions_commands/init.lua index 91c7366..229f460 100644 --- a/worldeditadditions_commands/init.lua +++ b/worldeditadditions_commands/init.lua @@ -30,6 +30,7 @@ dofile(we_c.modpath.."/commands/walls.lua") dofile(we_c.modpath.."/commands/maze.lua") dofile(we_c.modpath.."/commands/replacemix.lua") dofile(we_c.modpath.."/commands/convolve.lua") +dofile(we_c.modpath.."/commands/erode.lua") dofile(we_c.modpath.."/commands/count.lua") From 997eb4d1018f7e9087c11f62c505009158519074 Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Fri, 21 Aug 2020 15:21:10 +0100 Subject: [PATCH 4/8] fix all the crashes with the new erosion algorithm ......but it doesn't look like it's functioning as intended just yet --- worldeditadditions/lib/erode/erode.lua | 7 ++- worldeditadditions/lib/erode/snowballs.lua | 61 ++++++++++++------- worldeditadditions/utils/strings.lua | 2 +- worldeditadditions/utils/terrain.lua | 14 +++-- .../commands/erode.lua | 4 +- 5 files changed, 54 insertions(+), 34 deletions(-) diff --git a/worldeditadditions/lib/erode/erode.lua b/worldeditadditions/lib/erode/erode.lua index 4e87ffe..5c49784 100644 --- a/worldeditadditions/lib/erode/erode.lua +++ b/worldeditadditions/lib/erode/erode.lua @@ -27,13 +27,14 @@ function worldeditadditions.erode.run(pos1, pos2, algorithm, params) return false, "Error: Unknown algorithm '"..algorithm.."'. Currently implemented algorithms: snowballs (2d; hydraulic-like). Ideas for algorithms to implement are welcome!" end - local success, msg = worldeditadditions.apply_heightmap_changes( + local success, stats = worldeditadditions.apply_heightmap_changes( pos1, pos2, area, data, heightmap, heightmap_eroded, heightmap_size ) - if not success then return success, msg end - + if not success then return success, stats end worldedit.manip_helpers.finish(manip, data) + print("[erode] stats") + print(worldeditadditions.map_stringify(stats)) return true, stats end diff --git a/worldeditadditions/lib/erode/snowballs.lua b/worldeditadditions/lib/erode/snowballs.lua index 0cf2b7c..e5b8398 100644 --- a/worldeditadditions/lib/erode/snowballs.lua +++ b/worldeditadditions/lib/erode/snowballs.lua @@ -1,46 +1,48 @@ +-- Test command: //multi //fp set1 1312 5 5543 //fp set2 1336 18 5521 //erode//multi //fp set1 1312 5 5543 //fp set2 1336 18 5521 //erode + local function snowball(heightmap, normalmap, heightmap_size, startpos, params) - local offset = { -- Random jitter - apparently helps to avoid snowballs from entrenching too much - x = (math.random() * 2 - 1) * params.radius, - z = (math.random() * 2 - 1) * params.radius - } local sediment = 0 local pos = { x = startpos.x, z = startpos.z } local pos_prev = { x = pos.x, z = pos.z } local velocity = { x = 0, z = 0 } local heightmap_length = #heightmap + -- print("[snowball] startpos ("..pos.x..", "..pos.z..")") + for i = 1, params.snowball_max_steps do - local hi = math.floor(pos.z+offset.z+0.5)*heightmap_size[1] + math.floor(pos.x+offset.x+0.5) - if hi > heightmap_length then break end - + local x = pos.x + local z = pos.z + local hi = math.floor(z+0.5)*heightmap_size[1] + math.floor(x+0.5) -- Stop if we go out of bounds - if offset.x < 0 or offset.z < 0 - or offset.x >= heightmap[1] or offset.z >= heightmap[0] then - break + if x < 0 or z < 0 + or x >= heightmap[1]-1 or z >= heightmap[0]-1 then + -- print("[snowball] hit edge; stopping at ("..x..", "..z.."), (bounds @ "..heightmap_size[1]..", "..heightmap_size[0]..")") + return end + -- print("[snowball] now at ("..x..", "..z..") (bounds @ "..heightmap_size[1]..", "..heightmap_size[0]..")") + if hi > heightmap_length then print("[snowball] out-of-bounds on the array, hi: "..hi..", heightmap_length: "..heightmap_length) return end + + -- print("[snowball] sediment", sediment, "rate_deposit", params.rate_deposit, "normalmap[hi].z", normalmap[hi].z) local step_deposit = sediment * params.rate_deposit * normalmap[hi].z local step_erode = params.rate_erosion * (1 - normalmap[hi].z) * math.min(1, i*params.scale_iterations) + local step_diff = step_deposit - step_erode + -- Erode / Deposit, but only if we are on a different node than we were in the last step - if math.floor(pos_prev.x) ~= math.floor(pos.x) - and math.floor(pos_prev.z) ~= math.floor(pos.z) then - heightmap[hi] = heightmap[hi] + (deposit - erosion) + if math.floor(pos_prev.x) ~= math.floor(x) + and math.floor(pos_prev.z) ~= math.floor(z) then + heightmap[hi] = heightmap[hi] + step_diff end velocity.x = params.friction * velocity.x + normalmap[hi].x * params.speed velocity.z = params.friction * velocity.z + normalmap[hi].y * params.speed - pos_prev.x = pos.x - pos_prev.z = pos.z + pos_prev.x = x + pos_prev.z = z pos.x = pos.x + velocity.x pos.z = pos.z + velocity.z - end - - -- Round everything to the nearest int, since you can't really have - -- something like .141592671 of a node - for i,v in ipairs(heightmap) do - heightmap[i] = math.floor(heightmap[i] + 0.5) + sediment = sediment + step_diff end end @@ -64,12 +66,27 @@ function worldeditadditions.erode.snowballs(heightmap, heightmap_size, params) snowball_count = 50000 }, params) + print("[erode/snowballs] params: "..worldeditadditions.map_stringify(params)) + local normals = worldeditadditions.calculate_normals(heightmap, heightmap_size) for i = 1, params.snowball_count do snowball( heightmap, normals, heightmap_size, - { x = math.random() } + { + x = math.random() * (heightmap_size[1] - 1), + z = math.random() * (heightmap_size[0] - 1) + }, + params ) end + + -- Round everything to the nearest int, since you can't really have + -- something like .141592671 of a node + -- Note that we do this after *all* the erosion is complete + for i,v in ipairs(heightmap) do + heightmap[i] = math.floor(heightmap[i] + 0.5) + end + + return true, params.snowball_count.." snowballs simulated" end diff --git a/worldeditadditions/utils/strings.lua b/worldeditadditions/utils/strings.lua index f3e75fa..673eef9 100644 --- a/worldeditadditions/utils/strings.lua +++ b/worldeditadditions/utils/strings.lua @@ -231,7 +231,7 @@ end function worldeditadditions.map_stringify(map) local result = {} for key, value in pairs(map) do - table.insert(key.."\t"..value) + table.insert(result, key.."\t"..value) end return table.concat(result, "\n") end diff --git a/worldeditadditions/utils/terrain.lua b/worldeditadditions/utils/terrain.lua index c941608..d4353b6 100644 --- a/worldeditadditions/utils/terrain.lua +++ b/worldeditadditions/utils/terrain.lua @@ -62,16 +62,18 @@ function worldeditadditions.calculate_normals(heightmap, heightmap_size) if x - 1 > 0 then left = heightmap[z*heightmap_size[1] + (x-1)] end if x + 1 < heightmap_size[1]-1 then right = heightmap[z*heightmap_size[1] + (x+1)] end - print("[normals] UP | index", (z-1)*heightmap_size[1] + x, "z", z, "z-1", z - 1, "up", up, "limit", 0) - print("[normals] DOWN | index", (z+1)*heightmap_size[1] + x, "z", z, "z+1", z + 1, "down", down, "limit", heightmap_size[1]-1) - print("[normals] LEFT | index", z*heightmap_size[1] + (x-1), "x", x, "x-1", x - 1, "left", left, "limit", 0) - print("[normals] RIGHT | index", z*heightmap_size[1] + (x+1), "x", x, "x+1", x + 1, "right", right, "limit", heightmap_size[1]-1) + -- print("[normals] UP | index", (z-1)*heightmap_size[1] + x, "z", z, "z-1", z - 1, "up", up, "limit", 0) + -- print("[normals] DOWN | index", (z+1)*heightmap_size[1] + x, "z", z, "z+1", z + 1, "down", down, "limit", heightmap_size[1]-1) + -- print("[normals] LEFT | index", z*heightmap_size[1] + (x-1), "x", x, "x-1", x - 1, "left", left, "limit", 0) + -- print("[normals] RIGHT | index", z*heightmap_size[1] + (x+1), "x", x, "x+1", x + 1, "right", right, "limit", heightmap_size[1]-1) result[hi] = worldeditadditions.vector.normalize({ x = left - right, y = 2, -- Z & Y are flipped z = down - up }) + + -- print("[normals] at "..hi.." ("..x..", "..z..") normal "..worldeditadditions.vector.tostring(result[hi])) end end return result @@ -85,9 +87,9 @@ function worldeditadditions.apply_heightmap_changes(pos1, pos2, area, data, heig for x = heightmap_size[1], 0, -1 do local hi = z*heightmap_size[1] + x - local height_old = heightmap[hi] + local height_old = heightmap_old[hi] local height_new = heightmap_new[hi] - -- print("[conv/save] hi", hi, "height_old", heightmap[hi], "height_new", heightmap_new[hi], "z", z, "x", x, "pos1.y", pos1.y) + -- print("[conv/save] hi", hi, "height_old", heightmap_old[hi], "height_new", heightmap_new[hi], "z", z, "x", x, "pos1.y", pos1.y) -- Lua doesn't have a continue statement :-/ if height_old == height_new then diff --git a/worldeditadditions_commands/commands/erode.lua b/worldeditadditions_commands/commands/erode.lua index 81b98e1..8b13003 100644 --- a/worldeditadditions_commands/commands/erode.lua +++ b/worldeditadditions_commands/commands/erode.lua @@ -37,7 +37,7 @@ worldedit.register_command("erode", { ) local time_taken = worldeditadditions.get_ms_time() - start_time - minetest.log("action", name .. " used //erode "..algorithm.." at " .. worldeditadditions.vector.tostring(worldedit.pos1[name]) .. ", replacing " .. changes.replaced .. " nodes and skipping " .. changes.skipped_columns .. " columns in " .. time_taken .. "s") - return true, changes.replaced .. " nodes replaced and " .. changes.skipped_columns .. " columns skipped in " .. worldeditadditions.human_time(time_taken) + minetest.log("action", name .. " used //erode "..algorithm.." at " .. worldeditadditions.vector.tostring(worldedit.pos1[name]) .. ", adding " .. stats.added .. " nodes and removing " .. stats.removed .. " nodes in " .. time_taken .. "s") + return true, stats.added .. " nodes added and " .. stats.removed .. " nodes removed in " .. worldeditadditions.human_time(time_taken) end }) From fd5804dd9c30fedf8757ac12185a004c5381df45 Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Fri, 21 Aug 2020 20:59:50 +0100 Subject: [PATCH 5/8] //erode: Finish the initial round of bugfixing, but I'm on the fence about it. Specifically, I'm unsure about whether I'm happy with the effects of the algorithm. Also, we convolve with a 3x3 gaussian kernel after erosion is complete - and we have verified that the erosion is having an positive effect at "roughening up" a terrain surface. It seems like the initial blog post was correct: the algorithm does tend to make steep surfaces steeper. It also appears that it's more effective on larger areas, and 'gentler' curves. THis might be because the surface normals are more conducive to making the snowballs roll. Finally, we need to decide whether we want to keep the precomputed normals as we have now, or whether we want to dynamically compute them at the some of request. --- worldeditadditions/lib/conv/convolve.lua | 12 +- worldeditadditions/lib/erode/erode.lua | 8 +- worldeditadditions/lib/erode/snowballs.lua | 106 +++++++++++++----- worldeditadditions/utils/strings.lua | 2 +- worldeditadditions/utils/tables.lua | 3 + worldeditadditions/utils/vector.lua | 5 + .../commands/erode.lua | 11 +- 7 files changed, 104 insertions(+), 43 deletions(-) diff --git a/worldeditadditions/lib/conv/convolve.lua b/worldeditadditions/lib/conv/convolve.lua index 3b1bb70..9404bf0 100644 --- a/worldeditadditions/lib/conv/convolve.lua +++ b/worldeditadditions/lib/conv/convolve.lua @@ -16,12 +16,12 @@ function worldeditadditions.conv.convolve(heightmap, heightmap_size, matrix, mat local border_size = {} border_size[0] = (matrix_size[0]-1) / 2 -- height border_size[1] = (matrix_size[1]-1) / 2 -- width - print("[convolve] matrix_size", matrix_size[0], matrix_size[1]) - print("[convolve] border_size", border_size[0], border_size[1]) - print("[convolve] heightmap_size: ", heightmap_size[0], heightmap_size[1]) - - print("[convolve] z: from", (heightmap_size[0]-border_size[0]) - 1, "to", border_size[0], "step", -1) - print("[convolve] x: from", (heightmap_size[1]-border_size[1]) - 1, "to", border_size[1], "step", -1) + -- print("[convolve] matrix_size", matrix_size[0], matrix_size[1]) + -- print("[convolve] border_size", border_size[0], border_size[1]) + -- print("[convolve] heightmap_size: ", heightmap_size[0], heightmap_size[1]) + -- + -- print("[convolve] z: from", (heightmap_size[0]-border_size[0]) - 1, "to", border_size[0], "step", -1) + -- print("[convolve] x: from", (heightmap_size[1]-border_size[1]) - 1, "to", border_size[1], "step", -1) -- Convolve over only the bit that allows us to use the full convolution matrix for z = (heightmap_size[0]-border_size[0]) - 1, border_size[0], -1 do diff --git a/worldeditadditions/lib/erode/erode.lua b/worldeditadditions/lib/erode/erode.lua index 5c49784..eb2a24c 100644 --- a/worldeditadditions/lib/erode/erode.lua +++ b/worldeditadditions/lib/erode/erode.lua @@ -13,15 +13,17 @@ function worldeditadditions.erode.run(pos1, pos2, algorithm, params) heightmap_size[0] = (pos2.z - pos1.z) + 1 heightmap_size[1] = (pos2.x - pos1.x) + 1 + local region_height = (pos2.y - pos1.y) + 1 + local heightmap = worldeditadditions.make_heightmap(pos1, pos2, manip, area, data) local heightmap_eroded = worldeditadditions.shallowcopy(heightmap) - print("[erode.run] algorithm: "..algorithm..", params:"); - print(worldeditadditions.map_stringify(params)) + -- print("[erode.run] algorithm: "..algorithm..", params:"); + -- print(worldeditadditions.map_stringify(params)) worldeditadditions.print_2d(heightmap, heightmap_size[1]) if algorithm == "snowballs" then - local success, msg = worldeditadditions.erode.snowballs(heightmap_eroded, heightmap_size, params) + local success, msg = worldeditadditions.erode.snowballs(heightmap, heightmap_eroded, heightmap_size, region_height, params) if not success then return success, msg end else return false, "Error: Unknown algorithm '"..algorithm.."'. Currently implemented algorithms: snowballs (2d; hydraulic-like). Ideas for algorithms to implement are welcome!" diff --git a/worldeditadditions/lib/erode/snowballs.lua b/worldeditadditions/lib/erode/snowballs.lua index e5b8398..7672dc5 100644 --- a/worldeditadditions/lib/erode/snowballs.lua +++ b/worldeditadditions/lib/erode/snowballs.lua @@ -1,49 +1,69 @@ --- Test command: //multi //fp set1 1312 5 5543 //fp set2 1336 18 5521 //erode//multi //fp set1 1312 5 5543 //fp set2 1336 18 5521 //erode +-- Test command: //multi //fp set1 1313 6 5540 //fp set2 1338 17 5521 //erode snowballs local function snowball(heightmap, normalmap, heightmap_size, startpos, params) local sediment = 0 local pos = { x = startpos.x, z = startpos.z } local pos_prev = { x = pos.x, z = pos.z } - local velocity = { x = 0, z = 0 } + local velocity = { + x = (math.random() * 2 - 1) * params.init_velocity, + z = (math.random() * 2 - 1) * params.init_velocity + } local heightmap_length = #heightmap - -- print("[snowball] startpos ("..pos.x..", "..pos.z..")") + -- print("[snowball] startpos ("..pos.x..", "..pos.z.."), velocity: ("..velocity.x..", "..velocity.z..")") - for i = 1, params.snowball_max_steps do + local hist_velocity = {} + + for i = 1, params.max_steps do local x = pos.x local z = pos.z local hi = math.floor(z+0.5)*heightmap_size[1] + math.floor(x+0.5) -- Stop if we go out of bounds if x < 0 or z < 0 - or x >= heightmap[1]-1 or z >= heightmap[0]-1 then - -- print("[snowball] hit edge; stopping at ("..x..", "..z.."), (bounds @ "..heightmap_size[1]..", "..heightmap_size[0]..")") - return + or x >= heightmap_size[1]-1 or z >= heightmap_size[0]-1 then + -- print("[snowball] hit edge; stopping at ("..x..", "..z.."), (bounds @ "..(heightmap_size[1]-1)..", "..(heightmap_size[0]-1)..")", "x", x, "/", heightmap_size[1]-1, "z", z, "/", heightmap_size[0]-1) + return true, i end - -- print("[snowball] now at ("..x..", "..z..") (bounds @ "..heightmap_size[1]..", "..heightmap_size[0]..")") - if hi > heightmap_length then print("[snowball] out-of-bounds on the array, hi: "..hi..", heightmap_length: "..heightmap_length) return end + if #hist_velocity > 0 and i > 5 + and worldeditadditions.average(hist_velocity) < 0.03 then + -- print("[snowball] It looks like we've stopped") + return true, i + end + + if normalmap[hi].y == 1 then return true, i end + + if hi > heightmap_length then return false, "Out-of-bounds on the array, hi: "..hi..", heightmap_length: "..heightmap_length end -- print("[snowball] sediment", sediment, "rate_deposit", params.rate_deposit, "normalmap[hi].z", normalmap[hi].z) local step_deposit = sediment * params.rate_deposit * normalmap[hi].z local step_erode = params.rate_erosion * (1 - normalmap[hi].z) * math.min(1, i*params.scale_iterations) - local step_diff = step_deposit - step_erode - -- Erode / Deposit, but only if we are on a different node than we were in the last step if math.floor(pos_prev.x) ~= math.floor(x) and math.floor(pos_prev.z) ~= math.floor(z) then - heightmap[hi] = heightmap[hi] + step_diff + heightmap[hi] = heightmap[hi] + (step_deposit - step_erode) end velocity.x = params.friction * velocity.x + normalmap[hi].x * params.speed velocity.z = params.friction * velocity.z + normalmap[hi].y * params.speed + + -- print("[snowball] now at ("..x..", "..z..") velocity "..worldeditadditions.vector.lengthsquared(velocity)..", sediment "..sediment) + local new_vel_sq = worldeditadditions.vector.lengthsquared(velocity) + if new_vel_sq > 1 then + -- print("[snowball] velocity squared over 1, normalising") + velocity = worldeditadditions.vector.normalize(velocity) + end + table.insert(hist_velocity, new_vel_sq) + if #hist_velocity > params.velocity_hist_count then table.remove(hist_velocity, 1) end pos_prev.x = x pos_prev.z = z pos.x = pos.x + velocity.x pos.z = pos.z + velocity.z - sediment = sediment + step_diff + sediment = sediment + (step_erode - step_deposit) -- Needs to be erosion - deposit, which is the opposite to the above end + return true, params.max_steps end --[[ @@ -52,26 +72,32 @@ Note that this *mutates* the given heightmap. @source https://jobtalle.com/simulating_hydraulic_erosion.html ]]-- -function worldeditadditions.erode.snowballs(heightmap, heightmap_size, params) - -- Apply the default settings - worldeditadditions.table_apply({ - rate_deposit = 0.03, - rate_erosion = 0.04, +function worldeditadditions.erode.snowballs(heightmap_initial, heightmap, heightmap_size, region_height, params_custom) + local params = { + rate_deposit = 0.03, -- 0.03 + rate_erosion = 0.04, -- 0.04 friction = 0.07, - speed = 0.15, - radius = 0.8, - snowball_max_steps = 80, + speed = 1, + max_steps = 80, + velocity_hist_count = 3, + init_velocity = 0.25, scale_iterations = 0.04, - drops_per_cell = 0.4, - snowball_count = 50000 - }, params) + maxdiff = 0.4, + count = 50000 + } + -- Apply the default settings + worldeditadditions.table_apply(params_custom, params) + + print("[erode/snowballs] params: ") + print(worldeditadditions.map_stringify(params)) - print("[erode/snowballs] params: "..worldeditadditions.map_stringify(params)) local normals = worldeditadditions.calculate_normals(heightmap, heightmap_size) - for i = 1, params.snowball_count do - snowball( + local stats_steps = {} + for i = 1, params.count do + -- print("[snowballs] starting snowball ", i) + local success, steps = snowball( heightmap, normals, heightmap_size, { x = math.random() * (heightmap_size[1] - 1), @@ -79,14 +105,38 @@ function worldeditadditions.erode.snowballs(heightmap, heightmap_size, params) }, params ) + table.insert(stats_steps, steps) + + if not success then return false, "Error: Failed at snowball "..i..":"..steps end end + print("[snowballs] "..#stats_steps.." snowballs simulated, max "..params.max_steps.." steps, averaged ~"..worldeditadditions.average(stats_steps).."") + -- Round everything to the nearest int, since you can't really have -- something like .141592671 of a node -- Note that we do this after *all* the erosion is complete + local clamp_limit = math.floor(region_height * params.maxdiff + 0.5) for i,v in ipairs(heightmap) do heightmap[i] = math.floor(heightmap[i] + 0.5) + if heightmap[i] < 0 then heightmap[i] = 0 end + -- Limit the distance to params.maxdiff% of the region height + if math.abs(heightmap_initial[i] - heightmap[i]) > region_height * params.maxdiff then + if heightmap_initial[i] - heightmap[i] > 0 then + heightmap[i] = heightmap_initial[i] - clamp_limit + else + heightmap[i] = heightmap_initial[i] + clamp_limit + end + end end - return true, params.snowball_count.." snowballs simulated" + local success, matrix = worldeditadditions.get_conv_kernel("gaussian", 3, 3) + if not success then return success, matrix end + matrix_size = {} matrix_size[0] = 3 matrix_size[1] = 3 + worldeditadditions.conv.convolve( + heightmap, heightmap_size, + matrix, + matrix_size + ) + + return true, params.count.." snowballs simulated" end diff --git a/worldeditadditions/utils/strings.lua b/worldeditadditions/utils/strings.lua index 673eef9..1b2f805 100644 --- a/worldeditadditions/utils/strings.lua +++ b/worldeditadditions/utils/strings.lua @@ -216,7 +216,7 @@ function worldeditadditions.parse_map(params_text) local last_key = nil for i, part in ipairs(parts) do - if i % 2 == 1 then + if i % 2 == 0 then -- Lua starts at 1 :-/ -- Try converting to a number to see if it works local part_converted = tonumber(part) if as_number == nil then part_converted = part end diff --git a/worldeditadditions/utils/tables.lua b/worldeditadditions/utils/tables.lua index 2c868b3..99e41ac 100644 --- a/worldeditadditions/utils/tables.lua +++ b/worldeditadditions/utils/tables.lua @@ -21,7 +21,10 @@ end -- @param source table The source to take values from -- @param target table The target to write values to function worldeditadditions.table_apply(source, target) + print("[table_apply] start") for key, value in pairs(source) do + print("[table_apply] Applying", key, "=", value) target[key] = value end + print("[table_apply] end") end diff --git a/worldeditadditions/utils/vector.lua b/worldeditadditions/utils/vector.lua index 73e86f1..b7c2357 100644 --- a/worldeditadditions/utils/vector.lua +++ b/worldeditadditions/utils/vector.lua @@ -8,6 +8,7 @@ end -- @param v Vector The vector to operate on -- @return number The length of the given vector squared function worldeditadditions.vector.lengthsquared(v) + if not v.y then return v.x*v.x + v.z*v.z end return v.x*v.x + v.y*v.y + v.z*v.z end @@ -18,6 +19,10 @@ end -- @return Vector A new normalised vector. function worldeditadditions.vector.normalize(v) local length = math.sqrt(worldeditadditions.vector.lengthsquared(v)) + if not v.y then return { + x = v.x / length, + z = v.z / length + } end return { x = v.x / length, y = v.y / length, diff --git a/worldeditadditions_commands/commands/erode.lua b/worldeditadditions_commands/commands/erode.lua index 8b13003..4d468c5 100644 --- a/worldeditadditions_commands/commands/erode.lua +++ b/worldeditadditions_commands/commands/erode.lua @@ -1,8 +1,8 @@ --- ██████ ██ ██ ███████ ██████ ██ █████ ██ ██ --- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ --- ██ ██ ██ ██ █████ ██████ ██ ███████ ████ --- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ --- ██████ ████ ███████ ██ ██ ███████ ██ ██ ██ +-- ███████ ██████ ██████ ██████ ███████ +-- ██ ██ ██ ██ ██ ██ ██ ██ +-- █████ ██████ ██ ██ ██ ██ █████ +-- ██ ██ ██ ██ ██ ██ ██ ██ +-- ███████ ██ ██ ██████ ██████ ███████ worldedit.register_command("erode", { params = "[ [ []] [ []] ...]", description = "Runs the specified erosion algorithm over the given defined region. This may occur in 2d or 3d. Currently implemented algorithms: snowballs (default;2d hydraulic-like). Also optionally takes an arbitrary set of key - value pairs representing parameters to pass to the algorithm. See the full documentation for details.", @@ -35,6 +35,7 @@ worldedit.register_command("erode", { worldedit.pos1[name], worldedit.pos2[name], algorithm, params ) + if not success then return success, stats end local time_taken = worldeditadditions.get_ms_time() - start_time minetest.log("action", name .. " used //erode "..algorithm.." at " .. worldeditadditions.vector.tostring(worldedit.pos1[name]) .. ", adding " .. stats.added .. " nodes and removing " .. stats.removed .. " nodes in " .. time_taken .. "s") From 07588e094055ddec71106d05dbc420dc13d4fcf0 Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Fri, 21 Aug 2020 21:00:45 +0100 Subject: [PATCH 6/8] erode/snowballs: Add comment --- worldeditadditions/lib/erode/snowballs.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/worldeditadditions/lib/erode/snowballs.lua b/worldeditadditions/lib/erode/snowballs.lua index 7672dc5..dd3e369 100644 --- a/worldeditadditions/lib/erode/snowballs.lua +++ b/worldeditadditions/lib/erode/snowballs.lua @@ -36,6 +36,7 @@ local function snowball(heightmap, normalmap, heightmap_size, startpos, params) if hi > heightmap_length then return false, "Out-of-bounds on the array, hi: "..hi..", heightmap_length: "..heightmap_length end + -- NOTE: We need to decide whether we want to keep the precomputed normals as we have now, or whether we want to dynamically compute them at the some of request. -- print("[snowball] sediment", sediment, "rate_deposit", params.rate_deposit, "normalmap[hi].z", normalmap[hi].z) local step_deposit = sediment * params.rate_deposit * normalmap[hi].z local step_erode = params.rate_erosion * (1 - normalmap[hi].z) * math.min(1, i*params.scale_iterations) From e8a9ba88006e4995c8ec671fbbcb4e920a389dc6 Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Fri, 21 Aug 2020 21:26:29 +0100 Subject: [PATCH 7/8] Update changelog & document //erode in the README --- CHANGELOG.md | 1 + README.md | 40 ++++++++++++++++++- .../commands/erode.lua | 2 +- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 721a74c..28ed75f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ It's about time I started a changelog! This will serve from now on as the master ## v1.9 (unreleased) - Add `//many` for executing a command many times in a row + - Add **experimental** `//erode` command ## v1.8: The Quality of Life Update (17th July 2020) diff --git a/README.md b/README.md index c547237..f6742af 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ If you can dream of it, it probably belongs here! - [`//overlay [] [] [ []] ...`](#overlay-node_name_a-chance_a-node_name_b-chance_b-node_name_n-chance_n-) - [`//layers [ []] [ []] ...`](#layers-node_name_1-layer_count_1-node_name_2-layer_count_2-) - [`//convolve [[,]] []`](#convolve-kernel-widthheight-sigma) + - [`//erode [ [ []] [ []] ...]`](#) **experimental** ### Statistics - [`//count`](#count) @@ -53,7 +54,7 @@ Floods all connected nodes of the same type starting at _pos1_ with [] [] [ []] ...` +### `//overlay [ [ []] [ []] ...]_a>] [] [ []] ...` Places `` in the last contiguous air space encountered above the first non-air node. In other words, overlays all top-most nodes in the specified area with ``. Optionally supports a mix of node names and chances, as `//mix` (WorldEdit) and `//replacemix` (WorldEditAdditions) does. Will also work in caves, as it scans columns of nodes from top to bottom, skipping every non-air node until it finds one - and only then will it start searching for a node to place the target node on top of. @@ -256,6 +257,43 @@ The sigma value is only applicable to the `gaussian` kernel, and can be thought //convolve gaussian 5 0.2 ``` +## `//erode [ [ []] [ []] ...]` +Runs an erosion algorithm over the defined region, optionally passing a number of key - value pairs representing parameters that are passed to the chosen algorithm. This command is **experimental**, as the author is currently on-the-fence about the effects it produces. + +Currently implemented algorithms: + +Algorithm | Mode | Description +------------|-------|------------------- +`snowballs` | 2D | The default. Simulates snowballs rolling across the terrain, eroding & depositing material. Then runs a 3x3 gaussian kernel over the result (i.e. like the `//conv` / `//smoothadv` command). + +Usage examples: + +``` +//erode +//erode snowballs +//erode snowballs count 25000 +``` + +Each of the algorithms above have 1 or more parameters that they support. These are detailed below. + +### Parameters: snowballs + +Parameter | Type | Default Value | Description +--------------------|-----------|-------------------|------------------------ +rate_deposit | `float` | 0.03 | The rate at which snowballs will deposit material +rate_erosion | `float` | 0.04 | The rate at which snowballs will erode material +friction | `float` | 0.07 | More friction slows snowballs down more. +speed | `float` | 1 | Speed multiplier to apply to snowballs at each step. +max_steps | `float` | 80 | The maximum number of steps to simulate each snowball for. +velocity_hist_count | `float` | 3 | The number of previous history values to average when detecting whether a snowball has stopped or not +init_velocity | `float` | 0.25 | The maximum random initial velocity of a snowball for each component of the velocity vector. +scale_iterations | `float` | 0.04 | How much to scale erosion by as time goes on. Higher values mean that any given snowball will erode more later on as more steps pass. +maxdiff | `float` | 0.4 | The maximum difference in height (between 0 and 1) that is acceptable as a percentage of the defined region's height. +count | `float` | 50000 | The number of snowballs to simulate. + +If you find any good combinations of these parameters, please [open an issue](https://github.com/sbrl/Minetest-WorldEditAdditions/issues/new) (or a PR!) and let me know! I'll include good combinations here. + + ### `//count` Counts all the nodes in the defined region and returns the result along with calculated percentages (note that if the chat window used a monospace font, the returned result would be a perfect table. If someone has a ~~hack~~ solution to make the columns line up neatly, please [open an issue](https://github.com/sbrl/Minetest-WorldEditAdditions/issues/new) :D) diff --git a/worldeditadditions_commands/commands/erode.lua b/worldeditadditions_commands/commands/erode.lua index 4d468c5..927bd38 100644 --- a/worldeditadditions_commands/commands/erode.lua +++ b/worldeditadditions_commands/commands/erode.lua @@ -5,7 +5,7 @@ -- ███████ ██ ██ ██████ ██████ ███████ worldedit.register_command("erode", { params = "[ [ []] [ []] ...]", - description = "Runs the specified erosion algorithm over the given defined region. This may occur in 2d or 3d. Currently implemented algorithms: snowballs (default;2d hydraulic-like). Also optionally takes an arbitrary set of key - value pairs representing parameters to pass to the algorithm. See the full documentation for details.", + description = "**experimental** Runs the specified erosion algorithm over the given defined region. This may occur in 2d or 3d. Currently implemented algorithms: snowballs (default;2d hydraulic-like). Also optionally takes an arbitrary set of key - value pairs representing parameters to pass to the algorithm. See the full documentation for details.", privs = { worldedit = true }, require_pos = 2, parse = function(params_text) From 6846369cf9f713b390f46aeb976e84d5f161e20e Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Fri, 21 Aug 2020 21:28:14 +0100 Subject: [PATCH 8/8] typo & insert link into quick reference --- README.md | 4 ++-- worldeditadditions_commands/commands/erode.lua | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f6742af..887d07c 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ If you can dream of it, it probably belongs here! - [`//overlay [] [] [ []] ...`](#overlay-node_name_a-chance_a-node_name_b-chance_b-node_name_n-chance_n-) - [`//layers [ []] [ []] ...`](#layers-node_name_1-layer_count_1-node_name_2-layer_count_2-) - [`//convolve [[,]] []`](#convolve-kernel-widthheight-sigma) - - [`//erode [ [ []] [ []] ...]`](#) **experimental** + - [`//erode [ [ []] [ []] ...]`](#erode-snowballs-key_1-value_1-key_2-value_2-) **experimental** ### Statistics - [`//count`](#count) @@ -257,7 +257,7 @@ The sigma value is only applicable to the `gaussian` kernel, and can be thought //convolve gaussian 5 0.2 ``` -## `//erode [ [ []] [ []] ...]` +## `//erode [ [ []] [ []] ...]` Runs an erosion algorithm over the defined region, optionally passing a number of key - value pairs representing parameters that are passed to the chosen algorithm. This command is **experimental**, as the author is currently on-the-fence about the effects it produces. Currently implemented algorithms: diff --git a/worldeditadditions_commands/commands/erode.lua b/worldeditadditions_commands/commands/erode.lua index 927bd38..e5c4ad7 100644 --- a/worldeditadditions_commands/commands/erode.lua +++ b/worldeditadditions_commands/commands/erode.lua @@ -4,7 +4,7 @@ -- ██ ██ ██ ██ ██ ██ ██ ██ -- ███████ ██ ██ ██████ ██████ ███████ worldedit.register_command("erode", { - params = "[ [ []] [ []] ...]", + params = "[ [ []] [ []] ...]", description = "**experimental** Runs the specified erosion algorithm over the given defined region. This may occur in 2d or 3d. Currently implemented algorithms: snowballs (default;2d hydraulic-like). Also optionally takes an arbitrary set of key - value pairs representing parameters to pass to the algorithm. See the full documentation for details.", privs = { worldedit = true }, require_pos = 2,